first commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user