Refactoring of the interaction system and preliminary integration of save/load functionality across the game. (#44)
### Interactables Architecture Refactor - Converted composition to inheritance, moved from component-based to class-based interactables. No more requirement for chain of "Interactable -> Item" etc. - Created `InteractableBase` abstract base class with common functionality that replaces the old component - Specialized child classes: `Pickup`, `ItemSlot`, `LevelSwitch`, `MinigameSwitch`, `CombinationItem`, `OneClickInteraction` are now children classes - Light updates to the interactable inspector, moved some things arround, added collapsible inspector sections in the UI for better editor experience ### State Machine Integration - Custom `AppleMachine` inheritong from Pixelplacement's StateMachine which implements our own interface for saving, easy place for future improvements - Replaced all previous StateMachines by `AppleMachine` - Custom `AppleState` extends from default `State`. Added serialization, split state logic into "EnterState", "RestoreState", "ExitState" allowing for separate logic when triggering in-game vs loading game - Restores directly to target state without triggering transitional logic - Migration tool converts existing instances ### Prefab Organization - Saved changes from scenes into prefabs - Cleaned up duplicated components, confusing prefabs hierarchies - Created prefab variants where possible - Consolidated Environment prefabs and moved them out of Placeholders subfolder into main Environment folder - Organized item prefabs from PrefabsPLACEHOLDER into proper Items folder - Updated prefab references - All scene references updated to new locations - Removed placeholder files from Characters, Levels, UI, and Minigames folders ### Scene Updates - Quarry scene with major updates - Saved multiple working versions (Quarry, Quarry_Fixed, Quarry_OLD) - Added proper lighting data - Updated all interactable components to new architecture ### Minor editor tools - New tool for testing cards from an editor window (no in-scene object required) - Updated Interactable Inspector - New debug option to opt in-and-out of the save/load system - Tooling for easier migration Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com> Reviewed-on: #44
This commit is contained in:
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Interactions;
|
||||
using Bootstrap;
|
||||
using Core.SaveLoad;
|
||||
|
||||
namespace Core
|
||||
{
|
||||
@@ -59,6 +60,7 @@ namespace Core
|
||||
{
|
||||
// Subscribe to scene load completed so we can clear registrations when scenes change
|
||||
SceneManagerService.Instance.SceneLoadStarted += OnSceneLoadStarted;
|
||||
|
||||
Logging.Debug("[ItemManager] Subscribed to SceneManagerService events");
|
||||
}
|
||||
|
||||
@@ -244,5 +246,36 @@ namespace Core
|
||||
|
||||
public IEnumerable<Pickup> Pickups => _pickups;
|
||||
public IEnumerable<ItemSlot> ItemSlots => _itemSlots;
|
||||
|
||||
/// <summary>
|
||||
/// Gets all registered pickups. Used by save/load system to find items by save ID.
|
||||
/// </summary>
|
||||
public IEnumerable<Pickup> GetAllPickups() => _pickups;
|
||||
|
||||
/// <summary>
|
||||
/// Gets all registered item slots. Used by save/load system.
|
||||
/// </summary>
|
||||
public IEnumerable<ItemSlot> GetAllItemSlots() => _itemSlots;
|
||||
|
||||
/// <summary>
|
||||
/// Finds a pickup by its save ID. Used by save/load system to restore item references.
|
||||
/// </summary>
|
||||
/// <param name="saveId">The save ID to search for</param>
|
||||
/// <returns>The pickup's GameObject if found, null otherwise</returns>
|
||||
public GameObject FindPickupBySaveId(string saveId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(saveId)) return null;
|
||||
|
||||
// Search through all registered pickups
|
||||
foreach (var pickup in _pickups)
|
||||
{
|
||||
if (pickup is SaveableInteractable saveable && saveable.GetSaveId() == saveId)
|
||||
{
|
||||
return pickup.gameObject;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
257
Assets/Scripts/Core/SaveLoad/AppleMachine.cs
Normal file
257
Assets/Scripts/Core/SaveLoad/AppleMachine.cs
Normal file
@@ -0,0 +1,257 @@
|
||||
using UnityEngine;
|
||||
using Pixelplacement;
|
||||
using Bootstrap;
|
||||
|
||||
namespace Core.SaveLoad
|
||||
{
|
||||
// SaveableStateMachine - Inherits from StateMachine, uses SaveableState for states
|
||||
// Auto-generates Save ID from scene name and hierarchy path (like SaveableInteractable)
|
||||
|
||||
/// <summary>
|
||||
/// Extended StateMachine that integrates with the AppleHills save/load system.
|
||||
/// Inherits from Pixelplacement.StateMachine and adds save/load functionality.
|
||||
/// Use SaveableState (not State) for child states to get save/load hooks.
|
||||
/// </summary>
|
||||
public class AppleMachine : StateMachine, ISaveParticipant
|
||||
{
|
||||
[SerializeField]
|
||||
[Tooltip("Optional custom save ID. If empty, will auto-generate from scene name and hierarchy path.")]
|
||||
private string customSaveId = "";
|
||||
|
||||
/// <summary>
|
||||
/// Is this state machine currently being restored from a save file?
|
||||
/// </summary>
|
||||
public bool IsRestoring { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Has this state machine been restored from save data?
|
||||
/// </summary>
|
||||
public bool HasBeenRestored { get; private set; }
|
||||
|
||||
// Override ChangeState to call OnEnterState on SaveableState components
|
||||
public new GameObject ChangeState(GameObject state)
|
||||
{
|
||||
var result = base.ChangeState(state);
|
||||
|
||||
// If not restoring and change was successful, call OnEnterState
|
||||
if (!IsRestoring && result != null && currentState != null)
|
||||
{
|
||||
var saveableState = currentState.GetComponent<AppleState>();
|
||||
if (saveableState != null)
|
||||
{
|
||||
saveableState.OnEnterState();
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public new GameObject ChangeState(string state)
|
||||
{
|
||||
var result = base.ChangeState(state);
|
||||
|
||||
// If not restoring and change was successful, call OnEnterState
|
||||
if (!IsRestoring && result != null && currentState != null)
|
||||
{
|
||||
var saveableState = currentState.GetComponent<AppleState>();
|
||||
if (saveableState != null)
|
||||
{
|
||||
saveableState.OnEnterState();
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public new GameObject ChangeState(int childIndex)
|
||||
{
|
||||
var result = base.ChangeState(childIndex);
|
||||
|
||||
// If not restoring and change was successful, call OnEnterState
|
||||
if (!IsRestoring && result != null && currentState != null)
|
||||
{
|
||||
var saveableState = currentState.GetComponent<AppleState>();
|
||||
if (saveableState != null)
|
||||
{
|
||||
saveableState.OnEnterState();
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// Register with save system (no validation needed - we auto-generate ID)
|
||||
BootCompletionService.RegisterInitAction(() =>
|
||||
{
|
||||
if (SaveLoadManager.Instance != null)
|
||||
{
|
||||
SaveLoadManager.Instance.RegisterParticipant(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[SaveableStateMachine] SaveLoadManager.Instance is null, cannot register '{name}'", this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void OnValidate()
|
||||
{
|
||||
// Optional: Log the auto-generated ID in verbose mode
|
||||
if (verbose && string.IsNullOrEmpty(customSaveId))
|
||||
{
|
||||
Debug.Log($"[SaveableStateMachine] '{name}' will use auto-generated Save ID: {GetSaveId()}", this);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
// Unregister from save system
|
||||
if (SaveLoadManager.Instance != null)
|
||||
{
|
||||
SaveLoadManager.Instance.UnregisterParticipant(GetSaveId());
|
||||
}
|
||||
}
|
||||
|
||||
#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}/StateMachine_{hierarchyPath}";
|
||||
}
|
||||
|
||||
private string GetSceneName()
|
||||
{
|
||||
return gameObject.scene.name;
|
||||
}
|
||||
|
||||
private string GetHierarchyPath()
|
||||
{
|
||||
string path = gameObject.name;
|
||||
Transform parent = transform.parent;
|
||||
|
||||
while (parent != null)
|
||||
{
|
||||
path = parent.name + "/" + path;
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
public string SerializeState()
|
||||
{
|
||||
if (currentState == null)
|
||||
{
|
||||
return JsonUtility.ToJson(new StateMachineSaveData { stateName = "", stateData = "" });
|
||||
}
|
||||
|
||||
AppleState appleState = currentState.GetComponent<AppleState>();
|
||||
string stateData = appleState?.SerializeState() ?? "";
|
||||
|
||||
var saveData = new StateMachineSaveData
|
||||
{
|
||||
stateName = currentState.name,
|
||||
stateData = stateData
|
||||
};
|
||||
|
||||
return JsonUtility.ToJson(saveData);
|
||||
}
|
||||
|
||||
public void RestoreState(string data)
|
||||
{
|
||||
if (string.IsNullOrEmpty(data))
|
||||
{
|
||||
if (verbose)
|
||||
{
|
||||
Debug.LogWarning($"[SaveableStateMachine] No data to restore for '{name}'", this);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
StateMachineSaveData saveData = JsonUtility.FromJson<StateMachineSaveData>(data);
|
||||
|
||||
if (string.IsNullOrEmpty(saveData.stateName))
|
||||
{
|
||||
if (verbose)
|
||||
{
|
||||
Debug.LogWarning($"[SaveableStateMachine] No state name in save data for '{name}'", this);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Set IsRestoring flag so we won't call OnEnterState
|
||||
IsRestoring = true;
|
||||
|
||||
// Change to the saved state
|
||||
ChangeState(saveData.stateName);
|
||||
|
||||
// Now explicitly call OnRestoreState with the saved data
|
||||
if (currentState != null)
|
||||
{
|
||||
AppleState appleState = currentState.GetComponent<AppleState>();
|
||||
if (appleState != null)
|
||||
{
|
||||
appleState.OnRestoreState(saveData.stateData);
|
||||
}
|
||||
}
|
||||
|
||||
HasBeenRestored = true;
|
||||
IsRestoring = false;
|
||||
|
||||
if (verbose)
|
||||
{
|
||||
Debug.Log($"[SaveableStateMachine] Restored '{name}' to state: {saveData.stateName}", this);
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogError($"[SaveableStateMachine] Exception restoring '{name}': {ex.Message}", this);
|
||||
IsRestoring = false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Editor Utilities
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[ContextMenu("Log Save ID")]
|
||||
private void LogSaveId()
|
||||
{
|
||||
Debug.Log($"Save ID: {GetSaveId()}", this);
|
||||
}
|
||||
|
||||
[ContextMenu("Test Serialize")]
|
||||
private void TestSerialize()
|
||||
{
|
||||
string serialized = SerializeState();
|
||||
Debug.Log($"Serialized state: {serialized}", this);
|
||||
}
|
||||
#endif
|
||||
|
||||
#endregion
|
||||
|
||||
[System.Serializable]
|
||||
private class StateMachineSaveData
|
||||
{
|
||||
public string stateName;
|
||||
public string stateData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3
Assets/Scripts/Core/SaveLoad/AppleMachine.cs.meta
Normal file
3
Assets/Scripts/Core/SaveLoad/AppleMachine.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6f56763d30b94bf6873d395a6c116eb5
|
||||
timeCreated: 1762116611
|
||||
47
Assets/Scripts/Core/SaveLoad/AppleState.cs
Normal file
47
Assets/Scripts/Core/SaveLoad/AppleState.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using Pixelplacement;
|
||||
|
||||
namespace Core.SaveLoad
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for states that need save/load functionality.
|
||||
/// Inherit from this instead of Pixelplacement.State for states in SaveableStateMachines.
|
||||
/// </summary>
|
||||
public class AppleState : State
|
||||
{
|
||||
/// <summary>
|
||||
/// Called when this state is entered during normal gameplay.
|
||||
/// Override this method to implement state initialization logic
|
||||
/// (animations, player movement, event subscriptions, etc.).
|
||||
/// This is NOT called when restoring from a save file.
|
||||
/// </summary>
|
||||
public virtual void OnEnterState()
|
||||
{
|
||||
// Default: Do nothing
|
||||
// States override this to implement their entry logic
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when this state is being restored from a save file.
|
||||
/// Override this method to restore state from saved data without
|
||||
/// playing animations or triggering side effects.
|
||||
/// </summary>
|
||||
/// <param name="data">Serialized state data from SerializeState()</param>
|
||||
public virtual void OnRestoreState(string data)
|
||||
{
|
||||
// Default: Do nothing
|
||||
// States override this to implement their restoration logic
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the state machine is being saved.
|
||||
/// Override this method to serialize this state's internal data.
|
||||
/// </summary>
|
||||
/// <returns>Serialized state data as a string (JSON recommended)</returns>
|
||||
public virtual string SerializeState()
|
||||
{
|
||||
// Default: No state data to save
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3
Assets/Scripts/Core/SaveLoad/AppleState.cs.meta
Normal file
3
Assets/Scripts/Core/SaveLoad/AppleState.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 95e46aacea5b42888ee7881894193c11
|
||||
timeCreated: 1762121675
|
||||
34
Assets/Scripts/Core/SaveLoad/ISaveParticipant.cs
Normal file
34
Assets/Scripts/Core/SaveLoad/ISaveParticipant.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
namespace Core.SaveLoad
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for objects that participate in the save/load system.
|
||||
/// Participants must provide a unique ID and handle their own serialization/deserialization.
|
||||
/// </summary>
|
||||
public interface ISaveParticipant
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a globally unique identifier for this participant.
|
||||
/// Must be consistent across sessions for the same logical object.
|
||||
/// </summary>
|
||||
string GetSaveId();
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the current state of this participant to a string.
|
||||
/// Can use JSON, custom format, or any serialization method.
|
||||
/// </summary>
|
||||
string SerializeState();
|
||||
|
||||
/// <summary>
|
||||
/// Restores the state of this participant from previously serialized data.
|
||||
/// Should handle null/empty data gracefully with default behavior.
|
||||
/// </summary>
|
||||
void RestoreState(string serializedData);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this participant has already had its state restored.
|
||||
/// Used to prevent double-restoration when inactive objects become active.
|
||||
/// </summary>
|
||||
bool HasBeenRestored { get; }
|
||||
}
|
||||
}
|
||||
|
||||
12
Assets/Scripts/Core/SaveLoad/ISaveParticipant.cs.meta
Normal file
12
Assets/Scripts/Core/SaveLoad/ISaveParticipant.cs.meta
Normal file
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7f3a4e8c9d2b1a5f6e8c4d9a2b7e5f3a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -5,28 +5,22 @@ namespace Core.SaveLoad
|
||||
[System.Serializable]
|
||||
public class SaveLoadData
|
||||
{
|
||||
public bool playedDivingTutorial = false;
|
||||
|
||||
// Snapshot of the player's card collection (MVP)
|
||||
public CardCollectionState cardCollection;
|
||||
public bool playedDivingTutorial;
|
||||
|
||||
// List of unlocked minigames by name
|
||||
public List<string> unlockedMinigames = new List<string>();
|
||||
|
||||
// List of participant states (directly serializable by JsonUtility)
|
||||
public List<ParticipantStateEntry> participantStates = new List<ParticipantStateEntry>();
|
||||
}
|
||||
|
||||
// Minimal DTOs for card persistence
|
||||
|
||||
/// <summary>
|
||||
/// Serializable key-value pair for participant state storage.
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public class CardCollectionState
|
||||
public class ParticipantStateEntry
|
||||
{
|
||||
public int boosterPackCount;
|
||||
public List<SavedCardEntry> cards = new List<SavedCardEntry>();
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class SavedCardEntry
|
||||
{
|
||||
public string definitionId;
|
||||
public AppleHills.Data.CardSystem.CardRarity rarity;
|
||||
public int copiesOwned;
|
||||
public string saveId;
|
||||
public string serializedState;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,21 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using AppleHills.Core.Settings;
|
||||
using Bootstrap;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Core.SaveLoad
|
||||
{
|
||||
/// <summary>
|
||||
/// Simple Save/Load manager that follows the project's bootstrap pattern.
|
||||
/// Save/Load manager that follows the project's bootstrap pattern.
|
||||
/// - Singleton instance
|
||||
/// - Registers a post-boot init action with BootCompletionService
|
||||
/// - Exposes simple async Save/Load methods (PlayerPrefs-backed placeholder)
|
||||
/// - Manages participant registration for save/load operations
|
||||
/// - Exposes simple async Save/Load methods
|
||||
/// - Fires events on completion
|
||||
/// This is intended as boilerplate to be expanded with a real serialization backend.
|
||||
/// </summary>
|
||||
public class SaveLoadManager : MonoBehaviour
|
||||
{
|
||||
@@ -23,51 +26,318 @@ namespace Core.SaveLoad
|
||||
private static string DefaultSaveFolder => Path.Combine(Application.persistentDataPath, "GameSaves");
|
||||
public SaveLoadData currentSaveData;
|
||||
|
||||
// Participant registry
|
||||
private readonly Dictionary<string, ISaveParticipant> participants = new Dictionary<string, ISaveParticipant>();
|
||||
|
||||
// Pending participants (registered during restoration)
|
||||
private readonly List<ISaveParticipant> pendingParticipants = new List<ISaveParticipant>();
|
||||
|
||||
// State
|
||||
public bool IsSaving { get; private set; }
|
||||
public bool IsLoading { get; private set; }
|
||||
public bool IsSaveDataLoaded { get; private set; }
|
||||
public bool IsRestoringState { get; private set; }
|
||||
|
||||
// Events
|
||||
public event Action<string> OnSaveCompleted;
|
||||
public event Action<string> OnLoadCompleted;
|
||||
public event Action OnParticipantStatesRestored;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
_instance = this;
|
||||
IsSaveDataLoaded = false;
|
||||
IsRestoringState = false;
|
||||
BootCompletionService.RegisterInitAction(InitializePostBoot);
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
Load();
|
||||
#if UNITY_EDITOR
|
||||
OnSceneLoadCompleted("RestoreInEditor");
|
||||
#endif
|
||||
if (DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().useSaveLoadSystem)
|
||||
{
|
||||
Load();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnApplicationQuit()
|
||||
{
|
||||
Save();
|
||||
if (DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().useSaveLoadSystem)
|
||||
{
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializePostBoot()
|
||||
{
|
||||
Logging.Debug("[SaveLoadManager] Post-boot initialization complete");
|
||||
|
||||
// Subscribe to scene lifecycle events if SceneManagerService is available
|
||||
if (SceneManagerService.Instance != null)
|
||||
{
|
||||
SceneManagerService.Instance.SceneLoadCompleted += OnSceneLoadCompleted;
|
||||
SceneManagerService.Instance.SceneUnloadStarted += OnSceneUnloadStarted;
|
||||
Logging.Debug("[SaveLoadManager] Subscribed to SceneManagerService events");
|
||||
}
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (_instance == this)
|
||||
{
|
||||
// Unsubscribe from scene events
|
||||
if (SceneManagerService.Instance != null)
|
||||
{
|
||||
SceneManagerService.Instance.SceneLoadCompleted -= OnSceneLoadCompleted;
|
||||
SceneManagerService.Instance.SceneUnloadStarted -= OnSceneUnloadStarted;
|
||||
}
|
||||
|
||||
_instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
#region Participant Registration
|
||||
|
||||
/// <summary>
|
||||
/// Registers a participant with the save/load system.
|
||||
/// Should be called by participants during their initialization (post-boot).
|
||||
/// </summary>
|
||||
public void RegisterParticipant(ISaveParticipant participant)
|
||||
{
|
||||
if (participant == null)
|
||||
{
|
||||
Logging.Warning("[SaveLoadManager] Attempted to register null participant");
|
||||
return;
|
||||
}
|
||||
|
||||
string saveId = participant.GetSaveId();
|
||||
if (string.IsNullOrEmpty(saveId))
|
||||
{
|
||||
Logging.Warning("[SaveLoadManager] Participant provided null or empty save ID");
|
||||
return;
|
||||
}
|
||||
|
||||
if (participants.ContainsKey(saveId))
|
||||
{
|
||||
Logging.Warning($"[SaveLoadManager] Participant with ID '{saveId}' is already registered. Overwriting.");
|
||||
}
|
||||
|
||||
participants[saveId] = participant;
|
||||
Logging.Debug($"[SaveLoadManager] Registered participant: {saveId}");
|
||||
|
||||
// If we have save data loaded and the participant hasn't been restored yet
|
||||
if (IsSaveDataLoaded && currentSaveData != null && !participant.HasBeenRestored)
|
||||
{
|
||||
if (IsRestoringState)
|
||||
{
|
||||
// We're currently restoring - queue this participant for later restoration
|
||||
pendingParticipants.Add(participant);
|
||||
Logging.Debug($"[SaveLoadManager] Queued participant for pending restoration: {saveId}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not currently restoring - restore this participant's state immediately
|
||||
RestoreParticipantState(participant);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters a participant from the save/load system.
|
||||
/// Should be called by participants during their destruction.
|
||||
/// </summary>
|
||||
public void UnregisterParticipant(string saveId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(saveId))
|
||||
{
|
||||
Logging.Warning("[SaveLoadManager] Attempted to unregister with null or empty save ID");
|
||||
return;
|
||||
}
|
||||
|
||||
if (participants.Remove(saveId))
|
||||
{
|
||||
Logging.Debug($"[SaveLoadManager] Unregistered participant: {saveId}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a registered participant by their save ID. Returns null if not found.
|
||||
/// </summary>
|
||||
public ISaveParticipant GetParticipant(string saveId)
|
||||
{
|
||||
participants.TryGetValue(saveId, out var participant);
|
||||
return participant;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Scene Lifecycle
|
||||
|
||||
private void OnSceneLoadCompleted(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");
|
||||
}
|
||||
|
||||
private void OnSceneUnloadStarted(string sceneName)
|
||||
{
|
||||
Logging.Debug($"[SaveLoadManager] Scene '{sceneName}' unloading. Note: Participants should unregister themselves.");
|
||||
|
||||
// We don't force-clear here because participants should manage their own lifecycle
|
||||
// This allows for proper cleanup in OnDestroy
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region State Restoration
|
||||
|
||||
/// <summary>
|
||||
/// Restores state for a single participant from the current save data.
|
||||
/// </summary>
|
||||
private void RestoreParticipantState(ISaveParticipant participant)
|
||||
{
|
||||
if (currentSaveData == null || currentSaveData.participantStates == null)
|
||||
return;
|
||||
|
||||
string saveId = participant.GetSaveId();
|
||||
|
||||
// Find the participant state in the list
|
||||
var entry = currentSaveData.participantStates.Find(e => e.saveId == saveId);
|
||||
if (entry != null && !string.IsNullOrEmpty(entry.serializedState))
|
||||
{
|
||||
try
|
||||
{
|
||||
participant.RestoreState(entry.serializedState);
|
||||
Logging.Debug($"[SaveLoadManager] Restored state for participant: {saveId}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.Warning($"[SaveLoadManager] Exception while restoring state for '{saveId}': {ex}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logging.Debug($"[SaveLoadManager] No saved state found for participant: {saveId}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores state for all currently registered participants.
|
||||
/// Called after loading save data.
|
||||
/// Uses pending queue to handle participants that register during restoration.
|
||||
/// </summary>
|
||||
private void RestoreAllParticipantStates()
|
||||
{
|
||||
if (currentSaveData == null || currentSaveData.participantStates == null)
|
||||
return;
|
||||
|
||||
IsRestoringState = true;
|
||||
int restoredCount = 0;
|
||||
|
||||
// Clear pending queue at the start
|
||||
pendingParticipants.Clear();
|
||||
|
||||
// Create a snapshot to avoid collection modification during iteration
|
||||
// (RestoreState can trigger GameObject activation which can register new participants)
|
||||
foreach (var kvp in participants.ToList())
|
||||
{
|
||||
string saveId = kvp.Key;
|
||||
ISaveParticipant participant = kvp.Value;
|
||||
|
||||
// Find the participant state in the list
|
||||
var entry = currentSaveData.participantStates.Find(e => e.saveId == saveId);
|
||||
if (entry != null && !string.IsNullOrEmpty(entry.serializedState))
|
||||
{
|
||||
try
|
||||
{
|
||||
participant.RestoreState(entry.serializedState);
|
||||
restoredCount++;
|
||||
Logging.Debug($"[SaveLoadManager] Restored state for participant: {saveId}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.Warning($"[SaveLoadManager] Exception while restoring state for '{saveId}': {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process pending participants that registered during the main restoration loop
|
||||
const int maxPendingPasses = 10;
|
||||
int pendingPass = 0;
|
||||
int totalPendingRestored = 0;
|
||||
|
||||
while (pendingParticipants.Count > 0 && pendingPass < maxPendingPasses)
|
||||
{
|
||||
pendingPass++;
|
||||
|
||||
// Take snapshot of current pending list and clear the main list
|
||||
// (restoring pending participants might add more pending participants)
|
||||
var currentPending = new List<ISaveParticipant>(pendingParticipants);
|
||||
pendingParticipants.Clear();
|
||||
|
||||
int passRestored = 0;
|
||||
foreach (var participant in currentPending)
|
||||
{
|
||||
try
|
||||
{
|
||||
RestoreParticipantState(participant);
|
||||
passRestored++;
|
||||
totalPendingRestored++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.Warning($"[SaveLoadManager] Exception while restoring pending participant: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
Logging.Debug($"[SaveLoadManager] Pending pass {pendingPass}: Restored {passRestored} participants");
|
||||
}
|
||||
|
||||
if (pendingParticipants.Count > 0)
|
||||
{
|
||||
Logging.Warning($"[SaveLoadManager] Reached maximum pending passes ({maxPendingPasses}). {pendingParticipants.Count} participants remain unrestored.");
|
||||
}
|
||||
|
||||
// Final cleanup
|
||||
pendingParticipants.Clear();
|
||||
IsRestoringState = false;
|
||||
|
||||
Logging.Debug($"[SaveLoadManager] Restored state for {restoredCount} participants + {totalPendingRestored} pending participants");
|
||||
OnParticipantStatesRestored?.Invoke();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private string GetFilePath(string slot)
|
||||
{
|
||||
return Path.Combine(DefaultSaveFolder, $"save_{slot}.json");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Entry point to save to a named slot. Starts an async coroutine that writes to PlayerPrefs
|
||||
/// (placeholder behavior). Fires OnSaveCompleted when finished.
|
||||
/// Entry point to save to a named slot. Starts an async coroutine that writes to disk.
|
||||
/// Fires OnSaveCompleted when finished.
|
||||
/// </summary>
|
||||
public void Save(string slot = "default")
|
||||
{
|
||||
@@ -81,8 +351,8 @@ namespace Core.SaveLoad
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Entry point to load from a named slot. Starts an async coroutine that reads from PlayerPrefs
|
||||
/// (placeholder behavior). Fires OnLoadCompleted when finished.
|
||||
/// Entry point to load from a named slot. Starts an async coroutine that reads from disk.
|
||||
/// Fires OnLoadCompleted when finished.
|
||||
/// </summary>
|
||||
public void Load(string slot = "default")
|
||||
{
|
||||
@@ -95,7 +365,6 @@ namespace Core.SaveLoad
|
||||
StartCoroutine(LoadAsync(slot));
|
||||
}
|
||||
|
||||
// TODO: This is stupid overkill, but over verbose logging is king for now
|
||||
private IEnumerator SaveAsync(string slot)
|
||||
{
|
||||
Logging.Debug($"[SaveLoadManager] Starting save for slot '{slot}'");
|
||||
@@ -106,7 +375,7 @@ namespace Core.SaveLoad
|
||||
string json = null;
|
||||
bool prepFailed = false;
|
||||
|
||||
// Prep phase: ensure folder exists and serialize (no yields allowed inside try/catch)
|
||||
// Prep phase: ensure folder exists and serialize
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(DefaultSaveFolder);
|
||||
@@ -117,11 +386,43 @@ namespace Core.SaveLoad
|
||||
currentSaveData = new SaveLoadData();
|
||||
}
|
||||
|
||||
// Pull latest card collection snapshot from CardSystem before serializing (don't overwrite with null)
|
||||
if (Data.CardSystem.CardSystemManager.Instance != null)
|
||||
// Ensure participantStates list exists
|
||||
if (currentSaveData.participantStates == null)
|
||||
{
|
||||
currentSaveData.cardCollection = Data.CardSystem.CardSystemManager.Instance.ExportCardCollectionState();
|
||||
currentSaveData.participantStates = new List<ParticipantStateEntry>();
|
||||
}
|
||||
else
|
||||
{
|
||||
currentSaveData.participantStates.Clear();
|
||||
}
|
||||
|
||||
// Capture state from all registered participants directly into the list
|
||||
// Create a snapshot to avoid collection modification during iteration
|
||||
int savedCount = 0;
|
||||
foreach (var kvp in participants.ToList())
|
||||
{
|
||||
string saveId = kvp.Key;
|
||||
ISaveParticipant participant = kvp.Value;
|
||||
|
||||
try
|
||||
{
|
||||
string serializedState = participant.SerializeState();
|
||||
currentSaveData.participantStates.Add(new ParticipantStateEntry
|
||||
{
|
||||
saveId = saveId,
|
||||
serializedState = serializedState
|
||||
});
|
||||
savedCount++;
|
||||
Logging.Debug($"[SaveLoadManager] Captured state for participant: {saveId}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logging.Warning($"[SaveLoadManager] Exception while serializing participant '{saveId}': {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
Logging.Debug($"[SaveLoadManager] Captured state from {savedCount} participants");
|
||||
|
||||
|
||||
json = JsonUtility.ToJson(currentSaveData, true);
|
||||
}
|
||||
@@ -133,7 +434,6 @@ namespace Core.SaveLoad
|
||||
|
||||
if (prepFailed || string.IsNullOrEmpty(json))
|
||||
{
|
||||
// Ensure we clean up state and notify listeners outside of the try/catch
|
||||
IsSaving = false;
|
||||
OnSaveCompleted?.Invoke(slot);
|
||||
Logging.Warning($"[SaveLoadManager] Aborting save for slot '{slot}' due to prep failure");
|
||||
@@ -172,7 +472,6 @@ namespace Core.SaveLoad
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: This is stupid overkill, but over verbose logging is king for now
|
||||
private IEnumerator LoadAsync(string slot)
|
||||
{
|
||||
Logging.Debug($"[SaveLoadManager] Starting load for slot '{slot}'");
|
||||
@@ -185,16 +484,18 @@ namespace Core.SaveLoad
|
||||
Logging.Debug($"[SaveLoadManager] No save found at '{path}', creating defaults");
|
||||
currentSaveData = new SaveLoadData();
|
||||
|
||||
// Simulate async operation and finish
|
||||
yield return null;
|
||||
|
||||
IsLoading = false;
|
||||
IsSaveDataLoaded = true;
|
||||
|
||||
// Restore any already-registered participants (e.g., those initialized during boot)
|
||||
RestoreAllParticipantStates();
|
||||
|
||||
OnLoadCompleted?.Invoke(slot);
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Simulate async operation (optional)
|
||||
yield return null;
|
||||
|
||||
try
|
||||
@@ -208,7 +509,6 @@ namespace Core.SaveLoad
|
||||
}
|
||||
else
|
||||
{
|
||||
// Attempt to deserialize; if it fails or returns null, fall back to defaults
|
||||
var loaded = JsonUtility.FromJson<SaveLoadData>(json);
|
||||
if (loaded == null)
|
||||
{
|
||||
@@ -218,6 +518,12 @@ namespace Core.SaveLoad
|
||||
else
|
||||
{
|
||||
currentSaveData = loaded;
|
||||
|
||||
// Ensure participantStates list exists even if not in save file
|
||||
if (currentSaveData.participantStates == null)
|
||||
{
|
||||
currentSaveData.participantStates = new List<ParticipantStateEntry>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,6 +536,10 @@ namespace Core.SaveLoad
|
||||
{
|
||||
IsLoading = false;
|
||||
IsSaveDataLoaded = true;
|
||||
|
||||
// Restore state for any already-registered participants
|
||||
RestoreAllParticipantStates();
|
||||
|
||||
OnLoadCompleted?.Invoke(slot);
|
||||
Logging.Debug($"[SaveLoadManager] Load completed for slot '{slot}'");
|
||||
}
|
||||
|
||||
@@ -28,6 +28,10 @@ namespace AppleHills.Core.Settings
|
||||
[Tooltip("Should Time.timeScale be set to 0 when the game is paused")]
|
||||
[SerializeField] public bool pauseTimeOnPauseGame = true;
|
||||
|
||||
[Header("Save Load Options")]
|
||||
[Tooltip("Should use save laod system?")]
|
||||
[SerializeField] public bool useSaveLoadSystem = true;
|
||||
|
||||
[Header("Logging Options")]
|
||||
[Tooltip("Logging level for bootstrap services")]
|
||||
[SerializeField] public LogVerbosity bootstrapLogVerbosity = LogVerbosity.Warning;
|
||||
|
||||
Reference in New Issue
Block a user