Files
skills/docs/gaming/games/game-server/src/enhanced/replay/replay-player/replay-player.md
2026-05-29 08:20:56 +01:00

3.6 KiB
Raw Blame History

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 / 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

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): 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.