@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.
Files changed (125) hide show
  1. package/build/MatchMaker.cjs +3 -7
  2. package/build/MatchMaker.cjs.map +2 -2
  3. package/build/MatchMaker.mjs +4 -8
  4. package/build/MatchMaker.mjs.map +2 -2
  5. package/build/Protocol.cjs +3 -3
  6. package/build/Protocol.cjs.map +1 -1
  7. package/build/Protocol.d.ts +3 -3
  8. package/build/Protocol.mjs +3 -3
  9. package/build/Protocol.mjs.map +1 -1
  10. package/build/Rewind.cjs +385 -0
  11. package/build/Rewind.cjs.map +7 -0
  12. package/build/Rewind.d.ts +291 -0
  13. package/build/Rewind.mjs +359 -0
  14. package/build/Rewind.mjs.map +7 -0
  15. package/build/Room.cjs +280 -281
  16. package/build/Room.cjs.map +3 -3
  17. package/build/Room.d.ts +185 -81
  18. package/build/Room.mjs +286 -286
  19. package/build/Room.mjs.map +2 -2
  20. package/build/RoomMessages.cjs +265 -0
  21. package/build/RoomMessages.cjs.map +7 -0
  22. package/build/RoomMessages.d.ts +49 -0
  23. package/build/RoomMessages.mjs +240 -0
  24. package/build/RoomMessages.mjs.map +7 -0
  25. package/build/Server.cjs +88 -31
  26. package/build/Server.cjs.map +2 -2
  27. package/build/Server.d.ts +36 -0
  28. package/build/Server.mjs +88 -31
  29. package/build/Server.mjs.map +2 -2
  30. package/build/Transport.cjs +26 -0
  31. package/build/Transport.cjs.map +2 -2
  32. package/build/Transport.d.ts +69 -2
  33. package/build/Transport.mjs +25 -0
  34. package/build/Transport.mjs.map +2 -2
  35. package/build/errors/RoomExceptions.cjs +7 -4
  36. package/build/errors/RoomExceptions.cjs.map +2 -2
  37. package/build/errors/RoomExceptions.d.ts +15 -3
  38. package/build/errors/RoomExceptions.mjs +5 -3
  39. package/build/errors/RoomExceptions.mjs.map +2 -2
  40. package/build/index.cjs +19 -2
  41. package/build/index.cjs.map +2 -2
  42. package/build/index.d.ts +7 -4
  43. package/build/index.mjs +17 -4
  44. package/build/index.mjs.map +2 -2
  45. package/build/input/InputBuffer.cjs +439 -16
  46. package/build/input/InputBuffer.cjs.map +2 -2
  47. package/build/input/InputBuffer.d.ts +186 -96
  48. package/build/input/InputBuffer.mjs +436 -15
  49. package/build/input/InputBuffer.mjs.map +2 -2
  50. package/build/input/RoomInput.cjs +306 -0
  51. package/build/input/RoomInput.cjs.map +7 -0
  52. package/build/input/RoomInput.d.ts +91 -0
  53. package/build/input/RoomInput.mjs +287 -0
  54. package/build/input/RoomInput.mjs.map +7 -0
  55. package/build/input/types.cjs +18 -0
  56. package/build/input/types.cjs.map +7 -0
  57. package/build/input/types.d.ts +476 -0
  58. package/build/input/types.mjs +0 -0
  59. package/build/input/types.mjs.map +7 -0
  60. package/build/rooms/QueueRoom.cjs +1 -1
  61. package/build/rooms/QueueRoom.cjs.map +2 -2
  62. package/build/rooms/QueueRoom.mjs +1 -1
  63. package/build/rooms/QueueRoom.mjs.map +2 -2
  64. package/build/router/default_routes.cjs +4 -1
  65. package/build/router/default_routes.cjs.map +2 -2
  66. package/build/router/default_routes.mjs +4 -1
  67. package/build/router/default_routes.mjs.map +2 -2
  68. package/build/router/index.cjs +6 -1
  69. package/build/router/index.cjs.map +2 -2
  70. package/build/router/index.mjs +6 -1
  71. package/build/router/index.mjs.map +2 -2
  72. package/build/router/node.cjs +37 -2
  73. package/build/router/node.cjs.map +2 -2
  74. package/build/router/node.d.ts +10 -0
  75. package/build/router/node.mjs +35 -1
  76. package/build/router/node.mjs.map +2 -2
  77. package/build/serializer/NoneSerializer.cjs +2 -2
  78. package/build/serializer/NoneSerializer.cjs.map +2 -2
  79. package/build/serializer/NoneSerializer.d.ts +3 -3
  80. package/build/serializer/NoneSerializer.mjs +2 -2
  81. package/build/serializer/NoneSerializer.mjs.map +2 -2
  82. package/build/serializer/SchemaSerializer.cjs +44 -14
  83. package/build/serializer/SchemaSerializer.cjs.map +2 -2
  84. package/build/serializer/SchemaSerializer.d.ts +3 -3
  85. package/build/serializer/SchemaSerializer.mjs +46 -16
  86. package/build/serializer/SchemaSerializer.mjs.map +2 -2
  87. package/build/serializer/Serializer.cjs.map +1 -1
  88. package/build/serializer/Serializer.d.ts +12 -2
  89. package/build/utils/DevMode.cjs +14 -14
  90. package/build/utils/DevMode.cjs.map +2 -2
  91. package/build/utils/DevMode.d.ts +0 -2
  92. package/build/utils/DevMode.mjs +14 -12
  93. package/build/utils/DevMode.mjs.map +2 -2
  94. package/build/utils/Env.cjs +8 -4
  95. package/build/utils/Env.cjs.map +3 -3
  96. package/build/utils/Env.mjs +8 -4
  97. package/build/utils/Env.mjs.map +2 -2
  98. package/build/utils/Utils.cjs +3 -0
  99. package/build/utils/Utils.cjs.map +2 -2
  100. package/build/utils/Utils.d.ts +8 -0
  101. package/build/utils/Utils.mjs +2 -0
  102. package/build/utils/Utils.mjs.map +2 -2
  103. package/package.json +11 -11
  104. package/src/MatchMaker.ts +7 -9
  105. package/src/Protocol.ts +3 -3
  106. package/src/Rewind.ts +572 -0
  107. package/src/Room.ts +409 -448
  108. package/src/RoomMessages.ts +342 -0
  109. package/src/Server.ts +112 -38
  110. package/src/Transport.ts +97 -2
  111. package/src/errors/RoomExceptions.ts +18 -4
  112. package/src/index.ts +13 -2
  113. package/src/input/InputBuffer.ts +464 -107
  114. package/src/input/RoomInput.ts +337 -0
  115. package/src/input/types.ts +503 -0
  116. package/src/rooms/QueueRoom.ts +1 -1
  117. package/src/router/default_routes.ts +6 -1
  118. package/src/router/index.ts +6 -1
  119. package/src/router/node.ts +44 -0
  120. package/src/serializer/NoneSerializer.ts +3 -3
  121. package/src/serializer/SchemaSerializer.ts +110 -19
  122. package/src/serializer/Serializer.ts +13 -2
  123. package/src/utils/DevMode.ts +18 -13
  124. package/src/utils/Env.ts +12 -4
  125. 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 { unpack } from "msgpackr";
3
- import { decode, Encoder, Reflection, $changes } from "@colyseus/schema";
4
- import { InputDecoder } from "@colyseus/schema/input";
5
- import { InputAccessorImpl, InputBufferImpl, NO_OP_INPUT_ACCESSOR } from "./input/InputBuffer.mjs";
6
- import {} from "./input/InputBuffer.mjs";
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, OnMessageException, OnReconnectException, SimulationIntervalException, TimedEventException } from "./errors/RoomExceptions.mjs";
20
- import { standardValidate } from "./utils/StandardSchema.mjs";
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
- ResponseStatus
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
- function toResponseError(e) {
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
- this.onMessageEvents = createNanoEvents();
113
- this.onMessageValidators = {};
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
- this._afterNextPatchQueue = [];
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
- * callable accessor that gets assigned to `this.input` — call
309
- * `this.input(sessionId)` per tick to consume.
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
- * input = this.defineInput(MoveInput, {
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 — defaults to seqField: "seq", bufferMaxSize: 32:
319
- * // input = this.defineInput(MoveInput);
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`: `"seq"`framework dedupes by `input.seq` if the schema has
325
- * such a field. Schemas without it gracefully skip dedupe.
325
+ * - `seqField`: unsetdedupe 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
- * `room.input(sessionId).drain() / .peek() / .at()`. Set to `0` to disable
328
- * buffering (the `.latest` read still works).
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.inputOptions = {
332
- ctor: type,
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.getById(sessionId)?.reconnectionToken === reconnectionToken;
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
- * (Optional) Set a simulation interval that can change the state of the game.
422
- * The simulation interval is your game loop.
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 - You can implement your physics or world updates here!
427
- * This is a good place to update the room state.
428
- * @param delay - Interval delay on executing `onTickCallback` in milliseconds.
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
- setSimulationInterval(onTickCallback, delay = DEFAULT_SIMULATION_INTERVAL) {
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), SimulationIntervalException, "setSimulationInterval");
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
- * Run a fixed-rate simulation tagged with a monotonic server tick number.
446
- * Combine with `room.input(sessionId).at(tick)` to retrieve each client's
447
- * input *for a specific tick* — the building block for lockstep / rollback
448
- * netcode.
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
- * Replaces any previous {@link setSimulationInterval}. The current tick is
451
- * exposed via {@link tick}.
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
- * @example
454
- * ```typescript
455
- * class LockstepRoom extends Room<{ input: MoveInput }> {
456
- * input = this.defineInput(MoveInput, { seqField: "tick", bufferMaxSize: 64 });
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
- * onCreate() {
459
- * this.setTickedSimulation((tick, dt) => {
460
- * for (const c of this.clients) {
461
- * const snapshot = this.input(c.sessionId).at(tick);
462
- * if (snapshot) this.apply(c, snapshot);
463
- * // else: predict, freeze, etc. game-level decision
464
- * }
465
- * }, 1000 / 60);
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
- setTickedSimulation(onTickCallback, delay = DEFAULT_SIMULATION_INTERVAL, startTick = 0) {
471
- this.#_tick = startTick;
472
- this.setSimulationInterval((dt) => {
473
- onTickCallback(this.#_tick, dt);
474
- this.#_tick++;
475
- }, delay);
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
- * Current server tick. Incremented by {@link setTickedSimulation} after each
479
- * tick callback returns. Returns 0 when no ticked simulation is running.
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
- get tick() {
482
- return this.#_tick;
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 hasChanges = this._serializer.applyPatches(this.clients, this.state);
689
- this._dequeueAfterPatchMessages();
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
- const messageType = _messageType.toString();
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
- const messageType = `_$b${_messageType}`;
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
- if (this.inputOptions !== void 0) {
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.getById(sessionId)?.reconnectionToken === connectionOptions.reconnectionToken) {
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._afterNextPatchQueue = this._afterNextPatchQueue;
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
- let extraSections;
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
- // Encode and enqueue a ROOM_RESPONSE for a client request. If the client has
1070
- // already left, `enqueueRaw` is a no-op no response is needed.
1071
- #replyToRequest(client, requestId, status, payload) {
1072
- debugMessage("response #%d: status=%d -> %j (roomId: %s)", requestId, status, payload, this.roomId);
1073
- client.enqueueRaw(getMessageBytes[Protocol.ROOM_RESPONSE](requestId, status, payload));
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(client));
1181
+ client.raw(this._serializer.getFullState(
1182
+ client,
1183
+ this._inputController !== void 0 ? { sNow: this.clock.elapsedTime } : void 0
1184
+ ));
1077
1185
  }
1078
- _dequeueAfterPatchMessages() {
1079
- const length = this._afterNextPatchQueue.length;
1080
- if (length > 0) {
1081
- for (let i = 0; i < length; i++) {
1082
- const [target, args] = this._afterNextPatchQueue[i];
1083
- if (target === "broadcast") {
1084
- this.broadcast.apply(this, args);
1085
- } else {
1086
- target.raw.apply(target, args);
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
- clearInterval(this._autoDisposeTimeout);
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
- const messageType = decode.stringCheck(buffer, it) ? decode.string(buffer, it) : decode.number(buffer, it);
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
- const requestId = decode.number(buffer, it);
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
- const messageType = decode.stringCheck(buffer, it) ? decode.string(buffer, it) : decode.number(buffer, it);
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
- if (client._inputDecoder) {
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
- if (client._inputDecoder) {
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
  };