using System; using System.Collections.Generic; using System.Threading.Tasks; using AppleHills.Core.Settings; using Core.Lifecycle; using Core.SaveLoad; using UI; using UnityEngine; using UnityEngine.SceneManagement; namespace Core { /// /// Singleton service for loading and unloading Unity scenes asynchronously, with events for progress and completion. /// public class SceneManagerService : ManagedBehaviour { private LoadingScreenController _loadingScreen; private static SceneManagerService _instance; /// /// Singleton instance of the SceneManagerService. No longer creates an instance if one doesn't exist. /// public static SceneManagerService Instance => _instance; // Events for scene lifecycle // NOTE: Most components should use lifecycle hooks (OnSceneReady, OnSceneUnloading) // instead of subscribing to these events. Events are primarily for orchestration. /// /// Fired when a scene starts loading. Used by loading screen orchestration. /// public event Action SceneLoadStarted; /// /// Fired when a scene finishes loading. /// Used by loading screen orchestration and cross-scene components (e.g., PauseMenu). /// For component initialization, use OnSceneReady() lifecycle hook instead. /// public event Action SceneLoadCompleted; private readonly Dictionary _activeLoads = new(); private readonly Dictionary _activeUnloads = new(); private LogVerbosity _logVerbosity = LogVerbosity.Debug; private const string BootstrapSceneName = "BootstrapScene"; // ManagedBehaviour configuration public override int ManagedAwakePriority => 15; // Core infrastructure, after GameManager private new void Awake() { base.Awake(); // CRITICAL: Register with LifecycleManager! // Set instance immediately so it's available before OnManagedAwake() is called _instance = this; // Initialize current scene tracking - critical for scene management InitializeCurrentSceneTracking(); // Ensure BootstrapScene is loaded at startup var bootstrap = SceneManager.GetSceneByName(BootstrapSceneName); if (!bootstrap.isLoaded) { SceneManager.LoadScene(BootstrapSceneName, LoadSceneMode.Additive); } } protected override void OnManagedAwake() { // Set up loading screen reference and events // This must happen in ManagedAwake because LoadingScreenController instance needs to be set first _loadingScreen = LoadingScreenController.Instance; SetupLoadingScreenEvents(); // Load verbosity settings _logVerbosity = DeveloperSettingsProvider.Instance.GetSettings().sceneLogVerbosity; LogDebugMessage($"SceneManagerService initialized, current scene is: {CurrentGameplayScene}"); } /// /// Initialize current scene tracking immediately in Awake /// This ensures scene management works correctly regardless of boot timing /// private void InitializeCurrentSceneTracking() { // Get the active scene and use it as the current gameplay scene Scene activeScene = SceneManager.GetActiveScene(); if (activeScene.IsValid()) { // If this is the MainMenu or another gameplay scene, track it if (activeScene.name != BootstrapSceneName) { CurrentGameplayScene = activeScene.name; LogDebugMessage($"Initialized with current scene: {CurrentGameplayScene}"); } // Otherwise default to MainMenu else { CurrentGameplayScene = "AppleHillsOverworld"; LogDebugMessage($"Initialized with default scene: {CurrentGameplayScene}"); } } else { CurrentGameplayScene = "AppleHillsOverworld"; LogDebugMessage($"No valid active scene, defaulting to: {CurrentGameplayScene}"); } } private void SetupLoadingScreenEvents() { if (_loadingScreen == null) return; SceneLoadStarted += _ => _loadingScreen.ShowLoadingScreen(() => GetAggregateLoadProgress()); SceneLoadCompleted += _ => _loadingScreen.HideLoadingScreen(); } /// /// Load a single scene asynchronously (additive). /// /// Name of the scene to load. /// Optional progress reporter. public async Task LoadSceneAsync(string sceneName, IProgress progress = null) { SceneLoadStarted?.Invoke(sceneName); var op = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive); _activeLoads[sceneName] = op; while (!op.isDone) { progress?.Report(op.progress); await Task.Yield(); } _activeLoads.Remove(sceneName); SceneLoadCompleted?.Invoke(sceneName); } /// /// Unload a single scene asynchronously. /// /// Name of the scene to unload. /// Optional progress reporter. public async Task UnloadSceneAsync(string sceneName, IProgress progress = null) { var scene = SceneManager.GetSceneByName(sceneName); if (!scene.isLoaded) { Logging.Warning($"[SceneManagerService] Attempted to unload scene '{sceneName}', but it is not loaded."); return; } var op = SceneManager.UnloadSceneAsync(sceneName); _activeUnloads[sceneName] = op; while (!op.isDone) { progress?.Report(op.progress); await Task.Yield(); } _activeUnloads.Remove(sceneName); } /// /// Load multiple scenes asynchronously. /// /// Enumerable of scene names to load. /// Optional progress reporter. public async Task LoadScenesAsync(IEnumerable sceneNames, IProgress progress = null) { // Show loading screen at the start of multiple scene loading if (_loadingScreen != null) { _loadingScreen.ShowLoadingScreen(); } int total = 0; int done = 0; var ops = new List(); foreach (var name in sceneNames) { total++; var op = SceneManager.LoadSceneAsync(name, LoadSceneMode.Additive); _activeLoads[name] = op; ops.Add(op); SceneLoadStarted?.Invoke(name); } while (done < total) { done = 0; float aggregate = 0f; foreach (var op in ops) { if (op.isDone) done++; aggregate += op.progress; } float avgProgress = aggregate / total; progress?.Report(avgProgress); await Task.Yield(); } foreach (var name in sceneNames) { _activeLoads.Remove(name); SceneLoadCompleted?.Invoke(name); } // Hide loading screen after all scenes are loaded if (_loadingScreen != null) { _loadingScreen.HideLoadingScreen(); } } /// /// Unload multiple scenes asynchronously. /// /// Enumerable of scene names to unload. /// Optional progress reporter. public async Task UnloadScenesAsync(IEnumerable sceneNames, IProgress progress = null) { // Show loading screen at the start of multiple scene unloading if (_loadingScreen != null) { _loadingScreen.ShowLoadingScreen(); } int total = 0; int done = 0; var ops = new List(); foreach (var name in sceneNames) { total++; var op = SceneManager.UnloadSceneAsync(name); _activeUnloads[name] = op; ops.Add(op); } while (done < total) { done = 0; float aggregate = 0f; foreach (var op in ops) { aggregate += op.progress; if (op.isDone) done++; } float avg = aggregate / total; progress?.Report(avg); await Task.Yield(); } foreach (var name in sceneNames) { _activeUnloads.Remove(name); } // Hide loading screen after all scenes are unloaded if (_loadingScreen != null) { _loadingScreen.HideLoadingScreen(); } } // Optionally: expose current progress for all active operations public float GetAggregateLoadProgress() { if (_activeLoads.Count == 0) return 1f; float sum = 0f; foreach (var op in _activeLoads.Values) sum += op.progress; return sum / _activeLoads.Count; } public float GetAggregateUnloadProgress() { if (_activeUnloads.Count == 0) return 1f; float sum = 0f; foreach (var op in _activeUnloads.Values) sum += op.progress; return sum / _activeUnloads.Count; } // Tracks the currently loaded gameplay scene (not persistent/bootstrapper) public string CurrentGameplayScene { get; set; } = "AppleHillsOverworld"; public async Task ReloadCurrentScene(IProgress progress = null, bool autoHideLoadingScreen = true, bool skipSave = false) { await SwitchSceneAsync(CurrentGameplayScene, progress, autoHideLoadingScreen, skipSave); } /// /// Switches from current gameplay scene to a new one /// /// Name of the scene to load /// Optional progress reporter /// Whether to automatically hide the loading screen when complete. If false, caller must hide it manually. /// If true, skips saving scene data during transition. Useful for level restart to prevent re-saving cleared data. public async Task SwitchSceneAsync(string newSceneName, IProgress progress = null, bool autoHideLoadingScreen = true, bool skipSave = false) { string oldSceneName = CurrentGameplayScene; // PHASE 1: Show loading screen at the start // Use explicit progress provider to combine unload + load progress if (_loadingScreen != null) { _loadingScreen.ShowLoadingScreen(() => GetAggregateLoadProgress()); } // PHASE 2: Broadcast scene unloading - notify components to cleanup LogDebugMessage($"Broadcasting OnSceneUnloading for: {oldSceneName}"); LifecycleManager.Instance?.BroadcastSceneUnloading(oldSceneName); // PHASE 3: Save scene-specific data via SaveLoadManager (unless skipSave is true) if (!skipSave && SaveLoadManager.Instance != null) { var debugSettings = DeveloperSettingsProvider.Instance.GetSettings(); if (debugSettings.useSaveLoadSystem) { LogDebugMessage($"Saving scene data for: {oldSceneName}"); SaveLoadManager.Instance.SaveSceneData(); } } else if (skipSave) { LogDebugMessage($"Skipping save for: {oldSceneName} (skipSave=true)"); } // PHASE 4: Clear PuzzleManager state before scene transition if (PuzzleS.PuzzleManager.Instance != null) { LogDebugMessage($"Clearing puzzle state before scene transition"); PuzzleS.PuzzleManager.Instance.ClearPuzzleState(); } // PHASE 5: Remove all AstarPath (A* Pathfinder) singletons before loading the new scene var astarPaths = FindObjectsByType(FindObjectsSortMode.None); foreach (var astar in astarPaths) { if (Application.isPlaying) Destroy(astar.gameObject); else DestroyImmediate(astar.gameObject); } // PHASE 6: Unload previous gameplay scene (Unity will call OnDestroy → OnManagedDestroy) if (!string.IsNullOrEmpty(oldSceneName) && oldSceneName != BootstrapSceneName) { var prevScene = SceneManager.GetSceneByName(oldSceneName); if (prevScene.isLoaded) { await UnloadSceneAsync(oldSceneName); } else { Logging.Warning($"[SceneManagerService] Previous scene '{oldSceneName}' is not loaded, skipping unload."); } } // PHASE 7: Ensure BootstrapScene is loaded before loading new scene var bootstrap = SceneManager.GetSceneByName(BootstrapSceneName); if (!bootstrap.isLoaded) { SceneManager.LoadScene(BootstrapSceneName, LoadSceneMode.Additive); } // PHASE 8: Begin scene loading mode - enables priority-ordered component initialization LogDebugMessage($"Beginning scene load for: {newSceneName}"); LifecycleManager.Instance?.BeginSceneLoad(newSceneName); // PHASE 9: Load new gameplay scene await LoadSceneAsync(newSceneName, progress); CurrentGameplayScene = newSceneName; // PHASE 10: Broadcast scene ready - processes batched components in priority order, then calls OnSceneReady LogDebugMessage($"Broadcasting OnSceneReady for: {newSceneName}"); LifecycleManager.Instance?.BroadcastSceneReady(newSceneName); // PHASE 11: Restore scene-specific data via SaveLoadManager if (!skipSave && SaveLoadManager.Instance != null) { var debugSettings = DeveloperSettingsProvider.Instance.GetSettings(); if (debugSettings.useSaveLoadSystem) { LogDebugMessage($"Restoring scene data for: {newSceneName}"); SaveLoadManager.Instance.RestoreSceneData(); } } else if (skipSave) { LogDebugMessage($"Skipping restore for: {newSceneName} (skipSave=true)"); } // PHASE 12: Only hide the loading screen if autoHideLoadingScreen is true if (autoHideLoadingScreen && _loadingScreen != null) { _loadingScreen.HideLoadingScreen(); } } private void LogDebugMessage(string message) { if (_logVerbosity <= LogVerbosity.Debug) { Logging.Debug($"[SceneManagerService] {message}"); } } } }