first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,104 @@
# ReplayPlayer
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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
```ts
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
```ts
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.