418 lines
17 KiB
C#
418 lines
17 KiB
C#
#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
|