715 lines
23 KiB
Markdown
715 lines
23 KiB
Markdown
# ShrinkModFramework
|
||
|
||
一个面向 Unity 与 Godot C# 的轻量模组框架,目标是给 `Shrink` 系列提供接近 Forge 的核心能力:
|
||
|
||
共享模组合同和加载规则位于 `DotNet~`,Godot `AssemblyLoadContext` 适配与打包工程位于 `Godot~`。Godot 项目安装 `ShrinkSDK.ModFramework.Godot`,会同时取得共享核心包。
|
||
|
||
- 模组声明
|
||
- 模组发现
|
||
- 依赖解析
|
||
- 生命周期阶段
|
||
- 内容注册表
|
||
- 可选显式启动
|
||
- 外部 DLL 模组热加载
|
||
- Harmony 热补丁接入
|
||
- 网络同步通道
|
||
|
||
## 当前定位
|
||
|
||
这是一套 Unity 可落地的 Forge 风格核心框架,不是 Minecraft Forge 的逐项复刻。
|
||
|
||
当前重点是:
|
||
|
||
- 已编译进工程内的模组
|
||
- 外部 DLL 模组按内容 revision 的增量发现与替换
|
||
- `ShrinkContextHost` 驱动的模组组件生命周期与失败恢复
|
||
- Harmony 补丁自动应用
|
||
- 与具体联网库解耦的网络同步框架
|
||
|
||
当前不包含:
|
||
|
||
- 运行时卸载已加载程序集(程序集仍受 Unity/Mono AppDomain 限制驻留)
|
||
- IL2CPP Player 下的外部 DLL 动态加载
|
||
- 内置的资源包系统、命令系统、配方编辑器
|
||
- 内置的具体联网实现
|
||
|
||
## 启动
|
||
|
||
安全默认值不会自动启动或扫描外部 DLL。需要装载工程内模组时,可以显式调用
|
||
`ShrinkModLoader.LoadAll(settings)`;需要外部代码模组时,调用方必须先完成自己的清单、
|
||
启用状态和哈希校验,再提交精确白名单:
|
||
|
||
```csharp
|
||
ShrinkModRuntimeBootstrap.InitializeDriver(settings);
|
||
ShrinkModLoader.LoadAuthorized(settings, authorizedDllPaths);
|
||
```
|
||
|
||
只有显式打开 `autoLoadOnStartup` 时,`AfterAssembliesLoaded` 入口才会装载工程内模组;
|
||
该自动入口仍不加载任何未授权外部 DLL。
|
||
|
||
相关入口:
|
||
|
||
- `Runtime/Bootstrap/ShrinkModRuntimeBootstrap.cs`
|
||
- `Runtime/Loading/ShrinkModLoader.cs`
|
||
|
||
`ShrinkModBootstrap` 仍然保留,作为手动覆盖或调试入口,但不是必需品。
|
||
|
||
## 配置文件
|
||
|
||
菜单 `ShrinkSDK/模组/创建设置` 会在
|
||
`Assets/Resources/GameAssets/Runtime/Data/ShrinkSDK/ShrinkModFrameworkSettings.asset`
|
||
创建配置。运行时优先加载该路径,并保留旧根路径回退。
|
||
|
||
关键配置包括:
|
||
|
||
- `autoLoadOnStartup`
|
||
- 是否在启动时自动装载工程内模组,默认关闭
|
||
- `useContextHost`
|
||
- 默认开启:把模组四阶段放进 `ShrinkModContextHost`,替换失败恢复旧组件源;关闭后回退旧 Loader
|
||
- `verboseLogging`
|
||
- 是否输出详细日志
|
||
- `assemblyNamePrefixes`
|
||
- 只扫描指定前缀的程序集
|
||
- `enableExternalDllMods`
|
||
- 是否允许显式白名单中的外部 DLL 模组,默认关闭
|
||
- `externalModsFolderName`
|
||
- 外部模组目录名,默认 `Mods`
|
||
- `watchExternalModsDirectory`
|
||
- 是否监听文件变化并重新提交既有白名单,默认关闭;监听不会扩大授权范围
|
||
- `externalModsReloadDelaySeconds`
|
||
- 文件变更后延迟多少秒再尝试热加载
|
||
- `externalAssemblyRevisionSoftLimit`
|
||
- 外部 DLL 常驻 revision 的软阈值;达到阈值时提示执行 Domain Reload 或重启进程,默认 `16`
|
||
- `enableHarmonyPatching`
|
||
- 是否启用 Harmony 自动补丁
|
||
- `enableNetworkSync`
|
||
- 是否启用网络同步框架
|
||
|
||
## 目录结构
|
||
|
||
当前 `Assets/Modules/ShrinkModFramework/Runtime/` 按职责拆成:
|
||
|
||
- `Bootstrap/`
|
||
- 启动入口、运行时驱动、设置资产
|
||
- `Core/`
|
||
- 模组实例生命周期核心类型
|
||
- `Metadata/`
|
||
- 模组特性、依赖、状态、版本信息
|
||
- `Registry/`
|
||
- 模组注册表与注册表管理器
|
||
- `Loading/`
|
||
- 模组发现、装载、外部 DLL 与 Harmony 接入
|
||
- `Network/`
|
||
- 模组网络抽象层
|
||
- `Integration/`
|
||
- 对现有 Shrink 模块的可选自动接入胶水
|
||
|
||
推荐阅读顺序:
|
||
|
||
1. `Bootstrap/`
|
||
2. `Core/` + `Metadata/`
|
||
3. `Loading/`
|
||
4. `Registry/` + `Network/` + `Integration/`
|
||
|
||
另外还有一层编辑器脚手架:
|
||
|
||
- `Editor/Scaffolding/`
|
||
- `ShrinkExternalModTemplateGenerator.cs`
|
||
- `ExternalModProjectTemplate/`
|
||
|
||
这层专门给模组开发者生成外部 DLL 模组模板,不参与运行时装载。
|
||
|
||
## 模组定义
|
||
|
||
### 基础模组
|
||
|
||
```csharp
|
||
using ShrinkModFramework;
|
||
|
||
[ShrinkMod("demo.core", "Demo Core", "1.0.0")]
|
||
public class DemoCoreMod : ShrinkModBase
|
||
{
|
||
public override void OnRegisterContent(ShrinkModContext context)
|
||
{
|
||
context.RegisterContent("items", "iron_hammer", "Iron Hammer");
|
||
}
|
||
|
||
public override void OnInitialize(ShrinkModContext context)
|
||
{
|
||
context.Log("初始化完成");
|
||
}
|
||
}
|
||
```
|
||
|
||
### 声明依赖
|
||
|
||
```csharp
|
||
using ShrinkModFramework;
|
||
|
||
[ShrinkMod("demo.magic", "Demo Magic", "1.0.0")]
|
||
[ShrinkModDependency("demo.core", "1.0.0")]
|
||
public class DemoMagicMod : ShrinkModBase
|
||
{
|
||
public override void OnInitialize(ShrinkModContext context)
|
||
{
|
||
context.Log("我会在 demo.core 之后初始化");
|
||
}
|
||
}
|
||
```
|
||
|
||
### 关闭模组自动补丁
|
||
|
||
```csharp
|
||
[ShrinkMod("demo.safe", "Demo Safe", "1.0.0", AutoApplyHarmonyPatches = false)]
|
||
public class DemoSafeMod : ShrinkModBase
|
||
{
|
||
}
|
||
```
|
||
|
||
## 生命周期
|
||
|
||
每个模组按依赖顺序进入以下阶段:
|
||
|
||
1. `OnConstruct`
|
||
2. `OnRegisterContent`
|
||
3. `OnInitialize`
|
||
4. `OnReady`
|
||
|
||
推荐职责:
|
||
|
||
- `OnConstruct`
|
||
- 初始化本模组运行时对象
|
||
- `OnRegisterContent`
|
||
- 向注册表注册物品、方块、能力、配方等
|
||
- `OnInitialize`
|
||
- 注册事件、接线系统、绑定网络频道
|
||
- `OnReady`
|
||
- 做依赖其他模组最终状态的收尾逻辑
|
||
|
||
## 外部 DLL 模组热加载
|
||
|
||
框架不会递归扫描模组目录。调用方必须把每个允许进入 AppDomain 的入口 DLL 绝对路径
|
||
作为白名单提交给 `LoadAuthorized`;目录中未列出的 DLL 不会被读取或加载。
|
||
|
||
ContextLoader 装载规则:
|
||
|
||
- 以 DLL 内容 SHA-256 作为 revision;同 revision 幂等,不触发重载
|
||
- 文件变更会加载新程序集并提交新的模组组件源;旧程序集仍驻留,但旧 fiber 的效应会先回滚
|
||
- 新模组任何阶段失败时,Host 会重新协调旧 source;旧注册表内容和已登记效应恢复
|
||
- 文件删除会移除当前 source 并卸载模组实例;不会宣称程序集已从 AppDomain 卸载
|
||
- 不支持 IL2CPP Player 动态程序集加载
|
||
- 支持同目录依赖程序集解析
|
||
|
||
首次安全加载:
|
||
|
||
```csharp
|
||
ShrinkModRuntimeBootstrap.InitializeDriver(settings);
|
||
ShrinkModLoader.LoadAuthorized(settings, authorizedDllPaths);
|
||
```
|
||
|
||
后续可调用 `ShrinkModLoader.LoadNewExternalMods(settings)` 重新读取同一白名单的 revision,
|
||
把新增、替换、删除映射为完整期望组合;变更事务失败时保留旧模组组合。设置
|
||
`useContextHost = false` 时,旧路径同样只读取显式白名单,但仍保持只增不减的兼容语义。
|
||
|
||
如果 `watchExternalModsDirectory = true`,框架会监听目录变化并在主线程 debouncer 后重新提交
|
||
既有白名单;未授权文件即使触发通知也不会被加载。同一 burst 内的中间坏文件不会覆盖当前有效 revision。
|
||
|
||
### 常驻 revision 诊断
|
||
|
||
Unity/Mono 不能从当前 AppDomain 单独卸载已载入程序集。框架保留当前 revision 的生效语义,同时通过以下接口暴露实际常驻情况:
|
||
|
||
```csharp
|
||
var snapshot = ShrinkModDiagnostics.CaptureExternalAssemblies(settings);
|
||
```
|
||
|
||
快照包含当前与历史 revision、来源路径、程序集名、SHA-256 revision、载入字节数、常驻数量和软阈值状态。失败 revision 可以成为已载入的历史程序集,但不会成为 current;达到软阈值只告警,不伪造卸载行为。
|
||
|
||
导入 `ShrinkContext.AppAdapter` 的 Editor 工具后,也可以从 `ShrinkSDK/上下文/诊断与组合` 查看同一份常驻快照;该窗口通过可选反射读取,不会让 AppAdapter 对 ModFramework 建立硬依赖。
|
||
|
||
## Harmony 热补丁
|
||
|
||
如果运行环境里存在 `0Harmony`,ContextHost 会把每个模组的补丁租约作为可逆效应管理:激活时调用
|
||
`PatchAll(modAssembly)`,卸载/替换时调用 `UnpatchSelf`(或兼容的 `UnpatchAll(id)`)。旧 Loader
|
||
路径仍按原逻辑只应用一次。
|
||
|
||
旧路径的调用形态是:
|
||
|
||
- `Harmony("shrink.mod.<modId>")`
|
||
- `PatchAll(modAssembly)`
|
||
|
||
这意味着:
|
||
|
||
- 模组可以在自己的程序集里直接写 Harmony Patch 类
|
||
- 框架负责发现模组后自动补丁,并在 ContextHost 路径登记逆操作
|
||
- 如果没有 Harmony,框架只会跳过,不会强依赖崩溃
|
||
|
||
注意:
|
||
|
||
- 当前是“可选桥接”,不是把 Harmony 打进框架里
|
||
- 需要你自己把 `0Harmony.dll` 放进项目环境或外部模组依赖中
|
||
|
||
## 注册表
|
||
|
||
### 注册内容
|
||
|
||
```csharp
|
||
var key = context.RegisterContent("items", "sword", "Sword");
|
||
// key == "demo.core:sword"
|
||
```
|
||
|
||
基础注册不接受任意完整键。框架始终使用当前 `ModId` 生成 `<owner>:<localKey>`,避免模组误写其它 owner 的 namespace。
|
||
|
||
### 覆盖已有内容
|
||
|
||
```csharp
|
||
context.OverrideContent(
|
||
"items",
|
||
"core:iron_sword",
|
||
priority: 100,
|
||
value: "Overridden Sword");
|
||
```
|
||
|
||
### 读取内容
|
||
|
||
```csharp
|
||
var itemRegistry = context.GetRegistry<string>("items");
|
||
if (itemRegistry.TryGet("demo.core:sword", out var itemName))
|
||
{
|
||
context.Log($"找到内容:{itemName}");
|
||
}
|
||
```
|
||
|
||
规则:
|
||
|
||
- 基础项的完整键固定为 `<ownerModId>:<localKey>`
|
||
- `context.GetRegistry<T>()` 只返回 `IReadOnlyShrinkModRegistry<T>`;写入只能经过 `RegisterContent` / `OverrideContent`
|
||
- 覆盖目标必须已经存在
|
||
- 优先级越高越先命中;同目标、同优先级直接报冲突
|
||
- 每条基础项和覆盖项都记录 owner;owner 卸载时自动撤回,并恢复下一个覆盖或基础值
|
||
- `Entries` 返回当前生效项,`BaseEntries` 返回未应用覆盖的基础项
|
||
- 注册表按类型和名称双重隔离
|
||
|
||
## 网络同步
|
||
|
||
框架内置的是**网络同步抽象层**,不是具体联网库。
|
||
|
||
核心接口:
|
||
|
||
- `IShrinkModNetworkTransport`
|
||
- `ShrinkModNetworkManager`
|
||
|
||
这意味着你可以把它接到:
|
||
|
||
- NGO
|
||
- Mirror
|
||
- FishNet
|
||
- 自己的 Socket/Relay 层
|
||
|
||
### 注册消息处理器
|
||
|
||
```csharp
|
||
context.RegisterNetworkHandler<int>("sync.hp", (messageContext, value) =>
|
||
{
|
||
Debug.Log($"收到 HP:{value}");
|
||
});
|
||
```
|
||
|
||
### 发送消息
|
||
|
||
```csharp
|
||
context.SendToServer("sync.hp", 100);
|
||
context.SendToAllClients("sync.hp", 100);
|
||
context.SendToClient("sync.hp", 100, "client-1");
|
||
```
|
||
|
||
### 绑定传输层
|
||
|
||
```csharp
|
||
public sealed class MyTransport : IShrinkModNetworkTransport
|
||
{
|
||
public bool IsServer => true;
|
||
public bool IsClient => true;
|
||
|
||
public event Action<ShrinkModNetworkEnvelope> OnEnvelopeReceived;
|
||
|
||
public void Send(ShrinkModNetworkEnvelope envelope)
|
||
{
|
||
// 这里接你自己的网络库
|
||
}
|
||
}
|
||
|
||
ShrinkModNetworkManager.SetTransport(new MyTransport());
|
||
```
|
||
|
||
## 与现有模块协作
|
||
|
||
当前仓库里已经有这些可复用模块:
|
||
|
||
- `ShrinkEventBus`
|
||
- `ShrinkDataSaver`
|
||
- `ShrinkCommand`
|
||
- `ShrinkCommand.Integration.EventBus`
|
||
- `ShrinkCommand.Integration.Network`
|
||
- `ShrinkNetwork`
|
||
- `ShrinkNetwork.Integration.EventBus`
|
||
- `ShrinkDataSaver.Integration.EventBus`
|
||
|
||
`ShrinkModFramework` 当前的定位不是“重新包一层这些模块”,而是给模组提供统一生命周期,然后在合适阶段把模组实例接到这些模块现成的注册入口上。
|
||
|
||
### 自动接入了什么
|
||
|
||
从当前版本开始,模组经过:
|
||
|
||
1. `OnConstruct`
|
||
2. `OnRegisterContent`
|
||
|
||
之后,框架会自动尝试把**模组实例本身**接入这些现有模块:
|
||
|
||
- 如果模组类带 `[ShrinkEventSubscriber]` 且携带生成绑定,自动 `Attach` 到 `ShrinkBusKey.Mod(modId)`,并在模组生命周期结束时释放 binding
|
||
- 如果模组类带 `[ShrinkCommandSubscriber]`,自动调用 `ShrinkCommandRuntime.Default.RegisterCommands(modInstance)`
|
||
- 如果模组类带 `[ShrinkNetworkSubscriber]`,自动调用 `ShrinkNetworkRuntime.Default.RegisterHandlers(modInstance)`
|
||
- 如果项目里存在 `ShrinkNetwork.Integration.EventBus`,会额外调用 `ShrinkNetworkEventBusBridge.RefreshBindings()`,把新模组里声明的网络事件类型补进桥接层
|
||
|
||
这意味着:
|
||
|
||
- **项目内模组**和**运行时外部 DLL 模组**都可以把实例方法挂到 `EventBus / Command / Network`
|
||
- 模组作者不需要自己再写一遍宿主层胶水
|
||
- EventBus 是模组框架的正式依赖;Command、Network 及其桥接仍按安装情况接入
|
||
|
||
### 推荐写法
|
||
|
||
最稳的做法是把“和现有模块交互的入口”直接写在模组类实例上,而不是依赖外部 DLL 的静态自动扫描。
|
||
|
||
```csharp
|
||
using Cysharp.Threading.Tasks;
|
||
using ShrinkCommand;
|
||
using ShrinkDataSaver;
|
||
using ShrinkEventBus;
|
||
using ShrinkModFramework;
|
||
using ShrinkNetwork;
|
||
|
||
[ShrinkMod("demo.full", "Demo Full", "1.0.0")]
|
||
[ShrinkEventSubscriber(OwnerId = "demo.full", DefaultBus = "mod:demo.full")]
|
||
[ShrinkCommandSubscriber]
|
||
[ShrinkNetworkSubscriber]
|
||
public sealed partial class DemoFullMod : ShrinkModBase
|
||
{
|
||
private DemoSaveData _saveData = new();
|
||
|
||
public override void OnInitialize(ShrinkModContext context)
|
||
{
|
||
ShrinkSave.RegisterModule(
|
||
$"{context.ModInfo.ModId}.save",
|
||
() => _saveData,
|
||
data => _saveData = data ?? new DemoSaveData());
|
||
|
||
context.RegisterNetworkHandler<int>("sync.level", (messageContext, level) =>
|
||
{
|
||
_saveData.Level = level;
|
||
context.Log($"同步等级:{level}");
|
||
});
|
||
}
|
||
|
||
[ShrinkSubscribe]
|
||
private void OnSaveCompleted(ShrinkDataSaver.Integration.SaveCompletedEvent evt)
|
||
{
|
||
// 这里只是示意:模组类实例会被框架自动接到 EventBus
|
||
}
|
||
|
||
[ShrinkCommand("demo set-level <value>", Permission = "demo.admin")]
|
||
private string SetLevel(int value)
|
||
{
|
||
_saveData.Level = value;
|
||
return $"level={value}";
|
||
}
|
||
|
||
[ShrinkNetworkSubscribe]
|
||
private UniTask<DemoPingResponse> HandlePing(DemoPingRequest request)
|
||
{
|
||
return UniTask.FromResult(new DemoPingResponse
|
||
{
|
||
Message = "pong:" + request.Message
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
### 各模块怎么用
|
||
|
||
#### 1. `ShrinkEventBus`
|
||
|
||
适合:
|
||
|
||
- 模组内部系统解耦
|
||
- 监听 `ShrinkDataSaver.Integration.EventBus`
|
||
- 配合 `ShrinkNetwork.Integration.EventBus` 做事件即网络消息
|
||
|
||
推荐:
|
||
|
||
- 模组类本身带 `[ShrinkEventSubscriber]` 并声明 `DefaultBus = "mod:<modId>"`
|
||
- 实例方法上写 `[ShrinkSubscribe]`
|
||
- 类型声明为 `partial`,由 ILPostProcessor 或 Roslyn incremental generator 生成强类型 binding
|
||
- 由 `ShrinkModFramework` 自动 Attach 到模组 Bus,并随模组生命周期释放
|
||
|
||
注意:
|
||
|
||
- Unity 项目内类型由 ILPostProcessor 生成 binding;外部 DLL 使用导出 SDK 中的 `ShrinkEventBus.Generator` 分析器
|
||
- 外部 DLL 必须携带生成合同;正式运行时不会反射扫描没有生成合同的旧程序集
|
||
- 模组框架只负责把已生成的实例 binding Attach 到对应 Mod Bus,不会枚举方法或调用 `MethodInfo.Invoke`
|
||
|
||
#### 2. `ShrinkDataSaver`
|
||
|
||
适合:
|
||
|
||
- 模组自己的配置和进度持久化
|
||
- 跨模组只读查询
|
||
|
||
推荐:
|
||
|
||
- 在 `OnInitialize` 里调用 `ShrinkSave.RegisterModule(...)`
|
||
- 模块 key 用 `modId` 做前缀,例如 `demo.full.save`
|
||
- 读取别的模块数据时优先走 `ShrinkSave.QueryModule<T>(...)`
|
||
|
||
当前边界:
|
||
|
||
- `ShrinkModFramework` 不会替你自动注册存档模块,因为每个模组要保存什么只能由业务自己决定
|
||
- 当前也没有热卸载流程,所以运行时新增 DLL 后注册的存档模块默认跟随本轮进程直到退出
|
||
|
||
#### 3. `ShrinkCommand`
|
||
|
||
适合:
|
||
|
||
- 模组开放调试命令
|
||
- 服务端管理命令
|
||
- 配合网络桥接做远程命令
|
||
|
||
推荐:
|
||
|
||
- 模组类带 `[ShrinkCommandSubscriber]`
|
||
- 实例方法写 `[ShrinkCommand("path ...")]`
|
||
- 交给框架自动注册到 `ShrinkCommandRuntime.Default`
|
||
|
||
如果要继续往外接:
|
||
|
||
- 需要 EventBus 请求式命令时,用 `ShrinkCommand.Integration.EventBus`
|
||
- 需要远程命令 RPC 时,用 `ShrinkCommand.Integration.Network`
|
||
|
||
#### 4. `ShrinkNetwork`
|
||
|
||
适合:
|
||
|
||
- 模组自己的 RPC / 消息协议
|
||
- 模组内部状态同步
|
||
|
||
推荐分两层:
|
||
|
||
- 简单场景:继续用 `context.RegisterNetworkHandler(...)` 和 `context.SendToServer/Client/...`
|
||
- 需要完整 `ShrinkNetwork` 能力时:模组类带 `[ShrinkNetworkSubscriber]`,实例方法写 `[ShrinkNetworkSubscribe]`
|
||
|
||
当前自动接入的是:
|
||
|
||
- `ShrinkNetworkRuntime.Default.RegisterHandlers(modInstance)`
|
||
|
||
这意味着:
|
||
|
||
- 模组实例上的网络 handler 可以直接进默认服务
|
||
- handler 参数里的消息类型如果带 `[ShrinkNetworkMessage]`,注册时会一起补齐消息元数据
|
||
|
||
#### 5. `ShrinkNetwork.Integration.EventBus`
|
||
|
||
适合:
|
||
|
||
- 事件本身就是网络协议
|
||
- 本地 EventBus 与远端 EventBus 保持同一套事件语义
|
||
|
||
推荐:
|
||
|
||
- 事件类型同时满足 `IShrinkEvent + IShrinkNetworkMessage`
|
||
- 标记 `[ShrinkNetworkEvent] + [ShrinkNetworkMessage(...)]`
|
||
- 让模组监听或发布这些事件,而不是再写一层重复 DTO
|
||
|
||
当前补的胶水:
|
||
|
||
- 外部 DLL 模组装载后,框架会调用 `ShrinkNetworkEventBusBridge.RefreshBindings()`
|
||
|
||
这能解决:
|
||
|
||
- 新模组里的网络事件类型,原来桥接层启动时看不到
|
||
- 现在新增 DLL 后可把这些事件补进桥接层的入站注册表
|
||
|
||
### 项目内模组 vs 外部 DLL 模组
|
||
|
||
这两类不要混着理解:
|
||
|
||
- **项目内模组**
|
||
- 编译时就在 Unity 当前 AppDomain 里
|
||
- 由 Unity ILPostProcessor 生成 EventBus binding
|
||
- **运行时外部 DLL 模组**
|
||
- 是 `ShrinkModFramework` 后续动态加载进来的
|
||
- 必须引用导出包里的 `ShrinkEventBus.Generator.dll` 生成同一份 binding 合同
|
||
|
||
所以当前推荐原则很明确:
|
||
|
||
- **对外部 DLL,优先写实例方法**
|
||
- 不要把可发现性建立在“宿主已经提前扫过一次全局静态特性”上
|
||
|
||
### 还没自动做的事
|
||
|
||
当前版本还**没有**替你自动做这些:
|
||
|
||
- 不会自动把模组数据注册进 `ShrinkDataSaver`
|
||
- 不会自动帮你给 `ShrinkModNetworkManager` 绑定 `ShrinkNetwork` 传输层
|
||
- 不会为外部 DLL 的静态 `EventBus / Command / Network` 特性类做全局重复扫描
|
||
|
||
原因很直接:
|
||
|
||
- `DataSaver` 需要业务决定保存什么
|
||
- `ShrinkModNetworkManager` 只是抽象层,具体要不要复用 `ShrinkNetwork`、Mirror、NGO 取决于宿主策略
|
||
- 对现有静态扫描入口做“再次全局扫描”容易产生重复注册
|
||
|
||
如果后面要继续往前推,最值得补的不是再加更多反射,而是做两块正式基础设施:
|
||
|
||
- `ShrinkModFramework.Integration.ShrinkNetwork`
|
||
- 把 `IShrinkModNetworkTransport` 正式桥到 `ShrinkNetworkSession / ShrinkNetworkService`
|
||
- `ShrinkModFramework.Integration.DataSaver`
|
||
- 给模组提供规范化的 `modId` 前缀存档注册和可选卸载清理策略
|
||
|
||
## 生成模组模板
|
||
|
||
现在已经提供和 `ShrinkNetwork` 独立服务器生成器同风格的模组开发入口:
|
||
|
||
- 菜单:`ShrinkSDK/模组/导出开发包`
|
||
- 菜单:`ShrinkSDK/模组/生成仓库内模板(推荐)`
|
||
- 菜单:`ShrinkSDK/模组/生成外部模板`
|
||
|
||
更推荐优先用“仓库内模组模板”:
|
||
|
||
- 直接生成到当前 Unity 项目的 `Assets/GeneratedMods/...`
|
||
- 复用当前 `ShrinkSDK.sln`
|
||
- 复用当前 Unity / asmdef 编译链
|
||
- 不需要单独打开外部 `csproj`
|
||
- 也就不会撞到独立工程那类 IDE / workload SDK 解析问题
|
||
|
||
适合:
|
||
|
||
- 你自己就在这个 SDK 仓库里开发模组
|
||
- 想最快开始写代码
|
||
- 想直接享受当前解决方案里的跳转、补全、编译和 Unity 刷新
|
||
|
||
“外部模组模板”更适合:
|
||
|
||
- 真正要把模组工程单独发给外部开发者
|
||
- 或者明确要独立于当前仓库维护一个 DLL 工程
|
||
|
||
如果目标是给**外部模组开发者**发 SDK,当前更推荐直接用:
|
||
|
||
- `ShrinkSDK/模组/导出开发包`
|
||
|
||
它会导出:
|
||
|
||
- `Libs/`
|
||
- 已编译好的框架运行时 DLL
|
||
- 当前游戏 DLL
|
||
- 模板构建所需的基础依赖
|
||
- `Templates/ExternalMod/SampleShrinkMod/`
|
||
- 一个已经改成引用 `Libs/*.dll` 的外部模组模板
|
||
- 自带 `build.cmd` 与 `dev-shell.cmd`
|
||
- 不再直接引用你本地仓库 `csproj`
|
||
|
||
这套开发包比“只给一个模板工程”更适合外发,因为:
|
||
|
||
- 外部开发者不需要拿到完整仓库
|
||
- 不需要依赖你本地 `ShrinkSDK.sln`
|
||
- 运行时框架和游戏 API 会跟模板一起发出去
|
||
- 当前默认游戏 API 先用 `Assembly-CSharp.dll`
|
||
- 后续如果你单独抽出 `Game.ModAPI.dll`,只需要替换导出内容即可
|
||
|
||
生成器会:
|
||
|
||
- 让你选择一个输出目录
|
||
- 以目录名推导默认的 `ProjectName / Namespace / DisplayName / ModId`
|
||
- 生成一个可独立构建的外部 DLL 模组工程
|
||
|
||
默认生成内容包括:
|
||
|
||
- `README.md`
|
||
- `build.ps1`
|
||
- `.gitignore`
|
||
- `global.json`
|
||
- `__PROJECT_NAME__.csproj`
|
||
- `src/__PROJECT_NAME__Mod.cs`
|
||
- `src/__PROJECT_NAME__Contracts.cs`
|
||
|
||
模板工程默认引用当前仓库里的:
|
||
|
||
- `ShrinkModFramework.Runtime.csproj`
|
||
- `ShrinkEventBus.Runtime.csproj`
|
||
- `ShrinkCommand.Runtime.csproj`
|
||
- `ShrinkNetwork.Runtime.csproj`
|
||
|
||
所以它不是一个“纯空壳”,而是直接演示:
|
||
|
||
- `[ShrinkMod]`
|
||
- `ShrinkModBase` 生命周期
|
||
- `EventBus / Command / Network` 实例自动接入
|
||
- 一个最小 RPC 请求/响应示例
|
||
|
||
兼容性处理:
|
||
|
||
- 模板 `csproj` 默认关闭 `MSBuild workload resolver`
|
||
- 模板同时会写入 `global.json`,锁到当前生成时检测到的 `dotnet --version`
|
||
- 模板还会生成 `dev-env.ps1`,用于在启动 IDE 前先设置 `MSBuildEnableWorkloadResolver=false`
|
||
|
||
这样可以尽量避免某些 IDE / MSBuild 环境在打开项目时误触发
|
||
`Microsoft.NET.SDK.WorkloadAutoImportPropsLocator` 解析失败。
|
||
|
||
建议流程:
|
||
|
||
1. 你自己在当前仓库开发时,优先用 `ShrinkSDK/模组/生成仓库内模板(推荐)`
|
||
2. 先改 `modId`、命名空间、命令路径、`opcode`
|
||
3. 在当前 Unity/IDE 里直接开发和编译
|
||
4. 如果要发给外部模组开发者,改用 `ShrinkSDK/模组/导出开发包`
|
||
|
||
如果你已经知道目标 `Mods` 目录,也可以直接跑模板里的:
|
||
|
||
```powershell
|
||
.\build.ps1 -ModsDir "D:\YourGame\Mods"
|
||
```
|
||
|
||
## 设计约束
|
||
|
||
- 一个模组类必须有 `[ShrinkMod]`
|
||
- 一个模组类必须实现 `IShrinkMod`
|
||
- 一个模组类必须能被无参构造
|
||
- 模组 ID 必须全局唯一
|
||
- 必选依赖缺失或版本不满足时,装载直接失败
|
||
- 检测到循环依赖时,装载直接失败
|
||
- 外部 DLL 支持当前组合的添加、替换与删除;已载入的程序集 revision 仍常驻,回滚和卸载单位是模组 fiber 与其效应
|
||
|
||
## 关键文件
|
||
|
||
- `Runtime/Bootstrap/ShrinkModRuntimeBootstrap.cs`
|
||
- `Runtime/Loading/ShrinkModLoader.cs`
|
||
- `Runtime/Loading/ShrinkExternalModAssemblyLoader.cs`
|
||
- `Runtime/Loading/ShrinkHarmonyPatchService.cs`
|
||
- `Runtime/Registry/ShrinkModRegistry.cs`
|
||
- `Runtime/Network/ShrinkModNetworkManager.cs`
|
||
- `Runtime/Network/IShrinkModNetworkTransport.cs`
|
||
- `Runtime/Integration/ShrinkModOptionalRuntimeIntegration.cs`
|
||
|
||
## 结论
|
||
|
||
现在这套框架已经从“只能在工程内静态发现模组”的骨架,升级成了:
|
||
|
||
- 可显式托管启动
|
||
- 可按精确白名单增量接入外部 DLL
|
||
- 可选 Harmony 补丁
|
||
- 可扩展的网络同步框架
|
||
|
||
如果继续往 Forge 靠,下一步最值得做的是:
|
||
|
||
- 模组配置系统
|
||
- 模组资源系统
|
||
- 模组命令系统
|
||
- 模组专属存档与同步策略
|