@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.mjs
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// packages/core/src/Room.ts
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { $changes } from "@colyseus/schema";
|
|
3
|
+
import { validateSubSteps } from "./input/InputBuffer.mjs";
|
|
4
|
+
import { RoomInput } from "./input/RoomInput.mjs";
|
|
5
|
+
import { RoomMessages } from "./RoomMessages.mjs";
|
|
6
|
+
import { Rewind } from "./Rewind.mjs";
|
|
7
|
+
import {} from "./input/types.mjs";
|
|
4
8
|
import { ClockTimer as Clock } from "@colyseus/timer";
|
|
5
9
|
import { EventEmitter } from "events";
|
|
6
10
|
import { logger } from "./Logger.mjs";
|
|
@@ -8,22 +12,32 @@ import { NoneSerializer } from "./serializer/NoneSerializer.mjs";
|
|
|
8
12
|
import { SchemaSerializer } from "./serializer/SchemaSerializer.mjs";
|
|
9
13
|
import { getMessageBytes } from "./Protocol.mjs";
|
|
10
14
|
import { Deferred, generateId, wrapTryCatch } from "./utils/Utils.mjs";
|
|
11
|
-
import { createNanoEvents } from "./utils/nanoevents.mjs";
|
|
12
15
|
import { isDevMode } from "./utils/DevMode.mjs";
|
|
13
16
|
import { debugAndPrintError, debugMatchMaking, debugMessage } from "./Debug.mjs";
|
|
14
17
|
import { ServerError } from "./errors/ServerError.mjs";
|
|
15
18
|
import { ClientState, ClientArray } from "./Transport.mjs";
|
|
16
|
-
import { OnAuthException, OnCreateException, OnDisposeException, OnDropException, OnJoinException, OnLeaveException,
|
|
17
|
-
import
|
|
19
|
+
import { OnAuthException, OnCreateException, OnDisposeException, OnDropException, OnJoinException, OnLeaveException, OnReconnectException, TimestepException, TimedEventException } from "./errors/RoomExceptions.mjs";
|
|
20
|
+
import "./utils/StandardSchema.mjs";
|
|
18
21
|
import * as matchMaker from "./MatchMaker.mjs";
|
|
19
22
|
import {
|
|
20
23
|
CloseCode,
|
|
21
24
|
ErrorCode,
|
|
22
|
-
Protocol
|
|
25
|
+
Protocol,
|
|
26
|
+
PROTOCOL_CODE_MASK,
|
|
27
|
+
PROTOCOL_MODIFIER_MASK
|
|
23
28
|
} from "@colyseus/shared-types";
|
|
29
|
+
import {
|
|
30
|
+
setupRoomPlugins
|
|
31
|
+
} from "./RoomPlugin.mjs";
|
|
32
|
+
import {
|
|
33
|
+
RoomPlugin as RoomPlugin2,
|
|
34
|
+
definePlugins,
|
|
35
|
+
attachToTestRoom
|
|
36
|
+
} from "./RoomPlugin.mjs";
|
|
24
37
|
var DEFAULT_PATCH_RATE = 1e3 / 20;
|
|
25
38
|
var DEFAULT_SIMULATION_INTERVAL = 1e3 / 60;
|
|
26
39
|
var noneSerializer = new NoneSerializer();
|
|
40
|
+
var AFTER_PATCH_OPTS = Object.freeze({ afterNextPatch: true });
|
|
27
41
|
var DEFAULT_SEAT_RESERVATION_TIME = Number(process.env.COLYSEUS_SEAT_RESERVATION_TIME || 15);
|
|
28
42
|
function validate(format, handler) {
|
|
29
43
|
return { format, handler };
|
|
@@ -88,21 +102,19 @@ var Room = class _Room {
|
|
|
88
102
|
this._reservedSeatTimeouts = {};
|
|
89
103
|
this._reconnections = {};
|
|
90
104
|
this._reconnectionAttempts = {};
|
|
91
|
-
|
|
92
|
-
this
|
|
93
|
-
this.onMessageFallbacks = {
|
|
94
|
-
"__no_message_handler": (client, messageType, _) => {
|
|
95
|
-
const errorMessage = `room onMessage for "${messageType}" not registered.`;
|
|
96
|
-
debugMessage(`${errorMessage} (roomId: ${this.roomId})`);
|
|
97
|
-
if (isDevMode) {
|
|
98
|
-
client.error(ErrorCode.INVALID_PAYLOAD, errorMessage);
|
|
99
|
-
} else {
|
|
100
|
-
client.leave(CloseCode.WITH_ERROR, errorMessage);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
};
|
|
105
|
+
/** @internal Message-routing layer: handler registry + user-message decode/dispatch. */
|
|
106
|
+
this.#_messages = new RoomMessages(this);
|
|
104
107
|
this._serializer = noneSerializer;
|
|
105
|
-
|
|
108
|
+
/** Clients that staged `afterNextPatch` frames since the last patch, so the
|
|
109
|
+
* post-patch flush iterates only these — never the full client list. Shared by
|
|
110
|
+
* reference onto each client at join. Reset each `broadcastPatch`. */
|
|
111
|
+
this.#pendingFrameClients = [];
|
|
112
|
+
/** `broadcast(..., { afterNextPatch })` in NON-timed rooms, sent as a SHARED
|
|
113
|
+
* frame right after the next patch (one encode, N sends). Non-timed patches are
|
|
114
|
+
* themselves a shared buffer, so a per-client copy would force N× allocation in
|
|
115
|
+
* large rooms — sharing avoids that. TIMED rooms instead stage the broadcast onto
|
|
116
|
+
* each client's `_pendingFrames`. Drained each `broadcastPatch`. */
|
|
117
|
+
this.#afterPatchBroadcasts = [];
|
|
106
118
|
this._internalState = RoomInternalState.CREATING;
|
|
107
119
|
this._lockedExplicitly = false;
|
|
108
120
|
this.#_locked = false;
|
|
@@ -158,6 +170,17 @@ var Room = class _Room {
|
|
|
158
170
|
#_patchRate;
|
|
159
171
|
#_patchInterval;
|
|
160
172
|
#_state;
|
|
173
|
+
#_messages;
|
|
174
|
+
// Re-expose the registry for @colyseus/playground introspection and
|
|
175
|
+
// @colyseus/testing handler-swapping (both reach in via bracket access).
|
|
176
|
+
get onMessageEvents() {
|
|
177
|
+
return this.#_messages.events;
|
|
178
|
+
}
|
|
179
|
+
get onMessageValidators() {
|
|
180
|
+
return this.#_messages.validators;
|
|
181
|
+
}
|
|
182
|
+
#pendingFrameClients;
|
|
183
|
+
#afterPatchBroadcasts;
|
|
161
184
|
#_locked;
|
|
162
185
|
/**
|
|
163
186
|
* This method is called by the MatchMaker before onCreate()
|
|
@@ -222,6 +245,9 @@ var Room = class _Room {
|
|
|
222
245
|
if (this.#_state) {
|
|
223
246
|
this.state = this.#_state;
|
|
224
247
|
}
|
|
248
|
+
if (this.plugins !== void 0) {
|
|
249
|
+
setupRoomPlugins(this);
|
|
250
|
+
}
|
|
225
251
|
if (this.messages !== void 0) {
|
|
226
252
|
if (this.messages["_"]) {
|
|
227
253
|
this.onMessage("*", this.messages["_"].bind(this));
|
|
@@ -278,6 +304,44 @@ var Room = class _Room {
|
|
|
278
304
|
}
|
|
279
305
|
this.#_roomId = roomId;
|
|
280
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* Declare the input schema and configuration in a single line. Returns the
|
|
309
|
+
* input API that gets assigned to `this.inputs` — call
|
|
310
|
+
* `this.inputs.get(sessionId)` per tick to consume.
|
|
311
|
+
*
|
|
312
|
+
* ```typescript
|
|
313
|
+
* class FpsRoom extends Room<{ input: MoveInput }> {
|
|
314
|
+
* inputs = this.defineInput(MoveInput, {
|
|
315
|
+
* seqField: "tick", // typed: only numeric fields of MoveInput
|
|
316
|
+
* bufferMaxSize: 64,
|
|
317
|
+
* });
|
|
318
|
+
*
|
|
319
|
+
* // …or without options — no seq dedupe, bufferMaxSize: 32:
|
|
320
|
+
* // inputs = this.defineInput(MoveInput);
|
|
321
|
+
* }
|
|
322
|
+
* ```
|
|
323
|
+
*
|
|
324
|
+
* **Defaults** when `opts` (or individual fields) are omitted:
|
|
325
|
+
* - `seqField`: unset — dedupe and `this.inputs.get(sessionId).at(value)` lookup are
|
|
326
|
+
* OPT-IN (lockstep / rollback). Name a monotonic numeric field here to enable
|
|
327
|
+
* them; redundant frames (`input[seqField]` ≤ the last seen) are then dropped.
|
|
328
|
+
* Leave unset for reliable, in-order channels where every frame is unique.
|
|
329
|
+
* - `bufferMaxSize`: `32` — enables per-client snapshot buffering for
|
|
330
|
+
* `this.inputs.get(sessionId)` iteration / `.consume() / .drain() / .next() /
|
|
331
|
+
* .take() / .peek() / .at()`. Set to `0` to disable buffering (the `.latest`
|
|
332
|
+
* read still works).
|
|
333
|
+
*
|
|
334
|
+
* **Consuming the buffer** — `for (const inp of this.inputs.get(sessionId))` (sugar
|
|
335
|
+
* for `.consume()`) is the per-entity loop: it consumes one at a time, so
|
|
336
|
+
* lag-comp `renderTime` tracks each input. `next()` (take exactly one, ack +1)
|
|
337
|
+
* suits a shared physics world stepped once for everyone; `drain()` returns the
|
|
338
|
+
* whole pending set as an array. See {@link InputAccessor} for the full
|
|
339
|
+
* per-entity-vs-shared-world guidance and why the choice affects the reconcile ack.
|
|
340
|
+
*/
|
|
341
|
+
defineInput(type, opts) {
|
|
342
|
+
this._inputController ??= new RoomInput(this);
|
|
343
|
+
return this._inputController.define(type, opts);
|
|
344
|
+
}
|
|
281
345
|
/**
|
|
282
346
|
* This method is called before onJoin() - this is where you should authenticate the client
|
|
283
347
|
* @param client - The client that is authenticating.
|
|
@@ -337,7 +401,7 @@ var Room = class _Room {
|
|
|
337
401
|
reservedSeat[3] && this._reconnections[reconnectionToken]?.[0] === sessionId
|
|
338
402
|
);
|
|
339
403
|
} else if (typeof reconnectionToken === "string") {
|
|
340
|
-
return this.clients.
|
|
404
|
+
return this.clients.get(sessionId)?.reconnectionToken === reconnectionToken;
|
|
341
405
|
}
|
|
342
406
|
return false;
|
|
343
407
|
}
|
|
@@ -355,22 +419,27 @@ var Room = class _Room {
|
|
|
355
419
|
return void 0;
|
|
356
420
|
}
|
|
357
421
|
/**
|
|
358
|
-
*
|
|
359
|
-
*
|
|
422
|
+
* Set the room's game loop. `onTickCallback` runs every `delay` ms and receives
|
|
423
|
+
* the MEASURED wall-clock delta since the previous tick (a VARIABLE timestep).
|
|
424
|
+
* For deterministic, prediction-friendly simulation prefer
|
|
425
|
+
* {@link Room.setFixedTimestep}, which advances a fixed step via an accumulator.
|
|
360
426
|
*
|
|
361
427
|
* @default 16.6ms (60fps)
|
|
362
428
|
*
|
|
363
|
-
* @param onTickCallback -
|
|
364
|
-
*
|
|
365
|
-
* @param delay - Interval
|
|
429
|
+
* @param onTickCallback - Your physics / world update — a good place to mutate
|
|
430
|
+
* room state. Receives the measured delta (`this.clock.deltaTime`).
|
|
431
|
+
* @param delay - Interval between ticks in milliseconds.
|
|
366
432
|
*/
|
|
367
|
-
|
|
433
|
+
setTimestep(onTickCallback, delay = DEFAULT_SIMULATION_INTERVAL) {
|
|
368
434
|
if (this._simulationInterval) {
|
|
369
435
|
clearInterval(this._simulationInterval);
|
|
370
436
|
}
|
|
437
|
+
if (onTickCallback && this._inputController && this._inputController.options.tickRate === void 0) {
|
|
438
|
+
this._inputController.options.tickRate = Math.round(1e3 / delay);
|
|
439
|
+
}
|
|
371
440
|
if (onTickCallback) {
|
|
372
441
|
if (this.onUncaughtException !== void 0) {
|
|
373
|
-
onTickCallback = wrapTryCatch(onTickCallback, this.onUncaughtException.bind(this),
|
|
442
|
+
onTickCallback = wrapTryCatch(onTickCallback, this.onUncaughtException.bind(this), TimestepException, "setTimestep");
|
|
374
443
|
}
|
|
375
444
|
this._simulationInterval = setInterval(() => {
|
|
376
445
|
this.clock.tick();
|
|
@@ -378,6 +447,158 @@ var Room = class _Room {
|
|
|
378
447
|
}, delay);
|
|
379
448
|
}
|
|
380
449
|
}
|
|
450
|
+
/**
|
|
451
|
+
* @deprecated Renamed to {@link Room.setTimestep} (which pairs with
|
|
452
|
+
* {@link Room.setFixedTimestep}). Kept for backwards compatibility — forwards
|
|
453
|
+
* to `setTimestep` unchanged.
|
|
454
|
+
*/
|
|
455
|
+
setSimulationInterval(onTickCallback, delay = DEFAULT_SIMULATION_INTERVAL) {
|
|
456
|
+
this.setTimestep(onTickCallback, delay);
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Fixed-timestep game loop with a framework-owned accumulator — the right
|
|
460
|
+
* default for prediction/rollback. Unlike {@link setTimestep} (which
|
|
461
|
+
* hands you the *measured* wall-clock delta), this runs `step` a whole number
|
|
462
|
+
* of times per real frame so each step advances by the SAME fixed
|
|
463
|
+
* `dt = 1/tickRate`; the measured delta only decides HOW MANY steps run. The
|
|
464
|
+
* fixed dt is delivered via {@link StepContext}, so the jittery wall-clock
|
|
465
|
+
* delta can't leak into deterministic simulation.
|
|
466
|
+
*
|
|
467
|
+
* `tickRate` is also the SINGLE SOURCE of the simulation rate: it's advertised
|
|
468
|
+
* to predicting clients via the join handshake (they predict at the matching
|
|
469
|
+
* `dt`), so don't also pass `tickRate` to {@link defineInput}.
|
|
470
|
+
*
|
|
471
|
+
* **`tickRate` couples three rates** — by design, one input == one fixed step
|
|
472
|
+
* == one server tick, so lowering `tickRate` to save bandwidth also lowers
|
|
473
|
+
* the simulation rate. To keep high-fidelity physics on a lower network rate,
|
|
474
|
+
* pass `{ subSteps: N }`: one input still drives one fixed step, but you
|
|
475
|
+
* integrate `N` engine sub-steps of `ctx.subDt` (= `ctx.dt / N`) inside it —
|
|
476
|
+
* physics at `tickRate * N` Hz, inputs at `tickRate`/sec. The same
|
|
477
|
+
* `subSteps`/`subDt` are cascaded to predicting clients (via the join
|
|
478
|
+
* handshake, onto their reconciler's step context), so client replay
|
|
479
|
+
* reproduces the sub-stepped trajectory exactly. Render interpolation already
|
|
480
|
+
* smooths above the step rate — most games don't need this; reach for it when
|
|
481
|
+
* the *simulation* needs the extra Hz (fast projectiles, stacking, tunneling).
|
|
482
|
+
*
|
|
483
|
+
* On a hitch the accumulator runs at most a few catch-up steps then drops the
|
|
484
|
+
* backlog (no spiral of death). Lag-comp is recorded once per real frame.
|
|
485
|
+
*
|
|
486
|
+
* **Consuming input inside the step** — how you consume each client's buffer
|
|
487
|
+
* depends on who integrates (see {@link InputAccessor}):
|
|
488
|
+
* - *Per-entity* (each body integrates itself): `for (const cmd of
|
|
489
|
+
* this.inputs.get(sid)) applyInput(player, cmd, ctx.dt)` — N inputs =
|
|
490
|
+
* N sub-integrations, ack lands on the newest applied, and `renderTime`
|
|
491
|
+
* tracks each input (lag comp stays exact per step).
|
|
492
|
+
* - *Shared world* (one solver step advances every body): consume exactly one
|
|
493
|
+
* input per entity per step with `this.inputs.get(sid).next()` (or `take(n)` +
|
|
494
|
+
* sub-step), then `world.step()` once. Draining all and applying only the
|
|
495
|
+
* latest would jump the reconcile ack past inputs you never simulated.
|
|
496
|
+
*
|
|
497
|
+
* @param step - Called once per fixed step with a {@link StepContext}.
|
|
498
|
+
* @param tickRate - Simulation rate in **Hz**. Defaults to 60.
|
|
499
|
+
* @param opts - `subSteps`: physics sub-steps per fixed step (integer ≥ 1,
|
|
500
|
+
* default 1) — see above.
|
|
501
|
+
*
|
|
502
|
+
* @example
|
|
503
|
+
* ```ts
|
|
504
|
+
* onCreate() {
|
|
505
|
+
* // 30 inputs/sec on the wire, physics integrated at 60 Hz:
|
|
506
|
+
* this.setFixedTimestep((ctx) => {
|
|
507
|
+
* this.applyInputs(ctx); // consume ONE input per client per step
|
|
508
|
+
* for (let i = 0; i < ctx.subSteps; i++) this.world.step(ctx.subDt);
|
|
509
|
+
* }, 30, { subSteps: 2 });
|
|
510
|
+
* }
|
|
511
|
+
* ```
|
|
512
|
+
*/
|
|
513
|
+
setFixedTimestep(step, tickRate = Math.round(1e3 / DEFAULT_SIMULATION_INTERVAL), opts) {
|
|
514
|
+
if (this._simulationInterval) {
|
|
515
|
+
clearInterval(this._simulationInterval);
|
|
516
|
+
}
|
|
517
|
+
const stepMs = 1e3 / tickRate;
|
|
518
|
+
const stepSeconds = 1 / tickRate;
|
|
519
|
+
const subSteps = validateSubSteps(opts?.subSteps, "setFixedTimestep") ?? this._inputController?.options.subSteps ?? 1;
|
|
520
|
+
if (this._inputController) {
|
|
521
|
+
this._inputController.options.tickRate = tickRate;
|
|
522
|
+
this._inputController.options.subSteps = subSteps;
|
|
523
|
+
}
|
|
524
|
+
let cb = step;
|
|
525
|
+
if (this.onUncaughtException !== void 0) {
|
|
526
|
+
cb = wrapTryCatch(step, this.onUncaughtException.bind(this), TimestepException, "setFixedTimestep");
|
|
527
|
+
}
|
|
528
|
+
let acc = 0;
|
|
529
|
+
let tick = 0;
|
|
530
|
+
const ctx = {
|
|
531
|
+
dt: stepSeconds,
|
|
532
|
+
dtMs: stepMs,
|
|
533
|
+
tick: 0,
|
|
534
|
+
subSteps,
|
|
535
|
+
subDt: stepSeconds / subSteps,
|
|
536
|
+
subDtMs: stepMs / subSteps
|
|
537
|
+
};
|
|
538
|
+
const MAX_CATCHUP_STEPS = 5;
|
|
539
|
+
this._simulationInterval = setInterval(() => {
|
|
540
|
+
this.clock.tick();
|
|
541
|
+
acc += this.clock.deltaTime;
|
|
542
|
+
let ran = 0;
|
|
543
|
+
while (acc >= stepMs && ran < MAX_CATCHUP_STEPS) {
|
|
544
|
+
acc -= stepMs;
|
|
545
|
+
ctx.tick = tick++;
|
|
546
|
+
cb(ctx);
|
|
547
|
+
ran++;
|
|
548
|
+
}
|
|
549
|
+
if (ran === MAX_CATCHUP_STEPS) {
|
|
550
|
+
acc = 0;
|
|
551
|
+
}
|
|
552
|
+
}, stepMs);
|
|
553
|
+
}
|
|
554
|
+
/** Server-side lag compensation, lazily created. @see allowRewindState */
|
|
555
|
+
#rewind;
|
|
556
|
+
/** @internal The rewind attachments' timeline mode (snapshot/reckon), or
|
|
557
|
+
* `undefined` when no rewind is configured. {@link RoomInput} reads it to
|
|
558
|
+
* derive the wire input stamp mode (snapshot → renderTime, reckon → reckonTime). */
|
|
559
|
+
_timelineMode() {
|
|
560
|
+
return this.#rewind?.timelineMode();
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Enable server-side lag compensation: returns a {@link Rewind} that records the
|
|
564
|
+
* position history of the entities you attach and automatically snapshots them
|
|
565
|
+
* on each broadcast (the patchRate cadence). Attach the collections to rewind,
|
|
566
|
+
* then read past positions (at a client's renderTime) in your hit tests.
|
|
567
|
+
*
|
|
568
|
+
* @example
|
|
569
|
+
* ```ts
|
|
570
|
+
* const rewind = this.allowRewindState({ maxRewindMs: 500 });
|
|
571
|
+
* rewind.attachAll(this.state.enemies, { fields: ["x", "y"] });
|
|
572
|
+
* // in a hit test — rewind to where the SHOOTER saw the world:
|
|
573
|
+
* const seen = rewind.lastSeenBy(shooterSessionId); // needs this.defineInput(...)
|
|
574
|
+
* const seenX = seen.value(enemy, "x"), seenY = seen.value(enemy, "y");
|
|
575
|
+
* ```
|
|
576
|
+
*
|
|
577
|
+
* Per-client stamps auto-enable from the `attachAll` `mode` of the groups you
|
|
578
|
+
* rewind — no `renderTime` flag. The default auto-record fires on each
|
|
579
|
+
* broadcast, snapshotting exactly what the client receives — so the rewind
|
|
580
|
+
* reproduces the client's interpolation and hits stay exact even when the
|
|
581
|
+
* broadcast rate differs from the sim rate (`patchRate ≠ timestep`). Call
|
|
582
|
+
* `rewind.record()` yourself during a tick to take over that cadence (you then
|
|
583
|
+
* own correctness against your own broadcast rate).
|
|
584
|
+
*/
|
|
585
|
+
allowRewindState(opts) {
|
|
586
|
+
this.#rewind = Rewind.get(this, opts);
|
|
587
|
+
this.#rewind.bindRenderTime((sessionId) => {
|
|
588
|
+
const api = this._inputController?.api;
|
|
589
|
+
if (api === void 0) {
|
|
590
|
+
throw new Error(
|
|
591
|
+
"rewind.lastSeenBy() found no input API. Declare `inputs = this.defineInput(YourInput)`, or use rewind.at(time) with a render time you track yourself."
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
return api.get(sessionId).renderTime;
|
|
595
|
+
});
|
|
596
|
+
this.#rewind.bindReckonTime(
|
|
597
|
+
(sessionId) => this._inputController?.rawReckonTime(sessionId) ?? 0
|
|
598
|
+
);
|
|
599
|
+
this.#rewind.bindNow(() => this.clock.elapsedTime);
|
|
600
|
+
return this.#rewind;
|
|
601
|
+
}
|
|
381
602
|
/**
|
|
382
603
|
* @deprecated Use `.patchRate=` instead.
|
|
383
604
|
*/
|
|
@@ -394,19 +615,7 @@ var Room = class _Room {
|
|
|
394
615
|
this._serializer = serializer;
|
|
395
616
|
}
|
|
396
617
|
async setMetadata(meta, persist = true) {
|
|
397
|
-
|
|
398
|
-
this._listing.metadata = meta;
|
|
399
|
-
} else {
|
|
400
|
-
for (const field in meta) {
|
|
401
|
-
if (!meta.hasOwnProperty(field)) {
|
|
402
|
-
continue;
|
|
403
|
-
}
|
|
404
|
-
this._listing.metadata[field] = meta[field];
|
|
405
|
-
}
|
|
406
|
-
if ("markModified" in this._listing) {
|
|
407
|
-
this._listing.markModified("metadata");
|
|
408
|
-
}
|
|
409
|
-
}
|
|
618
|
+
this._listing.metadata = meta;
|
|
410
619
|
if (persist && this._internalState === RoomInternalState.CREATED) {
|
|
411
620
|
await matchMaker.driver.persist(this._listing);
|
|
412
621
|
this._events.emit("metadata-change");
|
|
@@ -447,7 +656,8 @@ var Room = class _Room {
|
|
|
447
656
|
*
|
|
448
657
|
* @example
|
|
449
658
|
* ```typescript
|
|
450
|
-
* //
|
|
659
|
+
* // Merging with existing metadata: spread `this.metadata` yourself.
|
|
660
|
+
* // `metadata` is always REPLACED (not merged) by setMatchmaking()/setMetadata().
|
|
451
661
|
* await this.setMatchmaking({
|
|
452
662
|
* metadata: { ...this.metadata, round: this.metadata.round + 1 }
|
|
453
663
|
* });
|
|
@@ -561,22 +771,12 @@ var Room = class _Room {
|
|
|
561
771
|
*/
|
|
562
772
|
broadcast(type, ...args) {
|
|
563
773
|
const [message, options] = args;
|
|
564
|
-
if (options && options.afterNextPatch) {
|
|
565
|
-
delete options.afterNextPatch;
|
|
566
|
-
this._afterNextPatchQueue.push(["broadcast", [type, ...args]]);
|
|
567
|
-
return;
|
|
568
|
-
}
|
|
569
774
|
this.broadcastMessageType(type, message, options);
|
|
570
775
|
}
|
|
571
776
|
/**
|
|
572
777
|
* Broadcast bytes (UInt8Arrays) to a particular room
|
|
573
778
|
*/
|
|
574
779
|
broadcastBytes(type, message, options) {
|
|
575
|
-
if (options && options.afterNextPatch) {
|
|
576
|
-
delete options.afterNextPatch;
|
|
577
|
-
this._afterNextPatchQueue.push(["broadcastBytes", arguments]);
|
|
578
|
-
return;
|
|
579
|
-
}
|
|
580
780
|
this.broadcastMessageType(type, message, options);
|
|
581
781
|
}
|
|
582
782
|
/**
|
|
@@ -592,33 +792,118 @@ var Room = class _Room {
|
|
|
592
792
|
if (!this.state) {
|
|
593
793
|
return false;
|
|
594
794
|
}
|
|
595
|
-
const
|
|
596
|
-
this.
|
|
795
|
+
const sNow = this.clock.elapsedTime;
|
|
796
|
+
const hasChanges = this._serializer.applyPatches(
|
|
797
|
+
this.clients,
|
|
798
|
+
this.state,
|
|
799
|
+
this._inputController !== void 0 ? { sNow } : void 0
|
|
800
|
+
);
|
|
801
|
+
this._flushPendingClientFrames();
|
|
802
|
+
this._flushAfterPatchBroadcasts();
|
|
803
|
+
const rw = this.#rewind;
|
|
804
|
+
if (rw !== void 0 && rw.lastRecordedAt !== sNow) {
|
|
805
|
+
rw.record(sNow, this.#_patchRate || void 0);
|
|
806
|
+
}
|
|
597
807
|
return hasChanges;
|
|
598
808
|
}
|
|
599
809
|
onMessage(_messageType, _validationSchema, _callback) {
|
|
600
|
-
|
|
601
|
-
const validationSchema = typeof _callback === "function" ? _validationSchema : void 0;
|
|
602
|
-
const callback = validationSchema === void 0 ? _validationSchema : _callback;
|
|
603
|
-
const removeListener = this.onMessageEvents.on(messageType, this.onUncaughtException !== void 0 ? wrapTryCatch(callback, this.onUncaughtException.bind(this), OnMessageException, "onMessage", false, _messageType) : callback);
|
|
604
|
-
if (validationSchema !== void 0) {
|
|
605
|
-
this.onMessageValidators[messageType] = validationSchema;
|
|
606
|
-
}
|
|
607
|
-
return () => {
|
|
608
|
-
removeListener();
|
|
609
|
-
if (this.onMessageEvents.events[messageType].length === 0) {
|
|
610
|
-
delete this.onMessageValidators[messageType];
|
|
611
|
-
}
|
|
612
|
-
};
|
|
810
|
+
return this.#_messages.on(_messageType, _validationSchema, _callback);
|
|
613
811
|
}
|
|
614
812
|
onMessageBytes(_messageType, _validationSchema, _callback) {
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
813
|
+
return this.#_messages.on(`_$b${_messageType}`, _validationSchema, _callback);
|
|
814
|
+
}
|
|
815
|
+
// ---------------------------------------------------------------------------
|
|
816
|
+
// Operator API — used by @colyseus/admin (and monitor in due course)
|
|
817
|
+
// through `remoteRoomCall(roomId, methodName)`. Marked `@internal` because
|
|
818
|
+
// they're framework-tooling primitives, not part of the game-code surface.
|
|
819
|
+
// ---------------------------------------------------------------------------
|
|
820
|
+
/**
|
|
821
|
+
* Snapshot the room's live state for an inspector / admin UI. Includes:
|
|
822
|
+
*
|
|
823
|
+
* roomId, name, maxClients, locked, elapsedTime (ms),
|
|
824
|
+
* metadata, clients (sessionId + per-client elapsed + userId when set),
|
|
825
|
+
* state (the schema/json the SDK would see),
|
|
826
|
+
* stateSize (bytes of the encoded full state, or 0 when no serializer).
|
|
827
|
+
*
|
|
828
|
+
* The payload is intentionally plain JSON — `remoteRoomCall` serializes
|
|
829
|
+
* the return value across process boundaries.
|
|
830
|
+
*
|
|
831
|
+
* @internal Operator-only. Game code should not call this.
|
|
832
|
+
*/
|
|
833
|
+
getInspectorView() {
|
|
834
|
+
const elapsed = this.clock.elapsedTime;
|
|
835
|
+
return {
|
|
836
|
+
roomId: this.roomId,
|
|
837
|
+
name: this.roomName,
|
|
838
|
+
clients: this.clients.length,
|
|
839
|
+
maxClients: this.maxClients,
|
|
840
|
+
locked: this.#_locked,
|
|
841
|
+
elapsedTime: elapsed,
|
|
842
|
+
metadata: this.metadata ?? null,
|
|
843
|
+
// Cast through `unknown`: `ExtractRoomClient<T>` vs `Client & ClientPrivate`
|
|
844
|
+
// don't structurally overlap (the user's `client` generic may carry a
|
|
845
|
+
// narrower userData/auth shape), but the runtime objects we walk here
|
|
846
|
+
// always have the private join-time field. The narrow per-property
|
|
847
|
+
// `(c as any)` reads below keep the cast scoped.
|
|
848
|
+
//
|
|
849
|
+
// userId / userEmail read straight off `client.auth` — the JWT
|
|
850
|
+
// payload @colyseus/auth's default onAuth decodes carries both
|
|
851
|
+
// when the user has them on file. Saves the admin a per-client
|
|
852
|
+
// database round-trip; falls back to null when the client signed
|
|
853
|
+
// in anonymously (or with a custom onAuth that returns a shape
|
|
854
|
+
// without those fields).
|
|
855
|
+
clientList: this.clients.map((c) => {
|
|
856
|
+
const auth = c.auth;
|
|
857
|
+
return {
|
|
858
|
+
sessionId: c.sessionId,
|
|
859
|
+
userId: c.userId ?? auth?.id ?? null,
|
|
860
|
+
userEmail: auth?.email ?? null,
|
|
861
|
+
elapsedTime: elapsed - (c._joinedAt ?? elapsed)
|
|
862
|
+
};
|
|
863
|
+
}),
|
|
864
|
+
state: this.state ?? null,
|
|
865
|
+
stateSize: this.#_inspectorStateSize()
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Force-disconnect a single client by sessionId. No-op when the client
|
|
870
|
+
* isn't connected (idempotent — the caller doesn't need to race-check).
|
|
871
|
+
*
|
|
872
|
+
* @internal Operator-only. Game code disconnects clients by calling
|
|
873
|
+
* `.leave()` on the Client object directly.
|
|
874
|
+
*/
|
|
875
|
+
kickClient(sessionId, closeCode = CloseCode.CONSENTED, reason) {
|
|
876
|
+
for (const client of this.clients) {
|
|
877
|
+
if (client.sessionId === sessionId) {
|
|
878
|
+
this.#_forciblyCloseClient(client, closeCode, reason);
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Best-effort byte size of the current full state. Falls back to `0`
|
|
885
|
+
* when the room has no serializer or the serializer can't produce a
|
|
886
|
+
* payload (raw rooms, very-early-onCreate, etc.). The serializer
|
|
887
|
+
* detection mirrors `@colyseus/monitor`'s — we read whichever buffer
|
|
888
|
+
* is available across schema v2 and v3.
|
|
889
|
+
*/
|
|
890
|
+
#_inspectorStateSize() {
|
|
891
|
+
const ser = this._serializer;
|
|
892
|
+
if (!ser) {
|
|
893
|
+
return 0;
|
|
894
|
+
}
|
|
895
|
+
const hasState = ser.encoder || ser.state;
|
|
896
|
+
if (!hasState) {
|
|
897
|
+
return 0;
|
|
898
|
+
}
|
|
899
|
+
try {
|
|
900
|
+
const full = ser.getFullState?.();
|
|
901
|
+
if (!full) {
|
|
902
|
+
return 0;
|
|
903
|
+
}
|
|
904
|
+
return full.byteLength ?? full.length ?? 0;
|
|
905
|
+
} catch {
|
|
906
|
+
return 0;
|
|
622
907
|
}
|
|
623
908
|
}
|
|
624
909
|
/**
|
|
@@ -658,6 +943,7 @@ var Room = class _Room {
|
|
|
658
943
|
async _onJoin(client, authContext, connectionOptions) {
|
|
659
944
|
const sessionId = client.sessionId;
|
|
660
945
|
client.reconnectionToken = generateId();
|
|
946
|
+
this._inputController?.allocate(client);
|
|
661
947
|
if (this._reservedSeatTimeouts[sessionId]) {
|
|
662
948
|
clearTimeout(this._reservedSeatTimeouts[sessionId]);
|
|
663
949
|
delete this._reservedSeatTimeouts[sessionId];
|
|
@@ -666,7 +952,7 @@ var Room = class _Room {
|
|
|
666
952
|
clearTimeout(this._autoDisposeTimeout);
|
|
667
953
|
this._autoDisposeTimeout = void 0;
|
|
668
954
|
}
|
|
669
|
-
if (this._reservedSeats[sessionId] === void 0 && connectionOptions?.reconnectionToken && this.clients.
|
|
955
|
+
if (this._reservedSeats[sessionId] === void 0 && connectionOptions?.reconnectionToken && this.clients.get(sessionId)?.reconnectionToken === connectionOptions.reconnectionToken) {
|
|
670
956
|
debugMatchMaking("attempting to reconnect client with a stale previous connection - sessionId: '%s', roomId: '%s'", client.sessionId, this.roomId);
|
|
671
957
|
this._reconnectionAttempts[connectionOptions.reconnectionToken] = new Deferred();
|
|
672
958
|
const reconnectionAttemptTimeout = setTimeout(() => {
|
|
@@ -687,13 +973,14 @@ var Room = class _Room {
|
|
|
687
973
|
}
|
|
688
974
|
this._reservedSeats[sessionId][2] = true;
|
|
689
975
|
debugMatchMaking("consuming seat reservation, sessionId: '%s' (roomId: %s)", client.sessionId, this.roomId);
|
|
690
|
-
client.
|
|
976
|
+
client._pendingFrameClients = this.#pendingFrameClients;
|
|
691
977
|
client.ref["onleave"] = (_) => client.state = ClientState.LEAVING;
|
|
692
978
|
client.ref.once("close", client.ref["onleave"]);
|
|
693
979
|
if (isWaitingReconnection) {
|
|
694
980
|
const reconnectionToken = connectionOptions?.reconnectionToken;
|
|
695
981
|
if (reconnectionToken && this._reconnections[reconnectionToken]?.[0] === sessionId) {
|
|
696
982
|
this.clients.push(client);
|
|
983
|
+
this._inputController?.register(sessionId, client);
|
|
697
984
|
await this._reconnections[reconnectionToken]?.[1].resolve(client);
|
|
698
985
|
try {
|
|
699
986
|
if (this.onReconnect) {
|
|
@@ -733,6 +1020,7 @@ var Room = class _Room {
|
|
|
733
1020
|
throw new ServerError(CloseCode.WITH_ERROR, "already disconnected");
|
|
734
1021
|
}
|
|
735
1022
|
this.clients.push(client);
|
|
1023
|
+
this._inputController?.register(sessionId, client);
|
|
736
1024
|
Object.defineProperty(this._reservedSeats, sessionId, {
|
|
737
1025
|
value: this._reservedSeats[sessionId],
|
|
738
1026
|
enumerable: false
|
|
@@ -760,6 +1048,7 @@ var Room = class _Room {
|
|
|
760
1048
|
client.ref["onleave"] = this._onLeave.bind(this, client);
|
|
761
1049
|
client.ref.once("close", client.ref["onleave"]);
|
|
762
1050
|
client.ref.on("message", this._onMessage.bind(this, client));
|
|
1051
|
+
const extraSections = this._inputController?.handshakeSections();
|
|
763
1052
|
client.raw(getMessageBytes[Protocol.JOIN_ROOM](
|
|
764
1053
|
client.reconnectionToken,
|
|
765
1054
|
this._serializer.id,
|
|
@@ -767,7 +1056,8 @@ var Room = class _Room {
|
|
|
767
1056
|
* if skipHandshake is true, we don't need to send the handshake
|
|
768
1057
|
* (in case client already has handshake data)
|
|
769
1058
|
*/
|
|
770
|
-
connectionOptions?.skipHandshake ? void 0 : this._serializer.handshake && this._serializer.handshake()
|
|
1059
|
+
connectionOptions?.skipHandshake ? void 0 : this._serializer.handshake && this._serializer.handshake(),
|
|
1060
|
+
extraSections
|
|
771
1061
|
));
|
|
772
1062
|
}
|
|
773
1063
|
}
|
|
@@ -834,7 +1124,7 @@ var Room = class _Room {
|
|
|
834
1124
|
}).finally(() => {
|
|
835
1125
|
cleanup();
|
|
836
1126
|
});
|
|
837
|
-
if (this._reconnectionAttempts[reconnectionToken]) {
|
|
1127
|
+
if (this._reconnectionAttempts[reconnectionToken] !== void 0) {
|
|
838
1128
|
debugMatchMaking("resolving reconnection attempt for client - sessionId: '%s', roomId: '%s'", sessionId, this.roomId);
|
|
839
1129
|
this._reconnectionAttempts[reconnectionToken].resolve(true);
|
|
840
1130
|
}
|
|
@@ -854,30 +1144,64 @@ var Room = class _Room {
|
|
|
854
1144
|
debugMessage("broadcast: %O (roomId: %s)", message, this.roomId);
|
|
855
1145
|
const encodedMessage = message instanceof Uint8Array ? getMessageBytes.raw(Protocol.ROOM_DATA_BYTES, type, void 0, message) : getMessageBytes.raw(Protocol.ROOM_DATA, type, message);
|
|
856
1146
|
const except = typeof options.except !== "undefined" ? Array.isArray(options.except) ? options.except : [options.except] : void 0;
|
|
1147
|
+
if (options.afterNextPatch && this._inputController === void 0) {
|
|
1148
|
+
this.#afterPatchBroadcasts.push({ bytes: encodedMessage, except });
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
const sendOpts = options.afterNextPatch ? AFTER_PATCH_OPTS : void 0;
|
|
857
1152
|
let numClients = this.clients.length;
|
|
858
1153
|
while (numClients--) {
|
|
859
1154
|
const client = this.clients[numClients];
|
|
860
1155
|
if (!except || !except.includes(client)) {
|
|
861
|
-
client.enqueueRaw(encodedMessage);
|
|
1156
|
+
client.enqueueRaw(encodedMessage, sendOpts);
|
|
862
1157
|
}
|
|
863
1158
|
}
|
|
864
1159
|
}
|
|
1160
|
+
/** Drain `#afterPatchBroadcasts` right after the patch — each as ONE shared
|
|
1161
|
+
* buffer fanned out to its (non-excepted) recipients. `enqueueRaw` handles
|
|
1162
|
+
* JOINED (send now) vs still-joining (buffer until JOIN) per client. */
|
|
1163
|
+
_flushAfterPatchBroadcasts() {
|
|
1164
|
+
const queued = this.#afterPatchBroadcasts;
|
|
1165
|
+
if (queued.length === 0) {
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
for (let i = 0; i < queued.length; i++) {
|
|
1169
|
+
const { bytes, except } = queued[i];
|
|
1170
|
+
let numClients = this.clients.length;
|
|
1171
|
+
while (numClients--) {
|
|
1172
|
+
const client = this.clients[numClients];
|
|
1173
|
+
if (!except || !except.includes(client)) {
|
|
1174
|
+
client.enqueueRaw(bytes);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
queued.length = 0;
|
|
1179
|
+
}
|
|
865
1180
|
sendFullState(client) {
|
|
866
|
-
client.raw(this._serializer.getFullState(
|
|
1181
|
+
client.raw(this._serializer.getFullState(
|
|
1182
|
+
client,
|
|
1183
|
+
this._inputController !== void 0 ? { sNow: this.clock.elapsedTime } : void 0
|
|
1184
|
+
));
|
|
867
1185
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1186
|
+
/** Send each client's staged `afterNextPatch` frames as standalone frames right
|
|
1187
|
+
* after the patch, then reset the tracker. Iterates only the clients that staged
|
|
1188
|
+
* frames this cycle (`#pendingFrameClients`), never the full client list; empty
|
|
1189
|
+
* (→ O(1)) on a tick where nothing was staged. */
|
|
1190
|
+
_flushPendingClientFrames() {
|
|
1191
|
+
const dirty = this.#pendingFrameClients;
|
|
1192
|
+
if (dirty.length === 0) {
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
for (let i = 0; i < dirty.length; i++) {
|
|
1196
|
+
const frames = dirty[i]._pendingFrames;
|
|
1197
|
+
if (frames !== void 0 && frames.length > 0) {
|
|
1198
|
+
for (let j = 0; j < frames.length; j++) {
|
|
1199
|
+
dirty[i].raw(frames[j]);
|
|
877
1200
|
}
|
|
1201
|
+
frames.length = 0;
|
|
878
1202
|
}
|
|
879
|
-
this._afterNextPatchQueue.splice(0, length);
|
|
880
1203
|
}
|
|
1204
|
+
dirty.length = 0;
|
|
881
1205
|
}
|
|
882
1206
|
async _reserveSeat(sessionId, joinOptions = true, authData = void 0, seconds = this.seatReservationTimeout, allowReconnection = false, devModeReconnectionToken) {
|
|
883
1207
|
if (!allowReconnection && this.hasReachedMaxClients()) {
|
|
@@ -911,6 +1235,7 @@ var Room = class _Room {
|
|
|
911
1235
|
delete this._reconnections[devModeReconnectionToken];
|
|
912
1236
|
delete this._reservedSeats[sessionId];
|
|
913
1237
|
delete this._reservedSeatTimeouts[sessionId];
|
|
1238
|
+
this._inputController?.release(sessionId);
|
|
914
1239
|
if (!allowReconnection) {
|
|
915
1240
|
await this.#_decrementClientCount();
|
|
916
1241
|
}
|
|
@@ -952,11 +1277,12 @@ var Room = class _Room {
|
|
|
952
1277
|
this._simulationInterval = void 0;
|
|
953
1278
|
}
|
|
954
1279
|
if (this._autoDisposeTimeout) {
|
|
955
|
-
|
|
1280
|
+
clearTimeout(this._autoDisposeTimeout);
|
|
956
1281
|
this._autoDisposeTimeout = void 0;
|
|
957
1282
|
}
|
|
958
1283
|
this.clock.clear();
|
|
959
1284
|
this.clock.stop();
|
|
1285
|
+
this._inputController?.dispose();
|
|
960
1286
|
return await (userReturnData || Promise.resolve());
|
|
961
1287
|
}
|
|
962
1288
|
_onMessage(client, buffer) {
|
|
@@ -975,49 +1301,18 @@ var Room = class _Room {
|
|
|
975
1301
|
return this.#_forciblyCloseClient(client, CloseCode.WITH_ERROR);
|
|
976
1302
|
}
|
|
977
1303
|
const it = { offset: 1 };
|
|
978
|
-
const code = buffer[0];
|
|
1304
|
+
const code = buffer[0] & PROTOCOL_CODE_MASK;
|
|
1305
|
+
const modifiers = buffer[0] & PROTOCOL_MODIFIER_MASK;
|
|
979
1306
|
if (code === Protocol.ROOM_DATA) {
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
message = buffer.byteLength > it.offset ? unpack(buffer.subarray(it.offset, buffer.byteLength)) : void 0;
|
|
984
|
-
debugMessage("received: '%s' -> %j (roomId: %s)", messageType, message, this.roomId);
|
|
985
|
-
if (this.onMessageValidators[messageType] !== void 0) {
|
|
986
|
-
message = standardValidate(this.onMessageValidators[messageType], message);
|
|
987
|
-
}
|
|
988
|
-
} catch (e) {
|
|
989
|
-
debugAndPrintError(e);
|
|
990
|
-
client.leave(CloseCode.WITH_ERROR);
|
|
991
|
-
return;
|
|
992
|
-
}
|
|
993
|
-
if (this.onMessageEvents.events[messageType]) {
|
|
994
|
-
this.onMessageEvents.emit(messageType, client, message);
|
|
995
|
-
} else if (this.onMessageEvents.events["*"]) {
|
|
996
|
-
this.onMessageEvents.emit("*", client, messageType, message);
|
|
997
|
-
} else {
|
|
998
|
-
this.onMessageFallbacks["__no_message_handler"](client, messageType, message);
|
|
999
|
-
}
|
|
1307
|
+
this.#_messages.onData(client, buffer, it);
|
|
1308
|
+
} else if (code === Protocol.ROOM_REQUEST) {
|
|
1309
|
+
this.#_messages.onRequest(client, buffer, it);
|
|
1000
1310
|
} else if (code === Protocol.ROOM_DATA_BYTES) {
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
if (this.onMessageValidators[bytesMessageType] !== void 0) {
|
|
1007
|
-
message = standardValidate(this.onMessageValidators[bytesMessageType], message);
|
|
1008
|
-
}
|
|
1009
|
-
} catch (e) {
|
|
1010
|
-
debugAndPrintError(e);
|
|
1011
|
-
client.leave(CloseCode.WITH_ERROR);
|
|
1012
|
-
return;
|
|
1013
|
-
}
|
|
1014
|
-
if (this.onMessageEvents.events[bytesMessageType]) {
|
|
1015
|
-
this.onMessageEvents.emit(bytesMessageType, client, message);
|
|
1016
|
-
} else if (this.onMessageEvents.events["*"]) {
|
|
1017
|
-
this.onMessageEvents.emit("*", client, messageType, message);
|
|
1018
|
-
} else {
|
|
1019
|
-
this.onMessageFallbacks["__no_message_handler"](client, messageType, message);
|
|
1020
|
-
}
|
|
1311
|
+
this.#_messages.onDataBytes(client, buffer, it);
|
|
1312
|
+
} else if (code === Protocol.ROOM_INPUT_RELIABLE) {
|
|
1313
|
+
this._inputController?.decodeReliable(client, buffer, it, modifiers);
|
|
1314
|
+
} else if (code === Protocol.ROOM_INPUT_UNRELIABLE) {
|
|
1315
|
+
this._inputController?.decodeUnreliable(client, buffer, modifiers);
|
|
1021
1316
|
} else if (code === Protocol.JOIN_ROOM && client.state === ClientState.JOINING) {
|
|
1022
1317
|
client.state = ClientState.JOINED;
|
|
1023
1318
|
client._joinedAt = this.clock.elapsedTime;
|
|
@@ -1034,10 +1329,10 @@ var Room = class _Room {
|
|
|
1034
1329
|
this.#_forciblyCloseClient(client, CloseCode.CONSENTED);
|
|
1035
1330
|
}
|
|
1036
1331
|
}
|
|
1037
|
-
#_forciblyCloseClient(client, closeCode) {
|
|
1332
|
+
#_forciblyCloseClient(client, closeCode, reason) {
|
|
1038
1333
|
client.ref.removeAllListeners("message");
|
|
1039
1334
|
client.ref.removeListener("close", client.ref["onleave"]);
|
|
1040
|
-
this._onLeave(client, closeCode).then(() => client.leave(closeCode));
|
|
1335
|
+
this._onLeave(client, closeCode).then(() => client.leave(closeCode, reason));
|
|
1041
1336
|
}
|
|
1042
1337
|
async _onLeave(client, code) {
|
|
1043
1338
|
const method = code === CloseCode.CONSENTED || client.state === ClientState.RECONNECTING ? this.onLeave : this.onDrop || this.onLeave;
|
|
@@ -1045,6 +1340,7 @@ var Room = class _Room {
|
|
|
1045
1340
|
if (!this.clients.delete(client)) {
|
|
1046
1341
|
return;
|
|
1047
1342
|
}
|
|
1343
|
+
this._inputController?.freeze(client);
|
|
1048
1344
|
if (method) {
|
|
1049
1345
|
debugMatchMaking(`${method.name}, sessionId: '%s' (close code: %d, roomId: %s)`, client.sessionId, code, this.roomId);
|
|
1050
1346
|
try {
|
|
@@ -1071,6 +1367,7 @@ var Room = class _Room {
|
|
|
1071
1367
|
}
|
|
1072
1368
|
const willDispose = await this.#_decrementClientCount();
|
|
1073
1369
|
if (this._reservedSeats[client.sessionId] === void 0) {
|
|
1370
|
+
this._inputController?.release(client.sessionId);
|
|
1074
1371
|
this._events.emit("leave", client, willDispose);
|
|
1075
1372
|
}
|
|
1076
1373
|
}
|
|
@@ -1134,27 +1431,12 @@ var Room = class _Room {
|
|
|
1134
1431
|
}
|
|
1135
1432
|
}
|
|
1136
1433
|
};
|
|
1137
|
-
function room(options) {
|
|
1138
|
-
class _ extends Room {
|
|
1139
|
-
constructor() {
|
|
1140
|
-
super();
|
|
1141
|
-
this.messages = options.messages;
|
|
1142
|
-
if (options.state && typeof options.state === "function") {
|
|
1143
|
-
this.state = options.state();
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
for (const key in options) {
|
|
1148
|
-
if (typeof options[key] === "function") {
|
|
1149
|
-
_.prototype[key] = options[key];
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
return _;
|
|
1153
|
-
}
|
|
1154
1434
|
export {
|
|
1155
1435
|
DEFAULT_SEAT_RESERVATION_TIME,
|
|
1156
1436
|
Room,
|
|
1157
1437
|
RoomInternalState,
|
|
1158
|
-
|
|
1438
|
+
RoomPlugin2 as RoomPlugin,
|
|
1439
|
+
attachToTestRoom,
|
|
1440
|
+
definePlugins,
|
|
1159
1441
|
validate
|
|
1160
1442
|
};
|