feat(cordis): 接入上下文组合与模组事务热替换

This commit is contained in:
2026-08-16 23:20:40 +08:00
commit ad256f109b
676 changed files with 52168 additions and 0 deletions
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a4c5850a094f141429521fdc9e82295b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2b6d34b770edc7b4bb76e93b13ffd2ab
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,3 @@
bin/
obj/
artifacts/
@@ -0,0 +1,100 @@
# __PROJECT_NAME__
这是由 `ShrinkModFramework` 生成器创建的外部 DLL 模组模板。
## 模板内容
- `src/__PROJECT_NAME__Mod.cs`
- 模组主入口,演示 `ShrinkModFramework + ShrinkEventBus + ShrinkCommand + ShrinkNetwork`
- `src/__PROJECT_NAME__Contracts.cs`
- 示例事件、RPC 请求与响应
- `__PROJECT_NAME__.csproj`
- 独立类库工程,默认引用当前 SDK 仓库里的运行时工程
- `build.ps1`
- 一键构建,可选复制 DLL 到你的模组目录
- `dev-env.ps1`
- 先设置 `MSBuildEnableWorkloadResolver=false`,适合有些 IDE 打开项目前先跑一遍
- `global.json`
- 锁定到生成模板时检测到的 `dotnet SDK` 版本
## 默认标识
- `ProjectName`: `__PROJECT_NAME__`
- `Namespace`: `__ROOT_NAMESPACE__`
- `ModId`: `__MOD_ID__`
- `DisplayName`: `__DISPLAY_NAME__`
生成后建议你先改这几处,再开始写正式业务。
## 构建
```powershell
dotnet build .\__PROJECT_NAME__.csproj -c Release /nr:false
```
或者直接运行:
```powershell
.\build.ps1
```
如果你的 IDE 打开项目时报类似:
```text
Microsoft.NET.SDK.WorkloadAutoImportPropsLocator
```
先在 PowerShell 里执行:
```powershell
.\dev-env.ps1
```
然后在**同一个 shell** 里启动 IDE,或者直接继续 `dotnet build`。
默认产物输出到:
```text
artifacts\Release\
```
## 投放到游戏
`ShrinkModFramework` 当前会扫描:
```text
Application.persistentDataPath/Mods
```
所以常见流程是:
1. 先构建出 `__PROJECT_NAME__.dll`
2. 把 DLL 复制到游戏实际使用的 `Mods` 目录
3. 启动游戏,让 `ShrinkModLoader` 自动发现
如果你已经知道目标 `Mods` 目录,也可以直接用:
```powershell
.\build.ps1 -ModsDir "D:\YourGameMods\Mods"
```
## 这个模板示范了什么
- `[ShrinkMod]` 模组声明
- `ShrinkModBase` 生命周期
- `EventBusSubscriber` 实例自动接入
- `ShrinkCommandSubscriber` 实例命令自动接入
- `ShrinkNetworkSubscriber` 实例网络处理自动接入
- `context.GetRegistry(...)` 注册内容
- `context.RegisterNetworkHandler(...)` 走模组网络抽象层
## 注意
- 示例里用到的 `opcode`、命令路径、`modId` 都只是模板默认值,正式模组请自己调整,避免冲突
- 当前模板默认依赖当前仓库根目录下的:
- `ShrinkModFramework.Runtime.csproj`
- `ShrinkEventBus.Runtime.csproj`
- `ShrinkCommand.Runtime.csproj`
- `ShrinkNetwork.Runtime.csproj`
- 如果你把模板工程移到别处,需要同步修正 `__PROJECT_NAME__.csproj` 里的 `ProjectReference`
- 某些 IDE / MSBuild 环境里,`MSBuildEnableWorkloadResolver=false` 写在项目文件内仍然太晚;这种情况要靠 `dev-env.ps1` 或系统环境变量在**打开项目之前**先设置
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: dfa94a3d7a41c6b4e81e7675db94e55f
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net471</TargetFramework>
<LangVersion>9.0</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<MSBuildEnableWorkloadResolver>false</MSBuildEnableWorkloadResolver>
<RootNamespace>__ROOT_NAMESPACE__</RootNamespace>
<AssemblyName>__PROJECT_NAME__</AssemblyName>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<EnableDefaultCompileItems>true</EnableDefaultCompileItems>
<OutputPath>artifacts\$(Configuration)\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="__SDK_ROOT_RELATIVE__\ShrinkModFramework.Runtime.csproj" />
<ProjectReference Include="__SDK_ROOT_RELATIVE__\ShrinkEventBus.Runtime.csproj" />
<ProjectReference Include="__SDK_ROOT_RELATIVE__\ShrinkCommand.Runtime.csproj" />
<ProjectReference Include="__SDK_ROOT_RELATIVE__\ShrinkNetwork.Runtime.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 770057fe3b5782f4ba28e2e36f087b96
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,32 @@
param(
[string]$Configuration = "Release",
[string]$ModsDir = ""
)
$ErrorActionPreference = "Stop"
$projectPath = Join-Path $PSScriptRoot "__PROJECT_NAME__.csproj"
dotnet build $projectPath -c $Configuration /nr:false
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
$artifactDir = Join-Path $PSScriptRoot ("artifacts\" + $Configuration)
$dllPath = Join-Path $artifactDir "__PROJECT_NAME__.dll"
Write-Host "Build completed: $dllPath"
if ([string]::IsNullOrWhiteSpace($ModsDir)) {
return
}
New-Item -ItemType Directory -Force -Path $ModsDir | Out-Null
Copy-Item -LiteralPath $dllPath -Destination (Join-Path $ModsDir "__PROJECT_NAME__.dll") -Force
$pdbPath = Join-Path $artifactDir "__PROJECT_NAME__.pdb"
if (Test-Path -LiteralPath $pdbPath) {
Copy-Item -LiteralPath $pdbPath -Destination (Join-Path $ModsDir "__PROJECT_NAME__.pdb") -Force
}
Write-Host "Copied artifacts to: $ModsDir"
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 956930660ab00e74692b22857f3159b7
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
$env:MSBuildEnableWorkloadResolver = "false"
Write-Host "MSBuildEnableWorkloadResolver=false"
Write-Host "Current dotnet: $(dotnet --version)"
Write-Host ""
Write-Host "现在请在这个 PowerShell 里启动你的 IDE,或直接在这个 shell 里运行 dotnet build。"
Write-Host "示例:"
Write-Host " dotnet build .\\__PROJECT_NAME__.csproj -c Release /nr:false"
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 56c53370b8dbbab4ea70d4566bc1fed8
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,6 @@
{
"sdk": {
"version": "__DOTNET_SDK_VERSION__",
"rollForward": "latestPatch"
}
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 48ebb276437630c4495d82c4c8e4ae2e
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c8713158fd023ce48abfb7365184ac7c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,25 @@
#nullable enable
using ShrinkEventBus;
using ShrinkNetwork;
namespace __ROOT_NAMESPACE__
{
public sealed class __PROJECT_NAME__Event : EventBase
{
public string Message { get; set; } = string.Empty;
}
[ShrinkNetworkMessage(__REQUEST_OPCODE__, "__ROUTE_PREFIX__/ping")]
public sealed class __PROJECT_NAME__PingRequest : IShrinkNetworkRequest
{
public string Message { get; set; } = string.Empty;
}
[ShrinkNetworkMessage(__RESPONSE_OPCODE__, "__ROUTE_PREFIX__/ping_response")]
public sealed class __PROJECT_NAME__PingResponse : ShrinkRpcResponseBase
{
public string Message { get; set; } = string.Empty;
public int Counter { get; set; }
}
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: f4ff81758d9539947ba6f4df94c2db91
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,70 @@
#nullable enable
using ShrinkCommand;
using ShrinkEventBus;
using ShrinkModFramework;
using ShrinkNetwork;
namespace __ROOT_NAMESPACE__
{
[ShrinkMod("__MOD_ID__", "__DISPLAY_NAME__", "1.0.0")]
[EventBusSubscriber]
[ShrinkCommandSubscriber]
[ShrinkNetworkSubscriber]
public sealed class __PROJECT_NAME__Mod : ShrinkModBase
{
private readonly RuntimeState _state = new();
public override void OnConstruct(ShrinkModContext context)
{
context.Log("模组已构造。");
}
public override void OnRegisterContent(ShrinkModContext context)
{
var registry = context.GetRegistry<string>("example.items");
registry.Register(
context.ModInfo.ModId,
context.ModInfo.ModId + ":example_item",
"__DISPLAY_NAME__ Item");
}
public override void OnInitialize(ShrinkModContext context)
{
context.RegisterNetworkHandler<int>("sync.counter", (_, value) =>
{
_state.Counter = value;
context.Log("收到模组网络计数:" + value);
});
}
[EventSubscribe]
private void OnExampleEvent(__PROJECT_NAME__Event evt)
{
_state.LastEventMessage = evt.Message ?? string.Empty;
_state.Counter++;
}
[ShrinkCommand("__COMMAND_ROOT__ ping", Description = "测试模组命令", Permission = "__MOD_ID__.command.ping")]
private string Ping()
{
return "pong:" + _state.Counter;
}
[ShrinkNetworkSubscribe(Permission = "__MOD_ID__.rpc.ping")]
private __PROJECT_NAME__PingResponse HandlePing(__PROJECT_NAME__PingRequest request)
{
return new __PROJECT_NAME__PingResponse
{
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;
}
}
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: a2a0ac418f07ef74393b89e9612e3f5f
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8b337545ac56d6f46a969397d332e35c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,39 @@
# __PROJECT_NAME__
这是 `ShrinkModFramework` 生成的仓库内模组模板。
## 为什么推荐这个模板
这个模板直接生成到当前 Unity 项目的 `Assets` 目录下,复用:
- 当前 Unity 工程
- 当前 `ShrinkSDK.sln`
- 当前 asmdef 编译链
所以它比“独立外部 csproj 模板”更省事,也不会碰到额外的 IDE / workload SDK 解析问题。
## 生成位置
- 当前输出目录:`__ASSET_RELATIVE_OUTPUT__`
## 模板内容
- `Runtime/__ASMDEF_NAME__.asmdef`
- `Runtime/__PROJECT_NAME__Mod.cs`
- `Runtime/__PROJECT_NAME__Contracts.cs`
- `README.md`
## 建议流程
1. 生成模板
2. 在现有 Unity/Rider/VS 解决方案里直接修改代码
3. 等 Unity 正常编译通过
4. 后续如果需要,再单独补“导出 DLL 到 Mods 目录”的步骤
## 默认标识
- `ProjectName`: `__PROJECT_NAME__`
- `Namespace`: `__ROOT_NAMESPACE__`
- `Assembly`: `__ASMDEF_NAME__`
- `ModId`: `__MOD_ID__`
- `DisplayName`: `__DISPLAY_NAME__`
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 8aa46c7dafda5194b94ab88513c56b44
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f7a48cec05ee9c842ad9bc0b7e7ec425
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,18 @@
{
"name": "__ASMDEF_NAME__",
"rootNamespace": "__ROOT_NAMESPACE__",
"references": [
"ShrinkModFramework.Runtime",
"ShrinkEventBus.Runtime",
"ShrinkCommand.Runtime",
"ShrinkNetwork.Runtime"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 9358a269a6e4a9740a69798dba1c1655
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,25 @@
#nullable enable
using ShrinkEventBus;
using ShrinkNetwork;
namespace __ROOT_NAMESPACE__
{
public sealed class __PROJECT_NAME__Event : EventBase
{
public string Message { get; set; } = string.Empty;
}
[ShrinkNetworkMessage(32001, "__MOD_ID__/ping")]
public sealed class __PROJECT_NAME__PingRequest : IShrinkNetworkRequest
{
public string Message { get; set; } = string.Empty;
}
[ShrinkNetworkMessage(32002, "__MOD_ID__/ping_response")]
public sealed class __PROJECT_NAME__PingResponse : ShrinkRpcResponseBase
{
public string Message { get; set; } = string.Empty;
public int Counter { get; set; }
}
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 2dbfd0d836e24d84eabaff73259c7e90
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,70 @@
#nullable enable
using ShrinkCommand;
using ShrinkEventBus;
using ShrinkModFramework;
using ShrinkNetwork;
namespace __ROOT_NAMESPACE__
{
[ShrinkMod("__MOD_ID__", "__DISPLAY_NAME__", "1.0.0")]
[EventBusSubscriber]
[ShrinkCommandSubscriber]
[ShrinkNetworkSubscriber]
public sealed class __PROJECT_NAME__Mod : ShrinkModBase
{
private readonly RuntimeState _state = new();
public override void OnConstruct(ShrinkModContext context)
{
context.Log("模组已构造。");
}
public override void OnRegisterContent(ShrinkModContext context)
{
var registry = context.GetRegistry<string>("example.items");
registry.Register(
context.ModInfo.ModId,
context.ModInfo.ModId + ":example_item",
"__DISPLAY_NAME__ Item");
}
public override void OnInitialize(ShrinkModContext context)
{
context.RegisterNetworkHandler<int>("sync.counter", (_, value) =>
{
_state.Counter = value;
context.Log("收到模组网络计数:" + value);
});
}
[EventSubscribe]
private void OnExampleEvent(__PROJECT_NAME__Event evt)
{
_state.LastEventMessage = evt.Message ?? string.Empty;
_state.Counter++;
}
[ShrinkCommand("__COMMAND_ROOT__ ping", Description = "测试模组命令", Permission = "__MOD_ID__.command.ping")]
private string Ping()
{
return "pong:" + _state.Counter;
}
[ShrinkNetworkSubscribe(Permission = "__MOD_ID__.rpc.ping")]
private __PROJECT_NAME__PingResponse HandlePing(__PROJECT_NAME__PingRequest request)
{
return new __PROJECT_NAME__PingResponse
{
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;
}
}
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 5e1319b1cd7dc0d4b918d12152b358ab
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,248 @@
#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 ShrinkExternalModTemplateGenerator
{
private const string GenerateMenuPath = "ShrinkSDK/Mod/生成外部模组模板";
private const string TemplateRelativePath = "Assets/Modules/ShrinkModFramework/Editor/Scaffolding/ExternalModProjectTemplate";
private const string DefaultGeneratedModsRelativePath = "GeneratedMods";
private const string TemplateStampFileName = ".shrink-mod-template.json";
private const string TemplateManifestFileName = ".shrink-mod-template-files.txt";
[MenuItem(GenerateMenuPath)]
public static void GenerateTemplate()
{
try
{
var context = BuildContext();
if (context == null)
return;
CopyTemplateProject(context.Value.TemplateRoot, context.Value.OutputRoot, context.Value.Replacements);
AssetDatabase.Refresh();
UnityEngine.Debug.Log($"[ShrinkModFramework] 已生成外部模组模板:{context.Value.OutputRoot}");
}
catch (Exception ex)
{
UnityEngine.Debug.LogError($"[ShrinkModFramework] 生成外部模组模板失败:{ex.Message}");
UnityEngine.Debug.LogException(ex);
}
}
private static (string ProjectRoot, string TemplateRoot, string OutputRoot, Dictionary<string, string> Replacements)? BuildContext()
{
var projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
var templateRoot = Path.Combine(projectRoot, TemplateRelativePath.Replace('/', Path.DirectorySeparatorChar));
if (!Directory.Exists(templateRoot))
throw new DirectoryNotFoundException($"未找到模组模板目录:{templateRoot}");
var generatedModsRoot = Path.Combine(projectRoot, DefaultGeneratedModsRelativePath);
Directory.CreateDirectory(generatedModsRoot);
var outputRoot = EditorUtility.SaveFolderPanel(
"选择外部模组模板输出目录",
generatedModsRoot,
"SampleShrinkMod");
if (string.IsNullOrWhiteSpace(outputRoot))
return null;
outputRoot = Path.GetFullPath(outputRoot);
var directoryName = new DirectoryInfo(outputRoot).Name;
var projectName = SanitizeProjectName(directoryName);
var rootNamespace = projectName;
var displayName = projectName;
var modId = BuildDefaultModId(projectName);
var commandRoot = ToCommandToken(projectName);
var routePrefix = modId.Replace('.', '/');
var sdkRootRelativePath = NormalizePath(Path.GetRelativePath(outputRoot, projectRoot));
var replacements = new Dictionary<string, string>(StringComparer.Ordinal)
{
["__PROJECT_NAME__"] = projectName,
["__ROOT_NAMESPACE__"] = rootNamespace,
["__DISPLAY_NAME__"] = displayName,
["__MOD_ID__"] = modId,
["__COMMAND_ROOT__"] = commandRoot,
["__ROUTE_PREFIX__"] = routePrefix,
["__SDK_ROOT_RELATIVE__"] = sdkRootRelativePath,
["__DOTNET_SDK_VERSION__"] = GetPreferredSdkVersion(),
["__REQUEST_OPCODE__"] = "31001",
["__RESPONSE_OPCODE__"] = "31002"
};
return (projectRoot, templateRoot, outputRoot, replacements);
}
private static void CopyTemplateProject(string templateRoot, string outputRoot, IReadOnlyDictionary<string, string> replacements)
{
Directory.CreateDirectory(outputRoot);
var stampPath = Path.Combine(outputRoot, TemplateStampFileName);
if (Directory.EnumerateFileSystemEntries(outputRoot).Any() && !File.Exists(stampPath))
{
throw new InvalidOperationException(
$"目标目录已存在且不是 ShrinkModFramework 自动生成的模组模板,为避免覆盖人工修改,已中止:{outputRoot}");
}
var managedFiles = new List<string>();
foreach (var templateFile in Directory.EnumerateFiles(templateRoot, "*", SearchOption.AllDirectories))
{
var relativePath = Path.GetRelativePath(templateRoot, templateFile);
relativePath = ReplaceTokens(relativePath, replacements);
var outputRelativePath = relativePath.EndsWith(".txt", StringComparison.Ordinal)
? relativePath[..^".txt".Length]
: relativePath;
managedFiles.Add(NormalizePath(outputRelativePath));
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);
content = ReplaceTokens(content, replacements);
File.WriteAllText(outputFile, content, new UTF8Encoding(false));
}
var manifestPath = Path.Combine(outputRoot, TemplateManifestFileName);
if (File.Exists(manifestPath))
{
var previousManagedFiles = File.ReadAllLines(manifestPath, Encoding.UTF8)
.Where(line => !string.IsNullOrWhiteSpace(line))
.ToArray();
foreach (var previousManagedFile in previousManagedFiles)
{
if (managedFiles.Contains(previousManagedFile, StringComparer.Ordinal))
continue;
var staleFile = Path.Combine(outputRoot, previousManagedFile.Replace('/', Path.DirectorySeparatorChar));
if (File.Exists(staleFile))
File.Delete(staleFile);
}
}
File.WriteAllLines(manifestPath, managedFiles.OrderBy(item => item, StringComparer.Ordinal), new UTF8Encoding(false));
var stamp = new StringBuilder();
stamp.AppendLine("{");
stamp.AppendLine(@" ""generatedBy"": ""ShrinkExternalModTemplateGenerator"",");
stamp.AppendLine($@" ""generatedAt"": ""{DateTime.UtcNow:O}""");
stamp.AppendLine("}");
File.WriteAllText(stampPath, stamp.ToString(), new UTF8Encoding(false));
}
private static string ReplaceTokens(string input, IReadOnlyDictionary<string, string> replacements)
{
var result = input;
foreach (var pair in replacements)
result = result.Replace(pair.Key, pair.Value);
return result;
}
private static string SanitizeProjectName(string rawName)
{
if (string.IsNullOrWhiteSpace(rawName))
return "SampleShrinkMod";
var builder = new StringBuilder();
var capitalizeNext = true;
foreach (var ch in rawName)
{
if (char.IsLetterOrDigit(ch))
{
builder.Append(capitalizeNext ? char.ToUpperInvariant(ch) : ch);
capitalizeNext = false;
}
else
{
capitalizeNext = true;
}
}
if (builder.Length == 0)
builder.Append("SampleShrinkMod");
if (char.IsDigit(builder[0]))
builder.Insert(0, 'M');
return builder.ToString();
}
private static string BuildDefaultModId(string projectName)
{
var token = ToKebabCase(projectName);
return string.IsNullOrWhiteSpace(token) ? "example.sample-mod" : $"example.{token}";
}
private static string ToCommandToken(string projectName)
{
var token = ToKebabCase(projectName).Replace("-", "_");
return string.IsNullOrWhiteSpace(token) ? "sample_mod" : token;
}
private static string ToKebabCase(string value)
{
if (string.IsNullOrWhiteSpace(value))
return string.Empty;
var builder = new StringBuilder();
for (var i = 0; i < value.Length; i++)
{
var ch = value[i];
if (!char.IsLetterOrDigit(ch))
{
if (builder.Length > 0 && builder[^1] != '-')
builder.Append('-');
continue;
}
if (char.IsUpper(ch) && i > 0 && builder.Length > 0 && builder[^1] != '-')
builder.Append('-');
builder.Append(char.ToLowerInvariant(ch));
}
return builder.ToString().Trim('-');
}
private static string NormalizePath(string path)
=> path.Replace('\\', '/');
private static string GetPreferredSdkVersion()
{
try
{
var startInfo = new ProcessStartInfo
{
FileName = "dotnet",
Arguments = "--version",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var process = Process.Start(startInfo);
if (process == null)
return "9.0.312";
var output = process.StandardOutput.ReadToEnd().Trim();
process.WaitForExit(3000);
return string.IsNullOrWhiteSpace(output) ? "9.0.312" : output;
}
catch
{
return "9.0.312";
}
}
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 92c78e493d52a144389ddc6073111b0e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,203 @@
#if UNITY_EDITOR
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEngine;
public static class ShrinkInRepoModTemplateGenerator
{
private const string GenerateMenuPath = "ShrinkSDK/Mod/生成仓库内模组模板(推荐)";
private const string TemplateRelativePath = "Assets/Modules/ShrinkModFramework/Editor/Scaffolding/InRepoModTemplate";
private const string DefaultOutputRootRelativePath = "Assets/GeneratedMods";
private const string TemplateStampFileName = ".shrink-inrepo-mod-template.json";
private const string TemplateManifestFileName = ".shrink-inrepo-mod-template-files.txt";
[MenuItem(GenerateMenuPath)]
public static void GenerateTemplate()
{
try
{
var context = BuildContext();
if (context == null)
return;
CopyTemplateProject(context.Value.TemplateRoot, context.Value.OutputRoot, context.Value.Replacements);
AssetDatabase.Refresh();
UnityEngine.Debug.Log($"[ShrinkModFramework] 已生成仓库内模组模板:{context.Value.OutputRoot}");
}
catch (Exception ex)
{
UnityEngine.Debug.LogError($"[ShrinkModFramework] 生成仓库内模组模板失败:{ex.Message}");
UnityEngine.Debug.LogException(ex);
}
}
private static (string TemplateRoot, string OutputRoot, Dictionary<string, string> Replacements)? BuildContext()
{
var projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
var templateRoot = Path.Combine(projectRoot, TemplateRelativePath.Replace('/', Path.DirectorySeparatorChar));
if (!Directory.Exists(templateRoot))
throw new DirectoryNotFoundException($"未找到仓库内模组模板目录:{templateRoot}");
var defaultOutputRoot = Path.Combine(projectRoot, DefaultOutputRootRelativePath.Replace('/', Path.DirectorySeparatorChar));
Directory.CreateDirectory(defaultOutputRoot);
var outputRoot = EditorUtility.SaveFolderPanel(
"选择仓库内模组模板输出目录",
defaultOutputRoot,
"SampleShrinkMod");
if (string.IsNullOrWhiteSpace(outputRoot))
return null;
outputRoot = Path.GetFullPath(outputRoot);
var assetsRoot = Path.GetFullPath(Application.dataPath);
if (!outputRoot.StartsWith(assetsRoot, StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("仓库内模组模板必须生成到当前项目的 Assets 目录下。");
var directoryName = new DirectoryInfo(outputRoot).Name;
var projectName = SanitizeProjectName(directoryName);
var rootNamespace = projectName;
var displayName = projectName;
var modId = BuildDefaultModId(projectName);
var commandRoot = ToCommandToken(projectName);
var asmdefName = projectName + ".Runtime";
var assetRelativeOutput = "Assets" + outputRoot.Substring(assetsRoot.Length).Replace('\\', '/');
var replacements = new Dictionary<string, string>(StringComparer.Ordinal)
{
["__PROJECT_NAME__"] = projectName,
["__ROOT_NAMESPACE__"] = rootNamespace,
["__DISPLAY_NAME__"] = displayName,
["__MOD_ID__"] = modId,
["__COMMAND_ROOT__"] = commandRoot,
["__ASMDEF_NAME__"] = asmdefName,
["__ASSET_RELATIVE_OUTPUT__"] = assetRelativeOutput
};
return (templateRoot, outputRoot, replacements);
}
private static void CopyTemplateProject(string templateRoot, string outputRoot, IReadOnlyDictionary<string, string> replacements)
{
Directory.CreateDirectory(outputRoot);
var stampPath = Path.Combine(outputRoot, TemplateStampFileName);
if (Directory.EnumerateFileSystemEntries(outputRoot).Any() && !File.Exists(stampPath))
{
throw new InvalidOperationException(
$"目标目录已存在且不是 ShrinkModFramework 自动生成的仓库内模组模板,为避免覆盖人工修改,已中止:{outputRoot}");
}
var managedFiles = new List<string>();
foreach (var templateFile in Directory.EnumerateFiles(templateRoot, "*", SearchOption.AllDirectories))
{
var relativePath = Path.GetRelativePath(templateRoot, templateFile);
relativePath = ReplaceTokens(relativePath, replacements);
var outputRelativePath = relativePath.EndsWith(".txt", StringComparison.Ordinal)
? relativePath[..^".txt".Length]
: relativePath;
managedFiles.Add(NormalizePath(outputRelativePath));
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);
content = ReplaceTokens(content, replacements);
File.WriteAllText(outputFile, content, new UTF8Encoding(false));
}
var manifestPath = Path.Combine(outputRoot, TemplateManifestFileName);
File.WriteAllLines(manifestPath, managedFiles.OrderBy(item => item, StringComparer.Ordinal), new UTF8Encoding(false));
var stamp = new StringBuilder();
stamp.AppendLine("{");
stamp.AppendLine(@" ""generatedBy"": ""ShrinkInRepoModTemplateGenerator"",");
stamp.AppendLine($@" ""generatedAt"": ""{DateTime.UtcNow:O}""");
stamp.AppendLine("}");
File.WriteAllText(stampPath, stamp.ToString(), new UTF8Encoding(false));
}
private static string ReplaceTokens(string input, IReadOnlyDictionary<string, string> replacements)
{
var result = input;
foreach (var pair in replacements)
result = result.Replace(pair.Key, pair.Value);
return result;
}
private static string SanitizeProjectName(string rawName)
{
if (string.IsNullOrWhiteSpace(rawName))
return "SampleShrinkMod";
var builder = new StringBuilder();
var capitalizeNext = true;
foreach (var ch in rawName)
{
if (char.IsLetterOrDigit(ch))
{
builder.Append(capitalizeNext ? char.ToUpperInvariant(ch) : ch);
capitalizeNext = false;
}
else
{
capitalizeNext = true;
}
}
if (builder.Length == 0)
builder.Append("SampleShrinkMod");
if (char.IsDigit(builder[0]))
builder.Insert(0, 'M');
return builder.ToString();
}
private static string BuildDefaultModId(string projectName)
{
var token = ToKebabCase(projectName);
return string.IsNullOrWhiteSpace(token) ? "example.sample-mod" : $"example.{token}";
}
private static string ToCommandToken(string projectName)
{
var token = ToKebabCase(projectName).Replace("-", "_");
return string.IsNullOrWhiteSpace(token) ? "sample_mod" : token;
}
private static string ToKebabCase(string value)
{
if (string.IsNullOrWhiteSpace(value))
return string.Empty;
var builder = new StringBuilder();
for (var i = 0; i < value.Length; i++)
{
var ch = value[i];
if (!char.IsLetterOrDigit(ch))
{
if (builder.Length > 0 && builder[^1] != '-')
builder.Append('-');
continue;
}
if (char.IsUpper(ch) && i > 0 && builder.Length > 0 && builder[^1] != '-')
builder.Append('-');
builder.Append(char.ToLowerInvariant(ch));
}
return builder.ToString().Trim('-');
}
private static string NormalizePath(string path)
=> path.Replace('\\', '/');
}
#endif
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b6cd6b51094b7e442bf0e5c09fcd42d3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,488 @@
#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/Mod/导出 Mod SDK 开发包";
private const string DefaultOutputRootRelativePath = "GeneratedModSdk";
private const string StampFileName = ".shrink-mod-sdk.json";
private const string TemplateProjectName = "SampleShrinkMod";
private static readonly string[] ExportAssemblyNames =
{
"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"
};
[MenuItem(ExportMenuPath)]
public static void ExportSdk()
{
try
{
var projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
BuildSolution(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, 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 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 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 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.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、当前游戏 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/`
- 示例模组代码
## 推荐构建方式
双击或在命令行运行:
```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("</Project>");
return builder.ToString();
}
private static string BuildTemplateContractsCs()
{
return @"#nullable enable
using ShrinkEventBus;
using ShrinkNetwork;
namespace SampleShrinkMod
{
public sealed class SampleShrinkModEvent : EventBase
{
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"")]
[EventBusSubscriber]
[ShrinkCommandSubscriber]
[ShrinkNetworkSubscriber]
public sealed class SampleShrinkModMod : ShrinkModBase
{
private readonly RuntimeState _state = new();
public override void OnConstruct(ShrinkModContext context)
{
context.Log(""模组已构造。"");
}
public override void OnRegisterContent(ShrinkModContext context)
{
var registry = context.GetRegistry<string>(""example.items"");
registry.Register(
context.ModInfo.ModId,
context.ModInfo.ModId + "":example_item"",
""Sample Shrink Mod Item"");
}
public override void OnInitialize(ShrinkModContext context)
{
context.RegisterNetworkHandler<int>(""sync.counter"", (_, value) =>
{
_state.Counter = value;
context.Log(""收到模组网络计数:"" + value);
});
}
[EventSubscribe]
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
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ebad25910ea711d41b17c9dd2bbc6032
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: