@irtio/cli 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/api-keys-UTLYMZYN.js +222 -0
- package/dist/api.js +1 -1
- package/dist/bundle.js +1 -1
- package/dist/{chunk-3HQMVCYA.js → chunk-BQBOBFBO.js} +4 -4
- package/dist/{chunk-RNAH5T4W.js → chunk-IDF46P7R.js} +2 -0
- package/dist/{chunk-DKWG7MGO.js → chunk-JL235KIE.js} +1 -1
- package/dist/{chunk-OTSFRVJN.js → chunk-NOYGY3HA.js} +10 -3
- package/dist/{chunk-ZD4ND6X6.js → chunk-OCVALOGK.js} +1 -1
- package/dist/{chunk-RQSJZWQC.js → chunk-WFMRNGO5.js} +134 -105
- package/dist/{delete-project-VENS2B44.js → delete-project-MXUYNGAO.js} +2 -2
- package/dist/deploy.d.ts +3 -1
- package/dist/deploy.js +164 -6
- package/dist/{dev-QM26ONKS.js → dev-VJ2ATJTF.js} +220 -14
- package/dist/index.js +39 -15
- package/dist/init.js +3 -3
- package/dist/{keys-JHLMEGRA.js → keys-NXRIBJZP.js} +3 -3
- package/dist/{leaderboard-SYPSBPS3.js → leaderboard-ZBJBKETM.js} +65 -11
- package/dist/{login-2M73HBZT.js → login-3RVN5PPN.js} +2 -2
- package/dist/{logs-2W7CPZO5.js → logs-AT7G6YRH.js} +3 -3
- package/dist/{migrate-T3DZJREY.js → migrate-5YCFT63L.js} +4 -4
- package/dist/{ratings-VG32WFDG.js → ratings-XBLX2MUW.js} +3 -3
- package/dist/{rollback-SO74MVZV.js → rollback-LI4TDLQA.js} +3 -3
- package/dist/{rooms-VI33P4RA.js → rooms-52Q5KBUS.js} +179 -20
- package/dist/simulate.d.ts +163 -10
- package/dist/simulate.js +345 -38
- package/dist/{static-deploy-KOWFKWZA.js → static-deploy-7UCYINJB.js} +5 -5
- package/dist/{status-HF3ZEKB7.js → status-JZGKH2P6.js} +3 -3
- package/dist/{usage-4G23QXCH.js → usage-7S447INI.js} +3 -3
- package/dist/{whoami-KTMTQNHM.js → whoami-UFSWPWK6.js} +2 -2
- package/package.json +8 -7
package/dist/simulate.js
CHANGED
|
@@ -6,8 +6,8 @@ import {
|
|
|
6
6
|
helpFor,
|
|
7
7
|
helpRequested,
|
|
8
8
|
trailerFor
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
import "./chunk-
|
|
9
|
+
} from "./chunk-OCVALOGK.js";
|
|
10
|
+
import "./chunk-IDF46P7R.js";
|
|
11
11
|
import "./chunk-UPHQM6NZ.js";
|
|
12
12
|
|
|
13
13
|
// src/simulate.ts
|
|
@@ -76,7 +76,9 @@ async function loadScenario(options) {
|
|
|
76
76
|
platform: "node",
|
|
77
77
|
outfile,
|
|
78
78
|
// The physics engine is resolved at runtime by whoever needs it, never bundled twice.
|
|
79
|
-
|
|
79
|
+
// All three, not just the 3D one: a 2D scenario's engine is loaded by the runtime the same
|
|
80
|
+
// way, and bundling a copy here would be a second instance beside it.
|
|
81
|
+
external: ["@dimforge/rapier3d-compat", "matter-js", "@dimforge/rapier2d-compat"],
|
|
80
82
|
...Object.keys(alias).length > 0 ? { alias } : {}
|
|
81
83
|
});
|
|
82
84
|
} catch (err) {
|
|
@@ -276,6 +278,13 @@ options:
|
|
|
276
278
|
--overruns-max <n> fail above this many server tick overruns in the run window
|
|
277
279
|
(default 0; the count is read from the server, and the run says
|
|
278
280
|
so when it could not be read)
|
|
281
|
+
--queue <name> queue for rooms through the matchmaker instead of creating one:
|
|
282
|
+
every bot calls the real match API and joins the room its ticket
|
|
283
|
+
names. Needs --control
|
|
284
|
+
--control <https://...> the control plane the queue lives on. \`irtio dev\` runs none, so a
|
|
285
|
+
queued run points at a real one (staging, or a test plane)
|
|
286
|
+
--party <n> group each n consecutive bots into one party, so they queue
|
|
287
|
+
together and land in one room. Needs --queue
|
|
279
288
|
--profile print where the run's bytes went, by collection and field, as the
|
|
280
289
|
bots saw them
|
|
281
290
|
--profile-top <n> rows in that table (default 12; implies --profile)
|
|
@@ -350,6 +359,21 @@ function parseSimulateArgs(args) {
|
|
|
350
359
|
case "--truth":
|
|
351
360
|
parsed.truth = true;
|
|
352
361
|
break;
|
|
362
|
+
// ---- M6 lane E: testing parity ---- (D73-c)
|
|
363
|
+
case "--queue":
|
|
364
|
+
parsed.queue = value();
|
|
365
|
+
break;
|
|
366
|
+
case "--control":
|
|
367
|
+
parsed.control = value();
|
|
368
|
+
break;
|
|
369
|
+
case "--party": {
|
|
370
|
+
const size = positive(value(), "--party");
|
|
371
|
+
if (!Number.isInteger(size) || size < 2) {
|
|
372
|
+
throw new Error("irtio simulate: --party must be a whole number of bots, at least 2");
|
|
373
|
+
}
|
|
374
|
+
parsed.party = size;
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
353
377
|
case "--profile":
|
|
354
378
|
parsed.profile = true;
|
|
355
379
|
break;
|
|
@@ -378,6 +402,16 @@ function parseSimulateArgs(args) {
|
|
|
378
402
|
throw new Error(`irtio simulate: unknown option ${JSON.stringify(arg)}`);
|
|
379
403
|
}
|
|
380
404
|
}
|
|
405
|
+
if (parsed.queue !== void 0 && parsed.control === void 0) {
|
|
406
|
+
throw new Error(
|
|
407
|
+
"irtio simulate: --queue needs --control. The matchmaker lives on the control plane and `irtio dev` runs none, so there is no queue at a dev server to join."
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
if (parsed.party !== void 0 && parsed.queue === void 0) {
|
|
411
|
+
throw new Error(
|
|
412
|
+
"irtio simulate: --party needs --queue. A party is a group that queues together, and a run that is not queueing has nothing to group."
|
|
413
|
+
);
|
|
414
|
+
}
|
|
381
415
|
return parsed;
|
|
382
416
|
}
|
|
383
417
|
function monorepoPackages() {
|
|
@@ -423,16 +457,18 @@ async function loadProjectSchema(options) {
|
|
|
423
457
|
}
|
|
424
458
|
return { schema, file: entry };
|
|
425
459
|
}
|
|
460
|
+
function is2dWorld(world) {
|
|
461
|
+
return world !== void 0 && world.engine === "matter2d" && !("why" in world);
|
|
462
|
+
}
|
|
463
|
+
function isRapier2dWorld(world) {
|
|
464
|
+
return world !== void 0 && world.engine === "rapier2d" && !("why" in world);
|
|
465
|
+
}
|
|
426
466
|
function isUnpredictedWorld(world) {
|
|
427
|
-
return world !== void 0 && "
|
|
467
|
+
return world !== void 0 && "why" in world;
|
|
428
468
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
(f) => existsSync2(f)
|
|
432
|
-
);
|
|
433
|
-
if (entry === void 0) return void 0;
|
|
469
|
+
var ROOM_CANDIDATES = ["irtio/room.ts", "irtio/room.js", "room.ts"];
|
|
470
|
+
async function bundleAndImport(entry, outfile, options) {
|
|
434
471
|
await mkdir2(options.outDir, { recursive: true });
|
|
435
|
-
const outfile = path2.join(options.outDir, "world.mjs");
|
|
436
472
|
const alias = options.irtioPackages ?? monorepoPackages();
|
|
437
473
|
await esbuild2.build({
|
|
438
474
|
entryPoints: [entry],
|
|
@@ -440,34 +476,106 @@ async function loadProjectWorld(options) {
|
|
|
440
476
|
format: "esm",
|
|
441
477
|
platform: "node",
|
|
442
478
|
outfile,
|
|
443
|
-
external: ["@dimforge/rapier3d-compat"],
|
|
479
|
+
external: ["@dimforge/rapier3d-compat", "@dimforge/rapier2d-compat", "matter-js"],
|
|
444
480
|
...alias !== void 0 ? { alias } : {}
|
|
445
481
|
});
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
482
|
+
return await import(`${pathToFileURL2(outfile).href}?v=${Date.now()}-${importSeq2++}`);
|
|
483
|
+
}
|
|
484
|
+
function hookMap(value) {
|
|
485
|
+
return typeof value === "object" && value !== null ? value : void 0;
|
|
486
|
+
}
|
|
487
|
+
async function detectRoomEngine(options) {
|
|
488
|
+
const entry = ROOM_CANDIDATES.map((c) => path2.resolve(options.cwd, c)).find((f) => existsSync2(f));
|
|
489
|
+
if (entry === void 0) return void 0;
|
|
490
|
+
let module;
|
|
491
|
+
try {
|
|
492
|
+
module = await bundleAndImport(entry, path2.join(options.outDir, "room-engine.mjs"), options);
|
|
493
|
+
} catch {
|
|
494
|
+
return void 0;
|
|
454
495
|
}
|
|
455
|
-
|
|
456
|
-
|
|
496
|
+
const definition = module.default ?? module.room;
|
|
497
|
+
const engine = definition?.config?.physics?.engine;
|
|
498
|
+
return typeof engine === "string" ? { engine, file: entry } : void 0;
|
|
499
|
+
}
|
|
500
|
+
async function detectMatter2dRoom(options) {
|
|
501
|
+
const declared = await detectRoomEngine(options);
|
|
502
|
+
return declared?.engine === "matter2d" ? declared.file : void 0;
|
|
503
|
+
}
|
|
504
|
+
function whyNotPredictable(world) {
|
|
505
|
+
if (world.bodies === void 0 || Object.keys(world.bodies).length === 0) {
|
|
506
|
+
return `${world.file} exports no \`bodies\`, so there are no shapes to build a local world from; bots run without client prediction.
|
|
507
|
+
export the body factories your client passes to \`joinRoom({ physics2d })\`.`;
|
|
508
|
+
}
|
|
509
|
+
const settle = world.engine === "matter2d" ? world.settle : void 0;
|
|
510
|
+
const steered = world.intents !== void 0 || settle !== void 0;
|
|
511
|
+
if (!steered && world.gravity.x === 0 && world.gravity.y === 0) {
|
|
512
|
+
const hooks = world.engine === "matter2d" ? "no `intents` and no `settle`" : "no `intents`";
|
|
513
|
+
return `${world.file} exports ${hooks}, and its gravity is zero, so nothing in a local world built from it could ever move a body; bots run without client prediction.
|
|
514
|
+
export the steering hooks your client passes to \`joinRoom({ physics2d })\` (a room that applies its forces inside \`tick()\` has none to share).`;
|
|
515
|
+
}
|
|
516
|
+
return void 0;
|
|
517
|
+
}
|
|
518
|
+
async function loadProjectWorld(options) {
|
|
519
|
+
const entry = WORLD_CANDIDATES.map((c) => path2.resolve(options.cwd, c)).find(
|
|
520
|
+
(f) => existsSync2(f)
|
|
521
|
+
);
|
|
522
|
+
const declared = await detectRoomEngine(options);
|
|
523
|
+
const planarRoom = (why) => declared === void 0 || declared.engine !== "matter2d" && declared.engine !== "rapier2d" ? void 0 : { engine: declared.engine, why: why(declared.file), file: declared.file };
|
|
524
|
+
if (entry === void 0) {
|
|
525
|
+
return planarRoom(
|
|
526
|
+
(room) => `${room} declares a ${declared?.engine ?? "2D"} room and there is no shared world module beside it, so bots run without client prediction.
|
|
527
|
+
export the \`physics2d\` block your client passes to \`joinRoom\` from \`irtio/world.ts\` to predict.`
|
|
528
|
+
);
|
|
457
529
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
530
|
+
const module = await bundleAndImport(entry, path2.join(options.outDir, "world.mjs"), options);
|
|
531
|
+
const gravity = module.gravity;
|
|
532
|
+
const notAWorld = new Error(
|
|
533
|
+
`irtio simulate: ${entry} does not export a { x, y, z } gravity.
|
|
461
534
|
the shared world-builder must export the same gravity the room config uses
|
|
462
535
|
(this is what \`irtio init --physics\` writes)`
|
|
536
|
+
);
|
|
537
|
+
if (typeof gravity !== "object" || gravity === null || typeof gravity.x !== "number") {
|
|
538
|
+
const recognised = planarRoom(
|
|
539
|
+
() => `${entry} exports no gravity, so it is not the client world a ${declared?.engine ?? "2D"} room predicts from; bots run without client prediction.
|
|
540
|
+
export the \`physics2d\` block your client passes to \`joinRoom\` from this module to predict.`
|
|
463
541
|
);
|
|
542
|
+
if (recognised !== void 0) return recognised;
|
|
543
|
+
throw notAWorld;
|
|
544
|
+
}
|
|
545
|
+
const planarEngine = declared?.engine === "rapier2d" ? "rapier2d" : declared?.engine === "matter2d" ? "matter2d" : declared === void 0 && typeof gravity.z !== "number" ? "matter2d" : void 0;
|
|
546
|
+
if (planarEngine !== void 0) {
|
|
547
|
+
if (typeof gravity.y !== "number") throw notAWorld;
|
|
548
|
+
const common = {
|
|
549
|
+
gravity: { x: gravity.x, y: gravity.y },
|
|
550
|
+
...typeof module.timestep === "number" ? { timestep: module.timestep } : {},
|
|
551
|
+
...hookMap(module.bodies) !== void 0 ? { bodies: module.bodies } : {},
|
|
552
|
+
...hookMap(module.intents) !== void 0 ? { intents: module.intents } : {},
|
|
553
|
+
...typeof module.epsilon === "number" ? { epsilon: module.epsilon } : {},
|
|
554
|
+
...typeof module.maxPredictedBodies === "number" ? { maxPredictedBodies: module.maxPredictedBodies } : {},
|
|
555
|
+
...typeof module.smoothingHalfLifeMs === "number" ? { smoothingHalfLifeMs: module.smoothingHalfLifeMs } : {},
|
|
556
|
+
file: entry
|
|
557
|
+
};
|
|
558
|
+
const planar = planarEngine === "rapier2d" ? {
|
|
559
|
+
engine: "rapier2d",
|
|
560
|
+
...common,
|
|
561
|
+
...typeof module.setup === "function" ? { setup: module.setup } : {}
|
|
562
|
+
} : {
|
|
563
|
+
engine: "matter2d",
|
|
564
|
+
...common,
|
|
565
|
+
...typeof module.setup === "function" ? { setup: module.setup } : {},
|
|
566
|
+
...hookMap(module.settle) !== void 0 ? { settle: module.settle } : {}
|
|
567
|
+
};
|
|
568
|
+
const why = whyNotPredictable(planar);
|
|
569
|
+
return why === void 0 ? planar : { engine: planarEngine, why, file: entry };
|
|
464
570
|
}
|
|
571
|
+
if (typeof gravity.y !== "number" || typeof gravity.z !== "number") throw notAWorld;
|
|
465
572
|
return {
|
|
573
|
+
engine: "rapier3d",
|
|
466
574
|
gravity,
|
|
467
575
|
...typeof module.timestep === "number" ? { timestep: module.timestep } : {},
|
|
468
576
|
...typeof module.setup === "function" ? { setup: module.setup } : {},
|
|
469
|
-
...
|
|
470
|
-
...
|
|
577
|
+
...hookMap(module.bodies) !== void 0 ? { bodies: module.bodies } : {},
|
|
578
|
+
...hookMap(module.intents) !== void 0 ? { intents: module.intents } : {},
|
|
471
579
|
file: entry
|
|
472
580
|
};
|
|
473
581
|
}
|
|
@@ -493,6 +601,80 @@ function everyByteEqual(a, b) {
|
|
|
493
601
|
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
494
602
|
return true;
|
|
495
603
|
}
|
|
604
|
+
var DETERMINISM_STEPS_2D = 60;
|
|
605
|
+
function canonicalMatterWorld(matter, engine) {
|
|
606
|
+
return matter.Composite.allBodies(engine.world).map(
|
|
607
|
+
(b) => [b.position.x, b.position.y, b.angle, b.velocity.x, b.velocity.y, b.angularVelocity].join(
|
|
608
|
+
","
|
|
609
|
+
)
|
|
610
|
+
).join(";");
|
|
611
|
+
}
|
|
612
|
+
async function checkWorldRapier2dDeterminism(world, schema) {
|
|
613
|
+
const factories = world.bodies ?? {};
|
|
614
|
+
const exercised = schema ? schema.collections.filter(
|
|
615
|
+
(c) => c.physics !== void 0 && factories[c.name] !== void 0
|
|
616
|
+
) : [];
|
|
617
|
+
if (!world.setup && exercised.length === 0) return void 0;
|
|
618
|
+
const { initRapier2d } = await import("@irtio/runtime");
|
|
619
|
+
const { defaultRecord } = await import("@irtio/schema");
|
|
620
|
+
const rapier = await initRapier2d();
|
|
621
|
+
const build3 = () => {
|
|
622
|
+
const w = new rapier.World({ x: world.gravity.x, y: world.gravity.y });
|
|
623
|
+
try {
|
|
624
|
+
world.setup?.(w, rapier);
|
|
625
|
+
for (const desc of exercised) {
|
|
626
|
+
try {
|
|
627
|
+
const spec = factories[desc.name](rapier, defaultRecord(desc), "irtio-determinism-check");
|
|
628
|
+
if (spec?.body === void 0) continue;
|
|
629
|
+
const body = w.createRigidBody(spec.body);
|
|
630
|
+
for (const collider of spec.colliders ?? []) w.createCollider(collider, body);
|
|
631
|
+
} catch {
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return w.takeSnapshot();
|
|
635
|
+
} finally {
|
|
636
|
+
w.free();
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
const first = build3();
|
|
640
|
+
const second = build3();
|
|
641
|
+
if (first.length === second.length && everyByteEqual(first, second)) return void 0;
|
|
642
|
+
return `the world builder is not deterministic: two builds of ${world.file} disagree. It must be pure over synced inputs \u2014 no Math.random(), no clock, no module state; put seeds and level parameters in synced state instead.`;
|
|
643
|
+
}
|
|
644
|
+
async function checkWorld2dDeterminism(world, schema) {
|
|
645
|
+
const factories = world.bodies ?? {};
|
|
646
|
+
const exercised = schema ? schema.collections.filter(
|
|
647
|
+
(c) => c.physics !== void 0 && factories[c.name] !== void 0
|
|
648
|
+
) : [];
|
|
649
|
+
if (!world.setup && exercised.length === 0) return void 0;
|
|
650
|
+
const { initMatter } = await import("@irtio/runtime");
|
|
651
|
+
const { defaultRecord } = await import("@irtio/schema");
|
|
652
|
+
const matter = await initMatter();
|
|
653
|
+
const stepMs = (world.timestep ?? 1 / 60) * 1e3;
|
|
654
|
+
const build3 = () => {
|
|
655
|
+
const engine = matter.Engine.create();
|
|
656
|
+
engine.gravity.x = world.gravity.x;
|
|
657
|
+
engine.gravity.y = world.gravity.y;
|
|
658
|
+
try {
|
|
659
|
+
world.setup?.(engine, matter);
|
|
660
|
+
for (const desc of exercised) {
|
|
661
|
+
try {
|
|
662
|
+
const spec = factories[desc.name](matter, defaultRecord(desc), "irtio-determinism-check");
|
|
663
|
+
if (spec?.body !== void 0) matter.Composite.add(engine.world, spec.body);
|
|
664
|
+
} catch {
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
for (let i = 0; i < DETERMINISM_STEPS_2D; i++) matter.Engine.update(engine, stepMs);
|
|
668
|
+
return canonicalMatterWorld(matter, engine);
|
|
669
|
+
} finally {
|
|
670
|
+
matter.Engine.clear(engine);
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
const first = build3();
|
|
674
|
+
const second = build3();
|
|
675
|
+
if (first === second) return void 0;
|
|
676
|
+
return `the world builder is not deterministic: two builds of ${world.file} disagree after ${DETERMINISM_STEPS_2D} steps. It must be pure over synced inputs \u2014 no Math.random(), no clock, no module state; put seeds and level parameters in synced state instead.`;
|
|
677
|
+
}
|
|
496
678
|
var TICK_HEALTH_TIMEOUT_MS = 2e3;
|
|
497
679
|
function stateUrlFor(wsUrl) {
|
|
498
680
|
let parsed;
|
|
@@ -647,11 +829,30 @@ function formatReport(report, schemaLoaded) {
|
|
|
647
829
|
const convergence = report.convergenceLagMs === void 0 ? "convergence not sampled" : `convergence ${report.convergenceLagMs} ms (p50 of ${report.convergence?.samples ?? 0})`;
|
|
648
830
|
const { mispredictions, mispredictionMagnitude, mispredictionMax, snaps } = report.totals;
|
|
649
831
|
const misprediction = mispredictions === 0 ? "misprediction 0" : `misprediction mean ${(mispredictionMagnitude / mispredictions).toFixed(1)} max ${mispredictionMax.toFixed(1)}`;
|
|
832
|
+
const { messagesSent, messagesReceived, messagesDropped } = report.totals;
|
|
833
|
+
const messages = messagesSent + messagesReceived + messagesDropped === 0 ? "" : ` \xB7 ${messagesSent} msg out / ${messagesReceived} in` + (messagesDropped > 0 ? ` / ${messagesDropped} dropped` : "");
|
|
650
834
|
lines.push(
|
|
651
835
|
pc.dim(
|
|
652
|
-
` ${report.framesPerSecond} frames/s \xB7 in ${rate(report.bytesInPerSecondPerBot)}/bot \xB7 out ${rate(report.bytesOutPerSecondPerBot)}/bot \xB7 ${report.totals.corrections} corrections \xB7 ${misprediction} \xB7 ${snaps} snap(s) \xB7 ${convergence}`
|
|
836
|
+
` ${report.framesPerSecond} frames/s \xB7 in ${rate(report.bytesInPerSecondPerBot)}/bot \xB7 out ${rate(report.bytesOutPerSecondPerBot)}/bot \xB7 ${report.totals.corrections} corrections \xB7 ${misprediction} \xB7 ${snaps} snap(s) \xB7 ${convergence}${messages}`
|
|
653
837
|
)
|
|
654
838
|
);
|
|
839
|
+
const prediction = report.prediction;
|
|
840
|
+
if (prediction !== void 0) {
|
|
841
|
+
const { syncCorrections, proxiedCorrections, suppressedCorrections } = report.totals;
|
|
842
|
+
lines.push(
|
|
843
|
+
pc.dim(
|
|
844
|
+
` prediction: ${prediction.bots}/${report.bots} bot(s) held a local world \xB7 peak ${prediction.proxies} proxied, ${prediction.absent} absent \xB7 ${mispredictions} misprediction(s), ${suppressedCorrections} within-epsilon, ${proxiedCorrections} proxied, ${syncCorrections} body-sync`
|
|
845
|
+
)
|
|
846
|
+
);
|
|
847
|
+
if (prediction.absent > 0) {
|
|
848
|
+
lines.push(
|
|
849
|
+
pc.yellow(
|
|
850
|
+
` ${prediction.absent} instance(s) had no body in a local world at once, so predicted bodies passed through them.
|
|
851
|
+
raise maxProxyBodies, or give the collection a body factory on the client.`
|
|
852
|
+
)
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
655
856
|
if (report.tracePath !== void 0) lines.push(pc.dim(` trace: ${report.tracePath}`));
|
|
656
857
|
lines.push("");
|
|
657
858
|
const failed = report.invariants.filter((i) => !i.ok);
|
|
@@ -742,7 +943,8 @@ function formatHits(rows) {
|
|
|
742
943
|
` ${pc.bold("hit registration")} ${pc.dim(`(${rows.length} shot(s))`)}`
|
|
743
944
|
];
|
|
744
945
|
for (const row of rows) {
|
|
745
|
-
const
|
|
946
|
+
const marks = [row.estimated ? "estimated" : void 0, row.rewound ? "rewound" : void 0].filter((m) => m !== void 0).map((m) => ` (${m})`).join("");
|
|
947
|
+
const at = row.serverTick === void 0 ? "no tick" : `tick ${row.serverTick}${marks}`;
|
|
746
948
|
if (row.missDistance === void 0) {
|
|
747
949
|
lines.push(
|
|
748
950
|
pc.yellow(
|
|
@@ -758,14 +960,48 @@ function formatHits(rows) {
|
|
|
758
960
|
const measured = rows.filter((r) => r.missDistance !== void 0);
|
|
759
961
|
if (measured.length > 0) {
|
|
760
962
|
const mean = measured.reduce((s, r) => s + (r.missDistance ?? 0), 0) / measured.length;
|
|
963
|
+
const worst = Math.max(...measured.map((r) => r.missDistance ?? 0));
|
|
964
|
+
const rewound = measured.filter((r) => r.rewound === true).length;
|
|
965
|
+
const verdict = rewound === 0 ? "Nothing here compensates for lag; this is what a player at this latency would experience." : `${rewound} of ${measured.length} shot(s) were resolved through room.rewind, against the world the shooter was looking at.`;
|
|
761
966
|
lines.push(
|
|
762
967
|
pc.dim(
|
|
763
|
-
` mean miss ${mean.toFixed(2)} over ${measured.length} shot(s).
|
|
968
|
+
` mean miss ${mean.toFixed(2)}, worst ${worst.toFixed(2)} over ${measured.length} shot(s). ${verdict}`
|
|
764
969
|
)
|
|
765
970
|
);
|
|
766
971
|
}
|
|
767
972
|
return lines;
|
|
768
973
|
}
|
|
974
|
+
function formatMatchmaking(summary, failures) {
|
|
975
|
+
const lines = ["", ` ${pc.bold("matchmaking")}`];
|
|
976
|
+
lines.push(
|
|
977
|
+
pc.dim(
|
|
978
|
+
` ${summary.tickets} ticket(s), ${summary.rooms} room(s), wait p50 ${summary.waitP50Ms} ms p95 ${summary.waitP95Ms} ms max ${summary.waitMaxMs} ms, ${summary.timeouts} timeout(s)` + (summary.backfills > 0 ? `, ${summary.backfills} backfill(s)` : "") + (summary.parties > 0 ? `, parties intact ${summary.partiesIntact}/${summary.parties}` : "")
|
|
979
|
+
)
|
|
980
|
+
);
|
|
981
|
+
for (const split of summary.splitParties) {
|
|
982
|
+
lines.push(
|
|
983
|
+
pc.red(
|
|
984
|
+
` FAIL party ${split.party} was split across ${split.rooms.length} rooms: ` + split.rooms.join(", ")
|
|
985
|
+
)
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
for (const room of summary.overfullRooms) {
|
|
989
|
+
lines.push(
|
|
990
|
+
pc.red(
|
|
991
|
+
` FAIL room ${room.room} holds ${room.bots} bots on a ticket that seats ${room.size}`
|
|
992
|
+
)
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
for (const failure of failures) {
|
|
996
|
+
lines.push(
|
|
997
|
+
pc.red(` FAIL bot ${failure.requested} got no ticket: ${failure.code} \u2014 ${failure.message}`)
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
if (summary.splitParties.length === 0 && summary.overfullRooms.length === 0 && failures.length === 0) {
|
|
1001
|
+
lines.push(pc.green(" every bot was answered, and every party landed in one room."));
|
|
1002
|
+
}
|
|
1003
|
+
return lines;
|
|
1004
|
+
}
|
|
769
1005
|
function point(p) {
|
|
770
1006
|
const parts = Object.entries(p).map(([k, v]) => `${k} ${Math.round(v * 100) / 100}`);
|
|
771
1007
|
return parts.length === 0 ? "(none)" : `(${parts.join(", ")})`;
|
|
@@ -856,15 +1092,23 @@ async function runSimulation(options = {}) {
|
|
|
856
1092
|
(c) => c.physics !== void 0
|
|
857
1093
|
);
|
|
858
1094
|
const loadedWorld = hasPhysics ? await loadProjectWorld({ cwd, outDir, irtioPackages: options.irtioPackages }) : void 0;
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
1095
|
+
const world2d = is2dWorld(loadedWorld) ? loadedWorld : void 0;
|
|
1096
|
+
const worldR2d = isRapier2dWorld(loadedWorld) ? loadedWorld : void 0;
|
|
1097
|
+
const world = is2dWorld(loadedWorld) || isRapier2dWorld(loadedWorld) || isUnpredictedWorld(loadedWorld) ? void 0 : loadedWorld;
|
|
1098
|
+
if (world2d !== void 0) {
|
|
1099
|
+
log(`physics: ${world2d.file} is a matter2d world; bots predict with physics2d`);
|
|
1100
|
+
} else if (worldR2d !== void 0) {
|
|
1101
|
+
log(`physics: ${worldR2d.file} is a rapier2d world; bots predict with physics2d`);
|
|
1102
|
+
} else if (isUnpredictedWorld(loadedWorld)) {
|
|
1103
|
+
log(pc.yellow(`physics: ${loadedWorld.why}`));
|
|
863
1104
|
}
|
|
864
|
-
const world = isUnpredictedWorld(loadedWorld) ? void 0 : loadedWorld;
|
|
865
1105
|
let worldError;
|
|
866
1106
|
if (world) {
|
|
867
1107
|
worldError = await checkWorldDeterminism(world);
|
|
1108
|
+
} else if (world2d) {
|
|
1109
|
+
worldError = await checkWorld2dDeterminism(world2d, loaded?.schema);
|
|
1110
|
+
} else if (worldR2d) {
|
|
1111
|
+
worldError = await checkWorldRapier2dDeterminism(worldR2d, loaded?.schema);
|
|
868
1112
|
}
|
|
869
1113
|
const conditionsSpec = scenario?.conditions ?? conditionsFrom(options);
|
|
870
1114
|
const cheatSpec = scenario?.cheat ?? cheatFrom(options);
|
|
@@ -883,9 +1127,27 @@ async function runSimulation(options = {}) {
|
|
|
883
1127
|
...options.overrunsMax !== void 0 ? { overrunsMax: options.overrunsMax } : {},
|
|
884
1128
|
...options.profile === true ? { profile: true } : {}
|
|
885
1129
|
};
|
|
886
|
-
const predicted = world !== void 0 && worldError === void 0;
|
|
1130
|
+
const predicted = (world !== void 0 || world2d !== void 0 || worldR2d !== void 0) && worldError === void 0;
|
|
1131
|
+
const projectKey = options.key ?? loaded?.schema?.project;
|
|
1132
|
+
if (options.queue !== void 0 && (projectKey === void 0 || projectKey === "")) {
|
|
1133
|
+
throw new RunNotPerformedError(
|
|
1134
|
+
"irtio simulate: --queue needs a project key, and this schema carries none.\n build the schema with `irtio` so it names its project, or pass --key."
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
const match = options.queue !== void 0 && options.control !== void 0 ? {
|
|
1138
|
+
controlUrl: options.control,
|
|
1139
|
+
project: projectKey,
|
|
1140
|
+
queue: options.queue,
|
|
1141
|
+
...options.matchTimeoutMs !== void 0 ? { timeoutMs: options.matchTimeoutMs } : {},
|
|
1142
|
+
...options.party !== void 0 ? (
|
|
1143
|
+
// Consecutive groups of `n`: bots 0..n-1 are party 0, n..2n-1 are party 1. A
|
|
1144
|
+
// trailing group smaller than `n` is still a party, of whatever is left.
|
|
1145
|
+
{ party: (index) => Math.floor(index / options.party) }
|
|
1146
|
+
) : {}
|
|
1147
|
+
} : void 0;
|
|
887
1148
|
const spawn = {
|
|
888
1149
|
...common,
|
|
1150
|
+
...match !== void 0 ? { match } : {},
|
|
889
1151
|
...options.joinTimeoutMs !== void 0 ? { joinTimeoutMs: options.joinTimeoutMs } : {},
|
|
890
1152
|
...options.roomGoneGraceMs !== void 0 ? { roomGoneGraceMs: options.roomGoneGraceMs } : {}
|
|
891
1153
|
};
|
|
@@ -895,7 +1157,7 @@ async function runSimulation(options = {}) {
|
|
|
895
1157
|
...spawn,
|
|
896
1158
|
schema: loaded.schema,
|
|
897
1159
|
script: scenario?.script ?? randomScript(loaded.schema, { cheat: cheatSpec ?? false }),
|
|
898
|
-
...predicted ? {
|
|
1160
|
+
...predicted && world !== void 0 ? {
|
|
899
1161
|
physics: {
|
|
900
1162
|
gravity: world.gravity,
|
|
901
1163
|
...world.timestep !== void 0 ? { timestep: world.timestep } : {},
|
|
@@ -903,6 +1165,35 @@ async function runSimulation(options = {}) {
|
|
|
903
1165
|
...world.bodies !== void 0 ? { bodies: world.bodies } : {},
|
|
904
1166
|
...world.intents !== void 0 ? { intents: world.intents } : {}
|
|
905
1167
|
}
|
|
1168
|
+
} : {},
|
|
1169
|
+
// ---- M6 lane E: testing parity ---- (D73-a)
|
|
1170
|
+
...predicted && world2d !== void 0 ? {
|
|
1171
|
+
physics2d: {
|
|
1172
|
+
gravity: world2d.gravity,
|
|
1173
|
+
...world2d.timestep !== void 0 ? { timestep: world2d.timestep } : {},
|
|
1174
|
+
...world2d.setup !== void 0 ? { setup: world2d.setup } : {},
|
|
1175
|
+
...world2d.bodies !== void 0 ? { bodies: world2d.bodies } : {},
|
|
1176
|
+
...world2d.intents !== void 0 ? { intents: world2d.intents } : {},
|
|
1177
|
+
...world2d.settle !== void 0 ? { settle: world2d.settle } : {},
|
|
1178
|
+
...world2d.epsilon !== void 0 ? { epsilon: world2d.epsilon } : {},
|
|
1179
|
+
...world2d.maxPredictedBodies !== void 0 ? { maxPredictedBodies: world2d.maxPredictedBodies } : {},
|
|
1180
|
+
...world2d.smoothingHalfLifeMs !== void 0 ? { smoothingHalfLifeMs: world2d.smoothingHalfLifeMs } : {}
|
|
1181
|
+
}
|
|
1182
|
+
} : {},
|
|
1183
|
+
// The same `physics2d` option, under the engine discriminant: one planar option, two
|
|
1184
|
+
// engines, and the client dispatches on the name rather than on the gravity's shape.
|
|
1185
|
+
...predicted && worldR2d !== void 0 ? {
|
|
1186
|
+
physics2d: {
|
|
1187
|
+
engine: "rapier2d",
|
|
1188
|
+
gravity: worldR2d.gravity,
|
|
1189
|
+
...worldR2d.timestep !== void 0 ? { timestep: worldR2d.timestep } : {},
|
|
1190
|
+
...worldR2d.setup !== void 0 ? { setup: worldR2d.setup } : {},
|
|
1191
|
+
...worldR2d.bodies !== void 0 ? { bodies: worldR2d.bodies } : {},
|
|
1192
|
+
...worldR2d.intents !== void 0 ? { intents: worldR2d.intents } : {},
|
|
1193
|
+
...worldR2d.epsilon !== void 0 ? { epsilon: worldR2d.epsilon } : {},
|
|
1194
|
+
...worldR2d.maxPredictedBodies !== void 0 ? { maxPredictedBodies: worldR2d.maxPredictedBodies } : {},
|
|
1195
|
+
...worldR2d.smoothingHalfLifeMs !== void 0 ? { smoothingHalfLifeMs: worldR2d.smoothingHalfLifeMs } : {}
|
|
1196
|
+
}
|
|
906
1197
|
} : {}
|
|
907
1198
|
}) : await spawnBots(bots, { ...spawn, script: relayEchoScript() });
|
|
908
1199
|
} catch (err) {
|
|
@@ -1049,6 +1340,9 @@ async function runSimulation(options = {}) {
|
|
|
1049
1340
|
if (report.profile !== void 0) {
|
|
1050
1341
|
for (const line of formatRunProfile(report, options.profileTop)) log(line);
|
|
1051
1342
|
}
|
|
1343
|
+
if (report.matchmaking !== void 0) {
|
|
1344
|
+
for (const line of formatMatchmaking(report.matchmaking, runner.matchFailures)) log(line);
|
|
1345
|
+
}
|
|
1052
1346
|
if (adversarial !== void 0) for (const line of formatAdversarial(adversarial)) log(line);
|
|
1053
1347
|
if (hits.length > 0) for (const line of formatHits(hits)) log(line);
|
|
1054
1348
|
if (truth !== void 0) for (const line of formatTruth(truth)) log(line);
|
|
@@ -1059,11 +1353,14 @@ async function runSimulation(options = {}) {
|
|
|
1059
1353
|
log(pc.red(` FAIL world-builder: ${worldError}`));
|
|
1060
1354
|
} else if (predicted) {
|
|
1061
1355
|
const suppressed = report.totals.suppressedCorrections;
|
|
1356
|
+
const engine = world2d !== void 0 ? "matter2d" : worldR2d !== void 0 ? "rapier2d" : "rapier3d";
|
|
1062
1357
|
log(
|
|
1063
1358
|
pc.dim(
|
|
1064
|
-
` bots predicted physics from ${path2.relative(cwd, world?.file ?? "")} (build-twice check held; ${suppressed} within-epsilon correction(s) suppressed)`
|
|
1359
|
+
` bots predicted ${engine} physics from ${path2.relative(cwd, world?.file ?? world2d?.file ?? worldR2d?.file ?? "")} (build-twice check held; ${suppressed} within-epsilon correction(s) suppressed)`
|
|
1065
1360
|
)
|
|
1066
1361
|
);
|
|
1362
|
+
} else if (hasPhysics && isUnpredictedWorld(loadedWorld)) {
|
|
1363
|
+
log(pc.yellow(" bots interpolated instead of predicting; see the physics line above."));
|
|
1067
1364
|
} else if (hasPhysics) {
|
|
1068
1365
|
log(
|
|
1069
1366
|
pc.yellow(
|
|
@@ -1075,7 +1372,10 @@ async function runSimulation(options = {}) {
|
|
|
1075
1372
|
log(pc.red(` bot ${failure.bot} script failed: ${String(failure.error)}`));
|
|
1076
1373
|
}
|
|
1077
1374
|
if (fatal !== void 0) log(pc.red(` run ended early: ${fatal}`));
|
|
1078
|
-
const exitCode = endedBy === "ceiling" ? 2 : !report.ok || worldError !== void 0 || runner.scriptErrors.length > 0 ||
|
|
1375
|
+
const exitCode = endedBy === "ceiling" ? 2 : !report.ok || worldError !== void 0 || runner.scriptErrors.length > 0 || // D73-c: a bot that never got a ticket is a measured failure of the front door, so it
|
|
1376
|
+
// is exit 1 like every other one. The run still happened for the bots that got in, and
|
|
1377
|
+
// the matchmaking section names the ones that did not.
|
|
1378
|
+
runner.matchFailures.length > 0 || scenarioRun?.ok === false || // D43: a difference between what the clients held and what the save says the server
|
|
1079
1379
|
// held is a measured violation, so it is exit 1 like every other one. A run that could
|
|
1080
1380
|
// not take a save at all is not: nothing was measured, and the section says so in
|
|
1081
1381
|
// words rather than turning "unread" into a verdict, which is D36's `unavailable` rule
|
|
@@ -1091,6 +1391,7 @@ async function runSimulation(options = {}) {
|
|
|
1091
1391
|
...truth !== void 0 ? { truth } : {},
|
|
1092
1392
|
schema: loaded !== void 0,
|
|
1093
1393
|
predicted,
|
|
1394
|
+
matchFailures: runner.matchFailures,
|
|
1094
1395
|
...worldError !== void 0 ? { worldError } : {},
|
|
1095
1396
|
endedBy,
|
|
1096
1397
|
...fatal !== void 0 ? { fatal } : {},
|
|
@@ -1129,8 +1430,14 @@ export {
|
|
|
1129
1430
|
ScenarioNotRunError,
|
|
1130
1431
|
USAGE,
|
|
1131
1432
|
ceilingFor,
|
|
1433
|
+
checkWorld2dDeterminism,
|
|
1132
1434
|
checkWorldDeterminism,
|
|
1435
|
+
checkWorldRapier2dDeterminism,
|
|
1436
|
+
detectMatter2dRoom,
|
|
1437
|
+
detectRoomEngine,
|
|
1133
1438
|
formatBuildIdentity,
|
|
1439
|
+
is2dWorld,
|
|
1440
|
+
isRapier2dWorld,
|
|
1134
1441
|
isUnpredictedWorld,
|
|
1135
1442
|
loadProjectSchema,
|
|
1136
1443
|
loadProjectWorld,
|
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
runStaticDeploy,
|
|
5
5
|
staticDeploy,
|
|
6
6
|
walkStaticDir
|
|
7
|
-
} from "./chunk-
|
|
8
|
-
import "./chunk-
|
|
9
|
-
import "./chunk-
|
|
10
|
-
import "./chunk-
|
|
11
|
-
import "./chunk-
|
|
7
|
+
} from "./chunk-BQBOBFBO.js";
|
|
8
|
+
import "./chunk-WFMRNGO5.js";
|
|
9
|
+
import "./chunk-JL235KIE.js";
|
|
10
|
+
import "./chunk-OCVALOGK.js";
|
|
11
|
+
import "./chunk-IDF46P7R.js";
|
|
12
12
|
import "./chunk-UPHQM6NZ.js";
|
|
13
13
|
export {
|
|
14
14
|
USAGE,
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
readProjectConfig
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-JL235KIE.js";
|
|
4
4
|
import {
|
|
5
5
|
HelpRequested,
|
|
6
6
|
helpFor,
|
|
7
7
|
helpRequested
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-OCVALOGK.js";
|
|
9
9
|
import {
|
|
10
10
|
createApiClient,
|
|
11
11
|
isLoginRequired
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-IDF46P7R.js";
|
|
13
13
|
import {
|
|
14
14
|
resolveControlUrlForUser
|
|
15
15
|
} from "./chunk-UPHQM6NZ.js";
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
readProjectConfig
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-JL235KIE.js";
|
|
4
4
|
import {
|
|
5
5
|
HelpRequested,
|
|
6
6
|
helpFor,
|
|
7
7
|
helpRequested
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-OCVALOGK.js";
|
|
9
9
|
import {
|
|
10
10
|
createApiClient,
|
|
11
11
|
isLoginRequired
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-IDF46P7R.js";
|
|
13
13
|
import {
|
|
14
14
|
resolveControlUrlForUser
|
|
15
15
|
} from "./chunk-UPHQM6NZ.js";
|
|
@@ -2,11 +2,11 @@ import {
|
|
|
2
2
|
HelpRequested,
|
|
3
3
|
helpFor,
|
|
4
4
|
helpRequested
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-OCVALOGK.js";
|
|
6
6
|
import {
|
|
7
7
|
createApiClient,
|
|
8
8
|
isLoginRequired
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-IDF46P7R.js";
|
|
10
10
|
import {
|
|
11
11
|
resolveControlUrlForUser
|
|
12
12
|
} from "./chunk-UPHQM6NZ.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@irtio/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "irtio CLI: local dev server, room scaffolding, bot simulation, deploy, logs and rooms",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -45,16 +45,17 @@
|
|
|
45
45
|
"esbuild": "^0.27.3",
|
|
46
46
|
"picocolors": "^1.1.1",
|
|
47
47
|
"ws": "^8.18.0",
|
|
48
|
-
"@irtio/
|
|
49
|
-
"@irtio/
|
|
50
|
-
"@irtio/
|
|
51
|
-
"@irtio/
|
|
52
|
-
"@irtio/
|
|
53
|
-
"@irtio/
|
|
48
|
+
"@irtio/bots": "0.8.0",
|
|
49
|
+
"@irtio/client": "0.8.0",
|
|
50
|
+
"@irtio/runtime": "0.8.0",
|
|
51
|
+
"@irtio/schema": "0.8.0",
|
|
52
|
+
"@irtio/server": "0.8.0",
|
|
53
|
+
"@irtio/protocol": "0.8.0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@types/ws": "^8.5.13",
|
|
57
57
|
"@dimforge/rapier3d-compat": "0.20.0",
|
|
58
|
+
"@dimforge/rapier2d-compat": "0.20.0",
|
|
58
59
|
"@irtio/store": "0.0.0",
|
|
59
60
|
"@irtio/supervisor": "0.0.0"
|
|
60
61
|
},
|