UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

217 lines 9.28 kB
/** * Server-authoritative orchestrator with deterministic per-tick rollback. * * Encapsulates the algorithm previously inlined in * `app/src/prototype/network_prototype_prediction.js`: * * 1. Inbound action packets do NOT execute on arrival. Instead, a deferral * hook on the {@link Replicator} drops them into a per-tick pending log * tagged with `(client_frame, sender_id, type_id, payload_bytes)`. * 2. Once per server tick, `tick(current_frame)` consumes the pending log: * - Rejects entries older than the action_log's rewindable window. * - Determines the oldest pending frame `replay_start`. * - Rewinds the server world back to end-of-(replay_start - 1) via the * action_log's prior-state captures (see {@link RewindEngine}). * - Replays forward `[replay_start, current_frame]`. For each frame `f`: * - Read historical actions out of `action_log[f]` BEFORE * `begin_frame(f)` recycles the buffer. * - Append pending entries for `f`. * - Stable-sort the merged list by `sender_id` (so multi-client order * is deterministic across peers — relies on per-record sender_id in * the action log). * - `executor.execute` each in sorted order, then run the user- * supplied local sim via {@link onLocalSim}. * - Clear pending. * * Net effect: a client action tagged at client tick K lands on the server as * if applied against end-of-K-1 server state, regardless of arrival timing * or order. Both peers compute identical world states for identical inputs. * * The user wires this orchestrator up by: * 1. Constructing with a world, replication setup, and (typically) an * {@link OwnerAwareScope} filter so client-owned-entity actions are * not echoed back. * 2. Calling {@link connect_peer} for each connected client. * 3. Subscribing to {@link onLocalSim} for per-frame server-side game logic * (e.g. clamps, physics, collision) and {@link onTickComplete} for * end-of-tick work (e.g. sending AUTH_STATE). * 4. Calling {@link tick} once per server frame. * * @author Alex Goldring * @copyright Company Named Limited (c) 2025 */ export class ServerAuthoritativeServer { /** * @param {{ * world: EntityComponentDataset, * binary_registry: BinarySerializationRegistry, * replicated_components: Function[], * action_classes: Function[], * scope_filter?: object, * frame_capacity?: number, * initial_buffer_size?: number, * simulation_delay_ticks?: number, * }} options * * `simulation_delay_ticks`: server-side input buffer (Overwatch-style). * When > 0, {@link tick}(wall_frame) simulates * `sim_frame = wall_frame - simulation_delay_ticks`. Pending actions * tagged for `> sim_frame` stay queued and apply when `sim_frame` * catches up — most client actions land before the server needs * them, so rollback rarely fires. Costs N ticks of perceived * input-to-feedback latency (client prediction hides it). Default 0 * reproduces pre-feature behavior. */ constructor({ world, binary_registry, replicated_components, action_classes, scope_filter, frame_capacity, initial_buffer_size, simulation_delay_ticks, }: { world: EntityComponentDataset; binary_registry: BinarySerializationRegistry; replicated_components: Function[]; action_classes: Function[]; scope_filter?: object; frame_capacity?: number; initial_buffer_size?: number; simulation_delay_ticks?: number; }); /** @type {NetworkPeer} */ peer: NetworkPeer; /** @type {EntityComponentDataset} */ world: EntityComponentDataset; /** @type {SimActionExecutor} */ executor: SimActionExecutor; /** @type {ReplicationSlotTable} */ slot_table: ReplicationSlotTable; /** @type {ActionLog} */ action_log: ActionLog; /** @type {SimActionRegistry} */ action_registry: SimActionRegistry; /** @type {ReplicatedComponentRegistry} */ component_registry: ReplicatedComponentRegistry; /** @type {RewindEngine} */ rewind_engine: RewindEngine; /** * Highest client-tagged frame received (across all peers). * RECEIPT semantic — under `simulation_delay_ticks > 0` this * may be ahead of {@link current_sim_frame}. Tag AUTH_STATE * with `current_sim_frame`, not this. * @type {number} */ last_client_frame_processed: number; /** * Number of ticks the server's simulation lags behind the caller's * wall frame. See constructor doc. * @type {number} */ simulation_delay_ticks: number; /** * Most recently simulated frame. `-1` during warmup * (`wall_frame < simulation_delay_ticks`), advances by 1 per * `tick()` after. * @type {number} */ current_sim_frame: number; /** * Fires once per frame during the replay loop, AFTER all merged * actions for that frame have been applied and BEFORE the action_log * frame is closed. Use this for server-authoritative local sim * (clamps, collision, AI step, etc.). The current world state * reflects all inputs for this frame; the local sim should be * idempotent under repeated application (because it may run many * times under rollback). * * Args: `(frame_number)`. * @type {Signal} */ onLocalSim: Signal; /** * Fires after `tick()` completes (pending drained, all frames in the * replay window applied). Use for end-of-tick work: sending * AUTH_STATE, recording diagnostics, etc. * * Args: `(current_frame)`. * @type {Signal} */ onTickComplete: Signal; /** * Fired when a tick triggers an actual rewind (i.e. the replay * window includes already-committed frames). Args: * `(committed_top, replay_target, depth)` where depth is * `committed_top - replay_target` (number of frames undone). * * Diagnostic: counts rollbacks and characterizes how deep they * went, which directly maps to how late client actions arrived. * Tick paths that ran without a rewind (steady-state, no pending * for past frames) do NOT fire this. * @type {Signal} */ onRewind: Signal; /** * Connect a client peer over a transport. Subsequent action packets from * this peer will be buffered in the pending log via the deferral hook * and consumed on the next `tick()`. * * @param {number} peer_id * @param {object} transport */ connect_peer(peer_id: number, transport: object): void; /** * Disconnect a previously-connected peer. * @param {number} peer_id */ disconnect_peer(peer_id: number): void; /** * Forward to {@link NetworkPeer#send_auth_state}. Typically called from * an {@link onTickComplete} subscriber. * * @param {number} peer_id * @param {number} frame_number * @param {number} network_id * @param {(buf: BinaryBuffer) => void} write_fn */ send_auth_state(peer_id: number, frame_number: number, network_id: number, write_fn: (buf: BinaryBuffer) => void): void; /** * Channel accessor for the user's manual packet plumbing (e.g. sending * empty ACK packets in the prototype). Most callers won't need this. * @param {number} peer_id */ channel_for(peer_id: number): import("../transport/Channel.js").Channel; /** * Send the current buffer-depth observation for one peer to that peer. * Convenience wrapper around {@link NetworkPeer#send_time_dilation_feedback} * that pulls the depth from {@link buffer_depth_for_peer}. Typically * invoked once per tick (or every few ticks) from an * {@link onTickComplete} subscriber. * * @param {number} peer_id */ send_time_dilation_feedback(peer_id: number): void; /** * Drive the rollback flow for one tick. * * With `simulation_delay_ticks = 0` (default), simulates the caller's * `wall_frame` and drains pending. With `> 0`, simulates * `sim_frame = wall_frame - simulation_delay_ticks` and holds * `frame > sim_frame` pending until the sim catches up. Warmup * (`sim_frame < 0`) is a no-op; `onTickComplete` does not fire. * * `onTickComplete` fires with `sim_frame` (not `wall_frame`). * * @param {number} current_frame the caller's wall frame */ tick(current_frame: number): void; /** * Current input-buffer depth for a peer: `max_received_frame - current_sim_frame`. * Positive ⇒ peer is sending ahead of sim (good); zero/negative ⇒ * peer is falling behind. Returns 0 if no inputs from this peer. * During warmup returns the count of received frames. * * @param {number} peer_id * @returns {number} */ buffer_depth_for_peer(peer_id: number): number; #private; } import { NetworkPeer } from "./NetworkPeer.js"; import { RewindEngine } from "../sim/RewindEngine.js"; import Signal from "../../../core/events/signal/Signal.js"; import { BinaryBuffer } from "../../../core/binary/BinaryBuffer.js"; //# sourceMappingURL=ServerAuthoritativeServer.d.ts.map