@genex-ai/multiplayer 0.17.9 → 0.17.10

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.
@@ -9,10 +9,10 @@ import {
9
9
  } from "./chunk-4R7C2TLH.js";
10
10
  import {
11
11
  connectAuthority
12
- } from "./chunk-MQYARUGU.js";
13
- import "./chunk-TEHGMFAG.js";
12
+ } from "./chunk-SQC6WTMF.js";
13
+ import "./chunk-42R7NREA.js";
14
14
  import "./chunk-YWQ3ALYY.js";
15
- import "./chunk-ZDGSUR6E.js";
15
+ import "./chunk-HHETHPGL.js";
16
16
  import "./chunk-OD6NRSX2.js";
17
17
  import {
18
18
  firstpersonWorld
@@ -22,7 +22,7 @@ import {
22
22
  CloseCode,
23
23
  ServerMessage,
24
24
  isReservedEventType
25
- } from "./chunk-WEMRRIHH.js";
25
+ } from "./chunk-7J3YWPCA.js";
26
26
 
27
27
  // src/bridge-authority.ts
28
28
  import { getAuthorityRegions, getAuthorityUrls, getColyseusAuth } from "@genex-ai/embed-sdk";
package/dist/bridge.js CHANGED
@@ -15,14 +15,14 @@ import {
15
15
  joinPrivate,
16
16
  matchmake,
17
17
  roomOf
18
- } from "./chunk-DJ2XYETS.js";
19
- import "./chunk-TEHGMFAG.js";
18
+ } from "./chunk-NSQQCLMA.js";
19
+ import "./chunk-42R7NREA.js";
20
20
  import {
21
21
  Limits,
22
22
  RESERVED_SHARED_PREFIX,
23
23
  ServerMessage,
24
24
  isReservedEventType
25
- } from "./chunk-WEMRRIHH.js";
25
+ } from "./chunk-7J3YWPCA.js";
26
26
 
27
27
  // src/bridge.ts
28
28
  var version = 2;
@@ -2,7 +2,7 @@ import {
2
2
  ClientReconnection,
3
3
  JoinError,
4
4
  PROTOCOL_VERSION
5
- } from "./chunk-WEMRRIHH.js";
5
+ } from "./chunk-7J3YWPCA.js";
6
6
 
7
7
  // src/join.ts
8
8
  import { Client as Client2 } from "@colyseus/sdk";
@@ -55,7 +55,7 @@ var AuthorityJoinOption = {
55
55
  * refused with 400. */
56
56
  SUCCESSOR_CLOSED: "successorClosed"
57
57
  };
58
- var AUTHORITY_SDK_LABEL = "ts/0.17.9";
58
+ var AUTHORITY_SDK_LABEL = "ts/0.17.10";
59
59
  var AuthorityCaps = {
60
60
  /** Decodes and draws §17.10's physics bodies. A physics room refuses a join without it (426). An
61
61
  * SDK sends it only once its drawing passes `physics-draw.json` and, for an engine SDK, its own
@@ -19,7 +19,7 @@ import {
19
19
  JoinError,
20
20
  Limits,
21
21
  isReservedEventType
22
- } from "./chunk-WEMRRIHH.js";
22
+ } from "./chunk-7J3YWPCA.js";
23
23
 
24
24
  // src/authority/room-key.ts
25
25
  var AuthorityRoomLimits = {
@@ -55,7 +55,7 @@ function matchKeyOf(value2) {
55
55
  if (value2 === void 0 || value2 === "") return "";
56
56
  return validMatchKey(value2) ? value2 : null;
57
57
  }
58
- var AuthorityJoinMethod = { JoinOrCreate: "joinOrCreate", Create: "create", JoinById: "joinById" };
58
+ var AuthorityJoinMethod = { JoinOrCreate: "joinOrCreate", Create: "create", JoinById: "joinById", Join: "join" };
59
59
  var AuthorityTargetRefusal = { BadMatchKey: "bad_match_key", BadCode: "bad_code", Conflict: "conflict" };
60
60
  function planAuthorityJoin(target) {
61
61
  const key = target.matchKey ?? "";
@@ -8,7 +8,7 @@ import {
8
8
  waitForFirstState,
9
9
  warnProtocolRefusal,
10
10
  withoutReconnection
11
- } from "./chunk-TEHGMFAG.js";
11
+ } from "./chunk-42R7NREA.js";
12
12
  import {
13
13
  ClientMessage,
14
14
  ClientReconnection,
@@ -23,7 +23,7 @@ import {
23
23
  SEND_TIME_KEY,
24
24
  ServerMessage,
25
25
  isReservedEventType
26
- } from "./chunk-WEMRRIHH.js";
26
+ } from "./chunk-7J3YWPCA.js";
27
27
 
28
28
  // src/index.ts
29
29
  import { getStateCallbacks } from "@colyseus/sdk";
@@ -5,7 +5,7 @@ import {
5
5
  joinOptions,
6
6
  useSdkReconnection,
7
7
  waitForFirstState
8
- } from "./chunk-TEHGMFAG.js";
8
+ } from "./chunk-42R7NREA.js";
9
9
  import {
10
10
  udpFastOpener
11
11
  } from "./chunk-YWQ3ALYY.js";
@@ -60,7 +60,7 @@ import {
60
60
  serverTick,
61
61
  tickInstant,
62
62
  validRoomCode
63
- } from "./chunk-ZDGSUR6E.js";
63
+ } from "./chunk-HHETHPGL.js";
64
64
  import {
65
65
  RemotePredict
66
66
  } from "./chunk-OD6NRSX2.js";
@@ -88,10 +88,17 @@ import {
88
88
  RoomType,
89
89
  ServerMessage,
90
90
  authorityCloseCodeOf
91
- } from "./chunk-WEMRRIHH.js";
91
+ } from "./chunk-7J3YWPCA.js";
92
92
 
93
93
  // src/authority/regions.ts
94
94
  var AUTHORITY_REFUSALS_PER_REGION = 2;
95
+ var AUTHORITY_MAX_PING_MS = 350;
96
+ var RegionRule = {
97
+ /** A room with players in any region in reach, nearest first; else a new room in the nearest region. */
98
+ Players: "players",
99
+ /** The nearest region alone, whoever is elsewhere. */
100
+ Nearest: "nearest"
101
+ };
95
102
  var isUrl = (u) => typeof u === "string" && u.length > 0;
96
103
  function normalizeAuthorityRegions(regions, urls) {
97
104
  const out = [];
@@ -113,6 +120,9 @@ function regionProbePlan(urls, samples) {
113
120
  }
114
121
  var answered = (ms) => typeof ms === "number" && Number.isFinite(ms) && ms >= 0;
115
122
  function rankAuthorityRegions(regions, best) {
123
+ return rankWithLatency(regions, best).map((x) => x.pick);
124
+ }
125
+ function rankWithLatency(regions, best) {
116
126
  const heard = [];
117
127
  const silent = [];
118
128
  regions.forEach((r, index) => {
@@ -127,7 +137,17 @@ function rankAuthorityRegions(regions, best) {
127
137
  (ms === null ? silent : heard).push({ index, ms, pick: { region: r.region, url } });
128
138
  });
129
139
  heard.sort((a, b) => a.ms - b.ms || a.index - b.index);
130
- return [...heard, ...silent].map((x) => x.pick);
140
+ return [...heard, ...silent];
141
+ }
142
+ function planAuthoritySearch(regions, best, options = {}) {
143
+ const ranked = rankWithLatency(regions, best);
144
+ const create = ranked.map((x) => x.pick);
145
+ const pinned = typeof options.region === "string" ? ranked.find((x) => x.pick.region === options.region) : void 0;
146
+ if (pinned !== void 0) return { search: [], create: [pinned.pick] };
147
+ if (options.rule === RegionRule.Nearest) return { search: [], create };
148
+ const limit = answered(options.maxPingMs) ? options.maxPingMs : AUTHORITY_MAX_PING_MS;
149
+ const reach = ranked.filter((x) => x.ms !== null && x.ms <= limit).map((x) => x.pick);
150
+ return { search: reach.length >= 2 ? reach : [], create };
131
151
  }
132
152
  function capacityWalk(regionCount, delays) {
133
153
  const attempts = [{ region: 0, waitMs: 0 }];
@@ -151,6 +171,16 @@ function refusalKind(code) {
151
171
  if (typeof code !== "number" || code <= 0 || DEAD_PATH_REFUSALS.has(code)) return RefusalKind.NextRegion;
152
172
  return CAPACITY_REFUSALS.has(code) ? RefusalKind.Capacity : RefusalKind.Final;
153
173
  }
174
+ var SearchRefusal = {
175
+ /** No room here (Colyseus's 521, no room matches the criteria), a dead path, a full or draining
176
+ * region: the next region is asked. */
177
+ Next: "next",
178
+ /** The server's answer about this player or game, which every region would repeat. */
179
+ Final: "final"
180
+ };
181
+ function searchRefusalKind(code) {
182
+ return refusalKind(code) === RefusalKind.Final ? SearchRefusal.Final : SearchRefusal.Next;
183
+ }
154
184
  function nextAttempt(state, regionCount, delays) {
155
185
  const refused = state.refused + 1;
156
186
  if (refused >= AUTHORITY_REFUSALS_PER_REGION && state.region + 1 < regionCount) {
@@ -164,10 +194,13 @@ function nextAttempt(state, regionCount, delays) {
164
194
  async function selectAuthorityRegions(regions, options = {}) {
165
195
  if (regions.length === 0) return [];
166
196
  if (regions.length === 1) return rankAuthorityRegions(regions, []);
197
+ return rankAuthorityRegions(regions, await probeBest(regions, options));
198
+ }
199
+ async function probeBest(regions, options) {
167
200
  const samples = Math.max(1, options.samples ?? RELAY_SELECT_DEFAULTS.SAMPLES);
168
201
  const plans = regions.map((r) => regionProbePlan(r.urls, samples));
169
202
  const results = await probeLatencies(plans, options);
170
- const best = regions.map(
203
+ return regions.map(
171
204
  (r, i) => r.urls.map((url) => {
172
205
  let min;
173
206
  plans[i]?.forEach((planned, n) => {
@@ -177,7 +210,10 @@ async function selectAuthorityRegions(regions, options = {}) {
177
210
  return min;
178
211
  })
179
212
  );
180
- return rankAuthorityRegions(regions, best);
213
+ }
214
+ async function selectAuthoritySearch(regions, search = {}, options = {}) {
215
+ if (regions.length <= 1) return planAuthoritySearch(regions, [], search);
216
+ return planAuthoritySearch(regions, await probeBest(regions, options), search);
181
217
  }
182
218
  async function probeLatencies(plans, options = {}) {
183
219
  const probe = options.probe ?? defaultProbe;
@@ -5288,17 +5324,38 @@ function resimBudgetMs() {
5288
5324
  const agent = typeof navigator === "undefined" ? "" : String(navigator.userAgent ?? "");
5289
5325
  return /Mobi|Android|iPhone|iPad/i.test(agent) ? PHYS_RESIM_BUDGET_MS.mobile : PHYS_RESIM_BUDGET_MS.desktop;
5290
5326
  }
5291
- async function joinTargets(opts) {
5292
- if (opts.url) return [opts.url];
5327
+ async function joinTargets(opts, method) {
5328
+ if (opts.url) return { search: [], create: [opts.url] };
5293
5329
  const regions = normalizeAuthorityRegions(opts.regions, opts.urls);
5294
5330
  if (regions.length === 0) throw new Error("genex multiplayer: `urls` must contain at least one authority server URL");
5295
- const ranked = await selectAuthorityRegions(regions, opts.probe === void 0 ? {} : { probe: opts.probe });
5296
- return ranked.map((pick) => pick.url);
5331
+ const pool = method === AuthorityJoinMethod.JoinOrCreate;
5332
+ const plan = await selectAuthoritySearch(
5333
+ regions,
5334
+ {
5335
+ rule: pool ? opts.regionRule : RegionRule.Nearest,
5336
+ maxPingMs: opts.maxPingMs,
5337
+ ...method === AuthorityJoinMethod.JoinById ? {} : { region: opts.region }
5338
+ },
5339
+ opts.probe === void 0 ? {} : { probe: opts.probe }
5340
+ );
5341
+ return { search: plan.search.map((pick) => pick.url), create: plan.create.map((pick) => pick.url) };
5342
+ }
5343
+ async function joinWherePlayersAre(opts, plan, search, targets) {
5344
+ for (const url of search) {
5345
+ try {
5346
+ return { room: await joinOnce(new Client(url), opts, { ...plan, method: AuthorityJoinMethod.Join }), url, targets };
5347
+ } catch (err) {
5348
+ if (searchRefusalKind(errorCode(err)) === SearchRefusal.Final) explainAuthorityRefusal(err);
5349
+ }
5350
+ }
5351
+ return null;
5297
5352
  }
5298
5353
  async function joinAuthority(opts, target) {
5299
5354
  const plan = planAuthorityJoin(target);
5300
5355
  if ("refused" in plan) throw new TypeError(`genex multiplayer: this room cannot be asked for (${plan.refused})`);
5301
- const targets = await joinTargets(opts);
5356
+ const { search, create: targets } = await joinTargets(opts, plan.method);
5357
+ const found = await joinWherePlayersAre(opts, plan, search, targets);
5358
+ if (found !== null) return found;
5302
5359
  const delays = opts.capacityRetryMs ?? CAPACITY_RETRY_MS;
5303
5360
  let state = { region: 0, refused: 0, step: 0 };
5304
5361
  for (; ; ) {
@@ -5341,6 +5398,7 @@ function joinOnce(client, opts, plan) {
5341
5398
  };
5342
5399
  if (plan.method === AuthorityJoinMethod.JoinById) return client.joinById(plan.roomId, options);
5343
5400
  if (plan.method === AuthorityJoinMethod.Create) return client.create(RoomType.AUTHORITY, options);
5401
+ if (plan.method === AuthorityJoinMethod.Join) return client.join(RoomType.AUTHORITY, options);
5344
5402
  return client.joinOrCreate(RoomType.AUTHORITY, options);
5345
5403
  }
5346
5404
  function roomPreset(presetText) {
@@ -6729,13 +6787,19 @@ function attachAuthoritySession(room, opts, url) {
6729
6787
 
6730
6788
  export {
6731
6789
  AUTHORITY_REFUSALS_PER_REGION,
6790
+ AUTHORITY_MAX_PING_MS,
6791
+ RegionRule,
6732
6792
  normalizeAuthorityRegions,
6733
6793
  regionProbePlan,
6734
6794
  rankAuthorityRegions,
6795
+ planAuthoritySearch,
6735
6796
  capacityWalk,
6736
6797
  RefusalKind,
6737
6798
  refusalKind,
6799
+ SearchRefusal,
6800
+ searchRefusalKind,
6738
6801
  selectAuthorityRegions,
6802
+ selectAuthoritySearch,
6739
6803
  REJOIN,
6740
6804
  SUCCESSOR_OPEN_MS,
6741
6805
  RejoinWay,
package/dist/index.js CHANGED
@@ -3,9 +3,9 @@ import {
3
3
  createPrivate,
4
4
  joinPrivate,
5
5
  matchmake
6
- } from "./chunk-DJ2XYETS.js";
7
- import "./chunk-TEHGMFAG.js";
8
- import "./chunk-WEMRRIHH.js";
6
+ } from "./chunk-NSQQCLMA.js";
7
+ import "./chunk-42R7NREA.js";
8
+ import "./chunk-7J3YWPCA.js";
9
9
  export {
10
10
  connect,
11
11
  createPrivate,
@@ -76,7 +76,7 @@ interface AuthorityDrainNotice {
76
76
  drainMs: number;
77
77
  }
78
78
  /** The SDK label this TypeScript client sends. tests hold it to package.json. */
79
- declare const AUTHORITY_SDK_LABEL = "ts/0.17.9";
79
+ declare const AUTHORITY_SDK_LABEL = "ts/0.17.10";
80
80
  /** Client capabilities (§17.10 *Joining*). */
81
81
  declare const AuthorityCaps: {
82
82
  /** Decodes and draws §17.10's physics bodies. A physics room refuses a join without it (426). An
package/dist/protocol.js CHANGED
@@ -50,7 +50,7 @@ import {
50
50
  isReservedEventType,
51
51
  roomKeyFor,
52
52
  slugOfRoomKey
53
- } from "./chunk-WEMRRIHH.js";
53
+ } from "./chunk-7J3YWPCA.js";
54
54
  export {
55
55
  AUTHORITY_ENCODER_BUFFER_BYTES,
56
56
  AUTHORITY_MAX_FRAME_BYTES,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/multiplayer",
3
- "version": "0.17.9",
3
+ "version": "0.17.10",
4
4
  "description": "The multiplayer client wrapper + shared room schema for genex.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",