@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,292 @@
1
+ /**
2
+ * Booting a served bundle THE WAY ITS OWN DOCUMENT SAYS TO.
3
+ *
4
+ * Every other ingest entry is an ES module and the host imports it. A bundle
5
+ * built by a CLASSIC-SCRIPT toolchain is not one, and importing it as a module
6
+ * is not a degraded boot — it is a different language mode. Measured on
7
+ * `public/ingest/cuberun` (Create React App / webpack 4, the recorded build
8
+ * recipe in `scripts/cuberun/steps/01-build.mjs`):
9
+ *
10
+ * TypeError: Cannot read properties of undefined (reading 'webpackJsonpcubeworld')
11
+ *
12
+ * because the chunk opens `(this.webpackJsonpcubeworld = this.webpackJsonpcubeworld || [])`
13
+ * and top-level `this` is the window in a classic script and `undefined` in an
14
+ * ES module. Two more facts come with it: the entry is the LAST of three
15
+ * scripts (webpack runtime, vendor chunk, app chunk) that must run in order,
16
+ * and the app mounts into a `<div id="root">` its own document declares.
17
+ *
18
+ * ZERO INFERENCE about any of that. `index.html` beside the bundle is the
19
+ * game's OWN statement of its boot: which scripts, in what order, in which
20
+ * language mode, and what DOM exists before they run. This module reads that
21
+ * statement and does what it says; it never sniffs the source, and a bundle
22
+ * whose entry the document loads with `type="module"` (tanks, simcity, every
23
+ * three.js example here) takes the ordinary module path untouched.
24
+ *
25
+ * WHAT THIS DOES NOT DO, because the host already does it:
26
+ * - SIZE/CONTAIN the page. `mountIngestGame` makes `hostEl` the realm's
27
+ * `document.body` (`setGameSurface`), absolutely positioned and full-size,
28
+ * so the transplanted body lands in a real box.
29
+ * - STYLE it. The root's `styles` manifest field serves the bundle's own
30
+ * stylesheet rewritten into `@scope` (`server/scoped-game-css.ts`), which is
31
+ * what turns `#root,body,html{width:100%;height:100%}` into a sized root
32
+ * without the editor seeing one of those rules. A `<link rel=stylesheet>` in
33
+ * the document is NOT auto-loaded here: an unscoped page sheet restyles the
34
+ * editor, and the declaration is the mechanism that already exists for it.
35
+ * - SHADOW globals. The concatenated source is prefixed with
36
+ * `GAME_GLOBALS_PRELUDE`, whose own doc comment records that it is kept
37
+ * evaluable outside ES-module scope precisely so a non-module evaluation
38
+ * path like this one can use it.
39
+ */
40
+
41
+ import { loadGameCssScoper, SCOPED_STYLE_MARK } from '../host/game-realm-page';
42
+ import { GAME_GLOBALS_PRELUDE, gameRealmPage } from '../host/gated-globals';
43
+ import { ingestGameRealmWindow } from './game-contract-realm';
44
+
45
+ /** One `<script>` the document declares, in document order. */
46
+ export interface DeclaredScript {
47
+ /** Absolute served URL, or `null` for an inline script. */
48
+ readonly src: string | null;
49
+ /** Inline body; empty for a `src` script. */
50
+ readonly inline: string;
51
+ /** `true` when the document loads it with `type="module"`. */
52
+ readonly module: boolean;
53
+ }
54
+
55
+ /** What a served bundle's own `index.html` declares about its boot. */
56
+ export interface DeclaredBoot {
57
+ /** The document's own inline `<style>` texts, in order — installed SCOPED. */
58
+ readonly inlineCss: readonly string[];
59
+ readonly scripts: readonly DeclaredScript[];
60
+ /** The document's body markup with every `<script>` removed. */
61
+ readonly bodyHtml: string;
62
+ }
63
+
64
+ /**
65
+ * Parse a served bundle's `index.html`. `null` when the folder ships none —
66
+ * an ordinary case (a bundle built as a bare module has nothing to declare),
67
+ * and never an error.
68
+ */
69
+ export async function readDeclaredBoot(
70
+ baseUrl: string,
71
+ fetchImpl: typeof fetch = globalThis.fetch,
72
+ ): Promise<DeclaredBoot | null> {
73
+ let html: string;
74
+ try {
75
+ const response = await fetchImpl(`${baseUrl}index.html`);
76
+ if (!response.ok) return null;
77
+ html = await response.text();
78
+ } catch {
79
+ return null;
80
+ }
81
+ const parsed = new DOMParser().parseFromString(html, 'text/html');
82
+ // The dev server answers an unknown path with the EDITOR's index.html, which
83
+ // parses perfectly well and would then be read as this game's boot. A served
84
+ // bundle's own document is the one whose scripts resolve under its own base.
85
+ // The page's own inline <style> blocks are part of its statement too — a
86
+ // game may size its mount div there (phaser-catch-the-cat's
87
+ // `#catch-the-cat { width: 100% }`). They are collected for SCOPED
88
+ // installation (the same `@scope` transform declared sheets get), never
89
+ // injected raw: an unscoped page rule would restyle the editor.
90
+ const inlineCss: string[] = [];
91
+ for (const el of Array.from(parsed.querySelectorAll('style'))) {
92
+ const text = el.textContent ?? '';
93
+ if (text.trim().length > 0) inlineCss.push(text);
94
+ el.remove();
95
+ }
96
+ const scripts: DeclaredScript[] = [];
97
+ for (const el of Array.from(parsed.querySelectorAll('script'))) {
98
+ // A `<script>` with a non-JS `type` is DATA the game reads back out of
99
+ // the DOM (`x-shader/x-vertex` blocks fetched by id — three.js example
100
+ // convention), never code. It must SURVIVE into the staged DOM, not be
101
+ // queued for evaluation: concatenating GLSL as JS is a SyntaxError, and
102
+ // stripping it left `getElementById('vertexshader')` null (measured:
103
+ // three-points-waves' TypeError, 2026-08-27). Head-
104
+ // declared data tags are moved into the body so the transplant carries
105
+ // them.
106
+ const type = el.getAttribute('type');
107
+ const executable =
108
+ type === null || type === 'module' || /^(text|application)\/javascript$/i.test(type);
109
+ if (!executable) {
110
+ if (!parsed.body.contains(el)) parsed.body.prepend(el);
111
+ continue;
112
+ }
113
+ const rawSrc = el.getAttribute('src');
114
+ // A cross-origin script keeps its FULL href: flattening a CDN URL to its
115
+ // pathname re-rooted it onto this origin, where the SPA fallback answered
116
+ // with HTML (server-survival's three.min.js CDN dependency, 2026-08-27).
117
+ const resolved =
118
+ rawSrc === null ? null : new URL(rawSrc, `${globalThis.location.origin}${baseUrl}`);
119
+ scripts.push({
120
+ src:
121
+ resolved === null
122
+ ? null
123
+ : resolved.origin === globalThis.location.origin
124
+ ? resolved.pathname
125
+ : resolved.href,
126
+ inline: rawSrc === null ? (el.textContent ?? '') : '',
127
+ module: type === 'module',
128
+ });
129
+ el.remove();
130
+ }
131
+ if (!scripts.some((script) => script.src?.startsWith(baseUrl))) return null;
132
+ return { scripts, bodyHtml: parsed.body.innerHTML, inlineCss };
133
+ }
134
+
135
+ /**
136
+ * Does this document load `entryUrl` as a CLASSIC script?
137
+ *
138
+ * `false` for "as a module" AND for "does not mention it at all" — in both
139
+ * cases the ordinary module import is the right and unchanged behaviour.
140
+ */
141
+ export function declaresClassicEntry(boot: DeclaredBoot | null, entryUrl: string): boolean {
142
+ const declared = boot?.scripts.find((script) => script.src === entryUrl);
143
+ return declared !== undefined && !declared.module;
144
+ }
145
+
146
+ /**
147
+ * Run the bundle exactly as its document declares: its body DOM first, then
148
+ * every script in order, as ONE classic script with `this === window`.
149
+ *
150
+ * The scripts are concatenated into a single function body rather than
151
+ * evaluated one at a time because that is what classic scripts on a page
152
+ * actually get — a shared variable environment. A webpack build communicates
153
+ * only through `this.webpackJsonp<name>`, so either shape boots it; a bundle
154
+ * that splits a `var` across two scripts needs the shared one.
155
+ */
156
+ /**
157
+ * Transplant the declared document's BODY into the game realm — the DOM the
158
+ * game's own scripts assume exists before they run (mount divs, and the
159
+ * non-JS `<script>` data tags `readDeclaredBoot` preserves). Shared by the
160
+ * classic boot below and by {@link import('./served-bundle').servedEntryLoader}'s
161
+ * MODULE branch: a module-entry game reads its page's elements exactly as a
162
+ * classic one does, and the module path used to import with an empty realm —
163
+ * `getElementById(...)` answered null and the mount failed by TypeError
164
+ * (three-points-waves, 2026-08-27). Idempotent per realm: a
165
+ * previous transplant is replaced, never doubled.
166
+ */
167
+ export async function stageDeclaredDom(boot: DeclaredBoot): Promise<void> {
168
+ const realmDocument = ingestGameRealmWindow().document;
169
+ realmDocument.querySelector('[data-vgai-declared-dom]')?.remove();
170
+ if (boot.bodyHtml.trim().length === 0 && boot.inlineCss.length === 0) return;
171
+ const holder = realmDocument.createElement('div');
172
+ holder.setAttribute('data-vgai-declared-dom', '');
173
+ // `pointer-events:none` on the WRAPPER, restored per child: the holder is a
174
+ // host artifact, not part of the game's page, and it blanketed the game's
175
+ // canvas — a Babylon game whose whole UI is in-canvas (space-truckers' GUI
176
+ // 'press any key') never received a single click, which two human passes
177
+ // read as 'no input works' (2026-08-28). The game's own DOM children stay
178
+ // fully interactive; only the wrapper's empty area falls through.
179
+ holder.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;pointer-events:none';
180
+ holder.innerHTML = boot.bodyHtml;
181
+ for (const child of Array.from(holder.children)) {
182
+ if (child instanceof HTMLElement && child.style.pointerEvents === '') {
183
+ child.style.pointerEvents = 'auto';
184
+ }
185
+ }
186
+ if (boot.inlineCss.length > 0) {
187
+ // Scoped through the SAME transform declared sheets get — CSS applies
188
+ // document-wide wherever the element sits, so containment comes from
189
+ // `@scope`, and lifecycle comes from living inside the holder.
190
+ try {
191
+ const scopeCss = await loadGameCssScoper();
192
+ const style = realmDocument.createElement('style');
193
+ const scoped = boot.inlineCss.map(scopeCss).join('\n');
194
+ // Already scoped: the realm's injected-style observer must not scope it twice.
195
+ style.dataset[SCOPED_STYLE_MARK] = scoped;
196
+ style.textContent = scoped;
197
+ holder.prepend(style);
198
+ } catch {
199
+ // Unscopeable inline CSS stays uninstalled — never injected raw.
200
+ }
201
+ }
202
+ realmDocument.body.appendChild(holder);
203
+ // An id the page itself supplies wins over the host's stand-ins for it.
204
+ // Two stand-ins exist and both would shadow the real element in
205
+ // `getElementById` order: a host-made stub (appended before the page was
206
+ // staged) and the mount host itself, which carries `id="container"` for
207
+ // games that expect to mount into one (`ingest-root-adapter.ts`). The
208
+ // game's scripts have not run yet, so retiring them here loses nothing —
209
+ // and keeping them measured as css3d_periodictable's renderer landing on
210
+ // the HOST after the page's own menu, painting over its buttons.
211
+ const surface = realmDocument.body;
212
+ for (const staged of holder.querySelectorAll('[id]')) {
213
+ if (surface.id === staged.id) surface.removeAttribute('id');
214
+ for (const stub of surface.querySelectorAll(
215
+ `[data-vgai-dom-stub][id="${CSS.escape(staged.id)}"]`,
216
+ )) {
217
+ if (stub !== staged) stub.remove();
218
+ }
219
+ }
220
+ }
221
+
222
+ export async function bootDeclaredDocument(
223
+ boot: DeclaredBoot,
224
+ entryUrl: string,
225
+ fetchImpl: typeof fetch = globalThis.fetch,
226
+ ): Promise<void> {
227
+ await stageDeclaredDom(boot);
228
+ const sources: string[] = [];
229
+ for (const script of boot.scripts) {
230
+ // Module scripts belong to the module system, never to this classic
231
+ // concatenation — the entry itself is imported by `servedEntryLoader`,
232
+ // and evaluating module syntax as a classic script is a SyntaxError.
233
+ if (script.module) continue;
234
+ if (script.src === null) {
235
+ sources.push(script.inline);
236
+ continue;
237
+ }
238
+ let response: Response;
239
+ try {
240
+ response = await fetchImpl(script.src);
241
+ } catch (error) {
242
+ // A bare TypeError('Failed to fetch') names nothing; the script URL is
243
+ // the whole diagnosis (a cross-origin CDN without CORS headers cannot
244
+ // be fetch()ed at all — script TAGS don't need CORS, fetch does).
245
+ throw new Error(
246
+ `served bundle boot: could not fetch its own declared <script src="${script.src}"> — ` +
247
+ `${error instanceof Error ? error.message : String(error)}`,
248
+ );
249
+ }
250
+ if (!response.ok) {
251
+ throw new Error(
252
+ `served bundle boot: its own index.html declares <script src="${script.src}">, and that ` +
253
+ `URL answers ${response.status} ${response.statusText}.`,
254
+ );
255
+ }
256
+ sources.push(await response.text());
257
+ }
258
+ // `.call(window)` is the whole point: `window` here is the prelude's
259
+ // realm-shadowed binding, and a classic script's top-level `this` IS the
260
+ // window. An ES-module evaluation makes it `undefined`, which is the exact
261
+ // TypeError this path exists to stop.
262
+ // CLASSIC-PAGE GLOBAL SCOPING, scoped to what games actually author. On a
263
+ // real page, `window.THREE = ...` in one script and a bare `THREE` in the
264
+ // next are one binding. Here the write lands on the prelude's shadowed
265
+ // realm window, which a bare read never consults — measured twice
266
+ // (phaser-catch-the-cat's inline `new CatchTheCatGame(...)`,
267
+ // server-survival's vendor script reading `THREE`, 2026-08-27). A blanket
268
+ // `with (window)` is NOT the answer: it re-binds every bare identifier in
269
+ // minified code against ALL window properties and broke cuberun's webpack
270
+ // chunks outright. The scope object below answers `has` ONLY for keys that
271
+ // are not browser built-ins (pristine set, snapshotted before any script
272
+ // runs) — i.e. the globals the game itself authors — so built-ins and
273
+ // minified locals resolve exactly as before, while game-authored globals
274
+ // become one binding across scripts, page-fashion. Lookups consult the
275
+ // LIVE realm window each time, so script N sees what script N-1 wrote.
276
+ // The proxy TARGET is a plain empty object, never the window itself: `has`
277
+ // hiding a target's own non-configurable property is a proxy INVARIANT
278
+ // violation ("trap returned falsish for property 'window'"), so the traps
279
+ // delegate to the captured realm window instead of proxying it.
280
+ const scope =
281
+ 'var __vgaiPristine = new Set(Object.getOwnPropertyNames(globalThis));\n' +
282
+ '__vgaiPristine.add("__vgaiPristine"); __vgaiPristine.add("__vgaiRealmGlobals");\n' +
283
+ 'var __vgaiRealmGlobals = (function (w) {\n' +
284
+ ' return new Proxy({}, {\n' +
285
+ ' has: function (_, k) { return typeof k === "string" && !__vgaiPristine.has(k) && k in w; },\n' +
286
+ ' get: function (_, k) { return typeof k === "string" ? w[k] : undefined; },\n' +
287
+ ' set: function (_, k, v) { w[k] = v; return true; },\n' +
288
+ ' });\n' +
289
+ '})(window);\n';
290
+ const body = `${GAME_GLOBALS_PRELUDE}\n${scope}\nreturn (function(){ with (__vgaiRealmGlobals) {\n${sources.join('\n;\n')}\n} }).call(window);`;
291
+ gameRealmPage().withCurrentScript(entryUrl, () => new Function(body)());
292
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * `canvas`-surface MOUNT builder for in-tree ingest fixtures (was
3
+ * `ingest/discovery2d.ts`, whose name and glob both said "2d/pixi" about a
4
+ * surface that is a platform primitive: a canvas root may be Pixi, Phaser,
5
+ * Babylon, raw WebGL/WebGPU or a 2D context).
6
+ *
7
+ * Discovery moved to `registry.ts` (one glob, one directory, dispatched on the
8
+ * manifest's `surface`). What is left here is the genuinely per-surface part:
9
+ * turning a `canvas` entry into the `IngestGame2D` descriptor
10
+ * `ingest/mount-canvas-ingest-root.ts` mounts.
11
+ *
12
+ * ID CONTINUITY (§1.E/§1.G): each fixture's root id is UNCHANGED from its
13
+ * registry-era id (`bunnymark`/`flappy`/`tilemap-camera`, no `2d-` prefix).
14
+ */
15
+
16
+ import {
17
+ entryModules,
18
+ gameFileKey,
19
+ type IngestEntry,
20
+ ingestEntriesOnSurface,
21
+ } from '../host/ingest/registry';
22
+ import type { IngestGame2D } from '@volter/game-runtime/pixi/ingest';
23
+ import { composeIngestLoad } from './entry-load';
24
+
25
+ type BaseFields2D = Pick<IngestGame2D, 'id' | 'name' | 'description' | 'captureTimeoutMs'>;
26
+
27
+ function fixtureModuleLoader(folderId: string, relPath: string): () => Promise<unknown> {
28
+ const key = gameFileKey(folderId, relPath);
29
+ const load = entryModules[key];
30
+ if (!load) {
31
+ throw new Error(
32
+ `canvas ingest discovery: "${folderId}"'s module "${relPath}" did not match any game ` +
33
+ `module (looked for glob key "${key}").`,
34
+ );
35
+ }
36
+ return load;
37
+ }
38
+
39
+ /** Build the mountable `IngestGame2D` for one `canvas`-surface registry entry. */
40
+ export function buildIngestGame2D({ folderId, manifest, root }: IngestEntry): IngestGame2D {
41
+ const { adapter } = root;
42
+ if (!root.entry) {
43
+ throw new Error(
44
+ `canvas ingest discovery: "${folderId}" has no \`entry\` — an ingest root mounts the ` +
45
+ "game's own entry module.",
46
+ );
47
+ }
48
+
49
+ const base: BaseFields2D = {
50
+ id: root.id,
51
+ name: manifest.name,
52
+ description: root.description ?? '',
53
+ ...(adapter.captureTimeoutMs !== undefined
54
+ ? { captureTimeoutMs: adapter.captureTimeoutMs }
55
+ : {}),
56
+ };
57
+
58
+ return {
59
+ ...base,
60
+ load: composeIngestLoad(
61
+ fixtureModuleLoader(folderId, root.entry),
62
+ adapter.contractShim === undefined
63
+ ? undefined
64
+ : fixtureModuleLoader(folderId, adapter.contractShim),
65
+ ),
66
+ };
67
+ }
68
+
69
+ let _cache: IngestGame2D[] | null = null;
70
+
71
+ /** Every in-tree `canvas`-surface ingest fixture. */
72
+ export function discoverIngestGames2D(): IngestGame2D[] {
73
+ if (!_cache) {
74
+ _cache = ingestEntriesOnSurface('canvas')
75
+ .map(buildIngestGame2D)
76
+ .sort((a, b) => a.id.localeCompare(b.id));
77
+ }
78
+ return _cache;
79
+ }
80
+
81
+ /** Look up one discovered canvas ingest game by id. */
82
+ export function getIngestGame2D(id: string): IngestGame2D | undefined {
83
+ return discoverIngestGames2D().find((g) => g.id === id);
84
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * `dom`-surface MOUNT resolution for in-tree ingest fixtures (was
3
+ * `ingest/discovery-react.ts`, which named React in a seam that is about the
4
+ * DOM: a dom root may be React, Vue, Svelte or plain HTML).
5
+ *
6
+ * Discovery moved to `registry.ts` (one glob, one directory, dispatched on the
7
+ * manifest's `surface`). What is left here is the per-surface part, and for
8
+ * this surface it is small: a `dom` ingest root mounts through
9
+ * `resolveRootBinding` (`roots/react-root.ts`'s `resolveIngestReactAdapter`,
10
+ * N1) — the SAME `/@fs/`-dynamic-import + `createRoot` host stack `default-react`
11
+ * uses — rather than a capture-based bridge, so there is no descriptor with its
12
+ * own `load` thunk to build. The one thing the mount needs that discovery
13
+ * cannot give it is the fixture's ABSOLUTE on-disk folder, which is what
14
+ * {@link ingestGameDomProjectRoot} computes.
15
+ *
16
+ * `projectRoot` can't come from `import.meta.glob`/`import.meta.url` alone
17
+ * (those resolve to HTTP URLs in the browser, not filesystem paths) — it is
18
+ * built from `__VGAI_ENGINE_ROOT__` (a dev-server-only Vite `define`,
19
+ * `server/dev.ts`, mirroring the existing `__VGAI_PROJECT_PATH__` define) plus
20
+ * this folder's FIXED, known-at-build-time relative offset. `browser`
21
+ * editor-mode builds (no dev server) never define `__VGAI_ENGINE_ROOT__` —
22
+ * callers must check `getEditorMode() !== 'browser'` first (mirrors every other
23
+ * `/@fs/`-dependent ingest route's guard, e.g. `ingest/mount-ingest-root.ts`'s
24
+ * `tryManifestIngestRoute`).
25
+ */
26
+
27
+ import { type IngestEntry, ingestEntriesOnSurface } from '../host/ingest/registry';
28
+ import type { ResolvedGameManifest } from '@volter/editor-project/manifest/load';
29
+
30
+ declare const __VGAI_ENGINE_ROOT__: string;
31
+
32
+ /** A discovered `dom`-surface ingest fixture: its parsed manifest (exactly one
33
+ * ingest root) plus that root's id for convenience. */
34
+ export interface IngestGameDom {
35
+ readonly folderId: string;
36
+ readonly manifest: ResolvedGameManifest;
37
+ readonly worldId: string;
38
+ }
39
+
40
+ /** Validate a `dom` entry against the surface's v1 scope and project it into
41
+ * the shape the mount route consumes. */
42
+ export function buildIngestGameDom({ folderId, manifest, root }: IngestEntry): IngestGameDom {
43
+ if (!root.entry) {
44
+ throw new Error(
45
+ `dom ingest discovery: "${folderId}" has no \`entry\` — a dom ingest root's \`entry\` ` +
46
+ 'must point at the host-shim module in the same folder (D-N2).',
47
+ );
48
+ }
49
+ return { folderId, manifest, worldId: root.id };
50
+ }
51
+
52
+ let _cache: IngestGameDom[] | null = null;
53
+
54
+ /** Every in-tree `dom`-surface ingest fixture. */
55
+ export function discoverIngestGamesDom(): IngestGameDom[] {
56
+ if (!_cache) {
57
+ _cache = ingestEntriesOnSurface('dom')
58
+ .map(buildIngestGameDom)
59
+ .sort((a, b) => a.folderId.localeCompare(b.folderId));
60
+ }
61
+ return _cache;
62
+ }
63
+
64
+ /** Look up one discovered dom ingest fixture by its folder id (the same id
65
+ * resolution — the folder id IS the root id by convention, one root per
66
+ * manifest, but callers should key off `folderId` since that's what the URL
67
+ * param names). */
68
+ export function getIngestGameDom(folderId: string): IngestGameDom | undefined {
69
+ return discoverIngestGamesDom().find((g) => g.folderId === folderId);
70
+ }
71
+
72
+ /**
73
+ * The absolute on-disk `projectRoot` for a discovered dom ingest fixture's
74
+ * folder — what `resolveRootBinding`/`resolveAllRoots` need to resolve its
75
+ * `entry`. `undefined` when `__VGAI_ENGINE_ROOT__` isn't defined (browser-mode
76
+ * static build — no dev server, no `/@fs/` route to resolve through anyway).
77
+ */
78
+ export function ingestGameDomProjectRoot(folderId: string): string | undefined {
79
+ if (typeof __VGAI_ENGINE_ROOT__ !== 'string' || !__VGAI_ENGINE_ROOT__) return undefined;
80
+ const root = __VGAI_ENGINE_ROOT__.endsWith('/')
81
+ ? __VGAI_ENGINE_ROOT__.slice(0, -1)
82
+ : __VGAI_ENGINE_ROOT__;
83
+ return `${root}/packages/editor/src/ingest/games/${folderId}`;
84
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * `three`-surface MOUNT builder for in-tree ingest fixtures (was
3
+ * `ingest/discovery.ts`, which owned its own glob of `games/*`).
4
+ *
5
+ * Discovery is no longer here: `registry.ts` does the one glob over
6
+ * `./games/*\/vgai.project.json` and hands back parsed entries. This module's only
7
+ * job is turning a `three`-surface entry into the `IngestGame` descriptor
8
+ * `ingest/mount-ingest-root.ts` mounts — which is genuinely per-surface work, and is why the
9
+ * builders stayed split when discovery was unified.
10
+ *
11
+ * Module-loading choice (kept from the original): these are IN-TREE fixtures
12
+ * Vite already knows statically, so the descriptor is built from
13
+ * `import.meta.glob`s (`registry.ts` owns them) rather than through
14
+ * `resolveIngestDescriptor`'s `/@fs/` dynamic-import route, which exists for an
15
+ * EXTERNAL project folder:
16
+ * - entry modules (LAZY thunks) for the game's own entry (and its
17
+ * `contractShim`, when it declares one);
18
+ * - `?url` asset globs for the manifest's `assets` map.
19
+ */
20
+
21
+ import {
22
+ assetUrls,
23
+ entryModules,
24
+ gameFileKey,
25
+ type IngestEntry,
26
+ ingestEntriesOnSurface,
27
+ } from '../host/ingest/registry';
28
+ import { composeIngestLoad, declaredSourceModules, ingestDataWriter } from './entry-load';
29
+ import type { IngestGame } from './types';
30
+
31
+ function resolveAssetUrls(
32
+ folderId: string,
33
+ assets: Record<string, string>,
34
+ ): Record<string, string> {
35
+ const resolved: Record<string, string> = {};
36
+ for (const [pathSubstring, relPath] of Object.entries(assets)) {
37
+ const key = gameFileKey(folderId, relPath);
38
+ const url = assetUrls[key];
39
+ if (!url) {
40
+ throw new Error(
41
+ `ingest discovery: "${folderId}"'s asset "${relPath}" did not match any served asset ` +
42
+ `(looked for glob key "${key}" — available: ${Object.keys(assetUrls).join(', ')})`,
43
+ );
44
+ }
45
+ resolved[pathSubstring] = url;
46
+ }
47
+ return resolved;
48
+ }
49
+
50
+ type BaseFields = Pick<
51
+ IngestGame,
52
+ | 'id'
53
+ | 'name'
54
+ | 'description'
55
+ | 'assets'
56
+ | 'domStubs'
57
+ | 'captureTimeoutMs'
58
+ | 'gameVersion'
59
+ | 'sourceModules'
60
+ >;
61
+
62
+ /** The entry-module glob's thunk for one folder-relative module path. */
63
+ export function fixtureModuleLoader(folderId: string, relPath: string): () => Promise<unknown> {
64
+ const key = gameFileKey(folderId, relPath);
65
+ const load = entryModules[key];
66
+ if (!load) {
67
+ throw new Error(
68
+ `ingest discovery: "${folderId}"'s module "${relPath}" did not match any game ` +
69
+ `module (looked for glob key "${key}").`,
70
+ );
71
+ }
72
+ return load;
73
+ }
74
+
75
+ /** Build the mountable `IngestGame` for one `three`-surface registry entry. */
76
+ export function buildIngestGame({ folderId, manifest, root }: IngestEntry): IngestGame {
77
+ const { adapter } = root;
78
+ if (!root.entry) {
79
+ throw new Error(
80
+ `ingest discovery: "${folderId}" has no \`entry\` — an ingest root mounts the game's own ` +
81
+ 'entry module.',
82
+ );
83
+ }
84
+
85
+ const base: BaseFields = {
86
+ id: root.id,
87
+ name: manifest.name,
88
+ description: root.description ?? '',
89
+ // The manifest's own `version`, so a later mount can compare a saved
90
+ // overlay's `authoredAgainst.gameVersion` against what's currently
91
+ // running.
92
+ gameVersion: manifest.version,
93
+ sourceModules: declaredSourceModules(root.entry, root.world?.entry),
94
+ ...ingestDataWriter(adapter.dataWriter, (rel) => fixtureModuleLoader(folderId, rel)),
95
+ ...(adapter.assets !== undefined ? { assets: resolveAssetUrls(folderId, adapter.assets) } : {}),
96
+ ...(adapter.domStubs !== undefined ? { domStubs: adapter.domStubs } : {}),
97
+ ...(adapter.captureTimeoutMs !== undefined
98
+ ? { captureTimeoutMs: adapter.captureTimeoutMs }
99
+ : {}),
100
+ };
101
+
102
+ return {
103
+ ...base,
104
+ load: composeIngestLoad(
105
+ fixtureModuleLoader(folderId, root.entry),
106
+ adapter.contractShim === undefined
107
+ ? undefined
108
+ : fixtureModuleLoader(folderId, adapter.contractShim),
109
+ ),
110
+ };
111
+ }
112
+
113
+ let _cache: IngestGame[] | null = null;
114
+
115
+ /** Every in-tree `three`-surface ingest fixture. */
116
+ export function discoverIngestGames(): IngestGame[] {
117
+ if (!_cache) {
118
+ _cache = ingestEntriesOnSurface('three')
119
+ .map(buildIngestGame)
120
+ .sort((a, b) => a.id.localeCompare(b.id));
121
+ }
122
+ return _cache;
123
+ }
124
+
125
+ /** Look up one discovered three ingest game by its folder id. */
126
+ export function getIngestGame(id: string): IngestGame | undefined {
127
+ return discoverIngestGames().find((g) => g.id === id);
128
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * The `IngestGame` descriptor — the mountable shape `ingest/mount-three-ingest-root.ts`
3
+ * (`mountThreeIngestRoot`) and `authoring/ingest-root-adapter.ts`
4
+ * (`mountIngestGame`) consume, regardless of where it came from.
5
+ *
6
+ * It is the common contract every discovery frontend builds:
7
+ * `ingest/surface-three.ts`'s glob-based discovery of the in-tree fixture games
8
+ * under `ingest/games/<id>/vgai.project.json`, `binding-resolver.ts`'s
9
+ * `resolveIngestDescriptor` (an arbitrary project folder's manifest, mounted
10
+ * through the dev server), and `ingest/discovery-public-ingest.ts` (a game
11
+ * served out of the repo's public dir, by id).
12
+ *
13
+ * Every field is a mounting FACT. The host runs the game's own entry module in
14
+ * the editor's realm — there is no choice of route to declare.
15
+ */
16
+ export interface IngestGame {
17
+ /** Stable id (used by the launcher + conformance test; == the manifest world id). */
18
+ id: string;
19
+ /** Human label shown in the editor's game menu (== the manifest's game name). */
20
+ name: string;
21
+ /** One-line description (== the manifest world's optional `description`, or `''`). */
22
+ description: string;
23
+ /**
24
+ * Import + run the unmodified game module. When the manifest declares a
25
+ * `contractShim`, this thunk imports that module first — same realm, same
26
+ * tick order the shim's timing contract describes.
27
+ */
28
+ load: () => Promise<unknown>;
29
+ /**
30
+ * The game's own DATA writer, when it declares one (`ingest.dataWriter`) —
31
+ * the module that turns an authored move of an object the game placed from a
32
+ * level file into new bytes of that file. Carried as its specifier plus a
33
+ * loader, because the loader is the only thing that knows how this game's
34
+ * files are served (globbed fixture, `public/` url, `/@fs/` folder) and the
35
+ * specifier is what a refusal has to name. Absent ⇒ this game's authorable
36
+ * truth is source only, and a record-anchored object refuses by name.
37
+ */
38
+ dataWriter?: { specifier: string; load: () => Promise<unknown> };
39
+ /**
40
+ * The PROJECT-RELATIVE paths of the game's own source modules this root
41
+ * declares — its `entry` and, when it names one, `world.entry`.
42
+ *
43
+ * The lane decision reads them (`authoring/ingest-root-adapter.ts`,
44
+ * `ingestSourceRoots`): their parent directories are the scope in which a
45
+ * serve-time OID stamp counts as THIS root's. Carried as declared strings
46
+ * rather than resolved paths because a discovery frontend does not always
47
+ * know the project's absolute location; the mount does (`getCurrentProject`).
48
+ * Absent ⇒ only the project folder itself scopes the serve-time door.
49
+ */
50
+ sourceModules?: readonly string[];
51
+ /** How long to wait for the game's first captured frame before failing. */
52
+ captureTimeoutMs?: number;
53
+ /**
54
+ * Map of game-relative-path substring → served asset URL. The ingest adapter
55
+ * installs a `THREE.DefaultLoadingManager` URL modifier that rewrites any
56
+ * requested URL containing a key to its value.
57
+ */
58
+ assets?: Record<string, string>;
59
+ /**
60
+ * Element ids the game expects to already exist in the DOM (e.g. an FPS game's
61
+ * `#blocker`/`#instructions` overlay). The host creates hidden stub elements
62
+ * with these ids before running the game, so DOM-dependent games stay
63
+ * unmodified.
64
+ */
65
+ domStubs?: string[];
66
+ /**
67
+ * The game's own manifest `version` (D9 — orphan/mismatch detection), when
68
+ * this descriptor was built straight from a manifest that has one.
69
+ * `ingest/surface-three.ts`'s in-tree fixture route and
70
+ * `ingest/discovery-public-ingest.ts` set this from the parsed manifest's
71
+ * `version`; `ingest/resolve-three.ts`'s `resolveIngestDescriptor` (the
72
+ * CLI-on-folder / manifest-world route) cannot — it only has a
73
+ * `ResolvedAdapterRoot`, not the manifest's top-level `version`.
74
+ */
75
+ gameVersion?: string;
76
+ }