SaveLoad using managed lifecycle

This commit is contained in:
Michal Pikulski
2025-11-04 20:01:27 +01:00
parent 379a033d6b
commit bb68d1fd31
23 changed files with 1083 additions and 648 deletions

View File

@@ -266,7 +266,7 @@ namespace Core
// Search through all registered pickups
foreach (var pickup in _pickups)
{
if (pickup is SaveableInteractable saveable && saveable.GetSaveId() == saveId)
if (pickup is SaveableInteractable saveable && saveable.SaveId == saveId)
{
return pickup.gameObject;
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using UnityEngine;
@@ -62,6 +62,11 @@ namespace Core.Lifecycle
private bool isBootComplete = false;
private string currentSceneReady = "";
// Scene loading state tracking
private bool isLoadingScene = false;
private string sceneBeingLoaded = "";
private List<ManagedBehaviour> pendingSceneComponents = new List<ManagedBehaviour>();
[SerializeField] private bool enableDebugLogging = true;
#endregion
@@ -115,26 +120,36 @@ namespace Core.Lifecycle
// Track which scene this component belongs to
componentScenes[component] = sceneName;
// Register for ManagedAwake
// ALWAYS add to managedAwakeList - this is the master list used for save/load
InsertSorted(managedAwakeList, component, component.ManagedAwakePriority);
// Handle ManagedAwake timing based on boot state
if (isBootComplete)
{
// Boot already complete - call OnManagedAwake immediately
LogDebug($"Late registration: Calling OnManagedAwake immediately for {component.gameObject.name}");
try
// Check if we're currently loading a scene
if (isLoadingScene && sceneName == sceneBeingLoaded)
{
component.InvokeManagedAwake();
HandleAutoRegistrations(component);
// Batch this component - will be processed in priority order when scene load completes
pendingSceneComponents.Add(component);
LogDebug($"Batched component for scene load: {component.gameObject.name} (Scene: {sceneName})");
}
catch (Exception ex)
else
{
Debug.LogError($"[LifecycleManager] Error in OnManagedAwake for {component.gameObject.name}: {ex}");
// Truly late registration (component enabled after scene is ready)
// Call OnManagedAwake immediately since boot already completed
LogDebug($"Late registration: Calling OnManagedAwake immediately for {component.gameObject.name}");
try
{
component.InvokeManagedAwake();
HandleAutoRegistrations(component);
}
catch (Exception ex)
{
Debug.LogError($"[LifecycleManager] Error in OnManagedAwake for {component.gameObject.name}: {ex}");
}
}
}
else
{
// Boot not complete yet - add to list for broadcast
InsertSorted(managedAwakeList, component, component.ManagedAwakePriority);
}
// If boot not complete, component stays in list and will be processed by BroadcastManagedAwake()
// Register for all scene lifecycle hooks
InsertSorted(sceneUnloadingList, component, component.SceneUnloadingPriority);
@@ -143,20 +158,8 @@ namespace Core.Lifecycle
InsertSorted(restoreRequestedList, component, component.RestorePriority);
InsertSorted(destroyList, component, component.DestroyPriority);
// If this scene is already ready, call OnSceneReady immediately
// Check both currentSceneReady AND if the Unity scene is actually loaded
// (during scene loading, components Awake before BroadcastSceneReady is called)
bool sceneIsReady = currentSceneReady == sceneName;
// Also check if this is happening during boot and the scene is the active scene
// This handles components that register during initial scene load
if (!sceneIsReady && isBootComplete && sceneName != "DontDestroyOnLoad")
{
var scene = UnityEngine.SceneManagement.SceneManager.GetSceneByName(sceneName);
sceneIsReady = scene.isLoaded;
}
if (sceneIsReady)
// If this scene is already ready (and we're not in loading mode), call OnSceneReady immediately
if (!isLoadingScene && currentSceneReady == sceneName)
{
LogDebug($"Late registration: Calling OnSceneReady immediately for {component.gameObject.name}");
try
@@ -233,8 +236,63 @@ namespace Core.Lifecycle
}
}
// Clear the list - components already initialized
managedAwakeList.Clear();
// NOTE: We do NOT clear managedAwakeList here!
// This list is reused for save/load broadcasts and must persist for the lifetime of the game.
// Components are added during registration and removed during Unregister (OnDestroy).
}
/// <summary>
/// Begins scene loading mode for the specified scene.
/// Components that register during this time will be batched and processed in priority order.
/// Call this BEFORE starting to load a scene.
/// </summary>
public void BeginSceneLoad(string sceneName)
{
isLoadingScene = true;
sceneBeingLoaded = sceneName;
pendingSceneComponents.Clear();
LogDebug($"Began scene loading mode for: {sceneName}");
}
/// <summary>
/// Processes all batched components from the scene load in priority order.
/// Called automatically by BroadcastSceneReady.
/// </summary>
private void ProcessBatchedSceneComponents()
{
if (pendingSceneComponents.Count == 0)
{
isLoadingScene = false;
sceneBeingLoaded = "";
return;
}
LogDebug($"Processing {pendingSceneComponents.Count} batched components for scene: {sceneBeingLoaded}");
// Sort by ManagedAwake priority (lower values first)
pendingSceneComponents.Sort((a, b) => a.ManagedAwakePriority.CompareTo(b.ManagedAwakePriority));
// Call OnManagedAwake in priority order
foreach (var component in pendingSceneComponents)
{
if (component == null) continue;
try
{
component.InvokeManagedAwake();
HandleAutoRegistrations(component);
LogDebug($"Processed batched component: {component.gameObject.name} (Priority: {component.ManagedAwakePriority})");
}
catch (Exception ex)
{
Debug.LogError($"[LifecycleManager] Error in OnManagedAwake for batched component {component.gameObject.name}: {ex}");
}
}
// Clear state
pendingSceneComponents.Clear();
isLoadingScene = false;
sceneBeingLoaded = "";
}
/// <summary>
@@ -264,41 +322,21 @@ namespace Core.Lifecycle
}
}
/// <summary>
/// Broadcast OnSaveRequested to components in the specified scene (reverse priority order).
/// </summary>
public void BroadcastSaveRequested(string sceneName)
{
LogDebug($"Broadcasting SaveRequested for scene: {sceneName}");
// Iterate backwards (high priority → low priority)
for (int i = saveRequestedList.Count - 1; i >= 0; i--)
{
var component = saveRequestedList[i];
if (component == null) continue;
if (componentScenes.TryGetValue(component, out string compScene) && compScene == sceneName)
{
try
{
component.InvokeSaveRequested();
}
catch (Exception ex)
{
Debug.LogError($"[LifecycleManager] Error in OnSaveRequested for {component.gameObject.name}: {ex}");
}
}
}
}
/// <summary>
/// Broadcast OnSceneReady to components in the specified scene (priority order).
/// If scene loading mode is active, processes batched components first.
/// </summary>
public void BroadcastSceneReady(string sceneName)
{
LogDebug($"Broadcasting SceneReady for scene: {sceneName}");
currentSceneReady = sceneName;
// If we were in scene loading mode for this scene, process batched components first
if (isLoadingScene && sceneBeingLoaded == sceneName)
{
ProcessBatchedSceneComponents();
}
foreach (var component in sceneReadyList)
{
if (component == null) continue;
@@ -318,28 +356,179 @@ namespace Core.Lifecycle
}
/// <summary>
/// Broadcast OnRestoreRequested to components in the specified scene (priority order).
/// Broadcasts scene save request to all registered components that opt-in.
/// Collects and returns serialized data from components that return non-null values.
/// Called by SaveLoadManager during scene transitions.
/// </summary>
public void BroadcastRestoreRequested(string sceneName)
public Dictionary<string, string> BroadcastSceneSaveRequested()
{
LogDebug($"Broadcasting RestoreRequested for scene: {sceneName}");
foreach (var component in restoreRequestedList)
var saveData = new Dictionary<string, string>();
foreach (var component in managedAwakeList)
{
if (component == null) continue;
if (component == null || !component.AutoRegisterForSave) continue;
try
{
string serializedData = component.InvokeSceneSaveRequested();
if (!string.IsNullOrEmpty(serializedData))
{
saveData[component.SaveId] = serializedData;
LogDebug($"Collected scene save data from: {component.SaveId}");
}
}
catch (Exception ex)
{
Debug.LogError($"[LifecycleManager] Exception during scene save for {component.SaveId}: {ex}");
}
}
LogDebug($"Collected scene save data from {saveData.Count} components");
return saveData;
}
if (componentScenes.TryGetValue(component, out string compScene) && compScene == sceneName)
/// <summary>
/// Broadcasts global save request to all registered components that opt-in.
/// Collects and returns serialized data from components that return non-null values.
/// Called by SaveLoadManager when writing save file to disk (quit, manual save).
/// </summary>
public Dictionary<string, string> BroadcastGlobalSaveRequested()
{
var saveData = new Dictionary<string, string>();
foreach (var component in managedAwakeList)
{
if (component == null || !component.AutoRegisterForSave) continue;
try
{
string serializedData = component.InvokeGlobalSaveRequested();
if (!string.IsNullOrEmpty(serializedData))
{
saveData[component.SaveId] = serializedData;
LogDebug($"Collected global save data from: {component.SaveId}");
}
}
catch (Exception ex)
{
Debug.LogError($"[LifecycleManager] Exception during global save for {component.SaveId}: {ex}");
}
}
LogDebug($"Collected global save data from {saveData.Count} components");
return saveData;
}
/// <summary>
/// Broadcasts scene restore request to all registered components that opt-in.
/// Distributes serialized data to matching components by SaveId.
/// Called by SaveLoadManager during scene load.
/// </summary>
public void BroadcastSceneRestoreRequested(Dictionary<string, string> saveData)
{
if (saveData == null) return;
int restoredCount = 0;
foreach (var component in managedAwakeList)
{
if (component == null || !component.AutoRegisterForSave) continue;
if (saveData.TryGetValue(component.SaveId, out string serializedData))
{
try
{
component.InvokeRestoreRequested();
component.InvokeSceneRestoreRequested(serializedData);
restoredCount++;
LogDebug($"Restored scene data to: {component.SaveId}");
}
catch (Exception ex)
{
Debug.LogError($"[LifecycleManager] Error in OnRestoreRequested for {component.gameObject.name}: {ex}");
Debug.LogError($"[LifecycleManager] Exception during scene restore for {component.SaveId}: {ex}");
}
}
}
LogDebug($"Restored scene data to {restoredCount} components");
}
/// <summary>
/// Broadcasts global restore request to all registered components that opt-in.
/// Distributes serialized data to matching components by SaveId.
/// Called by SaveLoadManager during initial boot load.
/// </summary>
public void BroadcastGlobalRestoreRequested(Dictionary<string, string> saveData)
{
if (saveData == null) return;
int restoredCount = 0;
foreach (var component in managedAwakeList)
{
if (component == null || !component.AutoRegisterForSave) continue;
if (saveData.TryGetValue(component.SaveId, out string serializedData))
{
try
{
component.InvokeGlobalRestoreRequested(serializedData);
restoredCount++;
LogDebug($"Restored global data to: {component.SaveId}");
}
catch (Exception ex)
{
Debug.LogError($"[LifecycleManager] Exception during global restore for {component.SaveId}: {ex}");
}
}
}
LogDebug($"Restored global data to {restoredCount} components");
}
/// <summary>
/// Broadcasts global load completed event to all registered components that opt-in.
/// Called ONCE after save file is successfully loaded on game boot.
/// NOT called during scene transitions.
/// </summary>
public void BroadcastGlobalLoadCompleted()
{
LogDebug("Broadcasting GlobalLoadCompleted");
foreach (var component in managedAwakeList)
{
if (component == null || !component.AutoRegisterForSave) continue;
try
{
component.InvokeGlobalLoadCompleted();
}
catch (Exception ex)
{
Debug.LogError($"[LifecycleManager] Exception during global load for {component.name}: {ex}");
}
}
}
/// <summary>
/// Broadcasts global save started event to all registered components that opt-in.
/// Called ONCE before save file is written to disk.
/// NOT called during scene transitions.
/// </summary>
public void BroadcastGlobalSaveStarted()
{
LogDebug("Broadcasting GlobalSaveStarted");
foreach (var component in managedAwakeList)
{
if (component == null || !component.AutoRegisterForSave) continue;
try
{
component.InvokeGlobalSaveStarted();
}
catch (Exception ex)
{
Debug.LogError($"[LifecycleManager] Exception during global save for {component.name}: {ex}");
}
}
}
#endregion

View File

@@ -57,6 +57,27 @@ namespace Core.Lifecycle
/// </summary>
public virtual bool AutoRegisterPausable => false;
/// <summary>
/// If true, this component participates in the save/load system.
/// Components should override OnSaveRequested() and OnRestoreRequested().
/// Default: false
/// </summary>
public virtual bool AutoRegisterForSave => false;
/// <summary>
/// Unique identifier for this component in the save system.
/// Default: "SceneName/GameObjectName"
/// Override ONLY for special cases (e.g., singletons like "PlayerController", or custom IDs).
/// </summary>
public virtual string SaveId
{
get
{
string sceneName = gameObject.scene.IsValid() ? gameObject.scene.name : "UnknownScene";
return $"{sceneName}/{gameObject.name}";
}
}
#endregion
#region Public Accessors (for LifecycleManager)
@@ -65,9 +86,13 @@ namespace Core.Lifecycle
public void InvokeManagedAwake() => OnManagedAwake();
public void InvokeSceneUnloading() => OnSceneUnloading();
public void InvokeSceneReady() => OnSceneReady();
public void InvokeSaveRequested() => OnSaveRequested();
public void InvokeRestoreRequested() => OnRestoreRequested();
public string InvokeSceneSaveRequested() => OnSceneSaveRequested();
public void InvokeSceneRestoreRequested(string data) => OnSceneRestoreRequested(data);
public string InvokeGlobalSaveRequested() => OnGlobalSaveRequested();
public void InvokeGlobalRestoreRequested(string data) => OnGlobalRestoreRequested(data);
public void InvokeManagedDestroy() => OnManagedDestroy();
public void InvokeGlobalLoadCompleted() => OnGlobalLoadCompleted();
public void InvokeGlobalSaveStarted() => OnGlobalSaveStarted();
#endregion
@@ -158,25 +183,91 @@ namespace Core.Lifecycle
}
/// <summary>
/// Called before scene unloads to save data via SaveLoadManager.
/// Called in REVERSE priority order (higher values execute first).
/// Integrates with existing SaveLoadManager save system.
/// Return serialized state string (e.g., JsonUtility.ToJson(myData)).
/// Called during scene transitions to save scene-specific state.
/// Return serialized data (e.g., JsonUtility.ToJson(myData)).
/// Return null if component has no scene-specific state to save.
///
/// TIMING:
/// - Called BEFORE scene unload during scene transitions
/// - Frequency: Every scene transition
/// - Use for: Level progress, object positions, puzzle states
/// </summary>
protected virtual void OnSaveRequested()
protected virtual string OnSceneSaveRequested()
{
// Override in derived classes
return null; // Default: no data to save
}
/// <summary>
/// Called after scene loads to restore data via SaveLoadManager.
/// Called in priority order (lower values execute first).
/// Integrates with existing SaveLoadManager restore system.
/// Receives serialized state string to restore from.
/// Called during scene transitions to restore scene-specific state.
/// Receives previously serialized data (from OnSceneSaveRequested).
///
/// TIMING:
/// - Called AFTER scene load, during OnSceneReady phase
/// - Frequency: Every scene transition
/// - Use for: Restoring level progress, object positions, puzzle states
/// </summary>
protected virtual void OnRestoreRequested()
protected virtual void OnSceneRestoreRequested(string serializedData)
{
// Override in derived classes
// Default: no-op
}
/// <summary>
/// Called once on game boot to restore global persistent state.
/// Receives data that was saved via OnGlobalSaveRequested.
///
/// TIMING:
/// - Called ONCE on game boot after save file is read
/// - NOT called during scene transitions
/// - Use for: Player inventory, unlocked features, card collections
/// </summary>
protected virtual void OnGlobalRestoreRequested(string serializedData)
{
// Default: no-op
}
/// <summary>
/// Called once before game save file is written to disk.
/// Return serialized data for global persistent state.
/// Return null if component has no global state to save.
///
/// TIMING:
/// - Called ONCE before save file is written (on quit, manual save, etc.)
/// - NOT called during scene transitions
/// - Use for: Player inventory, unlocked features, card collections
/// </summary>
protected virtual string OnGlobalSaveRequested()
{
return null; // Default: no data to save
}
/// <summary>
/// Called once when game save data is initially loaded from disk.
/// Use for global managers that need to react to load completion.
/// Does NOT receive data - use OnGlobalRestoreRequested for that.
///
/// TIMING:
/// - Called ONCE on game boot after all restore operations complete
/// - NOT called during scene transitions
/// - Use for: Triggering UI updates, broadcasting load events
/// </summary>
protected virtual void OnGlobalLoadCompleted()
{
// Default: no-op
}
/// <summary>
/// Called once before save file is written to disk.
/// Use for global managers that need to perform cleanup before save.
/// Does NOT return data - use OnGlobalSaveRequested for that.
///
/// TIMING:
/// - Called ONCE before save file is written
/// - NOT called during scene transitions
/// - Use for: Final validation, cleanup operations
/// </summary>
protected virtual void OnGlobalSaveStarted()
{
// Default: no-op
}
/// <summary>

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
@@ -62,10 +62,6 @@ namespace Core.SaveLoad
{
Logging.Debug("[SaveLoadManager] Initialized");
#if UNITY_EDITOR
DiscoverInactiveSaveables("RestoreInEditor");
#endif
// Load save data if save system is enabled (depends on settings from GameManager)
if (DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().useSaveLoadSystem)
{
@@ -75,16 +71,20 @@ namespace Core.SaveLoad
protected override void OnSceneReady()
{
// Discover and register inactive SaveableInteractables in the newly loaded scene
string sceneName = UnityEngine.SceneManagement.SceneManager.GetActiveScene().name;
DiscoverInactiveSaveables(sceneName);
// SaveableInteractables now auto-register via ManagedBehaviour lifecycle
// No need to discover and register them manually
}
protected override void OnSaveRequested()
protected override string OnSceneSaveRequested()
{
// Scene is about to unload - this is now handled by SceneManagerService
// which calls Save() globally before scene transitions
Logging.Debug($"[SaveLoadManager] OnSaveRequested called");
// SaveLoadManager orchestrates saves, doesn't participate in them
return null;
}
protected override string OnGlobalSaveRequested()
{
// SaveLoadManager orchestrates saves, doesn't participate in them
return null;
}
private void OnApplicationQuit()
@@ -180,40 +180,6 @@ namespace Core.SaveLoad
return participant;
}
#endregion
#region Scene Lifecycle
/// <summary>
/// Discovers and registers inactive SaveableInteractables in the scene.
/// Active SaveableInteractables register themselves via their Start() method.
/// </summary>
private void DiscoverInactiveSaveables(string sceneName)
{
Logging.Debug($"[SaveLoadManager] Scene '{sceneName}' loaded. Discovering inactive SaveableInteractables...");
// Find ONLY INACTIVE SaveableInteractables (active ones will register themselves via Start())
var inactiveSaveables = FindObjectsByType(
typeof(Interactions.SaveableInteractable),
FindObjectsInactive.Include,
FindObjectsSortMode.None
);
int registeredCount = 0;
foreach (var obj in inactiveSaveables)
{
var saveable = obj as Interactions.SaveableInteractable;
if (saveable != null && !saveable.gameObject.activeInHierarchy)
{
// Only register if it's actually inactive
RegisterParticipant(saveable);
registeredCount++;
}
}
Logging.Debug($"[SaveLoadManager] Discovered and registered {registeredCount} inactive SaveableInteractables");
}
#endregion
@@ -260,8 +226,31 @@ namespace Core.SaveLoad
return;
IsRestoringState = true;
int restoredCount = 0;
// Build dictionary for efficient lookup
var saveDataDict = new Dictionary<string, string>();
foreach (var entry in currentSaveData.participantStates)
{
saveDataDict[entry.saveId] = entry.serializedState;
}
// NEW: Restore GLOBAL data via LifecycleManager (called ONCE on boot)
if (Lifecycle.LifecycleManager.Instance != null)
{
Lifecycle.LifecycleManager.Instance.BroadcastGlobalRestoreRequested(saveDataDict);
Logging.Debug($"[SaveLoadManager] Broadcast GLOBAL restore to LifecycleManager");
}
// NEW: Restore SCENE data via LifecycleManager (for currently loaded scenes)
if (Lifecycle.LifecycleManager.Instance != null)
{
Lifecycle.LifecycleManager.Instance.BroadcastSceneRestoreRequested(saveDataDict);
Logging.Debug($"[SaveLoadManager] Broadcast SCENE restore to LifecycleManager");
}
// EXISTING: Restore ISaveParticipants (backward compatibility)
int restoredCount = 0;
// Clear pending queue at the start
pendingParticipants.Clear();
@@ -272,19 +261,17 @@ namespace Core.SaveLoad
string saveId = kvp.Key;
ISaveParticipant participant = kvp.Value;
// Find the participant state in the list
var entry = currentSaveData.participantStates.Find(e => e.saveId == saveId);
if (entry != null && !string.IsNullOrEmpty(entry.serializedState))
if (saveDataDict.TryGetValue(saveId, out string serializedState))
{
try
{
participant.RestoreState(entry.serializedState);
participant.RestoreState(serializedState);
restoredCount++;
Logging.Debug($"[SaveLoadManager] Restored state for participant: {saveId}");
Logging.Debug($"[SaveLoadManager] Restored ISaveParticipant: {saveId}");
}
catch (Exception ex)
{
Logging.Warning($"[SaveLoadManager] Exception while restoring state for '{saveId}': {ex}");
Logging.Warning($"[SaveLoadManager] Exception while restoring '{saveId}': {ex}");
}
}
}
@@ -330,7 +317,7 @@ namespace Core.SaveLoad
pendingParticipants.Clear();
IsRestoringState = false;
Logging.Debug($"[SaveLoadManager] Restored state for {restoredCount} participants + {totalPendingRestored} pending participants");
Logging.Debug($"[SaveLoadManager] Restored {restoredCount} ISaveParticipants + {totalPendingRestored} pending participants");
OnParticipantStatesRestored?.Invoke();
}
@@ -341,6 +328,76 @@ namespace Core.SaveLoad
return Path.Combine(DefaultSaveFolder, $"save_{slot}.json");
}
/// <summary>
/// Saves scene-specific data during scene transitions.
/// This updates the in-memory save data but does NOT write to disk.
/// Call Save() to persist to disk.
/// </summary>
public void SaveSceneData()
{
if (currentSaveData == null)
{
Logging.Warning("[SaveLoadManager] Cannot save scene data - no save data loaded");
return;
}
Logging.Debug("[SaveLoadManager] Saving scene-specific data...");
// Collect scene data from LifecycleManager
if (Lifecycle.LifecycleManager.Instance != null)
{
var sceneData = Lifecycle.LifecycleManager.Instance.BroadcastSceneSaveRequested();
// Remove old scene data and add new
if (currentSaveData.participantStates != null)
{
// Remove existing entries for these SaveIds (to avoid duplicates)
currentSaveData.participantStates.RemoveAll(entry => sceneData.ContainsKey(entry.saveId));
// Add new scene data
foreach (var kvp in sceneData)
{
currentSaveData.participantStates.Add(new ParticipantStateEntry
{
saveId = kvp.Key,
serializedState = kvp.Value
});
}
}
Logging.Debug($"[SaveLoadManager] Updated {sceneData.Count} scene data entries in memory");
}
}
/// <summary>
/// Restores scene-specific data after scene load.
/// Distributes data to components in the newly loaded scene.
/// </summary>
public void RestoreSceneData()
{
if (currentSaveData == null || currentSaveData.participantStates == null)
{
Logging.Debug("[SaveLoadManager] No scene data to restore");
return;
}
Logging.Debug("[SaveLoadManager] Restoring scene-specific data...");
// Build dictionary for efficient lookup
var saveDataDict = new Dictionary<string, string>();
foreach (var entry in currentSaveData.participantStates)
{
saveDataDict[entry.saveId] = entry.serializedState;
}
// Restore scene data via LifecycleManager
if (Lifecycle.LifecycleManager.Instance != null)
{
Lifecycle.LifecycleManager.Instance.BroadcastSceneRestoreRequested(saveDataDict);
Logging.Debug($"[SaveLoadManager] Broadcast scene restore to LifecycleManager");
}
}
/// <summary>
/// Entry point to save to a named slot. Starts an async coroutine that writes to disk.
/// Fires OnSaveCompleted when finished.
@@ -397,14 +454,44 @@ namespace Core.SaveLoad
{
currentSaveData.participantStates = new List<ParticipantStateEntry>();
}
else
// NOTE: We do NOT clear participantStates here!
// We preserve data from all scenes and update/add as needed.
// This allows Level A data to persist when saving from Level B.
int savedCount = 0;
// NEW: Broadcast global save started event (ONCE)
if (Lifecycle.LifecycleManager.Instance != null)
{
currentSaveData.participantStates.Clear();
Lifecycle.LifecycleManager.Instance.BroadcastGlobalSaveStarted();
}
// Capture state from all registered participants directly into the list
// Create a snapshot to avoid collection modification during iteration
int savedCount = 0;
// Build a dictionary of all new data to save
var allNewData = new Dictionary<string, string>();
// NEW: Collect GLOBAL data from ManagedBehaviours via LifecycleManager
if (Lifecycle.LifecycleManager.Instance != null)
{
var globalData = Lifecycle.LifecycleManager.Instance.BroadcastGlobalSaveRequested();
foreach (var kvp in globalData)
{
allNewData[kvp.Key] = kvp.Value;
}
Logging.Debug($"[SaveLoadManager] Collected {globalData.Count} GLOBAL save states");
}
// NEW: Collect SCENE data from all loaded scenes
if (Lifecycle.LifecycleManager.Instance != null)
{
var sceneData = Lifecycle.LifecycleManager.Instance.BroadcastSceneSaveRequested();
foreach (var kvp in sceneData)
{
allNewData[kvp.Key] = kvp.Value;
}
Logging.Debug($"[SaveLoadManager] Collected {sceneData.Count} SCENE save states");
}
// EXISTING: Collect data from ISaveParticipants (backward compatibility)
foreach (var kvp in participants.ToList())
{
string saveId = kvp.Key;
@@ -413,13 +500,8 @@ namespace Core.SaveLoad
try
{
string serializedState = participant.SerializeState();
currentSaveData.participantStates.Add(new ParticipantStateEntry
{
saveId = saveId,
serializedState = serializedState
});
savedCount++;
Logging.Debug($"[SaveLoadManager] Captured state for participant: {saveId}");
allNewData[saveId] = serializedState;
Logging.Debug($"[SaveLoadManager] Captured state for ISaveParticipant: {saveId}");
}
catch (Exception ex)
{
@@ -427,7 +509,28 @@ namespace Core.SaveLoad
}
}
Logging.Debug($"[SaveLoadManager] Captured state from {savedCount} participants");
// Update existing entries or add new ones (preserves data from unloaded scenes)
foreach (var kvp in allNewData)
{
var existingEntry = currentSaveData.participantStates.Find(e => e.saveId == kvp.Key);
if (existingEntry != null)
{
// Update existing entry
existingEntry.serializedState = kvp.Value;
}
else
{
// Add new entry
currentSaveData.participantStates.Add(new ParticipantStateEntry
{
saveId = kvp.Key,
serializedState = kvp.Value
});
}
savedCount++;
}
Logging.Debug($"[SaveLoadManager] Captured state from {savedCount} total participants");
json = JsonUtility.ToJson(currentSaveData, true);
@@ -546,6 +649,12 @@ namespace Core.SaveLoad
// Restore state for any already-registered participants
RestoreAllParticipantStates();
// NEW: Broadcast global load completed event (ONCE, on boot)
if (Lifecycle.LifecycleManager.Instance != null)
{
Lifecycle.LifecycleManager.Instance.BroadcastGlobalLoadCompleted();
}
OnLoadCompleted?.Invoke(slot);
Logging.Debug($"[SaveLoadManager] Load completed for slot '{slot}'");
}

View File

@@ -309,18 +309,14 @@ namespace Core
LogDebugMessage($"Broadcasting OnSceneUnloading for: {oldSceneName}");
LifecycleManager.Instance?.BroadcastSceneUnloading(oldSceneName);
// PHASE 3: Broadcast save request - components save their level-specific data
LogDebugMessage($"Broadcasting OnSaveRequested for: {oldSceneName}");
LifecycleManager.Instance?.BroadcastSaveRequested(oldSceneName);
// PHASE 4: Trigger global save if save system is enabled
// PHASE 3: Save scene-specific data via SaveLoadManager
if (SaveLoadManager.Instance != null)
{
var debugSettings = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>();
if (debugSettings.useSaveLoadSystem)
{
LogDebugMessage("Saving global game state");
SaveLoadManager.Instance.Save();
LogDebugMessage($"Saving scene data for: {oldSceneName}");
SaveLoadManager.Instance.SaveSceneData();
}
}
@@ -355,19 +351,30 @@ namespace Core
SceneManager.LoadScene(BootstrapSceneName, LoadSceneMode.Additive);
}
// PHASE 8: Load new gameplay scene
// 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 9: Broadcast scene ready - components can now initialize scene-specific state
// PHASE 10: Broadcast scene ready - processes batched components in priority order, then calls OnSceneReady
LogDebugMessage($"Broadcasting OnSceneReady for: {newSceneName}");
LifecycleManager.Instance?.BroadcastSceneReady(newSceneName);
// PHASE 10: Broadcast restore request - components restore their level-specific data
LogDebugMessage($"Broadcasting OnRestoreRequested for: {newSceneName}");
LifecycleManager.Instance?.BroadcastRestoreRequested(newSceneName);
// PHASE 11: Restore scene-specific data via SaveLoadManager
if (SaveLoadManager.Instance != null)
{
var debugSettings = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>();
if (debugSettings.useSaveLoadSystem)
{
LogDebugMessage($"Restoring scene data for: {newSceneName}");
SaveLoadManager.Instance.RestoreSceneData();
}
}
// PHASE 11: Only hide the loading screen if autoHideLoadingScreen is true
// PHASE 12: Only hide the loading screen if autoHideLoadingScreen is true
if (autoHideLoadingScreen && _loadingScreen != null)
{
_loadingScreen.HideLoadingScreen();

View File

@@ -1,4 +1,4 @@
using System.Collections;
using System.Collections;
using AppleHills.Core.Settings;
using Core.Lifecycle;
using Settings;
@@ -32,28 +32,30 @@ namespace Core
_logVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().sceneLogVerbosity;
LogDebugMessage("Initialized");
// Subscribe to sceneLoaded event immediately
SceneManager.sceneLoaded += OnSceneLoaded;
#if UNITY_EDITOR
// When playing in the editor, manually invoke OnSceneLoaded for the currently active scene
if (Application.isPlaying)
{
OnSceneLoaded(SceneManager.GetActiveScene(), LoadSceneMode.Single);
}
#endif
}
protected override void OnManagedAwake()
{
// Initialization already done in Awake
#if UNITY_EDITOR
// When playing in the editor, manually invoke orientation check for the currently active scene
if (Application.isPlaying)
{
HandleSceneOrientation(SceneManager.GetActiveScene().name);
}
#endif
}
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
protected override void OnSceneReady()
{
// Handle orientation when scene is ready
// Note: This fires for the scene that just loaded, LifecycleManager tracks which scene
string sceneName = UnityEngine.SceneManagement.SceneManager.GetActiveScene().name;
HandleSceneOrientation(sceneName);
}
private void HandleSceneOrientation(string sceneName)
{
// Determine desired orientation for this scene
string sceneName = scene.name;
ScreenOrientationRequirement requirement = ScreenOrientationRequirement.NotApplicable;
if (sceneName.ToLower().Contains("bootstrap"))
@@ -94,7 +96,6 @@ namespace Core
protected override void OnDestroy()
{
base.OnDestroy(); // Important: call base
SceneManager.sceneLoaded -= OnSceneLoaded;
}
/// <summary>