3.6 KiB
ReplayPlayer
(no submodules)
Purpose
ReplayPlayer plays back a ReplayData<TState, TInput> 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 / 4with 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
import { ReplayPlayer } from "@suaveplan/game-server/enhanced/replay/replay-player";
const player = new ReplayPlayer<GameState, GameInput>();
player.load(replay);
player.onInput((input) => engine.apply(input.data, input.playerId));
player.play();
setInterval(() => player.tick(), 1000 / 60);
Usage: Advanced
import { ReplayPlayer } from "@suaveplan/game-server/enhanced/replay/replay-player";
const player = new ReplayPlayer<GameState, GameInput>(
{ 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): voidplay(): void/pause(): void/stop(): voidtick(): voidstepForward(): void/stepBackward(): voidseekToTick(target): voidsetSpeed(speed: PlaybackSpeed): voidonInput(listener): () => voidonSnapshotRestore(listener): () => voidgetPosition(): PlaybackPositiongetMetadata(): ReplayMetadata | nullgetStatus(): PlaybackStatus/getSpeed(): PlaybackSpeedgetCurrentState(): 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.