@genex-ai/multiplayer 0.17.1 → 0.17.8

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 (45) hide show
  1. package/PROTOCOL.md +761 -9
  2. package/README.md +285 -1
  3. package/WORLD-GLTF.md +109 -0
  4. package/WORLD-SCENE.md +135 -0
  5. package/dist/authority/fast-udp.d.ts +15 -0
  6. package/dist/authority/fast-udp.js +14 -0
  7. package/dist/authority/fast-webtransport.d.ts +67 -0
  8. package/dist/authority/fast-webtransport.js +21 -0
  9. package/dist/authority/index.d.ts +2671 -0
  10. package/dist/authority/index.js +958 -0
  11. package/dist/authority/module.d.ts +2 -0
  12. package/dist/authority/module.js +12 -0
  13. package/dist/bridge-authority.d.ts +101 -0
  14. package/dist/bridge-authority.global.js +15 -0
  15. package/dist/bridge-authority.js +291 -0
  16. package/dist/bridge-core-Dhc73GC8.d.ts +27 -0
  17. package/dist/bridge.d.ts +26 -20
  18. package/dist/bridge.global.js +7 -7
  19. package/dist/bridge.js +17 -48
  20. package/dist/chunk-2NGXXQDS.js +1916 -0
  21. package/dist/chunk-4R7C2TLH.js +68 -0
  22. package/dist/{chunk-C52XFIB6.js → chunk-CSQEEANB.js} +217 -4
  23. package/dist/chunk-FPWP22Y7.js +143 -0
  24. package/dist/chunk-NFMGIKFZ.js +2627 -0
  25. package/dist/chunk-OD6NRSX2.js +29 -0
  26. package/dist/{chunk-3ZUPU3GT.js → chunk-QY3QQNDJ.js} +13 -122
  27. package/dist/chunk-QZOCUQJT.js +6900 -0
  28. package/dist/chunk-TSYEWPVO.js +44 -0
  29. package/dist/chunk-TVTVIMIH.js +675 -0
  30. package/dist/chunk-YWQ3ALYY.js +69 -0
  31. package/dist/fast-client--A88q0kk.d.ts +546 -0
  32. package/dist/index.d.ts +3 -5
  33. package/dist/index.js +3 -2
  34. package/dist/join-CGoInvUM.d.ts +6 -0
  35. package/dist/level-build-CbRqrYJq.d.ts +140 -0
  36. package/dist/module-CSpXrzFc.d.ts +1187 -0
  37. package/dist/physics-predict-C7ZB1gDG.d.ts +1223 -0
  38. package/dist/protocol.d.ts +222 -2
  39. package/dist/protocol.js +47 -1
  40. package/dist/shot-world-D22tqOUT.d.ts +485 -0
  41. package/dist/world-fit/index.d.ts +549 -0
  42. package/dist/world-fit/index.js +4763 -0
  43. package/dist/world-fit/three.d.ts +77 -0
  44. package/dist/world-fit/three.js +140 -0
  45. package/package.json +46 -4
@@ -0,0 +1,68 @@
1
+ // src/bridge-core.ts
2
+ import { getColyseusAuth, getColyseusUrls, initEmbed, waitForPlayer } from "@genex-ai/embed-sdk";
3
+ var embedIdentity = {
4
+ ready(gameId) {
5
+ const cfg = globalThis.__GENEX__;
6
+ if (gameId && cfg && typeof cfg.apiUrl === "string" && Array.isArray(cfg.dashboardOrigins)) {
7
+ initEmbed({
8
+ slug: gameId,
9
+ apiUrl: cfg.apiUrl,
10
+ dashboardOrigins: cfg.dashboardOrigins.filter((o) => typeof o === "string")
11
+ });
12
+ }
13
+ return waitForPlayer();
14
+ },
15
+ auth: () => getColyseusAuth(),
16
+ urls: () => getColyseusUrls()
17
+ };
18
+ var MAX_QUEUED_EVENTS = 4096;
19
+ function push(e, event) {
20
+ if (e.q.length >= MAX_QUEUED_EVENTS) {
21
+ if (e.overflowed) e.q.splice(1, 1);
22
+ else {
23
+ e.overflowed = true;
24
+ e.q.splice(0, 2, { type: "warning", message: "genex bridge: event queue full; drain it every frame (oldest events dropped)" });
25
+ }
26
+ }
27
+ e.q.push(event);
28
+ }
29
+ var warningEvent = (message) => ({ type: "warning", message: `genex bridge: ${message}` });
30
+ function eventJson(event) {
31
+ try {
32
+ return JSON.stringify(event);
33
+ } catch (err) {
34
+ const name = typeof event.kind === "string" ? event.kind : event.type;
35
+ return JSON.stringify(warningEvent(`event ${JSON.stringify(name)} dropped: JSON cannot carry it (${errorMessage(err)})`));
36
+ }
37
+ }
38
+ function drainQueueJson(e) {
39
+ e.overflowed = false;
40
+ return `[${e.q.splice(0).map(eventJson).join(",")}]`;
41
+ }
42
+ var isObject = (v) => !!v && typeof v === "object" && !Array.isArray(v);
43
+ var errorCode = (err) => {
44
+ const c = isObject(err) || err instanceof Error ? err.code : void 0;
45
+ return typeof c === "number" && Number.isFinite(c) ? c : 0;
46
+ };
47
+ var errorMessage = (err) => err instanceof Error ? err.message : typeof err === "string" ? err : "join failed";
48
+ function parseJson(json, what) {
49
+ if (typeof json !== "string") throw new TypeError(`genex bridge: ${what} must be a JSON string`);
50
+ return JSON.parse(json);
51
+ }
52
+ function parseObject(json, what) {
53
+ const v = parseJson(json, what);
54
+ if (!isObject(v)) throw new TypeError(`genex bridge: ${what} must be a JSON object`);
55
+ return v;
56
+ }
57
+
58
+ export {
59
+ embedIdentity,
60
+ push,
61
+ warningEvent,
62
+ drainQueueJson,
63
+ isObject,
64
+ errorCode,
65
+ errorMessage,
66
+ parseJson,
67
+ parseObject
68
+ };
@@ -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,100 @@ 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
+ /** The client's SDK and version, `<sdk>/<version>` (§17.1): counted so a protocol bump can see whom it would
41
+ * strand. Telemetry only: anything malformed is counted as unknown, never refused. */
42
+ SDK: "sdk",
43
+ /** The pool this join asks for (§17.2 *Choosing a room*): rooms are found by it beside the game and
44
+ * the region. Absent or empty = the game's one pool, as a client that names no room has always had.
45
+ * Malformed (authority/room-key.ts `matchKeyOf`) is refused with 400. */
46
+ MATCH_KEY: "matchKey",
47
+ /** `true` on a `create` makes a private room: no matchmaking reaches it, only its code. Any other
48
+ * type is refused with 400. */
49
+ PRIVATE: "private",
50
+ /** The room this seat lost (§17.13 *Drain and rejoin*): a room code. Rooms are found by it too, so a
51
+ * lost room's seats meet in one successor, which alone reads the room's hand-off. Absent or empty =
52
+ * no successor. Malformed is refused with 400. */
53
+ SUCCESSOR_OF: "successorOf",
54
+ /** `true` keeps a successor closed to the game's pool: the lost room was private. Any other type is
55
+ * refused with 400. */
56
+ SUCCESSOR_CLOSED: "successorClosed"
57
+ };
58
+ var AUTHORITY_SDK_LABEL = "ts/0.17.8";
59
+ var AuthorityCaps = {
60
+ /** Decodes and draws §17.10's physics bodies. A physics room refuses a join without it (426). An
61
+ * SDK sends it only once its drawing passes `physics-draw.json` and, for an engine SDK, its own
62
+ * probe on the pinned core (§17.10 *Conformance*). */
63
+ PHYSICS: "physics@1",
64
+ /** Also predicts a predicting room's bodies (§17.11): it builds the room's world from `physDesc`,
65
+ * adopts and replays keyframes, and draws its predicted bodies. A client sends it beside
66
+ * `physics@1`, never alone; a room serves the same state to both, so the cap only says what the
67
+ * client does. An SDK sends it only once it passes `physics-predict.json` (§17.11 *Conformance*). */
68
+ PHYSICS_PREDICT: "physics@2",
69
+ /** Reconciles a predicting room's repeated inputs (§17.11 *Input pacing*): it predicts its own
70
+ * entity from the state as stepped through `ackSeq + inRep`, not `ackSeq`. A predicting room repeats
71
+ * a starved seat's last input only for a client that sends it; without it a starved tick steps
72
+ * nothing, as before. An SDK sends it only once it passes `input-repeat.json`. */
73
+ INPUT_REPEAT: "repeat@1",
74
+ /** Collides with, rides and draws a room's declared lifts (§17.11 *Lifts*). A room that declares
75
+ * lifts refuses a join without it (426, `lifts@1 required`): a client that does not pose them
76
+ * would fall through them in its prediction. An SDK sends it only once it passes `lifts.json`. */
77
+ LIFTS: "lifts@1",
78
+ /** Writes into each input the server tick its prediction steps it at (`AuthorityInput.tick`, modulo
79
+ * 2^16; §17.11 *The input's tick*): the server then judges a re-timed input (a tick that steps two of
80
+ * a seat's inputs or none) at the tick it was meant for — a firstperson view on a spinning lift
81
+ * carried by the lift's turn between the two, lag compensation's lift frames. Without it the field
82
+ * is ignored, as before. */
83
+ INPUT_TICK: "tick@1",
84
+ /** Moves the seat's inputs and motion state to the fast channel (§17.12): redundant input datagrams
85
+ * and ack-delta snapshots over UDP or WebTransport, the WebSocket kept for everything else. Sent
86
+ * beside `tick@1`. An SDK sends it only once it passes `fast-transport.json` and its transport
87
+ * tests; a room that does not offer the channel ignores it. */
88
+ FAST: "fast@1",
89
+ /** The seat can come back to the fast channel after a fallback (§17.12 *Return to the channel*): it asks
90
+ * for a fresh binding, proves the path and swaps. Sent beside `fast@1`; a host that does not know it
91
+ * ignores it, and a client asks only a host whose binding says `rev` 2. */
92
+ FAST_RETURN: "fast-return@1",
93
+ MAX_CAPS: 16,
94
+ MAX_CAP_CHARS: 32
95
+ };
96
+ function capsOf(value) {
97
+ if (value === void 0) return [];
98
+ if (!Array.isArray(value) || value.length > AuthorityCaps.MAX_CAPS) return null;
99
+ const ok = value.every((cap) => typeof cap === "string" && cap.length <= AuthorityCaps.MAX_CAP_CHARS);
100
+ return ok ? [...value] : null;
101
+ }
102
+ var AUTHORITY_PHYSICS_UNITS = 6;
103
+ var SIXTIETHS_PER_SECOND = 60;
104
+ var AUTHORITY_PHYSICS_TICK_WEIGHT = { 20: 1, 30: 1, 60: 1, 120: 2 };
105
+ function authorityRoomUnits(physics, tickHz) {
106
+ return physics ? AUTHORITY_PHYSICS_UNITS * AUTHORITY_PHYSICS_TICK_WEIGHT[tickHz] : 1;
107
+ }
108
+ function authorityPhysicsBudgetSixtieths(tickHz) {
109
+ return SIXTIETHS_PER_SECOND / tickHz * AUTHORITY_PHYSICS_TICK_WEIGHT[tickHz];
110
+ }
111
+ var MS_PER_SECOND = 1e3;
112
+ function authorityTickBudgetMs(tickHz) {
113
+ return authorityPhysicsBudgetSixtieths(tickHz) * MS_PER_SECOND / SIXTIETHS_PER_SECOND;
114
+ }
115
+ var AUTHORITY_ENCODER_BUFFER_BYTES = 1048576;
22
116
  var SERVER_OWNED_JOIN_OPTIONS = ["clients", "preset", "winCondition", "config", "matchmade"];
117
+ var AUTHORITY_SERVER_OWNED_JOIN_OPTIONS = [
118
+ ...SERVER_OWNED_JOIN_OPTIONS,
119
+ "moduleSha",
120
+ "tickHz",
121
+ "maxPlayers",
122
+ "lagComp",
123
+ "seed",
124
+ "units",
125
+ "leaseId",
126
+ "region",
127
+ "createdBy"
128
+ ];
23
129
  var JoinError = {
24
130
  /** A server-owned option was sent. */
25
131
  BAD_OPTION: 400,
@@ -27,8 +133,21 @@ var JoinError = {
27
133
  AUTH_REQUIRED: 401,
28
134
  /** Token for another game ('wrong game'), or the room's guest cap is full. */
29
135
  FORBIDDEN: 403,
30
- /** `protocol` missing or outside [MIN_PROTOCOL_VERSION, PROTOCOL_VERSION]. Update the client. */
31
- PROTOCOL_UNSUPPORTED: 426
136
+ /** A join by code entered through another region than the room's (§17.2 *Choosing a room*): the
137
+ * client tries its next region. */
138
+ WRONG_REGION: 421,
139
+ /** `protocol` missing or outside the room type's window ({@link RELAY_WINDOW},
140
+ * {@link AUTHORITY_WINDOW}). Update the client. */
141
+ PROTOCOL_UNSUPPORTED: 426,
142
+ // ---- authority rooms only (§17.2); a relay room never answers these ----
143
+ /** The game's owner is not entitled to server-authoritative rooms. Terminal: do not retry. */
144
+ NOT_ENTITLED: 402,
145
+ /** The owner's room quota, concurrency cap or credits refused a NEW room. Live rooms never close for it. */
146
+ ROOM_QUOTA: 429,
147
+ /** The game has no published room module on this channel, or it could not be loaded. */
148
+ MODULE_UNAVAILABLE: 424,
149
+ /** No authority server can take the room right now, or admission could not be checked. Retry later. */
150
+ AUTHORITY_CAPACITY: 503
32
151
  };
33
152
  var STAGING_ROOM_PREFIX = "preview--";
34
153
  var roomKeyFor = (slug, channel) => channel === "staging" ? `${STAGING_ROOM_PREFIX}${slug}` : slug;
@@ -97,6 +216,14 @@ var RATE_LANES = {
97
216
  };
98
217
  var GLOBAL_RATE = { rate: 180, burst: 360 };
99
218
  var MAX_MESSAGES_PER_SECOND = 1200;
219
+ var AUTHORITY_MAX_MESSAGES_PER_SECOND = 2400;
220
+ var AUTHORITY_MAX_FRAME_BYTES = 4 * 1024;
221
+ var SeatCloseReason = {
222
+ /** Its socket passed {@link AUTHORITY_MAX_MESSAGES_PER_SECOND} (4002). */
223
+ MessageFlood: "message_flood",
224
+ /** Its input had not changed for the room's idle limit (4408). */
225
+ SeatIdle: "seat_idle"
226
+ };
100
227
  var MESSAGE_LANE = {
101
228
  [ClientMessage.STATE]: "state",
102
229
  [ClientMessage.STATE_SNAP]: "state",
@@ -141,8 +268,48 @@ var CloseCode = {
141
268
  /** The player's session was revoked (signed out from the dashboard). Do not rejoin with it. */
142
269
  REVOKED: 4401,
143
270
  /** A newer session of the same player took the seat (second tab, or the page reloaded). */
144
- SUPERSEDED: 4409
271
+ SUPERSEDED: 4409,
272
+ /** Authority rooms: the seat's input had not changed for the room's idle limit (§17.6 *Room lifetime*). */
273
+ IDLE: 4408,
274
+ // ---- authority rooms only (§17.6), terminal ----
275
+ /** The operator or the platform halted the room (kill switch, takedown, disabled game). */
276
+ MODULE_HALTED: 4500,
277
+ /** The game's room module broke its limits or threw; the room is gone. */
278
+ MODULE_FAULT: 4501
279
+ };
280
+ var AuthorityCloseCode = {
281
+ /** A call ran past its hard timeout (an infinite loop, a long microtask). */
282
+ ModuleTimeout: "module_timeout",
283
+ /** The module hit its memory limit, or the heap poll caught it near the limit. */
284
+ ModuleMemory: "module_memory",
285
+ /** The module threw (or failed to load) too many times. */
286
+ ModuleError: "module_error",
287
+ /** The module's output broke the wire limits too many times. */
288
+ ModuleOutputInvalid: "module_output_invalid",
289
+ /** The module stayed under the hard timeout but over the mean CPU budget for a whole window. */
290
+ ModuleBudget: "module_budget",
291
+ /** The operator or the platform stopped the room (kill switch, takedown, disabled game). */
292
+ Halted: "halted",
293
+ /** isolated-vm's wall-clock backstop stopped a call while the guest's own CPU stayed under its limit: the
294
+ * host stalled (contention, a GC pause, descheduled). The guest was cut off mid-call, so it is
295
+ * never called again; the room ends, but the module is not at fault. */
296
+ HostStall: "host_stall",
297
+ /** The room's physics passed the physics meter for the third strike (§17.10). */
298
+ PhysicsBudget: "physics_budget",
299
+ /** The room reached its age limit (§17.6 *Room lifetime*), announced like a drain. Not a fault. */
300
+ RoomAge: "room_age",
301
+ /** The room sent more than its ceiling for a whole window (§17.6 *The downlink budget*): its module writes
302
+ * too much state or too many events for its seats. Lower what it writes; no strike comes first. */
303
+ ModuleBandwidth: "module_bandwidth",
304
+ /** The physics core answered `internal` (C§11.3): every physics room on that instance ends. Never
305
+ * the module's fault. */
306
+ PhysicsInternal: "physics_internal"
145
307
  };
308
+ function authorityCloseCodeOf(reason) {
309
+ const known = Object.values(AuthorityCloseCode);
310
+ return known.includes(reason) ? reason : null;
311
+ }
312
+ var AUTHORITY_ROOM_ENDED_TEXT = "This match ended because of a problem with the game. Please try again.";
146
313
  var Limits = {
147
314
  ROOM_MAX_CLIENTS: 64,
148
315
  MAX_NAME_CHARS: 64,
@@ -207,6 +374,29 @@ var RelayTiming = {
207
374
  /** Embed tokens are re-checked for revocation on this cadence (REVOKED close on hit). */
208
375
  REVOCATION_SWEEP_MS: 3e4
209
376
  };
377
+ var AuthorityTiming = {
378
+ /** Fixed simulation step rate when the module does not choose one. */
379
+ TICK_HZ: 30,
380
+ /** How far behind the server clock clients render other entities (the interpolation buffer). */
381
+ RENDER_DELAY_MS: 100,
382
+ /** The furthest back lag compensation rewinds for one input's `seen` snapshot. */
383
+ MAX_REWIND_MS: 500,
384
+ /** Inputs buffered per seat before the oldest are folded away. */
385
+ INPUT_BUFFER: 32,
386
+ /** State patch cadence. The spike measured its feel gates at 50 ms; 33 is an owner call. */
387
+ PATCH_INTERVAL_MS: 50,
388
+ /** A predicting room repeats a starved seat's last input for at most this long of repeats not yet
389
+ * paid back (§17.11 *Input pacing*), then the seat holds: a vanished player stops instead of
390
+ * driving into a wall. Rocket League's client resends its last 10 frames of input in every packet
391
+ * (83 ms at 120 Hz), the gap its server's repeat bridges; 100 ms is that rounded up to whole ticks
392
+ * at every tick rate (12 at 120 Hz, 6 at 60, 3 at 30). */
393
+ INPUT_REPEAT_MAX_MS: 100,
394
+ /** One starve run is repeated for at most this long; past it the seat holds as before (§17.11 *Input
395
+ * pacing*): a stick steady before a starve is less and less likely to stay so through it, and on
396
+ * car-ball (5 runs a profile) 4 repeats a run at 120 Hz kept its corrections over 20 u at the base's
397
+ * under loss where 12 doubled them. 4 ticks at 120 Hz, 2 at 60, 1 at 30. */
398
+ INPUT_REPEAT_RUN_MS: 33
399
+ };
210
400
  var ClientReconnection = {
211
401
  enabled: true,
212
402
  minUptime: 0,
@@ -284,9 +474,25 @@ var ClockSync = {
284
474
  export {
285
475
  PROTOCOL_VERSION,
286
476
  MIN_PROTOCOL_VERSION,
477
+ AUTHORITY_PROTOCOL_VERSION,
478
+ AUTHORITY_MIN_PROTOCOL_VERSION,
479
+ RELAY_WINDOW,
480
+ AUTHORITY_WINDOW,
481
+ inProtocolWindow,
287
482
  RoomType,
288
483
  JoinOption,
484
+ AuthorityJoinOption,
485
+ AUTHORITY_SDK_LABEL,
486
+ AuthorityCaps,
487
+ capsOf,
488
+ AUTHORITY_PHYSICS_UNITS,
489
+ AUTHORITY_PHYSICS_TICK_WEIGHT,
490
+ authorityRoomUnits,
491
+ authorityPhysicsBudgetSixtieths,
492
+ authorityTickBudgetMs,
493
+ AUTHORITY_ENCODER_BUFFER_BYTES,
289
494
  SERVER_OWNED_JOIN_OPTIONS,
495
+ AUTHORITY_SERVER_OWNED_JOIN_OPTIONS,
290
496
  JoinError,
291
497
  STAGING_ROOM_PREFIX,
292
498
  roomKeyFor,
@@ -301,10 +507,17 @@ export {
301
507
  RATE_LANES,
302
508
  GLOBAL_RATE,
303
509
  MAX_MESSAGES_PER_SECOND,
510
+ AUTHORITY_MAX_MESSAGES_PER_SECOND,
511
+ AUTHORITY_MAX_FRAME_BYTES,
512
+ SeatCloseReason,
304
513
  MESSAGE_LANE,
305
514
  CloseCode,
515
+ AuthorityCloseCode,
516
+ authorityCloseCodeOf,
517
+ AUTHORITY_ROOM_ENDED_TEXT,
306
518
  Limits,
307
519
  RelayTiming,
520
+ AuthorityTiming,
308
521
  ClientReconnection,
309
522
  POSE_KEYS,
310
523
  Interpolation,
@@ -0,0 +1,143 @@
1
+ import {
2
+ ClientReconnection,
3
+ JoinError,
4
+ PROTOCOL_VERSION
5
+ } from "./chunk-CSQEEANB.js";
6
+
7
+ // src/join.ts
8
+ import { Client as Client2 } from "@colyseus/sdk";
9
+
10
+ // src/relay-select.ts
11
+ import { Client } from "@colyseus/sdk";
12
+ var RELAY_SELECT_DEFAULTS = {
13
+ SAMPLES: 3,
14
+ // Well past Colyseus's 1.5 s, which a loading web export's main thread was measured exceeding.
15
+ TIMEOUT_MS: 4e3,
16
+ // A far relay's sample lands within a few hundred ms of a near one's; a dead relay must not hold
17
+ // a working join for the full timeout.
18
+ GRACE_MS: 1e3
19
+ };
20
+ async function selectRelayUrl(urls, options = {}) {
21
+ const first = urls[0];
22
+ if (first === void 0) throw new Error("genex multiplayer: `urls` must contain at least one relay URL");
23
+ if (urls.length === 1) return first;
24
+ const probe = options.probe ?? defaultProbe;
25
+ const samples = Math.max(1, options.samples ?? RELAY_SELECT_DEFAULTS.SAMPLES);
26
+ const timeoutMs = options.timeoutMs ?? RELAY_SELECT_DEFAULTS.TIMEOUT_MS;
27
+ const graceMs = options.graceMs ?? RELAY_SELECT_DEFAULTS.GRACE_MS;
28
+ const best = urls.map(() => void 0);
29
+ await new Promise((resolve) => {
30
+ let pending = urls.length * samples;
31
+ let grace;
32
+ const ceiling = setTimeout(done, timeoutMs);
33
+ function done() {
34
+ clearTimeout(ceiling);
35
+ clearTimeout(grace);
36
+ resolve();
37
+ }
38
+ const settle = () => {
39
+ if (--pending === 0) done();
40
+ };
41
+ urls.forEach((url, i) => {
42
+ for (let n = 0; n < samples; n++) {
43
+ let sample;
44
+ try {
45
+ sample = probe(url, timeoutMs);
46
+ } catch (err) {
47
+ sample = Promise.reject(err);
48
+ }
49
+ sample.then(
50
+ (ms) => {
51
+ if (Number.isFinite(ms) && ms >= 0) {
52
+ best[i] = Math.min(best[i] ?? Infinity, ms);
53
+ grace ??= setTimeout(done, graceMs);
54
+ }
55
+ settle();
56
+ },
57
+ settle
58
+ );
59
+ }
60
+ });
61
+ });
62
+ let pick = -1;
63
+ best.forEach((ms, i) => {
64
+ if (ms !== void 0 && (pick < 0 || ms < best[pick])) pick = i;
65
+ });
66
+ if (pick < 0) {
67
+ console.warn(`genex multiplayer: no relay answered the latency probe \u2014 connecting to ${first}`);
68
+ return first;
69
+ }
70
+ return urls[pick];
71
+ }
72
+ var defaultProbe = (url, timeoutMs) => new Client(url).getLatency({ pingCount: 1, timeout: timeoutMs });
73
+
74
+ // src/join.ts
75
+ var resolveAuth = (auth) => typeof auth === "function" ? auth() : auth;
76
+ async function relayClient(opts) {
77
+ if (opts.url) return new Client2(opts.url);
78
+ return new Client2(await selectRelayUrl(opts.urls));
79
+ }
80
+ function joinOptions(gameId, name, auth) {
81
+ return {
82
+ gameId,
83
+ name,
84
+ embedToken: resolveAuth(auth)?.embedToken,
85
+ protocol: PROTOCOL_VERSION
86
+ };
87
+ }
88
+ var isProtocolRefusal = (err) => !!err && typeof err === "object" && err.code === JoinError.PROTOCOL_UNSUPPORTED;
89
+ var warnProtocolRefusal = () => console.error(
90
+ `[multiplayer] the relay refused protocol ${PROTOCOL_VERSION} (code 426) \u2014 this game was built with a @genex-ai/multiplayer that this relay no longer serves. Update the package and republish.`
91
+ );
92
+ var explainJoinRefusal = (err) => {
93
+ if (isProtocolRefusal(err)) warnProtocolRefusal();
94
+ throw err;
95
+ };
96
+
97
+ // src/room-lifecycle.ts
98
+ function useSdkReconnection(room) {
99
+ const r = room.reconnection;
100
+ r.enabled = ClientReconnection.enabled;
101
+ r.minUptime = ClientReconnection.minUptime;
102
+ r.delay = ClientReconnection.delay;
103
+ r.minDelay = ClientReconnection.minDelay;
104
+ r.maxDelay = ClientReconnection.maxDelay;
105
+ r.maxRetries = ClientReconnection.maxRetries;
106
+ r.maxEnqueuedMessages = ClientReconnection.maxEnqueuedMessages;
107
+ r.enqueuedMessages.length = 0;
108
+ return room;
109
+ }
110
+ function withoutReconnection(room) {
111
+ room.reconnection.enabled = false;
112
+ return room;
113
+ }
114
+ function waitForFirstState(room, timeoutMs) {
115
+ return new Promise((resolve) => {
116
+ const onState = () => {
117
+ clearTimeout(timer);
118
+ resolve(true);
119
+ };
120
+ const timer = setTimeout(() => {
121
+ room.onStateChange.remove(onState);
122
+ resolve(false);
123
+ }, timeoutMs);
124
+ room.onStateChange.once(onState);
125
+ });
126
+ }
127
+ function seedsInterpolation(id, selfId, sendTime) {
128
+ return id !== selfId && sendTime != null;
129
+ }
130
+
131
+ export {
132
+ RELAY_SELECT_DEFAULTS,
133
+ defaultProbe,
134
+ relayClient,
135
+ joinOptions,
136
+ isProtocolRefusal,
137
+ warnProtocolRefusal,
138
+ explainJoinRefusal,
139
+ useSdkReconnection,
140
+ withoutReconnection,
141
+ waitForFirstState,
142
+ seedsInterpolation
143
+ };