@genex-ai/cli-demo 1.2.4-dev.363 → 1.3.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.
@@ -46,7 +46,8 @@ npm i @genex-ai/multiplayer@^0.12.0
46
46
  > landed in 0.10; confirmed object controls, snaps, host-tick teardown, and reconnect rebasing
47
47
  > in 0.9. An older resolve does not have those.
48
48
 
49
- This skill targets `@genex-ai/multiplayer` **≥ 0.13.0** (`objects`/`host` since 0.4; verified per-player `avatarUrl` since 0.12;
49
+ This skill targets `@genex-ai/multiplayer` **≥ 0.15.0** (`objects`/`host` since 0.4; verified per-player `avatarUrl` since 0.12;
50
+ room text chat via `room.chat` since 0.14; mesh voice via `room.voice` since 0.15;
50
51
  `matchmake()` since 0.5; private lobbies since 0.7; auto-reconnect + `inputs`/`onHostTick`
51
52
  since 0.8; soft ownership handoff since 0.8.4; confirmed controls, snap epochs, and host-tick
52
53
  lifecycle guarantees since 0.9; regional relay selection via `getColyseusUrls()` since 0.10; host-only `setRoomOpen()` since 0.13;
@@ -536,9 +537,11 @@ of five kinds — put each on its channel and the game just works:
536
537
  | Your own avatar (position, rotation, anim) | `me.set` → others read `players` | you (each player their own) |
537
538
  | A moving thing nobody owns (**ball**, puck, NPC) | `objects` (claim + set) | the one current **owner** |
538
539
  | Slow agreed facts (score, round, wave, seed) | `shared` | the **host** (`isHost`) |
539
- | One-off actions (shot, emote, hit, chat) | `send` + `on` | whoever did it |
540
+ | One-off actions (shot, emote, hit) | `send` + `on` | whoever did it |
540
541
  | Discrete per-player values (hp, ammo, flags) | in `me.set`, read via `stateRaw` | you |
541
542
  | Which avatar MODEL a player is (VRM look) | already on `players` as `p.avatarUrl` — sync nothing | the **relay** (verified identity) |
543
+ | **Text chat** | `room.chat` — its own channel, NOT `send` | the **relay** (stamps the sender) |
544
+ | **Voice chat** | `room.voice` — peer-to-peer audio, max 6 | nobody (the relay carries no audio) |
542
545
 
543
546
  Getting the channel right is the whole game. A ball on `shared` stutters (not smoothed) and
544
547
  fights (many writers). A ball on `objects` glides and has one owner. That's the difference.
@@ -651,6 +654,28 @@ fights (many writers). A ball on `objects` glides and has one owner. That's the
651
654
  you**, so apply your own action's local effect directly (draw your own tracer at fire time),
652
655
  not inside `on(...)`. Relay-internal names (`state`, `shared`, `claim`, `obj`, `release`,
653
656
  `destroy`, `match:*`, `__*`) are refused — pick your own event names. `room.leave()`.
657
+ - `room.chat` — room text chat, **its own channel, not `send`**: `chat.send(text)`,
658
+ `chat.on(m => …)`, `chat.history`, `chat.mute(id)` / `chat.unmute(id)` / `chat.muted`.
659
+ The relay stamps `m.from`/`m.name` from the verified session (nobody can post under
660
+ another player's name), sanitizes `m.text`, caps it at 500 chars, and replays the last
661
+ ~50 lines so a late joiner sees the conversation. **Your own lines echo back to you** —
662
+ render only what arrives in `on`, or every message appears twice. UI:
663
+ `npx genex controller chat`, and you MUST pass its `onTypingChange` (or check
664
+ `chat.isTyping`) so the game stops reading the keyboard while someone types — otherwise
665
+ typing "was that a wall?" walks and jumps the player. Render `m.text` with `textContent`,
666
+ never `innerHTML`. See [references/realtime-patterns.md](references/realtime-patterns.md).
667
+ - `room.voice` — mesh voice chat: `voice.join(opts)` / `leave()` / `setMicEnabled(on)` /
668
+ `mute(id)` / `participants` / `active` / `speaking`, plus `room.on('voice', …)` and
669
+ `room.on('voice:rejected', …)`. **Party-sized: the relay caps it at 6** (audio is a full
670
+ mesh — everyone uploads to everyone, and reliability, not bandwidth, breaks first). The
671
+ room still holds 64 players; the 7th `join()` is refused, so handle `voice:rejected`
672
+ instead of retrying. `join()` MUST run inside a click/tap — browsers gate both the mic
673
+ prompt and audio playback on a user gesture — and it REJECTS when the player denies the
674
+ mic, which is an ordinary outcome to show, not an error to swallow. For a 3D game use
675
+ `npx genex controller voice` (proximity audio + push-to-talk); it joins with
676
+ `{ spatial: true }` and drives a `THREE.PositionalAudio` per speaker. **Configure TURN
677
+ for anything real**: ~10–20% of players are behind symmetric NAT and connect to nobody
678
+ without it, while everyone else works — a miserable bug to diagnose from a report.
654
679
  - `room.inputs.send(payload)` / `room.inputs.on((fromId, payload) => …)` — the host-routed
655
680
  input channel for host-authoritative physics: anyone sends, ONLY the current host receives.
656
681
  See [references/host-physics.md](references/host-physics.md).
@@ -912,8 +937,8 @@ from any still capture whether movement feels smooth.** Don't try — it leads t
912
937
 
913
938
  1. **Trust the SDK's smoothing.** Draw `state` directly; don't add your own.
914
939
  2. **Verify it *runs*:** two clients, distinct meshes, both move, no console errors, each sees the
915
- other (and the ball, if any). That's all a capture can prove. Local test mode (the embed-auth
916
- skill's `?genex_local_test=1`) can NOT do this: it mints no relay credential, so `connect()`
940
+ other (and the ball, if any). That's all a capture can prove. Local test mode (what the dev
941
+ server boots into — the embed-auth skill) can NOT do this: it mints no relay credential, so `connect()`
917
942
  fails there by design and two local-test tabs never see each other — run the two-client check
918
943
  on the published game (or have the owner open their draft), and if multiplayer wasn't
919
944
  exercised, say exactly that in your handoff instead of implying it was.
@@ -180,9 +180,10 @@ room.on("host", () => {/* host migrated — the new host takes over writing */})
180
180
  de-duped) — but for a value you render every frame, read `room.shared.get(...)` in your loop (as
181
181
  `updateHud` does) rather than wiring render state through the event.
182
182
 
183
- ## Custom events (shots, emotes, chat)
183
+ ## Custom events (shots, emotes)
184
184
 
185
- For one-off actions that aren't continuous state, use `send`. The shooter judges the hit **locally**
185
+ For one-off actions that aren't continuous state, use `send`. (Text chat is NOT one of these —
186
+ it has its own channel, `room.chat`; see below.) The shooter judges the hit **locally**
186
187
  against what it sees (favor-the-shooter) and announces it; the victim applies its own damage.
187
188
 
188
189
  **`send` reaches only OTHER clients — it never echoes back to you.** So apply your own action's
@@ -210,6 +211,47 @@ if (!t) return;
210
211
  const px = t.stateRaw.x, pz = t.stateRaw.z; // test against the raw latest
211
212
  ```
212
213
 
214
+ ## Text chat
215
+
216
+ Chat has its own channel — **never build it on `send`**. The relay stamps the sender from the
217
+ verified session, sanitizes the text, and replays the recent history to late joiners; `send` gives
218
+ you none of that and lets a client post under any name it likes.
219
+
220
+ Install the overlay with `npx genex controller chat` (writes `src/controllers/chat/chat-overlay.ts`),
221
+ then wire it:
222
+
223
+ ```ts
224
+ import { ChatOverlay } from "./controllers/chat/chat-overlay.ts";
225
+
226
+ const chat = new ChatOverlay({
227
+ room,
228
+ onTypingChange: (typing) => { keyboard.enabled = !typing; }, // ← REQUIRED, see below
229
+ });
230
+ ```
231
+
232
+ **The one bug everybody ships: the game reads the keyboard while the player types.** Someone
233
+ types "was that a wall?" and their character walks, reverses and jumps. Either pass
234
+ `onTypingChange` (above) or check `chat.isTyping` in your input code. There is no third option.
235
+
236
+ Rolling your own UI instead? Three rules:
237
+
238
+ ```ts
239
+ room.chat.send("on my way"); // your OWN line comes back via on() — don't also append it
240
+ room.chat.on((m) => {
241
+ el.textContent = `${m.name}: ${m.text}`; // textContent, NEVER innerHTML — remote input
242
+ });
243
+ room.chat.history; // render on mount: a late joiner sees the conversation
244
+ ```
245
+
246
+ - **Your own lines echo back** (unlike `send`), in server order with the server's timestamp.
247
+ Appending locally on submit shows every message twice.
248
+ - **`m.name` is relay-verified** — safe to display, and a player cannot post under another name.
249
+ - **`m.text` is sanitized but not markup-escaped** — control and bidi characters are stripped by
250
+ the relay, but you must still render it as text.
251
+
252
+ Chat has its own rate budget (~2 lines/second per player), separate from gameplay traffic: a chat
253
+ flood cannot starve movement, and a busy game cannot starve chat.
254
+
213
255
  ## Persistence helper
214
256
 
215
257
  ```ts
@@ -129,13 +129,9 @@ everything twice.
129
129
  visible; the lock may only ever engage from the Play/Resume click or a
130
130
  gameplay canvas click (the phase binding `setPaused(phase !== "playing")` is
131
131
  what guarantees this — check it rides `setPhase`, not the render loop).
132
- Headless caveat, measured on Chromium 151: `requestPointerLock` does NOT
133
- throw it locks, with or without a user gesture, so the lock and the
134
- unlocked cue ARE yours to assert headless. What does not survive is the
135
- both-axes look check: synthesised mouse movement cancels to a net zero
136
- delta, so turning right then left proves nothing about direction. Assert the
137
- wiring and the cue in a screenshot, and say plainly that confirming which way
138
- the view turns needs one manual pass with a real mouse.
132
+ Headless caveat: `requestPointerLock` throws in headless Chromium —
133
+ assert the wiring and the unlocked cue in a screenshot, and say plainly that
134
+ the lock itself needs one manual click (do the both-axes look check there).
139
135
  7. **Ask the scene the three things the screenshot cannot answer** (below). Run it
140
136
  once, in the same browser you already have open.
141
137
 
@@ -38,7 +38,7 @@ update, so update immediately.)
38
38
  Run exactly the command the nudge printed, from the game project root:
39
39
 
40
40
  ```bash
41
- npm i -D @genex-ai/cli-demo@dev # the genex CLI (a dev dependency)
41
+ npm i -D @genex-ai/cli-demo@latest # the genex CLI (a dev dependency)
42
42
  npm i @genex-ai/embed-sdk@latest # identity/saves SDK (ships inside the game)
43
43
  npm i @genex-ai/multiplayer@latest # multiplayer SDK (only if the game uses it)
44
44
  ```