Save/Load System
Relevant source files
The following files were used as context for generating this wiki page:The Save/Load system in Souless is responsible for persisting and retrieving game state. It is designed to function in two different environments: a standard web browser using localStorage, and a desktop application via Electron, where it uses the local file system. The system supports multiple save slots, caches data for performance, and handles asynchronous save operations to prevent blocking the game loop.
The primary component of this system is the SaveSlotManager, which provides a comprehensive API for managing save slots and various types of game data. It interacts with the Electron main process via IPC for file operations when running as a desktop application. In-game scenes like GameScene and UI scenes like MainMenuScene rely on this manager to handle all persistence-related tasks, from auto-saving player progress to loading a game from a selected slot.
Architecture Overview
The save system's architecture is built around the SaveSlotManager class, which acts as a central hub for all save and load requests. It abstracts the underlying storage mechanism, automatically choosing between browser localStorage and Electron's file system.
- Browser Environment: Data is saved as a JSON string in
localStorageunder keys prefixed withsave_slot_. - Electron Environment: The renderer process sends save/load requests to the main process (
main.js) via IPC. The main process then reads or writes JSON files to the application'suserDatadirectory.
The diagram below illustrates the high-level interaction between components.
This diagram shows how game scenes interact with SaveSlotManager, which then routes the request to either localStorage or the Electron file system via an IPC bridge.
Sources: src/utils/SaveSlotManager.js:21-23, main.js:80-145, src/scenes/GameScene.js:103-111, src/scenes/MainMenuScene.js:13-16
Core Component: SaveSlotManager
SaveSlotManager is a static class that orchestrates all data persistence. It manages a fixed number of save slots (MAX_SLOTS = 3) and handles the logic for reading, writing, and clearing data for each slot.
Sources: src/utils/SaveSlotManager.js
Key Responsibilities
| Feature | Description |
|---|---|
| Slot Management | Manages which slot is currently active (CURRENT_SLOT_KEY) and provides methods to load all slot summaries (loadAllSlots) for the main menu. |
| Asynchronous Saving | Uses a save queue (_saveQueue) and a flag (_isSaving) to process save requests asynchronously. This prevents data corruption from multiple rapid save calls by only saving the most recent data in the queue. |
| Data Caching | Caches the data for the currently loaded slot (_cachedData) to provide fast, synchronous-like access via the load() method if the data is already in memory. |
| Environment Abstraction | Contains the isElectron() check to determine whether to use localStorage or dispatch an IPC call to the Electron main process. |
| Data Serialization | Manages saving complex game state objects, such as KillTracker and PortalConditionManager, by calling their respective serialize() methods. |
Sources: src/utils/SaveSlotManager.js:9-19, src/utils/SaveSlotManager.js:115-140, src/utils/SaveSlotManager.js:150-178
Save Process Flow
When SaveSlotManager.save() is called, the data is not immediately written to storage. Instead, it's pushed to a queue, and a processing function handles the actual file/localStorage write. This ensures that only the latest version of the data is saved if multiple save requests occur in quick succession.
This sequence diagram shows the asynchronous save queue mechanism.
Sources: src/utils/SaveSlotManager.js:101-140
Load Process Flow
The load() method first checks its internal cache. If the requested slot's data is cached, it's returned immediately. Otherwise, it retrieves the data from the appropriate storage medium.
This diagram illustrates the caching logic within the load process.
Sources: src/utils/SaveSlotManager.js:70-100
Electron Integration (IPC)
When the game runs in an Electron environment, SaveSlotManager communicates with the main process (main.js) to perform file system operations. This is necessary because the renderer process has restricted access to the local file system for security reasons.
The following IPC channels are defined in main.js to handle file operations:
| IPC Handler | Parameters | Description |
|---|---|---|
load-save | slotIndex | Reads and parses the JSON file for the given slot. Returns null if the file doesn't exist. |
save-save | data, slotIndex | Stringifies and writes the provided data to the JSON file for the given slot. |
clear-save | slotIndex | Deletes the save file for the given slot. |
get-save-path | slotIndex | Returns the absolute path to the save file (used for debugging). |
Save files are stored in the path provided by app.getPath('userData'), with each file named save_slot_{slotIndex}.json.
Sources: main.js:80-145
The code snippet below shows the save-save handler in the Electron main process.
// main.js
ipcMain.handle('save-save', async (event, data, slotIndex) => {
try {
const savePath = getSavePath(slotIndex);
await fs.writeFile(savePath, JSON.stringify(data, null, 2), 'utf-8');
return true;
} catch (error) {
console.error(`Save save error (slot ${slotIndex}):`, error);
throw error;
}
});
Sources: main.js:115-125
Save Data Structure
All game progress is stored in a single JSON object. The SaveSlotManager.getDefaultSaveData() method defines the default structure for a new or empty save file.
The following diagram represents the relationships between the main entities in the save data JSON.
UI and In-Game Integration
Main Menu (MainMenuScene)
The main menu uses SaveSlotManager to display and manage save slots.
- On creation,
MainMenuScenecallsSaveSlotManager.loadAllSlots()to fetch a summary for each slot. - It uses
SaveSlotManager.isSlotReallyEmpty()to determine if a slot contains actual gameplay data or is just default data. - When a player starts a new game or loads an existing one,
SaveSlotManager.selectSlot()is called. This method sets the active slot, clears caches, and initializes or loads the relevantKillTrackerandPortalConditionManagerdata. - The scene also uses
calculateClearPercentage()based on saved data (clearedBosses,levelSystem) to display progress.
Sources: src/scenes/MainMenuScene.js:90-97, src/scenes/MainMenuScene.js:100-106, src/scenes/MainMenuScene.js:421-438
Game Scene (GameScene)
GameScene is responsible for triggering saves during gameplay.
- Auto-Save: An
autoSavemethod is called periodically (every 5 seconds) in theupdateloop to save the current player position, resources, level system data, and kill/portal progress. - Event-Based Saving: Saves are also triggered on key events, such as scene shutdown (
shutdown), pausing the game (pause), or entering a portal (onPortalEnter). - Loading Data: On initialization,
GameScene.loadSaveData()checks for a saved position. If the saved map differs from the current one, it redirects the player to the correct scene. Otherwise, it uses the saved data to spawn the player at the last known location.
Sources: src/scenes/GameScene.js:1052-1062, src/scenes/GameScene.js:203-209, src/scenes/GameScene.js:283-305
Conclusion
The Souless Save/Load System is a well-structured and robust feature that handles data persistence across different application environments. Its core, SaveSlotManager, effectively abstracts storage details, manages multiple slots, and uses an asynchronous queue to ensure data integrity without impacting game performance. The system's integration with other game modules like LevelSystem and PortalConditionManager allows it to save a comprehensive snapshot of the player's progress, making it a critical foundation of the game's architecture.