@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
package/src/play-mode.ts
ADDED
|
@@ -0,0 +1,1148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Play-mode orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* Editor and game are fully isolated — separate canvas, renderer, and scene.
|
|
5
|
+
* Play mode creates a new game canvas, launches a standalone game session,
|
|
6
|
+
* and tears it all down on stop. The editor scene is never touched.
|
|
7
|
+
*
|
|
8
|
+
* Flow:
|
|
9
|
+
* enterPlayMode → create canvas → createGameRuntime() → game loop
|
|
10
|
+
* exitPlayMode → game stop → remove canvas → re-enable editor
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { AuthoringAdapter } from '@engine/adapter';
|
|
14
|
+
import type { VgaiMountedGame } from '@engine/adapter/vgai-scene-game-adapter';
|
|
15
|
+
import { assertNever } from '@engine/adapter/world-kind';
|
|
16
|
+
import { _engineLogActive } from '@engine/dev/logger';
|
|
17
|
+
import type { GameComponentClass } from '@engine/ecs/game-component';
|
|
18
|
+
import * as gameComponentModule from '@engine/ecs/game-component';
|
|
19
|
+
import { logHmrSwapMiss } from '@engine/ecs/hmr-swap-report';
|
|
20
|
+
import type { GameSession } from '@engine/runtime/create-runtime';
|
|
21
|
+
import { isFirstPartyMounted, type WorldInstance } from '@engine/runtime/game';
|
|
22
|
+
import { parseSceneFile, SceneParseError } from '@engine/scene/parse';
|
|
23
|
+
import type { Overlay2D } from '@engine/world2d/authoring-2d';
|
|
24
|
+
import { createPhysics2DRegistry } from '@engine/world2d/physics2d-registry';
|
|
25
|
+
import { createPhysicsAdapter2D } from '@engine/world2d/system-adapters-2d';
|
|
26
|
+
import gsap from 'gsap';
|
|
27
|
+
import * as THREE from 'three';
|
|
28
|
+
import { resolveAllWorldEntries, resolveWorldAdapter } from './adapter-resolver';
|
|
29
|
+
import {
|
|
30
|
+
getActiveAuthoring,
|
|
31
|
+
getAuthoringOverride,
|
|
32
|
+
setActiveAuthoring,
|
|
33
|
+
} from './authoring/active-adapter';
|
|
34
|
+
import { setActiveSystems } from './authoring/active-systems';
|
|
35
|
+
import {
|
|
36
|
+
CompositeAuthoringAdapter,
|
|
37
|
+
type CompositeChild,
|
|
38
|
+
} from './authoring/composite-authoring-adapter';
|
|
39
|
+
import { createEphemeralPersistence } from './authoring/ephemeral-persistence';
|
|
40
|
+
import { IngestAuthoringAdapter } from './authoring/ingest-authoring-adapter';
|
|
41
|
+
import type { IngestOverlay } from './authoring/ingest-overlay';
|
|
42
|
+
import { ReactWorldAuthoringAdapter } from './authoring/react-world-authoring-adapter';
|
|
43
|
+
import { type UndoAdapter, unregisterUndoAdapter } from './authoring/undo-timeline';
|
|
44
|
+
import { loadWorldOverlay, saveWorldOverlay, worldOverlayPath } from './authoring/world-overlay';
|
|
45
|
+
import { World2DAuthoringAdapter } from './authoring/world2d-authoring-adapter';
|
|
46
|
+
import { importFromSource, registerRuntimeModules } from './browser-transpile';
|
|
47
|
+
import { watchScriptChanges } from './browser-watch-reload';
|
|
48
|
+
import type { LogEntry } from './editor-api';
|
|
49
|
+
import { endLogSession, flushLogEntries, startLogSession } from './editor-api';
|
|
50
|
+
import type { ConsoleEntry } from './editor-console';
|
|
51
|
+
import { editorConsole } from './editor-console';
|
|
52
|
+
import { getEditorMode } from './editor-mode';
|
|
53
|
+
import type { EditorStore } from './editor-store';
|
|
54
|
+
import { installGatedGameGlobals, setGameInputGate } from './gated-globals';
|
|
55
|
+
import { exampleDispatchFor } from './hosted-example';
|
|
56
|
+
import { fetchGameManifest } from './manifest-project';
|
|
57
|
+
import { getCurrentProject } from './project-manager';
|
|
58
|
+
import { loadProjectScripts, type ProjectScripts, reloadProjectScripts } from './script-loader';
|
|
59
|
+
import { createHttpSourceWriteBackend } from './ui-source/source-write-backend';
|
|
60
|
+
|
|
61
|
+
/** Context needed by the orchestrator (passed from ViewportPanel). */
|
|
62
|
+
export interface PlayModeContext {
|
|
63
|
+
store: EditorStore;
|
|
64
|
+
/** Container for the game canvas (the Game tab panel). */
|
|
65
|
+
gameContainer: HTMLElement;
|
|
66
|
+
/** Returns the editor viewport camera (for copying viewpoint into game). */
|
|
67
|
+
getViewportCamera: () => THREE.PerspectiveCamera;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let _gameContainerEl: HTMLElement | null = null;
|
|
71
|
+
let _ctx: PlayModeContext | null = null;
|
|
72
|
+
let _session: GameSession | null = null;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Play-mode generation counter. Bumped on every enterPlayMode and every
|
|
76
|
+
* exitPlayMode. enterPlayMode captures the value before its async boot and
|
|
77
|
+
* re-checks it around `createGameRuntime` — if exitPlayMode (Stop/Escape) ran
|
|
78
|
+
* while the runtime was still booting, the freshly-created session belongs to
|
|
79
|
+
* an already-exited play and must be stopped and abandoned, NOT adopted.
|
|
80
|
+
* Without this, a stop-during-boot left the store swapped onto the orphaned
|
|
81
|
+
* live game scene (empty hierarchy in edit mode) with the session leaked
|
|
82
|
+
* (RAF loop, Rapier world, WebGL context never released).
|
|
83
|
+
*/
|
|
84
|
+
let _playEpoch = 0;
|
|
85
|
+
|
|
86
|
+
// T6.3: install the gated window/document proxies once so the dev server's/
|
|
87
|
+
// browser-transpile's GAME_GLOBALS_PRELUDE (prepended to project modules) has
|
|
88
|
+
// something to resolve to. No-op outside a browser (headless unit tests).
|
|
89
|
+
installGatedGameGlobals();
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The first-party game runtime behind the current session. HMR (component
|
|
93
|
+
* hot-swap, warm restart) and physics edit-sync are inherently first-party, so
|
|
94
|
+
* play-mode reaches the `GameContext` + `hotReload` via the `VgaiMountedGame`
|
|
95
|
+
* extras on `session.mounted`. The generic host (`createGameRuntime`) no longer
|
|
96
|
+
* exposes these — that confinement is the adapter inversion.
|
|
97
|
+
*/
|
|
98
|
+
/** e2e counter: proofs read `window.__vgaiHmrCount` to assert HMR fired. */
|
|
99
|
+
function bumpHmrCount(): void {
|
|
100
|
+
const w = window as unknown as { __vgaiHmrCount?: number };
|
|
101
|
+
w.__vgaiHmrCount = (w.__vgaiHmrCount ?? 0) + 1;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function firstParty(session: GameSession): VgaiMountedGame {
|
|
105
|
+
return session.mounted as VgaiMountedGame;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Fan HMR's `hotSwap` out across EVERY first-party world in the session
|
|
110
|
+
* (§7.1-4, probes 2/2b) — both call sites below used to reach only
|
|
111
|
+
* `firstParty(_session).ctx.components.hotSwap(...)`, i.e. the DEFAULT
|
|
112
|
+
* world's `ComponentManager` alone. In a multi-world session (T6.1) a
|
|
113
|
+
* component living in a second/third world never saw a hot update at all
|
|
114
|
+
* (probe2), and — worse — a component living ONLY in a non-default world
|
|
115
|
+
* produced a misleading "no live instance" swap-miss warning (probe2b) even
|
|
116
|
+
* though the instance was alive the whole time, just in a manager this call
|
|
117
|
+
* never consulted; structurally identical to "you renamed the class."
|
|
118
|
+
*
|
|
119
|
+
* Iterates `session.game.worlds` (declaration order), skipping any
|
|
120
|
+
* non-first-party mount (`isFirstPartyMounted` — an opaque/foreign or react
|
|
121
|
+
* mount has no `ComponentManager` to hotSwap against), and calls each
|
|
122
|
+
* first-party world's OWN `hotSwap` with its own swap-miss warning
|
|
123
|
+
* suppressed (`{ warnOnMiss: false }`) — a per-world zero-match is
|
|
124
|
+
* meaningless on its own (the name may simply live in a DIFFERENT world).
|
|
125
|
+
* The ONE real "nothing anywhere matched this name" warning is emitted HERE,
|
|
126
|
+
* exactly once, with the SAME message convention every other swap-miss uses
|
|
127
|
+
* (`logHmrSwapMiss`), only when the total across every first-party world is
|
|
128
|
+
* zero.
|
|
129
|
+
*
|
|
130
|
+
* Returns the total number of live instances swapped, summed across worlds.
|
|
131
|
+
*/
|
|
132
|
+
export function hotSwapAcrossWorlds(
|
|
133
|
+
session: GameSession,
|
|
134
|
+
name: string,
|
|
135
|
+
NewClass: GameComponentClass,
|
|
136
|
+
): number {
|
|
137
|
+
let total = 0;
|
|
138
|
+
for (const world of session.game.worlds) {
|
|
139
|
+
if (!isFirstPartyMounted(world.mounted)) continue;
|
|
140
|
+
total += world.mounted.ctx.components.hotSwap(name, NewClass, { warnOnMiss: false });
|
|
141
|
+
}
|
|
142
|
+
if (total === 0) {
|
|
143
|
+
logHmrSwapMiss({ key: name, reason: 'no-live-instance' });
|
|
144
|
+
}
|
|
145
|
+
return total;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Warm-restart's fan-out investigation (§7.1-4's second half): the
|
|
150
|
+
* non-component-edit HMR branch below re-runs ONLY the default world's
|
|
151
|
+
* `hotReload` — it does NOT fan out across `session.game.worlds` the way
|
|
152
|
+
* {@link hotSwapAcrossWorlds} does for true-HMR component swaps. This is a
|
|
153
|
+
* STRUCTURAL limitation, not an oversight left for later:
|
|
154
|
+
*
|
|
155
|
+
* 1. `hotReload` is a `VgaiMountedGame` (first-party THREEJS mount) extra —
|
|
156
|
+
* `world2d/pixi-game-adapter.ts`'s pixi mount and `create-runtime.ts`'s
|
|
157
|
+
* react mount expose no such method at all, so there is no capability to
|
|
158
|
+
* fan out to on a non-threejs world even in principle.
|
|
159
|
+
* 2. Even restricted to first-party THREEJS worlds, `reloadProjectScripts`
|
|
160
|
+
* (`script-loader.ts`) resolves exactly ONE project entry
|
|
161
|
+
* (`src/scripts/main.ts` by default) and returns ONE `setup` function —
|
|
162
|
+
* it has no per-world mapping, so there is no way to know which world a
|
|
163
|
+
* second/third `hotReload` call should even run against, let alone which
|
|
164
|
+
* source file to re-import for it.
|
|
165
|
+
*
|
|
166
|
+
* So this reports the limitation loudly instead of silently reloading only
|
|
167
|
+
* part of a multi-world session — called right after a successful warm
|
|
168
|
+
* restart, from the `vgai:script-update` handler below.
|
|
169
|
+
*/
|
|
170
|
+
export function warnIfOtherFirstPartyWorldsUnrestarted(session: GameSession): void {
|
|
171
|
+
const worlds = session.game?.worlds;
|
|
172
|
+
if (!worlds || worlds.length <= 1) return;
|
|
173
|
+
const defaultWorldId = session.game?.defaultWorld.id;
|
|
174
|
+
const otherFirstParty = worlds.some(
|
|
175
|
+
(w) => w.id !== defaultWorldId && isFirstPartyMounted(w.mounted),
|
|
176
|
+
);
|
|
177
|
+
if (otherFirstParty) {
|
|
178
|
+
editorConsole.warn(
|
|
179
|
+
'Warm restart applied to the default world only — components in other worlds keep ' +
|
|
180
|
+
'running old code until play restarts (structural limitation: hotReload is a first-party ' +
|
|
181
|
+
'threejs-mount-only capability, and reloadProjectScripts resolves a single ' +
|
|
182
|
+
'src/scripts/main.ts entry with no per-world mapping).',
|
|
183
|
+
'play-mode',
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The warm-restart HMR bracket's pause/resume wrapper (§7.1 item 1 /
|
|
190
|
+
* probe3-warm-restart-unpauses: the bracket used to call
|
|
191
|
+
* `_session.pause(); await hotReload(...); _session.resume();`
|
|
192
|
+
* unconditionally, so it silently un-paused a user-paused game — the UI kept
|
|
193
|
+
* saying "paused" while the simulation resumed running). Fix: capture
|
|
194
|
+
* whether the game was ALREADY paused from the SAME source of truth
|
|
195
|
+
* `pause()`/`resume()` drive (`session.game.play.paused`, `game.ts:713`)
|
|
196
|
+
* BEFORE unconditionally pausing for the reload, and only resume afterward
|
|
197
|
+
* if it was not already paused — `pause()`/`resume()` are idempotent, so an
|
|
198
|
+
* unconditional `pause()` up front is always safe. try/finally so a throw
|
|
199
|
+
* mid-`reload()` still restores the correct pre-bracket state (a throw must
|
|
200
|
+
* never leave a previously-RUNNING game stuck paused). Exported for direct
|
|
201
|
+
* unit testing without any `import.meta.hot`/Vite HMR event machinery — see
|
|
202
|
+
* `packages/editor/test/play-mode-warm-restart-pause.test.ts`.
|
|
203
|
+
*/
|
|
204
|
+
export async function runWarmRestartPauseBracket(
|
|
205
|
+
session: GameSession,
|
|
206
|
+
reload: () => Promise<void>,
|
|
207
|
+
): Promise<void> {
|
|
208
|
+
const wasPaused = session.game.play.paused;
|
|
209
|
+
session.pause();
|
|
210
|
+
try {
|
|
211
|
+
await reload();
|
|
212
|
+
} finally {
|
|
213
|
+
if (!wasPaused) session.resume();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
let _gameCanvas: HTMLCanvasElement | null = null;
|
|
217
|
+
let _resizeObserver: ResizeObserver | null = null;
|
|
218
|
+
let _unsubStore: (() => void) | null = null;
|
|
219
|
+
/** Browser-mode component-source watch (Phase A2); null in server mode / stopped. */
|
|
220
|
+
let _unsubBrowserScriptWatch: (() => void) | null = null;
|
|
221
|
+
/** True for the CURRENT session iff it mounted through the worlds path with
|
|
222
|
+
* more than one world — decides which of `installPlayAuthoring` (ephemeral,
|
|
223
|
+
* single `.vscn` preview) vs `installMultiWorldAuthoring` (real per-world
|
|
224
|
+
* overlay persistence) ran, so `exitPlayMode` tears down the right one. */
|
|
225
|
+
let _isMultiWorldSession = false;
|
|
226
|
+
/**
|
|
227
|
+
* The authoring override that was active before THIS play session installed its
|
|
228
|
+
* own (normally `null` — nothing else authors while playing today, but this
|
|
229
|
+
* restores whatever was there rather than assuming null; see
|
|
230
|
+
* `installPlayAuthoring`/`restorePriorAuthoring`). `undefined` ⇒ this play
|
|
231
|
+
* session never installed one (e.g. it bailed before adopting the scene) — exit
|
|
232
|
+
* must then leave the authoring override untouched.
|
|
233
|
+
*/
|
|
234
|
+
let _priorAuthoringOverride: AuthoringAdapter | null | undefined;
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Play adoption drives the SAME authoring adapter the editor already uses for
|
|
238
|
+
* `store` (first-party today — `getActiveAuthoring` falls back to it absent an
|
|
239
|
+
* override), just with its `persistence` swapped for the ephemeral provider
|
|
240
|
+
* (design `docs/PERSISTENCE-PROVIDER-DESIGN.md` §2/§7 slice 1): play-time edits
|
|
241
|
+
* apply to the live game scene exactly as before, but the store's autosave/
|
|
242
|
+
* saveNow guards now see `isDirty() → false` / no real destination instead of
|
|
243
|
+
* relying on the `_playState` flag alone. `undo`/`redo`/`subscribe` are rebound
|
|
244
|
+
* explicitly (a plain spread would keep them pointing at `base`'s `this`, which
|
|
245
|
+
* is fine for arrow-function fields but not for prototype methods).
|
|
246
|
+
*/
|
|
247
|
+
function installPlayAuthoring(store: EditorStore): void {
|
|
248
|
+
_priorAuthoringOverride = getAuthoringOverride();
|
|
249
|
+
const base = getActiveAuthoring(store);
|
|
250
|
+
const wrapped: AuthoringAdapter = {
|
|
251
|
+
...base,
|
|
252
|
+
persistence: createEphemeralPersistence(() => store.getDocument()),
|
|
253
|
+
};
|
|
254
|
+
// Only rebind when `base` actually implements these (exactOptionalPropertyTypes
|
|
255
|
+
// rejects assigning `undefined` to an already-optional property explicitly).
|
|
256
|
+
if (base.subscribe) wrapped.subscribe = base.subscribe.bind(base);
|
|
257
|
+
if (base.undo) wrapped.undo = base.undo.bind(base);
|
|
258
|
+
if (base.redo) wrapped.redo = base.redo.bind(base);
|
|
259
|
+
setActiveAuthoring(wrapped);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Restore whatever authoring override (if any) was active before play started. */
|
|
263
|
+
function restorePriorAuthoring(): void {
|
|
264
|
+
if (_priorAuthoringOverride === undefined) return; // installPlayAuthoring never ran
|
|
265
|
+
setActiveAuthoring(_priorAuthoringOverride);
|
|
266
|
+
_priorAuthoringOverride = undefined;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Per-world adapters built for the CURRENT multi-world session, tracked ONLY so
|
|
271
|
+
* `exitMultiWorldAuthoring` can unregister any that self-registered with the
|
|
272
|
+
* global undo timeline. As of v4 §7.1-13, `IngestAuthoringAdapter` AND
|
|
273
|
+
* `World2DAuthoringAdapter` both self-register (the `ReactWorldAuthoringAdapter`
|
|
274
|
+
* branch does too) — mirrors `ingest-mode.ts`'s teardown of its own single
|
|
275
|
+
* adapter.
|
|
276
|
+
*/
|
|
277
|
+
let _multiWorldChildren: AuthoringAdapter[] = [];
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Build ONE {@link AuthoringAdapter} per mounted world (T6.1 slice 3,
|
|
281
|
+
* docs/MULTI-WORLD-DESIGN.md §1.F/§3 slice 3) and install the N-ary
|
|
282
|
+
* {@link CompositeAuthoringAdapter} over all of them as the active authoring
|
|
283
|
+
* override.
|
|
284
|
+
*
|
|
285
|
+
* Study finding (this slice): `resolveAllWorlds`'s own doc comment records that
|
|
286
|
+
* NO world in a multi-world manifest gets the live editor document/editorPreview
|
|
287
|
+
* — every world mounts from its own on-disk `scene`/`entry`, single-world OR
|
|
288
|
+
* not. So there is no "the store's document IS this world's live scene" world
|
|
289
|
+
* the way `VgaiSceneAuthoringAdapter` assumes for the single-world path — EVERY
|
|
290
|
+
* world here (including a `kind:'threejs'` world) gets a live-tree REFLECTION
|
|
291
|
+
* adapter, the same shape `ingest-mode.ts` already uses for an unmodified game's
|
|
292
|
+
* `Object3D` tree: `IngestAuthoringAdapter` for a threejs world's `threeRoot()`,
|
|
293
|
+
* `World2DAuthoringAdapter` for a pixijs world's `pixiRoot()`. Persistence is
|
|
294
|
+
* REAL (not the ephemeral discard-on-stop wrapper `installPlayAuthoring` uses
|
|
295
|
+
* for the single-world `.vscn` preview path) — each per-world adapter's overlay
|
|
296
|
+
* (§1.G: `.vgai/overlays/<world-id>.json`, project-root-relative, `authoring/
|
|
297
|
+
* world-overlay.ts`) is the honest floor a manifest-mounted world has today,
|
|
298
|
+
* exactly like `ingest-mode.ts`'s own overlay persistence (both its three-side
|
|
299
|
+
* and — T6.1 slice 4 — pixi-side ingest sessions) —
|
|
300
|
+
* this is not a "preview of an editable document" session the way single-world
|
|
301
|
+
* play is, so ephemeral-wrapping it would silently discard the ONLY persistence
|
|
302
|
+
* mechanism these worlds have.
|
|
303
|
+
*
|
|
304
|
+
* `store` is used ONLY as the editor-global selection bag + change-notification
|
|
305
|
+
* bus (`selectedEntityIds`/`selectMultiple`/`subscribe`/`notifyIngestEdit`) —
|
|
306
|
+
* neither per-world adapter reads/writes `store`'s document, matching how
|
|
307
|
+
* `ingest-mode.ts` already uses the SAME store for the same narrow purpose
|
|
308
|
+
* over a live foreign tree.
|
|
309
|
+
*
|
|
310
|
+
* A `kind: 'react'` world (T6.2 slice 2, `docs/REACT-WORLD-DESIGN.md` §1.D/§3
|
|
311
|
+
* slice 2) gets a {@link ReactWorldAuthoringAdapter} over its `reactRoot()` DOM —
|
|
312
|
+
* NOT the overlay-file persistence the threejs/pixijs branches use: a react
|
|
313
|
+
* world's truth is JSX SOURCE (D4), so its persistence is the SAME immediate
|
|
314
|
+
* source-write seam `UIAuthoringAdapter` already uses
|
|
315
|
+
* (`../ui-source/source-write-backend.ts`), wired only under the dev server
|
|
316
|
+
* (`import.meta.env.DEV`) exactly like `edit-mode-authoring.ts`'s scene-UI
|
|
317
|
+
* child does — a hosted/no-dev-server session degrades to selection/
|
|
318
|
+
* inspection only, writes reporting unavailable (never silently faked).
|
|
319
|
+
*/
|
|
320
|
+
async function installMultiWorldAuthoring(
|
|
321
|
+
store: EditorStore,
|
|
322
|
+
worlds: readonly WorldInstance[],
|
|
323
|
+
): Promise<void> {
|
|
324
|
+
_priorAuthoringOverride = getAuthoringOverride();
|
|
325
|
+
const children: CompositeChild[] = [];
|
|
326
|
+
for (const world of worlds) {
|
|
327
|
+
if (world.kind === 'threejs') {
|
|
328
|
+
const overlay = await loadWorldOverlay<IngestOverlay>(world.id);
|
|
329
|
+
const adapter = new IngestAuthoringAdapter(store, world.threeRoot(), {
|
|
330
|
+
overlay,
|
|
331
|
+
persist: (o) => saveWorldOverlay(world.id, o),
|
|
332
|
+
destination: worldOverlayPath(world.id),
|
|
333
|
+
});
|
|
334
|
+
children.push({ worldId: world.id, kind: world.kind, adapter });
|
|
335
|
+
} else if (world.kind === 'pixijs') {
|
|
336
|
+
const overlay = await loadWorldOverlay<Overlay2D>(world.id);
|
|
337
|
+
const physics = createPhysicsAdapter2D(world.physics2d ?? createPhysics2DRegistry());
|
|
338
|
+
const adapter = new World2DAuthoringAdapter(world.pixiRoot(), physics, store, {
|
|
339
|
+
overlay,
|
|
340
|
+
persist: (o) => saveWorldOverlay(world.id, o),
|
|
341
|
+
destination: worldOverlayPath(world.id),
|
|
342
|
+
});
|
|
343
|
+
children.push({ worldId: world.id, kind: world.kind, adapter });
|
|
344
|
+
} else if (world.kind === 'react') {
|
|
345
|
+
const adapter = new ReactWorldAuthoringAdapter(world.reactRoot(), store, {
|
|
346
|
+
// Same dev-server-only gating `edit-mode-authoring.ts`'s scene-UI
|
|
347
|
+
// child uses for the identical seam: a hosted/production build has
|
|
348
|
+
// no `/__ui-source` middleware.
|
|
349
|
+
...(import.meta.env.DEV ? { writeBackend: createHttpSourceWriteBackend() } : {}),
|
|
350
|
+
});
|
|
351
|
+
children.push({ worldId: world.id, kind: world.kind, adapter });
|
|
352
|
+
} else {
|
|
353
|
+
// Exhaustiveness guard (§7.4-2): the pre-existing if/else-if chain over
|
|
354
|
+
// `WorldInstance.kind` had no trailing else — a hypothetical 4th kind
|
|
355
|
+
// would silently get NO authoring child (no error, just missing
|
|
356
|
+
// authoring for that world) rather than failing loudly/at compile time.
|
|
357
|
+
assertNever(world.kind, 'installMultiWorldAuthoring');
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
_multiWorldChildren = children.map((c) => c.adapter);
|
|
361
|
+
const composite = new CompositeAuthoringAdapter(children);
|
|
362
|
+
setActiveAuthoring(composite);
|
|
363
|
+
// `setActiveAuthoring` is a plain module-level variable, not React state —
|
|
364
|
+
// the left hierarchy panel only re-checks `hasAuthoringOverride()` when the
|
|
365
|
+
// store notifies (same reason `ingest-mode.ts` calls this right after
|
|
366
|
+
// installing its own override).
|
|
367
|
+
store.notifyIngestEdit();
|
|
368
|
+
|
|
369
|
+
// Dev/e2e diagnostic handle — the SAME pattern `ingest-mode.ts`'s
|
|
370
|
+
// `window.__vgaiIngest`/`window.__vgaiIngest2D` use:
|
|
371
|
+
// `store.saveNow()` (Ctrl/Cmd+S) and `_autoSave()` are both structurally
|
|
372
|
+
// guarded OFF while ANY authoring override is active (`hasAuthoringOverride()`
|
|
373
|
+
// / the play-state check) — the adapter's OWN `persistence.save()` is the
|
|
374
|
+
// only way an override session's edits reach disk, so tests/tooling need a
|
|
375
|
+
// handle to call it directly, exactly as the ingest sessions expose.
|
|
376
|
+
if (import.meta.env.DEV) {
|
|
377
|
+
(window as unknown as Record<string, unknown>)['__vgaiMultiWorld'] = {
|
|
378
|
+
adapter: composite,
|
|
379
|
+
worldIds: children.map((c) => c.worldId),
|
|
380
|
+
store,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** Teardown for `installMultiWorldAuthoring` — unregisters any per-world child
|
|
386
|
+
* that self-registered with the global undo timeline (mirrors
|
|
387
|
+
* `ingest-mode.ts`'s `unregisterUndoAdapter` call on its own adapter). Must
|
|
388
|
+
* pass the ORIGINAL adapter instance (not a rebuilt wrapper) — the timeline
|
|
389
|
+
* keys registration by object identity (see undo-timeline.ts's doc comment),
|
|
390
|
+
* and `registerUndoAdapter(this)` inside each of `IngestAuthoringAdapter`'s,
|
|
391
|
+
* `World2DAuthoringAdapter`'s (v4 §7.1-13), and `ReactWorldAuthoringAdapter`'s
|
|
392
|
+
* constructors registered exactly that instance. The type guard below is now
|
|
393
|
+
* just a defensive narrowing (every current child kind DOES register), kept
|
|
394
|
+
* in case a future child kind legitimately has no undo/redo of its own. */
|
|
395
|
+
function exitMultiWorldAuthoring(): void {
|
|
396
|
+
for (const adapter of _multiWorldChildren) {
|
|
397
|
+
if (adapter.undo && adapter.redo)
|
|
398
|
+
unregisterUndoAdapter(adapter as AuthoringAdapter & UndoAdapter);
|
|
399
|
+
}
|
|
400
|
+
_multiWorldChildren = [];
|
|
401
|
+
if (import.meta.env.DEV) {
|
|
402
|
+
delete (window as unknown as Record<string, unknown>)['__vgaiMultiWorld'];
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
let _escapeListener: ((e: KeyboardEvent) => void) | null = null;
|
|
406
|
+
let _originalConsoleLog: typeof console.log | null = null;
|
|
407
|
+
let _originalConsoleInfo: typeof console.info | null = null;
|
|
408
|
+
let _originalConsoleWarn: typeof console.warn | null = null;
|
|
409
|
+
let _originalConsoleError: typeof console.error | null = null;
|
|
410
|
+
let _logFlushChain: Promise<unknown> = Promise.resolve();
|
|
411
|
+
let _flushInterval: ReturnType<typeof setInterval> | null = null;
|
|
412
|
+
let _pendingEntries: LogEntry[] = [];
|
|
413
|
+
|
|
414
|
+
// --- Script HMR ---
|
|
415
|
+
if (import.meta.hot) {
|
|
416
|
+
import.meta.hot.on('vgai:script-update', async (data: { file: string }) => {
|
|
417
|
+
if (!_session || !_ctx) return;
|
|
418
|
+
const project = getCurrentProject();
|
|
419
|
+
if (!project) return;
|
|
420
|
+
try {
|
|
421
|
+
const { store } = _ctx;
|
|
422
|
+
|
|
423
|
+
// Strategy based on which file changed:
|
|
424
|
+
// Component source files → true HMR (import only the file, swap prototypes)
|
|
425
|
+
// Entry point or registry → warm restart (re-import main, re-run setup)
|
|
426
|
+
const isComponentFile =
|
|
427
|
+
!data.file.endsWith('/main.ts') && !data.file.endsWith('/registry.ts');
|
|
428
|
+
|
|
429
|
+
if (isComponentFile) {
|
|
430
|
+
// True HMR: import only the changed file to get fresh class definitions.
|
|
431
|
+
// This avoids re-importing the full module chain which can trigger page reloads.
|
|
432
|
+
const modulePath = `/@fs/${data.file}?t=${Date.now()}`;
|
|
433
|
+
const mod = await import(/* @vite-ignore */ modulePath);
|
|
434
|
+
for (const [key, value] of Object.entries(mod)) {
|
|
435
|
+
// §7.1-5/probe7: filter to GameComponent subclasses BEFORE ever
|
|
436
|
+
// calling hotSwap — a plain helper function export sharing a live
|
|
437
|
+
// component's registry key/name must never reach
|
|
438
|
+
// `Object.setPrototypeOf`. `gameComponentModule.GameComponent` is
|
|
439
|
+
// the SAME singleton class the running session's components were
|
|
440
|
+
// built from — Vite's dev-server module graph resolves
|
|
441
|
+
// `@engine/ecs/game-component` to one cached instance for both this
|
|
442
|
+
// file and the `/@fs/`-imported component file (the cache-busting
|
|
443
|
+
// `?t=` query targets ONLY the edited file's own module id, not its
|
|
444
|
+
// import specifiers), so `instanceof` here holds across the two
|
|
445
|
+
// import paths. See `applyBrowserComponentEdit` below for the
|
|
446
|
+
// in-browser (esbuild-wasm) path's identity guarantee, which is
|
|
447
|
+
// explicit-by-construction via `registerRuntimeModules`.
|
|
448
|
+
if (
|
|
449
|
+
typeof value === 'function' &&
|
|
450
|
+
value.prototype instanceof gameComponentModule.GameComponent
|
|
451
|
+
) {
|
|
452
|
+
hotSwapAcrossWorlds(_session, key, value as unknown as GameComponentClass);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
bumpHmrCount();
|
|
456
|
+
editorConsole.log('Hot-swapped component classes (true HMR)', 'play-mode');
|
|
457
|
+
} else {
|
|
458
|
+
// Warm restart: re-run setup() with fresh code
|
|
459
|
+
const scripts = await reloadProjectScripts(project.rootPath);
|
|
460
|
+
// W1 (docs/R3F-FIRST-PARTY-DESIGN.md §1.A) widened `ProjectScripts` so
|
|
461
|
+
// `setup` is no longer guaranteed — an entry that exports only
|
|
462
|
+
// `adapter` has no re-runnable setup() for `hotReload` to call (the
|
|
463
|
+
// adapter owns its whole mount lifecycle itself; O3 in the design doc
|
|
464
|
+
// already flags entry-module HMR as unverified for v1). Fail loudly
|
|
465
|
+
// here instead of handing `hotReload` an `undefined` — the outer
|
|
466
|
+
// try/catch (below) reports it exactly like any other HMR failure.
|
|
467
|
+
const reloadedSetup = scripts.setup;
|
|
468
|
+
if (!reloadedSetup) {
|
|
469
|
+
throw new Error(
|
|
470
|
+
'Warm restart failed: the project entry now exports only `adapter`, not `setup` — ' +
|
|
471
|
+
'hot-reloading a pre-built GameAdapter has no setup() to re-run in v1 ' +
|
|
472
|
+
'(docs/R3F-FIRST-PARTY-DESIGN.md O3). Restart play instead.',
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
const hotViewCam = _ctx.getViewportCamera();
|
|
476
|
+
// Pause the host loop around the warm restart (the generic host no longer
|
|
477
|
+
// owns hotReload; gate the loop so it doesn't tick mid-rebuild) — but never
|
|
478
|
+
// un-pause a game the USER had already paused (§7.1-1, probe3).
|
|
479
|
+
await runWarmRestartPauseBracket(_session, () =>
|
|
480
|
+
firstParty(_session as GameSession).hotReload(reloadedSetup, {
|
|
481
|
+
scenePath: store.savePath,
|
|
482
|
+
sceneData: store.getDocument(),
|
|
483
|
+
viewportCamera: {
|
|
484
|
+
position: hotViewCam.position.toArray() as [number, number, number],
|
|
485
|
+
quaternion: hotViewCam.quaternion.toArray() as [number, number, number, number],
|
|
486
|
+
},
|
|
487
|
+
}),
|
|
488
|
+
);
|
|
489
|
+
bumpHmrCount();
|
|
490
|
+
editorConsole.log('Hot-reloaded game scripts (warm restart)', 'play-mode');
|
|
491
|
+
warnIfOtherFirstPartyWorldsUnrestarted(_session);
|
|
492
|
+
}
|
|
493
|
+
} catch (err) {
|
|
494
|
+
editorConsole.error(`Script hot-reload failed: ${err}`, 'play-mode');
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// --- In-browser HMR (no server) ---
|
|
500
|
+
//
|
|
501
|
+
// The `vgai:script-update` handler above relies on the Vite dev server to
|
|
502
|
+
// transpile the changed file and on a filesystem watcher to fire the event.
|
|
503
|
+
// The path below does the same live prototype-swap, but the source comes from
|
|
504
|
+
// the browser and is transpiled in-browser via esbuild-wasm — no Node, no Vite
|
|
505
|
+
// transpile, no file write. This is what makes hot reload possible entirely
|
|
506
|
+
// client-side. The transpiled component shares the running session's THREE and
|
|
507
|
+
// GameComponent singletons (registered below), so prototype chains stay valid.
|
|
508
|
+
|
|
509
|
+
let _runtimeModulesRegistered = false;
|
|
510
|
+
function ensureRuntimeModulesRegistered(): void {
|
|
511
|
+
if (_runtimeModulesRegistered) return;
|
|
512
|
+
registerRuntimeModules({
|
|
513
|
+
three: THREE as unknown as Record<string, unknown>,
|
|
514
|
+
gsap: { default: gsap } as Record<string, unknown>,
|
|
515
|
+
'@engine/ecs/game-component': gameComponentModule as unknown as Record<string, unknown>,
|
|
516
|
+
});
|
|
517
|
+
_runtimeModulesRegistered = true;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Apply an in-browser source edit to the running game: transpile the given
|
|
522
|
+
* component module source with esbuild-wasm and hot-swap every exported class
|
|
523
|
+
* onto its live instances. Returns the names of the classes that were swapped.
|
|
524
|
+
*
|
|
525
|
+
* This is the in-browser analogue of the `vgai:script-update` HMR handler —
|
|
526
|
+
* same engine `hotSwap`, but driven entirely client-side.
|
|
527
|
+
*/
|
|
528
|
+
export async function applyBrowserComponentEdit(source: string): Promise<string[]> {
|
|
529
|
+
if (!_session) throw new Error('Not in play mode — enter play mode before editing components.');
|
|
530
|
+
ensureRuntimeModulesRegistered();
|
|
531
|
+
const mod = await importFromSource(source);
|
|
532
|
+
const swapped: string[] = [];
|
|
533
|
+
for (const [name, value] of Object.entries(mod)) {
|
|
534
|
+
// §7.1-5/probe7: same GameComponent-subclass filter as the vgai:script-update
|
|
535
|
+
// handler above — identity here is explicit-by-construction (not merely
|
|
536
|
+
// module-graph coincidence): `ensureRuntimeModulesRegistered` registers
|
|
537
|
+
// THIS session's live `gameComponentModule` singleton via
|
|
538
|
+
// `registerRuntimeModules`, and `importFromSource`'s esbuild-wasm output
|
|
539
|
+
// has its bare `@engine/ecs/game-component` import rewritten to a blob-URL
|
|
540
|
+
// shim that re-exports exactly that registered namespace — so a
|
|
541
|
+
// transpiled component's `class Foo extends GameComponent` extends the
|
|
542
|
+
// SAME class object checked here.
|
|
543
|
+
if (
|
|
544
|
+
typeof value === 'function' &&
|
|
545
|
+
value.prototype instanceof gameComponentModule.GameComponent
|
|
546
|
+
) {
|
|
547
|
+
hotSwapAcrossWorlds(_session, name, value as GameComponentClass);
|
|
548
|
+
swapped.push(name);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (swapped.length > 0) {
|
|
552
|
+
const w = window as unknown as Record<string, unknown>;
|
|
553
|
+
w['__vgaiHmrCount'] = ((w['__vgaiHmrCount'] as number) ?? 0) + 1;
|
|
554
|
+
editorConsole.log(`In-browser hot-swap: ${swapped.join(', ')}`, 'play-mode');
|
|
555
|
+
}
|
|
556
|
+
return swapped;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Dev-only handle so the editor UI / e2e can drive in-browser component edits.
|
|
560
|
+
if (import.meta.env.DEV) {
|
|
561
|
+
(window as unknown as Record<string, unknown>)['__vgaiBrowserHmr'] = {
|
|
562
|
+
applyBrowserComponentEdit,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** Set the Game panel container element (called by GamePanel on mount). */
|
|
567
|
+
export function setGameContainer(el: HTMLElement | null): void {
|
|
568
|
+
_gameContainerEl = el;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** Get the Game panel container element (shared with ingest-mode). */
|
|
572
|
+
export function getGameContainer(): HTMLElement | null {
|
|
573
|
+
return _gameContainerEl;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** Bind the play-mode orchestrator to editor context. Call once at init. */
|
|
577
|
+
export function bindPlayMode(ctx: Omit<PlayModeContext, 'gameContainer'>): void {
|
|
578
|
+
_ctx = { ...ctx, gameContainer: null! }; // gameContainer set dynamically via setGameContainer
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/** True if play mode is currently active (playing or paused). */
|
|
582
|
+
export function isPlayModeActive(): boolean {
|
|
583
|
+
return _session !== null;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/** Get the running game's audio context (for volume/mute control). */
|
|
587
|
+
export function getGameAudio() {
|
|
588
|
+
return _session ? firstParty(_session).ctx.audio : null;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/** Get the running game's scene (for editor Scene tab rendering). */
|
|
592
|
+
export function getGameScene(): THREE.Scene | null {
|
|
593
|
+
return _session?.scene ?? null;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/** Resize the running game to a specific resolution. */
|
|
597
|
+
export function resizeGame(width: number, height: number): void {
|
|
598
|
+
_session?.resize(width, height);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Enter play mode: create a game canvas, start game, disable editor controls.
|
|
603
|
+
*/
|
|
604
|
+
export async function enterPlayMode(): Promise<void> {
|
|
605
|
+
if (!_ctx) return;
|
|
606
|
+
// The play button is only shown while stopped, so a non-null _session here is
|
|
607
|
+
// stale — a previous play that didn't fully tear down (e.g. a re-entry race).
|
|
608
|
+
// Clean it up so re-entering play mode always works.
|
|
609
|
+
if (_session) exitPlayMode();
|
|
610
|
+
// Capture this play's generation AFTER the stale-session cleanup (which
|
|
611
|
+
// bumps the epoch via exitPlayMode). Any exitPlayMode — or a newer
|
|
612
|
+
// enterPlayMode — during the async boot below invalidates this generation.
|
|
613
|
+
const epoch = ++_playEpoch;
|
|
614
|
+
const { store } = _ctx;
|
|
615
|
+
|
|
616
|
+
// Validate scene before entering play mode
|
|
617
|
+
try {
|
|
618
|
+
parseSceneFile(store.getDocument());
|
|
619
|
+
} catch (err) {
|
|
620
|
+
const msg = err instanceof SceneParseError ? err.message : `Scene validation failed: ${err}`;
|
|
621
|
+
editorConsole.error(msg, 'scene');
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Patch console → editorConsole tagged [game]
|
|
626
|
+
patchConsole();
|
|
627
|
+
|
|
628
|
+
// Start log session and register sink for JSONL persistence
|
|
629
|
+
await startLogSession();
|
|
630
|
+
_pendingEntries = [];
|
|
631
|
+
_logFlushChain = Promise.resolve();
|
|
632
|
+
editorConsole.setSink((entry: ConsoleEntry) => {
|
|
633
|
+
_pendingEntries.push({
|
|
634
|
+
t: entry.timestamp,
|
|
635
|
+
level: entry.level,
|
|
636
|
+
msg: entry.message,
|
|
637
|
+
...(entry.source ? { source: entry.source } : {}),
|
|
638
|
+
...(entry.subsystem ? { sub: entry.subsystem } : {}),
|
|
639
|
+
...(entry.metadata ? { meta: entry.metadata } : {}),
|
|
640
|
+
});
|
|
641
|
+
});
|
|
642
|
+
_flushInterval = setInterval(() => {
|
|
643
|
+
if (_pendingEntries.length > 0) {
|
|
644
|
+
const batch = _pendingEntries.splice(0);
|
|
645
|
+
_logFlushChain = _logFlushChain.then(() => flushLogEntries(batch));
|
|
646
|
+
}
|
|
647
|
+
}, 200);
|
|
648
|
+
|
|
649
|
+
// Register Escape → stop play mode
|
|
650
|
+
_escapeListener = (e: KeyboardEvent) => {
|
|
651
|
+
if (e.key === 'Escape') exitPlayMode();
|
|
652
|
+
};
|
|
653
|
+
window.addEventListener('keydown', _escapeListener);
|
|
654
|
+
|
|
655
|
+
// Create game canvas inside the dedicated game container
|
|
656
|
+
if (!_gameContainerEl) {
|
|
657
|
+
editorConsole.error('Game container not mounted', 'play-mode');
|
|
658
|
+
// Tear down the console patch + flush interval + escape listener set up
|
|
659
|
+
// above, same as the other failure paths (ED8).
|
|
660
|
+
exitPlayMode();
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
const gameContainer = _gameContainerEl;
|
|
664
|
+
const w = gameContainer.clientWidth;
|
|
665
|
+
const h = gameContainer.clientHeight;
|
|
666
|
+
|
|
667
|
+
const gameCanvas = document.createElement('canvas');
|
|
668
|
+
gameCanvas.style.cssText = 'width:100%;height:100%;display:block;';
|
|
669
|
+
gameContainer.appendChild(gameCanvas);
|
|
670
|
+
_gameCanvas = gameCanvas;
|
|
671
|
+
|
|
672
|
+
// setPlayState auto-switches to Game tab
|
|
673
|
+
store.setPlayState('playing');
|
|
674
|
+
editorConsole.log('Play mode started', 'play-mode');
|
|
675
|
+
|
|
676
|
+
try {
|
|
677
|
+
const sceneData = store.getDocument();
|
|
678
|
+
const project = getCurrentProject();
|
|
679
|
+
|
|
680
|
+
if (!project) {
|
|
681
|
+
editorConsole.error('No project open — open or create a project first', 'play-mode');
|
|
682
|
+
exitPlayMode();
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const { createGameRuntime } = await import('@engine/runtime/create-runtime');
|
|
687
|
+
|
|
688
|
+
// T3.3 slice 2 part C: a manifest-backed project (vgai.game.json present)
|
|
689
|
+
// plays through the resolver, against its first world — the manifest
|
|
690
|
+
// wins for game identity/mounting per docs/CLI-ON-FOLDER-DESIGN.md §1B.
|
|
691
|
+
// A legacy (project.json-only) project has no `/vgai.game.json` to fetch
|
|
692
|
+
// (404 -> null below) and falls through to today's scripts-based path,
|
|
693
|
+
// UNCHANGED. Browser mode (the hosted, no-dev-server build) never has
|
|
694
|
+
// this route served at all, so it always takes the legacy branch too.
|
|
695
|
+
const manifest =
|
|
696
|
+
getEditorMode() === 'browser' ? null : await fetchGameManifest().catch(() => null);
|
|
697
|
+
const firstWorld = manifest?.worlds[0];
|
|
698
|
+
|
|
699
|
+
// E4.R2 (reopen fix) — honest scoping of the three branches below,
|
|
700
|
+
// replacing the old blanket "editor-side because Vite-coupled" framing
|
|
701
|
+
// (`mount-manifest.ts`'s header comment, which is true of ADAPTER
|
|
702
|
+
// CONSTRUCTION generally but was being read as though it explained every
|
|
703
|
+
// branch here equally). Each branch has its OWN, different reason to
|
|
704
|
+
// live where it does:
|
|
705
|
+
// - The multi-world branch (below) is the one design/26 S-D D2 actually
|
|
706
|
+
// names ("editor play mode calls it, adapter-resolver keeps only
|
|
707
|
+
// editor-specific concerns: dev-server URLs, ingest session wiring")
|
|
708
|
+
// — it migrated onto the engine's `mountManifestWorlds` composer.
|
|
709
|
+
// - The single-manifest-world `else if (firstWorld)` branch stays HERE,
|
|
710
|
+
// permanently, NOT because of dev-server coupling but because it
|
|
711
|
+
// threads `editorPreview` (the live in-memory document + viewport
|
|
712
|
+
// camera, so unsaved edits play and the editor's own camera seeds the
|
|
713
|
+
// session) — a channel the engine composer must never grow, since it
|
|
714
|
+
// is inherently "there is exactly one editable document" (the
|
|
715
|
+
// editor's), which stops making sense the moment a manifest declares
|
|
716
|
+
// more than one world.
|
|
717
|
+
// - The no-manifest `else` branch (project.json-only legacy projects)
|
|
718
|
+
// stays HERE because there is no manifest world to resolve AT ALL —
|
|
719
|
+
// not a scoping choice, a structural one.
|
|
720
|
+
// - `ingest-mode.ts`/`module-mode.ts` (separate files, not this
|
|
721
|
+
// dispatch) still call `resolveAllWorlds` un-migrated — genuinely OUT
|
|
722
|
+
// of E4's scope (not named by it), left for a follow-on unit; their
|
|
723
|
+
// OWN dev-server `/@fs/` coupling for ingest session wiring and
|
|
724
|
+
// custom-module loading is real and correctly stays editor-side.
|
|
725
|
+
let session: GameSession;
|
|
726
|
+
if (manifest && (manifest.worlds.length > 1 || firstWorld?.kind === 'pixijs')) {
|
|
727
|
+
// T6.1 slice 2 (docs/MULTI-WORLD-DESIGN.md §3 slice 2): a manifest with
|
|
728
|
+
// MORE THAN ONE world mounts through the slice-1 worlds path — the
|
|
729
|
+
// single-canvas legacy path below is three-only (its `createGameRuntime`
|
|
730
|
+
// call constructs exactly one THREE renderer against one canvas) and
|
|
731
|
+
// cannot represent N stacked worlds. A LONE pixijs world routes here
|
|
732
|
+
// too: the legacy path's HostContext is three-shaped (≠ World2DHost),
|
|
733
|
+
// so it could resolve but never mount a pixi adapter — and lone pixi
|
|
734
|
+
// had no working legacy behavior to preserve (it THREW pre-slice), so
|
|
735
|
+
// this is new capability, not a stability break. A single-THREE-world
|
|
736
|
+
// manifest NEVER reaches this branch (the `else if (firstWorld)`
|
|
737
|
+
// branch below is BYTE-STABLE — same call shape as before this slice). The
|
|
738
|
+
// already-created single `gameCanvas` above is unused on this path —
|
|
739
|
+
// the composer's own host builds its OWN canvas stack directly inside
|
|
740
|
+
// `gameContainer` (`mountManifestWorlds`'s `MountManifestOptions
|
|
741
|
+
// .container`), so discard it rather than leave an orphaned extra
|
|
742
|
+
// canvas in the DOM.
|
|
743
|
+
gameCanvas.remove();
|
|
744
|
+
_gameCanvas = null;
|
|
745
|
+
|
|
746
|
+
// E4.R2 (reopen fix, design/26 S-D D2 — "editor play mode calls it,
|
|
747
|
+
// adapter-resolver keeps only editor-specific concerns: dev-server
|
|
748
|
+
// URLs, ingest session wiring"): per-world adapter construction stays
|
|
749
|
+
// HERE, editor-side, via `resolveAllWorldEntries` (which is itself just
|
|
750
|
+
// the scope-checked `resolveWorldAdapter` fan-out — the SAME dev-server
|
|
751
|
+
// `/@fs/` mechanics `resolveWorldAdapter` always used). What moved OFF
|
|
752
|
+
// this file is the WorldMountSpec ASSEMBLY (id/zOrder/pausable/loop +
|
|
753
|
+
// per-kind dispatch) that `resolveAllWorlds` used to hand-build for
|
|
754
|
+
// this call site — `mountManifestWorlds` below now does that from the
|
|
755
|
+
// manifest itself (`mount-manifest.ts`'s `buildWorldMountSpec`, the
|
|
756
|
+
// engine's own single authoritative implementation of that
|
|
757
|
+
// translation), so it is never duplicated here. `entries` (not the
|
|
758
|
+
// process-global `registerWorldKind` registry) is the right channel
|
|
759
|
+
// for an editor session specifically because play mode mounts a FRESH
|
|
760
|
+
// session on every play — registering into the global registry would
|
|
761
|
+
// double-register (and throw) on the second play; `entries` carries no
|
|
762
|
+
// such hazard since it is rebuilt from scratch every `enterPlayMode`.
|
|
763
|
+
const entries = await resolveAllWorldEntries(manifest, project.rootPath);
|
|
764
|
+
editorConsole.log(
|
|
765
|
+
`Resolved ${manifest.worlds.length} manifest worlds via the composer path: ` +
|
|
766
|
+
manifest.worlds.map((w) => `${w.id} (${w.kind})`).join(', '),
|
|
767
|
+
'play-mode',
|
|
768
|
+
);
|
|
769
|
+
// Play may have been exited while resolving — don't boot a runtime for
|
|
770
|
+
// a play that is already over.
|
|
771
|
+
if (epoch !== _playEpoch) return;
|
|
772
|
+
// Re-read the container's real size instead of the `w`/`h` captured
|
|
773
|
+
// BEFORE `store.setPlayState('playing')` above (that capture can race
|
|
774
|
+
// the Game-tab layout switch and read 0x0). This still matters for the
|
|
775
|
+
// INITIAL render-buffer resolution (`mountManifestWorlds`'s own
|
|
776
|
+
// `width`/`height` default otherwise falls back to the manifest's
|
|
777
|
+
// `resolution`) even though it is no longer the ONLY thing standing
|
|
778
|
+
// between a 0x0 mount and a correctly-laid-out canvas: the worlds-path
|
|
779
|
+
// canvas's on-screen SIZE is now fully CSS-container-driven
|
|
780
|
+
// (`create-runtime.ts`'s `mountOneThreeWorld`, E4.R1 reopen fix), not
|
|
781
|
+
// pinned to whatever it mounted at.
|
|
782
|
+
const rw = gameContainer.clientWidth || w;
|
|
783
|
+
const rh = gameContainer.clientHeight || h;
|
|
784
|
+
const { mountManifestWorlds } = await import('@engine/runtime/mount-manifest');
|
|
785
|
+
session = await mountManifestWorlds({
|
|
786
|
+
manifest,
|
|
787
|
+
container: gameContainer,
|
|
788
|
+
entries,
|
|
789
|
+
width: rw,
|
|
790
|
+
height: rh,
|
|
791
|
+
});
|
|
792
|
+
} else if (firstWorld) {
|
|
793
|
+
// The SAME editorPreview the legacy branch builds below — live in-memory
|
|
794
|
+
// document (so unsaved edits play), current save path, viewport-camera
|
|
795
|
+
// spawn. The resolver threads it into whichever default-three branch it
|
|
796
|
+
// takes (entry -> setup(ctx, editorPreview), byte-equivalent to the
|
|
797
|
+
// legacy path; scene-only -> mounts sceneData, camera honored).
|
|
798
|
+
const manifestViewCam = _ctx.getViewportCamera();
|
|
799
|
+
const adapter = await resolveWorldAdapter(firstWorld, project.rootPath, {
|
|
800
|
+
editorPreview: {
|
|
801
|
+
scenePath: store.savePath,
|
|
802
|
+
sceneData,
|
|
803
|
+
viewportCamera: {
|
|
804
|
+
position: manifestViewCam.position.toArray() as [number, number, number],
|
|
805
|
+
quaternion: manifestViewCam.quaternion.toArray() as [number, number, number, number],
|
|
806
|
+
},
|
|
807
|
+
},
|
|
808
|
+
});
|
|
809
|
+
editorConsole.log(
|
|
810
|
+
`Resolved manifest world "${firstWorld.id}" (${firstWorld.adapter.identity})`,
|
|
811
|
+
'play-mode',
|
|
812
|
+
);
|
|
813
|
+
// Play may have been exited while resolving — don't boot a runtime for
|
|
814
|
+
// a play that is already over.
|
|
815
|
+
if (epoch !== _playEpoch) return;
|
|
816
|
+
session = await createGameRuntime({ canvas: gameCanvas, width: w, height: h, adapter });
|
|
817
|
+
} else {
|
|
818
|
+
// Load project scripts and run with createGameRuntime.
|
|
819
|
+
// Server build: dev-server import of the on-disk project entry.
|
|
820
|
+
// Browser build: no dev server — load the pre-bundled scripts and transpile
|
|
821
|
+
// them in-browser (same esbuild-wasm path the Playground uses).
|
|
822
|
+
let scripts: ProjectScripts;
|
|
823
|
+
if (getEditorMode() === 'browser') {
|
|
824
|
+
const { loadBrowserProjectScripts } = await import('./browser-project-scripts');
|
|
825
|
+
scripts = await loadBrowserProjectScripts();
|
|
826
|
+
} else {
|
|
827
|
+
scripts = await loadProjectScripts(project.rootPath);
|
|
828
|
+
}
|
|
829
|
+
editorConsole.log('Loaded project scripts', 'play-mode');
|
|
830
|
+
|
|
831
|
+
const viewCam = _ctx.getViewportCamera();
|
|
832
|
+
// Play may have been exited while the scripts were loading — don't boot a
|
|
833
|
+
// runtime for a play that is already over.
|
|
834
|
+
if (epoch !== _playEpoch) return;
|
|
835
|
+
// Hosted example project (Track H): route play through the bundled
|
|
836
|
+
// example's own setup — or, for a `?scene=` override inside the project,
|
|
837
|
+
// play the open scene generically but keep its asset prefix. Pure
|
|
838
|
+
// decision in hosted-example.ts; empty object everywhere else.
|
|
839
|
+
const exampleDispatch =
|
|
840
|
+
getEditorMode() === 'browser'
|
|
841
|
+
? exampleDispatchFor(
|
|
842
|
+
project.exampleId,
|
|
843
|
+
store.savePath,
|
|
844
|
+
new URLSearchParams(window.location.search).has('scene'),
|
|
845
|
+
)
|
|
846
|
+
: {};
|
|
847
|
+
session = await createGameRuntime({
|
|
848
|
+
canvas: gameCanvas,
|
|
849
|
+
width: w,
|
|
850
|
+
height: h,
|
|
851
|
+
setup: scripts.setup,
|
|
852
|
+
editorPreview: {
|
|
853
|
+
scenePath: store.savePath,
|
|
854
|
+
sceneData,
|
|
855
|
+
viewportCamera: {
|
|
856
|
+
position: viewCam.position.toArray() as [number, number, number],
|
|
857
|
+
quaternion: viewCam.quaternion.toArray() as [number, number, number, number],
|
|
858
|
+
},
|
|
859
|
+
...exampleDispatch,
|
|
860
|
+
},
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Play was exited (Stop/Escape) — or re-entered — while the runtime was
|
|
865
|
+
// booting. This session belongs to a play that is already over: stop it
|
|
866
|
+
// and bail WITHOUT adopting its scene into the store. Adopting here left
|
|
867
|
+
// the editor showing an empty hierarchy in edit mode and leaked the
|
|
868
|
+
// session (RAF loop, Rapier world, WebGL context).
|
|
869
|
+
if (epoch !== _playEpoch) {
|
|
870
|
+
session.stop();
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
_session = session;
|
|
874
|
+
// A2: browser mode has no Vite dev server, so no `vgai:script-update` HMR
|
|
875
|
+
// event ever fires (the `import.meta.hot` handler below is dead in the
|
|
876
|
+
// hosted build). Drive the SAME in-browser hot-swap from the storage
|
|
877
|
+
// backend's self-derived write events instead: any component source written
|
|
878
|
+
// through the backend is transpiled + prototype-swapped onto its live
|
|
879
|
+
// instances. Subscribed here (not at module load) so it binds the backend
|
|
880
|
+
// that is actually active by play time — after any File System Access pick
|
|
881
|
+
// has replaced the boot-default OPFS backend. Torn down in exitPlayMode.
|
|
882
|
+
if (getEditorMode() === 'browser') {
|
|
883
|
+
_unsubBrowserScriptWatch = watchScriptChanges(({ path, source }) => {
|
|
884
|
+
void applyBrowserComponentEdit(source).catch((err) =>
|
|
885
|
+
editorConsole.error(`In-browser hot-swap failed for ${path}: ${err}`, 'play-mode'),
|
|
886
|
+
);
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
// `session.game.worlds` is always present on a REAL `GameSession` (T6.1
|
|
890
|
+
// slice 1 — `create-runtime.ts` unconditionally sets `game`); the
|
|
891
|
+
// optional-chaining guard here is for the LEGACY/single-world play-mode
|
|
892
|
+
// unit tests (`adapter-resolver-play-path.test.ts`,
|
|
893
|
+
// `play-mode-multi-world.test.ts`), whose `fakeSession()` fixtures predate
|
|
894
|
+
// this slice and stub only the fields THEY need — never touch those
|
|
895
|
+
// pinned files, so this reads defensively instead.
|
|
896
|
+
_isMultiWorldSession = (session.game?.worlds.length ?? 1) > 1;
|
|
897
|
+
// Wave 20 (docs/MULTI-WORLD-DESIGN.md §1.F candidate improvement /
|
|
898
|
+
// MASTER-ARCHITECTURE-REVIEW §7.4 gap 4): surface WHICH persistence regime
|
|
899
|
+
// this session's edits are under. Derived from the SAME
|
|
900
|
+
// `_isMultiWorldSession` boolean the branch below uses to pick
|
|
901
|
+
// `installMultiWorldAuthoring` (overlay, real per-world persistence) vs
|
|
902
|
+
// `installPlayAuthoring` (ephemeral, discarded on stop) — never a second,
|
|
903
|
+
// independently-computed condition, so this can't drift from the actual
|
|
904
|
+
// provider that gets installed.
|
|
905
|
+
store.setPlayEditRegime(_isMultiWorldSession ? 'overlay' : 'ephemeral');
|
|
906
|
+
|
|
907
|
+
// Swap store to game scene so hierarchy/inspector show game entities.
|
|
908
|
+
// liveHierarchy: first-party play mode is the ONLY adoption path that may
|
|
909
|
+
// synthesize descriptors for untagged runtime objects (anti-shim rule —
|
|
910
|
+
// ingest/module mounts adopt foreign scenes and must never fabricate).
|
|
911
|
+
store.enterPlayScene(session.scene, { liveHierarchy: true });
|
|
912
|
+
if (_isMultiWorldSession) {
|
|
913
|
+
// T6.1 slice 3 (docs/MULTI-WORLD-DESIGN.md §1.F/§3 slice 3): N-ary
|
|
914
|
+
// Composite over one per-world adapter each, REAL (overlay) persistence —
|
|
915
|
+
// see installMultiWorldAuthoring's doc comment for why this differs from
|
|
916
|
+
// the ephemeral-wrapped single-world path below.
|
|
917
|
+
await installMultiWorldAuthoring(store, session.game!.worlds);
|
|
918
|
+
} else {
|
|
919
|
+
// Carry an ephemeral persistence provider for the duration of play (design
|
|
920
|
+
// docs/PERSISTENCE-PROVIDER-DESIGN.md §2/§7 slice 1) — see installPlayAuthoring.
|
|
921
|
+
installPlayAuthoring(store);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Wire physics sync THROUGH the first-party `RapierPhysicsAdapter` (Phase E):
|
|
925
|
+
// the gizmo path freezes a Rapier-owned body on beginEdit, commits the edited
|
|
926
|
+
// pose each frame (the callback below), and unfreezes on release — so the
|
|
927
|
+
// postPhysics writer doesn't snap the gizmo edit back. The editor speaks only
|
|
928
|
+
// the `PhysicsAdapter` interface, never Rapier directly.
|
|
929
|
+
// Install the GAME-scoped System-adapter aggregate (§7.1-3, probe1) — every
|
|
930
|
+
// world's `mounted.systems` merged (first registration wins per key), NOT
|
|
931
|
+
// just the default world's own copy (`firstParty(session).systems`, the
|
|
932
|
+
// pre-fix read) — so a networking/etc. adapter registered from ANY world's
|
|
933
|
+
// setup is visible to the editor's gizmo path + inspector panels.
|
|
934
|
+
// `session.game` is absent on the legacy fake-session fixtures some
|
|
935
|
+
// play-mode unit tests use (predate T6.1's `game` field) — optional-chain
|
|
936
|
+
// through so those keep passing unmodified, same fallback as before.
|
|
937
|
+
const systems = session.game?.systemAdapters ?? {};
|
|
938
|
+
setActiveSystems(systems);
|
|
939
|
+
|
|
940
|
+
// T6.3: gate game input (raw `window`/`document` listeners in game code via
|
|
941
|
+
// gated-globals, AND the default world's first-party InputManager) to only
|
|
942
|
+
// fire while play is actually running AND the Game tab is the focused
|
|
943
|
+
// viewport — so keystrokes typed into the editor (Scene tab, inspector
|
|
944
|
+
// fields) don't leak into the running game. `session.game` is absent on the
|
|
945
|
+
// legacy fake-session fixtures some play-mode unit tests use (predate T6.1's
|
|
946
|
+
// `game` field) — optional-chain through so those keep passing unmodified.
|
|
947
|
+
const gameInputActive = () =>
|
|
948
|
+
store.playState === 'playing' && store.activeViewportTab === 'game';
|
|
949
|
+
setGameInputGate(gameInputActive);
|
|
950
|
+
// `Game.input` is a THROWING getter, not an optional field — it delegates
|
|
951
|
+
// to the default world's first-party ctx and throws when that mount has
|
|
952
|
+
// no `firstParty` brand (`game.ts`'s `requireFirstPartyCtx`). Every
|
|
953
|
+
// scene/setup world mounts through `VgaiSceneGameAdapter` (branded), so
|
|
954
|
+
// this never fired before W1 — but an `adapter`-export default-three
|
|
955
|
+
// world (docs/R3F-FIRST-PARTY-DESIGN.md §1.A, e.g. the R3F bridge) owns
|
|
956
|
+
// its whole mount and correctly has no first-party `InputManager`. There
|
|
957
|
+
// is nothing to wire in that case: the gated-globals half of T6.3
|
|
958
|
+
// (`setGameInputGate` above) still fences raw window/document listeners;
|
|
959
|
+
// only the InputManager half is absent.
|
|
960
|
+
let gameInput: NonNullable<typeof session.game>['input'] | undefined;
|
|
961
|
+
try {
|
|
962
|
+
gameInput = session.game?.input;
|
|
963
|
+
} catch {
|
|
964
|
+
gameInput = undefined;
|
|
965
|
+
}
|
|
966
|
+
const syncInputEnabled = () => gameInput?.setEnabled(gameInputActive());
|
|
967
|
+
syncInputEnabled();
|
|
968
|
+
_unsubStore = store.subscribe(syncInputEnabled);
|
|
969
|
+
|
|
970
|
+
const physics = systems.physics;
|
|
971
|
+
store.setEcsSyncTransform((obj) => {
|
|
972
|
+
physics?.commit(obj, {
|
|
973
|
+
position: obj.position.toArray() as [number, number, number],
|
|
974
|
+
rotation: obj.quaternion.toArray() as [number, number, number, number],
|
|
975
|
+
scale: obj.scale.toArray() as [number, number, number],
|
|
976
|
+
});
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
// Container may have been display:none when w/h were read above.
|
|
980
|
+
// Now that the session exists, do one resize with the actual dimensions.
|
|
981
|
+
const actualW = gameContainer.clientWidth;
|
|
982
|
+
const actualH = gameContainer.clientHeight;
|
|
983
|
+
if (actualW > 0 && actualH > 0 && (actualW !== w || actualH !== h)) {
|
|
984
|
+
session.resize(actualW, actualH);
|
|
985
|
+
}
|
|
986
|
+
} catch (err) {
|
|
987
|
+
const msg = `Play mode failed to start: ${err}`;
|
|
988
|
+
editorConsole.error(msg, 'play-mode');
|
|
989
|
+
// Roll back only if THIS play is still the live generation — if it was
|
|
990
|
+
// already exited during boot (epoch moved on), a second exitPlayMode here
|
|
991
|
+
// could tear down a newer play that started in the meantime.
|
|
992
|
+
if (epoch === _playEpoch) exitPlayMode();
|
|
993
|
+
throw new Error(msg);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* Exit play mode: stop game, remove canvas, re-enable editor.
|
|
999
|
+
*/
|
|
1000
|
+
export function exitPlayMode(): void {
|
|
1001
|
+
if (!_ctx) return;
|
|
1002
|
+
// Invalidate any enterPlayMode still awaiting its runtime boot (see
|
|
1003
|
+
// _playEpoch): its session must be abandoned, not adopted.
|
|
1004
|
+
_playEpoch++;
|
|
1005
|
+
const { store } = _ctx;
|
|
1006
|
+
|
|
1007
|
+
// Restore editor scene before stopping (stop disposes game scene children)
|
|
1008
|
+
store.setEcsSyncTransform(null);
|
|
1009
|
+
setActiveSystems(null);
|
|
1010
|
+
store.exitPlayScene();
|
|
1011
|
+
if (_isMultiWorldSession) exitMultiWorldAuthoring();
|
|
1012
|
+
restorePriorAuthoring();
|
|
1013
|
+
_isMultiWorldSession = false;
|
|
1014
|
+
// Readout-only — clear the regime the moment play is no longer active
|
|
1015
|
+
// (mirrors _isMultiWorldSession's own reset just above). Optional-chained:
|
|
1016
|
+
// some pre-existing play-mode unit tests (e.g. play-mode-teardown.test.ts's
|
|
1017
|
+
// ED8 no-container path) pass a minimal hand-built fake store that predates
|
|
1018
|
+
// this signal and stubs only the methods THEY need — never touch those
|
|
1019
|
+
// pinned fixtures, so this degrades gracefully instead (same convention as
|
|
1020
|
+
// this file's other `session.game?.` guards for legacy fixtures).
|
|
1021
|
+
store.setPlayEditRegime?.(null);
|
|
1022
|
+
|
|
1023
|
+
// Always null _session even if stop() throws — otherwise enterPlayMode's
|
|
1024
|
+
// `if (_session) return` guard would permanently block re-entering play mode.
|
|
1025
|
+
if (_session) {
|
|
1026
|
+
try {
|
|
1027
|
+
_session.stop();
|
|
1028
|
+
} catch (err) {
|
|
1029
|
+
editorConsole.error(`Error stopping play session: ${err}`, 'play-mode');
|
|
1030
|
+
} finally {
|
|
1031
|
+
_session = null;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// Remove game canvas
|
|
1036
|
+
_gameCanvas?.remove();
|
|
1037
|
+
_gameCanvas = null;
|
|
1038
|
+
|
|
1039
|
+
// Cleanup subscriptions
|
|
1040
|
+
_unsubStore?.();
|
|
1041
|
+
_unsubStore = null;
|
|
1042
|
+
_unsubBrowserScriptWatch?.();
|
|
1043
|
+
_unsubBrowserScriptWatch = null;
|
|
1044
|
+
|
|
1045
|
+
// T6.3: no game running — game input (raw window/document listeners AND the
|
|
1046
|
+
// InputManager sync above) should never be suppressed again until the next
|
|
1047
|
+
// play session re-gates it.
|
|
1048
|
+
setGameInputGate(() => true);
|
|
1049
|
+
|
|
1050
|
+
_resizeObserver?.disconnect();
|
|
1051
|
+
_resizeObserver = null;
|
|
1052
|
+
|
|
1053
|
+
if (_escapeListener) {
|
|
1054
|
+
window.removeEventListener('keydown', _escapeListener);
|
|
1055
|
+
_escapeListener = null;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// Log before clearing sink so this message gets persisted
|
|
1059
|
+
editorConsole.log('Play mode stopped', 'play-mode');
|
|
1060
|
+
|
|
1061
|
+
// Flush remaining log entries and end session
|
|
1062
|
+
if (_flushInterval) {
|
|
1063
|
+
clearInterval(_flushInterval);
|
|
1064
|
+
_flushInterval = null;
|
|
1065
|
+
}
|
|
1066
|
+
editorConsole.setSink(null);
|
|
1067
|
+
if (_pendingEntries.length > 0) {
|
|
1068
|
+
const batch = _pendingEntries.splice(0);
|
|
1069
|
+
_logFlushChain = _logFlushChain.then(() => flushLogEntries(batch));
|
|
1070
|
+
}
|
|
1071
|
+
// The server has one active log file. End it only after every queued batch
|
|
1072
|
+
// has reached the append endpoint, otherwise Stop can race the final fetch
|
|
1073
|
+
// and silently discard the most useful end-of-run evidence.
|
|
1074
|
+
_logFlushChain = _logFlushChain.then(() => endLogSession());
|
|
1075
|
+
|
|
1076
|
+
unpatchConsole();
|
|
1077
|
+
|
|
1078
|
+
// setPlayState('stopped') auto-switches back to Scene tab
|
|
1079
|
+
store.setPlayState('stopped');
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/**
|
|
1083
|
+
* Pause play mode: freeze game, allow editor inspection.
|
|
1084
|
+
*/
|
|
1085
|
+
export function pausePlayMode(): void {
|
|
1086
|
+
if (!_session || !_ctx) return;
|
|
1087
|
+
_session.pause();
|
|
1088
|
+
_ctx.store.setPlayState('paused');
|
|
1089
|
+
|
|
1090
|
+
editorConsole.log('Play mode paused', 'play-mode');
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/**
|
|
1094
|
+
* Resume play mode from pause.
|
|
1095
|
+
*/
|
|
1096
|
+
export function resumePlayMode(): void {
|
|
1097
|
+
if (!_session || !_ctx) return;
|
|
1098
|
+
|
|
1099
|
+
_session.resume();
|
|
1100
|
+
_ctx.store.setPlayState('playing');
|
|
1101
|
+
|
|
1102
|
+
editorConsole.log('Play mode resumed', 'play-mode');
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* Step one fixed-timestep frame while paused.
|
|
1107
|
+
*/
|
|
1108
|
+
export function stepPlayMode(): void {
|
|
1109
|
+
if (!_session) return;
|
|
1110
|
+
_session.step();
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// --- Console patching ---
|
|
1114
|
+
|
|
1115
|
+
function patchConsole(): void {
|
|
1116
|
+
_originalConsoleLog = console.log;
|
|
1117
|
+
_originalConsoleInfo = console.info;
|
|
1118
|
+
_originalConsoleWarn = console.warn;
|
|
1119
|
+
_originalConsoleError = console.error;
|
|
1120
|
+
|
|
1121
|
+
console.log = (...args: unknown[]) => {
|
|
1122
|
+
_originalConsoleLog?.apply(console, args);
|
|
1123
|
+
if (!_engineLogActive) editorConsole.log(args.map(String).join(' '), 'game');
|
|
1124
|
+
};
|
|
1125
|
+
console.info = (...args: unknown[]) => {
|
|
1126
|
+
_originalConsoleInfo?.apply(console, args);
|
|
1127
|
+
if (!_engineLogActive) editorConsole.log(args.map(String).join(' '), 'game');
|
|
1128
|
+
};
|
|
1129
|
+
console.warn = (...args: unknown[]) => {
|
|
1130
|
+
_originalConsoleWarn?.apply(console, args);
|
|
1131
|
+
if (!_engineLogActive) editorConsole.warn(args.map(String).join(' '), 'game');
|
|
1132
|
+
};
|
|
1133
|
+
console.error = (...args: unknown[]) => {
|
|
1134
|
+
_originalConsoleError?.apply(console, args);
|
|
1135
|
+
if (!_engineLogActive) editorConsole.error(args.map(String).join(' '), 'game');
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function unpatchConsole(): void {
|
|
1140
|
+
if (_originalConsoleLog) console.log = _originalConsoleLog;
|
|
1141
|
+
if (_originalConsoleInfo) console.info = _originalConsoleInfo;
|
|
1142
|
+
if (_originalConsoleWarn) console.warn = _originalConsoleWarn;
|
|
1143
|
+
if (_originalConsoleError) console.error = _originalConsoleError;
|
|
1144
|
+
_originalConsoleLog = null;
|
|
1145
|
+
_originalConsoleInfo = null;
|
|
1146
|
+
_originalConsoleWarn = null;
|
|
1147
|
+
_originalConsoleError = null;
|
|
1148
|
+
}
|