@genex-ai/multiplayer 0.16.2 → 0.17.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/PROTOCOL.md +625 -0
- package/README.md +113 -22
- package/dist/bridge.d.ts +89 -0
- package/dist/bridge.global.js +14 -0
- package/dist/bridge.js +558 -0
- package/dist/chunk-3ZUPU3GT.js +2252 -0
- package/dist/chunk-C52XFIB6.js +312 -0
- package/dist/index.d.ts +119 -26
- package/dist/index.js +7 -1809
- package/dist/protocol.d.ts +419 -0
- package/dist/protocol.js +58 -0
- package/dist/schema.d.ts +122 -68
- package/dist/schema.js +95 -16
- package/package.json +27 -10
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
// src/protocol.ts
|
|
2
|
+
var PROTOCOL_VERSION = 3;
|
|
3
|
+
var MIN_PROTOCOL_VERSION = 3;
|
|
4
|
+
var RoomType = {
|
|
5
|
+
/** The plain relay room: joinOrCreate, fullest-first placement. */
|
|
6
|
+
PLAY: "play",
|
|
7
|
+
/** The matchmaking search pool. No game session — it only hands out seat reservations. */
|
|
8
|
+
QUEUE: "queue",
|
|
9
|
+
/** A capped match arena: seat reservation (matchmade), create (private lobby) or joinById (invite). */
|
|
10
|
+
MATCH: "match"
|
|
11
|
+
};
|
|
12
|
+
var JoinOption = {
|
|
13
|
+
/** The game's slug. Must equal the embed token's `slug` claim. */
|
|
14
|
+
GAME_ID: "gameId",
|
|
15
|
+
/** A fresh `embed:play` JWT. Required — there is no anonymous join. */
|
|
16
|
+
EMBED_TOKEN: "embedToken",
|
|
17
|
+
/** Display-name fallback when the token carries none. */
|
|
18
|
+
NAME: "name",
|
|
19
|
+
/** {@link PROTOCOL_VERSION}. Required. */
|
|
20
|
+
PROTOCOL: "protocol"
|
|
21
|
+
};
|
|
22
|
+
var SERVER_OWNED_JOIN_OPTIONS = ["clients", "preset", "winCondition", "config", "matchmade"];
|
|
23
|
+
var JoinError = {
|
|
24
|
+
/** A server-owned option was sent. */
|
|
25
|
+
BAD_OPTION: 400,
|
|
26
|
+
/** No token, an invalid/expired token, or a revoked session. */
|
|
27
|
+
AUTH_REQUIRED: 401,
|
|
28
|
+
/** Token for another game ('wrong game'), or the room's guest cap is full. */
|
|
29
|
+
FORBIDDEN: 403,
|
|
30
|
+
/** `protocol` missing or outside [MIN_PROTOCOL_VERSION, PROTOCOL_VERSION]. Update the client. */
|
|
31
|
+
PROTOCOL_UNSUPPORTED: 426
|
|
32
|
+
};
|
|
33
|
+
var STAGING_ROOM_PREFIX = "preview--";
|
|
34
|
+
var roomKeyFor = (slug, channel) => channel === "staging" ? `${STAGING_ROOM_PREFIX}${slug}` : slug;
|
|
35
|
+
var slugOfRoomKey = (key) => key.startsWith(STAGING_ROOM_PREFIX) ? key.slice(STAGING_ROOM_PREFIX.length) : key;
|
|
36
|
+
var ClientMessage = {
|
|
37
|
+
STATE: "state",
|
|
38
|
+
STATE_SNAP: "__state:snap",
|
|
39
|
+
SHARED: "shared",
|
|
40
|
+
CLAIM: "claim",
|
|
41
|
+
RELEASE: "release",
|
|
42
|
+
DESTROY: "destroy",
|
|
43
|
+
OBJECT: "obj",
|
|
44
|
+
OBJECT_SNAP: "__object:snap",
|
|
45
|
+
INPUT: "__input",
|
|
46
|
+
ROOM_OPEN: "__room:open",
|
|
47
|
+
CHAT: "__chat",
|
|
48
|
+
CHAT_HISTORY_REQUEST: "__chat:history:request",
|
|
49
|
+
VOICE_ICE_REQUEST: "__voice:ice:request",
|
|
50
|
+
VOICE_JOIN: "__voice:join",
|
|
51
|
+
VOICE_LEAVE: "__voice:leave",
|
|
52
|
+
VOICE_SIGNAL: "__voice:signal",
|
|
53
|
+
// match rooms only — self-reports, accepted only from an active seat
|
|
54
|
+
MATCH_ELIMINATED: "match:eliminated",
|
|
55
|
+
MATCH_SCORE: "match:score",
|
|
56
|
+
MATCH_FINISH: "match:finish",
|
|
57
|
+
// optional typed pose (§6.6) — appended in protocol 3
|
|
58
|
+
POSE: "__pose",
|
|
59
|
+
POSE_SNAP: "__pose:snap"
|
|
60
|
+
};
|
|
61
|
+
var ServerMessage = {
|
|
62
|
+
OBJECT_RESULT: "__object:result",
|
|
63
|
+
INPUT: "__input",
|
|
64
|
+
ROOM_OPEN: "__room:open",
|
|
65
|
+
CHAT: "__chat",
|
|
66
|
+
CHAT_HISTORY: "__chat:history",
|
|
67
|
+
VOICE_ICE: "__voice:ice",
|
|
68
|
+
VOICE_SIGNAL: "__voice:signal",
|
|
69
|
+
VOICE_REJECTED: "__voice:rejected",
|
|
70
|
+
RATE_DROP: "rate:drop",
|
|
71
|
+
SERVER_RESTART: "server:restart",
|
|
72
|
+
// match rooms only
|
|
73
|
+
MATCH_START: "match:start",
|
|
74
|
+
MATCH_ENDED: "match:ended",
|
|
75
|
+
MATCH_REQUEUE: "match:requeue",
|
|
76
|
+
// queue room only
|
|
77
|
+
QUEUE: "queue",
|
|
78
|
+
SEAT: "seat"
|
|
79
|
+
};
|
|
80
|
+
var RESERVED_EVENT_PREFIXES = ["__", "match:", "server:", "rate:"];
|
|
81
|
+
var RESERVED_EVENT_TYPES = ["state", "shared", "claim", "release", "destroy", "obj", "queue", "seat"];
|
|
82
|
+
function isReservedEventType(type) {
|
|
83
|
+
return RESERVED_EVENT_TYPES.includes(type) || RESERVED_EVENT_PREFIXES.some((prefix) => type.startsWith(prefix));
|
|
84
|
+
}
|
|
85
|
+
var RESERVED_SHARED_PREFIX = "__";
|
|
86
|
+
var SEND_TIME_KEY = "__t";
|
|
87
|
+
var RATE_LANES = {
|
|
88
|
+
state: { rate: 30, burst: 60, softZone: 0 },
|
|
89
|
+
control: { rate: 40, burst: 80, softZone: 0 },
|
|
90
|
+
input: { rate: 60, burst: 120, softZone: 0 },
|
|
91
|
+
bulk: { rate: 120, burst: 240, softZone: 60 },
|
|
92
|
+
chat: { rate: 2, burst: 5, softZone: 0 },
|
|
93
|
+
voice: { rate: 25, burst: 60, softZone: 0 },
|
|
94
|
+
/** `__pose` / `__pose:snap` (§6.6). Its own reserved lane, outside the global bucket like
|
|
95
|
+
* `state`: a posed player publishes pose AND (low-rate) `data`, and neither may starve the other. */
|
|
96
|
+
pose: { rate: 30, burst: 60, softZone: 0 }
|
|
97
|
+
};
|
|
98
|
+
var GLOBAL_RATE = { rate: 180, burst: 360 };
|
|
99
|
+
var MAX_MESSAGES_PER_SECOND = 1200;
|
|
100
|
+
var MESSAGE_LANE = {
|
|
101
|
+
[ClientMessage.STATE]: "state",
|
|
102
|
+
[ClientMessage.STATE_SNAP]: "state",
|
|
103
|
+
[ClientMessage.SHARED]: "bulk",
|
|
104
|
+
[ClientMessage.CLAIM]: "control",
|
|
105
|
+
[ClientMessage.RELEASE]: "control",
|
|
106
|
+
[ClientMessage.DESTROY]: "control",
|
|
107
|
+
[ClientMessage.OBJECT]: "bulk",
|
|
108
|
+
[ClientMessage.OBJECT_SNAP]: "bulk",
|
|
109
|
+
[ClientMessage.INPUT]: "input",
|
|
110
|
+
[ClientMessage.ROOM_OPEN]: "control",
|
|
111
|
+
[ClientMessage.CHAT]: "chat",
|
|
112
|
+
[ClientMessage.CHAT_HISTORY_REQUEST]: "control",
|
|
113
|
+
[ClientMessage.VOICE_ICE_REQUEST]: "voice",
|
|
114
|
+
[ClientMessage.VOICE_JOIN]: "voice",
|
|
115
|
+
[ClientMessage.VOICE_LEAVE]: "voice",
|
|
116
|
+
[ClientMessage.VOICE_SIGNAL]: "voice",
|
|
117
|
+
[ClientMessage.MATCH_ELIMINATED]: "bulk",
|
|
118
|
+
[ClientMessage.MATCH_SCORE]: "bulk",
|
|
119
|
+
[ClientMessage.MATCH_FINISH]: "bulk",
|
|
120
|
+
[ClientMessage.POSE]: "pose",
|
|
121
|
+
[ClientMessage.POSE_SNAP]: "pose"
|
|
122
|
+
};
|
|
123
|
+
var CloseCode = {
|
|
124
|
+
// reconnectable (the seat is held for the grace window)
|
|
125
|
+
GOING_AWAY: 1001,
|
|
126
|
+
NO_STATUS: 1005,
|
|
127
|
+
ABNORMAL: 1006,
|
|
128
|
+
/** Colyseus: the server is going down, come back. The one ≥4000 code that is not terminal. */
|
|
129
|
+
MAY_TRY_RECONNECT: 4010,
|
|
130
|
+
// terminal
|
|
131
|
+
NORMAL: 1e3,
|
|
132
|
+
/** A deliberate leave (Colyseus CONSENTED) — and the code a match room boots a requeued player
|
|
133
|
+
* with when it ignored `match:requeue` (Genex MATCH_BOOT). Same number, same meaning: seat gone. */
|
|
134
|
+
CONSENTED: 4e3,
|
|
135
|
+
MATCH_BOOT: 4e3,
|
|
136
|
+
SERVER_SHUTDOWN: 4001,
|
|
137
|
+
/** Colyseus: protocol error, or the socket exceeded MAX_MESSAGES_PER_SECOND. */
|
|
138
|
+
WITH_ERROR: 4002,
|
|
139
|
+
/** Colyseus SDK: every reconnection attempt failed (the grace expired). */
|
|
140
|
+
FAILED_TO_RECONNECT: 4003,
|
|
141
|
+
/** The player's session was revoked (signed out from the dashboard). Do not rejoin with it. */
|
|
142
|
+
REVOKED: 4401,
|
|
143
|
+
/** A newer session of the same player took the seat (second tab, or the page reloaded). */
|
|
144
|
+
SUPERSEDED: 4409
|
|
145
|
+
};
|
|
146
|
+
var Limits = {
|
|
147
|
+
ROOM_MAX_CLIENTS: 64,
|
|
148
|
+
MAX_NAME_CHARS: 64,
|
|
149
|
+
MAX_AVATAR_URL_CHARS: 512,
|
|
150
|
+
MAX_UID_CHARS: 64,
|
|
151
|
+
/** `state` / `__state:snap` payload (the JSON text). */
|
|
152
|
+
MAX_STATE_BYTES: 16 * 1024,
|
|
153
|
+
MAX_SHARED_KEY_CHARS: 256,
|
|
154
|
+
MAX_SHARED_VALUE_BYTES: 64 * 1024,
|
|
155
|
+
/** Distinct game keys a room's `shared` map may ever hold (keys are never deleted). */
|
|
156
|
+
MAX_SHARED_KEYS: 256,
|
|
157
|
+
MAX_OBJECT_ID_CHARS: 128,
|
|
158
|
+
/** An object's `d` as stored (JSON text). */
|
|
159
|
+
MAX_OBJECT_STATE_BYTES: 64 * 1024,
|
|
160
|
+
/** Live objects per room (an id counts from its first accepted claim until `destroy`). */
|
|
161
|
+
MAX_OBJECTS: 128,
|
|
162
|
+
MAX_REQUEST_ID_CHARS: 96,
|
|
163
|
+
/** Game events and `__input`: the payload's JSON encoding. */
|
|
164
|
+
MAX_EVENT_BYTES: 16 * 1024,
|
|
165
|
+
MAX_EVENT_TYPE_CHARS: 128,
|
|
166
|
+
/** JSON payload shape bounds: nesting depth and total node count. */
|
|
167
|
+
MAX_JSON_DEPTH: 32,
|
|
168
|
+
MAX_JSON_NODES: 4096,
|
|
169
|
+
/** Code points per chat line after normalization. */
|
|
170
|
+
MAX_CHAT_CHARS: 500,
|
|
171
|
+
CHAT_HISTORY_MAX: 50,
|
|
172
|
+
CHAT_HISTORY_MAX_BYTES: 8 * 1024,
|
|
173
|
+
MAX_VOICE_PARTICIPANTS: 6,
|
|
174
|
+
MAX_VOICE_SIGNAL_BYTES: 8 * 1024,
|
|
175
|
+
/** `match:score` delta is clamped to ±this. */
|
|
176
|
+
MAX_SCORE_DELTA: 1e3,
|
|
177
|
+
/** Results kept per session for request-id replay, and how long. */
|
|
178
|
+
CONTROL_RESULTS_PER_SESSION: 128,
|
|
179
|
+
CONTROL_RESULT_TTL_MS: 6e4,
|
|
180
|
+
/** Open preset `teams` knob range (0 = no teams). */
|
|
181
|
+
MAX_TEAMS: 8,
|
|
182
|
+
/** `__pose` `anim` string (§6.6). */
|
|
183
|
+
MAX_ANIM_CHARS: 64,
|
|
184
|
+
/** Every `__pose` number must be finite with |v| ≤ this (float32 holds it without overflowing to
|
|
185
|
+
* Infinity; past ~1e7 a float32 position is coarser than a metre anyway). */
|
|
186
|
+
MAX_POSE_ABS: 1e9
|
|
187
|
+
};
|
|
188
|
+
var RelayTiming = {
|
|
189
|
+
/** State patch cadence: every accumulated change goes out on this tick. */
|
|
190
|
+
PATCH_INTERVAL_MS: 1e3 / 30,
|
|
191
|
+
/** Seat grace after an abnormal drop (1006 etc.). */
|
|
192
|
+
RECONNECT_GRACE_S: 30,
|
|
193
|
+
/** Seat grace after a 1001 close (reload/navigate) — long enough to rejoin and supersede. */
|
|
194
|
+
GOING_AWAY_GRACE_S: 5,
|
|
195
|
+
/** A dropped host keeps host for this long before the relay fails over. */
|
|
196
|
+
HOST_FAILOVER_GRACE_MS: 1500,
|
|
197
|
+
/** A freshly changed owner cannot be displaced by an ordinary claim for this long. */
|
|
198
|
+
MIN_OWNERSHIP_HOLD_MS: 300,
|
|
199
|
+
/** After `server:restart`, clients are dropped this much later. */
|
|
200
|
+
SHUTDOWN_DRAIN_MS: 3e3,
|
|
201
|
+
/** A requeued match player that has not left is booted (MATCH_BOOT) after this. */
|
|
202
|
+
REQUEUE_GRACE_MS: 5e3,
|
|
203
|
+
/** A seat reservation must be consumed within this. */
|
|
204
|
+
SEAT_RESERVATION_S: 15,
|
|
205
|
+
/** The queue room's matchmaking pass. */
|
|
206
|
+
QUEUE_TICK_MS: 1e3,
|
|
207
|
+
/** Embed tokens are re-checked for revocation on this cadence (REVOKED close on hit). */
|
|
208
|
+
REVOCATION_SWEEP_MS: 3e4
|
|
209
|
+
};
|
|
210
|
+
var ClientReconnection = {
|
|
211
|
+
enabled: true,
|
|
212
|
+
minUptime: 0,
|
|
213
|
+
delay: 125,
|
|
214
|
+
minDelay: 100,
|
|
215
|
+
maxDelay: 4e3,
|
|
216
|
+
maxRetries: 9,
|
|
217
|
+
maxEnqueuedMessages: 0,
|
|
218
|
+
/** After the SDK reports reconnected, wait at most this for the first state decode (§8). */
|
|
219
|
+
firstStateTimeoutMs: 2e3
|
|
220
|
+
};
|
|
221
|
+
var POSE_KEYS = ["x", "y", "z", "yaw", "vx", "vy", "vz", "anim"];
|
|
222
|
+
var Interpolation = {
|
|
223
|
+
/** Render delay target = RATIO × median publish interval of that sender… */
|
|
224
|
+
RATIO: 1.5,
|
|
225
|
+
/** …but at least JITTER_SAFETY × (p90 − min) of that sender's (arrival − sendTime)… */
|
|
226
|
+
JITTER_SAFETY: 1.25,
|
|
227
|
+
/** …clamped to [MIN_MS, MAX_MS], and DEFAULT_MS before anything is measured. */
|
|
228
|
+
MIN_MS: 90,
|
|
229
|
+
MAX_MS: 300,
|
|
230
|
+
DEFAULT_MS: 100,
|
|
231
|
+
/** Per-sample fraction the delay moves toward its target (no pops). */
|
|
232
|
+
EASE: 0.1,
|
|
233
|
+
/**
|
|
234
|
+
* The min-filtered clock-offset window, kept PER SENDER: it holds at least the last CLOCK_WINDOW
|
|
235
|
+
* samples AND at least CLOCK_WINDOW_MS of them, whichever reaches further back, so a sender that
|
|
236
|
+
* owns many streams never shrinks the time the window covers. The jitter term reads each stream's
|
|
237
|
+
* own last CLOCK_WINDOW samples.
|
|
238
|
+
*/
|
|
239
|
+
CLOCK_WINDOW: 80,
|
|
240
|
+
CLOCK_WINDOW_MS: 5e3,
|
|
241
|
+
/** Hard cap on one sender's window (memory bound for a sender publishing very many streams). */
|
|
242
|
+
CLOCK_WINDOW_MAX: 4096,
|
|
243
|
+
/** Publish-gap history for the median; gaps outside (MIN_GAP_MS, MAX_GAP_MS) are ignored. */
|
|
244
|
+
INTERVAL_WINDOW: 30,
|
|
245
|
+
MIN_GAP_MS: 5,
|
|
246
|
+
MAX_GAP_MS: 2e3,
|
|
247
|
+
/** Snapshot history kept per stream (never fewer than 2). */
|
|
248
|
+
HISTORY_MS: 1500,
|
|
249
|
+
/** Past the newest snapshot: dead-reckon position at most this far, rotation this far. */
|
|
250
|
+
MAX_EXTRAP_MS: 250,
|
|
251
|
+
ROT_EXTRAP_MS: 150,
|
|
252
|
+
/** Output smoothing time constants (alpha = 1 − e^(−dt/τ), dt clamped to 250 ms). */
|
|
253
|
+
SMOOTH_TAU_POS_MS: 30,
|
|
254
|
+
SMOOTH_TAU_ROT_MS: 70,
|
|
255
|
+
/** A head snapshot this far in the future of the newest is a foreign-clock poison: drop it. */
|
|
256
|
+
POISON_GUARD_MS: 2e3,
|
|
257
|
+
/** A reconnect gap longer than this discards every stream's history (one deliberate rebase). */
|
|
258
|
+
REBASE_AFTER_GAP_MS: 1e3,
|
|
259
|
+
/** Host tick dt is clamped to this (a backgrounded tab must not tunnel the simulation). */
|
|
260
|
+
MAX_HOST_TICK_DT_MS: 250
|
|
261
|
+
};
|
|
262
|
+
var ClockSync = {
|
|
263
|
+
/** The heartbeat rate a client SHOULD send at, from a wall-clock timer (never the render loop). */
|
|
264
|
+
HEARTBEAT_HZ: 10,
|
|
265
|
+
/** A client that sends heartbeats MUST stay within [MIN_HEARTBEAT_HZ, MAX_HEARTBEAT_HZ] while its
|
|
266
|
+
* page is foregrounded. Below MIN the offset and RTT refresh too slowly; above MAX buys nothing,
|
|
267
|
+
* and the relay's small buffer (RELAY_BUFFER) folds the excess away. */
|
|
268
|
+
MIN_HEARTBEAT_HZ: 5,
|
|
269
|
+
MAX_HEARTBEAT_HZ: 20,
|
|
270
|
+
/** Each interval is the nominal one × (1 ± up to this), clamped to the band above. A fixed interval
|
|
271
|
+
* that divides the patch interval phase-locks to the patch tick (every RTT sample then carries the
|
|
272
|
+
* same hold); the dither walks the phase so near-zero holds keep turning up. */
|
|
273
|
+
HEARTBEAT_DITHER: 0.15,
|
|
274
|
+
/** Relay `defineInput({ bufferMaxSize })`. Overflow counts as consumed, so a burst still acks. */
|
|
275
|
+
RELAY_BUFFER: 4,
|
|
276
|
+
/** Acked heartbeats (RTT-valid samples) before a client treats `serverNow` as synced and uses it
|
|
277
|
+
* for deadlines (≈3 s at 10 Hz). The room clock seeds its offset from the first TIMED frame, which
|
|
278
|
+
* is biased by the one-way latency, and then moves by an EMA (α = 0.1) per RTT sample, so after k
|
|
279
|
+
* samples 0.9^k of that bias remains: ≈35% at 10, ≈4% at 30 (≈6 ms at 150 ms one way). 30 is also
|
|
280
|
+
* where the room clock's RTT gate stops being bypassed (its warmup). */
|
|
281
|
+
WARMUP_ACKS: 30
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
export {
|
|
285
|
+
PROTOCOL_VERSION,
|
|
286
|
+
MIN_PROTOCOL_VERSION,
|
|
287
|
+
RoomType,
|
|
288
|
+
JoinOption,
|
|
289
|
+
SERVER_OWNED_JOIN_OPTIONS,
|
|
290
|
+
JoinError,
|
|
291
|
+
STAGING_ROOM_PREFIX,
|
|
292
|
+
roomKeyFor,
|
|
293
|
+
slugOfRoomKey,
|
|
294
|
+
ClientMessage,
|
|
295
|
+
ServerMessage,
|
|
296
|
+
RESERVED_EVENT_PREFIXES,
|
|
297
|
+
RESERVED_EVENT_TYPES,
|
|
298
|
+
isReservedEventType,
|
|
299
|
+
RESERVED_SHARED_PREFIX,
|
|
300
|
+
SEND_TIME_KEY,
|
|
301
|
+
RATE_LANES,
|
|
302
|
+
GLOBAL_RATE,
|
|
303
|
+
MAX_MESSAGES_PER_SECOND,
|
|
304
|
+
MESSAGE_LANE,
|
|
305
|
+
CloseCode,
|
|
306
|
+
Limits,
|
|
307
|
+
RelayTiming,
|
|
308
|
+
ClientReconnection,
|
|
309
|
+
POSE_KEYS,
|
|
310
|
+
Interpolation,
|
|
311
|
+
ClockSync
|
|
312
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
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. */
|
|
@@ -43,13 +44,14 @@ type ColyseusAuth = {
|
|
|
43
44
|
};
|
|
44
45
|
type AuthSource = ColyseusAuth | (() => ColyseusAuth | undefined);
|
|
45
46
|
type ConnectOptions = {
|
|
46
|
-
/** Regional relay urls (pass `getColyseusUrls()`) — the SDK
|
|
47
|
-
* lowest-latency one; a single entry
|
|
47
|
+
/** Regional relay urls (pass `getColyseusUrls()`) — the SDK probes each and connects to the
|
|
48
|
+
* lowest-latency one (a probe that times out falls back, it never fails the join); a single entry
|
|
49
|
+
* just connects. */
|
|
48
50
|
urls: string[];
|
|
49
|
-
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* nearest-region play. */
|
|
51
|
+
/** Override: connect to THIS exact relay url, skipping latency selection. Relays that share
|
|
52
|
+
* presence (global matchmaking) seat players together whichever relay each enters through, so
|
|
53
|
+
* this is only needed against relays that do not — there two players meet only on one relay, and
|
|
54
|
+
* an invitee follows the inviter's region. Omit for normal nearest-region play. */
|
|
53
55
|
url?: string;
|
|
54
56
|
/** Game id — everyone who passes the same id shares a room; different ids are
|
|
55
57
|
* fully isolated. Use your project slug (or any stable per-game string). */
|
|
@@ -70,6 +72,16 @@ type ConnectOptions = {
|
|
|
70
72
|
auth?: AuthSource;
|
|
71
73
|
/** Extra join options passed to the room. */
|
|
72
74
|
options?: Record<string, unknown>;
|
|
75
|
+
} & SessionOptions;
|
|
76
|
+
/** How remote posed players are smoothed (EXPERIMENTAL, see {@link SessionOptions.smoothing}). */
|
|
77
|
+
type PredictSmoothing = {
|
|
78
|
+
engine: 'predict';
|
|
79
|
+
/** Colyseus `Predict` mode. Default `'lerp'`. */
|
|
80
|
+
mode?: 'lerp' | 'damped';
|
|
81
|
+
/** `lerp` render delay in ms (Predict's default 100). */
|
|
82
|
+
delay?: number;
|
|
83
|
+
/** Predict's output spring / damped chase rate in ms (Predict's defaults when omitted). */
|
|
84
|
+
smoothMs?: number;
|
|
73
85
|
};
|
|
74
86
|
type PlayerView<TState> = {
|
|
75
87
|
id: string;
|
|
@@ -81,6 +93,14 @@ type PlayerView<TState> = {
|
|
|
81
93
|
avatarUrl: string;
|
|
82
94
|
/** Transport is live now. False while this reserved seat is inside reconnect grace. */
|
|
83
95
|
connected: boolean;
|
|
96
|
+
/** This player's stable id IN THIS GAME: the same across reloads, devices and rooms of the
|
|
97
|
+
* game, different in every other game, never the account id. Key anything that outlives a
|
|
98
|
+
* session (scores, colours, saves, "is this me") by it — `id` is a session id and changes on
|
|
99
|
+
* every reload. Opaque, ≤64 chars: never parse it. Relay-stamped from the verified token. */
|
|
100
|
+
uid: string;
|
|
101
|
+
/** Team index the relay assigned, or `-1` for no team: the `open` preset's `teams: N` (kept by
|
|
102
|
+
* `uid` for the room's life, never reshuffled) or the `teams` preset's current round. */
|
|
103
|
+
team: number;
|
|
84
104
|
/** RENDER ONLY — for remote players this is **auto-smoothed** (interpolated) and delayed;
|
|
85
105
|
* for yourself it's live. Numeric fields are lerped; everything else snaps. Use `stateRaw`
|
|
86
106
|
* for every gameplay read (hits, reach, pickups, flags, physics adoption). */
|
|
@@ -103,8 +123,12 @@ type ObjectView<TState> = {
|
|
|
103
123
|
/** The latest raw snapshot (never smoothed/delayed) — for hit-tests / logic. */
|
|
104
124
|
stateRaw: TState;
|
|
105
125
|
};
|
|
106
|
-
type ObjectControlOperation =
|
|
107
|
-
|
|
126
|
+
type ObjectControlOperation = ObjectControlOperation$1;
|
|
127
|
+
/** The relay's reasons (PROTOCOL.md §7.2), plus the ones this client settles a request with
|
|
128
|
+
* itself: `timeout`, `aborted` (your signal, or `leave()`), `disconnected` (the session ended). */
|
|
129
|
+
type ObjectControlReason = ObjectControlReason$1
|
|
130
|
+
/** @deprecated Never produced since protocol 3 — every relay answers control requests. */
|
|
131
|
+
| 'unsupported' | 'timeout' | 'aborted' | 'disconnected';
|
|
108
132
|
type ObjectControlResult = {
|
|
109
133
|
requestId: string;
|
|
110
134
|
operation: ObjectControlOperation;
|
|
@@ -185,7 +209,8 @@ type Session<TState extends Record<string, unknown>> = {
|
|
|
185
209
|
/** True if you are the elected room host — the earliest signed-in player still present
|
|
186
210
|
* (guests host only while no account is in the room); can migrate on join as well as leave.
|
|
187
211
|
* Settles within the first state patch after connect — read it in your loop or react to
|
|
188
|
-
* `on('host')`, not once.
|
|
212
|
+
* `on('host')`, not once. False while the session is reconnecting (until the fresh snapshot
|
|
213
|
+
* says who the host is now) and after it ends, so a write behind `isHost` is never stale. */
|
|
189
214
|
readonly isHost: boolean;
|
|
190
215
|
/** Session id of the current room host, or `undefined` until the first host patch lands. */
|
|
191
216
|
readonly host: string | undefined;
|
|
@@ -228,27 +253,35 @@ type Session<TState extends Record<string, unknown>> = {
|
|
|
228
253
|
limit?: number;
|
|
229
254
|
}) => void): () => void;
|
|
230
255
|
/**
|
|
231
|
-
* The connection dropped unexpectedly and auto-reconnect is underway (
|
|
232
|
-
* ~0.
|
|
233
|
-
* keep your loop running — the world resumes seamlessly on
|
|
256
|
+
* The connection dropped unexpectedly and auto-reconnect is underway (one event per
|
|
257
|
+
* attempt; the attempts back off ~0.25s→4s and all land inside the relay's 30s seat grace).
|
|
258
|
+
* Show a "reconnecting…" overlay; keep your loop running — the world resumes seamlessly on
|
|
259
|
+
* `reconnected`. Host ticks pause until then.
|
|
234
260
|
*/
|
|
235
261
|
on(event: 'reconnecting', cb: (info: {
|
|
236
262
|
attempt: number;
|
|
237
263
|
code: number;
|
|
238
264
|
}) => void): () => void;
|
|
239
|
-
/** Auto-reconnect succeeded: same session id, same seat — nothing was torn down.
|
|
265
|
+
/** Auto-reconnect succeeded: same session id, same seat — nothing was torn down. Fires once
|
|
266
|
+
* the room's state is current again (host, owners, players), so it is safe to read here. */
|
|
240
267
|
on(event: 'reconnected', cb: () => void): () => void;
|
|
241
268
|
/**
|
|
242
269
|
* The connection is gone for good: the server closed it on purpose (restart, revoked
|
|
243
|
-
* session, match eviction) or every reconnect attempt
|
|
244
|
-
* to play again, read a FRESH `getColyseusAuth()` and
|
|
270
|
+
* session 4401, a newer tab took the seat 4409, match eviction) or every reconnect attempt
|
|
271
|
+
* failed (4003). The session is dead — to play again, read a FRESH `getColyseusAuth()` and
|
|
272
|
+
* `connect()` anew (but not automatically after 4409: the player is playing elsewhere).
|
|
245
273
|
*/
|
|
246
274
|
on(event: 'disconnect', cb: (code: number) => void): () => void;
|
|
275
|
+
/** The host opened or closed this room to new matchmaking placements (`setRoomOpen`). */
|
|
276
|
+
on(event: 'room:open', cb: (info: {
|
|
277
|
+
open: boolean;
|
|
278
|
+
}) => void): () => void;
|
|
247
279
|
/** A custom event another client sent via `send(type, ...)`. */
|
|
248
280
|
on(event: string & {}, cb: (payload: unknown) => void): () => void;
|
|
249
281
|
/** Fire-and-forget event to all OTHER clients (not stored in state). Event names used
|
|
250
|
-
* by the relay itself (`state`, `shared`, `claim`, `
|
|
251
|
-
* `match:`/`
|
|
282
|
+
* by the relay itself (`state`, `shared`, `claim`, `release`, `destroy`, `obj`, `queue`,
|
|
283
|
+
* `seat`, and the `__`/`match:`/`server:`/`rate:` prefixes) and names over 128 characters
|
|
284
|
+
* are refused with a console warning — pick another name. */
|
|
252
285
|
send(type: string, payload?: unknown): void;
|
|
253
286
|
/** Text chat for this room. See {@link ChatApi}. */
|
|
254
287
|
chat: ChatApi;
|
|
@@ -263,12 +296,56 @@ type Session<TState extends Record<string, unknown>> = {
|
|
|
263
296
|
* keeps playing, and a reconnecting player still gets back in (reconnection uses a
|
|
264
297
|
* reserved seat, not matchmaking).
|
|
265
298
|
*
|
|
266
|
-
* Ignored (with no error) when called by a non-host — read `isHost` first.
|
|
267
|
-
*
|
|
299
|
+
* Ignored (with no error) when called by a non-host — read `isHost` first. When the host
|
|
300
|
+
* calls it every client is told, so you can render "closed" from the `room:open` event.
|
|
268
301
|
*/
|
|
269
302
|
setRoomOpen(open: boolean): void;
|
|
270
303
|
/** Leave the room (deliberate — no auto-reconnect, no `disconnect` event). */
|
|
271
304
|
leave(): void;
|
|
305
|
+
/**
|
|
306
|
+
* EXPERIMENTAL — the relay's server clock (PROTOCOL.md §16): milliseconds since the room was
|
|
307
|
+
* created, estimated from this client's heartbeat acks. `undefined` against a relay that does not
|
|
308
|
+
* publish its clock. For server-stamped deadlines prefer {@link timeUntil}; never use this to drive
|
|
309
|
+
* rendering (remote players stay on their senders' clocks).
|
|
310
|
+
*/
|
|
311
|
+
serverNow(): number | undefined;
|
|
312
|
+
/** EXPERIMENTAL — smoothed round-trip time to the relay in ms, from heartbeat acks. `undefined`
|
|
313
|
+
* until the first ack, and always against a relay that does not publish its clock. It includes
|
|
314
|
+
* the relay's hold until its next patch (0 to ~33 ms), so read it as an upper bound. */
|
|
315
|
+
readonly rtt: number | undefined;
|
|
316
|
+
/** EXPERIMENTAL — true once enough heartbeats were acked (~3 s) that `serverNow()` is within a few ms of the room clock. */
|
|
317
|
+
readonly clockSynced: boolean;
|
|
318
|
+
/**
|
|
319
|
+
* Milliseconds from now until a relay deadline stamped in epoch ms (a match's `startsAt` /
|
|
320
|
+
* `endsBy`; negative once it passed). Uses the server clock once it is synced — immune to a wrong
|
|
321
|
+
* clock on the player's device — and falls back to `deadline − Date.now()` otherwise.
|
|
322
|
+
*/
|
|
323
|
+
timeUntil(epochMs: number): number;
|
|
324
|
+
};
|
|
325
|
+
/** Session behaviour options shared by `connect()`, `matchmake()`, `createPrivate()` and
|
|
326
|
+
* `joinPrivate()`. Every one is EXPERIMENTAL and off by default: each is an A/B arm of the
|
|
327
|
+
* netcode bench (PROTOCOL.md §15) and stays opt-in until it wins its gate. */
|
|
328
|
+
type SessionOptions = {
|
|
329
|
+
/**
|
|
330
|
+
* EXPERIMENTAL — typed pose (PROTOCOL.md §6.6). Default `false`. When `true`, the reserved keys
|
|
331
|
+
* `x y z yaw vx vy vz anim` of `me.set()` / `me.snap()` are published as typed relay fields
|
|
332
|
+
* (float32 deltas instead of a re-sent JSON blob) and everything else as `data`, which is then sent
|
|
333
|
+
* only when it changes. If you omit `vx/vy/vz` they are derived from successive positions.
|
|
334
|
+
*
|
|
335
|
+
* Opt in for EVERY client of the game at once. Receivers then interpolate only the pose — `yaw`
|
|
336
|
+
* along the shortest arc (radians), `anim` switches when the rendered pose reaches it — and SNAP
|
|
337
|
+
* everything else in your state: a rotation you sync as a quaternion is no longer smoothed, so put
|
|
338
|
+
* the heading in `yaw`. A remote posed player's `state` always carries all eight pose keys.
|
|
339
|
+
*/
|
|
340
|
+
pose?: boolean;
|
|
341
|
+
/**
|
|
342
|
+
* EXPERIMENTAL — remote smoothing engine for POSED players. Default `'genex'`: the SDK's own
|
|
343
|
+
* sender-clock interpolation (PROTOCOL.md §10.1), which every other stream keeps whatever this says.
|
|
344
|
+
* `'predict'` (or a {@link PredictSmoothing}) hands posed players' `x y z vx vy vz` and `yaw` to
|
|
345
|
+
* Colyseus `Predict` instead — receive-time stamps, a fixed delay, no dead reckoning past the newest
|
|
346
|
+
* sample, and `anim`/`data` applied on arrival. It exists so the bench can A/B the two engines.
|
|
347
|
+
*/
|
|
348
|
+
smoothing?: 'genex' | 'predict' | PredictSmoothing;
|
|
272
349
|
};
|
|
273
350
|
/** One chat line, as the relay stamped it. Only `text` came from the sender's client —
|
|
274
351
|
* `from` and `name` are filled in server-side from the verified session, so a player
|
|
@@ -397,6 +474,16 @@ type MatchmakingView = {
|
|
|
397
474
|
/** Winner of the round shown while `status === 'ended'` (competitor id; null on a draw). */
|
|
398
475
|
winnerId: string | null;
|
|
399
476
|
message: string;
|
|
477
|
+
/** The relay's epoch-ms `startsAt` — when the countdown (or the result screen before the next
|
|
478
|
+
* round) ends — else null. */
|
|
479
|
+
startsAt: number | null;
|
|
480
|
+
/** Round time limit: the relay's epoch-ms `endsBy` (presets with a round limit), else null. */
|
|
481
|
+
endsBy: number | null;
|
|
482
|
+
/** Milliseconds until `startsAt` as of this read (≥ 0), or null. Measured on the relay's server
|
|
483
|
+
* clock once it is synced, so a wrong clock on the player's device cannot skew a countdown. */
|
|
484
|
+
startsInMs: number | null;
|
|
485
|
+
/** Milliseconds until `endsBy` as of this read (≥ 0), or null. Same clock as `startsInMs`. */
|
|
486
|
+
endsInMs: number | null;
|
|
400
487
|
};
|
|
401
488
|
type Matchmaking<TState extends Record<string, unknown>> = {
|
|
402
489
|
/** The live game session, or null while searching for a match. */
|
|
@@ -406,11 +493,17 @@ type Matchmaking<TState extends Record<string, unknown>> = {
|
|
|
406
493
|
/**
|
|
407
494
|
* searching: (re)entered the queue. queue: your position changed ({position,size}).
|
|
408
495
|
* matched: `session` is now live. matchStart: a round began ({roundId}).
|
|
409
|
-
* matchEnded: {winnerId,
|
|
496
|
+
* matchEnded: {winnerId,losers,draw,scores}. spectate: you're watching, not playing.
|
|
410
497
|
* error: joining the queue failed even after the SDK's automatic retries (persistent —
|
|
411
498
|
* e.g. auth is broken or the server is unreachable); fix the cause, then call `retry()`.
|
|
499
|
+
* A relay that refuses this SDK's protocol (code 426) fails at once, without retries: the
|
|
500
|
+
* game needs an SDK update.
|
|
501
|
+
* disconnect: the match seat ended with 4409 (this player opened the game in another tab or
|
|
502
|
+
* device) or 4401 (signed out). The handle stops and does NOT requeue; `session` is null. Tell
|
|
503
|
+
* the player, and call `retry()` only if they choose to play here again. Every other terminal
|
|
504
|
+
* drop requeues automatically.
|
|
412
505
|
*/
|
|
413
|
-
on(event: 'searching' | 'queue' | 'matched' | 'matchStart' | 'matchEnded' | 'spectate' | 'error', cb: (payload?: unknown) => void): () => void;
|
|
506
|
+
on(event: 'searching' | 'queue' | 'matched' | 'matchStart' | 'matchEnded' | 'spectate' | 'error' | 'disconnect', cb: (payload?: unknown) => void): () => void;
|
|
414
507
|
/** Report your OWN elimination (the `lastStanding` win condition). */
|
|
415
508
|
eliminated(): void;
|
|
416
509
|
/** Report a score delta for yourself (the `firstToScore` / `highScoreInTime` conditions). */
|
|
@@ -426,8 +519,8 @@ type Matchmaking<TState extends Record<string, unknown>> = {
|
|
|
426
519
|
type MatchmakeOptions = {
|
|
427
520
|
/** Regional relay urls (pass `getColyseusUrls()`) — pings each, connects to the lowest-latency one. */
|
|
428
521
|
urls: string[];
|
|
429
|
-
/**
|
|
430
|
-
*
|
|
522
|
+
/** Override: matchmake on THIS exact relay url, skipping latency selection — only needed against
|
|
523
|
+
* relays that do not share presence (see ConnectOptions.url). Omit for normal nearest-region play. */
|
|
431
524
|
url?: string;
|
|
432
525
|
/** Game id (your project slug/id) — isolates this game's matchmaking. */
|
|
433
526
|
room: string;
|
|
@@ -439,7 +532,7 @@ type MatchmakeOptions = {
|
|
|
439
532
|
* token; a static object is read only once and will be rejected once it expires.
|
|
440
533
|
*/
|
|
441
534
|
auth?: AuthSource;
|
|
442
|
-
};
|
|
535
|
+
} & SessionOptions;
|
|
443
536
|
/**
|
|
444
537
|
* Join a game's matchmaking. Returns immediately in the SEARCHING state (`session` is null); the
|
|
445
538
|
* matchmaker seats you into a match room, at which point `session` becomes a live Session and a
|
|
@@ -469,4 +562,4 @@ declare function createPrivate<TState extends Record<string, unknown> = never>(o
|
|
|
469
562
|
*/
|
|
470
563
|
declare function joinPrivate<TState extends Record<string, unknown> = never>(code: string, opts: MatchmakeOptions): Promise<Matchmaking<TState>>;
|
|
471
564
|
|
|
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 };
|
|
565
|
+
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 };
|