Skip to content

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.

typescript
const saveManager = engine.managers.saveManager;

SimulationSaveManager

MethodMeaning
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

typescript
interface SaveEnvelope
{
    saveFormatVersion : number;
    engineVersion : string;
    level : string;
    timestamp : number;
    custom : Record<string, unknown>;
    snapshot : GameSimulationSnapshot;
}
FieldMeaning
saveFormatVersionThe save format, currently 1. A load rejects a version it does not know.
engineVersionThe SAGE version that wrote the save
levelThe name of the level the game was in
timestampWhen the save was written, in milliseconds
customMerged data from every onBeforeSave hook
snapshotExactly 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

typescript
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

typescript
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:

  1. The level manager transitions to envelope.level with forRestore: 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.
  2. fromSnapshot builds the replacement simulation from envelope.snapshot, restoring every entity without running onCreate. Each restored NodeEntity builds its node, claims a pooled set, or adopts the authored node its nodeName names, and loads its mesh the same way a fresh spawn does.
  3. load answers with the replacement simulation and the envelope it read, custom included.

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:

typescript
function saveToSlot(slot : number, simulation : NodeSimulation) : void
{
    localStorage.setItem(`save-slot-${ slot }`, JSON.stringify(saveManager.save(simulation)));
}

Released under the MIT License.