Save & Restore
simulatorClient.matches.save() captures a full state snapshot of one live match into a wire-format MatchSavePayload. simulatorClient.matches.restore() reconstitutes that payload into a fresh live session — new match id, fresh seat tokens, AI driver respawned if the original session had one.
This is not the same as the older debug replay flow. Replay re-runs every command from match creation; save captures the engine state directly, works for AI-opponent matches, and does not re-execute any commands.
When to use this
- "Bookmark" a match position so testers or developers can return to it.
- Move an in-progress match between simulator instances (export from one server, import on another running the same release).
- Snapshot a tricky state for a bug report — paired with the wire-format schema, the payload is everything an investigator needs to reproduce.
Save
const payload = await simulatorClient.matches.save(hostAccess)The returned payload includes:
version: wire-format version. Compare to your SDK'sschemaVersionif you persist payloads across releases.sourceMatchId: original match id, retained for traceability.seats: per-seat decks. Used at restore to rebuild the catalog.aiOpponents: oneAiOpponentConfigper AI seat the source match had.state: opaque engine state. Treat as a black box; round-trip it byte-for-byte.
The simulator does not persist the payload. Save it wherever fits your deployment — localStorage, IndexedDB, a file download, or a server upload sit equally well. JSON serialization is the only requirement.
const payload = await simulatorClient.matches.save(hostAccess)
// localStorage example. Use the schema version in the key so a future SDK
// release does not silently load an incompatible payload.
localStorage.setItem('swu-save', JSON.stringify(payload))Restore
Public HTTP save and restore require SWU_SIMULATOR_SAVE_SIGNING_SECRET. The server signs every payload field with HMAC-SHA256 and binds that signature to the seat token supplied outside the payload. Keep this server-only secret stable across restarts; changing it intentionally invalidates prior public saves. The save payload never contains the seat token.
import type { MatchSavePayload } from '@my-swu/simulator-client'
const persisted = JSON.parse(
localStorage.getItem('swu-save') ?? '{}',
) as MatchSavePayload
// The source access proves the caller owned the saved match: the server
// checks the payload signature against this seat token.
const restored = await simulatorClient.matches.restore(hostAccess, persisted)restored carries everything you need to reconnect:
matchId: freshly minted id for the restored session.accesses: oneMatchAccessResponseper seat, with newseatTokens. Original tokens are not preserved.snapshot: public board state at restore time, identical in shape to aGET /api/matches/{id}response.
Connect the restored seat tokens the same way you would for a freshly created match:
const persistedSave = JSON.parse(
localStorage.getItem('swu-save') ?? '{}',
) as import('@my-swu/simulator-client').MatchSavePayload
const restored = await simulatorClient.matches.restore(hostAccess, persistedSave)
const restoredHost = restored.accesses.find(access => access.seat === 'one')
if (!restoredHost) throw new Error('seat one missing')
const session = simulatorClient.session.create(restoredHost)
await session.connect()What is preserved
- Engine state (board, hands, decks, prompt, phase, round number).
MatchSeed-driven RNG (the saved seed continues to drive any subsequent shuffle or AI decision).- AI configuration: an AI-opponent match comes back with the AI driver re-spawned from saved
AiOpponentConfigentries. The MCTS tree is rebuilt fresh; only the configuration rides along, not the search state.
What is not preserved
- Match id and seat tokens. Both are minted fresh on restore. Hand the new tokens out to your seats; the old ones do not authorize anything.
- The websocket transport. Restore is HTTP-only; reopen sockets against the new match id and seat tokens after the call returns.
- AI search state (MCTS tree, RNG step). The rebuilt driver starts cold.
Validation
Restore rejects payloads early so the HTTP surface returns 400 rather than 500. The current rejections are:
- Wire-format version mismatch.
- Empty
seatslist. - Duplicate seats in
seats. - Seat one missing from
seats. - A seat referenced in
seatsthat is not configured for the match'splayerCount. - A
joinedseat whose deck is not present inseats.
These are surface-level integrity checks. Beyond them, restore re-validates every seat's deck against the catalog, which catches stale saves built against a different card set.
Limitations
- Save and restore are HTTP-only. Saving from a websocket client requires a separate
POST /api/matches/{id}/saveHTTP call. - The simulator stores nothing on disk. Persistence is the SDK consumer's responsibility.
- Save payloads are tied to the wire format. Bumping
schemaVersioninvalidates older payloads. - Restore re-validates decks against the running server's catalog. A save from a release with a card the new server does not know will fail.
