@colyseus/core 0.18.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/build/MatchMaker.cjs +3 -7
  2. package/build/MatchMaker.cjs.map +2 -2
  3. package/build/MatchMaker.mjs +4 -8
  4. package/build/MatchMaker.mjs.map +2 -2
  5. package/build/Protocol.cjs +3 -3
  6. package/build/Protocol.cjs.map +1 -1
  7. package/build/Protocol.d.ts +3 -3
  8. package/build/Protocol.mjs +3 -3
  9. package/build/Protocol.mjs.map +1 -1
  10. package/build/Rewind.cjs +385 -0
  11. package/build/Rewind.cjs.map +7 -0
  12. package/build/Rewind.d.ts +291 -0
  13. package/build/Rewind.mjs +359 -0
  14. package/build/Rewind.mjs.map +7 -0
  15. package/build/Room.cjs +280 -281
  16. package/build/Room.cjs.map +3 -3
  17. package/build/Room.d.ts +185 -81
  18. package/build/Room.mjs +286 -286
  19. package/build/Room.mjs.map +2 -2
  20. package/build/RoomMessages.cjs +265 -0
  21. package/build/RoomMessages.cjs.map +7 -0
  22. package/build/RoomMessages.d.ts +49 -0
  23. package/build/RoomMessages.mjs +240 -0
  24. package/build/RoomMessages.mjs.map +7 -0
  25. package/build/Server.cjs +88 -31
  26. package/build/Server.cjs.map +2 -2
  27. package/build/Server.d.ts +36 -0
  28. package/build/Server.mjs +88 -31
  29. package/build/Server.mjs.map +2 -2
  30. package/build/Transport.cjs +26 -0
  31. package/build/Transport.cjs.map +2 -2
  32. package/build/Transport.d.ts +69 -2
  33. package/build/Transport.mjs +25 -0
  34. package/build/Transport.mjs.map +2 -2
  35. package/build/errors/RoomExceptions.cjs +7 -4
  36. package/build/errors/RoomExceptions.cjs.map +2 -2
  37. package/build/errors/RoomExceptions.d.ts +15 -3
  38. package/build/errors/RoomExceptions.mjs +5 -3
  39. package/build/errors/RoomExceptions.mjs.map +2 -2
  40. package/build/index.cjs +19 -2
  41. package/build/index.cjs.map +2 -2
  42. package/build/index.d.ts +7 -4
  43. package/build/index.mjs +17 -4
  44. package/build/index.mjs.map +2 -2
  45. package/build/input/InputBuffer.cjs +439 -16
  46. package/build/input/InputBuffer.cjs.map +2 -2
  47. package/build/input/InputBuffer.d.ts +186 -96
  48. package/build/input/InputBuffer.mjs +436 -15
  49. package/build/input/InputBuffer.mjs.map +2 -2
  50. package/build/input/RoomInput.cjs +306 -0
  51. package/build/input/RoomInput.cjs.map +7 -0
  52. package/build/input/RoomInput.d.ts +91 -0
  53. package/build/input/RoomInput.mjs +287 -0
  54. package/build/input/RoomInput.mjs.map +7 -0
  55. package/build/input/types.cjs +18 -0
  56. package/build/input/types.cjs.map +7 -0
  57. package/build/input/types.d.ts +476 -0
  58. package/build/input/types.mjs +0 -0
  59. package/build/input/types.mjs.map +7 -0
  60. package/build/rooms/QueueRoom.cjs +1 -1
  61. package/build/rooms/QueueRoom.cjs.map +2 -2
  62. package/build/rooms/QueueRoom.mjs +1 -1
  63. package/build/rooms/QueueRoom.mjs.map +2 -2
  64. package/build/router/default_routes.cjs +4 -1
  65. package/build/router/default_routes.cjs.map +2 -2
  66. package/build/router/default_routes.mjs +4 -1
  67. package/build/router/default_routes.mjs.map +2 -2
  68. package/build/router/index.cjs +6 -1
  69. package/build/router/index.cjs.map +2 -2
  70. package/build/router/index.mjs +6 -1
  71. package/build/router/index.mjs.map +2 -2
  72. package/build/router/node.cjs +37 -2
  73. package/build/router/node.cjs.map +2 -2
  74. package/build/router/node.d.ts +10 -0
  75. package/build/router/node.mjs +35 -1
  76. package/build/router/node.mjs.map +2 -2
  77. package/build/serializer/NoneSerializer.cjs +2 -2
  78. package/build/serializer/NoneSerializer.cjs.map +2 -2
  79. package/build/serializer/NoneSerializer.d.ts +3 -3
  80. package/build/serializer/NoneSerializer.mjs +2 -2
  81. package/build/serializer/NoneSerializer.mjs.map +2 -2
  82. package/build/serializer/SchemaSerializer.cjs +44 -14
  83. package/build/serializer/SchemaSerializer.cjs.map +2 -2
  84. package/build/serializer/SchemaSerializer.d.ts +3 -3
  85. package/build/serializer/SchemaSerializer.mjs +46 -16
  86. package/build/serializer/SchemaSerializer.mjs.map +2 -2
  87. package/build/serializer/Serializer.cjs.map +1 -1
  88. package/build/serializer/Serializer.d.ts +12 -2
  89. package/build/utils/DevMode.cjs +14 -14
  90. package/build/utils/DevMode.cjs.map +2 -2
  91. package/build/utils/DevMode.d.ts +0 -2
  92. package/build/utils/DevMode.mjs +14 -12
  93. package/build/utils/DevMode.mjs.map +2 -2
  94. package/build/utils/Env.cjs +8 -4
  95. package/build/utils/Env.cjs.map +3 -3
  96. package/build/utils/Env.mjs +8 -4
  97. package/build/utils/Env.mjs.map +2 -2
  98. package/build/utils/Utils.cjs +3 -0
  99. package/build/utils/Utils.cjs.map +2 -2
  100. package/build/utils/Utils.d.ts +8 -0
  101. package/build/utils/Utils.mjs +2 -0
  102. package/build/utils/Utils.mjs.map +2 -2
  103. package/package.json +11 -11
  104. package/src/MatchMaker.ts +7 -9
  105. package/src/Protocol.ts +3 -3
  106. package/src/Rewind.ts +572 -0
  107. package/src/Room.ts +409 -448
  108. package/src/RoomMessages.ts +342 -0
  109. package/src/Server.ts +112 -38
  110. package/src/Transport.ts +97 -2
  111. package/src/errors/RoomExceptions.ts +18 -4
  112. package/src/index.ts +13 -2
  113. package/src/input/InputBuffer.ts +464 -107
  114. package/src/input/RoomInput.ts +337 -0
  115. package/src/input/types.ts +503 -0
  116. package/src/rooms/QueueRoom.ts +1 -1
  117. package/src/router/default_routes.ts +6 -1
  118. package/src/router/index.ts +6 -1
  119. package/src/router/node.ts +44 -0
  120. package/src/serializer/NoneSerializer.ts +3 -3
  121. package/src/serializer/SchemaSerializer.ts +110 -19
  122. package/src/serializer/Serializer.ts +13 -2
  123. package/src/utils/DevMode.ts +18 -13
  124. package/src/utils/Env.ts +12 -4
  125. package/src/utils/Utils.ts +9 -0
@@ -0,0 +1,476 @@
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
+ * Names of fields on `I` whose values are `number` — used by
9
+ * `Room.defineInput()` to constrain `seqField` to actually-numeric fields
10
+ * on the input schema. Filters out booleans, strings, methods, etc.
11
+ */
12
+ export type NumericFieldsOf<I> = {
13
+ [K in keyof I]: I[K] extends number ? (K extends string ? K : never) : never;
14
+ }[keyof I];
15
+ /**
16
+ * Context handed to the `idle` callback form — see {@link IdleInput}. A reused
17
+ * per-client scratch: read it inside the callback, don't store it. It carries
18
+ * MECHANISM only — derived judgments (e.g. liveness) stay in userland; look the
19
+ * client up via `this.clients.get(ctx.sessionId)` when your policy needs it.
20
+ */
21
+ export interface IdleContext<I> {
22
+ /** Last decoded input (`undefined` before the client's first). */
23
+ latest: I | undefined;
24
+ sessionId: string;
25
+ }
26
+ /**
27
+ * What to synthesize when the buffer is empty (declared room-wide at
28
+ * `defineInput({ idle })`, or per-call via {@link ConsumeOptions.idle}):
29
+ * - `true` — pure schema defaults.
30
+ * - `Partial<I>` — defaults overlaid with these fields. A full schema instance
31
+ * (e.g. {@link InputAccessor.latest}) also works — fields are copied BY NAME,
32
+ * so its prototype accessors are read correctly.
33
+ * - callback — invoked LAZILY (only on an actually-empty tick) with an
34
+ * {@link IdleContext}, returning either of the above. The right form when the
35
+ * overrides take work to compute (entity lookups, held-button carry-over).
36
+ *
37
+ * ⚠ Do NOT build overrides by spreading a schema instance (`{ ...latest, x }`):
38
+ * schema fields live on the prototype, so the spread copies NOTHING of them.
39
+ * Return `latest` itself, or name the fields (`{ x, plant: !!latest?.plant }`).
40
+ */
41
+ export type IdleInput<I> = true | Partial<I> | ((ctx: IdleContext<I>) => true | Partial<I>);
42
+ /**
43
+ * Options for {@link InputAccessor.drain} / {@link InputAccessor.next}.
44
+ * `idle` overrides the room-level `defineInput({ idle })` policy for this call
45
+ * (see {@link IdleInput}); pass `false` to suppress it (force skip behavior).
46
+ */
47
+ export interface ConsumeOptions<I> {
48
+ idle?: IdleInput<I> | false;
49
+ }
50
+ /**
51
+ * Input sanitization, declared at `defineInput({ sanitize })` — never trust the
52
+ * wire. Applied to each decoded frame IN PLACE, before anything reads it
53
+ * (`latest`, the buffer, the `idle` callback's ctx):
54
+ * - **Map form** — per-field `[min, max]` range clamps with NaN-safe semantics:
55
+ * `NaN` (both comparisons false) lands on `min`, closing the classic
56
+ * `Math.min(NaN, …)` poisoning hole.
57
+ * - **Callback form** — arbitrary in-place fix-up (wrap an angle, enforce a
58
+ * cross-field rule) for anything beyond ranges.
59
+ *
60
+ * Sanitizers MODIFY, they never reject — a malformed value becomes a legal one
61
+ * instead of dropping the frame. Must not touch the `seqField` (it runs before
62
+ * dedupe). Semantic validation ("slot must name an owned weapon") stays in
63
+ * your sim — this handles value domains, not game rules.
64
+ */
65
+ export type SanitizeInput<I> = Partial<Record<NumericFieldsOf<I>, readonly [number, number]>> | ((input: I) => void);
66
+ /** Options for `Room.defineInput()`. The user-facing twin of the internal
67
+ * normalized {@link NormalizedInputOptions}. */
68
+ export interface DefineInputOptions<I = any> {
69
+ seqField?: NumericFieldsOf<I>;
70
+ bufferMaxSize?: number;
71
+ /**
72
+ * Input sanitization — never trust the wire. Applied IN PLACE to every
73
+ * decoded frame before anything reads it (`latest`, the buffer, the `idle`
74
+ * ctx). Map form = per-field `[min, max]` clamps with NaN-safe semantics
75
+ * (NaN → min — closes the `Math.min(NaN, …)` poisoning hole); callback form
76
+ * = arbitrary in-place fix-up. Sanitizers MODIFY, never reject. See
77
+ * {@link SanitizeInput}.
78
+ *
79
+ * ```ts
80
+ * sanitize: { moveF: [-1, 1], pitch: [-PITCH_LIMIT, PITCH_LIMIT], dt: [0, MAX_DT] },
81
+ * // or: sanitize: (f) => { f.angle = wrapAngle(f.angle); },
82
+ * ```
83
+ */
84
+ sanitize?: SanitizeInput<I>;
85
+ /**
86
+ * Room-level absence policy: when a tick has no buffered input, bare
87
+ * `drain()` / `next()` synthesize ONE "idle" frame from it — the schema's
88
+ * defaults overlaid with the policy's overrides — so the sim loop needs no
89
+ * empty-branch. Prefer the callback form, invoked lazily (only on
90
+ * actually-empty ticks) with an {@link IdleContext} (`latest` + `sessionId`):
91
+ *
92
+ * ```ts
93
+ * idle: ({ latest, sessionId }) => {
94
+ * const p = this.state.players.get(sessionId); // closes over the room
95
+ * return p ? { yaw: p.yaw, plant: !!latest?.plant } : true;
96
+ * }
97
+ * ```
98
+ *
99
+ * Not declaring it keeps the skip behavior (`drain()` → `[]`). Per-call
100
+ * `{ idle }` overrides this default; `{ idle: false }` suppresses it.
101
+ * Synthesized frames never advance the reconcile ack or `renderTime`.
102
+ */
103
+ idle?: IdleInput<I>;
104
+ /**
105
+ * Fixed step rate in **Hz** cascaded to the client via the join handshake;
106
+ * it predicts at dt = 1/tickRate for deterministic rollback. Defaults to
107
+ * the `setTimestep` rate — pass this only when the prediction
108
+ * step differs from it. NOTE: a *rate*, not an interval — `1000/30` is the
109
+ * step in ms, a classic mistake; use {@link stepMs} for that. Superseded by
110
+ * {@link stepMs} / {@link stepSeconds} when those are given.
111
+ */
112
+ tickRate?: number;
113
+ /**
114
+ * Fixed step as a duration in **milliseconds** — the unit-safe alternative
115
+ * to {@link tickRate} (`stepMs: 1000/30` is unambiguous where
116
+ * `tickRate: 1000/30` is a bug). Normalized to the canonical Hz value
117
+ * (`1000/stepMs`). Takes precedence over `tickRate`.
118
+ */
119
+ stepMs?: number;
120
+ /**
121
+ * Fixed step as a duration in **seconds** (e.g. `1/30`). Normalized to the
122
+ * canonical Hz value (`1/stepSeconds`). Highest precedence.
123
+ */
124
+ stepSeconds?: number;
125
+ /**
126
+ * Physics sub-steps per input tick (integer ≥ 1, default 1) — decouples the
127
+ * PHYSICS rate from the input/network rate. One input still drives exactly
128
+ * one fixed step (the replay invariant), but the simulation integrates
129
+ * `subSteps` engine steps of `stepSeconds/subSteps` inside it, identically
130
+ * on client and server — physics at `tickRate * subSteps` Hz while sending
131
+ * `tickRate` inputs/sec. Cascaded to clients via the join handshake;
132
+ * both sides read the derived numbers off their step context
133
+ * (`ctx.subSteps` / `ctx.subDt`) so N and dt can't drift apart.
134
+ * Usually declared via {@link Room.setFixedTimestep}'s `subSteps` option
135
+ * instead — pass it here only when the room runs its own loop.
136
+ */
137
+ subSteps?: number;
138
+ }
139
+ /** `true` when the defineInput opts declared an `idle` policy — narrows the
140
+ * returned {@link InputAPI} so bare `next()` types non-optional `I`.
141
+ * @internal — exported for {@link RoomInput.define}. */
142
+ export type IdleDeclared<O, I> = O extends {
143
+ idle: IdleInput<I>;
144
+ } ? true : false;
145
+ /**
146
+ * Internal: input configuration captured by `Room.defineInput()` — the
147
+ * normalized form of {@link DefineInputOptions} (step declarations resolved to
148
+ * Hz, sanitizer compiled). The schema constructor is stored here so the
149
+ * runtime doesn't need to know it through the public `room.inputs` (the
150
+ * `.get(sessionId)` accessor).
151
+ *
152
+ * @internal
153
+ */
154
+ export interface NormalizedInputOptions {
155
+ /**
156
+ * Schema constructor used to allocate per-client input instances on join.
157
+ * Captured by `defineInput()` from its `type` argument.
158
+ *
159
+ * Typed loosely (`new () => any`) to sidestep type-identity issues across
160
+ * duplicate `@colyseus/schema` installs; the runtime calls
161
+ * `instance.clone()` and friends, which match by shape.
162
+ */
163
+ ctor: new () => any;
164
+ /**
165
+ * Name of a monotonically-increasing numeric field on the input schema used
166
+ * to order and dedupe incoming frames. Unset by default (opt-in) — dedupe and
167
+ * `.at()` lookup are off unless you name a field here. When set, the framework:
168
+ * - Drops redundant frames (`input[seqField]` ≤ the last-seen value are
169
+ * discarded before they enter the buffer) — the unreliable-mode
170
+ * ring-redundancy pattern.
171
+ * - Powers `room.inputs.get(sessionId).at(value)` lookups.
172
+ *
173
+ * Despite the name, "seq" here is broader than an integer counter — any
174
+ * monotonic numeric field works:
175
+ * - **Sequence counter** (`"seq"`, `"tick"`, `"frame"`) — typical for
176
+ * lockstep / rollback netcode.
177
+ * - **Timestamp** (`"timestamp"`, milliseconds or seconds) — useful for
178
+ * variable-rate clients, lag compensation, hit registration (a float-seconds
179
+ * timestamp stamped on each saved move is the common shape).
180
+ *
181
+ * Whichever you use, the field must increase monotonically across frames
182
+ * for dedupe to work.
183
+ */
184
+ seqField?: string;
185
+ /**
186
+ * > 0 enables per-client buffering of cloned snapshots — required for
187
+ * `room.inputs.get(sessionId).drain() / .peek() / .at()` to return populated
188
+ * data. Oldest drops on overflow. Set to `0` to disable (`.latest` still
189
+ * works).
190
+ */
191
+ bufferMaxSize: number;
192
+ /**
193
+ * Fixed step rate (Hz) advertised to clients via the join handshake; they
194
+ * predict at dt = 1/tickRate. Set explicitly via `defineInput`, or derived
195
+ * from `setTimestep`. Unset = not advertised.
196
+ */
197
+ tickRate?: number;
198
+ /**
199
+ * Physics sub-steps per input tick (integer ≥ 1) advertised to clients via
200
+ * the join handshake — the simulation integrates `subSteps` engine steps of
201
+ * `(1/tickRate)/subSteps` per input on BOTH sides, so physics runs at
202
+ * `tickRate * subSteps` Hz on a `tickRate` input/network rate. Set via
203
+ * `setFixedTimestep(..., { subSteps })` (or `defineInput`). Unset/1 = not
204
+ * advertised (input rate == physics rate).
205
+ */
206
+ subSteps?: number;
207
+ /**
208
+ * Room-level absence policy: bare `drain()` / `next()` synthesize one idle
209
+ * frame from it when a tick has no input. See {@link IdleInput}.
210
+ */
211
+ idle?: IdleInput<any>;
212
+ /**
213
+ * COMPILED sanitizer (see {@link SanitizeInput} / `compileSanitizer`) —
214
+ * applied in place to each decoded frame before it becomes visible to
215
+ * `latest` / the buffer / the idle ctx.
216
+ */
217
+ sanitize?: (instance: any) => void;
218
+ }
219
+ /**
220
+ * Per-client input accessor returned by `room.inputs.get(sessionId)`. Combines the
221
+ * latest decoded instance with the (optional) snapshot ring buffer.
222
+ *
223
+ * - {@link latest} — the bound Schema instance, mutated in place by the
224
+ * decoder. Cheapest read; use when only the most recent state matters.
225
+ * - {@link consume} / `for (const inp of accessor)` / {@link drain} /
226
+ * {@link next} / {@link take} / {@link peek} / {@link at} — populated when
227
+ * `defineInput()` was called with `bufferMaxSize > 0` (default 32). Use for
228
+ * rollback netcode / lockstep where every frame matters.
229
+ *
230
+ * **Per-entity vs shared world** — pick the consume primitive by who integrates:
231
+ * - **Per-entity** (each body integrates itself): iterate the accessor —
232
+ * `for (const inp of this.inputs.get(sid))` (sugar for {@link consume}) — and
233
+ * sub-integrate one per input. N inputs = N steps for that player, ack lands
234
+ * on the newest applied. The clean default. Iterating consumes ONE AT A TIME,
235
+ * so {@link renderTime} tracks each input (lag comp stays exact per step) and
236
+ * `break` leaves the rest buffered. {@link drain} returns the same set as an
237
+ * array (retainable), but reports only the newest input's {@link renderTime}.
238
+ * - **Shared world** (one solver step advances every body together): you can't
239
+ * replay N inputs as N world steps without over-stepping everyone else, so
240
+ * {@link next} exactly one input per entity per tick (or {@link take} a bounded
241
+ * few and sub-step the solver per input). The ack then === inputs actually
242
+ * simulated; `drain()`-then-apply-latest would silently jump the ack past
243
+ * inputs the server never simulated, snapping the client's reconciler.
244
+ *
245
+ * Returned for unknown sessionIds and rooms without `defineInput()` is a
246
+ * frozen no-op accessor (latest=undefined, drain/next/take/peek=[]/undefined,
247
+ * at=undefined, size=0, clear=no-op).
248
+ *
249
+ * `Idle` is `true` when the room declared `defineInput({ idle })` — it narrows
250
+ * {@link next} to non-optional `I` (bare calls always yield a frame).
251
+ */
252
+ export interface InputAccessor<I = any, Idle extends boolean = false> {
253
+ /** Latest decoded input. `undefined` when unknown sessionId or no input declared. */
254
+ readonly latest: I | undefined;
255
+ /**
256
+ * Find the buffered snapshot whose `[seqField]` equals `value`. The field
257
+ * name is the Room's `defineInput()` `seqField`. Linear scan — cheap for
258
+ * typical buffer sizes; not intended for very large rings. Returns
259
+ * `undefined` when no match is buffered (or `seqField` isn't configured).
260
+ *
261
+ * Useful for tick-aligned retrieval (lockstep, rollback).
262
+ */
263
+ at(value: number): I | undefined;
264
+ /**
265
+ * The blessed **per-entity** loop: consume each pending input oldest → newest,
266
+ * ONE AT A TIME. Unlike {@link drain} (which reports only the newest input's
267
+ * stamps up front), iterating updates {@link renderTime}/{@link reckonTime} and
268
+ * advances {@link consumedCount} per yielded input — so per-step lag comp
269
+ * rewinds to the exact instant of the input being applied:
270
+ *
271
+ * ```ts
272
+ * for (const inp of this.inputs.get(sid)) { // sugar: accessor is iterable
273
+ * applyInput(p, inp, dt);
274
+ * this.collide(sid, p); // renderTime === THIS input's stamp
275
+ * if (!p.alive) break; // break leaves the rest buffered
276
+ * }
277
+ * ```
278
+ *
279
+ * When the buffer is empty and an `idle` policy is in effect (room-level, or
280
+ * per-call `opts.idle`), yields EXACTLY ONE idle frame (not consumed: no ack
281
+ * bump, no {@link renderTime} change, {@link wasIdle} === true) then stops. No
282
+ * policy + empty → zero iterations. Unknown sessionId / `bufferMaxSize: 0` →
283
+ * empty. `for (const inp of accessor)` is `consume()` with no opts.
284
+ *
285
+ * The returned iterator is POOLED (reused per call, allocation-free). For
286
+ * `for..of` / spread / `Array.from` this is invisible. If you drive it by hand,
287
+ * iterate it once to completion (or call `.return()` / `break`) before the next
288
+ * `consume()`, and read each `.next()` result before the next — don't retain or
289
+ * compare result objects. (Nesting two `consume()` loops over the SAME channel
290
+ * is handled safely but is meaningless — both share one cursor.)
291
+ */
292
+ consume(opts?: ConsumeOptions<I>): IterableIterator<I>;
293
+ /** Iterate pending (and/or one idle) inputs — sugar for {@link consume}(). */
294
+ [Symbol.iterator](): IterableIterator<I>;
295
+ /**
296
+ * Take everything buffered (oldest → newest) and clear. Snapshots are safe to retain.
297
+ *
298
+ * With a room-level `defineInput({ idle })` policy the result is TOTAL: an
299
+ * empty tick yields one synthesized "idle" frame instead of `[]`, so the sim
300
+ * loop needs no empty-branch (gravity still integrates, action guards
301
+ * naturally no-op on default values). The synthesized frame is the schema's
302
+ * DEFAULTS overlaid with the policy's overrides (see {@link IdleInput}):
303
+ *
304
+ * ```ts
305
+ * // declared once at defineInput({ idle: (ctx) => ({ ... }) }); then simply:
306
+ * for (const f of this.inputs.get(sid).drain()) {
307
+ * stepPlayer(p, f, world); // ≥1 frame, always
308
+ * if (f.fire) tryFire(...); // idle frame: fire=false → no-op
309
+ * }
310
+ * ```
311
+ *
312
+ * Pass `{ idle }` to override the room policy for this call, or
313
+ * `{ idle: false }` to suppress it.
314
+ *
315
+ * The idle frame is NOT a consumed input: it advances neither the reconcile
316
+ * ack (`consumedCount`) nor {@link renderTime}. It is ONE reused instance per
317
+ * client, refilled on each synthesis — read it within the tick, don't store
318
+ * it. No synthesis for unknown sessionIds or `bufferMaxSize: 0`.
319
+ */
320
+ drain(opts?: ConsumeOptions<I>): I[];
321
+ /**
322
+ * Consume the single oldest buffered input and advance the reconcile ack by
323
+ * exactly one — the complement to {@link peek}. Returns `undefined`
324
+ * when the buffer is empty.
325
+ *
326
+ * Use this in a **shared-world** loop where one solver step advances every
327
+ * entity together: you can't apply N-inputs-per-player as N world steps without
328
+ * over-stepping the others, so consume one input per entity per fixed step
329
+ * instead. {@link drain} (consume all, ack jumps to newest) is correct only
330
+ * when each entity integrates ITSELF — see the {@link InputAccessor} docs.
331
+ *
332
+ * `consumedCount`/{@link renderTime} stay exact: the ack reflects only the
333
+ * inputs you actually simulated, and `renderTime` is the stamp of THIS input.
334
+ *
335
+ * With a room-level `defineInput({ idle })` policy (or a per-call `{ idle }`
336
+ * override) an empty tick returns a synthesized frame instead of `undefined`
337
+ * — same contract as {@link drain}'s. A "held key keeps moving through a
338
+ * packet gap" loop:
339
+ *
340
+ * @example
341
+ * ```ts
342
+ * // declared once:
343
+ * inputs = this.defineInput(InputSchema, {
344
+ * idle: ({ latest }) => latest ?? true, // empty tick → last input verbatim
345
+ * });
346
+ * // per tick:
347
+ * this.setFixedTimestep(() => {
348
+ * for (const [sid, body] of bodies) {
349
+ * applyInputToBody(body, this.inputs.get(sid).next()); // typed I — never undefined
350
+ * }
351
+ * world.step(); // one step for everyone
352
+ * }, 30);
353
+ * ```
354
+ */
355
+ next(): Idle extends true ? I : I | undefined;
356
+ next(opts: {
357
+ idle: IdleInput<I>;
358
+ }): I;
359
+ next(opts?: ConsumeOptions<I>): I | undefined;
360
+ /**
361
+ * Consume up to `n` oldest buffered inputs (oldest → newest) and advance the
362
+ * reconcile ack by the count actually taken. Returns fewer than `n` (or `[]`)
363
+ * when the buffer holds fewer. Use for shared-world **sub-stepping** — apply
364
+ * each taken input as its own solver sub-step within one tick, bounding the
365
+ * sub-steps per tick. {@link next} is the `take(1)` shorthand.
366
+ */
367
+ take(n: number): I[];
368
+ /** Read everything buffered without consuming. */
369
+ peek(): I[];
370
+ /** Number of snapshots currently buffered. */
371
+ readonly size: number;
372
+ /**
373
+ * Cumulative count of this client's inputs CONSUMED so far — the reconcile ack
374
+ * echoed back to the client (its `lastProcessed`). Advances by one per
375
+ * {@link next}, by the count taken per {@link take}/{@link drain}, by one per
376
+ * yield while iterating {@link consume}, and on overflow-drop; a synthesized
377
+ * idle frame NEVER advances it. Monotonic, so it doubles as a server-side input
378
+ * seq — gate per-input actions off it (e.g. a fire cooldown counted in inputs)
379
+ * instead of hand-rolling a counter. `0` before the first consume / for the
380
+ * no-op accessor.
381
+ */
382
+ readonly consumedCount: number;
383
+ /**
384
+ * `true` when the most recently produced frame was a synthesized idle (vs a
385
+ * real consumed input) — the "skip work on idle" guard. Set per consume
386
+ * call/yield: `true` after a {@link next}/{@link consume}/{@link drain} that
387
+ * fell back to the `idle` policy on an empty buffer, `false` after a real
388
+ * input. Always `false` for {@link take} (never synthesizes) and the no-op
389
+ * accessor. Reads meaningfully right after the consume that produced the frame.
390
+ */
391
+ readonly wasIdle: boolean;
392
+ /** Drop all buffered snapshots (also resets the dedupe tracker). */
393
+ clear(): void;
394
+ /**
395
+ * Server-clock render timestamp (ms since room start) of the most recently
396
+ * consumed input — the time the client was rendering the world at when it
397
+ * issued that input. Tracks the consume primitive: {@link drain}/{@link take}
398
+ * report the NEWEST input they consumed; {@link next} and each
399
+ * {@link consume}/`for..of` yield report THAT one input, so per-input loops
400
+ * rewind to the exact instant of the input simulated.
401
+ * `0` until the first render-time-stamped input is consumed. Populated only
402
+ * when the room rewinds a `mode:"snapshot"` group (which auto-enables the
403
+ * renderTime stamp) and `bufferMaxSize > 0`. Usually you don't read this
404
+ * directly — pass the
405
+ * sessionId to `rewind.lastSeenBy(sessionId)` (which resolves this value, clamps
406
+ * it, and falls back to live) for lag-compensated "what you see is what you
407
+ * hit" hit registration; use this getter only for a custom rewind time.
408
+ */
409
+ readonly renderTime: number;
410
+ /**
411
+ * Reckon-display stamp (server-clock ms) of the most recently consumed input
412
+ * — the client's serverNow ESTIMATE when it sampled that input, i.e. the
413
+ * exact instant its forward-reckoned (`mode:"reckon"`) entities were
414
+ * displayed at. Stamping the display instant DIRECTLY makes the rewind read
415
+ * immune to the client's RTT-estimation error: the client displayed
416
+ * `f(serverNow_est)` and the server reads `f(serverNow_est)` — the same
417
+ * index into the same recorded timeline, so estimation error cancels (the
418
+ * `maxRewindMs` clamp still bounds spoofing). Same consume semantics as
419
+ * {@link renderTime}; consumed automatically by `rewind.lastSeenBy()` —
420
+ * rarely read directly.
421
+ *
422
+ * Always a usable instant: when no reckon-stamped input has been consumed
423
+ * yet (lag-comp off, client clock unsynced, nothing consumed), resolves to
424
+ * the room clock's current `elapsedTime` — the fallback rooms previously
425
+ * hand-wrote as `reckonTime > 0 ? reckonTime : now`. `rewind.lastSeenBy()`
426
+ * internally reads the raw stamp, so its midpoint reconstruction still
427
+ * engages for unstamped clients. The no-op accessor (unknown session / no
428
+ * `defineInput()`) returns `0`.
429
+ */
430
+ readonly reckonTime: number;
431
+ }
432
+ /**
433
+ * Returned by `Room.defineInput()`. Assign it to `this.inputs`, then call
434
+ * `room.inputs.get(sessionId)` per tick to read each client's input stream —
435
+ * the same per-session lookup verb as `room.clients.get(sessionId)`.
436
+ *
437
+ * The fixed-step metadata lives HERE (one per room), not on the per-client
438
+ * {@link InputAccessor} — these values are identical for every client, so they'd
439
+ * be pure duplication per connection. `.get()` keeps the accessor and this
440
+ * metadata cleanly separated.
441
+ */
442
+ export type InputAPI<I = any, Idle extends boolean = false> = {
443
+ /**
444
+ * The input accessor for `sessionId` — the per-client stream you iterate /
445
+ * `.next()` / `.drain()`. Returns a frozen no-op accessor for unknown sessions.
446
+ * Mirrors `room.clients.get(sessionId)`.
447
+ */
448
+ get(sessionId: string): InputAccessor<I, Idle>;
449
+ /**
450
+ * Server-advertised fixed step rate in **Hz** — from `defineInput`'s
451
+ * `tickRate`/`stepMs`/`stepSeconds`, or derived from `setTimestep` —
452
+ * the same value cascaded to predicting clients. `undefined` when no fixed
453
+ * step is advertised.
454
+ */
455
+ readonly tickRate?: number;
456
+ /**
457
+ * The fixed step as **seconds** (`1/tickRate`): the dt to integrate one input
458
+ * with, bit-identical to the client's prediction dt. Pass it to your physics
459
+ * step (`applyInput(p, cmd, level, room.inputs.stepSeconds)`) so server and
460
+ * client share one timestep instead of each keeping a constant that can drift.
461
+ * `undefined` when no rate is advertised.
462
+ */
463
+ readonly stepSeconds?: number;
464
+ /** The fixed step as **milliseconds** (`1000/tickRate`). `undefined` when no rate. */
465
+ readonly stepMs?: number;
466
+ /** Physics sub-steps per input tick (≥ 1; `1` unless declared via
467
+ * `setFixedTimestep(..., { subSteps })` / `defineInput`). */
468
+ readonly subSteps: number;
469
+ /** The physics sub-step as **seconds** (`stepSeconds / subSteps`) — the engine
470
+ * dt when sub-stepping; equals {@link stepSeconds} when `subSteps` is 1.
471
+ * `undefined` when no rate is advertised. */
472
+ readonly subStepSeconds?: number;
473
+ /** The physics sub-step as **milliseconds** (`stepMs / subSteps`). `undefined`
474
+ * when no rate is advertised. */
475
+ readonly subStepMs?: number;
476
+ };
File without changes
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": [],
4
+ "sourcesContent": [],
5
+ "mappings": "",
6
+ "names": []
7
+ }
@@ -98,7 +98,7 @@ var QueueRoom = class extends import_Room.Room {
98
98
  throw new import_ServerError.ServerError(import_shared_types.ErrorCode.APPLICATION_ERROR, "QueueRoom: 'matchRoomName' option is required.");
99
99
  }
100
100
  (0, import_Debug.debugMatchMaking)("QueueRoom#onCreate() maxPlayers: %d, maxWaitingCycles: %d, maxTeamSize: %d, allowIncompleteGroups: %d, roomNameToCreate: %s", this.maxPlayers, this.maxWaitingCycles, this.maxTeamSize, this.allowIncompleteGroups, this.matchRoomName);
101
- this.setSimulationInterval(() => this.reassignMatchGroups(), this.cycleTickInterval);
101
+ this.setTimestep(() => this.reassignMatchGroups(), this.cycleTickInterval);
102
102
  }
103
103
  onJoin(client, options, auth) {
104
104
  this.addToQueue(client, {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/rooms/QueueRoom.ts"],
4
- "sourcesContent": ["import { Room } from '../Room.ts';\nimport type { Client } from '../Transport.ts';\nimport type { IRoomCache } from '../matchmaker/driver.ts';\nimport * as matchMaker from '../MatchMaker.ts';\nimport { debugMatchMaking } from '../Debug.ts';\nimport { ServerError } from '../errors/ServerError.ts';\nimport { CloseCode, ErrorCode } from '@colyseus/shared-types';\n\nexport interface QueueOptions {\n /**\n * number of players on each match\n */\n maxPlayers?: number;\n\n /**\n * name of the room to create\n */\n matchRoomName: string;\n\n /**\n * after these cycles, create a match with a bot\n */\n maxWaitingCycles?: number;\n\n /**\n * after this time, try to fit this client with a not-so-compatible group\n */\n maxWaitingCyclesForPriority?: number;\n\n /**\n * If set, teams must have the same size to be matched together\n */\n maxTeamSize?: number;\n\n /**\n * If `allowIncompleteGroups` is true, players inside an unmatched group (that\n * did not reached `maxPlayers`, and `maxWaitingCycles` has been\n * reached) will be matched together. Your room should fill the remaining\n * spots with \"bots\" on this case.\n */\n allowIncompleteGroups?: boolean;\n\n /**\n * Comparison function for matching clients to groups\n * Returns true if the client is compatible with the group\n */\n compare?: (client: QueueClientData, matchGroup: QueueMatchGroup) => boolean;\n\n /**\n *\n * When onGroupReady is set, the \"roomNameToCreate\" option is ignored.\n */\n onGroupReady?: (this: QueueRoom, group: QueueMatchGroup) => Promise<IRoomCache>;\n}\n\nexport interface QueueMatchGroup {\n averageRank: number;\n clients: Array<Client<{ userData: QueueClientData }>>,\n ready?: boolean;\n confirmed?: number;\n}\n\nexport interface QueueMatchTeam {\n averageRank: number;\n clients: Array<Client<{ userData: QueueClientData }>>,\n teamId: string | symbol;\n}\n\nexport interface QueueClientData {\n /**\n * Rank of the client\n */\n rank: number;\n\n /**\n * Timestamp of when the client entered the queue\n */\n currentCycle?: number;\n\n /**\n * Optional: if matching with a team, the team ID\n */\n teamId?: string;\n\n /**\n * Additional options passed by the client when joining the room\n */\n options?: any;\n\n /**\n * Match group the client is currently in\n */\n group?: QueueMatchGroup;\n\n /**\n * Whether the client has confirmed the connection to the room\n */\n confirmed?: boolean;\n\n /**\n * Whether the client should be prioritized in the queue\n * (e.g. for players that are waiting for a long time)\n */\n highPriority?: boolean;\n\n /**\n * The last number of clients in the queue sent to the client\n */\n lastQueueClientCount?: number;\n}\n\n//\n// Optional: strongly-typed client messages\n// (This is optional, but recommended for better type safety and code generation for native SDKs)\n//\ntype QueueClient = Client<{\n userData: QueueClientData;\n messages: {\n clients: number;\n seat: matchMaker.ISeatReservation;\n }\n}>;\n\nconst DEFAULT_TEAM = Symbol(\"$default_team\");\nconst DEFAULT_COMPARE = (client: QueueClientData, matchGroup: QueueMatchGroup) => {\n const diff = Math.abs(client.rank - matchGroup.averageRank);\n const diffRatio = (diff / matchGroup.averageRank);\n // If diff ratio is too high, create a new match group\n return (diff < 10 || diffRatio <= 2);\n}\n\nexport class QueueRoom extends Room {\n maxPlayers = 4;\n maxTeamSize: number;\n allowIncompleteGroups: boolean = false;\n\n maxWaitingCycles = 15;\n maxWaitingCyclesForPriority?: number = 10;\n\n /**\n * Evaluate groups for each client at interval\n */\n cycleTickInterval = 1000;\n\n /**\n * Groups of players per iteration\n */\n groups: QueueMatchGroup[] = [];\n highPriorityGroups: QueueMatchGroup[] = [];\n\n matchRoomName: string;\n\n protected compare = DEFAULT_COMPARE;\n protected onGroupReady = (group: QueueMatchGroup) => matchMaker.createRoom(this.matchRoomName, {});\n\n messages = {\n confirm: (client: Client, _: unknown) => {\n const queueData = client.userData;\n\n if (queueData && queueData.group && typeof (queueData.group.confirmed) === \"number\") {\n queueData.confirmed = true;\n queueData.group.confirmed++;\n client.leave(CloseCode.NORMAL_CLOSURE);\n }\n },\n }\n\n onCreate(options: QueueOptions) {\n if (typeof(options.maxWaitingCycles) === \"number\") {\n this.maxWaitingCycles = options.maxWaitingCycles;\n }\n\n if (typeof(options.maxPlayers) === \"number\") {\n this.maxPlayers = options.maxPlayers;\n }\n\n if (typeof(options.maxTeamSize) === \"number\") {\n this.maxTeamSize = options.maxTeamSize;\n }\n\n if (typeof(options.allowIncompleteGroups) !== \"undefined\") {\n this.allowIncompleteGroups = options.allowIncompleteGroups;\n }\n\n if (typeof(options.compare) === \"function\") {\n this.compare = options.compare;\n }\n\n if (typeof(options.onGroupReady) === \"function\") {\n this.onGroupReady = options.onGroupReady;\n }\n\n if (options.matchRoomName) {\n this.matchRoomName = options.matchRoomName;\n\n } else {\n throw new ServerError(ErrorCode.APPLICATION_ERROR, \"QueueRoom: 'matchRoomName' option is required.\");\n }\n\n debugMatchMaking(\"QueueRoom#onCreate() maxPlayers: %d, maxWaitingCycles: %d, maxTeamSize: %d, allowIncompleteGroups: %d, roomNameToCreate: %s\", this.maxPlayers, this.maxWaitingCycles, this.maxTeamSize, this.allowIncompleteGroups, this.matchRoomName);\n\n /**\n * Redistribute clients into groups at every interval\n */\n this.setSimulationInterval(() => this.reassignMatchGroups(), this.cycleTickInterval);\n }\n\n onJoin(client: QueueClient, options: any, auth?: unknown) {\n this.addToQueue(client, {\n rank: options.rank,\n teamId: options.teamId,\n options,\n });\n }\n\n addToQueue(client: QueueClient, queueData: QueueClientData) {\n if (queueData.currentCycle === undefined) {\n queueData.currentCycle = 0;\n }\n client.userData = queueData;\n\n // FIXME: reassign groups upon joining [?] (without incrementing cycle count)\n client.send(\"clients\", 1);\n }\n\n createMatchGroup() {\n const group: QueueMatchGroup = { clients: [], averageRank: 0 };\n this.groups.push(group);\n return group;\n }\n\n reassignMatchGroups() {\n // Re-set all groups\n this.groups.length = 0;\n this.highPriorityGroups.length = 0;\n\n const sortedClients = (this.clients)\n .filter((client) => {\n // Filter out:\n // - clients that are not in the queue\n // - clients that are already in a \"ready\" group\n return (\n client.userData &&\n client.userData.group?.ready !== true\n );\n })\n .sort((a, b) => {\n //\n // Sort by rank ascending\n //\n return a.userData.rank - b.userData.rank;\n });\n\n //\n // The room either distribute by teams or by clients\n //\n if (typeof(this.maxTeamSize) === \"number\") {\n this.redistributeTeams(sortedClients);\n\n } else {\n this.redistributeClients(sortedClients);\n }\n\n this.evaluateHighPriorityGroups();\n this.processGroupsReady();\n }\n\n redistributeTeams(sortedClients: Client<{ userData: QueueClientData }>[]) {\n const teamsByID: { [teamId: string | symbol]: QueueMatchTeam } = {};\n\n sortedClients.forEach((client) => {\n const teamId = client.userData.teamId || DEFAULT_TEAM;\n\n // Create a new team if it doesn't exist\n if (!teamsByID[teamId]) {\n teamsByID[teamId] = { teamId: teamId, clients: [], averageRank: 0, };\n }\n\n teamsByID[teamId].averageRank += client.userData.rank;\n teamsByID[teamId].clients.push(client);\n });\n\n // Calculate average rank for each team\n let teams = Object.values(teamsByID).map((team) => {\n team.averageRank /= team.clients.length;\n return team;\n }).sort((a, b) => {\n // Sort by average rank ascending\n return a.averageRank - b.averageRank;\n });\n\n // Iterate over teams multiple times until all clients are assigned to a group\n do {\n let currentGroup: QueueMatchGroup = this.createMatchGroup();\n teams = teams.filter((team) => {\n // Remove clients from the team and add them to the current group\n const totalRank = team.averageRank * team.clients.length;\n\n // currentGroup.averageRank = (currentGroup.averageRank === undefined)\n // ? team.averageRank\n // : (currentGroup.averageRank + team.averageRank) / ;\n currentGroup = this.redistributeClients(team.clients.splice(0, this.maxTeamSize), currentGroup, totalRank);\n\n if (team.clients.length >= this.maxTeamSize) {\n // team still has enough clients to form a group\n return true;\n }\n\n // increment cycle count for all clients in the team\n team.clients.forEach((client) => client.userData.currentCycle++);\n\n return false;\n });\n } while (teams.length >= 2);\n }\n\n redistributeClients(\n sortedClients: Client<{ userData: QueueClientData }>[],\n currentGroup: QueueMatchGroup = this.createMatchGroup(),\n totalRank: number = 0,\n ) {\n for (let i = 0, l = sortedClients.length; i < l; i++) {\n const client = sortedClients[i];\n const userData = client.userData;\n const currentCycle = userData.currentCycle++;\n\n if (currentGroup.averageRank > 0) {\n if (\n !this.compare(userData, currentGroup) &&\n !userData.highPriority\n ) {\n currentGroup = this.createMatchGroup();\n totalRank = 0;\n }\n }\n\n userData.group = currentGroup;\n currentGroup.clients.push(client);\n\n totalRank += userData.rank;\n currentGroup.averageRank = totalRank / currentGroup.clients.length;\n\n // Enough players in the group, mark it as ready!\n if (currentGroup.clients.length === this.maxPlayers) {\n currentGroup.ready = true;\n currentGroup = this.createMatchGroup();\n totalRank = 0;\n continue;\n }\n\n if (currentCycle >= this.maxWaitingCycles && this.allowIncompleteGroups) {\n /**\n * Match long-waiting clients with bots\n */\n if (this.highPriorityGroups.indexOf(currentGroup) === -1) {\n this.highPriorityGroups.push(currentGroup);\n }\n\n } else if (\n this.maxWaitingCyclesForPriority !== undefined &&\n currentCycle >= this.maxWaitingCyclesForPriority\n ) {\n /**\n * Force this client to join a group, even if rank is incompatible\n */\n userData.highPriority = true;\n }\n }\n\n return currentGroup;\n }\n\n evaluateHighPriorityGroups() {\n /**\n * Evaluate groups with high priority clients\n */\n this.highPriorityGroups.forEach((group) => {\n group.ready = group.clients.every((c) => {\n // Give new clients another chance to join a group that is not \"high priority\"\n return c.userData?.currentCycle > 1;\n // return c.userData?.currentCycle >= this.maxWaitingCycles;\n });\n });\n }\n\n processGroupsReady() {\n this.groups.forEach(async (group) => {\n if (group.ready) {\n group.confirmed = 0;\n\n try {\n /**\n * Create room instance in the server.\n */\n const room = await this.onGroupReady.call(this, group);\n\n /**\n * Reserve a seat for each client in the group.\n * (If one fails, force all clients to leave, re-queueing is up to the client-side logic)\n */\n await matchMaker.reserveMultipleSeatsFor(\n room,\n group.clients.map((client) => ({\n sessionId: client.sessionId,\n options: client.userData.options,\n auth: client.auth,\n })),\n );\n\n /**\n * Send room data for new WebSocket connection!\n */\n group.clients.forEach((client, i) => {\n client.send(\"seat\", matchMaker.buildSeatReservation(room, client.sessionId));\n });\n\n } catch (e: any) {\n //\n // If creating a room, or reserving a seat failed - fail all clients\n // Whether the clients retry or not is up to the client-side logic\n //\n group.clients.forEach(client => client.leave(1011, e.message));\n }\n\n } else {\n /**\n * Notify clients within the group on how many players are in the queue\n */\n group.clients.forEach((client) => {\n //\n // avoid sending the same number of clients to the client if it hasn't changed\n //\n const queueClientCount = group.clients.length;\n if (client.userData.lastQueueClientCount !== queueClientCount) {\n client.userData.lastQueueClientCount = queueClientCount;\n client.send(\"clients\", queueClientCount);\n }\n });\n }\n });\n }\n\n}"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAAqB;AAGrB,iBAA4B;AAC5B,mBAAiC;AACjC,yBAA4B;AAC5B,0BAAqC;AAqHrC,IAAM,eAAe,uBAAO,eAAe;AAC3C,IAAM,kBAAkB,CAAC,QAAyB,eAAgC;AAChF,QAAM,OAAO,KAAK,IAAI,OAAO,OAAO,WAAW,WAAW;AAC1D,QAAM,YAAa,OAAO,WAAW;AAErC,SAAQ,OAAO,MAAM,aAAa;AACpC;AAEO,IAAM,YAAN,cAAwB,iBAAK;AAAA,EAA7B;AAAA;AACL,sBAAa;AAEb,iCAAiC;AAEjC,4BAAmB;AACnB,uCAAuC;AAKvC;AAAA;AAAA;AAAA,6BAAoB;AAKpB;AAAA;AAAA;AAAA,kBAA4B,CAAC;AAC7B,8BAAwC,CAAC;AAIzC,SAAU,UAAU;AACpB,SAAU,eAAe,CAAC,UAAsC,sBAAW,KAAK,eAAe,CAAC,CAAC;AAEjG,oBAAW;AAAA,MACT,SAAS,CAAC,QAAgB,MAAe;AACvC,cAAM,YAAY,OAAO;AAEzB,YAAI,aAAa,UAAU,SAAS,OAAQ,UAAU,MAAM,cAAe,UAAU;AACnF,oBAAU,YAAY;AACtB,oBAAU,MAAM;AAChB,iBAAO,MAAM,8BAAU,cAAc;AAAA,QACvC;AAAA,MACF;AAAA,IACF;AAAA;AAAA,EAEA,SAAS,SAAuB;AAC9B,QAAI,OAAO,QAAQ,qBAAsB,UAAU;AACjD,WAAK,mBAAmB,QAAQ;AAAA,IAClC;AAEA,QAAI,OAAO,QAAQ,eAAgB,UAAU;AAC3C,WAAK,aAAa,QAAQ;AAAA,IAC5B;AAEA,QAAI,OAAO,QAAQ,gBAAiB,UAAU;AAC5C,WAAK,cAAc,QAAQ;AAAA,IAC7B;AAEA,QAAI,OAAO,QAAQ,0BAA2B,aAAa;AACzD,WAAK,wBAAwB,QAAQ;AAAA,IACvC;AAEA,QAAI,OAAO,QAAQ,YAAa,YAAY;AAC1C,WAAK,UAAU,QAAQ;AAAA,IACzB;AAEA,QAAI,OAAO,QAAQ,iBAAkB,YAAY;AAC/C,WAAK,eAAe,QAAQ;AAAA,IAC9B;AAEA,QAAI,QAAQ,eAAe;AACzB,WAAK,gBAAgB,QAAQ;AAAA,IAE/B,OAAO;AACL,YAAM,IAAI,+BAAY,8BAAU,mBAAmB,gDAAgD;AAAA,IACrG;AAEA,uCAAiB,+HAA+H,KAAK,YAAY,KAAK,kBAAkB,KAAK,aAAa,KAAK,uBAAuB,KAAK,aAAa;AAKxP,SAAK,sBAAsB,MAAM,KAAK,oBAAoB,GAAG,KAAK,iBAAiB;AAAA,EACrF;AAAA,EAEA,OAAO,QAAqB,SAAc,MAAgB;AACxD,SAAK,WAAW,QAAQ;AAAA,MACtB,MAAM,QAAQ;AAAA,MACd,QAAQ,QAAQ;AAAA,MAChB;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,WAAW,QAAqB,WAA4B;AAC1D,QAAI,UAAU,iBAAiB,QAAW;AACxC,gBAAU,eAAe;AAAA,IAC3B;AACA,WAAO,WAAW;AAGlB,WAAO,KAAK,WAAW,CAAC;AAAA,EAC1B;AAAA,EAEA,mBAAmB;AACjB,UAAM,QAAyB,EAAE,SAAS,CAAC,GAAG,aAAa,EAAE;AAC7D,SAAK,OAAO,KAAK,KAAK;AACtB,WAAO;AAAA,EACT;AAAA,EAEA,sBAAsB;AAEpB,SAAK,OAAO,SAAS;AACrB,SAAK,mBAAmB,SAAS;AAEjC,UAAM,gBAAiB,KAAK,QACzB,OAAO,CAAC,WAAW;AAIlB,aACE,OAAO,YACP,OAAO,SAAS,OAAO,UAAU;AAAA,IAErC,CAAC,EACA,KAAK,CAAC,GAAG,MAAM;AAId,aAAO,EAAE,SAAS,OAAO,EAAE,SAAS;AAAA,IACtC,CAAC;AAKH,QAAI,OAAO,KAAK,gBAAiB,UAAU;AACzC,WAAK,kBAAkB,aAAa;AAAA,IAEtC,OAAO;AACL,WAAK,oBAAoB,aAAa;AAAA,IACxC;AAEA,SAAK,2BAA2B;AAChC,SAAK,mBAAmB;AAAA,EAC1B;AAAA,EAEA,kBAAkB,eAAwD;AACxE,UAAM,YAA2D,CAAC;AAElE,kBAAc,QAAQ,CAAC,WAAW;AAChC,YAAM,SAAS,OAAO,SAAS,UAAU;AAGzC,UAAI,CAAC,UAAU,MAAM,GAAG;AACtB,kBAAU,MAAM,IAAI,EAAE,QAAgB,SAAS,CAAC,GAAG,aAAa,EAAG;AAAA,MACrE;AAEA,gBAAU,MAAM,EAAE,eAAe,OAAO,SAAS;AACjD,gBAAU,MAAM,EAAE,QAAQ,KAAK,MAAM;AAAA,IACvC,CAAC;AAGD,QAAI,QAAQ,OAAO,OAAO,SAAS,EAAE,IAAI,CAAC,SAAS;AACjD,WAAK,eAAe,KAAK,QAAQ;AACjC,aAAO;AAAA,IACT,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM;AAEhB,aAAO,EAAE,cAAc,EAAE;AAAA,IAC3B,CAAC;AAGD,OAAG;AACD,UAAI,eAAgC,KAAK,iBAAiB;AAC1D,cAAQ,MAAM,OAAO,CAAC,SAAS;AAE7B,cAAM,YAAY,KAAK,cAAc,KAAK,QAAQ;AAKlD,uBAAe,KAAK,oBAAoB,KAAK,QAAQ,OAAO,GAAG,KAAK,WAAW,GAAG,cAAc,SAAS;AAEzG,YAAI,KAAK,QAAQ,UAAU,KAAK,aAAa;AAE3C,iBAAO;AAAA,QACT;AAGA,aAAK,QAAQ,QAAQ,CAAC,WAAW,OAAO,SAAS,cAAc;AAE/D,eAAO;AAAA,MACT,CAAC;AAAA,IACH,SAAS,MAAM,UAAU;AAAA,EAC3B;AAAA,EAEA,oBACE,eACA,eAAgC,KAAK,iBAAiB,GACtD,YAAoB,GACpB;AACA,aAAS,IAAI,GAAG,IAAI,cAAc,QAAQ,IAAI,GAAG,KAAK;AACpD,YAAM,SAAS,cAAc,CAAC;AAC9B,YAAM,WAAW,OAAO;AACxB,YAAM,eAAe,SAAS;AAE9B,UAAI,aAAa,cAAc,GAAG;AAChC,YACE,CAAC,KAAK,QAAQ,UAAU,YAAY,KACpC,CAAC,SAAS,cACV;AACA,yBAAe,KAAK,iBAAiB;AACrC,sBAAY;AAAA,QACd;AAAA,MACF;AAEA,eAAS,QAAQ;AACjB,mBAAa,QAAQ,KAAK,MAAM;AAEhC,mBAAa,SAAS;AACtB,mBAAa,cAAc,YAAY,aAAa,QAAQ;AAG5D,UAAI,aAAa,QAAQ,WAAW,KAAK,YAAY;AACnD,qBAAa,QAAQ;AACrB,uBAAe,KAAK,iBAAiB;AACrC,oBAAY;AACZ;AAAA,MACF;AAEA,UAAI,gBAAgB,KAAK,oBAAoB,KAAK,uBAAuB;AAIvE,YAAI,KAAK,mBAAmB,QAAQ,YAAY,MAAM,IAAI;AACxD,eAAK,mBAAmB,KAAK,YAAY;AAAA,QAC3C;AAAA,MAEF,WACE,KAAK,gCAAgC,UACrC,gBAAgB,KAAK,6BACrB;AAIA,iBAAS,eAAe;AAAA,MAC1B;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,6BAA6B;AAI3B,SAAK,mBAAmB,QAAQ,CAAC,UAAU;AACzC,YAAM,QAAQ,MAAM,QAAQ,MAAM,CAAC,MAAM;AAEvC,eAAO,EAAE,UAAU,eAAe;AAAA,MAEpC,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA,EAEA,qBAAqB;AACnB,SAAK,OAAO,QAAQ,OAAO,UAAU;AACnC,UAAI,MAAM,OAAO;AACf,cAAM,YAAY;AAElB,YAAI;AAIF,gBAAM,OAAO,MAAM,KAAK,aAAa,KAAK,MAAM,KAAK;AAMrD,gBAAiB;AAAA,YACf;AAAA,YACA,MAAM,QAAQ,IAAI,CAAC,YAAY;AAAA,cAC7B,WAAW,OAAO;AAAA,cAClB,SAAS,OAAO,SAAS;AAAA,cACzB,MAAM,OAAO;AAAA,YACf,EAAE;AAAA,UACJ;AAKA,gBAAM,QAAQ,QAAQ,CAAC,QAAQ,MAAM;AACnC,mBAAO,KAAK,QAAmB,gCAAqB,MAAM,OAAO,SAAS,CAAC;AAAA,UAC7E,CAAC;AAAA,QAEH,SAAS,GAAQ;AAKf,gBAAM,QAAQ,QAAQ,YAAU,OAAO,MAAM,MAAM,EAAE,OAAO,CAAC;AAAA,QAC/D;AAAA,MAEF,OAAO;AAIL,cAAM,QAAQ,QAAQ,CAAC,WAAW;AAIhC,gBAAM,mBAAmB,MAAM,QAAQ;AACvC,cAAI,OAAO,SAAS,yBAAyB,kBAAkB;AAC7D,mBAAO,SAAS,uBAAuB;AACvC,mBAAO,KAAK,WAAW,gBAAgB;AAAA,UACzC;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF,CAAC;AAAA,EACH;AAEF;",
4
+ "sourcesContent": ["import { Room } from '../Room.ts';\nimport type { Client } from '../Transport.ts';\nimport type { IRoomCache } from '../matchmaker/driver.ts';\nimport * as matchMaker from '../MatchMaker.ts';\nimport { debugMatchMaking } from '../Debug.ts';\nimport { ServerError } from '../errors/ServerError.ts';\nimport { CloseCode, ErrorCode } from '@colyseus/shared-types';\n\nexport interface QueueOptions {\n /**\n * number of players on each match\n */\n maxPlayers?: number;\n\n /**\n * name of the room to create\n */\n matchRoomName: string;\n\n /**\n * after these cycles, create a match with a bot\n */\n maxWaitingCycles?: number;\n\n /**\n * after this time, try to fit this client with a not-so-compatible group\n */\n maxWaitingCyclesForPriority?: number;\n\n /**\n * If set, teams must have the same size to be matched together\n */\n maxTeamSize?: number;\n\n /**\n * If `allowIncompleteGroups` is true, players inside an unmatched group (that\n * did not reached `maxPlayers`, and `maxWaitingCycles` has been\n * reached) will be matched together. Your room should fill the remaining\n * spots with \"bots\" on this case.\n */\n allowIncompleteGroups?: boolean;\n\n /**\n * Comparison function for matching clients to groups\n * Returns true if the client is compatible with the group\n */\n compare?: (client: QueueClientData, matchGroup: QueueMatchGroup) => boolean;\n\n /**\n *\n * When onGroupReady is set, the \"roomNameToCreate\" option is ignored.\n */\n onGroupReady?: (this: QueueRoom, group: QueueMatchGroup) => Promise<IRoomCache>;\n}\n\nexport interface QueueMatchGroup {\n averageRank: number;\n clients: Array<Client<{ userData: QueueClientData }>>,\n ready?: boolean;\n confirmed?: number;\n}\n\nexport interface QueueMatchTeam {\n averageRank: number;\n clients: Array<Client<{ userData: QueueClientData }>>,\n teamId: string | symbol;\n}\n\nexport interface QueueClientData {\n /**\n * Rank of the client\n */\n rank: number;\n\n /**\n * Timestamp of when the client entered the queue\n */\n currentCycle?: number;\n\n /**\n * Optional: if matching with a team, the team ID\n */\n teamId?: string;\n\n /**\n * Additional options passed by the client when joining the room\n */\n options?: any;\n\n /**\n * Match group the client is currently in\n */\n group?: QueueMatchGroup;\n\n /**\n * Whether the client has confirmed the connection to the room\n */\n confirmed?: boolean;\n\n /**\n * Whether the client should be prioritized in the queue\n * (e.g. for players that are waiting for a long time)\n */\n highPriority?: boolean;\n\n /**\n * The last number of clients in the queue sent to the client\n */\n lastQueueClientCount?: number;\n}\n\n//\n// Optional: strongly-typed client messages\n// (This is optional, but recommended for better type safety and code generation for native SDKs)\n//\ntype QueueClient = Client<{\n userData: QueueClientData;\n messages: {\n clients: number;\n seat: matchMaker.ISeatReservation;\n }\n}>;\n\nconst DEFAULT_TEAM = Symbol(\"$default_team\");\nconst DEFAULT_COMPARE = (client: QueueClientData, matchGroup: QueueMatchGroup) => {\n const diff = Math.abs(client.rank - matchGroup.averageRank);\n const diffRatio = (diff / matchGroup.averageRank);\n // If diff ratio is too high, create a new match group\n return (diff < 10 || diffRatio <= 2);\n}\n\nexport class QueueRoom extends Room {\n maxPlayers = 4;\n maxTeamSize: number;\n allowIncompleteGroups: boolean = false;\n\n maxWaitingCycles = 15;\n maxWaitingCyclesForPriority?: number = 10;\n\n /**\n * Evaluate groups for each client at interval\n */\n cycleTickInterval = 1000;\n\n /**\n * Groups of players per iteration\n */\n groups: QueueMatchGroup[] = [];\n highPriorityGroups: QueueMatchGroup[] = [];\n\n matchRoomName: string;\n\n protected compare = DEFAULT_COMPARE;\n protected onGroupReady = (group: QueueMatchGroup) => matchMaker.createRoom(this.matchRoomName, {});\n\n messages = {\n confirm: (client: Client, _: unknown) => {\n const queueData = client.userData;\n\n if (queueData && queueData.group && typeof (queueData.group.confirmed) === \"number\") {\n queueData.confirmed = true;\n queueData.group.confirmed++;\n client.leave(CloseCode.NORMAL_CLOSURE);\n }\n },\n }\n\n onCreate(options: QueueOptions) {\n if (typeof(options.maxWaitingCycles) === \"number\") {\n this.maxWaitingCycles = options.maxWaitingCycles;\n }\n\n if (typeof(options.maxPlayers) === \"number\") {\n this.maxPlayers = options.maxPlayers;\n }\n\n if (typeof(options.maxTeamSize) === \"number\") {\n this.maxTeamSize = options.maxTeamSize;\n }\n\n if (typeof(options.allowIncompleteGroups) !== \"undefined\") {\n this.allowIncompleteGroups = options.allowIncompleteGroups;\n }\n\n if (typeof(options.compare) === \"function\") {\n this.compare = options.compare;\n }\n\n if (typeof(options.onGroupReady) === \"function\") {\n this.onGroupReady = options.onGroupReady;\n }\n\n if (options.matchRoomName) {\n this.matchRoomName = options.matchRoomName;\n\n } else {\n throw new ServerError(ErrorCode.APPLICATION_ERROR, \"QueueRoom: 'matchRoomName' option is required.\");\n }\n\n debugMatchMaking(\"QueueRoom#onCreate() maxPlayers: %d, maxWaitingCycles: %d, maxTeamSize: %d, allowIncompleteGroups: %d, roomNameToCreate: %s\", this.maxPlayers, this.maxWaitingCycles, this.maxTeamSize, this.allowIncompleteGroups, this.matchRoomName);\n\n /**\n * Redistribute clients into groups at every interval\n */\n this.setTimestep(() => this.reassignMatchGroups(), this.cycleTickInterval);\n }\n\n onJoin(client: QueueClient, options: any, auth?: unknown) {\n this.addToQueue(client, {\n rank: options.rank,\n teamId: options.teamId,\n options,\n });\n }\n\n addToQueue(client: QueueClient, queueData: QueueClientData) {\n if (queueData.currentCycle === undefined) {\n queueData.currentCycle = 0;\n }\n client.userData = queueData;\n\n // FIXME: reassign groups upon joining [?] (without incrementing cycle count)\n client.send(\"clients\", 1);\n }\n\n createMatchGroup() {\n const group: QueueMatchGroup = { clients: [], averageRank: 0 };\n this.groups.push(group);\n return group;\n }\n\n reassignMatchGroups() {\n // Re-set all groups\n this.groups.length = 0;\n this.highPriorityGroups.length = 0;\n\n const sortedClients = (this.clients)\n .filter((client) => {\n // Filter out:\n // - clients that are not in the queue\n // - clients that are already in a \"ready\" group\n return (\n client.userData &&\n client.userData.group?.ready !== true\n );\n })\n .sort((a, b) => {\n //\n // Sort by rank ascending\n //\n return a.userData.rank - b.userData.rank;\n });\n\n //\n // The room either distribute by teams or by clients\n //\n if (typeof(this.maxTeamSize) === \"number\") {\n this.redistributeTeams(sortedClients);\n\n } else {\n this.redistributeClients(sortedClients);\n }\n\n this.evaluateHighPriorityGroups();\n this.processGroupsReady();\n }\n\n redistributeTeams(sortedClients: Client<{ userData: QueueClientData }>[]) {\n const teamsByID: { [teamId: string | symbol]: QueueMatchTeam } = {};\n\n sortedClients.forEach((client) => {\n const teamId = client.userData.teamId || DEFAULT_TEAM;\n\n // Create a new team if it doesn't exist\n if (!teamsByID[teamId]) {\n teamsByID[teamId] = { teamId: teamId, clients: [], averageRank: 0, };\n }\n\n teamsByID[teamId].averageRank += client.userData.rank;\n teamsByID[teamId].clients.push(client);\n });\n\n // Calculate average rank for each team\n let teams = Object.values(teamsByID).map((team) => {\n team.averageRank /= team.clients.length;\n return team;\n }).sort((a, b) => {\n // Sort by average rank ascending\n return a.averageRank - b.averageRank;\n });\n\n // Iterate over teams multiple times until all clients are assigned to a group\n do {\n let currentGroup: QueueMatchGroup = this.createMatchGroup();\n teams = teams.filter((team) => {\n // Remove clients from the team and add them to the current group\n const totalRank = team.averageRank * team.clients.length;\n\n // currentGroup.averageRank = (currentGroup.averageRank === undefined)\n // ? team.averageRank\n // : (currentGroup.averageRank + team.averageRank) / ;\n currentGroup = this.redistributeClients(team.clients.splice(0, this.maxTeamSize), currentGroup, totalRank);\n\n if (team.clients.length >= this.maxTeamSize) {\n // team still has enough clients to form a group\n return true;\n }\n\n // increment cycle count for all clients in the team\n team.clients.forEach((client) => client.userData.currentCycle++);\n\n return false;\n });\n } while (teams.length >= 2);\n }\n\n redistributeClients(\n sortedClients: Client<{ userData: QueueClientData }>[],\n currentGroup: QueueMatchGroup = this.createMatchGroup(),\n totalRank: number = 0,\n ) {\n for (let i = 0, l = sortedClients.length; i < l; i++) {\n const client = sortedClients[i];\n const userData = client.userData;\n const currentCycle = userData.currentCycle++;\n\n if (currentGroup.averageRank > 0) {\n if (\n !this.compare(userData, currentGroup) &&\n !userData.highPriority\n ) {\n currentGroup = this.createMatchGroup();\n totalRank = 0;\n }\n }\n\n userData.group = currentGroup;\n currentGroup.clients.push(client);\n\n totalRank += userData.rank;\n currentGroup.averageRank = totalRank / currentGroup.clients.length;\n\n // Enough players in the group, mark it as ready!\n if (currentGroup.clients.length === this.maxPlayers) {\n currentGroup.ready = true;\n currentGroup = this.createMatchGroup();\n totalRank = 0;\n continue;\n }\n\n if (currentCycle >= this.maxWaitingCycles && this.allowIncompleteGroups) {\n /**\n * Match long-waiting clients with bots\n */\n if (this.highPriorityGroups.indexOf(currentGroup) === -1) {\n this.highPriorityGroups.push(currentGroup);\n }\n\n } else if (\n this.maxWaitingCyclesForPriority !== undefined &&\n currentCycle >= this.maxWaitingCyclesForPriority\n ) {\n /**\n * Force this client to join a group, even if rank is incompatible\n */\n userData.highPriority = true;\n }\n }\n\n return currentGroup;\n }\n\n evaluateHighPriorityGroups() {\n /**\n * Evaluate groups with high priority clients\n */\n this.highPriorityGroups.forEach((group) => {\n group.ready = group.clients.every((c) => {\n // Give new clients another chance to join a group that is not \"high priority\"\n return c.userData?.currentCycle > 1;\n // return c.userData?.currentCycle >= this.maxWaitingCycles;\n });\n });\n }\n\n processGroupsReady() {\n this.groups.forEach(async (group) => {\n if (group.ready) {\n group.confirmed = 0;\n\n try {\n /**\n * Create room instance in the server.\n */\n const room = await this.onGroupReady.call(this, group);\n\n /**\n * Reserve a seat for each client in the group.\n * (If one fails, force all clients to leave, re-queueing is up to the client-side logic)\n */\n await matchMaker.reserveMultipleSeatsFor(\n room,\n group.clients.map((client) => ({\n sessionId: client.sessionId,\n options: client.userData.options,\n auth: client.auth,\n })),\n );\n\n /**\n * Send room data for new WebSocket connection!\n */\n group.clients.forEach((client, i) => {\n client.send(\"seat\", matchMaker.buildSeatReservation(room, client.sessionId));\n });\n\n } catch (e: any) {\n //\n // If creating a room, or reserving a seat failed - fail all clients\n // Whether the clients retry or not is up to the client-side logic\n //\n group.clients.forEach(client => client.leave(1011, e.message));\n }\n\n } else {\n /**\n * Notify clients within the group on how many players are in the queue\n */\n group.clients.forEach((client) => {\n //\n // avoid sending the same number of clients to the client if it hasn't changed\n //\n const queueClientCount = group.clients.length;\n if (client.userData.lastQueueClientCount !== queueClientCount) {\n client.userData.lastQueueClientCount = queueClientCount;\n client.send(\"clients\", queueClientCount);\n }\n });\n }\n });\n }\n\n}"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -64,7 +64,7 @@ var QueueRoom = class extends Room {
64
64
  throw new ServerError(ErrorCode.APPLICATION_ERROR, "QueueRoom: 'matchRoomName' option is required.");
65
65
  }
66
66
  debugMatchMaking("QueueRoom#onCreate() maxPlayers: %d, maxWaitingCycles: %d, maxTeamSize: %d, allowIncompleteGroups: %d, roomNameToCreate: %s", this.maxPlayers, this.maxWaitingCycles, this.maxTeamSize, this.allowIncompleteGroups, this.matchRoomName);
67
- this.setSimulationInterval(() => this.reassignMatchGroups(), this.cycleTickInterval);
67
+ this.setTimestep(() => this.reassignMatchGroups(), this.cycleTickInterval);
68
68
  }
69
69
  onJoin(client, options, auth) {
70
70
  this.addToQueue(client, {