chore: initialize standalone UPM package
Publish UPM package / publish (push) Failing after 1s

This commit is contained in:
2026-08-26 02:50:31 +08:00
commit 78ccae3e07
142 changed files with 6572 additions and 0 deletions
+585
View File
@@ -0,0 +1,585 @@
#if UNITY_EDITOR
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEditor.PackageManager;
using UnityEngine;
public static class ShrinkModSdkExporter
{
private const string ExportMenuPath = "ShrinkSDK/Mod/导出 Mod SDK 开发包";
private const string DefaultOutputRootRelativePath = "GeneratedModSdk";
private const string StampFileName = ".shrink-mod-sdk.json";
private const string TemplateProjectName = "SampleShrinkMod";
private const string EventBusPackageName = "com.cneicy.shrink-eventbus";
private const string ModFrameworkPackageName = "com.cneicy.shrink-mod-framework";
private const string EventBusGeneratorProjectRelativePath = "Tools~/DotNet/ShrinkEventBus.Generator/ShrinkEventBus.Generator.csproj";
private const string BundledGeneratorRelativePath = "Editor/Scaffolding/Support/ShrinkEventBus.Generator.dll.bytes";
private static readonly string[] ExportAssemblyNames =
{
"ShrinkModFramework.Runtime",
"ShrinkEventBus.Runtime",
"ShrinkEventBus.Generator",
"ShrinkCommand.Runtime",
"ShrinkCommand.Integration.EventBus",
"ShrinkCommand.Integration.Network",
"ShrinkNetwork.Runtime",
"ShrinkNetwork.Integration.EventBus",
"ShrinkDataSaver.Runtime",
"ShrinkDataSaver.Integration.EventBus",
"Assembly-CSharp",
"UniTask",
"Newtonsoft.Json",
"Kcp-CSharp",
"System.Runtime.CompilerServices.Unsafe",
"UnityEngine",
"UnityEngine.CoreModule"
};
[MenuItem(ExportMenuPath)]
public static void ExportSdk()
{
try
{
var projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
BuildSolution(projectRoot);
var generatorAssemblyPath = BuildOrResolveEventBusGenerator(projectRoot);
var defaultOutputRoot = Path.Combine(projectRoot, DefaultOutputRootRelativePath);
Directory.CreateDirectory(defaultOutputRoot);
var outputRoot = EditorUtility.SaveFolderPanel(
"选择 Mod SDK 导出目录",
defaultOutputRoot,
"ShrinkModSdk");
if (string.IsNullOrWhiteSpace(outputRoot))
return;
outputRoot = Path.GetFullPath(outputRoot);
PrepareOutputDirectory(outputRoot);
var libsRoot = Path.Combine(outputRoot, "Libs");
var templateRoot = Path.Combine(outputRoot, "Templates", "ExternalMod", TemplateProjectName);
Directory.CreateDirectory(libsRoot);
Directory.CreateDirectory(templateRoot);
var exportedFiles = ExportLibraries(projectRoot, generatorAssemblyPath, libsRoot);
WriteSdkReadme(outputRoot, exportedFiles);
WriteSdkManifest(outputRoot, exportedFiles);
WriteExternalTemplate(templateRoot);
WriteStamp(outputRoot);
UnityEngine.Debug.Log($"[ShrinkModFramework] 已导出 Mod SDK 开发包:{outputRoot}");
EditorUtility.RevealInFinder(outputRoot);
}
catch (Exception ex)
{
UnityEngine.Debug.LogError($"[ShrinkModFramework] 导出 Mod SDK 开发包失败:{ex.Message}");
UnityEngine.Debug.LogException(ex);
}
}
private static void BuildSolution(string projectRoot)
{
var solutionPath = Path.Combine(projectRoot, "ShrinkSDK.sln");
if (!File.Exists(solutionPath))
{
UnityEngine.Debug.Log("[ShrinkModFramework] 未检测到 Workspace 解决方案,直接使用当前 Unity 已编译程序集导出 Mod SDK。");
return;
}
var startInfo = new ProcessStartInfo
{
FileName = "dotnet",
Arguments = "build .\\ShrinkSDK.sln /m:1 /nr:false -p:BaseIntermediateOutputPath=Temp\\CliObj\\ModSdkExport\\",
WorkingDirectory = projectRoot,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var process = Process.Start(startInfo);
if (process == null)
throw new InvalidOperationException("无法启动 dotnet build。");
var stdout = process.StandardOutput.ReadToEnd();
var stderr = process.StandardError.ReadToEnd();
process.WaitForExit();
if (process.ExitCode != 0)
{
throw new InvalidOperationException(
"构建 ShrinkSDK.sln 失败。\n" +
stdout + "\n" +
stderr);
}
}
private static string BuildOrResolveEventBusGenerator(string projectRoot)
{
var generatorProjectPath = ResolveEventBusGeneratorProjectPath(projectRoot);
if (generatorProjectPath == null)
{
var bundledGeneratorPath = Path.Combine(ResolveModFrameworkPackagePath(projectRoot), BundledGeneratorRelativePath);
if (File.Exists(bundledGeneratorPath))
return bundledGeneratorPath;
throw new InvalidOperationException(
"未找到 ShrinkEventBus.Generator 源码或随包分发的分析器。请安装 com.cneicy.shrink-eventbus,或使用包含 ShrinkModFramework 支持资源的完整包版本。");
}
var startInfo = new ProcessStartInfo
{
FileName = "dotnet",
Arguments = "build \"" + generatorProjectPath + "\" -c Release /nr:false",
WorkingDirectory = Path.GetDirectoryName(generatorProjectPath),
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var process = Process.Start(startInfo);
if (process == null)
throw new InvalidOperationException("无法启动 ShrinkEventBus.Generator 构建。");
var stdout = process.StandardOutput.ReadToEnd();
var stderr = process.StandardError.ReadToEnd();
process.WaitForExit();
if (process.ExitCode != 0)
throw new InvalidOperationException("构建 ShrinkEventBus.Generator 失败。\n" + stdout + "\n" + stderr);
var generatorAssemblyPath = Path.Combine(
Path.GetDirectoryName(generatorProjectPath) ?? string.Empty,
"bin",
"Release",
"netstandard2.0",
"ShrinkEventBus.Generator.dll");
if (!File.Exists(generatorAssemblyPath))
throw new InvalidOperationException("ShrinkEventBus.Generator 构建完成但未找到输出 DLL" + generatorAssemblyPath);
return generatorAssemblyPath;
}
private static string? ResolveEventBusGeneratorProjectPath(string projectRoot)
{
var workspaceProjectPath = Path.Combine(
projectRoot,
"Assets",
"Modules",
"ShrinkEventBus",
EventBusGeneratorProjectRelativePath);
if (File.Exists(workspaceProjectPath))
return workspaceProjectPath;
var package = PackageInfo.GetAllRegisteredPackages()
.FirstOrDefault(candidate => string.Equals(candidate.name, EventBusPackageName, StringComparison.Ordinal));
if (package == null)
return null;
var packageProjectPath = Path.Combine(package.resolvedPath, EventBusGeneratorProjectRelativePath);
return File.Exists(packageProjectPath) ? packageProjectPath : null;
}
private static string ResolveModFrameworkPackagePath(string projectRoot)
{
var workspacePackagePath = Path.Combine(projectRoot, "Assets", "Modules", "ShrinkModFramework");
if (File.Exists(Path.Combine(workspacePackagePath, "package.json")))
return workspacePackagePath;
var package = PackageInfo.GetAllRegisteredPackages()
.FirstOrDefault(candidate => string.Equals(candidate.name, ModFrameworkPackageName, StringComparison.Ordinal));
if (package != null && Directory.Exists(package.resolvedPath))
return package.resolvedPath;
throw new InvalidOperationException("无法解析 ShrinkModFramework 包路径。");
}
private static void PrepareOutputDirectory(string outputRoot)
{
Directory.CreateDirectory(outputRoot);
var stampPath = Path.Combine(outputRoot, StampFileName);
if (Directory.EnumerateFileSystemEntries(outputRoot).Any() && !File.Exists(stampPath))
{
throw new InvalidOperationException(
$"目标目录已存在且不是 ShrinkModFramework 自动导出的 Mod SDK 目录,为避免覆盖人工内容,已中止:{outputRoot}");
}
foreach (var entry in Directory.EnumerateFileSystemEntries(outputRoot).ToArray())
{
if (Directory.Exists(entry))
Directory.Delete(entry, true);
else
File.Delete(entry);
}
}
private static List<string> ExportLibraries(string projectRoot, string generatorAssemblyPath, string libsRoot)
{
var candidateDirectories = new[]
{
Path.Combine(projectRoot, "Temp", "Bin", "Debug", "Assembly-CSharp"),
Path.Combine(projectRoot, "Temp", "Bin", "Debug", "ShrinkModFramework.Runtime"),
Path.Combine(projectRoot, "Temp", "Bin", "Debug", "ShrinkEventBus.Runtime"),
Path.GetDirectoryName(generatorAssemblyPath) ?? string.Empty,
Path.Combine(projectRoot, "Temp", "Bin", "Debug", "ShrinkCommand.Runtime"),
Path.Combine(projectRoot, "Temp", "Bin", "Debug", "ShrinkCommand.Integration.EventBus"),
Path.Combine(projectRoot, "Temp", "Bin", "Debug", "ShrinkCommand.Integration.Network"),
Path.Combine(projectRoot, "Temp", "Bin", "Debug", "ShrinkNetwork.Runtime"),
Path.Combine(projectRoot, "Temp", "Bin", "Debug", "ShrinkNetwork.Integration.EventBus"),
Path.Combine(projectRoot, "Temp", "Bin", "Debug", "ShrinkDataSaver.Runtime"),
Path.Combine(projectRoot, "Temp", "Bin", "Debug", "ShrinkDataSaver.Integration.EventBus")
};
var copied = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var exported = new List<string>();
foreach (var assemblyName in ExportAssemblyNames)
{
foreach (var extension in new[] { ".dll", ".pdb", ".xml" })
{
var fileName = assemblyName + extension;
if (copied.Contains(fileName))
continue;
var source = candidateDirectories
.Select(directory => Path.Combine(directory, fileName))
.FirstOrDefault(File.Exists);
if (string.IsNullOrWhiteSpace(source))
continue;
var destination = Path.Combine(libsRoot, fileName);
File.Copy(source, destination, true);
copied.Add(fileName);
exported.Add(fileName.Replace('\\', '/'));
}
}
return exported.OrderBy(item => item, StringComparer.OrdinalIgnoreCase).ToList();
}
private static void WriteSdkReadme(string outputRoot, IReadOnlyList<string> exportedFiles)
{
var builder = new StringBuilder();
builder.AppendLine("# Shrink Mod SDK");
builder.AppendLine();
builder.AppendLine("这是给外部模组开发者使用的开发包。");
builder.AppendLine();
builder.AppendLine("## 内容");
builder.AppendLine();
builder.AppendLine("- `Libs/`");
builder.AppendLine(" - 已编译好的框架运行时 DLL、EventBus 代码生成器、当前游戏 DLL,以及模板需要的基础依赖");
builder.AppendLine("- `Templates/ExternalMod/SampleShrinkMod/`");
builder.AppendLine(" - 可直接复制一份开始写外部模组的模板工程");
builder.AppendLine();
builder.AppendLine("## 推荐用法");
builder.AppendLine();
builder.AppendLine("1. 复制 `Templates/ExternalMod/SampleShrinkMod/` 到你自己的工作目录");
builder.AppendLine("2. 修改项目名、命名空间、`modId`、命令路径、消息 opcode");
builder.AppendLine("3. 优先使用模板里的 `build.cmd` 构建");
builder.AppendLine("4. 把构建出来的 DLL 投放到游戏的 `Mods` 目录");
builder.AppendLine();
builder.AppendLine("## 为什么不用直接引用仓库 csproj");
builder.AppendLine();
builder.AppendLine("- 外部开发者通常拿不到完整仓库环境");
builder.AppendLine("- 独立打开 `Sdk-style csproj` 时,有些 IDE/MSBuild 环境会碰到 workload SDK 解析问题");
builder.AppendLine("- 这个开发包的目标就是让外部开发者只依赖 `Libs/` 就能开始写模组");
builder.AppendLine();
builder.AppendLine("## 当前游戏 API");
builder.AppendLine();
builder.AppendLine("- 当前先直接导出 `Assembly-CSharp.dll` 作为游戏侧 API");
builder.AppendLine("- 长期更推荐单独抽一个稳定的 `Game.ModAPI.dll`,避免把整个游戏主程序集暴露给外部模组");
builder.AppendLine();
builder.AppendLine("## 已导出的文件");
builder.AppendLine();
foreach (var exportedFile in exportedFiles)
builder.AppendLine("- `Libs/" + exportedFile + "`");
File.WriteAllText(Path.Combine(outputRoot, "README.md"), builder.ToString(), new UTF8Encoding(false));
}
private static void WriteSdkManifest(string outputRoot, IReadOnlyList<string> exportedFiles)
{
var unityVersion = File.Exists(Path.Combine(Path.GetFullPath(Path.Combine(Application.dataPath, "..")), "ProjectSettings", "ProjectVersion.txt"))
? File.ReadAllLines(Path.Combine(Path.GetFullPath(Path.Combine(Application.dataPath, "..")), "ProjectSettings", "ProjectVersion.txt"))
.FirstOrDefault(line => line.StartsWith("m_EditorVersion:", StringComparison.Ordinal))?
.Split(':').Last().Trim() ?? string.Empty
: string.Empty;
var builder = new StringBuilder();
builder.AppendLine("{");
builder.AppendLine($@" ""exportedAt"": ""{DateTime.UtcNow:O}"",");
builder.AppendLine($@" ""unityVersion"": ""{unityVersion}"",");
builder.AppendLine(@" ""assemblies"": [");
for (var i = 0; i < exportedFiles.Count; i++)
{
var suffix = i == exportedFiles.Count - 1 ? string.Empty : ",";
builder.AppendLine($@" ""{exportedFiles[i]}""{suffix}");
}
builder.AppendLine(" ]");
builder.AppendLine("}");
File.WriteAllText(Path.Combine(outputRoot, "manifest.json"), builder.ToString(), new UTF8Encoding(false));
}
private static void WriteExternalTemplate(string templateRoot)
{
Directory.CreateDirectory(Path.Combine(templateRoot, "src"));
File.WriteAllText(Path.Combine(templateRoot, "README.md"), BuildTemplateReadme(), new UTF8Encoding(false));
File.WriteAllText(Path.Combine(templateRoot, "global.json"), BuildTemplateGlobalJson(), new UTF8Encoding(false));
File.WriteAllText(Path.Combine(templateRoot, "build.cmd"), BuildTemplateBuildCmd(), new UTF8Encoding(false));
File.WriteAllText(Path.Combine(templateRoot, "dev-shell.cmd"), BuildTemplateDevShellCmd(), new UTF8Encoding(false));
File.WriteAllText(Path.Combine(templateRoot, TemplateProjectName + ".csproj"), BuildTemplateCsproj(), new UTF8Encoding(false));
File.WriteAllText(Path.Combine(templateRoot, "src", TemplateProjectName + "Contracts.cs"), BuildTemplateContractsCs(), new UTF8Encoding(false));
File.WriteAllText(Path.Combine(templateRoot, "src", TemplateProjectName + "Mod.cs"), BuildTemplateModCs(), new UTF8Encoding(false));
}
private static string BuildTemplateReadme()
{
return @"# SampleShrinkMod
这是给外部模组开发者的 DLL 模组模板。
## 目录
- `SampleShrinkMod.csproj`
- 引用 `..\..\..\Libs\` 下的 SDK DLL
- `build.cmd`
- 推荐的构建入口,会先设置 `MSBuildEnableWorkloadResolver=false`
- `dev-shell.cmd`
- 打开一个已经设置好环境变量的命令行
- `src/`
- 示例模组代码
EventBus handler 只使用 `[ShrinkEventSubscriber]` / `[ShrinkSubscribe]`。项目已把 `Libs/ShrinkEventBus.Generator.dll` 作为 Roslyn analyzer 引入,因此 Attach/Post 路径不依赖运行时反射。
## 推荐构建方式
双击或在命令行运行:
```cmd
build.cmd
```
## 如果你想开 IDE
先运行:
```cmd
dev-shell.cmd
```
然后在这个 shell 里再启动 IDE,或者继续执行 `build.cmd`。
## 注意
- 当前模板默认引用 `..\..\..\Libs\Assembly-CSharp.dll` 作为游戏 API
- 长期更推荐游戏方单独提供一个稳定的 `Game.ModAPI.dll`
";
}
private static string BuildTemplateGlobalJson()
{
return @"{
""sdk"": {
""version"": ""9.0.312"",
""rollForward"": ""latestPatch""
}
}";
}
private static string BuildTemplateBuildCmd()
{
return @"@echo off
setlocal
set MSBuildEnableWorkloadResolver=false
dotnet build .\SampleShrinkMod.csproj -c Release /nr:false
endlocal";
}
private static string BuildTemplateDevShellCmd()
{
return @"@echo off
set MSBuildEnableWorkloadResolver=false
echo MSBuildEnableWorkloadResolver=false
echo Current dotnet:
dotnet --version
cmd /k";
}
private static string BuildTemplateCsproj()
{
var references = new[]
{
"ShrinkModFramework.Runtime",
"ShrinkEventBus.Runtime",
"ShrinkCommand.Runtime",
"ShrinkCommand.Integration.EventBus",
"ShrinkCommand.Integration.Network",
"ShrinkNetwork.Runtime",
"ShrinkNetwork.Integration.EventBus",
"ShrinkDataSaver.Runtime",
"ShrinkDataSaver.Integration.EventBus",
"Assembly-CSharp",
"UniTask",
"Newtonsoft.Json",
"Kcp-CSharp",
"System.Runtime.CompilerServices.Unsafe",
"UnityEngine",
"UnityEngine.CoreModule"
};
var builder = new StringBuilder();
builder.AppendLine("<Project Sdk=\"Microsoft.NET.Sdk\">");
builder.AppendLine(" <PropertyGroup>");
builder.AppendLine(" <TargetFramework>net471</TargetFramework>");
builder.AppendLine(" <LangVersion>9.0</LangVersion>");
builder.AppendLine(" <Nullable>enable</Nullable>");
builder.AppendLine(" <ImplicitUsings>disable</ImplicitUsings>");
builder.AppendLine(" <MSBuildEnableWorkloadResolver>false</MSBuildEnableWorkloadResolver>");
builder.AppendLine(" <RootNamespace>SampleShrinkMod</RootNamespace>");
builder.AppendLine(" <AssemblyName>SampleShrinkMod</AssemblyName>");
builder.AppendLine(" <GenerateAssemblyInfo>false</GenerateAssemblyInfo>");
builder.AppendLine(" <EnableDefaultCompileItems>true</EnableDefaultCompileItems>");
builder.AppendLine(" <OutputPath>artifacts\\$(Configuration)\\</OutputPath>");
builder.AppendLine(" <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>");
builder.AppendLine(" </PropertyGroup>");
builder.AppendLine();
builder.AppendLine(" <ItemGroup>");
foreach (var reference in references)
{
builder.AppendLine($" <Reference Include=\"{reference}\">");
builder.AppendLine($" <HintPath>..\\..\\..\\Libs\\{reference}.dll</HintPath>");
builder.AppendLine(" <Private>true</Private>");
builder.AppendLine(" </Reference>");
}
builder.AppendLine(" </ItemGroup>");
builder.AppendLine();
builder.AppendLine(" <ItemGroup>");
builder.AppendLine(" <Analyzer Include=\"..\\..\\..\\Libs\\ShrinkEventBus.Generator.dll\" />");
builder.AppendLine(" </ItemGroup>");
builder.AppendLine("</Project>");
return builder.ToString();
}
private static string BuildTemplateContractsCs()
{
return @"#nullable enable
using ShrinkEventBus;
using ShrinkNetwork;
namespace SampleShrinkMod
{
public sealed class SampleShrinkModEvent : IShrinkEvent
{
public string Message { get; set; } = string.Empty;
}
[ShrinkNetworkMessage(41001, ""sample.shrink.mod/ping"")]
public sealed class SampleShrinkModPingRequest : IShrinkNetworkRequest
{
public string Message { get; set; } = string.Empty;
}
[ShrinkNetworkMessage(41002, ""sample.shrink.mod/ping_response"")]
public sealed class SampleShrinkModPingResponse : ShrinkRpcResponseBase
{
public string Message { get; set; } = string.Empty;
public int Counter { get; set; }
}
}";
}
private static string BuildTemplateModCs()
{
return @"#nullable enable
using ShrinkCommand;
using ShrinkEventBus;
using ShrinkModFramework;
using ShrinkNetwork;
namespace SampleShrinkMod
{
[ShrinkMod(""sample.shrink.mod"", ""Sample Shrink Mod"", ""1.0.0"")]
[ShrinkEventSubscriber(OwnerId = ""sample.shrink.mod"", DefaultBus = ""mod:sample.shrink.mod"")]
[ShrinkCommandSubscriber]
[ShrinkNetworkSubscriber]
public sealed partial class SampleShrinkModMod : ShrinkModBase
{
private readonly RuntimeState _state = new();
public override void OnConstruct(ShrinkModContext context)
{
context.Log(""模组已构造。"");
}
public override void OnRegisterContent(ShrinkModContext context)
{
context.RegisterContent(""example.items"", ""example_item"", ""Sample Shrink Mod Item"");
}
public override void OnInitialize(ShrinkModContext context)
{
context.RegisterNetworkHandler<int>(""sync.counter"", (_, value) =>
{
_state.Counter = value;
context.Log(""收到模组网络计数:"" + value);
});
}
[ShrinkSubscribe]
private void OnExampleEvent(SampleShrinkModEvent evt)
{
_state.LastEventMessage = evt.Message ?? string.Empty;
_state.Counter++;
}
[ShrinkCommand(""sample_shrink_mod ping"", Description = ""测试模组命令"", Permission = ""sample.shrink.mod.command.ping"")]
private string Ping()
{
return ""pong:"" + _state.Counter;
}
[ShrinkNetworkSubscribe(Permission = ""sample.shrink.mod.rpc.ping"")]
private SampleShrinkModPingResponse HandlePing(SampleShrinkModPingRequest request)
{
return new SampleShrinkModPingResponse
{
Message = ""pong:"" + (request.Message ?? string.Empty),
Counter = _state.Counter
};
}
private sealed class RuntimeState
{
public int Counter { get; set; }
public string LastEventMessage { get; set; } = string.Empty;
}
}
}";
}
private static void WriteStamp(string outputRoot)
{
var builder = new StringBuilder();
builder.AppendLine("{");
builder.AppendLine(@" ""generatedBy"": ""ShrinkModSdkExporter"",");
builder.AppendLine($@" ""generatedAt"": ""{DateTime.UtcNow:O}""");
builder.AppendLine("}");
File.WriteAllText(Path.Combine(outputRoot, StampFileName), builder.ToString(), new UTF8Encoding(false));
}
}
#endif