using System; using System.Collections.Generic; using System.Runtime.ExceptionServices; using System.Threading; using System.Threading.Tasks; using FlowScope.Audio; using FlowScope.Config; using FlowScope.Flow; using FlowScope.Resources; using FlowScope.Save; using FlowScope.UI; using UnityEngine; using UnityEngine.UI; using ContainerType = FlowScope.Container.Container; namespace FlowScope.Samples.MainMenuP0 { public sealed class GameBootstrap : MonoBehaviour { private const string SaveKey = "main-menu-player"; [SerializeField] private Canvas hudCanvas; [SerializeField] private MainMenuPanel mainMenuPanelPrefab; [SerializeField] private int defaultGold; private ContainerType _root; private GameFlow _gameFlow; private ISaveService _saveService; private UIPreloadService _preloadService; private SampleResourceBackend _sampleBackend; private PlayerData _playerData; private CancellationTokenSource _lifetime; private bool _started; private bool _shutdownInProgress; private bool _shutdown; public int ReleasedResourceCount => _sampleBackend?.ReleaseCount ?? 0; /// /// True after runtime cleanup has completed; shutdown sub-step failures are still reported by ShutdownAsync. /// public bool IsShutdown => _shutdown; public Task StartupTask { get; private set; } private void Awake() { _lifetime = new CancellationTokenSource(); StartupTask = RunStartupWithLoggingAsync(_lifetime.Token); } public async Task StartupAsync(CancellationToken cancellationToken) { if (_started) { return; } _started = true; _shutdown = false; _root = new ContainerType(); _gameFlow = new GameFlow(); _sampleBackend = new SampleResourceBackend(); _sampleBackend.Register( "FlowScope/Samples/MainMenuP0/MainMenuPanel", ResolveMainMenuPanelPrefab().gameObject); var resources = new ResourceService(_sampleBackend); _preloadService = new UIPreloadService(resources); var uiManager = new UIManager(resources, _preloadService); uiManager.RegisterLayer("hud", ResolveHudCanvas(), 0, PanelStrategy.Destroy); var screenNavigator = new UIScreenNavigator(uiManager); var migrations = new SaveMigrationRegistry(); migrations.Register(new PlayerDataV1ToV2Migration()); _saveService = new SaveService( new FileSaveStorage(), new JsonSaveSerializer(2, migrations)); _playerData = await _saveService.LoadAsync( SaveKey, new PlayerData { Gold = new R3.ReactiveProperty(defaultGold) }, cancellationToken); await _preloadService.PreloadAsync(cancellationToken); var audioService = gameObject.AddComponent().Initialize(resources); _root.RegisterInstance(_root); _root.RegisterInstance( new JsonConfigProvider(new[] { JsonConfigProvider.Mapping( "main_menu.json", new InMemoryConfigSource( new Dictionary { { "main_menu.json", "[{\"Id\":1,\"Title\":\"Main Menu P1\"}]" } }, "MainMenuP0"), new JsonConfigParser()) })); _root.RegisterInstance(resources); _root.RegisterInstance(_saveService); _root.RegisterInstance(audioService); _root.RegisterInstance(uiManager); _root.RegisterInstance(_preloadService); _root.RegisterInstance(screenNavigator); _root.RegisterInstance(_playerData); _root.RegisterTransient(_ => new MainMenuFeature(screenNavigator, _playerData)); await _gameFlow.StartupAsync(_root, cancellationToken); } public async Task ShutdownAsync(CancellationToken cancellationToken) { if (_shutdown || _shutdownInProgress) { return; } _shutdownInProgress = true; var exceptions = new List(); try { if (_saveService != null && _playerData != null) { try { await _saveService.SaveAsync(SaveKey, _playerData, cancellationToken); } catch (Exception exception) { exceptions.Add(exception); } } if (_gameFlow != null && _gameFlow.State != GameFlowState.Disposed) { try { await _gameFlow.ShutdownAsync(cancellationToken); } catch (Exception exception) { exceptions.Add(exception); } } try { _preloadService?.ReleaseAll(); } catch (Exception exception) { exceptions.Add(exception); } _shutdown = true; } finally { _shutdownInProgress = false; } if (exceptions.Count == 1) { ExceptionDispatchInfo.Capture(exceptions[0]).Throw(); } if (exceptions.Count > 1) { throw new AggregateException("MainMenuP0 shutdown completed with failures.", exceptions); } } private void OnApplicationQuit() { _lifetime?.Cancel(); _ = RunShutdownWithLoggingAsync(CancellationToken.None); } private void OnDestroy() { _lifetime?.Cancel(); _lifetime?.Dispose(); } private async Task RunStartupWithLoggingAsync(CancellationToken cancellationToken) { try { await StartupAsync(cancellationToken); } catch (Exception exception) { Debug.LogException(exception); throw; } } private async Task RunShutdownWithLoggingAsync(CancellationToken cancellationToken) { try { await ShutdownAsync(cancellationToken); } catch (Exception exception) { Debug.LogException(exception); } } private Canvas ResolveHudCanvas() { if (hudCanvas != null) { return hudCanvas; } var canvasObject = new GameObject("FlowScope HUD Canvas"); canvasObject.transform.SetParent(transform, false); hudCanvas = canvasObject.AddComponent(); hudCanvas.renderMode = RenderMode.ScreenSpaceOverlay; return hudCanvas; } private MainMenuPanel ResolveMainMenuPanelPrefab() { if (mainMenuPanelPrefab != null) { return mainMenuPanelPrefab; } var panelObject = new GameObject("MainMenuPanel Template"); panelObject.transform.SetParent(transform, false); panelObject.SetActive(false); var panel = panelObject.AddComponent(); var goldObject = new GameObject("GoldText"); goldObject.transform.SetParent(panelObject.transform, false); var goldText = goldObject.AddComponent(); goldText.text = defaultGold.ToString(); goldText.alignment = TextAnchor.MiddleCenter; var buttonObject = new GameObject("IncrementGoldButton"); buttonObject.transform.SetParent(panelObject.transform, false); buttonObject.AddComponent(); var button = buttonObject.AddComponent