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.
Save slots
Section titled “Save slots”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'); // truesaves.savedAt('slot1'); // epoch ms the slot was written, or nullsaves.remove('slot1'); // delete the slotsave(slot, data)writesdatastamped with the current version and time.load(slot)returns the slot’s data (migrated forward), ornullwhen the slot is empty or unreadable.has/remove/savedAtquery, delete, and timestamp a slot.
Schema migration
Section titled “Schema migration”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.
Plain key/value storage
Section titled “Plain key/value storage”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); // trueStorage.getNumber('volume', 1); // 0.8Storage.has('keybinds'); // trueStorage also has getString / setString, remove(key), and clear().
SaveManager is built on top of it, so both persist to the same per-platform
backend.
Where it actually lands
Section titled “Where it actually lands”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.