PrismatiXEngine
Shipping and updates

Saves, rollback, and migrations

Distinguish Preview checkpoints from persistent saves and preserve Story and extension-state compatibility.

繁體中文

1. Preview is not persistent Player storage

Preview Save/Load slots are session-only checkpoints and are cleared by a rebuild or Preview shutdown. Use a packaged Player to test persistence across process restarts and release upgrades. Preview's temporary data is not evidence about persistent save behavior.

Start with the default save.open, load.open, and backlog.open Actions. When building custom UI, call the SDK rather than creating a separate JSON save system disconnected from the engine.

2. Work with slots

Inside an extension callback with persistence capability:

const saved = ctx.saves.save(3);
if (!saved) {
  ctx.raw.log('save', 'Slot 3 could not be saved');
}

const information = ctx.saves.query(3);
ctx.raw.log('save information', JSON.stringify(information));
const slots = ctx.saves.list();
ctx.raw.log('available slots', JSON.stringify(slots));

Load with ctx.saves.load(3) and autosave with autosave(). delete(3) deletes the slot; do not run it automatically against real player data during initialization or tests. Query first, clearly communicate the operation to the player, and handle failure as well as success.

A save includes more than a variable dictionary: Story location, managed UI/Stage state, and pending continuations can matter. Do not repair an unknown private format by manually editing it.

3. Stable identities and versions

Preserve IDs for equivalent Story operations, scene documents, resources, and state providers. Distinguish version for a game release, contentVersion for content, and saveVersion for save-format compatibility. They are not counters to increment on every save operation.

Variable renames, type changes, route changes, split Story operations, and removed resources can require compatibility handling. Keeping one dialogue ID does not make arbitrary changes to document identity, operation kind, or state schema safe.

4. A declarative migration example

In an isolated test project, first create a baseline with contentVersion tutorial-v1 and saveVersion 1. Package it, play it, and create test saves. This example renames v1's score to v2's points. Update the catalog and code references while also checking whether Story anchors really remain compatible.

Create Content/Migrations/v1-v2.pxsavemigration:

{
  "format": "PrismatiXSaveMigration",
  "schemaRevision": 2,
  "id": "tutorial.v1-v2",
  "from": {"contentVersion": "tutorial-v1", "saveVersion": 1},
  "to": {"contentVersion": "tutorial-v2", "saveVersion": 2},
  "anchor": {"policy": "preserve"},
  "operations": [
    {"op": "renameVariable", "from": "score", "to": "points"}
  ]
}

Merge the new project configuration:

{
  "contentVersion": "tutorial-v2",
  "saveVersion": 2,
  "saveMigrations": [
    {
      "id": "tutorial.v1-v2",
      "from": {"contentVersion": "tutorial-v1", "saveVersion": 1},
      "to": {"contentVersion": "tutorial-v2", "saveVersion": 2},
      "asset": "Content/Migrations/v1-v2.pxsavemigration"
    }
  ]
}

This is not a universal repair file. From/to must match real versions, the catalog must accept the new name and type, and preserve is appropriate only for compatible Story positioning. When anchors change, the schema supports policy: map with real runtimeDocumentId, sourceId, and operationId mappings. Do not invent internal identities from the prose visible on screen.

Other declarative operations include removeVariable, setVariable, renameRoute, and setScript. These can alter player state, so back up data and test each supported source version separately. The CLI's author-document migrate command is not the same contract as Player save migrations.

5. Migrate extension-owned state

Plugin state formats need versions too. Suppose v1 stored {score: number} and v2 stores {points: number}:

type PluginState = { points: number };
let pluginState: PluginState = { points: 0 };

ctx.state.registerProvider<PluginState>('tutorial.points', 2, {
  capture: () => ({ ...pluginState }),
  restore: (saved) => {
    pluginState = { ...saved };
  },
  migrate: (saved, fromVersion) => {
    if (fromVersion === 1 && saved !== null && typeof saved === 'object' && !Array.isArray(saved)) {
      const value = (saved as { score?: unknown }).score;
      if (typeof value === 'number' && Number.isFinite(value)) {
        return { points: value };
      }
    }
    throw new Error('Unsupported tutorial.points state migration');
  },
});

The previous version must have used the same provider ID. This does not silently rename the earlier tutorial.visits provider. Increase the version for a format change and explicitly reject unsupported data rather than quietly resetting unknown saves to zero.

Provider callbacks must be synchronous, deterministic JSON transformations. Do not play animations, emit audio, or invoke engine mutations inside restore/migrate.

6. Regression testing

Retain Players and isolated test saves for published versions. Cover ordinary dialogue, pending choices, animation waits, extension awaits, both languages, profile unlocks, and backlog rollback. Verify that restored state is complete and that replay does not repeat inappropriate side effects.

Full-screen transitions are transient. Managed UI/Stage state is authoritative after restore; old GPU frames and sample-exact media playback are not promised. Before inspecting a save, run prismatix inspect-save --help and supply the matching package or secret as required. Never include real player saves, secrets, or private paths in a public issue.

Sources: Save migration schema, Project schema, Runtime SDK, Compatibility cookbook.

On this page