@irtio/runtime 0.6.0 → 0.8.0

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.
@@ -136,6 +136,10 @@ type HostCall = {
136
136
  readonly board: string;
137
137
  readonly playerId: string;
138
138
  readonly score: number;
139
+ /** M6 lane B (D69-c): the cohort of a bucketed board. Absent on every board that declares
140
+ * no buckets, which is every board that existed before M6. There is deliberately no
141
+ * `period` beside it: rotation is control's clock, never the room's. */
142
+ readonly bucket?: string;
139
143
  }
140
144
  /**
141
145
  * D59: `room.bus.send(roomId, payload)` — directed, durable, at-least-once delivery to one room
@@ -246,6 +250,24 @@ interface RoomHost {
246
250
  * A host with no room list (the in-process test harness) records it and does nothing else.
247
251
  */
248
252
  setBackfill(open: boolean): void;
253
+ /**
254
+ * D75: `room.lobby.setPublic(v)`, and the terminal deregistration a game start performs.
255
+ *
256
+ * Fire-and-forget, and host state rather than room state, for exactly `setBackfill`'s reason:
257
+ * the supervisor holds it, reports it on the room list the agent already polls, and control
258
+ * turns that report into a registry row. None of which the room could arrange for itself and
259
+ * none of which belongs in the hibernation blob.
260
+ *
261
+ * `started` is a one-way door. A room that reports it is deregistered terminally, and no later
262
+ * report — raced, stale or forged — can put it back in front of a stranger.
263
+ *
264
+ * A host with no room list (the in-process test harness) records it and does nothing else.
265
+ */
266
+ setLobby(update: {
267
+ readonly public?: boolean;
268
+ readonly queue?: string;
269
+ readonly started?: boolean;
270
+ }): void;
249
271
  /**
250
272
  * D44: `room.spawnNPC(config)`. The host opens a loopback client session under `clientId` and
251
273
  * runs the named entry of the room definition's `npcs` map against it. The room minted the id,
@@ -340,6 +362,13 @@ interface RoomStats {
340
362
  membershipEnters: number;
341
363
  membershipLeaves: number;
342
364
  corrections: number;
365
+ /**
366
+ * D70: typed peer messages this room refused to hand to `onMessage` — an index its schema does
367
+ * not have, or a payload the codec would not decode. Every one of them is a peer sending
368
+ * something this room's schema does not describe, so a number climbing here means a client on a
369
+ * stale build or one probing, and never a room bug.
370
+ */
371
+ messagesDropped: number;
343
372
  }
344
373
  /** The surface the worker host and the harness drive. Implemented by `RoomCore`. */
345
374
  interface RoomCoreApi<S extends AnySchema = AnySchema> {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export { D as DEFAULT_TIMELINE_MAX_RECORDS, a as DEFAULT_TIMELINE_MAX_TICKS, E as EVENT_RING_SIZE, H as HOST_CALL_TIMEOUT_MS, b as HostCall, c as HostCallResult, J as JoinOptions, d as JoinResult, L as LogLevel, R as RoomCoreApi, e as RoomCoreOptions, f as RoomEvent, g as RoomEventKind, h as RoomFullError, i as RoomHost, j as RoomInspection, k as RoomStats, T as TimelineDump, l as TimelineFrame, m as TimelineRecorder, n as TimelineRecorderOptions, o as inspectState } from './contract-BjMsoJIV.js';
2
- export { C as CRASH_AFTER_THROWS, M as MAX_CATCHUP, a as MatterBodyRecord, b as MatterSection, c as Mulberry32, P as PhysicsEngineTag, d as PhysicsSection, R as RoomCore, e as decodeMatterBodies, f as decodeMatterSectionEnvelope, g as decodePhysicsSection, h as encodeMatterBodies, i as encodeMatterSectionEnvelope, j as encodePhysicsSection, k as initMatter, l as initPhysics, m as loadedMatter, n as loadedPhysics, p as physicsSectionEngine, r as resetMatterForTests, o as resetPhysicsForTests } from './room-CfnEjlcg.js';
1
+ export { D as DEFAULT_TIMELINE_MAX_RECORDS, a as DEFAULT_TIMELINE_MAX_TICKS, E as EVENT_RING_SIZE, H as HOST_CALL_TIMEOUT_MS, b as HostCall, c as HostCallResult, J as JoinOptions, d as JoinResult, L as LogLevel, R as RoomCoreApi, e as RoomCoreOptions, f as RoomEvent, g as RoomEventKind, h as RoomFullError, i as RoomHost, j as RoomInspection, k as RoomStats, T as TimelineDump, l as TimelineFrame, m as TimelineRecorder, n as TimelineRecorderOptions, o as inspectState } from './contract-CRilLVdx.js';
2
+ export { C as CRASH_AFTER_THROWS, M as MAX_CATCHUP, a as MatterBodyRecord, b as MatterSection, c as Mulberry32, P as PhysicsEngineTag, d as PhysicsSection, R as RoomCore, e as decodeMatterBodies, f as decodeMatterSectionEnvelope, g as decodePhysicsSection, h as decodeRapier2dSectionEnvelope, i as encodeMatterBodies, j as encodeMatterSectionEnvelope, k as encodePhysicsSection, l as encodeRapier2dSectionEnvelope, m as initMatter, n as initPhysics, o as initRapier2d, p as loadedMatter, q as loadedPhysics, r as loadedRapier2d, s as physicsSectionEngine, t as resetMatterForTests, u as resetPhysicsForTests, v as resetRapier2dForTests } from './room-YJGz6Caw.js';
3
3
  import { CollectionDesc, AnySchema, PlainState, DirtySet } from '@irtio/schema';
4
4
  import '@irtio/protocol';
5
5
  import '@irtio/server';
@@ -121,7 +121,7 @@ interface DecodedSave extends SnapshotHeader {
121
121
  * there is no section at all. A pre-D45 blob has no discriminant and reads as `'rapier3d'`,
122
122
  * which is what it is.
123
123
  */
124
- readonly physicsEngine: 'rapier3d' | 'matter2d' | undefined;
124
+ readonly physicsEngine: 'rapier3d' | 'matter2d' | 'rapier2d' | undefined;
125
125
  /**
126
126
  * The room's authoritative state, in exactly the shape `inspectState` and the recorded
127
127
  * timeline use: entity collections as `{ id: { owner, value } }`, singletons as their value.
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  fromMigrationState,
3
3
  migrateSnapshot,
4
4
  toMigrationState
5
- } from "./chunk-VMCE3LRO.js";
5
+ } from "./chunk-DBGMG2S3.js";
6
6
  import {
7
7
  CRASH_AFTER_THROWS,
8
8
  DEFAULT_TIMELINE_MAX_RECORDS,
@@ -22,27 +22,32 @@ import {
22
22
  decodeMatterBodies,
23
23
  decodeMatterSectionEnvelope,
24
24
  decodePhysicsSection,
25
+ decodeRapier2dSectionEnvelope,
25
26
  decodeSave,
26
27
  encodeMatterBodies,
27
28
  encodeMatterSectionEnvelope,
28
29
  encodePhysicsSection,
30
+ encodeRapier2dSectionEnvelope,
29
31
  encodeViewDelta,
30
32
  encodeViewSnapshot,
31
33
  initMatter,
32
34
  initPhysics,
35
+ initRapier2d,
33
36
  inspectState,
34
37
  isVisible,
35
38
  loadedMatter,
36
39
  loadedPhysics,
40
+ loadedRapier2d,
37
41
  parseHibernationBlob,
38
42
  physicsSectionEngine,
39
43
  resetMatterForTests,
40
44
  resetPhysicsForTests,
45
+ resetRapier2dForTests,
41
46
  viewKeyFor,
42
47
  visibleNames,
43
48
  visibleTo,
44
49
  writeHibernationBlob
45
- } from "./chunk-K42HA75G.js";
50
+ } from "./chunk-ZCD2TBXB.js";
46
51
  export {
47
52
  CRASH_AFTER_THROWS,
48
53
  DEFAULT_TIMELINE_MAX_RECORDS,
@@ -62,24 +67,29 @@ export {
62
67
  decodeMatterBodies,
63
68
  decodeMatterSectionEnvelope,
64
69
  decodePhysicsSection,
70
+ decodeRapier2dSectionEnvelope,
65
71
  decodeSave,
66
72
  encodeMatterBodies,
67
73
  encodeMatterSectionEnvelope,
68
74
  encodePhysicsSection,
75
+ encodeRapier2dSectionEnvelope,
69
76
  encodeViewDelta,
70
77
  encodeViewSnapshot,
71
78
  fromMigrationState,
72
79
  initMatter,
73
80
  initPhysics,
81
+ initRapier2d,
74
82
  inspectState,
75
83
  isVisible,
76
84
  loadedMatter,
77
85
  loadedPhysics,
86
+ loadedRapier2d,
78
87
  migrateSnapshot,
79
88
  parseHibernationBlob,
80
89
  physicsSectionEngine,
81
90
  resetMatterForTests,
82
91
  resetPhysicsForTests,
92
+ resetRapier2dForTests,
83
93
  toMigrationState,
84
94
  viewKeyFor,
85
95
  visibleNames,
@@ -1,7 +1,7 @@
1
1
  import { AttributeOptions, ProfileSnapshot } from '@irtio/protocol';
2
2
  import { DirtySet, AnySchema, PlainState, Tracked, State } from '@irtio/schema';
3
- import { RoomDefinition, RoomMode, Room, Ctx, MatterModule, MatterEngine, MatterBody, RapierModule, RapierWorld, RapierRigidBody, ResolvedRoomConfig, LeaveReason } from '@irtio/server';
4
- import { i as RoomHost, k as RoomStats, L as LogLevel, R as RoomCoreApi, e as RoomCoreOptions, g as RoomEventKind, n as TimelineRecorderOptions, T as TimelineDump, j as RoomInspection, J as JoinOptions, d as JoinResult, c as HostCallResult } from './contract-BjMsoJIV.js';
3
+ import { RoomDefinition, RoomMode, Room, RewindView, Ctx, MatterModule, MatterEngine, MatterBody, RapierModule, RapierWorld, RapierRigidBody, Rapier2dModule, Rapier2dWorld, Rapier2dRigidBody, ResolvedRoomConfig, LeaveReason } from '@irtio/server';
4
+ import { i as RoomHost, k as RoomStats, L as LogLevel, R as RoomCoreApi, e as RoomCoreOptions, g as RoomEventKind, n as TimelineRecorderOptions, T as TimelineDump, j as RoomInspection, J as JoinOptions, d as JoinResult, c as HostCallResult } from './contract-CRilLVdx.js';
5
5
 
6
6
  /**
7
7
  * `room.random()` — mulberry32 over a u32 seed. Deterministic, tiny, and serializable: the
@@ -93,8 +93,8 @@ interface QueuedFrame {
93
93
  }
94
94
  /** What the core modules may do with the physics world (`core/physics.ts` implements it). */
95
95
  interface PhysicsApi {
96
- /** D45: which of the two blessed engines this room is running. */
97
- readonly engineKind: 'rapier3d' | 'matter2d';
96
+ /** D45: which of the blessed engines this room is running. */
97
+ readonly engineKind: 'rapier3d' | 'matter2d' | 'rapier2d';
98
98
  readonly timestep: number;
99
99
  /** Creates bodies for new instances, destroys bodies whose instance is gone. */
100
100
  reconcile(): void;
@@ -102,9 +102,17 @@ interface PhysicsApi {
102
102
  /** Body → schema, through the tracked proxies. */
103
103
  sync(): void;
104
104
  bodyFor(collection: string, id: string): unknown;
105
- /** rapier3d only. */
105
+ /**
106
+ * D72: every body this runtime tracks, in the same collection-then-instance order `sync()`
107
+ * walks. Read-only and allocation-free: the pose history calls it once per tick to copy poses
108
+ * out, and the rewind scratch calls it to keep its doubles in step with the live world. It is
109
+ * the only thing outside the engine runtimes that sees a live body other than through
110
+ * `bodyFor`, and it never hands out the map itself.
111
+ */
112
+ eachTrackedBody(fn: (collection: string, id: string, body: unknown) => void): void;
113
+ /** rapier3d and rapier2d: the engine namespace. `engineKind` says which module it is. */
106
114
  readonly rapier: unknown;
107
- /** rapier3d only. */
115
+ /** rapier3d and rapier2d: the live `World`. */
108
116
  readonly world: unknown;
109
117
  /** matter2d only: the `matter-js` namespace. */
110
118
  readonly matter: unknown;
@@ -151,6 +159,13 @@ interface RoomInternals {
151
159
  guard<T>(name: string, fn: () => T): T | undefined;
152
160
  /** D41: record this tick on the authoritative timeline. A no-op while the recorder is unarmed. */
153
161
  captureTimeline(): void;
162
+ /**
163
+ * D72: record this tick's body poses. A no-op — and, more to the point, a call that never
164
+ * reaches the engine runtime at all — in a room whose config declares no `physics.history`.
165
+ */
166
+ captureHistory(): void;
167
+ /** D72: `room.rewind(tick, fn)`. Throws when the room declares no `physics.history`. */
168
+ rewind<T>(tick: number, fn: (past: RewindView) => T): T;
154
169
  recordEvent(kind: 'join' | 'leave' | 'write' | 'write-rejected' | 'correct' | 'call' | 'reply' | 'msg' | 'alarm' | 'bus' | 'error', clientId?: string, detail?: string): void;
155
170
  /** `guard` that also reports whether the handler threw (the tick loop needs this). */
156
171
  tryRun<T>(name: string, fn: () => T): GuardResult<T>;
@@ -337,6 +352,11 @@ declare class MatterRuntime {
337
352
  free(): void;
338
353
  bodyFor(collection: string, id: string): MatterBody | undefined;
339
354
  private create;
355
+ /**
356
+ * D72: every tracked body, in the order `sync()` walks them. The matter half of the same
357
+ * read-only accessor `PhysicsRuntime` carries; see its comment for what calls it and when.
358
+ */
359
+ eachTrackedBody(fn: (collection: string, id: string, body: MatterBody) => void): void;
340
360
  private applyState;
341
361
  private applyRecordToBody;
342
362
  reconcile(): void;
@@ -411,9 +431,27 @@ declare function encodePhysicsSection(section: PhysicsSection): Uint8Array;
411
431
  * pre-D45 blob is therefore byte-identical and reads as Rapier without a version bump, which is
412
432
  * what the recommendation in the plan asked for and why format 3 was not needed.
413
433
  */
414
- type PhysicsEngineTag = 'rapier3d' | 'matter2d';
415
- /** Reads the engine out of an encoded section without decoding the rest of it. */
434
+ type PhysicsEngineTag = 'rapier3d' | 'matter2d' | 'rapier2d';
435
+ /**
436
+ * Reads the engine out of an encoded section without decoding the rest of it.
437
+ *
438
+ * The `else` is deliberately bare rather than a third tag comparison: a pre-D45 blob has no
439
+ * discriminant at all, and every byte sequence that is not the zero-length-world envelope is a
440
+ * rapier3d world snapshot. Adding rapier2d must not change that, or every blob written before
441
+ * this engine existed stops decoding.
442
+ */
416
443
  declare function physicsSectionEngine(bytes: Uint8Array): PhysicsEngineTag;
444
+ /**
445
+ * Wraps a rapier2d section in the same discriminated envelope, under its own tag.
446
+ *
447
+ * The payload is an ordinary {@link encodePhysicsSection} — rapier2d has a real
448
+ * `world.takeSnapshot()`, so unlike matter2d it has a world to write, and the only thing the
449
+ * envelope buys is telling the two Rapiers apart. Restoring a 2D snapshot into a 3D world (or the
450
+ * reverse) does not fail cleanly, which is exactly why the discriminant is not optional here.
451
+ */
452
+ declare function encodeRapier2dSectionEnvelope(payload: Uint8Array): Uint8Array;
453
+ /** The payload inside a rapier2d envelope. Throws if the section was written by another engine. */
454
+ declare function decodeRapier2dSectionEnvelope(bytes: Uint8Array): Uint8Array;
417
455
  /** Wraps a matter2d body-state payload in the discriminated envelope. */
418
456
  declare function encodeMatterSectionEnvelope(payload: Uint8Array): Uint8Array;
419
457
  /** The payload inside a matter2d envelope. Throws if the section is a Rapier one. */
@@ -458,6 +496,13 @@ declare class PhysicsRuntime {
458
496
  /** The body behind an instance, created on demand so a handler's own `add` is usable at once. */
459
497
  bodyFor(collection: string, id: string): RapierRigidBody | undefined;
460
498
  private create;
499
+ /**
500
+ * D72: every tracked body, in the order `sync()` walks them (collection order, then instance
501
+ * order). Read-only: it hands out the live bodies and nothing else, and the map stays private.
502
+ * The pose history calls this once per tick and the rewind scratch calls it per rewind; a room
503
+ * that declares no `physics.history` never calls it at all.
504
+ */
505
+ eachTrackedBody(fn: (collection: string, id: string, body: RapierRigidBody) => void): void;
461
506
  private applyRecordToBody;
462
507
  /**
463
508
  * Creates bodies for new instances and destroys bodies whose instance is gone. Runs once per
@@ -474,6 +519,106 @@ declare class PhysicsRuntime {
474
519
  serialize(): PhysicsSection;
475
520
  }
476
521
 
522
+ /**
523
+ * The rapier2d world inside the room: Rapier, held in a plane.
524
+ *
525
+ * It is modelled on `core/physics.ts` rather than on `core/matter.ts`, and the choice is the whole
526
+ * point of the engine existing. Everything that made the Rapier path good is kept:
527
+ *
528
+ * - **A real engine snapshot.** `world.takeSnapshot()` / `World.restoreSnapshot()` exist in the 2D
529
+ * build too, so a woken room is byte-for-byte the world that went to sleep, contacts and sleep
530
+ * timers included, and `setup` runs only on a genuine rebuild. matter2d has no equivalent and
531
+ * rebuilds every wake; that difference is why a settled pile can jolt there and does not here.
532
+ * - **The world applies gravity.** It goes into the `World` constructor and dynamic bodies fall
533
+ * without the room lifting a finger. A matter2d room's hooks apply gravity themselves; a
534
+ * rapier2d room that copied that would fall twice as fast.
535
+ * - **Velocities are per second.** `linvel()` is metres per second, not matter's per-step
536
+ * displacement, so nothing here rescales anything.
537
+ *
538
+ * What it takes from the 2D side is only the plane: `{ x, y }` gravity, and the channel mapping in
539
+ * `@irtio/schema` (`channelOf2d` / `applyChannel2d` / `angleFrom2d`), which is the single place the
540
+ * six planar quantities are written onto the thirteen 3D wire channels. A second copy of that
541
+ * mapping here would be a way for the two sides to disagree by a sign, so there is not one.
542
+ *
543
+ * There is no planar-lock warning in this file. Bug 6's friction trap needs a third axis to lock,
544
+ * and this engine does not have one.
545
+ *
546
+ * ## Async init
547
+ *
548
+ * Rapier's WASM needs `await RAPIER.init()`, and room handlers are synchronous, so the engine is
549
+ * initialized before the room is constructed — by the worker host at bundle-load time, and by
550
+ * `initRapier2d()` in the test harness. This is `initPhysics()`'s twin, against the 2D module.
551
+ */
552
+
553
+ /**
554
+ * Loads and initializes `@dimforge/rapier2d-compat` once per process. Idempotent and safe to call
555
+ * concurrently. It is a *separate* WASM module from the 3D build — the two do not share an
556
+ * instance and loading one does not load the other — so a rapier2d room pays the 2D module's
557
+ * footprint and nothing else.
558
+ */
559
+ declare function initRapier2d(): Promise<Rapier2dModule>;
560
+ /** The initialized engine, or `undefined` when `initRapier2d()` has not resolved yet. */
561
+ declare function loadedRapier2d(): Rapier2dModule | undefined;
562
+ /** Test seam: forget the loaded engine (never used in production paths). */
563
+ declare function resetRapier2dForTests(): void;
564
+ interface Rapier2dRuntimeOptions {
565
+ /** From a v2 hibernation blob's rapier2d section. Absent → a fresh world, and `setup` runs. */
566
+ readonly restore?: PhysicsSection;
567
+ /** Seconds per step when the config does not name one (the tick interval). */
568
+ readonly defaultTimestep: number;
569
+ }
570
+ declare class Rapier2dRuntime {
571
+ readonly engineKind: "rapier2d";
572
+ /** matter2d only; present so every runtime satisfies one internal shape. */
573
+ readonly matter: undefined;
574
+ readonly matterEngine: undefined;
575
+ readonly rapier: Rapier2dModule;
576
+ readonly world: Rapier2dWorld;
577
+ /** `true` when the world was built from scratch and `setup` has to run. */
578
+ readonly rebuilt: boolean;
579
+ /** Rapier restores a whole world, contacts included, so `setup` runs only on a rebuild. */
580
+ get needsSetup(): boolean;
581
+ private readonly core;
582
+ private readonly config;
583
+ /** Physics-backed collections, in schema (name-sorted) order. */
584
+ private readonly collections;
585
+ private readonly bodies;
586
+ /** See `PhysicsRuntime.sleepSynced`: the one extra sync a body owes on the tick it sleeps. */
587
+ private readonly sleepSynced;
588
+ constructor(core: RoomInternals, rapier: Rapier2dModule, options: Rapier2dRuntimeOptions);
589
+ get timestep(): number;
590
+ /** Runs the room's `setup` — static geometry — on a world that was built rather than restored. */
591
+ runSetup(room: Room): void;
592
+ free(): void;
593
+ /** The body behind an instance, created on demand so a handler's own `add` is usable at once. */
594
+ bodyFor(collection: string, id: string): Rapier2dRigidBody | undefined;
595
+ private create;
596
+ /**
597
+ * D72: every tracked body, in the order `sync()` walks them (collection order, then instance
598
+ * order). The rapier2d third of the same read-only accessor the other two runtimes carry.
599
+ */
600
+ eachTrackedBody(fn: (collection: string, id: string, body: Rapier2dRigidBody) => void): void;
601
+ private applyRecordToBody;
602
+ /**
603
+ * Creates bodies for new instances and destroys bodies whose instance is gone. Runs once per
604
+ * tick, right before the step, in collection order then instance order.
605
+ */
606
+ reconcile(): void;
607
+ step(): void;
608
+ /**
609
+ * Body → schema, through the tracked proxies, so movement leaves the room as an ordinary delta.
610
+ * Values are `Math.fround`ed for f32 fields, so room code reads exactly what the wire carries.
611
+ */
612
+ sync(): void;
613
+ /**
614
+ * The same `PhysicsSection` the 3D runtime writes — a real engine snapshot plus the
615
+ * entity↔handle map, whose handles must ride as f64 for the reason spelled out on
616
+ * {@link PhysicsSection}. What tells the two apart on the way back in is the envelope
617
+ * `core/physics.ts` wraps this in, not anything in here.
618
+ */
619
+ serialize(): PhysicsSection;
620
+ }
621
+
477
622
  /**
478
623
  * `RoomCore` — the host-agnostic room runtime. It owns the extended schema, the
479
624
  * tracked authority state, presence, the loop, and the frame dispatch; everything outside comes
@@ -494,7 +639,7 @@ declare class RoomCore<S extends AnySchema = AnySchema> implements RoomCoreApi<S
494
639
  readonly clients: Map<string, ClientEntry>;
495
640
  readonly loop: Loop;
496
641
  /** D22: the Rapier world, or `undefined` in a room whose config declares no physics. */
497
- readonly physics: PhysicsRuntime | MatterRuntime | undefined;
642
+ readonly physics: PhysicsRuntime | MatterRuntime | Rapier2dRuntime | undefined;
498
643
  readonly stats: RoomStats;
499
644
  tick: number;
500
645
  stopped: boolean;
@@ -516,6 +661,13 @@ declare class RoomCore<S extends AnySchema = AnySchema> implements RoomCoreApi<S
516
661
  * cost the profiler has is behind this `undefined`.
517
662
  */
518
663
  private readonly ledger;
664
+ /**
665
+ * D72: the pose history and the rewind scratch, present only when the room's physics config
666
+ * declares `history`. Every cost this lane has is behind this `undefined`, and it is
667
+ * deliberately not in the hibernation blob: a woken room starts with an empty buffer and fills
668
+ * it again over its next `history` ticks.
669
+ */
670
+ private readonly rewindState;
519
671
  private readonly seed;
520
672
  private readonly api;
521
673
  private readonly internals;
@@ -562,6 +714,12 @@ declare class RoomCore<S extends AnySchema = AnySchema> implements RoomCoreApi<S
562
714
  * worth logging.
563
715
  */
564
716
  private buildMatter;
717
+ /**
718
+ * The rapier2d half. It follows the *rapier3d* shape rather than matter2d's, because the 2D
719
+ * build has a real `takeSnapshot()`: a restored world comes back whole and `setup` does not run
720
+ * again. The only thing that differs from `buildPhysics` is which engine and which envelope.
721
+ */
722
+ private buildRapier2d;
565
723
  /** Convenience for hosts: `RoomCore.restore(def, bytes, host, opts)`. */
566
724
  static restore<S2 extends AnySchema>(definition: RoomDefinition<S2>, bytes: Uint8Array, host: RoomHost, options: Omit<RoomCoreOptions, 'restoreFrom'>): RoomCore<S2>;
567
725
  get schema(): S;
@@ -581,6 +739,17 @@ declare class RoomCore<S extends AnySchema = AnySchema> implements RoomCoreApi<S
581
739
  recording(): TimelineDump | undefined;
582
740
  /** D41: called at the end of every tick (and every event-mode flush). No-op when unarmed. */
583
741
  captureTimeline(): void;
742
+ /**
743
+ * D72: record this tick's body poses, right after `physics.sync()` — the poses the clients are
744
+ * about to be told about, under the tick number they will be told it under, which is the tick a
745
+ * client later stamps its `CALL` with.
746
+ */
747
+ captureHistory(): void;
748
+ /**
749
+ * D72: `room.rewind(tick, fn)`. The live world is not touched and nothing is re-simulated; `fn`
750
+ * queries a scratch world holding every tracked body at its pose at `tick`.
751
+ */
752
+ rewind<T>(tick: number, fn: (past: RewindView) => T): T;
584
753
  /** Live JSON view of the room for the dev page / supervisor admin API. */
585
754
  inspect(): RoomInspection;
586
755
  guard<T>(name: string, fn: () => T): T | undefined;
@@ -662,4 +831,4 @@ declare class RoomCore<S extends AnySchema = AnySchema> implements RoomCoreApi<S
662
831
  flush(): void;
663
832
  }
664
833
 
665
- export { CRASH_AFTER_THROWS as C, MAX_CATCHUP as M, type PhysicsEngineTag as P, RoomCore as R, type MatterBodyRecord as a, type MatterSection as b, Mulberry32 as c, type PhysicsSection as d, decodeMatterBodies as e, decodeMatterSectionEnvelope as f, decodePhysicsSection as g, encodeMatterBodies as h, encodeMatterSectionEnvelope as i, encodePhysicsSection as j, initMatter as k, initPhysics as l, loadedMatter as m, loadedPhysics as n, resetPhysicsForTests as o, physicsSectionEngine as p, resetMatterForTests as r };
834
+ export { CRASH_AFTER_THROWS as C, MAX_CATCHUP as M, type PhysicsEngineTag as P, RoomCore as R, type MatterBodyRecord as a, type MatterSection as b, Mulberry32 as c, type PhysicsSection as d, decodeMatterBodies as e, decodeMatterSectionEnvelope as f, decodePhysicsSection as g, decodeRapier2dSectionEnvelope as h, encodeMatterBodies as i, encodeMatterSectionEnvelope as j, encodePhysicsSection as k, encodeRapier2dSectionEnvelope as l, initMatter as m, initPhysics as n, initRapier2d as o, loadedMatter as p, loadedPhysics as q, loadedRapier2d as r, physicsSectionEngine as s, resetMatterForTests as t, resetPhysicsForTests as u, resetRapier2dForTests as v };
@@ -1,8 +1,8 @@
1
- import { R as RoomCore } from '../room-CfnEjlcg.js';
2
- export { k as initMatter, l as initPhysics } from '../room-CfnEjlcg.js';
1
+ import { R as RoomCore } from '../room-YJGz6Caw.js';
2
+ export { m as initMatter, n as initPhysics, o as initRapier2d } from '../room-YJGz6Caw.js';
3
3
  import { ErrorCodeName, FrameType } from '@irtio/protocol';
4
4
  import { NpcConfig, LeaveReason, Room, RoomDefinition } from '@irtio/server';
5
- import { i as RoomHost, L as LogLevel, R as RoomCoreApi, b as HostCall, c as HostCallResult, k as RoomStats } from '../contract-BjMsoJIV.js';
5
+ import { i as RoomHost, L as LogLevel, R as RoomCoreApi, b as HostCall, c as HostCallResult, k as RoomStats } from '../contract-CRilLVdx.js';
6
6
  import { AnySchema, PlainState, EntityCollection, State } from '@irtio/schema';
7
7
 
8
8
  /**
@@ -166,6 +166,17 @@ declare class HarnessHost implements RoomHost {
166
166
  /** D63-e: the harness has no room list to report on, so it records the value and stops. */
167
167
  backfillOpen: boolean | undefined;
168
168
  setBackfill(open: boolean): void;
169
+ /** D75: recorded, like the backfill flag, and for the same reason — no room list to report on. */
170
+ lobby: {
171
+ public?: boolean;
172
+ queue?: string;
173
+ started?: boolean;
174
+ };
175
+ setLobby(update: {
176
+ readonly public?: boolean;
177
+ readonly queue?: string;
178
+ readonly started?: boolean;
179
+ }): void;
169
180
  busPublish(channel: string, payload: string): void;
170
181
  busSubscribe(channel: string, subscribed: boolean): void;
171
182
  setAlarm(name: string, atMs: number | undefined): void;
@@ -3,8 +3,9 @@ import {
3
3
  createVisibilityPolicy,
4
4
  initMatter,
5
5
  initPhysics,
6
+ initRapier2d,
6
7
  visibleNames
7
- } from "../chunk-K42HA75G.js";
8
+ } from "../chunk-ZCD2TBXB.js";
8
9
 
9
10
  // src/test/clock.ts
10
11
  var FakeClock = class {
@@ -197,7 +198,7 @@ var HarnessHost = class {
197
198
  return { ok: true };
198
199
  }
199
200
  case "lbSubmit": {
200
- const key = `${call.board} ${call.playerId}`;
201
+ const key = call.bucket === void 0 ? `${call.board} ${call.playerId}` : `${call.board} ${call.bucket} ${call.playerId}`;
201
202
  const existing = this.scores.get(key);
202
203
  if (existing === void 0 || call.score > existing) this.scores.set(key, call.score);
203
204
  return { ok: true };
@@ -235,6 +236,12 @@ var HarnessHost = class {
235
236
  setBackfill(open) {
236
237
  this.backfillOpen = open;
237
238
  }
239
+ // ---- M6 lane H: lobby front door ----
240
+ /** D75: recorded, like the backfill flag, and for the same reason — no room list to report on. */
241
+ lobby = {};
242
+ setLobby(update) {
243
+ this.lobby = { ...this.lobby, ...update };
244
+ }
238
245
  busPublish(channel, payload) {
239
246
  this.busPublishes.push({ channel, payload });
240
247
  }
@@ -974,5 +981,6 @@ export {
974
981
  createRoomHarness,
975
982
  frameTypeName,
976
983
  initMatter,
977
- initPhysics
984
+ initPhysics,
985
+ initRapier2d
978
986
  };
@@ -1,4 +1,4 @@
1
- import { L as LogLevel, T as TimelineDump, k as RoomStats, b as HostCall, c as HostCallResult, R as RoomCoreApi, i as RoomHost } from '../contract-BjMsoJIV.js';
1
+ import { L as LogLevel, T as TimelineDump, k as RoomStats, b as HostCall, c as HostCallResult, R as RoomCoreApi, i as RoomHost } from '../contract-CRilLVdx.js';
2
2
  import { ErrorCodeName, ProfileSnapshot } from '@irtio/protocol';
3
3
  import { NpcConfig, LeaveReason } from '@irtio/server';
4
4
  import '@irtio/schema';
@@ -151,6 +151,9 @@ type FromWorker = {
151
151
  /** D63-e: the room type's declared backfill eligibility. The ceiling on
152
152
  * `room.backfill.set`, and `false` for every room that declares nothing. */
153
153
  backfill?: boolean;
154
+ /** D75: the room type declared a lobby. The ceiling on `setLobby`, and the supervisor
155
+ * re-checks it for exactly the reason it re-checks `backfill`. */
156
+ lobby?: boolean;
154
157
  /** D47: the physics engine this room runs, when it declares one. Reported so room-hours
155
158
  * rows carry the engine as a dimension; the supervisor never acts on it. */
156
159
  engine?: string;
@@ -289,6 +292,11 @@ type FromWorker = {
289
292
  | {
290
293
  t: 'setBackfill';
291
294
  open: boolean;
295
+ } | {
296
+ t: 'setLobby';
297
+ public?: boolean;
298
+ queue?: string;
299
+ started?: boolean;
292
300
  }
293
301
  /**
294
302
  * D59: `room.bus.publish(channel, payload)`. Fire-and-forget by design — the room gets no
@@ -1,14 +1,15 @@
1
1
  import {
2
2
  migrateSnapshot
3
- } from "../chunk-VMCE3LRO.js";
3
+ } from "../chunk-DBGMG2S3.js";
4
4
  import {
5
5
  RoomCore,
6
6
  RoomFullError,
7
7
  initMatter,
8
8
  initPhysics,
9
+ initRapier2d,
9
10
  onFirstRapierStep,
10
11
  rapierHasStepped
11
- } from "../chunk-K42HA75G.js";
12
+ } from "../chunk-ZCD2TBXB.js";
12
13
 
13
14
  // src/worker/index.ts
14
15
  import { getHeapStatistics } from "v8";
@@ -79,6 +80,10 @@ function createWorkerHost(post) {
79
80
  setBackfill(open) {
80
81
  post({ t: "setBackfill", open });
81
82
  },
83
+ // ---- M6 lane H: lobby front door ----
84
+ setLobby(update) {
85
+ post({ t: "setLobby", ...update });
86
+ },
82
87
  setAlarm(name, atMs) {
83
88
  if (atMs === void 0) {
84
89
  post({ t: "setAlarm", name });
@@ -153,7 +158,8 @@ async function handleMessage(state, host, post, msg) {
153
158
  try {
154
159
  if (def.config.physics.engine === "matter2d") await initMatter();
155
160
  else {
156
- await initPhysics();
161
+ if (def.config.physics.engine === "rapier2d") await initRapier2d();
162
+ else await initPhysics();
157
163
  onFirstRapierStep(() => {
158
164
  const core = state.core;
159
165
  if (core) post(statsPayload(core.stats, void 0, core.profile()));
@@ -211,6 +217,11 @@ async function handleMessage(state, host, post, msg) {
211
217
  // `engine` is: `ready.config` replaces the supervisor's copy, so a flag threaded
212
218
  // only through `loadBundle` would be dropped the moment a worker started.
213
219
  backfill: c.backfill === true,
220
+ // ---- M6 lane H: lobby front door ----
221
+ // D75: the declared ceiling on `setLobby`, carried for exactly `backfill`'s reason.
222
+ // The supervisor re-checks it, because a check only the tenant's own worker makes is
223
+ // advice rather than a boundary.
224
+ lobby: c.lobby !== void 0,
214
225
  // D47: `ready.config` REPLACES the supervisor's copy of the bundle config, so an
215
226
  // engine threaded only through `loadBundle` would be dropped here the moment a
216
227
  // worker started. That is exactly how the first known-shape run read `engine: none`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@irtio/runtime",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "irtio room runtime: RoomCore, worker_threads host, in-process test harness",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -28,11 +28,12 @@
28
28
  "dist"
29
29
  ],
30
30
  "dependencies": {
31
+ "@dimforge/rapier2d-compat": "0.20.0",
31
32
  "@dimforge/rapier3d-compat": "0.20.0",
32
33
  "matter-js": "0.20.0",
33
- "@irtio/protocol": "0.6.0",
34
- "@irtio/schema": "0.6.0",
35
- "@irtio/server": "0.6.0"
34
+ "@irtio/protocol": "0.8.0",
35
+ "@irtio/schema": "0.8.0",
36
+ "@irtio/server": "0.8.0"
36
37
  },
37
38
  "devDependencies": {
38
39
  "@types/matter-js": "0.20.2"