@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.
Files changed (236) hide show
  1. package/contributions/audio-unlock.service.ts +2 -2
  2. package/contributions/autoplay.service.ts +1 -1
  3. package/contributions/bridge.command.ts +3 -3
  4. package/contributions/canvas/component-board.service.ts +16 -0
  5. package/contributions/canvas/design-time-mount.service.ts +45 -0
  6. package/contributions/canvas-story-capture.service.ts +12 -0
  7. package/contributions/edit-mode-audio.service.ts +2 -2
  8. package/contributions/edit-mode-networking.service.ts +1 -1
  9. package/contributions/gameplay.command.ts +4 -4
  10. package/contributions/generation.service.ts +1 -1
  11. package/contributions/godot.style.ts +26 -6
  12. package/contributions/godot.view.ts +6 -0
  13. package/contributions/ingest.service.ts +2 -2
  14. package/contributions/instances.command.ts +1 -1
  15. package/contributions/navmesh.menu.ts +1 -1
  16. package/contributions/network-observer.service.ts +14 -0
  17. package/contributions/play.command.ts +10 -4
  18. package/contributions/react/component-board.service.ts +1 -1
  19. package/contributions/react/design-time-mount.service.ts +1 -1
  20. package/contributions/scene-document.service.ts +3 -3
  21. package/contributions/state-watch.menu.ts +1 -1
  22. package/contributions/state-watch.utility.tsx +1 -1
  23. package/contributions/team-playtest.service.ts +4 -4
  24. package/contributions/three/component-board.service.ts +1 -1
  25. package/contributions/three/component-verbs.command.ts +6 -6
  26. package/contributions/three/three-authoring.service.ts +8 -5
  27. package/contributions/three-story-capture.service.ts +12 -0
  28. package/contributions/unity.style.ts +17 -7
  29. package/contributions/unity.view.ts +6 -0
  30. package/contributions/unreal.style.ts +10 -2
  31. package/contributions/unreal.view.ts +5 -0
  32. package/package.json +20 -10
  33. package/src/asset-budget/AssetBudgetPanel.tsx +1 -1
  34. package/src/asset-budget/asset-budget-model.ts +1 -1
  35. package/src/audio/AudioDebuggerPanel.tsx +1 -1
  36. package/src/bridge/dispatch.ts +14 -14
  37. package/src/bridge/live-frames.ts +1 -1
  38. package/src/bridge/screenshot.ts +3 -3
  39. package/src/build/BuildProfilesPanel.tsx +3 -3
  40. package/src/canvas/canvas-board/CanvasBoardDocument.tsx +676 -0
  41. package/src/canvas/canvas-board/canvas-board-model.ts +407 -0
  42. package/src/canvas/canvas-board/canvas-component-board.ts +56 -0
  43. package/src/canvas/canvas-design-mount.ts +524 -0
  44. package/src/canvas/design-time-canvas-mount.ts +79 -0
  45. package/src/coverage/live-authoring-surface.ts +5 -5
  46. package/src/coverage/live-project-verbs.ts +1 -1
  47. package/src/coverage/native-system-coverage.ts +5 -5
  48. package/src/coverage/root-coverage.ts +1 -1
  49. package/src/coverage/session-coverage.ts +3 -3
  50. package/src/design-system-stories/ApplicationChrome.stories.tsx +5 -5
  51. package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +7 -7
  52. package/src/edit-mode/edit-mode-audio.ts +4 -4
  53. package/src/edit-mode/edit-mode-networking.ts +2 -2
  54. package/src/game-document/GameCaptureFrameButton.tsx +1 -1
  55. package/src/game-document/GameDocument.tsx +2 -2
  56. package/src/game-document/GamePanel.tsx +4 -4
  57. package/src/game-document/InstanceInspectorPicker.tsx +2 -2
  58. package/src/game-document/crowd-debug.ts +1 -1
  59. package/src/game-document/device-preview.ts +10 -11
  60. package/src/game-document/physics-debug.ts +1 -1
  61. package/src/generation/GenerationActivity.tsx +3 -3
  62. package/src/generation/generation-documents.tsx +5 -5
  63. package/src/generation/generation-jobs.ts +1 -1
  64. package/src/host/adapter-runtime-bindings.ts +96 -5
  65. package/src/host/authoring/babylon-authoring-adapter.ts +6 -6
  66. package/src/host/authoring/gesture-persist.ts +1 -1
  67. package/src/host/authoring/ingest-data-writer.ts +1 -1
  68. package/src/host/authoring/ingest-source-persistence.ts +4 -4
  69. package/src/host/authoring/mounted-authoring.ts +4 -4
  70. package/src/host/authoring/phaser-live-authoring-adapter.ts +4 -4
  71. package/src/host/authoring/pixi-authoring-adapter.ts +40 -15
  72. package/src/host/authoring/pixi-creation-site-write-target.ts +2 -2
  73. package/src/host/authoring/pixi-live-write-target.ts +4 -4
  74. package/src/host/authoring/pixi-source-identity.ts +3 -3
  75. package/src/host/authoring/pixi-source-write-target.ts +1614 -0
  76. package/src/host/authoring/pixi-still-presentation.ts +1 -1
  77. package/src/host/authoring/pixi-structure-history.ts +2 -2
  78. package/src/host/authoring/pixi-transform-channels.ts +16 -14
  79. package/src/host/authoring/source-persistence-backend.ts +3 -3
  80. package/src/host/authoring/struct-write-pipe.ts +1 -1
  81. package/src/host/binding-resolver.ts +8 -9
  82. package/src/host/browser-transpile.ts +1 -1
  83. package/src/host/canvas-entry-runtime.ts +58 -47
  84. package/src/host/canvas-preview-frames.ts +482 -0
  85. package/src/host/components/CameraAuthoringOverlay.tsx +1 -1
  86. package/src/host/components/HeaderTelemetry.tsx +4 -4
  87. package/src/host/components/PixiIsolationSceneContent.tsx +10 -10
  88. package/src/host/components/ThreeIsolationSceneContent.tsx +3 -3
  89. package/src/host/components/frame-debugger-model.ts +2 -2
  90. package/src/host/components/header-telemetry-model.ts +2 -2
  91. package/src/host/components/scene-document.tsx +14 -14
  92. package/src/host/components/utility-view-state.ts +1 -1
  93. package/src/host/components/world-root-stage-binding.tsx +12 -12
  94. package/src/host/components/world-root-stage.ts +70 -49
  95. package/src/host/coverage/system-adapter-coverage.ts +3 -4
  96. package/src/host/design-system-stories/fixtures/editor-runtime.tsx +9 -11
  97. package/src/host/document-preview-three.ts +1 -1
  98. package/src/host/entry-adjudication.ts +6 -6
  99. package/src/host/game-css-scope-transform.ts +4 -0
  100. package/src/host/game-realm-page.ts +1 -1
  101. package/src/host/gameplay-export.ts +25 -14
  102. package/src/host/gameplay-recording.ts +5 -5
  103. package/src/host/gated-globals.ts +2 -2
  104. package/src/host/history/json-history-resource.ts +1 -1
  105. package/src/host/projection/pixi.ts +24 -2
  106. package/src/host/r3f-entry-runtime.ts +65 -34
  107. package/src/host/react-mount-runtime.ts +7 -48
  108. package/src/host/realm-services.ts +1 -1
  109. package/src/host/roots/canvas-root.tsx +361 -0
  110. package/src/host/roots/r3f-root.tsx +473 -0
  111. package/src/host/roots/react-root.ts +9 -43
  112. package/src/host/served-bundle-runtime-modules.ts +1 -19
  113. package/src/host/server-log-bridge.ts +2 -2
  114. package/src/host/stories/mounted-story-viewport-source.ts +1 -1
  115. package/src/host/stories/pixi-story-model.ts +30 -0
  116. package/src/host/stories/story-media-captures.ts +46 -0
  117. package/src/host/stories/story-media-presence.ts +3 -3
  118. package/src/host/stories/story-pixi-preview.ts +408 -0
  119. package/src/host/stories/story-three-preview.ts +806 -0
  120. package/src/host/stories/three-story-captures.ts +35 -0
  121. package/src/host/story-three-preview-runtime.ts +56 -0
  122. package/src/host/use-active-performance-source.ts +2 -2
  123. package/src/host/viewport-pose-memory.ts +1 -1
  124. package/src/host/viewport-root-presentation.ts +6 -5
  125. package/src/ingest/active-ingest.ts +1 -1
  126. package/src/ingest/authoring/ingest-dom-surface-authoring.ts +4 -4
  127. package/src/ingest/authoring/ingest-root-adapter.ts +8 -8
  128. package/src/ingest/deferred-ingest-play.ts +6 -6
  129. package/src/ingest/discovery-public-ingest.ts +2 -2
  130. package/src/ingest/ingest-boot-viewport.ts +2 -2
  131. package/src/ingest/ingest-canvas-scene-document.tsx +8 -8
  132. package/src/ingest/ingest-canvas-scene.ts +3 -3
  133. package/src/ingest/ingest-evidence-hook.ts +1 -1
  134. package/src/ingest/ingest-frame-snapshot.ts +1 -1
  135. package/src/ingest/ingest-play-control.ts +1 -1
  136. package/src/ingest/ingest-render-debug.ts +10 -10
  137. package/src/ingest/ingest-siblings.ts +13 -25
  138. package/src/ingest/module-mode.ts +14 -14
  139. package/src/ingest/mount-canvas-ingest-root.ts +21 -21
  140. package/src/ingest/mount-coverage.ts +2 -2
  141. package/src/ingest/mount-dom-ingest-root.ts +11 -11
  142. package/src/ingest/mount-ingest-root.ts +8 -8
  143. package/src/ingest/mount-three-ingest-root.ts +8 -8
  144. package/src/ingest/resolve-canvas.ts +1 -1
  145. package/src/ingest/served-html-boot.ts +1 -1
  146. package/src/ingest/surface-canvas.ts +1 -1
  147. package/src/ingest/unmount-ingest-root.ts +5 -5
  148. package/src/navmesh/navmesh-handler.ts +24 -16
  149. package/src/network/NetworkInspectorPanel.tsx +449 -14
  150. package/src/network/network-inspector-model.ts +14 -2
  151. package/src/play/play-log-events.ts +1 -1
  152. package/src/play/play-mode.ts +76 -133
  153. package/src/play/play-recording.ts +1 -1
  154. package/src/play/react-play-live-authoring.ts +3 -3
  155. package/src/play/run-selection.ts +93 -0
  156. package/src/play-bar/PlayBar.tsx +20 -39
  157. package/src/profiler/FrameDebuggerPanel.tsx +1 -1
  158. package/src/profiler/PerformancePanel.tsx +3 -3
  159. package/src/react/design-time-react-mount.ts +23 -65
  160. package/src/react/dom-authoring-adapter.ts +9 -9
  161. package/src/react/react-inspector-section.tsx +5 -5
  162. package/src/react/react-world-authoring-adapter.ts +11 -11
  163. package/src/react/story-documents/story-documents.tsx +9 -9
  164. package/src/react/ui-board-document.tsx +7 -7
  165. package/src/react/ui-component-board.ts +2 -2
  166. package/src/runtime/adapter/audio-meter.ts +21 -0
  167. package/src/runtime/adapter/first-party-audio-system.ts +230 -0
  168. package/src/runtime/adapter/ingest/contract-debug-adapter.ts +114 -0
  169. package/src/runtime/adapter/ingest/contract-system-adapters.ts +256 -0
  170. package/src/runtime/adapter/ingest/merge-debug-adapters.ts +197 -0
  171. package/src/runtime/adapter/ingest/observation-debug-adapter.ts +162 -0
  172. package/src/runtime/adapter/ingest/upstream-pin.ts +51 -0
  173. package/src/runtime/adapter/native-debug-module.ts +498 -0
  174. package/src/runtime/audio/bus-mixer.ts +161 -0
  175. package/src/runtime/audio/pose-guard.ts +80 -0
  176. package/src/runtime/core/frame-pacing.ts +126 -0
  177. package/src/runtime/core/game-loop.ts +225 -0
  178. package/src/runtime/core/game-scoped-slot.ts +28 -0
  179. package/src/runtime/core/seeded-random.ts +162 -0
  180. package/src/runtime/core/sim-clock.ts +391 -0
  181. package/src/runtime/core/system-runner.ts +269 -0
  182. package/src/runtime/core/types.ts +104 -0
  183. package/src/runtime/create-runtime.ts +1128 -0
  184. package/src/runtime/debug-bridge.ts +570 -0
  185. package/src/runtime/debug-registry.ts +899 -0
  186. package/src/runtime/dev/chrome-trace.ts +153 -0
  187. package/src/runtime/dev/instruments.ts +403 -0
  188. package/src/runtime/dev/logger.ts +119 -0
  189. package/src/runtime/dev/performance-profiler.ts +367 -0
  190. package/src/runtime/dev/register-render-vitals.ts +276 -0
  191. package/src/runtime/dev/render-census.ts +354 -0
  192. package/src/runtime/dev/render-debug-adapter.ts +218 -0
  193. package/src/runtime/dev/render-memory.ts +226 -0
  194. package/src/runtime/dev/render-vitals.ts +338 -0
  195. package/src/runtime/dev/static-batch-advisor.ts +188 -0
  196. package/src/runtime/dev/webgl-frame-capture.ts +366 -0
  197. package/src/runtime/dev/webgl-gpu-timer.ts +53 -0
  198. package/src/runtime/dev-build.ts +47 -0
  199. package/src/runtime/game.ts +1636 -0
  200. package/src/runtime/gameplay-rng-trap.ts +135 -0
  201. package/src/runtime/host-context.ts +64 -0
  202. package/src/runtime/input-router.ts +169 -0
  203. package/src/runtime/mount-manifest.ts +480 -0
  204. package/src/runtime/pixi/authoring.ts +706 -0
  205. package/src/runtime/pixi/ingest.ts +116 -0
  206. package/src/runtime/pixi/physics-registry.ts +49 -0
  207. package/src/runtime/pixi/render-pass-bracket.ts +117 -0
  208. package/src/runtime/pixi/scene-capture.ts +179 -0
  209. package/src/runtime/pixi/system-adapters.ts +69 -0
  210. package/src/runtime/playtest.ts +22 -0
  211. package/src/runtime/presentation.ts +141 -0
  212. package/src/runtime/render-control.ts +642 -0
  213. package/src/runtime/render-seed.ts +77 -0
  214. package/src/runtime/run-ticks-settled.ts +73 -0
  215. package/src/runtime/setup/setup-audio.ts +72 -0
  216. package/src/services/audio-pose-guard.ts +2 -2
  217. package/src/services/game-audio.ts +152 -0
  218. package/src/services/game-network.ts +657 -0
  219. package/src/services/game-physics.ts +334 -0
  220. package/src/state-watch/StateWatchPanel.tsx +1 -1
  221. package/src/three/authoring/camera-runtime-inspector-section.tsx +3 -2
  222. package/src/three/authoring/constraint-inspector-section.tsx +6 -5
  223. package/src/three/authoring/model-asset-inspector-section.tsx +7 -6
  224. package/src/three/authoring/oid-source-persistence.ts +7 -7
  225. package/src/three/authoring/r3f-design-session.ts +58 -54
  226. package/src/three/authoring/r3f-source-authoring-adapter.ts +95 -91
  227. package/src/three/authoring/reflection-probe-inspector-section.tsx +3 -2
  228. package/src/three/authoring/three-authoring-adapter.ts +29 -29
  229. package/src/three/component-verbs/extract-menu.ts +7 -6
  230. package/src/three/component-verbs/fork-menu.ts +7 -6
  231. package/src/three/component-verbs/internals-menu.ts +2 -2
  232. package/src/three/story-documents/three-story-documents.tsx +13 -13
  233. package/src/three/three-board/ThreeBoardDocument.tsx +13 -12
  234. package/src/three/three-board/board-scene.ts +6 -6
  235. package/src/three/three-board/three-component-board.ts +2 -2
  236. package/src/services/game-audio-unlock.ts +0 -48
@@ -0,0 +1,230 @@
1
+ /**
2
+ * The first-party `AudioAdapter` over the game runtime's master-gain bus
3
+ * (`setup/setup-audio.ts`'s `AudioContext.masterGain`) — the seam
4
+ * `Game.play.pause()` calls to silence a first-party world's audio, plus the
5
+ * graph snapshot and the native analyser taps the editor's Audio panel and
6
+ * gameplay recorder read.
7
+ *
8
+ * Its NAVIGATION sibling (`createNavigationAdapter`) is the three.js twin's —
9
+ * `@volter/threejs-runtime/adapter/first-party-navigation-system` — for the same
10
+ * reason this one is here: each lives with the subsystem it wraps.
11
+ *
12
+ * Restoring after a mute puts the gain back at whatever value it held right
13
+ * before muting (not a hardcoded `1`), which composes correctly with an
14
+ * independent manual mute: pausing while already user-muted resumes muted,
15
+ * exactly as a user would expect.
16
+ */
17
+
18
+ import type {
19
+ AudioAdapter,
20
+ AudioGraphNode,
21
+ AudioMeterFrame,
22
+ AudioMeterHandle,
23
+ AudioRecordingHandle,
24
+ } from '@volter/editor-project/adapter/system-adapter';
25
+ import type { AudioContext as GameAudio } from '../setup/setup-audio';
26
+
27
+ /**
28
+ * First-party `AudioAdapter` over the engine's master-gain bus
29
+ * (`setup-audio.ts`'s `AudioContext.masterGain`) — the seam `Game.play.pause()`
30
+ * (D10, T7.6) calls to silence a first-party world's audio. Restoring after a
31
+ * mute puts the gain back at whatever value it held right before muting (not
32
+ * a hardcoded `1`) — this composes correctly with an independent manual mute
33
+ * (the editor's `MuteButton`, `PlayBar.tsx`): pausing while already
34
+ * user-muted resumes muted, exactly as a user would expect.
35
+ */
36
+ function audioNodeTypeName(value: object): string {
37
+ const name = (value as { constructor?: { name?: string } }).constructor?.name;
38
+ return name && name !== 'Object' ? name : 'AudioNode';
39
+ }
40
+
41
+ /** This world's real native context, when it has one (headless worlds do not). */
42
+ function nativeAudioContext(audio: GameAudio): (BaseAudioContext & Partial<AudioContext>) | null {
43
+ const context = (audio.listener as { context?: BaseAudioContext } | undefined)?.context;
44
+ return context && typeof context.state === 'string'
45
+ ? (context as BaseAudioContext & Partial<AudioContext>)
46
+ : null;
47
+ }
48
+
49
+ /**
50
+ * Live meter handles per adapter, so a world teardown can release analyser
51
+ * taps a consumer forgot to dispose.
52
+ *
53
+ * This registry follows `acquireMeters` out of `audio-introspection.ts`: the
54
+ * safety net has to live wherever the taps are created, and they are created
55
+ * here now. Keyed weakly so an adapter that is simply dropped takes its entry
56
+ * with it.
57
+ */
58
+ const meterHandlesByAdapter = new WeakMap<AudioAdapter, Set<AudioMeterHandle>>();
59
+
60
+ /** Release every meter tap still open on `adapter` (world stop / teardown). */
61
+ export function releaseAudioMeters(adapter: AudioAdapter): void {
62
+ const handles = meterHandlesByAdapter.get(adapter);
63
+ if (!handles) return;
64
+ for (const handle of [...handles]) handle.dispose();
65
+ }
66
+
67
+ export function createAudioSystemAdapter(audio: GameAudio): AudioAdapter {
68
+ let muted = false;
69
+ let priorGain = audio.masterGain.gain.value;
70
+ const liveMeterHandles = new Set<AudioMeterHandle>();
71
+ const adapter: AudioAdapter = {
72
+ resume: () => audio.resume(),
73
+ setMuted(next: boolean) {
74
+ if (next === muted) return;
75
+ muted = next;
76
+ if (next) {
77
+ priorGain = audio.masterGain.gain.value;
78
+ audio.masterGain.gain.value = 0;
79
+ } else {
80
+ audio.masterGain.gain.value = priorGain;
81
+ }
82
+ },
83
+ isMuted: () => muted,
84
+
85
+ /**
86
+ * The engine's own bus hierarchy.
87
+ *
88
+ * This lived in `audio-introspection.ts` — the Tone-backed module — which
89
+ * meant the editor could only ever show an audio graph for a game that had
90
+ * loaded Tone. Nothing here needs Tone: it is the destination, the master
91
+ * gain, the three engine buses and the THREE listener. The Tone half (nodes
92
+ * a game routed through `connectToneBusToMasterGain`) is APPENDED by that
93
+ * module when it attaches, which is the honest split — Tone routes are
94
+ * Tone's to report.
95
+ */
96
+ graphSnapshot(): AudioGraphNode[] {
97
+ const contextState = nativeAudioContext(audio)?.state;
98
+ const destination: AudioGraphNode = {
99
+ id: 'destination',
100
+ type: 'AudioDestinationNode',
101
+ label: 'Output',
102
+ outputs: [],
103
+ };
104
+ if (contextState !== undefined) destination.state = contextState;
105
+ const nodes: AudioGraphNode[] = [
106
+ destination,
107
+ {
108
+ id: 'master',
109
+ type: audioNodeTypeName(audio.masterGain),
110
+ label: 'Master',
111
+ outputs: ['destination'],
112
+ },
113
+ {
114
+ id: 'bus-music',
115
+ type: audioNodeTypeName(audio.buses.music),
116
+ label: 'Music bus',
117
+ outputs: ['master'],
118
+ },
119
+ {
120
+ id: 'bus-sfx',
121
+ type: audioNodeTypeName(audio.buses.sfx),
122
+ label: 'SFX bus',
123
+ outputs: ['master'],
124
+ },
125
+ {
126
+ id: 'bus-voice',
127
+ type: audioNodeTypeName(audio.buses.voice),
128
+ label: 'Voice bus',
129
+ outputs: ['master'],
130
+ },
131
+ ];
132
+ if (audio.listener) {
133
+ nodes.push({
134
+ id: 'listener',
135
+ type: audioNodeTypeName(audio.listener),
136
+ label: '3D listener (THREE)',
137
+ outputs: ['master'],
138
+ });
139
+ }
140
+ return nodes;
141
+ },
142
+
143
+ /**
144
+ * Per-bus linear-RMS levels, via native `AnalyserNode` taps.
145
+ *
146
+ * Zero Tone — analysers work identically whether or not Tone is bridged,
147
+ * and disposal is a plain disconnect. It was in the Tone module purely by
148
+ * where it was written, so a game that never touches Tone had no meters.
149
+ */
150
+ acquireMeters(): AudioMeterHandle | null {
151
+ const meterContext = nativeAudioContext(audio);
152
+ if (!meterContext || typeof meterContext.createAnalyser !== 'function') {
153
+ return null; // headless world — metering is impossible, and says so
154
+ }
155
+ const taps = (
156
+ [
157
+ ['master', 'Master', audio.masterGain],
158
+ ['bus-music', 'Music', audio.buses.music],
159
+ ['bus-sfx', 'SFX', audio.buses.sfx],
160
+ ['bus-voice', 'Voice', audio.buses.voice],
161
+ ] as const
162
+ ).map(([id, label, gain]) => {
163
+ const analyser = meterContext.createAnalyser();
164
+ analyser.fftSize = 1024;
165
+ gain.connect(analyser); // a TAP (analyser is a sink), not an insert
166
+ return { id, label, gain, analyser, buffer: new Float32Array(analyser.fftSize) };
167
+ });
168
+ let disposed = false;
169
+ const handle: AudioMeterHandle = {
170
+ read: (): AudioMeterFrame[] =>
171
+ taps.map(({ id, label, analyser, buffer }) => {
172
+ analyser.getFloatTimeDomainData(buffer);
173
+ let sum = 0;
174
+ for (let i = 0; i < buffer.length; i++) {
175
+ const sample = buffer[i] as number;
176
+ sum += sample * sample;
177
+ }
178
+ return { id, label, level: Math.sqrt(sum / buffer.length) };
179
+ }),
180
+ dispose: (): void => {
181
+ if (disposed) return;
182
+ disposed = true;
183
+ for (const tap of taps) {
184
+ try {
185
+ tap.gain.disconnect(tap.analyser);
186
+ } catch {
187
+ // already torn down with the context — nothing to release
188
+ }
189
+ }
190
+ liveMeterHandles.delete(handle);
191
+ },
192
+ };
193
+ liveMeterHandles.add(handle);
194
+ return handle;
195
+ },
196
+
197
+ /** Native Web Audio tap used by the editor's gameplay recorder. It is a
198
+ * sink beside the speakers, never an insert, so recording cannot change
199
+ * what the player hears. A suspended context produces no audio frames and
200
+ * stalls MediaRecorder muxing, so it cannot supply a recording track. */
201
+ acquireRecordingStream(): AudioRecordingHandle | null {
202
+ const recordingContext = nativeAudioContext(audio);
203
+ if (
204
+ !recordingContext ||
205
+ recordingContext.state !== 'running' ||
206
+ typeof recordingContext.createMediaStreamDestination !== 'function'
207
+ ) {
208
+ return null;
209
+ }
210
+ const destination = recordingContext.createMediaStreamDestination();
211
+ audio.masterGain.connect(destination);
212
+ let disposed = false;
213
+ return {
214
+ stream: destination.stream,
215
+ dispose(): void {
216
+ if (disposed) return;
217
+ disposed = true;
218
+ try {
219
+ audio.masterGain.disconnect(destination);
220
+ } catch {
221
+ // The world/context may already have been torn down.
222
+ }
223
+ for (const track of destination.stream.getTracks()) track.stop();
224
+ },
225
+ };
226
+ },
227
+ };
228
+ meterHandlesByAdapter.set(adapter, liveMeterHandles);
229
+ return adapter;
230
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Projects a game's DECLARED system surface (`window.vgaiGame.systems`, see
3
+ * `game-contract.ts`) onto the host's existing {@link DebugAdapter} — the one
4
+ * seam `game.commands()` / `game.state()` / `game.providers()` already read
5
+ * through for first-party content (`command-listener.ts`'s
6
+ * `dispatchBridgeMethod` → `getActiveSystems().debug`).
7
+ *
8
+ * Why a projection rather than a second door: an agent driving an ingested
9
+ * game must not have to learn a parallel vocabulary. A first-party game
10
+ * registers verbs with `ctx.debug.registerCommand`; an ingested game declares
11
+ * them in its entry shim. Both arrive at the SAME `DebugAdapter`, so every
12
+ * consumer downstream — the CLI's `vgai eval`, `@vgai/e2e`'s `GameClient`, the
13
+ * editor's Debug Console and State Watch panels — works unchanged and unaware
14
+ * of the provenance.
15
+ *
16
+ * What this deliberately does NOT do:
17
+ * - **Validate arguments.** The first-party path validates against a declared
18
+ * Zod tuple; a shim beside a foreign game is plain JS and has none. A verb
19
+ * validates itself and throws its own error, which surfaces wrapped as
20
+ * `DEBUG_COMMAND_FAILED` — the host never invents a schema to check against.
21
+ * - **Fabricate an event ring.** There is no tick loop behind an ingested
22
+ * game's declarations, so `events()` is empty. That is accurate (nothing was
23
+ * emitted), not a stub standing in for a missing feature.
24
+ */
25
+
26
+ import type { VgaiGameSystems } from '@volter/editor-project/adapter/ingest/game-contract';
27
+ import type {
28
+ DebugAdapter,
29
+ DebugCommandInfo,
30
+ TickStampedEvent,
31
+ } from '@volter/editor-project/adapter/system-adapter';
32
+ import { DebugError } from '../../debug-registry';
33
+
34
+ /**
35
+ * Build a {@link DebugAdapter} over a declared system surface, or `null` when
36
+ * the game declared no verbs and no state. `null` is the honest floor: the
37
+ * bridge then reports `DEBUG_ADAPTER_UNAVAILABLE` exactly as it did before,
38
+ * rather than serving an empty adapter that reads as "this game has no state"
39
+ * when the truth is "this game declared nothing".
40
+ */
41
+ export function createContractDebugAdapter(
42
+ systems: VgaiGameSystems | undefined | null,
43
+ ): DebugAdapter | null {
44
+ const commandList = systems?.commands ?? [];
45
+ const providerList = systems?.state ?? [];
46
+ if (commandList.length === 0 && providerList.length === 0) return null;
47
+
48
+ // Snapshot into name-keyed maps once. A later duplicate shadows an earlier
49
+ // one and the listing de-duplicates with it, so `commands()` can never
50
+ // advertise a name `invoke()` would resolve differently.
51
+ const byCommand = new Map(commandList.map((c) => [c.name, c]));
52
+ const byProvider = new Map(providerList.map((p) => [p.name, p]));
53
+
54
+ return {
55
+ providers: () =>
56
+ [...byProvider.values()].map((p) => ({ name: p.name, tier: p.tier ?? 'observable' })),
57
+
58
+ state: (name: string): unknown => {
59
+ const provider = byProvider.get(name);
60
+ if (!provider) {
61
+ throw new DebugError(
62
+ 'STATE_PROVIDER_NOT_FOUND',
63
+ `No state provider named "${name}" is declared by this game.`,
64
+ { registered: [...byProvider.keys()] },
65
+ );
66
+ }
67
+ return provider.read();
68
+ },
69
+
70
+ stateAll: (): Record<string, unknown> => {
71
+ const out: Record<string, unknown> = {};
72
+ for (const [name, provider] of byProvider) {
73
+ // One throwing provider must not cost the caller every other reading —
74
+ // the same per-key isolation the first-party registry gives.
75
+ try {
76
+ out[name] = provider.read();
77
+ } catch (err) {
78
+ out[name] = { __error: String(err) };
79
+ }
80
+ }
81
+ return out;
82
+ },
83
+
84
+ commands: (): DebugCommandInfo[] =>
85
+ [...byCommand.values()].map((c) => ({
86
+ name: c.name,
87
+ description: c.description,
88
+ argsJsonSchema: c.argsJsonSchema,
89
+ // An ingested game runs entirely in the browser realm the host mounted
90
+ // it in; there is no server leg for its verbs to route to.
91
+ locus: 'client' as const,
92
+ })),
93
+
94
+ invoke: async (name: string, args: unknown[]): Promise<unknown> => {
95
+ const command = byCommand.get(name);
96
+ if (!command) {
97
+ throw new DebugError(
98
+ 'DEBUG_COMMAND_NOT_REGISTERED',
99
+ `No command named "${name}" is declared by this game.`,
100
+ { registered: [...byCommand.keys()] },
101
+ );
102
+ }
103
+ try {
104
+ return await command.run(...args);
105
+ } catch (err) {
106
+ throw new DebugError('DEBUG_COMMAND_FAILED', `Command "${name}" threw: ${String(err)}`, {
107
+ name,
108
+ });
109
+ }
110
+ },
111
+
112
+ events: (): TickStampedEvent[] => [],
113
+ };
114
+ }
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Projects a game's declared {@link VgaiGameSystemAdapters}
3
+ * (`window.vgaiGame.systems.systemAdapters`) onto the host's ordinary
4
+ * {@link SystemAdapters} bag — the ingest-realm counterpart of a first-party
5
+ * world's `ctx.registerSystemAdapter`, and the sibling of
6
+ * `contract-debug-adapter.ts` (which owns the `debug` slot).
7
+ *
8
+ * ## Why a projection with a verdict per slot
9
+ *
10
+ * The ingestion bar (docs/BRINGING-AN-EXISTING-GAME.md §The ingestion
11
+ * checklist) admits exactly two terminal states per slot: IMPLEMENTED or
12
+ * IMPLEMENTED-EMPTY. `SystemAdapters` alone cannot express the second — an
13
+ * omitted key means "unsupported", which is the same shape as "nobody looked".
14
+ * So this returns THREE lists, and the caller hands the non-`bound` ones to the
15
+ * coverage report, where a positively-answered absence prints as terminal and an
16
+ * unanswered slot prints as a work order.
17
+ *
18
+ * ## Why the shape check
19
+ *
20
+ * A slot that declares `{}` would otherwise register as "implemented" and light
21
+ * up an editor panel over an object with no methods. Each slot therefore names
22
+ * the members its interface makes REQUIRED, and a declaration missing any of
23
+ * them is `malformed` — reported by name, never silently bound and never
24
+ * silently dropped. This is the same honesty the anti-shim rule asks of the
25
+ * other direction: a wrong "present" claim is as much a defect as a wrong
26
+ * "empty" one.
27
+ *
28
+ * ## Why `physics` also gets a KEYING check
29
+ *
30
+ * The physics slot is the one whose vocabulary follows the SURFACE
31
+ * ({@link PHYSICS_KEYING_BY_SURFACE}): a three-surface game keys by host node
32
+ * id, a canvas-surface game by the display object it already holds. Both shapes
33
+ * carry the same four member names, so presence-and-typeof cannot tell them
34
+ * apart — a carrier keyed for the other surface would pass every check here and
35
+ * then be called with values it has none of, which is the "green coverage row
36
+ * bought with four methods that have no caller" defect this whole slot was left
37
+ * open to avoid. So a mount that KNOWS its surface says so, and a mismatch is
38
+ * `malformed` by name. A caller with no surface fact (`undefined`) does not
39
+ * check — it has nothing to check against, and inventing a default would be a
40
+ * guess wearing a verdict.
41
+ *
42
+ * Nothing here calls a declared method. Presence and shape only.
43
+ */
44
+
45
+ import type {
46
+ ContractSystemEmptySlot,
47
+ ContractSystemMalformedSlot,
48
+ ContractSystemSlot,
49
+ } from '@volter/editor-project/adapter/ingest/contract-system-slots';
50
+ import type { SystemAdapters } from '@volter/editor-project/adapter/system-adapter';
51
+
52
+ export type {
53
+ ContractSystemEmptySlot,
54
+ ContractSystemMalformedSlot,
55
+ } from '@volter/editor-project/adapter/ingest/contract-system-slots';
56
+
57
+ import type {
58
+ VgaiGameSystemAdapters,
59
+ VgaiGameSystemEmpty,
60
+ VgaiGameSystems,
61
+ } from '@volter/editor-project/adapter/ingest/game-contract';
62
+ import { GAME_SYSTEM_ADAPTERS_SHAPE } from '@volter/editor-project/adapter/ingest/game-contract-seams';
63
+ import {
64
+ AUDIO_ADAPTER_SHAPE,
65
+ CAMERA_ADAPTER_SHAPE,
66
+ NAVIGATION_ADAPTER_SHAPE,
67
+ NETWORKING_ADAPTER_SHAPE,
68
+ PHYSICS_ADAPTER_SHAPE,
69
+ RENDER_DEBUG_ADAPTER_SHAPE,
70
+ } from '@volter/editor-project/adapter/system-seam-contract';
71
+
72
+ /** The slots a game may declare through the contract (`debug` is projected from
73
+ * `commands`/`state` instead — see {@link VgaiGameSystemAdapters}). */
74
+ export type { ContractSystemSlot } from '@volter/editor-project/adapter/ingest/contract-system-slots';
75
+
76
+ /**
77
+ * The members each slot's interface makes NON-optional. A declaration missing
78
+ * one of these cannot honour the interface, whatever else it carries.
79
+ *
80
+ * This table is the single place the required surface is spelled; it is checked
81
+ * against the interfaces by the compiler at the point of use below (each list
82
+ * is typed as keys of its own adapter), so it cannot drift into naming a member
83
+ * that does not exist.
84
+ */
85
+ const SYSTEM_SHAPES = {
86
+ physics: PHYSICS_ADAPTER_SHAPE,
87
+ networking: NETWORKING_ADAPTER_SHAPE,
88
+ navigation: NAVIGATION_ADAPTER_SHAPE,
89
+ audio: AUDIO_ADAPTER_SHAPE,
90
+ camera: CAMERA_ADAPTER_SHAPE,
91
+ renderDebug: RENDER_DEBUG_ADAPTER_SHAPE,
92
+ } as const satisfies Record<ContractSystemSlot, object>;
93
+
94
+ /**
95
+ * The render surface a declaration is being read ON — the same three words
96
+ * `IngestKind`/`AdapterSurface` already use, so nothing has to be declared
97
+ * twice.
98
+ */
99
+ export type ContractSurface = 'three' | 'canvas' | 'dom';
100
+
101
+ /**
102
+ * THE surface→physics-vocabulary rule, stated once.
103
+ *
104
+ * `null` for `dom`: that surface has no physics consumer at all, so nothing
105
+ * here can say which keying would be right for it and the check stands down
106
+ * rather than inventing an answer.
107
+ */
108
+ const PHYSICS_KEYING_BY_SURFACE: Readonly<Record<ContractSurface, 'node-id' | 'display' | null>> = {
109
+ three: 'node-id',
110
+ canvas: 'display',
111
+ dom: null,
112
+ };
113
+
114
+ /** Fixed slot order, so two reports of the same mount are diffable line for
115
+ * line (the same rule the coverage report's row order follows). */
116
+ export const CONTRACT_SYSTEM_SLOTS = Object.keys(
117
+ GAME_SYSTEM_ADAPTERS_SHAPE,
118
+ ) as ContractSystemSlot[];
119
+
120
+ export interface ContractSystemAdapterProjection {
121
+ /** Real adapters, ready to spread onto a `MountedRoot`'s `systems`. */
122
+ readonly bound: Partial<Pick<SystemAdapters, ContractSystemSlot>>;
123
+ /** Slots answered IMPLEMENTED-EMPTY, with the evidence. */
124
+ readonly empty: readonly ContractSystemEmptySlot[];
125
+ /** Slots declared but unusable — bound to nothing, reported by name. */
126
+ readonly malformed: readonly ContractSystemMalformedSlot[];
127
+ }
128
+
129
+ const EMPTY_PROJECTION: ContractSystemAdapterProjection = {
130
+ bound: {},
131
+ empty: [],
132
+ malformed: [],
133
+ };
134
+
135
+ function isEmptyRecord(value: object): value is VgaiGameSystemEmpty {
136
+ return (value as { present?: unknown }).present === false;
137
+ }
138
+
139
+ function missingMembers(slot: ContractSystemSlot, value: object): string[] {
140
+ const missing: string[] = [];
141
+ for (const [member, spec] of Object.entries(
142
+ SYSTEM_SHAPES[slot] as Readonly<Record<string, { readonly optional: boolean }>>,
143
+ )) {
144
+ if (spec.optional) continue;
145
+ if (typeof (value as Record<string, unknown>)[member] !== 'function') {
146
+ missing.push(member);
147
+ }
148
+ }
149
+ return missing;
150
+ }
151
+
152
+ /**
153
+ * The reason a declared physics carrier cannot serve `surface`, or `null` when
154
+ * it can (including when the caller supplied no surface to check against).
155
+ *
156
+ * The keying a carrier actually has is its tag's own answer, and an ABSENT tag
157
+ * IS `'node-id'` — see `PhysicsAdapter`'s comment for why the original
158
+ * vocabulary is the untagged one.
159
+ */
160
+ function physicsKeyingMismatch(value: object, surface: ContractSurface | undefined): string | null {
161
+ const want = surface ? PHYSICS_KEYING_BY_SURFACE[surface] : null;
162
+ if (want === null) return null;
163
+ const got = (value as { keyedBy?: unknown }).keyedBy === 'display' ? 'display' : 'node-id';
164
+ if (got === want) return null;
165
+ return (
166
+ `declared a ${got}-keyed physics carrier on the ${surface} surface, whose gizmo path ` +
167
+ `addresses bodies by ${want} — the four members are all present but nothing on this lane ` +
168
+ `can call them with values they accept (a ${surface}-surface carrier declares ` +
169
+ `\`keyedBy: '${want}'\`)`
170
+ );
171
+ }
172
+
173
+ /** One slot's verdict, before it is filed into the three lists. */
174
+ type SlotVerdict =
175
+ | { readonly kind: 'skip' }
176
+ | { readonly kind: 'bound' }
177
+ | { readonly kind: 'empty'; readonly evidence: string }
178
+ | { readonly kind: 'malformed'; readonly reason: string };
179
+
180
+ /**
181
+ * THE per-slot decision, in the order the module header states: is it declared
182
+ * at all, is it the positively-answered empty, does it carry the members its
183
+ * interface requires, and (physics only) is it keyed for this surface.
184
+ *
185
+ * Split out of {@link projectContractSystemAdapters} so the projection itself
186
+ * is a loop over verdicts — one place decides, one place files.
187
+ */
188
+ function classifyDeclaredSlot(
189
+ slot: ContractSystemSlot,
190
+ value: unknown,
191
+ surface: ContractSurface | undefined,
192
+ ): SlotVerdict {
193
+ if (value === undefined || value === null) return { kind: 'skip' };
194
+ if (typeof value !== 'object') {
195
+ return {
196
+ kind: 'malformed',
197
+ reason: `declared as ${typeof value}; expected an adapter object or { present: false, evidence }`,
198
+ };
199
+ }
200
+ if (isEmptyRecord(value)) {
201
+ const evidence = (value as VgaiGameSystemEmpty).evidence;
202
+ // An empty claim with no evidence is exactly the "wrong empty claim" the
203
+ // bar calls a defect — it asserts an absence nobody can re-check.
204
+ if (typeof evidence !== 'string' || evidence.trim() === '') {
205
+ return {
206
+ kind: 'malformed',
207
+ reason: 'declared `present: false` with no `evidence` string to back the absence',
208
+ };
209
+ }
210
+ return { kind: 'empty', evidence };
211
+ }
212
+ const missing = missingMembers(slot, value);
213
+ if (missing.length > 0) {
214
+ return {
215
+ kind: 'malformed',
216
+ reason: `declared an adapter missing required member(s): ${missing.join(', ')}`,
217
+ };
218
+ }
219
+ const mismatch = slot === 'physics' ? physicsKeyingMismatch(value, surface) : null;
220
+ if (mismatch !== null) return { kind: 'malformed', reason: mismatch };
221
+ return { kind: 'bound' };
222
+ }
223
+
224
+ /**
225
+ * Read the declared slots and sort each into exactly one verdict. A game that
226
+ * declared no `systemAdapters` at all projects to three empty lists — which is
227
+ * the "nobody answered" case, and is deliberately NOT the same as declaring
228
+ * every slot empty.
229
+ *
230
+ * `surface` is the mount's own surface fact when it has one; see the module
231
+ * header for what it buys and why its absence checks nothing.
232
+ */
233
+ export function projectContractSystemAdapters(
234
+ systems: VgaiGameSystems | undefined | null,
235
+ surface?: ContractSurface | undefined,
236
+ ): ContractSystemAdapterProjection {
237
+ const declared = systems?.systemAdapters;
238
+ if (!declared) return EMPTY_PROJECTION;
239
+
240
+ const bound: Record<string, unknown> = {};
241
+ const empty: ContractSystemEmptySlot[] = [];
242
+ const malformed: ContractSystemMalformedSlot[] = [];
243
+
244
+ for (const slot of CONTRACT_SYSTEM_SLOTS) {
245
+ const verdict = classifyDeclaredSlot(slot, declared[slot], surface);
246
+ if (verdict.kind === 'bound') bound[slot] = declared[slot];
247
+ else if (verdict.kind === 'empty') empty.push({ slot, evidence: verdict.evidence });
248
+ else if (verdict.kind === 'malformed') malformed.push({ slot, reason: verdict.reason });
249
+ }
250
+
251
+ return {
252
+ bound: bound as ContractSystemAdapterProjection['bound'],
253
+ empty,
254
+ malformed,
255
+ };
256
+ }