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; } /// /// 模组的 Cordis 组合宿主。配置应用是事务:新组合任一 fiber 失败时,重新协调到旧组件源; /// Mono/IL2CPP 下旧程序集仍驻留,但旧/新模组实例及其已追踪效应会被正确卸载或恢复。 /// public sealed class ShrinkModContextHost { private readonly Dictionary _mods = new(StringComparer.Ordinal); private readonly Dictionary _catalogNames = new(); private readonly List _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 OnModReady; public event Action> OnAllModsReady; public bool EnableHarmonyPatching { get; } public bool VerboseLogging { get; } public ShrinkContextRuntime Runtime { get; } public ShrinkContextLoader Loader { get; } public IReadOnlyDictionary Mods => _mods; public IReadOnlyList CurrentSources => _currentSources.ToArray(); internal ShrinkModRegistryManager RegistryManager { get; } public ShrinkModRegistry GetOrCreateRegistry(string name) => RegistryManager.GetOrCreateRegistry(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 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()); _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 BuildEntries(IEnumerable sources) { var entries = new List(); 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 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 ValidateAndOrder( IReadOnlyList sources) { var map = new Dictionary(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(); var visiting = new HashSet(StringComparer.Ordinal); var visited = new HashSet(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 ReuseUnchangedSources( IEnumerable desired) { var currentById = _currentSources.ToDictionary(source => source.Info.ModId, StringComparer.Ordinal); var normalized = new List(); 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 map, ISet visiting, ISet visited, ICollection 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); } } }