@colyseus/core 0.17.44 → 0.18.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/build/MatchMaker.cjs +22 -13
- package/build/MatchMaker.cjs.map +2 -2
- package/build/MatchMaker.d.ts +10 -0
- package/build/MatchMaker.mjs +22 -14
- package/build/MatchMaker.mjs.map +2 -2
- package/build/Protocol.cjs +102 -37
- package/build/Protocol.cjs.map +2 -2
- package/build/Protocol.d.ts +33 -2
- package/build/Protocol.mjs +102 -37
- package/build/Protocol.mjs.map +2 -2
- package/build/Rewind.cjs +385 -0
- package/build/Rewind.cjs.map +7 -0
- package/build/Rewind.d.ts +291 -0
- package/build/Rewind.mjs +359 -0
- package/build/Rewind.mjs.map +7 -0
- package/build/Room.cjs +425 -149
- package/build/Room.cjs.map +3 -3
- package/build/Room.d.ts +314 -27
- package/build/Room.mjs +434 -152
- package/build/Room.mjs.map +3 -3
- package/build/RoomMessages.cjs +265 -0
- package/build/RoomMessages.cjs.map +7 -0
- package/build/RoomMessages.d.ts +49 -0
- package/build/RoomMessages.mjs +240 -0
- package/build/RoomMessages.mjs.map +7 -0
- package/build/RoomPlugin.cjs +252 -0
- package/build/RoomPlugin.cjs.map +7 -0
- package/build/RoomPlugin.d.ts +271 -0
- package/build/RoomPlugin.mjs +220 -0
- package/build/RoomPlugin.mjs.map +7 -0
- package/build/Server.cjs +49 -11
- package/build/Server.cjs.map +2 -2
- package/build/Server.d.ts +26 -0
- package/build/Server.mjs +50 -12
- package/build/Server.mjs.map +2 -2
- package/build/Transport.cjs +64 -2
- package/build/Transport.cjs.map +2 -2
- package/build/Transport.d.ts +108 -5
- package/build/Transport.mjs +63 -2
- package/build/Transport.mjs.map +2 -2
- package/build/errors/RoomExceptions.cjs +7 -4
- package/build/errors/RoomExceptions.cjs.map +2 -2
- package/build/errors/RoomExceptions.d.ts +15 -3
- package/build/errors/RoomExceptions.mjs +5 -3
- package/build/errors/RoomExceptions.mjs.map +2 -2
- package/build/index.cjs +30 -4
- package/build/index.cjs.map +2 -2
- package/build/index.d.ts +9 -5
- package/build/index.mjs +29 -6
- package/build/index.mjs.map +2 -2
- package/build/input/InputBuffer.cjs +536 -0
- package/build/input/InputBuffer.cjs.map +7 -0
- package/build/input/InputBuffer.d.ts +226 -0
- package/build/input/InputBuffer.mjs +507 -0
- package/build/input/InputBuffer.mjs.map +7 -0
- package/build/input/RoomInput.cjs +306 -0
- package/build/input/RoomInput.cjs.map +7 -0
- package/build/input/RoomInput.d.ts +91 -0
- package/build/input/RoomInput.mjs +287 -0
- package/build/input/RoomInput.mjs.map +7 -0
- package/build/input/types.cjs +18 -0
- package/build/input/types.cjs.map +7 -0
- package/build/input/types.d.ts +476 -0
- package/build/input/types.mjs +0 -0
- package/build/input/types.mjs.map +7 -0
- package/build/internal.cjs +61 -0
- package/build/internal.cjs.map +7 -0
- package/build/internal.d.ts +9 -0
- package/build/internal.mjs +29 -0
- package/build/internal.mjs.map +7 -0
- package/build/matchmaker/LocalDriver/LocalDriver.cjs +13 -0
- package/build/matchmaker/LocalDriver/LocalDriver.cjs.map +2 -2
- package/build/matchmaker/LocalDriver/LocalDriver.d.ts +1 -0
- package/build/matchmaker/LocalDriver/LocalDriver.mjs +13 -0
- package/build/matchmaker/LocalDriver/LocalDriver.mjs.map +2 -2
- package/build/matchmaker/driver.cjs.map +1 -1
- package/build/matchmaker/driver.d.ts +12 -0
- package/build/matchmaker/driver.mjs.map +1 -1
- package/build/presence/LocalPresence.d.ts +1 -1
- package/build/rooms/LobbyRoom.cjs +8 -10
- package/build/rooms/LobbyRoom.cjs.map +2 -2
- package/build/rooms/LobbyRoom.d.ts +4 -3
- package/build/rooms/LobbyRoom.mjs +8 -10
- package/build/rooms/LobbyRoom.mjs.map +2 -2
- package/build/rooms/QueueRoom.cjs +1 -1
- package/build/rooms/QueueRoom.cjs.map +2 -2
- package/build/rooms/QueueRoom.mjs +1 -1
- package/build/rooms/QueueRoom.mjs.map +2 -2
- package/build/rooms/RelayRoom.cjs +12 -16
- package/build/rooms/RelayRoom.cjs.map +2 -2
- package/build/rooms/RelayRoom.d.ts +32 -11
- package/build/rooms/RelayRoom.mjs +10 -16
- package/build/rooms/RelayRoom.mjs.map +2 -2
- package/build/router/default_routes.cjs +4 -1
- package/build/router/default_routes.cjs.map +2 -2
- package/build/router/default_routes.mjs +4 -1
- package/build/router/default_routes.mjs.map +2 -2
- package/build/router/index.cjs +71 -5
- package/build/router/index.cjs.map +2 -2
- package/build/router/index.d.ts +30 -6
- package/build/router/index.mjs +72 -7
- package/build/router/index.mjs.map +3 -3
- package/build/serializer/NoneSerializer.cjs +2 -2
- package/build/serializer/NoneSerializer.cjs.map +2 -2
- package/build/serializer/NoneSerializer.d.ts +3 -3
- package/build/serializer/NoneSerializer.mjs +2 -2
- package/build/serializer/NoneSerializer.mjs.map +2 -2
- package/build/serializer/SchemaSerializer.cjs +44 -14
- package/build/serializer/SchemaSerializer.cjs.map +2 -2
- package/build/serializer/SchemaSerializer.d.ts +3 -3
- package/build/serializer/SchemaSerializer.mjs +46 -16
- package/build/serializer/SchemaSerializer.mjs.map +2 -2
- package/build/serializer/Serializer.cjs.map +1 -1
- package/build/serializer/Serializer.d.ts +12 -2
- package/build/utils/DevMode.cjs +14 -14
- package/build/utils/DevMode.cjs.map +2 -2
- package/build/utils/DevMode.d.ts +0 -2
- package/build/utils/DevMode.mjs +14 -12
- package/build/utils/DevMode.mjs.map +2 -2
- package/build/utils/UserSessionIndex.cjs +162 -0
- package/build/utils/UserSessionIndex.cjs.map +7 -0
- package/build/utils/UserSessionIndex.d.ts +166 -0
- package/build/utils/UserSessionIndex.mjs +130 -0
- package/build/utils/UserSessionIndex.mjs.map +7 -0
- package/build/utils/Utils.cjs +3 -0
- package/build/utils/Utils.cjs.map +2 -2
- package/build/utils/Utils.d.ts +8 -0
- package/build/utils/Utils.mjs +2 -0
- package/build/utils/Utils.mjs.map +2 -2
- package/package.json +19 -14
- package/src/MatchMaker.ts +47 -15
- package/src/Protocol.ts +130 -59
- package/src/Rewind.ts +572 -0
- package/src/Room.ts +649 -235
- package/src/RoomMessages.ts +342 -0
- package/src/RoomPlugin.ts +563 -0
- package/src/Server.ts +82 -15
- package/src/Transport.ts +172 -9
- package/src/errors/RoomExceptions.ts +18 -4
- package/src/index.ts +23 -3
- package/src/input/InputBuffer.ts +549 -0
- package/src/input/RoomInput.ts +337 -0
- package/src/input/types.ts +503 -0
- package/src/internal.ts +46 -0
- package/src/matchmaker/LocalDriver/LocalDriver.ts +10 -0
- package/src/matchmaker/driver.ts +13 -0
- package/src/rooms/LobbyRoom.ts +12 -8
- package/src/rooms/QueueRoom.ts +1 -1
- package/src/rooms/RelayRoom.ts +9 -15
- package/src/router/default_routes.ts +6 -1
- package/src/router/index.ts +117 -11
- package/src/serializer/NoneSerializer.ts +3 -3
- package/src/serializer/SchemaSerializer.ts +110 -19
- package/src/serializer/Serializer.ts +13 -2
- package/src/utils/DevMode.ts +18 -13
- package/src/utils/UserSessionIndex.ts +311 -0
- package/src/utils/Utils.ts +9 -0
package/build/Room.cjs
CHANGED
|
@@ -33,12 +33,18 @@ __export(Room_exports, {
|
|
|
33
33
|
DEFAULT_SEAT_RESERVATION_TIME: () => DEFAULT_SEAT_RESERVATION_TIME,
|
|
34
34
|
Room: () => Room,
|
|
35
35
|
RoomInternalState: () => RoomInternalState,
|
|
36
|
-
|
|
36
|
+
RoomPlugin: () => import_RoomPlugin2.RoomPlugin,
|
|
37
|
+
attachToTestRoom: () => import_RoomPlugin2.attachToTestRoom,
|
|
38
|
+
definePlugins: () => import_RoomPlugin2.definePlugins,
|
|
37
39
|
validate: () => validate
|
|
38
40
|
});
|
|
39
41
|
module.exports = __toCommonJS(Room_exports);
|
|
40
|
-
var import_msgpackr = require("@colyseus/msgpackr");
|
|
41
42
|
var import_schema = require("@colyseus/schema");
|
|
43
|
+
var import_InputBuffer = require("./input/InputBuffer.cjs");
|
|
44
|
+
var import_RoomInput = require("./input/RoomInput.cjs");
|
|
45
|
+
var import_RoomMessages = require("./RoomMessages.cjs");
|
|
46
|
+
var import_Rewind = require("./Rewind.cjs");
|
|
47
|
+
var import_types = require("./input/types.cjs");
|
|
42
48
|
var import_timer = require("@colyseus/timer");
|
|
43
49
|
var import_events = require("events");
|
|
44
50
|
var import_Logger = require("./Logger.cjs");
|
|
@@ -46,7 +52,6 @@ var import_NoneSerializer = require("./serializer/NoneSerializer.cjs");
|
|
|
46
52
|
var import_SchemaSerializer = require("./serializer/SchemaSerializer.cjs");
|
|
47
53
|
var import_Protocol = require("./Protocol.cjs");
|
|
48
54
|
var import_Utils = require("./utils/Utils.cjs");
|
|
49
|
-
var import_nanoevents = require("./utils/nanoevents.cjs");
|
|
50
55
|
var import_DevMode = require("./utils/DevMode.cjs");
|
|
51
56
|
var import_Debug = require("./Debug.cjs");
|
|
52
57
|
var import_ServerError = require("./errors/ServerError.cjs");
|
|
@@ -55,9 +60,12 @@ var import_RoomExceptions = require("./errors/RoomExceptions.cjs");
|
|
|
55
60
|
var import_StandardSchema = require("./utils/StandardSchema.cjs");
|
|
56
61
|
var matchMaker = __toESM(require("./MatchMaker.cjs"), 1);
|
|
57
62
|
var import_shared_types = require("@colyseus/shared-types");
|
|
63
|
+
var import_RoomPlugin = require("./RoomPlugin.cjs");
|
|
64
|
+
var import_RoomPlugin2 = require("./RoomPlugin.cjs");
|
|
58
65
|
var DEFAULT_PATCH_RATE = 1e3 / 20;
|
|
59
66
|
var DEFAULT_SIMULATION_INTERVAL = 1e3 / 60;
|
|
60
67
|
var noneSerializer = new import_NoneSerializer.NoneSerializer();
|
|
68
|
+
var AFTER_PATCH_OPTS = Object.freeze({ afterNextPatch: true });
|
|
61
69
|
var DEFAULT_SEAT_RESERVATION_TIME = Number(process.env.COLYSEUS_SEAT_RESERVATION_TIME || 15);
|
|
62
70
|
function validate(format, handler) {
|
|
63
71
|
return { format, handler };
|
|
@@ -122,21 +130,19 @@ var Room = class _Room {
|
|
|
122
130
|
this._reservedSeatTimeouts = {};
|
|
123
131
|
this._reconnections = {};
|
|
124
132
|
this._reconnectionAttempts = {};
|
|
125
|
-
|
|
126
|
-
this
|
|
127
|
-
this.onMessageFallbacks = {
|
|
128
|
-
"__no_message_handler": (client, messageType, _) => {
|
|
129
|
-
const errorMessage = `room onMessage for "${messageType}" not registered.`;
|
|
130
|
-
(0, import_Debug.debugMessage)(`${errorMessage} (roomId: ${this.roomId})`);
|
|
131
|
-
if (import_DevMode.isDevMode) {
|
|
132
|
-
client.error(import_shared_types.ErrorCode.INVALID_PAYLOAD, errorMessage);
|
|
133
|
-
} else {
|
|
134
|
-
client.leave(import_shared_types.CloseCode.WITH_ERROR, errorMessage);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
};
|
|
133
|
+
/** @internal Message-routing layer: handler registry + user-message decode/dispatch. */
|
|
134
|
+
this.#_messages = new import_RoomMessages.RoomMessages(this);
|
|
138
135
|
this._serializer = noneSerializer;
|
|
139
|
-
|
|
136
|
+
/** Clients that staged `afterNextPatch` frames since the last patch, so the
|
|
137
|
+
* post-patch flush iterates only these — never the full client list. Shared by
|
|
138
|
+
* reference onto each client at join. Reset each `broadcastPatch`. */
|
|
139
|
+
this.#pendingFrameClients = [];
|
|
140
|
+
/** `broadcast(..., { afterNextPatch })` in NON-timed rooms, sent as a SHARED
|
|
141
|
+
* frame right after the next patch (one encode, N sends). Non-timed patches are
|
|
142
|
+
* themselves a shared buffer, so a per-client copy would force N× allocation in
|
|
143
|
+
* large rooms — sharing avoids that. TIMED rooms instead stage the broadcast onto
|
|
144
|
+
* each client's `_pendingFrames`. Drained each `broadcastPatch`. */
|
|
145
|
+
this.#afterPatchBroadcasts = [];
|
|
140
146
|
this._internalState = RoomInternalState.CREATING;
|
|
141
147
|
this._lockedExplicitly = false;
|
|
142
148
|
this.#_locked = false;
|
|
@@ -192,6 +198,17 @@ var Room = class _Room {
|
|
|
192
198
|
#_patchRate;
|
|
193
199
|
#_patchInterval;
|
|
194
200
|
#_state;
|
|
201
|
+
#_messages;
|
|
202
|
+
// Re-expose the registry for @colyseus/playground introspection and
|
|
203
|
+
// @colyseus/testing handler-swapping (both reach in via bracket access).
|
|
204
|
+
get onMessageEvents() {
|
|
205
|
+
return this.#_messages.events;
|
|
206
|
+
}
|
|
207
|
+
get onMessageValidators() {
|
|
208
|
+
return this.#_messages.validators;
|
|
209
|
+
}
|
|
210
|
+
#pendingFrameClients;
|
|
211
|
+
#afterPatchBroadcasts;
|
|
195
212
|
#_locked;
|
|
196
213
|
/**
|
|
197
214
|
* This method is called by the MatchMaker before onCreate()
|
|
@@ -256,6 +273,9 @@ var Room = class _Room {
|
|
|
256
273
|
if (this.#_state) {
|
|
257
274
|
this.state = this.#_state;
|
|
258
275
|
}
|
|
276
|
+
if (this.plugins !== void 0) {
|
|
277
|
+
(0, import_RoomPlugin.setupRoomPlugins)(this);
|
|
278
|
+
}
|
|
259
279
|
if (this.messages !== void 0) {
|
|
260
280
|
if (this.messages["_"]) {
|
|
261
281
|
this.onMessage("*", this.messages["_"].bind(this));
|
|
@@ -312,6 +332,44 @@ var Room = class _Room {
|
|
|
312
332
|
}
|
|
313
333
|
this.#_roomId = roomId;
|
|
314
334
|
}
|
|
335
|
+
/**
|
|
336
|
+
* Declare the input schema and configuration in a single line. Returns the
|
|
337
|
+
* input API that gets assigned to `this.inputs` — call
|
|
338
|
+
* `this.inputs.get(sessionId)` per tick to consume.
|
|
339
|
+
*
|
|
340
|
+
* ```typescript
|
|
341
|
+
* class FpsRoom extends Room<{ input: MoveInput }> {
|
|
342
|
+
* inputs = this.defineInput(MoveInput, {
|
|
343
|
+
* seqField: "tick", // typed: only numeric fields of MoveInput
|
|
344
|
+
* bufferMaxSize: 64,
|
|
345
|
+
* });
|
|
346
|
+
*
|
|
347
|
+
* // …or without options — no seq dedupe, bufferMaxSize: 32:
|
|
348
|
+
* // inputs = this.defineInput(MoveInput);
|
|
349
|
+
* }
|
|
350
|
+
* ```
|
|
351
|
+
*
|
|
352
|
+
* **Defaults** when `opts` (or individual fields) are omitted:
|
|
353
|
+
* - `seqField`: unset — dedupe and `this.inputs.get(sessionId).at(value)` lookup are
|
|
354
|
+
* OPT-IN (lockstep / rollback). Name a monotonic numeric field here to enable
|
|
355
|
+
* them; redundant frames (`input[seqField]` ≤ the last seen) are then dropped.
|
|
356
|
+
* Leave unset for reliable, in-order channels where every frame is unique.
|
|
357
|
+
* - `bufferMaxSize`: `32` — enables per-client snapshot buffering for
|
|
358
|
+
* `this.inputs.get(sessionId)` iteration / `.consume() / .drain() / .next() /
|
|
359
|
+
* .take() / .peek() / .at()`. Set to `0` to disable buffering (the `.latest`
|
|
360
|
+
* read still works).
|
|
361
|
+
*
|
|
362
|
+
* **Consuming the buffer** — `for (const inp of this.inputs.get(sessionId))` (sugar
|
|
363
|
+
* for `.consume()`) is the per-entity loop: it consumes one at a time, so
|
|
364
|
+
* lag-comp `renderTime` tracks each input. `next()` (take exactly one, ack +1)
|
|
365
|
+
* suits a shared physics world stepped once for everyone; `drain()` returns the
|
|
366
|
+
* whole pending set as an array. See {@link InputAccessor} for the full
|
|
367
|
+
* per-entity-vs-shared-world guidance and why the choice affects the reconcile ack.
|
|
368
|
+
*/
|
|
369
|
+
defineInput(type, opts) {
|
|
370
|
+
this._inputController ??= new import_RoomInput.RoomInput(this);
|
|
371
|
+
return this._inputController.define(type, opts);
|
|
372
|
+
}
|
|
315
373
|
/**
|
|
316
374
|
* This method is called before onJoin() - this is where you should authenticate the client
|
|
317
375
|
* @param client - The client that is authenticating.
|
|
@@ -371,7 +429,7 @@ var Room = class _Room {
|
|
|
371
429
|
reservedSeat[3] && this._reconnections[reconnectionToken]?.[0] === sessionId
|
|
372
430
|
);
|
|
373
431
|
} else if (typeof reconnectionToken === "string") {
|
|
374
|
-
return this.clients.
|
|
432
|
+
return this.clients.get(sessionId)?.reconnectionToken === reconnectionToken;
|
|
375
433
|
}
|
|
376
434
|
return false;
|
|
377
435
|
}
|
|
@@ -389,22 +447,27 @@ var Room = class _Room {
|
|
|
389
447
|
return void 0;
|
|
390
448
|
}
|
|
391
449
|
/**
|
|
392
|
-
*
|
|
393
|
-
*
|
|
450
|
+
* Set the room's game loop. `onTickCallback` runs every `delay` ms and receives
|
|
451
|
+
* the MEASURED wall-clock delta since the previous tick (a VARIABLE timestep).
|
|
452
|
+
* For deterministic, prediction-friendly simulation prefer
|
|
453
|
+
* {@link Room.setFixedTimestep}, which advances a fixed step via an accumulator.
|
|
394
454
|
*
|
|
395
455
|
* @default 16.6ms (60fps)
|
|
396
456
|
*
|
|
397
|
-
* @param onTickCallback -
|
|
398
|
-
*
|
|
399
|
-
* @param delay - Interval
|
|
457
|
+
* @param onTickCallback - Your physics / world update — a good place to mutate
|
|
458
|
+
* room state. Receives the measured delta (`this.clock.deltaTime`).
|
|
459
|
+
* @param delay - Interval between ticks in milliseconds.
|
|
400
460
|
*/
|
|
401
|
-
|
|
461
|
+
setTimestep(onTickCallback, delay = DEFAULT_SIMULATION_INTERVAL) {
|
|
402
462
|
if (this._simulationInterval) {
|
|
403
463
|
clearInterval(this._simulationInterval);
|
|
404
464
|
}
|
|
465
|
+
if (onTickCallback && this._inputController && this._inputController.options.tickRate === void 0) {
|
|
466
|
+
this._inputController.options.tickRate = Math.round(1e3 / delay);
|
|
467
|
+
}
|
|
405
468
|
if (onTickCallback) {
|
|
406
469
|
if (this.onUncaughtException !== void 0) {
|
|
407
|
-
onTickCallback = (0, import_Utils.wrapTryCatch)(onTickCallback, this.onUncaughtException.bind(this), import_RoomExceptions.
|
|
470
|
+
onTickCallback = (0, import_Utils.wrapTryCatch)(onTickCallback, this.onUncaughtException.bind(this), import_RoomExceptions.TimestepException, "setTimestep");
|
|
408
471
|
}
|
|
409
472
|
this._simulationInterval = setInterval(() => {
|
|
410
473
|
this.clock.tick();
|
|
@@ -412,6 +475,158 @@ var Room = class _Room {
|
|
|
412
475
|
}, delay);
|
|
413
476
|
}
|
|
414
477
|
}
|
|
478
|
+
/**
|
|
479
|
+
* @deprecated Renamed to {@link Room.setTimestep} (which pairs with
|
|
480
|
+
* {@link Room.setFixedTimestep}). Kept for backwards compatibility — forwards
|
|
481
|
+
* to `setTimestep` unchanged.
|
|
482
|
+
*/
|
|
483
|
+
setSimulationInterval(onTickCallback, delay = DEFAULT_SIMULATION_INTERVAL) {
|
|
484
|
+
this.setTimestep(onTickCallback, delay);
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Fixed-timestep game loop with a framework-owned accumulator — the right
|
|
488
|
+
* default for prediction/rollback. Unlike {@link setTimestep} (which
|
|
489
|
+
* hands you the *measured* wall-clock delta), this runs `step` a whole number
|
|
490
|
+
* of times per real frame so each step advances by the SAME fixed
|
|
491
|
+
* `dt = 1/tickRate`; the measured delta only decides HOW MANY steps run. The
|
|
492
|
+
* fixed dt is delivered via {@link StepContext}, so the jittery wall-clock
|
|
493
|
+
* delta can't leak into deterministic simulation.
|
|
494
|
+
*
|
|
495
|
+
* `tickRate` is also the SINGLE SOURCE of the simulation rate: it's advertised
|
|
496
|
+
* to predicting clients via the join handshake (they predict at the matching
|
|
497
|
+
* `dt`), so don't also pass `tickRate` to {@link defineInput}.
|
|
498
|
+
*
|
|
499
|
+
* **`tickRate` couples three rates** — by design, one input == one fixed step
|
|
500
|
+
* == one server tick, so lowering `tickRate` to save bandwidth also lowers
|
|
501
|
+
* the simulation rate. To keep high-fidelity physics on a lower network rate,
|
|
502
|
+
* pass `{ subSteps: N }`: one input still drives one fixed step, but you
|
|
503
|
+
* integrate `N` engine sub-steps of `ctx.subDt` (= `ctx.dt / N`) inside it —
|
|
504
|
+
* physics at `tickRate * N` Hz, inputs at `tickRate`/sec. The same
|
|
505
|
+
* `subSteps`/`subDt` are cascaded to predicting clients (via the join
|
|
506
|
+
* handshake, onto their reconciler's step context), so client replay
|
|
507
|
+
* reproduces the sub-stepped trajectory exactly. Render interpolation already
|
|
508
|
+
* smooths above the step rate — most games don't need this; reach for it when
|
|
509
|
+
* the *simulation* needs the extra Hz (fast projectiles, stacking, tunneling).
|
|
510
|
+
*
|
|
511
|
+
* On a hitch the accumulator runs at most a few catch-up steps then drops the
|
|
512
|
+
* backlog (no spiral of death). Lag-comp is recorded once per real frame.
|
|
513
|
+
*
|
|
514
|
+
* **Consuming input inside the step** — how you consume each client's buffer
|
|
515
|
+
* depends on who integrates (see {@link InputAccessor}):
|
|
516
|
+
* - *Per-entity* (each body integrates itself): `for (const cmd of
|
|
517
|
+
* this.inputs.get(sid)) applyInput(player, cmd, ctx.dt)` — N inputs =
|
|
518
|
+
* N sub-integrations, ack lands on the newest applied, and `renderTime`
|
|
519
|
+
* tracks each input (lag comp stays exact per step).
|
|
520
|
+
* - *Shared world* (one solver step advances every body): consume exactly one
|
|
521
|
+
* input per entity per step with `this.inputs.get(sid).next()` (or `take(n)` +
|
|
522
|
+
* sub-step), then `world.step()` once. Draining all and applying only the
|
|
523
|
+
* latest would jump the reconcile ack past inputs you never simulated.
|
|
524
|
+
*
|
|
525
|
+
* @param step - Called once per fixed step with a {@link StepContext}.
|
|
526
|
+
* @param tickRate - Simulation rate in **Hz**. Defaults to 60.
|
|
527
|
+
* @param opts - `subSteps`: physics sub-steps per fixed step (integer ≥ 1,
|
|
528
|
+
* default 1) — see above.
|
|
529
|
+
*
|
|
530
|
+
* @example
|
|
531
|
+
* ```ts
|
|
532
|
+
* onCreate() {
|
|
533
|
+
* // 30 inputs/sec on the wire, physics integrated at 60 Hz:
|
|
534
|
+
* this.setFixedTimestep((ctx) => {
|
|
535
|
+
* this.applyInputs(ctx); // consume ONE input per client per step
|
|
536
|
+
* for (let i = 0; i < ctx.subSteps; i++) this.world.step(ctx.subDt);
|
|
537
|
+
* }, 30, { subSteps: 2 });
|
|
538
|
+
* }
|
|
539
|
+
* ```
|
|
540
|
+
*/
|
|
541
|
+
setFixedTimestep(step, tickRate = Math.round(1e3 / DEFAULT_SIMULATION_INTERVAL), opts) {
|
|
542
|
+
if (this._simulationInterval) {
|
|
543
|
+
clearInterval(this._simulationInterval);
|
|
544
|
+
}
|
|
545
|
+
const stepMs = 1e3 / tickRate;
|
|
546
|
+
const stepSeconds = 1 / tickRate;
|
|
547
|
+
const subSteps = (0, import_InputBuffer.validateSubSteps)(opts?.subSteps, "setFixedTimestep") ?? this._inputController?.options.subSteps ?? 1;
|
|
548
|
+
if (this._inputController) {
|
|
549
|
+
this._inputController.options.tickRate = tickRate;
|
|
550
|
+
this._inputController.options.subSteps = subSteps;
|
|
551
|
+
}
|
|
552
|
+
let cb = step;
|
|
553
|
+
if (this.onUncaughtException !== void 0) {
|
|
554
|
+
cb = (0, import_Utils.wrapTryCatch)(step, this.onUncaughtException.bind(this), import_RoomExceptions.TimestepException, "setFixedTimestep");
|
|
555
|
+
}
|
|
556
|
+
let acc = 0;
|
|
557
|
+
let tick = 0;
|
|
558
|
+
const ctx = {
|
|
559
|
+
dt: stepSeconds,
|
|
560
|
+
dtMs: stepMs,
|
|
561
|
+
tick: 0,
|
|
562
|
+
subSteps,
|
|
563
|
+
subDt: stepSeconds / subSteps,
|
|
564
|
+
subDtMs: stepMs / subSteps
|
|
565
|
+
};
|
|
566
|
+
const MAX_CATCHUP_STEPS = 5;
|
|
567
|
+
this._simulationInterval = setInterval(() => {
|
|
568
|
+
this.clock.tick();
|
|
569
|
+
acc += this.clock.deltaTime;
|
|
570
|
+
let ran = 0;
|
|
571
|
+
while (acc >= stepMs && ran < MAX_CATCHUP_STEPS) {
|
|
572
|
+
acc -= stepMs;
|
|
573
|
+
ctx.tick = tick++;
|
|
574
|
+
cb(ctx);
|
|
575
|
+
ran++;
|
|
576
|
+
}
|
|
577
|
+
if (ran === MAX_CATCHUP_STEPS) {
|
|
578
|
+
acc = 0;
|
|
579
|
+
}
|
|
580
|
+
}, stepMs);
|
|
581
|
+
}
|
|
582
|
+
/** Server-side lag compensation, lazily created. @see allowRewindState */
|
|
583
|
+
#rewind;
|
|
584
|
+
/** @internal The rewind attachments' timeline mode (snapshot/reckon), or
|
|
585
|
+
* `undefined` when no rewind is configured. {@link RoomInput} reads it to
|
|
586
|
+
* derive the wire input stamp mode (snapshot → renderTime, reckon → reckonTime). */
|
|
587
|
+
_timelineMode() {
|
|
588
|
+
return this.#rewind?.timelineMode();
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Enable server-side lag compensation: returns a {@link Rewind} that records the
|
|
592
|
+
* position history of the entities you attach and automatically snapshots them
|
|
593
|
+
* on each broadcast (the patchRate cadence). Attach the collections to rewind,
|
|
594
|
+
* then read past positions (at a client's renderTime) in your hit tests.
|
|
595
|
+
*
|
|
596
|
+
* @example
|
|
597
|
+
* ```ts
|
|
598
|
+
* const rewind = this.allowRewindState({ maxRewindMs: 500 });
|
|
599
|
+
* rewind.attachAll(this.state.enemies, { fields: ["x", "y"] });
|
|
600
|
+
* // in a hit test — rewind to where the SHOOTER saw the world:
|
|
601
|
+
* const seen = rewind.lastSeenBy(shooterSessionId); // needs this.defineInput(...)
|
|
602
|
+
* const seenX = seen.value(enemy, "x"), seenY = seen.value(enemy, "y");
|
|
603
|
+
* ```
|
|
604
|
+
*
|
|
605
|
+
* Per-client stamps auto-enable from the `attachAll` `mode` of the groups you
|
|
606
|
+
* rewind — no `renderTime` flag. The default auto-record fires on each
|
|
607
|
+
* broadcast, snapshotting exactly what the client receives — so the rewind
|
|
608
|
+
* reproduces the client's interpolation and hits stay exact even when the
|
|
609
|
+
* broadcast rate differs from the sim rate (`patchRate ≠ timestep`). Call
|
|
610
|
+
* `rewind.record()` yourself during a tick to take over that cadence (you then
|
|
611
|
+
* own correctness against your own broadcast rate).
|
|
612
|
+
*/
|
|
613
|
+
allowRewindState(opts) {
|
|
614
|
+
this.#rewind = import_Rewind.Rewind.get(this, opts);
|
|
615
|
+
this.#rewind.bindRenderTime((sessionId) => {
|
|
616
|
+
const api = this._inputController?.api;
|
|
617
|
+
if (api === void 0) {
|
|
618
|
+
throw new Error(
|
|
619
|
+
"rewind.lastSeenBy() found no input API. Declare `inputs = this.defineInput(YourInput)`, or use rewind.at(time) with a render time you track yourself."
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
return api.get(sessionId).renderTime;
|
|
623
|
+
});
|
|
624
|
+
this.#rewind.bindReckonTime(
|
|
625
|
+
(sessionId) => this._inputController?.rawReckonTime(sessionId) ?? 0
|
|
626
|
+
);
|
|
627
|
+
this.#rewind.bindNow(() => this.clock.elapsedTime);
|
|
628
|
+
return this.#rewind;
|
|
629
|
+
}
|
|
415
630
|
/**
|
|
416
631
|
* @deprecated Use `.patchRate=` instead.
|
|
417
632
|
*/
|
|
@@ -428,19 +643,7 @@ var Room = class _Room {
|
|
|
428
643
|
this._serializer = serializer;
|
|
429
644
|
}
|
|
430
645
|
async setMetadata(meta, persist = true) {
|
|
431
|
-
|
|
432
|
-
this._listing.metadata = meta;
|
|
433
|
-
} else {
|
|
434
|
-
for (const field in meta) {
|
|
435
|
-
if (!meta.hasOwnProperty(field)) {
|
|
436
|
-
continue;
|
|
437
|
-
}
|
|
438
|
-
this._listing.metadata[field] = meta[field];
|
|
439
|
-
}
|
|
440
|
-
if ("markModified" in this._listing) {
|
|
441
|
-
this._listing.markModified("metadata");
|
|
442
|
-
}
|
|
443
|
-
}
|
|
646
|
+
this._listing.metadata = meta;
|
|
444
647
|
if (persist && this._internalState === RoomInternalState.CREATED) {
|
|
445
648
|
await matchMaker.driver.persist(this._listing);
|
|
446
649
|
this._events.emit("metadata-change");
|
|
@@ -481,7 +684,8 @@ var Room = class _Room {
|
|
|
481
684
|
*
|
|
482
685
|
* @example
|
|
483
686
|
* ```typescript
|
|
484
|
-
* //
|
|
687
|
+
* // Merging with existing metadata: spread `this.metadata` yourself.
|
|
688
|
+
* // `metadata` is always REPLACED (not merged) by setMatchmaking()/setMetadata().
|
|
485
689
|
* await this.setMatchmaking({
|
|
486
690
|
* metadata: { ...this.metadata, round: this.metadata.round + 1 }
|
|
487
691
|
* });
|
|
@@ -595,22 +799,12 @@ var Room = class _Room {
|
|
|
595
799
|
*/
|
|
596
800
|
broadcast(type, ...args) {
|
|
597
801
|
const [message, options] = args;
|
|
598
|
-
if (options && options.afterNextPatch) {
|
|
599
|
-
delete options.afterNextPatch;
|
|
600
|
-
this._afterNextPatchQueue.push(["broadcast", [type, ...args]]);
|
|
601
|
-
return;
|
|
602
|
-
}
|
|
603
802
|
this.broadcastMessageType(type, message, options);
|
|
604
803
|
}
|
|
605
804
|
/**
|
|
606
805
|
* Broadcast bytes (UInt8Arrays) to a particular room
|
|
607
806
|
*/
|
|
608
807
|
broadcastBytes(type, message, options) {
|
|
609
|
-
if (options && options.afterNextPatch) {
|
|
610
|
-
delete options.afterNextPatch;
|
|
611
|
-
this._afterNextPatchQueue.push(["broadcastBytes", arguments]);
|
|
612
|
-
return;
|
|
613
|
-
}
|
|
614
808
|
this.broadcastMessageType(type, message, options);
|
|
615
809
|
}
|
|
616
810
|
/**
|
|
@@ -626,33 +820,118 @@ var Room = class _Room {
|
|
|
626
820
|
if (!this.state) {
|
|
627
821
|
return false;
|
|
628
822
|
}
|
|
629
|
-
const
|
|
630
|
-
this.
|
|
823
|
+
const sNow = this.clock.elapsedTime;
|
|
824
|
+
const hasChanges = this._serializer.applyPatches(
|
|
825
|
+
this.clients,
|
|
826
|
+
this.state,
|
|
827
|
+
this._inputController !== void 0 ? { sNow } : void 0
|
|
828
|
+
);
|
|
829
|
+
this._flushPendingClientFrames();
|
|
830
|
+
this._flushAfterPatchBroadcasts();
|
|
831
|
+
const rw = this.#rewind;
|
|
832
|
+
if (rw !== void 0 && rw.lastRecordedAt !== sNow) {
|
|
833
|
+
rw.record(sNow, this.#_patchRate || void 0);
|
|
834
|
+
}
|
|
631
835
|
return hasChanges;
|
|
632
836
|
}
|
|
633
837
|
onMessage(_messageType, _validationSchema, _callback) {
|
|
634
|
-
|
|
635
|
-
const validationSchema = typeof _callback === "function" ? _validationSchema : void 0;
|
|
636
|
-
const callback = validationSchema === void 0 ? _validationSchema : _callback;
|
|
637
|
-
const removeListener = this.onMessageEvents.on(messageType, this.onUncaughtException !== void 0 ? (0, import_Utils.wrapTryCatch)(callback, this.onUncaughtException.bind(this), import_RoomExceptions.OnMessageException, "onMessage", false, _messageType) : callback);
|
|
638
|
-
if (validationSchema !== void 0) {
|
|
639
|
-
this.onMessageValidators[messageType] = validationSchema;
|
|
640
|
-
}
|
|
641
|
-
return () => {
|
|
642
|
-
removeListener();
|
|
643
|
-
if (this.onMessageEvents.events[messageType].length === 0) {
|
|
644
|
-
delete this.onMessageValidators[messageType];
|
|
645
|
-
}
|
|
646
|
-
};
|
|
838
|
+
return this.#_messages.on(_messageType, _validationSchema, _callback);
|
|
647
839
|
}
|
|
648
840
|
onMessageBytes(_messageType, _validationSchema, _callback) {
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
841
|
+
return this.#_messages.on(`_$b${_messageType}`, _validationSchema, _callback);
|
|
842
|
+
}
|
|
843
|
+
// ---------------------------------------------------------------------------
|
|
844
|
+
// Operator API — used by @colyseus/admin (and monitor in due course)
|
|
845
|
+
// through `remoteRoomCall(roomId, methodName)`. Marked `@internal` because
|
|
846
|
+
// they're framework-tooling primitives, not part of the game-code surface.
|
|
847
|
+
// ---------------------------------------------------------------------------
|
|
848
|
+
/**
|
|
849
|
+
* Snapshot the room's live state for an inspector / admin UI. Includes:
|
|
850
|
+
*
|
|
851
|
+
* roomId, name, maxClients, locked, elapsedTime (ms),
|
|
852
|
+
* metadata, clients (sessionId + per-client elapsed + userId when set),
|
|
853
|
+
* state (the schema/json the SDK would see),
|
|
854
|
+
* stateSize (bytes of the encoded full state, or 0 when no serializer).
|
|
855
|
+
*
|
|
856
|
+
* The payload is intentionally plain JSON — `remoteRoomCall` serializes
|
|
857
|
+
* the return value across process boundaries.
|
|
858
|
+
*
|
|
859
|
+
* @internal Operator-only. Game code should not call this.
|
|
860
|
+
*/
|
|
861
|
+
getInspectorView() {
|
|
862
|
+
const elapsed = this.clock.elapsedTime;
|
|
863
|
+
return {
|
|
864
|
+
roomId: this.roomId,
|
|
865
|
+
name: this.roomName,
|
|
866
|
+
clients: this.clients.length,
|
|
867
|
+
maxClients: this.maxClients,
|
|
868
|
+
locked: this.#_locked,
|
|
869
|
+
elapsedTime: elapsed,
|
|
870
|
+
metadata: this.metadata ?? null,
|
|
871
|
+
// Cast through `unknown`: `ExtractRoomClient<T>` vs `Client & ClientPrivate`
|
|
872
|
+
// don't structurally overlap (the user's `client` generic may carry a
|
|
873
|
+
// narrower userData/auth shape), but the runtime objects we walk here
|
|
874
|
+
// always have the private join-time field. The narrow per-property
|
|
875
|
+
// `(c as any)` reads below keep the cast scoped.
|
|
876
|
+
//
|
|
877
|
+
// userId / userEmail read straight off `client.auth` — the JWT
|
|
878
|
+
// payload @colyseus/auth's default onAuth decodes carries both
|
|
879
|
+
// when the user has them on file. Saves the admin a per-client
|
|
880
|
+
// database round-trip; falls back to null when the client signed
|
|
881
|
+
// in anonymously (or with a custom onAuth that returns a shape
|
|
882
|
+
// without those fields).
|
|
883
|
+
clientList: this.clients.map((c) => {
|
|
884
|
+
const auth = c.auth;
|
|
885
|
+
return {
|
|
886
|
+
sessionId: c.sessionId,
|
|
887
|
+
userId: c.userId ?? auth?.id ?? null,
|
|
888
|
+
userEmail: auth?.email ?? null,
|
|
889
|
+
elapsedTime: elapsed - (c._joinedAt ?? elapsed)
|
|
890
|
+
};
|
|
891
|
+
}),
|
|
892
|
+
state: this.state ?? null,
|
|
893
|
+
stateSize: this.#_inspectorStateSize()
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Force-disconnect a single client by sessionId. No-op when the client
|
|
898
|
+
* isn't connected (idempotent — the caller doesn't need to race-check).
|
|
899
|
+
*
|
|
900
|
+
* @internal Operator-only. Game code disconnects clients by calling
|
|
901
|
+
* `.leave()` on the Client object directly.
|
|
902
|
+
*/
|
|
903
|
+
kickClient(sessionId, closeCode = import_shared_types.CloseCode.CONSENTED, reason) {
|
|
904
|
+
for (const client of this.clients) {
|
|
905
|
+
if (client.sessionId === sessionId) {
|
|
906
|
+
this.#_forciblyCloseClient(client, closeCode, reason);
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Best-effort byte size of the current full state. Falls back to `0`
|
|
913
|
+
* when the room has no serializer or the serializer can't produce a
|
|
914
|
+
* payload (raw rooms, very-early-onCreate, etc.). The serializer
|
|
915
|
+
* detection mirrors `@colyseus/monitor`'s — we read whichever buffer
|
|
916
|
+
* is available across schema v2 and v3.
|
|
917
|
+
*/
|
|
918
|
+
#_inspectorStateSize() {
|
|
919
|
+
const ser = this._serializer;
|
|
920
|
+
if (!ser) {
|
|
921
|
+
return 0;
|
|
922
|
+
}
|
|
923
|
+
const hasState = ser.encoder || ser.state;
|
|
924
|
+
if (!hasState) {
|
|
925
|
+
return 0;
|
|
926
|
+
}
|
|
927
|
+
try {
|
|
928
|
+
const full = ser.getFullState?.();
|
|
929
|
+
if (!full) {
|
|
930
|
+
return 0;
|
|
931
|
+
}
|
|
932
|
+
return full.byteLength ?? full.length ?? 0;
|
|
933
|
+
} catch {
|
|
934
|
+
return 0;
|
|
656
935
|
}
|
|
657
936
|
}
|
|
658
937
|
/**
|
|
@@ -692,6 +971,7 @@ var Room = class _Room {
|
|
|
692
971
|
async _onJoin(client, authContext, connectionOptions) {
|
|
693
972
|
const sessionId = client.sessionId;
|
|
694
973
|
client.reconnectionToken = (0, import_Utils.generateId)();
|
|
974
|
+
this._inputController?.allocate(client);
|
|
695
975
|
if (this._reservedSeatTimeouts[sessionId]) {
|
|
696
976
|
clearTimeout(this._reservedSeatTimeouts[sessionId]);
|
|
697
977
|
delete this._reservedSeatTimeouts[sessionId];
|
|
@@ -700,7 +980,7 @@ var Room = class _Room {
|
|
|
700
980
|
clearTimeout(this._autoDisposeTimeout);
|
|
701
981
|
this._autoDisposeTimeout = void 0;
|
|
702
982
|
}
|
|
703
|
-
if (this._reservedSeats[sessionId] === void 0 && connectionOptions?.reconnectionToken && this.clients.
|
|
983
|
+
if (this._reservedSeats[sessionId] === void 0 && connectionOptions?.reconnectionToken && this.clients.get(sessionId)?.reconnectionToken === connectionOptions.reconnectionToken) {
|
|
704
984
|
(0, import_Debug.debugMatchMaking)("attempting to reconnect client with a stale previous connection - sessionId: '%s', roomId: '%s'", client.sessionId, this.roomId);
|
|
705
985
|
this._reconnectionAttempts[connectionOptions.reconnectionToken] = new import_Utils.Deferred();
|
|
706
986
|
const reconnectionAttemptTimeout = setTimeout(() => {
|
|
@@ -721,13 +1001,14 @@ var Room = class _Room {
|
|
|
721
1001
|
}
|
|
722
1002
|
this._reservedSeats[sessionId][2] = true;
|
|
723
1003
|
(0, import_Debug.debugMatchMaking)("consuming seat reservation, sessionId: '%s' (roomId: %s)", client.sessionId, this.roomId);
|
|
724
|
-
client.
|
|
1004
|
+
client._pendingFrameClients = this.#pendingFrameClients;
|
|
725
1005
|
client.ref["onleave"] = (_) => client.state = import_Transport.ClientState.LEAVING;
|
|
726
1006
|
client.ref.once("close", client.ref["onleave"]);
|
|
727
1007
|
if (isWaitingReconnection) {
|
|
728
1008
|
const reconnectionToken = connectionOptions?.reconnectionToken;
|
|
729
1009
|
if (reconnectionToken && this._reconnections[reconnectionToken]?.[0] === sessionId) {
|
|
730
1010
|
this.clients.push(client);
|
|
1011
|
+
this._inputController?.register(sessionId, client);
|
|
731
1012
|
await this._reconnections[reconnectionToken]?.[1].resolve(client);
|
|
732
1013
|
try {
|
|
733
1014
|
if (this.onReconnect) {
|
|
@@ -767,6 +1048,7 @@ var Room = class _Room {
|
|
|
767
1048
|
throw new import_ServerError.ServerError(import_shared_types.CloseCode.WITH_ERROR, "already disconnected");
|
|
768
1049
|
}
|
|
769
1050
|
this.clients.push(client);
|
|
1051
|
+
this._inputController?.register(sessionId, client);
|
|
770
1052
|
Object.defineProperty(this._reservedSeats, sessionId, {
|
|
771
1053
|
value: this._reservedSeats[sessionId],
|
|
772
1054
|
enumerable: false
|
|
@@ -794,6 +1076,7 @@ var Room = class _Room {
|
|
|
794
1076
|
client.ref["onleave"] = this._onLeave.bind(this, client);
|
|
795
1077
|
client.ref.once("close", client.ref["onleave"]);
|
|
796
1078
|
client.ref.on("message", this._onMessage.bind(this, client));
|
|
1079
|
+
const extraSections = this._inputController?.handshakeSections();
|
|
797
1080
|
client.raw(import_Protocol.getMessageBytes[import_shared_types.Protocol.JOIN_ROOM](
|
|
798
1081
|
client.reconnectionToken,
|
|
799
1082
|
this._serializer.id,
|
|
@@ -801,7 +1084,8 @@ var Room = class _Room {
|
|
|
801
1084
|
* if skipHandshake is true, we don't need to send the handshake
|
|
802
1085
|
* (in case client already has handshake data)
|
|
803
1086
|
*/
|
|
804
|
-
connectionOptions?.skipHandshake ? void 0 : this._serializer.handshake && this._serializer.handshake()
|
|
1087
|
+
connectionOptions?.skipHandshake ? void 0 : this._serializer.handshake && this._serializer.handshake(),
|
|
1088
|
+
extraSections
|
|
805
1089
|
));
|
|
806
1090
|
}
|
|
807
1091
|
}
|
|
@@ -868,7 +1152,7 @@ var Room = class _Room {
|
|
|
868
1152
|
}).finally(() => {
|
|
869
1153
|
cleanup();
|
|
870
1154
|
});
|
|
871
|
-
if (this._reconnectionAttempts[reconnectionToken]) {
|
|
1155
|
+
if (this._reconnectionAttempts[reconnectionToken] !== void 0) {
|
|
872
1156
|
(0, import_Debug.debugMatchMaking)("resolving reconnection attempt for client - sessionId: '%s', roomId: '%s'", sessionId, this.roomId);
|
|
873
1157
|
this._reconnectionAttempts[reconnectionToken].resolve(true);
|
|
874
1158
|
}
|
|
@@ -888,30 +1172,64 @@ var Room = class _Room {
|
|
|
888
1172
|
(0, import_Debug.debugMessage)("broadcast: %O (roomId: %s)", message, this.roomId);
|
|
889
1173
|
const encodedMessage = message instanceof Uint8Array ? import_Protocol.getMessageBytes.raw(import_shared_types.Protocol.ROOM_DATA_BYTES, type, void 0, message) : import_Protocol.getMessageBytes.raw(import_shared_types.Protocol.ROOM_DATA, type, message);
|
|
890
1174
|
const except = typeof options.except !== "undefined" ? Array.isArray(options.except) ? options.except : [options.except] : void 0;
|
|
1175
|
+
if (options.afterNextPatch && this._inputController === void 0) {
|
|
1176
|
+
this.#afterPatchBroadcasts.push({ bytes: encodedMessage, except });
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
const sendOpts = options.afterNextPatch ? AFTER_PATCH_OPTS : void 0;
|
|
891
1180
|
let numClients = this.clients.length;
|
|
892
1181
|
while (numClients--) {
|
|
893
1182
|
const client = this.clients[numClients];
|
|
894
1183
|
if (!except || !except.includes(client)) {
|
|
895
|
-
client.enqueueRaw(encodedMessage);
|
|
1184
|
+
client.enqueueRaw(encodedMessage, sendOpts);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
/** Drain `#afterPatchBroadcasts` right after the patch — each as ONE shared
|
|
1189
|
+
* buffer fanned out to its (non-excepted) recipients. `enqueueRaw` handles
|
|
1190
|
+
* JOINED (send now) vs still-joining (buffer until JOIN) per client. */
|
|
1191
|
+
_flushAfterPatchBroadcasts() {
|
|
1192
|
+
const queued = this.#afterPatchBroadcasts;
|
|
1193
|
+
if (queued.length === 0) {
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
for (let i = 0; i < queued.length; i++) {
|
|
1197
|
+
const { bytes, except } = queued[i];
|
|
1198
|
+
let numClients = this.clients.length;
|
|
1199
|
+
while (numClients--) {
|
|
1200
|
+
const client = this.clients[numClients];
|
|
1201
|
+
if (!except || !except.includes(client)) {
|
|
1202
|
+
client.enqueueRaw(bytes);
|
|
1203
|
+
}
|
|
896
1204
|
}
|
|
897
1205
|
}
|
|
1206
|
+
queued.length = 0;
|
|
898
1207
|
}
|
|
899
1208
|
sendFullState(client) {
|
|
900
|
-
client.raw(this._serializer.getFullState(
|
|
1209
|
+
client.raw(this._serializer.getFullState(
|
|
1210
|
+
client,
|
|
1211
|
+
this._inputController !== void 0 ? { sNow: this.clock.elapsedTime } : void 0
|
|
1212
|
+
));
|
|
901
1213
|
}
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1214
|
+
/** Send each client's staged `afterNextPatch` frames as standalone frames right
|
|
1215
|
+
* after the patch, then reset the tracker. Iterates only the clients that staged
|
|
1216
|
+
* frames this cycle (`#pendingFrameClients`), never the full client list; empty
|
|
1217
|
+
* (→ O(1)) on a tick where nothing was staged. */
|
|
1218
|
+
_flushPendingClientFrames() {
|
|
1219
|
+
const dirty = this.#pendingFrameClients;
|
|
1220
|
+
if (dirty.length === 0) {
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
for (let i = 0; i < dirty.length; i++) {
|
|
1224
|
+
const frames = dirty[i]._pendingFrames;
|
|
1225
|
+
if (frames !== void 0 && frames.length > 0) {
|
|
1226
|
+
for (let j = 0; j < frames.length; j++) {
|
|
1227
|
+
dirty[i].raw(frames[j]);
|
|
911
1228
|
}
|
|
1229
|
+
frames.length = 0;
|
|
912
1230
|
}
|
|
913
|
-
this._afterNextPatchQueue.splice(0, length);
|
|
914
1231
|
}
|
|
1232
|
+
dirty.length = 0;
|
|
915
1233
|
}
|
|
916
1234
|
async _reserveSeat(sessionId, joinOptions = true, authData = void 0, seconds = this.seatReservationTimeout, allowReconnection = false, devModeReconnectionToken) {
|
|
917
1235
|
if (!allowReconnection && this.hasReachedMaxClients()) {
|
|
@@ -945,6 +1263,7 @@ var Room = class _Room {
|
|
|
945
1263
|
delete this._reconnections[devModeReconnectionToken];
|
|
946
1264
|
delete this._reservedSeats[sessionId];
|
|
947
1265
|
delete this._reservedSeatTimeouts[sessionId];
|
|
1266
|
+
this._inputController?.release(sessionId);
|
|
948
1267
|
if (!allowReconnection) {
|
|
949
1268
|
await this.#_decrementClientCount();
|
|
950
1269
|
}
|
|
@@ -986,11 +1305,12 @@ var Room = class _Room {
|
|
|
986
1305
|
this._simulationInterval = void 0;
|
|
987
1306
|
}
|
|
988
1307
|
if (this._autoDisposeTimeout) {
|
|
989
|
-
|
|
1308
|
+
clearTimeout(this._autoDisposeTimeout);
|
|
990
1309
|
this._autoDisposeTimeout = void 0;
|
|
991
1310
|
}
|
|
992
1311
|
this.clock.clear();
|
|
993
1312
|
this.clock.stop();
|
|
1313
|
+
this._inputController?.dispose();
|
|
994
1314
|
return await (userReturnData || Promise.resolve());
|
|
995
1315
|
}
|
|
996
1316
|
_onMessage(client, buffer) {
|
|
@@ -1009,49 +1329,18 @@ var Room = class _Room {
|
|
|
1009
1329
|
return this.#_forciblyCloseClient(client, import_shared_types.CloseCode.WITH_ERROR);
|
|
1010
1330
|
}
|
|
1011
1331
|
const it = { offset: 1 };
|
|
1012
|
-
const code = buffer[0];
|
|
1332
|
+
const code = buffer[0] & import_shared_types.PROTOCOL_CODE_MASK;
|
|
1333
|
+
const modifiers = buffer[0] & import_shared_types.PROTOCOL_MODIFIER_MASK;
|
|
1013
1334
|
if (code === import_shared_types.Protocol.ROOM_DATA) {
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
message = buffer.byteLength > it.offset ? (0, import_msgpackr.unpack)(buffer.subarray(it.offset, buffer.byteLength)) : void 0;
|
|
1018
|
-
(0, import_Debug.debugMessage)("received: '%s' -> %j (roomId: %s)", messageType, message, this.roomId);
|
|
1019
|
-
if (this.onMessageValidators[messageType] !== void 0) {
|
|
1020
|
-
message = (0, import_StandardSchema.standardValidate)(this.onMessageValidators[messageType], message);
|
|
1021
|
-
}
|
|
1022
|
-
} catch (e) {
|
|
1023
|
-
(0, import_Debug.debugAndPrintError)(e);
|
|
1024
|
-
client.leave(import_shared_types.CloseCode.WITH_ERROR);
|
|
1025
|
-
return;
|
|
1026
|
-
}
|
|
1027
|
-
if (this.onMessageEvents.events[messageType]) {
|
|
1028
|
-
this.onMessageEvents.emit(messageType, client, message);
|
|
1029
|
-
} else if (this.onMessageEvents.events["*"]) {
|
|
1030
|
-
this.onMessageEvents.emit("*", client, messageType, message);
|
|
1031
|
-
} else {
|
|
1032
|
-
this.onMessageFallbacks["__no_message_handler"](client, messageType, message);
|
|
1033
|
-
}
|
|
1335
|
+
this.#_messages.onData(client, buffer, it);
|
|
1336
|
+
} else if (code === import_shared_types.Protocol.ROOM_REQUEST) {
|
|
1337
|
+
this.#_messages.onRequest(client, buffer, it);
|
|
1034
1338
|
} else if (code === import_shared_types.Protocol.ROOM_DATA_BYTES) {
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
if (this.onMessageValidators[bytesMessageType] !== void 0) {
|
|
1041
|
-
message = (0, import_StandardSchema.standardValidate)(this.onMessageValidators[bytesMessageType], message);
|
|
1042
|
-
}
|
|
1043
|
-
} catch (e) {
|
|
1044
|
-
(0, import_Debug.debugAndPrintError)(e);
|
|
1045
|
-
client.leave(import_shared_types.CloseCode.WITH_ERROR);
|
|
1046
|
-
return;
|
|
1047
|
-
}
|
|
1048
|
-
if (this.onMessageEvents.events[bytesMessageType]) {
|
|
1049
|
-
this.onMessageEvents.emit(bytesMessageType, client, message);
|
|
1050
|
-
} else if (this.onMessageEvents.events["*"]) {
|
|
1051
|
-
this.onMessageEvents.emit("*", client, messageType, message);
|
|
1052
|
-
} else {
|
|
1053
|
-
this.onMessageFallbacks["__no_message_handler"](client, messageType, message);
|
|
1054
|
-
}
|
|
1339
|
+
this.#_messages.onDataBytes(client, buffer, it);
|
|
1340
|
+
} else if (code === import_shared_types.Protocol.ROOM_INPUT_RELIABLE) {
|
|
1341
|
+
this._inputController?.decodeReliable(client, buffer, it, modifiers);
|
|
1342
|
+
} else if (code === import_shared_types.Protocol.ROOM_INPUT_UNRELIABLE) {
|
|
1343
|
+
this._inputController?.decodeUnreliable(client, buffer, modifiers);
|
|
1055
1344
|
} else if (code === import_shared_types.Protocol.JOIN_ROOM && client.state === import_Transport.ClientState.JOINING) {
|
|
1056
1345
|
client.state = import_Transport.ClientState.JOINED;
|
|
1057
1346
|
client._joinedAt = this.clock.elapsedTime;
|
|
@@ -1068,10 +1357,10 @@ var Room = class _Room {
|
|
|
1068
1357
|
this.#_forciblyCloseClient(client, import_shared_types.CloseCode.CONSENTED);
|
|
1069
1358
|
}
|
|
1070
1359
|
}
|
|
1071
|
-
#_forciblyCloseClient(client, closeCode) {
|
|
1360
|
+
#_forciblyCloseClient(client, closeCode, reason) {
|
|
1072
1361
|
client.ref.removeAllListeners("message");
|
|
1073
1362
|
client.ref.removeListener("close", client.ref["onleave"]);
|
|
1074
|
-
this._onLeave(client, closeCode).then(() => client.leave(closeCode));
|
|
1363
|
+
this._onLeave(client, closeCode).then(() => client.leave(closeCode, reason));
|
|
1075
1364
|
}
|
|
1076
1365
|
async _onLeave(client, code) {
|
|
1077
1366
|
const method = code === import_shared_types.CloseCode.CONSENTED || client.state === import_Transport.ClientState.RECONNECTING ? this.onLeave : this.onDrop || this.onLeave;
|
|
@@ -1079,6 +1368,7 @@ var Room = class _Room {
|
|
|
1079
1368
|
if (!this.clients.delete(client)) {
|
|
1080
1369
|
return;
|
|
1081
1370
|
}
|
|
1371
|
+
this._inputController?.freeze(client);
|
|
1082
1372
|
if (method) {
|
|
1083
1373
|
(0, import_Debug.debugMatchMaking)(`${method.name}, sessionId: '%s' (close code: %d, roomId: %s)`, client.sessionId, code, this.roomId);
|
|
1084
1374
|
try {
|
|
@@ -1105,6 +1395,7 @@ var Room = class _Room {
|
|
|
1105
1395
|
}
|
|
1106
1396
|
const willDispose = await this.#_decrementClientCount();
|
|
1107
1397
|
if (this._reservedSeats[client.sessionId] === void 0) {
|
|
1398
|
+
this._inputController?.release(client.sessionId);
|
|
1108
1399
|
this._events.emit("leave", client, willDispose);
|
|
1109
1400
|
}
|
|
1110
1401
|
}
|
|
@@ -1168,28 +1459,13 @@ var Room = class _Room {
|
|
|
1168
1459
|
}
|
|
1169
1460
|
}
|
|
1170
1461
|
};
|
|
1171
|
-
function room(options) {
|
|
1172
|
-
class _ extends Room {
|
|
1173
|
-
constructor() {
|
|
1174
|
-
super();
|
|
1175
|
-
this.messages = options.messages;
|
|
1176
|
-
if (options.state && typeof options.state === "function") {
|
|
1177
|
-
this.state = options.state();
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
for (const key in options) {
|
|
1182
|
-
if (typeof options[key] === "function") {
|
|
1183
|
-
_.prototype[key] = options[key];
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
return _;
|
|
1187
|
-
}
|
|
1188
1462
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1189
1463
|
0 && (module.exports = {
|
|
1190
1464
|
DEFAULT_SEAT_RESERVATION_TIME,
|
|
1191
1465
|
Room,
|
|
1192
1466
|
RoomInternalState,
|
|
1193
|
-
|
|
1467
|
+
RoomPlugin,
|
|
1468
|
+
attachToTestRoom,
|
|
1469
|
+
definePlugins,
|
|
1194
1470
|
validate
|
|
1195
1471
|
});
|