@vgai/engine 0.5.2 → 0.5.3
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/README.md +18 -11
- package/package.json +10 -6
- package/schemas/engine-api.json +1 -68
- package/schemas/engine-api.md +1 -32
- package/schemas/engine-capabilities.json +30 -42
- package/schemas/{vgai-game.schema.json → vgai-project.schema.json} +52 -34
- package/src/adapter/adapter-surface.ts +5 -5
- package/src/adapter/authoring.ts +168 -226
- package/src/adapter/colyseus-networking-adapter.ts +44 -5
- package/src/adapter/first-party-systems.ts +156 -42
- package/src/adapter/host-context.ts +177 -46
- package/src/adapter/index.ts +47 -51
- package/src/adapter/ingest/game-contract.ts +2 -2
- package/src/adapter/ingest/scene-capture.ts +18 -19
- package/src/adapter/ingest/structural-ids.ts +127 -0
- package/src/adapter/ingest/upstream-pin.ts +9 -12
- package/src/adapter/loop-gate-report.ts +11 -11
- package/src/adapter/rapier-physics-adapter.ts +27 -9
- package/src/adapter/root-adapter.ts +217 -0
- package/src/adapter/{vgai-scene-game-adapter.ts → setup-three-root-adapter.ts} +173 -351
- package/src/adapter/system-adapter.ts +80 -63
- package/src/ai/navigation.ts +1 -1
- package/src/animation/animation-clock.ts +1 -1
- package/src/animation/camera-ownership.ts +1 -2
- package/src/animation/cubic-spline-interpolant.ts +132 -0
- package/src/animation/theatre-clock-binding.ts +2 -2
- package/src/animation/theatre-object-binding.ts +4 -4
- package/src/animation/xstate-animation-binding.ts +75 -5
- package/src/{scene/schema → asset-formats}/camera.ts +2 -5
- package/src/{scene/schema → asset-formats}/collider.ts +2 -5
- package/src/asset-formats/index.ts +54 -0
- package/src/{scene/schema → asset-formats}/instances.ts +10 -6
- package/src/{scene/schema → asset-formats}/light.ts +3 -6
- package/src/{scene/schema → asset-formats}/material.ts +4 -7
- package/src/{scene/schema → asset-formats}/mesh.ts +4 -7
- package/src/asset-formats/parse.ts +39 -0
- package/src/{scene/schema → asset-formats}/particles.ts +3 -6
- package/src/{scene/schema/environment.ts → asset-formats/render-env.ts} +23 -86
- package/src/{scene/schema → asset-formats}/tuples.ts +1 -1
- package/src/{scene/asset-loaders.ts → asset-loaders.ts} +8 -9
- package/src/asset-parse-error.ts +33 -0
- package/src/{scene/asset-registry.ts → asset-registry.ts} +1 -1
- package/src/assets.ts +1 -1
- package/src/audio/wav-encode.ts +9 -9
- package/src/canvas-react/engine-bridge.ts +59 -0
- package/src/canvas-react/index.ts +50 -0
- package/src/canvas-react/pixi-primitive.tsx +202 -0
- package/src/canvas-react/pixi-react-adapter.tsx +290 -0
- package/src/canvas-react/pixi-react-root-factory.tsx +88 -0
- package/src/canvas-react/world-context.ts +328 -0
- package/src/core/frame-pacing.ts +100 -0
- package/src/core/game-loop.ts +50 -28
- package/src/core/seeded-random.ts +7 -7
- package/src/core/sim-clock.ts +388 -0
- package/src/core/system-runner.ts +17 -63
- package/src/core/types.ts +34 -15
- package/src/data/data-asset.ts +3 -3
- package/src/data/data-check-core.ts +6 -7
- package/src/data/data-ref.ts +11 -11
- package/src/data/vite-plugin-data.ts +10 -10
- package/src/{scene/defaults.ts → defaults.ts} +18 -40
- package/src/dev/render-debug-adapter.ts +1 -1
- package/src/dev/webgl-frame-capture.ts +1 -1
- package/src/ecs/scene-index.ts +439 -0
- package/src/ecs/scene-query.ts +43 -0
- package/src/{scene → ecs}/user-data.ts +17 -36
- package/src/index.ts +7 -9
- package/src/input/input-manager.ts +29 -32
- package/src/input/input-types.ts +2 -2
- package/src/input/schema.ts +5 -5
- package/src/loader.ts +57 -0
- package/src/manifest/editor-port.ts +69 -0
- package/src/manifest/filename.ts +49 -0
- package/src/manifest/index.ts +8 -2
- package/src/manifest/load-file.ts +11 -0
- package/src/manifest/load.ts +65 -77
- package/src/manifest/locate.ts +55 -0
- package/src/manifest/schema.ts +400 -233
- package/src/{scene → physics}/collider-dimensions.ts +3 -3
- package/src/physics/physics-registry.ts +1 -1
- package/src/{world2d/authoring-2d.ts → pixi/authoring.ts} +24 -11
- package/src/pixi/index.ts +43 -0
- package/src/{world2d/ingest-iframe-2d.ts → pixi/ingest-iframe.ts} +9 -9
- package/src/{world2d/ingest2d.ts → pixi/ingest.ts} +28 -28
- package/src/{world2d/physics2d-registry.ts → pixi/physics-registry.ts} +1 -1
- package/src/{world2d/scene-capture-2d.ts → pixi/scene-capture.ts} +3 -3
- package/src/{world2d/system-adapters-2d.ts → pixi/system-adapters.ts} +2 -2
- package/src/react/unmanaged-root-detector.ts +26 -1
- package/src/react/use-data.ts +5 -5
- package/src/react/use-selection.tsx +15 -42
- package/src/react/{game-state.tsx → world-state.tsx} +44 -46
- package/src/render/auto-batcher.ts +1 -2
- package/src/{scene → render}/instance-mesh.ts +1 -1
- package/src/{scene → render}/light-camera-factory.ts +14 -13
- package/src/render/lod.ts +17 -0
- package/src/{scene → render}/material-factory.ts +8 -7
- package/src/{scene → render}/particles-factory.ts +62 -12
- package/src/render/render-batch-system.ts +14 -41
- package/src/render/render-features.ts +1 -1
- package/src/render/render-settings.ts +1 -2
- package/src/render/spark-renderer-lifecycle.ts +1 -1
- package/src/runtime/create-runtime.ts +352 -463
- package/src/runtime/debug-bridge.ts +148 -93
- package/src/runtime/debug-registry.ts +79 -54
- package/src/runtime/dev-layers.ts +40 -0
- package/src/runtime/frame-selector-cache.ts +4 -4
- package/src/runtime/game.ts +618 -369
- package/src/runtime/gameplay-rng-trap.ts +6 -7
- package/src/runtime/input-router.ts +11 -11
- package/src/runtime/mount-game.ts +54 -55
- package/src/runtime/mount-manifest.ts +154 -150
- package/src/runtime/presentation.ts +141 -0
- package/src/runtime/render-audio-control.ts +64 -53
- package/src/runtime/render-control.ts +45 -51
- package/src/runtime/render-seed.ts +3 -4
- package/src/runtime/state-bridge.ts +17 -18
- package/src/runtime/types.ts +94 -61
- package/src/setup/setup-renderer.ts +11 -6
- package/src/world3d-react/engine-bridge.ts +46 -33
- package/src/world3d-react/index.ts +31 -26
- package/src/world3d-react/r3f-adapter.tsx +211 -96
- package/src/world3d-react/r3f-root-factory.tsx +91 -0
- package/src/world3d-react/renderer-config.ts +137 -0
- package/src/world3d-react/world-context.ts +141 -111
- package/schemas/entity2d.schema.json +0 -468
- package/schemas/prefab.schema.json +0 -9992
- package/schemas/scn2d.schema.json +0 -494
- package/schemas/vscn.schema.json +0 -10851
- package/src/adapter/game-adapter.ts +0 -164
- package/src/adapter/ingest/overlay-applier.ts +0 -207
- package/src/adapter/ingest/overlay-apply.ts +0 -168
- package/src/adapter/ingest/overlay-file.ts +0 -126
- package/src/adapter/ingest/overlay-report.ts +0 -176
- package/src/animation/gsap-registration.ts +0 -184
- package/src/audio/audio-introspection.ts +0 -290
- package/src/audio/index.ts +0 -39
- package/src/audio/tone-clock-binding.ts +0 -98
- package/src/audio/tone-context.ts +0 -175
- package/src/audio/tone-offline-render.ts +0 -167
- package/src/ecs/component-manager.ts +0 -814
- package/src/ecs/game-component.ts +0 -260
- package/src/ecs/hmr-swap-report.ts +0 -65
- package/src/physics/trigger-dispatch.ts +0 -97
- package/src/react/root-adapter.tsx +0 -49
- package/src/scene/asset-paths.ts +0 -121
- package/src/scene/asset-ref-check.ts +0 -248
- package/src/scene/component-registry.ts +0 -51
- package/src/scene/parse.ts +0 -204
- package/src/scene/scene-apply.ts +0 -407
- package/src/scene/scene-diff-schema.ts +0 -115
- package/src/scene/scene-diff-types.ts +0 -29
- package/src/scene/scene-loader.ts +0 -1526
- package/src/scene/scene-query.ts +0 -63
- package/src/scene/scene-types.ts +0 -33
- package/src/scene/scene-version.ts +0 -40
- package/src/scene/schema/animation.ts +0 -46
- package/src/scene/schema/audio.ts +0 -25
- package/src/scene/schema/entity-ref.ts +0 -78
- package/src/scene/schema/entity.ts +0 -189
- package/src/scene/schema/index.ts +0 -51
- package/src/scene/schema/joint.ts +0 -26
- package/src/scene/schema/physics.ts +0 -49
- package/src/scene/schema/scene-file.ts +0 -292
- package/src/scene/schema/shadow.ts +0 -24
- package/src/scene/schema/spline.ts +0 -21
- package/src/world2d/asset-paths2d.ts +0 -44
- package/src/world2d/capture-to-scene2d.ts +0 -52
- package/src/world2d/collision-2d.ts +0 -99
- package/src/world2d/entity2d-asset.ts +0 -22
- package/src/world2d/index.ts +0 -91
- package/src/world2d/physics2d-transform.ts +0 -173
- package/src/world2d/physics2d-units.ts +0 -10
- package/src/world2d/pixi-game-adapter.ts +0 -439
- package/src/world2d/pixi-surface.ts +0 -78
- package/src/world2d/scene2d-identity.ts +0 -49
- package/src/world2d/scene2d-loader.ts +0 -433
- package/src/world2d/schema/entity2d.ts +0 -163
- package/src/world2d/schema/physics2d.ts +0 -64
- package/src/world2d/schema/sprite.ts +0 -99
- package/src/world2d/schema/tilemap.ts +0 -39
- package/src/world2d/schema/tuples2d.ts +0 -25
- package/src/world2d/transform-writer-2d.ts +0 -42
- package/src/world2d/types.ts +0 -74
- package/src/world3d-react/behavior.tsx +0 -146
- /package/src/{scene → render}/mesh-shadow.ts +0 -0
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The standalone in-page debug bridge
|
|
3
|
-
*
|
|
4
|
-
* `
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* {@link maybeInstallDebugBridge} unconditionally on every boot still only
|
|
9
|
-
* ever gets a handle when the gate below actually passes).
|
|
2
|
+
* The standalone in-page debug bridge. Query-gated `window.__vgai`, the same
|
|
3
|
+
* production-protection-lives-in-the-installer pattern `render-control.ts`'s
|
|
4
|
+
* `installRenderControlHarness` established for `?vgai-render=1` (production
|
|
5
|
+
* protection lives HERE, not just at whatever call site invokes this — a
|
|
6
|
+
* caller that calls {@link maybeInstallDebugBridge} unconditionally on every
|
|
7
|
+
* boot still only ever gets a handle when the gate below actually passes).
|
|
10
8
|
*
|
|
11
9
|
* D17: this is door (a) of the seam's three doors — the SAME `DebugAdapter`
|
|
12
|
-
* names/JSON the editor relay (door b) and `@vgai/
|
|
10
|
+
* names/JSON the editor relay (door b) and `@vgai/live` (door c, which
|
|
13
11
|
* drives door (a) itself over Playwright) all read. D18: the bridge installs
|
|
14
12
|
* only when the page opts in (`?vgai-debug=1`) AND is either a dev build
|
|
15
13
|
* (`import.meta.env.DEV`) or the project's manifest explicitly opts a
|
|
@@ -40,7 +38,7 @@ export interface DebugBridgeManifest {
|
|
|
40
38
|
* import. Every method takes an optional trailing `worldId` (D15/T-D15.5,
|
|
41
39
|
* the review-objection-2 fix): omitted, it resolves to the SAME default
|
|
42
40
|
* world the editor relay's `inject-input` case resolves to (one shared
|
|
43
|
-
* resolution function, `debug-registry.ts`'s `
|
|
41
|
+
* resolution function, `debug-registry.ts`'s `resolveInputRootId`) — never
|
|
44
42
|
* "whichever world's `InputManager` happened to register last". An
|
|
45
43
|
* explicit `worldId` reaches that world's `InputManager` specifically. */
|
|
46
44
|
export interface VgaiDebugInputHandle {
|
|
@@ -52,15 +50,14 @@ export interface VgaiDebugInputHandle {
|
|
|
52
50
|
tapVirtualAction(action: string, worldId?: string): { delivered: boolean; reason?: string };
|
|
53
51
|
clearVirtualActions(worldId?: string): void;
|
|
54
52
|
/**
|
|
55
|
-
* D15/T-D15.5
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* already elapsed.
|
|
53
|
+
* D15/T-D15.5: schedule a virtual actuation for a specific tick — applied
|
|
54
|
+
* at the START of that tick's input phase, composing with `runTicks` (a
|
|
55
|
+
* schedule for tick 500 fires exactly once the sim has been driven
|
|
56
|
+
* through tick 500, regardless of burst size). A digital `true` produces
|
|
57
|
+
* a genuine `isJustPressed` edge exactly at the target tick. Throws
|
|
58
|
+
* `INPUT_ACTION_NOT_FOUND`/a valueType mismatch (same as
|
|
59
|
+
* `setVirtualAction`) or `TICK_ALREADY_PASSED` (`data.currentTick`, the
|
|
60
|
+
* NEXT tick to be serviced) for a tick that already elapsed.
|
|
64
61
|
*
|
|
65
62
|
* A scheduled tick whose input phase never runs at all (that world
|
|
66
63
|
* paused/frozen, or skipped by a multi-tick gap, when the target tick
|
|
@@ -130,20 +127,20 @@ export interface VgaiDebugHandle {
|
|
|
130
127
|
invoke(name: string, args: unknown[]): Promise<unknown>;
|
|
131
128
|
events: DebugAdapter['events'];
|
|
132
129
|
input: VgaiDebugInputHandle;
|
|
133
|
-
/** See `DebugAdapter.events`'s doc comment for `
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
snapshot(
|
|
130
|
+
/** See `DebugAdapter.events`'s doc comment for `sinceSeq`'s contract
|
|
131
|
+
* (run-4 friction #5) — `snapshot`'s `events` member is filtered the exact
|
|
132
|
+
* same way, just batched with `time`/`state`/`pageErrors` into one
|
|
133
|
+
* synchronous read. */
|
|
134
|
+
snapshot(sinceSeq?: number): VgaiDebugSnapshot;
|
|
138
135
|
/**
|
|
139
|
-
* D15/T-D15.4 door (a)
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
136
|
+
* D15/T-D15.4 door (a): synchronously drive `n` fixed gameplay ticks via the
|
|
137
|
+
* live `Game`'s `GameInternal.runTicks` (`runtime/game.ts`) — reached through
|
|
138
|
+
* `DebugRegistry.getRunTicksTarget()`, the SAME target the editor relay's
|
|
139
|
+
* `run-ticks` case (→ `play.runTicks`, door b) calls, so behavior is
|
|
140
|
+
* byte-identical across every door (D17). Throws `DEBUG_RUN_TICKS_UNAVAILABLE`
|
|
141
|
+
* if no `Game` has wired a target yet (no world mounted); throws whatever
|
|
142
|
+
* `runTicks` itself throws otherwise (e.g. `RUN_TICKS_PAUSED`) — never a
|
|
143
|
+
* silent no-op.
|
|
147
144
|
*/
|
|
148
145
|
runTicks(n: number, opts?: RunTicksOptions): void;
|
|
149
146
|
/**
|
|
@@ -152,7 +149,7 @@ export interface VgaiDebugHandle {
|
|
|
152
149
|
* this is the honest human-input-path proof) → `input.clearVirtualActions()`
|
|
153
150
|
* into ONE async bridge call, as a TOP-LEVEL method (not under `input`)
|
|
154
151
|
* because it needs the sim clock, not just the input target. Was 15+
|
|
155
|
-
* transport round trips over the editor relay (`@vgai/
|
|
152
|
+
* transport round trips over the editor relay (`@vgai/live`'s old
|
|
156
153
|
* `GameInput.hold`: set → a 150ms-interval `waitSimTime` snapshot poll loop
|
|
157
154
|
* → clear); now one `page.evaluate`/relay call that runs the wait
|
|
158
155
|
* in-process on the page.
|
|
@@ -179,7 +176,7 @@ export interface VgaiDebugHandle {
|
|
|
179
176
|
* Defect 5 fix — undo everything THIS install did: remove the
|
|
180
177
|
* `error`/`unhandledrejection` window listeners it added, and delete
|
|
181
178
|
* `window.__vgai` so the registry is no longer reachable from the page.
|
|
182
|
-
* Idempotent (a second call is a harmless no-op). `
|
|
179
|
+
* Idempotent (a second call is a harmless no-op). `mountManifestRoots`
|
|
183
180
|
* wires this into its session's `stop()`; a caller mounting more
|
|
184
181
|
* directly (a hand-rolled host) should call it on its own teardown path.
|
|
185
182
|
*/
|
|
@@ -224,72 +221,128 @@ export interface MaybeInstallDebugBridgeOptions {
|
|
|
224
221
|
const PAGE_ERROR_CAP = 100;
|
|
225
222
|
|
|
226
223
|
/** `holdFor`'s in-process poll interval — this loop never leaves the page
|
|
227
|
-
* (no transport round trip per poll, unlike `@vgai/
|
|
224
|
+
* (no transport round trip per poll, unlike `@vgai/live`'s old
|
|
228
225
|
* `waitSimTime`), so it can afford to be tighter than that loop's 150ms. */
|
|
229
226
|
const HOLD_FOR_POLL_MS = 50;
|
|
230
227
|
|
|
231
228
|
/** Consecutive `HOLD_FOR_POLL_MS` polls with the tick unchanged before
|
|
232
229
|
* `holdFor` gives up waiting and treats the game as stopped — mirrors
|
|
233
|
-
* `@vgai/
|
|
230
|
+
* `@vgai/live`'s `WAIT_FOR_STALL_POLL_LIMIT` reasoning (`wait-for.ts`:
|
|
234
231
|
* a genuinely frozen sim clock must never poll forever), scaled to this
|
|
235
232
|
* faster in-process interval so the wall-clock grace period (~5s) lands in
|
|
236
233
|
* the same neighborhood. */
|
|
237
234
|
const HOLD_FOR_STALL_POLL_LIMIT = 100;
|
|
238
235
|
|
|
239
|
-
/**
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
* `
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
236
|
+
/** Ticks driven per synchronous hidden-tab batch. Sized like
|
|
237
|
+
* `fast-forward.ts`'s own batching rationale: big enough that per-batch
|
|
238
|
+
* bookkeeping is negligible, small enough that one batch of a complex game's
|
|
239
|
+
* phases stays a short synchronous burst. */
|
|
240
|
+
const HIDDEN_DRIVE_BATCH_TICKS = 60;
|
|
241
|
+
|
|
242
|
+
/** Hard ceiling on hidden-drive batches for ONE hold — `HIDDEN_DRIVE_BATCH_TICKS
|
|
243
|
+
* * this` ≈ 33 sim-minutes. A bound, not a timeout: it exists so a
|
|
244
|
+
* `runTicks` target that advances the tick counter without advancing the sim
|
|
245
|
+
* clock can never spin forever, and it is far past any honest hold. */
|
|
246
|
+
const HIDDEN_DRIVE_MAX_BATCHES = 2000;
|
|
247
|
+
|
|
248
|
+
/** The message a hold gets when the loop is hidden-paused and NOTHING can
|
|
249
|
+
* drive it — the one case where the platform genuinely prevents the verb
|
|
250
|
+
* from working. Names the cause and the fix rather than expiring into a
|
|
251
|
+
* generic stall (which reported the wrong cause: "play stopped during
|
|
252
|
+
* hold"). One exported constant so both doors and their tests read the same
|
|
253
|
+
* string. */
|
|
254
|
+
export const HOLD_HIDDEN_NO_DRIVER_REASON =
|
|
255
|
+
'the editor tab is hidden and this session has no way to drive ticks — the engine ' +
|
|
256
|
+
'hidden-pauses its loop while document.hidden, so zero ticks are happening. Bring the ' +
|
|
257
|
+
'editor tab to the foreground (or start play, which wires the run-ticks target) and retry.';
|
|
258
|
+
|
|
259
|
+
/** What `adapter.state('time')` returns for the two fields this loop reads,
|
|
260
|
+
* plus the liveness the hidden path branches on. */
|
|
261
|
+
interface HoldClockReading {
|
|
262
|
+
simSeconds: number;
|
|
263
|
+
tick: number;
|
|
264
|
+
loopLiveness?: GameLoopLiveness | null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Resolves once `simSeconds` of sim time has elapsed since the call, or once
|
|
269
|
+
* the wait provably cannot make progress. Never rejects. Exported (not just
|
|
270
|
+
* used by `holdFor` below) so the editor relay's `command-listener.ts`
|
|
271
|
+
* `holdFor` case can share the EXACT same logic against its own
|
|
272
|
+
* `DebugAdapter` (reached via `getActiveSystems().debug` rather than a
|
|
273
|
+
* `DebugRegistry`) — D17: byte-identical behavior across doors, not two
|
|
274
|
+
* hand-copies that can silently drift apart.
|
|
275
|
+
*
|
|
276
|
+
* Two regimes, and the split is the whole point:
|
|
277
|
+
*
|
|
278
|
+
* - **Visible**: the host loop is ticking on its own, so this polls it every
|
|
279
|
+
* `HOLD_FOR_POLL_MS` and gives up after `HOLD_FOR_STALL_POLL_LIMIT`
|
|
280
|
+
* consecutive unchanged-tick polls (`stalled: true` — play stopped/paused).
|
|
281
|
+
*
|
|
282
|
+
* - **Hidden-paused**: the host loop has stopped outright (T2.1's idle
|
|
283
|
+
* throttle), so sim time only moves if this drives it. It therefore drives
|
|
284
|
+
* the WHOLE remaining budget in synchronous batches, yielding a MICROTASK
|
|
285
|
+
* between them — never a timer. That distinction is load-bearing, not
|
|
286
|
+
* stylistic: a hidden tab clamps `setTimeout` to ~1s (and to ~1/minute under
|
|
287
|
+
* Chrome's intensive throttling after 5 minutes hidden), so the previous
|
|
288
|
+
* "drive 3 ticks, then `setTimeout(poll, 50)`" shape ran the sim at ~3
|
|
289
|
+
* ticks per SECOND. A 1.2s hold needed ~24 clamped turns — ~24s of wall
|
|
290
|
+
* clock against the editor relay's 5s per-command budget, which is exactly
|
|
291
|
+
* how a hold that passes on a foregrounded tab died with a generic
|
|
292
|
+
* "editor connected but did not respond" on a backgrounded one. Microtasks
|
|
293
|
+
* are not throttled, so the hidden path now costs sim-work time and nothing
|
|
294
|
+
* else.
|
|
295
|
+
*
|
|
296
|
+
* The regime is re-read every iteration, so a tab foregrounded (or hidden)
|
|
297
|
+
* mid-hold crosses over without restarting the budget.
|
|
298
|
+
*
|
|
299
|
+
* `hiddenWithoutDriver: true` is the one honest refusal: hidden-paused with
|
|
300
|
+
* no `driveHiddenTicks` hook means nothing in this process can advance the
|
|
301
|
+
* clock, so it reports that IMMEDIATELY (see
|
|
302
|
+
* {@link HOLD_HIDDEN_NO_DRIVER_REASON}) instead of burning the stall guard
|
|
303
|
+
* and then blaming a stopped game.
|
|
304
|
+
*/
|
|
305
|
+
export async function waitForHoldBudget(
|
|
252
306
|
adapter: DebugAdapter,
|
|
253
307
|
simSeconds: number,
|
|
254
308
|
driveHiddenTicks?: (n: number) => void,
|
|
255
|
-
): Promise<{ stalled: boolean }> {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const ticks = Math.max(1, Math.round(HOLD_FOR_POLL_MS / (1000 / 60)));
|
|
279
|
-
driveHiddenTicks(ticks);
|
|
280
|
-
setTimeout(poll, HOLD_FOR_POLL_MS);
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
stalledPolls = current.tick === lastTick ? stalledPolls + 1 : 0;
|
|
284
|
-
lastTick = current.tick;
|
|
285
|
-
if (stalledPolls >= HOLD_FOR_STALL_POLL_LIMIT) {
|
|
286
|
-
resolve({ stalled: true });
|
|
287
|
-
return;
|
|
309
|
+
): Promise<{ stalled: boolean; hiddenWithoutDriver?: boolean }> {
|
|
310
|
+
const readTime = () => adapter.state('time') as HoldClockReading;
|
|
311
|
+
const start = readTime();
|
|
312
|
+
let lastTick = start.tick;
|
|
313
|
+
let stalledPolls = 0;
|
|
314
|
+
let hiddenBatches = 0;
|
|
315
|
+
|
|
316
|
+
for (;;) {
|
|
317
|
+
const current = readTime();
|
|
318
|
+
if (current.simSeconds - start.simSeconds >= simSeconds) return { stalled: false };
|
|
319
|
+
|
|
320
|
+
if (current.loopLiveness === 'hidden-paused') {
|
|
321
|
+
if (!driveHiddenTicks) return { stalled: true, hiddenWithoutDriver: true };
|
|
322
|
+
if (hiddenBatches >= HIDDEN_DRIVE_MAX_BATCHES) return { stalled: true };
|
|
323
|
+
hiddenBatches += 1;
|
|
324
|
+
driveHiddenTicks(HIDDEN_DRIVE_BATCH_TICKS);
|
|
325
|
+
const after = readTime();
|
|
326
|
+
// A batch that moved neither the tick counter nor the sim clock means
|
|
327
|
+
// the drive is a no-op (paused game, torn-down runtime) — the same
|
|
328
|
+
// "nothing is advancing" verdict the visible path's stall guard
|
|
329
|
+
// reaches, just observable in one batch instead of a hundred polls.
|
|
330
|
+
if (after.tick === current.tick && after.simSeconds === current.simSeconds) {
|
|
331
|
+
return { stalled: true };
|
|
288
332
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
333
|
+
lastTick = after.tick;
|
|
334
|
+
stalledPolls = 0;
|
|
335
|
+
// Yield so the page can service other work between batches, WITHOUT
|
|
336
|
+
// handing control to a clock a hidden tab is allowed to starve.
|
|
337
|
+
await Promise.resolve();
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
stalledPolls = current.tick === lastTick ? stalledPolls + 1 : 0;
|
|
342
|
+
lastTick = current.tick;
|
|
343
|
+
if (stalledPolls >= HOLD_FOR_STALL_POLL_LIMIT) return { stalled: true };
|
|
344
|
+
await new Promise<void>((resolve) => setTimeout(resolve, HOLD_FOR_POLL_MS));
|
|
345
|
+
}
|
|
293
346
|
}
|
|
294
347
|
|
|
295
348
|
function hasRealWindow(): boolean {
|
|
@@ -337,7 +390,7 @@ function buildDebugHandle(opts: {
|
|
|
337
390
|
if (!target) {
|
|
338
391
|
throw new DebugError(
|
|
339
392
|
'DEBUG_INPUT_UNAVAILABLE',
|
|
340
|
-
`debug bridge: ${method}() has no virtual-input target wired — no default
|
|
393
|
+
`debug bridge: ${method}() has no virtual-input target wired — no default three ` +
|
|
341
394
|
"world has mounted yet, or this project's mount path never wired one",
|
|
342
395
|
);
|
|
343
396
|
}
|
|
@@ -391,7 +444,7 @@ function buildDebugHandle(opts: {
|
|
|
391
444
|
injectPointerPosition: (sourceId, value, worldId) =>
|
|
392
445
|
requireInputTarget('injectPointerPosition', worldId).injectPointerPosition(sourceId, value),
|
|
393
446
|
},
|
|
394
|
-
snapshot(
|
|
447
|
+
snapshot(sinceSeq) {
|
|
395
448
|
// One synchronous pass — see VgaiDebugSnapshot's doc comment.
|
|
396
449
|
return {
|
|
397
450
|
time: adapter.state('time') as {
|
|
@@ -400,7 +453,7 @@ function buildDebugHandle(opts: {
|
|
|
400
453
|
loopLiveness: GameLoopLiveness | null;
|
|
401
454
|
},
|
|
402
455
|
state: adapter.stateAll(),
|
|
403
|
-
events: adapter.events(
|
|
456
|
+
events: adapter.events(sinceSeq),
|
|
404
457
|
pageErrors: pageErrors.slice(),
|
|
405
458
|
};
|
|
406
459
|
},
|
|
@@ -428,15 +481,17 @@ function buildDebugHandle(opts: {
|
|
|
428
481
|
: { delivered: false };
|
|
429
482
|
}
|
|
430
483
|
const runTicksTarget = registry.getRunTicksTarget();
|
|
431
|
-
const { stalled } = await waitForHoldBudget(
|
|
484
|
+
const { stalled, hiddenWithoutDriver } = await waitForHoldBudget(
|
|
432
485
|
adapter,
|
|
433
486
|
simSeconds,
|
|
434
487
|
runTicksTarget ? (n) => runTicksTarget.runTicks(n, { render: 'last' }) : undefined,
|
|
435
488
|
);
|
|
436
489
|
target.clearVirtualActions();
|
|
437
|
-
return
|
|
438
|
-
|
|
439
|
-
|
|
490
|
+
if (!stalled) return { delivered: true };
|
|
491
|
+
return {
|
|
492
|
+
delivered: false,
|
|
493
|
+
reason: hiddenWithoutDriver ? HOLD_HIDDEN_NO_DRIVER_REASON : 'play stopped during hold',
|
|
494
|
+
};
|
|
440
495
|
},
|
|
441
496
|
uninstall() {
|
|
442
497
|
opts.window.removeEventListener?.('error', onWindowError);
|