@irtio/bots 0.6.0 → 0.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/index.d.ts +370 -22
- package/dist/index.js +483 -118
- package/package.json +5 -5
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { PlainState, AnySchema, Delta, TypeDesc } from '@irtio/schema';
|
|
2
2
|
import { TimelineDump } from '@irtio/runtime';
|
|
3
3
|
export { TimelineDump, TimelineFrame } from '@irtio/runtime';
|
|
4
|
-
import { Correction, Room, RelayRoom, Transport, JoinOptions } from '@irtio/client';
|
|
4
|
+
import { PredictionStatus, Correction, MatchTicket, Room, RelayRoom, Transport, JoinOptions } from '@irtio/client';
|
|
5
5
|
import { ProfileSnapshot } from '@irtio/protocol';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -17,7 +17,7 @@ import { ProfileSnapshot } from '@irtio/protocol';
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
/** The invariants every simulation checks, in report order. */
|
|
20
|
-
declare const INVARIANT_NAMES: readonly ["schema-validity", "visibility-leak", "bandwidth", "handler-error", "correction-storm", "misprediction", "snaps", "disconnects", "tick-health"];
|
|
20
|
+
declare const INVARIANT_NAMES: readonly ["schema-validity", "visibility-leak", "bandwidth", "handler-error", "correction-storm", "misprediction", "snaps", "disconnects", "tick-health", "typed-message-drops", "party-integrity"];
|
|
21
21
|
type InvariantName = (typeof INVARIANT_NAMES)[number];
|
|
22
22
|
/**
|
|
23
23
|
* D36: three states, not two. `unavailable` exists because the server's own tick counters cannot
|
|
@@ -48,6 +48,27 @@ interface TickHealthReading {
|
|
|
48
48
|
/** Where the numbers came from, so a reader can go and check. */
|
|
49
49
|
readonly source?: string;
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* M6 lane E (D73-c): what the queue answered, per bot, for the `party-integrity` invariant and the
|
|
53
|
+
* matchmaking report section. Absent ⇒ the run did not queue and the invariant is `unavailable`,
|
|
54
|
+
* which is D36's rule applied here: "nobody asked" and "nothing was wrong" stay different answers.
|
|
55
|
+
*/
|
|
56
|
+
interface MatchmakingReading {
|
|
57
|
+
readonly tickets: readonly {
|
|
58
|
+
readonly bot: number;
|
|
59
|
+
readonly room: string;
|
|
60
|
+
readonly queue: string;
|
|
61
|
+
/** Seats the queue says the room holds. `-1` seat means a backfill answer, not a position. */
|
|
62
|
+
readonly size: number;
|
|
63
|
+
readonly backfill: boolean;
|
|
64
|
+
readonly waitedMs: number;
|
|
65
|
+
readonly party?: number;
|
|
66
|
+
}[];
|
|
67
|
+
readonly failures: readonly {
|
|
68
|
+
readonly requested: number;
|
|
69
|
+
readonly code: string;
|
|
70
|
+
}[];
|
|
71
|
+
}
|
|
51
72
|
interface SpatialVisibilityContext {
|
|
52
73
|
/**
|
|
53
74
|
* The state the anchor and the judged positions are read from. Server truth when a test has it
|
|
@@ -91,6 +112,16 @@ interface InvariantThresholds {
|
|
|
91
112
|
* paid for — and one `--overruns-max` away from being usable on a deliberately busy room.
|
|
92
113
|
*/
|
|
93
114
|
readonly overrunsMax: number;
|
|
115
|
+
/**
|
|
116
|
+
* D70 `typed-message-drops`: typed peer messages a bot dropped across the run — an index its
|
|
117
|
+
* schema does not have, or a payload it could not decode.
|
|
118
|
+
*
|
|
119
|
+
* Zero by default, and strict on purpose. Every drop is a peer sending a shape this bot's
|
|
120
|
+
* schema does not describe, which in a simulation means the scenario and the room disagree.
|
|
121
|
+
* Before this the disagreement was invisible: the message simply never arrived, and the run
|
|
122
|
+
* went green.
|
|
123
|
+
*/
|
|
124
|
+
readonly typedMessageDropsMax: number;
|
|
94
125
|
}
|
|
95
126
|
declare const DEFAULT_THRESHOLDS: InvariantThresholds;
|
|
96
127
|
/**
|
|
@@ -287,21 +318,27 @@ declare function conditionedTransport(base: ConditionedTransport, conditions: Ne
|
|
|
287
318
|
*
|
|
288
319
|
* ## How a shot is correlated
|
|
289
320
|
*
|
|
290
|
-
*
|
|
321
|
+
* Every row names one server tick — **the tick the shot was judged at** — and the miss distance is
|
|
322
|
+
* how far the aim was from where authority had the target at that tick. There are two ways to
|
|
323
|
+
* learn that tick, and the difference between them is why every row says which one it used.
|
|
291
324
|
*
|
|
292
|
-
* -
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
325
|
+
* - **Exact (D72).** The game's own RPC tells the bot which tick it judged the shot at, and the
|
|
326
|
+
* bot passes it to `bot.shot({ ..., serverTick })`. Nothing is inferred and no clock is compared
|
|
327
|
+
* with any other clock. A room with no lag compensation reports the tick the `CALL` landed on, a
|
|
328
|
+
* round trip after the shooter fired; a room that rewinds reports the tick its rewind answered
|
|
329
|
+
* from, which is the tick the shooter was looking at. Moving that tick **is** lag compensation,
|
|
330
|
+
* and the miss distance follows it.
|
|
331
|
+
* - **Estimated (D42, the fallback).** With no reported tick, the receive tick is the first
|
|
332
|
+
* recorded tick whose state settled at or after `sentAt` plus the shot's uplink delay
|
|
333
|
+
* (`rttMs / 2` of whatever was injected into that bot). It is an estimate, it is labelled one on
|
|
334
|
+
* every row, and it rests on the recorder and the shooter sharing a clock — true under
|
|
335
|
+
* `irtio dev`, where the room runs in a worker thread of the same process, and the reason this
|
|
336
|
+
* is a local-dev capability exactly as the recorded timeline is.
|
|
302
337
|
*
|
|
303
|
-
*
|
|
304
|
-
* the
|
|
338
|
+
* The authoritative value is read straight out of the recording either way, so a row can be
|
|
339
|
+
* checked against the timeline file by hand. A row whose target was not in the recording at that
|
|
340
|
+
* tick, or whose shot landed past the end of the recording, says so rather than reporting a
|
|
341
|
+
* distance of zero.
|
|
305
342
|
*/
|
|
306
343
|
|
|
307
344
|
/** One aim point: field name to value, over the target collection's numeric fields. */
|
|
@@ -315,6 +352,20 @@ interface ShotRequest {
|
|
|
315
352
|
readonly aim: AimPoint;
|
|
316
353
|
/** Free-form label, carried through to the row. */
|
|
317
354
|
readonly label?: string;
|
|
355
|
+
/**
|
|
356
|
+
* D72: the server tick the room says it **judged this shot at**, when the game's own RPC
|
|
357
|
+
* reports one. Supplying it makes the row exact — no clock is compared with any other clock —
|
|
358
|
+
* and the report drops the word `(estimated)`.
|
|
359
|
+
*
|
|
360
|
+
* For a room with no lag compensation that is the tick the `CALL` was applied at, a round trip
|
|
361
|
+
* after the shooter fired. For a room that rewinds it is the tick the rewind answered from,
|
|
362
|
+
* which is the tick the shooter was looking at. That difference is the whole measurement: the
|
|
363
|
+
* miss distance is always "how far the aim was from where authority had the target **at the
|
|
364
|
+
* tick the shot was judged**", and lag compensation is what moves that tick.
|
|
365
|
+
*/
|
|
366
|
+
readonly serverTick?: number | undefined;
|
|
367
|
+
/** D72: whether the room resolved this shot through `room.rewind`, when it says. */
|
|
368
|
+
readonly rewound?: boolean | undefined;
|
|
318
369
|
}
|
|
319
370
|
/** A shot as recorded, before any correlation. */
|
|
320
371
|
interface ShotRecord extends ShotRequest {
|
|
@@ -334,18 +385,27 @@ interface HitRow extends ShotRecord {
|
|
|
334
385
|
/**
|
|
335
386
|
* `true` when `serverTick` was derived through an injected uplink delay rather than read off a
|
|
336
387
|
* frame the shot could only have landed in. The report prints the word.
|
|
388
|
+
*
|
|
389
|
+
* D72: a shot whose room reported its own judging tick is never estimated, whatever the
|
|
390
|
+
* latency, because nothing was inferred from two clocks.
|
|
337
391
|
*/
|
|
338
392
|
readonly estimated: boolean;
|
|
339
393
|
/** The target's authoritative value at `serverTick`, over the fields the aim named. */
|
|
340
394
|
readonly authoritative: AimPoint | undefined;
|
|
341
395
|
/** Euclidean distance between `aim` and `authoritative`, or `undefined` when unknown. */
|
|
342
396
|
readonly missDistance: number | undefined;
|
|
397
|
+
/** D72: whether the room resolved this shot through `room.rewind`. `undefined` when it did not say. */
|
|
398
|
+
readonly rewound: boolean | undefined;
|
|
343
399
|
/** Why a row has no distance, in one clause. Absent when it has one. */
|
|
344
400
|
readonly unresolved?: string;
|
|
345
401
|
}
|
|
346
402
|
/**
|
|
347
403
|
* Correlates every shot against the recording. Pure: same shots and same dump, same rows, which
|
|
348
404
|
* is what lets a scenario assert on them.
|
|
405
|
+
*
|
|
406
|
+
* D72: a shot that carries `serverTick` is correlated to exactly that frame and is not estimated.
|
|
407
|
+
* A shot that does not falls back to the wall-clock path this started as, which is an estimate
|
|
408
|
+
* and says so on every row.
|
|
349
409
|
*/
|
|
350
410
|
declare function correlateShots(shots: readonly ShotRecord[], dump: TimelineDump): HitRow[];
|
|
351
411
|
|
|
@@ -511,6 +571,16 @@ declare function makeTrace(startedAt: number, rings: () => readonly TraceRing[])
|
|
|
511
571
|
* bytes does not.
|
|
512
572
|
*/
|
|
513
573
|
|
|
574
|
+
/**
|
|
575
|
+
* M6 lane E (D73-b): what this client's local world holds for one instance right now.
|
|
576
|
+
*
|
|
577
|
+
* The three states `room.prediction` distinguishes since D71, in the one word each the observer
|
|
578
|
+
* needs. `predicted` is simulated locally; `proxied` is a kinematic collider at the drawn pose,
|
|
579
|
+
* which predicted bodies stand on and never moves on its own; `absent` is no body at all, which a
|
|
580
|
+
* predicted body falls straight through. A bot that joined without physics sees everything as
|
|
581
|
+
* `absent`, which is exactly what it had before this existed.
|
|
582
|
+
*/
|
|
583
|
+
type BodyKind = 'predicted' | 'proxied' | 'absent';
|
|
514
584
|
/** Per-bot counters the report prints verbatim. */
|
|
515
585
|
interface BotStats {
|
|
516
586
|
readonly index: number;
|
|
@@ -530,6 +600,16 @@ interface BotStats {
|
|
|
530
600
|
* the meaning: once the client re-steps predicted bodies, these become real mispredictions.
|
|
531
601
|
*/
|
|
532
602
|
readonly syncCorrections: number;
|
|
603
|
+
/**
|
|
604
|
+
* M6 lane E (D73-b): corrections that touched only body fields of **proxied** instances.
|
|
605
|
+
*
|
|
606
|
+
* Reported, never thresholded. A proxy is moved to the pose the renderer draws, so a correction
|
|
607
|
+
* on one is the server disagreeing with what this client drew — the interpolation residual —
|
|
608
|
+
* rather than a prediction being wrong or a body arriving for the first time. Split out of
|
|
609
|
+
* `syncCorrections`, where these used to land: a run with no proxies counts exactly what it
|
|
610
|
+
* counted before.
|
|
611
|
+
*/
|
|
612
|
+
readonly proxiedCorrections: number;
|
|
533
613
|
/**
|
|
534
614
|
* D22 part 2: predicted-body corrections whose values matched the local prediction within the
|
|
535
615
|
* epsilon — authority confirming the prediction, not disagreeing with it. Counted apart so a
|
|
@@ -551,6 +631,37 @@ interface BotStats {
|
|
|
551
631
|
readonly disconnects: number;
|
|
552
632
|
readonly peakBytesInPerSec: number;
|
|
553
633
|
readonly peakCorrectionsPerSec: number;
|
|
634
|
+
/** D70: peer messages this bot sent, raw and typed together. */
|
|
635
|
+
readonly messagesSent: number;
|
|
636
|
+
/** D70: peer messages this bot received and could read. */
|
|
637
|
+
readonly messagesReceived: number;
|
|
638
|
+
/** D70: typed peer messages this bot could not read — an unknown index, or a bad payload. */
|
|
639
|
+
readonly messagesDropped: number;
|
|
640
|
+
/**
|
|
641
|
+
* M6 lane E (D73-a): did this bot's client ever simulate a local world during the run?
|
|
642
|
+
*
|
|
643
|
+
* Latched from `room.prediction.active` as frames arrive, rather than read when the report is
|
|
644
|
+
* built, and both halves of that are load-bearing. The engine loads *off* the join path, so a
|
|
645
|
+
* read at join time says `false` on every run; and `stop()` leaves the room before it takes the
|
|
646
|
+
* report, and a room that has been left has freed its world, so a read at the end says `false`
|
|
647
|
+
* too. What a caller wants to know is whether prediction was live while the run happened.
|
|
648
|
+
*
|
|
649
|
+
* False on a bot that joined without `physics`/`physics2d`. The report prints it rather than
|
|
650
|
+
* assuming that a run which asked for prediction got it.
|
|
651
|
+
*/
|
|
652
|
+
readonly predicting: boolean;
|
|
653
|
+
/**
|
|
654
|
+
* D73-b: the most kinematic proxies this bot's local world held at once
|
|
655
|
+
* (`room.prediction.stats.proxies`), sampled as frames arrived. Zero without prediction.
|
|
656
|
+
*/
|
|
657
|
+
readonly proxies: number;
|
|
658
|
+
/**
|
|
659
|
+
* D73-b: the most instances this bot's local world had **no body at all** for at once
|
|
660
|
+
* (`room.prediction.stats.absent`). A predicted body passes straight through one of these, so a
|
|
661
|
+
* non-zero value on a collection anything stands on is a gameplay bug rather than a tradeoff —
|
|
662
|
+
* which is why it is reported beside the proxies rather than folded into them.
|
|
663
|
+
*/
|
|
664
|
+
readonly absent: number;
|
|
554
665
|
}
|
|
555
666
|
/**
|
|
556
667
|
* Shared across bots: the bridge that turns "bot A wrote x=7" plus "bot B was told x=7" into a
|
|
@@ -580,8 +691,8 @@ interface ObserverOptions {
|
|
|
580
691
|
declare class BotObserver {
|
|
581
692
|
private readonly options;
|
|
582
693
|
readonly ring: TraceRing;
|
|
583
|
-
readonly violations: Map<"schema-validity" | "visibility-leak" | "bandwidth" | "handler-error" | "correction-storm" | "misprediction" | "snaps" | "disconnects" | "tick-health", string[]>;
|
|
584
|
-
readonly counts: Map<"schema-validity" | "visibility-leak" | "bandwidth" | "handler-error" | "correction-storm" | "misprediction" | "snaps" | "disconnects" | "tick-health", number>;
|
|
694
|
+
readonly violations: Map<"schema-validity" | "visibility-leak" | "bandwidth" | "handler-error" | "correction-storm" | "misprediction" | "snaps" | "disconnects" | "tick-health" | "typed-message-drops" | "party-integrity", string[]>;
|
|
695
|
+
readonly counts: Map<"schema-validity" | "visibility-leak" | "bandwidth" | "handler-error" | "correction-storm" | "misprediction" | "snaps" | "disconnects" | "tick-health" | "typed-message-drops" | "party-integrity", number>;
|
|
585
696
|
/**
|
|
586
697
|
* Bug #28: the decode extension, *mutable*. It starts as `options.ext` (the schema the run was
|
|
587
698
|
* spawned with) and is rebuilt in place when an inbound `SCHEMA` frame (13) lands — a D50
|
|
@@ -609,14 +720,29 @@ declare class BotObserver {
|
|
|
609
720
|
bytesOut: number;
|
|
610
721
|
corrections: number;
|
|
611
722
|
syncCorrections: number;
|
|
723
|
+
proxiedCorrections: number;
|
|
612
724
|
suppressedCorrections: number;
|
|
613
725
|
mispredictions: number;
|
|
614
726
|
/**
|
|
615
|
-
*
|
|
616
|
-
* `spawnBots` once the room exists (`room.prediction`);
|
|
617
|
-
* it cannot be derived from the bytes the observer otherwise sticks to.
|
|
727
|
+
* M6 lane E (D73-b): what this bot's client's local world holds for `collection[id]` right now.
|
|
728
|
+
* Assigned by `spawnBots` once the room exists (`room.prediction`); the local world's shape is
|
|
729
|
+
* client-local, so it cannot be derived from the bytes the observer otherwise sticks to.
|
|
730
|
+
*
|
|
731
|
+
* Unset means "no local world at all", which answers `absent` for everything — the behaviour a
|
|
732
|
+
* bot without physics has always had.
|
|
733
|
+
*/
|
|
734
|
+
bodyKind: ((collection: string, id: string) => BodyKind) | undefined;
|
|
735
|
+
/**
|
|
736
|
+
* M6 lane E (D73-a): the client's own `room.prediction`, when this bot joined with `physics` or
|
|
737
|
+
* `physics2d`. Held rather than copied because `active` flips once the engine has loaded, which
|
|
738
|
+
* happens off the join path — a boolean read at join time would say `false` on every run.
|
|
618
739
|
*/
|
|
619
|
-
|
|
740
|
+
prediction: PredictionStatus | undefined;
|
|
741
|
+
/** D73-a: latched true the first time {@link prediction} reported an active local world. */
|
|
742
|
+
predicted: boolean;
|
|
743
|
+
/** D73-b: the high-water marks of `prediction.stats.proxies` / `.absent` across the run. */
|
|
744
|
+
proxies: number;
|
|
745
|
+
absent: number;
|
|
620
746
|
mispredictionMagnitude: number;
|
|
621
747
|
mispredictionMax: number;
|
|
622
748
|
snaps: number;
|
|
@@ -625,6 +751,10 @@ declare class BotObserver {
|
|
|
625
751
|
disconnects: number;
|
|
626
752
|
peakBytesInPerSec: number;
|
|
627
753
|
peakCorrectionsPerSec: number;
|
|
754
|
+
/** D70: peer-message counters, filled by `inspect`'s MSG case in both directions. */
|
|
755
|
+
messagesSent: number;
|
|
756
|
+
messagesReceived: number;
|
|
757
|
+
messagesDropped: number;
|
|
628
758
|
/**
|
|
629
759
|
* Spatial-grid ops actually put to the AOI policy. Zero on a run whose room has no spatial
|
|
630
760
|
* collection; zero on a run that *does* and would mean the invariant passed vacuously, which is
|
|
@@ -659,6 +789,22 @@ declare class BotObserver {
|
|
|
659
789
|
onFrame(dir: 'in' | 'out', type: number, bytes: Uint8Array): void;
|
|
660
790
|
/** Decodes the payload independently. Throws on a bad frame; the caller counts that. */
|
|
661
791
|
private inspect;
|
|
792
|
+
/**
|
|
793
|
+
* D70: one peer message, in either direction, as a trace note.
|
|
794
|
+
*
|
|
795
|
+
* Before this, `MSG` fell through to `undefined` and a trace showed a frame with a byte count
|
|
796
|
+
* and nothing else — which was tolerable while every message was opaque bytes and is not now
|
|
797
|
+
* that some of them have declared shapes. A raw message notes its target; a typed one notes its
|
|
798
|
+
* name and its value when this bot holds the schema, and its index when it does not.
|
|
799
|
+
*
|
|
800
|
+
* A typed message this bot cannot read is counted as a **drop**, which is what the
|
|
801
|
+
* `typed-message-drops` invariant fails a run on. That is the whole point of the counter: a
|
|
802
|
+
* scenario sending a shape the schema does not describe used to be a message that silently
|
|
803
|
+
* never arrived.
|
|
804
|
+
*/
|
|
805
|
+
private onMsg;
|
|
806
|
+
/** D70: one typed message this bot could not read — a counter and an invariant violation. */
|
|
807
|
+
private dropTyped;
|
|
662
808
|
private onWelcome;
|
|
663
809
|
/**
|
|
664
810
|
* Bug #28: a `SCHEMA` frame (D50 additive migrate). Rebuild the decode extension from the
|
|
@@ -721,6 +867,9 @@ interface SimulationTotals {
|
|
|
721
867
|
readonly corrections: number;
|
|
722
868
|
/** D22: corrections carrying only simulated body state — the sync path, not a disagreement. */
|
|
723
869
|
readonly syncCorrections: number;
|
|
870
|
+
/** D73-b: corrections carrying only body state of **proxied** instances — the interpolation
|
|
871
|
+
* residual, not a misprediction. Reported, never thresholded. */
|
|
872
|
+
readonly proxiedCorrections: number;
|
|
724
873
|
/** D22 part 2: predicted-body corrections matching the prediction within epsilon (quiet). */
|
|
725
874
|
readonly suppressedCorrections: number;
|
|
726
875
|
/** Correction ops seen via the rooms' `correct` events, across every bot. */
|
|
@@ -733,6 +882,70 @@ interface SimulationTotals {
|
|
|
733
882
|
readonly snaps: number;
|
|
734
883
|
readonly calls: number;
|
|
735
884
|
readonly errors: number;
|
|
885
|
+
/** D70: peer messages sent across every bot, raw and typed together. */
|
|
886
|
+
readonly messagesSent: number;
|
|
887
|
+
/** D70: peer messages received and read across every bot. */
|
|
888
|
+
readonly messagesReceived: number;
|
|
889
|
+
/** D70: typed peer messages no bot could read. Any of these fails `typed-message-drops`. */
|
|
890
|
+
readonly messagesDropped: number;
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* M6 lane E (D73-b): what the bots' local worlds held, across the run. Absent when no bot
|
|
894
|
+
* predicted, so a run without physics prints exactly what it printed before.
|
|
895
|
+
*
|
|
896
|
+
* `proxies` and `absent` are the **peaks**, summed over the bots that predicted — the worst
|
|
897
|
+
* moment, not an average, because the number that matters is how much of the world a client was
|
|
898
|
+
* ever unable to simulate. There is deliberately no "predicted bodies" count: `PredictionStats`
|
|
899
|
+
* exposes none, and inventing one here would mean the observer enumerating the local world every
|
|
900
|
+
* frame to answer a question the client can answer itself.
|
|
901
|
+
*/
|
|
902
|
+
interface PredictionSummary {
|
|
903
|
+
/** Bots whose client held a live local world at some point in the run. */
|
|
904
|
+
readonly bots: number;
|
|
905
|
+
/** Peak kinematic proxies (D71), summed across those bots. */
|
|
906
|
+
readonly proxies: number;
|
|
907
|
+
/** Peak instances with no local body at all, summed across those bots. */
|
|
908
|
+
readonly absent: number;
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* M6 lane E (D73-c): what the queue did, for the report's matchmaking section.
|
|
912
|
+
*
|
|
913
|
+
* `waitP50Ms`/`waitP95Ms` are over the tickets that were answered; a timeout contributes to
|
|
914
|
+
* `timeouts` and not to the percentiles, because folding a deadline into a wait distribution turns
|
|
915
|
+
* "nobody came" into "everybody waited exactly the timeout", which reads as a slow queue rather
|
|
916
|
+
* than an empty one.
|
|
917
|
+
*/
|
|
918
|
+
interface MatchmakingSummary {
|
|
919
|
+
/** Tickets answered. */
|
|
920
|
+
readonly tickets: number;
|
|
921
|
+
/** Distinct rooms those tickets named. */
|
|
922
|
+
readonly rooms: number;
|
|
923
|
+
readonly waitP50Ms: number;
|
|
924
|
+
readonly waitP95Ms: number;
|
|
925
|
+
readonly waitMaxMs: number;
|
|
926
|
+
/** Tickets that came back `E_NO_MATCH` — the queue's own deadline. */
|
|
927
|
+
readonly timeouts: number;
|
|
928
|
+
/** Every other way a ticket failed, by code. */
|
|
929
|
+
readonly failures: readonly {
|
|
930
|
+
readonly requested: number;
|
|
931
|
+
readonly code: string;
|
|
932
|
+
}[];
|
|
933
|
+
/** Tickets answered with a seat in a room that was already running. */
|
|
934
|
+
readonly backfills: number;
|
|
935
|
+
/** Parties that landed whole, out of the parties that queued. */
|
|
936
|
+
readonly partiesIntact: number;
|
|
937
|
+
readonly parties: number;
|
|
938
|
+
/** Rooms holding more bots than their ticket said they seat. Any is a `party-integrity` fail. */
|
|
939
|
+
readonly overfullRooms: readonly {
|
|
940
|
+
readonly room: string;
|
|
941
|
+
readonly bots: number;
|
|
942
|
+
readonly size: number;
|
|
943
|
+
}[];
|
|
944
|
+
/** Parties whose members did not all land in one room. */
|
|
945
|
+
readonly splitParties: readonly {
|
|
946
|
+
readonly party: number;
|
|
947
|
+
readonly rooms: readonly string[];
|
|
948
|
+
}[];
|
|
736
949
|
}
|
|
737
950
|
interface SimulationReport {
|
|
738
951
|
/** True when every invariant passed. `irtio simulate` exits 1 when it is not. */
|
|
@@ -757,7 +970,13 @@ interface SimulationReport {
|
|
|
757
970
|
* per-bot rate the rest of this report prints. Absent when nobody profiled.
|
|
758
971
|
*/
|
|
759
972
|
readonly profile?: ProfileSnapshot;
|
|
973
|
+
/** D73-b: the local worlds the bots held. Absent when no bot predicted. */
|
|
974
|
+
readonly prediction?: PredictionSummary;
|
|
975
|
+
/** D73-c: what the queue did. Absent when the run did not queue. */
|
|
976
|
+
readonly matchmaking?: MatchmakingSummary;
|
|
760
977
|
}
|
|
978
|
+
/** D73-c: folds the queue's answers into the summary and the `party-integrity` verdict. */
|
|
979
|
+
declare function matchmakingSummary(reading: MatchmakingReading): MatchmakingSummary;
|
|
761
980
|
/** `undefined` when nothing converged during the run — an honest gap beats a fabricated zero. */
|
|
762
981
|
declare function convergenceStats(lags: readonly number[]): ConvergenceStats | undefined;
|
|
763
982
|
interface BuildReportOptions {
|
|
@@ -772,12 +991,103 @@ interface BuildReportOptions {
|
|
|
772
991
|
* reports `unavailable`, which is the honest answer for a caller that could not read it.
|
|
773
992
|
*/
|
|
774
993
|
readonly tickHealth?: TickHealthReading | undefined;
|
|
994
|
+
/** D73-c: what the queue answered per bot. Absent ⇒ `party-integrity` reports `unavailable`. */
|
|
995
|
+
readonly matchmaking?: MatchmakingReading | undefined;
|
|
775
996
|
/** D65: one ledger per bot, merged into `SimulationReport.profile`. */
|
|
776
997
|
readonly profiles?: readonly ProfileSnapshot[] | undefined;
|
|
777
998
|
}
|
|
778
999
|
/** Folds the observers into the report `irtio simulate` prints and tests assert on. */
|
|
779
1000
|
declare function buildReport(options: BuildReportOptions): SimulationReport;
|
|
780
1001
|
|
|
1002
|
+
/**
|
|
1003
|
+
* M6 lane E (D73-c): bots that queue.
|
|
1004
|
+
*
|
|
1005
|
+
* `spawnBots` has always put every bot in one room by construction — bot 0 creates it and the rest
|
|
1006
|
+
* join the code it came back with — which is right for a load run and means matchmaking, the front
|
|
1007
|
+
* door every game will use, had never been driven by the harness that exists to drive things. This
|
|
1008
|
+
* module is the other way in: each bot calls the real `findMatch` against a real control plane and
|
|
1009
|
+
* joins whatever room its ticket names.
|
|
1010
|
+
*
|
|
1011
|
+
* Real, in the same sense the rest of `@irtio/bots` is real. `findMatch` is `@irtio/client`'s own
|
|
1012
|
+
* function over its own HTTP, `createParty` mints a real code, and the room the ticket names is
|
|
1013
|
+
* joined through the ordinary `joinRoom` — nothing here knows it was a matchmaker that filled the
|
|
1014
|
+
* room, which is the property D52's whole design rests on. The one seam is `fetch`, injected so a
|
|
1015
|
+
* unit test can answer tickets without a plane.
|
|
1016
|
+
*
|
|
1017
|
+
* Parties are a group of bot indices sharing a `party(index)` value. One member mints the code and
|
|
1018
|
+
* every member sends it with its own call, exactly as two friends would: nobody sends anybody
|
|
1019
|
+
* else's identity, because a device credential is a secret that never leaves the browser that
|
|
1020
|
+
* minted it. A group of one queues alone rather than minting a party of one, which the queue would
|
|
1021
|
+
* accept and which would prove nothing.
|
|
1022
|
+
*/
|
|
1023
|
+
|
|
1024
|
+
/** How long a bot waits for a ticket when nothing says otherwise. Control caps this at two
|
|
1025
|
+
* minutes and applies its own default (30 s) when it is omitted; a run wants a bound it can
|
|
1026
|
+
* outlive rather than one it inherits. */
|
|
1027
|
+
declare const DEFAULT_MATCH_TIMEOUT_MS = 20000;
|
|
1028
|
+
interface BotMatchOptions {
|
|
1029
|
+
/** The control plane origin. `irtio dev` runs none, so a queued run needs a real one. */
|
|
1030
|
+
readonly controlUrl: string;
|
|
1031
|
+
/** The project the queue belongs to — a project key, as `schema.project` carries it. */
|
|
1032
|
+
readonly project: string;
|
|
1033
|
+
/** Queue name. Omitted uses the project's `default` queue (a party of two). */
|
|
1034
|
+
readonly queue?: string;
|
|
1035
|
+
/**
|
|
1036
|
+
* A platform identity assertion per bot, when the run has them.
|
|
1037
|
+
*
|
|
1038
|
+
* The queue's one-ticket-per-player rule keys on the account an identity belongs to; without
|
|
1039
|
+
* one it keys on a fresh anonymous id per request, which is why twenty anonymous bots can queue
|
|
1040
|
+
* at once and why a run that wants to prove the dedupe rule has to mint identities first.
|
|
1041
|
+
*/
|
|
1042
|
+
readonly identity?: (index: number) => string | undefined;
|
|
1043
|
+
/**
|
|
1044
|
+
* Which party group this bot queues in, or `undefined` to queue alone. Bots answering the same
|
|
1045
|
+
* value are one party and land in one room.
|
|
1046
|
+
*/
|
|
1047
|
+
readonly party?: (index: number) => number | undefined;
|
|
1048
|
+
readonly timeoutMs?: number;
|
|
1049
|
+
/** @internal Test seam: the unit tests answer tickets without a control plane. */
|
|
1050
|
+
readonly fetch?: typeof fetch;
|
|
1051
|
+
}
|
|
1052
|
+
/** One bot's answer from the queue. */
|
|
1053
|
+
interface BotTicket {
|
|
1054
|
+
/** The bot this became, once the failures were dropped — see {@link BotMatchResult}. */
|
|
1055
|
+
readonly bot: number;
|
|
1056
|
+
/** Which of the `n` queue attempts this was. Equal to `bot` when nothing failed. */
|
|
1057
|
+
readonly requested: number;
|
|
1058
|
+
readonly ticket: MatchTicket;
|
|
1059
|
+
/** Wall clock from the call to the answer. The number a "still looking…" screen would show. */
|
|
1060
|
+
readonly waitedMs: number;
|
|
1061
|
+
/** The party group this bot queued in, when it queued in one. */
|
|
1062
|
+
readonly party?: number;
|
|
1063
|
+
}
|
|
1064
|
+
/** One bot that never got a ticket. A timeout is one of these, not a hang. */
|
|
1065
|
+
interface BotMatchFailure {
|
|
1066
|
+
readonly requested: number;
|
|
1067
|
+
readonly code: string;
|
|
1068
|
+
readonly message: string;
|
|
1069
|
+
readonly waitedMs: number;
|
|
1070
|
+
readonly party?: number;
|
|
1071
|
+
}
|
|
1072
|
+
interface BotMatchResult {
|
|
1073
|
+
/** Tickets in request order, renumbered densely so `bot` indexes the run that follows. */
|
|
1074
|
+
readonly tickets: readonly BotTicket[];
|
|
1075
|
+
readonly failures: readonly BotMatchFailure[];
|
|
1076
|
+
/** The party code minted per group, for the groups that queued as parties. */
|
|
1077
|
+
readonly parties: ReadonlyMap<number, string>;
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Queues `n` bots and answers what the queue said, per bot.
|
|
1081
|
+
*
|
|
1082
|
+
* Nothing throws for one bot's failure: a queue that refused three of twenty is a measurement, and
|
|
1083
|
+
* the caller decides what to do with it (`spawnBots` runs the ones that got in and fails the run).
|
|
1084
|
+
* A party whose code could not be minted fails as a whole, because a party that has lost its code
|
|
1085
|
+
* is a group of strangers and queueing them anyway would quietly test something else.
|
|
1086
|
+
*/
|
|
1087
|
+
declare function matchBots(n: number, options: BotMatchOptions): Promise<BotMatchResult>;
|
|
1088
|
+
/** Bot indices per room, in the order the tickets came back. */
|
|
1089
|
+
declare function roomsOf(tickets: readonly BotTicket[]): Map<string, number[]>;
|
|
1090
|
+
|
|
781
1091
|
/**
|
|
782
1092
|
* `spawnBots` — N real `@irtio/client` sessions in one Node process, each running a script, each
|
|
783
1093
|
* watched by a `BotObserver`.
|
|
@@ -886,6 +1196,12 @@ interface SpawnOptionsBase {
|
|
|
886
1196
|
readonly snapsMax?: number;
|
|
887
1197
|
/** `tick-health` threshold: server tick overruns tolerated in the run window. Default 0. */
|
|
888
1198
|
readonly overrunsMax?: number;
|
|
1199
|
+
/**
|
|
1200
|
+
* D70 `typed-message-drops` threshold: typed peer messages a bot may fail to read across the
|
|
1201
|
+
* run. Default 0 — a drop means a peer sent a shape this schema does not declare, which in a
|
|
1202
|
+
* simulation is a scenario and a room disagreeing.
|
|
1203
|
+
*/
|
|
1204
|
+
readonly typedMessageDropsMax?: number;
|
|
889
1205
|
/**
|
|
890
1206
|
* D36: how long one bot's initial join may take before `spawnBots` gives up on the whole run.
|
|
891
1207
|
* Default {@link DEFAULT_JOIN_TIMEOUT_MS}; `0` restores the old unbounded wait.
|
|
@@ -908,6 +1224,15 @@ interface SpawnOptionsBase {
|
|
|
908
1224
|
* sockets at all. The vocabulary is deliberately the same and the two are not interchangeable.
|
|
909
1225
|
*/
|
|
910
1226
|
readonly conditions?: NetworkConditions | ((index: number) => NetworkConditions | undefined);
|
|
1227
|
+
/**
|
|
1228
|
+
* M6 lane E (D73-c): queue for rooms instead of building one.
|
|
1229
|
+
*
|
|
1230
|
+
* With this set the one-room contract below does not apply: every bot calls the real
|
|
1231
|
+
* `findMatch` against the control plane named here and joins whatever room its ticket names, so
|
|
1232
|
+
* a run drives the front door a game actually has. Bots sharing a `party(index)` value queue as
|
|
1233
|
+
* one party and land together. `room` is ignored — the queue decides.
|
|
1234
|
+
*/
|
|
1235
|
+
readonly match?: BotMatchOptions;
|
|
911
1236
|
}
|
|
912
1237
|
interface SpawnOptions<S extends AnySchema> extends SpawnOptionsBase {
|
|
913
1238
|
readonly schema: S;
|
|
@@ -920,6 +1245,15 @@ interface SpawnOptions<S extends AnySchema> extends SpawnOptionsBase {
|
|
|
920
1245
|
* corrections classify as real mispredictions (in world units) instead of `syncCorrections`.
|
|
921
1246
|
*/
|
|
922
1247
|
readonly physics?: JoinOptions<S>['physics'];
|
|
1248
|
+
/**
|
|
1249
|
+
* M6 lane E (D73-a): the matter2d twin of `physics`, passed to every bot's
|
|
1250
|
+
* `joinRoom({ physics2d })`.
|
|
1251
|
+
*
|
|
1252
|
+
* The two are mutually exclusive for the same reason `joinRoom` makes them so — a room runs one
|
|
1253
|
+
* engine and the client predicts with that one — and passing both throws with the sentence
|
|
1254
|
+
* `joinRoom` uses, before any socket opens.
|
|
1255
|
+
*/
|
|
1256
|
+
readonly physics2d?: JoinOptions<S>['physics2d'];
|
|
923
1257
|
}
|
|
924
1258
|
/** Schema-less relay: no schema, so `joinRelay` and a `RelayRoom`. */
|
|
925
1259
|
interface RelaySpawnOptions extends SpawnOptionsBase {
|
|
@@ -936,7 +1270,21 @@ interface BotConditions {
|
|
|
936
1270
|
}
|
|
937
1271
|
interface BotRunner<S = undefined> extends Iterable<Bot<S>> {
|
|
938
1272
|
readonly bots: readonly Bot<S>[];
|
|
1273
|
+
/**
|
|
1274
|
+
* The room this run is about. With `match`, several rooms exist and this is the first ticket's
|
|
1275
|
+
* — the one a caller reading the server's own counters has to pick one of. `rooms` has them all.
|
|
1276
|
+
*/
|
|
939
1277
|
readonly roomId: string;
|
|
1278
|
+
/** D73-c: the ticket each bot queued for, in bot order. Empty on a run that did not queue. */
|
|
1279
|
+
readonly matches: readonly BotTicket[];
|
|
1280
|
+
/** D73-c: bot indices by room. One entry on a run that did not queue. */
|
|
1281
|
+
readonly rooms: ReadonlyMap<string, readonly number[]>;
|
|
1282
|
+
/** D73-c: bots that never got a ticket, and what the queue said. Empty when nothing failed. */
|
|
1283
|
+
readonly matchFailures: readonly {
|
|
1284
|
+
requested: number;
|
|
1285
|
+
code: string;
|
|
1286
|
+
message: string;
|
|
1287
|
+
}[];
|
|
940
1288
|
/** Every bot's frames, merged and time-ordered. `trace.save(path)` writes JSON. */
|
|
941
1289
|
readonly trace: Trace;
|
|
942
1290
|
/** Anything a script threw, in the order it happened. */
|
|
@@ -1185,4 +1533,4 @@ interface RelayEchoScriptOptions {
|
|
|
1185
1533
|
*/
|
|
1186
1534
|
declare function relayEchoScript(options?: RelayEchoScriptOptions): BotScript<undefined>;
|
|
1187
1535
|
|
|
1188
|
-
export { type AimPoint, type AssertionResult, type Bot, type BotConditions, BotObserver, type BotRoom, type BotRunner, type BotScript, type BotStats, type BuildReportOptions, type ClientStateView, type ConditionCounters, type ConditionTimers, type ConditionedSocket, type ConditionedTransport, type ConditionedTransportOptions, type ConvergenceStats, DEFAULT_JOIN_TIMEOUT_MS, DEFAULT_REORDER_MS, DEFAULT_ROOM_GONE_GRACE_MS, DEFAULT_SEED, DEFAULT_THRESHOLDS, DEFAULT_TRACE_LIMIT, type HitRow, INVARIANT_NAMES, type InvariantName, type InvariantResult, type InvariantState, type InvariantThresholds, JoinTimeoutError, type NetworkConditions, type ObserverOptions, type RandomScriptOptions, type RelayEchoScriptOptions, type RelaySpawnOptions, type Rng, type RunEnd, type ScenarioDefinition, type ScenarioEvidence, type ShotRecord, type ShotRequest, type SimulationReport, type SimulationTotals, type SpatialVisibilityContext, type SpawnOptions, type TickHealthReading, TickNotRecordedError, type Timeline, type TimelineCollection, type TimelineMoment, type TimelineRecord, type TimelineState, type Trace, type TraceDump, type TraceEntry, TraceRing, type TruthBotResult, type TruthDiff, type TruthDifference, UnknownCollectionError, type UntilOptions, type ValueContext, WriteLog, buildReport, captureClientState, cheatPredicate, conditionedTransport, convergenceStats, correlateShots, defineScenario, deltaVisibilityLeaks, describeConditions, diffAgainstSave, frameName, frameVisibilityLeaks, freshValue, hasConditions, looksLikeScenario, makeRng, makeTimeline, makeTrace, newConditionCounters, nextValue, randomScript, relayEchoScript, snapshotVisibilityLeaks, spawnBots };
|
|
1536
|
+
export { type AimPoint, type AssertionResult, type BodyKind, type Bot, type BotConditions, type BotMatchFailure, type BotMatchOptions, type BotMatchResult, BotObserver, type BotRoom, type BotRunner, type BotScript, type BotStats, type BotTicket, type BuildReportOptions, type ClientStateView, type ConditionCounters, type ConditionTimers, type ConditionedSocket, type ConditionedTransport, type ConditionedTransportOptions, type ConvergenceStats, DEFAULT_JOIN_TIMEOUT_MS, DEFAULT_MATCH_TIMEOUT_MS, DEFAULT_REORDER_MS, DEFAULT_ROOM_GONE_GRACE_MS, DEFAULT_SEED, DEFAULT_THRESHOLDS, DEFAULT_TRACE_LIMIT, type HitRow, INVARIANT_NAMES, type InvariantName, type InvariantResult, type InvariantState, type InvariantThresholds, JoinTimeoutError, type MatchmakingReading, type MatchmakingSummary, type NetworkConditions, type ObserverOptions, type PredictionSummary, type RandomScriptOptions, type RelayEchoScriptOptions, type RelaySpawnOptions, type Rng, type RunEnd, type ScenarioDefinition, type ScenarioEvidence, type ShotRecord, type ShotRequest, type SimulationReport, type SimulationTotals, type SpatialVisibilityContext, type SpawnOptions, type TickHealthReading, TickNotRecordedError, type Timeline, type TimelineCollection, type TimelineMoment, type TimelineRecord, type TimelineState, type Trace, type TraceDump, type TraceEntry, TraceRing, type TruthBotResult, type TruthDiff, type TruthDifference, UnknownCollectionError, type UntilOptions, type ValueContext, WriteLog, buildReport, captureClientState, cheatPredicate, conditionedTransport, convergenceStats, correlateShots, defineScenario, deltaVisibilityLeaks, describeConditions, diffAgainstSave, frameName, frameVisibilityLeaks, freshValue, hasConditions, looksLikeScenario, makeRng, makeTimeline, makeTrace, matchBots, matchmakingSummary, newConditionCounters, nextValue, randomScript, relayEchoScript, roomsOf, snapshotVisibilityLeaks, spawnBots };
|