@volter/editor-game 0.5.66 → 0.5.67
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/contributions/audio-unlock.service.ts +2 -2
- package/contributions/autoplay.service.ts +1 -1
- package/contributions/bridge.command.ts +3 -3
- package/contributions/canvas/component-board.service.ts +16 -0
- package/contributions/canvas/design-time-mount.service.ts +45 -0
- package/contributions/canvas-story-capture.service.ts +12 -0
- package/contributions/edit-mode-audio.service.ts +2 -2
- package/contributions/edit-mode-networking.service.ts +1 -1
- package/contributions/gameplay.command.ts +4 -4
- package/contributions/generation.service.ts +1 -1
- package/contributions/godot.style.ts +26 -6
- package/contributions/godot.view.ts +6 -0
- package/contributions/ingest.service.ts +2 -2
- package/contributions/instances.command.ts +1 -1
- package/contributions/navmesh.menu.ts +1 -1
- package/contributions/network-observer.service.ts +14 -0
- package/contributions/play.command.ts +10 -4
- package/contributions/react/component-board.service.ts +1 -1
- package/contributions/react/design-time-mount.service.ts +1 -1
- package/contributions/scene-document.service.ts +3 -3
- package/contributions/state-watch.menu.ts +1 -1
- package/contributions/state-watch.utility.tsx +1 -1
- package/contributions/team-playtest.service.ts +4 -4
- package/contributions/three/component-board.service.ts +1 -1
- package/contributions/three/component-verbs.command.ts +6 -6
- package/contributions/three/three-authoring.service.ts +8 -5
- package/contributions/three-story-capture.service.ts +12 -0
- package/contributions/unity.style.ts +17 -7
- package/contributions/unity.view.ts +6 -0
- package/contributions/unreal.style.ts +10 -2
- package/contributions/unreal.view.ts +5 -0
- package/package.json +20 -10
- package/src/asset-budget/AssetBudgetPanel.tsx +1 -1
- package/src/asset-budget/asset-budget-model.ts +1 -1
- package/src/audio/AudioDebuggerPanel.tsx +1 -1
- package/src/bridge/dispatch.ts +14 -14
- package/src/bridge/live-frames.ts +1 -1
- package/src/bridge/screenshot.ts +3 -3
- package/src/build/BuildProfilesPanel.tsx +3 -3
- package/src/canvas/canvas-board/CanvasBoardDocument.tsx +676 -0
- package/src/canvas/canvas-board/canvas-board-model.ts +407 -0
- package/src/canvas/canvas-board/canvas-component-board.ts +56 -0
- package/src/canvas/canvas-design-mount.ts +524 -0
- package/src/canvas/design-time-canvas-mount.ts +79 -0
- package/src/coverage/live-authoring-surface.ts +5 -5
- package/src/coverage/live-project-verbs.ts +1 -1
- package/src/coverage/native-system-coverage.ts +5 -5
- package/src/coverage/root-coverage.ts +1 -1
- package/src/coverage/session-coverage.ts +3 -3
- package/src/design-system-stories/ApplicationChrome.stories.tsx +5 -5
- package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +7 -7
- package/src/edit-mode/edit-mode-audio.ts +4 -4
- package/src/edit-mode/edit-mode-networking.ts +2 -2
- package/src/game-document/GameCaptureFrameButton.tsx +1 -1
- package/src/game-document/GameDocument.tsx +2 -2
- package/src/game-document/GamePanel.tsx +4 -4
- package/src/game-document/InstanceInspectorPicker.tsx +2 -2
- package/src/game-document/crowd-debug.ts +1 -1
- package/src/game-document/device-preview.ts +10 -11
- package/src/game-document/physics-debug.ts +1 -1
- package/src/generation/GenerationActivity.tsx +3 -3
- package/src/generation/generation-documents.tsx +5 -5
- package/src/generation/generation-jobs.ts +1 -1
- package/src/host/adapter-runtime-bindings.ts +96 -5
- package/src/host/authoring/babylon-authoring-adapter.ts +6 -6
- package/src/host/authoring/gesture-persist.ts +1 -1
- package/src/host/authoring/ingest-data-writer.ts +1 -1
- package/src/host/authoring/ingest-source-persistence.ts +4 -4
- package/src/host/authoring/mounted-authoring.ts +4 -4
- package/src/host/authoring/phaser-live-authoring-adapter.ts +4 -4
- package/src/host/authoring/pixi-authoring-adapter.ts +40 -15
- package/src/host/authoring/pixi-creation-site-write-target.ts +2 -2
- package/src/host/authoring/pixi-live-write-target.ts +4 -4
- package/src/host/authoring/pixi-source-identity.ts +3 -3
- package/src/host/authoring/pixi-source-write-target.ts +1614 -0
- package/src/host/authoring/pixi-still-presentation.ts +1 -1
- package/src/host/authoring/pixi-structure-history.ts +2 -2
- package/src/host/authoring/pixi-transform-channels.ts +16 -14
- package/src/host/authoring/source-persistence-backend.ts +3 -3
- package/src/host/authoring/struct-write-pipe.ts +1 -1
- package/src/host/binding-resolver.ts +8 -9
- package/src/host/browser-transpile.ts +1 -1
- package/src/host/canvas-entry-runtime.ts +58 -47
- package/src/host/canvas-preview-frames.ts +482 -0
- package/src/host/components/CameraAuthoringOverlay.tsx +1 -1
- package/src/host/components/HeaderTelemetry.tsx +4 -4
- package/src/host/components/PixiIsolationSceneContent.tsx +10 -10
- package/src/host/components/ThreeIsolationSceneContent.tsx +3 -3
- package/src/host/components/frame-debugger-model.ts +2 -2
- package/src/host/components/header-telemetry-model.ts +2 -2
- package/src/host/components/scene-document.tsx +14 -14
- package/src/host/components/utility-view-state.ts +1 -1
- package/src/host/components/world-root-stage-binding.tsx +12 -12
- package/src/host/components/world-root-stage.ts +70 -49
- package/src/host/coverage/system-adapter-coverage.ts +3 -4
- package/src/host/design-system-stories/fixtures/editor-runtime.tsx +9 -11
- package/src/host/document-preview-three.ts +1 -1
- package/src/host/entry-adjudication.ts +6 -6
- package/src/host/game-css-scope-transform.ts +4 -0
- package/src/host/game-realm-page.ts +1 -1
- package/src/host/gameplay-export.ts +25 -14
- package/src/host/gameplay-recording.ts +5 -5
- package/src/host/gated-globals.ts +2 -2
- package/src/host/history/json-history-resource.ts +1 -1
- package/src/host/projection/pixi.ts +24 -2
- package/src/host/r3f-entry-runtime.ts +65 -34
- package/src/host/react-mount-runtime.ts +7 -48
- package/src/host/realm-services.ts +1 -1
- package/src/host/roots/canvas-root.tsx +361 -0
- package/src/host/roots/r3f-root.tsx +473 -0
- package/src/host/roots/react-root.ts +9 -43
- package/src/host/served-bundle-runtime-modules.ts +1 -19
- package/src/host/server-log-bridge.ts +2 -2
- package/src/host/stories/mounted-story-viewport-source.ts +1 -1
- package/src/host/stories/pixi-story-model.ts +30 -0
- package/src/host/stories/story-media-captures.ts +46 -0
- package/src/host/stories/story-media-presence.ts +3 -3
- package/src/host/stories/story-pixi-preview.ts +408 -0
- package/src/host/stories/story-three-preview.ts +806 -0
- package/src/host/stories/three-story-captures.ts +35 -0
- package/src/host/story-three-preview-runtime.ts +56 -0
- package/src/host/use-active-performance-source.ts +2 -2
- package/src/host/viewport-pose-memory.ts +1 -1
- package/src/host/viewport-root-presentation.ts +6 -5
- package/src/ingest/active-ingest.ts +1 -1
- package/src/ingest/authoring/ingest-dom-surface-authoring.ts +4 -4
- package/src/ingest/authoring/ingest-root-adapter.ts +8 -8
- package/src/ingest/deferred-ingest-play.ts +6 -6
- package/src/ingest/discovery-public-ingest.ts +2 -2
- package/src/ingest/ingest-boot-viewport.ts +2 -2
- package/src/ingest/ingest-canvas-scene-document.tsx +8 -8
- package/src/ingest/ingest-canvas-scene.ts +3 -3
- package/src/ingest/ingest-evidence-hook.ts +1 -1
- package/src/ingest/ingest-frame-snapshot.ts +1 -1
- package/src/ingest/ingest-play-control.ts +1 -1
- package/src/ingest/ingest-render-debug.ts +10 -10
- package/src/ingest/ingest-siblings.ts +13 -25
- package/src/ingest/module-mode.ts +14 -14
- package/src/ingest/mount-canvas-ingest-root.ts +21 -21
- package/src/ingest/mount-coverage.ts +2 -2
- package/src/ingest/mount-dom-ingest-root.ts +11 -11
- package/src/ingest/mount-ingest-root.ts +8 -8
- package/src/ingest/mount-three-ingest-root.ts +8 -8
- package/src/ingest/resolve-canvas.ts +1 -1
- package/src/ingest/served-html-boot.ts +1 -1
- package/src/ingest/surface-canvas.ts +1 -1
- package/src/ingest/unmount-ingest-root.ts +5 -5
- package/src/navmesh/navmesh-handler.ts +24 -16
- package/src/network/NetworkInspectorPanel.tsx +449 -14
- package/src/network/network-inspector-model.ts +14 -2
- package/src/play/play-log-events.ts +1 -1
- package/src/play/play-mode.ts +76 -133
- package/src/play/play-recording.ts +1 -1
- package/src/play/react-play-live-authoring.ts +3 -3
- package/src/play/run-selection.ts +93 -0
- package/src/play-bar/PlayBar.tsx +20 -39
- package/src/profiler/FrameDebuggerPanel.tsx +1 -1
- package/src/profiler/PerformancePanel.tsx +3 -3
- package/src/react/design-time-react-mount.ts +23 -65
- package/src/react/dom-authoring-adapter.ts +9 -9
- package/src/react/react-inspector-section.tsx +5 -5
- package/src/react/react-world-authoring-adapter.ts +11 -11
- package/src/react/story-documents/story-documents.tsx +9 -9
- package/src/react/ui-board-document.tsx +7 -7
- package/src/react/ui-component-board.ts +2 -2
- package/src/runtime/adapter/audio-meter.ts +21 -0
- package/src/runtime/adapter/first-party-audio-system.ts +230 -0
- package/src/runtime/adapter/ingest/contract-debug-adapter.ts +114 -0
- package/src/runtime/adapter/ingest/contract-system-adapters.ts +256 -0
- package/src/runtime/adapter/ingest/merge-debug-adapters.ts +197 -0
- package/src/runtime/adapter/ingest/observation-debug-adapter.ts +162 -0
- package/src/runtime/adapter/ingest/upstream-pin.ts +51 -0
- package/src/runtime/adapter/native-debug-module.ts +498 -0
- package/src/runtime/audio/bus-mixer.ts +161 -0
- package/src/runtime/audio/pose-guard.ts +80 -0
- package/src/runtime/core/frame-pacing.ts +126 -0
- package/src/runtime/core/game-loop.ts +225 -0
- package/src/runtime/core/game-scoped-slot.ts +28 -0
- package/src/runtime/core/seeded-random.ts +162 -0
- package/src/runtime/core/sim-clock.ts +391 -0
- package/src/runtime/core/system-runner.ts +269 -0
- package/src/runtime/core/types.ts +104 -0
- package/src/runtime/create-runtime.ts +1128 -0
- package/src/runtime/debug-bridge.ts +570 -0
- package/src/runtime/debug-registry.ts +899 -0
- package/src/runtime/dev/chrome-trace.ts +153 -0
- package/src/runtime/dev/instruments.ts +403 -0
- package/src/runtime/dev/logger.ts +119 -0
- package/src/runtime/dev/performance-profiler.ts +367 -0
- package/src/runtime/dev/register-render-vitals.ts +276 -0
- package/src/runtime/dev/render-census.ts +354 -0
- package/src/runtime/dev/render-debug-adapter.ts +218 -0
- package/src/runtime/dev/render-memory.ts +226 -0
- package/src/runtime/dev/render-vitals.ts +338 -0
- package/src/runtime/dev/static-batch-advisor.ts +188 -0
- package/src/runtime/dev/webgl-frame-capture.ts +366 -0
- package/src/runtime/dev/webgl-gpu-timer.ts +53 -0
- package/src/runtime/dev-build.ts +47 -0
- package/src/runtime/game.ts +1636 -0
- package/src/runtime/gameplay-rng-trap.ts +135 -0
- package/src/runtime/host-context.ts +64 -0
- package/src/runtime/input-router.ts +169 -0
- package/src/runtime/mount-manifest.ts +480 -0
- package/src/runtime/pixi/authoring.ts +706 -0
- package/src/runtime/pixi/ingest.ts +116 -0
- package/src/runtime/pixi/physics-registry.ts +49 -0
- package/src/runtime/pixi/render-pass-bracket.ts +117 -0
- package/src/runtime/pixi/scene-capture.ts +179 -0
- package/src/runtime/pixi/system-adapters.ts +69 -0
- package/src/runtime/playtest.ts +22 -0
- package/src/runtime/presentation.ts +141 -0
- package/src/runtime/render-control.ts +642 -0
- package/src/runtime/render-seed.ts +77 -0
- package/src/runtime/run-ticks-settled.ts +73 -0
- package/src/runtime/setup/setup-audio.ts +72 -0
- package/src/services/audio-pose-guard.ts +2 -2
- package/src/services/game-audio.ts +152 -0
- package/src/services/game-network.ts +657 -0
- package/src/services/game-physics.ts +334 -0
- package/src/state-watch/StateWatchPanel.tsx +1 -1
- package/src/three/authoring/camera-runtime-inspector-section.tsx +3 -2
- package/src/three/authoring/constraint-inspector-section.tsx +6 -5
- package/src/three/authoring/model-asset-inspector-section.tsx +7 -6
- package/src/three/authoring/oid-source-persistence.ts +7 -7
- package/src/three/authoring/r3f-design-session.ts +58 -54
- package/src/three/authoring/r3f-source-authoring-adapter.ts +95 -91
- package/src/three/authoring/reflection-probe-inspector-section.tsx +3 -2
- package/src/three/authoring/three-authoring-adapter.ts +29 -29
- package/src/three/component-verbs/extract-menu.ts +7 -6
- package/src/three/component-verbs/fork-menu.ts +7 -6
- package/src/three/component-verbs/internals-menu.ts +2 -2
- package/src/three/story-documents/three-story-documents.tsx +13 -13
- package/src/three/three-board/ThreeBoardDocument.tsx +13 -12
- package/src/three/three-board/board-scene.ts +6 -6
- package/src/three/three-board/three-component-board.ts +2 -2
- package/src/services/game-audio-unlock.ts +0 -48
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic `Math.random` seeding for render-control mode (I2 AC:
|
|
3
|
+
* "`Math.random` is deterministically seeded/stubbed page-wide before any
|
|
4
|
+
* game module executes").
|
|
5
|
+
*
|
|
6
|
+
* Deliberately a ZERO-IMPORT module (no `three`, no other engine file) so
|
|
7
|
+
* its own evaluation is instantaneous — a render-mode entry page loads this
|
|
8
|
+
* as its FIRST `<script type="module">` tag, before the tag that pulls in
|
|
9
|
+
* the actual game/runtime modules. Per the HTML module-script ordering
|
|
10
|
+
* rules, sibling `<script type="module">` tags without `async` execute in
|
|
11
|
+
* document order, and each one's own dependency graph (here, empty) is
|
|
12
|
+
* fully evaluated before the browser moves to the next tag — so calling
|
|
13
|
+
* {@link maybeInstallRenderModeRandom} from this module's importer, in a
|
|
14
|
+
* script tag placed before the game's own entry tag, guarantees every
|
|
15
|
+
* subsequent module's top-level code (and everything it does at runtime)
|
|
16
|
+
* observes the seeded generator, never the platform's real
|
|
17
|
+
* cryptographically-random `Math.random`.
|
|
18
|
+
*
|
|
19
|
+
* This module does not decide WHETHER to seed on its own (importing it has
|
|
20
|
+
* no side effect) — the caller decides, by calling
|
|
21
|
+
* {@link maybeInstallRenderModeRandom} (query-param gated) or
|
|
22
|
+
* {@link installDeterministicRandom} (unconditional) from a tiny bootstrap
|
|
23
|
+
* entry. See `packages/engine/e2e/fixture/render-seed-entry.ts` for the
|
|
24
|
+
* worked bootstrap this module is designed to be loaded from.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* mulberry32 — a small, fast, deterministic 32-bit PRNG. Not cryptographic;
|
|
29
|
+
* this is explicitly a determinism tool (reproducible capture), not a
|
|
30
|
+
* security primitive. Returns a function with the same call signature as
|
|
31
|
+
* `Math.random` (no arguments, `[0, 1)` float).
|
|
32
|
+
*/
|
|
33
|
+
export function createMulberry32(seed: number): () => number {
|
|
34
|
+
let state = seed >>> 0;
|
|
35
|
+
return function mulberry32(): number {
|
|
36
|
+
state = (state + 0x6d2b79f5) | 0;
|
|
37
|
+
let t = state;
|
|
38
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
39
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
40
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Replace the global `Math.random` with a seeded, deterministic generator.
|
|
46
|
+
* Idempotent to call more than once (each call re-seeds from scratch — the
|
|
47
|
+
* last call wins), which matters for HMR-style re-execution.
|
|
48
|
+
*/
|
|
49
|
+
export function installDeterministicRandom(seed: number): void {
|
|
50
|
+
Math.random = createMulberry32(seed);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Query-param name that opts a page into render-control mode (I2 AC 4: the
|
|
54
|
+
* mode must be unavailable/protected in normal production gameplay unless
|
|
55
|
+
* explicitly enabled). Shared with `render-control.ts`'s own production
|
|
56
|
+
* guard so both checks agree on the same flag. */
|
|
57
|
+
export const RENDER_MODE_QUERY_PARAM = 'vgai-render';
|
|
58
|
+
/** Optional query param to pin a specific seed (default below when absent). */
|
|
59
|
+
export const RENDER_SEED_QUERY_PARAM = 'vgai-seed';
|
|
60
|
+
const DEFAULT_RENDER_SEED = 0x9e3779b9; // golden-ratio constant — an arbitrary but fixed default
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Seed `Math.random` iff the page's URL opts into render mode
|
|
64
|
+
* (`?vgai-render=1`), reading an optional `?vgai-seed=<int>` override.
|
|
65
|
+
* Returns whether it seeded, purely for the caller's own diagnostics/log —
|
|
66
|
+
* the bootstrap entry doesn't need to branch on it.
|
|
67
|
+
*/
|
|
68
|
+
export function maybeInstallRenderModeRandom(
|
|
69
|
+
location: { search: string } = window.location,
|
|
70
|
+
): boolean {
|
|
71
|
+
const params = new URLSearchParams(location.search);
|
|
72
|
+
if (params.get(RENDER_MODE_QUERY_PARAM) !== '1') return false;
|
|
73
|
+
const seedParam = params.get(RENDER_SEED_QUERY_PARAM);
|
|
74
|
+
const seed = seedParam !== null && seedParam !== '' ? Number(seedParam) : DEFAULT_RENDER_SEED;
|
|
75
|
+
installDeterministicRandom(Number.isFinite(seed) ? seed : DEFAULT_RENDER_SEED);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `runTicksWhenSettled` — the settled-aware tick driver behind BOTH session run-ticks doors
|
|
3
|
+
* (the bridge's `window.__vgai.runTicksSettled`, the editor relay's `run-ticks` case), one
|
|
4
|
+
* implementation with byte-identical semantics across doors, exactly as `runTicks` itself is
|
|
5
|
+
* shared (D17).
|
|
6
|
+
*
|
|
7
|
+
* Why it exists: `Game.runTicks` is deliberately SYNCHRONOUS, but a game may be intentionally
|
|
8
|
+
* BETWEEN worlds when a tick arrives — `reload_current_scene` unmounts the running scene and
|
|
9
|
+
* the fresh one arrives on an ASYNC React commit. A tick driven into that gap runs against
|
|
10
|
+
* nothing, and WHICH tick first runs the fresh world then depends on wall timing between
|
|
11
|
+
* driver calls — measured on the starter-kit fidelity drive: the same script produced 7 or 8
|
|
12
|
+
* post-respawn walked ticks depending on how long the driver idled between ticks. The fidelity
|
|
13
|
+
* contract treats a nondeterministic channel as a MECHANISM defect (the instrument that spelled
|
|
14
|
+
* that out went with the archived import lanes, `archive/*-lane-2026-09-19`; the rule did not),
|
|
15
|
+
* and this is the mechanism: ticks must not race remounts.
|
|
16
|
+
*
|
|
17
|
+
* So this driver runs the budget ONE tick at a time and, before each tick, waits for every
|
|
18
|
+
* registered world-settled probe ({@link DebugRegistry.registerWorldSettledProbe} — declared by
|
|
19
|
+
* the game through its `debug.settled` entry export) to answer `true`. The wall-clock poll is
|
|
20
|
+
* sound because the OUTCOME no longer depends on timing: however long the commit takes, the
|
|
21
|
+
* next tick is always the fresh world's first. A game that registers no probe gets the old
|
|
22
|
+
* behavior exactly (every wait resolves immediately).
|
|
23
|
+
*
|
|
24
|
+
* The wait is BOUNDED and loud: a remount that never settles is a real defect, and
|
|
25
|
+
* `WORLD_UNSETTLED_TIMEOUT` names it rather than letting the driver hang.
|
|
26
|
+
*/
|
|
27
|
+
import { DebugError, type DebugRegistry, type RunTicksOptions } from './debug-registry';
|
|
28
|
+
|
|
29
|
+
/** A remount is a couple of React commits on an idle main thread; polling faster buys nothing
|
|
30
|
+
* and slower delays every settled check by the poll period. */
|
|
31
|
+
const SETTLED_POLL_MS = 4;
|
|
32
|
+
/** A commit that hasn't landed after this long is not "in flight", it is stuck. */
|
|
33
|
+
const SETTLED_TIMEOUT_MS = 10_000;
|
|
34
|
+
|
|
35
|
+
export async function runTicksWhenSettled(
|
|
36
|
+
registry: Pick<DebugRegistry, 'worldSettled' | 'getRunTicksTarget'>,
|
|
37
|
+
n: number,
|
|
38
|
+
opts?: RunTicksOptions,
|
|
39
|
+
/** Test-only overrides — real callers leave this unset (the same seam
|
|
40
|
+
* convention as fast-forward's `batchTicks`). */
|
|
41
|
+
waits?: { timeoutMs?: number; pollMs?: number },
|
|
42
|
+
): Promise<void> {
|
|
43
|
+
const target = registry.getRunTicksTarget();
|
|
44
|
+
if (target === null) {
|
|
45
|
+
throw new DebugError(
|
|
46
|
+
'RUN_TICKS_UNAVAILABLE',
|
|
47
|
+
'runTicksWhenSettled: no run-ticks target is wired — no Game has mounted yet',
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
const timeoutMs = waits?.timeoutMs ?? SETTLED_TIMEOUT_MS;
|
|
51
|
+
const pollMs = waits?.pollMs ?? SETTLED_POLL_MS;
|
|
52
|
+
const render = opts?.render ?? 'last';
|
|
53
|
+
for (let i = 0; i < n; i++) {
|
|
54
|
+
if (!registry.worldSettled()) {
|
|
55
|
+
const deadline = Date.now() + timeoutMs;
|
|
56
|
+
while (!registry.worldSettled()) {
|
|
57
|
+
if (Date.now() > deadline) {
|
|
58
|
+
throw new DebugError(
|
|
59
|
+
'WORLD_UNSETTLED_TIMEOUT',
|
|
60
|
+
`runTicksWhenSettled: a world-settled probe still answers false after ${timeoutMs}ms — ` +
|
|
61
|
+
'a scene remount (or a broken probe) is stuck, and driving ticks into it would race it',
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const isFinalTick = i === n - 1;
|
|
68
|
+
// Same per-tick render policy `Game.runTicks` applies across its own loop.
|
|
69
|
+
const tickRender =
|
|
70
|
+
render === 'all' ? 'all' : render === 'none' ? 'none' : isFinalTick ? 'last' : 'none';
|
|
71
|
+
target.runTicks(1, { render: tickRender });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { type AudioBusMixer, createAudioBusMixer } from '../audio/bus-mixer';
|
|
3
|
+
import { installAudioPoseGuard } from '../audio/pose-guard';
|
|
4
|
+
|
|
5
|
+
export interface AudioContext {
|
|
6
|
+
listener: THREE.AudioListener;
|
|
7
|
+
masterGain: GainNode;
|
|
8
|
+
buses: {
|
|
9
|
+
music: GainNode;
|
|
10
|
+
sfx: GainNode;
|
|
11
|
+
voice: GainNode;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* The named-bus mixer the three below are buses OF (`../audio/bus-mixer.ts`).
|
|
15
|
+
*
|
|
16
|
+
* `buses` is a fixed struct because those three names are the ones the engine
|
|
17
|
+
* itself routes; a game with an `ambience`, a `ui` or a per-character bus asks
|
|
18
|
+
* the mixer for it by name — `audio.mixer.bus('ambience', 'master')` mints it
|
|
19
|
+
* under the master gain, `audio.mixer.setDb('ambience', -6)` sets it in
|
|
20
|
+
* decibels — and `audio.mixer.disconnect()` is the ONE teardown for all of
|
|
21
|
+
* them, the three struct members included.
|
|
22
|
+
*/
|
|
23
|
+
mixer: AudioBusMixer;
|
|
24
|
+
/** Call this on first user interaction (click/key) to resume audio context */
|
|
25
|
+
resume: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The bus every other bus routes into, and the node `masterGain` is. */
|
|
29
|
+
const MASTER_BUS = 'master';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Creates the audio listener and bus hierarchy.
|
|
33
|
+
*
|
|
34
|
+
* Bus routing: music/sfx/voice → master → destination
|
|
35
|
+
* Attach `listener` to the active camera: `camera.add(audioCtx.listener)`.
|
|
36
|
+
*/
|
|
37
|
+
export function setupAudio(camera: THREE.Camera): AudioContext {
|
|
38
|
+
installAudioPoseGuard();
|
|
39
|
+
const listener = new THREE.AudioListener();
|
|
40
|
+
camera.add(listener);
|
|
41
|
+
|
|
42
|
+
const ctx = listener.context;
|
|
43
|
+
const mixer = createAudioBusMixer({ context: ctx, destination: ctx.destination });
|
|
44
|
+
const masterGain = mixer.bus(MASTER_BUS);
|
|
45
|
+
|
|
46
|
+
// Route the Three.js listener through masterGain so that THREE.Audio /
|
|
47
|
+
// THREE.PositionalAudio objects are also silenced by the mute button.
|
|
48
|
+
listener.gain.disconnect();
|
|
49
|
+
listener.gain.connect(masterGain);
|
|
50
|
+
|
|
51
|
+
const musicGain = mixer.bus('music', MASTER_BUS);
|
|
52
|
+
musicGain.gain.value = 0.7;
|
|
53
|
+
|
|
54
|
+
const sfxGain = mixer.bus('sfx', MASTER_BUS);
|
|
55
|
+
const voiceGain = mixer.bus('voice', MASTER_BUS);
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
listener,
|
|
59
|
+
masterGain,
|
|
60
|
+
buses: {
|
|
61
|
+
music: musicGain,
|
|
62
|
+
sfx: sfxGain,
|
|
63
|
+
voice: voiceGain,
|
|
64
|
+
},
|
|
65
|
+
mixer,
|
|
66
|
+
resume() {
|
|
67
|
+
if (ctx.state === 'suspended') {
|
|
68
|
+
ctx.resume();
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -60,9 +60,9 @@
|
|
|
60
60
|
*/
|
|
61
61
|
|
|
62
62
|
import { editorHost } from '@volter/editor-sdk/host';
|
|
63
|
-
import { installAudioPoseGuard as installSharedAudioPoseGuard } from '
|
|
63
|
+
import { installAudioPoseGuard as installSharedAudioPoseGuard } from '../runtime/audio/pose-guard';
|
|
64
64
|
|
|
65
|
-
export { audioPoseUpdatesDropped } from '
|
|
65
|
+
export { audioPoseUpdatesDropped } from '../runtime/audio/pose-guard';
|
|
66
66
|
|
|
67
67
|
let reported = false;
|
|
68
68
|
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE PAGE'S VIEW OF A RUNNING GAME'S WEB AUDIO — the editor's own audio adapter for a game
|
|
3
|
+
* that declares none (the ingest way: the game is idiomatic code, and the editor reads it).
|
|
4
|
+
*
|
|
5
|
+
* UNLOCK. A game running in the editor's page can create its AudioContext before the
|
|
6
|
+
* visitor's first gesture (survivor's auto-attack lands kills while they are still reading),
|
|
7
|
+
* and browsers with a strict autoplay policy then hold it suspended until `resume()` runs
|
|
8
|
+
* INSIDE a trusted input handler. The game's own unlock listeners go through the realm's
|
|
9
|
+
* gated re-dispatch, which is no longer that trusted stack — two human passes heard both
|
|
10
|
+
* flagship canvas games silent on first load and cured them only by restarting under a
|
|
11
|
+
* gesture (runhuman 23/33). The EDITOR owns the page, so it wraps the page's AudioContext
|
|
12
|
+
* constructor once at boot to track instances, and resumes any suspended one from
|
|
13
|
+
* capture-phase pointerdown/keydown handlers — the real, trusted stack.
|
|
14
|
+
*
|
|
15
|
+
* MUTE AND THE GRAPH. The same wrap is what lets the editor pause a game's sound and show
|
|
16
|
+
* what it built without the game implementing anything: every connection a game makes to
|
|
17
|
+
* its context's destination is routed through one editor-owned gain per context, and every
|
|
18
|
+
* connection is recorded. {@link observedGameAudio} is that adapter; Play installs it for a
|
|
19
|
+
* game whose systems declare no `audio` (`play-mode.ts`).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { AudioAdapter, AudioGraphNode } from '@volter/editor-project/adapter/system-adapter';
|
|
23
|
+
|
|
24
|
+
const tracked = new Set<WeakRef<AudioContext>>();
|
|
25
|
+
|
|
26
|
+
function resumeSuspended(): void {
|
|
27
|
+
for (const ref of tracked) {
|
|
28
|
+
const ctx = ref.deref();
|
|
29
|
+
if (!ctx || ctx.state === 'closed') {
|
|
30
|
+
tracked.delete(ref);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (ctx.state === 'suspended') void ctx.resume();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Install once at editor boot (`audio.service.ts`). Idempotent. */
|
|
38
|
+
export function installGameAudio(): void {
|
|
39
|
+
const host = window as unknown as { __vgaiAudioUnlock?: boolean } & typeof window;
|
|
40
|
+
if (host.__vgaiAudioUnlock) return;
|
|
41
|
+
host.__vgaiAudioUnlock = true;
|
|
42
|
+
const Orig = window.AudioContext;
|
|
43
|
+
if (typeof Orig !== 'function') return;
|
|
44
|
+
window.AudioContext = class extends Orig {
|
|
45
|
+
constructor(...args: ConstructorParameters<typeof AudioContext>) {
|
|
46
|
+
super(...args);
|
|
47
|
+
tracked.add(new WeakRef(this));
|
|
48
|
+
trackedContexts.add(this);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
installConnectionRecorder();
|
|
52
|
+
window.addEventListener('pointerdown', resumeSuspended, { capture: true });
|
|
53
|
+
window.addEventListener('keydown', resumeSuspended, { capture: true });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---- Routing and recording -------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
/** One editor gain per tracked context, between the game's graph and the real output. */
|
|
59
|
+
const buses = new WeakMap<BaseAudioContext, GainNode>();
|
|
60
|
+
const trackedContexts = new WeakSet<BaseAudioContext>();
|
|
61
|
+
let muted = false;
|
|
62
|
+
|
|
63
|
+
/** The game's recorded connections: node → the nodes it feeds. */
|
|
64
|
+
const outputs = new WeakMap<AudioNode, Set<AudioNode>>();
|
|
65
|
+
const seen = new Set<WeakRef<AudioNode>>();
|
|
66
|
+
const ids = new WeakMap<AudioNode, string>();
|
|
67
|
+
let nextId = 0;
|
|
68
|
+
|
|
69
|
+
function idOf(node: AudioNode): string {
|
|
70
|
+
if (node instanceof AudioDestinationNode) return 'destination';
|
|
71
|
+
let id = ids.get(node);
|
|
72
|
+
if (!id) {
|
|
73
|
+
id = `n${++nextId}`;
|
|
74
|
+
ids.set(node, id);
|
|
75
|
+
seen.add(new WeakRef(node));
|
|
76
|
+
}
|
|
77
|
+
return id;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function installConnectionRecorder(): void {
|
|
81
|
+
const connect = AudioNode.prototype.connect as (this: AudioNode, ...args: unknown[]) => unknown;
|
|
82
|
+
const disconnect = AudioNode.prototype.disconnect as (this: AudioNode, ...args: unknown[]) => unknown;
|
|
83
|
+
const busFor = (context: BaseAudioContext): GainNode => {
|
|
84
|
+
let bus = buses.get(context);
|
|
85
|
+
if (!bus) {
|
|
86
|
+
bus = context.createGain();
|
|
87
|
+
bus.gain.value = muted ? 0 : 1;
|
|
88
|
+
connect.call(bus, context.destination);
|
|
89
|
+
buses.set(context, bus);
|
|
90
|
+
}
|
|
91
|
+
return bus;
|
|
92
|
+
};
|
|
93
|
+
const routed = (target: unknown): unknown =>
|
|
94
|
+
target instanceof AudioDestinationNode && trackedContexts.has(target.context) ? busFor(target.context) : target;
|
|
95
|
+
AudioNode.prototype.connect = function (this: AudioNode, ...args: unknown[]) {
|
|
96
|
+
const [target, ...rest] = args;
|
|
97
|
+
if (target instanceof AudioNode && !(buses.get(this.context) === this)) {
|
|
98
|
+
idOf(this);
|
|
99
|
+
idOf(target);
|
|
100
|
+
let set = outputs.get(this);
|
|
101
|
+
if (!set) outputs.set(this, (set = new Set()));
|
|
102
|
+
set.add(target);
|
|
103
|
+
}
|
|
104
|
+
const result = connect.call(this, routed(target), ...rest);
|
|
105
|
+
// A game chains `a.connect(b).connect(c)`: it gets back what it asked for.
|
|
106
|
+
return target instanceof AudioDestinationNode ? target : result;
|
|
107
|
+
} as typeof AudioNode.prototype.connect;
|
|
108
|
+
AudioNode.prototype.disconnect = function (this: AudioNode, ...args: unknown[]) {
|
|
109
|
+
const [target] = args;
|
|
110
|
+
if (target === undefined || typeof target === 'number') outputs.delete(this);
|
|
111
|
+
else if (target instanceof AudioNode) outputs.get(this)?.delete(target);
|
|
112
|
+
return disconnect.call(this, ...(args.length ? [routed(target), ...args.slice(1)] : []));
|
|
113
|
+
} as typeof AudioNode.prototype.disconnect;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Silence every tracked game context, or restore it; the game's own gains are untouched. */
|
|
117
|
+
function setGameAudioMuted(next: boolean): void {
|
|
118
|
+
muted = next;
|
|
119
|
+
for (const ref of tracked) {
|
|
120
|
+
const bus = ref.deref() && buses.get(ref.deref()!);
|
|
121
|
+
if (bus) bus.gain.value = next ? 0 : 1;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** What the game has built, as the recorded connections read it now. */
|
|
126
|
+
function gameAudioGraph(): AudioGraphNode[] {
|
|
127
|
+
const nodes: AudioGraphNode[] = [];
|
|
128
|
+
let destination = false;
|
|
129
|
+
for (const ref of seen) {
|
|
130
|
+
const node = ref.deref();
|
|
131
|
+
if (!node) {
|
|
132
|
+
seen.delete(ref);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const targets = [...(outputs.get(node) ?? [])];
|
|
136
|
+
if (targets.some((target) => target instanceof AudioDestinationNode)) destination = true;
|
|
137
|
+
nodes.push({ id: idOf(node), type: node.constructor.name, outputs: targets.map(idOf), state: node.context.state });
|
|
138
|
+
}
|
|
139
|
+
if (destination) nodes.push({ id: 'destination', type: 'AudioDestinationNode', label: 'Output', outputs: [] });
|
|
140
|
+
return nodes;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The editor's audio adapter for a game whose systems declare none: mute through the editor's
|
|
145
|
+
* bus, resume through the trusted-gesture unlock, and the graph the game built.
|
|
146
|
+
*/
|
|
147
|
+
export const observedGameAudio: AudioAdapter = {
|
|
148
|
+
resume: resumeSuspended,
|
|
149
|
+
setMuted: setGameAudioMuted,
|
|
150
|
+
isMuted: () => muted,
|
|
151
|
+
graphSnapshot: gameAudioGraph,
|
|
152
|
+
};
|