# ReplayPlayer _(no submodules)_ ## Purpose `ReplayPlayer` plays back a `ReplayData` document produced by `ReplayRecorder`. It exposes the controls expected of a media player — play, pause, stop, step forward/backward, seek, speed — but is driven by explicit `tick()` calls rather than a timer, so playback is fully deterministic and trivially exercised in tests or headless replay rigs. The player delivers replayed inputs through observer subscriptions (`onInput`, `onSnapshotRestore`) so consumer engines stay in control of their own dispatch pipeline. OTel counters track `playback_advance_total` and `playback_seek_total` so dashboards can monitor replay throughput in production replay-analysis workloads. ## Features - Tick-driven, deterministic playback. - Speeds: `0.25 / 0.5 / 1 / 2 / 4` with Zod-enforced enum. - Snapshot-aware seek and stepBackward. - Loop mode for highlight loops or attract screens. - Observer-style input + snapshot listeners (no global emitter). - Stable error codes: `REPLAY_NOT_LOADED`, `REPLAY_PARSE_FAILED`, `REPLAY_INVALID_SPEED`. - OTel counters tagged per advance / seek. ## Usage: Basic ```ts import { ReplayPlayer } from "@suaveplan/game-server/enhanced/replay/replay-player"; const player = new ReplayPlayer(); player.load(replay); player.onInput((input) => engine.apply(input.data, input.playerId)); player.play(); setInterval(() => player.tick(), 1000 / 60); ``` ## Usage: Advanced ```ts import { ReplayPlayer } from "@suaveplan/game-server/enhanced/replay/replay-player"; const player = new ReplayPlayer( { loop: true, initialSpeed: 2 } ); player.load(replay); const offInput = player.onInput((i) => engine.apply(i.data, i.playerId)); const offSnap = player.onSnapshotRestore((state) => engine.replace(state)); player.play(); for (let i = 0; i < replay.metadata.totalTicks * 4; i += 1) player.tick(); player.seekToTick(replay.metadata.totalTicks / 2); player.setSpeed(0.5); player.stepBackward(); offInput(); offSnap(); ``` ## API Reference - `new ReplayPlayer(config?, deps?)` - `load(replay): void` - `play(): void` / `pause(): void` / `stop(): void` - `tick(): void` - `stepForward(): void` / `stepBackward(): void` - `seekToTick(target): void` - `setSpeed(speed: PlaybackSpeed): void` - `onInput(listener): () => void` - `onSnapshotRestore(listener): () => void` - `getPosition(): PlaybackPosition` - `getMetadata(): ReplayMetadata | null` - `getStatus(): PlaybackStatus` / `getSpeed(): PlaybackSpeed` - `getCurrentState(): TState | null` / `isLoaded(): boolean` ## Implementation Notes Playback advancement multiplies `speed` into the number of ticks consumed per `tick()` call, which yields exact rather than approximate replay speed on every callable cadence (60Hz, 30Hz, single-step debugging). Speeds outside the canonical set are rejected via Zod-validated config and an explicit `REPLAY_INVALID_SPEED` error at `setSpeed` time. Seek walks the recorded snapshots backwards to find the most-recent snapshot at or before the target tick, restores from it, then advances input-by-input to the requested tick. With no snapshots, the player falls back to the initial state — slower but always correct. Engines that need snappy scrubbing should record interval snapshots at a reasonable rate (60–300 ticks). End-of-replay handling is split between `loop=true` (rewinds and keeps playing) and `loop=false` (transitions to `ended`). The transition to `ended` is sticky — further `tick()` calls remain a no-op until `play()` or `seekToTick()` rewinds the cursor, which prevents accidental input re-emission from off-by-one tick loops.