feat: 公开宿主组合加载 API、Forge 风格依赖版本区间与加载顺序、程序集解析策略
- ShrinkModComponentDiscovery.Discover 公开:只发现、不执行生命周期 - ShrinkModLoader.LoadComposed / ShrinkModCordisRuntime.ApplyComposedAsync:宿主提供元数据组合加载,支持纯数据包与 IShrinkModWrapper 公开真实实例 - 公开 TryAttachEventBusInstance,随生命周期逆操作释放 - 依赖新增 VersionRange(Maven/Forge 区间语法)与 Ordering(Before/After) - AssemblyResolvePolicy 可限制跨模组程序集解析
This commit is contained in:
1 parent
5398036d3b
commit
d15bf00512
29 files changed
+917
-132
No files matched your search
@@ -19,6 +19,24 @@ namespace ShrinkModFramework
|
||||
private static readonly List<ShrinkModHandle> LoadSequence = new();
|
||||
private static readonly ShrinkModRegistryManager RegistryManager = new();
|
||||
private static string[] _authorizedDllPaths = Array.Empty<string>();
|
||||
private static bool _hostComposed;
|
||||
|
||||
/// <summary>可选解析策略:请求方程序集(可能为 null)、请求名称、候选授权 DLL 的绝对路径。
|
||||
/// 只约束本框架 AssemblyResolve 回调,不是 CLR 已加载程序集的访问沙箱。</summary>
|
||||
public static Func<Assembly, AssemblyName, string, bool> AssemblyResolvePolicy { get; set; }
|
||||
|
||||
public static IReadOnlyDictionary<string, ShrinkModHandle> LoadComposed(
|
||||
ShrinkModFrameworkSettings settings, IReadOnlyList<ShrinkModComponentSource> sources)
|
||||
{
|
||||
if (sources == null) throw new ArgumentNullException(nameof(sources));
|
||||
if (LoadedMods.Count != 0) throw new InvalidOperationException("Cannot compose over legacy loaded mods.");
|
||||
var previous = Mods.ToDictionary(pair => pair.Key, pair => pair.Value.Generation, StringComparer.Ordinal);
|
||||
var result = ShrinkModCordisRuntime.ApplyComposedAsync(settings, sources).GetAwaiter().GetResult();
|
||||
_hostComposed = true;
|
||||
PublishReady(result, previous);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public static bool IsLoaded { get; private set; }
|
||||
public static IReadOnlyDictionary<string, ShrinkModHandle> Mods =>
|
||||
@@ -48,6 +66,7 @@ namespace ShrinkModFramework
|
||||
ShrinkModFrameworkSettings settings,
|
||||
IEnumerable<string> authorizedDllPaths)
|
||||
{
|
||||
if (_hostComposed) throw new InvalidOperationException("Use LoadComposed to update a host composition.");
|
||||
settings ??= ShrinkModFrameworkSettings.Instance;
|
||||
_authorizedDllPaths = authorizedDllPaths
|
||||
.Where(path => !string.IsNullOrWhiteSpace(path))
|
||||
@@ -97,6 +116,7 @@ namespace ShrinkModFramework
|
||||
|
||||
public static IReadOnlyDictionary<string, ShrinkModHandle> LoadNewExternalMods(ShrinkModFrameworkSettings settings = null)
|
||||
{
|
||||
if (_hostComposed) return Mods; // A watcher cannot reconstruct host manifest metadata.
|
||||
settings ??= ShrinkModFrameworkSettings.Instance;
|
||||
if (settings == null || settings.useContextHost)
|
||||
return ApplyContextComposition(settings, _authorizedDllPaths);
|
||||
@@ -173,6 +193,8 @@ namespace ShrinkModFramework
|
||||
ShrinkExternalModAssemblyLoader.ResetForTesting();
|
||||
ShrinkHarmonyPatchService.ResetForTesting();
|
||||
_authorizedDllPaths = Array.Empty<string>();
|
||||
_hostComposed = false;
|
||||
AssemblyResolvePolicy = null;
|
||||
}
|
||||
|
||||
internal static void ResetForDomainReload()
|
||||
@@ -189,6 +211,8 @@ namespace ShrinkModFramework
|
||||
ShrinkExternalModAssemblyLoader.ResetForDomainReload();
|
||||
ShrinkHarmonyPatchService.ResetForTesting();
|
||||
_authorizedDllPaths = Array.Empty<string>();
|
||||
_hostComposed = false;
|
||||
AssemblyResolvePolicy = null;
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, ShrinkModHandle> ApplyContextComposition(
|
||||
@@ -204,6 +228,13 @@ namespace ShrinkModFramework
|
||||
.ApplyDiscoveredAsync(settings, authorizedDllPaths)
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
PublishReady(result, previousGenerations);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void PublishReady(IReadOnlyDictionary<string, ShrinkModHandle> result,
|
||||
IReadOnlyDictionary<string, long> previousGenerations)
|
||||
{
|
||||
IsLoaded = true;
|
||||
foreach (var pair in result.OrderBy(pair => pair.Key, StringComparer.Ordinal))
|
||||
{
|
||||
@@ -214,7 +245,6 @@ namespace ShrinkModFramework
|
||||
}
|
||||
}
|
||||
OnAllModsReady?.Invoke(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ShrinkModHandle InstantiateAndTrack(DiscoveredMod discoveredMod,
|
||||
@@ -267,7 +297,7 @@ namespace ShrinkModFramework
|
||||
|
||||
var dependencies = type
|
||||
.GetCustomAttributes<ShrinkModDependencyAttribute>(false)
|
||||
.Select(attr => new ShrinkModDependency(attr.ModId, attr.MinimumVersion, attr.Optional))
|
||||
.Select(attr => new ShrinkModDependency(attr.ModId, attr.MinimumVersion, attr.Optional, attr.Ordering, attr.VersionRange))
|
||||
.ToArray();
|
||||
|
||||
results.Add(new DiscoveredMod
|
||||
@@ -349,55 +379,20 @@ namespace ShrinkModFramework
|
||||
|
||||
private static void ValidateDependencyVersion(ShrinkModInfo owner, ShrinkModDependency dependency, string actualVersion)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(dependency.MinimumVersion) &&
|
||||
ShrinkVersionUtility.Compare(actualVersion, dependency.MinimumVersion) < 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"模组 {owner.ModId} 依赖 {dependency.ModId} >= {dependency.MinimumVersion},但当前只有 {actualVersion}");
|
||||
}
|
||||
ShrinkModDependencyGraph.ValidateVersion(owner, dependency, actualVersion);
|
||||
}
|
||||
|
||||
private static List<DiscoveredMod> ResolveLoadOrder(List<DiscoveredMod> discovered, bool allowExistingLoadedDependencies)
|
||||
{
|
||||
var map = discovered.ToDictionary(x => x.Info.ModId, x => x, StringComparer.Ordinal);
|
||||
var result = new List<DiscoveredMod>();
|
||||
var visiting = new HashSet<string>(StringComparer.Ordinal);
|
||||
var visited = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var mod in discovered.OrderBy(x => x.Info.LoadOrder).ThenBy(x => x.Info.ModId, StringComparer.Ordinal))
|
||||
Visit(mod, map, visiting, visited, result, allowExistingLoadedDependencies);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void Visit(DiscoveredMod current, Dictionary<string, DiscoveredMod> map,
|
||||
HashSet<string> visiting, HashSet<string> visited, List<DiscoveredMod> result,
|
||||
bool allowExistingLoadedDependencies)
|
||||
{
|
||||
if (visited.Contains(current.Info.ModId))
|
||||
return;
|
||||
|
||||
if (!visiting.Add(current.Info.ModId))
|
||||
throw new InvalidOperationException($"检测到模组循环依赖,涉及 {current.Info.ModId}");
|
||||
|
||||
foreach (var dependency in current.Info.Dependencies.OrderBy(x => x.ModId, StringComparer.Ordinal))
|
||||
{
|
||||
if (map.TryGetValue(dependency.ModId, out var target))
|
||||
{
|
||||
Visit(target, map, visiting, visited, result, allowExistingLoadedDependencies);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (allowExistingLoadedDependencies && LoadedMods.ContainsKey(dependency.ModId))
|
||||
continue;
|
||||
|
||||
if (!dependency.Optional)
|
||||
throw new InvalidOperationException($"模组 {current.Info.ModId} 缺少必选依赖 {dependency.ModId}");
|
||||
}
|
||||
|
||||
visiting.Remove(current.Info.ModId);
|
||||
visited.Add(current.Info.ModId);
|
||||
result.Add(current);
|
||||
var map = discovered.ToDictionary(mod => mod.Info.ModId, StringComparer.Ordinal);
|
||||
var infos = discovered.Select(mod => mod.Info).ToList();
|
||||
if (allowExistingLoadedDependencies)
|
||||
infos.AddRange(LoadedMods.Values.Where(handle => !map.ContainsKey(handle.Info.ModId)).Select(handle => handle.Info));
|
||||
var ordered = ShrinkModDependencyGraph.Order(infos);
|
||||
foreach (var loaded in LoadedMods.Keys)
|
||||
if (ShrinkModDependencyGraph.Predecessors(loaded, infos).Any(map.ContainsKey))
|
||||
throw new InvalidOperationException($"Cannot load a new mod before already loaded mod {loaded}; use ContextHost.");
|
||||
return ordered.Where(info => map.ContainsKey(info.ModId)).Select(info => map[info.ModId]).ToList();
|
||||
}
|
||||
|
||||
private static IShrinkMod CreateInstance(DiscoveredMod discovered)
|
||||
|
||||
Reference in new issue
Block a user