@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
165 lines • 7.02 kB
TypeScript
/**
* 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