Files
AppleHillsProduction/docs/Interactables_SaveLoad_Integration_Plan.md
2025-11-02 12:48:48 +01:00

818 lines
28 KiB
Markdown

# Interactables Save/Load Integration Plan
## Overview
This document outlines the complete implementation plan for integrating the Interactable system (InteractableBase and all child classes) into the existing save/load system using the ISaveParticipant pattern.
---
## Current State Analysis
### Interactable Hierarchy
```
InteractableBase (abstract)
├── Pickup
│ └── ItemSlot (extends Pickup)
├── OneClickInteraction
├── LevelSwitch
└── MinigameSwitch
```
### Key Observations
1. **InteractableBase** - Abstract base class with common interaction flow, events, cooldown system
2. **Pickup** - Tracks `isPickedUp` state, handles item pickup/combination logic
3. **ItemSlot** - Extends Pickup, tracks slotted item state (`ItemSlotState`, `_currentlySlottedItemData`, `_currentlySlottedItemObject`)
4. **OneClickInteraction** - Stateless, completes immediately (likely no save state needed)
5. **LevelSwitch** - Appears to be stateless switch behavior (no persistent state)
6. **MinigameSwitch** - **Already partially integrated** with save system (tracks unlock state in `SaveLoadData.unlockedMinigames`)
### State Machine Challenge
- State machines (Pixelplacement's StateMachine) are used in scenes
- Calling `ChangeState()` triggers `OnEnable()` in target state, which can:
- Start animations
- Move player characters
- Enable child GameObjects with Start() calls
- **Problem**: Restoring a state directly would replay all initialization logic
- **Solution**: Need a way to restore state without triggering initialization side effects
---
## Implementation Strategy
### ~~Phase 1: Create Base SaveableInteractable Class~~ ✅ **COMPLETED**
**Goal**: Establish the foundation for saving/loading interactable state
**Phase 1 Completion Summary:**
- ✅ Created `SaveableInteractable` abstract class inheriting from `InteractableBase`
- ✅ Implemented `ISaveParticipant` interface with GetSaveId(), SerializeState(), RestoreState()
- ✅ Added hybrid save ID generation: custom IDs or auto-generated from hierarchy path
- ✅ Implemented registration/unregistration in Start()/OnDestroy()
- ✅ Added `IsRestoringFromSave` protected flag for child classes
- ✅ Created abstract methods `GetSerializableState()` and `ApplySerializableState()` for children
- ✅ Added `InteractableBaseSaveData` structure with position, rotation, and active state
- ✅ Included editor helper methods (Log Save ID, Test Serialize/Deserialize)
- ✅ No compilation errors, only minor style warnings
**Files Created**:
- `Assets/Scripts/Interactions/SaveableInteractable.cs`
---
### ~~Phase 2: Migrate Pickup to SaveableInteractable~~ ✅ **COMPLETED**
**Goal**: Make Pickup save/restore its picked-up state and world position
**Phase 2 Completion Summary:**
- ✅ Changed `Pickup` to extend `SaveableInteractable` instead of `InteractableBase`
- ✅ Created `PickupSaveData` structure with: isPickedUp, worldPosition, worldRotation, isActive
- ✅ Updated Start() to call base.Start() and skip ItemManager registration if already picked up
- ✅ Updated OnDestroy() to call base.OnDestroy() for save system unregistration
- ✅ Implemented `GetSerializableState()` to capture current pickup state
- ✅ Implemented `ApplySerializableState()` to restore state without triggering events
-**Added world position/rotation saving** for items that haven't been picked up (user requirement)
- ✅ Hide GameObject if picked up, restore position/rotation if not picked up
- ✅ No OnItemPickedUp events fired during restoration (prevents duplicate logic)
- ✅ No compilation errors, only style warnings
**Files Modified**:
- `Assets/Scripts/Interactions/Pickup.cs`
---
### ~~Phase 3: Migrate ItemSlot to SaveableInteractable~~ ✅ **COMPLETED**
**Goal**: Make ItemSlot save/restore slotted item state
**State to Save**:
- `_currentState` (ItemSlotState enum) - Current slot validation state
- `_currentlySlottedItemData` (PickupItemData reference) - Which item is slotted
- Base Pickup state (isPickedUp) - Inherited from Pickup
**Serialization Strategy**:
```csharp
[System.Serializable]
public class ItemSlotSaveData
{
public bool isPickedUp; // From Pickup base
public ItemSlotState slotState;
public string slottedItemDataGuid; // Reference to PickupItemData asset
}
```
**Restoration Logic**:
1. Restore base Pickup state (isPickedUp)
**Phase 3 Completion Summary:**
- ✅ Created `ItemSlotSaveData` structure with: pickupData, slotState, slottedItemSaveId, slottedItemDataAssetPath
- ✅ Updated Start() to call base.Start() and additionally register as ItemSlot
- ✅ Updated OnDestroy() to call base.OnDestroy() and unregister from ItemSlot manager
- ✅ Implemented `GetSerializableState()` to capture slot state and slotted item references
- ✅ Implemented `ApplySerializableState()` to restore base pickup state + slot state
-**Created `ApplySlottedItemState()` method** - unified slotting logic with triggerEvents parameter (post-refactor)
-**Created `RestoreSlottedItem()` method** - finds slotted item by save ID and restores it
-**SlotItem() refactored** - now a clean wrapper around ApplySlottedItemState (post-refactor)
- ✅ Reuses base Pickup serialization through inheritance (code reuse achieved!)
- ✅ No compilation errors, only style warnings
**Code Reuse Pattern Identified:**
- ItemSlot calls `base.GetSerializableState()` to get PickupSaveData
- ItemSlot calls `base.ApplySerializableState()` to restore pickup state first
- ApplySlottedItemState() handles both gameplay and restoration with a single code path
- This pattern can be used by future child classes (inheritance-based state composition)
**Files Modified**:
- `Assets/Scripts/Interactions/ItemSlot.cs`
- `Assets/Scripts/Core/ItemManager.cs` (added GetAllPickups/GetAllItemSlots/FindPickupBySaveId methods)
---
### ~~Phase 4: Handle Stateless Interactables~~ ✅ **COMPLETED**
**Goal**: Determine which interactables need saving and migrate MinigameSwitch to participant pattern
**Phase 4 Completion Summary:**
-**OneClickInteraction**: Confirmed stateless, kept inheriting from InteractableBase
-**LevelSwitch**: Confirmed stateless, kept inheriting from InteractableBase
-**MinigameSwitch**: Successfully migrated to SaveableInteractable
- Created `MinigameSwitchSaveData` structure with isUnlocked flag
- Changed inheritance from InteractableBase to SaveableInteractable
- Removed old direct SaveLoadManager.currentSaveData access pattern
- Removed manual event subscription to OnLoadCompleted
- Added `_isUnlocked` private field to track state
- Implemented `GetSerializableState()` and `ApplySerializableState()`
- Updated Start() to check IsRestoringFromSave flag and set initial active state
- Updated HandleAllPuzzlesComplete() to set unlock flag (save happens automatically)
- GameObject.activeSelf now managed by save/load system
- ✅ No compilation errors, only style warnings
**Migration Benefit:**
- MinigameSwitch now uses the same pattern as all other saveable interactables
- No special case code needed in SaveLoadManager
- Cleaner, more maintainable architecture
**Files Modified**:
- `Assets/Scripts/Levels/MinigameSwitch.cs`
---
## Code Quality Improvements (Post Phase 1-4)
### Refactoring Round 1 ✅ **COMPLETED**
**Issues Identified:**
1. **ItemSlot Code Duplication** - SlotItem() and SlotItemSilent() had significant duplicate logic
2. **Poor Method Placement** - FindPickupBySaveId() was in ItemSlot but should be in ItemManager
3. **Bootstrap Timing Issue** - SaveableInteractable didn't handle save data loading before/after registration
**Improvements Implemented:**
#### 1. ItemSlot Refactoring
-**Extracted common logic** into `ApplySlottedItemState(itemToSlot, itemToSlotData, triggerEvents, clearFollowerHeldItem)`
-**Eliminated code duplication** - SlotItem() and restoration both use the same core logic
-**Added triggerEvents parameter** - single method handles both interactive and silent slotting
-**Simplified public API** - `SlotItem()` is now a thin wrapper around `ApplySlottedItemState()`
**Before:**
```csharp
SlotItem() // 60 lines of logic
SlotItemSilent() // 15 lines of duplicated logic
```
**After:**
```csharp
ApplySlottedItemState(triggerEvents) // 85 lines - single source of truth
SlotItem() // 3 lines - wrapper for gameplay
RestoreSlottedItem() // uses ApplySlottedItemState with triggerEvents=false
```
#### 2. ItemManager Enhancement
-**Moved FindPickupBySaveId()** from ItemSlot to ItemManager (proper separation of concerns)
-**Centralized item lookup** - ItemManager is now the single source for finding items by save ID
-**Cleaner architecture** - ItemSlot no longer needs to know about scene searching
**New ItemManager API:**
```csharp
public GameObject FindPickupBySaveId(string saveId)
```
#### 3. SaveableInteractable Bootstrap Fix
-**Added hasRestoredState flag** - tracks whether state has been restored
-**Subscribes to OnLoadCompleted** - handles save data loading after registration
-**Unsubscribes properly** - prevents memory leaks
-**Mirrors MinigameSwitch pattern** - uses same pattern that was working before
**Bootstrap Flow:**
1. SaveableInteractable registers in Start()
2. Checks if save data is already loaded
3. If not loaded yet, subscribes to OnLoadCompleted event
4. When load completes, SaveLoadManager calls RestoreState() automatically
5. Unsubscribes from event to prevent duplicate restoration
**Files Modified:**
- `Assets/Scripts/Interactions/ItemSlot.cs` (refactored slot logic)
- `Assets/Scripts/Core/ItemManager.cs` (added FindPickupBySaveId)
- `Assets/Scripts/Interactions/SaveableInteractable.cs` (fixed bootstrap timing)
---
### Refactoring Round 2 - Critical Fixes ✅ **COMPLETED**
#### Issue 1: Item Swap Not Resetting Pickup State
**Problem:** When picking up Item B while holding Item A, Item A was dropped but remained marked as `isPickedUp = true`, causing it to be hidden on load.
**Solution:**
- ✅ Added `ResetPickupState()` method to Pickup class
- ✅ Changed `isPickedUp` setter from `private` to `internal`
- ✅ Updated `FollowerController.DropItem()` to call `ResetPickupState()`
- ✅ Fixed combination edge case: base items marked as picked up before destruction
**Files Modified:**
- `Assets/Scripts/Interactions/Pickup.cs`
- `Assets/Scripts/Movement/FollowerController.cs`
#### Issue 2: Serialization Field Name Conflict
**Problem:** Unity error - `_isActive` field name serialized multiple times in class hierarchy (InteractableBase → MinigameSwitch/LevelSwitch).
**Solution:**
- ✅ Renamed child class fields to be more specific: `_isActive``switchActive`
- ✅ Updated all references in MinigameSwitch (4 occurrences)
- ✅ Updated all references in LevelSwitch (5 occurrences)
**Files Modified:**
- `Assets/Scripts/Levels/MinigameSwitch.cs`
- `Assets/Scripts/Levels/LevelSwitch.cs`
#### Issue 3: State Machine Initialization Order - CRITICAL 🔥 **FIXED**
**Problem:** Objects controlled by state machines start disabled. When they're enabled mid-gameplay:
1. Object becomes active
2. `Start()` runs → registers with SaveLoadManager
3. SaveLoadManager immediately calls `RestoreState()`
4. Object teleports to saved position **during gameplay**
**Root Cause:** Disabled GameObjects don't run `Awake()`/`Start()` until enabled. Late registration triggers immediate restoration.
**Solution - SaveLoadManager Discovery + Participant Self-Tracking:**
**Key Principles:**
- ✅ SaveableInteractable keeps normal lifecycle (Start/Awake/OnDestroy) unchanged
- ✅ SaveLoadManager actively discovers INACTIVE SaveableInteractables on scene load
- ✅ Each participant tracks if it's been restored via `HasBeenRestored` property
- ✅ Prevent double-restoration using participant's own state (no redundant tracking)
**Implementation:**
1. **ISaveParticipant Interface** - Added HasBeenRestored property:
```csharp
bool HasBeenRestored { get; }
```
2. **SaveableInteractable** - Exposed existing flag:
```csharp
private bool hasRestoredState;
public bool HasBeenRestored => hasRestoredState;
```
3. **SaveLoadManager.OnSceneLoadCompleted()** - Active Discovery:
```csharp
var inactiveSaveables = FindObjectsByType(typeof(SaveableInteractable),
FindObjectsInactive.Include, FindObjectsSortMode.None);
foreach (var obj in inactiveSaveables) {
var saveable = obj as SaveableInteractable;
if (saveable != null && !saveable.gameObject.activeInHierarchy) {
RegisterParticipant(saveable); // Register inactive objects
}
}
```
4. **RegisterParticipant()** - Check Participant's Own State:
```csharp
if (IsSaveDataLoaded && !IsRestoringState && !participant.HasBeenRestored) {
RestoreParticipantState(participant); // Only if not already restored
}
```
**Flow:**
**Initial Scene Load (with inactive objects):**
1. Scene loads
2. Active objects: Start() → RegisterParticipant() → RestoreState() → hasRestoredState = true
3. OnSceneLoadCompleted() fires
4. FindObjectsByType(includeInactive) discovers inactive SaveableInteractables
5. RegisterParticipant(inactive object) → checks HasBeenRestored → false → RestoreState() → hasRestoredState = true
6. All objects restored and tracked ✅
**Mid-Gameplay Object Enablement:**
1. State machine enables GameObject
2. Awake() runs → Start() runs → RegisterParticipant()
3. Check: `participant.HasBeenRestored` → TRUE
4. Skip RestoreParticipantState() ✅
5. **NO TELEPORTATION** ✅
**Files Modified:**
- `Assets/Scripts/Core/SaveLoad/ISaveParticipant.cs` (added HasBeenRestored property)
- `Assets/Scripts/Interactions/SaveableInteractable.cs` (exposed hasRestoredState)
- `Assets/Scripts/Core/SaveLoad/SaveLoadManager.cs` (discovery + participant check)
- `Assets/Scripts/Data/CardSystem/CardSystemManager.cs` (implemented HasBeenRestored)
---
### Phase 5: State Machine Integration Pattern
**Goal**: Provide a safe way for state machines to react to interactable restoration without replaying initialization
**Problem Analysis**:
- State machines like `GardenerBehaviour.stateSwitch(string stateName)` are called via UnityEvents
- Current flow: Pickup interaction → Event → stateSwitch() → ChangeState() → OnEnable() → Animations/Movement
- During load: We want to restore the state WITHOUT triggering animations/movement
**Solution Options**:
#### Option A: State Restoration Flag (Recommended)
1. Add `IsRestoringFromSave` static/singleton flag to SaveLoadManager
2. State machines check this flag in OnEnable():
```csharp
void OnEnable()
{
if (SaveLoadManager.Instance?.IsRestoringState == true)
{
// Silent restoration - set state variables only
return;
}
// Normal initialization with animations/movement
}
```
3. SaveableInteractable sets flag before calling RestoreState(), clears after
**Advantages**:
- Minimal changes to existing state machine code
- Clear separation of concerns
- Easy to understand and debug
**Disadvantages**:
- Requires modifying every State class
- Global flag could cause issues if restoration is async
#### Option B: Separate Restoration Methods
1. State classes implement two entry points:
- `OnEnable()` - Normal initialization with side effects
- `RestoreState()` - Silent state restoration without side effects
2. Add a `StateRestorationHelper` that can restore states silently:
```csharp
public static class StateRestorationHelper
{
public static void RestoreStateSilently(StateMachine sm, string stateName)
{
// Manually set active state without calling OnEnable
}
}
```
**Advantages**:
- More explicit, less global state
- States control their own restoration logic
**Disadvantages**:
- More code duplication
- Harder to implement with existing StateMachine plugin
#### Option C: Save State Machine State Separately
1. Don't tie state machine state to interactable state
2. Create separate `SaveableStateMachine` component
3. State machines save their own current state
4. On load, restore state machine state independently
**Advantages**:
- Clean separation of concerns
- State machines are self-contained
**Disadvantages**:
- More complex save data structure
- Need to coordinate restoration order
**Recommendation**: Use **Option A** with a restoration flag. It's the least invasive and most compatible with the existing Pixelplacement StateMachine plugin.
**Implementation Plan**:
1. Add `IsRestoringState` flag to SaveLoadManager (already exists!)
2. Create helper component `SaveableState` that wraps State classes:
```csharp
public class SaveableState : State
{
protected virtual void OnEnableBase()
{
if (SaveLoadManager.Instance?.IsRestoringState == true)
{
OnRestoreState();
return;
}
OnNormalEnable();
}
protected virtual void OnNormalEnable() { }
protected virtual void OnRestoreState() { }
}
```
3. Migrate existing states to inherit from SaveableState
4. Move initialization logic from OnEnable to OnNormalEnable
5. Add minimal restoration logic to OnRestoreState
**Files to Create**:
- `Assets/Scripts/StateMachines/SaveableState.cs`
**Files to Modify** (examples):
- `Assets/Scripts/StateMachines/Quarry/AnneLise/TakePhotoState.cs`
- Any other State classes that react to interactable events
---
### Phase 6: Unique ID Generation Strategy
**Goal**: Ensure every saveable interactable has a persistent, unique ID across sessions
**Challenge**:
- Scene instance IDs change between sessions
- Need deterministic IDs that survive scene reloads
- Manual GUID assignment is error-prone
**Solution: Hybrid Approach**
#### For Prefab Instances:
- Use prefab path + scene path + sibling index
- Example: `"Quarry/PickupApple_0"` (first apple in scene)
- Stable as long as prefab hierarchy doesn't change
#### For Unique Scene Objects:
- Add `[SerializeField] private string customSaveId`
- Editor tool validates uniqueness within scene
- Example: `"Quarry/UniquePickup_GoldenApple"`
#### Implementation:
```csharp
public abstract class SaveableInteractable : InteractableBase, ISaveParticipant
{
[SerializeField] private string customSaveId = "";
public string GetSaveId()
{
if (!string.IsNullOrEmpty(customSaveId))
{
return $"{GetSceneName()}/{customSaveId}";
}
// Auto-generate from hierarchy
string path = GetHierarchyPath();
return $"{GetSceneName()}/{path}";
}
private string GetHierarchyPath()
{
// Build path from scene root to this object
// Include sibling index for uniqueness
}
}
```
**Editor Tool**:
- Create validation window that scans scene for duplicate IDs
- Auto-generate IDs for objects missing them
- Warn if hierarchy changes break ID stability
**Files to Create**:
- `Assets/Editor/SaveIdValidator.cs`
---
### Phase 7: ItemManager Integration
**Goal**: Handle cases where slotted items need to be spawned/found during restoration
**Current ItemManager Functionality**:
- Registers/unregisters Pickups and ItemSlots
- No explicit save/load support
**Required Additions**:
1. **Item Lookup by Save ID**:
```csharp
public Pickup GetPickupBySaveId(string saveId)
```
2. **Item Reference Serialization**:
```csharp
[System.Serializable]
public class ItemReference
{
public string saveId; // For scene pickups
public string prefabPath; // For prefab items
public string itemDataGuid; // For PickupItemData
}
```
3. **Item Spawning for Restoration**:
```csharp
public GameObject SpawnItemForLoad(ItemReference itemRef)
{
// Check if item exists in scene first
// If not, instantiate from prefab
// Apply item data
// Return GameObject
}
```
**ItemSlot Restoration Flow**:
1. ItemSlot restores state, finds it had a slotted item
2. Calls `ItemManager.GetPickupBySaveId(slottedItemSaveId)`
3. If found: Use existing GameObject
4. If not found: Call `ItemManager.SpawnItemForLoad(itemReference)`
5. Call `SlotItem()` with restored GameObject
**Edge Cases**:
- Item was picked up and slotted elsewhere: Use save ID to track
- Item was combined before slotting: May not exist anymore (store flag)
- Item was from player inventory: Need FollowerController integration
**Files to Modify**:
- `Assets/Scripts/Core/ItemManager.cs`
---
### Phase 8: FollowerController Integration (Optional)
**Goal**: Save/restore items currently held by the follower character
**State to Save**:
- Currently held item reference
- Held item position/rotation
**Decision**:
- **Phase 1**: Skip this, assume player starts levels with empty inventory
- **Future Enhancement**: Add when inventory persistence is needed
**Reasoning**:
- Most levels are self-contained
- Overworld persistence can be added later
- Reduces initial complexity
---
## Implementation Order & Dependencies
### Phase 1: Foundation (No Dependencies) ✅
- Create SaveableInteractable base class
- Implement ISaveParticipant
- Add save ID generation
- Test with dummy data
**Deliverable**: SaveableInteractable.cs compiles and can register/unregister
---
### Phase 2: Pickup Migration (Depends on Phase 1)
- Modify Pickup to extend SaveableInteractable
- Implement PickupSaveData serialization
- Test pickup state save/load
- Verify picked-up items stay hidden after load
**Deliverable**: Can save/load a scene with pickups, picked-up items remain picked up
---
### Phase 3: ItemSlot Migration (Depends on Phases 1, 2, 7)
- Modify ItemSlot to extend SaveableInteractable
- Implement ItemSlotSaveData serialization
- Integrate with ItemManager for item references
- Test slotted items restore correctly
**Deliverable**: Can save/load a scene with item slots, slotted items remain slotted
---
### Phase 4: Stateless Interactables (Depends on Phase 1)
- Migrate MinigameSwitch to SaveableInteractable
- Remove direct SaveLoadData access
- Test minigame unlock persistence
**Deliverable**: MinigameSwitch uses participant pattern instead of direct access
---
### Phase 5: State Machine Integration (Independent)
- Create SaveableState base class
- Migrate example states (TakePhotoState, etc.)
- Test state restoration without side effects
- Document pattern for future states
**Deliverable**: State machines can restore without replaying animations
---
### Phase 6: Save ID System (Depends on Phase 1)
- Implement GetSaveId() with hierarchy path
- Add custom save ID field
- Create editor validation tool
- Test ID stability across sessions
**Deliverable**: All saveable interactables have stable, unique IDs
---
### Phase 7: ItemManager Integration (Independent, but needed for Phase 3)
- Add item lookup methods to ItemManager
- Implement ItemReference serialization
- Add item spawning for restoration
- Test with ItemSlot restoration
**Deliverable**: ItemManager can find/spawn items by reference during load
---
### Phase 8: Testing & Polish (Depends on all previous phases)
- Test full save/load cycle in each level
- Test edge cases (combined items, missing prefabs, etc.)
- Add error handling and logging
- Update documentation
**Deliverable**: Robust save/load system for all interactables
---
## Data Structures
### SaveableInteractable Base State
```csharp
[System.Serializable]
public class InteractableBaseSaveData
{
public bool isActive; // GameObject.activeSelf
public float remainingCooldown; // If cooldown system is used
}
```
### Pickup State
```csharp
[System.Serializable]
public class PickupSaveData
{
public InteractableBaseSaveData baseData;
public bool isPickedUp;
}
```
### ItemSlot State
```csharp
[System.Serializable]
public class ItemSlotSaveData
{
public PickupSaveData pickupData; // Inherited state
public ItemSlotState slotState;
public ItemReference slottedItem; // Null if empty
}
[System.Serializable]
public class ItemReference
{
public string saveId; // For scene objects
public string prefabPath; // For prefab spawning
public string itemDataGuid; // PickupItemData reference
}
```
### MinigameSwitch State
```csharp
[System.Serializable]
public class MinigameSwitchSaveData
{
public InteractableBaseSaveData baseData;
public bool isUnlocked;
}
```
---
## Save/Load Flow
### Save Flow
1. SaveLoadManager iterates all registered participants
2. Calls `GetSaveId()` on each SaveableInteractable
3. Calls `SerializeState()` on each
4. SaveableInteractable:
- Calls virtual `GetSerializableState()` (child override)
- Serializes to JSON
- Returns string
5. SaveLoadManager stores in `participantStates` dictionary
6. Writes entire SaveLoadData to disk
### Load Flow
1. SaveLoadManager reads SaveLoadData from disk
2. Sets `IsRestoringState = true`
3. Scene loads (interactables register during Start())
4. On registration, SaveLoadManager checks for existing state
5. If state exists, immediately calls `RestoreState(serializedData)`
6. SaveableInteractable:
- Deserializes JSON
- Calls virtual `ApplySerializableState(stateData)` (child override)
- Child applies state WITHOUT triggering events/initialization
7. SaveLoadManager sets `IsRestoringState = false`
### State Machine Flow (During Load)
1. Interactable restores, determines it should trigger state "Scared"
2. Instead of firing UnityEvent, directly calls StateMachine.ChangeState("Scared")
3. StateMachine activates "Scared" state GameObject
4. "Scared" State's OnEnable() checks `SaveLoadManager.IsRestoringState`
5. If true: Sets internal variables only (no animations/movement)
6. If false: Normal initialization
---
## Error Handling
### Missing Item References
- **Problem**: ItemSlot has slotted item, but item no longer exists
- **Solution**: Log warning, set slot to empty state
### Duplicate Save IDs
- **Problem**: Two interactables generate same save ID
- **Solution**: Editor validation tool catches this, manual fix required
### Corrupted Save Data
- **Problem**: JSON deserialization fails
- **Solution**: Log error, use default state, continue loading
### State Machine Mismatch
- **Problem**: Saved state references state that no longer exists
- **Solution**: Log warning, set to initial state
---
## Testing Strategy
### Unit Tests
1. SaveableInteractable registration/unregistration
2. Save ID generation uniqueness
3. Serialization/deserialization round-trip
### Integration Tests
1. Save scene with various interactable states
2. Load scene and verify all states restored
3. Test item slot with slotted items
4. Test state machine restoration
5. Test edge cases (missing items, corrupted data)
### Playthrough Tests
1. Play through each level
2. Save at various points
3. Load and verify game state is correct
4. Test all puzzle solutions still work
---
## Migration Checklist
- [ ] Phase 1: Create SaveableInteractable base class
- [ ] Phase 2: Migrate Pickup to SaveableInteractable
- [ ] Phase 3: Migrate ItemSlot to SaveableInteractable
- [ ] Phase 4: Migrate MinigameSwitch to SaveableInteractable
- [ ] Phase 5: Create SaveableState pattern for state machines
- [ ] Phase 6: Implement save ID validation system
- [ ] Phase 7: Extend ItemManager with lookup/spawn functionality
- [ ] Phase 8: Full integration testing and polish
---
## Future Enhancements
1. **Inventory Persistence**: Save FollowerController held items
2. **Dynamic Object Spawning**: Handle runtime-spawned interactables
3. **State Machine Auto-Save**: Automatically save StateMachine current state
4. **Incremental Saves**: Save only changed interactables (delta saves)
5. **Cloud Sync**: Sync save data across devices
6. **Save Slots**: Multiple save files per player
---
## Notes & Considerations
### Why Not Auto-Discover Interactables?
- Performance: Scanning scenes is expensive
- Determinism: Registration order affects save ID generation
- Control: Objects control their own lifecycle
### Why Scene-Based Save IDs?
- Isolation: Each scene's interactables are independent
- Clarity: Easy to debug (see which scene an ID belongs to)
- Flexibility: Can reload individual scenes without affecting others
### Why Not Save Everything?
- Stateless interactables (OneClickInteraction, LevelSwitch) don't need persistence
- Reduces save file size
- Faster save/load times
- Less complexity
### State Machine Plugin Limitations
- Pixelplacement's StateMachine doesn't support silent state changes
- OnEnable is always called when state activates
- Need wrapper pattern (SaveableState) to intercept
- Alternative: Fork plugin and add restoration mode
---
## Document Version
- **Version**: 1.0
- **Date**: November 2, 2025
- **Author**: GitHub Copilot
- **Status**: Ready for Implementation