@volter/editor-game 0.5.65
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/LICENSE +661 -0
- package/NOTICE +23 -0
- package/contributions/asset-budget-asset.menu.ts +26 -0
- package/contributions/asset-budget.action.ts +18 -0
- package/contributions/asset-budget.document.tsx +21 -0
- package/contributions/asset-budget.menu.ts +22 -0
- package/contributions/audio-unlock.service.ts +17 -0
- package/contributions/audio.utility.tsx +15 -0
- package/contributions/autoplay.service.ts +48 -0
- package/contributions/bridge.command.ts +172 -0
- package/contributions/build-profiles.document.tsx +21 -0
- package/contributions/build-progress.status.tsx +45 -0
- package/contributions/build.action.ts +26 -0
- package/contributions/build.command.ts +27 -0
- package/contributions/build.header.tsx +37 -0
- package/contributions/build.menu.ts +31 -0
- package/contributions/build.service.ts +31 -0
- package/contributions/connection.status.tsx +43 -0
- package/contributions/coverage.service.ts +118 -0
- package/contributions/edit-mode-audio.service.ts +23 -0
- package/contributions/edit-mode-networking.service.ts +30 -0
- package/contributions/game-document.service.ts +25 -0
- package/contributions/game-eval.command.ts +210 -0
- package/contributions/game.layout.ts +19 -0
- package/contributions/gameplay.command.ts +255 -0
- package/contributions/generation.service.ts +96 -0
- package/contributions/generations.status.tsx +53 -0
- package/contributions/ingest.service.ts +80 -0
- package/contributions/instances.command.ts +75 -0
- package/contributions/navmesh.menu.ts +39 -0
- package/contributions/navmesh.service.ts +19 -0
- package/contributions/network.utility.tsx +17 -0
- package/contributions/play.command.ts +355 -0
- package/contributions/profiler.action.ts +16 -0
- package/contributions/profiler.menu.ts +22 -0
- package/contributions/profiler.utility.tsx +17 -0
- package/contributions/react/component-board.service.ts +21 -0
- package/contributions/react/design-time-mount.service.ts +60 -0
- package/contributions/react/pasteboard.action.ts +41 -0
- package/contributions/react/react-inspector.service.ts +85 -0
- package/contributions/react/story-documents.service.ts +44 -0
- package/contributions/scene-document.service.ts +32 -0
- package/contributions/state-watch.action.ts +17 -0
- package/contributions/state-watch.menu.ts +23 -0
- package/contributions/state-watch.utility.tsx +20 -0
- package/contributions/team-playtest.service.ts +124 -0
- package/contributions/three/camera-runtime.inspector.tsx +34 -0
- package/contributions/three/component-board.service.ts +24 -0
- package/contributions/three/component-verbs.command.ts +110 -0
- package/contributions/three/component-verbs.service.ts +92 -0
- package/contributions/three/constraints.inspector.tsx +34 -0
- package/contributions/three/model-asset-sections.service.ts +26 -0
- package/contributions/three/reflection-probe-capture.inspector.tsx +32 -0
- package/contributions/three/story-documents.service.ts +31 -0
- package/contributions/three/three-authoring.service.ts +66 -0
- package/contributions/transport.header.tsx +19 -0
- package/contributions/xstate-behavior.action.ts +42 -0
- package/contributions/xstate-behavior.document.tsx +73 -0
- package/contributions/xstate-behavior.inspector.tsx +28 -0
- package/contributions/xstate-behavior.menu.ts +23 -0
- package/package.json +144 -0
- package/src/asset-budget/AssetBudgetPanel.tsx +1172 -0
- package/src/asset-budget/asset-budget-model.ts +799 -0
- package/src/asset-budget/basis-encoder.ts +165 -0
- package/src/asset-budget/gltf-io.ts +154 -0
- package/src/asset-budget/gltf-optimize.ts +384 -0
- package/src/asset-budget/image-dims.ts +78 -0
- package/src/asset-budget/optimize-apply.ts +221 -0
- package/src/audio/AudioDebuggerPanel.tsx +457 -0
- package/src/audio/audio-debugger-model.ts +87 -0
- package/src/bridge/call.ts +60 -0
- package/src/bridge/dispatch.ts +459 -0
- package/src/bridge/live-frames.ts +25 -0
- package/src/bridge/screenshot.ts +316 -0
- package/src/build/BuildProfilesPanel.tsx +476 -0
- package/src/build/build-session.ts +247 -0
- package/src/build/format-bytes.ts +14 -0
- package/src/command-results.ts +36 -0
- package/src/coverage/live-authoring-surface.ts +30 -0
- package/src/coverage/live-project-verbs.ts +162 -0
- package/src/coverage/native-system-coverage.ts +143 -0
- package/src/coverage/root-coverage.ts +79 -0
- package/src/coverage/session-coverage.ts +193 -0
- package/src/design-system-stories/ApplicationChrome.stories.tsx +100 -0
- package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +406 -0
- package/src/edit-mode/edit-mode-audio.ts +56 -0
- package/src/edit-mode/edit-mode-networking.ts +112 -0
- package/src/game-document/DevicePresetPicker.tsx +84 -0
- package/src/game-document/GameCaptureFrameButton.tsx +45 -0
- package/src/game-document/GameDocument.tsx +215 -0
- package/src/game-document/GamePanel.tsx +662 -0
- package/src/game-document/InstanceInspectorPicker.tsx +183 -0
- package/src/game-document/crowd-debug.ts +183 -0
- package/src/game-document/device-preview.ts +336 -0
- package/src/game-document/game-view-store.ts +107 -0
- package/src/game-document/physics-debug.ts +187 -0
- package/src/generation/GenerationActivity.tsx +421 -0
- package/src/generation/GenerationGallery.css +231 -0
- package/src/generation/generation-documents.tsx +338 -0
- package/src/generation/generation-jobs.ts +128 -0
- package/src/generation/generation-presentation.ts +257 -0
- package/src/host/adapter-reach.ts +445 -0
- package/src/host/adapter-runtime-bindings.ts +303 -0
- package/src/host/after-paint.ts +112 -0
- package/src/host/api/configurations.ts +66 -0
- package/src/host/authoring/babylon-authoring-adapter.ts +703 -0
- package/src/host/authoring/canvas-runtime-recognition.ts +50 -0
- package/src/host/authoring/contract-hierarchy-authoring.ts +144 -0
- package/src/host/authoring/contract-scenes-stories.ts +204 -0
- package/src/host/authoring/creation-site-related.ts +55 -0
- package/src/host/authoring/ephemeral-persistence.ts +29 -0
- package/src/host/authoring/gesture-persist.ts +84 -0
- package/src/host/authoring/ingest-data-writer.ts +232 -0
- package/src/host/authoring/ingest-source-persistence.ts +826 -0
- package/src/host/authoring/mount-isolated-pixi-screen.ts +250 -0
- package/src/host/authoring/mounted-authoring.ts +41 -0
- package/src/host/authoring/owned-pixi-ticker-listeners.ts +96 -0
- package/src/host/authoring/phaser-live-authoring-adapter.ts +265 -0
- package/src/host/authoring/pixi-authoring-adapter.ts +1554 -0
- package/src/host/authoring/pixi-creation-site-write-target.ts +60 -0
- package/src/host/authoring/pixi-isolation-assets.ts +25 -0
- package/src/host/authoring/pixi-live-write-target.ts +979 -0
- package/src/host/authoring/pixi-source-identity.ts +141 -0
- package/src/host/authoring/pixi-still-presentation.ts +71 -0
- package/src/host/authoring/pixi-structure-history.ts +237 -0
- package/src/host/authoring/pixi-transform-channels.ts +205 -0
- package/src/host/authoring/selection-remount-handoff.ts +23 -0
- package/src/host/authoring/source-persistence-backend.ts +373 -0
- package/src/host/authoring/source-refresh-revisions.ts +81 -0
- package/src/host/authoring/struct-write-pipe.ts +143 -0
- package/src/host/auto-frame-window.ts +89 -0
- package/src/host/binding-resolver.ts +393 -0
- package/src/host/browser-transpile.ts +631 -0
- package/src/host/canvas-entry-runtime.ts +95 -0
- package/src/host/components/CameraAuthoringOverlay.tsx +216 -0
- package/src/host/components/HeaderTelemetry.tsx +341 -0
- package/src/host/components/PixiIsolationSceneContent.tsx +280 -0
- package/src/host/components/ResolutionPicker.tsx +89 -0
- package/src/host/components/ThreeIsolationSceneContent.tsx +180 -0
- package/src/host/components/frame-debugger-model.ts +579 -0
- package/src/host/components/header-telemetry-model.ts +74 -0
- package/src/host/components/scene-document.tsx +447 -0
- package/src/host/components/utility-view-state.ts +87 -0
- package/src/host/components/world-root-stage-binding.tsx +133 -0
- package/src/host/components/world-root-stage.ts +1046 -0
- package/src/host/coverage/authoring-read-probe.ts +583 -0
- package/src/host/coverage/capability-coverage.ts +1587 -0
- package/src/host/coverage/coverage-accounting.ts +261 -0
- package/src/host/coverage/game-contract-seam-evidence.ts +82 -0
- package/src/host/coverage/project-verb-coverage.ts +148 -0
- package/src/host/coverage/system-adapter-coverage.ts +373 -0
- package/src/host/design-system-stories/StoryLayout.tsx +104 -0
- package/src/host/design-system-stories/fixtures/authoring.ts +247 -0
- package/src/host/design-system-stories/fixtures/editor-runtime.tsx +115 -0
- package/src/host/document-preview-three.ts +110 -0
- package/src/host/entry-adjudication.ts +89 -0
- package/src/host/game-location-guard.ts +150 -0
- package/src/host/game-module-access.ts +196 -0
- package/src/host/game-realm-page.ts +358 -0
- package/src/host/game-realm-reclaim.ts +55 -0
- package/src/host/game-realm-storage.ts +104 -0
- package/src/host/gameplay-export.ts +288 -0
- package/src/host/gameplay-recording.ts +717 -0
- package/src/host/gated-globals.ts +1511 -0
- package/src/host/history/json-history-resource.ts +254 -0
- package/src/host/ingest/registry.ts +261 -0
- package/src/host/instance-extract-actions.ts +120 -0
- package/src/host/instance-fork-actions.ts +120 -0
- package/src/host/play-control-hook.ts +26 -0
- package/src/host/playwright-shim.ts +479 -0
- package/src/host/projection/dom.ts +295 -0
- package/src/host/projection/pixi.ts +288 -0
- package/src/host/r3f-entry-runtime.ts +77 -0
- package/src/host/react-mount-runtime.ts +162 -0
- package/src/host/realm-services.ts +148 -0
- package/src/host/recording-preview.ts +106 -0
- package/src/host/roots/module-root.ts +203 -0
- package/src/host/roots/react-root.ts +181 -0
- package/src/host/same-realm-loop-gate.ts +544 -0
- package/src/host/scene-view-drawability.ts +69 -0
- package/src/host/sdk/tools.ts +31 -0
- package/src/host/served-bundle-runtime-modules.ts +331 -0
- package/src/host/server-log-bridge.ts +60 -0
- package/src/host/staged-projects.ts +24 -0
- package/src/host/stories/mounted-story-viewport-source.ts +64 -0
- package/src/host/stories/story-arg-descriptors.ts +50 -0
- package/src/host/stories/story-media-presence.ts +152 -0
- package/src/host/surface-content.ts +87 -0
- package/src/host/take-named-export.ts +23 -0
- package/src/host/three-ingest-runtime.ts +76 -0
- package/src/host/types-fastnoise-lite.d.ts +7 -0
- package/src/host/types-mikktspace.d.ts +20 -0
- package/src/host/types-troika-three-text.d.ts +7 -0
- package/src/host/use-active-performance-source.ts +53 -0
- package/src/host/viewport-pose-memory.ts +48 -0
- package/src/host/viewport-root-presentation.ts +40 -0
- package/src/ingest/active-ingest.ts +251 -0
- package/src/ingest/active-scene-navigation.ts +43 -0
- package/src/ingest/authoring/ingest-dom-surface-authoring.ts +191 -0
- package/src/ingest/authoring/ingest-root-adapter.ts +897 -0
- package/src/ingest/capture-wait-report.ts +122 -0
- package/src/ingest/deferred-ingest-play.ts +216 -0
- package/src/ingest/deferred-ingest-session.ts +23 -0
- package/src/ingest/discovery-public-ingest.ts +242 -0
- package/src/ingest/dom-stub-mark.ts +7 -0
- package/src/ingest/entry-load.ts +56 -0
- package/src/ingest/game-contract-realm.ts +34 -0
- package/src/ingest/game-pointer-lock.ts +89 -0
- package/src/ingest/held-scene-repaint.ts +73 -0
- package/src/ingest/host-surface-box.ts +174 -0
- package/src/ingest/ingest-boot-viewport.ts +54 -0
- package/src/ingest/ingest-canvas-scene-document.tsx +177 -0
- package/src/ingest/ingest-canvas-scene.ts +50 -0
- package/src/ingest/ingest-evidence-hook.ts +131 -0
- package/src/ingest/ingest-frame-snapshot.ts +183 -0
- package/src/ingest/ingest-play-commands.ts +134 -0
- package/src/ingest/ingest-play-control.ts +299 -0
- package/src/ingest/ingest-render-debug.ts +320 -0
- package/src/ingest/ingest-siblings.ts +487 -0
- package/src/ingest/ingest-status.ts +62 -0
- package/src/ingest/live-ingest-facet.ts +46 -0
- package/src/ingest/module-mode.ts +229 -0
- package/src/ingest/mount-canvas-ingest-root.ts +899 -0
- package/src/ingest/mount-coverage.ts +296 -0
- package/src/ingest/mount-dom-ingest-root.ts +282 -0
- package/src/ingest/mount-ingest-root.ts +744 -0
- package/src/ingest/mount-three-ingest-root.ts +366 -0
- package/src/ingest/resolve-canvas.ts +47 -0
- package/src/ingest/resolve-three.ts +123 -0
- package/src/ingest/served-bundle.ts +109 -0
- package/src/ingest/served-html-boot.ts +292 -0
- package/src/ingest/surface-canvas.ts +84 -0
- package/src/ingest/surface-dom.ts +84 -0
- package/src/ingest/surface-three.ts +128 -0
- package/src/ingest/types.ts +76 -0
- package/src/ingest/unmount-ingest-root.ts +224 -0
- package/src/navmesh/navmesh-actions.ts +27 -0
- package/src/navmesh/navmesh-handler.ts +237 -0
- package/src/navmesh/navmesh-workflow-store.ts +78 -0
- package/src/network/NetworkInspectorPanel.tsx +644 -0
- package/src/network/network-inspector-model.ts +225 -0
- package/src/play/play-boot-stall.ts +118 -0
- package/src/play/play-log-events.ts +26 -0
- package/src/play/play-mode.ts +2553 -0
- package/src/play/play-recording.ts +335 -0
- package/src/play/react-play-live-authoring.ts +165 -0
- package/src/play-bar/PlayBar.tsx +488 -0
- package/src/play-bar/PlayerCountPicker.tsx +100 -0
- package/src/profiler/FrameDebuggerPanel.tsx +458 -0
- package/src/profiler/PerformancePanel.tsx +1068 -0
- package/src/profiler/ProfilerPanel.tsx +53 -0
- package/src/profiler/frame-debugger-store.ts +97 -0
- package/src/profiler/main-thread-busy.ts +90 -0
- package/src/react/design-time-react-mount.ts +782 -0
- package/src/react/dom-authoring-adapter.ts +764 -0
- package/src/react/pasteboard-materialize.ts +154 -0
- package/src/react/react-inspector-section.tsx +2496 -0
- package/src/react/react-world-authoring-adapter.ts +3795 -0
- package/src/react/story-documents/story-args-section.ts +19 -0
- package/src/react/story-documents/story-documents.tsx +1380 -0
- package/src/react/story-paint-bounds.ts +94 -0
- package/src/react/ui-board-document.tsx +101 -0
- package/src/react/ui-board-title.ts +9 -0
- package/src/react/ui-component-board.ts +72 -0
- package/src/services/audio-pose-guard.ts +81 -0
- package/src/services/game-audio-unlock.ts +48 -0
- package/src/state-watch/StateWatchPanel.tsx +535 -0
- package/src/three/authoring/camera-runtime-inspector-section.tsx +135 -0
- package/src/three/authoring/constraint-inspector-section.tsx +189 -0
- package/src/three/authoring/design-time-renderer.ts +189 -0
- package/src/three/authoring/model-asset-inspector-section.css +41 -0
- package/src/three/authoring/model-asset-inspector-section.tsx +869 -0
- package/src/three/authoring/oid-source-persistence.ts +557 -0
- package/src/three/authoring/r3f-design-session.ts +1174 -0
- package/src/three/authoring/r3f-source-authoring-adapter.ts +5949 -0
- package/src/three/authoring/reflection-probe-inspector-section.tsx +76 -0
- package/src/three/authoring/spatial-audio-handles.ts +215 -0
- package/src/three/authoring/spatial-collider-handles.ts +229 -0
- package/src/three/authoring/spatial-joint-handles.ts +114 -0
- package/src/three/authoring/spatial-light-handles.ts +178 -0
- package/src/three/authoring/spatial-lod-handles.ts +93 -0
- package/src/three/authoring/spatial-particle-handles.ts +368 -0
- package/src/three/authoring/three-authoring-adapter.ts +1953 -0
- package/src/three/authoring/three-scene-identity.ts +19 -0
- package/src/three/authoring/three-spatial-handles.ts +161 -0
- package/src/three/authoring/typed-three-inspector.ts +528 -0
- package/src/three/component-verbs/extract-menu.ts +71 -0
- package/src/three/component-verbs/fork-menu.ts +78 -0
- package/src/three/component-verbs/internals-menu.ts +91 -0
- package/src/three/story-documents/three-story-documents.tsx +607 -0
- package/src/three/three-board/ThreeBoardDocument.tsx +888 -0
- package/src/three/three-board/board-framing.ts +412 -0
- package/src/three/three-board/board-layout.ts +401 -0
- package/src/three/three-board/board-scene.ts +901 -0
- package/src/three/three-board/three-component-board.ts +62 -0
- package/src/xstate/XStateBehaviorSection.tsx +130 -0
- package/src/xstate/XStateMachineInspector.tsx +566 -0
- package/src/xstate/character-animation-machine.fixture.ts +74 -0
- package/src/xstate/live-behaviors.ts +106 -0
- package/src/xstate/use-live-actor-state.ts +44 -0
- package/src/xstate/xstate-graph.ts +235 -0
- package/src/xstate/xstate-layout.ts +76 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audio debugger tab (W3c, F3) — the read-only introspection surface over
|
|
3
|
+
* `SystemAdapters.AudioAdapter`:
|
|
4
|
+
*
|
|
5
|
+
* - active-node GRAPH (live `graphSnapshot()` poll — engine buses + every
|
|
6
|
+
* Tone node the game routed into the master bus);
|
|
7
|
+
* - TRANSPORT strip (`transportState()`: state/position/bpm; `null` renders
|
|
8
|
+
* the honest "no musical transport in this world");
|
|
9
|
+
* - per-bus METERS (`acquireMeters()` handle, held only while this panel is
|
|
10
|
+
* mounted with a live adapter and ALWAYS disposed — level updates write
|
|
11
|
+
* straight to DOM style via refs on a 100 ms interval, so the node
|
|
12
|
+
* tree/log never re-render at meter rate);
|
|
13
|
+
* - EVENT LOG (`audioEvents` seq-fenced ring → `AudioEventLogModel`;
|
|
14
|
+
* pausable).
|
|
15
|
+
*
|
|
16
|
+
* Degradation ladder rendered honestly (W3a contract): no adapter means the
|
|
17
|
+
* editor has not mounted a project audio graph yet; an adapter
|
|
18
|
+
* missing an optional capability marks that section "not provided"; nothing
|
|
19
|
+
* is ever fabricated. Editor code never imports Tone here — only the
|
|
20
|
+
* `AudioAdapter` interface (the networking rule's audio mirror). The full
|
|
21
|
+
* MIXER (sends/effects/gain editing) is SQ-4-gated and deliberately absent.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { editorHost } from '@volter/editor-sdk/host';
|
|
25
|
+
import { Button, fontSizeVar, spaceVar, themeVars } from '@volter/editor-sdk/widgets';
|
|
26
|
+
import { meterPercent } from '@volter/game-runtime/adapter/audio-meter';
|
|
27
|
+
import type { AudioAdapter, AudioDebugEvent, AudioMeterFrame } from '@volter/editor-project/adapter';
|
|
28
|
+
import { useEffect, useReducer, useRef, useState } from 'react';
|
|
29
|
+
import {
|
|
30
|
+
AudioEventLogModel,
|
|
31
|
+
deriveAudioCapabilities,
|
|
32
|
+
formatTransportSeconds,
|
|
33
|
+
} from './audio-debugger-model';
|
|
34
|
+
|
|
35
|
+
const MONO: React.CSSProperties = {
|
|
36
|
+
fontFamily: themeVars.typography.mono,
|
|
37
|
+
fontSize: fontSizeVar.base,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function AbsentNote({ children, testId }: { children: React.ReactNode; testId?: string }) {
|
|
41
|
+
return (
|
|
42
|
+
<div
|
|
43
|
+
data-testid={testId}
|
|
44
|
+
style={{
|
|
45
|
+
padding: spaceVar[4],
|
|
46
|
+
color: themeVars.content.muted,
|
|
47
|
+
fontSize: fontSizeVar.base,
|
|
48
|
+
fontStyle: 'italic',
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
{children}
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function ResumeAudioButton({
|
|
57
|
+
adapter,
|
|
58
|
+
onResumed,
|
|
59
|
+
}: {
|
|
60
|
+
adapter: AudioAdapter;
|
|
61
|
+
onResumed: () => void;
|
|
62
|
+
}) {
|
|
63
|
+
if (!adapter.resume) return null;
|
|
64
|
+
return (
|
|
65
|
+
<Button
|
|
66
|
+
type="button"
|
|
67
|
+
variant="secondary"
|
|
68
|
+
data-testid="audio-resume"
|
|
69
|
+
onClick={() => {
|
|
70
|
+
adapter.resume?.();
|
|
71
|
+
onResumed();
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
Enable Audio
|
|
75
|
+
</Button>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// --- Transport strip --------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
const TRANSPORT_COLORS: Record<string, string> = {
|
|
82
|
+
started: themeVars.semantic.success,
|
|
83
|
+
paused: themeVars.semantic.warning,
|
|
84
|
+
stopped: themeVars.content.muted,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
function TransportStrip({ adapter }: { adapter: AudioAdapter }) {
|
|
88
|
+
const transport = adapter.transportState?.() ?? null;
|
|
89
|
+
if (!transport) {
|
|
90
|
+
return (
|
|
91
|
+
<AbsentNote testId="audio-transport-none">
|
|
92
|
+
No musical transport in this world (the game has not bridged Tone onto its audio context).
|
|
93
|
+
</AbsentNote>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return (
|
|
97
|
+
<div
|
|
98
|
+
data-testid="audio-transport"
|
|
99
|
+
data-state={transport.state}
|
|
100
|
+
style={{
|
|
101
|
+
display: 'flex',
|
|
102
|
+
gap: spaceVar[6],
|
|
103
|
+
alignItems: 'center',
|
|
104
|
+
padding: `${spaceVar[2]} ${spaceVar[4]}`,
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
<span style={{ color: TRANSPORT_COLORS[transport.state] ?? themeVars.content.primary }}>
|
|
108
|
+
● {transport.state}
|
|
109
|
+
</span>
|
|
110
|
+
<span
|
|
111
|
+
data-testid="audio-transport-seconds"
|
|
112
|
+
style={{ ...MONO, color: themeVars.content.primary }}
|
|
113
|
+
>
|
|
114
|
+
{formatTransportSeconds(transport.seconds)}
|
|
115
|
+
</span>
|
|
116
|
+
{transport.position !== undefined && (
|
|
117
|
+
<span style={{ ...MONO, color: themeVars.content.muted }}>{transport.position}</span>
|
|
118
|
+
)}
|
|
119
|
+
<span style={{ fontSize: fontSizeVar.sm, color: themeVars.content.muted }}>
|
|
120
|
+
bpm{' '}
|
|
121
|
+
<span style={{ ...MONO, color: themeVars.content.primary }}>
|
|
122
|
+
{transport.bpm.toFixed(0)}
|
|
123
|
+
</span>
|
|
124
|
+
</span>
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// --- Meters -----------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Owns one metering session for `adapter`'s lifetime in this panel: acquires
|
|
133
|
+
* the handle on mount / adapter change, disposes it on cleanup (Stop tears
|
|
134
|
+
* the whole panel state down through the adapter-identity poll, so no
|
|
135
|
+
* analyser tap survives a session). Level updates bypass React entirely —
|
|
136
|
+
* a 100 ms interval writes bar width/`data-level` straight onto the DOM.
|
|
137
|
+
*/
|
|
138
|
+
function MeterBars({ adapter }: { adapter: AudioAdapter }) {
|
|
139
|
+
const [buses, setBuses] = useState<AudioMeterFrame[] | null | undefined>(undefined);
|
|
140
|
+
const fillRefs = useRef(new Map<string, HTMLDivElement>());
|
|
141
|
+
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
const handle = adapter.acquireMeters?.() ?? null;
|
|
144
|
+
if (!handle) {
|
|
145
|
+
setBuses(null);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
setBuses(handle.read()); // bus ids/labels — the one React render
|
|
149
|
+
const id = setInterval(() => {
|
|
150
|
+
for (const frame of handle.read()) {
|
|
151
|
+
const el = fillRefs.current.get(frame.id);
|
|
152
|
+
if (!el) continue;
|
|
153
|
+
el.style.width = `${meterPercent(frame.level)}%`;
|
|
154
|
+
el.dataset['level'] = frame.level.toFixed(4);
|
|
155
|
+
}
|
|
156
|
+
}, 100);
|
|
157
|
+
return () => {
|
|
158
|
+
clearInterval(id);
|
|
159
|
+
handle.dispose();
|
|
160
|
+
};
|
|
161
|
+
}, [adapter]);
|
|
162
|
+
|
|
163
|
+
if (buses === undefined) return null; // first effect pass hasn't run yet
|
|
164
|
+
if (buses === null) {
|
|
165
|
+
return (
|
|
166
|
+
<AbsentNote testId="audio-meters-unavailable">
|
|
167
|
+
Metering unavailable in this environment (no real audio context).
|
|
168
|
+
</AbsentNote>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
return (
|
|
172
|
+
<div
|
|
173
|
+
data-testid="audio-meters"
|
|
174
|
+
style={{
|
|
175
|
+
display: 'flex',
|
|
176
|
+
gap: 14,
|
|
177
|
+
alignItems: 'center',
|
|
178
|
+
padding: `${spaceVar[2]} ${spaceVar[4]}`,
|
|
179
|
+
}}
|
|
180
|
+
>
|
|
181
|
+
{buses.map((bus) => (
|
|
182
|
+
<div key={bus.id} style={{ display: 'flex', alignItems: 'center', gap: spaceVar[2] }}>
|
|
183
|
+
<span style={{ fontSize: fontSizeVar.sm, color: themeVars.content.muted }}>
|
|
184
|
+
{bus.label}
|
|
185
|
+
</span>
|
|
186
|
+
<div
|
|
187
|
+
style={{
|
|
188
|
+
width: 96,
|
|
189
|
+
height: 8,
|
|
190
|
+
background: themeVars.surface.inset,
|
|
191
|
+
borderRadius: themeVars.shape.small,
|
|
192
|
+
overflow: 'hidden',
|
|
193
|
+
}}
|
|
194
|
+
>
|
|
195
|
+
<div
|
|
196
|
+
data-testid="audio-meter-fill"
|
|
197
|
+
data-meter-id={bus.id}
|
|
198
|
+
data-level="0.0000"
|
|
199
|
+
ref={(el) => {
|
|
200
|
+
if (el) fillRefs.current.set(bus.id, el);
|
|
201
|
+
else fillRefs.current.delete(bus.id);
|
|
202
|
+
}}
|
|
203
|
+
style={{
|
|
204
|
+
width: '0%',
|
|
205
|
+
height: '100%',
|
|
206
|
+
background: themeVars.semantic.success,
|
|
207
|
+
transition: 'width 80ms linear',
|
|
208
|
+
}}
|
|
209
|
+
/>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
))}
|
|
213
|
+
</div>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// --- Node graph -------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
function NodeRow({
|
|
220
|
+
node,
|
|
221
|
+
}: {
|
|
222
|
+
node: { id: string; type: string; label?: string; outputs: string[]; state?: string };
|
|
223
|
+
}) {
|
|
224
|
+
return (
|
|
225
|
+
<div
|
|
226
|
+
data-testid="audio-node-row"
|
|
227
|
+
data-node-id={node.id}
|
|
228
|
+
style={{ ...MONO, lineHeight: '18px', padding: `0 ${spaceVar[4]}`, whiteSpace: 'nowrap' }}
|
|
229
|
+
>
|
|
230
|
+
<span style={{ color: themeVars.content.primary }}>{node.label ?? node.id}</span>
|
|
231
|
+
<span style={{ color: themeVars.content.dim }}> {node.type}</span>
|
|
232
|
+
{node.state !== undefined && (
|
|
233
|
+
<span style={{ color: themeVars.semantic.warning }}> [{node.state}]</span>
|
|
234
|
+
)}
|
|
235
|
+
{node.outputs.length > 0 && (
|
|
236
|
+
<span style={{ color: themeVars.content.muted }}> → {node.outputs.join(', ')}</span>
|
|
237
|
+
)}
|
|
238
|
+
</div>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// --- Event log --------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
function eventColor(kind: AudioDebugEvent['kind']): string {
|
|
245
|
+
if (kind === 'error') return themeVars.semantic.danger;
|
|
246
|
+
if (kind === 'mute' || kind === 'unmute') return themeVars.semantic.warning;
|
|
247
|
+
return themeVars.accent.default;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function EventRow({ event }: { event: AudioDebugEvent }) {
|
|
251
|
+
return (
|
|
252
|
+
<div
|
|
253
|
+
data-testid="audio-log-row"
|
|
254
|
+
data-kind={event.kind}
|
|
255
|
+
style={{
|
|
256
|
+
...MONO,
|
|
257
|
+
display: 'grid',
|
|
258
|
+
gridTemplateColumns: '64px 1fr',
|
|
259
|
+
gap: spaceVar[2],
|
|
260
|
+
lineHeight: '17px',
|
|
261
|
+
padding: `0 ${spaceVar[4]}`,
|
|
262
|
+
}}
|
|
263
|
+
>
|
|
264
|
+
<span style={{ color: themeVars.content.dim }}>{(event.time / 1000).toFixed(2)}s</span>
|
|
265
|
+
<span style={{ color: eventColor(event.kind) }}>
|
|
266
|
+
{event.kind}
|
|
267
|
+
{event.detail !== undefined ? (
|
|
268
|
+
<span style={{ color: themeVars.content.muted }}> {event.detail}</span>
|
|
269
|
+
) : null}
|
|
270
|
+
</span>
|
|
271
|
+
</div>
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// --- The panel --------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
export function AudioDebuggerPanel() {
|
|
278
|
+
const [, force] = useReducer((c: number) => c + 1, 0);
|
|
279
|
+
const logRef = useRef(new AudioEventLogModel());
|
|
280
|
+
const adapterRef = useRef<AudioAdapter | null>(null);
|
|
281
|
+
const [paused, setPaused] = useState(false);
|
|
282
|
+
|
|
283
|
+
useEffect(() => {
|
|
284
|
+
const syncAdapter = () => {
|
|
285
|
+
const adapter = editorHost().systems.inspectedAudio() ?? null;
|
|
286
|
+
if (adapter !== adapterRef.current) {
|
|
287
|
+
// Session started/stopped (or a different game mounted): the old
|
|
288
|
+
// session's event log is stale, and MeterBars' `adapter`-keyed effect
|
|
289
|
+
// re-acquires/disposes its handle off this same identity change.
|
|
290
|
+
adapterRef.current = adapter;
|
|
291
|
+
logRef.current = new AudioEventLogModel();
|
|
292
|
+
logRef.current.paused = paused;
|
|
293
|
+
}
|
|
294
|
+
return adapter;
|
|
295
|
+
};
|
|
296
|
+
// 250 ms consume/render poll — the NetworkInspectorPanel idiom; also how
|
|
297
|
+
// the panel notices play-mode start/stop (adapter identity).
|
|
298
|
+
const tick = () => {
|
|
299
|
+
logRef.current.pull(syncAdapter());
|
|
300
|
+
force();
|
|
301
|
+
};
|
|
302
|
+
tick();
|
|
303
|
+
const pollId = setInterval(tick, 250);
|
|
304
|
+
return () => clearInterval(pollId);
|
|
305
|
+
}, [paused]);
|
|
306
|
+
|
|
307
|
+
const adapter = adapterRef.current ?? editorHost().systems.inspectedAudio() ?? null;
|
|
308
|
+
const caps = deriveAudioCapabilities(adapter);
|
|
309
|
+
|
|
310
|
+
if (!adapter || !caps) {
|
|
311
|
+
return (
|
|
312
|
+
<div
|
|
313
|
+
data-testid="audio-debugger-empty"
|
|
314
|
+
style={{
|
|
315
|
+
padding: spaceVar[8],
|
|
316
|
+
color: themeVars.content.muted,
|
|
317
|
+
fontSize: fontSizeVar.md,
|
|
318
|
+
lineHeight: '18px',
|
|
319
|
+
}}
|
|
320
|
+
>
|
|
321
|
+
<div style={{ color: themeVars.content.primary, marginBottom: spaceVar[2] }}>
|
|
322
|
+
Audio is still initializing.
|
|
323
|
+
</div>
|
|
324
|
+
The editor registers a first-party <code style={MONO}>AudioAdapter</code> in edit mode; Play
|
|
325
|
+
temporarily replaces it with the running game's adapter.
|
|
326
|
+
</div>
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const nodes = caps.graph ? (adapter.graphSnapshot?.() ?? []) : null;
|
|
331
|
+
const events = logRef.current.all;
|
|
332
|
+
|
|
333
|
+
return (
|
|
334
|
+
<div
|
|
335
|
+
data-testid="audio-debugger"
|
|
336
|
+
style={{
|
|
337
|
+
flex: 1,
|
|
338
|
+
minHeight: 0,
|
|
339
|
+
display: 'flex',
|
|
340
|
+
flexDirection: 'column',
|
|
341
|
+
fontSize: fontSizeVar.md,
|
|
342
|
+
}}
|
|
343
|
+
>
|
|
344
|
+
{/* Header: mute state + transport strip + meters */}
|
|
345
|
+
<div
|
|
346
|
+
style={{
|
|
347
|
+
display: 'flex',
|
|
348
|
+
flexWrap: 'wrap',
|
|
349
|
+
gap: spaceVar[4],
|
|
350
|
+
alignItems: 'center',
|
|
351
|
+
borderBottom: `1px solid ${themeVars.boundary.default}`,
|
|
352
|
+
}}
|
|
353
|
+
>
|
|
354
|
+
<ResumeAudioButton adapter={adapter} onResumed={force} />
|
|
355
|
+
<span
|
|
356
|
+
data-testid="audio-mute-state"
|
|
357
|
+
style={{
|
|
358
|
+
padding: `${spaceVar[2]} ${spaceVar[4]}`,
|
|
359
|
+
color: adapter.isMuted() ? themeVars.semantic.warning : themeVars.semantic.success,
|
|
360
|
+
}}
|
|
361
|
+
>
|
|
362
|
+
{adapter.isMuted() ? 'muted' : 'audible'}
|
|
363
|
+
</span>
|
|
364
|
+
{caps.transport ? (
|
|
365
|
+
<TransportStrip adapter={adapter} />
|
|
366
|
+
) : (
|
|
367
|
+
<AbsentNote testId="audio-transport-absent">
|
|
368
|
+
Transport not provided by this adapter.
|
|
369
|
+
</AbsentNote>
|
|
370
|
+
)}
|
|
371
|
+
{caps.meters ? (
|
|
372
|
+
<MeterBars adapter={adapter} />
|
|
373
|
+
) : (
|
|
374
|
+
<AbsentNote testId="audio-meters-absent">Meters not provided by this adapter.</AbsentNote>
|
|
375
|
+
)}
|
|
376
|
+
</div>
|
|
377
|
+
|
|
378
|
+
{/* Body: node graph | event log */}
|
|
379
|
+
<div style={{ flex: 1, minHeight: 0, display: 'flex' }}>
|
|
380
|
+
<div
|
|
381
|
+
data-testid="audio-graph"
|
|
382
|
+
style={{
|
|
383
|
+
flex: 1,
|
|
384
|
+
minWidth: 0,
|
|
385
|
+
overflow: 'auto',
|
|
386
|
+
borderRight: `1px solid ${themeVars.boundary.default}`,
|
|
387
|
+
padding: `${spaceVar[2]} 0`,
|
|
388
|
+
}}
|
|
389
|
+
>
|
|
390
|
+
{nodes ? (
|
|
391
|
+
nodes.map((node) => <NodeRow key={node.id} node={node} />)
|
|
392
|
+
) : (
|
|
393
|
+
<AbsentNote testId="audio-graph-absent">
|
|
394
|
+
Audio graph not provided by this adapter.
|
|
395
|
+
</AbsentNote>
|
|
396
|
+
)}
|
|
397
|
+
</div>
|
|
398
|
+
|
|
399
|
+
<div
|
|
400
|
+
data-testid="audio-event-log"
|
|
401
|
+
style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column' }}
|
|
402
|
+
>
|
|
403
|
+
{caps.events ? (
|
|
404
|
+
<>
|
|
405
|
+
<div
|
|
406
|
+
style={{
|
|
407
|
+
display: 'flex',
|
|
408
|
+
gap: spaceVar[3],
|
|
409
|
+
alignItems: 'center',
|
|
410
|
+
padding: spaceVar[2],
|
|
411
|
+
}}
|
|
412
|
+
>
|
|
413
|
+
<Button
|
|
414
|
+
type="button"
|
|
415
|
+
variant="ghost"
|
|
416
|
+
data-testid="audio-log-pause"
|
|
417
|
+
onClick={() => {
|
|
418
|
+
const next = !paused;
|
|
419
|
+
logRef.current.paused = next;
|
|
420
|
+
setPaused(next);
|
|
421
|
+
}}
|
|
422
|
+
>
|
|
423
|
+
{paused ? 'Resume' : 'Pause'}
|
|
424
|
+
</Button>
|
|
425
|
+
<Button
|
|
426
|
+
type="button"
|
|
427
|
+
variant="ghost"
|
|
428
|
+
data-testid="audio-log-clear"
|
|
429
|
+
onClick={() => {
|
|
430
|
+
logRef.current.clear();
|
|
431
|
+
force();
|
|
432
|
+
}}
|
|
433
|
+
>
|
|
434
|
+
Clear
|
|
435
|
+
</Button>
|
|
436
|
+
<span style={{ fontSize: fontSizeVar.sm, color: themeVars.content.muted }}>
|
|
437
|
+
{events.length} events
|
|
438
|
+
</span>
|
|
439
|
+
</div>
|
|
440
|
+
<div style={{ flex: 1, minHeight: 0, overflow: 'auto' }}>
|
|
441
|
+
{events.length === 0 ? (
|
|
442
|
+
<AbsentNote>No audio events yet.</AbsentNote>
|
|
443
|
+
) : (
|
|
444
|
+
events.map((event) => <EventRow key={event.seq} event={event} />)
|
|
445
|
+
)}
|
|
446
|
+
</div>
|
|
447
|
+
</>
|
|
448
|
+
) : (
|
|
449
|
+
<AbsentNote testId="audio-log-absent">
|
|
450
|
+
Event log not provided by this adapter.
|
|
451
|
+
</AbsentNote>
|
|
452
|
+
)}
|
|
453
|
+
</div>
|
|
454
|
+
</div>
|
|
455
|
+
</div>
|
|
456
|
+
);
|
|
457
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure data-path logic for the Audio debugger tab (W3c) — no React/DOM, so
|
|
3
|
+
* the consumer side of the `AudioAdapter` introspection capabilities is
|
|
4
|
+
* headless-testable. The rendering component (`AudioDebuggerPanel.tsx`) owns
|
|
5
|
+
* only JSX; capability detection, the seq-fenced event-log ring with pause,
|
|
6
|
+
* and the meter geometry math live here (the W3b
|
|
7
|
+
* `network-inspector-model.ts` split, applied to audio).
|
|
8
|
+
*
|
|
9
|
+
* Editor code never imports Tone — only the `AudioAdapter` interface (the
|
|
10
|
+
* networking rule's audio mirror). Read-only debugger: the full mixer is
|
|
11
|
+
* SQ-4-gated and has no surface here.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { AudioAdapter, AudioDebugEvent } from '@volter/editor-project/adapter';
|
|
15
|
+
|
|
16
|
+
/** Which optional introspection capabilities the active adapter provides —
|
|
17
|
+
* the degradation ladder's per-section verdict. `null` input (no adapter =
|
|
18
|
+
* no running session) stays `null`: the panel renders the honest no-session
|
|
19
|
+
* notice instead of a fabricated all-false map. Mute (`setMuted`/`isMuted`)
|
|
20
|
+
* is a REQUIRED `AudioAdapter` member, so it needs no entry. */
|
|
21
|
+
export interface AudioCapabilityMap {
|
|
22
|
+
graph: boolean;
|
|
23
|
+
transport: boolean;
|
|
24
|
+
meters: boolean;
|
|
25
|
+
events: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function deriveAudioCapabilities(
|
|
29
|
+
adapter: AudioAdapter | null | undefined,
|
|
30
|
+
): AudioCapabilityMap | null {
|
|
31
|
+
if (!adapter) return null;
|
|
32
|
+
return {
|
|
33
|
+
graph: typeof adapter.graphSnapshot === 'function',
|
|
34
|
+
transport: typeof adapter.transportState === 'function',
|
|
35
|
+
meters: typeof adapter.acquireMeters === 'function',
|
|
36
|
+
events: typeof adapter.audioEvents === 'function',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Consumer-side event log over `AudioAdapter.audioEvents` — the
|
|
42
|
+
* `MessageLogModel` idiom: pulls seq-fenced events from the adapter's ring
|
|
43
|
+
* into a bounded local ring. PAUSE stops consuming with the fence frozen, so
|
|
44
|
+
* resume picks up whatever the adapter's ring still holds (a long pause
|
|
45
|
+
* loses only what the adapter itself evicted; nothing fills the gap).
|
|
46
|
+
*/
|
|
47
|
+
export class AudioEventLogModel {
|
|
48
|
+
private entries: AudioDebugEvent[] = [];
|
|
49
|
+
private lastSeq = 0;
|
|
50
|
+
paused = false;
|
|
51
|
+
|
|
52
|
+
constructor(private readonly capacity = 500) {}
|
|
53
|
+
|
|
54
|
+
/** Consume new events (`seq > fence`). Returns how many were appended
|
|
55
|
+
* (0 while paused or when the capability is absent). */
|
|
56
|
+
pull(adapter: AudioAdapter | null | undefined): number {
|
|
57
|
+
if (this.paused || !adapter?.audioEvents) return 0;
|
|
58
|
+
const fresh = adapter.audioEvents(this.lastSeq);
|
|
59
|
+
if (fresh.length === 0) return 0;
|
|
60
|
+
for (const event of fresh) {
|
|
61
|
+
this.entries.push(event);
|
|
62
|
+
if (event.seq > this.lastSeq) this.lastSeq = event.seq;
|
|
63
|
+
}
|
|
64
|
+
if (this.entries.length > this.capacity) {
|
|
65
|
+
this.entries.splice(0, this.entries.length - this.capacity);
|
|
66
|
+
}
|
|
67
|
+
return fresh.length;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get all(): readonly AudioDebugEvent[] {
|
|
71
|
+
return this.entries;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
clear(): void {
|
|
75
|
+
// Fence deliberately NOT reset: clear empties the view; already-observed
|
|
76
|
+
// events don't come back.
|
|
77
|
+
this.entries = [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Transport seconds → `m:ss.t` (e.g. `1:03.4`) for the transport strip. */
|
|
82
|
+
export function formatTransportSeconds(seconds: number): string {
|
|
83
|
+
const clamped = Math.max(0, seconds);
|
|
84
|
+
const minutes = Math.floor(clamped / 60);
|
|
85
|
+
const rest = clamped - minutes * 60;
|
|
86
|
+
return `${minutes}:${rest < 10 ? '0' : ''}${rest.toFixed(1)}`;
|
|
87
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `bridge-call` — the handler behind the verb, moved out of the editor's
|
|
3
|
+
* `command-listener.ts` with it (WORK.md §The workbench). The method table it
|
|
4
|
+
* dispatches through is `./dispatch.ts`; play mode, the ingest latch and the
|
|
5
|
+
* editor console are reached through the `@editor/*` alias until those
|
|
6
|
+
* modules move too. Nothing here is host API.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { EditorCommandMessage, EditorCommandResult } from '@volter/editor-sdk/commands';
|
|
10
|
+
import { notPlayingResult, structuredErrorResult } from '../command-results';
|
|
11
|
+
import { isIngestActive } from '../ingest/active-ingest';
|
|
12
|
+
import { isPlayModeActive } from '../play/play-mode';
|
|
13
|
+
import { dispatchBridgeMethod, hasLiveDebugPlane } from './dispatch';
|
|
14
|
+
|
|
15
|
+
/** `bridge-call` relay op handler — an unreachable debug seam is the one
|
|
16
|
+
* precondition checked up front (mirrors every other debug-seam verb of
|
|
17
|
+
* this package); everything else is `dispatchBridgeMethod`'s own structured
|
|
18
|
+
* failure, caught here and shipped back via `structuredErrorResult` so
|
|
19
|
+
* `code`/`data` survive the relay round trip byte-equivalent to the
|
|
20
|
+
* page-transport's `unwrap()` path (`packages/vgai-live/src/game-client/client.ts`). */
|
|
21
|
+
export async function handleBridgeCall(cmd: EditorCommandMessage): Promise<EditorCommandResult> {
|
|
22
|
+
// The precondition is "no plane at all", not "not playing" — see
|
|
23
|
+
// `hasLiveDebugPlane`. The per-method preconditions inside
|
|
24
|
+
// `dispatchBridgeMethod` are unchanged and still refuse honestly: the
|
|
25
|
+
// run-shaped methods (`runTicks`, `input.*`, `holdFor`) resolve through
|
|
26
|
+
// `getPlayRuntimeAccess()`, which is null outside play, so they answer
|
|
27
|
+
// `RUN_TICKS_UNAVAILABLE`/`DEBUG_INPUT_UNAVAILABLE` rather than pretending
|
|
28
|
+
// the still edit world can be driven.
|
|
29
|
+
if (!isPlayModeActive() && !hasLiveDebugPlane()) {
|
|
30
|
+
// A LIVE INGEST that publishes no plane is a third state, and answering it
|
|
31
|
+
// with "not in play mode" sends the reader to `vgai play` over a game that
|
|
32
|
+
// is already running and visible. MEASURED on the `flappy` canvas ingest:
|
|
33
|
+
// `game.waitSimTime(...)` refused with the play sentence while the mount's
|
|
34
|
+
// own play state read `playing`. The mount is what is missing a plane, so
|
|
35
|
+
// the refusal says so — and deliberately stays a REFUSAL, because the
|
|
36
|
+
// alternative (letting the call through to answer `[]`) would report "this
|
|
37
|
+
// game registered no commands", which is a different and equally false
|
|
38
|
+
// claim.
|
|
39
|
+
return isIngestActive()
|
|
40
|
+
? {
|
|
41
|
+
ok: false,
|
|
42
|
+
error:
|
|
43
|
+
'this ingest mount is live, but it publishes no debug plane — the mount installs no ' +
|
|
44
|
+
'SystemAdapters.debug, so there is nothing to enumerate, read or drive through the ' +
|
|
45
|
+
'game seam. Starting play will not change that; the game itself must declare the ' +
|
|
46
|
+
'vgai game contract (or carry a contract shim beside it) for these calls to have an ' +
|
|
47
|
+
'answer.',
|
|
48
|
+
}
|
|
49
|
+
: notPlayingResult();
|
|
50
|
+
}
|
|
51
|
+
const method = cmd['method'] as string;
|
|
52
|
+
const callArgs = (cmd['callArgs'] as unknown[] | undefined) ?? [];
|
|
53
|
+
try {
|
|
54
|
+
const instance = cmd['instance'] as string | undefined;
|
|
55
|
+
const result = await dispatchBridgeMethod(method, callArgs, instance);
|
|
56
|
+
return { ok: true, data: { result } };
|
|
57
|
+
} catch (err) {
|
|
58
|
+
return structuredErrorResult(err);
|
|
59
|
+
}
|
|
60
|
+
}
|