@genex-ai/cli-demo 0.28.0 → 0.30.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "0.28.0",
3
+ "version": "0.30.0",
4
4
  "description": "Set up your ~/.claude workspace, authorize, create a game project, generate AI assets, and publish (genex CLI).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -69,6 +69,24 @@ keep its renderer, structure, and conventions — extend, don't rebuild.
69
69
  Running `genex init` in such a folder only wires identity/publishing and
70
70
  refreshes the genex-owned skills; the game's own files are never touched.
71
71
 
72
+ ## Hosting your game's source
73
+
74
+ By default `genex init` creates a **managed repo** for your source — you push to
75
+ it over HTTPS with a token the API mints per push, so it works from any device
76
+ with no SSH key, and `preview`/`publish` save the source there automatically.
77
+
78
+ To keep the source in **your own git repo** instead, pass it at init:
79
+
80
+ ```bash
81
+ npx genex init my-game --repo https://git.example.com/you/my-game.git
82
+ ```
83
+
84
+ Then `preview`/`publish` push the source to **your** repo using the git
85
+ credentials `git push` already uses on this machine (https or ssh). Point it at a
86
+ repo dedicated to this game — each publish overwrites its `main` with the current
87
+ source. Without push access, publish stops with a clear error and nothing ships.
88
+ The playable game is served by Genex either way.
89
+
72
90
  ## Publishing
73
91
 
74
92
  Before your first `genex preview`, the play URL (`https://<slug>.genex.technology/`)
@@ -40,7 +40,7 @@ npm i @genex-ai/multiplayer@^0.8.0
40
40
  > Pin `@^0.8.0` (not a bare `npm i`): `inputs`/`onHostTick`, auto-reconnect, and the `reconnecting`
41
41
  > events this skill relies on landed in 0.8. An older resolve would throw `room.onHostTick is not a function` at runtime.
42
42
 
43
- This skill targets `@genex-ai/multiplayer` **≥ 0.8.0** (`objects`/`host` since 0.4; `matchmake()` since 0.5; presets + `score()`/`finish()` since 0.6; `createPrivate()`/`joinPrivate()` since 0.7; matchmake auto-retry + `retry()` since 0.7.1; auto-reconnect + `inputs`/`onHostTick` since 0.8).
43
+ This skill targets `@genex-ai/multiplayer` **≥ 0.8.0** (`objects`/`host` since 0.4; `matchmake()` since 0.5; presets + `score()`/`finish()` since 0.6; `createPrivate()`/`joinPrivate()` since 0.7; matchmake auto-retry + `retry()` since 0.7.1; auto-reconnect + `inputs`/`onHostTick` since 0.8; soft ownership handoff — claim-on-touch objects glide instead of teleporting — since 0.8.4).
44
44
 
45
45
  ## Trust model (say it plainly in your game's copy)
46
46
 
@@ -300,16 +300,22 @@ For host-simulated NPCs, the host claims and drives each enemy as an `object`; w
300
300
  leaves, its enemies are reassigned to the new host, which reads their `stateRaw` and keeps
301
301
  simulating. See the co-op recipe in [references/genre-recipes.md](references/genre-recipes.md).
302
302
 
303
- ### Contested physics (two players pushing ONE thing) — host-authoritative
303
+ ### Pushable / contested physics pick the tier
304
304
 
305
- Claim-on-touch is perfect for one-touch objects (kick a ball). It **breaks down under
306
- sustained contact** two players pushing the same crate steal ownership back and forth and
307
- the crate judders. For contested objects, ONE simulation must own the contest: the **host**
308
- runs the physics for those objects; everyone else sends **inputs**
309
- (`room.inputs.send({ push })`), which the relay routes to the host only; the host applies
310
- them on `room.onHostTick(...)` and publishes results via `objects` (smooth for everyone).
311
- Full recipe including surviving host migration and wiring the Rapier controllers
312
- in [references/host-physics.md](references/host-physics.md).
305
+ Claim-on-touch is the default for any **ownable** body a ball, a box, a prop, a pickup — even when
306
+ players take turns bumping it: whoever touches it owns + simulates it with real Rapier physics, and
307
+ since `@genex-ai/multiplayer` 0.8.4 the ownership handoff **glides** (a soft handoff) instead of
308
+ teleporting. Give it a Rapier proxy that is `dynamic` while you own it (your character shoves it through
309
+ the solver) and a `kinematicPosition` follower of the smoothed `state` when you don't.
310
+
311
+ Reserve **host-authoritative** for a genuine *simultaneous* contest two players pushing ONE crate
312
+ *against each other*, sumo, tug-of-war — where "whoever touched last owns it" is the wrong model. There
313
+ one neutral simulation (the host) runs the physics; everyone else sends **inputs**
314
+ (`room.inputs.send({ push })`), which the relay routes to the host only; the host applies them on
315
+ `room.onHostTick(...)` and publishes results via `objects` (smooth for everyone).
316
+
317
+ Both patterns — the claim-on-touch Rapier proxy and the host-authoritative contest, plus surviving host
318
+ migration and wiring the vendored controllers — are in [references/host-physics.md](references/host-physics.md).
313
319
 
314
320
  ## Smoothness is felt, not seen — hand the feel to a human
315
321
 
@@ -397,7 +403,7 @@ host-driven saving works as long as ANY account is in the room.
397
403
  - [ ] `npm i @genex-ai/multiplayer@^0.8.0` (auto-reconnect, `inputs`, `onHostTick` — pin `^0.8.0`, a bare install can resolve older); config wired into the build.
398
404
  - [ ] `reconnecting`/`reconnected`/`disconnect` render an overlay (don't tear the scene down).
399
405
  - [ ] Numbers rounded (~2 decimals) before `me.set`/`objects.set`.
400
- - [ ] Contested (sustained-contact) objects use the host-physics pattern, not claim-on-touch.
406
+ - [ ] Pushable/ownable objects (ball, box, prop) use claim-on-touch + a Rapier proxy (the soft handoff glides the handoff); only a genuine simultaneous tug-of-war (sumo) uses the host-authoritative pattern. See host-physics.md.
401
407
  - [ ] Idle/unchanged objects republish at ≤2 Hz keepalive, never every tick (message budget).
402
408
  - [ ] Host renders objects OWNED BY OTHERS from the stream (authority follows ownership).
403
409
  - [ ] `connect()` runs AFTER `await waitForPlayer()` (never `waitForAuth()` — guests would
@@ -1,22 +1,118 @@
1
- # Host-authoritative physics — contested objects + networked controllers
1
+ # Networked physics — pushable/ownable objects, contested objects + controllers
2
2
 
3
- Two networking tiers exist for moving things, and picking the right one per object is the
3
+ Three networking tiers exist for moving things, and picking the right one per object is the
4
4
  whole trick:
5
5
 
6
- | Object kind | Tier | Why |
6
+ | Object kind | Tier | How |
7
7
  | --- | --- | --- |
8
- | One-touch (kick a ball, throw a crate once) | **claim-on-touch** (`objects.claim` on contact, owner simulates) | lowest latency your kick lands instantly |
9
- | Sustained contact / contested (two players pushing one crate, tug-of-war, sumo, shared vehicles) | **host-authoritative** (this doc) | one simulation owns the contest no ownership ping-pong, physics stays consistent |
8
+ | **Pushable / ownable body** — a ball, a crate you shove, a physics prop, a pickup; one player interacts at a time (even if players take turns) | **claim-on-touch + a Rapier proxy** (Tier 1) | whoever touches it owns + simulates it locally with REAL physics; the soft handoff glides ownership changes |
9
+ | **Genuine simultaneous contest** two players pushing ONE object *against each other*: sumo, tug-of-war, a shared crate nobody should "own" | **host-authoritative** (Tier 2) | one neutral simulation (the host) runs the contest; everyone else sends inputs |
10
+ | **Your own controller** — the vendored character / vehicle / drone rig | **publish-and-playback** (Tier 3) | each player simulates their OWN rig; remotes replay the pose |
10
11
 
11
- Claim-on-touch under sustained contact means every contact steals ownership, resets the
12
- simulation, and the object judders between two owners' views. Host-authoritative kills that
13
- by construction: **the host runs the ONE Rapier world for contested objects; everyone else
14
- sends inputs.**
12
+ The old advice was "claim-on-touch breaks down under sustained contact, use host-authoritative for
13
+ anything contested." That was true only because an ownership handoff used to **teleport** the object
14
+ the interpolation buffer reset on every claim. **Since `@genex-ai/multiplayer` 0.8.4 the handoff is
15
+ a soft handoff: the render glides across the ownership change instead of snapping** — so claim-on-touch
16
+ is now the right default for any *ownable* body (a ball, a box, a prop), even when players take turns
17
+ bumping it. Reserve host-authoritative (Tier 2) for a genuine *simultaneous* tug-of-war where handing
18
+ ownership to "whoever touched last" is itself the wrong model.
15
19
 
16
- ## The pattern (complete)
20
+ ## Tier 1 — pushable / ownable body (claim-on-touch + a Rapier proxy)
17
21
 
18
- Every client runs this same code `onHostTick` only fires on the current host, so there is
19
- no "am I host?" bookkeeping and host migration is automatic:
22
+ The object is a REAL Rapier body on every client, and its body TYPE follows ownership:
23
+
24
+ - **You own it → a `dynamic` body.** Your (dynamic) character capsule collides with it and Rapier's
25
+ solver resolves the push on the fixed substep — real momentum, spin, friction, bounce, zero latency.
26
+ A *regular pushable object*, not a dribble and not an attachment.
27
+ - **You don't own it → a `kinematicPosition` body** you drive each frame to the smoothed
28
+ `objects.get(id).state`. It's solid, so your character physically bumps it — and that bump fires your
29
+ claim.
30
+
31
+ This is ONE body that IS the render — not a second "prediction" body running alongside the stream
32
+ (that would be double-simulation, the sibling of double-smoothing). When you own it you draw the dynamic
33
+ body; when you don't you draw the smoothed stream and park the kinematic body on it.
34
+
35
+ ```ts
36
+ import RAPIER from "@dimforge/rapier3d-compat";
37
+ // Proxy body: kinematic to start (owned by nobody, or by someone else).
38
+ const body = physics.createBody({ type: "kinematicPosition", position: [x, y, z], ccd: true });
39
+ ballCollider(physics.world, body, RADIUS, { friction: 0.5, restitution: 0.35, density: 0.6 });
40
+ let wasTouching = false, wasMine = false, claimCd = 0;
41
+
42
+ // Per-frame, BEFORE physics.step (so the kinematic proxy is in place for the character to hit it):
43
+ function updateBall(dt: number) {
44
+ let v = room.objects.get("ball");
45
+ let mine = !!v?.isMine;
46
+
47
+ // Rising-EDGE claim: sustained contact yields NO new rising edge, so ONE owner holds it
48
+ // (no per-frame ownership ping-pong). A short cooldown throttles a steal-war.
49
+ const touching = onFoot && feet.distanceTo(ballMesh.position) < RADIUS + REACH;
50
+ if (claimCd > 0) claimCd -= dt;
51
+ if (touching && !wasTouching && !mine && claimCd <= 0) {
52
+ room.objects.claim("ball"); // optimistic: isMine flips true locally THIS frame
53
+ claimCd = 0.25;
54
+ v = room.objects.get("ball"); mine = !!v?.isMine;
55
+ }
56
+ wasTouching = touching;
57
+
58
+ if (mine && !wasMine) {
59
+ // Gained ownership → dynamic. SEED velocity from the wire (NEVER zero, or a rolling ball
60
+ // stalls on the handoff); keep the position where the proxy already renders (no snap).
61
+ body.setBodyType(RAPIER.RigidBodyType.Dynamic, true);
62
+ const raw = v?.stateRaw;
63
+ body.setLinvel({ x: raw?.vx ?? 0, y: raw?.vy ?? 0, z: raw?.vz ?? 0 }, true);
64
+ } else if (!mine && wasMine) {
65
+ body.setBodyType(RAPIER.RigidBodyType.KinematicPositionBased, true); // lost → follower
66
+ }
67
+ wasMine = mine;
68
+
69
+ if (mine) {
70
+ ballMesh.position.copy(body.translation()); // draw the dynamic body I simulate
71
+ ballMesh.quaternion.copy(body.rotation());
72
+ } else if (v?.state) {
73
+ ballMesh.position.set(v.state.x, v.state.y, v.state.z); // draw the smoothed stream
74
+ body.setNextKinematicTranslation(ballMesh.position); // keep the proxy on it → I can bump → claim
75
+ }
76
+ }
77
+
78
+ // Owner-gated publish on your fixed tick (30Hz for a fast ball) — moved-only + a ~2Hz keepalive.
79
+ // Publish VELOCITY too, so the next owner seeds from it and the handoff doesn't stall.
80
+ if (room.objects.get("ball")?.isMine) {
81
+ const t = body.translation(), q = body.rotation(), lv = body.linvel();
82
+ room.objects.set("ball", { x: r2(t.x), y: r2(t.y), z: r2(t.z),
83
+ q: [r2(q.x), r2(q.y), r2(q.z), r2(q.w)], vx: r2(lv.x), vy: r2(lv.y), vz: r2(lv.z) });
84
+ }
85
+ const r2 = (v: number) => Math.round(v * 100) / 100;
86
+ ```
87
+
88
+ Rules that make it correct:
89
+
90
+ - **Rising-edge claim + a short cooldown** (~0.25s), never "am I touching": sustained contact must not
91
+ re-claim every frame. Add a minimum-hold (~0.5s) before you'll `release`, so a shove finishes under
92
+ your sim.
93
+ - **Never zero velocity on claim** — seed `linvel` from the published `vx/vy/vz`, or a rolling ball snaps
94
+ to a dead stop on every handoff.
95
+ - **Publish velocity, not just position** — the next owner seeds from it, so the object keeps its motion
96
+ across the handoff.
97
+ - **Release-on-rest** (optional): once you own an object that's been at rest + untouched a couple of
98
+ seconds, `room.objects.release("ball")` so ownership doesn't pile up on a lone wanderer — its last
99
+ state persists on the wire for the next toucher, who re-claims.
100
+ - **Distributed by construction:** each client publishes only what it currently owns, so the message
101
+ budget spreads across players instead of funnelling every object through one host's send budget. This
102
+ is why Tier 1 scales to many pushable objects where a host-authoritative fleet (Tier 2) would blow one
103
+ client's rate cap.
104
+ - **Do NOT `registerBody(proxy, mesh)`** — drive the mesh yourself (from the body when you own it, from
105
+ the stream when you don't). Registering it fights the SDK's smoothing on the stream half.
106
+
107
+ ## Tier 2 — genuine simultaneous contest (host-authoritative)
108
+
109
+ For a real tug-of-war — two players pushing ONE crate *against each other*, sumo — handing ownership to
110
+ "whoever touched last" is the wrong model: a single neutral simulation must own the contest. The **host**
111
+ runs the ONE Rapier world for those objects; everyone else sends **inputs**, which the relay routes to
112
+ the current host only.
113
+
114
+ Every client runs this same code — `onHostTick` only fires on the current host, so there is no "am I
115
+ host?" bookkeeping and host migration is automatic:
20
116
 
21
117
  ```ts
22
118
  // ---- inputs: NON-hosts (and the host itself) report intent, ~10-15Hz or on action ----
@@ -69,13 +165,16 @@ Rules that make it correct:
69
165
  via `stateRaw`.
70
166
  - **Latency honesty:** a non-host's push lands after ~RTT to the relay. At casual scale that
71
167
  reads as weight, not lag. The host's own pushes are instant — that asymmetry is the tier's
72
- price; don't fight it with client-side guessing.
73
- - **Rate budget:** the host publishes N contested objects at 20–30 Hz; keep N modest (≤ ~10)
74
- and state flat + quantized. Inputs are single-receiver and cheap.
75
- - **Do not** run a second Rapier body for a contested object on non-hosts "for prediction" —
76
- that's the double-simulation version of double-smoothing. Draw `state`.
168
+ price. (Tier 1 has no such asymmetry — the owner IS the simulator — which is another reason to
169
+ prefer it for a plain pushable body.)
170
+ - **Rate budget:** the host publishes N contested objects at 20–30 Hz; keep N modest (≤ ~8) and
171
+ state flat + quantized. Inputs are single-receiver and cheap.
172
+ - **Do not** run a second Rapier body for a Tier-2 object on non-hosts "for prediction" — that's the
173
+ double-simulation version of double-smoothing. Non-hosts draw `state`. (This is the opposite of the
174
+ Tier-1 proxy, which is a single body that IS the render for the object you OWN — not a shadow sim of
175
+ one the host owns.)
77
176
 
78
- ## Networked controllers (the vendored character / vehicle / drone)
177
+ ## Tier 3 — networked controllers (the vendored character / vehicle / drone)
79
178
 
80
179
  The `genex controller` controllers are **local-only physics** — each player simulates their
81
180
  OWN rig (self-authoritative, zero latency). Networking them is publish-and-playback, never
@@ -131,5 +230,5 @@ What to publish per controller:
131
230
 
132
231
  Player-vs-player physical contact (bumping cars) stays approximate at this tier — each
133
232
  client is authoritative over itself, so contacts are cosmetic. If a game's core loop IS
134
- contested vehicle contact, that's the host-authoritative tier above, with the vehicles as
135
- host-simulated objects and player inputs over `inputs.send`.
233
+ contested vehicle contact, that's Tier 2 above, with the vehicles as host-simulated objects and
234
+ player inputs over `inputs.send`.
@@ -87,7 +87,9 @@ enemies). That skill covers the rules that keep it smooth (draw `state` directly
87
87
  yourself and objects you own from a local object, quaternion rotation, `stateRaw` for
88
88
  hit-tests), the per-genre recipes (sports/ball, shooter, co-op), config wiring, and the
89
89
  persistent-world API. Reconnection is built into the SDK (render `reconnecting`/
90
- `reconnected`, never rebuild it), and **contested physics** (two players pushing one
91
- objectsumo, tug-of-war, shared crates) routes to its host-authoritative pattern
92
- (`inputs` + `onHostTick`, see that skill's host-physics reference)never claim-on-touch.
90
+ `reconnected`, never rebuild it). **Pushable/ownable bodies** (a ball, a box, a prop) use
91
+ claim-on-touch + a Rapier proxy the soft handoff glides the ownership change; only a genuine
92
+ **simultaneous** contest (two players pushing one crate against each othersumo, tug-of-war)
93
+ uses the host-authoritative pattern (`inputs` + `onHostTick`). Both are in that skill's
94
+ host-physics reference.
93
95
  Use only the APIs that skill documents — do not invent transport methods.