Files
AppleHillsProduction/Assets/Scripts/Interactions/SaveableInteractable.cs
2025-11-02 12:48:48 +01:00

262 lines
8.2 KiB
C#

using Core.SaveLoad;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace Interactions
{
/// <summary>
/// Base class for interactables that participate in the save/load system.
/// Provides common save ID generation and serialization infrastructure.
/// </summary>
public abstract class SaveableInteractable : InteractableBase, ISaveParticipant
{
[Header("Save System")]
[SerializeField]
[Tooltip("Optional custom save ID. If empty, will auto-generate from hierarchy path.")]
private string customSaveId = "";
/// <summary>
/// Flag to indicate we're currently restoring from save data.
/// Child classes can check this to skip initialization logic during load.
/// </summary>
protected bool IsRestoringFromSave { get; private set; }
private bool hasRegistered;
private bool hasRestoredState;
/// <summary>
/// Returns true if this participant has already had its state restored.
/// </summary>
public bool HasBeenRestored => hasRestoredState;
protected virtual void Awake()
{
// Register early in Awake so even disabled objects are tracked
RegisterWithSaveSystem();
}
protected virtual void Start()
{
// If we didn't register in Awake (shouldn't happen), register now
if (!hasRegistered)
{
RegisterWithSaveSystem();
}
}
protected virtual void OnDestroy()
{
UnregisterFromSaveSystem();
}
private void RegisterWithSaveSystem()
{
if (hasRegistered) return;
if (SaveLoadManager.Instance != null)
{
SaveLoadManager.Instance.RegisterParticipant(this);
hasRegistered = true;
// Check if save data was already loaded before we registered
// If so, we need to subscribe to the next load event
if (!SaveLoadManager.Instance.IsSaveDataLoaded && !hasRestoredState)
{
SaveLoadManager.Instance.OnLoadCompleted += OnSaveDataLoadedHandler;
}
}
else
{
Debug.LogWarning($"[SaveableInteractable] SaveLoadManager not found for {gameObject.name}");
}
}
private void UnregisterFromSaveSystem()
{
if (!hasRegistered) return;
if (SaveLoadManager.Instance != null)
{
SaveLoadManager.Instance.UnregisterParticipant(GetSaveId());
SaveLoadManager.Instance.OnLoadCompleted -= OnSaveDataLoadedHandler;
hasRegistered = false;
}
}
/// <summary>
/// Event handler for when save data finishes loading.
/// Called if the object registered before save data was loaded.
/// </summary>
private void OnSaveDataLoadedHandler(string slot)
{
// The SaveLoadManager will automatically call RestoreState on us
// We just need to unsubscribe from the event
if (SaveLoadManager.Instance != null)
{
SaveLoadManager.Instance.OnLoadCompleted -= OnSaveDataLoadedHandler;
}
}
#region ISaveParticipant Implementation
public string GetSaveId()
{
string sceneName = GetSceneName();
if (!string.IsNullOrEmpty(customSaveId))
{
return $"{sceneName}/{customSaveId}";
}
// Auto-generate from hierarchy path
string hierarchyPath = GetHierarchyPath();
return $"{sceneName}/{hierarchyPath}";
}
public string SerializeState()
{
object stateData = GetSerializableState();
if (stateData == null)
{
return "{}";
}
return JsonUtility.ToJson(stateData);
}
public void RestoreState(string serializedData)
{
if (string.IsNullOrEmpty(serializedData))
{
Debug.LogWarning($"[SaveableInteractable] Empty save data for {GetSaveId()}");
return;
}
// CRITICAL: Only restore state if we're actually in a restoration context
// This prevents state machines from teleporting objects when they enable them mid-gameplay
if (SaveLoadManager.Instance != null && !SaveLoadManager.Instance.IsRestoringState)
{
// If we're not in an active restoration cycle, this is probably a late registration
// (object was disabled during initial load and just got enabled)
// Skip restoration to avoid mid-gameplay teleportation
Debug.Log($"[SaveableInteractable] Skipping late restoration for {GetSaveId()} - object enabled after initial load");
hasRestoredState = true; // Mark as restored to prevent future attempts
return;
}
IsRestoringFromSave = true;
hasRestoredState = true;
try
{
ApplySerializableState(serializedData);
}
catch (System.Exception e)
{
Debug.LogError($"[SaveableInteractable] Failed to restore state for {GetSaveId()}: {e.Message}");
}
finally
{
IsRestoringFromSave = false;
}
}
#endregion
#region Virtual Methods for Child Classes
/// <summary>
/// Child classes override this to return their serializable state data.
/// Return an object that can be serialized with JsonUtility.
/// </summary>
protected abstract object GetSerializableState();
/// <summary>
/// Child classes override this to apply restored state data.
/// Should NOT trigger events or re-initialize logic that already happened.
/// </summary>
/// <param name="serializedData">JSON string containing the saved state</param>
protected abstract void ApplySerializableState(string serializedData);
#endregion
#region Helper Methods
private string GetSceneName()
{
Scene scene = gameObject.scene;
if (!scene.IsValid())
{
Debug.LogWarning($"[SaveableInteractable] GameObject {gameObject.name} has invalid scene");
return "UnknownScene";
}
return scene.name;
}
private string GetHierarchyPath()
{
// Build path from scene root to this object
// Format: ParentName/ChildName/ObjectName_SiblingIndex
string path = gameObject.name;
Transform current = transform.parent;
while (current != null)
{
path = $"{current.name}/{path}";
current = current.parent;
}
// Add sibling index for uniqueness among same-named objects
int siblingIndex = transform.GetSiblingIndex();
if (siblingIndex > 0)
{
path = $"{path}_{siblingIndex}";
}
return path;
}
#endregion
#region Editor Helpers
#if UNITY_EDITOR
[ContextMenu("Log Save ID")]
private void LogSaveId()
{
Debug.Log($"Save ID: {GetSaveId()}");
}
[ContextMenu("Test Serialize/Deserialize")]
private void TestSerializeDeserialize()
{
string serialized = SerializeState();
Debug.Log($"Serialized state: {serialized}");
RestoreState(serialized);
Debug.Log("Deserialization test complete");
}
#endif
#endregion
}
#region Common Save Data Structures
/// <summary>
/// Base save data for all interactables.
/// Can be extended by child classes.
/// </summary>
[System.Serializable]
public class InteractableBaseSaveData
{
public bool isActive;
public Vector3 worldPosition;
public Quaternion worldRotation;
}
#endregion
}