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