Documentation
    Preparing search index...

    Class ReplayPlayer

    Plays a §34 replay document back into a ReplayTarget, with the §113 inspection controls.

    A player is a small state machine over one recording: constructed (compatible with the target, but the target untouched), loaded (initial snapshot restored, step 0), and then advanced by whichever control the caller prefers — all of them are the same fixed steps in the same order, so they can be mixed freely:

    Control Advances by
    ReplayPlayer.stepOnce exactly one fixed step
    ReplayPlayer.advanceFrame to a recorded frame boundary (§34)
    ReplayPlayer.advanceRealtime the §10 accumulator's worth of steps
    ReplayPlayer.seekToStep to any step, forwards or backwards
    const player = new ReplayPlayer(recording, {
    target: world,
    stepFn: (dt) => { world.step(dt); },
    });
    player.load();
    while (player.stepOnce()) {
    // inspect world here — one fixed step at a time (§113)
    }
    player.verifyChecksum(); // true when the run reproduced the recording (§33)
    Index
    • Validates the pairing of recording and target, without touching either's state.

      Three things are checked here rather than at the first step, so a mis-wired replay fails at construction with a useful message:

      1. the document is a valid §34 recording (it is re-validated even when it came from decodeReplayRecording, so a hand-built object cannot smuggle a bad field into the player — the cost is one deep copy at construction);
      2. §34's refusal — the recording's adapter name and version must match the target's, compared against a fresh target.createSnapshot() (the only way to read a target's adapter identity; the snapshot's bytes are discarded and the target is not modified);
      3. a recording with inputs needs a target that can apply them. applyInput is optional on ReplayTarget — PhysicsWorld does not have it — and replaying recorded inputs into a target that cannot receive them would diverge silently, which §42's "warn rather than silently overwrite" instinct forbids just as much here.

      Parameters

      Returns ReplayPlayer

      FourError INVALID_APPLICATION_STATE on an adapter mismatch (§34), or when the recording has inputs and the target has no applyInput

      FourError SERIALIZATION_VERSION_MISMATCH if the document's formatVersion is not this build's

      TypeError if the document is malformed, or stepFn is not a function

      RangeError if maximumSubSteps is not an integer >= 1

    onStep: ReplayStepListener | undefined

    Called after every replayed fixed step, including those a ReplayPlayer.seekToStep re-simulates.

    Assignable after construction, in the style of Scheduler.onFixedStep. When it is undefined a replayed step allocates nothing and computes no checksum.

    recording: ReplayRecording

    The document being replayed, in canonical, frozen form (§34).

    verifyChecksums: boolean

    Whether ReplayPlayer.onStep events carry a per-step checksum.

    Cost: off (the default) a step is applyInput? + stepFn and nothing else. On, every step additionally calls target.checksum(), which for a real solver hashes the whole world — the dominant cost of the replay, easily larger than the step itself. Turn it on to bisect a divergence, off to watch a replay run.

    It is a hook, not a check: §34's envelope stores one checksum for the whole run (ReplayRecording.finalChecksum), so the player has nothing per-step to compare against and does not pretend otherwise. What the hook buys is the ability to record two runs' per-step digests and find the first step where they differ — which is how a determinism defect (§33) gets located rather than merely detected.

    • get accumulator(): number

      Real time accumulated toward the next step, in seconds; always < fixedDeltaTime except immediately after a clamp (§10).

      Returns number

    • get droppedTime(): number

      Replay time the maximumSubSteps clamp discarded since ReplayPlayer.load, in seconds — §10's TimeState.droppedTime for the replay clock.

      Time that arrives after the recording is exhausted is not counted: there is nothing left to simulate, so nothing was dropped.

      Returns number

    • get framesCompleted(): number

      Recorded frames fully replayed so far.

      A frame counts as completed once every one of its steps has run, so a frame that recorded zero steps (§10 runs none when the accumulator is short) completes as soon as the player reaches its boundary.

      Returns number

    • get interpolationAlpha(): number

      The §10 interpolation alpha of the replay clock, in [0, 1] — how far the accumulator sits between the last replayed step and the next.

      Offered for a host that renders an interpolated pose while scrubbing (§43). Like every other alpha it must never feed back into simulation state (§42).

      Returns number

    • get speed(): number

      Playback rate for ReplayPlayer.advanceRealtime: 0.25 is quarter speed (§113 slow motion), 1 real time, 4 fast-forward.

      This is timeScale on the replay clock (P10-3), not on the simulation: the fixed step never changes, only how much recorded time one second of real time is worth. 0 is legal and freezes playback while leaving ReplayPlayer.isPlaying true; negative values are rejected, because running the recorded steps backwards is not something a solver can do — step backwards with ReplayPlayer.seekToStep, which re-simulates.

      Returns number

      RangeError if set to anything but a finite number >= 0

    • set speed(value: number): void

      Parameters

      • value: number

      Returns void

    • get totalSteps(): number

      Steps in the recording — the sum of every recorded frame's stepCount (§34), and the step index at which the run's finalChecksum was taken.

      Returns number

    • Replays whole recorded frames — the §113 "frame by frame" control.

      With no argument it advances to the next frame boundary at or after the current step, finishing a partially replayed frame rather than running a whole extra one. With a frameIndex it advances to the end of that frame, which may be several frames ahead.

      A frame is §34's record of one rendered frame: the fixed steps §10's accumulator ran for it, which is 0 when the frame was short and up to maximumSubSteps when it was long. Advancing by frames therefore replays the run's original step pattern, which is what a defect that only appears on a long frame needs.

      Parameters

      • OptionalframeIndex: number

        the frame to stop at the end of; omit for the next one

      Returns number

      fixed steps replayed (0 at the end of the recording, or when the requested frame ends exactly where the player already is)

      FourError INVALID_APPLICATION_STATE if not loaded

      RangeError if frameIndex is not an index of a recorded frame, or names a frame that ended before the current step — advancing never goes backwards; ReplayPlayer.seekToStep does

    • Advances playback by elapsedSeconds of the host's real time, running whole fixed steps out of §10's accumulator.

      The rules, mirroring Scheduler.step so a replayed run behaves like a live one, and reported here because a caller counting steps deserves to know them:

      1. Paused ⇒ nothing. No accumulation, no steps, 0 returned. Real time spent paused is not owed to the replay later.
      2. Scaled accumulation. accumulator += elapsedSeconds * speed. Slow motion is a smaller number of accumulated seconds, never a smaller fixed step (§10: the fixed step is immutable).
      3. Whole steps only, while accumulator >= fixedDeltaTime, at most ReplayPlayer.maximumSubSteps of them per call, and never past the end of the recording.
      4. The clamp. If steps remain owed after the sub-step limit, the excess beyond one fixed step is added to ReplayPlayer.droppedTime and the accumulator is pinned at fixedDeltaTime (alpha 1) — §10's bound on how far a single call can fall behind, so a debugger pause in the middle of an inspection session does not produce a burst of simulation afterwards.
      5. End of recording. When the last step has run, the accumulator is cleared instead of clamped: leftover real time is not dropped simulation, there is simply no more recording, and counting it as droppedTime would misreport a finished replay as a stuttering one.

      Parameters

      • elapsedSeconds: number

        real time since the previous call, in seconds

      • speed: number = ...

        per-call override of ReplayPlayer.speed; the property itself is unchanged

      Returns number

      fixed steps replayed by this call

      FourError INVALID_APPLICATION_STATE if not loaded

      RangeError if either number is not finite and >= 0

    • Restores the recording's initial snapshot and rewinds to step 0.

      Idempotent: calling it twice restores the same bytes and leaves the same state, so it doubles as "restart this replay". It resets the replay clock (accumulator and ReplayPlayer.droppedTime) but deliberately does not touch ReplayPlayer.isPlaying or ReplayPlayer.speed (decision, WP-10.2): those describe the transport the caller is operating, not the recording, and a looping player that had to re-press play after every rewind would be the surprising design.

      Returns void

    • Moves to any step of the recording, forwards or backwards, by restoring the nearest snapshot at or before it and re-simulating the remainder (§34).

      The snapshot chosen is the last periodic snapshot whose step is <= step, or the initial snapshot when there is none — so with a recorder configured for snapshotIntervalSteps: N, a seek costs fewer than N steps of re-simulation instead of up to totalSteps of it. That is the entire reason §34 has periodic snapshots, and the reason the player prefers them even when seeking forwards.

      Seeking always restores, even for a forward seek the player could reach by simply stepping (decision, WP-10.2). The player does not assume the target's live state is the state the recording says it should be: the host owns that world and may have poked it between calls, and a seek that silently kept a poked state would report a divergence at the wrong step. Restoring is also what makes the operation idempotent, which a scrub bar needs.

      Re-simulation applies the recorded inputs exactly as playback does, so by §33 the state reached is the state playback would have reached.

      Parameters

      • step: number

        the target step, in [0, totalSteps]

      Returns number

      fixed steps re-simulated — the cost of the seek, and what a test asserts on to prove the nearest snapshot was used

      FourError INVALID_APPLICATION_STATE if not loaded

      RangeError if step is not an integer in [0, totalSteps]

    • Replays exactly one fixed step: the inputs recorded for the current step are applied first, in insertion order, then stepFn runs (§34).

      At the end of the recording this is a no-op returning false, not a throw (decision, WP-10.2). Reaching the end is the expected outcome of a replay, not a misuse: while (player.stepOnce()) { … } is the natural inspection loop, and it only reads that way if the terminal call is ordinary. Misuse — stepping a player that was never loaded — still throws.

      Returns boolean

      whether a step ran

      FourError INVALID_APPLICATION_STATE if ReplayPlayer.load has not run

    • §33's gate: does the replayed run end on the checksum the recording ended on?

      Only meaningful at the end of the recording — that is the one step index §34 stored a checksum for — so calling it early is a usage error rather than a false, which would be indistinguishable from a real divergence.

      Returns boolean

      whether target.checksum() equals the recording's finalChecksum

      FourError INVALID_APPLICATION_STATE if not loaded, or if the player has not replayed every step