@genex-ai/cli-demo 0.92.0-dev.231 → 0.93.0-dev.234

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "0.92.0-dev.231",
3
+ "version": "0.93.0-dev.234",
4
4
  "description": "Set up your project's agent workspace (.claude/.codex/.cursor in the game folder), authorize, create a game project, generate AI assets, and publish (genex CLI).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -331,6 +331,23 @@ halves of this rule are non-negotiable:
331
331
  `minPlayers: 2, maxPlayers: 2`: the instant the second player joins). If it doesn't close on
332
332
  both, the lobby is broken — fix it; do not ship a waiting room you haven't watched close.
333
333
 
334
+ **While `status === 'searching'` the roster is EMPTY — count the queue, not `players`.** The
335
+ matchmaker never parks a searcher in a room as a non-player: until it can seat you, you are in the
336
+ search line and in no room at all, so `players`/`connectedPlayers` are `[]`. A pre-match screen that
337
+ counts `players.length` there shows `0 / 2` to a player who is sitting in it, forever. Use the queue
338
+ count — it includes YOU, so it reads `1 / 2` when you are alone — and switch to the roster the moment
339
+ you are seated:
340
+
341
+ ```ts
342
+ const v = mm.matchmaking;
343
+ const lobbyCount = v.status === "searching" ? Math.max(v.queue.size, 1) : v.connectedPlayers.length;
344
+ ```
345
+
346
+ This bites hardest in the commonest co-op shape: with `minPlayers === maxPlayers` (e.g. `2` and `2`)
347
+ the room can only be formed once BOTH searchers exist, so you go `searching → playing` and **never
348
+ pass through `waiting`** — the screen the player stares at is the SEARCHING screen, and driving it
349
+ off `players.length` pins it at `0 / target` for its entire life.
350
+
334
351
  Two ways to present the lobby:
335
352
 
336
353
  - **A) UI lobby (Dota-style).** While `status === 'waiting'`, render an OVERLAY from `mm.matchmaking`
@@ -552,6 +569,35 @@ fights (many writers). A ball on `objects` glides and has one owner. That's the
552
569
  nearest `tier.remoteAvatarCap` remotes (`$genex-threejs-adaptive-quality`) — freeze the
553
570
  mixer and billboard or hide the rest; a room allows up to 64 players and 64 live avatars
554
571
  is a real phone memory kill on its own.
572
+ - **Build each remote's body exactly ONCE — reserve the id BEFORE the first `await`.** You read
573
+ `room.players` every frame, but loading a body is async and slow (seconds). If the only guard is
574
+ the map you fill *after* the await, every frame in that window passes it: you spawn one body per
575
+ frame, each stranded where it spawned — a trail of frozen clones behind the moving player — plus
576
+ dozens of wasted loads. Mark the id synchronously, and drop the body if that player left mid-load:
577
+
578
+ ```ts
579
+ const bodies = new Map<string, Body>();
580
+ const loading = new Set<string>(); // the seat reservation
581
+ function ensureBody(p: Player) {
582
+ if (bodies.has(p.id) || loading.has(p.id)) return; // BOTH — checked before any await
583
+ loading.add(p.id);
584
+ void (async () => {
585
+ try {
586
+ const body = await loadVrmClone(p.avatarUrl || "./assets/avatar.vrm");
587
+ if (!room.activePlayers.has(p.id)) return; // left while loading — never add it
588
+ scene.add(body.scene);
589
+ bodies.set(p.id, body);
590
+ } finally { loading.delete(p.id); } // always release, success or throw
591
+ })();
592
+ }
593
+ // Each frame: ensureBody for every remote, then drop bodies whose id is gone.
594
+ for (const [id, body] of bodies) {
595
+ if (!room.activePlayers.has(id)) { scene.remove(body.scene); bodies.delete(id); }
596
+ }
597
+ ```
598
+
599
+ The same rule covers anything else you lazily load per player or per object (nameplates, weapon
600
+ models, audio): a check that straddles an `await` is not a guard.
555
601
  - `room.activePlayers` — the connected-only subset of `room.players`; use its size for live quorum.
556
602
  - `room.objects` — shared objects nobody owns until claimed (a ball, an NPC):
557
603
  - `claim(id)` — **legacy** optimistic request. It flips local ownership immediately and is corrected