#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 Messages { get; } = new(); public List Subscribers { get; } = new(); public List Enums { get; } = new(); public List 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 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( 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 inputTypes) { var types = inputTypes .Where(type => type != null && !type.ContainsGenericParameters) .Distinct() .OrderBy(type => type.FullName, StringComparer.Ordinal) .ToArray(); var availableTypes = new HashSet(types); var result = new ShrinkNetworkSemanticScanResult(); var messageTypes = new HashSet(); 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 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 messageTypes, ISet availableTypes) { var queue = new Queue(messageTypes .SelectMany(GetSerializableProperties) .Select(property => property.PropertyType)); var visited = new HashSet(); var dependencyTypes = new HashSet(); 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 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 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 GetLoadableTypes(System.Reflection.Assembly assembly) { try { return assembly.GetTypes(); } catch (ReflectionTypeLoadException exception) { return exception.Types.OfType(); } } } #endif