> For the complete documentation index, see [llms.txt](https://series-1.gitbook.io/rundot-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://series-1.gitbook.io/rundot-docs/v5.29.1/readme/syncplay.md).

# Syncplay: Deterministic Multiplayer API (1.0 Candidate)

The deterministic core ships inside `@series-inc/rundot-syncplay`; core APIs live under the `/core` subpaths. Syncplay supplies input authority, prediction, rollback, replay, late join, secrets, and transport for a signed simulation product. The core owns the simulation itself: state, RNG streams, mechanics, fixed ticks, checkpoints, serialization, and checksums.

Mechanics come in two supported shapes. **Installed packs are package code configured by canonical data** — a module declaration cannot smuggle executable callbacks; the data selects and parameterizes shipped reducers. **Custom executable mechanics are source-bound code** — built against `createInstalledRuntimeAdapter` with the implementation folded into runtime identity at build time. Either way, games create a Kinetix runtime from the product and the room's canonical session config, then pass it to the single Syncplay session factory (or let the [session runner](#session-runner) do it).

Use the [runtime composition cookbook](/rundot-docs/v5.29.1/readme/syncplay/syncplay-runtime-compositions.md) to choose between a preset, a game-owned adapter, and a hand-written runtime, then copy the identity, checkpoint, projection, command, and deterministic-asset patterns that apply to your game.

## What authoritative means here

Authority is positional. It is not a property of an API.

The core runs one loop for each frame. It calls your `step` with that frame's inputs and commands. It then calls `captureState`. It then hashes the captured state. That hash is the frame checksum. Each client reports its checksum for sampled frames, and the authority compares the reports for one frame. Two reports that differ for the same frame are a desync.

**A value is authoritative if, and only if, your `step` wrote it into the state that `captureState` returns.** Nothing else makes a value authoritative. No API is authoritative or cosmetic by itself.

Two rules follow:

* Every deterministic API in this SDK is legal inside `step`. Physics steps, raycasts, overlaps, and shape casts are legal there. Their results become authoritative because `step` writes them into captured state. Do not build a second hit finder for your simulation tick.
* The same API is cosmetic when you call it outside `step`, or when you keep its result out of captured state. A cosmetic call cannot desync a room. A cosmetic call also cannot decide a hit.

Never read a nondeterministic source inside `step`. Wall-clock time, `Math.random`, network responses, and ephemeral relay payloads are nondeterministic. Rendering, interpolation, audio, and effects run outside `step`. They must never write to authoritative state.

The [runtime composition cookbook](/rundot-docs/v5.29.1/readme/syncplay/syncplay-runtime-compositions.md#2-adapt-immutable-game-state-the-authoritative-path) shows this capture contract in code. [Syncplay: Physics & Collision](/rundot-docs/v5.29.1/readme/syncplay/syncplay-physics.md) lists the physics APIs you can call inside `step`.

## Install and verify

Never install this package unpinned. A bare `npm install` — or an agent `add_dependency` with no version — resolves the `latest` dist-tag, which trails the builds these docs describe: an install below 5.26.0 publishes no `/core`, `/core/runtime` or `/modules/*` subpath at all, so every import here fails with `ERR_PACKAGE_PATH_NOT_EXPORTED`.

`latest` does not satisfy that floor today. Read the tags first:

```bash
npm view @series-inc/rundot-syncplay dist-tags
```

The RC release workflow adds `rc: 6.0.0-rc.22`. That tag carries the API that these docs describe. Install the `rc` tag explicitly and pin the version it resolves to:

```bash
npm install --save-exact @series-inc/rundot-syncplay@rc
```

`--save-exact` writes the resolved version into `package.json` with no range prefix. Never accept a caret or tilde range here. A range lets a later install move the build, and the installed version is folded into runtime identity (step 1 below). Read the pinned version back from `package.json` after the install, and confirm it is **>= 5.26.0**. Expect npm to raise `@series-inc/rundot-game-sdk` to satisfy the peer range; that bump is correct. Then verify the subpaths resolve before writing code:

```bash
node --input-type=module -e "
const fs = await import('node:fs');
console.log('syncplay', JSON.parse(fs.readFileSync('node_modules/@series-inc/rundot-syncplay/package.json','utf8')).version);
await import('@series-inc/rundot-syncplay/core/runtime');
await import('@series-inc/rundot-syncplay/creator');
console.log('subpaths resolve OK');
"
```

A subpath that will not resolve is a version problem, never an account, entitlement, or beta-enablement problem — no Syncplay feature is gated per creator.

## 1. Bind your runtime source into identity

```typescript
import {
  deriveCustomRuntimeIdentity,
  kinetixRuntimeModuleManifest,
} from '@series-inc/rundot-syncplay/core'
import pkg from '../package.json' with { type: 'json' }

// The pinned dependency string. `--save-exact` makes it the installed version.
const syncplayVersion = pkg.dependencies['@series-inc/rundot-syncplay']

export const runtimeIdentity = deriveCustomRuntimeIdentity({
  tickRate: 30,
  inputSchema: { id: 'my-game-input/v1' },
  stateSchema: { id: 'my-game-state/v1' },
  deterministicVersion: `my-game@1+syncplay${syncplayVersion}`,
  runtimeModules: kinetixRuntimeModuleManifest('src', [
    'sim/runtime.ts',
    'sim/step.ts',
  ]),
  sessionConfigSchema: {
    id: 'my-game-session/v1',
    version: 1,
    maxBytes: 65536,
    fields: { playerCount: { type: 'integer', min: 1, max: 8 } },
  },
})
```

Run this in a checked generator with a fixed module list. Generated identity files must not appear in their own manifest. Bind the exact installed `@series-inc/rundot-syncplay` version into `deterministicVersion`, so a dependency change requires regeneration. Read that version from `package.json`, as shown above. A hard-coded version literal goes stale without an error. The version string is folded into `engineIdentityHash`, so the old generated product cannot retain the same engine hash after a dependency bump. `tickRate` accepts 10, 20, 30, and 60 only; any other value throws `KINETIX_CUSTOM_RUNTIME_IDENTITY_INVALID`. Preset games can instead use a Kinetix data pack and its provided runtime factory.

## 2. Construct the runtime and session

```typescript
import {
  createInstalledRuntimeAdapter,
} from '@series-inc/rundot-syncplay/core/runtime'
import { createSyncplaySession } from '@series-inc/rundot-syncplay/creator'

const runtime = createInstalledRuntimeAdapter({
  identity: runtimeIdentity,
  sessionConfigBytes,
  captureState: () => engine.state,
  restoreState: (state) => { engine.state = structuredClone(state) },
  frameOf: (state) => state.frame,
  step: (inputs) => engine.step(inputs),
  projectState: (state) => state,
})

const session = createSyncplaySession(runtime, {
  playerCount: 4,
  defaultInput: { throttle: 0, steer: 0, brake: false },
  snapshotBufferSize: 256,
  checksumIntervalFrames: 30,
})
```

`createSyncplaySession(runtime, policy)` accepts a constructed Kinetix runtime only. The policy accepts twelve fields and nothing else: `playerCount`, `defaultInput`, `snapshotBufferSize`, `checksumIntervalFrames`, `maxCatchUpFrames`, `requires`, `copyInput`, `canonicalizeInput`, `canonicalizeCommand`, `onInputFrameRecorded`, `onCommandFrameRecorded`, and `onChecksumRecorded`. Any other key throws `Unknown Syncplay session field <name>`. The policy accepts no presentation callback: presentation is a projection the renderer reads, not a hook the session calls. The policy also cannot supply initial state, a reducer, a random seed, or a simulation callback.

Live stepping and rollback use opaque checkpoint handles. Serialization and hydration happen only for explicit v2 promotion/late-join transfer. A v2 snapshot contains opaque runtime bytes plus runtime identity, session-config digest, frame, and checksum; every field is checked before the destination mutates.

## 3. Go multiplayer

```typescript
import {
  createNetworkedSyncplayClient,
  createSyncplayRoom,
  defineSyncplayInputCodec,
  quickMatchSyncplayRoom,
  joinSyncplayRoomByCode,
} from '@series-inc/rundot-syncplay/browser'

const inputCodec = defineSyncplayInputCodec({
  throttle: { kind: 'int', min: 0, max: 100 },
  steer: { kind: 'int', min: -100, max: 100 },
  brake: { kind: 'boolean' },
})

const room = await createSyncplayRoom(multiplayerApi, {
  maxPlayers: 4,
  runtimeIdentity: runtime.identity,
  sessionConfigBytes: runtime.sessionConfigBytes(),
  sessionConfigDigest: runtime.sessionConfigDigest,
})

const client = createNetworkedSyncplayClient({
  transport: room,
  runtimeFactory: (identity, configBytes) => loadSignedRuntime(identity, configBytes),
  localInputForTick: readControls,
  defaultInput: inputCodec.neutralInput,
  encodeInput: inputCodec.encodeInput,
  decodeInput: inputCodec.decodeInput,
})
```

`defaultInput`, `encodeInput`, and `decodeInput` are required, together with `transport`, `runtimeFactory`, and `localInputForTick`. `decodeInput` must map the canonical `null` substitute to `defaultInput`, because the authority sends `null` for a slot with no confirmed input. `defineSyncplayInputCodec` derives all three from one schema and does that mapping for you. Hand-written codecs must do it themselves.

Create and quick match carry runtime identity and config. Matchmaking always isolates engine identity and config digest. Joining by private code accepts no replacement config: the joiner receives the room-owned bytes, verifies its catalog-loaded signed product matches, and constructs frame 0 only after that check. Reconnect and crash recovery repeat the exact bytes.

`createSyncplayRoom()` and `quickMatchSyncplayRoom()` create the room type `deterministic`. The matching entry in `rundot/realtime.config.json` must use that type. These helpers add `createOptions.metadata.syncplayRuntime`. A generic `joinOrCreateRoom()` call does not add this metadata.

## Presentation

Rendering reads a runtime **projection**, not a captured second runtime. A live `NetworkedSyncplayClient` exposes `getPresentationFrame()`, `renderAlpha`, `currentFrame`, and `caughtUp`, plus an opt-in bounded confirmed-frame queue (`confirmedPresentationBufferSize` + `drainConfirmedPresentationFrames()`) and `beginPresentationCatchUp()` for reconnects. The [session runner](#session-runner) wraps all of this behind a `presentation` contract. Rendering, interpolation, audio, and effects never mutate authoritative state, and presentation output is never fed back into simulation.

## Session config and external deterministic assets

Canonical session config is capped at **65,536 bytes** (`MAX_KINETIX_SESSION_CONFIG_BYTES`). Content larger than that — a UGC level — rides its own storage: stored UGC `data` is capped at **100 KiB** by default (a per-app configurable cap), and a session carries `levelEntryId + levelDigest + levelByteLength`. Joiners fetch by **entry id** (no digest lookup endpoint), re-encode the stored content canonically, and verify digest and length with `verifyKinetixAuxiliaryAsset` before frame 0.

Referenced entries are **never updated in place** — a `ugc.update` under a live session pin makes the room permanently unjoinable. Publish every edit as a new entry and resolve and verify the immutable pin before frame 0.

## Session runner

`createSyncplayRunner` owns the `offline | connecting | syncing | live | stopped | error` status, offline bot filling, network pacing, cancellable UGC asset preflight (`prepareNetworkRuntime` / `prepareNetworkedSyncplayTransport`), and presentation/event delivery behind one runtime factory.

### Config surface

`SyncplayRunnerConfig` — everything the runner accepts, nothing else:

| Field                         | Required | Meaning                                                                                                                                                                                                                                                                                                                                                                                                           |
| ----------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `runtimeFactory`              | yes      | `(identity, sessionConfigBytes) => KinetixRuntime`. The one place a runtime is constructed, offline and networked alike.                                                                                                                                                                                                                                                                                          |
| `defaultInput`                | yes      | The neutral input. Fills bot slots and is the reset value for local input.                                                                                                                                                                                                                                                                                                                                        |
| `encodeInput` / `decodeInput` | yes      | Wire codec. Derive it with `defineSyncplayInputCodec` rather than hand-writing it.                                                                                                                                                                                                                                                                                                                                |
| `presentation`                | yes      | `{ project(projection, { localSlot, status }), interpolate?(previous, current, alpha), events?(projection) }`. Presentation-only; never feeds simulation.                                                                                                                                                                                                                                                         |
| `prepareNetworkRuntime`       | no       | `(descriptor, signal) => Promise<void>` — async asset preflight run **before** the client is constructed. The transport buffers ordered `session-start`/history traffic while it awaits. Aborted on `stop()` and on a superseded connection.                                                                                                                                                                      |
| `maxPredictionTicks`          | no       | Rollback/latency budget passed through to the networked client. Not a player-count limit.                                                                                                                                                                                                                                                                                                                         |
| `maxOfflineStepsPerUpdate`    | no       | Default **8**. Caps catch-up stepping in one `update(deltaMs)` so a backgrounded tab cannot spiral.                                                                                                                                                                                                                                                                                                               |
| `predictionMode`              | no       | `'always'` (default) or `'confirmed-only'`. In confirmed-only the runner applies confirmed frames only — no prediction, no rollback — while local input still flows to the authority. Use it for editing surfaces, where a rollback yank on a placed tile is worse than a frame of latency.                                                                                                                       |
| `remoteInputPrediction`       | no       | `repeat-last` (default) repeats each remote slot's last confirmed input and is appropriate for held movement controls. Use `neutral` for pulse/edge command codecs so a confirmed one-shot is not repeated into speculative frames.                                                                                                                                                                               |
| `localInputForTick`           | no       | `(slot, frame) => Input` — per-tick local input provider for incremental adoption (a game whose input is a per-tick pulse supplies its own reader instead of the persistent `applyInput` value). Called once per simulated local frame, offline and networked; when omitted the runner uses the last `applyInput(...)` value verbatim.                                                                            |
| `pacing`                      | no       | Wall-clock pacing + clock-drift correction for the networked client, forwarded verbatim. Omit for the client defaults.                                                                                                                                                                                                                                                                                            |
| `inputDelay`                  | no       | Local input delay for the networked client; `maxTicks > minTicks` auto-tunes from measured RTT. Forwarded verbatim; omit for the client default (0).                                                                                                                                                                                                                                                              |
| `requires`                    | no       | `{ commands?, projection? }` — the runtime capabilities this game needs, forwarded to the offline session and to the networked client. `projection: true` on a runtime with no `project` throws `SYNCPLAY_PROJECTION_UNSUPPORTED`; `commands: true` on a runtime built without `decodeCommand` throws `SYNCPLAY_RUNTIME_COMMANDS_UNSUPPORTED`. Both checks run at session construction, not at the first command. |
| `diagnostics`                 | no       | Sink for host notices, currently the deprecation notice each compatibility method emits. One notice per key per runner. Omit it to print the notice with `console.warn`. Pass `null` to disable notices. A sink failure cannot fail the session.                                                                                                                                                                  |

Offline start options (`SyncplayRunnerOfflineOptions`): `identity`, `sessionConfigBytes`, `playerCount`, `localSlot?`, `localSlots?`, `botInputForTick?(slot, frame)`.

`localSlots` is shared-screen co-op: every listed slot is driven locally, and `applyInput(input, slot)` targets one of them (`localSlot` is sugar for a single entry). This is **offline only** — one connection is one seat is a room-server seating invariant, so networked play stays single-seat.

### Snapshot

`subscribe(listener)` publishes a frozen `SyncplayRunnerSnapshot` on every status or presentation change:

| Field              | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `status`           | `offline \| connecting \| syncing \| live \| stopped \| error`                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `lifecycle`        | A second vocabulary, and not a rename of `status`: `{ state: 'idle' \| 'starting' \| 'running' \| 'stopped' \| 'failed' \| 'disposed' }`. `starting` and `running` also carry `mode`; `running` carries `localSlots`, and a networked `running` also carries `connection`; `failed` carries `error`. `start()` succeeds only from `idle`, `stopped`, or `failed`, and from any other state throws a `SyncplayError` with code `runner.start-state-invalid`. `dispose()` moves to `disposed`, which is terminal. |
| `renderState?`     | Present once a frame has been projected                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `renderAlpha`      | Interpolation alpha within the current frame                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `localSlot`        | `-1` until the client is ready                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `rollbackCount`    | Cumulative client rollbacks; `0` in offline mode and in `confirmed-only`                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `ready`            | False until a session/client exists. Offline: true once a session exists and the frame counters track the local session                                                                                                                                                                                                                                                                                                                                                                                         |
| `appliedThrough`   | Last authority-confirmed frame; `-1` before any session/client                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `predictedThrough` | Last locally predicted frame; `0` before any session/client                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `error?`           | Present only when `status === 'error'`                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `connection`       | `'connected'` or `'reconnecting'` — the transport's phase                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `roomCode?`        | Present when the transport carries one                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `occupancy?`       | `{ occupied, maxPlayers, full, locked }` when the transport reports it                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `presence?`        | The client's confirmed per-slot presence                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `netStats?`        | Client net stats, including `droppedConfirmedPresentationFrames` and `rejectedCommands`                                                                                                                                                                                                                                                                                                                                                                                                                         |

`update(deltaMs)` throws `SYNCPLAY_RUNNER_DELTA_INVALID` on a negative or non-finite delta; `getRenderState()` before the first projection throws `SYNCPLAY_RUNNER_NO_RENDER_STATE`; a malformed `maxOfflineStepsPerUpdate` throws `SYNCPLAY_RUNNER_CONFIG_INVALID` at construction, and malformed offline options throw `SYNCPLAY_RUNNER_OFFLINE_OPTIONS_INVALID`.

### Reconnect contract

**The transport owns reconnection — the runner adds no re-dial machinery.** Re-invoking an opaque connect factory risks a fresh join instead of reclaiming the seat, so the runner deliberately does not do it.

| Transport event  | Runner behaviour                                                                                                                                                                                                                 |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `'reconnecting'` | **Non-terminal.** Status drops to `syncing`, `beginPresentationCatchUp()` is called so replayed history is silent, and the runner returns to `live` once the client is caught up *and* has advanced past the pre-recovery frame. |
| `'disconnected'` | **Terminal.** Teardown, `status: 'error'` carrying `SYNCPLAY_TRANSPORT_DISCONNECTED`.                                                                                                                                            |
| `'error'`        | **Terminal.** Teardown, `status: 'error'` carrying the transport's message.                                                                                                                                                      |

Because a hard disconnect is terminal, **your game should offer an explicit re-join action** on terminal `error` — call `await runner.start({ mode: 'networked', connect })` again with a fresh connect factory. Starting any mode always tears down the previous one, and a stale async connection or preparation closes without replacing the current session. `startNetworked` and `startOffline` are deprecated compatibility methods; they remain until major version 6 and cannot cancel startup.

See also the [Syncplay runner README](https://www.npmjs.com/package/@series-inc/rundot-syncplay).

## Collaborative editing

Ordered edit commands, confirmed-only prediction, live session reconfiguration, and an ephemeral presence channel compose into a shared editor on the same deterministic session — no second realtime path. See [Syncplay: Collaborative Editing](/rundot-docs/v5.29.1/readme/syncplay/syncplay-editor.md).

## Browser room intent and sharing

`resolveSyncplayBrowserIntent`, `openSyncplayBrowserRoom`, and `shareSyncplayRoom` compose RUN launch-intent, the anonymous sign-in gate, and share links over the existing room APIs. They expose state and actions only — no badge, modal, or sign-in UI.

## Correctness tests

`@series-inc/rundot-syncplay/testing` exposes exactly nine supported entry points — `assertDeterministic`, `assertReplayVerifies`, `assertTwoClientConvergence`, `assertLateJoinHydration`, `createSimulatedSyncplayMatch`, `runSyncplaySynctest`, `createSyncplayRunnerHarness`, `assertRunnerLifecycle`, and `assertPresentationParity`. The network assertions run the real authority room and networked client; browser/runtime APIs do not leak through the subpath.

The last three cover the seam games actually ship on — the runner, rather than the client underneath it:

| Entry point                   | What it proves                                                                                                                                                                                                                                                                |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `createSyncplayRunnerHarness` | Drives a real runner against the simulated match on a fully controlled clock, and records every status transition. Nothing advances outside `advance()`.                                                                                                                      |
| `assertRunnerLifecycle`       | offline → networked → offline → stop, asserting the status sequence at each step.                                                                                                                                                                                             |
| `assertPresentationParity`    | The same inputs produce the same confirmed presentation offline and networked. This catches projections that are deterministically **wrong but checksum-consistent** — a class of bug convergence tests cannot see, because the state agrees and only the projection differs. |

`createSimulatedSyncplayMatch` also wires `playerId`, `sendSecret` and `onSecretMessage`, so secret systems are testable, and `openTransport(playerId)` returns a raw transport when you want a real runner to own the seat.

## Capacity and late-join budgets

Snapshot transfer defaults are **256 KiB** chunks and a **4 MiB** total ceiling; the immutable level payload is fetched and verified separately, never inside a snapshot. 128 slots is the tested protocol/load ceiling, not a 60 Hz performance guarantee for an arbitrary game; `maxPredictionTicks` is a rollback/latency budget, not a player-count limit.

## Replays and late join

```typescript
import { verifyReplay } from '@series-inc/rundot-syncplay/creator'

const result = verifyReplay({
  replay,
  runtimeFactory: (identity, configBytes) => loadSignedRuntime(identity, configBytes),
  policy,
})
```

Replay v2 records runtime identity, canonical session-config bytes/digest, inputs, authority commands, and checksums. Reducer replay v1 and snapshot v1 are rejected; there is no migration decoder.

## Determinism and trust

* Only identity-bound game runtimes or Kinetix preset mechanics execute authoritative gameplay.
* Runtime identity is compiler-derived and signed with the product.
* The authority orders inputs but does not invent runtime identity or config.
* A bogus room cannot recruit an honest client whose loaded signed pack differs.
* Optional hidden choices, random draws, decks, bags, and roles use [Syncplay Secret Systems](/rundot-docs/v5.29.1/readme/syncplay/syncplay-secrets.md).

The Kinetix world runtime is a correctness/reference backend with deep-copy checkpoints. The shared batched ABI is the insertion point for optimized JS, WASM, or native backends without changing Syncplay.

## Where to go next

| Your question                                                                    | Page                                                                                                                                                                                                               |
| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Can I run physics or raycasts on my simulation tick?                             | Yes, when you call them inside `step`. Read [What authoritative means here](#what-authoritative-means-here) first, then [Syncplay: Physics & Collision](/rundot-docs/v5.29.1/readme/syncplay/syncplay-physics.md). |
| How do I build my own runtime, projection, checkpoint codec, or command decoder? | [Runtime Composition Cookbook](/rundot-docs/v5.29.1/readme/syncplay/syncplay-runtime-compositions.md)                                                                                                              |
| My game is 2D. Which physics do I use?                                           | [Syncplay: 2D Physics](/rundot-docs/v5.29.1/readme/syncplay/syncplay-physics-2d.md)                                                                                                                                |
| How do I migrate a game that uses beta fluid physics?                            | [Syncplay: RC1 migration](/rundot-docs/v5.29.1/readme/syncplay/syncplay-rc1-migration.md#check-removed-beta-features)                                                                                              |
| How do I move a character deterministically?                                     | [Syncplay: Character Movement](/rundot-docs/v5.29.1/readme/syncplay/syncplay-movement.md)                                                                                                                          |
| How do I drive animation from simulation state?                                  | [Syncplay: Animation](/rundot-docs/v5.29.1/readme/syncplay/syncplay-animation.md)                                                                                                                                  |
| How do I find a path across the map?                                             | [Syncplay: Pathfinding](/rundot-docs/v5.29.1/readme/syncplay/syncplay-pathfinding.md)                                                                                                                              |
| How do I resolve melee attacks and damage?                                       | [Syncplay: Combat Modules](/rundot-docs/v5.29.1/readme/syncplay/syncplay-combat.md)                                                                                                                                |
| How do I fire a weapon?                                                          | [Syncplay: Firearm Module](/rundot-docs/v5.29.1/readme/syncplay/syncplay-firearm.md)                                                                                                                               |
| How do I apply a buff, a debuff, or damage over time?                            | [Syncplay: Gameplay Effects](/rundot-docs/v5.29.1/readme/syncplay/syncplay-gameplay-effects.md)                                                                                                                    |
| How do I model health, resources, cooldowns, and abilities?                      | [Syncplay: Stats and Abilities](/rundot-docs/v5.29.1/readme/syncplay/syncplay-stats-abilities.md)                                                                                                                  |
| How do I carry and equip items?                                                  | [Syncplay: Inventory and Equipment](/rundot-docs/v5.29.1/readme/syncplay/syncplay-inventory-equipment.md)                                                                                                          |
| How do I pick up, open, or use a world object?                                   | [Syncplay: Spatial Interaction](/rundot-docs/v5.29.1/readme/syncplay/syncplay-spatial-interaction.md)                                                                                                              |
| I am building a 2D platformer.                                                   | [Syncplay: 2D Platformer Module](/rundot-docs/v5.29.1/readme/syncplay/syncplay-platformer-2d.md)                                                                                                                   |
| I am building an RTS.                                                            | [Syncplay: RTS Module](/rundot-docs/v5.29.1/readme/syncplay/syncplay-rts.md)                                                                                                                                       |
| How do I select units and issue orders?                                          | [Syncplay: Unit Control Module](/rundot-docs/v5.29.1/readme/syncplay/syncplay-unit-control.md)                                                                                                                     |
| How do I give entities stable ids across peers?                                  | [Syncplay: Entity Registry](/rundot-docs/v5.29.1/readme/syncplay/syncplay-entity-registry.md)                                                                                                                      |
| What are the shipped component modules?                                          | [Syncplay: Component Modules](/rundot-docs/v5.29.1/readme/syncplay/syncplay-components.md)                                                                                                                         |
| Is a module certified for production?                                            | [Syncplay: Module Certification](/rundot-docs/v5.29.1/readme/syncplay/syncplay-module-certification.md)                                                                                                            |
| How do I run NPC behavior in the simulation?                                     | [Syncplay: Actors and AI](/rundot-docs/v5.29.1/readme/syncplay/syncplay-actors-ai.md)                                                                                                                              |
| How do I use a 3D character rig?                                                 | [Syncplay: 3D Character Modules](/rundot-docs/v5.29.1/readme/syncplay/syncplay-character-3d.md)                                                                                                                    |
| How do I fill empty slots with bots?                                             | [Syncplay: Bots & AI](/rundot-docs/v5.29.1/readme/syncplay/syncplay-bots.md)                                                                                                                                       |
| How do I hide a card, a role, or a random draw from a peer?                      | [Syncplay: Secret Systems](/rundot-docs/v5.29.1/readme/syncplay/syncplay-secrets.md)                                                                                                                               |
| How do several people edit one world together?                                   | [Syncplay: Collaborative Editing](/rundot-docs/v5.29.1/readme/syncplay/syncplay-editor.md)                                                                                                                         |
| A fast-moving target is hard to hit. What do I do?                               | [Syncplay: Lag Compensation](/rundot-docs/v5.29.1/readme/syncplay/syncplay-lag-compensation.md)                                                                                                                    |
| I upgraded the package and something broke.                                      | [Syncplay: RC1 migration](/rundot-docs/v5.29.1/readme/syncplay/syncplay-rc1-migration.md)                                                                                                                          |

## Canonical serialization and parsing

Syncplay provides `canonicalStringify` and `canonicalParse` for deterministic JSON serialization:

* `canonicalStringify(value)`: Serializes an arbitrary JavaScript object or value into a canonical, reproducible JSON string with deterministically sorted keys. Special JavaScript values that standard JSON cannot express (such as `-0`, `NaN`, `Infinity`, `-Infinity`, `BigInt`, `Map`, `Set`, and typed arrays) are encoded using sentinel objects (e.g. `{"$rdm":"negative-zero"}`, `{"$rdm":"bigint","value":"123"}`).
* `canonicalParse<T>(text)`: Decodes a canonical JSON string back into JavaScript values, restoring all sentinel tags into their true primitive or object forms (including `-0` and `BigInt`).
* **Important**: Decoding a canonical string with standard `JSON.parse` leaves sentinel objects unexpanded (e.g., yielding `{ $rdm: "negative-zero" }` instead of `-0`). Always use `canonicalParse` or pass standard parsed JSON through `decodeCanonicalValue(parsed)` to recover true values.
