@genex-ai/multiplayer 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,23 +13,24 @@ import { connect } from '@genex-ai/multiplayer';
13
13
  ## Connect
14
14
 
15
15
  ```ts
16
- import { waitForPlayer, getColyseusAuth } from '@genex-ai/embed-sdk';
16
+ import { waitForPlayer, getColyseusAuth, getColyseusUrls } from '@genex-ai/embed-sdk';
17
17
 
18
18
  type State = { x: number; y: number; z: number }; // YOUR per-player state — any JSON shape
19
19
 
20
20
  await waitForPlayer(); // identity gate — resolves for guests AND accounts
21
21
  const room = await connect<State>({
22
- url: 'wss://demo-colyseus.glotech.world', // the genex multiplayer server
22
+ urls: getColyseusUrls(), // regional relays for this session — SDK joins the fastest
23
23
  room: '<game-id>', // everyone with the same id shares a room (use the project slug)
24
24
  name: 'ada', // optional display name
25
- auth: getColyseusAuth()!, // REQUIRED — the relay rejects tokenless joins
25
+ auth: () => getColyseusAuth(), // REQUIRED — resolved fresh for this connect
26
26
  });
27
27
  ```
28
28
 
29
29
  `auth` is **required** — the relay rejects tokenless joins. Get it from `@genex-ai/embed-sdk`:
30
30
  `await waitForPlayer()` first (never `waitForAuth()` — that resolves only for signed-in accounts
31
- and keeps guests out forever), then read `getColyseusAuth()` **fresh at every `connect()`** —
32
- tokens rotate every ~10 min, so never cache the object across reconnects. Never log it.
31
+ and keeps guests out forever), then pass `() => getColyseusAuth()` so each explicit
32
+ `connect()` reads it fresh — tokens rotate every ~10 min, so never cache the object
33
+ across reconnects. Never log it.
33
34
 
34
35
  Different `room` ids are **fully isolated** — players in game A never see game B.
35
36
 
@@ -46,7 +47,7 @@ room.me.snap({ x: spawnX, y: spawnY, z: spawnZ }); // respawn/teleport: hard-res
46
47
 
47
48
  ## `players` — everyone in the room
48
49
 
49
- A fresh `Map` each read; **includes you** (skip with `id === room.id`). For remote players `p.state` is **auto-smoothed** (interpolated); for yourself it's live. `p.stateRaw` is always the latest raw snapshot (use it for hit-tests / logic).
50
+ A fresh `Map` each read; **includes you** (skip with `id === room.id`). For remote players `p.state` is **auto-smoothed** (interpolated); for yourself it's live. `p.stateRaw` is always the latest raw snapshot (use it for hit-tests / logic). A reserved reconnecting seat stays in `players` with `p.connected === false`; use `room.activePlayers` when gameplay needs the live transport quorum.
50
51
 
51
52
  ```ts
52
53
  for (const [id, p] of room.players) {
@@ -246,14 +247,14 @@ bounded extrapolation.
246
247
 
247
248
  ```ts
248
249
  import { connect } from '@genex-ai/multiplayer';
249
- import { waitForPlayer, getColyseusAuth } from '@genex-ai/embed-sdk';
250
+ import { waitForPlayer, getColyseusAuth, getColyseusUrls } from '@genex-ai/embed-sdk';
250
251
  type S = { x: number; z: number };
251
252
 
252
253
  await waitForPlayer(); // identity gate (guest OR signed-in) — see Connect above
253
254
  const room = await connect<S>({
254
- url: 'wss://demo-colyseus.glotech.world',
255
+ urls: getColyseusUrls(),
255
256
  room: GAME_ID,
256
- auth: getColyseusAuth()!, // REQUIRED — tokenless joins are rejected
257
+ auth: () => getColyseusAuth(), // REQUIRED — resolved fresh for this connect
257
258
  });
258
259
 
259
260
  const me = { x: 0, z: 0 };
@@ -289,7 +290,7 @@ import { matchmake } from "@genex-ai/multiplayer";
289
290
 
290
291
  // Pass `auth` as a FUNCTION — a handle re-joins many times (re-search/requeue) and tokens rotate
291
292
  // (~10 min); a function is read fresh each join, a static object goes stale mid-session.
292
- const mm = await matchmake<State>({ url, room: slug, auth: () => getColyseusAuth() });
293
+ const mm = await matchmake<State>({ urls, room: slug, auth: () => getColyseusAuth() });
293
294
 
294
295
  // `session` is null WHILE SEARCHING — render your own "finding a match…" HUD until it goes live.
295
296
  // each frame:
@@ -317,7 +318,7 @@ A failed queue join is **retried automatically** with backoff (~10s total), so t
317
318
  (a token mid-rotation, identity still settling on page load) never surface. Only a persistent
318
319
  failure emits `error`; the handle then idles until you call `mm.retry()`.
319
320
 
320
- **`mm.matchmaking`** (read every frame for your HUD): `status` (`searching` | `waiting` | `countdown` | `playing` | `ended`; the legacy `spectating` is never emitted — server-side spectators were removed), `queue` (`{ position, size }`), `players`, `opponents`, `teams`, `myTeam`, `scores`, `winCondition`, `config` (numeric knobs), `roundId`, `winnerId`, `message`. `championId`/`challengerId` are 1v1 aliases.
321
+ **`mm.matchmaking`** (read every frame for your HUD): `status` (`searching` | `waiting` | `countdown` | `playing` | `ended`; the legacy `spectating` is never emitted — server-side spectators were removed), `queue` (`{ position, size }`), `players` (reserved seats), `connectedPlayers` (live transports), `opponents`, `teams`, `myTeam`, `scores`, `winCondition`, `config` (numeric knobs), `roundId`, `winnerId`, `message`. `championId`/`challengerId` are 1v1 aliases. During an automatic reconnect, the SDK retains the last coherent roster and match view until the replacement room hydrates, so status does not transiently regress to `waiting`.
321
322
 
322
323
  Configure once in the game's `package.json` (reported at `genex preview` AND `genex publish`;
323
324
  removing it clears the stored config on the next preview/publish — never sent from the client):
@@ -344,11 +345,11 @@ For `preset: 'private'` games, skip the queue: a host mints an invite **code**,
344
345
  import { createPrivate, joinPrivate } from "@genex-ai/multiplayer";
345
346
 
346
347
  // host
347
- const lobby = await createPrivate<State>({ url, room: slug, auth: () => getColyseusAuth() });
348
+ const lobby = await createPrivate<State>({ urls, room: slug, auth: () => getColyseusAuth() });
348
349
  share(lobby.code); // e.g. show it in the UI
349
350
 
350
351
  // friend (elsewhere)
351
- const joined = await joinPrivate<State>(code, { url, room: slug, auth: () => getColyseusAuth() });
352
+ const joined = await joinPrivate<State>(code, { urls, room: slug, auth: () => getColyseusAuth() });
352
353
 
353
354
  // then identical to matchmake(): .session, .matchmaking, eliminated()/score()/finish(), cancel()
354
355
  ```
package/dist/index.d.ts CHANGED
@@ -1,9 +1,14 @@
1
1
  export { PlayerState, RoomState } from './schema.js';
2
2
  import '@colyseus/schema';
3
3
 
4
+ type ColyseusAuth = {
5
+ embedToken: string;
6
+ };
7
+ type AuthSource = ColyseusAuth | (() => ColyseusAuth | undefined);
4
8
  type ConnectOptions = {
5
- /** Colyseus server url, e.g. "ws://localhost:2567". */
6
- url: string;
9
+ /** Regional relay urls (pass `getColyseusUrls()`) — the SDK pings each and connects to the
10
+ * lowest-latency one; a single entry just connects. */
11
+ urls: string[];
7
12
  /** Game id — everyone who passes the same id shares a room; different ids are
8
13
  * fully isolated. Use your project slug (or any stable per-game string). */
9
14
  room: string;
@@ -16,18 +21,19 @@ type ConnectOptions = {
16
21
  * `genexEmbed.getColyseusAuth()` AFTER `await genexEmbed.waitForPlayer()`
17
22
  * (from `@genex-ai/embed-sdk`). NOT `waitForAuth()` — that resolves only for
18
23
  * signed-in accounts and would keep guests out of multiplayer forever.
19
- * Read it fresh at every connect() call — never cache the object across
20
- * reconnects (tokens rotate every ~10 min). NEVER log this value.
24
+ * Prefer a supplier `() => getColyseusAuth()` so every explicit connect()
25
+ * call reads the current token (tokens rotate every ~10 min). A current
26
+ * value remains supported for backward compatibility. NEVER log it.
21
27
  */
22
- auth?: {
23
- embedToken: string;
24
- };
28
+ auth?: AuthSource;
25
29
  /** Extra join options passed to the room. */
26
30
  options?: Record<string, unknown>;
27
31
  };
28
32
  type PlayerView<TState> = {
29
33
  id: string;
30
34
  name: string;
35
+ /** Transport is live now. False while this reserved seat is inside reconnect grace. */
36
+ connected: boolean;
31
37
  /** State to render — for remote players this is **auto-smoothed** (interpolated);
32
38
  * for yourself it's live. Numeric fields are lerped; everything else snaps. */
33
39
  state: TState;
@@ -80,6 +86,8 @@ type Session<TState extends Record<string, unknown>> = {
80
86
  * `state` is auto-smoothed; read `stateRaw` for the raw latest value.
81
87
  */
82
88
  readonly players: Map<string, PlayerView<TState>>;
89
+ /** Connected subset of `players`; excludes seats currently held only by reconnect grace. */
90
+ readonly activePlayers: Map<string, PlayerView<TState>>;
83
91
  readonly me: {
84
92
  /**
85
93
  * Publish your synced state (replaces it wholesale). Call on a **fixed tick**
@@ -197,7 +205,7 @@ type Session<TState extends Record<string, unknown>> = {
197
205
  * await waitForPlayer(); // identity gate (guest OR signed-in)
198
206
  * const room = await connect<{ x: number; y: number; z: number }>({
199
207
  * url: 'ws://localhost:2567', room: gameId, name: 'ada',
200
- * auth: getColyseusAuth()!, // REQUIRED — the relay rejects tokenless joins
208
+ * auth: () => getColyseusAuth(), // REQUIRED — fresh on every explicit connect call
201
209
  * });
202
210
  * room.me.set({ x, y, z }); // typed + validated by the compiler
203
211
  * room.players.get(id)?.state.x; // number — auto-smoothed for remote players
@@ -219,6 +227,8 @@ type MatchmakingView = {
219
227
  };
220
228
  /** Every active participant id (N-player: 1v1, FFA, teams). */
221
229
  players: string[];
230
+ /** Participants whose transport is live now; graced seats remain only in `players`. */
231
+ connectedPlayers: string[];
222
232
  /** The other active player id(s) — everyone in `players` except you. */
223
233
  opponents: string[];
224
234
  /** teamId -> member ids for team presets; empty for FFA + 1v1. */
@@ -264,8 +274,8 @@ type Matchmaking<TState extends Record<string, unknown>> = {
264
274
  cancel(): void;
265
275
  };
266
276
  type MatchmakeOptions = {
267
- /** Colyseus server url. */
268
- url: string;
277
+ /** Regional relay urls (pass `getColyseusUrls()`) — pings each, connects to the lowest-latency one. */
278
+ urls: string[];
269
279
  /** Game id (your project slug/id) — isolates this game's matchmaking. */
270
280
  room: string;
271
281
  name?: string;
@@ -275,11 +285,7 @@ type MatchmakeOptions = {
275
285
  * requeue). Pass a FUNCTION — `() => genexEmbed.getColyseusAuth()` — so each (re)join reads a FRESH
276
286
  * token; a static object is read only once and will be rejected once it expires.
277
287
  */
278
- auth?: {
279
- embedToken: string;
280
- } | (() => {
281
- embedToken: string;
282
- } | undefined);
288
+ auth?: AuthSource;
283
289
  };
284
290
  /**
285
291
  * Join a game's matchmaking. Returns immediately in the SEARCHING state (`session` is null); the
@@ -310,4 +316,4 @@ declare function createPrivate<TState extends Record<string, unknown> = never>(o
310
316
  */
311
317
  declare function joinPrivate<TState extends Record<string, unknown> = never>(code: string, opts: MatchmakeOptions): Promise<Matchmaking<TState>>;
312
318
 
313
- export { type ConnectOptions, type MatchStatus, type MatchmakeOptions, type Matchmaking, type MatchmakingView, type ObjectControlOperation, type ObjectControlOptions, type ObjectControlReason, type ObjectControlResult, type ObjectView, type PlayerView, type PrivateMatch, type Session, connect, createPrivate, joinPrivate, matchmake };
319
+ export { type AuthSource, type ColyseusAuth, type ConnectOptions, type MatchStatus, type MatchmakeOptions, type Matchmaking, type MatchmakingView, type ObjectControlOperation, type ObjectControlOptions, type ObjectControlReason, type ObjectControlResult, type ObjectView, type PlayerView, type PrivateMatch, type Session, connect, createPrivate, joinPrivate, matchmake };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/index.ts
2
- import { Client, getStateCallbacks } from "colyseus.js";
2
+ import { Client, getStateCallbacks } from "@colyseus/sdk";
3
3
  var RELAY_ROOM_TYPE = "play";
4
4
  var QUEUE_ROOM_TYPE = "queue";
5
5
  var MATCH_ROOM_TYPE = "match";
@@ -27,6 +27,13 @@ var OBJECT_CONTROL_VERSION = 2;
27
27
  var STATE_SNAP_MSG = "__state:snap";
28
28
  var OBJECT_SNAP_MSG = "__object:snap";
29
29
  var MP_PROTOCOL = 2;
30
+ async function relayClient(opts) {
31
+ if (opts.urls.length > 1) return Client.selectByLatency(opts.urls);
32
+ const url = opts.urls[0];
33
+ if (!url) throw new Error("genex multiplayer: `urls` must contain at least one relay URL");
34
+ return new Client(url);
35
+ }
36
+ var resolveAuth = (auth) => typeof auth === "function" ? auth() : auth;
30
37
  function parseData(s) {
31
38
  if (!s) return { state: {} };
32
39
  try {
@@ -151,11 +158,11 @@ function quantile(xs, q) {
151
158
  return s[i];
152
159
  }
153
160
  async function connect(opts) {
154
- const client = new Client(opts.url);
161
+ const client = await relayClient(opts);
155
162
  const room = await client.joinOrCreate(RELAY_ROOM_TYPE, {
156
163
  gameId: opts.room,
157
164
  name: opts.name,
158
- embedToken: opts.auth?.embedToken,
165
+ embedToken: resolveAuth(opts.auth)?.embedToken,
159
166
  ...opts.options,
160
167
  __v: MP_PROTOCOL
161
168
  // tells the relay this client hides reserved `__` keys (see MP_PROTOCOL)
@@ -362,6 +369,7 @@ function attachSession(client, initialRoom) {
362
369
  };
363
370
  const parseCache = /* @__PURE__ */ new Map();
364
371
  const playerEpochs = /* @__PURE__ */ new Map();
372
+ let lastPlayersView = /* @__PURE__ */ new Map();
365
373
  const cachedParse = (id, raw) => {
366
374
  if (!raw) return {};
367
375
  const hit = parseCache.get(id);
@@ -652,7 +660,6 @@ function attachSession(client, initialRoom) {
652
660
  try {
653
661
  const next = await client.reconnect(token);
654
662
  room = next;
655
- connected = true;
656
663
  const reconnectGap = reconnectStartedAt ? nowMs() - reconnectStartedAt : 0;
657
664
  reconnectStartedAt = 0;
658
665
  if (reconnectGap > 1e3) {
@@ -661,6 +668,7 @@ function attachSession(client, initialRoom) {
661
668
  const hydrated = wireRoom(next);
662
669
  await Promise.race([hydrated, new Promise((r) => setTimeout(r, 2e3))]);
663
670
  if (room !== next || dead || intentionalLeave) return;
671
+ connected = true;
664
672
  syncHostTicks();
665
673
  for (const pending of pendingControls.values()) sendPendingControl(pending);
666
674
  emit("reconnected");
@@ -712,10 +720,22 @@ function attachSession(client, initialRoom) {
712
720
  } else {
713
721
  state = sampleSmoothed(id, localNow) ?? latest;
714
722
  }
715
- out.set(id, { id, name: p.name, state, stateRaw: raw });
723
+ out.set(id, {
724
+ id,
725
+ name: p.name,
726
+ connected: p.connected !== false,
727
+ // old relay schemas did not carry the field
728
+ state,
729
+ stateRaw: raw
730
+ });
716
731
  });
732
+ if (!connected && out.size === 0 && lastPlayersView.size > 0) return new Map(lastPlayersView);
733
+ lastPlayersView = out;
717
734
  return out;
718
735
  },
736
+ get activePlayers() {
737
+ return new Map([...session.players].filter(([, player]) => player.connected));
738
+ },
719
739
  me: {
720
740
  set(state) {
721
741
  if (SEND_TIME_KEY in state) {
@@ -888,7 +908,6 @@ function attachSession(client, initialRoom) {
888
908
  return session;
889
909
  }
890
910
  var SEARCH_BACKOFF_MS = [1e3, 2e3, 4e3];
891
- var resolveAuth = (auth) => typeof auth === "function" ? auth() : auth;
892
911
  var matchJoinOptions = (opts) => ({
893
912
  gameId: opts.room,
894
913
  name: opts.name,
@@ -905,6 +924,7 @@ function buildView(matchRoom, queueInfo) {
905
924
  role: "player",
906
925
  queue: queueInfo,
907
926
  players: [],
927
+ connectedPlayers: [],
908
928
  opponents: [],
909
929
  teams: {},
910
930
  myTeam: null,
@@ -923,6 +943,7 @@ function buildView(matchRoom, queueInfo) {
923
943
  const challengerId = m.challengerId ?? null;
924
944
  const teams = m.teams ?? {};
925
945
  const players = m.players ?? [championId, challengerId].filter((x) => !!x);
946
+ const connectedPlayers = m.connectedPlayers ?? players;
926
947
  const isPlayer = myId != null && players.includes(myId);
927
948
  const status = isPlayer ? m.phase : "spectating";
928
949
  const opponents = players.filter((id) => id !== myId);
@@ -933,6 +954,7 @@ function buildView(matchRoom, queueInfo) {
933
954
  role: isPlayer ? "player" : "spectator",
934
955
  queue: { position: 0, size: 0 },
935
956
  players,
957
+ connectedPlayers,
936
958
  opponents,
937
959
  teams,
938
960
  myTeam,
@@ -947,7 +969,7 @@ function buildView(matchRoom, queueInfo) {
947
969
  };
948
970
  }
949
971
  async function matchmake(opts) {
950
- const client = new Client(opts.url);
972
+ const client = await relayClient(opts);
951
973
  const emitters = /* @__PURE__ */ new Map();
952
974
  const emit = (event, ...args) => emitters.get(event)?.forEach((cb) => cb(...args));
953
975
  const on = ((event, cb) => {
@@ -962,9 +984,13 @@ async function matchmake(opts) {
962
984
  let queueInfo = { position: 0, size: 0 };
963
985
  let cancelled = false;
964
986
  let searching = false;
987
+ let lifecycleEpoch = 0;
988
+ let seating = false;
989
+ let requeueing = false;
965
990
  const search = async () => {
966
991
  if (cancelled || searching) return;
967
992
  searching = true;
993
+ const epoch = ++lifecycleEpoch;
968
994
  try {
969
995
  session = null;
970
996
  matchRoom = null;
@@ -988,7 +1014,7 @@ async function matchmake(opts) {
988
1014
  if (cancelled) return;
989
1015
  continue;
990
1016
  }
991
- if (cancelled) {
1017
+ if (cancelled || epoch !== lifecycleEpoch) {
992
1018
  void q.leave();
993
1019
  return;
994
1020
  }
@@ -997,10 +1023,13 @@ async function matchmake(opts) {
997
1023
  queueInfo = { position: msg.position, size: msg.size };
998
1024
  emit("queue", msg);
999
1025
  });
1000
- q.onMessage(
1001
- "seat",
1002
- (msg) => void consumeSeat(msg.reservation, msg.role)
1003
- );
1026
+ q.onMessage("seat", (msg) => {
1027
+ if (cancelled || epoch !== lifecycleEpoch || queueRoom !== q || seating) return;
1028
+ seating = true;
1029
+ queueRoom = null;
1030
+ void q.leave();
1031
+ void consumeSeat(msg.reservation, msg.role, epoch);
1032
+ });
1004
1033
  q.onLeave(() => {
1005
1034
  if (cancelled || queueRoom !== q) return;
1006
1035
  queueRoom = null;
@@ -1020,10 +1049,10 @@ async function matchmake(opts) {
1020
1049
  searching = false;
1021
1050
  }
1022
1051
  };
1023
- const consumeSeat = async (reservation, role) => {
1052
+ const consumeSeat = async (reservation, role, epoch) => {
1024
1053
  try {
1025
1054
  const mr = await client.consumeSeatReservation(reservation);
1026
- if (cancelled) {
1055
+ if (cancelled || epoch !== lifecycleEpoch) {
1027
1056
  void mr.leave();
1028
1057
  return;
1029
1058
  }
@@ -1032,7 +1061,9 @@ async function matchmake(opts) {
1032
1061
  session = s;
1033
1062
  s.on("match:start", (m) => emit("matchStart", m));
1034
1063
  s.on("match:ended", (m) => emit("matchEnded", m));
1035
- s.on("match:requeue", () => void requeue());
1064
+ s.on("match:requeue", () => {
1065
+ if (session === s) void requeue();
1066
+ });
1036
1067
  s.on("disconnect", () => {
1037
1068
  if (!cancelled && session === s) void requeue();
1038
1069
  });
@@ -1043,23 +1074,50 @@ async function matchmake(opts) {
1043
1074
  emit("matched");
1044
1075
  if (role === "spectator") emit("spectate");
1045
1076
  } catch {
1046
- void search();
1077
+ if (!cancelled && epoch === lifecycleEpoch) {
1078
+ seating = false;
1079
+ void search();
1080
+ }
1081
+ } finally {
1082
+ if (epoch === lifecycleEpoch) seating = false;
1047
1083
  }
1048
1084
  };
1049
1085
  const requeue = async () => {
1050
- session?.leave();
1051
- matchRoom = null;
1052
- session = null;
1053
- await search();
1086
+ if (cancelled || requeueing) return;
1087
+ requeueing = true;
1088
+ try {
1089
+ lifecycleEpoch++;
1090
+ seating = false;
1091
+ session?.leave();
1092
+ matchRoom = null;
1093
+ session = null;
1094
+ await search();
1095
+ } finally {
1096
+ requeueing = false;
1097
+ }
1054
1098
  };
1055
1099
  void search();
1056
1100
  const liveRoom = () => session ? roomOf(session) : null;
1101
+ let viewSession = null;
1102
+ let lastHydratedView = null;
1103
+ const stableView = () => {
1104
+ if (session !== viewSession) {
1105
+ viewSession = session;
1106
+ lastHydratedView = null;
1107
+ }
1108
+ const live = liveRoom();
1109
+ const hydrated = live?.state?.shared?.get(MATCH_KEY) != null;
1110
+ if (!hydrated && session && lastHydratedView) return lastHydratedView;
1111
+ const next = buildView(live, queueInfo);
1112
+ if (hydrated) lastHydratedView = next;
1113
+ return next;
1114
+ };
1057
1115
  return {
1058
1116
  get session() {
1059
1117
  return session;
1060
1118
  },
1061
1119
  get matchmaking() {
1062
- return buildView(liveRoom(), queueInfo);
1120
+ return stableView();
1063
1121
  },
1064
1122
  on,
1065
1123
  eliminated() {
@@ -1077,6 +1135,8 @@ async function matchmake(opts) {
1077
1135
  },
1078
1136
  cancel() {
1079
1137
  cancelled = true;
1138
+ lifecycleEpoch++;
1139
+ seating = false;
1080
1140
  session?.leave();
1081
1141
  if (!session && matchRoom) void matchRoom.leave();
1082
1142
  if (queueRoom) void queueRoom.leave();
@@ -1128,14 +1188,14 @@ function wirePrivate(client, room) {
1128
1188
  };
1129
1189
  }
1130
1190
  async function createPrivate(opts) {
1131
- const client = new Client(opts.url);
1191
+ const client = await relayClient(opts);
1132
1192
  const room = await client.create(MATCH_ROOM_TYPE, matchJoinOptions(opts));
1133
1193
  const handle = wirePrivate(client, room);
1134
1194
  Object.defineProperty(handle, "code", { value: room.roomId, enumerable: true });
1135
1195
  return handle;
1136
1196
  }
1137
1197
  async function joinPrivate(code, opts) {
1138
- const client = new Client(opts.url);
1198
+ const client = await relayClient(opts);
1139
1199
  const room = await client.joinById(code, matchJoinOptions(opts));
1140
1200
  return wirePrivate(client, room);
1141
1201
  }
package/dist/schema.d.ts CHANGED
@@ -5,10 +5,12 @@ declare const PlayerState: _colyseus_schema.SchemaWithExtendsConstructor<{
5
5
  name: "string";
6
6
  data: "string";
7
7
  epoch: "number";
8
+ connected: "boolean";
8
9
  }, _colyseus_schema.AssignableProps<_colyseus_schema.InferSchemaInstanceType<{
9
10
  name: "string";
10
11
  data: "string";
11
12
  epoch: "number";
13
+ connected: "boolean";
12
14
  }>>, typeof _colyseus_schema.Schema>;
13
15
  type PlayerState = SchemaType<typeof PlayerState>;
14
16
  declare const RoomState: _colyseus_schema.SchemaWithExtendsConstructor<{
@@ -17,15 +19,18 @@ declare const RoomState: _colyseus_schema.SchemaWithExtendsConstructor<{
17
19
  name: "string";
18
20
  data: "string";
19
21
  epoch: "number";
22
+ connected: "boolean";
20
23
  }, _colyseus_schema.AssignableProps<_colyseus_schema.InferSchemaInstanceType<{
21
24
  name: "string";
22
25
  data: "string";
23
26
  epoch: "number";
27
+ connected: "boolean";
24
28
  }>>, typeof _colyseus_schema.Schema>;
25
29
  default: MapSchema<_colyseus_schema.InferSchemaInstanceType<{
26
30
  name: "string";
27
31
  data: "string";
28
32
  epoch: "number";
33
+ connected: "boolean";
29
34
  }>, string>;
30
35
  };
31
36
  shared: {
@@ -38,15 +43,18 @@ declare const RoomState: _colyseus_schema.SchemaWithExtendsConstructor<{
38
43
  name: "string";
39
44
  data: "string";
40
45
  epoch: "number";
46
+ connected: "boolean";
41
47
  }, _colyseus_schema.AssignableProps<_colyseus_schema.InferSchemaInstanceType<{
42
48
  name: "string";
43
49
  data: "string";
44
50
  epoch: "number";
51
+ connected: "boolean";
45
52
  }>>, typeof _colyseus_schema.Schema>;
46
53
  default: MapSchema<_colyseus_schema.InferSchemaInstanceType<{
47
54
  name: "string";
48
55
  data: "string";
49
56
  epoch: "number";
57
+ connected: "boolean";
50
58
  }>, string>;
51
59
  };
52
60
  shared: {
package/dist/schema.js CHANGED
@@ -6,7 +6,10 @@ var PlayerState = schema(
6
6
  data: "string",
7
7
  // Relay-owned discontinuity epoch. Ordinary state updates keep it stable;
8
8
  // state:snap increments it so new SDKs discard interpolation history.
9
- epoch: "number"
9
+ epoch: "number",
10
+ // Seat presence is distinct from connectivity: a dropped player keeps the
11
+ // seat during reconnection grace but flips connected=false immediately.
12
+ connected: "boolean"
10
13
  },
11
14
  "PlayerState"
12
15
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/multiplayer",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "description": "The multiplayer client wrapper + shared room schema for genex.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -30,8 +30,8 @@
30
30
  "access": "public"
31
31
  },
32
32
  "dependencies": {
33
- "@colyseus/schema": "^3.0.76",
34
- "colyseus.js": "^0.16.22"
33
+ "@colyseus/schema": "^4.0.27",
34
+ "@colyseus/sdk": "^0.17.43"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "three": ">=0.160.0"