SaveLoad using managed lifecycle
This commit is contained in:
@@ -166,7 +166,6 @@ MonoBehaviour:
|
|||||||
interactionComplete:
|
interactionComplete:
|
||||||
m_PersistentCalls:
|
m_PersistentCalls:
|
||||||
m_Calls: []
|
m_Calls: []
|
||||||
customSaveId:
|
|
||||||
itemData: {fileID: 11400000, guid: 0c6986639ca176a419c92f5a327d95ce, type: 2}
|
itemData: {fileID: 11400000, guid: 0c6986639ca176a419c92f5a327d95ce, type: 2}
|
||||||
iconRenderer: {fileID: 7494677664706785084}
|
iconRenderer: {fileID: 7494677664706785084}
|
||||||
--- !u!1001 &8589202998731622905
|
--- !u!1001 &8589202998731622905
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -5,6 +5,7 @@ using Core;
|
|||||||
using Core.Lifecycle;
|
using Core.Lifecycle;
|
||||||
using UnityEngine.SceneManagement;
|
using UnityEngine.SceneManagement;
|
||||||
using Cinematics;
|
using Cinematics;
|
||||||
|
using Core.SaveLoad;
|
||||||
|
|
||||||
namespace Bootstrap
|
namespace Bootstrap
|
||||||
{
|
{
|
||||||
@@ -204,8 +205,12 @@ namespace Bootstrap
|
|||||||
LogDebugMessage($"Broadcasting OnSceneReady for: {mainSceneName}");
|
LogDebugMessage($"Broadcasting OnSceneReady for: {mainSceneName}");
|
||||||
LifecycleManager.Instance?.BroadcastSceneReady(mainSceneName);
|
LifecycleManager.Instance?.BroadcastSceneReady(mainSceneName);
|
||||||
|
|
||||||
LogDebugMessage($"Broadcasting OnRestoreRequested for: {mainSceneName}");
|
// Restore scene data for the main menu
|
||||||
LifecycleManager.Instance?.BroadcastRestoreRequested(mainSceneName);
|
if (SaveLoadManager.Instance != null)
|
||||||
|
{
|
||||||
|
LogDebugMessage($"Restoring scene data for: {mainSceneName}");
|
||||||
|
SaveLoadManager.Instance.RestoreSceneData();
|
||||||
|
}
|
||||||
|
|
||||||
// Step 2: Scene is fully loaded, now hide the loading screen
|
// Step 2: Scene is fully loaded, now hide the loading screen
|
||||||
// This will trigger OnInitialLoadingComplete via the event when animation completes
|
// This will trigger OnInitialLoadingComplete via the event when animation completes
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ namespace Core
|
|||||||
// Search through all registered pickups
|
// Search through all registered pickups
|
||||||
foreach (var pickup in _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;
|
return pickup.gameObject;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
@@ -62,6 +62,11 @@ namespace Core.Lifecycle
|
|||||||
private bool isBootComplete = false;
|
private bool isBootComplete = false;
|
||||||
private string currentSceneReady = "";
|
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;
|
[SerializeField] private bool enableDebugLogging = true;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -115,26 +120,36 @@ namespace Core.Lifecycle
|
|||||||
// Track which scene this component belongs to
|
// Track which scene this component belongs to
|
||||||
componentScenes[component] = sceneName;
|
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)
|
if (isBootComplete)
|
||||||
{
|
{
|
||||||
// Boot already complete - call OnManagedAwake immediately
|
// Check if we're currently loading a scene
|
||||||
LogDebug($"Late registration: Calling OnManagedAwake immediately for {component.gameObject.name}");
|
if (isLoadingScene && sceneName == sceneBeingLoaded)
|
||||||
try
|
|
||||||
{
|
{
|
||||||
component.InvokeManagedAwake();
|
// Batch this component - will be processed in priority order when scene load completes
|
||||||
HandleAutoRegistrations(component);
|
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
|
// If boot not complete, component stays in list and will be processed by BroadcastManagedAwake()
|
||||||
{
|
|
||||||
// Boot not complete yet - add to list for broadcast
|
|
||||||
InsertSorted(managedAwakeList, component, component.ManagedAwakePriority);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register for all scene lifecycle hooks
|
// Register for all scene lifecycle hooks
|
||||||
InsertSorted(sceneUnloadingList, component, component.SceneUnloadingPriority);
|
InsertSorted(sceneUnloadingList, component, component.SceneUnloadingPriority);
|
||||||
@@ -143,20 +158,8 @@ namespace Core.Lifecycle
|
|||||||
InsertSorted(restoreRequestedList, component, component.RestorePriority);
|
InsertSorted(restoreRequestedList, component, component.RestorePriority);
|
||||||
InsertSorted(destroyList, component, component.DestroyPriority);
|
InsertSorted(destroyList, component, component.DestroyPriority);
|
||||||
|
|
||||||
// If this scene is already ready, call OnSceneReady immediately
|
// If this scene is already ready (and we're not in loading mode), call OnSceneReady immediately
|
||||||
// Check both currentSceneReady AND if the Unity scene is actually loaded
|
if (!isLoadingScene && currentSceneReady == sceneName)
|
||||||
// (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)
|
|
||||||
{
|
{
|
||||||
LogDebug($"Late registration: Calling OnSceneReady immediately for {component.gameObject.name}");
|
LogDebug($"Late registration: Calling OnSceneReady immediately for {component.gameObject.name}");
|
||||||
try
|
try
|
||||||
@@ -233,8 +236,63 @@ namespace Core.Lifecycle
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear the list - components already initialized
|
// NOTE: We do NOT clear managedAwakeList here!
|
||||||
managedAwakeList.Clear();
|
// 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>
|
/// <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>
|
/// <summary>
|
||||||
/// Broadcast OnSceneReady to components in the specified scene (priority order).
|
/// Broadcast OnSceneReady to components in the specified scene (priority order).
|
||||||
|
/// If scene loading mode is active, processes batched components first.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void BroadcastSceneReady(string sceneName)
|
public void BroadcastSceneReady(string sceneName)
|
||||||
{
|
{
|
||||||
LogDebug($"Broadcasting SceneReady for scene: {sceneName}");
|
LogDebug($"Broadcasting SceneReady for scene: {sceneName}");
|
||||||
currentSceneReady = 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)
|
foreach (var component in sceneReadyList)
|
||||||
{
|
{
|
||||||
if (component == null) continue;
|
if (component == null) continue;
|
||||||
@@ -318,28 +356,179 @@ namespace Core.Lifecycle
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </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
|
try
|
||||||
{
|
{
|
||||||
component.InvokeRestoreRequested();
|
component.InvokeSceneRestoreRequested(serializedData);
|
||||||
|
restoredCount++;
|
||||||
|
LogDebug($"Restored scene data to: {component.SaveId}");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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
|
#endregion
|
||||||
|
|||||||
@@ -57,6 +57,27 @@ namespace Core.Lifecycle
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual bool AutoRegisterPausable => false;
|
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
|
#endregion
|
||||||
|
|
||||||
#region Public Accessors (for LifecycleManager)
|
#region Public Accessors (for LifecycleManager)
|
||||||
@@ -65,9 +86,13 @@ namespace Core.Lifecycle
|
|||||||
public void InvokeManagedAwake() => OnManagedAwake();
|
public void InvokeManagedAwake() => OnManagedAwake();
|
||||||
public void InvokeSceneUnloading() => OnSceneUnloading();
|
public void InvokeSceneUnloading() => OnSceneUnloading();
|
||||||
public void InvokeSceneReady() => OnSceneReady();
|
public void InvokeSceneReady() => OnSceneReady();
|
||||||
public void InvokeSaveRequested() => OnSaveRequested();
|
public string InvokeSceneSaveRequested() => OnSceneSaveRequested();
|
||||||
public void InvokeRestoreRequested() => OnRestoreRequested();
|
public void InvokeSceneRestoreRequested(string data) => OnSceneRestoreRequested(data);
|
||||||
|
public string InvokeGlobalSaveRequested() => OnGlobalSaveRequested();
|
||||||
|
public void InvokeGlobalRestoreRequested(string data) => OnGlobalRestoreRequested(data);
|
||||||
public void InvokeManagedDestroy() => OnManagedDestroy();
|
public void InvokeManagedDestroy() => OnManagedDestroy();
|
||||||
|
public void InvokeGlobalLoadCompleted() => OnGlobalLoadCompleted();
|
||||||
|
public void InvokeGlobalSaveStarted() => OnGlobalSaveStarted();
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -158,25 +183,91 @@ namespace Core.Lifecycle
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Called before scene unloads to save data via SaveLoadManager.
|
/// Called during scene transitions to save scene-specific state.
|
||||||
/// Called in REVERSE priority order (higher values execute first).
|
/// Return serialized data (e.g., JsonUtility.ToJson(myData)).
|
||||||
/// Integrates with existing SaveLoadManager save system.
|
/// Return null if component has no scene-specific state to save.
|
||||||
/// Return serialized state string (e.g., JsonUtility.ToJson(myData)).
|
///
|
||||||
|
/// TIMING:
|
||||||
|
/// - Called BEFORE scene unload during scene transitions
|
||||||
|
/// - Frequency: Every scene transition
|
||||||
|
/// - Use for: Level progress, object positions, puzzle states
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual void OnSaveRequested()
|
protected virtual string OnSceneSaveRequested()
|
||||||
{
|
{
|
||||||
// Override in derived classes
|
return null; // Default: no data to save
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Called after scene loads to restore data via SaveLoadManager.
|
/// Called during scene transitions to restore scene-specific state.
|
||||||
/// Called in priority order (lower values execute first).
|
/// Receives previously serialized data (from OnSceneSaveRequested).
|
||||||
/// Integrates with existing SaveLoadManager restore system.
|
///
|
||||||
/// Receives serialized state string to restore from.
|
/// TIMING:
|
||||||
|
/// - Called AFTER scene load, during OnSceneReady phase
|
||||||
|
/// - Frequency: Every scene transition
|
||||||
|
/// - Use for: Restoring level progress, object positions, puzzle states
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@@ -62,10 +62,6 @@ namespace Core.SaveLoad
|
|||||||
{
|
{
|
||||||
Logging.Debug("[SaveLoadManager] Initialized");
|
Logging.Debug("[SaveLoadManager] Initialized");
|
||||||
|
|
||||||
#if UNITY_EDITOR
|
|
||||||
DiscoverInactiveSaveables("RestoreInEditor");
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Load save data if save system is enabled (depends on settings from GameManager)
|
// Load save data if save system is enabled (depends on settings from GameManager)
|
||||||
if (DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().useSaveLoadSystem)
|
if (DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().useSaveLoadSystem)
|
||||||
{
|
{
|
||||||
@@ -75,16 +71,20 @@ namespace Core.SaveLoad
|
|||||||
|
|
||||||
protected override void OnSceneReady()
|
protected override void OnSceneReady()
|
||||||
{
|
{
|
||||||
// Discover and register inactive SaveableInteractables in the newly loaded scene
|
// SaveableInteractables now auto-register via ManagedBehaviour lifecycle
|
||||||
string sceneName = UnityEngine.SceneManagement.SceneManager.GetActiveScene().name;
|
// No need to discover and register them manually
|
||||||
DiscoverInactiveSaveables(sceneName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnSaveRequested()
|
protected override string OnSceneSaveRequested()
|
||||||
{
|
{
|
||||||
// Scene is about to unload - this is now handled by SceneManagerService
|
// SaveLoadManager orchestrates saves, doesn't participate in them
|
||||||
// which calls Save() globally before scene transitions
|
return null;
|
||||||
Logging.Debug($"[SaveLoadManager] OnSaveRequested called");
|
}
|
||||||
|
|
||||||
|
protected override string OnGlobalSaveRequested()
|
||||||
|
{
|
||||||
|
// SaveLoadManager orchestrates saves, doesn't participate in them
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnApplicationQuit()
|
private void OnApplicationQuit()
|
||||||
@@ -180,40 +180,6 @@ namespace Core.SaveLoad
|
|||||||
return participant;
|
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
|
#endregion
|
||||||
|
|
||||||
@@ -260,6 +226,29 @@ namespace Core.SaveLoad
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
IsRestoringState = true;
|
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;
|
int restoredCount = 0;
|
||||||
|
|
||||||
// Clear pending queue at the start
|
// Clear pending queue at the start
|
||||||
@@ -272,19 +261,17 @@ namespace Core.SaveLoad
|
|||||||
string saveId = kvp.Key;
|
string saveId = kvp.Key;
|
||||||
ISaveParticipant participant = kvp.Value;
|
ISaveParticipant participant = kvp.Value;
|
||||||
|
|
||||||
// Find the participant state in the list
|
if (saveDataDict.TryGetValue(saveId, out string serializedState))
|
||||||
var entry = currentSaveData.participantStates.Find(e => e.saveId == saveId);
|
|
||||||
if (entry != null && !string.IsNullOrEmpty(entry.serializedState))
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
participant.RestoreState(entry.serializedState);
|
participant.RestoreState(serializedState);
|
||||||
restoredCount++;
|
restoredCount++;
|
||||||
Logging.Debug($"[SaveLoadManager] Restored state for participant: {saveId}");
|
Logging.Debug($"[SaveLoadManager] Restored ISaveParticipant: {saveId}");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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();
|
pendingParticipants.Clear();
|
||||||
IsRestoringState = false;
|
IsRestoringState = false;
|
||||||
|
|
||||||
Logging.Debug($"[SaveLoadManager] Restored state for {restoredCount} participants + {totalPendingRestored} pending participants");
|
Logging.Debug($"[SaveLoadManager] Restored {restoredCount} ISaveParticipants + {totalPendingRestored} pending participants");
|
||||||
OnParticipantStatesRestored?.Invoke();
|
OnParticipantStatesRestored?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,6 +328,76 @@ namespace Core.SaveLoad
|
|||||||
return Path.Combine(DefaultSaveFolder, $"save_{slot}.json");
|
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>
|
/// <summary>
|
||||||
/// Entry point to save to a named slot. Starts an async coroutine that writes to disk.
|
/// Entry point to save to a named slot. Starts an async coroutine that writes to disk.
|
||||||
/// Fires OnSaveCompleted when finished.
|
/// Fires OnSaveCompleted when finished.
|
||||||
@@ -397,14 +454,44 @@ namespace Core.SaveLoad
|
|||||||
{
|
{
|
||||||
currentSaveData.participantStates = new List<ParticipantStateEntry>();
|
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
|
// Build a dictionary of all new data to save
|
||||||
// Create a snapshot to avoid collection modification during iteration
|
var allNewData = new Dictionary<string, string>();
|
||||||
int savedCount = 0;
|
|
||||||
|
// 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())
|
foreach (var kvp in participants.ToList())
|
||||||
{
|
{
|
||||||
string saveId = kvp.Key;
|
string saveId = kvp.Key;
|
||||||
@@ -413,13 +500,8 @@ namespace Core.SaveLoad
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
string serializedState = participant.SerializeState();
|
string serializedState = participant.SerializeState();
|
||||||
currentSaveData.participantStates.Add(new ParticipantStateEntry
|
allNewData[saveId] = serializedState;
|
||||||
{
|
Logging.Debug($"[SaveLoadManager] Captured state for ISaveParticipant: {saveId}");
|
||||||
saveId = saveId,
|
|
||||||
serializedState = serializedState
|
|
||||||
});
|
|
||||||
savedCount++;
|
|
||||||
Logging.Debug($"[SaveLoadManager] Captured state for participant: {saveId}");
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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);
|
json = JsonUtility.ToJson(currentSaveData, true);
|
||||||
@@ -546,6 +649,12 @@ namespace Core.SaveLoad
|
|||||||
// Restore state for any already-registered participants
|
// Restore state for any already-registered participants
|
||||||
RestoreAllParticipantStates();
|
RestoreAllParticipantStates();
|
||||||
|
|
||||||
|
// NEW: Broadcast global load completed event (ONCE, on boot)
|
||||||
|
if (Lifecycle.LifecycleManager.Instance != null)
|
||||||
|
{
|
||||||
|
Lifecycle.LifecycleManager.Instance.BroadcastGlobalLoadCompleted();
|
||||||
|
}
|
||||||
|
|
||||||
OnLoadCompleted?.Invoke(slot);
|
OnLoadCompleted?.Invoke(slot);
|
||||||
Logging.Debug($"[SaveLoadManager] Load completed for slot '{slot}'");
|
Logging.Debug($"[SaveLoadManager] Load completed for slot '{slot}'");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -309,18 +309,14 @@ namespace Core
|
|||||||
LogDebugMessage($"Broadcasting OnSceneUnloading for: {oldSceneName}");
|
LogDebugMessage($"Broadcasting OnSceneUnloading for: {oldSceneName}");
|
||||||
LifecycleManager.Instance?.BroadcastSceneUnloading(oldSceneName);
|
LifecycleManager.Instance?.BroadcastSceneUnloading(oldSceneName);
|
||||||
|
|
||||||
// PHASE 3: Broadcast save request - components save their level-specific data
|
// PHASE 3: Save scene-specific data via SaveLoadManager
|
||||||
LogDebugMessage($"Broadcasting OnSaveRequested for: {oldSceneName}");
|
|
||||||
LifecycleManager.Instance?.BroadcastSaveRequested(oldSceneName);
|
|
||||||
|
|
||||||
// PHASE 4: Trigger global save if save system is enabled
|
|
||||||
if (SaveLoadManager.Instance != null)
|
if (SaveLoadManager.Instance != null)
|
||||||
{
|
{
|
||||||
var debugSettings = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>();
|
var debugSettings = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>();
|
||||||
if (debugSettings.useSaveLoadSystem)
|
if (debugSettings.useSaveLoadSystem)
|
||||||
{
|
{
|
||||||
LogDebugMessage("Saving global game state");
|
LogDebugMessage($"Saving scene data for: {oldSceneName}");
|
||||||
SaveLoadManager.Instance.Save();
|
SaveLoadManager.Instance.SaveSceneData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,19 +351,30 @@ namespace Core
|
|||||||
SceneManager.LoadScene(BootstrapSceneName, LoadSceneMode.Additive);
|
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);
|
await LoadSceneAsync(newSceneName, progress);
|
||||||
CurrentGameplayScene = newSceneName;
|
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}");
|
LogDebugMessage($"Broadcasting OnSceneReady for: {newSceneName}");
|
||||||
LifecycleManager.Instance?.BroadcastSceneReady(newSceneName);
|
LifecycleManager.Instance?.BroadcastSceneReady(newSceneName);
|
||||||
|
|
||||||
// PHASE 10: Broadcast restore request - components restore their level-specific data
|
// PHASE 11: Restore scene-specific data via SaveLoadManager
|
||||||
LogDebugMessage($"Broadcasting OnRestoreRequested for: {newSceneName}");
|
if (SaveLoadManager.Instance != null)
|
||||||
LifecycleManager.Instance?.BroadcastRestoreRequested(newSceneName);
|
{
|
||||||
|
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)
|
if (autoHideLoadingScreen && _loadingScreen != null)
|
||||||
{
|
{
|
||||||
_loadingScreen.HideLoadingScreen();
|
_loadingScreen.HideLoadingScreen();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Collections;
|
using System.Collections;
|
||||||
using AppleHills.Core.Settings;
|
using AppleHills.Core.Settings;
|
||||||
using Core.Lifecycle;
|
using Core.Lifecycle;
|
||||||
using Settings;
|
using Settings;
|
||||||
@@ -32,28 +32,30 @@ namespace Core
|
|||||||
_logVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().sceneLogVerbosity;
|
_logVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().sceneLogVerbosity;
|
||||||
|
|
||||||
LogDebugMessage("Initialized");
|
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()
|
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
|
// Determine desired orientation for this scene
|
||||||
string sceneName = scene.name;
|
|
||||||
ScreenOrientationRequirement requirement = ScreenOrientationRequirement.NotApplicable;
|
ScreenOrientationRequirement requirement = ScreenOrientationRequirement.NotApplicable;
|
||||||
|
|
||||||
if (sceneName.ToLower().Contains("bootstrap"))
|
if (sceneName.ToLower().Contains("bootstrap"))
|
||||||
@@ -94,7 +96,6 @@ namespace Core
|
|||||||
protected override void OnDestroy()
|
protected override void OnDestroy()
|
||||||
{
|
{
|
||||||
base.OnDestroy(); // Important: call base
|
base.OnDestroy(); // Important: call base
|
||||||
SceneManager.sceneLoaded -= OnSceneLoaded;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using AppleHills.Data.CardSystem;
|
using AppleHills.Data.CardSystem;
|
||||||
@@ -14,14 +14,19 @@ namespace Data.CardSystem
|
|||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Manages the player's card collection, booster packs, and related operations.
|
/// Manages the player's card collection, booster packs, and related operations.
|
||||||
/// Uses a singleton pattern for global access.
|
/// Manages the card collection system for the game.
|
||||||
/// Implements ISaveParticipant to integrate with the save/load system.
|
/// Handles unlocking cards, tracking collections, and integrating with the save/load system.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CardSystemManager : ManagedBehaviour, ISaveParticipant
|
public class CardSystemManager : ManagedBehaviour
|
||||||
{
|
{
|
||||||
private static CardSystemManager _instance;
|
private static CardSystemManager _instance;
|
||||||
public static CardSystemManager Instance => _instance;
|
public static CardSystemManager Instance => _instance;
|
||||||
|
|
||||||
|
// Save system configuration
|
||||||
|
public override bool AutoRegisterForSave => true;
|
||||||
|
public override string SaveId => "CardSystemManager";
|
||||||
|
|
||||||
|
|
||||||
[Header("Card Collection")]
|
[Header("Card Collection")]
|
||||||
[SerializeField] private List<CardDefinition> availableCards = new List<CardDefinition>();
|
[SerializeField] private List<CardDefinition> availableCards = new List<CardDefinition>();
|
||||||
|
|
||||||
@@ -87,17 +92,6 @@ namespace Data.CardSystem
|
|||||||
BuildDefinitionLookup();
|
BuildDefinitionLookup();
|
||||||
|
|
||||||
Logging.Debug($"[CardSystemManager] Loaded {availableCards.Count} card definitions from Addressables");
|
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
|
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>
|
/// <summary>
|
||||||
/// Builds a lookup dictionary for quick access to card definitions by ID
|
/// 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;
|
protected override string OnGlobalSaveRequested()
|
||||||
|
|
||||||
/// <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()
|
|
||||||
{
|
{
|
||||||
var state = ExportCardCollectionState();
|
var state = ExportCardCollectionState();
|
||||||
return JsonUtility.ToJson(state);
|
return JsonUtility.ToJson(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
protected override void OnGlobalRestoreRequested(string serializedData)
|
||||||
/// Restores the card collection state from serialized JSON data.
|
|
||||||
/// </summary>
|
|
||||||
public void RestoreState(string serializedData)
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(serializedData))
|
if (string.IsNullOrEmpty(serializedData))
|
||||||
{
|
{
|
||||||
Logging.Debug("[CardSystemManager] No saved state to restore, using defaults");
|
Logging.Debug("[CardSystemManager] No saved state to restore, using defaults");
|
||||||
hasBeenRestored = true;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -506,7 +469,6 @@ namespace Data.CardSystem
|
|||||||
if (state != null)
|
if (state != null)
|
||||||
{
|
{
|
||||||
ApplyCardCollectionState(state);
|
ApplyCardCollectionState(state);
|
||||||
hasBeenRestored = true;
|
|
||||||
Logging.Debug("[CardSystemManager] Successfully restored card collection state");
|
Logging.Debug("[CardSystemManager] Successfully restored card collection state");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ namespace Input
|
|||||||
/// Handles player movement in response to tap and hold input events.
|
/// 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.
|
/// Supports both direct and pathfinding movement modes, and provides event/callbacks for arrival/cancellation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class PlayerTouchController : ManagedBehaviour, ITouchInputConsumer, ISaveParticipant
|
public class PlayerTouchController : ManagedBehaviour, ITouchInputConsumer
|
||||||
{
|
{
|
||||||
// --- Movement State ---
|
// --- Movement State ---
|
||||||
private Vector3 targetPosition;
|
private Vector3 targetPosition;
|
||||||
@@ -67,9 +67,10 @@ namespace Input
|
|||||||
private bool interruptMoveTo;
|
private bool interruptMoveTo;
|
||||||
private LogVerbosity _logVerbosity = LogVerbosity.Warning;
|
private LogVerbosity _logVerbosity = LogVerbosity.Warning;
|
||||||
|
|
||||||
// Save system tracking
|
// Save system configuration
|
||||||
private bool hasBeenRestored;
|
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
|
public override int ManagedAwakePriority => 100; // Player controller
|
||||||
|
|
||||||
protected override void OnManagedAwake()
|
protected override void OnManagedAwake()
|
||||||
@@ -93,28 +94,6 @@ namespace Input
|
|||||||
InputManager.Instance?.SetDefaultConsumer(this);
|
InputManager.Instance?.SetDefaultConsumer(this);
|
||||||
|
|
||||||
_logVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().inputLogVerbosity;
|
_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>
|
/// <summary>
|
||||||
@@ -454,16 +433,9 @@ namespace Input
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#region ISaveParticipant Implementation
|
#region Save/Load Lifecycle Hooks
|
||||||
|
|
||||||
public bool HasBeenRestored => hasBeenRestored;
|
protected override string OnSceneSaveRequested()
|
||||||
|
|
||||||
public string GetSaveId()
|
|
||||||
{
|
|
||||||
return "PlayerController";
|
|
||||||
}
|
|
||||||
|
|
||||||
public string SerializeState()
|
|
||||||
{
|
{
|
||||||
var saveData = new PlayerSaveData
|
var saveData = new PlayerSaveData
|
||||||
{
|
{
|
||||||
@@ -473,12 +445,11 @@ namespace Input
|
|||||||
return JsonUtility.ToJson(saveData);
|
return JsonUtility.ToJson(saveData);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RestoreState(string serializedData)
|
protected override void OnSceneRestoreRequested(string serializedData)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(serializedData))
|
if (string.IsNullOrEmpty(serializedData))
|
||||||
{
|
{
|
||||||
Logging.Debug("[PlayerTouchController] No saved state to restore");
|
Logging.Debug("[PlayerTouchController] No saved state to restore");
|
||||||
hasBeenRestored = true;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -489,7 +460,6 @@ namespace Input
|
|||||||
{
|
{
|
||||||
transform.position = saveData.worldPosition;
|
transform.position = saveData.worldPosition;
|
||||||
transform.rotation = saveData.worldRotation;
|
transform.rotation = saveData.worldRotation;
|
||||||
hasBeenRestored = true;
|
|
||||||
Logging.Debug($"[PlayerTouchController] Restored position: {saveData.worldPosition}");
|
Logging.Debug($"[PlayerTouchController] Restored position: {saveData.worldPosition}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.Events;
|
using UnityEngine.Events;
|
||||||
using System; // for Action<T>
|
using System; // for Action<T>
|
||||||
@@ -24,6 +24,7 @@ namespace Interactions
|
|||||||
{
|
{
|
||||||
public ItemSlotState slotState;
|
public ItemSlotState slotState;
|
||||||
public string slottedItemSaveId;
|
public string slottedItemSaveId;
|
||||||
|
public string slottedItemDataId; // ItemId of the PickupItemData (for verification)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -227,6 +228,16 @@ namespace Interactions
|
|||||||
{
|
{
|
||||||
var previousData = currentlySlottedItemData;
|
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;
|
currentlySlottedItemObject = null;
|
||||||
currentlySlottedItemData = null;
|
currentlySlottedItemData = null;
|
||||||
currentState = ItemSlotState.None;
|
currentState = ItemSlotState.None;
|
||||||
@@ -272,17 +283,15 @@ namespace Interactions
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
// Register with ItemManager when enabled
|
// Register with ItemManager when enabled
|
||||||
protected override void Start()
|
private void OnEnable()
|
||||||
{
|
{
|
||||||
base.Start(); // SaveableInteractable registration
|
|
||||||
|
|
||||||
// Register as ItemSlot
|
// Register as ItemSlot
|
||||||
ItemManager.Instance?.RegisterItemSlot(this);
|
ItemManager.Instance?.RegisterItemSlot(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnDestroy()
|
protected override void OnDestroy()
|
||||||
{
|
{
|
||||||
base.OnDestroy(); // SaveableInteractable cleanup
|
base.OnDestroy();
|
||||||
|
|
||||||
// Unregister from slot manager
|
// Unregister from slot manager
|
||||||
ItemManager.Instance?.UnregisterItemSlot(this);
|
ItemManager.Instance?.UnregisterItemSlot(this);
|
||||||
@@ -294,20 +303,28 @@ namespace Interactions
|
|||||||
{
|
{
|
||||||
// Get slotted item save ID if there's a slotted item
|
// Get slotted item save ID if there's a slotted item
|
||||||
string slottedSaveId = "";
|
string slottedSaveId = "";
|
||||||
|
string slottedDataId = "";
|
||||||
|
|
||||||
if (currentlySlottedItemObject != null)
|
if (currentlySlottedItemObject != null)
|
||||||
{
|
{
|
||||||
var slottedPickup = currentlySlottedItemObject.GetComponent<Pickup>();
|
var slottedPickup = currentlySlottedItemObject.GetComponent<Pickup>();
|
||||||
if (slottedPickup is SaveableInteractable saveablePickup)
|
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
|
return new ItemSlotSaveData
|
||||||
{
|
{
|
||||||
slotState = currentState,
|
slotState = currentState,
|
||||||
slottedItemSaveId = slottedSaveId
|
slottedItemSaveId = slottedSaveId,
|
||||||
|
slottedItemDataId = slottedDataId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,7 +343,8 @@ namespace Interactions
|
|||||||
// Restore slotted item if there was one
|
// Restore slotted item if there was one
|
||||||
if (!string.IsNullOrEmpty(data.slottedItemSaveId))
|
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.
|
/// Restore a slotted item from save data.
|
||||||
/// This is called during load restoration and should NOT trigger events.
|
/// This is called during load restoration and should NOT trigger events.
|
||||||
/// </summary>
|
/// </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
|
// Try to find the item in the scene by its save ID via ItemManager
|
||||||
GameObject slottedObject = ItemManager.Instance?.FindPickupBySaveId(slottedItemSaveId);
|
GameObject slottedObject = ItemManager.Instance?.FindPickupBySaveId(slottedItemSaveId);
|
||||||
@@ -351,11 +369,33 @@ namespace Interactions
|
|||||||
if (pickup != null)
|
if (pickup != null)
|
||||||
{
|
{
|
||||||
slottedData = pickup.itemData;
|
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)
|
// Silently slot the item (no events, no interaction completion)
|
||||||
// Follower state is managed separately during save/load restoration
|
// Follower state is managed separately during save/load restoration
|
||||||
ApplySlottedItemState(slottedObject, slottedData, triggerEvents: false);
|
ApplySlottedItemState(slottedObject, slottedData, triggerEvents: false);
|
||||||
|
|
||||||
|
Debug.Log($"[ItemSlot] Successfully restored slotted item: {slottedData.itemName} (itemId: {slottedData.itemId})");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -370,7 +410,16 @@ namespace Interactions
|
|||||||
{
|
{
|
||||||
if (itemToSlot == null)
|
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;
|
var previousData = currentlySlottedItemData;
|
||||||
currentlySlottedItemObject = null;
|
currentlySlottedItemObject = null;
|
||||||
currentlySlottedItemData = null;
|
currentlySlottedItemData = null;
|
||||||
@@ -391,6 +440,14 @@ namespace Interactions
|
|||||||
SetSlottedObject(itemToSlot);
|
SetSlottedObject(itemToSlot);
|
||||||
currentlySlottedItemData = itemToSlotData;
|
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
|
// Determine if correct
|
||||||
var config = interactionSettings?.GetSlotItemConfig(itemData);
|
var config = interactionSettings?.GetSlotItemConfig(itemData);
|
||||||
var allowed = config?.allowedItems ?? new List<PickupItemData>();
|
var allowed = config?.allowedItems ?? new List<PickupItemData>();
|
||||||
@@ -433,6 +490,33 @@ namespace Interactions
|
|||||||
ApplySlottedItemState(itemToSlot, itemToSlotData, triggerEvents: true);
|
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
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ namespace Interactions
|
|||||||
{
|
{
|
||||||
public bool isPickedUp;
|
public bool isPickedUp;
|
||||||
public bool wasHeldByFollower;
|
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 Vector3 worldPosition;
|
||||||
public Quaternion worldRotation;
|
public Quaternion worldRotation;
|
||||||
public bool isActive;
|
public bool isActive;
|
||||||
@@ -24,6 +26,9 @@ namespace Interactions
|
|||||||
public SpriteRenderer iconRenderer;
|
public SpriteRenderer iconRenderer;
|
||||||
public bool IsPickedUp { get; internal set; }
|
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> OnItemPickedUp;
|
||||||
public event Action<PickupItemData, PickupItemData, PickupItemData> OnItemsCombined;
|
public event Action<PickupItemData, PickupItemData, PickupItemData> OnItemsCombined;
|
||||||
|
|
||||||
@@ -37,18 +42,16 @@ namespace Interactions
|
|||||||
ApplyItemData();
|
ApplyItemData();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Start()
|
// Always register with ItemManager, even if picked up
|
||||||
{
|
|
||||||
base.Start(); // Register with save system
|
|
||||||
|
|
||||||
// Always register with ItemManager, even if picked up
|
|
||||||
// This allows the save/load system to find held items when restoring state
|
// This allows the save/load system to find held items when restoring state
|
||||||
|
protected override void OnManagedAwake()
|
||||||
|
{
|
||||||
ItemManager.Instance?.RegisterPickup(this);
|
ItemManager.Instance?.RegisterPickup(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnDestroy()
|
protected override void OnDestroy()
|
||||||
{
|
{
|
||||||
base.OnDestroy(); // Unregister from save system
|
base.OnDestroy();
|
||||||
|
|
||||||
// Unregister from ItemManager
|
// Unregister from ItemManager
|
||||||
ItemManager.Instance?.UnregisterPickup(this);
|
ItemManager.Instance?.UnregisterPickup(this);
|
||||||
@@ -139,10 +142,16 @@ namespace Interactions
|
|||||||
// Check if this pickup is currently held by the follower
|
// Check if this pickup is currently held by the follower
|
||||||
bool isHeldByFollower = IsPickedUp && !gameObject.activeSelf && transform.parent != null;
|
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
|
return new PickupSaveData
|
||||||
{
|
{
|
||||||
isPickedUp = this.IsPickedUp,
|
isPickedUp = this.IsPickedUp,
|
||||||
wasHeldByFollower = isHeldByFollower,
|
wasHeldByFollower = isHeldByFollower,
|
||||||
|
wasInSlot = isInSlot,
|
||||||
|
slotSaveId = slotId,
|
||||||
worldPosition = transform.position,
|
worldPosition = transform.position,
|
||||||
worldRotation = transform.rotation,
|
worldRotation = transform.rotation,
|
||||||
isActive = gameObject.activeSelf
|
isActive = gameObject.activeSelf
|
||||||
@@ -177,6 +186,20 @@ namespace Interactions
|
|||||||
follower.TryClaimHeldItem(this);
|
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
|
else
|
||||||
{
|
{
|
||||||
@@ -190,6 +213,28 @@ namespace Interactions
|
|||||||
// This prevents duplicate logic execution
|
// 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>
|
/// <summary>
|
||||||
/// Resets the pickup state when the item is dropped back into the world.
|
/// Resets the pickup state when the item is dropped back into the world.
|
||||||
/// Called by FollowerController when swapping items.
|
/// Called by FollowerController when swapping items.
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using Core.SaveLoad;
|
using UnityEngine;
|
||||||
using UnityEngine;
|
|
||||||
using UnityEngine.SceneManagement;
|
|
||||||
|
|
||||||
namespace Interactions
|
namespace Interactions
|
||||||
{
|
{
|
||||||
@@ -8,21 +6,13 @@ namespace Interactions
|
|||||||
/// Base class for interactables that participate in the save/load system.
|
/// Base class for interactables that participate in the save/load system.
|
||||||
/// Provides common save ID generation and serialization infrastructure.
|
/// Provides common save ID generation and serialization infrastructure.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class SaveableInteractable : InteractableBase, ISaveParticipant
|
public abstract class SaveableInteractable : InteractableBase
|
||||||
{
|
{
|
||||||
[Header("Save System")]
|
[Header("Save System")]
|
||||||
[SerializeField]
|
[SerializeField]
|
||||||
[Tooltip("Optional custom save ID. If empty, will auto-generate from hierarchy path.")]
|
|
||||||
private string customSaveId = "";
|
|
||||||
|
|
||||||
/// <summary>
|
// Save system configuration
|
||||||
/// Sets a custom save ID for this interactable.
|
public override bool AutoRegisterForSave => true;
|
||||||
/// Used when spawning dynamic objects that need stable save IDs.
|
|
||||||
/// </summary>
|
|
||||||
public void SetCustomSaveId(string saveId)
|
|
||||||
{
|
|
||||||
customSaveId = saveId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Flag to indicate we're currently restoring from save data.
|
/// Flag to indicate we're currently restoring from save data.
|
||||||
@@ -30,99 +20,10 @@ namespace Interactions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected bool IsRestoringFromSave { get; private set; }
|
protected bool IsRestoringFromSave { get; private set; }
|
||||||
|
|
||||||
private bool hasRegistered;
|
|
||||||
private bool hasRestoredState;
|
|
||||||
|
|
||||||
/// <summary>
|
#region Save/Load Lifecycle Hooks
|
||||||
/// Returns true if this participant has already had its state restored.
|
|
||||||
/// </summary>
|
|
||||||
public bool HasBeenRestored => hasRestoredState;
|
|
||||||
|
|
||||||
protected virtual void Awake()
|
protected override string OnSceneSaveRequested()
|
||||||
{
|
|
||||||
// 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()
|
|
||||||
{
|
{
|
||||||
object stateData = GetSerializableState();
|
object stateData = GetSerializableState();
|
||||||
if (stateData == null)
|
if (stateData == null)
|
||||||
@@ -133,28 +34,17 @@ namespace Interactions
|
|||||||
return JsonUtility.ToJson(stateData);
|
return JsonUtility.ToJson(stateData);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RestoreState(string serializedData)
|
protected override void OnSceneRestoreRequested(string serializedData)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(serializedData))
|
if (string.IsNullOrEmpty(serializedData))
|
||||||
{
|
{
|
||||||
Debug.LogWarning($"[SaveableInteractable] Empty save data for {GetSaveId()}");
|
Debug.LogWarning($"[SaveableInteractable] Empty save data for {SaveId}");
|
||||||
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
|
|
||||||
return;
|
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;
|
IsRestoringFromSave = true;
|
||||||
hasRestoredState = true;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -162,7 +52,7 @@ namespace Interactions
|
|||||||
}
|
}
|
||||||
catch (System.Exception e)
|
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
|
finally
|
||||||
{
|
{
|
||||||
@@ -189,61 +79,22 @@ namespace Interactions
|
|||||||
|
|
||||||
#endregion
|
#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
|
#region Editor Helpers
|
||||||
|
|
||||||
#if UNITY_EDITOR
|
#if UNITY_EDITOR
|
||||||
[ContextMenu("Log Save ID")]
|
[ContextMenu("Log Save ID")]
|
||||||
private void LogSaveId()
|
private void LogSaveId()
|
||||||
{
|
{
|
||||||
Debug.Log($"Save ID: {GetSaveId()}");
|
Debug.Log($"Save ID: {SaveId}");
|
||||||
}
|
}
|
||||||
|
|
||||||
[ContextMenu("Test Serialize/Deserialize")]
|
[ContextMenu("Test Serialize/Deserialize")]
|
||||||
private void TestSerializeDeserialize()
|
private void TestSerializeDeserialize()
|
||||||
{
|
{
|
||||||
string serialized = SerializeState();
|
string serialized = OnSceneSaveRequested();
|
||||||
Debug.Log($"Serialized state: {serialized}");
|
Debug.Log($"Serialized state: {serialized}");
|
||||||
|
|
||||||
RestoreState(serialized);
|
OnSceneRestoreRequested(serialized);
|
||||||
Debug.Log("Deserialization test complete");
|
Debug.Log("Deserialization test complete");
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ namespace Levels
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected override void Awake()
|
protected override void Awake()
|
||||||
{
|
{
|
||||||
base.Awake(); // Register with save system
|
base.Awake();
|
||||||
|
|
||||||
switchActive = true;
|
switchActive = true;
|
||||||
if (iconRenderer == null)
|
if (iconRenderer == null)
|
||||||
@@ -52,11 +52,11 @@ namespace Levels
|
|||||||
ApplySwitchData();
|
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)
|
if (PuzzleManager.Instance != null)
|
||||||
{
|
{
|
||||||
PuzzleManager.Instance.OnAllPuzzlesComplete += HandleAllPuzzlesComplete;
|
PuzzleManager.Instance.OnAllPuzzlesComplete += HandleAllPuzzlesComplete;
|
||||||
@@ -69,8 +69,10 @@ namespace Levels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDestroy()
|
protected override void OnDestroy()
|
||||||
{
|
{
|
||||||
|
base.OnDestroy();
|
||||||
|
|
||||||
if (PuzzleManager.Instance != null)
|
if (PuzzleManager.Instance != null)
|
||||||
{
|
{
|
||||||
PuzzleManager.Instance.OnAllPuzzlesComplete -= HandleAllPuzzlesComplete;
|
PuzzleManager.Instance.OnAllPuzzlesComplete -= HandleAllPuzzlesComplete;
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
using Interactions;
|
using Interactions;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using Pathfinding;
|
using Pathfinding;
|
||||||
using UnityEngine.SceneManagement;
|
|
||||||
using Utils;
|
using Utils;
|
||||||
using AppleHills.Core.Settings;
|
using AppleHills.Core.Settings;
|
||||||
using Core;
|
using Core;
|
||||||
using Core.Lifecycle;
|
using Core.Lifecycle;
|
||||||
using Core.SaveLoad;
|
|
||||||
using UnityEngine.Events;
|
using UnityEngine.Events;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -18,13 +16,13 @@ public class FollowerSaveData
|
|||||||
public Vector3 worldPosition;
|
public Vector3 worldPosition;
|
||||||
public Quaternion worldRotation;
|
public Quaternion worldRotation;
|
||||||
public string heldItemSaveId; // Save ID of held pickup (if any)
|
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>
|
/// <summary>
|
||||||
/// Controls the follower character, including following the player, handling pickups, and managing held items.
|
/// Controls the follower character, including following the player, handling pickups, and managing held items.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class FollowerController : ManagedBehaviour, ISaveParticipant
|
public class FollowerController : ManagedBehaviour
|
||||||
{
|
{
|
||||||
private static readonly int CombineTrigger = Animator.StringToHash("Combine");
|
private static readonly int CombineTrigger = Animator.StringToHash("Combine");
|
||||||
|
|
||||||
@@ -54,6 +52,12 @@ public class FollowerController : ManagedBehaviour, ISaveParticipant
|
|||||||
// Direction variables for 2D blend tree animation
|
// Direction variables for 2D blend tree animation
|
||||||
private float _lastDirX = 0f; // -1 (left) to 1 (right)
|
private float _lastDirX = 0f; // -1 (left) to 1 (right)
|
||||||
private float _lastDirY = -1f; // -1 (down) to 1 (up)
|
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 float _currentSpeed = 0f;
|
||||||
private Animator _animator;
|
private Animator _animator;
|
||||||
private Transform _artTransform;
|
private Transform _artTransform;
|
||||||
@@ -98,8 +102,7 @@ public class FollowerController : ManagedBehaviour, ISaveParticipant
|
|||||||
|
|
||||||
private Input.PlayerTouchController _playerTouchController;
|
private Input.PlayerTouchController _playerTouchController;
|
||||||
|
|
||||||
// Save system tracking
|
// Save system tracking for bilateral restoration
|
||||||
private bool hasBeenRestored;
|
|
||||||
private bool _hasRestoredHeldItem; // Track if held item restoration completed
|
private bool _hasRestoredHeldItem; // Track if held item restoration completed
|
||||||
private string _expectedHeldItemSaveId; // Expected saveId during restoration
|
private string _expectedHeldItemSaveId; // Expected saveId during restoration
|
||||||
|
|
||||||
@@ -124,38 +127,11 @@ public class FollowerController : ManagedBehaviour, ISaveParticipant
|
|||||||
// Initialize settings references
|
// Initialize settings references
|
||||||
_settings = GameManager.GetSettingsObject<IPlayerFollowerSettings>();
|
_settings = GameManager.GetSettingsObject<IPlayerFollowerSettings>();
|
||||||
_interactionSettings = GameManager.GetSettingsObject<IInteractionSettings>();
|
_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()
|
protected override void OnSceneReady()
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
|
// Find player reference when scene is ready (called for every scene load)
|
||||||
FindPlayerReference();
|
FindPlayerReference();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,9 +139,7 @@ public class FollowerController : ManagedBehaviour, ISaveParticipant
|
|||||||
{
|
{
|
||||||
if (_playerTransform == null)
|
if (_playerTransform == null)
|
||||||
{
|
{
|
||||||
FindPlayerReference();
|
return;
|
||||||
if (_playerTransform == null)
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip all movement logic when playing a stationary animation
|
// Skip all movement logic when playing a stationary animation
|
||||||
@@ -749,16 +723,9 @@ public class FollowerController : ManagedBehaviour, ISaveParticipant
|
|||||||
|
|
||||||
#endregion ItemInteractions
|
#endregion ItemInteractions
|
||||||
|
|
||||||
#region ISaveParticipant Implementation
|
#region Save/Load Lifecycle Hooks
|
||||||
|
|
||||||
public bool HasBeenRestored => hasBeenRestored;
|
protected override string OnSceneSaveRequested()
|
||||||
|
|
||||||
public string GetSaveId()
|
|
||||||
{
|
|
||||||
return "FollowerController";
|
|
||||||
}
|
|
||||||
|
|
||||||
public string SerializeState()
|
|
||||||
{
|
{
|
||||||
var saveData = new FollowerSaveData
|
var saveData = new FollowerSaveData
|
||||||
{
|
{
|
||||||
@@ -772,26 +739,24 @@ public class FollowerController : ManagedBehaviour, ISaveParticipant
|
|||||||
var pickup = _cachedPickupObject.GetComponent<Pickup>();
|
var pickup = _cachedPickupObject.GetComponent<Pickup>();
|
||||||
if (pickup is SaveableInteractable saveable)
|
if (pickup is SaveableInteractable saveable)
|
||||||
{
|
{
|
||||||
saveData.heldItemSaveId = saveable.GetSaveId();
|
saveData.heldItemSaveId = saveable.SaveId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save the itemId for build-compatible restoration
|
||||||
if (_currentlyHeldItemData != null)
|
if (_currentlyHeldItemData != null)
|
||||||
{
|
{
|
||||||
#if UNITY_EDITOR
|
saveData.heldItemDataAssetPath = _currentlyHeldItemData.itemId;
|
||||||
saveData.heldItemDataAssetPath = UnityEditor.AssetDatabase.GetAssetPath(_currentlyHeldItemData);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return JsonUtility.ToJson(saveData);
|
return JsonUtility.ToJson(saveData);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RestoreState(string serializedData)
|
protected override void OnSceneRestoreRequested(string serializedData)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(serializedData))
|
if (string.IsNullOrEmpty(serializedData))
|
||||||
{
|
{
|
||||||
Logging.Debug("[FollowerController] No saved state to restore");
|
Logging.Debug("[FollowerController] No saved state to restore");
|
||||||
hasBeenRestored = true;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -811,7 +776,6 @@ public class FollowerController : ManagedBehaviour, ISaveParticipant
|
|||||||
TryRestoreHeldItem(saveData.heldItemSaveId, saveData.heldItemDataAssetPath);
|
TryRestoreHeldItem(saveData.heldItemSaveId, saveData.heldItemDataAssetPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
hasBeenRestored = true;
|
|
||||||
Logging.Debug($"[FollowerController] Restored position: {saveData.worldPosition}");
|
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.
|
/// 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.
|
/// If pickup doesn't exist yet, it will try to claim us when it restores.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void TryRestoreHeldItem(string heldItemSaveId, string heldItemDataAssetPath)
|
private void TryRestoreHeldItem(string heldItemSaveId, string itemDataId)
|
||||||
{
|
{
|
||||||
if (_hasRestoredHeldItem)
|
if (_hasRestoredHeldItem)
|
||||||
{
|
{
|
||||||
@@ -850,7 +814,7 @@ public class FollowerController : ManagedBehaviour, ISaveParticipant
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Claim the pickup
|
// Claim the pickup
|
||||||
TakeOwnership(pickup, heldItemDataAssetPath);
|
TakeOwnership(pickup, itemDataId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -871,9 +835,9 @@ public class FollowerController : ManagedBehaviour, ISaveParticipant
|
|||||||
// Verify this is the expected pickup
|
// Verify this is the expected pickup
|
||||||
if (pickup is SaveableInteractable saveable)
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -886,28 +850,29 @@ public class FollowerController : ManagedBehaviour, ISaveParticipant
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Takes ownership of a pickup during restoration. Called by both restoration paths.
|
/// Takes ownership of a pickup during restoration. Called by both restoration paths.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void TakeOwnership(Pickup pickup, string itemDataAssetPath)
|
private void TakeOwnership(Pickup pickup, string itemDataIdOrPath)
|
||||||
{
|
{
|
||||||
if (_hasRestoredHeldItem)
|
if (_hasRestoredHeldItem)
|
||||||
return; // Already claimed
|
return; // Already claimed
|
||||||
|
|
||||||
// Get the item data
|
// Get the item data from the pickup
|
||||||
PickupItemData heldData = pickup.itemData;
|
PickupItemData heldData = pickup.itemData;
|
||||||
|
|
||||||
#if UNITY_EDITOR
|
// Fallback: If pickup doesn't have itemData, log detailed error
|
||||||
// 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
|
|
||||||
|
|
||||||
if (heldData == null)
|
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;
|
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
|
// Setup the held item
|
||||||
_cachedPickupObject = pickup.gameObject;
|
_cachedPickupObject = pickup.gameObject;
|
||||||
_cachedPickupObject.SetActive(false); // Held items should be hidden
|
_cachedPickupObject.SetActive(false); // Held items should be hidden
|
||||||
@@ -915,7 +880,7 @@ public class FollowerController : ManagedBehaviour, ISaveParticipant
|
|||||||
_animator.SetBool("IsCarrying", true);
|
_animator.SetBool("IsCarrying", true);
|
||||||
_hasRestoredHeldItem = 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>
|
/// <summary>
|
||||||
@@ -927,7 +892,7 @@ public class FollowerController : ManagedBehaviour, ISaveParticipant
|
|||||||
return FindObjectOfType<FollowerController>();
|
return FindObjectOfType<FollowerController>();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion ISaveParticipant Implementation
|
#endregion Save/Load Lifecycle Hooks
|
||||||
|
|
||||||
#if UNITY_EDITOR
|
#if UNITY_EDITOR
|
||||||
void OnDrawGizmos()
|
void OnDrawGizmos()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using Interactions;
|
using Interactions;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using Core;
|
using Core;
|
||||||
|
using Core.Lifecycle;
|
||||||
|
|
||||||
namespace PuzzleS
|
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.
|
/// Manages the state and interactions for a single puzzle step, including unlock/lock logic and event handling.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[RequireComponent(typeof(InteractableBase))]
|
[RequireComponent(typeof(InteractableBase))]
|
||||||
public class ObjectiveStepBehaviour : MonoBehaviour, IPuzzlePrompt
|
public class ObjectiveStepBehaviour : ManagedBehaviour, IPuzzlePrompt
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The data object representing this puzzle step.
|
/// The data object representing this puzzle step.
|
||||||
@@ -31,8 +32,10 @@ namespace PuzzleS
|
|||||||
// Enum for tracking proximity state (simplified to just Close and Far)
|
// Enum for tracking proximity state (simplified to just Close and Far)
|
||||||
public enum ProximityState { Close, Far }
|
public enum ProximityState { Close, Far }
|
||||||
|
|
||||||
void Awake()
|
protected override void Awake()
|
||||||
{
|
{
|
||||||
|
base.Awake();
|
||||||
|
|
||||||
_interactable = GetComponent<InteractableBase>();
|
_interactable = GetComponent<InteractableBase>();
|
||||||
|
|
||||||
// Initialize the indicator if it exists, but ensure it's hidden initially
|
// 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()
|
void OnEnable()
|
||||||
{
|
{
|
||||||
if (_interactable == null)
|
if (_interactable == null)
|
||||||
@@ -69,27 +87,18 @@ namespace PuzzleS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Start()
|
void OnDisable()
|
||||||
{
|
|
||||||
// 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()
|
|
||||||
{
|
{
|
||||||
if (_interactable != null)
|
if (_interactable != null)
|
||||||
{
|
{
|
||||||
_interactable.interactionStarted.RemoveListener(OnInteractionStarted);
|
_interactable.interactionStarted.RemoveListener(OnInteractionStarted);
|
||||||
_interactable.interactionComplete.RemoveListener(OnInteractionComplete);
|
_interactable.interactionComplete.RemoveListener(OnInteractionComplete);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDestroy()
|
||||||
|
{
|
||||||
|
base.OnDestroy();
|
||||||
|
|
||||||
if (PuzzleManager.Instance != null && stepData != null)
|
if (PuzzleManager.Instance != null && stepData != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ using UnityEngine.SceneManagement;
|
|||||||
using AppleHills.Core.Settings;
|
using AppleHills.Core.Settings;
|
||||||
using Core;
|
using Core;
|
||||||
using Core.Lifecycle;
|
using Core.Lifecycle;
|
||||||
using Core.SaveLoad;
|
|
||||||
using UnityEngine.AddressableAssets;
|
using UnityEngine.AddressableAssets;
|
||||||
using UnityEngine.ResourceManagement.AsyncOperations;
|
using UnityEngine.ResourceManagement.AsyncOperations;
|
||||||
using Utils;
|
using Utils;
|
||||||
@@ -28,7 +27,7 @@ namespace PuzzleS
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Manages puzzle step registration, dependency management, and step completion for the puzzle system.
|
/// Manages puzzle step registration, dependency management, and step completion for the puzzle system.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class PuzzleManager : ManagedBehaviour, ISaveParticipant
|
public class PuzzleManager : ManagedBehaviour
|
||||||
{
|
{
|
||||||
private static PuzzleManager _instance;
|
private static PuzzleManager _instance;
|
||||||
|
|
||||||
@@ -49,6 +48,10 @@ namespace PuzzleS
|
|||||||
// Store registered behaviors that are waiting for data to be loaded
|
// Store registered behaviors that are waiting for data to be loaded
|
||||||
private List<ObjectiveStepBehaviour> _registeredBehaviours = new List<ObjectiveStepBehaviour>();
|
private List<ObjectiveStepBehaviour> _registeredBehaviours = new List<ObjectiveStepBehaviour>();
|
||||||
|
|
||||||
|
// Save system configuration
|
||||||
|
public override bool AutoRegisterForSave => true;
|
||||||
|
public override string SaveId => $"{SceneManager.GetActiveScene().name}/PuzzleManager";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Singleton instance of the PuzzleManager.
|
/// Singleton instance of the PuzzleManager.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -66,7 +69,6 @@ namespace PuzzleS
|
|||||||
|
|
||||||
// Save/Load restoration tracking
|
// Save/Load restoration tracking
|
||||||
private bool _isDataRestored = false;
|
private bool _isDataRestored = false;
|
||||||
private bool _hasBeenRestored = false;
|
|
||||||
private List<ObjectiveStepBehaviour> _pendingRegistrations = new List<ObjectiveStepBehaviour>();
|
private List<ObjectiveStepBehaviour> _pendingRegistrations = new List<ObjectiveStepBehaviour>();
|
||||||
|
|
||||||
// Registration for ObjectiveStepBehaviour
|
// Registration for ObjectiveStepBehaviour
|
||||||
@@ -75,12 +77,6 @@ namespace PuzzleS
|
|||||||
// Track pending unlocks for steps that were unlocked before their behavior registered
|
// Track pending unlocks for steps that were unlocked before their behavior registered
|
||||||
private HashSet<string> _pendingUnlocks = new HashSet<string>();
|
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
|
public override int ManagedAwakePriority => 80; // Puzzle systems
|
||||||
|
|
||||||
private new void Awake()
|
private new void Awake()
|
||||||
@@ -108,10 +104,6 @@ namespace PuzzleS
|
|||||||
LoadPuzzleDataForCurrentScene();
|
LoadPuzzleDataForCurrentScene();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register with save/load system
|
|
||||||
SaveLoadManager.Instance.RegisterParticipant(this);
|
|
||||||
Logging.Debug("[PuzzleManager] Registered with SaveLoadManager");
|
|
||||||
|
|
||||||
// Subscribe to scene load events from SceneManagerService
|
// Subscribe to scene load events from SceneManagerService
|
||||||
// This is necessary because PuzzleManager is in DontDestroyOnLoad and won't receive OnSceneReady() callbacks
|
// This is necessary because PuzzleManager is in DontDestroyOnLoad and won't receive OnSceneReady() callbacks
|
||||||
if (SceneManagerService.Instance != null)
|
if (SceneManagerService.Instance != null)
|
||||||
@@ -558,21 +550,9 @@ namespace PuzzleS
|
|||||||
return _isDataLoaded;
|
return _isDataLoaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
#region ISaveParticipant Implementation
|
#region Save/Load Lifecycle Hooks
|
||||||
|
|
||||||
/// <summary>
|
protected override string OnSceneSaveRequested()
|
||||||
/// 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()
|
|
||||||
{
|
{
|
||||||
if (_currentLevelData == null)
|
if (_currentLevelData == null)
|
||||||
{
|
{
|
||||||
@@ -592,16 +572,12 @@ namespace PuzzleS
|
|||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
protected override void OnSceneRestoreRequested(string data)
|
||||||
/// Restore puzzle state from serialized JSON data
|
|
||||||
/// </summary>
|
|
||||||
public void RestoreState(string data)
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(data) || data == "{}")
|
if (string.IsNullOrEmpty(data) || data == "{}")
|
||||||
{
|
{
|
||||||
Logging.Debug("[PuzzleManager] No puzzle save data to restore");
|
Logging.Debug("[PuzzleManager] No puzzle save data to restore");
|
||||||
_isDataRestored = true;
|
_isDataRestored = true;
|
||||||
_hasBeenRestored = true;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -612,7 +588,6 @@ namespace PuzzleS
|
|||||||
{
|
{
|
||||||
Logging.Warning("[PuzzleManager] Failed to deserialize puzzle save data");
|
Logging.Warning("[PuzzleManager] Failed to deserialize puzzle save data");
|
||||||
_isDataRestored = true;
|
_isDataRestored = true;
|
||||||
_hasBeenRestored = true;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -621,7 +596,6 @@ namespace PuzzleS
|
|||||||
_unlockedSteps = new HashSet<string>(saveData.unlockedStepIds ?? new List<string>());
|
_unlockedSteps = new HashSet<string>(saveData.unlockedStepIds ?? new List<string>());
|
||||||
|
|
||||||
_isDataRestored = true;
|
_isDataRestored = true;
|
||||||
_hasBeenRestored = true;
|
|
||||||
|
|
||||||
Logging.Debug($"[PuzzleManager] Restored puzzle state: {_completedSteps.Count} completed, {_unlockedSteps.Count} unlocked steps");
|
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}");
|
Debug.LogError($"[PuzzleManager] Error restoring puzzle state: {e.Message}");
|
||||||
_isDataRestored = true;
|
_isDataRestored = true;
|
||||||
_hasBeenRestored = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using AppleHills.Data.CardSystem;
|
using AppleHills.Data.CardSystem;
|
||||||
using Core;
|
using Core;
|
||||||
using Data.CardSystem;
|
using Data.CardSystem;
|
||||||
@@ -34,8 +34,6 @@ namespace UI.CardSystem
|
|||||||
|
|
||||||
private void Awake()
|
private void Awake()
|
||||||
{
|
{
|
||||||
_cardManager = CardSystemManager.Instance;
|
|
||||||
|
|
||||||
// Make sure we have a CanvasGroup for transitions
|
// Make sure we have a CanvasGroup for transitions
|
||||||
if (canvasGroup == null)
|
if (canvasGroup == null)
|
||||||
canvasGroup = GetComponent<CanvasGroup>();
|
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>
|
/// <summary>
|
||||||
/// Sets up the album when the page becomes active
|
/// Sets up the album when the page becomes active
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using AppleHills.Data.CardSystem;
|
using AppleHills.Data.CardSystem;
|
||||||
using Core;
|
using Core;
|
||||||
@@ -52,7 +52,6 @@ namespace UI.CardSystem
|
|||||||
|
|
||||||
private void Awake()
|
private void Awake()
|
||||||
{
|
{
|
||||||
_cardManager = CardSystemManager.Instance;
|
|
||||||
_cardAlbumUI = FindFirstObjectByType<CardAlbumUI>();
|
_cardAlbumUI = FindFirstObjectByType<CardAlbumUI>();
|
||||||
|
|
||||||
// Set up button listeners
|
// Set up button listeners
|
||||||
@@ -86,6 +85,14 @@ namespace UI.CardSystem
|
|||||||
HideAllCardBacks();
|
HideAllCardBacks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnManagedAwake()
|
||||||
|
{
|
||||||
|
base.OnManagedAwake();
|
||||||
|
|
||||||
|
// Safe to access manager instance here
|
||||||
|
_cardManager = CardSystemManager.Instance;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cache all card back buttons from the container
|
/// Cache all card back buttons from the container
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ namespace UI.CardSystem
|
|||||||
|
|
||||||
protected override void OnManagedAwake()
|
protected override void OnManagedAwake()
|
||||||
{
|
{
|
||||||
|
base.OnManagedAwake();
|
||||||
|
|
||||||
|
// Get card manager - safe to access .Instance here
|
||||||
|
_cardManager = CardSystemManager.Instance;
|
||||||
|
|
||||||
// Set up backpack button
|
// Set up backpack button
|
||||||
if (backpackButton != null)
|
if (backpackButton != null)
|
||||||
{
|
{
|
||||||
@@ -54,20 +59,7 @@ namespace UI.CardSystem
|
|||||||
// Initialize pages and hide them
|
// Initialize pages and hide them
|
||||||
InitializePages();
|
InitializePages();
|
||||||
|
|
||||||
// React to global UI hide/show events (top-page only) by toggling this GameObject
|
// Subscribe to card manager events
|
||||||
if (UIPageController.Instance != null)
|
|
||||||
{
|
|
||||||
UIPageController.Instance.OnAllUIHidden += HandleAllUIHidden;
|
|
||||||
UIPageController.Instance.OnAllUIShown += HandleAllUIShown;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Start()
|
|
||||||
{
|
|
||||||
// Get card manager
|
|
||||||
_cardManager = CardSystemManager.Instance;
|
|
||||||
|
|
||||||
// Subscribe to events
|
|
||||||
if (_cardManager != null)
|
if (_cardManager != null)
|
||||||
{
|
{
|
||||||
_cardManager.OnBoosterCountChanged += UpdateBoosterCount;
|
_cardManager.OnBoosterCountChanged += UpdateBoosterCount;
|
||||||
@@ -78,6 +70,13 @@ namespace UI.CardSystem
|
|||||||
// Initialize UI with current values
|
// Initialize UI with current values
|
||||||
UpdateBoosterCount(_cardManager.GetBoosterPackCount());
|
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()
|
private void OnDestroy()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Core;
|
using Core;
|
||||||
using Data.CardSystem;
|
using Data.CardSystem;
|
||||||
using Pixelplacement;
|
using Pixelplacement;
|
||||||
using UI.Core;
|
using UI.Core;
|
||||||
@@ -28,9 +28,8 @@ namespace UI.CardSystem
|
|||||||
|
|
||||||
private void Awake()
|
private void Awake()
|
||||||
{
|
{
|
||||||
// Get references
|
// Get UI reference
|
||||||
_cardAlbumUI = FindAnyObjectByType<CardAlbumUI>();
|
_cardAlbumUI = FindAnyObjectByType<CardAlbumUI>();
|
||||||
_cardManager = CardSystemManager.Instance;
|
|
||||||
|
|
||||||
// Make sure we have a CanvasGroup
|
// Make sure we have a CanvasGroup
|
||||||
if (canvasGroup == null)
|
if (canvasGroup == null)
|
||||||
@@ -48,6 +47,14 @@ namespace UI.CardSystem
|
|||||||
{
|
{
|
||||||
viewAlbumButton.onClick.AddListener(OnViewAlbumClicked);
|
viewAlbumButton.onClick.AddListener(OnViewAlbumClicked);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnManagedAwake()
|
||||||
|
{
|
||||||
|
base.OnManagedAwake();
|
||||||
|
|
||||||
|
// Safe to access manager instance here
|
||||||
|
_cardManager = CardSystemManager.Instance;
|
||||||
|
|
||||||
if (changeClothesButton != null)
|
if (changeClothesButton != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ MonoBehaviour:
|
|||||||
m_EditorClassIdentifier: AppleHillsScripts::AppleHills.Core.Settings.DebugSettings
|
m_EditorClassIdentifier: AppleHillsScripts::AppleHills.Core.Settings.DebugSettings
|
||||||
showDebugUiMessages: 1
|
showDebugUiMessages: 1
|
||||||
pauseTimeOnPauseGame: 0
|
pauseTimeOnPauseGame: 0
|
||||||
useSaveLoadSystem: 0
|
useSaveLoadSystem: 1
|
||||||
bootstrapLogVerbosity: 0
|
bootstrapLogVerbosity: 0
|
||||||
settingsLogVerbosity: 1
|
settingsLogVerbosity: 1
|
||||||
gameManagerLogVerbosity: 1
|
gameManagerLogVerbosity: 1
|
||||||
|
|||||||
Reference in New Issue
Block a user