@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,366 @@
1
+ /**
2
+ * Mount a `{ ingest }` root on the THREE surface — an UNMODIFIED three.js game
3
+ * loaded into the real editor with ZERO edits to the game's code.
4
+ *
5
+ * This is a thin HOST. It mounts the game through the
6
+ * {@link mountIngestGame} `RootAdapter` path — which captures the game's live
7
+ * scene and returns an `IngestMount` whose `authoring` is the three
8
+ * authoring adapter implemented DIRECTLY over the live `Object3D` tree (NO
9
+ * fabricated descriptors). The host then:
10
+ * - installs that adapter as the editor's active authoring override, so the
11
+ * hierarchy + inspector render from the adapter's providers, and
12
+ * - swaps the editor viewport onto the live scene (`enterPlayScene`, handed
13
+ * the adapter's pure stable-id projection) so selection + gizmo work.
14
+ *
15
+ * The first-party editor surface is untouched; the SAME editor surface now
16
+ * drives a foreign game. Its canvas and DOM siblings live in
17
+ * `mount-canvas-ingest-root.ts` and `mount-dom-ingest-root.ts`; the routing
18
+ * that picks between them is `mount-ingest-root.ts`.
19
+ */
20
+
21
+ import { measureAdapterReach } from '../host/adapter-reach';
22
+ import { setActiveAuthoring } from '@volter/editor-core/authoring/active-adapter';
23
+ import { setActiveSystems } from '@volter/editor-core/authoring/active-systems';
24
+ import { withContractHierarchy } from '../host/authoring/contract-hierarchy-authoring';
25
+ import { clearMountFailureReports } from '@volter/editor-core/authoring/mount-failure-report';
26
+ import { editorConsole } from '@volter/editor-core/editor-console';
27
+ import type { EditorShellStore } from '@volter/editor-core/editor-shell-store';
28
+ import { gameLoopGate, setGameInputGate } from '../host/gated-globals';
29
+ import { acquireLiveDocument, liveDocumentContainer } from '@volter/editor-core/live-document';
30
+ import { pickGameCamera } from '@volter/editor-core/scene-framing';
31
+ import type { ResolvedAdapterRoot } from '@volter/editor-project/manifest/load';
32
+ import type { CaptureMechanism } from '@volter/threejs-runtime/adapter/ingest/scene-capture';
33
+ import { type IngestSession, serializeEntry, setActiveIngest } from './active-ingest';
34
+ import { withDetectedDomSurface } from './authoring/ingest-dom-surface-authoring';
35
+ import { type MountIngestOptions, mountIngestGame } from './authoring/ingest-root-adapter';
36
+ import { releaseGamePointerLock } from './game-pointer-lock';
37
+ import { landIngestBootInEdit } from './ingest-boot-viewport';
38
+ import {
39
+ defineLoopHookFields,
40
+ ingestHookEvidence,
41
+ publishIngestHook,
42
+ } from './ingest-evidence-hook';
43
+ import {
44
+ activeIngestContract,
45
+ bindIngestLifecycle,
46
+ ingestPlaying,
47
+ setIngestPlaying,
48
+ } from './ingest-play-control';
49
+ import {
50
+ beginMountCoverage,
51
+ recordMountCoverage,
52
+ reportMeasuredLoop,
53
+ threeMountLoopFacts,
54
+ } from './mount-coverage';
55
+ import { resolveIngestDescriptor } from './resolve-three';
56
+ import { getIngestGame } from './surface-three';
57
+ import type { IngestGame } from './types';
58
+ import { exitActiveIngest, unmountThreeIngestRoot } from './unmount-ingest-root';
59
+
60
+ /**
61
+ * How this mount reached the game's runtime, named for the reader of any
62
+ * capability warning it produces — and named from the MEASUREMENT
63
+ * (`SceneCaptureHandle.capturedVia`), never assumed.
64
+ *
65
+ * There are two, and the difference is what a reader needs: a `shared-three`
66
+ * game runs on the editor's very `three` instance; a `devtools-observer` game
67
+ * runs its OWN pinned revision (a vendored build bundles or pins one) and only
68
+ * its renderer is shared, through three's public `__THREE_DEVTOOLS__` seam.
69
+ * Printing the first sentence for both is how a coverage row starts lying.
70
+ */
71
+ function captureMechanism(via: CaptureMechanism | null): string {
72
+ if (via === 'devtools-observer') {
73
+ return (
74
+ "this game runs its OWN copy of three; the host reached its renderer through three's " +
75
+ '`__THREE_DEVTOOLS__` observation seam'
76
+ );
77
+ }
78
+ return "this game imports the host's three directly, in the editor's own realm";
79
+ }
80
+
81
+ /**
82
+ * Publish a captured three ingest to the whole editor: resize lifecycle,
83
+ * hierarchy/inspector adapter, viewport adoption and the `__vgaiIngest`
84
+ * proof hook.
85
+ */
86
+ function activateCapturedThreeIngest(args: {
87
+ store: EditorShellStore;
88
+ session: IngestSession;
89
+ gameContainer: HTMLElement;
90
+ sessionId: string;
91
+ }): void {
92
+ const { store, session, sessionId } = args;
93
+ const mount = session.mount;
94
+ if (!mount) return;
95
+ const rawAuthoring = mount.authoring;
96
+ const contract = activeIngestContract();
97
+ const contractAuthoring = withContractHierarchy(rawAuthoring, contract?.systems?.hierarchy);
98
+ const liveAuthoring = withDetectedDomSurface({
99
+ primary: contractAuthoring,
100
+ worldId: sessionId,
101
+ hostEl: mount.hostEl,
102
+ ...(contract?.root ? { declaredRoot: contract.root } : {}),
103
+ store,
104
+ });
105
+ const liveScene = mount.scene;
106
+
107
+ // Host lifecycle: keep the game's renderer sized to its container (resize).
108
+ const ro = new ResizeObserver(() => {
109
+ const r = args.gameContainer.getBoundingClientRect();
110
+ if (r.width > 0 && r.height > 0) mount.game.resize?.(r.width, r.height);
111
+ });
112
+ ro.observe(args.gameContainer);
113
+ session.resizeObserver = ro;
114
+
115
+ const stats = rawAuthoring.refresh();
116
+ // WHERE EDITS GO, measured rather than asserted. This line used to end in a
117
+ // flat "edits are live-only (this game has no source the editor may write)",
118
+ // which is a claim about every ingested game ever mounted — and it was wrong
119
+ // for the first one whose own source the editor's authoring transform
120
+ // reached. The adapter's own reach measurement answers instead, in the
121
+ // persistence backend's own words.
122
+ const reachNow = rawAuthoring.measureWriteReach();
123
+ editorConsole.log(
124
+ `Captured live scene (${liveScene.children.length} top-level children, ` +
125
+ `${mount.capture.getDrawCount()} draws); ingested ${stats.count} objects ` +
126
+ `(${stats.meshes} meshes, ${stats.lights} lights` +
127
+ // Named only when there IS instanced content: an InstancedMesh is one
128
+ // object drawing N units, so every count on this line under-reports the
129
+ // scene by `instances - instancedMeshes` without saying so.
130
+ (stats.instancedMeshes > 0
131
+ ? `, incl. ${stats.instancedMeshes} InstancedMesh drawing ${stats.instances} units`
132
+ : '') +
133
+ '); ' +
134
+ `${reachNow.addressable} of ${reachNow.total} have a source address — ` +
135
+ `edits go to ${reachNow.destination}`,
136
+ 'ingest',
137
+ );
138
+ reportMeasuredLoop(sessionId, mount.realmLoopVerdict?.() ?? undefined);
139
+
140
+ // The editor's hierarchy + inspector now render from THIS adapter.
141
+ setActiveAuthoring(liveAuthoring);
142
+ // An unmodified game exposes NO engine System adapters (it owns its own
143
+ // physics/input/animation/etc.) → those are correctly N/A while ingested. Only
144
+ // the asset-path remap (done at mount) is a first-party asset-adapter behavior.
145
+ // The ONE exception is earned, never inferred: a game that DECLARES verbs and
146
+ // state in its contract (`window.vgaiGame.systems`) gets a `debug` adapter
147
+ // projected from those declarations by `mountIngestGame`, so `game.commands()`
148
+ // /`game.state()` reach it through the ordinary bridge. A game that declares
149
+ // nothing still lands here with `{}` and the pre-contract floor stands.
150
+ setActiveSystems(mount.game.systems ?? {});
151
+ // Bind the viewport/objectMap/gizmo to the live scene through the adapter's
152
+ // pure projection. The game's own Object3Ds remain untouched.
153
+ store.enterPlayScene(liveScene, undefined, rawAuthoring.objectMapSnapshot());
154
+ // The editor camera boots at (10,10,10) looking at the origin — a pose that
155
+ // means something for a scene the reader authored, and nothing at all for a
156
+ // world they did not: an ingested world centred hundreds of units from the
157
+ // origin and hundreds of units across leaves the opening Scene view on empty
158
+ // space. Frame the world instead — from the GAME'S OWN camera when it has
159
+ // one, which is the view its author chose. The lookup is passed LIVE, not
160
+ // resolved here: an R3F game's camera is often not in the scene yet on the
161
+ // frame we capture it on (its `<Suspense>` content has not resolved), and a
162
+ // camera resolved once at mount would miss it. The viewport measures a
163
+ // framing meanwhile, and keeps asking until there IS content — a
164
+ // level-streaming game captures on its first drawn frame and then parses its
165
+ // world in for several seconds afterwards.
166
+ store.focusOnScene(() => pickGameCamera(mount.capture.captured?.camera, liveScene));
167
+ landIngestBootInEdit(store);
168
+ store.notifyIngestEdit(); // ensure panels re-render against the override
169
+ clearMountFailureReports(); // D-W3: mount succeeded
170
+
171
+ // What this editor CANNOT do with this game, said out loud once, right where
172
+ // the mount has just finished saying what it could (`capability-coverage.ts`).
173
+ const reach = measureAdapterReach({ authoring: liveAuthoring });
174
+ recordMountCoverage({
175
+ reach,
176
+ reachMechanism: captureMechanism(mount.capture.capturedVia),
177
+ loop: () => threeMountLoopFacts(mount),
178
+ // The adapter's OWN measurement of how far this world reaches into source.
179
+ // A function, like the loop verdict beside it: a captured world keeps
180
+ // streaming objects in, so a count taken at mount would start lying.
181
+ writeReach: () => rawAuthoring.measureWriteReach(),
182
+ });
183
+
184
+ // Dev/proof hook: expose ingest state for automation + screenshots.
185
+ const hook: Record<string, unknown> = {
186
+ gameId: sessionId,
187
+ ...ingestHookEvidence(liveAuthoring),
188
+ drawCount: () => mount.capture.getDrawCount(),
189
+ // LIVE, like `drawCount`/`engineSystems` beside it — not the mount-time
190
+ // `stats` snapshot this used to hand out. An ingested game keeps loading
191
+ // after we mount it (games-fps streams its whole collision world in via
192
+ // GLTFLoader well after first capture), so a frozen count is not merely
193
+ // stale, it is unfalsifiable: anything polling it waits forever on a
194
+ // number that cannot change. That cost a week of nightly red, read the
195
+ // whole time as "the world GLTF never loads" — the asset was arriving
196
+ // fine with a 200, and the snapshot simply predated it.
197
+ get reflected() {
198
+ return rawAuthoring.refresh();
199
+ },
200
+ get sceneChildren() {
201
+ return liveScene.children.length;
202
+ },
203
+ // D-A3: the MEASURED editor reach of a headless-verifiable healthy mount.
204
+ reach,
205
+ // Host-lifecycle resize: drive the game renderer's size (proof/automation).
206
+ resize: (w: number, h: number) => mount.game.resize?.(w, h),
207
+ // Loop-host: drive the SAME normalized lifecycle as the Play surface.
208
+ ...(session.lifecycle.setPaused ? { setPaused: session.lifecycle.setPaused } : {}),
209
+ // DEV-only: the live authoring adapter + store, so e2e/proof can drive the
210
+ // SAME providers the hierarchy/inspector/gizmo use.
211
+ ...(import.meta.env.DEV ? { adapter: liveAuthoring, store } : {}),
212
+ };
213
+ defineLoopHookFields(hook, mount);
214
+ publishIngestHook('__vgaiIngest', hook);
215
+ }
216
+
217
+ async function mountThreeIngestRootInner(
218
+ store: EditorShellStore,
219
+ game: IngestGame,
220
+ sessionId: string,
221
+ ): Promise<void> {
222
+ // D-V1: tear down WHATEVER kind is currently active, not just a prior
223
+ // three session — closes the cross-kind leak (F26 residual).
224
+ exitActiveIngest();
225
+
226
+ // A new mount is a new coverage answer, even for the same world: the console
227
+ // door's once-guard keys on this token.
228
+ beginMountCoverage(sessionId);
229
+
230
+ // The Game document exists only while a runtime does, so install it (and
231
+ // wait for its panel to commit) before reading the container it owns.
232
+ await acquireLiveDocument();
233
+ const gameContainer = liveDocumentContainer();
234
+ if (!gameContainer) {
235
+ editorConsole.error('Game container not mounted — cannot ingest', 'ingest');
236
+ return;
237
+ }
238
+
239
+ // Ingest edits are LIVE-ONLY. Autosave/save-guard no longer suppress on a
240
+ // flag — the active adapter installed below (setActiveAuthoring) carries its
241
+ // own persistence provider, which is what EditorShellStore's autosave/saveNow
242
+ // now consult (design). Still switch into the guarded "playing" state so the
243
+ // viewport swaps onto the live scene.
244
+ store.setPlayState('playing');
245
+ editorConsole.log(`Ingesting unmodified game: ${game.name}`, 'ingest');
246
+
247
+ // Drive the SAME input gate play-mode uses (play-mode.ts's
248
+ // `gameInputActive`, wired via `setGameInputGate`) so an ingested game's raw
249
+ // window/document input listeners (gated via gated-globals.ts's proxy —
250
+ // reachable because `server/dev.ts`'s prelude shadows the in-tree ingest
251
+ // fixture roots too, see game-globals-shadow.ts) only fire while the Play
252
+ // tab is the active viewport surface, exactly like a first-party play
253
+ // session.
254
+ const ingestInputActive = () =>
255
+ store.playState === 'playing' && store.activeViewportTab === 'play';
256
+
257
+ try {
258
+ const mountOpts: MountIngestOptions = { captureTimeoutMs: game.captureTimeoutMs };
259
+
260
+ // Mount the unmodified game through the RootAdapter path → captured live
261
+ // scene + a three authoring adapter over its Object3D tree,
262
+ // whose edits are live-only (no sidecar, no source write).
263
+ const mount = await mountIngestGame(store, game, gameContainer, mountOpts);
264
+ const setPaused = mount.game.setPaused;
265
+ const session: IngestSession = {
266
+ store,
267
+ mount,
268
+ lifecycle: bindIngestLifecycle(
269
+ setPaused
270
+ ? {
271
+ setPaused: (paused) => setPaused(paused),
272
+ step: () => gameLoopGate()?.step(1),
273
+ canStep: () =>
274
+ mount.realmLoopVerdict?.()?.loop === 'gated' && gameLoopGate() !== null,
275
+ }
276
+ : {
277
+ pauseGap: 'the captured Three root exposes no mounted setPaused capability',
278
+ },
279
+ ),
280
+ };
281
+ setActiveIngest({ kind: 'three', session, siblings: [], worldId: sessionId });
282
+ setGameInputGate(ingestInputActive);
283
+ // The other half of the pointer-lock gate (`ingest/game-pointer-lock.ts`
284
+ // refuses ACQUISITION while the gate is closed): a lock the game already
285
+ // holds when the gate CLOSES — leaving play, or switching to the Edit tab
286
+ // mid-flight — must be released, or the editor is left with a captured,
287
+ // invisible cursor over a game that is no longer receiving input.
288
+ session.inputGateUnsubscribe = store.subscribe(() => {
289
+ if (!ingestInputActive()) releaseGamePointerLock();
290
+ });
291
+ // The realm gate only just became reachable, so push the play state we
292
+ // already hold onto it (see setIngestPlaying). A fresh mount is not
293
+ // playing, which is what makes Edit quiet from the moment the host can
294
+ // speak to the realm at all.
295
+ setIngestPlaying(ingestPlaying());
296
+
297
+ activateCapturedThreeIngest({ store, session, gameContainer, sessionId });
298
+ } catch (err) {
299
+ editorConsole.error(`Ingest failed: ${err}`, 'ingest');
300
+ unmountThreeIngestRoot();
301
+ throw err;
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Mount an already-resolved {@link IngestGame} descriptor, however it was
307
+ * built. Resolves once the game's live scene is captured, the authoring adapter
308
+ * is active, and it is shown in the editor for editing.
309
+ *
310
+ * T3.3 slice 3 split this out of {@link mountThreeIngestRootById} so the
311
+ * descriptor's ORIGIN is a separate concern from mounting it:
312
+ * {@link mountThreeIngestRootById} resolves `gameId` against the in-tree
313
+ * fixture glob (`ingest/surface-three.ts`);
314
+ * {@link mountThreeIngestRootFromManifest} builds the descriptor straight from
315
+ * an arbitrary project's manifest instead (the CLI-on-folder route, for a game
316
+ * folder never registered in source) — both end up here. `sessionId` is the
317
+ * id this session is known by — the `__vgaiIngest` dev-hook's reported
318
+ * `gameId` and the id every log line names; it is always the world/game id
319
+ * (`game.id`), passed explicitly rather than re-read off `game` so the two
320
+ * callers' "id" concept — a fixture's registry-era id vs. a manifest's world
321
+ * id — stays one parameter, not two silently-assumed-equal fields.
322
+ *
323
+ * Serialized through the shared entry queue; see `active-ingest.ts`'s
324
+ * {@link serializeEntry}.
325
+ */
326
+ export function mountThreeIngestRoot(
327
+ store: EditorShellStore,
328
+ game: IngestGame,
329
+ sessionId: string,
330
+ ): Promise<void> {
331
+ return serializeEntry(() => mountThreeIngestRootInner(store, game, sessionId));
332
+ }
333
+
334
+ /**
335
+ * Mount an in-tree fixture game by its discovered id —
336
+ * `ingest/surface-three.ts`'s glob discovery is the lookup.
337
+ */
338
+ export async function mountThreeIngestRootById(
339
+ store: EditorShellStore,
340
+ gameId: string,
341
+ ): Promise<void> {
342
+ const game = getIngestGame(gameId);
343
+ if (!game) {
344
+ editorConsole.error(`Unknown ingest game: ${gameId}`, 'ingest');
345
+ return;
346
+ }
347
+ return mountThreeIngestRoot(store, game, gameId);
348
+ }
349
+
350
+ /**
351
+ * Mount straight from a manifest's `ingest-three` world — the CLI-on-folder
352
+ * route (T3.3 slice 3): a game folder never registered in source, whose
353
+ * `vgai.project.json` declares an ingest-three world. Builds the descriptor via
354
+ * `resolveIngestDescriptor` (ingest/resolve-three.ts — parameterized by the
355
+ * manifest's ingest fields, no registry lookup) and mounts it exactly like any
356
+ * other {@link IngestGame}. The session is known by the world id — the same
357
+ * namespace an in-tree fixture id lives in, different origin.
358
+ */
359
+ export async function mountThreeIngestRootFromManifest(
360
+ store: EditorShellStore,
361
+ world: ResolvedAdapterRoot,
362
+ projectRoot: string,
363
+ ): Promise<void> {
364
+ const game = resolveIngestDescriptor(world, projectRoot);
365
+ return mountThreeIngestRoot(store, game, world.id);
366
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * An `ingest-pixi` root's descriptor — the canvas sibling of
3
+ * `./resolve-three.ts`, and here for the same reason: served-bundle vs `/@fs/`
4
+ * is a per-PROJECT property, not a per-realm one.
5
+ */
6
+
7
+ import type { IngestGame2D } from '@volter/game-runtime/pixi/ingest';
8
+ import type { ResolvedAdapterRoot } from '@volter/editor-project/manifest/load';
9
+ import { composeIngestLoad } from './entry-load';
10
+ import { ingestModuleLoaders } from './resolve-three';
11
+
12
+ /**
13
+ * Build the {@link IngestGame2D} descriptor an `ingest-pixi` world resolves
14
+ * to, straight from its manifest fields: the world's `entry` becomes a
15
+ * `load()` thunk over the same loader pair the three lane uses, so the game's
16
+ * modules are served with `pixi.js` deduped to the editor's own instance — and,
17
+ * for a served bundle, over the served-URL route instead.
18
+ */
19
+ export function resolveIngest2DDescriptor(
20
+ world: ResolvedAdapterRoot,
21
+ projectRoot: string,
22
+ ): IngestGame2D {
23
+ const { adapter } = world;
24
+ if (adapter.type !== 'ingest') {
25
+ throw new Error(`resolveIngest2DDescriptor: world "${world.id}" is not an { ingest } world.`);
26
+ }
27
+ if (!world.entry) {
28
+ throw new Error(
29
+ `resolveIngest2DDescriptor: world "${world.id}" has no \`entry\` — an ingest root mounts ` +
30
+ "the game's own entry module.",
31
+ );
32
+ }
33
+
34
+ const loaders = ingestModuleLoaders(projectRoot);
35
+ return {
36
+ id: world.id,
37
+ name: world.id,
38
+ description: world.description ?? '',
39
+ ...(adapter.captureTimeoutMs !== undefined
40
+ ? { captureTimeoutMs: adapter.captureTimeoutMs }
41
+ : {}),
42
+ load: composeIngestLoad(
43
+ loaders.entry(world.entry),
44
+ adapter.contractShim === undefined ? undefined : loaders.module(adapter.contractShim),
45
+ ),
46
+ };
47
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * An `ingest-three` root's descriptor, built straight from its manifest fields.
3
+ *
4
+ * This lives with the rest of the ingest code, NOT in `RealmServices`, because
5
+ * the choice it makes is a PER-PROJECT property rather than a per-realm one:
6
+ * whether this game's modules are reached by URL (its folder is a served
7
+ * bundle under the editor's own `public/`) or by `/@fs/` is decided by how
8
+ * that one game was vendored, and two ingested games open in the same realm
9
+ * can answer differently. A realm cannot know it; only the project can.
10
+ */
11
+
12
+ import {
13
+ beginProjectMountEpoch,
14
+ fsImportPath,
15
+ INGEST_REMOUNT_QUERY,
16
+ } from '@volter/editor-sdk/session/project-module-url';
17
+ import type { ResolvedAdapterRoot } from '@volter/editor-project/manifest/load';
18
+ import { composeIngestLoad, declaredSourceModules, ingestDataWriter } from './entry-load';
19
+ import { servedEntryLoader, servedModuleLoader, servedProjectBaseUrl } from './served-bundle';
20
+ import type { IngestGame } from './types';
21
+
22
+ /**
23
+ * How THIS project's ingest modules are reached: by URL when the folder is a
24
+ * SERVED BUNDLE under the editor's own `public/`, by `/@fs/` otherwise.
25
+ *
26
+ * Vite refuses to put a file inside its `public/` directory into the module
27
+ * graph at all, so `/@fs/` is not merely suboptimal there — it is a 500. See
28
+ * `./served-bundle.ts` for the measured failure and the rule it restores.
29
+ *
30
+ * THE TWO SURFACES WANT OPPOSITE REUSE, so `remountEpochs` is a parameter
31
+ * rather than a policy:
32
+ *
33
+ * - A THREE ingest's teardown disposes the R3F root, so a remount MUST
34
+ * re-evaluate the entry — `remountEpochs: true` gives every `load()` its
35
+ * own `?vgai-ingest-remount=` buster (see `INGEST_REMOUNT_QUERY` for why
36
+ * it is deliberately NOT `vgai-mount`, whose realm-keying and subgraph
37
+ * stamping the ingest readers cannot follow; an ingest's `load` IS its
38
+ * mount). A bare URL made stop → ▶ a NO-OP: browser module identity is
39
+ * per-URL, the already-evaluated entry's top-level boot did not re-run,
40
+ * nothing rendered, and the mount failed by name after the 10s measured
41
+ * wait — measured on racing-game, 2026-08-21.
42
+ * - A CANVAS ingest's module-scope `Application` SURVIVES teardown and the
43
+ * remount re-adopts it through the render trap — the cached no-op
44
+ * re-import is the design. Busting it evaluated a SECOND app whose asset
45
+ * load died against the realm's already-populated singleton registries
46
+ * (pixi-sound's alias table) and the play mount failed "rendered no
47
+ * capturable frame" — measured on bubbo-bubbo the same day, by the same
48
+ * edit that fixed racing.
49
+ *
50
+ * The served-bundle lane is already fresh per call (it evaluates a new blob
51
+ * URL each time) and takes no query either way.
52
+ */
53
+ export function ingestModuleLoaders(
54
+ projectRoot: string,
55
+ options: { remountEpochs: boolean } = { remountEpochs: false },
56
+ ): {
57
+ entry: (relPath: string) => () => Promise<unknown>;
58
+ module: (relPath: string) => () => Promise<unknown>;
59
+ } {
60
+ const servedBase = servedProjectBaseUrl(projectRoot);
61
+ if (servedBase === undefined) {
62
+ const fs = (rel: string) => () =>
63
+ import(
64
+ /* @vite-ignore */ options.remountEpochs
65
+ ? `${fsImportPath(projectRoot, rel)}?${INGEST_REMOUNT_QUERY}=${beginProjectMountEpoch()}`
66
+ : fsImportPath(projectRoot, rel)
67
+ );
68
+ return { entry: fs, module: fs };
69
+ }
70
+ return {
71
+ entry: (rel) => servedEntryLoader(servedBase, rel),
72
+ module: (rel) => servedModuleLoader(servedBase, rel),
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Build the {@link IngestGame} descriptor an `ingest-three` world resolves to,
78
+ * straight from its manifest fields — deliberately WITHOUT looking up any
79
+ * registry, because there is none: the in-tree fixture games are discovered by
80
+ * `./surface-three.ts`'s own `import.meta.glob`, and this function is the
81
+ * builder for the other half — an arbitrary manifest-backed project folder
82
+ * never registered in source (the CLI-on-folder route).
83
+ *
84
+ * The world's `entry` resolves to `IngestGame.load` through
85
+ * {@link ingestModuleLoaders}, so the game's modules are served by the dev
86
+ * server with `three` deduped to the editor's own instance — unless the folder
87
+ * is a served bundle, which Vite refuses to import at all.
88
+ */
89
+ export function resolveIngestDescriptor(
90
+ world: ResolvedAdapterRoot,
91
+ projectRoot: string,
92
+ ): IngestGame {
93
+ const { adapter } = world;
94
+ if (adapter.type !== 'ingest') {
95
+ throw new Error(`resolveIngestDescriptor: world "${world.id}" is not an { ingest } world.`);
96
+ }
97
+ if (!world.entry) {
98
+ throw new Error(
99
+ `resolveIngestDescriptor: world "${world.id}" has no \`entry\` — an ingest root mounts ` +
100
+ "the game's own entry module.",
101
+ );
102
+ }
103
+
104
+ const loaders = ingestModuleLoaders(projectRoot, { remountEpochs: true });
105
+ return {
106
+ id: world.id,
107
+ name: world.id,
108
+ description: world.description ?? '',
109
+ // The lane decision's scope (see `IngestGame.sourceModules`): a vendored
110
+ // game's TSX lives OUTSIDE the project folder, named only by these paths.
111
+ sourceModules: declaredSourceModules(world.entry, world.world?.entry),
112
+ ...ingestDataWriter(adapter.dataWriter, loaders.module),
113
+ ...(adapter.assets !== undefined ? { assets: adapter.assets } : {}),
114
+ ...(adapter.domStubs !== undefined ? { domStubs: adapter.domStubs } : {}),
115
+ ...(adapter.captureTimeoutMs !== undefined
116
+ ? { captureTimeoutMs: adapter.captureTimeoutMs }
117
+ : {}),
118
+ load: composeIngestLoad(
119
+ loaders.entry(world.entry),
120
+ adapter.contractShim === undefined ? undefined : loaders.module(adapter.contractShim),
121
+ ),
122
+ };
123
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * A vendored game committed as a SERVED BUNDLE, opened locally as a project.
3
+ *
4
+ * `public/ingest/<id>/` holds games vendored as build output rather than as
5
+ * source (CLAUDE.md, "Repo layout"). Those folders carry their own
6
+ * `vgai.project.json`, so `vgai edit public/ingest/<id>` opens them like any
7
+ * other project — and the manifest ingest route then built the entry's import
8
+ * url with `fsImportPath`, i.e. `/@fs/<abs>/…`, putting a file that lives in
9
+ * VITE'S OWN `public/` DIRECTORY into Vite's module graph.
10
+ *
11
+ * That is the one thing Vite refuses outright. Measured on this route
12
+ * (2026-08-20, `vgai edit public/ingest/tanks`, identically for `simcity`):
13
+ *
14
+ * [vite] Internal Server Error
15
+ * Cannot import non-asset file /ingest/tanks/three-r170.module.js which is
16
+ * inside /public. JS/CSS files inside /public are copied as-is on build and
17
+ * can only be referenced via <script src> or <link href> in html.
18
+ *
19
+ * → 500 → `Failed to fetch dynamically imported module: …/@fs/…/tanks-entry.js`
20
+ * → mount failed, no world, nothing on screen.
21
+ *
22
+ * `discovery-public-ingest.ts`'s header already states the rule this route
23
+ * broke: `public/` is deliberately OUTSIDE the module graph in both builds,
24
+ * "and importing from it is explicitly unsupported" — which is exactly why the
25
+ * hosted lane fetches these bundles as ordinary static files and bundles them
26
+ * client-side (`browser-transpile.ts`'s `importServedModule`). A bundle does
27
+ * not stop being served because the door that opened it was a session rather
28
+ * than a `?project=` id, so the LOCAL route resolves the same way: by URL.
29
+ *
30
+ * The `__VGAI_ENGINE_ROOT__` read is the exact inverse of
31
+ * `imported-gallery.ts`'s `importedProjectPath` (gallery card → absolute folder);
32
+ * this is absolute folder → served URL.
33
+ */
34
+
35
+ import { importServedModule } from '../host/browser-transpile';
36
+ import { ensureServedBundleRuntimeModules } from '../host/served-bundle-runtime-modules';
37
+ import { bootDeclaredDocument, declaresClassicEntry, readDeclaredBoot } from './served-html-boot';
38
+
39
+ declare const __VGAI_ENGINE_ROOT__: string;
40
+
41
+ function withoutTrailingSlash(path: string): string {
42
+ return path.endsWith('/') ? path.slice(0, -1) : path;
43
+ }
44
+
45
+ /**
46
+ * The root-relative URL base a project folder is SERVED at, or `undefined`
47
+ * when this project is not served by the editor's own static root.
48
+ *
49
+ * `<engineRoot>/public/ingest/tanks` → `/ingest/tanks/`. Deliberately
50
+ * id-generic and not `ingest`-specific: what makes a folder served is that it
51
+ * sits under the editor app root's `public/`, which is a fact about the
52
+ * checkout, not about the game.
53
+ */
54
+ export function servedProjectBaseUrl(projectRoot: string): string | undefined {
55
+ if (typeof __VGAI_ENGINE_ROOT__ !== 'string' || !__VGAI_ENGINE_ROOT__) return undefined;
56
+ const publicRoot = `${withoutTrailingSlash(__VGAI_ENGINE_ROOT__)}/public/`;
57
+ const dir = `${withoutTrailingSlash(projectRoot)}/`;
58
+ if (!dir.startsWith(publicRoot) || dir === publicRoot) return undefined;
59
+ return `/${dir.slice(publicRoot.length)}`;
60
+ }
61
+
62
+ /**
63
+ * Import one module out of a served bundle, in the host realm — the ONE
64
+ * loader both the hosted lane (`discovery-public-ingest.ts`) and the local
65
+ * session route (`ingest/resolve-three.ts`'s `resolveIngestDescriptor`) use, so
66
+ * the two doors onto the same folder cannot diverge in how they run it.
67
+ */
68
+ export function servedModuleLoader(baseUrl: string, relPath: string): () => Promise<unknown> {
69
+ const url = servedUrl(baseUrl, relPath);
70
+ return async () => {
71
+ await ensureServedBundleRuntimeModules();
72
+ return importServedModule(url);
73
+ };
74
+ }
75
+
76
+ function servedUrl(baseUrl: string, relPath: string): string {
77
+ const rel = relPath.startsWith('/') ? relPath.slice(1) : relPath;
78
+ return `${baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`}${rel}`;
79
+ }
80
+
81
+ /**
82
+ * The loader for a served bundle's ENTRY specifically — the one module whose
83
+ * LANGUAGE MODE the bundle's own `index.html` gets to decide.
84
+ *
85
+ * Its siblings (`ingest.contractShim`, `ingest.dataWriter`) stay on
86
+ * {@link servedModuleLoader}: those are host-authored ES modules by contract,
87
+ * and nothing declares them anywhere else. Only the entry is the game's own
88
+ * build output, and only for the entry does "is this an ES module?" have an
89
+ * answer that is not the host's to assume — see `served-html-boot.ts`.
90
+ */
91
+ export function servedEntryLoader(baseUrl: string, relPath: string): () => Promise<unknown> {
92
+ const url = servedUrl(baseUrl, relPath);
93
+ return async () => {
94
+ await ensureServedBundleRuntimeModules();
95
+ const boot = await readDeclaredBoot(baseUrl);
96
+ if (!declaresClassicEntry(boot, url)) {
97
+ // A module entry's document still gets its say: its BODY is staged and
98
+ // its CLASSIC PRELUDE scripts run first (a page may load a CDN global
99
+ // like `THREE` classically and read it from the module —
100
+ // server-survival's declared boot). `bootDeclaredDocument` skips module
101
+ // scripts, so the entry itself is imported exactly once, below.
102
+ if (boot) await bootDeclaredDocument(boot, url);
103
+ return importServedModule(url);
104
+ }
105
+ // `boot` is non-null whenever `declaresClassicEntry` answered true.
106
+ await bootDeclaredDocument(boot as NonNullable<typeof boot>, url);
107
+ return {};
108
+ };
109
+ }