Refactor interactions, introduce template-method lifecycle management, work on save-load system (#51)
# Lifecycle Management & Save System Revamp
## Overview
Complete overhaul of game lifecycle management, interactable system, and save/load architecture. Introduces centralized `ManagedBehaviour` base class for consistent initialization ordering and lifecycle hooks across all systems.
## Core Architecture
### New Lifecycle System
- **`LifecycleManager`**: Centralized coordinator for all managed objects
- **`ManagedBehaviour`**: Base class replacing ad-hoc initialization patterns
- `OnManagedAwake()`: Priority-based initialization (0-100, lower = earlier)
- `OnSceneReady()`: Scene-specific setup after managers ready
- Replaces `BootCompletionService` (deleted)
- **Priority groups**: Infrastructure (0-20) → Game Systems (30-50) → Data (60-80) → UI/Gameplay (90-100)
- **Editor support**: `EditorLifecycleBootstrap` ensures lifecycle works in editor mode
### Unified SaveID System
- Consistent format: `{ParentName}_{ComponentType}`
- Auto-registration via `AutoRegisterForSave = true`
- New `DebugSaveIds` editor tool for inspection
## Save/Load Improvements
### Enhanced State Management
- **Extended SaveLoadData**: Unlocked minigames, card collection states, combination items, slot occupancy
- **Async loading**: `ApplyCardCollectionState()` waits for card definitions before restoring
- **New `SaveablePlayableDirector`**: Timeline sequences save/restore playback state
- **Fixed race conditions**: Proper initialization ordering prevents data corruption
## Interactable & Pickup System
- Migrated to `OnManagedAwake()` for consistent initialization
- Template method pattern for state restoration (`RestoreInteractionState()`)
- Fixed combination item save/load bugs (items in slots vs. follower hand)
- Dynamic spawning support for combined items on load
- **Breaking**: `Interactable.Awake()` now sealed, use `OnManagedAwake()` instead
## UI System Changes
- **AlbumViewPage** and **BoosterNotificationDot**: Migrated to `ManagedBehaviour`
- **Fixed menu persistence bug**: Menus no longer reappear after scene transitions
- **Pause Menu**: Now reacts to all scene loads (not just first scene)
- **Orientation Enforcer**: Enforces per-scene via `SceneManagementService`
- **Loading Screen**: Integrated with new lifecycle
## ⚠️ Breaking Changes
1. **`BootCompletionService` removed** → Use `ManagedBehaviour.OnManagedAwake()` with priority
2. **`Interactable.Awake()` sealed** → Override `OnManagedAwake()` instead
3. **SaveID format changed** → Now `{ParentName}_{ComponentType}` consistently
4. **MonoBehaviours needing init ordering** → Must inherit from `ManagedBehaviour`
Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com>
Co-authored-by: Michal Pikulski <michal@foolhardyhorizons.com>
Reviewed-on: #51
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
using UnityEngine;
|
||||
using Pixelplacement;
|
||||
using Bootstrap;
|
||||
|
||||
namespace Core.SaveLoad
|
||||
{
|
||||
@@ -82,18 +81,15 @@ namespace Core.SaveLoad
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// Register with save system (no validation needed - we auto-generate ID)
|
||||
BootCompletionService.RegisterInitAction(() =>
|
||||
// Direct registration - SaveLoadManager guaranteed available (priority 25)
|
||||
if (SaveLoadManager.Instance != null)
|
||||
{
|
||||
if (SaveLoadManager.Instance != null)
|
||||
{
|
||||
SaveLoadManager.Instance.RegisterParticipant(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[SaveableStateMachine] SaveLoadManager.Instance is null, cannot register '{name}'", this);
|
||||
}
|
||||
});
|
||||
SaveLoadManager.Instance.RegisterParticipant(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[AppleMachine] SaveLoadManager not available for '{name}'", this);
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
@@ -127,9 +123,8 @@ namespace Core.SaveLoad
|
||||
return $"{sceneName}/{customSaveId}";
|
||||
}
|
||||
|
||||
// Auto-generate from hierarchy path
|
||||
string hierarchyPath = GetHierarchyPath();
|
||||
return $"{sceneName}/StateMachine_{hierarchyPath}";
|
||||
// Match ManagedBehaviour convention: SceneName/GameObjectName/ComponentType
|
||||
return $"{sceneName}/{gameObject.name}/AppleMachine";
|
||||
}
|
||||
|
||||
private string GetSceneName()
|
||||
@@ -137,19 +132,6 @@ namespace Core.SaveLoad
|
||||
return gameObject.scene.name;
|
||||
}
|
||||
|
||||
private string GetHierarchyPath()
|
||||
{
|
||||
string path = gameObject.name;
|
||||
Transform parent = transform.parent;
|
||||
|
||||
while (parent != null)
|
||||
{
|
||||
path = parent.name + "/" + path;
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
public string SerializeState()
|
||||
{
|
||||
|
||||
@@ -4,20 +4,20 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using AppleHills.Core.Settings;
|
||||
using Bootstrap;
|
||||
using Core.Lifecycle;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Core.SaveLoad
|
||||
{
|
||||
/// <summary>
|
||||
/// Save/Load manager that follows the project's bootstrap pattern.
|
||||
/// Save/Load manager that follows the project's lifecycle pattern.
|
||||
/// - Singleton instance
|
||||
/// - Registers a post-boot init action with BootCompletionService
|
||||
/// - Inherits from ManagedBehaviour for lifecycle integration
|
||||
/// - Manages participant registration for save/load operations
|
||||
/// - Exposes simple async Save/Load methods
|
||||
/// - Fires events on completion
|
||||
/// </summary>
|
||||
public class SaveLoadManager : MonoBehaviour
|
||||
public class SaveLoadManager : ManagedBehaviour
|
||||
{
|
||||
private static SaveLoadManager _instance;
|
||||
public static SaveLoadManager Instance => _instance;
|
||||
@@ -43,24 +43,49 @@ namespace Core.SaveLoad
|
||||
public event Action<string> OnLoadCompleted;
|
||||
public event Action OnParticipantStatesRestored;
|
||||
|
||||
void Awake()
|
||||
// ManagedBehaviour configuration
|
||||
public override int ManagedAwakePriority => 20; // After GameManager and SceneManagerService
|
||||
|
||||
private new void Awake()
|
||||
{
|
||||
base.Awake(); // CRITICAL: Register with LifecycleManager!
|
||||
|
||||
// Set instance immediately so it's available before OnManagedAwake() is called
|
||||
_instance = this;
|
||||
|
||||
// Initialize critical state immediately
|
||||
IsSaveDataLoaded = false;
|
||||
IsRestoringState = false;
|
||||
BootCompletionService.RegisterInitAction(InitializePostBoot);
|
||||
}
|
||||
|
||||
private void Start()
|
||||
protected override void OnManagedAwake()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
OnSceneLoadCompleted("RestoreInEditor");
|
||||
#endif
|
||||
Logging.Debug("[SaveLoadManager] Initialized");
|
||||
|
||||
// Load save data if save system is enabled (depends on settings from GameManager)
|
||||
if (DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().useSaveLoadSystem)
|
||||
{
|
||||
Load();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnSceneReady()
|
||||
{
|
||||
// SaveableInteractables now auto-register via ManagedBehaviour lifecycle
|
||||
// No need to discover and register them manually
|
||||
}
|
||||
|
||||
protected override string OnSceneSaveRequested()
|
||||
{
|
||||
// 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()
|
||||
{
|
||||
@@ -70,30 +95,14 @@ namespace Core.SaveLoad
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializePostBoot()
|
||||
{
|
||||
Logging.Debug("[SaveLoadManager] Post-boot initialization complete");
|
||||
|
||||
// Subscribe to scene lifecycle events if SceneManagerService is available
|
||||
if (SceneManagerService.Instance != null)
|
||||
{
|
||||
SceneManagerService.Instance.SceneLoadCompleted += OnSceneLoadCompleted;
|
||||
SceneManagerService.Instance.SceneUnloadStarted += OnSceneUnloadStarted;
|
||||
Logging.Debug("[SaveLoadManager] Subscribed to SceneManagerService events");
|
||||
}
|
||||
}
|
||||
// ...existing code...
|
||||
|
||||
void OnDestroy()
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
base.OnDestroy(); // Important: call base to unregister from LifecycleManager
|
||||
|
||||
if (_instance == this)
|
||||
{
|
||||
// Unsubscribe from scene events
|
||||
if (SceneManagerService.Instance != null)
|
||||
{
|
||||
SceneManagerService.Instance.SceneLoadCompleted -= OnSceneLoadCompleted;
|
||||
SceneManagerService.Instance.SceneUnloadStarted -= OnSceneUnloadStarted;
|
||||
}
|
||||
|
||||
_instance = null;
|
||||
}
|
||||
}
|
||||
@@ -173,40 +182,93 @@ namespace Core.SaveLoad
|
||||
|
||||
#endregion
|
||||
|
||||
#region Scene Lifecycle
|
||||
#region Unlocked Minigames Management
|
||||
|
||||
private void OnSceneLoadCompleted(string sceneName)
|
||||
/// <summary>
|
||||
/// Marks a minigame as unlocked in the global save data.
|
||||
/// This is separate from scene-specific participant states and persists across all saves.
|
||||
/// </summary>
|
||||
/// <param name="minigameName">The name/identifier of the minigame (typically scene name)</param>
|
||||
public void UnlockMinigame(string minigameName)
|
||||
{
|
||||
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)
|
||||
if (string.IsNullOrEmpty(minigameName))
|
||||
{
|
||||
var saveable = obj as Interactions.SaveableInteractable;
|
||||
if (saveable != null && !saveable.gameObject.activeInHierarchy)
|
||||
{
|
||||
// Only register if it's actually inactive
|
||||
RegisterParticipant(saveable);
|
||||
registeredCount++;
|
||||
}
|
||||
Logging.Warning("[SaveLoadManager] Attempted to unlock minigame with null or empty name");
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentSaveData == null)
|
||||
{
|
||||
Logging.Warning("[SaveLoadManager] Cannot unlock minigame - no save data loaded");
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentSaveData.unlockedMinigames == null)
|
||||
{
|
||||
currentSaveData.unlockedMinigames = new System.Collections.Generic.List<string>();
|
||||
}
|
||||
|
||||
if (!currentSaveData.unlockedMinigames.Contains(minigameName))
|
||||
{
|
||||
currentSaveData.unlockedMinigames.Add(minigameName);
|
||||
Logging.Debug($"[SaveLoadManager] Unlocked minigame: {minigameName}");
|
||||
}
|
||||
|
||||
Logging.Debug($"[SaveLoadManager] Discovered and registered {registeredCount} inactive SaveableInteractables");
|
||||
}
|
||||
|
||||
private void OnSceneUnloadStarted(string sceneName)
|
||||
/// <summary>
|
||||
/// Checks if a minigame has been unlocked.
|
||||
/// </summary>
|
||||
/// <param name="minigameName">The name/identifier of the minigame</param>
|
||||
/// <returns>True if the minigame is unlocked, false otherwise</returns>
|
||||
public bool IsMinigameUnlocked(string minigameName)
|
||||
{
|
||||
Logging.Debug($"[SaveLoadManager] Scene '{sceneName}' unloading. Note: Participants should unregister themselves.");
|
||||
|
||||
// We don't force-clear here because participants should manage their own lifecycle
|
||||
// This allows for proper cleanup in OnDestroy
|
||||
if (string.IsNullOrEmpty(minigameName))
|
||||
return false;
|
||||
|
||||
if (currentSaveData == null || currentSaveData.unlockedMinigames == null)
|
||||
return false;
|
||||
|
||||
return currentSaveData.unlockedMinigames.Contains(minigameName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a read-only list of all unlocked minigames.
|
||||
/// </summary>
|
||||
public System.Collections.Generic.IReadOnlyList<string> GetUnlockedMinigames()
|
||||
{
|
||||
if (currentSaveData == null || currentSaveData.unlockedMinigames == null)
|
||||
return new System.Collections.Generic.List<string>();
|
||||
|
||||
return currentSaveData.unlockedMinigames.AsReadOnly();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all save data for a specific level/scene.
|
||||
/// Removes all participant states that belong to the specified scene.
|
||||
/// Useful for "restart level" functionality to wipe progress.
|
||||
/// </summary>
|
||||
/// <param name="sceneName">The name of the scene to clear data for</param>
|
||||
public void ClearLevelData(string sceneName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(sceneName))
|
||||
{
|
||||
Logging.Warning("[SaveLoadManager] Cannot clear level data - scene name is null or empty");
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentSaveData == null || currentSaveData.participantStates == null)
|
||||
{
|
||||
Logging.Warning("[SaveLoadManager] Cannot clear level data - no save data loaded");
|
||||
return;
|
||||
}
|
||||
|
||||
// SaveId format is "SceneName/ObjectName/ComponentType"
|
||||
// Remove all entries that start with "sceneName/"
|
||||
string scenePrefix = $"{sceneName}/";
|
||||
int removedCount = currentSaveData.participantStates.RemoveAll(entry =>
|
||||
entry.saveId.StartsWith(scenePrefix));
|
||||
|
||||
Logging.Debug($"[SaveLoadManager] Cleared {removedCount} save entries for level: {sceneName}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -254,8 +316,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();
|
||||
|
||||
@@ -266,19 +351,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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -324,7 +407,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();
|
||||
}
|
||||
|
||||
@@ -335,6 +418,115 @@ 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...");
|
||||
|
||||
// Build a dictionary of all data to save
|
||||
var allSceneData = new Dictionary<string, string>();
|
||||
|
||||
// Collect scene data from ManagedBehaviours via LifecycleManager
|
||||
if (Lifecycle.LifecycleManager.Instance != null)
|
||||
{
|
||||
var sceneData = Lifecycle.LifecycleManager.Instance.BroadcastSceneSaveRequested();
|
||||
foreach (var kvp in sceneData)
|
||||
{
|
||||
allSceneData[kvp.Key] = kvp.Value;
|
||||
}
|
||||
Logging.Debug($"[SaveLoadManager] Collected {sceneData.Count} ManagedBehaviour scene states");
|
||||
}
|
||||
|
||||
// Collect data from ISaveParticipants (all currently registered, identified by SaveId)
|
||||
foreach (var kvp in participants.ToList())
|
||||
{
|
||||
string saveId = kvp.Key;
|
||||
ISaveParticipant participant = kvp.Value;
|
||||
|
||||
try
|
||||
{
|
||||
string serializedState = participant.SerializeState();
|
||||
allSceneData[saveId] = serializedState;
|
||||
Logging.Debug($"[SaveLoadManager] Captured state for ISaveParticipant: {saveId}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.Warning($"[SaveLoadManager] Exception while serializing ISaveParticipant '{saveId}': {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
// Update existing entries or add new ones (matches SaveAsync() pattern)
|
||||
if (currentSaveData.participantStates != null)
|
||||
{
|
||||
int updatedCount = 0;
|
||||
|
||||
foreach (var kvp in allSceneData)
|
||||
{
|
||||
var existingEntry = currentSaveData.participantStates.Find(e => e.saveId == kvp.Key);
|
||||
if (existingEntry != null)
|
||||
{
|
||||
// Update existing entry in place
|
||||
existingEntry.serializedState = kvp.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add new entry
|
||||
currentSaveData.participantStates.Add(new ParticipantStateEntry
|
||||
{
|
||||
saveId = kvp.Key,
|
||||
serializedState = kvp.Value
|
||||
});
|
||||
}
|
||||
updatedCount++;
|
||||
}
|
||||
|
||||
Logging.Debug($"[SaveLoadManager] Updated {updatedCount} scene data entries in memory");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logging.Warning("[SaveLoadManager] participantStates list is null, cannot save scene data");
|
||||
}
|
||||
}
|
||||
|
||||
/// <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.
|
||||
@@ -391,14 +583,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;
|
||||
@@ -407,13 +629,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)
|
||||
{
|
||||
@@ -421,7 +638,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);
|
||||
@@ -540,6 +778,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}'");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user