Files
ShrinkNetwork/Editor/Scaffolding/ShrinkNetworkSemanticScanner.cs
T
cneicy 8eaaa3040a
Publish UPM package / publish (push) Failing after 1s
chore: initialize standalone UPM package
2026-08-26 02:50:34 +08:00

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