incanto 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +30 -0
- package/README.md +36 -0
- package/THIRD-PARTY-NOTICES.md +88 -0
- package/assets/audio/attacked.mp3 +0 -0
- package/assets/audio/explosion.mp3 +0 -0
- package/assets/audio/gold_loot.mp3 +0 -0
- package/assets/audio/heal.mp3 +0 -0
- package/assets/audio/hit_metal_bang.mp3 +0 -0
- package/assets/audio/ice_spear.mp3 +0 -0
- package/assets/audio/monster_died.mp3 +0 -0
- package/assets/audio/slash.mp3 +0 -0
- package/assets/audio/smite.mp3 +0 -0
- package/assets/audio/spells_cast.mp3 +0 -0
- package/assets/audio/ui_click.wav +0 -0
- package/assets/audio/walk.mp3 +0 -0
- package/assets/catalog.json +390 -0
- package/assets/characters/2dbasic.json +41 -0
- package/assets/characters/2dbasic.png +0 -0
- package/assets/characters/ghost.json +46 -0
- package/assets/characters/ghost.png +0 -0
- package/assets/characters/goblin.json +40 -0
- package/assets/characters/goblin.png +0 -0
- package/assets/characters/medieval-knight.json +41 -0
- package/assets/characters/medieval-knight.png +0 -0
- package/assets/effects/swoosh.png +0 -0
- package/assets/items/box.png +0 -0
- package/assets/items/buff_potion.png +0 -0
- package/assets/items/coin.png +0 -0
- package/assets/items/gem.png +0 -0
- package/assets/items/gold.png +0 -0
- package/assets/items/hp_potion.png +0 -0
- package/assets/items/locked_item_box.png +0 -0
- package/assets/items/map.png +0 -0
- package/assets/items/resurrection_potion.png +0 -0
- package/assets/items/super_box.png +0 -0
- package/assets/items/trap.png +0 -0
- package/assets/tiles/floor00.jpg +0 -0
- package/assets/tiles/minecraft-tiles.png +0 -0
- package/assets/tiles/wall00.jpg +0 -0
- package/assets/vegetation/ash_color.png +0 -0
- package/assets/vegetation/aspen_color.png +0 -0
- package/assets/vegetation/bark/birch_color_1k.jpg +0 -0
- package/assets/vegetation/bark/birch_normal_1k.jpg +0 -0
- package/assets/vegetation/bark/birch_roughness_1k.jpg +0 -0
- package/assets/vegetation/bark/oak_color_1k.jpg +0 -0
- package/assets/vegetation/bark/oak_normal_1k.jpg +0 -0
- package/assets/vegetation/bark/oak_roughness_1k.jpg +0 -0
- package/assets/vegetation/bark/pine_color_1k.jpg +0 -0
- package/assets/vegetation/bark/pine_normal_1k.jpg +0 -0
- package/assets/vegetation/bark/pine_roughness_1k.jpg +0 -0
- package/assets/vegetation/ground/dirt_color.jpg +0 -0
- package/assets/vegetation/ground/dirt_normal.jpg +0 -0
- package/assets/vegetation/ground/grass.jpg +0 -0
- package/assets/vegetation/oak_color.png +0 -0
- package/assets/vegetation/pine_color.png +0 -0
- package/bin/incanto-assets.mjs +107 -0
- package/bin/incanto-check.mjs +107 -0
- package/bin/incanto-editor.mjs +343 -0
- package/bin/incanto-env.mjs +144 -0
- package/bin/incanto-model.mjs +296 -0
- package/bin/incanto-play.mjs +219 -0
- package/bin/incanto-skills.mjs +71 -0
- package/dist/2d.d.ts +642 -0
- package/dist/2d.js +44 -0
- package/dist/3d.d.ts +1860 -0
- package/dist/3d.js +5 -0
- package/dist/agent8-DzU2fFyH.js +129 -0
- package/dist/audio-player-DqUR3XFs.d.ts +110 -0
- package/dist/behavior-BAQq7HGM.d.ts +851 -0
- package/dist/create-game-BdjpTHrW.js +1725 -0
- package/dist/create-game-CZHROKcT.js +527 -0
- package/dist/debug-draw-CZmOYjL2.js +13 -0
- package/dist/debug.d.ts +66 -0
- package/dist/debug.js +658 -0
- package/dist/duplicate-DP2WPYom.js +22 -0
- package/dist/env.d.ts +430 -0
- package/dist/env.js +3152 -0
- package/dist/errors-BMFaY68Q.d.ts +33 -0
- package/dist/errors-BpWbnbb_.js +13 -0
- package/dist/gameplay-Ccruc3Wd.js +1501 -0
- package/dist/gameplay.d.ts +543 -0
- package/dist/gameplay.js +2 -0
- package/dist/heightmap-CroQPEER.js +185 -0
- package/dist/index.d.ts +305 -0
- package/dist/index.js +62 -0
- package/dist/json-BLk7H2Qa.js +30 -0
- package/dist/loader-CGs_G-r0.js +919 -0
- package/dist/loader-Mo0KghCv.d.ts +41 -0
- package/dist/net.d.ts +427 -0
- package/dist/net.js +772 -0
- package/dist/noise-CGUMx44x.js +82 -0
- package/dist/particle-sim-CbN4YUuH.d.ts +63 -0
- package/dist/particle-sim-DYuSUxvK.js +1319 -0
- package/dist/physics-2d-KuMWPTf6.js +288 -0
- package/dist/physics-3d-Dl67vOLT.js +434 -0
- package/dist/react.d.ts +65 -0
- package/dist/react.js +209 -0
- package/dist/register-BuUV1_KB.js +561 -0
- package/dist/register-CNlYAS1_.js +10634 -0
- package/dist/register-DPEV9_9t.js +851 -0
- package/dist/register-Dasmnurl.js +374 -0
- package/dist/registry-BVJ2HbCn.js +132 -0
- package/dist/rng-DP-SR7eg.js +38 -0
- package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/schema-CcoWb32N.d.ts +104 -0
- package/dist/test.d.ts +158 -0
- package/dist/test.js +275 -0
- package/dist/touch-031PxtCR.js +208 -0
- package/dist/vite.d.ts +26 -0
- package/dist/vite.js +57 -0
- package/editor/assets/GameServer-C56iOUgF.js +1 -0
- package/editor/assets/agent8-Bp7QFI7v.js +1 -0
- package/editor/assets/index-DF3tMeKJ.css +1 -0
- package/editor/assets/index-Dl2pjA8e.js +7365 -0
- package/editor/assets/rapier-CEuLKeCu.js +1 -0
- package/editor/assets/rapier-DE6a0vmv.js +1 -0
- package/editor/index.html +169 -0
- package/package.json +97 -0
- package/schemas/scene.schema.json +4254 -0
- package/skills/README.md +9 -0
- package/skills/incanto-3d-character.md +229 -0
- package/skills/incanto-3d-models.md +151 -0
- package/skills/incanto-assets.md +118 -0
- package/skills/incanto-audio.md +309 -0
- package/skills/incanto-behaviors-and-scripts.md +169 -0
- package/skills/incanto-building-2d-games.md +242 -0
- package/skills/incanto-building-3d-games.md +245 -0
- package/skills/incanto-editor.md +163 -0
- package/skills/incanto-environment.md +743 -0
- package/skills/incanto-gameplay-behaviors.md +707 -0
- package/skills/incanto-multiplayer.md +264 -0
- package/skills/incanto-node-reference.md +797 -0
- package/skills/incanto-physics-and-input.md +164 -0
- package/skills/incanto-scene-json-authoring.md +325 -0
- package/skills/incanto-verifying-your-game.md +191 -0
- package/skills/incanto-web-integration.md +96 -0
- package/templates/agent8-server.js +84 -0
- package/templates/agent8-server.ts +138 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: incanto-multiplayer
|
|
3
|
+
description: Incanto multiplayer over a pluggable NetworkTransport — NetworkManager, owner replication via scene-JSON network blobs, NetworkSpawner, the built-in Loopback transport, the @agent8/gameserver adapter, and custom transports for any backend. Use when adding multiplayer to a game.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Multiplayer with Incanto
|
|
7
|
+
|
|
8
|
+
> Shipped inside the `incanto` npm package — this document always matches the
|
|
9
|
+
> installed engine version. Sibling skills live in `node_modules/incanto/skills/`.
|
|
10
|
+
|
|
11
|
+
## Mental model
|
|
12
|
+
|
|
13
|
+
Incanto multiplayer is **transport-agnostic**: the engine speaks one small
|
|
14
|
+
`NetworkTransport` interface, and ANY backend plugs in behind it —
|
|
15
|
+
|
|
16
|
+
| Transport | When |
|
|
17
|
+
|---|---|
|
|
18
|
+
| `LoopbackTransport` (built in) | offline dev, tests, local split-screen — built-in protocol only, zero infra |
|
|
19
|
+
| `LocalGameServer` (built in) | **PREVIEW**: run your REAL `server/src/server.ts` (custom rules + `$roomTick`) in memory — no cloud, no auth |
|
|
20
|
+
| `createAgent8Server()` (built in) | the agent8/Verse8 platform (`@agent8/gameserver`) — production |
|
|
21
|
+
| your own implementation | any server: Socket.IO, Colyseus, Supabase Realtime, custom WebSocket… |
|
|
22
|
+
|
|
23
|
+
Three rungs, same game code: **Loopback** (movement/co-presence, no server logic) →
|
|
24
|
+
**LocalGameServer** (your actual server-authoritative `Server` class, run locally) →
|
|
25
|
+
**Agent8** (that same `Server` deployed live). You climb rungs by swapping ONE line —
|
|
26
|
+
the transport — never the game or the server file.
|
|
27
|
+
|
|
28
|
+
The replication model is **data-authoritative, not simulation-authoritative**: clients own
|
|
29
|
+
their state; the backend stores/relays it (cadence ≥ tens of ms). Design for **casual
|
|
30
|
+
sync** — co-presence, turn-based, racing-with-tolerance. Fast-paced fairness (fighting
|
|
31
|
+
games, physics duels) needs a simulation-authoritative server, which is out of scope.
|
|
32
|
+
|
|
33
|
+
Authority rules:
|
|
34
|
+
- movement/cosmetics → **owner-authoritative** (the replication below)
|
|
35
|
+
- score/economy/inventory → **server-function-authoritative** (extend `server.js`;
|
|
36
|
+
`$sender.account` is trustworthy, args are NOT; guard with `$lock`)
|
|
37
|
+
- physics/AI run client-side; one-simulator needs (NPC waves) use the host-client pattern
|
|
38
|
+
|
|
39
|
+
## Scene JSON
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
"multiplayer": { "room": "auto" },
|
|
43
|
+
"root": { ..., "children": [
|
|
44
|
+
{ "name": "Player", "type": "CharacterBody2D",
|
|
45
|
+
"network": { "mode": "owner", "sync": ["position", "Skin.animation"], "throttleMs": 50 },
|
|
46
|
+
"children": [ ... ] },
|
|
47
|
+
{ "name": "Remotes", "type": "NetworkSpawner",
|
|
48
|
+
"props": { "source": "users", "scene": "remote-player" } }
|
|
49
|
+
] }
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- **ONE owner node per player.** Its `sync` keys are relative to ITSELF
|
|
53
|
+
(`position` = own prop, `Skin.animation` = child path + prop). Changed values coalesce
|
|
54
|
+
into one throttled `setMyState({sync: {...}})` per window. Spawned entities (bullets,
|
|
55
|
+
pickups) go through **collections**, never extra owner nodes.
|
|
56
|
+
- **`NetworkSpawner`** (register with `registerNodesNet()`): `source: "users"` spawns one
|
|
57
|
+
instance of the registered scene per OTHER account (self skipped); the flat `sync` patch
|
|
58
|
+
applies onto the spawned scene's root; `position` lerps when `interpolate: true`
|
|
59
|
+
(remote entities render slightly in the past — that's correct). Emits
|
|
60
|
+
`spawned(node, key)` / `despawned(node, key)`. `source: "collection:<id>"` mirrors a
|
|
61
|
+
room collection by `__id`.
|
|
62
|
+
|
|
63
|
+
## Boot
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { NetworkManager, registerNodesNet, LoopbackHub } from 'incanto/net';
|
|
67
|
+
|
|
68
|
+
registerNodesNet();
|
|
69
|
+
const scene = loadScene(json);
|
|
70
|
+
engine.setScene(scene);
|
|
71
|
+
|
|
72
|
+
// DEV / offline / split-screen: zero infrastructure, built-in protocol only
|
|
73
|
+
const hub = new LoopbackHub();
|
|
74
|
+
const manager = await NetworkManager.create(engine, { transport: hub.createClient('p1') });
|
|
75
|
+
|
|
76
|
+
// PREVIEW with YOUR server logic (custom rules + $roomTick), still no cloud:
|
|
77
|
+
// const local = createLocalGameServer({ server: Server }); // see "Preview" below
|
|
78
|
+
// const manager = await NetworkManager.create(engine, { transport: local.createClient('p1') });
|
|
79
|
+
|
|
80
|
+
// LIVE on agent8 (install the optional peer dep `@agent8/gameserver`):
|
|
81
|
+
// const manager = await NetworkManager.create(engine); // verse/account/auth auto-resolve
|
|
82
|
+
// …and run the server/ init flow below ("Going live on agent8 v2").
|
|
83
|
+
|
|
84
|
+
// ANY OTHER BACKEND: implement the NetworkTransport interface (see below) and
|
|
85
|
+
// pass it the same way: NetworkManager.create(engine, { transport: myTransport }).
|
|
86
|
+
|
|
87
|
+
manager.registerScene('remote-player', remotePlayerSceneJson);
|
|
88
|
+
engine.start();
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`NetworkManager` API:
|
|
92
|
+
- ROOM signals `roomState`, `allUserStates`, `userJoined/Left`, `message(type)`,
|
|
93
|
+
`collection(id)`; snapshots `latestUserStates`/`latestRoomState`/`latestCollection(id)`.
|
|
94
|
+
- GLOBAL signals `globalState`, `globalMyState`, `asset` (the local account's `$asset`
|
|
95
|
+
ledger — wire a HUD straight to it, no polling), `globalCollection(id)`,
|
|
96
|
+
`globalMessage(type)`; snapshots `latestGlobalState`/`latestGlobalMyState`/
|
|
97
|
+
`latestAsset`/`latestGlobalCollection(id)`. These fire only on a transport with a
|
|
98
|
+
global tier (LocalGameServer, the agent8 adapter); they stay quiet on raw Loopback.
|
|
99
|
+
- Outbound `setMyState`, `patchRoomState`, `addEntity/updateEntity/removeEntity`,
|
|
100
|
+
`sendEvent(type, payload)`; **`call(fn, ...args)`** to invoke a CUSTOM server function
|
|
101
|
+
(your `server/src/server.ts` method — roomId is prepended automatically, e.g.
|
|
102
|
+
`manager.call('claimCoin', id)` → server `claimCoin(roomId, id)`, returns the server's
|
|
103
|
+
result). Global state is server-authoritative — the client READS it via the signals
|
|
104
|
+
above and WRITES it only through `call(...)`. `dispose()` leaves the room.
|
|
105
|
+
|
|
106
|
+
The agent8 adapter (`createAgent8Server`) deep-imports the framework-free
|
|
107
|
+
`GameServer` class from `@agent8/gameserver` (skipping its React/zustand index) and
|
|
108
|
+
converts the v2 wire shapes to incanto's: `subscribeRoomAllUserStates` arrives as a
|
|
109
|
+
delta-merged ARRAY `[{...state, account, __updated}]` → an account-keyed Record;
|
|
110
|
+
`subscribeRoomCollection` arrives as `{items:[{__id,…}], changes}` → a `__id`-keyed
|
|
111
|
+
Record. It also re-issues every subscription after a reconnect.
|
|
112
|
+
|
|
113
|
+
## Writing a custom transport (any backend)
|
|
114
|
+
|
|
115
|
+
Implement `NetworkTransport` (exported from `incanto/net`): `connect`/`disconnect`,
|
|
116
|
+
`remoteFunction(fn, args)` answering the room protocol
|
|
117
|
+
(`joinRoom`/`leaveRoom`/`setMyState` shallow-merge/`patchRoomState`/
|
|
118
|
+
`addEntity`/`updateEntity`/`removeEntity`/`sendEvent`), and the subscription methods
|
|
119
|
+
(`subscribeRoomState`, `subscribeRoomMyState`, `subscribeRoomAllUserStates`,
|
|
120
|
+
`subscribeRoomCollection`, `onRoomMessage`, `onRoomUserJoin/Leave`). `LoopbackTransport`'s source is the reference
|
|
121
|
+
implementation — anything that behaves like it works with the whole engine.
|
|
122
|
+
|
|
123
|
+
## Preview: run your REAL server locally (no cloud, no auth)
|
|
124
|
+
|
|
125
|
+
`LoopbackHub` answers only the FIXED built-in protocol — it cannot run your
|
|
126
|
+
server-authoritative rules (`claimCoin`, `castSpell`) or your `$roomTick`. To
|
|
127
|
+
PLAY the whole game — client + the actual `server/src/server.ts` `Server` class —
|
|
128
|
+
in dev, use `LocalGameServer`:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import { createLocalGameServer, NetworkManager, registerNodesNet } from 'incanto/net';
|
|
132
|
+
import { Server } from '../server/src/server'; // your deployable v2 Server class
|
|
133
|
+
|
|
134
|
+
const local = createLocalGameServer({ server: Server }); // runs Server in memory
|
|
135
|
+
const manager = await NetworkManager.create(engine, { transport: local.createClient('p1') });
|
|
136
|
+
// …a second client: local.createClient('p2') — split-screen on one page.
|
|
137
|
+
|
|
138
|
+
// $roomTick is server-DRIVEN: pump it from the engine ONCE (clamp dt so a
|
|
139
|
+
// backgrounded tab can't fast-forward the match):
|
|
140
|
+
engine.updated.connect((dt) => void local.tick(Math.min(dt, 0.05) * 1000));
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
It runs the SAME class body the cloud runs: the v2 globals (`$sender`/`$global`/
|
|
144
|
+
`$room`/`$lock`) are injected per call, a FRESH `Server` instance runs per request
|
|
145
|
+
(so `this.*` never persists), and calls are serialized (no global leaks across
|
|
146
|
+
`await`s). `$roomTick(deltaMS, roomId)` runs only while a room has users.
|
|
147
|
+
|
|
148
|
+
It is a FUNCTIONAL emulator, NOT the platform: no isolated-vm sandbox, no rate
|
|
149
|
+
limits, and no DURABLE persistence (global state lives only for the preview process
|
|
150
|
+
— it is not saved across runs, and rooms still clear when empty). It proves your
|
|
151
|
+
game logic; deploy for the rest. Omitting `server` (`createLocalGameServer()`) gives
|
|
152
|
+
the built-in kernel only — identical to a raw `LoopbackHub`.
|
|
153
|
+
|
|
154
|
+
The preview injects `$sender`, `$room`, `$lock`, `$global`, and `$asset`, so
|
|
155
|
+
SERVER-side economy and persistence logic runs locally:
|
|
156
|
+
- `$room` — full room state/user-state/collections: `getMyState`/`updateMyState`,
|
|
157
|
+
`getRoomState` (always carries the `roomId` + `$users` defaults) / `updateRoomState`,
|
|
158
|
+
`getUserState`/`updateUserState`, `getAllUserStates` (array, each with `account`),
|
|
159
|
+
`countUsers`, room collections (`add`/`get`/`getCollectionItems`/`countCollectionItems`/
|
|
160
|
+
`delete`/`deleteCollection`), and `broadcastToRoom`.
|
|
161
|
+
- `$global` — `joinRoom`/`leaveRoom`, process-lifetime global state (`getGlobalState`/
|
|
162
|
+
`updateGlobalState`), per-account global user state (`getMyState`/`updateMyState`/
|
|
163
|
+
`getUserState`/`updateUserState`), global collections (`add`/`update`/`delete`/
|
|
164
|
+
`get`/`getCollectionItems`/`countCollectionItems` with `filters`/`orderBy`/`limit`
|
|
165
|
+
query options), and room management (`countRoomUsers`/`getAllRoomIds`/
|
|
166
|
+
`getRoomUserAccounts`/`getRoomState`/`updateRoomState`/…).
|
|
167
|
+
- `$asset` — a per-account currency ledger: `mint`/`burn`/`has`/`get`/`getAll`/
|
|
168
|
+
`transfer` (`burn`/`transfer` throw on an insufficient balance, and all amounts
|
|
169
|
+
must be non-negative & finite — surfacing economy bugs just as live would).
|
|
170
|
+
|
|
171
|
+
Client-side GLOBAL subscriptions ARE emulated: the preview client reacts to
|
|
172
|
+
`globalState`/`globalMyState`/`asset`/`globalCollection`/`globalMessage` (surfaced as
|
|
173
|
+
NetworkManager signals above), and `$global.broadcastToAll`/`sendMessageToUser` deliver
|
|
174
|
+
to `onGlobalMessage` (account-targeted for the latter).
|
|
175
|
+
|
|
176
|
+
Still NOT emulated (use these only LIVE): `$room.sendMessageToUser` targeted ROOM
|
|
177
|
+
delivery (the preview room message bus isn't account-scoped — a callable no-op;
|
|
178
|
+
`$room.broadcastToRoom` DOES deliver) and system handlers like `$onItemPurchased`
|
|
179
|
+
(VX shop). And the platform's hard constraints (isolated-vm, real concurrency, rate
|
|
180
|
+
limits, durable persistence) are still not enforced — a game green in preview must be
|
|
181
|
+
re-tested live.
|
|
182
|
+
|
|
183
|
+
Runnable reference: `examples/arena-preview` (Coin Dash — server-authoritative
|
|
184
|
+
`$lock`'d coin claim + `$roomTick` match clock, two clients split-screen). Going
|
|
185
|
+
live is the one-line transport swap below.
|
|
186
|
+
|
|
187
|
+
## Going live on agent8 (gameserver-sdk v2)
|
|
188
|
+
|
|
189
|
+
The agent8 platform exposes NOTHING client-callable by default — every function
|
|
190
|
+
NetworkManager calls (`joinRoom`/`leaveRoom`/`setMyState`/`patchRoomState`/
|
|
191
|
+
`addEntity`/`updateEntity`/`removeEntity`/`sendEvent`) must be defined in YOUR
|
|
192
|
+
server code. incanto ships that body for both server styles:
|
|
193
|
+
|
|
194
|
+
**Structured project (v2, recommended).** Server code lives in `server/src/server.ts`.
|
|
195
|
+
NEVER hand-create `server/package.json`, `server/tsconfig.json`, or `server/src/server.ts`
|
|
196
|
+
— the init command generates them:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
npx -y @agent8/gameserver-node init # only if server/ does not exist yet
|
|
200
|
+
# then replace server/src/server.ts with the incanto kernel body:
|
|
201
|
+
# node_modules/incanto/templates/agent8-server.ts (export class Server { … })
|
|
202
|
+
npx -y @agent8/gameserver-node test # write + run server tests
|
|
203
|
+
npx -y @agent8/gameserver-node build # generates server/dist/server.js
|
|
204
|
+
# then DEPLOY = push to the repository — the platform auto-builds + deploys.
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Legacy (root `server.js`).** Only for projects that already ship one. Body:
|
|
208
|
+
`node_modules/incanto/templates/agent8-server.js` (`class Server`, NO `export`).
|
|
209
|
+
|
|
210
|
+
The client side is unchanged — `await NetworkManager.create(engine)` uses the
|
|
211
|
+
built-in `createAgent8Server()` transport (verse/account/auth auto-resolve via the
|
|
212
|
+
SDK). Develop against `LoopbackHub`, then drop the `{ transport }` option to go live.
|
|
213
|
+
|
|
214
|
+
> **Cross-links — defer server-code specifics to the service skills, don't duplicate them here:**
|
|
215
|
+
> - **`gameserver-sdk-v2`** — the authoritative server AND client reference: the
|
|
216
|
+
> `$global`/`$room`/`$sender`/`$asset`/`$lock` contexts, `$roomTick`, collection/state
|
|
217
|
+
> APIs, the init/test/build/deploy flow, the isolated-vm limits, AND client connection +
|
|
218
|
+
> auth/identity (`useGameServer`, `server.connect`, `$sender.isGuest`/`isFollower`/
|
|
219
|
+
> `isSubscriber`, Verse8 identity). Read it before editing `server/src/server.ts` and for
|
|
220
|
+
> how the live client connects.
|
|
221
|
+
> - **`gameserver-sdk`** — legacy v1 single-file `server.js` reference.
|
|
222
|
+
|
|
223
|
+
How incanto's model maps onto v2 rooms:
|
|
224
|
+
- **`network: {mode:'owner', sync:[...]}`** → throttled `setMyState(roomId, {sync:{…}})`
|
|
225
|
+
→ server `$room.updateMyState(patch)` (shallow merge) → surfaces to every other
|
|
226
|
+
client through v2's `subscribeRoomAllUserStates` (account-keyed after the adapter).
|
|
227
|
+
- **`NetworkSpawner` `source:'users'`** consumes `manager.latestUserStates` (the
|
|
228
|
+
account-keyed snapshot), spawning one instance per OTHER account; `onRoomUserJoin/Leave`
|
|
229
|
+
drive `userJoined`/`userLeft`. A user leaving simply disappears from the next
|
|
230
|
+
all-user-states array, so the spawner despawns them.
|
|
231
|
+
- **`source:'collection:<id>'`** consumes `addEntity`→`$room.addCollectionItem` etc.,
|
|
232
|
+
mirrored by v2's `subscribeRoomCollection` (keyed by `__id` after the adapter).
|
|
233
|
+
|
|
234
|
+
Kernel constraints (agent8 platform facts — don't fight them):
|
|
235
|
+
- a NEW Server instance per request: `this.*` never persists; use `$global`/`$room`,
|
|
236
|
+
and `$roomTick` (100–1000ms) for timed logic — NO setTimeout/setInterval/Node builtins/fetch
|
|
237
|
+
- `updateMyState`/`updateRoomState` are SHALLOW merges — keep state maps flat; you can't delete by omission
|
|
238
|
+
- room data is ephemeral — persist to `$global` before rooms empty
|
|
239
|
+
- never expose unthrottled per-frame remoteFunction (rapid calls are rejected); guard score/economy with `$lock`
|
|
240
|
+
|
|
241
|
+
## State rules
|
|
242
|
+
|
|
243
|
+
- JSON-safe by construction: vectors as arrays, no `undefined`/`NaN`/typed arrays
|
|
244
|
+
- Reserved names (never use as keys): `$users`, `roomId`, `account`, `__id`, `__roomId`,
|
|
245
|
+
`__updated`, `__leaved`, anything `$`-prefixed
|
|
246
|
+
- `LoopbackHub` implements the SAME room protocol as the agent8 server templates,
|
|
247
|
+
so passing locally proves your game LOGIC + data flow are right — necessary, but
|
|
248
|
+
NOT sufficient. The preview does NOT enforce the platform's hard constraints, so a
|
|
249
|
+
game that's green locally can still misbehave live; you MUST re-test live for:
|
|
250
|
+
- **isolated-vm limits** — `setTimeout`/`setInterval`/`fetch`/Node builtins run
|
|
251
|
+
fine in preview but are REJECTED live (timed logic must be `$roomTick`);
|
|
252
|
+
- **concurrency** — the preview serializes every call, so a FORGOTTEN `$lock`
|
|
253
|
+
still passes locally; live, parallel requests race (double-award, last-write-wins);
|
|
254
|
+
- **rate limits** — unthrottled per-frame `remoteFunction` passes locally, throttled live;
|
|
255
|
+
- **persistence/ephemerality** — preview state lives forever in a Map; live, room
|
|
256
|
+
data is cleared when the last user leaves (persist to `$global` before then);
|
|
257
|
+
- **wire shapes & auth** — array/delta conversions and `$sender.account` trust
|
|
258
|
+
exist only on the live adapter.
|
|
259
|
+
- Runnable references: `examples/arena-loopback` (two clients on one `LoopbackHub`)
|
|
260
|
+
and `examples/arena-preview` (the real `server/src/server.ts` run locally via
|
|
261
|
+
`LocalGameServer` — server-authoritative scoring + `$roomTick`). Going live = the
|
|
262
|
+
client transport swap (for a normal single-client game, just omit `{transport}`;
|
|
263
|
+
a split-screen demo like arena-preview also drops its local-only scaffolding) PLUS
|
|
264
|
+
deploying `server/` (the "Going live on agent8" section above) — not literally one line.
|