@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.
- package/build/MatchMaker.cjs +3 -7
- package/build/MatchMaker.cjs.map +2 -2
- package/build/MatchMaker.mjs +4 -8
- package/build/MatchMaker.mjs.map +2 -2
- package/build/Protocol.cjs +3 -3
- package/build/Protocol.cjs.map +1 -1
- package/build/Protocol.d.ts +3 -3
- package/build/Protocol.mjs +3 -3
- package/build/Protocol.mjs.map +1 -1
- package/build/Rewind.cjs +385 -0
- package/build/Rewind.cjs.map +7 -0
- package/build/Rewind.d.ts +291 -0
- package/build/Rewind.mjs +359 -0
- package/build/Rewind.mjs.map +7 -0
- package/build/Room.cjs +280 -281
- package/build/Room.cjs.map +3 -3
- package/build/Room.d.ts +185 -81
- package/build/Room.mjs +286 -286
- package/build/Room.mjs.map +2 -2
- package/build/RoomMessages.cjs +265 -0
- package/build/RoomMessages.cjs.map +7 -0
- package/build/RoomMessages.d.ts +49 -0
- package/build/RoomMessages.mjs +240 -0
- package/build/RoomMessages.mjs.map +7 -0
- package/build/Server.cjs +88 -31
- package/build/Server.cjs.map +2 -2
- package/build/Server.d.ts +36 -0
- package/build/Server.mjs +88 -31
- package/build/Server.mjs.map +2 -2
- package/build/Transport.cjs +26 -0
- package/build/Transport.cjs.map +2 -2
- package/build/Transport.d.ts +69 -2
- package/build/Transport.mjs +25 -0
- package/build/Transport.mjs.map +2 -2
- package/build/errors/RoomExceptions.cjs +7 -4
- package/build/errors/RoomExceptions.cjs.map +2 -2
- package/build/errors/RoomExceptions.d.ts +15 -3
- package/build/errors/RoomExceptions.mjs +5 -3
- package/build/errors/RoomExceptions.mjs.map +2 -2
- package/build/index.cjs +19 -2
- package/build/index.cjs.map +2 -2
- package/build/index.d.ts +7 -4
- package/build/index.mjs +17 -4
- package/build/index.mjs.map +2 -2
- package/build/input/InputBuffer.cjs +439 -16
- package/build/input/InputBuffer.cjs.map +2 -2
- package/build/input/InputBuffer.d.ts +186 -96
- package/build/input/InputBuffer.mjs +436 -15
- package/build/input/InputBuffer.mjs.map +2 -2
- package/build/input/RoomInput.cjs +306 -0
- package/build/input/RoomInput.cjs.map +7 -0
- package/build/input/RoomInput.d.ts +91 -0
- package/build/input/RoomInput.mjs +287 -0
- package/build/input/RoomInput.mjs.map +7 -0
- package/build/input/types.cjs +18 -0
- package/build/input/types.cjs.map +7 -0
- package/build/input/types.d.ts +476 -0
- package/build/input/types.mjs +0 -0
- package/build/input/types.mjs.map +7 -0
- package/build/rooms/QueueRoom.cjs +1 -1
- package/build/rooms/QueueRoom.cjs.map +2 -2
- package/build/rooms/QueueRoom.mjs +1 -1
- package/build/rooms/QueueRoom.mjs.map +2 -2
- package/build/router/default_routes.cjs +4 -1
- package/build/router/default_routes.cjs.map +2 -2
- package/build/router/default_routes.mjs +4 -1
- package/build/router/default_routes.mjs.map +2 -2
- package/build/router/index.cjs +6 -1
- package/build/router/index.cjs.map +2 -2
- package/build/router/index.mjs +6 -1
- package/build/router/index.mjs.map +2 -2
- package/build/router/node.cjs +37 -2
- package/build/router/node.cjs.map +2 -2
- package/build/router/node.d.ts +10 -0
- package/build/router/node.mjs +35 -1
- package/build/router/node.mjs.map +2 -2
- package/build/serializer/NoneSerializer.cjs +2 -2
- package/build/serializer/NoneSerializer.cjs.map +2 -2
- package/build/serializer/NoneSerializer.d.ts +3 -3
- package/build/serializer/NoneSerializer.mjs +2 -2
- package/build/serializer/NoneSerializer.mjs.map +2 -2
- package/build/serializer/SchemaSerializer.cjs +44 -14
- package/build/serializer/SchemaSerializer.cjs.map +2 -2
- package/build/serializer/SchemaSerializer.d.ts +3 -3
- package/build/serializer/SchemaSerializer.mjs +46 -16
- package/build/serializer/SchemaSerializer.mjs.map +2 -2
- package/build/serializer/Serializer.cjs.map +1 -1
- package/build/serializer/Serializer.d.ts +12 -2
- package/build/utils/DevMode.cjs +14 -14
- package/build/utils/DevMode.cjs.map +2 -2
- package/build/utils/DevMode.d.ts +0 -2
- package/build/utils/DevMode.mjs +14 -12
- package/build/utils/DevMode.mjs.map +2 -2
- package/build/utils/Env.cjs +8 -4
- package/build/utils/Env.cjs.map +3 -3
- package/build/utils/Env.mjs +8 -4
- package/build/utils/Env.mjs.map +2 -2
- package/build/utils/Utils.cjs +3 -0
- package/build/utils/Utils.cjs.map +2 -2
- package/build/utils/Utils.d.ts +8 -0
- package/build/utils/Utils.mjs +2 -0
- package/build/utils/Utils.mjs.map +2 -2
- package/package.json +11 -11
- package/src/MatchMaker.ts +7 -9
- package/src/Protocol.ts +3 -3
- package/src/Rewind.ts +572 -0
- package/src/Room.ts +409 -448
- package/src/RoomMessages.ts +342 -0
- package/src/Server.ts +112 -38
- package/src/Transport.ts +97 -2
- package/src/errors/RoomExceptions.ts +18 -4
- package/src/index.ts +13 -2
- package/src/input/InputBuffer.ts +464 -107
- package/src/input/RoomInput.ts +337 -0
- package/src/input/types.ts +503 -0
- package/src/rooms/QueueRoom.ts +1 -1
- package/src/router/default_routes.ts +6 -1
- package/src/router/index.ts +6 -1
- package/src/router/node.ts +44 -0
- package/src/serializer/NoneSerializer.ts +3 -3
- package/src/serializer/SchemaSerializer.ts +110 -19
- package/src/serializer/Serializer.ts +13 -2
- package/src/utils/DevMode.ts +18 -13
- package/src/utils/Env.ts +12 -4
- 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
|
|
@@ -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.
|
|
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,
|
|
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.
|
|
67
|
+
this.setTimestep(() => this.reassignMatchGroups(), this.cycleTickInterval);
|
|
68
68
|
}
|
|
69
69
|
onJoin(client, options, auth) {
|
|
70
70
|
this.addToQueue(client, {
|