Files
ShrinkModFramework/README.md
T
cneicy 78ccae3e07
Publish UPM package / publish (push) Failing after 1s
chore: initialize standalone UPM package
2026-08-26 02:50:31 +08:00

711 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ShrinkModFramework
一个面向 Unity 的轻量模组框架,目标是给 `Shrink` 系列提供接近 Forge 的核心能力:
- 模组声明
- 模组发现
- 依赖解析
- 生命周期阶段
- 内容注册表
- 自动启动
- 外部 DLL 模组热加载
- Harmony 热补丁接入
- 网络同步通道
## 当前定位
这是一套 Unity 可落地的 Forge 风格核心框架,不是 Minecraft Forge 的逐项复刻。
当前重点是:
- 已编译进工程内的模组
- 外部 DLL 模组按内容 revision 的增量发现与替换
- `ShrinkContextHost` 驱动的模组组件生命周期与失败恢复
- Harmony 补丁自动应用
- 与具体联网库解耦的网络同步框架
当前不包含:
- 运行时卸载已加载程序集(程序集仍受 Unity/Mono AppDomain 限制驻留)
- IL2CPP Player 下的外部 DLL 动态加载
- 内置的资源包系统、命令系统、配方编辑器
- 内置的具体联网实现
## 自动启动
现在默认**不需要**把 `ShrinkModBootstrap` 挂到场景里。
框架会通过:
```csharp
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
```
自动读取 `ShrinkModFrameworkSettings` 并调用装载流程。
相关入口:
- `Runtime/Bootstrap/ShrinkModRuntimeBootstrap.cs`
- `Runtime/Loading/ShrinkModLoader.cs`
`ShrinkModBootstrap` 仍然保留,作为手动覆盖或调试入口,但不是必需品。
## 配置文件
创建 `ShrinkModFrameworkSettings` 资产后,框架会自动查找它。
关键配置包括:
- `autoLoadOnStartup`
- 是否在启动时自动装载模组
- `useContextHost`
- 默认开启:把模组四阶段放进 `ShrinkModContextHost`,替换失败恢复旧组件源;关闭后回退旧 Loader
- `verboseLogging`
- 是否输出详细日志
- `assemblyNamePrefixes`
- 只扫描指定前缀的程序集
- `enableExternalDllMods`
- 是否启用外部 DLL 模组
- `externalModsFolderName`
- 外部模组目录名,默认 `Mods`
- `watchExternalModsDirectory`
- 是否自动监听目录变化并协调外部 DLL revision 变化
- `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 模组热加载
框架会扫描:
`Application.persistentDataPath/<externalModsFolderName>`
默认就是:
`Application.persistentDataPath/Mods`
ContextLoader 装载规则:
- 以 DLL 内容 SHA-256 作为 revision;同 revision 幂等,不触发重载
- 文件变更会加载新程序集并提交新的模组组件源;旧程序集仍驻留,但旧 fiber 的效应会先回滚
- 新模组任何阶段失败时,Host 会重新协调旧 source;旧注册表内容和已登记效应恢复
- 文件删除会移除当前 source 并卸载模组实例;不会宣称程序集已从 AppDomain 卸载
- 不支持 IL2CPP Player 动态程序集加载
- 支持同目录依赖程序集解析
你可以在运行时调用:
```csharp
ShrinkModLoader.LoadNewExternalMods();
```
默认会扫描当前 DLL revision,并把新增、替换、删除映射为一个完整期望组合;
变更事务失败时保留旧模组组合。设置 `useContextHost = false` 才回退为仅新增 DLL 的旧路径。
如果 `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/Cordis/诊断与组合` 查看同一份常驻快照;该窗口通过可选反射读取,不会让 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/Mod/导出 Mod SDK 开发包`
- 菜单:`ShrinkSDK/Mod/生成仓库内模组模板(推荐)`
- 菜单:`ShrinkSDK/Mod/生成外部模组模板`
更推荐优先用“仓库内模组模板”:
- 直接生成到当前 Unity 项目的 `Assets/GeneratedMods/...`
- 复用当前 `ShrinkSDK.sln`
- 复用当前 Unity / asmdef 编译链
- 不需要单独打开外部 `csproj`
- 也就不会撞到独立工程那类 IDE / workload SDK 解析问题
适合:
- 你自己就在这个 SDK 仓库里开发模组
- 想最快开始写代码
- 想直接享受当前解决方案里的跳转、补全、编译和 Unity 刷新
“外部模组模板”更适合:
- 真正要把模组工程单独发给外部开发者
- 或者明确要独立于当前仓库维护一个 DLL 工程
如果目标是给**外部模组开发者**发 SDK,当前更推荐直接用:
- `ShrinkSDK/Mod/导出 Mod SDK 开发包`
它会导出:
- `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/Mod/生成仓库内模组模板(推荐)`
2. 先改 `modId`、命名空间、命令路径、`opcode`
3. 在当前 Unity/IDE 里直接开发和编译
4. 如果要发给外部模组开发者,改用 `ShrinkSDK/Mod/导出 Mod SDK 开发包`
如果你已经知道目标 `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 靠,下一步最值得做的是:
- 模组配置系统
- 模组资源系统
- 模组命令系统
- 模组专属存档与同步策略