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
@@ -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