@vgai/editor 0.2.0 → 0.4.0-canary.20260715.0

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 (430) hide show
  1. package/README.md +24 -0
  2. package/dist/assets/axe-C7YBtOuM.js +30 -0
  3. package/dist/assets/browserAll-DDtGGE48.js +1 -0
  4. package/dist/assets/create-runtime-DbTlBlKG.js +1 -0
  5. package/dist/assets/{game-BI111o5f.js → game-BE0r7DMI.js} +3 -3
  6. package/dist/assets/game-BdTHR2_X.js +1 -0
  7. package/dist/assets/{game-pt_1lQLv.js → game-C3iQGSqo.js} +1 -1
  8. package/dist/assets/game-CcHfYxjP.js +1 -0
  9. package/dist/assets/game-Cqzj_lpD.js +1 -0
  10. package/dist/assets/game-DSZdeCHu.js +1 -0
  11. package/dist/assets/game-Dch9sAPc.js +1 -0
  12. package/dist/assets/{game-xsOho_wg.js → game-SAFn90pA.js} +3 -3
  13. package/dist/assets/game-loop-DMIgSMV-.js +1 -0
  14. package/dist/assets/{game-source-CvidN7AB.js → game-source-Dk83w26t.js} +1 -1
  15. package/dist/assets/{index-Bp5cecD6.js → index-4idIsHHJ.js} +2 -2
  16. package/dist/assets/{index-DL7RcQ0n.js → index-C-LhOTBN.js} +1 -1
  17. package/dist/assets/{index-Cs8xwv3e.js → index-CDkxceqE.js} +1 -1
  18. package/dist/assets/index-CQEfi217.js +15412 -0
  19. package/dist/assets/index-Dvi-V0NE.css +1 -0
  20. package/dist/assets/ingest-siblings-s0P5I1Ub.js +1 -0
  21. package/dist/assets/mount-manifest-DXMOlF79.js +1 -0
  22. package/dist/assets/react-18-C9eHn8x1.js +1 -0
  23. package/dist/assets/webworkerAll-Fn0e-ouF.js +1 -0
  24. package/dist/assets/world-documents-GM0a9Kv7.js +1 -0
  25. package/dist/index.html +15 -13
  26. package/dist/ingest/racing-game/vgai.game.json +5 -4
  27. package/dist/ingest/simcity/vgai.game.json +5 -4
  28. package/dist/ingest/tanks/vgai.game.json +5 -4
  29. package/dist/project-scripts.bundle.js +3917 -0
  30. package/dist/scenes/untitled.vscn.json +2 -0
  31. package/dist-server/packaged.mjs +7697 -3435
  32. package/package.json +31 -4
  33. package/scripts/probe-reporter.ts +349 -0
  34. package/scripts/run-probe.ts +2260 -0
  35. package/server/asset-catalog-audit.ts +268 -0
  36. package/server/asset-catalog-v2.ts +146 -0
  37. package/server/asset-library-routes.ts +276 -58
  38. package/server/canonical-path.ts +1 -1
  39. package/server/dev.ts +148 -44
  40. package/server/editor-server.ts +888 -61
  41. package/server/editor-sse.ts +115 -4
  42. package/server/harness-chat-service.ts +796 -0
  43. package/server/local-asset-catalog.ts +157 -22
  44. package/server/packaged.ts +51 -21
  45. package/server/process-shutdown.ts +136 -0
  46. package/server/prod.ts +9 -6
  47. package/server/project-hmr-files.ts +41 -0
  48. package/server/project-operations.ts +343 -0
  49. package/server/project-optimize-deps-entries.ts +4 -4
  50. package/server/project-output-writer.ts +156 -0
  51. package/server/project-validation.ts +140 -0
  52. package/server/project-view.ts +8 -50
  53. package/server/server-utils.ts +75 -10
  54. package/server/session-registry.ts +12 -0
  55. package/src/EditorContext.tsx +107 -3
  56. package/src/action-registry.ts +115 -17
  57. package/src/adapter-resolver.ts +213 -478
  58. package/src/asset-events.ts +64 -1
  59. package/src/asset-preview.ts +906 -0
  60. package/src/asset-selection.ts +121 -0
  61. package/src/asset-workflow/asset-capabilities.ts +259 -0
  62. package/src/asset-workflow/asset-editor-errors.ts +46 -0
  63. package/src/asset-workflow/asset-import-pipeline.ts +176 -0
  64. package/src/asset-workflow/asset-library-query.ts +107 -0
  65. package/src/asset-workflow/asset-types.ts +192 -0
  66. package/src/asset-workflow/asset-workflow-quality.ts +65 -0
  67. package/src/asset-workflow/import-contract.ts +113 -0
  68. package/src/asset-workflow/model-conversion.ts +126 -0
  69. package/src/asset-workflow/model-inspection.ts +185 -0
  70. package/src/asset-workflow/project-asset-commands.ts +23 -0
  71. package/src/asset-workflow/project-asset-health.ts +153 -0
  72. package/src/asset-workflow/project-asset-operations.ts +119 -0
  73. package/src/asset-workflow/thumbnail-system.ts +172 -0
  74. package/src/authoring/active-systems.ts +15 -0
  75. package/src/authoring/boundary-authoring-adapter.ts +16 -5
  76. package/src/authoring/composite-authoring-adapter.ts +305 -124
  77. package/src/authoring/design-time-layers.ts +300 -117
  78. package/src/authoring/edit-mode-authoring.ts +278 -293
  79. package/src/authoring/embed-only-adapter.ts +4 -2
  80. package/src/authoring/first-party-hierarchy-menu.ts +7 -2
  81. package/src/authoring/first-party-inspector/AddSectionMenu.tsx +5 -12
  82. package/src/authoring/first-party-inspector/EnvironmentSection.tsx +53 -126
  83. package/src/authoring/first-party-inspector/HeaderSection.tsx +54 -65
  84. package/src/authoring/first-party-inspector/is-first-party-selection.ts +30 -11
  85. package/src/authoring/first-party-inspector/register.tsx +13 -2
  86. package/src/authoring/ingest-authoring-adapter.ts +142 -45
  87. package/src/authoring/mount-failure-report.ts +5 -5
  88. package/src/authoring/provenance.ts +41 -0
  89. package/src/authoring/react-dom-authoring-adapter.ts +92 -92
  90. package/src/authoring/react-inspector-section.tsx +489 -51
  91. package/src/authoring/react-world-authoring-adapter.ts +541 -306
  92. package/src/authoring/story-overlay.ts +1 -1
  93. package/src/authoring/story-types.ts +11 -0
  94. package/src/authoring/vgai-scene-authoring-adapter.ts +54 -25
  95. package/src/authoring/viewport-tool-context.ts +78 -0
  96. package/src/authoring/world-canvas-viewport-state.ts +35 -0
  97. package/src/authoring/world-hidden-viewport.ts +186 -0
  98. package/src/authoring/world-pan-state.ts +33 -17
  99. package/src/authoring/world-session-state.ts +1 -1
  100. package/src/authoring/world2d-authoring-adapter.ts +109 -52
  101. package/src/availability-tick.ts +56 -0
  102. package/src/browser-play-entry.ts +5 -5
  103. package/src/browser-project-scripts.ts +1 -0
  104. package/src/build-identity.ts +15 -0
  105. package/src/command-listener.ts +893 -19
  106. package/src/components/AlignToolbar.tsx +74 -63
  107. package/src/components/AppRoot.tsx +193 -26
  108. package/src/components/ApplicationMenus.tsx +272 -0
  109. package/src/components/AssetBrowser.tsx +1450 -293
  110. package/src/components/AssetEditorShell.tsx +141 -0
  111. package/src/components/AssetImportDetails.tsx +218 -0
  112. package/src/components/AssetLibraryPanel.tsx +14 -0
  113. package/src/components/CameraInfo.tsx +11 -20
  114. package/src/components/CenterDocuments.tsx +585 -0
  115. package/src/components/CommandPalette.tsx +75 -30
  116. package/src/components/ConsolePanel.css +55 -0
  117. package/src/components/ConsolePanel.tsx +57 -112
  118. package/src/components/DebugConsolePanel.tsx +461 -0
  119. package/src/components/DebuggerPanel.tsx +165 -0
  120. package/src/components/DefaultEditorLayout.tsx +167 -231
  121. package/src/components/GameHierarchy.tsx +873 -290
  122. package/src/components/GamePerformanceOverlay.tsx +46 -0
  123. package/src/components/HarnessChatPanel.tsx +2352 -0
  124. package/src/components/HistoryPanel.tsx +226 -0
  125. package/src/components/Inspector.tsx +153 -99
  126. package/src/components/InspectorToolSection.tsx +6 -4
  127. package/src/components/MountFailureBanner.tsx +31 -48
  128. package/src/components/NetworkSection.tsx +25 -14
  129. package/src/components/OnlineAssetBrowser.tsx +794 -343
  130. package/src/components/OverlayReportBanner.tsx +38 -64
  131. package/src/components/PerformancePanel.tsx +206 -0
  132. package/src/components/PlayBar.tsx +93 -263
  133. package/src/components/PrefabSaveDialog.tsx +55 -127
  134. package/src/components/ProjectHeader.css +60 -0
  135. package/src/components/ProjectHeader.tsx +170 -105
  136. package/src/components/ProjectScreen.tsx +246 -184
  137. package/src/components/ReactCanvasControls.tsx +185 -0
  138. package/src/components/ResolutionPicker.tsx +85 -0
  139. package/src/components/RootActivateDialog.tsx +39 -68
  140. package/src/components/SaveDialog.tsx +55 -127
  141. package/src/components/SaveStatus.tsx +11 -29
  142. package/src/components/StartupLoadingScreen.tsx +96 -0
  143. package/src/components/StateWatchPanel.tsx +302 -0
  144. package/src/components/StatsOverlay.tsx +12 -5
  145. package/src/components/StatusBar.tsx +64 -0
  146. package/src/components/ThemeManagerDialog.css +89 -0
  147. package/src/components/ThemeManagerDialog.tsx +371 -0
  148. package/src/components/ToolHost.tsx +26 -15
  149. package/src/components/Toolbar.tsx +117 -214
  150. package/src/components/VgaiVoxelLogo.tsx +89 -0
  151. package/src/components/ViewportOverlay.tsx +97 -172
  152. package/src/components/ViewportPanel.tsx +130 -31
  153. package/src/components/WorkspaceDock.tsx +891 -0
  154. package/src/components/WorldSelectionOverlay.tsx +51 -63
  155. package/src/components/WorldTextEditor.tsx +9 -5
  156. package/src/components/asset-documents.tsx +390 -0
  157. package/src/components/asset-editor-persistence.ts +44 -0
  158. package/src/components/asset-selection-section.tsx +338 -0
  159. package/src/components/asset-viewers/AudioViewer.tsx +73 -21
  160. package/src/components/asset-viewers/GenericJsonViewer.tsx +26 -11
  161. package/src/components/asset-viewers/ImageViewer.tsx +43 -11
  162. package/src/components/asset-viewers/InputMapViewer.tsx +512 -134
  163. package/src/components/asset-viewers/MaterialAssetEditor.tsx +540 -0
  164. package/src/components/asset-viewers/ModelViewer.tsx +805 -121
  165. package/src/components/asset-viewers/OnlineAssetDetail.tsx +177 -60
  166. package/src/components/asset-viewers/SourceAssetViewer.tsx +121 -0
  167. package/src/components/asset-workflow.css +506 -0
  168. package/src/components/build-documents.tsx +696 -0
  169. package/src/components/core-utilities.tsx +75 -0
  170. package/src/components/data-documents.tsx +364 -0
  171. package/src/components/{DataPanel.tsx → data-editor.tsx} +138 -288
  172. package/src/components/debugger-panel-state.ts +57 -0
  173. package/src/components/inspector-widgets/AlignmentGrid.tsx +8 -14
  174. package/src/components/inspector-widgets/BorderEditor.tsx +41 -33
  175. package/src/components/inspector-widgets/ColorPicker.tsx +39 -39
  176. package/src/components/inspector-widgets/FontPicker.tsx +13 -21
  177. package/src/components/inspector-widgets/GradientEditor.tsx +11 -19
  178. package/src/components/inspector-widgets/ScrubbableInput.tsx +6 -3
  179. package/src/components/inspector-widgets/ShadowEditor.tsx +20 -33
  180. package/src/components/inspector-widgets/shared.tsx +90 -49
  181. package/src/components/inspectors/AnimationSection.tsx +39 -40
  182. package/src/components/inspectors/AudioSection.tsx +28 -39
  183. package/src/components/inspectors/BoneAttachmentSection.tsx +19 -24
  184. package/src/components/inspectors/CameraSection.tsx +14 -23
  185. package/src/components/inspectors/ComponentsSection.tsx +39 -43
  186. package/src/components/inspectors/JointsSection.tsx +34 -43
  187. package/src/components/inspectors/LightSection.tsx +90 -35
  188. package/src/components/inspectors/MaterialSection.tsx +137 -174
  189. package/src/components/inspectors/MeshSection.tsx +9 -17
  190. package/src/components/inspectors/NavigationSection.tsx +8 -15
  191. package/src/components/inspectors/ParticleSection.tsx +91 -76
  192. package/src/components/inspectors/PhysicsSection.tsx +42 -46
  193. package/src/components/inspectors/PostProcessingSection.tsx +28 -36
  194. package/src/components/inspectors/RenderingSection.tsx +12 -20
  195. package/src/components/inspectors/ShadowSection.tsx +7 -8
  196. package/src/components/inspectors/SplineSection.tsx +31 -42
  197. package/src/components/inspectors/TransformSection.tsx +14 -22
  198. package/src/components/patterns/Dialog.tsx +95 -0
  199. package/src/components/patterns/Fields.tsx +44 -0
  200. package/src/components/patterns/List.tsx +25 -0
  201. package/src/components/patterns/StateSurface.tsx +40 -0
  202. package/src/components/patterns/Surfaces.tsx +115 -0
  203. package/src/components/patterns/Tabs.tsx +80 -0
  204. package/src/components/patterns/Toolbar.tsx +51 -0
  205. package/src/components/patterns/Tree.tsx +72 -0
  206. package/src/components/primitives/Button.tsx +46 -0
  207. package/src/components/primitives/ColorInput.tsx +11 -47
  208. package/src/components/primitives/EditorIcon.tsx +99 -0
  209. package/src/components/primitives/FormControls.tsx +71 -0
  210. package/src/components/primitives/JsonInput.tsx +15 -2
  211. package/src/components/primitives/Layout.tsx +100 -0
  212. package/src/components/primitives/Menu.tsx +105 -0
  213. package/src/components/primitives/NumberInput.tsx +8 -35
  214. package/src/components/primitives/Panel.tsx +49 -44
  215. package/src/components/primitives/SectionHeader.tsx +10 -28
  216. package/src/components/primitives/Text.tsx +46 -0
  217. package/src/components/primitives/Tooltip.tsx +20 -12
  218. package/src/components/primitives/Vec3Input.tsx +3 -0
  219. package/src/components/primitives/banner-tones.ts +33 -0
  220. package/src/components/primitives/editor-icons.ts +102 -0
  221. package/src/components/primitives/panel-header-styles.ts +30 -0
  222. package/src/components/project-operation-documents.tsx +226 -0
  223. package/src/components/status-contributions.tsx +175 -0
  224. package/src/components/story-documents-register.ts +15 -0
  225. package/src/components/story-documents.tsx +1185 -0
  226. package/src/components/tool-documents.tsx +110 -0
  227. package/src/components/workspace-dock.css +293 -0
  228. package/src/components/workspace-history.ts +33 -0
  229. package/src/components/world-documents.tsx +145 -0
  230. package/src/components/xstate-documents.tsx +209 -0
  231. package/src/composite-screenshot.ts +248 -0
  232. package/src/design-system/index.ts +70 -0
  233. package/src/design-system-stories/Foundation.stories.tsx +183 -0
  234. package/src/design-system-stories/Patterns.stories.tsx +251 -0
  235. package/src/design-system-stories/Workspace.stories.tsx +538 -0
  236. package/src/doctor/report.ts +57 -13
  237. package/src/editor-api.ts +295 -31
  238. package/src/editor-compatibility.ts +200 -0
  239. package/src/editor-console.ts +35 -17
  240. package/src/editor-hotkeys.ts +85 -33
  241. package/src/editor-store.ts +538 -233
  242. package/src/editor-viewport.ts +160 -34
  243. package/src/entity-factory.ts +14 -3
  244. package/src/gizmo-registry.ts +21 -5
  245. package/src/harness-chat-editor-context.ts +37 -0
  246. package/src/harness-chat-indicator.ts +68 -0
  247. package/src/harness-chat-layout.ts +53 -0
  248. package/src/harness-chat-lifecycle.ts +170 -0
  249. package/src/harness-chat-reconnect.ts +85 -0
  250. package/src/harness-chat-types.ts +409 -0
  251. package/src/hierarchy-drop.ts +50 -0
  252. package/src/hierarchy-expansion-state.ts +100 -0
  253. package/src/hierarchy-menu-registry.ts +1 -1
  254. package/src/hierarchy-projection.ts +76 -0
  255. package/src/history/editor-session.ts +21 -0
  256. package/src/history/history-commands.ts +147 -0
  257. package/src/history/history-service.ts +904 -0
  258. package/src/history/json-history-resource.ts +165 -0
  259. package/src/history/persistence-coordinator.ts +35 -0
  260. package/src/history/project-file-history.ts +266 -0
  261. package/src/history/project-root-history-backends.ts +53 -0
  262. package/src/history/resource-registry.ts +209 -0
  263. package/src/history/snapshot-store.ts +108 -0
  264. package/src/history/source-history-backend.ts +374 -0
  265. package/src/history/types.ts +100 -0
  266. package/src/hmr-registration-group.ts +67 -0
  267. package/src/hmr-stable-react-context.ts +23 -0
  268. package/src/hosted-example.ts +12 -2
  269. package/src/hotkeys.ts +89 -18
  270. package/src/ingest/discovery-react.ts +9 -10
  271. package/src/ingest/discovery.ts +4 -4
  272. package/src/ingest/discovery2d.ts +5 -5
  273. package/src/ingest/games/bloom-composer/vgai.game.json +3 -3
  274. package/src/ingest/games/embed-bundle/vgai.game.json +3 -3
  275. package/src/ingest/games/games-fps/vgai.game.json +3 -3
  276. package/src/ingest/games/iframe-reachable/vgai.game.json +3 -3
  277. package/src/ingest/games/interactive-cubes/vgai.game.json +3 -3
  278. package/src/ingest/games/mismatched-three/vgai.game.json +3 -3
  279. package/src/ingest/games/own-three-bundled/vgai.game.json +3 -3
  280. package/src/ingest/games/pointerlock-maze/vgai.game.json +7 -4
  281. package/src/ingest/games-2d/bubbo-bubbo/vgai.game.json +3 -3
  282. package/src/ingest/games-2d/bunnymark/vgai.game.json +3 -3
  283. package/src/ingest/games-2d/flappy/vgai.game.json +3 -3
  284. package/src/ingest/games-2d/flappy-pixi/vgai.game.json +3 -3
  285. package/src/ingest/games-2d/puzzling-potions/vgai.game.json +3 -3
  286. package/src/ingest/games-2d/tilemap-camera/vgai.game.json +3 -3
  287. package/src/ingest/games-react/react-rpg/vgai.game.json +3 -3
  288. package/src/ingest/types.ts +1 -1
  289. package/src/ingest-mode.ts +47 -52
  290. package/src/ingest-siblings.ts +23 -33
  291. package/src/initial-scene.ts +1 -2
  292. package/src/main.tsx +16 -8
  293. package/src/manifest-project.ts +10 -12
  294. package/src/model-thumbnail.ts +261 -26
  295. package/src/module-mode.ts +44 -50
  296. package/src/play-log-events.ts +26 -0
  297. package/src/play-mode.ts +495 -401
  298. package/src/play-scene-adoption.ts +23 -0
  299. package/src/playground/main.ts +68 -24
  300. package/src/playwright-shim.ts +473 -0
  301. package/src/project-manager.ts +90 -10
  302. package/src/project-operations.ts +112 -0
  303. package/src/project-story-discovery.ts +25 -0
  304. package/src/project-tool-discovery.ts +43 -0
  305. package/src/scene-serializer.ts +0 -6
  306. package/src/scene-sync.ts +7 -2
  307. package/src/script-loader.ts +3 -3
  308. package/src/skeleton-helper.ts +57 -0
  309. package/src/storage/seed.ts +68 -8
  310. package/src/stories/StoryPreviewMount.tsx +265 -0
  311. package/src/stories/action-stories.fixture.tsx +44 -0
  312. package/src/stories/button-stories.fixture.tsx +50 -0
  313. package/src/stories/compose-project-stories.ts +182 -0
  314. package/src/stories/loader-stories.fixture.tsx +53 -0
  315. package/src/stories/story-discovery.ts +130 -0
  316. package/src/stories/story-registry.ts +146 -0
  317. package/src/theme-css.ts +1049 -0
  318. package/src/theme-library.ts +238 -0
  319. package/src/theme-preference.ts +130 -0
  320. package/src/theme.ts +527 -0
  321. package/src/tool-loader.ts +80 -24
  322. package/src/ui-source/inspect.ts +27 -4
  323. package/src/ui-source/source-write-backend.ts +83 -22
  324. package/src/widgets/index.ts +9 -18
  325. package/src/workspace-aux-commands.ts +11 -0
  326. package/src/workspace-dock-controller.ts +64 -0
  327. package/src/workspace-document-ids.ts +5 -0
  328. package/src/workspace-document-registry.ts +367 -0
  329. package/src/workspace-state-persistence.ts +455 -0
  330. package/src/workspace-status-registry.ts +110 -0
  331. package/src/workspace-utility-commands.ts +49 -0
  332. package/src/workspace-utility-registry.ts +161 -0
  333. package/src/world-document-routing.ts +16 -0
  334. package/src/xstate-inspect/XStateMachineInspector.tsx +544 -0
  335. package/src/xstate-inspect/character-animation-machine.fixture.ts +74 -0
  336. package/src/xstate-inspect/use-live-actor-state.ts +39 -0
  337. package/src/xstate-inspect/xstate-graph.ts +265 -0
  338. package/src/xstate-inspect/xstate-layout.ts +76 -0
  339. package/src/z-index.ts +26 -0
  340. package/template/.claude/skills/editor/SKILL.md +129 -90
  341. package/template/AGENTS.md +1110 -0
  342. package/template/CLAUDE.md +1 -512
  343. package/template/IDIOMS.md +50 -0
  344. package/template/check-idioms.ts +706 -0
  345. package/template/manifest-entry-modules-plugin.ts +53 -0
  346. package/template/package.json +13 -4
  347. package/template/public/scenes/default.scn2d.json +6 -0
  348. package/template/public/scenes/default.vscn.json +7 -1
  349. package/template/public/scenes/untitled.vscn.json +10 -0
  350. package/template/scripts/playtest.ts +40 -0
  351. package/template/server/loopback.ts +75 -0
  352. package/template/server/rooms/arena-room.ts +69 -1
  353. package/template/server/rooms/game-room.ts +68 -1
  354. package/template/src/data/README.md +9 -4
  355. package/template/src/data/data-ref.ts +35 -0
  356. package/template/src/data/tuning.schema.ts +1 -1
  357. package/template/src/main.ts +41 -62
  358. package/template/src/manifest-entry-modules.d.ts +3 -0
  359. package/template/src/operations/README.md +88 -0
  360. package/template/src/runtime/pixi-adapter.ts +24 -0
  361. package/template/src/scripts/components/data-spinner.ts +43 -2
  362. package/template/src/scripts/components/spin-lap-math.ts +27 -0
  363. package/template/src/tools/README.md +5 -3
  364. package/template/src/tools/example.tool.tsx +13 -7
  365. package/template/src/ui/Button.stories.tsx +66 -0
  366. package/template/src/ui/RebindPanel.tsx +2 -6
  367. package/template/src/ui/game.tsx +17 -0
  368. package/template/tests/acceptance/example.spec.ts +64 -0
  369. package/template/tests/logic/example.test.ts +31 -0
  370. package/template/tests/logic/room.test.ts +100 -0
  371. package/template/tests/tsconfig.json +30 -0
  372. package/template/validate-manifest.ts +5 -5
  373. package/template/validate-scenes.ts +50 -18
  374. package/template/vgai.game.json +20 -13
  375. package/template/vite.config.ts +77 -16
  376. package/vite-plugin-ui-oid.ts +261 -17
  377. package/dist/assets/CCDIKSolver-Dz-ysxZF.js +0 -1
  378. package/dist/assets/SelectionBox-BXsv_6aH.js +0 -1
  379. package/dist/assets/browser-project-scripts-DOru4L1x.js +0 -2
  380. package/dist/assets/browserAll-CEkWYlFk.js +0 -1
  381. package/dist/assets/create-runtime-DYIWr3va.js +0 -1
  382. package/dist/assets/data-asset-Bb9sIM7i.js +0 -5
  383. package/dist/assets/game-8SDZPPgH.js +0 -1
  384. package/dist/assets/game-CevorxyV.js +0 -1
  385. package/dist/assets/game-Di7XlywD.js +0 -1
  386. package/dist/assets/game-DjnCaw8R.js +0 -1
  387. package/dist/assets/game-Q9A0aXSV.js +0 -1
  388. package/dist/assets/index-CPVV6pjw.js +0 -1
  389. package/dist/assets/index-CpOnYyeD.js +0 -213
  390. package/dist/assets/index-Dr8H_6Rl.js +0 -1
  391. package/dist/assets/index-_bCaipUk.js +0 -13650
  392. package/dist/assets/ingest-siblings-Dz8eEqrV.js +0 -1
  393. package/dist/assets/mount-manifest-Bhk0ohsl.js +0 -1
  394. package/dist/assets/scene-query-BivAHhJu.js +0 -1
  395. package/dist/assets/webworkerAll-BLrH0F6F.js +0 -1
  396. package/dist/assets/xstate-animation-binding-1NGLmw6y.js +0 -1
  397. package/dist/assets/xstate.esm-BgUD33nl.js +0 -3
  398. package/src/authoring/scene-ui-layer.ts +0 -485
  399. package/src/authoring/ui-authoring-adapter.ts +0 -325
  400. package/src/authoring/ui-hierarchy-menu.ts +0 -40
  401. package/src/authoring/ui-inspector-sections.tsx +0 -156
  402. package/src/authoring/undo-timeline.ts +0 -145
  403. package/src/components/BottomPanel.tsx +0 -326
  404. package/src/components/BuildPanel.tsx +0 -455
  405. package/src/components/FolderTree.tsx +0 -179
  406. package/src/components/RightPanel.tsx +0 -166
  407. package/src/components/WorldContextMenu.tsx +0 -257
  408. package/src/components/asset-viewers/AssetViewer.tsx +0 -38
  409. package/src/ingest/games-react/react-rpg/src/ui/game-state.tsx +0 -129
  410. package/src/scene-ui-mode/active-scene-ui.ts +0 -58
  411. package/src/scene-ui-mode/scene-ui-palette-panel.tsx +0 -152
  412. package/src/scene-ui-mode/scene-ui-timeline-tab.tsx +0 -29
  413. package/src/scene-ui-mode/ui-prefab-dialog-host.tsx +0 -74
  414. package/src/scene-ui-mode/ui-prefab-hierarchy-menu.ts +0 -49
  415. package/src/scene-ui-mode/ui-prefab-io.ts +0 -76
  416. package/src/ui-editor/inspector.tsx +0 -563
  417. package/src/ui-editor/node-ops.ts +0 -266
  418. package/src/ui-editor/overlay.tsx +0 -380
  419. package/src/ui-editor/palette.tsx +0 -114
  420. package/src/ui-editor/react-store.ts +0 -78
  421. package/src/ui-editor/timeline-panel.tsx +0 -443
  422. package/src/ui-editor/ui-edit-store.ts +0 -552
  423. package/src/ui-editor/ui-prefab.ts +0 -95
  424. package/template/project.json +0 -9
  425. package/template/src/scripts/main.ts +0 -94
  426. package/template/src/ui/DefaultHud.tsx +0 -40
  427. package/template/src/ui/game-state.tsx +0 -37
  428. package/template/src/ui/mount-default-hud.tsx +0 -19
  429. package/template/src/ui/mount-ui.ts +0 -15
  430. package/vite-plugin-react-world-provider.ts +0 -81
@@ -9,7 +9,7 @@
9
9
  * (docs/REACT-INGEST-LANDING-DESIGN.md D-N2/D-N3/D-N4) for `ingest-react`
10
10
  * (vendored native-React game ingestion — the deduped-only mount).
11
11
  *
12
- * Turns one manifest world (`ResolvedWorldEntry`, from `@engine/manifest/load`)
12
+ * Turns one manifest world (`ResolvedAdapterRoot`, from `@engine/manifest/load`)
13
13
  * into a mountable {@link GameAdapter} (or, for a pixijs world, a structurally
14
14
  * distinct {@link Pixi2DGameAdapter} — see the default-pixi section below).
15
15
  * Lives editor-side, not in the engine package: it needs `/@fs/` dynamic-import
@@ -34,11 +34,11 @@
34
34
  * slice 4) — same two-layer shape as ingest-three (descriptor builder +
35
35
  * a named-stub `GameAdapter`; the real mount is `ingest-mode.ts`'s
36
36
  * `enterIngestModeFromManifestWorld2D`).
37
- * - `default-react` -> a `ReactWorldAdapter` (T6.2, see
37
+ * - `default-react` -> a `ReactRootAdapter` (T6.2, see
38
38
  * `resolveDefaultReactAdapter` below for the `entry`-only rule — `scene`
39
39
  * is a named error, D4 — and the two engine-seam findings its doc
40
40
  * comment reports).
41
- * - `ingest-react` -> also a `ReactWorldAdapter` (Track N, N1 —
41
+ * - `ingest-react` -> also a `ReactRootAdapter` (Track N, N1 —
42
42
  * docs/REACT-INGEST-LANDING-DESIGN.md D-N2/D-N3, see
43
43
  * `resolveIngestReactAdapter` below): same T6.2 mount machinery as
44
44
  * `default-react`, restricted to the `'deduped'` strategy rung (a named
@@ -51,7 +51,7 @@
51
51
  * ingest embed-only degradation path's retrofit (`ingest-mode.ts`).
52
52
  *
53
53
  * `resolveAllWorlds` (T6.1 slice 2, extended T6.2 for react) is the
54
- * multi-world sibling of `resolveWorldAdapter`: it resolves every world in a
54
+ * multi-world sibling of `resolveRootAdapter`: it resolves every world in a
55
55
  * `ResolvedGameManifest` into slice-1 `WorldMountSpec`s, enforcing the v1
56
56
  * scope rule for manifests with more than one world (docs/MULTI-WORLD-
57
57
  * DESIGN.md §3 slice 4, docs/REACT-WORLD-DESIGN.md §3 for react's admission).
@@ -59,14 +59,15 @@
59
59
 
60
60
  import type { GameAdapter } from '@engine/adapter';
61
61
  import { VgaiSceneGameAdapter } from '@engine/adapter';
62
- import { assertNever } from '@engine/adapter/world-kind';
62
+ import { assertNever } from '@engine/adapter/adapter-surface';
63
+ import type { GameComponentClass } from '@engine/ecs/game-component';
63
64
  import { resolveUrl } from '@engine/loader';
64
- import type { ResolvedGameManifest, ResolvedWorldEntry } from '@engine/manifest/load';
65
+ import type { ResolvedAdapterRoot, ResolvedGameManifest } from '@engine/manifest/load';
65
66
  import { GameProvider as EngineGameProvider } from '@engine/react/game-state';
66
67
  import type {
67
68
  MountedReactGame,
68
69
  Pixi2DGameAdapter,
69
- ReactWorldAdapter,
70
+ ReactRootAdapter,
70
71
  ReactWorldHost,
71
72
  WorldMountSpec,
72
73
  } from '@engine/runtime/create-runtime';
@@ -78,11 +79,10 @@ import { SceneParseError } from '@engine/scene/parse';
78
79
  import type { IngestGame2D } from '@engine/world2d/ingest2d';
79
80
  import { PixiSceneGameAdapter, type World2DSetupFn } from '@engine/world2d/pixi-game-adapter';
80
81
  import { type Scene2DFile, Scene2DSchema } from '@engine/world2d/schema/entity2d';
81
- import type { ProjectUIRegistry } from '@vgai/scene-ui/registry';
82
+ import type { Component2DRegistry } from '@engine/world2d/types';
82
83
  import { type ComponentType, createElement } from 'react';
83
84
  import { flushSync } from 'react-dom';
84
85
  import { createRoot } from 'react-dom/client';
85
- import { reactWorldProviderImportSpecifier } from '../vite-plugin-react-world-provider';
86
86
  import { REACT_WORLD_RUNTIME_PATH } from '../vite-plugin-react-world-runtime';
87
87
  import {
88
88
  computeAchievedTier,
@@ -126,8 +126,8 @@ export function fsImportPath(projectRoot: string, relativePath: string): string
126
126
  return root.startsWith('/') ? `/@fs${root}/${rel}` : `/@fs/${root}/${rel}`;
127
127
  }
128
128
 
129
- /** Optional editor-session context for {@link resolveWorldAdapter}. */
130
- export interface ResolveWorldAdapterOptions {
129
+ /** Optional editor-session context for {@link resolveRootAdapter}. */
130
+ export interface ResolveRootAdapterOptions {
131
131
  /**
132
132
  * The editor's play-launch context (live in-memory scene document +
133
133
  * viewport-camera spawn) — supplied by `play-mode.ts` when the resolver is
@@ -150,6 +150,51 @@ export interface ResolveWorldAdapterOptions {
150
150
  // Part B — componentRegistry via the project's script registry
151
151
  // ---------------------------------------------------------------------------
152
152
 
153
+ /**
154
+ * Fresh component constructors imported by the dev-server HMR path, scoped to
155
+ * their project and registry key. Vite correctly lets us import the edited
156
+ * leaf module with a cache-busting URL for live prototype swapping, but a
157
+ * later game-only restart can still resolve the cached registry module and
158
+ * therefore reconstruct instances with the pre-edit constructor/field
159
+ * initializers. Keeping the exact fresh constructor at this existing editor
160
+ * boundary makes `Restart Game` a real remount without reloading the editor.
161
+ *
162
+ * Overrides never fabricate registry entries: they replace only keys the
163
+ * project's current registry still exports. A registry edit clears the map,
164
+ * so deleting/remapping a key remains authoritative.
165
+ */
166
+ const componentHotOverridesByProject = new Map<string, Map<string, GameComponentClass>>();
167
+
168
+ export function registerProjectComponentHotOverride(
169
+ projectRoot: string,
170
+ key: string,
171
+ component: GameComponentClass,
172
+ ): void {
173
+ let overrides = componentHotOverridesByProject.get(projectRoot);
174
+ if (!overrides) {
175
+ overrides = new Map();
176
+ componentHotOverridesByProject.set(projectRoot, overrides);
177
+ }
178
+ overrides.set(key, component);
179
+ }
180
+
181
+ export function clearProjectComponentHotOverrides(projectRoot: string): void {
182
+ componentHotOverridesByProject.delete(projectRoot);
183
+ }
184
+
185
+ export function applyProjectComponentHotOverrides(
186
+ projectRoot: string,
187
+ registry: ComponentRegistry,
188
+ ): ComponentRegistry {
189
+ const overrides = componentHotOverridesByProject.get(projectRoot);
190
+ if (!overrides || overrides.size === 0) return registry;
191
+ const next = { ...registry };
192
+ for (const [key, component] of overrides) {
193
+ if (Object.hasOwn(registry, key)) next[key] = component;
194
+ }
195
+ return next;
196
+ }
197
+
153
198
  /**
154
199
  * Load the project's component registry (`src/scripts/registry.ts`'s
155
200
  * `componentRegistry` export) via the same `/@fs/` dev-server route
@@ -173,98 +218,29 @@ export async function loadComponentRegistryForProject(
173
218
  const mod = (await import(/* @vite-ignore */ modulePath)) as {
174
219
  componentRegistry?: ComponentRegistry;
175
220
  };
176
- return mod.componentRegistry ?? {};
221
+ return applyProjectComponentHotOverrides(projectRoot, mod.componentRegistry ?? {});
177
222
  } catch {
178
223
  return {};
179
224
  }
180
225
  }
181
226
 
182
- // ---------------------------------------------------------------------------
183
- // Part B.2 — the project's scene-UI registry (design/24-scene-ui.md D9/D11)
184
- // ---------------------------------------------------------------------------
185
-
186
- /**
187
- * Load the project's scene-UI registry (`src/ui/registry.ts`'s `uiRegistry`
188
- * export) via the same `/@fs/` dev-server route as {@link
189
- * loadComponentRegistryForProject} above — but with ONE deliberate departure
190
- * from that loader's behavior: an import FAILURE (the file exists but throws
191
- * — a syntax error, a bad import, a thrown top-level expression) logs a loud
192
- * `editorConsole.warn` naming the real error before falling back to `null`.
193
- * `loadComponentRegistryForProject` folds "absent" and "broken" into the same
194
- * silent `{}` because most projects simply have no `src/scripts/registry.ts`
195
- * at all; a scene-UI registry is different — the whole point of this PR is
196
- * project-authored UI, so a registry with a typo silently demoting the
197
- * editor back to demo fixtures (D10) is exactly the confusing failure mode
198
- * this loader exists to make loud instead.
199
- *
200
- * The common "no registry file" case (every scaffolded project, by design —
201
- * D10: the template does NOT ship one) stays silent: the editor server's
202
- * `/__editor/project` response carries `hasUiRegistry` (a server-side stat of
203
- * `src/ui/registry.ts`), which distinguishes "the file doesn't exist" (no
204
- * warning, resolves `null`) from "the file exists but the import threw"
205
- * (warns). Probing the `/@fs/` URL itself cannot make that call: a dev server
206
- * answers 200 for missing files (SPA fallback), and the doomed dynamic import
207
- * that follows logs an unsuppressible browser network error on every
208
- * registry-less project — which strict harnesses
209
- * (scripts/check-vgai-generated-project-p2p.ts) rightly fail on.
210
- * Returns `null` for both "absent" and "present but empty" (`uiRegistry`
211
- * undefined) — D10 treats both the same (demo mode).
212
- */
213
- export async function loadProjectUIRegistry(
227
+ /** Pixi sibling of {@link loadComponentRegistryForProject}. A project may
228
+ * export both registries from the same conventional module; each world kind
229
+ * receives only the classes valid for its native node type. */
230
+ export async function loadPixiComponentRegistryForProject(
214
231
  projectRoot: string,
215
- ): Promise<ProjectUIRegistry | null> {
216
- const modulePath = fsImportPath(projectRoot, 'src/ui/registry.ts');
217
- // Inline fetch rather than editor-api's getCurrentServerProject(): this
218
- // module must stay importable by headless Node/vitest suites, and
219
- // editor-api's module top level pulls in ./storage.
220
- let exists: boolean;
221
- try {
222
- const res = await fetch('/__editor/project');
223
- const data = (await res.json()) as { project?: { hasUiRegistry?: boolean } | null };
224
- exists = Boolean(data.project?.hasUiRegistry);
225
- } catch {
226
- exists = false;
227
- }
228
- if (!exists) return null;
232
+ ): Promise<Component2DRegistry> {
233
+ const modulePath = fsImportPath(projectRoot, 'src/scripts/registry.ts');
229
234
  try {
230
235
  const mod = (await import(/* @vite-ignore */ modulePath)) as {
231
- uiRegistry?: ProjectUIRegistry;
236
+ pixiComponentRegistry?: Component2DRegistry;
232
237
  };
233
- return mod.uiRegistry ?? null;
234
- } catch (err) {
235
- // Lazy dynamic import — see withAchievedTierCheck's doc comment above:
236
- // this resolver is imported by headless Node/vitest suites with no
237
- // `window`, and editor-console.ts's module top level touches `window`
238
- // unconditionally.
239
- const { editorConsole } = await import('./editor-console');
240
- editorConsole.warn(
241
- `src/ui/registry.ts failed to import — UI mode falls back to demo fixtures: ${String(err)}`,
242
- 'ui-mode',
243
- );
244
- return null;
238
+ return mod.pixiComponentRegistry ?? {};
239
+ } catch {
240
+ return {};
245
241
  }
246
242
  }
247
243
 
248
- let _cachedProjectUIRegistry: ProjectUIRegistry | null = null;
249
-
250
- /**
251
- * Cache slot for the project's scene-UI registry (D9/D11) — populated by
252
- * `authoring/scene-ui-layer.ts`'s design-time layer mount (once per project
253
- * open, when the scene has a `ui` root to disclose — C3) and read by
254
- * `resolveDefaultThreeAdapter` below so the editor-play path doesn't
255
- * reload/re-parse the same module a second time per play. A headless/CLI-
256
- * on-folder resolve (no editor session, no scene-UI layer ever mounts)
257
- * simply reads the initial `null` — zero behavior change there.
258
- */
259
- export function setCachedProjectUIRegistry(registry: ProjectUIRegistry | null): void {
260
- _cachedProjectUIRegistry = registry;
261
- }
262
-
263
- /** @see setCachedProjectUIRegistry */
264
- export function getCachedProjectUIRegistry(): ProjectUIRegistry | null {
265
- return _cachedProjectUIRegistry;
266
- }
267
-
268
244
  // ---------------------------------------------------------------------------
269
245
  // Named errors for identities not yet resolvable (report the task, not just fail)
270
246
  // ---------------------------------------------------------------------------
@@ -274,9 +250,9 @@ export function getCachedProjectUIRegistry(): ProjectUIRegistry | null {
274
250
  // ---------------------------------------------------------------------------
275
251
 
276
252
  async function resolveDefaultThreeAdapter(
277
- world: ResolvedWorldEntry,
253
+ world: ResolvedAdapterRoot,
278
254
  projectRoot: string,
279
- opts?: ResolveWorldAdapterOptions,
255
+ opts?: ResolveRootAdapterOptions,
280
256
  ): Promise<GameAdapter> {
281
257
  const editorPreview = opts?.editorPreview;
282
258
 
@@ -337,27 +313,24 @@ async function resolveDefaultThreeAdapter(
337
313
  // populated (e.g. a headless/CLI-on-folder resolve, or a scene with no
338
314
  // `ui` root) so the spread below omits the key entirely rather than
339
315
  // passing an explicit `uiRegistry: null`.
340
- const uiRegistry = getCachedProjectUIRegistry() ?? undefined;
341
316
  if (editorPreview) {
342
317
  return new VgaiSceneGameAdapter({
343
318
  id: world.id,
344
319
  sceneData: editorPreview.sceneData,
345
320
  editorPreview,
346
321
  componentRegistry,
347
- uiRegistry,
348
322
  });
349
323
  }
350
324
  return new VgaiSceneGameAdapter({
351
325
  id: world.id,
352
326
  scenePath: world.scene,
353
327
  componentRegistry,
354
- uiRegistry,
355
328
  });
356
329
  }
357
330
  // Unreachable in practice: `load.ts`'s `checkAdapterSceneEntryRules` requires
358
331
  // `scene` or `entry` for every 'default' adapter world at manifest-load time.
359
332
  throw new Error(
360
- `resolveWorldAdapter: world "${world.id}" is default-three with neither \`scene\` nor ` +
333
+ `resolveRootAdapter: world "${world.id}" is default-three with neither \`scene\` nor ` +
361
334
  '`entry` — this should have been rejected by manifest validation.',
362
335
  );
363
336
  }
@@ -389,7 +362,7 @@ export async function fetchScene2DFile(path: string): Promise<Scene2DFile> {
389
362
  const res = await fetch(resolveUrl(path));
390
363
  if (!res.ok) {
391
364
  throw new Error(
392
- `resolveWorldAdapter: failed to load world2d scene "${path}" (${res.status} ${res.statusText}).`,
365
+ `resolveRootAdapter: failed to load world2d scene "${path}" (${res.status} ${res.statusText}).`,
393
366
  );
394
367
  }
395
368
  const result = Scene2DSchema.safeParse(await res.json());
@@ -411,7 +384,7 @@ export async function fetchScene2DFile(path: string): Promise<Scene2DFile> {
411
384
  * pixi-game-adapter.ts`) is an established, exported config shape (`PixiSceneConfig.setup`,
412
385
  * re-exported from `@engine/world2d`'s barrel) structurally parallel to
413
386
  * `default-three`'s `GameSetupFn`; it simply has no production caller YET
414
- * (only hand-built test wiring, e.g. `create-runtime-worlds.test.ts`'s pixi
387
+ * (only hand-built test wiring, e.g. `create-runtime-roots.test.ts`'s pixi
415
388
  * world). So `entry` resolves via the SAME `/@fs/` dynamic-import route
416
389
  * `loadProjectScripts` uses for `default-three`'s entry branch — no new
417
390
  * mechanism invented, just a different exported shape (`setup(ctx:
@@ -422,7 +395,7 @@ export async function fetchScene2DFile(path: string): Promise<Scene2DFile> {
422
395
  * is shape-agnostic.
423
396
  */
424
397
  async function resolveDefaultPixiAdapter(
425
- world: ResolvedWorldEntry,
398
+ world: ResolvedAdapterRoot,
426
399
  projectRoot: string,
427
400
  ): Promise<GameAdapter> {
428
401
  if (world.entry !== undefined) {
@@ -436,8 +409,8 @@ async function resolveDefaultPixiAdapter(
436
409
  // instead of letting `undefined` silently reach `PixiSceneGameAdapter`.
437
410
  if (!setup) {
438
411
  throw new Error(
439
- `resolveWorldAdapter: world "${world.id}"'s entry "${world.entry}" exports \`adapter\` ` +
440
- `but no \`setup\` — default-pixi worlds only accept a \`setup(ctx: World2DContext)\` ` +
412
+ `resolveRootAdapter: world "${world.id}"'s entry "${world.entry}" exports \`adapter\` ` +
413
+ `but no \`setup\` — default-pixi roots only accept a \`setup(ctx: World2DContext)\` ` +
441
414
  `export${adapter ? ' (an `adapter` export is default-three/R3F-only, W1b)' : ''}.`,
442
415
  );
443
416
  }
@@ -448,12 +421,17 @@ async function resolveDefaultPixiAdapter(
448
421
  }
449
422
  if (world.scene !== undefined) {
450
423
  const sceneData = await fetchScene2DFile(world.scene);
451
- return new PixiSceneGameAdapter({ id: world.id, sceneData }) as unknown as GameAdapter;
424
+ const registry = await loadPixiComponentRegistryForProject(projectRoot);
425
+ return new PixiSceneGameAdapter({
426
+ id: world.id,
427
+ sceneData,
428
+ registry,
429
+ }) as unknown as GameAdapter;
452
430
  }
453
431
  // Unreachable in practice: `load.ts`'s `checkAdapterSceneEntryRules` requires
454
432
  // `scene` or `entry` for every 'default' adapter world at manifest-load time.
455
433
  throw new Error(
456
- `resolveWorldAdapter: world "${world.id}" is default-pixi with neither \`scene\` nor ` +
434
+ `resolveRootAdapter: world "${world.id}" is default-pixi with neither \`scene\` nor ` +
457
435
  '`entry` — this should have been rejected by manifest validation.',
458
436
  );
459
437
  }
@@ -476,13 +454,13 @@ async function resolveDefaultPixiAdapter(
476
454
  * shim is still just a module default-exporting a component).
477
455
  */
478
456
  function loadReactEntryComponent(
479
- world: ResolvedWorldEntry,
457
+ world: ResolvedAdapterRoot,
480
458
  mod: { default?: unknown },
481
- identityLabel: 'default-react' | 'ingest-react' = 'default-react',
459
+ identityLabel: 'react' | 'ingest-react' = 'react',
482
460
  ): ComponentType {
483
461
  if (typeof mod.default !== 'function') {
484
462
  throw new Error(
485
- `resolveWorldAdapter: world "${world.id}" (${identityLabel}) entry module does not ` +
463
+ `resolveRootAdapter: world "${world.id}" (${identityLabel}) entry module does not ` +
486
464
  `default-export a component — got ${typeof mod.default}. A react world's \`entry\` must ` +
487
465
  'default-export a React component (docs/REACT-WORLD-DESIGN.md §1.B).',
488
466
  );
@@ -565,7 +543,7 @@ async function loadPackagedReactWorldMountRuntime(): Promise<ReactWorldMountRunt
565
543
  typeof mod.EngineGameProvider !== 'function'
566
544
  ) {
567
545
  throw new Error(
568
- "resolveWorldAdapter: the packaged runtime's synthetic react-world-runtime module " +
546
+ "resolveRootAdapter: the packaged runtime's synthetic react-world-runtime module " +
569
547
  `("${REACT_WORLD_RUNTIME_PATH}") did not export createElement/createRoot/flushSync/` +
570
548
  'EngineGameProvider as functions — see vite-plugin-react-world-runtime.ts.',
571
549
  );
@@ -618,13 +596,13 @@ let cachedPackagedReactWorldMountRuntime: Promise<ReactWorldMountRuntime> | null
618
596
  * BYTE-IDENTICAL to the pre-C1 behavior. Memoized: `isPackagedRuntime()` is
619
597
  * itself session-memoized, and the packaged module load below is cached here
620
598
  * too, so this only ever performs at most one `/__editor/project` fetch and
621
- * one dynamic import per editor session, however many react worlds mount.
599
+ * one dynamic import per editor session, however many react roots mount.
622
600
  *
623
601
  * Exported: `design-time-layers.ts`'s OWN, separate react-world layer mount
624
602
  * (`mountReactLayer` — the edit-mode composite's live preview of a non-
625
603
  * focused react/pixi world, B1) needs this SAME resolution — it is a second
626
604
  * mount call site with the identical dual-instance exposure, not routed
627
- * through this file's `ReactWorldAdapter.mount()` at all.
605
+ * through this file's `ReactRootAdapter.mount()` at all.
628
606
  */
629
607
  export async function resolveReactWorldMountRuntime(): Promise<ReactWorldMountRuntime> {
630
608
  const packaged = await isPackagedRuntime();
@@ -643,20 +621,9 @@ export function resetReactWorldMountRuntimeCacheForTest(): void {
643
621
  }
644
622
 
645
623
  /**
646
- * SECOND FINDING (report prominently): `GameProvider` is not an engine or
647
- * editor export — by design (`docs/REACT-STATE-BRIDGE.md` §2: "colocated
648
- * with `mountUI`… rather than under `packages/engine/src/runtime/` — that
649
- * directory stays react-free"), it lives PER-PROJECT at
650
- * `src/ui/game-state.tsx` (`packages/editor/template/src/ui/
651
- * game-state.tsx`, copied verbatim into every scaffolded project by
652
- * `create-vgai-project`'s `scaffoldProject` — see that package's README:
653
- * "Copies the template into the target directory"). There is no engine-side
654
- * canonical `GameProvider` this resolver could import once and reuse for
655
- * every project — it has to load the SAME project's own copy the entry
656
- * module's OWN `../ui/game-state` import resolves to (a resolver-local
657
- * `createContext()` would be a SECOND, non-identical Context: `useGame()`
658
- * would throw "no Game in context" against it even though `<GameProvider>`
659
- * genuinely wrapped the tree).
624
+ * `GameProvider` is the canonical engine export from
625
+ * `@vgai/engine/react/game-state`. Hosts and game entries share that module,
626
+ * preserving a single React context identity.
660
627
  *
661
628
  * THIRD FINDING, and the actual engine-adjacent-tooling gap this slice
662
629
  * closes (T6.2 slice 3 — verified empirically, not assumed): a hand-built
@@ -677,38 +644,12 @@ export function resetReactWorldMountRuntimeCacheForTest(): void {
677
644
  * transform-time resolver a react-world entry's own static import goes
678
645
  * through, converging both on the identical final module.
679
646
  *
680
- * D-Z5 (`docs/WAVE3-ADAPTER-PLUMBING-DESIGN.md` §D-Z5) revises what happens
681
- * when `src/ui/game-state.tsx` is simply ABSENT: this function is now only
682
- * ever called after a caller has confirmed (via `probeGameStateFileExists`)
683
- * that the file genuinely exists, so the hard throw below is reserved for
684
- * the file EXISTING but not exporting `GameProvider` correctly — a real bug,
685
- * never swallowed. Absence itself degrades to the engine-published
686
- * `GameProvider` (`@engine/react/game-state`) instead, at both call sites
687
- * (`resolveDefaultReactAdapter` and `resolveIngestReactAdapter` below) — see
688
- * `resolveGameProviderForProject`, the shared probe-then-load-or-fallback
689
- * helper this revision introduces.
647
+ * `resolveGameProviderForProject` below supplies that canonical provider for
648
+ * both first-party and ingested React roots.
690
649
  */
691
- async function loadProjectGameProvider(
692
- projectRoot: string,
693
- ): Promise<ComponentType<{ game: Game; children?: unknown }>> {
694
- const modulePath = reactWorldProviderImportSpecifier(projectRoot);
695
- const mod = (await import(/* @vite-ignore */ modulePath)) as {
696
- GameProvider?: ComponentType<{ game: Game; children?: unknown }>;
697
- };
698
- if (typeof mod.GameProvider !== 'function') {
699
- throw new Error(
700
- 'resolveWorldAdapter: could not find `GameProvider` at ' +
701
- `"src/ui/game-state.tsx" under project root "${projectRoot}" — a react world's ` +
702
- "mount needs the project's own GameProvider (docs/REACT-STATE-BRIDGE.md §2) so " +
703
- "useGameState/useGame resolve against the SAME React context the entry's own code uses.",
704
- );
705
- }
706
- return mod.GameProvider;
707
- }
708
-
709
650
  /**
710
651
  * Resolve a `kind: 'react'` + `adapter: 'default'` world into a
711
- * `ReactWorldAdapter` (docs/REACT-WORLD-DESIGN.md §1.B, T6.2 slice 1 task
652
+ * `ReactRootAdapter` (docs/REACT-WORLD-DESIGN.md §1.B, T6.2 slice 1 task
712
653
  * list's "Mount" item) — the react sibling of `resolveDefaultThreeAdapter`/
713
654
  * `resolveDefaultPixiAdapter`. `entry` resolves via the SAME `/@fs/`
714
655
  * dynamic-import route `loadProjectScripts` uses (`resolveDefaultPixiAdapter`'s
@@ -724,16 +665,8 @@ async function loadProjectGameProvider(
724
665
  * honest gaps this depends on (the engine-merged `ReactWorldHost` not yet
725
666
  * carrying `game`; `GameProvider` being a per-project file, not an export).
726
667
  *
727
- * D-Z5: `GameProvider` here comes from `resolveGameProviderForProject`, which
728
- * now falls back to the engine-PUBLISHED `GameProvider`
729
- * (`@engine/react/game-state`) plus a loud once-per-world report when the
730
- * project ships no `src/ui/game-state.tsx` at all — this retires the former
731
- * hard throw for that case (a project whose file EXISTS but is broken still
732
- * throws for real; only genuine absence degrades). Safe by construction: the
733
- * template's own `src/ui/game-state.tsx` is now itself a thin re-export of
734
- * this same engine module (D-Z5 step 3), so a freshly scaffolded project's
735
- * "present" and "absent" paths converge on the identical `GameProvider`
736
- * either way.
668
+ * `GameProvider` here comes from the canonical engine React entry via
669
+ * `resolveGameProviderForProject`.
737
670
  *
738
671
  * `scene` on a react world is a NAMED error (JSX-as-truth, D4 —
739
672
  * docs/REACT-WORLD-DESIGN.md §1.A: "A react WORLD's truth is JSX source —
@@ -742,7 +675,7 @@ async function loadProjectGameProvider(
742
675
  * `checkAdapterSceneEntryRules` only requires ONE of `scene`/`entry` for a
743
676
  * `default`-adapter world (it does not forbid `scene` on a react world
744
677
  * specifically — see `load.ts`), so this resolver is where the D4 rule is
745
- * actually enforced for react worlds.
678
+ * actually enforced for react roots.
746
679
  */
747
680
 
748
681
  /**
@@ -756,14 +689,14 @@ async function loadProjectGameProvider(
756
689
  * `Game` beside an ingest world), while `resolveDefaultReactAdapter` wraps
757
690
  * it in `<GameProvider>` as it always has. Exported for that one caller.
758
691
  */
759
- export async function resolveReactWorldEntryComponent(
760
- world: ResolvedWorldEntry,
692
+ export async function resolveReactAdapterRootComponent(
693
+ world: ResolvedAdapterRoot,
761
694
  projectRoot: string,
762
695
  ): Promise<ComponentType> {
763
696
  if (world.scene !== undefined) {
764
697
  throw new Error(
765
- `resolveWorldAdapter: world "${world.id}" (default-react) declares \`scene\` — react ` +
766
- 'worlds have no scene-file format: JSX source is the truth for a react world (D4, ' +
698
+ `resolveRootAdapter: world "${world.id}" (default-react) declares \`scene\` — react ` +
699
+ 'roots have no scene-file format: JSX source is the truth for a react world (D4, ' +
767
700
  'docs/REACT-WORLD-DESIGN.md §1.A). Use `entry` (a .tsx module default-exporting a ' +
768
701
  'component) instead.',
769
702
  );
@@ -774,7 +707,7 @@ export async function resolveReactWorldEntryComponent(
774
707
  // and the check above already rejects `scene` for a react world — so a real
775
708
  // manifest can never reach this branch. Still fails loudly, not silently.
776
709
  throw new Error(
777
- `resolveWorldAdapter: world "${world.id}" is default-react with no \`entry\` — this should ` +
710
+ `resolveRootAdapter: world "${world.id}" is default-react with no \`entry\` — this should ` +
778
711
  'have been rejected by manifest validation.',
779
712
  );
780
713
  }
@@ -785,19 +718,19 @@ export async function resolveReactWorldEntryComponent(
785
718
  }
786
719
 
787
720
  async function resolveDefaultReactAdapter(
788
- world: ResolvedWorldEntry,
721
+ world: ResolvedAdapterRoot,
789
722
  projectRoot: string,
790
723
  ): Promise<GameAdapter> {
791
- const Entry = await resolveReactWorldEntryComponent(world, projectRoot);
792
- const GameProvider = await resolveGameProviderForProject(projectRoot, world.id, 'default-react');
724
+ const Entry = await resolveReactAdapterRootComponent(world, projectRoot);
725
+ const GameProvider = await resolveGameProviderForProject(projectRoot, world.id, 'react');
793
726
 
794
- const adapter: ReactWorldAdapter = {
727
+ const adapter: ReactRootAdapter = {
795
728
  id: world.id,
796
729
  async mount(host: ReactWorldHost): Promise<MountedReactGame> {
797
730
  const game = hostGame(host);
798
731
  if (!game) {
799
732
  throw new Error(
800
- `resolveWorldAdapter: world "${world.id}" (default-react) mount() received a ` +
733
+ `resolveRootAdapter: world "${world.id}" (default-react) mount() received a ` +
801
734
  'ReactWorldHost with no `game` — GameProvider needs a live Game at mount time and ' +
802
735
  "this host did not carry one (see adapter-resolver.ts's `hostGame` doc comment for " +
803
736
  'the engine-side gap this indicates).',
@@ -810,9 +743,14 @@ async function resolveDefaultReactAdapter(
810
743
  // entry component's own hooks. See that function's doc comment.
811
744
  const runtime = await resolveReactWorldMountRuntime();
812
745
  const root = runtime.createRoot(host.container);
813
- root.render(
814
- runtime.createElement(GameProvider, { game, children: runtime.createElement(Entry) }),
815
- );
746
+ // The play authoring adapter is installed immediately after mount()
747
+ // resolves. Commit the initial DOM before returning so its first
748
+ // hierarchy snapshot sees the OID tree instead of an empty container.
749
+ runtime.flushSync(() => {
750
+ root.render(
751
+ runtime.createElement(GameProvider, { game, children: runtime.createElement(Entry) }),
752
+ );
753
+ });
816
754
  return {
817
755
  kind: 'react',
818
756
  container: host.container,
@@ -833,7 +771,7 @@ async function resolveDefaultReactAdapter(
833
771
 
834
772
  /**
835
773
  * Resolve a `kind: 'react'` + `{ ingest: { strategy: 'deduped' } }` world into
836
- * a `ReactWorldAdapter` — the ingest sibling of `resolveDefaultReactAdapter`
774
+ * a `ReactRootAdapter` — the ingest sibling of `resolveDefaultReactAdapter`
837
775
  * immediately above. Mounts through the EXACT SAME T6.2 react host stack:
838
776
  * the same `/@fs/` `entry`-module dynamic import, the same default-export-
839
777
  * component validation (`loadReactEntryComponent`), the same per-project
@@ -873,183 +811,44 @@ async function resolveDefaultReactAdapter(
873
811
  * ever runs against a REAL manifest: `load.ts`'s `checkAdapterSceneEntryRules`
874
812
  * forbids `scene` on any `{ ingest }`-adapter world, kind-agnostic. This
875
813
  * function still guards it defensively (never trust a hand-built
876
- * `ResolvedWorldEntry` a unit test — or a future caller — might construct
814
+ * `ResolvedAdapterRoot` a unit test — or a future caller — might construct
877
815
  * past that check), with the same message shape `resolveDefaultReactAdapter`'s
878
816
  * own `scene` check uses.
879
817
  *
880
818
  * D-N8 ("no authoring in v1") and D-N4 item 1 (no `writeBackend` for an
881
- * ingest-react world): this function's return value is `{ id, mount }` only
882
- * — structurally identical in shape to `resolveDefaultReactAdapter`'s return
883
- * — there is no authoring/write-back hook anywhere on a `ReactWorldAdapter`
884
- * for EITHER react identity. `ReactWorldAuthoringAdapter`/`writeBackend`
885
- * wiring lives entirely outside this file, in `play-mode.ts`'s
886
- * `installMultiWorldAuthoring`, keyed on `world.kind === 'react'` — not on
887
- * adapter identity. That function is structurally unreachable for
888
- * `ingest-react` today: `resolveAllWorlds`'s `MULTI_WORLD_ALLOWED_IDENTITIES`
889
- * (below) deliberately excludes `'ingest-react'`, so an ingest-react world
890
- * can only ever be the LONE world of a single-world manifest — and
891
- * `play-mode.ts`'s single-world play branch calls `installPlayAuthoring`
892
- * (wraps `getActiveAuthoring(store)`, the first-party
893
- * `VgaiSceneAuthoringAdapter`) instead, for every world kind, never
894
- * `installMultiWorldAuthoring`. So "no write-capable authoring surface" holds
895
- * for `ingest-react` today by construction, not by an extra guard this
896
- * function adds — recorded honestly here rather than claimed as this
897
- * function's own achievement; see the N1 report for the residual this
898
- * implies for N2 if react ingestion ever needs to join a multi-world
899
- * manifest.
819
+ * ingest-react world): this function's return value is `{ id, mount }` only.
820
+ * Composition resolution is cardinality-neutral, so play mode enforces the
821
+ * write boundary explicitly by installing a read-only BoundaryAuthoringAdapter
822
+ * whenever the declaration identity is `ingest-react`. Universal hierarchy
823
+ * membership does not imply write authority over vendored source.
900
824
  *
901
- * D-Y3 (wave 2, docs/EXTERNAL-REACT-AND-MODULE-DESIGN.md §1) introduced ONE
902
- * piece of the above: `loadProjectGameProvider`'s hard throw when
903
- * `src/ui/game-state.tsx` is absent was right for `default-react` (a
904
- * vgai-native world legitimately depends on the bridge) but wrong for
905
- * `ingest-react` — a foreign tree ingested as-is has no reason to ship that
906
- * file at all (it calls no vgai hooks unless its own shim explicitly wants
907
- * them). `probeGameStateFileExists` checks (via a plain `fetch()` of the
908
- * exact `/@fs/` path `loadProjectGameProvider` would import, mirroring
909
- * `fetchScene2DFile`'s existing fetch-based existence check elsewhere in
910
- * this file) whether the file is even THERE before ever calling the real
911
- * loader.
912
- *
913
- * D-Z5 (`docs/WAVE3-ADAPTER-PLUMBING-DESIGN.md` §D-Z5) revises what D-Y3's
914
- * "absent" branch DOES, for both react identities: absence now wraps with
915
- * the engine-PUBLISHED `GameProvider` (`@engine/react/game-state`) instead
916
- * of mounting unwrapped — safe because an unmodified foreign entry (or a
917
- * project with no bridge file of its own) imports no vgai hooks bound to a
918
- * DIFFERENT context, so there is no context-identity split to cause (see
919
- * `packages/engine/src/react/game-state.tsx`'s module doc comment, and
920
- * `game-state-context-split.test.tsx`'s red proof of what WOULD happen if a
921
- * project's own copy were wrapped by the wrong provider). Presence still
922
- * runs the real loader (so a game-state.tsx that EXISTS but is broken still
923
- * throws its own real error loudly — the probe never swallows that, it only
924
- * distinguishes "not there" from "there"). See `resolveGameProviderForProject`
925
- * below, the shared probe-then-load-or-fallback helper both `default-react`
926
- * and `ingest-react` now call.
927
- */
928
- async function probeGameStateFileExists(projectRoot: string): Promise<boolean> {
929
- const path = fsImportPath(projectRoot, 'src/ui/game-state.tsx');
930
- const relative = resolveUrl(path);
931
- // `fetch()` needs an absolute URL when there is no implicit browser
932
- // document base to resolve a leading-slash path against (a real dev-server
933
- // session always has one; a headless Node/vitest caller does not) — build
934
- // one explicitly rather than relying on an environment-specific implicit
935
- // base, so this probe behaves identically (and is mockable) in both.
936
- const origin =
937
- typeof window !== 'undefined' && window.location ? window.location.origin : 'http://localhost';
938
- try {
939
- const res = await fetch(new URL(relative, origin).toString());
940
- // `res.ok` alone is NOT "the file exists": Vite's SPA fallback answers
941
- // any unknown GET — including a missing `/@fs/…` path — with index.html
942
- // and a 200, so on a real dev server the absence branch below was
943
- // unreachable and a project genuinely shipping no `src/ui/game-state.tsx`
944
- // crashed later inside `loadProjectGameProvider`'s synthetic re-export
945
- // instead of degrading to the engine provider (found by the first such
946
- // project: alien-stories' proto). A real hit comes back as transformed
947
- // JS; only the fallback is `text/html`. (`headers` is optional-chained
948
- // for partial `Response` stubs in tests.)
949
- const contentType = res.headers?.get?.('content-type') ?? '';
950
- return res.ok && !contentType.includes('text/html');
951
- } catch {
952
- // Fetch itself failing (network hiccup, no dev server route, …) is
953
- // treated the same as "absent" — the honest fallback here is the
954
- // engine-provider-plus-loud-report degrade (`resolveGameProviderForProject`),
955
- // never a silent crash on an ambiguous probe failure.
956
- return false;
957
- }
958
- }
959
-
960
- /** Dedup key set for {@link reportEngineProviderFallbackOnce} — one
961
- * structured warning per (identity, world id) per session, mirroring
962
- * `runtime/game.ts`'s `reportGateShortfallOnce` "once per (kind, id)" idiom
963
- * (`docs/CAPABILITY-TIERS.md` §d's reporting precedent) rather than
964
- * re-logging on every mount of a session that re-enters the same world. */
965
- const REPORTED_ENGINE_PROVIDER_FALLBACK = new Set<string>();
966
-
967
- /**
968
- * Greppable prefix for the D-Z5 "wrapped with the engine-published
969
- * GameProvider" report — the renamed successor to wave-2's
970
- * `INGEST_REACT_NO_PROVIDER_PREFIX` ("ingest-react-no-provider"), retired
971
- * because that name described the OLD "mounted unwrapped" behavior; this
972
- * fallback always wraps now, so the honest name says what actually happens.
973
- * Shared by both `default-react` and `ingest-react` (D-Z5 extends the D-Y3
974
- * ingest-only degrade to `default-react` too, since the template re-export
975
- * — D-Z5 step 3 — landed in the same slice).
825
+ * Ingested and first-party React roots use the same canonical engine
826
+ * provider. Unmodified foreign entries need no project-local bridge file.
976
827
  */
977
- export const ENGINE_GAME_PROVIDER_FALLBACK_PREFIX = 'engine-game-provider-fallback';
978
-
979
- /** Plain `console.warn` (not `editorConsole`) — same reasoning + idiom as
980
- * `runtime/game.ts`'s `reportGateShortfallOnce`: this resolver is imported
981
- * by headless Node/vitest suites with no `window`, and `editor-console.ts`
982
- * touches `window` unconditionally at its module top level, so a
983
- * first-class report here stays environment-neutral rather than forcing
984
- * every caller (test or real) to stub `window` just to see it. */
985
- function reportEngineProviderFallbackOnce(
986
- identity: 'default-react' | 'ingest-react',
987
- worldId: string,
988
- ): void {
989
- const key = `${identity}:${worldId}`;
990
- if (REPORTED_ENGINE_PROVIDER_FALLBACK.has(key)) return;
991
- REPORTED_ENGINE_PROVIDER_FALLBACK.add(key);
992
- const locationText = identity === 'ingest-react' ? 'beside the manifest' : 'in this project';
993
- // biome-ignore lint/suspicious/noConsole: structured, greppable — mirrors runtime/game.ts's reportGateShortfallOnce
994
- console.warn(
995
- `${ENGINE_GAME_PROVIDER_FALLBACK_PREFIX} world "${worldId}" (${identity}): no ` +
996
- `src/ui/game-state.tsx ${locationText} — wrapping the entry with the engine-published ` +
997
- 'GameProvider (@engine/react/game-state) instead. useGameState/useGame work against the ' +
998
- 'engine bridge; ship your own src/ui/game-state.tsx to override with a custom bridge ' +
999
- '(docs/WAVE3-ADAPTER-PLUMBING-DESIGN.md D-Z5, docs/REACT-STATE-BRIDGE.md §2/addendum). ' +
1000
- `${ENGINE_GAME_PROVIDER_FALLBACK_PREFIX} ${JSON.stringify({ worldId, identity })}`,
1001
- );
1002
- }
1003
-
1004
828
  /**
1005
- * Shared probe-then-load-or-fallback helper (D-Z5) for both react
1006
- * identities: if the project ships `src/ui/game-state.tsx`, load it for real
1007
- * (`loadProjectGameProvider` — a file that exists but is broken still throws
1008
- * its own real error, never swallowed); if it's genuinely absent, fall back
1009
- * to the engine-published `GameProvider` and report once per (identity,
1010
- * world id). See the doc comment on `probeGameStateFileExists` above for the
1011
- * full D-Y3 -> D-Z5 history of this behavior.
1012
- *
1013
- * Exported (B1): `design-time-layers.ts` needs this SAME per-project
1014
- * resolution (a project's own `src/ui/game-state.tsx`, when present, has its
1015
- * own `createContext()` — wrapping it with the wrong `GameProvider` splits
1016
- * context and `useGame()` throws even though a provider IS in the tree, see
1017
- * `@engine/react/game-state`'s module doc comment) for its design-time react
1018
- * layer mount, rather than re-deriving project-vs-engine provider resolution.
829
+ * Resolve the canonical engine provider for both React adapter identities.
830
+ * Exported because design-time React layers use the identical provider.
1019
831
  */
1020
832
  export async function resolveGameProviderForProject(
1021
- projectRoot: string,
1022
- worldId: string,
1023
- identity: 'default-react' | 'ingest-react',
833
+ _projectRoot: string,
834
+ _worldId: string,
835
+ _identity: 'react' | 'ingest-react',
1024
836
  ): Promise<ComponentType<{ game: Game; children?: unknown }>> {
1025
- const hasGameProvider = await probeGameStateFileExists(projectRoot);
1026
- if (hasGameProvider) {
1027
- return loadProjectGameProvider(projectRoot);
1028
- }
1029
- reportEngineProviderFallbackOnce(identity, worldId);
1030
- // C1: the engine-published `GameProvider` fallback itself must come from
1031
- // the SAME react instance the mount's `createRoot`/the entry component's
1032
- // hooks use — under the packaged runtime that is the PROJECT's react, not
1033
- // this module's own static `EngineGameProvider` import (the editor's
1034
- // separately-bundled copy). `resolveReactWorldMountRuntime` already
1035
- // encapsulates the dev/Electron-vs-packaged gate (byte-identical to the
1036
- // old `EngineGameProvider as unknown as …` cast when not packaged); reuse
1037
- // it here instead of re-deriving the gate.
1038
837
  const runtime = await resolveReactWorldMountRuntime();
1039
838
  return runtime.engineGameProvider;
1040
839
  }
1041
840
 
1042
841
  async function resolveIngestReactAdapter(
1043
- world: ResolvedWorldEntry,
842
+ world: ResolvedAdapterRoot,
1044
843
  projectRoot: string,
1045
844
  ): Promise<GameAdapter> {
1046
845
  const { adapter } = world;
1047
846
  if (adapter.type !== 'ingest') {
1048
- throw new Error(`resolveWorldAdapter: world "${world.id}" is not an { ingest } world.`);
847
+ throw new Error(`resolveRootAdapter: world "${world.id}" is not an { ingest } world.`);
1049
848
  }
1050
849
  if (adapter.strategy !== 'deduped') {
1051
850
  throw new Error(
1052
- `resolveWorldAdapter: world "${world.id}" (ingest-react) declares strategy ` +
851
+ `resolveRootAdapter: world "${world.id}" (ingest-react) declares strategy ` +
1053
852
  `"${adapter.strategy}" — react ingestion v1 supports ONLY the 'deduped' rung (loud ` +
1054
853
  'degrade, no silent floor: docs/REACT-INGEST-LANDING-DESIGN.md D-N3). An iframe-' +
1055
854
  'reachable/opaque-embed floor is explicitly out of scope for v1.',
@@ -1057,15 +856,15 @@ async function resolveIngestReactAdapter(
1057
856
  }
1058
857
  if (world.scene !== undefined) {
1059
858
  throw new Error(
1060
- `resolveWorldAdapter: world "${world.id}" (ingest-react) declares \`scene\` — react ` +
1061
- 'worlds have no scene-file format: JSX source is the truth for a react world (D4, ' +
859
+ `resolveRootAdapter: world "${world.id}" (ingest-react) declares \`scene\` — react ` +
860
+ 'roots have no scene-file format: JSX source is the truth for a react world (D4, ' +
1062
861
  'docs/REACT-WORLD-DESIGN.md §1.A), and an ingested react world doubly so — the vendored ' +
1063
862
  'game is never modified. Use `entry` (the host-shim module, D-N2) instead.',
1064
863
  );
1065
864
  }
1066
865
  if (world.entry === undefined) {
1067
866
  throw new Error(
1068
- `resolveWorldAdapter: world "${world.id}" (ingest-react) has no \`entry\` — an ingested ` +
867
+ `resolveRootAdapter: world "${world.id}" (ingest-react) has no \`entry\` — an ingested ` +
1069
868
  "react world's `entry` MUST point at a host-shim module that imports the vendored " +
1070
869
  "game's own component and default-exports it (D-N2, docs/REACT-INGEST-LANDING-DESIGN.md).",
1071
870
  );
@@ -1079,13 +878,13 @@ async function resolveIngestReactAdapter(
1079
878
  // `resolveGameProviderForProject`'s doc comment for the full history.
1080
879
  const GameProvider = await resolveGameProviderForProject(projectRoot, world.id, 'ingest-react');
1081
880
 
1082
- const reactAdapter: ReactWorldAdapter = {
881
+ const reactAdapter: ReactRootAdapter = {
1083
882
  id: world.id,
1084
883
  async mount(host: ReactWorldHost): Promise<MountedReactGame> {
1085
884
  const game = hostGame(host);
1086
885
  if (!game) {
1087
886
  throw new Error(
1088
- `resolveWorldAdapter: world "${world.id}" (ingest-react) mount() received a ` +
887
+ `resolveRootAdapter: world "${world.id}" (ingest-react) mount() received a ` +
1089
888
  'ReactWorldHost with no `game` — GameProvider needs a live Game at mount time and ' +
1090
889
  "this host did not carry one (see adapter-resolver.ts's `hostGame` doc comment for " +
1091
890
  'the engine-side gap this indicates).',
@@ -1159,8 +958,8 @@ async function fetchProjectStaticText(entryHtmlPath: string): Promise<string> {
1159
958
  * `threeModuleUrl` field: "no consumer exists").
1160
959
  */
1161
960
  function buildIframeReachableIngestDescriptor(
1162
- world: ResolvedWorldEntry,
1163
- adapter: Extract<ResolvedWorldEntry['adapter'], { type: 'ingest' }>,
961
+ world: ResolvedAdapterRoot,
962
+ adapter: Extract<ResolvedAdapterRoot['adapter'], { type: 'ingest' }>,
1164
963
  base: Pick<
1165
964
  IngestGame,
1166
965
  'id' | 'name' | 'description' | 'assets' | 'domStubs' | 'captureTimeoutMs'
@@ -1201,8 +1000,8 @@ function buildIframeReachableIngestDescriptor(
1201
1000
  * actually mounted (`enterIngestModeWithGame` awaits `game.embedHtml()`).
1202
1001
  */
1203
1002
  function buildOpaqueEmbedIngestDescriptor(
1204
- world: ResolvedWorldEntry,
1205
- adapter: Extract<ResolvedWorldEntry['adapter'], { type: 'ingest' }>,
1003
+ world: ResolvedAdapterRoot,
1004
+ adapter: Extract<ResolvedAdapterRoot['adapter'], { type: 'ingest' }>,
1206
1005
  base: Pick<
1207
1006
  IngestGame,
1208
1007
  'id' | 'name' | 'description' | 'assets' | 'domStubs' | 'captureTimeoutMs'
@@ -1248,7 +1047,7 @@ function buildOpaqueEmbedIngestDescriptor(
1248
1047
  * SHARED-instance-mounted string has no wired mechanism this wave.
1249
1048
  */
1250
1049
  export function resolveIngestDescriptor(
1251
- world: ResolvedWorldEntry,
1050
+ world: ResolvedAdapterRoot,
1252
1051
  projectRoot: string,
1253
1052
  ): IngestGame {
1254
1053
  const { adapter } = world;
@@ -1277,7 +1076,7 @@ export function resolveIngestDescriptor(
1277
1076
  if (!world.entry) {
1278
1077
  throw new Error(
1279
1078
  `resolveIngestDescriptor: world "${world.id}" (strategy "${adapter.strategy}") has no ` +
1280
- "`entry` — shared/deduped ingest worlds need the game module's path there.",
1079
+ "`entry` — shared/deduped ingest roots need the game module's path there.",
1281
1080
  );
1282
1081
  }
1283
1082
  const modulePath = fsImportPath(projectRoot, world.entry);
@@ -1339,8 +1138,8 @@ export function resolveIngestDescriptor(
1339
1138
  * `buildIframeReachableGame2D` already uses for its in-tree-fixture sibling.
1340
1139
  */
1341
1140
  function buildIframeReachableDescriptor2D(
1342
- world: ResolvedWorldEntry,
1343
- adapter: Extract<ResolvedWorldEntry['adapter'], { type: 'ingest' }>,
1141
+ world: ResolvedAdapterRoot,
1142
+ adapter: Extract<ResolvedAdapterRoot['adapter'], { type: 'ingest' }>,
1344
1143
  base: Pick<IngestGame2D, 'id' | 'name' | 'description' | 'captureTimeoutMs'>,
1345
1144
  ): IngestGame2D {
1346
1145
  if (!adapter.bundleUrl) {
@@ -1367,7 +1166,7 @@ function buildIframeReachableDescriptor2D(
1367
1166
  }
1368
1167
 
1369
1168
  export function resolveIngest2DDescriptor(
1370
- world: ResolvedWorldEntry,
1169
+ world: ResolvedAdapterRoot,
1371
1170
  projectRoot: string,
1372
1171
  ): IngestGame2D {
1373
1172
  const { adapter } = world;
@@ -1388,7 +1187,7 @@ export function resolveIngest2DDescriptor(
1388
1187
  if (!world.entry) {
1389
1188
  throw new Error(
1390
1189
  `resolveIngest2DDescriptor: world "${world.id}" (strategy "${adapter.strategy}") has no ` +
1391
- "`entry` — shared/deduped ingest-pixi worlds need the game module's path there.",
1190
+ "`entry` — shared/deduped ingest-pixi roots need the game module's path there.",
1392
1191
  );
1393
1192
  }
1394
1193
  const modulePath = fsImportPath(projectRoot, world.entry);
@@ -1432,7 +1231,7 @@ export function resolveIngest2DDescriptor(
1432
1231
  * and a container — exactly `enterIngestModeFromManifestWorld`'s three-side
1433
1232
  * counterpart.
1434
1233
  */
1435
- function resolveIngestPixiAdapter(world: ResolvedWorldEntry, projectRoot: string): GameAdapter {
1234
+ function resolveIngestPixiAdapter(world: ResolvedAdapterRoot, projectRoot: string): GameAdapter {
1436
1235
  const descriptor = resolveIngest2DDescriptor(world, projectRoot);
1437
1236
  return {
1438
1237
  id: world.id,
@@ -1440,7 +1239,7 @@ function resolveIngestPixiAdapter(world: ResolvedWorldEntry, projectRoot: string
1440
1239
  descriptor,
1441
1240
  async mount() {
1442
1241
  throw new Error(
1443
- `resolveWorldAdapter: world "${world.id}" (ingest-pixi) cannot mount through the ` +
1242
+ `resolveRootAdapter: world "${world.id}" (ingest-pixi) cannot mount through the ` +
1444
1243
  'generic GameAdapter/HostContext contract — the world2d ingest machinery ' +
1445
1244
  '(mountIngestGame2D + World2DAuthoringAdapter) requires an EditorStore and a ' +
1446
1245
  'dedicated game-container element that HostContext does not provide (mirrors the ' +
@@ -1482,7 +1281,7 @@ function resolveIngestPixiAdapter(world: ResolvedWorldEntry, projectRoot: string
1482
1281
  * opens, plays, edits" — docs/CLI-ON-FOLDER-DESIGN.md §4) for the
1483
1282
  * `shared`/`deduped` strategies this function builds.
1484
1283
  */
1485
- function resolveIngestThreeAdapter(world: ResolvedWorldEntry, projectRoot: string): GameAdapter {
1284
+ function resolveIngestThreeAdapter(world: ResolvedAdapterRoot, projectRoot: string): GameAdapter {
1486
1285
  const descriptor = resolveIngestDescriptor(world, projectRoot);
1487
1286
  return {
1488
1287
  id: world.id,
@@ -1490,7 +1289,7 @@ function resolveIngestThreeAdapter(world: ResolvedWorldEntry, projectRoot: strin
1490
1289
  descriptor,
1491
1290
  async mount() {
1492
1291
  throw new Error(
1493
- `resolveWorldAdapter: world "${world.id}" (ingest-three) cannot mount through the ` +
1292
+ `resolveRootAdapter: world "${world.id}" (ingest-three) cannot mount through the ` +
1494
1293
  'generic GameAdapter/HostContext contract — the existing ingest machinery ' +
1495
1294
  '(mountIngestGame/mountIngestGameIframeReachable) requires an EditorStore and a ' +
1496
1295
  'dedicated game-container element that HostContext does not provide (T3.3 slice 2 ' +
@@ -1519,10 +1318,10 @@ function resolveIngestThreeAdapter(world: ResolvedWorldEntry, projectRoot: strin
1519
1318
  * canonicalization for the storage backends, not a trust-boundary check).
1520
1319
  * Returns the normalized (slash-joined, no leading/trailing slash) path.
1521
1320
  */
1522
- export function guardModulePath(world: ResolvedWorldEntry, moduleId: string): string {
1321
+ export function guardModulePath(world: ResolvedAdapterRoot, moduleId: string): string {
1523
1322
  const fail = (reason: string): never => {
1524
1323
  throw new Error(
1525
- `resolveWorldAdapter: world "${world.id}" declares { module: "${moduleId}" } — ${reason} ` +
1324
+ `resolveRootAdapter: world "${world.id}" declares { module: "${moduleId}" } — ${reason} ` +
1526
1325
  '(docs/ADAPTER-AUTHORING-DESIGN.md §1.A: the module path must be game-folder-relative, ' +
1527
1326
  'normalized, with no `..` escape — the same rule the save-file endpoint enforces).',
1528
1327
  );
@@ -1547,13 +1346,13 @@ export function guardModulePath(world: ResolvedWorldEntry, moduleId: string): st
1547
1346
  /** Runtime shape every `GameAdapter` must satisfy — the module branch cannot
1548
1347
  * trust TypeScript alone (the imported value is `unknown` until checked). */
1549
1348
  function guardModuleShape(
1550
- world: ResolvedWorldEntry,
1349
+ world: ResolvedAdapterRoot,
1551
1350
  moduleId: string,
1552
1351
  candidate: unknown,
1553
1352
  ): asserts candidate is GameAdapter {
1554
1353
  const fail = (reason: string): never => {
1555
1354
  throw new Error(
1556
- `resolveWorldAdapter: world "${world.id}"'s custom adapter module "${moduleId}" ${reason} ` +
1355
+ `resolveRootAdapter: world "${world.id}"'s custom adapter module "${moduleId}" ${reason} ` +
1557
1356
  '— a { module } adapter must default-export an object shaped like `GameAdapter`: ' +
1558
1357
  '`{ id: string, mount(host): Promise<MountedGame> }` (docs/ADAPTER-AUTHORING-DESIGN.md §1.A).',
1559
1358
  );
@@ -1593,7 +1392,7 @@ function guardModuleShape(
1593
1392
  * `window` is expected to exist — the module-adapter mount path only runs
1594
1393
  * inside the editor's dev-server browser session, per the note above).
1595
1394
  */
1596
- function withAchievedTierCheck(world: ResolvedWorldEntry, adapter: GameAdapter): GameAdapter {
1395
+ function withAchievedTierCheck(world: ResolvedAdapterRoot, adapter: GameAdapter): GameAdapter {
1597
1396
  return {
1598
1397
  id: adapter.id,
1599
1398
  async mount(host) {
@@ -1625,7 +1424,7 @@ function withAchievedTierCheck(world: ResolvedWorldEntry, adapter: GameAdapter):
1625
1424
  * `mount` is wrapped with the achieved-tier check (§1.C, `withAchievedTierCheck`).
1626
1425
  */
1627
1426
  export async function resolveModuleAdapter(
1628
- world: ResolvedWorldEntry,
1427
+ world: ResolvedAdapterRoot,
1629
1428
  projectRoot: string,
1630
1429
  ): Promise<GameAdapter> {
1631
1430
  const { adapter } = world;
@@ -1652,19 +1451,19 @@ export async function resolveModuleAdapter(
1652
1451
  * adapter is not yet mounted; call `.mount(host)` to run it (see each
1653
1452
  * identity's doc comment above for what that does or doesn't support today).
1654
1453
  */
1655
- export async function resolveWorldAdapter(
1656
- world: ResolvedWorldEntry,
1454
+ export async function resolveRootAdapter(
1455
+ world: ResolvedAdapterRoot,
1657
1456
  projectRoot: string,
1658
- opts?: ResolveWorldAdapterOptions,
1457
+ opts?: ResolveRootAdapterOptions,
1659
1458
  ): Promise<GameAdapter> {
1660
1459
  const { adapter } = world;
1661
1460
 
1662
1461
  switch (adapter.identity) {
1663
- case 'default-three':
1462
+ case 'threejs':
1664
1463
  return resolveDefaultThreeAdapter(world, projectRoot, opts);
1665
- case 'default-pixi':
1464
+ case 'pixijs':
1666
1465
  return resolveDefaultPixiAdapter(world, projectRoot);
1667
- case 'default-react':
1466
+ case 'react':
1668
1467
  return resolveDefaultReactAdapter(world, projectRoot);
1669
1468
  case 'module':
1670
1469
  return resolveModuleAdapter(world, projectRoot);
@@ -1677,111 +1476,40 @@ export async function resolveWorldAdapter(
1677
1476
  default: {
1678
1477
  const exhaustive: never = adapter;
1679
1478
  throw new Error(
1680
- `resolveWorldAdapter: unhandled adapter identity ${JSON.stringify(exhaustive)}`,
1479
+ `resolveRootAdapter: unhandled adapter identity ${JSON.stringify(exhaustive)}`,
1681
1480
  );
1682
1481
  }
1683
1482
  }
1684
1483
  }
1685
1484
 
1686
1485
  // ---------------------------------------------------------------------------
1687
- // Multi-world entry point (T6.1 slice 2 — docs/MULTI-WORLD-DESIGN.md §3 slice 2)
1486
+ // Composition entry points
1688
1487
  // ---------------------------------------------------------------------------
1689
1488
 
1690
- /**
1691
- * Adapter identities a manifest with MORE THAN ONE world may mix in this
1692
- * slice (docs/MULTI-WORLD-DESIGN.md §3 slice 4's scope rule, extended by
1693
- * docs/REACT-WORLD-DESIGN.md §3 slice 1/3 to admit `default-react`): only
1694
- * `'default'`-adapter threejs/pixijs/react worlds. `ingest-three`/
1695
- * `ingest-pixi` and `{ module }` still join a multi-world manifest in a
1696
- * LATER slice (slice 4's ingest-pixi + retirement work, T3.4's module
1697
- * adapters) — a worlds>1 manifest naming any of them today gets a named
1698
- * scope-rule error instead of a silent/confusing failure deep inside
1699
- * `createGameRuntime`'s worlds path. `default-react` is admitted here
1700
- * because `resolveAllWorlds`/`createWorldsGameRuntime` is exactly the host
1701
- * path a react world mounts through (docs/REACT-WORLD-DESIGN.md §1.B: a
1702
- * react world's DOM-root layer is "one stacking model, no special case" —
1703
- * `packages/engine/test/create-runtime-worlds.test.ts` and `-react-world
1704
- * .test.ts` already prove N worlds, including react, mounting through this
1705
- * exact host) — the AC fixture in that design (§2: 1 threejs + 2 react
1706
- * worlds) is a worlds>1 manifest by construction, so react could not stay
1707
- * scope-barred here without contradicting the design it is implementing.
1708
- *
1709
- * `ingest-react` (Track N, N1) stays OUT of this set on purpose — it is an
1710
- * `{ ingest }`-adapter identity like `ingest-three`/`ingest-pixi`, both of
1711
- * which are already excluded here, and docs/REACT-INGEST-LANDING-DESIGN.md
1712
- * ships single-world manifests only ("Track N ships single-world manifests
1713
- * and does not touch this", per that design's §2 seam table). This exclusion
1714
- * also does load-bearing work for D-N4/D-N8 (no authoring surface for an
1715
- * ingested react world): `installMultiWorldAuthoring` (`play-mode.ts`) is
1716
- * the ONLY place a `ReactWorldAuthoringAdapter`/`writeBackend` gets
1717
- * constructed, and it is only reachable via THIS multi-world path — keeping
1718
- * `ingest-react` out of it keeps that constructor permanently unreachable
1719
- * for a vendored game, not just unreachable today.
1720
- */
1721
- const MULTI_WORLD_ALLOWED_IDENTITIES: ReadonlySet<string> = new Set([
1722
- 'default-three',
1723
- 'default-pixi',
1724
- 'default-react',
1725
- ]);
1726
-
1727
- function throwMultiWorldScopeViolation(world: ResolvedWorldEntry): never {
1728
- throw new Error(
1729
- `resolveAllWorlds: world "${world.id}" resolved to adapter identity ` +
1730
- `"${world.adapter.identity}" — a manifest with more than one world may only mix ` +
1731
- "'default'-adapter threejs/pixijs/react worlds in this slice (docs/MULTI-WORLD-DESIGN.md " +
1732
- '§3 slice 4 reserves ingest/module worlds out of THIS path permanently, not just for a ' +
1733
- 'later slice: a composite manifest (one { ingest } world + siblings) mounts via ' +
1734
- "`ingest-mode.ts`'s own composite route instead (docs/WAVE5-MULTIWORLD-INGEST-DESIGN.md " +
1735
- 'D-V2..D-V7), never resolveAllWorlds/installMultiWorldAuthoring — the play path stays out ' +
1736
- 'of scope for ingest worlds by design (docs/REACT-WORLD-DESIGN.md §3 admits default-react ' +
1737
- 'here).',
1738
- );
1739
- }
1740
-
1741
- /**
1742
- * Resolve EVERY world in a manifest into slice-1 `WorldMountSpec`s
1743
- * (docs/MULTI-WORLD-DESIGN.md §1.A's host-facing shape), in the manifest's
1744
- * already-z-sorted `worlds` order (`@engine/manifest/load`'s
1745
- * `loadGameManifest` sorts by `zOrder` before returning — this function does
1746
- * not re-sort). No `editorPreview` threading here (v1 scope decision, T6.1
1747
- * slice 2's report): `default-pixi` has none by design (§1.D), and a
1748
- * multi-world manifest's live editor document corresponds to no single one
1749
- * of N worlds today (the editor is still a single-scene store) — EVERY
1750
- * world in the worlds path mounts from its own on-disk `scene`/`entry`,
1751
- * matching how `default-pixi` already behaves. Single-world manifests never
1752
- * call this function (see `play-mode.ts`'s routing) — resolveWorldAdapter's
1753
- * own editorPreview threading there is completely unaffected.
1754
- *
1755
- * Called only for manifests with more than one world (the play path's own
1756
- * routing decision) but written to work for any `worlds.length >= 1` so a
1757
- * caller (or a unit test) can exercise it directly.
1758
- */
1759
-
1760
1489
  /**
1761
1490
  * Shared per-world resolution loop behind {@link resolveAllWorlds} and
1762
- * {@link resolveAllWorldEntries} (E4.R2 reopen fix, split out so the two
1763
- * never re-derive the multi-world scope check or the `resolveWorldAdapter`
1764
- * fan-out independently): enforces the v1 scope rule (module/ingest worlds
1765
- * banned from a >1-world manifest — `MULTI_WORLD_ALLOWED_IDENTITIES`), then
1766
- * resolves every world's adapter, in manifest order.
1491
+ * {@link resolveAllWorldEntries}. Every world is resolved independently in
1492
+ * manifest order. An optional editor preview is scoped to the focused Three
1493
+ * world only. D19 deliberately has no cardinality gate: an adapter's capability
1494
+ * or mount failure belongs to that world, not to the number of siblings beside it.
1767
1495
  */
1768
- async function resolveAllWorldAdapters(
1496
+ async function resolveAllRootAdapters(
1769
1497
  manifest: ResolvedGameManifest,
1770
1498
  projectRoot: string,
1771
- ): Promise<{ world: ResolvedWorldEntry; adapter: GameAdapter }[]> {
1772
- const { worlds } = manifest;
1773
-
1774
- if (worlds.length > 1) {
1775
- for (const world of worlds) {
1776
- if (!MULTI_WORLD_ALLOWED_IDENTITIES.has(world.adapter.identity)) {
1777
- throwMultiWorldScopeViolation(world);
1778
- }
1779
- }
1780
- }
1781
-
1782
- const resolved: { world: ResolvedWorldEntry; adapter: GameAdapter }[] = [];
1783
- for (const world of worlds) {
1784
- resolved.push({ world, adapter: await resolveWorldAdapter(world, projectRoot) });
1499
+ opts?: ResolveAllWorldEntriesOptions,
1500
+ ): Promise<{ world: ResolvedAdapterRoot; adapter: GameAdapter }[]> {
1501
+ const { roots } = manifest;
1502
+
1503
+ const resolved: { world: ResolvedAdapterRoot; adapter: GameAdapter }[] = [];
1504
+ for (const world of roots) {
1505
+ const worldOptions =
1506
+ opts?.editorPreview && world.id === opts.editorPreviewWorldId
1507
+ ? { editorPreview: opts.editorPreview }
1508
+ : undefined;
1509
+ resolved.push({
1510
+ world,
1511
+ adapter: await resolveRootAdapter(world, projectRoot, worldOptions),
1512
+ });
1785
1513
  }
1786
1514
  return resolved;
1787
1515
  }
@@ -1790,34 +1518,42 @@ export async function resolveAllWorlds(
1790
1518
  manifest: ResolvedGameManifest,
1791
1519
  projectRoot: string,
1792
1520
  ): Promise<WorldMountSpec[]> {
1793
- const resolved = await resolveAllWorldAdapters(manifest, projectRoot);
1521
+ const resolved = await resolveAllRootAdapters(manifest, projectRoot);
1794
1522
 
1795
1523
  const specs: WorldMountSpec[] = [];
1796
1524
  for (const { world, adapter } of resolved) {
1797
1525
  const base = { id: world.id, zOrder: world.zOrder, pausable: world.pausable, loop: world.loop };
1798
- if (world.kind === 'pixijs') {
1526
+ if (world.surface === 'pixijs') {
1799
1527
  specs.push({ ...base, kind: 'pixijs', adapter: adapter as unknown as Pixi2DGameAdapter });
1800
- } else if (world.kind === 'react') {
1801
- specs.push({ ...base, kind: 'react', adapter: adapter as unknown as ReactWorldAdapter });
1802
- } else if (world.kind === 'threejs') {
1528
+ } else if (world.surface === 'react') {
1529
+ specs.push({ ...base, kind: 'react', adapter: adapter as unknown as ReactRootAdapter });
1530
+ } else if (world.surface === 'threejs') {
1803
1531
  specs.push({ ...base, kind: 'threejs', adapter });
1804
1532
  } else {
1805
- // Exhaustiveness guard (§7.4-2): `world.kind` is `WorldEntry['kind']`,
1806
- // the same closed `'threejs' | 'pixijs' | 'react'` union as `WorldKind`
1807
- // (schema.ts's `WorldEntrySchema.kind` is `z.enum([...])`, so this is
1533
+ // Exhaustiveness guard (§7.4-2): `world.surface` is `AdapterRoot['kind']`,
1534
+ // the same closed `'threejs' | 'pixijs' | 'react'` union as `AdapterSurface`
1535
+ // (schema.ts's `AdapterRootSchema.kind` is `z.enum([...])`, so this is
1808
1536
  // NOT a plain-`string` field) — a hypothetical 4th kind must fail to
1809
1537
  // compile here rather than silently default to 'threejs' as the old
1810
1538
  // catch-all `else` did.
1811
- assertNever(world.kind, 'resolveAllWorlds');
1539
+ assertNever(world.surface, 'resolveAllWorlds');
1812
1540
  }
1813
1541
  }
1814
1542
  return specs;
1815
1543
  }
1816
1544
 
1545
+ /** Editor-only context for resolving a manifest composition. The live scene
1546
+ * document belongs to exactly one focused threejs world; every sibling still
1547
+ * resolves from its own manifest-owned content. */
1548
+ export interface ResolveAllWorldEntriesOptions {
1549
+ readonly editorPreview?: EditorPreview | undefined;
1550
+ readonly editorPreviewWorldId?: string | undefined;
1551
+ }
1552
+
1817
1553
  /**
1818
1554
  * E4.R2 (reopen fix — design/26 S-D D2's "editor play mode calls it"):
1819
1555
  * `resolveAllWorlds`'s composer-path sibling. Resolves every world's adapter
1820
- * through the SAME scope-checked loop ({@link resolveAllWorldAdapters}) but
1556
+ * through the SAME scope-checked loop ({@link resolveAllRootAdapters}) but
1821
1557
  * returns `mountManifestWorlds`'s own `entries` shape (`{ kind, adapter }`
1822
1558
  * per world id, `@engine/runtime/mount-manifest`) instead of a hand-built
1823
1559
  * `WorldMountSpec[]` — so `zOrder`/`pausable`/`loop` and the per-kind
@@ -1826,32 +1562,31 @@ export async function resolveAllWorlds(
1826
1562
  * rather than being hand-duplicated a second time here AND at the
1827
1563
  * `mountManifestWorlds` call site (the exact duplication a reopen review
1828
1564
  * found in `play-mode.ts`'s prior `resolveAllWorlds` + `createGameRuntime
1829
- * ({ container, worlds })` call shape).
1565
+ * ({ container, roots })` call shape).
1830
1566
  *
1831
- * `resolveAllWorlds` itself is UNCHANGED/byte-stable for its other callers
1832
- * (`ingest-mode.ts`, `module-mode.ts`, its own unit tests) — this is purely
1833
- * an additional entry point for `play-mode.ts`'s multi-world branch, which
1834
- * now calls `mountManifestWorlds({ manifest, container, entries, ... })`
1835
- * directly instead of `createGameRuntime({ container, worlds })`.
1567
+ * `resolveAllWorlds` remains the `WorldMountSpec[]` form used by other callers;
1568
+ * editor play uses this entries form for every manifest composition and lets
1569
+ * `mountManifestWorlds` remain the authoritative declaration-to-mount translator.
1836
1570
  */
1837
1571
  export async function resolveAllWorldEntries(
1838
1572
  manifest: ResolvedGameManifest,
1839
1573
  projectRoot: string,
1574
+ opts?: ResolveAllWorldEntriesOptions,
1840
1575
  ): Promise<Record<string, MountEntry>> {
1841
- const resolved = await resolveAllWorldAdapters(manifest, projectRoot);
1576
+ const resolved = await resolveAllRootAdapters(manifest, projectRoot, opts);
1842
1577
 
1843
1578
  const entries: Record<string, MountEntry> = {};
1844
1579
  for (const { world, adapter } of resolved) {
1845
- if (world.kind === 'pixijs') {
1580
+ if (world.surface === 'pixijs') {
1846
1581
  entries[world.id] = { kind: 'pixijs', adapter: adapter as unknown as Pixi2DGameAdapter };
1847
- } else if (world.kind === 'react') {
1848
- entries[world.id] = { kind: 'react', adapter: adapter as unknown as ReactWorldAdapter };
1849
- } else if (world.kind === 'threejs') {
1582
+ } else if (world.surface === 'react') {
1583
+ entries[world.id] = { kind: 'react', adapter: adapter as unknown as ReactRootAdapter };
1584
+ } else if (world.surface === 'threejs') {
1850
1585
  entries[world.id] = { kind: 'threejs', adapter };
1851
1586
  } else {
1852
1587
  // Exhaustiveness guard (§7.4-2) — same reasoning as `resolveAllWorlds`'s
1853
1588
  // own identical guard above.
1854
- assertNever(world.kind, 'resolveAllWorldEntries');
1589
+ assertNever(world.surface, 'resolveAllWorldEntries');
1855
1590
  }
1856
1591
  }
1857
1592
  return entries;