@genex-ai/multiplayer 0.16.1 → 0.17.0

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/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- export { PlayerState, RoomState } from './schema.js';
1
+ import { ObjectControlOperation as ObjectControlOperation$1, ObjectControlReason as ObjectControlReason$1 } from './protocol.js';
2
+ export { ObjectState, PlayerState, RoomState } from './schema.js';
2
3
  import '@colyseus/schema';
3
4
 
4
5
  /** One remote participant in the voice mesh. */
@@ -70,6 +71,16 @@ type ConnectOptions = {
70
71
  auth?: AuthSource;
71
72
  /** Extra join options passed to the room. */
72
73
  options?: Record<string, unknown>;
74
+ } & SessionOptions;
75
+ /** How remote posed players are smoothed (EXPERIMENTAL, see {@link SessionOptions.smoothing}). */
76
+ type PredictSmoothing = {
77
+ engine: 'predict';
78
+ /** Colyseus `Predict` mode. Default `'lerp'`. */
79
+ mode?: 'lerp' | 'damped';
80
+ /** `lerp` render delay in ms (Predict's default 100). */
81
+ delay?: number;
82
+ /** Predict's output spring / damped chase rate in ms (Predict's defaults when omitted). */
83
+ smoothMs?: number;
73
84
  };
74
85
  type PlayerView<TState> = {
75
86
  id: string;
@@ -81,6 +92,14 @@ type PlayerView<TState> = {
81
92
  avatarUrl: string;
82
93
  /** Transport is live now. False while this reserved seat is inside reconnect grace. */
83
94
  connected: boolean;
95
+ /** This player's stable id IN THIS GAME: the same across reloads, devices and rooms of the
96
+ * game, different in every other game, never the account id. Key anything that outlives a
97
+ * session (scores, colours, saves, "is this me") by it — `id` is a session id and changes on
98
+ * every reload. Opaque, ≤64 chars: never parse it. Relay-stamped from the verified token. */
99
+ uid: string;
100
+ /** Team index the relay assigned, or `-1` for no team: the `open` preset's `teams: N` (kept by
101
+ * `uid` for the room's life, never reshuffled) or the `teams` preset's current round. */
102
+ team: number;
84
103
  /** RENDER ONLY — for remote players this is **auto-smoothed** (interpolated) and delayed;
85
104
  * for yourself it's live. Numeric fields are lerped; everything else snaps. Use `stateRaw`
86
105
  * for every gameplay read (hits, reach, pickups, flags, physics adoption). */
@@ -103,8 +122,12 @@ type ObjectView<TState> = {
103
122
  /** The latest raw snapshot (never smoothed/delayed) — for hit-tests / logic. */
104
123
  stateRaw: TState;
105
124
  };
106
- type ObjectControlOperation = 'claim' | 'release' | 'remove';
107
- type ObjectControlReason = 'accepted' | 'already-owner' | 'held' | 'rate-limited' | 'not-owner' | 'capacity' | 'invalid' | 'missing' | 'host-authority' | 'not-host' | 'unsupported' | 'timeout' | 'aborted' | 'disconnected';
125
+ type ObjectControlOperation = ObjectControlOperation$1;
126
+ /** The relay's reasons (PROTOCOL.md §7.2), plus the ones this client settles a request with
127
+ * itself: `timeout`, `aborted` (your signal, or `leave()`), `disconnected` (the session ended). */
128
+ type ObjectControlReason = ObjectControlReason$1
129
+ /** @deprecated Never produced since protocol 3 — every relay answers control requests. */
130
+ | 'unsupported' | 'timeout' | 'aborted' | 'disconnected';
108
131
  type ObjectControlResult = {
109
132
  requestId: string;
110
133
  operation: ObjectControlOperation;
@@ -185,7 +208,8 @@ type Session<TState extends Record<string, unknown>> = {
185
208
  /** True if you are the elected room host — the earliest signed-in player still present
186
209
  * (guests host only while no account is in the room); can migrate on join as well as leave.
187
210
  * Settles within the first state patch after connect — read it in your loop or react to
188
- * `on('host')`, not once. */
211
+ * `on('host')`, not once. False while the session is reconnecting (until the fresh snapshot
212
+ * says who the host is now) and after it ends, so a write behind `isHost` is never stale. */
189
213
  readonly isHost: boolean;
190
214
  /** Session id of the current room host, or `undefined` until the first host patch lands. */
191
215
  readonly host: string | undefined;
@@ -228,27 +252,35 @@ type Session<TState extends Record<string, unknown>> = {
228
252
  limit?: number;
229
253
  }) => void): () => void;
230
254
  /**
231
- * The connection dropped unexpectedly and auto-reconnect is underway (attempts run
232
- * ~0.5s→8s backoff inside the server's grace window). Show a "reconnecting…" overlay;
233
- * keep your loop running — the world resumes seamlessly on `reconnected`.
255
+ * The connection dropped unexpectedly and auto-reconnect is underway (one event per
256
+ * attempt; the attempts back off ~0.25s→4s and all land inside the relay's 30s seat grace).
257
+ * Show a "reconnecting…" overlay; keep your loop running — the world resumes seamlessly on
258
+ * `reconnected`. Host ticks pause until then.
234
259
  */
235
260
  on(event: 'reconnecting', cb: (info: {
236
261
  attempt: number;
237
262
  code: number;
238
263
  }) => void): () => void;
239
- /** Auto-reconnect succeeded: same session id, same seat — nothing was torn down. */
264
+ /** Auto-reconnect succeeded: same session id, same seat — nothing was torn down. Fires once
265
+ * the room's state is current again (host, owners, players), so it is safe to read here. */
240
266
  on(event: 'reconnected', cb: () => void): () => void;
241
267
  /**
242
268
  * The connection is gone for good: the server closed it on purpose (restart, revoked
243
- * session, match eviction) or every reconnect attempt failed. The session is dead —
244
- * to play again, read a FRESH `getColyseusAuth()` and `connect()` anew.
269
+ * session 4401, a newer tab took the seat 4409, match eviction) or every reconnect attempt
270
+ * failed (4003). The session is dead — to play again, read a FRESH `getColyseusAuth()` and
271
+ * `connect()` anew (but not automatically after 4409: the player is playing elsewhere).
245
272
  */
246
273
  on(event: 'disconnect', cb: (code: number) => void): () => void;
274
+ /** The host opened or closed this room to new matchmaking placements (`setRoomOpen`). */
275
+ on(event: 'room:open', cb: (info: {
276
+ open: boolean;
277
+ }) => void): () => void;
247
278
  /** A custom event another client sent via `send(type, ...)`. */
248
279
  on(event: string & {}, cb: (payload: unknown) => void): () => void;
249
280
  /** Fire-and-forget event to all OTHER clients (not stored in state). Event names used
250
- * by the relay itself (`state`, `shared`, `claim`, `obj`, `release`, `destroy`, and the
251
- * `match:`/`__` prefixes) are refused with a console warning — pick another name. */
281
+ * by the relay itself (`state`, `shared`, `claim`, `release`, `destroy`, `obj`, `queue`,
282
+ * `seat`, and the `__`/`match:`/`server:`/`rate:` prefixes) and names over 128 characters
283
+ * are refused with a console warning — pick another name. */
252
284
  send(type: string, payload?: unknown): void;
253
285
  /** Text chat for this room. See {@link ChatApi}. */
254
286
  chat: ChatApi;
@@ -263,12 +295,56 @@ type Session<TState extends Record<string, unknown>> = {
263
295
  * keeps playing, and a reconnecting player still gets back in (reconnection uses a
264
296
  * reserved seat, not matchmaking).
265
297
  *
266
- * Ignored (with no error) when called by a non-host — read `isHost` first. All
267
- * clients are told either way, so you can render "closed" from the `room:open` event.
298
+ * Ignored (with no error) when called by a non-host — read `isHost` first. When the host
299
+ * calls it every client is told, so you can render "closed" from the `room:open` event.
268
300
  */
269
301
  setRoomOpen(open: boolean): void;
270
302
  /** Leave the room (deliberate — no auto-reconnect, no `disconnect` event). */
271
303
  leave(): void;
304
+ /**
305
+ * EXPERIMENTAL — the relay's server clock (PROTOCOL.md §16): milliseconds since the room was
306
+ * created, estimated from this client's heartbeat acks. `undefined` against a relay that does not
307
+ * publish its clock. For server-stamped deadlines prefer {@link timeUntil}; never use this to drive
308
+ * rendering (remote players stay on their senders' clocks).
309
+ */
310
+ serverNow(): number | undefined;
311
+ /** EXPERIMENTAL — smoothed round-trip time to the relay in ms, from heartbeat acks. `undefined`
312
+ * until the first ack, and always against a relay that does not publish its clock. It includes
313
+ * the relay's hold until its next patch (0 to ~33 ms), so read it as an upper bound. */
314
+ readonly rtt: number | undefined;
315
+ /** EXPERIMENTAL — true once enough heartbeats were acked (~3 s) that `serverNow()` is within a few ms of the room clock. */
316
+ readonly clockSynced: boolean;
317
+ /**
318
+ * Milliseconds from now until a relay deadline stamped in epoch ms (a match's `startsAt` /
319
+ * `endsBy`; negative once it passed). Uses the server clock once it is synced — immune to a wrong
320
+ * clock on the player's device — and falls back to `deadline − Date.now()` otherwise.
321
+ */
322
+ timeUntil(epochMs: number): number;
323
+ };
324
+ /** Session behaviour options shared by `connect()`, `matchmake()`, `createPrivate()` and
325
+ * `joinPrivate()`. Every one is EXPERIMENTAL and off by default: each is an A/B arm of the
326
+ * netcode bench (PROTOCOL.md §15) and stays opt-in until it wins its gate. */
327
+ type SessionOptions = {
328
+ /**
329
+ * EXPERIMENTAL — typed pose (PROTOCOL.md §6.6). Default `false`. When `true`, the reserved keys
330
+ * `x y z yaw vx vy vz anim` of `me.set()` / `me.snap()` are published as typed relay fields
331
+ * (float32 deltas instead of a re-sent JSON blob) and everything else as `data`, which is then sent
332
+ * only when it changes. If you omit `vx/vy/vz` they are derived from successive positions.
333
+ *
334
+ * Opt in for EVERY client of the game at once. Receivers then interpolate only the pose — `yaw`
335
+ * along the shortest arc (radians), `anim` switches when the rendered pose reaches it — and SNAP
336
+ * everything else in your state: a rotation you sync as a quaternion is no longer smoothed, so put
337
+ * the heading in `yaw`. A remote posed player's `state` always carries all eight pose keys.
338
+ */
339
+ pose?: boolean;
340
+ /**
341
+ * EXPERIMENTAL — remote smoothing engine for POSED players. Default `'genex'`: the SDK's own
342
+ * sender-clock interpolation (PROTOCOL.md §10.1), which every other stream keeps whatever this says.
343
+ * `'predict'` (or a {@link PredictSmoothing}) hands posed players' `x y z vx vy vz` and `yaw` to
344
+ * Colyseus `Predict` instead — receive-time stamps, a fixed delay, no dead reckoning past the newest
345
+ * sample, and `anim`/`data` applied on arrival. It exists so the bench can A/B the two engines.
346
+ */
347
+ smoothing?: 'genex' | 'predict' | PredictSmoothing;
272
348
  };
273
349
  /** One chat line, as the relay stamped it. Only `text` came from the sender's client —
274
350
  * `from` and `name` are filled in server-side from the verified session, so a player
@@ -397,6 +473,16 @@ type MatchmakingView = {
397
473
  /** Winner of the round shown while `status === 'ended'` (competitor id; null on a draw). */
398
474
  winnerId: string | null;
399
475
  message: string;
476
+ /** The relay's epoch-ms `startsAt` — when the countdown (or the result screen before the next
477
+ * round) ends — else null. */
478
+ startsAt: number | null;
479
+ /** Round time limit: the relay's epoch-ms `endsBy` (presets with a round limit), else null. */
480
+ endsBy: number | null;
481
+ /** Milliseconds until `startsAt` as of this read (≥ 0), or null. Measured on the relay's server
482
+ * clock once it is synced, so a wrong clock on the player's device cannot skew a countdown. */
483
+ startsInMs: number | null;
484
+ /** Milliseconds until `endsBy` as of this read (≥ 0), or null. Same clock as `startsInMs`. */
485
+ endsInMs: number | null;
400
486
  };
401
487
  type Matchmaking<TState extends Record<string, unknown>> = {
402
488
  /** The live game session, or null while searching for a match. */
@@ -406,11 +492,17 @@ type Matchmaking<TState extends Record<string, unknown>> = {
406
492
  /**
407
493
  * searching: (re)entered the queue. queue: your position changed ({position,size}).
408
494
  * matched: `session` is now live. matchStart: a round began ({roundId}).
409
- * matchEnded: {winnerId,loserId,draw,scores}. spectate: you're watching, not playing.
495
+ * matchEnded: {winnerId,losers,draw,scores}. spectate: you're watching, not playing.
410
496
  * error: joining the queue failed even after the SDK's automatic retries (persistent —
411
497
  * e.g. auth is broken or the server is unreachable); fix the cause, then call `retry()`.
498
+ * A relay that refuses this SDK's protocol (code 426) fails at once, without retries: the
499
+ * game needs an SDK update.
500
+ * disconnect: the match seat ended with 4409 (this player opened the game in another tab or
501
+ * device) or 4401 (signed out). The handle stops and does NOT requeue; `session` is null. Tell
502
+ * the player, and call `retry()` only if they choose to play here again. Every other terminal
503
+ * drop requeues automatically.
412
504
  */
413
- on(event: 'searching' | 'queue' | 'matched' | 'matchStart' | 'matchEnded' | 'spectate' | 'error', cb: (payload?: unknown) => void): () => void;
505
+ on(event: 'searching' | 'queue' | 'matched' | 'matchStart' | 'matchEnded' | 'spectate' | 'error' | 'disconnect', cb: (payload?: unknown) => void): () => void;
414
506
  /** Report your OWN elimination (the `lastStanding` win condition). */
415
507
  eliminated(): void;
416
508
  /** Report a score delta for yourself (the `firstToScore` / `highScoreInTime` conditions). */
@@ -439,7 +531,7 @@ type MatchmakeOptions = {
439
531
  * token; a static object is read only once and will be rejected once it expires.
440
532
  */
441
533
  auth?: AuthSource;
442
- };
534
+ } & SessionOptions;
443
535
  /**
444
536
  * Join a game's matchmaking. Returns immediately in the SEARCHING state (`session` is null); the
445
537
  * matchmaker seats you into a match room, at which point `session` becomes a live Session and a
@@ -469,4 +561,4 @@ declare function createPrivate<TState extends Record<string, unknown> = never>(o
469
561
  */
470
562
  declare function joinPrivate<TState extends Record<string, unknown> = never>(code: string, opts: MatchmakeOptions): Promise<Matchmaking<TState>>;
471
563
 
472
- export { type AuthSource, type ChatApi, type ChatMessage, 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, type VoiceApi, type VoiceJoinOptions, type VoiceParticipant, connect, createPrivate, joinPrivate, matchmake };
564
+ export { type AuthSource, type ChatApi, type ChatMessage, 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 PredictSmoothing, type PrivateMatch, type Session, type SessionOptions, type VoiceApi, type VoiceJoinOptions, type VoiceParticipant, connect, createPrivate, joinPrivate, matchmake };