UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

389 lines 16.7 kB
/** * Wire packet types. The first byte of every NetworkPeer-routed payload picks * the dispatch path. New types can be added without breaking the action stream * because old peers will just see an unknown type and drop the packet. */ export type NetworkPacketType = number; export namespace NetworkPacketType { let ACTION_STREAM: number; let RECOVERY_REQUEST: number; let STATE_BURST: number; let AUTH_STATE: number; let FRAGMENT: number; let RELIABLE_COMMAND: number; let NACK_FRAGMENT: number; let TIME_DILATION: number; let INITIAL_SYNC: number; let RESUME_HELLO: number; let RESUME_ACCEPT: number; let RESUME_REJECT: number; let DISCONNECT: number; } /** * Reason codes for {@link NetworkPacketType.RESUME_REJECT }. */ export type ResumeRejectReason = number; /** * Reason codes for {@link NetworkPacketType.RESUME_REJECT}. * @readonly @enum {number} */ export const ResumeRejectReason: Readonly<{ UnknownPeer: 0; GraceExpired: 1; TokenMismatch: 2; PeerIdCollision: 3; }>; /** * Per-tick orchestrator that wires the action infrastructure to one or more * peers over a Transport. * * Holds a single world (`EntityComponentDataset`) plus the action machinery * (executor, replicator, action log). For each connected peer it owns a * `Channel` and a `seq → frame_end` map so that ack notifications advance the * `Baseline.last_acked(peer_id)` watermark. * * Use as both server (many connected peers) and client (one peer = the server). * The asymmetry is in *who* connects — the orchestrator itself is symmetric. * * Lifecycle: * 1. `connect_peer(peer_id, transport)` — wire a transport for a peer. * 2. Per tick: * - `begin_tick(frame_number)` — open the action log for this frame. * - User code runs game logic, calling `executor.execute(action)` for each * replicated state mutation. Inbound actions from peers also flow into * the executor automatically (via the channel.onPayload wiring). * - `end_tick()` — close the frame and dispatch outgoing packets to every peer. * 3. `disconnect_peer(peer_id)` — release the channel + baseline state. * * @author Alex Goldring * @copyright Company Named Limited (c) 2025 */ export class NetworkPeer { /** * @param {{ * world: EntityComponentDataset, * binary_registry: BinarySerializationRegistry, * replicated_components: Function[], * action_classes: Function[], * scope_filter?: { is_entity_in_scope(peer_id: number, network_id: number): boolean }, * frame_capacity?: number, * initial_buffer_size?: number, * mutation_ledger?: MutationLedger|null, * changed_set_capacity?: number, * }} options */ constructor({ world, binary_registry, replicated_components, action_classes, scope_filter, frame_capacity, initial_buffer_size, mutation_ledger, changed_set_capacity, }: { world: EntityComponentDataset; binary_registry: BinarySerializationRegistry; replicated_components: Function[]; action_classes: Function[]; scope_filter?: { is_entity_in_scope(peer_id: number, network_id: number): boolean; }; frame_capacity?: number; initial_buffer_size?: number; mutation_ledger?: MutationLedger | null; changed_set_capacity?: number; }); /** @type {EntityComponentDataset} */ world: EntityComponentDataset; /** @type {ReplicatedComponentRegistry} */ component_registry: ReplicatedComponentRegistry; /** @type {SimActionRegistry} */ action_registry: SimActionRegistry; /** @type {ReplicationSlotTable} */ slot_table: ReplicationSlotTable; /** @type {ActionLog} */ action_log: ActionLog; /** * Optional per-tick mutation tracker. When a `MutationLedger` is supplied, * this peer records each tick's affected network IDs into a fresh * `ChangedEntitySet`, then compacts the set into the ledger at `end_tick`. * Server-side peers should provide a ledger so they can answer recovery * queries. Receive-only client peers don't need one. * * @type {MutationLedger|null} */ mutation_ledger: MutationLedger | null; /** @type {SimActionExecutor} */ executor: SimActionExecutor; /** @type {Replicator} */ replicator: Replicator; /** @type {Baseline} */ baseline: Baseline; /** * Fired when an inbound packet is a `RECOVERY_REQUEST` (peer is asking * which entities mutated in a tick range so it can request their state). * Args: `(peer_id, start_tick, end_tick)`. Subscriber typically responds * via {@link send_state_burst_for_range}. * @type {Signal} */ onRecoveryRequest: Signal; /** * Fired when an inbound packet is a `STATE_BURST` carrying current state * for a list of entities (response to a recovery request, or push). * Args: `(peer_id, buffer, length)` where `buffer` is positioned just * past the packet-type prefix and `length` is the byte count of the * burst payload (not counting the prefix). Apply via * {@link snapshotter_apply_to_existing}. * @type {Signal} */ onStateBurst: Signal; /** * Fired when an inbound packet is an `AUTH_STATE` (server's authoritative * state for one entity, tagged with the frame the state represents). * Args: `(peer_id, frame_number, network_id, buffer)` where `buffer` is * positioned at the start of the entity's state payload. The handler * is responsible for deserializing the bytes and triggering whatever * reconciliation policy the application wants (snap, rewind+replay). * @type {Signal} */ onAuthState: Signal; /** * Fires on TIME_DILATION packet arrival. * Args: `(peer_id, buffer_depth)` — signed int16. * @type {Signal} */ onTimeDilationFeedback: Signal; /** * Fires on INITIAL_SYNC packet arrival. * Args: `(peer_id, session_token, frame_number, buffer, payload_end)`. * `session_token` is a `Uint8Array(16)` (UUID v1 raw bytes); the * buffer is positioned at the start of the Snapshotter payload. * @type {Signal} */ onInitialSync: Signal; /** * Fires on RESUME_HELLO arrival (host-only path in practice). * Args: `(peer_id, local_peer_id, last_acked_frame, session_token)`. * Subscriber decides whether to accept (call * {@link send_resume_accept}) or reject (call * {@link send_resume_reject}). * @type {Signal} */ onResumeHello: Signal; /** * Fires on RESUME_ACCEPT arrival (client-only in practice). * Args: `(peer_id)`. * @type {Signal} */ onResumeAccept: Signal; /** * Fires on RESUME_REJECT arrival (client-only in practice). * Args: `(peer_id, reason_code)` — see {@link ResumeRejectReason}. * @type {Signal} */ onResumeReject: Signal; /** * Fires on DISCONNECT arrival. * Args: `(peer_id, reason_label)` — `reason_label` is a string, * opaque to the engine. * @type {Signal} */ onDisconnectPacket: Signal; /** * Fired when a reliable command arrives from a peer (delivered via * {@link ReliableCommandPipeline}, dedup'd against retransmits). * * Args: `(peer_id, buffer, payload_offset, payload_length)`. The * buffer is the per-peer scratch and may be reused after the handler * returns; consumers must copy the payload bytes if they need them * to outlive the call. * * Used for chat, level transitions, lobby/room state — application- * level messages that need at-least-once delivery semantics rather * than the action stream's best-effort. * @type {Signal} */ onReliableCommand: Signal; /** * Wire `transport` to the peer identified by `peer_id`. Creates a Channel * over the transport and connects ack/payload notifications to the local * Baseline / Replicator. * * @param {number} peer_id * @param {{ send(bytes: Uint8Array, length: number): void, onReceive: any }} transport */ connect_peer(peer_id: number, transport: { send(bytes: Uint8Array, length: number): void; onReceive: any; }): void; /** * Unwire and forget a peer. * @param {number} peer_id */ disconnect_peer(peer_id: number): void; /** * Whether `peer_id` is currently connected. * @param {number} peer_id * @returns {boolean} */ is_connected(peer_id: number): boolean; /** * Direct access to a peer's channel (for sending acks-only packets, raw payloads, etc.). * @param {number} peer_id * @returns {Channel|undefined} */ channel_for(peer_id: number): Channel | undefined; /** * Open the action log for `frame_number`. Call before running game logic * for the tick. The user's code (or the engine's `EntityManager.simulate`) * runs between `begin_tick` and `end_tick`, calling `executor.execute()` * for each replicated state mutation. * * @param {number} frame_number */ begin_tick(frame_number: number): void; /** * Close the current tick and send pending action records to every peer. * For each peer, packs frames `[Baseline.last_acked(peer) + 1, current_frame]` * and sends via the channel; records the seq→frame_end mapping so the next * ack from that peer advances the baseline. * * If a {@link MutationLedger} was supplied at construction, this tick's * accumulated changed-entity set is also compacted into the ledger. */ end_tick(): void; /** * Pack and dispatch the outbound action stream for `current_frame` to * every connected peer. Second half of {@link end_tick} — exposed * separately so orchestrators that manage action_log frames directly * (e.g. {@link ServerAuthoritativeServer}'s rollback flow, which * begin_frame/end_frames many frames per tick) can drive the * outbound dispatch without going through begin_tick/end_tick. * * Assumes the action_log frame for `current_frame` is already CLOSED * by the caller. Records the tick's accumulated mutations into the * MutationLedger (if configured), then packs frames * `[last_acked+1, current_frame]` for each peer via * `Replicator.pack_for_peer`. Empty packs (no in-scope actions) are * not sent. Clears the per-tick changed-entity set after recording. * * @param {number} current_frame */ flush_outbound(current_frame: number): void; /** * Send a recovery request to `peer_id`: "tell me the current state of every * entity that mutated between `start_tick` and `end_tick`." The peer's * server-side handler ({@link onRecoveryRequest}) is expected to respond by * calling {@link send_state_burst_for_range}. * * Best-effort delivery via the same UDP-style channel — if the request is * lost, the client is expected to retry. * * @param {number} peer_id * @param {number} start_tick * @param {number} end_tick */ send_recovery_request(peer_id: number, start_tick: number, end_tick: number): void; /** * Server-side recovery response. Looks up which entities mutated in * `[start_tick, end_tick]` via the local `MutationLedger` (must be present), * then sends the current state of those entities to `peer_id` via a * `STATE_BURST` packet. Receiver applies via `snapshotter_apply_to_existing`. * * If no entities are in the range (or the range is older than the ledger * retains), nothing is sent. The requester should treat absence of response * after a timeout as "needs full re-init." * * @param {number} peer_id * @param {number} start_tick * @param {number} end_tick */ send_state_burst_for_range(peer_id: number, start_tick: number, end_tick: number): void; /** * Send the authoritative state of one entity to `peer_id`, tagged with * the frame the state represents. Used by client-side prediction: the * server emits this every tick (or every N ticks) for client-owned * entities; the client compares to its predicted state at the same frame * and reconciles via rewind+replay if they differ. * * The payload is whatever `write_fn(buffer)` writes — typically a * `BinaryClassSerializationAdapter.serialize(buffer, component)` call. * The receiver's `onAuthState` handler reads the same bytes via the * matching deserializer. * * @param {number} peer_id * @param {number} frame_number sender's frame the state corresponds to * @param {number} network_id which entity this state is for * @param {function(BinaryBuffer): void} write_fn writes the entity's state bytes */ send_auth_state(peer_id: number, frame_number: number, network_id: number, write_fn: (arg0: BinaryBuffer) => void): void; /** * Send an INITIAL_SYNC packet. `write_fn` receives the send-buffer * positioned just past the `(packet_type, session_token, frame_number)` * header and writes the Snapshotter payload. Fragments transparently. * * @param {number} peer_id * @param {Uint8Array} session_token 16-byte UUID v1 identifying this peer-session * @param {number} frame_number server sim frame this snapshot represents * @param {function(BinaryBuffer): void} write_fn */ send_initial_sync(peer_id: number, session_token: Uint8Array, frame_number: number, write_fn: (arg0: BinaryBuffer) => void): void; /** * Send a RESUME_HELLO packet to the server. Client-only direction. * * @param {number} peer_id remote peer (the server) * @param {number} local_peer_id the client's own peer id * @param {number} last_acked_frame the client's view of last-acked frame * @param {Uint8Array} session_token 16-byte token from a prior INITIAL_SYNC */ send_resume_hello(peer_id: number, local_peer_id: number, last_acked_frame: number, session_token: Uint8Array): void; /** * Send a RESUME_ACCEPT packet. Host-only direction. * @param {number} peer_id */ send_resume_accept(peer_id: number): void; /** * Send a RESUME_REJECT packet. Host-only direction. * @param {number} peer_id * @param {number} reason_code one of {@link ResumeRejectReason} */ send_resume_reject(peer_id: number, reason_code: number): void; /** * Send a DISCONNECT packet. Either direction. `reason_label` is an * opaque string (255 byte cap after UTF-8 encoding). * * @param {number} peer_id * @param {string} [reason_label] */ send_disconnect(peer_id: number, reason_label?: string): void; /** * Send a TIME_DILATION feedback packet. `buffer_depth` is clamped * to the int16 range — saturating is fine since the controller * doesn't need sub-int16 precision. * * @param {number} peer_id * @param {number} buffer_depth signed integer; clamped to int16 */ send_time_dilation_feedback(peer_id: number, buffer_depth: number): void; /** * Send a reliable command to a connected peer. Delivered at-least-once * with sender-side retransmit on Channel-detected loss and receiver- * side dedup by logical seq. Use for messages where the action-stream * model (best-effort, per-frame back-fill) isn't suitable — chat, * lobby/room state, level transitions. * * Returns the logical seq the pipeline assigned (diagnostic only; the * receiver dedups internally regardless). * * @param {number} peer_id * @param {Uint8Array} payload * @param {number} length * @returns {number} logical seq */ send_reliable_command(peer_id: number, payload: Uint8Array, length: number): number; #private; } import { ReplicatedComponentRegistry } from "../sim/ReplicatedComponentRegistry.js"; import { SimActionRegistry } from "../sim/SimActionRegistry.js"; import { ReplicationSlotTable } from "../state/ReplicationSlotTable.js"; import { ActionLog } from "../sim/ActionLog.js"; import { MutationLedger } from "../state/MutationLedger.js"; import { SimActionExecutor } from "../sim/SimActionExecutor.js"; import { Replicator } from "../replication/Replicator.js"; import { Baseline } from "../state/Baseline.js"; import Signal from "../../../core/events/signal/Signal.js"; import { Channel } from "../transport/Channel.js"; import { BinaryBuffer } from "../../../core/binary/BinaryBuffer.js"; //# sourceMappingURL=NetworkPeer.d.ts.map