@genex-ai/multiplayer 0.17.0 → 0.17.6

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/PROTOCOL.md CHANGED
@@ -1,6 +1,6 @@
1
- # Genex relay protocol, version 3
1
+ # Genex relay protocol, version 3 (authority rooms: version 4)
2
2
 
3
- This document is the normative contract between the Genex multiplayer relay (`apps/colyseus`) and every client that joins it: the TypeScript SDK in this package and the Rust, Godot (GDScript) and Unity (C#) clients in the repository's `sdks/`. It is written so that someone holding only an official Colyseus 0.18 SDK for their engine can write a conforming client.
3
+ This document is the normative contract between the Genex multiplayer relay (`apps/colyseus`) and every client that joins it, and (§17) between the authority servers and clients of server-authoritative rooms: the TypeScript SDK in this package and the Rust, Godot (GDScript) and Unity (C#) clients in the repository's `sdks/`. It is written so that someone holding only an official Colyseus 0.18 SDK for their engine can write a conforming client.
4
4
 
5
5
  The machine-readable copy of every name, number and limit below is [`src/protocol.ts`](src/protocol.ts) (published as `@genex-ai/multiplayer/protocol`) and [`src/schema.ts`](src/schema.ts) (`@genex-ai/multiplayer/schema`). The relay imports both, so when this document and those files disagree, the code is what runs. Fix the disagreement in the same change.
6
6
 
@@ -9,7 +9,7 @@ The machine-readable copy of every name, number and limit below is [`src/protoco
9
9
  The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are used as in RFC 2119.
10
10
 
11
11
  - **Transport.** Colyseus 0.18 over WebSocket: HTTP matchmaking, seat reservation, then a room socket. Room messages are a string type plus a msgpack payload, and room state is `@colyseus/schema` 5 with delta patches. When the relay publishes its clock (§16), every state frame also carries Colyseus's TIMED prefix, which the official SDKs decode. A client uses its engine's official Colyseus SDK for all of this and never reimplements the wire.
12
- - **The relay is not a game server.** It runs no simulation, prediction, reconciliation or lag compensation, and no per-game code. Games are client- and host-authoritative. The relay enforces only what a client cannot be trusted with: who is in the room, who owns an object, who the host is, rate and size limits, chat and voice identity, and the match state machine.
12
+ - **The relay is not a game server.** This holds for the relay rooms (`play`, `queue`, `match`); authority rooms are §17's and run a game's own server code. A relay room runs no simulation, prediction, reconciliation or lag compensation, and no per-game code. Games are client- and host-authoritative. The relay enforces only what a client cannot be trusted with: who is in the room, who owns an object, who the host is, rate and size limits, chat and voice identity, and the match state machine.
13
13
  - **Drop, don't error.** A message the relay will not act on is dropped silently. That covers malformed, oversized, unauthorised and over-budget messages. Only three things ever answer a client:
14
14
  - an `__object:result` for a control request it could answer;
15
15
  - `__voice:rejected` when voice is full;
@@ -22,7 +22,7 @@ The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are used as in RFC 2119
22
22
  ## 2. Versioning
23
23
 
24
24
  - The protocol version is `PROTOCOL_VERSION = 3`. A client MUST send it as the `protocol` join option on every join, create, joinById and queue join.
25
- - The relay accepts `[MIN_PROTOCOL_VERSION, PROTOCOL_VERSION]`, which is `[3, 3]` today. Any other value, or a missing option, is refused before a room is found or created. The refusal is matchmaking error code **426** with the message `unsupported protocol: <sent>, relay speaks <min>..<max>`. A client receiving 426 MUST NOT retry and SHOULD tell the player the game needs an update.
25
+ - Each room type has its own protocol window. The relay rooms accept `RELAY_WINDOW` = `[MIN_PROTOCOL_VERSION, PROTOCOL_VERSION]`, which is `[3, 3]` today; authority rooms accept `AUTHORITY_WINDOW` = `[4, 4]` (§17.1), so protocol 4 never reaches a relay room and protocol 3 never reaches an authority room. Any other value, or a missing option, is refused before a room is found or created. The refusal is matchmaking error code **426** with the message `unsupported protocol: <sent>, relay speaks <min>..<max>`. A client receiving 426 MUST NOT retry and SHOULD tell the player the game needs an update.
26
26
  - This is the only version check. There is no capability handshake and no per-feature negotiation. Protocol 2 (`__v`), its JSON mirrors under `__obj:`/`__host`/`__match`/`__voice`, and its legacy claim forms are not served.
27
27
  - These changes are not version bumps: appending a field to the end of a schema, adding a message type, and adding an optional payload key. Clients MUST ignore unknown message types, unknown payload keys and unknown match-state fields.
28
28
  - These changes are version bumps: inserting, removing, reordering or retyping a schema field; reshaping a payload; changing a semantic in this document.
@@ -33,10 +33,11 @@ The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are used as in RFC 2119
33
33
 
34
34
  The embed layer supplies a list of regional relay URLs (`getColyseusUrls()` from `@genex-ai/embed-sdk`; the API returns it as `colyseus.urls` with the embed session).
35
35
  - With one URL, the client uses it.
36
- - With several, it SHOULD measure latency to each (Colyseus `getLatency` / `Client.selectByLatency`) and use the lowest.
37
- - An explicit invite URL (a friend's region) MUST override selection. Rooms exist per relay, so two players share a room only if they share a URL.
36
+ - With several, it SHOULD measure latency to each (Colyseus `getLatency`) and use the lowest. A probe is a preference, never a gate: when a relay does not answer in time, the client MUST still join — the lowest-latency relay that did answer, else the first URL. (Colyseus `Client.selectByLatency` throws instead; the TS SDK does not use it.)
37
+ - An explicit invite URL (a friend's region) MUST override selection.
38
+ - Regional relays MAY share presence (one room registry). Then every relay serves every room: two players meet whichever relay each enters through, and a seat reservation's `publicAddress` names the relay process that owns the room. Without shared presence, rooms exist per relay, so two players share a room only if they share a URL.
38
39
 
39
- A client MUST keep using the chosen URL for the life of a session, across reconnects and seat reservations.
40
+ A client MUST keep using the chosen URL for the life of a session, across reconnects and seat reservations — as its matchmaking and reconnect entry. The room socket itself goes where the reservation says: `<publicAddress>/<processId>/<roomId>` when `publicAddress` is present (every official Colyseus SDK does this), else the chosen URL.
40
41
 
41
42
  ### 3.2 Embed token
42
43
 
@@ -73,7 +74,7 @@ Deployment note: the API's `uid` claim ships with this relay, so the fallback se
73
74
  | `name` | string | Display-name fallback, truncated to 64. The token's name wins when it has one. |
74
75
  | `protocol` | number | Required (§2). |
75
76
 
76
- The options `clients`, `preset`, `winCondition`, `config` and `matchmade` are server-owned, and sending any of them is refused. Other keys are ignored.
77
+ The options `clients`, `preset`, `winCondition`, `config` and `matchmade` are server-owned, and sending any of them is refused. Other keys are ignored. Authority rooms refuse more (§17.2).
77
78
 
78
79
  | Code | Meaning |
79
80
  |---|---|
@@ -81,6 +82,7 @@ The options `clients`, `preset`, `winCondition`, `config` and `matchmade` are se
81
82
  | 401 | `auth required`, `invalid token` or `session revoked`. Get a fresh token; if it repeats, the player must sign in again. |
82
83
  | 403 | `wrong game` or `guest capacity` (the room's guest cap is full). |
83
84
  | 426 | Unsupported protocol (§2). |
85
+ | 402, 424, 429, 503 | Authority rooms only (§17.2). |
84
86
 
85
87
  ### 3.5 Room types
86
88
 
@@ -89,6 +91,7 @@ The options `clients`, `preset`, `winCondition`, `config` and `matchmade` are se
89
91
  | `play` | `joinOrCreate('play', options)` | The plain relay room. Placement prefers the fullest open room of the game. At most 64 seats; when full, the next join creates a sibling room. |
90
92
  | `queue` | `joinOrCreate('queue', options)` | The matchmaking search pool. It has no game state; it hands out seat reservations (§9). |
91
93
  | `match` | A seat reservation from the queue; `create('match', options)` for a private lobby; `joinById(code, options)` with an invite code | A capped match arena: everything `play` does, plus the match state machine (§9). |
94
+ | `authority` | `joinOrCreate('authority', options)` with `protocol: 4`, on an authority URL | A server-authoritative room running the game's room module (§17). Never served by a relay. |
92
95
 
93
96
  ### 3.6 One seat per identity
94
97
 
@@ -505,6 +508,8 @@ An engine that uses Colyseus `Predict` for posed players attaches `x y z vx vy v
505
508
  | 4003 | All reconnection attempts failed | Terminal |
506
509
  | 4401 | Session revoked | Terminal; do not rejoin with this identity |
507
510
  | 4409 | Superseded by a newer session of the same player | Terminal; do not rejoin automatically |
511
+ | 4500 | Authority rooms: the room was halted (kill switch, takedown, disabled game) | Terminal |
512
+ | 4501 | Authority rooms: the room module faulted | Terminal |
508
513
 
509
514
  "Terminal" means the seat is gone and a reconnect would be refused. A client emits `disconnect(code)` and stops.
510
515
 
@@ -612,10 +617,687 @@ The relay MAY publish its room clock, so that every client gets server time and
612
617
  - Deadlines use `serverNow()`, never `renderNow()`.
613
618
  - The relay keeps writing deadlines in epoch ms. The §9.3 shape does not change.
614
619
 
620
+ ## 17. Authority rooms (protocol 4)
621
+
622
+ An authority room is server-authoritative: the server runs the game's own **room module** at a fixed tick, clients send typed input and render the state the server writes, and hits are lag-compensated on the server. Everything in §§1–16 is about the relay rooms and is unchanged by this section. The machine-readable copy is `src/protocol.ts` (`AUTHORITY_*`, `AuthorityTiming`) and `src/authority/` (published as `@genex-ai/multiplayer/authority`, with the room module types alone at `@genex-ai/multiplayer/authority/module`).
623
+
624
+ The TypeScript client session is `connectAuthority` (`src/authority/session.ts`). The production server is `apps/colyseus` started as an authority worker (`RELAY_POOL=authority`), in a cluster of its own; a local development server serves the same room in Studio. No relay process ever serves authority rooms.
625
+
626
+ ### 17.1 Versioning and connection
627
+
628
+ - An authority client sends `protocol: 4` (`AUTHORITY_PROTOCOL_VERSION`). Authority rooms accept `AUTHORITY_WINDOW` = `[4, 4]`; anything else is refused with **426** before a room is found or created. A relay room refuses 4 the same way (§2), so neither kind of client can land in the other kind of room.
629
+ - Authority rooms are served by their own cluster, never by a relay. The embed layer supplies its URLs next to the relay URLs (`colyseus.authorityUrls`; in local test mode, a loopback `genex_authority_url` on a loopback page, with a fixed local credential in place of a token), and the same cluster by region (`colyseus.authorityRegions`, below).
630
+ - *Regions.* `colyseus.authorityRegions` is a list of `{ region, urls[] }`; each region is its own matchmaking pool, because the server filters rooms by the region a join entered through (the server-owned `region` option, §17.2). A client reads it leniently: an entry without a non-empty name or without a non-empty url string is dropped, and a repeated name keeps its first entry. When no entry is usable, every `authorityUrls` entry is a region of its own (an older stand, local test mode). With one region the client joins through its first url. With several, it MUST probe as §3.1 does, a preference and never a gate: 3 samples per region, sample n going to the region's url n modulo its url count; a sample is failed when it has not answered after 4000 ms, which is also the ceiling of the whole probe, and once any sample has answered the others get 1000 ms more. A url's latency is its best answered sample (a finite number of ms, at least 0). Regions are tried in this order: those with an answer, nearest first (a tie keeps list order), each entered through its best url (the first on a tie); then the silent ones in list order, through their first url. A join refused 503 twice in a row on one region moves to the next region at once; any other 503 waits the capacity schedule's next step on the same region, and the join gives up with the last refusal when the schedule is spent. An explicit `url` (an invite: the inviter's session url) skips the probe, so a party plays in its leader's region. `netcode-bench/conformance/authority/authority-regions.json` pins every rule; the TypeScript, Unreal, Unity and Rust clients pass it exactly. The Godot client joins its first url (labelled in CI).
631
+ - A client joins with `joinOrCreate('authority', { gameId, embedToken, name, protocol: 4 })`. The token rules of §3.2 apply. A preview (staging) token joins the game's staging module, in rooms apart from the live game's (§3.4). A join lands in a room running the module the game currently ships: once a newer module has a room, new joins go there, while rooms on the older one keep their players until they empty.
632
+ - Appending a schema field, adding a message or adding an optional key is not a bump; the §2 bump rules apply to this section with `AUTHORITY_PROTOCOL_VERSION` in place of `PROTOCOL_VERSION`.
633
+
634
+ ### 17.2 Join options and refusals
635
+
636
+ The options are §3.4's. `AUTHORITY_SERVER_OWNED_JOIN_OPTIONS` are server-owned and refused with 400: the relay's five plus `moduleSha`, `tickHz`, `maxPlayers`, `lagComp`, `seed`, `units` and `leaseId`, which the server sets from the room's admission, and `region`, which it sets to the region the join entered through (§17.1 *Regions*). The relay rooms' list does not change. A physics room (§17.10) also refuses, with 426, a join whose `caps` option lacks `physics@1`.
637
+
638
+ A new authority room is admitted by the platform before it is created. Admission fails closed: when it cannot be checked the room is refused, never run unmetered. A refusal never closes a room that is already live.
639
+
640
+ | Code | Meaning | Client action |
641
+ |---|---|---|
642
+ | 400, 401, 403, 426 | As §3.4. 403 also covers a game that was taken down or disabled, or whose owner's account is gone. | As §3.4 |
643
+ | 402 | The game's owner is not entitled to authority rooms. | Terminal; do not retry |
644
+ | 424 | The game has no published room module on this channel, or it could not be loaded. | Terminal until the game is republished |
645
+ | 429 | The owner's room quota, concurrency cap or credits refused a new room. | MAY retry later |
646
+ | 503 | No authority server can take the room, or admission could not be checked. | MAY retry with backoff |
647
+
648
+ ### 17.3 State and input
649
+
650
+ The server alone writes the state; field order is wire order and fields are only appended (`src/authority/schema.ts`).
651
+
652
+ **`AuthorityRoomState`**: `players` map<string, AuthorityPlayer> · `entities` map<string, AuthorityEntity> · `tick` number (the fixed-step tick last written) · `clockOrigin` float64 (as §16.1) · `preset` string (`{"name":…,"params":{…}}` JSON text, `''` = none) · `moduleSha` string (hex sha256 of the running module) · `tickOrigin` float64 (the room clock at tick 0's nominal instant, §17.9) · `physics` string · `lengthUnit` float32 · `bodies` map<string, AuthorityBody> (the physics hash, the world's length unit and the body descriptors, §17.10).
653
+
654
+ **`AuthorityPlayer`**: `name` string · `uid` string (§3.3) · `team` string (`''` = none) · `connected` boolean · `avatarUrl` string · `seqBase` number (the inputs the server consumed from the seat's earlier sockets, where this socket's client sequence counts on from; 0 until a reconnect, §17.9).
655
+
656
+ **`AuthorityEntity`**, keyed by entity id (at most 64 characters): `kind` string · `owner` string (seat session id or `''`; a client predicts the entity it owns) · `x` `y` `z` `vx` `vy` `vz` `yaw` float32 · `hp` number · `flags` number (integer below 2^24) · `epoch` number (a change means snap, never interpolate) · `data` string (JSON text, at most 256 characters, `''` = none) · `t0` number · `g` float32 · `src` string (a projectile's launch tick, gravity and shooter, §17.9; `t0` = 0 is not a projectile) · `pitch` `duck` float32 · `mstate` number · `cap` float32 (default 1; a `firstperson` body's aim pitch, duck amount, movement bits and speed multiplier, §17.7) · `spin` `fuel` float32 (a `vehicle` body's yaw rate and boost fuel, §17.7) · `qx` `qy` `qz` `qw` `lvx` `lvy` `lvz` `avx` `avy` `avz` (16-bit quantized) · `sleep` boolean (a physics body's state, written by the host only and without a default, so a room without physics never sends them, §17.10). Entity floats are plain float32, rounded by the server before writing, and stay unquantized until every engine decoder passes conformance with quantized fields; §17.10's quantized body fields reach only clients that claim `physics@1`.
657
+
658
+ **`AuthorityInput`** is sent with Colyseus 0.18 `room.input()` (`defineInput`): `moveX` `moveY` `moveZ` `yaw` `pitch` float32 · `buttons` number. Before a module sees it, the server sanitizes it (`sanitizeInput`): a non-finite axis is 0 and every axis is clamped to [-1, 1]; `yaw` and `pitch` are wrapped to [-π, π) and rounded to float32, and a result that rounds to float32(π) is written as −float32(π), so the range stays half-open (non-finite, or larger than 1e9 in magnitude: 0); `buttons` is an integer in [0, 2^24), else 0. Inputs reach the module one at a time, in each player's sequence order, before the tick's step.
659
+
660
+ A client sends one input per fixed step while its game is playing, and nothing otherwise: an authority room carries no §16 heartbeat, because there `room.input()` is the game's own input channel and a heartbeat would be simulated as blank input. The game's inputs drive the room clock instead. Reconnection is §8's, with the same settings; no input is queued across a drop, and after the reconnect's first state the client re-seeds its prediction from the server's state, so nothing sent before the drop is replayed. A client predicts its own entity, the first entity it owns that is not a projectile (`t0` = 0) and, in a physics room, not a body entity (§17.10), with the room's movement preset (§17.7; `firstperson` and `vehicle` only while the client's world matches the room's). "First" is in the state's `entities` order, which the server and the sandbox share: an id that one output both removes and writes keeps its place there, and any other new id goes last. When that entity's `epoch` changes (a respawn or teleport), the client snaps it to the server's new pose and replays on top of it every input after the ack that came with the bump: the server applies the inputs that were in flight to the new life, so dropping them would leave the prediction a round trip behind and correct it tick by tick. The snap is not counted as a correction. It renders every other entity but a projectile (§17.9) interpolated `AuthorityTiming.RENDER_DELAY_MS` behind, snapping instead when its `epoch` changes. The snap is on that same render timeline: until the render time reaches the patch that carried the new epoch, the client draws the old life and holds the pose of the patch before it, and §17.5's rewind steps a respawn the same way, so a shot is judged where it was aimed.
661
+
662
+ ### 17.4 Room modules and the sandbox wire
663
+
664
+ A room module is one ES module whose default export is `defineRoom({ contract: 1, config?, world?, init, onJoin, onLeave, onInput, step })` (`src/authority/module.ts`; `world` is the collision world of `firstperson` and `vehicle`, §17.7). It runs in a sandbox with no clock, timers, network, filesystem, `require` or shared memory; `ctx.random()` is seeded per room, so a replay of the same inputs is exact. `config` may ask for `tickHz` 20, 30 or 60 (or 120 with `physics`, §17.11), `maxPlayers`, a movement `preset` (§17.7), a lag-compensation button mask and `physics` (§17.10); the server clamps each to the room's admission. A module that asks for no `tickHz` runs at its admission's rate: in production the one its go-live declared (the manifest's `tickHz`, else the report's), and a 120 Hz go-live is refused (`authority_tick_rate_needs_physics`) unless the validated module asks for physics. `genex-authority-dev validate` and the local server admit at `MAX_TICK_HZ` (120) with an `unaskedTickHz` of `DEFAULT_TICK_HZ` (60), so a module's own rate is what its report says and one asking none reports 60.
665
+
666
+ The server and the sandbox exchange JSON text only, copied in and out, with one guest call per tick: `__genex_init(configJson, seed)` returns `InitOut`, and `__genex_tick(tickInJson)` returns `TickOut` (`src/authority/wire.ts`). `TickIn` carries the tick, `dt`, joins, leaves and inputs; each input carries its seat, the host's own per-seat count `seq`, the input's client sequence `cseq` (§17.9) and, for lag compensation, `seen` (§17.5). `TickOut` carries the changed entities only (`up`, an id plus the changed fields), removed ids (`rm`) and game events (`ev`: `type`, optional JSON `data`, optional seat `to`). In a physics room both also carry §17.10's keys, and the host calls a third entry, `__genex_world()`, once at load.
667
+
668
+ Guest output is untrusted. `validateTickOut` / `validateInitOut` rebuild it from scratch and refuse the whole output with a typed fault for any of: text that is not JSON or longer than 32 KiB (64 KiB in a physics room, §17.10); a value of the wrong shape; a non-finite number; an entity number past 1e9 in magnitude; `flags` or a lag-comp mask that is not an integer below 2^24; an `epoch` that is not an integer in [0, 2^32 − 1]; an empty, over-long or prototype-reaching id (`__proto__`, `constructor`, `prototype`); entity `data` that is not JSON text or is over 256 characters; more than 256 updates or removals, or more than 64 events, in one tick; a tick that would leave the room holding more than 1024 entities (`MAX_ROOM_ENTITIES`: the server passes the room's current entity ids, and an id both updated and removed in one tick counts as kept); an event `type` reserved by §5.4 or an event over 16 KiB; event data deeper than 32 levels, with more than 4096 nodes, or with a prototype-reaching key; an init `config` with a tick rate, player count, preset or mask outside its range; and a projectile field outside §17.9's limits (a `t0` is exempt from the 1e9 bound and may reach 2^32 − 1; the owner rule is judged on the entity as the tick leaves it, over the room's current fields when the server passes them). Unknown keys are dropped, `-0` is written as 0, and entity floats are rounded to float32. A fault is a strike against the module; repeated faults or broken CPU and memory limits end the room with **4501**.
669
+
670
+ ### 17.5 Lag compensation
671
+
672
+ When an input's `buttons` intersect the module's `lagComp.buttons`, the server attaches `seen` to that input: entity id → `[x, y, z]` as that player was rendering it (`rewind.lastSeenBy`), at most `AuthorityTiming.MAX_REWIND_MS` back. While the module handles that input, `ctx.rewind.hitscan` and `ctx.rewind.overlapAt` test against the `seen` positions; otherwise they test the current ones. `ctx.rewind.at(id)` answers that same position as `{ x, y, z }` (an entity absent from `seen` where it stood when the tick began, as the two tests judge it), so a module can judge a shot against something that is not a sphere where the shooter saw it (a closing door or shutter, a moving platform's edge); it answers `null` for an id that does not exist, a projectile (§17.9) or a physics body entity (§17.10). The sandbox never holds a server object: `seen` is plain data inside `TickIn`. In a room with lifts (§17.11 *Lifts*) each seen entity that stands on a lift now (its state against the lifts at the tick the shot is judged in: the tick its input is applied in, within a tick of the shooter's drawing) is moved into that lift's frame from the shooter's render tick to that tick, `P_L(to) ∘ P_L(from)⁻¹ ∘ seen` on x and y, at its height now (TypeScript `authority-host/src/lag-comp.ts` `seenInLiftFrames`): the shooter drew a rider on a moving lift where the lift carries it on its own clock.
673
+
674
+ ### 17.6 Timing and close codes
675
+
676
+ `AuthorityTiming` holds the defaults: a 30 Hz fixed step, a 100 ms render delay for other entities, a 500 ms rewind window, a 32-input buffer per seat, a 50 ms state patch interval (the value the server-authority spike measured its feel gates at), and at most 100 ms of a starved seat's repeated inputs owed in a predicting room (`INPUT_REPEAT_MAX_MS`, §17.11 *Input pacing*). Every value but the module's own tick rate is a server knob.
677
+
678
+ Close codes 4500 (halted by the operator or the platform: kill switch, takedown, a disabled game) and 4501 (the module faulted) are terminal, like the other ≥4000 codes of §11. A physics room's failures (§17.10) close with these two.
679
+
680
+ ### 17.7 Movement presets
681
+
682
+ A preset is movement the server simulates and each client's reconciler replays identically, so prediction matches the server by construction. Presets are append-only by name; `src/authority/presets.ts` is the reference. Every step here and in §17.9 rounds each operation on its own: a port never fuses a multiply and an add into one rounding (C, C++ and IL2CPP output are compiled with `-ffp-contract=off` or the equivalent, and no fused multiply-add is called), because a fused result lands on another float32 now and then.
683
+
684
+ **`topdown`** moves a body `{x, y, yaw}` on the x/y plane. Parameters: `speed` (units per second, [0, 1000], default 6) and `arena` (half side, (0, 1e6], default 20); unknown or out-of-range parameters are refused. One step with input `(moveX, moveY, yaw)` and step length `dt`, in float64:
685
+ 1. `moveX` and `moveY`: non-finite → 0, clamped to [-1, 1], rounded to float32.
686
+ 2. `dt`: non-finite or ≤ 0 → 0, capped at 0.25 s (`PRESET_MAX_DT_S`).
687
+ 3. `len = sqrt(moveX² + moveY²)` (a correctly rounded square root, never `hypot`); `scale = len > 1 ? 1 / len : 1`.
688
+ 4. `x' = float32(clamp(x + moveX * scale * speed * dt, -arena, arena))`, evaluated left to right; `y'` likewise with `moveY`.
689
+ 5. `yaw' = float32(yaw − 2π·floor((yaw + π) / 2π))` when `yaw` is finite with magnitude at most 1e9, and −float32(π) when that rounds to float32(π); otherwise the previous yaw.
690
+ 6. A `-0` result is written as 0.
691
+
692
+ **`firstperson`** is Source-engine character movement as Counter-Strike plays it: friction with a stop-speed floor, acceleration by projection onto the wish direction, the air wish-speed clamp that makes air-strafing work, a free step up ledges, staying on the ground down slopes and stairs, ducking and crouch-jumping, and a swept hull that slides along what it hits. It moves an axis-aligned hull whose feet are centred at `(x, y, z)` through a static collision world, z up. It ports cs2-threejs's `SourceMover` with the changes stated here (Source's touching rule for a hull snapped onto a ramp, a ramp's flat cap, steep ramps that block a trace down, `StayOnGround`, the in-air duck, a jump edge taken once per input, and a `cap` that never reduces the push). Water is not part of this version.
693
+
694
+ *Input.* `moveX` and `moveY` are the wish direction in world axes: a client turns its camera-relative keys by its own view yaw before sending them, so the step uses no trigonometry. `yaw` and `pitch` are only stored, as the view's aim for drawing and for the module's hitscan. `moveZ` is unused. `buttons` carries jump, duck and walk under the masks below. The step applies its own rules (steps 1, 2 and 4) to the input exactly as it is handed, and never §17.3's sanitizing on top of them: the two differ (there an unusable `yaw` or `pitch` becomes 0, here it keeps the previous one), the server and a predicting client both hand it the input §17.3 already sanitized, and the vectors (§17.8) hand it raw, hostile inputs.
695
+
696
+ *Tick rate.* One input carries one wish direction for its whole step, so air-strafing, which gains speed by turning that direction while airborne, gains with the input rate, and sub-steps do not change it: a perfect strafer gains about 31 units/s per jump at `tickHz` 30, 70 at 60 (64-tick Counter-Strike: about 74). A jump pressed on an input whose last sub-step lands is lost, as on a Source tick. A `firstperson` game should ask for `tickHz` 60.
697
+
698
+ *Parameters.* Each is a finite number in its range, else refused, and a name the table does not list is refused too. Each parameter the table does not mark integer is used as float32 (`friction` 5.2 steps as 5.19999980926513671875), so a parameter whose range excludes 0 is also refused when its float32 is 0 (1e-46, say): the range holds for the value the step uses. An integer parameter must be an integer and is used exactly: the masks and `substeps` are below 2^24, which float32 holds, but `world` reaches 2^32 − 1, which it does not, so `world` is never rounded and is compared with a world's hash as an exact u32, bit for bit. The defaults are cs2's, in Source units (about an inch); a game in other units sets every distance, speed and acceleration in its own, including `skin` and `groundSnap`.
699
+
700
+ | Name | Default | Range | Meaning |
701
+ |---|---|---|---|
702
+ | `maxSpeed` | 250 | [0, 10000] | Run speed, units/s, before `cap` |
703
+ | `walkSpeed` | 0.52 | [0, 1] | Speed multiplier while walk is held |
704
+ | `crouchSpeed` | 0.34 | [0, 1] | Speed multiplier while ducking (wins over walk) |
705
+ | `accelerate` | 5.5 | [0, 1000] | Ground acceleration |
706
+ | `airAccelerate` | 12 | [0, 1000] | Air acceleration |
707
+ | `airMaxWishSpeed` | 30 | [0, 10000] | Air wish speed is clamped to this before the projection |
708
+ | `friction` | 5.2 | [0, 1000] | Ground friction |
709
+ | `stopSpeed` | 80 | [0, 10000] | Below this speed friction acts as if at this speed |
710
+ | `gravity` | 800 | [0, 100000] | Units/s² along −z |
711
+ | `jumpImpulse` | 301.993 | [0, 100000] | The `vz` a jump sets |
712
+ | `maxVelocity` | 3500 | [0, 100000] | Each velocity axis is clamped to ±this |
713
+ | `radius` | 16 | (0, 10000] | Half the side of the hull's square footprint |
714
+ | `standHeight` | 72 | (0, 10000] | Hull height standing |
715
+ | `crouchHeight` | 54 | (0, 10000] | Hull height fully ducked |
716
+ | `duckTime` | 0.4 | (0, 100] | Seconds to duck fully, and to stand back up |
717
+ | `stepHeight` | 18 | [0, 10000] | Highest ledge walked up without a jump |
718
+ | `standable` | 0.7 | [0, 1] | Least z of a ramp top's unit normal that can be stood on |
719
+ | `groundTrace` | 2.5 | [0, 10000] | How far below the feet ground is found |
720
+ | `groundSnap` | 1 | [0, 10000] | How far above the feet ground is found (a hull sunk that far is lifted out) |
721
+ | `skin` | 0.03 | [0, 100] | Clearance kept from a surface (Source's DIST_EPSILON) |
722
+ | `pitchLimit` | 1.5533430576324463 | [0, 1.5707963705062866] | float32(89°), radians |
723
+ | `jumpBit`, `duckBit`, `walkBit` | 1, 2, 4 | integer, [0, 2^24) | Button masks: any shared bit counts as held; 0 disables |
724
+ | `substeps` | 2 | integer, [1, 8] | Sub-steps per step |
725
+ | `world` | 2615243109 | integer, [0, 2^32 − 1] | The collision world's hash, written by the sandbox (below) |
726
+
727
+ *Body.* The step reads and writes `x y z vx vy vz yaw pitch duck mstate` and reads `cap`; the module writes `cap` (a weapon's or a wound's speed) and the step never does. `cap` scales the speed a body accelerates to, never how hard it accelerates (`accelerate()` pushes with `maxSpeed`), so a wound may slow a player to a crawl without leaving it unable to overcome friction. Each is read sanitized: `x` to `vz` missing or non-finite → 0, clamped to [−1e9, 1e9]; `yaw` and `pitch` missing or non-finite → 0; `duck` (0 standing, 1 fully ducked) missing or non-finite → 0, clamped to [0, 1]; `cap` missing or non-finite → 1, clamped to [0, 4]; each of these then float32. `mstate` is an integer in [0, 2^24), else 0, of which only the bits of value 1 (`onGround`), 2 (`ducking`) and 4 (`jumpHeld`) are kept. The game derives its eye height from `duck` (cs2: 64 standing, 46 ducked); it is not part of the step.
728
+
729
+ *World.* `{ boxes: [[minX, minY, minZ, maxX, maxY, maxZ], …], ramps: [[minX, minY, minZ, maxX, maxY, maxZ, axis, lo, hi], …] }`. A ramp is a box whose top is a plane rising along x (`axis` 0) or y (`axis` 1), at height `lo` at that axis's min end and `hi` at its max end. A world is refused unless it has at most 4096 boxes and 512 ramps, each an array of exactly six (box) or nine (ramp) numbers, every value is a float32 (`float32(v) = v`) of magnitude at most 1e9 (−0 is read as 0), min < max on every axis, and each ramp has `axis` 0 or 1, `minZ ≤ lo`, `minZ ≤ hi` and `maxZ = max(lo, hi)`. Its order is the order given: solids are indexed boxes first (0 … B−1), then ramps (B … B+R−1). The world is fixed for a room's life and never enters the state or crosses the wire.
730
+ - The module exports it beside its config, `defineRoom({ contract: 1, config: { preset: { name: 'firstperson', params } }, world, … })`; no `world` is the empty world. At init the sandbox validates it (a refused world is a bad config, §17.4), keeps its own copy, and writes its hash into the resolved preset's `params.world`. A module that sets `params.world` must give that hash, or the config is refused.
731
+ - The game client hands its SDK the same world (TypeScript `session.setWorld(world)`, the same call in each engine SDK), validated by the same rules; until then its world is the empty world. It predicts its own entity only while its world's hash equals the room's `params.world`, and otherwise renders it from the server's state without prediction, as §17.8 says of an engine without a passing port.
732
+ - The hash is 32-bit FNV-1a: `hash = 2166136261`, then for each byte `b`, `hash = ((hash XOR b) × 16777619) mod 2^32`. The bytes are the box count and the ramp count, each a little-endian u32, then all the boxes in order, six values each, then all the ramps in order, nine values each (`axis` as 0.0 or 1.0), each value a little-endian IEEE 754 binary32. The empty world hashes to 2615243109 (0x9BE17165).
733
+ - There is no implicit floor: in the empty world a body falls, until z reaches −1e9. A game that wants open ground adds one box, so every helper below has one kind of surface to test.
734
+ - Each helper visits "every solid" in index order. A port may skip a solid (a grid, a bounding-volume tree, a per-sub-step candidate list) only when that solid's test could not have reported a hit, an overlap or a top; a broadphase changes the cost, never a result. Every solid a helper can report lies within its box `[minX, maxX] × [minY, maxY] × [minZ, maxZ]` grown by `r` on x and y (and, for `sweep`, by `H` down), so a broadphase culls on z as well as x and y: a world that stacks many solids in one x/y column (a tower, a pile of slabs) must cost no more than one that spreads them out, and one whose solids each span the map must load in memory linear in its solid count.
735
+
736
+ *Arithmetic.* Every value is float64 and every expression is evaluated by the usual precedence and otherwise left to right as written (`a · b · c` is `(a · b) · c`, `a − b · c + d` is `(a − (b · c)) + d`), under the no-fused-multiply-add rule above; `sqrt` is the correctly rounded square root and the only function beyond `+ − · /`. `clamp(v, lo, hi)` is `lo` when `v < lo`, `hi` when `v > hi`, else `v`; `min` and `max` take two numbers; `|v|` is the absolute value. A zero's sign never changes a comparison or a nonzero result in this step, so a port need not track it; stored values are written with −0 as 0. Values are rounded to float32 only where a step says so.
737
+
738
+ One step with input `(moveX, moveY, yaw, pitch, buttons)` and length `dt`:
739
+ 1. `mx`, `my`: `moveX`, `moveY` as topdown's step 1. `len = sqrt(mx · mx + my · my)`. When `len > 0.0001`: `dirX = mx / len`, `dirY = my / len`, `amount = min(len, 1)`; otherwise `dirX`, `dirY` and `amount` are 0.
740
+ 2. `buttons`: an integer in [0, 2^24), else 0. `jump = (buttons AND jumpBit) ≠ 0`; `duckDown` and `walk` likewise with `duckBit` and `walkBit`.
741
+ 3. `dt` as topdown's step 2; `h = dt / substeps`.
742
+ 4. `yaw'` as topdown's step 5: the input `yaw` wrapped, or, when that is not usable, the previous yaw as the body was read (*Body*: float32, never wrapped, so a previous yaw of 4 stays 4). `pitch'` is the input `pitch` wrapped as `yaw'` is, or, when that is not usable, the previous pitch as the body was read (never wrapped); either is then clamped to [−`pitchLimit`, `pitchLimit`] (a float32 already), so a previous pitch of 4 gives `pitchLimit`, not the −`pitchLimit` a wrapped 4 − 2π would.
743
+ 5. When `dt > 0`: `pressed = jump and not jumpHeld` (the jump edge, taken once per input), then `jumpHeld = jump`, then run `substeps` sub-steps in turn, each with step length `h`. When `dt = 0`, none, and `jumpHeld` stays as read.
744
+ 6. Write `x y z vx vy vz duck` as the last sub-step left them (or as read, with −0 as 0), `yaw'`, `pitch'`, and `mstate = onGround + 2 · ducking + 4 · jumpHeld`.
745
+
746
+ One sub-step, with `hull(level) = standHeight + (crouchHeight − standHeight) · level`, `H` the current hull height `hull(duck)` wherever it is used, and `r = radius`. Each procedure's names are its own: a name a procedure sets is not the same value as one another procedure sets.
747
+ 1. `duck()`.
748
+ 2. `was = onGround`; `checkGround()`; when `onGround` and not `was`: `vz = 0`.
749
+ 3. `base = maxSpeed · cap`; `target = base · crouchSpeed` when `ducking`, else `base · walkSpeed` when `walk`, else `base`; `wishSpeed = amount · target`.
750
+ 4. When `onGround`: if `pressed`, then `vz = jumpImpulse`, `onGround = false`, `pressed = false` and `airAccelerate()`; otherwise `friction()`, `accelerate()` and `vz = 0`. When not `onGround`: `airAccelerate()`. (A press made while airborne therefore jumps on the first sub-step of its input that starts on the ground, before any friction.)
751
+ 5. `falling = not onGround`; when `falling`: `vz = vz − gravity · h · 0.5`.
752
+ 6. `vx`, `vy`, `vz` each clamped to [−`maxVelocity`, `maxVelocity`].
753
+ 7. When `onGround`: `groundMove()`, then `stayOnGround()`; otherwise `slideMove(h)`.
754
+ 8. `checkGround()`; then when `falling` and not `onGround`: `vz = vz − gravity · h · 0.5`; else when `onGround`: `vz = 0`.
755
+ 9. `x y z vx vy vz` are clamped to [−1e9, 1e9] and rounded to float32, `duck` is rounded to float32, and a −0 among them is written as 0. The next sub-step starts from these values.
756
+
757
+ The procedures, each reading and writing the sub-step's state:
758
+ - **`duck()`**: `rate = h / duckTime`. When `duckDown`: `next = min(1, duck + rate)`; when not `onGround`, `z = z + (hull(duck) − hull(next))` (in the air the hull shrinks from below, its top staying put, as Source's in-air duck lifts the origin: a crouch-jump clears higher ledges); then `duck = next` and `ducking = true`. Otherwise, when `duck > 0`: `next = max(0, duck − rate)`; `zn = z` when `onGround`, else `z − (hull(next) − hull(duck))` (in the air it grows back downward); if `fits(x, y, zn, hull(next))`, then `duck = next`, `z = zn`, and `ducking = false` when `duck = 0`; if it does not fit, `duck`, `z` and `ducking` stay as they are. Otherwise (duck not held and `duck = 0`) nothing changes. `ducking` is therefore cleared only on the sub-step that brings `duck` down to 0, never set or cleared by anything but `duck()`: a body read with `ducking` set and `duck` 0 keeps crouch speed until a duck is pressed and released.
759
+ - **`support(ramp, px, py)`**, the ramp top's highest z under the footprint: with `m0`, `m1`, `u` = `minX`, `maxX`, `px` for `axis` 0 and `minY`, `maxY`, `py` for `axis` 1: `span = m1 − m0`; `slope = (hi − lo) / span`; `u' = u + r` when `slope > 0`, `u − r` when `slope < 0`, else `u`; result `lo + slope · clamp(u' − m0, 0, span)`.
760
+ - **`overlaps(px, py, solid)`**: `px + r > minX`, `px − r < maxX`, `py + r > minY` and `py − r < maxY`, all strict.
761
+ - **`fits(px, py, pz, height)`**: false when any solid `overlaps(px, py, solid)` with `pz + height > minZ` and `pz < top − 0.0001`, where `top` is a box's `maxZ` or a ramp's `support(ramp, px, py)`; otherwise true.
762
+ - **`traceDown(px, py, pz, down, up)`**: over the solids that `overlaps(px, py, solid)` with `top ≤ pz + up` and `top ≥ pz − down`, where `top` is a box's `maxZ` or a ramp's `support(ramp, px, py)`: `g`, the largest `top` of a box or of a ramp with `1 / sqrt(1 + slope · slope) ≥ standable` (`slope` as in `support`), and `steep`, the largest `top` of any other ramp (one too steep to stand on). The result is `g`; none when there is no `g`, or when there is a `steep` and `steep > g` (a hull traced down stops on the steep ramp first, and there is no ground under it).
763
+ - **`checkGround()`**: when `vz > 0.1`, `onGround = false`. Otherwise, with `g = traceDown(x, y, z, groundTrace, groundSnap)`: when there is one, `z = g` and `onGround = true`; when there is none, `onGround = false`.
764
+ - *Starting inside.* A hull that starts a move more than `skin` inside a solid (the box or ramp grown by it holds its point that deep from every face) was put there by something other than its own moves: a bump, a teleport, a module's write. The move's own tests never see such a solid (every face's entry is behind the point), so `slab` and `wedge` report it first: a hit at `t = 0` whose normal is the solid's least deep face, and whose `depth` is how deep, and `slideMove` pushes the hull out through that face before it moves. The face is never the solid's bottom: a body pushed sideways into a wall that stands on a floor, deeper than its own height, would be pushed down through the floor. Nor is it a face that points up (a box's top, a ramp's top or cap) that another solid covers. `covered(q, p, self)`, for the point `p` and the point pushed out through the face, `q = (px + nx · depth, py + ny · depth, pz + nz · depth)` with the face's unit normal `n` and its depth, is true when some solid but `self` has `q` `inside` it and not `p` (a solid the hull is already in is no lid on the way out: it pushes the hull out in turn). A body pushed sideways into a wall whose top meets a ceiling or a roof, deeper than its own height, would otherwise be pushed up onto the wall's top, inside the ceiling, and from there (the ceiling's bottom never being the way out) out over it. `inside(px, py, pz, solid, height)` is true when the point is more than `skin` inside every face of the solid grown by the hull: for a box, `px − (minX − r)`, `(maxX + r) − px`, `py − (minY − r)`, `(maxY + r) − py`, `pz − (minZ − height)` and `maxZ − pz` are all greater than `skin`; for a ramp, each of `wedge`'s seven planes' depth `−(nx · px + ny · py + nz · pz − e) · inv` is. `covered` tests every solid of the world but `self`, every box and every ramp, never only a broad phase's candidates, so a broad phase gives the same answer as none. A hull's own moves keep it `skin` clear of every face they meet, so a body that nothing else moves meets this rule only after starting a move at least `skin` behind a face (which the touching rule leaves unentered). A hit on the way in has `depth` 0.
765
+ - **`slab(px, py, pz, dx, dy, dz, box, height)`**, a point against the box grown by the hull, `[minX − r, maxX + r] × [minY − r, maxY + r] × [minZ − height, maxZ]`. First *Starting inside*: when `px − lo`, `hi − px`, `py − lo`, `hi − py`, `pz − lo` and `hi − pz` (each axis's grown `lo` and `hi`) are all greater than `skin`, the test reports a hit at `t = 0` and stops. Its face is the least of five, in this order, a later one only when strictly smaller: `px − lo` with normal (−1, 0, 0), `hi − px` with (1, 0, 0), `py − lo` with (0, −1, 0), `hi − py` with (0, 1, 0), and `hi − pz` with (0, 0, 1) (never `pz − lo`, the bottom), the top only when it is not covered (`covered` of the point pushed out through it, `(px + 0 · hz, py + 0 · hz, pz + 1 · hz)` with `hz = hi − pz`, which a port tests only when `hz` would win); `depth` is that least value. Otherwise: `tMin = 0`, `tMax = 1`, no axis. For x, then y, then z, with that axis's `p`, `d` and grown `lo`, `hi`:
766
+ - when `|d| < 0.000000001` (the move is parallel to this axis's faces): unless `p > lo` and `p < hi`, the test reports no hit and stops; otherwise go to the next axis.
767
+ - `t1 = (lo − p) / d`, `t2 = (hi − p) / d`, `sign = −1`; when `t1 > t2`, swap them and `sign = 1`.
768
+ - when `t1 ≥ tMin`: `tMin = t1`, and this axis is taken with this `sign`. When `t2 < tMax`: `tMax = t2`. When `tMin > tMax`: the test reports no hit and stops.
769
+
770
+ After z: no hit when `tMin ≥ 1` or no axis was taken; otherwise a hit at `tMin` whose normal is the taken axis's `sign` along that axis (0 on the other two), with `depth` 0.
771
+ - **`wedge(px, py, pz, dx, dy, dz, ramp, height)`**: `span`, `slope` as in `support`, `m0` its min end. The planes `(n, e)`, in this order: `((1, 0, 0), maxX + r)`, `((−1, 0, 0), −minX + r)`, `((0, 1, 0), maxY + r)`, `((0, −1, 0), −minY + r)`, `((0, 0, −1), −minZ + height)`, the top, `n = (−slope, 0, 1)` for `axis` 0 or `(0, −slope, 1)` for `axis` 1 with `e = lo − slope · m0 + r · |slope|`, and the cap, `((0, 0, 1), maxZ)` (the grown top rises past `maxZ` within `r` of the high end, where `support` is flat; the cap keeps the grown wedge the hull's true sum with the ramp, so no lip stands there). `enter = 0`, `leave = 1`, no normal. For each plane:
772
+ - `dist = nx · px + ny · py + nz · pz − e`, `motion = nx · dx + ny · dy + nz · dz`, `inv = 1 / sqrt(nx · nx + ny · ny + nz · nz)`, and the plane's depth `−dist · inv`, for *Starting inside* below.
773
+ - when `|motion| < 0.000000001` (the move is parallel to the plane): if `dist > 0`, the test reports no hit and stops; otherwise go to the next plane.
774
+ - `t = −dist / motion`, except `t = 0` when `motion < 0`, `dist ≤ 0` and `dist > −skin` (a plane the point starts on or at most `skin` behind, as `checkGround` and the float32 rounding leave a hull snapped onto a ramp, is entered at once: Source's touching rule). When `motion < 0` and `t ≥ enter`: `enter = t`, and the normal is `(nx · inv, ny · inv, nz · inv)` with `inv = 1 / sqrt(nx · nx + ny · ny + nz · nz)`. When `motion > 0`: `leave = min(leave, t)`. When `enter > leave`: the test reports no hit and stops.
775
+
776
+ After the cap, *Starting inside* first: when every plane's depth is greater than `skin`, the result is a hit at `t = 0` whose normal is `(nx · inv, ny · inv, nz · inv)` of the plane with the least depth among all but the bottom `(0, 0, −1)` (the earlier plane on a tie), the top and the cap each only when it is not covered (`covered` of the point pushed out through it along its unit normal, which a port tests only when that plane would win), with that depth as `depth`. (No plane's test above stops early for a point that deep inside, so a port may gather the depths in the same loop.) Otherwise: a hit at `enter` with that normal when a normal was taken, `enter < 1` and `leave ≥ 0`, with `depth` 0; otherwise no hit.
777
+ - **`sweep(px, py, pz, dx, dy, dz)`**: every box by `slab` and every ramp by `wedge`, both with `H`. The winner is chosen on each hit's raw `t`, as `slab` or `wedge` reported it: the smallest wins, a later solid only when its `t` is strictly smaller, so a tie goes to the lower index. Only then is the winner's fraction adjusted: the result is that hit with fraction `max(0, t − 0.0001)`, its normal and its `depth`, or, with no hit, fraction 1 and no normal. (Two hits whose raw `t` are both below 0.0001 both adjust to 0, and the smaller raw `t` still wins, not the lower index.)
778
+ - **`slideMove(T)`**: `remaining = T`, no previous normal. Up to four times, while `remaining > 0.000001`:
779
+ 1. stop when `vx · vx + vy · vy + vz · vz < 0.00000001`.
780
+ 2. `(t, n) = sweep(x, y, z, vx · remaining, vy · remaining, vz · remaining)`.
781
+ 3. when `t > 0`: `used = t · remaining`; `x = x + vx · used`, `y = y + vy · used`, `z = z + vz · used`; `remaining = remaining − used`.
782
+ 4. stop when there is no normal. When the hit's `depth > 0` (*Starting inside*): `x = x + nx · depth`, `y = y + ny · depth`, `z = z + nz · depth`, out through the face the hit reported.
783
+ 5. `x = x + nx · skin`, `y = y + ny · skin`, `z = z + nz · skin`.
784
+ 6. `into = vx · nx + vy · ny + vz · nz`; when `into < 0`: `vx = vx + nx · (−into)`, `vy = vy + ny · (−into)`, `vz = vz + nz · (−into)`.
785
+ 7. when there is a previous normal `q`: the crease `c = (qy · nz − qz · ny, qz · nx − qx · nz, qx · ny − qy · nx)`; `L = cx · cx + cy · cy + cz · cz`; when `L > 0.000001`: `inv = 1 / sqrt(L)`, `c = (cx · inv, cy · inv, cz · inv)`, `along = vx · cx + vy · cy + vz · cz`, `v = (cx · along, cy · along, cz · along)`.
786
+ 8. the previous normal is now `n`; stop when `vx · vx + vy · vy + vz · vz < 0.00000001`.
787
+ - **`groundMove()`**: save `(x0, y0, z0)` and `v0`. `slideMove(h)`; save its position `(x1, y1, z1)` and velocity `v1`, and `flat = ax · ax + ay · ay` with `ax = x1 − x0`, `ay = y1 − y0`. Restore `(x0, y0, z0)` and `v0`. `lift = stepHeight + skin`; `(t, ·) = sweep(x0, y0, z0, 0, 0, lift)`; `zl = z0 + lift · t`. When `t > 0` and `fits(x0, y0, zl, H)`: `z = zl`; `slideMove(h)`; `g = traceDown(x, y, z, stepHeight · 2, 0.001)`, and `z = g` when there is one; `stepped = bx · bx + by · by` with `bx = x − x0`, `by = y − y0`; when there is a `g` and `stepped > flat + 0.001`: `vz = 0`, and the stepped move stands. In every other case the position and velocity are `(x1, y1, z1)` and `v1`. (A ramp too steep to stand on under the stepped hull leaves no `g`, so it is a wall here, never a step.)
788
+ - **`stayOnGround()`**, Source's `StayOnGround`: `g = traceDown(x, y, z, stepHeight, groundSnap)`; when there is one: `z = g` and `vz = 0`. A body running down a slope or a stair keeps its feet on it instead of hopping off each sub-step's drop.
789
+ - **`friction()`**: `speed = sqrt(vx · vx + vy · vy)`. When `speed < 0.1`: `vx = 0`, `vy = 0`. Otherwise `control = speed < stopSpeed ? stopSpeed : speed`; `drop = control · friction · h`; `keep = max(0, speed − drop) / speed`; `vx = vx · keep`, `vy = vy · keep`.
790
+ - **`accelerate()`**: `add = wishSpeed − (vx · dirX + vy · dirY)`; nothing when `add ≤ 0`. `a = accelerate · h · max(wishSpeed, maxSpeed)` (`maxSpeed`, not `base`: `cap` never weakens the push), then `a = add` when `a > add`; `vx = vx + a · dirX`, `vy = vy + a · dirY`.
791
+ - **`airAccelerate()`**: `add = min(wishSpeed, airMaxWishSpeed) − (vx · dirX + vy · dirY)`; nothing when `add ≤ 0`. `a = airAccelerate · wishSpeed · h`, then `a = add` when `a > add`; `vx = vx + a · dirX`, `vy = vy + a · dirY`.
792
+
793
+ *Posed solids.* A step may be handed a tick's *posed solids* beside the world (TypeScript `posedWorld`; §17.11 *Lifts* poses them): each an axis-aligned box `[min, max]`, a box of half extents `(hx, hy, hz)` about centre `c` turned about z by an angle given as its sine and cosine (`obox`; the pair is the yaw's `psincos`), or an upright disc of `radius` about `c` with half height `hz`. They are indexed after the world's solids, in the order given, and every procedure above treats them as solids: `overlaps` (a box's footprint test; an obox's strictly on its four axes, `|c·dx + s·dy| < hx + r(|c| + |s|)` and `|−s·dx + c·dy| < hy + r(|c| + |s|)`, after its bounding box's; a disc's when the hull square's nearest point is strictly inside the circle, `ax² + ay² < R²` with `ax = max(|dx| − r, 0)`), a flat top (`maxZ = cz + hz`, standable as a box's), `fits`, `traceDown`, `inside` and `covered` (over every solid, posed ones included). In `sweep` a posed box is a box (`slab`); an obox is `wedge`'s plane method over ten planes in this order, each normal made unit as `wedge` makes it: its four faces `±u`, `±v` (`u = (c, s, 0)`, `v = (−s, c, 0)`) at `±(u·C) + hx + r(|c| + |s|)` and `±(v·C) + hy + r(|c| + |s|)` (u+, u−, v+, v−), the hull's four `±x`, `±y` at `±cx + r + hx|c| + hy|s|` and `±cy + r + hx|s| + hy|c|` (x+, x−, y+, y−), the bottom `(0, 0, −1)` at `−minZ + H`, the top `(0, 0, 1)` at `maxZ` (a lid: *Starting inside* takes it only when uncovered); a disc is first *Starting inside* (more than skin inside its z span and its grown footprint: depth `R + r − max(|dx|, |dy|)` along the axis with the larger, x on a tie, when both `|dx|, |dy| ≤ r`, else `R − |q|` along `q/|q|`, `q` the offset beyond the hull square; out through that side, or its top when shallower and uncovered, never its bottom), then the earliest hit of its pieces, the x box `[cx − R, cx + R] × [cy, cy]`, the y box `[cx, cx] × [cy − R, cy + R]` (each by `slab`, z `[minZ, maxZ]`) and four upright cylinders of radius `R` about `(cx ∓ r, cy ∓ r)` in the order (−,−), (+,−), (−,+), (+,+), z `[minZ − H, maxZ]` (the circle's entry `t = (−b − √(b² − a·c))/a`, `a = dx² + dy²`, `b = ex·dx + ey·dy`, `c = ex² + ey² − R²`, none when the discriminant is negative, inside the circle all along when `a < 1e-18` and `c < 0`; then z as `slab`'s axis; a side hit's normal radial at the hit point), a later piece only when strictly earlier. A query visits a posed solid when its bounding box, grown by `r` on x and y and the query's margin, meets the query's box (the broadphase's own test; the result never depends on it). Without posed solids every result is as above, bit for bit. Held to `lifts.json`'s `presetOnLift`.
794
+
795
+ **Portable sine and cosine.** `psin(a)` and `pcos(a)` are the sine and cosine of a float64 `a` that every port computes to the same bits. A preset that needs an angle's sine or cosine calls these and never the platform's `sin` and `cos`, which differ in the last bit between C libraries, .NET, V8 and GDScript. They are Cody–Waite reduction by π/2 with fdlibm's three-part split of π/2 (`e_rem_pio2.c`), followed by fdlibm's sine and cosine polynomials (`k_sin.c`, and `k_cos.c` in FreeBSD msun's form). The polynomials are in Horner form, and the kernels' tail argument is dropped. The arithmetic is `firstperson`'s (*Arithmetic*: float64, the written order, no fused multiply-add) using `+ − ·`, `floor`, and one division by 4 (exact here).
796
+
797
+ The constants are float64 values with the bits shown, each equal to the exact value beside it. A port writes each constant in one of three ways: by its bits; as `M / 2^k` evaluated in float64 (`M` is an integer below 2^53 and `2^k` a power of two, both exact, so the one division is exact); or as a decimal literal, but only in a language that parses decimals correctly rounded (a port that cannot rely on that, GDScript among them, uses the `M / 2^k` form).
798
+
799
+ | Name | Bits | Exact value |
800
+ |---|---|---|
801
+ | `invpio2` | `0x3FE45F306DC9C883` | 5734161139222659 / 2^53 = 0.63661977236758138243288840385503135621547698974609375 |
802
+ | `pio2_1` | `0x3FF921FB54400000` | 1686629713 / 2^30 = 1.570796326734125614166259765625 |
803
+ | `pio2_2` | `0x3DD0B4611A600000` | 2242054355 / 2^65 = 0.00000000006077100506303965976595549136618501506745815277099609375 |
804
+ | `pio2_2t` | `0x3BA3198A2E037073` | 5376105825661043 / 2^121 = 0.0000000000000000000020222662487959506315411442613699269807606419657893030583802572441864953134427196346223354339599609375 |
805
+ | `S1` | `0xBFC5555555555549` | −6004799503160649 / 2^55 = −0.1666666666666663243479007405767333693802356719970703125 |
806
+ | `S2` | `0x3F8111111110F8A6` | 2401919801261139 / 2^58 = 0.0083333333333224894612367705803990247659385204315185546875 |
807
+ | `S3` | `0xBF2A01A019C161D5` | −7320136532976085 / 2^65 = −0.00019841269829857949313391130008454865674139000475406646728515625 |
808
+ | `S4` | `0x3EC71DE357B1FE7D` | 6506786730409597 / 2^71 = 0.00000275573137070700676789039053338203899556901887990534305572509765625 |
809
+ | `S5` | `0xBE5AE5E68A2B9CEB` | −7571127717829867 / 2^78 = −0.000000025050760253406863419538120707312500723418224879424087703227996826171875 |
810
+ | `S6` | `0x3DE5D93A5ACFD57C` | 1537454791456095 / 2^83 = 0.00000000015896909952115501022112634498906040547705487142593483440577983856201171875 |
811
+ | `C1` | `0x3FA555555555554C` | 1501199875790163 / 2^55 = 0.0416666666666666019036568968658684752881526947021484375 |
812
+ | `C2` | `0xBF56C16C16C15177` | −6405119470031223 / 2^62 = −0.00138888888888741095749279619298022225848399102687835693359375 |
813
+ | `C3` | `0x3EFA01A019CB1590` | 457508533350745 / 2^64 = 0.0000248015872894767294178086647349346094415523111820220947265625 |
814
+ | `C4` | `0xBE927E4F809C52AD` | −5205429506036397 / 2^74 = −0.00000027557314351390663303457907902516144105220519122667610645294189453125 |
815
+ | `C5` | `0x3E21EE9EBDB4B1C4` | 1261860039765105 / 2^79 = 0.0000000020875723212981748278973986626856629289505917768110521137714385986328125 |
816
+ | `C6` | `0xBDA8FAE9BE8838D4` | −1757820317994549 / 2^87 = −0.000000000011359647557788194826454950643333297911896639931228492059744894504547119140625 |
817
+
818
+ For one `a`, both functions run:
819
+ 1. When `a` is not finite, or `|a| > 1e9`: `psin(a) = 0` and `pcos(a) = 1`, and the steps below are skipped. An angle that cannot be used is read as 0, just as an input axis is.
820
+ 2. `n = floor(a · invpio2 + 0.5)`. This is the nearest integer to `a · 2/π`, with a half rounded up (toward +∞), except where the float64 sum rounds onto the other neighbour. The kernels stay accurate a little past `|r|` = π/4, so that choice costs no accuracy, and these steps fix which neighbour is chosen. `|n| ≤ 636619773`, so a port may hold `n` as a 32-bit integer.
821
+ 3. `q = n − 4 · floor(n / 4)`, in {0, 1, 2, 3}. It is exact: on a two's-complement integer it is `n AND 3`, so −1 gives 3.
822
+ 4. `r = ((a − n · pio2_1) − n · pio2_2) − n · pio2_2t`, with `n` as a float64.
823
+ 5. `z = r · r`. The sine kernel is `ks = r + (z · r) · (S1 + z · (S2 + z · (S3 + z · (S4 + z · (S5 + z · S6)))))`. For the cosine kernel, `hz = 0.5 · z`, `w = 1 − hz`, and `kc = w + (((1 − w) − hz) + z · (z · (C1 + z · (C2 + z · (C3 + z · (C4 + z · (C5 + z · C6)))))))`.
824
+ 6. By `q`:
825
+
826
+ | `q` | `psin(a)` | `pcos(a)` |
827
+ |---|---|---|
828
+ | 0 | `ks` | `kc` |
829
+ | 1 | `kc` | `−ks` |
830
+ | 2 | `−ks` | `−kc` |
831
+ | 3 | `−kc` | `ks` |
832
+
833
+ 7. A −0 result is returned as 0.
834
+
835
+ A port may compute steps 2–5 once for both functions, since their results are the same. *Accuracy (not normative):* measured against a 300-bit reference, `psin` and `pcos` are never more than one double from the correctly rounded value for every float32 sampled in [−π, π] (at most about 1.4 ulp from the true value: the dropped tail argument costs up to about 0.4 ulp), and within 3 ulp (2.24 measured) for float64 inputs up to `|a|` = 2^21 · π/2 ≈ 3.29e6. Up to that size `n · pio2_1` and `n · pio2_2` are exact. Past it `n · pio2_2` rounds: near a zero of the function the error is then absolute rather than relative, about 3e-20 up to 2^22 · π/2, where `n · pio2_1` starts to round as well, and it grows to about 6e-8 absolute at 1e9; the result is still deterministic. About a quarter of float32 inputs give something other than the correctly rounded value, so a port that calls the platform's `sin` or `cos` instead fails `sincos.json` (§17.8).
836
+
837
+ **`vehicle`** is an arcade car based on Rocket League's ground and air handling, with the car kept level (no pitch or roll). It has throttle, braking and coasting, boost from a fuel tank, and grip that turns the velocity with the heading. A drift button lets the tail slide out. The turn rate grows with speed and mirrors in reverse. It also has a jump and steering in the air. The car moves the same axis-aligned hull through the same world as `firstperson`, with that preset's procedures: `checkGround`, `traceDown`, `fits`, `sweep` (with `slab` and `wedge`), `slideMove`, `groundMove` and `stayOnGround`, exactly as written there, except that the slides inside its ground move are the car's ground slide (below). In those procedures `H` is the `height` parameter throughout, and `r = radius`. The hull does not turn with the heading. Its square footprint stands in for the car's: with the defaults it is 100 across, for a car about 118 long and 84 wide.
838
+
839
+ *Input.* `moveY` is the throttle: below 0 drives in reverse. `moveX` steers: a positive value turns right, toward −yaw (with z up, +yaw is counterclockwise seen from above), so a client sends its steer-right key as `moveX` = +1, as it does for any other right input. `yaw`, `pitch` and `moveZ` are unused, because the heading is part of the body's state, not the input. `buttons` carries jump, boost and drift under the masks below. The step applies its own rules to the input exactly as it is handed, as `firstperson`'s *Input* says.
840
+
841
+ *Tick rate.* The step integrates the yaw rate, the grip and the heading's turn of the velocity once per sub-step. A different `tickHz` or `substeps` therefore gives a slightly different car, though every engine still gives the same one. A jump pressed on an input whose last sub-step lands is lost, as in `firstperson`. With the default `stepHeight`, `stayOnGround` holds the car to a downhill road only while one sub-step's drop stays under `stepHeight`: at top boost speed that means a slope of about 23° at 120 sub-steps per second, and about 12° at 60. The same rule decides a crest, where a ramp gives way to flatter ground: the car keeps the ramp's `vz` and leaves the ground when one sub-step's rise past the crest is more than `stepHeight`, so whether it hops there depends on the rate and the phase. Uphill there is no such limit: the ground slide (sub-step step 9) lifts the velocity onto a face the car can drive on and keeps its horizontal speed, so a car climbs any standable ramp at the speed it drives on the flat, at any rate. A `vehicle` game should ask for `tickHz` 60.
842
+
843
+ *Walls.* A face the car meets on the ground is driven up when it is a ramp top it can drive on (`standable`) or a ledge within `stepHeight`; any other face is a wall, a ramp top too steep to drive on included (the ground slide makes its normal level, so the car is never lifted up it). A wall clips the velocity along it, and nothing bounces: a car that meets one head-on, a kerb over `stepHeight` included, stops in one sub-step. A car scraping along a wall with its heading turned into it by θ loses speed, as a real car's tyres would make it: each sub-step's clip turns the velocity off the heading and `grip` then removes that sideways part, so the throttle holds the speed only while `tan θ < accel / grip` (about 9.5° with the defaults). At 15° a car on full throttle slows from 1410 to about 780 in a second, and at 30° to a crawl; turning parallel to the wall recovers it (`wall-scrape-15` and `wall-angle` in `vehicle.json`).
844
+
845
+ *Parameters.* The rules of `firstperson`'s *Parameters* apply unchanged (range, refusal, float32 use, `world` as an exact u32), with this table. The defaults are a small arcade car in units of about a centimetre, which is Rocket League's own scale: a car about 120 long, a top speed of 1410 on the throttle and 2300 with boost, and gravity of 650. A game in other units sets every distance, speed and acceleration in its own units, `skin` included.
846
+
847
+ | Name | Default | Range | Meaning |
848
+ |---|---|---|---|
849
+ | `maxSpeed` | 1410 | [0, 100000] | Top speed on the throttle, units/s, before `cap` |
850
+ | `accel` | 1000 | [0, 100000] | Throttle acceleration, units/s² |
851
+ | `brake` | 3500 | [0, 100000] | Deceleration while the throttle opposes the motion |
852
+ | `coast` | 525 | [0, 100000] | Deceleration with no throttle, and down to the throttle's speed when above it without boost |
853
+ | `boostAccel` | 1000 | [0, 100000] | Boost's acceleration along the heading, on top of the throttle's |
854
+ | `boostMaxSpeed` | 2300 | [0, 100000] | Top speed under boost, before `cap` |
855
+ | `boostBurn` | 0.3333333432674408 | [0, 1000] | Fuel burned per second of boost: a full tank (1) lasts 3 s |
856
+ | `grip` | 6000 | [0, 100000] | Deceleration of the sideways speed on the ground, units/s² |
857
+ | `driftGrip` | 1500 | [0, 100000] | The same, while drift is held |
858
+ | `turnRate` | 2.2 | [0, 100] | Yaw rate at full steer on the ground, rad/s, at or above `turnSpeed` |
859
+ | `turnSpeed` | 400 | (0, 100000] | The forward speed at which the full turn rate is reached; below it, the rate scales with speed |
860
+ | `spinAccel` | 30 | [0, 10000] | How fast the yaw rate follows the steering on the ground, rad/s² |
861
+ | `airTurnRate` | 5.5 | [0, 100] | Yaw rate at full steer in the air, rad/s |
862
+ | `airSpinAccel` | 9 | [0, 10000] | How fast the yaw rate follows the steering in the air, rad/s² |
863
+ | `gravity` | 650 | [0, 100000] | Units/s² along −z |
864
+ | `jumpImpulse` | 450 | [0, 100000] | The `vz` a jump sets |
865
+ | `maxVelocity` | 6000 | [0, 100000] | Each velocity axis is clamped to ±this |
866
+ | `radius` | 50 | (0, 10000] | Half the side of the hull's square footprint |
867
+ | `height` | 36 | (0, 10000] | Hull height |
868
+ | `stepHeight` | 8 | [0, 10000] | Highest ledge driven up without a jump, and the deepest drop a sub-step follows down |
869
+ | `standable` | 0.7 | [0, 1] | Least z of a ramp top's unit normal that can be driven on |
870
+ | `groundTrace` | 4 | [0, 10000] | How far below the hull's base ground is found |
871
+ | `groundSnap` | 2 | [0, 10000] | How far above the hull's base ground is found |
872
+ | `skin` | 0.03 | [0, 100] | Clearance kept from a surface |
873
+ | `jumpBit`, `boostBit`, `driftBit` | 1, 2, 4 | integer, [0, 2^24) | Button masks: any shared bit counts as held; 0 disables |
874
+ | `substeps` | 2 | integer, [1, 8] | Sub-steps per step |
875
+ | `world` | 2615243109 | integer, [0, 2^32 − 1] | The collision world's hash, as `firstperson`'s |
876
+
877
+ *Body.* The step reads and writes `x y z vx vy vz yaw spin fuel mstate`, and reads `cap`. It never writes `pitch` or `duck`, which stay as they are. Each field is read sanitized:
878
+ - `x` to `vz`, `yaw` and `cap`: as `firstperson`'s *Body*.
879
+ - `spin`, the yaw rate in rad/s (positive toward +yaw): missing or non-finite → 0, clamped to [−1000, 1000].
880
+ - `fuel`, the boost tank: missing or non-finite → 0, clamped to [0, 1].
881
+ - Each of these is then rounded to float32.
882
+ - `mstate`: as `firstperson`'s, but only the bits of value 1 (`onGround`) and 4 (`jumpHeld`) are kept, so the `ducking` bit is read as 0.
883
+
884
+ The module writes `cap`, which scales the two top speeds and never an acceleration. It writes `fuel` to refuel (a boost pad, a respawn), and on a respawn it writes `spin` 0 and `mstate` 0. The step writes `fuel` only to burn it.
885
+
886
+ *World.* As `firstperson`'s *World* in every respect, with `name: 'vehicle'` in the module's config: the same `world` export, the same validation and the hash written into `params.world`, the same client `setWorld` and prediction gate, and the same broadphase rule.
887
+
888
+ *Arithmetic.* As `firstperson`'s, with `floor`, `psin` and `pcos` added to the functions allowed beyond `+ − · /`. As there, a zero's sign never changes a comparison or a nonzero result, so a port need not track it. The step uses two helpers:
889
+ - **`toward(v, target, d)`**, with `d ≥ 0`: `min(target, v + d)` when `v < target`, `max(target, v − d)` when `v > target`, else `v`. It never passes `target`.
890
+ - **`wrap(v)`**, topdown's step 5 for a usable value: `float32(v − 2π · floor((v + π) / 2π))`. When that equals float32(π) the result is −float32(π), and a −0 result is 0.
891
+
892
+ One step with input `(moveX, moveY, buttons)` and length `dt`:
893
+ 1. `steer` is `−moveX` (so a positive `steer` turns toward +yaw) and `throttle` is `moveY`, each sanitized as in topdown's step 1, the negation applied after: a `−0` steer is 0. Neither is normalized against the other.
894
+ 2. `buttons`: an integer in [0, 2^24), else 0. `jump = (buttons AND jumpBit) ≠ 0`; `boost` and `drift` likewise, with `boostBit` and `driftBit`.
895
+ 3. `dt` as in topdown's step 2; `h = dt / substeps`.
896
+ 4. `yaw = wrap(yaw)` on the yaw as read when its magnitude is at most 1e9; otherwise 0.
897
+ 5. When `dt > 0`: `pressed = jump and not jumpHeld`, then `jumpHeld = jump`, then run `substeps` sub-steps in turn, each of length `h`. When `dt = 0`: no sub-steps run, and `jumpHeld` stays as read.
898
+ 6. Write `x y z vx vy vz yaw spin fuel` as the last sub-step left them (with no sub-step: as read, with step 4's `yaw`, and −0 as 0), and `mstate = onGround + 4 · jumpHeld`.
899
+
900
+ One sub-step:
901
+ 1. `was = onGround`; `checkGround()`; when `onGround` and not `was`: `vz = 0`.
902
+ 2. `c = pcos(yaw)`, `s = psin(yaw)`. `f = vx · c + vy · s` is the speed along the heading, and `l = vy · c − vx · s` the speed along the heading turned by +90°.
903
+ 3. `boosting = boost and fuel > 0`. When `boosting`: `fuel = max(0, fuel − boostBurn · h)`. The sub-step that empties the tank still boosts.
904
+ 4. When `onGround` and `pressed`: `vz = jumpImpulse`, `onGround = false`, `pressed = false`.
905
+ 5. When `onGround`: `drive()`. Otherwise: `air()`. (The sub-step that jumps steers by the air rules.)
906
+ 6. `falling = not onGround`; when `falling`: `vz = vz − gravity · h · 0.5`.
907
+ 7. `vx`, `vy` and `vz` are each clamped to [−`maxVelocity`, `maxVelocity`].
908
+ 8. `yaw = wrap(yaw + spin · h)`. The argument is always usable here: `|yaw| ≤ float32(π)` and `|spin · h| ≤ 1000 · 0.25`.
909
+ 9. When `onGround`: `groundMove()`, then `stayOnGround()`, where each `slideMove` inside `groundMove` is the car's ground slide (below). Otherwise: `slideMove(h)` as written. A wall the car meets clips the velocity along it (`slideMove`'s slide); nothing bounces.
910
+ 10. `checkGround()`; then, when `falling` and not `onGround`: `vz = vz − gravity · h · 0.5`; else, when `onGround`: `vz = 0`.
911
+ 11. `x y z vx vy vz` are clamped to [−1e9, 1e9] and rounded to float32, `spin` and `fuel` are rounded to float32, and a −0 among them is written as 0. The next sub-step starts from these values.
912
+
913
+ The procedures, each reading and writing the sub-step's state. `c` and `s` are sub-step step 2's. `f` and `l` start as step 2 computed them; `drive()` updates them in place, and each later line of `drive()` reads the updated values (`air()` never changes them).
914
+ - **ground slide**: `slideMove` as `firstperson` writes it, except that after its step 4 (a hull that started inside a solid is out of it by then, through the face as the hit reported it) the hit's unit normal `n` is read by its `nz`:
915
+ - when `nz > 0` and `nz ≥ standable` (a face the car can drive on, the same test `traceDown` makes): steps 5, 7 and 8 as written, and step 6 is instead `into = vx · nx + vy · ny + vz · nz`; when `into < 0`: `vz = vz − into / nz`. The velocity is lifted onto the face with its `vx` and `vy` unchanged, so the climb costs no horizontal speed.
916
+ - when `0 < nz < standable` (a face too steep to drive on): `k = 1 / sqrt(nx · nx + ny · ny)`, and `n = (nx · k, ny · k, 0)` for steps 5 to 8. The face is a wall: the car stops against it or slides along it level, and is never lifted up it.
917
+ - otherwise (`nz ≤ 0`): as written.
918
+ - **`drive()`**:
919
+ 1. `top = maxSpeed · cap`; `t = 1` when `boosting` (boost always drives forward at full throttle), else `throttle`; `target = t · top`.
920
+ 2. The forward speed:
921
+ - when `t = 0`: `f = toward(f, 0, coast · h)`;
922
+ - otherwise, when `t > 0` and `f < 0`, or `t < 0` and `f > 0`: `f = toward(f, 0, brake · h)`;
923
+ - otherwise, when `|f| < |target|`: `f = toward(f, target, accel · h)`;
924
+ - otherwise, unless `boosting`: `f = toward(f, target, coast · h)`. Above the throttle's speed, the car slows back to it.
925
+ 3. When `boosting`: `boostTop = boostMaxSpeed · cap`; when `f < boostTop`: `f = min(boostTop, f + boostAccel · h)`.
926
+ 4. `l = toward(l, 0, grip · h)`, or with `driftGrip` in place of `grip` when `drift` is held.
927
+ 5. `want = steer · turnRate · min(1, |f| / turnSpeed)`; `want = −want` when `f < 0` (reverse steering is mirrored, as in a real car). `spin = toward(spin, want, spinAccel · h)`. A car standing still (`f = 0`) has `want` = 0, so its yaw rate returns to 0 (`turnSpeed` is never 0 as a float32, by *Parameters*, so `|f| / turnSpeed` is never 0/0).
928
+ 6. `vx = f · c − l · s`, `vy = f · s + l · c`, `vz = 0`. The velocity is rebuilt on the axes it was measured on. It turns with the heading only through `grip`, which removes the sideways speed the next sub-step's heading finds; with `driftGrip` the tail slides.
929
+ - **`air()`**:
930
+ 1. `spin = toward(spin, steer · airTurnRate, airSpinAccel · h)`.
931
+ 2. When `boosting`: `boostTop = boostMaxSpeed · cap`; when `f < boostTop`: `a = min(boostAccel · h, boostTop − f)`, `vx = vx + a · c`, `vy = vy + a · s`. Boost pushes along the level heading only and never changes `vz`.
932
+
933
+ There is no throttle, grip or drag in the air.
934
+
935
+ ### 17.8 Conformance
936
+
937
+ `packages/netcode-bench/conformance/authority/presets/<name>.json` holds each preset's vectors: parameters, a starting body, an input log with optional per-step `dt`, and the expected body after every step. Non-finite inputs are spelled `"NaN"`, `"Infinity"` and `"-Infinity"`. `firstperson.json`'s and `vehicle.json`'s cases each also carry the world they step in and its hash. The file holds only parameters and worlds a room accepts: refusing a config (a parameter out of range or not in the table, a refused world) is the server's (§17.4), which `test/authority-firstperson.test.ts` and `test/authority-vehicle.test.ts` hold to §17.7's rules, and is not something an engine's step does. An engine's port of a preset conforms when every field of every step lands within the file's `tolerance` (1e-4), or within a case's own `tolerance` where the case states one: every `discriminate-` case of `vehicle.json` states 0 and is matched bit for bit, because the mistakes it was chosen for (a fused multiply-add, the platform's sine) move a field by less than 1e-4, and only an exact match catches them. The TypeScript reference is held to the same file in `test/authority-presets.test.ts` (`firstperson.json` and `vehicle.json`, bit for bit and with and without the broadphase, in `test/authority-firstperson.test.ts` and `test/authority-vehicle.test.ts`). The expected values are not that reference's output: `<name>.reference.mjs` beside each file is a second implementation written from this section's text, imports nothing, and prints the file byte for byte (the same test regenerates and compares it). Until an engine's port passes, that engine renders its own entity from the server's state without prediction.
938
+
939
+ `projectile.json` beside them holds §17.9's flight vectors the same way (`projectile.reference.mjs` prints it): each case is a tick rate, a launch (`x y z vx vy vz g t0`) and samples `{ T, expect: { x, y, z, vx, vy, vz } }` at whole and fractional ticks, including ticks before the launch and hostile values. The `order-*` cases sample ticks where a reordered, float32-only or fused (multiply-add) evaluation lands on another float32, so such a port fails bit for bit. An engine's flight function conforms on the same terms as a preset's step.
940
+
941
+ `sincos.json` holds §17.7's portable sine and cosine the same way (`sincos.reference.mjs` prints it): inputs `a` (finite, non-finite, past 1e9, near multiples of π/2, and the float32 ends of [−π, π)), each with `psin(a)` and `pcos(a)` as exact float64 values. A port matches them bit for bit, with tolerance 0 (the TypeScript `psin` and `pcos` in `test/authority-sincos.test.ts`). A sine one ulp off moves a `vehicle` body's float32 state now and then, so the 1e-4 tolerance for preset fields does not apply here.
942
+
943
+ Physics has no preset-style step vectors: the core's own vectors are vendored under `conformance/authority/physics/` with a provenance pin, and an engine's drawing of bodies is held by `physics-draw.json` on the same terms as a preset's step (§17.10, *Conformance*).
944
+
945
+ ### 17.9 Projectiles
946
+
947
+ A projectile is an entity on a closed-form ballistic flight. The server writes its launch once, and every client computes where it is at any instant from that launch. Other players' projectiles therefore draw at the server's present instead of `RENDER_DELAY_MS` behind, and the shooter's own draw from the moment it fires. Projectiles combine with any movement preset (§17.7) or with none.
948
+
949
+ **Fields.** `AuthorityEntity` appends:
950
+ - `t0` number: the launch tick, an integer in [0, 2^32 − 1]; `0` means not a projectile.
951
+ - `g` float32: downward acceleration along −z, in units/s², in [0, 1000].
952
+ - `src` string: the shooter's seat session id, `''` for none, at most 64 characters.
953
+
954
+ `AuthorityRoomState` appends `tickOrigin` float64: the room clock (§16.1's `sNow` scale, ms) at tick 0's nominal instant, written when the fixed step starts and written again whenever a hitch makes the server drop steps it owes (Colyseus runs at most five in one call and drops the rest), so it always says when the ticks actually run. Tick `n`'s nominal instant is `tickOrigin + n · 1000 / tickHz`, from the latest `tickOrigin`; a client reads it live. The axes are the presets': x/y is the ground plane and +z is up.
955
+
956
+ **Flight.** At tick `T` (whole or fractional), a projectile launched at `(x, y, z, vx, vy, vz, g, t0)` in a room at `tickHz` is at, in float64:
957
+ 1. `s = (T − t0) / tickHz`; non-finite or negative → 0 (before its launch a projectile sits at its origin).
958
+ 2. `x' = float32(x + vx · s)`, `y' = float32(y + vy · s)`.
959
+ 3. `z' = float32(z + vz · s − 0.5 · g · s · s)`, evaluated left to right as `((z + (vz · s)) − (((0.5 · g) · s) · s))`.
960
+ 4. `vx' = vx`, `vy' = vy`, `vz' = float32(vz − g · s)`.
961
+ 5. A `-0` result is written as 0.
962
+
963
+ The inputs are the entity's float32 fields and the integer `t0` (a server holds what the wire holds: a value a module wrote that is not a float32 is read rounded), and only `+ − · /` are used, each rounded on its own (no fused multiply-add, §17.7), so the result is identical in every engine that follows these steps.
964
+
965
+ **Server.** A module launches with `ctx.projectiles.launch({ kind, x, y, z, vx, vy, vz, g?, hp?, flags?, data?, id? })`, which writes the entity with `t0` set to the tick being stepped and `owner` = `''`.
966
+ - **Launched while handling a seat's input:** `src` is that seat and the id is `<seat>.<seq>.<n>`. `seq` is that input's client sequence: the seat's `seqBase` (§17.3) plus Colyseus's `ackSeq` for it on its current socket, which counts that socket's inputs from 1. A reconnect is a new socket whose count starts again, and the server adds what the earlier sockets consumed to `seqBase` before the reconnect's first state, so the sequence goes on and an id never comes back. `n` counts that input's launches from 0. An `id` passed here is refused.
967
+ - **Launched anywhere else in a tick** (`step`, a join or a leave): `id` is required and `src` is `''`.
968
+ - **In `init`** a launch is refused: `t0` is the tick being stepped, and `init` runs before the first tick.
969
+ - **An id that already exists** is re-launched, for a bounce or a redirect: the new launch replaces the old one whole.
970
+
971
+ `ctx.projectiles.at(id, tick?)` returns the flight at a tick (default: the tick being stepped). `ctx.projectiles.sweep(id, { kinds, radius })` returns the entities of those kinds whose sphere of that radius the projectile's path crosses between the previous tick and this one, nearest first. A sweep visits only the entities of the kinds it asks for, so its cost is the size of those kinds: a couple of hundred projectiles each swept against the players fit a tick's CPU budget, while hundreds swept against each other do not. A module removes a projectile like any other entity; the server never ages one out. Projectiles take no part in lag compensation (§17.5): `rewind.hitscan` and `overlapAt` skip entities with `t0 > 0`, and `seen` never lists them.
972
+
973
+ **Client.** An entity with `t0 > 0` is drawn from its flight and never interpolated. It is never the client's own entity: §17.3's own entity is the first owned entity with `t0 = 0`. Let `serverTick(now) = (serverNow − tickOrigin) · tickHz / 1000`.
974
+ - **Another seat's projectile, or a server one:** drawn at `T = serverTick(now)`.
975
+ - **Your own (`src` = your seat):** the game predicts it when it fires, by calling `session.predictProjectile({ kind, x, y, z, vx, vy, vz, g })` while staging the input that fires.
976
+ - The client draws it at once from its own launch, keyed `<seat>.<seq>.<n>` (`seq` is the sequence of the input being staged, `n` its launches so far).
977
+ - While the socket is down (before the first connection, and from a drop until the reconnect's first state) the client predicts nothing and `predictProjectile` answers `''`: no input is queued across a drop, so the fire is never sent, and the seat's sequence is not known until that state writes its `seqBase`.
978
+ - When the server's entity with that id arrives, the client draws the server's flight at `T = serverTick(now) + lead` while its `src` is your seat. `lead` is the measured input lead in ticks: the server entity's launch instant minus the prediction's. The flight therefore continues the predicted one instead of snapping back by `lead × velocity`. Once the server re-launches it as its own (a bounce from `step`: `src` becomes `''`), it is a server projectile and is drawn as one.
979
+ - A prediction is dropped, unconfirmed, as soon as a state patch whose acknowledgement covers its firing input (Colyseus's last processed input, on the same socket, at or past its sequence) leaves the state with no entity of its id: the server refused the shot (a cooldown, a dead shooter), or launched it and removed it before any patch carried it (a hit at point blank). Not seeing it never tells which; the module's own events do. A prediction made on an earlier socket that the reconnect's first state does not confirm is dropped then: the server consumed what it received of that socket's inputs before it sent that state, so whatever they launched is in it or already removed, and a lost input's sequence is used again on the new socket, so the next shot would reuse the id and be confirmed against the lost prediction instead of its own. A prediction nothing else settles (its input was lost to a drop that has not reconnected yet) is dropped `max(2 · RTT, 600 ms)` after it was made, not counting a stall: while the socket is up, the time no state arrives is not a lost input (a reliable socket loses none), so the drop waits out the current silence, and for `max(2 · RTT, 600 ms)` after a stall ends it also waits out that stall's length (a shot fired into a one-second burst of loss would otherwise vanish and snap back when the server's arrives).
980
+ - This is Colyseus's predicted-spawn store (`@colyseus/sdk/predict`, `PredictedSpawns`), keyed by id.
981
+ - **A client that does not implement this section** draws each projectile frozen at its launch point: the server writes the launch once and never moves it, so there is nothing to interpolate. Every client of a room whose module launches projectiles must implement this section, and an engine SDK claims it only by passing the projectile conformance checks (§17.8, the netcode bench's `projectile` capability).
982
+
983
+ **Limits.** Projectiles count as entities against §17.4's caps (1024 per room, 256 updates and 32 KiB of output per tick), and the output cap binds first: a new projectile goes out whole, about 250 characters when its floats print in full. The launches waiting to go out (since the last output the server applied) may therefore hold at most 224 entities and 28 KiB of that output (`AuthorityLimits.MAX_LAUNCH_UPDATES`, `MAX_LAUNCH_BYTES`), about a hundred new projectiles a tick: a launch past either throws a `RangeError` and writes nothing, so a burst never makes an output the server must refuse. Uncaught, it is a strike for that tick, and the launches before it go out with the next. `validateTickOut` refuses:
984
+ - a `t0` that is not an integer in its range;
985
+ - a `g` outside [0, 1000];
986
+ - an entity with `t0 > 0` and a non-empty `owner`;
987
+ - a `src` that is neither `''` nor a valid seat id.
988
+
989
+ **A ball (not normative).** A module runs a ball, such as a `vehicle` game's, as one server projectile (`src` = `''`, a fixed id such as `ball`). It re-launches that projectile under the same id whenever the ball's flight changes, so every client draws the ball from its flight at the server's present, with no interpolation delay. Every tick in `step`, the module reads the ball with `ctx.projectiles.at('ball')` and re-launches it from where it is:
990
+ - **Off the world.** When the module's own sphere test against its exported world (floor, walls, ceiling, ramps by their closest point, not their planes, which report a touch near an edge before there is one) finds a contact with normal `n` while `v · n < 0`, the new velocity is `v − (1 + e) · (v · n) · n`, with restitution `e` (around 0.6). A tangential loss, if the game wants one, belongs to a real impact (`−v · n` over a threshold): a ball sliding on a slope touches it again every few ticks under gravity, and a loss on each of those glues it to the slope.
991
+ - **Off a car.** The contact is found where the ball and the car first touch during the tick, not where they stand after it: the ball's flight from the tick before and the car's move since the module last saw it, swept relative to each other to first contact (sampled, then halved down). Tested only at the end of the tick, a hit's normal and speed depend on how deep the car got in that tick, and at 30 Hz a fast car passes under the ball. At that contact the ball gets an impulse along the normal from the car's velocity relative to the ball, plus whatever share of the car's speed the game tunes in, and it is placed by where the car is now. A minimum hit speed, if the game wants one, scales with the car's own speed into the ball: a fixed one adds energy at every contact with a parked car, so a ball bounces forever on its roof or between it and a wall.
992
+ - **While rolling.** A ball resting on the floor is launched with `g = 0` and `vz = 0`, so its flight stays on the floor. Every N ticks (say 6 at 60 Hz) it is re-launched with its speed reduced by rolling friction, a share of it and a constant deceleration (`max(0, s · (1 − k · N / tickHz) − d · N / tickHz)`), and once it is slow it is launched at rest. A share alone decays exponentially, so a slow ball creeps for many seconds.
993
+
994
+ Each re-launch is one entity update of about 250 characters, which is far inside §17.4's caps. Each client draws the ball where the server has it now. A client's own car, though, is drawn ahead of the server by its input lead, and other cars are drawn `RENDER_DELAY_MS` behind. So a touch by your own car shows your car reaching the ball before the ball reacts: the server's car gets there one lead later, and the re-launch takes another one-way trip to arrive. Another car's touch shows the ball leaving before that car visibly arrives. Both are off by about the one-way latency. This is a known limit; predicting the ball on the client (replaying the module's bounces against the predicted car) is future work.
995
+
996
+ ### 17.10 Physics
997
+
998
+ A physics room simulates rigid bodies (crates, balls, doors on joints, a pushed car's props) on the server, with the Genex Physics core. The core's API, units, errors, events, hashes and limits are **the contract**: Genex Physics Layer 1 contract 1.0 (`native/genex-rust/crates/genex-physics/CONTRACT.md` in genex-desktop, tag `contract-1.0`; cited below as C§n). This section says only how an authority room uses it; it never restates or changes the contract. Here the server alone steps physics and every client draws the server's bodies with eased corrections (below); a predicting room (§17.11) adds client prediction on top, and a client that does not predict draws as this section says. A room without physics behaves exactly as §17.1–§17.9 say, and nothing in this section reaches its wire but the schema's reflection, which names the new fields (*State*). The machine-readable copy is `src/authority/physics.ts` (the key and kind rules, the default limits and the ceiling, `resolvePhysicsConfig`, the world description, the TickIn arrays, the quantization, the interest and drawing constants), `src/authority/physics-draw.ts` (the drawing), `AuthorityLimits`' `MAX_PHYS_*` and `MAX_WORLD_TEXT_CHARS` in `wire.ts`, `AuthorityPhysicsRoomState` in `src/authority/schema.ts` (*Interest*), and `AuthorityCaps`, `capsOf`, `AUTHORITY_PHYSICS_UNITS` and `AUTHORITY_ENCODER_BUFFER_BYTES` in `src/protocol.ts`.
999
+
1000
+ **The core.** The host runs the core outside the sandbox, as one of two builds of the same pinned version: the WebAssembly build (`@genex-ai/physics`) in the trusted host process, or a native process of the same build (`@genex-ai/physics-native`, a `genex-physics-room` process spoken to over two FIFOs in the worker's temp folder). Both are pinned to an exact version, and the host checks the bytes it runs against pinned sha256s (the WebAssembly module, and the native binary of each platform) when it loads them. **Each room's world runs on a core instance of its own**: a WebAssembly instance per room (the module compiled once, instantiated per room), or a native process per room, which ends with the room and is never reused (a worker keeps a few started ahead of need). `GENEX_PHYSICS_BACKEND` chooses: `auto` (the default) uses the native core when it passes its checks and its host check (*The host factor*), and the WebAssembly one otherwise, `native` uses the native core or none, `wasm` never starts a process. The native core is used only after it passed, at load: its binary's sha256, a process that starts and answers, a `buildId` equal to the WebAssembly build's compared without its `+<target>` suffix (C§2), and the parity scene (the load scene below plus 128 crates for 120 ticks) giving the WebAssembly core's `physicsHash` and `stateHash` after every tick. The first check that fails is the reason a worker falls back (`unpinned`, `sha`, `spawn`, `hello`, `parity`), logged once. A worker with no core (a check that fails on the WebAssembly build, or on the native one with `native` chosen) admits no physics room (503). On load the host runs one small scene through the core (a static world, each body type, a joint, a sensor, contacts, a kinematic pose, the queries, the export and both hashes) and frees it: the WebAssembly engine compiles a function on its first call, and that cost belongs to the load, not to the first room's meter; a core that cannot run that scene is no core. On either core a tick's steps 3 to 11 cross in one crossing (a native round trip, or one call into the WebAssembly instance; a second when the tick has queries or a body escaped), with the same results as the separate calls. Every number below that the contract owns (limits, key rules, error codes) is the contract's.
1001
+
1002
+ **Opt-in.** A module asks for physics in its config: `physics: true` (the same as `{}`), or an object with any of the keys below. `physics` absent, `false` or `null` means no physics.
1003
+ - `lengthUnit`: caller units per metre, (0, 1e6]. Default: 39.37 with `firstperson`, 100 with `vehicle`, otherwise 1.
1004
+ - `gravity`: units/s² along −z, [0, 1e7]. Default: the resolved preset's `gravity` parameter with `firstperson` or `vehicle`, otherwise 9.81 · `lengthUnit` (in float64; the core rounds it, C§4.1). The range holds for a given value; a default is never refused.
1005
+ - `iterations`: the solver hint, an integer in [1, 16], default 4 (C§4).
1006
+ - `limits`: any of C§11's limits, each an integer (or, for `worldExtent`, `maxSpeed` and `maxAngularSpeed`, a number) above 0. A module may lower a limit. A limit above the room's ceiling is clamped to it: the contract's default (for `worldExtent` and `maxSpeed`, the default at the room's `lengthUnit`), or on a tier that raises it, up to 4× that default (C§11). In release 1 every room's ceiling is the default. A ceiling gives `worldExtent` and `maxSpeed` per unit of `lengthUnit` (`PhysicsCeiling`), so it holds at any `lengthUnit`.
1007
+ - `subscribe`: at most 8 distinct physics kinds (*Keys and kinds*) whose changed body states the module receives each tick (*TickIn*); a kind listed twice is a bad config.
1008
+ - `predict`, `patchHz`, `remotePredict`, `remoteDecayMs`, `leadMs`: client prediction (§17.11 *Opt-in*).
1009
+ - `player`: `{ radius?, height?, events? }`. `radius` and `height` (each in (0, 1e6]) size a `topdown` room's preset bodies (*Preset bodies*) and are refused with any other preset; `events` (default false) opts the preset bodies' colliders into contact events (C§6.2).
1010
+
1011
+ Any other key, or a value outside its range, is a bad config (§17.4). The world the host creates is the contract's `WorldDesc` (C§4) with `contract` `"1.0"`, `dt` = 1/`tickHz`, `substeps` 3, 2, 1 and 1 at 20, 30, 60 and 120 Hz, `upAxis` `"z"`, `gravity` `[0, 0, −gravity]`, the `lengthUnit`, the clamped `limits` and `solver.iterations`.
1012
+
1013
+ **Admission and placement.** Whether a module uses physics is part of what admission knows about the published module (its validated config): the platform reads it from the stored `validate` report (its `config.physics` is not null) when the module goes live, whatever the publishing request says, and keeps it with the release. A physics room weighs `AUTHORITY_PHYSICS_UNITS` (6) times a room's weight up to 60 Hz and twice that at 120 Hz (`AUTHORITY_PHYSICS_TICK_WEIGHT`, `authorityRoomUnits`: it steps twice as often), so 6 or 12 units where a room without physics weighs 1, frozen at admission like any weight, and its admission carries its limit ceiling (in release 1, the default ceiling). A room without physics is admitted exactly as before. A worker refuses with 503 an admission whose ceiling is outside the range *Opt-in* gives. Placement chooses a worker before admission, so it places a room at the weight its game's last admission granted (a game's first room counts 1), and the worker checks the granted weight again once admitted, refusing with 503 a room that does not fit; a worker counts each room at its admitted weight from then on. The host's config text for `__genex_init` (§17.4) carries whether the room was admitted with physics and its limit ceiling. A module whose config asks for physics in a room admitted without it, or the reverse, is refused as a bad config at load. Physics is free within the default limits; a raised ceiling is the owner's paid tier.
1014
+
1015
+ **The resolved config.** The sandbox answers `config.physics` in `InitOut` with the request, defaults filled in (every limit included) and `limits` clamped to the ceiling the host passed (or `null` without physics; an absent `physics` reads as `null`); the module's `init` sees that object as `config.physics`, so its limits are the room's. The host validates it (a limit above the ceiling is a bad config), creates the world, imports the static world (below), applies `init`'s commands (*The tick*) and then holds the room's resolved config with `physics` = the world's `physicsHash()` (16 lowercase hex digits, C§2, C§10.2), or `''` without physics. The hash covers the build (without its target), the static world and the resolved description, and it is not §17.7's `params.world`. `AuthorityRoomState` appends `physics` string (that hash) and `lengthUnit` float32 (the world's, 0 without physics). A client that runs a copy of the core hands its SDK that world's hash (TypeScript `session.setPhysics(hash)`, the same call in each engine SDK); when the room's `physics` is not `''` and differs from it, the session reports `physics_mismatch` and the client never steps that copy from the room's state, as `world_mismatch` does for presets (§17.7). In this release no SDK steps bodies, so the report is the whole effect.
1016
+
1017
+ **Joining.** Authority join options gain `caps`, an array of at most 16 strings of at most 32 characters each; absent is `[]`. Every authority room refuses any other `caps` value with 400. A client that implements this section sends `caps: ['physics@1']`. A physics room refuses a join whose `caps` lacks `physics@1` with **426** (`physics@1 required`), so a client whose decoder predates the body fields is turned away instead of mis-decoding them; a room without physics ignores a well-formed `caps`. An SDK sends `physics@1` only once its drawing passes `physics-draw.json` and, for an engine SDK, its own headless probe on the pinned core (*Conformance*); the bench's feel room adds its bar for an engine once that engine's driver advertises `physics` (§17.8).
1018
+
1019
+ **The static world.** In a physics room the module's `world` export (§17.7's *World*, validated by its rules whatever the preset; none is the empty world) is the room's static geometry, imported into the core once with `addStaticWorld` (C§6.5). It never travels in `InitOut`, which a full world would overflow. After `__genex_init` succeeds, the host calls a third guest entry, `__genex_world()`, once, as a call of its own held to `init`'s CPU and memory limits; it returns the validated world as base64 (RFC 4648, standard alphabet, padded) of exactly the bytes §17.7's hash reads: the box and ramp counts, then every value as a little-endian binary32, −0 written as +0. The empty world (no export, or no boxes and no ramps) returns exactly `''` and stands for §17.7's empty world (hash 2615243109); the 8-byte text of two zero counts is refused. The text is at most 155,660 characters (`MAX_WORLD_TEXT_CHARS`: 4 · ⌈(8 + 24 · 4096 + 36 · 512) / 3⌉). Canonical base64 is text that decoding and encoding again gives back unchanged (no whitespace, the padding present, the unused bits of the last character zero). The host refuses it as a bad config, and the room does not open, when the call throws, runs past its limits or returns anything but a string, or the text is longer, not canonical base64, of a length that disagrees with its counts (8 + 24 · boxes + 36 · ramps bytes), a world §17.7 refuses, or, with `firstperson` or `vehicle`, a world whose hash (recomputed by the host from the bytes) is not the resolved `params.world`. The empty world is not imported. A core failure while the host builds the world (`World.create` or `addStaticWorld` refused, for example `limit-static` under a lowered `staticPieces`) is a bad config too; `internal` there refuses the room with 503 instead.
1020
+
1021
+ **Keys and kinds.** A body's key is its entity's id, and a joint's key is the module's. Entity ids are wider than contract keys (C§3), so an entity with a body must have an id that is a contract key: 1 to 64 bytes of `[A-Za-z0-9_.:\-]`, not `__proto__`, `constructor` or `prototype`, and not starting with `static:`. Host ids (`e<n>`) fit; ids a module picks may not. A physics kind (a `BodyDesc`'s `kind`, which is the body's own and independent of its entity's `kind` field) is 1 to 32 bytes of the same charset; `''` (or absent) means none, and the host passes it to the core absent. The prelude and then the host refuse any other body key or kind with `invalid-key` before the command reaches the core; every other key a command carries (colliders, joints, filters) is the core's to judge (C§3).
1022
+
1023
+ **Bodies and entities.** Every body the module spawns belongs to one entity, the *body entity*. A preset body (below) is not a module body, and its entity is not a body entity.
1024
+ - `spawn` names an entity that exists once the same tick's `up` and `rm` are applied (in `init`, `InitOut`'s); otherwise it is refused with `unknown-key`. A projectile (`t0 > 0`) or a seat's own entity (below) gets no module body (`wrong-body-type`), with or without preset bodies: a body would turn the entity a client predicts into one it draws from the server in the middle of the seat's play. A module that wants a seat to own a body entity spawns the body first and writes `owner` in a later tick.
1025
+ - An entity removed by `rm` loses its body, and every joint on it, before that tick's `phys`. That holds when the same output writes the id again in `up` (the module removed the entity and spawned one with the same id): the entity stays in the table as a new, ordinary entity, and is not a body entity for the checks below. `remove` drops the body and its descriptor and keeps the entity, frozen at its last state; it is an ordinary entity again, and the body fields it still carries mean nothing without a descriptor.
1026
+ - The host writes the body fields of a body entity: `x y z` and the fields under *State*. The module writes its other fields (`kind`, `owner`, `hp`, `flags`, `epoch`, `data`, `vx vy vz yaw` …). The prelude refuses a module write of a host field on a body entity (it throws), and `validateTickOut` refuses an output that carries one. For both checks a body entity is one whose body existed when the tick began (the host passes `validateTickOut` the room's body keys after the previous tick): an entity the module spawns a body on in this tick's `phys` is not one yet. Launching a projectile on a body entity throws in the prelude, and `validateTickOut` refuses a `t0 > 0` on one.
1027
+ - The module's own entity view never shows host-written fields: a body entity's `x y z` there are what the module last wrote. A module reads a body through `subscribe` and `ctx.physics.body(key)`. Before its hooks run, the prelude drops from that view the entities listed in `physEsc`, with any of their changes still waiting to go out.
1028
+ - A body entity takes no part in lag compensation, like a projectile (§17.5, §17.9): `seen` never lists it and `rewind` skips it. A module tests bodies with `ctx.physics` queries.
1029
+ - In a physics room §17.3's own entity is the first entity a seat owns that is neither a projectile nor a body entity, for the server's preset and for a client's prediction alike, so owning a body entity never makes it predicted.
1030
+ - A body entity's `epoch` is the module's. Bumping it moves nothing: a module that respawns a body bumps the `epoch` and issues `setPose` with `resetVelocity: true, wake: true` in the same tick (C§7), and clients snap on the bump (*Drawing*).
1031
+
1032
+ **Preset bodies.** Each seat's own entity (above) carries a preset body: a kinematic body the host manages, with no module commands, so any command naming one is refused with `wrong-body-type`. Its body key is the entity id and it has no physics kind. It has one collider, keyed `hull`: a box with `q` = identity (the hulls of §17.7 never turn), the contract's collider defaults and `events` = `player.events`. The half-extents are `(radius, radius, standHeight / 2)` for `firstperson` (ducking is not reflected), `(radius, radius, height / 2)` for `vehicle`, and `(player.radius, player.radius, player.height / 2)` for `topdown`. A `topdown` room without both sizes, and a room without a preset, has no preset bodies. Its centre is the entity's `x y z` plus half its height on z. The host spawns it on the first tick the entity is a seat's own entity, removes it when it stops being one (removed, or no longer the seat's own entity), and removes and spawns it again when its `epoch` changes, so a respawn never sweeps through props. Otherwise it gives it `setNextKinematicPose` each tick. Preset bodies count toward `limits.bodies` (C§11) and are spawned before the module's commands of the same tick (*The tick*). A preset body whose centre lies outside `worldExtent` has no body until it is back inside, and one whose id is not a contract key has none at all (the host logs it once). When the core refuses a preset-body command (`limit-bodies`, `limit-memory`, `out-of-range`), that seat has no body, the host tries the spawn again the next tick and logs the first refusal; it is never a strike and never reaches the module. A preset body the core lists as escaped loses only its body, never its entity, and is spawned again by the same rules. Preset bodies push dynamic bodies; dynamic bodies never block them, because presets still move only through the static world (§17.7), and the contacts of the two differ by shape (C§6.5).
1033
+
1034
+ **The module API.** `ctx.physics` is in every hook. In `init` its commands are allowed (they apply before tick 0) and its queries throw. In a room without physics `config.physics` is `null` and every member of `ctx.physics` throws a `TypeError`. Every value the contract types `Vec3` is `{ x, y, z }` and every `Quat` is `{ x, y, z, w }`, like `ctx.rewind` (a `Pose` is `{ p, q }` of those, a hull's `points` a list of them); every other array (`lockRotation`, a trimesh's `indices`, a heightfield's `heights`, a joint's `limits`) stays an array. The prelude converts to the contract's arrays, and back for what it hands the module.
1035
+ - Commands, one per C§7 verb, with the C§7 arguments: `spawn(desc)`, `remove(key)`, `addJoint(desc)`, `removeJoint(key)`, `impulse(key, j, at?)`, `setVelocity(key, linvel?, angvel?)`, `setPose(key, pose, options?)`, `setNextKinematicPose(key, pose)`, `areaPush(center, radius, strength, falloff, groups?)`, `setMotor(jointKey, motor)`, `wake(key)` and `sleep(key)`. Each returns its command number, or `'invalid-key'` when the prelude refuses it. `spawn` reads its `desc` once, as JSON text, and checks and records the key and kind of that copy; `setPose` takes only `resetVelocity` and `wake` from `options`. The module's commands are numbered 0, 1, 2 … across the room's life, from its first one in `init`; a command the prelude refused takes no number.
1036
+ - Queries, in C§13's terms: `ray(origin, dir, maxDist, filter?)`, `shapeCast(shape, pose, dir, maxDist, filter?)` and `overlap(shape, pose, filter?)`. Each returns its index in this tick's `physQ`; the answer is `ctx.physics.answers[index]` on the next tick, never this one. `ray` and `shapeCast` answer `null` or `{ body, collider, dist, point, normal }`, `overlap` answers `{ keys, more }` (`more` a boolean), and a refused query answers its C§14 code as a string. When the host did not apply a tick's output, its queries are dropped and `answers` is empty on the next tick.
1037
+ - What the last step did, as the tick's `TickIn` carried it: `refused` (`{ n, code }`, `n` the command number), `events` (C§8.1's objects, vectors as `{ x, y, z }`, a missing kind absent), `overflowed`, `escaped` (ids) and `body(key)`. `body(key)` is `{ p, q, v, w, sleeping }` (C§9.2's `NetBodyState`, vectors as objects): the latest state `physBodies` carried for that body, kept across ticks until the body is removed or escapes, and `undefined` for a body whose kind is not subscribed or that no `physBodies` has carried yet.
1038
+ - Past a cap of *TickOut*, a command or query throws a `RangeError` and adds nothing, as a launch past §17.9's does, so a burst never makes an output the host must refuse. The caps count every command waiting to go out, those of an output the host did not apply included.
1039
+
1040
+ **TickOut.** `TickOut` gains `phys` and `physQ`; `InitOut` gains `phys` only, and refuses a `physQ`:
1041
+ - `phys`: the commands in C§7.3's JSON form, in call order. At most 256 (`MAX_PHYS_COMMANDS`).
1042
+ - `physQ`: the queries in C§13's JSON form. At most the world's `queriesPerStep`.
1043
+
1044
+ `phys` and `physQ` together may hold at most 32 KiB of JSON (`MAX_PHYS_BYTES`), apart from the 32 KiB the rest of the output may hold, and the output text itself at most 64 KiB (`MAX_PHYSICS_OUTPUT_BYTES`), refused before it is parsed; the two keys' names count only toward the text, so both parts can never be full at once. Sizes are counted as §17.4 counts text: the two together are the length of `JSON.stringify` of the parsed `phys` plus that of the parsed `physQ`, and the rest is the length of `JSON.stringify` of the parsed output without those two keys. Past any of these caps, or with an element that is not a one-key object, `validateTickOut` refuses the whole output (a strike, §17.4): a count with `too_many_commands`, a size with `too_large`, a shape with `bad_shape`. A host field written on a body entity, or a `t0 > 0` on one, is `bad_shape`. In a room without physics the two keys are unknown keys, dropped like any other. Everything inside a command is the core's to judge: the host hands each command to the core as parsed from the text, never a guest object, and applies them with `apply(…, "each")` (C§7.1). A refused command changes nothing, the commands after it still apply, and the refusal comes back in `physRefused`. When the binding cannot take a batch at all and answers one failure instead of one result per command (a command nesting deeper than any contract shape is `bad-command` for the whole batch), the host applies each command of that batch alone, so the failure is that command's and the rest still apply. A refusal is never a strike. An output the host did not apply (a fault, a throw) has its `phys` sent again ahead of the next tick's, as its entity changes are (§17.4), and its `physQ` dropped. The host's own refusals of a command (`invalid-key`, and the `unknown-key` and `wrong-body-type` of *Bodies and entities*) come back in `physRefused` like the core's.
1045
+
1046
+ **TickIn.** `TickIn` gains these keys, each present only when non-empty (`physOverflow` only when `true`). Each describes the world after the previous tick's step. Numbers are the core's float32 values as JSON numbers, and vectors are caller-frame (z up). Tick 0's carries what applying `init`'s commands did: `physRefused` for them and `physBodies` for the subscribed bodies they spawned, and no other physics key.
1047
+ - `physRefused`: `[[index, code], …]`, an index into the `phys` array the host last applied (the previous tick's as sent, any commands sent again included) and a C§14 code. The prelude turns each index into the command's number.
1048
+ - `physEv`: the step's events in C§8.1's order, each a fixed array `[kindCode, a, b, ca, cb, ka, kb, impulse, px, py, pz, nx, ny, nz]`. `kindCode` is 0 `contactBegin`, 1 `contactEnd`, 2 `triggerEnter` and 3 `triggerExit`. A trigger puts `sensor, other, sc, oc, ks, ko` in the places of `a, b, ca, cb, ka, kb`. A missing kind is `''`. Every event but `contactBegin` ends after `kb`. There are at most `eventsPerStep` events.
1049
+ - `physOverflow`: `true` when the step dropped events (C§8.1).
1050
+ - `physAns`: one answer per query in the previous tick's `physQ`, in order. `ray` and `shapeCast` give `null` (no hit) or `[body, collider, dist, px, py, pz, nx, ny, nz]`. `overlap` gives `[more, key, …]`: at most 64 keys in key order, with `more` 1 when the core found more, else 0. A refused query gives its C§14 code as a string (`limit-queries` past the quota).
1051
+ - `physBodies`: for the subscribed kinds, every body changed since the previous export (C§9.1's "changed": by the previous tick's commands or its step), in key order, each `[key, px, py, pz, qx, qy, qz, qw, vx, vy, vz, wx, wy, wz, sleeping]` (`sleeping` 0 or 1).
1052
+ - `physEsc`: the ids the step's escaped module bodies belonged to (C§8.2), in key order. The host has removed those entities.
1053
+
1054
+ **At load.** After the world is built and the static world imported, the host applies `InitOut`'s `phys` like step 5 below, with no step, and writes each body they spawned into its entity, so the room's first state carries them. That work is not metered.
1055
+
1056
+ **The tick.** Tick `t` of a physics room runs in this order:
1057
+ 1. The guest call (§17.4): joins, then each input (the preset's step, then `onInput`), leaves, `step`.
1058
+ 2. The host validates the output. A refused output is dropped whole (a strike), and the tick goes on from step 4 with no module commands.
1059
+ 3. `up`, `rm` and `ev` are applied, and the bodies of removed entities are removed.
1060
+ 4. Preset bodies are spawned, removed, re-spawned or given their next kinematic pose, from the poses this tick's output wrote (after a refused output, none: they hold their pose).
1061
+ 5. `phys` is applied, in order (*TickOut*).
1062
+ 6. `step()`, once (its sub-steps are the core's).
1063
+ 7. Escaped module bodies are removed, with their entities and descriptors; an escaped preset body loses only its body (*Preset bodies*).
1064
+ 8. When the step ended with `memoryExceeded`, the room ends (*Limits and failures*).
1065
+ 9. Every module body changed since the previous tick's step 9 (by step 5's commands or the step) is written into its entity (*State*).
1066
+ 10. `physQ` is answered, against the world as step 6 and 7 left it. Those queries count toward the next step's quota (C§11).
1067
+ 11. Tick `t + 1`'s `TickIn` physics keys are assembled.
1068
+
1069
+ A command issued anywhere in tick `t` therefore takes effect in that tick's step. Its result goes out in the first patch after tick `t`, and the module sees its events, refusals, answers and states in tick `t + 1`.
1070
+
1071
+ **State.** `AuthorityEntity` appends `qx` `qy` `qz` `qw` (the orientation, each quantized in [−1, 1]), `lvx` `lvy` `lvz` (linear velocity divided by `lengthUnit`, so metres per second, quantized in [−64, 64]), `avx` `avy` `avz` (angular velocity, rad/s, quantized in [−64, 64]) and `sleep` boolean. These fields, and `AuthorityRoomState`'s `physics`, `lengthUnit` and `bodies` below, have no default (an exception to §17.3's defaulted fields): Colyseus encodes a default like any other value, in every full state and every new entity, so a defaulted field would reach every room. They are on the wire only once the host writes them, and a decoder reads one it never received as 0, `false`, `''` or an empty map. Every engine's generated schema declares them, so the reflection handshake of a room without physics names them and changes nothing else. Each quantized field is Colyseus schema 5's `t.quantized({ min, max, bits: 16 })`, clamp mode, a uint16 on the wire: a value outside its range is clamped, NaN is written as `min`, a value `v` is sent as `q = floor((v − min) / (max − min) · 65534 + 0.5)`, and the decoded value is `min + (q / 65534) · (max − min)`, in float64, so 0 is exact. A client normalizes the decoded quaternion before it uses it. Position stays `x y z`, plain float32 (the core's value). The host writes these fields only on body entities, and only in physics rooms, so a room without physics never sends them. The other fields of §17.3 keep their meaning; the host never writes `vx vy vz yaw` on a body entity, and a client does not use them to draw one. Entity floats elsewhere stay unquantized (§17.3).
1072
+
1073
+ `AuthorityRoomState` appends `bodies` map<string, AuthorityBody>, keyed by body key, written once when a body is spawned and removed with it: `type` string (`dynamic`, `fixed` or `kinematic`) · `kind` string (`''` for none) · `colliders` string (JSON text of the spawn's `colliders` as the core accepted them, in C§6.2's form: only C§6.2's fields, in its order, each only when it has C§6.2's type, so any other key the core accepted is left out; vectors as arrays and every number as its float32, except that a hull's shape is `{"kind":"hull","points":<count>}`, a trimesh's `{"kind":"trimesh","triangles":<count>}` and a heightfield's keeps `rows`, `cols` and `size` and leaves out `heights`; so at most 8 colliders of about 120 characters). Preset bodies have no descriptor. A body exists for a client exactly while its descriptor does; a body whose entity the client has never received is not drawn.
1074
+
1075
+ **Interest.** A physics room sends each client a view of its own (Colyseus 0.18 `StateView`). Every entity that is not a body entity (seats' own entities included), and every descriptor, is in every view, as in any room. Body entities are chosen each patch per client:
1076
+ - The first 100 (`PHYSICS_FULL_RATE_BODIES`) in this order are in the view: awake bodies before sleeping ones, then by the distance from the body to the client's own entity (§17.3's own entity as this section narrows it: the first the client's seat owns that is neither a projectile nor a body entity), `√(dx² + dy² + dz²)` computed in float64 from the float32 fields, to the origin when it has none, then by key. Key order here and below is code-unit order.
1077
+ - Up to 25 more (`PHYSICS_ROTATING_BODIES`) of the others are added in key order, starting after the last key a patch added this way for that client (from the first key when none has yet; a patch that adds none keeps the earlier one) and wrapping around once. A body the client already holds asleep, unchanged since it last received it, is skipped and not counted. Unchanged means every field of its entity is what the client last received; an id removed and spawned again is another body, which the client does not hold. A client holds only what reached it: until it has acknowledged its join (Colyseus sends a joining client nothing, then its whole state through its view as the view is then), it holds nothing, so a body that passed through its view during the handshake is still sent.
1078
+ - The server keeps `entities` view-filtered only in a physics room: its state is `AuthorityPhysicsRoomState`, the same fields in the same order under the same name, so a decoder cannot tell the two apart, and a client's view is brought to this selection when it joins (before its first state) and before every patch. A room without physics has no views and sends exactly what it sent before.
1079
+ - A body entity that leaves a client's view is not removed for that client: its descriptor stays, and the client holds the body at its last state until it comes back. Colyseus deletes the entity from that client's copy of the state, so the client judges each patch whole: an entity missing while its descriptor exists is out of view, and an entity and descriptor deleted together are gone.
1080
+
1081
+ Every authority worker sets Colyseus's `Encoder.BUFFER_SIZE` to 1 MiB (`AUTHORITY_ENCODER_BUFFER_BYTES`) before it creates any room, because the setting is process-wide and a physics room's full state outgrows the 16 KiB default (500 bodies are about 35 KB; a predicting room's, §17.11, with a canonical descriptor on every body and up to 32 shared shapes, measured 672 KB at 332 bodies and 32 near-full shapes). Colyseus grows a buffer that is too small, with a warning, and re-runs the full sync of the joiner's view, which sent the C# and C-core decoders a state they could not decode; so no room's full state may reach it. Patches stay every 50 ms (§17.6).
1082
+
1083
+ **Drawing.** A client draws a body entity at the server's present, never `RENDER_DELAY_MS` behind, and never steps physics. A body state that arrived in a patch whose `tick` was `t` is the body at tick `t + 1`. It arrives at the room-clock instant the client decodes that patch (its estimate of §16.1's `sNow` then, the clock `serverTick` reads). A patch that leaves every body field and the `epoch` of an entity as they were carries no new state for it. Quaternions are `[x, y, z, w]` and `⊗` is the Hamilton product; a client normalizes each decoded orientation before it uses it, and one of length 0 is the identity. Arithmetic is float64; trigonometry is the platform's, which is why ports agree within the vectors' tolerance rather than bit for bit.
1084
+ - At render time the client extrapolates from the body's latest state by `Δ = clamp((serverTick(now) − (t + 1)) / tickHz, −1 / tickHz, 0.25)` seconds (`BODY_EXTRAPOLATE_MAX_MS` 250, `BODY_EXTRAPOLATE_BACK_TICKS` 1; `serverTick` is §17.9's). Tick `t` is stepped at its own instant and its state is the body at `t + 1`, so the patch after a step lands up to a tick before the instant its state is for: until then the state is moved back by its velocity, a tick at most, and the body is drawn at the present (held at `Δ` 0, it was drawn ahead and each newer state eased in from its line, a tick's travel off by the patch's phase). Position moves by the velocity: `p + (lv · lengthUnit) · Δ`. Orientation turns by the angular velocity, which is in world axes: `q' = normalize(r ⊗ q)`, `r` the rotation by `|av| · Δ` about `av / |av|` (identity when `av` is 0). `Δ` is 0 while `sleep` is set.
1085
+ - When a newer state arrives, the offset is the pose the client would draw at that instant from the older state (its extrapolation plus its decaying offset) less the pose the newer state gives at the same instant: `dp = p_old − p_new` for position, and `dq = q_old ⊗ q_new⁻¹` for orientation, negated when its `w` is below 0 (the short way: `q` and `−q` are one orientation). The client then draws position `p + dp · k` and orientation `slerp(identity, dq, k) ⊗ q`, where `p` and `q` are the newer state's extrapolation, with `k = e^(−age / 100 ms)` (`BODY_CORRECTION_TAU_MS`) and `age` the time since that arrival (0 before it). `slerp(identity, dq, k)` is the rotation by `k · θ` about `dq`'s axis, `θ = 2 · atan2(|(dq.x, dq.y, dq.z)|, dq.w)`, and the identity when that vector part is 0. An `epoch` change, `|dp|` over one metre (`lengthUnit` units), or a body that just got its descriptor snaps instead (no offset).
1086
+ - A body out of the client's view (*Interest*) is drawn from its last state by the same rules, so it comes to rest 250 ms on; when it comes back, its state is a newer state like any other.
1087
+ - The position moved is the body's `x y z` (its origin), by `lv`, the core's linear velocity. The contract does not say which point of the body that velocity belongs to (C§9.1), and a Rapier-backed core gives it at the centre of mass: a body whose colliders are offset from its origin, turning, is then drawn off by up to its spin times that offset times `Δ` until the next state corrects it. A module that wants such a body drawn well spawns it with its origin at the centre of mass (a door's at the middle of its leaf, swung about the hinge by its pose or a joint).
1088
+ - `physics-draw.json`, each engine SDK's own probe and, once its driver advertises `physics`, the bench's feel room hold each engine to these rules (*Conformance*).
1089
+
1090
+ **Limits and failures.**
1091
+ - **The physics meter.** The host measures its own thread CPU across steps 3 to 11 of each tick, plus, on a native core, the CPU the room's process spent on that tick (which the process measures itself, per round trip). A breach is a tick over 8 ms per 1/60 s of simulated time (8, 16 and 24 ms at 60, 30 and 20 Hz), or a mean over the last 300 ticks above 1 ms per 1/60 s (1, 2 and 3 ms a tick). A 120 Hz tick gets a 60 Hz tick's bounds, 8 ms and a 1 ms mean, not half of them: an owner-set limit (the owner's per-tick rule, 2026-11-03), so a 120 Hz room has twice the physics CPU per simulated second, as it weighs twice in placement (*Admission and placement*; `authorityPhysicsBudgetSixtieths` in `@genex-ai/multiplayer/protocol`: per simulated second, a room's budget is always its `AUTHORITY_PHYSICS_TICK_WEIGHT` times a 60 Hz room's). The guest's own CPU limits are per tick at every rate and never scale with `tickHz`. The mean slides: it is judged after every tick once 300 ticks have been measured since the room started or since the last breach, whichever is later, and a breach empties the window. A tick that breaches the per-tick bound is one breach, and the mean is not judged on it. Each breach is a strike, on the same count as the module's faults (§17.4). The third strike ends the room with reason `physics_budget` (4501). Physics CPU is not the guest's: it never counts toward the guest's budget, and it is reported apart in Studio's report line and the room's telemetry.
1092
+ - **The host factor.** The meter's numbers are reference milliseconds: what the reference host (a DigitalOcean s5, Zen 5) measures. A room that passes the meter on one server passes it on every other, whatever its CPU. A worker measures itself once its core has loaded and before its first physics room. One reading is the parity scene (*The core*) on its own backend, a warm-up run and then three runs, their median physics CPU per tick over the reference host's. The host check takes up to five readings, 250 ms apart plus up to 250 ms at random (so workers booted together drift out of step), stopping once two are under the cap, and keeps the best (the least CPU per tick): contention from a busy sibling hyperthread or another process only ever adds CPU, so the least reading is the nearest to the host's own speed, the one the reference figure and the cap describe. That ratio, at least 1, is its host factor. Every bound above (per tick, mean) and a native round trip's deadline are multiplied by it, frozen when the room opens. A backend whose factor passes 3.5, or one of whose readings is not a positive number (the meter runs on the same clock), does not pass. Under `auto` a native core that does not pass gives way to the WebAssembly core it was checked against, measured the same way, and the native processes are stopped. A worker on which no backend passes runs no physics rooms: a physics admission there is refused 503, so the room is placed elsewhere, and the first physics room 60 s later measures again; a backend that passed is kept until the worker restarts. The decision is a typed status (`passed`, `fell_back`, `too_slow`, `unmeasured`) with every backend's readings, on the worker's load row (`physicsHostCheck`). The demo servers (car-ball, Netcode Park, the physics arena) run the same check through the same code. A physics room takes its admitted units times the host factor of the worker's capacity; it is billed its admitted units, the same on every host. The room's telemetry carries the factor (`physicsHostFactor`); a local tool (authority-dev) measures its own machine the same way.
1093
+ - **Memory.** Commands that would pass `memoryBytes` are refused by the core (`limit-memory`). A step that ends over it (`memoryExceeded`, C§11.1) ends the room at once with `module_memory` (4501).
1094
+ - **`internal`.** When a room's core answers `internal` (C§11.3: a WebAssembly trap; a native process that crashed, was killed, broke the frame protocol, or hung or ran away past its deadline, below), **that room alone** ends with reason `physics_internal`; no other room, and not the worker, notices. That is never a strike nor the module's fault, and it closes with **4500**, terminal, so no client rejoins into the same failure. A host error inside steps 3 to 11 (a throw out of the host's physics code, or out of a core call, which C§1.5 says never throws) ends that room the same way, with reason `physics_internal`.
1095
+ - **A native round trip's deadline.** A native process is hung when it makes no CPU progress, never because wall time passed: a busy host deschedules a healthy process for longer than any deadline worth having, and a stopped process makes no progress either, so the time a call takes says nothing about the process. A round trip's deadline is twice the meter's per-tick bound (16, 32 and 48 ms at 60, 30 and 20 Hz, 16 ms at 120 Hz, times the host factor); a call past it is judged by its process's CPU and run state, read off the room's thread (`physics/hang-watch.ts`: /proc on Linux, `ps` on macOS). A process that kept computing a further deadline of CPU past it (a runaway: a tick that never ends cannot reach the meter) is killed and the room ends `physics_internal`, as is one that stayed stopped with no CPU progress through four deadlines of time the host ran the watch on time (hung). A process that is runnable and gets no CPU is the host's doing, one asleep is waiting on the host (the room's thread starved before it wrote its frame or read the reply), and a reading that fails is no evidence: a call judged neither by `PHYSICS_WALL_BACKSTOP_MS` (1.5 s) after it started is cut off (the process killed, so the worker is not held) and the room ends **`host_stall`**, never `physics_internal`: no strike, `host_fault` for metering, and **4001** (server shutdown, as a drain and as a module call the host stalled): the server's trouble, not the game's, so a client may join again, where 4500 tells it not to. A late call that completes is kept, its process's CPU on the meter. The package's own deadline and the process's (2 s, setup frames included) stay behind the watch as the last resort; a call they end past the backstop is a host stall too.
1096
+ - **Escapes.** An escaped module body (C§8.2) is removed with its entity and descriptor in step 7, and listed in `physEsc`. It is not a strike.
1097
+
1098
+ **Replay and validation.** The replay (`replay.ts`) and `genex-authority-dev validate` drive the same physics as a live room. After each tick (and after load), a physics room's snapshot is `[t, rows, events, stateHash]`, `stateHash` being the core's `stateHash()` as 16 hex digits (C§10.1), and each body entity's row has its physics fields appended (`qx qy qz qw lvx lvy lvz avx avy avz sleep`, present only on body entities, each as the state holds it: a quantized field holds its decoded value). A room without physics hashes exactly as before. A replay records the core's `buildId` and the room's `physics` hash. The same module, seed, input log and `buildId` give the same hash on both sandbox runtimes, because physics runs in the host. A replay does not meter physics: CPU time is not deterministic, so the meter's strikes are a live room's alone. A replay under another `buildId`, compared without its `+<target>` suffix (C§2), is reported as such, never compared. `validate` runs a physics module on the core a worker would load (the same pin), admitted at the release-1 ceiling; it learns that the module asks for physics as a server would, from the load without physics being refused at `$.config.physics`, and loads it again with physics. With no core it fails `physics_unavailable`; two replays under different builds fail `physics_build`, uncompared. Its report and its `VALIDATE` line carry, and it prints, the `buildId`, the `physics` hash and the last `stateHash` (`physics`, null for a module without physics). `validate --physics-backend both` runs the fuzz and both replays on the WebAssembly core, then replays the same record on the native core of the same build: every `stateHash`, after load and after each tick, must equal the first replay's. The first that differs fails `physics_backend_parity`, naming the tick and both hashes; the report's `physicsParity` holds the two backends, the hashes compared and that tick (null when all match).
1099
+
1100
+ **Conformance.** The core's own vectors (C§16) are vendored, unmodified, into `packages/netcode-bench/conformance/authority/physics/`, beside a `PROVENANCE.json` that pins the contract version, the core's version and `buildId`, the source repository and commit, and each file's sha256. A test checks every file against its pin and runs each vector through the host's core binding with the C§16 runner. Moving the core's pin moves its vectors and `PROVENANCE.json` in the same change. The host's own rules here (the static-world text, the TickIn arrays, the quantization, the tick order, interest and the meter) are held by `@genex-ai/authority-host`'s tests. An engine SDK's drawing is held by the netcode bench's `physics` capability, on §17.8's terms. First, `conformance/authority/physics-draw.json` holds cases of a `tickHz`, a `lengthUnit` and a `tickOrigin`, and one body's timeline in order: patches (each its `tick`, its arrival time on the room clock, the body's fields as quantized integers, float32 position, `sleep` and `epoch`, and `spawn` on the first state after its descriptor arrived) and render instants, each with the expected drawn pose after decoding, extrapolation and correction; `physics-draw.reference.mjs` beside it is written from this section's text and prints it byte for byte. A port passes within the file's `tolerance` (1e-4). Then the bench's physics feel room (a pushed crate, a falling pile, swinging doors and a fast ball with continuous collision, patches every 50 ms) judges a client's drawing live against the server's own bodies at network profiles with latency, jitter and stalls: how far it is drawn from the server's present, how far it pops between two frames beyond what the server's body did, and that a body asleep stays still. The TypeScript SDK passes both. The Godot, Unity, Unreal and Rust SDKs pass the vectors and a headless probe of their own (each SDK's README names it), which joins a physics room on the pinned core and judges what the SDK draws against the server's bodies every tick; the feel room judges an engine once its bench driver advertises `physics` (DRIVER.md). An SDK sends `physics@1` (*Joining*) only once its drawing passes the vectors and, for an engine SDK, its own probe.
1101
+
1102
+ ### 17.11 Physics prediction
1103
+
1104
+ A predicting physics room lets a client step the bodies near it in a world of its own and draw them at its present, not 100 ms or more behind. It needs the core's contract 1.1 draft calls (C§20: `canonicalize({ quantize })`, `importWire`, the sleep timer `rest`, carried contact pairs, canonical descriptors). The host's machine-readable copy is `src/authority/physics.ts` (`PHYSICS_PATCH_HZ`, `keyframeTicks`, `PhysicsPredictRange`, `PHYSICS_PREDICT_MAX_*`, `PHYSICS_CONTESTED_METRES`, the stacking hook `PHYSICS_PREDICT_MIN_ITERATIONS`), `src/authority/physics-keyframe.ts` (`physDesc`, the wire row, the clamp, shapes by reference, islands, `remoteInputWeight`, `PredictSmoothing`) the client's `src/authority/physics-predict.ts` (`PhysicsPredictor`, body rules, the budgets and fallbacks) `src/authority/seat-contacts.ts` (`SeatContacts`, `separateSeats`) and `src/authority/seat-predict.ts` (`SeatWorld`, the `predicted` drawing's seat prediction). Everything in §17.10 holds unless this section says otherwise.
1105
+
1106
+ **Opt-in.** `config.physics` takes five more keys; any other value is a bad config:
1107
+ - `predict`: boolean, default `true`. A new physics room predicts unless its module says `false`.
1108
+ - `patchHz`: keyframes per second, one of 10, 15, 20, 30 and 60; default 20. The room uses the largest of those at or below it that divides `tickHz` (20 at 30 Hz is 15). `K = tickHz / patchHz`: K 1 at 20 Hz, 2 at 30 Hz (15), 3 at 60 Hz, 6 at 120 Hz; a car-ball room asks for 60 (K 2 at 120 Hz).
1109
+ - `remotePredict`: `'extrapolate'` (default), `'hold'` or `'inputDecay'`: how a client moves other players' entities and preset bodies between keyframes (*The client world*, *Drawing other seats*). `hold` is the Rocket League model: the room preset's step under the seat's last input, held.
1110
+ - `remoteDecayMs`: `inputDecay`'s decay, in [0, 1000] ms, default 50.
1111
+ - `leadMs`: the lead cap, in [50, 1000] ms, default 250 (*Budgets*).
1112
+
1113
+ `tickHz` 120 is legal for a physics room only (a room without physics asking for it is a bad config at `$.config.tickHz`): one inner step per tick, and the meter's bounds are a 60 Hz tick's (8 ms a tick, a 1 ms mean; *Limits and failures*). A room predicts when its module asks, the worker's operator allows it (`AUTHORITY_PHYSICS_PREDICT`, default `on`; `off` runs every room as §17.10 says) and the core has C§20's calls. In a predicting room the solver runs at least `PHYSICS_PREDICT_MIN_ITERATIONS` iterations, the hook the engine lane's stacking rule fills (unset: the module's `iterations`; R2-0b measured 10-high stacks falling at 4 because canonicalize drops warm starts), and `genex-authority-dev validate` reports a `warnings` slot for that rule.
1114
+
1115
+ **Joining.** A client that predicts sends `caps: ['physics@1', 'physics@2']`. A room serves the same state to both caps; `physics@2` only says what the client does with it. A client that reconciles repeats (*Input pacing*) also sends `repeat@1`; a room whose input repeat setting is off ignores it (*Input pacing*, *The setting*). A `physics@1` client in a predicting room draws as §17.10 says, held to `physics-draw.json` unchanged.
1116
+
1117
+ **The tick.** A predicting room's tick is §17.10's with one step after step 8:
1118
+ - 8a. Every dynamic module body whose linear speed is over `64 · lengthUnit` m/s, or angular speed over 64 rad/s, is brought to that speed by magnitude (`setVelocity`, the other velocity as it is), every tick: the wire's ranges, so quantizing never bends a velocity. Then, on every tick `t ≡ 0 (mod K)`, the host calls `canonicalize({ quantize: true })`: the world becomes exactly the state its wire rows say. That call is counted in the physics meter.
1119
+
1120
+ Step 9 then writes entities only on a keyframe tick (and at load), so the state always holds the last keyframe. A predicting room's tick runs as separate core calls (step 8a sits between the step and the export).
1121
+
1122
+ **Input pacing.** A predicting room feeds each seat's buffered inputs to the module one a tick (§17.3's "one at a time, in sequence order", at most one per tick), not as they arrive: a page drawing 60 frames a second sends two inputs a frame to a 120 Hz room, and fed as they came a car moved two steps on one tick and none on the next (every other client drew it stuttering) while every prediction that puts input `seq` at tick `keyframe + (seq − ack)` was off by the bunching. The buffer sizes itself to the link's jitter: a tick with nothing buffered is *starved*, and leaves the next inputs a tick longer in it; a seat whose buffer never fell below 2 inputs for a whole second (`PACE_WINDOW_S`) is fed one extra input (a tick of latency nobody needed), unless it starved within the last 10 s (`PACE_KEEP_S`: the cushion the starve showed the jitter needs is kept); a buffer holding more than 50 ms of inputs (`PACE_MAX_DEPTH_S`; a stall's backlog) is fed down to that at once, within the per-tick cap (`packages/authority-host/src/input-pacing.ts`). A room that does not predict feeds what arrived, as before. Inputs travel on a reliable, ordered socket, so a client never resends one, and its pacing follows the server's clock (§16), so no clock scaling is needed beyond the extra input.
1123
+
1124
+ **The input's tick.** A client that joins with the cap `tick@1` writes into each input's `tick` (a number in [0, 65536), the last field of `AuthorityInput`, so a client that omits it encodes as before) the server tick its view and aim were set for, modulo 65536: the tick its frame's first input is predicted at, `(state.tick − steppedAck) + seq` by the map of its inputs as its last frame ended (the map its view was turned by and its drawing timed by), the same on every input of the frame, which all carry one view (TypeScript `input-tick.ts` `inputTickField`, `session.ts`). Pacing still decides when an input is applied (a tick that steps two of a seat's inputs or none re-times every input in flight); the server reads, at the tick `T` it applies an input, the *lag* `T − carried`, the carried tick being the one congruent to the field nearest `T` (`inputTickLag`): a field that is not an integer in [0, 65536), or a lag over the slack (0.5 s of ticks, at least 1: `INPUT_TICK_SLACK_S`), reads as 0, as does every input of a seat without the cap, so a late, early, repeated or hostile field changes nothing beyond the slack (`input-tick.json` holds the table). The host hands it to the guest as the input's `lt` (absent: 0), a repeat's as the lag of the input it repeats, and lists in TickIn `vt` the cap's seats that step none of their inputs this tick. In a room with lifts: a `firstperson` seat whose base at the tick's start is a spinning lift has the view of an input with lag `lt` carried by the lift's turn over it, `yaw + δ·lt` as float32 (`viewOnLift`; the module hears that view too), and a `vt` seat's view turns by one tick's `δ`; so the server's view stays on the spinner tick for tick across every re-timing (without it, every input in flight stood a tick of turn off it for a round trip: LG2). Lag compensation (§17.5) moves a target on a lift into that lift's frame at the tick the input was meant for (`T + 1 − lt`). Engines that do not send the cap keep the old behaviour.
1125
+
1126
+ A starved tick of a seat whose client sent `repeat@1`, in a room with a preset, whose latest `INPUT_REPEAT_STEADY` (4) real inputs in a row are alike (a steady stick), *repeats* (Rocket League's server buffer): the preset steps the seat's last real input again, so the seat moves on as every other client predicts it (`remotePredict: 'hold'`) instead of standing still for the tick. Only held state is repeated, and the distinction is typed in the sandbox wire (`TickInput.use`, `InputUse` in `src/authority/wire.ts`): an input's move axes, aim and buttons are held state for the preset (it finds a press edge itself, from `mstate`'s jumpHeld, so a held jump never jumps twice), while the module's `onInput` hears each real input exactly once and never a repeat (a FIRE, a use, a sequenced command is consumed once, however many ticks are repeated); the module's `step` runs as on any tick. A repeat stands in for the seat's next input. Each repeat is owed until a later tick pays it back: a tick that consumes more than one input (one more is consumed whenever a repeat is owed and the buffer holds more than the pacer's feed: the late input with one behind it; also the pacer's extra input and a stall's catch-up) *absorbs* each input but the tick's last, oldest first, while a repeat is owed and its buttons equal those the preset last stepped: `onInput` hears it, the preset does not step it again. So no press edge is made or lost, a seat is stepped once a tick through a starve, and input k is stepped at slot k again within a tick or two: the room and the client agree on which input stepped which tick. (Owing repeats until the pacer's own extra feed instead applied input k at slot k + inRep for seconds, which mispredicted a changing stick: car-ball's own corrections over 20 u went from 24 to 84 a run at rtt-200; evidence folder, input-repeat/.)
1127
+
1128
+ The cushion a starve shows the buffer needs is built by the client, not owed by the room (Rocket League's upstream throttle): a `repeat@1` seat's keyframe also carries `inStarve` (its starve runs since its first input; starved ticks in a row count once) and `inBuf` (its inputs left buffered after the tick). A client that sees a new starve run with at most `THROTTLE_LOW_INPUTS` (1) buffered runs its input clock `THROTTLE_DILATION` (3%; RL's time scaling is about 1%) fast until it has sent one extra input (half way through), at most one owed at a time, and draws other seats on `THROTTLE_SEAT_LEAD` (0.5) of that clock's lead (all of it drew them further ahead and eased 5% more correction into them; none cost the ball), so the drawing never jumps when the extra input goes out; a deeper buffer had the cushion, and its starve was a stall's (`input-repeat.ts`). One starve run is repeated for at most `INPUT_REPEAT_RUN_MS` (33 ms: 4 ticks at 120 Hz, 2 at 60, 1 at 30), then the seat holds for the rest of it: a stick steady before a starve is less likely to stay so the longer it lasts (on car-ball under loss, 12 repeats a run doubled the own corrections over 20 u; 4 kept them at the base's). Repeats are bounded: a seat that owes `INPUT_REPEAT_MAX_MS` (100 ms, `AuthorityTiming`; 12 ticks at 120 Hz, 6 at 60, 3 at 30) holds on a further starved tick, not stepped, so a vanished player stops instead of driving into a wall. Rocket League's client resends its last 10 frames of input in every packet (83 ms at 120 Hz), the gap its server's repeat bridges; the cap is that rounded up to whole ticks. A seat without the cap, before its first input, or whose stick is on the move holds on a starved tick as before: on car-ball, 0-7% of repeats after 1-3 alike inputs stood in exactly for the late input, 66-100% after 4 or more. Repeats already owed go on through their run. A reconnect's new socket owes nothing.
1129
+
1130
+ **The setting.** Repeats, absorbs, the keyframe's `inRep`, `inStarve` and `inBuf`, the client's stepped-through ack and its throttle are all one setting of the room: `config.inputs` (TypeScript `packages/multiplayer/src/authority/input-config.ts`), an object with optional `repeat` (`on` or `off`) and `preset` (a genre preset of *Contact policy*: `shooter`, `platformer`, `vehicle-arena`, `physics-sandbox` or `party`). The room's setting is `repeat` as given, else the table of `preset`, else of the declared contact policy's `preset` (`config.contacts`), else `on`; a game names its genre, never its own name. The genre table: `vehicle-arena` off (on car-ball 3v3 at a clean 200 ms round trip the repeats and the throttle cost other players' cars, remote corrections p95 and errAtLead p95 worse at p ≤ .013, while a client without them stayed within the base); `shooter` on (Mechanism Hall and Blast Yard under loss draw your own character with fewer pops and hitches, stack and rocket better); `platformer`, `party` and `physics-sandbox` on, as measured for characters (they seat characters as a shooter does and have no measurement of their own). Any other key, a `preset` that is not one of those, a `repeat` that is not `on` or `off`, or a value that is not an object is a bad config refused by its path (`$.config.inputs.repeat`). The host publishes the resolved setting in `AuthorityRoomState.preset`'s JSON text as its `inputs` key (`{ "repeat": "off" }`; absent when the module declares neither inputs nor a contact policy, and from an older host: on). In a room whose setting is off the room treats every seat as one that did not send `repeat@1`: a starved tick holds, nothing is owed or absorbed, and no `inRep`, `inStarve` or `inBuf` is written (the pacer's feed, its window and its keep are unchanged); a client reads the setting from the preset text (`off` only when the text is an object whose `inputs` is an object whose `repeat` is the string `off`; anything else, a hostile value included, reads on) and then reads the plain ack (`steppedThrough` returns `ack`) and takes no throttle reading, whatever the state carries. A client still sends `repeat@1` at its join, before it can read the room's setting; an off room ignores it, so on the wire an off room is exactly a room of clients that never sent the cap. In a room whose setting is on everything above holds unchanged. Held to `input-repeat.json` (its `settings` section, and every case with `repeat: 'off'`).
1131
+
1132
+ The ack stays exactly the real inputs consumed (absorbed ones included). `AuthorityPlayer.inRep` (number, written at every keyframe for a seat that sent `repeat@1`, absent otherwise) is the repeats it owes, so the state holds the seat's entity as stepped through input `ack + inRep`: the preset has taken `ack + inRep` steps, each repeat counted as the input it stood in for. A client that sent `repeat@1` compares its prediction with the state at input `ack + inRep` once it has sent that input; while `ack + inRep > sent` (a hitch drained the seat's buffer and the room repeated for inputs the client has not sent yet) no prediction of its own is at the state's input, so its reconcile waits at the input it compared at last, never past `sent` (the hitch clause of *The reconciler's ack* below; compared at `sent` instead, a held stick was pulled forward by the repeats and back by as many once the next inputs went out). A replay from the state replays the inputs after `min(ack + inRep, sent)` (`steppedThrough`; a value that is not a non-negative integer reads 0), and times input `seq` at tick `tick + (seq − (ack + inRep))` (seat rules, seat contacts, the client world); only a projectile's settling reads the true ack, since an input a repeat stood in for has not reached the module yet. Under a held input the prediction then matches the state exactly through every starve; an absorbed input that steered differently from its repeat is an ordinary correction. Held to `packages/netcode-bench/conformance/authority/input-repeat.json` (printed by `input-repeat.reference.mjs` beside it): its `rooms` cases are one seat's ticks (arrivals and the pacer's feed) and what the room hands the module, its `inRep`, `inStarve` and ack; its `clients` cases the stepped-through input and its `reconciles` cases the hitch clause alone (`reconcileThrough`: the input a reconcile compares at, from the one it compared at last, `held`), hostile values included; its `throttle` cases the throttle event by event (a keyframe's readings or a frame), its `seatClock` cases the clock other seats are drawn on, and its `tuning` the client constants, which every engine copy of the schema carries as generated constants (`gen-engine-schema.ts`), so a retune is a regeneration, never an edit of engine code.
1133
+
1134
+ **The reconciler's ack.** A client replays the inputs after the stepped-through ack from its input handle's replay ring, which Colyseus 0.18 sizes to (1000 ms + the patch interval) of inputs at the room's tick rate, times 1.5, at least 64 (TypeScript, C# and the native SDK alike: 180 at 120 Hz, 64 at 30 Hz), plus any copy the fast channel keeps (§17.12). A link that holds its downlink past that (TCP resending a lost segment twice on Wi-Fi) and then lets the backlog out over a few round trips hands the client states whose ack is still more than a ring behind its sends; reconciled at that ack, the replay skips the inputs gone from the ring, and an exact prediction is drawn back by their motion and then forward again (a held throttle in a 30 Hz vehicle room: four visible corrections, 423 units, after a 2.5 s hold). So the ack a client's reconciler is told is decided from the input the state holds its entity stepped through, `ack + inRep` (the plain ack when the setting is off) and never clamped to `sent`, by two clauses in order. The hitch's: past `sent` it is the ack the reconciler was told last, never past `sent` (no prediction of the client's is at the state's input yet; above). The ring's, on what the hitch's leaves (`stepped`): `stepped` unless it advanced while the oldest input after it (`stepped + 1`) is in neither the ring nor another copy the reconciler reads (TypeScript's replay falls back to the fast channel's copy; the C#, Unreal and Rust ports replay only from their shadow handle's ring): then it stays the ack it was last told, and the client keeps predicting on its own inputs until a state's ack is within the ring again, which reconciles every input. The hold lasts at most a ring's length of sends from the first held ack (`sent − heldFrom < ring`): a link whose acks stay that far behind is reconciled at them with what is left, and the hold is armed again only by an ack within the ring or a reset (an ack that moved back, such as a reconnect's). A hitch neither arms nor ends a hold: a backlog that lands caught up and stepped past `sent` is reconciled once that input is sent, the whole replay then within the ring. A port gives `input-repeat.json`'s `reconcilerAck` cases (both clauses, poll by poll, from `ack + inRep` as `stepped`) and its `reconciles` cases exactly (TypeScript `packages/multiplayer/src/authority/input-repeat.ts` `reconcilerAck`, from `stateThrough`); Godot's reconciler reads the room's own handle and is a labelled skip.
1135
+
1136
+ **Keyframes.** A predicting room never patches on §17.6's timer: it patches once after every keyframe tick, so each patch's `tick` is a keyframe's, and its acknowledgement is exactly the inputs the room consumed through that tick. `AuthorityRoomState` appends `physDesc` string, `keyframe` number, `shapes` map<string, AuthorityShape> and `joints` map<string, AuthorityJoint>, none defaulted, written in predicting rooms only:
1137
+ - `physDesc`: JSON `{ world, keyframeTicks, maxLinear, maxAngular, presetBody, remotePredict, remoteDecayMs, leadMs }`: the `WorldDesc` the room created (C§4), K, the clamp, the preset bodies' collider (`{ half, events }` or null) and the client knobs.
1138
+ - `keyframe`: the tick of the keyframe the state holds.
1139
+ - `AuthorityEntity` appends `rest` number: a body's sleep timer in whole inner steps, `min(65535, floor(rest / innerDt + 0.5))` (C§20.2). With `x y z`, the quantized fields and `sleep` it is the body's wire row (C§20.2): a client re-quantizes the decoded fields, which gives back the integers sent, and imports them with `importWire`.
1140
+ - `AuthorityBody` appends `desc`: the core's canonical descriptor (C§20.5) with each hull, trimesh or heightfield shape replaced by `{"ref":"<16 hex>"}`, the first 16 hex digits of the sha256 of its JSON; `shapes` holds that JSON under its ref (`data`), at most 32 refs of at most 16 KiB each, removed when no body names it. A body whose shape does not fit has `desc` `''`: it is drawn, never predicted.
1141
+ - `joints`, by joint key: `desc` the canonical joint descriptor and `motor` the motor the module last set (keys sorted, float32 numbers), `''` until it sets one; removed with the joint or either body.
1142
+ - `AuthorityPlayer` appends `inMoveX inMoveY inMoveZ inYaw inPitch` float32 and `inButtons` number: the seat's last input the room applied as of the keyframe; then `inRep`, `inStarve` and `inBuf` number: the repeats the seat owes, its starve runs and its buffered inputs (*Input pacing*), written only for a seat that sent `repeat@1`.
1143
+
1144
+ **Interest.** §17.10's view rules hold, with two changes in a predicting room. A body within `PHYSICS_CONTESTED_METRES` (6) · `lengthUnit` of any seat's own entity is contested: it is in every client's view at every keyframe, asleep or not, ranked before all others (a ball at rest that no update reaches is a "phantom ball" on the client). And the full-rate bodies are taken as whole contact islands: bodies whose bounding spheres (from their canonical descriptors) come within `0.5 · lengthUnit` of each other, or that a joint links, are one island; an island that does not fit whole under the 100 is skipped whole (R2-0b: a body is exact only when its whole island is simulated).
1145
+
1146
+ **The client world.** One per room: the static world (the module's `world`, whose `physicsHash` with the description must equal the room's), the bodies of whole islands in view whose members all have a descriptor (nearest your own entity first, up to the adaptive set size), each seat's preset body, the joints among them. Every predicted tick runs the server's order: preset and kinematic poses, `step()`, the clamp, and on `t ≡ 0 (mod K)` `canonicalize({ quantize: true })`.
1147
+ - Your preset body is posed from your predicted entity after each input (the input-sequence ring). At every keyframe the ring's inputs after the ack are stepped again from the state's own entity (your prediction's step, then *Seat contacts*), so a replay never poses your body where an older prediction, since corrected, had it; your velocity after each input is kept with it (a body rule reads it).
1148
+ - Another seat's is posed, `j` ticks after the keyframe, where the client's seat prediction has that seat after your input `ack + j` (*Drawing other seats*, `predicted`: the same entity the client draws, bumps and all). Without one it is moved by its entity velocity (`extrapolate`), or by the room preset's step (after the room's *Seat rules*, when the client has one) under its last input, held (`hold`) or repeated with its movement axes scaled by `w(j) = clamp(1 − (j − 1) · (1000 / tickHz) / remoteDecayMs, 0, 1)` (`inputDecay`; with `remoteDecayMs` 0: 1 on the first tick, then 0). Aim and held buttons repeat as they are, so no button's press edge (a jump, a flip, a boost start) is ever predicted: a held jump stays held (the preset reads the press edge from its own `mstate`).
1149
+ - A module's kinematic body (a door, a platform) holds the keyframe's pose, never extrapolated, is drawn from the server's states (§17.10), and is not compared at adoption (R2-0c: extrapolating a door was 2.7x worse than drawing it and forced a replay every keyframe), unless the room's *Kinematic rule* poses it.
1150
+ - Module commands are not predicted; they arrive as corrections. The exceptions are rules the module declares as pure functions of the state and runs itself (the client runs the very same function): *Seat contacts*, *Seat rules*, *Body rules* and *Kinematic rules*.
1151
+
1152
+ **A burst of keyframes.** A keyframe is adopted as it arrives (with the ring's inputs after the ack stepped again from its state, *The client world*) until the adoptions since the last frame began have taken `ADOPT_FRAME_BUDGET_MS`, one 60 Hz frame (1000/60 ms, timed on the session's clock); past it the keyframe is held for the next frame with what it arrived with (the state, your inputs sent, the instant), a newer one replacing it unadopted, and the next frame adopts the one held, ring and all, before its inputs go (TypeScript `frame()`, Unity `Frame()`, Unreal's and Godot's frame, Rust `frame`), which opens the budget again. A WebSocket hands over a second of states at once after a TCP stall: adopted each, ring and all, they made one frame of 100-616 ms (rec-real4/eu, a 250 ms route; Unity 387 ms for 79 states on the 250 ms seam, TypeScript 225 ms after a 1.3 s stall). Under the budget every keyframe is adopted as it arrives, so a client that never bursts adopts as before.
1153
+
1154
+ **Seat contacts.** A seat contact rule is what the module does to two seats' own entities after every tick's preset steps (a car-to-car bump), a pure function of the two (`a` the lower id) that changes their fields in place; the module runs it on every pair, in id order. A client given it (TypeScript `connectAuthority({ contacts })`) runs it in its own entity's prediction: after each predicted input `seq`, against every other seat's own entity in the latest state, moved on by its velocity to that input's tick (`seq − ack` ticks after the state, `1 / tickHz` each), in id order, its own entity `a` or `b` by id. A pass is the inputs stepped from one state: a seat the rule changed moves on from the input it changed at, until the next pass (a state with a new ack, or an input not after the last one stepped: a replay). Your fields it changed (`x y z vx vy vz yaw spin`) are written back as float32. When the other seats keep their course the predicted contact is the server's, exactly; when one swerves, the correction is a round trip late, as without the rule. Held to `seat-contacts.json` (*Conformance*). A module may also hand over its whole *seat contact pass*, a pure function of every seat's entity (ids ascending) changed in place: car-ball's pairs in id order, a pile's further passes and the world passes that push a car the bumps drove into a wall back out (`connectAuthority({ contactPass })`, the very function its `step` runs). With a seat prediction (`seatDrawing: 'predicted'`, or none given, whatever drawing a declared policy then names) the client runs that pass, not the velocity-moved pairs above: after each predicted input `seq` every other seat is stepped to that input's tick (*Drawing other seats*) and the pass runs on all of them and your entity together (without a pass: the rule once on every pair, id order), so a pile, a bump between two other seats and your bump with one are the server's whenever the inputs held are. A module that declares `config.contacts` (*Contact policy*) hands over neither function: every SDK runs the declared pass as both (TypeScript does whenever `connectAuthority` is given no `contacts`), and the drawing guard (*Drawing other seats*) runs over its pair rule.
1155
+
1156
+ **Contact policy.** A module may declare its seat contacts as data instead of handing over functions: `config.contacts` (TypeScript `packages/multiplayer/src/authority/contact-config.ts`, `contact-policy.ts`, `contact-world.ts`). A module that declares it needs no `contacts` or `contactPass` functions: every SDK runs the declared pass in its prediction and replay. The host resolves, refuses and publishes the declaration, and runs the pass itself: after the tick's joins, inputs (their preset steps) and leaves and before the module's `step`, on every joined player's seat body (ids ascending; `x y z vx vy vz yaw spin flags` read, `x y z vx vy vz spin` written back as float32), from a generated build of the TypeScript policy in the sandbox's prelude (`packages/authority-host/scripts/contact-policy-build.ts`, never a hand port). A module that declares `config.contacts` must not run a pass of its own; the function path above stays for rules the table cannot express. The resolved policy reaches clients inside `AuthorityRoomState.preset`'s JSON text as its `contacts` key (absent: none declared). Declaration: an object with `preset` (required: `shooter`, `platformer`, `vehicle-arena`, `physics-sandbox` or `party`) and optional `shape` (`{ kind: 'box', length, width, top }`, a body turned with the seat's `yaw` and laid on its support up; or `{ kind: 'hull', radius, height }`, an upright square footprint; default the movement preset's hull, a box `2·radius` square and `height` tall over a `vehicle`), `top` (`{ policy: 'stand', ride, center, slide }` (`ride` a boolean, `center` in (0, 1] or null, `slide` `{ speed, step, share }`; each left-out cell from the preset's stand, else `ride` true, `center` null and the shape's slide), `{ policy: 'slide', speed, step, share }`, `{ policy: 'bounce', stompMin, bounceSpeed, jumpBoost }` or `{ policy: 'ghost' }`, each key optional and defaulted from the preset), `offWall` (`{ speed, step, share }`), `restitution`, `passes` (an integer, 1 to 64), `slop`, `supportCos`, `entrySpeed`, `entryWindow` (seconds, at most 1), `roofSkin`, `maxVelocity` (null: no clamp; default the movement preset's), `stopSpin` (a boolean), `drawing` (`predicted`, `separate` or `timeline`: how a client draws other seats when the game names none, *Drawing other seats*; a lag-compensated room judges a shot where the shooter's render timeline drew its target, §17.5, so the character presets draw them there), `push` (`shared` or `mover`, step 4: a shooter's players are solid, a walker stopped by a standing one, never shoving it), `headFriction` (`slide` or `carry`: a dynamic module body on a seat's top slides off it or rests while level, §17.10's preset bodies), `propSlide` (`{ speed, step }`, step above 0: the nudge that slides it off) and `pairs` (at most 32 of `{ a, b, top }`: tags of 1 to 64 characters). Every number is finite and at most 100000; `share` and `supportCos` are in [0, 1], a slide's `step` is above 0 or `null` (at once). The room must run the `firstperson` or `vehicle` preset (the policy reads its `radius`, `height` or `standHeight`, `skin`, `groundTrace` and `maxVelocity`). Any other key or value is a bad config refused by its path (`$.config.contacts.top.speed`). A seat's tags are its entity's own (`tags`, TypeScript only) and `flag:k` for each bit `k` set in its `flags`; the first pair rule whose `a` and `b` both match, either way round, gives the pair's top rule, else `top`.
1157
+
1158
+ | Setting (by preset) | `shooter` | `platformer` | `vehicle-arena` | `physics-sandbox` | `party` |
1159
+ |---|---|---|---|---|---|
1160
+ | `top`, hull seats | stand, no ride, center 0.25, slide 300 / step 150 / share 1 | stand + ride, center null, slide 300 / 150 / 1 (the stomp is opt-in: `top: { policy: 'bounce' }`, stompMin 150, bounceSpeed 450, jumpBoost 150) | slide 300 / 150 / 1 | stand + ride, center null, slide 300 / 150 / 1 | stand + ride, center null, slide 300 / 150 / 1 |
1161
+ | `top`, box seats | slide 950 / 480 / 1 | slide 950 / 480 / 1 | slide 950 / 480 / 1 | stand + ride, center null, slide 950 / 480 / 1 | slide 950 / 480 / 1 |
1162
+ | `headFriction` / `propSlide` | slide / 300, 150 | slide / 300, 150 | carry / 300, 150 | carry / 300, 150 | slide / 300, 150 |
1163
+ | `offWall`, box seats | 950 / 480 / 1 | 950 / 480 / 1 | 600 / at once / 0.5 | 950 / 480 / 1 | 950 / 480 / 1 |
1164
+ | `offWall`, hull seats | the top's slide (else 300 / 150 / 1) | the bounce's slide | 300 / 150 / 1 | 300 / 150 / 1 | 300 / 150 / 1 |
1165
+ | `restitution` / `passes` / `stopSpin` | 0 / 16 / no | 0 / 16 / no | 0.6 / 32 / yes | 0.2 / 16 / no | 0.3 / 16 / no |
1166
+ | `drawing` | `separate` | `separate` | `predicted` | `separate` | `separate` |
1167
+ | `push` | `mover` | `mover` | `shared` | `shared` | `shared` |
1168
+ | `supportCos` / `entrySpeed` / `entryWindow` / `slop` / `roofSkin` | 0.7 / 1 / 1/60 / 0.1 / 0.01 | (all presets) | | | |
1169
+
1170
+ The world is §17.7's boxes and ramps grown by the hull (a box `[x0 y0 z0 x1 y1 z1]` to the planes `(−1,0,0, −(x0−r))`, `(1,0,0, x1+r)`, `(0,−1,0, −(y0−r))`, `(0,1,0, y1+r)`, `(0,0,1, z1)`, `(0,0,−1, −(z0−h))`; a ramp to `(1,0,0, x1+r)`, `(−1,0,0, −x0+r)`, `(0,1,0, y1+r)`, `(0,−1,0, −y0+r)`, its top `(−s·k,0,k, e)` along x or `(0,−s·k,k, e)` along y with `s = (hi−lo)/(max−min)`, `k = 1/√(s²+1)`, `e = (lo − s·min + r·|s|)·k`, then `(0,0,1, z1)` and `(0,0,−1, −z0+h)`), boxes then ramps; a point `p` is inside a solid when `e − n·p > skin` for each of its planes. A *cast* of a seat's base `p` along unit `d` for `len` visits the solids in order: one `p` is inside is skipped; one `p` is within `skin` of (every plane `e − n·p > −skin`) stops it at 0 when some plane with `e − n·p ≤ skin` has `n·d < 0`, else never; otherwise it stops at the least `t ≥ 0` where `p + t·d` is within `skin` of the solid (every plane `n·(p+td) < e + skin`): with `t_k = (e + skin − n·p)/(n·d)`, the greatest `t_k` of the planes with `n·d < 0` (its plane the face; a tie keeps the earlier), even when it is at or under 0 (a base left exactly `skin` off a face rounds either side of it), raised to 0, when it is under every `t_k` of the planes with `n·d > 0` and under `len` (a plane with `n·d = 0` and `e + skin − n·p ≤ 0` means never); the room is the least stop, `len` when none. A seat's own casts (its side push and its slide ways) pass through the ground it rides: a face whose z is at least `supportCos` and whose dot with the seat's support up is too, so a push along a curved bank is not stopped by the bank; a wall (any other face) always stops it. The *support up* of a base is the module's function (car-ball's `carUp`, TypeScript `ContactEnv.supportUp`) or else the normal of the face a cast straight down for `groundTrace` stops on, when its z is above 0, else `(0,0,1)`. The *push-out* of a base is §17.7's *Starting inside* over these solids: up to 4 times, the first solid it is inside, its least deep face but the bottom (a face pointing up that another solid covers is skipped; a tie keeps the earlier), the base moved along it by its depth plus `skin`.
1171
+
1172
+ One pair `a`, `b` (`a` the lower id), in float64 in this order:
1173
+ 1. A ghost pair (its top rule `ghost`) is skipped. Each seat's frame: a hull's up is `(0,0,1)`, half extents `radius` both, heading `(1, 0)`; a box's up is its support up, half extents `length/2`, `width/2`, heading `psincos(yaw)`; both carry their support up and `top` (a hull's `height`).
1174
+ 2. They cannot touch when their bases are `√(R² + T²) + T` or more apart on x, y or z (`R` the sum of the two half diagonals, `T` the mean top). Both ups `(0,0,1)` (level): `across = (b.z − a.z) + (top_b − top_a)/2`, no contact when `|across| ≥ T`; none when `|dx|` or `|dy| ≥ R`; the four edge normals `(c_a, s_a)`, `(−s_a, c_a)`, `(c_b, s_b)`, `(−s_b, c_b)`, each `depth = h_a + h_b − |along|` with `h = hl·|c·nx + s·ny| + hw·|c·ny − s·nx|` and `along = dx·nx + dy·ny`; any `depth ≤ 0` separates; the side axis is the least depth (the first of equals), from a to b (`along < 0` turns it); the across axis is `±z` by `across`'s sign (0: +z), depth `T − |across|`. Otherwise (tilted) the pair's up `n` is the two ups' sum made unit (no contact when it has no length), the centres each half its top up its up, `d` their difference, `across = d·n` (none when `|across| ≥ T`), each heading laid in the plane across `n` (`h − (h·n)n`, made unit; along `n`: straight up the plane), the axes `f_a, n×f_a, f_b, n×f_b`, each `depth = Σ hl·|f·k| + hw·|(n×f)·k| − |d·k|`, the side axis the least, the across axis `±n` with depth `T − |across|`: this *plane method* when the two ups are equal. Two ups that differ (one exactly, value for value) go by the separating axis test on the boxes each laid on its own up: each box's axes its heading laid on its own up (as above, `up` for `n`), `up × that`, and `up`, half extents `hl, hw, top/2`, centre half its top up its up; the 15 axes are `a`'s three, `b`'s three, then each of `a`'s crossed with each of `b`'s (`a`'s order outer), a cross product of length `≤ 0.000001` skipped and the others made unit; `depth = r_a(k) + r_b(k) − |d·k|` (`r` a box's `Σ half·|axis·k|`), any `≤ 0` separates; each axis turned from a to b (`d·k < 0` turns it); the side axis the least depth (the first of equals) of all 15 (the minimum translation axis: the push runs along it) and the in-plane axes all 15; the across axis `±n` with depth `r_a(n) + r_b(n) − |d·n|`; half extents along any `w` are `r_a(w) + r_b(w)`. The plane method laid both boxes in one plane: against the separating axis test it was off by a median 2.7 u with the ups 5° apart and 13.5 u at 90° (seeded car pairs). At the switch a touching pair (depth up to 4 u) keeps its depth within 0.01 u; a deep pair partly stacked (less inside along an up than across) is pushed along its least depth, where the plane method would push it across by the whole footprint overlap.
1175
+ 3. The pair *landed* when it came down across the plane within the window and less than halfway (`t_up = depth_up / max(closing_up, entrySpeed) ≤ entryWindow`, closing `−(v_b − v_a)·axis`, and `depth_up ≤ T/2`), or, tilted, when `depth_up ≤ groundTrace` (a tilted body is laid on its support up again every tick, which moves it a few units across the plane with no closing speed). The contact axis is the across axis when the pair landed and `t_up` is under `t_side` (the side axis's, alike), or equal with the lesser depth; else the side axis. Two seats that do not touch are still in a *resting* top contact when the higher base let down by `groundTrace` meets the other across the plane, that axis shallower than the in-plane one and within `supportCos` of the lower seat's support up, the lower seat can be stood on (its support up's z at least `supportCos`: up a wall two seats a hair apart across its plane are side by side), and the upper seat moves away from the lower along it at most `entrySpeed`: its depth is that overlap less `groundTrace` (at most 0), it is never placed (step 5 moves nothing for a depth plus `roofSkin` at most 0), and its slide goes on (a car on a car in the air rides one arc with it, a skin apart). An overlap older than that was there before the last pass, so two seats at rest are parted in the plane they ride (side by side up a wall, half each, as two on the floor). The upper seat is the one the across axis points to. The contact is `top` when the across axis is chosen and, turned to point at the upper seat, its dot with the lower seat's support up is at least `supportCos`; else `side`.
1176
+ 4. Side: each seat's *way* for its push (`a` along `−n`, `b` along `n`, for `depth`, `d` that direction): straight to `t`, where its cast stops (`depth` when nothing stops it; a push with `d·up < −0.5`, more than 30° into the ground the seat rides, `up` its support up, is cast without passing that ground, so it stops there), then, when a face `f` stopped it: when `f·up ≥ 0.7` (the seat's own ground, a car on a wall) along that face, `l = d − (d·f)f`, `c = |l|`; otherwise along that wall in the plane the seat rides, `l = f × up`, `c = (l·d)/|l|`; when `|l| > 0` and `|c| ≥ 0.5`, the slide `u = l·sign(c)/|l|` from `p + t·d`, cast for `(depth − t)/|c|` to `r`, and its room is `t + r·|c|`; otherwise its room is `t`. The part `a` would take, `w`: ½ under `push: 'shared'`; under `mover` `c_a/(c_a + c_b)` with `c_a = max(0, v_a·n)` and `c_b = max(0, −v_b·n)` (z only when `n.z ≠ 0`), ½ when both are 0. The shares `s_b = min(room_b/depth, 1 − min(room_a/depth, w))` and `s_a = min(room_a/depth, 1 − s_b)` (without a world `w` and `1 − w`); a *fixed* seat (a drawing's own entity) takes 0 and the other `room/depth` (1 without a world), and what a wedged pair leaves goes all to the other. A pair wedged along `n` (`s_a + s_b < 1`) takes the first other in-plane axis, least deep first (equal depths in the axes' order), whose shares for its own depth leave none, as its `n` and `depth`; with none, `n` and what is left split half each (the push-out takes it out of the walls). Then `closing = (v_b − v_a)·n` (z only when `n.z ≠ 0`), `j = −(1 + restitution)·closing/2` when closing (0 while the release-1 cooldown holds it), each seat moved its way by its part of the push, `q = depth·s`: `q·d` when `q ≤ t` or it has no slide, else `t·d + ((q − t)/|c|)·u` (its z only when that move's z is not 0), velocities `− 2j·w·n` on `a` and `+ 2j·(1 − w)·n` on `b` per axis (`j` each when `w = ½`) (x and y always, z when `n.z ≠ 0`), each clamped to `±maxVelocity`, and `spin` 0 on both when `stopSpin`.
1177
+ 5. Top: `up` the axis toward the upper seat; `need = depth + roofSkin`, the upper seat's cast along `up` for `need`; level with room for it: the upper base at `z_lower + top_lower + roofSkin`; otherwise the upper base moved by its room along `up` and the lower base by its own cast along `−up` for what is left (a *fixed* seat's room and cast are 0). Then when `v_upper·up < v_lower·up` the upper velocity's part along `up` is set to the lower's (level: `vz = vz_lower`).
1178
+ 6. Its policy. A lower seat whose support up's z is under `supportCos` slides by `offWall`. `stand`: the *stand test* on the pair as it came in (before step 5) says how the upper seat is held: with `center` null it *stands*; else with `off` the upper centre's offset from the lower's in the lower's footprint frame as a share of its half extent (a hull: `max(|dx|, |dy|) / radius`; a box: `max(|along| / (length/2), |across| / (width/2))` in its heading frame, `psincos(yaw)`), `off ≤ center` stands; beyond it a *landing* (`vz_lower − vz_upper > 100` u/s, `HEAD_LANDING_SPEED`) slides; a resting seat whose lower moves away from under it (`−v_lower·d/|d|` in the plane, `d` from the lower's centre to the upper's; 0 when `ride`, the lower carrying it, and when `|d|` is 0) at half the slide's `step` or more (its `speed` when `step` is null) is *released* (the upper's own speed never releases it: it is on the head's ground, whose friction stops a lander's run-out as the floor's would; counted, a centred lander glided across the head at its landing speed, LG7, 2026-11-02); any other slides when `off > 1` and stands while `off ≤ 1`. Standing or released: nothing more (a released seat gets no head slab, *Lifts*, so it drops off as the lower walks away under it); sliding: as `slide` below with the stand's `slide`. `slide`: the ways, from the lower seat toward the upper's side, are (a) the side axis turned toward the upper seat, (b) its opposite, (c) `p = plane × (a)` and (d) `−p`, each with `move = (h_a + h_b along it) − (centre_upper − centre_lower)·w`, taken least `move` first (ties in that order: a slide blocked one way leaves by the nearest edge, not over the lower seat's whole length); in a pass, the upper seat's own `move` along each way is the largest of this pair's and that of every other seat of the pass it rests on (a top contact with it above, its pair not a ghost: `h` and the centres of that pair along the same `w`), so a seat across two others is not slid off one onto the other; the first the upper seat's cast for its `move` clears (or `move ≤ 0`) is taken: `parting = (v_upper − v_lower)·w`, `k = min(speed − parting, step)` (all of it with `step` null) when `parting < speed`, else 0; the upper velocity `+ k·share·w`, the lower `− k·(1 − share)·w` when `share ≠ 1` (x and y always, z when `w.z ≠ 0`, clamped). None free for it: the same ways turned round, for the lower seat's base: the first clear is taken by the lower seat alone (`parting = (v_lower − v_upper)·w`, `+ k·w`); none for either: the pair stands (*pinned*). `bounce`: when the closing speed along `up` (before step 5) is at least `stompMin`, the upper velocity's part along `up` is set to `bounceSpeed` (plus `jumpBoost` while its entity's `jump` is held) and the module hears `stomp` (upper, lower); damage stays a server event, never predicted; under `stompMin` it slides by its slide.
1179
+ 7. The pass: every pair in id order; then further passes, up to `passes` in all, on the pairs whose contact (steps 1-3) is a side contact deeper than `slop` or a top contact at all (a stack a third seat moved back into is set again); after every pass that touched a pair, every seat pushed out of the world; a pass that touches none ends it. A push-out through a face no seat stands on (z under `supportCos`) is a *wall push*: what the contacts drove into a wall (the G6 judge reads it, TypeScript `ContactReport.walls`). A module may answer the first pass's pairs with its own rule (car-ball's release-1 sweep over its 60 Hz tick).
1180
+ 8. The *ride*, in the seat-rule slot before the tick's preset steps, from every seat as it was before any moved: a seat whose base is inside a kinematic body's footprint and within `groundTrace` of its top is carried by that body's move to the next tick (`kinematicRule(t+1) − kinematicRule(t)`, x and y); else one standing on another seat (their top rule `stand` with `ride`, the lower's support standable, and the upper let down by `groundTrace` meeting it across the plane from above within `supportCos` of its support) by that seat's `vx vy` over the tick; a seat leaving upward (`vz > 0`) adds that velocity to its own.
1181
+
1182
+ **Seat rules.** A seat rule is what the module does to a seat's own entity at a tick it announced in advance (a kickoff that holds cars with `cap` 0 and sets `cap` 1 in its step of tick `go`): a function of the entity's fields and the tick that seat's input is applied on, changing the fields in place before that input's step. A client given it (TypeScript `connectAuthority({ seatRule })`, which may read the latest state, such as the announced `go`) runs it on your own entity before each predicted input `seq`, at tick `tick + (seq − ack)` of the state that acked through `ack` (*Input pacing*: one input a tick; `ack + inRep` for a client that sent `repeat@1`), handing it `seq` too (so a rule keyed to one of your inputs, such as the blast of a rocket you fired at input `s`, finds that input's tick as `tick − (seq − s)`), and, in an `inputDecay` room, on each other seat's body before each of its steps, at keyframe tick `+ j`. So a release is predicted, not learned a round trip late. Held to `seat-contacts.json` (your entity) and `physics-predict.json` (other seats).
1183
+
1184
+ **Body rules.** A body rule is what the module does each tick to its dynamic bodies' velocities from the seats' entities (a scripted hit on a ball, a speed limit): a pure function of every seat's own entity at that tick (key order; position and velocity in room units, per second) and the bodies' states as the last step left them, returning the velocities to set; the module sends them as `setVelocity` commands from its `step` (after its other commands). A client given it (TypeScript `connectAuthority({ bodyRule })`) runs it in the client world before every predicted step, after the preset and kinematic poses: seats as *The client world* moves them (yours from the ring with its velocity, others with their keyframe velocity), each velocity it returns set on a simulated dynamic body it names, in its order. Rules that read anything else (events, timers, cooldowns) are not body rules: they stay corrections.
1185
+
1186
+ **Kinematic rules.** A kinematic rule is where the module poses one of its kinematic bodies (a door, a sliding gate, a moving floor) at each tick: a pure function of the body's key and the tick (its keyframed schedule), answering a pose or nothing for a body it does not pose. The module sends `setNextKinematicPose(key, rule(key, t + 1))` from its `step` of tick t, so the body ends that tick's step on the rule's pose for t + 1. A client given it (TypeScript `connectAuthority({ kinematicRule })`) poses each simulated module kinematic body the rule poses with `rule(key, t + 1)` before its predicted step of tick t, in place of the keyframe's pose, and draws it as a predicted body (*Drawing*: at its present state, its correction eased), so what rides or meets it (a crate on a moving floor, a crate a gate shoves, your own body carried by a seat rule) is predicted on the same timeline as it, and a body that stops on its schedule is never drawn overshooting. It is still not compared at adoption. A body the rule poses that the client world does not simulate (out of view, or its island not chosen) is drawn by the rule all the same, on the world's timeline: its pose for the last stepped tick plus one, toward the next tick's by the time since that step (at most one tick), so it never jumps between the client's timeline and the server's present as it enters and leaves the set; it is drawn on a clock that runs with the room clock and slews toward the world's timeline by at most `RULED_SLEW` (0.1) of the time passed, snapping past `RULED_SNAP_TICKS` (30), so a replay that moves the timeline (the server ran a tick without your input, or took two in one: *Input pacing*) never makes it jump or move back. A client that does not draw the bodies from a client world (no core, `cpu`, any fallback level) still draws a body the rule poses by the rule, on the server's timeline: at the server's present `now`, between `rule(key, T)` and `rule(key, T + 1)` for `T = floor((now − tickOrigin) · tickHz / 1000)`, never by §17.10's extrapolation (which carries a gate past its stops at every reversal). Held to `physics-predict.json`'s case `kinematic rule`. Netcode Park's Mechanism Hall measured the alternative: a moving floor and a sliding gate held at the keyframe's pose in the client world and drawn by §17.10's extrapolation (crates riding the floor drawn 160 ms behind it at a 200 ms round trip, the gate drawn 0.66 m p95 from where it was as it overshot its stops).
1187
+
1188
+ **Lifts.** A module may declare moving platforms as data: `config.lifts` (TypeScript `packages/multiplayer/src/authority/lifts-config.ts`, `lifts.ts`), resolved, refused and published by the host (the state's `preset` text carries it as `lifts`) and posed by every SDK from the same rules, so no module function is handed over or ported. It needs the `firstperson` or `vehicle` preset. Declaration: `{ platforms: [ … ] (1 to 32), bodies?: boolean (default true) }`; a platform is `{ id (1 to 32 of a-z, 0-9 and -, unique), shape: { kind: 'box', half: [hx, hy, hz] } | { kind: 'disc', radius, half }, at?: [x, y, z] (required without a path), path?: { points, legs, waits?, mode?: 'pingpong' (default) | 'loop', ease?: 'linear' (default) | 'smooth', phase? }, spin?: { period, dir?: 1 | −1, phase? }, surface?: [vx, vy] (a conveyor, u/s in the platform's frame), impart?: 'all' (default) | 'none', called?: boolean }`. Sizes, points and speeds are finite, of magnitude at most 1e6, used as float32 (sizes above 0); tick counts are integers in [lo, 2²⁴] (legs ≥ 1, waits and phases ≥ 0, a spin's period ≥ 8); pingpong has one point more than legs, loop one point a leg, at least 2 points and at most 16 legs, `waits` one per point (default all 0); a called lift takes no `phase`. A platform any point of which moves more than the preset's `radius` in a tick is refused (no seat can be passed through): its fastest move is, over its legs, `|b − a| / n` (`1.5 ×` that for `smooth`, its peak) plus a spin's tip `reach · 2π / period` (`reach` a disc's radius, a box's `√(hx² + hy²)`), and the refusal names the path of that leg (or `spin.period`), the platform, that move and the most allowed at the room's `tickHz`. Any other key or value is a bad config refused by its path.
1189
+
1190
+ *Pose at tick t* (integer arithmetic for the phase, float64 for the rest, then float32): with no path the centre is `at`; else the phase `m = ((t + phase) mod P + P) mod P` with `P` the cycle (pingpong `2·Σlegs + 2·Σwaits − waits[0] − waits[last]`, loop `Σlegs + Σwaits`), or for a called lift the ticks since its call for one cycle (0 before it, after it, and until called). The cycle runs leg by leg, each followed by the wait at its end point: pingpong legs `0 … n−1` out and back `n−1 … 0` (the wait at a point after arriving there), loop leg `k` from point `k` to `k + 1 mod n`. At tick `i` of a leg of `n` ticks from `a` to `b`, each axis is `a + ((b − a)·i)/n` (linear) or `a + (b − a)·e` with `s = i/n`, `e = s·s·(3 − 2s)` (smooth); a wait holds its point. A spin's yaw is `float32(dir · ((2π · ((t + phase) mod period)) / period))` (positive mod), else 0. *Carry* from `t` to `t + 1`: with spin step `δ = dir·2π/period` (0 without), `[s, c] = psincos(δ)`, a rider at `p` goes to `c(t+1) + R(δ)(p − c(t))` (without a spin, `p + (c(t+1) − c(t))` on x and y), `z + (cz(t+1) − cz(t))`, then a conveyor adds `R(yaw(t+1))·surface / tickHz` (unturned when that yaw is 0); its yaw turns by `δ`, and its velocity over the tick is the move times `tickHz`. *Posed solids*: each lift at its pose, in declared order: a box without a spin as the box `[c − half, c + half]`, a box with one as an obox with `psincos(yaw)`, a disc as a disc. The inputs of tick `t` step through the lifts posed at `t + 1` (where the tick leaves them). *The slots* (the host, after the tick's joins and before its inputs; the impart after the contact pass), every seat ids ascending, every support and carry read from the seats as they were: (1) its *support* at `t`: the preset hull's `traceDown` from its base (−1 while `vz > 0.1`) among the world, the lifts posed at `t` and, in a room whose contact policy has a `stand` rule, its *head slabs* at `dt` 0: the highest top within `[z − groundTrace, z + groundSnap]` that is a lift's (the first such in order) names that lift; one that is a slab names that seat, ridden when their stand has `ride`; (2) the *carry*: on lift L, L's carry from `t` to `t + 1` (a vehicle's yaw turned by `δ`); riding seat S, S's carry (S's walk follows in the ride slot), through at most 4 seats; written as float32; (3) the *push-out*: every other lift the seat is more than `skin` inside at `t + 1`, in order, moves it by that lift's carry at its point, then §17.7 *Starting inside* (up to four zero-length sweeps out by depth then skin); still inside a solid, it is *crushed* by that lift (`ctx.lifts.crushed()`, seat and lift). Each seat's input step then runs through the world, the lifts at `t + 1` and its own head slabs at the tick's `dt`. (4) The *ride slot*, after every input's step and before the tick's leaves and contact pass: a seat riding S moves on x and y by S's walk this tick (S's place now less its place after (3): every preset step S's inputs took, a repeat counted and an absorbed input not, so none on a held starve and two when the pacer feeds two, *Input pacing*) plus S's own ride, through at most 4 seats, written as float32; its carry velocity gains that move times `tickHz`. A rider so keeps its place on a walking seat's head from the first step to the stop (owner, 2026-11-02: it trailed by up to a tick's walk when the carry read S's velocity at the tick's start). (5) The *impart*, after the contact pass: a seat carried by lift L that no longer stands on L at `t + 1` adds L's carry velocity when L imparts (`impart: 'all'`); one that rode seat S and no longer stands on S's slab (at `dt` 0) adds its carry's velocity. *Head slabs*: seat U's slabs, in seat order, are the tops of every other seat L with their pair rule a `stand`, U's base in `[L.z + top − groundTrace, L.z + 2·top]`, U's `off` from L (the stand test's) under 2, L's support up's z at least `supportCos`, and the stand test *standing*: L at its place plus its velocity times `dt`, a hull's `[x ± r, y ± r, z + top − groundTrace … z + top]` box, a box seat's obox turned by `psincos(yaw)` (half `[length/2, width/2, groundTrace/2]` about `z + top − groundTrace/2`). A room with a stand rule and no lifts runs the slots over no lifts. Held to `packages/netcode-bench/conformance/authority/lifts.json` (sections `config`, `pose`, `carry`, `posed`, `presetOnLift`, `presetOnLiftVehicle`, `ticks`, `heads`), printed byte for byte by `lifts.reference.mjs` beside it.
1191
+
1192
+ *On a client* (TypeScript `lift-predict.ts`, `session.ts`). Your seat's prediction runs the slots round each input at the server's tick for it (`state.tick + seq − steppedAck`, *Input pacing*): the carry, the step through the lifts at `t + 1` and your head slabs (other seats from the seat prediction at your input `seq − 1`, else the latest state), the ride slot (the seat prediction's, with the other seats' steps of that tick), the contacts, the impart; so a rider with held inputs is the server's bit for bit at any round trip. `session.liftPose(id)` draws each lift on your entity's clock: at the fractional tick `state.tick + j + 1` with `j` your drawn input (the state after an input stands with the lifts at its tick + 1), else on the render timeline. A tick that steps two of your inputs or none (*Input pacing*) moves every input in flight a tick: a live step goes on with the ticks the inputs before it were carried at (so your pose never stands a tick off the lift under it), the next replay times them all from the latest state, and a reconcile that finds you on the same lift before and after carries the pose from before in that lift's frame to the new ticks before it compares and eases it (Unreal's based movement corrects relative to the base): a re-timing is never drawn or counted as a correction; the lifts are drawn on those same ticks. `session.viewCarry()` is the turn your view owes the lift your entity stands on since the last call: the lift's turn between the ticks your newest input is applied at then and now, by the latest state's map of your inputs (0 off one, and on the call that finds you on it); a `firstperson` game adds it to its view yaw once a frame (Unreal's base rotation), since a firstperson view is input, so the view turns with the lift tick for tick across doubled and starved ticks. Another seat drawn on the render timeline that stands on a lift in the latest state (exact: its support against the lifts at `T + 1`) is drawn in that lift's frame, `P_L(drawn) ∘ P_L(render)⁻¹` on its render pose, at its height over the lift's drawn top; a change of lift eases over 100 ms (`LIFT_BLEND_MS`). The seat you ride is drawn under you: while your newest predicted input rides seat S (their stand with `ride`), S is drawn at your drawn place less your place against S in the latest state, a change eased over `LIFT_BLEND_MS`, its heading its own (lag compensation still judges a shot at it on its timeline, §17.5); on its own timeline a 200 ms client drew it about 40 u behind you.
1193
+
1194
+ *In a physics room* (step 9 of the tick): each lift is a kinematic body `lift.<id>` (TypeScript `lift-predict.ts` `liftBodySpawn`): one collider `lift`, its box (a disc a 16-sided prism, its corners `(radius·cos, radius·sin)` of `2πi/16` by the portable sine, at `±half`), frictionless (`friction: 0`) on a conveyor, at its pose with the quaternion `[0, 0, sin(yaw/2), cos(yaw/2)]` (portable sine, float32). The host spawns them at load at `P(1)`, before the module's own bodies, and in tick `t`, after the module's commands (whether or not its output applied), sends `setNextKinematicPose(lift.<id>, P(t + 1))`, so the step leaves them where the seats' slots put the lifts. A module command naming a lift's body is refused `wrong-body-type`, as one naming a preset body is. Then the built-in body rules after the module's, on the dynamic bodies as the host's last export left them (key order), only after an applied output: the *conveyor rule*, then the prop rule (*Contact policy* `headFriction`). The conveyor rule: a body whose centre is over a conveyor's top at `P(t)` (inside its footprint in its own frame, `0 < z − top ≤` the preset body's height; the first such conveyor in declared order) has its horizontal velocity moved the share `min(1, CONVEYOR_RATE / tickHz)` (`CONVEYOR_RATE` 20 a second, so a crate is at 95% of the belt's speed in about 150 ms in any length unit) of the way to its point's carry velocity from `t` to `t + 1` (the carry above: `(p' − p) · tickHz` with `p'` the carried point, the surface included), `vz` kept, as `setVelocity`.
1195
+
1196
+ **Adopt and replay.** When a patch carries a new keyframe (its `tick` equals `keyframe`):
1197
+ 1. Skip-on-match: when the world predicted that very keyframe (the same bodies, descriptors, joints and seats, every compared wire row bit-equal, the ack where the prediction put it), nothing is redone. With two or more players this is rare; every keyframe is budgeted for a replay.
1198
+ 2. Otherwise the world is brought to the keyframe's bodies, joints and seats (spawned from the canonical descriptors, removed, preset bodies spawned at their keyframe pose), imports every row (`importWire`, preset bodies as their centre, identity, still), canonicalizes, and replays your inputs `ack + 1 … sent`, at most `ceil(leadMs · tickHz / 1000) + K − 1` of them.
1199
+ 3. In each replayed tick the seats are posed after the room's declared contact pass (*Contact policy*) has run on them (ids ascending, from their poses and velocities at that tick), on ticks where the seat prediction (*Drawing other seats* `predicted`) posed none of them: that prediction runs the pass itself.
1200
+ 4. A room with lifts (*Lifts*, *In a physics room*): the rebuild spawns each lift's body once (its descriptor as the host's) and imports its row at `P(keyframe + 1)` (its centre, its yaw's quaternion, still, awake, no sleep timer) after the seats'; each replayed tick `t` poses every lift at `P(t + 1)` and runs the conveyor rule after the room's body rule and before the prop rule. Another seat stepped under its held or decayed input (`hold`, `inputDecay`) runs the lift slots around its step, alone (no head slabs): the carry of tick `t`, the step through the lifts at `t + 1`, the impart. Held to `physics-predict.json`'s case `lifts in the replay` (its `lifts`: a declaration and the movement preset's params; a crate riding a shuttle by friction, one on a belt).
1201
+
1202
+ **Drawing.** A predicted body is drawn at its present state, moved on by its velocities for the time since the world last stepped (at most one tick; none while asleep). After a replay, each body whose pose moved starts a correction from where it was drawn: `dp = p_old − p_new`, `dq = q_old ⊗ q_new⁻¹` the short way, drawn as `p + o(age)`, `slerp(identity, dq, e^(−age / τ)) ⊗ q`, the position's offset critically damped from `o(0) = dp` and the rate `r(0)` the body's previous correction had at that instant (0 for a first one or after a snap): per axis, with `h = τ / 2`, `o(t) = (dp + (r(0) + dp / h) · t) · e^(−t / h)`, its rate `r(t) = (r(0) − (r(0) + dp / h) · t / h) · e^(−t / h)` (at `h = τ / 2` it trails a steady drift as far as the first-order `τ` did; at `h = τ` the drawn ball trailed the truth by 80 ms more). So the frame a correction starts on draws the body where it was drawn, moving on with the drawing's own rate, and a body corrected on every keyframe still comes home (a correction started from rest each time barely decayed between keyframes: the drawn ball fell 300 ms behind); the first-order `dp · e^(−age / τ)` moved it `|dp| / τ` at once, and on real home Wi-Fi a car-ball ball corrected 3 to 4 m was drawn jumping 300 to 450 units in a frame. τ follows the correction's size in metres: 300 ms up to 0.25 m, 100 ms from 1 m, linear between. A correction over 2 m (a body of physics kind `player` or `vehicle`) or 6 m (any other; at 3 m a remote touch learned a round trip late, 3 to 4.5 m on real-mac-wifi, snapped) snaps, and so does a body whose `epoch` moved since the keyframe that last carried it (the module respawned it, §17.3: its new life is drawn at once, never eased from the old one). Only the drawn offset decays; the client world always takes the exact state. Bodies the client does not predict are drawn as §17.10 says. A body that enters or leaves the predicted set (into view, its island chosen or dropped, a fallback or a recovery) starts a correction from where it was last drawn, eased and snapped by the same rules (never across an `epoch` change), so it never jumps by its velocity times the lead between the client's timeline and the server's present. Another seat drawn `predicted` (*Drawing other seats*) eases its corrections with its own rule (`PredictSmoothing`, `seat-predict.ts` `seatOffset`): its offset (position and heading) and the offset's rate return to 0 critically damped, each frame of `dt` ms by `o' = (o + (v + o/τ)·dt)·e^(−dt/τ)`, `v' = (v − (v + o/τ)·dt/τ)·e^(−dt/τ)`, with τ `SEAT_TAU_MS` (70 ms; a tenth left after about 280 ms) or `SEAT_CONTACT_TAU_MS` (100 ms) on a frame whose predicted tick has it touching your entity (so a bump lands where it is drawn). A new pass's difference at the drawn input, or at the last input the pass it was drawn from stepped when the drawn one is past that (the seat clock can draw up to 1.5 inputs past your last; there the current pass moves a seat on by its velocity, a tick at most, and the pass before along its last steps, so comparing there read the drawing's own advance as a correction: up to a tick's travel each new pass, 82 u on a steady course at 120 Hz), joins the offset (position and heading) and the difference of its track's slope there joins the offset's rate (`SeatWorld.changeAt`, `motionAt`: the predicted pose and the track's change over half an input either side, per ms). The three passes before the current one are stepped on with it, tick by tick with your entity as the current pass was given it, so the two are compared at an input both stepped: a bump or a turn the newest tick brings is in both and is drawn on time, not eased as a correction (a pass older than that goes on past its last tick along its last three ticks' steps and their change; moved on by its velocity alone, an accelerating car read slower than the new pass at every pass and was drawn behind it). So on that frame the drawn seat keeps both its place and its velocity and turns onto the new track over the next frames (a first-order decay moved a correction C by C/τ at once, and a new pass that changed the predicted velocity changed the drawn one at once: the car-ball bench read them as one-frame snaps and lurches at 200 ms). The return then never pulls the seat back against the way it was drawn moving (the unit direction of its drawn velocity, kept while that is over `SEAT_MOVING_UNITS_PER_MS`, 0.2 units a ms) while its track at the drawn input still goes that way: on a frame that moves it back along it, or leaves it moving back along it, the frame's move back is taken off the offset and the offset's rate along it set so the drawn seat is at rest along it, and the correction shrinks as the prediction comes up to it (a frame that still moved it on while the return's rate already drew it moving back once let that backward rate become its drawn way, and every frame after pulled it back: car-ball's brake at 200 ms under load); once the seat has been drawn slower than that for `SEAT_HOLD_MS` (120 ms), or its track turns back, it returns the critically damped way from rest (a held input wrong for a quarter second at 200 ms drew the car pulled back against its motion: rubber bands). A track turning back does not cut that stand short when a stop was found late, while the seat is drawn ahead of its track and its velocity in the latest state (`SeatWorld.stateRate`, the server's car) still goes that way faster than `SEAT_MOVING_UNITS_PER_MS`: other seats are drawn on your input clock, and the lead a jittered uplink builds draws a braking car's held input stopped and reversing while the server's car still rolls on (released then, 0–30 ms of uplink jitter pulled it back up to 20 u a frame). A stop is found late (`stopFound`) when a new pass has the seat slower along the way it was drawn moving than the pass it was drawn from, at the same input, by more than `SEAT_MOVING_UNITS_PER_MS`; it holds while the seat is drawn stopped, and each pass that arrives while it is drawn moving decides it again. A track that turns back with no stop found late (a bounce, a turn the state already had) is drawn turning back at once: stood, a seat froze while its prediction came away (car-ball at 200 ms: corrections p95 +8%). Snapped over `SEAT_SNAP_METRES` (6 m) or at a new life.
1203
+
1204
+ **Drawing other seats.** Other entities are drawn as §17.3 says (interpolated `RENDER_DELAY_MS` behind, snapping on an epoch change), with one change in a predicting room: each patch is one keyframe, so an entity's sample is timed by that keyframe's tick (`tickOrigin + tick / tickHz`), not by the patch's server stamp, which is the room clock at the fixed step's call that ran the tick (late by up to a tick, and shared by every tick a late call caught up: at 60 keyframes a second the drawn speed jumped from segment to segment). An entity a keyframe left unchanged sends nothing; it held its fields since the keyframe before, so a sample after such a gap first repeats the last one a keyframe period earlier (`src/authority/tick-drawing.ts`). Lag compensation still rewinds by the server's stamps (§17.5), within a tick of what was drawn. With a seat contact rule (*Seat contacts*), a client draws other seats in one of three ways (`seatDrawing`): the game's, else a declared policy's `drawing` (*Contact policy*), else `predicted`. Your entity is drawn at your newest predicted input (a round trip and the input buffer ahead of the latest state) and the render timeline a render delay and half a round trip behind it, so at a 200 ms round trip the two are about 350 ms apart and two seats that miss each other on the server can be drawn driving through each other. `timeline` draws every seat on the render timeline, as above, and nothing more. `separate` keeps every seat on the render timeline and only moves one drawn inside or across your entity out of its way (the last step below). `predicted` (the Rocket League model, the car-ball owner's reference) draws every other seat on your own entity's clock, from the client's seat prediction (`SeatWorld`): a *pass* starts from the latest state whenever its ack or tick changes or your prediction replays (an input not after the last one stepped), takes every other seat's entity (the room preset's fields) and last applied input from it, and after each of your predicted inputs `ack + j` steps every other seat to that tick (its *Seat rules*, then the room preset's step under its input as `remotePredict` says: `hold` the input as it is, `inputDecay` decayed, `extrapolate` or no input or no preset: moved by its velocity; ticks your prediction did not step again take your entity as the pass before left it), then runs *Seat contacts*' pass on every seat and yours, ids ascending. Such a re-run keeps none of its changes to your entity, so a state that put your inputs in flight on later ticks (one that consumed none of them, then several) can find a seat touching yours in an input whose live step it did not touch (`takeMissed`, `conformance/carball/carball-seat-predict.json` `remaps`): your inputs are then predicted again from the state at once, and your entity's drawing eased from where it was over `SEAT_TAU_MS` in every SDK (`shoves` in the same file pins what is left of the ease at each time, and when it is done under 0.01 units). Kept as it was, your entity stood as a wall the seat stopped against until the server's bump came back a round trip later (`authority-contact-late-inputs.test.ts`). Past `leadMs + STALL_PREDICT_MS` (500 ms) of ticks the seats hold (a stall). The seat is drawn at the input your entity is drawn at (`ack + j`, `j = sent − ack − 1 + α` with α the render's fraction between your last two predicted inputs) on a smooth clock of its own (`seatClock`): last frame's input moved on by the frame's time at the tick rate, pulled toward that one by a share `1 − e^(−dt / SEAT_CLOCK_TAU_MS)` (100 ms), never back, and taken at once when more than `SEAT_CLOCK_SNAP_INPUTS` (1.5) off (Colyseus' Predict restarts α at 0 on a frame that sends an input more than its time asked for, so your entity's input moved on by most of a third input on one frame in four at 57 frames a second, and every seat drawn there stepped half a frame's travel at once); between the two ticks around it (positions linear, the heading along the short arc; past the last tick moved on by its velocity, at most a tick), plus its correction (*Drawing*): when a new pass draws it elsewhere than the pass it was last drawn from, the difference at that input joins the offset, which then decays (a new life of the seat starts it at 0). *Touching* (the Rocket League model's rule that what you interact with is drawn on your clock, for every room and preset): a seat touching your entity is drawn at the input your entity is drawn at exactly (`ack + j`, without the throttle's share and without `seatClock`'s slew), blended in and out: it is drawn as above plus its share of the way from where the seat prediction has it at the seat clock's input to where it has it at your exact one (positions, and the heading along the short arc). A seat is touching when the pass's contact test had it touching your entity in the tick of that input or the next one (a contact your predicted steps resolve, *Seat contacts*), or when the room's contact rule touches your entity there with the seat moved on from it at the two's relative velocity (their bodies' `vx`, `vy`, `vz`) by `k/8` of `SEAT_CLOCK_TAU_MS` (`k` = 0..8). Its share moves toward 1 while it touches and toward 0 while it does not by `dt / SEAT_CLOCK_TAU_MS` (the seat clock's own time to move other seats between two input clocks), within [0, 1]; a seat without one takes its target. So a seat closing at a steady speed is touching the seat clock's 100 ms before it reaches you and is drawn at your input on the frame it does. Only the clock is blended: the seat keeps its correction, a rival misprediction no client can know of (a host stall starves the room, which holds each seat's input past the repeat cap; the next state brings where the other car stood instead) is still eased in, and on the frame your entity is shoved the other may be drawn short of it by that correction. Drawing a touching seat where your prediction has it instead (its share taken at once, its correction faded with it) moved a car-ball car its whole correction (about 100 u at 200 ms) in a frame: remote snaps went from 5 to 24 a run. Drawn on the seat clock, a car driving into yours was drawn short of it on the frame yours was shoved, by the clock's slew after a host stall and by the throttle's share (up to a quarter of an input): 3 of 10 seeded stalled runs on the virtual-clock session tests (`authority-seat-predicted-bump.test.ts`). `conformance/authority/input-repeat.json`'s `seatTouch` cases hold the share frame by frame. So a bump between your entity and another seat, or between two others, is drawn when and where it happens, on both seats at once, and a seat whose held input was right is drawn exactly where the server will have it. The session reports the tick it drew other seats at (`drawTick`: the state after it, fractional) and as a lead over the server's present (`drawLead`, ms), and each such seat's view carries `predicted` and `correction` (how far it is drawn from its prediction, room units). Then, in `separate` and `predicted`, draw-only, the seat is moved out of your drawn entity and never across it, a seat's old life too while it is drawn on until the render timeline reaches its respawn (at a kickoff every car respawns on one tick, and your car is drawn at its new spot first). *Lifted*: first, a seat that touches your drawn entity and that a move along z of at most `SEAT_LIFT_MAX` (4 units; up when its centre is at or above yours, else down) takes off it is moved by the least such move (20 halvings): it rests on your roof, or you on its, and both drawings put its floor a unit or two through yours from frame to frame; separated across the ground it was pushed out by the whole footprint every frame its floor dipped under (about 90 units at car-ball's 200 ms). Only against your entity: lifting two other seats off each other in the *Pile* left two drawn 27 u inside each other in one of 7 car-ball runs. *Separated*: the contact rule run on copies of your drawn entity and the (lifted) seat at rest (a declared policy's with your entity *fixed*, *Contact policy*: the seat takes the whole push its way allows, along a wall and never into it), the seat moved by its change less yours (the whole push is the seat's; your entity and the state are never moved); run again on where it left the seat while the push is over 0.01 units, up to 8 pushes (a rule whose bodies turn with where they are, laid on a bank or a wall, may leave it a little inside); a seat the rule still pushes more than 0.001 units then goes on along the way the pushes took it from its own pose (no way: the rule's push there) to where it stops touching (doubling from its distance, at least 1 unit, up to 8 times while it touches, then 30 halvings, the far end kept; left where the pushes put it when it touches at the far end). A client keeps, per seat, last frame's drawn offset from your drawn entity, whether that frame drew the seat off its own pose (*held*), the seat's own pose's offset and the frame's time; a new life of either entity, or a gap of more than 250 ms, starts it afresh. *Crosses*: the straight move from last frame's offset to this frame's separated one, sampled at `k/16` (`k` = 1..15), pushes some point out by more than 0.05 of the shorter of the move's length and last frame's distance from your centre, or by more than 8 units (a long move: a seat let go far from its own pose jumps back to it, and 5% of a 250-unit jump let it graze your corner 12 units deep). When the move does not cross, or there is no last frame, the seat is drawn separated. When it crosses and the seat was not held, it is drawn separated unless that offset points away from last frame's (their horizontal dot product below 0); then it is pushed out on the side it came from: along last frame's horizontal heading about your centre, at the separated height, at the nearest distance where it stops touching (*clear*: 0 when it touches nowhere along that heading; else doubling from the larger of the two offsets' horizontal lengths up to 8 times while it touches, then 30 halvings, the far end kept). When it crosses and the seat was held, it goes round your entity: its horizontal heading about your centre turns toward the separated one's along the short arc, by at most the turn left, at least `3π · dt` (`dt` the seconds since last frame) and as much as `π/2 · |b − b′| / ρ′` (`b`, `b′` the seat's own pose's horizontal offset now and last frame, `ρ′` last frame's horizontal distance); its horizontal distance and height move from last frame's toward the separated one's by the same share of the turn, and the distance is never less than *clear* at the new heading and height. A seat pushed out on the side it came from or taken round is then *separated* from where that put it when the rule still pushes it there more than 0.001 units: bodies that turn with where they are can stop touching along a heading and touch again further out, and a car going round yours up a wall was drawn 14 units inside it. Without the round trip a seat the render timeline drew through yours was pushed out on the side nearest it, which flips when the timeline's seat passes your middle, and it was drawn jumping across your car between two frames; going round, it takes as long as the way through. *Pile*: the seats are then kept out of each other, every frame, all together: a seat moved out of yours alone could land in a third car (two seats the timeline draws inside yours are both pushed to your side, and one taken round your car swept through one against it: 105 units deep on the car-ball bench). A client keeps, per pair of other seats, last frame's offset of the higher id's seat from the lower's (forgotten as a seat's) and, for a pair it pushed on a *side*, that side (a horizontal unit vector `n`) and how much further apart along `n` the two were drawn than their own poses (`p`). First, a pair without a side, or one that *crossed* (below), whose offset now points the opposite way from last frame's (horizontal dot product below 0) and whose move between the two *crosses* the lower id's seat (as above, the pair's two seats in place of yours and the seat) is put back on its sides: the seat that moved further from last frame's drawn offset from your entity (a seat without one: the furthest) is placed along last frame's horizontal heading from the other at last frame's height, at *clear* from the other (searched from last frame's horizontal distance), and the other stays; a seat let go off yours jumps back to its own pose in one frame, and this keeps it from jumping over a car it was pressed against. Then up to 12 passes, in id order: each seat drawn inside yours is separated from yours unless the straight move from where *separated* or *goes round* first drew it this frame to that point *crosses* your entity (then it is drawn there again); then every pair, in id order: a pair whose own poses (the seats' poses before any of this) touch, or that has a side from last frame, is *pushed on its side*; any other pair that touches is separated by the rule, the push shared half each (`a` the lower id moved by minus half, `b` by plus half). *Pushed on its side*: a pair takes its side on the first pass it is pushed in a frame and keeps it for the frame: last frame's side, else the axis the rule pushes `b` along (both drawn, at rest), turned toward last frame's offset of `b` from `a` (else this frame's drawn offset) when it points away from it, or that offset's direction when the rule does not push, or +x; a pair with a side from last frame whose own poses no longer touch and point against it (their offset's horizontal dot product with `n` below 0: they *crossed* while pushed) is let go and separated by the rule instead, after being put back on its sides as above (let go at once, two such were drawn jumping across each other on the car-ball bench). The pair is pushed apart along `n` by the least `k` from a floor up that leaves the rule nothing to push, `a` by `−k/2 · n` and `b` by `+k/2 · n` (`k` the floor when the two do not touch there; else doubling from the floor plus 64 up to 8 times while they touch, then 24 halvings, the far end kept); the floor is 0, but on a frame's first pass of a pair with a side from last frame it is `max(0, p − SEAT_PAIR_EASE · min(dt, 0.1) − q)` (`SEAT_PAIR_EASE` 720 units a second, `dt` the seconds since that memory, `q` how much further apart along `n` the two are drawn now than their own poses), so a pair the prediction lets go of eases back to its poses, 6 units a car in a 60 Hz frame. A pass in which no pair was pushed ends the passes, and the last pass ends with the step out of yours. Pushed on the side the rule finds nearest each frame, two seats the prediction drew a few units from one centre flipped sides every frame (each car 27 units off its pose one way, then the other: 18 of one car-ball rtt-200 run's 30 contact jumps). Each seat's memory is its final drawn offset from yours, *held* when that is off its own pose; a pair keeps its side and `p` (the final drawing's) while `p` is above 0. A seat squeezed between yours and a third car comes out within a few hundredths of a unit; your entity is never moved and no seat is left inside it. A frame draws the seats once, at its first read (a new state draws them again): drawn afresh at each read, a game reading each seat on its own (`entity(id)`) got the second from a drawing that started from the memory the first had just written, and two cars read one from each were drawn 3.8 units inside each other. `separate` draws a seat off the render timeline only near yours, which the car-ball bench measures as remote-car pops and hitches (one or two at each near miss); `predicted` draws every seat on another clock than the render timeline's, which no fixed-clock judge fits through a bump, so the bench judges it by one-frame snaps and velocity jumps at the instant each frame drew (`snapsOf`, `lurchesOf`) instead. A `clock` drawing (a seat near yours moved on by its velocity onto your clock, blended into the timeline by distance) was measured and removed: every keyframe restarted its guess with nothing to ease the change, 58-750 hitches a 30 s run. No conformance vectors of its own (drawing, like §17.3's interpolation; car-ball's `conformance/carball/carball-separate.json` holds it with that room's rule, single seats and piles); `multiplayer/test/authority-seat-{separate,separate-flip,separate-pile,drawing,predicted-held,predicted-bump,predicted-pair,predicted-brake,predicted-clock}.test.ts`, `authority-seat-predict.test.ts` and netcode-bench's `carball-separate.test.ts` hold the TypeScript behaviour.
1205
+
1206
+ **Budgets.** A replay's cost is the wall clock around one adoption's rebuild and replay (nothing else). The client judges a rolling window of the last `PHYS_BUDGET_WINDOW` (60) costs, once it holds `PHYS_BUDGET_MIN_SAMPLES` (20), against `PHYS_RESIM_BUDGET_MS` (4 ms desktop, 5 ms mobile; a client's explicit `physicsBudgetMs`, a finite number over 0, replaces it, for benches and CI that hold every client to one budget on a slow host): over budget when the window's median is over the budget or its p90 over `PHYS_BUDGET_P90_FACTOR` (2) budgets, so a single slow replay (a GC pause, the OS taking the thread) never decides. Over budget, the simulated set shrinks to the next of 128, 64, 32 that is smaller than the bodies the last choice could take (a size that would choose the same bodies is skipped); with none left the client draws every body as §17.10 says, status `cpu`, and the window starts again after every change. A full window whose p90 is at most `PHYS_BUDGET_CALM_SHARE` (half) of the budget grows a shrunken set back one size. `cpu` is not final: every `PHYS_CPU_PROBE_MS` (500 ms) one keyframe is adopted by a full replay (a probe, not drawn); after `PHYS_CPU_CALM_PROBES` (4) probes in a row each at most half the budget the status is `on` again, drawing from the probe's world (no correction from the §17.10 drawing), and a probe over half the budget starts the count again. `physicsPrediction` reports the slowest replay and the latest (`peakResimMs`, `lastResimMs`) and how many replays took longer than a 60 Hz frame (`slowResims`, over `RESIM_FRAME_MS`): each held that frame back.
1207
+
1208
+ **Fallback levels.** A client draws §17.10's way when: (1) the room's `physicsHash` or `physDesc` is not its world's (`mismatch`); (2) the core fails a self-test scene at load (a world stepped, canonicalized with quantize, and a second world built from the same descriptors importing its rows must step on with equal stateHashes; `self_test`); (3) more than 2% of pure bodies (in islands without a kinematic body and without a seat near them where this keyframe or the one before has it, so the bodies a seat leaves, by a respawn or between keyframes, are no sample; not respawned) mismatch over the last 500 compared at adoption, judged from 200 (`diverged`); only a body the client world predicted for that keyframe is compared, so one that just entered the simulated set (back into view, or its island whole again) is no sample; (4) a kill switch: the room's `predict: false`, the operator's `AUTHORITY_PHYSICS_PREDICT=off`, or the client's `predictPhysics: false`. The core loads only once a room sends keyframes; a browser without WebAssembly SIMD (Safari before 16.4) loads the same build without SIMD (`packages/multiplayer/vendor/`), which gives the same stateHashes.
1209
+
1210
+ **Replay and validation.** The replay and `validate` run a predicting room exactly as a live one (the clamp and canonicalize at the same ticks), so their `stateHash` after each tick equals the live room's.
1211
+
1212
+ **Engine bridge.** The authority bridge is contract version 3: `bodiesF32(h)` answers every physics body as drawn now, 8 float32s each (position, orientation `[x, y, z, w]`, flags: 1 predicted, 2 asleep) in the order `bodyKeysJson(h)` names; `setAuthorityPhysicsCore(load)` hands the host page's core over, and the open options take `predictPhysics`.
1213
+
1214
+ **Conformance.** `packages/netcode-bench/conformance/authority/physics-predict.json`, printed byte for byte by `physics-predict.reference.mjs` beside it (written from this section and the pinned core only). It is what every engine's predictor is held to. Top level: `predict` (`"physics"`), `description`, `encoding`, `build` (the core's `buildId` that printed it), `tolerance` (1e-4) and `cases`. A case has `name`, `about`, `tickHz`, `physDesc`, `physicsHash`, `staticWorld`, `remoteModel` (null, or `{ kind: "axes", speed }`: a remote seat moves by its movement axes times `speed` m/s each tick, the `inputDecay` step), `descs` (each body key's canonical descriptor, shapes inlined) and `events` in order, one per line:
1215
+ - `{ kind: "send", at, seq, own }`: your input `seq` leaves at room-clock `at` ms; `own` is your entity's position after it;
1216
+ - `{ kind: "keyframe", at, keyframe }`: a keyframe arrives: `tick`, `ack`, `bodies` in view (`key`, `row` the 15-number wire row), `outside` (bodies out of view: `key`, last `x y z`, `radius`), `joints` (`key`, `desc`, `motor`), `seats` (`key`, float32 `x y z`, velocity `vx vy vz` in room units per second, `epoch`, `own`, and `input` when the room carries one);
1217
+ - `{ kind: "draw", at, key }`: draw that body.
1218
+
1219
+ A port builds its world from `physDesc` and `staticWorld`, runs every event through its predictor and must give: `expect.adoptions` exactly (each keyframe's `tick`, `kind` `match` or `replay`, inputs `replayed`), `expect.hashes` exactly (the client world's `stateHash` after every predicted tick, in order: tolerance 0) and each `expect.draws` pose within `tolerance` (`null`: not drawn by the predictor). The cases: lone push, remote push, module impulse, view churn, spawn, epoch, four inputs a tick, stack, CCD, sleep, an asymmetric pair touching a ball on one tick (seen by a 10 ms and a 200 ms client, 120 Hz, K 2), remote input decay (120 Hz, K 2), a body rule (120 Hz, K 2), a seat rule (120 Hz, K 2: another seat held until a release, `seatRule` `release`, its seat row carrying `body.cap`, which the `axes` model multiplies) and a kinematic rule (60 Hz, K 2: a deck slid by the case's `kinematicRule`, `slide`: body `key` at tick t at `from` moved by `v` m/s for min(t, `ticks`) ticks, unturned, with a crate riding it). A case's `bodyRule` is null or the room's body rule, `kick` (each seat, key order, whose preset body's centre is within `reach` of body `key`'s centre and closes on it adds `speed` along the line from that centre), and a `send` event of such a case carries `ownV`, your predicted velocity after the input. The TypeScript SDK passes them on both the SIMD and the non-SIMD module.
1220
+
1221
+ `packages/netcode-bench/conformance/authority/seat-contacts.json`, printed by `seat-contacts.reference.mjs` beside it, holds *Seat contacts*: each case is one client's `state` events (the ack, your entity, the other seats) and `send` events, with the case's step (a drift by velocity, float32) and rule (`bump`, spelt out in the file); a port must give your entity after every event exactly (tolerance 0). The cases: head-on, moving away, a replay after a swerve, two others either side of your id, a new state mid-pass, and a kickoff release (a case's `seatRule` `release` run on your entity before each input's step, at the state's `tick` plus `seq − ack`; the case's step moves a held entity by its velocity times its `cap`).
1222
+
1223
+ `packages/netcode-bench/conformance/authority/contact-policy.json`, printed by `contact-policy.reference.mjs` beside it (written from *Contact policy*, importing nothing), holds the contact policy over a test yard (`world`: a floor, four walls, a 22° and a 63° ramp, a goal pocket with a crossbar, a box canyon, a corridor and a ring of walls at head height) and named `policies` (a declaration and its movement preset's params each): `config` (declarations resolved, or the refused path), `classify` (a pair's `kind`, `n`, `depth`, `upperIsB`), `pairs` (one pair step per policy, each with the outcome's `j`, `way` and the way's unit `w`, what the module heard, and both seats after; the `stand: …` rows hold the stand test: a centred and an off-centre landing, the edge band resting, parting and walked away from, past the footprint, anywhere with `center` null, and a box's heading frame), `yield` (side shares against a wall and in a corner), `passes` (named piles — open floor, goal mouth, goal corner, side wall, arena corner, ramp, airborne lower, lower on a slope, box canyon — and 120 seeded ones: the report's `passes`, `pinned`, `pushed`, `walls` and every seat after) and `ride` (the rides and the seats after). Every number is a float64: a port gives every field exactly. The physics replay's contact pass (*Adopt and replay* step 3) is `physics-predict.json`'s case `contact policy in the replay` (its `contactPolicy`: a declaration and the movement preset's params; three other seats piling up beside a crate, the room's pass on them every tick, the client's on each replayed tick).
1224
+
1225
+ **Engines.** Unity, Unreal, Rust and Godot (a GDExtension, Godot 4.3+) predict on Genex Physics' C ABI: the same core as the WebAssembly module, built from the same commit, which each SDK vendors with a `PROVENANCE.json` pinning every file's sha256 (`packages/netcode-bench/test/authority/physics-capi-vendored.test.ts` holds the four copies to their pins and to each other). It is loaded at run time, never linked, and refused unless its ABI version is 1 and its `buildId` is the pinned build's; a missing or refused library leaves the client drawing as §17.10 says. Each engine's predictor passes every case of `physics-predict.json` on it (every adoption and client stateHash exact, every draw within the tolerance), and its join sends `physics@2` only with a core and prediction on. Its conformance driver advertises `physicsPredict` once the core loads, is the pinned build and passes the self-test scene, and predicts only when `authorityConnect` says `predictPhysics: true` (DRIVER.md).
1226
+
1227
+ ### 17.12 The fast channel (`fast@1`)
1228
+
1229
+ A seat may move its inputs and its motion state off the WebSocket onto an unordered, unreliable *fast channel*: raw UDP for the engines, WebTransport datagrams in the browser. On the WebSocket one lost TCP segment holds every input and every patch behind it for a round trip or more (0.7–1.08 s measured on a home Wi-Fi link), and then about 126 inputs land at once. On the fast channel each datagram stands alone: the uplink carries each input in several datagrams, and a downlink snapshot that is lost is replaced by the next one. Everything else stays on the WebSocket (*the reliable channel*): join and auth, room state other than the motion records, game events, chat, admin, reconnection. The machine-readable copy is `src/authority/fast-transport.ts` (the tunables, `encodeUplink`/`decodeUplink`, `acceptUplink`, `uplinkFor`, `diffSnapshot`/`encodeSnapshot`, `decodeFragment`, `receiveFragment`). It is pure: the authority host, the TypeScript client and every engine compute the same bytes and the same decisions, held to `test/vectors/fast-transport.json`.
1230
+
1231
+ **Status.** The authority host serves the channel over raw UDP (`fast-endpoint.ts`). Four clients speak it over raw UDP, each passing the vectors with zero skips and a live check against a real room and endpoint on loopback: the TypeScript client in Node (`connectAuthority({ fast: openUdpFastSocket })`, *Client* below), Unreal's GenexCore (`AuthorityOptions::fast`, a socket opener; `FGenexAuthorityOptions::bFastChannel` in the plugin), the Rust SDK (`AuthorityOptions::fast(true)`) and the Unity core (`GenexAuthorityOptions.Fast`; not on WebGL, which has no sockets). Not yet in Godot: a Godot seat never sends `fast@1` and plays as a WebSocket seat. A browser reaches the same endpoint through a WebTransport relay (*WebTransport* below): the TypeScript client in the browser (`connectAuthority({ fast: openWebTransportFastSocket })`, `@genex-ai/multiplayer/authority/fast-webtransport`), live on loopback in headless Chromium through the relay. A browser without WebTransport, or a room whose worker has no relay, is a WebSocket seat as before.
1232
+
1233
+ **Capability.** A client that implements this section sends `fast@1` in `caps` beside `tick@1` (every slot carries the input's tick). A room that offers the channel answers a `fast@1` join with the reliable message `__fast` `{ sid, key, url }` (`FastMessage.Bind`; reserved, so never a game event): `sid` a uint32 that names the seat on the channel, `key` 8 random bytes as 16 hex digits (`FAST_KEY_BYTES`), minted per socket, and `url` the endpoint (`udp://host:port` or an `https://` WebTransport URL). A room that does not offer it sends nothing and the seat stays a WebSocket seat; a client must never assume the channel before `__fast` arrives.
1234
+
1235
+ **Encoding.** Little-endian. Every datagram starts with `version` uint8 (`FAST_VERSION` = 1) and `kind` uint8 (`FastKind`: 1 uplink, 2 snapshot fragment). No datagram is longer than `FAST_MAX_DATAGRAM_BYTES` (1200: the IPv6 minimum MTU 1280 less IP, UDP and QUIC headers; WebTransport datagrams fit it on every path). A receiver drops, with no side effect at all, a datagram that is longer, shorter than its header, of another version or kind, that has bytes left over or that breaks any rule below (`FastFault` names each reason).
1236
+
1237
+ **Session binding.** Every uplink datagram carries `sid` and `key`, and the server checks both against the seat's binding before it reads anything else: a wrong `sid` or `key` is dropped (`session`, `key`). The first datagram that passes binds the seat's source address (on UDP; a WebTransport session is its own binding), and a later one that passes from another address rebinds it (a NAT rebinding), so the key, never the address, is the credential. The key travels only on the reliable channel (TLS) and in the seat's own datagrams; a reconnect mints a new one and the old one is refused from then on.
1238
+
1239
+ **Endpoint guards** (authority-host `fast-endpoint.ts`). Before a seat reads a datagram, the server's UDP endpoint checks, cheapest first and with no side effect: the operator's switch, the size (at least the header through the key, at most `FAST_MAX_DATAGRAM_BYTES`), the version and kind bytes (an uplink), a token bucket per source address (`FAST_ADDRESS_DATAGRAMS_PER_S`, eight seats' worth for a carrier NAT; the table holds `FAST_ADDRESSES_TRACKED` sources, the least recently seen giving way), the sid, the key in constant time, then a token bucket per seat (`FAST_SEAT_DATAGRAMS_PER_S`, four datagrams a tick at 120 Hz). A wrong key spends no seat tokens, so a flood naming a sid cannot starve its holder. Each drop is counted by its `FastDrop` reason (`disabled`, `size`, `header`, `address_rate`, `unknown_sid`, `key`, `seat_rate`, and `refused` for a keyed datagram the seat's own checks drop). Nothing is sent to an address before a datagram from it passed its seat's key, so the endpoint never reflects a stranger's datagram. The operator's switch rides every admission grant and usage answer (`fast`): a grant with it false (or without it) offers no seat of that room the channel, and off on an answer the endpoint drops everything and sends nothing, so each fast seat falls back by the rule below; on again, bound seats are heard.
1240
+
1241
+ **Uplink datagram** (`kind` 1), 29 bytes of header:
1242
+
1243
+ | Field | Type | Meaning |
1244
+ |---|---|---|
1245
+ | `sid` | uint32 | the seat's channel id |
1246
+ | `key` | 8 bytes | the seat's binding key |
1247
+ | `ackSnap` | uint32 | the newest snapshot the client applied (0: none) |
1248
+ | `newest` | uint32 | the wire seq of the newest input in the window (at least 1) |
1249
+ | `count` | uint8 | inputs in the window, 1 to `INPUT_REDUNDANCY_MAX` (32) |
1250
+ | `fresh` | uint8 | how many of the window's newest inputs this datagram sends for the first time, 1 to `count` |
1251
+ | `actions` | uint8 | held one-shot inputs before the window, 0 to `FAST_MAX_ACTIONS` (8) |
1252
+ | `render` | uint32 | the newest render time of the window's slots (0: none is stamped; *Render-time stamp* below) |
1253
+
1254
+ Then `actions` held inputs, ascending seq, each `seq` uint32, `tick` uint16, `render` uint32 (its render time, 0: unstamped) and the payload; then the window's `count` *slots*, oldest first, slot `i` being input `newest − count + 1 + i`. A slot is a `flags` uint8 (bit 0 `same`: the payload is the previous slot's; bit 1 `tickSame`: the tick is the previous slot's; bit 2 `tickNext`: it is the previous slot's plus one, modulo 65536), then `tick` uint16 unless a tick bit is set, then `back` uint8 (its render time is the header's `render` − `back` ms; 255, or a difference at or below 0: unstamped), then the payload unless `same`. The first slot sets no bit, `tickSame` and `tickNext` never together, and no other bit. The payload is `AuthorityInput`'s six fields (§17.3): `moveX` `moveY` `moveZ` `yaw` `pitch` float32 and `buttons` uint32, 24 bytes. A held car is two bytes a slot; the largest datagram (32 slots and 8 actions, all full) is 1197 bytes. The tick is §17.11's *input's tick*, modulo 65536. Each action's seq is below the window's first. The server sanitizes every payload as §17.3 says (a NaN axis is 0, buttons at or past 2^24 are 0); a payload is data, never a reason to drop.
1255
+
1256
+ **Render-time stamp.** Lag compensation (§17.5) judges a FIRE input at the render time its sender drew the world at; on the WebSocket that stamp rides each input frame (Colyseus's TIMED prefix: `round(serverNow) − round(renderDelay + rtt/2)`, whole ms, 0 before the clock syncs) and the room reads it as `rewind.lastSeenBy(sid)`, the stamp of the seat's last consumed WebSocket input. A fast seat's inputs reach the server by datagram, so each carries its own: an action's `render`, or a slot's `back` against the header's `render` (a slot more than 254 ms behind the newest is sent unstamped: a window spans 170 ms, so only an input from before a long hitch is). Every fast client stamps each input, as it sends it, with the value its WebSocket frame would carry (TypeScript: the input handle's own stamp; Unreal, Rust, Unity: `renderStamp` on their room clock and the handle's render delay, held to the `renderStamp` vectors); Godot has no fast channel (*Status*). `acceptUplink` yields each accepted input with its render time (a filled input takes the render time of the input after its gap); the room judges a fast seat's lag-compensated input at it (`rewind.at(render)`), and at `lastSeenBy` only an unstamped one. Before, a fast seat's FIRE was judged at its last WebSocket stamp: TCP-late for a client that keeps its input frames on the socket (Unreal, Rust), and frozen at the binding for one that keeps them off (TypeScript, Unity), so clamped to `MAX_REWIND_MS` (500 ms) behind. One stamp a datagram would not do: a datagram carries the window's older inputs again, and a recovered input (its first datagram lost), or a frame of catch-up inputs after a hitch, was drawn at an earlier render time than the datagram that delivers it.
1257
+
1258
+ **Wire seq.** The seq is the input's client sequence (§17.9's `cseq`): a fast seat numbers its inputs from `seqBase + 1`, one per input, and never reuses one. It is a uint32 that never wraps: a seat that would pass 2^32 − 1 (414 days at 120 Hz) reconnects, so a seq at or below the last accepted one is always stale, including 0 after 2^32 − 1.
1259
+
1260
+ **Redundancy and one-shot inputs.** A client sends one datagram whenever it sends inputs (each frame), whose window is its last `inputRedundancyTicks(tickHz)` inputs (`INPUT_REDUNDANCY_MS` = 170 ms of them, at least 3 and at most 32: 21 at 120 Hz, 11 at 60, 6 at 30). An input with a *press* (a button bit its previous input lacked) that has slid out of the window and that the server has not yet acknowledged (`inputAck` below) rides as an action until it is, the oldest `FAST_MAX_ACTIONS` of them (`uplinkFor`). The server takes every slot oldest first, so the first datagram it accepts past a press carries that press: a jump or a shot is never lost, only late.
1261
+
1262
+ **Server dedup** (`acceptUplink`, per seat: the last accepted seq and input, the newest `ackSnap`). The datagram is checked whole before any of it is used: a `newest` more than `FAST_SEQ_AHEAD_MS` (5 s) of ticks past the last accepted seq is dropped whole, with no side effect (a seq that far ahead would make every honest later input stale). Then each held input and slot, ascending, goes through Colyseus `InputBuffer.accept(seq)`'s rule: a seq at or below the last accepted is a duplicate or stale and is dropped with no side effect; a greater one is accepted. A seq past the last accepted plus one leaves a *gap* of inputs no datagram the server got carried: up to `gapFillMax(tickHz)` of them (`FAST_GAP_FILL_MS` = 250 ms) are *filled*, each a copy of the last accepted input with no press of its own (the buttons held as they were), and accepted before it; a longer gap is *skipped* (its seqs are consumed and never stepped; the client was gone longer than a repeat would stand in). An accepted input is *recovered* when its seq is below the datagram's `newest − fresh + 1`: it reached the server by redundancy after the datagram that first sent it was lost. `ackSnap` moves the seat's snapshot ack forward only, and only to a snapshot the server sent.
1263
+
1264
+ **Downlink snapshot** (`kind` 2). Each seat gets its own snapshots: the room's *motion records* (below), delta-coded against the newest snapshot that seat acknowledged. A fragment has 29 bytes of header: `sid` uint32 · `snap` uint32 (the snapshot id, from 1, rising by at least one per snapshot) · `base` uint32 (the snapshot the delta is against; 0 for a keyframe) · `tick` uint32 (the server tick it was written at) · `inputAck` uint32 (the seat's last accepted input seq) · `flags` uint8 (bit 0 *keyframe*, set exactly when `base` is 0) · `frag` uint8 · `frags` uint8 (`frag < frags ≤ FAST_MAX_FRAGMENTS`, 32) · `sent` uint32 (the room clock, ms since room start, when the server wrote the snapshot: the `sNow` a WebSocket patch's TIMED prefix carries, §16.1), then that fragment's slice of the body. The body, split into slices of at most `FAST_MAX_DATAGRAM_BYTES − 29` bytes, is `removed` uint16 and that many record keys uint16, ascending, then `records` uint16 and that many records, ascending by key, each `key` uint16, `len` uint16 (at most `FAST_MAX_RECORD_BYTES`, 1024) and `len` bytes. A delta lists the records new or changed since `base` and the keys gone since it; a keyframe lists every record and removes nothing. The server keeps its last `FAST_SNAPSHOT_HISTORY` (32) snapshots per seat and writes a delta against the seat's `ackSnap` while it holds that snapshot, else a keyframe. A body over `FAST_MAX_FRAGMENTS` fragments is not sent on this channel.
1265
+
1266
+ **Motion records** (`src/authority/fast-records.ts`, `encodeRecord`/`decodeRecord`). Each record is one entity's or one seat's: `kind` uint8 (`MotionRecordKind`: 1 entity, 2 seat), `idLen` uint8 and that many UTF-8 bytes of the entity id or seat session id (1 to 64 characters), a `mask` uint32 whose bit *i* says field *i* is present, then each present field in table order. An entity's fields, bit 0 up: `x` `y` `z` `vx` `vy` `vz` `yaw` `pitch` `duck` `spin` `fuel` float32; `epoch` `mstate` uint32; `qx` `qy` `qz` `qw` (orientation, [−1, 1]) and `lvx` `lvy` `lvz` `avx` `avy` `avz` (velocity, [−64, 64]) as uint16 codes of the schema's own quantize (clamp, span 2^16 − 2, half-up, so 0 is code 32767 and a fast seat decodes bit for bit what a WebSocket seat decodes); `sleep` (the bit alone); `rest` uint16. A seat's: `inMoveX` `inMoveY` `inMoveZ` `inYaw` `inPitch` float32; `inButtons` `inRep` `inStarve` `inBuf` `inAck` uint32, `inAck` being, in the receiving seat's own record only, its last input the server consumed (stepped or skipped) as of the snapshot, which its reconciler replays from (the header's `inputAck` is the last *accepted*, which trims actions). A field at its default (0, −0, false, never written) is left out and a field present at its default is a fault, so the bytes are canonical and a delta carries a record only when one of its fields changed; a field left out reads as its default. The entity's other fields (`kind`, `owner`, `hp`, `flags`, `data`, projectile and `cap` fields) and everything else in the state stay on the reliable channel. The server gives each live id a record key (uint16) of its own for the room; a client reads the id from the record, never from the key. A decoder drops, with its `FastFault`, a record of another kind, one that is truncated or has bytes left over, an empty, over-long or malformed id, an unknown mask bit, or a present field at its default, non-finite or past its span; a client leaves that id as it was.
1267
+
1268
+ **Applying a snapshot** (`receiveFragment`, the client). A fragment whose `sid` is not the seat's is dropped. Fragments are gathered by `snap`; one whose header differs from its set's first (`base`, `tick`, `inputAck`, `flags`, `frags`, `sent`), or a duplicate, is dropped; at most `FAST_REASSEMBLY_SLOTS` (4) sets are gathered at once, the oldest giving way. A complete set is applied when its `snap` is newer than the newest applied and its base is a snapshot the client still holds (its last `FAST_SNAPSHOT_HISTORY` applied) or it is a keyframe; otherwise it is dropped, and so is a body that does not parse to the byte. Applying it copies the base's records, removes the removed keys, writes the records, and makes it the newest: its id is the next `ackSnap`, and its `inputAck` the ack `uplinkFor` trims actions by. Applying drops every incomplete set at or below the new snapshot: a lost fragment loses its snapshot, never the next one.
1269
+
1270
+ **WebTransport** (`apps/fast-relay`; `fast-webtransport.ts` the browser socket). A browser has no raw UDP, so a worker whose droplet runs the relay adds `wt` `{ url, hashes }` to every binding: `url` the relay's session for this worker, `https://<relay>/w/<n>` with `n` the worker's offset from port 2567, and `hashes` the SHA-256 (64 lowercase hex digits each, at most `FAST_WT_HASHES_MAX` = 4) of the certificates the relay may serve, for `serverCertificateHashes`. Every client but a browser ignores `wt`; a binding without one, or with one a browser cannot use (not `https://`, credentials, a query or fragment, a malformed hash), leaves a browser seat on the WebSocket. The browser opens the session with `requireUnreliable` and refuses one whose `reliability` is `reliable-only` (WebTransport over HTTP/2, a TCP stream); a refused or failed session is a fallback at once (`failed()` on the socket, reason `unreachable`), with no deadline to wait out, and so is one whose `datagrams.maxDatagramSize` (the browser's outgoing limit: it sends only uplinks) is below the seat's largest uplink, `uplinkMaxBytes(tickHz)` = 29 + 8 × 34 + window × 28 bytes (609 at 60 Hz, 889 at 120, 1197 at 240; Chromium 134 reports 1024). Datagrams queued while the session connects (QUIC and HTTP/3 take about two round trips) go when it is ready, the newest 32; the handshake deadline counts from the first queued one. The relay is not an open proxy: its upstreams are a fixed allowlist (`/w/<n>` to worker `n`'s UDP endpoint), any other path is answered 404 before any socket opens, and each session is one connected UDP socket to its worker, so only that worker's datagrams reach the browser. It forwards every datagram verbatim and checks only what needs no key: the size (an uplink at least its header through the key, any datagram at most `FAST_MAX_DATAGRAM_BYTES`), the version and kind bytes (uplinks one way, snapshot fragments the other), a token bucket per session (the worker's seat rate, 480 a second, burst 240) and a cap on sessions per client address (16) and in all (4096); the worker checks everything else, so to the worker a browser seat is one more UDP source. The worker keys its per-address bucket by address and port for the relay's addresses (`FAST_RELAY_SOURCES`), one relay session per browser. Its QUIC starts at a 1232-byte UDP payload (the IPv6 minimum MTU less the IPv6 and UDP headers), so a whole fast@1 datagram fits from the first packet, and a session whose datagrams cannot hold one is closed. Certificates: ECDSA P-256, minted in the relay's memory, valid 13 days; every 6 days the next becomes the one served and a new next is minted, and the relay publishes `{ "v": 1, "hashes": [current, next] }` to a file the workers read at most once a second (`FAST_WT_HASHES_FILE`), so a binding sent just before a rotation still matches. The relay's key never enters a worker. Worker configuration: `FAST_WT_ORIGIN` (`https://host[:port]`; unset, no `wt`), `FAST_WT_HASHES_FILE`, `FAST_RELAY_SOURCES`.
1271
+
1272
+ **Fallback and mixed rooms.** A room mixes fast and WebSocket seats; a WebSocket seat is exactly as before (§17.3, §17.11). A fast-capable client falls back to the WebSocket for the rest of the socket when no snapshot arrives within `FAST_HANDSHAKE_MS` (1 s) of its first datagram, or none for `FAST_STALL_FALLBACK_MS` (2 s) later: it sends the reliable message `__fast:off` `{ last }` (`FastMessage.Off`, the last seq it sent) and sends its next input by `room.input()`. The server drops any WebSocket input the seat sent before the message, counts the WebSocket inputs on from `max(last, accepted)` (the first is `max(last, accepted) + 1`, so the client sequence never repeats), and feeds what it accepted by datagram first. A datagram the client sent before its message can reach the server after it (another path, or a socket read later: the Unreal `off` run lost one input of 117 so), so until the seat has accepted through `max(last, accepted)` or `FAST_OFF_GRACE_MS` (100 ms, `authority-host/src/fast-seat.ts`) has passed since the message, the server still reads the seat's datagrams, taking only their inputs through that seq, and steps none of its WebSocket inputs (a starved tick meanwhile is an ordinary starve); then it stops reading the seat's datagrams (its binding is closed), and seqs sent by datagram that never arrived are consumed without a step. A `last` that is not a uint32 within `FAST_SEQ_AHEAD_MS` of the accepted seq, a second `__fast:off`, or one from a WebSocket seat changes nothing. On the authority host the client sequence of a consumed input is today `seqBase + consumedCount` (`room.ts` 606–631, the reliable implicit count); a fast seat's is the entry's wire seq, which a fill keeps contiguous and only a skip moves past the count, so the repeat ledger (`repeat@1`) and every `ackSeq` key on seq, not on count. A fast seat's motion records reach it by snapshot (*Downlink* below), and the client keeps the snapshot's (*Client* below).
1273
+
1274
+ **Downlink.** In a physics room (§17.10: every client has a view) the state's entities are `AuthorityPhysicsEntity`, `AuthorityEntity`'s fields in its order under its name (decoders and the handshake's type description are unchanged), whose motion fields (an entity record's, *Motion records* below) carry the Colyseus view tag `MOTION_VIEW_TAG`; every entity enters a view with it. Once a fast seat has acknowledged a snapshot (`ackSnap` past 0) while its binding is open, each entity its view introduced in an earlier patch leaves its view's tag, silently (no delete reaches the client): its WebSocket carries that entity's other fields, the seat echoes and the root's `tick` and `keyframe`, never its motion. When the channel closes (the seat's `__fast:off` or the server's), or the socket reconnects, every such entity's motion is sent again in full in the next patch (or the reconnect's first state). A room without physics writes motion to every seat's WebSocket as before. **Liveness.** A datagram that passed a seat's binding proves its client alive: it answers the WebSocket transport's heartbeat for the seat's socket (uWebSockets.js, the production transport, closes a socket only after `idleTimeout`, 60 s, without a frame; every server on Colyseus's ws transport, the dev server, the bench's rooms and parks, runs its heartbeat at that deadline, 4 unanswered 15 s pings, `WS_TRANSPORT_HEARTBEAT`, where its default of 2 unanswered 3 s pings closed seats production keeps through a long route's TCP stall), so a TCP stall the datagrams outlive never closes a playing seat. A fast seat whose socket did drop keeps its snapshots, of the view it dropped with, through the reconnection grace, while its datagrams still feed the module.
1275
+
1276
+ **Client** (`src/authority/fast-client.ts`, `fast-session.ts`; `fast-udp.ts` the Node socket). A client with a socket opener sends `fast@1`. It takes `__fast` once the socket's state is decoded (its `seqBase` is the player's), and an unusable one (not a uint32 sid, a 16-hex-digit key and a url, or a url it cannot open: in Node anything but `udp://host:port` with a port in 1..65535, an IPv6 host in brackets, and no credentials, path, query or fragment) is a fallback at once. Its wire seq is `seqBase` plus the input's count on the socket. The inputs it sent by WebSocket before the binding arrived were dropped by the server unstepped: its first datagrams carry them again, and whenever more new inputs are due than one window holds it sends one datagram per window's worth, oldest first, so none is stepped as a fill. While the channel is live the server drops its WebSocket inputs unstepped; its ack is its own seat record's `inAck`; and it writes each applied snapshot into its state as a patch would (each record's fields through the schema, a field the record leaves at its default unwritten where the state never wrote one, with the state's `tick` and, in a predicting room, which patches only on keyframes, `keyframe` set to the snapshot's `tick`), then runs the state's change listeners, so prediction, reconcile and interpolation never see which channel delivered a state. A WebSocket patch that would write one of those fields for an id the channel holds is put back before any listener runs: a patch held behind a lost TCP segment never moves an entity or the tick back. The client keeps the WebSocket's latest value of each field it holds, and decodes each patch against those values (Colyseus sends a field only when it changes and reports a decoded field only when it differs from the one held). Motion comes by snapshot and the entity list by WebSocket, so an entity can be listed while the newest snapshot does not carry its record: it left your view and the WebSocket's removal is still on its way, or it came back into view by WebSocket first. Its fields then hold another keyframe's motion, so a predicting client keeps its body out of the keyframe it adopts, as one out of view (§17.11 *The client world*), until a snapshot carries it again. Every fast client with body prediction does (TypeScript, Unreal, Rust, Unity), held to the `motionKnown` vectors. Adopted as part of the keyframe, a body a keyframe behind failed the canary's exact comparison: on Mechanism Hall at `near-wifi`, where every radio episode releases snapshots and patches in separate bursts, prediction turned `diverged` for 7 of 28 fast seats a batch and for none on WebSocket. At a fallback, every held field takes the WebSocket's latest value in one patch, so no field keeps a snapshot's value the server has since moved on from (a seat that stopped while the snapshots were cut is drawn where it stopped); in a physics room an entity's motion instead keeps the channel's values until the server's full resend lands (*Downlink*: its WebSocket values date from when the channel went live), so nothing is drawn back where it was then. Each snapshot is a clock sample at its `sent`, with the round trip of the input its `inAck` newly acknowledges (timed from that input's send, which the client keeps for its newest `FAST_SEQ_AHEAD_MS` of inputs: the server consumes an input well after it received it), so a fast seat's clock reads what a WebSocket seat's does; the tick's nominal instant would read up to a step early, by however late the server ran that step. After a fallback the WebSocket ack counts that socket's WebSocket inputs (the dropped ones too), so the client's ack is its count at the fallback plus the WebSocket ack past the inputs it sent by WebSocket before it. A client may keep sending its input frames while live (Unreal and Rust, whose core's input handle exposes neither baseline below) or keep them off the socket (TypeScript and Unity, so a TCP stall holds no inputs); either way an input frame is a delta against the last frame the client *encoded* (the fields that changed since) and its lag-compensation stamp a delta against the last stamp it wrote, so a frame kept off the socket moves both baselines past what the server decoded. A client that keeps frames off must therefore, at the fallback, send its next frame whole (every field) and its next stamp as a delta against the last stamp the server got (the one before the binding); otherwise each input it sends after the fallback is stepped with the fields the server last decoded (a held axis read as 0) and judged seconds in the past. The server may end the channel with `__fast:off` `{}`; the client answers with its own `__fast:off { last }` and falls back as above (no server sends it yet). A seat that falls back before its first datagram loses the inputs it sent by WebSocket before the binding.
1277
+
1278
+ **Pacing** (§17.11 *Input pacing*, unchanged in P1). A recovered input usually arrives one datagram late, so recovery after a short loss is no burst; what stays bursty is a radio queue's delay (70–170 ms episodes on `real-mac-wifi`), which is the same on either transport. Fills and recovered inputs enter the seat's buffer like any other and the pacer is the same: `MAX_INPUTS_PER_TICK_PER_PLAYER` (4, `limits.ts`) and `PACE_MAX_DEPTH_S` (50 ms, `input-pacing.ts`) apply. Proposal, to be measured in P4 before it is adopted: a starve that ends with recovered inputs was loss, not jitter, so it should not hold the cushion for `PACE_KEEP_S` (the excess drains at the next `PACE_WINDOW_S`), and with `repeat@1` recovered inputs and fills pay owed repeats back first (absorbed, `feedWithPayback`), so a recovered burst is not stepped twice. P4 measures starved ticks, own popP99 and remote lurches on fast seats with the cap at 4 and at 2 and `PACE_MAX_DEPTH_S` at 0.05 and 0.1, against the WebSocket base.
1279
+
1280
+ **Tunables** (`fast-transport.ts`, every one a typed constant with its reason):
1281
+
1282
+ | Constant | Value | Why |
1283
+ |---|---|---|
1284
+ | `FAST_MAX_DATAGRAM_BYTES` | 1200 | IPv6 minimum MTU less headers; no IP fragmentation on any path |
1285
+ | `INPUT_REDUNDANCY_MS` | 170 | the longest delay episode measured on `real-mac-wifi` (70–170 ms), the playtest's burst: 21 inputs at 120 Hz |
1286
+ | `INPUT_REDUNDANCY_MIN` / `_MAX` | 3 / 32 | two lost datagrams in a row survive at any rate; 32 full slots and 8 actions still fit 1200 bytes |
1287
+ | `FAST_MAX_ACTIONS` | 8 | presses awaiting an ack beyond the window: 170 ms + a 409 ms p99 round trip holds a few |
1288
+ | `FAST_GAP_FILL_MS` | 250 | longer than the window plus a lost round of datagrams; past it the seat was gone and a fill would invent input |
1289
+ | `FAST_SEQ_AHEAD_MS` | 5000 | more than the 2 s an honest client stays on the channel without a snapshot |
1290
+ | `FAST_SNAPSHOT_HISTORY` | 32 | 533 ms of 60 Hz keyframes, over the 409 ms p99 round trip plus an episode |
1291
+ | `FAST_MAX_FRAGMENTS` | 32 | about 37 KB; at 1% loss a 32-fragment set still lands 72% of the time, larger goes reliable |
1292
+ | `FAST_REASSEMBLY_SLOTS` | 4 | sets in flight at once at 60 Hz over a 170 ms episode's reordering |
1293
+ | `FAST_MAX_RECORD_BYTES` | 1024 | one record never needs more; bounds a hostile length |
1294
+ | `FAST_HANDSHAKE_MS` / `FAST_STALL_FALLBACK_MS` | 1000 / 2000 | a blocked UDP path falls back within a second; a dead one within two |
1295
+
1296
+ **Conformance.** `packages/multiplayer/test/vectors/fast-transport.json`, printed by `scripts/gen-fast-transport-vectors.ts` from the reference and checked byte for byte by `test/fast-transport.test.ts`, holds: `tunables` and `derived` (window, fill and ahead bounds per tick rate); `uplink` (fields and hex, both ways, with `decoded` where a decoder reads back other fields: a slot sent unstamped); `snapshot` (records, base, body and fragments' hex, both ways); `records` (a motion record, its hex and the record a decoder reads back); `dedup` (a seat's accept steps: duplicate, stale, recovered, gap fill, skip, the uint32 top, `ackSnap`, a sanitized NaN payload, a seq too far ahead); `recovery` (a client's inputs, each frame's datagram from `uplinkFor` under a loss pattern with the input ack `ackDelayFrames` late, and what the server accepts, presses included); `apply` (fragments delivered in an order and the client's newest records, ack and gathered sets after each); `renderStamp` (a clock's server time, null before it syncs, a render delay and a round trip, null for NaN, and the stamp *Render-time stamp* gives: `floor(x + 0.5)` whole ms, the delay clamped to [0, 65535], the clock to uint32); `motionKnown` (snapshots delivered to a live client and, after each, which of the entities the WebSocket lists the newest applied snapshot carries an entity record for: the bodies a predicting client keeps in its keyframe, *Client*); `hostile` (by `side`: `uplink` bytes the decoder drops, `snapshot` bytes a client that applied `setup` drops, `accept` datagrams that change no seat, `encode` values the encoder refuses, `record` record bytes the decoder drops, `recordEncode` records the encoder refuses, `null` standing for NaN; each with its `FastFault`). A port decodes, encodes and replays every case exactly, with zero skips, before it sends `fast@1`.
1297
+
615
1298
  ## Appendix: deliberately not in protocol 3
616
1299
 
617
- - **Server-authoritative simulation, prediction reconciliation and lag compensation.** The relay runs no game code.
618
- - **A relay fixed timestep, a typed universal input schema, or rewind stamps.** The §16 heartbeat exists only for the clock. A typed input belongs to a specific authoritative kernel, and the relay has none.
1300
+ - **Server simulation, a fixed timestep, typed input, reconciliation and lag compensation in relay rooms.** The relay runs no game code; the §16 heartbeat exists only for the clock. These belong to authority rooms (§17), a separate room type on separate servers.
619
1301
  - **Remote interpolation on the server timeline.** §10 stays on sender clocks. The server clock serves deadlines and RTT (§16).
620
1302
  - **Colyseus `room.request()`.** The Godot binding does not expose it. Control requests carry `requestId` in the payload instead.
621
1303
  - **Colyseus `validate()` on messages.** A failed validation closes the socket with 4002, which contradicts §1's drop policy.