UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

165 lines 7.02 kB
/** * Per-tick recording of replicated component state for snapshot interpolation. * * Two ring buffers — both wrap at tick boundaries only: * * - `__buffer` (BinaryBuffer): raw component bytes, written via `begin_record` * / `end_record`. Variable size per record. * * - `__records` (Uint32Array): the metadata table, holding one row per * `(network_id, type_id, byte_offset, byte_length)` plus a bloom-filter * header every {@link METADATA_PAGE_SIZE} records. The structure: * * page = [bloom_0..7] [rec_0_n, _t, _o, _l] ... [rec_38_n, _t, _o, _l] * * A new page header is written at `begin_tick` and then again every 39 * records within a tick. The bloom filter is keyed by `network_id`, so a * `locate(network_id, type_id)` can skip whole pages with a single 256-bit * check before falling back to a per-record scan inside the page. Pages * never straddle tick boundaries. * * Per-tick metadata is small: byte range + word range in `__records` + record * count. The oldest tick is tracked explicitly (`__oldest_tick`) so the squash * check at each {@link begin_tick} is amortized O(1) — squash the oldest while * it overlaps the new write region, advance the pointer, repeat. * * Pair with {@link BinaryInterpolationAdapter} concrete subclasses (e.g. * {@link Vector3InterpolationAdapter}) which know each component type's * encoding and blend math. * * Typical receive-side use: * ``` * onFrameApplied(_, frame_number) { * log.begin_tick(frame_number); * const buf = log.begin_record(network_id, type_id); * adapter.serialize(buf, value); * log.end_record(); * log.end_tick(); * } * ``` * * Typical render-time use: * ``` * const ok = log.interpolate(out_buffer, network_id, type_id, tick_a, tick_b, t, adapter); * if (ok) { /* deserialize from out_buffer into the live component } * ``` * * @author Alex Goldring * @copyright Company Named Limited (c) 2025 */ export class InterpolationLog { /** * @param {{ buffer_capacity_bytes?: number, records_capacity?: number }} [options] * `records_capacity` is in *records* (not bytes); the underlying word * array is sized large enough to fit that many records plus their pages' * bloom-filter headers. */ constructor({ buffer_capacity_bytes, records_capacity }?: { buffer_capacity_bytes?: number; records_capacity?: number; }); /** @readonly @type {number} */ readonly buffer_capacity: number; /** @readonly @type {number} */ readonly records_capacity: number; /** * Open a new tick for recording. May wrap either ring at this point — but * never mid-tick, so a single tick's data is always contiguous in both * the byte buffer and the metadata table. After wrapping, any old tick * whose data overlaps the new write region is squashed (in oldest-first * order) and `__oldest_tick` advances. * * @param {number} tick non-negative integer */ begin_tick(tick: number): void; /** * Begin recording one component. Returns the underlying byte buffer * positioned at the next write slot — the caller writes the component * payload directly (e.g. via a `BinaryClassSerializationAdapter.serialize` * call), then calls {@link end_record} to finalize the (offset, length). * * Splitting begin/end (vs. taking a write callback) avoids the per-record * closure allocation and lets the caller pass the buffer straight to an * adapter without wrapping. * * @param {number} network_id * @param {number} type_id component type id (small integer; matches the registry) * @returns {BinaryBuffer} the log's byte buffer, positioned for writing the payload */ begin_record(network_id: number, type_id: number): BinaryBuffer; /** * Finalize the in-progress record opened by {@link begin_record}. Reads * the buffer's current position to compute the payload length, inserts * the network_id into the current page's bloom filter, writes the row to * the metadata table, and advances the cursors. */ end_record(): void; /** * Close the currently-open tick. Updates the max-observed-tick sizes so * the next {@link begin_tick} can size its wrap heuristic correctly. * * Throws if a record is still open — silently dropping the in-progress * record's metadata (offset/length) would leak buffer space and produce * stale bytes that no one can locate. */ end_tick(): void; /** * @param {number} tick * @returns {boolean} true if the tick has live (non-squashed) data */ has_tick(tick: number): boolean; /** * Find a component's stored slice within a tick. Walks the tick's pages, * bloom-filter-skipping pages that don't contain `network_id`, and * scanning records inside any page where the bloom hits. * * On success, writes `out[out_offset+0] = byte_offset` and * `out[out_offset+1] = byte_length`. * * @param {number[]|Uint32Array} out * @param {number} out_offset * @param {number} tick * @param {number} network_id * @param {number} type_id * @returns {boolean} false if the tick is missing/squashed or doesn't carry this component */ locate(out: number[] | Uint32Array, out_offset: number, tick: number, network_id: number, type_id: number): boolean; /** * Interpolate one component between two ticks via `adapter` and write the * result to `out_buffer` at its current position. * * Outcome by available data: * - Both ticks carry the component → adapter blends them at `t`. * - Only one carries it → adapter is called with the same offset twice * (snap to the surviving snapshot; `t` is irrelevant). * - Neither carries it → returns false; `out_buffer` is unchanged. * * @param {BinaryBuffer} out_buffer * @param {number} network_id * @param {number} type_id * @param {number} tick_a * @param {number} tick_b * @param {number} t * @param {BinaryInterpolationAdapter} adapter * @returns {boolean} true if a payload was written; false if neither tick has the component */ interpolate(out_buffer: BinaryBuffer, network_id: number, type_id: number, tick_a: number, tick_b: number, t: number, adapter: BinaryInterpolationAdapter): boolean; /** * Number of ticks currently held. Squashed ticks are excluded. * @returns {number} */ size(): number; /** * Tick number of the oldest live tick, or -1 if empty. * @returns {number} */ oldest_tick(): number; /** * Drop all recorded ticks. Resets both ring cursors to 0. Useful on * reconnect or level transition. */ clear(): void; #private; } import { BinaryBuffer } from "../../../core/binary/BinaryBuffer.js"; //# sourceMappingURL=InterpolationLog.d.ts.map