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

@@ -166,7 +166,6 @@ MonoBehaviour:
interactionComplete:
m_PersistentCalls:
m_Calls: []
customSaveId:
itemData: {fileID: 11400000, guid: 0c6986639ca176a419c92f5a327d95ce, type: 2}
iconRenderer: {fileID: 7494677664706785084}
--- !u!1001 &8589202998731622905

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,7 @@ using Core;
using Core.Lifecycle;
using UnityEngine.SceneManagement;
using Cinematics;
using Core.SaveLoad;
namespace Bootstrap
{
@@ -204,8 +205,12 @@ namespace Bootstrap
LogDebugMessage($"Broadcasting OnSceneReady for: {mainSceneName}");
LifecycleManager.Instance?.BroadcastSceneReady(mainSceneName);
LogDebugMessage($"Broadcasting OnRestoreRequested for: {mainSceneName}");
LifecycleManager.Instance?.BroadcastRestoreRequested(mainSceneName);
// Restore scene data for the main menu
if (SaveLoadManager.Instance != null)
{
LogDebugMessage($"Restoring scene data for: {mainSceneName}");
SaveLoadManager.Instance.RestoreSceneData();
}
// Step 2: Scene is fully loaded, now hide the loading screen
// This will trigger OnInitialLoadingComplete via the event when animation completes

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}");
var saveData = new Dictionary<string, string>();
foreach (var component in restoreRequestedList)
foreach (var component in managedAwakeList)
{
if (component == null) continue;
if (component == null || !component.AutoRegisterForSave) continue;
if (componentScenes.TryGetValue(component, out string compScene) && compScene == sceneName)
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;
}
/// <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,6 +226,29 @@ namespace Core.SaveLoad
return;
IsRestoringState = true;
// 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
@@ -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>

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using AppleHills.Data.CardSystem;
@@ -14,14 +14,19 @@ namespace Data.CardSystem
{
/// <summary>
/// Manages the player's card collection, booster packs, and related operations.
/// Uses a singleton pattern for global access.
/// Implements ISaveParticipant to integrate with the save/load system.
/// Manages the card collection system for the game.
/// Handles unlocking cards, tracking collections, and integrating with the save/load system.
/// </summary>
public class CardSystemManager : ManagedBehaviour, ISaveParticipant
public class CardSystemManager : ManagedBehaviour
{
private static CardSystemManager _instance;
public static CardSystemManager Instance => _instance;
// Save system configuration
public override bool AutoRegisterForSave => true;
public override string SaveId => "CardSystemManager";
[Header("Card Collection")]
[SerializeField] private List<CardDefinition> availableCards = new List<CardDefinition>();
@@ -87,17 +92,6 @@ namespace Data.CardSystem
BuildDefinitionLookup();
Logging.Debug($"[CardSystemManager] Loaded {availableCards.Count} card definitions from Addressables");
// NOW register with save/load system (definitions are ready for state restoration)
if (SaveLoadManager.Instance != null)
{
SaveLoadManager.Instance.RegisterParticipant(this);
Logging.Debug("[CardSystemManager] Registered with SaveLoadManager after definitions loaded");
}
else
{
Logging.Warning("[CardSystemManager] SaveLoadManager not available for registration");
}
}
else
{
@@ -105,14 +99,6 @@ namespace Data.CardSystem
}
}
private void OnDestroy()
{
// Unregister from save/load system
if (SaveLoadManager.Instance != null)
{
SaveLoadManager.Instance.UnregisterParticipant(GetSaveId());
}
}
/// <summary>
/// Builds a lookup dictionary for quick access to card definitions by ID
@@ -461,42 +447,19 @@ namespace Data.CardSystem
}
}
#region ISaveParticipant Implementation
#region Save/Load Lifecycle Hooks
private bool hasBeenRestored;
/// <summary>
/// Returns true if this participant has already had its state restored.
/// </summary>
public bool HasBeenRestored => hasBeenRestored;
/// <summary>
/// Returns the unique save ID for the CardSystemManager.
/// Since this is a singleton global system, the ID is constant.
/// </summary>
public string GetSaveId()
{
return "CardSystemManager";
}
/// <summary>
/// Serializes the current card collection state to JSON.
/// </summary>
public string SerializeState()
protected override string OnGlobalSaveRequested()
{
var state = ExportCardCollectionState();
return JsonUtility.ToJson(state);
}
/// <summary>
/// Restores the card collection state from serialized JSON data.
/// </summary>
public void RestoreState(string serializedData)
protected override void OnGlobalRestoreRequested(string serializedData)
{
if (string.IsNullOrEmpty(serializedData))
{
Logging.Debug("[CardSystemManager] No saved state to restore, using defaults");
hasBeenRestored = true;
return;
}
@@ -506,7 +469,6 @@ namespace Data.CardSystem
if (state != null)
{
ApplyCardCollectionState(state);
hasBeenRestored = true;
Logging.Debug("[CardSystemManager] Successfully restored card collection state");
}
else

View File

@@ -21,7 +21,7 @@ namespace Input
/// Handles player movement in response to tap and hold input events.
/// Supports both direct and pathfinding movement modes, and provides event/callbacks for arrival/cancellation.
/// </summary>
public class PlayerTouchController : ManagedBehaviour, ITouchInputConsumer, ISaveParticipant
public class PlayerTouchController : ManagedBehaviour, ITouchInputConsumer
{
// --- Movement State ---
private Vector3 targetPosition;
@@ -67,9 +67,10 @@ namespace Input
private bool interruptMoveTo;
private LogVerbosity _logVerbosity = LogVerbosity.Warning;
// Save system tracking
private bool hasBeenRestored;
// Save system configuration
public override bool AutoRegisterForSave => true;
// Scene-specific SaveId - each level has its own player state
public override string SaveId => $"{gameObject.scene.name}/PlayerController";
public override int ManagedAwakePriority => 100; // Player controller
protected override void OnManagedAwake()
@@ -93,28 +94,6 @@ namespace Input
InputManager.Instance?.SetDefaultConsumer(this);
_logVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().inputLogVerbosity;
// Register with save system
if (SaveLoadManager.Instance != null)
{
SaveLoadManager.Instance.RegisterParticipant(this);
Logging.Debug("[PlayerTouchController] Registered with SaveLoadManager");
}
else
{
Logging.Warning("[PlayerTouchController] SaveLoadManager not available for registration");
}
}
protected override void OnDestroy()
{
base.OnDestroy();
// Unregister from save system
if (SaveLoadManager.Instance != null)
{
SaveLoadManager.Instance.UnregisterParticipant(GetSaveId());
}
}
/// <summary>
@@ -454,16 +433,9 @@ namespace Input
}
}
#region ISaveParticipant Implementation
#region Save/Load Lifecycle Hooks
public bool HasBeenRestored => hasBeenRestored;
public string GetSaveId()
{
return "PlayerController";
}
public string SerializeState()
protected override string OnSceneSaveRequested()
{
var saveData = new PlayerSaveData
{
@@ -473,12 +445,11 @@ namespace Input
return JsonUtility.ToJson(saveData);
}
public void RestoreState(string serializedData)
protected override void OnSceneRestoreRequested(string serializedData)
{
if (string.IsNullOrEmpty(serializedData))
{
Logging.Debug("[PlayerTouchController] No saved state to restore");
hasBeenRestored = true;
return;
}
@@ -489,7 +460,6 @@ namespace Input
{
transform.position = saveData.worldPosition;
transform.rotation = saveData.worldRotation;
hasBeenRestored = true;
Logging.Debug($"[PlayerTouchController] Restored position: {saveData.worldPosition}");
}
}

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using System; // for Action<T>
@@ -24,6 +24,7 @@ namespace Interactions
{
public ItemSlotState slotState;
public string slottedItemSaveId;
public string slottedItemDataId; // ItemId of the PickupItemData (for verification)
}
/// <summary>
@@ -227,6 +228,16 @@ namespace Interactions
{
var previousData = currentlySlottedItemData;
// Clear the pickup's OwningSlot reference
if (currentlySlottedItemObject != null)
{
var pickup = currentlySlottedItemObject.GetComponent<Pickup>();
if (pickup != null)
{
pickup.OwningSlot = null;
}
}
currentlySlottedItemObject = null;
currentlySlottedItemData = null;
currentState = ItemSlotState.None;
@@ -272,17 +283,15 @@ namespace Interactions
#endregion
// Register with ItemManager when enabled
protected override void Start()
private void OnEnable()
{
base.Start(); // SaveableInteractable registration
// Register as ItemSlot
ItemManager.Instance?.RegisterItemSlot(this);
}
protected override void OnDestroy()
{
base.OnDestroy(); // SaveableInteractable cleanup
base.OnDestroy();
// Unregister from slot manager
ItemManager.Instance?.UnregisterItemSlot(this);
@@ -294,20 +303,28 @@ namespace Interactions
{
// Get slotted item save ID if there's a slotted item
string slottedSaveId = "";
string slottedDataId = "";
if (currentlySlottedItemObject != null)
{
var slottedPickup = currentlySlottedItemObject.GetComponent<Pickup>();
if (slottedPickup is SaveableInteractable saveablePickup)
{
slottedSaveId = saveablePickup.GetSaveId();
slottedSaveId = saveablePickup.SaveId;
}
}
// Also save the itemData ID for verification
if (currentlySlottedItemData != null)
{
slottedDataId = currentlySlottedItemData.itemId;
}
return new ItemSlotSaveData
{
slotState = currentState,
slottedItemSaveId = slottedSaveId
slottedItemSaveId = slottedSaveId,
slottedItemDataId = slottedDataId
};
}
@@ -326,7 +343,8 @@ namespace Interactions
// Restore slotted item if there was one
if (!string.IsNullOrEmpty(data.slottedItemSaveId))
{
RestoreSlottedItem(data.slottedItemSaveId);
Debug.Log($"[ItemSlot] Restoring slotted item: {data.slottedItemSaveId} (itemId: {data.slottedItemDataId})");
RestoreSlottedItem(data.slottedItemSaveId, data.slottedItemDataId);
}
}
@@ -334,7 +352,7 @@ namespace Interactions
/// Restore a slotted item from save data.
/// This is called during load restoration and should NOT trigger events.
/// </summary>
private void RestoreSlottedItem(string slottedItemSaveId)
private void RestoreSlottedItem(string slottedItemSaveId, string expectedItemDataId)
{
// Try to find the item in the scene by its save ID via ItemManager
GameObject slottedObject = ItemManager.Instance?.FindPickupBySaveId(slottedItemSaveId);
@@ -351,11 +369,33 @@ namespace Interactions
if (pickup != null)
{
slottedData = pickup.itemData;
// Verify itemId matches if we have it (safety check)
if (slottedData != null && !string.IsNullOrEmpty(expectedItemDataId))
{
if (slottedData.itemId != expectedItemDataId)
{
Debug.LogWarning($"[ItemSlot] ItemId mismatch! Pickup has '{slottedData.itemId}' but expected '{expectedItemDataId}'");
}
}
if (slottedData == null)
{
Debug.LogWarning($"[ItemSlot] Pickup {pickup.gameObject.name} has null itemData! Expected itemId: {expectedItemDataId}");
return;
}
}
else
{
Debug.LogWarning($"[ItemSlot] Slotted object has no Pickup component: {slottedObject.name}");
return;
}
// Silently slot the item (no events, no interaction completion)
// Follower state is managed separately during save/load restoration
ApplySlottedItemState(slottedObject, slottedData, triggerEvents: false);
Debug.Log($"[ItemSlot] Successfully restored slotted item: {slottedData.itemName} (itemId: {slottedData.itemId})");
}
/// <summary>
@@ -370,7 +410,16 @@ namespace Interactions
{
if (itemToSlot == null)
{
// Clear slot
// Clear slot - also clear the pickup's OwningSlot reference
if (currentlySlottedItemObject != null)
{
var oldPickup = currentlySlottedItemObject.GetComponent<Pickup>();
if (oldPickup != null)
{
oldPickup.OwningSlot = null;
}
}
var previousData = currentlySlottedItemData;
currentlySlottedItemObject = null;
currentlySlottedItemData = null;
@@ -391,6 +440,14 @@ namespace Interactions
SetSlottedObject(itemToSlot);
currentlySlottedItemData = itemToSlotData;
// Mark the pickup as picked up and track slot ownership for save/load
var pickup = itemToSlot.GetComponent<Pickup>();
if (pickup != null)
{
pickup.IsPickedUp = true;
pickup.OwningSlot = this;
}
// Determine if correct
var config = interactionSettings?.GetSlotItemConfig(itemData);
var allowed = config?.allowedItems ?? new List<PickupItemData>();
@@ -433,6 +490,33 @@ namespace Interactions
ApplySlottedItemState(itemToSlot, itemToSlotData, triggerEvents: true);
}
/// <summary>
/// Bilateral restoration entry point: Pickup calls this to offer itself to the Slot.
/// Returns true if claim was successful, false if slot already has an item or wrong pickup.
/// </summary>
public bool TryClaimSlottedItem(Pickup pickup)
{
if (pickup == null)
return false;
// If slot already has an item, reject the claim
if (currentlySlottedItemObject != null)
{
Debug.LogWarning($"[ItemSlot] Already has a slotted item, rejecting claim from {pickup.gameObject.name}");
return false;
}
// Verify this pickup's SaveId matches what we expect (from our save data)
// Note: We don't have easy access to the expected SaveId here, so we just accept it
// The Pickup's bilateral restoration ensures it only claims the correct slot
// Claim the pickup
ApplySlottedItemState(pickup.gameObject, pickup.itemData, triggerEvents: false);
Debug.Log($"[ItemSlot] Successfully claimed slotted item: {pickup.itemData?.itemName}");
return true;
}
#endregion
}
}

View File

@@ -13,6 +13,8 @@ namespace Interactions
{
public bool isPickedUp;
public bool wasHeldByFollower;
public bool wasInSlot; // NEW: Was this pickup in a slot?
public string slotSaveId; // NEW: Which slot held this pickup?
public Vector3 worldPosition;
public Quaternion worldRotation;
public bool isActive;
@@ -24,6 +26,9 @@ namespace Interactions
public SpriteRenderer iconRenderer;
public bool IsPickedUp { get; internal set; }
// Track which slot owns this pickup (for bilateral restoration)
internal ItemSlot OwningSlot { get; set; }
public event Action<PickupItemData> OnItemPickedUp;
public event Action<PickupItemData, PickupItemData, PickupItemData> OnItemsCombined;
@@ -37,18 +42,16 @@ namespace Interactions
ApplyItemData();
}
protected override void Start()
{
base.Start(); // Register with save system
// Always register with ItemManager, even if picked up
// Always register with ItemManager, even if picked up
// This allows the save/load system to find held items when restoring state
protected override void OnManagedAwake()
{
ItemManager.Instance?.RegisterPickup(this);
}
protected override void OnDestroy()
{
base.OnDestroy(); // Unregister from save system
base.OnDestroy();
// Unregister from ItemManager
ItemManager.Instance?.UnregisterPickup(this);
@@ -139,10 +142,16 @@ namespace Interactions
// Check if this pickup is currently held by the follower
bool isHeldByFollower = IsPickedUp && !gameObject.activeSelf && transform.parent != null;
// Check if this pickup is in a slot
bool isInSlot = OwningSlot != null;
string slotId = isInSlot && OwningSlot is SaveableInteractable saveableSlot ? saveableSlot.SaveId : "";
return new PickupSaveData
{
isPickedUp = this.IsPickedUp,
wasHeldByFollower = isHeldByFollower,
wasInSlot = isInSlot,
slotSaveId = slotId,
worldPosition = transform.position,
worldRotation = transform.rotation,
isActive = gameObject.activeSelf
@@ -177,6 +186,20 @@ namespace Interactions
follower.TryClaimHeldItem(this);
}
}
// If this was in a slot, try bilateral restoration with the slot
else if (data.wasInSlot && !string.IsNullOrEmpty(data.slotSaveId))
{
// Try to give this pickup to the slot
var slot = FindSlotBySaveId(data.slotSaveId);
if (slot != null)
{
slot.TryClaimSlottedItem(this);
}
else
{
Debug.LogWarning($"[Pickup] Could not find slot with SaveId: {data.slotSaveId}");
}
}
}
else
{
@@ -190,6 +213,28 @@ namespace Interactions
// This prevents duplicate logic execution
}
/// <summary>
/// Find an ItemSlot by its SaveId (for bilateral restoration).
/// </summary>
private ItemSlot FindSlotBySaveId(string slotSaveId)
{
if (string.IsNullOrEmpty(slotSaveId)) return null;
// Get all ItemSlots from ItemManager
var allSlots = ItemManager.Instance?.GetAllItemSlots();
if (allSlots == null) return null;
foreach (var slot in allSlots)
{
if (slot is SaveableInteractable saveable && saveable.SaveId == slotSaveId)
{
return slot;
}
}
return null;
}
/// <summary>
/// Resets the pickup state when the item is dropped back into the world.
/// Called by FollowerController when swapping items.

View File

@@ -1,6 +1,4 @@
using Core.SaveLoad;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine;
namespace Interactions
{
@@ -8,21 +6,13 @@ namespace Interactions
/// Base class for interactables that participate in the save/load system.
/// Provides common save ID generation and serialization infrastructure.
/// </summary>
public abstract class SaveableInteractable : InteractableBase, ISaveParticipant
public abstract class SaveableInteractable : InteractableBase
{
[Header("Save System")]
[SerializeField]
[Tooltip("Optional custom save ID. If empty, will auto-generate from hierarchy path.")]
private string customSaveId = "";
/// <summary>
/// Sets a custom save ID for this interactable.
/// Used when spawning dynamic objects that need stable save IDs.
/// </summary>
public void SetCustomSaveId(string saveId)
{
customSaveId = saveId;
}
// Save system configuration
public override bool AutoRegisterForSave => true;
/// <summary>
/// Flag to indicate we're currently restoring from save data.
@@ -30,99 +20,10 @@ namespace Interactions
/// </summary>
protected bool IsRestoringFromSave { get; private set; }
private bool hasRegistered;
private bool hasRestoredState;
/// <summary>
/// Returns true if this participant has already had its state restored.
/// </summary>
public bool HasBeenRestored => hasRestoredState;
#region Save/Load Lifecycle Hooks
protected virtual void Awake()
{
// Register early in Awake so even disabled objects are tracked
RegisterWithSaveSystem();
}
protected virtual void Start()
{
// If we didn't register in Awake (shouldn't happen), register now
if (!hasRegistered)
{
RegisterWithSaveSystem();
}
}
protected virtual void OnDestroy()
{
UnregisterFromSaveSystem();
}
private void RegisterWithSaveSystem()
{
if (hasRegistered) return;
if (SaveLoadManager.Instance != null)
{
SaveLoadManager.Instance.RegisterParticipant(this);
hasRegistered = true;
// Check if save data was already loaded before we registered
// If so, we need to subscribe to the next load event
if (!SaveLoadManager.Instance.IsSaveDataLoaded && !hasRestoredState)
{
SaveLoadManager.Instance.OnLoadCompleted += OnSaveDataLoadedHandler;
}
}
else
{
Debug.LogWarning($"[SaveableInteractable] SaveLoadManager not found for {gameObject.name}");
}
}
private void UnregisterFromSaveSystem()
{
if (!hasRegistered) return;
if (SaveLoadManager.Instance != null)
{
SaveLoadManager.Instance.UnregisterParticipant(GetSaveId());
SaveLoadManager.Instance.OnLoadCompleted -= OnSaveDataLoadedHandler;
hasRegistered = false;
}
}
/// <summary>
/// Event handler for when save data finishes loading.
/// Called if the object registered before save data was loaded.
/// </summary>
private void OnSaveDataLoadedHandler(string slot)
{
// The SaveLoadManager will automatically call RestoreState on us
// We just need to unsubscribe from the event
if (SaveLoadManager.Instance != null)
{
SaveLoadManager.Instance.OnLoadCompleted -= OnSaveDataLoadedHandler;
}
}
#region ISaveParticipant Implementation
public string GetSaveId()
{
string sceneName = GetSceneName();
if (!string.IsNullOrEmpty(customSaveId))
{
return $"{sceneName}/{customSaveId}";
}
// Auto-generate from hierarchy path
string hierarchyPath = GetHierarchyPath();
return $"{sceneName}/{hierarchyPath}";
}
public string SerializeState()
protected override string OnSceneSaveRequested()
{
object stateData = GetSerializableState();
if (stateData == null)
@@ -133,28 +34,17 @@ namespace Interactions
return JsonUtility.ToJson(stateData);
}
public void RestoreState(string serializedData)
protected override void OnSceneRestoreRequested(string serializedData)
{
if (string.IsNullOrEmpty(serializedData))
{
Debug.LogWarning($"[SaveableInteractable] Empty save data for {GetSaveId()}");
return;
}
// CRITICAL: Only restore state if we're actually in a restoration context
// This prevents state machines from teleporting objects when they enable them mid-gameplay
if (SaveLoadManager.Instance != null && !SaveLoadManager.Instance.IsRestoringState)
{
// If we're not in an active restoration cycle, this is probably a late registration
// (object was disabled during initial load and just got enabled)
// Skip restoration to avoid mid-gameplay teleportation
Debug.Log($"[SaveableInteractable] Skipping late restoration for {GetSaveId()} - object enabled after initial load");
hasRestoredState = true; // Mark as restored to prevent future attempts
Debug.LogWarning($"[SaveableInteractable] Empty save data for {SaveId}");
return;
}
// OnSceneRestoreRequested is guaranteed by the lifecycle system to only fire during actual restoration
// No need to check IsRestoringState - the lifecycle manager handles timing deterministically
IsRestoringFromSave = true;
hasRestoredState = true;
try
{
@@ -162,7 +52,7 @@ namespace Interactions
}
catch (System.Exception e)
{
Debug.LogError($"[SaveableInteractable] Failed to restore state for {GetSaveId()}: {e.Message}");
Debug.LogError($"[SaveableInteractable] Failed to restore state for {SaveId}: {e.Message}");
}
finally
{
@@ -189,61 +79,22 @@ namespace Interactions
#endregion
#region Helper Methods
private string GetSceneName()
{
Scene scene = gameObject.scene;
if (!scene.IsValid())
{
Debug.LogWarning($"[SaveableInteractable] GameObject {gameObject.name} has invalid scene");
return "UnknownScene";
}
return scene.name;
}
private string GetHierarchyPath()
{
// Build path from scene root to this object
// Format: ParentName/ChildName/ObjectName_SiblingIndex
string path = gameObject.name;
Transform current = transform.parent;
while (current != null)
{
path = $"{current.name}/{path}";
current = current.parent;
}
// Add sibling index for uniqueness among same-named objects
int siblingIndex = transform.GetSiblingIndex();
if (siblingIndex > 0)
{
path = $"{path}_{siblingIndex}";
}
return path;
}
#endregion
#region Editor Helpers
#if UNITY_EDITOR
[ContextMenu("Log Save ID")]
private void LogSaveId()
{
Debug.Log($"Save ID: {GetSaveId()}");
Debug.Log($"Save ID: {SaveId}");
}
[ContextMenu("Test Serialize/Deserialize")]
private void TestSerializeDeserialize()
{
string serialized = SerializeState();
string serialized = OnSceneSaveRequested();
Debug.Log($"Serialized state: {serialized}");
RestoreState(serialized);
OnSceneRestoreRequested(serialized);
Debug.Log("Deserialization test complete");
}
#endif

View File

@@ -41,7 +41,7 @@ namespace Levels
/// </summary>
protected override void Awake()
{
base.Awake(); // Register with save system
base.Awake();
switchActive = true;
if (iconRenderer == null)
@@ -52,11 +52,11 @@ namespace Levels
ApplySwitchData();
}
protected override void Start()
protected override void OnManagedAwake()
{
base.Start(); // Register with save system
base.OnManagedAwake();
// Direct subscription - PuzzleManager available by this point
// Subscribe to PuzzleManager - safe to access .Instance here
if (PuzzleManager.Instance != null)
{
PuzzleManager.Instance.OnAllPuzzlesComplete += HandleAllPuzzlesComplete;
@@ -69,8 +69,10 @@ namespace Levels
}
}
private void OnDestroy()
protected override void OnDestroy()
{
base.OnDestroy();
if (PuzzleManager.Instance != null)
{
PuzzleManager.Instance.OnAllPuzzlesComplete -= HandleAllPuzzlesComplete;

View File

@@ -1,12 +1,10 @@
using Interactions;
using UnityEngine;
using Pathfinding;
using UnityEngine.SceneManagement;
using Utils;
using AppleHills.Core.Settings;
using Core;
using Core.Lifecycle;
using Core.SaveLoad;
using UnityEngine.Events;
/// <summary>
@@ -18,13 +16,13 @@ public class FollowerSaveData
public Vector3 worldPosition;
public Quaternion worldRotation;
public string heldItemSaveId; // Save ID of held pickup (if any)
public string heldItemDataAssetPath; // Asset path to PickupItemData
public string heldItemDataAssetPath; // ItemId of the PickupItemData (for fallback restoration)
}
/// <summary>
/// Controls the follower character, including following the player, handling pickups, and managing held items.
/// </summary>
public class FollowerController : ManagedBehaviour, ISaveParticipant
public class FollowerController : ManagedBehaviour
{
private static readonly int CombineTrigger = Animator.StringToHash("Combine");
@@ -54,6 +52,12 @@ public class FollowerController : ManagedBehaviour, ISaveParticipant
// Direction variables for 2D blend tree animation
private float _lastDirX = 0f; // -1 (left) to 1 (right)
private float _lastDirY = -1f; // -1 (down) to 1 (up)
// Save system configuration
public override bool AutoRegisterForSave => true;
// Scene-specific SaveId - each level has its own follower state
public override string SaveId => $"{gameObject.scene.name}/FollowerController";
private float _currentSpeed = 0f;
private Animator _animator;
private Transform _artTransform;
@@ -98,8 +102,7 @@ public class FollowerController : ManagedBehaviour, ISaveParticipant
private Input.PlayerTouchController _playerTouchController;
// Save system tracking
private bool hasBeenRestored;
// Save system tracking for bilateral restoration
private bool _hasRestoredHeldItem; // Track if held item restoration completed
private string _expectedHeldItemSaveId; // Expected saveId during restoration
@@ -124,38 +127,11 @@ public class FollowerController : ManagedBehaviour, ISaveParticipant
// Initialize settings references
_settings = GameManager.GetSettingsObject<IPlayerFollowerSettings>();
_interactionSettings = GameManager.GetSettingsObject<IInteractionSettings>();
// Register with save system
if (SaveLoadManager.Instance != null)
{
SaveLoadManager.Instance.RegisterParticipant(this);
Logging.Debug("[FollowerController] Registered with SaveLoadManager");
}
else
{
Logging.Warning("[FollowerController] SaveLoadManager not available for registration");
}
}
void OnEnable()
{
SceneManager.sceneLoaded += OnSceneLoaded;
FindPlayerReference();
}
void OnDisable()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
// Unregister from save system
if (SaveLoadManager.Instance != null)
{
SaveLoadManager.Instance.UnregisterParticipant(GetSaveId());
}
}
void OnSceneLoaded(Scene scene, LoadSceneMode mode)
protected override void OnSceneReady()
{
// Find player reference when scene is ready (called for every scene load)
FindPlayerReference();
}
@@ -163,9 +139,7 @@ public class FollowerController : ManagedBehaviour, ISaveParticipant
{
if (_playerTransform == null)
{
FindPlayerReference();
if (_playerTransform == null)
return;
return;
}
// Skip all movement logic when playing a stationary animation
@@ -749,16 +723,9 @@ public class FollowerController : ManagedBehaviour, ISaveParticipant
#endregion ItemInteractions
#region ISaveParticipant Implementation
#region Save/Load Lifecycle Hooks
public bool HasBeenRestored => hasBeenRestored;
public string GetSaveId()
{
return "FollowerController";
}
public string SerializeState()
protected override string OnSceneSaveRequested()
{
var saveData = new FollowerSaveData
{
@@ -772,26 +739,24 @@ public class FollowerController : ManagedBehaviour, ISaveParticipant
var pickup = _cachedPickupObject.GetComponent<Pickup>();
if (pickup is SaveableInteractable saveable)
{
saveData.heldItemSaveId = saveable.GetSaveId();
saveData.heldItemSaveId = saveable.SaveId;
}
// Save the itemId for build-compatible restoration
if (_currentlyHeldItemData != null)
{
#if UNITY_EDITOR
saveData.heldItemDataAssetPath = UnityEditor.AssetDatabase.GetAssetPath(_currentlyHeldItemData);
#endif
saveData.heldItemDataAssetPath = _currentlyHeldItemData.itemId;
}
}
return JsonUtility.ToJson(saveData);
}
public void RestoreState(string serializedData)
protected override void OnSceneRestoreRequested(string serializedData)
{
if (string.IsNullOrEmpty(serializedData))
{
Logging.Debug("[FollowerController] No saved state to restore");
hasBeenRestored = true;
return;
}
@@ -811,7 +776,6 @@ public class FollowerController : ManagedBehaviour, ISaveParticipant
TryRestoreHeldItem(saveData.heldItemSaveId, saveData.heldItemDataAssetPath);
}
hasBeenRestored = true;
Logging.Debug($"[FollowerController] Restored position: {saveData.worldPosition}");
}
}
@@ -825,7 +789,7 @@ public class FollowerController : ManagedBehaviour, ISaveParticipant
/// Bilateral restoration: Follower tries to find and claim the held item.
/// If pickup doesn't exist yet, it will try to claim us when it restores.
/// </summary>
private void TryRestoreHeldItem(string heldItemSaveId, string heldItemDataAssetPath)
private void TryRestoreHeldItem(string heldItemSaveId, string itemDataId)
{
if (_hasRestoredHeldItem)
{
@@ -850,7 +814,7 @@ public class FollowerController : ManagedBehaviour, ISaveParticipant
}
// Claim the pickup
TakeOwnership(pickup, heldItemDataAssetPath);
TakeOwnership(pickup, itemDataId);
}
/// <summary>
@@ -871,9 +835,9 @@ public class FollowerController : ManagedBehaviour, ISaveParticipant
// Verify this is the expected pickup
if (pickup is SaveableInteractable saveable)
{
if (saveable.GetSaveId() != _expectedHeldItemSaveId)
if (saveable.SaveId != _expectedHeldItemSaveId)
{
Logging.Warning($"[FollowerController] Pickup tried to claim but saveId mismatch: {saveable.GetSaveId()} != {_expectedHeldItemSaveId}");
Logging.Warning($"[FollowerController] Pickup tried to claim but saveId mismatch: {saveable.SaveId} != {_expectedHeldItemSaveId}");
return false;
}
}
@@ -886,28 +850,29 @@ public class FollowerController : ManagedBehaviour, ISaveParticipant
/// <summary>
/// Takes ownership of a pickup during restoration. Called by both restoration paths.
/// </summary>
private void TakeOwnership(Pickup pickup, string itemDataAssetPath)
private void TakeOwnership(Pickup pickup, string itemDataIdOrPath)
{
if (_hasRestoredHeldItem)
return; // Already claimed
// Get the item data
// Get the item data from the pickup
PickupItemData heldData = pickup.itemData;
#if UNITY_EDITOR
// Try loading from asset path if available and pickup doesn't have data
if (heldData == null && !string.IsNullOrEmpty(itemDataAssetPath))
{
heldData = UnityEditor.AssetDatabase.LoadAssetAtPath<PickupItemData>(itemDataAssetPath);
}
#endif
// Fallback: If pickup doesn't have itemData, log detailed error
if (heldData == null)
{
Logging.Warning($"[FollowerController] Could not get item data for pickup: {pickup.gameObject.name}");
Logging.Warning($"[FollowerController] Pickup {pickup.gameObject.name} has null itemData!");
Logging.Warning($"[FollowerController] Expected itemId: {itemDataIdOrPath}");
Logging.Warning($"[FollowerController] This pickup prefab may be missing its PickupItemData reference.");
return;
}
// Verify itemId matches if we have it (additional safety check)
if (!string.IsNullOrEmpty(itemDataIdOrPath) && heldData.itemId != itemDataIdOrPath)
{
Logging.Warning($"[FollowerController] ItemId mismatch! Pickup has '{heldData.itemId}' but expected '{itemDataIdOrPath}'");
}
// Setup the held item
_cachedPickupObject = pickup.gameObject;
_cachedPickupObject.SetActive(false); // Held items should be hidden
@@ -915,7 +880,7 @@ public class FollowerController : ManagedBehaviour, ISaveParticipant
_animator.SetBool("IsCarrying", true);
_hasRestoredHeldItem = true;
Logging.Debug($"[FollowerController] Successfully restored held item: {heldData.itemName}");
Logging.Debug($"[FollowerController] Successfully restored held item: {heldData.itemName} (itemId: {heldData.itemId})");
}
/// <summary>
@@ -927,7 +892,7 @@ public class FollowerController : ManagedBehaviour, ISaveParticipant
return FindObjectOfType<FollowerController>();
}
#endregion ISaveParticipant Implementation
#endregion Save/Load Lifecycle Hooks
#if UNITY_EDITOR
void OnDrawGizmos()

View File

@@ -2,6 +2,7 @@
using Interactions;
using UnityEngine;
using Core;
using Core.Lifecycle;
namespace PuzzleS
{
@@ -9,7 +10,7 @@ namespace PuzzleS
/// Manages the state and interactions for a single puzzle step, including unlock/lock logic and event handling.
/// </summary>
[RequireComponent(typeof(InteractableBase))]
public class ObjectiveStepBehaviour : MonoBehaviour, IPuzzlePrompt
public class ObjectiveStepBehaviour : ManagedBehaviour, IPuzzlePrompt
{
/// <summary>
/// The data object representing this puzzle step.
@@ -31,8 +32,10 @@ namespace PuzzleS
// Enum for tracking proximity state (simplified to just Close and Far)
public enum ProximityState { Close, Far }
void Awake()
protected override void Awake()
{
base.Awake();
_interactable = GetComponent<InteractableBase>();
// Initialize the indicator if it exists, but ensure it's hidden initially
@@ -57,6 +60,21 @@ namespace PuzzleS
}
}
protected override void OnManagedAwake()
{
base.OnManagedAwake();
// Register with PuzzleManager - safe to access .Instance here
if (stepData != null && PuzzleManager.Instance != null)
{
PuzzleManager.Instance.RegisterStepBehaviour(this);
}
else if (stepData == null)
{
Logging.Warning($"[Puzzles] Cannot register step on {gameObject.name}: stepData is null");
}
}
void OnEnable()
{
if (_interactable == null)
@@ -69,27 +87,18 @@ namespace PuzzleS
}
}
void Start()
{
// Simply register with the PuzzleManager
// The manager will handle state updates appropriately based on whether data is loaded
if (stepData != null && PuzzleManager.Instance != null)
{
PuzzleManager.Instance.RegisterStepBehaviour(this);
}
else if (stepData == null)
{
Logging.Warning($"[Puzzles] Cannot register step on {gameObject.name}: stepData is null");
}
}
void OnDestroy()
void OnDisable()
{
if (_interactable != null)
{
_interactable.interactionStarted.RemoveListener(OnInteractionStarted);
_interactable.interactionComplete.RemoveListener(OnInteractionComplete);
}
}
protected override void OnDestroy()
{
base.OnDestroy();
if (PuzzleManager.Instance != null && stepData != null)
{

View File

@@ -7,7 +7,6 @@ using UnityEngine.SceneManagement;
using AppleHills.Core.Settings;
using Core;
using Core.Lifecycle;
using Core.SaveLoad;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using Utils;
@@ -28,7 +27,7 @@ namespace PuzzleS
/// <summary>
/// Manages puzzle step registration, dependency management, and step completion for the puzzle system.
/// </summary>
public class PuzzleManager : ManagedBehaviour, ISaveParticipant
public class PuzzleManager : ManagedBehaviour
{
private static PuzzleManager _instance;
@@ -49,6 +48,10 @@ namespace PuzzleS
// Store registered behaviors that are waiting for data to be loaded
private List<ObjectiveStepBehaviour> _registeredBehaviours = new List<ObjectiveStepBehaviour>();
// Save system configuration
public override bool AutoRegisterForSave => true;
public override string SaveId => $"{SceneManager.GetActiveScene().name}/PuzzleManager";
/// <summary>
/// Singleton instance of the PuzzleManager.
/// </summary>
@@ -66,7 +69,6 @@ namespace PuzzleS
// Save/Load restoration tracking
private bool _isDataRestored = false;
private bool _hasBeenRestored = false;
private List<ObjectiveStepBehaviour> _pendingRegistrations = new List<ObjectiveStepBehaviour>();
// Registration for ObjectiveStepBehaviour
@@ -75,12 +77,6 @@ namespace PuzzleS
// Track pending unlocks for steps that were unlocked before their behavior registered
private HashSet<string> _pendingUnlocks = new HashSet<string>();
/// <summary>
/// Returns true if this participant has already had its state restored.
/// Used by SaveLoadManager to prevent double-restoration.
/// </summary>
public bool HasBeenRestored => _hasBeenRestored;
public override int ManagedAwakePriority => 80; // Puzzle systems
private new void Awake()
@@ -108,10 +104,6 @@ namespace PuzzleS
LoadPuzzleDataForCurrentScene();
}
// Register with save/load system
SaveLoadManager.Instance.RegisterParticipant(this);
Logging.Debug("[PuzzleManager] Registered with SaveLoadManager");
// Subscribe to scene load events from SceneManagerService
// This is necessary because PuzzleManager is in DontDestroyOnLoad and won't receive OnSceneReady() callbacks
if (SceneManagerService.Instance != null)
@@ -558,21 +550,9 @@ namespace PuzzleS
return _isDataLoaded;
}
#region ISaveParticipant Implementation
#region Save/Load Lifecycle Hooks
/// <summary>
/// Get unique save ID for this puzzle manager instance
/// </summary>
public string GetSaveId()
{
string sceneName = SceneManager.GetActiveScene().name;
return $"{sceneName}/PuzzleManager";
}
/// <summary>
/// Serialize current puzzle state to JSON
/// </summary>
public string SerializeState()
protected override string OnSceneSaveRequested()
{
if (_currentLevelData == null)
{
@@ -592,16 +572,12 @@ namespace PuzzleS
return json;
}
/// <summary>
/// Restore puzzle state from serialized JSON data
/// </summary>
public void RestoreState(string data)
protected override void OnSceneRestoreRequested(string data)
{
if (string.IsNullOrEmpty(data) || data == "{}")
{
Logging.Debug("[PuzzleManager] No puzzle save data to restore");
_isDataRestored = true;
_hasBeenRestored = true;
return;
}
@@ -612,7 +588,6 @@ namespace PuzzleS
{
Logging.Warning("[PuzzleManager] Failed to deserialize puzzle save data");
_isDataRestored = true;
_hasBeenRestored = true;
return;
}
@@ -621,7 +596,6 @@ namespace PuzzleS
_unlockedSteps = new HashSet<string>(saveData.unlockedStepIds ?? new List<string>());
_isDataRestored = true;
_hasBeenRestored = true;
Logging.Debug($"[PuzzleManager] Restored puzzle state: {_completedSteps.Count} completed, {_unlockedSteps.Count} unlocked steps");
@@ -636,7 +610,6 @@ namespace PuzzleS
{
Debug.LogError($"[PuzzleManager] Error restoring puzzle state: {e.Message}");
_isDataRestored = true;
_hasBeenRestored = true;
}
}

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using AppleHills.Data.CardSystem;
using Core;
using Data.CardSystem;
@@ -34,8 +34,6 @@ namespace UI.CardSystem
private void Awake()
{
_cardManager = CardSystemManager.Instance;
// Make sure we have a CanvasGroup for transitions
if (canvasGroup == null)
canvasGroup = GetComponent<CanvasGroup>();
@@ -49,6 +47,14 @@ namespace UI.CardSystem
}
}
protected override void OnManagedAwake()
{
base.OnManagedAwake();
// Safe to access manager instance here
_cardManager = CardSystemManager.Instance;
}
/// <summary>
/// Sets up the album when the page becomes active
/// </summary>

View File

@@ -1,4 +1,4 @@
using System.Collections;
using System.Collections;
using System.Collections.Generic;
using AppleHills.Data.CardSystem;
using Core;
@@ -52,7 +52,6 @@ namespace UI.CardSystem
private void Awake()
{
_cardManager = CardSystemManager.Instance;
_cardAlbumUI = FindFirstObjectByType<CardAlbumUI>();
// Set up button listeners
@@ -86,6 +85,14 @@ namespace UI.CardSystem
HideAllCardBacks();
}
protected override void OnManagedAwake()
{
base.OnManagedAwake();
// Safe to access manager instance here
_cardManager = CardSystemManager.Instance;
}
/// <summary>
/// Cache all card back buttons from the container
/// </summary>

View File

@@ -38,6 +38,11 @@ namespace UI.CardSystem
protected override void OnManagedAwake()
{
base.OnManagedAwake();
// Get card manager - safe to access .Instance here
_cardManager = CardSystemManager.Instance;
// Set up backpack button
if (backpackButton != null)
{
@@ -54,20 +59,7 @@ namespace UI.CardSystem
// Initialize pages and hide them
InitializePages();
// React to global UI hide/show events (top-page only) by toggling this GameObject
if (UIPageController.Instance != null)
{
UIPageController.Instance.OnAllUIHidden += HandleAllUIHidden;
UIPageController.Instance.OnAllUIShown += HandleAllUIShown;
}
}
private void Start()
{
// Get card manager
_cardManager = CardSystemManager.Instance;
// Subscribe to events
// Subscribe to card manager events
if (_cardManager != null)
{
_cardManager.OnBoosterCountChanged += UpdateBoosterCount;
@@ -78,6 +70,13 @@ namespace UI.CardSystem
// Initialize UI with current values
UpdateBoosterCount(_cardManager.GetBoosterPackCount());
}
// React to global UI hide/show events (top-page only) by toggling this GameObject
if (UIPageController.Instance != null)
{
UIPageController.Instance.OnAllUIHidden += HandleAllUIHidden;
UIPageController.Instance.OnAllUIShown += HandleAllUIShown;
}
}
private void OnDestroy()

View File

@@ -1,4 +1,4 @@
using Core;
using Core;
using Data.CardSystem;
using Pixelplacement;
using UI.Core;
@@ -28,9 +28,8 @@ namespace UI.CardSystem
private void Awake()
{
// Get references
// Get UI reference
_cardAlbumUI = FindAnyObjectByType<CardAlbumUI>();
_cardManager = CardSystemManager.Instance;
// Make sure we have a CanvasGroup
if (canvasGroup == null)
@@ -48,6 +47,14 @@ namespace UI.CardSystem
{
viewAlbumButton.onClick.AddListener(OnViewAlbumClicked);
}
}
protected override void OnManagedAwake()
{
base.OnManagedAwake();
// Safe to access manager instance here
_cardManager = CardSystemManager.Instance;
if (changeClothesButton != null)
{

View File

@@ -14,7 +14,7 @@ MonoBehaviour:
m_EditorClassIdentifier: AppleHillsScripts::AppleHills.Core.Settings.DebugSettings
showDebugUiMessages: 1
pauseTimeOnPauseGame: 0
useSaveLoadSystem: 0
useSaveLoadSystem: 1
bootstrapLogVerbosity: 0
settingsLogVerbosity: 1
gameManagerLogVerbosity: 1