@colyseus/core 0.18.0 → 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 +3 -7
- package/build/MatchMaker.cjs.map +2 -2
- package/build/MatchMaker.mjs +4 -8
- package/build/MatchMaker.mjs.map +2 -2
- package/build/Protocol.cjs +3 -3
- package/build/Protocol.cjs.map +1 -1
- package/build/Protocol.d.ts +3 -3
- package/build/Protocol.mjs +3 -3
- package/build/Protocol.mjs.map +1 -1
- 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 +280 -281
- package/build/Room.cjs.map +3 -3
- package/build/Room.d.ts +185 -81
- package/build/Room.mjs +286 -286
- package/build/Room.mjs.map +2 -2
- 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/Server.cjs +88 -31
- package/build/Server.cjs.map +2 -2
- package/build/Server.d.ts +36 -0
- package/build/Server.mjs +88 -31
- package/build/Server.mjs.map +2 -2
- package/build/Transport.cjs +26 -0
- package/build/Transport.cjs.map +2 -2
- package/build/Transport.d.ts +69 -2
- package/build/Transport.mjs +25 -0
- 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 +19 -2
- package/build/index.cjs.map +2 -2
- package/build/index.d.ts +7 -4
- package/build/index.mjs +17 -4
- package/build/index.mjs.map +2 -2
- package/build/input/InputBuffer.cjs +439 -16
- package/build/input/InputBuffer.cjs.map +2 -2
- package/build/input/InputBuffer.d.ts +186 -96
- package/build/input/InputBuffer.mjs +436 -15
- package/build/input/InputBuffer.mjs.map +2 -2
- 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/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/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 +6 -1
- package/build/router/index.cjs.map +2 -2
- package/build/router/index.mjs +6 -1
- package/build/router/index.mjs.map +2 -2
- package/build/router/node.cjs +37 -2
- package/build/router/node.cjs.map +2 -2
- package/build/router/node.d.ts +10 -0
- package/build/router/node.mjs +35 -1
- package/build/router/node.mjs.map +2 -2
- 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/Env.cjs +8 -4
- package/build/utils/Env.cjs.map +3 -3
- package/build/utils/Env.mjs +8 -4
- package/build/utils/Env.mjs.map +2 -2
- 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 +11 -11
- package/src/MatchMaker.ts +7 -9
- package/src/Protocol.ts +3 -3
- package/src/Rewind.ts +572 -0
- package/src/Room.ts +409 -448
- package/src/RoomMessages.ts +342 -0
- package/src/Server.ts +112 -38
- package/src/Transport.ts +97 -2
- package/src/errors/RoomExceptions.ts +18 -4
- package/src/index.ts +13 -2
- package/src/input/InputBuffer.ts +464 -107
- package/src/input/RoomInput.ts +337 -0
- package/src/input/types.ts +503 -0
- package/src/rooms/QueueRoom.ts +1 -1
- package/src/router/default_routes.ts +6 -1
- package/src/router/index.ts +6 -1
- package/src/router/node.ts +44 -0
- 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/Env.ts +12 -4
- package/src/utils/Utils.ts +9 -0
package/build/Room.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// packages/core/src/Room.ts
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {} from "./
|
|
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";
|
|
7
8
|
import { ClockTimer as Clock } from "@colyseus/timer";
|
|
8
9
|
import { EventEmitter } from "events";
|
|
9
10
|
import { logger } from "./Logger.mjs";
|
|
@@ -11,20 +12,19 @@ import { NoneSerializer } from "./serializer/NoneSerializer.mjs";
|
|
|
11
12
|
import { SchemaSerializer } from "./serializer/SchemaSerializer.mjs";
|
|
12
13
|
import { getMessageBytes } from "./Protocol.mjs";
|
|
13
14
|
import { Deferred, generateId, wrapTryCatch } from "./utils/Utils.mjs";
|
|
14
|
-
import { createNanoEvents } from "./utils/nanoevents.mjs";
|
|
15
15
|
import { isDevMode } from "./utils/DevMode.mjs";
|
|
16
16
|
import { debugAndPrintError, debugMatchMaking, debugMessage } from "./Debug.mjs";
|
|
17
17
|
import { ServerError } from "./errors/ServerError.mjs";
|
|
18
18
|
import { ClientState, ClientArray } from "./Transport.mjs";
|
|
19
|
-
import { OnAuthException, OnCreateException, OnDisposeException, OnDropException, OnJoinException, OnLeaveException,
|
|
20
|
-
import
|
|
19
|
+
import { OnAuthException, OnCreateException, OnDisposeException, OnDropException, OnJoinException, OnLeaveException, OnReconnectException, TimestepException, TimedEventException } from "./errors/RoomExceptions.mjs";
|
|
20
|
+
import "./utils/StandardSchema.mjs";
|
|
21
21
|
import * as matchMaker from "./MatchMaker.mjs";
|
|
22
22
|
import {
|
|
23
23
|
CloseCode,
|
|
24
24
|
ErrorCode,
|
|
25
|
-
HandshakeSection,
|
|
26
25
|
Protocol,
|
|
27
|
-
|
|
26
|
+
PROTOCOL_CODE_MASK,
|
|
27
|
+
PROTOCOL_MODIFIER_MASK
|
|
28
28
|
} from "@colyseus/shared-types";
|
|
29
29
|
import {
|
|
30
30
|
setupRoomPlugins
|
|
@@ -34,17 +34,10 @@ import {
|
|
|
34
34
|
definePlugins,
|
|
35
35
|
attachToTestRoom
|
|
36
36
|
} from "./RoomPlugin.mjs";
|
|
37
|
-
var _inputReflectionCache = /* @__PURE__ */ new WeakMap();
|
|
38
37
|
var DEFAULT_PATCH_RATE = 1e3 / 20;
|
|
39
38
|
var DEFAULT_SIMULATION_INTERVAL = 1e3 / 60;
|
|
40
39
|
var noneSerializer = new NoneSerializer();
|
|
41
|
-
|
|
42
|
-
if (e instanceof Error) {
|
|
43
|
-
const code = e.code;
|
|
44
|
-
return code !== void 0 ? { name: e.name, message: e.message, code } : { name: e.name, message: e.message };
|
|
45
|
-
}
|
|
46
|
-
return { name: "Error", message: String(e) };
|
|
47
|
-
}
|
|
40
|
+
var AFTER_PATCH_OPTS = Object.freeze({ afterNextPatch: true });
|
|
48
41
|
var DEFAULT_SEAT_RESERVATION_TIME = Number(process.env.COLYSEUS_SEAT_RESERVATION_TIME || 15);
|
|
49
42
|
function validate(format, handler) {
|
|
50
43
|
return { format, handler };
|
|
@@ -109,25 +102,22 @@ var Room = class _Room {
|
|
|
109
102
|
this._reservedSeatTimeouts = {};
|
|
110
103
|
this._reconnections = {};
|
|
111
104
|
this._reconnectionAttempts = {};
|
|
112
|
-
|
|
113
|
-
this
|
|
114
|
-
this.onMessageFallbacks = {
|
|
115
|
-
"__no_message_handler": (client, messageType, _) => {
|
|
116
|
-
const errorMessage = `room onMessage for "${messageType}" not registered.`;
|
|
117
|
-
debugMessage(`${errorMessage} (roomId: ${this.roomId})`);
|
|
118
|
-
if (isDevMode) {
|
|
119
|
-
client.error(ErrorCode.INVALID_PAYLOAD, errorMessage);
|
|
120
|
-
} else {
|
|
121
|
-
client.leave(CloseCode.WITH_ERROR, errorMessage);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
};
|
|
105
|
+
/** @internal Message-routing layer: handler registry + user-message decode/dispatch. */
|
|
106
|
+
this.#_messages = new RoomMessages(this);
|
|
125
107
|
this._serializer = noneSerializer;
|
|
126
|
-
|
|
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 = [];
|
|
127
118
|
this._internalState = RoomInternalState.CREATING;
|
|
128
119
|
this._lockedExplicitly = false;
|
|
129
120
|
this.#_locked = false;
|
|
130
|
-
this.#_tick = 0;
|
|
131
121
|
this._events.once("dispose", () => {
|
|
132
122
|
this.#_dispose().catch((e) => debugAndPrintError(`onDispose error: ${e && e.stack || e.message || e || "promise rejected"} (roomId: ${this.roomId})`)).finally(() => this._events.emit("disconnect"));
|
|
133
123
|
});
|
|
@@ -180,6 +170,17 @@ var Room = class _Room {
|
|
|
180
170
|
#_patchRate;
|
|
181
171
|
#_patchInterval;
|
|
182
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;
|
|
183
184
|
#_locked;
|
|
184
185
|
/**
|
|
185
186
|
* This method is called by the MatchMaker before onCreate()
|
|
@@ -305,41 +306,41 @@ var Room = class _Room {
|
|
|
305
306
|
}
|
|
306
307
|
/**
|
|
307
308
|
* Declare the input schema and configuration in a single line. Returns the
|
|
308
|
-
*
|
|
309
|
-
* `this.
|
|
309
|
+
* input API that gets assigned to `this.inputs` — call
|
|
310
|
+
* `this.inputs.get(sessionId)` per tick to consume.
|
|
310
311
|
*
|
|
311
312
|
* ```typescript
|
|
312
313
|
* class FpsRoom extends Room<{ input: MoveInput }> {
|
|
313
|
-
*
|
|
314
|
+
* inputs = this.defineInput(MoveInput, {
|
|
314
315
|
* seqField: "tick", // typed: only numeric fields of MoveInput
|
|
315
316
|
* bufferMaxSize: 64,
|
|
316
317
|
* });
|
|
317
318
|
*
|
|
318
|
-
* // …or without options —
|
|
319
|
-
* //
|
|
319
|
+
* // …or without options — no seq dedupe, bufferMaxSize: 32:
|
|
320
|
+
* // inputs = this.defineInput(MoveInput);
|
|
320
321
|
* }
|
|
321
322
|
* ```
|
|
322
323
|
*
|
|
323
324
|
* **Defaults** when `opts` (or individual fields) are omitted:
|
|
324
|
-
* - `seqField`:
|
|
325
|
-
*
|
|
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.
|
|
326
329
|
* - `bufferMaxSize`: `32` — enables per-client snapshot buffering for
|
|
327
|
-
* `
|
|
328
|
-
* buffering (the `.latest`
|
|
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.
|
|
329
340
|
*/
|
|
330
341
|
defineInput(type, opts) {
|
|
331
|
-
this.
|
|
332
|
-
|
|
333
|
-
seqField: opts?.seqField ?? "seq",
|
|
334
|
-
bufferMaxSize: opts?.bufferMaxSize ?? 32
|
|
335
|
-
};
|
|
336
|
-
if (!_inputReflectionCache.has(type)) {
|
|
337
|
-
_inputReflectionCache.set(type, Reflection.encode(new Encoder(new type())));
|
|
338
|
-
}
|
|
339
|
-
return ((sessionId) => {
|
|
340
|
-
const c = this.clients.getById(sessionId);
|
|
341
|
-
return c?._inputAccessor ?? NO_OP_INPUT_ACCESSOR;
|
|
342
|
-
});
|
|
342
|
+
this._inputController ??= new RoomInput(this);
|
|
343
|
+
return this._inputController.define(type, opts);
|
|
343
344
|
}
|
|
344
345
|
/**
|
|
345
346
|
* This method is called before onJoin() - this is where you should authenticate the client
|
|
@@ -400,7 +401,7 @@ var Room = class _Room {
|
|
|
400
401
|
reservedSeat[3] && this._reconnections[reconnectionToken]?.[0] === sessionId
|
|
401
402
|
);
|
|
402
403
|
} else if (typeof reconnectionToken === "string") {
|
|
403
|
-
return this.clients.
|
|
404
|
+
return this.clients.get(sessionId)?.reconnectionToken === reconnectionToken;
|
|
404
405
|
}
|
|
405
406
|
return false;
|
|
406
407
|
}
|
|
@@ -418,22 +419,27 @@ var Room = class _Room {
|
|
|
418
419
|
return void 0;
|
|
419
420
|
}
|
|
420
421
|
/**
|
|
421
|
-
*
|
|
422
|
-
*
|
|
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.
|
|
423
426
|
*
|
|
424
427
|
* @default 16.6ms (60fps)
|
|
425
428
|
*
|
|
426
|
-
* @param onTickCallback -
|
|
427
|
-
*
|
|
428
|
-
* @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.
|
|
429
432
|
*/
|
|
430
|
-
|
|
433
|
+
setTimestep(onTickCallback, delay = DEFAULT_SIMULATION_INTERVAL) {
|
|
431
434
|
if (this._simulationInterval) {
|
|
432
435
|
clearInterval(this._simulationInterval);
|
|
433
436
|
}
|
|
437
|
+
if (onTickCallback && this._inputController && this._inputController.options.tickRate === void 0) {
|
|
438
|
+
this._inputController.options.tickRate = Math.round(1e3 / delay);
|
|
439
|
+
}
|
|
434
440
|
if (onTickCallback) {
|
|
435
441
|
if (this.onUncaughtException !== void 0) {
|
|
436
|
-
onTickCallback = wrapTryCatch(onTickCallback, this.onUncaughtException.bind(this),
|
|
442
|
+
onTickCallback = wrapTryCatch(onTickCallback, this.onUncaughtException.bind(this), TimestepException, "setTimestep");
|
|
437
443
|
}
|
|
438
444
|
this._simulationInterval = setInterval(() => {
|
|
439
445
|
this.clock.tick();
|
|
@@ -442,46 +448,157 @@ var Room = class _Room {
|
|
|
442
448
|
}
|
|
443
449
|
}
|
|
444
450
|
/**
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
|
|
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.
|
|
449
466
|
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
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}.
|
|
452
470
|
*
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
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).
|
|
457
482
|
*
|
|
458
|
-
*
|
|
459
|
-
*
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
-
*
|
|
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 });
|
|
467
510
|
* }
|
|
468
511
|
* ```
|
|
469
512
|
*/
|
|
470
|
-
|
|
471
|
-
this
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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();
|
|
476
561
|
}
|
|
477
562
|
/**
|
|
478
|
-
*
|
|
479
|
-
*
|
|
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).
|
|
480
584
|
*/
|
|
481
|
-
|
|
482
|
-
|
|
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;
|
|
483
601
|
}
|
|
484
|
-
#_tick;
|
|
485
602
|
/**
|
|
486
603
|
* @deprecated Use `.patchRate=` instead.
|
|
487
604
|
*/
|
|
@@ -654,22 +771,12 @@ var Room = class _Room {
|
|
|
654
771
|
*/
|
|
655
772
|
broadcast(type, ...args) {
|
|
656
773
|
const [message, options] = args;
|
|
657
|
-
if (options && options.afterNextPatch) {
|
|
658
|
-
delete options.afterNextPatch;
|
|
659
|
-
this._afterNextPatchQueue.push(["broadcast", [type, ...args]]);
|
|
660
|
-
return;
|
|
661
|
-
}
|
|
662
774
|
this.broadcastMessageType(type, message, options);
|
|
663
775
|
}
|
|
664
776
|
/**
|
|
665
777
|
* Broadcast bytes (UInt8Arrays) to a particular room
|
|
666
778
|
*/
|
|
667
779
|
broadcastBytes(type, message, options) {
|
|
668
|
-
if (options && options.afterNextPatch) {
|
|
669
|
-
delete options.afterNextPatch;
|
|
670
|
-
this._afterNextPatchQueue.push(["broadcastBytes", arguments]);
|
|
671
|
-
return;
|
|
672
|
-
}
|
|
673
780
|
this.broadcastMessageType(type, message, options);
|
|
674
781
|
}
|
|
675
782
|
/**
|
|
@@ -685,34 +792,25 @@ var Room = class _Room {
|
|
|
685
792
|
if (!this.state) {
|
|
686
793
|
return false;
|
|
687
794
|
}
|
|
688
|
-
const
|
|
689
|
-
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
|
+
}
|
|
690
807
|
return hasChanges;
|
|
691
808
|
}
|
|
692
809
|
onMessage(_messageType, _validationSchema, _callback) {
|
|
693
|
-
|
|
694
|
-
const validationSchema = typeof _callback === "function" ? _validationSchema : void 0;
|
|
695
|
-
const callback = validationSchema === void 0 ? _validationSchema : _callback;
|
|
696
|
-
const removeListener = this.onMessageEvents.on(messageType, this.onUncaughtException !== void 0 ? wrapTryCatch(callback, this.onUncaughtException.bind(this), OnMessageException, "onMessage", false, _messageType) : callback);
|
|
697
|
-
if (validationSchema !== void 0) {
|
|
698
|
-
this.onMessageValidators[messageType] = validationSchema;
|
|
699
|
-
}
|
|
700
|
-
return () => {
|
|
701
|
-
removeListener();
|
|
702
|
-
if (this.onMessageEvents.events[messageType].length === 0) {
|
|
703
|
-
delete this.onMessageValidators[messageType];
|
|
704
|
-
}
|
|
705
|
-
};
|
|
810
|
+
return this.#_messages.on(_messageType, _validationSchema, _callback);
|
|
706
811
|
}
|
|
707
812
|
onMessageBytes(_messageType, _validationSchema, _callback) {
|
|
708
|
-
|
|
709
|
-
const validationSchema = typeof _callback === "function" ? _validationSchema : void 0;
|
|
710
|
-
const callback = validationSchema === void 0 ? _validationSchema : _callback;
|
|
711
|
-
if (validationSchema !== void 0) {
|
|
712
|
-
return this.onMessage(messageType, validationSchema, callback);
|
|
713
|
-
} else {
|
|
714
|
-
return this.onMessage(messageType, callback);
|
|
715
|
-
}
|
|
813
|
+
return this.#_messages.on(`_$b${_messageType}`, _validationSchema, _callback);
|
|
716
814
|
}
|
|
717
815
|
// ---------------------------------------------------------------------------
|
|
718
816
|
// Operator API — used by @colyseus/admin (and monitor in due course)
|
|
@@ -845,15 +943,7 @@ var Room = class _Room {
|
|
|
845
943
|
async _onJoin(client, authContext, connectionOptions) {
|
|
846
944
|
const sessionId = client.sessionId;
|
|
847
945
|
client.reconnectionToken = generateId();
|
|
848
|
-
|
|
849
|
-
client._input = new this.inputOptions.ctor();
|
|
850
|
-
client._inputDecoder = new InputDecoder(client._input);
|
|
851
|
-
const maxSize = this.inputOptions.bufferMaxSize;
|
|
852
|
-
if (maxSize > 0) {
|
|
853
|
-
client._inputBuffer = new InputBufferImpl(maxSize, this.inputOptions.seqField);
|
|
854
|
-
}
|
|
855
|
-
client._inputAccessor = new InputAccessorImpl(client);
|
|
856
|
-
}
|
|
946
|
+
this._inputController?.allocate(client);
|
|
857
947
|
if (this._reservedSeatTimeouts[sessionId]) {
|
|
858
948
|
clearTimeout(this._reservedSeatTimeouts[sessionId]);
|
|
859
949
|
delete this._reservedSeatTimeouts[sessionId];
|
|
@@ -862,7 +952,7 @@ var Room = class _Room {
|
|
|
862
952
|
clearTimeout(this._autoDisposeTimeout);
|
|
863
953
|
this._autoDisposeTimeout = void 0;
|
|
864
954
|
}
|
|
865
|
-
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) {
|
|
866
956
|
debugMatchMaking("attempting to reconnect client with a stale previous connection - sessionId: '%s', roomId: '%s'", client.sessionId, this.roomId);
|
|
867
957
|
this._reconnectionAttempts[connectionOptions.reconnectionToken] = new Deferred();
|
|
868
958
|
const reconnectionAttemptTimeout = setTimeout(() => {
|
|
@@ -883,13 +973,14 @@ var Room = class _Room {
|
|
|
883
973
|
}
|
|
884
974
|
this._reservedSeats[sessionId][2] = true;
|
|
885
975
|
debugMatchMaking("consuming seat reservation, sessionId: '%s' (roomId: %s)", client.sessionId, this.roomId);
|
|
886
|
-
client.
|
|
976
|
+
client._pendingFrameClients = this.#pendingFrameClients;
|
|
887
977
|
client.ref["onleave"] = (_) => client.state = ClientState.LEAVING;
|
|
888
978
|
client.ref.once("close", client.ref["onleave"]);
|
|
889
979
|
if (isWaitingReconnection) {
|
|
890
980
|
const reconnectionToken = connectionOptions?.reconnectionToken;
|
|
891
981
|
if (reconnectionToken && this._reconnections[reconnectionToken]?.[0] === sessionId) {
|
|
892
982
|
this.clients.push(client);
|
|
983
|
+
this._inputController?.register(sessionId, client);
|
|
893
984
|
await this._reconnections[reconnectionToken]?.[1].resolve(client);
|
|
894
985
|
try {
|
|
895
986
|
if (this.onReconnect) {
|
|
@@ -929,6 +1020,7 @@ var Room = class _Room {
|
|
|
929
1020
|
throw new ServerError(CloseCode.WITH_ERROR, "already disconnected");
|
|
930
1021
|
}
|
|
931
1022
|
this.clients.push(client);
|
|
1023
|
+
this._inputController?.register(sessionId, client);
|
|
932
1024
|
Object.defineProperty(this._reservedSeats, sessionId, {
|
|
933
1025
|
value: this._reservedSeats[sessionId],
|
|
934
1026
|
enumerable: false
|
|
@@ -956,13 +1048,7 @@ var Room = class _Room {
|
|
|
956
1048
|
client.ref["onleave"] = this._onLeave.bind(this, client);
|
|
957
1049
|
client.ref.once("close", client.ref["onleave"]);
|
|
958
1050
|
client.ref.on("message", this._onMessage.bind(this, client));
|
|
959
|
-
|
|
960
|
-
if (!connectionOptions?.skipHandshake && this.inputOptions !== void 0) {
|
|
961
|
-
const inputBytes = _inputReflectionCache.get(this.inputOptions.ctor);
|
|
962
|
-
if (inputBytes !== void 0) {
|
|
963
|
-
extraSections = [{ tag: HandshakeSection.INPUT_REFLECTION, bytes: inputBytes }];
|
|
964
|
-
}
|
|
965
|
-
}
|
|
1051
|
+
const extraSections = this._inputController?.handshakeSections();
|
|
966
1052
|
client.raw(getMessageBytes[Protocol.JOIN_ROOM](
|
|
967
1053
|
client.reconnectionToken,
|
|
968
1054
|
this._serializer.id,
|
|
@@ -1058,36 +1144,64 @@ var Room = class _Room {
|
|
|
1058
1144
|
debugMessage("broadcast: %O (roomId: %s)", message, this.roomId);
|
|
1059
1145
|
const encodedMessage = message instanceof Uint8Array ? getMessageBytes.raw(Protocol.ROOM_DATA_BYTES, type, void 0, message) : getMessageBytes.raw(Protocol.ROOM_DATA, type, message);
|
|
1060
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;
|
|
1061
1152
|
let numClients = this.clients.length;
|
|
1062
1153
|
while (numClients--) {
|
|
1063
1154
|
const client = this.clients[numClients];
|
|
1064
1155
|
if (!except || !except.includes(client)) {
|
|
1065
|
-
client.enqueueRaw(encodedMessage);
|
|
1156
|
+
client.enqueueRaw(encodedMessage, sendOpts);
|
|
1066
1157
|
}
|
|
1067
1158
|
}
|
|
1068
1159
|
}
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
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;
|
|
1074
1179
|
}
|
|
1075
1180
|
sendFullState(client) {
|
|
1076
|
-
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
|
+
));
|
|
1077
1185
|
}
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
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]);
|
|
1087
1200
|
}
|
|
1201
|
+
frames.length = 0;
|
|
1088
1202
|
}
|
|
1089
|
-
this._afterNextPatchQueue.splice(0, length);
|
|
1090
1203
|
}
|
|
1204
|
+
dirty.length = 0;
|
|
1091
1205
|
}
|
|
1092
1206
|
async _reserveSeat(sessionId, joinOptions = true, authData = void 0, seconds = this.seatReservationTimeout, allowReconnection = false, devModeReconnectionToken) {
|
|
1093
1207
|
if (!allowReconnection && this.hasReachedMaxClients()) {
|
|
@@ -1121,6 +1235,7 @@ var Room = class _Room {
|
|
|
1121
1235
|
delete this._reconnections[devModeReconnectionToken];
|
|
1122
1236
|
delete this._reservedSeats[sessionId];
|
|
1123
1237
|
delete this._reservedSeatTimeouts[sessionId];
|
|
1238
|
+
this._inputController?.release(sessionId);
|
|
1124
1239
|
if (!allowReconnection) {
|
|
1125
1240
|
await this.#_decrementClientCount();
|
|
1126
1241
|
}
|
|
@@ -1162,33 +1277,14 @@ var Room = class _Room {
|
|
|
1162
1277
|
this._simulationInterval = void 0;
|
|
1163
1278
|
}
|
|
1164
1279
|
if (this._autoDisposeTimeout) {
|
|
1165
|
-
|
|
1280
|
+
clearTimeout(this._autoDisposeTimeout);
|
|
1166
1281
|
this._autoDisposeTimeout = void 0;
|
|
1167
1282
|
}
|
|
1168
1283
|
this.clock.clear();
|
|
1169
1284
|
this.clock.stop();
|
|
1285
|
+
this._inputController?.dispose();
|
|
1170
1286
|
return await (userReturnData || Promise.resolve());
|
|
1171
1287
|
}
|
|
1172
|
-
/**
|
|
1173
|
-
* After the decoder has mutated `client._input`, push a clone into the
|
|
1174
|
-
* per-client buffer (when buffering is enabled). Honors
|
|
1175
|
-
* `inputOptions.seqField` for dedupe of redundant frames.
|
|
1176
|
-
*/
|
|
1177
|
-
#captureInput(client) {
|
|
1178
|
-
const buf = client._inputBuffer;
|
|
1179
|
-
if (!buf) {
|
|
1180
|
-
return;
|
|
1181
|
-
}
|
|
1182
|
-
const inst = client._input;
|
|
1183
|
-
const seqField = this.inputOptions?.seqField;
|
|
1184
|
-
if (seqField !== void 0) {
|
|
1185
|
-
const value = inst[seqField];
|
|
1186
|
-
if (typeof value === "number" && !buf.accept(value)) {
|
|
1187
|
-
return;
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
buf.push(inst.clone());
|
|
1191
|
-
}
|
|
1192
1288
|
_onMessage(client, buffer) {
|
|
1193
1289
|
if (client.state === ClientState.LEAVING) {
|
|
1194
1290
|
return;
|
|
@@ -1205,98 +1301,18 @@ var Room = class _Room {
|
|
|
1205
1301
|
return this.#_forciblyCloseClient(client, CloseCode.WITH_ERROR);
|
|
1206
1302
|
}
|
|
1207
1303
|
const it = { offset: 1 };
|
|
1208
|
-
const code = buffer[0];
|
|
1304
|
+
const code = buffer[0] & PROTOCOL_CODE_MASK;
|
|
1305
|
+
const modifiers = buffer[0] & PROTOCOL_MODIFIER_MASK;
|
|
1209
1306
|
if (code === Protocol.ROOM_DATA) {
|
|
1210
|
-
|
|
1211
|
-
let message;
|
|
1212
|
-
try {
|
|
1213
|
-
message = buffer.byteLength > it.offset ? unpack(buffer.subarray(it.offset, buffer.byteLength)) : void 0;
|
|
1214
|
-
debugMessage("received: '%s' -> %j (roomId: %s)", messageType, message, this.roomId);
|
|
1215
|
-
if (this.onMessageValidators[messageType] !== void 0) {
|
|
1216
|
-
message = standardValidate(this.onMessageValidators[messageType], message);
|
|
1217
|
-
}
|
|
1218
|
-
} catch (e) {
|
|
1219
|
-
debugAndPrintError(e);
|
|
1220
|
-
client.leave(CloseCode.WITH_ERROR);
|
|
1221
|
-
return;
|
|
1222
|
-
}
|
|
1223
|
-
if (this.onMessageEvents.events[messageType]) {
|
|
1224
|
-
this.onMessageEvents.emit(messageType, client, message);
|
|
1225
|
-
} else if (this.onMessageEvents.events["*"]) {
|
|
1226
|
-
this.onMessageEvents.emit("*", client, messageType, message);
|
|
1227
|
-
} else {
|
|
1228
|
-
this.onMessageFallbacks["__no_message_handler"](client, messageType, message);
|
|
1229
|
-
}
|
|
1307
|
+
this.#_messages.onData(client, buffer, it);
|
|
1230
1308
|
} else if (code === Protocol.ROOM_REQUEST) {
|
|
1231
|
-
|
|
1232
|
-
const messageType = decode.stringCheck(buffer, it) ? decode.string(buffer, it) : decode.number(buffer, it);
|
|
1233
|
-
let message;
|
|
1234
|
-
try {
|
|
1235
|
-
message = buffer.byteLength > it.offset ? unpack(buffer.subarray(it.offset, buffer.byteLength)) : void 0;
|
|
1236
|
-
debugMessage("request #%d: '%s' -> %j (roomId: %s)", requestId, messageType, message, this.roomId);
|
|
1237
|
-
if (this.onMessageValidators[messageType] !== void 0) {
|
|
1238
|
-
message = standardValidate(this.onMessageValidators[messageType], message);
|
|
1239
|
-
}
|
|
1240
|
-
} catch (e) {
|
|
1241
|
-
debugAndPrintError(e);
|
|
1242
|
-
this.#replyToRequest(client, requestId, ResponseStatus.ERROR, toResponseError(e));
|
|
1243
|
-
return;
|
|
1244
|
-
}
|
|
1245
|
-
const handler = this.onMessageEvents.events[messageType]?.[0];
|
|
1246
|
-
if (handler === void 0) {
|
|
1247
|
-
this.#replyToRequest(client, requestId, ResponseStatus.ERROR, {
|
|
1248
|
-
name: "no_handler",
|
|
1249
|
-
message: `room "${this.roomName}" has no onMessage("${messageType}") handler to answer this request.`
|
|
1250
|
-
});
|
|
1251
|
-
return;
|
|
1252
|
-
}
|
|
1253
|
-
Promise.resolve().then(() => handler(client, message)).then(
|
|
1254
|
-
(response) => this.#replyToRequest(client, requestId, ResponseStatus.OK, response),
|
|
1255
|
-
(e) => {
|
|
1256
|
-
debugAndPrintError(e);
|
|
1257
|
-
this.#replyToRequest(client, requestId, ResponseStatus.ERROR, toResponseError(e));
|
|
1258
|
-
}
|
|
1259
|
-
);
|
|
1309
|
+
this.#_messages.onRequest(client, buffer, it);
|
|
1260
1310
|
} else if (code === Protocol.ROOM_DATA_BYTES) {
|
|
1261
|
-
|
|
1262
|
-
let message = buffer.subarray(it.offset, buffer.byteLength);
|
|
1263
|
-
debugMessage("received: '%s' -> %j (roomId: %s)", messageType, message, this.roomId);
|
|
1264
|
-
const bytesMessageType = `_$b${messageType}`;
|
|
1265
|
-
try {
|
|
1266
|
-
if (this.onMessageValidators[bytesMessageType] !== void 0) {
|
|
1267
|
-
message = standardValidate(this.onMessageValidators[bytesMessageType], message);
|
|
1268
|
-
}
|
|
1269
|
-
} catch (e) {
|
|
1270
|
-
debugAndPrintError(e);
|
|
1271
|
-
client.leave(CloseCode.WITH_ERROR);
|
|
1272
|
-
return;
|
|
1273
|
-
}
|
|
1274
|
-
if (this.onMessageEvents.events[bytesMessageType]) {
|
|
1275
|
-
this.onMessageEvents.emit(bytesMessageType, client, message);
|
|
1276
|
-
} else if (this.onMessageEvents.events["*"]) {
|
|
1277
|
-
this.onMessageEvents.emit("*", client, messageType, message);
|
|
1278
|
-
} else {
|
|
1279
|
-
this.onMessageFallbacks["__no_message_handler"](client, messageType, message);
|
|
1280
|
-
}
|
|
1311
|
+
this.#_messages.onDataBytes(client, buffer, it);
|
|
1281
1312
|
} else if (code === Protocol.ROOM_INPUT_RELIABLE) {
|
|
1282
|
-
|
|
1283
|
-
try {
|
|
1284
|
-
client._inputDecoder.decode(buffer.subarray(1));
|
|
1285
|
-
} catch (e) {
|
|
1286
|
-
debugAndPrintError(e);
|
|
1287
|
-
return;
|
|
1288
|
-
}
|
|
1289
|
-
this.#captureInput(client);
|
|
1290
|
-
}
|
|
1313
|
+
this._inputController?.decodeReliable(client, buffer, it, modifiers);
|
|
1291
1314
|
} else if (code === Protocol.ROOM_INPUT_UNRELIABLE) {
|
|
1292
|
-
|
|
1293
|
-
try {
|
|
1294
|
-
client._inputDecoder.decodeAll(buffer.subarray(1), () => this.#captureInput(client));
|
|
1295
|
-
} catch (e) {
|
|
1296
|
-
debugAndPrintError(e);
|
|
1297
|
-
return;
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1315
|
+
this._inputController?.decodeUnreliable(client, buffer, modifiers);
|
|
1300
1316
|
} else if (code === Protocol.JOIN_ROOM && client.state === ClientState.JOINING) {
|
|
1301
1317
|
client.state = ClientState.JOINED;
|
|
1302
1318
|
client._joinedAt = this.clock.elapsedTime;
|
|
@@ -1324,6 +1340,7 @@ var Room = class _Room {
|
|
|
1324
1340
|
if (!this.clients.delete(client)) {
|
|
1325
1341
|
return;
|
|
1326
1342
|
}
|
|
1343
|
+
this._inputController?.freeze(client);
|
|
1327
1344
|
if (method) {
|
|
1328
1345
|
debugMatchMaking(`${method.name}, sessionId: '%s' (close code: %d, roomId: %s)`, client.sessionId, code, this.roomId);
|
|
1329
1346
|
try {
|
|
@@ -1350,6 +1367,7 @@ var Room = class _Room {
|
|
|
1350
1367
|
}
|
|
1351
1368
|
const willDispose = await this.#_decrementClientCount();
|
|
1352
1369
|
if (this._reservedSeats[client.sessionId] === void 0) {
|
|
1370
|
+
this._inputController?.release(client.sessionId);
|
|
1353
1371
|
this._events.emit("leave", client, willDispose);
|
|
1354
1372
|
}
|
|
1355
1373
|
}
|
|
@@ -1413,23 +1431,6 @@ var Room = class _Room {
|
|
|
1413
1431
|
}
|
|
1414
1432
|
}
|
|
1415
1433
|
};
|
|
1416
|
-
function room(options) {
|
|
1417
|
-
class _ extends Room {
|
|
1418
|
-
constructor() {
|
|
1419
|
-
super();
|
|
1420
|
-
this.messages = options.messages;
|
|
1421
|
-
if (options.state && typeof options.state === "function") {
|
|
1422
|
-
this.state = options.state();
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
for (const key in options) {
|
|
1427
|
-
if (typeof options[key] === "function") {
|
|
1428
|
-
_.prototype[key] = options[key];
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
return _;
|
|
1432
|
-
}
|
|
1433
1434
|
export {
|
|
1434
1435
|
DEFAULT_SEAT_RESERVATION_TIME,
|
|
1435
1436
|
Room,
|
|
@@ -1437,6 +1438,5 @@ export {
|
|
|
1437
1438
|
RoomPlugin2 as RoomPlugin,
|
|
1438
1439
|
attachToTestRoom,
|
|
1439
1440
|
definePlugins,
|
|
1440
|
-
room,
|
|
1441
1441
|
validate
|
|
1442
1442
|
};
|