@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,2092 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ingest-mode orchestrator — load an UNMODIFIED three.js OR PixiJS game into
|
|
3
|
+
* the real editor and make its live scene editable, with ZERO edits to the
|
|
4
|
+
* game's code.
|
|
5
|
+
*
|
|
6
|
+
* Phase B (the inversion done right): this is a thin HOST. It mounts the game
|
|
7
|
+
* through the {@link mountIngestGame} `GameAdapter` path — which captures the
|
|
8
|
+
* game's live scene and returns a `MountedGame` whose `authoring` is an
|
|
9
|
+
* {@link IngestAuthoringAdapter} implemented DIRECTLY over the live `Object3D`
|
|
10
|
+
* tree (NO fabricated `SceneEntity`). The host then:
|
|
11
|
+
* - installs that adapter as the editor's active authoring override, so the
|
|
12
|
+
* hierarchy + inspector render from the adapter's providers, and
|
|
13
|
+
* - swaps the editor viewport onto the live scene (`enterPlayScene`, which binds
|
|
14
|
+
* by the stable `entityId` the adapter tagged) so selection + gizmo work.
|
|
15
|
+
*
|
|
16
|
+
* The first-party `.vscn` editor is untouched; the SAME editor surface now drives
|
|
17
|
+
* a foreign game. See `docs/ADAPTER-ARCHITECTURE.md` §5 (migration, steps 3–4).
|
|
18
|
+
*
|
|
19
|
+
* T6.1 slice 4 (docs/MULTI-WORLD-DESIGN.md §1.E/§3 slice 4, world2d silo
|
|
20
|
+
* retirement) adds the PixiJS sibling of this same shape below
|
|
21
|
+
* (`enterIngestModeFromManifestWorld2D` + `tryManifestIngestRoute2D`):
|
|
22
|
+
* `resolveIngest2DDescriptor` (adapter-resolver.ts) builds an `IngestGame2D`
|
|
23
|
+
* straight from an `ingest-pixi` manifest world, `mountIngestGame2D` captures
|
|
24
|
+
* its live Pixi stage via the SAME shared-instance render-trap mechanics as
|
|
25
|
+
* the three-side `shared`/`deduped` strategies, and a `World2DAuthoringAdapter`
|
|
26
|
+
* (the pixi peer of `IngestAuthoringAdapter`) becomes the active authoring
|
|
27
|
+
* override — no `SceneEntity`, no separate editor data model, exactly like
|
|
28
|
+
* the three-side path. The silo's `world2d-mode.ts` (deleted this slice) used
|
|
29
|
+
* to own this kind of session for its 'vendored'/'realgame' hand-launched
|
|
30
|
+
* demos; this is its manifest-driven, discoverable-fixture replacement.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { readGameContract } from '@engine/adapter/ingest/game-contract';
|
|
34
|
+
import { extractUpstreamPin } from '@engine/adapter/ingest/upstream-pin';
|
|
35
|
+
import type { ResolvedGameManifest, ResolvedWorldEntry } from '@engine/manifest/load';
|
|
36
|
+
import type { Tier } from '@engine/manifest/schema';
|
|
37
|
+
import type { Overlay2D } from '@engine/world2d/authoring-2d';
|
|
38
|
+
import {
|
|
39
|
+
type EmbedMount2D,
|
|
40
|
+
type IframeReachableMount2D,
|
|
41
|
+
mountIngestGame2DEmbed,
|
|
42
|
+
mountIngestGame2DIframeReachableMulti,
|
|
43
|
+
} from '@engine/world2d/ingest-iframe-2d';
|
|
44
|
+
import type { IngestGame2D, IngestMount2D } from '@engine/world2d/ingest2d';
|
|
45
|
+
import { mountIngestGame2D } from '@engine/world2d/ingest2d';
|
|
46
|
+
import { createPhysics2DRegistry } from '@engine/world2d/physics2d-registry';
|
|
47
|
+
import { createPhysicsAdapter2D } from '@engine/world2d/system-adapters-2d';
|
|
48
|
+
import type { Container } from 'pixi.js';
|
|
49
|
+
import * as PIXI from 'pixi.js';
|
|
50
|
+
import {
|
|
51
|
+
computeAchievedTier,
|
|
52
|
+
computeTierShortfall,
|
|
53
|
+
formatTierShortfallMessage,
|
|
54
|
+
} from './achieved-tier';
|
|
55
|
+
import {
|
|
56
|
+
resolveAllWorlds,
|
|
57
|
+
resolveIngest2DDescriptor,
|
|
58
|
+
resolveIngestDescriptor,
|
|
59
|
+
} from './adapter-resolver';
|
|
60
|
+
import { getAuthoringOverride, setActiveAuthoring } from './authoring/active-adapter';
|
|
61
|
+
import { getActiveSystems, setActiveSystems } from './authoring/active-systems';
|
|
62
|
+
import {
|
|
63
|
+
CompositeAuthoringAdapter,
|
|
64
|
+
type CompositeChild,
|
|
65
|
+
} from './authoring/composite-authoring-adapter';
|
|
66
|
+
import { makeEmbedOnlyAdapter } from './authoring/embed-only-adapter';
|
|
67
|
+
import {
|
|
68
|
+
type IngestMount,
|
|
69
|
+
type MountIngestOptions,
|
|
70
|
+
mountIngestGame,
|
|
71
|
+
} from './authoring/ingest-game-adapter';
|
|
72
|
+
import { type EmbedMount, mountIngestGameEmbed } from './authoring/ingest-iframe-adapter';
|
|
73
|
+
import {
|
|
74
|
+
mountIngestGameIframeReachable,
|
|
75
|
+
mountIngestGameIframeReachableBundle,
|
|
76
|
+
} from './authoring/ingest-iframe-reachable-adapter';
|
|
77
|
+
import { loadOverlay, overlayPath, saveOverlay } from './authoring/ingest-overlay';
|
|
78
|
+
import {
|
|
79
|
+
addMountFailureReport,
|
|
80
|
+
formatMountFailureMessage,
|
|
81
|
+
getMountFailureReports,
|
|
82
|
+
setMountFailureReport,
|
|
83
|
+
} from './authoring/mount-failure-report';
|
|
84
|
+
import {
|
|
85
|
+
buildOverlayApplyReport,
|
|
86
|
+
logOverlayApplyReport,
|
|
87
|
+
setActiveOverlayReport,
|
|
88
|
+
} from './authoring/overlay-report';
|
|
89
|
+
import {
|
|
90
|
+
ReactDomAuthoringAdapter,
|
|
91
|
+
type ReactDomOverlay,
|
|
92
|
+
} from './authoring/react-dom-authoring-adapter';
|
|
93
|
+
import { unregisterUndoAdapter } from './authoring/undo-timeline';
|
|
94
|
+
import { loadWorldOverlay, saveWorldOverlay, worldOverlayPath } from './authoring/world-overlay';
|
|
95
|
+
import { World2DAuthoringAdapter } from './authoring/world2d-authoring-adapter';
|
|
96
|
+
import { editorConsole } from './editor-console';
|
|
97
|
+
import { getEditorMode } from './editor-mode';
|
|
98
|
+
import type { EditorStore } from './editor-store';
|
|
99
|
+
import { setGameInputGate } from './gated-globals';
|
|
100
|
+
import { getIngestGame } from './ingest/discovery';
|
|
101
|
+
import { getIngestGameReact, ingestGameReactProjectRoot } from './ingest/discovery-react';
|
|
102
|
+
import { getIngestGame2D } from './ingest/discovery2d';
|
|
103
|
+
import type { IngestGame } from './ingest/types';
|
|
104
|
+
// TYPE-ONLY (D-V2): `ingest-siblings.ts` imports from `module-mode.ts`, which
|
|
105
|
+
// itself statically imports `waitForGameContainer` from THIS module — a
|
|
106
|
+
// value import here would close that cycle at the wrong end. The actual
|
|
107
|
+
// `mountIngestSiblings` call below is a dynamic `import()`, right where it's
|
|
108
|
+
// used (`mountCompositeSiblings`); only the TYPE is needed at the top level,
|
|
109
|
+
// and type-only imports are erased before the cycle could ever matter.
|
|
110
|
+
import type { SiblingMount } from './ingest-siblings';
|
|
111
|
+
import { fetchGameManifest } from './manifest-project';
|
|
112
|
+
import { getGameContainer } from './play-mode';
|
|
113
|
+
import { getCurrentProject } from './project-manager';
|
|
114
|
+
|
|
115
|
+
interface IngestSession {
|
|
116
|
+
store: EditorStore;
|
|
117
|
+
/** Shared-`three` capture path (a real authoring adapter over the live scene). */
|
|
118
|
+
mount?: IngestMount;
|
|
119
|
+
/** Opaque-bundle embed path (iframe-hosted, embed-only). */
|
|
120
|
+
embed?: EmbedMount;
|
|
121
|
+
/** Keeps the captured game renderer sized to its container (host lifecycle). */
|
|
122
|
+
resizeObserver?: ResizeObserver;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* F26 slot collapse (D-V1, docs/WAVE5-MULTIWORLD-INGEST-DESIGN.md): at most
|
|
127
|
+
* one ingest session is EVER active, of any kind — this is the single
|
|
128
|
+
* module-level slot for it. Pre-collapse, this was three independent
|
|
129
|
+
* per-kind singletons (`_session`/`_session2D`/`_sessionReact`, one per
|
|
130
|
+
* `IngestSession*` shape below), each with its own entry guard that only
|
|
131
|
+
* tore down its OWN slot — so switching kinds leaked the prior kind's live
|
|
132
|
+
* session (F26 residual, docs/FTUE-EXTERNAL-R3F-GAME.md :222-231).
|
|
133
|
+
* `IngestSession2D`/`IngestSessionReact` are declared further down (their
|
|
134
|
+
* own sections), referenced here by forward type reference.
|
|
135
|
+
*
|
|
136
|
+
* `siblings` (D-V2, docs/WAVE5-MULTIWORLD-INGEST-DESIGN.md, F24 composite):
|
|
137
|
+
* a composite manifest's OTHER worlds, mounted beside this session's ingest
|
|
138
|
+
* world (`ingest-siblings.ts`'s `mountIngestSiblings`) — always `[]` for
|
|
139
|
+
* every non-composite entry path (the in-tree fixture glob route, the
|
|
140
|
+
* `?ingest=` URL route, a single-world manifest). `exitActiveIngest` below
|
|
141
|
+
* disposes these, reverse order, in EVERY kind's teardown path — a sibling
|
|
142
|
+
* never outlives the session it was mounted beside.
|
|
143
|
+
*/
|
|
144
|
+
type ActiveIngest = (
|
|
145
|
+
| { kind: 'threejs'; session: IngestSession }
|
|
146
|
+
| { kind: 'pixi'; session: IngestSession2D }
|
|
147
|
+
| { kind: 'react'; session: IngestSessionReact }
|
|
148
|
+
) & { siblings: SiblingMount[] };
|
|
149
|
+
|
|
150
|
+
let _active: ActiveIngest | null = null;
|
|
151
|
+
let _boundStore: EditorStore | null = null;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Shared shortfall-reporting step for both embed-only degradation points in
|
|
155
|
+
* {@link enterIngestModeWithGame} below (T3.4 slice 1 retrofit, §1.C): both
|
|
156
|
+
* degrade to the achieved tier 'opaque-embed'; report a structured shortfall
|
|
157
|
+
* only when a declared tier is actually known (anti-shim — never fabricate
|
|
158
|
+
* one). Returns the report (for the `__vgaiIngest` dev hook) or `null`.
|
|
159
|
+
*/
|
|
160
|
+
function reportEmbedOnlyShortfall(
|
|
161
|
+
worldId: string,
|
|
162
|
+
declaredTier: Tier | undefined,
|
|
163
|
+
): ReturnType<typeof computeTierShortfall> {
|
|
164
|
+
if (declaredTier === undefined) return null;
|
|
165
|
+
const shortfall = computeTierShortfall(worldId, declaredTier, 'opaque-embed');
|
|
166
|
+
if (shortfall) editorConsole.warn(formatTierShortfallMessage(shortfall), 'adapter');
|
|
167
|
+
return shortfall;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* R5 (docs/MASTER-ARCHITECTURE-REVIEW.md §4(c)/§7.1 item 9): best-effort read
|
|
172
|
+
* of the CURRENTLY-OPEN project's `UPSTREAM.md` pin, for the overlay-identity
|
|
173
|
+
* fix — binding a saved overlay to the vendored game's actual upstream commit
|
|
174
|
+
* rather than only its (often static) manifest `version`.
|
|
175
|
+
*
|
|
176
|
+
* Fetches root-relative `/UPSTREAM.md` — the SAME "the dev server's document
|
|
177
|
+
* root IS the open project's root" convention `./manifest-project.ts`'s
|
|
178
|
+
* `fetchGameManifest` already relies on for `/vgai.game.json` (no `/@fs/`
|
|
179
|
+
* needed; that route is for ESM import specifiers, not plain-text reads).
|
|
180
|
+
* Every failure mode resolves to `null`, never throws: no dev server
|
|
181
|
+
* (`getEditorMode() === 'browser'`), a 404 (the common case — most projects/
|
|
182
|
+
* fixtures are NOT vendored upstream games and have no `UPSTREAM.md` at all,
|
|
183
|
+
* e.g. every in-tree fixture under `packages/editor/src/ingest/games/*`, or
|
|
184
|
+
* an externally-authored non-vendored project opened via the CLI-on-folder
|
|
185
|
+
* route), or a network error. READ-ONLY: this never writes `UPSTREAM.md`
|
|
186
|
+
* (anti-shim — the pin is read, never fabricated or persisted back).
|
|
187
|
+
*
|
|
188
|
+
* Only wired into the manifest / CLI-on-folder route below
|
|
189
|
+
* (`tryManifestIngestRoute`) — the in-tree fixture route (`enterIngestMode`)
|
|
190
|
+
* has no project root the dev server is serving as document root beyond the
|
|
191
|
+
* editor's OWN source tree, so there is no meaningful "current project" to
|
|
192
|
+
* fetch an `UPSTREAM.md` for there; it correctly stays `null`/never-called,
|
|
193
|
+
* same honest outcome as any other non-vendored folder.
|
|
194
|
+
*/
|
|
195
|
+
export async function readVendoredUpstreamPin(): Promise<string | null> {
|
|
196
|
+
if (getEditorMode() === 'browser') return null; // no dev server to serve /UPSTREAM.md from
|
|
197
|
+
try {
|
|
198
|
+
const res = await fetch('/UPSTREAM.md');
|
|
199
|
+
if (!res.ok) return null;
|
|
200
|
+
return extractUpstreamPin(await res.text());
|
|
201
|
+
} catch {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Bind the editor store so the dev hook / UI can launch ingestion. Called
|
|
208
|
+
* once per `ViewportPanel` mount, right before `loadInitialScene` +
|
|
209
|
+
* `autoLaunchIngestFromURL` — i.e. once per project session (`AppRoot`
|
|
210
|
+
* unmounts/remounts `DefaultEditorLayout` under a fresh `EditorProvider` on
|
|
211
|
+
* "return to project screen" -> reopen).
|
|
212
|
+
*
|
|
213
|
+
* D-W3: there is no OTHER project-switch teardown hook in this codebase
|
|
214
|
+
* today — `switchProject`/`onReturnToProjectScreen` reset React state, not
|
|
215
|
+
* this module-level ingest singleton (`_active`, D-V1's slot collapse, and
|
|
216
|
+
* the `active-adapter.ts`/`overlay-report.ts` globals it sets), and nothing
|
|
217
|
+
* calls `exitIngestMode`/`exitModuleMode` on that path — a pre-existing gap,
|
|
218
|
+
* not introduced or fixed here. This bind call is
|
|
219
|
+
* the earliest point every new project session reaches, so it is the best
|
|
220
|
+
* available place to make sure a PREVIOUS project's stale mount-failure
|
|
221
|
+
* banner never bleeds into a freshly opened one.
|
|
222
|
+
*/
|
|
223
|
+
export function bindIngestMode(store: EditorStore): void {
|
|
224
|
+
_boundStore = store;
|
|
225
|
+
setMountFailureReport(null);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* The world kind of the currently-active ingest session, if any (F26,
|
|
230
|
+
* VGAI_FTUE_LOG.md): ingest is an ADAPTER TIER, not a mode of one kind — D-V1
|
|
231
|
+
* collapsed the three per-kind session slots this used to fixed-priority-scan
|
|
232
|
+
* (`_session` / `_session2D` / `_sessionReact`) into the single `_active` slot
|
|
233
|
+
* above, so at most one session is ever active, of any kind, and this is just
|
|
234
|
+
* that slot's tag. The play surface ({@link getIngestPlayControl}) and
|
|
235
|
+
* {@link isIngestActive} read this, never a slot directly.
|
|
236
|
+
*/
|
|
237
|
+
export type IngestKind = 'threejs' | 'pixi' | 'react';
|
|
238
|
+
|
|
239
|
+
export function activeIngestKind(): IngestKind | null {
|
|
240
|
+
return _active?.kind ?? null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** True if an unmodified game is currently ingested — ANY kind (F26). */
|
|
244
|
+
export function isIngestActive(): boolean {
|
|
245
|
+
return activeIngestKind() !== null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The play-state control surface for the CURRENT ingest session (F17/F21,
|
|
250
|
+
* docs/FTUE-EXTERNAL-R3F-GAME.md): mounts are COLD by doctrine — the adapter
|
|
251
|
+
* sets `window.__vgaiMountCold` before the game's entry executes, and a
|
|
252
|
+
* session-driven game may defer its side-effects (backend connection,
|
|
253
|
+
* narrative, audio) until the first `play()`. Everything is contract-first
|
|
254
|
+
* (game-contract.ts): `lifecycle.start` is called AT MOST ONCE per mount —
|
|
255
|
+
* later ▶ presses resume rather than re-start, so games never need an
|
|
256
|
+
* idempotent start(); `lifecycle.pause`/`resume` are preferred over the
|
|
257
|
+
* outside-in loop gate (`MountedGame.setPaused`), which cannot gate a raw
|
|
258
|
+
* requestAnimationFrame loop (R3F games) at all. Games that declare nothing
|
|
259
|
+
* get the pre-contract wire event + loop gate, exactly as before — nothing
|
|
260
|
+
* is demanded of them.
|
|
261
|
+
*/
|
|
262
|
+
export interface IngestPlayControl {
|
|
263
|
+
readonly playing: boolean;
|
|
264
|
+
play(): void;
|
|
265
|
+
pause(): void;
|
|
266
|
+
resume(): void;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
let _ingestPlaying = false;
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* F26: the play surface's state is session-global (one active session,
|
|
273
|
+
* whatever its kind), so EVERY per-kind exit path resets it — the playing
|
|
274
|
+
* flag plus the cold-mount/played window globals the entry paths set.
|
|
275
|
+
*/
|
|
276
|
+
function resetIngestPlaySurface(): void {
|
|
277
|
+
_ingestPlaying = false;
|
|
278
|
+
const w = window as unknown as { __vgaiMountCold?: boolean; __vgaiIngestPlayed?: boolean };
|
|
279
|
+
delete w.__vgaiMountCold;
|
|
280
|
+
delete w.__vgaiIngestPlayed;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* F26 helper: the active session's kind + its own honest pause capability
|
|
285
|
+
* (D-V1: dispatches on the single `_active` slot, never a per-kind global).
|
|
286
|
+
* `null` when nothing is ingested OR the session is opaque-embed (iframe
|
|
287
|
+
* realm: the game-contract global is unreachable and there is no loop to
|
|
288
|
+
* gate — no play surface is honest there).
|
|
289
|
+
*/
|
|
290
|
+
function activeIngestSurface(): { kind: IngestKind; setPaused(paused: boolean): void } | null {
|
|
291
|
+
const active = _active;
|
|
292
|
+
if (!active) return null;
|
|
293
|
+
switch (active.kind) {
|
|
294
|
+
case 'threejs': {
|
|
295
|
+
const mount = active.session.mount;
|
|
296
|
+
if (!mount) return null; // opaque-embed
|
|
297
|
+
return { kind: 'threejs', setPaused: (p) => mount.game.setPaused?.(p) };
|
|
298
|
+
}
|
|
299
|
+
case 'pixi': {
|
|
300
|
+
const session = active.session;
|
|
301
|
+
if (session.embed) return null; // D-W2 opaque-embed — no honest play surface, same doctrine as three's embed branch
|
|
302
|
+
return { kind: 'pixi', setPaused: (p) => session.setPaused(p) };
|
|
303
|
+
}
|
|
304
|
+
case 'react':
|
|
305
|
+
return { kind: 'react', setPaused: (p) => active.session.setPaused(p) };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* D-V1 slot collapse: tears down WHATEVER kind is currently active,
|
|
311
|
+
* dispatching to that kind's own teardown body (`disposeThreejsSession`/
|
|
312
|
+
* `disposePixiSession`/`disposeReactSession`, each below in its own
|
|
313
|
+
* section) — the exact same per-kind dispose semantics as before the
|
|
314
|
+
* collapse (resizeObserver disconnect, `exitPlayScene`, mount/embed
|
|
315
|
+
* dispose, undo-adapter unregister, authoring/systems/overlay-report/
|
|
316
|
+
* mount-failure clears, canvas removal, react `stop()`), just no longer
|
|
317
|
+
* gated behind "is THIS kind the one currently active" — every
|
|
318
|
+
* `enterIngestMode*` entry guard calls this unconditionally, closing the
|
|
319
|
+
* cross-kind leak (F26 residual): switching kinds always disposes the
|
|
320
|
+
* prior session, whichever kind it was. `exitIngestMode`/`exitIngestMode2D`/
|
|
321
|
+
* `exitIngestModeReact` (the frozen public API) become thin wrappers around
|
|
322
|
+
* this that only act when `_active` is their own kind — so a same-kind
|
|
323
|
+
* caller sees the exact same observable behavior as before the collapse.
|
|
324
|
+
*
|
|
325
|
+
* D-V2 (docs/WAVE5-MULTIWORLD-INGEST-DESIGN.md, F24 composite): a composite
|
|
326
|
+
* session's `siblings` (mounted by `ingest-siblings.ts`'s
|
|
327
|
+
* `mountIngestSiblings`, attached via `attachIngestSiblings` below) tear
|
|
328
|
+
* down FIRST, in REVERSE mount order, before the primary session's own
|
|
329
|
+
* per-kind dispose body runs — every kind shares this ONE teardown path, so
|
|
330
|
+
* a sibling never outlives the ingest session it was mounted beside. A
|
|
331
|
+
* throwing sibling `dispose()` is caught individually (never blocks the rest
|
|
332
|
+
* of teardown) and logged loudly, never silently swallowed.
|
|
333
|
+
*/
|
|
334
|
+
function exitActiveIngest(): void {
|
|
335
|
+
const active = _active;
|
|
336
|
+
if (!active) return;
|
|
337
|
+
_active = null;
|
|
338
|
+
resetIngestPlaySurface();
|
|
339
|
+
for (let i = active.siblings.length - 1; i >= 0; i--) {
|
|
340
|
+
try {
|
|
341
|
+
active.siblings[i]!.dispose();
|
|
342
|
+
} catch (err) {
|
|
343
|
+
editorConsole.error(`Composite sibling dispose failed: ${err}`, 'ingest');
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
switch (active.kind) {
|
|
347
|
+
case 'threejs':
|
|
348
|
+
disposeThreejsSession(active.session);
|
|
349
|
+
return;
|
|
350
|
+
case 'pixi':
|
|
351
|
+
disposePixiSession(active.session);
|
|
352
|
+
return;
|
|
353
|
+
case 'react':
|
|
354
|
+
disposeReactSession(active.session);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* D-B2 (docs/WAVE6-DOCTOR-DESIGN.md Slice B, wave-5 residual closure): fan a
|
|
361
|
+
* pause/resume state out to every mounted composite sibling's OPTIONAL
|
|
362
|
+
* `SiblingMount.setPaused` (`ingest-siblings.ts`), AFTER the primary
|
|
363
|
+
* surface's own pause/resume already ran above — mirrors
|
|
364
|
+
* `exitActiveIngest`'s reverse-order dispose fan-out just above (the same
|
|
365
|
+
* "one bad sibling never breaks the rest" isolation: per-sibling try/catch,
|
|
366
|
+
* loud log on failure, never swallowed silently). A sibling with no
|
|
367
|
+
* `setPaused` at all (the bare `default-react` sibling, D-B1 — it has no
|
|
368
|
+
* loop to gate) is silently skipped via the optional call — never an error,
|
|
369
|
+
* never a fabricated pause. A no-op (empty `siblings`, or no `_active`
|
|
370
|
+
* session at all) for every non-composite entry path, unchanged.
|
|
371
|
+
*/
|
|
372
|
+
function fanOutSiblingPause(paused: boolean): void {
|
|
373
|
+
const active = _active;
|
|
374
|
+
if (!active) return;
|
|
375
|
+
for (const sibling of active.siblings) {
|
|
376
|
+
try {
|
|
377
|
+
sibling.setPaused?.(paused);
|
|
378
|
+
} catch (err) {
|
|
379
|
+
editorConsole.error(`Composite sibling setPaused(${paused}) failed: ${err}`, 'ingest');
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function getIngestPlayControl(): IngestPlayControl | null {
|
|
385
|
+
const surface = activeIngestSurface();
|
|
386
|
+
if (!surface) return null;
|
|
387
|
+
const resume = () => {
|
|
388
|
+
const lifecycle = readGameContract()?.lifecycle;
|
|
389
|
+
if (lifecycle?.resume) lifecycle.resume();
|
|
390
|
+
else surface.setPaused(false);
|
|
391
|
+
fanOutSiblingPause(false);
|
|
392
|
+
_ingestPlaying = true;
|
|
393
|
+
};
|
|
394
|
+
return {
|
|
395
|
+
get playing() {
|
|
396
|
+
return _ingestPlaying;
|
|
397
|
+
},
|
|
398
|
+
play() {
|
|
399
|
+
// Late-constructed game clients must not re-defer: the flag records
|
|
400
|
+
// that play already happened this session — and makes ▶-after-⏸ a
|
|
401
|
+
// RESUME, so `lifecycle.start` fires at most once per mount.
|
|
402
|
+
const w = window as unknown as { __vgaiIngestPlayed?: boolean };
|
|
403
|
+
if (w.__vgaiIngestPlayed) return resume();
|
|
404
|
+
w.__vgaiIngestPlayed = true;
|
|
405
|
+
// Contract-first: a game declaring `vgaiGame.lifecycle.start` gets the
|
|
406
|
+
// typed call; otherwise the wire event covers pre-contract games.
|
|
407
|
+
const lifecycle = readGameContract()?.lifecycle;
|
|
408
|
+
if (lifecycle?.start) lifecycle.start();
|
|
409
|
+
else window.dispatchEvent(new CustomEvent('vgai:ingest-play'));
|
|
410
|
+
surface.setPaused(false);
|
|
411
|
+
fanOutSiblingPause(false);
|
|
412
|
+
_ingestPlaying = true;
|
|
413
|
+
},
|
|
414
|
+
pause() {
|
|
415
|
+
const lifecycle = readGameContract()?.lifecycle;
|
|
416
|
+
if (lifecycle?.pause) lifecycle.pause();
|
|
417
|
+
else surface.setPaused(true);
|
|
418
|
+
fanOutSiblingPause(true);
|
|
419
|
+
_ingestPlaying = false;
|
|
420
|
+
},
|
|
421
|
+
resume,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* D-W1 (docs/WAVE4-FTUE-HARDENING-DESIGN.md) — mount an `IngestGame` whose
|
|
427
|
+
* descriptor carries a `bundleUrl` (an externalized-`three` prebuilt bundle)
|
|
428
|
+
* via `mountIngestGameIframeReachableBundle` — the three-side sibling of
|
|
429
|
+
* {@link mountIframeReachableGame2D} below. `extraDeps` is a LAZY thunk on
|
|
430
|
+
* the descriptor (`resolveIngestDescriptor`, adapter-resolver.ts) — invoked
|
|
431
|
+
* here, right before the actual mount, so the host-namespace registry's
|
|
432
|
+
* dynamic imports only ever run for a game actually being ingested (same
|
|
433
|
+
* "resolve lazily, at mount time" discipline as the pixi sibling).
|
|
434
|
+
* `assetBaseUrl` is root-relative-resolved against the editor page's own URL
|
|
435
|
+
* here too (a static manifest can't know the dev-server port) — the exact
|
|
436
|
+
* `mountIframeReachableGame2D` idiom this mirrors.
|
|
437
|
+
*/
|
|
438
|
+
async function mountThreeIframeReachableBundle(
|
|
439
|
+
game: IngestGame,
|
|
440
|
+
container: HTMLElement,
|
|
441
|
+
store: EditorStore,
|
|
442
|
+
opts: MountIngestOptions,
|
|
443
|
+
): Promise<IngestMount> {
|
|
444
|
+
if (!game.bundleUrl) {
|
|
445
|
+
throw new Error(
|
|
446
|
+
'mountThreeIframeReachableBundle: called with no `bundleUrl` on the descriptor — ' +
|
|
447
|
+
'unreachable given the caller already checked `game.bundleUrl` (bug if hit).',
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
const resolvedExtraDeps = game.extraDeps ? await game.extraDeps() : { namespaces: {}, urls: {} };
|
|
451
|
+
// The asset-adapter URL modifier needs an ABSOLUTE URL (see
|
|
452
|
+
// `makeAssetUrlModifier`'s doc comment), but a static manifest can't know
|
|
453
|
+
// the editor's origin/port. Resolve a root-relative manifest value against
|
|
454
|
+
// the editor page here; absolute URLs pass through `new URL()` unchanged.
|
|
455
|
+
const assetBaseUrl =
|
|
456
|
+
game.assetBaseUrl !== undefined
|
|
457
|
+
? new URL(game.assetBaseUrl, window.location.href).href
|
|
458
|
+
: undefined;
|
|
459
|
+
return mountIngestGameIframeReachableBundle(store, container, {
|
|
460
|
+
...opts,
|
|
461
|
+
bundleUrl: game.bundleUrl,
|
|
462
|
+
...(game.baseHref !== undefined ? { baseHref: game.baseHref } : {}),
|
|
463
|
+
...(assetBaseUrl !== undefined ? { assetBaseUrl } : {}),
|
|
464
|
+
...(game.bodyHtml !== undefined ? { bodyHtml: game.bodyHtml } : {}),
|
|
465
|
+
extraDeps: resolvedExtraDeps.namespaces,
|
|
466
|
+
...(Object.keys(resolvedExtraDeps.urls).length > 0
|
|
467
|
+
? { extraDepUrls: resolvedExtraDeps.urls }
|
|
468
|
+
: {}),
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Enter ingest mode for an already-resolved {@link IngestGame} descriptor,
|
|
474
|
+
* however it was built. Resolves once the game's live scene is captured, the
|
|
475
|
+
* authoring adapter is active, and it is shown in the editor for editing.
|
|
476
|
+
*
|
|
477
|
+
* T3.3 slice 3 (docs/CLI-ON-FOLDER-DESIGN.md §1E) split this out of
|
|
478
|
+
* `enterIngestMode` so the descriptor's ORIGIN is a separate concern from
|
|
479
|
+
* mounting it: `enterIngestMode(store, gameId)` below resolves `gameId`
|
|
480
|
+
* against the in-tree fixture glob (`ingest/discovery.ts`, the `?ingest=`
|
|
481
|
+
* URL route's discovery frontend); `enterIngestModeFromManifestWorld` builds
|
|
482
|
+
* the descriptor straight from an arbitrary project's manifest instead (the
|
|
483
|
+
* CLI-on-folder route, for a game folder never registered in source) — both
|
|
484
|
+
* end up here. `overlayId` namespaces the saved-edit overlay
|
|
485
|
+
* (`.vgai/overlays/<overlayId>.json`) and the `__vgaiIngest` dev-hook's
|
|
486
|
+
* reported `gameId`; it is always the world/game id (`game.id`), passed
|
|
487
|
+
* explicitly rather than re-read off `game` so the two callers' "id" concept
|
|
488
|
+
* — a fixture's registry-era id vs. a manifest's world id — stays one
|
|
489
|
+
* parameter, not two silently-assumed-equal fields.
|
|
490
|
+
*
|
|
491
|
+
* `declaredTier` (T3.4 slice 1, docs/ADAPTER-AUTHORING-DESIGN.md §1.C
|
|
492
|
+
* retrofit) is the world's declared `capabilities.local` tier, when known —
|
|
493
|
+
* only `enterIngestModeFromManifestWorld` has a `ResolvedWorldEntry` to read
|
|
494
|
+
* it from; the in-tree fixture route (`enterIngestMode`) has no manifest at
|
|
495
|
+
* all, so it stays `undefined` there (anti-shim: never fabricate a declared
|
|
496
|
+
* tier that was never actually declared). Both embed-only degradation points
|
|
497
|
+
* below report a shortfall through the SAME structured log
|
|
498
|
+
* (`reportTierShortfall`, `./achieved-tier.ts`) the module-adapter
|
|
499
|
+
* achieved-tier check uses, ONLY when a declared tier is actually known.
|
|
500
|
+
*
|
|
501
|
+
* `gameVersion` (T7.7, D9 — docs/DECISIONS-PENDING.md §D9) is the game's own
|
|
502
|
+
* manifest `version`, when known at this call site — it gets stamped onto the
|
|
503
|
+
* overlay envelope's `authoredAgainst.gameVersion` on save, and compared
|
|
504
|
+
* against a PREVIOUSLY-saved overlay's stamp to report a mismatch
|
|
505
|
+
* (`./authoring/overlay-report.ts`). Falls back to `game.gameVersion` (set by
|
|
506
|
+
* `ingest/discovery.ts` for the in-tree fixture route) when omitted, since
|
|
507
|
+
* `enterIngestModeFromManifestWorld` below is the only caller that has a
|
|
508
|
+
* SEPARATE source for it (the resolved manifest, which `resolveIngestDescriptor`
|
|
509
|
+
* has no access to — see `IngestGame.gameVersion`'s doc comment). Neither
|
|
510
|
+
* source existing is legal — it stamps `null` rather than fabricating a
|
|
511
|
+
* version (anti-shim).
|
|
512
|
+
*
|
|
513
|
+
* `upstreamPin` (R5, docs/MASTER-ARCHITECTURE-REVIEW.md §4(c)/§7.1 item 9) is
|
|
514
|
+
* this session's discovered `UPSTREAM.md` pin, when the caller found one
|
|
515
|
+
* (`tryManifestIngestRoute` below, via `readVendoredUpstreamPin`) — `null`
|
|
516
|
+
* when it looked and found nothing (no fallback source: unlike `gameVersion`,
|
|
517
|
+
* `IngestGame` carries no pin field, since no discovery route can populate one
|
|
518
|
+
* for the in-tree fixture glob — those fixtures are first-party, never
|
|
519
|
+
* vendored). It serves BOTH ends of the same check: at APPLY time it's
|
|
520
|
+
* compared against the loaded overlay's `authoredAgainst.upstreamPin`
|
|
521
|
+
* (`./authoring/overlay-report.ts`'s `upstreamPin.mismatch`); at SAVE time
|
|
522
|
+
* it's the value threaded into `saveOverlay`'s stamp, so a save made THIS
|
|
523
|
+
* session records what it was actually authored against.
|
|
524
|
+
*/
|
|
525
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one cohesive mount sequence (embed-only vs. captured-scene dispatch, capture-failure degrade, tier-shortfall reporting, gameVersion/upstreamPin resolution) — R5's one extra `??` tipped this over the threshold; splitting it would fragment the documented ordering contract rather than reduce real complexity (same precedent as game.ts's runFrame/achieved-tier fan-outs).
|
|
526
|
+
export async function enterIngestModeWithGame(
|
|
527
|
+
store: EditorStore,
|
|
528
|
+
game: IngestGame,
|
|
529
|
+
overlayId: string,
|
|
530
|
+
declaredTier?: Tier,
|
|
531
|
+
gameVersion?: string,
|
|
532
|
+
upstreamPin?: string | null,
|
|
533
|
+
): Promise<void> {
|
|
534
|
+
// D-V1: tear down WHATEVER kind is currently active, not just a prior
|
|
535
|
+
// threejs session — closes the cross-kind leak (F26 residual).
|
|
536
|
+
exitActiveIngest();
|
|
537
|
+
const effectiveGameVersion = gameVersion ?? game.gameVersion ?? null;
|
|
538
|
+
const effectiveUpstreamPin = upstreamPin ?? null;
|
|
539
|
+
|
|
540
|
+
const gameContainer = getGameContainer();
|
|
541
|
+
if (!gameContainer) {
|
|
542
|
+
editorConsole.error('Game container not mounted — cannot ingest', 'ingest');
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Ingest edits are runtime-only. Autosave/save-guard no longer suppress on a
|
|
547
|
+
// flag — the active adapter installed below (setActiveAuthoring) carries its
|
|
548
|
+
// own persistence provider (overlay destination, or absent for embed-only),
|
|
549
|
+
// which is what EditorStore's autosave/saveNow now consult (design
|
|
550
|
+
// docs/PERSISTENCE-PROVIDER-DESIGN.md §2). Still switch into the guarded
|
|
551
|
+
// "playing" state so the viewport swaps onto the live scene.
|
|
552
|
+
store.setPlayState('playing');
|
|
553
|
+
editorConsole.log(`Ingesting unmodified game: ${game.name}`, 'ingest');
|
|
554
|
+
|
|
555
|
+
// R2c (docs/MASTER-ARCHITECTURE-REVIEW.md §7.1 item 10): drive the SAME
|
|
556
|
+
// input gate play-mode uses (play-mode.ts's `gameInputActive`, wired via
|
|
557
|
+
// `setGameInputGate` at ~691) so a same-realm ingested game's raw
|
|
558
|
+
// window/document input listeners (gated via gated-globals.ts's proxy —
|
|
559
|
+
// reachable here because `server/dev.ts`'s prelude now shadows the in-tree
|
|
560
|
+
// ingest fixture roots too, see game-globals-shadow.ts) only fire while the
|
|
561
|
+
// Game tab is the active viewport surface, exactly like a first-party play
|
|
562
|
+
// session. Harmless for the iframe-reachable/opaque-embed tiers below too
|
|
563
|
+
// (a separate browsing context never reads this gate at all) — set once
|
|
564
|
+
// here, unconditionally, right alongside every `_active` assignment below,
|
|
565
|
+
// rather than duplicating it per branch.
|
|
566
|
+
const ingestInputActive = () =>
|
|
567
|
+
store.playState === 'playing' && store.activeViewportTab === 'game';
|
|
568
|
+
|
|
569
|
+
// Opaque prebuilt bundle → iframe embed-only floor (no scene introspection).
|
|
570
|
+
// `embedHtml` is either an already-read string (the in-tree fixture glob
|
|
571
|
+
// route) or a D-W2 lazy thunk (a manifest-driven external folder reading
|
|
572
|
+
// its `entryHtml` file's bytes through the project-static route) —
|
|
573
|
+
// resolve either shape to a plain string before mounting.
|
|
574
|
+
if (game.embedHtml) {
|
|
575
|
+
let embedHtmlText: string;
|
|
576
|
+
try {
|
|
577
|
+
embedHtmlText = typeof game.embedHtml === 'string' ? game.embedHtml : await game.embedHtml();
|
|
578
|
+
} catch (err) {
|
|
579
|
+
editorConsole.error(
|
|
580
|
+
`Failed to load opaque-embed entryHtml for "${game.name}": ${err}`,
|
|
581
|
+
'ingest',
|
|
582
|
+
);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
const embed = mountIngestGameEmbed(embedHtmlText, gameContainer);
|
|
586
|
+
_active = { kind: 'threejs', session: { store, embed }, siblings: [] };
|
|
587
|
+
setGameInputGate(ingestInputActive);
|
|
588
|
+
setActiveAuthoring(makeEmbedOnlyAdapter(store, game.name));
|
|
589
|
+
store.setActiveViewportTab('game'); // show the hosted iframe
|
|
590
|
+
store.notifyIngestEdit();
|
|
591
|
+
setMountFailureReport(null); // D-W3: a (degraded-but-hosted) mount succeeded
|
|
592
|
+
editorConsole.log(`Embedded opaque bundle "${game.name}" (embed-only)`, 'ingest');
|
|
593
|
+
const shortfall = reportEmbedOnlyShortfall(overlayId, declaredTier);
|
|
594
|
+
(window as unknown as Record<string, unknown>)['__vgaiIngest'] = {
|
|
595
|
+
gameId: overlayId,
|
|
596
|
+
embedOnly: true,
|
|
597
|
+
reflected: { count: 0, meshes: 0, lights: 0 },
|
|
598
|
+
...(shortfall ? { tierShortfall: shortfall } : {}),
|
|
599
|
+
};
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
try {
|
|
604
|
+
// Load any saved overlay envelope (previous-session edits + save-time
|
|
605
|
+
// provenance — D9) to reapply on mount. `overlay.overrides` is the flat
|
|
606
|
+
// `IngestOverlay` the authoring adapter operates on; `overlay.authoredAgainst`
|
|
607
|
+
// is what the OverlayApplyReport below compares against `effectiveGameVersion`.
|
|
608
|
+
const overlay = await loadOverlay(overlayId);
|
|
609
|
+
const mountOpts: MountIngestOptions = {
|
|
610
|
+
overlay: overlay.overrides,
|
|
611
|
+
persist: (o) => saveOverlay(overlayId, o, effectiveGameVersion, effectiveUpstreamPin),
|
|
612
|
+
destination: overlayPath(overlayId),
|
|
613
|
+
captureTimeoutMs: game.captureTimeoutMs,
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
// Mount the unmodified game through the GameAdapter path → captured live scene
|
|
617
|
+
// + an IngestAuthoringAdapter over its Object3D tree (no SceneEntity). The
|
|
618
|
+
// adapter reapplies the overlay after walking the scene. Reachable-three
|
|
619
|
+
// prebuilt bundles (inline source OR a D-W1 externalized `bundleUrl`) run
|
|
620
|
+
// in an iframe but share the host three → same contract.
|
|
621
|
+
const mount = game.iframeReachableSrc
|
|
622
|
+
? await mountIngestGameIframeReachable(
|
|
623
|
+
store,
|
|
624
|
+
game.iframeReachableSrc,
|
|
625
|
+
gameContainer,
|
|
626
|
+
mountOpts,
|
|
627
|
+
)
|
|
628
|
+
: game.bundleUrl
|
|
629
|
+
? await mountThreeIframeReachableBundle(game, gameContainer, store, mountOpts)
|
|
630
|
+
: await mountIngestGame(store, game, gameContainer, mountOpts);
|
|
631
|
+
const session: IngestSession = { store, mount };
|
|
632
|
+
_active = { kind: 'threejs', session, siblings: [] };
|
|
633
|
+
setGameInputGate(ingestInputActive);
|
|
634
|
+
|
|
635
|
+
// Capture failed → the game's three is unreachable (bundled / MISMATCHED version).
|
|
636
|
+
// Degrade gracefully to embed-only (host it, no scene introspection) — never silent.
|
|
637
|
+
if (mount.embedOnly || !mount.authoring || !mount.scene) {
|
|
638
|
+
setActiveAuthoring(
|
|
639
|
+
makeEmbedOnlyAdapter(store, `${game.name} — capture failed (three unreachable/mismatched)`),
|
|
640
|
+
);
|
|
641
|
+
store.setActiveViewportTab('game');
|
|
642
|
+
store.notifyIngestEdit();
|
|
643
|
+
setMountFailureReport(null); // D-W3: a (degraded-but-hosted) mount succeeded
|
|
644
|
+
editorConsole.log(
|
|
645
|
+
`Capture failed for "${game.name}": three unreachable/mismatched — hosting embed-only`,
|
|
646
|
+
'ingest',
|
|
647
|
+
);
|
|
648
|
+
const shortfall = reportEmbedOnlyShortfall(overlayId, declaredTier);
|
|
649
|
+
(window as unknown as Record<string, unknown>)['__vgaiIngest'] = {
|
|
650
|
+
gameId: overlayId,
|
|
651
|
+
embedOnly: true,
|
|
652
|
+
captureFailed: true,
|
|
653
|
+
reflected: { count: 0, meshes: 0, lights: 0 },
|
|
654
|
+
...(shortfall ? { tierShortfall: shortfall } : {}),
|
|
655
|
+
};
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Host lifecycle: keep the game's renderer sized to its container (resize).
|
|
660
|
+
const ro = new ResizeObserver(() => {
|
|
661
|
+
const r = gameContainer.getBoundingClientRect();
|
|
662
|
+
if (r.width > 0 && r.height > 0) mount.game.resize?.(r.width, r.height);
|
|
663
|
+
});
|
|
664
|
+
ro.observe(gameContainer);
|
|
665
|
+
session.resizeObserver = ro;
|
|
666
|
+
|
|
667
|
+
const stats = mount.authoring.refresh();
|
|
668
|
+
const overlayCount = Object.keys(overlay.overrides).length;
|
|
669
|
+
editorConsole.log(
|
|
670
|
+
`Captured live scene (${mount.scene.children.length} top-level children, ` +
|
|
671
|
+
`${mount.capture.getDrawCount()} draws); ingested ${stats.count} objects ` +
|
|
672
|
+
`(${stats.meshes} meshes, ${stats.lights} lights)` +
|
|
673
|
+
(overlayCount ? `; reapplied ${overlayCount} saved edit(s)` : ''),
|
|
674
|
+
'ingest',
|
|
675
|
+
);
|
|
676
|
+
|
|
677
|
+
// D9 (docs/DECISIONS-PENDING.md §D9): report a version mismatch and/or
|
|
678
|
+
// orphaned overlay ids — never blocks mounting, everything applicable was
|
|
679
|
+
// already applied by the adapter's constructor-time `applyOverlay`
|
|
680
|
+
// (`mount.authoring.lastApplyResult` is that call's result). R5 adds the
|
|
681
|
+
// upstream-pin check (§4(c)) — `overlay.authoredAgainst.upstreamPin` is
|
|
682
|
+
// `undefined` for a legacy overlay (never recorded — the report's
|
|
683
|
+
// `upstreamPin.recorded` reflects that honestly) and `effectiveUpstreamPin`
|
|
684
|
+
// is this session's discovered pin (`null` if undiscoverable here).
|
|
685
|
+
const overlayReport = buildOverlayApplyReport({
|
|
686
|
+
gameId: overlayId,
|
|
687
|
+
authoredAgainst: overlay.authoredAgainst.gameVersion,
|
|
688
|
+
currentVersion: effectiveGameVersion,
|
|
689
|
+
applied: mount.authoring.lastApplyResult.applied,
|
|
690
|
+
orphanedIds: mount.authoring.lastApplyResult.orphanedIds,
|
|
691
|
+
authoredUpstreamPin: overlay.authoredAgainst.upstreamPin,
|
|
692
|
+
currentUpstreamPin: effectiveUpstreamPin,
|
|
693
|
+
});
|
|
694
|
+
logOverlayApplyReport(overlayReport);
|
|
695
|
+
setActiveOverlayReport(overlayReport);
|
|
696
|
+
|
|
697
|
+
// D-A3 (docs/WAVE6-DOCTOR-DESIGN.md): the achieved tier for a healthy,
|
|
698
|
+
// fully-captured ingest mount — 'shared'/'deduped' rank identically
|
|
699
|
+
// (TIER_RANK, achieved-tier.ts), so this collapses to 'shared' for
|
|
700
|
+
// either load()-based route; the iframe-reachable-bundle route
|
|
701
|
+
// (`game.iframeReachableSrc`/`game.bundleUrl`) reports its own rung.
|
|
702
|
+
// Ingest tier was previously console-only (`reportEmbedOnlyShortfall`
|
|
703
|
+
// covers only the embed-only floor) — doctor's headless mount phase
|
|
704
|
+
// needs it on the healthy path too, since it has no console to read.
|
|
705
|
+
const achievedTier: Tier =
|
|
706
|
+
game.iframeReachableSrc || game.bundleUrl ? 'iframe-reachable' : 'shared';
|
|
707
|
+
|
|
708
|
+
// The editor's hierarchy + inspector now render from THIS adapter.
|
|
709
|
+
setActiveAuthoring(mount.authoring);
|
|
710
|
+
// An unmodified game exposes NO engine System adapters (it owns its own
|
|
711
|
+
// physics/input/animation/etc.) → those are correctly N/A while ingested. Only
|
|
712
|
+
// the asset-path remap (done at mount) is a first-party asset-adapter behavior.
|
|
713
|
+
setActiveSystems(mount.game.systems ?? {});
|
|
714
|
+
// Bind the viewport/objectMap/gizmo to the live scene (by the tagged entityId).
|
|
715
|
+
store.enterPlayScene(mount.scene);
|
|
716
|
+
store.setActiveViewportTab('scene');
|
|
717
|
+
store.notifyIngestEdit(); // ensure panels re-render against the override
|
|
718
|
+
setMountFailureReport(null); // D-W3: mount succeeded
|
|
719
|
+
|
|
720
|
+
// Dev/proof hook: expose ingest state for automation + screenshots.
|
|
721
|
+
(window as unknown as Record<string, unknown>)['__vgaiIngest'] = {
|
|
722
|
+
gameId: overlayId,
|
|
723
|
+
drawCount: () => mount.capture.getDrawCount(),
|
|
724
|
+
reflected: stats,
|
|
725
|
+
sceneChildren: mount.scene.children.length,
|
|
726
|
+
// D-A3: achieved tier for a headless-verifiable healthy mount (see comment above).
|
|
727
|
+
achievedTier,
|
|
728
|
+
// Host-lifecycle resize: drive the game renderer's size (proof/automation).
|
|
729
|
+
resize: (w: number, h: number) => mount.game.resize?.(w, h),
|
|
730
|
+
// Loop-host: freeze/resume the game's own rAF (proof/automation).
|
|
731
|
+
setPaused: (p: boolean) => mount.game.setPaused?.(p),
|
|
732
|
+
// Recorded N/A: which engine System adapters are active for this game (none
|
|
733
|
+
// for an unmodified game — it owns its own subsystems).
|
|
734
|
+
engineSystems: () => Object.keys(getActiveSystems()),
|
|
735
|
+
// D9 (T7.7): the version-mismatch/orphan findings for this mount, for
|
|
736
|
+
// automation/proof — mirrors the embed-only branches' `tierShortfall`.
|
|
737
|
+
overlayReport,
|
|
738
|
+
// DEV-only: the live authoring adapter + store, so e2e/proof can drive the
|
|
739
|
+
// SAME providers the hierarchy/inspector/gizmo use.
|
|
740
|
+
...(import.meta.env.DEV ? { adapter: mount.authoring, store } : {}),
|
|
741
|
+
};
|
|
742
|
+
} catch (err) {
|
|
743
|
+
editorConsole.error(`Ingest failed: ${err}`, 'ingest');
|
|
744
|
+
exitIngestMode();
|
|
745
|
+
throw err;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Enter ingest mode for an in-tree fixture game by its discovered id (the
|
|
751
|
+
* `?ingest=<id>` URL route's entry point — `ingest/discovery.ts`'s glob
|
|
752
|
+
* discovery replaces the deleted `INGEST_GAMES` registry lookup here).
|
|
753
|
+
*/
|
|
754
|
+
export async function enterIngestMode(store: EditorStore, gameId: string): Promise<void> {
|
|
755
|
+
const game = getIngestGame(gameId);
|
|
756
|
+
if (!game) {
|
|
757
|
+
editorConsole.error(`Unknown ingest game: ${gameId}`, 'ingest');
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
return enterIngestModeWithGame(store, game, gameId);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Enter ingest mode straight from a manifest's `ingest-three` world — the
|
|
765
|
+
* CLI-on-folder route (T3.3 slice 3, docs/CLI-ON-FOLDER-DESIGN.md §1E): a
|
|
766
|
+
* game folder never registered in source, whose `vgai.game.json` declares an
|
|
767
|
+
* ingest-three world. Builds the descriptor via `resolveIngestDescriptor`
|
|
768
|
+
* (adapter-resolver.ts — parameterized by the manifest's ingest fields, no
|
|
769
|
+
* registry lookup) and mounts it exactly like any other {@link IngestGame}.
|
|
770
|
+
* The overlay namespaces off the world id, consistent with today's
|
|
771
|
+
* `.vgai/overlays/<id>.json` convention (the `?ingest=<id>` route's `id` IS a
|
|
772
|
+
* world id too, for the in-tree fixtures — same namespace, different origin).
|
|
773
|
+
*
|
|
774
|
+
* `gameVersion` (T7.7/D9): `resolveIngestDescriptor` only has a
|
|
775
|
+
* `ResolvedWorldEntry`, not the manifest's top-level `version` — so the caller
|
|
776
|
+
* (`tryManifestIngestRoute` below, which fetched the full `ResolvedGameManifest`)
|
|
777
|
+
* passes it here explicitly, threaded straight through to `enterIngestModeWithGame`.
|
|
778
|
+
*
|
|
779
|
+
* `upstreamPin` (R5, docs/MASTER-ARCHITECTURE-REVIEW.md §4(c)): same shape —
|
|
780
|
+
* `resolveIngestDescriptor` has no way to discover an `UPSTREAM.md` pin
|
|
781
|
+
* either, so `tryManifestIngestRoute` (which has a dev server + project root
|
|
782
|
+
* to fetch one from, via `readVendoredUpstreamPin`) passes it here explicitly.
|
|
783
|
+
*/
|
|
784
|
+
export async function enterIngestModeFromManifestWorld(
|
|
785
|
+
store: EditorStore,
|
|
786
|
+
world: ResolvedWorldEntry,
|
|
787
|
+
projectRoot: string,
|
|
788
|
+
gameVersion?: string,
|
|
789
|
+
upstreamPin?: string | null,
|
|
790
|
+
): Promise<void> {
|
|
791
|
+
const game = resolveIngestDescriptor(world, projectRoot);
|
|
792
|
+
return enterIngestModeWithGame(
|
|
793
|
+
store,
|
|
794
|
+
game,
|
|
795
|
+
world.id,
|
|
796
|
+
world.capabilities.local,
|
|
797
|
+
gameVersion,
|
|
798
|
+
upstreamPin,
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// ---------------------------------------------------------------------------
|
|
803
|
+
// PixiJS ingest sessions (T6.1 slice 4 — docs/MULTI-WORLD-DESIGN.md §1.E)
|
|
804
|
+
// ---------------------------------------------------------------------------
|
|
805
|
+
|
|
806
|
+
interface IngestSession2D {
|
|
807
|
+
store: EditorStore;
|
|
808
|
+
/** Absent for a D-W2 opaque-embed session — see `embed` below. */
|
|
809
|
+
adapter?: World2DAuthoringAdapter;
|
|
810
|
+
/** Opaque prebuilt-bundle embed path (D-W2, iframe-hosted, embed-only — mirrors the three side's `IngestSession.embed`). */
|
|
811
|
+
embed?: EmbedMount2D;
|
|
812
|
+
/** The mount's own honest pause (ticker stop/start) — the F26 play surface. No-op for an opaque-embed session (no reachable pixi instance). */
|
|
813
|
+
setPaused: (paused: boolean) => void;
|
|
814
|
+
dispose: () => void;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/** True if an unmodified PixiJS game is currently ingested. */
|
|
818
|
+
export function isIngestActive2D(): boolean {
|
|
819
|
+
return _active?.kind === 'pixi';
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* D-P4 (docs/PIXI-INGEST-LANDING-DESIGN.md §3) — mount-shape adapter:
|
|
824
|
+
* `IframeReachableMount2D` (`ingest-iframe-2d.ts`) has `{ iframe, capture,
|
|
825
|
+
* captured, dispose }`, not the `IngestMount2D` shape (`{ stage, capture,
|
|
826
|
+
* embedOnly, setPaused, dispose }`) the rest of ingest-mode.ts's PixiJS path
|
|
827
|
+
* expects. This wraps the former into the latter:
|
|
828
|
+
* - `stage`: the captured runtime's stage, or `null` if capture never
|
|
829
|
+
* yielded one (timed out / pixi unreachable) — the SAME "null stage"
|
|
830
|
+
* signal `mountIngestGame2D`'s degrade path already produces, so the
|
|
831
|
+
* caller's existing `!mount.stage` check covers both mounts uniformly.
|
|
832
|
+
* - `embedOnly`: always `false` — this route has no embed-only floor
|
|
833
|
+
* (capture either succeeded, or the mount call itself already failed
|
|
834
|
+
* loudly / `stage` is null); never fabricate a degraded-but-hosted state.
|
|
835
|
+
* - `setPaused`: a REAL capability, not a shim — the trap yields the live
|
|
836
|
+
* `Application` (`captured.app`), so pausing drives its OWN
|
|
837
|
+
* `ticker.stop()`/`start()`. If capture hasn't yielded an app yet (or
|
|
838
|
+
* failed), this THROWS loudly rather than silently no-oping — the honest-
|
|
839
|
+
* pause design call.
|
|
840
|
+
* - `dispose`: delegates straight to the iframe mount's own `dispose`.
|
|
841
|
+
*/
|
|
842
|
+
function wrapIframeReachableMount2D(mount: IframeReachableMount2D): IngestMount2D {
|
|
843
|
+
return {
|
|
844
|
+
stage: mount.captured?.stage ?? null,
|
|
845
|
+
capture: mount.capture,
|
|
846
|
+
embedOnly: false,
|
|
847
|
+
setPaused(paused: boolean): void {
|
|
848
|
+
const app = mount.captured?.app as
|
|
849
|
+
| { ticker?: { stop: () => void; start: () => void } }
|
|
850
|
+
| undefined;
|
|
851
|
+
if (!app?.ticker) {
|
|
852
|
+
throw new Error(
|
|
853
|
+
'ingest-mode: setPaused() called on an iframe-reachable-multi mount before capture ' +
|
|
854
|
+
'yielded a live Application (or capture failed) — this never silently no-ops (D-P4, ' +
|
|
855
|
+
'docs/PIXI-INGEST-LANDING-DESIGN.md §3).',
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
if (paused) app.ticker.stop();
|
|
859
|
+
else app.ticker.start();
|
|
860
|
+
},
|
|
861
|
+
dispose: () => mount.dispose(),
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Mount an `iframe-reachable` `IngestGame2D` descriptor (Track P,
|
|
867
|
+
* docs/PIXI-INGEST-LANDING-DESIGN.md §2/P2) via
|
|
868
|
+
* `mountIngestGame2DIframeReachableMulti` — the externalized-built-bundle
|
|
869
|
+
* sibling of {@link mountIngestGame2D}'s shared-instance path. `extraDeps`/
|
|
870
|
+
* `pixiNamespace` are LAZY thunks on the descriptor (`resolveIngest2DDescriptor`
|
|
871
|
+
* / `discovery2d.ts`) — invoked here, right before the actual mount, so the
|
|
872
|
+
* host-namespace registry's dynamic imports (`@pixi/sound`/`gsap`/spine/a
|
|
873
|
+
* standalone pixi build) only ever run for a game actually being ingested.
|
|
874
|
+
*/
|
|
875
|
+
async function mountIframeReachableGame2D(
|
|
876
|
+
game: IngestGame2D,
|
|
877
|
+
container: HTMLElement,
|
|
878
|
+
): Promise<IngestMount2D> {
|
|
879
|
+
if (!game.bundleUrl) {
|
|
880
|
+
throw new Error(
|
|
881
|
+
`enterIngestModeWithGame2D: world2d ingest game "${game.id}" has neither \`load\` nor ` +
|
|
882
|
+
'`bundleUrl` — resolveIngest2DDescriptor only builds shared/deduped (load) or ' +
|
|
883
|
+
'iframe-reachable (bundleUrl) descriptors this slice.',
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
const pixiNamespace = game.pixiNamespace ? await game.pixiNamespace() : PIXI;
|
|
887
|
+
// D-Z3: two kinds — registry-known specifiers resolve to host namespaces
|
|
888
|
+
// (`.namespaces`, unchanged shape/behavior); everything else resolves via
|
|
889
|
+
// the project-node_modules fallback into verbatim importmap URLs (`.urls`).
|
|
890
|
+
const resolvedExtraDeps = game.extraDeps ? await game.extraDeps() : { namespaces: {}, urls: {} };
|
|
891
|
+
// The Assets.init basePath remap needs an ABSOLUTE URL (Pixi's Assets resolver
|
|
892
|
+
// builds URLs from window.location — see IframeReachableMultiOpts.assetBaseUrl),
|
|
893
|
+
// but a static manifest can't know the editor's origin/port. Resolve a
|
|
894
|
+
// root-relative manifest value against the editor page here; absolute URLs
|
|
895
|
+
// pass through new URL() unchanged.
|
|
896
|
+
const assetBaseUrl =
|
|
897
|
+
game.assetBaseUrl !== undefined
|
|
898
|
+
? new URL(game.assetBaseUrl, window.location.href).href
|
|
899
|
+
: undefined;
|
|
900
|
+
const mount = await mountIngestGame2DIframeReachableMulti(pixiNamespace, container, {
|
|
901
|
+
extraDeps: resolvedExtraDeps.namespaces,
|
|
902
|
+
...(Object.keys(resolvedExtraDeps.urls).length > 0
|
|
903
|
+
? { extraDepUrls: resolvedExtraDeps.urls }
|
|
904
|
+
: {}),
|
|
905
|
+
bundleUrl: game.bundleUrl,
|
|
906
|
+
...(game.baseHref !== undefined ? { baseHref: game.baseHref } : {}),
|
|
907
|
+
...(assetBaseUrl !== undefined ? { assetBaseUrl } : {}),
|
|
908
|
+
...(game.bodyHtml !== undefined ? { bodyHtml: game.bodyHtml } : {}),
|
|
909
|
+
...(game.captureTimeoutMs !== undefined ? { captureTimeoutMs: game.captureTimeoutMs } : {}),
|
|
910
|
+
});
|
|
911
|
+
return wrapIframeReachableMount2D(mount);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Enter ingest mode for an already-resolved {@link IngestGame2D} descriptor —
|
|
916
|
+
* the pixi sibling of {@link enterIngestModeWithGame}. `overlayId` is always
|
|
917
|
+
* the world id, namespacing the saved overlay at the §1.G unified path
|
|
918
|
+
* (`.vgai/overlays/<worldId>.json`, `authoring/world-overlay.ts` — the SAME
|
|
919
|
+
* module `play-mode.ts`'s multi-world path already uses, NOT the retired
|
|
920
|
+
* `2d-`-prefixed `world2d-overlay.ts`).
|
|
921
|
+
*/
|
|
922
|
+
/**
|
|
923
|
+
* D-W2 (docs/WAVE4-FTUE-HARDENING-DESIGN.md): opaque prebuilt bundle → iframe
|
|
924
|
+
* embed-only floor (no scene introspection) — the pixi sibling of
|
|
925
|
+
* `enterIngestModeWithGame`'s identical `game.embedHtml` branch. Extracted
|
|
926
|
+
* out of {@link enterIngestModeWithGame2D} to keep that function's cognitive
|
|
927
|
+
* complexity under biome's `noExcessiveCognitiveComplexity` ceiling (this
|
|
928
|
+
* branch's own try/catch + session/hook wiring pushed it over on its own).
|
|
929
|
+
* Always a lazy thunk here (see `IngestGame2D.embedHtml`'s doc comment).
|
|
930
|
+
*/
|
|
931
|
+
async function enterIngestModeWithGame2DEmbed(
|
|
932
|
+
store: EditorStore,
|
|
933
|
+
game: IngestGame2D,
|
|
934
|
+
overlayId: string,
|
|
935
|
+
gameContainer: HTMLElement,
|
|
936
|
+
embedHtml: () => Promise<string>,
|
|
937
|
+
): Promise<void> {
|
|
938
|
+
let embedHtmlText: string;
|
|
939
|
+
try {
|
|
940
|
+
embedHtmlText = await embedHtml();
|
|
941
|
+
} catch (err) {
|
|
942
|
+
editorConsole.error(
|
|
943
|
+
`Failed to load opaque-embed entryHtml for "${game.name}" (world2d): ${err}`,
|
|
944
|
+
'ingest',
|
|
945
|
+
);
|
|
946
|
+
store.setPlayState('stopped');
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
const embed: EmbedMount2D = mountIngestGame2DEmbed(embedHtmlText, gameContainer);
|
|
950
|
+
_active = {
|
|
951
|
+
kind: 'pixi',
|
|
952
|
+
session: {
|
|
953
|
+
store,
|
|
954
|
+
embed,
|
|
955
|
+
setPaused: () => {},
|
|
956
|
+
dispose: () => embed.dispose(),
|
|
957
|
+
},
|
|
958
|
+
siblings: [],
|
|
959
|
+
};
|
|
960
|
+
setGameInputGate(() => store.playState === 'playing' && store.activeViewportTab === 'game');
|
|
961
|
+
setActiveAuthoring(makeEmbedOnlyAdapter(store, game.name));
|
|
962
|
+
setActiveSystems({});
|
|
963
|
+
store.setActiveViewportTab('game');
|
|
964
|
+
store.notifyIngestEdit();
|
|
965
|
+
editorConsole.log(`Embedded opaque pixi bundle "${game.name}" (embed-only, world2d)`, 'ingest');
|
|
966
|
+
(window as unknown as Record<string, unknown>)['__vgaiIngest2D'] = {
|
|
967
|
+
gameId: overlayId,
|
|
968
|
+
embedOnly: true,
|
|
969
|
+
reflected: { count: 0, sprites: 0 },
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
export async function enterIngestModeWithGame2D(
|
|
974
|
+
store: EditorStore,
|
|
975
|
+
game: IngestGame2D,
|
|
976
|
+
overlayId: string,
|
|
977
|
+
): Promise<void> {
|
|
978
|
+
// D-V1: tear down WHATEVER kind is currently active, not just a prior
|
|
979
|
+
// pixi session — closes the cross-kind leak (F26 residual).
|
|
980
|
+
exitActiveIngest();
|
|
981
|
+
|
|
982
|
+
const gameContainer = getGameContainer();
|
|
983
|
+
if (!gameContainer) {
|
|
984
|
+
editorConsole.error('Game container not mounted — cannot ingest (world2d)', 'ingest');
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
store.setPlayState('playing');
|
|
989
|
+
editorConsole.log(`Ingesting unmodified PixiJS game: ${game.name}`, 'ingest');
|
|
990
|
+
|
|
991
|
+
if (game.embedHtml) {
|
|
992
|
+
return enterIngestModeWithGame2DEmbed(store, game, overlayId, gameContainer, game.embedHtml);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
if (!game.load && !game.bundleUrl) {
|
|
996
|
+
throw new Error(
|
|
997
|
+
`enterIngestModeWithGame2D: world2d ingest game "${game.id}" has neither \`load\` nor ` +
|
|
998
|
+
'`bundleUrl` — only shared/deduped (load) or iframe-reachable (bundleUrl) ingest-pixi ' +
|
|
999
|
+
'worlds are mountable this slice (resolveIngest2DDescriptor).',
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Track P (docs/PIXI-INGEST-LANDING-DESIGN.md §2/D-P4): the iframe-
|
|
1004
|
+
// reachable-multi route mounts its OWN iframe straight into `gameContainer`
|
|
1005
|
+
// (`mountIngestGame2DIframeReachableMulti`) — it needs no placeholder
|
|
1006
|
+
// `<canvas>`, unlike the shared-instance path below (whose game creates its
|
|
1007
|
+
// own PixiJS `Application` and needs a container element reserved for it).
|
|
1008
|
+
const canvas = game.load ? document.createElement('canvas') : undefined;
|
|
1009
|
+
if (canvas) gameContainer.appendChild(canvas);
|
|
1010
|
+
|
|
1011
|
+
// F26: same cold-mount announcement the threejs adapter makes
|
|
1012
|
+
// (ingest-game-adapter.ts) — a shared-realm pixi game may declare the
|
|
1013
|
+
// game contract and defer its session until ▶; games that ignore the flag
|
|
1014
|
+
// (including iframe-realm bundles, which can't see it) behave as before.
|
|
1015
|
+
(window as unknown as { __vgaiMountCold?: boolean }).__vgaiMountCold = true;
|
|
1016
|
+
const overlay = await loadWorldOverlay<Overlay2D>(overlayId);
|
|
1017
|
+
const mount = game.load
|
|
1018
|
+
? await mountIngestGame2D(
|
|
1019
|
+
PIXI,
|
|
1020
|
+
game,
|
|
1021
|
+
game.captureTimeoutMs !== undefined ? { captureTimeoutMs: game.captureTimeoutMs } : {},
|
|
1022
|
+
)
|
|
1023
|
+
: await mountIframeReachableGame2D(game, gameContainer);
|
|
1024
|
+
|
|
1025
|
+
if (mount.embedOnly || !mount.stage) {
|
|
1026
|
+
canvas?.remove();
|
|
1027
|
+
mount.dispose();
|
|
1028
|
+
editorConsole.error(
|
|
1029
|
+
`world2d ingest capture failed for "${game.name}" (pixi unreachable) — no embed-only ` +
|
|
1030
|
+
'floor exists for ingest-pixi this slice (resolveIngest2DDescriptor builds no ' +
|
|
1031
|
+
'embedHtml games; iframe-reachable-multi capture just timed out/failed).',
|
|
1032
|
+
'ingest',
|
|
1033
|
+
);
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
const stage = mount.stage as Container;
|
|
1038
|
+
// The game's async setup may add display objects after its first captured frame —
|
|
1039
|
+
// wait for the stage to settle (mirrors world2d-mode.ts's prior behavior).
|
|
1040
|
+
for (let i = 0; i < 90 && (stage.children?.length ?? 0) === 0; i++) {
|
|
1041
|
+
await new Promise((r) => requestAnimationFrame(r));
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
const physicsAdapter = createPhysicsAdapter2D(createPhysics2DRegistry());
|
|
1045
|
+
const adapter = new World2DAuthoringAdapter(stage, physicsAdapter, store, {
|
|
1046
|
+
overlay,
|
|
1047
|
+
persist: (o) => saveWorldOverlay(overlayId, o),
|
|
1048
|
+
destination: worldOverlayPath(overlayId),
|
|
1049
|
+
});
|
|
1050
|
+
const stats = adapter.refresh();
|
|
1051
|
+
|
|
1052
|
+
// D-A3 (docs/WAVE6-DOCTOR-DESIGN.md): pixi sibling of the three-side
|
|
1053
|
+
// achieved-tier computation above — `game.load` is the shared/deduped
|
|
1054
|
+
// load() route, otherwise this reached here via the iframe-reachable-multi
|
|
1055
|
+
// bundleUrl route (`mountIframeReachableGame2D`).
|
|
1056
|
+
const achievedTier: Tier = game.load ? 'shared' : 'iframe-reachable';
|
|
1057
|
+
|
|
1058
|
+
_active = {
|
|
1059
|
+
kind: 'pixi',
|
|
1060
|
+
session: {
|
|
1061
|
+
store,
|
|
1062
|
+
adapter,
|
|
1063
|
+
setPaused: (p) => mount.setPaused(p),
|
|
1064
|
+
dispose: () => {
|
|
1065
|
+
mount.dispose();
|
|
1066
|
+
canvas?.remove();
|
|
1067
|
+
},
|
|
1068
|
+
},
|
|
1069
|
+
siblings: [],
|
|
1070
|
+
};
|
|
1071
|
+
|
|
1072
|
+
// R2c: same-realm PixiJS ingest fixtures (`ingest/games-2d/**`) are just as
|
|
1073
|
+
// unshadowed-by-default as the three.js ones — drive the same gate as
|
|
1074
|
+
// {@link enterIngestModeWithGame} (see that function's doc comment).
|
|
1075
|
+
setGameInputGate(() => store.playState === 'playing' && store.activeViewportTab === 'game');
|
|
1076
|
+
|
|
1077
|
+
setActiveAuthoring(adapter);
|
|
1078
|
+
setActiveSystems({});
|
|
1079
|
+
store.setActiveViewportTab('game');
|
|
1080
|
+
store.notifyIngestEdit();
|
|
1081
|
+
setMountFailureReport(null); // D-W3: mount succeeded
|
|
1082
|
+
|
|
1083
|
+
editorConsole.log(
|
|
1084
|
+
`world2d ingest: captured "${game.name}" (${stats.count} nodes, ${stats.sprites} sprites)`,
|
|
1085
|
+
'ingest',
|
|
1086
|
+
);
|
|
1087
|
+
|
|
1088
|
+
(window as unknown as Record<string, unknown>)['__vgaiIngest2D'] = {
|
|
1089
|
+
gameId: overlayId,
|
|
1090
|
+
drawCount: () => mount.capture.getDrawCount(),
|
|
1091
|
+
reflected: stats,
|
|
1092
|
+
setPaused: (p: boolean) => mount.setPaused(p),
|
|
1093
|
+
// D-A3: achieved tier for a headless-verifiable healthy mount (see comment above).
|
|
1094
|
+
achievedTier,
|
|
1095
|
+
adapter,
|
|
1096
|
+
store,
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* Enter ingest mode straight from a manifest's `ingest-pixi` world — the pixi
|
|
1102
|
+
* sibling of {@link enterIngestModeFromManifestWorld}. Builds the descriptor
|
|
1103
|
+
* via `resolveIngest2DDescriptor` (adapter-resolver.ts) and mounts it exactly
|
|
1104
|
+
* like any other {@link IngestGame2D}.
|
|
1105
|
+
*/
|
|
1106
|
+
export async function enterIngestModeFromManifestWorld2D(
|
|
1107
|
+
store: EditorStore,
|
|
1108
|
+
world: ResolvedWorldEntry,
|
|
1109
|
+
projectRoot: string,
|
|
1110
|
+
): Promise<void> {
|
|
1111
|
+
const game = resolveIngest2DDescriptor(world, projectRoot);
|
|
1112
|
+
return enterIngestModeWithGame2D(store, game, world.id);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
/**
|
|
1116
|
+
* Enter ingest mode for an in-tree world2d fixture game by its discovered id
|
|
1117
|
+
* (the `?ingest=<id>` URL route's pixi fallback — `ingest/discovery2d.ts`'s
|
|
1118
|
+
* glob discovery replaces the deleted `WORLD2D_INGEST_GAMES` registry lookup
|
|
1119
|
+
* here). Pixi sibling of {@link enterIngestMode}.
|
|
1120
|
+
*/
|
|
1121
|
+
export async function enterIngestMode2D(store: EditorStore, gameId: string): Promise<void> {
|
|
1122
|
+
const game = getIngestGame2D(gameId);
|
|
1123
|
+
if (!game) {
|
|
1124
|
+
editorConsole.error(`Unknown world2d ingest game: ${gameId}`, 'ingest');
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
return enterIngestModeWithGame2D(store, game, gameId);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
/**
|
|
1131
|
+
* D-V1: the pixi teardown body — byte-for-byte the pre-collapse
|
|
1132
|
+
* `exitIngestMode2D` logic below, extracted so `exitActiveIngest` can
|
|
1133
|
+
* dispatch to it for ANY kind switch. `_active`/`resetIngestPlaySurface` are
|
|
1134
|
+
* handled by the caller (`exitActiveIngest`) — this only does the
|
|
1135
|
+
* pixi-specific teardown.
|
|
1136
|
+
*/
|
|
1137
|
+
function disposePixiSession(s: IngestSession2D): void {
|
|
1138
|
+
// R2c: reset the input gate — see disposeThreejsSession's identical comment.
|
|
1139
|
+
setGameInputGate(() => true);
|
|
1140
|
+
// v4 §7.1-13: `World2DAuthoringAdapter` now self-registers with the undo
|
|
1141
|
+
// timeline (mirrors `IngestAuthoringAdapter` — see disposeThreejsSession's
|
|
1142
|
+
// identical call above), so this session's entries must be dropped the same
|
|
1143
|
+
// way on exit. Absent for a D-W2 opaque-embed session (no adapter, no undo entries).
|
|
1144
|
+
if (s.adapter) unregisterUndoAdapter(s.adapter);
|
|
1145
|
+
setActiveAuthoring(null);
|
|
1146
|
+
setActiveSystems(null);
|
|
1147
|
+
setMountFailureReport(null); // D-W3: no session -> no banner to show (mirrors setActiveOverlayReport(null) above)
|
|
1148
|
+
s.store.setPlayState('stopped');
|
|
1149
|
+
try {
|
|
1150
|
+
s.dispose();
|
|
1151
|
+
} catch {
|
|
1152
|
+
/* ignore */
|
|
1153
|
+
}
|
|
1154
|
+
delete (window as unknown as Record<string, unknown>)['__vgaiIngest2D'];
|
|
1155
|
+
editorConsole.log('world2d ingest mode stopped', 'ingest');
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/**
|
|
1159
|
+
* Exit PixiJS ingest mode: tear down the captured game + adapter. D-V1: a
|
|
1160
|
+
* thin wrapper that only acts when a pixi session is the one currently
|
|
1161
|
+
* active in `_active` — same observable behavior as before the collapse;
|
|
1162
|
+
* the actual teardown is `disposePixiSession` via `exitActiveIngest`.
|
|
1163
|
+
*/
|
|
1164
|
+
export function exitIngestMode2D(): void {
|
|
1165
|
+
if (_active?.kind !== 'pixi') return;
|
|
1166
|
+
exitActiveIngest();
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// ---------------------------------------------------------------------------
|
|
1170
|
+
// Native-React ingest sessions (Track N, N2 — docs/REACT-INGEST-LANDING-
|
|
1171
|
+
// DESIGN.md D-N2/D-N8)
|
|
1172
|
+
// ---------------------------------------------------------------------------
|
|
1173
|
+
|
|
1174
|
+
interface IngestSessionReact {
|
|
1175
|
+
store: EditorStore;
|
|
1176
|
+
/** The runtime Game's real play-state control — the F26 play surface. */
|
|
1177
|
+
setPaused: (paused: boolean) => void;
|
|
1178
|
+
stop: () => void;
|
|
1179
|
+
/** D-K4 (wave 10 Slice A): the live DOM-authoring adapter both routes now
|
|
1180
|
+
* install — self-registered with the global undo timeline at construction,
|
|
1181
|
+
* so teardown must unregister it (mirrors `IngestSession2D.adapter`). */
|
|
1182
|
+
adapter: ReactDomAuthoringAdapter;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/** True if an ingested native-React game is currently mounted. */
|
|
1186
|
+
export function isIngestActiveReact(): boolean {
|
|
1187
|
+
return _active?.kind === 'react';
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
/**
|
|
1191
|
+
* D-L1 (wave 10 Slice B, docs/WAVE10-REACT-INGEST-DOM-AUTHORING-DESIGN.md
|
|
1192
|
+
* §2) — the LIVE value behind `__vgaiIngestReact.domEvidence()`, shared by
|
|
1193
|
+
* both react ingest routes below. Evaluated at scrape time (a function
|
|
1194
|
+
* result, never memoized), same "read the live state fresh on every call"
|
|
1195
|
+
* discipline as `__vgaiSiblingMounts` (D-H1).
|
|
1196
|
+
*
|
|
1197
|
+
* `elementCount` prefers the DOM's real `querySelectorAll('*').length` — a
|
|
1198
|
+
* real browser `HTMLElement` (what `reactRoot()` actually returns in
|
|
1199
|
+
* production) always has it — and falls back to the adapter's own
|
|
1200
|
+
* structural-walk size (`nodeCount()`) only when the mounted root doesn't
|
|
1201
|
+
* expose `querySelectorAll` at all (this repo's own vitest-node adapter
|
|
1202
|
+
* fixtures have no real DOM). Never a fabricated number either way: both
|
|
1203
|
+
* paths count real, currently-present elements.
|
|
1204
|
+
*
|
|
1205
|
+
* `reactRootPresent` checks for a React 19 `createRoot` own-property mount
|
|
1206
|
+
* stamp (`__reactContainer$<hash>`) on `root` itself — the SAME element
|
|
1207
|
+
* `reactRoot()` returned, which is exactly the element `createRoot(...)`
|
|
1208
|
+
* rendered into (`create-runtime.ts`'s `ReactWorldHost.container` doc
|
|
1209
|
+
* comment: "the adapter's `mount` renders its react tree INTO this exact
|
|
1210
|
+
* element via `createRoot`").
|
|
1211
|
+
*/
|
|
1212
|
+
function reactDomEvidence(
|
|
1213
|
+
root: HTMLElement,
|
|
1214
|
+
adapter: ReactDomAuthoringAdapter,
|
|
1215
|
+
): { elementCount: number; hierarchyNodeCount: number; reactRootPresent: boolean } {
|
|
1216
|
+
const withQuery = root as unknown as { querySelectorAll?: (sel: string) => ArrayLike<unknown> };
|
|
1217
|
+
const elementCount =
|
|
1218
|
+
typeof withQuery.querySelectorAll === 'function'
|
|
1219
|
+
? withQuery.querySelectorAll('*').length
|
|
1220
|
+
: adapter.nodeCount();
|
|
1221
|
+
const reactRootPresent = Object.keys(root as object).some((k) =>
|
|
1222
|
+
k.startsWith('__reactContainer'),
|
|
1223
|
+
);
|
|
1224
|
+
return { elementCount, hierarchyNodeCount: adapter.nodeCount(), reactRootPresent };
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
/**
|
|
1228
|
+
* Enter ingest mode for a discovered native-React fixture game (the
|
|
1229
|
+
* `?ingest=<id>` URL route's react fallback, tried after the three/pixi
|
|
1230
|
+
* fixture globs — see `autoLaunchIngestFromURL` below). Mounts through the
|
|
1231
|
+
* EXACT SAME T6.2 react-world host stack `default-react`/multi-world react
|
|
1232
|
+
* worlds use — NOT the capture-based three/pixi bridge (there is no scene to
|
|
1233
|
+
* capture; the mounted tree IS React, D-N1).
|
|
1234
|
+
*
|
|
1235
|
+
* Reuses `resolveAllWorlds` + the WORLDS-PATH `createGameRuntime` (not the
|
|
1236
|
+
* legacy single-`{canvas,adapter}` path, which is three-only and has no
|
|
1237
|
+
* `ReactWorldHost`/`container` concept — see `create-runtime.ts`'s
|
|
1238
|
+
* `HostContext`) rather than hand-building a `ReactWorldHost`/`Game`: a
|
|
1239
|
+
* single-element `worlds` array gets EVERY host concern for free (the
|
|
1240
|
+
* absolutely-positioned DOM-root layer, a real `Game`, dispose via
|
|
1241
|
+
* `session.stop()`) with zero reinvention — `resolveAllWorlds`'s
|
|
1242
|
+
* `MULTI_WORLD_ALLOWED_IDENTITIES` gate only fires for `worlds.length > 1`
|
|
1243
|
+
* (see that function's doc comment), so a ONE-element manifest containing an
|
|
1244
|
+
* `ingest-react` world resolves cleanly through it despite that identity
|
|
1245
|
+
* being excluded from real multi-world manifests.
|
|
1246
|
+
*
|
|
1247
|
+
* D-N8 v2 / D-K4 (wave 10 Slice A, docs/WAVE10-REACT-INGEST-DOM-AUTHORING-
|
|
1248
|
+
* DESIGN.md §1): the active authoring override is a real
|
|
1249
|
+
* `ReactDomAuthoringAdapter` — a structural-path DOM walker over the mounted
|
|
1250
|
+
* react tree, persisting edits to an overlay (never to source, see that
|
|
1251
|
+
* file's doc comment). The published `achievedTier` is COMPUTED from this
|
|
1252
|
+
* real adapter via `computeAchievedTier` (the same honest authority the
|
|
1253
|
+
* embed-only floor used to derive `'opaque-embed'` from), not hard-coded.
|
|
1254
|
+
*/
|
|
1255
|
+
export async function enterIngestModeReact(store: EditorStore, folderId: string): Promise<void> {
|
|
1256
|
+
if (getEditorMode() === 'browser') {
|
|
1257
|
+
editorConsole.error(
|
|
1258
|
+
'Native-React ingest needs the dev server (/@fs/ dynamic import) — unavailable in browser mode',
|
|
1259
|
+
'ingest',
|
|
1260
|
+
);
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
const game = getIngestGameReact(folderId);
|
|
1264
|
+
if (!game) {
|
|
1265
|
+
editorConsole.error(`Unknown react ingest game: ${folderId}`, 'ingest');
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
const projectRoot = ingestGameReactProjectRoot(folderId);
|
|
1269
|
+
if (!projectRoot) {
|
|
1270
|
+
editorConsole.error(
|
|
1271
|
+
`React ingest "${folderId}": no __VGAI_ENGINE_ROOT__ available to resolve its entry`,
|
|
1272
|
+
'ingest',
|
|
1273
|
+
);
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
const gameContainer = getGameContainer();
|
|
1277
|
+
if (!gameContainer) {
|
|
1278
|
+
editorConsole.error('Game container not mounted — cannot ingest (react)', 'ingest');
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
// D-V1: tear down WHATEVER kind is currently active, not just a prior
|
|
1282
|
+
// react session — closes the cross-kind leak (F26 residual).
|
|
1283
|
+
exitActiveIngest();
|
|
1284
|
+
|
|
1285
|
+
store.setPlayState('playing');
|
|
1286
|
+
editorConsole.log(`Ingesting unmodified native-React game: ${game.manifest.name}`, 'ingest');
|
|
1287
|
+
|
|
1288
|
+
// F26: same cold-mount announcement as the other ingest tiers — the react
|
|
1289
|
+
// world's entry executes in the editor realm and may declare the game
|
|
1290
|
+
// contract to defer its session until ▶. Opt-in, never demanded.
|
|
1291
|
+
(window as unknown as { __vgaiMountCold?: boolean }).__vgaiMountCold = true;
|
|
1292
|
+
const { createGameRuntime } = await import('@engine/runtime/create-runtime');
|
|
1293
|
+
const specs = await resolveAllWorlds(game.manifest, projectRoot);
|
|
1294
|
+
const w = gameContainer.clientWidth;
|
|
1295
|
+
const h = gameContainer.clientHeight;
|
|
1296
|
+
const session = await createGameRuntime({
|
|
1297
|
+
container: gameContainer,
|
|
1298
|
+
worlds: specs,
|
|
1299
|
+
width: w,
|
|
1300
|
+
height: h,
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
// D-K4: load any previously-saved DOM style overlay, then construct the
|
|
1304
|
+
// real authoring adapter over the mounted world's live DOM root. The
|
|
1305
|
+
// adapter has its own `selection` provider (store-backed, same shape as
|
|
1306
|
+
// `ReactWorldAuthoringAdapter.selection`), so — unlike a `{ module }`
|
|
1307
|
+
// adapter with none — it is installed directly, never `withStoreSelection`-wrapped.
|
|
1308
|
+
const worldId = game.worldId;
|
|
1309
|
+
const overlay = await loadWorldOverlay<ReactDomOverlay>(worldId);
|
|
1310
|
+
const root = session.game.worlds[0]!.reactRoot();
|
|
1311
|
+
const adapter = new ReactDomAuthoringAdapter(root, store, {
|
|
1312
|
+
overlay,
|
|
1313
|
+
persist: (o) => saveWorldOverlay(worldId, o),
|
|
1314
|
+
destination: worldOverlayPath(worldId),
|
|
1315
|
+
});
|
|
1316
|
+
// Unlike the capture tiers, a react ingest mounts through the NATIVE
|
|
1317
|
+
// runtime — its Game.play pause/resume IS the honest control (D10). Named
|
|
1318
|
+
// (rather than inlined below) so `setPausedPresent`'s D-L1 `typeof`
|
|
1319
|
+
// check reads the SAME real control the session actually installs, never
|
|
1320
|
+
// an independent literal.
|
|
1321
|
+
const setPaused = (p: boolean) => (p ? session.game.play.pause() : session.game.play.resume());
|
|
1322
|
+
|
|
1323
|
+
_active = {
|
|
1324
|
+
kind: 'react',
|
|
1325
|
+
session: {
|
|
1326
|
+
store,
|
|
1327
|
+
setPaused,
|
|
1328
|
+
stop: () => session.stop(),
|
|
1329
|
+
adapter,
|
|
1330
|
+
},
|
|
1331
|
+
siblings: [],
|
|
1332
|
+
};
|
|
1333
|
+
// Same play-mode input gate every other same-realm ingest tier drives
|
|
1334
|
+
// (R2c) — harmless here too even though a native-React game's own
|
|
1335
|
+
// listeners are NOT same-realm-shadowed the way `ingest/games`/`games-2d`
|
|
1336
|
+
// are (see game-globals-shadow.ts's roots — `games-react` is not among
|
|
1337
|
+
// them; R-N6 residual, recorded in the N2 report).
|
|
1338
|
+
setGameInputGate(() => store.playState === 'playing' && store.activeViewportTab === 'game');
|
|
1339
|
+
|
|
1340
|
+
setActiveAuthoring(adapter);
|
|
1341
|
+
setActiveSystems({});
|
|
1342
|
+
store.setActiveViewportTab('game');
|
|
1343
|
+
store.notifyIngestEdit();
|
|
1344
|
+
setMountFailureReport(null); // D-W3: mount succeeded
|
|
1345
|
+
|
|
1346
|
+
(window as unknown as Record<string, unknown>)['__vgaiIngestReact'] = {
|
|
1347
|
+
gameId: folderId,
|
|
1348
|
+
worldId: game.worldId,
|
|
1349
|
+
noAuthoring: false,
|
|
1350
|
+
// D-K4: COMPUTED from the real adapter (the same honest authority
|
|
1351
|
+
// `achieved-tier.ts`'s `computeAchievedTier` already was for the prior
|
|
1352
|
+
// embed-only floor) — a hierarchy+inspector+persistence adapter with NO
|
|
1353
|
+
// `transforms` honestly bands as 'shared', never hard-coded.
|
|
1354
|
+
achievedTier: computeAchievedTier({ authoring: adapter }),
|
|
1355
|
+
// D-L1 (wave 10 Slice B): the doctor MOUNTED-bar evidence accessor —
|
|
1356
|
+
// see `reactDomEvidence`'s own doc comment above.
|
|
1357
|
+
domEvidence: () => reactDomEvidence(root, adapter),
|
|
1358
|
+
// D-L1: honest `typeof === 'function'` check on the REAL session
|
|
1359
|
+
// control this route just installed above — never an independent
|
|
1360
|
+
// literal `true` (mirrors `__vgaiSiblingMounts`'s identical discipline).
|
|
1361
|
+
setPausedPresent: typeof setPaused === 'function',
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
/**
|
|
1366
|
+
* D-Y2 (docs/EXTERNAL-REACT-AND-MODULE-DESIGN.md §1/§4 slice S2) — the
|
|
1367
|
+
* manifest-route sibling of {@link enterIngestModeReact} above: an
|
|
1368
|
+
* EXTERNAL-FOLDER `ingest-react` world (the opened project's OWN
|
|
1369
|
+
* `vgai.game.json`, not the vendored `games-react/<id>` discovery glob)
|
|
1370
|
+
* enters the same react ingest session. Exactly the same worlds-path recipe
|
|
1371
|
+
* (one-element `resolveAllWorlds` + worlds-path `createGameRuntime`) and the
|
|
1372
|
+
* same `ReactDomAuthoringAdapter` D-K4 wiring (see that function's doc
|
|
1373
|
+
* comment) — only the SOURCE of `world`/`projectRoot` differs (the opened project's manifest +
|
|
1374
|
+
* `project.rootPath`, via `tryManifestIngestRouteReact` below, instead of
|
|
1375
|
+
* `getIngestGameReact`/`ingestGameReactProjectRoot`'s vendored-fixture
|
|
1376
|
+
* lookup). `resolveIngestReactAdapter`'s own guards (D-N3 deduped-only,
|
|
1377
|
+
* `entry` required, no `scene`) run unchanged inside `resolveAllWorlds` — this
|
|
1378
|
+
* function adds no new validation of its own, mirroring
|
|
1379
|
+
* `enterIngestModeFromManifestWorld2D`'s relationship to its vendored
|
|
1380
|
+
* `?ingest=<id>` sibling.
|
|
1381
|
+
*/
|
|
1382
|
+
export async function enterIngestModeFromManifestWorldReact(
|
|
1383
|
+
store: EditorStore,
|
|
1384
|
+
manifest: ResolvedGameManifest,
|
|
1385
|
+
world: ResolvedWorldEntry,
|
|
1386
|
+
projectRoot: string,
|
|
1387
|
+
): Promise<void> {
|
|
1388
|
+
if (getEditorMode() === 'browser') {
|
|
1389
|
+
editorConsole.error(
|
|
1390
|
+
'Native-React ingest needs the dev server (/@fs/ dynamic import) — unavailable in browser mode',
|
|
1391
|
+
'ingest',
|
|
1392
|
+
);
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
const gameContainer = getGameContainer();
|
|
1396
|
+
if (!gameContainer) {
|
|
1397
|
+
editorConsole.error('Game container not mounted — cannot ingest (react)', 'ingest');
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
// D-V1: tear down WHATEVER kind is currently active, not just a prior
|
|
1401
|
+
// react session — closes the cross-kind leak (F26 residual).
|
|
1402
|
+
exitActiveIngest();
|
|
1403
|
+
|
|
1404
|
+
store.setPlayState('playing');
|
|
1405
|
+
editorConsole.log(
|
|
1406
|
+
`Ingesting unmodified native-React game (external folder): ${world.id}`,
|
|
1407
|
+
'ingest',
|
|
1408
|
+
);
|
|
1409
|
+
|
|
1410
|
+
// F26: same cold-mount announcement the vendored route (`enterIngestModeReact`
|
|
1411
|
+
// above) makes — an external-folder game may declare the game contract too.
|
|
1412
|
+
(window as unknown as { __vgaiMountCold?: boolean }).__vgaiMountCold = true;
|
|
1413
|
+
const { createGameRuntime } = await import('@engine/runtime/create-runtime');
|
|
1414
|
+
const specs = await resolveAllWorlds({ ...manifest, worlds: [world] }, projectRoot);
|
|
1415
|
+
const w = gameContainer.clientWidth;
|
|
1416
|
+
const h = gameContainer.clientHeight;
|
|
1417
|
+
const session = await createGameRuntime({
|
|
1418
|
+
container: gameContainer,
|
|
1419
|
+
worlds: specs,
|
|
1420
|
+
width: w,
|
|
1421
|
+
height: h,
|
|
1422
|
+
});
|
|
1423
|
+
|
|
1424
|
+
// D-K4: same overlay-load + adapter construction as the vendored route
|
|
1425
|
+
// above — `worldId` here is `world.id` (the manifest world's own id),
|
|
1426
|
+
// not `game.worldId` (the vendored-fixture wrapper's field).
|
|
1427
|
+
const worldId = world.id;
|
|
1428
|
+
const overlay = await loadWorldOverlay<ReactDomOverlay>(worldId);
|
|
1429
|
+
const root = session.game.worlds[0]!.reactRoot();
|
|
1430
|
+
const adapter = new ReactDomAuthoringAdapter(root, store, {
|
|
1431
|
+
overlay,
|
|
1432
|
+
persist: (o) => saveWorldOverlay(worldId, o),
|
|
1433
|
+
destination: worldOverlayPath(worldId),
|
|
1434
|
+
});
|
|
1435
|
+
// Same honest control as the vendored route (D10): a manifest-routed react
|
|
1436
|
+
// ingest mounts through the NATIVE runtime, so Game.play pause/resume IS
|
|
1437
|
+
// real. Named (D-L1) so `setPausedPresent` checks the SAME real control.
|
|
1438
|
+
const setPaused = (p: boolean) => (p ? session.game.play.pause() : session.game.play.resume());
|
|
1439
|
+
|
|
1440
|
+
_active = {
|
|
1441
|
+
kind: 'react',
|
|
1442
|
+
session: {
|
|
1443
|
+
store,
|
|
1444
|
+
setPaused,
|
|
1445
|
+
stop: () => session.stop(),
|
|
1446
|
+
adapter,
|
|
1447
|
+
},
|
|
1448
|
+
siblings: [],
|
|
1449
|
+
};
|
|
1450
|
+
setGameInputGate(() => store.playState === 'playing' && store.activeViewportTab === 'game');
|
|
1451
|
+
|
|
1452
|
+
setActiveAuthoring(adapter);
|
|
1453
|
+
setActiveSystems({});
|
|
1454
|
+
store.setActiveViewportTab('game');
|
|
1455
|
+
store.notifyIngestEdit();
|
|
1456
|
+
setMountFailureReport(null); // D-W3: mount succeeded
|
|
1457
|
+
|
|
1458
|
+
(window as unknown as Record<string, unknown>)['__vgaiIngestReact'] = {
|
|
1459
|
+
worldId: world.id,
|
|
1460
|
+
noAuthoring: false,
|
|
1461
|
+
// D-K4: see enterIngestModeReact's identical comment above — COMPUTED,
|
|
1462
|
+
// never hard-coded.
|
|
1463
|
+
achievedTier: computeAchievedTier({ authoring: adapter }),
|
|
1464
|
+
// D-L1: see enterIngestModeReact's identical comment above.
|
|
1465
|
+
domEvidence: () => reactDomEvidence(root, adapter),
|
|
1466
|
+
setPausedPresent: typeof setPaused === 'function',
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* D-V1: the react teardown body — byte-for-byte the pre-collapse
|
|
1472
|
+
* `exitIngestModeReact` logic below, extracted so `exitActiveIngest` can
|
|
1473
|
+
* dispatch to it for ANY kind switch. `_active`/`resetIngestPlaySurface` are
|
|
1474
|
+
* handled by the caller (`exitActiveIngest`) — this only does the
|
|
1475
|
+
* react-specific teardown.
|
|
1476
|
+
*/
|
|
1477
|
+
function disposeReactSession(s: IngestSessionReact): void {
|
|
1478
|
+
setGameInputGate(() => true);
|
|
1479
|
+
// D-K4: this session's ReactDomAuthoringAdapter self-registered with the
|
|
1480
|
+
// global undo timeline at construction (mirrors `IngestAuthoringAdapter`/
|
|
1481
|
+
// `World2DAuthoringAdapter`/`ReactWorldAuthoringAdapter`) — drop it so any
|
|
1482
|
+
// of its entries still on the global stack are dropped (with a warn), not
|
|
1483
|
+
// undone against a torn-down DOM.
|
|
1484
|
+
unregisterUndoAdapter(s.adapter);
|
|
1485
|
+
setActiveAuthoring(null);
|
|
1486
|
+
setActiveSystems(null);
|
|
1487
|
+
setMountFailureReport(null); // D-W3: no session -> no banner to show (mirrors disposeThreejsSession's identical call)
|
|
1488
|
+
s.store.setPlayState('stopped');
|
|
1489
|
+
try {
|
|
1490
|
+
s.stop();
|
|
1491
|
+
} catch {
|
|
1492
|
+
/* ignore */
|
|
1493
|
+
}
|
|
1494
|
+
delete (window as unknown as Record<string, unknown>)['__vgaiIngestReact'];
|
|
1495
|
+
editorConsole.log('react ingest mode stopped', 'ingest');
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
/**
|
|
1499
|
+
* Exit react ingest mode: unmount the react world + tear down its Game/loop.
|
|
1500
|
+
* D-V1: a thin wrapper that only acts when a react session is the one
|
|
1501
|
+
* currently active in `_active` — same observable behavior as before the
|
|
1502
|
+
* collapse; the actual teardown is `disposeReactSession` via
|
|
1503
|
+
* `exitActiveIngest`.
|
|
1504
|
+
*/
|
|
1505
|
+
export function exitIngestModeReact(): void {
|
|
1506
|
+
if (_active?.kind !== 'react') return;
|
|
1507
|
+
exitActiveIngest();
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
/**
|
|
1511
|
+
* D-V1: the threejs teardown body — byte-for-byte the pre-collapse
|
|
1512
|
+
* `exitIngestMode` logic below, extracted so `exitActiveIngest` can dispatch
|
|
1513
|
+
* to it for ANY kind switch, not just a same-kind `exitIngestMode()` call.
|
|
1514
|
+
* `_active`/`resetIngestPlaySurface` are handled by the caller
|
|
1515
|
+
* (`exitActiveIngest`) — this only does the threejs-specific teardown.
|
|
1516
|
+
*/
|
|
1517
|
+
function disposeThreejsSession(s: IngestSession): void {
|
|
1518
|
+
// R2c: no ingest session running — the input gate `enterIngestModeWithGame`
|
|
1519
|
+
// set (alongside every `_active` assignment above) must never stay
|
|
1520
|
+
// suppressed. Mirrors play-mode.ts's exitPlayMode, which resets the same
|
|
1521
|
+
// way on exit.
|
|
1522
|
+
setGameInputGate(() => true);
|
|
1523
|
+
|
|
1524
|
+
// T3.2 slice 2: this ingest session's authoring adapter is done recording —
|
|
1525
|
+
// drop it from the undo timeline's live set so any of its entries still on the
|
|
1526
|
+
// global stack are dropped (with a warn), not undone against a torn-down scene.
|
|
1527
|
+
if (s.mount?.authoring) unregisterUndoAdapter(s.mount.authoring);
|
|
1528
|
+
setActiveAuthoring(null);
|
|
1529
|
+
setActiveSystems(null);
|
|
1530
|
+
setActiveOverlayReport(null); // D9: no session -> no banner to show
|
|
1531
|
+
setMountFailureReport(null); // D-W3: no session -> no mount-failure banner to show either
|
|
1532
|
+
s.resizeObserver?.disconnect();
|
|
1533
|
+
// Only the shared-`three` capture path swaps the editor scene; embed-only never did.
|
|
1534
|
+
if (s.mount) {
|
|
1535
|
+
try {
|
|
1536
|
+
s.store.exitPlayScene();
|
|
1537
|
+
} catch {
|
|
1538
|
+
/* scene may not have been swapped if capture failed */
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
s.store.setPlayState('stopped');
|
|
1542
|
+
|
|
1543
|
+
// Tear down the mounted game (stops its loop, frees renderer, uninstalls trap)
|
|
1544
|
+
// or the embedded iframe.
|
|
1545
|
+
try {
|
|
1546
|
+
s.mount?.game.dispose();
|
|
1547
|
+
s.embed?.dispose();
|
|
1548
|
+
} catch {
|
|
1549
|
+
/* ignore */
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
delete (window as unknown as Record<string, unknown>)['__vgaiIngest'];
|
|
1553
|
+
editorConsole.log('Ingest mode stopped', 'ingest');
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
/**
|
|
1557
|
+
* Exit ingest mode: restore the editor scene and tear down the game. D-V1: a
|
|
1558
|
+
* thin wrapper that only acts when a threejs session is the one currently
|
|
1559
|
+
* active in `_active` — same observable behavior as before the collapse
|
|
1560
|
+
* (a no-op if nothing, or a DIFFERENT kind, is active); the actual teardown
|
|
1561
|
+
* is `disposeThreejsSession` via `exitActiveIngest`.
|
|
1562
|
+
*/
|
|
1563
|
+
export function exitIngestMode(): void {
|
|
1564
|
+
if (_active?.kind !== 'threejs') return;
|
|
1565
|
+
exitActiveIngest();
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
// Dev hook so the editor UI / e2e can drive ingestion against the bound store.
|
|
1569
|
+
if (import.meta.env.DEV) {
|
|
1570
|
+
(window as unknown as Record<string, unknown>)['__vgaiIngestApi'] = {
|
|
1571
|
+
enter: (gameId: string) => {
|
|
1572
|
+
if (!_boundStore) throw new Error('ingest-mode not bound');
|
|
1573
|
+
return enterIngestMode(_boundStore, gameId);
|
|
1574
|
+
},
|
|
1575
|
+
exit: exitIngestMode,
|
|
1576
|
+
// This hook is the per-kind (threejs) API — keep it slot-specific;
|
|
1577
|
+
// `isIngestActive` itself is kind-agnostic as of F26.
|
|
1578
|
+
isActive: () => _active?.kind === 'threejs',
|
|
1579
|
+
// Test/dev: reset a game's persisted overlay to empty (clean-slate ingests).
|
|
1580
|
+
clearOverlay: (gameId: string) => saveOverlay(gameId, {}),
|
|
1581
|
+
};
|
|
1582
|
+
// Pixi sibling (T6.1 slice 4) — same shape, over the world2d ingest session.
|
|
1583
|
+
(window as unknown as Record<string, unknown>)['__vgaiIngestApi2D'] = {
|
|
1584
|
+
enter: (gameId: string) => {
|
|
1585
|
+
if (!_boundStore) throw new Error('ingest-mode not bound');
|
|
1586
|
+
return enterIngestMode2D(_boundStore, gameId);
|
|
1587
|
+
},
|
|
1588
|
+
exit: exitIngestMode2D,
|
|
1589
|
+
isActive: isIngestActive2D,
|
|
1590
|
+
clearOverlay: (worldId: string) => saveWorldOverlay(worldId, {}),
|
|
1591
|
+
};
|
|
1592
|
+
// React sibling (Track N, N2) — same shape, over the native-React ingest
|
|
1593
|
+
// session. No `clearOverlay` (D-N8: no authoring/overlay surface in v1).
|
|
1594
|
+
(window as unknown as Record<string, unknown>)['__vgaiIngestApiReact'] = {
|
|
1595
|
+
enter: (folderId: string) => {
|
|
1596
|
+
if (!_boundStore) throw new Error('ingest-mode not bound');
|
|
1597
|
+
return enterIngestModeReact(_boundStore, folderId);
|
|
1598
|
+
},
|
|
1599
|
+
exit: exitIngestModeReact,
|
|
1600
|
+
isActive: isIngestActiveReact,
|
|
1601
|
+
};
|
|
1602
|
+
// D-A3 (docs/WAVE6-DOCTOR-DESIGN.md): headless-readable mount outcomes —
|
|
1603
|
+
// `packages/editor/src/authoring/mount-failure-report.ts`'s multi-entry
|
|
1604
|
+
// list is otherwise only ever read by `MountFailureBanner.tsx` (a React
|
|
1605
|
+
// component), invisible to `page.evaluate()`. This is a plain function
|
|
1606
|
+
// returning the LIVE list (never a snapshot) — `vgai doctor`'s browser
|
|
1607
|
+
// phase is this surface's first consumer, but any other dev/e2e caller can
|
|
1608
|
+
// poll it the same way `__vgaiIngestApi*` are already polled.
|
|
1609
|
+
(window as unknown as Record<string, unknown>)['__vgaiMountFailureReports'] = () =>
|
|
1610
|
+
getMountFailureReports();
|
|
1611
|
+
// D-H1 (docs/WAVE9-SIBLING-PERSISTENCE-DOCTOR-DESIGN.md Slice B):
|
|
1612
|
+
// headless-readable per-sibling signal — `_active.siblings` (D-V2's
|
|
1613
|
+
// composite-sibling handles, `ingest-siblings.ts`'s `SiblingMount[]`) is
|
|
1614
|
+
// otherwise invisible to `page.evaluate()`, same blind spot
|
|
1615
|
+
// `__vgaiMountFailureReports` closed for mount failures. A plain function
|
|
1616
|
+
// returning a LIVE view (never a snapshot, re-reads `_active` on every
|
|
1617
|
+
// call) of honest per-sibling flags only — no capture/draw proof, see
|
|
1618
|
+
// `DoctorSiblingEvidence`'s doc comment (`packages/editor/src/doctor/report.ts`).
|
|
1619
|
+
(window as unknown as Record<string, unknown>)['__vgaiSiblingMounts'] = () =>
|
|
1620
|
+
(_active?.siblings ?? []).map((s) => ({
|
|
1621
|
+
worldId: s.worldId,
|
|
1622
|
+
setPausedPresent: typeof s.setPaused === 'function',
|
|
1623
|
+
authoringPresent: !!s.authoring,
|
|
1624
|
+
authoringPersist: s.authoring?.capabilities.persist === true,
|
|
1625
|
+
}));
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
/**
|
|
1629
|
+
* Wait for the Game-tab container (registered via a callback ref at layout
|
|
1630
|
+
* mount) — poll a bounded number of rAF ticks so we don't race the first
|
|
1631
|
+
* paint. Exported (T3.4 slice 2) so `module-mode.ts`'s manifest-route auto-
|
|
1632
|
+
* mount for `{ module }` worlds — the sibling of `tryManifestIngestRoute`
|
|
1633
|
+
* below — can reuse the SAME wait rather than duplicating it.
|
|
1634
|
+
*/
|
|
1635
|
+
export async function waitForGameContainer(): Promise<boolean> {
|
|
1636
|
+
return new Promise<boolean>((resolve) => {
|
|
1637
|
+
let tries = 0;
|
|
1638
|
+
const tick = () => {
|
|
1639
|
+
if (getGameContainer()) return resolve(true);
|
|
1640
|
+
if (++tries > 120) return resolve(false); // ~2s at 60fps
|
|
1641
|
+
requestAnimationFrame(tick);
|
|
1642
|
+
};
|
|
1643
|
+
tick();
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
/**
|
|
1648
|
+
* D-V2 (docs/WAVE5-MULTIWORLD-INGEST-DESIGN.md, F24 composite): every world
|
|
1649
|
+
* in `manifest.worlds` whose adapter is `{ ingest }` — a composite manifest
|
|
1650
|
+
* caps this at exactly one (see each `tryManifestIngestRoute*` below); ≥2 is
|
|
1651
|
+
* a loud named error via {@link reportTooManyIngestWorlds}, never a silent
|
|
1652
|
+
* first-wins pick (mirrors `run-bundle.ts`'s fail-on-ambiguity pattern).
|
|
1653
|
+
*/
|
|
1654
|
+
function findIngestWorlds(manifest: ResolvedGameManifest): ResolvedWorldEntry[] {
|
|
1655
|
+
return manifest.worlds.filter((w) => w.adapter.type === 'ingest');
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
/**
|
|
1659
|
+
* D-V2: the "composite declares ≥2 ingest worlds" violation — named, listing
|
|
1660
|
+
* every offending world id, reported once (into BOTH `editorConsole` and the
|
|
1661
|
+
* D-V5 multi-entry mount-failure report, one entry per offending world so
|
|
1662
|
+
* the banner names all of them) by whichever `tryManifestIngestRoute*`
|
|
1663
|
+
* happens to run first (module route order: three, pixi, react — see
|
|
1664
|
+
* `autoLaunchIngestFromURL`) — the caller returns `true` right after this
|
|
1665
|
+
* (claims the route), so the other two ingest routes never also report it.
|
|
1666
|
+
*/
|
|
1667
|
+
function reportTooManyIngestWorlds(ingestWorlds: ResolvedWorldEntry[]): void {
|
|
1668
|
+
const ids = ingestWorlds.map((w) => `"${w.id}"`).join(', ');
|
|
1669
|
+
const message =
|
|
1670
|
+
'a composite manifest declares exactly ONE ingest world in this slice — found ' +
|
|
1671
|
+
`${ingestWorlds.length}: ${ids} (docs/WAVE5-MULTIWORLD-INGEST-DESIGN.md D-V2).`;
|
|
1672
|
+
editorConsole.error(`Manifest composite ingest route: ${message}`, 'ingest');
|
|
1673
|
+
for (const world of ingestWorlds) {
|
|
1674
|
+
addMountFailureReport({
|
|
1675
|
+
worldId: world.id,
|
|
1676
|
+
kind: world.kind,
|
|
1677
|
+
identity: world.adapter.identity,
|
|
1678
|
+
message,
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
/**
|
|
1684
|
+
* D-V2: attach a composite's sibling handles onto the JUST-entered `_active`
|
|
1685
|
+
* session (set by the primary ingest world's own `enterIngestMode*` call,
|
|
1686
|
+
* immediately before this runs) — a no-op if `_active` is somehow no longer
|
|
1687
|
+
* set (defensive only; nothing in this synchronous call chain clears it
|
|
1688
|
+
* between the primary mount and this attach).
|
|
1689
|
+
*/
|
|
1690
|
+
function attachIngestSiblings(siblings: readonly SiblingMount[]): void {
|
|
1691
|
+
if (_active) _active.siblings = [...siblings];
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
/**
|
|
1695
|
+
* D-V2: mount every OTHER world in a composite manifest (every world besides
|
|
1696
|
+
* `ingestWorldId`, the one just entered through its own `enterIngestMode*`
|
|
1697
|
+
* path) via `ingest-siblings.ts`'s `mountIngestSiblings`, and attach the
|
|
1698
|
+
* resulting handles onto `_active` so `exitActiveIngest` tears them down.
|
|
1699
|
+
* Dynamically imported — `ingest-siblings.ts` imports from `module-mode.ts`,
|
|
1700
|
+
* which itself statically imports `waitForGameContainer` from THIS module;
|
|
1701
|
+
* a static import here would close that cycle at the wrong end (mirrors
|
|
1702
|
+
* `autoLaunchIngestFromURL`'s own dynamic import of `module-mode.ts` below,
|
|
1703
|
+
* for the identical reason). Never throws: `mountIngestSiblings` already
|
|
1704
|
+
* catches every individual sibling's failure into the mount-failure report.
|
|
1705
|
+
* Returns the mounted handles so the caller can promote authoring (D-E2,
|
|
1706
|
+
* {@link promoteToCompositeAuthoring}) — `store` is threaded through so each
|
|
1707
|
+
* sibling gets an honest `SiblingMount.authoring` (D-E1).
|
|
1708
|
+
*/
|
|
1709
|
+
async function mountCompositeSiblings(
|
|
1710
|
+
manifest: ResolvedGameManifest,
|
|
1711
|
+
ingestWorldId: string,
|
|
1712
|
+
gameContainer: HTMLElement,
|
|
1713
|
+
projectRoot: string,
|
|
1714
|
+
store: EditorStore,
|
|
1715
|
+
): Promise<SiblingMount[]> {
|
|
1716
|
+
const { mountIngestSiblings } = await import('./ingest-siblings');
|
|
1717
|
+
const siblings = await mountIngestSiblings(
|
|
1718
|
+
manifest,
|
|
1719
|
+
ingestWorldId,
|
|
1720
|
+
gameContainer,
|
|
1721
|
+
projectRoot,
|
|
1722
|
+
store,
|
|
1723
|
+
);
|
|
1724
|
+
attachIngestSiblings(siblings);
|
|
1725
|
+
return siblings;
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
/**
|
|
1729
|
+
* D-E2 (docs/WAVE8-SIBLING-AUTHORING-DESIGN.md, §1F promotion on the ingest
|
|
1730
|
+
* composite): when a composite's siblings actually mounted (`siblings.length
|
|
1731
|
+
* > 0`), promote the active authoring override — currently the PRIMARY
|
|
1732
|
+
* ingest world's own plain adapter, installed by the route's own enter path
|
|
1733
|
+
* that just ran immediately before this call — to a `CompositeAuthoringAdapter`
|
|
1734
|
+
* whose ordered children are the primary world FIRST (it is the route's
|
|
1735
|
+
* anchor) followed by every mounted sibling in manifest order, each routed by
|
|
1736
|
+
* its own manifest `kind`. A single-world mount (`siblings.length === 0`) is
|
|
1737
|
+
* untouched — byte-stable, no group node, no wrapper: §1F's single-world
|
|
1738
|
+
* clause.
|
|
1739
|
+
*
|
|
1740
|
+
* The primary's own adapter is read back via `getAuthoringOverride()` rather
|
|
1741
|
+
* than threaded as a parameter — every composite route's own `enterIngestMode*`
|
|
1742
|
+
* call already installed it via `setActiveAuthoring` synchronously before
|
|
1743
|
+
* this runs (mirrors `attachIngestSiblings`'s identical "just installed"
|
|
1744
|
+
* assumption above); `null` (defensive only — nothing in this synchronous
|
|
1745
|
+
* call chain clears it in between) is a no-op, same guard shape.
|
|
1746
|
+
*/
|
|
1747
|
+
function promoteToCompositeAuthoring(
|
|
1748
|
+
manifest: ResolvedGameManifest,
|
|
1749
|
+
ingestWorld: ResolvedWorldEntry,
|
|
1750
|
+
siblings: readonly SiblingMount[],
|
|
1751
|
+
): void {
|
|
1752
|
+
if (siblings.length === 0) return;
|
|
1753
|
+
const primaryAdapter = getAuthoringOverride();
|
|
1754
|
+
if (!primaryAdapter) return;
|
|
1755
|
+
const children: CompositeChild[] = [
|
|
1756
|
+
{ worldId: ingestWorld.id, kind: ingestWorld.kind, adapter: primaryAdapter },
|
|
1757
|
+
...siblings.map((s) => ({
|
|
1758
|
+
worldId: s.worldId,
|
|
1759
|
+
kind: manifest.worlds.find((w) => w.id === s.worldId)?.kind ?? 'unknown',
|
|
1760
|
+
// D-E1: every successfully-mounted SiblingMount gets an honest
|
|
1761
|
+
// `authoring` whenever a store was supplied — every composite route
|
|
1762
|
+
// below supplies one, so this is never actually undefined in
|
|
1763
|
+
// production; the non-null assertion mirrors D-E2's own spec.
|
|
1764
|
+
adapter: s.authoring!,
|
|
1765
|
+
})),
|
|
1766
|
+
];
|
|
1767
|
+
setActiveAuthoring(new CompositeAuthoringAdapter(children));
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
/**
|
|
1771
|
+
* Route 1 of `autoLaunchIngestFromURL` (T3.3 slice 3, §1E): a manifest-backed
|
|
1772
|
+
* project declaring an `ingest-three` world enters ingest mode straight from
|
|
1773
|
+
* that world. Returns `true` if this route applied (whether it succeeded or
|
|
1774
|
+
* logged an error) — the caller must not also try route 2 in that case
|
|
1775
|
+
* (§1B's "manifest wins" discovery order); `false` means "no manifest, no
|
|
1776
|
+
* ingest world at all, or the one ingest world isn't ingest-three", so the
|
|
1777
|
+
* next route (pixi, react, then the URL route) still gets a chance.
|
|
1778
|
+
*
|
|
1779
|
+
* D-V2 (F24 composite): scans ALL worlds for `{ ingest }` adapters, not just
|
|
1780
|
+
* `worlds[0]` — 0 ingest worlds declines (unchanged non-ingest-manifest
|
|
1781
|
+
* behavior); ≥2 claims the route and fails loudly
|
|
1782
|
+
* ({@link reportTooManyIngestWorlds}), mounting nothing; exactly 1 whose
|
|
1783
|
+
* identity is `ingest-three` mounts THAT world (wherever it sits in the
|
|
1784
|
+
* array) through the existing enter path, then mounts every other world as a
|
|
1785
|
+
* sibling ({@link mountCompositeSiblings}) — a sibling failure never
|
|
1786
|
+
* unmounts the ingest world (it is caught internally, per-world, by
|
|
1787
|
+
* `ingest-siblings.ts`).
|
|
1788
|
+
*/
|
|
1789
|
+
async function tryManifestIngestRoute(store: EditorStore): Promise<boolean> {
|
|
1790
|
+
if (getEditorMode() === 'browser') return false; // no dev server to serve /vgai.game.json from
|
|
1791
|
+
const manifest = await fetchGameManifest().catch(() => null);
|
|
1792
|
+
if (!manifest) return false;
|
|
1793
|
+
const ingestWorlds = findIngestWorlds(manifest);
|
|
1794
|
+
if (ingestWorlds.length === 0) return false;
|
|
1795
|
+
if (ingestWorlds.length > 1) {
|
|
1796
|
+
reportTooManyIngestWorlds(ingestWorlds);
|
|
1797
|
+
return true;
|
|
1798
|
+
}
|
|
1799
|
+
const ingestWorld = ingestWorlds[0]!;
|
|
1800
|
+
if (ingestWorld.adapter.identity !== 'ingest-three') return false;
|
|
1801
|
+
|
|
1802
|
+
const project = getCurrentProject();
|
|
1803
|
+
if (!project) {
|
|
1804
|
+
editorConsole.error(
|
|
1805
|
+
'Manifest declares an ingest-three world but no project is open — cannot auto-ingest',
|
|
1806
|
+
'ingest',
|
|
1807
|
+
);
|
|
1808
|
+
return true;
|
|
1809
|
+
}
|
|
1810
|
+
const ready = await waitForGameContainer();
|
|
1811
|
+
if (!ready) {
|
|
1812
|
+
editorConsole.error('Game container never mounted — cannot auto-ingest', 'ingest');
|
|
1813
|
+
return true;
|
|
1814
|
+
}
|
|
1815
|
+
try {
|
|
1816
|
+
// D9/T7.7: thread the manifest's OWN version through — resolveIngestDescriptor
|
|
1817
|
+
// (called inside enterIngestModeFromManifestWorld) only sees the per-world
|
|
1818
|
+
// ResolvedWorldEntry, not this top-level field.
|
|
1819
|
+
// R5 (§4(c)): best-effort read of this project's UPSTREAM.md pin, same
|
|
1820
|
+
// spirit — resolveIngestDescriptor has no way to discover one either.
|
|
1821
|
+
const upstreamPin = await readVendoredUpstreamPin();
|
|
1822
|
+
await enterIngestModeFromManifestWorld(
|
|
1823
|
+
store,
|
|
1824
|
+
ingestWorld,
|
|
1825
|
+
project.rootPath,
|
|
1826
|
+
manifest.version,
|
|
1827
|
+
upstreamPin,
|
|
1828
|
+
);
|
|
1829
|
+
// D-V2: mount every OTHER world as a composite sibling — a no-op loop
|
|
1830
|
+
// when this is a single-world manifest (nothing else to iterate).
|
|
1831
|
+
const gameContainer = getGameContainer();
|
|
1832
|
+
if (gameContainer) {
|
|
1833
|
+
const siblings = await mountCompositeSiblings(
|
|
1834
|
+
manifest,
|
|
1835
|
+
ingestWorld.id,
|
|
1836
|
+
gameContainer,
|
|
1837
|
+
project.rootPath,
|
|
1838
|
+
store,
|
|
1839
|
+
);
|
|
1840
|
+
// D-E2: promote to a composite once any sibling actually mounted —
|
|
1841
|
+
// byte-stable (no-op) for a single-world manifest.
|
|
1842
|
+
promoteToCompositeAuthoring(manifest, ingestWorld, siblings);
|
|
1843
|
+
}
|
|
1844
|
+
} catch (err) {
|
|
1845
|
+
editorConsole.error(`Manifest ingest-three auto-launch failed: ${err}`, 'ingest');
|
|
1846
|
+
// D-W3 (F9): this console line was, until now, the ONLY trace this
|
|
1847
|
+
// failure left anywhere in the UI — the default Untitled scene has
|
|
1848
|
+
// already loaded by this point (§0.4), so nothing visibly changes.
|
|
1849
|
+
// Populate the loud banner slot too.
|
|
1850
|
+
setMountFailureReport({
|
|
1851
|
+
worldId: ingestWorld.id,
|
|
1852
|
+
kind: ingestWorld.kind,
|
|
1853
|
+
identity: ingestWorld.adapter.identity,
|
|
1854
|
+
message: formatMountFailureMessage(err),
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
return true;
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
/**
|
|
1861
|
+
* PixiJS sibling of {@link tryManifestIngestRoute} (T6.1 slice 4): a
|
|
1862
|
+
* manifest-backed project declaring an `ingest-pixi` world enters world2d
|
|
1863
|
+
* ingest mode straight from that world. Same "applied or not" contract, same
|
|
1864
|
+
* D-V2 composite scan/route-selection/sibling-mount shape as its three
|
|
1865
|
+
* sibling above.
|
|
1866
|
+
*/
|
|
1867
|
+
async function tryManifestIngestRoute2D(store: EditorStore): Promise<boolean> {
|
|
1868
|
+
if (getEditorMode() === 'browser') return false; // no dev server to serve /vgai.game.json from
|
|
1869
|
+
const manifest = await fetchGameManifest().catch(() => null);
|
|
1870
|
+
if (!manifest) return false;
|
|
1871
|
+
const ingestWorlds = findIngestWorlds(manifest);
|
|
1872
|
+
if (ingestWorlds.length === 0) return false;
|
|
1873
|
+
if (ingestWorlds.length > 1) {
|
|
1874
|
+
reportTooManyIngestWorlds(ingestWorlds);
|
|
1875
|
+
return true;
|
|
1876
|
+
}
|
|
1877
|
+
const ingestWorld = ingestWorlds[0]!;
|
|
1878
|
+
if (ingestWorld.adapter.identity !== 'ingest-pixi') return false;
|
|
1879
|
+
|
|
1880
|
+
const project = getCurrentProject();
|
|
1881
|
+
if (!project) {
|
|
1882
|
+
editorConsole.error(
|
|
1883
|
+
'Manifest declares an ingest-pixi world but no project is open — cannot auto-ingest',
|
|
1884
|
+
'ingest',
|
|
1885
|
+
);
|
|
1886
|
+
return true;
|
|
1887
|
+
}
|
|
1888
|
+
const ready = await waitForGameContainer();
|
|
1889
|
+
if (!ready) {
|
|
1890
|
+
editorConsole.error('Game container never mounted — cannot auto-ingest', 'ingest');
|
|
1891
|
+
return true;
|
|
1892
|
+
}
|
|
1893
|
+
try {
|
|
1894
|
+
await enterIngestModeFromManifestWorld2D(store, ingestWorld, project.rootPath);
|
|
1895
|
+
const gameContainer = getGameContainer();
|
|
1896
|
+
if (gameContainer) {
|
|
1897
|
+
const siblings = await mountCompositeSiblings(
|
|
1898
|
+
manifest,
|
|
1899
|
+
ingestWorld.id,
|
|
1900
|
+
gameContainer,
|
|
1901
|
+
project.rootPath,
|
|
1902
|
+
store,
|
|
1903
|
+
);
|
|
1904
|
+
// D-E2: promote to a composite once any sibling actually mounted —
|
|
1905
|
+
// byte-stable (no-op) for a single-world manifest.
|
|
1906
|
+
promoteToCompositeAuthoring(manifest, ingestWorld, siblings);
|
|
1907
|
+
}
|
|
1908
|
+
} catch (err) {
|
|
1909
|
+
editorConsole.error(`Manifest ingest-pixi auto-launch failed: ${err}`, 'ingest');
|
|
1910
|
+
// D-W3 (F9) — see tryManifestIngestRoute's identical comment above.
|
|
1911
|
+
setMountFailureReport({
|
|
1912
|
+
worldId: ingestWorld.id,
|
|
1913
|
+
kind: ingestWorld.kind,
|
|
1914
|
+
identity: ingestWorld.adapter.identity,
|
|
1915
|
+
message: formatMountFailureMessage(err),
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
return true;
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
/**
|
|
1922
|
+
* React sibling of {@link tryManifestIngestRoute}/{@link tryManifestIngestRoute2D}
|
|
1923
|
+
* (D-Y2, docs/EXTERNAL-REACT-AND-MODULE-DESIGN.md §1/§4 slice S2): a
|
|
1924
|
+
* manifest-backed project declaring an `ingest-react` world enters the
|
|
1925
|
+
* native-React ingest session straight from that world (external folder —
|
|
1926
|
+
* `vgai edit <folder>` — not the vendored `games-react/<id>` discovery glob).
|
|
1927
|
+
* Same "applied or not" contract, same D-V2 composite scan/route-selection/
|
|
1928
|
+
* sibling-mount shape as its three/pixi siblings above.
|
|
1929
|
+
*/
|
|
1930
|
+
async function tryManifestIngestRouteReact(store: EditorStore): Promise<boolean> {
|
|
1931
|
+
if (getEditorMode() === 'browser') return false; // no dev server to serve /vgai.game.json from
|
|
1932
|
+
const manifest = await fetchGameManifest().catch(() => null);
|
|
1933
|
+
if (!manifest) return false;
|
|
1934
|
+
const ingestWorlds = findIngestWorlds(manifest);
|
|
1935
|
+
if (ingestWorlds.length === 0) return false;
|
|
1936
|
+
if (ingestWorlds.length > 1) {
|
|
1937
|
+
reportTooManyIngestWorlds(ingestWorlds);
|
|
1938
|
+
return true;
|
|
1939
|
+
}
|
|
1940
|
+
const ingestWorld = ingestWorlds[0]!;
|
|
1941
|
+
if (ingestWorld.adapter.identity !== 'ingest-react') return false;
|
|
1942
|
+
|
|
1943
|
+
const project = getCurrentProject();
|
|
1944
|
+
if (!project) {
|
|
1945
|
+
editorConsole.error(
|
|
1946
|
+
'Manifest declares an ingest-react world but no project is open — cannot auto-ingest',
|
|
1947
|
+
'ingest',
|
|
1948
|
+
);
|
|
1949
|
+
return true;
|
|
1950
|
+
}
|
|
1951
|
+
const ready = await waitForGameContainer();
|
|
1952
|
+
if (!ready) {
|
|
1953
|
+
editorConsole.error('Game container never mounted — cannot auto-ingest', 'ingest');
|
|
1954
|
+
return true;
|
|
1955
|
+
}
|
|
1956
|
+
try {
|
|
1957
|
+
await enterIngestModeFromManifestWorldReact(store, manifest, ingestWorld, project.rootPath);
|
|
1958
|
+
const gameContainer = getGameContainer();
|
|
1959
|
+
if (gameContainer) {
|
|
1960
|
+
const siblings = await mountCompositeSiblings(
|
|
1961
|
+
manifest,
|
|
1962
|
+
ingestWorld.id,
|
|
1963
|
+
gameContainer,
|
|
1964
|
+
project.rootPath,
|
|
1965
|
+
store,
|
|
1966
|
+
);
|
|
1967
|
+
// D-E2: promote to a composite once any sibling actually mounted —
|
|
1968
|
+
// byte-stable (no-op) for a single-world manifest.
|
|
1969
|
+
promoteToCompositeAuthoring(manifest, ingestWorld, siblings);
|
|
1970
|
+
}
|
|
1971
|
+
} catch (err) {
|
|
1972
|
+
editorConsole.error(`Manifest ingest-react auto-launch failed: ${err}`, 'ingest');
|
|
1973
|
+
// D-W3 (F9) — see tryManifestIngestRoute's identical comment above.
|
|
1974
|
+
setMountFailureReport({
|
|
1975
|
+
worldId: ingestWorld.id,
|
|
1976
|
+
kind: ingestWorld.kind,
|
|
1977
|
+
identity: ingestWorld.adapter.identity,
|
|
1978
|
+
message: formatMountFailureMessage(err),
|
|
1979
|
+
});
|
|
1980
|
+
}
|
|
1981
|
+
return true;
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
/**
|
|
1985
|
+
* Hosted entry point, called once after `loadInitialScene`: auto-launch ingest
|
|
1986
|
+
* mode from whichever discovery route applies, so ingestion is drivable
|
|
1987
|
+
* without the dev hook. Checked in order (§1B's "manifest wins" discovery
|
|
1988
|
+
* order):
|
|
1989
|
+
*
|
|
1990
|
+
* 1. Manifest route, `{ module }` (T3.4 slice 2, docs/ADAPTER-AUTHORING-
|
|
1991
|
+
* DESIGN.md §2/§3 slice 2, `./module-mode.ts`'s `tryManifestModuleRoute`)
|
|
1992
|
+
* — a manifest's first world declaring a custom adapter module
|
|
1993
|
+
* auto-mounts through the generic `GameAdapter`/`HostContext` contract
|
|
1994
|
+
* (or, since wave 2's D-Y1, the WORLDS PATH for a non-three module world
|
|
1995
|
+
* — see that file) and installs ITS OWN `MountedGame.authoring` as the
|
|
1996
|
+
* active override, so the SAME adapter-driven hierarchy/inspector panels
|
|
1997
|
+
* below drive it. Mutually exclusive with routes 2–4 by construction (a
|
|
1998
|
+
* world resolves to exactly one adapter identity).
|
|
1999
|
+
* 2. Manifest route, three (T3.3 slice 3, docs/CLI-ON-FOLDER-DESIGN.md §1E,
|
|
2000
|
+
* `tryManifestIngestRoute` above) — the CLI-on-folder AC ("point the CLI
|
|
2001
|
+
* at an ingest game folder never registered in source -> opens, plays,
|
|
2002
|
+
* edits"). Browser-mode builds and legacy `project.json`-only projects
|
|
2003
|
+
* both fall straight through to route 5, unchanged.
|
|
2004
|
+
* 3. Manifest route, pixi (T6.1 slice 4, `tryManifestIngestRoute2D` above) —
|
|
2005
|
+
* the world2d silo retirement's replacement discovery path for an
|
|
2006
|
+
* `ingest-pixi` world.
|
|
2007
|
+
* 4. Manifest route, react (D-Y2, wave 2, `tryManifestIngestRouteReact`
|
|
2008
|
+
* above) — the external-folder replacement discovery path for an
|
|
2009
|
+
* `ingest-react` world (the vendored `?ingest=<id>` glob below stays the
|
|
2010
|
+
* in-tree-fixture route). A manifest's first world resolves to exactly
|
|
2011
|
+
* one adapter identity, so routes 1–4 are mutually exclusive in
|
|
2012
|
+
* practice; all are tried since neither can tell that in advance without
|
|
2013
|
+
* fetching.
|
|
2014
|
+
* 5. `?ingest=<gameId>` URL route: resolves `gameId` against the in-tree
|
|
2015
|
+
* three.js fixture glob first (`ingest/discovery.ts`, unchanged), then —
|
|
2016
|
+
* T6.1 slice 4 — the world2d fixture glob (`ingest/discovery2d.ts`,
|
|
2017
|
+
* bunnymark/flappy/tilemap-camera's replacement launch route for the
|
|
2018
|
+
* retired `__vgaiWorld2dApi.enterVendored`-style dev hook), then —
|
|
2019
|
+
* Track N, N2 — the native-React fixture glob (`ingest/discovery-react
|
|
2020
|
+
* .ts`, e.g. `?ingest=react-rpg`). Unknown ids are ignored (a console
|
|
2021
|
+
* note); the editor stays a normal session.
|
|
2022
|
+
*/
|
|
2023
|
+
export async function autoLaunchIngestFromURL(store: EditorStore): Promise<void> {
|
|
2024
|
+
const { tryManifestModuleRoute } = await import('./module-mode');
|
|
2025
|
+
if (await tryManifestModuleRoute(store)) return;
|
|
2026
|
+
if (await tryManifestIngestRoute(store)) return;
|
|
2027
|
+
if (await tryManifestIngestRoute2D(store)) return;
|
|
2028
|
+
if (await tryManifestIngestRouteReact(store)) return;
|
|
2029
|
+
|
|
2030
|
+
const gameId = new URLSearchParams(window.location.search).get('ingest');
|
|
2031
|
+
if (!gameId) return;
|
|
2032
|
+
const ready = await waitForGameContainer();
|
|
2033
|
+
if (getIngestGame(gameId)) {
|
|
2034
|
+
if (!ready) {
|
|
2035
|
+
editorConsole.error('Game container never mounted — cannot auto-ingest', 'ingest');
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
2038
|
+
try {
|
|
2039
|
+
await enterIngestMode(store, gameId);
|
|
2040
|
+
} catch (err) {
|
|
2041
|
+
editorConsole.error(`Auto-ingest failed: ${err}`, 'ingest');
|
|
2042
|
+
// D-W3 (F9): the `?ingest=<id>` URL-fallback route has no
|
|
2043
|
+
// `ResolvedWorldEntry` to read a world id/kind from (it resolves
|
|
2044
|
+
// straight off the in-tree fixture globs) — `gameId` and the fixed
|
|
2045
|
+
// 'threejs'/'ingest-three' vocabulary are the closest honest analog.
|
|
2046
|
+
setMountFailureReport({
|
|
2047
|
+
worldId: gameId,
|
|
2048
|
+
kind: 'threejs',
|
|
2049
|
+
identity: 'ingest-three',
|
|
2050
|
+
message: formatMountFailureMessage(err),
|
|
2051
|
+
});
|
|
2052
|
+
}
|
|
2053
|
+
return;
|
|
2054
|
+
}
|
|
2055
|
+
if (getIngestGame2D(gameId)) {
|
|
2056
|
+
if (!ready) {
|
|
2057
|
+
editorConsole.error('Game container never mounted — cannot auto-ingest', 'ingest');
|
|
2058
|
+
return;
|
|
2059
|
+
}
|
|
2060
|
+
try {
|
|
2061
|
+
await enterIngestMode2D(store, gameId);
|
|
2062
|
+
} catch (err) {
|
|
2063
|
+
editorConsole.error(`Auto-ingest failed (world2d): ${err}`, 'ingest');
|
|
2064
|
+
setMountFailureReport({
|
|
2065
|
+
worldId: gameId,
|
|
2066
|
+
kind: 'pixijs',
|
|
2067
|
+
identity: 'ingest-pixi',
|
|
2068
|
+
message: formatMountFailureMessage(err),
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
return;
|
|
2072
|
+
}
|
|
2073
|
+
if (getIngestGameReact(gameId)) {
|
|
2074
|
+
if (!ready) {
|
|
2075
|
+
editorConsole.error('Game container never mounted — cannot auto-ingest', 'ingest');
|
|
2076
|
+
return;
|
|
2077
|
+
}
|
|
2078
|
+
try {
|
|
2079
|
+
await enterIngestModeReact(store, gameId);
|
|
2080
|
+
} catch (err) {
|
|
2081
|
+
editorConsole.error(`Auto-ingest failed (react): ${err}`, 'ingest');
|
|
2082
|
+
setMountFailureReport({
|
|
2083
|
+
worldId: gameId,
|
|
2084
|
+
kind: 'react',
|
|
2085
|
+
identity: 'ingest-react',
|
|
2086
|
+
message: formatMountFailureMessage(err),
|
|
2087
|
+
});
|
|
2088
|
+
}
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
editorConsole.error(`Unknown ingest game in ?ingest= : "${gameId}"`, 'ingest');
|
|
2092
|
+
}
|