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

28 KiB

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:

[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:

SlotItem()        // 60 lines of logic
SlotItemSilent()  // 15 lines of duplicated logic

After:

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:

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: _isActiveswitchActive
  • 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:

    bool HasBeenRestored { get; }
    
  2. SaveableInteractable - Exposed existing flag:

    private bool hasRestoredState;
    public bool HasBeenRestored => hasRestoredState;
    
  3. SaveLoadManager.OnSceneLoadCompleted() - Active Discovery:

    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:

    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:

  1. Add IsRestoringFromSave static/singleton flag to SaveLoadManager

  2. State machines check this flag in OnEnable():

    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:

    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:

    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:

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:

    public Pickup GetPickupBySaveId(string saveId)
    
  2. Item Reference Serialization:

    [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:

    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

[System.Serializable]
public class InteractableBaseSaveData
{
    public bool isActive;           // GameObject.activeSelf
    public float remainingCooldown; // If cooldown system is used
}

Pickup State

[System.Serializable]
public class PickupSaveData
{
    public InteractableBaseSaveData baseData;
    public bool isPickedUp;
}

ItemSlot State

[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

[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