Skip to content

SavePlugin

Manages game state persistence with auto-save, manual save slots, and quick save/load functionality.

Location

Configuration

The SavePlugin uses the following settings from pedre.conf.settings:

Save Plugin Settings

  • SAVE_FOLDER - Directory where save files are stored (default: "saves")
  • SAVE_QUICK_SAVE_KEY - Keybind for quick save action (default: "F5")
  • SAVE_QUICK_LOAD_KEY - Keybind for quick load action (default: "F9")
  • SAVE_SFX_FILE - Sound effect played when saving/loading (default: "save.wav")

These can be overridden in your project's settings.py:

# Custom save settings
SAVE_FOLDER = "game_saves"
SAVE_QUICK_SAVE_KEY = "F6"
SAVE_QUICK_LOAD_KEY = "F8"
SAVE_SFX_FILE = "menu_select.wav"

Public API

Save Operations

save_game

save_game(slot: int) -> bool

Save game to a specified slot.

Parameters:

  • slot - Save slot number (0 for auto-save, 1-3 for manual saves)

Returns:

  • True if save succeeded and file was written, False if any error occurred

Example:

# Save to slot 1
success = save_plugin.save_game(slot=1)
if success:
    print("Game saved successfully!")

Notes:

  • Creates a complete snapshot of the current game state
  • Gathers state from all registered save providers (plugins)
  • Writes to JSON file with 2-space indentation for readability
  • Updates current_slot tracker
  • Automatically caches current scene before saving

auto_save

auto_save() -> bool

Auto-save to the special auto-save slot (slot 0).

Returns:

  • True if auto-save succeeded, False if it failed

Example:

# Perform auto-save
if save_plugin.auto_save():
    print("Auto-save completed")

Notes:

  • Uses slot 0 for auto-save
  • Intended for crash recovery and quick save/load
  • Called automatically by quick save (F5 by default)

Load Operations

load_game

load_game(slot: int) -> GameSaveData | None

Load game from a specified slot.

Parameters:

  • slot - Save slot number (0 for auto-save, 1-3 for manual saves)

Returns:

  • GameSaveData object containing all saved state if successful
  • None if the save file doesn't exist or if loading failed

Example:

save_data = save_plugin.load_game(slot=1)
if save_data:
    save_plugin.restore_game_data(save_data)
    print("Game loaded successfully!")

Notes:

  • Reads save file from specified slot
  • Deserializes JSON into GameSaveData object
  • Updates current_slot tracker
  • Does NOT automatically restore state (call restore_game_data for that)

load_auto_save

load_auto_save() -> GameSaveData | None

Load from the auto-save slot (slot 0).

Returns:

  • GameSaveData object with auto-save state if successful
  • None if no auto-save exists or loading failed

Example:

save_data = save_plugin.load_auto_save()
if save_data:
    save_plugin.restore_game_data(save_data)

Notes:

  • Convenience method for loading slot 0
  • Called automatically by quick load (F9 by default)

restore_game_data

restore_game_data(save_data: GameSaveData) -> None

Phase 1: Restore metadata state from save data before sprites exist.

Parameters:

  • save_data - The GameSaveData object loaded from a save file

Example:

save_data = save_plugin.load_game(slot=1)
if save_data:
    # Phase 1: Restore metadata (settings, flags, which map to load)
    save_plugin.restore_game_data(save_data)

    # Phase 2 happens automatically after ScenePlugin loads sprites

Notes:

  • Restores non-entity state (settings, flags, which map to load)
  • Stores save data for Phase 2 (entity state restoration)
  • Entity-specific state (positions, visibility) is applied later via apply_entity_states()
  • Each plugin's restore_save_state() method is called
  • Scene cache state is also restored if present

apply_entity_states

apply_entity_states() -> None

Phase 2: Apply entity-specific state after sprites exist.

Example:

# Called automatically by ScenePlugin after load_from_tiled()
save_plugin.apply_entity_states()

Notes:

  • Called by ScenePlugin after load_from_tiled() has created all sprites
  • Applies positions, visibility, and other state that requires sprites to exist
  • Each plugin's apply_entity_state() method is called
  • Clears pending save data after application

Save Slot Management

save_exists

save_exists(slot: int) -> bool

Check if a save file exists in a slot.

Parameters:

  • slot - Save slot number (0 for auto-save, 1-3 for manual saves)

Returns:

  • True if a save file exists in the slot, False otherwise

Example:

if save_plugin.save_exists(1):
    print("Slot 1 has a save")
    info = save_plugin.get_save_info(1)
    print(f"Saved at: {info['date_string']}")

get_save_info

get_save_info(slot: int) -> dict[str, Any] | None

Get basic info about a save file without fully loading it.

Parameters:

  • slot - Save slot number (0 for auto-save, 1-3 for manual saves)

Returns:

  • Dictionary with save metadata if the file exists and is readable
  • None if the file doesn't exist or if an error occurred

Example:

info = save_plugin.get_save_info(1)
if info:
    print(f"Slot: {info['slot']}")
    print(f"Map: {info['map']}")
    print(f"Saved: {info['date_string']}")
    print(f"Version: {info['version']}")

Returned Fields:

  • slot (int) - The slot number
  • map (str) - Name of the map when saved
  • timestamp (float) - Unix timestamp
  • date_string (str) - Formatted date string (YYYY-MM-DD HH:MM)
  • version (str) - Save format version

delete_save

delete_save(slot: int) -> bool

Delete a save file.

Parameters:

  • slot - Save slot number (0 for auto-save, 1-3 for manual saves)

Returns:

  • True if save file existed and was deleted successfully
  • False if file didn't exist or deletion failed

Example:

if save_plugin.delete_save(2):
    print("Slot 2 deleted")

Quick Save/Load

The SavePlugin automatically handles quick save and quick load via keyboard shortcuts.

Quick Save (F5 by default):

# Triggered automatically when player presses F5
# Can be customized via SAVE_QUICK_SAVE_KEY setting

Quick Load (F9 by default):

# Triggered automatically when player presses F9
# Can be customized via SAVE_QUICK_LOAD_KEY setting

Notes:

  • Quick save uses the auto-save slot (slot 0)
  • Plays configured SFX on successful save/load
  • Logs warnings/info messages for debugging

Plugin Lifecycle

setup

setup(context: GameContext) -> None

Initialize the save plugin with game context.

Parameters:

  • context - Game context providing access to other plugins

Notes:

  • Called automatically by PluginLoader
  • Stores reference to game context

cleanup

cleanup() -> None

Clean up save plugin resources.

Notes:

  • Currently a no-op (no cleanup needed)
  • Called automatically by PluginLoader

on_key_press

on_key_press(symbol: int, modifiers: int) -> bool

Handle quick save/load hotkeys.

Parameters:

  • symbol - Keyboard symbol
  • modifiers - Key modifiers

Returns:

  • True if hotkey was handled, False otherwise

Notes:

  • Called automatically by PluginLoader
  • Checks for configured quick save/load keys
  • Uses matches_key helper to support custom key bindings

Save File Format

Directory Structure

Save files are stored in the configured save directory (default: saves/):

saves/
  ├── autosave.json
  ├── save_slot_1.json
  ├── save_slot_2.json
  └── save_slot_3.json

JSON Format

Save files use JSON with 2-space indentation for human readability:

{
  "save_states": {
    "scene": {
      "current_map": "village.tmx",
      "spawn_waypoint": null
    },
    "player": {
      "position": {
        "x": 320.0,
        "y": 240.0
      }
    },
    "npc": {
      "npcs": {
        "merchant": {
          "dialog_level": 2,
          "position": {"x": 400.0, "y": 300.0},
          "visible": true
        }
      },
      "interactions": {
        "village": ["merchant", "guard"]
      }
    },
    "inventory": {
      "items": {
        "health_potion": {
          "acquired": true,
          "consumed": false
        }
      },
      "accessed": true
    },
    "_scene_caches": {
      "village": {
        "npc": { /* cached NPC state */ },
        "player": { /* cached player state */ }
      }
    }
  },
  "save_timestamp": 1704067200.0,
  "save_version": "2.0"
}

Structure:

  • save_states - Dictionary mapping plugin names to their saved state
  • Each plugin manages its own state structure
  • _scene_caches stores cached states for scene transitions
  • save_timestamp - Unix timestamp when save was created
  • save_version - Save format version for future migrations

GameSaveData

Data class representing complete game state snapshot.

Attributes:

  • save_states: dict[str, Any] - Dictionary mapping save provider names to their serialized state
  • save_timestamp: float - Unix timestamp when save was created (seconds since epoch)
  • save_version: str - Save format version string (default: "2.0")

Methods:

  • to_dict() -> dict[str, Any] - Convert to dictionary for JSON serialization
  • from_dict(data: dict[str, Any]) -> GameSaveData - Create from dictionary loaded from JSON

Example:

# Create save data
save_data = GameSaveData(
    save_states={
        "player": {"position": {"x": 100.0, "y": 200.0}},
        "inventory": {"items": {}},
    },
    save_timestamp=datetime.now(UTC).timestamp(),
    save_version="2.0"
)

# Serialize to dict
data_dict = save_data.to_dict()

# Deserialize from dict
loaded_data = GameSaveData.from_dict(data_dict)

Save Providers

Any plugin can participate in the save plugin by implementing save/load methods.

Implementing Save Support

from pedre.plugins.base import BasePlugin
from pedre.plugins.registry import PluginRegistry

@PluginRegistry.register
class MyCustomPlugin(BasePlugin):
    name = "my_plugin"

    def get_save_state(self) -> dict[str, Any]:
        """Return serializable state for saving."""
        return {
            "my_data": self.some_data,
            "my_flags": self.flags,
        }

    def restore_save_state(self, state: dict[str, Any]) -> None:
        """Phase 1: Restore metadata state (before sprites exist)."""
        self.some_data = state.get("my_data", {})
        self.flags = state.get("my_flags", {})

    def apply_entity_state(self, state: dict[str, Any]) -> None:
        """Phase 2: Apply entity state (after sprites exist)."""
        # Restore positions, visibility, etc. that require sprites
        pass

Notes:

  • get_save_state() returns a JSON-serializable dictionary
  • restore_save_state() restores metadata state before sprites exist
  • apply_entity_state() applies entity state after sprites exist
  • Both restore methods receive the same state dict returned by get_save_state()
  • State is automatically included in save files under the plugin's name

Scene Caching

The SavePlugin also handles scene caching for smooth transitions.

Scene Cache Flow:

  1. Player enters portal to new scene
  2. SavePlugin caches current scene state under _scene_caches
  3. New scene loads
  4. Player returns to previous scene via portal
  5. SavePlugin restores cached state

Notes:

  • Scene caching is automatic when using portals
  • Cached state is separate from manual save slots
  • Cache is preserved across save/load operations
  • Each scene's cache includes all plugin entity states

Save Plugin Behavior

Two-Phase Loading

The save plugin uses a two-phase approach to handle sprite dependencies:

Phase 1: Metadata Restoration (restore_save_state)

  1. Save file loaded from disk
  2. Plugin metadata restored (flags, settings, which map to load)
  3. Scene begins loading
  4. Sprites created via load_from_tiled()

Phase 2: Entity State Application (apply_entity_state)

  1. All sprites now exist
  2. Entity-specific state applied (positions, visibility, dialog levels)
  3. Game resumes with full state restored

Why Two Phases?

  • Some state (player position, NPC visibility) requires sprites to exist
  • Other state (which map to load, inventory flags) doesn't require sprites
  • Two-phase approach cleanly separates these concerns

Save State Gathering

When saving, the SavePlugin collects state from all plugins:

save_states = {}
for plugin in registered_plugins:
    if hasattr(plugin, 'get_save_state'):
        save_states[plugin.name] = plugin.get_save_state()

Automatic Collection:

  • No manual registration required
  • Plugins opt-in by implementing get_save_state()
  • State automatically namespaced by plugin name
  • JSON serialization handled automatically

File Operations

Save files are managed using Python's standard library:

# Save file path
save_path = Path(SAVE_FOLDER) / f"save_slot_{slot}.json"

# Write save
with open(save_path, "w") as f:
    json.dump(save_data.to_dict(), f, indent=2)

# Read save
with open(save_path, "r") as f:
    data = json.load(f)

Error Handling:

  • File I/O errors caught and logged
  • Returns False or None on failure
  • Safe to call even if save folder doesn't exist
  • Creates save folder automatically on first save

Implementation Details

Save Slot Plugin

The SavePlugin uses a simple slot-based plugin:

Slot 0 (Auto-save):

  • Used for quick save/load
  • Overwritten automatically
  • Crash recovery
  • File: autosave.json

Slots 1-3 (Manual saves):

  • User-controlled saves
  • Never overwritten automatically
  • Persists between game sessions
  • Files: save_slot_1.json, save_slot_2.json, save_slot_3.json

Current Slot Tracking

The plugin tracks which slot was last saved/loaded:

self.current_slot: int | None = None

Usage:

  • Updated on every save/load operation
  • Used for "Continue" functionality
  • Available via get_current_slot()
  • None if no save/load has occurred

Quick Save/Load Implementation

Quick save and load are implemented via keyboard handlers:

def on_key_press(self, symbol: int, modifiers: int) -> bool:
    if matches_key(symbol, settings.SAVE_QUICK_SAVE_KEY):
        # Quick save to slot 0
        if self.auto_save():
            self.context.audio_plugin.play_sfx(settings.SAVE_SFX_FILE)
        return True

    if matches_key(symbol, settings.SAVE_QUICK_LOAD_KEY):
        # Quick load from slot 0
        save_data = self.load_auto_save()
        if save_data:
            self.restore_game_data(save_data)
        return True

    return False

Usage Examples

Basic Save/Load

# Save to slot 1
if save_plugin.save_game(slot=1):
    print("Game saved!")

# Load from slot 1
save_data = save_plugin.load_game(slot=1)
if save_data:
    save_plugin.restore_game_data(save_data)
    # Scene will load and apply entity states automatically

Checking Save Slots

# Check all save slots
for slot in range(1, 4):
    if save_plugin.save_exists(slot):
        info = save_plugin.get_save_info(slot)
        print(f"Slot {slot}: {info['map']} - {info['date_string']}")
    else:
        print(f"Slot {slot}: Empty")

Auto-save Before Dangerous Action

# Auto-save before boss fight
if save_plugin.auto_save():
    audio_plugin.play_sfx("save.wav")
    print("Progress auto-saved")

# Start boss fight

Delete Old Save

# Confirm with player first
if player_confirmed_deletion:
    if save_plugin.delete_save(slot=2):
        print("Slot 2 cleared")

Custom Quick Save Handler

# In your custom plugin
def on_key_press(self, symbol: int, modifiers: int) -> bool:
    from pedre.helpers import matches_key
    from pedre.conf import settings

    if matches_key(symbol, settings.SAVE_QUICK_SAVE_KEY):
        # Custom save logic
        if save_plugin.auto_save():
            show_save_notification()
        return True

    return False

Integration with Other Plugins

ScenePlugin Integration

The ScenePlugin coordinates scene loading during game restoration:

# SavePlugin triggers scene load
scene_name = save_data.save_states["scene"]["current_map"]
context.scene_plugin.request_transition(
    map_file=scene_name,
    spawn_waypoint=None  # Position restored via entity state
)

# After scene loads, ScenePlugin calls apply_entity_states()
context.save_plugin.apply_entity_states()

Notes:

  • ScenePlugin loads the map specified in save data
  • Player position restored after map loads
  • Scene caching preserved across save/load
  • Spawn waypoints cleared during save restoration

PlayerPlugin Integration

The PlayerPlugin saves and restores player position:

# Saving player state
def get_save_state(self) -> dict[str, Any]:
    player = self.get_player_sprite()
    return {
        "player_x": player.center_x,
        "player_y": player.center_y
    }

# Restoring player position (Phase 2)
def apply_entity_state(self, state: dict[str, Any]) -> None:
    player = self.get_player_sprite()
    player.center_x = state["player_x"]
    player.center_y = state["player_y"]

Notes:

  • Position only restored in Phase 2 (after sprite exists)
  • Player sprite must exist before state can be applied
  • Coordinates stored as floats for precision

NPCPlugin Integration

The NPCPlugin saves NPC states and interaction history:

# NPC state includes positions, dialog levels, visibility
{
  "npcs": {
    "merchant": {
      "dialog_level": 2,
      "position": {"x": 400.0, "y": 300.0},
      "visible": true
    }
  },
  "interactions": {
    "village": ["merchant", "guard"]
  }
}

Notes:

  • Dialog progression preserved
  • Per-scene interaction tracking saved
  • NPC positions and visibility restored
  • Interaction history available for conditions

AudioPlugin Integration

Audio plays feedback for save/load operations:

# Play save sound
if save_plugin.auto_save():
    context.audio_plugin.play_sfx(settings.SAVE_SFX_FILE)

Notes:

  • Configurable via SAVE_SFX_FILE setting
  • Audio feedback confirms successful operations
  • No sound played on failures

ScriptPlugin Integration

Script execution state can be saved:

# Script plugin tracks completed scripts
{
  "completed_scripts": ["intro_cutscene", "merchant_greeting"],
  "run_once_scripts": {"boss_defeated": true}
}

Notes:

  • run_once scripts tracked for save/load
  • Script conditions can check saved state
  • Prevents cutscenes from replaying

Troubleshooting

Save Not Working

If saves fail silently:

  1. Check save folder - Verify SAVE_FOLDER directory exists or can be created
  2. Check permissions - Ensure write permissions for save directory
  3. Review logs - Look for I/O errors or JSON serialization issues
  4. Test manually - Try save_game(1) and check return value
  5. Verify plugins - Ensure all plugins return JSON-serializable data from get_save_state()

Load Not Restoring State

If load succeeds but state isn't restored:

  1. Check Phase 1 - Verify restore_save_state() called for all plugins
  2. Check Phase 2 - Ensure apply_entity_states() called after scene loads
  3. Verify save data - Check save file contains expected plugin states
  4. Review plugin implementations - Ensure plugins implement both restore methods
  5. Check sprite existence - Entity state requires sprites to exist

Quick Save/Load Not Working

If keyboard shortcuts don't trigger saves:

  1. Check key bindings - Verify SAVE_QUICK_SAVE_KEY and SAVE_QUICK_LOAD_KEY settings
  2. Test key codes - Use matches_key helper to debug key detection
  3. Check auto-save slot - Ensure slot 0 is writable
  4. Review on_key_press - Ensure SavePlugin's key handler is called by PluginLoader

Save File Corruption

If save files are unreadable:

  1. Check JSON format - Open save file and verify valid JSON
  2. Check version - Ensure save_version matches expected version
  3. Backup saves - Keep backups before testing new features
  4. Validate serialization - Test get_save_state() returns JSON-serializable data
  5. Review custom plugins - Check custom plugins don't save unsupported types

Missing State After Load

If some state is missing after loading:

  1. Check plugin registration - Ensure all plugins are registered and loaded
  2. Verify save state - Check get_save_state() returns complete data
  3. Test both phases - Ensure both restore_save_state() and apply_entity_state() implemented
  4. Check scene caching - Verify scene cache includes all necessary state
  5. Review namespacing - Ensure plugin names match between save and load

Custom SavePlugin Implementation

If you need to replace the save plugin with a custom implementation, you can extend the SaveBasePlugin abstract base class.

SaveBasePlugin

Location: src/pedre/plugins/save/base.py

The SaveBasePlugin class defines the minimum interface that any save plugin must implement.

Required Methods

Your custom save plugin must implement these abstract methods:

from pedre.plugins.save.base import SaveBasePlugin, GameSaveData
from pedre.plugins.registry import PluginRegistry

@PluginRegistry.register
class CustomSavePlugin(SaveBasePlugin):
    """Custom save implementation."""

    name = "save"

    def restore_game_data(self, save_data: GameSaveData) -> None:
        """Restore all state from save data to save providers."""
        ...

    def load_auto_save(self) -> GameSaveData | None:
        """Load from auto-save slot."""
        ...

    def load_game(self, slot: int) -> GameSaveData | None:
        """Load game from a slot."""
        ...

    def get_save_info(self, slot: int) -> dict[str, Any] | None:
        """Get basic info about a save file without fully loading it."""
        ...

    def save_exists(self, slot: int) -> bool:
        """Check if a save file exists in a slot."""
        ...

    def save_game(self, slot: int) -> bool:
        """Save game to a slot."""
        ...

    def apply_entity_states(self) -> None:
        """Phase 2: Apply entity-specific state after sprites exist."""
        ...

Registration

Register your custom save plugin using the @PluginRegistry.register decorator:

from pedre.plugins.registry import PluginRegistry
from pedre.plugins.save.base import SaveBasePlugin

@PluginRegistry.register
class CloudSavePlugin(SaveBasePlugin):
    name = "save"

    # ... implement all abstract methods ...

Notes on Custom Implementation

  • Your custom plugin inherits from BasePlugin (via SaveBasePlugin), so you must implement the standard plugin lifecycle methods: setup(), cleanup(), and potentially reset()
  • The role attribute is set to "save_plugin" in the base class
  • Your implementation can use any storage backend (cloud, database, encrypted files, etc.)
  • Register your custom save plugin in your project's INSTALLED_PLUGINS setting before the default "pedre.plugins.save" to replace it

Example Custom Implementation:

# In myproject/plugins/cloud_save.py
from pedre.plugins.registry import PluginRegistry
from pedre.plugins.save.base import SaveBasePlugin, GameSaveData

@PluginRegistry.register
class CloudSavePlugin(SaveBasePlugin):
    """Save plugin that stores saves in the cloud."""

    name = "save"

    def __init__(self):
        self.cloud_client = CloudStorageClient()
        # ... rest of initialization ...

    def save_game(self, slot: int) -> bool:
        # Upload save to cloud storage
        return self.cloud_client.upload(slot, save_data)

    # ... implement other abstract methods ...
# In myproject/settings.py
INSTALLED_PLUGINS = [
    "myproject.plugins.cloud_save",  # Load custom save first
    "pedre.plugins.camera",
    "pedre.plugins.audio",
    # ... rest of plugins (omit "pedre.plugins.save") ...
]

See Also