- ShrinkModComponentDiscovery.Discover 公开:只发现、不执行生命周期 - ShrinkModLoader.LoadComposed / ShrinkModCordisRuntime.ApplyComposedAsync:宿主提供元数据组合加载,支持纯数据包与 IShrinkModWrapper 公开真实实例 - 公开 TryAttachEventBusInstance,随生命周期逆操作释放 - 依赖新增 VersionRange(Maven/Forge 区间语法)与 Ordering(Before/After) - AssemblyResolvePolicy 可限制跨模组程序集解析
30 KiB
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);需要外部代码模组时,调用方必须先完成自己的清单、
启用状态和哈希校验,再提交精确白名单:
ShrinkModRuntimeBootstrap.InitializeDriver(settings);
ShrinkModLoader.LoadAuthorized(settings, authorizedDllPaths);
只有显式打开 autoLoadOnStartup 时,AfterAssembliesLoaded 入口才会装载工程内模组;
该自动入口仍不加载任何未授权外部 DLL。
相关入口:
Runtime/Bootstrap/ShrinkModRuntimeBootstrap.csRuntime/Loading/ShrinkModLoader.cs
ShrinkModBootstrap 仍然保留,作为手动覆盖或调试入口,但不是必需品。
宿主清单组合(0.3.8)
Unity 宿主可以先发现授权 DLL,再用清单替换元数据和生命周期工厂,最后提交完整组合。
Discover 不构造模组、不运行生命周期、不改变已提交的 current revision;DLL 会进入 AppDomain,
外部源的 Revision 仍为 SHA-256:入口类型全名。工程内源仍按 assemblyNamePrefixes 筛选。
var discovered = ShrinkModComponentDiscovery.Discover(settings, authorizedDllPaths);
var original = discovered.Single(source => source.Info.ModId == "demo.code");
var info = new ShrinkModInfo("demo.code", "代码包", "1.2.0", 0,
original.Info.EntryType, original.Info.AutoApplyHarmonyPatches,
new[] { new ShrinkModDependency("demo.data", "1.0", optional: false) });
var code = original.WithMetadata(info, original.Revision,
() => new PackageWrapper(original.Factory()));
var dataInfo = new ShrinkModInfo("demo.data", "数据包", "1.0", 0,
entryType: null, autoApplyHarmonyPatches: false,
dependencies: Array.Empty<ShrinkModDependency>());
var data = ShrinkModComponentSource.CreateDataOnly(dataInfo, "manifest-sha256");
ShrinkModLoader.LoadComposed(settings, new[] { code, data });
sealed class PackageWrapper : ShrinkModBase, IShrinkModWrapper
{
public PackageWrapper(IShrinkMod instance) => PublicInstance = instance;
public IShrinkMod PublicInstance { get; }
public override void OnConstruct(ShrinkModContext ctx) => PublicInstance.OnConstruct(ctx);
public override void OnRegisterContent(ShrinkModContext ctx) => PublicInstance.OnRegisterContent(ctx);
public override void OnInitialize(ShrinkModContext ctx) => PublicInstance.OnInitialize(ctx);
public override void OnReady(ShrinkModContext ctx) => PublicInstance.OnReady(ctx);
}
包装器可以在各阶段加入宿主作用域、数据注册和 EffectInverse。Handle.Instance 保持原有
IShrinkMod 类型,返回 IShrinkModWrapper.PublicInstance,自动 EventBus 绑定也使用这个真实实例。
CreateDataOnly 的默认工厂不执行业务,公开实例为 null;传入自定义 Func<IShrinkMod> 可处理数据注册。
数据包的 EntryType 必须为 null,框架不会尝试为它应用 Harmony 补丁。
LoadComposed 始终使用可逆 ContextHost,与 useContextHost 的旧 Loader 开关无关。每个模组按
依赖/排序关系执行 Construct → RegisterContent → Initialize → Ready;成功后设置 IsLoaded,
对新建或重建的 handle 发送 OnModReady,再发送 OnAllModsReady。失败恢复旧组合及 DLL revision。
相同 revision、元数据和排序前驱保持幂等;工厂行为或清单数据改变时,宿主应更新 revision。
WithMetadata 保留发现快照,修改外部源时应使用它。不要把不同扫描中同一路径的不同 revision 混进一次提交。
热更新时重新发现、重新按清单组合,再调用 LoadComposed;删除源即卸载,空列表卸载全部实例。
宿主组合模式下 LoadNewExternalMods 保留当前组合,内置 watcher 不会丢弃清单元数据;宿主负责触发重组。
已有 LoadAuthorized、普通 ContextHost 和旧 Loader 的入口保持可用,不混用旧 Loader 实例与宿主组合。
高级异步宿主可调用 ShrinkModCordisRuntime.ApplyComposedAsync(settings, sources),它与既有
ApplyDiscoveredAsync 一样仅负责根事务;需要 Loader 状态与静态通知时使用 LoadComposed。
依赖区间和排序
[ShrinkModDependency("demo.core", optional: false,
VersionRange = "[1.0,2.0)", Ordering = ShrinkModDependencyOrdering.After)]
[ShrinkModDependency("demo.ui", optional: true,
Ordering = ShrinkModDependencyOrdering.Before)]
public sealed class MyMod : ShrinkModBase { }
var dependency = new ShrinkModDependency("demo.core", minimumVersion: null,
optional: true, ordering: ShrinkModDependencyOrdering.After, versionRange: "[1.2,)");
bool matches = ShrinkModVersionRange.Parse("[1.0,2.0)").Contains("1.5.0");
区间支持开闭边界、无上/下界、精确值 [1.2] 和并集 (,1.0],[1.2,);裸版本 1.0
表示 >= 1.0。数字版本的尾部零等价(1.2 == 1.2.0),带后缀版本沿用框架版本比较规则,
不承诺 Maven 的全部 qualifier 排序规则。无效区间、范围不满足、必选依赖缺失和排序环均明确报错。
optional 只决定目标能否缺席;目标存在时仍检查区间。None 不添加先后边,Before 让本模组
先于目标,After 让目标先于本模组。若只想排序且不要求安装目标,使用 optional: true。
原 minimumVersion 参数与三参数构造函数保留,未指定排序时仍为 After;VersionRange 优先于
MinimumVersion。旧 Loader 的增量路径无法把新模组插到已运行模组之前,会明确要求改用 ContextHost。
按清单约束程序集解析
在发现之前设置策略;宿主把程序集简单名称、授权绝对路径映射回自己的 packageId:
ShrinkModLoader.AssemblyResolvePolicy = (requestingAssembly, requestedName, candidatePath) =>
{
if (requestingAssembly == null) return false;
if (!assemblyOwners.TryGetValue(requestingAssembly.GetName().Name, out var requesterId)) return false;
if (!pathOwners.TryGetValue(candidatePath, out var candidateId)) return false;
return requesterId == candidateId || declaredDependencies[requesterId].Contains(candidateId);
};
assemblyOwners、pathOwners 和 declaredDependencies 由宿主清单建立。回调在返回缓存候选或加载
候选字节前执行,返回 false 拒绝本次解析;候选始终来自授权白名单,策略不能扩大白名单。
未设置策略时保持原行为。入口 DLL 文件名应与程序集简单名称一致。策略仅约束框架的
AssemblyResolve 回调,不能拦截 CLR 已解析的程序集引用,也不是不可信代码沙箱。
显式绑定生成订阅
宿主组合在 RegisterContent 后自动为公开实例调用以下入口,并将 Dispose 登记为可逆效应; Initialize/Ready 失败、替换或卸载都会退订。自动路径不需要包装器再手动绑定同一个实例。 如果需要手动绑定其它订阅对象:
var binding = ShrinkModOptionalRuntimeIntegration.TryAttachEventBusInstance(
subscriber.GetType(), subscriber, context.ModInfo.ModId, verboseLogging: false);
if (binding != null) context.EffectInverse(binding.Dispose);
返回值为 IDisposable;没有生成合同或可选绑定失败时返回 null。它使用 ShrinkBusKey.Mod(modId),
不会反射枚举订阅方法。手动调用方负责释放;ContextHost 自动路径还会在退役时移除对应 Mod Bus。
共享 NuGet ShrinkSDK.ModFramework 0.3.8 包含依赖元数据和范围解析;上述 Unity 发现/ContextHost API
属于 UPM Runtime。Godot 适配包独立版本为 0.1.7,本次仅更新它对共享包的发布引用。
配置文件
菜单 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
- 外部 DLL 常驻 revision 的软阈值;达到阈值时提示执行 Domain Reload 或重启进程,默认
enableHarmonyPatching- 是否启用 Harmony 自动补丁
enableNetworkSync- 是否启用网络同步框架
目录结构
当前 Assets/Modules/ShrinkModFramework/Runtime/ 按职责拆成:
Bootstrap/- 启动入口、运行时驱动、设置资产
Core/- 模组实例生命周期核心类型
Metadata/- 模组特性、依赖、状态、版本信息
Registry/- 模组注册表与注册表管理器
Loading/- 模组发现、装载、外部 DLL 与 Harmony 接入
Network/- 模组网络抽象层
Integration/- 对现有 Shrink 模块的可选自动接入胶水
推荐阅读顺序:
Bootstrap/Core/+Metadata/Loading/Registry/+Network/+Integration/
另外还有一层编辑器脚手架:
Editor/Scaffolding/ShrinkExternalModTemplateGenerator.csExternalModProjectTemplate/
这层专门给模组开发者生成外部 DLL 模组模板,不参与运行时装载。
模组定义
基础模组
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("初始化完成");
}
}
声明依赖
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 之后初始化");
}
}
关闭模组自动补丁
[ShrinkMod("demo.safe", "Demo Safe", "1.0.0", AutoApplyHarmonyPatches = false)]
public class DemoSafeMod : ShrinkModBase
{
}
生命周期
每个模组按依赖顺序进入以下阶段:
OnConstructOnRegisterContentOnInitializeOnReady
推荐职责:
OnConstruct- 初始化本模组运行时对象
OnRegisterContent- 向注册表注册物品、方块、能力、配方等
OnInitialize- 注册事件、接线系统、绑定网络频道
OnReady- 做依赖其他模组最终状态的收尾逻辑
外部 DLL 模组热加载
框架不会递归扫描模组目录。调用方必须把每个允许进入 AppDomain 的入口 DLL 绝对路径
作为白名单提交给 LoadAuthorized;目录中未列出的 DLL 不会被读取或加载。
ContextLoader 装载规则:
- 以 DLL 内容 SHA-256 作为 revision;同 revision 幂等,不触发重载
- 文件变更会加载新程序集并提交新的模组组件源;旧程序集仍驻留,但旧 fiber 的效应会先回滚
- 新模组任何阶段失败时,Host 会重新协调旧 source;旧注册表内容和已登记效应恢复
- 文件删除会移除当前 source 并卸载模组实例;不会宣称程序集已从 AppDomain 卸载
- 不支持 IL2CPP Player 动态程序集加载
- 支持同目录依赖程序集解析
首次安全加载:
ShrinkModRuntimeBootstrap.InitializeDriver(settings);
ShrinkModLoader.LoadAuthorized(settings, authorizedDllPaths);
后续可调用 ShrinkModLoader.LoadNewExternalMods(settings) 重新读取同一白名单的 revision,
把新增、替换、删除映射为完整期望组合;变更事务失败时保留旧模组组合。设置
useContextHost = false 时,旧路径同样只读取显式白名单,但仍保持只增不减的兼容语义。
如果 watchExternalModsDirectory = true,框架会监听目录变化并在主线程 debouncer 后重新提交
既有白名单;未授权文件即使触发通知也不会被加载。同一 burst 内的中间坏文件不会覆盖当前有效 revision。
常驻 revision 诊断
Unity/Mono 不能从当前 AppDomain 单独卸载已载入程序集。框架保留当前 revision 的生效语义,同时通过以下接口暴露实际常驻情况:
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放进项目环境或外部模组依赖中
注册表
注册内容
var key = context.RegisterContent("items", "sword", "Sword");
// key == "demo.core:sword"
基础注册不接受任意完整键。框架始终使用当前 ModId 生成 <owner>:<localKey>,避免模组误写其它 owner 的 namespace。
覆盖已有内容
context.OverrideContent(
"items",
"core:iron_sword",
priority: 100,
value: "Overridden Sword");
读取内容
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返回未应用覆盖的基础项- 注册表按类型和名称双重隔离
网络同步
框架内置的是网络同步抽象层,不是具体联网库。
核心接口:
IShrinkModNetworkTransportShrinkModNetworkManager
这意味着你可以把它接到:
- NGO
- Mirror
- FishNet
- 自己的 Socket/Relay 层
注册消息处理器
context.RegisterNetworkHandler<int>("sync.hp", (messageContext, value) =>
{
Debug.Log($"收到 HP:{value}");
});
发送消息
context.SendToServer("sync.hp", 100);
context.SendToAllClients("sync.hp", 100);
context.SendToClient("sync.hp", 100, "client-1");
绑定传输层
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());
与现有模块协作
当前仓库里已经有这些可复用模块:
ShrinkEventBusShrinkDataSaverShrinkCommandShrinkCommand.Integration.EventBusShrinkCommand.Integration.NetworkShrinkNetworkShrinkNetwork.Integration.EventBusShrinkDataSaver.Integration.EventBus
ShrinkModFramework 当前的定位不是“重新包一层这些模块”,而是给模组提供统一生命周期,然后在合适阶段把模组实例接到这些模块现成的注册入口上。
自动接入了什么
从当前版本开始,模组经过:
OnConstructOnRegisterContent
之后,框架会自动尝试把模组实例本身接入这些现有模块:
- 如果模组类带
[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 的静态自动扫描。
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.mdbuild.ps1.gitignoreglobal.json__PROJECT_NAME__.csprojsrc/__PROJECT_NAME__Mod.cssrc/__PROJECT_NAME__Contracts.cs
模板工程默认引用当前仓库里的:
ShrinkModFramework.Runtime.csprojShrinkEventBus.Runtime.csprojShrinkCommand.Runtime.csprojShrinkNetwork.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 解析失败。
建议流程:
- 你自己在当前仓库开发时,优先用
ShrinkSDK/模组/生成仓库内模板(推荐) - 先改
modId、命名空间、命令路径、opcode - 在当前 Unity/IDE 里直接开发和编译
- 如果要发给外部模组开发者,改用
ShrinkSDK/模组/导出开发包
如果你已经知道目标 Mods 目录,也可以直接跑模板里的:
.\build.ps1 -ModsDir "D:\YourGame\Mods"
设计约束
- 自动发现的模组类必须有
[ShrinkMod];宿主组合使用显式元数据和工厂 - 一个模组类必须实现
IShrinkMod - 自动发现的模组类必须能被无参构造;宿主工厂可自行构造包装器
- 模组 ID 必须全局唯一
- 必选依赖缺失或版本不满足时,装载直接失败
- 检测到循环依赖时,装载直接失败
- 外部 DLL 支持当前组合的添加、替换与删除;已载入的程序集 revision 仍常驻,回滚和卸载单位是模组 fiber 与其效应
关键文件
Runtime/Bootstrap/ShrinkModRuntimeBootstrap.csRuntime/Loading/ShrinkModLoader.csRuntime/Loading/ShrinkExternalModAssemblyLoader.csRuntime/Loading/ShrinkHarmonyPatchService.csRuntime/Registry/ShrinkModRegistry.csRuntime/Network/ShrinkModNetworkManager.csRuntime/Network/IShrinkModNetworkTransport.csRuntime/Integration/ShrinkModOptionalRuntimeIntegration.cs
结论
现在这套框架已经从“只能在工程内静态发现模组”的骨架,升级成了:
- 可显式托管启动
- 可按精确白名单增量接入外部 DLL
- 可选 Harmony 补丁
- 可扩展的网络同步框架
如果继续往 Forge 靠,下一步最值得做的是:
- 模组配置系统
- 模组资源系统
- 模组命令系统
- 模组专属存档与同步策略
可复用能力
Capability: 模组发现、依赖、阶段生命周期、内容注册和外部程序集加载 Aliases: mod 插件 模组 模组退役 热加载 内容注册 Limits: 动态程序集不能假定全部卸载;宿主加载上下文须单独验证;不默认 NativeAOT Extension: 模组声明、阶段钩子与宿主加载接口 Evidence: ShrinkModContextHost