@vgai/live 0.5.41 → 0.5.42
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/.tsbuildinfo +1 -1
- package/dist/editor.d.ts +2 -0
- package/dist/editor.js +4 -0
- package/dist/game-client/client.d.ts +4 -1
- package/dist/game-client/client.js +43 -23
- package/dist/game-client/fast-forward.d.ts +5 -0
- package/package.json +4 -3
- package/src/editor.ts +5 -0
- package/src/game-client/client.ts +60 -12
- package/src/game-client/fast-forward.ts +5 -0
package/dist/editor.d.ts
CHANGED
|
@@ -178,6 +178,8 @@ export declare class LiveEditor {
|
|
|
178
178
|
* tree would be a fabricated answer about a surface nobody is being shown.
|
|
179
179
|
*/
|
|
180
180
|
hierarchy(): Promise<InspectedHierarchy>;
|
|
181
|
+
/** Expand every branch through the Hierarchy panel's own action. */
|
|
182
|
+
expandHierarchyAll(): Promise<void>;
|
|
181
183
|
/**
|
|
182
184
|
* Write one editable field from `inspect()` by its stable path, through the
|
|
183
185
|
* same Inspector IO and persistence boundary the human control uses.
|
package/dist/editor.js
CHANGED
|
@@ -289,6 +289,10 @@ export class LiveEditor {
|
|
|
289
289
|
async hierarchy() {
|
|
290
290
|
return this.#client.hierarchy();
|
|
291
291
|
}
|
|
292
|
+
/** Expand every branch through the Hierarchy panel's own action. */
|
|
293
|
+
async expandHierarchyAll() {
|
|
294
|
+
await this.#client.expandHierarchyAll();
|
|
295
|
+
}
|
|
292
296
|
/**
|
|
293
297
|
* Write one editable field from `inspect()` by its stable path, through the
|
|
294
298
|
* same Inspector IO and persistence boundary the human control uses.
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import type { Page } from '@playwright/test';
|
|
10
10
|
import type { BridgeCallOutcome, BridgeTransport } from './bridge-transport.js';
|
|
11
11
|
import { type CaptureListener, type CaptureNotes } from './capture-notes.js';
|
|
12
|
-
import { type FastForwardBudget, type FastForwardOptions } from './fast-forward.js';
|
|
12
|
+
import { type FastForwardBudget, type FastForwardOptions, type FastForwardTime } from './fast-forward.js';
|
|
13
13
|
import { type TpsStats } from './perf-sampling.js';
|
|
14
14
|
import type { DebugCommandInfo, DebugSnapshot, ProviderInfo, VirtualActionValue } from './types.js';
|
|
15
15
|
import { type WaitForBudget } from './wait-for.js';
|
|
@@ -252,6 +252,9 @@ export declare class GameClient {
|
|
|
252
252
|
* AFTER the tps baseline reset below, so it never itself corrupts the tps
|
|
253
253
|
* stats either).
|
|
254
254
|
*/
|
|
255
|
+
fastForward(budget: FastForwardBudget, opts: FastForwardOptions & {
|
|
256
|
+
result: 'time';
|
|
257
|
+
}): Promise<FastForwardTime>;
|
|
255
258
|
fastForward(budget: FastForwardBudget, opts?: FastForwardOptions): Promise<DebugSnapshot>;
|
|
256
259
|
waitFor(pred: (s: (name: string) => unknown) => boolean, budget: WaitForBudget): Promise<void>;
|
|
257
260
|
/** Used by `input.hold` — waits for a sim-time delta to pass with no
|
|
@@ -77,6 +77,24 @@ async function bridgeCallInPageAsync(args) {
|
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* A relay step crosses the wire as function source, so compiler-owned helpers that live beside the
|
|
82
|
+
* function in its Node module are closures too. esbuild's `keepNames` transform is the live case:
|
|
83
|
+
* a named helper inside an otherwise literal callback becomes `__name(fn, "helper")`, while the
|
|
84
|
+
* module-level `__name` implementation is absent after `step.toString()`. Seat that exact compiler
|
|
85
|
+
* primitive inside the serialized function instead of installing a page global or asking every
|
|
86
|
+
* caller to avoid ordinary named local helpers.
|
|
87
|
+
*
|
|
88
|
+
* Ordinary source stays byte-for-byte unchanged. That preserves the public wire account and keeps
|
|
89
|
+
* unsupported user closures loud; this only completes the source for a compiler helper whose
|
|
90
|
+
* semantics are intrinsic and deterministic.
|
|
91
|
+
*/
|
|
92
|
+
function selfContainedStepSource(step) {
|
|
93
|
+
const source = step.toString();
|
|
94
|
+
if (!/\b__name\s*\(/u.test(source))
|
|
95
|
+
return source;
|
|
96
|
+
return `(scope) => { const __name = (target, value) => Object.defineProperty(target, "name", { value, configurable: true }); return (${source})(scope); }`;
|
|
97
|
+
}
|
|
80
98
|
/**
|
|
81
99
|
* #140 — the `BridgeTransport` `page.evaluate` implementation. This is the
|
|
82
100
|
* ONE place a `Page` is ever touched to drive `window.__vgai` (module doc
|
|
@@ -527,18 +545,6 @@ export class GameClient {
|
|
|
527
545
|
hiddenRecoveryTriggered() {
|
|
528
546
|
return this.#hiddenRecovery.wasTriggered();
|
|
529
547
|
}
|
|
530
|
-
/**
|
|
531
|
-
* D15/T-D15.4 — synchronously drives `budget` worth of sim time/ticks via
|
|
532
|
-
* the live `Game`'s `runTicks` (through the debug bridge), instead of
|
|
533
|
-
* waiting for real wall-clock time to pass. Doctrine (see
|
|
534
|
-
* `fast-forward.ts`'s module doc, which also documents the two honesty
|
|
535
|
-
* decisions this wraps): SETUP/STAGING traversal — reaching a known
|
|
536
|
-
* late-game state fast — not a substitute for real-input proofs, which
|
|
537
|
-
* still run in real ticks. Returns the final `{time, state, events,
|
|
538
|
-
* pageErrors}` snapshot (a completely ordinary `snapshot()` read, taken
|
|
539
|
-
* AFTER the tps baseline reset below, so it never itself corrupts the tps
|
|
540
|
-
* stats either).
|
|
541
|
-
*/
|
|
542
548
|
async fastForward(budget, opts) {
|
|
543
549
|
// Same options-object-only contract as `waitSimTime` (`ticksForBudget`
|
|
544
550
|
// would otherwise fail on `'simTicks' in 0.5` with a raw TypeError that
|
|
@@ -570,21 +576,26 @@ export class GameClient {
|
|
|
570
576
|
}
|
|
571
577
|
},
|
|
572
578
|
readTime: async () => {
|
|
573
|
-
// Raw bridge read — deliberately NOT `this.snapshot()`,
|
|
574
|
-
//
|
|
575
|
-
|
|
576
|
-
|
|
579
|
+
// Raw, clock-only bridge read — deliberately NOT `this.snapshot()`,
|
|
580
|
+
// which both feeds the TpsAccumulator and serializes every declared
|
|
581
|
+
// provider. Large imported worlds can carry megabytes of census state;
|
|
582
|
+
// the batching math needs only these two numbers.
|
|
583
|
+
const time = await this.callBridge('state', 'time');
|
|
584
|
+
return { tick: time.tick, simSeconds: time.simSeconds };
|
|
577
585
|
},
|
|
578
586
|
heartbeat: (info) => {
|
|
579
587
|
console.log(`vgai fastForward: ${info.ticksDone}/${info.ticksTotal} ticks driven`);
|
|
580
588
|
},
|
|
581
589
|
};
|
|
582
|
-
await runFastForward(budget, opts ?? {}, clock);
|
|
590
|
+
const finalTime = await runFastForward(budget, opts ?? {}, clock);
|
|
583
591
|
// The burst is over — reset the baseline so the very next ordinary poll
|
|
584
592
|
// (including the `snapshot()` call right below) treats itself as a fresh
|
|
585
593
|
// "first observation" rather than diffing across the burst's enormous
|
|
586
594
|
// tick delta over a near-zero wall delta.
|
|
587
595
|
this.#tps.resetBaseline();
|
|
596
|
+
if (opts?.result === 'time') {
|
|
597
|
+
return finalTime;
|
|
598
|
+
}
|
|
588
599
|
return this.snapshot();
|
|
589
600
|
}
|
|
590
601
|
async waitFor(pred, budget) {
|
|
@@ -626,7 +637,13 @@ export class GameClient {
|
|
|
626
637
|
// fires either. Refuse in the caller's own vocabulary instead of hanging.
|
|
627
638
|
assertValidWaitForBudget(budget, 'waitSimTime');
|
|
628
639
|
const startWall = Date.now();
|
|
629
|
-
|
|
640
|
+
// Predicate-free means clock-only from the FIRST read, not merely from the
|
|
641
|
+
// second poll onward. A full initial snapshot serializes every provider;
|
|
642
|
+
// the Unity FPS provider alone carries ~4,500 objects and measured a
|
|
643
|
+
// 150-300ms main-thread hitch each time a caller began an otherwise cheap
|
|
644
|
+
// wait. Failure diagnostics still take one complete terminal snapshot
|
|
645
|
+
// below, only on the failure path where its state is actually printed.
|
|
646
|
+
const startTime = await this.readTime();
|
|
630
647
|
let lastTick = null;
|
|
631
648
|
let stalledPolls = 0;
|
|
632
649
|
// Fixture heartbeat — same invariant as
|
|
@@ -634,10 +651,10 @@ export class GameClient {
|
|
|
634
651
|
// silence AND the tick having advanced since the last one emitted, so a
|
|
635
652
|
// genuinely stalled sim clock (caught by `stalledPolls` above, ~30s)
|
|
636
653
|
// goes heartbeat-silent well before this loop's own guard ever needs to.
|
|
637
|
-
let heartbeat = { lastEmitWallMs: startWall, lastEmitTick:
|
|
654
|
+
let heartbeat = { lastEmitWallMs: startWall, lastEmitTick: startTime.tick };
|
|
638
655
|
for (;;) {
|
|
639
656
|
const currentTime = await this.readTime();
|
|
640
|
-
if (currentTime.simSeconds -
|
|
657
|
+
if (currentTime.simSeconds - startTime.simSeconds >= budget.simSeconds)
|
|
641
658
|
return;
|
|
642
659
|
stalledPolls = lastTick !== null && currentTime.tick === lastTick ? stalledPolls + 1 : 0;
|
|
643
660
|
lastTick = currentTime.tick;
|
|
@@ -647,7 +664,10 @@ export class GameClient {
|
|
|
647
664
|
const current = await this.snapshot();
|
|
648
665
|
throw await this.toSessionFailure(new WaitForTimeoutError({
|
|
649
666
|
budget,
|
|
650
|
-
|
|
667
|
+
// The timeout renderer reads only the start clock; provider state
|
|
668
|
+
// is intentionally terminal-only because no initial provider read
|
|
669
|
+
// occurred. Empty collections state that absence honestly.
|
|
670
|
+
startSnapshot: { time: startTime, state: {}, events: [], pageErrors: [] },
|
|
651
671
|
lastSnapshot: current,
|
|
652
672
|
wallElapsedMs: Date.now() - startWall,
|
|
653
673
|
predicateSource: '(no predicate — game.input.hold is waiting for a sim-time delta to pass)',
|
|
@@ -740,7 +760,7 @@ export class GameClient {
|
|
|
740
760
|
*/
|
|
741
761
|
async page(step) {
|
|
742
762
|
const erased = (arg) => step(arg);
|
|
743
|
-
const outcome = await this.#transport.runPageScript(step
|
|
763
|
+
const outcome = await this.#transport.runPageScript(selfContainedStepSource(step), erased);
|
|
744
764
|
return this.unwrap(outcome);
|
|
745
765
|
}
|
|
746
766
|
/**
|
|
@@ -763,7 +783,7 @@ export class GameClient {
|
|
|
763
783
|
*/
|
|
764
784
|
async run(step, opts) {
|
|
765
785
|
const erased = (arg) => step(arg);
|
|
766
|
-
const outcome = await this.#transport.runGameScript(step
|
|
786
|
+
const outcome = await this.#transport.runGameScript(selfContainedStepSource(step), erased, opts?.instance);
|
|
767
787
|
return this.unwrap(outcome);
|
|
768
788
|
}
|
|
769
789
|
/**
|
|
@@ -64,6 +64,11 @@ export interface FastForwardOptions {
|
|
|
64
64
|
/** Overrides `DEFAULT_FAST_FORWARD_BATCH_TICKS` — test-only seam; real
|
|
65
65
|
* callers should leave this unset. */
|
|
66
66
|
batchTicks?: number;
|
|
67
|
+
/** What the caller needs back after the burst. Defaults to the complete
|
|
68
|
+
* debug snapshot. Tick-by-tick orchestration that samples providers only at
|
|
69
|
+
* explicit boundaries selects `'time'` so it does not serialize every large
|
|
70
|
+
* state provider after every staging tick. */
|
|
71
|
+
result?: 'snapshot' | 'time';
|
|
67
72
|
}
|
|
68
73
|
/** One fixed timestep, matching every real host's loop construction
|
|
69
74
|
* (`createGameLoop`'s own `fixedTimestep ?? 1/60` default: "fixedDt = the
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@vgai/live",
|
|
3
3
|
"author": "Volter AI, Inc.",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
|
-
"version": "0.5.
|
|
5
|
+
"version": "0.5.42",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"prepack": "npm run build"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@vgai/editor-sdk": "0.5.
|
|
36
|
-
"@vgai/sdk": "0.5.
|
|
35
|
+
"@vgai/editor-sdk": "0.5.42",
|
|
36
|
+
"@vgai/sdk": "0.5.42"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@playwright/test": ">=1.58.2 <2"
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@playwright/test": "^1.58.2",
|
|
43
43
|
"@types/node": "^25.3.0",
|
|
44
|
+
"esbuild": "^0.25.12",
|
|
44
45
|
"typescript": "^5.6.0"
|
|
45
46
|
},
|
|
46
47
|
"description": "Live-session client for VGAI projects: { editor, game, page, tools } over the vgai edit session wire.",
|
package/src/editor.ts
CHANGED
|
@@ -353,6 +353,11 @@ export class LiveEditor {
|
|
|
353
353
|
return this.#client.hierarchy();
|
|
354
354
|
}
|
|
355
355
|
|
|
356
|
+
/** Expand every branch through the Hierarchy panel's own action. */
|
|
357
|
+
async expandHierarchyAll(): Promise<void> {
|
|
358
|
+
await this.#client.expandHierarchyAll();
|
|
359
|
+
}
|
|
360
|
+
|
|
356
361
|
/**
|
|
357
362
|
* Write one editable field from `inspect()` by its stable path, through the
|
|
358
363
|
* same Inspector IO and persistence boundary the human control uses.
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
type FastForwardBudget,
|
|
21
21
|
type FastForwardClock,
|
|
22
22
|
type FastForwardOptions,
|
|
23
|
+
type FastForwardTime,
|
|
23
24
|
runFastForward,
|
|
24
25
|
} from './fast-forward.js';
|
|
25
26
|
import { HiddenRecoveryDriver } from './hidden-recovery.js';
|
|
@@ -104,6 +105,24 @@ async function bridgeCallInPageAsync(args: {
|
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
/**
|
|
109
|
+
* A relay step crosses the wire as function source, so compiler-owned helpers that live beside the
|
|
110
|
+
* function in its Node module are closures too. esbuild's `keepNames` transform is the live case:
|
|
111
|
+
* a named helper inside an otherwise literal callback becomes `__name(fn, "helper")`, while the
|
|
112
|
+
* module-level `__name` implementation is absent after `step.toString()`. Seat that exact compiler
|
|
113
|
+
* primitive inside the serialized function instead of installing a page global or asking every
|
|
114
|
+
* caller to avoid ordinary named local helpers.
|
|
115
|
+
*
|
|
116
|
+
* Ordinary source stays byte-for-byte unchanged. That preserves the public wire account and keeps
|
|
117
|
+
* unsupported user closures loud; this only completes the source for a compiler helper whose
|
|
118
|
+
* semantics are intrinsic and deterministic.
|
|
119
|
+
*/
|
|
120
|
+
function selfContainedStepSource(step: (scope: unknown) => unknown): string {
|
|
121
|
+
const source = step.toString();
|
|
122
|
+
if (!/\b__name\s*\(/u.test(source)) return source;
|
|
123
|
+
return `(scope) => { const __name = (target, value) => Object.defineProperty(target, "name", { value, configurable: true }); return (${source})(scope); }`;
|
|
124
|
+
}
|
|
125
|
+
|
|
107
126
|
/**
|
|
108
127
|
* #140 — the `BridgeTransport` `page.evaluate` implementation. This is the
|
|
109
128
|
* ONE place a `Page` is ever touched to drive `window.__vgai` (module doc
|
|
@@ -666,7 +685,15 @@ export class GameClient {
|
|
|
666
685
|
* AFTER the tps baseline reset below, so it never itself corrupts the tps
|
|
667
686
|
* stats either).
|
|
668
687
|
*/
|
|
669
|
-
async fastForward(
|
|
688
|
+
async fastForward(
|
|
689
|
+
budget: FastForwardBudget,
|
|
690
|
+
opts: FastForwardOptions & { result: 'time' },
|
|
691
|
+
): Promise<FastForwardTime>;
|
|
692
|
+
async fastForward(budget: FastForwardBudget, opts?: FastForwardOptions): Promise<DebugSnapshot>;
|
|
693
|
+
async fastForward(
|
|
694
|
+
budget: FastForwardBudget,
|
|
695
|
+
opts?: FastForwardOptions,
|
|
696
|
+
): Promise<DebugSnapshot | FastForwardTime> {
|
|
670
697
|
// Same options-object-only contract as `waitSimTime` (`ticksForBudget`
|
|
671
698
|
// would otherwise fail on `'simTicks' in 0.5` with a raw TypeError that
|
|
672
699
|
// names neither the method nor the shape).
|
|
@@ -696,21 +723,26 @@ export class GameClient {
|
|
|
696
723
|
}
|
|
697
724
|
},
|
|
698
725
|
readTime: async () => {
|
|
699
|
-
// Raw bridge read — deliberately NOT `this.snapshot()`,
|
|
700
|
-
//
|
|
701
|
-
|
|
702
|
-
|
|
726
|
+
// Raw, clock-only bridge read — deliberately NOT `this.snapshot()`,
|
|
727
|
+
// which both feeds the TpsAccumulator and serializes every declared
|
|
728
|
+
// provider. Large imported worlds can carry megabytes of census state;
|
|
729
|
+
// the batching math needs only these two numbers.
|
|
730
|
+
const time = await this.callBridge<DebugSnapshot['time']>('state', 'time');
|
|
731
|
+
return { tick: time.tick, simSeconds: time.simSeconds };
|
|
703
732
|
},
|
|
704
733
|
heartbeat: (info) => {
|
|
705
734
|
console.log(`vgai fastForward: ${info.ticksDone}/${info.ticksTotal} ticks driven`);
|
|
706
735
|
},
|
|
707
736
|
};
|
|
708
|
-
await runFastForward(budget, opts ?? {}, clock);
|
|
737
|
+
const finalTime = await runFastForward(budget, opts ?? {}, clock);
|
|
709
738
|
// The burst is over — reset the baseline so the very next ordinary poll
|
|
710
739
|
// (including the `snapshot()` call right below) treats itself as a fresh
|
|
711
740
|
// "first observation" rather than diffing across the burst's enormous
|
|
712
741
|
// tick delta over a near-zero wall delta.
|
|
713
742
|
this.#tps.resetBaseline();
|
|
743
|
+
if (opts?.result === 'time') {
|
|
744
|
+
return finalTime;
|
|
745
|
+
}
|
|
714
746
|
return this.snapshot();
|
|
715
747
|
}
|
|
716
748
|
|
|
@@ -760,7 +792,13 @@ export class GameClient {
|
|
|
760
792
|
// fires either. Refuse in the caller's own vocabulary instead of hanging.
|
|
761
793
|
assertValidWaitForBudget(budget, 'waitSimTime');
|
|
762
794
|
const startWall = Date.now();
|
|
763
|
-
|
|
795
|
+
// Predicate-free means clock-only from the FIRST read, not merely from the
|
|
796
|
+
// second poll onward. A full initial snapshot serializes every provider;
|
|
797
|
+
// the Unity FPS provider alone carries ~4,500 objects and measured a
|
|
798
|
+
// 150-300ms main-thread hitch each time a caller began an otherwise cheap
|
|
799
|
+
// wait. Failure diagnostics still take one complete terminal snapshot
|
|
800
|
+
// below, only on the failure path where its state is actually printed.
|
|
801
|
+
const startTime = await this.readTime();
|
|
764
802
|
let lastTick: number | null = null;
|
|
765
803
|
let stalledPolls = 0;
|
|
766
804
|
// Fixture heartbeat — same invariant as
|
|
@@ -768,10 +806,10 @@ export class GameClient {
|
|
|
768
806
|
// silence AND the tick having advanced since the last one emitted, so a
|
|
769
807
|
// genuinely stalled sim clock (caught by `stalledPolls` above, ~30s)
|
|
770
808
|
// goes heartbeat-silent well before this loop's own guard ever needs to.
|
|
771
|
-
let heartbeat: HeartbeatState = { lastEmitWallMs: startWall, lastEmitTick:
|
|
809
|
+
let heartbeat: HeartbeatState = { lastEmitWallMs: startWall, lastEmitTick: startTime.tick };
|
|
772
810
|
for (;;) {
|
|
773
811
|
const currentTime = await this.readTime();
|
|
774
|
-
if (currentTime.simSeconds -
|
|
812
|
+
if (currentTime.simSeconds - startTime.simSeconds >= budget.simSeconds) return;
|
|
775
813
|
stalledPolls = lastTick !== null && currentTime.tick === lastTick ? stalledPolls + 1 : 0;
|
|
776
814
|
lastTick = currentTime.tick;
|
|
777
815
|
if (stalledPolls >= WAIT_FOR_STALL_POLL_LIMIT) {
|
|
@@ -781,7 +819,10 @@ export class GameClient {
|
|
|
781
819
|
throw await this.toSessionFailure(
|
|
782
820
|
new WaitForTimeoutError({
|
|
783
821
|
budget,
|
|
784
|
-
|
|
822
|
+
// The timeout renderer reads only the start clock; provider state
|
|
823
|
+
// is intentionally terminal-only because no initial provider read
|
|
824
|
+
// occurred. Empty collections state that absence honestly.
|
|
825
|
+
startSnapshot: { time: startTime, state: {}, events: [], pageErrors: [] },
|
|
785
826
|
lastSnapshot: current,
|
|
786
827
|
wallElapsedMs: Date.now() - startWall,
|
|
787
828
|
predicateSource:
|
|
@@ -876,7 +917,10 @@ export class GameClient {
|
|
|
876
917
|
*/
|
|
877
918
|
async page<T = unknown>(step: (page: Page) => T | Promise<T>): Promise<T> {
|
|
878
919
|
const erased = (arg: unknown) => step(arg as Page);
|
|
879
|
-
const outcome = await this.#transport.runPageScript(
|
|
920
|
+
const outcome = await this.#transport.runPageScript(
|
|
921
|
+
selfContainedStepSource(step as (scope: unknown) => unknown),
|
|
922
|
+
erased,
|
|
923
|
+
);
|
|
880
924
|
return this.unwrap<T>(outcome);
|
|
881
925
|
}
|
|
882
926
|
|
|
@@ -907,7 +951,11 @@ export class GameClient {
|
|
|
907
951
|
opts?: { instance?: string },
|
|
908
952
|
): Promise<T> {
|
|
909
953
|
const erased = (arg: unknown) => step(arg as Parameters<typeof step>[0]);
|
|
910
|
-
const outcome = await this.#transport.runGameScript(
|
|
954
|
+
const outcome = await this.#transport.runGameScript(
|
|
955
|
+
selfContainedStepSource(step as (scope: unknown) => unknown),
|
|
956
|
+
erased,
|
|
957
|
+
opts?.instance,
|
|
958
|
+
);
|
|
911
959
|
return this.unwrap<T>(outcome);
|
|
912
960
|
}
|
|
913
961
|
|
|
@@ -63,6 +63,11 @@ export interface FastForwardOptions {
|
|
|
63
63
|
/** Overrides `DEFAULT_FAST_FORWARD_BATCH_TICKS` — test-only seam; real
|
|
64
64
|
* callers should leave this unset. */
|
|
65
65
|
batchTicks?: number;
|
|
66
|
+
/** What the caller needs back after the burst. Defaults to the complete
|
|
67
|
+
* debug snapshot. Tick-by-tick orchestration that samples providers only at
|
|
68
|
+
* explicit boundaries selects `'time'` so it does not serialize every large
|
|
69
|
+
* state provider after every staging tick. */
|
|
70
|
+
result?: 'snapshot' | 'time';
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
/** One fixed timestep, matching every real host's loop construction
|