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

+5 -5
View File
@@ -19,9 +19,8 @@ namespace ShrinkModFramework
{
_host = host ?? throw new ArgumentNullException(nameof(host));
_source = source ?? throw new ArgumentNullException(nameof(source));
_inject = source.Info.Dependencies
.Where(dependency => !dependency.Optional)
.Select(dependency => ShrinkModContextHost.GetModKey(dependency.ModId))
_inject = host.GetPredecessors(source.Info.ModId)
.Select(ShrinkModContextHost.GetModKey)
.Distinct(StringComparer.Ordinal)
.ToArray();
_provide = new[] { ShrinkModContextHost.GetModKey(source.Info.ModId) };
@@ -37,7 +36,8 @@ namespace ShrinkModFramework
if (instance == null)
throw new InvalidOperationException($"Mod factory returned null: {_source.Info.ModId}");
var handle = new ShrinkModHandle(_source.Info, instance)
var publicInstance = instance is IShrinkModWrapper wrapper ? wrapper.PublicInstance : instance;
var handle = new ShrinkModHandle(_source.Info, publicInstance)
{
Generation = _host.NextGeneration()
};
@@ -70,7 +70,7 @@ namespace ShrinkModFramework
IDisposable eventBinding = null;
ctx.Effect(
() => eventBinding = ShrinkModOptionalRuntimeIntegration.TryAttachEventBusInstance(
instance.GetType(), instance, handle.Info.ModId, _host.VerboseLogging),
publicInstance?.GetType(), publicInstance, handle.Info.ModId, _host.VerboseLogging),
() =>
{
eventBinding?.Dispose();
+22 -2
View File
@@ -10,9 +10,29 @@ namespace ShrinkModFramework
/// 把工程内程序集和外部 DLL 的当前 revision 投影为延迟组件源。
/// 同一路径的旧外部程序集无法从 Mono AppDomain 卸载,但不会再进入候选集合。
/// </summary>
internal static class ShrinkModComponentDiscovery
public static class ShrinkModComponentDiscovery
{
public static IReadOnlyList<ShrinkModComponentSource> Discover(
ShrinkModFrameworkSettings settings, IEnumerable<string> authorizedDllPaths) =>
Discover(settings, settings != null && settings.verboseLogging, authorizedDllPaths);
/// <summary>只发现,不构造实例或提交 current revision。WithMetadata 可保留发现快照。</summary>
public static IReadOnlyList<ShrinkModComponentSource> Discover(
ShrinkModFrameworkSettings settings, bool verboseLogging, IEnumerable<string> authorizedDllPaths)
{
if (authorizedDllPaths == null) throw new ArgumentNullException(nameof(authorizedDllPaths));
var previous = ShrinkExternalModAssemblyLoader.CaptureState();
try
{
var sources = DiscoverCurrent(settings, verboseLogging, authorizedDllPaths);
var state = ShrinkExternalModAssemblyLoader.CaptureState();
foreach (var source in sources) source.DiscoveryState = state;
return sources;
}
finally { ShrinkExternalModAssemblyLoader.RestoreState(previous); }
}
internal static IReadOnlyList<ShrinkModComponentSource> DiscoverCurrent(
ShrinkModFrameworkSettings settings,
bool verboseLogging,
IEnumerable<string> authorizedDllPaths)
@@ -53,7 +73,7 @@ namespace ShrinkModFramework
var dependencies = type
.GetCustomAttributes<ShrinkModDependencyAttribute>(false)
.Select(attribute => new ShrinkModDependency(
attribute.ModId, attribute.MinimumVersion, attribute.Optional))
attribute.ModId, attribute.MinimumVersion, attribute.Optional, attribute.Ordering, attribute.VersionRange))
.ToArray();
var info = new ShrinkModInfo(
modAttribute.ModId,
+29 -1
View File
@@ -2,9 +2,15 @@ using System;
namespace ShrinkModFramework
{
/// <summary>宿主包装器执行生命周期;Handle 和自动订阅使用 PublicInstance。数据包可返回 null。</summary>
public interface IShrinkModWrapper : IShrinkMod
{
IShrinkMod PublicInstance { get; }
}
/// <summary>
/// 延迟模组组件源。Revision 是来源内容的稳定指纹(外部 DLL 可使用文件哈希);
/// 同一 ModId 的 revision/factory 变化由 ShrinkModContextHost 作为替换事务处理。
/// 同一 ModId 的 revision/metadata 变化由 ShrinkModContextHost 作为替换事务处理。
/// </summary>
public sealed class ShrinkModComponentSource
{
@@ -23,5 +29,27 @@ namespace ShrinkModFramework
public ShrinkModInfo Info { get; }
public string Revision { get; }
public Func<IShrinkMod> Factory { get; }
internal ShrinkExternalModAssemblyLoader.RevisionState DiscoveryState { get; set; }
/// <summary>替换清单元数据或生命周期工厂,同时保留授权发现的程序集 revision。</summary>
public ShrinkModComponentSource WithMetadata(ShrinkModInfo info, string revision,
Func<IShrinkMod> factory = null) =>
new ShrinkModComponentSource(info, revision, factory ?? Factory) { DiscoveryState = DiscoveryState };
/// <summary>创建无入口类型的数据包;可选生命周期工厂用于注册内容和可逆效应。</summary>
public static ShrinkModComponentSource CreateDataOnly(ShrinkModInfo info, string revision,
Func<IShrinkMod> factory = null)
{
if (info == null) throw new ArgumentNullException(nameof(info));
if (info.EntryType != null)
throw new ArgumentException("A data-only mod must have no entry type.", nameof(info));
return new ShrinkModComponentSource(info, revision, factory ?? (() => new EmptyDataMod()));
}
private sealed class EmptyDataMod : ShrinkModBase, IShrinkModWrapper
{
public IShrinkMod PublicInstance => null;
}
}
}
+21 -56
View File
@@ -33,6 +33,10 @@ namespace ShrinkModFramework
private long _generation;
private int _catalogGeneration;
private bool _applying;
private IReadOnlyList<ShrinkModComponentSource> _buildingSources = Array.Empty<ShrinkModComponentSource>();
internal IEnumerable<string> GetPredecessors(string modId) =>
ShrinkModDependencyGraph.Predecessors(modId, _buildingSources.Select(source => source.Info));
public ShrinkModContextHost(bool enableHarmonyPatching = true, bool verboseLogging = false)
{
@@ -64,7 +68,9 @@ namespace ShrinkModFramework
return "shrink.mod/" + modId.Trim();
}
public async UniTask ApplyAsync(IReadOnlyList<ShrinkModComponentSource> desiredSources)
public UniTask ApplyAsync(IReadOnlyList<ShrinkModComponentSource> desiredSources) => ApplyAsync(desiredSources, null);
internal async UniTask ApplyAsync(IReadOnlyList<ShrinkModComponentSource> desiredSources, Action beforeRestore)
{
if (desiredSources == null)
throw new ArgumentNullException(nameof(desiredSources));
@@ -95,8 +101,11 @@ namespace ShrinkModFramework
catch (Exception applyError)
{
Exception restoreError = null;
_currentSources.Clear();
_currentSources.AddRange(previous);
try
{
beforeRestore?.Invoke();
await Loader.ApplyAsync(BuildEntries(previous));
EnsureActive(previous);
}
@@ -143,6 +152,7 @@ namespace ShrinkModFramework
private IReadOnlyList<ShrinkLoaderEntry> BuildEntries(IEnumerable<ShrinkModComponentSource> sources)
{
_buildingSources = sources.ToArray();
var entries = new List<ShrinkLoaderEntry>();
foreach (var source in sources)
{
@@ -184,79 +194,34 @@ namespace ShrinkModFramework
throw new InvalidOperationException($"Duplicate mod source id: {source.Info.ModId}");
}
foreach (var source in sources)
{
foreach (var dependency in source.Info.Dependencies)
{
if (!map.TryGetValue(dependency.ModId, out var target))
{
if (!dependency.Optional)
throw new InvalidOperationException(
$"Mod {source.Info.ModId} is missing required dependency {dependency.ModId}.");
continue;
}
if (!string.IsNullOrEmpty(dependency.MinimumVersion) &&
ShrinkVersionUtility.Compare(target.Info.Version, dependency.MinimumVersion) < 0)
{
throw new InvalidOperationException(
$"Mod {source.Info.ModId} requires {dependency.ModId} >= {dependency.MinimumVersion}, " +
$"but found {target.Info.Version}.");
}
}
}
var result = new List<ShrinkModComponentSource>();
var visiting = new HashSet<string>(StringComparer.Ordinal);
var visited = new HashSet<string>(StringComparer.Ordinal);
foreach (var source in sources.OrderBy(item => item.Info.LoadOrder)
.ThenBy(item => item.Info.ModId, StringComparer.Ordinal))
{
Visit(source, map, visiting, visited, result);
}
return result;
return ShrinkModDependencyGraph.Order(sources.Select(source => source.Info))
.Select(info => map[info.ModId]).ToList();
}
private List<ShrinkModComponentSource> ReuseUnchangedSources(
IEnumerable<ShrinkModComponentSource> desired)
{
var desiredArray = desired.ToArray();
var currentById = _currentSources.ToDictionary(source => source.Info.ModId, StringComparer.Ordinal);
var normalized = new List<ShrinkModComponentSource>();
foreach (var source in desired)
foreach (var source in desiredArray)
{
if (currentById.TryGetValue(source.Info.ModId, out var current) &&
string.Equals(current.Revision, source.Revision, StringComparison.Ordinal))
string.Equals(current.Revision, source.Revision, StringComparison.Ordinal) &&
ShrinkModDependencyGraph.SameMetadata(current.Info, source.Info) &&
ShrinkModDependencyGraph.Predecessors(source.Info.ModId, _currentSources.Select(item => item.Info)).OrderBy(id => id)
.SequenceEqual(ShrinkModDependencyGraph.Predecessors(source.Info.ModId, desiredArray.Select(item => item.Info)).OrderBy(id => id)))
{
normalized.Add(current);
}
else
{
normalized.Add(source);
normalized.Add(ReferenceEquals(source, current)
? source.WithMetadata(source.Info, source.Revision) : source);
}
}
return normalized;
}
private static void Visit(ShrinkModComponentSource source,
IReadOnlyDictionary<string, ShrinkModComponentSource> map,
ISet<string> visiting,
ISet<string> visited,
ICollection<ShrinkModComponentSource> result)
{
if (visited.Contains(source.Info.ModId))
return;
if (!visiting.Add(source.Info.ModId))
throw new InvalidOperationException($"Circular mod dependency includes {source.Info.ModId}.");
foreach (var dependency in source.Info.Dependencies.OrderBy(item => item.ModId, StringComparer.Ordinal))
{
if (map.TryGetValue(dependency.ModId, out var target))
Visit(target, map, visiting, visited, result);
}
visiting.Remove(source.Info.ModId);
visited.Add(source.Info.ModId);
result.Add(source);
}
}
}
+38 -2
View File
@@ -37,9 +37,9 @@ namespace ShrinkModFramework
try
{
var authorizedPaths = authorizedDllPaths?.ToArray() ?? Array.Empty<string>();
var sources = ShrinkModComponentDiscovery.Discover(
var sources = ShrinkModComponentDiscovery.DiscoverCurrent(
settings, verboseLogging, authorizedPaths);
await _host.ApplyAsync(sources);
await _host.ApplyAsync(sources, () => ShrinkExternalModAssemblyLoader.RestoreState(revisionState));
}
catch
{
@@ -54,6 +54,42 @@ namespace ShrinkModFramework
return _host.Mods;
}
/// <summary>提交宿主组合;生命周期或发现失败时恢复之前的程序集 revision 和组件。</summary>
public static async UniTask<IReadOnlyDictionary<string, ShrinkModHandle>> ApplyComposedAsync(
ShrinkModFrameworkSettings settings, IReadOnlyList<ShrinkModComponentSource> sources)
{
if (sources == null) throw new ArgumentNullException(nameof(sources));
settings ??= ShrinkModFrameworkSettings.Instance;
var verbose = settings != null && settings.verboseLogging;
if (_host == null) _host = new ShrinkModContextHost(settings == null || settings.enableHarmonyPatching, verbose);
var previous = ShrinkExternalModAssemblyLoader.CaptureState();
try
{
var states = sources.Where(source => source != null && source.DiscoveryState != null)
.Select(source => source.DiscoveryState).Distinct().ToArray();
var composedState = new ShrinkExternalModAssemblyLoader.RevisionState();
foreach (var state in states)
{
foreach (var pair in state.Current)
{
if (composedState.Current.TryGetValue(pair.Key, out var other) && other.Revision != pair.Value.Revision)
throw new InvalidOperationException($"Conflicting discovered revisions for {pair.Key}.");
composedState.Current[pair.Key] = pair.Value;
}
foreach (var pair in state.Known) composedState.Known[pair.Key] = pair.Value;
}
ShrinkExternalModAssemblyLoader.RestoreState(composedState);
ShrinkModNetworkManager.Configure(settings == null || settings.enableNetworkSync, verbose);
await _host.ApplyAsync(sources, () => ShrinkExternalModAssemblyLoader.RestoreState(previous));
}
catch
{
ShrinkExternalModAssemblyLoader.RestoreState(previous);
throw;
}
return _host.Mods;
}
public static IReadOnlyShrinkModRegistry<T> GetOrCreateRegistry<T>(string name) =>
Host.GetOrCreateRegistry<T>(name);