feat(packages): 内置 SDK 包并完善 ContextLoader 集成
- 将 ShrinkEventBus、ShrinkDataSaver 及其 EventBus 集成从 gitlink 转为仓库直接维护的完整 UPM 包,补齐运行时、编辑器工具、测试与文档 - 新增 Command 和 Network 的 App 集成组件,支持 ContextLoader 服务发布、可逆注销及 Network Loopback 生命周期管理 - 更新 Starter 与演示组合逻辑,缺失模块时可注册、已有兼容安装器时可覆盖,并补充宿主启动断言 - 升级内部包依赖与 Shared CodeGen 包定义,放宽 Integration.App 包的 Git 忽略规则 - 将独立服务器生成器改为基于已编译程序集的语义扫描,支持 partial、复杂泛型、命名冲突检测及模板 SHA-256 覆写保护 - 新增 Network 语义扫描、模板保护和 App 组件生命周期测试 - 新增真实 UPM 消费工程验证脚本,校验内部版本一致性、程序集加载及 EditMode 测试 - 重构当前架构文档并归档已完成的 Cordis 迁移与旧代码地图
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("ShrinkNetwork.Editor.Tests")]
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 360b0480ff82f4d4bbb7cecae33717a3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+153
-252
@@ -4,8 +4,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -19,7 +19,7 @@ public static class ShrinkDedicatedServerScaffoldGenerator
|
||||
private const string TemplateStampFileName = ".shrink-server-template.json";
|
||||
private const string TemplateManifestFileName = ".shrink-server-template-files.txt";
|
||||
|
||||
private sealed class MessageSpec
|
||||
internal sealed class MessageSpec
|
||||
{
|
||||
public string TypeName = string.Empty;
|
||||
public string Kind = "message";
|
||||
@@ -41,7 +41,7 @@ public static class ShrinkDedicatedServerScaffoldGenerator
|
||||
public string SuggestedAction = string.Empty;
|
||||
}
|
||||
|
||||
private sealed class SubscriberSpec
|
||||
internal sealed class SubscriberSpec
|
||||
{
|
||||
public string MemberName = string.Empty;
|
||||
public string SourcePath = string.Empty;
|
||||
@@ -49,14 +49,14 @@ public static class ShrinkDedicatedServerScaffoldGenerator
|
||||
public string Permission = string.Empty;
|
||||
}
|
||||
|
||||
private sealed class EnumSpec
|
||||
internal sealed class EnumSpec
|
||||
{
|
||||
public string Name = string.Empty;
|
||||
public string SourcePath = string.Empty;
|
||||
public List<(string Name, string? Value)> Members = new();
|
||||
}
|
||||
|
||||
private sealed class DataTypeSpec
|
||||
internal sealed class DataTypeSpec
|
||||
{
|
||||
public string Name = string.Empty;
|
||||
public string Kind = "class";
|
||||
@@ -78,50 +78,6 @@ public static class ShrinkDedicatedServerScaffoldGenerator
|
||||
public string SnapshotElementType = string.Empty;
|
||||
}
|
||||
|
||||
private static readonly Regex MessageHeaderRegex = new(
|
||||
@"\[ShrinkNetworkMessage\((?<opcode>-?\d+)\s*,\s*""(?<route>[^""]+)""\)\][\s\S]*?(?<decl>public\s+(?:sealed\s+|partial\s+)?class\s+(?<name>\w+)\s*:\s*(?<bases>[^{\r\n]+))",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex PropertyRegex = new(
|
||||
@"public\s+(?<type>[\w<>\[\]\?,\.]+)\s+(?<name>\w+)\s*\{\s*get;\s*set;\s*\}",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex NetworkTypeRegex = new(
|
||||
@"public\s+(?:sealed\s+|partial\s+)?class\s+(?<name>\w+)\s*:\s*(?<bases>[^{\r\n]+)",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex RegisteredMessageRegex = new(
|
||||
@"RegisterMessage<(?<name>\w+)>\((?<opcode>-?\d+)\s*,\s*""(?<route>[^""]+)""\)",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex SubscriberRegex = new(
|
||||
@"\[ShrinkNetworkSubscribe\((?<args>.*?)\)\][\s\S]*?(?:UniTask<.*?>|UniTask|void|Task<.*?>|Task)\s+(?<name>\w+)\s*\(",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex StateSyncAttributeRegex = new(
|
||||
@"\[ShrinkNetworkStateSync\(\s*""(?<group>[^""]+)""\s*,\s*ShrinkNetworkStateSyncRole\.(?<role>\w+)\s*\)\]",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex EnumHeaderRegex = new(
|
||||
@"public\s+enum\s+(?<name>\w+)\s*",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex EnumMemberRegex = new(
|
||||
@"(?m)^\s*(?<name>[A-Za-z_]\w*)(?:\s*=\s*(?<value>[^,\r\n/]+))?\s*,?\s*(?://.*)?$",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex DataTypeHeaderRegex = new(
|
||||
@"public\s+(?:sealed\s+|partial\s+|readonly\s+)?(?<kind>class|struct)\s+(?<name>\w+)\s*(?::[^{\r\n]+)?",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex AuthorityRegex = new(
|
||||
@"Authority\s*=\s*ShrinkNetworkAuthority\.(?<value>\w+)",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex PermissionRegex = new(
|
||||
@"Permission\s*=\s*""(?<value>[^""]+)""",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private static readonly HashSet<string> KnownTypeTokens = new(StringComparer.Ordinal)
|
||||
{
|
||||
"bool", "byte", "sbyte", "short", "ushort", "int", "uint", "long", "ulong",
|
||||
@@ -209,7 +165,7 @@ public static class ShrinkDedicatedServerScaffoldGenerator
|
||||
return (projectRoot, assetsPath, templateRoot, serverProjectRoot, generatedOutputRoot);
|
||||
}
|
||||
|
||||
private static void CopyTemplateProject(string templateRoot, string outputRoot)
|
||||
internal static void CopyTemplateProject(string templateRoot, string outputRoot)
|
||||
{
|
||||
if (!Directory.Exists(templateRoot))
|
||||
throw new DirectoryNotFoundException($"未找到插件内服务器模板:{templateRoot}");
|
||||
@@ -222,41 +178,95 @@ public static class ShrinkDedicatedServerScaffoldGenerator
|
||||
$"目标目录已存在且不是 ShrinkNetwork 自动生成的模板工程,为避免覆盖人工修改,已中止:{outputRoot}");
|
||||
}
|
||||
|
||||
var newManagedFiles = new List<string>();
|
||||
var manifestPath = Path.Combine(outputRoot, TemplateManifestFileName);
|
||||
var previousManagedFiles = ReadTemplateManifest(manifestPath);
|
||||
var templates = new List<(string RelativePath, string OutputPath, byte[] Content)>();
|
||||
foreach (var templateFile in Directory.EnumerateFiles(templateRoot, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
var relativePath = Path.GetRelativePath(templateRoot, templateFile);
|
||||
var outputRelativePath = relativePath.EndsWith(".txt", StringComparison.Ordinal)
|
||||
? relativePath[..^".txt".Length]
|
||||
: relativePath;
|
||||
newManagedFiles.Add(outputRelativePath.Replace('\\', '/'));
|
||||
var outputFile = Path.Combine(outputRoot, outputRelativePath);
|
||||
var outputDir = Path.GetDirectoryName(outputFile);
|
||||
if (!string.IsNullOrWhiteSpace(outputDir))
|
||||
Directory.CreateDirectory(outputDir);
|
||||
|
||||
var content = File.ReadAllText(templateFile, Encoding.UTF8);
|
||||
File.WriteAllText(outputFile, content, new UTF8Encoding(false));
|
||||
var normalizedContent = new UTF8Encoding(false).GetBytes(content);
|
||||
var normalizedRelativePath = outputRelativePath.Replace('\\', '/');
|
||||
templates.Add((
|
||||
normalizedRelativePath,
|
||||
ResolveManagedOutputPath(outputRoot, normalizedRelativePath),
|
||||
normalizedContent));
|
||||
}
|
||||
|
||||
var manifestPath = Path.Combine(outputRoot, TemplateManifestFileName);
|
||||
if (File.Exists(manifestPath))
|
||||
var newManagedPaths = new HashSet<string>(templates.Select(item => item.RelativePath), StringComparer.Ordinal);
|
||||
var conflicts = new List<string>();
|
||||
foreach (var template in templates)
|
||||
{
|
||||
var previousManagedFiles = File.ReadAllLines(manifestPath, Encoding.UTF8)
|
||||
.Where(line => !string.IsNullOrWhiteSpace(line))
|
||||
.ToArray();
|
||||
foreach (var previousManagedFile in previousManagedFiles)
|
||||
{
|
||||
if (newManagedFiles.Contains(previousManagedFile, StringComparer.Ordinal))
|
||||
continue;
|
||||
if (!File.Exists(template.OutputPath))
|
||||
continue;
|
||||
|
||||
var staleFile = Path.Combine(outputRoot, previousManagedFile.Replace('/', Path.DirectorySeparatorChar));
|
||||
if (File.Exists(staleFile))
|
||||
File.Delete(staleFile);
|
||||
var currentHash = ComputeSha256(File.ReadAllBytes(template.OutputPath));
|
||||
if (previousManagedFiles.TryGetValue(template.RelativePath, out var previousHash))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(previousHash))
|
||||
{
|
||||
if (!string.Equals(currentHash, previousHash, StringComparison.OrdinalIgnoreCase))
|
||||
conflicts.Add(template.RelativePath);
|
||||
}
|
||||
else if (!string.Equals(currentHash, ComputeSha256(template.Content), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
conflicts.Add(template.RelativePath + "(旧清单无法证明文件未被修改)");
|
||||
}
|
||||
}
|
||||
else if (!string.Equals(currentHash, ComputeSha256(template.Content), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
conflicts.Add(template.RelativePath + "(不在生成清单中)");
|
||||
}
|
||||
}
|
||||
|
||||
File.WriteAllLines(manifestPath, newManagedFiles.OrderBy(item => item, StringComparer.Ordinal), new UTF8Encoding(false));
|
||||
foreach (var previousManagedFile in previousManagedFiles)
|
||||
{
|
||||
if (newManagedPaths.Contains(previousManagedFile.Key))
|
||||
continue;
|
||||
|
||||
var stalePath = ResolveManagedOutputPath(outputRoot, previousManagedFile.Key);
|
||||
if (!File.Exists(stalePath))
|
||||
continue;
|
||||
|
||||
var currentHash = ComputeSha256(File.ReadAllBytes(stalePath));
|
||||
if (string.IsNullOrWhiteSpace(previousManagedFile.Value) ||
|
||||
!string.Equals(currentHash, previousManagedFile.Value, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
conflicts.Add(previousManagedFile.Key + "(待删除的旧模板文件已被修改)");
|
||||
}
|
||||
}
|
||||
|
||||
if (conflicts.Count > 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"检测到人工修改的模板托管文件。生成器未写入任何文件,请先保留或迁移这些修改:" +
|
||||
Environment.NewLine + string.Join(Environment.NewLine, conflicts.OrderBy(item => item, StringComparer.Ordinal)));
|
||||
}
|
||||
|
||||
foreach (var template in templates)
|
||||
{
|
||||
var outputDirectory = Path.GetDirectoryName(template.OutputPath);
|
||||
if (!string.IsNullOrWhiteSpace(outputDirectory))
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
File.WriteAllBytes(template.OutputPath, template.Content);
|
||||
}
|
||||
|
||||
foreach (var previousManagedFile in previousManagedFiles.Keys)
|
||||
{
|
||||
if (newManagedPaths.Contains(previousManagedFile))
|
||||
continue;
|
||||
var stalePath = ResolveManagedOutputPath(outputRoot, previousManagedFile);
|
||||
if (File.Exists(stalePath))
|
||||
File.Delete(stalePath);
|
||||
}
|
||||
|
||||
var manifestLines = templates
|
||||
.OrderBy(item => item.RelativePath, StringComparer.Ordinal)
|
||||
.Select(item => item.RelativePath + "\t" + ComputeSha256(item.Content));
|
||||
File.WriteAllLines(manifestPath, manifestLines, new UTF8Encoding(false));
|
||||
|
||||
var stamp = new StringBuilder();
|
||||
stamp.AppendLine("{");
|
||||
@@ -267,39 +277,55 @@ public static class ShrinkDedicatedServerScaffoldGenerator
|
||||
File.WriteAllText(stampPath, stamp.ToString(), new UTF8Encoding(false));
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> ReadTemplateManifest(string manifestPath)
|
||||
{
|
||||
var result = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
if (!File.Exists(manifestPath))
|
||||
return result;
|
||||
|
||||
foreach (var rawLine in File.ReadAllLines(manifestPath, Encoding.UTF8))
|
||||
{
|
||||
var line = rawLine.Trim();
|
||||
if (line.Length == 0)
|
||||
continue;
|
||||
|
||||
var separatorIndex = line.IndexOf('\t');
|
||||
var relativePath = separatorIndex >= 0 ? line[..separatorIndex] : line;
|
||||
var hash = separatorIndex >= 0 ? line[(separatorIndex + 1)..].Trim() : string.Empty;
|
||||
result[relativePath.Replace('\\', '/')] = hash;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string ResolveManagedOutputPath(string outputRoot, string relativePath)
|
||||
{
|
||||
var normalizedRoot = Path.GetFullPath(outputRoot)
|
||||
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||
var fullPath = Path.GetFullPath(Path.Combine(normalizedRoot, relativePath.Replace('/', Path.DirectorySeparatorChar)));
|
||||
if (!fullPath.StartsWith(normalizedRoot, StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidOperationException($"模板清单包含越界路径:{relativePath}");
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
private static string ComputeSha256(byte[] content)
|
||||
{
|
||||
using var sha256 = SHA256.Create();
|
||||
return BitConverter.ToString(sha256.ComputeHash(content)).Replace("-", string.Empty);
|
||||
}
|
||||
|
||||
private static void GenerateScaffoldFiles(string projectRoot, string assetsPath, string outputDir)
|
||||
{
|
||||
Directory.CreateDirectory(outputDir);
|
||||
|
||||
var messages = new List<MessageSpec>();
|
||||
var subscribers = new List<SubscriberSpec>();
|
||||
var enums = new List<EnumSpec>();
|
||||
var dataTypes = new List<DataTypeSpec>();
|
||||
|
||||
foreach (var file in Directory.EnumerateFiles(assetsPath, "*.cs", SearchOption.AllDirectories))
|
||||
{
|
||||
if (file.IndexOf($"{Path.DirectorySeparatorChar}Editor{Path.DirectorySeparatorChar}", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
continue;
|
||||
|
||||
var content = File.ReadAllText(file, Encoding.UTF8);
|
||||
var relativePath = Path.GetRelativePath(projectRoot, file).Replace('\\', '/');
|
||||
|
||||
ExtractMessages(content, relativePath, messages);
|
||||
ExtractSubscribers(content, relativePath, subscribers);
|
||||
ExtractEnums(content, relativePath, enums);
|
||||
ExtractDataTypes(content, relativePath, dataTypes);
|
||||
}
|
||||
|
||||
messages = messages.GroupBy(item => item.TypeName, StringComparer.Ordinal).Select(group => group.First())
|
||||
var scanResult = ShrinkNetworkSemanticScanner.ScanCompiledPlayerAssemblies();
|
||||
var messages = scanResult.Messages
|
||||
.OrderBy(item => item.Opcode).ThenBy(item => item.TypeName, StringComparer.Ordinal).ToList();
|
||||
subscribers = subscribers.OrderBy(item => item.Permission, StringComparer.Ordinal)
|
||||
var subscribers = scanResult.Subscribers.OrderBy(item => item.Permission, StringComparer.Ordinal)
|
||||
.ThenBy(item => item.MemberName, StringComparer.Ordinal).ToList();
|
||||
enums = enums.GroupBy(item => item.Name, StringComparer.Ordinal).Select(group => group.First())
|
||||
var enums = scanResult.Enums
|
||||
.OrderBy(item => item.Name, StringComparer.Ordinal).ToList();
|
||||
|
||||
var messageTypeNames = new HashSet<string>(messages.Select(item => item.TypeName), StringComparer.Ordinal);
|
||||
dataTypes = dataTypes.Where(item => !messageTypeNames.Contains(item.Name))
|
||||
.GroupBy(item => item.Name, StringComparer.Ordinal).Select(group => group.First())
|
||||
var dataTypes = scanResult.DataTypes
|
||||
.OrderBy(item => item.Name, StringComparer.Ordinal).ToList();
|
||||
|
||||
ValidateTemplateConflicts(messages);
|
||||
@@ -326,152 +352,6 @@ public static class ShrinkDedicatedServerScaffoldGenerator
|
||||
}
|
||||
}
|
||||
|
||||
private static void ExtractMessages(string content, string relativePath, List<MessageSpec> target)
|
||||
{
|
||||
var metadataByType = new Dictionary<string, (int Opcode, string Route)>(StringComparer.Ordinal);
|
||||
foreach (Match match in MessageHeaderRegex.Matches(content))
|
||||
{
|
||||
metadataByType[match.Groups["name"].Value] = (
|
||||
int.Parse(match.Groups["opcode"].Value),
|
||||
match.Groups["route"].Value);
|
||||
}
|
||||
|
||||
foreach (Match match in RegisteredMessageRegex.Matches(content))
|
||||
{
|
||||
var typeName = match.Groups["name"].Value;
|
||||
if (!metadataByType.ContainsKey(typeName))
|
||||
{
|
||||
metadataByType[typeName] = (
|
||||
int.Parse(match.Groups["opcode"].Value),
|
||||
match.Groups["route"].Value);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Match match in NetworkTypeRegex.Matches(content))
|
||||
{
|
||||
var typeName = match.Groups["name"].Value;
|
||||
if (!metadataByType.TryGetValue(typeName, out var metadata))
|
||||
continue;
|
||||
|
||||
var bases = match.Groups["bases"].Value;
|
||||
var kind = bases.Contains("IShrinkNetworkRequest", StringComparison.Ordinal)
|
||||
? "request"
|
||||
: bases.Contains("ShrinkRpcResponseBase", StringComparison.Ordinal)
|
||||
? "response"
|
||||
: bases.Contains("IShrinkNetworkMessage", StringComparison.Ordinal)
|
||||
? "message"
|
||||
: string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(kind))
|
||||
continue;
|
||||
|
||||
var body = TryExtractBody(content, match.Index);
|
||||
var headerStart = Math.Max(0, match.Index - 256);
|
||||
var headerLength = Math.Min(content.Length - headerStart, match.Length + 256);
|
||||
var headerSnippet = content.Substring(headerStart, headerLength);
|
||||
var syncMatch = StateSyncAttributeRegex.Match(headerSnippet);
|
||||
var spec = new MessageSpec
|
||||
{
|
||||
TypeName = typeName,
|
||||
Opcode = metadata.Opcode,
|
||||
Route = metadata.Route,
|
||||
SourcePath = relativePath,
|
||||
HasResult = headerSnippet.Contains("[HasResult]", StringComparison.Ordinal),
|
||||
IsNetworkEvent = headerSnippet.Contains("[ShrinkNetworkEvent]", StringComparison.Ordinal),
|
||||
IsDeltaEvent = bases.Contains("IShrinkNetworkDeltaEvent", StringComparison.Ordinal),
|
||||
SyncGroup = syncMatch.Success ? syncMatch.Groups["group"].Value.Trim() : string.Empty,
|
||||
SyncRole = syncMatch.Success ? syncMatch.Groups["role"].Value.Trim() : string.Empty,
|
||||
Kind = kind
|
||||
};
|
||||
|
||||
foreach (Match propertyMatch in PropertyRegex.Matches(body))
|
||||
spec.Properties.Add((propertyMatch.Groups["type"].Value, propertyMatch.Groups["name"].Value));
|
||||
|
||||
target.Add(spec);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ExtractSubscribers(string content, string relativePath, List<SubscriberSpec> target)
|
||||
{
|
||||
foreach (Match match in SubscriberRegex.Matches(content))
|
||||
{
|
||||
var args = match.Groups["args"].Value;
|
||||
target.Add(new SubscriberSpec
|
||||
{
|
||||
SourcePath = relativePath,
|
||||
MemberName = match.Groups["name"].Value,
|
||||
Authority = AuthorityRegex.Match(args).Groups["value"].Value,
|
||||
Permission = PermissionRegex.Match(args).Groups["value"].Value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void ExtractEnums(string content, string relativePath, List<EnumSpec> target)
|
||||
{
|
||||
foreach (Match match in EnumHeaderRegex.Matches(content))
|
||||
{
|
||||
var body = TryExtractBody(content, match.Index);
|
||||
if (string.IsNullOrWhiteSpace(body))
|
||||
continue;
|
||||
|
||||
var spec = new EnumSpec { Name = match.Groups["name"].Value, SourcePath = relativePath };
|
||||
foreach (Match memberMatch in EnumMemberRegex.Matches(body))
|
||||
{
|
||||
var memberName = memberMatch.Groups["name"].Value;
|
||||
if (string.IsNullOrWhiteSpace(memberName))
|
||||
continue;
|
||||
var memberValue = memberMatch.Groups["value"].Success ? memberMatch.Groups["value"].Value.Trim() : null;
|
||||
spec.Members.Add((memberName, string.IsNullOrWhiteSpace(memberValue) ? null : memberValue));
|
||||
}
|
||||
|
||||
if (spec.Members.Count > 0)
|
||||
target.Add(spec);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ExtractDataTypes(string content, string relativePath, List<DataTypeSpec> target)
|
||||
{
|
||||
foreach (Match match in DataTypeHeaderRegex.Matches(content))
|
||||
{
|
||||
var body = TryExtractBody(content, match.Index);
|
||||
if (string.IsNullOrWhiteSpace(body))
|
||||
continue;
|
||||
|
||||
var spec = new DataTypeSpec
|
||||
{
|
||||
Name = match.Groups["name"].Value,
|
||||
Kind = match.Groups["kind"].Value,
|
||||
SourcePath = relativePath
|
||||
};
|
||||
|
||||
foreach (Match propertyMatch in PropertyRegex.Matches(body))
|
||||
spec.Properties.Add((propertyMatch.Groups["type"].Value, propertyMatch.Groups["name"].Value));
|
||||
|
||||
if (spec.Properties.Count > 0)
|
||||
target.Add(spec);
|
||||
}
|
||||
}
|
||||
|
||||
private static string TryExtractBody(string content, int startIndex)
|
||||
{
|
||||
var braceIndex = content.IndexOf('{', startIndex);
|
||||
if (braceIndex < 0)
|
||||
return string.Empty;
|
||||
|
||||
var depth = 0;
|
||||
for (var i = braceIndex; i < content.Length; i++)
|
||||
{
|
||||
if (content[i] == '{') depth++;
|
||||
else if (content[i] == '}')
|
||||
{
|
||||
depth--;
|
||||
if (depth == 0)
|
||||
return content.Substring(braceIndex + 1, i - braceIndex - 1);
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static void ResolvePortableDependencies(
|
||||
IReadOnlyList<MessageSpec> messages,
|
||||
IReadOnlyList<EnumSpec> enums,
|
||||
@@ -531,11 +411,22 @@ public static class ShrinkDedicatedServerScaffoldGenerator
|
||||
|
||||
private static IEnumerable<string> ExtractTypeDependencies(string typeName)
|
||||
{
|
||||
foreach (Match match in Regex.Matches(typeName, @"[A-Za-z_]\w*"))
|
||||
var token = new StringBuilder();
|
||||
foreach (var character in typeName.Append(' '))
|
||||
{
|
||||
var token = match.Value;
|
||||
if (!KnownTypeTokens.Contains(token))
|
||||
yield return token;
|
||||
if (char.IsLetterOrDigit(character) || character == '_')
|
||||
{
|
||||
token.Append(character);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (token.Length == 0)
|
||||
continue;
|
||||
|
||||
var value = token.ToString();
|
||||
token.Clear();
|
||||
if (!KnownTypeTokens.Contains(value) && !char.IsDigit(value[0]))
|
||||
yield return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1113,11 +1004,15 @@ public static class ShrinkDedicatedServerScaffoldGenerator
|
||||
return true;
|
||||
}
|
||||
|
||||
var genericMatch = Regex.Match(collectionType, @"^[A-Za-z_][\w\.]*<(?<element>[A-Za-z_][\w\.\?]*)>$");
|
||||
if (genericMatch.Success)
|
||||
var genericStart = collectionType.IndexOf('<');
|
||||
if (genericStart > 0 && collectionType.EndsWith(">", StringComparison.Ordinal))
|
||||
{
|
||||
elementType = genericMatch.Groups["element"].Value;
|
||||
return true;
|
||||
var candidate = collectionType.Substring(genericStart + 1, collectionType.Length - genericStart - 2).Trim();
|
||||
if (candidate.Length > 0)
|
||||
{
|
||||
elementType = candidate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
elementType = string.Empty;
|
||||
@@ -1137,7 +1032,13 @@ public static class ShrinkDedicatedServerScaffoldGenerator
|
||||
private static string SanitizeIdentifier(string value)
|
||||
{
|
||||
var token = value.Replace("/", "_").Replace("-", "_");
|
||||
token = Regex.Replace(token, @"[^A-Za-z0-9_]", "_");
|
||||
token = new string(token.Select(character =>
|
||||
(character >= 'A' && character <= 'Z') ||
|
||||
(character >= 'a' && character <= 'z') ||
|
||||
(character >= '0' && character <= '9') ||
|
||||
character == '_'
|
||||
? character
|
||||
: '_').ToArray());
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
return "Room";
|
||||
if (!char.IsLetter(token[0]) && token[0] != '_')
|
||||
|
||||
@@ -0,0 +1,409 @@
|
||||
#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 HasResultAttributeName = "ShrinkEventBus.HasResultAttribute";
|
||||
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 = HasAttribute(type, HasResultAttributeName),
|
||||
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 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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 81f2d65d24dd8184ea8b396ba83ba159
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "ShrinkNetwork.Editor",
|
||||
"rootNamespace": "ShrinkNetwork.Editor",
|
||||
"references": [
|
||||
"ShrinkNetwork.Runtime"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 12dae8366427fee4b805ff52065bd1c0
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user