feat: 公开宿主组合加载 API、Forge 风格依赖版本区间与加载顺序、程序集解析策略
Publish UPM package / publish (push) Successful in 3s
Publish NuGet packages / publish (push) Successful in 2m18s

- ShrinkModComponentDiscovery.Discover 公开:只发现、不执行生命周期
- ShrinkModLoader.LoadComposed / ShrinkModCordisRuntime.ApplyComposedAsync:宿主提供元数据组合加载,支持纯数据包与 IShrinkModWrapper 公开真实实例
- 公开 TryAttachEventBusInstance,随生命周期逆操作释放
- 依赖新增 VersionRange(Maven/Forge 区间语法)与 Ordering(Before/After)
- AssemblyResolvePolicy 可限制跨模组程序集解析
This commit is contained in:
cneicy committed 2026-11-10 07:39:22 +08:00
1 parent 5398036d3b
commit d15bf00512
29 files changed
+917 -132

No files matched your search

+110 -2
View File
@@ -54,6 +54,114 @@ ShrinkModLoader.LoadAuthorized(settings, authorizedDllPaths);
`ShrinkModBootstrap` 仍然保留,作为手动覆盖或调试入口,但不是必需品。
## 宿主清单组合(0.3.8)
Unity 宿主可以先发现授权 DLL,再用清单替换元数据和生命周期工厂,最后提交完整组合。
`Discover` 不构造模组、不运行生命周期、不改变已提交的 current revision;DLL 会进入 AppDomain,
外部源的 `Revision` 仍为 `SHA-256:入口类型全名`。工程内源仍按 `assemblyNamePrefixes` 筛选。
```csharp
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`。
### 依赖区间和排序
```csharp
[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:
```csharp
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 失败、替换或卸载都会退订。自动路径不需要包装器再手动绑定同一个实例。
如果需要手动绑定其它订阅对象:
```csharp
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/模组/创建设置` 会在
@@ -678,9 +786,9 @@ public sealed partial class DemoFullMod : ShrinkModBase
## 设计约束
- 一个模组类必须有 `[ShrinkMod]`
- 自动发现的模组类必须有 `[ShrinkMod]`;宿主组合使用显式元数据和工厂
- 一个模组类必须实现 `IShrinkMod`
- 一个模组类必须能被无参构造
- 自动发现的模组类必须能被无参构造;宿主工厂可自行构造包装器
- 模组 ID 必须全局唯一
- 必选依赖缺失或版本不满足时,装载直接失败
- 检测到循环依赖时,装载直接失败