@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.cjs
CHANGED
|
@@ -36,15 +36,15 @@ __export(Room_exports, {
|
|
|
36
36
|
RoomPlugin: () => import_RoomPlugin2.RoomPlugin,
|
|
37
37
|
attachToTestRoom: () => import_RoomPlugin2.attachToTestRoom,
|
|
38
38
|
definePlugins: () => import_RoomPlugin2.definePlugins,
|
|
39
|
-
room: () => room,
|
|
40
39
|
validate: () => validate
|
|
41
40
|
});
|
|
42
41
|
module.exports = __toCommonJS(Room_exports);
|
|
43
|
-
var import_msgpackr = require("msgpackr");
|
|
44
42
|
var import_schema = require("@colyseus/schema");
|
|
45
|
-
var import_input = require("@colyseus/schema/input");
|
|
46
43
|
var import_InputBuffer = require("./input/InputBuffer.cjs");
|
|
47
|
-
var
|
|
44
|
+
var import_RoomInput = require("./input/RoomInput.cjs");
|
|
45
|
+
var import_RoomMessages = require("./RoomMessages.cjs");
|
|
46
|
+
var import_Rewind = require("./Rewind.cjs");
|
|
47
|
+
var import_types = require("./input/types.cjs");
|
|
48
48
|
var import_timer = require("@colyseus/timer");
|
|
49
49
|
var import_events = require("events");
|
|
50
50
|
var import_Logger = require("./Logger.cjs");
|
|
@@ -52,7 +52,6 @@ var import_NoneSerializer = require("./serializer/NoneSerializer.cjs");
|
|
|
52
52
|
var import_SchemaSerializer = require("./serializer/SchemaSerializer.cjs");
|
|
53
53
|
var import_Protocol = require("./Protocol.cjs");
|
|
54
54
|
var import_Utils = require("./utils/Utils.cjs");
|
|
55
|
-
var import_nanoevents = require("./utils/nanoevents.cjs");
|
|
56
55
|
var import_DevMode = require("./utils/DevMode.cjs");
|
|
57
56
|
var import_Debug = require("./Debug.cjs");
|
|
58
57
|
var import_ServerError = require("./errors/ServerError.cjs");
|
|
@@ -63,17 +62,10 @@ var matchMaker = __toESM(require("./MatchMaker.cjs"), 1);
|
|
|
63
62
|
var import_shared_types = require("@colyseus/shared-types");
|
|
64
63
|
var import_RoomPlugin = require("./RoomPlugin.cjs");
|
|
65
64
|
var import_RoomPlugin2 = require("./RoomPlugin.cjs");
|
|
66
|
-
var _inputReflectionCache = /* @__PURE__ */ new WeakMap();
|
|
67
65
|
var DEFAULT_PATCH_RATE = 1e3 / 20;
|
|
68
66
|
var DEFAULT_SIMULATION_INTERVAL = 1e3 / 60;
|
|
69
67
|
var noneSerializer = new import_NoneSerializer.NoneSerializer();
|
|
70
|
-
|
|
71
|
-
if (e instanceof Error) {
|
|
72
|
-
const code = e.code;
|
|
73
|
-
return code !== void 0 ? { name: e.name, message: e.message, code } : { name: e.name, message: e.message };
|
|
74
|
-
}
|
|
75
|
-
return { name: "Error", message: String(e) };
|
|
76
|
-
}
|
|
68
|
+
var AFTER_PATCH_OPTS = Object.freeze({ afterNextPatch: true });
|
|
77
69
|
var DEFAULT_SEAT_RESERVATION_TIME = Number(process.env.COLYSEUS_SEAT_RESERVATION_TIME || 15);
|
|
78
70
|
function validate(format, handler) {
|
|
79
71
|
return { format, handler };
|
|
@@ -138,25 +130,22 @@ var Room = class _Room {
|
|
|
138
130
|
this._reservedSeatTimeouts = {};
|
|
139
131
|
this._reconnections = {};
|
|
140
132
|
this._reconnectionAttempts = {};
|
|
141
|
-
|
|
142
|
-
this
|
|
143
|
-
this.onMessageFallbacks = {
|
|
144
|
-
"__no_message_handler": (client, messageType, _) => {
|
|
145
|
-
const errorMessage = `room onMessage for "${messageType}" not registered.`;
|
|
146
|
-
(0, import_Debug.debugMessage)(`${errorMessage} (roomId: ${this.roomId})`);
|
|
147
|
-
if (import_DevMode.isDevMode) {
|
|
148
|
-
client.error(import_shared_types.ErrorCode.INVALID_PAYLOAD, errorMessage);
|
|
149
|
-
} else {
|
|
150
|
-
client.leave(import_shared_types.CloseCode.WITH_ERROR, errorMessage);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
};
|
|
133
|
+
/** @internal Message-routing layer: handler registry + user-message decode/dispatch. */
|
|
134
|
+
this.#_messages = new import_RoomMessages.RoomMessages(this);
|
|
154
135
|
this._serializer = noneSerializer;
|
|
155
|
-
|
|
136
|
+
/** Clients that staged `afterNextPatch` frames since the last patch, so the
|
|
137
|
+
* post-patch flush iterates only these — never the full client list. Shared by
|
|
138
|
+
* reference onto each client at join. Reset each `broadcastPatch`. */
|
|
139
|
+
this.#pendingFrameClients = [];
|
|
140
|
+
/** `broadcast(..., { afterNextPatch })` in NON-timed rooms, sent as a SHARED
|
|
141
|
+
* frame right after the next patch (one encode, N sends). Non-timed patches are
|
|
142
|
+
* themselves a shared buffer, so a per-client copy would force N× allocation in
|
|
143
|
+
* large rooms — sharing avoids that. TIMED rooms instead stage the broadcast onto
|
|
144
|
+
* each client's `_pendingFrames`. Drained each `broadcastPatch`. */
|
|
145
|
+
this.#afterPatchBroadcasts = [];
|
|
156
146
|
this._internalState = RoomInternalState.CREATING;
|
|
157
147
|
this._lockedExplicitly = false;
|
|
158
148
|
this.#_locked = false;
|
|
159
|
-
this.#_tick = 0;
|
|
160
149
|
this._events.once("dispose", () => {
|
|
161
150
|
this.#_dispose().catch((e) => (0, import_Debug.debugAndPrintError)(`onDispose error: ${e && e.stack || e.message || e || "promise rejected"} (roomId: ${this.roomId})`)).finally(() => this._events.emit("disconnect"));
|
|
162
151
|
});
|
|
@@ -209,6 +198,17 @@ var Room = class _Room {
|
|
|
209
198
|
#_patchRate;
|
|
210
199
|
#_patchInterval;
|
|
211
200
|
#_state;
|
|
201
|
+
#_messages;
|
|
202
|
+
// Re-expose the registry for @colyseus/playground introspection and
|
|
203
|
+
// @colyseus/testing handler-swapping (both reach in via bracket access).
|
|
204
|
+
get onMessageEvents() {
|
|
205
|
+
return this.#_messages.events;
|
|
206
|
+
}
|
|
207
|
+
get onMessageValidators() {
|
|
208
|
+
return this.#_messages.validators;
|
|
209
|
+
}
|
|
210
|
+
#pendingFrameClients;
|
|
211
|
+
#afterPatchBroadcasts;
|
|
212
212
|
#_locked;
|
|
213
213
|
/**
|
|
214
214
|
* This method is called by the MatchMaker before onCreate()
|
|
@@ -334,41 +334,41 @@ var Room = class _Room {
|
|
|
334
334
|
}
|
|
335
335
|
/**
|
|
336
336
|
* Declare the input schema and configuration in a single line. Returns the
|
|
337
|
-
*
|
|
338
|
-
* `this.
|
|
337
|
+
* input API that gets assigned to `this.inputs` — call
|
|
338
|
+
* `this.inputs.get(sessionId)` per tick to consume.
|
|
339
339
|
*
|
|
340
340
|
* ```typescript
|
|
341
341
|
* class FpsRoom extends Room<{ input: MoveInput }> {
|
|
342
|
-
*
|
|
342
|
+
* inputs = this.defineInput(MoveInput, {
|
|
343
343
|
* seqField: "tick", // typed: only numeric fields of MoveInput
|
|
344
344
|
* bufferMaxSize: 64,
|
|
345
345
|
* });
|
|
346
346
|
*
|
|
347
|
-
* // …or without options —
|
|
348
|
-
* //
|
|
347
|
+
* // …or without options — no seq dedupe, bufferMaxSize: 32:
|
|
348
|
+
* // inputs = this.defineInput(MoveInput);
|
|
349
349
|
* }
|
|
350
350
|
* ```
|
|
351
351
|
*
|
|
352
352
|
* **Defaults** when `opts` (or individual fields) are omitted:
|
|
353
|
-
* - `seqField`:
|
|
354
|
-
*
|
|
353
|
+
* - `seqField`: unset — dedupe and `this.inputs.get(sessionId).at(value)` lookup are
|
|
354
|
+
* OPT-IN (lockstep / rollback). Name a monotonic numeric field here to enable
|
|
355
|
+
* them; redundant frames (`input[seqField]` ≤ the last seen) are then dropped.
|
|
356
|
+
* Leave unset for reliable, in-order channels where every frame is unique.
|
|
355
357
|
* - `bufferMaxSize`: `32` — enables per-client snapshot buffering for
|
|
356
|
-
* `
|
|
357
|
-
* buffering (the `.latest`
|
|
358
|
+
* `this.inputs.get(sessionId)` iteration / `.consume() / .drain() / .next() /
|
|
359
|
+
* .take() / .peek() / .at()`. Set to `0` to disable buffering (the `.latest`
|
|
360
|
+
* read still works).
|
|
361
|
+
*
|
|
362
|
+
* **Consuming the buffer** — `for (const inp of this.inputs.get(sessionId))` (sugar
|
|
363
|
+
* for `.consume()`) is the per-entity loop: it consumes one at a time, so
|
|
364
|
+
* lag-comp `renderTime` tracks each input. `next()` (take exactly one, ack +1)
|
|
365
|
+
* suits a shared physics world stepped once for everyone; `drain()` returns the
|
|
366
|
+
* whole pending set as an array. See {@link InputAccessor} for the full
|
|
367
|
+
* per-entity-vs-shared-world guidance and why the choice affects the reconcile ack.
|
|
358
368
|
*/
|
|
359
369
|
defineInput(type, opts) {
|
|
360
|
-
this.
|
|
361
|
-
|
|
362
|
-
seqField: opts?.seqField ?? "seq",
|
|
363
|
-
bufferMaxSize: opts?.bufferMaxSize ?? 32
|
|
364
|
-
};
|
|
365
|
-
if (!_inputReflectionCache.has(type)) {
|
|
366
|
-
_inputReflectionCache.set(type, import_schema.Reflection.encode(new import_schema.Encoder(new type())));
|
|
367
|
-
}
|
|
368
|
-
return ((sessionId) => {
|
|
369
|
-
const c = this.clients.getById(sessionId);
|
|
370
|
-
return c?._inputAccessor ?? import_InputBuffer.NO_OP_INPUT_ACCESSOR;
|
|
371
|
-
});
|
|
370
|
+
this._inputController ??= new import_RoomInput.RoomInput(this);
|
|
371
|
+
return this._inputController.define(type, opts);
|
|
372
372
|
}
|
|
373
373
|
/**
|
|
374
374
|
* This method is called before onJoin() - this is where you should authenticate the client
|
|
@@ -429,7 +429,7 @@ var Room = class _Room {
|
|
|
429
429
|
reservedSeat[3] && this._reconnections[reconnectionToken]?.[0] === sessionId
|
|
430
430
|
);
|
|
431
431
|
} else if (typeof reconnectionToken === "string") {
|
|
432
|
-
return this.clients.
|
|
432
|
+
return this.clients.get(sessionId)?.reconnectionToken === reconnectionToken;
|
|
433
433
|
}
|
|
434
434
|
return false;
|
|
435
435
|
}
|
|
@@ -447,22 +447,27 @@ var Room = class _Room {
|
|
|
447
447
|
return void 0;
|
|
448
448
|
}
|
|
449
449
|
/**
|
|
450
|
-
*
|
|
451
|
-
*
|
|
450
|
+
* Set the room's game loop. `onTickCallback` runs every `delay` ms and receives
|
|
451
|
+
* the MEASURED wall-clock delta since the previous tick (a VARIABLE timestep).
|
|
452
|
+
* For deterministic, prediction-friendly simulation prefer
|
|
453
|
+
* {@link Room.setFixedTimestep}, which advances a fixed step via an accumulator.
|
|
452
454
|
*
|
|
453
455
|
* @default 16.6ms (60fps)
|
|
454
456
|
*
|
|
455
|
-
* @param onTickCallback -
|
|
456
|
-
*
|
|
457
|
-
* @param delay - Interval
|
|
457
|
+
* @param onTickCallback - Your physics / world update — a good place to mutate
|
|
458
|
+
* room state. Receives the measured delta (`this.clock.deltaTime`).
|
|
459
|
+
* @param delay - Interval between ticks in milliseconds.
|
|
458
460
|
*/
|
|
459
|
-
|
|
461
|
+
setTimestep(onTickCallback, delay = DEFAULT_SIMULATION_INTERVAL) {
|
|
460
462
|
if (this._simulationInterval) {
|
|
461
463
|
clearInterval(this._simulationInterval);
|
|
462
464
|
}
|
|
465
|
+
if (onTickCallback && this._inputController && this._inputController.options.tickRate === void 0) {
|
|
466
|
+
this._inputController.options.tickRate = Math.round(1e3 / delay);
|
|
467
|
+
}
|
|
463
468
|
if (onTickCallback) {
|
|
464
469
|
if (this.onUncaughtException !== void 0) {
|
|
465
|
-
onTickCallback = (0, import_Utils.wrapTryCatch)(onTickCallback, this.onUncaughtException.bind(this), import_RoomExceptions.
|
|
470
|
+
onTickCallback = (0, import_Utils.wrapTryCatch)(onTickCallback, this.onUncaughtException.bind(this), import_RoomExceptions.TimestepException, "setTimestep");
|
|
466
471
|
}
|
|
467
472
|
this._simulationInterval = setInterval(() => {
|
|
468
473
|
this.clock.tick();
|
|
@@ -471,46 +476,157 @@ var Room = class _Room {
|
|
|
471
476
|
}
|
|
472
477
|
}
|
|
473
478
|
/**
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
|
|
479
|
+
* @deprecated Renamed to {@link Room.setTimestep} (which pairs with
|
|
480
|
+
* {@link Room.setFixedTimestep}). Kept for backwards compatibility — forwards
|
|
481
|
+
* to `setTimestep` unchanged.
|
|
482
|
+
*/
|
|
483
|
+
setSimulationInterval(onTickCallback, delay = DEFAULT_SIMULATION_INTERVAL) {
|
|
484
|
+
this.setTimestep(onTickCallback, delay);
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Fixed-timestep game loop with a framework-owned accumulator — the right
|
|
488
|
+
* default for prediction/rollback. Unlike {@link setTimestep} (which
|
|
489
|
+
* hands you the *measured* wall-clock delta), this runs `step` a whole number
|
|
490
|
+
* of times per real frame so each step advances by the SAME fixed
|
|
491
|
+
* `dt = 1/tickRate`; the measured delta only decides HOW MANY steps run. The
|
|
492
|
+
* fixed dt is delivered via {@link StepContext}, so the jittery wall-clock
|
|
493
|
+
* delta can't leak into deterministic simulation.
|
|
478
494
|
*
|
|
479
|
-
*
|
|
480
|
-
*
|
|
495
|
+
* `tickRate` is also the SINGLE SOURCE of the simulation rate: it's advertised
|
|
496
|
+
* to predicting clients via the join handshake (they predict at the matching
|
|
497
|
+
* `dt`), so don't also pass `tickRate` to {@link defineInput}.
|
|
481
498
|
*
|
|
482
|
-
*
|
|
483
|
-
*
|
|
484
|
-
*
|
|
485
|
-
*
|
|
499
|
+
* **`tickRate` couples three rates** — by design, one input == one fixed step
|
|
500
|
+
* == one server tick, so lowering `tickRate` to save bandwidth also lowers
|
|
501
|
+
* the simulation rate. To keep high-fidelity physics on a lower network rate,
|
|
502
|
+
* pass `{ subSteps: N }`: one input still drives one fixed step, but you
|
|
503
|
+
* integrate `N` engine sub-steps of `ctx.subDt` (= `ctx.dt / N`) inside it —
|
|
504
|
+
* physics at `tickRate * N` Hz, inputs at `tickRate`/sec. The same
|
|
505
|
+
* `subSteps`/`subDt` are cascaded to predicting clients (via the join
|
|
506
|
+
* handshake, onto their reconciler's step context), so client replay
|
|
507
|
+
* reproduces the sub-stepped trajectory exactly. Render interpolation already
|
|
508
|
+
* smooths above the step rate — most games don't need this; reach for it when
|
|
509
|
+
* the *simulation* needs the extra Hz (fast projectiles, stacking, tunneling).
|
|
486
510
|
*
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
*
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
494
|
-
*
|
|
495
|
-
*
|
|
511
|
+
* On a hitch the accumulator runs at most a few catch-up steps then drops the
|
|
512
|
+
* backlog (no spiral of death). Lag-comp is recorded once per real frame.
|
|
513
|
+
*
|
|
514
|
+
* **Consuming input inside the step** — how you consume each client's buffer
|
|
515
|
+
* depends on who integrates (see {@link InputAccessor}):
|
|
516
|
+
* - *Per-entity* (each body integrates itself): `for (const cmd of
|
|
517
|
+
* this.inputs.get(sid)) applyInput(player, cmd, ctx.dt)` — N inputs =
|
|
518
|
+
* N sub-integrations, ack lands on the newest applied, and `renderTime`
|
|
519
|
+
* tracks each input (lag comp stays exact per step).
|
|
520
|
+
* - *Shared world* (one solver step advances every body): consume exactly one
|
|
521
|
+
* input per entity per step with `this.inputs.get(sid).next()` (or `take(n)` +
|
|
522
|
+
* sub-step), then `world.step()` once. Draining all and applying only the
|
|
523
|
+
* latest would jump the reconcile ack past inputs you never simulated.
|
|
524
|
+
*
|
|
525
|
+
* @param step - Called once per fixed step with a {@link StepContext}.
|
|
526
|
+
* @param tickRate - Simulation rate in **Hz**. Defaults to 60.
|
|
527
|
+
* @param opts - `subSteps`: physics sub-steps per fixed step (integer ≥ 1,
|
|
528
|
+
* default 1) — see above.
|
|
529
|
+
*
|
|
530
|
+
* @example
|
|
531
|
+
* ```ts
|
|
532
|
+
* onCreate() {
|
|
533
|
+
* // 30 inputs/sec on the wire, physics integrated at 60 Hz:
|
|
534
|
+
* this.setFixedTimestep((ctx) => {
|
|
535
|
+
* this.applyInputs(ctx); // consume ONE input per client per step
|
|
536
|
+
* for (let i = 0; i < ctx.subSteps; i++) this.world.step(ctx.subDt);
|
|
537
|
+
* }, 30, { subSteps: 2 });
|
|
496
538
|
* }
|
|
497
539
|
* ```
|
|
498
540
|
*/
|
|
499
|
-
|
|
500
|
-
this
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
541
|
+
setFixedTimestep(step, tickRate = Math.round(1e3 / DEFAULT_SIMULATION_INTERVAL), opts) {
|
|
542
|
+
if (this._simulationInterval) {
|
|
543
|
+
clearInterval(this._simulationInterval);
|
|
544
|
+
}
|
|
545
|
+
const stepMs = 1e3 / tickRate;
|
|
546
|
+
const stepSeconds = 1 / tickRate;
|
|
547
|
+
const subSteps = (0, import_InputBuffer.validateSubSteps)(opts?.subSteps, "setFixedTimestep") ?? this._inputController?.options.subSteps ?? 1;
|
|
548
|
+
if (this._inputController) {
|
|
549
|
+
this._inputController.options.tickRate = tickRate;
|
|
550
|
+
this._inputController.options.subSteps = subSteps;
|
|
551
|
+
}
|
|
552
|
+
let cb = step;
|
|
553
|
+
if (this.onUncaughtException !== void 0) {
|
|
554
|
+
cb = (0, import_Utils.wrapTryCatch)(step, this.onUncaughtException.bind(this), import_RoomExceptions.TimestepException, "setFixedTimestep");
|
|
555
|
+
}
|
|
556
|
+
let acc = 0;
|
|
557
|
+
let tick = 0;
|
|
558
|
+
const ctx = {
|
|
559
|
+
dt: stepSeconds,
|
|
560
|
+
dtMs: stepMs,
|
|
561
|
+
tick: 0,
|
|
562
|
+
subSteps,
|
|
563
|
+
subDt: stepSeconds / subSteps,
|
|
564
|
+
subDtMs: stepMs / subSteps
|
|
565
|
+
};
|
|
566
|
+
const MAX_CATCHUP_STEPS = 5;
|
|
567
|
+
this._simulationInterval = setInterval(() => {
|
|
568
|
+
this.clock.tick();
|
|
569
|
+
acc += this.clock.deltaTime;
|
|
570
|
+
let ran = 0;
|
|
571
|
+
while (acc >= stepMs && ran < MAX_CATCHUP_STEPS) {
|
|
572
|
+
acc -= stepMs;
|
|
573
|
+
ctx.tick = tick++;
|
|
574
|
+
cb(ctx);
|
|
575
|
+
ran++;
|
|
576
|
+
}
|
|
577
|
+
if (ran === MAX_CATCHUP_STEPS) {
|
|
578
|
+
acc = 0;
|
|
579
|
+
}
|
|
580
|
+
}, stepMs);
|
|
581
|
+
}
|
|
582
|
+
/** Server-side lag compensation, lazily created. @see allowRewindState */
|
|
583
|
+
#rewind;
|
|
584
|
+
/** @internal The rewind attachments' timeline mode (snapshot/reckon), or
|
|
585
|
+
* `undefined` when no rewind is configured. {@link RoomInput} reads it to
|
|
586
|
+
* derive the wire input stamp mode (snapshot → renderTime, reckon → reckonTime). */
|
|
587
|
+
_timelineMode() {
|
|
588
|
+
return this.#rewind?.timelineMode();
|
|
505
589
|
}
|
|
506
590
|
/**
|
|
507
|
-
*
|
|
508
|
-
*
|
|
591
|
+
* Enable server-side lag compensation: returns a {@link Rewind} that records the
|
|
592
|
+
* position history of the entities you attach and automatically snapshots them
|
|
593
|
+
* on each broadcast (the patchRate cadence). Attach the collections to rewind,
|
|
594
|
+
* then read past positions (at a client's renderTime) in your hit tests.
|
|
595
|
+
*
|
|
596
|
+
* @example
|
|
597
|
+
* ```ts
|
|
598
|
+
* const rewind = this.allowRewindState({ maxRewindMs: 500 });
|
|
599
|
+
* rewind.attachAll(this.state.enemies, { fields: ["x", "y"] });
|
|
600
|
+
* // in a hit test — rewind to where the SHOOTER saw the world:
|
|
601
|
+
* const seen = rewind.lastSeenBy(shooterSessionId); // needs this.defineInput(...)
|
|
602
|
+
* const seenX = seen.value(enemy, "x"), seenY = seen.value(enemy, "y");
|
|
603
|
+
* ```
|
|
604
|
+
*
|
|
605
|
+
* Per-client stamps auto-enable from the `attachAll` `mode` of the groups you
|
|
606
|
+
* rewind — no `renderTime` flag. The default auto-record fires on each
|
|
607
|
+
* broadcast, snapshotting exactly what the client receives — so the rewind
|
|
608
|
+
* reproduces the client's interpolation and hits stay exact even when the
|
|
609
|
+
* broadcast rate differs from the sim rate (`patchRate ≠ timestep`). Call
|
|
610
|
+
* `rewind.record()` yourself during a tick to take over that cadence (you then
|
|
611
|
+
* own correctness against your own broadcast rate).
|
|
509
612
|
*/
|
|
510
|
-
|
|
511
|
-
|
|
613
|
+
allowRewindState(opts) {
|
|
614
|
+
this.#rewind = import_Rewind.Rewind.get(this, opts);
|
|
615
|
+
this.#rewind.bindRenderTime((sessionId) => {
|
|
616
|
+
const api = this._inputController?.api;
|
|
617
|
+
if (api === void 0) {
|
|
618
|
+
throw new Error(
|
|
619
|
+
"rewind.lastSeenBy() found no input API. Declare `inputs = this.defineInput(YourInput)`, or use rewind.at(time) with a render time you track yourself."
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
return api.get(sessionId).renderTime;
|
|
623
|
+
});
|
|
624
|
+
this.#rewind.bindReckonTime(
|
|
625
|
+
(sessionId) => this._inputController?.rawReckonTime(sessionId) ?? 0
|
|
626
|
+
);
|
|
627
|
+
this.#rewind.bindNow(() => this.clock.elapsedTime);
|
|
628
|
+
return this.#rewind;
|
|
512
629
|
}
|
|
513
|
-
#_tick;
|
|
514
630
|
/**
|
|
515
631
|
* @deprecated Use `.patchRate=` instead.
|
|
516
632
|
*/
|
|
@@ -683,22 +799,12 @@ var Room = class _Room {
|
|
|
683
799
|
*/
|
|
684
800
|
broadcast(type, ...args) {
|
|
685
801
|
const [message, options] = args;
|
|
686
|
-
if (options && options.afterNextPatch) {
|
|
687
|
-
delete options.afterNextPatch;
|
|
688
|
-
this._afterNextPatchQueue.push(["broadcast", [type, ...args]]);
|
|
689
|
-
return;
|
|
690
|
-
}
|
|
691
802
|
this.broadcastMessageType(type, message, options);
|
|
692
803
|
}
|
|
693
804
|
/**
|
|
694
805
|
* Broadcast bytes (UInt8Arrays) to a particular room
|
|
695
806
|
*/
|
|
696
807
|
broadcastBytes(type, message, options) {
|
|
697
|
-
if (options && options.afterNextPatch) {
|
|
698
|
-
delete options.afterNextPatch;
|
|
699
|
-
this._afterNextPatchQueue.push(["broadcastBytes", arguments]);
|
|
700
|
-
return;
|
|
701
|
-
}
|
|
702
808
|
this.broadcastMessageType(type, message, options);
|
|
703
809
|
}
|
|
704
810
|
/**
|
|
@@ -714,34 +820,25 @@ var Room = class _Room {
|
|
|
714
820
|
if (!this.state) {
|
|
715
821
|
return false;
|
|
716
822
|
}
|
|
717
|
-
const
|
|
718
|
-
this.
|
|
823
|
+
const sNow = this.clock.elapsedTime;
|
|
824
|
+
const hasChanges = this._serializer.applyPatches(
|
|
825
|
+
this.clients,
|
|
826
|
+
this.state,
|
|
827
|
+
this._inputController !== void 0 ? { sNow } : void 0
|
|
828
|
+
);
|
|
829
|
+
this._flushPendingClientFrames();
|
|
830
|
+
this._flushAfterPatchBroadcasts();
|
|
831
|
+
const rw = this.#rewind;
|
|
832
|
+
if (rw !== void 0 && rw.lastRecordedAt !== sNow) {
|
|
833
|
+
rw.record(sNow, this.#_patchRate || void 0);
|
|
834
|
+
}
|
|
719
835
|
return hasChanges;
|
|
720
836
|
}
|
|
721
837
|
onMessage(_messageType, _validationSchema, _callback) {
|
|
722
|
-
|
|
723
|
-
const validationSchema = typeof _callback === "function" ? _validationSchema : void 0;
|
|
724
|
-
const callback = validationSchema === void 0 ? _validationSchema : _callback;
|
|
725
|
-
const removeListener = this.onMessageEvents.on(messageType, this.onUncaughtException !== void 0 ? (0, import_Utils.wrapTryCatch)(callback, this.onUncaughtException.bind(this), import_RoomExceptions.OnMessageException, "onMessage", false, _messageType) : callback);
|
|
726
|
-
if (validationSchema !== void 0) {
|
|
727
|
-
this.onMessageValidators[messageType] = validationSchema;
|
|
728
|
-
}
|
|
729
|
-
return () => {
|
|
730
|
-
removeListener();
|
|
731
|
-
if (this.onMessageEvents.events[messageType].length === 0) {
|
|
732
|
-
delete this.onMessageValidators[messageType];
|
|
733
|
-
}
|
|
734
|
-
};
|
|
838
|
+
return this.#_messages.on(_messageType, _validationSchema, _callback);
|
|
735
839
|
}
|
|
736
840
|
onMessageBytes(_messageType, _validationSchema, _callback) {
|
|
737
|
-
|
|
738
|
-
const validationSchema = typeof _callback === "function" ? _validationSchema : void 0;
|
|
739
|
-
const callback = validationSchema === void 0 ? _validationSchema : _callback;
|
|
740
|
-
if (validationSchema !== void 0) {
|
|
741
|
-
return this.onMessage(messageType, validationSchema, callback);
|
|
742
|
-
} else {
|
|
743
|
-
return this.onMessage(messageType, callback);
|
|
744
|
-
}
|
|
841
|
+
return this.#_messages.on(`_$b${_messageType}`, _validationSchema, _callback);
|
|
745
842
|
}
|
|
746
843
|
// ---------------------------------------------------------------------------
|
|
747
844
|
// Operator API — used by @colyseus/admin (and monitor in due course)
|
|
@@ -874,15 +971,7 @@ var Room = class _Room {
|
|
|
874
971
|
async _onJoin(client, authContext, connectionOptions) {
|
|
875
972
|
const sessionId = client.sessionId;
|
|
876
973
|
client.reconnectionToken = (0, import_Utils.generateId)();
|
|
877
|
-
|
|
878
|
-
client._input = new this.inputOptions.ctor();
|
|
879
|
-
client._inputDecoder = new import_input.InputDecoder(client._input);
|
|
880
|
-
const maxSize = this.inputOptions.bufferMaxSize;
|
|
881
|
-
if (maxSize > 0) {
|
|
882
|
-
client._inputBuffer = new import_InputBuffer.InputBufferImpl(maxSize, this.inputOptions.seqField);
|
|
883
|
-
}
|
|
884
|
-
client._inputAccessor = new import_InputBuffer.InputAccessorImpl(client);
|
|
885
|
-
}
|
|
974
|
+
this._inputController?.allocate(client);
|
|
886
975
|
if (this._reservedSeatTimeouts[sessionId]) {
|
|
887
976
|
clearTimeout(this._reservedSeatTimeouts[sessionId]);
|
|
888
977
|
delete this._reservedSeatTimeouts[sessionId];
|
|
@@ -891,7 +980,7 @@ var Room = class _Room {
|
|
|
891
980
|
clearTimeout(this._autoDisposeTimeout);
|
|
892
981
|
this._autoDisposeTimeout = void 0;
|
|
893
982
|
}
|
|
894
|
-
if (this._reservedSeats[sessionId] === void 0 && connectionOptions?.reconnectionToken && this.clients.
|
|
983
|
+
if (this._reservedSeats[sessionId] === void 0 && connectionOptions?.reconnectionToken && this.clients.get(sessionId)?.reconnectionToken === connectionOptions.reconnectionToken) {
|
|
895
984
|
(0, import_Debug.debugMatchMaking)("attempting to reconnect client with a stale previous connection - sessionId: '%s', roomId: '%s'", client.sessionId, this.roomId);
|
|
896
985
|
this._reconnectionAttempts[connectionOptions.reconnectionToken] = new import_Utils.Deferred();
|
|
897
986
|
const reconnectionAttemptTimeout = setTimeout(() => {
|
|
@@ -912,13 +1001,14 @@ var Room = class _Room {
|
|
|
912
1001
|
}
|
|
913
1002
|
this._reservedSeats[sessionId][2] = true;
|
|
914
1003
|
(0, import_Debug.debugMatchMaking)("consuming seat reservation, sessionId: '%s' (roomId: %s)", client.sessionId, this.roomId);
|
|
915
|
-
client.
|
|
1004
|
+
client._pendingFrameClients = this.#pendingFrameClients;
|
|
916
1005
|
client.ref["onleave"] = (_) => client.state = import_Transport.ClientState.LEAVING;
|
|
917
1006
|
client.ref.once("close", client.ref["onleave"]);
|
|
918
1007
|
if (isWaitingReconnection) {
|
|
919
1008
|
const reconnectionToken = connectionOptions?.reconnectionToken;
|
|
920
1009
|
if (reconnectionToken && this._reconnections[reconnectionToken]?.[0] === sessionId) {
|
|
921
1010
|
this.clients.push(client);
|
|
1011
|
+
this._inputController?.register(sessionId, client);
|
|
922
1012
|
await this._reconnections[reconnectionToken]?.[1].resolve(client);
|
|
923
1013
|
try {
|
|
924
1014
|
if (this.onReconnect) {
|
|
@@ -958,6 +1048,7 @@ var Room = class _Room {
|
|
|
958
1048
|
throw new import_ServerError.ServerError(import_shared_types.CloseCode.WITH_ERROR, "already disconnected");
|
|
959
1049
|
}
|
|
960
1050
|
this.clients.push(client);
|
|
1051
|
+
this._inputController?.register(sessionId, client);
|
|
961
1052
|
Object.defineProperty(this._reservedSeats, sessionId, {
|
|
962
1053
|
value: this._reservedSeats[sessionId],
|
|
963
1054
|
enumerable: false
|
|
@@ -985,13 +1076,7 @@ var Room = class _Room {
|
|
|
985
1076
|
client.ref["onleave"] = this._onLeave.bind(this, client);
|
|
986
1077
|
client.ref.once("close", client.ref["onleave"]);
|
|
987
1078
|
client.ref.on("message", this._onMessage.bind(this, client));
|
|
988
|
-
|
|
989
|
-
if (!connectionOptions?.skipHandshake && this.inputOptions !== void 0) {
|
|
990
|
-
const inputBytes = _inputReflectionCache.get(this.inputOptions.ctor);
|
|
991
|
-
if (inputBytes !== void 0) {
|
|
992
|
-
extraSections = [{ tag: import_shared_types.HandshakeSection.INPUT_REFLECTION, bytes: inputBytes }];
|
|
993
|
-
}
|
|
994
|
-
}
|
|
1079
|
+
const extraSections = this._inputController?.handshakeSections();
|
|
995
1080
|
client.raw(import_Protocol.getMessageBytes[import_shared_types.Protocol.JOIN_ROOM](
|
|
996
1081
|
client.reconnectionToken,
|
|
997
1082
|
this._serializer.id,
|
|
@@ -1087,36 +1172,64 @@ var Room = class _Room {
|
|
|
1087
1172
|
(0, import_Debug.debugMessage)("broadcast: %O (roomId: %s)", message, this.roomId);
|
|
1088
1173
|
const encodedMessage = message instanceof Uint8Array ? import_Protocol.getMessageBytes.raw(import_shared_types.Protocol.ROOM_DATA_BYTES, type, void 0, message) : import_Protocol.getMessageBytes.raw(import_shared_types.Protocol.ROOM_DATA, type, message);
|
|
1089
1174
|
const except = typeof options.except !== "undefined" ? Array.isArray(options.except) ? options.except : [options.except] : void 0;
|
|
1175
|
+
if (options.afterNextPatch && this._inputController === void 0) {
|
|
1176
|
+
this.#afterPatchBroadcasts.push({ bytes: encodedMessage, except });
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
const sendOpts = options.afterNextPatch ? AFTER_PATCH_OPTS : void 0;
|
|
1090
1180
|
let numClients = this.clients.length;
|
|
1091
1181
|
while (numClients--) {
|
|
1092
1182
|
const client = this.clients[numClients];
|
|
1093
1183
|
if (!except || !except.includes(client)) {
|
|
1094
|
-
client.enqueueRaw(encodedMessage);
|
|
1184
|
+
client.enqueueRaw(encodedMessage, sendOpts);
|
|
1095
1185
|
}
|
|
1096
1186
|
}
|
|
1097
1187
|
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1188
|
+
/** Drain `#afterPatchBroadcasts` right after the patch — each as ONE shared
|
|
1189
|
+
* buffer fanned out to its (non-excepted) recipients. `enqueueRaw` handles
|
|
1190
|
+
* JOINED (send now) vs still-joining (buffer until JOIN) per client. */
|
|
1191
|
+
_flushAfterPatchBroadcasts() {
|
|
1192
|
+
const queued = this.#afterPatchBroadcasts;
|
|
1193
|
+
if (queued.length === 0) {
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
for (let i = 0; i < queued.length; i++) {
|
|
1197
|
+
const { bytes, except } = queued[i];
|
|
1198
|
+
let numClients = this.clients.length;
|
|
1199
|
+
while (numClients--) {
|
|
1200
|
+
const client = this.clients[numClients];
|
|
1201
|
+
if (!except || !except.includes(client)) {
|
|
1202
|
+
client.enqueueRaw(bytes);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
queued.length = 0;
|
|
1103
1207
|
}
|
|
1104
1208
|
sendFullState(client) {
|
|
1105
|
-
client.raw(this._serializer.getFullState(
|
|
1209
|
+
client.raw(this._serializer.getFullState(
|
|
1210
|
+
client,
|
|
1211
|
+
this._inputController !== void 0 ? { sNow: this.clock.elapsedTime } : void 0
|
|
1212
|
+
));
|
|
1106
1213
|
}
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1214
|
+
/** Send each client's staged `afterNextPatch` frames as standalone frames right
|
|
1215
|
+
* after the patch, then reset the tracker. Iterates only the clients that staged
|
|
1216
|
+
* frames this cycle (`#pendingFrameClients`), never the full client list; empty
|
|
1217
|
+
* (→ O(1)) on a tick where nothing was staged. */
|
|
1218
|
+
_flushPendingClientFrames() {
|
|
1219
|
+
const dirty = this.#pendingFrameClients;
|
|
1220
|
+
if (dirty.length === 0) {
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
for (let i = 0; i < dirty.length; i++) {
|
|
1224
|
+
const frames = dirty[i]._pendingFrames;
|
|
1225
|
+
if (frames !== void 0 && frames.length > 0) {
|
|
1226
|
+
for (let j = 0; j < frames.length; j++) {
|
|
1227
|
+
dirty[i].raw(frames[j]);
|
|
1116
1228
|
}
|
|
1229
|
+
frames.length = 0;
|
|
1117
1230
|
}
|
|
1118
|
-
this._afterNextPatchQueue.splice(0, length);
|
|
1119
1231
|
}
|
|
1232
|
+
dirty.length = 0;
|
|
1120
1233
|
}
|
|
1121
1234
|
async _reserveSeat(sessionId, joinOptions = true, authData = void 0, seconds = this.seatReservationTimeout, allowReconnection = false, devModeReconnectionToken) {
|
|
1122
1235
|
if (!allowReconnection && this.hasReachedMaxClients()) {
|
|
@@ -1150,6 +1263,7 @@ var Room = class _Room {
|
|
|
1150
1263
|
delete this._reconnections[devModeReconnectionToken];
|
|
1151
1264
|
delete this._reservedSeats[sessionId];
|
|
1152
1265
|
delete this._reservedSeatTimeouts[sessionId];
|
|
1266
|
+
this._inputController?.release(sessionId);
|
|
1153
1267
|
if (!allowReconnection) {
|
|
1154
1268
|
await this.#_decrementClientCount();
|
|
1155
1269
|
}
|
|
@@ -1191,33 +1305,14 @@ var Room = class _Room {
|
|
|
1191
1305
|
this._simulationInterval = void 0;
|
|
1192
1306
|
}
|
|
1193
1307
|
if (this._autoDisposeTimeout) {
|
|
1194
|
-
|
|
1308
|
+
clearTimeout(this._autoDisposeTimeout);
|
|
1195
1309
|
this._autoDisposeTimeout = void 0;
|
|
1196
1310
|
}
|
|
1197
1311
|
this.clock.clear();
|
|
1198
1312
|
this.clock.stop();
|
|
1313
|
+
this._inputController?.dispose();
|
|
1199
1314
|
return await (userReturnData || Promise.resolve());
|
|
1200
1315
|
}
|
|
1201
|
-
/**
|
|
1202
|
-
* After the decoder has mutated `client._input`, push a clone into the
|
|
1203
|
-
* per-client buffer (when buffering is enabled). Honors
|
|
1204
|
-
* `inputOptions.seqField` for dedupe of redundant frames.
|
|
1205
|
-
*/
|
|
1206
|
-
#captureInput(client) {
|
|
1207
|
-
const buf = client._inputBuffer;
|
|
1208
|
-
if (!buf) {
|
|
1209
|
-
return;
|
|
1210
|
-
}
|
|
1211
|
-
const inst = client._input;
|
|
1212
|
-
const seqField = this.inputOptions?.seqField;
|
|
1213
|
-
if (seqField !== void 0) {
|
|
1214
|
-
const value = inst[seqField];
|
|
1215
|
-
if (typeof value === "number" && !buf.accept(value)) {
|
|
1216
|
-
return;
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
buf.push(inst.clone());
|
|
1220
|
-
}
|
|
1221
1316
|
_onMessage(client, buffer) {
|
|
1222
1317
|
if (client.state === import_Transport.ClientState.LEAVING) {
|
|
1223
1318
|
return;
|
|
@@ -1234,98 +1329,18 @@ var Room = class _Room {
|
|
|
1234
1329
|
return this.#_forciblyCloseClient(client, import_shared_types.CloseCode.WITH_ERROR);
|
|
1235
1330
|
}
|
|
1236
1331
|
const it = { offset: 1 };
|
|
1237
|
-
const code = buffer[0];
|
|
1332
|
+
const code = buffer[0] & import_shared_types.PROTOCOL_CODE_MASK;
|
|
1333
|
+
const modifiers = buffer[0] & import_shared_types.PROTOCOL_MODIFIER_MASK;
|
|
1238
1334
|
if (code === import_shared_types.Protocol.ROOM_DATA) {
|
|
1239
|
-
|
|
1240
|
-
let message;
|
|
1241
|
-
try {
|
|
1242
|
-
message = buffer.byteLength > it.offset ? (0, import_msgpackr.unpack)(buffer.subarray(it.offset, buffer.byteLength)) : void 0;
|
|
1243
|
-
(0, import_Debug.debugMessage)("received: '%s' -> %j (roomId: %s)", messageType, message, this.roomId);
|
|
1244
|
-
if (this.onMessageValidators[messageType] !== void 0) {
|
|
1245
|
-
message = (0, import_StandardSchema.standardValidate)(this.onMessageValidators[messageType], message);
|
|
1246
|
-
}
|
|
1247
|
-
} catch (e) {
|
|
1248
|
-
(0, import_Debug.debugAndPrintError)(e);
|
|
1249
|
-
client.leave(import_shared_types.CloseCode.WITH_ERROR);
|
|
1250
|
-
return;
|
|
1251
|
-
}
|
|
1252
|
-
if (this.onMessageEvents.events[messageType]) {
|
|
1253
|
-
this.onMessageEvents.emit(messageType, client, message);
|
|
1254
|
-
} else if (this.onMessageEvents.events["*"]) {
|
|
1255
|
-
this.onMessageEvents.emit("*", client, messageType, message);
|
|
1256
|
-
} else {
|
|
1257
|
-
this.onMessageFallbacks["__no_message_handler"](client, messageType, message);
|
|
1258
|
-
}
|
|
1335
|
+
this.#_messages.onData(client, buffer, it);
|
|
1259
1336
|
} else if (code === import_shared_types.Protocol.ROOM_REQUEST) {
|
|
1260
|
-
|
|
1261
|
-
const messageType = import_schema.decode.stringCheck(buffer, it) ? import_schema.decode.string(buffer, it) : import_schema.decode.number(buffer, it);
|
|
1262
|
-
let message;
|
|
1263
|
-
try {
|
|
1264
|
-
message = buffer.byteLength > it.offset ? (0, import_msgpackr.unpack)(buffer.subarray(it.offset, buffer.byteLength)) : void 0;
|
|
1265
|
-
(0, import_Debug.debugMessage)("request #%d: '%s' -> %j (roomId: %s)", requestId, messageType, message, this.roomId);
|
|
1266
|
-
if (this.onMessageValidators[messageType] !== void 0) {
|
|
1267
|
-
message = (0, import_StandardSchema.standardValidate)(this.onMessageValidators[messageType], message);
|
|
1268
|
-
}
|
|
1269
|
-
} catch (e) {
|
|
1270
|
-
(0, import_Debug.debugAndPrintError)(e);
|
|
1271
|
-
this.#replyToRequest(client, requestId, import_shared_types.ResponseStatus.ERROR, toResponseError(e));
|
|
1272
|
-
return;
|
|
1273
|
-
}
|
|
1274
|
-
const handler = this.onMessageEvents.events[messageType]?.[0];
|
|
1275
|
-
if (handler === void 0) {
|
|
1276
|
-
this.#replyToRequest(client, requestId, import_shared_types.ResponseStatus.ERROR, {
|
|
1277
|
-
name: "no_handler",
|
|
1278
|
-
message: `room "${this.roomName}" has no onMessage("${messageType}") handler to answer this request.`
|
|
1279
|
-
});
|
|
1280
|
-
return;
|
|
1281
|
-
}
|
|
1282
|
-
Promise.resolve().then(() => handler(client, message)).then(
|
|
1283
|
-
(response) => this.#replyToRequest(client, requestId, import_shared_types.ResponseStatus.OK, response),
|
|
1284
|
-
(e) => {
|
|
1285
|
-
(0, import_Debug.debugAndPrintError)(e);
|
|
1286
|
-
this.#replyToRequest(client, requestId, import_shared_types.ResponseStatus.ERROR, toResponseError(e));
|
|
1287
|
-
}
|
|
1288
|
-
);
|
|
1337
|
+
this.#_messages.onRequest(client, buffer, it);
|
|
1289
1338
|
} else if (code === import_shared_types.Protocol.ROOM_DATA_BYTES) {
|
|
1290
|
-
|
|
1291
|
-
let message = buffer.subarray(it.offset, buffer.byteLength);
|
|
1292
|
-
(0, import_Debug.debugMessage)("received: '%s' -> %j (roomId: %s)", messageType, message, this.roomId);
|
|
1293
|
-
const bytesMessageType = `_$b${messageType}`;
|
|
1294
|
-
try {
|
|
1295
|
-
if (this.onMessageValidators[bytesMessageType] !== void 0) {
|
|
1296
|
-
message = (0, import_StandardSchema.standardValidate)(this.onMessageValidators[bytesMessageType], message);
|
|
1297
|
-
}
|
|
1298
|
-
} catch (e) {
|
|
1299
|
-
(0, import_Debug.debugAndPrintError)(e);
|
|
1300
|
-
client.leave(import_shared_types.CloseCode.WITH_ERROR);
|
|
1301
|
-
return;
|
|
1302
|
-
}
|
|
1303
|
-
if (this.onMessageEvents.events[bytesMessageType]) {
|
|
1304
|
-
this.onMessageEvents.emit(bytesMessageType, client, message);
|
|
1305
|
-
} else if (this.onMessageEvents.events["*"]) {
|
|
1306
|
-
this.onMessageEvents.emit("*", client, messageType, message);
|
|
1307
|
-
} else {
|
|
1308
|
-
this.onMessageFallbacks["__no_message_handler"](client, messageType, message);
|
|
1309
|
-
}
|
|
1339
|
+
this.#_messages.onDataBytes(client, buffer, it);
|
|
1310
1340
|
} else if (code === import_shared_types.Protocol.ROOM_INPUT_RELIABLE) {
|
|
1311
|
-
|
|
1312
|
-
try {
|
|
1313
|
-
client._inputDecoder.decode(buffer.subarray(1));
|
|
1314
|
-
} catch (e) {
|
|
1315
|
-
(0, import_Debug.debugAndPrintError)(e);
|
|
1316
|
-
return;
|
|
1317
|
-
}
|
|
1318
|
-
this.#captureInput(client);
|
|
1319
|
-
}
|
|
1341
|
+
this._inputController?.decodeReliable(client, buffer, it, modifiers);
|
|
1320
1342
|
} else if (code === import_shared_types.Protocol.ROOM_INPUT_UNRELIABLE) {
|
|
1321
|
-
|
|
1322
|
-
try {
|
|
1323
|
-
client._inputDecoder.decodeAll(buffer.subarray(1), () => this.#captureInput(client));
|
|
1324
|
-
} catch (e) {
|
|
1325
|
-
(0, import_Debug.debugAndPrintError)(e);
|
|
1326
|
-
return;
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1343
|
+
this._inputController?.decodeUnreliable(client, buffer, modifiers);
|
|
1329
1344
|
} else if (code === import_shared_types.Protocol.JOIN_ROOM && client.state === import_Transport.ClientState.JOINING) {
|
|
1330
1345
|
client.state = import_Transport.ClientState.JOINED;
|
|
1331
1346
|
client._joinedAt = this.clock.elapsedTime;
|
|
@@ -1353,6 +1368,7 @@ var Room = class _Room {
|
|
|
1353
1368
|
if (!this.clients.delete(client)) {
|
|
1354
1369
|
return;
|
|
1355
1370
|
}
|
|
1371
|
+
this._inputController?.freeze(client);
|
|
1356
1372
|
if (method) {
|
|
1357
1373
|
(0, import_Debug.debugMatchMaking)(`${method.name}, sessionId: '%s' (close code: %d, roomId: %s)`, client.sessionId, code, this.roomId);
|
|
1358
1374
|
try {
|
|
@@ -1379,6 +1395,7 @@ var Room = class _Room {
|
|
|
1379
1395
|
}
|
|
1380
1396
|
const willDispose = await this.#_decrementClientCount();
|
|
1381
1397
|
if (this._reservedSeats[client.sessionId] === void 0) {
|
|
1398
|
+
this._inputController?.release(client.sessionId);
|
|
1382
1399
|
this._events.emit("leave", client, willDispose);
|
|
1383
1400
|
}
|
|
1384
1401
|
}
|
|
@@ -1442,23 +1459,6 @@ var Room = class _Room {
|
|
|
1442
1459
|
}
|
|
1443
1460
|
}
|
|
1444
1461
|
};
|
|
1445
|
-
function room(options) {
|
|
1446
|
-
class _ extends Room {
|
|
1447
|
-
constructor() {
|
|
1448
|
-
super();
|
|
1449
|
-
this.messages = options.messages;
|
|
1450
|
-
if (options.state && typeof options.state === "function") {
|
|
1451
|
-
this.state = options.state();
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1455
|
-
for (const key in options) {
|
|
1456
|
-
if (typeof options[key] === "function") {
|
|
1457
|
-
_.prototype[key] = options[key];
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
return _;
|
|
1461
|
-
}
|
|
1462
1462
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1463
1463
|
0 && (module.exports = {
|
|
1464
1464
|
DEFAULT_SEAT_RESERVATION_TIME,
|
|
@@ -1467,6 +1467,5 @@ function room(options) {
|
|
|
1467
1467
|
RoomPlugin,
|
|
1468
1468
|
attachToTestRoom,
|
|
1469
1469
|
definePlugins,
|
|
1470
|
-
room,
|
|
1471
1470
|
validate
|
|
1472
1471
|
});
|