@irtio/runtime 0.6.0 → 0.8.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-VMCE3LRO.js → chunk-DBGMG2S3.js} +1 -1
- package/dist/{chunk-K42HA75G.js → chunk-ZCD2TBXB.js} +1253 -26
- package/dist/{contract-BjMsoJIV.d.ts → contract-CRilLVdx.d.ts} +29 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +12 -2
- package/dist/{room-CfnEjlcg.d.ts → room-YJGz6Caw.d.ts} +179 -10
- package/dist/test/index.d.ts +14 -3
- package/dist/test/index.js +11 -3
- package/dist/worker/index.d.ts +9 -1
- package/dist/worker/index.js +14 -3
- package/package.json +5 -4
|
@@ -143,10 +143,10 @@ function handleCall(core, clientId, payload) {
|
|
|
143
143
|
return true;
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
|
-
const ctx = core.ctxFor(clientId);
|
|
146
|
+
const ctx = { ...core.ctxFor(clientId), clientTick: call.clientTick || void 0 };
|
|
147
147
|
let result;
|
|
148
148
|
if (desc.name === REQUEST_OWNERSHIP) {
|
|
149
|
-
result = runRequestOwnership(core, clientId, params);
|
|
149
|
+
result = runRequestOwnership(core, clientId, ctx, params);
|
|
150
150
|
} else {
|
|
151
151
|
const impl = core.definition.config.rpc?.[desc.name];
|
|
152
152
|
if (!impl) {
|
|
@@ -175,7 +175,7 @@ function handleCall(core, clientId, payload) {
|
|
|
175
175
|
}
|
|
176
176
|
return true;
|
|
177
177
|
}
|
|
178
|
-
function runRequestOwnership(core, clientId, params) {
|
|
178
|
+
function runRequestOwnership(core, clientId, ctx, params) {
|
|
179
179
|
const name = String(params.entity ?? "");
|
|
180
180
|
const id = String(params.id ?? "");
|
|
181
181
|
const c = collectionDescOf(core.ext, name);
|
|
@@ -185,10 +185,7 @@ function runRequestOwnership(core, clientId, params) {
|
|
|
185
185
|
const handler = core.definition.config.onOwnershipRequest;
|
|
186
186
|
const tracked = trackedEntity(core, name);
|
|
187
187
|
if (handler) {
|
|
188
|
-
const ran = core.tryRun(
|
|
189
|
-
"onOwnershipRequest",
|
|
190
|
-
() => handler(core.anyState, name, id, core.ctxFor(clientId))
|
|
191
|
-
);
|
|
188
|
+
const ran = core.tryRun("onOwnershipRequest", () => handler(core.anyState, name, id, ctx));
|
|
192
189
|
if (ran.ok && ran.value === true && plain.ownerOf(id) !== clientId) {
|
|
193
190
|
tracked.setOwner(id, clientId);
|
|
194
191
|
}
|
|
@@ -762,6 +759,7 @@ var Loop = class {
|
|
|
762
759
|
physics.reconcile();
|
|
763
760
|
physics.step();
|
|
764
761
|
physics.sync();
|
|
762
|
+
this.core.captureHistory();
|
|
765
763
|
});
|
|
766
764
|
if (!ran.ok) {
|
|
767
765
|
failed = failed === void 0 ? "the physics step" : `${failed} and the physics step`;
|
|
@@ -1054,6 +1052,20 @@ var MatterRuntime = class {
|
|
|
1054
1052
|
}
|
|
1055
1053
|
return spec.body;
|
|
1056
1054
|
}
|
|
1055
|
+
// ---- M6 lane F: rewind ----
|
|
1056
|
+
/**
|
|
1057
|
+
* D72: every tracked body, in the order `sync()` walks them. The matter half of the same
|
|
1058
|
+
* read-only accessor `PhysicsRuntime` carries; see its comment for what calls it and when.
|
|
1059
|
+
*/
|
|
1060
|
+
eachTrackedBody(fn) {
|
|
1061
|
+
for (const desc of this.collections) {
|
|
1062
|
+
const plainColl = plainEntity(this.core.plain, desc.name);
|
|
1063
|
+
for (const id of plainColl.ids()) {
|
|
1064
|
+
const attached = this.bodies.get(bodyKey(desc.name, id));
|
|
1065
|
+
if (attached) fn(desc.name, id, attached.body);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1057
1069
|
applyState(body, s) {
|
|
1058
1070
|
const M = this.matter;
|
|
1059
1071
|
M.Body.setPosition(body, { x: s.x, y: s.y });
|
|
@@ -1243,9 +1255,28 @@ function encodePhysicsSection(section) {
|
|
|
1243
1255
|
return w.finish();
|
|
1244
1256
|
}
|
|
1245
1257
|
var MATTER_TAG = 1;
|
|
1258
|
+
var RAPIER2D_TAG = 2;
|
|
1246
1259
|
function physicsSectionEngine(bytes) {
|
|
1247
1260
|
const r = new ByteReader2(bytes);
|
|
1248
|
-
|
|
1261
|
+
if (r.varint() !== 0) return "rapier3d";
|
|
1262
|
+
const tag = r.u8();
|
|
1263
|
+
if (tag === MATTER_TAG) return "matter2d";
|
|
1264
|
+
if (tag === RAPIER2D_TAG) return "rapier2d";
|
|
1265
|
+
return "rapier3d";
|
|
1266
|
+
}
|
|
1267
|
+
function encodeRapier2dSectionEnvelope(payload) {
|
|
1268
|
+
const w = new ByteWriter2(payload.length + 8);
|
|
1269
|
+
w.varint(0);
|
|
1270
|
+
w.u8(RAPIER2D_TAG);
|
|
1271
|
+
w.bytes(payload);
|
|
1272
|
+
return w.finish();
|
|
1273
|
+
}
|
|
1274
|
+
function decodeRapier2dSectionEnvelope(bytes) {
|
|
1275
|
+
const r = new ByteReader2(bytes);
|
|
1276
|
+
if (r.varint() !== 0 || r.u8() !== RAPIER2D_TAG) {
|
|
1277
|
+
throw new Error("irtio: this physics section was not written by rapier2d");
|
|
1278
|
+
}
|
|
1279
|
+
return r.rest();
|
|
1249
1280
|
}
|
|
1250
1281
|
function encodeMatterSectionEnvelope(payload) {
|
|
1251
1282
|
const w = new ByteWriter2(payload.length + 8);
|
|
@@ -1396,6 +1427,22 @@ var PhysicsRuntime = class {
|
|
|
1396
1427
|
this.bodies.set(bodyKey2(desc.name, id), body);
|
|
1397
1428
|
return body;
|
|
1398
1429
|
}
|
|
1430
|
+
// ---- M6 lane F: rewind ----
|
|
1431
|
+
/**
|
|
1432
|
+
* D72: every tracked body, in the order `sync()` walks them (collection order, then instance
|
|
1433
|
+
* order). Read-only: it hands out the live bodies and nothing else, and the map stays private.
|
|
1434
|
+
* The pose history calls this once per tick and the rewind scratch calls it per rewind; a room
|
|
1435
|
+
* that declares no `physics.history` never calls it at all.
|
|
1436
|
+
*/
|
|
1437
|
+
eachTrackedBody(fn) {
|
|
1438
|
+
for (const desc of this.collections) {
|
|
1439
|
+
const plainColl = plainEntity(this.core.plain, desc.name);
|
|
1440
|
+
for (const id of plainColl.ids()) {
|
|
1441
|
+
const body = this.bodies.get(bodyKey2(desc.name, id));
|
|
1442
|
+
if (body) fn(desc.name, id, body);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1399
1446
|
applyRecordToBody(desc, body, record) {
|
|
1400
1447
|
const physics = desc.physics;
|
|
1401
1448
|
if (!physics) return;
|
|
@@ -1599,6 +1646,262 @@ var Mulberry32 = class {
|
|
|
1599
1646
|
}
|
|
1600
1647
|
};
|
|
1601
1648
|
|
|
1649
|
+
// src/core/rapier2d.ts
|
|
1650
|
+
import { angleFrom2d, applyChannel2d as applyChannel2d2, channelOf2d as channelOf2d2 } from "@irtio/schema";
|
|
1651
|
+
var engine3;
|
|
1652
|
+
var loading3;
|
|
1653
|
+
async function initRapier2d() {
|
|
1654
|
+
if (engine3) return engine3;
|
|
1655
|
+
loading3 ??= (async () => {
|
|
1656
|
+
const mod = await import("@dimforge/rapier2d-compat");
|
|
1657
|
+
const ns = mod.default ?? mod;
|
|
1658
|
+
await ns.init();
|
|
1659
|
+
engine3 = ns;
|
|
1660
|
+
return ns;
|
|
1661
|
+
})();
|
|
1662
|
+
return loading3;
|
|
1663
|
+
}
|
|
1664
|
+
function loadedRapier2d() {
|
|
1665
|
+
return engine3;
|
|
1666
|
+
}
|
|
1667
|
+
function resetRapier2dForTests() {
|
|
1668
|
+
engine3 = void 0;
|
|
1669
|
+
loading3 = void 0;
|
|
1670
|
+
}
|
|
1671
|
+
function bodyKey3(collection, id) {
|
|
1672
|
+
return `${collection}\0${id}`;
|
|
1673
|
+
}
|
|
1674
|
+
var Rapier2dRuntime = class {
|
|
1675
|
+
engineKind = "rapier2d";
|
|
1676
|
+
/** matter2d only; present so every runtime satisfies one internal shape. */
|
|
1677
|
+
matter = void 0;
|
|
1678
|
+
matterEngine = void 0;
|
|
1679
|
+
rapier;
|
|
1680
|
+
world;
|
|
1681
|
+
/** `true` when the world was built from scratch and `setup` has to run. */
|
|
1682
|
+
rebuilt;
|
|
1683
|
+
/** Rapier restores a whole world, contacts included, so `setup` runs only on a rebuild. */
|
|
1684
|
+
get needsSetup() {
|
|
1685
|
+
return this.rebuilt;
|
|
1686
|
+
}
|
|
1687
|
+
core;
|
|
1688
|
+
config;
|
|
1689
|
+
/** Physics-backed collections, in schema (name-sorted) order. */
|
|
1690
|
+
collections;
|
|
1691
|
+
bodies = /* @__PURE__ */ new Map();
|
|
1692
|
+
/** See `PhysicsRuntime.sleepSynced`: the one extra sync a body owes on the tick it sleeps. */
|
|
1693
|
+
sleepSynced = /* @__PURE__ */ new Set();
|
|
1694
|
+
constructor(core, rapier, options) {
|
|
1695
|
+
const config = core.definition.config.physics;
|
|
1696
|
+
if (!config) throw new Error("Rapier2dRuntime: the room config declares no physics");
|
|
1697
|
+
this.core = core;
|
|
1698
|
+
this.config = config;
|
|
1699
|
+
this.rapier = rapier;
|
|
1700
|
+
this.collections = core.ext.collections.filter(
|
|
1701
|
+
(c) => c.physics !== void 0
|
|
1702
|
+
);
|
|
1703
|
+
if (options.restore) {
|
|
1704
|
+
this.world = rapier.World.restoreSnapshot(options.restore.world);
|
|
1705
|
+
this.rebuilt = false;
|
|
1706
|
+
for (const [name, id, handle] of options.restore.bodies) {
|
|
1707
|
+
const body = this.world.getRigidBody(handle);
|
|
1708
|
+
if (body) this.bodies.set(bodyKey3(name, id), body);
|
|
1709
|
+
}
|
|
1710
|
+
} else {
|
|
1711
|
+
const g = config.gravity;
|
|
1712
|
+
this.world = new rapier.World({ x: g.x, y: g.y });
|
|
1713
|
+
this.rebuilt = true;
|
|
1714
|
+
}
|
|
1715
|
+
this.world.timestep = config.timestep ?? options.defaultTimestep;
|
|
1716
|
+
}
|
|
1717
|
+
get timestep() {
|
|
1718
|
+
return this.world.timestep;
|
|
1719
|
+
}
|
|
1720
|
+
/** Runs the room's `setup` — static geometry — on a world that was built rather than restored. */
|
|
1721
|
+
runSetup(room) {
|
|
1722
|
+
const setup = this.config.setup;
|
|
1723
|
+
if (!setup) return;
|
|
1724
|
+
this.core.guard("physics.setup", () => setup(this.world, this.rapier, room));
|
|
1725
|
+
}
|
|
1726
|
+
free() {
|
|
1727
|
+
this.bodies.clear();
|
|
1728
|
+
this.world.free();
|
|
1729
|
+
}
|
|
1730
|
+
// -------------------------------------------------------------------------
|
|
1731
|
+
// Bodies
|
|
1732
|
+
// -------------------------------------------------------------------------
|
|
1733
|
+
/** The body behind an instance, created on demand so a handler's own `add` is usable at once. */
|
|
1734
|
+
bodyFor(collection, id) {
|
|
1735
|
+
const existing = this.bodies.get(bodyKey3(collection, id));
|
|
1736
|
+
if (existing) return existing;
|
|
1737
|
+
const desc = this.collections.find((c) => c.name === collection);
|
|
1738
|
+
if (!desc) return void 0;
|
|
1739
|
+
const coll = plainEntity(this.core.plain, collection);
|
|
1740
|
+
const record = coll.get(id);
|
|
1741
|
+
if (record === void 0) return void 0;
|
|
1742
|
+
return this.create(desc, id, record);
|
|
1743
|
+
}
|
|
1744
|
+
create(desc, id, record) {
|
|
1745
|
+
const factory = this.config.bodies?.[desc.name];
|
|
1746
|
+
if (!factory) {
|
|
1747
|
+
this.core.log("error", `irtio: physics.bodies.${desc.name} is missing; no body created`);
|
|
1748
|
+
return void 0;
|
|
1749
|
+
}
|
|
1750
|
+
const spec = this.core.guard(
|
|
1751
|
+
`physics.bodies.${desc.name}`,
|
|
1752
|
+
() => factory(this.rapier, record, id)
|
|
1753
|
+
);
|
|
1754
|
+
if (!spec || !spec.body) {
|
|
1755
|
+
this.core.log(
|
|
1756
|
+
"error",
|
|
1757
|
+
`irtio: physics.bodies.${desc.name} returned no { body } for ${JSON.stringify(id)}`
|
|
1758
|
+
);
|
|
1759
|
+
return void 0;
|
|
1760
|
+
}
|
|
1761
|
+
const body = this.world.createRigidBody(spec.body);
|
|
1762
|
+
for (const collider of spec.colliders ?? []) this.world.createCollider(collider, body);
|
|
1763
|
+
this.applyRecordToBody(desc, body, record);
|
|
1764
|
+
this.bodies.set(bodyKey3(desc.name, id), body);
|
|
1765
|
+
return body;
|
|
1766
|
+
}
|
|
1767
|
+
// ---- M6 lane F: rewind ----
|
|
1768
|
+
/**
|
|
1769
|
+
* D72: every tracked body, in the order `sync()` walks them (collection order, then instance
|
|
1770
|
+
* order). The rapier2d third of the same read-only accessor the other two runtimes carry.
|
|
1771
|
+
*/
|
|
1772
|
+
eachTrackedBody(fn) {
|
|
1773
|
+
for (const desc of this.collections) {
|
|
1774
|
+
const plainColl = plainEntity(this.core.plain, desc.name);
|
|
1775
|
+
for (const id of plainColl.ids()) {
|
|
1776
|
+
const body = this.bodies.get(bodyKey3(desc.name, id));
|
|
1777
|
+
if (body) fn(desc.name, id, body);
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
applyRecordToBody(desc, body, record) {
|
|
1782
|
+
const physics = desc.physics;
|
|
1783
|
+
if (!physics) return;
|
|
1784
|
+
const t = body.translation();
|
|
1785
|
+
const v = body.linvel();
|
|
1786
|
+
const angle = body.rotation();
|
|
1787
|
+
const target = {
|
|
1788
|
+
x: t.x,
|
|
1789
|
+
y: t.y,
|
|
1790
|
+
qz: Math.sin(angle / 2),
|
|
1791
|
+
qw: Math.cos(angle / 2),
|
|
1792
|
+
vx: v.x,
|
|
1793
|
+
vy: v.y,
|
|
1794
|
+
wz: body.angvel()
|
|
1795
|
+
};
|
|
1796
|
+
for (const [channel, field] of physics.channels) {
|
|
1797
|
+
const raw = record[field];
|
|
1798
|
+
if (typeof raw !== "number") continue;
|
|
1799
|
+
applyChannel2d2(channel, raw, target);
|
|
1800
|
+
}
|
|
1801
|
+
body.setTranslation({ x: target.x, y: target.y }, true);
|
|
1802
|
+
body.setRotation(angleFrom2d(target.qz, target.qw), true);
|
|
1803
|
+
body.setLinvel({ x: target.vx, y: target.vy }, true);
|
|
1804
|
+
body.setAngvel(target.wz, true);
|
|
1805
|
+
}
|
|
1806
|
+
/**
|
|
1807
|
+
* Creates bodies for new instances and destroys bodies whose instance is gone. Runs once per
|
|
1808
|
+
* tick, right before the step, in collection order then instance order.
|
|
1809
|
+
*/
|
|
1810
|
+
reconcile() {
|
|
1811
|
+
const live = /* @__PURE__ */ new Set();
|
|
1812
|
+
for (const desc of this.collections) {
|
|
1813
|
+
const coll = plainEntity(this.core.plain, desc.name);
|
|
1814
|
+
for (const id of coll.ids()) {
|
|
1815
|
+
const key = bodyKey3(desc.name, id);
|
|
1816
|
+
live.add(key);
|
|
1817
|
+
if (this.bodies.has(key)) continue;
|
|
1818
|
+
const record = coll.get(id);
|
|
1819
|
+
if (record !== void 0) this.create(desc, id, record);
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
for (const [key, body] of [...this.bodies]) {
|
|
1823
|
+
if (live.has(key)) continue;
|
|
1824
|
+
this.bodies.delete(key);
|
|
1825
|
+
this.sleepSynced.delete(key);
|
|
1826
|
+
this.world.removeRigidBody(body);
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
// -------------------------------------------------------------------------
|
|
1830
|
+
// Step and sync
|
|
1831
|
+
// -------------------------------------------------------------------------
|
|
1832
|
+
step() {
|
|
1833
|
+
this.world.step();
|
|
1834
|
+
if (!rapierHasStepped()) noteRapierStep();
|
|
1835
|
+
}
|
|
1836
|
+
/**
|
|
1837
|
+
* Body → schema, through the tracked proxies, so movement leaves the room as an ordinary delta.
|
|
1838
|
+
* Values are `Math.fround`ed for f32 fields, so room code reads exactly what the wire carries.
|
|
1839
|
+
*/
|
|
1840
|
+
sync() {
|
|
1841
|
+
for (const desc of this.collections) {
|
|
1842
|
+
const physics = desc.physics;
|
|
1843
|
+
if (!physics) continue;
|
|
1844
|
+
const tracked = this.core.anyState[desc.name];
|
|
1845
|
+
const plainColl = plainEntity(this.core.plain, desc.name);
|
|
1846
|
+
const rounders = roundersFor3(desc);
|
|
1847
|
+
for (const id of plainColl.ids()) {
|
|
1848
|
+
const key = bodyKey3(desc.name, id);
|
|
1849
|
+
const body = this.bodies.get(key);
|
|
1850
|
+
if (!body) continue;
|
|
1851
|
+
if (body.isSleeping()) {
|
|
1852
|
+
if (this.sleepSynced.has(key)) continue;
|
|
1853
|
+
this.sleepSynced.add(key);
|
|
1854
|
+
} else {
|
|
1855
|
+
this.sleepSynced.delete(key);
|
|
1856
|
+
}
|
|
1857
|
+
const record = tracked.get(id);
|
|
1858
|
+
if (!record) continue;
|
|
1859
|
+
const t = body.translation();
|
|
1860
|
+
const v = body.linvel();
|
|
1861
|
+
const state = {
|
|
1862
|
+
x: t.x,
|
|
1863
|
+
y: t.y,
|
|
1864
|
+
angle: body.rotation(),
|
|
1865
|
+
vx: v.x,
|
|
1866
|
+
vy: v.y,
|
|
1867
|
+
angularVelocity: body.angvel()
|
|
1868
|
+
};
|
|
1869
|
+
for (const [channel, field] of physics.channels) {
|
|
1870
|
+
const next = (rounders[field] ?? identity3)(channelOf2d2(channel, state));
|
|
1871
|
+
if (record[field] !== next) record[field] = next;
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
// -------------------------------------------------------------------------
|
|
1877
|
+
// Hibernation
|
|
1878
|
+
// -------------------------------------------------------------------------
|
|
1879
|
+
/**
|
|
1880
|
+
* The same `PhysicsSection` the 3D runtime writes — a real engine snapshot plus the
|
|
1881
|
+
* entity↔handle map, whose handles must ride as f64 for the reason spelled out on
|
|
1882
|
+
* {@link PhysicsSection}. What tells the two apart on the way back in is the envelope
|
|
1883
|
+
* `core/physics.ts` wraps this in, not anything in here.
|
|
1884
|
+
*/
|
|
1885
|
+
serialize() {
|
|
1886
|
+
const bodies = [];
|
|
1887
|
+
for (const [key, body] of this.bodies) {
|
|
1888
|
+
const sep = key.indexOf("\0");
|
|
1889
|
+
bodies.push([key.slice(0, sep), key.slice(sep + 1), body.handle]);
|
|
1890
|
+
}
|
|
1891
|
+
return { world: this.world.takeSnapshot(), bodies };
|
|
1892
|
+
}
|
|
1893
|
+
};
|
|
1894
|
+
function identity3(v) {
|
|
1895
|
+
return v;
|
|
1896
|
+
}
|
|
1897
|
+
function roundersFor3(desc) {
|
|
1898
|
+
const out = {};
|
|
1899
|
+
for (const f of desc.fields) {
|
|
1900
|
+
if (f.type.kind === "f32") out[f.name] = Math.fround;
|
|
1901
|
+
}
|
|
1902
|
+
return out;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1602
1905
|
// src/core/views.ts
|
|
1603
1906
|
import {
|
|
1604
1907
|
collectionDirty,
|
|
@@ -1963,8 +2266,723 @@ import {
|
|
|
1963
2266
|
validateForDeploy
|
|
1964
2267
|
} from "@irtio/schema";
|
|
1965
2268
|
|
|
2269
|
+
// src/core/history.ts
|
|
2270
|
+
var HISTORY_MAX_TICKS = 240;
|
|
2271
|
+
var STRIDE = 13;
|
|
2272
|
+
function emptyPose() {
|
|
2273
|
+
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
|
+
}
|
|
2275
|
+
function newEntry() {
|
|
2276
|
+
return { tick: -1, count: 0, collections: [], ids: [], values: new Float64Array(0) };
|
|
2277
|
+
}
|
|
2278
|
+
var PoseHistory = class {
|
|
2279
|
+
depth;
|
|
2280
|
+
entries = [];
|
|
2281
|
+
/** Index of the newest entry in `entries`, or -1 when nothing has been captured. */
|
|
2282
|
+
head = -1;
|
|
2283
|
+
size = 0;
|
|
2284
|
+
/** Reused across every body of every capture: the capture path allocates nothing per body. */
|
|
2285
|
+
scratchPose = emptyPose();
|
|
2286
|
+
constructor(depth) {
|
|
2287
|
+
this.depth = Math.max(1, Math.min(HISTORY_MAX_TICKS, Math.floor(depth)));
|
|
2288
|
+
for (let i = 0; i < this.depth; i++) this.entries.push(newEntry());
|
|
2289
|
+
}
|
|
2290
|
+
get length() {
|
|
2291
|
+
return this.size;
|
|
2292
|
+
}
|
|
2293
|
+
/** The newest tick captured, or `undefined` when the buffer is empty. */
|
|
2294
|
+
get newestTick() {
|
|
2295
|
+
return this.size === 0 ? void 0 : this.entries[this.head].tick;
|
|
2296
|
+
}
|
|
2297
|
+
/** The oldest tick still held, or `undefined` when the buffer is empty. */
|
|
2298
|
+
get oldestTick() {
|
|
2299
|
+
if (this.size === 0) return void 0;
|
|
2300
|
+
const i = (this.head - (this.size - 1) + this.depth * 2) % this.depth;
|
|
2301
|
+
return this.entries[i].tick;
|
|
2302
|
+
}
|
|
2303
|
+
/** Drops everything. Used by the hibernation path, where a buffer cannot survive. */
|
|
2304
|
+
clear() {
|
|
2305
|
+
this.head = -1;
|
|
2306
|
+
this.size = 0;
|
|
2307
|
+
}
|
|
2308
|
+
/** Captures one tick's poses off the live runtime. Called right after `physics.sync()`. */
|
|
2309
|
+
capture(tick, physics) {
|
|
2310
|
+
this.head = this.size === 0 ? 0 : (this.head + 1) % this.depth;
|
|
2311
|
+
if (this.size < this.depth) this.size++;
|
|
2312
|
+
const entry = this.entries[this.head];
|
|
2313
|
+
entry.tick = tick;
|
|
2314
|
+
entry.count = 0;
|
|
2315
|
+
const pose = this.scratchPose;
|
|
2316
|
+
const kind = physics.engineKind;
|
|
2317
|
+
physics.eachTrackedBody((collection, id, body) => {
|
|
2318
|
+
if (kind === "rapier3d") readRapierPose(body, pose);
|
|
2319
|
+
else if (kind === "rapier2d") readRapier2dPose(body, pose);
|
|
2320
|
+
else readMatterPose(body, pose);
|
|
2321
|
+
this.push(entry, collection, id, pose);
|
|
2322
|
+
});
|
|
2323
|
+
}
|
|
2324
|
+
push(entry, collection, id, pose) {
|
|
2325
|
+
const i = entry.count;
|
|
2326
|
+
const need = (i + 1) * STRIDE;
|
|
2327
|
+
if (entry.values.length < need) {
|
|
2328
|
+
const grown = new Float64Array(Math.max(need, entry.values.length * 2, STRIDE * 8));
|
|
2329
|
+
grown.set(entry.values);
|
|
2330
|
+
entry.values = grown;
|
|
2331
|
+
}
|
|
2332
|
+
entry.collections[i] = collection;
|
|
2333
|
+
entry.ids[i] = id;
|
|
2334
|
+
const v = entry.values;
|
|
2335
|
+
const o = i * STRIDE;
|
|
2336
|
+
v[o] = pose.x;
|
|
2337
|
+
v[o + 1] = pose.y;
|
|
2338
|
+
v[o + 2] = pose.z;
|
|
2339
|
+
v[o + 3] = pose.qx;
|
|
2340
|
+
v[o + 4] = pose.qy;
|
|
2341
|
+
v[o + 5] = pose.qz;
|
|
2342
|
+
v[o + 6] = pose.qw;
|
|
2343
|
+
v[o + 7] = pose.vx;
|
|
2344
|
+
v[o + 8] = pose.vy;
|
|
2345
|
+
v[o + 9] = pose.vz;
|
|
2346
|
+
v[o + 10] = pose.wx;
|
|
2347
|
+
v[o + 11] = pose.wy;
|
|
2348
|
+
v[o + 12] = pose.wz;
|
|
2349
|
+
entry.count = i + 1;
|
|
2350
|
+
}
|
|
2351
|
+
/**
|
|
2352
|
+
* The entry a rewind to `requested` answers from, clamped into the window the buffer actually
|
|
2353
|
+
* holds. `undefined` only when nothing has been captured at all.
|
|
2354
|
+
*/
|
|
2355
|
+
resolve(requested) {
|
|
2356
|
+
if (this.size === 0) return void 0;
|
|
2357
|
+
const newest = this.entries[this.head].tick;
|
|
2358
|
+
const oldest = this.oldestTick;
|
|
2359
|
+
const want = Number.isFinite(requested) ? Math.floor(requested) : newest;
|
|
2360
|
+
const tick = want < oldest ? oldest : want > newest ? newest : want;
|
|
2361
|
+
const entry = this.at(tick);
|
|
2362
|
+
if (!entry) return void 0;
|
|
2363
|
+
return { entry, tick: entry.tick, clamped: tick !== want };
|
|
2364
|
+
}
|
|
2365
|
+
/**
|
|
2366
|
+
* The entry for exactly `tick`. Index arithmetic first (captures are one tick apart, so the
|
|
2367
|
+
* offset from the head is the tick difference), with a scan as insurance: a room whose loop
|
|
2368
|
+
* ever skipped a capture would otherwise be answered with the wrong tick's poses, and answering
|
|
2369
|
+
* for a tick that was not recorded is the one thing this buffer must never do.
|
|
2370
|
+
*/
|
|
2371
|
+
at(tick) {
|
|
2372
|
+
const newest = this.entries[this.head].tick;
|
|
2373
|
+
const offset = newest - tick;
|
|
2374
|
+
if (offset >= 0 && offset < this.size) {
|
|
2375
|
+
const e = this.entries[(this.head - offset + this.depth * 2) % this.depth];
|
|
2376
|
+
if (e.tick === tick) return e;
|
|
2377
|
+
}
|
|
2378
|
+
for (let k = 0; k < this.size; k++) {
|
|
2379
|
+
const e = this.entries[(this.head - k + this.depth * 2) % this.depth];
|
|
2380
|
+
if (e.tick === tick) return e;
|
|
2381
|
+
}
|
|
2382
|
+
return void 0;
|
|
2383
|
+
}
|
|
2384
|
+
};
|
|
2385
|
+
function poseAt(entry, index, into) {
|
|
2386
|
+
const v = entry.values;
|
|
2387
|
+
const o = index * STRIDE;
|
|
2388
|
+
into.x = v[o];
|
|
2389
|
+
into.y = v[o + 1];
|
|
2390
|
+
into.z = v[o + 2];
|
|
2391
|
+
into.qx = v[o + 3];
|
|
2392
|
+
into.qy = v[o + 4];
|
|
2393
|
+
into.qz = v[o + 5];
|
|
2394
|
+
into.qw = v[o + 6];
|
|
2395
|
+
into.vx = v[o + 7];
|
|
2396
|
+
into.vy = v[o + 8];
|
|
2397
|
+
into.vz = v[o + 9];
|
|
2398
|
+
into.wx = v[o + 10];
|
|
2399
|
+
into.wy = v[o + 11];
|
|
2400
|
+
into.wz = v[o + 12];
|
|
2401
|
+
}
|
|
2402
|
+
function readRapierPose(body, into) {
|
|
2403
|
+
const t = body.translation();
|
|
2404
|
+
const r = body.rotation();
|
|
2405
|
+
const v = body.linvel();
|
|
2406
|
+
const w = body.angvel();
|
|
2407
|
+
into.x = t.x;
|
|
2408
|
+
into.y = t.y;
|
|
2409
|
+
into.z = t.z;
|
|
2410
|
+
into.qx = r.x;
|
|
2411
|
+
into.qy = r.y;
|
|
2412
|
+
into.qz = r.z;
|
|
2413
|
+
into.qw = r.w;
|
|
2414
|
+
into.vx = v.x;
|
|
2415
|
+
into.vy = v.y;
|
|
2416
|
+
into.vz = v.z;
|
|
2417
|
+
into.wx = w.x;
|
|
2418
|
+
into.wy = w.y;
|
|
2419
|
+
into.wz = w.z;
|
|
2420
|
+
}
|
|
2421
|
+
function readRapier2dPose(body, into) {
|
|
2422
|
+
const t = body.translation();
|
|
2423
|
+
const angle = body.rotation();
|
|
2424
|
+
const v = body.linvel();
|
|
2425
|
+
into.x = t.x;
|
|
2426
|
+
into.y = t.y;
|
|
2427
|
+
into.z = 0;
|
|
2428
|
+
into.qx = 0;
|
|
2429
|
+
into.qy = 0;
|
|
2430
|
+
into.qz = Math.sin(angle / 2);
|
|
2431
|
+
into.qw = Math.cos(angle / 2);
|
|
2432
|
+
into.vx = v.x;
|
|
2433
|
+
into.vy = v.y;
|
|
2434
|
+
into.vz = 0;
|
|
2435
|
+
into.wx = 0;
|
|
2436
|
+
into.wy = 0;
|
|
2437
|
+
into.wz = body.angvel();
|
|
2438
|
+
}
|
|
2439
|
+
function readMatterPose(body, into) {
|
|
2440
|
+
into.x = body.position.x;
|
|
2441
|
+
into.y = body.position.y;
|
|
2442
|
+
into.z = 0;
|
|
2443
|
+
into.qx = 0;
|
|
2444
|
+
into.qy = 0;
|
|
2445
|
+
into.qz = Math.sin(body.angle / 2);
|
|
2446
|
+
into.qw = Math.cos(body.angle / 2);
|
|
2447
|
+
into.vx = body.velocity.x;
|
|
2448
|
+
into.vy = body.velocity.y;
|
|
2449
|
+
into.vz = 0;
|
|
2450
|
+
into.wx = 0;
|
|
2451
|
+
into.wy = 0;
|
|
2452
|
+
into.wz = body.angularVelocity;
|
|
2453
|
+
}
|
|
2454
|
+
function historyDepthOf(config) {
|
|
2455
|
+
const declared = config?.history;
|
|
2456
|
+
if (typeof declared !== "number" || !Number.isFinite(declared) || declared <= 0) return void 0;
|
|
2457
|
+
return Math.min(HISTORY_MAX_TICKS, Math.floor(declared));
|
|
2458
|
+
}
|
|
2459
|
+
function keyOf(collection, id) {
|
|
2460
|
+
return `${collection} ${id}`;
|
|
2461
|
+
}
|
|
2462
|
+
function angleOf(qz, qw) {
|
|
2463
|
+
return 2 * Math.atan2(qz, qw);
|
|
2464
|
+
}
|
|
2465
|
+
var BaseScratch = class {
|
|
2466
|
+
constructor(physics, depth, tickNow) {
|
|
2467
|
+
this.physics = physics;
|
|
2468
|
+
this.depth = depth;
|
|
2469
|
+
this.tickNow = tickNow;
|
|
2470
|
+
}
|
|
2471
|
+
physics;
|
|
2472
|
+
depth;
|
|
2473
|
+
tickNow;
|
|
2474
|
+
bodies = /* @__PURE__ */ new Map();
|
|
2475
|
+
present = /* @__PURE__ */ new Set();
|
|
2476
|
+
pose = emptyPose();
|
|
2477
|
+
view(entry, tick, requested, clamped) {
|
|
2478
|
+
this.sync();
|
|
2479
|
+
this.present.clear();
|
|
2480
|
+
for (let i = 0; i < entry.count; i++) {
|
|
2481
|
+
const key = keyOf(entry.collections[i], entry.ids[i]);
|
|
2482
|
+
const e = this.bodies.get(key);
|
|
2483
|
+
if (!e) continue;
|
|
2484
|
+
poseAt(entry, i, this.pose);
|
|
2485
|
+
this.place(e, this.pose);
|
|
2486
|
+
this.present.add(key);
|
|
2487
|
+
}
|
|
2488
|
+
for (const [key, e] of this.bodies) {
|
|
2489
|
+
if (!this.present.has(key)) this.hide(e);
|
|
2490
|
+
}
|
|
2491
|
+
return this.build(tick, requested, clamped);
|
|
2492
|
+
}
|
|
2493
|
+
/**
|
|
2494
|
+
* Brings the scratch's body set in step with the live world: a double for every live body that
|
|
2495
|
+
* has none, a rebuild for one whose collider (or part) count changed, and a prune for one that
|
|
2496
|
+
* has been gone longer than the history is deep and so can no longer appear in any entry.
|
|
2497
|
+
*
|
|
2498
|
+
* The prune is conservative rather than exact: `lastLive` only advances when a rewind happens,
|
|
2499
|
+
* so a room that rewinds rarely holds its dead doubles a little longer than it strictly must.
|
|
2500
|
+
* Over-retention is a few hundred bytes; under-retention would be a wrong answer.
|
|
2501
|
+
*/
|
|
2502
|
+
sync() {
|
|
2503
|
+
const now = this.tickNow();
|
|
2504
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2505
|
+
this.physics.eachTrackedBody((collection, id, body) => {
|
|
2506
|
+
const key = keyOf(collection, id);
|
|
2507
|
+
seen.add(key);
|
|
2508
|
+
const parts = this.partsOf(body);
|
|
2509
|
+
const existing = this.bodies.get(key);
|
|
2510
|
+
if (existing && existing.parts === parts) {
|
|
2511
|
+
existing.lastLive = now;
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
if (existing) {
|
|
2515
|
+
this.bodies.delete(key);
|
|
2516
|
+
this.destroy(existing);
|
|
2517
|
+
}
|
|
2518
|
+
const made = this.clone(collection, id, body, parts);
|
|
2519
|
+
if (!made) return;
|
|
2520
|
+
made.lastLive = now;
|
|
2521
|
+
this.bodies.set(key, made);
|
|
2522
|
+
});
|
|
2523
|
+
for (const [key, e] of [...this.bodies]) {
|
|
2524
|
+
if (seen.has(key)) continue;
|
|
2525
|
+
if (now - e.lastLive <= this.depth) continue;
|
|
2526
|
+
this.bodies.delete(key);
|
|
2527
|
+
this.destroy(e);
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
};
|
|
2531
|
+
function qConj(q) {
|
|
2532
|
+
return { x: -q.x, y: -q.y, z: -q.z, w: q.w };
|
|
2533
|
+
}
|
|
2534
|
+
function qMul(a, b) {
|
|
2535
|
+
return {
|
|
2536
|
+
x: a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y,
|
|
2537
|
+
y: a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x,
|
|
2538
|
+
z: a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w,
|
|
2539
|
+
w: a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z
|
|
2540
|
+
};
|
|
2541
|
+
}
|
|
2542
|
+
function qRotate(q, v) {
|
|
2543
|
+
const p = qMul(qMul(q, { x: v.x, y: v.y, z: v.z, w: 0 }), qConj(q));
|
|
2544
|
+
return { x: p.x, y: p.y, z: p.z };
|
|
2545
|
+
}
|
|
2546
|
+
var RapierScratch = class extends BaseScratch {
|
|
2547
|
+
world;
|
|
2548
|
+
rapier;
|
|
2549
|
+
owners = /* @__PURE__ */ new Map();
|
|
2550
|
+
/** Live rigid-body handles the room tracks, so the statics pass knows what to skip. */
|
|
2551
|
+
trackedLive = /* @__PURE__ */ new Set();
|
|
2552
|
+
staticsCloned = false;
|
|
2553
|
+
constructor(physics, depth, tickNow) {
|
|
2554
|
+
super(physics, depth, tickNow);
|
|
2555
|
+
this.rapier = physics.rapier;
|
|
2556
|
+
this.world = new this.rapier.World({ x: 0, y: 0, z: 0 });
|
|
2557
|
+
this.world.timestep = 0;
|
|
2558
|
+
}
|
|
2559
|
+
partsOf(body) {
|
|
2560
|
+
return body.numColliders();
|
|
2561
|
+
}
|
|
2562
|
+
clone(collection, id, body, parts) {
|
|
2563
|
+
const live = body;
|
|
2564
|
+
this.trackedLive.add(live.handle);
|
|
2565
|
+
const made = this.world.createRigidBody(this.rapier.RigidBodyDesc.fixed());
|
|
2566
|
+
const origin = live.translation();
|
|
2567
|
+
const rot = live.rotation();
|
|
2568
|
+
const inv = qConj(rot);
|
|
2569
|
+
for (let i = 0; i < parts; i++) {
|
|
2570
|
+
const c = live.collider(i);
|
|
2571
|
+
const desc = new this.rapier.ColliderDesc(c.shape);
|
|
2572
|
+
desc.setSensor(c.isSensor());
|
|
2573
|
+
const w = c.translation();
|
|
2574
|
+
const local = qRotate(inv, { x: w.x - origin.x, y: w.y - origin.y, z: w.z - origin.z });
|
|
2575
|
+
desc.setTranslation(local.x, local.y, local.z);
|
|
2576
|
+
desc.setRotation(qMul(inv, c.rotation()));
|
|
2577
|
+
this.world.createCollider(desc, made);
|
|
2578
|
+
}
|
|
2579
|
+
this.owners.set(made.handle, { collection, id });
|
|
2580
|
+
return { collection, id, body: made, parts, lastLive: -1 };
|
|
2581
|
+
}
|
|
2582
|
+
place(entry, pose) {
|
|
2583
|
+
const b = entry.body;
|
|
2584
|
+
if (!b.isEnabled()) b.setEnabled(true);
|
|
2585
|
+
b.setTranslation({ x: pose.x, y: pose.y, z: pose.z }, false);
|
|
2586
|
+
b.setRotation({ x: pose.qx, y: pose.qy, z: pose.qz, w: pose.qw }, false);
|
|
2587
|
+
}
|
|
2588
|
+
/**
|
|
2589
|
+
* A body the requested tick did not have. `setEnabled(false)` takes the body and its colliders
|
|
2590
|
+
* out of the broad phase, so a query cannot hit it, which is the whole point: a body created
|
|
2591
|
+
* after the tick the shooter was looking at was not on their screen and must not be hittable.
|
|
2592
|
+
*/
|
|
2593
|
+
hide(entry) {
|
|
2594
|
+
if (entry.body.isEnabled()) entry.body.setEnabled(false);
|
|
2595
|
+
}
|
|
2596
|
+
destroy(entry) {
|
|
2597
|
+
this.owners.delete(entry.body.handle);
|
|
2598
|
+
this.world.removeRigidBody(entry.body);
|
|
2599
|
+
}
|
|
2600
|
+
build(tick, requested, clamped) {
|
|
2601
|
+
this.cloneStatics();
|
|
2602
|
+
this.world.step();
|
|
2603
|
+
const owners = this.owners;
|
|
2604
|
+
return {
|
|
2605
|
+
tick,
|
|
2606
|
+
requested,
|
|
2607
|
+
clamped,
|
|
2608
|
+
rapier: {
|
|
2609
|
+
world: this.world,
|
|
2610
|
+
who: (collider) => {
|
|
2611
|
+
const parent = collider.parent();
|
|
2612
|
+
return parent ? owners.get(parent.handle) : void 0;
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
};
|
|
2616
|
+
}
|
|
2617
|
+
/**
|
|
2618
|
+
* The live world's static geometry, everything `physics.setup` built, copied in once at the
|
|
2619
|
+
* first rewind. It is "as it stands now" rather than "as it stood then": static colliders do not
|
|
2620
|
+
* move, and a room that moves one has told the engine something that is not true.
|
|
2621
|
+
*/
|
|
2622
|
+
cloneStatics() {
|
|
2623
|
+
if (this.staticsCloned) return;
|
|
2624
|
+
this.staticsCloned = true;
|
|
2625
|
+
const live = this.physics.world;
|
|
2626
|
+
if (!live) return;
|
|
2627
|
+
live.forEachCollider((c) => {
|
|
2628
|
+
const parent = c.parent();
|
|
2629
|
+
if (parent !== null && (!parent.isFixed() || this.trackedLive.has(parent.handle))) return;
|
|
2630
|
+
const desc = new this.rapier.ColliderDesc(c.shape);
|
|
2631
|
+
desc.setSensor(c.isSensor());
|
|
2632
|
+
const t = c.translation();
|
|
2633
|
+
desc.setTranslation(t.x, t.y, t.z);
|
|
2634
|
+
desc.setRotation(c.rotation());
|
|
2635
|
+
this.world.createCollider(desc);
|
|
2636
|
+
});
|
|
2637
|
+
}
|
|
2638
|
+
free() {
|
|
2639
|
+
this.owners.clear();
|
|
2640
|
+
this.world.free();
|
|
2641
|
+
}
|
|
2642
|
+
};
|
|
2643
|
+
var Rapier2dScratch = class extends BaseScratch {
|
|
2644
|
+
world;
|
|
2645
|
+
rapier;
|
|
2646
|
+
owners = /* @__PURE__ */ new Map();
|
|
2647
|
+
/** Live rigid-body handles the room tracks, so the statics pass knows what to skip. */
|
|
2648
|
+
trackedLive = /* @__PURE__ */ new Set();
|
|
2649
|
+
staticsCloned = false;
|
|
2650
|
+
constructor(physics, depth, tickNow) {
|
|
2651
|
+
super(physics, depth, tickNow);
|
|
2652
|
+
this.rapier = physics.rapier;
|
|
2653
|
+
this.world = new this.rapier.World({ x: 0, y: 0 });
|
|
2654
|
+
this.world.timestep = 0;
|
|
2655
|
+
}
|
|
2656
|
+
partsOf(body) {
|
|
2657
|
+
return body.numColliders();
|
|
2658
|
+
}
|
|
2659
|
+
clone(collection, id, body, parts) {
|
|
2660
|
+
const live = body;
|
|
2661
|
+
this.trackedLive.add(live.handle);
|
|
2662
|
+
const made = this.world.createRigidBody(this.rapier.RigidBodyDesc.fixed());
|
|
2663
|
+
const origin = live.translation();
|
|
2664
|
+
const angle = live.rotation();
|
|
2665
|
+
const cos = Math.cos(-angle);
|
|
2666
|
+
const sin = Math.sin(-angle);
|
|
2667
|
+
for (let i = 0; i < parts; i++) {
|
|
2668
|
+
const c = live.collider(i);
|
|
2669
|
+
const desc = new this.rapier.ColliderDesc(c.shape);
|
|
2670
|
+
desc.setSensor(c.isSensor());
|
|
2671
|
+
const w = c.translation();
|
|
2672
|
+
const dx = w.x - origin.x;
|
|
2673
|
+
const dy = w.y - origin.y;
|
|
2674
|
+
desc.setTranslation(dx * cos - dy * sin, dx * sin + dy * cos);
|
|
2675
|
+
desc.setRotation(c.rotation() - angle);
|
|
2676
|
+
this.world.createCollider(desc, made);
|
|
2677
|
+
}
|
|
2678
|
+
this.owners.set(made.handle, { collection, id });
|
|
2679
|
+
return { collection, id, body: made, parts, lastLive: -1 };
|
|
2680
|
+
}
|
|
2681
|
+
place(entry, pose) {
|
|
2682
|
+
const b = entry.body;
|
|
2683
|
+
if (!b.isEnabled()) b.setEnabled(true);
|
|
2684
|
+
b.setTranslation({ x: pose.x, y: pose.y }, false);
|
|
2685
|
+
b.setRotation(angleOf(pose.qz, pose.qw), false);
|
|
2686
|
+
}
|
|
2687
|
+
hide(entry) {
|
|
2688
|
+
if (entry.body.isEnabled()) entry.body.setEnabled(false);
|
|
2689
|
+
}
|
|
2690
|
+
destroy(entry) {
|
|
2691
|
+
this.owners.delete(entry.body.handle);
|
|
2692
|
+
this.world.removeRigidBody(entry.body);
|
|
2693
|
+
}
|
|
2694
|
+
build(tick, requested, clamped) {
|
|
2695
|
+
this.cloneStatics();
|
|
2696
|
+
this.world.step();
|
|
2697
|
+
const owners = this.owners;
|
|
2698
|
+
return {
|
|
2699
|
+
tick,
|
|
2700
|
+
requested,
|
|
2701
|
+
clamped,
|
|
2702
|
+
rapier2d: {
|
|
2703
|
+
world: this.world,
|
|
2704
|
+
who: (collider) => {
|
|
2705
|
+
const parent = collider.parent();
|
|
2706
|
+
return parent ? owners.get(parent.handle) : void 0;
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
};
|
|
2710
|
+
}
|
|
2711
|
+
cloneStatics() {
|
|
2712
|
+
if (this.staticsCloned) return;
|
|
2713
|
+
this.staticsCloned = true;
|
|
2714
|
+
const live = this.physics.world;
|
|
2715
|
+
if (!live) return;
|
|
2716
|
+
live.forEachCollider((c) => {
|
|
2717
|
+
const parent = c.parent();
|
|
2718
|
+
if (parent !== null && (!parent.isFixed() || this.trackedLive.has(parent.handle))) return;
|
|
2719
|
+
const desc = new this.rapier.ColliderDesc(c.shape);
|
|
2720
|
+
desc.setSensor(c.isSensor());
|
|
2721
|
+
const t = c.translation();
|
|
2722
|
+
desc.setTranslation(t.x, t.y);
|
|
2723
|
+
desc.setRotation(c.rotation());
|
|
2724
|
+
this.world.createCollider(desc);
|
|
2725
|
+
});
|
|
2726
|
+
}
|
|
2727
|
+
free() {
|
|
2728
|
+
this.owners.clear();
|
|
2729
|
+
this.world.free();
|
|
2730
|
+
}
|
|
2731
|
+
};
|
|
2732
|
+
var MatterScratch = class extends BaseScratch {
|
|
2733
|
+
matter;
|
|
2734
|
+
owners = /* @__PURE__ */ new Map();
|
|
2735
|
+
/** Rebuilt per rewind: the doubles the requested tick actually had, plus the live statics. */
|
|
2736
|
+
visible = [];
|
|
2737
|
+
constructor(physics, depth, tickNow) {
|
|
2738
|
+
super(physics, depth, tickNow);
|
|
2739
|
+
this.matter = physics.matter;
|
|
2740
|
+
}
|
|
2741
|
+
partsOf(body) {
|
|
2742
|
+
return body.parts.length;
|
|
2743
|
+
}
|
|
2744
|
+
clone(collection, id, body, parts) {
|
|
2745
|
+
const M = this.matter;
|
|
2746
|
+
const live = body;
|
|
2747
|
+
const made = live.parts.length > 1 ? M.Body.create({ parts: live.parts.slice(1).map((p) => this.clonePart(p, live.angle)) }) : this.clonePart(live, live.angle);
|
|
2748
|
+
M.Body.setPosition(made, { x: 0, y: 0 });
|
|
2749
|
+
made.angle = 0;
|
|
2750
|
+
for (const p of made.parts) p.angle = 0;
|
|
2751
|
+
this.owners.set(made, { collection, id });
|
|
2752
|
+
return { collection, id, body: made, parts, lastLive: -1 };
|
|
2753
|
+
}
|
|
2754
|
+
/** One part, un-rotated by the parent's angle so the double starts at angle 0. */
|
|
2755
|
+
clonePart(part, parentAngle) {
|
|
2756
|
+
const M = this.matter;
|
|
2757
|
+
const verts = part.vertices.map((v) => ({ x: v.x, y: v.y }));
|
|
2758
|
+
if (parentAngle !== 0) M.Vertices.rotate(verts, -parentAngle, part.position);
|
|
2759
|
+
const made = M.Body.create({});
|
|
2760
|
+
M.Body.setVertices(made, verts);
|
|
2761
|
+
return made;
|
|
2762
|
+
}
|
|
2763
|
+
place(entry, pose) {
|
|
2764
|
+
const M = this.matter;
|
|
2765
|
+
M.Body.setAngle(entry.body, angleOf(pose.qz, pose.qw));
|
|
2766
|
+
M.Body.setPosition(entry.body, { x: pose.x, y: pose.y });
|
|
2767
|
+
this.visible.push(entry.body);
|
|
2768
|
+
}
|
|
2769
|
+
/** Nothing to undo: `visible` is rebuilt per rewind, so a body nobody placed is not in it. */
|
|
2770
|
+
hide() {
|
|
2771
|
+
}
|
|
2772
|
+
destroy(entry) {
|
|
2773
|
+
this.owners.delete(entry.body);
|
|
2774
|
+
}
|
|
2775
|
+
view(entry, tick, requested, clamped) {
|
|
2776
|
+
this.visible.length = 0;
|
|
2777
|
+
return super.view(entry, tick, requested, clamped);
|
|
2778
|
+
}
|
|
2779
|
+
build(tick, requested, clamped) {
|
|
2780
|
+
const M = this.matter;
|
|
2781
|
+
const engine4 = this.physics.matterEngine;
|
|
2782
|
+
if (engine4) {
|
|
2783
|
+
for (const b of M.Composite.allBodies(engine4.world)) {
|
|
2784
|
+
if (b.isStatic) this.visible.push(b);
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
const owners = this.owners;
|
|
2788
|
+
return {
|
|
2789
|
+
tick,
|
|
2790
|
+
requested,
|
|
2791
|
+
clamped,
|
|
2792
|
+
matter: {
|
|
2793
|
+
bodies: this.visible,
|
|
2794
|
+
who: (body) => owners.get(body)
|
|
2795
|
+
}
|
|
2796
|
+
};
|
|
2797
|
+
}
|
|
2798
|
+
free() {
|
|
2799
|
+
this.owners.clear();
|
|
2800
|
+
this.visible.length = 0;
|
|
2801
|
+
}
|
|
2802
|
+
};
|
|
2803
|
+
var RewindState = class {
|
|
2804
|
+
history;
|
|
2805
|
+
scratch;
|
|
2806
|
+
inside = false;
|
|
2807
|
+
constructor(depth) {
|
|
2808
|
+
this.history = new PoseHistory(depth);
|
|
2809
|
+
}
|
|
2810
|
+
free() {
|
|
2811
|
+
this.scratch?.free();
|
|
2812
|
+
this.scratch = void 0;
|
|
2813
|
+
}
|
|
2814
|
+
run(physics, tickNow, requested, fn) {
|
|
2815
|
+
if (this.inside) {
|
|
2816
|
+
throw new Error(
|
|
2817
|
+
"room.rewind: already inside a rewind. The second call would repose the same scratch world under the query still reading it, so it is refused rather than answered wrongly."
|
|
2818
|
+
);
|
|
2819
|
+
}
|
|
2820
|
+
const resolved = this.history.resolve(requested);
|
|
2821
|
+
if (!resolved) {
|
|
2822
|
+
throw new Error(
|
|
2823
|
+
`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).`
|
|
2824
|
+
);
|
|
2825
|
+
}
|
|
2826
|
+
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 view = this.scratch.view(resolved.entry, resolved.tick, requested, resolved.clamped);
|
|
2828
|
+
this.inside = true;
|
|
2829
|
+
try {
|
|
2830
|
+
return fn(view);
|
|
2831
|
+
} finally {
|
|
2832
|
+
this.inside = false;
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
};
|
|
2836
|
+
|
|
2837
|
+
// src/core/lobby.ts
|
|
2838
|
+
import {
|
|
2839
|
+
LOBBY_MEMBERS,
|
|
2840
|
+
LOBBY_MIN_PLAYERS,
|
|
2841
|
+
LOBBY_STATE,
|
|
2842
|
+
schemaHasLobby
|
|
2843
|
+
} from "@irtio/server";
|
|
2844
|
+
var runtimes = /* @__PURE__ */ new WeakMap();
|
|
2845
|
+
function runtimeOf(core) {
|
|
2846
|
+
let r = runtimes.get(core);
|
|
2847
|
+
if (!r) {
|
|
2848
|
+
r = { onStart: [], started: false };
|
|
2849
|
+
runtimes.set(core, r);
|
|
2850
|
+
}
|
|
2851
|
+
return r;
|
|
2852
|
+
}
|
|
2853
|
+
function lobbyConfigOf(core) {
|
|
2854
|
+
const declared = core.definition.config.lobby;
|
|
2855
|
+
if (declared === void 0) return void 0;
|
|
2856
|
+
if (!schemaHasLobby(core.definition.schema)) return void 0;
|
|
2857
|
+
return declared;
|
|
2858
|
+
}
|
|
2859
|
+
function stateOf3(core) {
|
|
2860
|
+
return core.anyState[LOBBY_STATE];
|
|
2861
|
+
}
|
|
2862
|
+
function evaluateLobby(core) {
|
|
2863
|
+
const config = lobbyConfigOf(core);
|
|
2864
|
+
if (config === void 0) return;
|
|
2865
|
+
const state = stateOf3(core);
|
|
2866
|
+
if (state === void 0) return;
|
|
2867
|
+
const members = trackedEntity(core, LOBBY_MEMBERS);
|
|
2868
|
+
const policy = config.start ?? "when-full";
|
|
2869
|
+
const present = [];
|
|
2870
|
+
for (const [clientId, entry] of core.clients) {
|
|
2871
|
+
if (!entry.connected) continue;
|
|
2872
|
+
present.push(clientId);
|
|
2873
|
+
if (!members.has(clientId)) members.add(clientId, { ready: false }, { owner: clientId });
|
|
2874
|
+
else if (members.ownerOf(clientId) !== clientId) members.setOwner(clientId, clientId);
|
|
2875
|
+
}
|
|
2876
|
+
const here = new Set(present);
|
|
2877
|
+
for (const id of [...members.ids()]) {
|
|
2878
|
+
if (!here.has(id)) members.remove(id);
|
|
2879
|
+
}
|
|
2880
|
+
const readyCount = policy === "when-ready" ? present.filter((id) => members.get(id)?.ready === true).length : 0;
|
|
2881
|
+
const capacity = Math.min(
|
|
2882
|
+
255,
|
|
2883
|
+
config.size ?? core.definition.config.maxClients,
|
|
2884
|
+
core.definition.config.maxClients
|
|
2885
|
+
);
|
|
2886
|
+
const min = Math.max(LOBBY_MIN_PLAYERS, config.min ?? LOBBY_MIN_PLAYERS);
|
|
2887
|
+
if (state.readyUi !== (policy === "when-ready")) state.readyUi = policy === "when-ready";
|
|
2888
|
+
if (state.present !== present.length) state.present = Math.min(255, present.length);
|
|
2889
|
+
if (state.ready !== readyCount) state.ready = readyCount;
|
|
2890
|
+
if (state.capacity !== capacity) state.capacity = capacity;
|
|
2891
|
+
if (state.phase !== "lobby") return;
|
|
2892
|
+
if (policy === "when-full") {
|
|
2893
|
+
if (present.length >= capacity && capacity >= LOBBY_MIN_PLAYERS) startLobby(core, "when-full");
|
|
2894
|
+
return;
|
|
2895
|
+
}
|
|
2896
|
+
if (policy === "when-ready") {
|
|
2897
|
+
if (present.length >= min && readyCount === present.length) startLobby(core, "when-ready");
|
|
2898
|
+
return;
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
function startLobby(core, why) {
|
|
2902
|
+
const runtime = runtimeOf(core);
|
|
2903
|
+
if (runtime.started) return;
|
|
2904
|
+
const state = stateOf3(core);
|
|
2905
|
+
if (state === void 0 || state.phase === "started") {
|
|
2906
|
+
runtime.started = true;
|
|
2907
|
+
return;
|
|
2908
|
+
}
|
|
2909
|
+
runtime.started = true;
|
|
2910
|
+
state.phase = "started";
|
|
2911
|
+
state.public = false;
|
|
2912
|
+
core.host.setLobby({ started: true });
|
|
2913
|
+
core.log("info", `lobby: the game started (${why})`);
|
|
2914
|
+
const declared = lobbyConfigOf(core)?.onStart;
|
|
2915
|
+
if (declared) {
|
|
2916
|
+
core.guard("lobby.onStart", () => declared(core.anyState, core.room));
|
|
2917
|
+
}
|
|
2918
|
+
for (const cb of [...runtime.onStart]) core.guard("lobby.onStart(cb)", cb);
|
|
2919
|
+
}
|
|
2920
|
+
function makeLobby(core) {
|
|
2921
|
+
const config = lobbyConfigOf(core);
|
|
2922
|
+
if (config === void 0) {
|
|
2923
|
+
throw new Error(
|
|
2924
|
+
"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"
|
|
2925
|
+
);
|
|
2926
|
+
}
|
|
2927
|
+
const state = stateOf3(core);
|
|
2928
|
+
const members = trackedEntity(core, LOBBY_MEMBERS);
|
|
2929
|
+
return {
|
|
2930
|
+
get phase() {
|
|
2931
|
+
return state.phase ?? "lobby";
|
|
2932
|
+
},
|
|
2933
|
+
get ready() {
|
|
2934
|
+
const out = [];
|
|
2935
|
+
for (const [clientId, entry] of core.clients) {
|
|
2936
|
+
if (!entry.connected) continue;
|
|
2937
|
+
out.push({
|
|
2938
|
+
clientId,
|
|
2939
|
+
ready: members.get(clientId)?.ready === true
|
|
2940
|
+
});
|
|
2941
|
+
}
|
|
2942
|
+
return out;
|
|
2943
|
+
},
|
|
2944
|
+
get public() {
|
|
2945
|
+
return state.public === true;
|
|
2946
|
+
},
|
|
2947
|
+
start() {
|
|
2948
|
+
startLobby(core, "room.lobby.start()");
|
|
2949
|
+
},
|
|
2950
|
+
onStart(cb) {
|
|
2951
|
+
if (typeof cb !== "function") {
|
|
2952
|
+
core.log("warn", "room.lobby.onStart: expects a function; ignored");
|
|
2953
|
+
return;
|
|
2954
|
+
}
|
|
2955
|
+
runtimeOf(core).onStart.push(cb);
|
|
2956
|
+
},
|
|
2957
|
+
setPublic(value) {
|
|
2958
|
+
setLobbyPublic(core, value === true);
|
|
2959
|
+
}
|
|
2960
|
+
};
|
|
2961
|
+
}
|
|
2962
|
+
function setLobbyPublic(core, value) {
|
|
2963
|
+
const config = lobbyConfigOf(core);
|
|
2964
|
+
if (config === void 0) return;
|
|
2965
|
+
const state = stateOf3(core);
|
|
2966
|
+
if (state === void 0) return;
|
|
2967
|
+
if (state.phase === "started") {
|
|
2968
|
+
core.log(
|
|
2969
|
+
"warn",
|
|
2970
|
+
"room.lobby.setPublic: the game has started; the room stays out of the registry"
|
|
2971
|
+
);
|
|
2972
|
+
return;
|
|
2973
|
+
}
|
|
2974
|
+
const veto = config.onSetPublic;
|
|
2975
|
+
if (veto) {
|
|
2976
|
+
const outcome = core.tryRun("lobby.onSetPublic", () => veto(core.anyState, value));
|
|
2977
|
+
if (!outcome.ok || outcome.value === false) return;
|
|
2978
|
+
}
|
|
2979
|
+
if (state.public !== value) state.public = value;
|
|
2980
|
+
core.host.setLobby({ public: value, queue: config.queue ?? "default" });
|
|
2981
|
+
}
|
|
2982
|
+
|
|
1966
2983
|
// src/core/messages.ts
|
|
1967
2984
|
import { FrameType as FrameType3, decodeMsg, encodeFrame as encodeFrame2, encodeMsg } from "@irtio/protocol";
|
|
2985
|
+
import { decodeFields as decodeFields2 } from "@irtio/schema";
|
|
1968
2986
|
function toRoomTarget(target) {
|
|
1969
2987
|
switch (target.kind) {
|
|
1970
2988
|
case "all":
|
|
@@ -1985,7 +3003,28 @@ function deliver(core, target, frame, exclude) {
|
|
|
1985
3003
|
if (entry.clientId === exclude) continue;
|
|
1986
3004
|
if (target.kind === "client" && entry.clientId !== target.clientId) continue;
|
|
1987
3005
|
if (target.kind === "role" && entry.role !== target.role) continue;
|
|
1988
|
-
core.send(entry.clientId, frame);
|
|
3006
|
+
core.send(entry.clientId, frame.slice());
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
function messagesOf(core) {
|
|
3010
|
+
return core.definition.schema.messages ?? [];
|
|
3011
|
+
}
|
|
3012
|
+
function decodeTyped(core, clientId, index, payload) {
|
|
3013
|
+
const desc = messagesOf(core)[index];
|
|
3014
|
+
if (!desc) {
|
|
3015
|
+
core.stats.messagesDropped++;
|
|
3016
|
+
core.log(
|
|
3017
|
+
"warn",
|
|
3018
|
+
`typed MSG from ${clientId}: no message with index ${index} in this schema; dropped`
|
|
3019
|
+
);
|
|
3020
|
+
return void 0;
|
|
3021
|
+
}
|
|
3022
|
+
try {
|
|
3023
|
+
return { name: desc.name, value: decodeFields2(desc.fields, payload) };
|
|
3024
|
+
} catch (err) {
|
|
3025
|
+
core.stats.messagesDropped++;
|
|
3026
|
+
core.log("warn", `typed MSG ${desc.name} from ${clientId} failed to decode:`, err);
|
|
3027
|
+
return void 0;
|
|
1989
3028
|
}
|
|
1990
3029
|
}
|
|
1991
3030
|
function handleMsg(core, clientId, payload) {
|
|
@@ -2001,31 +3040,54 @@ function handleMsg(core, clientId, payload) {
|
|
|
2001
3040
|
core.log("warn", `voice MSG from ${clientId} reached room code; dropped (supervisor bug)`);
|
|
2002
3041
|
return true;
|
|
2003
3042
|
}
|
|
3043
|
+
if (msg.typed && msg.target.kind === "server") {
|
|
3044
|
+
core.stats.messagesDropped++;
|
|
3045
|
+
core.log("warn", `typed MSG from ${clientId} addressed the server; dropped`);
|
|
3046
|
+
return true;
|
|
3047
|
+
}
|
|
2004
3048
|
const roomTarget = toRoomTarget(msg.target);
|
|
2005
3049
|
if (roomTarget === void 0) return true;
|
|
3050
|
+
let typed;
|
|
3051
|
+
if (msg.typed) {
|
|
3052
|
+
typed = decodeTyped(core, clientId, msg.typed.index, msg.payload);
|
|
3053
|
+
if (!typed) return true;
|
|
3054
|
+
}
|
|
2006
3055
|
const onMessage = core.definition.config.onMessage;
|
|
2007
3056
|
if (onMessage) {
|
|
2008
3057
|
const ran = core.tryRun(
|
|
2009
3058
|
"onMessage",
|
|
2010
|
-
() => onMessage(core.anyState, clientId, roomTarget, msg.payload, core.ctxFor(clientId))
|
|
3059
|
+
() => onMessage(core.anyState, clientId, roomTarget, msg.payload, core.ctxFor(clientId), typed)
|
|
2011
3060
|
);
|
|
2012
3061
|
if (!ran.ok || ran.value === false) return true;
|
|
2013
3062
|
}
|
|
2014
3063
|
if (msg.target.kind === "server") return true;
|
|
2015
3064
|
const frame = encodeFrame2(
|
|
2016
3065
|
FrameType3.MSG,
|
|
2017
|
-
encodeMsg({
|
|
3066
|
+
encodeMsg({
|
|
3067
|
+
target: { kind: "client", clientId },
|
|
3068
|
+
payload: msg.payload,
|
|
3069
|
+
...msg.typed ? { typed: msg.typed } : {}
|
|
3070
|
+
})
|
|
2018
3071
|
);
|
|
2019
3072
|
deliver(core, msg.target, frame, clientId);
|
|
2020
3073
|
return true;
|
|
2021
3074
|
}
|
|
3075
|
+
function wireTargetOf(target) {
|
|
3076
|
+
return target === "all" ? { kind: "all" } : typeof target === "string" ? { kind: "client", clientId: target } : { kind: "role", role: target.role };
|
|
3077
|
+
}
|
|
2022
3078
|
function sendMessage(core, target, bytes) {
|
|
2023
|
-
const wire = target === "all" ? { kind: "all" } : typeof target === "string" ? { kind: "client", clientId: target } : { kind: "role", role: target.role };
|
|
2024
3079
|
const frame = encodeFrame2(
|
|
2025
3080
|
FrameType3.MSG,
|
|
2026
3081
|
encodeMsg({ target: { kind: "server" }, payload: bytes })
|
|
2027
3082
|
);
|
|
2028
|
-
deliver(core,
|
|
3083
|
+
deliver(core, wireTargetOf(target), frame);
|
|
3084
|
+
}
|
|
3085
|
+
function sendTypedMessage(core, index, target, payload) {
|
|
3086
|
+
const frame = encodeFrame2(
|
|
3087
|
+
FrameType3.MSG,
|
|
3088
|
+
encodeMsg({ target: { kind: "server" }, payload, typed: { index } })
|
|
3089
|
+
);
|
|
3090
|
+
deliver(core, wireTargetOf(target), frame);
|
|
2029
3091
|
}
|
|
2030
3092
|
|
|
2031
3093
|
// src/core/room-api.ts
|
|
@@ -2035,7 +3097,7 @@ import {
|
|
|
2035
3097
|
busPayloadProblem,
|
|
2036
3098
|
encodeFrame as encodeFrame3
|
|
2037
3099
|
} from "@irtio/protocol";
|
|
2038
|
-
import { encodeDelta as encodeDelta3, isDirtyEmpty as isDirtyEmpty2 } from "@irtio/schema";
|
|
3100
|
+
import { encodeDelta as encodeDelta3, encodeFields as encodeFields2, isDirtyEmpty as isDirtyEmpty2 } from "@irtio/schema";
|
|
2039
3101
|
function makePhysicsApi(p) {
|
|
2040
3102
|
return {
|
|
2041
3103
|
get rapier() {
|
|
@@ -2068,6 +3130,22 @@ function makeMatterApi(p) {
|
|
|
2068
3130
|
}
|
|
2069
3131
|
};
|
|
2070
3132
|
}
|
|
3133
|
+
function makeRapier2dApi(p) {
|
|
3134
|
+
return {
|
|
3135
|
+
get rapier() {
|
|
3136
|
+
return p.rapier;
|
|
3137
|
+
},
|
|
3138
|
+
get world() {
|
|
3139
|
+
return p.world;
|
|
3140
|
+
},
|
|
3141
|
+
get timestep() {
|
|
3142
|
+
return p.timestep;
|
|
3143
|
+
},
|
|
3144
|
+
body(collection, id) {
|
|
3145
|
+
return p.bodyFor(collection, id);
|
|
3146
|
+
}
|
|
3147
|
+
};
|
|
3148
|
+
}
|
|
2071
3149
|
function makeKv(core) {
|
|
2072
3150
|
return {
|
|
2073
3151
|
get(playerId, key) {
|
|
@@ -2083,7 +3161,7 @@ function makeKv(core) {
|
|
|
2083
3161
|
}
|
|
2084
3162
|
function makeLeaderboard(core) {
|
|
2085
3163
|
return {
|
|
2086
|
-
submit(board, playerId, score) {
|
|
3164
|
+
submit(board, playerId, score, options) {
|
|
2087
3165
|
if (!Number.isInteger(score)) {
|
|
2088
3166
|
return rejectAsEvent(
|
|
2089
3167
|
core,
|
|
@@ -2092,7 +3170,17 @@ function makeLeaderboard(core) {
|
|
|
2092
3170
|
)
|
|
2093
3171
|
);
|
|
2094
3172
|
}
|
|
2095
|
-
return startHostCall(
|
|
3173
|
+
return startHostCall(
|
|
3174
|
+
core,
|
|
3175
|
+
{
|
|
3176
|
+
kind: "lbSubmit",
|
|
3177
|
+
board,
|
|
3178
|
+
playerId,
|
|
3179
|
+
score,
|
|
3180
|
+
...options?.bucket === void 0 ? {} : { bucket: options.bucket }
|
|
3181
|
+
},
|
|
3182
|
+
() => void 0
|
|
3183
|
+
);
|
|
2096
3184
|
}
|
|
2097
3185
|
};
|
|
2098
3186
|
}
|
|
@@ -2173,6 +3261,7 @@ function createRoomApi(core, publicUrl) {
|
|
|
2173
3261
|
let lastNow = Number.NEGATIVE_INFINITY;
|
|
2174
3262
|
let physicsApi;
|
|
2175
3263
|
let matterApi;
|
|
3264
|
+
let rapier2dApi;
|
|
2176
3265
|
let kvApi;
|
|
2177
3266
|
let busApi;
|
|
2178
3267
|
let leaderboardApi;
|
|
@@ -2183,6 +3272,14 @@ function createRoomApi(core, publicUrl) {
|
|
|
2183
3272
|
const sep = publicUrl.includes("?") ? "&" : "?";
|
|
2184
3273
|
return `${publicUrl}${sep}room=${encodeURIComponent(core.roomId)}`;
|
|
2185
3274
|
};
|
|
3275
|
+
const messages = {};
|
|
3276
|
+
for (const desc of core.definition.schema.messages ?? []) {
|
|
3277
|
+
messages[desc.name] = {
|
|
3278
|
+
send(target, value) {
|
|
3279
|
+
sendTypedMessage(core, desc.index, target, encodeFields2(desc.fields, value));
|
|
3280
|
+
}
|
|
3281
|
+
};
|
|
3282
|
+
}
|
|
2186
3283
|
const room = {
|
|
2187
3284
|
get id() {
|
|
2188
3285
|
return core.roomId;
|
|
@@ -2226,7 +3323,7 @@ function createRoomApi(core, publicUrl) {
|
|
|
2226
3323
|
}
|
|
2227
3324
|
if (p.engineKind !== "rapier3d") {
|
|
2228
3325
|
throw new Error(
|
|
2229
|
-
"room.physics: this room runs matter2d. Read room.physics2d instead (it has .matter and .engine, where this one has .rapier and .world)"
|
|
3326
|
+
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)"
|
|
2230
3327
|
);
|
|
2231
3328
|
}
|
|
2232
3329
|
return physicsApi ?? (physicsApi = makePhysicsApi(p));
|
|
@@ -2240,14 +3337,36 @@ function createRoomApi(core, publicUrl) {
|
|
|
2240
3337
|
}
|
|
2241
3338
|
if (p.engineKind !== "matter2d") {
|
|
2242
3339
|
throw new Error(
|
|
2243
|
-
"room.physics2d: this room runs rapier3d. Read room.physics instead (it has .rapier and .world, where this one has .matter and .engine)"
|
|
3340
|
+
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)"
|
|
2244
3341
|
);
|
|
2245
3342
|
}
|
|
2246
3343
|
return matterApi ?? (matterApi = makeMatterApi(p));
|
|
2247
3344
|
},
|
|
3345
|
+
get physicsRapier2d() {
|
|
3346
|
+
const p = core.physics;
|
|
3347
|
+
if (!p) {
|
|
3348
|
+
throw new Error(
|
|
3349
|
+
"room.physicsRapier2d: this room has no physics \u2014 add physics: { engine: 'rapier2d', gravity, bodies } to defineRoom(...)"
|
|
3350
|
+
);
|
|
3351
|
+
}
|
|
3352
|
+
if (p.engineKind !== "rapier2d") {
|
|
3353
|
+
throw new Error(
|
|
3354
|
+
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)"
|
|
3355
|
+
);
|
|
3356
|
+
}
|
|
3357
|
+
return rapier2dApi ?? (rapier2dApi = makeRapier2dApi(p));
|
|
3358
|
+
},
|
|
3359
|
+
// ---- M6 lane F: rewind ----
|
|
3360
|
+
// D72: one line, and deliberately only one. Everything the rewind does — the buffer, the
|
|
3361
|
+
// clamping, the scratch worlds, the reentrancy refusal — lives in `core/history.ts` behind
|
|
3362
|
+
// `RoomCore.rewind`, so this file stays what it says it is: a facade that reads through.
|
|
3363
|
+
rewind(tick, fn) {
|
|
3364
|
+
return core.rewind(tick, fn);
|
|
3365
|
+
},
|
|
2248
3366
|
send(target, bytes) {
|
|
2249
3367
|
sendMessage(core, target, bytes);
|
|
2250
3368
|
},
|
|
3369
|
+
messages,
|
|
2251
3370
|
setRole(clientId, role) {
|
|
2252
3371
|
setRole(core, clientId, role);
|
|
2253
3372
|
cached = void 0;
|
|
@@ -2313,6 +3432,10 @@ function createRoomApi(core, publicUrl) {
|
|
|
2313
3432
|
get bus() {
|
|
2314
3433
|
return busApi ?? (busApi = makeBus(core));
|
|
2315
3434
|
},
|
|
3435
|
+
// ---- M6 lane H: lobby front door ----
|
|
3436
|
+
get lobby() {
|
|
3437
|
+
return makeLobby(core);
|
|
3438
|
+
},
|
|
2316
3439
|
alarm(name, atMs) {
|
|
2317
3440
|
if (!isAlarmName(core, name, "room.alarm")) return;
|
|
2318
3441
|
if (!Number.isFinite(atMs)) {
|
|
@@ -2493,7 +3616,8 @@ var RoomCore = class _RoomCore {
|
|
|
2493
3616
|
visibleIdsTotal: 0,
|
|
2494
3617
|
membershipEnters: 0,
|
|
2495
3618
|
membershipLeaves: 0,
|
|
2496
|
-
corrections: 0
|
|
3619
|
+
corrections: 0,
|
|
3620
|
+
messagesDropped: 0
|
|
2497
3621
|
};
|
|
2498
3622
|
tick = 0;
|
|
2499
3623
|
stopped = false;
|
|
@@ -2515,6 +3639,13 @@ var RoomCore = class _RoomCore {
|
|
|
2515
3639
|
* cost the profiler has is behind this `undefined`.
|
|
2516
3640
|
*/
|
|
2517
3641
|
ledger;
|
|
3642
|
+
/**
|
|
3643
|
+
* D72: the pose history and the rewind scratch, present only when the room's physics config
|
|
3644
|
+
* declares `history`. Every cost this lane has is behind this `undefined`, and it is
|
|
3645
|
+
* deliberately not in the hibernation blob: a woken room starts with an empty buffer and fills
|
|
3646
|
+
* it again over its next `history` ticks.
|
|
3647
|
+
*/
|
|
3648
|
+
rewindState;
|
|
2518
3649
|
seed;
|
|
2519
3650
|
api;
|
|
2520
3651
|
internals;
|
|
@@ -2558,6 +3689,8 @@ var RoomCore = class _RoomCore {
|
|
|
2558
3689
|
this.loop = new Loop(self);
|
|
2559
3690
|
this.api = createRoomApi(self, options.publicUrl ?? DEFAULT_PUBLIC_URL);
|
|
2560
3691
|
this.physics = this.buildPhysics(restored);
|
|
3692
|
+
const historyDepth = this.physics ? historyDepthOf(definition.config.physics) : void 0;
|
|
3693
|
+
this.rewindState = historyDepth === void 0 ? void 0 : new RewindState(historyDepth);
|
|
2561
3694
|
this.subscribeDeclaredChannels();
|
|
2562
3695
|
if (restored) {
|
|
2563
3696
|
const onWake = definition.config.onWake;
|
|
@@ -2640,14 +3773,24 @@ var RoomCore = class _RoomCore {
|
|
|
2640
3773
|
const config = this.definition.config.physics;
|
|
2641
3774
|
if (!config) return void 0;
|
|
2642
3775
|
if (config.engine === "matter2d") return this.buildMatter(config, restored);
|
|
2643
|
-
|
|
2644
|
-
|
|
3776
|
+
if (config.engine === "rapier2d") return this.buildRapier2d(restored);
|
|
3777
|
+
const engine4 = loadedPhysics();
|
|
3778
|
+
if (!engine4) {
|
|
2645
3779
|
throw new Error(
|
|
2646
3780
|
"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"
|
|
2647
3781
|
);
|
|
2648
3782
|
}
|
|
2649
|
-
|
|
2650
|
-
|
|
3783
|
+
let section;
|
|
3784
|
+
if (restored?.physics) {
|
|
3785
|
+
const wrote = physicsSectionEngine(restored.physics);
|
|
3786
|
+
if (wrote === "rapier3d") section = decodePhysicsSection(restored.physics);
|
|
3787
|
+
else {
|
|
3788
|
+
this.host.log("warn", [
|
|
3789
|
+
`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.`
|
|
3790
|
+
]);
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3793
|
+
const physics = new PhysicsRuntime(this.internals, engine4, {
|
|
2651
3794
|
...section ? { restore: section } : {},
|
|
2652
3795
|
defaultTimestep: 1 / this.definition.config.tickRate
|
|
2653
3796
|
});
|
|
@@ -2678,11 +3821,12 @@ var RoomCore = class _RoomCore {
|
|
|
2678
3821
|
}
|
|
2679
3822
|
let section;
|
|
2680
3823
|
if (restored?.physics) {
|
|
2681
|
-
|
|
3824
|
+
const wrote = physicsSectionEngine(restored.physics);
|
|
3825
|
+
if (wrote === "matter2d") {
|
|
2682
3826
|
section = decodeMatterBodies(decodeMatterSectionEnvelope(restored.physics));
|
|
2683
3827
|
} else {
|
|
2684
3828
|
this.host.log("warn", [
|
|
2685
|
-
|
|
3829
|
+
`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.`
|
|
2686
3830
|
]);
|
|
2687
3831
|
}
|
|
2688
3832
|
}
|
|
@@ -2699,6 +3843,44 @@ var RoomCore = class _RoomCore {
|
|
|
2699
3843
|
physics.reconcile();
|
|
2700
3844
|
return physics;
|
|
2701
3845
|
}
|
|
3846
|
+
/**
|
|
3847
|
+
* The rapier2d half. It follows the *rapier3d* shape rather than matter2d's, because the 2D
|
|
3848
|
+
* build has a real `takeSnapshot()`: a restored world comes back whole and `setup` does not run
|
|
3849
|
+
* again. The only thing that differs from `buildPhysics` is which engine and which envelope.
|
|
3850
|
+
*/
|
|
3851
|
+
buildRapier2d(restored) {
|
|
3852
|
+
const engine4 = loadedRapier2d();
|
|
3853
|
+
if (!engine4) {
|
|
3854
|
+
throw new Error(
|
|
3855
|
+
"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"
|
|
3856
|
+
);
|
|
3857
|
+
}
|
|
3858
|
+
let section;
|
|
3859
|
+
if (restored?.physics) {
|
|
3860
|
+
const wrote = physicsSectionEngine(restored.physics);
|
|
3861
|
+
if (wrote === "rapier2d") {
|
|
3862
|
+
section = decodePhysicsSection(decodeRapier2dSectionEnvelope(restored.physics));
|
|
3863
|
+
} else {
|
|
3864
|
+
this.host.log("warn", [
|
|
3865
|
+
`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.`
|
|
3866
|
+
]);
|
|
3867
|
+
}
|
|
3868
|
+
}
|
|
3869
|
+
const physics = new Rapier2dRuntime(this.internals, engine4, {
|
|
3870
|
+
...section ? { restore: section } : {},
|
|
3871
|
+
defaultTimestep: 1 / this.definition.config.tickRate
|
|
3872
|
+
});
|
|
3873
|
+
if (physics.rebuilt) {
|
|
3874
|
+
if (restored) {
|
|
3875
|
+
this.host.log("info", [
|
|
3876
|
+
`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`
|
|
3877
|
+
]);
|
|
3878
|
+
}
|
|
3879
|
+
physics.runSetup(this.room);
|
|
3880
|
+
physics.reconcile();
|
|
3881
|
+
}
|
|
3882
|
+
return physics;
|
|
3883
|
+
}
|
|
2702
3884
|
/** Convenience for hosts: `RoomCore.restore(def, bytes, host, opts)`. */
|
|
2703
3885
|
static restore(definition, bytes, host, options) {
|
|
2704
3886
|
return new _RoomCore(definition, host, { ...options, restoreFrom: bytes });
|
|
@@ -2763,6 +3945,35 @@ var RoomCore = class _RoomCore {
|
|
|
2763
3945
|
captureTimeline() {
|
|
2764
3946
|
this.recorder?.capture(this.tick, this.ext, this.plain);
|
|
2765
3947
|
}
|
|
3948
|
+
// ---- M6 lane F: rewind ----
|
|
3949
|
+
/**
|
|
3950
|
+
* D72: record this tick's body poses, right after `physics.sync()` — the poses the clients are
|
|
3951
|
+
* about to be told about, under the tick number they will be told it under, which is the tick a
|
|
3952
|
+
* client later stamps its `CALL` with.
|
|
3953
|
+
*/
|
|
3954
|
+
captureHistory() {
|
|
3955
|
+
const physics = this.physics;
|
|
3956
|
+
if (!physics || !this.rewindState) return;
|
|
3957
|
+
this.rewindState.history.capture(this.tick, physics);
|
|
3958
|
+
}
|
|
3959
|
+
/**
|
|
3960
|
+
* D72: `room.rewind(tick, fn)`. The live world is not touched and nothing is re-simulated; `fn`
|
|
3961
|
+
* queries a scratch world holding every tracked body at its pose at `tick`.
|
|
3962
|
+
*/
|
|
3963
|
+
rewind(tick, fn) {
|
|
3964
|
+
const physics = this.physics;
|
|
3965
|
+
if (!physics) {
|
|
3966
|
+
throw new Error(
|
|
3967
|
+
"room.rewind: this room has no physics, so there are no poses to rewind. Add physics: { engine, gravity, bodies, history: <ticks> } to defineRoom(...)"
|
|
3968
|
+
);
|
|
3969
|
+
}
|
|
3970
|
+
if (!this.rewindState) {
|
|
3971
|
+
throw new Error(
|
|
3972
|
+
"room.rewind: this room declares no physics.history, so no poses are kept. Add history: <ticks> to the physics config; it is off by default because it costs heap per tick. See irt.io/docs/concepts/lag-compensation for how deep to make it."
|
|
3973
|
+
);
|
|
3974
|
+
}
|
|
3975
|
+
return this.rewindState.run(physics, () => this.tick, tick, fn);
|
|
3976
|
+
}
|
|
2766
3977
|
/** Live JSON view of the room for the dev page / supervisor admin API. */
|
|
2767
3978
|
inspect() {
|
|
2768
3979
|
return {
|
|
@@ -2797,6 +4008,7 @@ var RoomCore = class _RoomCore {
|
|
|
2797
4008
|
this.loop.stop();
|
|
2798
4009
|
rejectAllPending(this.internals, "room stopped");
|
|
2799
4010
|
rejectAllHostCalls(this.internals, "room stopped");
|
|
4011
|
+
this.rewindState?.free();
|
|
2800
4012
|
this.physics?.free();
|
|
2801
4013
|
}
|
|
2802
4014
|
/**
|
|
@@ -2986,6 +4198,9 @@ var RoomCore = class _RoomCore {
|
|
|
2986
4198
|
role: entry?.role ?? "",
|
|
2987
4199
|
name: entry?.name ?? "",
|
|
2988
4200
|
tick: this.tick,
|
|
4201
|
+
// D72: only an RPC has a stamp, and `rpc.ts` puts it on the ctx it hands the handler. Every
|
|
4202
|
+
// other entry point (join, leave, write, ownership) has no client tick by construction.
|
|
4203
|
+
clientTick: void 0,
|
|
2989
4204
|
reconnecting,
|
|
2990
4205
|
room: this.room
|
|
2991
4206
|
};
|
|
@@ -3154,6 +4369,7 @@ var RoomCore = class _RoomCore {
|
|
|
3154
4369
|
* its view's `DELTA` — encoded once per distinct view.
|
|
3155
4370
|
*/
|
|
3156
4371
|
flush() {
|
|
4372
|
+
evaluateLobby(this.internals);
|
|
3157
4373
|
const dirty = this.tracked.flush();
|
|
3158
4374
|
serverWinsCorrections(this.internals, dirty);
|
|
3159
4375
|
this.ledger?.newFlush();
|
|
@@ -3283,7 +4499,13 @@ var RoomCore = class _RoomCore {
|
|
|
3283
4499
|
}
|
|
3284
4500
|
};
|
|
3285
4501
|
function encodeSection(physics) {
|
|
3286
|
-
|
|
4502
|
+
if (physics.engineKind === "matter2d") {
|
|
4503
|
+
return encodeMatterSectionEnvelope(encodeMatterBodies(physics.serialize()));
|
|
4504
|
+
}
|
|
4505
|
+
if (physics.engineKind === "rapier2d") {
|
|
4506
|
+
return encodeRapier2dSectionEnvelope(encodePhysicsSection(physics.serialize()));
|
|
4507
|
+
}
|
|
4508
|
+
return encodePhysicsSection(physics.serialize());
|
|
3287
4509
|
}
|
|
3288
4510
|
|
|
3289
4511
|
export {
|
|
@@ -3306,10 +4528,15 @@ export {
|
|
|
3306
4528
|
onFirstRapierStep,
|
|
3307
4529
|
encodePhysicsSection,
|
|
3308
4530
|
physicsSectionEngine,
|
|
4531
|
+
encodeRapier2dSectionEnvelope,
|
|
4532
|
+
decodeRapier2dSectionEnvelope,
|
|
3309
4533
|
encodeMatterSectionEnvelope,
|
|
3310
4534
|
decodeMatterSectionEnvelope,
|
|
3311
4535
|
decodePhysicsSection,
|
|
3312
4536
|
Mulberry32,
|
|
4537
|
+
initRapier2d,
|
|
4538
|
+
loadedRapier2d,
|
|
4539
|
+
resetRapier2dForTests,
|
|
3313
4540
|
isVisible,
|
|
3314
4541
|
visibleTo,
|
|
3315
4542
|
visibleNames,
|