@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.
- package/dist/{chunk-K42HA75G.js → chunk-EXPFVRD4.js} +618 -16
- package/dist/{chunk-VMCE3LRO.js → chunk-ZNNTF7Y3.js} +1 -1
- package/dist/{contract-BjMsoJIV.d.ts → contract-DwxqjeWP.d.ts} +11 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/{room-CfnEjlcg.d.ts → room-CoQDczh2.d.ts} +47 -2
- package/dist/test/index.d.ts +3 -3
- package/dist/test/index.js +2 -2
- package/dist/worker/index.d.ts +1 -1
- package/dist/worker/index.js +2 -2
- package/package.json +4 -4
|
@@ -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({
|
|
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,
|
|
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(
|
|
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
|
};
|
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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;
|
package/dist/test/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { R as RoomCore } from '../room-
|
|
2
|
-
export { k as initMatter, l as initPhysics } from '../room-
|
|
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-
|
|
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
|
/**
|
package/dist/test/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
initMatter,
|
|
5
5
|
initPhysics,
|
|
6
6
|
visibleNames
|
|
7
|
-
} from "../chunk-
|
|
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 };
|
package/dist/worker/index.d.ts
CHANGED
|
@@ -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-
|
|
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';
|
package/dist/worker/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
migrateSnapshot
|
|
3
|
-
} from "../chunk-
|
|
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-
|
|
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.
|
|
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.
|
|
34
|
-
"@irtio/schema": "0.
|
|
35
|
-
"@irtio/server": "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"
|