UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

386 lines 15.8 kB
/** * Roles a session can occupy. The orchestrator it wires up underneath is * uniquely determined by the role. * * - `'client'` → {@link ServerAuthoritativeClient }: predicts locally * when an input sampler is registered; reconciles to * server-authoritative state arriving via AUTH_STATE. * With no input sampler, a client is a pure spectator * — it receives the action stream, smooths via * interpolation, and never predicts or rewinds. This * is the default role and covers both "playing" and * "spectating" use cases. * - `'host'` → {@link ServerAuthoritativeServer }: simulation * authority for one or more clients; runs the optional * server-side input buffer (`simulation_delay_ticks`). */ export type NetworkSessionRole = string; /** * Roles a session can occupy. The orchestrator it wires up underneath is * uniquely determined by the role. * * - `'client'` → {@link ServerAuthoritativeClient}: predicts locally * when an input sampler is registered; reconciles to * server-authoritative state arriving via AUTH_STATE. * With no input sampler, a client is a pure spectator * — it receives the action stream, smooths via * interpolation, and never predicts or rewinds. This * is the default role and covers both "playing" and * "spectating" use cases. * - `'host'` → {@link ServerAuthoritativeServer}: simulation * authority for one or more clients; runs the optional * server-side input buffer (`simulation_delay_ticks`). * * @readonly @enum {string} */ export const NetworkSessionRole: Readonly<{ Client: "client"; Host: "host"; }>; /** * High-level networking facade. Wraps the orchestrator + state machinery * into one config + lifecycle surface. The lower-level orchestrators * remain reachable through `session.server` / `session.client` / * `session.peer` for callers that need to override defaults post-hoc. * * Replication is component-driven: attaching a {@link NetworkIdentity} * to an entity registers it with the session via an `EntityObserver`. * Mutation is event-driven: callers fire * `dataset.sendEvent(entity, "net_mutate_component", payload)`. * * Per-tick driving is a single `session.tick(dt_seconds)`. The session * runs fixed-timestep stepping, client time dilation, server-side input * buffer, AUTH_STATE dispatch, time-dilation feedback, and interpolated * rendering for remote-owned entities. * * @author Alex Goldring * @copyright Company Named Limited (c) 2025 */ export class NetworkSession { /** * @param {{ * entity_manager: EntityManager, * transport?: object, * transport_factory?: (() => object), * role?: 'client'|'host', * local_peer_id?: number, * simulation_delay_ticks?: number, * tick_rate_hz?: number, * binary_registry?: BinarySerializationRegistry, * scope_filter?: object, * time_dilation?: TimeDilation, * adaptive_render_delay?: AdaptiveRenderDelay, * server_resume_grace_ms?: number, * reconnect?: { * enabled?: boolean, * max_attempts?: number, * base_delay_ms?: number, * max_delay_ms?: number, * exponential_factor?: number, * total_timeout_ms?: number, * accept_state_resync?: boolean, * }, * }} options * * - `transport` is not connected at construction — call {@link connect} * with an explicit remote peer id after {@link start}. * - `transport_factory` (client role): zero-arg function that returns * a fresh `Transport` on each reconnect attempt. Required for * automatic reconnect; without it, transport-level disconnects fall * straight through to `onConnectionPermanentlyLost`. * - `role` defaults to `'client'`. See {@link NetworkSessionRole}. * - `local_peer_id` defaults to 0 for host, 1 for client. Must be * unique across peers. * - `simulation_delay_ticks` is honored only under `role: 'host'`. * Default 4. See {@link ServerAuthoritativeServer}. * - `tick_rate_hz` defaults to 60. * - `scope_filter` defaults to {@link OwnerAwareScope} on the host. * - `server_resume_grace_ms` (host role): how long peer state is * retained after a transport drop before being freed. Default 30s. * - `reconnect` (client role): policy for the reconnect ladder. See * {@link DEFAULT_RECONNECT_POLICY}. Set `enabled: false` to opt * out entirely. */ constructor({ entity_manager, transport, transport_factory, role, local_peer_id, simulation_delay_ticks, tick_rate_hz, binary_registry, scope_filter, time_dilation, adaptive_render_delay, server_resume_grace_ms, reconnect, }?: { entity_manager: EntityManager; transport?: object; transport_factory?: (() => object); role?: 'client' | 'host'; local_peer_id?: number; simulation_delay_ticks?: number; tick_rate_hz?: number; binary_registry?: BinarySerializationRegistry; scope_filter?: object; time_dilation?: TimeDilation; adaptive_render_delay?: AdaptiveRenderDelay; server_resume_grace_ms?: number; reconnect?: { enabled?: boolean; max_attempts?: number; base_delay_ms?: number; max_delay_ms?: number; exponential_factor?: number; total_timeout_ms?: number; accept_state_resync?: boolean; }; }); /** * Set after `#render_interpolated_entities` writes interp values into * live components; cleared by `normalize_if_dirty`. Drives the * canonical-form invariant: live is canonical when this is false, * polluted (smooth) when true. * @private @type {boolean} */ private render_dirty; /** * Server-issued session token (raw UUID v1 bytes). Set on first * INITIAL_SYNC arrival; refreshed on every Tier-3 resync. Sent in * `RESUME_HELLO` to claim continuity with a prior session record. * Observable to game code for diagnostics; not auth-grade. * @type {UUID|null} */ session_token: UUID | null; /** * Host: a connected peer's transport reported a disconnect (the peer * has entered the grace window). Args: `(peer_id, reason)`. * @type {Signal} */ onPeerLost: Signal; /** * Host: a peer has been permanently freed — either via * `drop_peer()`, a grace-window timeout, or a peer-initiated * DISCONNECT. Args: `(peer_id, reason)`. * @type {Signal} */ onPeerPermanentlyDropped: Signal; /** * Client: the transport has reported a disconnect, and the * reconnect ladder has started. Args: `(reason)`. * @type {Signal} */ onConnectionLost: Signal; /** * Client: about to rebuild the transport and dial the host. Args: * `(attempt_number)`. * @type {Signal} */ onReconnectAttempt: Signal; /** * Client: reconnect succeeded. Args: `({ state_resynced: boolean })` — * `state_resynced` is `true` if we fell through to Tier-3 (server * had purged our state and re-sent INITIAL_SYNC), `false` for the * happy Tier-2 path where the action stream just resumed. * @type {Signal} */ onReconnected: Signal; /** * Client: the reconnect ladder is exhausted, or the host explicitly * kicked us with a DISCONNECT packet, or `reconnect.enabled` is * false and the link dropped. Args: `(reason)`. * @type {Signal} */ onConnectionPermanentlyLost: Signal; /** * @readonly * @type {EntityManager} */ readonly entity_manager: EntityManager; /** * @readonly * @type {EntityComponentDataset} */ readonly world: EntityComponentDataset; /** * @readonly * @type {NetworkSessionRole|string} */ readonly role: NetworkSessionRole | string; /** * @readonly * @type {number} */ readonly tick_period_ms: number; /** * @readonly * @type {number} */ readonly simulation_delay_ticks: number; /** * @readonly * @type {number} */ readonly local_peer_id: number; /** * @readonly * @type {BinarySerializationRegistry} */ readonly binary_registry: BinarySerializationRegistry; /** * Register a component class for network replication. * * The component must have a {@link BinaryClassSerializationAdapter} * registered for its `typeName` in the session's `binary_registry` * before {@link start} is called — that adapter handles the wire * format and rewind capture. * * `interpolator` is optional. When omitted, the component's state * snaps on each received update (no sub-tick smoothing). When * provided, it must extend {@link BinaryInterpolationAdapter} with * `kind = InterpolationKind.Linear` — Discrete and Cubic are * reserved for future support and currently throw. * * The order of `replicate()` calls must match across all peers in * the session: AUTH_STATE payload layout iterates components in * insertion order. * * @param {Function} ComponentClass * @param {BinaryInterpolationAdapter} [interpolator] */ replicate(ComponentClass: Function, interpolator?: BinaryInterpolationAdapter): void; /** * Register a user-defined {@link SimAction} subclass with the * session. The action's `type_id` is assigned by the underlying * registry when {@link start} runs. Calling code constructs * instances directly (`new MyAction(...)`) and dispatches via * {@link send} or returns them from the input sampler. * * @param {Function} SimActionClass class extending SimAction */ defineAction(SimActionClass: Function): void; /** * Install a per-tick input sampler (client role only). The sampler * is called once per local sim tick at predict time and must return * an array of {@link SimAction} instances representing the local * peer's inputs for that frame. The session executes them locally * (predict), records their serialized bytes for replay, and forwards * them over the action stream. * * The sampler is NOT called during reconciliation replay — the * recorded bytes are deserialized and re-executed instead, so the * action set faithfully reproduces what the user originally * sampled at that frame. * * Return `[]` (or `null`) for ticks with no inputs. * * @param {(frame: number) => Array<SimAction>|null} sampler_fn */ defineInputSampler(sampler_fn: (frame: number) => Array<SimAction> | null): void; /** * Wire the session to the engine. After this, the EntityObserver * is active, the right orchestrator is constructed, and a * {@link NetworkSystem} is attached. Subsequent {@link replicate} * / {@link defineAction} / {@link defineInputSampler} calls throw. * * Pre-condition: every component class passed to {@link replicate} * must already have a `BinaryClassSerializationAdapter` registered * in the session's binary_registry. */ start(): Promise<void>; /** * Connect to a remote peer. Host can connect many peers (one per * client); client typically connects one (the server). Called both * for first-time connects and reconnect attempts — the session * detects which based on whether a `session_token` is cached * (client) or whether the peer is in the grace window (host). * * @param {number} remote_peer_id * @param {object} transport */ connect(remote_peer_id: number, transport: object): void; /** * Host-only: forcibly free a peer's state, regardless of whether * they are actively connected, in the grace window, or already * unknown. Fires `onPeerPermanentlyDropped(peer_id, reason)`. * Sends a DISCONNECT packet if the peer is still actively * connected so the remote can short-circuit its reconnect attempts. * * Policy for *when* to drop (high server load, anti-cheat, admin * kick, etc.) is the caller's; the session provides the mechanism. * * @param {number} peer_id * @param {string} [reason] */ drop_peer(peer_id: number, reason?: string): void; /** * Client-only: voluntarily disconnect from the server. Sends a * DISCONNECT packet so the host can free state immediately and * skip the grace window. Disables reconnect for this session. * * @param {string} [reason] */ disconnect(reason?: string): void; /** * Tear down. Idempotent — safe to call from cleanup paths. */ stop(): void; /** * Fixed-timestep accumulator-driven step. Catch-up bounded by * `MAX_FIXED_STEPS_PER_TICK`. Under `role: 'client'` the local frame * advances at `1 / dilation_factor` per fixed step. * * @param {number} dt_seconds elapsed wall seconds since the previous call */ tick(dt_seconds: number): void; /** * Undo render-time interpolation on remote-owned components by * restoring each from its latest InterpolationLog entry. Idempotent * via the `render_dirty` flag — cheap when false. * * Called from `tick()` (top), `executor.before_execute` (per-action * applies, including transport-delivered action stream), and * `client.onBeforeReconcile` (before the rewind walk). * * @private */ private normalize_if_dirty; /** * Dispatch one user-defined action. Equivalent to * `peer.executor.execute(action, this.local_peer_id)` but exposed * at the session level so callers don't need a reference to the * executor or to know the local peer id. * * @param {SimAction} action_instance */ send(action_instance: SimAction): void; /** * @returns {ServerAuthoritativeServer|null} */ get server(): ServerAuthoritativeServer; /** * @returns {ServerAuthoritativeClient|null} */ get client(): ServerAuthoritativeClient; /** * @returns {NetworkPeer|null} */ get peer(): NetworkPeer; /** * @returns {InterpolationLog|null} */ get interpolation_log(): InterpolationLog; /** * @returns {AdaptiveRenderDelay} */ get adaptive_render_delay(): AdaptiveRenderDelay; /** * @returns {TimeDilation} */ get time_dilation(): TimeDilation; /** * Most recent locally-completed sim frame. */ get current_frame(): number; #private; } import { UUID } from "../ecs/guid/UUID.js"; import Signal from "../../core/events/signal/Signal.js"; import { BinarySerializationRegistry } from "../ecs/storage/binary/BinarySerializationRegistry.js"; import { BinaryInterpolationAdapter } from "./sim/BinaryInterpolationAdapter.js"; import { SimAction } from "./sim/SimAction.js"; import { ServerAuthoritativeServer } from "./orchestrator/ServerAuthoritativeServer.js"; import { ServerAuthoritativeClient } from "./orchestrator/ServerAuthoritativeClient.js"; import { NetworkPeer } from "./orchestrator/NetworkPeer.js"; import { InterpolationLog } from "./sim/InterpolationLog.js"; import { AdaptiveRenderDelay } from "./time/AdaptiveRenderDelay.js"; import { TimeDilation } from "./time/TimeDilation.js"; //# sourceMappingURL=NetworkSession.d.ts.map