This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using ShrinkContext;
|
||||
using ShrinkEventBus;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
/// <summary>把 IShrinkMod 四阶段生命周期映射为一个可逆 Cordis 组件。</summary>
|
||||
internal sealed class ShrinkModComponent : IShrinkComponent
|
||||
{
|
||||
private readonly ShrinkModContextHost _host;
|
||||
private readonly ShrinkModComponentSource _source;
|
||||
private readonly string[] _inject;
|
||||
private readonly string[] _provide;
|
||||
|
||||
public ShrinkModComponent(ShrinkModContextHost host, ShrinkModComponentSource source)
|
||||
{
|
||||
_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))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
_provide = new[] { ShrinkModContextHost.GetModKey(source.Info.ModId) };
|
||||
}
|
||||
|
||||
public string Name => "shrink.mod/" + _source.Info.ModId;
|
||||
public IReadOnlyList<string> Inject => _inject;
|
||||
public IReadOnlyList<string> Provide => _provide;
|
||||
|
||||
public UniTask ApplyAsync(ShrinkCtx ctx, object config)
|
||||
{
|
||||
var instance = _source.Factory();
|
||||
if (instance == null)
|
||||
throw new InvalidOperationException($"Mod factory returned null: {_source.Info.ModId}");
|
||||
|
||||
var handle = new ShrinkModHandle(_source.Info, instance)
|
||||
{
|
||||
Generation = _host.NextGeneration()
|
||||
};
|
||||
|
||||
ctx.Effect(
|
||||
() => _host.RegisterHandle(handle),
|
||||
() =>
|
||||
{
|
||||
_host.UnregisterHandle(handle);
|
||||
handle.State = ShrinkModState.Unloaded;
|
||||
});
|
||||
|
||||
IDisposable harmonyLease = null;
|
||||
ctx.Effect(
|
||||
() => harmonyLease = ShrinkHarmonyPatchService.AcquirePatchesIfNeeded(
|
||||
handle.Info, _host.EnableHarmonyPatching, _host.VerboseLogging),
|
||||
() => harmonyLease?.Dispose());
|
||||
|
||||
var modContext = new ShrinkModContext(handle.Info, _host.RegistryManager,
|
||||
_host.Mods, _host.VerboseLogging, ctx);
|
||||
|
||||
instance.OnConstruct(modContext);
|
||||
handle.State = ShrinkModState.Constructed;
|
||||
|
||||
// 先登记归属清理,OnRegisterContent 中途失败也能撤回已注册的部分内容。
|
||||
ctx.Effect(() => { }, () => _host.RegistryManager.RemoveOwnedEntries(handle.Info.ModId));
|
||||
instance.OnRegisterContent(modContext);
|
||||
handle.State = ShrinkModState.ContentRegistered;
|
||||
|
||||
IDisposable eventBinding = null;
|
||||
ctx.Effect(
|
||||
() => eventBinding = ShrinkModOptionalRuntimeIntegration.TryAttachEventBusInstance(
|
||||
instance.GetType(), instance, handle.Info.ModId, _host.VerboseLogging),
|
||||
() =>
|
||||
{
|
||||
eventBinding?.Dispose();
|
||||
EventBus.RemoveBus(ShrinkBusKey.Mod(handle.Info.ModId));
|
||||
});
|
||||
|
||||
// 模组网络 handler 以 ModId 为归属键,可在失败或卸载时整体撤回。
|
||||
ctx.Effect(() => { }, () => ShrinkModNetworkManager.UnregisterHandlers(handle.Info.ModId));
|
||||
instance.OnInitialize(modContext);
|
||||
handle.State = ShrinkModState.Initialized;
|
||||
|
||||
instance.OnReady(modContext);
|
||||
handle.State = ShrinkModState.Ready;
|
||||
ctx.Set(_provide[0], handle);
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 167a3359a4a357a49ab81ba266caa07f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,122 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
/// <summary>
|
||||
/// 把工程内程序集和外部 DLL 的当前 revision 投影为延迟组件源。
|
||||
/// 同一路径的旧外部程序集无法从 Mono AppDomain 卸载,但不会再进入候选集合。
|
||||
/// </summary>
|
||||
internal static class ShrinkModComponentDiscovery
|
||||
{
|
||||
public static IReadOnlyList<ShrinkModComponentSource> Discover(ShrinkModFrameworkSettings settings,
|
||||
bool verboseLogging)
|
||||
{
|
||||
ShrinkExternalModAssemblyLoader.ScanExternalAssemblyRevisions(settings, verboseLogging);
|
||||
var results = new List<ShrinkModComponentSource>();
|
||||
var prefixes = settings?.assemblyNamePrefixes;
|
||||
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
if (ShrinkExternalModAssemblyLoader.IsExternalAssembly(assembly) &&
|
||||
!ShrinkExternalModAssemblyLoader.TryGetCurrentRevision(assembly, out _))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!ShouldScanAssembly(assembly, prefixes))
|
||||
continue;
|
||||
|
||||
var assemblyRevision = GetAssemblyRevision(assembly);
|
||||
foreach (var type in GetTypesSafely(assembly))
|
||||
{
|
||||
if (type == null || type.IsAbstract || type.IsInterface)
|
||||
continue;
|
||||
|
||||
var modAttribute = type.GetCustomAttribute<ShrinkModAttribute>(false);
|
||||
if (modAttribute == null)
|
||||
continue;
|
||||
if (!typeof(IShrinkMod).IsAssignableFrom(type))
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"[ShrinkModFramework] 类型 {type.FullName} 标记了 ShrinkMod,但没有实现 IShrinkMod,已跳过。");
|
||||
continue;
|
||||
}
|
||||
if (type.GetConstructor(Type.EmptyTypes) == null)
|
||||
throw new InvalidOperationException($"模组 {type.FullName} 缺少无参构造函数,无法实例化。");
|
||||
|
||||
var dependencies = type
|
||||
.GetCustomAttributes<ShrinkModDependencyAttribute>(false)
|
||||
.Select(attribute => new ShrinkModDependency(
|
||||
attribute.ModId, attribute.MinimumVersion, attribute.Optional))
|
||||
.ToArray();
|
||||
var info = new ShrinkModInfo(
|
||||
modAttribute.ModId,
|
||||
modAttribute.DisplayName,
|
||||
modAttribute.Version,
|
||||
modAttribute.LoadOrder,
|
||||
type,
|
||||
modAttribute.AutoApplyHarmonyPatches,
|
||||
dependencies);
|
||||
var revision = $"{assemblyRevision}:{type.FullName}";
|
||||
results.Add(new ShrinkModComponentSource(info, revision,
|
||||
() => (IShrinkMod)Activator.CreateInstance(type)));
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static string GetAssemblyRevision(Assembly assembly)
|
||||
{
|
||||
if (ShrinkExternalModAssemblyLoader.TryGetCurrentRevision(assembly, out var externalRevision))
|
||||
return externalRevision;
|
||||
|
||||
try
|
||||
{
|
||||
return assembly.ManifestModule.ModuleVersionId.ToString("N");
|
||||
}
|
||||
catch
|
||||
{
|
||||
return assembly.FullName ?? assembly.GetName().Name ?? "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ShouldScanAssembly(Assembly assembly, string[] prefixes)
|
||||
{
|
||||
var name = assembly.GetName().Name;
|
||||
if (string.IsNullOrEmpty(name))
|
||||
return false;
|
||||
if (name.StartsWith("Unity", StringComparison.Ordinal) ||
|
||||
name.StartsWith("System", StringComparison.Ordinal) ||
|
||||
name.StartsWith("mscorlib", StringComparison.Ordinal) ||
|
||||
name.StartsWith("netstandard", StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (prefixes == null || prefixes.Length == 0)
|
||||
return true;
|
||||
|
||||
return prefixes.Any(prefix => !string.IsNullOrWhiteSpace(prefix) &&
|
||||
name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static IEnumerable<Type> GetTypesSafely(Assembly assembly)
|
||||
{
|
||||
try
|
||||
{
|
||||
return assembly.GetTypes();
|
||||
}
|
||||
catch (ReflectionTypeLoadException exception)
|
||||
{
|
||||
return exception.Types.Where(type => type != null);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Array.Empty<Type>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 95e3bad3c1c50b44db3b144af1aafda8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
/// <summary>
|
||||
/// 延迟模组组件源。Revision 是来源内容的稳定指纹(外部 DLL 可使用文件哈希);
|
||||
/// 同一 ModId 的 revision/factory 变化由 ShrinkModContextHost 作为替换事务处理。
|
||||
/// </summary>
|
||||
public sealed class ShrinkModComponentSource
|
||||
{
|
||||
public ShrinkModComponentSource(ShrinkModInfo info, string revision, Func<IShrinkMod> factory)
|
||||
{
|
||||
Info = info ?? throw new ArgumentNullException(nameof(info));
|
||||
if (string.IsNullOrWhiteSpace(info.ModId))
|
||||
throw new ArgumentException("ModId must not be empty.", nameof(info));
|
||||
if (string.IsNullOrWhiteSpace(revision))
|
||||
throw new ArgumentException("Revision must not be empty.", nameof(revision));
|
||||
|
||||
Revision = revision.Trim();
|
||||
Factory = factory ?? throw new ArgumentNullException(nameof(factory));
|
||||
}
|
||||
|
||||
public ShrinkModInfo Info { get; }
|
||||
public string Revision { get; }
|
||||
public Func<IShrinkMod> Factory { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0878db300b244c645bdc69c5b96b70d5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,262 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using ShrinkContext;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
public sealed class ShrinkModTransactionException : Exception
|
||||
{
|
||||
public ShrinkModTransactionException(string message, Exception applyError, Exception restoreError = null)
|
||||
: base(message, restoreError == null ? applyError : new AggregateException(applyError, restoreError))
|
||||
{
|
||||
ApplyError = applyError;
|
||||
RestoreError = restoreError;
|
||||
}
|
||||
|
||||
public Exception ApplyError { get; }
|
||||
public Exception RestoreError { get; }
|
||||
public bool PreviousCompositionRestored => RestoreError == null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模组的 Cordis 组合宿主。配置应用是事务:新组合任一 fiber 失败时,重新协调到旧组件源;
|
||||
/// Mono/IL2CPP 下旧程序集仍驻留,但旧/新模组实例及其已追踪效应会被正确卸载或恢复。
|
||||
/// </summary>
|
||||
public sealed class ShrinkModContextHost
|
||||
{
|
||||
private readonly Dictionary<string, ShrinkModHandle> _mods = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<ShrinkModComponentSource, string> _catalogNames = new();
|
||||
private readonly List<ShrinkModComponentSource> _currentSources = new();
|
||||
private readonly ShrinkComponentCatalog _catalog = new();
|
||||
private long _generation;
|
||||
private int _catalogGeneration;
|
||||
private bool _applying;
|
||||
|
||||
public ShrinkModContextHost(bool enableHarmonyPatching = true, bool verboseLogging = false)
|
||||
{
|
||||
EnableHarmonyPatching = enableHarmonyPatching;
|
||||
VerboseLogging = verboseLogging;
|
||||
Runtime = new ShrinkContextRuntime();
|
||||
Loader = new ShrinkContextLoader(Runtime, _catalog);
|
||||
RegistryManager = new ShrinkModRegistryManager();
|
||||
}
|
||||
|
||||
public event Action<ShrinkModHandle> OnModReady;
|
||||
public event Action<IReadOnlyDictionary<string, ShrinkModHandle>> OnAllModsReady;
|
||||
|
||||
public bool EnableHarmonyPatching { get; }
|
||||
public bool VerboseLogging { get; }
|
||||
public ShrinkContextRuntime Runtime { get; }
|
||||
public ShrinkContextLoader Loader { get; }
|
||||
public IReadOnlyDictionary<string, ShrinkModHandle> Mods => _mods;
|
||||
public IReadOnlyList<ShrinkModComponentSource> CurrentSources => _currentSources.ToArray();
|
||||
internal ShrinkModRegistryManager RegistryManager { get; }
|
||||
|
||||
public IReadOnlyShrinkModRegistry<T> GetOrCreateRegistry<T>(string name) =>
|
||||
RegistryManager.GetOrCreateRegistry<T>(name);
|
||||
|
||||
public static string GetModKey(string modId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(modId))
|
||||
throw new ArgumentException("ModId must not be empty.", nameof(modId));
|
||||
return "shrink.mod/" + modId.Trim();
|
||||
}
|
||||
|
||||
public async UniTask ApplyAsync(IReadOnlyList<ShrinkModComponentSource> desiredSources)
|
||||
{
|
||||
if (desiredSources == null)
|
||||
throw new ArgumentNullException(nameof(desiredSources));
|
||||
if (_applying)
|
||||
throw new InvalidOperationException("A mod composition transaction is already running.");
|
||||
|
||||
var desired = ReuseUnchangedSources(ValidateAndOrder(desiredSources));
|
||||
var previous = _currentSources.ToArray();
|
||||
var previousById = previous.ToDictionary(source => source.Info.ModId, StringComparer.Ordinal);
|
||||
_applying = true;
|
||||
try
|
||||
{
|
||||
await Loader.ApplyAsync(BuildEntries(desired));
|
||||
EnsureActive(desired);
|
||||
|
||||
_currentSources.Clear();
|
||||
_currentSources.AddRange(desired);
|
||||
foreach (var source in desired)
|
||||
{
|
||||
if (!previousById.TryGetValue(source.Info.ModId, out var oldSource) ||
|
||||
!ReferenceEquals(source, oldSource))
|
||||
{
|
||||
OnModReady?.Invoke(_mods[source.Info.ModId]);
|
||||
}
|
||||
}
|
||||
OnAllModsReady?.Invoke(Mods);
|
||||
}
|
||||
catch (Exception applyError)
|
||||
{
|
||||
Exception restoreError = null;
|
||||
try
|
||||
{
|
||||
await Loader.ApplyAsync(BuildEntries(previous));
|
||||
EnsureActive(previous);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
restoreError = ex;
|
||||
}
|
||||
|
||||
throw new ShrinkModTransactionException(
|
||||
restoreError == null
|
||||
? "Mod composition failed; the previous composition was restored."
|
||||
: "Mod composition failed and restoring the previous composition also failed.",
|
||||
applyError,
|
||||
restoreError);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_applying = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async UniTask ShutdownAsync()
|
||||
{
|
||||
if (_applying)
|
||||
throw new InvalidOperationException("Cannot shut down while a mod transaction is running.");
|
||||
|
||||
await Loader.ApplyAsync(Array.Empty<ShrinkLoaderEntry>());
|
||||
_currentSources.Clear();
|
||||
}
|
||||
|
||||
internal long NextGeneration() => ++_generation;
|
||||
|
||||
internal void RegisterHandle(ShrinkModHandle handle)
|
||||
{
|
||||
if (!_mods.TryAdd(handle.Info.ModId, handle))
|
||||
throw new InvalidOperationException($"Mod handle already active: {handle.Info.ModId}");
|
||||
}
|
||||
|
||||
internal void UnregisterHandle(ShrinkModHandle handle)
|
||||
{
|
||||
if (_mods.TryGetValue(handle.Info.ModId, out var current) && ReferenceEquals(current, handle))
|
||||
_mods.Remove(handle.Info.ModId);
|
||||
}
|
||||
|
||||
private IReadOnlyList<ShrinkLoaderEntry> BuildEntries(IEnumerable<ShrinkModComponentSource> sources)
|
||||
{
|
||||
var entries = new List<ShrinkLoaderEntry>();
|
||||
foreach (var source in sources)
|
||||
{
|
||||
if (!_catalogNames.TryGetValue(source, out var catalogName))
|
||||
{
|
||||
catalogName = $"shrink.mod.source/{source.Info.ModId}/{++_catalogGeneration}";
|
||||
_catalogNames.Add(source, catalogName);
|
||||
_catalog.Register(catalogName, () => new ShrinkModComponent(this, source));
|
||||
}
|
||||
|
||||
entries.Add(new ShrinkLoaderEntry(source.Info.ModId, catalogName));
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
private void EnsureActive(IEnumerable<ShrinkModComponentSource> sources)
|
||||
{
|
||||
foreach (var source in sources)
|
||||
{
|
||||
if (!Loader.TryGetFiber(source.Info.ModId, out var fiber))
|
||||
throw new InvalidOperationException($"Mod fiber was not created: {source.Info.ModId}");
|
||||
if (fiber.LastError != null)
|
||||
throw new InvalidOperationException($"Mod {source.Info.ModId} failed during apply.", fiber.LastError);
|
||||
if (fiber.State != ShrinkFiberState.Active)
|
||||
throw new InvalidOperationException(
|
||||
$"Mod {source.Info.ModId} did not become active (state={fiber.State}).");
|
||||
}
|
||||
}
|
||||
|
||||
private static List<ShrinkModComponentSource> ValidateAndOrder(
|
||||
IReadOnlyList<ShrinkModComponentSource> sources)
|
||||
{
|
||||
var map = new Dictionary<string, ShrinkModComponentSource>(StringComparer.Ordinal);
|
||||
foreach (var source in sources)
|
||||
{
|
||||
if (source == null)
|
||||
throw new ArgumentException("Mod sources must not contain null.", nameof(sources));
|
||||
if (!map.TryAdd(source.Info.ModId, source))
|
||||
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;
|
||||
}
|
||||
|
||||
private List<ShrinkModComponentSource> ReuseUnchangedSources(
|
||||
IEnumerable<ShrinkModComponentSource> desired)
|
||||
{
|
||||
var currentById = _currentSources.ToDictionary(source => source.Info.ModId, StringComparer.Ordinal);
|
||||
var normalized = new List<ShrinkModComponentSource>();
|
||||
foreach (var source in desired)
|
||||
{
|
||||
if (currentById.TryGetValue(source.Info.ModId, out var current) &&
|
||||
string.Equals(current.Revision, source.Revision, StringComparison.Ordinal))
|
||||
{
|
||||
normalized.Add(current);
|
||||
}
|
||||
else
|
||||
{
|
||||
normalized.Add(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a1df7a5c357c614a976ff968dac34fd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cysharp.Threading.Tasks;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
/// <summary>默认模组组件根;目录变更只提交完整的新组合,失败时由 Host 恢复旧组合。</summary>
|
||||
public static class ShrinkModCordisRuntime
|
||||
{
|
||||
private static ShrinkModContextHost _host;
|
||||
|
||||
public static bool IsInitialized => _host != null;
|
||||
public static ShrinkModContextHost Host => _host ?? throw new InvalidOperationException(
|
||||
"ShrinkModCordisRuntime has not been initialized.");
|
||||
public static IReadOnlyDictionary<string, ShrinkModHandle> Mods =>
|
||||
_host?.Mods ?? EmptyMods;
|
||||
|
||||
private static readonly IReadOnlyDictionary<string, ShrinkModHandle> EmptyMods =
|
||||
new Dictionary<string, ShrinkModHandle>();
|
||||
|
||||
public static async UniTask<IReadOnlyDictionary<string, ShrinkModHandle>> ApplyDiscoveredAsync(
|
||||
ShrinkModFrameworkSettings settings = null)
|
||||
{
|
||||
settings ??= ShrinkModFrameworkSettings.Instance;
|
||||
var verboseLogging = settings == null || settings.verboseLogging;
|
||||
if (_host == null)
|
||||
{
|
||||
_host = new ShrinkModContextHost(
|
||||
settings == null || settings.enableHarmonyPatching,
|
||||
verboseLogging);
|
||||
}
|
||||
|
||||
var revisionState = ShrinkExternalModAssemblyLoader.CaptureState();
|
||||
ShrinkModNetworkManager.Configure(settings == null || settings.enableNetworkSync, verboseLogging);
|
||||
try
|
||||
{
|
||||
var sources = ShrinkModComponentDiscovery.Discover(settings, verboseLogging);
|
||||
await _host.ApplyAsync(sources);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// DLL revision discovery is part of the same composition transaction:
|
||||
// a failing replacement must not make the failed assembly current.
|
||||
ShrinkExternalModAssemblyLoader.RestoreState(revisionState);
|
||||
throw;
|
||||
}
|
||||
|
||||
if (verboseLogging)
|
||||
UnityEngine.Debug.Log($"[ShrinkModFramework] Cordis 模组组合已提交,共 {_host.Mods.Count} 个。");
|
||||
return _host.Mods;
|
||||
}
|
||||
|
||||
public static IReadOnlyShrinkModRegistry<T> GetOrCreateRegistry<T>(string name) =>
|
||||
Host.GetOrCreateRegistry<T>(name);
|
||||
|
||||
internal static void ResetForDomainReload()
|
||||
{
|
||||
_host = null;
|
||||
}
|
||||
|
||||
internal static void ResetForTesting()
|
||||
{
|
||||
if (_host != null)
|
||||
_host.ShutdownAsync().GetAwaiter().GetResult();
|
||||
_host = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b8767fd6541a55046a6d2f2ec992c446
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user