@colyseus/core 0.17.42 → 0.18.0

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 (90) hide show
  1. package/build/MatchMaker.cjs +19 -6
  2. package/build/MatchMaker.cjs.map +2 -2
  3. package/build/MatchMaker.d.ts +10 -0
  4. package/build/MatchMaker.mjs +18 -6
  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/Room.cjs +296 -19
  12. package/build/Room.cjs.map +3 -3
  13. package/build/Room.d.ts +186 -3
  14. package/build/Room.mjs +303 -21
  15. package/build/Room.mjs.map +3 -3
  16. package/build/RoomPlugin.cjs +252 -0
  17. package/build/RoomPlugin.cjs.map +7 -0
  18. package/build/RoomPlugin.d.ts +271 -0
  19. package/build/RoomPlugin.mjs +220 -0
  20. package/build/RoomPlugin.mjs.map +7 -0
  21. package/build/Server.cjs +40 -7
  22. package/build/Server.cjs.map +2 -2
  23. package/build/Server.d.ts +25 -0
  24. package/build/Server.mjs +41 -8
  25. package/build/Server.mjs.map +2 -2
  26. package/build/Transport.cjs +38 -2
  27. package/build/Transport.cjs.map +2 -2
  28. package/build/Transport.d.ts +40 -4
  29. package/build/Transport.mjs +38 -2
  30. package/build/Transport.mjs.map +2 -2
  31. package/build/index.cjs +11 -2
  32. package/build/index.cjs.map +2 -2
  33. package/build/index.d.ts +2 -1
  34. package/build/index.mjs +12 -2
  35. package/build/index.mjs.map +2 -2
  36. package/build/input/InputBuffer.cjs +113 -0
  37. package/build/input/InputBuffer.cjs.map +7 -0
  38. package/build/input/InputBuffer.d.ts +136 -0
  39. package/build/input/InputBuffer.mjs +86 -0
  40. package/build/input/InputBuffer.mjs.map +7 -0
  41. package/build/internal.cjs +61 -0
  42. package/build/internal.cjs.map +7 -0
  43. package/build/internal.d.ts +9 -0
  44. package/build/internal.mjs +29 -0
  45. package/build/internal.mjs.map +7 -0
  46. package/build/matchmaker/LocalDriver/LocalDriver.cjs +13 -0
  47. package/build/matchmaker/LocalDriver/LocalDriver.cjs.map +2 -2
  48. package/build/matchmaker/LocalDriver/LocalDriver.d.ts +1 -0
  49. package/build/matchmaker/LocalDriver/LocalDriver.mjs +13 -0
  50. package/build/matchmaker/LocalDriver/LocalDriver.mjs.map +2 -2
  51. package/build/matchmaker/driver.cjs.map +1 -1
  52. package/build/matchmaker/driver.d.ts +12 -0
  53. package/build/matchmaker/driver.mjs.map +1 -1
  54. package/build/presence/LocalPresence.d.ts +1 -1
  55. package/build/rooms/LobbyRoom.cjs +8 -10
  56. package/build/rooms/LobbyRoom.cjs.map +2 -2
  57. package/build/rooms/LobbyRoom.d.ts +4 -3
  58. package/build/rooms/LobbyRoom.mjs +8 -10
  59. package/build/rooms/LobbyRoom.mjs.map +2 -2
  60. package/build/rooms/RelayRoom.cjs +12 -16
  61. package/build/rooms/RelayRoom.cjs.map +2 -2
  62. package/build/rooms/RelayRoom.d.ts +32 -11
  63. package/build/rooms/RelayRoom.mjs +10 -16
  64. package/build/rooms/RelayRoom.mjs.map +2 -2
  65. package/build/router/index.cjs +65 -4
  66. package/build/router/index.cjs.map +2 -2
  67. package/build/router/index.d.ts +30 -6
  68. package/build/router/index.mjs +66 -6
  69. package/build/router/index.mjs.map +3 -3
  70. package/build/utils/UserSessionIndex.cjs +162 -0
  71. package/build/utils/UserSessionIndex.cjs.map +7 -0
  72. package/build/utils/UserSessionIndex.d.ts +166 -0
  73. package/build/utils/UserSessionIndex.mjs +130 -0
  74. package/build/utils/UserSessionIndex.mjs.map +7 -0
  75. package/package.json +19 -14
  76. package/src/MatchMaker.ts +40 -6
  77. package/src/Protocol.ts +130 -59
  78. package/src/Room.ts +475 -22
  79. package/src/RoomPlugin.ts +563 -0
  80. package/src/Server.ts +72 -11
  81. package/src/Transport.ts +76 -8
  82. package/src/index.ts +10 -1
  83. package/src/input/InputBuffer.ts +192 -0
  84. package/src/internal.ts +46 -0
  85. package/src/matchmaker/LocalDriver/LocalDriver.ts +10 -0
  86. package/src/matchmaker/driver.ts +13 -0
  87. package/src/rooms/LobbyRoom.ts +12 -8
  88. package/src/rooms/RelayRoom.ts +9 -15
  89. package/src/router/index.ts +112 -11
  90. package/src/utils/UserSessionIndex.ts +311 -0
package/build/Room.mjs CHANGED
@@ -1,6 +1,9 @@
1
1
  // packages/core/src/Room.ts
2
- import { unpack } from "@colyseus/msgpackr";
3
- import { decode, $changes } from "@colyseus/schema";
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";
4
7
  import { ClockTimer as Clock } from "@colyseus/timer";
5
8
  import { EventEmitter } from "events";
6
9
  import { logger } from "./Logger.mjs";
@@ -19,11 +22,29 @@ import * as matchMaker from "./MatchMaker.mjs";
19
22
  import {
20
23
  CloseCode,
21
24
  ErrorCode,
22
- Protocol
25
+ HandshakeSection,
26
+ Protocol,
27
+ ResponseStatus
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";
37
+ var _inputReflectionCache = /* @__PURE__ */ new WeakMap();
24
38
  var DEFAULT_PATCH_RATE = 1e3 / 20;
25
39
  var DEFAULT_SIMULATION_INTERVAL = 1e3 / 60;
26
40
  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
+ }
27
48
  var DEFAULT_SEAT_RESERVATION_TIME = Number(process.env.COLYSEUS_SEAT_RESERVATION_TIME || 15);
28
49
  function validate(format, handler) {
29
50
  return { format, handler };
@@ -106,6 +127,7 @@ var Room = class _Room {
106
127
  this._internalState = RoomInternalState.CREATING;
107
128
  this._lockedExplicitly = false;
108
129
  this.#_locked = false;
130
+ this.#_tick = 0;
109
131
  this._events.once("dispose", () => {
110
132
  this.#_dispose().catch((e) => debugAndPrintError(`onDispose error: ${e && e.stack || e.message || e || "promise rejected"} (roomId: ${this.roomId})`)).finally(() => this._events.emit("disconnect"));
111
133
  });
@@ -222,6 +244,9 @@ var Room = class _Room {
222
244
  if (this.#_state) {
223
245
  this.state = this.#_state;
224
246
  }
247
+ if (this.plugins !== void 0) {
248
+ setupRoomPlugins(this);
249
+ }
225
250
  if (this.messages !== void 0) {
226
251
  if (this.messages["_"]) {
227
252
  this.onMessage("*", this.messages["_"].bind(this));
@@ -278,6 +303,44 @@ var Room = class _Room {
278
303
  }
279
304
  this.#_roomId = roomId;
280
305
  }
306
+ /**
307
+ * 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.
310
+ *
311
+ * ```typescript
312
+ * class FpsRoom extends Room<{ input: MoveInput }> {
313
+ * input = this.defineInput(MoveInput, {
314
+ * seqField: "tick", // typed: only numeric fields of MoveInput
315
+ * bufferMaxSize: 64,
316
+ * });
317
+ *
318
+ * // …or without options — defaults to seqField: "seq", bufferMaxSize: 32:
319
+ * // input = this.defineInput(MoveInput);
320
+ * }
321
+ * ```
322
+ *
323
+ * **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.
326
+ * - `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).
329
+ */
330
+ 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
+ });
343
+ }
281
344
  /**
282
345
  * This method is called before onJoin() - this is where you should authenticate the client
283
346
  * @param client - The client that is authenticating.
@@ -378,6 +441,47 @@ var Room = class _Room {
378
441
  }, delay);
379
442
  }
380
443
  }
444
+ /**
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.
449
+ *
450
+ * Replaces any previous {@link setSimulationInterval}. The current tick is
451
+ * exposed via {@link tick}.
452
+ *
453
+ * @example
454
+ * ```typescript
455
+ * class LockstepRoom extends Room<{ input: MoveInput }> {
456
+ * input = this.defineInput(MoveInput, { seqField: "tick", bufferMaxSize: 64 });
457
+ *
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
+ * }
467
+ * }
468
+ * ```
469
+ */
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);
476
+ }
477
+ /**
478
+ * Current server tick. Incremented by {@link setTickedSimulation} after each
479
+ * tick callback returns. Returns 0 when no ticked simulation is running.
480
+ */
481
+ get tick() {
482
+ return this.#_tick;
483
+ }
484
+ #_tick;
381
485
  /**
382
486
  * @deprecated Use `.patchRate=` instead.
383
487
  */
@@ -394,19 +498,7 @@ var Room = class _Room {
394
498
  this._serializer = serializer;
395
499
  }
396
500
  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
- }
501
+ this._listing.metadata = meta;
410
502
  if (persist && this._internalState === RoomInternalState.CREATED) {
411
503
  await matchMaker.driver.persist(this._listing);
412
504
  this._events.emit("metadata-change");
@@ -447,7 +539,8 @@ var Room = class _Room {
447
539
  *
448
540
  * @example
449
541
  * ```typescript
450
- * // Partial metadata update (merges with existing)
542
+ * // Merging with existing metadata: spread `this.metadata` yourself.
543
+ * // `metadata` is always REPLACED (not merged) by setMatchmaking()/setMetadata().
451
544
  * await this.setMatchmaking({
452
545
  * metadata: { ...this.metadata, round: this.metadata.round + 1 }
453
546
  * });
@@ -621,6 +714,100 @@ var Room = class _Room {
621
714
  return this.onMessage(messageType, callback);
622
715
  }
623
716
  }
717
+ // ---------------------------------------------------------------------------
718
+ // Operator API — used by @colyseus/admin (and monitor in due course)
719
+ // through `remoteRoomCall(roomId, methodName)`. Marked `@internal` because
720
+ // they're framework-tooling primitives, not part of the game-code surface.
721
+ // ---------------------------------------------------------------------------
722
+ /**
723
+ * Snapshot the room's live state for an inspector / admin UI. Includes:
724
+ *
725
+ * roomId, name, maxClients, locked, elapsedTime (ms),
726
+ * metadata, clients (sessionId + per-client elapsed + userId when set),
727
+ * state (the schema/json the SDK would see),
728
+ * stateSize (bytes of the encoded full state, or 0 when no serializer).
729
+ *
730
+ * The payload is intentionally plain JSON — `remoteRoomCall` serializes
731
+ * the return value across process boundaries.
732
+ *
733
+ * @internal Operator-only. Game code should not call this.
734
+ */
735
+ getInspectorView() {
736
+ const elapsed = this.clock.elapsedTime;
737
+ return {
738
+ roomId: this.roomId,
739
+ name: this.roomName,
740
+ clients: this.clients.length,
741
+ maxClients: this.maxClients,
742
+ locked: this.#_locked,
743
+ elapsedTime: elapsed,
744
+ metadata: this.metadata ?? null,
745
+ // Cast through `unknown`: `ExtractRoomClient<T>` vs `Client & ClientPrivate`
746
+ // don't structurally overlap (the user's `client` generic may carry a
747
+ // narrower userData/auth shape), but the runtime objects we walk here
748
+ // always have the private join-time field. The narrow per-property
749
+ // `(c as any)` reads below keep the cast scoped.
750
+ //
751
+ // userId / userEmail read straight off `client.auth` — the JWT
752
+ // payload @colyseus/auth's default onAuth decodes carries both
753
+ // when the user has them on file. Saves the admin a per-client
754
+ // database round-trip; falls back to null when the client signed
755
+ // in anonymously (or with a custom onAuth that returns a shape
756
+ // without those fields).
757
+ clientList: this.clients.map((c) => {
758
+ const auth = c.auth;
759
+ return {
760
+ sessionId: c.sessionId,
761
+ userId: c.userId ?? auth?.id ?? null,
762
+ userEmail: auth?.email ?? null,
763
+ elapsedTime: elapsed - (c._joinedAt ?? elapsed)
764
+ };
765
+ }),
766
+ state: this.state ?? null,
767
+ stateSize: this.#_inspectorStateSize()
768
+ };
769
+ }
770
+ /**
771
+ * Force-disconnect a single client by sessionId. No-op when the client
772
+ * isn't connected (idempotent — the caller doesn't need to race-check).
773
+ *
774
+ * @internal Operator-only. Game code disconnects clients by calling
775
+ * `.leave()` on the Client object directly.
776
+ */
777
+ kickClient(sessionId, closeCode = CloseCode.CONSENTED, reason) {
778
+ for (const client of this.clients) {
779
+ if (client.sessionId === sessionId) {
780
+ this.#_forciblyCloseClient(client, closeCode, reason);
781
+ return;
782
+ }
783
+ }
784
+ }
785
+ /**
786
+ * Best-effort byte size of the current full state. Falls back to `0`
787
+ * when the room has no serializer or the serializer can't produce a
788
+ * payload (raw rooms, very-early-onCreate, etc.). The serializer
789
+ * detection mirrors `@colyseus/monitor`'s — we read whichever buffer
790
+ * is available across schema v2 and v3.
791
+ */
792
+ #_inspectorStateSize() {
793
+ const ser = this._serializer;
794
+ if (!ser) {
795
+ return 0;
796
+ }
797
+ const hasState = ser.encoder || ser.state;
798
+ if (!hasState) {
799
+ return 0;
800
+ }
801
+ try {
802
+ const full = ser.getFullState?.();
803
+ if (!full) {
804
+ return 0;
805
+ }
806
+ return full.byteLength ?? full.length ?? 0;
807
+ } catch {
808
+ return 0;
809
+ }
810
+ }
624
811
  /**
625
812
  * Disconnect all connected clients, and then dispose the room.
626
813
  *
@@ -658,6 +845,15 @@ var Room = class _Room {
658
845
  async _onJoin(client, authContext, connectionOptions) {
659
846
  const sessionId = client.sessionId;
660
847
  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
+ }
661
857
  if (this._reservedSeatTimeouts[sessionId]) {
662
858
  clearTimeout(this._reservedSeatTimeouts[sessionId]);
663
859
  delete this._reservedSeatTimeouts[sessionId];
@@ -760,6 +956,13 @@ var Room = class _Room {
760
956
  client.ref["onleave"] = this._onLeave.bind(this, client);
761
957
  client.ref.once("close", client.ref["onleave"]);
762
958
  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
+ }
763
966
  client.raw(getMessageBytes[Protocol.JOIN_ROOM](
764
967
  client.reconnectionToken,
765
968
  this._serializer.id,
@@ -767,7 +970,8 @@ var Room = class _Room {
767
970
  * if skipHandshake is true, we don't need to send the handshake
768
971
  * (in case client already has handshake data)
769
972
  */
770
- connectionOptions?.skipHandshake ? void 0 : this._serializer.handshake && this._serializer.handshake()
973
+ connectionOptions?.skipHandshake ? void 0 : this._serializer.handshake && this._serializer.handshake(),
974
+ extraSections
771
975
  ));
772
976
  }
773
977
  }
@@ -834,7 +1038,7 @@ var Room = class _Room {
834
1038
  }).finally(() => {
835
1039
  cleanup();
836
1040
  });
837
- if (this._reconnectionAttempts[reconnectionToken]) {
1041
+ if (this._reconnectionAttempts[reconnectionToken] !== void 0) {
838
1042
  debugMatchMaking("resolving reconnection attempt for client - sessionId: '%s', roomId: '%s'", sessionId, this.roomId);
839
1043
  this._reconnectionAttempts[reconnectionToken].resolve(true);
840
1044
  }
@@ -862,6 +1066,12 @@ var Room = class _Room {
862
1066
  }
863
1067
  }
864
1068
  }
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));
1074
+ }
865
1075
  sendFullState(client) {
866
1076
  client.raw(this._serializer.getFullState(client));
867
1077
  }
@@ -959,6 +1169,26 @@ var Room = class _Room {
959
1169
  this.clock.stop();
960
1170
  return await (userReturnData || Promise.resolve());
961
1171
  }
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
+ }
962
1192
  _onMessage(client, buffer) {
963
1193
  if (client.state === ClientState.LEAVING) {
964
1194
  return;
@@ -997,6 +1227,36 @@ var Room = class _Room {
997
1227
  } else {
998
1228
  this.onMessageFallbacks["__no_message_handler"](client, messageType, message);
999
1229
  }
1230
+ } 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
+ );
1000
1260
  } else if (code === Protocol.ROOM_DATA_BYTES) {
1001
1261
  const messageType = decode.stringCheck(buffer, it) ? decode.string(buffer, it) : decode.number(buffer, it);
1002
1262
  let message = buffer.subarray(it.offset, buffer.byteLength);
@@ -1018,6 +1278,25 @@ var Room = class _Room {
1018
1278
  } else {
1019
1279
  this.onMessageFallbacks["__no_message_handler"](client, messageType, message);
1020
1280
  }
1281
+ } 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
+ }
1291
+ } 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
+ }
1021
1300
  } else if (code === Protocol.JOIN_ROOM && client.state === ClientState.JOINING) {
1022
1301
  client.state = ClientState.JOINED;
1023
1302
  client._joinedAt = this.clock.elapsedTime;
@@ -1034,10 +1313,10 @@ var Room = class _Room {
1034
1313
  this.#_forciblyCloseClient(client, CloseCode.CONSENTED);
1035
1314
  }
1036
1315
  }
1037
- #_forciblyCloseClient(client, closeCode) {
1316
+ #_forciblyCloseClient(client, closeCode, reason) {
1038
1317
  client.ref.removeAllListeners("message");
1039
1318
  client.ref.removeListener("close", client.ref["onleave"]);
1040
- this._onLeave(client, closeCode).then(() => client.leave(closeCode));
1319
+ this._onLeave(client, closeCode).then(() => client.leave(closeCode, reason));
1041
1320
  }
1042
1321
  async _onLeave(client, code) {
1043
1322
  const method = code === CloseCode.CONSENTED || client.state === ClientState.RECONNECTING ? this.onLeave : this.onDrop || this.onLeave;
@@ -1155,6 +1434,9 @@ export {
1155
1434
  DEFAULT_SEAT_RESERVATION_TIME,
1156
1435
  Room,
1157
1436
  RoomInternalState,
1437
+ RoomPlugin2 as RoomPlugin,
1438
+ attachToTestRoom,
1439
+ definePlugins,
1158
1440
  room,
1159
1441
  validate
1160
1442
  };