This commit is contained in:
@@ -0,0 +1,417 @@
|
||||
#if UNITY_EDITOR
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEditor.Compilation;
|
||||
|
||||
internal sealed class ShrinkNetworkSemanticScanResult
|
||||
{
|
||||
public List<ShrinkDedicatedServerScaffoldGenerator.MessageSpec> Messages { get; } = new();
|
||||
public List<ShrinkDedicatedServerScaffoldGenerator.SubscriberSpec> Subscribers { get; } = new();
|
||||
public List<ShrinkDedicatedServerScaffoldGenerator.EnumSpec> Enums { get; } = new();
|
||||
public List<ShrinkDedicatedServerScaffoldGenerator.DataTypeSpec> DataTypes { get; } = new();
|
||||
}
|
||||
|
||||
internal static class ShrinkNetworkSemanticScanner
|
||||
{
|
||||
private const string MessageAttributeName = "ShrinkNetwork.ShrinkNetworkMessageAttribute";
|
||||
private const string StateSyncAttributeName = "ShrinkNetwork.ShrinkNetworkStateSyncAttribute";
|
||||
private const string SubscribeAttributeName = "ShrinkNetwork.ShrinkNetworkSubscribeAttribute";
|
||||
private const string MessageInterfaceName = "ShrinkNetwork.IShrinkNetworkMessage";
|
||||
private const string RequestInterfaceName = "ShrinkNetwork.IShrinkNetworkRequest";
|
||||
private const string ResponseBaseName = "ShrinkNetwork.ShrinkRpcResponseBase";
|
||||
private const string ResultEventInterfaceName = "ShrinkEventBus.IShrinkResultEvent`1";
|
||||
private const string NetworkEventAttributeName = "ShrinkNetwork.Integration.EventBus.ShrinkNetworkEventAttribute";
|
||||
private const string DeltaEventInterfaceName = "ShrinkNetwork.Integration.EventBus.IShrinkNetworkDeltaEvent";
|
||||
|
||||
private static readonly Dictionary<Type, string> TypeAliases = new()
|
||||
{
|
||||
[typeof(void)] = "void",
|
||||
[typeof(bool)] = "bool",
|
||||
[typeof(byte)] = "byte",
|
||||
[typeof(sbyte)] = "sbyte",
|
||||
[typeof(short)] = "short",
|
||||
[typeof(ushort)] = "ushort",
|
||||
[typeof(int)] = "int",
|
||||
[typeof(uint)] = "uint",
|
||||
[typeof(long)] = "long",
|
||||
[typeof(ulong)] = "ulong",
|
||||
[typeof(float)] = "float",
|
||||
[typeof(double)] = "double",
|
||||
[typeof(decimal)] = "decimal",
|
||||
[typeof(char)] = "char",
|
||||
[typeof(string)] = "string",
|
||||
[typeof(object)] = "object"
|
||||
};
|
||||
|
||||
internal static ShrinkNetworkSemanticScanResult ScanCompiledPlayerAssemblies()
|
||||
{
|
||||
var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Where(assembly => !assembly.IsDynamic)
|
||||
.GroupBy(assembly => assembly.GetName().Name ?? string.Empty, StringComparer.Ordinal)
|
||||
.ToDictionary(group => group.Key, group => group.First(), StringComparer.Ordinal);
|
||||
var playerAssemblyNames = new HashSet<string>(
|
||||
CompilationPipeline.GetAssemblies(AssembliesType.Player).Select(assembly => assembly.name),
|
||||
StringComparer.Ordinal);
|
||||
var types = playerAssemblyNames
|
||||
.Where(loadedAssemblies.ContainsKey)
|
||||
.SelectMany(name => GetLoadableTypes(loadedAssemblies[name]))
|
||||
.Where(type => type != null)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
return ScanTypes(types!);
|
||||
}
|
||||
|
||||
internal static ShrinkNetworkSemanticScanResult ScanTypesForTests(params Type[] types)
|
||||
{
|
||||
return ScanTypes(types);
|
||||
}
|
||||
|
||||
internal static string FormatTypeForTests(Type type)
|
||||
{
|
||||
return FormatType(type);
|
||||
}
|
||||
|
||||
private static ShrinkNetworkSemanticScanResult ScanTypes(IEnumerable<Type> inputTypes)
|
||||
{
|
||||
var types = inputTypes
|
||||
.Where(type => type != null && !type.ContainsGenericParameters)
|
||||
.Distinct()
|
||||
.OrderBy(type => type.FullName, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
var availableTypes = new HashSet<Type>(types);
|
||||
var result = new ShrinkNetworkSemanticScanResult();
|
||||
var messageTypes = new HashSet<Type>();
|
||||
|
||||
foreach (var type in types)
|
||||
{
|
||||
var messageAttribute = FindAttribute(type.CustomAttributes, MessageAttributeName);
|
||||
if (messageAttribute == null || !IsNetworkContract(type))
|
||||
continue;
|
||||
|
||||
result.Messages.Add(BuildMessageSpec(type, messageAttribute));
|
||||
messageTypes.Add(type);
|
||||
}
|
||||
|
||||
ThrowOnPortableNameCollision(
|
||||
result.Messages.Select(message => (message.TypeName, message.SourcePath)),
|
||||
"网络消息");
|
||||
|
||||
foreach (var type in types)
|
||||
AddSubscriberSpecs(type, result.Subscribers);
|
||||
|
||||
AddPortableDependencySpecs(result, messageTypes, availableTypes);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ShrinkDedicatedServerScaffoldGenerator.MessageSpec BuildMessageSpec(
|
||||
Type type,
|
||||
CustomAttributeData messageAttribute)
|
||||
{
|
||||
if (messageAttribute.ConstructorArguments.Count == 0)
|
||||
throw new InvalidOperationException($"{type.FullName} 的 ShrinkNetworkMessage 缺少 opcode。");
|
||||
|
||||
var opcode = Convert.ToInt32(messageAttribute.ConstructorArguments[0].Value, CultureInfo.InvariantCulture);
|
||||
var route = messageAttribute.ConstructorArguments.Count > 1
|
||||
? messageAttribute.ConstructorArguments[1].Value as string ?? string.Empty
|
||||
: string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(route))
|
||||
throw new InvalidOperationException($"{type.FullName} 的 ShrinkNetworkMessage 必须声明非空 route,服务器合同无法依赖运行时回退值。");
|
||||
|
||||
var stateSyncAttributes = type.CustomAttributes
|
||||
.Where(attribute => IsAttribute(attribute, StateSyncAttributeName))
|
||||
.ToArray();
|
||||
if (stateSyncAttributes.Length > 1)
|
||||
throw new InvalidOperationException($"{type.FullName} 声明了多个 ShrinkNetworkStateSync,服务器脚手架要求每个消息只有一个同步角色。");
|
||||
|
||||
var stateSync = stateSyncAttributes.FirstOrDefault();
|
||||
var spec = new ShrinkDedicatedServerScaffoldGenerator.MessageSpec
|
||||
{
|
||||
TypeName = type.Name,
|
||||
Kind = GetMessageKind(type),
|
||||
Opcode = opcode,
|
||||
Route = route.Trim(),
|
||||
SourcePath = GetSourceName(type),
|
||||
HasResult = ImplementsOpenGeneric(type, ResultEventInterfaceName),
|
||||
IsNetworkEvent = HasAttribute(type, NetworkEventAttributeName),
|
||||
IsDeltaEvent = Implements(type, DeltaEventInterfaceName),
|
||||
SyncGroup = GetConstructorString(stateSync, 0),
|
||||
SyncRole = GetConstructorEnumName(stateSync, 1)
|
||||
};
|
||||
spec.Properties.AddRange(GetSerializableProperties(type)
|
||||
.Select(property => (FormatType(property.PropertyType), property.Name)));
|
||||
return spec;
|
||||
}
|
||||
|
||||
private static void AddSubscriberSpecs(
|
||||
Type type,
|
||||
ICollection<ShrinkDedicatedServerScaffoldGenerator.SubscriberSpec> target)
|
||||
{
|
||||
const BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic |
|
||||
BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
|
||||
foreach (var method in type.GetMethods(flags).OrderBy(method => method.MetadataToken))
|
||||
{
|
||||
foreach (var attribute in method.CustomAttributes.Where(item => IsAttribute(item, SubscribeAttributeName)))
|
||||
{
|
||||
target.Add(new ShrinkDedicatedServerScaffoldGenerator.SubscriberSpec
|
||||
{
|
||||
MemberName = type.Name + "." + method.Name,
|
||||
SourcePath = GetSourceName(type),
|
||||
Authority = GetNamedEnumName(attribute, "Authority"),
|
||||
Permission = GetNamedString(attribute, "Permission")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddPortableDependencySpecs(
|
||||
ShrinkNetworkSemanticScanResult result,
|
||||
ISet<Type> messageTypes,
|
||||
ISet<Type> availableTypes)
|
||||
{
|
||||
var queue = new Queue<Type>(messageTypes
|
||||
.SelectMany(GetSerializableProperties)
|
||||
.Select(property => property.PropertyType));
|
||||
var visited = new HashSet<Type>();
|
||||
var dependencyTypes = new HashSet<Type>();
|
||||
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var dependency = UnwrapType(queue.Dequeue());
|
||||
if (dependency == null || !visited.Add(dependency))
|
||||
continue;
|
||||
|
||||
if (dependency.IsGenericType)
|
||||
{
|
||||
foreach (var argument in dependency.GetGenericArguments())
|
||||
queue.Enqueue(argument);
|
||||
}
|
||||
|
||||
if (!availableTypes.Contains(dependency) || messageTypes.Contains(dependency) || IsFrameworkType(dependency))
|
||||
continue;
|
||||
|
||||
dependencyTypes.Add(dependency);
|
||||
if (!dependency.IsEnum)
|
||||
{
|
||||
foreach (var property in GetSerializableProperties(dependency))
|
||||
queue.Enqueue(property.PropertyType);
|
||||
}
|
||||
}
|
||||
|
||||
ThrowOnPortableNameCollision(
|
||||
dependencyTypes.Select(type => (type.Name, GetSourceName(type))),
|
||||
"消息依赖类型");
|
||||
|
||||
foreach (var type in dependencyTypes.OrderBy(type => type.Name, StringComparer.Ordinal))
|
||||
{
|
||||
if (type.IsEnum)
|
||||
result.Enums.Add(BuildEnumSpec(type));
|
||||
else
|
||||
result.DataTypes.Add(BuildDataTypeSpec(type));
|
||||
}
|
||||
}
|
||||
|
||||
private static ShrinkDedicatedServerScaffoldGenerator.EnumSpec BuildEnumSpec(Type type)
|
||||
{
|
||||
var spec = new ShrinkDedicatedServerScaffoldGenerator.EnumSpec
|
||||
{
|
||||
Name = type.Name,
|
||||
SourcePath = GetSourceName(type)
|
||||
};
|
||||
var underlyingType = Enum.GetUnderlyingType(type);
|
||||
foreach (var name in Enum.GetNames(type))
|
||||
{
|
||||
var rawValue = Enum.Parse(type, name);
|
||||
var value = underlyingType == typeof(ulong) || underlyingType == typeof(uint) || underlyingType == typeof(ushort) || underlyingType == typeof(byte)
|
||||
? Convert.ToUInt64(rawValue, CultureInfo.InvariantCulture).ToString(CultureInfo.InvariantCulture)
|
||||
: Convert.ToInt64(rawValue, CultureInfo.InvariantCulture).ToString(CultureInfo.InvariantCulture);
|
||||
spec.Members.Add((name, value));
|
||||
}
|
||||
return spec;
|
||||
}
|
||||
|
||||
private static ShrinkDedicatedServerScaffoldGenerator.DataTypeSpec BuildDataTypeSpec(Type type)
|
||||
{
|
||||
var spec = new ShrinkDedicatedServerScaffoldGenerator.DataTypeSpec
|
||||
{
|
||||
Name = type.Name,
|
||||
Kind = type.IsValueType ? "struct" : "class",
|
||||
SourcePath = GetSourceName(type)
|
||||
};
|
||||
spec.Properties.AddRange(GetSerializableProperties(type)
|
||||
.Select(property => (FormatType(property.PropertyType), property.Name)));
|
||||
return spec;
|
||||
}
|
||||
|
||||
private static IEnumerable<PropertyInfo> GetSerializableProperties(Type type)
|
||||
{
|
||||
const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly;
|
||||
return type.GetProperties(flags)
|
||||
.Where(property => property.GetIndexParameters().Length == 0 &&
|
||||
property.GetMethod?.IsPublic == true &&
|
||||
property.SetMethod?.IsPublic == true)
|
||||
.OrderBy(property => property.MetadataToken);
|
||||
}
|
||||
|
||||
private static string FormatType(Type type)
|
||||
{
|
||||
if (TypeAliases.TryGetValue(type, out var alias))
|
||||
return alias;
|
||||
if (type.IsArray)
|
||||
return FormatType(type.GetElementType()!) + "[" + new string(',', type.GetArrayRank() - 1) + "]";
|
||||
if (type.IsGenericParameter)
|
||||
return type.Name;
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
|
||||
return FormatType(type.GetGenericArguments()[0]) + "?";
|
||||
if (type.IsGenericType)
|
||||
{
|
||||
var name = type.Name;
|
||||
var backtickIndex = name.IndexOf('`');
|
||||
if (backtickIndex >= 0)
|
||||
name = name[..backtickIndex];
|
||||
return name + "<" + string.Join(", ", type.GetGenericArguments().Select(FormatType)) + ">";
|
||||
}
|
||||
return type.Name;
|
||||
}
|
||||
|
||||
private static Type? UnwrapType(Type type)
|
||||
{
|
||||
while (type.IsArray || type.IsByRef || type.IsPointer)
|
||||
type = type.GetElementType()!;
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
|
||||
return type.GetGenericArguments()[0];
|
||||
return type;
|
||||
}
|
||||
|
||||
private static bool IsFrameworkType(Type type)
|
||||
{
|
||||
var namespaceName = type.Namespace ?? string.Empty;
|
||||
return type.Assembly == typeof(string).Assembly ||
|
||||
namespaceName.StartsWith("System", StringComparison.Ordinal) ||
|
||||
namespaceName.StartsWith("Unity", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool IsNetworkContract(Type type)
|
||||
{
|
||||
return Implements(type, MessageInterfaceName) || Implements(type, RequestInterfaceName) || Inherits(type, ResponseBaseName);
|
||||
}
|
||||
|
||||
private static string GetMessageKind(Type type)
|
||||
{
|
||||
if (Implements(type, RequestInterfaceName))
|
||||
return "request";
|
||||
if (Inherits(type, ResponseBaseName))
|
||||
return "response";
|
||||
return "message";
|
||||
}
|
||||
|
||||
private static bool Implements(Type type, string interfaceFullName)
|
||||
{
|
||||
return type.GetInterfaces().Any(item => string.Equals(item.FullName, interfaceFullName, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
private static bool ImplementsOpenGeneric(Type type, string interfaceFullName)
|
||||
{
|
||||
return type.GetInterfaces().Any(item =>
|
||||
item.IsGenericType &&
|
||||
string.Equals(item.GetGenericTypeDefinition().FullName, interfaceFullName,
|
||||
StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
private static bool Inherits(Type type, string baseTypeFullName)
|
||||
{
|
||||
for (var current = type.BaseType; current != null; current = current.BaseType)
|
||||
{
|
||||
if (string.Equals(current.FullName, baseTypeFullName, StringComparison.Ordinal))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool HasAttribute(MemberInfo member, string attributeFullName)
|
||||
{
|
||||
return FindAttribute(member.CustomAttributes, attributeFullName) != null;
|
||||
}
|
||||
|
||||
private static CustomAttributeData? FindAttribute(IEnumerable<CustomAttributeData> attributes, string attributeFullName)
|
||||
{
|
||||
return attributes.FirstOrDefault(attribute => IsAttribute(attribute, attributeFullName));
|
||||
}
|
||||
|
||||
private static bool IsAttribute(CustomAttributeData attribute, string attributeFullName)
|
||||
{
|
||||
return string.Equals(attribute.AttributeType.FullName, attributeFullName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string GetConstructorString(CustomAttributeData? attribute, int index)
|
||||
{
|
||||
return attribute != null && attribute.ConstructorArguments.Count > index
|
||||
? attribute.ConstructorArguments[index].Value as string ?? string.Empty
|
||||
: string.Empty;
|
||||
}
|
||||
|
||||
private static string GetConstructorEnumName(CustomAttributeData? attribute, int index)
|
||||
{
|
||||
if (attribute == null || attribute.ConstructorArguments.Count <= index)
|
||||
return string.Empty;
|
||||
return GetEnumName(attribute.ConstructorArguments[index]);
|
||||
}
|
||||
|
||||
private static string GetNamedEnumName(CustomAttributeData attribute, string memberName)
|
||||
{
|
||||
var argument = attribute.NamedArguments.FirstOrDefault(item => string.Equals(item.MemberName, memberName, StringComparison.Ordinal));
|
||||
return argument.MemberName == null ? string.Empty : GetEnumName(argument.TypedValue);
|
||||
}
|
||||
|
||||
private static string GetNamedString(CustomAttributeData attribute, string memberName)
|
||||
{
|
||||
var argument = attribute.NamedArguments.FirstOrDefault(item => string.Equals(item.MemberName, memberName, StringComparison.Ordinal));
|
||||
return argument.MemberName == null ? string.Empty : argument.TypedValue.Value as string ?? string.Empty;
|
||||
}
|
||||
|
||||
private static string GetEnumName(CustomAttributeTypedArgument argument)
|
||||
{
|
||||
if (!argument.ArgumentType.IsEnum || argument.Value == null)
|
||||
return argument.Value?.ToString() ?? string.Empty;
|
||||
return Enum.GetName(argument.ArgumentType, argument.Value) ?? argument.Value.ToString() ?? string.Empty;
|
||||
}
|
||||
|
||||
private static void ThrowOnPortableNameCollision(IEnumerable<(string Name, string Source)> items, string category)
|
||||
{
|
||||
var collisions = items
|
||||
.GroupBy(item => item.Name, StringComparer.Ordinal)
|
||||
.Where(group => group.Select(item => item.Source).Distinct(StringComparer.Ordinal).Skip(1).Any())
|
||||
.OrderBy(group => group.Key, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
if (collisions.Length == 0)
|
||||
return;
|
||||
|
||||
var details = collisions.Select(group =>
|
||||
group.Key + ": " + string.Join(", ", group.Select(item => item.Source).Distinct(StringComparer.Ordinal).OrderBy(value => value, StringComparer.Ordinal)));
|
||||
throw new InvalidOperationException(
|
||||
$"{category}存在命名空间不同但简单类型名相同的类型。独立服务器合同会去掉命名空间,无法安全生成:" +
|
||||
Environment.NewLine + string.Join(Environment.NewLine, details));
|
||||
}
|
||||
|
||||
private static string GetSourceName(Type type)
|
||||
{
|
||||
return (type.Assembly.GetName().Name ?? "unknown") + "::" + (type.FullName ?? type.Name);
|
||||
}
|
||||
|
||||
private static IEnumerable<Type> GetLoadableTypes(System.Reflection.Assembly assembly)
|
||||
{
|
||||
try
|
||||
{
|
||||
return assembly.GetTypes();
|
||||
}
|
||||
catch (ReflectionTypeLoadException exception)
|
||||
{
|
||||
return exception.Types.OfType<Type>();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user