@irtio/runtime 4.5.1 → 4.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-VIN2SPZT.js → chunk-NSUQKWYO.js} +1 -1
- package/dist/{chunk-23TKVLTX.js → chunk-RLY4VAMM.js} +328 -12
- package/dist/{contract-B72MNcCa.d.ts → contract-CcZadpC6.d.ts} +18 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/{room-DY5J0VZR.d.ts → room-BW2ZSX_k.d.ts} +41 -1
- package/dist/test/index.d.ts +11 -3
- package/dist/test/index.js +4 -2
- package/dist/worker/index.d.ts +12 -1
- package/dist/worker/index.js +21 -4
- package/package.json +4 -4
|
@@ -222,6 +222,95 @@ function deepEqual(a, b) {
|
|
|
222
222
|
return true;
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
// src/core/body-guard.ts
|
|
226
|
+
var MAX_BODY_RESETS = 3;
|
|
227
|
+
function allFinite(values) {
|
|
228
|
+
for (const v of values) if (!Number.isFinite(v)) return false;
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
function describeNonFinite(entries) {
|
|
232
|
+
const bad = [];
|
|
233
|
+
for (const [name, v] of Object.entries(entries)) {
|
|
234
|
+
if (Number.isFinite(v)) continue;
|
|
235
|
+
bad.push(`${name}=${v}`);
|
|
236
|
+
if (bad.length === 4) break;
|
|
237
|
+
}
|
|
238
|
+
return bad.length > 0 ? bad.join(", ") : "non-finite";
|
|
239
|
+
}
|
|
240
|
+
var VELOCITY_CHANNELS = /* @__PURE__ */ new Set(["vx", "vy", "vz", "wx", "wy", "wz"]);
|
|
241
|
+
function zeroVelocityFields(desc, record) {
|
|
242
|
+
for (const [channel, field] of desc.physics?.channels ?? []) {
|
|
243
|
+
if (VELOCITY_CHANNELS.has(channel) && record[field] !== 0) record[field] = 0;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
var NUMERIC_KINDS = /* @__PURE__ */ new Set(["f32", "f64", "i32", "u8", "u16", "u32"]);
|
|
247
|
+
var NonFiniteGuard = class {
|
|
248
|
+
constructor(core) {
|
|
249
|
+
this.core = core;
|
|
250
|
+
}
|
|
251
|
+
core;
|
|
252
|
+
/** Resets so far, by body key (`collection\0id`). */
|
|
253
|
+
resets = /* @__PURE__ */ new Map();
|
|
254
|
+
/**
|
|
255
|
+
* Records one non-finite body and says what the engine should do with it. Also does the part
|
|
256
|
+
* that is the same on every engine: the log line, the room event, and the owner's intents.
|
|
257
|
+
*/
|
|
258
|
+
note(desc, id, key, detail) {
|
|
259
|
+
const count = (this.resets.get(key) ?? 0) + 1;
|
|
260
|
+
this.resets.set(key, count);
|
|
261
|
+
const name = desc.name;
|
|
262
|
+
const skip = count >= MAX_BODY_RESETS;
|
|
263
|
+
if (skip) {
|
|
264
|
+
this.core.log(
|
|
265
|
+
"error",
|
|
266
|
+
`irtio: physics body ${name}[${id}] went non-finite (${detail}); body skipped after ${MAX_BODY_RESETS} resets, room.physics.skip('${name}', '${id}', false) re-enables it`
|
|
267
|
+
);
|
|
268
|
+
} else {
|
|
269
|
+
this.core.log(
|
|
270
|
+
"warn",
|
|
271
|
+
`irtio: physics body ${name}[${id}] went non-finite (${detail}); reset to its last synced pose with zero velocity`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
this.attribute(desc, id, detail);
|
|
275
|
+
if (skip) this.resets.delete(key);
|
|
276
|
+
return skip ? "skip" : "reset";
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Names the owner, zeroes its intents, and tells it why. The zeroing goes through the tracked
|
|
280
|
+
* record, so it is an ordinary server write to a client-owned field, which the server-wins pass
|
|
281
|
+
* turns into a `CORRECT` for the owner.
|
|
282
|
+
*/
|
|
283
|
+
attribute(desc, id, detail) {
|
|
284
|
+
const core = this.core;
|
|
285
|
+
const owner = plainEntity(core.plain, desc.name).ownerOf(id);
|
|
286
|
+
const client = owner !== void 0 && core.clients.has(owner) ? owner : void 0;
|
|
287
|
+
core.recordEvent("error", client, `${desc.name}[${id}] non-finite: ${detail}`);
|
|
288
|
+
if (client === void 0) return;
|
|
289
|
+
const intents = desc.physics?.intents ?? [];
|
|
290
|
+
const record = core.anyState[desc.name].get(id);
|
|
291
|
+
if (record) {
|
|
292
|
+
for (const f of desc.fields) {
|
|
293
|
+
if (!intents.includes(f.name) || !NUMERIC_KINDS.has(f.type.kind)) continue;
|
|
294
|
+
if (record[f.name] !== 0) record[f.name] = 0;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
refuseByName(core, client, "E_WRITE_REJECTED", {
|
|
298
|
+
reason: `${desc.name}[${id}] went non-finite (${detail}); its intents were zeroed and the body reset. Clamp intents in validate`
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
/** Drops the counts of bodies whose row is gone. */
|
|
302
|
+
prune(live) {
|
|
303
|
+
for (const key of [...this.resets.keys()]) if (!live.has(key)) this.resets.delete(key);
|
|
304
|
+
}
|
|
305
|
+
/** Drops one body's count, when its row was replaced and the body rebuilt. */
|
|
306
|
+
forget(key) {
|
|
307
|
+
this.resets.delete(key);
|
|
308
|
+
}
|
|
309
|
+
clear() {
|
|
310
|
+
this.resets.clear();
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
225
314
|
// src/core/physics.ts
|
|
226
315
|
var engine;
|
|
227
316
|
var loading;
|
|
@@ -466,6 +555,10 @@ var PhysicsRuntime = class {
|
|
|
466
555
|
*/
|
|
467
556
|
skippedKeys = /* @__PURE__ */ new Set();
|
|
468
557
|
// ---- end M7 planned feature 3 (b) ----
|
|
558
|
+
/** bugs #116: what `sync()` does with a body that went non-finite. See `core/body-guard.ts`. */
|
|
559
|
+
nonFinite;
|
|
560
|
+
/** The mapped channel values of the body `sync()` is on, reused so the good path allocates nothing. */
|
|
561
|
+
nextScratch = [];
|
|
469
562
|
// ---- M7 planned feature 3 (a): physics-only unsynced bodies ----
|
|
470
563
|
/**
|
|
471
564
|
* Bodies with no schema row: rope anchors, dead-reckoned proxies, one-off sensors. Keyed by the
|
|
@@ -480,6 +573,7 @@ var PhysicsRuntime = class {
|
|
|
480
573
|
this.core = core;
|
|
481
574
|
this.config = config;
|
|
482
575
|
this.rapier = rapier;
|
|
576
|
+
this.nonFinite = new NonFiniteGuard(core);
|
|
483
577
|
this.collections = core.ext.collections.filter(
|
|
484
578
|
(c) => c.physics !== void 0
|
|
485
579
|
);
|
|
@@ -522,6 +616,7 @@ var PhysicsRuntime = class {
|
|
|
522
616
|
}
|
|
523
617
|
free() {
|
|
524
618
|
this.bodies.clear();
|
|
619
|
+
this.nonFinite.clear();
|
|
525
620
|
this.unsyncedBodies.clear();
|
|
526
621
|
this.world.free();
|
|
527
622
|
}
|
|
@@ -780,6 +875,7 @@ var PhysicsRuntime = class {
|
|
|
780
875
|
this.bodies.delete(key);
|
|
781
876
|
this.sleepSynced.delete(key);
|
|
782
877
|
this.sourceRecords.delete(key);
|
|
878
|
+
this.nonFinite.forget(key);
|
|
783
879
|
this.destroyBody(existing);
|
|
784
880
|
}
|
|
785
881
|
if (record !== void 0) this.create(desc, id, record);
|
|
@@ -793,6 +889,7 @@ var PhysicsRuntime = class {
|
|
|
793
889
|
this.destroyBody(body);
|
|
794
890
|
}
|
|
795
891
|
for (const key of [...this.skippedKeys]) if (!live.has(key)) this.skippedKeys.delete(key);
|
|
892
|
+
this.nonFinite.prune(live);
|
|
796
893
|
}
|
|
797
894
|
// -------------------------------------------------------------------------
|
|
798
895
|
// Step and sync
|
|
@@ -829,13 +926,68 @@ var PhysicsRuntime = class {
|
|
|
829
926
|
const r = body.rotation();
|
|
830
927
|
const v = body.linvel();
|
|
831
928
|
const w = body.angvel();
|
|
929
|
+
const nexts = this.nextScratch;
|
|
930
|
+
nexts.length = 0;
|
|
832
931
|
for (const [channel, field] of physics.channels) {
|
|
833
|
-
|
|
932
|
+
nexts.push((rounders[field] ?? identity)(channelValue(channel, t, r, v, w)));
|
|
933
|
+
}
|
|
934
|
+
if (!poseFinite(t, r, v, w) || !allFinite(nexts)) {
|
|
935
|
+
const detail = describeNonFinite({
|
|
936
|
+
x: t.x,
|
|
937
|
+
y: t.y,
|
|
938
|
+
z: t.z,
|
|
939
|
+
qx: r.x,
|
|
940
|
+
qy: r.y,
|
|
941
|
+
qz: r.z,
|
|
942
|
+
qw: r.w,
|
|
943
|
+
vx: v.x,
|
|
944
|
+
vy: v.y,
|
|
945
|
+
vz: v.z,
|
|
946
|
+
wx: w.x,
|
|
947
|
+
wy: w.y,
|
|
948
|
+
wz: w.z,
|
|
949
|
+
...Object.fromEntries(physics.channels.map(([, field], i2) => [field, nexts[i2] ?? 0]))
|
|
950
|
+
});
|
|
951
|
+
if (this.nonFinite.note(desc, id, key, detail) === "skip") {
|
|
952
|
+
this.setSkipped(desc.name, id, true);
|
|
953
|
+
} else {
|
|
954
|
+
this.resetBody(desc, body, record);
|
|
955
|
+
}
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
let i = 0;
|
|
959
|
+
for (const [, field] of physics.channels) {
|
|
960
|
+
const next = nexts[i++];
|
|
834
961
|
if (record[field] !== next) record[field] = next;
|
|
835
962
|
}
|
|
836
963
|
}
|
|
837
964
|
}
|
|
838
965
|
}
|
|
966
|
+
/**
|
|
967
|
+
* bugs #116: puts a non-finite body back at its row's pose (the last one `sync()` wrote, always
|
|
968
|
+
* finite), at rest, with no force left on it. Unmapped channels the engine made non-finite go to
|
|
969
|
+
* zero, or the identity rotation, since there is nothing better to put them back to.
|
|
970
|
+
*/
|
|
971
|
+
resetBody(desc, body, record) {
|
|
972
|
+
const t = body.translation();
|
|
973
|
+
const r = body.rotation();
|
|
974
|
+
body.setTranslation(
|
|
975
|
+
{
|
|
976
|
+
x: Number.isFinite(t.x) ? t.x : 0,
|
|
977
|
+
y: Number.isFinite(t.y) ? t.y : 0,
|
|
978
|
+
z: Number.isFinite(t.z) ? t.z : 0
|
|
979
|
+
},
|
|
980
|
+
true
|
|
981
|
+
);
|
|
982
|
+
if (!allFinite([r.x, r.y, r.z, r.w])) body.setRotation({ x: 0, y: 0, z: 0, w: 1 }, true);
|
|
983
|
+
this.applyRecordToBody(desc, body, record);
|
|
984
|
+
if (!body.isEnabled()) body.setEnabled(true);
|
|
985
|
+
body.setLinvel({ x: 0, y: 0, z: 0 }, true);
|
|
986
|
+
body.setAngvel({ x: 0, y: 0, z: 0 }, true);
|
|
987
|
+
body.resetForces(true);
|
|
988
|
+
body.resetTorques(true);
|
|
989
|
+
zeroVelocityFields(desc, record);
|
|
990
|
+
}
|
|
839
991
|
// -------------------------------------------------------------------------
|
|
840
992
|
// Hibernation
|
|
841
993
|
// -------------------------------------------------------------------------
|
|
@@ -856,6 +1008,9 @@ var PhysicsRuntime = class {
|
|
|
856
1008
|
};
|
|
857
1009
|
}
|
|
858
1010
|
};
|
|
1011
|
+
function poseFinite(t, r, v, w) {
|
|
1012
|
+
return Number.isFinite(t.x) && Number.isFinite(t.y) && Number.isFinite(t.z) && Number.isFinite(r.x) && Number.isFinite(r.y) && Number.isFinite(r.z) && Number.isFinite(r.w) && Number.isFinite(v.x) && Number.isFinite(v.y) && Number.isFinite(v.z) && Number.isFinite(w.x) && Number.isFinite(w.y) && Number.isFinite(w.z);
|
|
1013
|
+
}
|
|
859
1014
|
function identity(v) {
|
|
860
1015
|
return v;
|
|
861
1016
|
}
|
|
@@ -2634,11 +2789,12 @@ var Loop = class {
|
|
|
2634
2789
|
failed = failed === void 0 ? "the physics step" : `${failed} and the physics step`;
|
|
2635
2790
|
}
|
|
2636
2791
|
}
|
|
2792
|
+
const flushed = this.core.tryRun("flush", () => this.core.flush());
|
|
2793
|
+
if (!flushed.ok) failed = failed === void 0 ? "flush" : `${failed} and flush`;
|
|
2637
2794
|
if (failed === void 0) this.consecutiveThrows = 0;
|
|
2638
2795
|
else {
|
|
2639
2796
|
this.consecutiveThrows++;
|
|
2640
2797
|
if (this.consecutiveThrows >= CRASH_AFTER_THROWS) {
|
|
2641
|
-
this.core.flush();
|
|
2642
2798
|
const reason = `${failed} threw ${this.consecutiveThrows} times in a row`;
|
|
2643
2799
|
this.core.stopped = true;
|
|
2644
2800
|
this.stop();
|
|
@@ -2646,7 +2802,6 @@ var Loop = class {
|
|
|
2646
2802
|
return;
|
|
2647
2803
|
}
|
|
2648
2804
|
}
|
|
2649
|
-
this.core.flush();
|
|
2650
2805
|
this.core.captureTimeline();
|
|
2651
2806
|
const elapsed = Math.max(0, host.now() - started);
|
|
2652
2807
|
const stats = this.core.stats;
|
|
@@ -2669,7 +2824,7 @@ var Loop = class {
|
|
|
2669
2824
|
applyEvent(f) {
|
|
2670
2825
|
this.core.tick++;
|
|
2671
2826
|
const ok = this.applyFrame(f);
|
|
2672
|
-
this.core.flush();
|
|
2827
|
+
this.core.guard("flush", () => this.core.flush());
|
|
2673
2828
|
return ok;
|
|
2674
2829
|
}
|
|
2675
2830
|
noteActivity() {
|
|
@@ -2968,6 +3123,9 @@ var MatterRuntime = class {
|
|
|
2968
3123
|
// ---- M7 planned feature 3 (b): per-entity physics skip ----
|
|
2969
3124
|
/** See `PhysicsRuntime.skippedKeys`: entities whose body the room has switched off. */
|
|
2970
3125
|
skippedKeys = /* @__PURE__ */ new Set();
|
|
3126
|
+
/** bugs #116: see `PhysicsRuntime.nonFinite`. */
|
|
3127
|
+
nonFinite;
|
|
3128
|
+
nextScratch = [];
|
|
2971
3129
|
// ---- end M7 planned feature 3 (b) ----
|
|
2972
3130
|
// ---- M7 planned feature 3 (a): physics-only unsynced bodies ----
|
|
2973
3131
|
unsyncedBodies = /* @__PURE__ */ new Map();
|
|
@@ -2982,6 +3140,7 @@ var MatterRuntime = class {
|
|
|
2982
3140
|
if (!config) throw new Error("MatterRuntime: the room config declares no physics");
|
|
2983
3141
|
this.core = core;
|
|
2984
3142
|
this.config = config;
|
|
3143
|
+
this.nonFinite = new NonFiniteGuard(core);
|
|
2985
3144
|
this.matter = matter;
|
|
2986
3145
|
this.collections = core.ext.collections.filter(
|
|
2987
3146
|
(c) => c.physics !== void 0
|
|
@@ -3036,6 +3195,7 @@ var MatterRuntime = class {
|
|
|
3036
3195
|
}
|
|
3037
3196
|
free() {
|
|
3038
3197
|
this.bodies.clear();
|
|
3198
|
+
this.nonFinite.clear();
|
|
3039
3199
|
this.unsyncedBodies.clear();
|
|
3040
3200
|
this.matter.Engine.clear(this.engine);
|
|
3041
3201
|
}
|
|
@@ -3224,6 +3384,7 @@ var MatterRuntime = class {
|
|
|
3224
3384
|
this.bodies.delete(key);
|
|
3225
3385
|
this.sleepSynced.delete(key);
|
|
3226
3386
|
this.sourceRecords.delete(key);
|
|
3387
|
+
this.nonFinite.forget(key);
|
|
3227
3388
|
this.wakeContactsOf(existing.body);
|
|
3228
3389
|
this.matter.Composite.remove(this.engine.world, [existing.body, ...existing.constraints]);
|
|
3229
3390
|
}
|
|
@@ -3239,6 +3400,7 @@ var MatterRuntime = class {
|
|
|
3239
3400
|
this.matter.Composite.remove(this.engine.world, [attached.body, ...attached.constraints]);
|
|
3240
3401
|
}
|
|
3241
3402
|
for (const key of [...this.skippedKeys]) if (!live.has(key)) this.skippedKeys.delete(key);
|
|
3403
|
+
this.nonFinite.prune(live);
|
|
3242
3404
|
}
|
|
3243
3405
|
// -------------------------------------------------------------------------
|
|
3244
3406
|
// Step and sync
|
|
@@ -3274,13 +3436,48 @@ var MatterRuntime = class {
|
|
|
3274
3436
|
vy: body.velocity.y,
|
|
3275
3437
|
angularVelocity: body.angularVelocity
|
|
3276
3438
|
};
|
|
3439
|
+
const nexts = this.nextScratch;
|
|
3440
|
+
nexts.length = 0;
|
|
3277
3441
|
for (const [channel, field] of physics.channels) {
|
|
3278
|
-
|
|
3442
|
+
nexts.push((rounders[field] ?? identity2)(channelOf2d(channel, state)));
|
|
3443
|
+
}
|
|
3444
|
+
if (!(Number.isFinite(state.x) && Number.isFinite(state.y) && Number.isFinite(state.angle) && Number.isFinite(state.vx) && Number.isFinite(state.vy) && Number.isFinite(state.angularVelocity)) || !allFinite(nexts)) {
|
|
3445
|
+
const detail = describeNonFinite({
|
|
3446
|
+
...state,
|
|
3447
|
+
...Object.fromEntries(physics.channels.map(([, field], i2) => [field, nexts[i2] ?? 0]))
|
|
3448
|
+
});
|
|
3449
|
+
if (this.nonFinite.note(desc, id, key, detail) === "skip") {
|
|
3450
|
+
this.setSkipped(desc.name, id, true);
|
|
3451
|
+
} else {
|
|
3452
|
+
this.resetBody(desc, id, key, attached, record);
|
|
3453
|
+
}
|
|
3454
|
+
continue;
|
|
3455
|
+
}
|
|
3456
|
+
let i = 0;
|
|
3457
|
+
for (const [, field] of physics.channels) {
|
|
3458
|
+
const next = nexts[i++];
|
|
3279
3459
|
if (record[field] !== next) record[field] = next;
|
|
3280
3460
|
}
|
|
3281
3461
|
}
|
|
3282
3462
|
}
|
|
3283
3463
|
}
|
|
3464
|
+
/**
|
|
3465
|
+
* bugs #116: the matter2d half of `PhysicsRuntime.resetBody`, done as a rebuild rather than a
|
|
3466
|
+
* teleport. matter moves a body by translating its vertices by the difference between the new
|
|
3467
|
+
* position and the old one, and once the old one is not finite that difference is not either:
|
|
3468
|
+
* `Body.setPosition` cannot bring the vertices back. So the body is removed and built again
|
|
3469
|
+
* from its row, whose pose is the last finite one `sync()` wrote, with the row's velocity
|
|
3470
|
+
* fields zeroed first so the new body spawns at rest (and with no force, being new).
|
|
3471
|
+
*/
|
|
3472
|
+
resetBody(desc, id, key, attached, record) {
|
|
3473
|
+
zeroVelocityFields(desc, record);
|
|
3474
|
+
this.bodies.delete(key);
|
|
3475
|
+
this.sleepSynced.delete(key);
|
|
3476
|
+
this.sourceRecords.delete(key);
|
|
3477
|
+
this.matter.Composite.remove(this.engine.world, [attached.body, ...attached.constraints]);
|
|
3478
|
+
const plain = plainEntity(this.core.plain, desc.name).get(id);
|
|
3479
|
+
if (plain !== void 0) this.create(desc, id, plain);
|
|
3480
|
+
}
|
|
3284
3481
|
// -------------------------------------------------------------------------
|
|
3285
3482
|
// Hibernation
|
|
3286
3483
|
// -------------------------------------------------------------------------
|
|
@@ -3418,6 +3615,9 @@ var Rapier2dRuntime = class {
|
|
|
3418
3615
|
/** See `PhysicsRuntime.skippedKeys`: entities whose body the room has switched off. */
|
|
3419
3616
|
skippedKeys = /* @__PURE__ */ new Set();
|
|
3420
3617
|
// ---- end M7 planned feature 3 (b) ----
|
|
3618
|
+
/** bugs #116: see `PhysicsRuntime.nonFinite`. */
|
|
3619
|
+
nonFinite;
|
|
3620
|
+
nextScratch = [];
|
|
3421
3621
|
// ---- M7 planned feature 3 (a): physics-only unsynced bodies ----
|
|
3422
3622
|
/** See `PhysicsRuntime.unsyncedBodies`: bodies the world steps that no schema row describes. */
|
|
3423
3623
|
unsyncedBodies = /* @__PURE__ */ new Map();
|
|
@@ -3429,6 +3629,7 @@ var Rapier2dRuntime = class {
|
|
|
3429
3629
|
this.core = core;
|
|
3430
3630
|
this.config = config;
|
|
3431
3631
|
this.rapier = rapier;
|
|
3632
|
+
this.nonFinite = new NonFiniteGuard(core);
|
|
3432
3633
|
this.collections = core.ext.collections.filter(
|
|
3433
3634
|
(c) => c.physics !== void 0
|
|
3434
3635
|
);
|
|
@@ -3471,6 +3672,7 @@ var Rapier2dRuntime = class {
|
|
|
3471
3672
|
}
|
|
3472
3673
|
free() {
|
|
3473
3674
|
this.bodies.clear();
|
|
3675
|
+
this.nonFinite.clear();
|
|
3474
3676
|
this.unsyncedBodies.clear();
|
|
3475
3677
|
this.world.free();
|
|
3476
3678
|
}
|
|
@@ -3648,6 +3850,7 @@ var Rapier2dRuntime = class {
|
|
|
3648
3850
|
this.bodies.delete(key);
|
|
3649
3851
|
this.sleepSynced.delete(key);
|
|
3650
3852
|
this.sourceRecords.delete(key);
|
|
3853
|
+
this.nonFinite.forget(key);
|
|
3651
3854
|
this.destroyBody(existing);
|
|
3652
3855
|
}
|
|
3653
3856
|
if (record !== void 0) this.create(desc, id, record);
|
|
@@ -3661,6 +3864,7 @@ var Rapier2dRuntime = class {
|
|
|
3661
3864
|
this.destroyBody(body);
|
|
3662
3865
|
}
|
|
3663
3866
|
for (const key of [...this.skippedKeys]) if (!live.has(key)) this.skippedKeys.delete(key);
|
|
3867
|
+
this.nonFinite.prune(live);
|
|
3664
3868
|
}
|
|
3665
3869
|
// -------------------------------------------------------------------------
|
|
3666
3870
|
// Step and sync
|
|
@@ -3702,13 +3906,47 @@ var Rapier2dRuntime = class {
|
|
|
3702
3906
|
vy: v.y,
|
|
3703
3907
|
angularVelocity: body.angvel()
|
|
3704
3908
|
};
|
|
3909
|
+
const nexts = this.nextScratch;
|
|
3910
|
+
nexts.length = 0;
|
|
3705
3911
|
for (const [channel, field] of physics.channels) {
|
|
3706
|
-
|
|
3912
|
+
nexts.push((rounders[field] ?? identity3)(channelOf2d2(channel, state)));
|
|
3913
|
+
}
|
|
3914
|
+
if (!(Number.isFinite(state.x) && Number.isFinite(state.y) && Number.isFinite(state.angle) && Number.isFinite(state.vx) && Number.isFinite(state.vy) && Number.isFinite(state.angularVelocity)) || !allFinite(nexts)) {
|
|
3915
|
+
const detail = describeNonFinite({
|
|
3916
|
+
...state,
|
|
3917
|
+
...Object.fromEntries(physics.channels.map(([, field], i2) => [field, nexts[i2] ?? 0]))
|
|
3918
|
+
});
|
|
3919
|
+
if (this.nonFinite.note(desc, id, key, detail) === "skip") {
|
|
3920
|
+
this.setSkipped(desc.name, id, true);
|
|
3921
|
+
} else {
|
|
3922
|
+
this.resetBody(desc, body, record);
|
|
3923
|
+
}
|
|
3924
|
+
continue;
|
|
3925
|
+
}
|
|
3926
|
+
let i = 0;
|
|
3927
|
+
for (const [, field] of physics.channels) {
|
|
3928
|
+
const next = nexts[i++];
|
|
3707
3929
|
if (record[field] !== next) record[field] = next;
|
|
3708
3930
|
}
|
|
3709
3931
|
}
|
|
3710
3932
|
}
|
|
3711
3933
|
}
|
|
3934
|
+
/** bugs #116: see `PhysicsRuntime.resetBody`. */
|
|
3935
|
+
resetBody(desc, body, record) {
|
|
3936
|
+
const t = body.translation();
|
|
3937
|
+
body.setTranslation(
|
|
3938
|
+
{ x: Number.isFinite(t.x) ? t.x : 0, y: Number.isFinite(t.y) ? t.y : 0 },
|
|
3939
|
+
true
|
|
3940
|
+
);
|
|
3941
|
+
if (!Number.isFinite(body.rotation())) body.setRotation(0, true);
|
|
3942
|
+
this.applyRecordToBody(desc, body, record);
|
|
3943
|
+
if (!body.isEnabled()) body.setEnabled(true);
|
|
3944
|
+
body.setLinvel({ x: 0, y: 0 }, true);
|
|
3945
|
+
body.setAngvel(0, true);
|
|
3946
|
+
body.resetForces(true);
|
|
3947
|
+
body.resetTorques(true);
|
|
3948
|
+
zeroVelocityFields(desc, record);
|
|
3949
|
+
}
|
|
3712
3950
|
// -------------------------------------------------------------------------
|
|
3713
3951
|
// Hibernation
|
|
3714
3952
|
// -------------------------------------------------------------------------
|
|
@@ -3916,6 +4154,9 @@ var CustomRuntime = class {
|
|
|
3916
4154
|
sourceRecords = /* @__PURE__ */ new Map();
|
|
3917
4155
|
skippedKeys = /* @__PURE__ */ new Set();
|
|
3918
4156
|
scratch = emptyPose();
|
|
4157
|
+
/** bugs #116: see `PhysicsRuntime.nonFinite`. */
|
|
4158
|
+
nonFinite;
|
|
4159
|
+
nextScratch = [];
|
|
3919
4160
|
stepper;
|
|
3920
4161
|
/**
|
|
3921
4162
|
* M7 pf3 (a): a custom room's unsynced bodies are the stepper's own — it created them, it steps
|
|
@@ -3929,6 +4170,7 @@ var CustomRuntime = class {
|
|
|
3929
4170
|
if (!config) throw new Error("CustomRuntime: the room config declares no physics");
|
|
3930
4171
|
this.core = core;
|
|
3931
4172
|
this.config = config;
|
|
4173
|
+
this.nonFinite = new NonFiniteGuard(core);
|
|
3932
4174
|
this.collections = core.ext.collections.filter(
|
|
3933
4175
|
(c) => c.physics !== void 0
|
|
3934
4176
|
);
|
|
@@ -3963,6 +4205,7 @@ var CustomRuntime = class {
|
|
|
3963
4205
|
this.bodies.clear();
|
|
3964
4206
|
this.unsyncedBodies.clear();
|
|
3965
4207
|
this.sourceRecords.clear();
|
|
4208
|
+
this.nonFinite.clear();
|
|
3966
4209
|
this.stepper = void 0;
|
|
3967
4210
|
}
|
|
3968
4211
|
/** `room.physicsCustom.stepper`. Throws rather than handing back `undefined` half a world. */
|
|
@@ -4071,6 +4314,7 @@ var CustomRuntime = class {
|
|
|
4071
4314
|
if (source === record) continue;
|
|
4072
4315
|
this.bodies.delete(key);
|
|
4073
4316
|
this.sourceRecords.delete(key);
|
|
4317
|
+
this.nonFinite.forget(key);
|
|
4074
4318
|
this.stepper?.removeBody(existing);
|
|
4075
4319
|
}
|
|
4076
4320
|
if (record !== void 0) this.create(desc, id, record);
|
|
@@ -4083,6 +4327,7 @@ var CustomRuntime = class {
|
|
|
4083
4327
|
this.stepper?.removeBody(body);
|
|
4084
4328
|
}
|
|
4085
4329
|
for (const key of [...this.skippedKeys]) if (!live.has(key)) this.skippedKeys.delete(key);
|
|
4330
|
+
this.nonFinite.prune(live);
|
|
4086
4331
|
}
|
|
4087
4332
|
// -------------------------------------------------------------------------
|
|
4088
4333
|
// Step and sync
|
|
@@ -4108,20 +4353,60 @@ var CustomRuntime = class {
|
|
|
4108
4353
|
const plainColl = plainEntity(this.core.plain, desc.name);
|
|
4109
4354
|
const rounders = roundersFor4(desc);
|
|
4110
4355
|
for (const id of plainColl.ids()) {
|
|
4111
|
-
const
|
|
4356
|
+
const key = bodyKey4(desc.name, id);
|
|
4357
|
+
const body = this.bodies.get(key);
|
|
4112
4358
|
if (body === void 0) continue;
|
|
4113
4359
|
const record = tracked.get(id);
|
|
4114
4360
|
if (!record) continue;
|
|
4115
4361
|
const pose = this.scratch;
|
|
4116
4362
|
stepper.readPose(body, pose);
|
|
4363
|
+
const nexts = this.nextScratch;
|
|
4364
|
+
nexts.length = 0;
|
|
4117
4365
|
for (const [channel, field] of physics.channels) {
|
|
4118
|
-
|
|
4366
|
+
nexts.push((rounders[field] ?? identity4)(pose[channel]));
|
|
4367
|
+
}
|
|
4368
|
+
if (!poseFinite2(pose) || !allFinite(nexts)) {
|
|
4369
|
+
const detail = describeNonFinite({
|
|
4370
|
+
...pose,
|
|
4371
|
+
...Object.fromEntries(physics.channels.map(([, field], i2) => [field, nexts[i2] ?? 0]))
|
|
4372
|
+
});
|
|
4373
|
+
if (this.nonFinite.note(desc, id, key, detail) === "skip") {
|
|
4374
|
+
this.setSkipped(desc.name, id, true);
|
|
4375
|
+
} else {
|
|
4376
|
+
this.resetBody(stepper, desc, body, record);
|
|
4377
|
+
}
|
|
4378
|
+
continue;
|
|
4379
|
+
}
|
|
4380
|
+
let i = 0;
|
|
4381
|
+
for (const [, field] of physics.channels) {
|
|
4382
|
+
const next = nexts[i++];
|
|
4119
4383
|
if (record[field] !== next) record[field] = next;
|
|
4120
4384
|
}
|
|
4121
4385
|
}
|
|
4122
4386
|
}
|
|
4123
4387
|
}
|
|
4388
|
+
/**
|
|
4389
|
+
* bugs #116: see `PhysicsRuntime.resetBody`. The pose is built from the row's mapped channels,
|
|
4390
|
+
* with the identity rotation and the origin for unmapped ones and zero for every velocity; a
|
|
4391
|
+
* stepper keeps its own forces, if it has any, where this module cannot see them.
|
|
4392
|
+
*/
|
|
4393
|
+
resetBody(stepper, desc, body, record) {
|
|
4394
|
+
const pose = emptyPose();
|
|
4395
|
+
for (const [channel, field] of desc.physics?.channels ?? []) {
|
|
4396
|
+
if (VELOCITY_CHANNELS2.has(channel)) continue;
|
|
4397
|
+
const raw = record[field];
|
|
4398
|
+
if (typeof raw === "number" && Number.isFinite(raw)) pose[channel] = raw;
|
|
4399
|
+
}
|
|
4400
|
+
stepper.writePose(body, pose);
|
|
4401
|
+
zeroVelocityFields(desc, record);
|
|
4402
|
+
}
|
|
4124
4403
|
};
|
|
4404
|
+
var VELOCITY_CHANNELS2 = /* @__PURE__ */ new Set(["vx", "vy", "vz", "wx", "wy", "wz"]);
|
|
4405
|
+
var POSE_CHANNELS = Object.keys(emptyPose());
|
|
4406
|
+
function poseFinite2(pose) {
|
|
4407
|
+
for (const channel of POSE_CHANNELS) if (!Number.isFinite(pose[channel])) return false;
|
|
4408
|
+
return true;
|
|
4409
|
+
}
|
|
4125
4410
|
function identity4(v) {
|
|
4126
4411
|
return v;
|
|
4127
4412
|
}
|
|
@@ -7171,7 +7456,9 @@ var RoomCore = class _RoomCore {
|
|
|
7171
7456
|
get presence() {
|
|
7172
7457
|
return this.anyState[PRESENCE_COLLECTION];
|
|
7173
7458
|
}
|
|
7174
|
-
|
|
7459
|
+
/** Issue #123: clients already told that their requested role was ignored. Warn once each. */
|
|
7460
|
+
unverifiedRoleWarned = /* @__PURE__ */ new Set();
|
|
7461
|
+
resolveRole(clientId, requested, existing, verified) {
|
|
7175
7462
|
const roles = this.definition.schema.roles;
|
|
7176
7463
|
if (requested === void 0) return existing ?? roles[0] ?? "";
|
|
7177
7464
|
if (roles.length === 0) {
|
|
@@ -7183,6 +7470,17 @@ var RoomCore = class _RoomCore {
|
|
|
7183
7470
|
}
|
|
7184
7471
|
return "";
|
|
7185
7472
|
}
|
|
7473
|
+
if (!verified && this.definition.schema.clientRoles !== true) {
|
|
7474
|
+
const granted2 = existing ?? roles[0] ?? "";
|
|
7475
|
+
if (requested !== "" && requested !== granted2 && !this.unverifiedRoleWarned.has(clientId)) {
|
|
7476
|
+
this.unverifiedRoleWarned.add(clientId);
|
|
7477
|
+
this.log(
|
|
7478
|
+
"warn",
|
|
7479
|
+
`join: ignored the client-requested role ${JSON.stringify(requested)}; using ${JSON.stringify(granted2)}. Roles come from a JWT role claim or room.setRole; set clientRoles: true in defineSchema to let clients pick`
|
|
7480
|
+
);
|
|
7481
|
+
}
|
|
7482
|
+
return granted2;
|
|
7483
|
+
}
|
|
7186
7484
|
if (roles.includes(requested)) return requested;
|
|
7187
7485
|
this.log(
|
|
7188
7486
|
"warn",
|
|
@@ -7225,7 +7523,10 @@ var RoomCore = class _RoomCore {
|
|
|
7225
7523
|
const ctx = {
|
|
7226
7524
|
clientId,
|
|
7227
7525
|
subject,
|
|
7228
|
-
role: this.resolveRole(options.role, void 0),
|
|
7526
|
+
role: this.resolveRole(clientId, options.role, void 0, options.roleVerified === true),
|
|
7527
|
+
// Issue #123: which credential admitted this join, so a policy can tell anonymous from
|
|
7528
|
+
// verified.
|
|
7529
|
+
auth: options.auth ?? "key",
|
|
7229
7530
|
name: options.name ?? "",
|
|
7230
7531
|
tick: this.tick,
|
|
7231
7532
|
// Bug #133: no `ClientEntry` exists yet, so the flag comes straight from the join options —
|
|
@@ -7283,7 +7584,12 @@ var RoomCore = class _RoomCore {
|
|
|
7283
7584
|
const existing = this.clients.get(clientId);
|
|
7284
7585
|
if (this.stopped) {
|
|
7285
7586
|
this.log("warn", `join(${clientId}) after stop(); ignored`);
|
|
7286
|
-
const role2 = this.resolveRole(
|
|
7587
|
+
const role2 = this.resolveRole(
|
|
7588
|
+
clientId,
|
|
7589
|
+
options.role,
|
|
7590
|
+
existing?.role,
|
|
7591
|
+
options.roleVerified === true
|
|
7592
|
+
);
|
|
7287
7593
|
return {
|
|
7288
7594
|
tick: this.tick,
|
|
7289
7595
|
role: role2,
|
|
@@ -7304,7 +7610,12 @@ var RoomCore = class _RoomCore {
|
|
|
7304
7610
|
throw new RoomFullError(maxClients);
|
|
7305
7611
|
}
|
|
7306
7612
|
const reconnecting = options.reconnecting === true;
|
|
7307
|
-
const role = this.resolveRole(
|
|
7613
|
+
const role = this.resolveRole(
|
|
7614
|
+
clientId,
|
|
7615
|
+
options.role,
|
|
7616
|
+
existing?.role,
|
|
7617
|
+
options.roleVerified === true
|
|
7618
|
+
);
|
|
7308
7619
|
const name = options.name ?? existing?.name ?? "";
|
|
7309
7620
|
const record = presence.get(clientId);
|
|
7310
7621
|
if (record) {
|
|
@@ -7335,6 +7646,7 @@ var RoomCore = class _RoomCore {
|
|
|
7335
7646
|
playerId: options.playerId ?? clientId,
|
|
7336
7647
|
// M8 lane 5: the narrower fact beside the one above. See `JoinOptions.subject`.
|
|
7337
7648
|
subject: options.subject,
|
|
7649
|
+
auth: options.auth ?? "key",
|
|
7338
7650
|
npc: options.npc === true,
|
|
7339
7651
|
role,
|
|
7340
7652
|
name,
|
|
@@ -7419,6 +7731,7 @@ var RoomCore = class _RoomCore {
|
|
|
7419
7731
|
this.rpcTraceRing?.forget(clientId);
|
|
7420
7732
|
this.loop.dropFramesFor(clientId);
|
|
7421
7733
|
this.clients.delete(clientId);
|
|
7734
|
+
this.unverifiedRoleWarned.delete(clientId);
|
|
7422
7735
|
this.stats.bytesOutByClient.delete(clientId);
|
|
7423
7736
|
this.invalidateClients();
|
|
7424
7737
|
this.eventFlush();
|
|
@@ -7466,6 +7779,9 @@ var RoomCore = class _RoomCore {
|
|
|
7466
7779
|
return {
|
|
7467
7780
|
clientId,
|
|
7468
7781
|
playerId: entry?.playerId ?? clientId,
|
|
7782
|
+
// Issue #123: read from the seat, so every ctx reports the credential the seat was taken
|
|
7783
|
+
// with.
|
|
7784
|
+
auth: entry?.auth ?? "key",
|
|
7469
7785
|
role: entry?.role ?? "",
|
|
7470
7786
|
name: entry?.name ?? "",
|
|
7471
7787
|
tick: this.tick,
|
|
@@ -528,8 +528,25 @@ interface RoomCoreOptions {
|
|
|
528
528
|
readonly wallNow?: () => number;
|
|
529
529
|
}
|
|
530
530
|
interface JoinOptions {
|
|
531
|
-
/**
|
|
531
|
+
/**
|
|
532
|
+
* Requested role; unknown/missing → `roles[0]` (logged) or `''` when the schema has no roles.
|
|
533
|
+
*
|
|
534
|
+
* Issue #123: honoured only when `roleVerified` is true or the schema set `clientRoles: true`.
|
|
535
|
+
* Otherwise an unverified request is ignored: a new seat gets `roles[0]` and a rejoining one
|
|
536
|
+
* keeps the role it already holds.
|
|
537
|
+
*/
|
|
532
538
|
readonly role?: string;
|
|
539
|
+
/**
|
|
540
|
+
* Issue #123: `role` was asserted by something the host verified (a JWT `role` claim, a resume
|
|
541
|
+
* token it signed, a role room code granted, an NPC the room spawned) rather than typed by the
|
|
542
|
+
* client. Absent means unverified.
|
|
543
|
+
*/
|
|
544
|
+
readonly roleVerified?: boolean;
|
|
545
|
+
/**
|
|
546
|
+
* Issue #123: which credential admitted this join, surfaced as `ctx.auth`. Defaults to `'key'`,
|
|
547
|
+
* an anonymous project-key join.
|
|
548
|
+
*/
|
|
549
|
+
readonly auth?: 'key' | 'jwt' | 'assertion';
|
|
533
550
|
readonly name?: string;
|
|
534
551
|
/** A resumed session: presence record kept, `ctx.reconnecting = true`. */
|
|
535
552
|
readonly reconnecting?: boolean;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { A as AdmissionError, D as DEFAULT_RECORDS_PER_COLLECTION, a as DEFAULT_TIMELINE_MAX_RECORDS, b as DEFAULT_TIMELINE_MAX_TICKS, E as EVENT_RING_SIZE, H as HOST_CALL_TIMEOUT_MS, c as HostCall, d as HostCallResult, J as JoinOptions, e as JoinResult, L as LogLevel, P as PHYSICS_STEP_EMA_ALPHA, R as RELAY_RECORDS_PER_COLLECTION, f as RELAY_STATE_BYTES, g as RELAY_WRITES_PER_SECOND, h as RELAY_WRITE_CAPS, i as RPC_TRACE_SIZE, j as RPC_TRACE_SUMMARY_MAX, k as RoomCoreApi, l as RoomCoreOptions, m as RoomEvent, n as RoomEventKind, o as RoomFullError, p as RoomHost, q as RoomInspection, r as RoomStats, s as RpcOutcome, t as RpcTraceDump, u as RpcTraceEntry, v as RpcTraceRing, S as StampLag, T as TimelineDump, w as TimelineFrame, x as TimelineRecorder, y as TimelineRecorderOptions, W as WriteCaps, z as inspectState, B as summarizeRpcResult } from './contract-
|
|
2
|
-
export { C as CRASH_AFTER_THROWS, M as MAX_CATCHUP, a as MatterBodyRecord, b as MatterSection, c as Mulberry32, P as PhysicsEngineTag, d as PhysicsSection, R as RoomCore, e as decodeMatterBodies, f as decodeMatterSectionEnvelope, g as decodePhysicsSection, h as decodeRapier2dSectionEnvelope, i as encodeMatterBodies, j as encodeMatterSectionEnvelope, k as encodePhysicsSection, l as encodeRapier2dSectionEnvelope, m as initMatter, n as initPhysics, o as initRapier2d, p as loadedMatter, q as loadedPhysics, r as loadedRapier2d, s as physicsSectionEngine, t as resetMatterForTests, u as resetPhysicsForTests, v as resetRapier2dForTests } from './room-
|
|
1
|
+
export { A as AdmissionError, D as DEFAULT_RECORDS_PER_COLLECTION, a as DEFAULT_TIMELINE_MAX_RECORDS, b as DEFAULT_TIMELINE_MAX_TICKS, E as EVENT_RING_SIZE, H as HOST_CALL_TIMEOUT_MS, c as HostCall, d as HostCallResult, J as JoinOptions, e as JoinResult, L as LogLevel, P as PHYSICS_STEP_EMA_ALPHA, R as RELAY_RECORDS_PER_COLLECTION, f as RELAY_STATE_BYTES, g as RELAY_WRITES_PER_SECOND, h as RELAY_WRITE_CAPS, i as RPC_TRACE_SIZE, j as RPC_TRACE_SUMMARY_MAX, k as RoomCoreApi, l as RoomCoreOptions, m as RoomEvent, n as RoomEventKind, o as RoomFullError, p as RoomHost, q as RoomInspection, r as RoomStats, s as RpcOutcome, t as RpcTraceDump, u as RpcTraceEntry, v as RpcTraceRing, S as StampLag, T as TimelineDump, w as TimelineFrame, x as TimelineRecorder, y as TimelineRecorderOptions, W as WriteCaps, z as inspectState, B as summarizeRpcResult } from './contract-CcZadpC6.js';
|
|
2
|
+
export { C as CRASH_AFTER_THROWS, M as MAX_CATCHUP, a as MatterBodyRecord, b as MatterSection, c as Mulberry32, P as PhysicsEngineTag, d as PhysicsSection, R as RoomCore, e as decodeMatterBodies, f as decodeMatterSectionEnvelope, g as decodePhysicsSection, h as decodeRapier2dSectionEnvelope, i as encodeMatterBodies, j as encodeMatterSectionEnvelope, k as encodePhysicsSection, l as encodeRapier2dSectionEnvelope, m as initMatter, n as initPhysics, o as initRapier2d, p as loadedMatter, q as loadedPhysics, r as loadedRapier2d, s as physicsSectionEngine, t as resetMatterForTests, u as resetPhysicsForTests, v as resetRapier2dForTests } from './room-BW2ZSX_k.js';
|
|
3
3
|
import { CollectionDesc, AnySchema, PlainState, DirtySet } from '@irtio/schema';
|
|
4
4
|
import '@irtio/protocol';
|
|
5
5
|
import '@irtio/server';
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
fromMigrationState,
|
|
3
3
|
migrateSnapshot,
|
|
4
4
|
toMigrationState
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-NSUQKWYO.js";
|
|
6
6
|
import {
|
|
7
7
|
AdmissionError,
|
|
8
8
|
CRASH_AFTER_THROWS,
|
|
@@ -59,7 +59,7 @@ import {
|
|
|
59
59
|
visibleNames,
|
|
60
60
|
visibleTo,
|
|
61
61
|
writeHibernationBlob
|
|
62
|
-
} from "./chunk-
|
|
62
|
+
} from "./chunk-RLY4VAMM.js";
|
|
63
63
|
export {
|
|
64
64
|
AdmissionError,
|
|
65
65
|
CRASH_AFTER_THROWS,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { PlatformNotice, AttributeOptions, ProfileSnapshot } from '@irtio/protocol';
|
|
2
2
|
import { DirtySet, AnySchema, PlainState, Tracked, State } from '@irtio/schema';
|
|
3
3
|
import { RoomDefinition, RoomMode, Room, RewindView, Ctx, CustomStepper, CustomPose, MatterModule, MatterEngine, MatterBody, RapierModule, RapierWorld, RapierRigidBody, Rapier2dModule, Rapier2dWorld, Rapier2dRigidBody, ResolvedRoomConfig, LeaveReason } from '@irtio/server';
|
|
4
|
-
import { p as RoomHost, r as RoomStats, u as RpcTraceEntry, L as LogLevel, k as RoomCoreApi, l as RoomCoreOptions, n as RoomEventKind, y as TimelineRecorderOptions, T as TimelineDump, t as RpcTraceDump, q as RoomInspection, J as JoinOptions, e as JoinResult, d as HostCallResult } from './contract-
|
|
4
|
+
import { p as RoomHost, r as RoomStats, u as RpcTraceEntry, L as LogLevel, k as RoomCoreApi, l as RoomCoreOptions, n as RoomEventKind, y as TimelineRecorderOptions, T as TimelineDump, t as RpcTraceDump, q as RoomInspection, J as JoinOptions, e as JoinResult, d as HostCallResult } from './contract-CcZadpC6.js';
|
|
5
5
|
|
|
6
6
|
declare class Mulberry32 {
|
|
7
7
|
/** Current internal state (u32). Survives hibernation. */
|
|
@@ -37,6 +37,8 @@ interface ClientEntry {
|
|
|
37
37
|
* data rather than a caveat in a doc.
|
|
38
38
|
*/
|
|
39
39
|
readonly subject: string | undefined;
|
|
40
|
+
/** Issue #123: the credential kind of the first join, kept across reconnects like `subject`. */
|
|
41
|
+
readonly auth: 'key' | 'jwt' | 'assertion';
|
|
40
42
|
role: string;
|
|
41
43
|
name: string;
|
|
42
44
|
connected: boolean;
|
|
@@ -336,6 +338,9 @@ declare class CustomRuntime {
|
|
|
336
338
|
private readonly sourceRecords;
|
|
337
339
|
private readonly skippedKeys;
|
|
338
340
|
private readonly scratch;
|
|
341
|
+
/** bugs #116: see `PhysicsRuntime.nonFinite`. */
|
|
342
|
+
private readonly nonFinite;
|
|
343
|
+
private readonly nextScratch;
|
|
339
344
|
private stepper;
|
|
340
345
|
/**
|
|
341
346
|
* M7 pf3 (a): a custom room's unsynced bodies are the stepper's own — it created them, it steps
|
|
@@ -372,6 +377,12 @@ declare class CustomRuntime {
|
|
|
372
377
|
step(): void;
|
|
373
378
|
/** Body → schema, through the tracked proxies, `Math.fround`ed for f32 fields as Rapier's is. */
|
|
374
379
|
sync(): void;
|
|
380
|
+
/**
|
|
381
|
+
* bugs #116: see `PhysicsRuntime.resetBody`. The pose is built from the row's mapped channels,
|
|
382
|
+
* with the identity rotation and the origin for unmapped ones and zero for every velocity; a
|
|
383
|
+
* stepper keeps its own forces, if it has any, where this module cannot see them.
|
|
384
|
+
*/
|
|
385
|
+
private resetBody;
|
|
375
386
|
}
|
|
376
387
|
|
|
377
388
|
/**
|
|
@@ -640,6 +651,9 @@ declare class MatterRuntime {
|
|
|
640
651
|
private readonly warnedChannels;
|
|
641
652
|
/** See `PhysicsRuntime.skippedKeys`: entities whose body the room has switched off. */
|
|
642
653
|
private readonly skippedKeys;
|
|
654
|
+
/** bugs #116: see `PhysicsRuntime.nonFinite`. */
|
|
655
|
+
private readonly nonFinite;
|
|
656
|
+
private readonly nextScratch;
|
|
643
657
|
private readonly unsyncedBodies;
|
|
644
658
|
/** Saved unsynced state, consumed as `setup` rebuilds each body under the same key. */
|
|
645
659
|
private restoreUnsynced;
|
|
@@ -680,6 +694,15 @@ declare class MatterRuntime {
|
|
|
680
694
|
reconcile(): void;
|
|
681
695
|
step(): void;
|
|
682
696
|
sync(): void;
|
|
697
|
+
/**
|
|
698
|
+
* bugs #116: the matter2d half of `PhysicsRuntime.resetBody`, done as a rebuild rather than a
|
|
699
|
+
* teleport. matter moves a body by translating its vertices by the difference between the new
|
|
700
|
+
* position and the old one, and once the old one is not finite that difference is not either:
|
|
701
|
+
* `Body.setPosition` cannot bring the vertices back. So the body is removed and built again
|
|
702
|
+
* from its row, whose pose is the last finite one `sync()` wrote, with the row's velocity
|
|
703
|
+
* fields zeroed first so the new body spawns at rest (and with no force, being new).
|
|
704
|
+
*/
|
|
705
|
+
private resetBody;
|
|
683
706
|
serialize(): MatterSection;
|
|
684
707
|
}
|
|
685
708
|
|
|
@@ -845,6 +868,10 @@ declare class PhysicsRuntime {
|
|
|
845
868
|
* why it is spelled as "no body" rather than as "a body that is not stepped".
|
|
846
869
|
*/
|
|
847
870
|
private readonly skippedKeys;
|
|
871
|
+
/** bugs #116: what `sync()` does with a body that went non-finite. See `core/body-guard.ts`. */
|
|
872
|
+
private readonly nonFinite;
|
|
873
|
+
/** The mapped channel values of the body `sync()` is on, reused so the good path allocates nothing. */
|
|
874
|
+
private readonly nextScratch;
|
|
848
875
|
/**
|
|
849
876
|
* Bodies with no schema row: rope anchors, dead-reckoned proxies, one-off sensors. Keyed by the
|
|
850
877
|
* room's own string rather than by `collection\0id`, because there is no collection and no id.
|
|
@@ -928,6 +955,12 @@ declare class PhysicsRuntime {
|
|
|
928
955
|
* carries — and so a field that has not really moved does not re-dirty every tick.
|
|
929
956
|
*/
|
|
930
957
|
sync(): void;
|
|
958
|
+
/**
|
|
959
|
+
* bugs #116: puts a non-finite body back at its row's pose (the last one `sync()` wrote, always
|
|
960
|
+
* finite), at rest, with no force left on it. Unmapped channels the engine made non-finite go to
|
|
961
|
+
* zero, or the identity rotation, since there is nothing better to put them back to.
|
|
962
|
+
*/
|
|
963
|
+
private resetBody;
|
|
931
964
|
serialize(): PhysicsSection;
|
|
932
965
|
}
|
|
933
966
|
|
|
@@ -1009,6 +1042,9 @@ declare class Rapier2dRuntime {
|
|
|
1009
1042
|
private readonly warnedChannels;
|
|
1010
1043
|
/** See `PhysicsRuntime.skippedKeys`: entities whose body the room has switched off. */
|
|
1011
1044
|
private readonly skippedKeys;
|
|
1045
|
+
/** bugs #116: see `PhysicsRuntime.nonFinite`. */
|
|
1046
|
+
private readonly nonFinite;
|
|
1047
|
+
private readonly nextScratch;
|
|
1012
1048
|
/** See `PhysicsRuntime.unsyncedBodies`: bodies the world steps that no schema row describes. */
|
|
1013
1049
|
private readonly unsyncedBodies;
|
|
1014
1050
|
readonly unsynced: UnsyncedRegistry;
|
|
@@ -1058,6 +1094,8 @@ declare class Rapier2dRuntime {
|
|
|
1058
1094
|
* Values are `Math.fround`ed for f32 fields, so room code reads exactly what the wire carries.
|
|
1059
1095
|
*/
|
|
1060
1096
|
sync(): void;
|
|
1097
|
+
/** bugs #116: see `PhysicsRuntime.resetBody`. */
|
|
1098
|
+
private resetBody;
|
|
1061
1099
|
/**
|
|
1062
1100
|
* The same `PhysicsSection` the 3D runtime writes — a real engine snapshot plus the
|
|
1063
1101
|
* entity↔handle map, whose handles must ride as f64 for the reason spelled out on
|
|
@@ -1289,6 +1327,8 @@ declare class RoomCore<S extends AnySchema = AnySchema> implements RoomCoreApi<S
|
|
|
1289
1327
|
lastSerializeTimerAt: number | undefined;
|
|
1290
1328
|
serialize(): Uint8Array;
|
|
1291
1329
|
private get presence();
|
|
1330
|
+
/** Issue #123: clients already told that their requested role was ignored. Warn once each. */
|
|
1331
|
+
private readonly unverifiedRoleWarned;
|
|
1292
1332
|
private resolveRole;
|
|
1293
1333
|
/**
|
|
1294
1334
|
* Subjects `room.ban` has excluded, and when each stops being excluded. Values are wall-clock
|
package/dist/test/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { R as RoomCore } from '../room-
|
|
2
|
-
export { m as initMatter, n as initPhysics, o as initRapier2d } from '../room-
|
|
1
|
+
import { R as RoomCore } from '../room-BW2ZSX_k.js';
|
|
2
|
+
export { m as initMatter, n as initPhysics, o as initRapier2d } from '../room-BW2ZSX_k.js';
|
|
3
3
|
import { ErrorCodeName, FrameType } from '@irtio/protocol';
|
|
4
4
|
import { NpcConfig, LeaveReason, Room, RoomDefinition } from '@irtio/server';
|
|
5
|
-
import { p as RoomHost, L as LogLevel, k as RoomCoreApi, c as HostCall, d as HostCallResult, W as WriteCaps, r as RoomStats } from '../contract-
|
|
5
|
+
import { p as RoomHost, L as LogLevel, k as RoomCoreApi, c as HostCall, d as HostCallResult, W as WriteCaps, r as RoomStats } from '../contract-CcZadpC6.js';
|
|
6
6
|
import { AnySchema, PlainState, EntityCollection, State } from '@irtio/schema';
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -374,6 +374,14 @@ interface JoinSpec {
|
|
|
374
374
|
* the relay host reads from the store for an identified player, without a store or a socket.
|
|
375
375
|
*/
|
|
376
376
|
readonly persisted?: ReadonlyMap<string, Readonly<Record<string, unknown>>>;
|
|
377
|
+
/**
|
|
378
|
+
* Issue #123: `JoinOptions.roleVerified`. Defaults to `true` here: this harness stands where the
|
|
379
|
+
* host stands and its tests are about room behaviour, so `role` is a grant unless a test sets
|
|
380
|
+
* `false` to exercise the unverified path.
|
|
381
|
+
*/
|
|
382
|
+
readonly roleVerified?: boolean;
|
|
383
|
+
/** Issue #123: `JoinOptions.auth`, the credential kind `ctx.auth` reports. Default `'key'`. */
|
|
384
|
+
readonly auth?: 'key' | 'jwt' | 'assertion';
|
|
377
385
|
}
|
|
378
386
|
interface UntilOptions {
|
|
379
387
|
/** Default 1000. */
|
package/dist/test/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
initRapier2d,
|
|
7
7
|
isPerClient,
|
|
8
8
|
visibleNames
|
|
9
|
-
} from "../chunk-
|
|
9
|
+
} from "../chunk-RLY4VAMM.js";
|
|
10
10
|
|
|
11
11
|
// src/test/clock.ts
|
|
12
12
|
var FakeClock = class {
|
|
@@ -993,8 +993,10 @@ var Harness = class {
|
|
|
993
993
|
...spec.subject !== void 0 ? { subject: spec.subject } : {},
|
|
994
994
|
// ---- end M8 lane 5 ----
|
|
995
995
|
// ---- M8 proposal 13 ----
|
|
996
|
-
...spec.persisted !== void 0 ? { persisted: spec.persisted } : {}
|
|
996
|
+
...spec.persisted !== void 0 ? { persisted: spec.persisted } : {},
|
|
997
997
|
// ---- end M8 proposal 13 ----
|
|
998
|
+
...spec.roleVerified !== false ? { roleVerified: true } : {},
|
|
999
|
+
...spec.auth !== void 0 ? { auth: spec.auth } : {}
|
|
998
1000
|
};
|
|
999
1001
|
const client = new FakeClientImpl(this.bridge, id, options);
|
|
1000
1002
|
this.byId.set(id, client);
|
package/dist/worker/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { L as LogLevel, t as RpcTraceDump, T as TimelineDump, r as RoomStats, c as HostCall, d as HostCallResult, k as RoomCoreApi, p as RoomHost } from '../contract-
|
|
1
|
+
import { L as LogLevel, t as RpcTraceDump, T as TimelineDump, r as RoomStats, c as HostCall, d as HostCallResult, k as RoomCoreApi, p as RoomHost } from '../contract-CcZadpC6.js';
|
|
2
2
|
import { ErrorCodeName, ProfileSnapshot, PlatformNotice } from '@irtio/protocol';
|
|
3
3
|
import { NpcConfig, LeaveReason } from '@irtio/server';
|
|
4
4
|
import '@irtio/schema';
|
|
@@ -54,6 +54,10 @@ type ToWorker = {
|
|
|
54
54
|
/** M8 lane 5: the verified subject, absent for a key join. See `JoinOptions.subject` — it is
|
|
55
55
|
* deliberately narrower than `playerId`, which always has a value. */
|
|
56
56
|
subject?: string;
|
|
57
|
+
/** Issue #123: `JoinOptions.auth`, the credential kind. Absent means `'key'`. */
|
|
58
|
+
auth?: 'key' | 'jwt' | 'assertion';
|
|
59
|
+
/** Issue #123: `JoinOptions.roleVerified`. Absent means the role is a client request. */
|
|
60
|
+
roleVerified?: boolean;
|
|
57
61
|
} | {
|
|
58
62
|
t: 'leave';
|
|
59
63
|
clientId: string;
|
|
@@ -304,10 +308,17 @@ type FromWorker = {
|
|
|
304
308
|
* what cap it handed out to know how close the room is to it. Absent when the worker's
|
|
305
309
|
* V8 could not be sampled, which the supervisor must report as unread rather than as
|
|
306
310
|
* zero, for the same reason `ticks`/`overruns` are optional.
|
|
311
|
+
*
|
|
312
|
+
* bugs.md #146: `externalBytes` is the off-heap half — `ArrayBuffer` backing stores, WASM
|
|
313
|
+
* memory, `Buffer`s — which neither `usedBytes` nor the old-generation cap above it covers,
|
|
314
|
+
* and which is therefore the half a room can overshoot its whole budget with while every
|
|
315
|
+
* number the platform was reading stays small. Optional on the wire because an older worker
|
|
316
|
+
* does not send it, and because "V8 did not report it" is not "this room holds none".
|
|
307
317
|
*/
|
|
308
318
|
heap?: {
|
|
309
319
|
usedBytes: number;
|
|
310
320
|
limitBytes: number;
|
|
321
|
+
externalBytes?: number;
|
|
311
322
|
};
|
|
312
323
|
/**
|
|
313
324
|
* D56: `true` once any Rapier world in this worker process has stepped, which is the point
|
package/dist/worker/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
migrateSnapshot
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-NSUQKWYO.js";
|
|
4
4
|
import {
|
|
5
5
|
AdmissionError,
|
|
6
6
|
RoomCore,
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
initRapier2d,
|
|
11
11
|
onFirstRapierStep,
|
|
12
12
|
rapierHasStepped
|
|
13
|
-
} from "../chunk-
|
|
13
|
+
} from "../chunk-RLY4VAMM.js";
|
|
14
14
|
|
|
15
15
|
// src/worker/index.ts
|
|
16
16
|
import { getHeapStatistics } from "v8";
|
|
@@ -121,7 +121,12 @@ function heapSample() {
|
|
|
121
121
|
const s = getHeapStatistics();
|
|
122
122
|
if (!Number.isFinite(s.used_heap_size) || !Number.isFinite(s.heap_size_limit)) return void 0;
|
|
123
123
|
if (s.heap_size_limit <= 0) return void 0;
|
|
124
|
-
|
|
124
|
+
const external = s.external_memory;
|
|
125
|
+
return {
|
|
126
|
+
usedBytes: s.used_heap_size,
|
|
127
|
+
limitBytes: s.heap_size_limit,
|
|
128
|
+
...typeof external === "number" && Number.isFinite(external) && external >= 0 ? { externalBytes: external } : {}
|
|
129
|
+
};
|
|
125
130
|
} catch {
|
|
126
131
|
return void 0;
|
|
127
132
|
}
|
|
@@ -265,7 +270,10 @@ async function handleMessage(state, host, post, msg) {
|
|
|
265
270
|
...msg.npc === true ? { npc: true } : {},
|
|
266
271
|
// M8 lane 5: absent for a key join, and absent is the answer `onAdmit` and `room.ban`
|
|
267
272
|
// both need to be able to see.
|
|
268
|
-
...msg.subject !== void 0 ? { subject: msg.subject } : {}
|
|
273
|
+
...msg.subject !== void 0 ? { subject: msg.subject } : {},
|
|
274
|
+
// Issue #123: without these the room would treat every role as a client request.
|
|
275
|
+
...msg.auth !== void 0 ? { auth: msg.auth } : {},
|
|
276
|
+
...msg.roleVerified === true ? { roleVerified: true } : {}
|
|
269
277
|
};
|
|
270
278
|
const result = core.join(msg.clientId, joinOptions);
|
|
271
279
|
const snapshot = ownBuffer(result.snapshot);
|
|
@@ -463,6 +471,15 @@ if (!isMainThread && parentPort) {
|
|
|
463
471
|
]
|
|
464
472
|
});
|
|
465
473
|
});
|
|
474
|
+
process.on("uncaughtException", (err) => {
|
|
475
|
+
post({
|
|
476
|
+
t: "log",
|
|
477
|
+
level: "error",
|
|
478
|
+
args: ["worker: uncaught exception", err?.stack ?? String(err)]
|
|
479
|
+
});
|
|
480
|
+
post({ t: "crashed", reason: `uncaught exception: ${err?.message ?? String(err)}` });
|
|
481
|
+
process.exit(1);
|
|
482
|
+
});
|
|
466
483
|
const host = createWorkerHost(post);
|
|
467
484
|
const state = { core: void 0 };
|
|
468
485
|
port.on("message", (msg) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@irtio/runtime",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.7.0",
|
|
4
4
|
"description": "irtio room runtime: RoomCore, worker_threads host, in-process test harness",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"@dimforge/rapier2d-compat": "0.20.0",
|
|
37
37
|
"@dimforge/rapier3d-compat": "0.20.0",
|
|
38
38
|
"matter-js": "0.20.0",
|
|
39
|
-
"@irtio/protocol": "^4.
|
|
40
|
-
"@irtio/
|
|
41
|
-
"@irtio/
|
|
39
|
+
"@irtio/protocol": "^4.7.0",
|
|
40
|
+
"@irtio/server": "^4.7.0",
|
|
41
|
+
"@irtio/schema": "^4.7.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@types/matter-js": "0.20.2"
|