@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.
Files changed (302) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +23 -0
  3. package/contributions/asset-budget-asset.menu.ts +26 -0
  4. package/contributions/asset-budget.action.ts +18 -0
  5. package/contributions/asset-budget.document.tsx +21 -0
  6. package/contributions/asset-budget.menu.ts +22 -0
  7. package/contributions/audio-unlock.service.ts +17 -0
  8. package/contributions/audio.utility.tsx +15 -0
  9. package/contributions/autoplay.service.ts +48 -0
  10. package/contributions/bridge.command.ts +172 -0
  11. package/contributions/build-profiles.document.tsx +21 -0
  12. package/contributions/build-progress.status.tsx +45 -0
  13. package/contributions/build.action.ts +26 -0
  14. package/contributions/build.command.ts +27 -0
  15. package/contributions/build.header.tsx +37 -0
  16. package/contributions/build.menu.ts +31 -0
  17. package/contributions/build.service.ts +31 -0
  18. package/contributions/connection.status.tsx +43 -0
  19. package/contributions/coverage.service.ts +118 -0
  20. package/contributions/edit-mode-audio.service.ts +23 -0
  21. package/contributions/edit-mode-networking.service.ts +30 -0
  22. package/contributions/game-document.service.ts +25 -0
  23. package/contributions/game-eval.command.ts +210 -0
  24. package/contributions/game.layout.ts +19 -0
  25. package/contributions/gameplay.command.ts +255 -0
  26. package/contributions/generation.service.ts +96 -0
  27. package/contributions/generations.status.tsx +53 -0
  28. package/contributions/ingest.service.ts +80 -0
  29. package/contributions/instances.command.ts +75 -0
  30. package/contributions/navmesh.menu.ts +39 -0
  31. package/contributions/navmesh.service.ts +19 -0
  32. package/contributions/network.utility.tsx +17 -0
  33. package/contributions/play.command.ts +355 -0
  34. package/contributions/profiler.action.ts +16 -0
  35. package/contributions/profiler.menu.ts +22 -0
  36. package/contributions/profiler.utility.tsx +17 -0
  37. package/contributions/react/component-board.service.ts +21 -0
  38. package/contributions/react/design-time-mount.service.ts +60 -0
  39. package/contributions/react/pasteboard.action.ts +41 -0
  40. package/contributions/react/react-inspector.service.ts +85 -0
  41. package/contributions/react/story-documents.service.ts +44 -0
  42. package/contributions/scene-document.service.ts +32 -0
  43. package/contributions/state-watch.action.ts +17 -0
  44. package/contributions/state-watch.menu.ts +23 -0
  45. package/contributions/state-watch.utility.tsx +20 -0
  46. package/contributions/team-playtest.service.ts +124 -0
  47. package/contributions/three/camera-runtime.inspector.tsx +34 -0
  48. package/contributions/three/component-board.service.ts +24 -0
  49. package/contributions/three/component-verbs.command.ts +110 -0
  50. package/contributions/three/component-verbs.service.ts +92 -0
  51. package/contributions/three/constraints.inspector.tsx +34 -0
  52. package/contributions/three/model-asset-sections.service.ts +26 -0
  53. package/contributions/three/reflection-probe-capture.inspector.tsx +32 -0
  54. package/contributions/three/story-documents.service.ts +31 -0
  55. package/contributions/three/three-authoring.service.ts +66 -0
  56. package/contributions/transport.header.tsx +19 -0
  57. package/contributions/xstate-behavior.action.ts +42 -0
  58. package/contributions/xstate-behavior.document.tsx +73 -0
  59. package/contributions/xstate-behavior.inspector.tsx +28 -0
  60. package/contributions/xstate-behavior.menu.ts +23 -0
  61. package/package.json +144 -0
  62. package/src/asset-budget/AssetBudgetPanel.tsx +1172 -0
  63. package/src/asset-budget/asset-budget-model.ts +799 -0
  64. package/src/asset-budget/basis-encoder.ts +165 -0
  65. package/src/asset-budget/gltf-io.ts +154 -0
  66. package/src/asset-budget/gltf-optimize.ts +384 -0
  67. package/src/asset-budget/image-dims.ts +78 -0
  68. package/src/asset-budget/optimize-apply.ts +221 -0
  69. package/src/audio/AudioDebuggerPanel.tsx +457 -0
  70. package/src/audio/audio-debugger-model.ts +87 -0
  71. package/src/bridge/call.ts +60 -0
  72. package/src/bridge/dispatch.ts +459 -0
  73. package/src/bridge/live-frames.ts +25 -0
  74. package/src/bridge/screenshot.ts +316 -0
  75. package/src/build/BuildProfilesPanel.tsx +476 -0
  76. package/src/build/build-session.ts +247 -0
  77. package/src/build/format-bytes.ts +14 -0
  78. package/src/command-results.ts +36 -0
  79. package/src/coverage/live-authoring-surface.ts +30 -0
  80. package/src/coverage/live-project-verbs.ts +162 -0
  81. package/src/coverage/native-system-coverage.ts +143 -0
  82. package/src/coverage/root-coverage.ts +79 -0
  83. package/src/coverage/session-coverage.ts +193 -0
  84. package/src/design-system-stories/ApplicationChrome.stories.tsx +100 -0
  85. package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +406 -0
  86. package/src/edit-mode/edit-mode-audio.ts +56 -0
  87. package/src/edit-mode/edit-mode-networking.ts +112 -0
  88. package/src/game-document/DevicePresetPicker.tsx +84 -0
  89. package/src/game-document/GameCaptureFrameButton.tsx +45 -0
  90. package/src/game-document/GameDocument.tsx +215 -0
  91. package/src/game-document/GamePanel.tsx +662 -0
  92. package/src/game-document/InstanceInspectorPicker.tsx +183 -0
  93. package/src/game-document/crowd-debug.ts +183 -0
  94. package/src/game-document/device-preview.ts +336 -0
  95. package/src/game-document/game-view-store.ts +107 -0
  96. package/src/game-document/physics-debug.ts +187 -0
  97. package/src/generation/GenerationActivity.tsx +421 -0
  98. package/src/generation/GenerationGallery.css +231 -0
  99. package/src/generation/generation-documents.tsx +338 -0
  100. package/src/generation/generation-jobs.ts +128 -0
  101. package/src/generation/generation-presentation.ts +257 -0
  102. package/src/host/adapter-reach.ts +445 -0
  103. package/src/host/adapter-runtime-bindings.ts +303 -0
  104. package/src/host/after-paint.ts +112 -0
  105. package/src/host/api/configurations.ts +66 -0
  106. package/src/host/authoring/babylon-authoring-adapter.ts +703 -0
  107. package/src/host/authoring/canvas-runtime-recognition.ts +50 -0
  108. package/src/host/authoring/contract-hierarchy-authoring.ts +144 -0
  109. package/src/host/authoring/contract-scenes-stories.ts +204 -0
  110. package/src/host/authoring/creation-site-related.ts +55 -0
  111. package/src/host/authoring/ephemeral-persistence.ts +29 -0
  112. package/src/host/authoring/gesture-persist.ts +84 -0
  113. package/src/host/authoring/ingest-data-writer.ts +232 -0
  114. package/src/host/authoring/ingest-source-persistence.ts +826 -0
  115. package/src/host/authoring/mount-isolated-pixi-screen.ts +250 -0
  116. package/src/host/authoring/mounted-authoring.ts +41 -0
  117. package/src/host/authoring/owned-pixi-ticker-listeners.ts +96 -0
  118. package/src/host/authoring/phaser-live-authoring-adapter.ts +265 -0
  119. package/src/host/authoring/pixi-authoring-adapter.ts +1554 -0
  120. package/src/host/authoring/pixi-creation-site-write-target.ts +60 -0
  121. package/src/host/authoring/pixi-isolation-assets.ts +25 -0
  122. package/src/host/authoring/pixi-live-write-target.ts +979 -0
  123. package/src/host/authoring/pixi-source-identity.ts +141 -0
  124. package/src/host/authoring/pixi-still-presentation.ts +71 -0
  125. package/src/host/authoring/pixi-structure-history.ts +237 -0
  126. package/src/host/authoring/pixi-transform-channels.ts +205 -0
  127. package/src/host/authoring/selection-remount-handoff.ts +23 -0
  128. package/src/host/authoring/source-persistence-backend.ts +373 -0
  129. package/src/host/authoring/source-refresh-revisions.ts +81 -0
  130. package/src/host/authoring/struct-write-pipe.ts +143 -0
  131. package/src/host/auto-frame-window.ts +89 -0
  132. package/src/host/binding-resolver.ts +393 -0
  133. package/src/host/browser-transpile.ts +631 -0
  134. package/src/host/canvas-entry-runtime.ts +95 -0
  135. package/src/host/components/CameraAuthoringOverlay.tsx +216 -0
  136. package/src/host/components/HeaderTelemetry.tsx +341 -0
  137. package/src/host/components/PixiIsolationSceneContent.tsx +280 -0
  138. package/src/host/components/ResolutionPicker.tsx +89 -0
  139. package/src/host/components/ThreeIsolationSceneContent.tsx +180 -0
  140. package/src/host/components/frame-debugger-model.ts +579 -0
  141. package/src/host/components/header-telemetry-model.ts +74 -0
  142. package/src/host/components/scene-document.tsx +447 -0
  143. package/src/host/components/utility-view-state.ts +87 -0
  144. package/src/host/components/world-root-stage-binding.tsx +133 -0
  145. package/src/host/components/world-root-stage.ts +1046 -0
  146. package/src/host/coverage/authoring-read-probe.ts +583 -0
  147. package/src/host/coverage/capability-coverage.ts +1587 -0
  148. package/src/host/coverage/coverage-accounting.ts +261 -0
  149. package/src/host/coverage/game-contract-seam-evidence.ts +82 -0
  150. package/src/host/coverage/project-verb-coverage.ts +148 -0
  151. package/src/host/coverage/system-adapter-coverage.ts +373 -0
  152. package/src/host/design-system-stories/StoryLayout.tsx +104 -0
  153. package/src/host/design-system-stories/fixtures/authoring.ts +247 -0
  154. package/src/host/design-system-stories/fixtures/editor-runtime.tsx +115 -0
  155. package/src/host/document-preview-three.ts +110 -0
  156. package/src/host/entry-adjudication.ts +89 -0
  157. package/src/host/game-location-guard.ts +150 -0
  158. package/src/host/game-module-access.ts +196 -0
  159. package/src/host/game-realm-page.ts +358 -0
  160. package/src/host/game-realm-reclaim.ts +55 -0
  161. package/src/host/game-realm-storage.ts +104 -0
  162. package/src/host/gameplay-export.ts +288 -0
  163. package/src/host/gameplay-recording.ts +717 -0
  164. package/src/host/gated-globals.ts +1511 -0
  165. package/src/host/history/json-history-resource.ts +254 -0
  166. package/src/host/ingest/registry.ts +261 -0
  167. package/src/host/instance-extract-actions.ts +120 -0
  168. package/src/host/instance-fork-actions.ts +120 -0
  169. package/src/host/play-control-hook.ts +26 -0
  170. package/src/host/playwright-shim.ts +479 -0
  171. package/src/host/projection/dom.ts +295 -0
  172. package/src/host/projection/pixi.ts +288 -0
  173. package/src/host/r3f-entry-runtime.ts +77 -0
  174. package/src/host/react-mount-runtime.ts +162 -0
  175. package/src/host/realm-services.ts +148 -0
  176. package/src/host/recording-preview.ts +106 -0
  177. package/src/host/roots/module-root.ts +203 -0
  178. package/src/host/roots/react-root.ts +181 -0
  179. package/src/host/same-realm-loop-gate.ts +544 -0
  180. package/src/host/scene-view-drawability.ts +69 -0
  181. package/src/host/sdk/tools.ts +31 -0
  182. package/src/host/served-bundle-runtime-modules.ts +331 -0
  183. package/src/host/server-log-bridge.ts +60 -0
  184. package/src/host/staged-projects.ts +24 -0
  185. package/src/host/stories/mounted-story-viewport-source.ts +64 -0
  186. package/src/host/stories/story-arg-descriptors.ts +50 -0
  187. package/src/host/stories/story-media-presence.ts +152 -0
  188. package/src/host/surface-content.ts +87 -0
  189. package/src/host/take-named-export.ts +23 -0
  190. package/src/host/three-ingest-runtime.ts +76 -0
  191. package/src/host/types-fastnoise-lite.d.ts +7 -0
  192. package/src/host/types-mikktspace.d.ts +20 -0
  193. package/src/host/types-troika-three-text.d.ts +7 -0
  194. package/src/host/use-active-performance-source.ts +53 -0
  195. package/src/host/viewport-pose-memory.ts +48 -0
  196. package/src/host/viewport-root-presentation.ts +40 -0
  197. package/src/ingest/active-ingest.ts +251 -0
  198. package/src/ingest/active-scene-navigation.ts +43 -0
  199. package/src/ingest/authoring/ingest-dom-surface-authoring.ts +191 -0
  200. package/src/ingest/authoring/ingest-root-adapter.ts +897 -0
  201. package/src/ingest/capture-wait-report.ts +122 -0
  202. package/src/ingest/deferred-ingest-play.ts +216 -0
  203. package/src/ingest/deferred-ingest-session.ts +23 -0
  204. package/src/ingest/discovery-public-ingest.ts +242 -0
  205. package/src/ingest/dom-stub-mark.ts +7 -0
  206. package/src/ingest/entry-load.ts +56 -0
  207. package/src/ingest/game-contract-realm.ts +34 -0
  208. package/src/ingest/game-pointer-lock.ts +89 -0
  209. package/src/ingest/held-scene-repaint.ts +73 -0
  210. package/src/ingest/host-surface-box.ts +174 -0
  211. package/src/ingest/ingest-boot-viewport.ts +54 -0
  212. package/src/ingest/ingest-canvas-scene-document.tsx +177 -0
  213. package/src/ingest/ingest-canvas-scene.ts +50 -0
  214. package/src/ingest/ingest-evidence-hook.ts +131 -0
  215. package/src/ingest/ingest-frame-snapshot.ts +183 -0
  216. package/src/ingest/ingest-play-commands.ts +134 -0
  217. package/src/ingest/ingest-play-control.ts +299 -0
  218. package/src/ingest/ingest-render-debug.ts +320 -0
  219. package/src/ingest/ingest-siblings.ts +487 -0
  220. package/src/ingest/ingest-status.ts +62 -0
  221. package/src/ingest/live-ingest-facet.ts +46 -0
  222. package/src/ingest/module-mode.ts +229 -0
  223. package/src/ingest/mount-canvas-ingest-root.ts +899 -0
  224. package/src/ingest/mount-coverage.ts +296 -0
  225. package/src/ingest/mount-dom-ingest-root.ts +282 -0
  226. package/src/ingest/mount-ingest-root.ts +744 -0
  227. package/src/ingest/mount-three-ingest-root.ts +366 -0
  228. package/src/ingest/resolve-canvas.ts +47 -0
  229. package/src/ingest/resolve-three.ts +123 -0
  230. package/src/ingest/served-bundle.ts +109 -0
  231. package/src/ingest/served-html-boot.ts +292 -0
  232. package/src/ingest/surface-canvas.ts +84 -0
  233. package/src/ingest/surface-dom.ts +84 -0
  234. package/src/ingest/surface-three.ts +128 -0
  235. package/src/ingest/types.ts +76 -0
  236. package/src/ingest/unmount-ingest-root.ts +224 -0
  237. package/src/navmesh/navmesh-actions.ts +27 -0
  238. package/src/navmesh/navmesh-handler.ts +237 -0
  239. package/src/navmesh/navmesh-workflow-store.ts +78 -0
  240. package/src/network/NetworkInspectorPanel.tsx +644 -0
  241. package/src/network/network-inspector-model.ts +225 -0
  242. package/src/play/play-boot-stall.ts +118 -0
  243. package/src/play/play-log-events.ts +26 -0
  244. package/src/play/play-mode.ts +2553 -0
  245. package/src/play/play-recording.ts +335 -0
  246. package/src/play/react-play-live-authoring.ts +165 -0
  247. package/src/play-bar/PlayBar.tsx +488 -0
  248. package/src/play-bar/PlayerCountPicker.tsx +100 -0
  249. package/src/profiler/FrameDebuggerPanel.tsx +458 -0
  250. package/src/profiler/PerformancePanel.tsx +1068 -0
  251. package/src/profiler/ProfilerPanel.tsx +53 -0
  252. package/src/profiler/frame-debugger-store.ts +97 -0
  253. package/src/profiler/main-thread-busy.ts +90 -0
  254. package/src/react/design-time-react-mount.ts +782 -0
  255. package/src/react/dom-authoring-adapter.ts +764 -0
  256. package/src/react/pasteboard-materialize.ts +154 -0
  257. package/src/react/react-inspector-section.tsx +2496 -0
  258. package/src/react/react-world-authoring-adapter.ts +3795 -0
  259. package/src/react/story-documents/story-args-section.ts +19 -0
  260. package/src/react/story-documents/story-documents.tsx +1380 -0
  261. package/src/react/story-paint-bounds.ts +94 -0
  262. package/src/react/ui-board-document.tsx +101 -0
  263. package/src/react/ui-board-title.ts +9 -0
  264. package/src/react/ui-component-board.ts +72 -0
  265. package/src/services/audio-pose-guard.ts +81 -0
  266. package/src/services/game-audio-unlock.ts +48 -0
  267. package/src/state-watch/StateWatchPanel.tsx +535 -0
  268. package/src/three/authoring/camera-runtime-inspector-section.tsx +135 -0
  269. package/src/three/authoring/constraint-inspector-section.tsx +189 -0
  270. package/src/three/authoring/design-time-renderer.ts +189 -0
  271. package/src/three/authoring/model-asset-inspector-section.css +41 -0
  272. package/src/three/authoring/model-asset-inspector-section.tsx +869 -0
  273. package/src/three/authoring/oid-source-persistence.ts +557 -0
  274. package/src/three/authoring/r3f-design-session.ts +1174 -0
  275. package/src/three/authoring/r3f-source-authoring-adapter.ts +5949 -0
  276. package/src/three/authoring/reflection-probe-inspector-section.tsx +76 -0
  277. package/src/three/authoring/spatial-audio-handles.ts +215 -0
  278. package/src/three/authoring/spatial-collider-handles.ts +229 -0
  279. package/src/three/authoring/spatial-joint-handles.ts +114 -0
  280. package/src/three/authoring/spatial-light-handles.ts +178 -0
  281. package/src/three/authoring/spatial-lod-handles.ts +93 -0
  282. package/src/three/authoring/spatial-particle-handles.ts +368 -0
  283. package/src/three/authoring/three-authoring-adapter.ts +1953 -0
  284. package/src/three/authoring/three-scene-identity.ts +19 -0
  285. package/src/three/authoring/three-spatial-handles.ts +161 -0
  286. package/src/three/authoring/typed-three-inspector.ts +528 -0
  287. package/src/three/component-verbs/extract-menu.ts +71 -0
  288. package/src/three/component-verbs/fork-menu.ts +78 -0
  289. package/src/three/component-verbs/internals-menu.ts +91 -0
  290. package/src/three/story-documents/three-story-documents.tsx +607 -0
  291. package/src/three/three-board/ThreeBoardDocument.tsx +888 -0
  292. package/src/three/three-board/board-framing.ts +412 -0
  293. package/src/three/three-board/board-layout.ts +401 -0
  294. package/src/three/three-board/board-scene.ts +901 -0
  295. package/src/three/three-board/three-component-board.ts +62 -0
  296. package/src/xstate/XStateBehaviorSection.tsx +130 -0
  297. package/src/xstate/XStateMachineInspector.tsx +566 -0
  298. package/src/xstate/character-animation-machine.fixture.ts +74 -0
  299. package/src/xstate/live-behaviors.ts +106 -0
  300. package/src/xstate/use-live-actor-state.ts +44 -0
  301. package/src/xstate/xstate-graph.ts +235 -0
  302. package/src/xstate/xstate-layout.ts +76 -0
@@ -0,0 +1,131 @@
1
+ /**
2
+ * The `__vgaiIngest` / `__vgaiIngest2D` / `__vgaiIngestReact` dev hooks: the
3
+ * headless-readable evidence a live ingest mount publishes for automation,
4
+ * screenshots and `vgai doctor`.
5
+ *
6
+ * Everything here is EVIDENCE, not control: names read off the live adapter,
7
+ * counters read off the live gate, the coverage report re-derived on demand.
8
+ * Nothing is inferred from the route or the adapter's identity — a capability
9
+ * appears here because the mounted object actually has it.
10
+ */
11
+
12
+ import { getActiveSystems } from '@volter/editor-core/authoring/active-systems';
13
+ import { loadIngestOwnership } from '../host/authoring/ingest-source-persistence';
14
+ import { gameLoopGate } from '../host/gated-globals';
15
+ import type { DomAuthoringAdapter } from '../react/dom-authoring-adapter';
16
+ import type { AuthoringAdapter } from '@volter/editor-project/adapter/authoring';
17
+ import type { IngestMount } from './authoring/ingest-root-adapter';
18
+ import { ingestCoverageReport } from './mount-coverage';
19
+
20
+ /** The window key each surface's mount publishes its evidence under. */
21
+ export type IngestHookKey = '__vgaiIngest' | '__vgaiIngest2D' | '__vgaiIngestReact';
22
+
23
+ const AUTHORING_PROVIDER_KEYS = [
24
+ 'hierarchy',
25
+ 'selection',
26
+ 'transforms',
27
+ 'inspector',
28
+ 'assetSubject',
29
+ 'structure',
30
+ 'persistence',
31
+ 'pickable',
32
+ 'rects',
33
+ 'boxEdit',
34
+ 'text',
35
+ 'colorSample',
36
+ 'stories',
37
+ 'truth',
38
+ 'assetDrop',
39
+ ] as const satisfies readonly (keyof AuthoringAdapter)[];
40
+
41
+ /** Names only providers that exist on the LIVE adapter. This is the shelf's
42
+ * authoring-family evidence; no capability is inferred from adapter identity. */
43
+ export function authoringProviderNames(adapter: AuthoringAdapter): string[] {
44
+ return AUTHORING_PROVIDER_KEYS.filter((name) => adapter[name] !== undefined);
45
+ }
46
+
47
+ /** The shared evidence fields every surface-specific dev hook publishes. */
48
+ export function ingestHookEvidence(adapter: AuthoringAdapter): Record<string, unknown> {
49
+ return {
50
+ coverage: () => ingestCoverageReport(),
51
+ prepareCoverage: () => loadIngestOwnership(),
52
+ authoringProviders: () => authoringProviderNames(adapter),
53
+ // Registered-adapter names deliberately come from the live registry, not
54
+ // from the coverage report's game-contract row (WORK.md's explicit rule).
55
+ engineSystems: () => Object.keys(getActiveSystems()),
56
+ };
57
+ }
58
+
59
+ /**
60
+ * The loop fields the `__vgaiIngest` dev hook publishes — the measured verdict
61
+ * plus the gate's own counters, so automation can drive Play/Pause/Step through
62
+ * exactly the seam the editor's own buttons use rather than a parallel one.
63
+ */
64
+ export function defineLoopHookFields(target: Record<string, unknown>, mount: IngestMount): void {
65
+ // `defineProperty`, NOT a spread: the verdict is itself live (the gate
66
+ // re-measures it at every hold) and spreading an accessor evaluates it
67
+ // once, which would freeze the claim at mount time and start lying the moment
68
+ // the game's shape changed.
69
+ Object.defineProperties(target, {
70
+ loop: {
71
+ enumerable: true,
72
+ get: () => mount.realmLoopVerdict?.()?.loop ?? null,
73
+ },
74
+ loopReason: {
75
+ enumerable: true,
76
+ get: () => mount.realmLoopVerdict?.()?.reason ?? null,
77
+ },
78
+ });
79
+ // S-5: the gate's own counters — what a pause actually held back. Before
80
+ // this there was NOTHING to read about whether a pause held anything, which
81
+ // is how "the gate reports success while gating nothing" survived. `null`
82
+ // when no gate is installed; never a fabricated number.
83
+ target['realmLoopStats'] = () => gameLoopGate()?.stats() ?? null;
84
+ }
85
+
86
+ /**
87
+ * D-L1 — the LIVE value behind
88
+ * `__vgaiIngestReact.domEvidence()`, shared by both react ingest routes.
89
+ * Evaluated at scrape time (a function result, never memoized),
90
+ * same "read the live state fresh on every call" discipline as
91
+ * `__vgaiSiblingMounts` (D-H1).
92
+ *
93
+ * `elementCount` prefers the DOM's real `querySelectorAll('*').length` — a
94
+ * real browser `HTMLElement` (what `reactRoot()` actually returns in
95
+ * production) always has it — and falls back to the adapter's own
96
+ * structural-walk size (`nodeCount()`) only when the mounted root doesn't
97
+ * expose `querySelectorAll` at all (this repo's own vitest-node adapter
98
+ * fixtures have no real DOM). Never a fabricated number either way: both
99
+ * paths count real, currently-present elements.
100
+ *
101
+ * `reactRootPresent` checks for a React 19 `createRoot` own-property mount
102
+ * stamp (`__reactContainer$<hash>`) on `root` itself — the SAME element
103
+ * `reactRoot()` returned, which is exactly the element `createRoot(...)`
104
+ * rendered into (`create-runtime.ts`'s `DomHostContext.container` doc
105
+ * comment: "the adapter's `mount` renders its react tree INTO this exact
106
+ * element via `createRoot`").
107
+ */
108
+ export function reactDomEvidence(
109
+ root: HTMLElement,
110
+ adapter: DomAuthoringAdapter,
111
+ ): { elementCount: number; hierarchyNodeCount: number; reactRootPresent: boolean } {
112
+ const withQuery = root as unknown as { querySelectorAll?: (sel: string) => ArrayLike<unknown> };
113
+ const elementCount =
114
+ typeof withQuery.querySelectorAll === 'function'
115
+ ? withQuery.querySelectorAll('*').length
116
+ : adapter.nodeCount();
117
+ const reactRootPresent = Object.keys(root as object).some((k) =>
118
+ k.startsWith('__reactContainer'),
119
+ );
120
+ return { elementCount, hierarchyNodeCount: adapter.nodeCount(), reactRootPresent };
121
+ }
122
+
123
+ /** Publish a mount's evidence object. The ONE place these keys are written. */
124
+ export function publishIngestHook(key: IngestHookKey, hook: Record<string, unknown>): void {
125
+ (window as unknown as Record<string, unknown>)[key] = hook;
126
+ }
127
+
128
+ /** Drop a torn-down mount's evidence. The ONE place these keys are removed. */
129
+ export function clearIngestHook(key: IngestHookKey): void {
130
+ delete (window as unknown as Record<string, unknown>)[key];
131
+ }
@@ -0,0 +1,183 @@
1
+ /**
2
+ * SAME-FRAME PIXELS for an ingested game's canvas — the reason `vgai
3
+ * screenshot` no longer returns black over a running ingest.
4
+ *
5
+ * ## The failure
6
+ *
7
+ * `composite-screenshot.ts` reads a game's canvas with `drawImage`, and that
8
+ * only works on a WebGL canvas whose context was created with
9
+ * `preserveDrawingBuffer: true`. Every canvas the vgai RUNTIME mounts sets it
10
+ * (`@volter/game-runtime/runtime/create-runtime`), which is why the composite leg has always
11
+ * worked for first-party play. An INGESTED game creates its own canvas: the
12
+ * racing-game's `<Canvas>` passes no `gl` prop, so fiber's default
13
+ * (`preserveDrawingBuffer: false`) applies, the browser discards the drawing
14
+ * buffer after compositing, and a `drawImage` from a later task — which is
15
+ * exactly what the composite leg does, after `waitForPaint`/
16
+ * `waitForFiniteMotion` — reads BLACK. Silently: the composite's near-black
17
+ * retry is gated on there being no canvas at all.
18
+ *
19
+ * ## Why this and not `preserveDrawingBuffer`
20
+ *
21
+ * The rejected alternative was to make the ingested canvas readable — a global
22
+ * `HTMLCanvasElement.prototype.getContext` patch forcing the flag. Three things
23
+ * are wrong with it and none are stylistic: the realm's lexical
24
+ * `window`/`document` shadow deliberately skips `node_modules`
25
+ * (`server/game-globals-shadow.ts`), and R3F creates the canvas inside
26
+ * `@react-three/fiber`, so a realm-scoped trap cannot reach it at all; a global
27
+ * one sits in front of the EDITOR's own canvases too and needs a host-vs-game
28
+ * identity filter, which is the exact timing-sensitive problem
29
+ * `SceneCaptureOptions.isHostRenderer` was painfully introduced to solve; and
30
+ * it makes every ingested game pay a permanent per-frame cost so a screenshot
31
+ * can occasionally be taken.
32
+ *
33
+ * The drawing buffer is not "unreadable", it is unreadable LATE. Read inside
34
+ * the game's own render call and the pixels are there, no flag required. The
35
+ * host already has that bracket and did not have to invent it: the capture trap
36
+ * stands between an ingested game and its own `WebGLRenderer.render`
37
+ * (`@volter/threejs-runtime/adapter/ingest/scene-capture`), and its `RenderPassHooks.after()`
38
+ * runs synchronously inside that call — the same bracket `renderDebug` uses.
39
+ * So this is arm-and-deliver on an existing seam, in the shape
40
+ * `dev/render-debug-adapter.ts`'s `captureFrame()` already established: zero
41
+ * cost until armed, and it works regardless of the game's GL flags (a vanilla
42
+ * three ingest, or any future game that opts out, is covered for free).
43
+ *
44
+ * ## RESOURCE OWNERSHIP
45
+ *
46
+ * OWNER: the live three ingest mount. `wireIngestSystems`
47
+ * (`ingest-render-debug.ts`) is the one installer — it owns the single
48
+ * `setRenderPassHooks` slot and multiplexes it — and
49
+ * `ingest-root-adapter.ts`'s `dispose()` is the ONE teardown path, through
50
+ * {@link clearIngestFrameSource}. SHARERS: none; at most one ingest session is
51
+ * mounted at a time (`mount-ingest-root.ts` fails a second by name), which is
52
+ * why the armed state below is module-scoped rather than per-mount.
53
+ */
54
+
55
+ /** What the module needs from the captured runtime to take a snapshot. */
56
+ export interface IngestFrameSource {
57
+ readonly renderer: {
58
+ readonly domElement?: unknown;
59
+ getRenderTarget?(): unknown;
60
+ };
61
+ }
62
+
63
+ /** The live mount's source, or `null` between mounts. */
64
+ let source: IngestFrameSource | null = null;
65
+
66
+ interface ArmedSnapshot {
67
+ readonly settle: (image: HTMLCanvasElement | null) => void;
68
+ readonly timer: ReturnType<typeof setTimeout>;
69
+ /** The most recent copy taken this frame; see {@link deliverArmedIngestFrame}
70
+ * for why the LAST one is the answer and not the first. */
71
+ image: HTMLCanvasElement | null;
72
+ delivering: boolean;
73
+ }
74
+ let armed: ArmedSnapshot | null = null;
75
+
76
+ /** Install the live mount's runtime. Called by the one installer. */
77
+ export function setIngestFrameSource(next: IngestFrameSource | null): void {
78
+ source = next;
79
+ if (!next) clearIngestFrameSnapshot();
80
+ }
81
+
82
+ /** Teardown: drop the source AND settle any armed wait, so a screenshot in
83
+ * flight when the mount goes down resolves (as "no frame") instead of hanging
84
+ * until its timeout. */
85
+ export function clearIngestFrameSource(): void {
86
+ source = null;
87
+ clearIngestFrameSnapshot();
88
+ }
89
+
90
+ function clearIngestFrameSnapshot(): void {
91
+ const pending = armed;
92
+ armed = null;
93
+ if (pending) {
94
+ clearTimeout(pending.timer);
95
+ pending.settle(null);
96
+ }
97
+ }
98
+
99
+ /**
100
+ * The game's own canvas, or `null` when there is no live ingest runtime.
101
+ *
102
+ * Deliberately NOT an `instanceof HTMLCanvasElement` test: this module is
103
+ * imported by the command relay, which runs headless in Node for the relay
104
+ * suites — there `HTMLCanvasElement` is not even defined, and the reference
105
+ * alone would throw on every first-party screenshot. The only question a
106
+ * caller asks is identity ("is the canvas I found the ingest mount's?"), which
107
+ * needs no class.
108
+ */
109
+ export function ingestFrameCanvas(): HTMLCanvasElement | null {
110
+ const element = source?.renderer.domElement;
111
+ return typeof element === 'object' && element !== null ? (element as HTMLCanvasElement) : null;
112
+ }
113
+
114
+ /**
115
+ * Called from inside the game's render pass (the capture trap's `after`).
116
+ * Copies the live drawing buffer when a snapshot is armed AND this render went
117
+ * to the canvas rather than to an offscreen target — a game that bakes a
118
+ * render target mid-frame (racing-game's minimap does) must not have that pass
119
+ * mistaken for its picture.
120
+ *
121
+ * THE LAST CANVAS PASS OF THE FRAME IS THE PICTURE, not the first, and that is
122
+ * a MEASURED correction rather than a precaution. A frame is often several
123
+ * canvas-targeted `render()` calls: racing-game's minimap draws the world,
124
+ * then `clearDepth()`, then the map overlay — all to the canvas — and drei's
125
+ * `<Hud>` has the identical shape. Settling on the first bracket produced a
126
+ * capture with the minimap MISSING, i.e. a screenshot that silently omitted a
127
+ * HUD layer the human can see. So each qualifying pass overwrites the copy and
128
+ * the promise settles one macrotask later, once the frame's JS has finished
129
+ * and no further pass can arrive.
130
+ *
131
+ * Never throws: it runs inside the game's own frame.
132
+ */
133
+ export function deliverArmedIngestFrame(): void {
134
+ const pending = armed;
135
+ if (!pending || !source) return;
136
+ try {
137
+ const renderer = source.renderer;
138
+ if (typeof renderer.getRenderTarget === 'function' && renderer.getRenderTarget() !== null) {
139
+ return;
140
+ }
141
+ const canvas = ingestFrameCanvas();
142
+ if (!canvas || canvas.width === 0 || canvas.height === 0) return;
143
+ const copy = pending.image ?? canvas.ownerDocument.createElement('canvas');
144
+ copy.width = canvas.width;
145
+ copy.height = canvas.height;
146
+ const ctx = copy.getContext('2d');
147
+ if (!ctx) return;
148
+ ctx.drawImage(canvas, 0, 0);
149
+ pending.image = copy;
150
+ if (pending.delivering) return;
151
+ pending.delivering = true;
152
+ setTimeout(() => {
153
+ if (armed !== pending) return;
154
+ armed = null;
155
+ clearTimeout(pending.timer);
156
+ pending.settle(pending.image);
157
+ }, 0);
158
+ } catch {
159
+ // A snapshot that cannot be taken is a capture that degrades to the
160
+ // ordinary canvas read; it is never the game's frame breaking.
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Ask for the next frame the ingested game draws to its canvas, as a readable
166
+ * 2D canvas. Resolves `null` when nothing is mounted, when the game does not
167
+ * render inside `timeoutMs` (a paused or hidden game), or when the mount goes
168
+ * down first — every one of which means "capture it the ordinary way".
169
+ *
170
+ * Only one wait at a time; a second arm supersedes the first, which settles
171
+ * `null`.
172
+ */
173
+ export function armIngestFrameSnapshot(timeoutMs = 2000): Promise<HTMLCanvasElement | null> {
174
+ if (!source) return Promise.resolve(null);
175
+ clearIngestFrameSnapshot();
176
+ return new Promise<HTMLCanvasElement | null>((resolve) => {
177
+ const timer = setTimeout(() => {
178
+ if (armed?.timer === timer) armed = null;
179
+ resolve(null);
180
+ }, timeoutMs);
181
+ armed = { settle: resolve, timer, image: null, delivering: false };
182
+ });
183
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Relay play-control dispatch for an INGEST session — the SimCity ledger's S-2
3
+ * ("`vgai play` destroys a working source-mount").
4
+ *
5
+ * ## The defect
6
+ *
7
+ * `command-listener.ts`'s `play`/`stop`/`pause`/`resume`/`step` cases called
8
+ * straight into `play-mode.ts`, which is the FIRST-PARTY runtime boot. That
9
+ * boot mounts the manifest's roots into `getGameContainer()` — the same element
10
+ * an ingest mount already occupies — and a captured ingest is not a
11
+ * host-mounted `RootAdapter`. Play
12
+ * therefore always failed on an ingest project and ran `exitPlayMode()` in its
13
+ * own catch, which unconditionally reset the systems slot, popped the adopted
14
+ * scene, restored the pre-ingest authoring adapter, cleared the game input gate
15
+ * and set `playState: 'stopped'` — tearing the live mount's editor-side
16
+ * ownership out from under it, with no remount.
17
+ *
18
+ * The editor's own Play button never had this problem: `components/PlayBar.tsx`
19
+ * early-returns an entirely separate ingest toolbar wired to
20
+ * `getIngestPlayControl()`. The relay was the one play surface that did not
21
+ * branch, which is exactly why the defect only ever showed up through the CLI.
22
+ *
23
+ * ## Why a separate module
24
+ *
25
+ * So the dispatch is a pure function over the control surface, testable with a
26
+ * stub and no editor/browser at all (`packages/editor/test/ingest-play-commands.test.ts`).
27
+ */
28
+
29
+ import {
30
+ type CommandResult,
31
+ type IngestPlayCommandType,
32
+ isIngestPlayCommandType,
33
+ } from '@volter/editor-sdk/session/command-table';
34
+
35
+ /** The shape {@link handleIngestPlayControl} needs — structurally satisfied by
36
+ * `ingest/ingest-play-control.ts`'s `IngestPlayControl`, named locally so this module imports
37
+ * no session state. */
38
+ export interface IngestPlaySurface {
39
+ readonly playing: boolean;
40
+ play(): void;
41
+ pause(): void;
42
+ resume(): void;
43
+ readonly canStep: boolean;
44
+ step(): void;
45
+ }
46
+
47
+ /**
48
+ * Dispatch one relay play-control command to the live ingest session, or return
49
+ * `null` when this command is not a play control (the caller then runs its
50
+ * ordinary switch).
51
+ *
52
+ * `stop` maps to PAUSE by default, deliberately. An ingest session's mount is
53
+ * the editor's open document, not a play session — there is no "stop" that
54
+ * could unmount it and leave the editor in a sensible state, and destroying the
55
+ * mount is the very defect this module exists to fix. Halting the game's loop
56
+ * is what "stop the game" can honestly mean here, and it is what the editor's
57
+ * own ingest toolbar offers.
58
+ *
59
+ * `stopEndsRun` inverts exactly that premise, and nothing else. A root that
60
+ * declares its own world component mounts the GAME only when Play starts
61
+ * (`deferred-ingest-play.ts`), so its mount is a play run rather than the open
62
+ * document: ⏹ genuinely ends it and the editor returns to the design-time
63
+ * Scene. This dispatch then declines `stop` (returns `null`) so the caller's
64
+ * ordinary `exitPlayMode()` runs. ▶/⏸/step still belong here either way —
65
+ * they drive the ingested game's OWN loop, which no first-party session owns.
66
+ *
67
+ * `step` REFUSES when the mount cannot honestly step (`canStep` is false
68
+ * whenever no serve-time loop gate measured control over this game's frames) —
69
+ * a silent no-op would be a fabricated ack.
70
+ */
71
+ /**
72
+ * One handler per claimed verb, keyed by the command table's own play-control
73
+ * subset. A `Record` rather than a switch on purpose: adding a play verb to
74
+ * `command-table.ts` makes this object stop compiling until it has a handler,
75
+ * which a switch would not, and the vocabulary is then read from ONE place
76
+ * instead of being spelled again here.
77
+ *
78
+ * `null` means "not mine" — the caller runs its ordinary dispatch.
79
+ */
80
+ type IngestPlayOutcome =
81
+ /** Driven; the caller gets the standard `{ ingest: true, playing }` ack. */
82
+ | 'driven'
83
+ /** Not this lane's after all; the caller runs its ordinary dispatch. */
84
+ | 'declined'
85
+ /** A refusal with its own reason. */
86
+ | CommandResult;
87
+
88
+ const INGEST_PLAY_HANDLERS: Record<
89
+ IngestPlayCommandType,
90
+ (ingest: IngestPlaySurface, opts?: { readonly stopEndsRun?: boolean }) => IngestPlayOutcome
91
+ > = {
92
+ play: (ingest) => {
93
+ ingest.play();
94
+ return 'driven';
95
+ },
96
+ resume: (ingest) => {
97
+ ingest.resume();
98
+ return 'driven';
99
+ },
100
+ stop: (ingest, opts) => {
101
+ if (opts?.stopEndsRun) return 'declined';
102
+ ingest.pause();
103
+ return 'driven';
104
+ },
105
+ pause: (ingest) => {
106
+ ingest.pause();
107
+ return 'driven';
108
+ },
109
+ step: (ingest) => {
110
+ if (!ingest.canStep) {
111
+ return {
112
+ ok: false,
113
+ error:
114
+ 'this ingested game cannot be stepped — no serve-time loop gate measured control ' +
115
+ 'over its frames, so a single-frame step would be a button that lies (see ' +
116
+ 'ingest/same-realm-loop-gate.ts)',
117
+ };
118
+ }
119
+ ingest.step();
120
+ return 'driven';
121
+ },
122
+ };
123
+
124
+ export function handleIngestPlayControl(
125
+ ingest: IngestPlaySurface,
126
+ type: string,
127
+ opts?: { readonly stopEndsRun?: boolean },
128
+ ): CommandResult | null {
129
+ if (!isIngestPlayCommandType(type)) return null;
130
+ const outcome = INGEST_PLAY_HANDLERS[type](ingest, opts);
131
+ if (outcome === 'declined') return null;
132
+ if (outcome !== 'driven') return outcome;
133
+ return { ok: true, data: { ingest: true, playing: ingest.playing } };
134
+ }