@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
package/build/Room.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { type InputAPI, type NumericFieldsOf } from './input/InputBuffer.ts';
2
- export { type InputAccessor, type InputAPI, type InputOptions, type NumericFieldsOf } from './input/InputBuffer.ts';
1
+ import type { InputAPI, DefineInputOptions, IdleDeclared } from './input/types.ts';
2
+ import { Rewind, type RewindOptions } from './Rewind.ts';
3
+ export { type InputAccessor, type InputAPI, type NormalizedInputOptions, type ConsumeOptions, type IdleInput, type IdleContext, type SanitizeInput, type NumericFieldsOf, type DefineInputOptions, type IdleDeclared } from './input/types.ts';
3
4
  import { ClockTimer as Clock } from '@colyseus/timer';
4
5
  import type { Presence } from './presence/Presence.ts';
5
6
  import type { Serializer } from './serializer/Serializer.ts';
@@ -7,11 +8,44 @@ import { type Type, Deferred } from './utils/Utils.ts';
7
8
  import { type AuthContext, type Client, ClientArray, type ISendOptions, type MessageArgs } from './Transport.ts';
8
9
  import { type RoomMethodName, type RoomException } from './errors/RoomExceptions.ts';
9
10
  import { type StandardSchemaV1 } from './utils/StandardSchema.ts';
10
- import { type MessageHandlerWithFormat as SharedMessageHandlerWithFormat, type MessageHandler as SharedMessageHandler, type Messages as SharedMessages } from '@colyseus/shared-types';
11
+ import { type MessageHandlerWithFormat as SharedMessageHandlerWithFormat, type MessageHandler as SharedMessageHandler, type Messages as SharedMessages, type MessageContext } from '@colyseus/shared-types';
11
12
  import { RoomPlugin, type PluginLayout } from './RoomPlugin.ts';
12
13
  export { RoomPlugin, definePlugins, attachToTestRoom, type RoomPluginOrder, } from './RoomPlugin.ts';
13
14
  export declare const DEFAULT_SEAT_RESERVATION_TIME: number;
14
15
  export type SimulationCallback = (deltaTime: number) => void;
16
+ /**
17
+ * Per-step context passed to a {@link Room.setFixedTimestep} callback. Carries
18
+ * ONLY the fixed simulation step — never wall-clock/measured time — so feeding a
19
+ * jittery delta into deterministic sim math is unrepresentable. The same fixed
20
+ * `dt` is advertised to clients so prediction integrates identically. (The
21
+ * client's step context additionally carries an `isReplay` flag for rollback
22
+ * re-simulation; the server is authoritative and never replays, so it has none.)
23
+ */
24
+ export interface StepContext {
25
+ /** Fixed step in SECONDS (`1/tickRate`) — the dt to integrate one step with. */
26
+ readonly dt: number;
27
+ /** Fixed step in MILLISECONDS (`1000/tickRate`). */
28
+ readonly dtMs: number;
29
+ /** Monotonic index of the fixed step being simulated. */
30
+ readonly tick: number;
31
+ /**
32
+ * Physics sub-steps per fixed step (≥ 1; `1` unless declared via
33
+ * `setFixedTimestep(..., { subSteps })`). Run your engine `subSteps` times at
34
+ * {@link subDt} inside each step — `for (let i = 0; i < ctx.subSteps; i++)
35
+ * world.step(ctx.subDt)` — to integrate physics at `tickRate * subSteps` Hz
36
+ * while inputs flow at `tickRate`. The same numbers are cascaded to predicting
37
+ * clients (their step context carries identical `subSteps`/`subDt`), so the
38
+ * sub-stepped trajectory replays bit-identically.
39
+ */
40
+ readonly subSteps: number;
41
+ /** Physics sub-step in SECONDS (`dt / subSteps`); equals {@link dt} when
42
+ * `subSteps` is 1, so the loop above is valid for every room. */
43
+ readonly subDt: number;
44
+ /** Physics sub-step in MILLISECONDS (`dtMs / subSteps`). */
45
+ readonly subDtMs: number;
46
+ }
47
+ /** Fixed-timestep simulation callback. @see Room.setFixedTimestep */
48
+ export type FixedTimestepCallback = (ctx: StepContext) => void;
15
49
  export interface RoomOptions {
16
50
  state?: object;
17
51
  metadata?: any;
@@ -65,7 +99,7 @@ export type Messages<This extends Room> = SharedMessages<This, Client>;
65
99
  * }
66
100
  * ```
67
101
  */
68
- export declare function validate<T extends StandardSchemaV1, This = any>(format: T, handler: (this: This, client: Client, message: StandardSchemaV1.InferOutput<T>) => void): MessageHandlerWithFormat<T, This>;
102
+ export declare function validate<T extends StandardSchemaV1, This = any>(format: T, handler: (this: This, client: Client, message: StandardSchemaV1.InferOutput<T>, ctx: MessageContext) => unknown): MessageHandlerWithFormat<T, This>;
69
103
  export declare const RoomInternalState: {
70
104
  readonly CREATING: 0;
71
105
  readonly CREATED: 1;
@@ -227,11 +261,9 @@ export declare class Room<T extends RoomOptions = RoomOptions> {
227
261
  * @internal
228
262
  */
229
263
  static __pluginLayout?: PluginLayout | null;
230
- private onMessageEvents;
231
- private onMessageValidators;
232
- private onMessageFallbacks;
264
+ private get onMessageEvents();
265
+ private get onMessageValidators();
233
266
  private _serializer;
234
- private _afterNextPatchQueue;
235
267
  private _simulationInterval;
236
268
  private _internalState;
237
269
  private _lockedExplicitly;
@@ -304,63 +336,71 @@ export declare class Room<T extends RoomOptions = RoomOptions> {
304
336
  */
305
337
  onLeave?(client: ExtractRoomClient<T>, code?: number): void | Promise<any>;
306
338
  /**
307
- * Per-client input accessor. Set by `defineInput()`. Call `room.input(sessionId)`
308
- * each tick to read the latest decoded input and/or the buffered snapshot ring
309
- * for that client.
339
+ * Per-client input accessor. Assign it `= this.defineInput(...)`, then call
340
+ * `this.inputs.get(sessionId)` each tick to read that client's buffered input
341
+ * *stream* iterate it directly, or call `.next()` / `.drain()` / `.latest`.
342
+ *
343
+ * (In the `Room<{ input: I }>` generic, `input` is the input *type* of one
344
+ * frame; `this.inputs` is the per-client accessor over the stream of them.)
310
345
  *
311
346
  * @example
312
347
  * ```typescript
313
348
  * class FpsRoom extends Room<{ input: MoveInput }> {
314
- * input = this.defineInput(MoveInput);
349
+ * inputs = this.defineInput(MoveInput);
315
350
  *
316
351
  * onCreate() {
317
- * this.setSimulationInterval(() => {
318
- * for (const c of this.clients) {
319
- * const input = this.input(c.sessionId);
320
- * if (input.latest) this.apply(c, input.latest);
321
- * // for rollback / lockstep:
322
- * // const snapshot = input.at(this.clock.ticks);
323
- * // for (const snapshot of input.drain()) ...
324
- * }
325
- * }, 1000 / 30);
352
+ * this.setFixedTimestep((ctx) => {
353
+ * for (const [sid, p] of this.state.players)
354
+ * for (const input of this.inputs.get(sid)) // consume one-by-one
355
+ * applyInput(p, input, ctx.dt);
356
+ * }, 30);
326
357
  * }
327
358
  * }
328
359
  * ```
329
360
  */
330
- input?: InputAPI<ExtractRoomInput<T>>;
361
+ inputs?: InputAPI<ExtractRoomInput<T>>;
331
362
  /**
332
- * Input configuration. Set via {@link defineInput} only.
333
- * @internal
363
+ * @internal The per-room input subsystem (options, accessor registry, wire
364
+ * stamp mode, encode/decode/handshake). Created lazily on the first
365
+ * {@link defineInput} call — rooms without inputs allocate none of it, and its
366
+ * presence is what enables client-timed state messages. See {@link RoomInput}.
334
367
  */
335
- private inputOptions?;
368
+ private _inputController?;
336
369
  /**
337
370
  * Declare the input schema and configuration in a single line. Returns the
338
- * callable accessor that gets assigned to `this.input` — call
339
- * `this.input(sessionId)` per tick to consume.
371
+ * input API that gets assigned to `this.inputs` — call
372
+ * `this.inputs.get(sessionId)` per tick to consume.
340
373
  *
341
374
  * ```typescript
342
375
  * class FpsRoom extends Room<{ input: MoveInput }> {
343
- * input = this.defineInput(MoveInput, {
376
+ * inputs = this.defineInput(MoveInput, {
344
377
  * seqField: "tick", // typed: only numeric fields of MoveInput
345
378
  * bufferMaxSize: 64,
346
379
  * });
347
380
  *
348
- * // …or without options — defaults to seqField: "seq", bufferMaxSize: 32:
349
- * // input = this.defineInput(MoveInput);
381
+ * // …or without options — no seq dedupe, bufferMaxSize: 32:
382
+ * // inputs = this.defineInput(MoveInput);
350
383
  * }
351
384
  * ```
352
385
  *
353
386
  * **Defaults** when `opts` (or individual fields) are omitted:
354
- * - `seqField`: `"seq"`framework dedupes by `input.seq` if the schema has
355
- * such a field. Schemas without it gracefully skip dedupe.
387
+ * - `seqField`: unsetdedupe and `this.inputs.get(sessionId).at(value)` lookup are
388
+ * OPT-IN (lockstep / rollback). Name a monotonic numeric field here to enable
389
+ * them; redundant frames (`input[seqField]` ≤ the last seen) are then dropped.
390
+ * Leave unset for reliable, in-order channels where every frame is unique.
356
391
  * - `bufferMaxSize`: `32` — enables per-client snapshot buffering for
357
- * `room.input(sessionId).drain() / .peek() / .at()`. Set to `0` to disable
358
- * buffering (the `.latest` read still works).
392
+ * `this.inputs.get(sessionId)` iteration / `.consume() / .drain() / .next() /
393
+ * .take() / .peek() / .at()`. Set to `0` to disable buffering (the `.latest`
394
+ * read still works).
395
+ *
396
+ * **Consuming the buffer** — `for (const inp of this.inputs.get(sessionId))` (sugar
397
+ * for `.consume()`) is the per-entity loop: it consumes one at a time, so
398
+ * lag-comp `renderTime` tracks each input. `next()` (take exactly one, ack +1)
399
+ * suits a shared physics world stepped once for everyone; `drain()` returns the
400
+ * whole pending set as an array. See {@link InputAccessor} for the full
401
+ * per-entity-vs-shared-world guidance and why the choice affects the reconcile ack.
359
402
  */
360
- protected defineInput<C extends new () => any>(type: C, opts?: {
361
- seqField?: NumericFieldsOf<InstanceType<C>>;
362
- bufferMaxSize?: number;
363
- }): InputAPI<InstanceType<C>>;
403
+ protected defineInput<C extends new () => any, O extends DefineInputOptions<InstanceType<C>> = DefineInputOptions<InstanceType<C>>>(type: C, opts?: O): InputAPI<InstanceType<C>, IdleDeclared<O, InstanceType<C>>>;
364
404
  /**
365
405
  * This method is called when the room is disposed.
366
406
  */
@@ -373,7 +413,7 @@ export declare class Room<T extends RoomOptions = RoomOptions> {
373
413
  * - `onMessage`
374
414
  * - `onAuth` / `onJoin` / `onLeave` / `onCreate` / `onDispose`
375
415
  * - `clock.setTimeout` / `clock.setInterval`
376
- * - `setSimulationInterval`
416
+ * - `setTimestep` / `setFixedTimestep`
377
417
  *
378
418
  * (Experimental: this feature is subject to change in the future - we're currently getting feedback to improve it)
379
419
  */
@@ -429,48 +469,113 @@ export declare class Room<T extends RoomOptions = RoomOptions> {
429
469
  hasReservedSeat(sessionId: string, reconnectionToken?: string): boolean;
430
470
  checkReconnectionToken(reconnectionToken: string): string;
431
471
  /**
432
- * (Optional) Set a simulation interval that can change the state of the game.
433
- * The simulation interval is your game loop.
472
+ * Set the room's game loop. `onTickCallback` runs every `delay` ms and receives
473
+ * the MEASURED wall-clock delta since the previous tick (a VARIABLE timestep).
474
+ * For deterministic, prediction-friendly simulation prefer
475
+ * {@link Room.setFixedTimestep}, which advances a fixed step via an accumulator.
434
476
  *
435
477
  * @default 16.6ms (60fps)
436
478
  *
437
- * @param onTickCallback - You can implement your physics or world updates here!
438
- * This is a good place to update the room state.
439
- * @param delay - Interval delay on executing `onTickCallback` in milliseconds.
479
+ * @param onTickCallback - Your physics / world update a good place to mutate
480
+ * room state. Receives the measured delta (`this.clock.deltaTime`).
481
+ * @param delay - Interval between ticks in milliseconds.
482
+ */
483
+ setTimestep(onTickCallback?: SimulationCallback, delay?: number): void;
484
+ /**
485
+ * @deprecated Renamed to {@link Room.setTimestep} (which pairs with
486
+ * {@link Room.setFixedTimestep}). Kept for backwards compatibility — forwards
487
+ * to `setTimestep` unchanged.
440
488
  */
441
489
  setSimulationInterval(onTickCallback?: SimulationCallback, delay?: number): void;
442
490
  /**
443
- * Run a fixed-rate simulation tagged with a monotonic server tick number.
444
- * Combine with `room.input(sessionId).at(tick)` to retrieve each client's
445
- * input *for a specific tick* the building block for lockstep / rollback
446
- * netcode.
447
- *
448
- * Replaces any previous {@link setSimulationInterval}. The current tick is
449
- * exposed via {@link tick}.
491
+ * Fixed-timestep game loop with a framework-owned accumulator the right
492
+ * default for prediction/rollback. Unlike {@link setTimestep} (which
493
+ * hands you the *measured* wall-clock delta), this runs `step` a whole number
494
+ * of times per real frame so each step advances by the SAME fixed
495
+ * `dt = 1/tickRate`; the measured delta only decides HOW MANY steps run. The
496
+ * fixed dt is delivered via {@link StepContext}, so the jittery wall-clock
497
+ * delta can't leak into deterministic simulation.
498
+ *
499
+ * `tickRate` is also the SINGLE SOURCE of the simulation rate: it's advertised
500
+ * to predicting clients via the join handshake (they predict at the matching
501
+ * `dt`), so don't also pass `tickRate` to {@link defineInput}.
502
+ *
503
+ * **`tickRate` couples three rates** — by design, one input == one fixed step
504
+ * == one server tick, so lowering `tickRate` to save bandwidth also lowers
505
+ * the simulation rate. To keep high-fidelity physics on a lower network rate,
506
+ * pass `{ subSteps: N }`: one input still drives one fixed step, but you
507
+ * integrate `N` engine sub-steps of `ctx.subDt` (= `ctx.dt / N`) inside it —
508
+ * physics at `tickRate * N` Hz, inputs at `tickRate`/sec. The same
509
+ * `subSteps`/`subDt` are cascaded to predicting clients (via the join
510
+ * handshake, onto their reconciler's step context), so client replay
511
+ * reproduces the sub-stepped trajectory exactly. Render interpolation already
512
+ * smooths above the step rate — most games don't need this; reach for it when
513
+ * the *simulation* needs the extra Hz (fast projectiles, stacking, tunneling).
514
+ *
515
+ * On a hitch the accumulator runs at most a few catch-up steps then drops the
516
+ * backlog (no spiral of death). Lag-comp is recorded once per real frame.
517
+ *
518
+ * **Consuming input inside the step** — how you consume each client's buffer
519
+ * depends on who integrates (see {@link InputAccessor}):
520
+ * - *Per-entity* (each body integrates itself): `for (const cmd of
521
+ * this.inputs.get(sid)) applyInput(player, cmd, ctx.dt)` — N inputs =
522
+ * N sub-integrations, ack lands on the newest applied, and `renderTime`
523
+ * tracks each input (lag comp stays exact per step).
524
+ * - *Shared world* (one solver step advances every body): consume exactly one
525
+ * input per entity per step with `this.inputs.get(sid).next()` (or `take(n)` +
526
+ * sub-step), then `world.step()` once. Draining all and applying only the
527
+ * latest would jump the reconcile ack past inputs you never simulated.
528
+ *
529
+ * @param step - Called once per fixed step with a {@link StepContext}.
530
+ * @param tickRate - Simulation rate in **Hz**. Defaults to 60.
531
+ * @param opts - `subSteps`: physics sub-steps per fixed step (integer ≥ 1,
532
+ * default 1) — see above.
450
533
  *
451
534
  * @example
452
- * ```typescript
453
- * class LockstepRoom extends Room<{ input: MoveInput }> {
454
- * input = this.defineInput(MoveInput, { seqField: "tick", bufferMaxSize: 64 });
455
- *
456
- * onCreate() {
457
- * this.setTickedSimulation((tick, dt) => {
458
- * for (const c of this.clients) {
459
- * const snapshot = this.input(c.sessionId).at(tick);
460
- * if (snapshot) this.apply(c, snapshot);
461
- * // else: predict, freeze, etc. — game-level decision
462
- * }
463
- * }, 1000 / 60);
464
- * }
535
+ * ```ts
536
+ * onCreate() {
537
+ * // 30 inputs/sec on the wire, physics integrated at 60 Hz:
538
+ * this.setFixedTimestep((ctx) => {
539
+ * this.applyInputs(ctx); // consume ONE input per client per step
540
+ * for (let i = 0; i < ctx.subSteps; i++) this.world.step(ctx.subDt);
541
+ * }, 30, { subSteps: 2 });
465
542
  * }
466
543
  * ```
467
544
  */
468
- setTickedSimulation(onTickCallback: (tick: number, deltaTime: number) => void, delay?: number, startTick?: number): void;
469
- /**
470
- * Current server tick. Incremented by {@link setTickedSimulation} after each
471
- * tick callback returns. Returns 0 when no ticked simulation is running.
545
+ setFixedTimestep(step: FixedTimestepCallback, tickRate?: number, opts?: {
546
+ subSteps?: number;
547
+ }): void;
548
+ /** @internal The rewind attachments' timeline mode (snapshot/reckon), or
549
+ * `undefined` when no rewind is configured. {@link RoomInput} reads it to
550
+ * derive the wire input stamp mode (snapshot → renderTime, reckon → reckonTime). */
551
+ _timelineMode(): {
552
+ snapshot?: boolean;
553
+ reckon?: boolean;
554
+ } | undefined;
555
+ /**
556
+ * Enable server-side lag compensation: returns a {@link Rewind} that records the
557
+ * position history of the entities you attach and automatically snapshots them
558
+ * on each broadcast (the patchRate cadence). Attach the collections to rewind,
559
+ * then read past positions (at a client's renderTime) in your hit tests.
560
+ *
561
+ * @example
562
+ * ```ts
563
+ * const rewind = this.allowRewindState({ maxRewindMs: 500 });
564
+ * rewind.attachAll(this.state.enemies, { fields: ["x", "y"] });
565
+ * // in a hit test — rewind to where the SHOOTER saw the world:
566
+ * const seen = rewind.lastSeenBy(shooterSessionId); // needs this.defineInput(...)
567
+ * const seenX = seen.value(enemy, "x"), seenY = seen.value(enemy, "y");
568
+ * ```
569
+ *
570
+ * Per-client stamps auto-enable from the `attachAll` `mode` of the groups you
571
+ * rewind — no `renderTime` flag. The default auto-record fires on each
572
+ * broadcast, snapshotting exactly what the client receives — so the rewind
573
+ * reproduces the client's interpolation and hits stay exact even when the
574
+ * broadcast rate differs from the sim rate (`patchRate ≠ timestep`). Call
575
+ * `rewind.record()` yourself during a tick to take over that cadence (you then
576
+ * own correctness against your own broadcast rate).
472
577
  */
473
- get tick(): number;
578
+ allowRewindState(opts?: RewindOptions): Rewind;
474
579
  /**
475
580
  * @deprecated Use `.patchRate=` instead.
476
581
  */
@@ -581,8 +686,8 @@ export declare class Room<T extends RoomOptions = RoomOptions> {
581
686
  * ```
582
687
  */
583
688
  onMessage<T = any, C extends Client = ExtractRoomClient<T>>(messageType: '*', callback: (client: C, type: string | number, message: T) => void): any;
584
- onMessage<T = any, C extends Client = ExtractRoomClient<T>>(messageType: string | number, callback: (client: C, message: T) => void): any;
585
- onMessage<T = any, C extends Client = ExtractRoomClient<T>>(messageType: string | number, validationSchema: StandardSchemaV1<T>, callback: (client: C, message: T) => void): any;
689
+ onMessage<T = any, C extends Client = ExtractRoomClient<T>>(messageType: string | number, callback: (client: C, message: T, ctx: MessageContext) => unknown): any;
690
+ onMessage<T = any, C extends Client = ExtractRoomClient<T>>(messageType: string | number, validationSchema: StandardSchemaV1<T>, callback: (client: C, message: T, ctx: MessageContext) => unknown): any;
586
691
  onMessageBytes<T = any, C extends Client = ExtractRoomClient<T>>(messageType: string | number, callback: (client: C, message: T) => void): any;
587
692
  onMessageBytes<T = any, C extends Client = ExtractRoomClient<T>>(messageType: string | number, validationSchema: StandardSchemaV1<T>, callback: (client: C, message: T) => void): any;
588
693
  /**
@@ -653,19 +758,18 @@ export declare class Room<T extends RoomOptions = RoomOptions> {
653
758
  allowReconnection(previousClient: Client, seconds: number | "manual"): Deferred<Client>;
654
759
  private resetAutoDisposeTimeout;
655
760
  private broadcastMessageType;
761
+ /** Drain `#afterPatchBroadcasts` right after the patch — each as ONE shared
762
+ * buffer fanned out to its (non-excepted) recipients. `enqueueRaw` handles
763
+ * JOINED (send now) vs still-joining (buffer until JOIN) per client. */
764
+ private _flushAfterPatchBroadcasts;
656
765
  private sendFullState;
657
- private _dequeueAfterPatchMessages;
766
+ /** Send each client's staged `afterNextPatch` frames as standalone frames right
767
+ * after the patch, then reset the tracker. Iterates only the clients that staged
768
+ * frames this cycle (`#pendingFrameClients`), never the full client list; empty
769
+ * (→ O(1)) on a tick where nothing was staged. */
770
+ private _flushPendingClientFrames;
658
771
  private _reserveSeat;
659
772
  private _reserveMultipleSeats;
660
773
  private _onMessage;
661
774
  private _onLeave;
662
775
  }
663
- /**
664
- * (WIP) Alternative, method-based room definition.
665
- * We should be able to define
666
- */
667
- type RoomLifecycleMethods = 'messages' | 'onCreate' | 'onJoin' | 'onLeave' | 'onDispose' | 'onCacheRoom' | 'onRestoreRoom' | 'onDrop' | 'onReconnect' | 'onUncaughtException' | 'onAuth' | 'onBeforeShutdown' | 'onBeforePatch';
668
- type DefineRoomOptions<T extends RoomOptions = RoomOptions> = Partial<Pick<Room<T>, RoomLifecycleMethods>> & {
669
- state?: ExtractRoomState<T> | (() => ExtractRoomState<T>);
670
- } & ThisType<Exclude<Room<T>, RoomLifecycleMethods>> & ThisType<Room<T>>;
671
- export declare function room<T>(options: DefineRoomOptions<T>): typeof Room<T>;