@irtio/runtime 0.7.0 → 0.9.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-EXPFVRD4.js → chunk-ADZZEVJU.js} +868 -30
- package/dist/{chunk-ZNNTF7Y3.js → chunk-SBDZ45JG.js} +10 -2
- package/dist/{contract-DwxqjeWP.d.ts → contract-C5aqs49-.d.ts} +23 -1
- package/dist/index.d.ts +18 -5
- package/dist/index.js +12 -2
- package/dist/{room-CoQDczh2.d.ts → room-C4RZJ2KO.d.ts} +192 -15
- package/dist/test/index.d.ts +15 -4
- package/dist/test/index.js +12 -4
- package/dist/worker/index.d.ts +9 -1
- package/dist/worker/index.js +14 -3
- package/package.json +5 -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 ----
|
|
@@ -1089,6 +1106,17 @@ var MatterRuntime = class {
|
|
|
1089
1106
|
for (const [channel, field] of physics.channels) {
|
|
1090
1107
|
const raw = record[field];
|
|
1091
1108
|
if (typeof raw !== "number") continue;
|
|
1109
|
+
if (raw !== 0 && !PLANAR_CHANNELS.has(channel)) {
|
|
1110
|
+
const key = `${desc.name}.${field}`;
|
|
1111
|
+
if (!this.warnedChannels.has(key)) {
|
|
1112
|
+
this.warnedChannels.add(key);
|
|
1113
|
+
this.core.log(
|
|
1114
|
+
"warn",
|
|
1115
|
+
`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`
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1092
1120
|
applyChannel2d(channel, raw, t);
|
|
1093
1121
|
}
|
|
1094
1122
|
this.applyState(body, {
|
|
@@ -1110,8 +1138,21 @@ var MatterRuntime = class {
|
|
|
1110
1138
|
for (const id of coll.ids()) {
|
|
1111
1139
|
const key = bodyKey(desc.name, id);
|
|
1112
1140
|
live.add(key);
|
|
1113
|
-
if (this.bodies.has(key)) continue;
|
|
1114
1141
|
const record = coll.get(id);
|
|
1142
|
+
const existing = this.bodies.get(key);
|
|
1143
|
+
if (existing) {
|
|
1144
|
+
if (record === void 0) continue;
|
|
1145
|
+
const source = this.sourceRecords.get(key);
|
|
1146
|
+
if (source === void 0) {
|
|
1147
|
+
this.sourceRecords.set(key, record);
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
if (source === record) continue;
|
|
1151
|
+
this.bodies.delete(key);
|
|
1152
|
+
this.sleepSynced.delete(key);
|
|
1153
|
+
this.sourceRecords.delete(key);
|
|
1154
|
+
this.matter.Composite.remove(this.engine.world, [existing.body, ...existing.constraints]);
|
|
1155
|
+
}
|
|
1115
1156
|
if (record !== void 0) this.create(desc, id, record);
|
|
1116
1157
|
}
|
|
1117
1158
|
}
|
|
@@ -1119,6 +1160,7 @@ var MatterRuntime = class {
|
|
|
1119
1160
|
if (live.has(key)) continue;
|
|
1120
1161
|
this.bodies.delete(key);
|
|
1121
1162
|
this.sleepSynced.delete(key);
|
|
1163
|
+
this.sourceRecords.delete(key);
|
|
1122
1164
|
this.matter.Composite.remove(this.engine.world, [attached.body, ...attached.constraints]);
|
|
1123
1165
|
}
|
|
1124
1166
|
}
|
|
@@ -1255,9 +1297,28 @@ function encodePhysicsSection(section) {
|
|
|
1255
1297
|
return w.finish();
|
|
1256
1298
|
}
|
|
1257
1299
|
var MATTER_TAG = 1;
|
|
1300
|
+
var RAPIER2D_TAG = 2;
|
|
1258
1301
|
function physicsSectionEngine(bytes) {
|
|
1259
1302
|
const r = new ByteReader2(bytes);
|
|
1260
|
-
|
|
1303
|
+
if (r.varint() !== 0) return "rapier3d";
|
|
1304
|
+
const tag = r.u8();
|
|
1305
|
+
if (tag === MATTER_TAG) return "matter2d";
|
|
1306
|
+
if (tag === RAPIER2D_TAG) return "rapier2d";
|
|
1307
|
+
return "rapier3d";
|
|
1308
|
+
}
|
|
1309
|
+
function encodeRapier2dSectionEnvelope(payload) {
|
|
1310
|
+
const w = new ByteWriter2(payload.length + 8);
|
|
1311
|
+
w.varint(0);
|
|
1312
|
+
w.u8(RAPIER2D_TAG);
|
|
1313
|
+
w.bytes(payload);
|
|
1314
|
+
return w.finish();
|
|
1315
|
+
}
|
|
1316
|
+
function decodeRapier2dSectionEnvelope(bytes) {
|
|
1317
|
+
const r = new ByteReader2(bytes);
|
|
1318
|
+
if (r.varint() !== 0 || r.u8() !== RAPIER2D_TAG) {
|
|
1319
|
+
throw new Error("irtio: this physics section was not written by rapier2d");
|
|
1320
|
+
}
|
|
1321
|
+
return r.rest();
|
|
1261
1322
|
}
|
|
1262
1323
|
function encodeMatterSectionEnvelope(payload) {
|
|
1263
1324
|
const w = new ByteWriter2(payload.length + 8);
|
|
@@ -1330,6 +1391,19 @@ var PhysicsRuntime = class {
|
|
|
1330
1391
|
* world rebuilt from schema state would wake it with a kick). Found by the drift check.
|
|
1331
1392
|
*/
|
|
1332
1393
|
sleepSynced = /* @__PURE__ */ new Set();
|
|
1394
|
+
/**
|
|
1395
|
+
* The plain record object each body was built from, by body key. `add()` always installs a
|
|
1396
|
+
* **new** record object (`normalizeRecord` builds one), so an id that is removed and re-added
|
|
1397
|
+
* between two reconciles — the pooled-projectile shape — is identifiable by identity alone,
|
|
1398
|
+
* which is the only way to see it from here: both edits happened before this module looked.
|
|
1399
|
+
* Without it the old body kept its pose and velocity and the new row's spawn values were lost.
|
|
1400
|
+
*
|
|
1401
|
+
* Unknown means adopt, never rebuild: a world restored from a hibernation blob has bodies with
|
|
1402
|
+
* no recorded source, and rebuilding those would throw away exactly what the blob preserved.
|
|
1403
|
+
*/
|
|
1404
|
+
sourceRecords = /* @__PURE__ */ new Map();
|
|
1405
|
+
/** `collection.field` pairs already warned about a channel value the engine would not take. */
|
|
1406
|
+
warnedChannels = /* @__PURE__ */ new Set();
|
|
1333
1407
|
constructor(core, rapier, options) {
|
|
1334
1408
|
const config = core.definition.config.physics;
|
|
1335
1409
|
if (!config) throw new Error("PhysicsRuntime: the room config declares no physics");
|
|
@@ -1404,10 +1478,50 @@ var PhysicsRuntime = class {
|
|
|
1404
1478
|
}
|
|
1405
1479
|
const body = this.world.createRigidBody(spec.body);
|
|
1406
1480
|
for (const collider of spec.colliders ?? []) this.world.createCollider(collider, body);
|
|
1481
|
+
this.warnLockedChannels(desc, spec, record);
|
|
1407
1482
|
this.applyRecordToBody(desc, body, record);
|
|
1408
1483
|
this.bodies.set(bodyKey2(desc.name, id), body);
|
|
1484
|
+
this.sourceRecords.set(bodyKey2(desc.name, id), record);
|
|
1409
1485
|
return body;
|
|
1410
1486
|
}
|
|
1487
|
+
/**
|
|
1488
|
+
* A spawn value the engine will not take, said once per field.
|
|
1489
|
+
*
|
|
1490
|
+
* Rapier accepts `setLinvel` on an axis the body has disabled and then keeps zero, and it
|
|
1491
|
+
* accepts one on a fixed body and then ignores it — both read back as "the field was applied"
|
|
1492
|
+
* and behave as if it never was. The desc says which, before the body exists, so the check is
|
|
1493
|
+
* on the desc: the usual case is the 2D-in-3D recipe (`enabledTranslations(true, true, false)`)
|
|
1494
|
+
* meeting a row that spawns with a `vz`. Named by field, because the field is what the author
|
|
1495
|
+
* wrote and what they will grep for.
|
|
1496
|
+
*/
|
|
1497
|
+
warnLockedChannels(desc, spec, record) {
|
|
1498
|
+
const physics = desc.physics;
|
|
1499
|
+
if (!physics) return;
|
|
1500
|
+
const b = spec.body;
|
|
1501
|
+
const frozen = b.status !== 0;
|
|
1502
|
+
const enabled = {
|
|
1503
|
+
vx: b.translationsEnabledX,
|
|
1504
|
+
vy: b.translationsEnabledY,
|
|
1505
|
+
vz: b.translationsEnabledZ,
|
|
1506
|
+
wx: b.rotationsEnabledX,
|
|
1507
|
+
wy: b.rotationsEnabledY,
|
|
1508
|
+
wz: b.rotationsEnabledZ
|
|
1509
|
+
};
|
|
1510
|
+
for (const [channel, field] of physics.channels) {
|
|
1511
|
+
const allowed = enabled[channel];
|
|
1512
|
+
if (allowed === void 0) continue;
|
|
1513
|
+
const raw = record[field];
|
|
1514
|
+
if (typeof raw !== "number" || raw === 0) continue;
|
|
1515
|
+
if (allowed && !frozen) continue;
|
|
1516
|
+
const key = `${desc.name}.${field}`;
|
|
1517
|
+
if (this.warnedChannels.has(key)) continue;
|
|
1518
|
+
this.warnedChannels.add(key);
|
|
1519
|
+
this.core.log(
|
|
1520
|
+
"warn",
|
|
1521
|
+
`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"
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1411
1525
|
// ---- M6 lane F: rewind ----
|
|
1412
1526
|
/**
|
|
1413
1527
|
* D72: every tracked body, in the order `sync()` walks them (collection order, then instance
|
|
@@ -1503,8 +1617,21 @@ var PhysicsRuntime = class {
|
|
|
1503
1617
|
for (const id of coll.ids()) {
|
|
1504
1618
|
const key = bodyKey2(desc.name, id);
|
|
1505
1619
|
live.add(key);
|
|
1506
|
-
if (this.bodies.has(key)) continue;
|
|
1507
1620
|
const record = coll.get(id);
|
|
1621
|
+
const existing = this.bodies.get(key);
|
|
1622
|
+
if (existing) {
|
|
1623
|
+
if (record === void 0) continue;
|
|
1624
|
+
const source = this.sourceRecords.get(key);
|
|
1625
|
+
if (source === void 0) {
|
|
1626
|
+
this.sourceRecords.set(key, record);
|
|
1627
|
+
continue;
|
|
1628
|
+
}
|
|
1629
|
+
if (source === record) continue;
|
|
1630
|
+
this.bodies.delete(key);
|
|
1631
|
+
this.sleepSynced.delete(key);
|
|
1632
|
+
this.sourceRecords.delete(key);
|
|
1633
|
+
this.world.removeRigidBody(existing);
|
|
1634
|
+
}
|
|
1508
1635
|
if (record !== void 0) this.create(desc, id, record);
|
|
1509
1636
|
}
|
|
1510
1637
|
}
|
|
@@ -1512,6 +1639,7 @@ var PhysicsRuntime = class {
|
|
|
1512
1639
|
if (live.has(key)) continue;
|
|
1513
1640
|
this.bodies.delete(key);
|
|
1514
1641
|
this.sleepSynced.delete(key);
|
|
1642
|
+
this.sourceRecords.delete(key);
|
|
1515
1643
|
this.world.removeRigidBody(body);
|
|
1516
1644
|
}
|
|
1517
1645
|
}
|
|
@@ -1611,6 +1739,14 @@ function channelValue(channel, t, r, v, w) {
|
|
|
1611
1739
|
}
|
|
1612
1740
|
|
|
1613
1741
|
// src/core/random.ts
|
|
1742
|
+
function seedFromRoomId(roomId) {
|
|
1743
|
+
let h = 2166136261;
|
|
1744
|
+
for (let i = 0; i < roomId.length; i++) {
|
|
1745
|
+
h ^= roomId.charCodeAt(i);
|
|
1746
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
1747
|
+
}
|
|
1748
|
+
return h === 0 ? 2654435769 : h >>> 0;
|
|
1749
|
+
}
|
|
1614
1750
|
var Mulberry32 = class {
|
|
1615
1751
|
/** Current internal state (u32). Survives hibernation. */
|
|
1616
1752
|
state;
|
|
@@ -1627,6 +1763,324 @@ var Mulberry32 = class {
|
|
|
1627
1763
|
}
|
|
1628
1764
|
};
|
|
1629
1765
|
|
|
1766
|
+
// src/core/rapier2d.ts
|
|
1767
|
+
import { angleFrom2d, applyChannel2d as applyChannel2d2, channelOf2d as channelOf2d2 } from "@irtio/schema";
|
|
1768
|
+
var engine3;
|
|
1769
|
+
var loading3;
|
|
1770
|
+
async function initRapier2d() {
|
|
1771
|
+
if (engine3) return engine3;
|
|
1772
|
+
loading3 ??= (async () => {
|
|
1773
|
+
const mod = await import("@dimforge/rapier2d-compat");
|
|
1774
|
+
const ns = mod.default ?? mod;
|
|
1775
|
+
await ns.init();
|
|
1776
|
+
engine3 = ns;
|
|
1777
|
+
return ns;
|
|
1778
|
+
})();
|
|
1779
|
+
return loading3;
|
|
1780
|
+
}
|
|
1781
|
+
function loadedRapier2d() {
|
|
1782
|
+
return engine3;
|
|
1783
|
+
}
|
|
1784
|
+
function resetRapier2dForTests() {
|
|
1785
|
+
engine3 = void 0;
|
|
1786
|
+
loading3 = void 0;
|
|
1787
|
+
}
|
|
1788
|
+
function bodyKey3(collection, id) {
|
|
1789
|
+
return `${collection}\0${id}`;
|
|
1790
|
+
}
|
|
1791
|
+
var Rapier2dRuntime = class {
|
|
1792
|
+
engineKind = "rapier2d";
|
|
1793
|
+
/** matter2d only; present so every runtime satisfies one internal shape. */
|
|
1794
|
+
matter = void 0;
|
|
1795
|
+
matterEngine = void 0;
|
|
1796
|
+
rapier;
|
|
1797
|
+
world;
|
|
1798
|
+
/** `true` when the world was built from scratch and `setup` has to run. */
|
|
1799
|
+
rebuilt;
|
|
1800
|
+
/** Rapier restores a whole world, contacts included, so `setup` runs only on a rebuild. */
|
|
1801
|
+
get needsSetup() {
|
|
1802
|
+
return this.rebuilt;
|
|
1803
|
+
}
|
|
1804
|
+
core;
|
|
1805
|
+
config;
|
|
1806
|
+
/** Physics-backed collections, in schema (name-sorted) order. */
|
|
1807
|
+
collections;
|
|
1808
|
+
bodies = /* @__PURE__ */ new Map();
|
|
1809
|
+
/** See `PhysicsRuntime.sleepSynced`: the one extra sync a body owes on the tick it sleeps. */
|
|
1810
|
+
sleepSynced = /* @__PURE__ */ new Set();
|
|
1811
|
+
/**
|
|
1812
|
+
* See `PhysicsRuntime.sourceRecords`: the plain record object each body was built from. `add()`
|
|
1813
|
+
* always installs a new record object, so an id removed and re-added between two reconciles —
|
|
1814
|
+
* the pooled-projectile shape — is identifiable by identity alone, and its body is rebuilt so
|
|
1815
|
+
* the new row's spawn pose and velocity are what the world gets. Unknown means adopt, never
|
|
1816
|
+
* rebuild: a world restored from a hibernation blob has bodies with no recorded source.
|
|
1817
|
+
*/
|
|
1818
|
+
sourceRecords = /* @__PURE__ */ new Map();
|
|
1819
|
+
/** `collection.field` pairs already warned about a channel value the engine would not take. */
|
|
1820
|
+
warnedChannels = /* @__PURE__ */ new Set();
|
|
1821
|
+
constructor(core, rapier, options) {
|
|
1822
|
+
const config = core.definition.config.physics;
|
|
1823
|
+
if (!config) throw new Error("Rapier2dRuntime: the room config declares no physics");
|
|
1824
|
+
this.core = core;
|
|
1825
|
+
this.config = config;
|
|
1826
|
+
this.rapier = rapier;
|
|
1827
|
+
this.collections = core.ext.collections.filter(
|
|
1828
|
+
(c) => c.physics !== void 0
|
|
1829
|
+
);
|
|
1830
|
+
if (options.restore) {
|
|
1831
|
+
this.world = rapier.World.restoreSnapshot(options.restore.world);
|
|
1832
|
+
this.rebuilt = false;
|
|
1833
|
+
for (const [name, id, handle] of options.restore.bodies) {
|
|
1834
|
+
const body = this.world.getRigidBody(handle);
|
|
1835
|
+
if (body) this.bodies.set(bodyKey3(name, id), body);
|
|
1836
|
+
}
|
|
1837
|
+
} else {
|
|
1838
|
+
const g = config.gravity;
|
|
1839
|
+
this.world = new rapier.World({ x: g.x, y: g.y });
|
|
1840
|
+
this.rebuilt = true;
|
|
1841
|
+
}
|
|
1842
|
+
this.world.timestep = config.timestep ?? options.defaultTimestep;
|
|
1843
|
+
}
|
|
1844
|
+
get timestep() {
|
|
1845
|
+
return this.world.timestep;
|
|
1846
|
+
}
|
|
1847
|
+
/** Runs the room's `setup` — static geometry — on a world that was built rather than restored. */
|
|
1848
|
+
runSetup(room) {
|
|
1849
|
+
const setup = this.config.setup;
|
|
1850
|
+
if (!setup) return;
|
|
1851
|
+
this.core.guard("physics.setup", () => setup(this.world, this.rapier, room));
|
|
1852
|
+
}
|
|
1853
|
+
free() {
|
|
1854
|
+
this.bodies.clear();
|
|
1855
|
+
this.world.free();
|
|
1856
|
+
}
|
|
1857
|
+
// -------------------------------------------------------------------------
|
|
1858
|
+
// Bodies
|
|
1859
|
+
// -------------------------------------------------------------------------
|
|
1860
|
+
/** The body behind an instance, created on demand so a handler's own `add` is usable at once. */
|
|
1861
|
+
bodyFor(collection, id) {
|
|
1862
|
+
const existing = this.bodies.get(bodyKey3(collection, id));
|
|
1863
|
+
if (existing) return existing;
|
|
1864
|
+
const desc = this.collections.find((c) => c.name === collection);
|
|
1865
|
+
if (!desc) return void 0;
|
|
1866
|
+
const coll = plainEntity(this.core.plain, collection);
|
|
1867
|
+
const record = coll.get(id);
|
|
1868
|
+
if (record === void 0) return void 0;
|
|
1869
|
+
return this.create(desc, id, record);
|
|
1870
|
+
}
|
|
1871
|
+
create(desc, id, record) {
|
|
1872
|
+
const factory = this.config.bodies?.[desc.name];
|
|
1873
|
+
if (!factory) {
|
|
1874
|
+
this.core.log("error", `irtio: physics.bodies.${desc.name} is missing; no body created`);
|
|
1875
|
+
return void 0;
|
|
1876
|
+
}
|
|
1877
|
+
const spec = this.core.guard(
|
|
1878
|
+
`physics.bodies.${desc.name}`,
|
|
1879
|
+
() => factory(this.rapier, record, id)
|
|
1880
|
+
);
|
|
1881
|
+
if (!spec || !spec.body) {
|
|
1882
|
+
this.core.log(
|
|
1883
|
+
"error",
|
|
1884
|
+
`irtio: physics.bodies.${desc.name} returned no { body } for ${JSON.stringify(id)}`
|
|
1885
|
+
);
|
|
1886
|
+
return void 0;
|
|
1887
|
+
}
|
|
1888
|
+
const body = this.world.createRigidBody(spec.body);
|
|
1889
|
+
for (const collider of spec.colliders ?? []) this.world.createCollider(collider, body);
|
|
1890
|
+
this.warnLockedChannels(desc, spec, record);
|
|
1891
|
+
this.applyRecordToBody(desc, body, record);
|
|
1892
|
+
this.bodies.set(bodyKey3(desc.name, id), body);
|
|
1893
|
+
this.sourceRecords.set(bodyKey3(desc.name, id), record);
|
|
1894
|
+
return body;
|
|
1895
|
+
}
|
|
1896
|
+
/**
|
|
1897
|
+
* A spawn value the engine will not take, said once per field — the planar half of
|
|
1898
|
+
* `PhysicsRuntime.warnLockedChannels`.
|
|
1899
|
+
*
|
|
1900
|
+
* Rapier accepts `setLinvel` on an axis the body has disabled and then keeps zero, and it
|
|
1901
|
+
* accepts one on a fixed body and then ignores it: both read back as "the field was applied"
|
|
1902
|
+
* and behave as if it never was. The desc says which, before the body exists, so the check is on
|
|
1903
|
+
* the desc. In the plane there are three movable channels — `vx`, `vy` and the one rotation
|
|
1904
|
+
* `wz` — and the desc carries exactly those three flags. Named by field, because the field is
|
|
1905
|
+
* what the author wrote and what they will grep for.
|
|
1906
|
+
*/
|
|
1907
|
+
warnLockedChannels(desc, spec, record) {
|
|
1908
|
+
const physics = desc.physics;
|
|
1909
|
+
if (!physics) return;
|
|
1910
|
+
const b = spec.body;
|
|
1911
|
+
const frozen = b.status !== 0;
|
|
1912
|
+
const enabled = {
|
|
1913
|
+
vx: b.translationsEnabledX,
|
|
1914
|
+
vy: b.translationsEnabledY,
|
|
1915
|
+
wz: b.rotationsEnabled
|
|
1916
|
+
};
|
|
1917
|
+
for (const [channel, field] of physics.channels) {
|
|
1918
|
+
const allowed = enabled[channel];
|
|
1919
|
+
if (allowed === void 0) continue;
|
|
1920
|
+
const raw = record[field];
|
|
1921
|
+
if (typeof raw !== "number" || raw === 0) continue;
|
|
1922
|
+
if (allowed && !frozen) continue;
|
|
1923
|
+
const key = `${desc.name}.${field}`;
|
|
1924
|
+
if (this.warnedChannels.has(key)) continue;
|
|
1925
|
+
this.warnedChannels.add(key);
|
|
1926
|
+
this.core.log(
|
|
1927
|
+
"warn",
|
|
1928
|
+
`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"
|
|
1929
|
+
);
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
// ---- M6 lane F: rewind ----
|
|
1933
|
+
/**
|
|
1934
|
+
* D72: every tracked body, in the order `sync()` walks them (collection order, then instance
|
|
1935
|
+
* order). The rapier2d third of the same read-only accessor the other two runtimes carry.
|
|
1936
|
+
*/
|
|
1937
|
+
eachTrackedBody(fn) {
|
|
1938
|
+
for (const desc of this.collections) {
|
|
1939
|
+
const plainColl = plainEntity(this.core.plain, desc.name);
|
|
1940
|
+
for (const id of plainColl.ids()) {
|
|
1941
|
+
const body = this.bodies.get(bodyKey3(desc.name, id));
|
|
1942
|
+
if (body) fn(desc.name, id, body);
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
applyRecordToBody(desc, body, record) {
|
|
1947
|
+
const physics = desc.physics;
|
|
1948
|
+
if (!physics) return;
|
|
1949
|
+
const t = body.translation();
|
|
1950
|
+
const v = body.linvel();
|
|
1951
|
+
const angle = body.rotation();
|
|
1952
|
+
const target = {
|
|
1953
|
+
x: t.x,
|
|
1954
|
+
y: t.y,
|
|
1955
|
+
qz: Math.sin(angle / 2),
|
|
1956
|
+
qw: Math.cos(angle / 2),
|
|
1957
|
+
vx: v.x,
|
|
1958
|
+
vy: v.y,
|
|
1959
|
+
wz: body.angvel()
|
|
1960
|
+
};
|
|
1961
|
+
for (const [channel, field] of physics.channels) {
|
|
1962
|
+
const raw = record[field];
|
|
1963
|
+
if (typeof raw !== "number") continue;
|
|
1964
|
+
applyChannel2d2(channel, raw, target);
|
|
1965
|
+
}
|
|
1966
|
+
body.setTranslation({ x: target.x, y: target.y }, true);
|
|
1967
|
+
body.setRotation(angleFrom2d(target.qz, target.qw), true);
|
|
1968
|
+
body.setLinvel({ x: target.vx, y: target.vy }, true);
|
|
1969
|
+
body.setAngvel(target.wz, true);
|
|
1970
|
+
}
|
|
1971
|
+
/**
|
|
1972
|
+
* Creates bodies for new instances and destroys bodies whose instance is gone. Runs once per
|
|
1973
|
+
* tick, right before the step, in collection order then instance order.
|
|
1974
|
+
*/
|
|
1975
|
+
reconcile() {
|
|
1976
|
+
const live = /* @__PURE__ */ new Set();
|
|
1977
|
+
for (const desc of this.collections) {
|
|
1978
|
+
const coll = plainEntity(this.core.plain, desc.name);
|
|
1979
|
+
for (const id of coll.ids()) {
|
|
1980
|
+
const key = bodyKey3(desc.name, id);
|
|
1981
|
+
live.add(key);
|
|
1982
|
+
const record = coll.get(id);
|
|
1983
|
+
const existing = this.bodies.get(key);
|
|
1984
|
+
if (existing) {
|
|
1985
|
+
if (record === void 0) continue;
|
|
1986
|
+
const source = this.sourceRecords.get(key);
|
|
1987
|
+
if (source === void 0) {
|
|
1988
|
+
this.sourceRecords.set(key, record);
|
|
1989
|
+
continue;
|
|
1990
|
+
}
|
|
1991
|
+
if (source === record) continue;
|
|
1992
|
+
this.bodies.delete(key);
|
|
1993
|
+
this.sleepSynced.delete(key);
|
|
1994
|
+
this.sourceRecords.delete(key);
|
|
1995
|
+
this.world.removeRigidBody(existing);
|
|
1996
|
+
}
|
|
1997
|
+
if (record !== void 0) this.create(desc, id, record);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
for (const [key, body] of [...this.bodies]) {
|
|
2001
|
+
if (live.has(key)) continue;
|
|
2002
|
+
this.bodies.delete(key);
|
|
2003
|
+
this.sleepSynced.delete(key);
|
|
2004
|
+
this.sourceRecords.delete(key);
|
|
2005
|
+
this.world.removeRigidBody(body);
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
// -------------------------------------------------------------------------
|
|
2009
|
+
// Step and sync
|
|
2010
|
+
// -------------------------------------------------------------------------
|
|
2011
|
+
step() {
|
|
2012
|
+
this.world.step();
|
|
2013
|
+
if (!rapierHasStepped()) noteRapierStep();
|
|
2014
|
+
}
|
|
2015
|
+
/**
|
|
2016
|
+
* Body → schema, through the tracked proxies, so movement leaves the room as an ordinary delta.
|
|
2017
|
+
* Values are `Math.fround`ed for f32 fields, so room code reads exactly what the wire carries.
|
|
2018
|
+
*/
|
|
2019
|
+
sync() {
|
|
2020
|
+
for (const desc of this.collections) {
|
|
2021
|
+
const physics = desc.physics;
|
|
2022
|
+
if (!physics) continue;
|
|
2023
|
+
const tracked = this.core.anyState[desc.name];
|
|
2024
|
+
const plainColl = plainEntity(this.core.plain, desc.name);
|
|
2025
|
+
const rounders = roundersFor3(desc);
|
|
2026
|
+
for (const id of plainColl.ids()) {
|
|
2027
|
+
const key = bodyKey3(desc.name, id);
|
|
2028
|
+
const body = this.bodies.get(key);
|
|
2029
|
+
if (!body) continue;
|
|
2030
|
+
if (body.isSleeping()) {
|
|
2031
|
+
if (this.sleepSynced.has(key)) continue;
|
|
2032
|
+
this.sleepSynced.add(key);
|
|
2033
|
+
} else {
|
|
2034
|
+
this.sleepSynced.delete(key);
|
|
2035
|
+
}
|
|
2036
|
+
const record = tracked.get(id);
|
|
2037
|
+
if (!record) continue;
|
|
2038
|
+
const t = body.translation();
|
|
2039
|
+
const v = body.linvel();
|
|
2040
|
+
const state = {
|
|
2041
|
+
x: t.x,
|
|
2042
|
+
y: t.y,
|
|
2043
|
+
angle: body.rotation(),
|
|
2044
|
+
vx: v.x,
|
|
2045
|
+
vy: v.y,
|
|
2046
|
+
angularVelocity: body.angvel()
|
|
2047
|
+
};
|
|
2048
|
+
for (const [channel, field] of physics.channels) {
|
|
2049
|
+
const next = (rounders[field] ?? identity3)(channelOf2d2(channel, state));
|
|
2050
|
+
if (record[field] !== next) record[field] = next;
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
// -------------------------------------------------------------------------
|
|
2056
|
+
// Hibernation
|
|
2057
|
+
// -------------------------------------------------------------------------
|
|
2058
|
+
/**
|
|
2059
|
+
* The same `PhysicsSection` the 3D runtime writes — a real engine snapshot plus the
|
|
2060
|
+
* entity↔handle map, whose handles must ride as f64 for the reason spelled out on
|
|
2061
|
+
* {@link PhysicsSection}. What tells the two apart on the way back in is the envelope
|
|
2062
|
+
* `core/physics.ts` wraps this in, not anything in here.
|
|
2063
|
+
*/
|
|
2064
|
+
serialize() {
|
|
2065
|
+
const bodies = [];
|
|
2066
|
+
for (const [key, body] of this.bodies) {
|
|
2067
|
+
const sep = key.indexOf("\0");
|
|
2068
|
+
bodies.push([key.slice(0, sep), key.slice(sep + 1), body.handle]);
|
|
2069
|
+
}
|
|
2070
|
+
return { world: this.world.takeSnapshot(), bodies };
|
|
2071
|
+
}
|
|
2072
|
+
};
|
|
2073
|
+
function identity3(v) {
|
|
2074
|
+
return v;
|
|
2075
|
+
}
|
|
2076
|
+
function roundersFor3(desc) {
|
|
2077
|
+
const out = {};
|
|
2078
|
+
for (const f of desc.fields) {
|
|
2079
|
+
if (f.type.kind === "f32") out[f.name] = Math.fround;
|
|
2080
|
+
}
|
|
2081
|
+
return out;
|
|
2082
|
+
}
|
|
2083
|
+
|
|
1630
2084
|
// src/core/views.ts
|
|
1631
2085
|
import {
|
|
1632
2086
|
collectionDirty,
|
|
@@ -1869,8 +2323,8 @@ function catchUpDirty(ext, plain, fromRole, toRole) {
|
|
|
1869
2323
|
// src/core/snapshot.ts
|
|
1870
2324
|
import { withBuiltins } from "@irtio/protocol";
|
|
1871
2325
|
import { ByteReader as ByteReader3, ByteWriter as ByteWriter3, decodeSnapshot } from "@irtio/schema";
|
|
1872
|
-
var SNAPSHOT_FORMAT_VERSION =
|
|
1873
|
-
var READABLE_SNAPSHOT_VERSIONS = [1, 2];
|
|
2326
|
+
var SNAPSHOT_FORMAT_VERSION = 3;
|
|
2327
|
+
var READABLE_SNAPSHOT_VERSIONS = [1, 2, 3];
|
|
1874
2328
|
function parseHibernationBlob(bytes) {
|
|
1875
2329
|
const r = new ByteReader3(bytes);
|
|
1876
2330
|
const version = r.u8();
|
|
@@ -1881,22 +2335,31 @@ function parseHibernationBlob(bytes) {
|
|
|
1881
2335
|
const rngState = r.u32();
|
|
1882
2336
|
const tick = r.u32();
|
|
1883
2337
|
const mode = r.u8() === 1 ? "event" : "tick";
|
|
2338
|
+
const schemaHash8 = version >= 3 ? r.bytes(8).slice() : void 0;
|
|
1884
2339
|
let physics;
|
|
1885
2340
|
if (version >= 2) {
|
|
1886
2341
|
const section = r.blob();
|
|
1887
2342
|
if (section.length > 0) physics = section;
|
|
1888
2343
|
}
|
|
1889
|
-
return { version, seed, rngState, tick, mode, physics, snapshot: r.rest() };
|
|
2344
|
+
return { version, seed, rngState, tick, mode, schemaHash8, physics, snapshot: r.rest() };
|
|
2345
|
+
}
|
|
2346
|
+
function sameSchemaHash(a, b) {
|
|
2347
|
+
if (!a || !b) return true;
|
|
2348
|
+
if (a.length !== b.length) return false;
|
|
2349
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
2350
|
+
return true;
|
|
1890
2351
|
}
|
|
1891
2352
|
function writeHibernationBlob(header, snapshot, physics) {
|
|
1892
|
-
const
|
|
1893
|
-
const
|
|
2353
|
+
const hash = header.schemaHash8;
|
|
2354
|
+
const version = hash ? 3 : physics ? 2 : 1;
|
|
2355
|
+
const w = new ByteWriter3(snapshot.length + (physics?.length ?? 0) + 32);
|
|
1894
2356
|
w.u8(version);
|
|
1895
2357
|
w.u32(header.seed >>> 0);
|
|
1896
2358
|
w.u32(header.rngState >>> 0);
|
|
1897
2359
|
w.u32(header.tick >>> 0);
|
|
1898
2360
|
w.u8(header.mode === "event" ? 1 : 0);
|
|
1899
|
-
if (
|
|
2361
|
+
if (hash) w.bytes(hash);
|
|
2362
|
+
if (version >= 2) w.blob(physics ?? new Uint8Array(0));
|
|
1900
2363
|
w.bytes(snapshot);
|
|
1901
2364
|
return w.finish();
|
|
1902
2365
|
}
|
|
@@ -2038,9 +2501,10 @@ var PoseHistory = class {
|
|
|
2038
2501
|
entry.tick = tick;
|
|
2039
2502
|
entry.count = 0;
|
|
2040
2503
|
const pose = this.scratchPose;
|
|
2041
|
-
const
|
|
2504
|
+
const kind = physics.engineKind;
|
|
2042
2505
|
physics.eachTrackedBody((collection, id, body) => {
|
|
2043
|
-
if (
|
|
2506
|
+
if (kind === "rapier3d") readRapierPose(body, pose);
|
|
2507
|
+
else if (kind === "rapier2d") readRapier2dPose(body, pose);
|
|
2044
2508
|
else readMatterPose(body, pose);
|
|
2045
2509
|
this.push(entry, collection, id, pose);
|
|
2046
2510
|
});
|
|
@@ -2142,6 +2606,24 @@ function readRapierPose(body, into) {
|
|
|
2142
2606
|
into.wy = w.y;
|
|
2143
2607
|
into.wz = w.z;
|
|
2144
2608
|
}
|
|
2609
|
+
function readRapier2dPose(body, into) {
|
|
2610
|
+
const t = body.translation();
|
|
2611
|
+
const angle = body.rotation();
|
|
2612
|
+
const v = body.linvel();
|
|
2613
|
+
into.x = t.x;
|
|
2614
|
+
into.y = t.y;
|
|
2615
|
+
into.z = 0;
|
|
2616
|
+
into.qx = 0;
|
|
2617
|
+
into.qy = 0;
|
|
2618
|
+
into.qz = Math.sin(angle / 2);
|
|
2619
|
+
into.qw = Math.cos(angle / 2);
|
|
2620
|
+
into.vx = v.x;
|
|
2621
|
+
into.vy = v.y;
|
|
2622
|
+
into.vz = 0;
|
|
2623
|
+
into.wx = 0;
|
|
2624
|
+
into.wy = 0;
|
|
2625
|
+
into.wz = body.angvel();
|
|
2626
|
+
}
|
|
2145
2627
|
function readMatterPose(body, into) {
|
|
2146
2628
|
into.x = body.position.x;
|
|
2147
2629
|
into.y = body.position.y;
|
|
@@ -2346,6 +2828,95 @@ var RapierScratch = class extends BaseScratch {
|
|
|
2346
2828
|
this.world.free();
|
|
2347
2829
|
}
|
|
2348
2830
|
};
|
|
2831
|
+
var Rapier2dScratch = class extends BaseScratch {
|
|
2832
|
+
world;
|
|
2833
|
+
rapier;
|
|
2834
|
+
owners = /* @__PURE__ */ new Map();
|
|
2835
|
+
/** Live rigid-body handles the room tracks, so the statics pass knows what to skip. */
|
|
2836
|
+
trackedLive = /* @__PURE__ */ new Set();
|
|
2837
|
+
staticsCloned = false;
|
|
2838
|
+
constructor(physics, depth, tickNow) {
|
|
2839
|
+
super(physics, depth, tickNow);
|
|
2840
|
+
this.rapier = physics.rapier;
|
|
2841
|
+
this.world = new this.rapier.World({ x: 0, y: 0 });
|
|
2842
|
+
this.world.timestep = 0;
|
|
2843
|
+
}
|
|
2844
|
+
partsOf(body) {
|
|
2845
|
+
return body.numColliders();
|
|
2846
|
+
}
|
|
2847
|
+
clone(collection, id, body, parts) {
|
|
2848
|
+
const live = body;
|
|
2849
|
+
this.trackedLive.add(live.handle);
|
|
2850
|
+
const made = this.world.createRigidBody(this.rapier.RigidBodyDesc.fixed());
|
|
2851
|
+
const origin = live.translation();
|
|
2852
|
+
const angle = live.rotation();
|
|
2853
|
+
const cos = Math.cos(-angle);
|
|
2854
|
+
const sin = Math.sin(-angle);
|
|
2855
|
+
for (let i = 0; i < parts; i++) {
|
|
2856
|
+
const c = live.collider(i);
|
|
2857
|
+
const desc = new this.rapier.ColliderDesc(c.shape);
|
|
2858
|
+
desc.setSensor(c.isSensor());
|
|
2859
|
+
const w = c.translation();
|
|
2860
|
+
const dx = w.x - origin.x;
|
|
2861
|
+
const dy = w.y - origin.y;
|
|
2862
|
+
desc.setTranslation(dx * cos - dy * sin, dx * sin + dy * cos);
|
|
2863
|
+
desc.setRotation(c.rotation() - angle);
|
|
2864
|
+
this.world.createCollider(desc, made);
|
|
2865
|
+
}
|
|
2866
|
+
this.owners.set(made.handle, { collection, id });
|
|
2867
|
+
return { collection, id, body: made, parts, lastLive: -1 };
|
|
2868
|
+
}
|
|
2869
|
+
place(entry, pose) {
|
|
2870
|
+
const b = entry.body;
|
|
2871
|
+
if (!b.isEnabled()) b.setEnabled(true);
|
|
2872
|
+
b.setTranslation({ x: pose.x, y: pose.y }, false);
|
|
2873
|
+
b.setRotation(angleOf(pose.qz, pose.qw), false);
|
|
2874
|
+
}
|
|
2875
|
+
hide(entry) {
|
|
2876
|
+
if (entry.body.isEnabled()) entry.body.setEnabled(false);
|
|
2877
|
+
}
|
|
2878
|
+
destroy(entry) {
|
|
2879
|
+
this.owners.delete(entry.body.handle);
|
|
2880
|
+
this.world.removeRigidBody(entry.body);
|
|
2881
|
+
}
|
|
2882
|
+
build(tick, requested, clamped) {
|
|
2883
|
+
this.cloneStatics();
|
|
2884
|
+
this.world.step();
|
|
2885
|
+
const owners = this.owners;
|
|
2886
|
+
return {
|
|
2887
|
+
tick,
|
|
2888
|
+
requested,
|
|
2889
|
+
clamped,
|
|
2890
|
+
rapier2d: {
|
|
2891
|
+
world: this.world,
|
|
2892
|
+
who: (collider) => {
|
|
2893
|
+
const parent = collider.parent();
|
|
2894
|
+
return parent ? owners.get(parent.handle) : void 0;
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
};
|
|
2898
|
+
}
|
|
2899
|
+
cloneStatics() {
|
|
2900
|
+
if (this.staticsCloned) return;
|
|
2901
|
+
this.staticsCloned = true;
|
|
2902
|
+
const live = this.physics.world;
|
|
2903
|
+
if (!live) return;
|
|
2904
|
+
live.forEachCollider((c) => {
|
|
2905
|
+
const parent = c.parent();
|
|
2906
|
+
if (parent !== null && (!parent.isFixed() || this.trackedLive.has(parent.handle))) return;
|
|
2907
|
+
const desc = new this.rapier.ColliderDesc(c.shape);
|
|
2908
|
+
desc.setSensor(c.isSensor());
|
|
2909
|
+
const t = c.translation();
|
|
2910
|
+
desc.setTranslation(t.x, t.y);
|
|
2911
|
+
desc.setRotation(c.rotation());
|
|
2912
|
+
this.world.createCollider(desc);
|
|
2913
|
+
});
|
|
2914
|
+
}
|
|
2915
|
+
free() {
|
|
2916
|
+
this.owners.clear();
|
|
2917
|
+
this.world.free();
|
|
2918
|
+
}
|
|
2919
|
+
};
|
|
2349
2920
|
var MatterScratch = class extends BaseScratch {
|
|
2350
2921
|
matter;
|
|
2351
2922
|
owners = /* @__PURE__ */ new Map();
|
|
@@ -2395,9 +2966,9 @@ var MatterScratch = class extends BaseScratch {
|
|
|
2395
2966
|
}
|
|
2396
2967
|
build(tick, requested, clamped) {
|
|
2397
2968
|
const M = this.matter;
|
|
2398
|
-
const
|
|
2399
|
-
if (
|
|
2400
|
-
for (const b of M.Composite.allBodies(
|
|
2969
|
+
const engine4 = this.physics.matterEngine;
|
|
2970
|
+
if (engine4) {
|
|
2971
|
+
for (const b of M.Composite.allBodies(engine4.world)) {
|
|
2401
2972
|
if (b.isStatic) this.visible.push(b);
|
|
2402
2973
|
}
|
|
2403
2974
|
}
|
|
@@ -2440,7 +3011,7 @@ var RewindState = class {
|
|
|
2440
3011
|
`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
3012
|
);
|
|
2442
3013
|
}
|
|
2443
|
-
this.scratch ??= physics.engineKind === "rapier3d" ? new RapierScratch(physics, this.history.depth, tickNow) : new MatterScratch(physics, this.history.depth, tickNow);
|
|
3014
|
+
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);
|
|
2444
3015
|
const view = this.scratch.view(resolved.entry, resolved.tick, requested, resolved.clamped);
|
|
2445
3016
|
this.inside = true;
|
|
2446
3017
|
try {
|
|
@@ -2451,6 +3022,152 @@ var RewindState = class {
|
|
|
2451
3022
|
}
|
|
2452
3023
|
};
|
|
2453
3024
|
|
|
3025
|
+
// src/core/lobby.ts
|
|
3026
|
+
import {
|
|
3027
|
+
LOBBY_MEMBERS,
|
|
3028
|
+
LOBBY_MIN_PLAYERS,
|
|
3029
|
+
LOBBY_STATE,
|
|
3030
|
+
schemaHasLobby
|
|
3031
|
+
} from "@irtio/server";
|
|
3032
|
+
var runtimes = /* @__PURE__ */ new WeakMap();
|
|
3033
|
+
function runtimeOf(core) {
|
|
3034
|
+
let r = runtimes.get(core);
|
|
3035
|
+
if (!r) {
|
|
3036
|
+
r = { onStart: [], started: false };
|
|
3037
|
+
runtimes.set(core, r);
|
|
3038
|
+
}
|
|
3039
|
+
return r;
|
|
3040
|
+
}
|
|
3041
|
+
function lobbyConfigOf(core) {
|
|
3042
|
+
const declared = core.definition.config.lobby;
|
|
3043
|
+
if (declared === void 0) return void 0;
|
|
3044
|
+
if (!schemaHasLobby(core.definition.schema)) return void 0;
|
|
3045
|
+
return declared;
|
|
3046
|
+
}
|
|
3047
|
+
function stateOf3(core) {
|
|
3048
|
+
return core.anyState[LOBBY_STATE];
|
|
3049
|
+
}
|
|
3050
|
+
function evaluateLobby(core) {
|
|
3051
|
+
const config = lobbyConfigOf(core);
|
|
3052
|
+
if (config === void 0) return;
|
|
3053
|
+
const state = stateOf3(core);
|
|
3054
|
+
if (state === void 0) return;
|
|
3055
|
+
const members = trackedEntity(core, LOBBY_MEMBERS);
|
|
3056
|
+
const policy = config.start ?? "when-full";
|
|
3057
|
+
const present = [];
|
|
3058
|
+
for (const [clientId, entry] of core.clients) {
|
|
3059
|
+
if (!entry.connected) continue;
|
|
3060
|
+
present.push(clientId);
|
|
3061
|
+
if (!members.has(clientId)) members.add(clientId, { ready: false }, { owner: clientId });
|
|
3062
|
+
else if (members.ownerOf(clientId) !== clientId) members.setOwner(clientId, clientId);
|
|
3063
|
+
}
|
|
3064
|
+
const here = new Set(present);
|
|
3065
|
+
for (const id of [...members.ids()]) {
|
|
3066
|
+
if (!here.has(id)) members.remove(id);
|
|
3067
|
+
}
|
|
3068
|
+
const readyCount = policy === "when-ready" ? present.filter((id) => members.get(id)?.ready === true).length : 0;
|
|
3069
|
+
const capacity = Math.min(
|
|
3070
|
+
255,
|
|
3071
|
+
config.size ?? core.definition.config.maxClients,
|
|
3072
|
+
core.definition.config.maxClients
|
|
3073
|
+
);
|
|
3074
|
+
const min = Math.max(LOBBY_MIN_PLAYERS, config.min ?? LOBBY_MIN_PLAYERS);
|
|
3075
|
+
if (state.readyUi !== (policy === "when-ready")) state.readyUi = policy === "when-ready";
|
|
3076
|
+
if (state.present !== present.length) state.present = Math.min(255, present.length);
|
|
3077
|
+
if (state.ready !== readyCount) state.ready = readyCount;
|
|
3078
|
+
if (state.capacity !== capacity) state.capacity = capacity;
|
|
3079
|
+
if (state.phase !== "lobby") return;
|
|
3080
|
+
if (policy === "when-full") {
|
|
3081
|
+
if (present.length >= capacity && capacity >= LOBBY_MIN_PLAYERS) startLobby(core, "when-full");
|
|
3082
|
+
return;
|
|
3083
|
+
}
|
|
3084
|
+
if (policy === "when-ready") {
|
|
3085
|
+
if (present.length >= min && readyCount === present.length) startLobby(core, "when-ready");
|
|
3086
|
+
return;
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
function startLobby(core, why) {
|
|
3090
|
+
const runtime = runtimeOf(core);
|
|
3091
|
+
if (runtime.started) return;
|
|
3092
|
+
const state = stateOf3(core);
|
|
3093
|
+
if (state === void 0 || state.phase === "started") {
|
|
3094
|
+
runtime.started = true;
|
|
3095
|
+
return;
|
|
3096
|
+
}
|
|
3097
|
+
runtime.started = true;
|
|
3098
|
+
state.phase = "started";
|
|
3099
|
+
state.public = false;
|
|
3100
|
+
core.host.setLobby({ started: true });
|
|
3101
|
+
core.log("info", `lobby: the game started (${why})`);
|
|
3102
|
+
const declared = lobbyConfigOf(core)?.onStart;
|
|
3103
|
+
if (declared) {
|
|
3104
|
+
core.guard("lobby.onStart", () => declared(core.anyState, core.room));
|
|
3105
|
+
}
|
|
3106
|
+
for (const cb of [...runtime.onStart]) core.guard("lobby.onStart(cb)", cb);
|
|
3107
|
+
}
|
|
3108
|
+
function makeLobby(core) {
|
|
3109
|
+
const config = lobbyConfigOf(core);
|
|
3110
|
+
if (config === void 0) {
|
|
3111
|
+
throw new Error(
|
|
3112
|
+
"room.lobby: this room has no lobby. Spread lobbyCollections into the schema and declare lobby: {} in defineRoom \u2014 both are needed, and defineRoom refuses either one alone"
|
|
3113
|
+
);
|
|
3114
|
+
}
|
|
3115
|
+
const state = stateOf3(core);
|
|
3116
|
+
const members = trackedEntity(core, LOBBY_MEMBERS);
|
|
3117
|
+
return {
|
|
3118
|
+
get phase() {
|
|
3119
|
+
return state.phase ?? "lobby";
|
|
3120
|
+
},
|
|
3121
|
+
get ready() {
|
|
3122
|
+
const out = [];
|
|
3123
|
+
for (const [clientId, entry] of core.clients) {
|
|
3124
|
+
if (!entry.connected) continue;
|
|
3125
|
+
out.push({
|
|
3126
|
+
clientId,
|
|
3127
|
+
ready: members.get(clientId)?.ready === true
|
|
3128
|
+
});
|
|
3129
|
+
}
|
|
3130
|
+
return out;
|
|
3131
|
+
},
|
|
3132
|
+
get public() {
|
|
3133
|
+
return state.public === true;
|
|
3134
|
+
},
|
|
3135
|
+
start() {
|
|
3136
|
+
startLobby(core, "room.lobby.start()");
|
|
3137
|
+
},
|
|
3138
|
+
onStart(cb) {
|
|
3139
|
+
if (typeof cb !== "function") {
|
|
3140
|
+
core.log("warn", "room.lobby.onStart: expects a function; ignored");
|
|
3141
|
+
return;
|
|
3142
|
+
}
|
|
3143
|
+
runtimeOf(core).onStart.push(cb);
|
|
3144
|
+
},
|
|
3145
|
+
setPublic(value) {
|
|
3146
|
+
setLobbyPublic(core, value === true);
|
|
3147
|
+
}
|
|
3148
|
+
};
|
|
3149
|
+
}
|
|
3150
|
+
function setLobbyPublic(core, value) {
|
|
3151
|
+
const config = lobbyConfigOf(core);
|
|
3152
|
+
if (config === void 0) return;
|
|
3153
|
+
const state = stateOf3(core);
|
|
3154
|
+
if (state === void 0) return;
|
|
3155
|
+
if (state.phase === "started") {
|
|
3156
|
+
core.log(
|
|
3157
|
+
"warn",
|
|
3158
|
+
"room.lobby.setPublic: the game has started; the room stays out of the registry"
|
|
3159
|
+
);
|
|
3160
|
+
return;
|
|
3161
|
+
}
|
|
3162
|
+
const veto = config.onSetPublic;
|
|
3163
|
+
if (veto) {
|
|
3164
|
+
const outcome = core.tryRun("lobby.onSetPublic", () => veto(core.anyState, value));
|
|
3165
|
+
if (!outcome.ok || outcome.value === false) return;
|
|
3166
|
+
}
|
|
3167
|
+
if (state.public !== value) state.public = value;
|
|
3168
|
+
core.host.setLobby({ public: value, queue: config.queue ?? "default" });
|
|
3169
|
+
}
|
|
3170
|
+
|
|
2454
3171
|
// src/core/messages.ts
|
|
2455
3172
|
import { FrameType as FrameType3, decodeMsg, encodeFrame as encodeFrame2, encodeMsg } from "@irtio/protocol";
|
|
2456
3173
|
import { decodeFields as decodeFields2 } from "@irtio/schema";
|
|
@@ -2601,6 +3318,22 @@ function makeMatterApi(p) {
|
|
|
2601
3318
|
}
|
|
2602
3319
|
};
|
|
2603
3320
|
}
|
|
3321
|
+
function makeRapier2dApi(p) {
|
|
3322
|
+
return {
|
|
3323
|
+
get rapier() {
|
|
3324
|
+
return p.rapier;
|
|
3325
|
+
},
|
|
3326
|
+
get world() {
|
|
3327
|
+
return p.world;
|
|
3328
|
+
},
|
|
3329
|
+
get timestep() {
|
|
3330
|
+
return p.timestep;
|
|
3331
|
+
},
|
|
3332
|
+
body(collection, id) {
|
|
3333
|
+
return p.bodyFor(collection, id);
|
|
3334
|
+
}
|
|
3335
|
+
};
|
|
3336
|
+
}
|
|
2604
3337
|
function makeKv(core) {
|
|
2605
3338
|
return {
|
|
2606
3339
|
get(playerId, key) {
|
|
@@ -2716,6 +3449,7 @@ function createRoomApi(core, publicUrl) {
|
|
|
2716
3449
|
let lastNow = Number.NEGATIVE_INFINITY;
|
|
2717
3450
|
let physicsApi;
|
|
2718
3451
|
let matterApi;
|
|
3452
|
+
let rapier2dApi;
|
|
2719
3453
|
let kvApi;
|
|
2720
3454
|
let busApi;
|
|
2721
3455
|
let leaderboardApi;
|
|
@@ -2738,9 +3472,20 @@ function createRoomApi(core, publicUrl) {
|
|
|
2738
3472
|
get id() {
|
|
2739
3473
|
return core.roomId;
|
|
2740
3474
|
},
|
|
3475
|
+
/**
|
|
3476
|
+
* The same tracked state object handlers get. The one caller that has no other way to it is
|
|
3477
|
+
* `physics.setup`, which runs before `onCreate` on a fresh room and after the snapshot is
|
|
3478
|
+
* decoded on a rebuild — see the doc on `Room.state`.
|
|
3479
|
+
*/
|
|
3480
|
+
get state() {
|
|
3481
|
+
return core.anyState;
|
|
3482
|
+
},
|
|
2741
3483
|
get link() {
|
|
2742
3484
|
return link();
|
|
2743
3485
|
},
|
|
3486
|
+
get seed() {
|
|
3487
|
+
return core.seed;
|
|
3488
|
+
},
|
|
2744
3489
|
get tick() {
|
|
2745
3490
|
return core.tick;
|
|
2746
3491
|
},
|
|
@@ -2777,7 +3522,7 @@ function createRoomApi(core, publicUrl) {
|
|
|
2777
3522
|
}
|
|
2778
3523
|
if (p.engineKind !== "rapier3d") {
|
|
2779
3524
|
throw new Error(
|
|
2780
|
-
"room.physics: this room runs matter2d. Read room.physics2d instead (it has .matter and .engine, where this one has .rapier and .world)"
|
|
3525
|
+
p.engineKind === "matter2d" ? "room.physics: this room runs matter2d. Read room.physics2d instead (it has .matter and .engine, where this one has .rapier and .world)" : "room.physics: this room runs rapier2d. Read room.physicsRapier2d instead (same .rapier and .world, but the 2D module: a Vector2 world, not a Vector3 one)"
|
|
2781
3526
|
);
|
|
2782
3527
|
}
|
|
2783
3528
|
return physicsApi ?? (physicsApi = makePhysicsApi(p));
|
|
@@ -2791,11 +3536,25 @@ function createRoomApi(core, publicUrl) {
|
|
|
2791
3536
|
}
|
|
2792
3537
|
if (p.engineKind !== "matter2d") {
|
|
2793
3538
|
throw new Error(
|
|
2794
|
-
"room.physics2d: this room runs rapier3d. Read room.physics instead (it has .rapier and .world, where this one has .matter and .engine)"
|
|
3539
|
+
p.engineKind === "rapier2d" ? "room.physics2d: this room runs rapier2d, not matter2d. Read room.physicsRapier2d instead (it has .rapier and .world, where this one has .matter and .engine)" : "room.physics2d: this room runs rapier3d. Read room.physics instead (it has .rapier and .world, where this one has .matter and .engine)"
|
|
2795
3540
|
);
|
|
2796
3541
|
}
|
|
2797
3542
|
return matterApi ?? (matterApi = makeMatterApi(p));
|
|
2798
3543
|
},
|
|
3544
|
+
get physicsRapier2d() {
|
|
3545
|
+
const p = core.physics;
|
|
3546
|
+
if (!p) {
|
|
3547
|
+
throw new Error(
|
|
3548
|
+
"room.physicsRapier2d: this room has no physics \u2014 add physics: { engine: 'rapier2d', gravity, bodies } to defineRoom(...)"
|
|
3549
|
+
);
|
|
3550
|
+
}
|
|
3551
|
+
if (p.engineKind !== "rapier2d") {
|
|
3552
|
+
throw new Error(
|
|
3553
|
+
p.engineKind === "matter2d" ? "room.physicsRapier2d: this room runs matter2d. Read room.physics2d instead (it has .matter and .engine, where this one has .rapier and .world)" : "room.physicsRapier2d: this room runs rapier3d. Read room.physics instead (same .rapier and .world, but the 3D module: a Vector3 world, not a Vector2 one)"
|
|
3554
|
+
);
|
|
3555
|
+
}
|
|
3556
|
+
return rapier2dApi ?? (rapier2dApi = makeRapier2dApi(p));
|
|
3557
|
+
},
|
|
2799
3558
|
// ---- M6 lane F: rewind ----
|
|
2800
3559
|
// D72: one line, and deliberately only one. Everything the rewind does — the buffer, the
|
|
2801
3560
|
// clamping, the scratch worlds, the reentrancy refusal — lives in `core/history.ts` behind
|
|
@@ -2872,6 +3631,10 @@ function createRoomApi(core, publicUrl) {
|
|
|
2872
3631
|
get bus() {
|
|
2873
3632
|
return busApi ?? (busApi = makeBus(core));
|
|
2874
3633
|
},
|
|
3634
|
+
// ---- M6 lane H: lobby front door ----
|
|
3635
|
+
get lobby() {
|
|
3636
|
+
return makeLobby(core);
|
|
3637
|
+
},
|
|
2875
3638
|
alarm(name, atMs) {
|
|
2876
3639
|
if (!isAlarmName(core, name, "room.alarm")) return;
|
|
2877
3640
|
if (!Number.isFinite(atMs)) {
|
|
@@ -2913,6 +3676,9 @@ function createRoomApi(core, publicUrl) {
|
|
|
2913
3676
|
},
|
|
2914
3677
|
get broadcast() {
|
|
2915
3678
|
return broadcast;
|
|
3679
|
+
},
|
|
3680
|
+
broadcastExcept(clientId) {
|
|
3681
|
+
return createBroadcastExceptProxy(core, clientId);
|
|
2916
3682
|
}
|
|
2917
3683
|
};
|
|
2918
3684
|
const broadcast = createBroadcastProxy(core);
|
|
@@ -3101,8 +3867,13 @@ var RoomCore = class _RoomCore {
|
|
|
3101
3867
|
}
|
|
3102
3868
|
let restored;
|
|
3103
3869
|
let plain;
|
|
3104
|
-
|
|
3105
|
-
|
|
3870
|
+
const candidate = options.restoreFrom ? parseHibernationBlob(options.restoreFrom) : void 0;
|
|
3871
|
+
if (candidate && !sameSchemaHash(candidate.schemaHash8, this.ext.hash8)) {
|
|
3872
|
+
host.log("warn", [`irtio: room ${options.roomId}: schema changed, snapshot discarded`]);
|
|
3873
|
+
} else if (candidate) {
|
|
3874
|
+
restored = candidate;
|
|
3875
|
+
}
|
|
3876
|
+
if (restored) {
|
|
3106
3877
|
plain = decodeSnapshot2(this.ext, restored.snapshot).state;
|
|
3107
3878
|
const presence = plainEntity(plain, PRESENCE_COLLECTION);
|
|
3108
3879
|
for (const id of [...presence.ids()]) presence.remove(id);
|
|
@@ -3117,7 +3888,7 @@ var RoomCore = class _RoomCore {
|
|
|
3117
3888
|
this.plain = plain;
|
|
3118
3889
|
this.tracked = track(this.ext, plain);
|
|
3119
3890
|
this.anyState = this.tracked.state;
|
|
3120
|
-
this.seed = restored?.seed ?? options.seed ??
|
|
3891
|
+
this.seed = restored?.seed ?? options.seed ?? seedFromRoomId(this.roomId);
|
|
3121
3892
|
this.rng = new Mulberry32(restored?.rngState ?? this.seed);
|
|
3122
3893
|
this.tick = restored?.tick ?? 0;
|
|
3123
3894
|
const self = this;
|
|
@@ -3209,14 +3980,24 @@ var RoomCore = class _RoomCore {
|
|
|
3209
3980
|
const config = this.definition.config.physics;
|
|
3210
3981
|
if (!config) return void 0;
|
|
3211
3982
|
if (config.engine === "matter2d") return this.buildMatter(config, restored);
|
|
3212
|
-
|
|
3213
|
-
|
|
3983
|
+
if (config.engine === "rapier2d") return this.buildRapier2d(restored);
|
|
3984
|
+
const engine4 = loadedPhysics();
|
|
3985
|
+
if (!engine4) {
|
|
3214
3986
|
throw new Error(
|
|
3215
3987
|
"RoomCore: this room declares physics but the engine is not initialized \u2014 the host must `await initPhysics()` (from '@irtio/runtime') before constructing the room; handlers are synchronous, so the WASM cannot be loaded later"
|
|
3216
3988
|
);
|
|
3217
3989
|
}
|
|
3218
|
-
|
|
3219
|
-
|
|
3990
|
+
let section;
|
|
3991
|
+
if (restored?.physics) {
|
|
3992
|
+
const wrote = physicsSectionEngine(restored.physics);
|
|
3993
|
+
if (wrote === "rapier3d") section = decodePhysicsSection(restored.physics);
|
|
3994
|
+
else {
|
|
3995
|
+
this.host.log("warn", [
|
|
3996
|
+
`irtio: this snapshot carries a ${wrote} world and the room now runs rapier3d. The world is rebuilt from schema state and physics.setup runs again.`
|
|
3997
|
+
]);
|
|
3998
|
+
}
|
|
3999
|
+
}
|
|
4000
|
+
const physics = new PhysicsRuntime(this.internals, engine4, {
|
|
3220
4001
|
...section ? { restore: section } : {},
|
|
3221
4002
|
defaultTimestep: 1 / this.definition.config.tickRate
|
|
3222
4003
|
});
|
|
@@ -3247,11 +4028,12 @@ var RoomCore = class _RoomCore {
|
|
|
3247
4028
|
}
|
|
3248
4029
|
let section;
|
|
3249
4030
|
if (restored?.physics) {
|
|
3250
|
-
|
|
4031
|
+
const wrote = physicsSectionEngine(restored.physics);
|
|
4032
|
+
if (wrote === "matter2d") {
|
|
3251
4033
|
section = decodeMatterBodies(decodeMatterSectionEnvelope(restored.physics));
|
|
3252
4034
|
} else {
|
|
3253
4035
|
this.host.log("warn", [
|
|
3254
|
-
|
|
4036
|
+
`irtio: this snapshot carries a ${wrote} world and the room now runs matter2d. The world is rebuilt from schema state and physics.setup runs again.`
|
|
3255
4037
|
]);
|
|
3256
4038
|
}
|
|
3257
4039
|
}
|
|
@@ -3268,6 +4050,44 @@ var RoomCore = class _RoomCore {
|
|
|
3268
4050
|
physics.reconcile();
|
|
3269
4051
|
return physics;
|
|
3270
4052
|
}
|
|
4053
|
+
/**
|
|
4054
|
+
* The rapier2d half. It follows the *rapier3d* shape rather than matter2d's, because the 2D
|
|
4055
|
+
* build has a real `takeSnapshot()`: a restored world comes back whole and `setup` does not run
|
|
4056
|
+
* again. The only thing that differs from `buildPhysics` is which engine and which envelope.
|
|
4057
|
+
*/
|
|
4058
|
+
buildRapier2d(restored) {
|
|
4059
|
+
const engine4 = loadedRapier2d();
|
|
4060
|
+
if (!engine4) {
|
|
4061
|
+
throw new Error(
|
|
4062
|
+
"RoomCore: this room declares rapier2d physics but the engine is not initialized \u2014 the host must `await initRapier2d()` (from '@irtio/runtime') before constructing the room; handlers are synchronous, so the WASM cannot be loaded later"
|
|
4063
|
+
);
|
|
4064
|
+
}
|
|
4065
|
+
let section;
|
|
4066
|
+
if (restored?.physics) {
|
|
4067
|
+
const wrote = physicsSectionEngine(restored.physics);
|
|
4068
|
+
if (wrote === "rapier2d") {
|
|
4069
|
+
section = decodePhysicsSection(decodeRapier2dSectionEnvelope(restored.physics));
|
|
4070
|
+
} else {
|
|
4071
|
+
this.host.log("warn", [
|
|
4072
|
+
`irtio: this snapshot carries a ${wrote} world and the room now runs rapier2d. The world is rebuilt from schema state and physics.setup runs again.`
|
|
4073
|
+
]);
|
|
4074
|
+
}
|
|
4075
|
+
}
|
|
4076
|
+
const physics = new Rapier2dRuntime(this.internals, engine4, {
|
|
4077
|
+
...section ? { restore: section } : {},
|
|
4078
|
+
defaultTimestep: 1 / this.definition.config.tickRate
|
|
4079
|
+
});
|
|
4080
|
+
if (physics.rebuilt) {
|
|
4081
|
+
if (restored) {
|
|
4082
|
+
this.host.log("info", [
|
|
4083
|
+
`irtio: no rapier2d world in this snapshot (format v${restored.version}) \u2014 rebuilding it from schema state and re-running physics.setup; contact state is not restored`
|
|
4084
|
+
]);
|
|
4085
|
+
}
|
|
4086
|
+
physics.runSetup(this.room);
|
|
4087
|
+
physics.reconcile();
|
|
4088
|
+
}
|
|
4089
|
+
return physics;
|
|
4090
|
+
}
|
|
3271
4091
|
/** Convenience for hosts: `RoomCore.restore(def, bytes, host, opts)`. */
|
|
3272
4092
|
static restore(definition, bytes, host, options) {
|
|
3273
4093
|
return new _RoomCore(definition, host, { ...options, restoreFrom: bytes });
|
|
@@ -3412,7 +4232,13 @@ var RoomCore = class _RoomCore {
|
|
|
3412
4232
|
snapshot() {
|
|
3413
4233
|
const physics = this.physics ? encodeSection(this.physics) : void 0;
|
|
3414
4234
|
return writeHibernationBlob(
|
|
3415
|
-
{
|
|
4235
|
+
{
|
|
4236
|
+
seed: this.seed,
|
|
4237
|
+
rngState: this.rng.state,
|
|
4238
|
+
tick: this.tick,
|
|
4239
|
+
mode: this.mode,
|
|
4240
|
+
schemaHash8: this.ext.hash8
|
|
4241
|
+
},
|
|
3416
4242
|
encodeSnapshot2(this.ext, this.plain, { tick: this.tick }),
|
|
3417
4243
|
physics
|
|
3418
4244
|
);
|
|
@@ -3756,6 +4582,7 @@ var RoomCore = class _RoomCore {
|
|
|
3756
4582
|
* its view's `DELTA` — encoded once per distinct view.
|
|
3757
4583
|
*/
|
|
3758
4584
|
flush() {
|
|
4585
|
+
evaluateLobby(this.internals);
|
|
3759
4586
|
const dirty = this.tracked.flush();
|
|
3760
4587
|
serverWinsCorrections(this.internals, dirty);
|
|
3761
4588
|
this.ledger?.newFlush();
|
|
@@ -3885,7 +4712,13 @@ var RoomCore = class _RoomCore {
|
|
|
3885
4712
|
}
|
|
3886
4713
|
};
|
|
3887
4714
|
function encodeSection(physics) {
|
|
3888
|
-
|
|
4715
|
+
if (physics.engineKind === "matter2d") {
|
|
4716
|
+
return encodeMatterSectionEnvelope(encodeMatterBodies(physics.serialize()));
|
|
4717
|
+
}
|
|
4718
|
+
if (physics.engineKind === "rapier2d") {
|
|
4719
|
+
return encodeRapier2dSectionEnvelope(encodePhysicsSection(physics.serialize()));
|
|
4720
|
+
}
|
|
4721
|
+
return encodePhysicsSection(physics.serialize());
|
|
3889
4722
|
}
|
|
3890
4723
|
|
|
3891
4724
|
export {
|
|
@@ -3908,10 +4741,15 @@ export {
|
|
|
3908
4741
|
onFirstRapierStep,
|
|
3909
4742
|
encodePhysicsSection,
|
|
3910
4743
|
physicsSectionEngine,
|
|
4744
|
+
encodeRapier2dSectionEnvelope,
|
|
4745
|
+
decodeRapier2dSectionEnvelope,
|
|
3911
4746
|
encodeMatterSectionEnvelope,
|
|
3912
4747
|
decodeMatterSectionEnvelope,
|
|
3913
4748
|
decodePhysicsSection,
|
|
3914
4749
|
Mulberry32,
|
|
4750
|
+
initRapier2d,
|
|
4751
|
+
loadedRapier2d,
|
|
4752
|
+
resetRapier2dForTests,
|
|
3915
4753
|
isVisible,
|
|
3916
4754
|
visibleTo,
|
|
3917
4755
|
visibleNames,
|