Saving

The whole world is plain JSON, so saving is copying it and loading is putting it back. This page covers what’s in a save, the order to load in, history, versions, and changing a game’s content under existing saves.

Save and load

const data = world.save();                       // { tyrell: 'world', v: 1, s: { ... } }
localStorage.setItem('town', JSON.stringify(data));

// later, in a new session
const town = buildTown();                        // create, declare everything, add everyone
town.load(localStorage.getItem('town'));         // an object or its JSON string
town.focus([currentLevel]);

world.save(withHistory = false) returns SaveData: { tyrell: 'world', v, s, history? }, where v is the schema version and s a deep copy of the state. world.load(data) takes that object or its JSON string. A loaded world is the same world: the same hash, and it runs on exactly as the original would have (Voight-Kampff’s save-load check tests both).

If the game writes the save at once, it can store world.s itself instead of a copy: { tyrell: 'world', v: world.s.v, s: world.s } is what save() returns, without the copy.

What’s in a save

Everything that changes:

In the save  
The clock now, the sequence counter, the world’s stream counter
People Each person’s definition (name, traits, home, job, routine…), status, needs, variables, goods, place, slot, what they’re doing, habits, cooldowns, plan and set-aside plans, directive, time away, who they follow
Places Slot claims and stock
Facts All of them
The queue Pending action ends, thoughts, the hourly tick, game timers (world.at), module timers (witnesses’ pending reports), returns from away
The log The last config.logSize entries
The story With config.storySize, the entries Irulan keeps beyond the log (Irulan)
Module state Feelings, memories, beliefs, files, stress; relationships, standing, partners; heat, reports, secrets, favors; lines said; deck draws, intensity; purses, pantries, rent; stages, parents, the departed; promoted strangers
Kept game state Everything in world.keep
Tuning Settings changed during play with world.tune (and people’s own tune, in their definitions)

Not in the save, so the game provides them on every start:

Declaring and loading

Declare before loading, or after: both work, and both give the same world.

What load does: copies the saved state (the save object is never changed, so the same save can be loaded again and again), migrates it to the current schema, replaces the world’s state, gives modules their fresh state where it’s missing and calls their loaded hooks, makes sure every declared place has state, resets Esper (and restores its history if the save has one), clears Palantír’s decisions, disembodies every body, and embodies whoever is near now. Listeners stay registered.

load doesn’t move the clock: the loaded world is at the saved time. If the game owns the clock, load its own save first and then call world.advanceTo(gameMinutes()) as usual.

Setup that must happen once

Some setup is an input that changes the state, not a definition: starting money (ceres.pay), incomes and rent (ceres.income, ceres.rent), partners (fellowship.setPartner), secrets. Done on every start, it would happen again on top of a loaded save whenever the game declares after loading. Guard it with a kept flag:

if (!world.kept('setup:money')) {
  world.ceres.pay('world', 'h_okafor', 200, 'savings');
  world.ceres.income('ruth', 120, 7);
  world.keep('setup:money', true);
}
if (!world.fellowship.partner('mara')) world.fellowship.setPartner('mara', 'tomas');

Rocinante does exactly this.

Changing content under existing saves

Definitions come from code, so changing an action, a place or a line takes effect on the next start, saved games included. People are different: a person’s definition is saved with them, so a new routine in code doesn’t reach someone who’s already in a save.

Update saved people with a custom input, run once per content version:

world.defineInput('redefine', (id, patch) => {
  const p = world.get(id);
  if (p) Object.assign(p.def, patch);
});

world.load(saved);
if ((world.kept('content') ?? 1) < 2) {
  world.input('redefine', ['kofi', { routine: [[7, null, '*'], [12, 'park', 'sit_park'], [13, null, '*'], [23, 'home', 'sleep']] }]);
  world.keep('content', 2);
}

The change is an input, so it’s recorded and replays after a rewind. People added in a new version just need a world.person call (they’re added if missing). New needs are given to existing people the next time they think. A doing that names an action you’ve removed ends at its time and the person thinks again.

Saving with history

world.save(true) also includes Esper’s history (snapshots, recorded inputs and marks), so the loaded world can still rewind past the load. History is large: each snapshot is the whole state, so expect megabytes (Rocinante after a week: about 7 MB with history, 150 KB without). Save with history only when the game needs to rewind across a save, and lower config.logSize and Esper’s keep option to shrink it. See Time.

Without history, a loaded world starts a fresh history at its first advance: it can’t go back past the moment it was loaded.

Schema versions and migrations

Tyrell.SCHEMA is the version of the state’s shape (2 in this version). Every save carries it. When a future Tyrell changes the shape, it ships a migration in Tyrell.MIGRATIONS: MIGRATIONS[v] takes a version-v state and returns a version-v + 1 state. load applies them in turn, to the state and to the snapshots of a save’s history. Tyrell 1.1 moved to schema 2 (Cityspeak remembers lines said by their key, where 1.0 kept them by index): saves from 1.0 load and keep what was said. It throws if a step is missing (no migration from save version v) or if the save is newer than the engine (save version v is newer than this engine): catch that and tell the player, or start a new world.

try {
  world.load(saved);
} catch (e) {
  console.warn('The town could not be loaded; starting over.', e);
  // world is still the freshly built one
}

Tyrell’s migrations cover Tyrell’s own state. For the game’s content (routines, new people), version it yourself with a kept number, as above.

Where to store it

A save without history is small (tens to hundreds of KB for a town), and fits in localStorage. With history, use IndexedDB or a file. Keep Tyrell’s save inside the game’s own save, so the two never get out of step:

const save = { game: game.s, tyrell: world.save() };
localStorage.setItem('mygame.save.v1', JSON.stringify(save));