@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,744 @@
1
+ /**
2
+ * THE door: mounting a project whose manifest declares an `{ ingest }` root is
3
+ * ordinary root-adapter dispatch, exactly like every other root — this module
4
+ * is the dispatcher.
5
+ *
6
+ * It answers one question per surface ("does the OPEN PROJECT declare an
7
+ * `{ ingest }` root, and which surface is it?"), hands off to the per-surface
8
+ * mount module (`mount-three-ingest-root.ts` / `mount-canvas-ingest-root.ts` /
9
+ * `mount-dom-ingest-root.ts`), mounts a composite's remaining roots beside it
10
+ * (`ingest-siblings.ts`), and re-exports the mount/unmount API its callers
11
+ * drive. The measurement half lives in `mount-coverage.ts`, the live session in
12
+ * `active-ingest.ts`, teardown in `unmount-ingest-root.ts` — nothing here
13
+ * measures anything.
14
+ *
15
+ * Dispatching is ALL it does, and that is what keeps the mount-failure seam
16
+ * connected: the Game document belongs to whoever mounts into its container,
17
+ * so each per-surface mount module acquires it (`acquireGameDocument`) right
18
+ * before it reads `liveDocumentContainer()` and reports its own missing-container
19
+ * refusal. A route here that acquired first and refused on its own would
20
+ * pre-empt that mount — the failure would then be a bare console line that
21
+ * never reaches {@link reportFailedIngestMount}, which is precisely the
22
+ * trace-less failure D-W3/S-1 exists to eliminate. Every route's ONLY failure
23
+ * path is therefore its catch block.
24
+ */
25
+
26
+ import { getAuthoringOverride, setActiveAuthoring } from '@volter/editor-core/authoring/active-adapter';
27
+ import {
28
+ CompositeAuthoringAdapter,
29
+ type CompositeChild,
30
+ } from '@volter/editor-core/authoring/composite-authoring-adapter';
31
+ import {
32
+ addMountFailureReport,
33
+ clearMountFailureReports,
34
+ formatMountFailureMessage,
35
+ getMountFailureReports,
36
+ type MountFailureReport,
37
+ } from '@volter/editor-core/authoring/mount-failure-report';
38
+ import { editorConsole } from '@volter/editor-core/editor-console';
39
+ import type { EditorShellStore } from '@volter/editor-core/editor-shell-store';
40
+ import { registerEditorStateFacet } from '@volter/editor-core/editor-state-facets';
41
+ import { liveDocumentContainer } from '@volter/editor-core/live-document';
42
+ import { fetchGameManifest, isManifestAbsence } from '@volter/editor-core/manifest-project';
43
+ import { getCurrentProject } from '@volter/editor-core/project-manager';
44
+ import { onShellStore } from '@volter/editor-core/shell-store-door';
45
+ import { ingestRoots, rootById } from '@volter/editor-project/adapter/manifest-interpreter';
46
+ import { MANIFEST_FILENAME } from '@volter/editor-project/manifest/filename';
47
+ import type { ResolvedAdapterRoot, ResolvedGameManifest } from '@volter/editor-project/manifest/load';
48
+ import { activeIngest, attachIngestSiblings } from './active-ingest';
49
+ import { captureWaitStatus } from './capture-wait-report';
50
+ import { resetIngestPlaySurface } from './ingest-play-control';
51
+ // TYPE-ONLY (D-V2): `ingest-siblings.ts` imports from `module-mode.ts`, and a
52
+ // value import here would risk closing that cycle at the wrong end. The actual
53
+ // `mountIngestSiblings` call below is a dynamic `import()`, right where it's
54
+ // used (`mountCompositeSiblings`); only the TYPE is needed at the top level,
55
+ // and type-only imports are erased before the cycle could ever matter.
56
+ import type { SiblingMount } from './ingest-siblings';
57
+ import { ingestStatusFacet } from './live-ingest-facet';
58
+ import {
59
+ exitActiveIngest,
60
+ unmountCanvasIngestRoot,
61
+ unmountDomIngestRoot,
62
+ unmountThreeIngestRoot,
63
+ } from './unmount-ingest-root';
64
+
65
+ export {
66
+ activeIngestKind,
67
+ type IngestKind,
68
+ isIngestActive,
69
+ isIngestActive2D,
70
+ isIngestActiveReact,
71
+ } from './active-ingest';
72
+ export { getIngestPlayControl, type IngestPlayControl } from './ingest-play-control';
73
+ export { ingestStatusFacet } from './live-ingest-facet';
74
+ export {
75
+ unmountCanvasIngestRoot,
76
+ unmountDomIngestRoot,
77
+ unmountThreeIngestRoot,
78
+ } from './unmount-ingest-root';
79
+
80
+ type CanvasMounts = typeof import('./mount-canvas-ingest-root');
81
+ type DomMounts = typeof import('./mount-dom-ingest-root');
82
+ type ThreeMounts = typeof import('./mount-three-ingest-root');
83
+
84
+ const canvasMounts = () => import('./mount-canvas-ingest-root');
85
+ const domMounts = () => import('./mount-dom-ingest-root');
86
+ const threeMounts = () => import('./mount-three-ingest-root');
87
+
88
+ export function mountCanvasIngestRoot(
89
+ ...args: Parameters<CanvasMounts['mountCanvasIngestRoot']>
90
+ ): ReturnType<CanvasMounts['mountCanvasIngestRoot']> {
91
+ return canvasMounts().then((module) => module.mountCanvasIngestRoot(...args));
92
+ }
93
+
94
+ export function mountCanvasIngestRootById(
95
+ ...args: Parameters<CanvasMounts['mountCanvasIngestRootById']>
96
+ ): ReturnType<CanvasMounts['mountCanvasIngestRootById']> {
97
+ return canvasMounts().then((module) => module.mountCanvasIngestRootById(...args));
98
+ }
99
+
100
+ export function mountCanvasIngestRootFromManifest(
101
+ ...args: Parameters<CanvasMounts['mountCanvasIngestRootFromManifest']>
102
+ ): ReturnType<CanvasMounts['mountCanvasIngestRootFromManifest']> {
103
+ return canvasMounts().then((module) => module.mountCanvasIngestRootFromManifest(...args));
104
+ }
105
+
106
+ export function mountDomIngestRoot(
107
+ ...args: Parameters<DomMounts['mountDomIngestRoot']>
108
+ ): ReturnType<DomMounts['mountDomIngestRoot']> {
109
+ return domMounts().then((module) => module.mountDomIngestRoot(...args));
110
+ }
111
+
112
+ export function mountDomIngestRootFromManifest(
113
+ ...args: Parameters<DomMounts['mountDomIngestRootFromManifest']>
114
+ ): ReturnType<DomMounts['mountDomIngestRootFromManifest']> {
115
+ return domMounts().then((module) => module.mountDomIngestRootFromManifest(...args));
116
+ }
117
+
118
+ export function mountThreeIngestRoot(
119
+ ...args: Parameters<ThreeMounts['mountThreeIngestRoot']>
120
+ ): ReturnType<ThreeMounts['mountThreeIngestRoot']> {
121
+ return threeMounts().then((module) => module.mountThreeIngestRoot(...args));
122
+ }
123
+
124
+ export function mountThreeIngestRootById(
125
+ ...args: Parameters<ThreeMounts['mountThreeIngestRootById']>
126
+ ): ReturnType<ThreeMounts['mountThreeIngestRootById']> {
127
+ return threeMounts().then((module) => module.mountThreeIngestRootById(...args));
128
+ }
129
+
130
+ export function mountThreeIngestRootFromManifest(
131
+ ...args: Parameters<ThreeMounts['mountThreeIngestRootFromManifest']>
132
+ ): ReturnType<ThreeMounts['mountThreeIngestRootFromManifest']> {
133
+ return threeMounts().then((module) => module.mountThreeIngestRootFromManifest(...args));
134
+ }
135
+
136
+ let _boundStore: EditorShellStore | null = null;
137
+
138
+ /**
139
+ * The editor store, so the dev hook / UI can launch a mount — bound on the
140
+ * store's ARRIVAL (`shell-store-door.ts`), once per project session
141
+ * (`AppRoot` unmounts/remounts `DefaultEditorLayout` under a fresh
142
+ * `EditorProvider` on "return to project screen" -> reopen).
143
+ *
144
+ * D-W3: the arrival is the earliest point every new project session reaches,
145
+ * so it is where a PREVIOUS project's stale mount-failure report is dropped.
146
+ * The rest of the project-switch teardown — the live ingest session itself
147
+ * (`active-ingest.ts`'s slot and the `active-adapter.ts` globals it sets), the
148
+ * play session, the System adapters and the session measurement ledgers — is
149
+ * `project-session-reset.ts`, fired by `project-manager.ts`'s
150
+ * `onProjectSessionEnd`.
151
+ */
152
+ onShellStore((store) => {
153
+ _boundStore = store;
154
+ clearMountFailureReports();
155
+ });
156
+
157
+ // What only the ingest lane knows of the state report
158
+ // (`editor-state-facets.ts`): the live ingest session's OWN facts (world id,
159
+ // surface reached, whether its loop is running, the MEASURED loop verdict)
160
+ // — null when nothing is ingested; a non-null facet IS the mount evidence —
161
+ // and the state BETWEEN mounted and failed: a mount still waiting for the
162
+ // game's first frame, a budget of VISIBLE time (parked on a hidden tab).
163
+ registerEditorStateFacet(() => ({
164
+ ingest: ingestStatusFacet(),
165
+ ingestCaptureWait: captureWaitStatus(),
166
+ }));
167
+
168
+ /**
169
+ * S-1, the other half: a mount that FAILED must leave the editor in a stopped,
170
+ * empty state — never in the half-entered "playing" one its own prologue set up.
171
+ *
172
+ * The per-surface mount paths flip `store.setPlayState('playing')` BEFORE the
173
+ * mount is attempted (that flag is what swaps the viewport onto the live
174
+ * scene). When the mount then throws, that flag, plus any authoring
175
+ * adapter/adopted scene a partial mount installed, used to survive — so
176
+ * `vgai status` reported `playing`, the hierarchy kept presenting whatever tree
177
+ * was underneath, and the only trace of the failure was one console line. This
178
+ * is the single teardown every failure path runs: drop the partial session,
179
+ * reset the play surface, and record + broadcast the failure so the status item
180
+ * actually shows.
181
+ *
182
+ * Safe to call when nothing mounted at all (`exitActiveIngest` and
183
+ * `exitPlayScene` are both no-ops on an empty slot/stack), which is what makes
184
+ * it callable from any catch block without first proving how far the mount got.
185
+ */
186
+ function reportFailedIngestMount(store: EditorShellStore, report: MountFailureReport): void {
187
+ editorConsole.error(
188
+ `Ingest mount failed for "${report.worldId}" (${report.identity}): ${report.message}`,
189
+ 'ingest',
190
+ );
191
+ // Tear down whatever the mount DID publish. Deliberately only this: when
192
+ // the slot was never assigned, nothing overrode the edit-mode authoring
193
+ // composite or adopted a scene, so clearing those here would blank the
194
+ // hierarchy for the project's OTHER roots too. `exitActiveIngest` is a no-op
195
+ // on an empty slot and the full per-kind teardown otherwise.
196
+ exitActiveIngest();
197
+ resetIngestPlaySurface();
198
+ // The prologue's `setPlayState('playing')` must never outlive a failed mount
199
+ // — that flag is what `collectState` reported as a live game (S-1).
200
+ store.setPlayState('stopped');
201
+ addMountFailureReport(report);
202
+ // `addMountFailureReport` notifies the status item through the slot's own
203
+ // subscribe; this broadcast is for the STORE-subscribed panels (hierarchy,
204
+ // play controls) that must re-render off the teardown above — a failed
205
+ // mount never reaches the success path's `notifyIngestEdit()`.
206
+ store.notifyIngestEdit();
207
+ }
208
+
209
+ /**
210
+ * THE manifest read for the whole ingest route chain — and the one place a
211
+ * failure to read it is allowed to end the chain.
212
+ *
213
+ * Each `tryManifestIngestRoute*` used to fetch this for itself behind
214
+ * `.catch(() => null)` and then `return false`. ABSENCE and FAILURE therefore
215
+ * had the same shape, so a project that declared a perfectly good ingest root
216
+ * in a manifest the strict loader rejected — one unrecognized key is enough,
217
+ * the schema is `.strict()` — was declined by all three routes, fell past the
218
+ * hosted lane, and mounted NOTHING, with not one line anywhere saying why:
219
+ * a Boundary row with 0 children over a black frame and a silent console.
220
+ * An in-tree fixture's mount failure was loud through the same session,
221
+ * because `mountCanvasIngestRootById` takes a DISCOVERED descriptor and never
222
+ * reads a manifest at all — the two lanes disagreed about whether failing to
223
+ * mount is something the reader gets told.
224
+ *
225
+ * `null` still means "this chain has nothing to route", but only ever after
226
+ * the reason has been either (a) genuine absence — this project has no
227
+ * manifest, which is not an ingest project and not an error — or (b) said out
228
+ * loud.
229
+ */
230
+ async function readIngestChainManifest(): Promise<ResolvedGameManifest | null> {
231
+ // Same realm split as `module-mode.ts`: a storage-backed project's manifest
232
+ // reads through the realm; staged hosted content has its own discovery.
233
+ try {
234
+ return await fetchGameManifest();
235
+ } catch (err) {
236
+ if (isManifestAbsence(err)) return null; // not a manifest project; nothing to explain
237
+ editorConsole.error(
238
+ `Ingest routing: this project's ${MANIFEST_FILENAME} could not be read, so no root was ` +
239
+ `mounted from it — ${formatMountFailureMessage(err)}`,
240
+ 'ingest',
241
+ );
242
+ return null;
243
+ }
244
+ }
245
+
246
+ /**
247
+ * D-V2 (composite): every world in `manifest.roots` whose adapter is `{
248
+ * ingest }` — a composite manifest caps this at exactly one (see each
249
+ * `tryManifestIngestRoute*` below); ≥2 is a loud named error via {@link
250
+ * reportTooManyIngestRoots}, never a silent first-wins pick.
251
+ */
252
+ function findIngestRoots(manifest: ResolvedGameManifest): ResolvedAdapterRoot[] {
253
+ return ingestRoots(manifest);
254
+ }
255
+
256
+ /**
257
+ * D-V2: the "composite declares ≥2 ingest roots" violation — named, listing
258
+ * every offending world id, reported once (into BOTH `editorConsole` and the
259
+ * D-V5 multi-entry mount-failure report, one entry per offending world so
260
+ * the status item names all of them) by whichever `tryManifestIngestRoute*`
261
+ * happens to run first (module route order: three, pixi, react — see
262
+ * {@link autoLaunchIngest}) — the caller returns `true` right after this
263
+ * (claims the route), so the other two ingest routes never also report it.
264
+ */
265
+ function reportTooManyIngestRoots(ingestRoots: ResolvedAdapterRoot[]): void {
266
+ const ids = ingestRoots.map((w) => `"${w.id}"`).join(', ');
267
+ const message =
268
+ 'a composite manifest declares exactly ONE ingest world in this slice — found ' +
269
+ `${ingestRoots.length}: ${ids}.`;
270
+ editorConsole.error(`Manifest composite ingest route: ${message}`, 'ingest');
271
+ for (const world of ingestRoots) {
272
+ addMountFailureReport({
273
+ worldId: world.id,
274
+ kind: world.surface,
275
+ identity: world.adapter.identity,
276
+ message,
277
+ });
278
+ }
279
+ }
280
+
281
+ /**
282
+ * D-V2: mount every OTHER world in a composite manifest (every world besides
283
+ * `ingestRootId`, the one just mounted through its own per-surface path) via
284
+ * `ingest-siblings.ts`'s `mountIngestSiblings`, and attach the resulting
285
+ * handles onto the live slot so `exitActiveIngest` tears them down.
286
+ * Dynamically imported — `ingest-siblings.ts` imports from `module-mode.ts`,
287
+ * and a static import here would risk closing that cycle at the wrong end
288
+ * (mirrors {@link autoLaunchIngest}'s own dynamic import of `module-mode.ts`
289
+ * below, for the identical reason). Never throws: `mountIngestSiblings` already
290
+ * catches every individual sibling's failure into the mount-failure report.
291
+ * Returns the mounted handles so the caller can promote authoring (D-E2,
292
+ * {@link promoteToCompositeAuthoring}) — `store` is threaded through so each
293
+ * sibling gets an honest `SiblingMount.authoring` (D-E1).
294
+ */
295
+ async function mountCompositeSiblings(
296
+ manifest: ResolvedGameManifest,
297
+ ingestRootId: string,
298
+ gameContainer: HTMLElement,
299
+ projectRoot: string,
300
+ store: EditorShellStore,
301
+ ): Promise<SiblingMount[]> {
302
+ const { mountIngestSiblings } = await import('./ingest-siblings');
303
+ const siblings = await mountIngestSiblings(
304
+ manifest,
305
+ ingestRootId,
306
+ gameContainer,
307
+ projectRoot,
308
+ store,
309
+ );
310
+ attachIngestSiblings(siblings);
311
+ return siblings;
312
+ }
313
+
314
+ /**
315
+ * D-E2 (promotion on the ingest composite): when a composite's siblings
316
+ * actually mounted (`siblings.length > 0`), promote the active authoring
317
+ * override — currently the PRIMARY ingest world's own plain adapter, installed
318
+ * by the route's own mount that just ran immediately before this call — to
319
+ * a `CompositeAuthoringAdapter` whose ordered children are the primary world
320
+ * FIRST (it is the route's anchor) followed by every mounted sibling in
321
+ * manifest order, each routed by its own manifest `kind`. A single-world mount
322
+ * (`siblings.length === 0`) is untouched — byte-stable, no group node, no
323
+ * wrapper: §1F's single-world clause.
324
+ *
325
+ * The primary's own adapter is read back via `getAuthoringOverride()` rather
326
+ * than threaded as a parameter — every composite route's own mount call
327
+ * already installed it via `setActiveAuthoring` synchronously before
328
+ * this runs (mirrors `attachIngestSiblings`'s identical "just installed"
329
+ * assumption); `null` (defensive only — nothing in this synchronous
330
+ * call chain clears it in between) is a no-op, same guard shape.
331
+ */
332
+ function promoteToCompositeAuthoring(
333
+ manifest: ResolvedGameManifest,
334
+ ingestRoot: ResolvedAdapterRoot,
335
+ siblings: readonly SiblingMount[],
336
+ ): void {
337
+ if (siblings.length === 0) return;
338
+ const primaryAdapter = getAuthoringOverride();
339
+ if (!primaryAdapter) return;
340
+ // A readable hybrid ingest is already a small surface composite (Canvas +
341
+ // nested DOM UI). Flatten those children before adding manifest siblings:
342
+ // CompositeAuthoringAdapter's synthetic `world:*` ids belong to ONE
343
+ // composite and may never be nested inside another composite that would
344
+ // interpret them as its own group ids.
345
+ const primaryChildren: CompositeChild[] =
346
+ primaryAdapter instanceof CompositeAuthoringAdapter
347
+ ? primaryAdapter.childAdapters().map((child) => ({
348
+ worldId: child.worldId,
349
+ kind: child.kind,
350
+ adapter: child.adapter,
351
+ role: child.role,
352
+ zOrder: child.zOrder,
353
+ ...(child.label !== undefined ? { label: child.label } : {}),
354
+ ...(child.parentRootId !== undefined ? { parentRootId: child.parentRootId } : {}),
355
+ }))
356
+ : [{ worldId: ingestRoot.id, kind: ingestRoot.surface, adapter: primaryAdapter }];
357
+ const children: CompositeChild[] = [
358
+ ...primaryChildren,
359
+ ...siblings.map((s) => ({
360
+ worldId: s.worldId,
361
+ kind: rootById(manifest, s.worldId)?.surface ?? 'unknown',
362
+ // D-E1: every successfully-mounted SiblingMount gets an honest
363
+ // `authoring` whenever a store was supplied — every composite route
364
+ // below supplies one, so this is never actually undefined in
365
+ // production; the non-null assertion mirrors D-E2's own spec.
366
+ adapter: s.authoring!,
367
+ })),
368
+ ];
369
+ setActiveAuthoring(new CompositeAuthoringAdapter(children));
370
+ }
371
+
372
+ /**
373
+ * Route 1 of {@link autoLaunchIngest} (T3.3 slice 3, §1E): a manifest-backed
374
+ * project declaring a three-surface `{ ingest }` root mounts straight from
375
+ * that root. Returns `true` if this route applied (whether it succeeded or
376
+ * logged an error) — the caller must not also try route 2 in that case
377
+ * (§1B's "manifest wins" discovery order); `false` means "no manifest, no
378
+ * ingest root at all, or the one ingest root isn't ingest-three", so the
379
+ * next route (canvas, dom, then the hosted lane) still gets a chance.
380
+ *
381
+ * The manifest is HANDED IN ({@link readIngestChainManifest} reads it once for
382
+ * the chain) rather than fetched here. It was fetched per route behind a
383
+ * swallowing `catch`, which made "this project has no manifest" and "this
384
+ * project's manifest is unreadable" the same silent answer — see that
385
+ * function for the dark mount that produced.
386
+ *
387
+ * D-V2 (F24 composite): scans ALL roots for `{ ingest }` adapters, not just
388
+ * `roots[0]` — 0 ingest roots declines (unchanged non-ingest-manifest
389
+ * behavior); ≥2 claims the route and fails loudly
390
+ * ({@link reportTooManyIngestRoots}), mounting nothing; exactly 1 whose
391
+ * identity is `ingest-three` mounts THAT world (wherever it sits in the
392
+ * array) through the existing mount path, then mounts every other world as a
393
+ * sibling ({@link mountCompositeSiblings}) — a sibling failure never
394
+ * unmounts the ingest world (it is caught internally, per-world, by
395
+ * `ingest-siblings.ts`).
396
+ */
397
+ async function tryManifestIngestRoute(
398
+ store: EditorShellStore,
399
+ manifest: ResolvedGameManifest | null,
400
+ ): Promise<boolean> {
401
+ if (!manifest) return false;
402
+ const ingestRoots = findIngestRoots(manifest);
403
+ if (ingestRoots.length === 0) return false;
404
+ if (ingestRoots.length > 1) {
405
+ reportTooManyIngestRoots(ingestRoots);
406
+ return true;
407
+ }
408
+ const ingestRoot = ingestRoots[0]!;
409
+ if (ingestRoot.adapter.identity !== 'ingest-three') return false;
410
+
411
+ const project = getCurrentProject();
412
+ if (!project) {
413
+ editorConsole.error(
414
+ 'Manifest declares an ingest-three world but no project is open — cannot auto-ingest',
415
+ 'ingest',
416
+ );
417
+ return true;
418
+ }
419
+ try {
420
+ await mountThreeIngestRootFromManifest(store, ingestRoot, project.rootPath);
421
+ // D-V2: mount every OTHER world as a composite sibling — a no-op loop
422
+ // when this is a single-world manifest (nothing else to iterate).
423
+ const gameContainer = liveDocumentContainer();
424
+ if (gameContainer) {
425
+ const siblings = await mountCompositeSiblings(
426
+ manifest,
427
+ ingestRoot.id,
428
+ gameContainer,
429
+ project.rootPath,
430
+ store,
431
+ );
432
+ // D-E2: promote to a composite once any sibling actually mounted —
433
+ // byte-stable (no-op) for a single-world manifest.
434
+ promoteToCompositeAuthoring(manifest, ingestRoot, siblings);
435
+ }
436
+ } catch (err) {
437
+ // D-W3 (F9) + S-1: a console line was, until now, the ONLY trace this
438
+ // failure left anywhere in the UI — the default Untitled scene has
439
+ // already loaded by this point (§0.4), so nothing visibly changed, and
440
+ // the half-entered `playState: 'playing'` made the dead world report
441
+ // itself alive. `reportFailedIngestMount` is the one path that resets the
442
+ // surface, records the failure report AND broadcasts it.
443
+ reportFailedIngestMount(store, {
444
+ worldId: ingestRoot.id,
445
+ kind: ingestRoot.surface,
446
+ identity: ingestRoot.adapter.identity,
447
+ message: formatMountFailureMessage(err),
448
+ });
449
+ }
450
+ return true;
451
+ }
452
+
453
+ /**
454
+ * Canvas sibling of {@link tryManifestIngestRoute} (T6.1 slice 4): a
455
+ * manifest-backed project declaring an `ingest-pixi` root mounts straight from
456
+ * that root. Same "applied or not" contract, same D-V2 composite
457
+ * scan/route-selection/sibling-mount shape as its three sibling above.
458
+ */
459
+ async function tryManifestIngestRoute2D(
460
+ store: EditorShellStore,
461
+ manifest: ResolvedGameManifest | null,
462
+ ): Promise<boolean> {
463
+ if (!manifest) return false;
464
+ const ingestRoots = findIngestRoots(manifest);
465
+ if (ingestRoots.length === 0) return false;
466
+ if (ingestRoots.length > 1) {
467
+ reportTooManyIngestRoots(ingestRoots);
468
+ return true;
469
+ }
470
+ const ingestRoot = ingestRoots[0]!;
471
+ if (ingestRoot.adapter.identity !== 'ingest-pixi') return false;
472
+
473
+ const project = getCurrentProject();
474
+ if (!project) {
475
+ editorConsole.error(
476
+ 'Manifest declares an ingest-pixi world but no project is open — cannot auto-ingest',
477
+ 'ingest',
478
+ );
479
+ return true;
480
+ }
481
+ try {
482
+ await mountCanvasIngestRootFromManifest(store, ingestRoot, project.rootPath);
483
+ const gameContainer = liveDocumentContainer();
484
+ if (gameContainer) {
485
+ const siblings = await mountCompositeSiblings(
486
+ manifest,
487
+ ingestRoot.id,
488
+ gameContainer,
489
+ project.rootPath,
490
+ store,
491
+ );
492
+ // D-E2: promote to a composite once any sibling actually mounted —
493
+ // byte-stable (no-op) for a single-world manifest.
494
+ promoteToCompositeAuthoring(manifest, ingestRoot, siblings);
495
+ }
496
+ } catch (err) {
497
+ // D-W3 (F9) + S-1 — see tryManifestIngestRoute's identical comment above.
498
+ reportFailedIngestMount(store, {
499
+ worldId: ingestRoot.id,
500
+ kind: ingestRoot.surface,
501
+ identity: ingestRoot.adapter.identity,
502
+ message: formatMountFailureMessage(err),
503
+ });
504
+ }
505
+ return true;
506
+ }
507
+
508
+ /**
509
+ * DOM sibling of {@link tryManifestIngestRoute}/{@link tryManifestIngestRoute2D}
510
+ * (D-Y2, slice S2): a manifest-backed project declaring an `ingest-react` root
511
+ * mounts the native-React ingest session straight from that root (external
512
+ * folder — `vgai edit <folder>` — not the in-tree `ingest/games/<id>` fixture
513
+ * registry). Same "applied or not" contract, same D-V2 composite scan/
514
+ * route-selection/sibling-mount shape as its three/canvas siblings above.
515
+ */
516
+ async function tryManifestIngestRouteReact(
517
+ store: EditorShellStore,
518
+ manifest: ResolvedGameManifest | null,
519
+ ): Promise<boolean> {
520
+ if (!manifest) return false;
521
+ const ingestRoots = findIngestRoots(manifest);
522
+ if (ingestRoots.length === 0) return false;
523
+ if (ingestRoots.length > 1) {
524
+ reportTooManyIngestRoots(ingestRoots);
525
+ return true;
526
+ }
527
+ const ingestRoot = ingestRoots[0]!;
528
+ if (ingestRoot.adapter.identity !== 'ingest-react') return false;
529
+
530
+ const project = getCurrentProject();
531
+ if (!project) {
532
+ editorConsole.error(
533
+ 'Manifest declares an ingest-react world but no project is open — cannot auto-ingest',
534
+ 'ingest',
535
+ );
536
+ return true;
537
+ }
538
+ try {
539
+ await mountDomIngestRootFromManifest(store, manifest, ingestRoot, project.rootPath);
540
+ const gameContainer = liveDocumentContainer();
541
+ if (gameContainer) {
542
+ const siblings = await mountCompositeSiblings(
543
+ manifest,
544
+ ingestRoot.id,
545
+ gameContainer,
546
+ project.rootPath,
547
+ store,
548
+ );
549
+ // D-E2: promote to a composite once any sibling actually mounted —
550
+ // byte-stable (no-op) for a single-world manifest.
551
+ promoteToCompositeAuthoring(manifest, ingestRoot, siblings);
552
+ }
553
+ } catch (err) {
554
+ // D-W3 (F9) + S-1 — see tryManifestIngestRoute's identical comment above.
555
+ reportFailedIngestMount(store, {
556
+ worldId: ingestRoot.id,
557
+ kind: ingestRoot.surface,
558
+ identity: ingestRoot.adapter.identity,
559
+ message: formatMountFailureMessage(err),
560
+ });
561
+ }
562
+ return true;
563
+ }
564
+
565
+ /**
566
+ * Hosted entry point, called once after `loadInitialScene`: mount whichever
567
+ * `{ ingest }` root the open project declares, from whichever discovery route
568
+ * applies, so ingestion needs no dev hook. Checked in order (§1B's "manifest
569
+ * wins" discovery order):
570
+ *
571
+ * 1. Manifest route, `{ module }` (T3.4 slice 2, `./module-mode.ts`'s
572
+ * `tryManifestModuleRoute`) — a manifest's first world declaring a custom
573
+ * adapter module auto-mounts through the generic
574
+ * `RootAdapter`/`ThreeHostContext` contract (or, since D-Y1, the
575
+ * WORLDS PATH for a non-three module world — see that file) and installs ITS
576
+ * OWN `MountedThreeRoot.authoring` as the active override, so the SAME
577
+ * adapter-driven hierarchy/inspector panels below drive it. Mutually
578
+ * exclusive with routes 2–4 by construction (a world resolves to exactly one
579
+ * adapter identity).
580
+ * 2. Manifest route, three (T3.3 slice 3, {@link tryManifestIngestRoute}) — the
581
+ * CLI-on-folder AC ("point the CLI at an ingest game folder never registered
582
+ * in source -> opens, plays, edits"). Browser-mode builds fall straight
583
+ * through to route 5.
584
+ * 3. Manifest route, canvas (T6.1 slice 4, {@link tryManifestIngestRoute2D}) —
585
+ * the discovery path for an `ingest-pixi` world.
586
+ * 4. Manifest route, dom (D-Y2, {@link tryManifestIngestRouteReact}) —
587
+ * the external-folder discovery path for an `ingest-react` world. A
588
+ * manifest's first world resolves to exactly one adapter identity, so
589
+ * routes 1–4 are mutually exclusive in practice; all are tried since
590
+ * neither can tell that in advance without fetching.
591
+ *
592
+ * There is no sixth route by id. A vendored game IS a project and opens through
593
+ * the session like every other one, which is what routes 2–4 already read.
594
+ */
595
+ export function autoLaunchIngest(store: EditorShellStore): Promise<void> {
596
+ return dedupeAutoLaunch(store, () => autoLaunchIngestInner(store));
597
+ }
598
+
599
+ /**
600
+ * ONE auto-launch per (session, open project) — closing the
601
+ * double-entry the #745 review measured.
602
+ *
603
+ * Two independent bootstraps call {@link autoLaunchIngest}:
604
+ * `world-root-stage.ts`'s boot chain and `NonThreeAuthoringBootstrap.tsx`'s. Both
605
+ * are correct to — neither can know whether the other exists for this project —
606
+ * so the same launch happens twice. `serializeEntry` keeps that
607
+ * from leaving two live sessions, but serializing is not the same as
608
+ * deduplicating: the second call still tears the first mount down and MOUNTS THE
609
+ * GAME AGAIN. For anything with a module-level side effect that is a coin
610
+ * flip — a game registering a named easing curve at import time dies on the
611
+ * second construction with `Custom ease already registered`.
612
+ *
613
+ * The ARCHITECTURE-CORE contract behind problem 17 says it plainly: LOADING
614
+ * CONSTRUCTS, and it constructs once. So the launch is memoized on the store
615
+ * (session identity — a `WeakMap` keeps that from leaking) crossed with the
616
+ * OPEN PROJECT, which is precisely what determines what the launch will do:
617
+ * every route above reads the open project (locally the session's, hosted the
618
+ * one `?project=<id>` resolved) and nothing reads a URL. Both callers await the
619
+ * SAME promise and observe the same outcome, including a rejection.
620
+ */
621
+ const _autoLaunched = new WeakMap<EditorShellStore, Map<string, Promise<void>>>();
622
+
623
+ function dedupeAutoLaunch(store: EditorShellStore, run: () => Promise<void>): Promise<void> {
624
+ let perStore = _autoLaunched.get(store);
625
+ if (!perStore) {
626
+ perStore = new Map();
627
+ _autoLaunched.set(store, perStore);
628
+ }
629
+ // `?? ''` because the key only has to be STABLE per session: a boot with no
630
+ // project open yet (the local no-project dev server, a harness stub) has
631
+ // exactly one launch to dedupe, so one key is the right answer there rather
632
+ // than a crash on the boot path.
633
+ const key = getCurrentProject()?.rootPath ?? '';
634
+ const existing = perStore.get(key);
635
+ if (existing) return existing;
636
+ const started = run();
637
+ perStore.set(key, started);
638
+ return started;
639
+ }
640
+
641
+ /** TEST-ONLY: forget this store's auto-launch memo (see {@link dedupeAutoLaunch}). */
642
+ export function resetAutoLaunchDedupeForTests(store: EditorShellStore): void {
643
+ _autoLaunched.delete(store);
644
+ }
645
+
646
+ /**
647
+ * THE route chain, with no opinion about when it runs. Called at boot by
648
+ * {@link autoLaunchIngest} and at the play edge by
649
+ * `deferred-ingest-play.ts`'s `mountDeferredIngestForPlay` — one mount, two
650
+ * moments, so a deferred root's Play is byte-for-byte the mount that used to
651
+ * happen at boot rather than a second implementation of it.
652
+ */
653
+ export async function launchIngestRoutes(store: EditorShellStore): Promise<void> {
654
+ const { tryManifestModuleRoute } = await import('./module-mode');
655
+ if (await tryManifestModuleRoute(store)) return;
656
+ // ONE read for all three manifest routes (see `readIngestChainManifest`):
657
+ // they used to fetch it three times and swallow every failure identically.
658
+ const manifest = await readIngestChainManifest();
659
+ if (await tryManifestIngestRoute(store, manifest)) return;
660
+ if (await tryManifestIngestRoute2D(store, manifest)) return;
661
+ await tryManifestIngestRouteReact(store, manifest);
662
+ }
663
+
664
+ async function autoLaunchIngestInner(store: EditorShellStore): Promise<void> {
665
+ // Edit already owns pieces (a named world component, or isolation-document
666
+ // scene tabs) — do not construct the live game underneath them. Play
667
+ // mounts it (`deferred-ingest-play.ts`). Land on the default Edit piece.
668
+ const { deferredIngestPlayActive, ingestMountIsDeferredToPlay } = await import(
669
+ './deferred-ingest-play'
670
+ );
671
+ if (await ingestMountIsDeferredToPlay()) {
672
+ // Story discovery can make the second boot caller arrive after the user
673
+ // has already pressed ▶ and the deferred mount has claimed the session.
674
+ // That late BOOT intent is stale: landing on Edit here used to switch the
675
+ // running game out from under the user, and once cold landing also owned
676
+ // the required hold it paused the game as well. Play is the newer owner;
677
+ // never let an old bootstrap override it.
678
+ if (deferredIngestPlayActive()) return;
679
+ const { landIngestBootInEdit } = await import('./ingest-boot-viewport');
680
+ landIngestBootInEdit(store);
681
+ return;
682
+ }
683
+ await launchIngestRoutes(store);
684
+ }
685
+
686
+ // Dev hook so the editor UI / e2e can drive a mount against the bound store.
687
+ // The `typeof window` half matters: this module is imported by
688
+ // `command-listener.ts` (S-2 routes relayed play control to the ingest surface),
689
+ // and several of that module's suites run under the Node vitest environment
690
+ // where a bare top-level `window` write is a load-time ReferenceError.
691
+ if (import.meta.env.DEV && typeof window !== 'undefined') {
692
+ (window as unknown as Record<string, unknown>)['__vgaiIngestApi'] = {
693
+ enter: (gameId: string) => {
694
+ if (!_boundStore) throw new Error('ingest roots not bound');
695
+ return mountThreeIngestRootById(_boundStore, gameId);
696
+ },
697
+ exit: unmountThreeIngestRoot,
698
+ // This hook is the per-surface (three) API — keep it slot-specific;
699
+ // `isIngestActive` itself is kind-agnostic as of F26.
700
+ isActive: () => activeIngest()?.kind === 'three',
701
+ };
702
+ // Canvas sibling (T6.1 slice 4) — same shape, over the canvas ingest session.
703
+ (window as unknown as Record<string, unknown>)['__vgaiIngestApi2D'] = {
704
+ enter: (gameId: string) => {
705
+ if (!_boundStore) throw new Error('ingest roots not bound');
706
+ return mountCanvasIngestRootById(_boundStore, gameId);
707
+ },
708
+ exit: unmountCanvasIngestRoot,
709
+ isActive: () => activeIngest()?.kind === 'canvas',
710
+ };
711
+ // DOM sibling (Track N, N2) — same shape, over the native-React ingest
712
+ // session.
713
+ (window as unknown as Record<string, unknown>)['__vgaiIngestApiReact'] = {
714
+ enter: (folderId: string) => {
715
+ if (!_boundStore) throw new Error('ingest roots not bound');
716
+ return mountDomIngestRoot(_boundStore, folderId);
717
+ },
718
+ exit: unmountDomIngestRoot,
719
+ isActive: () => activeIngest()?.kind === 'dom',
720
+ };
721
+ // Headless-readable mount outcomes —
722
+ // `packages/editor/src/authoring/mount-failure-report.ts`'s multi-entry
723
+ // list is otherwise only ever read by the status bar's `mount-failure`
724
+ // contribution (a React component), invisible to `page.evaluate()`. This is
725
+ // a plain function returning the LIVE list — `vgai doctor`'s browser
726
+ // phase is this surface's first consumer, but any other dev/e2e caller can
727
+ // poll it the same way `__vgaiIngestApi*` are already polled.
728
+ (window as unknown as Record<string, unknown>)['__vgaiMountFailureReports'] = () =>
729
+ getMountFailureReports();
730
+ // D-H1 (Slice B): headless-readable per-sibling signal — the live slot's
731
+ // `siblings` (D-V2's composite-sibling handles, `ingest-siblings.ts`'s
732
+ // `SiblingMount[]`) is otherwise invisible to `page.evaluate()`, same blind spot
733
+ // `__vgaiMountFailureReports` closed for mount failures. A plain function
734
+ // returning a LIVE view (never a snapshot, re-reads the slot on every call) of
735
+ // honest per-sibling flags only — no capture/draw proof, see
736
+ // `DoctorSiblingEvidence`'s doc comment (`packages/editor/src/doctor/report.ts`).
737
+ (window as unknown as Record<string, unknown>)['__vgaiSiblingMounts'] = () =>
738
+ (activeIngest()?.siblings ?? []).map((s) => ({
739
+ worldId: s.worldId,
740
+ setPausedPresent: typeof s.setPaused === 'function',
741
+ authoringPresent: !!s.authoring,
742
+ authoringPersist: s.authoring?.capabilities.persist === true,
743
+ }));
744
+ }