@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,122 @@
1
+ /**
2
+ * The one slot that says an ingest mount is WAITING for the game's first
3
+ * frame, and whether that wait is parked because the tab is hidden.
4
+ *
5
+ * WHY IT EXISTS. The capture window is a budget of VISIBLE time
6
+ * (`@volter/threejs-runtime/adapter/ingest/visible-capture-window`), so a tab that boots in the
7
+ * background — the normal human path — no longer burns its window unable to
8
+ * draw. What it does instead is WAIT, potentially for as long as the human
9
+ * takes to come back to the tab, and a wait is exactly the state every door
10
+ * used to report as silence: nothing mounted, nothing failed, `vgai status`
11
+ * counting down a clock that was not running. So the wait is published:
12
+ * `collectState`'s `ingestCaptureWait` carries it, and `vgai status` prints
13
+ * "waiting for first visible frame — tab is hidden" instead of a countdown.
14
+ *
15
+ * A LIVE VIEW, never a snapshot — same rule as `IngestMount.realmLoopVerdict`
16
+ * and the `__vgaiIngest` hook's `reflected`/`sceneChildren`: the elapsed
17
+ * numbers move on their own, so a frozen copy starts lying the instant it is
18
+ * taken. The slot holds the window itself and derives the report at read time.
19
+ *
20
+ * RESOURCE OWNERSHIP: the MOUNT owns this slot. `mountIngestGame`'s
21
+ * `waitForCapture` sets it when the wait begins and clears it (with `null`) the
22
+ * moment the wait ends — captured, expired, or torn down — through the same
23
+ * `onWait` callback, so there is exactly one writer and no second teardown
24
+ * path. A second mount replaces the first's entry; there is only ever one wait.
25
+ */
26
+
27
+ import { notifyLiveSessionsChanged } from '@volter/editor-core/live-session-registry';
28
+ import type { VisibleCaptureWindow } from '@volter/threejs-runtime/adapter/ingest/visible-capture-window';
29
+
30
+ /** The wait as `vgai status` reports it. */
31
+ export interface CaptureWaitStatus {
32
+ /** The world whose first frame is being waited for. */
33
+ readonly worldId: string;
34
+ /** The capture window, in VISIBLE milliseconds. */
35
+ readonly budgetMs: number;
36
+ /**
37
+ * Visible milliseconds spent so far — the only ones the window counts.
38
+ *
39
+ * There is deliberately no hidden-time counterpart here. The server holds
40
+ * the last snapshot a tab POSTed, and a hidden tab posts nothing while it
41
+ * waits, so a hidden-ms field would be frozen at whatever it was when the
42
+ * wait began and read as "0ms hidden" after an hour of it. This one cannot
43
+ * go stale in the state that matters: while the tab is hidden it is not
44
+ * moving. (Hidden time IS measured live in the page, where it is honest —
45
+ * the capture-timeout error quotes it.)
46
+ */
47
+ readonly elapsedVisibleMs: number;
48
+ /** Whether the wait is parked RIGHT NOW because the tab is hidden. */
49
+ readonly hidden: boolean;
50
+ /** Whether the browser is currently presenting frames. Wider than hidden:
51
+ * WebKit also suspends an unfocused window while Page Visibility says visible. */
52
+ readonly suspended: boolean;
53
+ /** The observed browser condition parking the capture budget. */
54
+ readonly suspensionReason: 'hidden' | 'unfocused' | 'page-suspended' | null;
55
+ /** The wait in one sentence, for a reader who sees only this line. */
56
+ readonly reason: string;
57
+ }
58
+
59
+ interface ActiveWait {
60
+ readonly worldId: string;
61
+ readonly window: VisibleCaptureWindow;
62
+ }
63
+
64
+ let _wait: ActiveWait | null = null;
65
+ const _listeners = new Set<() => void>();
66
+
67
+ function notify(): void {
68
+ for (const listener of _listeners) listener();
69
+ }
70
+
71
+ /**
72
+ * Subscribe to wait start/end. Visibility transitions do NOT come through
73
+ * here — `command-listener.ts` already re-POSTs state on `visibilitychange`
74
+ * (its `reportPresence`), which is the same event this wait parks and resumes
75
+ * on, so a second listener for it would be a duplicate reporter of one event.
76
+ */
77
+ export function subscribeToCaptureWait(callback: () => void): () => void {
78
+ _listeners.add(callback);
79
+ return () => {
80
+ _listeners.delete(callback);
81
+ };
82
+ }
83
+
84
+ /** Publish the running wait, or `null` when it ends. The mount's `onWait`. */
85
+ export function setCaptureWait(worldId: string, window: VisibleCaptureWindow | null): void {
86
+ _wait = window ? { worldId, window } : null;
87
+ notify();
88
+ // The state report's external-change channel (`command-listener.ts`
89
+ // re-reports on the registry's version).
90
+ notifyLiveSessionsChanged();
91
+ }
92
+
93
+ /** The live wait, derived at read time, or `null` when nothing is waiting. */
94
+ export function captureWaitStatus(): CaptureWaitStatus | null {
95
+ const active = _wait;
96
+ if (!active) return null;
97
+ const hidden = active.window.isHidden();
98
+ const suspended = active.window.isSuspended();
99
+ const suspensionReason = active.window.suspensionReason();
100
+ return {
101
+ worldId: active.worldId,
102
+ budgetMs: active.window.budgetMs,
103
+ elapsedVisibleMs: Math.round(active.window.elapsedVisibleMs()),
104
+ hidden,
105
+ suspended,
106
+ suspensionReason,
107
+ reason:
108
+ suspensionReason === 'hidden'
109
+ ? 'waiting for first visible frame — tab is hidden (a hidden tab cannot render, so the ' +
110
+ 'capture window is parked, not counting down; bring the tab to the foreground)'
111
+ : suspensionReason === 'unfocused'
112
+ ? 'waiting for the browser to resume frame presentation — the editor window is ' +
113
+ 'unfocused (WebKit can suspend requestAnimationFrame in this state, so the capture ' +
114
+ 'window is parked; focus the editor window)'
115
+ : suspensionReason === 'page-suspended'
116
+ ? 'waiting for the browser to restore the suspended editor page — the capture ' +
117
+ 'window is parked, not counting down'
118
+ : suspended
119
+ ? 'waiting for the browser to resume frame presentation — the capture window is parked'
120
+ : "waiting for the game's first rendered frame",
121
+ };
122
+ }
@@ -0,0 +1,216 @@
1
+ /**
2
+ * The play-time half of an ingest root that declares a design-time world
3
+ * (the root's `world` — see the manifest schema's own description).
4
+ *
5
+ * WHAT THIS EXISTS FOR. An ingest root used to mount at BOOT and stay mounted:
6
+ * the running game WAS the edit surface, so the Scene view showed the game's
7
+ * mount surfaces (a `Canvas` root with a nested `DOM UI` document) as its
8
+ * hierarchy roots, and whatever the game animated animated there. A scene is a
9
+ * single root by definition (owner, 2026-08-15) and Edit ≠ Play
10
+ * (ARCHITECTURE-CORE: LOADING CONSTRUCTS, PLAY RUNS), so for a root that names
11
+ * its own world component the two mounts split:
12
+ *
13
+ * EDIT — isolation documents (or `r3f-design-session.ts` for a named world
14
+ * component) mount each piece ALONE. Nothing ticks.
15
+ * PLAY — this module mounts the WHOLE GAME through the ordinary ingest
16
+ * routes (`mount-ingest-root.ts`), byte-for-byte the mount that used
17
+ * to happen at boot: its own entry, its own contract shim, its own
18
+ * composition, HUD, physics and loop. Stop tears it down and asks for
19
+ * one edit-mode rebuild, which recreates the design session.
20
+ *
21
+ * A root that declares neither a world component nor isolation-document
22
+ * scene tabs is untouched by all of this: it mounts at boot exactly as
23
+ * before, and {@link ingestMountIsDeferredToPlay} answers `false` for it.
24
+ *
25
+ * RESOURCE OWNERSHIP. The ingest SESSION is owned by `active-ingest.ts`, as it
26
+ * always was, and the one teardown path allowed to end it stays
27
+ * `exitActiveIngest()`. This module owns nothing but the decision of WHEN the
28
+ * existing mount runs; `_claimedPlay` below is a latch over that decision, so
29
+ * Stop only tears down a session THIS module started (an ordinary boot-time
30
+ * ingest keeps its own lifecycle).
31
+ */
32
+
33
+ import { commandLine } from '@volter/editor-core/product-command';
34
+ import { queueEditModeRebuild } from '@volter/editor-core/authoring/edit-mode-authoring';
35
+ import type { EditorShellStore } from '@volter/editor-core/editor-shell-store';
36
+ import { fetchGameManifest } from '@volter/editor-core/manifest-project';
37
+ import {
38
+ type ProjectAdapterFacet,
39
+ projectAdapterFacet,
40
+ subscribeProjectAdapter,
41
+ } from '@volter/editor-core/project-adapter';
42
+ import { sceneTabRow } from '@volter/editor-core/scene-document-plan';
43
+ import { ingestRoots } from '@volter/editor-project/adapter/manifest-interpreter';
44
+ import { activeIngest } from './active-ingest';
45
+ import {
46
+ beginDeferredIngestPlaySession,
47
+ deferredIngestPlayActive,
48
+ endDeferredIngestPlaySession,
49
+ } from './deferred-ingest-session';
50
+ import { getIngestPlayControl } from './ingest-play-control';
51
+ import { exitActiveIngest } from './unmount-ingest-root';
52
+
53
+ export { deferredIngestPlayActive };
54
+
55
+ /**
56
+ * Does Edit already own pieces for this ingest root, so the live game must
57
+ * wait for Play?
58
+ *
59
+ * Two declarations, same answer:
60
+ * - the root's `world` — a named design-time world component;
61
+ * - isolation-document rows on the adapter scene table — per-piece Edit
62
+ * tabs (bubbo Title / Game / Result).
63
+ *
64
+ * Either one means Edit mounts constructs, not the running game
65
+ * (ARCHITECTURE-CORE: LOADING CONSTRUCTS, PLAY RUNS). A self-booting
66
+ * ingest with neither still mounts at boot — that is the degenerate
67
+ * "smallest piece is the whole game" case.
68
+ *
69
+ */
70
+ export async function ingestMountIsDeferredToPlay(): Promise<boolean> {
71
+ const manifest = await fetchGameManifest().catch(() => null);
72
+ if (!manifest) return false;
73
+ const declared = ingestRoots(manifest);
74
+ // Exactly one: a manifest declaring two ingest roots is already a named
75
+ // failure in `mount-ingest-root.ts`, and that error must keep being the one
76
+ // the user sees rather than being pre-empted by a silent deferral here.
77
+ if (declared.length !== 1) return false;
78
+ if (declared[0]!.world !== undefined) return true;
79
+ const facet = await adapterFacetWhenReady();
80
+ return isolationDocumentsDeclared(facet);
81
+ }
82
+
83
+ /** How long boot waits for the adapter facet before deciding it has none. */
84
+ const FACET_WAIT_MS = 4000;
85
+
86
+ /**
87
+ * The scene table is published asynchronously (`startProjectAdapterLoad`).
88
+ * Boot must wait for that publish — a miss here is how isolation tabs exist
89
+ * AND the live game still mounts underneath them.
90
+ */
91
+ function adapterFacetWhenReady(): Promise<ProjectAdapterFacet | null> {
92
+ const existing = projectAdapterFacet();
93
+ if (existing) return Promise.resolve(existing);
94
+ return new Promise((resolve) => {
95
+ const timer = setTimeout(() => {
96
+ unsub();
97
+ resolve(projectAdapterFacet());
98
+ }, FACET_WAIT_MS);
99
+ const unsub = subscribeProjectAdapter(() => {
100
+ const facet = projectAdapterFacet();
101
+ if (!facet) return;
102
+ clearTimeout(timer);
103
+ unsub();
104
+ resolve(facet);
105
+ });
106
+ });
107
+ }
108
+
109
+ function isolationDocumentsDeclared(facet: ProjectAdapterFacet | null): boolean {
110
+ if (!facet) return false;
111
+ return sceneTabRow(facet.scenes).some((row) => row.plan.kind === 'isolation-document');
112
+ }
113
+
114
+ /**
115
+ * Does the live ingest session belong to a PLAY RUN this module started?
116
+ *
117
+ * Two surfaces ask, and both ask it about the same thing — whether ⏹ means
118
+ * anything here. An ingest that mounted at boot IS the editor's open document,
119
+ * so "stop" can only honestly mean "pause its loop" (`ingest-play-commands.ts`
120
+ * says exactly that, and the PlayBar's ingest toolbar offers no Stop). A
121
+ * deferred run inverts that premise: Play constructed it, so Stop ends it and
122
+ * hands the surface back to the design-time Scene.
123
+ */
124
+ /**
125
+ * Play's mount for a deferred ingest root: run the ordinary ingest routes,
126
+ * then start the game through its own play control (the same ▶ the PlayBar
127
+ * drives for a boot-mounted ingest — contract-first `lifecycle.start`, else
128
+ * the wire event, plus the loop gate).
129
+ *
130
+ * Returns `false` when this project has no deferred ingest root, which is the
131
+ * signal for `play-mode.ts` to continue into its ordinary first-party boot.
132
+ */
133
+ export async function mountDeferredIngestForPlay(store: EditorShellStore): Promise<boolean> {
134
+ if (!(await ingestMountIsDeferredToPlay())) return false;
135
+ // Edit isolation and the live game share the browser's Pixi module realm.
136
+ // Isolation may therefore have initialized the singleton Assets manager,
137
+ // while the untouched game quite correctly initializes it at boot. Reset at
138
+ // the explicit realm-ownership boundary: live Play now owns the manager and
139
+ // loads its own manifest without a duplicate-init warning. Existing Edit
140
+ // display objects retain their texture objects while hidden; Stop rebuilds
141
+ // the Edit presentation through the ordinary path below.
142
+ //
143
+ // ON THIS SURFACE'S OWN NAMESPACE (`../vite-plugin-module-doorways.ts`): the
144
+ // manager being reset is a MODULE-GLOBAL of one `pixi.js` instance, so a
145
+ // reset on the wrong instance resets nothing at all — the same shape as
146
+ // `owned-pixi-ticker-listeners.ts`'s `Ticker.shared`. Under the packaged
147
+ // runtime both halves of the sentence above live in the PROJECT's graph:
148
+ // `mount-isolated-pixi-screen.ts` initializes assets through the game's own
149
+ // module (`authoring/pixi-isolation-assets.ts` calls the game's
150
+ // `initialize`), and the game's boot does too. A static `pixi.js` here is
151
+ // the shell's third instance, which neither of them ever touched.
152
+ const manifest = await fetchGameManifest().catch(() => null);
153
+ const deferredCanvasRoot = manifest
154
+ ? ingestRoots(manifest).some((root) => root.adapter.surface === 'canvas')
155
+ : false;
156
+ // …but ONLY when no full game boot owns the realm yet. When a held ingest
157
+ // session is live (a creation-site write materializes one — the write needs
158
+ // the live object), the shared singletons hold THE GAME'S OWN warm state,
159
+ // and this reset tears exactly half of it: the Assets cache empties while
160
+ // sibling registries the same boot populated (`@pixi/sound`'s alias table)
161
+ // stay full. The next boot then re-loads every bundle into duplicate-alias
162
+ // asserts, the load breaks partway, and the game never renders a frame —
163
+ // measured on bubbo-bubbo (2026-08-21): every ▶ after an authoring write
164
+ // died in the 30s capture timeout this way. A warm second boot instead
165
+ // finds its bundles cached (Assets.init warns duplicate-init and keeps the
166
+ // manifest, which is the same manifest) and renders. The reset stays for
167
+ // the case it was built for: isolation initialized the manager and no game
168
+ // boot ever ran, where the game's first boot deserves a virgin manager.
169
+ if (deferredCanvasRoot && !activeIngest()) {
170
+ const { resolveCanvasPixiForEditor } = await import('../host/canvas-entry-runtime');
171
+ const pixi = await resolveCanvasPixiForEditor();
172
+ pixi.Assets.reset();
173
+ }
174
+ const { launchIngestRoutes } = await import('./mount-ingest-root');
175
+ await launchIngestRoutes(store);
176
+ if (!activeIngest()) {
177
+ // The routes already reported the failure through the mount-failure
178
+ // report and reset the play surface; say why play produced nothing rather
179
+ // than leaving the caller to infer it from a quiet return.
180
+ throw new Error(
181
+ "Play could not start: this project's ingest root failed to mount — see the mount " +
182
+ `failure reported above (and in ${commandLine('status')}).`,
183
+ );
184
+ }
185
+ beginDeferredIngestPlaySession();
186
+ // The GAME document is where a running runtime presents (ARCHITECTURE-CORE:
187
+ // Game is a PLAY-TIME document). The three ingest mount leaves the editor on
188
+ // the Scene tab because, until now, that mount WAS edit mode; a play-time
189
+ // mount belongs on the play surface — which is also what opens the input
190
+ // gate (`mount-three-ingest-root.ts`'s `ingestInputActive` requires the play
191
+ // tab), so the game is drivable rather than merely visible.
192
+ store.setActiveViewportTab('play');
193
+ getIngestPlayControl()?.play();
194
+ return true;
195
+ }
196
+
197
+ /**
198
+ * Stop's half. Returns `true` when it tore a play-claimed ingest down, so
199
+ * `exitPlayMode` can skip a first-party teardown that has nothing to tear.
200
+ * Idempotent; a no-op for a boot-mounted ingest.
201
+ */
202
+ export function exitDeferredIngestPlay(store: EditorShellStore): boolean {
203
+ if (!deferredIngestPlayActive()) return false;
204
+ endDeferredIngestPlaySession();
205
+ // `exitActiveIngest` is the ONE teardown path for the session (this module's
206
+ // ownership note) — it unmounts by kind, clears the authoring override and
207
+ // the adopted scene, and resets the play surface.
208
+ exitActiveIngest();
209
+ store.setPlayState('stopped');
210
+ store.setActiveViewportTab('edit');
211
+ // Edit mode is rebuilt through the ONE authoritative reinstall path
212
+ // (`the world root's stage`'s `installAll`), which is what recreates the R3F design
213
+ // session over the world component.
214
+ queueEditModeRebuild();
215
+ return true;
216
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * THE SESSION FACT for a deferred ingest Play run.
3
+ *
4
+ * Kept in a dependency-free leaf because both the deferred mount orchestrator
5
+ * and the editor-wide mode predicate must read it. Putting the latch in either
6
+ * owner creates a cycle through `ingest-play-control` or `play-mode` and, more
7
+ * importantly, lets one consumer silently forget this is a Play SESSION even
8
+ * though it is not a first-party `play-mode.ts` session.
9
+ */
10
+
11
+ let active = false;
12
+
13
+ export function deferredIngestPlayActive(): boolean {
14
+ return active;
15
+ }
16
+
17
+ export function beginDeferredIngestPlaySession(): void {
18
+ active = true;
19
+ }
20
+
21
+ export function endDeferredIngestPlaySession(): void {
22
+ active = false;
23
+ }
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Vendored ingest discovery by id.
3
+ *
4
+ * The three sibling of `surface-canvas.ts` (the pixi lane, which is the idiom
5
+ * benchmark this file is modelled on): a game vendored under
6
+ * `public/ingest/<id>/` carries its own declarative `vgai.project.json`, and
7
+ * its id resolves it into the SAME `IngestGame` descriptor every other three
8
+ * ingest route produces. Adding a fourth vendored game is a folder plus a
9
+ * manifest — no edit to this file, no per-game page, no branch anywhere.
10
+ *
11
+ * ## Why `fetch`, where the in-tree lane uses `import.meta.glob`
12
+ *
13
+ * `registry.ts` globs IN-TREE fixture manifests under
14
+ * `packages/editor/src/ingest/games/`, which Vite knows statically (
15
+ * ONE manifest-driven registry, with only the per-surface MOUNT still split —
16
+ * `surface-three.ts`/`surface-canvas.ts`/`surface-dom.ts`).
17
+ * These manifests live in `public/`, which is deliberately OUTSIDE the module
18
+ * graph in both builds: Vite serves it verbatim in dev and copies it into
19
+ * `dist/` at build, and importing from it is explicitly unsupported (that
20
+ * verbatim serving is exactly why the vendored bundles can keep their absolute
21
+ * `import '/ingest/<id>/three-rNNN.module.js'` specifiers un-rewritten). A
22
+ * root-relative `fetch` is therefore the id-generic route that works
23
+ * identically on the dev server and on the deployed hosted editor — no
24
+ * dev-server middleware, nothing to build.
25
+ *
26
+ * There is deliberately no enumeration here: the caller supplies the id,
27
+ * and a 404 means "not a vendored three ingest", cleanly. A gallery that needs
28
+ * the LIST wants a build-time aggregate (the
29
+ * `project-scripts-by-example.generated.ts` pattern), not a runtime directory scan.
30
+ */
31
+
32
+ import {
33
+ isSafeIngestId,
34
+ PUBLIC_INGEST_BASE,
35
+ publicIngestManifestUrl,
36
+ } from '../host/staged-projects';
37
+ import { extractUpstreamPin } from '@volter/game-runtime/adapter/ingest/upstream-pin';
38
+ import type { IngestGame2D } from '@volter/game-runtime/pixi/ingest';
39
+ import { declaredRoots } from '@volter/editor-project/adapter/manifest-interpreter';
40
+ import { loadGameManifest } from '@volter/editor-project/manifest/load';
41
+ import { composeIngestLoad, ingestDataWriter } from './entry-load';
42
+ import { servedEntryLoader, servedModuleLoader } from './served-bundle';
43
+ import type { IngestGame } from './types';
44
+
45
+ /** What a vendored three ingest manifest resolves to. */
46
+ export interface PublicIngestResolution {
47
+ game: IngestGame;
48
+ }
49
+
50
+ /** What a vendored canvas ingest manifest resolves to. */
51
+ export interface PublicCanvasIngestResolution {
52
+ game: IngestGame2D;
53
+ }
54
+
55
+ /** Import a module served out of a vendored game's folder, in the host realm.
56
+ * The loader itself is `served-bundle.ts`'s — shared with the LOCAL session
57
+ * route so both doors onto one folder run it identically. */
58
+ function publicIngestModuleLoader(folderId: string, relPath: string): () => Promise<unknown> {
59
+ return servedModuleLoader(`${PUBLIC_INGEST_BASE}${folderId}/`, relPath);
60
+ }
61
+
62
+ /** The game's own entry module, preceded by its `contractShim` when it declares one. */
63
+ function publicIngestLoad(
64
+ folderId: string,
65
+ entry: string,
66
+ contractShim: string | undefined,
67
+ ): () => Promise<unknown> {
68
+ return composeIngestLoad(
69
+ servedEntryLoader(`${PUBLIC_INGEST_BASE}${folderId}/`, entry),
70
+ contractShim === undefined ? undefined : publicIngestModuleLoader(folderId, contractShim),
71
+ );
72
+ }
73
+
74
+ /** The single ingest root a vendored game's manifest must declare. */
75
+ function readVendoredIngestRoot(
76
+ folderId: string,
77
+ raw: unknown,
78
+ expect: { surface: 'three' | 'canvas'; identity: 'ingest-three' | 'ingest-pixi' },
79
+ ) {
80
+ const manifest = loadGameManifest(raw, { configurationKinds: 'defer' });
81
+ const roots = declaredRoots(manifest);
82
+ if (roots.length !== 1) {
83
+ throw new Error(
84
+ `vendored ingest discovery: "${folderId}/vgai.project.json" must declare exactly one root ` +
85
+ `(found ${roots.length}).`,
86
+ );
87
+ }
88
+ const root = roots[0]!;
89
+ if (
90
+ root.surface !== expect.surface ||
91
+ root.adapter.type !== 'ingest' ||
92
+ root.adapter.identity !== expect.identity
93
+ ) {
94
+ throw new Error(
95
+ `vendored ingest discovery: "${folderId}/vgai.project.json" must declare one ` +
96
+ `${expect.surface} { ingest } root (found surface "${root.surface}", adapter identity ` +
97
+ `"${root.adapter.identity}").`,
98
+ );
99
+ }
100
+ if (!root.entry) {
101
+ throw new Error(
102
+ `vendored ingest discovery: "${folderId}" has no \`entry\` — an ingest root mounts the ` +
103
+ "game's own entry module.",
104
+ );
105
+ }
106
+ return { manifest, root, adapter: root.adapter, entry: root.entry };
107
+ }
108
+
109
+ /**
110
+ * Build the descriptor from a folder id and its parsed manifest JSON. Pure —
111
+ * the unit-testable half, with every rejection a NAMED error rather than a
112
+ * silent skip (the anti-shim rule: an unmountable manifest must say why).
113
+ */
114
+ export function buildPublicIngestGame(folderId: string, raw: unknown): PublicIngestResolution {
115
+ const { manifest, root, adapter, entry } = readVendoredIngestRoot(folderId, raw, {
116
+ surface: 'three',
117
+ identity: 'ingest-three',
118
+ });
119
+ const game: IngestGame = {
120
+ id: root.id,
121
+ name: manifest.name,
122
+ description: root.description ?? '',
123
+ gameVersion: manifest.version,
124
+ load: publicIngestLoad(folderId, entry, adapter.contractShim),
125
+ ...ingestDataWriter(adapter.dataWriter, (rel) => publicIngestModuleLoader(folderId, rel)),
126
+ ...(adapter.assets !== undefined ? { assets: adapter.assets } : {}),
127
+ ...(adapter.captureTimeoutMs !== undefined
128
+ ? { captureTimeoutMs: adapter.captureTimeoutMs }
129
+ : {}),
130
+ ...(adapter.domStubs !== undefined ? { domStubs: adapter.domStubs } : {}),
131
+ };
132
+ return { game };
133
+ }
134
+
135
+ /**
136
+ * Canvas-surface sibling of {@link buildPublicIngestGame}. `ingest-pixi` is
137
+ * the existing schema identity for canvas ingest roots; nothing here infers
138
+ * Pixi from it. The mount recognizes the actual library in the live realm
139
+ * (`mount-canvas-ingest-root.ts` -> `authoring/canvas-runtime-recognition.ts`)
140
+ * and fails loudly when no structural adapter exists.
141
+ */
142
+ export function buildPublicCanvasIngestGame(
143
+ folderId: string,
144
+ raw: unknown,
145
+ ): PublicCanvasIngestResolution {
146
+ const { manifest, root, adapter, entry } = readVendoredIngestRoot(folderId, raw, {
147
+ surface: 'canvas',
148
+ identity: 'ingest-pixi',
149
+ });
150
+ return {
151
+ game: {
152
+ id: root.id,
153
+ name: manifest.name,
154
+ description: root.description ?? '',
155
+ load: publicIngestLoad(folderId, entry, adapter.contractShim),
156
+ ...(adapter.captureTimeoutMs !== undefined
157
+ ? { captureTimeoutMs: adapter.captureTimeoutMs }
158
+ : {}),
159
+ },
160
+ };
161
+ }
162
+
163
+ /**
164
+ * Resolve a vendored game id against `public/ingest/<id>/vgai.project.json`.
165
+ *
166
+ * `null` means "no such vendored three ingest" (no manifest served at that
167
+ * path) — the caller falls through to its own unknown-id report. A manifest
168
+ * that EXISTS but is unmountable THROWS, naming the reason: silently treating a
169
+ * malformed vendored manifest as absent is exactly the failure the anti-shim
170
+ * rule forbids.
171
+ */
172
+ export async function fetchPublicIngestGame(
173
+ id: string,
174
+ fetchImpl: typeof fetch = globalThis.fetch,
175
+ ): Promise<PublicIngestResolution | null> {
176
+ if (!isSafeIngestId(id)) return null;
177
+ const url = publicIngestManifestUrl(id);
178
+ let res: Response;
179
+ try {
180
+ res = await fetchImpl(url);
181
+ } catch {
182
+ return null;
183
+ }
184
+ if (!res.ok) return null;
185
+ // The dev server answers an unknown path with the editor's index.html rather
186
+ // than a 404, so an OK response is not by itself proof of a manifest — parse
187
+ // failure on this route means "not a vendored ingest", not a broken game.
188
+ let raw: unknown;
189
+ try {
190
+ raw = await res.json();
191
+ } catch {
192
+ return null;
193
+ }
194
+ return buildPublicIngestGame(id, raw);
195
+ }
196
+
197
+ /** Resolve a vendored canvas game, preserving the same 404 rules. */
198
+ export async function fetchPublicCanvasIngestGame(
199
+ id: string,
200
+ fetchImpl: typeof fetch = globalThis.fetch,
201
+ ): Promise<PublicCanvasIngestResolution | null> {
202
+ if (!isSafeIngestId(id)) return null;
203
+ let response: Response;
204
+ try {
205
+ response = await fetchImpl(publicIngestManifestUrl(id));
206
+ } catch {
207
+ return null;
208
+ }
209
+ if (!response.ok) return null;
210
+ let raw: unknown;
211
+ try {
212
+ raw = await response.json();
213
+ } catch {
214
+ return null;
215
+ }
216
+ return buildPublicCanvasIngestGame(id, raw);
217
+ }
218
+
219
+ /**
220
+ * Read THIS vendored game's own `UPSTREAM.md` pin (R5's overlay-identity check
221
+ * — an overlay is bound to the upstream commit the game was vendored at, not
222
+ * only to a static manifest `version`).
223
+ *
224
+ * Deliberately NOT `ingest/mount-ingest-root.ts`'s `readVendoredUpstreamPin`, which fetches
225
+ * the OPEN PROJECT's root `/UPSTREAM.md`: on this route the open project is
226
+ * whatever the editor happens to have open, and its pin has nothing to do with
227
+ * the vendored game being mounted. Every failure resolves to `null` — a missing
228
+ * or unparseable record is reported as "no pin", never fabricated.
229
+ */
230
+ export async function fetchPublicIngestUpstreamPin(
231
+ id: string,
232
+ fetchImpl: typeof fetch = globalThis.fetch,
233
+ ): Promise<string | null> {
234
+ if (!isSafeIngestId(id)) return null;
235
+ try {
236
+ const res = await fetchImpl(`${PUBLIC_INGEST_BASE}${id}/UPSTREAM.md`);
237
+ if (!res.ok) return null;
238
+ return extractUpstreamPin(await res.text());
239
+ } catch {
240
+ return null;
241
+ }
242
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The dataset key a host-made DOM stub carries (`ingest-root-adapter.ts`
3
+ * creates them for a game's declared `domStubs`), so a served bundle's staged
4
+ * page can retire the stub whose id it supplies itself (`served-html-boot.ts`).
5
+ * Shared here because the two sides must agree on one mark.
6
+ */
7
+ export const DOM_STUB_MARK = 'vgaiDomStub';
@@ -0,0 +1,56 @@
1
+ /**
2
+ * How an ingested game's modules are run: the game's own entry module, in the
3
+ * editor's realm, preceded by the host-added contract module the manifest names
4
+ * in `ingest.contractShim` when it declares one.
5
+ *
6
+ * ORDER IS THE WHOLE CONTRACT. The shim assigns `window.vgaiGame` synchronously
7
+ * at top level (see the field's schema description), so it must be evaluated
8
+ * BEFORE the game's entry — a game that reads the declaration during its own
9
+ * module evaluation would otherwise see nothing, and a shim that lazily
10
+ * `import()`s a game module inside a verb closure would reorder the game's own
11
+ * module side effects if it were evaluated after. Every discovery frontend
12
+ * (the in-tree fixture glob, the manifest/project-folder resolver, the vendored
13
+ * by-id fetcher) composes its two loaders through this one function rather than
14
+ * open-coding the sequence three times.
15
+ */
16
+ /**
17
+ * The manifest's `ingest.dataWriter`, resolved against however THIS discovery
18
+ * frontend serves the game's folder — the same three-way split `load` already
19
+ * has (globbed fixture, `public/` url, `/@fs/` project folder), spelled once so
20
+ * a fourth frontend cannot forget the field.
21
+ *
22
+ * The writer is deliberately NOT composed into `load`: it is not part of
23
+ * booting the game and must not be evaluated at mount. It is imported lazily,
24
+ * the first time an edit needs it, and a game that never has one edited never
25
+ * pays for it.
26
+ */
27
+ export function ingestDataWriter(
28
+ specifier: string | undefined,
29
+ loader: (relPath: string) => () => Promise<unknown>,
30
+ ): { dataWriter?: { specifier: string; load: () => Promise<unknown> } } {
31
+ return specifier === undefined ? {} : { dataWriter: { specifier, load: loader(specifier) } };
32
+ }
33
+
34
+ /**
35
+ * The game's OWN source modules this root declares — see
36
+ * {@link import('./types').IngestGame.sourceModules}. Spelled once here, beside
37
+ * `ingestDataWriter`, for the same reason: every discovery frontend must fill
38
+ * the field the same way or the lane decision's scope silently differs by route.
39
+ */
40
+ export function declaredSourceModules(
41
+ entry: string | undefined,
42
+ worldEntry: string | undefined,
43
+ ): string[] {
44
+ return [entry, worldEntry].filter((path): path is string => typeof path === 'string');
45
+ }
46
+
47
+ export function composeIngestLoad(
48
+ entry: () => Promise<unknown>,
49
+ contractShim: (() => Promise<unknown>) | undefined,
50
+ ): () => Promise<unknown> {
51
+ if (!contractShim) return entry;
52
+ return async () => {
53
+ await contractShim();
54
+ return entry();
55
+ };
56
+ }