Skip to content

Saving & Loading

SaveManager persists named save slots as versioned envelopes. Each save records the schema version it was written at, so a shipped game can change its save shape later and still read old saves — on load, older data is migrated forward.

Create one manager for your game (pick a current schema version), then read and write slots by name:

import { SaveManager } from 'esengine';
const saves = new SaveManager({ version: 1 });
saves.save('slot1', { level: 3, score: 1200, hp: 80 });
const data = saves.load<{ level: number; score: number; hp: number }>('slot1');
// -> { level: 3, score: 1200, hp: 80 } (or null if the slot is empty)
saves.has('slot1'); // true
saves.savedAt('slot1'); // epoch ms the slot was written, or null
saves.remove('slot1'); // delete the slot
  • save(slot, data) writes data stamped with the current version and time.
  • load(slot) returns the slot’s data (migrated forward), or null when the slot is empty or unreadable.
  • has / remove / savedAt query, delete, and timestamp a slot.

When your save shape changes, bump version and add a migration for each step. migrations[n] upgrades a save written at version n to n + 1; on load, every step from the save’s version up to the current one runs in order:

const saves = new SaveManager({
version: 2,
migrations: {
// v1 stored a flat score; v2 nests it under `stats`.
1: (old) => {
const s = old as { level: number; score: number };
return { level: s.level, stats: { score: s.score, deaths: 0 } };
},
},
});
// A v1 save on disk is upgraded to the v2 shape transparently on load.
const data = saves.load('slot1');

Migration is forward-only: loading a save that is newer than the current version throws (no downgrade), and a missing step in the chain throws too — so a gap is caught immediately instead of silently corrupting data.

For simple flags and preferences that don’t need versioning, use the Storage API directly — a per-platform key/value store (browser localStorage, WeChat storage, …) with typed helpers:

import { Storage } from 'esengine';
Storage.setBoolean('muted', true);
Storage.setNumber('volume', 0.8);
Storage.setJSON('keybinds', { jump: 'Space', fire: 'KeyJ' });
Storage.getBoolean('muted', false); // true
Storage.getNumber('volume', 1); // 0.8
Storage.has('keybinds'); // true

Storage also has getString / setString, remove(key), and clear(). SaveManager is built on top of it, so both persist to the same per-platform backend.

There is no directory to open, and that is deliberate: the web and the mini-game platforms have no path namespace to hand you, so a getSaveDirectory() would be a function only two of five platforms could answer, and every game calling it would grow a branch per platform. Storage is the portable answer — write through it and each platform’s durable store is used for you.

Platform Backing store
Web localStorage
WeChat / mini-games wx.setStorageSync and friends
iOS Application Support/estella-storage.json
Android files/estella-storage.json (the app’s internal data dir)
Node memory only — it does not outlive the process

On iOS this is Application Support rather than Caches on purpose — iOS empties Caches whenever it wants the space back, so a save kept there is one the player can lose. Both mobile directories are private to the app and included in the device backup a player restores a new phone from.

Builds before Estella 0.41 kept the same file in the cache directory. If you have already shipped one, the first launch after the update finds that save and moves it — your players keep their progress and there is nothing to write.

platformReadFile() is not a way to reach these. It reads files packaged into the build (the APK’s assets/, the iOS app bundle) and is read-only.

  • Scenes — loading and switching the scenes a save points at.
  • Assets — why bulk data belongs in assets, not in a save slot.