@genex-ai/multiplayer 0.17.6 → 0.17.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/PROTOCOL.md +102 -31
  2. package/README.md +35 -14
  3. package/WORLD-GLTF.md +109 -0
  4. package/WORLD-SCENE.md +135 -0
  5. package/dist/authority/fast-udp.d.ts +4 -2
  6. package/dist/authority/fast-udp.js +8 -46
  7. package/dist/authority/fast-webtransport.d.ts +26 -4
  8. package/dist/authority/fast-webtransport.js +12 -115
  9. package/dist/authority/index.d.ts +379 -41
  10. package/dist/authority/index.js +247 -47
  11. package/dist/authority/module.d.ts +2 -1
  12. package/dist/authority/module.js +1 -1
  13. package/dist/bridge-authority.d.ts +3 -2
  14. package/dist/bridge-authority.global.js +10 -9
  15. package/dist/bridge-authority.js +11 -6
  16. package/dist/bridge.global.js +5 -5
  17. package/dist/bridge.js +3 -3
  18. package/dist/chunk-2NGXXQDS.js +1916 -0
  19. package/dist/{chunk-7QVOIHJF.js → chunk-DJ2XYETS.js} +2 -2
  20. package/dist/{chunk-Y6XHXDIK.js → chunk-MQYARUGU.js} +864 -539
  21. package/dist/{chunk-K3SF5VDS.js → chunk-OD6NRSX2.js} +3 -1
  22. package/dist/{chunk-UX3F6YTV.js → chunk-TEHGMFAG.js} +1 -1
  23. package/dist/chunk-TSYEWPVO.js +44 -0
  24. package/dist/chunk-TVTVIMIH.js +675 -0
  25. package/dist/{chunk-ZSWF4VCN.js → chunk-WEMRRIHH.js} +80 -2
  26. package/dist/chunk-YWQ3ALYY.js +69 -0
  27. package/dist/{chunk-5JGRFT2N.js → chunk-ZDGSUR6E.js} +879 -1898
  28. package/dist/{fast-client-CZ41v-oB.d.ts → fast-client--A88q0kk.d.ts} +85 -4
  29. package/dist/index.js +3 -3
  30. package/dist/level-build-CbRqrYJq.d.ts +140 -0
  31. package/dist/{module-DqtFb5wA.d.ts → module-CSpXrzFc.d.ts} +151 -400
  32. package/dist/{physics-predict-49udN3a1.d.ts → physics-predict-C7ZB1gDG.d.ts} +18 -3
  33. package/dist/protocol.d.ts +90 -2
  34. package/dist/protocol.js +17 -1
  35. package/dist/shot-world-D22tqOUT.d.ts +485 -0
  36. package/dist/world-fit/index.d.ts +549 -0
  37. package/dist/world-fit/index.js +4763 -0
  38. package/dist/world-fit/three.d.ts +77 -0
  39. package/dist/world-fit/three.js +140 -0
  40. package/package.json +15 -3
@@ -5,8 +5,12 @@ import {
5
5
  joinOptions,
6
6
  useSdkReconnection,
7
7
  waitForFirstState
8
- } from "./chunk-UX3F6YTV.js";
8
+ } from "./chunk-TEHGMFAG.js";
9
9
  import {
10
+ udpFastOpener
11
+ } from "./chunk-YWQ3ALYY.js";
12
+ import {
13
+ AuthorityJoinMethod,
10
14
  BODY_CORRECTION_TAU_MS,
11
15
  BODY_EXTRAPOLATE_BACK_TICKS,
12
16
  BODY_EXTRAPOLATE_MAX_MS,
@@ -16,11 +20,12 @@ import {
16
20
  ContactDrawing,
17
21
  ContactPush,
18
22
  ContactShapeKind,
19
- EMPTY_WORLD,
23
+ FAST_BIND_REV,
20
24
  FAST_HANDSHAKE_MS,
21
- FAST_KEY_BYTES,
22
- FAST_STALL_FALLBACK_MS,
23
- FastFault,
25
+ FAST_KEEPALIVE_BIND_MS,
26
+ FAST_RETURN_PROOF,
27
+ FAST_RETURN_STABLE_MS,
28
+ FastFallbackReason,
24
29
  FastMessage,
25
30
  HeadFriction,
26
31
  InputRepeat,
@@ -30,48 +35,60 @@ import {
30
35
  LiftPathMode,
31
36
  LiftShapeKind,
32
37
  MAX_PROJECTILE_G,
38
+ MotionRecordKind,
33
39
  NO_LIFTS,
34
40
  PHYSICS_PREDICT_MAX_ANGULAR,
35
41
  PHYSICS_PREDICT_MAX_LINEAR,
36
42
  PHYSICS_PRESET_LENGTH_UNITS,
37
43
  PhysicsPredictRange,
38
- Preset,
39
44
  TopPolicy,
40
- definitionOf,
45
+ blankEntityMotion,
46
+ blankSeatEcho,
47
+ chooseFastSocket,
41
48
  dequantizeBodyValue,
42
- emptyClientRx,
43
- encodeUplink,
44
- firstpersonWorld,
45
- hullOps,
46
- inputRedundancyTicks,
49
+ fastRetryDelayMs,
50
+ isFastBind,
47
51
  isProjectile,
48
- posedWorld,
52
+ nextCodeRegion,
53
+ openFastClient,
54
+ planAuthorityJoin,
49
55
  presetInputRepeat,
50
56
  projectileAt,
51
- psincos,
52
57
  quantizeBodyValue,
53
- receiveFragment,
54
- resolvePresetParams,
55
58
  sanitizeInput,
56
59
  seqAheadMax,
57
60
  serverTick,
58
61
  tickInstant,
59
- unsignedZero,
60
- uplinkFor,
61
- uplinkMaxBytes
62
- } from "./chunk-5JGRFT2N.js";
62
+ validRoomCode
63
+ } from "./chunk-ZDGSUR6E.js";
63
64
  import {
64
65
  RemotePredict
65
- } from "./chunk-K3SF5VDS.js";
66
+ } from "./chunk-OD6NRSX2.js";
67
+ import {
68
+ EMPTY_WORLD,
69
+ Preset,
70
+ definitionOf,
71
+ firstpersonWorld,
72
+ hullOps,
73
+ posedWorld,
74
+ psincos,
75
+ resolvePresetParams,
76
+ unsignedZero
77
+ } from "./chunk-2NGXXQDS.js";
66
78
  import {
67
79
  AUTHORITY_PROTOCOL_VERSION,
80
+ AUTHORITY_SDK_LABEL,
68
81
  AuthorityCaps,
69
82
  AuthorityJoinOption,
70
83
  AuthorityTiming,
71
84
  ClientReconnection,
85
+ CloseCode,
86
+ FREE_QUOTA_REFUSAL,
72
87
  JoinError,
73
- RoomType
74
- } from "./chunk-ZSWF4VCN.js";
88
+ RoomType,
89
+ ServerMessage,
90
+ authorityCloseCodeOf
91
+ } from "./chunk-WEMRRIHH.js";
75
92
 
76
93
  // src/authority/regions.ts
77
94
  var AUTHORITY_REFUSALS_PER_REGION = 2;
@@ -120,6 +137,20 @@ function capacityWalk(regionCount, delays) {
120
137
  }
121
138
  return attempts;
122
139
  }
140
+ var RefusalKind = {
141
+ /** A full region, or a process shutting down that a sibling answers: the schedule, on this region. */
142
+ Capacity: "capacity",
143
+ /** A dead path, with other regions healthy: the next region at once. */
144
+ NextRegion: "next_region",
145
+ /** The server's answer: final. */
146
+ Final: "final"
147
+ };
148
+ var CAPACITY_REFUSALS = /* @__PURE__ */ new Set([503, 4213]);
149
+ var DEAD_PATH_REFUSALS = /* @__PURE__ */ new Set([502, 504, 520, 521, 522, 523, 524, 525, 526, 530]);
150
+ function refusalKind(code) {
151
+ if (typeof code !== "number" || code <= 0 || DEAD_PATH_REFUSALS.has(code)) return RefusalKind.NextRegion;
152
+ return CAPACITY_REFUSALS.has(code) ? RefusalKind.Capacity : RefusalKind.Final;
153
+ }
123
154
  function nextAttempt(state, regionCount, delays) {
124
155
  const refused = state.refused + 1;
125
156
  if (refused >= AUTHORITY_REFUSALS_PER_REGION && state.region + 1 < regionCount) {
@@ -192,6 +223,187 @@ async function probeLatencies(plans, options = {}) {
192
223
  return results;
193
224
  }
194
225
 
226
+ // src/authority/rejoin.ts
227
+ var REJOIN = {
228
+ /** Waits before each attempt; past the end the last one repeats. */
229
+ scheduleMs: [0, 250, 500, 1e3, 2e3, 4e3],
230
+ /** Each wait adds up to this much, by chance, so a room's seats do not arrive at once. */
231
+ jitterMs: 250,
232
+ /** How long a seat keeps trying: long enough for a roll's next process to come up on a full node. */
233
+ budgetMs: 18e4,
234
+ /** How long a seat stays with its own region before it tries the next one. */
235
+ regionStayMs: 6e4,
236
+ /** How long a public seat looks for its room's successor before it joins the game's pool. */
237
+ successorMs: 9e4
238
+ };
239
+ var SUCCESSOR_OPEN_MS = 12e4;
240
+ var RejoinWay = { Room: "room", Successor: "successor", Pool: "pool" };
241
+ var RejoinStop = { Final: "final", Budget: "budget" };
242
+ var COMES_BACK = /* @__PURE__ */ new Set([
243
+ CloseCode.GOING_AWAY,
244
+ CloseCode.NO_STATUS,
245
+ CloseCode.ABNORMAL,
246
+ 1011,
247
+ 1012,
248
+ 1013,
249
+ CloseCode.SERVER_SHUTDOWN,
250
+ CloseCode.FAILED_TO_RECONNECT,
251
+ CloseCode.MAY_TRY_RECONNECT
252
+ ]);
253
+ var FINAL_REFUSALS = /* @__PURE__ */ new Set([
254
+ JoinError.BAD_OPTION,
255
+ JoinError.NOT_ENTITLED,
256
+ JoinError.FORBIDDEN,
257
+ JoinError.MODULE_UNAVAILABLE,
258
+ JoinError.PROTOCOL_UNSUPPORTED
259
+ ]);
260
+ function comesBackAfter(code) {
261
+ return COMES_BACK.has(code);
262
+ }
263
+ function rejoinStep(input) {
264
+ const { attempt, elapsedMs, refusal, lostCode, closed, regions, jitter } = input;
265
+ if (refusal !== null && FINAL_REFUSALS.has(refusal)) return { stop: RejoinStop.Final };
266
+ if (elapsedMs >= REJOIN.budgetMs) return { stop: RejoinStop.Budget };
267
+ const last = REJOIN.scheduleMs.length - 1;
268
+ const base = REJOIN.scheduleMs[Math.min(attempt, last)];
269
+ const waitMs = base + Math.floor(jitter * REJOIN.jitterMs);
270
+ if (attempt === 0 && lostCode !== CloseCode.SERVER_SHUTDOWN) return { waitMs, region: 0, way: RejoinWay.Room };
271
+ const region = regions < 1 ? 0 : Math.floor(elapsedMs / REJOIN.regionStayMs) % regions;
272
+ const way = closed || elapsedMs < REJOIN.successorMs ? RejoinWay.Successor : RejoinWay.Pool;
273
+ return { waitMs, region, way };
274
+ }
275
+ function validSuccessorOf(value) {
276
+ return validRoomCode(value);
277
+ }
278
+ function planSuccessorJoin(input) {
279
+ return {
280
+ ...input.matchKey === "" ? {} : { matchKey: input.matchKey },
281
+ successorOf: input.lostRoomId,
282
+ ...input.closed ? { successorClosed: true } : {}
283
+ };
284
+ }
285
+
286
+ // src/authority/session-rejoin.ts
287
+ var RejoinEvent = { Draining: "draining", Rejoining: "rejoining", Rejoined: "rejoined" };
288
+ var DISCONNECT = "disconnect";
289
+ var UNKNOWN_CLOSE_MS = -1;
290
+ var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
291
+ var refusalOf = (err) => {
292
+ const code = err !== null && typeof err === "object" ? err.code : void 0;
293
+ return typeof code === "number" ? code : null;
294
+ };
295
+ function survivingSession(first, deps) {
296
+ const now = deps.now ?? Date.now;
297
+ const random = deps.random ?? Math.random;
298
+ const sleep = deps.sleep ?? defaultSleep;
299
+ const input = { ...first.input };
300
+ const listeners = /* @__PURE__ */ new Map();
301
+ let current = first;
302
+ let left = false;
303
+ let physicsHash = null;
304
+ let world = null;
305
+ const fire = (event, ...payload) => {
306
+ for (const handler of listeners.get(event) ?? []) handler(...payload);
307
+ };
308
+ const forward = (session, event) => {
309
+ if (event === DISCONNECT || event === RejoinEvent.Draining || event === RejoinEvent.Rejoining || event === RejoinEvent.Rejoined) return;
310
+ session.on(event, (...payload) => {
311
+ if (session === current) fire(event, ...payload);
312
+ });
313
+ };
314
+ const rejoin = async (lostCode, lostReason) => {
315
+ const startedAt = now();
316
+ const lost = { roomId: current.code, matchKey: current.matchKey, closed: deps.closed };
317
+ const regions = deps.targets.length;
318
+ let attempt = 0;
319
+ let refusal = null;
320
+ while (!left) {
321
+ const step = rejoinStep({ attempt, elapsedMs: now() - startedAt, refusal, lostCode, closed: deps.closed, regions, jitter: random() });
322
+ if ("stop" in step) {
323
+ fire(DISCONNECT, lostCode, lostReason);
324
+ return;
325
+ }
326
+ attempt += 1;
327
+ fire(RejoinEvent.Rejoining, { attempt, code: lostCode, waitMs: step.waitMs });
328
+ await sleep(step.waitMs);
329
+ if (left) return;
330
+ try {
331
+ const next = await deps.join(deps.targets[step.region], step.way, lost);
332
+ if (left) {
333
+ await next.leave().catch(() => {
334
+ });
335
+ return;
336
+ }
337
+ adopt(next);
338
+ fire(RejoinEvent.Rejoined, { code: next.code, url: next.url });
339
+ return;
340
+ } catch (err) {
341
+ refusal = refusalOf(err);
342
+ }
343
+ }
344
+ };
345
+ const adopt = (session) => {
346
+ current = session;
347
+ if (physicsHash !== null) session.setPhysics(physicsHash);
348
+ if (world !== null) session.setWorld(world);
349
+ for (const event of listeners.keys()) forward(session, event);
350
+ watch(session);
351
+ };
352
+ const watch = (session) => {
353
+ session.on(ServerMessage.SERVER_RESTART, (notice) => {
354
+ if (session !== current) return;
355
+ const drainMs = notice?.drainMs;
356
+ fire(RejoinEvent.Draining, { closesInMs: typeof drainMs === "number" ? drainMs : UNKNOWN_CLOSE_MS });
357
+ });
358
+ session.on(DISCONNECT, (code, reason) => {
359
+ if (session !== current || left) return;
360
+ if (comesBackAfter(code)) void rejoin(code, reason);
361
+ else fire(DISCONNECT, code, reason);
362
+ });
363
+ };
364
+ watch(first);
365
+ const own = {
366
+ input,
367
+ frame(frameNow) {
368
+ Object.assign(current.input, input);
369
+ return current.frame(frameNow);
370
+ },
371
+ setWorld(next) {
372
+ const hash = current.setWorld(next);
373
+ world = next;
374
+ return hash;
375
+ },
376
+ setPhysics(hash) {
377
+ physicsHash = hash;
378
+ return current.setPhysics(hash);
379
+ },
380
+ on(event, handler) {
381
+ let set = listeners.get(event);
382
+ if (set === void 0) {
383
+ set = /* @__PURE__ */ new Set();
384
+ listeners.set(event, set);
385
+ forward(current, event);
386
+ }
387
+ const fn = handler;
388
+ set.add(fn);
389
+ return () => {
390
+ set.delete(fn);
391
+ };
392
+ },
393
+ async leave() {
394
+ left = true;
395
+ await current.leave();
396
+ }
397
+ };
398
+ return new Proxy(first, {
399
+ get(_target, key) {
400
+ if (Object.hasOwn(own, key)) return own[key];
401
+ const value = current[key];
402
+ return typeof value === "function" ? value.bind(current) : value;
403
+ }
404
+ });
405
+ }
406
+
195
407
  // src/authority/physics-draw.ts
196
408
  var IDENTITY = [0, 0, 0, 1];
197
409
  function quatMul(a, b) {
@@ -289,85 +501,127 @@ var GRID_SIDE = 32;
289
501
  var GRID_CELL_MIN = 64;
290
502
  var GRID_WIDE = 64;
291
503
  var GRID_QUERY_CELLS = 16;
504
+ var GRID_MERGED_WIDE = 64;
292
505
  var UNSTICK_SOLIDS = 4;
293
- function grownBox(b, r, h) {
294
- const [x0, y0, z0, x1, y1, z1] = b;
295
- return [
296
- [-1, 0, 0, -(x0 - r)],
297
- [1, 0, 0, x1 + r],
298
- [0, -1, 0, -(y0 - r)],
299
- [0, 1, 0, y1 + r],
300
- [0, 0, 1, z1],
301
- [0, 0, -1, -(z0 - h)]
302
- ];
303
- }
304
- function grownRamp(ramp, r, h) {
506
+ var BOUNDS_STRIDE = 6;
507
+ var PLANE_STRIDE = 4;
508
+ var BOX_PLANES = 6;
509
+ var RAMP_PLANES = 7;
510
+ var RAMP_STRIDE = RAMP_PLANES * PLANE_STRIDE;
511
+ var BOX_NORMALS = [[-1, 0, 0], [1, 0, 0], [0, -1, 0], [0, 1, 0], [0, 0, 1], [0, 0, -1]];
512
+ var BOX_OFFSETS = [0, 3, 1, 4, 5, 2];
513
+ function writeRamp(out, o, ramp, r, h) {
305
514
  const [x0, y0, z0, x1, y1, z1, axis, lo, hi] = ramp;
306
515
  const m0 = axis === 0 ? x0 : y0;
307
516
  const slope = (hi - lo) / ((axis === 0 ? x1 : y1) - m0);
308
517
  const k = 1 / Math.sqrt(slope * slope + 1);
309
518
  const e = (lo - slope * m0 + r * Math.abs(slope)) * k;
310
- const top = axis === 0 ? [-slope * k, 0, k, e] : [0, -slope * k, k, e];
311
- return [[1, 0, 0, x1 + r], [-1, 0, 0, -x0 + r], [0, 1, 0, y1 + r], [0, -1, 0, -y0 + r], top, [0, 0, 1, z1], [0, 0, -1, -z0 + h]];
312
- }
313
- function solidOf(planes, src, r, h) {
314
- return { planes, min: [src[0] - r, src[1] - r, src[2] - h], max: [src[3] + r, src[4] + r, src[5]] };
519
+ const planes = [1, 0, 0, x1 + r, -1, 0, 0, -x0 + r, 0, 1, 0, y1 + r, 0, -1, 0, -y0 + r, axis === 0 ? -slope * k : 0, axis === 0 ? 0 : -slope * k, k, e, 0, 0, 1, z1, 0, 0, -1, -z0 + h];
520
+ out.set(planes, o);
315
521
  }
316
522
  function compileContactWorld(spec, r, h, skin) {
317
- const boxes = (spec?.boxes ?? []).map((b) => solidOf(grownBox(b, r, h), b, r, h));
318
- const ramps = (spec?.ramps ?? []).map((b) => solidOf(grownRamp(b, r, h), b, r, h));
319
- const solids = [...boxes, ...ramps];
320
- const grid = gridOf(solids);
321
- return grid === null ? { solids, skin } : { solids, skin, grid };
523
+ const boxes = spec?.boxes ?? [];
524
+ const ramps = spec?.ramps ?? [];
525
+ const count = boxes.length + ramps.length;
526
+ const bounds = new Float64Array(count * BOUNDS_STRIDE);
527
+ const rampPlanes = new Float64Array(ramps.length * RAMP_STRIDE);
528
+ for (let i = 0; i < count; i++) {
529
+ const src = i < boxes.length ? boxes[i] : ramps[i - boxes.length];
530
+ const o = i * BOUNDS_STRIDE;
531
+ bounds[o] = src[0] - r;
532
+ bounds[o + 1] = src[1] - r;
533
+ bounds[o + 2] = src[2] - h;
534
+ bounds[o + 3] = src[3] + r;
535
+ bounds[o + 4] = src[4] + r;
536
+ bounds[o + 5] = src[5];
537
+ if (i >= boxes.length) writeRamp(rampPlanes, (i - boxes.length) * RAMP_STRIDE, src, r, h);
538
+ }
539
+ const world = { count, boxCount: boxes.length, bounds, rampPlanes, skin };
540
+ const grid = gridOf(world);
541
+ return grid === null ? world : { ...world, grid };
542
+ }
543
+ function placeable(b, i) {
544
+ const o = i * BOUNDS_STRIDE;
545
+ return Number.isFinite(b[o]) && Number.isFinite(b[o + 3]) && Number.isFinite(b[o + 1]) && Number.isFinite(b[o + 4]);
322
546
  }
323
- var placeable = (s) => Number.isFinite(s.min[0]) && Number.isFinite(s.min[1]) && Number.isFinite(s.max[0]) && Number.isFinite(s.max[1]);
324
547
  function cellOf(v, origin, size, n) {
325
548
  const c = Math.floor((v - origin) / size);
326
549
  if (c < 0) return 0;
327
550
  return c > n - 1 ? n - 1 : c;
328
551
  }
329
- function gridOf(solids) {
330
- const placed2 = solids.filter(placeable);
331
- if (placed2.length < GRID_MIN_SOLIDS) return null;
552
+ function gridOf(world) {
553
+ const { count, bounds } = world;
554
+ let placed2 = 0;
332
555
  let x0 = Number.POSITIVE_INFINITY;
333
556
  let y0 = Number.POSITIVE_INFINITY;
334
557
  let x1 = Number.NEGATIVE_INFINITY;
335
558
  let y1 = Number.NEGATIVE_INFINITY;
336
- for (const s of placed2) {
337
- x0 = Math.min(x0, s.min[0] - CAST_MARGIN);
338
- y0 = Math.min(y0, s.min[1] - CAST_MARGIN);
339
- x1 = Math.max(x1, s.max[0] + CAST_MARGIN);
340
- y1 = Math.max(y1, s.max[1] + CAST_MARGIN);
341
- }
559
+ for (let k = 0; k < count; k++) {
560
+ if (!placeable(bounds, k)) continue;
561
+ placed2 += 1;
562
+ const o = k * BOUNDS_STRIDE;
563
+ x0 = Math.min(x0, bounds[o] - CAST_MARGIN);
564
+ y0 = Math.min(y0, bounds[o + 1] - CAST_MARGIN);
565
+ x1 = Math.max(x1, bounds[o + 3] + CAST_MARGIN);
566
+ y1 = Math.max(y1, bounds[o + 4] + CAST_MARGIN);
567
+ }
568
+ if (placed2 < GRID_MIN_SOLIDS) return null;
342
569
  const size = Math.max(GRID_CELL_MIN, (x1 - x0) / GRID_SIDE, (y1 - y0) / GRID_SIDE);
570
+ if (!Number.isFinite(size)) return null;
343
571
  const nx = Math.min(GRID_SIDE, Math.floor((x1 - x0) / size) + 1);
344
572
  const ny = Math.min(GRID_SIDE, Math.floor((y1 - y0) / size) + 1);
345
- const cells = Array.from({ length: nx * ny }, () => []);
346
- const wide = [];
347
- solids.forEach((s, k) => {
348
- if (!placeable(s)) {
349
- wide.push(k);
350
- return;
573
+ const visit = (onWide, onCell) => {
574
+ for (let k = 0; k < count; k++) {
575
+ if (!placeable(bounds, k)) {
576
+ onWide(k);
577
+ continue;
578
+ }
579
+ const o = k * BOUNDS_STRIDE;
580
+ const i0 = cellOf(bounds[o] - CAST_MARGIN, x0, size, nx);
581
+ const i1 = cellOf(bounds[o + 3] + CAST_MARGIN, x0, size, nx);
582
+ const j0 = cellOf(bounds[o + 1] - CAST_MARGIN, y0, size, ny);
583
+ const j1 = cellOf(bounds[o + 4] + CAST_MARGIN, y0, size, ny);
584
+ if ((i1 - i0 + 1) * (j1 - j0 + 1) > GRID_WIDE) {
585
+ onWide(k);
586
+ continue;
587
+ }
588
+ for (let j = j0; j <= j1; j++) for (let i = i0; i <= i1; i++) onCell(i + j * nx, k);
351
589
  }
352
- const i0 = cellOf(s.min[0] - CAST_MARGIN, x0, size, nx);
353
- const i1 = cellOf(s.max[0] + CAST_MARGIN, x0, size, nx);
354
- const j0 = cellOf(s.min[1] - CAST_MARGIN, y0, size, ny);
355
- const j1 = cellOf(s.max[1] + CAST_MARGIN, y0, size, ny);
356
- if ((i1 - i0 + 1) * (j1 - j0 + 1) > GRID_WIDE) {
357
- wide.push(k);
358
- return;
590
+ };
591
+ const cellStart = new Int32Array(nx * ny + 1);
592
+ let wideCount = 0;
593
+ visit(
594
+ () => {
595
+ wideCount += 1;
596
+ },
597
+ (c) => {
598
+ cellStart[c + 1] += 1;
359
599
  }
360
- for (let j = j0; j <= j1; j++) for (let i = i0; i <= i1; i++) cells[i + j * nx].push(k);
361
- });
362
- return { x0, y0, size, nx, ny, cells, wide };
600
+ );
601
+ for (let c = 0; c < nx * ny; c++) cellStart[c + 1] = cellStart[c + 1] + cellStart[c];
602
+ const cellSolids = new Int32Array(cellStart[nx * ny]);
603
+ const wide = new Int32Array(wideCount);
604
+ const written = cellStart.slice();
605
+ let wideAt = 0;
606
+ visit(
607
+ (k) => {
608
+ wide[wideAt] = k;
609
+ wideAt += 1;
610
+ },
611
+ (c, k) => {
612
+ cellSolids[written[c]] = k;
613
+ written[c] = written[c] + 1;
614
+ }
615
+ );
616
+ return { x0, y0, size, nx, ny, cellStart, cellSolids, wide, merged: new Array(wideAt <= GRID_MERGED_WIDE ? nx * ny : 0) };
363
617
  }
364
- function mergeAscending(a, b) {
365
- if (b.length === 0) return a;
618
+ function mergeAscending(a, b, from, to) {
619
+ if (to === from) return a;
366
620
  const out = [];
367
621
  let i = 0;
368
- let k = 0;
369
- while (i < a.length || k < b.length) {
370
- const fromA = k >= b.length || i < a.length && a[i] <= b[k];
622
+ let k = from;
623
+ while (i < a.length || k < to) {
624
+ const fromA = k >= to || i < a.length && a[i] <= b[k];
371
625
  const next = fromA ? a[i] : b[k];
372
626
  if (fromA) i += 1;
373
627
  else k += 1;
@@ -379,50 +633,100 @@ function solidsNear(world, lx, hx, ly, hy) {
379
633
  const g = world.grid;
380
634
  if (g === void 0 || !(Number.isFinite(lx) && Number.isFinite(hx) && Number.isFinite(ly) && Number.isFinite(hy))) return null;
381
635
  const i0 = cellOf(lx, g.x0, g.size, g.nx);
382
- const i1 = cellOf(hx, g.x0, g.size, g.nx);
636
+ const i1 = hx === lx ? i0 : cellOf(hx, g.x0, g.size, g.nx);
383
637
  const j0 = cellOf(ly, g.y0, g.size, g.ny);
384
- const j1 = cellOf(hy, g.y0, g.size, g.ny);
638
+ const j1 = hy === ly ? j0 : cellOf(hy, g.y0, g.size, g.ny);
385
639
  if ((i1 - i0 + 1) * (j1 - j0 + 1) > GRID_QUERY_CELLS) return null;
640
+ if (i0 === i1 && j0 === j1 && g.merged.length > 0) {
641
+ const c = i0 + j0 * g.nx;
642
+ const kept = g.merged[c];
643
+ if (kept !== void 0) return kept;
644
+ const made = mergeAscending(g.wide, g.cellSolids, g.cellStart[c], g.cellStart[c + 1]);
645
+ g.merged[c] = made;
646
+ return made;
647
+ }
386
648
  let near2 = g.wide;
387
- for (let j = j0; j <= j1; j++) for (let i = i0; i <= i1; i++) near2 = mergeAscending(near2, g.cells[i + j * g.nx]);
649
+ for (let j = j0; j <= j1; j++) {
650
+ for (let i = i0; i <= i1; i++) {
651
+ const c = i + j * g.nx;
652
+ near2 = mergeAscending(near2, g.cellSolids, g.cellStart[c], g.cellStart[c + 1]);
653
+ }
654
+ }
388
655
  return near2;
389
656
  }
390
- function insidePlanes(planes, skin, x, y, z) {
391
- for (const [nx, ny, nz, e] of planes) if (!(e - (nx * x + ny * y + nz * z) > skin)) return false;
657
+ var P = { nx: 0, ny: 0, nz: 0, e: 0 };
658
+ function readPlane(world, i, k) {
659
+ if (i < world.boxCount) {
660
+ const n = BOX_NORMALS[k];
661
+ const at2 = BOX_OFFSETS[k];
662
+ const v = world.bounds[i * BOUNDS_STRIDE + at2];
663
+ P.nx = n[0];
664
+ P.ny = n[1];
665
+ P.nz = n[2];
666
+ P.e = at2 < 3 ? -v : v;
667
+ return;
668
+ }
669
+ const o = (i - world.boxCount) * RAMP_STRIDE + k * PLANE_STRIDE;
670
+ const r = world.rampPlanes;
671
+ P.nx = r[o];
672
+ P.ny = r[o + 1];
673
+ P.nz = r[o + 2];
674
+ P.e = r[o + 3];
675
+ }
676
+ var planesOf = (world, i) => i < world.boxCount ? BOX_PLANES : RAMP_PLANES;
677
+ function insideSolid(world, i, skin, x, y, z) {
678
+ const n = planesOf(world, i);
679
+ for (let k = 0; k < n; k++) {
680
+ readPlane(world, i, k);
681
+ if (!(P.e - (P.nx * x + P.ny * y + P.nz * z) > skin)) return false;
682
+ }
392
683
  return true;
393
684
  }
685
+ var solidsAt = (world, x, y) => solidsNear(world, x, x, y, y);
394
686
  function insideWorld(world, x, y, z) {
395
- for (const s of world.solids) if (insidePlanes(s.planes, world.skin, x, y, z)) return true;
687
+ const near2 = solidsAt(world, x, y);
688
+ const n = near2 === null ? world.count : near2.length;
689
+ for (let m = 0; m < n; m++) {
690
+ if (insideSolid(world, near2 === null ? m : near2[m], world.skin, x, y, z)) return true;
691
+ }
396
692
  return false;
397
693
  }
398
- function segmentNear(s, p, d, len) {
694
+ function segmentNear(world, s, p, d, len) {
695
+ const o = s * BOUNDS_STRIDE;
399
696
  for (let i = 0; i < 3; i++) {
400
697
  const a = p[i];
401
698
  const b = a + d[i] * len;
402
699
  const lo = a < b ? a : b;
403
700
  const hi = a < b ? b : a;
404
- if (hi < s.min[i] - CAST_MARGIN || lo > s.max[i] + CAST_MARGIN) return false;
701
+ if (hi < world.bounds[o + i] - CAST_MARGIN || lo > world.bounds[o + 3 + i] + CAST_MARGIN) return false;
405
702
  }
406
703
  return true;
407
704
  }
408
- function bandHit(s, skin, p, d, rides) {
409
- for (const plane of s.planes) {
410
- const [nx, ny, nz, e] = plane;
705
+ function bandHit(world, s, skin, p, d, rides) {
706
+ const n = planesOf(world, s);
707
+ for (let k = 0; k < n; k++) {
708
+ readPlane(world, s, k);
709
+ const { nx, ny, nz, e } = P;
411
710
  const margin = e - (nx * p[0] + ny * p[1] + nz * p[2]);
412
- if (margin <= skin && nx * d[0] + ny * d[1] + nz * d[2] < 0 && !rides?.(plane)) return { t: 0, face: plane };
711
+ if (margin <= skin && nx * d[0] + ny * d[1] + nz * d[2] < 0) {
712
+ const plane = [nx, ny, nz, e];
713
+ if (!rides?.(plane)) return { t: 0, face: plane };
714
+ }
413
715
  }
414
716
  return null;
415
717
  }
416
718
  var ridesOn = (up, cos) => (face) => face[2] >= cos && face[0] * up[0] + face[1] * up[1] + face[2] * up[2] >= cos;
417
- function enterSolid(s, skin, p, d, len, rides) {
418
- if (insidePlanes(s.planes, skin, p[0], p[1], p[2])) return null;
419
- if (insidePlanes(s.planes, -skin, p[0], p[1], p[2])) return bandHit(s, skin, p, d, rides);
719
+ function enterSolid(world, s, skin, p, d, len, rides) {
720
+ if (insideSolid(world, s, skin, p[0], p[1], p[2])) return null;
721
+ if (insideSolid(world, s, -skin, p[0], p[1], p[2])) return bandHit(world, s, skin, p, d, rides);
420
722
  let lo = 0;
421
723
  let hi = len;
422
724
  let last = Number.NEGATIVE_INFINITY;
423
- let face = null;
424
- for (const plane of s.planes) {
425
- const [nx, ny, nz, e] = plane;
725
+ let face = -1;
726
+ const n = planesOf(world, s);
727
+ for (let k = 0; k < n; k++) {
728
+ readPlane(world, s, k);
729
+ const { nx, ny, nz, e } = P;
426
730
  const s0 = e + skin - (nx * p[0] + ny * p[1] + nz * p[2]);
427
731
  const sd = nx * d[0] + ny * d[1] + nz * d[2];
428
732
  if (sd === 0) {
@@ -433,26 +737,29 @@ function enterSolid(s, skin, p, d, len, rides) {
433
737
  if (sd > 0 && t2 < hi) hi = t2;
434
738
  if (sd < 0 && t2 > last) {
435
739
  last = t2;
436
- face = plane;
740
+ face = k;
437
741
  }
438
742
  }
439
743
  if (last > lo) lo = last;
440
- if (face === null || !(lo < hi) || rides?.(face)) return null;
441
- return { t: lo, face };
744
+ if (face < 0 || !(lo < hi)) return null;
745
+ readPlane(world, s, face);
746
+ const plane = [P.nx, P.ny, P.nz, P.e];
747
+ if (rides?.(plane)) return null;
748
+ return { t: lo, face: plane };
442
749
  }
443
750
  function castWorld(world, p, d, len, rides) {
444
751
  let best = { t: len, face: null };
445
752
  if (!(len > 0)) return { t: 0, face: null };
446
753
  const visit = (s) => {
447
- if (!segmentNear(s, p, d, best.t)) return;
448
- const hit = enterSolid(s, world.skin, p, d, best.t, rides);
754
+ if (!segmentNear(world, s, p, d, best.t)) return;
755
+ const hit = enterSolid(world, s, world.skin, p, d, best.t, rides);
449
756
  if (hit !== null && hit.t < best.t) best = hit;
450
757
  };
451
758
  const [px, py] = [p[0], p[1]];
452
759
  const [ex, ey] = [px + d[0] * len, py + d[1] * len];
453
760
  const near2 = solidsNear(world, Math.min(px, ex), Math.max(px, ex), Math.min(py, ey), Math.max(py, ey));
454
- if (near2 === null) for (const s of world.solids) visit(s);
455
- else for (const k of near2) visit(world.solids[k]);
761
+ if (near2 === null) for (let s = 0; s < world.count; s++) visit(s);
762
+ else for (let k = 0; k < near2.length; k++) visit(near2[k]);
456
763
  return best;
457
764
  }
458
765
  function roomAlong(world, p, d, len, rides) {
@@ -469,18 +776,22 @@ function surfaceUp(world, x, y, z, trace) {
469
776
  return [hit.face[0], hit.face[1], hit.face[2]];
470
777
  }
471
778
  function covered(world, self, x, y, z, q) {
472
- const { solids, skin } = world;
473
- for (let j = 0; j < solids.length; j++) {
779
+ const { count, skin } = world;
780
+ const near2 = solidsAt(world, q[0], q[1]);
781
+ const n = near2 === null ? count : near2.length;
782
+ for (let m = 0; m < n; m++) {
783
+ const j = near2 === null ? m : near2[m];
474
784
  if (j === self) continue;
475
- const planes = solids[j].planes;
476
- if (insidePlanes(planes, skin, q[0], q[1], q[2]) && !insidePlanes(planes, skin, x, y, z)) return true;
785
+ if (insideSolid(world, j, skin, q[0], q[1], q[2]) && !insideSolid(world, j, skin, x, y, z)) return true;
477
786
  }
478
787
  return false;
479
788
  }
480
- function leastFace(world, i, planes, x, y, z) {
789
+ function leastFace(world, i, x, y, z) {
481
790
  let least = null;
482
- for (let k = 0; k < planes.length - 1; k++) {
483
- const [nx, ny, nz, e] = planes[k];
791
+ const n = planesOf(world, i);
792
+ for (let k = 0; k < n - 1; k++) {
793
+ readPlane(world, i, k);
794
+ const { nx, ny, nz, e } = P;
484
795
  const depth = e - (nx * x + ny * y + nz * z);
485
796
  if (least !== null && !(depth < least[3])) continue;
486
797
  if (nz > 0 && covered(world, i, x, y, z, [x + nx * depth, y + ny * depth, z + nz * depth])) continue;
@@ -489,10 +800,12 @@ function leastFace(world, i, planes, x, y, z) {
489
800
  return least;
490
801
  }
491
802
  function insideFace(world, x, y, z) {
492
- const { solids, skin } = world;
493
- for (let i = 0; i < solids.length; i++) {
494
- const planes = solids[i].planes;
495
- if (insidePlanes(planes, skin, x, y, z)) return leastFace(world, i, planes, x, y, z);
803
+ const { count, skin } = world;
804
+ const near2 = solidsAt(world, x, y);
805
+ const n = near2 === null ? count : near2.length;
806
+ for (let m = 0; m < n; m++) {
807
+ const i = near2 === null ? m : near2[m];
808
+ if (insideSolid(world, i, skin, x, y, z)) return leastFace(world, i, x, y, z);
496
809
  }
497
810
  return null;
498
811
  }
@@ -528,7 +841,13 @@ var cross = (p, q) => [p[1] * q[2] - p[2] * q[1], p[2] * q[0] - p[0] * q[2], p[0
528
841
  var neg = (p) => [-p[0], -p[1], -p[2]];
529
842
  function supportOf(b, policy, env) {
530
843
  if (env.supportUp !== void 0) return env.supportUp(b.x, b.y, b.z);
531
- return surfaceUp(env.world, b.x, b.y, b.z, policy.groundTrace);
844
+ const memo = env.supports;
845
+ if (memo === void 0) return surfaceUp(env.world, b.x, b.y, b.z, policy.groundTrace);
846
+ const known = memo.get(b);
847
+ if (known !== void 0 && known.x === b.x && known.y === b.y && known.z === b.z) return known.up;
848
+ const up = surfaceUp(env.world, b.x, b.y, b.z, policy.groundTrace);
849
+ memo.set(b, { x: b.x, y: b.y, z: b.z, up });
850
+ return up;
532
851
  }
533
852
  function frameOf(b, policy, env) {
534
853
  const shape = policy.shape;
@@ -981,7 +1300,9 @@ function contactPair(a, b, policy, env, cooling = false, others = []) {
981
1300
  const fa = frameOf(a, policy, env);
982
1301
  const fb = frameOf(b, policy, env);
983
1302
  const found = contactOfFrames(fa, fb, policy);
984
- if (found === null) return null;
1303
+ return found === null ? null : stepFound(fa, fb, found, rule, policy, env, cooling, others);
1304
+ }
1305
+ function stepFound(fa, fb, found, rule, policy, env, cooling, others) {
985
1306
  const { contact, g } = found;
986
1307
  if (contact.kind === ContactKind.Side) return { ...contact, ...sideContact(fa, fb, contact, g, policy, env, cooling), way: null, w: null };
987
1308
  return { ...contact, ...topContact(fa, fb, contact, g, rule, policy, env, others) };
@@ -992,15 +1313,38 @@ function pairOfPass(a, b, pass, policy, env, options, seats) {
992
1313
  return own === void 0 ? contactPair(a, b, policy, env, cooling, seats) : own;
993
1314
  }
994
1315
  var stillTouching = (c, policy) => c !== null && (c.depth > policy.slop || c.kind === ContactKind.Top && c.depth > 0);
995
- function passOnce(seats, pass, policy, env, options, report) {
1316
+ var memoryOf = (count) => ({ moves: new Int32Array(count), still: new Int32Array(count * count).fill(-1) });
1317
+ function stepIfTouching(a, b, policy, env, seats) {
1318
+ if (farApart(a, b, policy)) return null;
1319
+ const rule = topRuleOf(policy, a, b);
1320
+ if (rule.policy === TopPolicy.Ghost) return null;
1321
+ const fa = frameOf(a, policy, env);
1322
+ const fb = frameOf(b, policy, env);
1323
+ const found = contactOfFrames(fa, fb, policy);
1324
+ if (found === null || !stillTouching(found.contact, policy)) return null;
1325
+ return stepFound(fa, fb, found, rule, policy, env, false, seats);
1326
+ }
1327
+ function passOnce(seats, pass, policy, env, options, report, memory) {
996
1328
  let touched = false;
997
- for (let i = 0; i < seats.length; i++) {
998
- for (let k = i + 1; k < seats.length; k++) {
1329
+ const count = seats.length;
1330
+ for (let i = 0; i < count; i++) {
1331
+ for (let k = i + 1; k < count; k++) {
999
1332
  const a = seats[i];
1000
1333
  const b = seats[k];
1001
- if (pass > 0 && !stillTouching(contactOf(a, b, policy, env), policy)) continue;
1002
- const out = pairOfPass(a, b, pass, policy, env, options, seats);
1003
- if (out === null) continue;
1334
+ const at2 = i * count + k;
1335
+ if (memory !== null && pass > 0 && memory.still[at2] === memory.moves[i] + memory.moves[k]) continue;
1336
+ let out;
1337
+ if (memory !== null && pass > 0) out = stepIfTouching(a, b, policy, env, seats);
1338
+ else if (pass > 0 && !stillTouching(contactOf(a, b, policy, env), policy)) continue;
1339
+ else out = pairOfPass(a, b, pass, policy, env, options, seats);
1340
+ if (out === null) {
1341
+ if (memory !== null) memory.still[at2] = memory.moves[i] + memory.moves[k];
1342
+ continue;
1343
+ }
1344
+ if (memory !== null) {
1345
+ memory.moves[i] = memory.moves[i] + 1;
1346
+ memory.moves[k] = memory.moves[k] + 1;
1347
+ }
1004
1348
  touched = true;
1005
1349
  if (out.way === SlideWay.Pinned) report.pinned += 1;
1006
1350
  options.onPair?.(a, b, out);
@@ -1010,17 +1354,20 @@ function passOnce(seats, pass, policy, env, options, report) {
1010
1354
  }
1011
1355
  function contactPass(seats, policy, env, options = {}) {
1012
1356
  const report = { passes: 0, pushed: /* @__PURE__ */ new Map(), walls: /* @__PURE__ */ new Map(), pinned: 0 };
1357
+ env = { ...env, supports: /* @__PURE__ */ new Map() };
1358
+ const memory = options.first === void 0 && options.onPair === void 0 && options.cooling === void 0 ? memoryOf(seats.length) : null;
1013
1359
  for (let pass = 0; pass < policy.passes; pass++) {
1014
- if (!passOnce(seats, pass, policy, env, options, report)) break;
1360
+ if (!passOnce(seats, pass, policy, env, options, report, memory)) break;
1015
1361
  report.passes += 1;
1016
1362
  if (env.world === null) continue;
1017
- for (const seat of seats) {
1363
+ seats.forEach((seat, s) => {
1018
1364
  const wall = (face) => {
1019
1365
  if (face[2] < policy.supportCos && face[3] > (report.walls.get(seat.id) ?? 0)) report.walls.set(seat.id, face[3]);
1020
1366
  };
1021
1367
  const depth = unstickBody(env.world, seat, wall);
1022
1368
  if (depth > (report.pushed.get(seat.id) ?? 0)) report.pushed.set(seat.id, depth);
1023
- }
1369
+ if (memory !== null && depth > 0) memory.moves[s] = memory.moves[s] + 1;
1370
+ });
1024
1371
  }
1025
1372
  return report;
1026
1373
  }
@@ -4110,7 +4457,10 @@ var roomStateFields = {
4110
4457
  joints: t.map(AuthorityJoint).optional(),
4111
4458
  // Lift rooms (PROTOCOL.md §17.11 *Lifts*): every called lift's start tick, JSON text `{"id":tick,…}`
4112
4459
  // (ids ascending), written when a module's ctx.lifts.call is taken. Absent = none called.
4113
- lifts: t.string().optional()
4460
+ lifts: t.string().optional(),
4461
+ // The room's match key (PROTOCOL.md §17.2 *Choosing a room*): how a friend who joined by code learns
4462
+ // which world to hand `setWorld`. Absent = no key, and never written.
4463
+ matchKey: t.string().optional()
4114
4464
  };
4115
4465
  var AuthorityRoomState = schema(roomStateFields, "AuthorityRoomState");
4116
4466
  var AuthorityPhysicsRoomState = schema({ ...roomStateFields, entities: t.map(AuthorityPhysicsEntity).view() }, "AuthorityRoomState");
@@ -4250,380 +4600,98 @@ function throttleSteps(t2, dtMs, stepMs) {
4250
4600
  return steps;
4251
4601
  }
4252
4602
 
4253
- // src/authority/fast-records.ts
4254
- var MotionRecordKind = { Entity: 1, Seat: 2 };
4255
- var FieldWire = {
4256
- F32: "f32",
4257
- U32: "u32",
4258
- U16: "u16",
4259
- /** A quantized orientation component in [−1, 1], uint16. */
4260
- Orientation: "q-orientation",
4261
- /** A quantized velocity in [−64, 64], uint16. */
4262
- Velocity: "q-velocity",
4263
- /** A boolean: the mask bit alone. */
4264
- Flag: "flag"
4603
+ // src/authority/fast-line.ts
4604
+ var FastTransport = { Udp: "udp", WebSocket: "websocket" };
4605
+ var FastNotAsked = {
4606
+ /** The game turned it off (`fast: false`, `bFastChannel = false`, ...). */
4607
+ Off: "off",
4608
+ /** This runtime has no UDP socket (WebGL, a browser with no opener). */
4609
+ NoUdp: "no_udp",
4610
+ /** The seat asked and the room sent no binding. */
4611
+ NotOffered: "not_offered"
4265
4612
  };
4266
- var ENTITY_FIELDS = [
4267
- ["x", FieldWire.F32],
4268
- ["y", FieldWire.F32],
4269
- ["z", FieldWire.F32],
4270
- ["vx", FieldWire.F32],
4271
- ["vy", FieldWire.F32],
4272
- ["vz", FieldWire.F32],
4273
- ["yaw", FieldWire.F32],
4274
- ["pitch", FieldWire.F32],
4275
- ["duck", FieldWire.F32],
4276
- ["spin", FieldWire.F32],
4277
- ["fuel", FieldWire.F32],
4278
- ["epoch", FieldWire.U32],
4279
- ["mstate", FieldWire.U32],
4280
- ["qx", FieldWire.Orientation],
4281
- ["qy", FieldWire.Orientation],
4282
- ["qz", FieldWire.Orientation],
4283
- ["qw", FieldWire.Orientation],
4284
- ["lvx", FieldWire.Velocity],
4285
- ["lvy", FieldWire.Velocity],
4286
- ["lvz", FieldWire.Velocity],
4287
- ["avx", FieldWire.Velocity],
4288
- ["avy", FieldWire.Velocity],
4289
- ["avz", FieldWire.Velocity],
4290
- ["sleep", FieldWire.Flag],
4291
- ["rest", FieldWire.U16]
4292
- ];
4293
- var SEAT_FIELDS = [
4294
- ["inMoveX", FieldWire.F32],
4295
- ["inMoveY", FieldWire.F32],
4296
- ["inMoveZ", FieldWire.F32],
4297
- ["inYaw", FieldWire.F32],
4298
- ["inPitch", FieldWire.F32],
4299
- ["inButtons", FieldWire.U32],
4300
- ["inRep", FieldWire.U32],
4301
- ["inStarve", FieldWire.U32],
4302
- ["inBuf", FieldWire.U32],
4303
- ["inAck", FieldWire.U32]
4304
- ];
4305
- var RECORD_ID_MAX_CHARS = 64;
4306
- var HEAD = 2;
4307
- var MASK_BYTES = 4;
4308
- var UINT32_MAX = 4294967295;
4309
- var UINT16_MAX = 65535;
4310
- var Q_SPAN = 65534;
4311
- var Q_ZERO = Q_SPAN / 2;
4312
- var Q_RANGE = { [FieldWire.Orientation]: 1, [FieldWire.Velocity]: 64 };
4313
- var blankEntityMotion = () => ({
4314
- x: 0,
4315
- y: 0,
4316
- z: 0,
4317
- vx: 0,
4318
- vy: 0,
4319
- vz: 0,
4320
- yaw: 0,
4321
- pitch: 0,
4322
- duck: 0,
4323
- spin: 0,
4324
- fuel: 0,
4325
- epoch: 0,
4326
- mstate: 0,
4327
- qx: 0,
4328
- qy: 0,
4329
- qz: 0,
4330
- qw: 0,
4331
- lvx: 0,
4332
- lvy: 0,
4333
- lvz: 0,
4334
- avx: 0,
4335
- avy: 0,
4336
- avz: 0,
4337
- sleep: false,
4338
- rest: 0
4339
- });
4340
- var blankSeatEcho = () => ({ inMoveX: 0, inMoveY: 0, inMoveZ: 0, inYaw: 0, inPitch: 0, inButtons: 0, inRep: 0, inStarve: 0, inBuf: 0, inAck: 0 });
4341
- function quantizeCode(half, value) {
4342
- const v = Math.min(half, Math.max(-half, value));
4343
- return Math.floor((v + half) / (2 * half) * Q_SPAN + 0.5);
4344
- }
4345
- var dequantizeCode = (half, code) => -half + code / Q_SPAN * (2 * half);
4346
- var fieldsOf = (kind) => {
4347
- if (kind === MotionRecordKind.Entity) return ENTITY_FIELDS;
4348
- if (kind === MotionRecordKind.Seat) return SEAT_FIELDS;
4349
- return null;
4613
+ var FAST_OFFER_WAIT_MS = FAST_HANDSHAKE_MS;
4614
+ var WHY = {
4615
+ [FastNotAsked.Off]: "the game turned the fast channel off",
4616
+ [FastNotAsked.NoUdp]: "this platform has no UDP",
4617
+ [FastNotAsked.NotOffered]: "the room does not offer the fast channel",
4618
+ unreachable: "fell back, the binding could not be used or its socket failed",
4619
+ handshake: "fell back, no UDP snapshot within 1 s of the first datagram (UDP blocked?)",
4620
+ stall: "fell back, no UDP snapshot for 2 s",
4621
+ server: "fell back, the room closed the channel",
4622
+ "no-webtransport": "this browser has no WebTransport",
4623
+ "no-relay": "the room has no WebTransport relay"
4350
4624
  };
4351
- var isUint = (v, max) => typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= max;
4352
- function wireValue(wire, v) {
4353
- switch (wire) {
4354
- case FieldWire.F32: {
4355
- if (typeof v !== "number" || !Number.isFinite(Math.fround(v))) return null;
4356
- return Math.fround(v) === 0 ? void 0 : Math.fround(v);
4357
- }
4358
- case FieldWire.U32:
4359
- if (!isUint(v, UINT32_MAX)) return null;
4360
- return v === 0 ? void 0 : v;
4361
- case FieldWire.U16:
4362
- if (!isUint(v, UINT16_MAX)) return null;
4363
- return v === 0 ? void 0 : v;
4364
- case FieldWire.Flag:
4365
- if (typeof v !== "boolean") return null;
4366
- return v ? 1 : void 0;
4367
- default: {
4368
- if (typeof v !== "number" || Number.isNaN(v)) return null;
4369
- const code = quantizeCode(Q_RANGE[wire], v);
4370
- return code === Q_ZERO ? void 0 : code;
4371
- }
4372
- }
4373
- }
4374
- var WIDTH = { f32: 4, u32: 4, u16: 2, "q-orientation": 2, "q-velocity": 2, flag: 0 };
4375
- function encodeRecord(r) {
4376
- const fields = fieldsOf(r.kind);
4377
- const id = typeof r.id === "string" ? new TextEncoder().encode(r.id) : null;
4378
- const idOk = id !== null && r.id.length >= 1 && r.id.length <= RECORD_ID_MAX_CHARS && id.length <= 255;
4379
- if (fields === null || !idOk || r.motion === null || typeof r.motion !== "object") return { ok: false, fault: FastFault.Field };
4380
- const motion = r.motion;
4381
- const present = [];
4382
- let mask = 0;
4383
- for (const [i, [name, wire]] of fields.entries()) {
4384
- const value = wireValue(wire, motion[name]);
4385
- if (value === null) return { ok: false, fault: FastFault.Field };
4386
- if (value === void 0) continue;
4387
- mask = (mask | 1 << i) >>> 0;
4388
- present.push([wire, value]);
4389
- }
4390
- const out = new Uint8Array(HEAD + id.length + MASK_BYTES + present.reduce((n, [w]) => n + WIDTH[w], 0));
4391
- const view = new DataView(out.buffer);
4392
- out[0] = r.kind;
4393
- out[1] = id.length;
4394
- out.set(id, HEAD);
4395
- let at2 = HEAD + id.length;
4396
- view.setUint32(at2, mask, true);
4397
- at2 += MASK_BYTES;
4398
- for (const [wire, value] of present) at2 = writeField(view, at2, wire, value);
4399
- return { ok: true, value: out };
4400
- }
4401
- function writeField(view, at2, wire, value) {
4402
- if (wire === FieldWire.F32) view.setFloat32(at2, value, true);
4403
- else if (wire === FieldWire.U32) view.setUint32(at2, value, true);
4404
- else if (wire !== FieldWire.Flag) view.setUint16(at2, value, true);
4405
- return at2 + WIDTH[wire];
4406
- }
4407
- function readField(view, at2, wire) {
4408
- if (at2 + WIDTH[wire] > view.byteLength) return { ok: false, fault: FastFault.Truncated };
4409
- switch (wire) {
4410
- case FieldWire.Flag:
4411
- return { ok: true, value: true };
4412
- case FieldWire.F32: {
4413
- const v = view.getFloat32(at2, true);
4414
- return Number.isFinite(v) && v !== 0 ? { ok: true, value: v } : { ok: false, fault: FastFault.Field };
4415
- }
4416
- case FieldWire.U32: {
4417
- const v = view.getUint32(at2, true);
4418
- return v !== 0 ? { ok: true, value: v } : { ok: false, fault: FastFault.Field };
4419
- }
4420
- case FieldWire.U16: {
4421
- const v = view.getUint16(at2, true);
4422
- return v !== 0 ? { ok: true, value: v } : { ok: false, fault: FastFault.Field };
4423
- }
4424
- default: {
4425
- const code = view.getUint16(at2, true);
4426
- const canonical = code !== Q_ZERO && code <= Q_SPAN;
4427
- return canonical ? { ok: true, value: dequantizeCode(Q_RANGE[wire], code) } : { ok: false, fault: FastFault.Field };
4428
- }
4429
- }
4430
- }
4431
- var UTF8 = new TextDecoder("utf-8", { fatal: true });
4432
- function readId(bytes) {
4433
- const len = bytes[1];
4434
- if (HEAD + len > bytes.length) return { ok: false, fault: FastFault.Truncated };
4435
- if (len === 0) return { ok: false, fault: FastFault.Field };
4436
- try {
4437
- const id = UTF8.decode(bytes.subarray(HEAD, HEAD + len));
4438
- return id.length <= RECORD_ID_MAX_CHARS ? { ok: true, value: id } : { ok: false, fault: FastFault.Field };
4439
- } catch {
4440
- return { ok: false, fault: FastFault.Field };
4441
- }
4442
- }
4443
- function decodeRecord(bytes) {
4444
- if (bytes.length < HEAD) return { ok: false, fault: FastFault.Truncated };
4445
- const fields = fieldsOf(bytes[0]);
4446
- if (fields === null) return { ok: false, fault: FastFault.Kind };
4447
- const id = readId(bytes);
4448
- if (!id.ok) return id;
4449
- const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
4450
- let at2 = HEAD + bytes[1];
4451
- if (at2 + MASK_BYTES > bytes.length) return { ok: false, fault: FastFault.Truncated };
4452
- const mask = view.getUint32(at2, true);
4453
- at2 += MASK_BYTES;
4454
- if (fields.length < 32 && mask >>> fields.length !== 0) return { ok: false, fault: FastFault.Flags };
4455
- if (bytes[0] === MotionRecordKind.Entity) {
4456
- const motion2 = readMotion(view, at2, mask, blankEntityMotion(), fields);
4457
- return motion2.ok ? { ok: true, value: { kind: MotionRecordKind.Entity, id: id.value, motion: motion2.value } } : motion2;
4458
- }
4459
- const motion = readMotion(view, at2, mask, blankSeatEcho(), fields);
4460
- return motion.ok ? { ok: true, value: { kind: MotionRecordKind.Seat, id: id.value, motion: motion.value } } : motion;
4461
- }
4462
- function readMotion(view, start, mask, blank, fields) {
4463
- const values = {};
4464
- let at2 = start;
4465
- for (const [i, [name, wire]] of fields.entries()) {
4466
- if ((mask & 1 << i) === 0) continue;
4467
- const v = readField(view, at2, wire);
4468
- if (!v.ok) return v;
4469
- values[name] = v.value;
4470
- at2 += WIDTH[wire];
4471
- }
4472
- if (at2 !== view.byteLength) return { ok: false, fault: FastFault.Trailing };
4473
- return { ok: true, value: Object.assign(blank, values) };
4474
- }
4475
-
4476
- // src/authority/fast-client.ts
4477
- var UINT32_MAX2 = 4294967295;
4478
- var FastFallbackReason = {
4479
- /** The `__fast` binding was malformed, this client cannot open its url, or its socket failed. */
4480
- Unreachable: "unreachable",
4481
- /** No snapshot within {@link FAST_HANDSHAKE_MS} of the first datagram. */
4482
- Handshake: "handshake",
4483
- /** No snapshot for {@link FAST_STALL_FALLBACK_MS} after the last one. */
4484
- Stall: "stall",
4485
- /** The server asked for it (`__fast:off` from the server). */
4486
- Server: "server"
4487
- };
4488
- var isUint2 = (v, max) => typeof v === "number" && Number.isInteger(v) && v >= 0 && v <= max;
4489
- function isFastBind(m) {
4490
- if (typeof m !== "object" || m === null) return false;
4491
- const { sid, key, url } = m;
4492
- const keyOk = typeof key === "string" && key.length === 2 * FAST_KEY_BYTES && /^[0-9a-f]+$/.test(key);
4493
- return isUint2(sid, UINT32_MAX2) && keyOk && typeof url === "string" && url.length > 0;
4494
- }
4495
- function heldInputs(sent, inputAck, tickHz) {
4496
- if (sent.length === 0) return [];
4497
- const newest = sent[sent.length - 1].seq;
4498
- const keepFrom = Math.min(inputAck, newest - inputRedundancyTicks(tickHz) + 1);
4499
- const capped = Math.max(keepFrom, newest - seqAheadMax(tickHz) + 1);
4500
- return sent.filter((s) => s.seq >= capped);
4501
- }
4502
- var recordName = (kind, id) => `${kind}:${id}`;
4503
- function openFastClient(o) {
4504
- let rx = emptyClientRx();
4505
- let sent = [];
4506
- const sentTimes = /* @__PURE__ */ new Map();
4507
- let fresh = 0;
4508
- let closed = false;
4509
- let firstSentAt = null;
4510
- let lastAppliedAt = null;
4511
- let decodedByKey = /* @__PURE__ */ new Map();
4512
- let latest = /* @__PURE__ */ new Map();
4513
- const inputAck = () => rx.history.at(-1)?.inputAck ?? 0;
4514
- const decodeChanged = (records) => {
4515
- const nextByKey = /* @__PURE__ */ new Map();
4516
- const nextLatest = /* @__PURE__ */ new Map();
4517
- const changed = [];
4518
- for (const r of records) {
4519
- const before = decodedByKey.get(r.key);
4520
- const same = before !== void 0 && (before.bytes === r.bytes || sameBytes(before.bytes, r.bytes));
4521
- const record = same ? before.record : decodeOrNull(r.bytes);
4522
- nextByKey.set(r.key, { bytes: r.bytes, record });
4523
- if (record === null) continue;
4524
- nextLatest.set(recordName(record.kind, record.id), record);
4525
- if (!same) changed.push(record);
4526
- }
4527
- decodedByKey = nextByKey;
4528
- latest = nextLatest;
4529
- return changed;
4530
- };
4531
- function receive(bytes) {
4532
- if (closed) return { applied: null };
4533
- const r = receiveFragment(rx, bytes, o.bind.sid);
4534
- rx = r.rx;
4535
- if (r.fault !== void 0 || r.applied === null) return { applied: null, fault: r.fault };
4536
- const held = rx.history.at(-1);
4537
- if (held === void 0) return { applied: null };
4538
- lastAppliedAt = o.now();
4539
- sent = heldInputs(sent, held.inputAck, o.tickHz);
4540
- const applied = { snap: held.snap, tick: held.tick, inputAck: held.inputAck, sent: held.sent, changed: decodeChanged(held.records) };
4541
- o.onSnapshot?.(applied);
4542
- return { applied };
4543
- }
4544
- const socket = o.open(
4545
- o.bind.url,
4546
- (bytes) => {
4547
- receive(bytes);
4548
- },
4549
- o.bind
4550
- );
4551
- if (socket === null) return null;
4552
- const largestUplink = uplinkMaxBytes(o.tickHz);
4625
+ var udpLine = (url) => `genex transport: udp fast@1 ${url}`;
4626
+ var webSocketLine = (reason) => `genex transport: websocket, ${reason}: ${WHY[reason] ?? "unknown"}`;
4627
+ function fastLineAnnouncer() {
4628
+ const said = /* @__PURE__ */ new Set();
4629
+ const once = (line) => {
4630
+ if (said.has(line)) return null;
4631
+ said.add(line);
4632
+ return line;
4633
+ };
4553
4634
  return {
4554
- bind: o.bind,
4555
- get rx() {
4556
- return rx;
4557
- },
4558
- get lastQueued() {
4559
- return sent.at(-1)?.seq ?? 0;
4560
- },
4561
- get closed() {
4562
- return closed;
4563
- },
4564
- queue(input) {
4565
- if (closed) return;
4566
- sent.push({ seq: input.seq, tick: input.tick, render: input.render, input: { ...input.input } });
4567
- fresh += 1;
4568
- },
4569
- flush() {
4570
- if (closed || fresh === 0) return;
4571
- const now = o.now();
4572
- for (const s of sent.slice(-fresh)) sentTimes.set(s.seq, now);
4573
- forgetSendTimes(sentTimes, (sent.at(-1)?.seq ?? 0) - seqAheadMax(o.tickHz) + 1);
4574
- const window = inputRedundancyTicks(o.tickHz);
4575
- let from = sent.length - fresh;
4576
- fresh = 0;
4577
- while (from < sent.length) {
4578
- const to = Math.min(from + window, sent.length);
4579
- const u = uplinkFor(sent.slice(0, to), { sid: o.bind.sid, key: o.bind.key, ackSnap: rx.newest, inputAck: inputAck(), fresh: to - from, tickHz: o.tickHz });
4580
- from = to;
4581
- const bytes = encodeUplink(u);
4582
- if (!bytes.ok) continue;
4583
- firstSentAt ??= now;
4584
- socket.send(bytes.value);
4585
- }
4586
- },
4587
- inputAt: (seq) => sent.find((s) => s.seq === seq),
4588
- sentAt: (seq) => sentTimes.get(seq),
4589
- motionOf: (kind, id) => latest.get(recordName(kind, id)),
4590
- due() {
4591
- if (closed) return null;
4592
- if (socket.failed?.() === true) return FastFallbackReason.Unreachable;
4593
- const room = socket.maxDatagramBytes?.();
4594
- if (room !== void 0 && room < largestUplink) return FastFallbackReason.Unreachable;
4595
- const now = o.now();
4596
- if (lastAppliedAt !== null) return now - lastAppliedAt > FAST_STALL_FALLBACK_MS ? FastFallbackReason.Stall : null;
4597
- return firstSentAt !== null && now - firstSentAt > FAST_HANDSHAKE_MS ? FastFallbackReason.Handshake : null;
4635
+ observe(o) {
4636
+ if (o.joinedMs === null) return null;
4637
+ if (!o.asked) return once(webSocketLine(FastNotAsked.Off));
4638
+ if (!o.udp) return once(webSocketLine(FastNotAsked.NoUdp));
4639
+ if (o.fallback !== null) return once(webSocketLine(o.fallback));
4640
+ if (o.url !== null) return o.snapshots > 0 ? once(udpLine(o.url)) : null;
4641
+ return o.joinedMs >= FAST_OFFER_WAIT_MS ? once(webSocketLine(FastNotAsked.NotOffered)) : null;
4598
4642
  },
4599
- receive,
4600
- close() {
4601
- if (closed) return;
4602
- closed = true;
4603
- socket.close();
4643
+ reset() {
4644
+ said.clear();
4604
4645
  }
4605
4646
  };
4606
4647
  }
4607
- function forgetSendTimes(times, from) {
4608
- for (const seq of times.keys()) {
4609
- if (seq >= from) return;
4610
- times.delete(seq);
4611
- }
4612
- }
4613
- function decodeOrNull(bytes) {
4614
- const d = decodeRecord(bytes);
4615
- return d.ok ? d.value : null;
4616
- }
4617
- var sameBytes = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
4618
4648
 
4619
4649
  // src/authority/fast-session.ts
4620
4650
  import { $refId, OPERATION } from "@colyseus/schema";
4621
4651
  var ROOM_INPUT_RELIABLE = 19;
4652
+ var RETRY_AFTER_SERVER_OFF = 3;
4622
4653
  var PROTOCOL_CODE_MASK = 31;
4623
4654
  var ENTITY_MOTION_FIELDS = Object.keys(blankEntityMotion());
4624
4655
  var SEAT_MOTION_FIELDS = Object.keys(blankSeatEcho()).filter((f) => f !== "inAck");
4625
4656
  var motionFieldsOf = (kind) => kind === MotionRecordKind.Entity ? ENTITY_MOTION_FIELDS : SEAT_MOTION_FIELDS;
4626
4657
  var ROOT_TICK_FIELDS = ["tick", "keyframe"];
4658
+ var SETTLE_ACK_TRAIL = 64;
4659
+ function settleAckTrail() {
4660
+ let trail = [];
4661
+ let floor = 0;
4662
+ return {
4663
+ snapshot(tick, ack) {
4664
+ const last = trail.at(-1)?.ack ?? floor;
4665
+ if (ack <= last) return;
4666
+ trail.push({ tick, ack });
4667
+ if (trail.length > SETTLE_ACK_TRAIL) trail = trail.slice(-SETTLE_ACK_TRAIL);
4668
+ },
4669
+ through(tick) {
4670
+ let kept = 0;
4671
+ while (kept < trail.length && trail[kept].tick <= tick) {
4672
+ floor = Math.max(floor, trail[kept].ack);
4673
+ kept += 1;
4674
+ }
4675
+ if (kept > 0) trail = trail.slice(kept);
4676
+ return floor;
4677
+ },
4678
+ reset(at2) {
4679
+ trail = [];
4680
+ floor = at2;
4681
+ }
4682
+ };
4683
+ }
4684
+ function defaultFastLog(line) {
4685
+ const stderr = globalThis.process?.stderr;
4686
+ if (stderr?.write !== void 0) stderr.write(`${line}
4687
+ `);
4688
+ else console.info(line);
4689
+ }
4690
+ var defaultEvery = (ms, fn) => {
4691
+ const timer = setInterval(fn, ms);
4692
+ timer.unref?.();
4693
+ return () => clearInterval(timer);
4694
+ };
4627
4695
  function baselinesOf(handle) {
4628
4696
  if (typeof handle !== "object" || handle === null) return null;
4629
4697
  const h = handle;
@@ -4642,13 +4710,31 @@ function renderStampOf(handle) {
4642
4710
  }
4643
4711
  function fastSeat(o) {
4644
4712
  const { room, handle } = o;
4645
- const status = { live: false, snapshots: 0, fallback: null };
4713
+ const status = { live: false, snapshots: 0, fallback: o.neverAsked, fallbacks: 0, returns: 0, transport: FastTransport.WebSocket, line: null };
4714
+ let returnsSaid = 0;
4715
+ const announcer = fastLineAnnouncer();
4716
+ let joinedAt = null;
4717
+ let boundUrl = null;
4718
+ let snapshotsBefore = 0;
4646
4719
  let link = null;
4647
4720
  let fallback = null;
4648
4721
  let fastAck = 0;
4722
+ const settle2 = settleAckTrail();
4649
4723
  let early = [];
4650
4724
  let pendingBind = null;
4725
+ let offerAwaitedFrom = null;
4726
+ let roundTripMs = 0;
4651
4727
  const guard = o.open === void 0 ? null : motionGuard(room, () => link?.client ?? null);
4728
+ const stopTimer = o.open === void 0 ? null : (o.every ?? defaultEvery)(FAST_KEEPALIVE_BIND_MS, () => {
4729
+ link?.client.keepalive();
4730
+ probe?.client.keepalive();
4731
+ });
4732
+ const random = o.random ?? Math.random;
4733
+ let probe = null;
4734
+ let hostReturns = false;
4735
+ let attempt = 0;
4736
+ let retryAt = null;
4737
+ let liveSince = null;
4652
4738
  const baselines = baselinesOf(handle);
4653
4739
  const renderStamp = renderStampOf(handle);
4654
4740
  const ack = () => {
@@ -4658,8 +4744,17 @@ function fastSeat(o) {
4658
4744
  return ws > 0 ? Math.max(fallback.ack, fallback.at + ws) : fallback.ack;
4659
4745
  };
4660
4746
  const seqBase = () => room.state.players?.get(room.sessionId)?.seqBase ?? 0;
4747
+ const scheduleRetry = () => {
4748
+ if (!hostReturns || o.open === void 0) return;
4749
+ retryAt = o.now() + fastRetryDelayMs(attempt, random());
4750
+ attempt += 1;
4751
+ };
4661
4752
  const fallBack = (reason) => {
4662
4753
  if (fallback !== null) return;
4754
+ if (liveSince !== null && o.now() - liveSince >= FAST_RETURN_STABLE_MS) attempt = 0;
4755
+ if (reason === FastFallbackReason.Server) attempt = Math.max(attempt, RETRY_AFTER_SERVER_OFF);
4756
+ liveSince = null;
4757
+ status.fallbacks += 1;
4663
4758
  const live = link;
4664
4759
  const base = link?.seqBase ?? seqBase();
4665
4760
  fallback = { at: handle.sentCount, wsBefore: link?.wsBefore ?? handle.sentCount, ack: ack(), reason };
@@ -4675,6 +4770,82 @@ function fastSeat(o) {
4675
4770
  const off = { last: base + handle.sentCount };
4676
4771
  room.send(FastMessage.Off, off);
4677
4772
  guard?.release();
4773
+ scheduleRetry();
4774
+ };
4775
+ const offChannel = (reason) => {
4776
+ if (status.fallback === null) status.fallbacks += 1;
4777
+ status.fallback = reason;
4778
+ };
4779
+ const abandonProbe = (reason) => {
4780
+ if (probe === null) return;
4781
+ probe.client.close();
4782
+ probe = null;
4783
+ offChannel(reason);
4784
+ const off = { last: seqBase() + handle.sentCount };
4785
+ room.send(FastMessage.Off, off);
4786
+ scheduleRetry();
4787
+ };
4788
+ const swapToProbe = (s) => {
4789
+ const p = probe;
4790
+ if (p === null || fallback === null) return;
4791
+ probe = null;
4792
+ const base = seqBase();
4793
+ const wsBefore = fallback.wsBefore + (handle.sentCount - fallback.at);
4794
+ fastAck = ack();
4795
+ settle2.reset(fastAck);
4796
+ link = { client: p.client, seqBase: base, wsBefore, stampAtBind: baselines?._lastStamp ?? 0 };
4797
+ fallback = null;
4798
+ retryAt = null;
4799
+ liveSince = o.now();
4800
+ status.live = true;
4801
+ status.fallback = null;
4802
+ if (status.fallbacks > 0) status.returns += 1;
4803
+ const on = { last: base + handle.sentCount };
4804
+ room.send(FastMessage.On, on);
4805
+ applied({ ...s, changed: p.client.records() });
4806
+ };
4807
+ const askRoundTrip = (client) => {
4808
+ if (!client.opening()) return;
4809
+ room.ping((ms) => {
4810
+ roundTripMs = ms;
4811
+ });
4812
+ };
4813
+ const startProbe = (message) => {
4814
+ if (o.open === void 0) return;
4815
+ attempt = Math.max(0, attempt - 1);
4816
+ hostReturns = true;
4817
+ probe?.client.close();
4818
+ probe = null;
4819
+ if (fallback === null) status.fallback = null;
4820
+ fallback ??= { at: 0, wsBefore: 0, ack: 0, reason: FastFallbackReason.Unreachable };
4821
+ early = [];
4822
+ if (!isFastBind(message)) {
4823
+ offChannel(FastFallbackReason.Unreachable);
4824
+ return scheduleRetry();
4825
+ }
4826
+ boundUrl = message.url;
4827
+ const proof = { client: null, applied: 0 };
4828
+ const client = openFastClient({
4829
+ bind: message,
4830
+ tickHz: o.tickHz(),
4831
+ open: o.open,
4832
+ now: o.now,
4833
+ roundTripMs: () => roundTripMs,
4834
+ onSnapshot: (s) => {
4835
+ if (link !== null && link.client === proof.client) return applied(s);
4836
+ proof.applied += 1;
4837
+ if (proof.applied >= FAST_RETURN_PROOF) swapToProbe(s);
4838
+ }
4839
+ });
4840
+ if (client === null) {
4841
+ offChannel(o.open.whyNull?.(message) ?? FastFallbackReason.Unreachable);
4842
+ return scheduleRetry();
4843
+ }
4844
+ askRoundTrip(client);
4845
+ proof.client = client;
4846
+ probe = proof;
4847
+ retryAt = null;
4848
+ client.keepalive();
4678
4849
  };
4679
4850
  const applied = (s) => {
4680
4851
  const l = link;
@@ -4689,25 +4860,55 @@ function fastSeat(o) {
4689
4860
  if (at2 !== void 0) rtt = o.now() - at2;
4690
4861
  fastAck = acked;
4691
4862
  }
4863
+ settle2.snapshot(s.tick, fastAck);
4692
4864
  room.clock.sample(s.sent, rtt);
4693
4865
  guard?.write(s);
4694
4866
  };
4695
4867
  const bind = (message) => {
4696
- if (o.open === void 0 || link !== null || fallback !== null) return;
4868
+ const asProbe = message?.probe === true;
4869
+ if (o.open === void 0 || link !== null || fallback !== null && !asProbe) return;
4697
4870
  if (!o.stateCurrent()) {
4698
4871
  pendingBind = message;
4699
4872
  return;
4700
4873
  }
4701
4874
  pendingBind = null;
4875
+ if (asProbe) return startProbe(message);
4876
+ status.fallback = null;
4702
4877
  if (!isFastBind(message)) return fallBack(FastFallbackReason.Unreachable);
4703
- const client = openFastClient({ bind: message, tickHz: o.tickHz(), open: o.open, now: o.now, onSnapshot: applied });
4704
- if (client === null) return fallBack(FastFallbackReason.Unreachable);
4878
+ boundUrl = message.url;
4879
+ hostReturns = typeof message.rev === "number" && message.rev >= FAST_BIND_REV;
4880
+ const client = openFastClient({ bind: message, tickHz: o.tickHz(), open: o.open, now: o.now, roundTripMs: () => roundTripMs, onSnapshot: applied });
4881
+ if (client === null) return fallBack(o.open.whyNull?.(message) ?? FastFallbackReason.Unreachable);
4882
+ askRoundTrip(client);
4705
4883
  link = { client, seqBase: seqBase(), wsBefore: handle.sentCount, stampAtBind: baselines?._lastStamp ?? 0 };
4706
4884
  fastAck = 0;
4885
+ settle2.reset(0);
4707
4886
  status.live = true;
4887
+ liveSince = o.now();
4708
4888
  for (const s of early) client.queue({ ...s, seq: link.seqBase + s.seq });
4709
4889
  early = [];
4710
4890
  client.flush();
4891
+ client.keepalive();
4892
+ };
4893
+ const noteOffer = () => {
4894
+ const waiting = o.open !== void 0 && link === null && fallback === null && pendingBind === null && o.stateCurrent();
4895
+ if (!waiting) return;
4896
+ offerAwaitedFrom ??= o.now();
4897
+ if (o.now() - offerAwaitedFrom > FAST_HANDSHAKE_MS) status.fallback = FastFallbackReason.NotOffered;
4898
+ };
4899
+ const announce = () => {
4900
+ if (joinedAt === null && o.stateCurrent() && room.state.players?.get(room.sessionId) !== void 0) joinedAt = o.now();
4901
+ const snapshots = status.snapshots - snapshotsBefore;
4902
+ status.transport = status.live && snapshots > 0 ? FastTransport.Udp : FastTransport.WebSocket;
4903
+ if (status.returns !== returnsSaid) {
4904
+ returnsSaid = status.returns;
4905
+ announcer.reset();
4906
+ }
4907
+ const fallback2 = status.fallback === FastFallbackReason.NotOffered ? null : status.fallback;
4908
+ const line = announcer.observe({ asked: !o.off, udp: o.open !== void 0, joinedMs: joinedAt === null ? null : o.now() - joinedAt, url: boundUrl, snapshots, fallback: fallback2 });
4909
+ if (line === null) return;
4910
+ status.line = line;
4911
+ o.log(line);
4711
4912
  };
4712
4913
  if (o.open !== void 0) {
4713
4914
  room.onMessage(FastMessage.Bind, (m) => bind(m));
@@ -4720,6 +4921,10 @@ function fastSeat(o) {
4720
4921
  return {
4721
4922
  status,
4722
4923
  ack,
4924
+ settleAck() {
4925
+ if (link === null) return ack();
4926
+ return Math.min(ack(), settle2.through(guard?.webSocketTick() ?? Number(room.state.tick ?? 0)));
4927
+ },
4723
4928
  sent(seq, tick, input) {
4724
4929
  if (o.open === void 0 || fallback !== null) return;
4725
4930
  const render = renderStamp();
@@ -4735,8 +4940,20 @@ function fastSeat(o) {
4735
4940
  link?.client.flush();
4736
4941
  },
4737
4942
  poll() {
4943
+ link?.client.framed();
4738
4944
  const reason = link?.client.due() ?? null;
4739
4945
  if (reason !== null) fallBack(reason);
4946
+ link?.client.keepalive();
4947
+ probe?.client.framed();
4948
+ const probeReason = probe?.client.due() ?? null;
4949
+ if (probeReason !== null) abandonProbe(probeReason);
4950
+ probe?.client.keepalive();
4951
+ if (retryAt !== null && o.now() >= retryAt && probe === null && fallback !== null) {
4952
+ room.send(FastMessage.Again, {});
4953
+ scheduleRetry();
4954
+ }
4955
+ noteOffer();
4956
+ announce();
4740
4957
  },
4741
4958
  motionKnown(id) {
4742
4959
  return link === null || link.client.motionOf(MotionRecordKind.Entity, id) !== void 0;
@@ -4749,15 +4966,30 @@ function fastSeat(o) {
4749
4966
  dropped() {
4750
4967
  link?.client.close();
4751
4968
  link = null;
4969
+ probe?.client.close();
4970
+ probe = null;
4971
+ retryAt = null;
4972
+ attempt = 0;
4973
+ liveSince = null;
4974
+ hostReturns = false;
4752
4975
  guard?.forget();
4753
4976
  fallback = null;
4754
4977
  fastAck = 0;
4755
4978
  early = [];
4756
4979
  pendingBind = null;
4980
+ offerAwaitedFrom = null;
4757
4981
  status.live = false;
4758
- status.fallback = null;
4982
+ status.fallback = o.neverAsked;
4983
+ status.transport = FastTransport.WebSocket;
4984
+ announcer.reset();
4985
+ joinedAt = null;
4986
+ boundUrl = null;
4987
+ snapshotsBefore = status.snapshots;
4759
4988
  },
4760
4989
  dispose() {
4990
+ stopTimer?.();
4991
+ probe?.client.close();
4992
+ probe = null;
4761
4993
  link?.client.close();
4762
4994
  link = null;
4763
4995
  status.live = false;
@@ -4809,7 +5041,7 @@ function motionGuard(room, live) {
4809
5041
  const known = fields.get(field);
4810
5042
  if (known === void 0) fields.set(field, { fast: value, ws: ref[field] ?? BLANK_MOTION[field] });
4811
5043
  else known.fast = value;
4812
- writeField2(ref, field, value, changes);
5044
+ writeField(ref, field, value, changes);
4813
5045
  };
4814
5046
  const expose = () => {
4815
5047
  learn();
@@ -4868,6 +5100,9 @@ function motionGuard(room, live) {
4868
5100
  }
4869
5101
  });
4870
5102
  return {
5103
+ webSocketTick() {
5104
+ return Number(held.get(state)?.get("tick")?.ws ?? room.state.tick ?? 0);
5105
+ },
4871
5106
  write(s) {
4872
5107
  learn();
4873
5108
  const changes = [];
@@ -4886,7 +5121,7 @@ function motionGuard(room, live) {
4886
5121
  const resent = room.state.physics !== void 0;
4887
5122
  for (const [ref, fields] of held) {
4888
5123
  if (!current(ref) || resent && owners.get(ref)?.kind === MotionRecordKind.Entity) continue;
4889
- for (const [field, f] of fields) writeField2(ref, field, f.ws, changes);
5124
+ for (const [field, f] of fields) writeField(ref, field, f.ws, changes);
4890
5125
  }
4891
5126
  held.clear();
4892
5127
  if (changes.length === 0) return;
@@ -4913,7 +5148,7 @@ function writeRecord(ref, record, write) {
4913
5148
  write(ref, field, value);
4914
5149
  }
4915
5150
  }
4916
- function writeField2(ref, field, value, changes) {
5151
+ function writeField(ref, field, value, changes) {
4917
5152
  const previousValue = ref[field];
4918
5153
  if (previousValue === value) return;
4919
5154
  ref[field] = value;
@@ -4924,6 +5159,29 @@ function writeField2(ref, field, value, changes) {
4924
5159
  import { Client, getStateCallbacks } from "@colyseus/sdk";
4925
5160
  import { Predict } from "@colyseus/sdk/predict";
4926
5161
 
5162
+ // src/authority/correction.ts
5163
+ var VISIBLE_CORRECTION_METRES = 0.05;
5164
+ var CORRECTION_BUCKET_METRES = [0.5, 1, 5, 20];
5165
+ var PLACE = /* @__PURE__ */ new Set(["x", "y", "z"]);
5166
+ var UNCOUNTED = /* @__PURE__ */ new Set(["mstate", "cap", "spin", "fuel"]);
5167
+ var TWO_PI4 = 2 * Math.PI;
5168
+ function correctionSizes(fields, before, after) {
5169
+ let squares = 0;
5170
+ let state = 0;
5171
+ fields.forEach((field, i) => {
5172
+ if (UNCOUNTED.has(field)) return;
5173
+ const delta = (after[i] ?? 0) - (before[i] ?? 0);
5174
+ if (PLACE.has(field)) squares += delta * delta;
5175
+ else state = Math.max(state, Math.abs(field === "yaw" ? delta - TWO_PI4 * Math.floor((delta + Math.PI) / TWO_PI4) : delta));
5176
+ });
5177
+ return { place: Math.sqrt(squares), state };
5178
+ }
5179
+ var isVisibleCorrection = (place2, unitsPerMetre) => place2 > VISIBLE_CORRECTION_METRES * unitsPerMetre;
5180
+ function correctionBucket(place2, unitsPerMetre) {
5181
+ const bucket = CORRECTION_BUCKET_METRES.findIndex((edge) => place2 < edge * unitsPerMetre);
5182
+ return bucket < 0 ? CORRECTION_BUCKET_METRES.length : bucket;
5183
+ }
5184
+
4927
5185
  // src/authority/tick-drawing.ts
4928
5186
  var RING = 80;
4929
5187
  var fold = (prev, next) => prev + Math.atan2(Math.sin(next - prev), Math.cos(next - prev));
@@ -4979,8 +5237,17 @@ var TickDrawing = class {
4979
5237
  }
4980
5238
  };
4981
5239
 
5240
+ // src/authority/fast-default.ts
5241
+ function defaultFastOpener() {
5242
+ const builtin = globalThis.process?.getBuiltinModule;
5243
+ const dgram = builtin?.("node:dgram");
5244
+ const dns = builtin?.("node:dns");
5245
+ const net = builtin?.("node:net");
5246
+ if (dgram === void 0 || dns === void 0 || net === void 0) return void 0;
5247
+ return udpFastOpener({ createSocket: dgram.createSocket, lookup: dns.lookup, isIP: net.isIP });
5248
+ }
5249
+
4982
5250
  // src/authority/session.ts
4983
- var VISIBLE_CORRECTION = 0.05;
4984
5251
  var LIFT_BLEND_MS = 100;
4985
5252
  var LIFT_DRAWN = ["x", "y", "z", "yaw"];
4986
5253
  var RIDE_FRAME_DONE = 1e-3;
@@ -4990,28 +5257,30 @@ function liftLeftover(was, k, shift, decay) {
4990
5257
  if (was.k === k) return kept;
4991
5258
  return was.shift.map((s, i) => s + kept[i] - shift[i]);
4992
5259
  }
4993
- var CORRECTION_BUCKETS = [0.5, 1, 5, 20];
4994
5260
  var CAPACITY_RETRY_MS = [1e3, 2e3, 4e3];
4995
5261
  var MAX_CATCH_UP_STEPS = 8;
4996
5262
  var SIDE_STALE_MS = 250;
4997
5263
  var PROJECTILE_TTL_MIN_MS = 600;
4998
5264
  var ALL_PREDICTED_FIELDS = ["x", "y", "z", "vx", "vy", "vz", "yaw"];
4999
- var NOT_A_PLACE = /* @__PURE__ */ new Set(["mstate", "cap", "spin", "fuel"]);
5000
- var ANGLES = /* @__PURE__ */ new Set(["yaw"]);
5001
5265
  var SessionEvent = {
5002
5266
  Reconnecting: "reconnecting",
5003
5267
  Reconnected: "reconnected",
5004
- Disconnect: "disconnect"
5268
+ Disconnect: "disconnect",
5269
+ ...RejoinEvent
5005
5270
  };
5006
5271
  var SESSION_EVENTS = new Set(Object.values(SessionEvent));
5007
5272
  var MESSAGE = {
5008
5273
  protocol: `[multiplayer] the authority server refused protocol ${AUTHORITY_PROTOCOL_VERSION} (code 426) \u2014 this game was built with a @genex-ai/multiplayer that the server no longer serves. Update the package and republish.`,
5009
- notEntitled: "[multiplayer] server-authoritative rooms are not available for this game (code 402): its owner is not entitled to them. This is final; the session does not retry."
5274
+ notEntitled: "[multiplayer] server-authoritative rooms are not available for this game (code 402): its owner is not entitled to them. This is final; the session does not retry.",
5275
+ freeQuota: "[multiplayer] no new server-authoritative room for this game (code 429): its owner is on Genex's free tier and has used the free included room time for this period. Rooms already running are not affected; new rooms open again next period, or with Genex Pro."
5010
5276
  };
5011
5277
  var errorCode = (err) => err && typeof err === "object" ? err.code : void 0;
5278
+ var errorMessage = (err) => err && typeof err === "object" ? err.message : void 0;
5279
+ var isFreeQuotaRefusal = (err) => errorCode(err) === JoinError.ROOM_QUOTA && errorMessage(err) === FREE_QUOTA_REFUSAL;
5012
5280
  function explainAuthorityRefusal(err) {
5013
5281
  if (isProtocolRefusal(err)) console.error(MESSAGE.protocol);
5014
5282
  else if (errorCode(err) === JoinError.NOT_ENTITLED) console.error(MESSAGE.notEntitled);
5283
+ else if (isFreeQuotaRefusal(err)) console.error(MESSAGE.freeQuota);
5015
5284
  throw err;
5016
5285
  }
5017
5286
  var predictsPhysics = (opts) => opts.physicsCore !== void 0 && opts.predictPhysics !== false;
@@ -5026,16 +5295,29 @@ async function joinTargets(opts) {
5026
5295
  const ranked = await selectAuthorityRegions(regions, opts.probe === void 0 ? {} : { probe: opts.probe });
5027
5296
  return ranked.map((pick) => pick.url);
5028
5297
  }
5029
- async function joinAuthority(opts) {
5298
+ async function joinAuthority(opts, target) {
5299
+ const plan = planAuthorityJoin(target);
5300
+ if ("refused" in plan) throw new TypeError(`genex multiplayer: this room cannot be asked for (${plan.refused})`);
5030
5301
  const targets = await joinTargets(opts);
5031
5302
  const delays = opts.capacityRetryMs ?? CAPACITY_RETRY_MS;
5032
5303
  let state = { region: 0, refused: 0, step: 0 };
5033
5304
  for (; ; ) {
5034
5305
  const url = targets[state.region];
5035
5306
  try {
5036
- return { room: await joinOnce(new Client(url), opts), url };
5307
+ return { room: await joinOnce(new Client(url), opts, plan), url, targets };
5037
5308
  } catch (err) {
5038
- const next = errorCode(err) === JoinError.AUTHORITY_CAPACITY ? nextAttempt(state, targets.length, delays) : null;
5309
+ if (plan.method === AuthorityJoinMethod.JoinById) {
5310
+ const region = nextCodeRegion(errorCode(err), state.region, targets.length);
5311
+ if (region === null) explainAuthorityRefusal(err);
5312
+ state = { region, refused: 0, step: 0 };
5313
+ continue;
5314
+ }
5315
+ const kind = refusalKind(errorCode(err));
5316
+ if (kind === RefusalKind.NextRegion && state.region + 1 < targets.length) {
5317
+ state = { region: state.region + 1, refused: 0, step: state.step };
5318
+ continue;
5319
+ }
5320
+ const next = kind === RefusalKind.Capacity ? nextAttempt(state, targets.length, delays) : null;
5039
5321
  if (next === null) explainAuthorityRefusal(err);
5040
5322
  const wait = next.attempt.waitMs;
5041
5323
  if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait));
@@ -5043,17 +5325,23 @@ async function joinAuthority(opts) {
5043
5325
  }
5044
5326
  }
5045
5327
  }
5046
- function joinOnce(client, opts) {
5047
- const fastCaps = opts.fast === void 0 ? [] : [AuthorityCaps.FAST];
5048
- return client.joinOrCreate(RoomType.AUTHORITY, {
5328
+ var fastChoiceOf = (opts) => chooseFastSocket(opts.fast ?? defaultFastOpener());
5329
+ function joinOnce(client, opts, plan) {
5330
+ const fastCaps = fastChoiceOf(opts).open === void 0 ? [] : [AuthorityCaps.FAST, AuthorityCaps.FAST_RETURN];
5331
+ const options = {
5049
5332
  ...joinOptions(opts.room, opts.name, opts.auth),
5333
+ ...plan.options,
5050
5334
  // §17.10 *Joining*: this session decodes and draws physics bodies (physics-draw.ts, held to
5051
5335
  // the netcode bench's physics-draw vectors), so it may join a physics room.
5052
5336
  // §17.11: a client that can load the core predicts too.
5053
5337
  [AuthorityJoinOption.CAPS]: predictsPhysics(opts) ? [AuthorityCaps.PHYSICS, AuthorityCaps.PHYSICS_PREDICT, AuthorityCaps.INPUT_REPEAT, AuthorityCaps.LIFTS, AuthorityCaps.INPUT_TICK, ...fastCaps] : [AuthorityCaps.PHYSICS, AuthorityCaps.INPUT_REPEAT, AuthorityCaps.LIFTS, AuthorityCaps.INPUT_TICK, ...fastCaps],
5338
+ [AuthorityJoinOption.SDK]: AUTHORITY_SDK_LABEL,
5054
5339
  // Last, so nothing can misreport it (PROTOCOL.md §17.1: anything but 4 is refused with 426).
5055
5340
  protocol: AUTHORITY_PROTOCOL_VERSION
5056
- });
5341
+ };
5342
+ if (plan.method === AuthorityJoinMethod.JoinById) return client.joinById(plan.roomId, options);
5343
+ if (plan.method === AuthorityJoinMethod.Create) return client.create(RoomType.AUTHORITY, options);
5344
+ return client.joinOrCreate(RoomType.AUTHORITY, options);
5057
5345
  }
5058
5346
  function roomPreset(presetText) {
5059
5347
  if (!presetText) return null;
@@ -5097,8 +5385,8 @@ function heldInput(player) {
5097
5385
  }
5098
5386
  var NOT_A_BODY = { body: false, qx: 0, qy: 0, qz: 0, qw: 1, sleep: false };
5099
5387
  var NOTHING_YET = /* @__PURE__ */ new Map();
5100
- var TWO_PI4 = 2 * Math.PI;
5101
- var wrapAngle = (v) => v - TWO_PI4 * Math.floor((v + Math.PI) / TWO_PI4);
5388
+ var TWO_PI5 = 2 * Math.PI;
5389
+ var wrapAngle = (v) => v - TWO_PI5 * Math.floor((v + Math.PI) / TWO_PI5);
5102
5390
  var SEAT_DRAWINGS = [ContactDrawing.Predicted, ContactDrawing.Separate, ContactDrawing.Timeline];
5103
5391
  var DEFAULT_SEAT_DRAWING = ContactDrawing.Predicted;
5104
5392
  var renderInternals = (reconciler) => {
@@ -5107,8 +5395,35 @@ var renderInternals = (reconciler) => {
5107
5395
  return ok ? r : null;
5108
5396
  };
5109
5397
  async function connectAuthority(opts) {
5110
- const joined = await joinAuthority(opts);
5111
- return attachAuthoritySession(useSdkReconnection(joined.room), opts, joined.url);
5398
+ const joined = await joinAuthority(opts, opts.matchKey === void 0 ? {} : { matchKey: opts.matchKey });
5399
+ return comingBack(attachAuthoritySession(useSdkReconnection(joined.room), opts, joined.url), opts, joined, false);
5400
+ }
5401
+ async function createPrivateAuthority(opts) {
5402
+ const joined = await joinAuthority(opts, { createPrivate: true, ...opts.matchKey === void 0 ? {} : { matchKey: opts.matchKey } });
5403
+ return comingBack(attachAuthoritySession(useSdkReconnection(joined.room), opts, joined.url), opts, joined, true);
5404
+ }
5405
+ async function joinPrivateAuthority(code, opts) {
5406
+ const joined = await joinAuthority(opts, { code, ...opts.matchKey === void 0 ? {} : { matchKey: opts.matchKey } });
5407
+ return comingBack(attachAuthoritySession(useSdkReconnection(joined.room), opts, joined.url), opts, joined, true);
5408
+ }
5409
+ function comingBack(session, opts, joined, closed) {
5410
+ if (opts.rejoin === false) return session;
5411
+ const others = joined.targets.filter((url) => url !== joined.url);
5412
+ return survivingSession(session, {
5413
+ closed,
5414
+ targets: [joined.url, ...others],
5415
+ join: async (url, way, lost) => {
5416
+ const room = await joinOnce(new Client(url), opts, rejoinPlan(way, lost));
5417
+ const next = attachAuthoritySession(useSdkReconnection(room), opts, url);
5418
+ await waitForFirstState(room, ClientReconnection.firstStateTimeoutMs);
5419
+ return next;
5420
+ }
5421
+ });
5422
+ }
5423
+ function rejoinPlan(way, lost) {
5424
+ if (way === RejoinWay.Room) return { method: AuthorityJoinMethod.JoinById, roomId: lost.roomId, options: {} };
5425
+ if (way === RejoinWay.Successor) return { method: AuthorityJoinMethod.JoinOrCreate, options: planSuccessorJoin({ lostRoomId: lost.roomId, matchKey: lost.matchKey, closed: lost.closed }) };
5426
+ return { method: AuthorityJoinMethod.JoinOrCreate, options: lost.matchKey === "" ? {} : { matchKey: lost.matchKey } };
5112
5427
  }
5113
5428
  function attachAuthoritySession(room, opts, url) {
5114
5429
  const state = room.state;
@@ -5116,10 +5431,14 @@ function attachAuthoritySession(room, opts, url) {
5116
5431
  const playerMap = () => state.players ?? NOTHING_YET;
5117
5432
  const renderDelay = opts.renderDelayMs ?? AuthorityTiming.RENDER_DELAY_MS;
5118
5433
  const handle = room.input({ type: AuthorityInput, mode: "reliable", renderDelay });
5434
+ const fastChoice = fastChoiceOf(opts);
5119
5435
  const fast = fastSeat({
5120
5436
  room,
5121
5437
  handle,
5122
- open: opts.fast,
5438
+ open: fastChoice.open,
5439
+ neverAsked: fastChoice.fallback,
5440
+ off: opts.fast === false,
5441
+ log: opts.fastLog ?? defaultFastLog,
5123
5442
  now: opts.fastClock ?? (() => performance.now()),
5124
5443
  tickHz: () => tickHz(),
5125
5444
  stateCurrent: () => !resyncFirstState
@@ -5172,9 +5491,10 @@ function attachAuthoritySession(room, opts, url) {
5172
5491
  inputsDropped: 0,
5173
5492
  reconciles: 0,
5174
5493
  visibleCorrections: 0,
5175
- correctionBuckets: CORRECTION_BUCKETS.map(() => 0).concat(0),
5494
+ correctionBuckets: CORRECTION_BUCKET_METRES.map(() => 0).concat(0),
5176
5495
  peakCorrection: 0,
5177
5496
  lastCorrection: 0,
5497
+ peakStateCorrection: 0,
5178
5498
  reseeds: 0,
5179
5499
  bodyStates: 0,
5180
5500
  bodySnaps: 0,
@@ -5185,8 +5505,8 @@ function attachAuthoritySession(room, opts, url) {
5185
5505
  lastProjectileLeadMs: 0
5186
5506
  };
5187
5507
  const listeners = /* @__PURE__ */ new Map();
5188
- const emit = (event, payload) => {
5189
- for (const handler of listeners.get(event) ?? []) handler(payload);
5508
+ const emit = (event, ...payload) => {
5509
+ for (const handler of listeners.get(event) ?? []) handler(...payload);
5190
5510
  };
5191
5511
  let connected = true;
5192
5512
  let ended = false;
@@ -5449,20 +5769,15 @@ function attachAuthoritySession(room, opts, url) {
5449
5769
  };
5450
5770
  const noteCorrection = (predicted, before, seqBefore) => {
5451
5771
  if (predicted.reconciler.reconcileSeq === seqBefore) return;
5452
- const after = predictedPose(predicted);
5453
- let magnitude = 0;
5454
- predicted.fields.forEach((field, i) => {
5455
- if (NOT_A_PLACE.has(field)) return;
5456
- const delta = (after[i] ?? 0) - (before[i] ?? 0);
5457
- magnitude = Math.max(magnitude, Math.abs(ANGLES.has(field) ? wrapAngle(delta) : delta));
5458
- });
5772
+ const { place: place2, state: stateGap } = correctionSizes(predicted.fields, before, predictedPose(predicted));
5459
5773
  telemetry.reconciles++;
5460
- telemetry.lastCorrection = magnitude;
5461
- telemetry.peakCorrection = Math.max(telemetry.peakCorrection, magnitude);
5462
- if (magnitude > VISIBLE_CORRECTION) {
5774
+ telemetry.lastCorrection = place2;
5775
+ telemetry.peakCorrection = Math.max(telemetry.peakCorrection, place2);
5776
+ telemetry.peakStateCorrection = Math.max(telemetry.peakStateCorrection, stateGap);
5777
+ const unit2 = unitsPerMetre();
5778
+ if (isVisibleCorrection(place2, unit2)) {
5463
5779
  telemetry.visibleCorrections++;
5464
- const bucket = CORRECTION_BUCKETS.findIndex((edge) => magnitude < edge);
5465
- telemetry.correctionBuckets[bucket < 0 ? CORRECTION_BUCKETS.length : bucket] += 1;
5780
+ telemetry.correctionBuckets[correctionBucket(place2, unit2)] += 1;
5466
5781
  }
5467
5782
  };
5468
5783
  const yawBefore = (predicted) => {
@@ -5692,7 +6007,7 @@ function attachAuthoritySession(room, opts, url) {
5692
6007
  const unsettled = /* @__PURE__ */ new Map();
5693
6008
  const settleShots = () => {
5694
6009
  if (unsettled.size === 0) return;
5695
- const acked = fast.ack();
6010
+ const acked = fast.settleAck();
5696
6011
  for (const [shot, drop] of unsettled) {
5697
6012
  if (shot.epoch !== handle.epoch || shot.seq > acked || entityMap().has(shot.id)) continue;
5698
6013
  unsettled.delete(shot);
@@ -5922,10 +6237,10 @@ function attachAuthoritySession(room, opts, url) {
5922
6237
  predict.dispose();
5923
6238
  physicsPrediction.dispose();
5924
6239
  };
5925
- room.onLeave((code) => {
6240
+ room.onLeave((code, reason) => {
5926
6241
  if (ended) return;
5927
6242
  end();
5928
- emit(SessionEvent.Disconnect, code);
6243
+ emit(SessionEvent.Disconnect, code, authorityCloseCodeOf(reason));
5929
6244
  });
5930
6245
  room.onMessage("*", (type, payload) => {
5931
6246
  if (typeof type !== "string") return;
@@ -6278,6 +6593,10 @@ function attachAuthoritySession(room, opts, url) {
6278
6593
  if (!ended) trackOwn();
6279
6594
  return own ? view(own.id, own.entity) : void 0;
6280
6595
  },
6596
+ code: room.roomId,
6597
+ get matchKey() {
6598
+ return state.matchKey ?? "";
6599
+ },
6281
6600
  setWorld(next) {
6282
6601
  const checked = firstpersonWorld(next);
6283
6602
  const changed2 = checked.hash !== world.hash;
@@ -6414,7 +6733,17 @@ export {
6414
6733
  regionProbePlan,
6415
6734
  rankAuthorityRegions,
6416
6735
  capacityWalk,
6736
+ RefusalKind,
6737
+ refusalKind,
6417
6738
  selectAuthorityRegions,
6739
+ REJOIN,
6740
+ SUCCESSOR_OPEN_MS,
6741
+ RejoinWay,
6742
+ RejoinStop,
6743
+ comesBackAfter,
6744
+ rejoinStep,
6745
+ validSuccessorOf,
6746
+ planSuccessorJoin,
6418
6747
  quatMul,
6419
6748
  quatNormalize,
6420
6749
  slerpFromIdentity,
@@ -6599,19 +6928,15 @@ export {
6599
6928
  throttleState,
6600
6929
  seatLeadClock,
6601
6930
  throttleSteps,
6602
- MotionRecordKind,
6603
- RECORD_ID_MAX_CHARS,
6604
- blankEntityMotion,
6605
- blankSeatEcho,
6606
- quantizeCode,
6607
- dequantizeCode,
6608
- encodeRecord,
6609
- decodeRecord,
6610
- FastFallbackReason,
6611
- isFastBind,
6612
- heldInputs,
6613
- openFastClient,
6931
+ FastTransport,
6932
+ FastNotAsked,
6933
+ FAST_OFFER_WAIT_MS,
6934
+ udpLine,
6935
+ webSocketLine,
6936
+ fastLineAnnouncer,
6614
6937
  SEAT_DRAWINGS,
6615
6938
  DEFAULT_SEAT_DRAWING,
6616
- connectAuthority
6939
+ connectAuthority,
6940
+ createPrivateAuthority,
6941
+ joinPrivateAuthority
6617
6942
  };