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,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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ using UnityEngine;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
internal static class ShrinkModOptionalRuntimeIntegration
|
||||
public static class ShrinkModOptionalRuntimeIntegration
|
||||
{
|
||||
public static IDisposable RegisterModInstance(IShrinkMod modInstance, ShrinkModInfo modInfo,
|
||||
internal static IDisposable RegisterModInstance(IShrinkMod modInstance, ShrinkModInfo modInfo,
|
||||
bool verboseLogging)
|
||||
{
|
||||
if (modInstance == null || modInfo == null)
|
||||
@@ -21,11 +21,12 @@ namespace ShrinkModFramework
|
||||
return eventBinding;
|
||||
}
|
||||
|
||||
public static void RefreshGlobalBindings(bool verboseLogging)
|
||||
internal static void RefreshGlobalBindings(bool verboseLogging)
|
||||
{
|
||||
TryRefreshNetworkEventBusBridge(verboseLogging);
|
||||
}
|
||||
|
||||
/// <summary>绑定生成的实例订阅。调用方须 Dispose 返回值,或通过 context.EffectInverse 登记释放。</summary>
|
||||
public static IDisposable TryAttachEventBusInstance(Type modType, object modInstance,
|
||||
string modId, bool verboseLogging)
|
||||
{
|
||||
|
||||
@@ -295,7 +295,7 @@ namespace ShrinkModFramework
|
||||
}
|
||||
}
|
||||
|
||||
private static Assembly OnAssemblyResolve(object sender, ResolveEventArgs args)
|
||||
internal static Assembly OnAssemblyResolve(object sender, ResolveEventArgs args)
|
||||
{
|
||||
var requestedName = new AssemblyName(args.Name).Name;
|
||||
if (string.IsNullOrWhiteSpace(requestedName))
|
||||
@@ -304,6 +304,10 @@ namespace ShrinkModFramework
|
||||
if (!KnownAssemblyFiles.TryGetValue(requestedName, out var path) || !File.Exists(path))
|
||||
return null;
|
||||
|
||||
if (ShrinkModLoader.AssemblyResolvePolicy != null &&
|
||||
!ShrinkModLoader.AssemblyResolvePolicy(args.RequestingAssembly, new AssemblyName(args.Name), path))
|
||||
return null;
|
||||
|
||||
lock (ResolveLock)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -88,7 +88,7 @@ namespace ShrinkModFramework
|
||||
private static IDisposable ApplyPatchesCore(ShrinkModInfo modInfo, bool enableHarmonyPatching,
|
||||
bool verboseLogging, bool reversible)
|
||||
{
|
||||
if (!enableHarmonyPatching || !modInfo.AutoApplyHarmonyPatches)
|
||||
if (!enableHarmonyPatching || !modInfo.AutoApplyHarmonyPatches || modInfo.EntryType == null)
|
||||
return EmptyLease.Instance;
|
||||
|
||||
var harmonyType = Type.GetType("HarmonyLib.Harmony, 0Harmony");
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -2,12 +2,16 @@ using System;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
public enum ShrinkModDependencyOrdering { None, Before, After }
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
|
||||
public sealed class ShrinkModDependencyAttribute : Attribute
|
||||
{
|
||||
public string ModId { get; }
|
||||
public string MinimumVersion { get; }
|
||||
public bool Optional { get; }
|
||||
public string VersionRange { get; set; }
|
||||
public ShrinkModDependencyOrdering Ordering { get; set; } = ShrinkModDependencyOrdering.After;
|
||||
|
||||
public ShrinkModDependencyAttribute(string modId, string minimumVersion = null, bool optional = false)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
internal static class ShrinkModDependencyGraph
|
||||
{
|
||||
internal static void ValidateVersion(ShrinkModInfo owner, ShrinkModDependency dependency, string actual)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(dependency.VersionRange) && !ShrinkModVersionRange.Parse(dependency.VersionRange).Contains(actual))
|
||||
throw new InvalidOperationException($"Mod {owner.ModId} requires {dependency.ModId} in range {dependency.VersionRange}, but found {actual}.");
|
||||
}
|
||||
|
||||
internal static IEnumerable<string> Predecessors(string id, IEnumerable<ShrinkModInfo> infos)
|
||||
{
|
||||
var map = infos.ToDictionary(info => info.ModId, StringComparer.Ordinal);
|
||||
foreach (var owner in map.Values)
|
||||
foreach (var dependency in owner.Dependencies)
|
||||
{
|
||||
if (!map.ContainsKey(dependency.ModId)) continue;
|
||||
if (owner.ModId == id && dependency.Ordering == ShrinkModDependencyOrdering.After)
|
||||
yield return dependency.ModId;
|
||||
if (dependency.ModId == id && dependency.Ordering == ShrinkModDependencyOrdering.Before)
|
||||
yield return owner.ModId;
|
||||
}
|
||||
}
|
||||
|
||||
internal static IReadOnlyList<ShrinkModInfo> Order(IEnumerable<ShrinkModInfo> infos)
|
||||
{
|
||||
var map = infos.ToDictionary(info => info.ModId, StringComparer.Ordinal);
|
||||
foreach (var owner in map.Values)
|
||||
foreach (var dependency in owner.Dependencies)
|
||||
{
|
||||
if (map.TryGetValue(dependency.ModId, out var target)) ValidateVersion(owner, dependency, target.Version);
|
||||
else if (!dependency.Optional)
|
||||
throw new InvalidOperationException($"Mod {owner.ModId} is missing required dependency {dependency.ModId}.");
|
||||
}
|
||||
var result = new List<ShrinkModInfo>();
|
||||
var visiting = new HashSet<string>(StringComparer.Ordinal);
|
||||
var visited = new HashSet<string>(StringComparer.Ordinal);
|
||||
void Visit(string id)
|
||||
{
|
||||
if (visited.Contains(id)) return;
|
||||
if (!visiting.Add(id)) throw new InvalidOperationException($"Circular mod dependency/Before/After ordering includes {id}.");
|
||||
foreach (var predecessor in Predecessors(id, map.Values).OrderBy(value => value, StringComparer.Ordinal)) Visit(predecessor);
|
||||
visiting.Remove(id); visited.Add(id); result.Add(map[id]);
|
||||
}
|
||||
foreach (var info in map.Values.OrderBy(info => info.LoadOrder).ThenBy(info => info.ModId, StringComparer.Ordinal)) Visit(info.ModId);
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static bool SameMetadata(ShrinkModInfo a, ShrinkModInfo b) =>
|
||||
a.ModId == b.ModId && a.DisplayName == b.DisplayName && a.Version == b.Version &&
|
||||
a.LoadOrder == b.LoadOrder && a.EntryType == b.EntryType && a.AutoApplyHarmonyPatches == b.AutoApplyHarmonyPatches &&
|
||||
a.Dependencies.Count == b.Dependencies.Count && a.Dependencies.Zip(b.Dependencies, (x, y) =>
|
||||
x.ModId == y.ModId && x.VersionRange == y.VersionRange && x.Optional == y.Optional && x.Ordering == y.Ordering).All(equal => equal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 343ada1a71214620a6c0ca56a56ff70d
|
||||
@@ -23,7 +23,9 @@ namespace ShrinkModFramework
|
||||
LoadOrder = loadOrder;
|
||||
EntryType = entryType;
|
||||
AutoApplyHarmonyPatches = autoApplyHarmonyPatches;
|
||||
Dependencies = dependencies;
|
||||
Dependencies = dependencies == null
|
||||
? Array.Empty<ShrinkModDependency>()
|
||||
: new List<ShrinkModDependency>(dependencies).AsReadOnly();
|
||||
}
|
||||
|
||||
public override string ToString() => $"{DisplayName} ({ModId}@{Version})";
|
||||
@@ -35,20 +37,27 @@ namespace ShrinkModFramework
|
||||
public string ModId { get; }
|
||||
public string MinimumVersion { get; }
|
||||
public bool Optional { get; }
|
||||
public string VersionRange { get; }
|
||||
public ShrinkModDependencyOrdering Ordering { get; }
|
||||
|
||||
public ShrinkModDependency(string modId, string minimumVersion, bool optional)
|
||||
: this(modId, minimumVersion, optional, ShrinkModDependencyOrdering.After, null) { }
|
||||
|
||||
public ShrinkModDependency(string modId, string minimumVersion, bool optional,
|
||||
ShrinkModDependencyOrdering ordering = ShrinkModDependencyOrdering.After, string versionRange = null)
|
||||
{
|
||||
ModId = modId;
|
||||
if (string.IsNullOrWhiteSpace(modId)) throw new ArgumentException("ModId must not be empty.", nameof(modId));
|
||||
if (!Enum.IsDefined(typeof(ShrinkModDependencyOrdering), ordering))
|
||||
throw new ArgumentOutOfRangeException(nameof(ordering));
|
||||
ModId = modId.Trim();
|
||||
MinimumVersion = minimumVersion;
|
||||
Optional = optional;
|
||||
VersionRange = versionRange ?? minimumVersion;
|
||||
Ordering = ordering;
|
||||
ShrinkModVersionRange.Parse(VersionRange);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (string.IsNullOrEmpty(MinimumVersion))
|
||||
return Optional ? $"{ModId} (optional)" : ModId;
|
||||
|
||||
return Optional ? $"{ModId} >= {MinimumVersion} (optional)" : $"{ModId} >= {MinimumVersion}";
|
||||
}
|
||||
public override string ToString() =>
|
||||
$"{ModId} {VersionRange ?? "*"} ({Ordering}{(Optional ? ", optional" : "")})";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
/// <summary>Maven 风格区间/并集;裸版本表示最低版本。版本比较沿用框架规则,数字尾部零等价。</summary>
|
||||
public sealed class ShrinkModVersionRange
|
||||
{
|
||||
private readonly List<Func<string, bool>> _ranges = new();
|
||||
private ShrinkModVersionRange() { }
|
||||
|
||||
public static ShrinkModVersionRange Parse(string expression)
|
||||
{
|
||||
var result = new ShrinkModVersionRange();
|
||||
if (string.IsNullOrWhiteSpace(expression)) return result;
|
||||
var text = expression.Trim();
|
||||
if (text[0] != '[' && text[0] != '(')
|
||||
{
|
||||
ValidateVersion(text);
|
||||
result._ranges.Add(version => ShrinkVersionUtility.Compare(version, text) >= 0);
|
||||
return result;
|
||||
}
|
||||
while (text.Length > 0)
|
||||
{
|
||||
var end = text.IndexOfAny(new[] { ']', ')' });
|
||||
if (end < 0 || (text[0] != '[' && text[0] != '(')) throw Invalid(expression);
|
||||
var lowerInclusive = text[0] == '[';
|
||||
var upperInclusive = text[end] == ']';
|
||||
var bounds = text.Substring(1, end - 1).Split(',');
|
||||
string lower, upper;
|
||||
if (bounds.Length == 1)
|
||||
{
|
||||
if (!lowerInclusive || !upperInclusive) throw Invalid(expression);
|
||||
lower = upper = bounds[0].Trim();
|
||||
ValidateVersion(lower);
|
||||
}
|
||||
else if (bounds.Length == 2)
|
||||
{
|
||||
lower = bounds[0].Trim(); upper = bounds[1].Trim();
|
||||
if (lower.Length > 0) ValidateVersion(lower);
|
||||
if (upper.Length > 0) ValidateVersion(upper);
|
||||
if ((lower.Length == 0 && lowerInclusive) || (upper.Length == 0 && upperInclusive))
|
||||
throw Invalid(expression);
|
||||
if (lower.Length > 0 && upper.Length > 0)
|
||||
{
|
||||
var comparison = ShrinkVersionUtility.Compare(lower, upper);
|
||||
if (comparison > 0 || (comparison == 0 && (!lowerInclusive || !upperInclusive))) throw Invalid(expression);
|
||||
}
|
||||
}
|
||||
else throw Invalid(expression);
|
||||
result._ranges.Add(version =>
|
||||
(lower.Length == 0 || (lowerInclusive ? ShrinkVersionUtility.Compare(version, lower) >= 0 : ShrinkVersionUtility.Compare(version, lower) > 0)) &&
|
||||
(upper.Length == 0 || (upperInclusive ? ShrinkVersionUtility.Compare(version, upper) <= 0 : ShrinkVersionUtility.Compare(version, upper) < 0)));
|
||||
text = text.Substring(end + 1).Trim();
|
||||
if (text.Length == 0) break;
|
||||
if (text[0] != ',' || text.Length == 1) throw Invalid(expression);
|
||||
text = text.Substring(1).Trim();
|
||||
if (text.Length == 0) throw Invalid(expression);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool Contains(string version)
|
||||
{
|
||||
ValidateVersion(version);
|
||||
return _ranges.Count == 0 || _ranges.Any(range => range(version));
|
||||
}
|
||||
|
||||
private static void ValidateVersion(string version)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(version) || !Regex.IsMatch(version, @"^[0-9]+(?:[.][0-9]+)*(?:[-+][A-Za-z0-9.-]+)?$"))
|
||||
throw Invalid(version);
|
||||
}
|
||||
private static FormatException Invalid(string value) => new FormatException($"Invalid mod version range: '{value}'.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c9d273c1b5d349a1822f3dfc3d8e6ff2
|
||||
@@ -23,6 +23,8 @@ namespace ShrinkModFramework
|
||||
return leftValue.CompareTo(rightValue);
|
||||
}
|
||||
|
||||
if (System.Text.RegularExpressions.Regex.IsMatch(left, @"^[0-9]+(?:[.][0-9]+)*$") &&
|
||||
System.Text.RegularExpressions.Regex.IsMatch(right, @"^[0-9]+(?:[.][0-9]+)*$")) return 0;
|
||||
return string.CompareOrdinal(left, right);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user