@genex-ai/multiplayer 0.10.0 → 0.10.2

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
@@ -22,17 +22,25 @@ const room = await connect<State>({
22
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
 
37
+ `connect()` joins immediately. Use it for one ongoing drop-in world. If the game has a
38
+ Play/Online/Local/Bots menu, call it only after the online click and call `room.leave()` when the
39
+ player returns to the offline menu. Immediate connect + spawn is valid only when loading the game
40
+ already means joining the always-online world; do not put a fake Play screen in front of a player
41
+ who is already present. Use `matchmake()` below for fresh bounded sessions with queue/quorum/fair
42
+ start/backfill behavior.
43
+
36
44
  ---
37
45
 
38
46
  ## `me` — publish your own state
@@ -46,7 +54,7 @@ room.me.snap({ x: spawnX, y: spawnY, z: spawnZ }); // respawn/teleport: hard-res
46
54
 
47
55
  ## `players` — everyone in the room
48
56
 
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).
57
+ 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
58
 
51
59
  ```ts
52
60
  for (const [id, p] of room.players) {
@@ -100,7 +108,11 @@ room.objects.snap('ball', { x: 0, y: 1, z: 0, q: [0, 0, 0, 1] });
100
108
  ```
101
109
 
102
110
  Keep object state **flat** (top-level numbers + a 4-number quaternion) so it smooths, same as a
103
- player. `state` is smoothed; `stateRaw` is the raw latest (hit-tests). `objects.ids()` lists them.
111
+ player. `state` is render-only and smoothed; `stateRaw` is the raw latest for every gameplay read
112
+ (hit-tests, reach, pickups, physics adoption, discrete values). `objects.ids()` lists them.
113
+ `objects.set()` and `objects.snap()` are owner-only: SDK 0.10.2+ ignores an unowned call and warns
114
+ once per object/operation. Await `claimConfirmed()` and check `accepted` before an irreversible
115
+ write; legacy `claim(); set()` remains valid because `claim()` is optimistic locally.
104
116
 
105
117
  The relay enforces a short minimum ownership hold to prevent contact thrash. A current-owner
106
118
  reassert is a true no-op: it does not extend that hold. Legacy `claim()` remains optimistic for
@@ -253,7 +265,7 @@ await waitForPlayer(); // identity gate (guest OR signed-in) — see Connect a
253
265
  const room = await connect<S>({
254
266
  urls: getColyseusUrls(),
255
267
  room: GAME_ID,
256
- auth: getColyseusAuth()!, // REQUIRED — tokenless joins are rejected
268
+ auth: () => getColyseusAuth(), // REQUIRED — resolved fresh for this connect
257
269
  });
258
270
 
259
271
  const me = { x: 0, z: 0 };
@@ -317,7 +329,7 @@ A failed queue join is **retried automatically** with backoff (~10s total), so t
317
329
  (a token mid-rotation, identity still settling on page load) never surface. Only a persistent
318
330
  failure emits `error`; the handle then idles until you call `mm.retry()`.
319
331
 
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.
332
+ **`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
333
 
322
334
  Configure once in the game's `package.json` (reported at `genex preview` AND `genex publish`;
323
335
  removing it clears the stored config on the next preview/publish — never sent from the client):
package/dist/index.d.ts CHANGED
@@ -1,6 +1,10 @@
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
9
  /** Regional relay urls (pass `getColyseusUrls()`) — the SDK pings each and connects to the
6
10
  * lowest-latency one; a single entry just connects. */
@@ -17,20 +21,22 @@ type ConnectOptions = {
17
21
  * `genexEmbed.getColyseusAuth()` AFTER `await genexEmbed.waitForPlayer()`
18
22
  * (from `@genex-ai/embed-sdk`). NOT `waitForAuth()` — that resolves only for
19
23
  * signed-in accounts and would keep guests out of multiplayer forever.
20
- * Read it fresh at every connect() call — never cache the object across
21
- * 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.
22
27
  */
23
- auth?: {
24
- embedToken: string;
25
- };
28
+ auth?: AuthSource;
26
29
  /** Extra join options passed to the room. */
27
30
  options?: Record<string, unknown>;
28
31
  };
29
32
  type PlayerView<TState> = {
30
33
  id: string;
31
34
  name: string;
32
- /** State to render — for remote players this is **auto-smoothed** (interpolated);
33
- * for yourself it's live. Numeric fields are lerped; everything else snaps. */
35
+ /** Transport is live now. False while this reserved seat is inside reconnect grace. */
36
+ connected: boolean;
37
+ /** RENDER ONLY — for remote players this is **auto-smoothed** (interpolated) and delayed;
38
+ * for yourself it's live. Numeric fields are lerped; everything else snaps. Use `stateRaw`
39
+ * for every gameplay read (hits, reach, pickups, flags, physics adoption). */
34
40
  state: TState;
35
41
  /** The latest raw snapshot (never smoothed/delayed) — use for hit-tests / logic. */
36
42
  stateRaw: TState;
@@ -44,8 +50,8 @@ type ObjectView<TState> = {
44
50
  owner: string | undefined;
45
51
  /** True if you own it (so you should simulate + `objects.set` it). */
46
52
  isMine: boolean;
47
- /** State to render — **auto-smoothed** like a remote player when someone else owns it;
48
- * live (your last `set`) when it's yours. */
53
+ /** RENDER ONLY — **auto-smoothed** and delayed like a remote player when someone else owns it;
54
+ * live (your last `set`) when it's yours. Use `stateRaw` for every gameplay read. */
49
55
  state: TState;
50
56
  /** The latest raw snapshot (never smoothed/delayed) — for hit-tests / logic. */
51
57
  stateRaw: TState;
@@ -81,6 +87,8 @@ type Session<TState extends Record<string, unknown>> = {
81
87
  * `state` is auto-smoothed; read `stateRaw` for the raw latest value.
82
88
  */
83
89
  readonly players: Map<string, PlayerView<TState>>;
90
+ /** Connected subset of `players`; excludes seats currently held only by reconnect grace. */
91
+ readonly activePlayers: Map<string, PlayerView<TState>>;
84
92
  readonly me: {
85
93
  /**
86
94
  * Publish your synced state (replaces it wholesale). Call on a **fixed tick**
@@ -198,7 +206,7 @@ type Session<TState extends Record<string, unknown>> = {
198
206
  * await waitForPlayer(); // identity gate (guest OR signed-in)
199
207
  * const room = await connect<{ x: number; y: number; z: number }>({
200
208
  * url: 'ws://localhost:2567', room: gameId, name: 'ada',
201
- * auth: getColyseusAuth()!, // REQUIRED — the relay rejects tokenless joins
209
+ * auth: () => getColyseusAuth(), // REQUIRED — fresh on every explicit connect call
202
210
  * });
203
211
  * room.me.set({ x, y, z }); // typed + validated by the compiler
204
212
  * room.players.get(id)?.state.x; // number — auto-smoothed for remote players
@@ -220,6 +228,8 @@ type MatchmakingView = {
220
228
  };
221
229
  /** Every active participant id (N-player: 1v1, FFA, teams). */
222
230
  players: string[];
231
+ /** Participants whose transport is live now; graced seats remain only in `players`. */
232
+ connectedPlayers: string[];
223
233
  /** The other active player id(s) — everyone in `players` except you. */
224
234
  opponents: string[];
225
235
  /** teamId -> member ids for team presets; empty for FFA + 1v1. */
@@ -276,11 +286,7 @@ type MatchmakeOptions = {
276
286
  * requeue). Pass a FUNCTION — `() => genexEmbed.getColyseusAuth()` — so each (re)join reads a FRESH
277
287
  * token; a static object is read only once and will be rejected once it expires.
278
288
  */
279
- auth?: {
280
- embedToken: string;
281
- } | (() => {
282
- embedToken: string;
283
- } | undefined);
289
+ auth?: AuthSource;
284
290
  };
285
291
  /**
286
292
  * Join a game's matchmaking. Returns immediately in the SEARCHING state (`session` is null); the
@@ -311,4 +317,4 @@ declare function createPrivate<TState extends Record<string, unknown> = never>(o
311
317
  */
312
318
  declare function joinPrivate<TState extends Record<string, unknown> = never>(code: string, opts: MatchmakeOptions): Promise<Matchmaking<TState>>;
313
319
 
314
- 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 };
320
+ 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
@@ -33,6 +33,7 @@ async function relayClient(opts) {
33
33
  if (!url) throw new Error("genex multiplayer: `urls` must contain at least one relay URL");
34
34
  return new Client(url);
35
35
  }
36
+ var resolveAuth = (auth) => typeof auth === "function" ? auth() : auth;
36
37
  function parseData(s) {
37
38
  if (!s) return { state: {} };
38
39
  try {
@@ -161,7 +162,7 @@ async function connect(opts) {
161
162
  const room = await client.joinOrCreate(RELAY_ROOM_TYPE, {
162
163
  gameId: opts.room,
163
164
  name: opts.name,
164
- embedToken: opts.auth?.embedToken,
165
+ embedToken: resolveAuth(opts.auth)?.embedToken,
165
166
  ...opts.options,
166
167
  __v: MP_PROTOCOL
167
168
  // tells the relay this client hides reserved `__` keys (see MP_PROTOCOL)
@@ -207,6 +208,7 @@ function attachSession(client, initialRoom) {
207
208
  const objEpochs = /* @__PURE__ */ new Map();
208
209
  const objRaw = /* @__PURE__ */ new Map();
209
210
  const localObjState = /* @__PURE__ */ new Map();
211
+ const warnedUnownedWrites = /* @__PURE__ */ new Set();
210
212
  const pendingBridge = /* @__PURE__ */ new Map();
211
213
  let hostId;
212
214
  let objectControlVersion = 0;
@@ -214,6 +216,17 @@ function attachSession(client, initialRoom) {
214
216
  const pendingControls = /* @__PURE__ */ new Map();
215
217
  const testSkew = typeof globalThis !== "undefined" && globalThis.__GENEX_MP_TEST_SKEW__ || 0;
216
218
  const now = testSkew ? () => nowMs() + testSkew : nowMs;
219
+ const rejectUnownedWrite = (operation, id) => {
220
+ if (objOwners.get(id) === room.sessionId) return false;
221
+ const warningKey = `${operation}:${id}`;
222
+ if (!warnedUnownedWrites.has(warningKey)) {
223
+ warnedUnownedWrites.add(warningKey);
224
+ console.warn(
225
+ `[multiplayer] objects.${operation}('${id}') ignored because this session does not own the object \u2014 await objects.claimConfirmed('${id}') and check accepted before publishing`
226
+ );
227
+ }
228
+ return true;
229
+ };
217
230
  const pushSnap = (id, state, sendTime) => {
218
231
  const arrival = now();
219
232
  const t = sendTime ?? arrival;
@@ -368,6 +381,7 @@ function attachSession(client, initialRoom) {
368
381
  };
369
382
  const parseCache = /* @__PURE__ */ new Map();
370
383
  const playerEpochs = /* @__PURE__ */ new Map();
384
+ let lastPlayersView = /* @__PURE__ */ new Map();
371
385
  const cachedParse = (id, raw) => {
372
386
  if (!raw) return {};
373
387
  const hit = parseCache.get(id);
@@ -658,7 +672,6 @@ function attachSession(client, initialRoom) {
658
672
  try {
659
673
  const next = await client.reconnect(token);
660
674
  room = next;
661
- connected = true;
662
675
  const reconnectGap = reconnectStartedAt ? nowMs() - reconnectStartedAt : 0;
663
676
  reconnectStartedAt = 0;
664
677
  if (reconnectGap > 1e3) {
@@ -667,6 +680,7 @@ function attachSession(client, initialRoom) {
667
680
  const hydrated = wireRoom(next);
668
681
  await Promise.race([hydrated, new Promise((r) => setTimeout(r, 2e3))]);
669
682
  if (room !== next || dead || intentionalLeave) return;
683
+ connected = true;
670
684
  syncHostTicks();
671
685
  for (const pending of pendingControls.values()) sendPendingControl(pending);
672
686
  emit("reconnected");
@@ -718,10 +732,22 @@ function attachSession(client, initialRoom) {
718
732
  } else {
719
733
  state = sampleSmoothed(id, localNow) ?? latest;
720
734
  }
721
- out.set(id, { id, name: p.name, state, stateRaw: raw });
735
+ out.set(id, {
736
+ id,
737
+ name: p.name,
738
+ connected: p.connected !== false,
739
+ // old relay schemas did not carry the field
740
+ state,
741
+ stateRaw: raw
742
+ });
722
743
  });
744
+ if (!connected && out.size === 0 && lastPlayersView.size > 0) return new Map(lastPlayersView);
745
+ lastPlayersView = out;
723
746
  return out;
724
747
  },
748
+ get activePlayers() {
749
+ return new Map([...session.players].filter(([, player]) => player.connected));
750
+ },
725
751
  me: {
726
752
  set(state) {
727
753
  if (SEND_TIME_KEY in state) {
@@ -787,12 +813,14 @@ function attachSession(client, initialRoom) {
787
813
  },
788
814
  set(id, state) {
789
815
  const key = String(id);
816
+ if (rejectUnownedWrite("set", key)) return;
790
817
  const clean = sanitizeFinite(state);
791
818
  if (objOwners.get(key) === room.sessionId) localObjState.set(key, clean);
792
819
  send0("obj", { id: key, d: clean, t: now() });
793
820
  },
794
821
  snap(id, state) {
795
822
  const key = String(id);
823
+ if (rejectUnownedWrite("snap", key)) return;
796
824
  const clean = sanitizeFinite(state);
797
825
  if (objOwners.get(key) === room.sessionId) localObjState.set(key, clean);
798
826
  send0(OBJECT_SNAP_MSG, { id: key, d: clean, t: now() });
@@ -894,7 +922,6 @@ function attachSession(client, initialRoom) {
894
922
  return session;
895
923
  }
896
924
  var SEARCH_BACKOFF_MS = [1e3, 2e3, 4e3];
897
- var resolveAuth = (auth) => typeof auth === "function" ? auth() : auth;
898
925
  var matchJoinOptions = (opts) => ({
899
926
  gameId: opts.room,
900
927
  name: opts.name,
@@ -911,6 +938,7 @@ function buildView(matchRoom, queueInfo) {
911
938
  role: "player",
912
939
  queue: queueInfo,
913
940
  players: [],
941
+ connectedPlayers: [],
914
942
  opponents: [],
915
943
  teams: {},
916
944
  myTeam: null,
@@ -929,6 +957,7 @@ function buildView(matchRoom, queueInfo) {
929
957
  const challengerId = m.challengerId ?? null;
930
958
  const teams = m.teams ?? {};
931
959
  const players = m.players ?? [championId, challengerId].filter((x) => !!x);
960
+ const connectedPlayers = m.connectedPlayers ?? players;
932
961
  const isPlayer = myId != null && players.includes(myId);
933
962
  const status = isPlayer ? m.phase : "spectating";
934
963
  const opponents = players.filter((id) => id !== myId);
@@ -939,6 +968,7 @@ function buildView(matchRoom, queueInfo) {
939
968
  role: isPlayer ? "player" : "spectator",
940
969
  queue: { position: 0, size: 0 },
941
970
  players,
971
+ connectedPlayers,
942
972
  opponents,
943
973
  teams,
944
974
  myTeam,
@@ -968,9 +998,13 @@ async function matchmake(opts) {
968
998
  let queueInfo = { position: 0, size: 0 };
969
999
  let cancelled = false;
970
1000
  let searching = false;
1001
+ let lifecycleEpoch = 0;
1002
+ let seating = false;
1003
+ let requeueing = false;
971
1004
  const search = async () => {
972
1005
  if (cancelled || searching) return;
973
1006
  searching = true;
1007
+ const epoch = ++lifecycleEpoch;
974
1008
  try {
975
1009
  session = null;
976
1010
  matchRoom = null;
@@ -994,7 +1028,7 @@ async function matchmake(opts) {
994
1028
  if (cancelled) return;
995
1029
  continue;
996
1030
  }
997
- if (cancelled) {
1031
+ if (cancelled || epoch !== lifecycleEpoch) {
998
1032
  void q.leave();
999
1033
  return;
1000
1034
  }
@@ -1003,10 +1037,13 @@ async function matchmake(opts) {
1003
1037
  queueInfo = { position: msg.position, size: msg.size };
1004
1038
  emit("queue", msg);
1005
1039
  });
1006
- q.onMessage(
1007
- "seat",
1008
- (msg) => void consumeSeat(msg.reservation, msg.role)
1009
- );
1040
+ q.onMessage("seat", (msg) => {
1041
+ if (cancelled || epoch !== lifecycleEpoch || queueRoom !== q || seating) return;
1042
+ seating = true;
1043
+ queueRoom = null;
1044
+ void q.leave();
1045
+ void consumeSeat(msg.reservation, msg.role, epoch);
1046
+ });
1010
1047
  q.onLeave(() => {
1011
1048
  if (cancelled || queueRoom !== q) return;
1012
1049
  queueRoom = null;
@@ -1026,10 +1063,10 @@ async function matchmake(opts) {
1026
1063
  searching = false;
1027
1064
  }
1028
1065
  };
1029
- const consumeSeat = async (reservation, role) => {
1066
+ const consumeSeat = async (reservation, role, epoch) => {
1030
1067
  try {
1031
1068
  const mr = await client.consumeSeatReservation(reservation);
1032
- if (cancelled) {
1069
+ if (cancelled || epoch !== lifecycleEpoch) {
1033
1070
  void mr.leave();
1034
1071
  return;
1035
1072
  }
@@ -1038,7 +1075,9 @@ async function matchmake(opts) {
1038
1075
  session = s;
1039
1076
  s.on("match:start", (m) => emit("matchStart", m));
1040
1077
  s.on("match:ended", (m) => emit("matchEnded", m));
1041
- s.on("match:requeue", () => void requeue());
1078
+ s.on("match:requeue", () => {
1079
+ if (session === s) void requeue();
1080
+ });
1042
1081
  s.on("disconnect", () => {
1043
1082
  if (!cancelled && session === s) void requeue();
1044
1083
  });
@@ -1049,23 +1088,50 @@ async function matchmake(opts) {
1049
1088
  emit("matched");
1050
1089
  if (role === "spectator") emit("spectate");
1051
1090
  } catch {
1052
- void search();
1091
+ if (!cancelled && epoch === lifecycleEpoch) {
1092
+ seating = false;
1093
+ void search();
1094
+ }
1095
+ } finally {
1096
+ if (epoch === lifecycleEpoch) seating = false;
1053
1097
  }
1054
1098
  };
1055
1099
  const requeue = async () => {
1056
- session?.leave();
1057
- matchRoom = null;
1058
- session = null;
1059
- await search();
1100
+ if (cancelled || requeueing) return;
1101
+ requeueing = true;
1102
+ try {
1103
+ lifecycleEpoch++;
1104
+ seating = false;
1105
+ session?.leave();
1106
+ matchRoom = null;
1107
+ session = null;
1108
+ await search();
1109
+ } finally {
1110
+ requeueing = false;
1111
+ }
1060
1112
  };
1061
1113
  void search();
1062
1114
  const liveRoom = () => session ? roomOf(session) : null;
1115
+ let viewSession = null;
1116
+ let lastHydratedView = null;
1117
+ const stableView = () => {
1118
+ if (session !== viewSession) {
1119
+ viewSession = session;
1120
+ lastHydratedView = null;
1121
+ }
1122
+ const live = liveRoom();
1123
+ const hydrated = live?.state?.shared?.get(MATCH_KEY) != null;
1124
+ if (!hydrated && session && lastHydratedView) return lastHydratedView;
1125
+ const next = buildView(live, queueInfo);
1126
+ if (hydrated) lastHydratedView = next;
1127
+ return next;
1128
+ };
1063
1129
  return {
1064
1130
  get session() {
1065
1131
  return session;
1066
1132
  },
1067
1133
  get matchmaking() {
1068
- return buildView(liveRoom(), queueInfo);
1134
+ return stableView();
1069
1135
  },
1070
1136
  on,
1071
1137
  eliminated() {
@@ -1083,6 +1149,8 @@ async function matchmake(opts) {
1083
1149
  },
1084
1150
  cancel() {
1085
1151
  cancelled = true;
1152
+ lifecycleEpoch++;
1153
+ seating = false;
1086
1154
  session?.leave();
1087
1155
  if (!session && matchRoom) void matchRoom.leave();
1088
1156
  if (queueRoom) void queueRoom.leave();
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.10.0",
3
+ "version": "0.10.2",
4
4
  "description": "The multiplayer client wrapper + shared room schema for genex.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",