Files
cneicy 39f6f97707
Publish UPM package / publish (push) Successful in 2s
chore(editor): localize mod framework menus
2026-08-26 12:48:32 +08:00

585 lines
23 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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 UnityEngine;
public static class ShrinkModSdkExporter
{
private const string ExportMenuPath = "ShrinkSDK/模组/导出开发包";
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 = UnityEditor.PackageManager.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 = UnityEditor.PackageManager.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