# 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.")` - `PatchAll(modAssembly)` 这意味着: - 模组可以在自己的程序集里直接写 Harmony Patch 类 - 框架负责发现模组后自动补丁,并在 ContextHost 路径登记逆操作 - 如果没有 Harmony,框架只会跳过,不会强依赖崩溃 注意: - 当前是“可选桥接”,不是把 Harmony 打进框架里 - 需要你自己把 `0Harmony.dll` 放进项目环境或外部模组依赖中 ## 注册表 ### 注册内容 ```csharp var key = context.RegisterContent("items", "sword", "Sword"); // key == "demo.core:sword" ``` 基础注册不接受任意完整键。框架始终使用当前 `ModId` 生成 `:`,避免模组误写其它 owner 的 namespace。 ### 覆盖已有内容 ```csharp context.OverrideContent( "items", "core:iron_sword", priority: 100, value: "Overridden Sword"); ``` ### 读取内容 ```csharp var itemRegistry = context.GetRegistry("items"); if (itemRegistry.TryGet("demo.core:sword", out var itemName)) { context.Log($"找到内容:{itemName}"); } ``` 规则: - 基础项的完整键固定为 `:` - `context.GetRegistry()` 只返回 `IReadOnlyShrinkModRegistry`;写入只能经过 `RegisterContent` / `OverrideContent` - 覆盖目标必须已经存在 - 优先级越高越先命中;同目标、同优先级直接报冲突 - 每条基础项和覆盖项都记录 owner;owner 卸载时自动撤回,并恢复下一个覆盖或基础值 - `Entries` 返回当前生效项,`BaseEntries` 返回未应用覆盖的基础项 - 注册表按类型和名称双重隔离 ## 网络同步 框架内置的是**网络同步抽象层**,不是具体联网库。 核心接口: - `IShrinkModNetworkTransport` - `ShrinkModNetworkManager` 这意味着你可以把它接到: - NGO - Mirror - FishNet - 自己的 Socket/Relay 层 ### 注册消息处理器 ```csharp context.RegisterNetworkHandler("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 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("sync.level", (messageContext, level) => { _saveData.Level = level; context.Log($"同步等级:{level}"); }); } [ShrinkSubscribe] private void OnSaveCompleted(ShrinkDataSaver.Integration.SaveCompletedEvent evt) { // 这里只是示意:模组类实例会被框架自动接到 EventBus } [ShrinkCommand("demo set-level ", Permission = "demo.admin")] private string SetLevel(int value) { _saveData.Level = value; return $"level={value}"; } [ShrinkNetworkSubscribe] private UniTask HandlePing(DemoPingRequest request) { return UniTask.FromResult(new DemoPingResponse { Message = "pong:" + request.Message }); } } ``` ### 各模块怎么用 #### 1. `ShrinkEventBus` 适合: - 模组内部系统解耦 - 监听 `ShrinkDataSaver.Integration.EventBus` - 配合 `ShrinkNetwork.Integration.EventBus` 做事件即网络消息 推荐: - 模组类本身带 `[ShrinkEventSubscriber]` 并声明 `DefaultBus = "mod:"` - 实例方法上写 `[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(...)` 当前边界: - `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 靠,下一步最值得做的是: - 模组配置系统 - 模组资源系统 - 模组命令系统 - 模组专属存档与同步策略