SavePlugin¶
Manages game state persistence with auto-save, manual save slots, and quick save/load functionality.
Location¶
- Implementation: src/pedre/plugins/save/plugin.py
- Base class: src/pedre/plugins/save/base.py
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:
Trueif save succeeded and file was written,Falseif 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_slottracker - Automatically caches current scene before saving
auto_save¶
auto_save() -> bool
Auto-save to the special auto-save slot (slot 0).
Returns:
Trueif auto-save succeeded,Falseif it failed
Example:
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:
GameSaveDataobject containing all saved state if successfulNoneif 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_slottracker - Does NOT automatically restore state (call
restore_game_datafor that)
load_auto_save¶
load_auto_save() -> GameSaveData | None
Load from the auto-save slot (slot 0).
Returns:
GameSaveDataobject with auto-save state if successfulNoneif no auto-save exists or loading failed
Example:
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:
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:
Trueif a save file exists in the slot,Falseotherwise
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
Noneif 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 numbermap(str) - Name of the map when savedtimestamp(float) - Unix timestampdate_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:
Trueif save file existed and was deleted successfullyFalseif file didn't exist or deletion failed
Example:
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 symbolmodifiers- Key modifiers
Returns:
Trueif hotkey was handled,Falseotherwise
Notes:
- Called automatically by PluginLoader
- Checks for configured quick save/load keys
- Uses
matches_keyhelper 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_cachesstores cached states for scene transitionssave_timestamp- Unix timestamp when save was createdsave_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 statesave_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 serializationfrom_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 dictionaryrestore_save_state()restores metadata state before sprites existapply_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:
- Player enters portal to new scene
- SavePlugin caches current scene state under
_scene_caches - New scene loads
- Player returns to previous scene via portal
- 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)
- Save file loaded from disk
- Plugin metadata restored (flags, settings, which map to load)
- Scene begins loading
- Sprites created via
load_from_tiled()
Phase 2: Entity State Application (apply_entity_state)
- All sprites now exist
- Entity-specific state applied (positions, visibility, dialog levels)
- 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
FalseorNoneon 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:
Usage:
- Updated on every save/load operation
- Used for "Continue" functionality
- Available via
get_current_slot() Noneif 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:
Notes:
- Configurable via
SAVE_SFX_FILEsetting - 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_oncescripts tracked for save/load- Script conditions can check saved state
- Prevents cutscenes from replaying
Troubleshooting¶
Save Not Working¶
If saves fail silently:
- Check save folder - Verify
SAVE_FOLDERdirectory exists or can be created - Check permissions - Ensure write permissions for save directory
- Review logs - Look for I/O errors or JSON serialization issues
- Test manually - Try
save_game(1)and check return value - 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:
- Check Phase 1 - Verify
restore_save_state()called for all plugins - Check Phase 2 - Ensure
apply_entity_states()called after scene loads - Verify save data - Check save file contains expected plugin states
- Review plugin implementations - Ensure plugins implement both restore methods
- Check sprite existence - Entity state requires sprites to exist
Quick Save/Load Not Working¶
If keyboard shortcuts don't trigger saves:
- Check key bindings - Verify
SAVE_QUICK_SAVE_KEYandSAVE_QUICK_LOAD_KEYsettings - Test key codes - Use
matches_keyhelper to debug key detection - Check auto-save slot - Ensure slot 0 is writable
- Review on_key_press - Ensure SavePlugin's key handler is called by PluginLoader
Save File Corruption¶
If save files are unreadable:
- Check JSON format - Open save file and verify valid JSON
- Check version - Ensure
save_versionmatches expected version - Backup saves - Keep backups before testing new features
- Validate serialization - Test
get_save_state()returns JSON-serializable data - Review custom plugins - Check custom plugins don't save unsupported types
Missing State After Load¶
If some state is missing after loading:
- Check plugin registration - Ensure all plugins are registered and loaded
- Verify save state - Check
get_save_state()returns complete data - Test both phases - Ensure both
restore_save_state()andapply_entity_state()implemented - Check scene caching - Verify scene cache includes all necessary state
- 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(viaSaveBasePlugin), so you must implement the standard plugin lifecycle methods:setup(),cleanup(), and potentiallyreset() - The
roleattribute 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_PLUGINSsetting 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¶
- ScenePlugin - Scene transitions and loading
- Configuration Guide
- Views