import { j as Space } from '../ConvexResult-Dq5DbvRj.cjs'; import '../PhysicsMetrics-QzFDRdGh.cjs'; /** * Per-frame callback that translates a user-defined input payload into engine * mutations on the supplied {@link Space}. Called by {@link Player} once per * frame during playback, BEFORE `space.step()`. * * The callback receives the payload exactly as supplied to * {@link Recorder.recordFrame} during recording — the recorder JSON-clones the * payload, so mutations made between frames don't leak into the log. * * @typeParam T - User input payload type (must be JSON-serializable). */ type ReplayInputApplier = (input: T, space: Space, frame: number) => void; /** Configuration options for {@link Recorder}. */ interface RecorderOptions { /** * Capture a full snapshot every N frames in addition to the initial one. * Enables fast random-access scrubbing in {@link Player.stepTo}: a backward * jump restores the latest keyframe at or before the target frame, then * steps forward through the input log. * * Set to `0` to disable keyframes (only the initial snapshot is captured). * Cost: each keyframe is roughly the size of one full snapshot * (~150-300 bytes per body), times `frameCount / keyframeEvery`. * * @default `60` (one keyframe per second at 60 fps) */ keyframeEvery?: number; } /** * A recorded simulation: an initial state snapshot plus a per-frame input log * (and optional intermediate keyframes for fast scrubbing). Lossless when * the user's `applyInput` callback is deterministic and `space.deterministic` * was set on the recording space. * * @typeParam T - User input payload type. */ interface Replay { /** Replay format version — bumped on breaking layout changes. */ readonly version: number; /** * Binary snapshot of the space at frame 0, before any input was applied or * any step was taken. Produced via `spaceToBinary`. */ readonly initialSnapshot: Uint8Array; /** * Sparse input log. Each entry is the payload supplied to * {@link Recorder.recordFrame} for the matching frame. Frames with no input * are absent from the array (saving space when most frames are idle). */ readonly inputs: ReadonlyArray>; /** * Intermediate snapshots for fast scrub. Always sorted by `frame` ascending * and never includes frame 0 (use {@link initialSnapshot} for that). Empty * when `keyframeEvery: 0` was used during recording. */ readonly keyframes: ReadonlyArray; /** * Total number of frames captured. After playback reaches `frameCount`, * the space has been stepped `frameCount` times. */ readonly frameCount: number; } /** One entry in the {@link Replay.inputs} log. */ interface ReplayFrameInput { /** Zero-based frame index — applied BEFORE the step at that frame. */ readonly frame: number; /** User-defined payload (deep-cloned from the value passed to `recordFrame`). */ readonly payload: T; } /** One entry in the {@link Replay.keyframes} list. */ interface ReplayKeyframe { /** Zero-based frame index. The space at this frame is the deserialised snapshot. */ readonly frame: number; /** Binary snapshot from `spaceToBinary` at that frame. */ readonly snapshot: Uint8Array; } /** Result of {@link validateDeterministicConfig}. */ interface DeterminismValidation { /** True if no critical issues were found. Warnings may still be present. */ ok: boolean; /** Human-readable diagnostic messages. */ warnings: string[]; } /** * Records a deterministic simulation by capturing an initial snapshot plus a * per-frame log of user-supplied inputs. The resulting {@link Replay} can be * encoded to binary, shared, and replayed elsewhere via {@link Player}. * * **Recording loop pattern:** * ```ts * const recorder = new Recorder(space); * for (let frame = 0; frame < N; frame++) { * const input = readUserInput(); * recorder.recordFrame(input); * applyInputToSpace(input, space); // user's own logic * space.step(1 / 60); * } * const replay = recorder.finish(); * ``` * * **For deterministic replay**, the recording space MUST satisfy: * - `space.deterministic = true` (set before any bodies are added). * - Step with a fixed `dt` and matching velocity / position iteration counts. * - Bodies must be added in the same order on the playback side — captured * automatically by the snapshot, but any post-restore logic must respect it. * - User's `applyInput` callback must be a pure function of `(input, space, frame)` * (no `Math.random()`, no wall-clock reads). * * @typeParam T - JSON-serializable user input payload. */ declare class Recorder { private readonly _initialSnapshot; private readonly _inputs; private readonly _keyframes; private readonly _keyframeEvery; private _frame; private _finished; private readonly _space; /** * Snapshot the supplied space at frame 0 and start a new recording. * The space reference is retained — keyframe captures sample its current * state, so the user MUST keep stepping the same space instance. */ constructor(space: Space, options?: RecorderOptions); /** Number of frames recorded so far. */ get frame(): number; /** True after {@link finish} has been called. Further recording throws. */ get finished(): boolean; /** * Log an input payload for the current frame, then advance the frame * counter. If `keyframeEvery > 0` and the new frame index is a multiple * of it, also captures a snapshot from the recording space. * * Pass `null` (or omit) for frames with no input — only frames with a * payload are stored, keeping the log compact. * * @param input - User payload. Deep-cloned via JSON before storage. */ recordFrame(input?: T | null): void; /** * Seal the recording and return the final {@link Replay} object. After * calling, further {@link recordFrame} calls throw. */ finish(): Replay; } /** Replay structure version — bumped on breaking layout changes. */ declare const REPLAY_VERSION = 1; /** * Plays back a {@link Replay} produced by {@link Recorder}. * * The player owns its own {@link Space} (deserialised from the replay's * initial snapshot). Each call to {@link step} advances exactly one frame — * applying the recorded input via the user's callback, then stepping physics. * * For random access, {@link stepTo} jumps to the nearest keyframe at or * before the target frame, then steps forward through the input log. Backward * jumps require keyframes to be present (otherwise the player must re-step * from frame 0, which still works but costs O(target) time). * * @typeParam T - User input payload type (must match the recorder's). */ declare class Player { private readonly _replay; private _applyInput; private _space; private _frame; private _velocityIterations; private _positionIterations; private _dt; /** * @param replay - Replay to play. * @param applyInput - Callback that translates input payloads into Space * mutations. Required for replays with non-empty input logs; replays with * only physics state (no user input) can pass `null`. * @param options - Playback config matching the recorder's step parameters. * Defaults: `dt = 1/60`, `velocityIterations = 8`, `positionIterations = 3`. */ constructor(replay: Replay, applyInput?: ReplayInputApplier | null, options?: { dt?: number; velocityIterations?: number; positionIterations?: number; }); /** * Restore the initial snapshot. Must be called before {@link step} or * {@link stepTo}. Re-calling rewinds to frame 0 (useful for looping). */ restore(): Space; /** The active space. Throws if {@link restore} has not been called. */ get space(): Space; /** Current frame index. 0 means "initial state, not yet stepped". */ get frame(): number; /** Total frames in the replay. */ get frameCount(): number; /** True when the player has reached the final frame. */ get finished(): boolean; /** The replay being played. */ get replay(): Replay; get applyInput(): ReplayInputApplier | null; set applyInput(fn: ReplayInputApplier | null); /** * Advance one frame: look up the input for the current frame (if any), * apply it via the callback, then step physics by `dt`. * * Throws if the player is at the end or {@link restore} hasn't been called. */ step(): void; /** * Jump to a specific frame. Forward jumps step through the log; backward * jumps restore the latest keyframe ≤ `target` (or the initial snapshot if * none) and step forward from there. * * After this call, {@link space} reflects the state AFTER `target` steps. * If `target === 0`, the space is the initial snapshot (pre-step). * * @param target - Target frame index in `[0, frameCount]`. */ stepTo(target: number): void; } /** * Binary encoding for {@link Replay} — pairs with {@link decodeReplay}. * * Format (little-endian unless noted): * - Magic "RPLY" (4 B, big-endian u32 0x52504C59) * - Version u16 * - Frame count u32 * - Initial snapshot: length u32, bytes * - Input count u32, then for each input: frame u32, JSON byte-length u32, UTF-8 bytes * - Keyframe count u32, then for each keyframe: frame u32, snapshot length u32, bytes * * The user payload is encoded as UTF-8 JSON. Binary payloads (e.g. ArrayBuffer) * are not supported directly — the user can base64-encode them into strings if * needed. */ /** Encode a {@link Replay} into a compact `Uint8Array`. */ declare function encodeReplay(replay: Replay): Uint8Array; /** Decode a `Uint8Array` produced by {@link encodeReplay} into a {@link Replay}. */ declare function decodeReplay(bytes: Uint8Array): Replay; /** * Inspect a {@link Space} and warn about configuration that prevents * deterministic replay. Pure inspection — does not mutate the space. * * Currently checks: * - `space.deterministic` is enabled. * - The space has been stepped at least once (sleep state can leak otherwise). * * Returns `{ ok: true }` with no warnings when the space looks ready for * recording; otherwise returns `ok: false` with a list of human-readable * messages. * * @example * ```ts * const { ok, warnings } = validateDeterministicConfig(space); * if (!ok) console.warn("Replay may drift:", warnings); * ``` */ declare function validateDeterministicConfig(space: Space): DeterminismValidation; export { type DeterminismValidation, Player, REPLAY_VERSION, Recorder, type RecorderOptions, type Replay, type ReplayFrameInput, type ReplayInputApplier, type ReplayKeyframe, decodeReplay, encodeReplay, validateDeterministicConfig };