@colyseus/core 0.17.44 → 0.18.1

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