feat(cordis): 接入上下文组合与模组事务热替换
This commit is contained in:
@@ -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:
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
bin/
|
||||
obj/
|
||||
artifacts/
|
||||
+100
@@ -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` 或系统环境变量在**打开项目之前**先设置
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dfa94a3d7a41c6b4e81e7675db94e55f
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+22
@@ -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>
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 770057fe3b5782f4ba28e2e36f087b96
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+32
@@ -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"
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 956930660ab00e74692b22857f3159b7
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+8
@@ -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"
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 56c53370b8dbbab4ea70d4566bc1fed8
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "__DOTNET_SDK_VERSION__",
|
||||
"rollForward": "latestPatch"
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 48ebb276437630c4495d82c4c8e4ae2e
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c8713158fd023ce48abfb7365184ac7c
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+25
@@ -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; }
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f4ff81758d9539947ba6f4df94c2db91
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+70
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
@@ -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__`
|
||||
+7
@@ -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:
|
||||
+18
@@ -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
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9358a269a6e4a9740a69798dba1c1655
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+25
@@ -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; }
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2dbfd0d836e24d84eabaff73259c7e90
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+70
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e1319b1cd7dc0d4b918d12152b358ab
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+248
@@ -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
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 92c78e493d52a144389ddc6073111b0e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+203
@@ -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
|
||||
+11
@@ -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:
|
||||
Reference in New Issue
Block a user