@genex-ai/multiplayer 0.17.1 → 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.
@@ -1,13 +1,26 @@
1
1
  // src/protocol.ts
2
2
  var PROTOCOL_VERSION = 3;
3
3
  var MIN_PROTOCOL_VERSION = 3;
4
+ var AUTHORITY_PROTOCOL_VERSION = 4;
5
+ var AUTHORITY_MIN_PROTOCOL_VERSION = 4;
6
+ var RELAY_WINDOW = Object.freeze({ min: MIN_PROTOCOL_VERSION, max: PROTOCOL_VERSION });
7
+ var AUTHORITY_WINDOW = Object.freeze({
8
+ min: AUTHORITY_MIN_PROTOCOL_VERSION,
9
+ max: AUTHORITY_PROTOCOL_VERSION
10
+ });
11
+ function inProtocolWindow(sent, window) {
12
+ return typeof sent === "number" && Number.isInteger(sent) && sent >= window.min && sent <= window.max;
13
+ }
4
14
  var RoomType = {
5
15
  /** The plain relay room: joinOrCreate, fullest-first placement. */
6
16
  PLAY: "play",
7
17
  /** The matchmaking search pool. No game session — it only hands out seat reservations. */
8
18
  QUEUE: "queue",
9
19
  /** A capped match arena: seat reservation (matchmade), create (private lobby) or joinById (invite). */
10
- MATCH: "match"
20
+ MATCH: "match",
21
+ /** A server-authoritative room (§17): the server runs the game's room module, clients send input.
22
+ * Served by the authority cluster, never by a relay. */
23
+ AUTHORITY: "authority"
11
24
  };
12
25
  var JoinOption = {
13
26
  /** The game's slug. Must equal the embed token's `slug` claim. */
@@ -19,7 +32,77 @@ var JoinOption = {
19
32
  /** {@link PROTOCOL_VERSION}. Required. */
20
33
  PROTOCOL: "protocol"
21
34
  };
35
+ var AuthorityJoinOption = {
36
+ /** The capabilities this client implements: at most {@link AuthorityCaps.MAX_CAPS} strings of at
37
+ * most {@link AuthorityCaps.MAX_CAP_CHARS} characters; absent = []. Any other value is refused
38
+ * with 400 by every authority room. */
39
+ CAPS: "caps"
40
+ };
41
+ var AuthorityCaps = {
42
+ /** Decodes and draws §17.10's physics bodies. A physics room refuses a join without it (426). An
43
+ * SDK sends it only once its drawing passes `physics-draw.json` and, for an engine SDK, its own
44
+ * probe on the pinned core (§17.10 *Conformance*). */
45
+ PHYSICS: "physics@1",
46
+ /** Also predicts a predicting room's bodies (§17.11): it builds the room's world from `physDesc`,
47
+ * adopts and replays keyframes, and draws its predicted bodies. A client sends it beside
48
+ * `physics@1`, never alone; a room serves the same state to both, so the cap only says what the
49
+ * client does. An SDK sends it only once it passes `physics-predict.json` (§17.11 *Conformance*). */
50
+ PHYSICS_PREDICT: "physics@2",
51
+ /** Reconciles a predicting room's repeated inputs (§17.11 *Input pacing*): it predicts its own
52
+ * entity from the state as stepped through `ackSeq + inRep`, not `ackSeq`. A predicting room repeats
53
+ * a starved seat's last input only for a client that sends it; without it a starved tick steps
54
+ * nothing, as before. An SDK sends it only once it passes `input-repeat.json`. */
55
+ INPUT_REPEAT: "repeat@1",
56
+ /** Collides with, rides and draws a room's declared lifts (§17.11 *Lifts*). A room that declares
57
+ * lifts refuses a join without it (426, `lifts@1 required`): a client that does not pose them
58
+ * would fall through them in its prediction. An SDK sends it only once it passes `lifts.json`. */
59
+ LIFTS: "lifts@1",
60
+ /** Writes into each input the server tick its prediction steps it at (`AuthorityInput.tick`, modulo
61
+ * 2^16; §17.11 *The input's tick*): the server then judges a re-timed input (a tick that steps two of
62
+ * a seat's inputs or none) at the tick it was meant for — a firstperson view on a spinning lift
63
+ * carried by the lift's turn between the two, lag compensation's lift frames. Without it the field
64
+ * is ignored, as before. */
65
+ INPUT_TICK: "tick@1",
66
+ /** Moves the seat's inputs and motion state to the fast channel (§17.12): redundant input datagrams
67
+ * and ack-delta snapshots over UDP or WebTransport, the WebSocket kept for everything else. Sent
68
+ * beside `tick@1`. An SDK sends it only once it passes `fast-transport.json` and its transport
69
+ * tests; a room that does not offer the channel ignores it. */
70
+ FAST: "fast@1",
71
+ MAX_CAPS: 16,
72
+ MAX_CAP_CHARS: 32
73
+ };
74
+ function capsOf(value) {
75
+ if (value === void 0) return [];
76
+ if (!Array.isArray(value) || value.length > AuthorityCaps.MAX_CAPS) return null;
77
+ const ok = value.every((cap) => typeof cap === "string" && cap.length <= AuthorityCaps.MAX_CAP_CHARS);
78
+ return ok ? [...value] : null;
79
+ }
80
+ var AUTHORITY_PHYSICS_UNITS = 6;
81
+ var SIXTIETHS_PER_SECOND = 60;
82
+ var AUTHORITY_PHYSICS_TICK_WEIGHT = { 20: 1, 30: 1, 60: 1, 120: 2 };
83
+ function authorityRoomUnits(physics, tickHz) {
84
+ return physics ? AUTHORITY_PHYSICS_UNITS * AUTHORITY_PHYSICS_TICK_WEIGHT[tickHz] : 1;
85
+ }
86
+ function authorityPhysicsBudgetSixtieths(tickHz) {
87
+ return SIXTIETHS_PER_SECOND / tickHz * AUTHORITY_PHYSICS_TICK_WEIGHT[tickHz];
88
+ }
89
+ var MS_PER_SECOND = 1e3;
90
+ function authorityTickBudgetMs(tickHz) {
91
+ return authorityPhysicsBudgetSixtieths(tickHz) * MS_PER_SECOND / SIXTIETHS_PER_SECOND;
92
+ }
93
+ var AUTHORITY_ENCODER_BUFFER_BYTES = 1048576;
22
94
  var SERVER_OWNED_JOIN_OPTIONS = ["clients", "preset", "winCondition", "config", "matchmade"];
95
+ var AUTHORITY_SERVER_OWNED_JOIN_OPTIONS = [
96
+ ...SERVER_OWNED_JOIN_OPTIONS,
97
+ "moduleSha",
98
+ "tickHz",
99
+ "maxPlayers",
100
+ "lagComp",
101
+ "seed",
102
+ "units",
103
+ "leaseId",
104
+ "region"
105
+ ];
23
106
  var JoinError = {
24
107
  /** A server-owned option was sent. */
25
108
  BAD_OPTION: 400,
@@ -27,8 +110,18 @@ var JoinError = {
27
110
  AUTH_REQUIRED: 401,
28
111
  /** Token for another game ('wrong game'), or the room's guest cap is full. */
29
112
  FORBIDDEN: 403,
30
- /** `protocol` missing or outside [MIN_PROTOCOL_VERSION, PROTOCOL_VERSION]. Update the client. */
31
- PROTOCOL_UNSUPPORTED: 426
113
+ /** `protocol` missing or outside the room type's window ({@link RELAY_WINDOW},
114
+ * {@link AUTHORITY_WINDOW}). Update the client. */
115
+ PROTOCOL_UNSUPPORTED: 426,
116
+ // ---- authority rooms only (§17.2); a relay room never answers these ----
117
+ /** The game's owner is not entitled to server-authoritative rooms. Terminal: do not retry. */
118
+ NOT_ENTITLED: 402,
119
+ /** The owner's room quota, concurrency cap or credits refused a NEW room. Live rooms never close for it. */
120
+ ROOM_QUOTA: 429,
121
+ /** The game has no published room module on this channel, or it could not be loaded. */
122
+ MODULE_UNAVAILABLE: 424,
123
+ /** No authority server can take the room right now, or admission could not be checked. Retry later. */
124
+ AUTHORITY_CAPACITY: 503
32
125
  };
33
126
  var STAGING_ROOM_PREFIX = "preview--";
34
127
  var roomKeyFor = (slug, channel) => channel === "staging" ? `${STAGING_ROOM_PREFIX}${slug}` : slug;
@@ -141,7 +234,12 @@ var CloseCode = {
141
234
  /** The player's session was revoked (signed out from the dashboard). Do not rejoin with it. */
142
235
  REVOKED: 4401,
143
236
  /** A newer session of the same player took the seat (second tab, or the page reloaded). */
144
- SUPERSEDED: 4409
237
+ SUPERSEDED: 4409,
238
+ // ---- authority rooms only (§17.6), terminal ----
239
+ /** The operator or the platform halted the room (kill switch, takedown, disabled game). */
240
+ MODULE_HALTED: 4500,
241
+ /** The game's room module broke its limits or threw; the room is gone. */
242
+ MODULE_FAULT: 4501
145
243
  };
146
244
  var Limits = {
147
245
  ROOM_MAX_CLIENTS: 64,
@@ -207,6 +305,29 @@ var RelayTiming = {
207
305
  /** Embed tokens are re-checked for revocation on this cadence (REVOKED close on hit). */
208
306
  REVOCATION_SWEEP_MS: 3e4
209
307
  };
308
+ var AuthorityTiming = {
309
+ /** Fixed simulation step rate when the module does not choose one. */
310
+ TICK_HZ: 30,
311
+ /** How far behind the server clock clients render other entities (the interpolation buffer). */
312
+ RENDER_DELAY_MS: 100,
313
+ /** The furthest back lag compensation rewinds for one input's `seen` snapshot. */
314
+ MAX_REWIND_MS: 500,
315
+ /** Inputs buffered per seat before the oldest are folded away. */
316
+ INPUT_BUFFER: 32,
317
+ /** State patch cadence. The spike measured its feel gates at 50 ms; 33 is an owner call. */
318
+ PATCH_INTERVAL_MS: 50,
319
+ /** A predicting room repeats a starved seat's last input for at most this long of repeats not yet
320
+ * paid back (§17.11 *Input pacing*), then the seat holds: a vanished player stops instead of
321
+ * driving into a wall. Rocket League's client resends its last 10 frames of input in every packet
322
+ * (83 ms at 120 Hz), the gap its server's repeat bridges; 100 ms is that rounded up to whole ticks
323
+ * at every tick rate (12 at 120 Hz, 6 at 60, 3 at 30). */
324
+ INPUT_REPEAT_MAX_MS: 100,
325
+ /** One starve run is repeated for at most this long; past it the seat holds as before (§17.11 *Input
326
+ * pacing*): a stick steady before a starve is less and less likely to stay so through it, and on
327
+ * car-ball (5 runs a profile) 4 repeats a run at 120 Hz kept its corrections over 20 u at the base's
328
+ * under loss where 12 doubled them. 4 ticks at 120 Hz, 2 at 60, 1 at 30. */
329
+ INPUT_REPEAT_RUN_MS: 33
330
+ };
210
331
  var ClientReconnection = {
211
332
  enabled: true,
212
333
  minUptime: 0,
@@ -284,9 +405,24 @@ var ClockSync = {
284
405
  export {
285
406
  PROTOCOL_VERSION,
286
407
  MIN_PROTOCOL_VERSION,
408
+ AUTHORITY_PROTOCOL_VERSION,
409
+ AUTHORITY_MIN_PROTOCOL_VERSION,
410
+ RELAY_WINDOW,
411
+ AUTHORITY_WINDOW,
412
+ inProtocolWindow,
287
413
  RoomType,
288
414
  JoinOption,
415
+ AuthorityJoinOption,
416
+ AuthorityCaps,
417
+ capsOf,
418
+ AUTHORITY_PHYSICS_UNITS,
419
+ AUTHORITY_PHYSICS_TICK_WEIGHT,
420
+ authorityRoomUnits,
421
+ authorityPhysicsBudgetSixtieths,
422
+ authorityTickBudgetMs,
423
+ AUTHORITY_ENCODER_BUFFER_BYTES,
289
424
  SERVER_OWNED_JOIN_OPTIONS,
425
+ AUTHORITY_SERVER_OWNED_JOIN_OPTIONS,
290
426
  JoinError,
291
427
  STAGING_ROOM_PREFIX,
292
428
  roomKeyFor,
@@ -305,6 +441,7 @@ export {
305
441
  CloseCode,
306
442
  Limits,
307
443
  RelayTiming,
444
+ AuthorityTiming,
308
445
  ClientReconnection,
309
446
  POSE_KEYS,
310
447
  Interpolation,
@@ -0,0 +1,465 @@
1
+ import { I as InputData } from './module-DqtFb5wA.js';
2
+
3
+ /** The datagram format's version, the first byte of every datagram. */
4
+ declare const FAST_VERSION = 1;
5
+ /** No datagram is longer: the IPv6 minimum MTU (1280) less IP, UDP and QUIC headers. */
6
+ declare const FAST_MAX_DATAGRAM_BYTES = 1200;
7
+ /** The binding key's length: 8 random bytes per socket, sent on the reliable channel at join. */
8
+ declare const FAST_KEY_BYTES = 8;
9
+ /** The redundant window as time: the longest delay episode measured on `real-mac-wifi` (70–170 ms). */
10
+ declare const INPUT_REDUNDANCY_MS = 170;
11
+ /** The window's least inputs: two datagrams lost in a row are recovered at any tick rate. */
12
+ declare const INPUT_REDUNDANCY_MIN = 3;
13
+ /** The window's most inputs, and the most a datagram may carry: 32 full slots and
14
+ * {@link FAST_MAX_ACTIONS} actions still fit {@link FAST_MAX_DATAGRAM_BYTES}. */
15
+ declare const INPUT_REDUNDANCY_MAX = 32;
16
+ /** Presses past the window awaiting the server's ack that one datagram carries (the oldest). */
17
+ declare const FAST_MAX_ACTIONS = 8;
18
+ /** The longest gap the server fills with the last input held; a longer one is skipped. */
19
+ declare const FAST_GAP_FILL_MS = 250;
20
+ /** A datagram whose newest seq is more than this far (as ticks) past the last accepted is dropped. */
21
+ declare const FAST_SEQ_AHEAD_MS = 5000;
22
+ /** Snapshots each side holds as bases: 533 ms at 60 Hz, over a 409 ms p99 round trip plus an episode. */
23
+ declare const FAST_SNAPSHOT_HISTORY = 32;
24
+ /** The most fragments one snapshot may have (about 37 KB); a larger body is not sent on this channel. */
25
+ declare const FAST_MAX_FRAGMENTS = 32;
26
+ /** Incomplete snapshots a client gathers at once; the oldest gives way. */
27
+ declare const FAST_REASSEMBLY_SLOTS = 4;
28
+ /** One record's most bytes. */
29
+ declare const FAST_MAX_RECORD_BYTES = 1024;
30
+ /** A client with no snapshot this long after its first datagram falls back to the WebSocket. */
31
+ declare const FAST_HANDSHAKE_MS = 1000;
32
+ /** A client with no snapshot for this long falls back to the WebSocket. */
33
+ declare const FAST_STALL_FALLBACK_MS = 2000;
34
+ /** The reliable messages of the fast channel (§17.12): the server's binding at join and the client's
35
+ * fallback. Reserved names (`__`), so no game event can be mistaken for one. */
36
+ declare const FastMessage: {
37
+ /** Server → client: `{ sid, key, url }`, the seat's binding. */
38
+ readonly Bind: "__fast";
39
+ /** Client → server: `{ last }`, the last seq the client sent by datagram; its inputs go by
40
+ * `room.input()` from now on. */
41
+ readonly Off: "__fast:off";
42
+ };
43
+ type FastMessage = (typeof FastMessage)[keyof typeof FastMessage];
44
+ /** Where a browser reaches the seat's channel (§17.12 *WebTransport*): a relay's WebTransport session
45
+ * `url` (`https://host:port/w/<worker>`) and the SHA-256 hashes (64 lowercase hex digits each) of the
46
+ * certificates it may serve, for `serverCertificateHashes`. The relay forwards the seat's datagrams,
47
+ * byte for byte, to the endpoint `url` names; the binding (`sid`, `key`) is the same. */
48
+ interface FastWebTransport {
49
+ url: string;
50
+ hashes: string[];
51
+ }
52
+ /** The `__fast` message's payload. `wt`, sent when the room's worker has a WebTransport relay, is read
53
+ * only by a browser; every other client ignores it. */
54
+ interface FastBindMessage {
55
+ sid: number;
56
+ key: string;
57
+ url: string;
58
+ wt?: FastWebTransport;
59
+ }
60
+ /** The `__fast:off` message's payload. */
61
+ interface FastOffMessage {
62
+ last: number;
63
+ }
64
+ /** The datagram kinds, the second byte. */
65
+ declare const FastKind: {
66
+ readonly Uplink: 1;
67
+ readonly Snapshot: 2;
68
+ };
69
+ type FastKind = (typeof FastKind)[keyof typeof FastKind];
70
+ /** Why a datagram or a value was refused. */
71
+ declare const FastFault: {
72
+ readonly Oversized: "oversized";
73
+ readonly Truncated: "truncated";
74
+ readonly Trailing: "trailing";
75
+ readonly Version: "version";
76
+ readonly Kind: "kind";
77
+ readonly Session: "session";
78
+ readonly Key: "key";
79
+ readonly Count: "count";
80
+ readonly Fresh: "fresh";
81
+ readonly Actions: "actions";
82
+ readonly Flags: "flags";
83
+ readonly Order: "order";
84
+ readonly Field: "field";
85
+ readonly Ahead: "ahead";
86
+ readonly Fragment: "fragment";
87
+ readonly Mismatch: "mismatch";
88
+ readonly Duplicate: "duplicate";
89
+ readonly Keyframe: "keyframe";
90
+ readonly Body: "body";
91
+ readonly Base: "base";
92
+ readonly Stale: "stale";
93
+ };
94
+ type FastFault = (typeof FastFault)[keyof typeof FastFault];
95
+ /** How the server came by an accepted input. */
96
+ declare const AcceptKind: {
97
+ /** First sent in the datagram that brought it. */
98
+ readonly Fresh: "fresh";
99
+ /** Sent first in an earlier datagram that was lost; carried again by redundancy or as an action. */
100
+ readonly Recovered: "recovered";
101
+ /** No datagram the server got carried it: the last input held stands in for it. */
102
+ readonly Fill: "fill";
103
+ };
104
+ type AcceptKind = (typeof AcceptKind)[keyof typeof AcceptKind];
105
+ type FastResult<T> = {
106
+ ok: true;
107
+ value: T;
108
+ } | {
109
+ ok: false;
110
+ fault: FastFault;
111
+ };
112
+ /** One input on the wire: its client sequence, its `tick@1` field, its render time and its payload. */
113
+ interface FastInput {
114
+ seq: number;
115
+ tick: number;
116
+ /** The render time its sender drew the world at when it sent it (§17.5): the room clock, whole ms,
117
+ * as its WebSocket input frame's stamp would carry it; 0: not stamped (no synced clock). */
118
+ render: number;
119
+ input: InputData;
120
+ }
121
+ /** An uplink datagram's content. `window` is contiguous, oldest first; `actions` ascend below it. */
122
+ interface Uplink {
123
+ sid: number;
124
+ key: string;
125
+ ackSnap: number;
126
+ fresh: number;
127
+ actions: FastInput[];
128
+ window: FastInput[];
129
+ }
130
+ /** A seat's fast-channel binding, from the reliable `fast` message. */
131
+ interface FastBinding {
132
+ sid: number;
133
+ key: string;
134
+ }
135
+ /** The redundant window at `tickHz`: {@link INPUT_REDUNDANCY_MS} of inputs, within its bounds. */
136
+ declare function inputRedundancyTicks(tickHz: number): number;
137
+ /** The longest uplink a seat at `tickHz` sends: the header, {@link FAST_MAX_ACTIONS} actions and a full
138
+ * window of slots, each at its largest (a socket that cannot carry it cannot carry the channel). */
139
+ declare function uplinkMaxBytes(tickHz: number): number;
140
+ /** The longest gap filled at `tickHz` ({@link FAST_GAP_FILL_MS} of ticks). */
141
+ declare const gapFillMax: (tickHz: number) => number;
142
+ /** The farthest a datagram's newest seq may be past the last accepted at `tickHz`. */
143
+ declare const seqAheadMax: (tickHz: number) => number;
144
+ /** Lowercase hex of `bytes`. */
145
+ declare function toHex(bytes: Uint8Array): string;
146
+ /** The bytes of hex text `hex` (an even count of hex digits; anything else reads as no bytes). */
147
+ declare function fromHex(hex: string): Uint8Array;
148
+ /** An input whose buttons gain a bit over `previous`'s. */
149
+ declare const isPress: (previous: InputData | null, input: InputData) => boolean;
150
+ /** Encodes uplink `u` (§17.12 *Uplink datagram*); refuses (`field`) what the wire cannot carry. */
151
+ declare function encodeUplink(u: Uplink): FastResult<Uint8Array>;
152
+ /** Decodes an uplink datagram for the seat bound by `binding`: checked whole, the session and key
153
+ * first; any fault drops it. Payloads are as sent (the server sanitizes as it accepts). */
154
+ declare function decodeUplink(bytes: Uint8Array, binding: FastBinding): FastResult<Uplink>;
155
+ /** The render time a fast input carries (§17.12 *Render-time stamp*), the stamp its WebSocket input
156
+ * frame would carry: `round(serverNow) − round(renderDelay + rtt/2)` whole ms, the delay clamped to
157
+ * [0, 65535], 0 when that is not above 0 or the clock has not synced (`serverNow` null). */
158
+ declare function renderStamp(serverNow: number | null, renderDelay: number, rtt: number): number;
159
+ /** What {@link uplinkFor} needs beside the inputs. */
160
+ interface UplinkOptions {
161
+ sid: number;
162
+ key: string;
163
+ /** The newest snapshot the client applied. */
164
+ ackSnap: number;
165
+ /** The server's last accepted input seq, from the newest applied snapshot's `inputAck`. */
166
+ inputAck: number;
167
+ /** How many of the newest inputs this datagram sends first. */
168
+ fresh: number;
169
+ tickHz: number;
170
+ }
171
+ /** The client's next uplink (§17.12 *Redundancy and one-shot inputs*): `sent` is every input it still
172
+ * holds, ascending and contiguous, back past `inputAck`; the window is the newest
173
+ * {@link inputRedundancyTicks}, and the oldest {@link FAST_MAX_ACTIONS} presses before it that the
174
+ * server has not acked ride as actions. */
175
+ declare function uplinkFor(sent: readonly FastInput[], o: UplinkOptions): Uplink;
176
+ /** A seat's receive state: the last accepted seq (from `seqBase` at bind), the last accepted input
177
+ * (null before the first) and the newest snapshot the seat acknowledged. */
178
+ interface SeatRx {
179
+ last: number;
180
+ input: InputData | null;
181
+ ackSnap: number;
182
+ }
183
+ /** One input the server accepted, sanitized, in seq order, with the render time lag compensation
184
+ * judges it at (0: unstamped; a filled input takes the one of the input after its gap). */
185
+ interface AcceptedInput {
186
+ seq: number;
187
+ tick: number;
188
+ render: number;
189
+ input: InputData;
190
+ kind: AcceptKind;
191
+ }
192
+ interface AcceptOptions {
193
+ tickHz: number;
194
+ /** The newest snapshot id the server sent this seat (an `ackSnap` past it is ignored). */
195
+ snapSent: number;
196
+ }
197
+ interface AcceptResult {
198
+ seat: SeatRx;
199
+ accepted: AcceptedInput[];
200
+ /** Seqs consumed without a step: a gap longer than {@link gapFillMax}. */
201
+ skipped: number;
202
+ /** Set when the whole datagram was dropped; `seat` is then the very one given. */
203
+ fault?: FastFault;
204
+ }
205
+ /** The server's take of decoded uplink `u` for seat `seat` (§17.12 *Server dedup*): every held input
206
+ * and slot ascending through `accept(seq)` (a seq at or below the last accepted is dropped), a gap
207
+ * filled with the last input held or, past {@link gapFillMax}, skipped; `ackSnap` forward only. A
208
+ * datagram whose newest seq is past {@link seqAheadMax} is dropped whole. Nothing new: the very same
209
+ * seat. */
210
+ declare function acceptUplink(seat: SeatRx, u: Uplink, o: AcceptOptions): AcceptResult;
211
+ /** A snapshot's motion records, ascending by key; each record's bytes are the record codec's. */
212
+ type Records = readonly {
213
+ key: number;
214
+ bytes: Uint8Array;
215
+ }[];
216
+ /** A snapshot body: the keys removed since the base, then the records new or changed. */
217
+ interface SnapshotBody {
218
+ removed: number[];
219
+ records: Records;
220
+ }
221
+ /** The snapshot header the server writes. `base` 0 is a keyframe. */
222
+ interface SnapshotHead {
223
+ sid: number;
224
+ snap: number;
225
+ base: number;
226
+ tick: number;
227
+ inputAck: number;
228
+ /** The room clock (ms since room start, §16.1 `sNow`, truncated to a uint32) when the server wrote
229
+ * the snapshot: the value a WebSocket patch's TIMED prefix carries, so a fast seat's clock samples
230
+ * exactly what a WebSocket seat's does (the tick's nominal instant is up to a step earlier). */
231
+ sent: number;
232
+ }
233
+ /** The body that turns `base` into `next` (null `base`: a keyframe, every record). */
234
+ declare function diffSnapshot(base: Records | null, next: Records): SnapshotBody;
235
+ /** The base the server deltas against: the seat's `ackSnap` while it still holds that snapshot
236
+ * (`held`, its last {@link FAST_SNAPSHOT_HISTORY} sent ids), else 0 (a keyframe). */
237
+ declare const deltaBase: (held: readonly number[], ackSnap: number) => number;
238
+ /** The fragments of snapshot `head` with body `body` (§17.12 *Downlink snapshot*): `oversized` past
239
+ * {@link FAST_MAX_FRAGMENTS}, `field` for a value the wire cannot carry. */
240
+ declare function encodeSnapshot(head: SnapshotHead, body: SnapshotBody): FastResult<Uint8Array[]>;
241
+ /** One decoded snapshot fragment. */
242
+ interface Fragment {
243
+ snap: number;
244
+ base: number;
245
+ tick: number;
246
+ inputAck: number;
247
+ sent: number;
248
+ keyframe: boolean;
249
+ frag: number;
250
+ frags: number;
251
+ chunk: Uint8Array;
252
+ }
253
+ /** Decodes a snapshot fragment for seat `sid`; any fault drops it. */
254
+ declare function decodeFragment(bytes: Uint8Array, sid: number): FastResult<Fragment>;
255
+ /** Parses a reassembled body to the byte (keys ascending and unique, lengths in bounds, nothing left). */
256
+ declare function parseBody(bytes: Uint8Array): FastResult<SnapshotBody>;
257
+ /** `base`'s records less `body.removed`, with `body.records` written over them. */
258
+ declare function applyBody(base: Records, body: SnapshotBody): Records;
259
+ /** An applied snapshot the client holds as a base. */
260
+ interface HeldSnapshot {
261
+ snap: number;
262
+ tick: number;
263
+ inputAck: number;
264
+ sent: number;
265
+ records: Records;
266
+ }
267
+ /** A snapshot being gathered: its first fragment's header and the chunks so far. */
268
+ interface PartialSnapshot {
269
+ snap: number;
270
+ base: number;
271
+ tick: number;
272
+ inputAck: number;
273
+ sent: number;
274
+ keyframe: boolean;
275
+ frags: number;
276
+ chunks: readonly (Uint8Array | null)[];
277
+ }
278
+ /** The client's receive state: sets being gathered (ascending), applied snapshots held as bases
279
+ * (oldest first, at most {@link FAST_SNAPSHOT_HISTORY}) and the newest applied id (0: none). */
280
+ interface ClientRx {
281
+ partials: readonly PartialSnapshot[];
282
+ history: readonly HeldSnapshot[];
283
+ newest: number;
284
+ }
285
+ declare const emptyClientRx: () => ClientRx;
286
+ interface ReceiveResult {
287
+ rx: ClientRx;
288
+ /** The snapshot this fragment completed and applied, else null. */
289
+ applied: number | null;
290
+ fault?: FastFault;
291
+ }
292
+ /** The client's take of one snapshot datagram (§17.12 *Applying a snapshot*). A fault drops the
293
+ * fragment; while nothing was gathered for it, the answer is the very `rx` given. */
294
+ declare function receiveFragment(rx: ClientRx, bytes: Uint8Array, sid: number): ReceiveResult;
295
+
296
+ /** What a record describes, the first byte. */
297
+ declare const MotionRecordKind: {
298
+ readonly Entity: 1;
299
+ readonly Seat: 2;
300
+ };
301
+ type MotionRecordKind = (typeof MotionRecordKind)[keyof typeof MotionRecordKind];
302
+ /** An entity's motion fields (AuthorityEntity, §17.3, §17.7, §17.10), each at its default when absent. */
303
+ interface EntityMotion {
304
+ x: number;
305
+ y: number;
306
+ z: number;
307
+ vx: number;
308
+ vy: number;
309
+ vz: number;
310
+ yaw: number;
311
+ pitch: number;
312
+ duck: number;
313
+ spin: number;
314
+ fuel: number;
315
+ epoch: number;
316
+ mstate: number;
317
+ qx: number;
318
+ qy: number;
319
+ qz: number;
320
+ qw: number;
321
+ lvx: number;
322
+ lvy: number;
323
+ lvz: number;
324
+ avx: number;
325
+ avy: number;
326
+ avz: number;
327
+ sleep: boolean;
328
+ rest: number;
329
+ }
330
+ /** A seat's input echo and pacing counters (AuthorityPlayer `in*`, §17.11), each 0 when absent. */
331
+ interface SeatEcho {
332
+ inMoveX: number;
333
+ inMoveY: number;
334
+ inMoveZ: number;
335
+ inYaw: number;
336
+ inPitch: number;
337
+ inButtons: number;
338
+ inRep: number;
339
+ inStarve: number;
340
+ inBuf: number;
341
+ /** The receiving seat's own record only: its last input the server consumed (stepped or skipped)
342
+ * as of this snapshot, the ack its reconciler replays from (0 in every other seat's record). */
343
+ inAck: number;
344
+ }
345
+ /** One motion record: an entity's or a seat's, by its id. */
346
+ type MotionRecord = {
347
+ kind: typeof MotionRecordKind.Entity;
348
+ id: string;
349
+ motion: EntityMotion;
350
+ } | {
351
+ kind: typeof MotionRecordKind.Seat;
352
+ id: string;
353
+ motion: SeatEcho;
354
+ };
355
+ /** The longest id a record carries, in characters (AuthorityLimits MAX_ENTITY_ID_CHARS and seat ids). */
356
+ declare const RECORD_ID_MAX_CHARS = 64;
357
+ /** Every entity motion field at its default. */
358
+ declare const blankEntityMotion: () => EntityMotion;
359
+ /** Every seat motion field at its default. */
360
+ declare const blankSeatEcho: () => SeatEcho;
361
+ /** The schema's quantize on a symmetric clamped range of half-width `half`: the uint16 code. */
362
+ declare function quantizeCode(half: number, value: number): number;
363
+ /** The schema's dequantize: the float a decoder reads for `code`. */
364
+ declare const dequantizeCode: (half: number, code: number) => number;
365
+ /** The record's bytes (§17.12 *Motion records*); refuses (`field`) what it cannot carry. */
366
+ declare function encodeRecord(r: MotionRecord): FastResult<Uint8Array>;
367
+ /** Reads one record to the byte; any fault drops it (the whole snapshot it came in). Every field
368
+ * left out reads as its default. */
369
+ declare function decodeRecord(bytes: Uint8Array): FastResult<MotionRecord>;
370
+
371
+ /** A datagram socket to the room's fast endpoint. `send` never throws; a lost datagram is the
372
+ * channel's normal case. */
373
+ interface FastDatagramSocket {
374
+ send(bytes: Uint8Array): void;
375
+ close(): void;
376
+ /** True once the socket knows it can never carry the channel (a session that opens asynchronously
377
+ * and was refused): the seat falls back at once instead of waiting out a deadline. */
378
+ failed?(): boolean;
379
+ /** The longest datagram the socket can send now, where it knows one (WebTransport's
380
+ * `maxDatagramSize`): below the seat's largest uplink ({@link uplinkMaxBytes}), the seat falls back
381
+ * at once. */
382
+ maxDatagramBytes?(): number | undefined;
383
+ }
384
+ /** Opens a socket to a `__fast` `url`, handing each datagram that arrives to `receive`; null when this
385
+ * client cannot reach that kind of url (the seat then falls back at once). `bind` is the whole
386
+ * binding (the client always passes it), for an opener that reaches the channel another way
387
+ * (`bind.wt`, WebTransport). */
388
+ type FastSocketOpener = (url: string, receive: (bytes: Uint8Array) => void, bind?: FastBindMessage) => FastDatagramSocket | null;
389
+ /** Why a fast seat went back to the WebSocket. */
390
+ declare const FastFallbackReason: {
391
+ /** The `__fast` binding was malformed, this client cannot open its url, or its socket failed. */
392
+ readonly Unreachable: "unreachable";
393
+ /** No snapshot within {@link FAST_HANDSHAKE_MS} of the first datagram. */
394
+ readonly Handshake: "handshake";
395
+ /** No snapshot for {@link FAST_STALL_FALLBACK_MS} after the last one. */
396
+ readonly Stall: "stall";
397
+ /** The server asked for it (`__fast:off` from the server). */
398
+ readonly Server: "server";
399
+ };
400
+ type FastFallbackReason = (typeof FastFallbackReason)[keyof typeof FastFallbackReason];
401
+ /** One applied snapshot as the session uses it: its header and the motion records that differ from the
402
+ * previous applied snapshot's (a record that does not decode is left out: its id stays as it was). */
403
+ interface FastSnapshot {
404
+ snap: number;
405
+ tick: number;
406
+ /** The last input seq the server accepted (the header's `inputAck`). */
407
+ inputAck: number;
408
+ /** The room clock the server wrote it at (the header's `sent`, a WebSocket patch's TIMED `sNow`). */
409
+ sent: number;
410
+ changed: readonly MotionRecord[];
411
+ }
412
+ /** What one received datagram did. */
413
+ interface FastReceipt {
414
+ applied: FastSnapshot | null;
415
+ fault?: FastFault;
416
+ }
417
+ interface FastClientOptions {
418
+ bind: FastBindMessage;
419
+ tickHz: number;
420
+ open: FastSocketOpener;
421
+ /** The clock the deadlines run on (ms). */
422
+ now: () => number;
423
+ /** Each applied snapshot, after the receive state took it. */
424
+ onSnapshot?: (s: FastSnapshot) => void;
425
+ }
426
+ /** One seat's fast channel on one socket. */
427
+ interface FastClient {
428
+ readonly bind: FastBindMessage;
429
+ /** The snapshot receive state (reassembly, held bases, newest applied). */
430
+ readonly rx: ClientRx;
431
+ /** The newest input seq queued (0: none). */
432
+ readonly lastQueued: number;
433
+ /** True once closed: nothing is sent or received again. */
434
+ readonly closed: boolean;
435
+ /** An input sent this frame: seqs ascend by one from the first queued. */
436
+ queue(input: FastInput): void;
437
+ /** One datagram carrying the inputs queued since the last flush and the redundant window (one more for
438
+ * each further window's worth of new inputs); nothing when none was queued. */
439
+ flush(): void;
440
+ /** The input of seq `seq` while it is still held. */
441
+ inputAt(seq: number): FastInput | undefined;
442
+ /** When the input of seq `seq` was first sent (by `now`), for each of the newest {@link seqAheadMax}
443
+ * inputs: the server consumes an input well after it received it (its buffer, the next keyframe), and
444
+ * the round trip is timed from the send of the input a snapshot acks as consumed. */
445
+ sentAt(seq: number): number | undefined;
446
+ /** The latest decoded record of `id` of `kind` (from the newest applied snapshot), else undefined. */
447
+ motionOf(kind: MotionRecordKind, id: string): MotionRecord | undefined;
448
+ /** Whether a deadline has passed (§17.12 *Fallback*), by the injected clock. */
449
+ due(): FastFallbackReason | null;
450
+ /** One datagram from the socket. A fault, a stale or duplicate fragment or a datagram for another
451
+ * seat changes nothing. */
452
+ receive(bytes: Uint8Array): FastReceipt;
453
+ close(): void;
454
+ }
455
+ /** A `__fast` payload this client can use: sid a uint32, key {@link FAST_KEY_BYTES} bytes of lowercase
456
+ * hex, url a string. */
457
+ declare function isFastBind(m: unknown): m is FastBindMessage;
458
+ /** The inputs `uplinkFor` still needs once the server accepted through `inputAck`: the window's newest
459
+ * inputs, every input after the ack, and the one before the oldest of those (a press is a bit its
460
+ * previous input lacked). At most {@link seqAheadMax} of them. */
461
+ declare function heldInputs(sent: readonly FastInput[], inputAck: number, tickHz: number): FastInput[];
462
+ /** Opens a seat's fast channel on `o.bind`; null when the opener cannot reach its url. */
463
+ declare function openFastClient(o: FastClientOptions): FastClient | null;
464
+
465
+ export { blankEntityMotion as $, AcceptKind as A, type FastReceipt as B, type ClientRx as C, type FastResult as D, type EntityMotion as E, type FastSocketOpener as F, type FastSnapshot as G, type FastWebTransport as H, type Fragment as I, type HeldSnapshot as J, INPUT_REDUNDANCY_MAX as K, INPUT_REDUNDANCY_MIN as L, INPUT_REDUNDANCY_MS as M, type MotionRecord as N, MotionRecordKind as O, type PartialSnapshot as P, type ReceiveResult as Q, RECORD_ID_MAX_CHARS as R, type Records as S, type SeatEcho as T, type SeatRx as U, type SnapshotBody as V, type SnapshotHead as W, type Uplink as X, type UplinkOptions as Y, acceptUplink as Z, applyBody as _, type FastBindMessage as a, blankSeatEcho as a0, decodeFragment as a1, decodeRecord as a2, decodeUplink as a3, deltaBase as a4, dequantizeCode as a5, diffSnapshot as a6, emptyClientRx as a7, encodeRecord as a8, encodeSnapshot as a9, encodeUplink as aa, fromHex as ab, gapFillMax as ac, heldInputs as ad, inputRedundancyTicks as ae, isFastBind as af, isPress as ag, openFastClient as ah, parseBody as ai, quantizeCode as aj, receiveFragment as ak, renderStamp as al, seqAheadMax as am, toHex as an, uplinkFor as ao, uplinkMaxBytes as ap, FastFallbackReason as b, type AcceptOptions as c, type AcceptResult as d, type AcceptedInput as e, FAST_GAP_FILL_MS as f, FAST_HANDSHAKE_MS as g, FAST_KEY_BYTES as h, FAST_MAX_ACTIONS as i, FAST_MAX_DATAGRAM_BYTES as j, FAST_MAX_FRAGMENTS as k, FAST_MAX_RECORD_BYTES as l, FAST_REASSEMBLY_SLOTS as m, FAST_SEQ_AHEAD_MS as n, FAST_SNAPSHOT_HISTORY as o, FAST_STALL_FALLBACK_MS as p, FAST_VERSION as q, type FastBinding as r, type FastClient as s, type FastClientOptions as t, type FastDatagramSocket as u, FastFault as v, type FastInput as w, FastKind as x, FastMessage as y, type FastOffMessage as z };