@irtio/runtime 0.6.0 → 0.7.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.
@@ -143,10 +143,10 @@ function handleCall(core, clientId, payload) {
143
143
  return true;
144
144
  }
145
145
  }
146
- const ctx = core.ctxFor(clientId);
146
+ const ctx = { ...core.ctxFor(clientId), clientTick: call.clientTick || void 0 };
147
147
  let result;
148
148
  if (desc.name === REQUEST_OWNERSHIP) {
149
- result = runRequestOwnership(core, clientId, params);
149
+ result = runRequestOwnership(core, clientId, ctx, params);
150
150
  } else {
151
151
  const impl = core.definition.config.rpc?.[desc.name];
152
152
  if (!impl) {
@@ -175,7 +175,7 @@ function handleCall(core, clientId, payload) {
175
175
  }
176
176
  return true;
177
177
  }
178
- function runRequestOwnership(core, clientId, params) {
178
+ function runRequestOwnership(core, clientId, ctx, params) {
179
179
  const name = String(params.entity ?? "");
180
180
  const id = String(params.id ?? "");
181
181
  const c = collectionDescOf(core.ext, name);
@@ -185,10 +185,7 @@ function runRequestOwnership(core, clientId, params) {
185
185
  const handler = core.definition.config.onOwnershipRequest;
186
186
  const tracked = trackedEntity(core, name);
187
187
  if (handler) {
188
- const ran = core.tryRun(
189
- "onOwnershipRequest",
190
- () => handler(core.anyState, name, id, core.ctxFor(clientId))
191
- );
188
+ const ran = core.tryRun("onOwnershipRequest", () => handler(core.anyState, name, id, ctx));
192
189
  if (ran.ok && ran.value === true && plain.ownerOf(id) !== clientId) {
193
190
  tracked.setOwner(id, clientId);
194
191
  }
@@ -762,6 +759,7 @@ var Loop = class {
762
759
  physics.reconcile();
763
760
  physics.step();
764
761
  physics.sync();
762
+ this.core.captureHistory();
765
763
  });
766
764
  if (!ran.ok) {
767
765
  failed = failed === void 0 ? "the physics step" : `${failed} and the physics step`;
@@ -1054,6 +1052,20 @@ var MatterRuntime = class {
1054
1052
  }
1055
1053
  return spec.body;
1056
1054
  }
1055
+ // ---- M6 lane F: rewind ----
1056
+ /**
1057
+ * D72: every tracked body, in the order `sync()` walks them. The matter half of the same
1058
+ * read-only accessor `PhysicsRuntime` carries; see its comment for what calls it and when.
1059
+ */
1060
+ eachTrackedBody(fn) {
1061
+ for (const desc of this.collections) {
1062
+ const plainColl = plainEntity(this.core.plain, desc.name);
1063
+ for (const id of plainColl.ids()) {
1064
+ const attached = this.bodies.get(bodyKey(desc.name, id));
1065
+ if (attached) fn(desc.name, id, attached.body);
1066
+ }
1067
+ }
1068
+ }
1057
1069
  applyState(body, s) {
1058
1070
  const M = this.matter;
1059
1071
  M.Body.setPosition(body, { x: s.x, y: s.y });
@@ -1396,6 +1408,22 @@ var PhysicsRuntime = class {
1396
1408
  this.bodies.set(bodyKey2(desc.name, id), body);
1397
1409
  return body;
1398
1410
  }
1411
+ // ---- M6 lane F: rewind ----
1412
+ /**
1413
+ * D72: every tracked body, in the order `sync()` walks them (collection order, then instance
1414
+ * order). Read-only: it hands out the live bodies and nothing else, and the map stays private.
1415
+ * The pose history calls this once per tick and the rewind scratch calls it per rewind; a room
1416
+ * that declares no `physics.history` never calls it at all.
1417
+ */
1418
+ eachTrackedBody(fn) {
1419
+ for (const desc of this.collections) {
1420
+ const plainColl = plainEntity(this.core.plain, desc.name);
1421
+ for (const id of plainColl.ids()) {
1422
+ const body = this.bodies.get(bodyKey2(desc.name, id));
1423
+ if (body) fn(desc.name, id, body);
1424
+ }
1425
+ }
1426
+ }
1399
1427
  applyRecordToBody(desc, body, record) {
1400
1428
  const physics = desc.physics;
1401
1429
  if (!physics) return;
@@ -1963,8 +1991,469 @@ import {
1963
1991
  validateForDeploy
1964
1992
  } from "@irtio/schema";
1965
1993
 
1994
+ // src/core/history.ts
1995
+ var HISTORY_MAX_TICKS = 240;
1996
+ var STRIDE = 13;
1997
+ function emptyPose() {
1998
+ 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 };
1999
+ }
2000
+ function newEntry() {
2001
+ return { tick: -1, count: 0, collections: [], ids: [], values: new Float64Array(0) };
2002
+ }
2003
+ var PoseHistory = class {
2004
+ depth;
2005
+ entries = [];
2006
+ /** Index of the newest entry in `entries`, or -1 when nothing has been captured. */
2007
+ head = -1;
2008
+ size = 0;
2009
+ /** Reused across every body of every capture: the capture path allocates nothing per body. */
2010
+ scratchPose = emptyPose();
2011
+ constructor(depth) {
2012
+ this.depth = Math.max(1, Math.min(HISTORY_MAX_TICKS, Math.floor(depth)));
2013
+ for (let i = 0; i < this.depth; i++) this.entries.push(newEntry());
2014
+ }
2015
+ get length() {
2016
+ return this.size;
2017
+ }
2018
+ /** The newest tick captured, or `undefined` when the buffer is empty. */
2019
+ get newestTick() {
2020
+ return this.size === 0 ? void 0 : this.entries[this.head].tick;
2021
+ }
2022
+ /** The oldest tick still held, or `undefined` when the buffer is empty. */
2023
+ get oldestTick() {
2024
+ if (this.size === 0) return void 0;
2025
+ const i = (this.head - (this.size - 1) + this.depth * 2) % this.depth;
2026
+ return this.entries[i].tick;
2027
+ }
2028
+ /** Drops everything. Used by the hibernation path, where a buffer cannot survive. */
2029
+ clear() {
2030
+ this.head = -1;
2031
+ this.size = 0;
2032
+ }
2033
+ /** Captures one tick's poses off the live runtime. Called right after `physics.sync()`. */
2034
+ capture(tick, physics) {
2035
+ this.head = this.size === 0 ? 0 : (this.head + 1) % this.depth;
2036
+ if (this.size < this.depth) this.size++;
2037
+ const entry = this.entries[this.head];
2038
+ entry.tick = tick;
2039
+ entry.count = 0;
2040
+ const pose = this.scratchPose;
2041
+ const rapier = physics.engineKind === "rapier3d";
2042
+ physics.eachTrackedBody((collection, id, body) => {
2043
+ if (rapier) readRapierPose(body, pose);
2044
+ else readMatterPose(body, pose);
2045
+ this.push(entry, collection, id, pose);
2046
+ });
2047
+ }
2048
+ push(entry, collection, id, pose) {
2049
+ const i = entry.count;
2050
+ const need = (i + 1) * STRIDE;
2051
+ if (entry.values.length < need) {
2052
+ const grown = new Float64Array(Math.max(need, entry.values.length * 2, STRIDE * 8));
2053
+ grown.set(entry.values);
2054
+ entry.values = grown;
2055
+ }
2056
+ entry.collections[i] = collection;
2057
+ entry.ids[i] = id;
2058
+ const v = entry.values;
2059
+ const o = i * STRIDE;
2060
+ v[o] = pose.x;
2061
+ v[o + 1] = pose.y;
2062
+ v[o + 2] = pose.z;
2063
+ v[o + 3] = pose.qx;
2064
+ v[o + 4] = pose.qy;
2065
+ v[o + 5] = pose.qz;
2066
+ v[o + 6] = pose.qw;
2067
+ v[o + 7] = pose.vx;
2068
+ v[o + 8] = pose.vy;
2069
+ v[o + 9] = pose.vz;
2070
+ v[o + 10] = pose.wx;
2071
+ v[o + 11] = pose.wy;
2072
+ v[o + 12] = pose.wz;
2073
+ entry.count = i + 1;
2074
+ }
2075
+ /**
2076
+ * The entry a rewind to `requested` answers from, clamped into the window the buffer actually
2077
+ * holds. `undefined` only when nothing has been captured at all.
2078
+ */
2079
+ resolve(requested) {
2080
+ if (this.size === 0) return void 0;
2081
+ const newest = this.entries[this.head].tick;
2082
+ const oldest = this.oldestTick;
2083
+ const want = Number.isFinite(requested) ? Math.floor(requested) : newest;
2084
+ const tick = want < oldest ? oldest : want > newest ? newest : want;
2085
+ const entry = this.at(tick);
2086
+ if (!entry) return void 0;
2087
+ return { entry, tick: entry.tick, clamped: tick !== want };
2088
+ }
2089
+ /**
2090
+ * The entry for exactly `tick`. Index arithmetic first (captures are one tick apart, so the
2091
+ * offset from the head is the tick difference), with a scan as insurance: a room whose loop
2092
+ * ever skipped a capture would otherwise be answered with the wrong tick's poses, and answering
2093
+ * for a tick that was not recorded is the one thing this buffer must never do.
2094
+ */
2095
+ at(tick) {
2096
+ const newest = this.entries[this.head].tick;
2097
+ const offset = newest - tick;
2098
+ if (offset >= 0 && offset < this.size) {
2099
+ const e = this.entries[(this.head - offset + this.depth * 2) % this.depth];
2100
+ if (e.tick === tick) return e;
2101
+ }
2102
+ for (let k = 0; k < this.size; k++) {
2103
+ const e = this.entries[(this.head - k + this.depth * 2) % this.depth];
2104
+ if (e.tick === tick) return e;
2105
+ }
2106
+ return void 0;
2107
+ }
2108
+ };
2109
+ function poseAt(entry, index, into) {
2110
+ const v = entry.values;
2111
+ const o = index * STRIDE;
2112
+ into.x = v[o];
2113
+ into.y = v[o + 1];
2114
+ into.z = v[o + 2];
2115
+ into.qx = v[o + 3];
2116
+ into.qy = v[o + 4];
2117
+ into.qz = v[o + 5];
2118
+ into.qw = v[o + 6];
2119
+ into.vx = v[o + 7];
2120
+ into.vy = v[o + 8];
2121
+ into.vz = v[o + 9];
2122
+ into.wx = v[o + 10];
2123
+ into.wy = v[o + 11];
2124
+ into.wz = v[o + 12];
2125
+ }
2126
+ function readRapierPose(body, into) {
2127
+ const t = body.translation();
2128
+ const r = body.rotation();
2129
+ const v = body.linvel();
2130
+ const w = body.angvel();
2131
+ into.x = t.x;
2132
+ into.y = t.y;
2133
+ into.z = t.z;
2134
+ into.qx = r.x;
2135
+ into.qy = r.y;
2136
+ into.qz = r.z;
2137
+ into.qw = r.w;
2138
+ into.vx = v.x;
2139
+ into.vy = v.y;
2140
+ into.vz = v.z;
2141
+ into.wx = w.x;
2142
+ into.wy = w.y;
2143
+ into.wz = w.z;
2144
+ }
2145
+ function readMatterPose(body, into) {
2146
+ into.x = body.position.x;
2147
+ into.y = body.position.y;
2148
+ into.z = 0;
2149
+ into.qx = 0;
2150
+ into.qy = 0;
2151
+ into.qz = Math.sin(body.angle / 2);
2152
+ into.qw = Math.cos(body.angle / 2);
2153
+ into.vx = body.velocity.x;
2154
+ into.vy = body.velocity.y;
2155
+ into.vz = 0;
2156
+ into.wx = 0;
2157
+ into.wy = 0;
2158
+ into.wz = body.angularVelocity;
2159
+ }
2160
+ function historyDepthOf(config) {
2161
+ const declared = config?.history;
2162
+ if (typeof declared !== "number" || !Number.isFinite(declared) || declared <= 0) return void 0;
2163
+ return Math.min(HISTORY_MAX_TICKS, Math.floor(declared));
2164
+ }
2165
+ function keyOf(collection, id) {
2166
+ return `${collection} ${id}`;
2167
+ }
2168
+ function angleOf(qz, qw) {
2169
+ return 2 * Math.atan2(qz, qw);
2170
+ }
2171
+ var BaseScratch = class {
2172
+ constructor(physics, depth, tickNow) {
2173
+ this.physics = physics;
2174
+ this.depth = depth;
2175
+ this.tickNow = tickNow;
2176
+ }
2177
+ physics;
2178
+ depth;
2179
+ tickNow;
2180
+ bodies = /* @__PURE__ */ new Map();
2181
+ present = /* @__PURE__ */ new Set();
2182
+ pose = emptyPose();
2183
+ view(entry, tick, requested, clamped) {
2184
+ this.sync();
2185
+ this.present.clear();
2186
+ for (let i = 0; i < entry.count; i++) {
2187
+ const key = keyOf(entry.collections[i], entry.ids[i]);
2188
+ const e = this.bodies.get(key);
2189
+ if (!e) continue;
2190
+ poseAt(entry, i, this.pose);
2191
+ this.place(e, this.pose);
2192
+ this.present.add(key);
2193
+ }
2194
+ for (const [key, e] of this.bodies) {
2195
+ if (!this.present.has(key)) this.hide(e);
2196
+ }
2197
+ return this.build(tick, requested, clamped);
2198
+ }
2199
+ /**
2200
+ * Brings the scratch's body set in step with the live world: a double for every live body that
2201
+ * has none, a rebuild for one whose collider (or part) count changed, and a prune for one that
2202
+ * has been gone longer than the history is deep and so can no longer appear in any entry.
2203
+ *
2204
+ * The prune is conservative rather than exact: `lastLive` only advances when a rewind happens,
2205
+ * so a room that rewinds rarely holds its dead doubles a little longer than it strictly must.
2206
+ * Over-retention is a few hundred bytes; under-retention would be a wrong answer.
2207
+ */
2208
+ sync() {
2209
+ const now = this.tickNow();
2210
+ const seen = /* @__PURE__ */ new Set();
2211
+ this.physics.eachTrackedBody((collection, id, body) => {
2212
+ const key = keyOf(collection, id);
2213
+ seen.add(key);
2214
+ const parts = this.partsOf(body);
2215
+ const existing = this.bodies.get(key);
2216
+ if (existing && existing.parts === parts) {
2217
+ existing.lastLive = now;
2218
+ return;
2219
+ }
2220
+ if (existing) {
2221
+ this.bodies.delete(key);
2222
+ this.destroy(existing);
2223
+ }
2224
+ const made = this.clone(collection, id, body, parts);
2225
+ if (!made) return;
2226
+ made.lastLive = now;
2227
+ this.bodies.set(key, made);
2228
+ });
2229
+ for (const [key, e] of [...this.bodies]) {
2230
+ if (seen.has(key)) continue;
2231
+ if (now - e.lastLive <= this.depth) continue;
2232
+ this.bodies.delete(key);
2233
+ this.destroy(e);
2234
+ }
2235
+ }
2236
+ };
2237
+ function qConj(q) {
2238
+ return { x: -q.x, y: -q.y, z: -q.z, w: q.w };
2239
+ }
2240
+ function qMul(a, b) {
2241
+ return {
2242
+ x: a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y,
2243
+ y: a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x,
2244
+ z: a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w,
2245
+ w: a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z
2246
+ };
2247
+ }
2248
+ function qRotate(q, v) {
2249
+ const p = qMul(qMul(q, { x: v.x, y: v.y, z: v.z, w: 0 }), qConj(q));
2250
+ return { x: p.x, y: p.y, z: p.z };
2251
+ }
2252
+ var RapierScratch = class extends BaseScratch {
2253
+ world;
2254
+ rapier;
2255
+ owners = /* @__PURE__ */ new Map();
2256
+ /** Live rigid-body handles the room tracks, so the statics pass knows what to skip. */
2257
+ trackedLive = /* @__PURE__ */ new Set();
2258
+ staticsCloned = false;
2259
+ constructor(physics, depth, tickNow) {
2260
+ super(physics, depth, tickNow);
2261
+ this.rapier = physics.rapier;
2262
+ this.world = new this.rapier.World({ x: 0, y: 0, z: 0 });
2263
+ this.world.timestep = 0;
2264
+ }
2265
+ partsOf(body) {
2266
+ return body.numColliders();
2267
+ }
2268
+ clone(collection, id, body, parts) {
2269
+ const live = body;
2270
+ this.trackedLive.add(live.handle);
2271
+ const made = this.world.createRigidBody(this.rapier.RigidBodyDesc.fixed());
2272
+ const origin = live.translation();
2273
+ const rot = live.rotation();
2274
+ const inv = qConj(rot);
2275
+ for (let i = 0; i < parts; i++) {
2276
+ const c = live.collider(i);
2277
+ const desc = new this.rapier.ColliderDesc(c.shape);
2278
+ desc.setSensor(c.isSensor());
2279
+ const w = c.translation();
2280
+ const local = qRotate(inv, { x: w.x - origin.x, y: w.y - origin.y, z: w.z - origin.z });
2281
+ desc.setTranslation(local.x, local.y, local.z);
2282
+ desc.setRotation(qMul(inv, c.rotation()));
2283
+ this.world.createCollider(desc, made);
2284
+ }
2285
+ this.owners.set(made.handle, { collection, id });
2286
+ return { collection, id, body: made, parts, lastLive: -1 };
2287
+ }
2288
+ place(entry, pose) {
2289
+ const b = entry.body;
2290
+ if (!b.isEnabled()) b.setEnabled(true);
2291
+ b.setTranslation({ x: pose.x, y: pose.y, z: pose.z }, false);
2292
+ b.setRotation({ x: pose.qx, y: pose.qy, z: pose.qz, w: pose.qw }, false);
2293
+ }
2294
+ /**
2295
+ * A body the requested tick did not have. `setEnabled(false)` takes the body and its colliders
2296
+ * out of the broad phase, so a query cannot hit it, which is the whole point: a body created
2297
+ * after the tick the shooter was looking at was not on their screen and must not be hittable.
2298
+ */
2299
+ hide(entry) {
2300
+ if (entry.body.isEnabled()) entry.body.setEnabled(false);
2301
+ }
2302
+ destroy(entry) {
2303
+ this.owners.delete(entry.body.handle);
2304
+ this.world.removeRigidBody(entry.body);
2305
+ }
2306
+ build(tick, requested, clamped) {
2307
+ this.cloneStatics();
2308
+ this.world.step();
2309
+ const owners = this.owners;
2310
+ return {
2311
+ tick,
2312
+ requested,
2313
+ clamped,
2314
+ rapier: {
2315
+ world: this.world,
2316
+ who: (collider) => {
2317
+ const parent = collider.parent();
2318
+ return parent ? owners.get(parent.handle) : void 0;
2319
+ }
2320
+ }
2321
+ };
2322
+ }
2323
+ /**
2324
+ * The live world's static geometry, everything `physics.setup` built, copied in once at the
2325
+ * first rewind. It is "as it stands now" rather than "as it stood then": static colliders do not
2326
+ * move, and a room that moves one has told the engine something that is not true.
2327
+ */
2328
+ cloneStatics() {
2329
+ if (this.staticsCloned) return;
2330
+ this.staticsCloned = true;
2331
+ const live = this.physics.world;
2332
+ if (!live) return;
2333
+ live.forEachCollider((c) => {
2334
+ const parent = c.parent();
2335
+ if (parent !== null && (!parent.isFixed() || this.trackedLive.has(parent.handle))) return;
2336
+ const desc = new this.rapier.ColliderDesc(c.shape);
2337
+ desc.setSensor(c.isSensor());
2338
+ const t = c.translation();
2339
+ desc.setTranslation(t.x, t.y, t.z);
2340
+ desc.setRotation(c.rotation());
2341
+ this.world.createCollider(desc);
2342
+ });
2343
+ }
2344
+ free() {
2345
+ this.owners.clear();
2346
+ this.world.free();
2347
+ }
2348
+ };
2349
+ var MatterScratch = class extends BaseScratch {
2350
+ matter;
2351
+ owners = /* @__PURE__ */ new Map();
2352
+ /** Rebuilt per rewind: the doubles the requested tick actually had, plus the live statics. */
2353
+ visible = [];
2354
+ constructor(physics, depth, tickNow) {
2355
+ super(physics, depth, tickNow);
2356
+ this.matter = physics.matter;
2357
+ }
2358
+ partsOf(body) {
2359
+ return body.parts.length;
2360
+ }
2361
+ clone(collection, id, body, parts) {
2362
+ const M = this.matter;
2363
+ const live = body;
2364
+ const made = live.parts.length > 1 ? M.Body.create({ parts: live.parts.slice(1).map((p) => this.clonePart(p, live.angle)) }) : this.clonePart(live, live.angle);
2365
+ M.Body.setPosition(made, { x: 0, y: 0 });
2366
+ made.angle = 0;
2367
+ for (const p of made.parts) p.angle = 0;
2368
+ this.owners.set(made, { collection, id });
2369
+ return { collection, id, body: made, parts, lastLive: -1 };
2370
+ }
2371
+ /** One part, un-rotated by the parent's angle so the double starts at angle 0. */
2372
+ clonePart(part, parentAngle) {
2373
+ const M = this.matter;
2374
+ const verts = part.vertices.map((v) => ({ x: v.x, y: v.y }));
2375
+ if (parentAngle !== 0) M.Vertices.rotate(verts, -parentAngle, part.position);
2376
+ const made = M.Body.create({});
2377
+ M.Body.setVertices(made, verts);
2378
+ return made;
2379
+ }
2380
+ place(entry, pose) {
2381
+ const M = this.matter;
2382
+ M.Body.setAngle(entry.body, angleOf(pose.qz, pose.qw));
2383
+ M.Body.setPosition(entry.body, { x: pose.x, y: pose.y });
2384
+ this.visible.push(entry.body);
2385
+ }
2386
+ /** Nothing to undo: `visible` is rebuilt per rewind, so a body nobody placed is not in it. */
2387
+ hide() {
2388
+ }
2389
+ destroy(entry) {
2390
+ this.owners.delete(entry.body);
2391
+ }
2392
+ view(entry, tick, requested, clamped) {
2393
+ this.visible.length = 0;
2394
+ return super.view(entry, tick, requested, clamped);
2395
+ }
2396
+ build(tick, requested, clamped) {
2397
+ const M = this.matter;
2398
+ const engine3 = this.physics.matterEngine;
2399
+ if (engine3) {
2400
+ for (const b of M.Composite.allBodies(engine3.world)) {
2401
+ if (b.isStatic) this.visible.push(b);
2402
+ }
2403
+ }
2404
+ const owners = this.owners;
2405
+ return {
2406
+ tick,
2407
+ requested,
2408
+ clamped,
2409
+ matter: {
2410
+ bodies: this.visible,
2411
+ who: (body) => owners.get(body)
2412
+ }
2413
+ };
2414
+ }
2415
+ free() {
2416
+ this.owners.clear();
2417
+ this.visible.length = 0;
2418
+ }
2419
+ };
2420
+ var RewindState = class {
2421
+ history;
2422
+ scratch;
2423
+ inside = false;
2424
+ constructor(depth) {
2425
+ this.history = new PoseHistory(depth);
2426
+ }
2427
+ free() {
2428
+ this.scratch?.free();
2429
+ this.scratch = void 0;
2430
+ }
2431
+ run(physics, tickNow, requested, fn) {
2432
+ if (this.inside) {
2433
+ throw new Error(
2434
+ "room.rewind: already inside a rewind. The second call would repose the same scratch world under the query still reading it, so it is refused rather than answered wrongly."
2435
+ );
2436
+ }
2437
+ const resolved = this.history.resolve(requested);
2438
+ if (!resolved) {
2439
+ throw new Error(
2440
+ `room.rewind: this room has recorded no ticks yet, so there is no past to answer from. A woken room starts with an empty history and fills it over its next ${this.history.depth} tick(s).`
2441
+ );
2442
+ }
2443
+ this.scratch ??= physics.engineKind === "rapier3d" ? new RapierScratch(physics, this.history.depth, tickNow) : new MatterScratch(physics, this.history.depth, tickNow);
2444
+ const view = this.scratch.view(resolved.entry, resolved.tick, requested, resolved.clamped);
2445
+ this.inside = true;
2446
+ try {
2447
+ return fn(view);
2448
+ } finally {
2449
+ this.inside = false;
2450
+ }
2451
+ }
2452
+ };
2453
+
1966
2454
  // src/core/messages.ts
1967
2455
  import { FrameType as FrameType3, decodeMsg, encodeFrame as encodeFrame2, encodeMsg } from "@irtio/protocol";
2456
+ import { decodeFields as decodeFields2 } from "@irtio/schema";
1968
2457
  function toRoomTarget(target) {
1969
2458
  switch (target.kind) {
1970
2459
  case "all":
@@ -1985,7 +2474,28 @@ function deliver(core, target, frame, exclude) {
1985
2474
  if (entry.clientId === exclude) continue;
1986
2475
  if (target.kind === "client" && entry.clientId !== target.clientId) continue;
1987
2476
  if (target.kind === "role" && entry.role !== target.role) continue;
1988
- core.send(entry.clientId, frame);
2477
+ core.send(entry.clientId, frame.slice());
2478
+ }
2479
+ }
2480
+ function messagesOf(core) {
2481
+ return core.definition.schema.messages ?? [];
2482
+ }
2483
+ function decodeTyped(core, clientId, index, payload) {
2484
+ const desc = messagesOf(core)[index];
2485
+ if (!desc) {
2486
+ core.stats.messagesDropped++;
2487
+ core.log(
2488
+ "warn",
2489
+ `typed MSG from ${clientId}: no message with index ${index} in this schema; dropped`
2490
+ );
2491
+ return void 0;
2492
+ }
2493
+ try {
2494
+ return { name: desc.name, value: decodeFields2(desc.fields, payload) };
2495
+ } catch (err) {
2496
+ core.stats.messagesDropped++;
2497
+ core.log("warn", `typed MSG ${desc.name} from ${clientId} failed to decode:`, err);
2498
+ return void 0;
1989
2499
  }
1990
2500
  }
1991
2501
  function handleMsg(core, clientId, payload) {
@@ -2001,31 +2511,54 @@ function handleMsg(core, clientId, payload) {
2001
2511
  core.log("warn", `voice MSG from ${clientId} reached room code; dropped (supervisor bug)`);
2002
2512
  return true;
2003
2513
  }
2514
+ if (msg.typed && msg.target.kind === "server") {
2515
+ core.stats.messagesDropped++;
2516
+ core.log("warn", `typed MSG from ${clientId} addressed the server; dropped`);
2517
+ return true;
2518
+ }
2004
2519
  const roomTarget = toRoomTarget(msg.target);
2005
2520
  if (roomTarget === void 0) return true;
2521
+ let typed;
2522
+ if (msg.typed) {
2523
+ typed = decodeTyped(core, clientId, msg.typed.index, msg.payload);
2524
+ if (!typed) return true;
2525
+ }
2006
2526
  const onMessage = core.definition.config.onMessage;
2007
2527
  if (onMessage) {
2008
2528
  const ran = core.tryRun(
2009
2529
  "onMessage",
2010
- () => onMessage(core.anyState, clientId, roomTarget, msg.payload, core.ctxFor(clientId))
2530
+ () => onMessage(core.anyState, clientId, roomTarget, msg.payload, core.ctxFor(clientId), typed)
2011
2531
  );
2012
2532
  if (!ran.ok || ran.value === false) return true;
2013
2533
  }
2014
2534
  if (msg.target.kind === "server") return true;
2015
2535
  const frame = encodeFrame2(
2016
2536
  FrameType3.MSG,
2017
- encodeMsg({ target: { kind: "client", clientId }, payload: msg.payload })
2537
+ encodeMsg({
2538
+ target: { kind: "client", clientId },
2539
+ payload: msg.payload,
2540
+ ...msg.typed ? { typed: msg.typed } : {}
2541
+ })
2018
2542
  );
2019
2543
  deliver(core, msg.target, frame, clientId);
2020
2544
  return true;
2021
2545
  }
2546
+ function wireTargetOf(target) {
2547
+ return target === "all" ? { kind: "all" } : typeof target === "string" ? { kind: "client", clientId: target } : { kind: "role", role: target.role };
2548
+ }
2022
2549
  function sendMessage(core, target, bytes) {
2023
- const wire = target === "all" ? { kind: "all" } : typeof target === "string" ? { kind: "client", clientId: target } : { kind: "role", role: target.role };
2024
2550
  const frame = encodeFrame2(
2025
2551
  FrameType3.MSG,
2026
2552
  encodeMsg({ target: { kind: "server" }, payload: bytes })
2027
2553
  );
2028
- deliver(core, wire, frame);
2554
+ deliver(core, wireTargetOf(target), frame);
2555
+ }
2556
+ function sendTypedMessage(core, index, target, payload) {
2557
+ const frame = encodeFrame2(
2558
+ FrameType3.MSG,
2559
+ encodeMsg({ target: { kind: "server" }, payload, typed: { index } })
2560
+ );
2561
+ deliver(core, wireTargetOf(target), frame);
2029
2562
  }
2030
2563
 
2031
2564
  // src/core/room-api.ts
@@ -2035,7 +2568,7 @@ import {
2035
2568
  busPayloadProblem,
2036
2569
  encodeFrame as encodeFrame3
2037
2570
  } from "@irtio/protocol";
2038
- import { encodeDelta as encodeDelta3, isDirtyEmpty as isDirtyEmpty2 } from "@irtio/schema";
2571
+ import { encodeDelta as encodeDelta3, encodeFields as encodeFields2, isDirtyEmpty as isDirtyEmpty2 } from "@irtio/schema";
2039
2572
  function makePhysicsApi(p) {
2040
2573
  return {
2041
2574
  get rapier() {
@@ -2083,7 +2616,7 @@ function makeKv(core) {
2083
2616
  }
2084
2617
  function makeLeaderboard(core) {
2085
2618
  return {
2086
- submit(board, playerId, score) {
2619
+ submit(board, playerId, score, options) {
2087
2620
  if (!Number.isInteger(score)) {
2088
2621
  return rejectAsEvent(
2089
2622
  core,
@@ -2092,7 +2625,17 @@ function makeLeaderboard(core) {
2092
2625
  )
2093
2626
  );
2094
2627
  }
2095
- return startHostCall(core, { kind: "lbSubmit", board, playerId, score }, () => void 0);
2628
+ return startHostCall(
2629
+ core,
2630
+ {
2631
+ kind: "lbSubmit",
2632
+ board,
2633
+ playerId,
2634
+ score,
2635
+ ...options?.bucket === void 0 ? {} : { bucket: options.bucket }
2636
+ },
2637
+ () => void 0
2638
+ );
2096
2639
  }
2097
2640
  };
2098
2641
  }
@@ -2183,6 +2726,14 @@ function createRoomApi(core, publicUrl) {
2183
2726
  const sep = publicUrl.includes("?") ? "&" : "?";
2184
2727
  return `${publicUrl}${sep}room=${encodeURIComponent(core.roomId)}`;
2185
2728
  };
2729
+ const messages = {};
2730
+ for (const desc of core.definition.schema.messages ?? []) {
2731
+ messages[desc.name] = {
2732
+ send(target, value) {
2733
+ sendTypedMessage(core, desc.index, target, encodeFields2(desc.fields, value));
2734
+ }
2735
+ };
2736
+ }
2186
2737
  const room = {
2187
2738
  get id() {
2188
2739
  return core.roomId;
@@ -2245,9 +2796,17 @@ function createRoomApi(core, publicUrl) {
2245
2796
  }
2246
2797
  return matterApi ?? (matterApi = makeMatterApi(p));
2247
2798
  },
2799
+ // ---- M6 lane F: rewind ----
2800
+ // D72: one line, and deliberately only one. Everything the rewind does — the buffer, the
2801
+ // clamping, the scratch worlds, the reentrancy refusal — lives in `core/history.ts` behind
2802
+ // `RoomCore.rewind`, so this file stays what it says it is: a facade that reads through.
2803
+ rewind(tick, fn) {
2804
+ return core.rewind(tick, fn);
2805
+ },
2248
2806
  send(target, bytes) {
2249
2807
  sendMessage(core, target, bytes);
2250
2808
  },
2809
+ messages,
2251
2810
  setRole(clientId, role) {
2252
2811
  setRole(core, clientId, role);
2253
2812
  cached = void 0;
@@ -2493,7 +3052,8 @@ var RoomCore = class _RoomCore {
2493
3052
  visibleIdsTotal: 0,
2494
3053
  membershipEnters: 0,
2495
3054
  membershipLeaves: 0,
2496
- corrections: 0
3055
+ corrections: 0,
3056
+ messagesDropped: 0
2497
3057
  };
2498
3058
  tick = 0;
2499
3059
  stopped = false;
@@ -2515,6 +3075,13 @@ var RoomCore = class _RoomCore {
2515
3075
  * cost the profiler has is behind this `undefined`.
2516
3076
  */
2517
3077
  ledger;
3078
+ /**
3079
+ * D72: the pose history and the rewind scratch, present only when the room's physics config
3080
+ * declares `history`. Every cost this lane has is behind this `undefined`, and it is
3081
+ * deliberately not in the hibernation blob: a woken room starts with an empty buffer and fills
3082
+ * it again over its next `history` ticks.
3083
+ */
3084
+ rewindState;
2518
3085
  seed;
2519
3086
  api;
2520
3087
  internals;
@@ -2558,6 +3125,8 @@ var RoomCore = class _RoomCore {
2558
3125
  this.loop = new Loop(self);
2559
3126
  this.api = createRoomApi(self, options.publicUrl ?? DEFAULT_PUBLIC_URL);
2560
3127
  this.physics = this.buildPhysics(restored);
3128
+ const historyDepth = this.physics ? historyDepthOf(definition.config.physics) : void 0;
3129
+ this.rewindState = historyDepth === void 0 ? void 0 : new RewindState(historyDepth);
2561
3130
  this.subscribeDeclaredChannels();
2562
3131
  if (restored) {
2563
3132
  const onWake = definition.config.onWake;
@@ -2763,6 +3332,35 @@ var RoomCore = class _RoomCore {
2763
3332
  captureTimeline() {
2764
3333
  this.recorder?.capture(this.tick, this.ext, this.plain);
2765
3334
  }
3335
+ // ---- M6 lane F: rewind ----
3336
+ /**
3337
+ * D72: record this tick's body poses, right after `physics.sync()` — the poses the clients are
3338
+ * about to be told about, under the tick number they will be told it under, which is the tick a
3339
+ * client later stamps its `CALL` with.
3340
+ */
3341
+ captureHistory() {
3342
+ const physics = this.physics;
3343
+ if (!physics || !this.rewindState) return;
3344
+ this.rewindState.history.capture(this.tick, physics);
3345
+ }
3346
+ /**
3347
+ * D72: `room.rewind(tick, fn)`. The live world is not touched and nothing is re-simulated; `fn`
3348
+ * queries a scratch world holding every tracked body at its pose at `tick`.
3349
+ */
3350
+ rewind(tick, fn) {
3351
+ const physics = this.physics;
3352
+ if (!physics) {
3353
+ throw new Error(
3354
+ "room.rewind: this room has no physics, so there are no poses to rewind. Add physics: { engine, gravity, bodies, history: <ticks> } to defineRoom(...)"
3355
+ );
3356
+ }
3357
+ if (!this.rewindState) {
3358
+ throw new Error(
3359
+ "room.rewind: this room declares no physics.history, so no poses are kept. Add history: <ticks> to the physics config; it is off by default because it costs heap per tick. See irt.io/docs/concepts/lag-compensation for how deep to make it."
3360
+ );
3361
+ }
3362
+ return this.rewindState.run(physics, () => this.tick, tick, fn);
3363
+ }
2766
3364
  /** Live JSON view of the room for the dev page / supervisor admin API. */
2767
3365
  inspect() {
2768
3366
  return {
@@ -2797,6 +3395,7 @@ var RoomCore = class _RoomCore {
2797
3395
  this.loop.stop();
2798
3396
  rejectAllPending(this.internals, "room stopped");
2799
3397
  rejectAllHostCalls(this.internals, "room stopped");
3398
+ this.rewindState?.free();
2800
3399
  this.physics?.free();
2801
3400
  }
2802
3401
  /**
@@ -2986,6 +3585,9 @@ var RoomCore = class _RoomCore {
2986
3585
  role: entry?.role ?? "",
2987
3586
  name: entry?.name ?? "",
2988
3587
  tick: this.tick,
3588
+ // D72: only an RPC has a stamp, and `rpc.ts` puts it on the ctx it hands the handler. Every
3589
+ // other entry point (join, leave, write, ownership) has no client tick by construction.
3590
+ clientTick: void 0,
2989
3591
  reconnecting,
2990
3592
  room: this.room
2991
3593
  };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  parseHibernationBlob,
3
3
  writeHibernationBlob
4
- } from "./chunk-K42HA75G.js";
4
+ } from "./chunk-EXPFVRD4.js";
5
5
 
6
6
  // src/migrate.ts
7
7
  import { PRESENCE_COLLECTION, withBuiltins } from "@irtio/protocol";
@@ -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
@@ -340,6 +344,13 @@ interface RoomStats {
340
344
  membershipEnters: number;
341
345
  membershipLeaves: number;
342
346
  corrections: number;
347
+ /**
348
+ * D70: typed peer messages this room refused to hand to `onMessage` — an index its schema does
349
+ * not have, or a payload the codec would not decode. Every one of them is a peer sending
350
+ * something this room's schema does not describe, so a number climbing here means a client on a
351
+ * stale build or one probing, and never a room bug.
352
+ */
353
+ messagesDropped: number;
343
354
  }
344
355
  /** The surface the worker host and the harness drive. Implemented by `RoomCore`. */
345
356
  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-DwxqjeWP.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-CoQDczh2.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-VMCE3LRO.js";
5
+ } from "./chunk-ZNNTF7Y3.js";
6
6
  import {
7
7
  CRASH_AFTER_THROWS,
8
8
  DEFAULT_TIMELINE_MAX_RECORDS,
@@ -42,7 +42,7 @@ import {
42
42
  visibleNames,
43
43
  visibleTo,
44
44
  writeHibernationBlob
45
- } from "./chunk-K42HA75G.js";
45
+ } from "./chunk-EXPFVRD4.js";
46
46
  export {
47
47
  CRASH_AFTER_THROWS,
48
48
  DEFAULT_TIMELINE_MAX_RECORDS,
@@ -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, 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-DwxqjeWP.js';
5
5
 
6
6
  /**
7
7
  * `room.random()` — mulberry32 over a u32 seed. Deterministic, tiny, and serializable: the
@@ -102,6 +102,14 @@ interface PhysicsApi {
102
102
  /** Body → schema, through the tracked proxies. */
103
103
  sync(): void;
104
104
  bodyFor(collection: string, id: string): unknown;
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;
105
113
  /** rapier3d only. */
106
114
  readonly rapier: unknown;
107
115
  /** rapier3d only. */
@@ -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;
@@ -458,6 +478,13 @@ declare class PhysicsRuntime {
458
478
  /** The body behind an instance, created on demand so a handler's own `add` is usable at once. */
459
479
  bodyFor(collection: string, id: string): RapierRigidBody | undefined;
460
480
  private create;
481
+ /**
482
+ * D72: every tracked body, in the order `sync()` walks them (collection order, then instance
483
+ * order). Read-only: it hands out the live bodies and nothing else, and the map stays private.
484
+ * The pose history calls this once per tick and the rewind scratch calls it per rewind; a room
485
+ * that declares no `physics.history` never calls it at all.
486
+ */
487
+ eachTrackedBody(fn: (collection: string, id: string, body: RapierRigidBody) => void): void;
461
488
  private applyRecordToBody;
462
489
  /**
463
490
  * Creates bodies for new instances and destroys bodies whose instance is gone. Runs once per
@@ -516,6 +543,13 @@ declare class RoomCore<S extends AnySchema = AnySchema> implements RoomCoreApi<S
516
543
  * cost the profiler has is behind this `undefined`.
517
544
  */
518
545
  private readonly ledger;
546
+ /**
547
+ * D72: the pose history and the rewind scratch, present only when the room's physics config
548
+ * declares `history`. Every cost this lane has is behind this `undefined`, and it is
549
+ * deliberately not in the hibernation blob: a woken room starts with an empty buffer and fills
550
+ * it again over its next `history` ticks.
551
+ */
552
+ private readonly rewindState;
519
553
  private readonly seed;
520
554
  private readonly api;
521
555
  private readonly internals;
@@ -581,6 +615,17 @@ declare class RoomCore<S extends AnySchema = AnySchema> implements RoomCoreApi<S
581
615
  recording(): TimelineDump | undefined;
582
616
  /** D41: called at the end of every tick (and every event-mode flush). No-op when unarmed. */
583
617
  captureTimeline(): void;
618
+ /**
619
+ * D72: record this tick's body poses, right after `physics.sync()` — the poses the clients are
620
+ * about to be told about, under the tick number they will be told it under, which is the tick a
621
+ * client later stamps its `CALL` with.
622
+ */
623
+ captureHistory(): void;
624
+ /**
625
+ * D72: `room.rewind(tick, fn)`. The live world is not touched and nothing is re-simulated; `fn`
626
+ * queries a scratch world holding every tracked body at its pose at `tick`.
627
+ */
628
+ rewind<T>(tick: number, fn: (past: RewindView) => T): T;
584
629
  /** Live JSON view of the room for the dev page / supervisor admin API. */
585
630
  inspect(): RoomInspection;
586
631
  guard<T>(name: string, fn: () => T): T | undefined;
@@ -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-CoQDczh2.js';
2
+ export { k as initMatter, l as initPhysics } from '../room-CoQDczh2.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-DwxqjeWP.js';
6
6
  import { AnySchema, PlainState, EntityCollection, State } from '@irtio/schema';
7
7
 
8
8
  /**
@@ -4,7 +4,7 @@ import {
4
4
  initMatter,
5
5
  initPhysics,
6
6
  visibleNames
7
- } from "../chunk-K42HA75G.js";
7
+ } from "../chunk-EXPFVRD4.js";
8
8
 
9
9
  // src/test/clock.ts
10
10
  var FakeClock = class {
@@ -197,7 +197,7 @@ var HarnessHost = class {
197
197
  return { ok: true };
198
198
  }
199
199
  case "lbSubmit": {
200
- const key = `${call.board} ${call.playerId}`;
200
+ const key = call.bucket === void 0 ? `${call.board} ${call.playerId}` : `${call.board} ${call.bucket} ${call.playerId}`;
201
201
  const existing = this.scores.get(key);
202
202
  if (existing === void 0 || call.score > existing) this.scores.set(key, call.score);
203
203
  return { ok: true };
@@ -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-DwxqjeWP.js';
2
2
  import { ErrorCodeName, ProfileSnapshot } from '@irtio/protocol';
3
3
  import { NpcConfig, LeaveReason } from '@irtio/server';
4
4
  import '@irtio/schema';
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  migrateSnapshot
3
- } from "../chunk-VMCE3LRO.js";
3
+ } from "../chunk-ZNNTF7Y3.js";
4
4
  import {
5
5
  RoomCore,
6
6
  RoomFullError,
@@ -8,7 +8,7 @@ import {
8
8
  initPhysics,
9
9
  onFirstRapierStep,
10
10
  rapierHasStepped
11
- } from "../chunk-K42HA75G.js";
11
+ } from "../chunk-EXPFVRD4.js";
12
12
 
13
13
  // src/worker/index.ts
14
14
  import { getHeapStatistics } from "v8";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@irtio/runtime",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "irtio room runtime: RoomCore, worker_threads host, in-process test harness",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -30,9 +30,9 @@
30
30
  "dependencies": {
31
31
  "@dimforge/rapier3d-compat": "0.20.0",
32
32
  "matter-js": "0.20.0",
33
- "@irtio/protocol": "0.6.0",
34
- "@irtio/schema": "0.6.0",
35
- "@irtio/server": "0.6.0"
33
+ "@irtio/protocol": "0.7.0",
34
+ "@irtio/schema": "0.7.0",
35
+ "@irtio/server": "0.7.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/matter-js": "0.20.2"