@irtio/runtime 0.9.0 → 0.10.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.
@@ -1079,7 +1079,8 @@ var MatterRuntime = class {
1079
1079
  const plainColl = plainEntity(this.core.plain, desc.name);
1080
1080
  for (const id of plainColl.ids()) {
1081
1081
  const attached = this.bodies.get(bodyKey(desc.name, id));
1082
- if (attached) fn(desc.name, id, attached.body);
1082
+ if (attached)
1083
+ fn(desc.name, id, attached.body, plainColl.get(id));
1083
1084
  }
1084
1085
  }
1085
1086
  }
@@ -1534,7 +1535,7 @@ var PhysicsRuntime = class {
1534
1535
  const plainColl = plainEntity(this.core.plain, desc.name);
1535
1536
  for (const id of plainColl.ids()) {
1536
1537
  const body = this.bodies.get(bodyKey2(desc.name, id));
1537
- if (body) fn(desc.name, id, body);
1538
+ if (body) fn(desc.name, id, body, plainColl.get(id));
1538
1539
  }
1539
1540
  }
1540
1541
  }
@@ -1939,7 +1940,7 @@ var Rapier2dRuntime = class {
1939
1940
  const plainColl = plainEntity(this.core.plain, desc.name);
1940
1941
  for (const id of plainColl.ids()) {
1941
1942
  const body = this.bodies.get(bodyKey3(desc.name, id));
1942
- if (body) fn(desc.name, id, body);
1943
+ if (body) fn(desc.name, id, body, plainColl.get(id));
1943
1944
  }
1944
1945
  }
1945
1946
  }
@@ -2456,23 +2457,45 @@ import {
2456
2457
 
2457
2458
  // src/core/history.ts
2458
2459
  var HISTORY_MAX_TICKS = 240;
2460
+ var HISTORY_MAX_CHANNELS = 8;
2459
2461
  var STRIDE = 13;
2460
2462
  function emptyPose() {
2461
2463
  return { x: 0, y: 0, z: 0, qx: 0, qy: 0, qz: 0, qw: 1, vx: 0, vy: 0, vz: 0, wx: 0, wy: 0, wz: 0 };
2462
2464
  }
2463
2465
  function newEntry() {
2464
- return { tick: -1, count: 0, collections: [], ids: [], values: new Float64Array(0) };
2466
+ return {
2467
+ tick: -1,
2468
+ count: 0,
2469
+ collections: [],
2470
+ ids: [],
2471
+ values: new Float64Array(0),
2472
+ channels: new Float32Array(0)
2473
+ };
2465
2474
  }
2466
2475
  var PoseHistory = class {
2467
2476
  depth;
2477
+ /** Declared channels by collection. Empty in a room that declared none. */
2478
+ channels;
2479
+ /**
2480
+ * Slots per body in `entry.channels`: the widest declared count, so a body's channels sit at
2481
+ * `i * channelStride` under the same body index the pose uses. `0` when nothing is declared, and
2482
+ * that zero is what short-circuits the whole channel path per body.
2483
+ */
2484
+ channelStride;
2468
2485
  entries = [];
2469
2486
  /** Index of the newest entry in `entries`, or -1 when nothing has been captured. */
2470
2487
  head = -1;
2471
2488
  size = 0;
2472
2489
  /** Reused across every body of every capture: the capture path allocates nothing per body. */
2473
2490
  scratchPose = emptyPose();
2474
- constructor(depth) {
2491
+ /** Collections whose `read` hook has already thrown, so the log is one line, not one per tick. */
2492
+ hookFailed = /* @__PURE__ */ new Set();
2493
+ constructor(depth, channels) {
2475
2494
  this.depth = Math.max(1, Math.min(HISTORY_MAX_TICKS, Math.floor(depth)));
2495
+ this.channels = channels ?? /* @__PURE__ */ new Map();
2496
+ let stride = 0;
2497
+ for (const spec of this.channels.values()) stride = Math.max(stride, spec.count);
2498
+ this.channelStride = stride;
2476
2499
  for (let i = 0; i < this.depth; i++) this.entries.push(newEntry());
2477
2500
  }
2478
2501
  get length() {
@@ -2502,14 +2525,14 @@ var PoseHistory = class {
2502
2525
  entry.count = 0;
2503
2526
  const pose = this.scratchPose;
2504
2527
  const kind = physics.engineKind;
2505
- physics.eachTrackedBody((collection, id, body) => {
2528
+ physics.eachTrackedBody((collection, id, body, instance) => {
2506
2529
  if (kind === "rapier3d") readRapierPose(body, pose);
2507
2530
  else if (kind === "rapier2d") readRapier2dPose(body, pose);
2508
2531
  else readMatterPose(body, pose);
2509
- this.push(entry, collection, id, pose);
2532
+ this.push(entry, collection, id, pose, instance);
2510
2533
  });
2511
2534
  }
2512
- push(entry, collection, id, pose) {
2535
+ push(entry, collection, id, pose, instance) {
2513
2536
  const i = entry.count;
2514
2537
  const need = (i + 1) * STRIDE;
2515
2538
  if (entry.values.length < need) {
@@ -2517,6 +2540,34 @@ var PoseHistory = class {
2517
2540
  grown.set(entry.values);
2518
2541
  entry.values = grown;
2519
2542
  }
2543
+ const stride = this.channelStride;
2544
+ if (stride > 0) {
2545
+ const want = (i + 1) * stride;
2546
+ if (entry.channels.length < want) {
2547
+ const grown = new Float32Array(Math.max(want, entry.channels.length * 2, stride * 8));
2548
+ grown.set(entry.channels);
2549
+ entry.channels = grown;
2550
+ }
2551
+ const o2 = i * stride;
2552
+ entry.channels.fill(0, o2, o2 + stride);
2553
+ const spec = this.channels.get(collection);
2554
+ if (spec) {
2555
+ const scratch = spec.scratch;
2556
+ scratch.fill(0);
2557
+ try {
2558
+ spec.read(instance, scratch);
2559
+ entry.channels.set(scratch, o2);
2560
+ } catch (err) {
2561
+ if (!this.hookFailed.has(collection)) {
2562
+ this.hookFailed.add(collection);
2563
+ console.error(
2564
+ `physics.history.channels.${collection}.read threw; that body's channels are zeros`,
2565
+ err
2566
+ );
2567
+ }
2568
+ }
2569
+ }
2570
+ }
2520
2571
  entry.collections[i] = collection;
2521
2572
  entry.ids[i] = id;
2522
2573
  const v = entry.values;
@@ -2587,6 +2638,10 @@ function poseAt(entry, index, into) {
2587
2638
  into.wy = v[o + 11];
2588
2639
  into.wz = v[o + 12];
2589
2640
  }
2641
+ function channelsAt(entry, index, stride, count) {
2642
+ const o = index * stride;
2643
+ return entry.channels.subarray(o, o + count);
2644
+ }
2590
2645
  function readRapierPose(body, into) {
2591
2646
  const t = body.translation();
2592
2647
  const r = body.rotation();
@@ -2639,10 +2694,20 @@ function readMatterPose(body, into) {
2639
2694
  into.wy = 0;
2640
2695
  into.wz = body.angularVelocity;
2641
2696
  }
2642
- function historyDepthOf(config) {
2697
+ function historyOf(config) {
2643
2698
  const declared = config?.history;
2644
- if (typeof declared !== "number" || !Number.isFinite(declared) || declared <= 0) return void 0;
2645
- return Math.min(HISTORY_MAX_TICKS, Math.floor(declared));
2699
+ const raw = typeof declared === "number" ? declared : typeof declared === "object" && declared !== null ? declared.depth : void 0;
2700
+ if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0) return void 0;
2701
+ const depth = Math.min(HISTORY_MAX_TICKS, Math.floor(raw));
2702
+ const channels = /* @__PURE__ */ new Map();
2703
+ if (typeof declared === "object" && declared !== null) {
2704
+ const spec = declared.channels;
2705
+ for (const [name, s] of Object.entries(spec ?? {})) {
2706
+ const count = Math.min(HISTORY_MAX_CHANNELS, Math.floor(s.count));
2707
+ channels.set(name, { count, read: s.read, scratch: new Float32Array(count) });
2708
+ }
2709
+ }
2710
+ return { depth, channels };
2646
2711
  }
2647
2712
  function keyOf(collection, id) {
2648
2713
  return `${collection} ${id}`;
@@ -2992,8 +3057,8 @@ var RewindState = class {
2992
3057
  history;
2993
3058
  scratch;
2994
3059
  inside = false;
2995
- constructor(depth) {
2996
- this.history = new PoseHistory(depth);
3060
+ constructor(setup) {
3061
+ this.history = new PoseHistory(setup.depth, setup.channels);
2997
3062
  }
2998
3063
  free() {
2999
3064
  this.scratch?.free();
@@ -3012,7 +3077,29 @@ var RewindState = class {
3012
3077
  );
3013
3078
  }
3014
3079
  this.scratch ??= physics.engineKind === "rapier3d" ? new RapierScratch(physics, this.history.depth, tickNow) : physics.engineKind === "rapier2d" ? new Rapier2dScratch(physics, this.history.depth, tickNow) : new MatterScratch(physics, this.history.depth, tickNow);
3015
- const view = this.scratch.view(resolved.entry, resolved.tick, requested, resolved.clamped);
3080
+ const engineView = this.scratch.view(
3081
+ resolved.entry,
3082
+ resolved.tick,
3083
+ requested,
3084
+ resolved.clamped
3085
+ );
3086
+ const entry = resolved.entry;
3087
+ const history = this.history;
3088
+ const view = {
3089
+ ...engineView,
3090
+ // A linear scan of the entry rather than an index: an entry holds one room's tracked bodies,
3091
+ // and building a map per rewind would cost more than the scan it saved.
3092
+ channels: (collection, id) => {
3093
+ const spec = history.channels.get(collection);
3094
+ if (!spec) return void 0;
3095
+ for (let i = 0; i < entry.count; i++) {
3096
+ if (entry.collections[i] === collection && entry.ids[i] === id) {
3097
+ return channelsAt(entry, i, history.channelStride, spec.count);
3098
+ }
3099
+ }
3100
+ return void 0;
3101
+ }
3102
+ };
3016
3103
  this.inside = true;
3017
3104
  try {
3018
3105
  return fn(view);
@@ -3896,8 +3983,8 @@ var RoomCore = class _RoomCore {
3896
3983
  this.loop = new Loop(self);
3897
3984
  this.api = createRoomApi(self, options.publicUrl ?? DEFAULT_PUBLIC_URL);
3898
3985
  this.physics = this.buildPhysics(restored);
3899
- const historyDepth = this.physics ? historyDepthOf(definition.config.physics) : void 0;
3900
- this.rewindState = historyDepth === void 0 ? void 0 : new RewindState(historyDepth);
3986
+ const historySetup = this.physics ? historyOf(definition.config.physics) : void 0;
3987
+ this.rewindState = historySetup === void 0 ? void 0 : new RewindState(historySetup);
3901
3988
  this.subscribeDeclaredChannels();
3902
3989
  if (restored) {
3903
3990
  const onWake = definition.config.onWake;
@@ -3984,7 +4071,7 @@ var RoomCore = class _RoomCore {
3984
4071
  const engine4 = loadedPhysics();
3985
4072
  if (!engine4) {
3986
4073
  throw new Error(
3987
- "RoomCore: this room declares physics but the engine is not initialized \u2014 the host must `await initPhysics()` (from '@irtio/runtime') before constructing the room; handlers are synchronous, so the WASM cannot be loaded later"
4074
+ "RoomCore: this room declares physics but the engine is not initialized \u2014 the host must `await initPhysics()` (in tests: from '@irtio/testing') before constructing the room; handlers are synchronous, so the WASM cannot be loaded later"
3988
4075
  );
3989
4076
  }
3990
4077
  let section;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  parseHibernationBlob,
3
3
  writeHibernationBlob
4
- } from "./chunk-ADZZEVJU.js";
4
+ } from "./chunk-KGZEQTJR.js";
5
5
 
6
6
  // src/migrate.ts
7
7
  import { PRESENCE_COLLECTION, withBuiltins } from "@irtio/protocol";
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
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-C5aqs49-.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-C4RZJ2KO.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-Db3iPBKM.js';
3
3
  import { CollectionDesc, AnySchema, PlainState, DirtySet } from '@irtio/schema';
4
4
  import '@irtio/protocol';
5
5
  import '@irtio/server';
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  fromMigrationState,
3
3
  migrateSnapshot,
4
4
  toMigrationState
5
- } from "./chunk-SBDZ45JG.js";
5
+ } from "./chunk-MTBMGUGG.js";
6
6
  import {
7
7
  CRASH_AFTER_THROWS,
8
8
  DEFAULT_TIMELINE_MAX_RECORDS,
@@ -47,7 +47,7 @@ import {
47
47
  visibleNames,
48
48
  visibleTo,
49
49
  writeHibernationBlob
50
- } from "./chunk-ADZZEVJU.js";
50
+ } from "./chunk-KGZEQTJR.js";
51
51
  export {
52
52
  CRASH_AFTER_THROWS,
53
53
  DEFAULT_TIMELINE_MAX_RECORDS,
@@ -105,7 +105,7 @@ interface PhysicsApi {
105
105
  * the only thing outside the engine runtimes that sees a live body other than through
106
106
  * `bodyFor`, and it never hands out the map itself.
107
107
  */
108
- eachTrackedBody(fn: (collection: string, id: string, body: unknown) => void): void;
108
+ eachTrackedBody(fn: (collection: string, id: string, body: unknown, instance: Record<string, unknown>) => void): void;
109
109
  /** rapier3d and rapier2d: the engine namespace. `engineKind` says which module it is. */
110
110
  readonly rapier: unknown;
111
111
  /** rapier3d and rapier2d: the live `World`. */
@@ -363,7 +363,7 @@ declare class MatterRuntime {
363
363
  * D72: every tracked body, in the order `sync()` walks them. The matter half of the same
364
364
  * read-only accessor `PhysicsRuntime` carries; see its comment for what calls it and when.
365
365
  */
366
- eachTrackedBody(fn: (collection: string, id: string, body: MatterBody) => void): void;
366
+ eachTrackedBody(fn: (collection: string, id: string, body: MatterBody, instance: Record<string, unknown>) => void): void;
367
367
  private applyState;
368
368
  private applyRecordToBody;
369
369
  reconcile(): void;
@@ -533,7 +533,7 @@ declare class PhysicsRuntime {
533
533
  * The pose history calls this once per tick and the rewind scratch calls it per rewind; a room
534
534
  * that declares no `physics.history` never calls it at all.
535
535
  */
536
- eachTrackedBody(fn: (collection: string, id: string, body: RapierRigidBody) => void): void;
536
+ eachTrackedBody(fn: (collection: string, id: string, body: RapierRigidBody, instance: Record<string, unknown>) => void): void;
537
537
  private applyRecordToBody;
538
538
  /**
539
539
  * Creates bodies for new instances and destroys bodies whose instance is gone. Runs once per
@@ -650,7 +650,7 @@ declare class Rapier2dRuntime {
650
650
  * D72: every tracked body, in the order `sync()` walks them (collection order, then instance
651
651
  * order). The rapier2d third of the same read-only accessor the other two runtimes carry.
652
652
  */
653
- eachTrackedBody(fn: (collection: string, id: string, body: Rapier2dRigidBody) => void): void;
653
+ eachTrackedBody(fn: (collection: string, id: string, body: Rapier2dRigidBody, instance: Record<string, unknown>) => void): void;
654
654
  private applyRecordToBody;
655
655
  /**
656
656
  * Creates bodies for new instances and destroys bodies whose instance is gone. Runs once per
@@ -1,5 +1,5 @@
1
- import { R as RoomCore } from '../room-C4RZJ2KO.js';
2
- export { m as initMatter, n as initPhysics, o as initRapier2d } from '../room-C4RZJ2KO.js';
1
+ import { R as RoomCore } from '../room-Db3iPBKM.js';
2
+ export { m as initMatter, n as initPhysics, o as initRapier2d } from '../room-Db3iPBKM.js';
3
3
  import { ErrorCodeName, FrameType } from '@irtio/protocol';
4
4
  import { NpcConfig, LeaveReason, Room, RoomDefinition } from '@irtio/server';
5
5
  import { i as RoomHost, L as LogLevel, R as RoomCoreApi, b as HostCall, c as HostCallResult, k as RoomStats } from '../contract-C5aqs49-.js';
@@ -5,7 +5,7 @@ import {
5
5
  initPhysics,
6
6
  initRapier2d,
7
7
  visibleNames
8
- } from "../chunk-ADZZEVJU.js";
8
+ } from "../chunk-KGZEQTJR.js";
9
9
 
10
10
  // src/test/clock.ts
11
11
  var FakeClock = class {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  migrateSnapshot
3
- } from "../chunk-SBDZ45JG.js";
3
+ } from "../chunk-MTBMGUGG.js";
4
4
  import {
5
5
  RoomCore,
6
6
  RoomFullError,
@@ -9,7 +9,7 @@ import {
9
9
  initRapier2d,
10
10
  onFirstRapierStep,
11
11
  rapierHasStepped
12
- } from "../chunk-ADZZEVJU.js";
12
+ } from "../chunk-KGZEQTJR.js";
13
13
 
14
14
  // src/worker/index.ts
15
15
  import { getHeapStatistics } from "v8";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@irtio/runtime",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "irtio room runtime: RoomCore, worker_threads host, in-process test harness",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -31,9 +31,9 @@
31
31
  "@dimforge/rapier2d-compat": "0.20.0",
32
32
  "@dimforge/rapier3d-compat": "0.20.0",
33
33
  "matter-js": "0.20.0",
34
- "@irtio/schema": "0.9.0",
35
- "@irtio/server": "0.9.0",
36
- "@irtio/protocol": "0.9.0"
34
+ "@irtio/protocol": "0.10.0",
35
+ "@irtio/schema": "0.10.0",
36
+ "@irtio/server": "0.10.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/matter-js": "0.20.2"