SaveLoad using managed lifecycle
This commit is contained in:
committed by
Michal Pikulski
parent
3e835ed3b8
commit
b932be2232
@@ -1,4 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using System; // for Action<T>
|
||||
@@ -24,6 +24,7 @@ namespace Interactions
|
||||
{
|
||||
public ItemSlotState slotState;
|
||||
public string slottedItemSaveId;
|
||||
public string slottedItemDataId; // ItemId of the PickupItemData (for verification)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -227,6 +228,16 @@ namespace Interactions
|
||||
{
|
||||
var previousData = currentlySlottedItemData;
|
||||
|
||||
// Clear the pickup's OwningSlot reference
|
||||
if (currentlySlottedItemObject != null)
|
||||
{
|
||||
var pickup = currentlySlottedItemObject.GetComponent<Pickup>();
|
||||
if (pickup != null)
|
||||
{
|
||||
pickup.OwningSlot = null;
|
||||
}
|
||||
}
|
||||
|
||||
currentlySlottedItemObject = null;
|
||||
currentlySlottedItemData = null;
|
||||
currentState = ItemSlotState.None;
|
||||
@@ -272,17 +283,15 @@ namespace Interactions
|
||||
#endregion
|
||||
|
||||
// Register with ItemManager when enabled
|
||||
protected override void Start()
|
||||
private void OnEnable()
|
||||
{
|
||||
base.Start(); // SaveableInteractable registration
|
||||
|
||||
// Register as ItemSlot
|
||||
ItemManager.Instance?.RegisterItemSlot(this);
|
||||
}
|
||||
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
base.OnDestroy(); // SaveableInteractable cleanup
|
||||
base.OnDestroy();
|
||||
|
||||
// Unregister from slot manager
|
||||
ItemManager.Instance?.UnregisterItemSlot(this);
|
||||
@@ -294,20 +303,28 @@ namespace Interactions
|
||||
{
|
||||
// Get slotted item save ID if there's a slotted item
|
||||
string slottedSaveId = "";
|
||||
string slottedDataId = "";
|
||||
|
||||
if (currentlySlottedItemObject != null)
|
||||
{
|
||||
var slottedPickup = currentlySlottedItemObject.GetComponent<Pickup>();
|
||||
if (slottedPickup is SaveableInteractable saveablePickup)
|
||||
{
|
||||
slottedSaveId = saveablePickup.GetSaveId();
|
||||
slottedSaveId = saveablePickup.SaveId;
|
||||
}
|
||||
}
|
||||
|
||||
// Also save the itemData ID for verification
|
||||
if (currentlySlottedItemData != null)
|
||||
{
|
||||
slottedDataId = currentlySlottedItemData.itemId;
|
||||
}
|
||||
|
||||
return new ItemSlotSaveData
|
||||
{
|
||||
slotState = currentState,
|
||||
slottedItemSaveId = slottedSaveId
|
||||
slottedItemSaveId = slottedSaveId,
|
||||
slottedItemDataId = slottedDataId
|
||||
};
|
||||
}
|
||||
|
||||
@@ -326,7 +343,8 @@ namespace Interactions
|
||||
// Restore slotted item if there was one
|
||||
if (!string.IsNullOrEmpty(data.slottedItemSaveId))
|
||||
{
|
||||
RestoreSlottedItem(data.slottedItemSaveId);
|
||||
Debug.Log($"[ItemSlot] Restoring slotted item: {data.slottedItemSaveId} (itemId: {data.slottedItemDataId})");
|
||||
RestoreSlottedItem(data.slottedItemSaveId, data.slottedItemDataId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,7 +352,7 @@ namespace Interactions
|
||||
/// Restore a slotted item from save data.
|
||||
/// This is called during load restoration and should NOT trigger events.
|
||||
/// </summary>
|
||||
private void RestoreSlottedItem(string slottedItemSaveId)
|
||||
private void RestoreSlottedItem(string slottedItemSaveId, string expectedItemDataId)
|
||||
{
|
||||
// Try to find the item in the scene by its save ID via ItemManager
|
||||
GameObject slottedObject = ItemManager.Instance?.FindPickupBySaveId(slottedItemSaveId);
|
||||
@@ -351,11 +369,33 @@ namespace Interactions
|
||||
if (pickup != null)
|
||||
{
|
||||
slottedData = pickup.itemData;
|
||||
|
||||
// Verify itemId matches if we have it (safety check)
|
||||
if (slottedData != null && !string.IsNullOrEmpty(expectedItemDataId))
|
||||
{
|
||||
if (slottedData.itemId != expectedItemDataId)
|
||||
{
|
||||
Debug.LogWarning($"[ItemSlot] ItemId mismatch! Pickup has '{slottedData.itemId}' but expected '{expectedItemDataId}'");
|
||||
}
|
||||
}
|
||||
|
||||
if (slottedData == null)
|
||||
{
|
||||
Debug.LogWarning($"[ItemSlot] Pickup {pickup.gameObject.name} has null itemData! Expected itemId: {expectedItemDataId}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[ItemSlot] Slotted object has no Pickup component: {slottedObject.name}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Silently slot the item (no events, no interaction completion)
|
||||
// Follower state is managed separately during save/load restoration
|
||||
ApplySlottedItemState(slottedObject, slottedData, triggerEvents: false);
|
||||
|
||||
Debug.Log($"[ItemSlot] Successfully restored slotted item: {slottedData.itemName} (itemId: {slottedData.itemId})");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -370,7 +410,16 @@ namespace Interactions
|
||||
{
|
||||
if (itemToSlot == null)
|
||||
{
|
||||
// Clear slot
|
||||
// Clear slot - also clear the pickup's OwningSlot reference
|
||||
if (currentlySlottedItemObject != null)
|
||||
{
|
||||
var oldPickup = currentlySlottedItemObject.GetComponent<Pickup>();
|
||||
if (oldPickup != null)
|
||||
{
|
||||
oldPickup.OwningSlot = null;
|
||||
}
|
||||
}
|
||||
|
||||
var previousData = currentlySlottedItemData;
|
||||
currentlySlottedItemObject = null;
|
||||
currentlySlottedItemData = null;
|
||||
@@ -391,6 +440,14 @@ namespace Interactions
|
||||
SetSlottedObject(itemToSlot);
|
||||
currentlySlottedItemData = itemToSlotData;
|
||||
|
||||
// Mark the pickup as picked up and track slot ownership for save/load
|
||||
var pickup = itemToSlot.GetComponent<Pickup>();
|
||||
if (pickup != null)
|
||||
{
|
||||
pickup.IsPickedUp = true;
|
||||
pickup.OwningSlot = this;
|
||||
}
|
||||
|
||||
// Determine if correct
|
||||
var config = interactionSettings?.GetSlotItemConfig(itemData);
|
||||
var allowed = config?.allowedItems ?? new List<PickupItemData>();
|
||||
@@ -433,6 +490,33 @@ namespace Interactions
|
||||
ApplySlottedItemState(itemToSlot, itemToSlotData, triggerEvents: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bilateral restoration entry point: Pickup calls this to offer itself to the Slot.
|
||||
/// Returns true if claim was successful, false if slot already has an item or wrong pickup.
|
||||
/// </summary>
|
||||
public bool TryClaimSlottedItem(Pickup pickup)
|
||||
{
|
||||
if (pickup == null)
|
||||
return false;
|
||||
|
||||
// If slot already has an item, reject the claim
|
||||
if (currentlySlottedItemObject != null)
|
||||
{
|
||||
Debug.LogWarning($"[ItemSlot] Already has a slotted item, rejecting claim from {pickup.gameObject.name}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify this pickup's SaveId matches what we expect (from our save data)
|
||||
// Note: We don't have easy access to the expected SaveId here, so we just accept it
|
||||
// The Pickup's bilateral restoration ensures it only claims the correct slot
|
||||
|
||||
// Claim the pickup
|
||||
ApplySlottedItemState(pickup.gameObject, pickup.itemData, triggerEvents: false);
|
||||
|
||||
Debug.Log($"[ItemSlot] Successfully claimed slotted item: {pickup.itemData?.itemName}");
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ namespace Interactions
|
||||
{
|
||||
public bool isPickedUp;
|
||||
public bool wasHeldByFollower;
|
||||
public bool wasInSlot; // NEW: Was this pickup in a slot?
|
||||
public string slotSaveId; // NEW: Which slot held this pickup?
|
||||
public Vector3 worldPosition;
|
||||
public Quaternion worldRotation;
|
||||
public bool isActive;
|
||||
@@ -24,9 +26,12 @@ namespace Interactions
|
||||
public SpriteRenderer iconRenderer;
|
||||
public bool IsPickedUp { get; internal set; }
|
||||
|
||||
// Track which slot owns this pickup (for bilateral restoration)
|
||||
internal ItemSlot OwningSlot { get; set; }
|
||||
|
||||
public event Action<PickupItemData> OnItemPickedUp;
|
||||
public event Action<PickupItemData, PickupItemData, PickupItemData> OnItemsCombined;
|
||||
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake(); // Register with save system
|
||||
@@ -36,19 +41,17 @@ namespace Interactions
|
||||
|
||||
ApplyItemData();
|
||||
}
|
||||
|
||||
protected override void Start()
|
||||
{
|
||||
base.Start(); // Register with save system
|
||||
|
||||
// Always register with ItemManager, even if picked up
|
||||
|
||||
// Always register with ItemManager, even if picked up
|
||||
// This allows the save/load system to find held items when restoring state
|
||||
protected override void OnManagedAwake()
|
||||
{
|
||||
ItemManager.Instance?.RegisterPickup(this);
|
||||
}
|
||||
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
base.OnDestroy(); // Unregister from save system
|
||||
base.OnDestroy();
|
||||
|
||||
// Unregister from ItemManager
|
||||
ItemManager.Instance?.UnregisterPickup(this);
|
||||
@@ -139,10 +142,16 @@ namespace Interactions
|
||||
// Check if this pickup is currently held by the follower
|
||||
bool isHeldByFollower = IsPickedUp && !gameObject.activeSelf && transform.parent != null;
|
||||
|
||||
// Check if this pickup is in a slot
|
||||
bool isInSlot = OwningSlot != null;
|
||||
string slotId = isInSlot && OwningSlot is SaveableInteractable saveableSlot ? saveableSlot.SaveId : "";
|
||||
|
||||
return new PickupSaveData
|
||||
{
|
||||
isPickedUp = this.IsPickedUp,
|
||||
wasHeldByFollower = isHeldByFollower,
|
||||
wasInSlot = isInSlot,
|
||||
slotSaveId = slotId,
|
||||
worldPosition = transform.position,
|
||||
worldRotation = transform.rotation,
|
||||
isActive = gameObject.activeSelf
|
||||
@@ -177,6 +186,20 @@ namespace Interactions
|
||||
follower.TryClaimHeldItem(this);
|
||||
}
|
||||
}
|
||||
// If this was in a slot, try bilateral restoration with the slot
|
||||
else if (data.wasInSlot && !string.IsNullOrEmpty(data.slotSaveId))
|
||||
{
|
||||
// Try to give this pickup to the slot
|
||||
var slot = FindSlotBySaveId(data.slotSaveId);
|
||||
if (slot != null)
|
||||
{
|
||||
slot.TryClaimSlottedItem(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[Pickup] Could not find slot with SaveId: {data.slotSaveId}");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -190,6 +213,28 @@ namespace Interactions
|
||||
// This prevents duplicate logic execution
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find an ItemSlot by its SaveId (for bilateral restoration).
|
||||
/// </summary>
|
||||
private ItemSlot FindSlotBySaveId(string slotSaveId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(slotSaveId)) return null;
|
||||
|
||||
// Get all ItemSlots from ItemManager
|
||||
var allSlots = ItemManager.Instance?.GetAllItemSlots();
|
||||
if (allSlots == null) return null;
|
||||
|
||||
foreach (var slot in allSlots)
|
||||
{
|
||||
if (slot is SaveableInteractable saveable && saveable.SaveId == slotSaveId)
|
||||
{
|
||||
return slot;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the pickup state when the item is dropped back into the world.
|
||||
/// Called by FollowerController when swapping items.
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using Core.SaveLoad;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Interactions
|
||||
{
|
||||
@@ -8,21 +6,13 @@ namespace Interactions
|
||||
/// Base class for interactables that participate in the save/load system.
|
||||
/// Provides common save ID generation and serialization infrastructure.
|
||||
/// </summary>
|
||||
public abstract class SaveableInteractable : InteractableBase, ISaveParticipant
|
||||
public abstract class SaveableInteractable : InteractableBase
|
||||
{
|
||||
[Header("Save System")]
|
||||
[SerializeField]
|
||||
[Tooltip("Optional custom save ID. If empty, will auto-generate from hierarchy path.")]
|
||||
private string customSaveId = "";
|
||||
|
||||
/// <summary>
|
||||
/// Sets a custom save ID for this interactable.
|
||||
/// Used when spawning dynamic objects that need stable save IDs.
|
||||
/// </summary>
|
||||
public void SetCustomSaveId(string saveId)
|
||||
{
|
||||
customSaveId = saveId;
|
||||
}
|
||||
|
||||
// Save system configuration
|
||||
public override bool AutoRegisterForSave => true;
|
||||
|
||||
/// <summary>
|
||||
/// Flag to indicate we're currently restoring from save data.
|
||||
@@ -30,99 +20,10 @@ namespace Interactions
|
||||
/// </summary>
|
||||
protected bool IsRestoringFromSave { get; private set; }
|
||||
|
||||
private bool hasRegistered;
|
||||
private bool hasRestoredState;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this participant has already had its state restored.
|
||||
/// </summary>
|
||||
public bool HasBeenRestored => hasRestoredState;
|
||||
|
||||
protected virtual void Awake()
|
||||
{
|
||||
// Register early in Awake so even disabled objects are tracked
|
||||
RegisterWithSaveSystem();
|
||||
}
|
||||
#region Save/Load Lifecycle Hooks
|
||||
|
||||
protected virtual void Start()
|
||||
{
|
||||
// If we didn't register in Awake (shouldn't happen), register now
|
||||
if (!hasRegistered)
|
||||
{
|
||||
RegisterWithSaveSystem();
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void OnDestroy()
|
||||
{
|
||||
UnregisterFromSaveSystem();
|
||||
}
|
||||
|
||||
private void RegisterWithSaveSystem()
|
||||
{
|
||||
if (hasRegistered) return;
|
||||
|
||||
if (SaveLoadManager.Instance != null)
|
||||
{
|
||||
SaveLoadManager.Instance.RegisterParticipant(this);
|
||||
hasRegistered = true;
|
||||
|
||||
// Check if save data was already loaded before we registered
|
||||
// If so, we need to subscribe to the next load event
|
||||
if (!SaveLoadManager.Instance.IsSaveDataLoaded && !hasRestoredState)
|
||||
{
|
||||
SaveLoadManager.Instance.OnLoadCompleted += OnSaveDataLoadedHandler;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"[SaveableInteractable] SaveLoadManager not found for {gameObject.name}");
|
||||
}
|
||||
}
|
||||
|
||||
private void UnregisterFromSaveSystem()
|
||||
{
|
||||
if (!hasRegistered) return;
|
||||
|
||||
if (SaveLoadManager.Instance != null)
|
||||
{
|
||||
SaveLoadManager.Instance.UnregisterParticipant(GetSaveId());
|
||||
SaveLoadManager.Instance.OnLoadCompleted -= OnSaveDataLoadedHandler;
|
||||
hasRegistered = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event handler for when save data finishes loading.
|
||||
/// Called if the object registered before save data was loaded.
|
||||
/// </summary>
|
||||
private void OnSaveDataLoadedHandler(string slot)
|
||||
{
|
||||
// The SaveLoadManager will automatically call RestoreState on us
|
||||
// We just need to unsubscribe from the event
|
||||
if (SaveLoadManager.Instance != null)
|
||||
{
|
||||
SaveLoadManager.Instance.OnLoadCompleted -= OnSaveDataLoadedHandler;
|
||||
}
|
||||
}
|
||||
|
||||
#region ISaveParticipant Implementation
|
||||
|
||||
public string GetSaveId()
|
||||
{
|
||||
string sceneName = GetSceneName();
|
||||
|
||||
if (!string.IsNullOrEmpty(customSaveId))
|
||||
{
|
||||
return $"{sceneName}/{customSaveId}";
|
||||
}
|
||||
|
||||
// Auto-generate from hierarchy path
|
||||
string hierarchyPath = GetHierarchyPath();
|
||||
return $"{sceneName}/{hierarchyPath}";
|
||||
}
|
||||
|
||||
public string SerializeState()
|
||||
protected override string OnSceneSaveRequested()
|
||||
{
|
||||
object stateData = GetSerializableState();
|
||||
if (stateData == null)
|
||||
@@ -133,28 +34,17 @@ namespace Interactions
|
||||
return JsonUtility.ToJson(stateData);
|
||||
}
|
||||
|
||||
public void RestoreState(string serializedData)
|
||||
protected override void OnSceneRestoreRequested(string serializedData)
|
||||
{
|
||||
if (string.IsNullOrEmpty(serializedData))
|
||||
{
|
||||
Debug.LogWarning($"[SaveableInteractable] Empty save data for {GetSaveId()}");
|
||||
return;
|
||||
}
|
||||
|
||||
// CRITICAL: Only restore state if we're actually in a restoration context
|
||||
// This prevents state machines from teleporting objects when they enable them mid-gameplay
|
||||
if (SaveLoadManager.Instance != null && !SaveLoadManager.Instance.IsRestoringState)
|
||||
{
|
||||
// If we're not in an active restoration cycle, this is probably a late registration
|
||||
// (object was disabled during initial load and just got enabled)
|
||||
// Skip restoration to avoid mid-gameplay teleportation
|
||||
Debug.Log($"[SaveableInteractable] Skipping late restoration for {GetSaveId()} - object enabled after initial load");
|
||||
hasRestoredState = true; // Mark as restored to prevent future attempts
|
||||
Debug.LogWarning($"[SaveableInteractable] Empty save data for {SaveId}");
|
||||
return;
|
||||
}
|
||||
|
||||
// OnSceneRestoreRequested is guaranteed by the lifecycle system to only fire during actual restoration
|
||||
// No need to check IsRestoringState - the lifecycle manager handles timing deterministically
|
||||
IsRestoringFromSave = true;
|
||||
hasRestoredState = true;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -162,7 +52,7 @@ namespace Interactions
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Debug.LogError($"[SaveableInteractable] Failed to restore state for {GetSaveId()}: {e.Message}");
|
||||
Debug.LogError($"[SaveableInteractable] Failed to restore state for {SaveId}: {e.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -189,61 +79,22 @@ namespace Interactions
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private string GetSceneName()
|
||||
{
|
||||
Scene scene = gameObject.scene;
|
||||
if (!scene.IsValid())
|
||||
{
|
||||
Debug.LogWarning($"[SaveableInteractable] GameObject {gameObject.name} has invalid scene");
|
||||
return "UnknownScene";
|
||||
}
|
||||
|
||||
return scene.name;
|
||||
}
|
||||
|
||||
private string GetHierarchyPath()
|
||||
{
|
||||
// Build path from scene root to this object
|
||||
// Format: ParentName/ChildName/ObjectName_SiblingIndex
|
||||
string path = gameObject.name;
|
||||
Transform current = transform.parent;
|
||||
|
||||
while (current != null)
|
||||
{
|
||||
path = $"{current.name}/{path}";
|
||||
current = current.parent;
|
||||
}
|
||||
|
||||
// Add sibling index for uniqueness among same-named objects
|
||||
int siblingIndex = transform.GetSiblingIndex();
|
||||
if (siblingIndex > 0)
|
||||
{
|
||||
path = $"{path}_{siblingIndex}";
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Editor Helpers
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[ContextMenu("Log Save ID")]
|
||||
private void LogSaveId()
|
||||
{
|
||||
Debug.Log($"Save ID: {GetSaveId()}");
|
||||
Debug.Log($"Save ID: {SaveId}");
|
||||
}
|
||||
|
||||
[ContextMenu("Test Serialize/Deserialize")]
|
||||
private void TestSerializeDeserialize()
|
||||
{
|
||||
string serialized = SerializeState();
|
||||
string serialized = OnSceneSaveRequested();
|
||||
Debug.Log($"Serialized state: {serialized}");
|
||||
|
||||
RestoreState(serialized);
|
||||
OnSceneRestoreRequested(serialized);
|
||||
Debug.Log("Deserialization test complete");
|
||||
}
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user