#nullable enable using System; using System.Collections.Generic; using System.Linq; using Cysharp.Threading.Tasks; using ShrinkApp; using ShrinkEventBus; using UnityEngine; namespace ShrinkContext.AppAdapter { /// /// 由 ShrinkContext 加载器驱动的 ShrinkApp 宿主(阶段 2 过渡路径,对应 ShrinkAppSettings.hostingMode = ContextLoader)。 /// /// 与经典 ShrinkAppHost 的行为差异: /// - 依赖缺失的安装器保持非活动等待(Waiting),而不是排序期抛错; /// - 支持运行中 SetModuleDisabledAsync 按模块 disable/enable,无需重启宿主; /// - 重复 ModuleId 在构造期抛错(与经典一致),依赖排序由响应式余效应结构性保证。 /// /// 与 ShrinkAppHost 相同的语义:安装器单例实例、ModuleId 大小写不敏感、 /// disabledModuleIds 作为初始禁用集合、启动完成发布 ShrinkAppStartedEvent。 /// public sealed class ShrinkAppLoaderHost { private sealed class ModuleRecord { public string ModuleId = string.Empty; } private sealed class CompositionOptions { public bool Enabled; public IReadOnlyDictionary? Isolate; public IReadOnlyDictionary>? Intercept; } private readonly Dictionary _modules = new(StringComparer.OrdinalIgnoreCase); private readonly HashSet _disabled = new(StringComparer.OrdinalIgnoreCase); private readonly HashSet _settingsDisabled = new(StringComparer.OrdinalIgnoreCase); private readonly ShrinkComponentCatalog _catalog; private Dictionary? _compositionOptions; private bool _includeUnlistedEntries = true; private bool _started; public ShrinkAppLoaderHost(ShrinkAppSettings? settings = null, IReadOnlyList? installers = null) { Settings = settings ?? ShrinkAppSettings.Instance; Services = new ShrinkAppServices(); Context = new ShrinkContextRuntime(); var catalog = new ShrinkComponentCatalog(); foreach (var installer in installers ?? DiscoverDefaultInstallers()) { if (installer == null || string.IsNullOrWhiteSpace(installer.ModuleId)) throw new InvalidOperationException( $"Installer '{installer?.GetType().FullName ?? ""}' has an empty ModuleId."); var moduleId = installer.ModuleId.Trim(); if (!_modules.TryAdd(moduleId, new ModuleRecord { ModuleId = moduleId })) throw new InvalidOperationException($"Duplicate installer ModuleId: {moduleId}"); // 工厂每次重建条目时包装同一个安装器实例(与经典宿主的单例安装器语义一致) catalog.Register(moduleId, () => new ShrinkAppInstallerComponent(installer, Settings)); } Loader = new ShrinkContextLoader(Context, catalog); _catalog = catalog; foreach (var rawId in Settings.disabledModuleIds ?? Array.Empty()) { if (!string.IsNullOrWhiteSpace(rawId)) { _settingsDisabled.Add(rawId.Trim()); _disabled.Add(rawId.Trim()); } } } public ShrinkAppSettings Settings { get; } public ShrinkAppServices Services { get; } public ShrinkContextRuntime Context { get; } public ShrinkContextLoader Loader { get; } public bool IsRunning { get; private set; } public ShrinkAppCompositionDocument? AppliedComposition { get; private set; } /// 全部已注册模块 id(有序)。 public IReadOnlyList ModuleIds => _modules.Values.Select(m => m.ModuleId).OrderBy(id => id, StringComparer.OrdinalIgnoreCase).ToArray(); public IReadOnlyList ActiveModuleIds => ModuleIds.Where(IsModuleActive).ToArray(); /// 已注册但当前非活动的模块(依赖缺失等待中;被禁用的模块不计入,见 IsModuleDisabled)。 public IReadOnlyList WaitingModuleIds => ModuleIds.Where(IsModuleWaiting).ToArray(); public bool IsModuleActive(string moduleId) => TryGetLoaderFiber(moduleId, out var fiber) && fiber is { State: ShrinkFiberState.Active }; public bool IsModuleWaiting(string moduleId) => TryGetLoaderFiber(moduleId, out var fiber) && fiber is { State: ShrinkFiberState.Inactive }; public bool IsModuleDisabled(string moduleId) => !string.IsNullOrWhiteSpace(moduleId) && (_disabled.Contains(moduleId.Trim()) || (!_includeUnlistedEntries && _compositionOptions != null && !_compositionOptions.ContainsKey(moduleId.Trim()))); /// 查询模块当前纤程(被禁用模块返回 false)。 public bool TryGetModuleFiber(string moduleId, out ShrinkFiber fiber) => TryGetLoaderFiber(moduleId, out fiber!); private bool TryGetLoaderFiber(string moduleId, out ShrinkFiber? fiber) { fiber = null; if (string.IsNullOrWhiteSpace(moduleId)) return false; if (!_modules.ContainsKey(moduleId.Trim())) return false; return Loader.TryGetFiber(moduleId.Trim(), out fiber!); } /// /// 阶段 3:用原生 Cordis 组件替换指定模块的安装器包装(须在 StartAsync 之前调用)。 /// 目录同键覆盖——安装器不再被实例化包装,模块行为完全由原生组件定义。 /// public void OverrideModuleComponent(string moduleId, Func componentFactory) { if (_started) throw new InvalidOperationException("Module components can only be overridden before StartAsync."); if (string.IsNullOrWhiteSpace(moduleId)) throw new ArgumentException("Module id must not be null or empty.", nameof(moduleId)); if (componentFactory == null) throw new ArgumentNullException(nameof(componentFactory)); if (!_modules.ContainsKey(moduleId.Trim())) throw new InvalidOperationException($"Unknown module id: '{moduleId}'."); _catalog.Register(moduleId.Trim(), componentFactory); } /// /// 向组合根加入没有旧安装器身份的原生组件(例如 Command-Network、EventBus 薄适配)。 /// 须在 StartAsync 前调用;模块 id 同时作为加载器条目 id 与组件目录键。 /// public void AddModuleComponent(string moduleId, Func componentFactory) { if (_started) throw new InvalidOperationException("Module components can only be added before StartAsync."); if (string.IsNullOrWhiteSpace(moduleId)) throw new ArgumentException("Module id must not be null or empty.", nameof(moduleId)); if (componentFactory == null) throw new ArgumentNullException(nameof(componentFactory)); var normalizedId = moduleId.Trim(); if (!_modules.TryAdd(normalizedId, new ModuleRecord { ModuleId = normalizedId })) throw new InvalidOperationException($"Duplicate module id: '{normalizedId}'."); _catalog.Register(normalizedId, componentFactory); } /// /// 在启动前应用声明式条目策略。组件工厂必须已经由组合根注册;配置不能实例化任意类型。 /// ShrinkAppSettings.disabledModuleIds 仍作为额外禁用集合保留。 /// public void ApplyComposition(ShrinkAppCompositionDocument document) { if (_started) throw new InvalidOperationException("Composition can only be applied before StartAsync."); var copy = (document ?? throw new ArgumentNullException(nameof(document))).Clone(); var options = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var entry in copy.entries) { var id = entry.NormalizedId; if (!_modules.ContainsKey(id)) throw new ShrinkLoaderException( $"Composition entry '{id}' has no registered module/component factory."); options.Add(id, new CompositionOptions { Enabled = entry.enabled, Isolate = entry.BuildIsolate(), Intercept = entry.BuildIntercept() }); } _compositionOptions = options; _includeUnlistedEntries = copy.includeUnlistedEntries; AppliedComposition = copy; _disabled.Clear(); foreach (var id in _settingsDisabled) _disabled.Add(id); foreach (var pair in options) { if (!pair.Value.Enabled) _disabled.Add(pair.Key); } } public async UniTask StartAsync() { if (_started) return; _started = true; await Loader.ApplyAsync(BuildEntries()); IsRunning = true; global::ShrinkApp.ShrinkApp.SetExternalHostRunning(true); var active = ActiveModuleIds; Debug.Log($"[ShrinkApp.LoaderHost] 启动完成:active={active.Count} " + $"waiting={WaitingModuleIds.Count} disabled={_disabled.Count} " + $"modules=[{string.Join(", ", active)}]"); EventBus.Post(new ShrinkAppStartedEvent { ModuleIds = active.ToArray() }); } /// 运行中按模块 disable/enable:增量协调,宿主与其余模块不重启。 public async UniTask SetModuleDisabledAsync(string moduleId, bool disabled) { EnsureStarted(); if (string.IsNullOrWhiteSpace(moduleId)) throw new ArgumentException("Module id must not be null or empty.", nameof(moduleId)); if (!TryGetRecord(moduleId, out _)) throw new InvalidOperationException($"Unknown module id: '{moduleId}'."); var normalizedId = moduleId.Trim(); if (!disabled && !_includeUnlistedEntries && _compositionOptions != null && !_compositionOptions.ContainsKey(normalizedId)) { throw new InvalidOperationException( $"Module '{normalizedId}' is excluded by the active composition profile."); } if (disabled) _disabled.Add(normalizedId); else _disabled.Remove(normalizedId); await Loader.ApplyAsync(BuildEntries()); } public async UniTask ShutdownAsync() { EnsureStarted(); await Context.ShutdownAsync(); IsRunning = false; global::ShrinkApp.ShrinkApp.SetExternalHostRunning(false); } private List BuildEntries() { var entries = new List(); foreach (var record in _modules.Values.OrderBy(m => m.ModuleId, StringComparer.OrdinalIgnoreCase)) { CompositionOptions? options = null; _compositionOptions?.TryGetValue(record.ModuleId, out options); if (!_includeUnlistedEntries && _compositionOptions != null && options == null) continue; entries.Add(new ShrinkLoaderEntry(record.ModuleId, record.ModuleId, Services, _disabled.Contains(record.ModuleId), options?.Isolate, options?.Intercept)); } return entries; } private bool TryGetRecord(string moduleId, out ModuleRecord record) { if (!string.IsNullOrWhiteSpace(moduleId)) return _modules.TryGetValue(moduleId.Trim(), out record!); record = null!; return false; } private void EnsureStarted() { if (!_started) throw new InvalidOperationException("ShrinkAppLoaderHost has not been started yet."); } private static IReadOnlyList DiscoverDefaultInstallers() { var installers = new List(); foreach (var type in ShrinkAppInstallers.GetDiscoveredInstallerTypes()) { if (Activator.CreateInstance(type) is IShrinkAppModuleInstaller installer) installers.Add(installer); else throw new InvalidOperationException($"Failed to create installer: {type.FullName}"); } return installers; } } }