@genex-ai/multiplayer 0.8.6 → 0.8.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.
package/README.md CHANGED
@@ -250,7 +250,8 @@ else renderSearchingHud(mm.matchmaking);
250
250
  mm.on("matched", () => {/* session is live */});
251
251
  mm.on("matchStart", ({ roundId }) => {/* round began */});
252
252
  mm.on("matchEnded", ({ winnerId, scores, draw }) => {/* result screen */});
253
- mm.on("spectate", () => {/* seated as a spectator (overflow) — read-only */});
253
+ mm.on("spectate", () => {/* DEPRECATED — never fires: there are no server-side spectators anymore.
254
+ An overflow searcher now waits in the queue instead of watching. */});
254
255
  mm.on("error", (e) => {/* queue join failed even after the SDK's automatic retries — persistent
255
256
  (broken auth / server unreachable); fix the cause, then mm.retry() */});
256
257
 
@@ -267,7 +268,7 @@ A failed queue join is **retried automatically** with backoff (~10s total), so t
267
268
  (a token mid-rotation, identity still settling on page load) never surface. Only a persistent
268
269
  failure emits `error`; the handle then idles until you call `mm.retry()`.
269
270
 
270
- **`mm.matchmaking`** (read every frame for your HUD): `status` (`searching` | `waiting` | `countdown` | `playing` | `ended` | `spectating`), `queue` (`{ position, size }`), `players`, `opponents`, `teams`, `myTeam`, `scores`, `winCondition`, `config` (numeric knobs), `roundId`, `winnerId`, `message`. `championId`/`challengerId` are 1v1 aliases.
271
+ **`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.
271
272
 
272
273
  Configure once in the game's `package.json` (reported at `genex preview` AND `genex publish`;
273
274
  removing it clears the stored config on the next preview/publish — never sent from the client):
@@ -275,14 +276,16 @@ removing it clears the stored config on the next preview/publish — never sent
275
276
  ```jsonc
276
277
  "genex": {
277
278
  "matchmaking": {
278
- "preset": "arena", // duel | arena | teams | private
279
+ "preset": "arena", // open | duel | arena | teams | private
279
280
  "winCondition": "firstToScore", // lastStanding | firstToScore | highScoreInTime | firstToFinish
280
281
  "config": { "scoreTarget": 20, "maxPlayers": 8 } // numeric knobs, optional
281
282
  }
282
283
  }
283
284
  ```
284
285
 
285
- Presets: **duel** (1v1 winner-stays), **arena** (N-player FFA, join-anytime), **teams** (balanced N-v-N), **private** (invite-code lobby). Win conditions: **lastStanding**, **firstToScore**, **highScoreInTime**, **firstToFinish**; a round that hits the time cap undecided is a draw.
286
+ Presets: **open** (a room of N players, your rules — see below), **duel** (1v1 winner-stays), **arena** (N-player FFA, join-anytime), **teams** (balanced N-v-N), **private** (invite-code lobby). Win conditions (for the batteries-included presets): **lastStanding**, **firstToScore**, **highScoreInTime**, **firstToFinish**; a round that hits the time cap undecided is a draw.
287
+
288
+ **`open` — bring your own rules.** The minimal preset: the server owns only seating, capacity, and refill; you build teams, rounds, scoring, and win logic yourself on `session.shared` + host election. It runs no match loop, so `status` only goes `waiting`→`playing` (never `ended`), `scores`/`winnerId` stay empty, and **nobody is ever evicted** (no `matchEnded`, no requeue). Config knobs: `minPlayers` (quorum to flip to `playing`; default 1), `maxPlayers` (seat cap, clamped 2–64), `fill` (`1` = keep the room full — grow to max and refill a seat a leaver frees, the default; `0` = lock the room for good once it is simultaneously full (max players at once) — a match with no substitutes; a freed seat stays closed). Pick `open` when you want your own rules; pick duel/arena/teams for a ready-made match loop.
286
289
 
287
290
  ### Private lobbies — `createPrivate` / `joinPrivate`
288
291
 
package/dist/index.js CHANGED
@@ -189,6 +189,7 @@ function attachSession(client, initialRoom) {
189
189
  const objOwners = /* @__PURE__ */ new Map();
190
190
  const objRaw = /* @__PURE__ */ new Map();
191
191
  const localObjState = /* @__PURE__ */ new Map();
192
+ const pendingBridge = /* @__PURE__ */ new Map();
192
193
  let hostId;
193
194
  const testSkew = typeof globalThis !== "undefined" && globalThis.__GENEX_MP_TEST_SKEW__ || 0;
194
195
  const now = testSkew ? () => nowMs() + testSkew : nowMs;
@@ -223,6 +224,12 @@ function attachSession(client, initialRoom) {
223
224
  }
224
225
  let buf = buffers.get(id);
225
226
  if (!buf) buffers.set(id, buf = []);
227
+ const bridge = pendingBridge.get(id);
228
+ if (bridge && buf.length === 0 && sendTime != null) {
229
+ const delay = autoDelayById.get(id) ?? INTERP_MIN_MS;
230
+ buf.push({ t: t - Math.max(1, delay), state: bridge });
231
+ }
232
+ pendingBridge.delete(id);
226
233
  buf.push({ t, state });
227
234
  const cutoff = t - 1500;
228
235
  while (buf.length > 2 && buf[0].t < cutoff) buf.shift();
@@ -255,6 +262,7 @@ function attachSession(client, initialRoom) {
255
262
  smoothedAt.delete(streamKey);
256
263
  intervalsById.delete(streamKey);
257
264
  autoDelayById.delete(streamKey);
265
+ pendingBridge.delete(streamKey);
258
266
  };
259
267
  const clearStreamClock = (streamKey) => {
260
268
  buffers.delete(streamKey);
@@ -311,7 +319,13 @@ function attachSession(client, initialRoom) {
311
319
  }
312
320
  if (blob.d && typeof blob.d === "object" && Object.keys(blob.d).length > 0) {
313
321
  objRaw.set(id, blob.d);
314
- if (owner !== room.sessionId) pushSnap(key, blob.d, blob.t);
322
+ if (owner !== room.sessionId) {
323
+ if (ownerChanged) {
324
+ pendingBridge.set(key, smoothed.get(key) ?? blob.d);
325
+ } else {
326
+ pushSnap(key, blob.d, blob.t);
327
+ }
328
+ }
315
329
  }
316
330
  if (ownerChanged) emit("object", id);
317
331
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/multiplayer",
3
- "version": "0.8.6",
3
+ "version": "0.8.8",
4
4
  "description": "The multiplayer client wrapper + shared room schema for genex.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",