Saves
A saved game is a versioned envelope around a core snapshot, built by sage's SimulationSaveManager. The manager produces and consumes plain JSON-compatible objects; where they are stored is the game's business.
const saveManager = engine.managers.saveManager;SimulationSaveManager
| Method | Meaning |
|---|---|
save(simulation) | A SaveEnvelope around simulation.snapshot() |
load(envelope, simulation) | Transitions to the saved level, builds a replacement simulation from the envelope's snapshot, and answers with both |
onBeforeSave(hook) | Registers a hook whose returned object is merged into the envelope's custom field |
SaveEnvelope
interface SaveEnvelope
{
saveFormatVersion : number;
engineVersion : string;
level : string;
timestamp : number;
custom : Record<string, unknown>;
snapshot : GameSimulationSnapshot;
}| Field | Meaning |
|---|---|
saveFormatVersion | The save format, currently 1. A load rejects a version it does not know. |
engineVersion | The SAGE version that wrote the save |
level | The name of the level the game was in |
timestamp | When the save was written, in milliseconds |
custom | Merged data from every onBeforeSave hook |
snapshot | Exactly what snapshot() returned, unmodified |
What a snapshot holds
The snapshot is the tick counter, the ID counter, and every entity's ID, name, and state bag, deep-copied at a fixed-step boundary. See Simulation.
Transforms and parent IDs are state fields on NodeEntity, so the snapshot already carries them. Behavior instance fields are not in it: anything worth saving lives in the state bag. Entity classes are referenced by name and must be in the class table when the save loads.
Saving
saveManager.onBeforeSave(() =>
{
return { score, questLog: quests.completedIDs() };
});
function saveGame(simulation : NodeSimulation) : void
{
const envelope = saveManager.save(simulation);
localStorage.setItem('save-slot-1', JSON.stringify(envelope));
}Several hooks may be registered. Their returned objects are shallow-merged into custom, later hooks winning on a shared key.
Loading
async function loadGame(simulation : NodeSimulation) : Promise<NodeSimulation | undefined>
{
const json = localStorage.getItem('save-slot-1');
if(json === null)
{
return undefined;
}
const envelope = JSON.parse(json) as SaveEnvelope;
const result = await saveManager.load(envelope, simulation);
score = result.envelope.custom.score as number;
return result.simulation as NodeSimulation;
}Loading runs in order:
- The level manager transitions to
envelope.levelwithforRestore: true. A level loaded this way spawns nothing of its own: its spawn markers and entity markers are skipped, and the snapshot is the sole source of entities. fromSnapshotbuilds the replacement simulation fromenvelope.snapshot, restoring every entity without runningonCreate. Each restoredNodeEntitybuilds its node, claims a pooled set, or adopts the authored node itsnodeNamenames, and loads its mesh the same way a fresh spawn does.loadanswers with the replacement simulation and the envelope it read,customincluded.
A load rejects a save-format version it does not know, and nothing converts a save from one version to another. A save written under 0.9 does not load.
Entity IDs are preserved
The core snapshot keys on entity IDs and nothing remaps them. An ID a game stored somewhere else, in custom or in another entity's state, still names the same entity after a load. The ID counter comes back with the snapshot, so IDs minted afterwards continue past every ID the save held.
The replacement and the engine
load hands the replacement simulation back to its caller. engine.simulation is not reassigned by the save manager; the level transition inside load has already rebuilt it for the new scene, and the replacement load answers with is built from the simulation the caller passed in. Wiring the restored entities into the running engine is not covered by this release. See Simulation for what a NodeSimulation carries over when it builds its replacement.
Storage
The manager produces and consumes plain objects. localStorage, IndexedDB, a server API, or a file all work. Save slots are a key the game chooses:
function saveToSlot(slot : number, simulation : NodeSimulation) : void
{
localStorage.setItem(`save-slot-${ slot }`, JSON.stringify(saveManager.save(simulation)));
}