SaveLoad using managed lifecycle

This commit is contained in:
Michal Pikulski
2025-11-04 20:01:27 +01:00
committed by Michal Pikulski
parent 3e835ed3b8
commit b932be2232
19 changed files with 1042 additions and 627 deletions

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}'");
}