@irtio/runtime 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-ZCD2TBXB.js → chunk-KGZEQTJR.js} +332 -32
- package/dist/{chunk-DBGMG2S3.js → chunk-MTBMGUGG.js} +10 -2
- package/dist/{contract-CRilLVdx.d.ts → contract-C5aqs49-.d.ts} +5 -1
- package/dist/index.d.ts +17 -4
- package/dist/index.js +2 -2
- package/dist/{room-YJGz6Caw.d.ts → room-Db3iPBKM.d.ts} +63 -10
- package/dist/test/index.d.ts +4 -4
- package/dist/test/index.js +3 -3
- package/dist/worker/index.d.ts +1 -1
- package/dist/worker/index.js +2 -2
- package/package.json +4 -4
|
@@ -250,7 +250,7 @@ function createCallProxy(core, clientId) {
|
|
|
250
250
|
}
|
|
251
251
|
);
|
|
252
252
|
}
|
|
253
|
-
function createBroadcastProxy(core) {
|
|
253
|
+
function createBroadcastProxy(core, except) {
|
|
254
254
|
return new Proxy(
|
|
255
255
|
{},
|
|
256
256
|
{
|
|
@@ -259,13 +259,19 @@ function createBroadcastProxy(core) {
|
|
|
259
259
|
return (params) => {
|
|
260
260
|
const desc = descFor(core, prop);
|
|
261
261
|
for (const entry of core.clients.values()) {
|
|
262
|
-
if (entry.connected)
|
|
262
|
+
if (!entry.connected) continue;
|
|
263
|
+
if (except?.has(entry.clientId)) continue;
|
|
264
|
+
sendCall(core, entry.clientId, desc, params ?? {});
|
|
263
265
|
}
|
|
264
266
|
};
|
|
265
267
|
}
|
|
266
268
|
}
|
|
267
269
|
);
|
|
268
270
|
}
|
|
271
|
+
function createBroadcastExceptProxy(core, except) {
|
|
272
|
+
const ids = typeof except === "string" ? [except] : except;
|
|
273
|
+
return createBroadcastProxy(core, new Set(ids));
|
|
274
|
+
}
|
|
269
275
|
function handleReply(core, clientId, payload) {
|
|
270
276
|
let reply;
|
|
271
277
|
try {
|
|
@@ -956,6 +962,7 @@ function decodeMatterBodies(bytes) {
|
|
|
956
962
|
function bodyKey(collection, id) {
|
|
957
963
|
return `${collection}\0${id}`;
|
|
958
964
|
}
|
|
965
|
+
var PLANAR_CHANNELS = /* @__PURE__ */ new Set(["x", "y", "qz", "qw", "vx", "vy", "wz"]);
|
|
959
966
|
var MatterRuntime = class {
|
|
960
967
|
engineKind = "matter2d";
|
|
961
968
|
/** rapier3d only; present so both runtimes satisfy one internal shape. */
|
|
@@ -981,6 +988,15 @@ var MatterRuntime = class {
|
|
|
981
988
|
restore;
|
|
982
989
|
stepMs;
|
|
983
990
|
sleepSynced = /* @__PURE__ */ new Set();
|
|
991
|
+
/**
|
|
992
|
+
* The plain record object each body was built from. See `PhysicsRuntime.sourceRecords`: `add()`
|
|
993
|
+
* installs a new record object, so an id removed and re-added between two reconciles (a pooled
|
|
994
|
+
* projectile) is identifiable by identity, and is the one case where a live body has to be
|
|
995
|
+
* rebuilt so the new row's spawn pose and velocity are honoured.
|
|
996
|
+
*/
|
|
997
|
+
sourceRecords = /* @__PURE__ */ new Map();
|
|
998
|
+
/** `collection.field` pairs already warned about a channel a plane cannot hold. */
|
|
999
|
+
warnedChannels = /* @__PURE__ */ new Set();
|
|
984
1000
|
constructor(core, matter, options) {
|
|
985
1001
|
const config = core.definition.config.physics;
|
|
986
1002
|
if (!config) throw new Error("MatterRuntime: the room config declares no physics");
|
|
@@ -1050,6 +1066,7 @@ var MatterRuntime = class {
|
|
|
1050
1066
|
} else {
|
|
1051
1067
|
this.applyRecordToBody(desc, spec.body, record);
|
|
1052
1068
|
}
|
|
1069
|
+
this.sourceRecords.set(key, record);
|
|
1053
1070
|
return spec.body;
|
|
1054
1071
|
}
|
|
1055
1072
|
// ---- M6 lane F: rewind ----
|
|
@@ -1062,7 +1079,8 @@ var MatterRuntime = class {
|
|
|
1062
1079
|
const plainColl = plainEntity(this.core.plain, desc.name);
|
|
1063
1080
|
for (const id of plainColl.ids()) {
|
|
1064
1081
|
const attached = this.bodies.get(bodyKey(desc.name, id));
|
|
1065
|
-
if (attached)
|
|
1082
|
+
if (attached)
|
|
1083
|
+
fn(desc.name, id, attached.body, plainColl.get(id));
|
|
1066
1084
|
}
|
|
1067
1085
|
}
|
|
1068
1086
|
}
|
|
@@ -1089,6 +1107,17 @@ var MatterRuntime = class {
|
|
|
1089
1107
|
for (const [channel, field] of physics.channels) {
|
|
1090
1108
|
const raw = record[field];
|
|
1091
1109
|
if (typeof raw !== "number") continue;
|
|
1110
|
+
if (raw !== 0 && !PLANAR_CHANNELS.has(channel)) {
|
|
1111
|
+
const key = `${desc.name}.${field}`;
|
|
1112
|
+
if (!this.warnedChannels.has(key)) {
|
|
1113
|
+
this.warnedChannels.add(key);
|
|
1114
|
+
this.core.log(
|
|
1115
|
+
"warn",
|
|
1116
|
+
`irtio: ${desc.name}.${field} maps body channel ${channel}, which a matter2d room cannot hold (the world is a plane); the ${raw} it was added with was dropped`
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1092
1121
|
applyChannel2d(channel, raw, t);
|
|
1093
1122
|
}
|
|
1094
1123
|
this.applyState(body, {
|
|
@@ -1110,8 +1139,21 @@ var MatterRuntime = class {
|
|
|
1110
1139
|
for (const id of coll.ids()) {
|
|
1111
1140
|
const key = bodyKey(desc.name, id);
|
|
1112
1141
|
live.add(key);
|
|
1113
|
-
if (this.bodies.has(key)) continue;
|
|
1114
1142
|
const record = coll.get(id);
|
|
1143
|
+
const existing = this.bodies.get(key);
|
|
1144
|
+
if (existing) {
|
|
1145
|
+
if (record === void 0) continue;
|
|
1146
|
+
const source = this.sourceRecords.get(key);
|
|
1147
|
+
if (source === void 0) {
|
|
1148
|
+
this.sourceRecords.set(key, record);
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
if (source === record) continue;
|
|
1152
|
+
this.bodies.delete(key);
|
|
1153
|
+
this.sleepSynced.delete(key);
|
|
1154
|
+
this.sourceRecords.delete(key);
|
|
1155
|
+
this.matter.Composite.remove(this.engine.world, [existing.body, ...existing.constraints]);
|
|
1156
|
+
}
|
|
1115
1157
|
if (record !== void 0) this.create(desc, id, record);
|
|
1116
1158
|
}
|
|
1117
1159
|
}
|
|
@@ -1119,6 +1161,7 @@ var MatterRuntime = class {
|
|
|
1119
1161
|
if (live.has(key)) continue;
|
|
1120
1162
|
this.bodies.delete(key);
|
|
1121
1163
|
this.sleepSynced.delete(key);
|
|
1164
|
+
this.sourceRecords.delete(key);
|
|
1122
1165
|
this.matter.Composite.remove(this.engine.world, [attached.body, ...attached.constraints]);
|
|
1123
1166
|
}
|
|
1124
1167
|
}
|
|
@@ -1349,6 +1392,19 @@ var PhysicsRuntime = class {
|
|
|
1349
1392
|
* world rebuilt from schema state would wake it with a kick). Found by the drift check.
|
|
1350
1393
|
*/
|
|
1351
1394
|
sleepSynced = /* @__PURE__ */ new Set();
|
|
1395
|
+
/**
|
|
1396
|
+
* The plain record object each body was built from, by body key. `add()` always installs a
|
|
1397
|
+
* **new** record object (`normalizeRecord` builds one), so an id that is removed and re-added
|
|
1398
|
+
* between two reconciles — the pooled-projectile shape — is identifiable by identity alone,
|
|
1399
|
+
* which is the only way to see it from here: both edits happened before this module looked.
|
|
1400
|
+
* Without it the old body kept its pose and velocity and the new row's spawn values were lost.
|
|
1401
|
+
*
|
|
1402
|
+
* Unknown means adopt, never rebuild: a world restored from a hibernation blob has bodies with
|
|
1403
|
+
* no recorded source, and rebuilding those would throw away exactly what the blob preserved.
|
|
1404
|
+
*/
|
|
1405
|
+
sourceRecords = /* @__PURE__ */ new Map();
|
|
1406
|
+
/** `collection.field` pairs already warned about a channel value the engine would not take. */
|
|
1407
|
+
warnedChannels = /* @__PURE__ */ new Set();
|
|
1352
1408
|
constructor(core, rapier, options) {
|
|
1353
1409
|
const config = core.definition.config.physics;
|
|
1354
1410
|
if (!config) throw new Error("PhysicsRuntime: the room config declares no physics");
|
|
@@ -1423,10 +1479,50 @@ var PhysicsRuntime = class {
|
|
|
1423
1479
|
}
|
|
1424
1480
|
const body = this.world.createRigidBody(spec.body);
|
|
1425
1481
|
for (const collider of spec.colliders ?? []) this.world.createCollider(collider, body);
|
|
1482
|
+
this.warnLockedChannels(desc, spec, record);
|
|
1426
1483
|
this.applyRecordToBody(desc, body, record);
|
|
1427
1484
|
this.bodies.set(bodyKey2(desc.name, id), body);
|
|
1485
|
+
this.sourceRecords.set(bodyKey2(desc.name, id), record);
|
|
1428
1486
|
return body;
|
|
1429
1487
|
}
|
|
1488
|
+
/**
|
|
1489
|
+
* A spawn value the engine will not take, said once per field.
|
|
1490
|
+
*
|
|
1491
|
+
* Rapier accepts `setLinvel` on an axis the body has disabled and then keeps zero, and it
|
|
1492
|
+
* accepts one on a fixed body and then ignores it — both read back as "the field was applied"
|
|
1493
|
+
* and behave as if it never was. The desc says which, before the body exists, so the check is
|
|
1494
|
+
* on the desc: the usual case is the 2D-in-3D recipe (`enabledTranslations(true, true, false)`)
|
|
1495
|
+
* meeting a row that spawns with a `vz`. Named by field, because the field is what the author
|
|
1496
|
+
* wrote and what they will grep for.
|
|
1497
|
+
*/
|
|
1498
|
+
warnLockedChannels(desc, spec, record) {
|
|
1499
|
+
const physics = desc.physics;
|
|
1500
|
+
if (!physics) return;
|
|
1501
|
+
const b = spec.body;
|
|
1502
|
+
const frozen = b.status !== 0;
|
|
1503
|
+
const enabled = {
|
|
1504
|
+
vx: b.translationsEnabledX,
|
|
1505
|
+
vy: b.translationsEnabledY,
|
|
1506
|
+
vz: b.translationsEnabledZ,
|
|
1507
|
+
wx: b.rotationsEnabledX,
|
|
1508
|
+
wy: b.rotationsEnabledY,
|
|
1509
|
+
wz: b.rotationsEnabledZ
|
|
1510
|
+
};
|
|
1511
|
+
for (const [channel, field] of physics.channels) {
|
|
1512
|
+
const allowed = enabled[channel];
|
|
1513
|
+
if (allowed === void 0) continue;
|
|
1514
|
+
const raw = record[field];
|
|
1515
|
+
if (typeof raw !== "number" || raw === 0) continue;
|
|
1516
|
+
if (allowed && !frozen) continue;
|
|
1517
|
+
const key = `${desc.name}.${field}`;
|
|
1518
|
+
if (this.warnedChannels.has(key)) continue;
|
|
1519
|
+
this.warnedChannels.add(key);
|
|
1520
|
+
this.core.log(
|
|
1521
|
+
"warn",
|
|
1522
|
+
`irtio: ${desc.name}.${field} (body channel ${channel}) was added as ${raw}, but ` + (frozen ? `physics.bodies.${desc.name} builds a non-dynamic body, which never moves under it` : `that degree of freedom is disabled by physics.bodies.${desc.name}`) + " \u2014 the value was dropped"
|
|
1523
|
+
);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1430
1526
|
// ---- M6 lane F: rewind ----
|
|
1431
1527
|
/**
|
|
1432
1528
|
* D72: every tracked body, in the order `sync()` walks them (collection order, then instance
|
|
@@ -1439,7 +1535,7 @@ var PhysicsRuntime = class {
|
|
|
1439
1535
|
const plainColl = plainEntity(this.core.plain, desc.name);
|
|
1440
1536
|
for (const id of plainColl.ids()) {
|
|
1441
1537
|
const body = this.bodies.get(bodyKey2(desc.name, id));
|
|
1442
|
-
if (body) fn(desc.name, id, body);
|
|
1538
|
+
if (body) fn(desc.name, id, body, plainColl.get(id));
|
|
1443
1539
|
}
|
|
1444
1540
|
}
|
|
1445
1541
|
}
|
|
@@ -1522,8 +1618,21 @@ var PhysicsRuntime = class {
|
|
|
1522
1618
|
for (const id of coll.ids()) {
|
|
1523
1619
|
const key = bodyKey2(desc.name, id);
|
|
1524
1620
|
live.add(key);
|
|
1525
|
-
if (this.bodies.has(key)) continue;
|
|
1526
1621
|
const record = coll.get(id);
|
|
1622
|
+
const existing = this.bodies.get(key);
|
|
1623
|
+
if (existing) {
|
|
1624
|
+
if (record === void 0) continue;
|
|
1625
|
+
const source = this.sourceRecords.get(key);
|
|
1626
|
+
if (source === void 0) {
|
|
1627
|
+
this.sourceRecords.set(key, record);
|
|
1628
|
+
continue;
|
|
1629
|
+
}
|
|
1630
|
+
if (source === record) continue;
|
|
1631
|
+
this.bodies.delete(key);
|
|
1632
|
+
this.sleepSynced.delete(key);
|
|
1633
|
+
this.sourceRecords.delete(key);
|
|
1634
|
+
this.world.removeRigidBody(existing);
|
|
1635
|
+
}
|
|
1527
1636
|
if (record !== void 0) this.create(desc, id, record);
|
|
1528
1637
|
}
|
|
1529
1638
|
}
|
|
@@ -1531,6 +1640,7 @@ var PhysicsRuntime = class {
|
|
|
1531
1640
|
if (live.has(key)) continue;
|
|
1532
1641
|
this.bodies.delete(key);
|
|
1533
1642
|
this.sleepSynced.delete(key);
|
|
1643
|
+
this.sourceRecords.delete(key);
|
|
1534
1644
|
this.world.removeRigidBody(body);
|
|
1535
1645
|
}
|
|
1536
1646
|
}
|
|
@@ -1630,6 +1740,14 @@ function channelValue(channel, t, r, v, w) {
|
|
|
1630
1740
|
}
|
|
1631
1741
|
|
|
1632
1742
|
// src/core/random.ts
|
|
1743
|
+
function seedFromRoomId(roomId) {
|
|
1744
|
+
let h = 2166136261;
|
|
1745
|
+
for (let i = 0; i < roomId.length; i++) {
|
|
1746
|
+
h ^= roomId.charCodeAt(i);
|
|
1747
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
1748
|
+
}
|
|
1749
|
+
return h === 0 ? 2654435769 : h >>> 0;
|
|
1750
|
+
}
|
|
1633
1751
|
var Mulberry32 = class {
|
|
1634
1752
|
/** Current internal state (u32). Survives hibernation. */
|
|
1635
1753
|
state;
|
|
@@ -1691,6 +1809,16 @@ var Rapier2dRuntime = class {
|
|
|
1691
1809
|
bodies = /* @__PURE__ */ new Map();
|
|
1692
1810
|
/** See `PhysicsRuntime.sleepSynced`: the one extra sync a body owes on the tick it sleeps. */
|
|
1693
1811
|
sleepSynced = /* @__PURE__ */ new Set();
|
|
1812
|
+
/**
|
|
1813
|
+
* See `PhysicsRuntime.sourceRecords`: the plain record object each body was built from. `add()`
|
|
1814
|
+
* always installs a new record object, so an id removed and re-added between two reconciles —
|
|
1815
|
+
* the pooled-projectile shape — is identifiable by identity alone, and its body is rebuilt so
|
|
1816
|
+
* the new row's spawn pose and velocity are what the world gets. Unknown means adopt, never
|
|
1817
|
+
* rebuild: a world restored from a hibernation blob has bodies with no recorded source.
|
|
1818
|
+
*/
|
|
1819
|
+
sourceRecords = /* @__PURE__ */ new Map();
|
|
1820
|
+
/** `collection.field` pairs already warned about a channel value the engine would not take. */
|
|
1821
|
+
warnedChannels = /* @__PURE__ */ new Set();
|
|
1694
1822
|
constructor(core, rapier, options) {
|
|
1695
1823
|
const config = core.definition.config.physics;
|
|
1696
1824
|
if (!config) throw new Error("Rapier2dRuntime: the room config declares no physics");
|
|
@@ -1760,10 +1888,48 @@ var Rapier2dRuntime = class {
|
|
|
1760
1888
|
}
|
|
1761
1889
|
const body = this.world.createRigidBody(spec.body);
|
|
1762
1890
|
for (const collider of spec.colliders ?? []) this.world.createCollider(collider, body);
|
|
1891
|
+
this.warnLockedChannels(desc, spec, record);
|
|
1763
1892
|
this.applyRecordToBody(desc, body, record);
|
|
1764
1893
|
this.bodies.set(bodyKey3(desc.name, id), body);
|
|
1894
|
+
this.sourceRecords.set(bodyKey3(desc.name, id), record);
|
|
1765
1895
|
return body;
|
|
1766
1896
|
}
|
|
1897
|
+
/**
|
|
1898
|
+
* A spawn value the engine will not take, said once per field — the planar half of
|
|
1899
|
+
* `PhysicsRuntime.warnLockedChannels`.
|
|
1900
|
+
*
|
|
1901
|
+
* Rapier accepts `setLinvel` on an axis the body has disabled and then keeps zero, and it
|
|
1902
|
+
* accepts one on a fixed body and then ignores it: both read back as "the field was applied"
|
|
1903
|
+
* and behave as if it never was. The desc says which, before the body exists, so the check is on
|
|
1904
|
+
* the desc. In the plane there are three movable channels — `vx`, `vy` and the one rotation
|
|
1905
|
+
* `wz` — and the desc carries exactly those three flags. Named by field, because the field is
|
|
1906
|
+
* what the author wrote and what they will grep for.
|
|
1907
|
+
*/
|
|
1908
|
+
warnLockedChannels(desc, spec, record) {
|
|
1909
|
+
const physics = desc.physics;
|
|
1910
|
+
if (!physics) return;
|
|
1911
|
+
const b = spec.body;
|
|
1912
|
+
const frozen = b.status !== 0;
|
|
1913
|
+
const enabled = {
|
|
1914
|
+
vx: b.translationsEnabledX,
|
|
1915
|
+
vy: b.translationsEnabledY,
|
|
1916
|
+
wz: b.rotationsEnabled
|
|
1917
|
+
};
|
|
1918
|
+
for (const [channel, field] of physics.channels) {
|
|
1919
|
+
const allowed = enabled[channel];
|
|
1920
|
+
if (allowed === void 0) continue;
|
|
1921
|
+
const raw = record[field];
|
|
1922
|
+
if (typeof raw !== "number" || raw === 0) continue;
|
|
1923
|
+
if (allowed && !frozen) continue;
|
|
1924
|
+
const key = `${desc.name}.${field}`;
|
|
1925
|
+
if (this.warnedChannels.has(key)) continue;
|
|
1926
|
+
this.warnedChannels.add(key);
|
|
1927
|
+
this.core.log(
|
|
1928
|
+
"warn",
|
|
1929
|
+
`irtio: ${desc.name}.${field} (body channel ${channel}) was added as ${raw}, but ` + (frozen ? `physics.bodies.${desc.name} builds a non-dynamic body, which never moves under it` : `that degree of freedom is disabled by physics.bodies.${desc.name}`) + " \u2014 the value was dropped"
|
|
1930
|
+
);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1767
1933
|
// ---- M6 lane F: rewind ----
|
|
1768
1934
|
/**
|
|
1769
1935
|
* D72: every tracked body, in the order `sync()` walks them (collection order, then instance
|
|
@@ -1774,7 +1940,7 @@ var Rapier2dRuntime = class {
|
|
|
1774
1940
|
const plainColl = plainEntity(this.core.plain, desc.name);
|
|
1775
1941
|
for (const id of plainColl.ids()) {
|
|
1776
1942
|
const body = this.bodies.get(bodyKey3(desc.name, id));
|
|
1777
|
-
if (body) fn(desc.name, id, body);
|
|
1943
|
+
if (body) fn(desc.name, id, body, plainColl.get(id));
|
|
1778
1944
|
}
|
|
1779
1945
|
}
|
|
1780
1946
|
}
|
|
@@ -1814,8 +1980,21 @@ var Rapier2dRuntime = class {
|
|
|
1814
1980
|
for (const id of coll.ids()) {
|
|
1815
1981
|
const key = bodyKey3(desc.name, id);
|
|
1816
1982
|
live.add(key);
|
|
1817
|
-
if (this.bodies.has(key)) continue;
|
|
1818
1983
|
const record = coll.get(id);
|
|
1984
|
+
const existing = this.bodies.get(key);
|
|
1985
|
+
if (existing) {
|
|
1986
|
+
if (record === void 0) continue;
|
|
1987
|
+
const source = this.sourceRecords.get(key);
|
|
1988
|
+
if (source === void 0) {
|
|
1989
|
+
this.sourceRecords.set(key, record);
|
|
1990
|
+
continue;
|
|
1991
|
+
}
|
|
1992
|
+
if (source === record) continue;
|
|
1993
|
+
this.bodies.delete(key);
|
|
1994
|
+
this.sleepSynced.delete(key);
|
|
1995
|
+
this.sourceRecords.delete(key);
|
|
1996
|
+
this.world.removeRigidBody(existing);
|
|
1997
|
+
}
|
|
1819
1998
|
if (record !== void 0) this.create(desc, id, record);
|
|
1820
1999
|
}
|
|
1821
2000
|
}
|
|
@@ -1823,6 +2002,7 @@ var Rapier2dRuntime = class {
|
|
|
1823
2002
|
if (live.has(key)) continue;
|
|
1824
2003
|
this.bodies.delete(key);
|
|
1825
2004
|
this.sleepSynced.delete(key);
|
|
2005
|
+
this.sourceRecords.delete(key);
|
|
1826
2006
|
this.world.removeRigidBody(body);
|
|
1827
2007
|
}
|
|
1828
2008
|
}
|
|
@@ -2144,8 +2324,8 @@ function catchUpDirty(ext, plain, fromRole, toRole) {
|
|
|
2144
2324
|
// src/core/snapshot.ts
|
|
2145
2325
|
import { withBuiltins } from "@irtio/protocol";
|
|
2146
2326
|
import { ByteReader as ByteReader3, ByteWriter as ByteWriter3, decodeSnapshot } from "@irtio/schema";
|
|
2147
|
-
var SNAPSHOT_FORMAT_VERSION =
|
|
2148
|
-
var READABLE_SNAPSHOT_VERSIONS = [1, 2];
|
|
2327
|
+
var SNAPSHOT_FORMAT_VERSION = 3;
|
|
2328
|
+
var READABLE_SNAPSHOT_VERSIONS = [1, 2, 3];
|
|
2149
2329
|
function parseHibernationBlob(bytes) {
|
|
2150
2330
|
const r = new ByteReader3(bytes);
|
|
2151
2331
|
const version = r.u8();
|
|
@@ -2156,22 +2336,31 @@ function parseHibernationBlob(bytes) {
|
|
|
2156
2336
|
const rngState = r.u32();
|
|
2157
2337
|
const tick = r.u32();
|
|
2158
2338
|
const mode = r.u8() === 1 ? "event" : "tick";
|
|
2339
|
+
const schemaHash8 = version >= 3 ? r.bytes(8).slice() : void 0;
|
|
2159
2340
|
let physics;
|
|
2160
2341
|
if (version >= 2) {
|
|
2161
2342
|
const section = r.blob();
|
|
2162
2343
|
if (section.length > 0) physics = section;
|
|
2163
2344
|
}
|
|
2164
|
-
return { version, seed, rngState, tick, mode, physics, snapshot: r.rest() };
|
|
2345
|
+
return { version, seed, rngState, tick, mode, schemaHash8, physics, snapshot: r.rest() };
|
|
2346
|
+
}
|
|
2347
|
+
function sameSchemaHash(a, b) {
|
|
2348
|
+
if (!a || !b) return true;
|
|
2349
|
+
if (a.length !== b.length) return false;
|
|
2350
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
2351
|
+
return true;
|
|
2165
2352
|
}
|
|
2166
2353
|
function writeHibernationBlob(header, snapshot, physics) {
|
|
2167
|
-
const
|
|
2168
|
-
const
|
|
2354
|
+
const hash = header.schemaHash8;
|
|
2355
|
+
const version = hash ? 3 : physics ? 2 : 1;
|
|
2356
|
+
const w = new ByteWriter3(snapshot.length + (physics?.length ?? 0) + 32);
|
|
2169
2357
|
w.u8(version);
|
|
2170
2358
|
w.u32(header.seed >>> 0);
|
|
2171
2359
|
w.u32(header.rngState >>> 0);
|
|
2172
2360
|
w.u32(header.tick >>> 0);
|
|
2173
2361
|
w.u8(header.mode === "event" ? 1 : 0);
|
|
2174
|
-
if (
|
|
2362
|
+
if (hash) w.bytes(hash);
|
|
2363
|
+
if (version >= 2) w.blob(physics ?? new Uint8Array(0));
|
|
2175
2364
|
w.bytes(snapshot);
|
|
2176
2365
|
return w.finish();
|
|
2177
2366
|
}
|
|
@@ -2268,23 +2457,45 @@ import {
|
|
|
2268
2457
|
|
|
2269
2458
|
// src/core/history.ts
|
|
2270
2459
|
var HISTORY_MAX_TICKS = 240;
|
|
2460
|
+
var HISTORY_MAX_CHANNELS = 8;
|
|
2271
2461
|
var STRIDE = 13;
|
|
2272
2462
|
function emptyPose() {
|
|
2273
2463
|
return { x: 0, y: 0, z: 0, qx: 0, qy: 0, qz: 0, qw: 1, vx: 0, vy: 0, vz: 0, wx: 0, wy: 0, wz: 0 };
|
|
2274
2464
|
}
|
|
2275
2465
|
function newEntry() {
|
|
2276
|
-
return {
|
|
2466
|
+
return {
|
|
2467
|
+
tick: -1,
|
|
2468
|
+
count: 0,
|
|
2469
|
+
collections: [],
|
|
2470
|
+
ids: [],
|
|
2471
|
+
values: new Float64Array(0),
|
|
2472
|
+
channels: new Float32Array(0)
|
|
2473
|
+
};
|
|
2277
2474
|
}
|
|
2278
2475
|
var PoseHistory = class {
|
|
2279
2476
|
depth;
|
|
2477
|
+
/** Declared channels by collection. Empty in a room that declared none. */
|
|
2478
|
+
channels;
|
|
2479
|
+
/**
|
|
2480
|
+
* Slots per body in `entry.channels`: the widest declared count, so a body's channels sit at
|
|
2481
|
+
* `i * channelStride` under the same body index the pose uses. `0` when nothing is declared, and
|
|
2482
|
+
* that zero is what short-circuits the whole channel path per body.
|
|
2483
|
+
*/
|
|
2484
|
+
channelStride;
|
|
2280
2485
|
entries = [];
|
|
2281
2486
|
/** Index of the newest entry in `entries`, or -1 when nothing has been captured. */
|
|
2282
2487
|
head = -1;
|
|
2283
2488
|
size = 0;
|
|
2284
2489
|
/** Reused across every body of every capture: the capture path allocates nothing per body. */
|
|
2285
2490
|
scratchPose = emptyPose();
|
|
2286
|
-
|
|
2491
|
+
/** Collections whose `read` hook has already thrown, so the log is one line, not one per tick. */
|
|
2492
|
+
hookFailed = /* @__PURE__ */ new Set();
|
|
2493
|
+
constructor(depth, channels) {
|
|
2287
2494
|
this.depth = Math.max(1, Math.min(HISTORY_MAX_TICKS, Math.floor(depth)));
|
|
2495
|
+
this.channels = channels ?? /* @__PURE__ */ new Map();
|
|
2496
|
+
let stride = 0;
|
|
2497
|
+
for (const spec of this.channels.values()) stride = Math.max(stride, spec.count);
|
|
2498
|
+
this.channelStride = stride;
|
|
2288
2499
|
for (let i = 0; i < this.depth; i++) this.entries.push(newEntry());
|
|
2289
2500
|
}
|
|
2290
2501
|
get length() {
|
|
@@ -2314,14 +2525,14 @@ var PoseHistory = class {
|
|
|
2314
2525
|
entry.count = 0;
|
|
2315
2526
|
const pose = this.scratchPose;
|
|
2316
2527
|
const kind = physics.engineKind;
|
|
2317
|
-
physics.eachTrackedBody((collection, id, body) => {
|
|
2528
|
+
physics.eachTrackedBody((collection, id, body, instance) => {
|
|
2318
2529
|
if (kind === "rapier3d") readRapierPose(body, pose);
|
|
2319
2530
|
else if (kind === "rapier2d") readRapier2dPose(body, pose);
|
|
2320
2531
|
else readMatterPose(body, pose);
|
|
2321
|
-
this.push(entry, collection, id, pose);
|
|
2532
|
+
this.push(entry, collection, id, pose, instance);
|
|
2322
2533
|
});
|
|
2323
2534
|
}
|
|
2324
|
-
push(entry, collection, id, pose) {
|
|
2535
|
+
push(entry, collection, id, pose, instance) {
|
|
2325
2536
|
const i = entry.count;
|
|
2326
2537
|
const need = (i + 1) * STRIDE;
|
|
2327
2538
|
if (entry.values.length < need) {
|
|
@@ -2329,6 +2540,34 @@ var PoseHistory = class {
|
|
|
2329
2540
|
grown.set(entry.values);
|
|
2330
2541
|
entry.values = grown;
|
|
2331
2542
|
}
|
|
2543
|
+
const stride = this.channelStride;
|
|
2544
|
+
if (stride > 0) {
|
|
2545
|
+
const want = (i + 1) * stride;
|
|
2546
|
+
if (entry.channels.length < want) {
|
|
2547
|
+
const grown = new Float32Array(Math.max(want, entry.channels.length * 2, stride * 8));
|
|
2548
|
+
grown.set(entry.channels);
|
|
2549
|
+
entry.channels = grown;
|
|
2550
|
+
}
|
|
2551
|
+
const o2 = i * stride;
|
|
2552
|
+
entry.channels.fill(0, o2, o2 + stride);
|
|
2553
|
+
const spec = this.channels.get(collection);
|
|
2554
|
+
if (spec) {
|
|
2555
|
+
const scratch = spec.scratch;
|
|
2556
|
+
scratch.fill(0);
|
|
2557
|
+
try {
|
|
2558
|
+
spec.read(instance, scratch);
|
|
2559
|
+
entry.channels.set(scratch, o2);
|
|
2560
|
+
} catch (err) {
|
|
2561
|
+
if (!this.hookFailed.has(collection)) {
|
|
2562
|
+
this.hookFailed.add(collection);
|
|
2563
|
+
console.error(
|
|
2564
|
+
`physics.history.channels.${collection}.read threw; that body's channels are zeros`,
|
|
2565
|
+
err
|
|
2566
|
+
);
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2332
2571
|
entry.collections[i] = collection;
|
|
2333
2572
|
entry.ids[i] = id;
|
|
2334
2573
|
const v = entry.values;
|
|
@@ -2399,6 +2638,10 @@ function poseAt(entry, index, into) {
|
|
|
2399
2638
|
into.wy = v[o + 11];
|
|
2400
2639
|
into.wz = v[o + 12];
|
|
2401
2640
|
}
|
|
2641
|
+
function channelsAt(entry, index, stride, count) {
|
|
2642
|
+
const o = index * stride;
|
|
2643
|
+
return entry.channels.subarray(o, o + count);
|
|
2644
|
+
}
|
|
2402
2645
|
function readRapierPose(body, into) {
|
|
2403
2646
|
const t = body.translation();
|
|
2404
2647
|
const r = body.rotation();
|
|
@@ -2451,10 +2694,20 @@ function readMatterPose(body, into) {
|
|
|
2451
2694
|
into.wy = 0;
|
|
2452
2695
|
into.wz = body.angularVelocity;
|
|
2453
2696
|
}
|
|
2454
|
-
function
|
|
2697
|
+
function historyOf(config) {
|
|
2455
2698
|
const declared = config?.history;
|
|
2456
|
-
|
|
2457
|
-
|
|
2699
|
+
const raw = typeof declared === "number" ? declared : typeof declared === "object" && declared !== null ? declared.depth : void 0;
|
|
2700
|
+
if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0) return void 0;
|
|
2701
|
+
const depth = Math.min(HISTORY_MAX_TICKS, Math.floor(raw));
|
|
2702
|
+
const channels = /* @__PURE__ */ new Map();
|
|
2703
|
+
if (typeof declared === "object" && declared !== null) {
|
|
2704
|
+
const spec = declared.channels;
|
|
2705
|
+
for (const [name, s] of Object.entries(spec ?? {})) {
|
|
2706
|
+
const count = Math.min(HISTORY_MAX_CHANNELS, Math.floor(s.count));
|
|
2707
|
+
channels.set(name, { count, read: s.read, scratch: new Float32Array(count) });
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
return { depth, channels };
|
|
2458
2711
|
}
|
|
2459
2712
|
function keyOf(collection, id) {
|
|
2460
2713
|
return `${collection} ${id}`;
|
|
@@ -2804,8 +3057,8 @@ var RewindState = class {
|
|
|
2804
3057
|
history;
|
|
2805
3058
|
scratch;
|
|
2806
3059
|
inside = false;
|
|
2807
|
-
constructor(
|
|
2808
|
-
this.history = new PoseHistory(depth);
|
|
3060
|
+
constructor(setup) {
|
|
3061
|
+
this.history = new PoseHistory(setup.depth, setup.channels);
|
|
2809
3062
|
}
|
|
2810
3063
|
free() {
|
|
2811
3064
|
this.scratch?.free();
|
|
@@ -2824,7 +3077,29 @@ var RewindState = class {
|
|
|
2824
3077
|
);
|
|
2825
3078
|
}
|
|
2826
3079
|
this.scratch ??= physics.engineKind === "rapier3d" ? new RapierScratch(physics, this.history.depth, tickNow) : physics.engineKind === "rapier2d" ? new Rapier2dScratch(physics, this.history.depth, tickNow) : new MatterScratch(physics, this.history.depth, tickNow);
|
|
2827
|
-
const
|
|
3080
|
+
const engineView = this.scratch.view(
|
|
3081
|
+
resolved.entry,
|
|
3082
|
+
resolved.tick,
|
|
3083
|
+
requested,
|
|
3084
|
+
resolved.clamped
|
|
3085
|
+
);
|
|
3086
|
+
const entry = resolved.entry;
|
|
3087
|
+
const history = this.history;
|
|
3088
|
+
const view = {
|
|
3089
|
+
...engineView,
|
|
3090
|
+
// A linear scan of the entry rather than an index: an entry holds one room's tracked bodies,
|
|
3091
|
+
// and building a map per rewind would cost more than the scan it saved.
|
|
3092
|
+
channels: (collection, id) => {
|
|
3093
|
+
const spec = history.channels.get(collection);
|
|
3094
|
+
if (!spec) return void 0;
|
|
3095
|
+
for (let i = 0; i < entry.count; i++) {
|
|
3096
|
+
if (entry.collections[i] === collection && entry.ids[i] === id) {
|
|
3097
|
+
return channelsAt(entry, i, history.channelStride, spec.count);
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
return void 0;
|
|
3101
|
+
}
|
|
3102
|
+
};
|
|
2828
3103
|
this.inside = true;
|
|
2829
3104
|
try {
|
|
2830
3105
|
return fn(view);
|
|
@@ -3284,9 +3559,20 @@ function createRoomApi(core, publicUrl) {
|
|
|
3284
3559
|
get id() {
|
|
3285
3560
|
return core.roomId;
|
|
3286
3561
|
},
|
|
3562
|
+
/**
|
|
3563
|
+
* The same tracked state object handlers get. The one caller that has no other way to it is
|
|
3564
|
+
* `physics.setup`, which runs before `onCreate` on a fresh room and after the snapshot is
|
|
3565
|
+
* decoded on a rebuild — see the doc on `Room.state`.
|
|
3566
|
+
*/
|
|
3567
|
+
get state() {
|
|
3568
|
+
return core.anyState;
|
|
3569
|
+
},
|
|
3287
3570
|
get link() {
|
|
3288
3571
|
return link();
|
|
3289
3572
|
},
|
|
3573
|
+
get seed() {
|
|
3574
|
+
return core.seed;
|
|
3575
|
+
},
|
|
3290
3576
|
get tick() {
|
|
3291
3577
|
return core.tick;
|
|
3292
3578
|
},
|
|
@@ -3477,6 +3763,9 @@ function createRoomApi(core, publicUrl) {
|
|
|
3477
3763
|
},
|
|
3478
3764
|
get broadcast() {
|
|
3479
3765
|
return broadcast;
|
|
3766
|
+
},
|
|
3767
|
+
broadcastExcept(clientId) {
|
|
3768
|
+
return createBroadcastExceptProxy(core, clientId);
|
|
3480
3769
|
}
|
|
3481
3770
|
};
|
|
3482
3771
|
const broadcast = createBroadcastProxy(core);
|
|
@@ -3665,8 +3954,13 @@ var RoomCore = class _RoomCore {
|
|
|
3665
3954
|
}
|
|
3666
3955
|
let restored;
|
|
3667
3956
|
let plain;
|
|
3668
|
-
|
|
3669
|
-
|
|
3957
|
+
const candidate = options.restoreFrom ? parseHibernationBlob(options.restoreFrom) : void 0;
|
|
3958
|
+
if (candidate && !sameSchemaHash(candidate.schemaHash8, this.ext.hash8)) {
|
|
3959
|
+
host.log("warn", [`irtio: room ${options.roomId}: schema changed, snapshot discarded`]);
|
|
3960
|
+
} else if (candidate) {
|
|
3961
|
+
restored = candidate;
|
|
3962
|
+
}
|
|
3963
|
+
if (restored) {
|
|
3670
3964
|
plain = decodeSnapshot2(this.ext, restored.snapshot).state;
|
|
3671
3965
|
const presence = plainEntity(plain, PRESENCE_COLLECTION);
|
|
3672
3966
|
for (const id of [...presence.ids()]) presence.remove(id);
|
|
@@ -3681,7 +3975,7 @@ var RoomCore = class _RoomCore {
|
|
|
3681
3975
|
this.plain = plain;
|
|
3682
3976
|
this.tracked = track(this.ext, plain);
|
|
3683
3977
|
this.anyState = this.tracked.state;
|
|
3684
|
-
this.seed = restored?.seed ?? options.seed ??
|
|
3978
|
+
this.seed = restored?.seed ?? options.seed ?? seedFromRoomId(this.roomId);
|
|
3685
3979
|
this.rng = new Mulberry32(restored?.rngState ?? this.seed);
|
|
3686
3980
|
this.tick = restored?.tick ?? 0;
|
|
3687
3981
|
const self = this;
|
|
@@ -3689,8 +3983,8 @@ var RoomCore = class _RoomCore {
|
|
|
3689
3983
|
this.loop = new Loop(self);
|
|
3690
3984
|
this.api = createRoomApi(self, options.publicUrl ?? DEFAULT_PUBLIC_URL);
|
|
3691
3985
|
this.physics = this.buildPhysics(restored);
|
|
3692
|
-
const
|
|
3693
|
-
this.rewindState =
|
|
3986
|
+
const historySetup = this.physics ? historyOf(definition.config.physics) : void 0;
|
|
3987
|
+
this.rewindState = historySetup === void 0 ? void 0 : new RewindState(historySetup);
|
|
3694
3988
|
this.subscribeDeclaredChannels();
|
|
3695
3989
|
if (restored) {
|
|
3696
3990
|
const onWake = definition.config.onWake;
|
|
@@ -3777,7 +4071,7 @@ var RoomCore = class _RoomCore {
|
|
|
3777
4071
|
const engine4 = loadedPhysics();
|
|
3778
4072
|
if (!engine4) {
|
|
3779
4073
|
throw new Error(
|
|
3780
|
-
"RoomCore: this room declares physics but the engine is not initialized \u2014 the host must `await initPhysics()` (from '@irtio/
|
|
4074
|
+
"RoomCore: this room declares physics but the engine is not initialized \u2014 the host must `await initPhysics()` (in tests: from '@irtio/testing') before constructing the room; handlers are synchronous, so the WASM cannot be loaded later"
|
|
3781
4075
|
);
|
|
3782
4076
|
}
|
|
3783
4077
|
let section;
|
|
@@ -4025,7 +4319,13 @@ var RoomCore = class _RoomCore {
|
|
|
4025
4319
|
snapshot() {
|
|
4026
4320
|
const physics = this.physics ? encodeSection(this.physics) : void 0;
|
|
4027
4321
|
return writeHibernationBlob(
|
|
4028
|
-
{
|
|
4322
|
+
{
|
|
4323
|
+
seed: this.seed,
|
|
4324
|
+
rngState: this.rng.state,
|
|
4325
|
+
tick: this.tick,
|
|
4326
|
+
mode: this.mode,
|
|
4327
|
+
schemaHash8: this.ext.hash8
|
|
4328
|
+
},
|
|
4029
4329
|
encodeSnapshot2(this.ext, this.plain, { tick: this.tick }),
|
|
4030
4330
|
physics
|
|
4031
4331
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
parseHibernationBlob,
|
|
3
3
|
writeHibernationBlob
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-KGZEQTJR.js";
|
|
5
5
|
|
|
6
6
|
// src/migrate.ts
|
|
7
7
|
import { PRESENCE_COLLECTION, withBuiltins } from "@irtio/protocol";
|
|
@@ -53,7 +53,15 @@ function migrateSnapshot(blob, chain, options) {
|
|
|
53
53
|
}
|
|
54
54
|
plain = fromMigrationState(withBuiltins(schema), state, last.version);
|
|
55
55
|
const bytes = writeHibernationBlob(
|
|
56
|
-
{
|
|
56
|
+
{
|
|
57
|
+
seed: parsed.seed,
|
|
58
|
+
rngState: parsed.rngState,
|
|
59
|
+
tick: parsed.tick,
|
|
60
|
+
mode: parsed.mode,
|
|
61
|
+
// Stamped with the *target* schema: these bytes are now that schema's, and a room waking on
|
|
62
|
+
// it must not see them as a mismatch.
|
|
63
|
+
schemaHash8: withBuiltins(schema).hash8
|
|
64
|
+
},
|
|
57
65
|
encodeSnapshot(withBuiltins(schema), plain, { tick: parsed.tick })
|
|
58
66
|
);
|
|
59
67
|
return {
|
|
@@ -302,7 +302,11 @@ interface RoomHost {
|
|
|
302
302
|
}
|
|
303
303
|
interface RoomCoreOptions {
|
|
304
304
|
readonly roomId: string;
|
|
305
|
-
/**
|
|
305
|
+
/**
|
|
306
|
+
* Seed for `room.random()`. Defaults to a stable 32-bit hash of `roomId`, so two rooms of the
|
|
307
|
+
* same game generate different worlds; pass a number to pin the sequence instead. A room
|
|
308
|
+
* restored from a snapshot ignores both and keeps the seed it was created with.
|
|
309
|
+
*/
|
|
306
310
|
readonly seed?: number;
|
|
307
311
|
/** Base URL for `room.link` (`<publicUrl>?room=<id>`); default `http://localhost/`. */
|
|
308
312
|
readonly publicUrl?: string;
|
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 decodeRapier2dSectionEnvelope, i as encodeMatterBodies, j as encodeMatterSectionEnvelope, k as encodePhysicsSection, l as encodeRapier2dSectionEnvelope, m as initMatter, n as initPhysics, o as initRapier2d, p as loadedMatter, q as loadedPhysics, r as loadedRapier2d, s as physicsSectionEngine, t as resetMatterForTests, u as resetPhysicsForTests, v as resetRapier2dForTests } from './room-
|
|
1
|
+
export { D as DEFAULT_TIMELINE_MAX_RECORDS, a as DEFAULT_TIMELINE_MAX_TICKS, E as EVENT_RING_SIZE, H as HOST_CALL_TIMEOUT_MS, b as HostCall, c as HostCallResult, J as JoinOptions, d as JoinResult, L as LogLevel, R as RoomCoreApi, e as RoomCoreOptions, f as RoomEvent, g as RoomEventKind, h as RoomFullError, i as RoomHost, j as RoomInspection, k as RoomStats, T as TimelineDump, l as TimelineFrame, m as TimelineRecorder, n as TimelineRecorderOptions, o as inspectState } from './contract-C5aqs49-.js';
|
|
2
|
+
export { C as CRASH_AFTER_THROWS, M as MAX_CATCHUP, a as MatterBodyRecord, b as MatterSection, c as Mulberry32, P as PhysicsEngineTag, d as PhysicsSection, R as RoomCore, e as decodeMatterBodies, f as decodeMatterSectionEnvelope, g as decodePhysicsSection, h as decodeRapier2dSectionEnvelope, i as encodeMatterBodies, j as encodeMatterSectionEnvelope, k as encodePhysicsSection, l as encodeRapier2dSectionEnvelope, m as initMatter, n as initPhysics, o as initRapier2d, p as loadedMatter, q as loadedPhysics, r as loadedRapier2d, s as physicsSectionEngine, t as resetMatterForTests, u as resetPhysicsForTests, v as resetRapier2dForTests } from './room-Db3iPBKM.js';
|
|
3
3
|
import { CollectionDesc, AnySchema, PlainState, DirtySet } from '@irtio/schema';
|
|
4
4
|
import '@irtio/protocol';
|
|
5
5
|
import '@irtio/server';
|
|
@@ -70,9 +70,16 @@ declare function catchUpDirty(ext: AnySchema, plain: PlainState, fromRole: strin
|
|
|
70
70
|
* u32 rng state
|
|
71
71
|
* u32 tick
|
|
72
72
|
* u8 mode (1 = event, 0 = tick)
|
|
73
|
+
* [v3] 8 bytes schema hash (`withBuiltins(schema).hash8`)
|
|
73
74
|
* [v2] blob physics section (varint length + bytes; length 0 = no world)
|
|
74
75
|
* ... encodeSnapshot(withBuiltins(schema), state)
|
|
75
76
|
*
|
|
77
|
+
* **v3** adds the schema hash. Before it, restoring a snapshot written under a different schema
|
|
78
|
+
* fed the old bytes to the new decoder and surfaced as a `TypeError` from deep inside the string
|
|
79
|
+
* decoder; the room could only be started again by hand-deleting `.irtio/snapshots`. The hash is
|
|
80
|
+
* in the envelope (not only in the codec payload) so the check costs no decode: a mismatch is
|
|
81
|
+
* seen, logged and the snapshot discarded before anything is parsed against the wrong schema.
|
|
82
|
+
*
|
|
76
83
|
* **v2 (week 9, D22)** adds the physics section: the Rapier world snapshot plus the entity↔handle
|
|
77
84
|
* map (`core/physics.ts` encodes it). It rides *inside* this blob rather than beside it so that
|
|
78
85
|
* wake is atomic — state and world always come back from the same bytes, and every store, cache
|
|
@@ -89,8 +96,8 @@ declare function catchUpDirty(ext: AnySchema, plain: PlainState, fromRole: strin
|
|
|
89
96
|
* instead of parsing bytes it may not yet know how to read.
|
|
90
97
|
*/
|
|
91
98
|
|
|
92
|
-
/** Written when the
|
|
93
|
-
declare const SNAPSHOT_FORMAT_VERSION =
|
|
99
|
+
/** Written when the header carries a schema hash (every room does); older versions still read. */
|
|
100
|
+
declare const SNAPSHOT_FORMAT_VERSION = 3;
|
|
94
101
|
/** Versions this build can parse. */
|
|
95
102
|
declare const READABLE_SNAPSHOT_VERSIONS: readonly number[];
|
|
96
103
|
type SnapshotMode = 'tick' | 'event';
|
|
@@ -99,6 +106,12 @@ interface SnapshotHeader {
|
|
|
99
106
|
readonly rngState: number;
|
|
100
107
|
readonly tick: number;
|
|
101
108
|
readonly mode: SnapshotMode;
|
|
109
|
+
/**
|
|
110
|
+
* `withBuiltins(schema).hash8` of the schema these bytes were written under. Omitted only by a
|
|
111
|
+
* writer that has no schema in hand; a restore that finds it absent cannot check and proceeds
|
|
112
|
+
* as pre-v3 builds did.
|
|
113
|
+
*/
|
|
114
|
+
readonly schemaHash8?: Uint8Array | undefined;
|
|
102
115
|
}
|
|
103
116
|
interface ParsedSnapshot extends SnapshotHeader {
|
|
104
117
|
/** The blob's own format version (1 or 2). */
|
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-MTBMGUGG.js";
|
|
6
6
|
import {
|
|
7
7
|
CRASH_AFTER_THROWS,
|
|
8
8
|
DEFAULT_TIMELINE_MAX_RECORDS,
|
|
@@ -47,7 +47,7 @@ import {
|
|
|
47
47
|
visibleNames,
|
|
48
48
|
visibleTo,
|
|
49
49
|
writeHibernationBlob
|
|
50
|
-
} from "./chunk-
|
|
50
|
+
} from "./chunk-KGZEQTJR.js";
|
|
51
51
|
export {
|
|
52
52
|
CRASH_AFTER_THROWS,
|
|
53
53
|
DEFAULT_TIMELINE_MAX_RECORDS,
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { AttributeOptions, ProfileSnapshot } from '@irtio/protocol';
|
|
2
2
|
import { DirtySet, AnySchema, PlainState, Tracked, State } from '@irtio/schema';
|
|
3
3
|
import { RoomDefinition, RoomMode, Room, RewindView, Ctx, MatterModule, MatterEngine, MatterBody, RapierModule, RapierWorld, RapierRigidBody, Rapier2dModule, Rapier2dWorld, Rapier2dRigidBody, ResolvedRoomConfig, LeaveReason } from '@irtio/server';
|
|
4
|
-
import { i as RoomHost, k as RoomStats, L as LogLevel, R as RoomCoreApi, e as RoomCoreOptions, g as RoomEventKind, n as TimelineRecorderOptions, T as TimelineDump, j as RoomInspection, J as JoinOptions, d as JoinResult, c as HostCallResult } from './contract-
|
|
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-C5aqs49-.js';
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* `room.random()` — mulberry32 over a u32 seed. Deterministic, tiny, and serializable: the
|
|
8
|
-
* generator's whole state is one u32, so `serialize()`/`restore()` round-trip it exactly.
|
|
9
|
-
*/
|
|
10
6
|
declare class Mulberry32 {
|
|
11
7
|
/** Current internal state (u32). Survives hibernation. */
|
|
12
8
|
state: number;
|
|
@@ -109,7 +105,7 @@ interface PhysicsApi {
|
|
|
109
105
|
* the only thing outside the engine runtimes that sees a live body other than through
|
|
110
106
|
* `bodyFor`, and it never hands out the map itself.
|
|
111
107
|
*/
|
|
112
|
-
eachTrackedBody(fn: (collection: string, id: string, body: unknown) => void): void;
|
|
108
|
+
eachTrackedBody(fn: (collection: string, id: string, body: unknown, instance: Record<string, unknown>) => void): void;
|
|
113
109
|
/** rapier3d and rapier2d: the engine namespace. `engineKind` says which module it is. */
|
|
114
110
|
readonly rapier: unknown;
|
|
115
111
|
/** rapier3d and rapier2d: the live `World`. */
|
|
@@ -139,6 +135,8 @@ interface RoomInternals {
|
|
|
139
135
|
readonly ext: AnySchema;
|
|
140
136
|
readonly host: RoomHost;
|
|
141
137
|
readonly roomId: string;
|
|
138
|
+
/** The resolved RNG seed: explicit option, else derived from the room id; restored on wake. */
|
|
139
|
+
readonly seed: number;
|
|
142
140
|
readonly mode: RoomMode;
|
|
143
141
|
/** The plain state the tracked proxies wrap (what the codec reads). */
|
|
144
142
|
readonly plain: PlainState;
|
|
@@ -346,6 +344,15 @@ declare class MatterRuntime {
|
|
|
346
344
|
private readonly restore;
|
|
347
345
|
private readonly stepMs;
|
|
348
346
|
private readonly sleepSynced;
|
|
347
|
+
/**
|
|
348
|
+
* The plain record object each body was built from. See `PhysicsRuntime.sourceRecords`: `add()`
|
|
349
|
+
* installs a new record object, so an id removed and re-added between two reconciles (a pooled
|
|
350
|
+
* projectile) is identifiable by identity, and is the one case where a live body has to be
|
|
351
|
+
* rebuilt so the new row's spawn pose and velocity are honoured.
|
|
352
|
+
*/
|
|
353
|
+
private readonly sourceRecords;
|
|
354
|
+
/** `collection.field` pairs already warned about a channel a plane cannot hold. */
|
|
355
|
+
private readonly warnedChannels;
|
|
349
356
|
constructor(core: RoomInternals, matter: MatterModule, options: MatterRuntimeOptions);
|
|
350
357
|
get timestep(): number;
|
|
351
358
|
runSetup(room: Room): void;
|
|
@@ -356,7 +363,7 @@ declare class MatterRuntime {
|
|
|
356
363
|
* D72: every tracked body, in the order `sync()` walks them. The matter half of the same
|
|
357
364
|
* read-only accessor `PhysicsRuntime` carries; see its comment for what calls it and when.
|
|
358
365
|
*/
|
|
359
|
-
eachTrackedBody(fn: (collection: string, id: string, body: MatterBody) => void): void;
|
|
366
|
+
eachTrackedBody(fn: (collection: string, id: string, body: MatterBody, instance: Record<string, unknown>) => void): void;
|
|
360
367
|
private applyState;
|
|
361
368
|
private applyRecordToBody;
|
|
362
369
|
reconcile(): void;
|
|
@@ -488,6 +495,19 @@ declare class PhysicsRuntime {
|
|
|
488
495
|
* world rebuilt from schema state would wake it with a kick). Found by the drift check.
|
|
489
496
|
*/
|
|
490
497
|
private readonly sleepSynced;
|
|
498
|
+
/**
|
|
499
|
+
* The plain record object each body was built from, by body key. `add()` always installs a
|
|
500
|
+
* **new** record object (`normalizeRecord` builds one), so an id that is removed and re-added
|
|
501
|
+
* between two reconciles — the pooled-projectile shape — is identifiable by identity alone,
|
|
502
|
+
* which is the only way to see it from here: both edits happened before this module looked.
|
|
503
|
+
* Without it the old body kept its pose and velocity and the new row's spawn values were lost.
|
|
504
|
+
*
|
|
505
|
+
* Unknown means adopt, never rebuild: a world restored from a hibernation blob has bodies with
|
|
506
|
+
* no recorded source, and rebuilding those would throw away exactly what the blob preserved.
|
|
507
|
+
*/
|
|
508
|
+
private readonly sourceRecords;
|
|
509
|
+
/** `collection.field` pairs already warned about a channel value the engine would not take. */
|
|
510
|
+
private readonly warnedChannels;
|
|
491
511
|
constructor(core: RoomInternals, rapier: RapierModule, options: PhysicsRuntimeOptions);
|
|
492
512
|
get timestep(): number;
|
|
493
513
|
/** Runs the room's `setup` — static geometry — on a world that was built rather than restored. */
|
|
@@ -496,13 +516,24 @@ declare class PhysicsRuntime {
|
|
|
496
516
|
/** The body behind an instance, created on demand so a handler's own `add` is usable at once. */
|
|
497
517
|
bodyFor(collection: string, id: string): RapierRigidBody | undefined;
|
|
498
518
|
private create;
|
|
519
|
+
/**
|
|
520
|
+
* A spawn value the engine will not take, said once per field.
|
|
521
|
+
*
|
|
522
|
+
* Rapier accepts `setLinvel` on an axis the body has disabled and then keeps zero, and it
|
|
523
|
+
* accepts one on a fixed body and then ignores it — both read back as "the field was applied"
|
|
524
|
+
* and behave as if it never was. The desc says which, before the body exists, so the check is
|
|
525
|
+
* on the desc: the usual case is the 2D-in-3D recipe (`enabledTranslations(true, true, false)`)
|
|
526
|
+
* meeting a row that spawns with a `vz`. Named by field, because the field is what the author
|
|
527
|
+
* wrote and what they will grep for.
|
|
528
|
+
*/
|
|
529
|
+
private warnLockedChannels;
|
|
499
530
|
/**
|
|
500
531
|
* D72: every tracked body, in the order `sync()` walks them (collection order, then instance
|
|
501
532
|
* order). Read-only: it hands out the live bodies and nothing else, and the map stays private.
|
|
502
533
|
* The pose history calls this once per tick and the rewind scratch calls it per rewind; a room
|
|
503
534
|
* that declares no `physics.history` never calls it at all.
|
|
504
535
|
*/
|
|
505
|
-
eachTrackedBody(fn: (collection: string, id: string, body: RapierRigidBody) => void): void;
|
|
536
|
+
eachTrackedBody(fn: (collection: string, id: string, body: RapierRigidBody, instance: Record<string, unknown>) => void): void;
|
|
506
537
|
private applyRecordToBody;
|
|
507
538
|
/**
|
|
508
539
|
* Creates bodies for new instances and destroys bodies whose instance is gone. Runs once per
|
|
@@ -585,6 +616,16 @@ declare class Rapier2dRuntime {
|
|
|
585
616
|
private readonly bodies;
|
|
586
617
|
/** See `PhysicsRuntime.sleepSynced`: the one extra sync a body owes on the tick it sleeps. */
|
|
587
618
|
private readonly sleepSynced;
|
|
619
|
+
/**
|
|
620
|
+
* See `PhysicsRuntime.sourceRecords`: the plain record object each body was built from. `add()`
|
|
621
|
+
* always installs a new record object, so an id removed and re-added between two reconciles —
|
|
622
|
+
* the pooled-projectile shape — is identifiable by identity alone, and its body is rebuilt so
|
|
623
|
+
* the new row's spawn pose and velocity are what the world gets. Unknown means adopt, never
|
|
624
|
+
* rebuild: a world restored from a hibernation blob has bodies with no recorded source.
|
|
625
|
+
*/
|
|
626
|
+
private readonly sourceRecords;
|
|
627
|
+
/** `collection.field` pairs already warned about a channel value the engine would not take. */
|
|
628
|
+
private readonly warnedChannels;
|
|
588
629
|
constructor(core: RoomInternals, rapier: Rapier2dModule, options: Rapier2dRuntimeOptions);
|
|
589
630
|
get timestep(): number;
|
|
590
631
|
/** Runs the room's `setup` — static geometry — on a world that was built rather than restored. */
|
|
@@ -593,11 +634,23 @@ declare class Rapier2dRuntime {
|
|
|
593
634
|
/** The body behind an instance, created on demand so a handler's own `add` is usable at once. */
|
|
594
635
|
bodyFor(collection: string, id: string): Rapier2dRigidBody | undefined;
|
|
595
636
|
private create;
|
|
637
|
+
/**
|
|
638
|
+
* A spawn value the engine will not take, said once per field — the planar half of
|
|
639
|
+
* `PhysicsRuntime.warnLockedChannels`.
|
|
640
|
+
*
|
|
641
|
+
* Rapier accepts `setLinvel` on an axis the body has disabled and then keeps zero, and it
|
|
642
|
+
* accepts one on a fixed body and then ignores it: both read back as "the field was applied"
|
|
643
|
+
* and behave as if it never was. The desc says which, before the body exists, so the check is on
|
|
644
|
+
* the desc. In the plane there are three movable channels — `vx`, `vy` and the one rotation
|
|
645
|
+
* `wz` — and the desc carries exactly those three flags. Named by field, because the field is
|
|
646
|
+
* what the author wrote and what they will grep for.
|
|
647
|
+
*/
|
|
648
|
+
private warnLockedChannels;
|
|
596
649
|
/**
|
|
597
650
|
* D72: every tracked body, in the order `sync()` walks them (collection order, then instance
|
|
598
651
|
* order). The rapier2d third of the same read-only accessor the other two runtimes carry.
|
|
599
652
|
*/
|
|
600
|
-
eachTrackedBody(fn: (collection: string, id: string, body: Rapier2dRigidBody) => void): void;
|
|
653
|
+
eachTrackedBody(fn: (collection: string, id: string, body: Rapier2dRigidBody, instance: Record<string, unknown>) => void): void;
|
|
601
654
|
private applyRecordToBody;
|
|
602
655
|
/**
|
|
603
656
|
* Creates bodies for new instances and destroys bodies whose instance is gone. Runs once per
|
|
@@ -668,7 +721,7 @@ declare class RoomCore<S extends AnySchema = AnySchema> implements RoomCoreApi<S
|
|
|
668
721
|
* it again over its next `history` ticks.
|
|
669
722
|
*/
|
|
670
723
|
private readonly rewindState;
|
|
671
|
-
|
|
724
|
+
readonly seed: number;
|
|
672
725
|
private readonly api;
|
|
673
726
|
private readonly internals;
|
|
674
727
|
private started;
|
package/dist/test/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { R as RoomCore } from '../room-
|
|
2
|
-
export { m as initMatter, n as initPhysics, o as initRapier2d } from '../room-
|
|
1
|
+
import { R as RoomCore } from '../room-Db3iPBKM.js';
|
|
2
|
+
export { m as initMatter, n as initPhysics, o as initRapier2d } from '../room-Db3iPBKM.js';
|
|
3
3
|
import { ErrorCodeName, FrameType } from '@irtio/protocol';
|
|
4
4
|
import { NpcConfig, LeaveReason, Room, RoomDefinition } from '@irtio/server';
|
|
5
|
-
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-C5aqs49-.js';
|
|
6
6
|
import { AnySchema, PlainState, EntityCollection, State } from '@irtio/schema';
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -331,7 +331,7 @@ interface FakeClient<S extends AnySchema = AnySchema> {
|
|
|
331
331
|
*/
|
|
332
332
|
|
|
333
333
|
interface HarnessOptions {
|
|
334
|
-
/** `room.random()` seed;
|
|
334
|
+
/** `room.random()` seed; defaults to a hash of `roomId`, exactly as `RoomCore` does. */
|
|
335
335
|
readonly seed?: number;
|
|
336
336
|
/** Base URL for `room.link`. */
|
|
337
337
|
readonly publicUrl?: string;
|
package/dist/test/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
initPhysics,
|
|
6
6
|
initRapier2d,
|
|
7
7
|
visibleNames
|
|
8
|
-
} from "../chunk-
|
|
8
|
+
} from "../chunk-KGZEQTJR.js";
|
|
9
9
|
|
|
10
10
|
// src/test/clock.ts
|
|
11
11
|
var FakeClock = class {
|
|
@@ -677,7 +677,7 @@ var Harness = class {
|
|
|
677
677
|
this.host.onSend = (clientId, frame) => this.deliver(clientId, frame);
|
|
678
678
|
this.coreRef = new RoomCore(definition, this.host, {
|
|
679
679
|
roomId: options.roomId ?? "test-room",
|
|
680
|
-
seed: options.seed
|
|
680
|
+
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
681
681
|
...options.publicUrl !== void 0 ? { publicUrl: options.publicUrl } : {},
|
|
682
682
|
...options.restoreFrom !== void 0 ? { restoreFrom: options.restoreFrom } : {}
|
|
683
683
|
});
|
|
@@ -963,7 +963,7 @@ var Harness = class {
|
|
|
963
963
|
this.nextClientId = 1;
|
|
964
964
|
this.coreRef = RoomCore.restore(this.definition, bytes, this.host, {
|
|
965
965
|
roomId: this.options.roomId ?? "test-room",
|
|
966
|
-
seed: this.options.seed
|
|
966
|
+
...this.options.seed !== void 0 ? { seed: this.options.seed } : {},
|
|
967
967
|
...this.options.publicUrl !== void 0 ? { publicUrl: this.options.publicUrl } : {}
|
|
968
968
|
});
|
|
969
969
|
this.coreRef.start();
|
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-C5aqs49-.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-MTBMGUGG.js";
|
|
4
4
|
import {
|
|
5
5
|
RoomCore,
|
|
6
6
|
RoomFullError,
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
initRapier2d,
|
|
10
10
|
onFirstRapierStep,
|
|
11
11
|
rapierHasStepped
|
|
12
|
-
} from "../chunk-
|
|
12
|
+
} from "../chunk-KGZEQTJR.js";
|
|
13
13
|
|
|
14
14
|
// src/worker/index.ts
|
|
15
15
|
import { getHeapStatistics } from "v8";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@irtio/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "irtio room runtime: RoomCore, worker_threads host, in-process test harness",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
"@dimforge/rapier2d-compat": "0.20.0",
|
|
32
32
|
"@dimforge/rapier3d-compat": "0.20.0",
|
|
33
33
|
"matter-js": "0.20.0",
|
|
34
|
-
"@irtio/protocol": "0.
|
|
35
|
-
"@irtio/schema": "0.
|
|
36
|
-
"@irtio/server": "0.
|
|
34
|
+
"@irtio/protocol": "0.10.0",
|
|
35
|
+
"@irtio/schema": "0.10.0",
|
|
36
|
+
"@irtio/server": "0.10.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/matter-js": "0.20.2"
|