@genex-ai/cli-demo 1.38.0 → 1.38.1-dev.1097

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.
Files changed (25) hide show
  1. package/dist/{blender-mcp-QVEGHE5L.js → blender-mcp-WSURPZET.js} +2 -2
  2. package/dist/{blender-serve-66OHC4ML.js → blender-serve-EH2MOW3U.js} +1 -1
  3. package/dist/{chunk-4BBTPRJW.js → chunk-EABIVAGT.js} +2 -2
  4. package/dist/{chunk-QI7FIYBY.js → chunk-K4EHXOZK.js} +1 -1
  5. package/dist/index.js +1700 -811
  6. package/package.json +1 -1
  7. package/templates/skills/genex-authoritative-multiplayer/SKILL.md +208 -0
  8. package/templates/skills/genex-authoritative-multiplayer/references/module-api.md +172 -0
  9. package/templates/skills/genex-authoritative-multiplayer/references/physics.md +150 -0
  10. package/templates/skills/genex-authoritative-multiplayer/references/presets.md +394 -0
  11. package/templates/skills/genex-authoritative-multiplayer/references/projectiles.md +195 -0
  12. package/templates/skills/genex-authoritative-multiplayer/references/room-template.md +1189 -0
  13. package/templates/skills/genex-authoritative-multiplayer/references/settings.md +122 -0
  14. package/templates/skills/genex-authoritative-multiplayer/references/testing.md +118 -0
  15. package/templates/skills/genex-game-director/SKILL.md +2 -1
  16. package/templates/skills/genex-getting-started/SKILL.md +2 -2
  17. package/templates/skills/genex-llm-in-games/SKILL.md +209 -72
  18. package/templates/skills/genex-llm-in-games/references/pricing.md +126 -85
  19. package/templates/skills/genex-monetization/SKILL.md +2 -11
  20. package/templates/skills/genex-threejs-embed-auth/SKILL.md +4 -1
  21. package/templates/skills/genex-threejs-embed-auth/references/shells.md +104 -0
  22. package/templates/skills/genex-threejs-multiplayer/SKILL.md +4 -0
  23. package/templates/skills/genex-tool-llm/SKILL.md +11 -9
  24. package/templates/skills/genex-tool-publish/SKILL.md +7 -0
  25. package/templates/skills/genex-updates/SKILL.md +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "1.38.0",
3
+ "version": "1.38.1-dev.1097",
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": {
@@ -0,0 +1,208 @@
1
+ ---
2
+ name: genex-authoritative-multiplayer
3
+ description: Server-authoritative multiplayer for Genex games — the game's own room module runs on Genex's servers, players only send input, and the server decides every move and hit (lag-compensated). Use for competitive play where cheating matters: ranked PvP, arenas, shooters (first-person, hitscan or dodgeable projectiles), car football, "so nobody can cheat". Casual co-op and hangouts use genex-threejs-multiplayer instead. Load before writing the room module or any networking code.
4
+ ---
5
+
6
+ # Genex Server-Authoritative Multiplayer
7
+
8
+ In a server-authoritative game the **server runs the game's rules**. The game ships a **room
9
+ module** — one script, `server/room.ts` or `server/room.js` — that Genex runs in a sandbox at a
10
+ fixed tick. Players send typed input (`moveX`, `moveY`, `yaw`, `buttons`, …); the server moves
11
+ every body, judges every shot against where the shooter saw its target, and writes the state every
12
+ client renders. A modified client can lie only about its own stick and buttons.
13
+
14
+ The client half is `connectAuthority` from `@genex-ai/multiplayer/authority`: it predicts the
15
+ player's own body with the same movement preset the server runs (so moving feels instant), draws
16
+ everything else 100 ms in the past, and reconciles when the server's truth arrives.
17
+
18
+ ## Casual or server-authoritative
19
+
20
+ | | Casual — `$genex-threejs-multiplayer` | Server-authoritative — this skill |
21
+ |---|---|---|
22
+ | Who runs the rules | Each player's browser; Genex's relay passes messages | Genex's servers run the game's room module |
23
+ | Cheating | A modified client can say where it is and what it hit | The server decides every move and every hit |
24
+ | Fits | Co-op, hangouts, party, turn-based, casual races | Competitive shooters, arenas, ranked play — anything with a winner worth faking |
25
+ | On Genex's servers | Free | Needs Genex Pro for the game's owner |
26
+ | Building and testing | Free | Free, on this machine, with no account |
27
+
28
+ "Competitive", "ranked", "PvP", "esports", "so nobody can cheat" or "server-authoritative" mean
29
+ this skill; everything else is casual. When the person has not said which, ask in one line and say
30
+ what each costs. Never mix the two in one game: an authority game has no relay `Session`, no host
31
+ and no shared objects — the server owns all state.
32
+
33
+ ## What you write
34
+
35
+ 1. **The room module**, `server/room.ts` (or `.js`): `export default` a room with `contract: 1`,
36
+ `config`, `init`, `onJoin`, `onLeave`, `onInput` and `step`. It is server code: keep it and
37
+ everything only it uses in `server/`. Shared pure code (a movement step the client also runs)
38
+ may live in `src/` and be imported by both. The full API and the sandbox's rules are in
39
+ [references/module-api.md](references/module-api.md); four complete games to start from (an
40
+ arena with instant shots, one with bolts players can dodge, a car arena with a ball, and the car
41
+ arena on server physics with crates) are in
42
+ [references/room-template.md](references/room-template.md).
43
+ 2. **The manifest block**, in package.json:
44
+
45
+ ```json
46
+ { "genex": { "authority": { "module": "server/room.ts", "tickHz": 30, "maxPlayers": 8, "preset": "topdown" } } }
47
+ ```
48
+
49
+ A game without a package.json (one made from Studio's template) puts the same object in
50
+ `genex.json` as `{ "authority": { … } }`. `module` is a plain relative path to a `.js` or `.ts`
51
+ file inside the game folder. The rest is optional: `maxPlayers` (1 to 64) is the most a room
52
+ seats (the module may ask for fewer), `tickHz` (20, 30, 60 or 120) is the rate for a module whose
53
+ `config` names none (120 only for a module that asks for physics), and `preset` names its movement preset. Left out, each comes from the
54
+ module's own `config`. A malformed block is not ignored — `genex preview` refuses it and says why.
55
+ 3. **The client**: `connectAuthority` with the identity from `$genex-threejs-embed-auth`, input
56
+ written to `session.input`, `session.frame(now)` once per render frame, and the scene drawn
57
+ from `session.entities()`.
58
+ 4. **The packages**: `npm i @genex-ai/multiplayer@latest @genex-ai/embed-sdk@latest` and
59
+ `npm i -D @genex-ai/authority-dev`. The game needs a `@genex-ai/multiplayer` that exports
60
+ `@genex-ai/multiplayer/authority` and an embed SDK at 0.35.0 or newer (it has
61
+ `getAuthorityUrls`). If the import does not resolve after installing `@latest`, the release with
62
+ server-authoritative rooms is not out yet: tell the person plainly and stop, rather than writing a
63
+ server of your own. Projectiles need `@genex-ai/multiplayer` 0.17.4 or newer.
64
+
65
+ ## Rules that decide whether it plays well
66
+
67
+ 1. **The server decides; the client asks.** Health, score, pickups, kills, respawns and round
68
+ state change only in the room module, and the client draws them from `session.entities()` and
69
+ the module's events. Show a muzzle flash or play a sound at once if you like, but never draw a
70
+ hit, a kill or a score the server has not sent.
71
+ 2. **Never move your own body yourself.** Write the stick and aim into `session.input`; the session
72
+ predicts your body with the room's preset and corrects it. A game whose movement is not the
73
+ preset passes its own step as `predict: { step }` — the exact step the module runs
74
+ ([references/presets.md](references/presets.md)).
75
+ 3. **The module must be deterministic.** The same seed and inputs must give the same room: no
76
+ clock, no timers, and randomness only from `ctx.random()` (`Math.random` is the same seeded
77
+ generator inside the sandbox). `validate` and `replay` check this, and a publish refuses a
78
+ module that fails.
79
+ 4. **Bump `epoch` to teleport.** A respawn or a teleport that does not bump the entity's `epoch`
80
+ slides across the map on every other screen.
81
+ 5. **Keep per-tick work bounded.** Each tick has a CPU and memory budget; a runaway loop ends the
82
+ room for everyone in it. Loops run over entities or players, never "until something happens".
83
+ 6. **The server's plane is x/y, and +z is up.** `yaw` turns from +x toward +y. For `firstperson`
84
+ and `vehicle`, draw at three.js `(x, z, -y)` with `rotation.y = yaw`: their input's "right" is
85
+ the server's, and a mirrored drawing steers the car the wrong way. The topdown templates draw the
86
+ server's `y` as three's `z`, which works there because they read input in the same drawn axes.
87
+
88
+ ## Movement: pick a preset
89
+
90
+ | The game | `config.preset` | Notes |
91
+ |---|---|---|
92
+ | Seen from above (twin-stick, top-down shooter) | `topdown` | 30 Hz is enough |
93
+ | On foot through a level (a first-person or third-person shooter) | `firstperson` | Source-engine-style movement through a world of boxes and ramps; `tickHz: 60` |
94
+ | Cars (car football, a derby, a track of ramps) | `vehicle` | Rocket League's handling, kept level, through the same kind of world; `tickHz: 60` |
95
+ | Anything else | none: your own step | Shared by the module and the client, `predict: { step }` |
96
+
97
+ `firstperson` and `vehicle` move through a fixed collision world the module exports
98
+ (`defineRoom({ world })`) and the client hands its session (`connectAuthority({ …, world })`),
99
+ from one file both import: the session predicts only while the two hash the same. Their inputs,
100
+ parameters, units and CPU budget are in [references/presets.md](references/presets.md); read it
101
+ before writing either side. A ball the cars knock about is one projectile the module re-launches
102
+ (template 3, and "Running a ball" in presets.md).
103
+
104
+ ## Feel settings: contacts, inputs, lifts, tick rate, regions, fast channel
105
+
106
+ Seats that touch (standing on heads, shoving, passing through), starved inputs, moving platforms, 120 Hz,
107
+ regions and the UDP fast channel are **settings in `config`**, picked by genre (`shooter`, `platformer`,
108
+ `party`, `physics-sandbox`, `vehicle-arena`), never code that names the game. Read
109
+ [references/settings.md](references/settings.md) for a worked config per genre before writing any of them.
110
+
111
+ ## Physics: crates, balls, doors
112
+
113
+ When things must collide with each other (a stack that topples, a ball between cars and walls, a
114
+ door a player shoves), the module asks for server physics (`config.physics`) and spawns bodies with
115
+ `ctx.physics`; clients only draw them. It is new: check physics.md for when the servers run it. Read
116
+ [references/physics.md](references/physics.md) first; template 4 is the car arena with a physics ball
117
+ and crate stacks.
118
+
119
+ ## Shots: hitscan or projectile
120
+
121
+ - **Instant** (a rifle, a laser): `ctx.rewind.hitscan` in `onInput`, lag-compensated with
122
+ `config.lagComp`. Template 1.
123
+ - **Something players see travel and can dodge** (a bolt, a rocket, a grenade, an arrow, a
124
+ dodgeball): a projectile. The module launches it once with `ctx.projectiles.launch` and judges it
125
+ with `ctx.projectiles.sweep`; the shooter's client calls `session.predictProjectile` while staging
126
+ the input that fires, and every client draws it from `session.entities()`. Template 2. Read
127
+ [references/projectiles.md](references/projectiles.md) before writing either side.
128
+
129
+ Never make a travelling shot a plain entity the module moves in `step`: it costs an update every
130
+ tick and every client draws it 100 ms late. Every client in a room whose module launches
131
+ projectiles must support them; one that does not draws each projectile frozen where it was
132
+ launched.
133
+
134
+ ## The client in one screen
135
+
136
+ ```ts
137
+ import { connectAuthority } from "@genex-ai/multiplayer/authority";
138
+ import { getAuthorityUrls, getColyseusAuth, waitForPlayer } from "@genex-ai/embed-sdk";
139
+ import { GENEX } from "./genex.config";
140
+
141
+ await waitForPlayer();
142
+ const session = await connectAuthority({
143
+ urls: getAuthorityUrls(),
144
+ room: GENEX.slug,
145
+ auth: () => getColyseusAuth(),
146
+ });
147
+
148
+ function loop(now: number) {
149
+ session.input.moveX = stick.x; // [-1, 1] on the server's x axis
150
+ session.input.moveY = stick.y; // [-1, 1] on the server's y axis
151
+ session.input.yaw = aimAngle; // radians, 0 along +x
152
+ session.input.buttons = firing ? FIRE : 0;
153
+ session.frame(now); // sends the due inputs, predicts you, interpolates everyone else
154
+ for (const [id, e] of session.entities()) drawBody(id, e.x, e.y, e.yaw, e.epoch);
155
+ requestAnimationFrame(loop);
156
+ }
157
+ requestAnimationFrame(loop);
158
+ session.on("shot", (data) => showTracer(data)); // an event the room module emitted
159
+ session.on("disconnect", (code) => showLeft(code));
160
+ ```
161
+
162
+ A refused join rejects with a `code`: 402 (the owner's plan does not include Genex servers — final),
163
+ 424 (no room module is published for this build yet), 426 (the SDK is too old), 429 (the owner's
164
+ server time or room limit is used up — try later) or 503 (no server free — the session already retried). Say each
165
+ in the player's words; never loop on 402 or 424. The whole client, with three.js, is in
166
+ [references/room-template.md](references/room-template.md).
167
+
168
+ ## Shipping it
169
+
170
+ `npx genex preview` and `npx genex publish` ship the room module with the build, and refuse to
171
+ ship one that is not ready:
172
+
173
+ - **Before anything is built**, the module is bundled and validated with the game's own
174
+ `@genex-ai/authority-dev` (the same check as `npx genex-authority-dev validate`). Any failure stops
175
+ the deploy and prints its typed reason; the full report is kept at `.genex/authority/report.json`.
176
+ - **Before any game file uploads**, the module and its report go to a private store that the game's
177
+ address never serves, and `server/` is left out of the public build.
178
+ - **On Genex's servers the owner needs Genex Pro.** A free account is refused with
179
+ `authority_requires_pro` and nothing is uploaded; it can still build and test everything locally.
180
+ Say so before the person publishes.
181
+ - A preview link plays in rooms apart from the live game, as casual multiplayer's does, and
182
+ `npx genex promote` / `npx genex rollback` move the module together with the build — a rollback can
183
+ restore the previous module alone when only `server/` changed.
184
+
185
+ ## Test it on this machine — CLI lane
186
+
187
+ Everything above holds in every lane. This section is how the server runs while you build: when the
188
+ app you run in loads this skill under its own preface, with its own local server tool, that preface
189
+ replaces this section wherever they differ.
190
+
191
+ ```sh
192
+ npx genex-authority-dev validate --module server/room.ts --root .
193
+ npx genex-authority-dev start --module server/room.ts --root . --bots 3 --latency 75 --report
194
+ ```
195
+
196
+ `start` prints `LISTEN ws://127.0.0.1:<port>`. Open the dev server with that address —
197
+ `http://localhost:5173/?genex_local_test=1&genex_authority_url=ws://127.0.0.1:<port>` — and
198
+ `getAuthorityUrls()` returns it, so the same client code joins the local room as a local player
199
+ next to the bots. `--latency 75` is 75 ms each way, the ping real players feel; test there, not at
200
+ zero. `--root .` lets the module import shared files outside `server/`. What each command checks,
201
+ the REPORT line and every failure reason are in [references/testing.md](references/testing.md).
202
+
203
+ Before calling it done: `validate` passes, `replay` says the hashes are equal, two people (a normal
204
+ and a private window both count) have moved and fired at 75 ms, and nobody slides after a respawn.
205
+ With projectiles, also the checks at the end of
206
+ [references/projectiles.md](references/projectiles.md). With `firstperson` or `vehicle`,
207
+ `session.prediction.predicting` is true while you play (never `world_mismatch`), and a full room
208
+ stays under budget in the REPORT line. Say plainly what was not exercised.
@@ -0,0 +1,172 @@
1
+ # Room module API
2
+
3
+ The room module is one ES module whose default export is a room. Its types are published at
4
+ `@genex-ai/multiplayer/authority/module`: `GenexRoomModule`, `RoomContext`, `InitContext`,
5
+ `EntityApi`, `EntityData`, `EntityInit`, `RewindApi`, `ProjectileApi`, `ProjectileInit`,
6
+ `ProjectileFlight`, `ProjectileHit`, `InputData`, `PlayerInfo`, `ModuleConfig`,
7
+ `ResolvedModuleConfig`, `PresetSpec`, `LeaveReason`, `Vec3`, `Json`, `MODULE_CONTRACT` and the
8
+ identity helper `defineRoom`.
9
+
10
+ A TypeScript module imports the types with `import type` (they are erased when it is bundled). A
11
+ module imports **nothing at runtime from a package**: the bundle runs in a sandbox and every byte it
12
+ needs must be inside the game folder. A plain-JavaScript module therefore writes its own one-line
13
+ `defineRoom`, as [the template](room-template.md) does. Relative imports of your own files are
14
+ fine — a movement step in `src/` that the client also runs is the usual one.
15
+
16
+ ```ts
17
+ import type { GenexRoomModule, InputData, PlayerInfo, RoomContext } from "@genex-ai/multiplayer/authority/module";
18
+
19
+ interface State {
20
+ score: Record<string, number>;
21
+ }
22
+
23
+ const room: GenexRoomModule<State> = {
24
+ contract: 1,
25
+ config: { tickHz: 30, maxPlayers: 8, preset: { name: "topdown" }, lagComp: { buttons: 1 } },
26
+ init: () => ({ score: {} }),
27
+ onJoin(ctx, player) { ctx.entities.spawn({ kind: "player", owner: player.id, hp: 3 }); },
28
+ onLeave(ctx, player) { const me = ctx.entities.ownedBy(player.id); if (me) ctx.entities.remove(me.id); },
29
+ onInput(ctx, playerId, input) { /* one input, after the preset moved the player's body */ },
30
+ step(ctx) { /* once per tick, after the tick's inputs */ },
31
+ };
32
+ export default room;
33
+ ```
34
+
35
+ ## The room
36
+
37
+ | Field | What it is |
38
+ |---|---|
39
+ | `contract` | Always `1`. A bundle whose default export has another contract does not load. |
40
+ | `config` | Optional, and the one place to set these. `tickHz` (20, 30, 60 or 120), 120 only with `physics`; `maxPlayers` (a room never seats more than the game's manifest allows), `preset` (`{ name, params? }` with `name` one of `topdown`, `firstperson` or `vehicle`, see [presets.md](presets.md)) and `lagComp: { buttons }` — the bitmask of input buttons that ask for lag compensation. What the room runs with is `ctx.config` in `init`. `physics` asks for server physics (crates, balls, doors): see [physics.md](physics.md), which also says when Genex's servers run it. |
41
+ | `world` | The `firstperson` and `vehicle` presets' collision world, `{ boxes, ramps }`, fixed for the room's life. Validated when the module loads; its hash becomes the preset's `params.world`, and the client hands its session the same object (`session.setWorld`). Left out, the world is empty: no floor. See [presets.md](presets.md). |
42
+ | `init(ctx)` | Runs once. Returns the module's own state, `ctx.state` from then on. Clients never see it. |
43
+ | `onJoin(ctx, player)` | A player took a seat. Spawn their body here. |
44
+ | `onLeave(ctx, player, reason)` | A seat ended. Remove what they owned. `reason` is a `LeaveReason`: `left`, `dropped` (the reconnect grace ran out), `superseded` (a newer session of the same player took the seat), `revoked` or `closing`. |
45
+ | `onInput(ctx, playerId, input)` | One call per input, in each player's order, before `step`. |
46
+ | `step(ctx)` | Once per tick, after the tick's inputs. |
47
+
48
+ `player` is a `PlayerInfo`: `id` is the **seat** (an entity's `owner`, and what `emit(…, to)` takes);
49
+ `uid` is the player's stable id for this game across visits — key saves and scores by it; `name`;
50
+ `team` (`''` for none). A hook that throws costs only itself — the tick's other hooks still run —
51
+ but it is a fault (below).
52
+
53
+ `input` is an `InputData`, already cleaned by the server: `moveX`, `moveY`, `moveZ` in [-1, 1],
54
+ `yaw` and `pitch` in [-π, π), `buttons` an integer bitmask below 2^24. With a `preset`, the server
55
+ has already moved the player's first owned entity by this input when `onInput` runs; `onInput` is
56
+ where buttons become actions.
57
+
58
+ ## The context
59
+
60
+ | Member | What it is |
61
+ |---|---|
62
+ | `ctx.state` | What `init` returned. |
63
+ | `ctx.tick` | The fixed-step tick, from 0. |
64
+ | `ctx.dt` | Seconds per tick (1 / tickHz). |
65
+ | `ctx.random()` | The room's seeded generator: the only randomness there is. `Math.random` is the same generator. |
66
+ | `ctx.entities` | The entities: the only state clients see. |
67
+ | `ctx.rewind` | Lag-compensated hit tests. |
68
+ | `ctx.projectiles` | Shots on a ballistic flight that every client draws from one launch (below, and [projectiles.md](projectiles.md)). |
69
+ | `ctx.emit(type, data?, to?)` | A game event to every client, or only to the seat `to`. Clients receive it with `session.on(type, handler)`. |
70
+ | `ctx.log(...values)` | A server log line, rate limited. |
71
+
72
+ `init` gets `ctx.config`, `ctx.seed`, `ctx.random()`, `ctx.entities`, `ctx.projectiles` and
73
+ `ctx.log` (no tick, no rewind, no emit yet). A projectile launch in `init` throws, and the module
74
+ does not load: launch from a hook that runs in a tick.
75
+
76
+ ## Entities
77
+
78
+ Each entity is an `EntityData`: `id`, `kind`, `owner` (a seat, or `''`), `x` `y` `z`, `vx` `vy`
79
+ `vz`, `yaw`, `hp`, `flags` (an integer bitmask below 2^24), `epoch` and `data` (any JSON), plus
80
+ three projectile fields that only `ctx.projectiles.launch` writes and `update` ignores: `t0` (the
81
+ launch tick; 0 = not a projectile), `g` (its gravity) and `src` (the seat that fired it, `''` for
82
+ none). Numbers are float32 on the wire. Only fields that changed this tick go out.
83
+
84
+ The movement presets add their own fields, which the preset writes from each input and the module
85
+ may write too (a respawn, a refuel):
86
+
87
+ | Field | Preset | What it is |
88
+ |---|---|---|
89
+ | `pitch` | `firstperson` | The aim's pitch, radians, positive up. |
90
+ | `duck` | `firstperson` | 0 standing to 1 fully ducked: derive the eye height from it. |
91
+ | `mstate` | `firstperson`, `vehicle` | Movement bits: 1 on the ground, 2 ducking, 4 jump held. Write 0 on a respawn. |
92
+ | `cap` | `firstperson`, `vehicle` | A speed multiplier from 0 to 4, default 1, that only the module writes (a weapon's weight, a wound, a slow zone); a car's applies to both top speeds. |
93
+ | `spin` | `vehicle` | The car's turn rate, rad/s, positive toward +yaw. Write 0 on a respawn. |
94
+ | `fuel` | `vehicle` | The boost tank, 0 to 1, default 0. The preset only burns it: write it to refuel. |
95
+
96
+ | Call | Does |
97
+ |---|---|
98
+ | `ctx.entities.spawn({ kind, ...fields })` | Adds one and returns its id (the `id` you pass when it is free, else a fresh one). |
99
+ | `ctx.entities.get(id)` | The entity, or `undefined`. Assigning to its fields updates it. |
100
+ | `ctx.entities.update(id, patch)` | Changes several fields at once. |
101
+ | `ctx.entities.remove(id)` | Removes it. |
102
+ | `ctx.entities.ownedBy(seat)` | The first entity that seat owns: a player's body. |
103
+ | `ctx.entities.all()` | Every entity. |
104
+
105
+ **Bump `epoch` when an entity jumps** (a respawn, a teleport): clients snap to it instead of sliding
106
+ across the map. Clients predict the first entity a player owns that is not a projectile, so give
107
+ each player one body, spawn everything else (pickups, props) unowned, and launch shots with
108
+ `ctx.projectiles.launch`, never as plain entities moved in `step`.
109
+
110
+ ## Lag compensation
111
+
112
+ `ctx.rewind.hitscan(origin, dir, { range, radius, kinds })` returns the nearest `{ id, dist }` along
113
+ the ray, or `null`; `ctx.rewind.overlapAt(center, radius)` returns every id within the radius. While
114
+ `onInput` handles an input whose `buttons` intersect `config.lagComp.buttons`, both test the
115
+ positions **that player saw** when they pressed it (at most 500 ms back); otherwise they test the
116
+ current positions. During that input the shooter's own entities are never its target. A shot judged
117
+ this way lands when it looked right on the shooter's screen.
118
+
119
+ ## Projectiles
120
+
121
+ A projectile's launch is written once; every client computes its flight, so it costs nothing per
122
+ tick on the wire and is drawn at the server's present. The whole guide, the client side included, is
123
+ [projectiles.md](projectiles.md).
124
+
125
+ | Call | Does |
126
+ |---|---|
127
+ | `ctx.projectiles.launch({ kind, x, y, z, vx, vy, vz, g?, hp?, flags?, data?, id? })` | Launches at the tick being stepped, `owner` `''`, and returns the id. In `onInput` the id is `<seat>.<seq>.<n>` (the one the shooter's client predicted) and `src` the seat; passing `id` there throws. Elsewhere `id` is required and `src` is `''`. An existing id is re-launched, replaced whole: a bounce. `g` is downward along −z, 0 to 1000 units/s². Throws a `RangeError`, writing nothing, past the launch budget. |
128
+ | `ctx.projectiles.at(id, tick?)` | `{ x, y, z, vx, vy, vz }` at that tick (default: this one), or `undefined` for an id that is not a projectile. |
129
+ | `ctx.projectiles.sweep(id, { kinds, radius })` | `{ id, dist }[]`, nearest first: the entities of those kinds whose sphere of `radius` the path crossed since the previous tick. The shooter's own body can be among them (a launch point inside it): skip it. Costs the number of entities of those kinds. |
130
+
131
+ Projectiles take no part in lag compensation: `ctx.rewind` skips them, and a sweep tests bodies
132
+ where they are now. The server never removes one: the module does, on a hit, at the walls or after a
133
+ maximum age. Change a flight by re-launching; `update` on its `x`…`vz` keeps the old launch tick, so
134
+ it jumps. The launches waiting to go out may hold at most 224 entities and 28 KiB of output, about a
135
+ hundred new projectiles in one tick.
136
+
137
+ ## What the sandbox has, and what it refuses
138
+
139
+ The module runs with no clock, no timers, no network, no files and no shared memory. Absent:
140
+ `Date`, `SharedArrayBuffer`, `Atomics`, `WebAssembly`, `console`, `WeakRef`, `FinalizationRegistry`,
141
+ `Intl`, `setTimeout`, `setInterval`, `setImmediate`, `clearTimeout`, `clearInterval`,
142
+ `queueMicrotask`, `fetch`, `require`, `process`, `performance`. Reaching the global object
143
+ (`globalThis`) instead of `ctx` fails validation too. Everything the module needs arrives through
144
+ `ctx`.
145
+
146
+ Its output is checked every tick, and each of these is a **fault**:
147
+
148
+ - a hook that throws;
149
+ - a number that is not finite, or past 1e9 in magnitude;
150
+ - at most 256 entity updates, and at most 256 removals, in one tick — more is a fault;
151
+ - at most 64 events in one tick;
152
+ - at most 1024 entities in the room at once;
153
+ - one event at most 16 KiB as JSON;
154
+ - entity `data` at most 256 characters as JSON;
155
+ - an id that is empty, longer than 64 characters, or `__proto__`, `constructor` or `prototype`.
156
+
157
+ The third fault ends the room (close code 4501, and every player sees `disconnect`). So does a tick
158
+ that runs past its deadline or a module whose CPU or memory use stays over budget. Keep per-tick
159
+ work small, bounded and the same every run: `validate` holds it to a local CPU budget so a module
160
+ that passes here fits Genex's servers.
161
+
162
+ ## Determinism
163
+
164
+ The same seed and the same inputs must give the same room. That is what makes a validation report
165
+ mean something and what `replay` checks. Rules that keep it true:
166
+
167
+ - randomness only from `ctx.random()`;
168
+ - iterate in a fixed order (`Object.keys` of objects you built in a fixed order, arrays, the entity
169
+ list) and never depend on timing;
170
+ - round what you store with `Math.fround` when it comes from `Math.sin`, `Math.cos`, `Math.atan2` or
171
+ `Math.exp`: their last bit may differ between Genex's servers and the local one, and float32
172
+ hides that difference, as the wire does.
@@ -0,0 +1,150 @@
1
+ # Server physics: crates, balls, doors
2
+
3
+ A room module can ask Genex's servers to simulate rigid bodies — crates that topple, a ball that
4
+ bounces off cars, doors on hinges — with the Genex Physics core. The server alone steps them. Every
5
+ client only **draws** them: at the server's present, smoothed when a correction arrives. A client
6
+ never moves a body itself.
7
+
8
+ **New, and not on Genex's servers until the release that carries the physics core is deployed.**
9
+ `genex-authority-dev validate` runs a physics module on that same core; a validator without it fails
10
+ `physics_unavailable` (update `@genex-ai/authority-dev`). Until the servers run it, a game that must
11
+ ship today keeps to presets and projectiles ([presets.md](presets.md),
12
+ [projectiles.md](projectiles.md)).
13
+
14
+ Reach for physics when things must collide with each other: a stack, a pile, a ball between cars
15
+ and walls, a door a player shoves. A player's own movement stays a preset, so it is predicted and
16
+ feels instant; a lone flying shot stays a projectile.
17
+
18
+ ## Asking for it
19
+
20
+ ```js
21
+ config: {
22
+ tickHz: 60,
23
+ preset: { name: "vehicle", params: { radius: 50, height: 36 } },
24
+ physics: { subscribe: ["ball"], player: { events: true } },
25
+ },
26
+ ```
27
+
28
+ `physics: true` takes every default. The keys:
29
+
30
+ | Key | Default | What it is |
31
+ |---|---|---|
32
+ | `lengthUnit` | 100 with `vehicle`, 39.37 with `firstperson`, else 1 | Your units per metre. |
33
+ | `gravity` | The preset's `gravity`, else 9.81 × `lengthUnit` | Units/s² along −z. |
34
+ | `iterations` | 4 | Solver iterations, 1 to 16. |
35
+ | `limits` | The defaults below | Lower any limit; higher ones are clamped. |
36
+ | `subscribe` | `[]` | Up to 8 physics kinds whose body states `ctx.physics.body(key)` returns. |
37
+ | `player` | `{ events: false }` | `events: true` reports the players' hulls touching bodies. A `topdown` room also gives `radius` and `height` here, or its players have no hulls. |
38
+
39
+ Any other key is a bad config. A room's limits: 512 bodies, 64 joints, 16 bodies with continuous
40
+ collision (`ccd`), 64 queries a tick, 64 events a tick, at most 256 commands a tick.
41
+
42
+ ## A body is an entity
43
+
44
+ Every body belongs to one entity with the same id. Spawn the entity, then its body:
45
+
46
+ ```js
47
+ init(ctx) {
48
+ ctx.entities.spawn({ id: "ball", kind: "ball", z: 92 });
49
+ ctx.physics.spawn({
50
+ key: "ball", kind: "ball", type: "dynamic", ccd: true,
51
+ pose: { p: { x: 0, y: 0, z: 92 }, q: { x: 0, y: 0, z: 0, w: 1 } },
52
+ colliders: [{ key: "ball", shape: { kind: "sphere", radius: 92 }, restitution: 0.65 }],
53
+ });
54
+ return {};
55
+ },
56
+ ```
57
+
58
+ - **Ids**: 1 to 64 of `A-Z a-z 0-9 _ . : -`, never starting `static:`. Ids the room makes up
59
+ (`e1`, `e2`, …) qualify. A body's `kind` (`"ball"`, `"crate"`) is its own, 1 to 32 of the same.
60
+ - **The server writes a body entity's `x y z`, orientation and velocities.** Your module must never
61
+ write those fields on a body entity: the write throws. Every other field is yours: `kind`,
62
+ `owner`, `hp`, `data`, `epoch`.
63
+ - **A teleport** (a kickoff, a respawn): `ctx.physics.setPose(key, pose, { resetVelocity: true, wake: true })`
64
+ and bump the entity's `epoch` in the same tick, so every client snaps instead of sliding there.
65
+ - **Removing** the entity removes its body. `ctx.physics.remove(key)` removes only the body.
66
+ - A player's own entity never gets a body, and a projectile never does.
67
+
68
+ Vectors are `{ x, y, z }` and rotations `{ x, y, z, w }`. Shapes: `box` (`half`), `sphere`
69
+ (`radius`), `capsule` (`halfHeight`, `radius`, `axis`), `hull` (`points`), `trimesh` and
70
+ `heightfield` (fixed bodies only).
71
+
72
+ **Put a body's origin at its centre of mass.** Clients extrapolate a body's origin by its centre's
73
+ velocity, so a spinning body whose colliders are offset from its origin is drawn off until the next
74
+ update. Swing a door by posing its middle around the hinge, or join it to the world with a hinge
75
+ joint, rather than giving it an offset collider.
76
+
77
+ ## Commands, queries, events
78
+
79
+ Commands take effect in this tick's step, and every hook may call them (`init` too):
80
+
81
+ | Call | Does |
82
+ |---|---|
83
+ | `ctx.physics.spawn(desc)` | Adds a body to an existing entity. |
84
+ | `ctx.physics.remove(key)` | Removes the body; the entity stays. |
85
+ | `ctx.physics.impulse(key, j, at?)` | A push, in mass × units/s. |
86
+ | `ctx.physics.setVelocity(key, linvel?, angvel?)` | Sets a dynamic body's velocities. |
87
+ | `ctx.physics.setPose(key, pose, options?)` | Moves it (bump the epoch with it). |
88
+ | `ctx.physics.setNextKinematicPose(key, pose)` | Where a kinematic body (a lift, a door you drive) is at the end of this tick. |
89
+ | `ctx.physics.addJoint(desc)` / `ctx.physics.removeJoint(key)` | Hinges, sliders, ropes; `b: null` is the world. |
90
+ | `ctx.physics.setMotor(jointKey, motor)` | Drives a joint. |
91
+ | `ctx.physics.areaPush(center, radius, strength, falloff, groups?)` | An explosion. |
92
+ | `ctx.physics.wake(key)` / `ctx.physics.sleep(key)` | |
93
+
94
+ Each returns its command number, or `'invalid-key'` for a bad id. A command the core refuses
95
+ changes nothing and is listed next tick in `ctx.physics.refused` (`{ n, code }`). It is never a
96
+ fault.
97
+
98
+ Queries (`ctx.physics.ray`, `ctx.physics.shapeCast`, `ctx.physics.overlap`) return an index; the
99
+ answer is `ctx.physics.answers[index]` on the **next** tick. Queries throw in `init`.
100
+
101
+ What the last step did: `ctx.physics.events` (`contactBegin`, `contactEnd`, `triggerEnter`,
102
+ `triggerExit`, for colliders with `events: true`), `ctx.physics.escaped` (ids whose bodies left the
103
+ world; the server removed those entities), and `ctx.physics.body(key)` (`{ p, q, v, w, sleeping }`
104
+ for a subscribed kind).
105
+
106
+ ## Players push bodies
107
+
108
+ With `firstperson` or `vehicle` (or `topdown` with `player.radius` and `player.height`), each
109
+ player's own entity carries a hull the server moves with it. Hulls push dynamic bodies; bodies never
110
+ block a player, who still moves only through the preset's world. With `player: { events: true }`,
111
+ `contactBegin` events name the player's entity id, so a module sees who touched the ball:
112
+
113
+ ```js
114
+ for (const ev of ctx.physics.events) {
115
+ if (ev.kind !== "contactBegin" || (ev.a !== "ball" && ev.b !== "ball")) continue;
116
+ const car = ctx.entities.get(ev.a === "ball" ? ev.b : ev.a);
117
+ if (car && car.kind === "car") ctx.state.lastTouch = car.owner;
118
+ }
119
+ ```
120
+
121
+ ## Drawing bodies on the client
122
+
123
+ Nothing to set up: `connectAuthority` joins physics rooms and draws bodies. In
124
+ `session.entities()`, a body has `body: true`, its orientation in `qx qy qz qw` (z up) and `sleep`.
125
+ Its `x y z` are already where the server has it now: extrapolated past the last update, never
126
+ 100 ms behind like other players. A correction eases out over about 100 ms, and a jump over one
127
+ metre (or an `epoch` bump) snaps. With three.js, z up becomes y up for both:
128
+
129
+ ```ts
130
+ mesh.position.set(e.x, e.z, -e.y);
131
+ if (e.body) mesh.quaternion.set(e.qx, e.qz, -e.qy, e.qw);
132
+ else mesh.rotation.set(0, e.yaw, 0);
133
+ ```
134
+
135
+ A body far from the player may update less often: each player gets the 100 nearest awake bodies
136
+ every update and the rest in turns. A body that has not updated for a while is held where it was.
137
+
138
+ ## Costs and limits
139
+
140
+ The server measures physics time apart from your module's. A tick over 8 ms per 1/60 s of game
141
+ time, or an average over 1 ms per 1/60 s, is a strike; the third ends the room. At 120 Hz a tick
142
+ gets a 60 Hz tick's 8 ms and 1 ms average, and the room counts twice toward the server's capacity. Asleep bodies cost
143
+ almost nothing, so let piles settle: `canSleep` is on by default.
144
+
145
+ ## Testing
146
+
147
+ `npx genex-authority-dev validate --module server/room.js` fuzzes a physics module like any other,
148
+ on the physics core. Play it at a real ping (`--latency 75`) and judge it there, not at zero: a
149
+ fast ball drawn from the server is a little behind the moment a car hits it, by about the one-way
150
+ latency, and one that bounces off a wall is drawn a little into it before it comes back.