#nullable enable using System; using System.Collections.Generic; using System.Linq; using Cysharp.Threading.Tasks; using ShrinkEventBus; using UnityEngine; namespace ShrinkApp { public sealed class ShrinkAppHost : MonoBehaviour { private readonly List _startedModules = new(); private ShrinkAppContext? _context; private bool _unityStarted; private bool _publishedStartedEvent; public bool IsRunning { get; private set; } public bool HasFailed { get; private set; } public string LastError { get; private set; } = string.Empty; public IReadOnlyList StartedModules => _startedModules; internal void Configure(ShrinkAppContext context) { _context = context ?? throw new ArgumentNullException(nameof(context)); } internal async UniTask StartAppAsync() { if (IsRunning || HasFailed) return; if (_context == null) throw new InvalidOperationException("ShrinkAppHost is not configured."); var installers = CreateInstallerInstances(); var orderedInstallers = SortInstallers(installers, _context.Settings); try { foreach (var installer in orderedInstallers) installer.RegisterServices(_context); foreach (var installer in orderedInstallers) { await installer.InitializeAsync(_context); _startedModules.Add(installer.ModuleId); } IsRunning = true; PublishStartedEventIfReady(); } catch (Exception ex) { HasFailed = true; LastError = ex.Message; EventBus.TriggerEvent(new ShrinkAppStartFailedEvent { ErrorMessage = ex.Message, FailedModuleId = _startedModules.LastOrDefault() ?? string.Empty }); throw; } } private void Start() { _unityStarted = true; PublishStartedEventIfReady(); } private void PublishStartedEventIfReady() { if (_publishedStartedEvent || !_unityStarted || !IsRunning) return; _publishedStartedEvent = true; EventBus.TriggerEvent(new ShrinkAppStartedEvent { ModuleIds = _startedModules.ToArray() }); } private static IReadOnlyList CreateInstallerInstances() { var installers = new List(); foreach (var installerType in ShrinkAppGeneratedRegistry.GetInstallerTypes()) { if (installerType == null) continue; if (!typeof(IShrinkAppModuleInstaller).IsAssignableFrom(installerType)) throw new InvalidOperationException($"Installer type does not implement IShrinkAppModuleInstaller: {installerType.FullName}"); if (installerType.IsAbstract) throw new InvalidOperationException($"Installer type cannot be abstract: {installerType.FullName}"); if (Activator.CreateInstance(installerType) is not IShrinkAppModuleInstaller installer) throw new InvalidOperationException($"Failed to create installer: {installerType.FullName}"); installers.Add(installer); } return installers; } private static IReadOnlyList SortInstallers( IReadOnlyList installers, ShrinkAppSettings settings) { var disabled = new HashSet( settings.disabledModuleIds?.Where(id => !string.IsNullOrWhiteSpace(id)).Select(id => id.Trim()) ?? Enumerable.Empty(), StringComparer.OrdinalIgnoreCase); var enabledInstallers = installers .Where(installer => !disabled.Contains(installer.ModuleId)) .ToArray(); var byId = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var installer in enabledInstallers) { if (string.IsNullOrWhiteSpace(installer.ModuleId)) throw new InvalidOperationException($"Installer {installer.GetType().FullName} has an empty ModuleId."); if (!byId.TryAdd(installer.ModuleId.Trim(), installer)) throw new InvalidOperationException($"Duplicate installer ModuleId: {installer.ModuleId}"); } var visitState = new Dictionary(StringComparer.OrdinalIgnoreCase); var ordered = new List(); foreach (var installer in enabledInstallers.OrderBy(item => item.Order).ThenBy(item => item.ModuleId, StringComparer.OrdinalIgnoreCase)) { Visit(installer, byId, visitState, ordered); } return ordered; } private static void Visit( IShrinkAppModuleInstaller installer, IReadOnlyDictionary byId, IDictionary visitState, IList ordered) { var state = visitState.TryGetValue(installer.ModuleId, out var existingState) ? existingState : 0; if (state == 2) return; if (state == 1) throw new InvalidOperationException($"Circular installer dependency detected at module: {installer.ModuleId}"); visitState[installer.ModuleId] = 1; foreach (var dependencyId in installer.DependsOn ?? Array.Empty()) { if (string.IsNullOrWhiteSpace(dependencyId)) continue; if (!byId.TryGetValue(dependencyId.Trim(), out var dependency)) throw new InvalidOperationException( $"Installer dependency missing. Module={installer.ModuleId}, DependsOn={dependencyId}"); Visit(dependency, byId, visitState, ordered); } visitState[installer.ModuleId] = 2; if (!ordered.Contains(installer)) ordered.Add(installer); } } }