@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.
- package/README.md +24 -0
- package/dist/assets/axe-C7YBtOuM.js +30 -0
- package/dist/assets/browserAll-DDtGGE48.js +1 -0
- package/dist/assets/create-runtime-DbTlBlKG.js +1 -0
- package/dist/assets/{game-BI111o5f.js → game-BE0r7DMI.js} +3 -3
- package/dist/assets/game-BdTHR2_X.js +1 -0
- package/dist/assets/{game-pt_1lQLv.js → game-C3iQGSqo.js} +1 -1
- package/dist/assets/game-CcHfYxjP.js +1 -0
- package/dist/assets/game-Cqzj_lpD.js +1 -0
- package/dist/assets/game-DSZdeCHu.js +1 -0
- package/dist/assets/game-Dch9sAPc.js +1 -0
- package/dist/assets/{game-xsOho_wg.js → game-SAFn90pA.js} +3 -3
- package/dist/assets/game-loop-DMIgSMV-.js +1 -0
- package/dist/assets/{game-source-CvidN7AB.js → game-source-Dk83w26t.js} +1 -1
- package/dist/assets/{index-Bp5cecD6.js → index-4idIsHHJ.js} +2 -2
- package/dist/assets/{index-DL7RcQ0n.js → index-C-LhOTBN.js} +1 -1
- package/dist/assets/{index-Cs8xwv3e.js → index-CDkxceqE.js} +1 -1
- package/dist/assets/index-CQEfi217.js +15412 -0
- package/dist/assets/index-Dvi-V0NE.css +1 -0
- package/dist/assets/ingest-siblings-s0P5I1Ub.js +1 -0
- package/dist/assets/mount-manifest-DXMOlF79.js +1 -0
- package/dist/assets/react-18-C9eHn8x1.js +1 -0
- package/dist/assets/webworkerAll-Fn0e-ouF.js +1 -0
- package/dist/assets/world-documents-GM0a9Kv7.js +1 -0
- package/dist/index.html +15 -13
- package/dist/ingest/racing-game/vgai.game.json +5 -4
- package/dist/ingest/simcity/vgai.game.json +5 -4
- package/dist/ingest/tanks/vgai.game.json +5 -4
- package/dist/project-scripts.bundle.js +3917 -0
- package/dist/scenes/untitled.vscn.json +2 -0
- package/dist-server/packaged.mjs +7697 -3435
- package/package.json +31 -4
- package/scripts/probe-reporter.ts +349 -0
- package/scripts/run-probe.ts +2260 -0
- package/server/asset-catalog-audit.ts +268 -0
- package/server/asset-catalog-v2.ts +146 -0
- package/server/asset-library-routes.ts +276 -58
- package/server/canonical-path.ts +1 -1
- package/server/dev.ts +148 -44
- package/server/editor-server.ts +888 -61
- package/server/editor-sse.ts +115 -4
- package/server/harness-chat-service.ts +796 -0
- package/server/local-asset-catalog.ts +157 -22
- package/server/packaged.ts +51 -21
- package/server/process-shutdown.ts +136 -0
- package/server/prod.ts +9 -6
- package/server/project-hmr-files.ts +41 -0
- package/server/project-operations.ts +343 -0
- package/server/project-optimize-deps-entries.ts +4 -4
- package/server/project-output-writer.ts +156 -0
- package/server/project-validation.ts +140 -0
- package/server/project-view.ts +8 -50
- package/server/server-utils.ts +75 -10
- package/server/session-registry.ts +12 -0
- package/src/EditorContext.tsx +107 -3
- package/src/action-registry.ts +115 -17
- package/src/adapter-resolver.ts +213 -478
- package/src/asset-events.ts +64 -1
- package/src/asset-preview.ts +906 -0
- package/src/asset-selection.ts +121 -0
- package/src/asset-workflow/asset-capabilities.ts +259 -0
- package/src/asset-workflow/asset-editor-errors.ts +46 -0
- package/src/asset-workflow/asset-import-pipeline.ts +176 -0
- package/src/asset-workflow/asset-library-query.ts +107 -0
- package/src/asset-workflow/asset-types.ts +192 -0
- package/src/asset-workflow/asset-workflow-quality.ts +65 -0
- package/src/asset-workflow/import-contract.ts +113 -0
- package/src/asset-workflow/model-conversion.ts +126 -0
- package/src/asset-workflow/model-inspection.ts +185 -0
- package/src/asset-workflow/project-asset-commands.ts +23 -0
- package/src/asset-workflow/project-asset-health.ts +153 -0
- package/src/asset-workflow/project-asset-operations.ts +119 -0
- package/src/asset-workflow/thumbnail-system.ts +172 -0
- package/src/authoring/active-systems.ts +15 -0
- package/src/authoring/boundary-authoring-adapter.ts +16 -5
- package/src/authoring/composite-authoring-adapter.ts +305 -124
- package/src/authoring/design-time-layers.ts +300 -117
- package/src/authoring/edit-mode-authoring.ts +278 -293
- package/src/authoring/embed-only-adapter.ts +4 -2
- package/src/authoring/first-party-hierarchy-menu.ts +7 -2
- package/src/authoring/first-party-inspector/AddSectionMenu.tsx +5 -12
- package/src/authoring/first-party-inspector/EnvironmentSection.tsx +53 -126
- package/src/authoring/first-party-inspector/HeaderSection.tsx +54 -65
- package/src/authoring/first-party-inspector/is-first-party-selection.ts +30 -11
- package/src/authoring/first-party-inspector/register.tsx +13 -2
- package/src/authoring/ingest-authoring-adapter.ts +142 -45
- package/src/authoring/mount-failure-report.ts +5 -5
- package/src/authoring/provenance.ts +41 -0
- package/src/authoring/react-dom-authoring-adapter.ts +92 -92
- package/src/authoring/react-inspector-section.tsx +489 -51
- package/src/authoring/react-world-authoring-adapter.ts +541 -306
- package/src/authoring/story-overlay.ts +1 -1
- package/src/authoring/story-types.ts +11 -0
- package/src/authoring/vgai-scene-authoring-adapter.ts +54 -25
- package/src/authoring/viewport-tool-context.ts +78 -0
- package/src/authoring/world-canvas-viewport-state.ts +35 -0
- package/src/authoring/world-hidden-viewport.ts +186 -0
- package/src/authoring/world-pan-state.ts +33 -17
- package/src/authoring/world-session-state.ts +1 -1
- package/src/authoring/world2d-authoring-adapter.ts +109 -52
- package/src/availability-tick.ts +56 -0
- package/src/browser-play-entry.ts +5 -5
- package/src/browser-project-scripts.ts +1 -0
- package/src/build-identity.ts +15 -0
- package/src/command-listener.ts +893 -19
- package/src/components/AlignToolbar.tsx +74 -63
- package/src/components/AppRoot.tsx +193 -26
- package/src/components/ApplicationMenus.tsx +272 -0
- package/src/components/AssetBrowser.tsx +1450 -293
- package/src/components/AssetEditorShell.tsx +141 -0
- package/src/components/AssetImportDetails.tsx +218 -0
- package/src/components/AssetLibraryPanel.tsx +14 -0
- package/src/components/CameraInfo.tsx +11 -20
- package/src/components/CenterDocuments.tsx +585 -0
- package/src/components/CommandPalette.tsx +75 -30
- package/src/components/ConsolePanel.css +55 -0
- package/src/components/ConsolePanel.tsx +57 -112
- package/src/components/DebugConsolePanel.tsx +461 -0
- package/src/components/DebuggerPanel.tsx +165 -0
- package/src/components/DefaultEditorLayout.tsx +167 -231
- package/src/components/GameHierarchy.tsx +873 -290
- package/src/components/GamePerformanceOverlay.tsx +46 -0
- package/src/components/HarnessChatPanel.tsx +2352 -0
- package/src/components/HistoryPanel.tsx +226 -0
- package/src/components/Inspector.tsx +153 -99
- package/src/components/InspectorToolSection.tsx +6 -4
- package/src/components/MountFailureBanner.tsx +31 -48
- package/src/components/NetworkSection.tsx +25 -14
- package/src/components/OnlineAssetBrowser.tsx +794 -343
- package/src/components/OverlayReportBanner.tsx +38 -64
- package/src/components/PerformancePanel.tsx +206 -0
- package/src/components/PlayBar.tsx +93 -263
- package/src/components/PrefabSaveDialog.tsx +55 -127
- package/src/components/ProjectHeader.css +60 -0
- package/src/components/ProjectHeader.tsx +170 -105
- package/src/components/ProjectScreen.tsx +246 -184
- package/src/components/ReactCanvasControls.tsx +185 -0
- package/src/components/ResolutionPicker.tsx +85 -0
- package/src/components/RootActivateDialog.tsx +39 -68
- package/src/components/SaveDialog.tsx +55 -127
- package/src/components/SaveStatus.tsx +11 -29
- package/src/components/StartupLoadingScreen.tsx +96 -0
- package/src/components/StateWatchPanel.tsx +302 -0
- package/src/components/StatsOverlay.tsx +12 -5
- package/src/components/StatusBar.tsx +64 -0
- package/src/components/ThemeManagerDialog.css +89 -0
- package/src/components/ThemeManagerDialog.tsx +371 -0
- package/src/components/ToolHost.tsx +26 -15
- package/src/components/Toolbar.tsx +117 -214
- package/src/components/VgaiVoxelLogo.tsx +89 -0
- package/src/components/ViewportOverlay.tsx +97 -172
- package/src/components/ViewportPanel.tsx +130 -31
- package/src/components/WorkspaceDock.tsx +891 -0
- package/src/components/WorldSelectionOverlay.tsx +51 -63
- package/src/components/WorldTextEditor.tsx +9 -5
- package/src/components/asset-documents.tsx +390 -0
- package/src/components/asset-editor-persistence.ts +44 -0
- package/src/components/asset-selection-section.tsx +338 -0
- package/src/components/asset-viewers/AudioViewer.tsx +73 -21
- package/src/components/asset-viewers/GenericJsonViewer.tsx +26 -11
- package/src/components/asset-viewers/ImageViewer.tsx +43 -11
- package/src/components/asset-viewers/InputMapViewer.tsx +512 -134
- package/src/components/asset-viewers/MaterialAssetEditor.tsx +540 -0
- package/src/components/asset-viewers/ModelViewer.tsx +805 -121
- package/src/components/asset-viewers/OnlineAssetDetail.tsx +177 -60
- package/src/components/asset-viewers/SourceAssetViewer.tsx +121 -0
- package/src/components/asset-workflow.css +506 -0
- package/src/components/build-documents.tsx +696 -0
- package/src/components/core-utilities.tsx +75 -0
- package/src/components/data-documents.tsx +364 -0
- package/src/components/{DataPanel.tsx → data-editor.tsx} +138 -288
- package/src/components/debugger-panel-state.ts +57 -0
- package/src/components/inspector-widgets/AlignmentGrid.tsx +8 -14
- package/src/components/inspector-widgets/BorderEditor.tsx +41 -33
- package/src/components/inspector-widgets/ColorPicker.tsx +39 -39
- package/src/components/inspector-widgets/FontPicker.tsx +13 -21
- package/src/components/inspector-widgets/GradientEditor.tsx +11 -19
- package/src/components/inspector-widgets/ScrubbableInput.tsx +6 -3
- package/src/components/inspector-widgets/ShadowEditor.tsx +20 -33
- package/src/components/inspector-widgets/shared.tsx +90 -49
- package/src/components/inspectors/AnimationSection.tsx +39 -40
- package/src/components/inspectors/AudioSection.tsx +28 -39
- package/src/components/inspectors/BoneAttachmentSection.tsx +19 -24
- package/src/components/inspectors/CameraSection.tsx +14 -23
- package/src/components/inspectors/ComponentsSection.tsx +39 -43
- package/src/components/inspectors/JointsSection.tsx +34 -43
- package/src/components/inspectors/LightSection.tsx +90 -35
- package/src/components/inspectors/MaterialSection.tsx +137 -174
- package/src/components/inspectors/MeshSection.tsx +9 -17
- package/src/components/inspectors/NavigationSection.tsx +8 -15
- package/src/components/inspectors/ParticleSection.tsx +91 -76
- package/src/components/inspectors/PhysicsSection.tsx +42 -46
- package/src/components/inspectors/PostProcessingSection.tsx +28 -36
- package/src/components/inspectors/RenderingSection.tsx +12 -20
- package/src/components/inspectors/ShadowSection.tsx +7 -8
- package/src/components/inspectors/SplineSection.tsx +31 -42
- package/src/components/inspectors/TransformSection.tsx +14 -22
- package/src/components/patterns/Dialog.tsx +95 -0
- package/src/components/patterns/Fields.tsx +44 -0
- package/src/components/patterns/List.tsx +25 -0
- package/src/components/patterns/StateSurface.tsx +40 -0
- package/src/components/patterns/Surfaces.tsx +115 -0
- package/src/components/patterns/Tabs.tsx +80 -0
- package/src/components/patterns/Toolbar.tsx +51 -0
- package/src/components/patterns/Tree.tsx +72 -0
- package/src/components/primitives/Button.tsx +46 -0
- package/src/components/primitives/ColorInput.tsx +11 -47
- package/src/components/primitives/EditorIcon.tsx +99 -0
- package/src/components/primitives/FormControls.tsx +71 -0
- package/src/components/primitives/JsonInput.tsx +15 -2
- package/src/components/primitives/Layout.tsx +100 -0
- package/src/components/primitives/Menu.tsx +105 -0
- package/src/components/primitives/NumberInput.tsx +8 -35
- package/src/components/primitives/Panel.tsx +49 -44
- package/src/components/primitives/SectionHeader.tsx +10 -28
- package/src/components/primitives/Text.tsx +46 -0
- package/src/components/primitives/Tooltip.tsx +20 -12
- package/src/components/primitives/Vec3Input.tsx +3 -0
- package/src/components/primitives/banner-tones.ts +33 -0
- package/src/components/primitives/editor-icons.ts +102 -0
- package/src/components/primitives/panel-header-styles.ts +30 -0
- package/src/components/project-operation-documents.tsx +226 -0
- package/src/components/status-contributions.tsx +175 -0
- package/src/components/story-documents-register.ts +15 -0
- package/src/components/story-documents.tsx +1185 -0
- package/src/components/tool-documents.tsx +110 -0
- package/src/components/workspace-dock.css +293 -0
- package/src/components/workspace-history.ts +33 -0
- package/src/components/world-documents.tsx +145 -0
- package/src/components/xstate-documents.tsx +209 -0
- package/src/composite-screenshot.ts +248 -0
- package/src/design-system/index.ts +70 -0
- package/src/design-system-stories/Foundation.stories.tsx +183 -0
- package/src/design-system-stories/Patterns.stories.tsx +251 -0
- package/src/design-system-stories/Workspace.stories.tsx +538 -0
- package/src/doctor/report.ts +57 -13
- package/src/editor-api.ts +295 -31
- package/src/editor-compatibility.ts +200 -0
- package/src/editor-console.ts +35 -17
- package/src/editor-hotkeys.ts +85 -33
- package/src/editor-store.ts +538 -233
- package/src/editor-viewport.ts +160 -34
- package/src/entity-factory.ts +14 -3
- package/src/gizmo-registry.ts +21 -5
- package/src/harness-chat-editor-context.ts +37 -0
- package/src/harness-chat-indicator.ts +68 -0
- package/src/harness-chat-layout.ts +53 -0
- package/src/harness-chat-lifecycle.ts +170 -0
- package/src/harness-chat-reconnect.ts +85 -0
- package/src/harness-chat-types.ts +409 -0
- package/src/hierarchy-drop.ts +50 -0
- package/src/hierarchy-expansion-state.ts +100 -0
- package/src/hierarchy-menu-registry.ts +1 -1
- package/src/hierarchy-projection.ts +76 -0
- package/src/history/editor-session.ts +21 -0
- package/src/history/history-commands.ts +147 -0
- package/src/history/history-service.ts +904 -0
- package/src/history/json-history-resource.ts +165 -0
- package/src/history/persistence-coordinator.ts +35 -0
- package/src/history/project-file-history.ts +266 -0
- package/src/history/project-root-history-backends.ts +53 -0
- package/src/history/resource-registry.ts +209 -0
- package/src/history/snapshot-store.ts +108 -0
- package/src/history/source-history-backend.ts +374 -0
- package/src/history/types.ts +100 -0
- package/src/hmr-registration-group.ts +67 -0
- package/src/hmr-stable-react-context.ts +23 -0
- package/src/hosted-example.ts +12 -2
- package/src/hotkeys.ts +89 -18
- package/src/ingest/discovery-react.ts +9 -10
- package/src/ingest/discovery.ts +4 -4
- package/src/ingest/discovery2d.ts +5 -5
- package/src/ingest/games/bloom-composer/vgai.game.json +3 -3
- package/src/ingest/games/embed-bundle/vgai.game.json +3 -3
- package/src/ingest/games/games-fps/vgai.game.json +3 -3
- package/src/ingest/games/iframe-reachable/vgai.game.json +3 -3
- package/src/ingest/games/interactive-cubes/vgai.game.json +3 -3
- package/src/ingest/games/mismatched-three/vgai.game.json +3 -3
- package/src/ingest/games/own-three-bundled/vgai.game.json +3 -3
- package/src/ingest/games/pointerlock-maze/vgai.game.json +7 -4
- package/src/ingest/games-2d/bubbo-bubbo/vgai.game.json +3 -3
- package/src/ingest/games-2d/bunnymark/vgai.game.json +3 -3
- package/src/ingest/games-2d/flappy/vgai.game.json +3 -3
- package/src/ingest/games-2d/flappy-pixi/vgai.game.json +3 -3
- package/src/ingest/games-2d/puzzling-potions/vgai.game.json +3 -3
- package/src/ingest/games-2d/tilemap-camera/vgai.game.json +3 -3
- package/src/ingest/games-react/react-rpg/vgai.game.json +3 -3
- package/src/ingest/types.ts +1 -1
- package/src/ingest-mode.ts +47 -52
- package/src/ingest-siblings.ts +23 -33
- package/src/initial-scene.ts +1 -2
- package/src/main.tsx +16 -8
- package/src/manifest-project.ts +10 -12
- package/src/model-thumbnail.ts +261 -26
- package/src/module-mode.ts +44 -50
- package/src/play-log-events.ts +26 -0
- package/src/play-mode.ts +495 -401
- package/src/play-scene-adoption.ts +23 -0
- package/src/playground/main.ts +68 -24
- package/src/playwright-shim.ts +473 -0
- package/src/project-manager.ts +90 -10
- package/src/project-operations.ts +112 -0
- package/src/project-story-discovery.ts +25 -0
- package/src/project-tool-discovery.ts +43 -0
- package/src/scene-serializer.ts +0 -6
- package/src/scene-sync.ts +7 -2
- package/src/script-loader.ts +3 -3
- package/src/skeleton-helper.ts +57 -0
- package/src/storage/seed.ts +68 -8
- package/src/stories/StoryPreviewMount.tsx +265 -0
- package/src/stories/action-stories.fixture.tsx +44 -0
- package/src/stories/button-stories.fixture.tsx +50 -0
- package/src/stories/compose-project-stories.ts +182 -0
- package/src/stories/loader-stories.fixture.tsx +53 -0
- package/src/stories/story-discovery.ts +130 -0
- package/src/stories/story-registry.ts +146 -0
- package/src/theme-css.ts +1049 -0
- package/src/theme-library.ts +238 -0
- package/src/theme-preference.ts +130 -0
- package/src/theme.ts +527 -0
- package/src/tool-loader.ts +80 -24
- package/src/ui-source/inspect.ts +27 -4
- package/src/ui-source/source-write-backend.ts +83 -22
- package/src/widgets/index.ts +9 -18
- package/src/workspace-aux-commands.ts +11 -0
- package/src/workspace-dock-controller.ts +64 -0
- package/src/workspace-document-ids.ts +5 -0
- package/src/workspace-document-registry.ts +367 -0
- package/src/workspace-state-persistence.ts +455 -0
- package/src/workspace-status-registry.ts +110 -0
- package/src/workspace-utility-commands.ts +49 -0
- package/src/workspace-utility-registry.ts +161 -0
- package/src/world-document-routing.ts +16 -0
- package/src/xstate-inspect/XStateMachineInspector.tsx +544 -0
- package/src/xstate-inspect/character-animation-machine.fixture.ts +74 -0
- package/src/xstate-inspect/use-live-actor-state.ts +39 -0
- package/src/xstate-inspect/xstate-graph.ts +265 -0
- package/src/xstate-inspect/xstate-layout.ts +76 -0
- package/src/z-index.ts +26 -0
- package/template/.claude/skills/editor/SKILL.md +129 -90
- package/template/AGENTS.md +1110 -0
- package/template/CLAUDE.md +1 -512
- package/template/IDIOMS.md +50 -0
- package/template/check-idioms.ts +706 -0
- package/template/manifest-entry-modules-plugin.ts +53 -0
- package/template/package.json +13 -4
- package/template/public/scenes/default.scn2d.json +6 -0
- package/template/public/scenes/default.vscn.json +7 -1
- package/template/public/scenes/untitled.vscn.json +10 -0
- package/template/scripts/playtest.ts +40 -0
- package/template/server/loopback.ts +75 -0
- package/template/server/rooms/arena-room.ts +69 -1
- package/template/server/rooms/game-room.ts +68 -1
- package/template/src/data/README.md +9 -4
- package/template/src/data/data-ref.ts +35 -0
- package/template/src/data/tuning.schema.ts +1 -1
- package/template/src/main.ts +41 -62
- package/template/src/manifest-entry-modules.d.ts +3 -0
- package/template/src/operations/README.md +88 -0
- package/template/src/runtime/pixi-adapter.ts +24 -0
- package/template/src/scripts/components/data-spinner.ts +43 -2
- package/template/src/scripts/components/spin-lap-math.ts +27 -0
- package/template/src/tools/README.md +5 -3
- package/template/src/tools/example.tool.tsx +13 -7
- package/template/src/ui/Button.stories.tsx +66 -0
- package/template/src/ui/RebindPanel.tsx +2 -6
- package/template/src/ui/game.tsx +17 -0
- package/template/tests/acceptance/example.spec.ts +64 -0
- package/template/tests/logic/example.test.ts +31 -0
- package/template/tests/logic/room.test.ts +100 -0
- package/template/tests/tsconfig.json +30 -0
- package/template/validate-manifest.ts +5 -5
- package/template/validate-scenes.ts +50 -18
- package/template/vgai.game.json +20 -13
- package/template/vite.config.ts +77 -16
- package/vite-plugin-ui-oid.ts +261 -17
- package/dist/assets/CCDIKSolver-Dz-ysxZF.js +0 -1
- package/dist/assets/SelectionBox-BXsv_6aH.js +0 -1
- package/dist/assets/browser-project-scripts-DOru4L1x.js +0 -2
- package/dist/assets/browserAll-CEkWYlFk.js +0 -1
- package/dist/assets/create-runtime-DYIWr3va.js +0 -1
- package/dist/assets/data-asset-Bb9sIM7i.js +0 -5
- package/dist/assets/game-8SDZPPgH.js +0 -1
- package/dist/assets/game-CevorxyV.js +0 -1
- package/dist/assets/game-Di7XlywD.js +0 -1
- package/dist/assets/game-DjnCaw8R.js +0 -1
- package/dist/assets/game-Q9A0aXSV.js +0 -1
- package/dist/assets/index-CPVV6pjw.js +0 -1
- package/dist/assets/index-CpOnYyeD.js +0 -213
- package/dist/assets/index-Dr8H_6Rl.js +0 -1
- package/dist/assets/index-_bCaipUk.js +0 -13650
- package/dist/assets/ingest-siblings-Dz8eEqrV.js +0 -1
- package/dist/assets/mount-manifest-Bhk0ohsl.js +0 -1
- package/dist/assets/scene-query-BivAHhJu.js +0 -1
- package/dist/assets/webworkerAll-BLrH0F6F.js +0 -1
- package/dist/assets/xstate-animation-binding-1NGLmw6y.js +0 -1
- package/dist/assets/xstate.esm-BgUD33nl.js +0 -3
- package/src/authoring/scene-ui-layer.ts +0 -485
- package/src/authoring/ui-authoring-adapter.ts +0 -325
- package/src/authoring/ui-hierarchy-menu.ts +0 -40
- package/src/authoring/ui-inspector-sections.tsx +0 -156
- package/src/authoring/undo-timeline.ts +0 -145
- package/src/components/BottomPanel.tsx +0 -326
- package/src/components/BuildPanel.tsx +0 -455
- package/src/components/FolderTree.tsx +0 -179
- package/src/components/RightPanel.tsx +0 -166
- package/src/components/WorldContextMenu.tsx +0 -257
- package/src/components/asset-viewers/AssetViewer.tsx +0 -38
- package/src/ingest/games-react/react-rpg/src/ui/game-state.tsx +0 -129
- package/src/scene-ui-mode/active-scene-ui.ts +0 -58
- package/src/scene-ui-mode/scene-ui-palette-panel.tsx +0 -152
- package/src/scene-ui-mode/scene-ui-timeline-tab.tsx +0 -29
- package/src/scene-ui-mode/ui-prefab-dialog-host.tsx +0 -74
- package/src/scene-ui-mode/ui-prefab-hierarchy-menu.ts +0 -49
- package/src/scene-ui-mode/ui-prefab-io.ts +0 -76
- package/src/ui-editor/inspector.tsx +0 -563
- package/src/ui-editor/node-ops.ts +0 -266
- package/src/ui-editor/overlay.tsx +0 -380
- package/src/ui-editor/palette.tsx +0 -114
- package/src/ui-editor/react-store.ts +0 -78
- package/src/ui-editor/timeline-panel.tsx +0 -443
- package/src/ui-editor/ui-edit-store.ts +0 -552
- package/src/ui-editor/ui-prefab.ts +0 -95
- package/template/project.json +0 -9
- package/template/src/scripts/main.ts +0 -94
- package/template/src/ui/DefaultHud.tsx +0 -40
- package/template/src/ui/game-state.tsx +0 -37
- package/template/src/ui/mount-default-hud.tsx +0 -19
- package/template/src/ui/mount-ui.ts +0 -15
- package/vite-plugin-react-world-provider.ts +0 -81
package/src/adapter-resolver.ts
CHANGED
|
@@ -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 (`
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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/
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
|
130
|
-
export interface
|
|
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
|
-
|
|
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<
|
|
216
|
-
const modulePath = fsImportPath(projectRoot, 'src/
|
|
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
|
-
|
|
236
|
+
pixiComponentRegistry?: Component2DRegistry;
|
|
232
237
|
};
|
|
233
|
-
return mod.
|
|
234
|
-
} catch
|
|
235
|
-
|
|
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:
|
|
253
|
+
world: ResolvedAdapterRoot,
|
|
278
254
|
projectRoot: string,
|
|
279
|
-
opts?:
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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-
|
|
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:
|
|
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
|
-
`
|
|
440
|
-
`but no \`setup\` — default-pixi
|
|
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
|
-
|
|
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
|
-
`
|
|
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:
|
|
457
|
+
world: ResolvedAdapterRoot,
|
|
480
458
|
mod: { default?: unknown },
|
|
481
|
-
identityLabel: '
|
|
459
|
+
identityLabel: 'react' | 'ingest-react' = 'react',
|
|
482
460
|
): ComponentType {
|
|
483
461
|
if (typeof mod.default !== 'function') {
|
|
484
462
|
throw new Error(
|
|
485
|
-
`
|
|
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
|
-
"
|
|
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
|
|
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 `
|
|
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
|
-
*
|
|
647
|
-
*
|
|
648
|
-
*
|
|
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
|
-
*
|
|
681
|
-
*
|
|
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
|
-
* `
|
|
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
|
-
*
|
|
728
|
-
*
|
|
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
|
|
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
|
|
760
|
-
world:
|
|
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
|
-
`
|
|
766
|
-
'
|
|
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
|
-
`
|
|
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:
|
|
721
|
+
world: ResolvedAdapterRoot,
|
|
789
722
|
projectRoot: string,
|
|
790
723
|
): Promise<GameAdapter> {
|
|
791
|
-
const Entry = await
|
|
792
|
-
const GameProvider = await resolveGameProviderForProject(projectRoot, world.id, '
|
|
724
|
+
const Entry = await resolveReactAdapterRootComponent(world, projectRoot);
|
|
725
|
+
const GameProvider = await resolveGameProviderForProject(projectRoot, world.id, 'react');
|
|
793
726
|
|
|
794
|
-
const adapter:
|
|
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
|
-
`
|
|
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
|
-
|
|
814
|
-
|
|
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 `
|
|
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
|
-
* `
|
|
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
|
-
*
|
|
883
|
-
*
|
|
884
|
-
*
|
|
885
|
-
*
|
|
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
|
-
*
|
|
902
|
-
*
|
|
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
|
-
*
|
|
1006
|
-
*
|
|
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
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
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:
|
|
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(`
|
|
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
|
-
`
|
|
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
|
-
`
|
|
1061
|
-
'
|
|
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
|
-
`
|
|
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:
|
|
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
|
-
`
|
|
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:
|
|
1163
|
-
adapter: Extract<
|
|
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:
|
|
1205
|
-
adapter: Extract<
|
|
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:
|
|
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
|
|
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:
|
|
1343
|
-
adapter: Extract<
|
|
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:
|
|
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
|
|
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:
|
|
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
|
-
`
|
|
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:
|
|
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
|
-
`
|
|
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:
|
|
1321
|
+
export function guardModulePath(world: ResolvedAdapterRoot, moduleId: string): string {
|
|
1523
1322
|
const fail = (reason: string): never => {
|
|
1524
1323
|
throw new Error(
|
|
1525
|
-
`
|
|
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:
|
|
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
|
-
`
|
|
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:
|
|
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:
|
|
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
|
|
1656
|
-
world:
|
|
1454
|
+
export async function resolveRootAdapter(
|
|
1455
|
+
world: ResolvedAdapterRoot,
|
|
1657
1456
|
projectRoot: string,
|
|
1658
|
-
opts?:
|
|
1457
|
+
opts?: ResolveRootAdapterOptions,
|
|
1659
1458
|
): Promise<GameAdapter> {
|
|
1660
1459
|
const { adapter } = world;
|
|
1661
1460
|
|
|
1662
1461
|
switch (adapter.identity) {
|
|
1663
|
-
case '
|
|
1462
|
+
case 'threejs':
|
|
1664
1463
|
return resolveDefaultThreeAdapter(world, projectRoot, opts);
|
|
1665
|
-
case '
|
|
1464
|
+
case 'pixijs':
|
|
1666
1465
|
return resolveDefaultPixiAdapter(world, projectRoot);
|
|
1667
|
-
case '
|
|
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
|
-
`
|
|
1479
|
+
`resolveRootAdapter: unhandled adapter identity ${JSON.stringify(exhaustive)}`,
|
|
1681
1480
|
);
|
|
1682
1481
|
}
|
|
1683
1482
|
}
|
|
1684
1483
|
}
|
|
1685
1484
|
|
|
1686
1485
|
// ---------------------------------------------------------------------------
|
|
1687
|
-
//
|
|
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}
|
|
1763
|
-
*
|
|
1764
|
-
*
|
|
1765
|
-
*
|
|
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
|
|
1496
|
+
async function resolveAllRootAdapters(
|
|
1769
1497
|
manifest: ResolvedGameManifest,
|
|
1770
1498
|
projectRoot: string,
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
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
|
|
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.
|
|
1526
|
+
if (world.surface === 'pixijs') {
|
|
1799
1527
|
specs.push({ ...base, kind: 'pixijs', adapter: adapter as unknown as Pixi2DGameAdapter });
|
|
1800
|
-
} else if (world.
|
|
1801
|
-
specs.push({ ...base, kind: 'react', adapter: adapter as unknown as
|
|
1802
|
-
} else if (world.
|
|
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.
|
|
1806
|
-
// the same closed `'threejs' | 'pixijs' | 'react'` union as `
|
|
1807
|
-
// (schema.ts's `
|
|
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.
|
|
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
|
|
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,
|
|
1565
|
+
* ({ container, roots })` call shape).
|
|
1830
1566
|
*
|
|
1831
|
-
* `resolveAllWorlds`
|
|
1832
|
-
*
|
|
1833
|
-
*
|
|
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
|
|
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.
|
|
1580
|
+
if (world.surface === 'pixijs') {
|
|
1846
1581
|
entries[world.id] = { kind: 'pixijs', adapter: adapter as unknown as Pixi2DGameAdapter };
|
|
1847
|
-
} else if (world.
|
|
1848
|
-
entries[world.id] = { kind: 'react', adapter: adapter as unknown as
|
|
1849
|
-
} else if (world.
|
|
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.
|
|
1589
|
+
assertNever(world.surface, 'resolveAllWorldEntries');
|
|
1855
1590
|
}
|
|
1856
1591
|
}
|
|
1857
1592
|
return entries;
|