UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

138 lines (126 loc) 4.29 kB
import { assert } from "../../../core/assert.js"; import { BinaryBuffer } from "../../../core/binary/BinaryBuffer.js"; /** * Append-only record of action bytes per frame. Useful for offline replay, * bug reproduction, and demos. * * Each entry stores a copy of the action bytes for one frame. The bytes * typically come from the per-frame `ActionLog` buffer (the action portion * after `Replicator.pack_for_peer` strips prior-state). The format on the wire * is the same one `Replicator.unpack_from_peer` consumes, so a recorded * session can be replayed by feeding the bytes back through a peer. * * Persistence to disk / IndexedDB / network is left to the application — * `serialize_to_buffer` produces a single contiguous `BinaryBuffer` snapshot. * * @author Alex Goldring * @copyright Company Named Limited (c) 2025 */ export class ReplayLog { #entries; constructor() { /** * @type {{frame: number, bytes: Uint8Array}[]} * @private */ this.#entries = []; } /** * Append a frame's bytes. The source array is copied; caller may reuse it. * * @param {number} frame * @param {Uint8Array} bytes * @param {number} length */ record(frame, bytes, length) { assert.isNonNegativeInteger(frame, 'frame'); assert.isNonNegativeInteger(length, 'length'); const copy = new Uint8Array(length); copy.set(bytes.subarray(0, length)); this.#entries.push({ frame, bytes: copy }); } /** * @returns {number} */ size() { return this.#entries.length; } /** * Iterate frames in `[start_frame, end_frame]` (inclusive). * @param {number} start_frame * @param {number} end_frame * @param {function(number, Uint8Array): void} callback receives (frame, bytes) */ for_each_in_range(start_frame, end_frame, callback) { assert.isNonNegativeInteger(start_frame, 'start_frame'); assert.isNonNegativeInteger(end_frame, 'end_frame'); assert.isFunction(callback, 'callback'); for (let i = 0; i < this.#entries.length; i++) { const e = this.#entries[i]; if (e.frame >= start_frame && e.frame <= end_frame) { callback(e.frame, e.bytes); } } } /** * Drop all entries. */ clear() { this.#entries.length = 0; } /** * Frame number of the first entry, or -1 if empty. */ earliest_frame() { return this.#entries.length === 0 ? -1 : this.#entries[0].frame; } /** * Frame number of the last entry, or -1 if empty. */ latest_frame() { return this.#entries.length === 0 ? -1 : this.#entries[this.#entries.length - 1].frame; } /** * Serialize the entire log to a fresh buffer. Format: * ``` * varint: entry_count * loop: * varint: frame * varint: bytes_length * bytes: payload * ``` * * @param {BinaryBuffer} [buffer] if provided, written into; otherwise a fresh one is created * @returns {BinaryBuffer} */ serialize_to_buffer(buffer) { const buf = buffer === undefined ? new BinaryBuffer() : buffer; buf.writeUintVar(this.#entries.length); for (let i = 0; i < this.#entries.length; i++) { const e = this.#entries[i]; buf.writeUintVar(e.frame); buf.writeUintVar(e.bytes.length); buf.writeBytes(e.bytes, 0, e.bytes.length); } return buf; } /** * Build a ReplayLog from a previously-serialized buffer. Reads from the * buffer's current position; advances the position past the consumed bytes. * * @param {BinaryBuffer} buffer * @returns {ReplayLog} */ static deserialize_from_buffer(buffer) { const log = new ReplayLog(); const count = buffer.readUintVar(); for (let i = 0; i < count; i++) { const frame = buffer.readUintVar(); const length = buffer.readUintVar(); const bytes = new Uint8Array(length); buffer.readUint8Array(bytes, 0, length); log.#entries.push({ frame, bytes }); } return log; } }