@colyseus/core 0.17.44 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/build/MatchMaker.cjs +22 -13
  2. package/build/MatchMaker.cjs.map +2 -2
  3. package/build/MatchMaker.d.ts +10 -0
  4. package/build/MatchMaker.mjs +22 -14
  5. package/build/MatchMaker.mjs.map +2 -2
  6. package/build/Protocol.cjs +102 -37
  7. package/build/Protocol.cjs.map +2 -2
  8. package/build/Protocol.d.ts +33 -2
  9. package/build/Protocol.mjs +102 -37
  10. package/build/Protocol.mjs.map +2 -2
  11. package/build/Rewind.cjs +385 -0
  12. package/build/Rewind.cjs.map +7 -0
  13. package/build/Rewind.d.ts +291 -0
  14. package/build/Rewind.mjs +359 -0
  15. package/build/Rewind.mjs.map +7 -0
  16. package/build/Room.cjs +425 -149
  17. package/build/Room.cjs.map +3 -3
  18. package/build/Room.d.ts +314 -27
  19. package/build/Room.mjs +434 -152
  20. package/build/Room.mjs.map +3 -3
  21. package/build/RoomMessages.cjs +265 -0
  22. package/build/RoomMessages.cjs.map +7 -0
  23. package/build/RoomMessages.d.ts +49 -0
  24. package/build/RoomMessages.mjs +240 -0
  25. package/build/RoomMessages.mjs.map +7 -0
  26. package/build/RoomPlugin.cjs +252 -0
  27. package/build/RoomPlugin.cjs.map +7 -0
  28. package/build/RoomPlugin.d.ts +271 -0
  29. package/build/RoomPlugin.mjs +220 -0
  30. package/build/RoomPlugin.mjs.map +7 -0
  31. package/build/Server.cjs +49 -11
  32. package/build/Server.cjs.map +2 -2
  33. package/build/Server.d.ts +26 -0
  34. package/build/Server.mjs +50 -12
  35. package/build/Server.mjs.map +2 -2
  36. package/build/Transport.cjs +64 -2
  37. package/build/Transport.cjs.map +2 -2
  38. package/build/Transport.d.ts +108 -5
  39. package/build/Transport.mjs +63 -2
  40. package/build/Transport.mjs.map +2 -2
  41. package/build/errors/RoomExceptions.cjs +7 -4
  42. package/build/errors/RoomExceptions.cjs.map +2 -2
  43. package/build/errors/RoomExceptions.d.ts +15 -3
  44. package/build/errors/RoomExceptions.mjs +5 -3
  45. package/build/errors/RoomExceptions.mjs.map +2 -2
  46. package/build/index.cjs +30 -4
  47. package/build/index.cjs.map +2 -2
  48. package/build/index.d.ts +9 -5
  49. package/build/index.mjs +29 -6
  50. package/build/index.mjs.map +2 -2
  51. package/build/input/InputBuffer.cjs +536 -0
  52. package/build/input/InputBuffer.cjs.map +7 -0
  53. package/build/input/InputBuffer.d.ts +226 -0
  54. package/build/input/InputBuffer.mjs +507 -0
  55. package/build/input/InputBuffer.mjs.map +7 -0
  56. package/build/input/RoomInput.cjs +306 -0
  57. package/build/input/RoomInput.cjs.map +7 -0
  58. package/build/input/RoomInput.d.ts +91 -0
  59. package/build/input/RoomInput.mjs +287 -0
  60. package/build/input/RoomInput.mjs.map +7 -0
  61. package/build/input/types.cjs +18 -0
  62. package/build/input/types.cjs.map +7 -0
  63. package/build/input/types.d.ts +476 -0
  64. package/build/input/types.mjs +0 -0
  65. package/build/input/types.mjs.map +7 -0
  66. package/build/internal.cjs +61 -0
  67. package/build/internal.cjs.map +7 -0
  68. package/build/internal.d.ts +9 -0
  69. package/build/internal.mjs +29 -0
  70. package/build/internal.mjs.map +7 -0
  71. package/build/matchmaker/LocalDriver/LocalDriver.cjs +13 -0
  72. package/build/matchmaker/LocalDriver/LocalDriver.cjs.map +2 -2
  73. package/build/matchmaker/LocalDriver/LocalDriver.d.ts +1 -0
  74. package/build/matchmaker/LocalDriver/LocalDriver.mjs +13 -0
  75. package/build/matchmaker/LocalDriver/LocalDriver.mjs.map +2 -2
  76. package/build/matchmaker/driver.cjs.map +1 -1
  77. package/build/matchmaker/driver.d.ts +12 -0
  78. package/build/matchmaker/driver.mjs.map +1 -1
  79. package/build/presence/LocalPresence.d.ts +1 -1
  80. package/build/rooms/LobbyRoom.cjs +8 -10
  81. package/build/rooms/LobbyRoom.cjs.map +2 -2
  82. package/build/rooms/LobbyRoom.d.ts +4 -3
  83. package/build/rooms/LobbyRoom.mjs +8 -10
  84. package/build/rooms/LobbyRoom.mjs.map +2 -2
  85. package/build/rooms/QueueRoom.cjs +1 -1
  86. package/build/rooms/QueueRoom.cjs.map +2 -2
  87. package/build/rooms/QueueRoom.mjs +1 -1
  88. package/build/rooms/QueueRoom.mjs.map +2 -2
  89. package/build/rooms/RelayRoom.cjs +12 -16
  90. package/build/rooms/RelayRoom.cjs.map +2 -2
  91. package/build/rooms/RelayRoom.d.ts +32 -11
  92. package/build/rooms/RelayRoom.mjs +10 -16
  93. package/build/rooms/RelayRoom.mjs.map +2 -2
  94. package/build/router/default_routes.cjs +4 -1
  95. package/build/router/default_routes.cjs.map +2 -2
  96. package/build/router/default_routes.mjs +4 -1
  97. package/build/router/default_routes.mjs.map +2 -2
  98. package/build/router/index.cjs +71 -5
  99. package/build/router/index.cjs.map +2 -2
  100. package/build/router/index.d.ts +30 -6
  101. package/build/router/index.mjs +72 -7
  102. package/build/router/index.mjs.map +3 -3
  103. package/build/serializer/NoneSerializer.cjs +2 -2
  104. package/build/serializer/NoneSerializer.cjs.map +2 -2
  105. package/build/serializer/NoneSerializer.d.ts +3 -3
  106. package/build/serializer/NoneSerializer.mjs +2 -2
  107. package/build/serializer/NoneSerializer.mjs.map +2 -2
  108. package/build/serializer/SchemaSerializer.cjs +44 -14
  109. package/build/serializer/SchemaSerializer.cjs.map +2 -2
  110. package/build/serializer/SchemaSerializer.d.ts +3 -3
  111. package/build/serializer/SchemaSerializer.mjs +46 -16
  112. package/build/serializer/SchemaSerializer.mjs.map +2 -2
  113. package/build/serializer/Serializer.cjs.map +1 -1
  114. package/build/serializer/Serializer.d.ts +12 -2
  115. package/build/utils/DevMode.cjs +14 -14
  116. package/build/utils/DevMode.cjs.map +2 -2
  117. package/build/utils/DevMode.d.ts +0 -2
  118. package/build/utils/DevMode.mjs +14 -12
  119. package/build/utils/DevMode.mjs.map +2 -2
  120. package/build/utils/UserSessionIndex.cjs +162 -0
  121. package/build/utils/UserSessionIndex.cjs.map +7 -0
  122. package/build/utils/UserSessionIndex.d.ts +166 -0
  123. package/build/utils/UserSessionIndex.mjs +130 -0
  124. package/build/utils/UserSessionIndex.mjs.map +7 -0
  125. package/build/utils/Utils.cjs +3 -0
  126. package/build/utils/Utils.cjs.map +2 -2
  127. package/build/utils/Utils.d.ts +8 -0
  128. package/build/utils/Utils.mjs +2 -0
  129. package/build/utils/Utils.mjs.map +2 -2
  130. package/package.json +19 -14
  131. package/src/MatchMaker.ts +47 -15
  132. package/src/Protocol.ts +130 -59
  133. package/src/Rewind.ts +572 -0
  134. package/src/Room.ts +649 -235
  135. package/src/RoomMessages.ts +342 -0
  136. package/src/RoomPlugin.ts +563 -0
  137. package/src/Server.ts +82 -15
  138. package/src/Transport.ts +172 -9
  139. package/src/errors/RoomExceptions.ts +18 -4
  140. package/src/index.ts +23 -3
  141. package/src/input/InputBuffer.ts +549 -0
  142. package/src/input/RoomInput.ts +337 -0
  143. package/src/input/types.ts +503 -0
  144. package/src/internal.ts +46 -0
  145. package/src/matchmaker/LocalDriver/LocalDriver.ts +10 -0
  146. package/src/matchmaker/driver.ts +13 -0
  147. package/src/rooms/LobbyRoom.ts +12 -8
  148. package/src/rooms/QueueRoom.ts +1 -1
  149. package/src/rooms/RelayRoom.ts +9 -15
  150. package/src/router/default_routes.ts +6 -1
  151. package/src/router/index.ts +117 -11
  152. package/src/serializer/NoneSerializer.ts +3 -3
  153. package/src/serializer/SchemaSerializer.ts +110 -19
  154. package/src/serializer/Serializer.ts +13 -2
  155. package/src/utils/DevMode.ts +18 -13
  156. package/src/utils/UserSessionIndex.ts +311 -0
  157. package/src/utils/Utils.ts +9 -0
@@ -0,0 +1,337 @@
1
+ import { decode, encode, Encoder, Reflection, type Iterator } from '@colyseus/schema';
2
+ import { InputDecoder } from '@colyseus/schema/input';
3
+ import { HandshakeSection, InputFlags, ProtocolModifier } from '@colyseus/shared-types';
4
+
5
+ import {
6
+ InputAccessorImpl, InputBufferImpl, NO_OP_INPUT_ACCESSOR,
7
+ compileSanitizer, validateSubSteps,
8
+ } from './InputBuffer.ts';
9
+ import type {
10
+ InputAccessor, InputAPI, NormalizedInputOptions,
11
+ DefineInputOptions, IdleDeclared,
12
+ } from './types.ts';
13
+ import type { Client, ClientPrivate } from '../Transport.ts';
14
+ import type { Room } from '../Room.ts';
15
+ import { debugAndPrintError } from '../Debug.ts';
16
+
17
+ /**
18
+ * Module-level cache of `Reflection.encode` output keyed by input
19
+ * constructor — pays the encoding cost once per Room class regardless of
20
+ * room instance count. WeakMap so unused classes can be GC'd.
21
+ */
22
+ const _inputReflectionCache = new WeakMap<Function, Uint8Array>();
23
+
24
+ /**
25
+ * Runtime behind {@link InputAPI}. A class, not a per-`define()` object literal:
26
+ * literal (and `defineProperty`) accessors carry their closure identity in the
27
+ * hidden class, so every room instance would get a UNIQUE map — sending shared
28
+ * `this.inputs.*` call sites megamorphic. Prototype getters are created once,
29
+ * every instance shares one hidden class (reads stay monomorphic), and
30
+ * `define()` allocates a single 1-field object instead of 7 closures + a map
31
+ * lineage per room.
32
+ *
33
+ * @internal
34
+ */
35
+ class InputAPIImpl {
36
+ private input: RoomInput;
37
+ constructor(input: RoomInput) { this.input = input; }
38
+ /** Registry, not room.clients: an accessor outlives its client through the
39
+ * reconnection window, so a dropped seat still synthesizes the idle policy. */
40
+ get(sessionId: string): InputAccessor<any> {
41
+ return this.input.accessors.get(sessionId) ?? NO_OP_INPUT_ACCESSOR;
42
+ }
43
+ // Live reads off `options` (mutated in place by a later setTimestep/
44
+ // setFixedTimestep back-fill, so a derived tickRate is reflected); 1/hz is
45
+ // correctly-rounded IEEE-754 → bit-identical to the client's stepSeconds.
46
+ get tickRate(): number | undefined { return this.input.options.tickRate; }
47
+ get stepSeconds(): number | undefined { const hz = this.input.options.tickRate; return hz ? 1 / hz : undefined; }
48
+ get stepMs(): number | undefined { const hz = this.input.options.tickRate; return hz ? 1000 / hz : undefined; }
49
+ // Sub-step trio: same derivation as the client handle ((1/hz)/n) — bit-identical dt.
50
+ get subSteps(): number { return this.input.options.subSteps ?? 1; }
51
+ get subStepSeconds(): number | undefined { const hz = this.input.options.tickRate; return hz ? (1 / hz) / (this.input.options.subSteps ?? 1) : undefined; }
52
+ get subStepMs(): number | undefined { const hz = this.input.options.tickRate; return hz ? (1000 / hz) / (this.input.options.subSteps ?? 1) : undefined; }
53
+ }
54
+
55
+ /**
56
+ * Per-room input subsystem, owned by {@link Room} and created lazily on the
57
+ * first {@link Room.defineInput} call — rooms without inputs allocate none of
58
+ * it. Owns the input options, the per-session accessor registry (decoupled
59
+ * from `this.clients` so an entry outlives a dropped client across its
60
+ * reconnection window), the wire stamp mode, and the encode/decode/handshake
61
+ * machinery. Reads back into its Room for context it doesn't own (the patch
62
+ * rate and the rewind timeline mode).
63
+ *
64
+ * @internal
65
+ */
66
+ export class RoomInput {
67
+ /** Owning room — for `patchRate` and the rewind timeline mode. */
68
+ private room: Room<any>;
69
+
70
+ /** Input configuration (ctor, seqField, bufferMaxSize, idle, sanitize,
71
+ * tickRate, subSteps). `tickRate`/`subSteps` may be back-filled by
72
+ * `setTimestep`/`setFixedTimestep`. Set in {@link define}. */
73
+ options!: NormalizedInputOptions;
74
+
75
+ /** The `InputAPI` returned to userland from `defineInput`, also the
76
+ * framework-owned handle the rewind binding resolves accessors through. */
77
+ api!: InputAPI<any>;
78
+
79
+ /** sessionId → accessor. Decoupled from `this.clients`: an entry outlives its
80
+ * client across the reconnection window, so {@link InputAPI.get} keeps
81
+ * synthesizing the idle policy for an absent seat (its buffer was cleared on
82
+ * leave → idle). Set on join, deleted on full leave. Read by
83
+ * {@link InputAPIImpl.get} (hence not `private`). */
84
+ readonly accessors: Map<string, InputAccessor<any>> = new Map();
85
+
86
+ // Wire stamp mode derived from the rewind timeline, resolved once on first
87
+ // handshake/decode then frozen so advertise + decode never disagree mid-session:
88
+ // the renderTime/reckonTime prefix per input.
89
+ #stampRender = false;
90
+ #stampReckon = false;
91
+ #stampResolved = false;
92
+
93
+ /** One shared now-resolver handed to every accessor (see {@link InputAccessorImpl}). */
94
+ private nowOf: () => number;
95
+
96
+ constructor(room: Room<any>) {
97
+ this.room = room;
98
+ this.nowOf = () => this.room.clock.elapsedTime;
99
+ }
100
+
101
+ /**
102
+ * Build the input configuration + `InputAPI` (the body of
103
+ * {@link Room.defineInput}). Returns the api; the Room hands it back to
104
+ * userland and keeps the framework-owned reference.
105
+ */
106
+ define<
107
+ C extends new () => any,
108
+ O extends DefineInputOptions<InstanceType<C>> = DefineInputOptions<InstanceType<C>>,
109
+ >(
110
+ type: C,
111
+ opts?: O,
112
+ ): InputAPI<InstanceType<C>, IdleDeclared<O, InstanceType<C>>> {
113
+ // Normalize the step declaration to the canonical wire form (Hz). stepSeconds
114
+ // / stepMs are the unit-safe spellings; both sides derive dt = 1/tickRate, so
115
+ // one declared number drives the server's physics step AND the client's.
116
+ const tickRate =
117
+ opts?.stepSeconds !== undefined ? 1 / opts.stepSeconds :
118
+ opts?.stepMs !== undefined ? 1000 / opts.stepMs :
119
+ opts?.tickRate;
120
+ if (
121
+ opts?.tickRate !== undefined && opts.stepMs === undefined && opts.stepSeconds === undefined &&
122
+ (!Number.isInteger(opts.tickRate) || opts.tickRate > 240)
123
+ ) {
124
+ console.warn(
125
+ `[defineInput] tickRate is a rate in Hz (got ${opts.tickRate}); a value ` +
126
+ `like 1000/rate is a step interval in ms — pass { stepMs } instead.`,
127
+ );
128
+ }
129
+ this.options = {
130
+ ctor: type,
131
+ // Opt-in: a default would silently drop frames for any app with a numeric `seq` field.
132
+ seqField: opts?.seqField,
133
+ bufferMaxSize: opts?.bufferMaxSize ?? 32,
134
+ idle: opts?.idle,
135
+ // Compiled once (map → dense min/max walk); applied per decoded frame.
136
+ sanitize: opts?.sanitize !== undefined ? compileSanitizer(opts.sanitize) : undefined,
137
+ tickRate,
138
+ subSteps: validateSubSteps(opts?.subSteps, 'defineInput'),
139
+ };
140
+ if (!_inputReflectionCache.has(type)) {
141
+ // SDK-deserializable ctor bytes (Reflection.decode rebuilds the ctor client-side).
142
+ _inputReflectionCache.set(type, Reflection.encode(new Encoder(new type())));
143
+ }
144
+ const api = new InputAPIImpl(this) as InputAPI<InstanceType<C>, IdleDeclared<O, InstanceType<C>>>;
145
+ this.api = api;
146
+ return api;
147
+ }
148
+
149
+ // --- per-client lifecycle (called from Room._onJoin/_onLeave/etc.) ---
150
+
151
+ /** Allocate the per-client input instance + decoder, the ring buffer (opt-in
152
+ * via `bufferMaxSize > 0`, for rollback/lockstep), and the accessor. */
153
+ allocate(client: Client & ClientPrivate): void {
154
+ client._input = new this.options.ctor();
155
+ client._inputDecoder = new InputDecoder(client._input);
156
+ client._reckonBaseline = 0; // mirrors the SDK's delta-coded stamp baseline (reset together on (re)connect)
157
+ const maxSize = this.options.bufferMaxSize;
158
+ if (maxSize > 0) {
159
+ // ctor → idle synthesis; client ref → idle ctx; idle policy → total drain()/next().
160
+ client._inputBuffer = new InputBufferImpl(maxSize, this.options.seqField, this.options.ctor, client, this.options.idle);
161
+ }
162
+ client._inputAccessor = new InputAccessorImpl(client, this.nowOf);
163
+ }
164
+
165
+ /** @internal Raw reckon stamp for {@link Room.allowRewindState}'s
166
+ * `bindReckonTime` — bypasses the accessor's resolved getter so the rewind
167
+ * midpoint fallback still engages for unstamped clients. instanceof-narrowed:
168
+ * the registry only ever holds `InputAccessorImpl` (see {@link allocate}),
169
+ * so the 0 fallback covers only a missing session. */
170
+ rawReckonTime(sessionId: string): number {
171
+ const acc = this.accessors.get(sessionId);
172
+ return acc instanceof InputAccessorImpl ? acc.rawReckonTime : 0;
173
+ }
174
+
175
+ /** Register a (re)joined client's accessor. On reconnect this overwrites the
176
+ * dropped session's stale entry. Called after the client is in `this.clients`
177
+ * (so a pre-push failure can't leak it) and before onJoin (so `get()` resolves
178
+ * there). */
179
+ register(sessionId: string, client: ClientPrivate): void {
180
+ this.accessors.set(sessionId, client._inputAccessor!);
181
+ }
182
+
183
+ /** Freeze a leaving seat: drop pending inputs so a held (reconnecting) session
184
+ * idles from the first tick rather than replaying last-known moves. `_input`
185
+ * (the idle ctx's `latest`) is left intact. */
186
+ freeze(client: ClientPrivate): void {
187
+ client._inputBuffer?.clear();
188
+ }
189
+
190
+ /** Drop a fully-gone session's accessor (delete of a missing key is a no-op). */
191
+ release(sessionId: string): void {
192
+ this.accessors.delete(sessionId);
193
+ }
194
+
195
+ /** Release every held accessor (room dispose). */
196
+ dispose(): void {
197
+ this.accessors.clear();
198
+ }
199
+
200
+ // --- encode / decode (called from Room._onMessage) ---
201
+
202
+ /**
203
+ * Sanitize the freshly-decoded `client._input`, then (when buffering is on)
204
+ * push a clone into the per-client buffer. Honors the framework seq
205
+ * (unreliable) / a user `inputOptions.seqField` (reliable) to dedupe the
206
+ * redundancy ring.
207
+ */
208
+ capture(client: ClientPrivate, renderTime: number = 0, reckonTime: number = 0, seq?: number): void {
209
+ // Sanitize before anything reads it (latest, the clone below, the idle ctx).
210
+ this.options.sanitize?.(client._input);
211
+ const buf = client._inputBuffer;
212
+ if (!buf) { return; } // no consumer registered — skip the clone allocation
213
+ const inst = client._input!;
214
+ if (seq !== undefined) {
215
+ // Unreliable: dedup the ring by the framework wire seq (user seqField is for .at() only).
216
+ if (!buf.accept(seq)) { return; }
217
+ } else {
218
+ // Reliable: no framework seq (implicit count). Honor a user `seqField` if set.
219
+ const seqField = this.options.seqField;
220
+ if (seqField !== undefined) {
221
+ const value = (inst as any)[seqField] as number;
222
+ if (typeof value === 'number' && !buf.accept(value)) { return; }
223
+ }
224
+ }
225
+ buf.push(inst.clone() as any, renderTime, reckonTime, seq);
226
+ }
227
+
228
+ /** Decode a `ROOM_INPUT_RELIABLE` frame: an optional TIMED stamp prefix (shape
229
+ * set by this room's derived stamp mode) then the input body. `it` starts at
230
+ * offset 1, so the body begins at `it.offset`. */
231
+ decodeReliable(client: ClientPrivate, buffer: Buffer, it: Iterator, modifiers: number): void {
232
+ if (!client._inputDecoder) { return; }
233
+ // Optional stamp prefix (TIMED bit), DELTA-CODED, length set by this room's
234
+ // stamp mode (the timeline value is reconstructed against the per-client
235
+ // baseline; the wire carries only the signed change from the previous frame):
236
+ // both: [varint Δreckon][uint16 renderDelta] → renderTime = reckon − renderDelta
237
+ // reckon-only: [varint Δreckon]
238
+ // render-only: [varint Δrender]
239
+ let renderTime = 0;
240
+ let reckonTime = 0;
241
+ if (modifiers & ProtocolModifier.TIMED) {
242
+ this.#resolveWireModes();
243
+ // Reconstruct the absolute timeline value: baseline += signed delta. Both
244
+ // sides start at 0 (so the first delta is absolute) and re-zero together on
245
+ // (re)connect; reliable+in-order keeps the mirror locked.
246
+ const stamp = client._reckonBaseline! += decode.number(buffer, it);
247
+ if (this.#stampReckon && this.#stampRender) {
248
+ reckonTime = stamp;
249
+ const renderDelta = decode.uint16(buffer, it);
250
+ renderTime = stamp > renderDelta ? stamp - renderDelta : 0;
251
+ } else if (this.#stampReckon) {
252
+ reckonTime = stamp;
253
+ } else {
254
+ renderTime = stamp;
255
+ }
256
+ }
257
+ try {
258
+ client._inputDecoder.decode(buffer.subarray(it.offset, buffer.byteLength));
259
+ } catch (e: any) {
260
+ debugAndPrintError(e);
261
+ return;
262
+ }
263
+ client._lastInputReceivedAt = performance.now();
264
+ // Mirrors the SDK's sent count; echoed back as lastInputSeq for RTT.
265
+ client._receivedInputCount = (client._receivedInputCount ?? 0) + 1;
266
+ this.capture(client, renderTime, reckonTime);
267
+ }
268
+
269
+ /** Decode a `ROOM_INPUT_UNRELIABLE` redundancy ring — each slot carries its
270
+ * framework seq (base seq + position) for ring dedupe, no user seqField. */
271
+ decodeUnreliable(client: ClientPrivate, buffer: Buffer, _modifiers: number): void {
272
+ if (!client._inputDecoder) { return; }
273
+ try {
274
+ client._inputDecoder.decodeAll(buffer.subarray(1), (_inst, seq) => this.capture(client, 0, 0, seq));
275
+ } catch (e: any) {
276
+ debugAndPrintError(e);
277
+ return;
278
+ }
279
+ client._lastInputReceivedAt = performance.now();
280
+ }
281
+
282
+ // --- handshake + stamp mode ---
283
+
284
+ /** Resolve the wire stamp mode from the rewind timeline (once, then frozen). */
285
+ #resolveWireModes(): void {
286
+ if (this.#stampResolved) return;
287
+ this.#stampResolved = true;
288
+ const tl = this.room._timelineMode();
289
+ this.#stampRender = tl?.snapshot ?? false;
290
+ this.#stampReckon = tl?.reckon ?? false;
291
+ }
292
+
293
+ /**
294
+ * The join-handshake input sections: INPUT_REFLECTION (the SDK-deserializable
295
+ * input ctor bytes) and, when any runtime config differs from defaults,
296
+ * INPUT_OPTIONS (`[flags uint8][tickRate varint?][patchRate varint?][subSteps
297
+ * varint?]`). The client mirrors RENDER_TIME/RECKON_TIME (auto-stamp timeline),
298
+ * tickRate (predict at dt=1/tickRate), patchRate (reconcile cadence), and
299
+ * subSteps (physics sub-steps per input, omitted when 1). Returns `undefined`
300
+ * when there's nothing to add.
301
+ */
302
+ handshakeSections(): Array<{ tag: number; bytes: Uint8Array }> | undefined {
303
+ let sections: Array<{ tag: number; bytes: Uint8Array }> | undefined;
304
+ const inputBytes = _inputReflectionCache.get(this.options.ctor);
305
+ if (inputBytes !== undefined) {
306
+ sections = [{ tag: HandshakeSection.INPUT_REFLECTION, bytes: inputBytes }];
307
+ }
308
+ this.#resolveWireModes();
309
+ const stampRender = this.#stampRender;
310
+ const stampReckon = this.#stampReckon;
311
+ const tickRate = this.options.tickRate;
312
+ const patchRate = (typeof this.room.patchRate === "number" && this.room.patchRate > 0)
313
+ ? Math.round(this.room.patchRate) : undefined;
314
+ const subSteps = (this.options.subSteps !== undefined && this.options.subSteps > 1)
315
+ ? this.options.subSteps : undefined;
316
+ if (stampRender || stampReckon || tickRate || patchRate || subSteps) {
317
+ let flags = 0;
318
+ if (stampRender) { flags |= InputFlags.RENDER_TIME; }
319
+ if (stampReckon) { flags |= InputFlags.RECKON_TIME; }
320
+ if (tickRate) { flags |= InputFlags.FIXED_TIMESTEP; }
321
+ if (patchRate) { flags |= InputFlags.PATCH_RATE; }
322
+ if (subSteps) { flags |= InputFlags.SUB_STEPS; }
323
+ // 24B: worst case = flags + float64 tickRate (9) + uint32 patchRate (5) + subSteps (2).
324
+ const buf = new Uint8Array(24);
325
+ const sit = { offset: 0 };
326
+ buf[sit.offset++] = flags;
327
+ if (tickRate) { encode.number(buf, tickRate, sit); }
328
+ if (patchRate) { encode.number(buf, patchRate, sit); }
329
+ if (subSteps) { encode.number(buf, subSteps, sit); }
330
+ (sections ??= []).push({
331
+ tag: HandshakeSection.INPUT_OPTIONS,
332
+ bytes: buf.subarray(0, sit.offset),
333
+ });
334
+ }
335
+ return sections;
336
+ }
337
+ }