@volter/editor-game 0.5.66 → 0.5.68

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 (239) 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/build-player.document.tsx +95 -0
  5. package/contributions/canvas/component-board.service.ts +16 -0
  6. package/contributions/canvas/design-time-mount.service.ts +45 -0
  7. package/contributions/canvas-story-capture.service.ts +12 -0
  8. package/contributions/edit-mode-audio.service.ts +2 -2
  9. package/contributions/edit-mode-networking.service.ts +1 -1
  10. package/contributions/gameplay.command.ts +4 -4
  11. package/contributions/generation.service.ts +1 -1
  12. package/contributions/godot.style.ts +26 -6
  13. package/contributions/godot.view.ts +6 -0
  14. package/contributions/ingest.service.ts +2 -2
  15. package/contributions/instances.command.ts +1 -1
  16. package/contributions/navmesh.menu.ts +1 -1
  17. package/contributions/network-observer.service.ts +14 -0
  18. package/contributions/play.command.ts +10 -4
  19. package/contributions/react/component-board.service.ts +1 -1
  20. package/contributions/react/design-time-mount.service.ts +1 -1
  21. package/contributions/scene-document.service.ts +3 -3
  22. package/contributions/state-watch.menu.ts +1 -1
  23. package/contributions/state-watch.utility.tsx +1 -1
  24. package/contributions/team-playtest.service.ts +4 -4
  25. package/contributions/three/component-board.service.ts +1 -1
  26. package/contributions/three/component-verbs.command.ts +6 -6
  27. package/contributions/three/three-authoring.service.ts +8 -5
  28. package/contributions/three-story-capture.service.ts +12 -0
  29. package/contributions/unity.style.ts +17 -7
  30. package/contributions/unity.view.ts +6 -0
  31. package/contributions/unreal.style.ts +10 -2
  32. package/contributions/unreal.view.ts +5 -0
  33. package/package.json +21 -10
  34. package/src/asset-budget/AssetBudgetPanel.tsx +1 -1
  35. package/src/asset-budget/asset-budget-model.ts +1 -1
  36. package/src/audio/AudioDebuggerPanel.tsx +1 -1
  37. package/src/bridge/dispatch.ts +14 -14
  38. package/src/bridge/live-frames.ts +1 -1
  39. package/src/bridge/screenshot.ts +3 -3
  40. package/src/build/BuildProfilesPanel.tsx +14 -3
  41. package/src/build/build-session.ts +35 -0
  42. package/src/canvas/canvas-board/CanvasBoardDocument.tsx +749 -0
  43. package/src/canvas/canvas-board/canvas-board-model.ts +407 -0
  44. package/src/canvas/canvas-board/canvas-component-board.ts +56 -0
  45. package/src/canvas/canvas-design-mount.ts +524 -0
  46. package/src/canvas/design-time-canvas-mount.ts +79 -0
  47. package/src/coverage/live-authoring-surface.ts +5 -5
  48. package/src/coverage/live-project-verbs.ts +1 -1
  49. package/src/coverage/native-system-coverage.ts +5 -5
  50. package/src/coverage/root-coverage.ts +1 -1
  51. package/src/coverage/session-coverage.ts +3 -3
  52. package/src/design-system-stories/ApplicationChrome.stories.tsx +5 -5
  53. package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +7 -7
  54. package/src/edit-mode/edit-mode-audio.ts +4 -4
  55. package/src/edit-mode/edit-mode-networking.ts +2 -2
  56. package/src/game-document/GameCaptureFrameButton.tsx +1 -1
  57. package/src/game-document/GameDocument.tsx +2 -2
  58. package/src/game-document/GamePanel.tsx +4 -4
  59. package/src/game-document/InstanceInspectorPicker.tsx +2 -2
  60. package/src/game-document/crowd-debug.ts +1 -1
  61. package/src/game-document/device-preview.ts +10 -11
  62. package/src/game-document/physics-debug.ts +1 -1
  63. package/src/generation/GenerationActivity.tsx +3 -3
  64. package/src/generation/generation-documents.tsx +5 -5
  65. package/src/generation/generation-jobs.ts +1 -1
  66. package/src/host/adapter-runtime-bindings.ts +96 -5
  67. package/src/host/authoring/babylon-authoring-adapter.ts +6 -6
  68. package/src/host/authoring/gesture-persist.ts +1 -1
  69. package/src/host/authoring/ingest-data-writer.ts +1 -1
  70. package/src/host/authoring/ingest-source-persistence.ts +4 -4
  71. package/src/host/authoring/mounted-authoring.ts +4 -4
  72. package/src/host/authoring/phaser-live-authoring-adapter.ts +4 -4
  73. package/src/host/authoring/pixi-authoring-adapter.ts +178 -31
  74. package/src/host/authoring/pixi-creatable-kinds.ts +62 -0
  75. package/src/host/authoring/pixi-creation-site-write-target.ts +2 -2
  76. package/src/host/authoring/pixi-live-write-target.ts +4 -4
  77. package/src/host/authoring/pixi-source-identity.ts +3 -3
  78. package/src/host/authoring/pixi-source-write-target.ts +1709 -0
  79. package/src/host/authoring/pixi-still-presentation.ts +1 -1
  80. package/src/host/authoring/pixi-structure-history.ts +2 -2
  81. package/src/host/authoring/pixi-transform-channels.ts +16 -14
  82. package/src/host/authoring/source-persistence-backend.ts +3 -3
  83. package/src/host/authoring/struct-write-pipe.ts +1 -1
  84. package/src/host/binding-resolver.ts +8 -9
  85. package/src/host/browser-transpile.ts +1 -1
  86. package/src/host/canvas-entry-runtime.ts +58 -47
  87. package/src/host/canvas-preview-frames.ts +482 -0
  88. package/src/host/components/CameraAuthoringOverlay.tsx +1 -1
  89. package/src/host/components/HeaderTelemetry.tsx +4 -4
  90. package/src/host/components/PixiIsolationSceneContent.tsx +11 -11
  91. package/src/host/components/ThreeIsolationSceneContent.tsx +3 -3
  92. package/src/host/components/frame-debugger-model.ts +2 -2
  93. package/src/host/components/header-telemetry-model.ts +2 -2
  94. package/src/host/components/scene-document.tsx +14 -14
  95. package/src/host/components/utility-view-state.ts +1 -1
  96. package/src/host/components/world-root-stage-binding.tsx +12 -12
  97. package/src/host/components/world-root-stage.ts +70 -49
  98. package/src/host/coverage/system-adapter-coverage.ts +3 -4
  99. package/src/host/design-system-stories/fixtures/editor-runtime.tsx +9 -11
  100. package/src/host/document-preview-three.ts +1 -1
  101. package/src/host/entry-adjudication.ts +6 -6
  102. package/src/host/game-css-scope-transform.ts +4 -0
  103. package/src/host/game-realm-page.ts +1 -1
  104. package/src/host/gameplay-export.ts +25 -14
  105. package/src/host/gameplay-recording.ts +5 -5
  106. package/src/host/gated-globals.ts +2 -2
  107. package/src/host/history/json-history-resource.ts +1 -1
  108. package/src/host/projection/pixi.ts +24 -2
  109. package/src/host/r3f-entry-runtime.ts +65 -34
  110. package/src/host/react-mount-runtime.ts +7 -48
  111. package/src/host/realm-services.ts +1 -1
  112. package/src/host/roots/canvas-root.tsx +373 -0
  113. package/src/host/roots/r3f-root.tsx +473 -0
  114. package/src/host/roots/react-root.ts +9 -43
  115. package/src/host/served-bundle-runtime-modules.ts +3 -19
  116. package/src/host/server-log-bridge.ts +2 -2
  117. package/src/host/stories/mounted-story-viewport-source.ts +1 -1
  118. package/src/host/stories/pixi-story-model.ts +30 -0
  119. package/src/host/stories/story-media-captures.ts +46 -0
  120. package/src/host/stories/story-media-presence.ts +3 -3
  121. package/src/host/stories/story-pixi-preview.ts +408 -0
  122. package/src/host/stories/story-three-preview.ts +806 -0
  123. package/src/host/stories/three-story-captures.ts +35 -0
  124. package/src/host/story-three-preview-runtime.ts +56 -0
  125. package/src/host/use-active-performance-source.ts +2 -2
  126. package/src/host/viewport-pose-memory.ts +1 -1
  127. package/src/host/viewport-root-presentation.ts +6 -5
  128. package/src/ingest/active-ingest.ts +1 -1
  129. package/src/ingest/authoring/ingest-dom-surface-authoring.ts +4 -4
  130. package/src/ingest/authoring/ingest-root-adapter.ts +8 -8
  131. package/src/ingest/deferred-ingest-play.ts +6 -6
  132. package/src/ingest/discovery-public-ingest.ts +2 -2
  133. package/src/ingest/ingest-boot-viewport.ts +2 -2
  134. package/src/ingest/ingest-canvas-scene-document.tsx +9 -9
  135. package/src/ingest/ingest-canvas-scene.ts +3 -3
  136. package/src/ingest/ingest-evidence-hook.ts +1 -1
  137. package/src/ingest/ingest-frame-snapshot.ts +1 -1
  138. package/src/ingest/ingest-play-control.ts +1 -1
  139. package/src/ingest/ingest-render-debug.ts +10 -10
  140. package/src/ingest/ingest-siblings.ts +13 -25
  141. package/src/ingest/module-mode.ts +14 -14
  142. package/src/ingest/mount-canvas-ingest-root.ts +21 -21
  143. package/src/ingest/mount-coverage.ts +2 -2
  144. package/src/ingest/mount-dom-ingest-root.ts +11 -11
  145. package/src/ingest/mount-ingest-root.ts +8 -8
  146. package/src/ingest/mount-three-ingest-root.ts +8 -8
  147. package/src/ingest/resolve-canvas.ts +1 -1
  148. package/src/ingest/served-html-boot.ts +1 -1
  149. package/src/ingest/surface-canvas.ts +1 -1
  150. package/src/ingest/unmount-ingest-root.ts +5 -5
  151. package/src/navmesh/navmesh-handler.ts +24 -16
  152. package/src/network/NetworkInspectorPanel.tsx +939 -37
  153. package/src/network/network-inspector-model.ts +20 -2
  154. package/src/play/play-log-events.ts +1 -1
  155. package/src/play/play-mode.ts +76 -133
  156. package/src/play/play-recording.ts +1 -1
  157. package/src/play/react-play-live-authoring.ts +3 -3
  158. package/src/play/run-selection.ts +93 -0
  159. package/src/play-bar/PlayBar.tsx +20 -39
  160. package/src/profiler/FrameDebuggerPanel.tsx +1 -1
  161. package/src/profiler/PerformancePanel.tsx +3 -3
  162. package/src/react/design-time-react-mount.ts +23 -65
  163. package/src/react/dom-authoring-adapter.ts +9 -9
  164. package/src/react/react-inspector-section.tsx +5 -5
  165. package/src/react/react-world-authoring-adapter.ts +11 -11
  166. package/src/react/story-documents/story-documents.tsx +9 -9
  167. package/src/react/ui-board-document.tsx +7 -7
  168. package/src/react/ui-component-board.ts +2 -2
  169. package/src/runtime/adapter/audio-meter.ts +21 -0
  170. package/src/runtime/adapter/first-party-audio-system.ts +230 -0
  171. package/src/runtime/adapter/ingest/contract-debug-adapter.ts +114 -0
  172. package/src/runtime/adapter/ingest/contract-system-adapters.ts +256 -0
  173. package/src/runtime/adapter/ingest/merge-debug-adapters.ts +197 -0
  174. package/src/runtime/adapter/ingest/observation-debug-adapter.ts +162 -0
  175. package/src/runtime/adapter/ingest/upstream-pin.ts +51 -0
  176. package/src/runtime/adapter/native-debug-module.ts +498 -0
  177. package/src/runtime/audio/bus-mixer.ts +161 -0
  178. package/src/runtime/audio/pose-guard.ts +80 -0
  179. package/src/runtime/core/frame-pacing.ts +126 -0
  180. package/src/runtime/core/game-loop.ts +225 -0
  181. package/src/runtime/core/game-scoped-slot.ts +28 -0
  182. package/src/runtime/core/seeded-random.ts +162 -0
  183. package/src/runtime/core/sim-clock.ts +391 -0
  184. package/src/runtime/core/system-runner.ts +269 -0
  185. package/src/runtime/core/types.ts +104 -0
  186. package/src/runtime/create-runtime.ts +1128 -0
  187. package/src/runtime/debug-bridge.ts +570 -0
  188. package/src/runtime/debug-registry.ts +899 -0
  189. package/src/runtime/dev/chrome-trace.ts +153 -0
  190. package/src/runtime/dev/instruments.ts +403 -0
  191. package/src/runtime/dev/logger.ts +119 -0
  192. package/src/runtime/dev/performance-profiler.ts +367 -0
  193. package/src/runtime/dev/register-render-vitals.ts +276 -0
  194. package/src/runtime/dev/render-census.ts +354 -0
  195. package/src/runtime/dev/render-debug-adapter.ts +218 -0
  196. package/src/runtime/dev/render-memory.ts +226 -0
  197. package/src/runtime/dev/render-vitals.ts +338 -0
  198. package/src/runtime/dev/static-batch-advisor.ts +188 -0
  199. package/src/runtime/dev/webgl-frame-capture.ts +366 -0
  200. package/src/runtime/dev/webgl-gpu-timer.ts +53 -0
  201. package/src/runtime/dev-build.ts +47 -0
  202. package/src/runtime/game.ts +1636 -0
  203. package/src/runtime/gameplay-rng-trap.ts +135 -0
  204. package/src/runtime/host-context.ts +64 -0
  205. package/src/runtime/input-router.ts +182 -0
  206. package/src/runtime/mount-manifest.ts +480 -0
  207. package/src/runtime/pixi/authoring.ts +706 -0
  208. package/src/runtime/pixi/ingest.ts +116 -0
  209. package/src/runtime/pixi/physics-registry.ts +49 -0
  210. package/src/runtime/pixi/render-pass-bracket.ts +117 -0
  211. package/src/runtime/pixi/scene-capture.ts +179 -0
  212. package/src/runtime/pixi/system-adapters.ts +69 -0
  213. package/src/runtime/playtest.ts +22 -0
  214. package/src/runtime/presentation.ts +141 -0
  215. package/src/runtime/render-control.ts +642 -0
  216. package/src/runtime/render-seed.ts +77 -0
  217. package/src/runtime/run-ticks-settled.ts +73 -0
  218. package/src/runtime/setup/setup-audio.ts +72 -0
  219. package/src/services/audio-pose-guard.ts +2 -2
  220. package/src/services/game-audio.ts +152 -0
  221. package/src/services/game-network.ts +767 -0
  222. package/src/services/game-physics.ts +334 -0
  223. package/src/state-watch/StateWatchPanel.tsx +1 -1
  224. package/src/three/authoring/camera-runtime-inspector-section.tsx +3 -2
  225. package/src/three/authoring/constraint-inspector-section.tsx +6 -5
  226. package/src/three/authoring/model-asset-inspector-section.tsx +7 -6
  227. package/src/three/authoring/oid-source-persistence.ts +7 -7
  228. package/src/three/authoring/r3f-design-session.ts +58 -54
  229. package/src/three/authoring/r3f-source-authoring-adapter.ts +95 -91
  230. package/src/three/authoring/reflection-probe-inspector-section.tsx +3 -2
  231. package/src/three/authoring/three-authoring-adapter.ts +29 -29
  232. package/src/three/component-verbs/extract-menu.ts +7 -6
  233. package/src/three/component-verbs/fork-menu.ts +7 -6
  234. package/src/three/component-verbs/internals-menu.ts +2 -2
  235. package/src/three/story-documents/three-story-documents.tsx +13 -13
  236. package/src/three/three-board/ThreeBoardDocument.tsx +13 -12
  237. package/src/three/three-board/board-scene.ts +6 -6
  238. package/src/three/three-board/three-component-board.ts +2 -2
  239. package/src/services/game-audio-unlock.ts +0 -48
@@ -0,0 +1,498 @@
1
+ /**
2
+ * The ordinary module surface a native game may expose to its own debugger.
3
+ *
4
+ * A root entry re-exports one `debug` object from the project's command module:
5
+ *
6
+ * export { debug } from './commands';
7
+ *
8
+ * The command module imports no vgai API. Its functions are the application's
9
+ * own functions over its own stores; the native adapter merely projects that
10
+ * existing registry onto the session debug/input doors after the root mounts.
11
+ * Loading through the root entry is load-bearing: the functions close over the
12
+ * exact module graph the mounted application uses, never a separately evaluated
13
+ * copy of its stores.
14
+ */
15
+
16
+ import type { VgaiGameSystemAdapters } from '@volter/editor-project/adapter/ingest/game-contract';
17
+ import type {
18
+ NativeCommandEntry,
19
+ NativeDebugBinding,
20
+ NativeDebugModule,
21
+ NativeInputValue,
22
+ NativeInputValueType,
23
+ NativeSystemsBinding,
24
+ NativeTable,
25
+ } from '@volter/editor-project/adapter/native-entry-surface';
26
+ import { NATIVE_INPUT_VALUE_TYPES } from '@volter/editor-project/adapter/native-entry-surface';
27
+ import {
28
+ DebugError,
29
+ type DebugRegistry,
30
+ type DebugVirtualInputTarget,
31
+ getDebugRegistry,
32
+ } from '../debug-registry';
33
+ import type { Game } from '../game';
34
+ import {
35
+ CONTRACT_SYSTEM_SLOTS,
36
+ type ContractSurface,
37
+ projectContractSystemAdapters,
38
+ } from './ingest/contract-system-adapters';
39
+
40
+ export type {
41
+ NativeCommandEntry,
42
+ NativeDebugBinding,
43
+ NativeDebugModule,
44
+ NativeInputValue,
45
+ NativeInputValueType,
46
+ NativeSystemsBinding,
47
+ NativeTable,
48
+ } from '@volter/editor-project/adapter/native-entry-surface';
49
+ export { NATIVE_INPUT_VALUE_TYPES } from '@volter/editor-project/adapter/native-entry-surface';
50
+
51
+ type NativeInputBinding = NonNullable<NativeDebugModule['input']>;
52
+
53
+ function isRecord(value: unknown): value is Record<string, unknown> {
54
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
55
+ }
56
+
57
+ function rejectUnknownKeys(value: Record<string, unknown>, allowed: readonly string[], at: string) {
58
+ const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
59
+ if (unknown.length > 0) {
60
+ throw new Error(
61
+ `${at} has unknown ${unknown.length === 1 ? 'key' : 'keys'}: ${unknown.join(', ')}`,
62
+ );
63
+ }
64
+ }
65
+
66
+ function functionRecord(
67
+ value: unknown,
68
+ at: string,
69
+ ): Record<string, (...args: unknown[]) => unknown> {
70
+ if (!isRecord(value)) throw new Error(`${at} must be an object of named functions.`);
71
+ const out: Record<string, (...args: unknown[]) => unknown> = {};
72
+ for (const [name, candidate] of Object.entries(value)) {
73
+ if (!name) throw new Error(`${at} contains an empty name.`);
74
+ if (typeof candidate !== 'function') throw new Error(`${at}.${name} must be a function.`);
75
+ out[name] = candidate as (...args: unknown[]) => unknown;
76
+ }
77
+ return out;
78
+ }
79
+
80
+ function parseActions(value: unknown): Record<string, NativeInputValueType> {
81
+ if (!isRecord(value)) {
82
+ throw new Error('debug.input.actions must be an object mapping action names to value types.');
83
+ }
84
+ const actions: Record<string, NativeInputValueType> = {};
85
+ for (const [name, valueType] of Object.entries(value)) {
86
+ if (!name) throw new Error('debug.input.actions contains an empty action name.');
87
+ if (!NATIVE_INPUT_VALUE_TYPES.includes(valueType as NativeInputValueType)) {
88
+ throw new Error(
89
+ `debug.input.actions.${name} must be one of ${NATIVE_INPUT_VALUE_TYPES.join(', ')}.`,
90
+ );
91
+ }
92
+ actions[name] = valueType as NativeInputValueType;
93
+ }
94
+ return actions;
95
+ }
96
+
97
+ function parseInput(value: unknown): NativeInputBinding {
98
+ if (!isRecord(value)) throw new Error('debug.input must be an object.');
99
+ rejectUnknownKeys(value, ['actions', 'set', 'clear', 'tap', 'scheduleAtTick'], 'debug.input');
100
+ if (typeof value['actions'] === 'function') {
101
+ // A live table: validated per read (requireAction/the actions source), not
102
+ // here — the whole point is that its content changes after install.
103
+ const set = value['set'];
104
+ const clear = value['clear'];
105
+ if (typeof set !== 'function') throw new Error('debug.input.set must be a function.');
106
+ if (typeof clear !== 'function') throw new Error('debug.input.clear must be a function.');
107
+ return value as unknown as NativeInputBinding;
108
+ }
109
+ const set = value['set'];
110
+ const clear = value['clear'];
111
+ const tap = value['tap'];
112
+ const scheduleAtTick = value['scheduleAtTick'];
113
+ if (typeof set !== 'function') throw new Error('debug.input.set must be a function.');
114
+ if (typeof clear !== 'function') throw new Error('debug.input.clear must be a function.');
115
+ if (tap !== undefined && typeof tap !== 'function') {
116
+ throw new Error('debug.input.tap must be a function when provided.');
117
+ }
118
+ if (scheduleAtTick !== undefined && typeof scheduleAtTick !== 'function') {
119
+ throw new Error('debug.input.scheduleAtTick must be a function when provided.');
120
+ }
121
+ return {
122
+ actions: parseActions(value['actions']),
123
+ set: set as NativeInputBinding['set'],
124
+ clear: clear as NativeInputBinding['clear'],
125
+ ...(typeof tap === 'function' ? { tap: tap as NonNullable<NativeInputBinding['tap']> } : {}),
126
+ ...(typeof scheduleAtTick === 'function'
127
+ ? {
128
+ scheduleAtTick: scheduleAtTick as NonNullable<NativeInputBinding['scheduleAtTick']>,
129
+ }
130
+ : {}),
131
+ };
132
+ }
133
+
134
+ /** Read the single native `debug` export from an already-loaded root module. */
135
+ export function nativeDebugBindingFromEntryModule(
136
+ rootId: string,
137
+ entryModule: unknown,
138
+ ): NativeDebugBinding | null {
139
+ if (!isRecord(entryModule) || entryModule['debug'] === undefined) return null;
140
+ if (!isRecord(entryModule['debug'])) {
141
+ throw new Error(`Root "${rootId}" exports \`debug\`, but it is not an object.`);
142
+ }
143
+ const raw = entryModule['debug'];
144
+ rejectUnknownKeys(
145
+ raw,
146
+ ['commands', 'state', 'input', 'events', 'settled'],
147
+ `Root "${rootId}" debug export`,
148
+ );
149
+
150
+ // A thunk table defers to install time (see {@link NativeTable}); a plain
151
+ // record validates here, where the error can still name the export.
152
+ const parseTable = (value: unknown, at: string) => {
153
+ if (value === undefined) return undefined;
154
+ if (typeof value === 'function') return value as () => Readonly<Record<string, never>>;
155
+ return functionRecord(value, at);
156
+ };
157
+ // Commands allow the `{ description?, run }` entry shape; full validation
158
+ // happens at install (`resolveCommandTable`), where thunks resolve too.
159
+ const parseCommands = (value: unknown, at: string) => {
160
+ if (value === undefined) return undefined;
161
+ if (typeof value === 'function') return value as () => Readonly<Record<string, never>>;
162
+ if (!isRecord(value)) throw new Error(`${at} must be an object of named commands.`);
163
+ return value as Readonly<Record<string, NativeCommandEntry>>;
164
+ };
165
+ const commands = parseCommands(raw['commands'], 'debug.commands');
166
+ const state = parseTable(raw['state'], 'debug.state');
167
+ const input = raw['input'] === undefined ? undefined : parseInput(raw['input']);
168
+ const events = raw['events'] === undefined ? undefined : parseEvents(raw['events']);
169
+ const settled = raw['settled'];
170
+ if (settled !== undefined && typeof settled !== 'function') {
171
+ throw new Error('debug.settled must be a function returning a boolean.');
172
+ }
173
+ if (!commands && !state && !input && !events && !settled) {
174
+ throw new Error(`Root "${rootId}" exports an empty \`debug\` object.`);
175
+ }
176
+ return {
177
+ rootId,
178
+ debug: {
179
+ ...(commands ? { commands } : {}),
180
+ ...(state ? { state } : {}),
181
+ ...(input ? { input } : {}),
182
+ ...(events ? { events } : {}),
183
+ ...(settled ? { settled: settled as () => boolean } : {}),
184
+ },
185
+ };
186
+ }
187
+
188
+ function parseEvents(value: unknown): NonNullable<NativeDebugModule['events']> {
189
+ if (!isRecord(value) || typeof value['subscribe'] !== 'function') {
190
+ throw new Error('debug.events must be an object with a subscribe(listener) function.');
191
+ }
192
+ return value as unknown as NonNullable<NativeDebugModule['events']>;
193
+ }
194
+
195
+ /** Resolve a possibly-thunk COMMAND table, validating each entry's shape. */
196
+ function resolveCommandTable(
197
+ table: NativeTable<NativeCommandEntry> | undefined,
198
+ at: string,
199
+ ): Readonly<Record<string, NativeCommandEntry>> {
200
+ if (table === undefined) return {};
201
+ const raw = typeof table === 'function' ? table() : table;
202
+ if (!isRecord(raw)) throw new Error(`${at} must be an object of named commands.`);
203
+ for (const [name, entry] of Object.entries(raw)) {
204
+ if (!name) throw new Error(`${at} contains an empty name.`);
205
+ const ok =
206
+ typeof entry === 'function' ||
207
+ (isRecord(entry) && typeof (entry as { run?: unknown }).run === 'function');
208
+ if (!ok) throw new Error(`${at}.${name} must be a function or { description?, run }.`);
209
+ }
210
+ return raw as Readonly<Record<string, NativeCommandEntry>>;
211
+ }
212
+
213
+ /** Resolve a possibly-thunk table at install time, validating the result. */
214
+ function resolveTable<T>(
215
+ table: NativeTable<T> | undefined,
216
+ at: string,
217
+ ): Readonly<Record<string, T>> {
218
+ if (table === undefined) return {};
219
+ if (typeof table === 'function') {
220
+ return functionRecord(table(), at) as Readonly<Record<string, T>>;
221
+ }
222
+ return table;
223
+ }
224
+
225
+ function valueMatches(type: NativeInputValueType, value: NativeInputValue): boolean {
226
+ if (type === 'digital') return typeof value === 'boolean';
227
+ if (type === 'scalar') return typeof value === 'number' && Number.isFinite(value);
228
+ return (
229
+ typeof value === 'object' &&
230
+ value !== null &&
231
+ typeof value.x === 'number' &&
232
+ Number.isFinite(value.x) &&
233
+ typeof value.y === 'number' &&
234
+ Number.isFinite(value.y)
235
+ );
236
+ }
237
+
238
+ function installInput(
239
+ game: Game,
240
+ registry: DebugRegistry,
241
+ binding: NativeDebugBinding,
242
+ input: NativeInputBinding,
243
+ ): void {
244
+ const trace: Array<{ tick: number; action: string; value: NativeInputValue }> = [];
245
+ let recording = false;
246
+
247
+ // LIVE read on every use — see the `actions` field's own comment.
248
+ const liveActions = (): Readonly<Record<string, NativeInputValueType>> =>
249
+ typeof input.actions === 'function' ? input.actions() : input.actions;
250
+
251
+ const requireAction = (action: string, value: NativeInputValue): NativeInputValueType => {
252
+ const actions = liveActions();
253
+ const type = actions[action];
254
+ if (!type) {
255
+ throw new DebugError(
256
+ 'INPUT_ACTION_NOT_FOUND',
257
+ `Native input has no action named "${action}".`,
258
+ {
259
+ registered: Object.keys(actions),
260
+ },
261
+ );
262
+ }
263
+ if (!valueMatches(type, value)) {
264
+ throw new DebugError(
265
+ 'INPUT_ACTION_VALUE_TYPE',
266
+ `Native input action "${action}" expects ${type}, not ${typeof value}.`,
267
+ { action, expected: type },
268
+ );
269
+ }
270
+ return type;
271
+ };
272
+
273
+ const apply = (action: string, value: NativeInputValue, tick: number): void => {
274
+ requireAction(action, value);
275
+ input.set(action, value);
276
+ if (recording) trace.push({ tick, action, value });
277
+ };
278
+
279
+ const unsupportedSource = (method: string): never => {
280
+ throw new DebugError(
281
+ 'NATIVE_INPUT_SOURCE_UNSUPPORTED',
282
+ `${method} addresses an engine test source, but this game exposes an app-owned action store. Drive it by action name instead.`,
283
+ );
284
+ };
285
+ const target: DebugVirtualInputTarget = {
286
+ setVirtualAction(action, value) {
287
+ apply(action, value, registry.getGameTick());
288
+ return { delivered: true };
289
+ },
290
+ tapVirtualAction(action) {
291
+ if (!input.tap) {
292
+ throw new DebugError(
293
+ 'NATIVE_INPUT_TAP_UNSUPPORTED',
294
+ `Native input for root "${binding.rootId}" does not export tap(); use setVirtualAction through the app-owned store.`,
295
+ );
296
+ }
297
+ requireAction(action, true);
298
+ input.tap(action);
299
+ return { delivered: true };
300
+ },
301
+ clearVirtualActions() {
302
+ input.clear();
303
+ },
304
+ scheduleActionAtTick(tick, action, value) {
305
+ const currentTick = registry.getGameTick();
306
+ if (!Number.isInteger(tick) || tick < currentTick) {
307
+ throw new DebugError(
308
+ 'TICK_ALREADY_PASSED',
309
+ `Cannot schedule native input for tick ${tick}; current tick is ${currentTick}.`,
310
+ { tick, currentTick },
311
+ );
312
+ }
313
+ requireAction(action, value);
314
+ if (!input.scheduleAtTick) {
315
+ throw new DebugError(
316
+ 'NATIVE_INPUT_SCHEDULE_UNSUPPORTED',
317
+ `Native input for root "${binding.rootId}" does not export scheduleAtTick(); the adapter will not create a host scheduler for it.`,
318
+ );
319
+ }
320
+ input.scheduleAtTick(tick, action, value);
321
+ },
322
+ startInputRecording() {
323
+ trace.length = 0;
324
+ recording = true;
325
+ },
326
+ stopInputRecording() {
327
+ recording = false;
328
+ },
329
+ isInputRecording() {
330
+ return recording;
331
+ },
332
+ injectAxis() {
333
+ unsupportedSource('injectAxis');
334
+ },
335
+ injectVector2() {
336
+ unsupportedSource('injectVector2');
337
+ },
338
+ injectPointerDelta() {
339
+ unsupportedSource('injectPointerDelta');
340
+ },
341
+ injectPointerPosition() {
342
+ unsupportedSource('injectPointerPosition');
343
+ },
344
+ };
345
+
346
+ registry.setInputActionsSource(binding.rootId, () =>
347
+ Object.entries(liveActions()).map(([name, valueType]) => ({ name, valueType })),
348
+ );
349
+ registry.setInputTraceSource(binding.rootId, () => ({
350
+ version: 1,
351
+ seed: null,
352
+ fixedDt: game.loop.fixedDt,
353
+ ticks: [...trace],
354
+ }));
355
+ registry.setVirtualInputTarget(binding.rootId, target);
356
+ }
357
+
358
+ /** Project native registries onto the existing session debugger after mount. */
359
+ export function installNativeDebugBindings(
360
+ game: Game,
361
+ bindings: readonly NativeDebugBinding[],
362
+ ): void {
363
+ if (bindings.length === 0) return;
364
+ const registry = getDebugRegistry(game);
365
+ if (!registry)
366
+ throw new Error('Cannot install native debug bindings: mounted Game has no debug registry.');
367
+ for (const binding of bindings) {
368
+ if (!game.world(binding.rootId)) {
369
+ throw new Error(`Native debug binding names unmounted root "${binding.rootId}".`);
370
+ }
371
+ const root = registry.forRoot(binding.rootId);
372
+ const state = resolveTable(binding.debug.state, `Root "${binding.rootId}" debug.state`);
373
+ for (const [name, read] of Object.entries(state)) {
374
+ root.registerStateProvider(name, read, { tier: 'assisted' });
375
+ }
376
+ const commands = resolveCommandTable(
377
+ binding.debug.commands,
378
+ `Root "${binding.rootId}" debug.commands`,
379
+ );
380
+ for (const [name, entry] of Object.entries(commands)) {
381
+ const run = typeof entry === 'function' ? entry : entry.run;
382
+ const description = typeof entry === 'function' ? undefined : entry.description;
383
+ root.registerCommand(
384
+ name,
385
+ { locus: 'client', ...(description === undefined ? {} : { description }) },
386
+ run,
387
+ );
388
+ }
389
+ if (binding.debug.input) installInput(game, registry, binding, binding.debug.input);
390
+ // The app's emitter → the session's tick-stamped event log. The
391
+ // subscription's disposer is deliberately dropped: it lives exactly as
392
+ // long as this Game's registry does.
393
+ binding.debug.events?.subscribe((event, detail) => root.emit(event, detail));
394
+ // The app's own between-worlds truth → the session's tick gate
395
+ // (`runTicksWhenSettled`). Same lifetime rule as the event subscription.
396
+ if (binding.debug.settled) registry.registerWorldSettledProbe(binding.debug.settled);
397
+ }
398
+ }
399
+
400
+ // ---------------------------------------------------------------------------
401
+ // The native `systems` export — the first-party door onto `SystemAdapters`
402
+ // ---------------------------------------------------------------------------
403
+ //
404
+ // The sibling of `debug` on the SAME module surface: a root entry re-exports
405
+ // one `systems` object beside it —
406
+ //
407
+ // export { debug, systems } from './commands';
408
+ //
409
+ // — whose slots are the ONE declarable carrier both realms share,
410
+ // `VgaiGameSystemAdapters` (`ingest/game-contract.ts`; the native engine is
411
+ // the premade 100% implementation of that contract). Validation is the SAME
412
+ // projection the ingest realm uses (`ingest/contract-system-adapters.ts`), so
413
+ // there is one shape law, not two. This door is what retires
414
+ // `ctx.registerSystemAdapter` from component code (ARCHITECTURE-CORE §System
415
+ // adapters: "a project's `vgai.adapter.ts` binds app-owned systems through
416
+ // declared native exports … Components never call `registerSystemAdapter`").
417
+ //
418
+ // Native-realm difference from ingest: a malformed slot THROWS (this is our
419
+ // own code failing its own contract — fail fast), where the ingest projection
420
+ // files a verdict for the coverage report. A `{ present: false, evidence }`
421
+ // slot is accepted as the positive absence it is and binds nothing.
422
+
423
+ /**
424
+ * Read the single native `systems` export from an already-loaded root module.
425
+ * `surface` is the root's own mount surface when the caller knows it — it
426
+ * feeds the physics keying check (`PHYSICS_KEYING_BY_SURFACE`).
427
+ */
428
+ export function nativeSystemsBindingFromEntryModule(
429
+ rootId: string,
430
+ entryModule: unknown,
431
+ surface?: ContractSurface | undefined,
432
+ ): NativeSystemsBinding | null {
433
+ if (!isRecord(entryModule) || entryModule['systems'] === undefined) return null;
434
+ const raw = entryModule['systems'];
435
+ if (!isRecord(raw)) {
436
+ throw new Error(`Root "${rootId}" exports \`systems\`, but it is not an object.`);
437
+ }
438
+ rejectUnknownKeys(raw, CONTRACT_SYSTEM_SLOTS, `Root "${rootId}" systems export`);
439
+ const projection = projectContractSystemAdapters(
440
+ { systemAdapters: raw as VgaiGameSystemAdapters },
441
+ surface,
442
+ );
443
+ if (projection.malformed.length > 0) {
444
+ throw new Error(
445
+ `Root "${rootId}" systems export is malformed: ` +
446
+ projection.malformed.map((slot) => `${slot.slot} — ${slot.reason}`).join('; '),
447
+ );
448
+ }
449
+ if (Object.keys(projection.bound).length === 0 && projection.empty.length === 0) {
450
+ throw new Error(`Root "${rootId}" exports an empty \`systems\` object.`);
451
+ }
452
+ return { rootId, slots: projection.bound, absent: projection.empty };
453
+ }
454
+
455
+ /**
456
+ * Install every declared binding onto the mounted Game's game-scoped slot
457
+ * table (`Game.installDeclaredSystemAdapters`), where the ordinary
458
+ * `game.systemAdapters` merge picks them up ahead of any lingering component
459
+ * registration for the same root.
460
+ *
461
+ * A root's ABSENCES travel the same call and install NOTHING — they are
462
+ * recorded, never bound, because the whole point of `absent()` is that no
463
+ * adapter exists to bind (see its comment: a marker, never a stub). They are
464
+ * recorded HERE rather than left on the binding alone so the answer is
465
+ * game-scoped, matching the registry every editor panel already reads: a
466
+ * `SystemAdapters` slot is filled by whichever root builds it, so "does this
467
+ * GAME have physics" cannot be answered one root at a time.
468
+ *
469
+ * A root that declares only absences therefore still calls through — skipping
470
+ * it on an empty `slots` map is what would drop exactly the games whose whole
471
+ * declaration is "I have none of these".
472
+ */
473
+ export function installNativeSystemsBindings(
474
+ game: Game,
475
+ bindings: readonly NativeSystemsBinding[],
476
+ ): void {
477
+ if (bindings.length === 0) return;
478
+ const install = game.installDeclaredSystemAdapters;
479
+ if (!install) {
480
+ throw new Error(
481
+ 'Cannot install native systems bindings: this Game does not implement ' +
482
+ 'installDeclaredSystemAdapters.',
483
+ );
484
+ }
485
+ for (const binding of bindings) {
486
+ if (Object.keys(binding.slots).length === 0 && binding.absent.length === 0) continue;
487
+ install.call(
488
+ game,
489
+ binding.rootId,
490
+ binding.slots,
491
+ binding.absent.map((slot) => ({
492
+ rootId: binding.rootId,
493
+ slot: slot.slot,
494
+ reason: slot.evidence,
495
+ })),
496
+ );
497
+ }
498
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * A named-bus mixer over Web Audio: buses created on demand, routed into each
3
+ * other by name, and set in DECIBELS.
4
+ *
5
+ * Every mixer a game reaches for is this shape — Unity's `AudioMixerGroup`,
6
+ * Godot's bus layout, FMOD's VCAs, and the `music`/`sfx`/`voice` trio
7
+ * `setup/setup-audio.ts` hands a first-party three world. What they share is
8
+ * not the protocol (that part is genuinely per-ecosystem and stays with the
9
+ * lane that speaks it) but the machinery underneath: a `GainNode` per bus, a
10
+ * name → node map, a parent to route into, and the one conversion below. This
11
+ * file owns exactly that, and nothing that names an ecosystem.
12
+ *
13
+ * ## dB is not a gain, and the mistake is silent
14
+ *
15
+ * `GainNode.gain` is a LINEAR amplitude multiplier; every mixer's authored
16
+ * volume is logarithmic dB, where 0 dB is unity and −80 dB is silence. Writing
17
+ * the dB number straight onto `gain.value` gives you a gain of −80 (a phase
18
+ * inversion at 80x) where the author asked for silence, and it does not throw,
19
+ * warn, or look wrong in a debugger — it is just loud. {@link dbToLinear} is
20
+ * the one conversion, `10^(dB/20)`, which is both Godot's `db_to_linear`
21
+ * (`core/math/math_funcs.h`: `exp(db * 0.11512925464970228…)`, the same
22
+ * function written in base e) and Unity's mixer volume curve.
23
+ *
24
+ * It deliberately does NOT clamp. A mixer's RANGE is protocol — Unity's
25
+ * exposed parameters saturate at its slider's ends, Godot's `volume_db` does
26
+ * not — so the lane that knows the range applies it before calling here, and
27
+ * this file cannot silently mute or silently boost anyone's authored value.
28
+ *
29
+ * ## Create-on-demand, and why it is the contract rather than a convenience
30
+ *
31
+ * {@link AudioBusMixer.bus} MINTS a bus the first time it is named and returns
32
+ * the SAME node forever after. The identity is the contract, not the minting:
33
+ * a source connects to a bus long before anything sets that bus's volume, and
34
+ * a parent is usually named by its child before anyone asks for it directly
35
+ * (`bus('music', 'master')` builds `master` too, and the later `bus('master')`
36
+ * must be that same node). A `bus()` that minted per call would hand the
37
+ * volume control a SECOND node — the sources would keep feeding the first one,
38
+ * every later write would land on a node nothing plays through, and nothing
39
+ * would throw or look wrong. So the map is authoritative, and the only way a
40
+ * name stops resolving to its node is {@link AudioBusMixer.remove} or
41
+ * {@link AudioBusMixer.disconnect}.
42
+ *
43
+ * A re-ask that names a DIFFERENT parent throws by name. Routing is decided at
44
+ * creation (the node is already connected by then), so honouring the second
45
+ * parent would mean silently re-routing live audio and ignoring it would mean
46
+ * silently keeping the first — a loud error is the only reading that is not a
47
+ * lie.
48
+ *
49
+ * ## RESOURCE OWNERSHIP
50
+ *
51
+ * No module-scoped state: {@link createAudioBusMixer} is a factory and the
52
+ * OWNER of an instance is whoever called it (a `setupAudio` graph, one
53
+ * translated port's mixer store). What an instance ALLOCATES is one `GainNode`
54
+ * per named bus, and disconnecting those is the whole of its teardown
55
+ * obligation — {@link AudioBusMixer.disconnect}, called once by that same
56
+ * owner. The `context` and the `destination` node are NOT its own: they are
57
+ * passed in, shared with everything else hanging off the same graph, and this
58
+ * file never creates, closes, suspends or disconnects either one.
59
+ */
60
+
61
+ /**
62
+ * The graph an {@link AudioBusMixer} hangs off, as the two members it touches.
63
+ *
64
+ * Narrow on purpose: `createGain` is all it needs from the context, so a real
65
+ * `AudioContext`, an `OfflineAudioContext` and a headless stand-in are all
66
+ * acceptable without a cast.
67
+ */
68
+ export interface AudioBusMixerOptions {
69
+ readonly context: { createGain(): GainNode };
70
+ /** The node every parentless bus feeds. Usually a master gain or the context's destination. */
71
+ readonly destination: AudioNode;
72
+ }
73
+
74
+ export interface AudioBusMixer {
75
+ /**
76
+ * The bus named `name`, created on the first ask and connected to `parent`
77
+ * (itself created on demand) or to the destination.
78
+ *
79
+ * The returned node is the real `GainNode` — set `gain.value`, schedule
80
+ * automation on `gain`, connect sources to it. Nothing here stands between a
81
+ * caller and Web Audio's own API.
82
+ */
83
+ bus(name: string, parent?: string): GainNode;
84
+ /** The bus named `name` if it EXISTS, else null. Never mints one. */
85
+ find(name: string): GainNode | null;
86
+ /** Set this bus's gain from decibels, creating it if absent. Clamp before calling — see the header. */
87
+ setDb(name: string, db: number, parent?: string): void;
88
+ /**
89
+ * Disconnect one bus and forget its name, so a later {@link bus} mints a
90
+ * fresh node. Buses that named it as their parent are NOT re-routed — they
91
+ * keep feeding the removed node, which now reaches nothing.
92
+ */
93
+ remove(name: string): void;
94
+ /** Disconnect every bus and forget them all. The owner's ONE teardown. */
95
+ disconnect(): void;
96
+ }
97
+
98
+ /** dB → linear amplitude: `10^(dB/20)`. 0 dB is 1, −6 dB is ~0.5, −80 dB is ~0.0001. Never clamps. */
99
+ export function dbToLinear(db: number): number {
100
+ return 10 ** (db / 20);
101
+ }
102
+
103
+ interface BusRecord {
104
+ readonly node: GainNode;
105
+ readonly parent: string | undefined;
106
+ }
107
+
108
+ function safeDisconnect(node: GainNode): void {
109
+ try {
110
+ node.disconnect();
111
+ } catch {
112
+ // already disconnected
113
+ }
114
+ }
115
+
116
+ /** Create one named-bus mixer. See the header for what owns the instance and what it allocates. */
117
+ export function createAudioBusMixer(options: AudioBusMixerOptions): AudioBusMixer {
118
+ const { context, destination } = options;
119
+ const buses = new Map<string, BusRecord>();
120
+
121
+ function bus(name: string, parent?: string): GainNode {
122
+ const existing = buses.get(name);
123
+ if (existing !== undefined) {
124
+ if (parent !== undefined && parent !== existing.parent) {
125
+ throw new Error(
126
+ `audio bus ${JSON.stringify(name)} already routes into ` +
127
+ `${existing.parent === undefined ? 'the destination' : JSON.stringify(existing.parent)}` +
128
+ `; it cannot also route into ${JSON.stringify(parent)}. Routing is decided when a bus ` +
129
+ 'is created (see packages/editor-game/src/runtime/audio/bus-mixer.ts).',
130
+ );
131
+ }
132
+ return existing.node;
133
+ }
134
+ // The parent is resolved (and minted, recursively) before this bus is
135
+ // seated, so a conflicting-parent throw upstream leaves nothing half-built.
136
+ const into = parent === undefined ? destination : bus(parent);
137
+ const node = context.createGain();
138
+ node.gain.value = 1;
139
+ node.connect(into);
140
+ buses.set(name, { node, parent });
141
+ return node;
142
+ }
143
+
144
+ return {
145
+ bus,
146
+ find: (name) => buses.get(name)?.node ?? null,
147
+ setDb: (name, db, parent) => {
148
+ bus(name, parent).gain.value = dbToLinear(db);
149
+ },
150
+ remove: (name) => {
151
+ const record = buses.get(name);
152
+ if (record === undefined) return;
153
+ buses.delete(name);
154
+ safeDisconnect(record.node);
155
+ },
156
+ disconnect: () => {
157
+ for (const record of buses.values()) safeDisconnect(record.node);
158
+ buses.clear();
159
+ },
160
+ };
161
+ }