/** * Incanto netcode kernel for the agent8 GameServer SDK v2 (STRUCTURED project). * * HOW TO USE — DO NOT hand-create the server project. Run the init command first * (it generates server/package.json, server/tsconfig.json, server/src/server.ts, * server/test/), THEN drop this class body into server/src/server.ts: * * npx -y @agent8/gameserver-node init # only if server/ does not exist yet * # then replace server/src/server.ts with the body below * npx -y @agent8/gameserver-node test # write + run server tests * npx -y @agent8/gameserver-node build # generates server/dist/server.js * # then DEPLOY = push to the repository (platform auto-builds + deploys). * * Server-code specifics (the $global/$room/$sender/$asset/$lock contexts, the * isolated-vm limits, build/deploy) AND the live client's connection + auth/identity * (useGameServer / server.connect / $sender.isGuest etc.) are owned by the service's * `gameserver-sdk-v2` skill — this file is ONLY the incanto-flavored room protocol body. * * This class implements EXACTLY the room protocol incanto's NetworkManager (and * the in-memory LoopbackHub) speak, so a game proven on Loopback runs live by * swapping the transport — nothing else changes. * * Platform facts baked into this design (do not fight them): * - A NEW Server instance runs per request in isolated-vm: `this.*` never persists. * Use $global/$room for state, $roomTick for periodic logic — NEVER setTimeout/setInterval. * - $room.updateMyState / $room.updateRoomState are SHALLOW merges — keep state maps flat. * - Room data is ephemeral (cleared when the last user leaves); persist long-term * data to $global before rooms empty. * - Trust $sender.account; never trust args. Guard economy/score writes with $lock. * * STRUCTURED v2 requires the `export` keyword (legacy root server.js must NOT export). */ // These globals are injected by the agent8 isolated-vm runtime (see gameserver-sdk-v2). declare const $sender: { account: string; roomId: string; isGuest?: boolean }; /** * Room membership, PERSISTENT global state, global collections, room management * and global messaging. Room data is ephemeral — anything that must outlive an * empty room is written here. */ declare const $global: { joinRoom(roomId?: string): Promise; leaveRoom(): Promise; getGlobalState(): Promise>; updateGlobalState(patch: Record): Promise>; getMyState(): Promise>; updateMyState(patch: Record): Promise>; getUserState(account: string): Promise>; updateUserState(account: string, patch: Record): Promise>; addCollectionItem(collectionId: string, item: Record): Promise<{ __id: string }>; updateCollectionItem( collectionId: string, item: Record, ): Promise<{ __id: string }>; deleteCollectionItem(collectionId: string, itemId: string): Promise<{ __id: string }>; deleteCollection(collectionId: string): Promise; getCollectionItem(collectionId: string, itemId: string): Promise>; getCollectionItems( collectionId: string, options?: CollectionQuery, ): Promise[]>; countCollectionItems(collectionId: string, options?: CollectionQuery): Promise; countRooms(): Promise; getAllRoomIds(): Promise; getAllRoomStates(): Promise[]>; getRoomUserAccounts(roomId: string): Promise; countRoomUsers(roomId: string): Promise; getRoomState(roomId: string): Promise>; updateRoomState(roomId: string, patch: Record): Promise>; getRoomUserState(roomId: string, account: string): Promise>; updateRoomUserState( roomId: string, account: string, patch: Record, ): Promise>; broadcastToAll(type: string, message: unknown): void; sendMessageToUser(account: string, type: string, message: unknown): void; }; /** Query options for a collection read (`filters` / `orderBy` / `limit`). */ interface CollectionQuery { filters?: Record; orderBy?: { field: string; direction?: 'asc' | 'desc' }; limit?: number; } /** The CURRENT room: shared state, per-user state, collections, messaging. */ declare const $room: { getMyState(): Promise>; updateMyState(patch: Record): Promise>; getRoomState(): Promise>; updateRoomState(patch: Record): Promise>; getUserState(account: string): Promise>; updateUserState(account: string, patch: Record): Promise>; getAllUserStates(): Promise[]>; countUsers(): Promise; addCollectionItem(collectionId: string, item: Record): Promise<{ __id: string }>; updateCollectionItem( collectionId: string, item: Record, ): Promise<{ __id: string }>; deleteCollectionItem(collectionId: string, itemId: string): Promise<{ __id: string }>; deleteCollection(collectionId: string): Promise; getCollectionItem(collectionId: string, itemId: string): Promise>; getCollectionItems( collectionId: string, options?: CollectionQuery, ): Promise[]>; countCollectionItems(collectionId: string, options?: CollectionQuery): Promise; broadcastToRoom(type: string, message: unknown): void; sendMessageToUser(account: string, type: string, message: unknown): void; }; /** * Serialize a read-modify-write against concurrent requests. * * The preview runs calls one at a time, so a FORGOTTEN lock still passes * locally — live, parallel requests race (double-award, last-write-wins). */ declare function $lock(key: string, fn: () => T | Promise): Promise; /** Per-account currency ledger. `burn`/`transfer` throw on an insufficient balance. */ declare const $asset: { mint(assetId: string, amount: number): Promise>; burn(assetId: string, amount: number): Promise>; has(assetId: string, amount: number): Promise; get(assetId: string): Promise; getAll(): Promise>; transfer(toAccount: string, assetId: string, amount: number): Promise>; }; export class Server { // ---- rooms ----------------------------------------------------------------- /** undefined roomId → server-assigned random room; explicit id → shared room. */ async joinRoom(roomId?: string): Promise { return $global.joinRoom(roomId); } // biome-ignore lint/correctness/noUnusedFunctionParameters: roomId kept for protocol symmetry with LoopbackHub. async leaveRoom(roomId?: string): Promise { return $global.leaveRoom(); // acts on $sender's current room } // ---- per-player state (owner-authoritative: movement, cosmetics) ------------ // NetworkManager throttles owner sync into one setMyState({sync:{…}}) per window. async setMyState(_roomId: string, patch: Record): Promise { await $room.updateMyState(patch); // shallow merge; surfaces via subscribeRoomAllUserStates return true; } // ---- shared room state (match phase, timers) -------------------------------- async patchRoomState(_roomId: string, patch: Record): Promise { await $room.updateRoomState(patch); // shallow merge return true; } // ---- room collections (spawned entities: bullets, pickups) ------------------- async addEntity( _roomId: string, collectionId: string, entity: Record, ): Promise { const doc = await $room.addCollectionItem(collectionId, entity); return doc.__id; } async updateEntity( _roomId: string, collectionId: string, id: string, patch: Record, ): Promise { await $room.updateCollectionItem(collectionId, { __id: id, ...patch }); return true; } async removeEntity(_roomId: string, collectionId: string, id: string): Promise { await $room.deleteCollectionItem(collectionId, id); return true; } // ---- transient events (explosions, chat) ------------------------------------- async sendEvent(_roomId: string, type: string, payload: unknown): Promise { $room.broadcastToRoom(type, payload); return true; } // ---- server-driven periodic logic (OPTIONAL) --------------------------------- // Runs every 100–1000ms while the room has users — the ONLY way to do timed // server logic (no setTimeout/setInterval). Drive match clocks, AI waves, etc. // Delete if your game is purely client-driven. // // $roomTick(deltaMS: number, roomId: string): void { // // e.g. advance a shared match timer in room state // } // ---- EXTEND BELOW: server-authoritative game rules ---------------------------- // Example — guarded score (never let clients write scores directly). $lock makes // the read-modify-write atomic against concurrent requests: // // async awardPoint(_roomId: string): Promise { // const account = $sender.account; // await $lock(`score:${account}`, async () => { // const state = (await $room.getUserState(account)) ?? {}; // await $room.updateUserState(account, { score: ((state.score as number) ?? 0) + 1 }); // }); // return true; // } }