@vgai/editor 0.2.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/LICENSE +661 -0
- package/README.md +35 -0
- package/dist/a/b/c/test-nested.json +1 -0
- package/dist/assets/CCDIKSolver-Dz-ysxZF.js +1 -0
- package/dist/assets/SelectionBox-BXsv_6aH.js +1 -0
- package/dist/assets/browser-project-scripts-DOru4L1x.js +2 -0
- package/dist/assets/browserAll-CEkWYlFk.js +1 -0
- package/dist/assets/collision-world-DniMB0qq.glb +0 -0
- package/dist/assets/create-runtime-DYIWr3va.js +1 -0
- package/dist/assets/data-asset-Bb9sIM7i.js +5 -0
- package/dist/assets/esbuild-BHljloGq.wasm +0 -0
- package/dist/assets/fake-three-BMTQ9rl_.js +1 -0
- package/dist/assets/game-8SDZPPgH.js +1 -0
- package/dist/assets/game-BI111o5f.js +10 -0
- package/dist/assets/game-CevorxyV.js +1 -0
- package/dist/assets/game-Di1H0gFj.js +1 -0
- package/dist/assets/game-Di7XlywD.js +1 -0
- package/dist/assets/game-DjnCaw8R.js +1 -0
- package/dist/assets/game-Q9A0aXSV.js +1 -0
- package/dist/assets/game-pt_1lQLv.js +1 -0
- package/dist/assets/game-source-CvidN7AB.js +1 -0
- package/dist/assets/game-xsOho_wg.js +83 -0
- package/dist/assets/index-Bp5cecD6.js +12 -0
- package/dist/assets/index-CPVV6pjw.js +1 -0
- package/dist/assets/index-CpOnYyeD.js +213 -0
- package/dist/assets/index-Cs8xwv3e.js +1 -0
- package/dist/assets/index-DL7RcQ0n.js +27 -0
- package/dist/assets/index-Dr8H_6Rl.js +1 -0
- package/dist/assets/index-_bCaipUk.js +13650 -0
- package/dist/assets/ingest-siblings-Dz8eEqrV.js +1 -0
- package/dist/assets/mount-manifest-Bhk0ohsl.js +1 -0
- package/dist/assets/recast-navigation.wasm-compat-CvP4UGK6.js +1 -0
- package/dist/assets/scene-query-BivAHhJu.js +1 -0
- package/dist/assets/stats.module--VATS4Kh.js +1 -0
- package/dist/assets/webworkerAll-BLrH0F6F.js +1 -0
- package/dist/assets/xstate-animation-binding-1NGLmw6y.js +1 -0
- package/dist/assets/xstate.esm-BgUD33nl.js +3 -0
- package/dist/index.html +37 -0
- package/dist/ingest/racing-game/UPSTREAM.md +68 -0
- package/dist/ingest/racing-game/a/Inter-Black.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-Black.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-BlackItalic.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-BlackItalic.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-Bold.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-Bold.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-BoldItalic.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-BoldItalic.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-ExtraBold.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-ExtraBold.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-ExtraBoldItalic.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-ExtraBoldItalic.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-ExtraLight.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-ExtraLight.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-ExtraLightItalic.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-ExtraLightItalic.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-Italic.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-Italic.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-Light.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-Light.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-LightItalic.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-LightItalic.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-Medium.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-Medium.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-MediumItalic.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-MediumItalic.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-Regular.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-Regular.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-SemiBold.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-SemiBold.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-SemiBoldItalic.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-SemiBoldItalic.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-Thin.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-Thin.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-ThinItalic.woff +0 -0
- package/dist/ingest/racing-game/a/Inter-ThinItalic.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-italic.var.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter-roman.var.woff2 +0 -0
- package/dist/ingest/racing-game/a/Inter.var.woff2 +0 -0
- package/dist/ingest/racing-game/a/index.css +1 -0
- package/dist/ingest/racing-game/images/bronze.png +0 -0
- package/dist/ingest/racing-game/images/gold.png +0 -0
- package/dist/ingest/racing-game/images/silver.png +0 -0
- package/dist/ingest/racing-game/index.html +15 -0
- package/dist/ingest/racing-game/models/chassis-draco.glb +0 -0
- package/dist/ingest/racing-game/models/track-draco.glb +0 -0
- package/dist/ingest/racing-game/models/wheel-draco.glb +0 -0
- package/dist/ingest/racing-game/rg-entry.js +1 -0
- package/dist/ingest/racing-game/sounds/accelerate.mp3 +0 -0
- package/dist/ingest/racing-game/sounds/boost.mp3 +0 -0
- package/dist/ingest/racing-game/sounds/crash.mp3 +0 -0
- package/dist/ingest/racing-game/sounds/engine.mp3 +0 -0
- package/dist/ingest/racing-game/sounds/honk.mp3 +0 -0
- package/dist/ingest/racing-game/sounds/tire-brake.mp3 +0 -0
- package/dist/ingest/racing-game/sounds/train.mp3 +0 -0
- package/dist/ingest/racing-game/sounds/water.mp3 +0 -0
- package/dist/ingest/racing-game/textures/cursor.svg +3 -0
- package/dist/ingest/racing-game/textures/dikhololo_night_1k.hdr +0 -0
- package/dist/ingest/racing-game/textures/heightmap_1024.png +0 -0
- package/dist/ingest/racing-game/textures/mask.svg +11 -0
- package/dist/ingest/racing-game/textures/no_sound.png +0 -0
- package/dist/ingest/racing-game/three-r139.module.js +50617 -0
- package/dist/ingest/racing-game/vendor.js +179 -0
- package/dist/ingest/racing-game/vgai.game.json +20 -0
- package/dist/ingest/simcity/UPSTREAM.md +65 -0
- package/dist/ingest/simcity/favicon.ico +0 -0
- package/dist/ingest/simcity/fonts/Bebas-Regular.otf +0 -0
- package/dist/ingest/simcity/fonts/Digitalt.otf +0 -0
- package/dist/ingest/simcity/fonts/Stanberry.ttf +0 -0
- package/dist/ingest/simcity/icons/bulldozer-color.png +0 -0
- package/dist/ingest/simcity/icons/bulldozer.png +0 -0
- package/dist/ingest/simcity/icons/calendar.png +0 -0
- package/dist/ingest/simcity/icons/factory-color.png +0 -0
- package/dist/ingest/simcity/icons/factory.png +0 -0
- package/dist/ingest/simcity/icons/house-color.png +0 -0
- package/dist/ingest/simcity/icons/house.png +0 -0
- package/dist/ingest/simcity/icons/job.png +0 -0
- package/dist/ingest/simcity/icons/office.png +0 -0
- package/dist/ingest/simcity/icons/pause-color.png +0 -0
- package/dist/ingest/simcity/icons/pause.png +0 -0
- package/dist/ingest/simcity/icons/person.png +0 -0
- package/dist/ingest/simcity/icons/play-color.png +0 -0
- package/dist/ingest/simcity/icons/play.png +0 -0
- package/dist/ingest/simcity/icons/power-color.png +0 -0
- package/dist/ingest/simcity/icons/power-line-color.png +0 -0
- package/dist/ingest/simcity/icons/power.png +0 -0
- package/dist/ingest/simcity/icons/road-color.png +0 -0
- package/dist/ingest/simcity/icons/road.png +0 -0
- package/dist/ingest/simcity/icons/select-color.png +0 -0
- package/dist/ingest/simcity/icons/select.png +0 -0
- package/dist/ingest/simcity/icons/store-color.png +0 -0
- package/dist/ingest/simcity/index.html +94 -0
- package/dist/ingest/simcity/index.js +58 -0
- package/dist/ingest/simcity/main.css +233 -0
- package/dist/ingest/simcity/models/armored-truck.glb +0 -0
- package/dist/ingest/simcity/models/building-block-4floor-corner.glb +0 -0
- package/dist/ingest/simcity/models/building-block-4floor-short.glb +0 -0
- package/dist/ingest/simcity/models/building-block-5floor.glb +0 -0
- package/dist/ingest/simcity/models/building-burger-joint.glb +0 -0
- package/dist/ingest/simcity/models/building-cafe.glb +0 -0
- package/dist/ingest/simcity/models/building-casino.glb +0 -0
- package/dist/ingest/simcity/models/building-cinema.glb +0 -0
- package/dist/ingest/simcity/models/building-house-block-big.glb +0 -0
- package/dist/ingest/simcity/models/building-house-family-large.glb +0 -0
- package/dist/ingest/simcity/models/building-house-family-small.glb +0 -0
- package/dist/ingest/simcity/models/building-office-balcony.glb +0 -0
- package/dist/ingest/simcity/models/building-office-big.glb +0 -0
- package/dist/ingest/simcity/models/building-office-pyramid.glb +0 -0
- package/dist/ingest/simcity/models/building-office-tall.glb +0 -0
- package/dist/ingest/simcity/models/building-office.glb +0 -0
- package/dist/ingest/simcity/models/building-restaurant.glb +0 -0
- package/dist/ingest/simcity/models/building-skyscraper.glb +0 -0
- package/dist/ingest/simcity/models/car-ambulance-pickup.glb +0 -0
- package/dist/ingest/simcity/models/car-baywatch.glb +0 -0
- package/dist/ingest/simcity/models/car-hippie-van.glb +0 -0
- package/dist/ingest/simcity/models/car-passenger-race.glb +0 -0
- package/dist/ingest/simcity/models/car-passenger.glb +0 -0
- package/dist/ingest/simcity/models/car-police.glb +0 -0
- package/dist/ingest/simcity/models/car-taxi.glb +0 -0
- package/dist/ingest/simcity/models/car-tow-truck.glb +0 -0
- package/dist/ingest/simcity/models/car-truck-dump.glb +0 -0
- package/dist/ingest/simcity/models/car-veteran.glb +0 -0
- package/dist/ingest/simcity/models/construction-small.glb +0 -0
- package/dist/ingest/simcity/models/data-center.glb +0 -0
- package/dist/ingest/simcity/models/industry-factory-old.glb +0 -0
- package/dist/ingest/simcity/models/industry-factory.glb +0 -0
- package/dist/ingest/simcity/models/industry-refinery.glb +0 -0
- package/dist/ingest/simcity/models/industry-warehouse.glb +0 -0
- package/dist/ingest/simcity/models/power_line_pole_modified.glb +0 -0
- package/dist/ingest/simcity/models/tile-plain_grass.glb +0 -0
- package/dist/ingest/simcity/models/tile-road-curve.glb +0 -0
- package/dist/ingest/simcity/models/tile-road-end.glb +0 -0
- package/dist/ingest/simcity/models/tile-road-intersection-t.glb +0 -0
- package/dist/ingest/simcity/models/tile-road-intersection.glb +0 -0
- package/dist/ingest/simcity/models/tile-road-straight.glb +0 -0
- package/dist/ingest/simcity/models/truck.glb +0 -0
- package/dist/ingest/simcity/statusIcons/no-power.png +0 -0
- package/dist/ingest/simcity/statusIcons/no-road-access.png +0 -0
- package/dist/ingest/simcity/textures/base.png +0 -0
- package/dist/ingest/simcity/textures/grid.png +0 -0
- package/dist/ingest/simcity/textures/specular.png +0 -0
- package/dist/ingest/simcity/three-r155.module.js +51466 -0
- package/dist/ingest/simcity/vgai.game.json +20 -0
- package/dist/ingest/tanks/UPSTREAM.md +69 -0
- package/dist/ingest/tanks/index.html +214 -0
- package/dist/ingest/tanks/models/pixelTank.fbx +0 -0
- package/dist/ingest/tanks/tanks-entry.js +92 -0
- package/dist/ingest/tanks/three-r170.module.js +54571 -0
- package/dist/ingest/tanks/vgai.game.json +20 -0
- package/dist/inputmaps/default.inputmap.json +41 -0
- package/dist/jsm/libs/draco/gltf/draco_decoder.js +33 -0
- package/dist/jsm/libs/draco/gltf/draco_decoder.wasm +0 -0
- package/dist/jsm/libs/draco/gltf/draco_wasm_wrapper.js +116 -0
- package/dist/scenes/untitled.vscn.json +29 -0
- package/dist/textures/waternormals.jpg +0 -0
- package/dist-server/packaged.mjs +18155 -0
- package/package.json +36 -0
- package/server/asset-library-routes.ts +833 -0
- package/server/canonical-path.ts +50 -0
- package/server/cloud-asset-catalog.ts +101 -0
- package/server/dev.ts +350 -0
- package/server/editor-server.ts +1537 -0
- package/server/editor-sse.ts +73 -0
- package/server/game-globals-shadow.ts +58 -0
- package/server/local-asset-catalog.ts +163 -0
- package/server/packaged.ts +571 -0
- package/server/prod.ts +75 -0
- package/server/project-optimize-deps-entries.ts +149 -0
- package/server/project-view.ts +65 -0
- package/server/server-utils.ts +430 -0
- package/server/session-registry.ts +93 -0
- package/src/EditorContext.tsx +87 -0
- package/src/achieved-tier.ts +126 -0
- package/src/action-registry.ts +268 -0
- package/src/adapter-resolver.ts +1858 -0
- package/src/asset-events.ts +100 -0
- package/src/authoring/active-adapter.ts +98 -0
- package/src/authoring/active-systems.ts +50 -0
- package/src/authoring/boundary-authoring-adapter.ts +178 -0
- package/src/authoring/composite-authoring-adapter.ts +891 -0
- package/src/authoring/design-time-layers.ts +776 -0
- package/src/authoring/edit-mode-authoring.ts +643 -0
- package/src/authoring/embed-only-adapter.ts +57 -0
- package/src/authoring/ephemeral-persistence.ts +26 -0
- package/src/authoring/eyedropper-session.ts +69 -0
- package/src/authoring/first-party-hierarchy-menu.ts +74 -0
- package/src/authoring/first-party-inspector/AddSectionMenu.tsx +118 -0
- package/src/authoring/first-party-inspector/EnvironmentSection.tsx +550 -0
- package/src/authoring/first-party-inspector/HeaderSection.tsx +329 -0
- package/src/authoring/first-party-inspector/is-first-party-selection.ts +44 -0
- package/src/authoring/first-party-inspector/register.tsx +108 -0
- package/src/authoring/ingest-authoring-adapter.ts +475 -0
- package/src/authoring/ingest-capture.ts +94 -0
- package/src/authoring/ingest-game-adapter.ts +291 -0
- package/src/authoring/ingest-iframe-adapter.ts +38 -0
- package/src/authoring/ingest-iframe-reachable-adapter.ts +401 -0
- package/src/authoring/ingest-overlay.ts +193 -0
- package/src/authoring/layered-pick.ts +87 -0
- package/src/authoring/mount-failure-report.ts +112 -0
- package/src/authoring/overlay-report.ts +52 -0
- package/src/authoring/project-scenes.ts +111 -0
- package/src/authoring/react-dom-authoring-adapter.ts +795 -0
- package/src/authoring/react-inspector-section.tsx +1041 -0
- package/src/authoring/react-world-authoring-adapter.ts +2400 -0
- package/src/authoring/root-activation.ts +61 -0
- package/src/authoring/scene-ui-layer.ts +485 -0
- package/src/authoring/story-overlay.ts +48 -0
- package/src/authoring/ui-authoring-adapter.ts +325 -0
- package/src/authoring/ui-hierarchy-menu.ts +40 -0
- package/src/authoring/ui-inspector-sections.tsx +156 -0
- package/src/authoring/undo-timeline.ts +145 -0
- package/src/authoring/vgai-scene-authoring-adapter.ts +746 -0
- package/src/authoring/viewport-pick-context.ts +31 -0
- package/src/authoring/viewport-raycast.ts +80 -0
- package/src/authoring/world-overlay.ts +76 -0
- package/src/authoring/world-pan-state.ts +115 -0
- package/src/authoring/world-session-state.ts +102 -0
- package/src/authoring/world2d-authoring-adapter.ts +313 -0
- package/src/browser-external-watch.ts +98 -0
- package/src/browser-play-entry.ts +112 -0
- package/src/browser-project-scripts.ts +112 -0
- package/src/browser-transpile.ts +246 -0
- package/src/browser-watch-reload.ts +100 -0
- package/src/command-listener.ts +326 -0
- package/src/components/AlignToolbar.tsx +189 -0
- package/src/components/AppRoot.tsx +73 -0
- package/src/components/AssetBrowser.tsx +502 -0
- package/src/components/BottomPanel.tsx +326 -0
- package/src/components/BuildPanel.tsx +455 -0
- package/src/components/CameraInfo.tsx +56 -0
- package/src/components/CommandPalette.tsx +265 -0
- package/src/components/ConsolePanel.tsx +272 -0
- package/src/components/DataPanel.tsx +1268 -0
- package/src/components/DefaultEditorLayout.tsx +509 -0
- package/src/components/FolderTree.tsx +179 -0
- package/src/components/GameHierarchy.tsx +1349 -0
- package/src/components/Inspector.tsx +671 -0
- package/src/components/InspectorToolSection.tsx +74 -0
- package/src/components/MountFailureBanner.tsx +110 -0
- package/src/components/NetworkSection.tsx +109 -0
- package/src/components/OnlineAssetBrowser.tsx +448 -0
- package/src/components/OverlayReportBanner.tsx +122 -0
- package/src/components/PlayBar.tsx +356 -0
- package/src/components/PrefabSaveDialog.tsx +201 -0
- package/src/components/ProjectHeader.tsx +174 -0
- package/src/components/ProjectScreen.tsx +1004 -0
- package/src/components/RightPanel.tsx +166 -0
- package/src/components/RootActivateDialog.tsx +105 -0
- package/src/components/SaveDialog.tsx +197 -0
- package/src/components/SaveStatus.tsx +69 -0
- package/src/components/StatsOverlay.tsx +35 -0
- package/src/components/ToolHost.tsx +80 -0
- package/src/components/Toolbar.tsx +307 -0
- package/src/components/ViewportOverlay.tsx +234 -0
- package/src/components/ViewportPanel.tsx +369 -0
- package/src/components/WorldContextMenu.tsx +257 -0
- package/src/components/WorldSelectionOverlay.tsx +2003 -0
- package/src/components/WorldTextEditor.tsx +86 -0
- package/src/components/asset-viewers/AssetViewer.tsx +38 -0
- package/src/components/asset-viewers/AudioViewer.tsx +149 -0
- package/src/components/asset-viewers/GenericJsonViewer.tsx +77 -0
- package/src/components/asset-viewers/ImageViewer.tsx +65 -0
- package/src/components/asset-viewers/InputMapViewer.tsx +206 -0
- package/src/components/asset-viewers/ModelViewer.tsx +180 -0
- package/src/components/asset-viewers/OnlineAssetDetail.tsx +236 -0
- package/src/components/data-csv.ts +301 -0
- package/src/components/entity-name-commit.ts +18 -0
- package/src/components/inspector-property-grouping.ts +65 -0
- package/src/components/inspector-widgets/AlignmentGrid.tsx +188 -0
- package/src/components/inspector-widgets/BorderEditor.tsx +305 -0
- package/src/components/inspector-widgets/ColorPicker.tsx +548 -0
- package/src/components/inspector-widgets/FilterEditor.tsx +108 -0
- package/src/components/inspector-widgets/FontPicker.tsx +196 -0
- package/src/components/inspector-widgets/GradientEditor.tsx +277 -0
- package/src/components/inspector-widgets/ScrubbableInput.tsx +152 -0
- package/src/components/inspector-widgets/ShadowEditor.tsx +332 -0
- package/src/components/inspector-widgets/color-utils.ts +202 -0
- package/src/components/inspector-widgets/gallery-main.tsx +167 -0
- package/src/components/inspector-widgets/index.ts +22 -0
- package/src/components/inspector-widgets/shared.tsx +135 -0
- package/src/components/inspectors/AnimationSection.tsx +143 -0
- package/src/components/inspectors/AudioSection.tsx +215 -0
- package/src/components/inspectors/BoneAttachmentSection.tsx +82 -0
- package/src/components/inspectors/CameraSection.tsx +105 -0
- package/src/components/inspectors/ComponentsSection.tsx +358 -0
- package/src/components/inspectors/JointsSection.tsx +255 -0
- package/src/components/inspectors/LightSection.tsx +116 -0
- package/src/components/inspectors/MaterialSection.tsx +1189 -0
- package/src/components/inspectors/MeshSection.tsx +99 -0
- package/src/components/inspectors/NavigationSection.tsx +58 -0
- package/src/components/inspectors/ParticleSection.tsx +681 -0
- package/src/components/inspectors/PhysicsSection.tsx +326 -0
- package/src/components/inspectors/PostProcessingSection.tsx +1290 -0
- package/src/components/inspectors/RenderingSection.tsx +109 -0
- package/src/components/inspectors/ShadowSection.tsx +84 -0
- package/src/components/inspectors/SplineSection.tsx +191 -0
- package/src/components/inspectors/TransformSection.tsx +191 -0
- package/src/components/inspectors/json-schema-fields.ts +281 -0
- package/src/components/inspectors/multi-edit-utils.ts +49 -0
- package/src/components/inspectors/zod-fields.ts +184 -0
- package/src/components/primitives/ColorInput.tsx +109 -0
- package/src/components/primitives/JsonInput.tsx +98 -0
- package/src/components/primitives/NumberInput.tsx +164 -0
- package/src/components/primitives/Panel.tsx +68 -0
- package/src/components/primitives/SectionHeader.tsx +45 -0
- package/src/components/primitives/Tooltip.tsx +82 -0
- package/src/components/primitives/Vec3Input.tsx +62 -0
- package/src/components/world-overlay-gestures.ts +1438 -0
- package/src/doctor/data-checks.ts +285 -0
- package/src/doctor/report.ts +386 -0
- package/src/editor-api.ts +700 -0
- package/src/editor-console.ts +194 -0
- package/src/editor-hotkeys.ts +441 -0
- package/src/editor-layers.ts +2 -0
- package/src/editor-mode.ts +28 -0
- package/src/editor-store.ts +2225 -0
- package/src/editor-viewport.ts +1924 -0
- package/src/entity-factory.ts +221 -0
- package/src/game-globals-prelude.ts +27 -0
- package/src/gated-globals.ts +131 -0
- package/src/generated/setup-by-example.generated.ts +53 -0
- package/src/gizmo-registry.ts +608 -0
- package/src/handle-system.ts +221 -0
- package/src/hierarchy-menu-registry.ts +70 -0
- package/src/hierarchy-rows.ts +73 -0
- package/src/hosted-example.ts +160 -0
- package/src/hotkeys.ts +142 -0
- package/src/ingest/discovery-react.ts +132 -0
- package/src/ingest/discovery.ts +205 -0
- package/src/ingest/discovery2d.ts +204 -0
- package/src/ingest/esm-entry-resolution.ts +102 -0
- package/src/ingest/games/bloom-composer/game.js +99 -0
- package/src/ingest/games/bloom-composer/vgai.game.json +21 -0
- package/src/ingest/games/embed-bundle/embed.html +10 -0
- package/src/ingest/games/embed-bundle/vgai.game.json +21 -0
- package/src/ingest/games/games-fps/assets/collision-world.glb +0 -0
- package/src/ingest/games/games-fps/game.js +479 -0
- package/src/ingest/games/games-fps/vgai.game.json +24 -0
- package/src/ingest/games/iframe-reachable/game-source.js +58 -0
- package/src/ingest/games/iframe-reachable/vgai.game.json +21 -0
- package/src/ingest/games/interactive-cubes/game.js +139 -0
- package/src/ingest/games/interactive-cubes/vgai.game.json +21 -0
- package/src/ingest/games/mismatched-three/fake-three.js +38 -0
- package/src/ingest/games/mismatched-three/game.js +10 -0
- package/src/ingest/games/mismatched-three/vgai.game.json +22 -0
- package/src/ingest/games/own-three-bundled/game.js +43 -0
- package/src/ingest/games/own-three-bundled/vgai.game.json +21 -0
- package/src/ingest/games/pointerlock-maze/game.js +288 -0
- package/src/ingest/games/pointerlock-maze/vgai.game.json +22 -0
- package/src/ingest/games-2d/_fixtures/first-party-scene.scn2d.json +124 -0
- package/src/ingest/games-2d/bubbo-bubbo/vgai.game.json +25 -0
- package/src/ingest/games-2d/bunnymark/game.ts +88 -0
- package/src/ingest/games-2d/bunnymark/vgai.game.json +21 -0
- package/src/ingest/games-2d/flappy/game.ts +148 -0
- package/src/ingest/games-2d/flappy/vgai.game.json +21 -0
- package/src/ingest/games-2d/flappy-pixi/vgai.game.json +25 -0
- package/src/ingest/games-2d/puzzling-potions/vgai.game.json +25 -0
- package/src/ingest/games-2d/tilemap-camera/game.ts +115 -0
- package/src/ingest/games-2d/tilemap-camera/vgai.game.json +21 -0
- package/src/ingest/games-react/react-rpg/entry.tsx +185 -0
- package/src/ingest/games-react/react-rpg/shims.d.ts +26 -0
- package/src/ingest/games-react/react-rpg/src/ui/game-state.tsx +129 -0
- package/src/ingest/games-react/react-rpg/vgai.game.json +21 -0
- package/src/ingest/host-namespace-registry.ts +196 -0
- package/src/ingest/types.ts +127 -0
- package/src/ingest-hierarchy-rows.ts +207 -0
- package/src/ingest-mode.ts +2092 -0
- package/src/ingest-siblings.ts +466 -0
- package/src/initial-scene.ts +194 -0
- package/src/inspector-section-registry.ts +117 -0
- package/src/interactive-edit-scope.ts +34 -0
- package/src/main.tsx +23 -0
- package/src/manifest-project.ts +43 -0
- package/src/model-thumbnail.ts +156 -0
- package/src/module-mode.ts +513 -0
- package/src/navmesh-handler.ts +94 -0
- package/src/packaged-runtime.ts +49 -0
- package/src/play-mode.ts +1148 -0
- package/src/playground/main.ts +206 -0
- package/src/project-manager.ts +173 -0
- package/src/scene-diff.ts +112 -0
- package/src/scene-serializer.ts +144 -0
- package/src/scene-sync.ts +587 -0
- package/src/scene-ui-mode/active-scene-ui.ts +58 -0
- package/src/scene-ui-mode/scene-ui-palette-panel.tsx +152 -0
- package/src/scene-ui-mode/scene-ui-timeline-tab.tsx +29 -0
- package/src/scene-ui-mode/ui-prefab-dialog-host.tsx +74 -0
- package/src/scene-ui-mode/ui-prefab-hierarchy-menu.ts +49 -0
- package/src/scene-ui-mode/ui-prefab-io.ts +76 -0
- package/src/script-loader.ts +143 -0
- package/src/storage/fsa-storage.ts +55 -0
- package/src/storage/handle-storage.ts +150 -0
- package/src/storage/handle-store.ts +116 -0
- package/src/storage/http-storage.ts +137 -0
- package/src/storage/index.ts +147 -0
- package/src/storage/mem-storage.ts +135 -0
- package/src/storage/opfs-storage.ts +25 -0
- package/src/storage/paths.ts +26 -0
- package/src/storage/seed.ts +106 -0
- package/src/storage/types.ts +110 -0
- package/src/tool-loader.ts +291 -0
- package/src/ui-editor/inspector.tsx +563 -0
- package/src/ui-editor/node-ops.ts +266 -0
- package/src/ui-editor/overlay.tsx +380 -0
- package/src/ui-editor/palette.tsx +114 -0
- package/src/ui-editor/react-store.ts +78 -0
- package/src/ui-editor/timeline-panel.tsx +443 -0
- package/src/ui-editor/ui-edit-store.ts +552 -0
- package/src/ui-editor/ui-prefab.ts +95 -0
- package/src/ui-source/inspect.ts +544 -0
- package/src/ui-source/oid-transform.ts +133 -0
- package/src/ui-source/source-write-backend.ts +203 -0
- package/src/ui-source/tailwind-map.ts +174 -0
- package/src/ui-source/writer.ts +1175 -0
- package/src/widgets/index.ts +73 -0
- package/template/.claude/skills/editor/SKILL.md +252 -0
- package/template/CLAUDE.md +512 -0
- package/template/LICENSE +30 -0
- package/template/emit-schemas.ts +42 -0
- package/template/index.html +17 -0
- package/template/package.json +53 -0
- package/template/project.json +9 -0
- package/template/public/audio/ambient-arena.wav +0 -0
- package/template/public/audio/ambient-music.wav +0 -0
- package/template/public/audio/ambient-nature.wav +0 -0
- package/template/public/audio/ambient-wind.wav +0 -0
- package/template/public/inputmaps/default.inputmap.json +41 -0
- package/template/public/jsm/libs/draco/gltf/draco_decoder.js +33 -0
- package/template/public/jsm/libs/draco/gltf/draco_decoder.wasm +0 -0
- package/template/public/jsm/libs/draco/gltf/draco_encoder.js +33 -0
- package/template/public/jsm/libs/draco/gltf/draco_wasm_wrapper.js +116 -0
- package/template/public/scenes/default.vscn.json +43 -0
- package/template/public/scenes/untitled.vscn.json +29 -0
- package/template/public/textures/waternormals.jpg +0 -0
- package/template/server/colyseus-setup.ts +85 -0
- package/template/server/main.ts +34 -0
- package/template/server/rooms/arena-room.ts +420 -0
- package/template/server/rooms/game-room.ts +162 -0
- package/template/server/rooms.ts +15 -0
- package/template/server/tsconfig.json +19 -0
- package/template/src/data/README.md +40 -0
- package/template/src/data/assets.ts +31 -0
- package/template/src/data/tuning.data.json +5 -0
- package/template/src/data/tuning.schema.json +21 -0
- package/template/src/data/tuning.schema.ts +34 -0
- package/template/src/data/tuning.ts +24 -0
- package/template/src/main.ts +157 -0
- package/template/src/scripts/components/data-spinner.ts +45 -0
- package/template/src/scripts/components/scene-camera.ts +40 -0
- package/template/src/scripts/main.ts +94 -0
- package/template/src/scripts/registry.ts +46 -0
- package/template/src/tools/README.md +14 -0
- package/template/src/tools/example.tool.tsx +91 -0
- package/template/src/tools/spin.tool.tsx +101 -0
- package/template/src/ui/Button.tsx +27 -0
- package/template/src/ui/DefaultHud.tsx +40 -0
- package/template/src/ui/KeyHint.tsx +25 -0
- package/template/src/ui/Panel.tsx +52 -0
- package/template/src/ui/RebindPanel.tsx +129 -0
- package/template/src/ui/Slider.tsx +28 -0
- package/template/src/ui/StatusDot.tsx +22 -0
- package/template/src/ui/Toggle.tsx +19 -0
- package/template/src/ui/game-state.tsx +37 -0
- package/template/src/ui/index.ts +7 -0
- package/template/src/ui/mount-default-hud.tsx +19 -0
- package/template/src/ui/mount-ui.ts +15 -0
- package/template/tsconfig.json +36 -0
- package/template/validate-asset-content.ts +78 -0
- package/template/validate-manifest.ts +55 -0
- package/template/validate-scenes.ts +211 -0
- package/template/vgai.game.json +21 -0
- package/template/vite.config.ts +128 -0
- package/vite-plugin-example-assets.ts +60 -0
- package/vite-plugin-game-globals.ts +46 -0
- package/vite-plugin-game-static.ts +71 -0
- package/vite-plugin-project-game-static.ts +84 -0
- package/vite-plugin-react-game-static.ts +98 -0
- package/vite-plugin-react-world-provider.ts +81 -0
- package/vite-plugin-react-world-runtime.ts +76 -0
- package/vite-plugin-ui-oid.ts +561 -0
|
@@ -0,0 +1,1858 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter resolver — T3.3 slice 2 (docs/CLI-ON-FOLDER-DESIGN.md §1D), extended
|
|
3
|
+
* in T6.1 slice 2 (docs/MULTI-WORLD-DESIGN.md §1.D/§3 slice 2) for `default-pixi`
|
|
4
|
+
* + the multi-world resolution surface, in T6.1 slice 4 (§1.E/§3 slice 4)
|
|
5
|
+
* for `ingest-pixi` (world2d silo retirement), in T6.2 (docs/REACT-WORLD-
|
|
6
|
+
* DESIGN.md §1.B/§3 slice 1) for `default-react`, in T3.4 slice 1
|
|
7
|
+
* (docs/ADAPTER-AUTHORING-DESIGN.md §1.A/§1.C) for `{ module }` (custom
|
|
8
|
+
* adapter modules) + achieved-tier verification, and in Track N task N1
|
|
9
|
+
* (docs/REACT-INGEST-LANDING-DESIGN.md D-N2/D-N3/D-N4) for `ingest-react`
|
|
10
|
+
* (vendored native-React game ingestion — the deduped-only mount).
|
|
11
|
+
*
|
|
12
|
+
* Turns one manifest world (`ResolvedWorldEntry`, from `@engine/manifest/load`)
|
|
13
|
+
* into a mountable {@link GameAdapter} (or, for a pixijs world, a structurally
|
|
14
|
+
* distinct {@link Pixi2DGameAdapter} — see the default-pixi section below).
|
|
15
|
+
* Lives editor-side, not in the engine package: it needs `/@fs/` dynamic-import
|
|
16
|
+
* mechanics (the same route `loadProjectScripts` uses, `./script-loader.ts`),
|
|
17
|
+
* and the engine's manifest loader deliberately has no fs/vite dependency
|
|
18
|
+
* (`manifest/load.ts`'s no-fs discipline — the pure identity-resolution half
|
|
19
|
+
* stays importable anywhere, including Node CLI code; this file is the
|
|
20
|
+
* instantiation half).
|
|
21
|
+
*
|
|
22
|
+
* Coverage in this slice (per the design docs' staging):
|
|
23
|
+
* - `default-three` -> a configured `VgaiSceneGameAdapter`, WITH
|
|
24
|
+
* `componentRegistry` threaded from the project's `src/scripts/registry.ts`
|
|
25
|
+
* (closes the T7.2 finding: `sceneData`/`scenePath` never passed a
|
|
26
|
+
* registry, so scene-authored NAMED components were unreachable through
|
|
27
|
+
* those two `VgaiSceneConfig` branches).
|
|
28
|
+
* - `default-pixi` -> a configured `PixiSceneGameAdapter` (T6.1 slice 2, see
|
|
29
|
+
* `resolveDefaultPixiAdapter` below for the `scene`/`entry` split and the
|
|
30
|
+
* `entry`-for-pixi adjudication).
|
|
31
|
+
* - `ingest-three` -> see `resolveIngestDescriptor`'s doc comment below for
|
|
32
|
+
* the shape landed on and the structural finding it reports.
|
|
33
|
+
* - `ingest-pixi` -> see `resolveIngest2DDescriptor`'s doc comment below (T6.1
|
|
34
|
+
* slice 4) — same two-layer shape as ingest-three (descriptor builder +
|
|
35
|
+
* a named-stub `GameAdapter`; the real mount is `ingest-mode.ts`'s
|
|
36
|
+
* `enterIngestModeFromManifestWorld2D`).
|
|
37
|
+
* - `default-react` -> a `ReactWorldAdapter` (T6.2, see
|
|
38
|
+
* `resolveDefaultReactAdapter` below for the `entry`-only rule — `scene`
|
|
39
|
+
* is a named error, D4 — and the two engine-seam findings its doc
|
|
40
|
+
* comment reports).
|
|
41
|
+
* - `ingest-react` -> also a `ReactWorldAdapter` (Track N, N1 —
|
|
42
|
+
* docs/REACT-INGEST-LANDING-DESIGN.md D-N2/D-N3, see
|
|
43
|
+
* `resolveIngestReactAdapter` below): same T6.2 mount machinery as
|
|
44
|
+
* `default-react`, restricted to the `'deduped'` strategy rung (a named
|
|
45
|
+
* error for any other strategy) with `entry` required (the D-N2 host-shim
|
|
46
|
+
* module) — and, per D-N8, no authoring adapter anywhere in v1.
|
|
47
|
+
* - `{ module }` -> `/@fs/`-imports the game folder's declared adapter module
|
|
48
|
+
* (path-guarded, shape-checked) — see `resolveModuleAdapter` below (T3.4
|
|
49
|
+
* slice 1). Achieved-tier verification (post-mount, against the actual
|
|
50
|
+
* `MountedGame` surface) lives in `./achieved-tier.ts`, shared with the
|
|
51
|
+
* ingest embed-only degradation path's retrofit (`ingest-mode.ts`).
|
|
52
|
+
*
|
|
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
|
|
55
|
+
* `ResolvedGameManifest` into slice-1 `WorldMountSpec`s, enforcing the v1
|
|
56
|
+
* scope rule for manifests with more than one world (docs/MULTI-WORLD-
|
|
57
|
+
* DESIGN.md §3 slice 4, docs/REACT-WORLD-DESIGN.md §3 for react's admission).
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
import type { GameAdapter } from '@engine/adapter';
|
|
61
|
+
import { VgaiSceneGameAdapter } from '@engine/adapter';
|
|
62
|
+
import { assertNever } from '@engine/adapter/world-kind';
|
|
63
|
+
import { resolveUrl } from '@engine/loader';
|
|
64
|
+
import type { ResolvedGameManifest, ResolvedWorldEntry } from '@engine/manifest/load';
|
|
65
|
+
import { GameProvider as EngineGameProvider } from '@engine/react/game-state';
|
|
66
|
+
import type {
|
|
67
|
+
MountedReactGame,
|
|
68
|
+
Pixi2DGameAdapter,
|
|
69
|
+
ReactWorldAdapter,
|
|
70
|
+
ReactWorldHost,
|
|
71
|
+
WorldMountSpec,
|
|
72
|
+
} from '@engine/runtime/create-runtime';
|
|
73
|
+
import type { Game } from '@engine/runtime/game';
|
|
74
|
+
import type { MountEntry } from '@engine/runtime/mount-manifest';
|
|
75
|
+
import type { EditorPreview } from '@engine/runtime/types';
|
|
76
|
+
import type { ComponentRegistry } from '@engine/scene/component-registry';
|
|
77
|
+
import { SceneParseError } from '@engine/scene/parse';
|
|
78
|
+
import type { IngestGame2D } from '@engine/world2d/ingest2d';
|
|
79
|
+
import { PixiSceneGameAdapter, type World2DSetupFn } from '@engine/world2d/pixi-game-adapter';
|
|
80
|
+
import { type Scene2DFile, Scene2DSchema } from '@engine/world2d/schema/entity2d';
|
|
81
|
+
import type { ProjectUIRegistry } from '@vgai/scene-ui/registry';
|
|
82
|
+
import { type ComponentType, createElement } from 'react';
|
|
83
|
+
import { flushSync } from 'react-dom';
|
|
84
|
+
import { createRoot } from 'react-dom/client';
|
|
85
|
+
import { reactWorldProviderImportSpecifier } from '../vite-plugin-react-world-provider';
|
|
86
|
+
import { REACT_WORLD_RUNTIME_PATH } from '../vite-plugin-react-world-runtime';
|
|
87
|
+
import {
|
|
88
|
+
computeAchievedTier,
|
|
89
|
+
computeTierShortfall,
|
|
90
|
+
formatTierShortfallMessage,
|
|
91
|
+
} from './achieved-tier';
|
|
92
|
+
import { resolveExtraDeps } from './ingest/host-namespace-registry';
|
|
93
|
+
import type { IngestGame } from './ingest/types';
|
|
94
|
+
import { isPackagedRuntime } from './packaged-runtime';
|
|
95
|
+
import { loadProjectScripts } from './script-loader';
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Build a `/@fs/<absolute path>` dev-server import URL WITHOUT a doubled
|
|
99
|
+
* leading slash (T6.2 slice 3 finding — see the "Report back" of that
|
|
100
|
+
* slice's dispatch). `projectRoot` is always an absolute path (it starts
|
|
101
|
+
* with `/`), so the naive `` `/@fs/${projectRoot}/...` `` template produces
|
|
102
|
+
* `/@fs//abs/olute/path/...` — cosmetically harmless on its own (Vite's
|
|
103
|
+
* `/@fs/` middleware tolerates it), UNTIL the SAME file is also reached via
|
|
104
|
+
* a DIFFERENT route: a react world's entry module's own STATIC relative
|
|
105
|
+
* import (e.g. `import { useGameState } from './src/ui/game-
|
|
106
|
+
* state'`) is rewritten by Vite's normal resolver into a SINGLE-slash,
|
|
107
|
+
* symlink-canonicalized `/@fs/` URL — a textually different string than
|
|
108
|
+
* this file's hand-built double-slash one. Browser ES module identity is
|
|
109
|
+
* per-URL, so the two builds of `/@fs/` string for the SAME file produced
|
|
110
|
+
* TWO SEPARATE module instances (two `createContext()` calls for
|
|
111
|
+
* `game-state.tsx`), and `useGameState`'s `useGame()` threw "no Game in
|
|
112
|
+
* context" even though `<GameProvider>` genuinely wrapped the entry — this
|
|
113
|
+
* is what `resolveDefaultReactAdapter`'s AC e2e (T6.2 slice 3) caught. The
|
|
114
|
+
* server-side half of this fix is `canonical-path.ts` (canonicalizes
|
|
115
|
+
* `projectRoot` itself so it agrees with Vite's own symlink-resolved form);
|
|
116
|
+
* this half removes the remaining single/double-slash divergence so the
|
|
117
|
+
* two strings are BYTE-IDENTICAL once `projectRoot` is canonical.
|
|
118
|
+
*/
|
|
119
|
+
export function fsImportPath(projectRoot: string, relativePath: string): string {
|
|
120
|
+
const root = projectRoot.endsWith('/') ? projectRoot.slice(0, -1) : projectRoot;
|
|
121
|
+
const rel = relativePath.startsWith('/') ? relativePath.slice(1) : relativePath;
|
|
122
|
+
// Windows canonical roots ('C:/…') carry no leading slash of their own —
|
|
123
|
+
// without inserting one the URL degenerates to '/@fsC:/…', which Vite
|
|
124
|
+
// serves as a 404. POSIX roots keep the concat form (root already starts
|
|
125
|
+
// with '/'), preserving the byte-identical contract described above.
|
|
126
|
+
return root.startsWith('/') ? `/@fs${root}/${rel}` : `/@fs/${root}/${rel}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Optional editor-session context for {@link resolveWorldAdapter}. */
|
|
130
|
+
export interface ResolveWorldAdapterOptions {
|
|
131
|
+
/**
|
|
132
|
+
* The editor's play-launch context (live in-memory scene document +
|
|
133
|
+
* viewport-camera spawn) — supplied by `play-mode.ts` when the resolver is
|
|
134
|
+
* invoked from the editor's play path, absent for non-editor callers (the
|
|
135
|
+
* CLI-on-folder flow). Threaded into BOTH `default-three` branches:
|
|
136
|
+
* the `entry` branch hands it to the project's `setup(ctx, editorPreview)`
|
|
137
|
+
* exactly like today's legacy play path (`createGameRuntime({ setup,
|
|
138
|
+
* editorPreview })` is the same `VgaiSceneGameAdapter` wrap), and the
|
|
139
|
+
* scene-only branch mounts `editorPreview.sceneData` (the LIVE document,
|
|
140
|
+
* unsaved edits included) instead of fetching `world.scene` from disk —
|
|
141
|
+
* matching the documented play-mode contract ("hands the in-memory scene
|
|
142
|
+
* document to the real engine runtime") the way `template/src/scripts/
|
|
143
|
+
* main.ts`'s generic fallback does (`editor ? loadSceneFromData(editor.
|
|
144
|
+
* sceneData) : loadScene(...)`).
|
|
145
|
+
*/
|
|
146
|
+
editorPreview?: EditorPreview | undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Part B — componentRegistry via the project's script registry
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Load the project's component registry (`src/scripts/registry.ts`'s
|
|
155
|
+
* `componentRegistry` export) via the same `/@fs/` dev-server route
|
|
156
|
+
* `loadProjectScripts` uses for `src/scripts/main.ts` (script-loader.ts).
|
|
157
|
+
*
|
|
158
|
+
* A project with no scripts registry — the file doesn't exist, doesn't export
|
|
159
|
+
* `componentRegistry`, or fails to import for any other reason — resolves to
|
|
160
|
+
* an EMPTY registry rather than throwing: most manifest-backed projects
|
|
161
|
+
* (especially ones with no named `GameComponent`s at all) have nothing here,
|
|
162
|
+
* and that must not block `default-three` resolution. This intentionally
|
|
163
|
+
* folds "file absent" and "file present but broken" into the same empty
|
|
164
|
+
* result; a project whose registry.ts has a real bug still resolves and
|
|
165
|
+
* mounts, just without any named components reachable from scene JSON
|
|
166
|
+
* (the same as today's behavior before this slice existed at all).
|
|
167
|
+
*/
|
|
168
|
+
export async function loadComponentRegistryForProject(
|
|
169
|
+
projectRoot: string,
|
|
170
|
+
): Promise<ComponentRegistry> {
|
|
171
|
+
const modulePath = fsImportPath(projectRoot, 'src/scripts/registry.ts');
|
|
172
|
+
try {
|
|
173
|
+
const mod = (await import(/* @vite-ignore */ modulePath)) as {
|
|
174
|
+
componentRegistry?: ComponentRegistry;
|
|
175
|
+
};
|
|
176
|
+
return mod.componentRegistry ?? {};
|
|
177
|
+
} catch {
|
|
178
|
+
return {};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
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(
|
|
214
|
+
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;
|
|
229
|
+
try {
|
|
230
|
+
const mod = (await import(/* @vite-ignore */ modulePath)) as {
|
|
231
|
+
uiRegistry?: ProjectUIRegistry;
|
|
232
|
+
};
|
|
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;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
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
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
// Named errors for identities not yet resolvable (report the task, not just fail)
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
// Part A — default-three
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
|
|
276
|
+
async function resolveDefaultThreeAdapter(
|
|
277
|
+
world: ResolvedWorldEntry,
|
|
278
|
+
projectRoot: string,
|
|
279
|
+
opts?: ResolveWorldAdapterOptions,
|
|
280
|
+
): Promise<GameAdapter> {
|
|
281
|
+
const editorPreview = opts?.editorPreview;
|
|
282
|
+
|
|
283
|
+
// `entry` is preferred when a world declares BOTH `entry` and `scene`: the
|
|
284
|
+
// code entry owns MOUNTING (the same contract `src/scripts/main.ts`
|
|
285
|
+
// satisfies today — it decides what to load, delegates to a matching
|
|
286
|
+
// example's setup for gameplay systems, etc.); `scene` remains the world's
|
|
287
|
+
// data identity (what the compat view's `defaultScene` is synthesized from,
|
|
288
|
+
// project-view.ts §1C). The manifest loader allows both
|
|
289
|
+
// (`checkAdapterSceneEntryRules` only rejects NEITHER).
|
|
290
|
+
if (world.entry !== undefined) {
|
|
291
|
+
// entry-based default-three: W1 (docs/R3F-FIRST-PARTY-DESIGN.md §1.A/§2)
|
|
292
|
+
// accepts EITHER a pre-built `adapter` export OR a `setup` function.
|
|
293
|
+
// Before W1, `loadProjectScripts` hard-threw on any entry lacking
|
|
294
|
+
// `setup`, so an `adapter`-export entry that mounted fine standalone
|
|
295
|
+
// failed here — asymmetry, not doctrine (§0's "ground truth").
|
|
296
|
+
//
|
|
297
|
+
// Precedence when BOTH are exported is `setup` FIRST — the OPPOSITE of
|
|
298
|
+
// the standalone path (`mount-game.ts:176-177` prefers `adapter`), and
|
|
299
|
+
// deliberately so. The template (and every project scaffolded from it)
|
|
300
|
+
// exports both, with `adapter = fromSetup('default', setup)` as the
|
|
301
|
+
// standalone convenience, so standalone's adapter-first is
|
|
302
|
+
// behavior-identical there. In the EDITOR the two paths are NOT
|
|
303
|
+
// equivalent: only the `setup` branch threads `editorPreview` (the live
|
|
304
|
+
// in-memory scene + viewport camera — a bare `GameAdapter.mount(host)`
|
|
305
|
+
// has no channel for it) and mounts through the firstParty-branded
|
|
306
|
+
// `VgaiSceneGameAdapter` that script-HMR's prototype swap requires.
|
|
307
|
+
// Preferring `adapter` here made every scaffolded project silently load
|
|
308
|
+
// its ON-DISK default scene instead of the scene being edited, and
|
|
309
|
+
// killed component HMR (caught by e2e 07-script-hmr +
|
|
310
|
+
// 10-render-fidelity). An adapter-ONLY entry (e.g. a react-three-fiber
|
|
311
|
+
// bridge, §1.C) still returns the project's adapter directly — it owns
|
|
312
|
+
// its full mount lifecycle, is not wrapped in `VgaiSceneGameAdapter`,
|
|
313
|
+
// and has no `setup` to prefer.
|
|
314
|
+
const { adapter, setup } = await loadProjectScripts(projectRoot, world.entry);
|
|
315
|
+
if (setup) {
|
|
316
|
+
// Byte-equivalent to the legacy play path: `createGameRuntime({ setup,
|
|
317
|
+
// editorPreview })` wraps the exact same `VgaiSceneConfig`.
|
|
318
|
+
return new VgaiSceneGameAdapter({ id: world.id, setup, editorPreview });
|
|
319
|
+
}
|
|
320
|
+
// `loadProjectScripts` only returns with neither `adapter` nor `setup`
|
|
321
|
+
// by throwing first, so `adapter` is guaranteed defined here.
|
|
322
|
+
return adapter as GameAdapter;
|
|
323
|
+
}
|
|
324
|
+
if (world.scene !== undefined) {
|
|
325
|
+
// Scene-only world (§1D): no custom setup(). Thread the project's
|
|
326
|
+
// component registry so scene-authored `components:` entries resolve —
|
|
327
|
+
// the T7.2 finding this slice closes. From the editor's play path
|
|
328
|
+
// (editorPreview present), mount the LIVE in-memory document —
|
|
329
|
+
// `VgaiSceneGameAdapter.mount()`'s `scenePath` branch reads
|
|
330
|
+
// `editorPreview` only for the viewport camera and would fetch the
|
|
331
|
+
// on-disk file, silently dropping unsaved edits — so hand it
|
|
332
|
+
// `sceneData` instead (whose branch also honors the viewport camera).
|
|
333
|
+
const componentRegistry = await loadComponentRegistryForProject(projectRoot);
|
|
334
|
+
// The project's scene-UI registry (D9/D11) — read from the CACHE
|
|
335
|
+
// `authoring/scene-ui-layer.ts`'s design-time layer mount populated, not
|
|
336
|
+
// reloaded/re-parsed here. `undefined` (not `null`) when absent/never
|
|
337
|
+
// populated (e.g. a headless/CLI-on-folder resolve, or a scene with no
|
|
338
|
+
// `ui` root) so the spread below omits the key entirely rather than
|
|
339
|
+
// passing an explicit `uiRegistry: null`.
|
|
340
|
+
const uiRegistry = getCachedProjectUIRegistry() ?? undefined;
|
|
341
|
+
if (editorPreview) {
|
|
342
|
+
return new VgaiSceneGameAdapter({
|
|
343
|
+
id: world.id,
|
|
344
|
+
sceneData: editorPreview.sceneData,
|
|
345
|
+
editorPreview,
|
|
346
|
+
componentRegistry,
|
|
347
|
+
uiRegistry,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
return new VgaiSceneGameAdapter({
|
|
351
|
+
id: world.id,
|
|
352
|
+
scenePath: world.scene,
|
|
353
|
+
componentRegistry,
|
|
354
|
+
uiRegistry,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
// Unreachable in practice: `load.ts`'s `checkAdapterSceneEntryRules` requires
|
|
358
|
+
// `scene` or `entry` for every 'default' adapter world at manifest-load time.
|
|
359
|
+
throw new Error(
|
|
360
|
+
`resolveWorldAdapter: world "${world.id}" is default-three with neither \`scene\` nor ` +
|
|
361
|
+
'`entry` — this should have been rejected by manifest validation.',
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
// Part C (T6.1 slice 2) — default-pixi
|
|
367
|
+
// ---------------------------------------------------------------------------
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Fetch + validate a `.scn2d.json` file from `path` (relative to the served
|
|
371
|
+
* project root, exactly like `default-three`'s `scenePath` branch fetches a
|
|
372
|
+
* `.vscn.json` inside `@engine/scene/scene-loader`'s `loadScene` — this file
|
|
373
|
+
* does the analogous fetch itself because `PixiSceneConfig` has no `scenePath`
|
|
374
|
+
* field of its own, only `sceneData` (see `resolveDefaultPixiAdapter` below).
|
|
375
|
+
* `resolveUrl` applies the SAME asset-prefix rewriting raw `fetch()` calls
|
|
376
|
+
* elsewhere in the engine use.
|
|
377
|
+
*
|
|
378
|
+
* A malformed payload throws a `SceneParseError` naming `path`, not a raw
|
|
379
|
+
* `ZodError` — the same T4.6 convention every fetched 3D scene format
|
|
380
|
+
* (`.vscn.json`/`.prefab.json`/`.mat.json`) already follows via
|
|
381
|
+
* `parseSceneFile`/`parsePrefabFile`/`parseMaterialFile`; `.scn2d.json` was
|
|
382
|
+
* the one fetched format still raw-parsing (§7.1-14).
|
|
383
|
+
*
|
|
384
|
+
* Exported (B1, docs/unified-world-editor/26-unified-editor-spec.md §3 B1):
|
|
385
|
+
* `design-time-layers.ts` reuses this exact fetch+validate step for a pixi
|
|
386
|
+
* world's design-time display-only mount, rather than duplicating it.
|
|
387
|
+
*/
|
|
388
|
+
export async function fetchScene2DFile(path: string): Promise<Scene2DFile> {
|
|
389
|
+
const res = await fetch(resolveUrl(path));
|
|
390
|
+
if (!res.ok) {
|
|
391
|
+
throw new Error(
|
|
392
|
+
`resolveWorldAdapter: failed to load world2d scene "${path}" (${res.status} ${res.statusText}).`,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
const result = Scene2DSchema.safeParse(await res.json());
|
|
396
|
+
if (!result.success) throw new SceneParseError(result.error.issues, path);
|
|
397
|
+
return result.data;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Resolve a `kind: 'pixijs'` + `adapter: 'default'` world into a
|
|
402
|
+
* `PixiSceneGameAdapter` (docs/MULTI-WORLD-DESIGN.md §1.D) — the pixi sibling
|
|
403
|
+
* of `resolveDefaultThreeAdapter` above, minus the `editorPreview`/legacy
|
|
404
|
+
* branch: v1 has NO live-document mount for pixi (§1.D — "No editorPreview
|
|
405
|
+
* equivalent in v1: the 2D editor edits flow through the overlay/authoring
|
|
406
|
+
* path, not a live-document preview"), so a pixi world always mounts from its
|
|
407
|
+
* on-disk `scene`/`entry`, editor session or not.
|
|
408
|
+
*
|
|
409
|
+
* `entry`-for-pixi ADJUDICATION (per this slice's study directive): the design
|
|
410
|
+
* doc already answers this — `World2DSetupFn` (`@engine/world2d/
|
|
411
|
+
* pixi-game-adapter.ts`) is an established, exported config shape (`PixiSceneConfig.setup`,
|
|
412
|
+
* re-exported from `@engine/world2d`'s barrel) structurally parallel to
|
|
413
|
+
* `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
|
|
415
|
+
* world). So `entry` resolves via the SAME `/@fs/` dynamic-import route
|
|
416
|
+
* `loadProjectScripts` uses for `default-three`'s entry branch — no new
|
|
417
|
+
* mechanism invented, just a different exported shape (`setup(ctx:
|
|
418
|
+
* World2DContext)` instead of `setup(ctx: GameContext, editorPreview?)`).
|
|
419
|
+
* `loadProjectScripts`'s return type is pinned to the three-shaped
|
|
420
|
+
* `GameSetupFn`; the cast back to `World2DSetupFn` below is a type-level
|
|
421
|
+
* relabeling only — the runtime check (`typeof mod.setup === 'function'`)
|
|
422
|
+
* is shape-agnostic.
|
|
423
|
+
*/
|
|
424
|
+
async function resolveDefaultPixiAdapter(
|
|
425
|
+
world: ResolvedWorldEntry,
|
|
426
|
+
projectRoot: string,
|
|
427
|
+
): Promise<GameAdapter> {
|
|
428
|
+
if (world.entry !== undefined) {
|
|
429
|
+
const { adapter, setup } = await loadProjectScripts(projectRoot, world.entry);
|
|
430
|
+
// W1 (docs/R3F-FIRST-PARTY-DESIGN.md §0.1's kind-parity matrix / §2 "W1b,
|
|
431
|
+
// out of scope") is a `default-three`-only parity fix — pixi has no
|
|
432
|
+
// standalone `adapter`-export factory to match, so an `adapter`-only
|
|
433
|
+
// entry is not a shape this branch can honor. `loadProjectScripts` no
|
|
434
|
+
// longer hard-throws on a `setup`-less module (it now also accepts
|
|
435
|
+
// `adapter`), so that guarantee has to be re-asserted HERE, loudly,
|
|
436
|
+
// instead of letting `undefined` silently reach `PixiSceneGameAdapter`.
|
|
437
|
+
if (!setup) {
|
|
438
|
+
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)\` ` +
|
|
441
|
+
`export${adapter ? ' (an `adapter` export is default-three/R3F-only, W1b)' : ''}.`,
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
return new PixiSceneGameAdapter({
|
|
445
|
+
id: world.id,
|
|
446
|
+
setup: setup as unknown as World2DSetupFn,
|
|
447
|
+
}) as unknown as GameAdapter;
|
|
448
|
+
}
|
|
449
|
+
if (world.scene !== undefined) {
|
|
450
|
+
const sceneData = await fetchScene2DFile(world.scene);
|
|
451
|
+
return new PixiSceneGameAdapter({ id: world.id, sceneData }) as unknown as GameAdapter;
|
|
452
|
+
}
|
|
453
|
+
// Unreachable in practice: `load.ts`'s `checkAdapterSceneEntryRules` requires
|
|
454
|
+
// `scene` or `entry` for every 'default' adapter world at manifest-load time.
|
|
455
|
+
throw new Error(
|
|
456
|
+
`resolveWorldAdapter: world "${world.id}" is default-pixi with neither \`scene\` nor ` +
|
|
457
|
+
'`entry` — this should have been rejected by manifest validation.',
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ---------------------------------------------------------------------------
|
|
462
|
+
// Part F (T6.2 slice — docs/REACT-WORLD-DESIGN.md §1.B/§3 slice 1) — default-react
|
|
463
|
+
// ---------------------------------------------------------------------------
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* The shape a react world's `entry` module must satisfy: default-export a
|
|
467
|
+
* React component (docs/REACT-WORLD-DESIGN.md §1.B — "The world's `entry`
|
|
468
|
+
* module default-exports a React component"). Validated loudly at import
|
|
469
|
+
* time below — a project whose entry exports the wrong shape gets a named
|
|
470
|
+
* error naming the world and the export it actually found, not a cryptic
|
|
471
|
+
* "X is not a function" deep inside `createRoot`/`render`. Shared by BOTH
|
|
472
|
+
* react identities (`default-react` and, since Track N, `ingest-react`) —
|
|
473
|
+
* `identityLabel` only changes the error message's parenthetical so a
|
|
474
|
+
* failure is attributed to the right one; the shape requirement itself is
|
|
475
|
+
* identical (an ingest world's `entry` is a host-shim module, D-N2, but a
|
|
476
|
+
* shim is still just a module default-exporting a component).
|
|
477
|
+
*/
|
|
478
|
+
function loadReactEntryComponent(
|
|
479
|
+
world: ResolvedWorldEntry,
|
|
480
|
+
mod: { default?: unknown },
|
|
481
|
+
identityLabel: 'default-react' | 'ingest-react' = 'default-react',
|
|
482
|
+
): ComponentType {
|
|
483
|
+
if (typeof mod.default !== 'function') {
|
|
484
|
+
throw new Error(
|
|
485
|
+
`resolveWorldAdapter: world "${world.id}" (${identityLabel}) entry module does not ` +
|
|
486
|
+
`default-export a component — got ${typeof mod.default}. A react world's \`entry\` must ` +
|
|
487
|
+
'default-export a React component (docs/REACT-WORLD-DESIGN.md §1.B).',
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
return mod.default as ComponentType;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* `ReactWorldHost.game` is a typed, guaranteed field (the T6.2 slice-1 gap
|
|
495
|
+
* this function originally worked around — `mountOneReactWorld` not
|
|
496
|
+
* threading `game` onto the host — was closed the same day, `5948678`,
|
|
497
|
+
* symmetric with `HostContext.game`). The `undefined` in the return type and
|
|
498
|
+
* the loud check at the call site stay: a foreign/hand-rolled host object
|
|
499
|
+
* that skipped the engine's mount path must still fail loudly, never lie an
|
|
500
|
+
* `undefined` Game into `<GameProvider>`.
|
|
501
|
+
*/
|
|
502
|
+
function hostGame(host: ReactWorldHost): Game | undefined {
|
|
503
|
+
return host.game;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// ---------------------------------------------------------------------------
|
|
507
|
+
// Part F.1 (C1, phase-b/c1-react-mount) — packaged-mode react-world mount
|
|
508
|
+
// runtime (the dual-React-instance fix)
|
|
509
|
+
// ---------------------------------------------------------------------------
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* The three pieces of "react itself" a react-world mount needs:
|
|
513
|
+
* `createElement`/`createRoot` (to build + render the `<GameProvider><Entry/>
|
|
514
|
+
* </GameProvider>` tree) and the engine-published `GameProvider` FALLBACK
|
|
515
|
+
* (`resolveGameProviderForProject`'s "absent" branch, D-Z5). Under dev/
|
|
516
|
+
* Electron these are always the module top level's own static imports
|
|
517
|
+
* (`createElement`/`createRoot`/`EngineGameProvider` above) — byte-identical
|
|
518
|
+
* to before this type existed. See {@link resolveReactWorldMountRuntime}'s
|
|
519
|
+
* doc comment for the packaged-mode branch this type also serves.
|
|
520
|
+
*/
|
|
521
|
+
export interface ReactWorldMountRuntime {
|
|
522
|
+
createElement: typeof createElement;
|
|
523
|
+
createRoot: typeof createRoot;
|
|
524
|
+
/** `react-dom`'s synchronous-flush escape hatch — `design-time-layers.ts`'s
|
|
525
|
+
* react-world layer mount needs it from the SAME react-dom peer as
|
|
526
|
+
* `createRoot` above (both are entry points of the same installed
|
|
527
|
+
* package). Unused by `adapter-resolver.ts`'s own two mount()s. */
|
|
528
|
+
flushSync: typeof flushSync;
|
|
529
|
+
engineGameProvider: ComponentType<{ game: Game; children?: unknown }>;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** The dev/Electron default: this module's own static imports, unchanged. */
|
|
533
|
+
function staticReactWorldMountRuntime(): ReactWorldMountRuntime {
|
|
534
|
+
return {
|
|
535
|
+
createElement,
|
|
536
|
+
createRoot,
|
|
537
|
+
flushSync,
|
|
538
|
+
engineGameProvider: EngineGameProvider as unknown as ComponentType<{
|
|
539
|
+
game: Game;
|
|
540
|
+
children?: unknown;
|
|
541
|
+
}>,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Import + shape-check the packaged runtime's synthetic
|
|
547
|
+
* `/__vgai-react-world-runtime` module (`vite-plugin-react-world-runtime.ts`,
|
|
548
|
+
* registered only by `packaged.ts`'s project-rooted Vite instance and by
|
|
549
|
+
* `vitest.config.ts` for test coverage — never by `dev.ts`/the root
|
|
550
|
+
* `vite.config.ts`). A loud, named failure if the module resolves but is
|
|
551
|
+
* malformed — never a silent fall-through that would re-introduce the dual-
|
|
552
|
+
* instance bug it exists to prevent.
|
|
553
|
+
*/
|
|
554
|
+
async function loadPackagedReactWorldMountRuntime(): Promise<ReactWorldMountRuntime> {
|
|
555
|
+
const mod = (await import(/* @vite-ignore */ REACT_WORLD_RUNTIME_PATH)) as {
|
|
556
|
+
createElement?: unknown;
|
|
557
|
+
createRoot?: unknown;
|
|
558
|
+
flushSync?: unknown;
|
|
559
|
+
EngineGameProvider?: unknown;
|
|
560
|
+
};
|
|
561
|
+
if (
|
|
562
|
+
typeof mod.createElement !== 'function' ||
|
|
563
|
+
typeof mod.createRoot !== 'function' ||
|
|
564
|
+
typeof mod.flushSync !== 'function' ||
|
|
565
|
+
typeof mod.EngineGameProvider !== 'function'
|
|
566
|
+
) {
|
|
567
|
+
throw new Error(
|
|
568
|
+
"resolveWorldAdapter: the packaged runtime's synthetic react-world-runtime module " +
|
|
569
|
+
`("${REACT_WORLD_RUNTIME_PATH}") did not export createElement/createRoot/flushSync/` +
|
|
570
|
+
'EngineGameProvider as functions — see vite-plugin-react-world-runtime.ts.',
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
return {
|
|
574
|
+
createElement: mod.createElement as typeof createElement,
|
|
575
|
+
createRoot: mod.createRoot as typeof createRoot,
|
|
576
|
+
flushSync: mod.flushSync as typeof flushSync,
|
|
577
|
+
engineGameProvider: mod.EngineGameProvider as ComponentType<{
|
|
578
|
+
game: Game;
|
|
579
|
+
children?: unknown;
|
|
580
|
+
}>,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
let cachedPackagedReactWorldMountRuntime: Promise<ReactWorldMountRuntime> | null = null;
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Resolve the react-world mount's OWN `createElement`/`createRoot`/engine-
|
|
588
|
+
* `GameProvider`-fallback — the C1 fix for the packaged runtime's dual-React-
|
|
589
|
+
* instance bug (`packaged.ts`'s header doc comment, "React-world OID
|
|
590
|
+
* authoring parity" section): a react world is an ISOLATED `createRoot` (its
|
|
591
|
+
* own DOM subtree / react tree) that does NOT need to share react with the
|
|
592
|
+
* editor's own UI — it only needs to be internally consistent (the wrapper +
|
|
593
|
+
* `GameProvider` + the project's entry component all sharing ONE react
|
|
594
|
+
* instance). Under the packaged runtime, the editor's own UI is a separately-
|
|
595
|
+
* built static bundle with its OWN baked-in `react`/`react-dom` copy,
|
|
596
|
+
* disconnected from the SEPARATE project-rooted Vite instance the entry
|
|
597
|
+
* component's hooks resolve `react` through — so mounting with the module top
|
|
598
|
+
* level's static imports (the editor's copy) produces two react instances and
|
|
599
|
+
* `useContext` throws on a `null` dispatcher (confirmed live, see
|
|
600
|
+
* `packaged.ts`'s header doc comment history).
|
|
601
|
+
*
|
|
602
|
+
* The fix: under the packaged runtime ONLY (gated on `isPackagedRuntime()`,
|
|
603
|
+
* `packaged-runtime.ts` — reads a `packaged` flag `/__editor/project` now
|
|
604
|
+
* reports, itself computed server-side via `isMonorepoScaffoldRoot`), resolve
|
|
605
|
+
* `createElement`/`createRoot`/the engine `GameProvider` fallback from the
|
|
606
|
+
* PROJECT's own module graph instead — via `/__vgai-react-world-runtime`, a
|
|
607
|
+
* synthetic virtual module registered only on that project-rooted Vite
|
|
608
|
+
* instance (`vite-plugin-react-world-runtime.ts`), so both the mount call and
|
|
609
|
+
* the entry component's own hooks converge on the SAME project-resolved react
|
|
610
|
+
* module instance, the same "static synthetic re-export" trick
|
|
611
|
+
* `reactWorldProviderPlugin`/`loadProjectGameProvider` already use one layer
|
|
612
|
+
* up for `GameProvider` itself.
|
|
613
|
+
*
|
|
614
|
+
* Under dev/Electron, `isPackagedRuntime()` resolves `false` (no such server
|
|
615
|
+
* flag; one shared Vite instance + root `vite.config.ts`'s `resolve.dedupe`
|
|
616
|
+
* already collapses the editor's and the project's react onto one instance)
|
|
617
|
+
* — this function then returns the module top level's own static imports,
|
|
618
|
+
* BYTE-IDENTICAL to the pre-C1 behavior. Memoized: `isPackagedRuntime()` is
|
|
619
|
+
* itself session-memoized, and the packaged module load below is cached here
|
|
620
|
+
* 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.
|
|
622
|
+
*
|
|
623
|
+
* Exported: `design-time-layers.ts`'s OWN, separate react-world layer mount
|
|
624
|
+
* (`mountReactLayer` — the edit-mode composite's live preview of a non-
|
|
625
|
+
* focused react/pixi world, B1) needs this SAME resolution — it is a second
|
|
626
|
+
* mount call site with the identical dual-instance exposure, not routed
|
|
627
|
+
* through this file's `ReactWorldAdapter.mount()` at all.
|
|
628
|
+
*/
|
|
629
|
+
export async function resolveReactWorldMountRuntime(): Promise<ReactWorldMountRuntime> {
|
|
630
|
+
const packaged = await isPackagedRuntime();
|
|
631
|
+
if (!packaged) return staticReactWorldMountRuntime();
|
|
632
|
+
if (!cachedPackagedReactWorldMountRuntime) {
|
|
633
|
+
cachedPackagedReactWorldMountRuntime = loadPackagedReactWorldMountRuntime();
|
|
634
|
+
}
|
|
635
|
+
return cachedPackagedReactWorldMountRuntime;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/** Test-only: reset the packaged react-world mount runtime cache between
|
|
639
|
+
* tests that toggle `isPackagedRuntime()`'s own (separately memoized, see
|
|
640
|
+
* `packaged-runtime.ts`'s `resetPackagedRuntimeCacheForTest`) result. */
|
|
641
|
+
export function resetReactWorldMountRuntimeCacheForTest(): void {
|
|
642
|
+
cachedPackagedReactWorldMountRuntime = null;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
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).
|
|
660
|
+
*
|
|
661
|
+
* THIRD FINDING, and the actual engine-adjacent-tooling gap this slice
|
|
662
|
+
* closes (T6.2 slice 3 — verified empirically, not assumed): a hand-built
|
|
663
|
+
* runtime `import(/* @vite-ignore *\/ '/@fs/' + projectRoot + '/src/
|
|
664
|
+
* ui/game-state.tsx')` does NOT reliably land on the SAME module
|
|
665
|
+
* instance a react-world entry's own STATIC relative import of that file
|
|
666
|
+
* resolves to. Vite's dev server treats the two differently at the HTTP
|
|
667
|
+
* level (a query-string divergence was observed in practice — confirmed by
|
|
668
|
+
* logging `import.meta.url` inside two otherwise-identical fetches) even
|
|
669
|
+
* though both target the identical file on disk; since browser ES module
|
|
670
|
+
* identity is keyed by the exact request URL, that is enough to produce TWO
|
|
671
|
+
* separate `createContext()` calls — exactly the multi-instance problem
|
|
672
|
+
* this doc comment already warns about, just triggered by the RESOLVER's
|
|
673
|
+
* own fetch mechanics rather than a hand-authored duplicate. The fix:
|
|
674
|
+
* `vite-plugin-react-world-provider.ts`'s tiny synthetic module, whose body
|
|
675
|
+
* is a LITERAL, STATIC `export { GameProvider } from '/@fs/…/game-state.tsx'`
|
|
676
|
+
* — Vite's import-analysis rewrites that specifier through the SAME
|
|
677
|
+
* transform-time resolver a react-world entry's own static import goes
|
|
678
|
+
* through, converging both on the identical final module.
|
|
679
|
+
*
|
|
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.
|
|
690
|
+
*/
|
|
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
|
+
/**
|
|
710
|
+
* Resolve a `kind: 'react'` + `adapter: 'default'` world into a
|
|
711
|
+
* `ReactWorldAdapter` (docs/REACT-WORLD-DESIGN.md §1.B, T6.2 slice 1 task
|
|
712
|
+
* list's "Mount" item) — the react sibling of `resolveDefaultThreeAdapter`/
|
|
713
|
+
* `resolveDefaultPixiAdapter`. `entry` resolves via the SAME `/@fs/`
|
|
714
|
+
* dynamic-import route `loadProjectScripts` uses (`resolveDefaultPixiAdapter`'s
|
|
715
|
+
* own entry-adjudication precedent) — but a react entry module default-
|
|
716
|
+
* exports a component, not a `setup` function, so this reads the module
|
|
717
|
+
* directly rather than forcing it through `loadProjectScripts`'s
|
|
718
|
+
* `GameSetupFn`-shaped return.
|
|
719
|
+
*
|
|
720
|
+
* `mount(host)` follows the engine team's recorded recipe exactly:
|
|
721
|
+
* `createRoot(host.container).render(<GameProvider game={game}><Entry/></GameProvider>)`,
|
|
722
|
+
* returning `{ drivesOwnLoop: false, dispose: () => root.unmount() }`. See
|
|
723
|
+
* `hostGame`'s and `loadProjectGameProvider`'s doc comments above for the two
|
|
724
|
+
* honest gaps this depends on (the engine-merged `ReactWorldHost` not yet
|
|
725
|
+
* carrying `game`; `GameProvider` being a per-project file, not an export).
|
|
726
|
+
*
|
|
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.
|
|
737
|
+
*
|
|
738
|
+
* `scene` on a react world is a NAMED error (JSX-as-truth, D4 —
|
|
739
|
+
* docs/REACT-WORLD-DESIGN.md §1.A: "A react WORLD's truth is JSX source —
|
|
740
|
+
* … It is code; it never serializes to `ui` JSON" — there is no scene file
|
|
741
|
+
* format for a react world at all). The manifest loader's own
|
|
742
|
+
* `checkAdapterSceneEntryRules` only requires ONE of `scene`/`entry` for a
|
|
743
|
+
* `default`-adapter world (it does not forbid `scene` on a react world
|
|
744
|
+
* specifically — see `load.ts`), so this resolver is where the D4 rule is
|
|
745
|
+
* actually enforced for react worlds.
|
|
746
|
+
*/
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* The `default-react` shared entry-loading step, extracted (D-V3,
|
|
750
|
+
* docs/WAVE5-MULTIWORLD-INGEST-DESIGN.md) so a composite's `default-react`
|
|
751
|
+
* SIBLING mount (`packages/editor/src/ingest-siblings.ts`) can reuse the
|
|
752
|
+
* EXACT SAME D4 `scene`-forbidden / `entry`-required checks and
|
|
753
|
+
* `loadReactEntryComponent` shape validation `resolveDefaultReactAdapter`
|
|
754
|
+
* below uses, WITHOUT duplicating any of it — the sibling then renders the
|
|
755
|
+
* returned component BARE (no `<GameProvider>` wrap: there is no native
|
|
756
|
+
* `Game` beside an ingest world), while `resolveDefaultReactAdapter` wraps
|
|
757
|
+
* it in `<GameProvider>` as it always has. Exported for that one caller.
|
|
758
|
+
*/
|
|
759
|
+
export async function resolveReactWorldEntryComponent(
|
|
760
|
+
world: ResolvedWorldEntry,
|
|
761
|
+
projectRoot: string,
|
|
762
|
+
): Promise<ComponentType> {
|
|
763
|
+
if (world.scene !== undefined) {
|
|
764
|
+
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, ' +
|
|
767
|
+
'docs/REACT-WORLD-DESIGN.md §1.A). Use `entry` (a .tsx module default-exporting a ' +
|
|
768
|
+
'component) instead.',
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
if (world.entry === undefined) {
|
|
772
|
+
// Unreachable in practice: `load.ts`'s `checkAdapterSceneEntryRules` requires
|
|
773
|
+
// `scene` or `entry` for every 'default' adapter world at manifest-load time,
|
|
774
|
+
// and the check above already rejects `scene` for a react world — so a real
|
|
775
|
+
// manifest can never reach this branch. Still fails loudly, not silently.
|
|
776
|
+
throw new Error(
|
|
777
|
+
`resolveWorldAdapter: world "${world.id}" is default-react with no \`entry\` — this should ` +
|
|
778
|
+
'have been rejected by manifest validation.',
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const modulePath = fsImportPath(projectRoot, world.entry);
|
|
783
|
+
const mod = (await import(/* @vite-ignore */ modulePath)) as { default?: unknown };
|
|
784
|
+
return loadReactEntryComponent(world, mod);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
async function resolveDefaultReactAdapter(
|
|
788
|
+
world: ResolvedWorldEntry,
|
|
789
|
+
projectRoot: string,
|
|
790
|
+
): Promise<GameAdapter> {
|
|
791
|
+
const Entry = await resolveReactWorldEntryComponent(world, projectRoot);
|
|
792
|
+
const GameProvider = await resolveGameProviderForProject(projectRoot, world.id, 'default-react');
|
|
793
|
+
|
|
794
|
+
const adapter: ReactWorldAdapter = {
|
|
795
|
+
id: world.id,
|
|
796
|
+
async mount(host: ReactWorldHost): Promise<MountedReactGame> {
|
|
797
|
+
const game = hostGame(host);
|
|
798
|
+
if (!game) {
|
|
799
|
+
throw new Error(
|
|
800
|
+
`resolveWorldAdapter: world "${world.id}" (default-react) mount() received a ` +
|
|
801
|
+
'ReactWorldHost with no `game` — GameProvider needs a live Game at mount time and ' +
|
|
802
|
+
"this host did not carry one (see adapter-resolver.ts's `hostGame` doc comment for " +
|
|
803
|
+
'the engine-side gap this indicates).',
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
// C1: createElement/createRoot come from resolveReactWorldMountRuntime
|
|
807
|
+
// (the PROJECT's own react under the packaged runtime, this module's
|
|
808
|
+
// static imports otherwise) - never the module top-level statics
|
|
809
|
+
// directly, so this mount always shares ONE react instance with the
|
|
810
|
+
// entry component's own hooks. See that function's doc comment.
|
|
811
|
+
const runtime = await resolveReactWorldMountRuntime();
|
|
812
|
+
const root = runtime.createRoot(host.container);
|
|
813
|
+
root.render(
|
|
814
|
+
runtime.createElement(GameProvider, { game, children: runtime.createElement(Entry) }),
|
|
815
|
+
);
|
|
816
|
+
return {
|
|
817
|
+
kind: 'react',
|
|
818
|
+
container: host.container,
|
|
819
|
+
drivesOwnLoop: false,
|
|
820
|
+
dispose(): void {
|
|
821
|
+
root.unmount();
|
|
822
|
+
},
|
|
823
|
+
};
|
|
824
|
+
},
|
|
825
|
+
};
|
|
826
|
+
return adapter as unknown as GameAdapter;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// ---------------------------------------------------------------------------
|
|
830
|
+
// Part G (Track N, N1 — docs/REACT-INGEST-LANDING-DESIGN.md D-N2/D-N3/D-N4) —
|
|
831
|
+
// ingest-react
|
|
832
|
+
// ---------------------------------------------------------------------------
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Resolve a `kind: 'react'` + `{ ingest: { strategy: 'deduped' } }` world into
|
|
836
|
+
* a `ReactWorldAdapter` — the ingest sibling of `resolveDefaultReactAdapter`
|
|
837
|
+
* immediately above. Mounts through the EXACT SAME T6.2 react host stack:
|
|
838
|
+
* the same `/@fs/` `entry`-module dynamic import, the same default-export-
|
|
839
|
+
* component validation (`loadReactEntryComponent`), the same per-project
|
|
840
|
+
* `GameProvider` wrap (`loadProjectGameProvider`), the same
|
|
841
|
+
* `createRoot(...).render(<GameProvider><Entry/></GameProvider>)` mount body.
|
|
842
|
+
* Only the adapter identity differs — D-N2's framing is explicit that this
|
|
843
|
+
* is "one stacking model, no special case": "the automatic `<GameProvider>`
|
|
844
|
+
* wrap is context-only and harmless to a foreign tree."
|
|
845
|
+
*
|
|
846
|
+
* `entry` here is NOT a first-party project's own react-world component —
|
|
847
|
+
* it is a HOST-SHIM module (D-N2) living beside the ingested game's per-game
|
|
848
|
+
* manifest (`packages/editor/src/ingest/games-react/<id>/`, N2), importing
|
|
849
|
+
* the vendored game's own `App`/store/`persistor` and default-exporting the
|
|
850
|
+
* composed component. This resolver has no knowledge of the vendored game's
|
|
851
|
+
* OWN standalone entry point (its raw `ReactDOM.render` call) either way —
|
|
852
|
+
* it only ever imports the manifest's declared `entry`, exactly like
|
|
853
|
+
* `default-react` — so that entry point is never reached through this path,
|
|
854
|
+
* matching D-N2's "never imported; it must never run."
|
|
855
|
+
*
|
|
856
|
+
* Two rules `default-react` does not need, both loud named errors — never a
|
|
857
|
+
* silent floor, matching every other ingest resolver in this file
|
|
858
|
+
* (`resolveIngestDescriptor`/`resolveIngest2DDescriptor` throw the same way
|
|
859
|
+
* for a strategy they can't build):
|
|
860
|
+
*
|
|
861
|
+
* - `strategy` MUST be `'deduped'` — react ingestion v1 ships ONLY the
|
|
862
|
+
* deduped rung (D-N3: "the vendored source is served through the
|
|
863
|
+
* editor's own Vite pipeline… with react/react-dom deduped to the host's
|
|
864
|
+
* React 19… an iframe/opaque-embed floor is recorded as out of scope for
|
|
865
|
+
* v1"). Any other strategy is structurally legal at the manifest-load
|
|
866
|
+
* layer (`load.ts` assigns `'ingest-react'` regardless of strategy,
|
|
867
|
+
* exactly like `'ingest-three'`/`'ingest-pixi'` stay strategy-agnostic
|
|
868
|
+
* there) but throws HERE, naming the strategy it actually got.
|
|
869
|
+
* - `entry` is REQUIRED — there is no scene-or-entry choice the way
|
|
870
|
+
* `default-three` has; an ingest-react world IS its host shim (D-N2).
|
|
871
|
+
*
|
|
872
|
+
* `scene` on an ingest-react world is already rejected before this function
|
|
873
|
+
* ever runs against a REAL manifest: `load.ts`'s `checkAdapterSceneEntryRules`
|
|
874
|
+
* forbids `scene` on any `{ ingest }`-adapter world, kind-agnostic. This
|
|
875
|
+
* function still guards it defensively (never trust a hand-built
|
|
876
|
+
* `ResolvedWorldEntry` a unit test — or a future caller — might construct
|
|
877
|
+
* past that check), with the same message shape `resolveDefaultReactAdapter`'s
|
|
878
|
+
* own `scene` check uses.
|
|
879
|
+
*
|
|
880
|
+
* 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.
|
|
900
|
+
*
|
|
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).
|
|
976
|
+
*/
|
|
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
|
+
/**
|
|
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.
|
|
1019
|
+
*/
|
|
1020
|
+
export async function resolveGameProviderForProject(
|
|
1021
|
+
projectRoot: string,
|
|
1022
|
+
worldId: string,
|
|
1023
|
+
identity: 'default-react' | 'ingest-react',
|
|
1024
|
+
): 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
|
+
const runtime = await resolveReactWorldMountRuntime();
|
|
1039
|
+
return runtime.engineGameProvider;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
async function resolveIngestReactAdapter(
|
|
1043
|
+
world: ResolvedWorldEntry,
|
|
1044
|
+
projectRoot: string,
|
|
1045
|
+
): Promise<GameAdapter> {
|
|
1046
|
+
const { adapter } = world;
|
|
1047
|
+
if (adapter.type !== 'ingest') {
|
|
1048
|
+
throw new Error(`resolveWorldAdapter: world "${world.id}" is not an { ingest } world.`);
|
|
1049
|
+
}
|
|
1050
|
+
if (adapter.strategy !== 'deduped') {
|
|
1051
|
+
throw new Error(
|
|
1052
|
+
`resolveWorldAdapter: world "${world.id}" (ingest-react) declares strategy ` +
|
|
1053
|
+
`"${adapter.strategy}" — react ingestion v1 supports ONLY the 'deduped' rung (loud ` +
|
|
1054
|
+
'degrade, no silent floor: docs/REACT-INGEST-LANDING-DESIGN.md D-N3). An iframe-' +
|
|
1055
|
+
'reachable/opaque-embed floor is explicitly out of scope for v1.',
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
if (world.scene !== undefined) {
|
|
1059
|
+
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, ' +
|
|
1062
|
+
'docs/REACT-WORLD-DESIGN.md §1.A), and an ingested react world doubly so — the vendored ' +
|
|
1063
|
+
'game is never modified. Use `entry` (the host-shim module, D-N2) instead.',
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
if (world.entry === undefined) {
|
|
1067
|
+
throw new Error(
|
|
1068
|
+
`resolveWorldAdapter: world "${world.id}" (ingest-react) has no \`entry\` — an ingested ` +
|
|
1069
|
+
"react world's `entry` MUST point at a host-shim module that imports the vendored " +
|
|
1070
|
+
"game's own component and default-exports it (D-N2, docs/REACT-INGEST-LANDING-DESIGN.md).",
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
const modulePath = fsImportPath(projectRoot, world.entry);
|
|
1075
|
+
const mod = (await import(/* @vite-ignore */ modulePath)) as { default?: unknown };
|
|
1076
|
+
const Entry = loadReactEntryComponent(world, mod, 'ingest-react');
|
|
1077
|
+
// D-Z5: always resolves to SOME GameProvider now — the project's own
|
|
1078
|
+
// (present) or the engine-published fallback + a loud report (absent). See
|
|
1079
|
+
// `resolveGameProviderForProject`'s doc comment for the full history.
|
|
1080
|
+
const GameProvider = await resolveGameProviderForProject(projectRoot, world.id, 'ingest-react');
|
|
1081
|
+
|
|
1082
|
+
const reactAdapter: ReactWorldAdapter = {
|
|
1083
|
+
id: world.id,
|
|
1084
|
+
async mount(host: ReactWorldHost): Promise<MountedReactGame> {
|
|
1085
|
+
const game = hostGame(host);
|
|
1086
|
+
if (!game) {
|
|
1087
|
+
throw new Error(
|
|
1088
|
+
`resolveWorldAdapter: world "${world.id}" (ingest-react) mount() received a ` +
|
|
1089
|
+
'ReactWorldHost with no `game` — GameProvider needs a live Game at mount time and ' +
|
|
1090
|
+
"this host did not carry one (see adapter-resolver.ts's `hostGame` doc comment for " +
|
|
1091
|
+
'the engine-side gap this indicates).',
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
// C1: createElement/createRoot come from resolveReactWorldMountRuntime
|
|
1095
|
+
// (the PROJECT's own react under the packaged runtime, this module's
|
|
1096
|
+
// static imports otherwise) - never the module top-level statics
|
|
1097
|
+
// directly, so this mount always shares ONE react instance with the
|
|
1098
|
+
// entry component's own hooks. See that function's doc comment.
|
|
1099
|
+
const runtime = await resolveReactWorldMountRuntime();
|
|
1100
|
+
const root = runtime.createRoot(host.container);
|
|
1101
|
+
root.render(
|
|
1102
|
+
runtime.createElement(GameProvider, { game, children: runtime.createElement(Entry) }),
|
|
1103
|
+
);
|
|
1104
|
+
return {
|
|
1105
|
+
kind: 'react',
|
|
1106
|
+
container: host.container,
|
|
1107
|
+
drivesOwnLoop: false,
|
|
1108
|
+
dispose(): void {
|
|
1109
|
+
root.unmount();
|
|
1110
|
+
},
|
|
1111
|
+
};
|
|
1112
|
+
},
|
|
1113
|
+
};
|
|
1114
|
+
return reactAdapter as unknown as GameAdapter;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// ---------------------------------------------------------------------------
|
|
1118
|
+
// Part A/D — ingest-three
|
|
1119
|
+
// ---------------------------------------------------------------------------
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* Fetch a project-relative file's TEXT bytes through the dev-server-only
|
|
1123
|
+
* `/project-game-static/<path>` route (D-W2, docs/WAVE4-FTUE-HARDENING-
|
|
1124
|
+
* DESIGN.md) — the opaque-embed floor's file read, shared by BOTH kinds
|
|
1125
|
+
* (`resolveIngestDescriptor`/`resolveIngest2DDescriptor` below): the route
|
|
1126
|
+
* serves the CURRENTLY OPEN project's folder verbatim
|
|
1127
|
+
* (`vite-plugin-project-game-static.ts`), so a manifest's `entryHtml` (a
|
|
1128
|
+
* project-relative path, exactly like the in-tree fixture glob's `entryHtml`
|
|
1129
|
+
* convention) maps directly to `/project-game-static/<entryHtml>`. Absolute
|
|
1130
|
+
* URL construction mirrors `probeGameStateFileExists`'s origin handling
|
|
1131
|
+
* above (a real dev-server session has an implicit browser document base; a
|
|
1132
|
+
* headless Node/vitest caller does not — build one explicitly so this stays
|
|
1133
|
+
* mockable/testable in both).
|
|
1134
|
+
*/
|
|
1135
|
+
async function fetchProjectStaticText(entryHtmlPath: string): Promise<string> {
|
|
1136
|
+
const rel = entryHtmlPath.startsWith('/') ? entryHtmlPath.slice(1) : entryHtmlPath;
|
|
1137
|
+
const url = `/project-game-static/${rel}`;
|
|
1138
|
+
const origin =
|
|
1139
|
+
typeof window !== 'undefined' && window.location ? window.location.origin : 'http://localhost';
|
|
1140
|
+
const res = await fetch(new URL(url, origin).toString());
|
|
1141
|
+
if (!res.ok) {
|
|
1142
|
+
throw new Error(
|
|
1143
|
+
`resolveIngestDescriptor: failed to load opaque-embed entryHtml "${entryHtmlPath}" via ` +
|
|
1144
|
+
`${url} (${res.status} ${res.statusText}).`,
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
return res.text();
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* The `iframe-reachable` branch of {@link resolveIngestDescriptor} — the
|
|
1152
|
+
* three-side twin of `resolveIngest2DDescriptor`'s
|
|
1153
|
+
* `buildIframeReachableDescriptor2D`, split out for the same cognitive-
|
|
1154
|
+
* complexity reason (biome's `noExcessiveCognitiveComplexity`). `bundleUrl`
|
|
1155
|
+
* is required (an `entryHtml`-only iframe-reachable world stays an unwired
|
|
1156
|
+
* named error, same text pattern pixi's sibling uses); a `pixiModuleUrl`
|
|
1157
|
+
* declared on a threejs world is a DIFFERENT named error — three has no
|
|
1158
|
+
* version-skew mount this wave (D-W7 explicitly parks a hypothetical
|
|
1159
|
+
* `threeModuleUrl` field: "no consumer exists").
|
|
1160
|
+
*/
|
|
1161
|
+
function buildIframeReachableIngestDescriptor(
|
|
1162
|
+
world: ResolvedWorldEntry,
|
|
1163
|
+
adapter: Extract<ResolvedWorldEntry['adapter'], { type: 'ingest' }>,
|
|
1164
|
+
base: Pick<
|
|
1165
|
+
IngestGame,
|
|
1166
|
+
'id' | 'name' | 'description' | 'assets' | 'domStubs' | 'captureTimeoutMs'
|
|
1167
|
+
>,
|
|
1168
|
+
): IngestGame {
|
|
1169
|
+
if (adapter.pixiModuleUrl !== undefined) {
|
|
1170
|
+
throw new Error(
|
|
1171
|
+
`resolveIngestDescriptor: world "${world.id}" (kind threejs, strategy ` +
|
|
1172
|
+
'"iframe-reachable") declares `pixiModuleUrl` — that field is pixi-specific version-' +
|
|
1173
|
+
'skew wiring (a standalone matching-major pixi build to trap instead of the host pixi) ' +
|
|
1174
|
+
'with no threejs equivalent implemented this wave: D-W7 explicitly parks a hypothetical ' +
|
|
1175
|
+
'`threeModuleUrl` field (docs/WAVE4-FTUE-HARDENING-DESIGN.md) — "no consumer exists".',
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
if (!adapter.bundleUrl) {
|
|
1179
|
+
throw new Error(
|
|
1180
|
+
`resolveIngestDescriptor: world "${world.id}" (strategy "iframe-reachable") has no ` +
|
|
1181
|
+
'`bundleUrl` — only the externalized-bundle route is buildable here (an ' +
|
|
1182
|
+
"`entryHtml`-only iframe-reachable world isn't wired; the manifest schema already " +
|
|
1183
|
+
'requires exactly one of bundleUrl/entryHtml for this strategy).',
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
const { bundleUrl, baseHref, assetBaseUrl, extraDeps, bodyHtml } = adapter;
|
|
1187
|
+
return {
|
|
1188
|
+
...base,
|
|
1189
|
+
bundleUrl,
|
|
1190
|
+
...(baseHref !== undefined ? { baseHref } : {}),
|
|
1191
|
+
...(assetBaseUrl !== undefined ? { assetBaseUrl } : {}),
|
|
1192
|
+
...(extraDeps !== undefined ? { extraDeps: () => resolveExtraDeps(extraDeps) } : {}),
|
|
1193
|
+
...(bodyHtml !== undefined ? { bodyHtml } : {}),
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* The `opaque-embed` branch of {@link resolveIngestDescriptor} (D-W2): reads
|
|
1199
|
+
* the declared `entryHtml` file's bytes through {@link fetchProjectStaticText}
|
|
1200
|
+
* as a LAZY thunk on the descriptor — never eagerly, only when the game is
|
|
1201
|
+
* actually mounted (`enterIngestModeWithGame` awaits `game.embedHtml()`).
|
|
1202
|
+
*/
|
|
1203
|
+
function buildOpaqueEmbedIngestDescriptor(
|
|
1204
|
+
world: ResolvedWorldEntry,
|
|
1205
|
+
adapter: Extract<ResolvedWorldEntry['adapter'], { type: 'ingest' }>,
|
|
1206
|
+
base: Pick<
|
|
1207
|
+
IngestGame,
|
|
1208
|
+
'id' | 'name' | 'description' | 'assets' | 'domStubs' | 'captureTimeoutMs'
|
|
1209
|
+
>,
|
|
1210
|
+
): IngestGame {
|
|
1211
|
+
if (!adapter.entryHtml) {
|
|
1212
|
+
throw new Error(
|
|
1213
|
+
`resolveIngestDescriptor: world "${world.id}" (strategy "opaque-embed") has no ` +
|
|
1214
|
+
'`entryHtml` — the opaque-embed floor needs the entry HTML file path to read (D-W2, ' +
|
|
1215
|
+
'docs/WAVE4-FTUE-HARDENING-DESIGN.md).',
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
const entryHtml = adapter.entryHtml;
|
|
1219
|
+
return { ...base, embedHtml: () => fetchProjectStaticText(entryHtml) };
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/**
|
|
1223
|
+
* Build the {@link IngestGame} descriptor an `ingest-three` world resolves
|
|
1224
|
+
* to, straight from its manifest fields — deliberately WITHOUT looking up any
|
|
1225
|
+
* registry. This is the part of §1D that "parameterizes by the manifest's
|
|
1226
|
+
* ingest fields rather than a registry id" — and, as of T3.3 slice 3
|
|
1227
|
+
* (§1E), there IS no registry left to look up: `ingest/games/registry.ts`
|
|
1228
|
+
* (`INGEST_GAMES`) is deleted; the in-tree fixture games it used to list are
|
|
1229
|
+
* discovered separately by `ingest/discovery.ts` (`import.meta.glob` over
|
|
1230
|
+
* `./games/*\/vgai.game.json`, NOT through this function — see that file's
|
|
1231
|
+
* doc comment for why). This function stays the builder for the OTHER half
|
|
1232
|
+
* of §1E: an arbitrary manifest-backed project folder never registered in
|
|
1233
|
+
* source (the CLI-on-folder route), whose `ingest-three` world this function
|
|
1234
|
+
* turns into a mountable descriptor via `/@fs/${projectRoot}/...`.
|
|
1235
|
+
*
|
|
1236
|
+
* `shared`/`deduped` resolve to `IngestGame.load` via the same `/@fs/`
|
|
1237
|
+
* dynamic-import route `loadProjectScripts` uses, keyed off the world's
|
|
1238
|
+
* `entry` field. D-W1 (docs/WAVE4-FTUE-HARDENING-DESIGN.md) adds
|
|
1239
|
+
* `iframe-reachable`: the manifest's `bundleUrl`/`baseHref`/`assetBaseUrl`/
|
|
1240
|
+
* `bodyHtml` are carried straight through, `extraDeps` becomes a LAZY thunk
|
|
1241
|
+
* (`IngestGame.extraDeps`) wired to `./ingest/host-namespace-registry.ts` —
|
|
1242
|
+
* resolved only when the ingest-mode mount bridge actually invokes it, never
|
|
1243
|
+
* eagerly here (this function stays synchronous). D-W2 adds `opaque-embed`:
|
|
1244
|
+
* the declared `entryHtml`'s bytes, read through the project-static route as
|
|
1245
|
+
* a lazy `embedHtml` thunk. `iframe-reachable`+`entryHtml`-only (no
|
|
1246
|
+
* `bundleUrl`) stays an unwired named error on this kind too (mirrors the
|
|
1247
|
+
* pixi sibling): reading a raw inline-module-source file's contents into a
|
|
1248
|
+
* SHARED-instance-mounted string has no wired mechanism this wave.
|
|
1249
|
+
*/
|
|
1250
|
+
export function resolveIngestDescriptor(
|
|
1251
|
+
world: ResolvedWorldEntry,
|
|
1252
|
+
projectRoot: string,
|
|
1253
|
+
): IngestGame {
|
|
1254
|
+
const { adapter } = world;
|
|
1255
|
+
if (adapter.type !== 'ingest') {
|
|
1256
|
+
throw new Error(`resolveIngestDescriptor: world "${world.id}" is not an { ingest } world.`);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
const base: Pick<
|
|
1260
|
+
IngestGame,
|
|
1261
|
+
'id' | 'name' | 'description' | 'assets' | 'domStubs' | 'captureTimeoutMs'
|
|
1262
|
+
> = {
|
|
1263
|
+
id: world.id,
|
|
1264
|
+
name: world.id,
|
|
1265
|
+
// T3.3 slice 3: `description` is the manifest's new optional per-world
|
|
1266
|
+
// field (schema.ts) — carried through `load.ts`'s `resolveWorld`, threaded
|
|
1267
|
+
// here into `IngestGame.description` (the field's designated reader).
|
|
1268
|
+
description: world.description ?? '',
|
|
1269
|
+
...(adapter.assets !== undefined ? { assets: adapter.assets } : {}),
|
|
1270
|
+
...(adapter.domStubs !== undefined ? { domStubs: adapter.domStubs } : {}),
|
|
1271
|
+
...(adapter.captureTimeoutMs !== undefined
|
|
1272
|
+
? { captureTimeoutMs: adapter.captureTimeoutMs }
|
|
1273
|
+
: {}),
|
|
1274
|
+
};
|
|
1275
|
+
|
|
1276
|
+
if (adapter.strategy === 'shared' || adapter.strategy === 'deduped') {
|
|
1277
|
+
if (!world.entry) {
|
|
1278
|
+
throw new Error(
|
|
1279
|
+
`resolveIngestDescriptor: world "${world.id}" (strategy "${adapter.strategy}") has no ` +
|
|
1280
|
+
"`entry` — shared/deduped ingest worlds need the game module's path there.",
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
const modulePath = fsImportPath(projectRoot, world.entry);
|
|
1284
|
+
return { ...base, load: () => import(/* @vite-ignore */ modulePath) };
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
if (adapter.strategy === 'iframe-reachable') {
|
|
1288
|
+
return buildIframeReachableIngestDescriptor(world, adapter, base);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
if (adapter.strategy === 'opaque-embed') {
|
|
1292
|
+
return buildOpaqueEmbedIngestDescriptor(world, adapter, base);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// Exhaustive per IngestStrategySchema's 4 members.
|
|
1296
|
+
const exhaustive: never = adapter.strategy;
|
|
1297
|
+
throw new Error(
|
|
1298
|
+
`resolveIngestDescriptor: world "${world.id}" has an unhandled ingest strategy ` +
|
|
1299
|
+
`${JSON.stringify(exhaustive)}.`,
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// ---------------------------------------------------------------------------
|
|
1304
|
+
// Part E (T6.1 slice 4) — ingest-pixi
|
|
1305
|
+
// ---------------------------------------------------------------------------
|
|
1306
|
+
|
|
1307
|
+
/**
|
|
1308
|
+
* Build the {@link IngestGame2D} descriptor an `ingest-pixi` world resolves to,
|
|
1309
|
+
* straight from its manifest fields — the pixi sibling of
|
|
1310
|
+
* {@link resolveIngestDescriptor} (docs/MULTI-WORLD-DESIGN.md §1.E: "the same
|
|
1311
|
+
* two-layer shape as ingest-three"). `shared`/`deduped` resolve to a `load()`
|
|
1312
|
+
* thunk via the `/@fs/` dynamic-import route `loadProjectScripts` uses, keyed
|
|
1313
|
+
* off the world's `entry` field. Track P (docs/PIXI-INGEST-LANDING-DESIGN.md
|
|
1314
|
+
* §2/P2) adds `iframe-reachable`: the manifest's `bundleUrl`/`baseHref`/
|
|
1315
|
+
* `assetBaseUrl`/`bodyHtml` are carried straight through, and `extraDeps`/
|
|
1316
|
+
* `pixiModuleUrl` become LAZY thunks (`IngestGame2D.extraDeps`/
|
|
1317
|
+
* `.pixiNamespace`) wired to `./ingest/host-namespace-registry.ts` / a
|
|
1318
|
+
* dynamic `import()` — resolved only when the ingest-mode mount bridge
|
|
1319
|
+
* actually invokes them, never eagerly here (this function stays
|
|
1320
|
+
* synchronous, matching `resolveIngestDescriptor`'s shape exactly). D-W2
|
|
1321
|
+
* (docs/WAVE4-FTUE-HARDENING-DESIGN.md) adds `opaque-embed`: the declared
|
|
1322
|
+
* `entryHtml`'s bytes, read through the project-static route
|
|
1323
|
+
* (`fetchProjectStaticText`, shared with the three side above) as a lazy
|
|
1324
|
+
* `embedHtml` thunk.
|
|
1325
|
+
*
|
|
1326
|
+
* `tier` is set to `'shared'` for BOTH the `shared` and `deduped` manifest
|
|
1327
|
+
* strategies: `IngestGame2D.tier` is descriptive-only (`mountIngestGame2D`
|
|
1328
|
+
* never reads it — grep-verified) and has no `'deduped'` member of its own;
|
|
1329
|
+
* both strategies are mechanically identical from `mountIngestGame2D`'s
|
|
1330
|
+
* perspective (a `load()` thunk + the shared-instance capture trap), matching
|
|
1331
|
+
* how `resolveIngestDescriptor` already treats them identically (both produce
|
|
1332
|
+
* a `load` thunk with no strategy-specific branching).
|
|
1333
|
+
*/
|
|
1334
|
+
/**
|
|
1335
|
+
* The `iframe-reachable` branch of {@link resolveIngest2DDescriptor}, split
|
|
1336
|
+
* out to keep that function's cognitive complexity in check (biome's
|
|
1337
|
+
* `noExcessiveCognitiveComplexity`, max 15) — same "each strategy branch is
|
|
1338
|
+
* its own small builder" shape `discovery2d.ts`'s
|
|
1339
|
+
* `buildIframeReachableGame2D` already uses for its in-tree-fixture sibling.
|
|
1340
|
+
*/
|
|
1341
|
+
function buildIframeReachableDescriptor2D(
|
|
1342
|
+
world: ResolvedWorldEntry,
|
|
1343
|
+
adapter: Extract<ResolvedWorldEntry['adapter'], { type: 'ingest' }>,
|
|
1344
|
+
base: Pick<IngestGame2D, 'id' | 'name' | 'description' | 'captureTimeoutMs'>,
|
|
1345
|
+
): IngestGame2D {
|
|
1346
|
+
if (!adapter.bundleUrl) {
|
|
1347
|
+
throw new Error(
|
|
1348
|
+
`resolveIngest2DDescriptor: world "${world.id}" (strategy "iframe-reachable") has no ` +
|
|
1349
|
+
'`bundleUrl` — only the externalized-bundle route is buildable here (an ' +
|
|
1350
|
+
"`entryHtml`-only iframe-reachable world isn't wired; the manifest schema already " +
|
|
1351
|
+
'requires exactly one of bundleUrl/entryHtml for this strategy).',
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
const { bundleUrl, baseHref, assetBaseUrl, extraDeps, pixiModuleUrl, bodyHtml } = adapter;
|
|
1355
|
+
return {
|
|
1356
|
+
...base,
|
|
1357
|
+
tier: 'iframe-reachable',
|
|
1358
|
+
bundleUrl,
|
|
1359
|
+
...(baseHref !== undefined ? { baseHref } : {}),
|
|
1360
|
+
...(assetBaseUrl !== undefined ? { assetBaseUrl } : {}),
|
|
1361
|
+
...(extraDeps !== undefined ? { extraDeps: () => resolveExtraDeps(extraDeps) } : {}),
|
|
1362
|
+
...(pixiModuleUrl !== undefined
|
|
1363
|
+
? { pixiNamespace: () => import(/* @vite-ignore */ pixiModuleUrl) }
|
|
1364
|
+
: {}),
|
|
1365
|
+
...(bodyHtml !== undefined ? { bodyHtml } : {}),
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
export function resolveIngest2DDescriptor(
|
|
1370
|
+
world: ResolvedWorldEntry,
|
|
1371
|
+
projectRoot: string,
|
|
1372
|
+
): IngestGame2D {
|
|
1373
|
+
const { adapter } = world;
|
|
1374
|
+
if (adapter.type !== 'ingest') {
|
|
1375
|
+
throw new Error(`resolveIngest2DDescriptor: world "${world.id}" is not an { ingest } world.`);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
const base: Pick<IngestGame2D, 'id' | 'name' | 'description' | 'captureTimeoutMs'> = {
|
|
1379
|
+
id: world.id,
|
|
1380
|
+
name: world.id,
|
|
1381
|
+
description: world.description ?? '',
|
|
1382
|
+
...(adapter.captureTimeoutMs !== undefined
|
|
1383
|
+
? { captureTimeoutMs: adapter.captureTimeoutMs }
|
|
1384
|
+
: {}),
|
|
1385
|
+
};
|
|
1386
|
+
|
|
1387
|
+
if (adapter.strategy === 'shared' || adapter.strategy === 'deduped') {
|
|
1388
|
+
if (!world.entry) {
|
|
1389
|
+
throw new Error(
|
|
1390
|
+
`resolveIngest2DDescriptor: world "${world.id}" (strategy "${adapter.strategy}") has no ` +
|
|
1391
|
+
"`entry` — shared/deduped ingest-pixi worlds need the game module's path there.",
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1394
|
+
const modulePath = fsImportPath(projectRoot, world.entry);
|
|
1395
|
+
return { ...base, tier: 'shared', load: () => import(/* @vite-ignore */ modulePath) };
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
if (adapter.strategy === 'iframe-reachable') {
|
|
1399
|
+
return buildIframeReachableDescriptor2D(world, adapter, base);
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
if (adapter.strategy === 'opaque-embed') {
|
|
1403
|
+
if (!adapter.entryHtml) {
|
|
1404
|
+
throw new Error(
|
|
1405
|
+
`resolveIngest2DDescriptor: world "${world.id}" (strategy "opaque-embed") has no ` +
|
|
1406
|
+
'`entryHtml` — the opaque-embed floor needs the entry HTML file path to read (D-W2, ' +
|
|
1407
|
+
'docs/WAVE4-FTUE-HARDENING-DESIGN.md).',
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
const entryHtml = adapter.entryHtml;
|
|
1411
|
+
return { ...base, tier: 'opaque-embed', embedHtml: () => fetchProjectStaticText(entryHtml) };
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// Exhaustive per IngestStrategySchema's 4 members.
|
|
1415
|
+
const exhaustive: never = adapter.strategy;
|
|
1416
|
+
throw new Error(
|
|
1417
|
+
`resolveIngest2DDescriptor: world "${world.id}" has an unhandled ingest strategy ` +
|
|
1418
|
+
`${JSON.stringify(exhaustive)}.`,
|
|
1419
|
+
);
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
/**
|
|
1423
|
+
* Same shape as {@link resolveIngestThreeAdapter}: the pixi world2d ingest
|
|
1424
|
+
* machinery (`mountIngestGame2D`, `World2DAuthoringAdapter`) needs an
|
|
1425
|
+
* `EditorStore` and a dedicated game-container element that `HostContext`
|
|
1426
|
+
* does not provide — so this resolver does NOT attempt to make ingest-pixi
|
|
1427
|
+
* mountable through the generic `GameAdapter`/`HostContext` contract. It
|
|
1428
|
+
* returns a `GameAdapter` whose `id`/`descriptor` are built straight from the
|
|
1429
|
+
* manifest, and whose `mount()` throws a named error pointing at the real
|
|
1430
|
+
* mount path: `resolveIngest2DDescriptor` + `enterIngestModeFromManifestWorld2D`
|
|
1431
|
+
* (`ingest-mode.ts`), called from an ingest-mode-shaped flow that has a store
|
|
1432
|
+
* and a container — exactly `enterIngestModeFromManifestWorld`'s three-side
|
|
1433
|
+
* counterpart.
|
|
1434
|
+
*/
|
|
1435
|
+
function resolveIngestPixiAdapter(world: ResolvedWorldEntry, projectRoot: string): GameAdapter {
|
|
1436
|
+
const descriptor = resolveIngest2DDescriptor(world, projectRoot);
|
|
1437
|
+
return {
|
|
1438
|
+
id: world.id,
|
|
1439
|
+
/** Exposed for slice-4 callers that want the descriptor without re-deriving it. */
|
|
1440
|
+
descriptor,
|
|
1441
|
+
async mount() {
|
|
1442
|
+
throw new Error(
|
|
1443
|
+
`resolveWorldAdapter: world "${world.id}" (ingest-pixi) cannot mount through the ` +
|
|
1444
|
+
'generic GameAdapter/HostContext contract — the world2d ingest machinery ' +
|
|
1445
|
+
'(mountIngestGame2D + World2DAuthoringAdapter) requires an EditorStore and a ' +
|
|
1446
|
+
'dedicated game-container element that HostContext does not provide (mirrors the ' +
|
|
1447
|
+
"ingest-three finding, see resolveIngestPixiAdapter's doc comment). Call " +
|
|
1448
|
+
'resolveIngest2DDescriptor() + enterIngestModeFromManifestWorld2D() directly from an ' +
|
|
1449
|
+
'ingest-mode-shaped flow instead (ingest-mode.ts).',
|
|
1450
|
+
);
|
|
1451
|
+
},
|
|
1452
|
+
} as GameAdapter & { descriptor: IngestGame2D };
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
/**
|
|
1456
|
+
* FINDING (report this prominently, see the slice's "Report back"):
|
|
1457
|
+
* `mountIngestGame`/`mountIngestGameIframeReachable`
|
|
1458
|
+
* (`packages/editor/src/authoring/ingest-game-adapter.ts`,
|
|
1459
|
+
* `ingest-iframe-reachable-adapter.ts`) are NOT shaped like
|
|
1460
|
+
* `GameAdapter.mount(host: HostContext)` — they require an `EditorStore`
|
|
1461
|
+
* (threaded straight into `new IngestAuthoringAdapter(store, ...)` for
|
|
1462
|
+
* selection/undo wiring) and a dedicated game-container `HTMLElement`
|
|
1463
|
+
* (`ingest-mode.ts`'s `getGameContainer()`), neither of which `HostContext`
|
|
1464
|
+
* carries (`packages/engine/src/adapter/host-context.ts` — by design: it is
|
|
1465
|
+
* the NEUTRAL surface any game gets, and an `EditorStore` is an editor-only
|
|
1466
|
+
* concept). Building a second, `HostContext`-only capture path that skips the
|
|
1467
|
+
* authoring adapter would duplicate ~most of `mountIngestGame`'s body
|
|
1468
|
+
* (capture install, asset-URL modifier, DOM stubs, settle-wait, teardown) and
|
|
1469
|
+
* risk the two drifting — exactly the "half-refactor" the slice's brief
|
|
1470
|
+
* warns against. So this resolver does NOT attempt to make ingest-three
|
|
1471
|
+
* mountable through the generic contract: it returns a `GameAdapter` whose
|
|
1472
|
+
* `id` and descriptor are built straight from the manifest (no registry
|
|
1473
|
+
* lookup — the AC this slice's tests hold it to), and whose `mount()` throws
|
|
1474
|
+
* a named error pointing at the real mount path
|
|
1475
|
+
* (`resolveIngestDescriptor` + `mountIngestGame`/
|
|
1476
|
+
* `mountIngestGameIframeReachable`, called from an ingest-mode-shaped flow
|
|
1477
|
+
* that has a store and a container). T3.3 slice 3 wires exactly that flow:
|
|
1478
|
+
* `ingest-mode.ts`'s `enterIngestModeFromManifestWorld` calls
|
|
1479
|
+
* `resolveIngestDescriptor` + the existing `enterIngestModeWithGame` (which
|
|
1480
|
+
* already has both a store and `getGameContainer()`), satisfying slice 3's AC
|
|
1481
|
+
* ("point the CLI at an ingest game folder never registered in source ->
|
|
1482
|
+
* opens, plays, edits" — docs/CLI-ON-FOLDER-DESIGN.md §4) for the
|
|
1483
|
+
* `shared`/`deduped` strategies this function builds.
|
|
1484
|
+
*/
|
|
1485
|
+
function resolveIngestThreeAdapter(world: ResolvedWorldEntry, projectRoot: string): GameAdapter {
|
|
1486
|
+
const descriptor = resolveIngestDescriptor(world, projectRoot);
|
|
1487
|
+
return {
|
|
1488
|
+
id: world.id,
|
|
1489
|
+
/** Exposed for slice-3 callers that want the descriptor without re-deriving it. */
|
|
1490
|
+
descriptor,
|
|
1491
|
+
async mount() {
|
|
1492
|
+
throw new Error(
|
|
1493
|
+
`resolveWorldAdapter: world "${world.id}" (ingest-three) cannot mount through the ` +
|
|
1494
|
+
'generic GameAdapter/HostContext contract — the existing ingest machinery ' +
|
|
1495
|
+
'(mountIngestGame/mountIngestGameIframeReachable) requires an EditorStore and a ' +
|
|
1496
|
+
'dedicated game-container element that HostContext does not provide (T3.3 slice 2 ' +
|
|
1497
|
+
"finding, see adapter-resolver.ts's resolveIngestThreeAdapter doc comment). Call " +
|
|
1498
|
+
'resolveIngestDescriptor() + mountIngestGame() directly from an ingest-mode-shaped ' +
|
|
1499
|
+
'flow instead; full CLI-on-folder ingest wiring is T3.3 slice 3.',
|
|
1500
|
+
);
|
|
1501
|
+
},
|
|
1502
|
+
} as GameAdapter & { descriptor: IngestGame };
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// ---------------------------------------------------------------------------
|
|
1506
|
+
// Part F (T3.4 slice 1) — module (docs/ADAPTER-AUTHORING-DESIGN.md §1.A/§1.C)
|
|
1507
|
+
// ---------------------------------------------------------------------------
|
|
1508
|
+
|
|
1509
|
+
/**
|
|
1510
|
+
* Validate `adapter.module` is a game-folder-RELATIVE path with no `..`
|
|
1511
|
+
* escape (§1.A: "validated game-folder-relative (normalized, no `..` escape —
|
|
1512
|
+
* the same rule the save-file endpoint enforces)"). This resolver runs
|
|
1513
|
+
* browser-side (no `node:path`), so the guard is hand-rolled by segment —
|
|
1514
|
+
* `packages/editor/server/editor-server.ts`'s `/__editor/save-file` handler is
|
|
1515
|
+
* the Node-side sibling this mirrors (reject empty, reject any `..` segment,
|
|
1516
|
+
* reject an absolute/rooted path); there is no reusable isomorphic helper in
|
|
1517
|
+
* the codebase today (`storage/paths.ts`'s `normalize()` collapses `.`
|
|
1518
|
+
* segments but deliberately does NOT reject `..` — a different job, key
|
|
1519
|
+
* canonicalization for the storage backends, not a trust-boundary check).
|
|
1520
|
+
* Returns the normalized (slash-joined, no leading/trailing slash) path.
|
|
1521
|
+
*/
|
|
1522
|
+
export function guardModulePath(world: ResolvedWorldEntry, moduleId: string): string {
|
|
1523
|
+
const fail = (reason: string): never => {
|
|
1524
|
+
throw new Error(
|
|
1525
|
+
`resolveWorldAdapter: world "${world.id}" declares { module: "${moduleId}" } — ${reason} ` +
|
|
1526
|
+
'(docs/ADAPTER-AUTHORING-DESIGN.md §1.A: the module path must be game-folder-relative, ' +
|
|
1527
|
+
'normalized, with no `..` escape — the same rule the save-file endpoint enforces).',
|
|
1528
|
+
);
|
|
1529
|
+
};
|
|
1530
|
+
|
|
1531
|
+
if (!moduleId || moduleId.trim() === '') fail('the module path is empty');
|
|
1532
|
+
if (moduleId.includes('\\')) fail('the module path contains a backslash (not POSIX-relative)');
|
|
1533
|
+
if (moduleId.startsWith('/') || /^[a-zA-Z]:/.test(moduleId)) {
|
|
1534
|
+
fail('the module path is absolute, not game-folder-relative');
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
const segments = moduleId.split('/');
|
|
1538
|
+
if (segments.some((seg) => seg === '..')) {
|
|
1539
|
+
fail('the module path contains a `..` segment and would escape the game folder');
|
|
1540
|
+
}
|
|
1541
|
+
const normalized = segments.filter((seg) => seg.length > 0 && seg !== '.').join('/');
|
|
1542
|
+
if (normalized === '') fail('the module path resolves to nothing (empty after normalization)');
|
|
1543
|
+
|
|
1544
|
+
return normalized;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
/** Runtime shape every `GameAdapter` must satisfy — the module branch cannot
|
|
1548
|
+
* trust TypeScript alone (the imported value is `unknown` until checked). */
|
|
1549
|
+
function guardModuleShape(
|
|
1550
|
+
world: ResolvedWorldEntry,
|
|
1551
|
+
moduleId: string,
|
|
1552
|
+
candidate: unknown,
|
|
1553
|
+
): asserts candidate is GameAdapter {
|
|
1554
|
+
const fail = (reason: string): never => {
|
|
1555
|
+
throw new Error(
|
|
1556
|
+
`resolveWorldAdapter: world "${world.id}"'s custom adapter module "${moduleId}" ${reason} ` +
|
|
1557
|
+
'— a { module } adapter must default-export an object shaped like `GameAdapter`: ' +
|
|
1558
|
+
'`{ id: string, mount(host): Promise<MountedGame> }` (docs/ADAPTER-AUTHORING-DESIGN.md §1.A).',
|
|
1559
|
+
);
|
|
1560
|
+
};
|
|
1561
|
+
|
|
1562
|
+
if (candidate === null || typeof candidate !== 'object') {
|
|
1563
|
+
fail('has no default export (or it is not an object)');
|
|
1564
|
+
}
|
|
1565
|
+
const obj = candidate as Record<string, unknown>;
|
|
1566
|
+
if (typeof obj['id'] !== 'string' || obj['id'].length === 0) {
|
|
1567
|
+
fail('default-exports an object with no non-empty string `id`');
|
|
1568
|
+
}
|
|
1569
|
+
if (typeof obj['mount'] !== 'function') {
|
|
1570
|
+
fail('default-exports an object with no `mount` function');
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
/**
|
|
1575
|
+
* Wrap a custom adapter's `mount` so that, once it resolves, the ACHIEVED
|
|
1576
|
+
* tier is computed from the real `MountedGame.authoring` surface and compared
|
|
1577
|
+
* against the world's declared `capabilities.local` (§1.C). `/@fs/` module
|
|
1578
|
+
* imports only run in the local CLI dev server (`getEditorMode() ===
|
|
1579
|
+
* 'server'` — see `editor-mode.ts`; the hosted/browser build has no such
|
|
1580
|
+
* import route, matching this design's non-goal "hosted-context module
|
|
1581
|
+
* adapters beyond honest tier declaration"), so `local` is the context this
|
|
1582
|
+
* resolver actually mounts under. A shortfall never blocks the mount — the
|
|
1583
|
+
* world still returns its (unmodified) `MountedGame`; the check only OBSERVES
|
|
1584
|
+
* and logs (§1.C: "the world still mounts at its achieved tier").
|
|
1585
|
+
*
|
|
1586
|
+
* The actual `editorConsole.warn(...)` call is a LAZY dynamic import of
|
|
1587
|
+
* `./editor-console`, not a static one: this resolver is imported by headless
|
|
1588
|
+
* Node/vitest suites with no `window` (`adapter-resolver.test.ts` et al.),
|
|
1589
|
+
* and `editor-console.ts`'s module top level touches `window`
|
|
1590
|
+
* unconditionally. A static import would break every existing Node suite
|
|
1591
|
+
* that imports this resolver; `import()` only evaluates that module the
|
|
1592
|
+
* first time a shortfall is actually reported (at which point a real
|
|
1593
|
+
* `window` is expected to exist — the module-adapter mount path only runs
|
|
1594
|
+
* inside the editor's dev-server browser session, per the note above).
|
|
1595
|
+
*/
|
|
1596
|
+
function withAchievedTierCheck(world: ResolvedWorldEntry, adapter: GameAdapter): GameAdapter {
|
|
1597
|
+
return {
|
|
1598
|
+
id: adapter.id,
|
|
1599
|
+
async mount(host) {
|
|
1600
|
+
const mounted = await adapter.mount(host);
|
|
1601
|
+
const shortfall = computeTierShortfall(
|
|
1602
|
+
world.id,
|
|
1603
|
+
world.capabilities.local,
|
|
1604
|
+
computeAchievedTier(mounted),
|
|
1605
|
+
);
|
|
1606
|
+
if (shortfall) {
|
|
1607
|
+
const { editorConsole } = await import('./editor-console');
|
|
1608
|
+
editorConsole.warn(formatTierShortfallMessage(shortfall), 'adapter');
|
|
1609
|
+
}
|
|
1610
|
+
return mounted;
|
|
1611
|
+
},
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
/**
|
|
1616
|
+
* Resolve a `{ module }` adapter world (§1.A): `/@fs/`-import the game
|
|
1617
|
+
* folder's declared module — the exact route `loadProjectScripts` uses for
|
|
1618
|
+
* `default-three`'s `entry` branch (`./script-loader.ts`), so it inherits the
|
|
1619
|
+
* SAME `resolve.dedupe: ['three', 'pixi.js']` collapse the root
|
|
1620
|
+
* `vite.config.ts` already applies repo-wide (proven for THIS path by
|
|
1621
|
+
* `vite-dedupe-module-adapter.test.ts`'s own fixture — §1.A: "Dedupe is NOT
|
|
1622
|
+
* inherited silently"). Path-guarded (`guardModulePath`) before the import,
|
|
1623
|
+
* shape-checked (`guardModuleShape`) after — both loud, named failures, never
|
|
1624
|
+
* a silent `undefined`/crash deep in the mount call. The returned adapter's
|
|
1625
|
+
* `mount` is wrapped with the achieved-tier check (§1.C, `withAchievedTierCheck`).
|
|
1626
|
+
*/
|
|
1627
|
+
export async function resolveModuleAdapter(
|
|
1628
|
+
world: ResolvedWorldEntry,
|
|
1629
|
+
projectRoot: string,
|
|
1630
|
+
): Promise<GameAdapter> {
|
|
1631
|
+
const { adapter } = world;
|
|
1632
|
+
if (adapter.type !== 'module') {
|
|
1633
|
+
throw new Error(`resolveModuleAdapter: world "${world.id}" is not a { module } world.`);
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
const normalizedPath = guardModulePath(world, adapter.module);
|
|
1637
|
+
const modulePath = fsImportPath(projectRoot, normalizedPath);
|
|
1638
|
+
const mod = (await import(/* @vite-ignore */ modulePath)) as { default?: unknown };
|
|
1639
|
+
const candidate = mod.default;
|
|
1640
|
+
guardModuleShape(world, adapter.module, candidate);
|
|
1641
|
+
|
|
1642
|
+
return withAchievedTierCheck(world, candidate);
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
// ---------------------------------------------------------------------------
|
|
1646
|
+
// Entry point
|
|
1647
|
+
// ---------------------------------------------------------------------------
|
|
1648
|
+
|
|
1649
|
+
/**
|
|
1650
|
+
* Resolve a manifest world entry into a mountable {@link GameAdapter}
|
|
1651
|
+
* (docs/CLI-ON-FOLDER-DESIGN.md §1D). Instantiation only — the returned
|
|
1652
|
+
* adapter is not yet mounted; call `.mount(host)` to run it (see each
|
|
1653
|
+
* identity's doc comment above for what that does or doesn't support today).
|
|
1654
|
+
*/
|
|
1655
|
+
export async function resolveWorldAdapter(
|
|
1656
|
+
world: ResolvedWorldEntry,
|
|
1657
|
+
projectRoot: string,
|
|
1658
|
+
opts?: ResolveWorldAdapterOptions,
|
|
1659
|
+
): Promise<GameAdapter> {
|
|
1660
|
+
const { adapter } = world;
|
|
1661
|
+
|
|
1662
|
+
switch (adapter.identity) {
|
|
1663
|
+
case 'default-three':
|
|
1664
|
+
return resolveDefaultThreeAdapter(world, projectRoot, opts);
|
|
1665
|
+
case 'default-pixi':
|
|
1666
|
+
return resolveDefaultPixiAdapter(world, projectRoot);
|
|
1667
|
+
case 'default-react':
|
|
1668
|
+
return resolveDefaultReactAdapter(world, projectRoot);
|
|
1669
|
+
case 'module':
|
|
1670
|
+
return resolveModuleAdapter(world, projectRoot);
|
|
1671
|
+
case 'ingest-pixi':
|
|
1672
|
+
return resolveIngestPixiAdapter(world, projectRoot);
|
|
1673
|
+
case 'ingest-three':
|
|
1674
|
+
return resolveIngestThreeAdapter(world, projectRoot);
|
|
1675
|
+
case 'ingest-react':
|
|
1676
|
+
return resolveIngestReactAdapter(world, projectRoot);
|
|
1677
|
+
default: {
|
|
1678
|
+
const exhaustive: never = adapter;
|
|
1679
|
+
throw new Error(
|
|
1680
|
+
`resolveWorldAdapter: unhandled adapter identity ${JSON.stringify(exhaustive)}`,
|
|
1681
|
+
);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
// ---------------------------------------------------------------------------
|
|
1687
|
+
// Multi-world entry point (T6.1 slice 2 — docs/MULTI-WORLD-DESIGN.md §3 slice 2)
|
|
1688
|
+
// ---------------------------------------------------------------------------
|
|
1689
|
+
|
|
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
|
+
/**
|
|
1761
|
+
* 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.
|
|
1767
|
+
*/
|
|
1768
|
+
async function resolveAllWorldAdapters(
|
|
1769
|
+
manifest: ResolvedGameManifest,
|
|
1770
|
+
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) });
|
|
1785
|
+
}
|
|
1786
|
+
return resolved;
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
export async function resolveAllWorlds(
|
|
1790
|
+
manifest: ResolvedGameManifest,
|
|
1791
|
+
projectRoot: string,
|
|
1792
|
+
): Promise<WorldMountSpec[]> {
|
|
1793
|
+
const resolved = await resolveAllWorldAdapters(manifest, projectRoot);
|
|
1794
|
+
|
|
1795
|
+
const specs: WorldMountSpec[] = [];
|
|
1796
|
+
for (const { world, adapter } of resolved) {
|
|
1797
|
+
const base = { id: world.id, zOrder: world.zOrder, pausable: world.pausable, loop: world.loop };
|
|
1798
|
+
if (world.kind === 'pixijs') {
|
|
1799
|
+
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') {
|
|
1803
|
+
specs.push({ ...base, kind: 'threejs', adapter });
|
|
1804
|
+
} 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
|
|
1808
|
+
// NOT a plain-`string` field) — a hypothetical 4th kind must fail to
|
|
1809
|
+
// compile here rather than silently default to 'threejs' as the old
|
|
1810
|
+
// catch-all `else` did.
|
|
1811
|
+
assertNever(world.kind, 'resolveAllWorlds');
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
return specs;
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
/**
|
|
1818
|
+
* E4.R2 (reopen fix — design/26 S-D D2's "editor play mode calls it"):
|
|
1819
|
+
* `resolveAllWorlds`'s composer-path sibling. Resolves every world's adapter
|
|
1820
|
+
* through the SAME scope-checked loop ({@link resolveAllWorldAdapters}) but
|
|
1821
|
+
* returns `mountManifestWorlds`'s own `entries` shape (`{ kind, adapter }`
|
|
1822
|
+
* per world id, `@engine/runtime/mount-manifest`) instead of a hand-built
|
|
1823
|
+
* `WorldMountSpec[]` — so `zOrder`/`pausable`/`loop` and the per-kind
|
|
1824
|
+
* `WorldMountSpec` ASSEMBLY come from `mount-manifest.ts`'s
|
|
1825
|
+
* `buildWorldMountSpec`, the one authoritative place that translation lives,
|
|
1826
|
+
* rather than being hand-duplicated a second time here AND at the
|
|
1827
|
+
* `mountManifestWorlds` call site (the exact duplication a reopen review
|
|
1828
|
+
* found in `play-mode.ts`'s prior `resolveAllWorlds` + `createGameRuntime
|
|
1829
|
+
* ({ container, worlds })` call shape).
|
|
1830
|
+
*
|
|
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 })`.
|
|
1836
|
+
*/
|
|
1837
|
+
export async function resolveAllWorldEntries(
|
|
1838
|
+
manifest: ResolvedGameManifest,
|
|
1839
|
+
projectRoot: string,
|
|
1840
|
+
): Promise<Record<string, MountEntry>> {
|
|
1841
|
+
const resolved = await resolveAllWorldAdapters(manifest, projectRoot);
|
|
1842
|
+
|
|
1843
|
+
const entries: Record<string, MountEntry> = {};
|
|
1844
|
+
for (const { world, adapter } of resolved) {
|
|
1845
|
+
if (world.kind === 'pixijs') {
|
|
1846
|
+
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') {
|
|
1850
|
+
entries[world.id] = { kind: 'threejs', adapter };
|
|
1851
|
+
} else {
|
|
1852
|
+
// Exhaustiveness guard (§7.4-2) — same reasoning as `resolveAllWorlds`'s
|
|
1853
|
+
// own identical guard above.
|
|
1854
|
+
assertNever(world.kind, 'resolveAllWorldEntries');
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
return entries;
|
|
1858
|
+
}
|