using Core.SaveLoad; using UnityEngine; using UnityEngine.SceneManagement; namespace Interactions { /// /// Base class for interactables that participate in the save/load system. /// Provides common save ID generation and serialization infrastructure. /// 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 = ""; /// /// Sets a custom save ID for this interactable. /// Used when spawning dynamic objects that need stable save IDs. /// public void SetCustomSaveId(string saveId) { customSaveId = saveId; } /// /// Flag to indicate we're currently restoring from save data. /// Child classes can check this to skip initialization logic during load. /// protected bool IsRestoringFromSave { get; private set; } private bool hasRegistered; private bool hasRestoredState; /// /// Returns true if this participant has already had its state restored. /// 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; } } /// /// Event handler for when save data finishes loading. /// Called if the object registered before save data was loaded. /// 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 /// /// Child classes override this to return their serializable state data. /// Return an object that can be serialized with JsonUtility. /// protected abstract object GetSerializableState(); /// /// Child classes override this to apply restored state data. /// Should NOT trigger events or re-initialize logic that already happened. /// /// JSON string containing the saved state 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 /// /// Base save data for all interactables. /// Can be extended by child classes. /// [System.Serializable] public class InteractableBaseSaveData { public bool isActive; public Vector3 worldPosition; public Quaternion worldRotation; } #endregion }