using System; using System.Collections.Generic; using System.Threading.Tasks; using AppleHills.Core.Settings; using UI; using UnityEngine; using UnityEngine.SceneManagement; using Bootstrap; namespace Core { /// /// Singleton service for loading and unloading Unity scenes asynchronously, with events for progress and completion. /// public class SceneManagerService : MonoBehaviour { 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 public event Action SceneLoadStarted; public event Action SceneLoadProgress; public event Action SceneLoadCompleted; public event Action SceneUnloadStarted; public event Action SceneUnloadProgress; public event Action SceneUnloadCompleted; private readonly Dictionary _activeLoads = new(); private readonly Dictionary _activeUnloads = new(); private LogVerbosity _logVerbosity = LogVerbosity.Debug; private const string BootstrapSceneName = "BootstrapScene"; void Awake() { _instance = this; // DontDestroyOnLoad(gameObject); // Initialize current scene tracking immediately in Awake InitializeCurrentSceneTracking(); // Register for post-boot initialization BootCompletionService.RegisterInitAction(InitializePostBoot); // Ensure BootstrapScene is loaded at startup var bootstrap = SceneManager.GetSceneByName(BootstrapSceneName); if (!bootstrap.isLoaded) { SceneManager.LoadScene(BootstrapSceneName, LoadSceneMode.Additive); } } private void Start() { _logVerbosity = DeveloperSettingsProvider.Instance.GetSettings().sceneLogVerbosity; } /// /// 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 InitializePostBoot() { // Set up loading screen reference and events after boot is complete _loadingScreen = LoadingScreenController.Instance; // Set up loading screen event handlers if available SetupLoadingScreenEvents(); LogDebugMessage($"Post-boot initialization complete, current scene is: {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); SceneLoadProgress?.Invoke(sceneName, 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; } SceneUnloadStarted?.Invoke(sceneName); var op = SceneManager.UnloadSceneAsync(sceneName); _activeUnloads[sceneName] = op; while (!op.isDone) { progress?.Report(op.progress); SceneUnloadProgress?.Invoke(sceneName, op.progress); await Task.Yield(); } _activeUnloads.Remove(sceneName); SceneUnloadCompleted?.Invoke(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); SceneUnloadStarted?.Invoke(name); } 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); SceneUnloadCompleted?.Invoke(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) { await SwitchSceneAsync(CurrentGameplayScene, progress, autoHideLoadingScreen); } /// /// 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. public async Task SwitchSceneAsync(string newSceneName, IProgress progress = null, bool autoHideLoadingScreen = true) { // Show loading screen at the start (whether using auto-hide or not) if (_loadingScreen != null && !_loadingScreen.IsActive) { _loadingScreen.ShowLoadingScreen(); } // 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); } // Unload previous gameplay scene (if not BootstrapScene and not same as new) if (!string.IsNullOrEmpty(CurrentGameplayScene)&& CurrentGameplayScene != BootstrapSceneName) { var prevScene = SceneManager.GetSceneByName(CurrentGameplayScene); if (prevScene.isLoaded) { await UnloadSceneAsync(CurrentGameplayScene); } else { Logging.Warning($"[SceneManagerService] Previous scene '{CurrentGameplayScene}' is not loaded, skipping unload."); } } // Ensure BootstrapScene is loaded before loading new scene var bootstrap = SceneManager.GetSceneByName(BootstrapSceneName); if (!bootstrap.isLoaded) { SceneManager.LoadScene(BootstrapSceneName, LoadSceneMode.Additive); } // Load new gameplay scene await LoadSceneAsync(newSceneName, progress); // Update tracker CurrentGameplayScene = newSceneName; // 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}"); } } } }