import { Lt as Signal, b as Engine, jt as Node } from "./behavior-B_245qRy.js"; import { t as DiagnosticSink } from "./diagnostics-Cu85N3tL.js"; import { a as SceneJson, c as JsonValue, s as JsonObject } from "./rng-BsXZg3D6.js"; import { t as LoadSceneOptions } from "./loader-CbkVdXL8.js"; //#region src/net/types.d.ts type Unsubscribe = () => void; /** * Incanto's pluggable multiplayer transport contract. The engine speaks ONLY * this interface — any backend works by implementing it: * * - `LoopbackTransport` (built in): in-memory — offline dev, tests, split-screen * - `createAgent8Server()` (built in): adapter for `@agent8/gameserver` * - your own: Socket.IO, Colyseus, Supabase Realtime, a custom WebSocket server… * * The room protocol a transport's backend must answer via `remoteFunction`: * `joinRoom(roomId?) → roomId` · `leaveRoom(roomId)` · `setMyState(roomId, patch)` * (SHALLOW merge) · `patchRoomState(roomId, patch)` · `addEntity(roomId, coll, * entity) → id` · `updateEntity(roomId, coll, id, patch)` · `removeEntity(roomId, * coll, id)` · `sendEvent(roomId, type, payload)`. * (The shape happens to align with @agent8/gameserver's GameServer class, so that * adapter is thin — but nothing in the engine depends on that backend.) */ interface NetworkTransport { readonly account: string; readonly connected: boolean; connect(): Promise; disconnect(): Promise; /** Calls a function defined in the game's server.js (see templates/agent8-server.js). */ remoteFunction(fn: string, args?: unknown[], opts?: { needResponse?: boolean; throttle?: number; throttleKey?: string; }): Promise; subscribeRoomState(roomId: string, cb: (state: JsonObject) => void): Unsubscribe; subscribeRoomMyState(roomId: string, cb: (state: JsonObject) => void): Unsubscribe; subscribeRoomAllUserStates(roomId: string, cb: (states: Record) => void): Unsubscribe; subscribeRoomCollection(roomId: string, collectionId: string, cb: (entities: Record) => void): Unsubscribe; onRoomMessage(roomId: string, type: string, cb: (message: JsonValue) => void): Unsubscribe; onRoomUserJoin(roomId: string, cb: (account: string) => void): Unsubscribe; onRoomUserLeave(roomId: string, cb: (account: string) => void): Unsubscribe; /** * GLOBAL (cross-room, persistent) channels — OPTIONAL. A backend that has no * global tier (e.g. the fixed `LoopbackHub`) simply omits them; NetworkManager * degrades gracefully (its global signals never fire). `LocalGameServer` and the * agent8 adapter implement them. Each fires the current value once on subscribe. */ subscribeGlobalState?(cb: (state: JsonObject) => void): Unsubscribe; /** The caller's own global user state. */ subscribeGlobalMyState?(cb: (state: JsonObject) => void): Unsubscribe; subscribeGlobalUserState?(account: string, cb: (state: JsonObject) => void): Unsubscribe; subscribeGlobalCollection?(collectionId: string, cb: (entities: Record) => void): Unsubscribe; /** An account's currency ledger (`$asset`). */ subscribeAsset?(account: string, cb: (assets: Record) => void): Unsubscribe; onGlobalMessage?(type: string, cb: (message: JsonValue) => void): Unsubscribe; } //#endregion //#region src/net/loopback.d.ts interface Room { users: Map; state: JsonObject; collections: Map>; } /** * In-memory multiplayer hub implementing the SAME kernel contract as * `templates/agent8-server.js` — local split-screen demos, offline development, * and headless tests run real multi-client flows without any infrastructure. */ declare class LoopbackHub { private readonly rooms; private accountSeq; private roomSeq; private entitySeq; /** Per-room listener registries, keyed by roomId. */ private readonly listeners; createClient(account?: string): LoopbackTransport; /** @internal */ _room(roomId: string): Room; /** @internal The server.js kernel, in memory. */ _call(account: string, fn: string, args: unknown[]): unknown; /** @internal */ _subscribe(kind: K, roomId: string, entry: LoopbackHub["listeners"][K] extends Map> ? E : never): Unsubscribe; /** @internal */ _snapshotUsers(roomId: string): Record; /** @internal */ _snapshotCollection(roomId: string, collectionId: string): Record; /** @internal */ _snapshotRoomState(roomId: string): JsonObject; private fire; private fireAllUsers; private fireCollection; } /** A client on a LoopbackHub. Subscriptions fire immediately with a snapshot. */ declare class LoopbackTransport implements NetworkTransport { private readonly hub; readonly account: string; connected: boolean; constructor(hub: LoopbackHub, account: string); connect(): Promise; disconnect(): Promise; remoteFunction(fn: string, args?: unknown[]): Promise; subscribeRoomState(roomId: string, cb: (state: JsonObject) => void): Unsubscribe; subscribeRoomMyState(roomId: string, cb: (state: JsonObject) => void): Unsubscribe; subscribeRoomAllUserStates(roomId: string, cb: (states: Record) => void): Unsubscribe; subscribeRoomCollection(roomId: string, collectionId: string, cb: (entities: Record) => void): Unsubscribe; onRoomMessage(roomId: string, type: string, cb: (message: JsonValue) => void): Unsubscribe; onRoomUserJoin(roomId: string, cb: (account: string) => void): Unsubscribe; onRoomUserLeave(roomId: string, cb: (account: string) => void): Unsubscribe; } //#endregion //#region src/net/local-game-server.d.ts /** A user's `server/src/server.ts` `Server` class (or any class with room methods). */ type ServerClass = new () => object; interface LocalGameServerOptions { /** * The game's `Server` class from `server/src/server.ts` (the v2 structured * project). Omit it to fall back to the built-in kernel only — identical to a * raw `LoopbackHub`. */ server?: ServerClass; /** * Where the preview server reports its own failures. * * It had nowhere to report them, and it is the server every multiplayer game * boots against. A `$roomTick` that THREW went to absolutely nothing — * `enqueue` marks the rejection handled and every shipped pump is * `void local.tick(...)` — so the server-driven match clock stopped forever * with `engine.log` empty and `stats().errors` 0. `createSplitScreen` passes * the first player's engine. */ diagnostics?: DiagnosticSink | null; } /** * Run a multiplayer game's REAL agent8-sdk-v2 `Server` class **locally**, in * memory, with NO cloud and NO auth — the preview/dev path. * * `LoopbackHub` answers only the fixed built-in room protocol (joinRoom, * setMyState, collections, events). Real games add server-AUTHORITATIVE rules * (`awardPoint`, `castSpell`, `$roomTick` match clocks) in `server/src/server.ts` * against the v2 contexts `$sender`/`$global`/`$room`/`$lock`. LocalGameServer * runs that exact class body so the whole game — client + server logic — is * playable in dev before it ever touches the Agent8 platform. Swap * `local.createClient(account)` for `await createAgent8Server()` to go live; * nothing else changes. * * How it stays faithful to the isolated-vm platform: * - A FRESH `Server` instance runs per request, so `this.*` never persists. * - The v2 globals are injected on `globalThis` for the duration of each call * (the SAME `server/src/server.ts` body that reads bare `$room`/`$sender` * runs unmodified). * - Calls are SERIALIZED through one queue, so a global binding can't leak across * another request's `await`s (the platform isolates per request; we serialize). * - `$roomTick(deltaMS, roomId)` runs only while a room has users — drive it from * the engine: `engine.updated.connect((dt) => local.tick(dt * 1000))`. * * NOT a substitute for the cloud: no isolated-vm sandboxing, no persistence, no * rate limits — a faithful FUNCTIONAL emulator for local play and tests. */ declare class LocalGameServer { private readonly hub; private readonly serverClass?; /** Each client's current room — binds `$sender.roomId` on later calls. */ private readonly clientRooms; /** All remoteFunction/tick calls run one-at-a-time so injected globals never overlap. */ private queue; /** Per-key mutex backing `$lock`. */ private readonly locks; /** Persistent (process-lifetime) store backing the `$global` + `$asset` contexts. */ private readonly g_state; private readonly g_userStates; private readonly g_collections; private readonly g_assets; private g_seq; /** Client-side global/asset subscription registries (fanned out on mutation). */ private readonly subState; private readonly subUser; private readonly subColl; private readonly subAsset; private readonly subMsg; private diagnostics; /** * Point the server's own failures at a log. * * `createSplitScreen` builds the server BEFORE the engines that will read it, * so the sink arrives after construction. Without one, a `$roomTick` that * throws and a wedged queue are console-only, which is exactly the silence * this reports. */ reportTo(sink: DiagnosticSink | null): void; /** Said once per room, so a per-frame pump cannot fill the log. */ private readonly reportedTickFailures; /** Said once per stuck call — see `enqueue`. */ private readonly reportedSlow; constructor(serverClass?: ServerClass, diagnostics?: DiagnosticSink | null); /** A client on this server. Distinct `account` per player (default auto-assigned). */ createClient(account?: string): LocalGameServerTransport; private collectionSnapshot; private assetSnapshot; /** @internal */ subscribeGlobalState(cb: (s: JsonObject) => void): Unsubscribe; /** @internal */ subscribeGlobalUserState(account: string, cb: (s: JsonObject) => void): Unsubscribe; /** @internal */ subscribeGlobalCollection(id: string, cb: (r: Record) => void): Unsubscribe; /** @internal */ subscribeAsset(account: string, cb: (a: Record) => void): Unsubscribe; /** @internal Messages are events — no immediate snapshot. */ onGlobalMessage(account: string, type: string, cb: (m: JsonValue) => void): Unsubscribe; private fireState; private fireUser; private fireColl; private fireAsset; private fireMsg; /** * Advance server-driven periodic logic. Calls the `Server`'s `$roomTick(deltaMS, * roomId)` once per room that currently has users. No-op without a `$roomTick`. */ tick(deltaMS: number): Promise; /** @internal Dispatch a remoteFunction (serialized). */ call(account: string, fn: string, args: unknown[]): Promise; /** How long one serialized call may take before the server says it is stuck. */ private static readonly SLOW_CALL_MS; private enqueue; private dispatch; private runTick; /** Install the v2 globals for one dispatch; returns a restorer. */ private bindGlobals; /** * The `$global` context — room membership, persistent GLOBAL state/user-state/ * collections, room management, and global messaging. Backed by an in-memory * store that survives between requests (unlike rooms, which the hub clears when * empty) — matching the platform's persistent-global / ephemeral-room split. */ private globalContext; /** * The `$asset` context — a per-account currency ledger. `burn`/`transfer` throw * on an insufficient balance (the documented pattern always checks `has` first), * surfacing economy bugs in preview just as the platform would. */ private assetContext; /** The `$room` context, bound to one room — maps onto the hub's kernel primitives. */ private roomContext; private withLock; } /** Firebase-style query options for `$global`/`$room` collections (preview subset). */ interface CollectionOptions { filters?: { field: string; operator: string; value: JsonValue; }[]; orderBy?: { field: string; direction?: "asc" | "desc"; }[]; limit?: number; startAfter?: JsonValue; endBefore?: JsonValue; } /** Construct a local preview server. With no `server`, it equals a raw LoopbackHub. */ declare function createLocalGameServer(opts?: LocalGameServerOptions): LocalGameServer; /** * A client of a {@link LocalGameServer}. Subscriptions read the shared in-memory * store directly (same fan-out as Loopback); `remoteFunction` routes through the * server's serialized dispatch so the game's `Server` class actually runs. */ declare class LocalGameServerTransport implements NetworkTransport { private readonly server; private readonly loop; readonly account: string; connected: boolean; constructor(server: LocalGameServer, loop: LoopbackTransport); connect(): Promise; disconnect(): Promise; remoteFunction(fn: string, args?: unknown[]): Promise; subscribeRoomState(roomId: string, cb: (state: JsonObject) => void): Unsubscribe; subscribeRoomMyState(roomId: string, cb: (state: JsonObject) => void): Unsubscribe; subscribeRoomAllUserStates(roomId: string, cb: (states: Record) => void): Unsubscribe; subscribeRoomCollection(roomId: string, collectionId: string, cb: (entities: Record) => void): Unsubscribe; onRoomMessage(roomId: string, type: string, cb: (message: JsonValue) => void): Unsubscribe; onRoomUserJoin(roomId: string, cb: (account: string) => void): Unsubscribe; onRoomUserLeave(roomId: string, cb: (account: string) => void): Unsubscribe; subscribeGlobalState(cb: (state: JsonObject) => void): Unsubscribe; subscribeGlobalMyState(cb: (state: JsonObject) => void): Unsubscribe; subscribeGlobalUserState(account: string, cb: (state: JsonObject) => void): Unsubscribe; subscribeGlobalCollection(collectionId: string, cb: (entities: Record) => void): Unsubscribe; subscribeAsset(account: string, cb: (assets: Record) => void): Unsubscribe; onGlobalMessage(type: string, cb: (message: JsonValue) => void): Unsubscribe; } //#endregion //#region src/net/network-manager.d.ts interface NetworkManagerOptions { /** * The transport to speak through — `LoopbackTransport` (offline/dev/tests), * `await createAgent8Server()` (agent8 platform), or any custom * NetworkTransport implementation. Default: the agent8 adapter. */ transport?: NetworkTransport; /** Room id; default: scene `multiplayer.room` ('auto' = server-assigned). */ room?: string; /** Config for the default agent8 adapter (ignored when `transport` is injected). */ config?: { verse?: string; account?: string; auth?: string; }; /** Replication send window in ms (default 50; node `network.throttleMs` can lower it). */ throttleMs?: number; /** * How often the FULL sync set is re-sent regardless of what changed, in ms * (default 2000; 0 disables). * * Deltas are right on the wire and wrong in the store: backends shallow-merge * one level down, so each send replaces the whole `sync` object and a key * that stopped changing is erased from the authoritative state. A keyframe * puts it back — for the next joiner, and for a send that never reached the * wire, which is never retried. */ keyframeMs?: number; } /** * Per-engine multiplayer hub: joins a room, exposes the room channels as core * Signals, replicates `network: {mode:'owner'}` node props on a throttle * window, and resolves registered scenes for NetworkSpawner. */ declare class NetworkManager { readonly account: string; readonly roomId: string; readonly roomState: Signal<[JsonObject]>; readonly allUserStates: Signal<[Record]>; readonly userJoined: Signal<[string]>; readonly userLeft: Signal<[string]>; /** * GLOBAL (cross-room, persistent) channels — populated only when the transport * supports them (`LocalGameServer`, the agent8 adapter). On a transport without a * global tier they stay empty / never fire. */ readonly globalState: Signal<[JsonObject]>; readonly globalMyState: Signal<[JsonObject]>; /** The local account's `$asset` ledger. */ readonly asset: Signal<[Record]>; /** Latest snapshots for polling consumers (NetworkSpawner). */ latestUserStates: Record; latestRoomState: JsonObject; latestGlobalState: JsonObject; latestGlobalMyState: JsonObject; latestAsset: Record; private readonly server; private readonly engine; private readonly throttleMs; private readonly keyframeMs; private readonly subs; private readonly messageSignals; private readonly collectionSignals; private readonly latestCollections; private readonly globalMessageSignals; private readonly globalCollectionSignals; private readonly latestGlobalCollections; private readonly scenes; private readonly lastSent; /** Sync keys already reported unreadable, so a per-frame send says it once. */ private readonly reportedDeadKeys; /** Name a sync key the owner cannot read — a typo in the prop or the path. */ /** * A sync key that has NEVER been readable, reported once — after a grace * period. * * Behaviours set their props in `onReady`/`update`, and replication runs on * the fixed step, so a perfectly correct key reads as `undefined` on the * first pass or two. Reporting immediately made the engine's own co-op * example print "sync key 'firing' … it is never sent" about a key that is * sent from frame 2 onward — a diagnostic that cries wolf teaches its reader * to ignore the one that does not. */ private reportDeadSyncKey; private sendAccumulator; /** Simulated ms since the last time the FULL sync set went out. */ private keyframeAccumulator; /** Simulated ms this manager has been replicating — see reportDeadSyncKey. */ private aliveMs; private detachReplication; private lastOwner; private boundScene; /** The room the BOUND scene declared, so a swap can tell same-room from not. */ private declaredRoom; /** Said once: a client that went mute must not also spam the console. */ private reportedUnbound; static get(engine: Engine): NetworkManager | null; static create(engine: Engine, opts?: NetworkManagerOptions): Promise; private constructor(); /** Signal for a typed room message (`sendEvent` on any client). */ message(type: string): Signal<[JsonValue]>; /** Signal + snapshot for a room collection (spawned entities). */ collection(id: string): Signal<[Record]>; latestCollection(id: string): Record; /** Signal for a typed GLOBAL message (`$global.broadcastToAll`/`sendMessageToUser`). */ globalMessage(type: string): Signal<[JsonValue]>; /** Signal + snapshot for a GLOBAL collection (persistent, cross-room). */ globalCollection(id: string): Signal<[Record]>; latestGlobalCollection(id: string): Record; setMyState(patch: JsonObject): Promise; patchRoomState(patch: JsonObject): Promise; addEntity(collectionId: string, entity: JsonObject): Promise; updateEntity(collectionId: string, id: string, patch: JsonObject): Promise; removeEntity(collectionId: string, id: string): Promise; sendEvent(type: string, payload: JsonValue): Promise; /** * Invoke a CUSTOM server-authoritative function — a method you added to your * `server/src/server.ts` `Server` class (run live on the Agent8 platform, or * locally via `createLocalGameServer`). The room id is prepended automatically, * matching the `(roomId, …)` shape every server method takes: * * `manager.call('claimCoin', coinId)` → server `claimCoin(roomId, coinId)` * * Returns the function's result (use it for server-validated outcomes — a * rejected claim, a rolled value). This is THE entry point for game rules the * client must not be trusted to compute. */ call(fn: string, ...args: JsonValue[]): Promise; /** Register a scene for NetworkSpawner (`"scene": ""`). */ /** * `json` is `unknown` for the reason `createGame2D.scene` is: an imported * `.scene.json` does not satisfy `SceneJson` (`format: number` vs `1`), and * the loader validates it properly anyway. */ registerScene(key: string, json: unknown): void; resolveScene(key: string): SceneJson; dispose(): Promise; private replicate; } /** * @internal Applies a replicated flat sync patch (`{'position': …, * 'Sprite.animation': …}`) onto a spawned subtree (NetworkSpawner). */ declare function applySyncPatch(root: Node, patch: JsonObject, setProp: (target: Node, prop: string, value: JsonValue) => void, onMissingNode?: (key: string) => void): void; //#endregion //#region src/net/split-screen.d.ts interface SplitScreenPlayer { account: string; engine: Engine; manager: NetworkManager; } interface SplitScreenOptions extends LocalGameServerOptions { /** * The shared scene JSON every panel loads (cloned per panel). * * `unknown`, like `createGame2D`'s: an imported `.scene.json` is typed * `{format: number}` and would not satisfy `SceneJson` (`format: 1`), so a * declared type here buys nothing but a cast in every consumer's boot file. * `loadScene` hard-validates the real thing a line later. */ scene: unknown; /** Player accounts, one panel each (default ['p1', 'p2']). */ accounts?: string[]; /** Called per player to finish wiring (renderer, input, HUD). */ setup: (player: SplitScreenPlayer, index: number) => void | Promise; /** name → scene JSON registered on every panel's NetworkManager. */ scenes?: Record; /** Engine seed base (each panel gets seed + index). */ seed?: number; /** * Loader options, per panel — `stubMissingBehaviors` for a headless check * that has no TypeScript, `resolveScene` for `instance:` sub-scenes. * * Without these, `createSplitScreen` hard-failed on any real game: it called * `loadScene` with nothing registered, so a scene naming one of its own * behaviours threw `Unknown behavior` before a single frame ran. */ load?: LoadSceneOptions; /** * The room every panel joins. Default: whatever the FIRST panel got. * * A shared scene usually says `multiplayer: { room: "auto" }`, and "auto" * means a server-ASSIGNED room — so each panel used to land in a room of its * own and the players never saw each other. Split screen means one game. */ room?: string; /** * Rapier per panel. `'auto'` (default) enables it when the scene has bodies, * exactly as `createGame2D`/`createGame3D` do. * * Left out, the one thing that makes a CharacterBody move was the caller's * job — in a helper whose whole purpose is booting the engines for you. A * two-player game came up with both players frozen at the origin and no error. */ physics?: "auto" | boolean; } /** * N local clients on ONE page sharing ONE in-memory game server — the * split-screen preview harness three examples were hand-wiring (~40 lines * each). Wires: LocalGameServer + per-player Engine/scene/NetworkManager + * the server tick driven from the first panel's clock. * * const { players, server } = await createSplitScreen({ * scene: gameJson, * server: MyServerClass, * scenes: { 'remote-player': remoteJson }, * setup: ({ engine }, i) => { * new Renderer2D({ canvas: canvases[i], engine }); * engine.input.attachKeyboard(window); * engine.start(); * }, * }); * * Going live stays the one-line story: replace the transport with * `createAgent8Server()` and boot ONE client per browser. */ declare function createSplitScreen(opts: SplitScreenOptions): Promise<{ server: ReturnType; players: SplitScreenPlayer[]; dispose(): void; }>; //#endregion export { NetworkManagerOptions as a, LocalGameServer as c, ServerClass as d, createLocalGameServer as f, Unsubscribe as g, NetworkTransport as h, NetworkManager as i, LocalGameServerOptions as l, LoopbackTransport as m, SplitScreenPlayer as n, applySyncPatch as o, LoopbackHub as p, createSplitScreen as r, CollectionOptions as s, SplitScreenOptions as t, LocalGameServerTransport as u };