@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
@@ -0,0 +1,503 @@
1
+ /**
2
+ * Types for the room input subsystem: the option shapes accepted by
3
+ * `Room.defineInput()`, the per-client {@link InputAccessor} / room-wide
4
+ * {@link InputAPI} surfaces, and the internal normalized options. Runtime
5
+ * lives in `InputBuffer.ts`.
6
+ */
7
+
8
+ /**
9
+ * Names of fields on `I` whose values are `number` — used by
10
+ * `Room.defineInput()` to constrain `seqField` to actually-numeric fields
11
+ * on the input schema. Filters out booleans, strings, methods, etc.
12
+ */
13
+ export type NumericFieldsOf<I> = {
14
+ [K in keyof I]: I[K] extends number ? (K extends string ? K : never) : never;
15
+ }[keyof I];
16
+
17
+ /**
18
+ * Context handed to the `idle` callback form — see {@link IdleInput}. A reused
19
+ * per-client scratch: read it inside the callback, don't store it. It carries
20
+ * MECHANISM only — derived judgments (e.g. liveness) stay in userland; look the
21
+ * client up via `this.clients.get(ctx.sessionId)` when your policy needs it.
22
+ */
23
+ export interface IdleContext<I> {
24
+ /** Last decoded input (`undefined` before the client's first). */
25
+ latest: I | undefined;
26
+ sessionId: string;
27
+ }
28
+
29
+ /**
30
+ * What to synthesize when the buffer is empty (declared room-wide at
31
+ * `defineInput({ idle })`, or per-call via {@link ConsumeOptions.idle}):
32
+ * - `true` — pure schema defaults.
33
+ * - `Partial<I>` — defaults overlaid with these fields. A full schema instance
34
+ * (e.g. {@link InputAccessor.latest}) also works — fields are copied BY NAME,
35
+ * so its prototype accessors are read correctly.
36
+ * - callback — invoked LAZILY (only on an actually-empty tick) with an
37
+ * {@link IdleContext}, returning either of the above. The right form when the
38
+ * overrides take work to compute (entity lookups, held-button carry-over).
39
+ *
40
+ * ⚠ Do NOT build overrides by spreading a schema instance (`{ ...latest, x }`):
41
+ * schema fields live on the prototype, so the spread copies NOTHING of them.
42
+ * Return `latest` itself, or name the fields (`{ x, plant: !!latest?.plant }`).
43
+ */
44
+ export type IdleInput<I> = true | Partial<I> | ((ctx: IdleContext<I>) => true | Partial<I>);
45
+
46
+ /**
47
+ * Options for {@link InputAccessor.drain} / {@link InputAccessor.next}.
48
+ * `idle` overrides the room-level `defineInput({ idle })` policy for this call
49
+ * (see {@link IdleInput}); pass `false` to suppress it (force skip behavior).
50
+ */
51
+ export interface ConsumeOptions<I> {
52
+ idle?: IdleInput<I> | false;
53
+ }
54
+
55
+ /**
56
+ * Input sanitization, declared at `defineInput({ sanitize })` — never trust the
57
+ * wire. Applied to each decoded frame IN PLACE, before anything reads it
58
+ * (`latest`, the buffer, the `idle` callback's ctx):
59
+ * - **Map form** — per-field `[min, max]` range clamps with NaN-safe semantics:
60
+ * `NaN` (both comparisons false) lands on `min`, closing the classic
61
+ * `Math.min(NaN, …)` poisoning hole.
62
+ * - **Callback form** — arbitrary in-place fix-up (wrap an angle, enforce a
63
+ * cross-field rule) for anything beyond ranges.
64
+ *
65
+ * Sanitizers MODIFY, they never reject — a malformed value becomes a legal one
66
+ * instead of dropping the frame. Must not touch the `seqField` (it runs before
67
+ * dedupe). Semantic validation ("slot must name an owned weapon") stays in
68
+ * your sim — this handles value domains, not game rules.
69
+ */
70
+ export type SanitizeInput<I> =
71
+ | Partial<Record<NumericFieldsOf<I>, readonly [number, number]>>
72
+ | ((input: I) => void);
73
+
74
+ /** Options for `Room.defineInput()`. The user-facing twin of the internal
75
+ * normalized {@link NormalizedInputOptions}. */
76
+ export interface DefineInputOptions<I = any> {
77
+ seqField?: NumericFieldsOf<I>;
78
+ bufferMaxSize?: number;
79
+ /**
80
+ * Input sanitization — never trust the wire. Applied IN PLACE to every
81
+ * decoded frame before anything reads it (`latest`, the buffer, the `idle`
82
+ * ctx). Map form = per-field `[min, max]` clamps with NaN-safe semantics
83
+ * (NaN → min — closes the `Math.min(NaN, …)` poisoning hole); callback form
84
+ * = arbitrary in-place fix-up. Sanitizers MODIFY, never reject. See
85
+ * {@link SanitizeInput}.
86
+ *
87
+ * ```ts
88
+ * sanitize: { moveF: [-1, 1], pitch: [-PITCH_LIMIT, PITCH_LIMIT], dt: [0, MAX_DT] },
89
+ * // or: sanitize: (f) => { f.angle = wrapAngle(f.angle); },
90
+ * ```
91
+ */
92
+ sanitize?: SanitizeInput<I>;
93
+ /**
94
+ * Room-level absence policy: when a tick has no buffered input, bare
95
+ * `drain()` / `next()` synthesize ONE "idle" frame from it — the schema's
96
+ * defaults overlaid with the policy's overrides — so the sim loop needs no
97
+ * empty-branch. Prefer the callback form, invoked lazily (only on
98
+ * actually-empty ticks) with an {@link IdleContext} (`latest` + `sessionId`):
99
+ *
100
+ * ```ts
101
+ * idle: ({ latest, sessionId }) => {
102
+ * const p = this.state.players.get(sessionId); // closes over the room
103
+ * return p ? { yaw: p.yaw, plant: !!latest?.plant } : true;
104
+ * }
105
+ * ```
106
+ *
107
+ * Not declaring it keeps the skip behavior (`drain()` → `[]`). Per-call
108
+ * `{ idle }` overrides this default; `{ idle: false }` suppresses it.
109
+ * Synthesized frames never advance the reconcile ack or `renderTime`.
110
+ */
111
+ idle?: IdleInput<I>;
112
+ /**
113
+ * Fixed step rate in **Hz** cascaded to the client via the join handshake;
114
+ * it predicts at dt = 1/tickRate for deterministic rollback. Defaults to
115
+ * the `setTimestep` rate — pass this only when the prediction
116
+ * step differs from it. NOTE: a *rate*, not an interval — `1000/30` is the
117
+ * step in ms, a classic mistake; use {@link stepMs} for that. Superseded by
118
+ * {@link stepMs} / {@link stepSeconds} when those are given.
119
+ */
120
+ tickRate?: number;
121
+ /**
122
+ * Fixed step as a duration in **milliseconds** — the unit-safe alternative
123
+ * to {@link tickRate} (`stepMs: 1000/30` is unambiguous where
124
+ * `tickRate: 1000/30` is a bug). Normalized to the canonical Hz value
125
+ * (`1000/stepMs`). Takes precedence over `tickRate`.
126
+ */
127
+ stepMs?: number;
128
+ /**
129
+ * Fixed step as a duration in **seconds** (e.g. `1/30`). Normalized to the
130
+ * canonical Hz value (`1/stepSeconds`). Highest precedence.
131
+ */
132
+ stepSeconds?: number;
133
+ /**
134
+ * Physics sub-steps per input tick (integer ≥ 1, default 1) — decouples the
135
+ * PHYSICS rate from the input/network rate. One input still drives exactly
136
+ * one fixed step (the replay invariant), but the simulation integrates
137
+ * `subSteps` engine steps of `stepSeconds/subSteps` inside it, identically
138
+ * on client and server — physics at `tickRate * subSteps` Hz while sending
139
+ * `tickRate` inputs/sec. Cascaded to clients via the join handshake;
140
+ * both sides read the derived numbers off their step context
141
+ * (`ctx.subSteps` / `ctx.subDt`) so N and dt can't drift apart.
142
+ * Usually declared via {@link Room.setFixedTimestep}'s `subSteps` option
143
+ * instead — pass it here only when the room runs its own loop.
144
+ */
145
+ subSteps?: number;
146
+ }
147
+
148
+ /** `true` when the defineInput opts declared an `idle` policy — narrows the
149
+ * returned {@link InputAPI} so bare `next()` types non-optional `I`.
150
+ * @internal — exported for {@link RoomInput.define}. */
151
+ export type IdleDeclared<O, I> = O extends { idle: IdleInput<I> } ? true : false;
152
+
153
+ /**
154
+ * Internal: input configuration captured by `Room.defineInput()` — the
155
+ * normalized form of {@link DefineInputOptions} (step declarations resolved to
156
+ * Hz, sanitizer compiled). The schema constructor is stored here so the
157
+ * runtime doesn't need to know it through the public `room.inputs` (the
158
+ * `.get(sessionId)` accessor).
159
+ *
160
+ * @internal
161
+ */
162
+ export interface NormalizedInputOptions {
163
+ /**
164
+ * Schema constructor used to allocate per-client input instances on join.
165
+ * Captured by `defineInput()` from its `type` argument.
166
+ *
167
+ * Typed loosely (`new () => any`) to sidestep type-identity issues across
168
+ * duplicate `@colyseus/schema` installs; the runtime calls
169
+ * `instance.clone()` and friends, which match by shape.
170
+ */
171
+ ctor: new () => any;
172
+
173
+ /**
174
+ * Name of a monotonically-increasing numeric field on the input schema used
175
+ * to order and dedupe incoming frames. Unset by default (opt-in) — dedupe and
176
+ * `.at()` lookup are off unless you name a field here. When set, the framework:
177
+ * - Drops redundant frames (`input[seqField]` ≤ the last-seen value are
178
+ * discarded before they enter the buffer) — the unreliable-mode
179
+ * ring-redundancy pattern.
180
+ * - Powers `room.inputs.get(sessionId).at(value)` lookups.
181
+ *
182
+ * Despite the name, "seq" here is broader than an integer counter — any
183
+ * monotonic numeric field works:
184
+ * - **Sequence counter** (`"seq"`, `"tick"`, `"frame"`) — typical for
185
+ * lockstep / rollback netcode.
186
+ * - **Timestamp** (`"timestamp"`, milliseconds or seconds) — useful for
187
+ * variable-rate clients, lag compensation, hit registration (a float-seconds
188
+ * timestamp stamped on each saved move is the common shape).
189
+ *
190
+ * Whichever you use, the field must increase monotonically across frames
191
+ * for dedupe to work.
192
+ */
193
+ seqField?: string;
194
+
195
+ /**
196
+ * > 0 enables per-client buffering of cloned snapshots — required for
197
+ * `room.inputs.get(sessionId).drain() / .peek() / .at()` to return populated
198
+ * data. Oldest drops on overflow. Set to `0` to disable (`.latest` still
199
+ * works).
200
+ */
201
+ bufferMaxSize: number;
202
+
203
+ /**
204
+ * Fixed step rate (Hz) advertised to clients via the join handshake; they
205
+ * predict at dt = 1/tickRate. Set explicitly via `defineInput`, or derived
206
+ * from `setTimestep`. Unset = not advertised.
207
+ */
208
+ tickRate?: number;
209
+
210
+ /**
211
+ * Physics sub-steps per input tick (integer ≥ 1) advertised to clients via
212
+ * the join handshake — the simulation integrates `subSteps` engine steps of
213
+ * `(1/tickRate)/subSteps` per input on BOTH sides, so physics runs at
214
+ * `tickRate * subSteps` Hz on a `tickRate` input/network rate. Set via
215
+ * `setFixedTimestep(..., { subSteps })` (or `defineInput`). Unset/1 = not
216
+ * advertised (input rate == physics rate).
217
+ */
218
+ subSteps?: number;
219
+
220
+ /**
221
+ * Room-level absence policy: bare `drain()` / `next()` synthesize one idle
222
+ * frame from it when a tick has no input. See {@link IdleInput}.
223
+ */
224
+ idle?: IdleInput<any>;
225
+
226
+ /**
227
+ * COMPILED sanitizer (see {@link SanitizeInput} / `compileSanitizer`) —
228
+ * applied in place to each decoded frame before it becomes visible to
229
+ * `latest` / the buffer / the idle ctx.
230
+ */
231
+ sanitize?: (instance: any) => void;
232
+ }
233
+
234
+ /**
235
+ * Per-client input accessor returned by `room.inputs.get(sessionId)`. Combines the
236
+ * latest decoded instance with the (optional) snapshot ring buffer.
237
+ *
238
+ * - {@link latest} — the bound Schema instance, mutated in place by the
239
+ * decoder. Cheapest read; use when only the most recent state matters.
240
+ * - {@link consume} / `for (const inp of accessor)` / {@link drain} /
241
+ * {@link next} / {@link take} / {@link peek} / {@link at} — populated when
242
+ * `defineInput()` was called with `bufferMaxSize > 0` (default 32). Use for
243
+ * rollback netcode / lockstep where every frame matters.
244
+ *
245
+ * **Per-entity vs shared world** — pick the consume primitive by who integrates:
246
+ * - **Per-entity** (each body integrates itself): iterate the accessor —
247
+ * `for (const inp of this.inputs.get(sid))` (sugar for {@link consume}) — and
248
+ * sub-integrate one per input. N inputs = N steps for that player, ack lands
249
+ * on the newest applied. The clean default. Iterating consumes ONE AT A TIME,
250
+ * so {@link renderTime} tracks each input (lag comp stays exact per step) and
251
+ * `break` leaves the rest buffered. {@link drain} returns the same set as an
252
+ * array (retainable), but reports only the newest input's {@link renderTime}.
253
+ * - **Shared world** (one solver step advances every body together): you can't
254
+ * replay N inputs as N world steps without over-stepping everyone else, so
255
+ * {@link next} exactly one input per entity per tick (or {@link take} a bounded
256
+ * few and sub-step the solver per input). The ack then === inputs actually
257
+ * simulated; `drain()`-then-apply-latest would silently jump the ack past
258
+ * inputs the server never simulated, snapping the client's reconciler.
259
+ *
260
+ * Returned for unknown sessionIds and rooms without `defineInput()` is a
261
+ * frozen no-op accessor (latest=undefined, drain/next/take/peek=[]/undefined,
262
+ * at=undefined, size=0, clear=no-op).
263
+ *
264
+ * `Idle` is `true` when the room declared `defineInput({ idle })` — it narrows
265
+ * {@link next} to non-optional `I` (bare calls always yield a frame).
266
+ */
267
+ export interface InputAccessor<I = any, Idle extends boolean = false> {
268
+ /** Latest decoded input. `undefined` when unknown sessionId or no input declared. */
269
+ readonly latest: I | undefined;
270
+
271
+ /**
272
+ * Find the buffered snapshot whose `[seqField]` equals `value`. The field
273
+ * name is the Room's `defineInput()` `seqField`. Linear scan — cheap for
274
+ * typical buffer sizes; not intended for very large rings. Returns
275
+ * `undefined` when no match is buffered (or `seqField` isn't configured).
276
+ *
277
+ * Useful for tick-aligned retrieval (lockstep, rollback).
278
+ */
279
+ at(value: number): I | undefined;
280
+
281
+ /**
282
+ * The blessed **per-entity** loop: consume each pending input oldest → newest,
283
+ * ONE AT A TIME. Unlike {@link drain} (which reports only the newest input's
284
+ * stamps up front), iterating updates {@link renderTime}/{@link reckonTime} and
285
+ * advances {@link consumedCount} per yielded input — so per-step lag comp
286
+ * rewinds to the exact instant of the input being applied:
287
+ *
288
+ * ```ts
289
+ * for (const inp of this.inputs.get(sid)) { // sugar: accessor is iterable
290
+ * applyInput(p, inp, dt);
291
+ * this.collide(sid, p); // renderTime === THIS input's stamp
292
+ * if (!p.alive) break; // break leaves the rest buffered
293
+ * }
294
+ * ```
295
+ *
296
+ * When the buffer is empty and an `idle` policy is in effect (room-level, or
297
+ * per-call `opts.idle`), yields EXACTLY ONE idle frame (not consumed: no ack
298
+ * bump, no {@link renderTime} change, {@link wasIdle} === true) then stops. No
299
+ * policy + empty → zero iterations. Unknown sessionId / `bufferMaxSize: 0` →
300
+ * empty. `for (const inp of accessor)` is `consume()` with no opts.
301
+ *
302
+ * The returned iterator is POOLED (reused per call, allocation-free). For
303
+ * `for..of` / spread / `Array.from` this is invisible. If you drive it by hand,
304
+ * iterate it once to completion (or call `.return()` / `break`) before the next
305
+ * `consume()`, and read each `.next()` result before the next — don't retain or
306
+ * compare result objects. (Nesting two `consume()` loops over the SAME channel
307
+ * is handled safely but is meaningless — both share one cursor.)
308
+ */
309
+ consume(opts?: ConsumeOptions<I>): IterableIterator<I>;
310
+
311
+ /** Iterate pending (and/or one idle) inputs — sugar for {@link consume}(). */
312
+ [Symbol.iterator](): IterableIterator<I>;
313
+
314
+ /**
315
+ * Take everything buffered (oldest → newest) and clear. Snapshots are safe to retain.
316
+ *
317
+ * With a room-level `defineInput({ idle })` policy the result is TOTAL: an
318
+ * empty tick yields one synthesized "idle" frame instead of `[]`, so the sim
319
+ * loop needs no empty-branch (gravity still integrates, action guards
320
+ * naturally no-op on default values). The synthesized frame is the schema's
321
+ * DEFAULTS overlaid with the policy's overrides (see {@link IdleInput}):
322
+ *
323
+ * ```ts
324
+ * // declared once at defineInput({ idle: (ctx) => ({ ... }) }); then simply:
325
+ * for (const f of this.inputs.get(sid).drain()) {
326
+ * stepPlayer(p, f, world); // ≥1 frame, always
327
+ * if (f.fire) tryFire(...); // idle frame: fire=false → no-op
328
+ * }
329
+ * ```
330
+ *
331
+ * Pass `{ idle }` to override the room policy for this call, or
332
+ * `{ idle: false }` to suppress it.
333
+ *
334
+ * The idle frame is NOT a consumed input: it advances neither the reconcile
335
+ * ack (`consumedCount`) nor {@link renderTime}. It is ONE reused instance per
336
+ * client, refilled on each synthesis — read it within the tick, don't store
337
+ * it. No synthesis for unknown sessionIds or `bufferMaxSize: 0`.
338
+ */
339
+ drain(opts?: ConsumeOptions<I>): I[];
340
+
341
+ /**
342
+ * Consume the single oldest buffered input and advance the reconcile ack by
343
+ * exactly one — the complement to {@link peek}. Returns `undefined`
344
+ * when the buffer is empty.
345
+ *
346
+ * Use this in a **shared-world** loop where one solver step advances every
347
+ * entity together: you can't apply N-inputs-per-player as N world steps without
348
+ * over-stepping the others, so consume one input per entity per fixed step
349
+ * instead. {@link drain} (consume all, ack jumps to newest) is correct only
350
+ * when each entity integrates ITSELF — see the {@link InputAccessor} docs.
351
+ *
352
+ * `consumedCount`/{@link renderTime} stay exact: the ack reflects only the
353
+ * inputs you actually simulated, and `renderTime` is the stamp of THIS input.
354
+ *
355
+ * With a room-level `defineInput({ idle })` policy (or a per-call `{ idle }`
356
+ * override) an empty tick returns a synthesized frame instead of `undefined`
357
+ * — same contract as {@link drain}'s. A "held key keeps moving through a
358
+ * packet gap" loop:
359
+ *
360
+ * @example
361
+ * ```ts
362
+ * // declared once:
363
+ * inputs = this.defineInput(InputSchema, {
364
+ * idle: ({ latest }) => latest ?? true, // empty tick → last input verbatim
365
+ * });
366
+ * // per tick:
367
+ * this.setFixedTimestep(() => {
368
+ * for (const [sid, body] of bodies) {
369
+ * applyInputToBody(body, this.inputs.get(sid).next()); // typed I — never undefined
370
+ * }
371
+ * world.step(); // one step for everyone
372
+ * }, 30);
373
+ * ```
374
+ */
375
+ next(): Idle extends true ? I : I | undefined;
376
+ next(opts: { idle: IdleInput<I> }): I;
377
+ next(opts?: ConsumeOptions<I>): I | undefined;
378
+
379
+ /**
380
+ * Consume up to `n` oldest buffered inputs (oldest → newest) and advance the
381
+ * reconcile ack by the count actually taken. Returns fewer than `n` (or `[]`)
382
+ * when the buffer holds fewer. Use for shared-world **sub-stepping** — apply
383
+ * each taken input as its own solver sub-step within one tick, bounding the
384
+ * sub-steps per tick. {@link next} is the `take(1)` shorthand.
385
+ */
386
+ take(n: number): I[];
387
+
388
+ /** Read everything buffered without consuming. */
389
+ peek(): I[];
390
+
391
+ /** Number of snapshots currently buffered. */
392
+ readonly size: number;
393
+
394
+ /**
395
+ * Cumulative count of this client's inputs CONSUMED so far — the reconcile ack
396
+ * echoed back to the client (its `lastProcessed`). Advances by one per
397
+ * {@link next}, by the count taken per {@link take}/{@link drain}, by one per
398
+ * yield while iterating {@link consume}, and on overflow-drop; a synthesized
399
+ * idle frame NEVER advances it. Monotonic, so it doubles as a server-side input
400
+ * seq — gate per-input actions off it (e.g. a fire cooldown counted in inputs)
401
+ * instead of hand-rolling a counter. `0` before the first consume / for the
402
+ * no-op accessor.
403
+ */
404
+ readonly consumedCount: number;
405
+
406
+ /**
407
+ * `true` when the most recently produced frame was a synthesized idle (vs a
408
+ * real consumed input) — the "skip work on idle" guard. Set per consume
409
+ * call/yield: `true` after a {@link next}/{@link consume}/{@link drain} that
410
+ * fell back to the `idle` policy on an empty buffer, `false` after a real
411
+ * input. Always `false` for {@link take} (never synthesizes) and the no-op
412
+ * accessor. Reads meaningfully right after the consume that produced the frame.
413
+ */
414
+ readonly wasIdle: boolean;
415
+
416
+ /** Drop all buffered snapshots (also resets the dedupe tracker). */
417
+ clear(): void;
418
+
419
+ /**
420
+ * Server-clock render timestamp (ms since room start) of the most recently
421
+ * consumed input — the time the client was rendering the world at when it
422
+ * issued that input. Tracks the consume primitive: {@link drain}/{@link take}
423
+ * report the NEWEST input they consumed; {@link next} and each
424
+ * {@link consume}/`for..of` yield report THAT one input, so per-input loops
425
+ * rewind to the exact instant of the input simulated.
426
+ * `0` until the first render-time-stamped input is consumed. Populated only
427
+ * when the room rewinds a `mode:"snapshot"` group (which auto-enables the
428
+ * renderTime stamp) and `bufferMaxSize > 0`. Usually you don't read this
429
+ * directly — pass the
430
+ * sessionId to `rewind.lastSeenBy(sessionId)` (which resolves this value, clamps
431
+ * it, and falls back to live) for lag-compensated "what you see is what you
432
+ * hit" hit registration; use this getter only for a custom rewind time.
433
+ */
434
+ readonly renderTime: number;
435
+
436
+ /**
437
+ * Reckon-display stamp (server-clock ms) of the most recently consumed input
438
+ * — the client's serverNow ESTIMATE when it sampled that input, i.e. the
439
+ * exact instant its forward-reckoned (`mode:"reckon"`) entities were
440
+ * displayed at. Stamping the display instant DIRECTLY makes the rewind read
441
+ * immune to the client's RTT-estimation error: the client displayed
442
+ * `f(serverNow_est)` and the server reads `f(serverNow_est)` — the same
443
+ * index into the same recorded timeline, so estimation error cancels (the
444
+ * `maxRewindMs` clamp still bounds spoofing). Same consume semantics as
445
+ * {@link renderTime}; consumed automatically by `rewind.lastSeenBy()` —
446
+ * rarely read directly.
447
+ *
448
+ * Always a usable instant: when no reckon-stamped input has been consumed
449
+ * yet (lag-comp off, client clock unsynced, nothing consumed), resolves to
450
+ * the room clock's current `elapsedTime` — the fallback rooms previously
451
+ * hand-wrote as `reckonTime > 0 ? reckonTime : now`. `rewind.lastSeenBy()`
452
+ * internally reads the raw stamp, so its midpoint reconstruction still
453
+ * engages for unstamped clients. The no-op accessor (unknown session / no
454
+ * `defineInput()`) returns `0`.
455
+ */
456
+ readonly reckonTime: number;
457
+ }
458
+
459
+ /**
460
+ * Returned by `Room.defineInput()`. Assign it to `this.inputs`, then call
461
+ * `room.inputs.get(sessionId)` per tick to read each client's input stream —
462
+ * the same per-session lookup verb as `room.clients.get(sessionId)`.
463
+ *
464
+ * The fixed-step metadata lives HERE (one per room), not on the per-client
465
+ * {@link InputAccessor} — these values are identical for every client, so they'd
466
+ * be pure duplication per connection. `.get()` keeps the accessor and this
467
+ * metadata cleanly separated.
468
+ */
469
+ export type InputAPI<I = any, Idle extends boolean = false> = {
470
+ /**
471
+ * The input accessor for `sessionId` — the per-client stream you iterate /
472
+ * `.next()` / `.drain()`. Returns a frozen no-op accessor for unknown sessions.
473
+ * Mirrors `room.clients.get(sessionId)`.
474
+ */
475
+ get(sessionId: string): InputAccessor<I, Idle>;
476
+ /**
477
+ * Server-advertised fixed step rate in **Hz** — from `defineInput`'s
478
+ * `tickRate`/`stepMs`/`stepSeconds`, or derived from `setTimestep` —
479
+ * the same value cascaded to predicting clients. `undefined` when no fixed
480
+ * step is advertised.
481
+ */
482
+ readonly tickRate?: number;
483
+ /**
484
+ * The fixed step as **seconds** (`1/tickRate`): the dt to integrate one input
485
+ * with, bit-identical to the client's prediction dt. Pass it to your physics
486
+ * step (`applyInput(p, cmd, level, room.inputs.stepSeconds)`) so server and
487
+ * client share one timestep instead of each keeping a constant that can drift.
488
+ * `undefined` when no rate is advertised.
489
+ */
490
+ readonly stepSeconds?: number;
491
+ /** The fixed step as **milliseconds** (`1000/tickRate`). `undefined` when no rate. */
492
+ readonly stepMs?: number;
493
+ /** Physics sub-steps per input tick (≥ 1; `1` unless declared via
494
+ * `setFixedTimestep(..., { subSteps })` / `defineInput`). */
495
+ readonly subSteps: number;
496
+ /** The physics sub-step as **seconds** (`stepSeconds / subSteps`) — the engine
497
+ * dt when sub-stepping; equals {@link stepSeconds} when `subSteps` is 1.
498
+ * `undefined` when no rate is advertised. */
499
+ readonly subStepSeconds?: number;
500
+ /** The physics sub-step as **milliseconds** (`stepMs / subSteps`). `undefined`
501
+ * when no rate is advertised. */
502
+ readonly subStepMs?: number;
503
+ };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Framework-internal entry — symbols used by the bundled `colyseus`
3
+ * plugins (track-user-sessions, unique-session) and the admin
4
+ * backend, but NOT part of `@colyseus/core`'s public surface.
5
+ *
6
+ * Third parties should reach for `TrackUserSessionsPlugin` and its
7
+ * `listUserSessions` static instead — the raw helpers below are
8
+ * implementation details and can change between minor versions.
9
+ *
10
+ * Resolved via the `./*` wildcard in `packages/core/package.json`.
11
+ *
12
+ * @internal
13
+ */
14
+ import * as matchMaker from './MatchMaker.ts';
15
+ import {
16
+ listUserSessions,
17
+ type ListUserSessionsOptions,
18
+ type UserSessionInfo,
19
+ } from './utils/UserSessionIndex.ts';
20
+
21
+ export {
22
+ userRoomsKey,
23
+ USER_ROOMS_KEY_PREFIX,
24
+ trackUserSession,
25
+ releaseUserSession,
26
+ trackRoomJoin,
27
+ releaseRoomLeave,
28
+ sweepRoomDispose,
29
+ listUserSessions,
30
+ type UserRoomEntry,
31
+ type UserSessionInfo,
32
+ type ListUserSessionsOptions,
33
+ } from './utils/UserSessionIndex.ts';
34
+
35
+ /**
36
+ * `listUserSessions` wired to the live `matchMaker` — what
37
+ * `TrackUserSessionsPlugin.listUserSessions` and the admin backend
38
+ * actually call. The pure `listUserSessions` is kept exported above
39
+ * so unit tests can drive it with fake presence + findRooms.
40
+ */
41
+ export function listUserSessionsLive(
42
+ userId: string,
43
+ options?: ListUserSessionsOptions,
44
+ ): Promise<UserSessionInfo[]> {
45
+ return listUserSessions(matchMaker.presence, matchMaker.findRoomsByIds, userId, options);
46
+ }
@@ -40,6 +40,16 @@ export class LocalDriver implements MatchMakerDriver {
40
40
  return query as unknown as Promise<IRoomCache>;
41
41
  }
42
42
 
43
+ public async findByIds(roomIds: string[]): Promise<Map<string, IRoomCache>> {
44
+ const result = new Map<string, IRoomCache>();
45
+ if (roomIds.length === 0) { return result; }
46
+ const wanted = new Set(roomIds);
47
+ for (const room of this.rooms) {
48
+ if (wanted.has(room.roomId)) { result.set(room.roomId, room); }
49
+ }
50
+ return result;
51
+ }
52
+
43
53
  public update(room: IRoomCache, operations: Partial<{ $set: Partial<IRoomCache>, $inc: Partial<IRoomCache> }>) {
44
54
  if (operations.$set) {
45
55
  for (const field in operations.$set) {
@@ -147,6 +147,19 @@ export interface MatchMakerDriver {
147
147
  sortOptions?: SortOptions
148
148
  ): Promise<IRoomCache<ExtractRoomCacheMetadata<T>>>;
149
149
 
150
+ /**
151
+ * Batch-resolve room caches by roomId in a single backend round
152
+ * trip. Missing roomIds are absent from the returned map. Used by
153
+ * hot paths (per-join uniqueness check, by-user reverse-index
154
+ * lookups) where K serial `findOne` calls would multiply round
155
+ * trips needlessly.
156
+ *
157
+ * @param roomIds - Room ids to look up.
158
+ *
159
+ * @returns Map keyed by roomId of the rooms that were found.
160
+ */
161
+ findByIds(roomIds: string[]): Promise<Map<string, IRoomCache>>;
162
+
150
163
  /**
151
164
  * Remove a room from room cache.
152
165
  *
@@ -31,25 +31,29 @@ export interface LobbyOptions {
31
31
  filter?: FilterInput;
32
32
  }
33
33
 
34
+ export type LobbyMessages = {
35
+ filter: (client: LobbyClient, filter: FilterInput) => void;
36
+ };
37
+
34
38
  export class LobbyRoom<Metadata = any> extends Room {
35
39
  public rooms: IRoomCache<Metadata>[] = [];
36
40
  public unsubscribeLobby: () => void;
37
41
 
38
42
  public clientOptions: { [sessionId: string]: LobbyOptions } = {};
39
43
 
40
- messages = {
41
- filter: (client: LobbyClient, filter: FilterInput) => {
44
+ declare messages: LobbyMessages;
45
+
46
+ public async onCreate(options: any) {
47
+ // prevent LobbyRoom to notify itself
48
+ this['_listing'].unlisted = true;
49
+
50
+ this.onMessage('filter', (client, filter) => {
42
51
  const clientOptions = this.clientOptions[client.sessionId];
43
52
  if (!clientOptions) { return; }
44
53
 
45
54
  clientOptions.filter = filter;
46
55
  client.send('rooms', this.filterItemsForClient(clientOptions));
47
- }
48
- }
49
-
50
- public async onCreate(options: any) {
51
- // prevent LobbyRoom to notify itself
52
- this['_listing'].unlisted = true;
56
+ });
53
57
 
54
58
  this.unsubscribeLobby = await subscribeLobby((roomId, data) => {
55
59
  const roomIndex = this.rooms.findIndex((room) => room.roomId === roomId);
@@ -202,7 +202,7 @@ export class QueueRoom extends Room {
202
202
  /**
203
203
  * Redistribute clients into groups at every interval
204
204
  */
205
- this.setSimulationInterval(() => this.reassignMatchGroups(), this.cycleTickInterval);
205
+ this.setTimestep(() => this.reassignMatchGroups(), this.cycleTickInterval);
206
206
  }
207
207
 
208
208
  onJoin(client: QueueClient, options: any, auth?: unknown) {
@@ -1,26 +1,20 @@
1
1
  import { CloseCode } from '@colyseus/shared-types';
2
- import { defineTypes, MapSchema, Schema } from '@colyseus/schema';
2
+ import { schema, t, type SchemaType } from '@colyseus/schema';
3
3
 
4
4
  import { Room } from '../Room.ts';
5
5
  import type { Client } from '../Transport.ts';
6
6
 
7
- class Player extends Schema {
8
- public connected: boolean;
9
- public name: string;
10
- public sessionId: string;
11
- }
12
- defineTypes(Player, {
13
- connected: 'boolean',
14
- name: 'string',
15
- sessionId: 'string',
7
+ export const Player = schema({
8
+ connected: t.boolean(),
9
+ name: t.string(),
10
+ sessionId: t.string(),
16
11
  });
12
+ export type Player = SchemaType<typeof Player>;
17
13
 
18
- class State extends Schema {
19
- public players = new MapSchema<Player>();
20
- }
21
- defineTypes(State, {
22
- players: { map: Player },
14
+ export const State = schema({
15
+ players: t.map(Player),
23
16
  });
17
+ export type State = SchemaType<typeof State>;
24
18
 
25
19
  /**
26
20
  * client.joinOrCreate("relayroom", {