@vgai/editor 0.2.0 → 0.4.0-canary.20260715.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/dist/assets/axe-C7YBtOuM.js +30 -0
- package/dist/assets/browserAll-DDtGGE48.js +1 -0
- package/dist/assets/create-runtime-DbTlBlKG.js +1 -0
- package/dist/assets/{game-BI111o5f.js → game-BE0r7DMI.js} +3 -3
- package/dist/assets/game-BdTHR2_X.js +1 -0
- package/dist/assets/{game-pt_1lQLv.js → game-C3iQGSqo.js} +1 -1
- package/dist/assets/game-CcHfYxjP.js +1 -0
- package/dist/assets/game-Cqzj_lpD.js +1 -0
- package/dist/assets/game-DSZdeCHu.js +1 -0
- package/dist/assets/game-Dch9sAPc.js +1 -0
- package/dist/assets/{game-xsOho_wg.js → game-SAFn90pA.js} +3 -3
- package/dist/assets/game-loop-DMIgSMV-.js +1 -0
- package/dist/assets/{game-source-CvidN7AB.js → game-source-Dk83w26t.js} +1 -1
- package/dist/assets/{index-Bp5cecD6.js → index-4idIsHHJ.js} +2 -2
- package/dist/assets/{index-DL7RcQ0n.js → index-C-LhOTBN.js} +1 -1
- package/dist/assets/{index-Cs8xwv3e.js → index-CDkxceqE.js} +1 -1
- package/dist/assets/index-CQEfi217.js +15412 -0
- package/dist/assets/index-Dvi-V0NE.css +1 -0
- package/dist/assets/ingest-siblings-s0P5I1Ub.js +1 -0
- package/dist/assets/mount-manifest-DXMOlF79.js +1 -0
- package/dist/assets/react-18-C9eHn8x1.js +1 -0
- package/dist/assets/webworkerAll-Fn0e-ouF.js +1 -0
- package/dist/assets/world-documents-GM0a9Kv7.js +1 -0
- package/dist/index.html +15 -13
- package/dist/ingest/racing-game/vgai.game.json +5 -4
- package/dist/ingest/simcity/vgai.game.json +5 -4
- package/dist/ingest/tanks/vgai.game.json +5 -4
- package/dist/project-scripts.bundle.js +3917 -0
- package/dist/scenes/untitled.vscn.json +2 -0
- package/dist-server/packaged.mjs +7697 -3435
- package/package.json +31 -4
- package/scripts/probe-reporter.ts +349 -0
- package/scripts/run-probe.ts +2260 -0
- package/server/asset-catalog-audit.ts +268 -0
- package/server/asset-catalog-v2.ts +146 -0
- package/server/asset-library-routes.ts +276 -58
- package/server/canonical-path.ts +1 -1
- package/server/dev.ts +148 -44
- package/server/editor-server.ts +888 -61
- package/server/editor-sse.ts +115 -4
- package/server/harness-chat-service.ts +796 -0
- package/server/local-asset-catalog.ts +157 -22
- package/server/packaged.ts +51 -21
- package/server/process-shutdown.ts +136 -0
- package/server/prod.ts +9 -6
- package/server/project-hmr-files.ts +41 -0
- package/server/project-operations.ts +343 -0
- package/server/project-optimize-deps-entries.ts +4 -4
- package/server/project-output-writer.ts +156 -0
- package/server/project-validation.ts +140 -0
- package/server/project-view.ts +8 -50
- package/server/server-utils.ts +75 -10
- package/server/session-registry.ts +12 -0
- package/src/EditorContext.tsx +107 -3
- package/src/action-registry.ts +115 -17
- package/src/adapter-resolver.ts +213 -478
- package/src/asset-events.ts +64 -1
- package/src/asset-preview.ts +906 -0
- package/src/asset-selection.ts +121 -0
- package/src/asset-workflow/asset-capabilities.ts +259 -0
- package/src/asset-workflow/asset-editor-errors.ts +46 -0
- package/src/asset-workflow/asset-import-pipeline.ts +176 -0
- package/src/asset-workflow/asset-library-query.ts +107 -0
- package/src/asset-workflow/asset-types.ts +192 -0
- package/src/asset-workflow/asset-workflow-quality.ts +65 -0
- package/src/asset-workflow/import-contract.ts +113 -0
- package/src/asset-workflow/model-conversion.ts +126 -0
- package/src/asset-workflow/model-inspection.ts +185 -0
- package/src/asset-workflow/project-asset-commands.ts +23 -0
- package/src/asset-workflow/project-asset-health.ts +153 -0
- package/src/asset-workflow/project-asset-operations.ts +119 -0
- package/src/asset-workflow/thumbnail-system.ts +172 -0
- package/src/authoring/active-systems.ts +15 -0
- package/src/authoring/boundary-authoring-adapter.ts +16 -5
- package/src/authoring/composite-authoring-adapter.ts +305 -124
- package/src/authoring/design-time-layers.ts +300 -117
- package/src/authoring/edit-mode-authoring.ts +278 -293
- package/src/authoring/embed-only-adapter.ts +4 -2
- package/src/authoring/first-party-hierarchy-menu.ts +7 -2
- package/src/authoring/first-party-inspector/AddSectionMenu.tsx +5 -12
- package/src/authoring/first-party-inspector/EnvironmentSection.tsx +53 -126
- package/src/authoring/first-party-inspector/HeaderSection.tsx +54 -65
- package/src/authoring/first-party-inspector/is-first-party-selection.ts +30 -11
- package/src/authoring/first-party-inspector/register.tsx +13 -2
- package/src/authoring/ingest-authoring-adapter.ts +142 -45
- package/src/authoring/mount-failure-report.ts +5 -5
- package/src/authoring/provenance.ts +41 -0
- package/src/authoring/react-dom-authoring-adapter.ts +92 -92
- package/src/authoring/react-inspector-section.tsx +489 -51
- package/src/authoring/react-world-authoring-adapter.ts +541 -306
- package/src/authoring/story-overlay.ts +1 -1
- package/src/authoring/story-types.ts +11 -0
- package/src/authoring/vgai-scene-authoring-adapter.ts +54 -25
- package/src/authoring/viewport-tool-context.ts +78 -0
- package/src/authoring/world-canvas-viewport-state.ts +35 -0
- package/src/authoring/world-hidden-viewport.ts +186 -0
- package/src/authoring/world-pan-state.ts +33 -17
- package/src/authoring/world-session-state.ts +1 -1
- package/src/authoring/world2d-authoring-adapter.ts +109 -52
- package/src/availability-tick.ts +56 -0
- package/src/browser-play-entry.ts +5 -5
- package/src/browser-project-scripts.ts +1 -0
- package/src/build-identity.ts +15 -0
- package/src/command-listener.ts +893 -19
- package/src/components/AlignToolbar.tsx +74 -63
- package/src/components/AppRoot.tsx +193 -26
- package/src/components/ApplicationMenus.tsx +272 -0
- package/src/components/AssetBrowser.tsx +1450 -293
- package/src/components/AssetEditorShell.tsx +141 -0
- package/src/components/AssetImportDetails.tsx +218 -0
- package/src/components/AssetLibraryPanel.tsx +14 -0
- package/src/components/CameraInfo.tsx +11 -20
- package/src/components/CenterDocuments.tsx +585 -0
- package/src/components/CommandPalette.tsx +75 -30
- package/src/components/ConsolePanel.css +55 -0
- package/src/components/ConsolePanel.tsx +57 -112
- package/src/components/DebugConsolePanel.tsx +461 -0
- package/src/components/DebuggerPanel.tsx +165 -0
- package/src/components/DefaultEditorLayout.tsx +167 -231
- package/src/components/GameHierarchy.tsx +873 -290
- package/src/components/GamePerformanceOverlay.tsx +46 -0
- package/src/components/HarnessChatPanel.tsx +2352 -0
- package/src/components/HistoryPanel.tsx +226 -0
- package/src/components/Inspector.tsx +153 -99
- package/src/components/InspectorToolSection.tsx +6 -4
- package/src/components/MountFailureBanner.tsx +31 -48
- package/src/components/NetworkSection.tsx +25 -14
- package/src/components/OnlineAssetBrowser.tsx +794 -343
- package/src/components/OverlayReportBanner.tsx +38 -64
- package/src/components/PerformancePanel.tsx +206 -0
- package/src/components/PlayBar.tsx +93 -263
- package/src/components/PrefabSaveDialog.tsx +55 -127
- package/src/components/ProjectHeader.css +60 -0
- package/src/components/ProjectHeader.tsx +170 -105
- package/src/components/ProjectScreen.tsx +246 -184
- package/src/components/ReactCanvasControls.tsx +185 -0
- package/src/components/ResolutionPicker.tsx +85 -0
- package/src/components/RootActivateDialog.tsx +39 -68
- package/src/components/SaveDialog.tsx +55 -127
- package/src/components/SaveStatus.tsx +11 -29
- package/src/components/StartupLoadingScreen.tsx +96 -0
- package/src/components/StateWatchPanel.tsx +302 -0
- package/src/components/StatsOverlay.tsx +12 -5
- package/src/components/StatusBar.tsx +64 -0
- package/src/components/ThemeManagerDialog.css +89 -0
- package/src/components/ThemeManagerDialog.tsx +371 -0
- package/src/components/ToolHost.tsx +26 -15
- package/src/components/Toolbar.tsx +117 -214
- package/src/components/VgaiVoxelLogo.tsx +89 -0
- package/src/components/ViewportOverlay.tsx +97 -172
- package/src/components/ViewportPanel.tsx +130 -31
- package/src/components/WorkspaceDock.tsx +891 -0
- package/src/components/WorldSelectionOverlay.tsx +51 -63
- package/src/components/WorldTextEditor.tsx +9 -5
- package/src/components/asset-documents.tsx +390 -0
- package/src/components/asset-editor-persistence.ts +44 -0
- package/src/components/asset-selection-section.tsx +338 -0
- package/src/components/asset-viewers/AudioViewer.tsx +73 -21
- package/src/components/asset-viewers/GenericJsonViewer.tsx +26 -11
- package/src/components/asset-viewers/ImageViewer.tsx +43 -11
- package/src/components/asset-viewers/InputMapViewer.tsx +512 -134
- package/src/components/asset-viewers/MaterialAssetEditor.tsx +540 -0
- package/src/components/asset-viewers/ModelViewer.tsx +805 -121
- package/src/components/asset-viewers/OnlineAssetDetail.tsx +177 -60
- package/src/components/asset-viewers/SourceAssetViewer.tsx +121 -0
- package/src/components/asset-workflow.css +506 -0
- package/src/components/build-documents.tsx +696 -0
- package/src/components/core-utilities.tsx +75 -0
- package/src/components/data-documents.tsx +364 -0
- package/src/components/{DataPanel.tsx → data-editor.tsx} +138 -288
- package/src/components/debugger-panel-state.ts +57 -0
- package/src/components/inspector-widgets/AlignmentGrid.tsx +8 -14
- package/src/components/inspector-widgets/BorderEditor.tsx +41 -33
- package/src/components/inspector-widgets/ColorPicker.tsx +39 -39
- package/src/components/inspector-widgets/FontPicker.tsx +13 -21
- package/src/components/inspector-widgets/GradientEditor.tsx +11 -19
- package/src/components/inspector-widgets/ScrubbableInput.tsx +6 -3
- package/src/components/inspector-widgets/ShadowEditor.tsx +20 -33
- package/src/components/inspector-widgets/shared.tsx +90 -49
- package/src/components/inspectors/AnimationSection.tsx +39 -40
- package/src/components/inspectors/AudioSection.tsx +28 -39
- package/src/components/inspectors/BoneAttachmentSection.tsx +19 -24
- package/src/components/inspectors/CameraSection.tsx +14 -23
- package/src/components/inspectors/ComponentsSection.tsx +39 -43
- package/src/components/inspectors/JointsSection.tsx +34 -43
- package/src/components/inspectors/LightSection.tsx +90 -35
- package/src/components/inspectors/MaterialSection.tsx +137 -174
- package/src/components/inspectors/MeshSection.tsx +9 -17
- package/src/components/inspectors/NavigationSection.tsx +8 -15
- package/src/components/inspectors/ParticleSection.tsx +91 -76
- package/src/components/inspectors/PhysicsSection.tsx +42 -46
- package/src/components/inspectors/PostProcessingSection.tsx +28 -36
- package/src/components/inspectors/RenderingSection.tsx +12 -20
- package/src/components/inspectors/ShadowSection.tsx +7 -8
- package/src/components/inspectors/SplineSection.tsx +31 -42
- package/src/components/inspectors/TransformSection.tsx +14 -22
- package/src/components/patterns/Dialog.tsx +95 -0
- package/src/components/patterns/Fields.tsx +44 -0
- package/src/components/patterns/List.tsx +25 -0
- package/src/components/patterns/StateSurface.tsx +40 -0
- package/src/components/patterns/Surfaces.tsx +115 -0
- package/src/components/patterns/Tabs.tsx +80 -0
- package/src/components/patterns/Toolbar.tsx +51 -0
- package/src/components/patterns/Tree.tsx +72 -0
- package/src/components/primitives/Button.tsx +46 -0
- package/src/components/primitives/ColorInput.tsx +11 -47
- package/src/components/primitives/EditorIcon.tsx +99 -0
- package/src/components/primitives/FormControls.tsx +71 -0
- package/src/components/primitives/JsonInput.tsx +15 -2
- package/src/components/primitives/Layout.tsx +100 -0
- package/src/components/primitives/Menu.tsx +105 -0
- package/src/components/primitives/NumberInput.tsx +8 -35
- package/src/components/primitives/Panel.tsx +49 -44
- package/src/components/primitives/SectionHeader.tsx +10 -28
- package/src/components/primitives/Text.tsx +46 -0
- package/src/components/primitives/Tooltip.tsx +20 -12
- package/src/components/primitives/Vec3Input.tsx +3 -0
- package/src/components/primitives/banner-tones.ts +33 -0
- package/src/components/primitives/editor-icons.ts +102 -0
- package/src/components/primitives/panel-header-styles.ts +30 -0
- package/src/components/project-operation-documents.tsx +226 -0
- package/src/components/status-contributions.tsx +175 -0
- package/src/components/story-documents-register.ts +15 -0
- package/src/components/story-documents.tsx +1185 -0
- package/src/components/tool-documents.tsx +110 -0
- package/src/components/workspace-dock.css +293 -0
- package/src/components/workspace-history.ts +33 -0
- package/src/components/world-documents.tsx +145 -0
- package/src/components/xstate-documents.tsx +209 -0
- package/src/composite-screenshot.ts +248 -0
- package/src/design-system/index.ts +70 -0
- package/src/design-system-stories/Foundation.stories.tsx +183 -0
- package/src/design-system-stories/Patterns.stories.tsx +251 -0
- package/src/design-system-stories/Workspace.stories.tsx +538 -0
- package/src/doctor/report.ts +57 -13
- package/src/editor-api.ts +295 -31
- package/src/editor-compatibility.ts +200 -0
- package/src/editor-console.ts +35 -17
- package/src/editor-hotkeys.ts +85 -33
- package/src/editor-store.ts +538 -233
- package/src/editor-viewport.ts +160 -34
- package/src/entity-factory.ts +14 -3
- package/src/gizmo-registry.ts +21 -5
- package/src/harness-chat-editor-context.ts +37 -0
- package/src/harness-chat-indicator.ts +68 -0
- package/src/harness-chat-layout.ts +53 -0
- package/src/harness-chat-lifecycle.ts +170 -0
- package/src/harness-chat-reconnect.ts +85 -0
- package/src/harness-chat-types.ts +409 -0
- package/src/hierarchy-drop.ts +50 -0
- package/src/hierarchy-expansion-state.ts +100 -0
- package/src/hierarchy-menu-registry.ts +1 -1
- package/src/hierarchy-projection.ts +76 -0
- package/src/history/editor-session.ts +21 -0
- package/src/history/history-commands.ts +147 -0
- package/src/history/history-service.ts +904 -0
- package/src/history/json-history-resource.ts +165 -0
- package/src/history/persistence-coordinator.ts +35 -0
- package/src/history/project-file-history.ts +266 -0
- package/src/history/project-root-history-backends.ts +53 -0
- package/src/history/resource-registry.ts +209 -0
- package/src/history/snapshot-store.ts +108 -0
- package/src/history/source-history-backend.ts +374 -0
- package/src/history/types.ts +100 -0
- package/src/hmr-registration-group.ts +67 -0
- package/src/hmr-stable-react-context.ts +23 -0
- package/src/hosted-example.ts +12 -2
- package/src/hotkeys.ts +89 -18
- package/src/ingest/discovery-react.ts +9 -10
- package/src/ingest/discovery.ts +4 -4
- package/src/ingest/discovery2d.ts +5 -5
- package/src/ingest/games/bloom-composer/vgai.game.json +3 -3
- package/src/ingest/games/embed-bundle/vgai.game.json +3 -3
- package/src/ingest/games/games-fps/vgai.game.json +3 -3
- package/src/ingest/games/iframe-reachable/vgai.game.json +3 -3
- package/src/ingest/games/interactive-cubes/vgai.game.json +3 -3
- package/src/ingest/games/mismatched-three/vgai.game.json +3 -3
- package/src/ingest/games/own-three-bundled/vgai.game.json +3 -3
- package/src/ingest/games/pointerlock-maze/vgai.game.json +7 -4
- package/src/ingest/games-2d/bubbo-bubbo/vgai.game.json +3 -3
- package/src/ingest/games-2d/bunnymark/vgai.game.json +3 -3
- package/src/ingest/games-2d/flappy/vgai.game.json +3 -3
- package/src/ingest/games-2d/flappy-pixi/vgai.game.json +3 -3
- package/src/ingest/games-2d/puzzling-potions/vgai.game.json +3 -3
- package/src/ingest/games-2d/tilemap-camera/vgai.game.json +3 -3
- package/src/ingest/games-react/react-rpg/vgai.game.json +3 -3
- package/src/ingest/types.ts +1 -1
- package/src/ingest-mode.ts +47 -52
- package/src/ingest-siblings.ts +23 -33
- package/src/initial-scene.ts +1 -2
- package/src/main.tsx +16 -8
- package/src/manifest-project.ts +10 -12
- package/src/model-thumbnail.ts +261 -26
- package/src/module-mode.ts +44 -50
- package/src/play-log-events.ts +26 -0
- package/src/play-mode.ts +495 -401
- package/src/play-scene-adoption.ts +23 -0
- package/src/playground/main.ts +68 -24
- package/src/playwright-shim.ts +473 -0
- package/src/project-manager.ts +90 -10
- package/src/project-operations.ts +112 -0
- package/src/project-story-discovery.ts +25 -0
- package/src/project-tool-discovery.ts +43 -0
- package/src/scene-serializer.ts +0 -6
- package/src/scene-sync.ts +7 -2
- package/src/script-loader.ts +3 -3
- package/src/skeleton-helper.ts +57 -0
- package/src/storage/seed.ts +68 -8
- package/src/stories/StoryPreviewMount.tsx +265 -0
- package/src/stories/action-stories.fixture.tsx +44 -0
- package/src/stories/button-stories.fixture.tsx +50 -0
- package/src/stories/compose-project-stories.ts +182 -0
- package/src/stories/loader-stories.fixture.tsx +53 -0
- package/src/stories/story-discovery.ts +130 -0
- package/src/stories/story-registry.ts +146 -0
- package/src/theme-css.ts +1049 -0
- package/src/theme-library.ts +238 -0
- package/src/theme-preference.ts +130 -0
- package/src/theme.ts +527 -0
- package/src/tool-loader.ts +80 -24
- package/src/ui-source/inspect.ts +27 -4
- package/src/ui-source/source-write-backend.ts +83 -22
- package/src/widgets/index.ts +9 -18
- package/src/workspace-aux-commands.ts +11 -0
- package/src/workspace-dock-controller.ts +64 -0
- package/src/workspace-document-ids.ts +5 -0
- package/src/workspace-document-registry.ts +367 -0
- package/src/workspace-state-persistence.ts +455 -0
- package/src/workspace-status-registry.ts +110 -0
- package/src/workspace-utility-commands.ts +49 -0
- package/src/workspace-utility-registry.ts +161 -0
- package/src/world-document-routing.ts +16 -0
- package/src/xstate-inspect/XStateMachineInspector.tsx +544 -0
- package/src/xstate-inspect/character-animation-machine.fixture.ts +74 -0
- package/src/xstate-inspect/use-live-actor-state.ts +39 -0
- package/src/xstate-inspect/xstate-graph.ts +265 -0
- package/src/xstate-inspect/xstate-layout.ts +76 -0
- package/src/z-index.ts +26 -0
- package/template/.claude/skills/editor/SKILL.md +129 -90
- package/template/AGENTS.md +1110 -0
- package/template/CLAUDE.md +1 -512
- package/template/IDIOMS.md +50 -0
- package/template/check-idioms.ts +706 -0
- package/template/manifest-entry-modules-plugin.ts +53 -0
- package/template/package.json +13 -4
- package/template/public/scenes/default.scn2d.json +6 -0
- package/template/public/scenes/default.vscn.json +7 -1
- package/template/public/scenes/untitled.vscn.json +10 -0
- package/template/scripts/playtest.ts +40 -0
- package/template/server/loopback.ts +75 -0
- package/template/server/rooms/arena-room.ts +69 -1
- package/template/server/rooms/game-room.ts +68 -1
- package/template/src/data/README.md +9 -4
- package/template/src/data/data-ref.ts +35 -0
- package/template/src/data/tuning.schema.ts +1 -1
- package/template/src/main.ts +41 -62
- package/template/src/manifest-entry-modules.d.ts +3 -0
- package/template/src/operations/README.md +88 -0
- package/template/src/runtime/pixi-adapter.ts +24 -0
- package/template/src/scripts/components/data-spinner.ts +43 -2
- package/template/src/scripts/components/spin-lap-math.ts +27 -0
- package/template/src/tools/README.md +5 -3
- package/template/src/tools/example.tool.tsx +13 -7
- package/template/src/ui/Button.stories.tsx +66 -0
- package/template/src/ui/RebindPanel.tsx +2 -6
- package/template/src/ui/game.tsx +17 -0
- package/template/tests/acceptance/example.spec.ts +64 -0
- package/template/tests/logic/example.test.ts +31 -0
- package/template/tests/logic/room.test.ts +100 -0
- package/template/tests/tsconfig.json +30 -0
- package/template/validate-manifest.ts +5 -5
- package/template/validate-scenes.ts +50 -18
- package/template/vgai.game.json +20 -13
- package/template/vite.config.ts +77 -16
- package/vite-plugin-ui-oid.ts +261 -17
- package/dist/assets/CCDIKSolver-Dz-ysxZF.js +0 -1
- package/dist/assets/SelectionBox-BXsv_6aH.js +0 -1
- package/dist/assets/browser-project-scripts-DOru4L1x.js +0 -2
- package/dist/assets/browserAll-CEkWYlFk.js +0 -1
- package/dist/assets/create-runtime-DYIWr3va.js +0 -1
- package/dist/assets/data-asset-Bb9sIM7i.js +0 -5
- package/dist/assets/game-8SDZPPgH.js +0 -1
- package/dist/assets/game-CevorxyV.js +0 -1
- package/dist/assets/game-Di7XlywD.js +0 -1
- package/dist/assets/game-DjnCaw8R.js +0 -1
- package/dist/assets/game-Q9A0aXSV.js +0 -1
- package/dist/assets/index-CPVV6pjw.js +0 -1
- package/dist/assets/index-CpOnYyeD.js +0 -213
- package/dist/assets/index-Dr8H_6Rl.js +0 -1
- package/dist/assets/index-_bCaipUk.js +0 -13650
- package/dist/assets/ingest-siblings-Dz8eEqrV.js +0 -1
- package/dist/assets/mount-manifest-Bhk0ohsl.js +0 -1
- package/dist/assets/scene-query-BivAHhJu.js +0 -1
- package/dist/assets/webworkerAll-BLrH0F6F.js +0 -1
- package/dist/assets/xstate-animation-binding-1NGLmw6y.js +0 -1
- package/dist/assets/xstate.esm-BgUD33nl.js +0 -3
- package/src/authoring/scene-ui-layer.ts +0 -485
- package/src/authoring/ui-authoring-adapter.ts +0 -325
- package/src/authoring/ui-hierarchy-menu.ts +0 -40
- package/src/authoring/ui-inspector-sections.tsx +0 -156
- package/src/authoring/undo-timeline.ts +0 -145
- package/src/components/BottomPanel.tsx +0 -326
- package/src/components/BuildPanel.tsx +0 -455
- package/src/components/FolderTree.tsx +0 -179
- package/src/components/RightPanel.tsx +0 -166
- package/src/components/WorldContextMenu.tsx +0 -257
- package/src/components/asset-viewers/AssetViewer.tsx +0 -38
- package/src/ingest/games-react/react-rpg/src/ui/game-state.tsx +0 -129
- package/src/scene-ui-mode/active-scene-ui.ts +0 -58
- package/src/scene-ui-mode/scene-ui-palette-panel.tsx +0 -152
- package/src/scene-ui-mode/scene-ui-timeline-tab.tsx +0 -29
- package/src/scene-ui-mode/ui-prefab-dialog-host.tsx +0 -74
- package/src/scene-ui-mode/ui-prefab-hierarchy-menu.ts +0 -49
- package/src/scene-ui-mode/ui-prefab-io.ts +0 -76
- package/src/ui-editor/inspector.tsx +0 -563
- package/src/ui-editor/node-ops.ts +0 -266
- package/src/ui-editor/overlay.tsx +0 -380
- package/src/ui-editor/palette.tsx +0 -114
- package/src/ui-editor/react-store.ts +0 -78
- package/src/ui-editor/timeline-panel.tsx +0 -443
- package/src/ui-editor/ui-edit-store.ts +0 -552
- package/src/ui-editor/ui-prefab.ts +0 -95
- package/template/project.json +0 -9
- package/template/src/scripts/main.ts +0 -94
- package/template/src/ui/DefaultHud.tsx +0 -40
- package/template/src/ui/game-state.tsx +0 -37
- package/template/src/ui/mount-default-hud.tsx +0 -19
- package/template/src/ui/mount-ui.ts +0 -15
- package/vite-plugin-react-world-provider.ts +0 -81
package/src/command-listener.ts
CHANGED
|
@@ -6,7 +6,17 @@
|
|
|
6
6
|
* This module translates them into store method calls.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { waitForHoldBudget } from '@engine/runtime/debug-bridge';
|
|
9
10
|
import { parseSceneFile, SceneParseError } from '@engine/scene/parse';
|
|
11
|
+
import {
|
|
12
|
+
type AssetPreviewBackground,
|
|
13
|
+
captureModelAssetPreview,
|
|
14
|
+
captureObjectAssetPreview,
|
|
15
|
+
} from './asset-preview';
|
|
16
|
+
import { getActiveSystems } from './authoring/active-systems';
|
|
17
|
+
import { isAssetDocumentId, openAssetDocument } from './components/asset-documents';
|
|
18
|
+
import { openBuildProfilesDocument } from './components/build-documents';
|
|
19
|
+
import { capturePlayComposite } from './composite-screenshot';
|
|
10
20
|
import { connectEvents, reportCommandResult, reportEditorState } from './editor-api';
|
|
11
21
|
import { editorConsole } from './editor-console';
|
|
12
22
|
import { getEditorMode } from './editor-mode';
|
|
@@ -14,10 +24,21 @@ import type { AssetKind, EditorStore, HelperVisibility } from './editor-store';
|
|
|
14
24
|
import {
|
|
15
25
|
enterPlayMode,
|
|
16
26
|
exitPlayMode,
|
|
27
|
+
getGameContainer,
|
|
28
|
+
getPlayCanvas,
|
|
29
|
+
getPlayRuntimeAccess,
|
|
30
|
+
getPlayStartedAt,
|
|
31
|
+
isPlayModeActive,
|
|
17
32
|
pausePlayMode,
|
|
18
33
|
resumePlayMode,
|
|
19
34
|
stepPlayMode,
|
|
20
35
|
} from './play-mode';
|
|
36
|
+
import { createPageShim, PageShimUnsupportedError } from './playwright-shim';
|
|
37
|
+
import {
|
|
38
|
+
activateWorkspaceDocument,
|
|
39
|
+
activeWorkspaceDocumentId,
|
|
40
|
+
closeWorkspaceDocument,
|
|
41
|
+
} from './workspace-document-registry';
|
|
21
42
|
|
|
22
43
|
export interface EditorCommand {
|
|
23
44
|
type: string;
|
|
@@ -25,10 +46,14 @@ export interface EditorCommand {
|
|
|
25
46
|
[key: string]: unknown;
|
|
26
47
|
}
|
|
27
48
|
|
|
49
|
+
/** Keep in sync with `../server/server-utils.ts`'s `CommandResult` — this is
|
|
50
|
+
* the browser half of the same wire shape. `data` is the payload leg for
|
|
51
|
+
* commands that answer with data (`capture-viewport`'s base64 PNG, the
|
|
52
|
+
* Wave 5 debug-seam reads); on failures it carries structured markers
|
|
53
|
+
* (`{ code: ... }`) the SDK matches on, never message prose. */
|
|
28
54
|
export interface CommandResult {
|
|
29
55
|
ok: boolean;
|
|
30
56
|
error?: string;
|
|
31
|
-
/** Optional payload for commands that answer with data (e.g. `capture-viewport`'s base64 PNG). */
|
|
32
57
|
data?: Record<string, unknown>;
|
|
33
58
|
}
|
|
34
59
|
|
|
@@ -50,13 +75,56 @@ function collectCamera(store: EditorStore):
|
|
|
50
75
|
};
|
|
51
76
|
}
|
|
52
77
|
|
|
78
|
+
/** #145 — what the human's tab is actually showing right now: page
|
|
79
|
+
* visibility + window focus. `null` outside a real browser (the relay's
|
|
80
|
+
* node-side tests). The agent reads this to know whether the user is
|
|
81
|
+
* LOOKING at the shared session before deciding how to narrate/verify;
|
|
82
|
+
* `connectCommandListener` re-reports state on visibilitychange/focus/blur
|
|
83
|
+
* so the server's `/__editor/state` snapshot stays current between
|
|
84
|
+
* commands. */
|
|
85
|
+
function collectPresence(): { visibility: DocumentVisibilityState; focused: boolean } | null {
|
|
86
|
+
if (typeof document === 'undefined') return null;
|
|
87
|
+
return { visibility: document.visibilityState, focused: document.hasFocus() };
|
|
88
|
+
}
|
|
89
|
+
|
|
53
90
|
export function collectState(store: EditorStore): Record<string, unknown> {
|
|
54
91
|
return {
|
|
55
92
|
playState: store.playState,
|
|
93
|
+
presence: collectPresence(),
|
|
94
|
+
// Target-blaster friction #3 (#146 ledger): the play-verify loop's error
|
|
95
|
+
// channel — the same current-run-fenced uncaught-error list the relay
|
|
96
|
+
// snapshot carries (see collectPageErrors), so `vgai status` answers
|
|
97
|
+
// "did anything go wrong since play started" without a second command.
|
|
98
|
+
// [] while stopped or when nothing threw.
|
|
99
|
+
pageErrors: collectPageErrors(),
|
|
100
|
+
// Wave 5: the live loop's time-scale, so `play.status` reports the real
|
|
101
|
+
// applied value after a `set-time-scale` instead of an honest-gap null.
|
|
102
|
+
timeScale: getPlayRuntimeAccess()?.loop.timeScale ?? null,
|
|
103
|
+
// Issue #175 — the REAL loop liveness (`GameLoop.liveness`), NOT `store.
|
|
104
|
+
// playState` above: `playState` is editor UI state that stays 'playing'
|
|
105
|
+
// even while the T2.1 idle throttle has hidden-paused the loop (a hidden
|
|
106
|
+
// tab stops ticking outright, deliberately — see game-loop.ts). Reading
|
|
107
|
+
// only `playState` is exactly the gap that let a frozen game report as
|
|
108
|
+
// healthy (measured: playState "playing", sim speed 0.00x over 32.9s
|
|
109
|
+
// wall). `null` outside play mode — there is no loop to report on.
|
|
110
|
+
loopLiveness: getPlayRuntimeAccess()?.loop.liveness ?? null,
|
|
111
|
+
// D15/T-D15.6: the live session's ctx.random root seed (real — reflects
|
|
112
|
+
// the boot seed or the last successful play.seed.set), and whether the
|
|
113
|
+
// running project declares determinism.seededRandom at all. Both null/
|
|
114
|
+
// false when no first-party Game is running.
|
|
115
|
+
seed: getPlayRuntimeAccess()?.random?.seed ?? null,
|
|
116
|
+
deterministic: getPlayRuntimeAccess()?.determinismDeclared ?? false,
|
|
56
117
|
selectedEntityId: store.selectedEntityId,
|
|
57
118
|
selectedEntityIds: [...store.selectedEntityIds],
|
|
58
119
|
activeViewportTab: store.activeViewportTab,
|
|
59
|
-
|
|
120
|
+
// W2: asset viewers are center workspace documents now; this facet keeps
|
|
121
|
+
// its legacy meaning for control-API consumers — the active ASSET
|
|
122
|
+
// document's key, or '__inspector__' (the right rail always shows the
|
|
123
|
+
// Inspector since W2).
|
|
124
|
+
activeTabKey: (() => {
|
|
125
|
+
const activeId = activeWorkspaceDocumentId();
|
|
126
|
+
return activeId && isAssetDocumentId(activeId) ? activeId : '__inspector__';
|
|
127
|
+
})(),
|
|
60
128
|
showGrid: store.showGrid,
|
|
61
129
|
showHelpers: store.showHelpers,
|
|
62
130
|
showStats: store.showStats,
|
|
@@ -67,6 +135,8 @@ export function collectState(store: EditorStore): Record<string, unknown> {
|
|
|
67
135
|
snapEnabled: store.snapEnabled,
|
|
68
136
|
entityCount: store.allEntities().length,
|
|
69
137
|
savePath: store.savePath,
|
|
138
|
+
// SDK clients need the same persistence truth the editor UI exposes.
|
|
139
|
+
saveState: store.saveState,
|
|
70
140
|
// Camera facet (B3-followup): real pose when a viewport is bound, absent
|
|
71
141
|
// otherwise — never a fabricated value (see EditorStore.cameraPose).
|
|
72
142
|
camera: collectCamera(store),
|
|
@@ -79,6 +149,475 @@ export function collectState(store: EditorStore): Record<string, unknown> {
|
|
|
79
149
|
};
|
|
80
150
|
}
|
|
81
151
|
|
|
152
|
+
/** Surface a thrown engine error carrying a machine-readable `code`
|
|
153
|
+
* (`DebugError`, `InputActionError` — duck-typed, so no engine-module
|
|
154
|
+
* identity coupling) as a structured relay failure: `data.code` plus the
|
|
155
|
+
* error's own data (registered-name lists, zod issues). The SDK maps these
|
|
156
|
+
* to operation error codes — never by parsing message prose. */
|
|
157
|
+
function structuredErrorResult(err: unknown): CommandResult {
|
|
158
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
159
|
+
const e = err as { code?: unknown; data?: unknown };
|
|
160
|
+
if (typeof e?.code === 'string') {
|
|
161
|
+
const extra = typeof e.data === 'object' && e.data !== null ? e.data : {};
|
|
162
|
+
return { ok: false, error: message, data: { ...extra, code: e.code } };
|
|
163
|
+
}
|
|
164
|
+
return { ok: false, error: message };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const notPlayingResult = (): CommandResult => ({
|
|
168
|
+
ok: false,
|
|
169
|
+
error: 'not in play mode — start play before using the debug seam',
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
/** Duck-typed structured error (see `structuredErrorResult` above) — a plain
|
|
173
|
+
* `Error` carrying a machine-readable `code` (and optional `data`), the same
|
|
174
|
+
* shape `DebugError`/`InputActionError` already throw. Used by
|
|
175
|
+
* `dispatchBridgeMethod` below for its OWN precondition failures (no debug
|
|
176
|
+
* adapter installed, no InputManager wired) so they survive
|
|
177
|
+
* `structuredErrorResult` exactly like an engine-thrown error would. */
|
|
178
|
+
function bridgeError(code: string, message: string, data?: Record<string, unknown>): Error {
|
|
179
|
+
const err = new Error(message) as Error & { code: string; data?: unknown };
|
|
180
|
+
err.code = code;
|
|
181
|
+
if (data) err.data = data;
|
|
182
|
+
return err;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* #140 — the generic session-wire dispatch table: the SAME method-name
|
|
187
|
+
* surface `window.__vgai` exposes (`@engine/runtime/debug-bridge`'s
|
|
188
|
+
* `VgaiDebugHandle` — `state`/`stateAll`/`providers`/`commands`/`events`/
|
|
189
|
+
* `snapshot`/`invoke`/`runTicks`/`input.*`), built from the SAME
|
|
190
|
+
* registry-backed accessors this file's own `list-gameplay-state`/
|
|
191
|
+
* `inject-input`/`run-ticks` cases already use. This is what makes the
|
|
192
|
+
* `bridge-call` relay op (below) a session-generic primitive rather than a
|
|
193
|
+
* probe-specific one — any client of the live session (the `@vgai/probe`
|
|
194
|
+
* `RelayTransport`, a future one-shot `vgai game state/call/hold` REPL) can
|
|
195
|
+
* issue `{method, callArgs}` against it with no dependency on a probe run's
|
|
196
|
+
* own lifecycle/env, and get byte-identical dispatch to the page-transport's
|
|
197
|
+
* `page.evaluate(bridgeCallInPage)` (D17) — without requiring the editor tab
|
|
198
|
+
* to be loaded with `?vgai-debug=1` (that gate exists for the bridge's own
|
|
199
|
+
* production exposure; this relay is already local-loopback-only — see
|
|
200
|
+
* `editor-server.ts`'s CSRF/origin guard).
|
|
201
|
+
*/
|
|
202
|
+
/** #146 — uncaught page errors from the CURRENT play run, for the relay
|
|
203
|
+
* `snapshot`. Reads the editor console's 'runtime'-source error entries
|
|
204
|
+
* (fed by ViewportPanel's window error/unhandledrejection capture — the
|
|
205
|
+
* same events, same message format as `window.__vgai`'s own pageErrors
|
|
206
|
+
* ring buffer), fenced to entries logged at/after play start. Same 100-cap
|
|
207
|
+
* as the bridge (`PAGE_ERROR_CAP`), keeping the most recent. */
|
|
208
|
+
function collectPageErrors(): string[] {
|
|
209
|
+
const startedAt = getPlayStartedAt();
|
|
210
|
+
if (startedAt === null) return [];
|
|
211
|
+
return editorConsole
|
|
212
|
+
.getEntries()
|
|
213
|
+
.filter((e) => e.level === 'error' && e.source === 'runtime' && e.timestamp >= startedAt)
|
|
214
|
+
.slice(-100)
|
|
215
|
+
.map((e) => (e.count > 1 ? `${e.message} (×${e.count})` : e.message));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function dispatchBridgeMethod(method: string, callArgs: unknown[]): Promise<unknown> {
|
|
219
|
+
const debug = getActiveSystems().debug;
|
|
220
|
+
const requireDebug = () => {
|
|
221
|
+
if (!debug) {
|
|
222
|
+
throw bridgeError(
|
|
223
|
+
'DEBUG_ADAPTER_UNAVAILABLE',
|
|
224
|
+
`session wire: "${method}" — this game exposes no debug adapter`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
return debug;
|
|
228
|
+
};
|
|
229
|
+
switch (method) {
|
|
230
|
+
case 'state':
|
|
231
|
+
return requireDebug().state(callArgs[0] as string);
|
|
232
|
+
case 'stateAll':
|
|
233
|
+
return requireDebug().stateAll();
|
|
234
|
+
case 'providers':
|
|
235
|
+
return requireDebug().providers();
|
|
236
|
+
case 'commands':
|
|
237
|
+
return requireDebug().commands();
|
|
238
|
+
case 'events':
|
|
239
|
+
return requireDebug().events(
|
|
240
|
+
callArgs[0] as number | undefined,
|
|
241
|
+
callArgs[1] as number | undefined,
|
|
242
|
+
);
|
|
243
|
+
case 'snapshot': {
|
|
244
|
+
const adapter = requireDebug();
|
|
245
|
+
return {
|
|
246
|
+
time: adapter.state('time') as { simSeconds: number; tick: number },
|
|
247
|
+
state: adapter.stateAll(),
|
|
248
|
+
// Run-4 friction #5 — same `sinceTick`/`sinceSeq` contract as the
|
|
249
|
+
// in-page bridge's `snapshot()` (`runtime/debug-bridge.ts`): both
|
|
250
|
+
// doors must fence identically, or a probe running `--in-editor`
|
|
251
|
+
// (this door) vs. standalone (that door) could disagree about which
|
|
252
|
+
// events are "new".
|
|
253
|
+
events: adapter.events(
|
|
254
|
+
callArgs[0] as number | undefined,
|
|
255
|
+
callArgs[1] as number | undefined,
|
|
256
|
+
),
|
|
257
|
+
// #146 (closes the #140 honest-empty gap): the editor page already
|
|
258
|
+
// funnels window error/unhandledrejection into the editor console as
|
|
259
|
+
// level 'error' / source 'runtime' (ViewportPanel's error-capture
|
|
260
|
+
// block) — the SAME events and message format `window.__vgai`'s own
|
|
261
|
+
// handle buffers. Fenced to the current play run via
|
|
262
|
+
// getPlayStartedAt() so a probe never reads a previous run's stale
|
|
263
|
+
// failures, capped at 100 for parity with the bridge's ring buffer.
|
|
264
|
+
pageErrors: collectPageErrors(),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
case 'invoke': {
|
|
268
|
+
const name = callArgs[0] as string;
|
|
269
|
+
const invokeArgs = (callArgs[1] as unknown[]) ?? [];
|
|
270
|
+
return requireDebug().invoke(name, invokeArgs);
|
|
271
|
+
}
|
|
272
|
+
case 'runTicks': {
|
|
273
|
+
const runtime = getPlayRuntimeAccess();
|
|
274
|
+
if (!runtime?.runTicks) {
|
|
275
|
+
throw bridgeError(
|
|
276
|
+
'RUN_TICKS_UNAVAILABLE',
|
|
277
|
+
'this play session has no first-party Game to run ticks on',
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
const n = callArgs[0] as number;
|
|
281
|
+
const opts = callArgs[1] as { render?: 'last' | 'all' | 'none' } | undefined;
|
|
282
|
+
runtime.runTicks(n, opts);
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
case 'input.setVirtualAction':
|
|
286
|
+
case 'input.tapVirtualAction':
|
|
287
|
+
case 'input.clearVirtualActions':
|
|
288
|
+
// Wave-2 bridge↔wire coverage-parity gate (`bridge-wire-parity.test.ts`)
|
|
289
|
+
// closed this trio's gap: `startRecording`/`stopRecording`/`isRecording`
|
|
290
|
+
// exist on the page bridge's `VgaiDebugInputHandle` (`runtime/
|
|
291
|
+
// debug-bridge.ts`) but had no relay case at all — an agent driving the
|
|
292
|
+
// input-trace recorder over the session wire (RelayTransport) instead of
|
|
293
|
+
// `?vgai-debug=1` simply had no door to it. Same single optional
|
|
294
|
+
// trailing `worldId` shape as `clearVirtualActions`.
|
|
295
|
+
case 'input.startRecording':
|
|
296
|
+
case 'input.stopRecording':
|
|
297
|
+
case 'input.isRecording': {
|
|
298
|
+
const runtime = getPlayRuntimeAccess();
|
|
299
|
+
// D15/T-D15.5 (rebase composition with #140): this wire is a THIRD
|
|
300
|
+
// door to the virtual-input target — it must resolve through the SAME
|
|
301
|
+
// `DebugRegistry.getVirtualInputTarget(worldId?)` the page bridge
|
|
302
|
+
// (`window.__vgai.input.*`) and the `inject-input` relay case use, so
|
|
303
|
+
// no two doors can ever disagree about which world an unqualified
|
|
304
|
+
// actuation targets. Each method takes the page bridge's own optional
|
|
305
|
+
// trailing `worldId` (byte-parity with `VgaiDebugInputHandle`); an
|
|
306
|
+
// explicit, unregistered id throws the registry's structured
|
|
307
|
+
// `DEBUG_INPUT_WORLD_NOT_FOUND`, shipped back over the wire as-is.
|
|
308
|
+
const worldId = (
|
|
309
|
+
method === 'input.setVirtualAction'
|
|
310
|
+
? callArgs[2]
|
|
311
|
+
: method === 'input.tapVirtualAction'
|
|
312
|
+
? callArgs[1]
|
|
313
|
+
: callArgs[0]
|
|
314
|
+
) as string | undefined;
|
|
315
|
+
const inputTarget = runtime ? runtime.getInputTarget(worldId) : null;
|
|
316
|
+
if (!inputTarget) {
|
|
317
|
+
throw bridgeError(
|
|
318
|
+
'DEBUG_INPUT_UNAVAILABLE',
|
|
319
|
+
`session wire: ${method}() has no virtual-input target wired — no default threejs ` +
|
|
320
|
+
"world has mounted yet, or this project's mount path never wired one",
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
if (method === 'input.setVirtualAction') {
|
|
324
|
+
return inputTarget.setVirtualAction(
|
|
325
|
+
callArgs[0] as string,
|
|
326
|
+
callArgs[1] as boolean | number | { x: number; y: number },
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
if (method === 'input.tapVirtualAction') {
|
|
330
|
+
return inputTarget.tapVirtualAction(callArgs[0] as string);
|
|
331
|
+
}
|
|
332
|
+
if (method === 'input.clearVirtualActions') {
|
|
333
|
+
inputTarget.clearVirtualActions();
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
if (method === 'input.startRecording') {
|
|
337
|
+
inputTarget.startInputRecording();
|
|
338
|
+
return undefined;
|
|
339
|
+
}
|
|
340
|
+
if (method === 'input.stopRecording') {
|
|
341
|
+
inputTarget.stopInputRecording();
|
|
342
|
+
return undefined;
|
|
343
|
+
}
|
|
344
|
+
return inputTarget.isInputRecording();
|
|
345
|
+
}
|
|
346
|
+
case 'input.scheduleActionAtTick': {
|
|
347
|
+
// Wave-2 bridge↔wire coverage-parity gate: `window.__vgai.input.
|
|
348
|
+
// scheduleActionAtTick` (`runtime/debug-bridge.ts`, D15/T-D15.5) had NO
|
|
349
|
+
// bridge-call case — it was reachable over the relay ONLY via the
|
|
350
|
+
// separate `inject-input` command's `atTick` field (see that case,
|
|
351
|
+
// above), never by this method name. Same `getInputTarget(worldId?)`
|
|
352
|
+
// resolution as the other `input.*` cases; forwards `tick`/`action`/
|
|
353
|
+
// `value` straight through and returns whatever the target returns
|
|
354
|
+
// (`void`, same as the page bridge's own `scheduleActionAtTick`).
|
|
355
|
+
const runtime = getPlayRuntimeAccess();
|
|
356
|
+
const tick = callArgs[0] as number;
|
|
357
|
+
const action = callArgs[1] as string;
|
|
358
|
+
const value = callArgs[2] as boolean | number | { x: number; y: number };
|
|
359
|
+
const worldId = callArgs[3] as string | undefined;
|
|
360
|
+
const inputTarget = runtime ? runtime.getInputTarget(worldId) : null;
|
|
361
|
+
if (!inputTarget) {
|
|
362
|
+
throw bridgeError(
|
|
363
|
+
'DEBUG_INPUT_UNAVAILABLE',
|
|
364
|
+
'session wire: input.scheduleActionAtTick() has no virtual-input target wired — no ' +
|
|
365
|
+
"default threejs world has mounted yet, or this project's mount path never wired one",
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
return inputTarget.scheduleActionAtTick(tick, action, value);
|
|
369
|
+
}
|
|
370
|
+
case 'input.injectPointerDelta':
|
|
371
|
+
case 'input.injectPointerPosition': {
|
|
372
|
+
// Wave-2 pointer-dispatch op: `window.__vgai.input.injectPointerDelta`/
|
|
373
|
+
// `injectPointerPosition` (`runtime/debug-bridge.ts`) had no relay
|
|
374
|
+
// case — this is the HONEST gameplay-seam reading of "pointer
|
|
375
|
+
// dispatch" (docs/SHARED-SESSION-SPEC.md): the engine's existing
|
|
376
|
+
// virtual pointer-injection, never a synthesized DOM mouse event (that
|
|
377
|
+
// boundary belongs to the future playwright shim). Same
|
|
378
|
+
// `getInputTarget(worldId?)` resolution as every other `input.*` case.
|
|
379
|
+
const runtime = getPlayRuntimeAccess();
|
|
380
|
+
const sourceId = callArgs[0] as string;
|
|
381
|
+
const value = callArgs[1] as { x: number; y: number };
|
|
382
|
+
const worldId = callArgs[2] as string | undefined;
|
|
383
|
+
const inputTarget = runtime ? runtime.getInputTarget(worldId) : null;
|
|
384
|
+
if (!inputTarget) {
|
|
385
|
+
throw bridgeError(
|
|
386
|
+
'DEBUG_INPUT_UNAVAILABLE',
|
|
387
|
+
`session wire: ${method}() has no virtual-input target wired — no default threejs ` +
|
|
388
|
+
"world has mounted yet, or this project's mount path never wired one",
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
if (method === 'input.injectPointerDelta') {
|
|
392
|
+
inputTarget.injectPointerDelta(sourceId, value);
|
|
393
|
+
} else {
|
|
394
|
+
inputTarget.injectPointerPosition(sourceId, value);
|
|
395
|
+
}
|
|
396
|
+
return undefined;
|
|
397
|
+
}
|
|
398
|
+
case 'holdFor': {
|
|
399
|
+
// #140/D17: byte-identical semantics to `window.__vgai.holdFor` (door
|
|
400
|
+
// a, `runtime/debug-bridge.ts`) — same `getInputTarget(worldId?)`
|
|
401
|
+
// resolution as the `input.*` cases above, same `waitForHoldBudget`
|
|
402
|
+
// poll/stall loop (imported, not hand-copied, so the two doors can
|
|
403
|
+
// never silently drift apart), reached through `requireDebug().state
|
|
404
|
+
// ('time')` instead of a `DebugRegistry` (this relay only has
|
|
405
|
+
// `getActiveSystems().debug`, not the registry itself).
|
|
406
|
+
const runtime = getPlayRuntimeAccess();
|
|
407
|
+
const worldId = callArgs[2] as string | undefined;
|
|
408
|
+
const inputTarget = runtime ? runtime.getInputTarget(worldId) : null;
|
|
409
|
+
if (!inputTarget) {
|
|
410
|
+
throw bridgeError(
|
|
411
|
+
'DEBUG_INPUT_UNAVAILABLE',
|
|
412
|
+
'session wire: holdFor() has no virtual-input target wired — no default threejs ' +
|
|
413
|
+
"world has mounted yet, or this project's mount path never wired one",
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
const action = callArgs[0] as string;
|
|
417
|
+
const simSeconds = callArgs[1] as number;
|
|
418
|
+
const setResult = inputTarget.setVirtualAction(action, true);
|
|
419
|
+
if (!setResult.delivered) {
|
|
420
|
+
inputTarget.clearVirtualActions();
|
|
421
|
+
return { delivered: false, reason: setResult.reason };
|
|
422
|
+
}
|
|
423
|
+
const { stalled } = await waitForHoldBudget(
|
|
424
|
+
requireDebug(),
|
|
425
|
+
simSeconds,
|
|
426
|
+
runtime?.runTicks ? (n) => runtime.runTicks?.(n, { render: 'last' }) : undefined,
|
|
427
|
+
);
|
|
428
|
+
inputTarget.clearVirtualActions();
|
|
429
|
+
return stalled
|
|
430
|
+
? { delivered: false, reason: 'play stopped during hold' }
|
|
431
|
+
: { delivered: true };
|
|
432
|
+
}
|
|
433
|
+
default:
|
|
434
|
+
throw bridgeError('UNKNOWN_BRIDGE_METHOD', `session wire: unknown bridge method "${method}"`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** `bridge-call` relay op handler — session not playing is the one
|
|
439
|
+
* precondition checked up front (mirrors every other debug-seam case in
|
|
440
|
+
* this file); everything else is `dispatchBridgeMethod`'s own structured
|
|
441
|
+
* failure, caught here and shipped back via `structuredErrorResult` so
|
|
442
|
+
* `code`/`data` survive the relay round trip byte-equivalent to the
|
|
443
|
+
* page-transport's `unwrap()` path (`packages/vgai-probe/src/client.ts`). */
|
|
444
|
+
async function handleBridgeCall(cmd: EditorCommand): Promise<CommandResult> {
|
|
445
|
+
if (!isPlayModeActive()) return notPlayingResult();
|
|
446
|
+
const method = cmd['method'] as string;
|
|
447
|
+
const callArgs = (cmd['callArgs'] as unknown[] | undefined) ?? [];
|
|
448
|
+
try {
|
|
449
|
+
const result = await dispatchBridgeMethod(method, callArgs);
|
|
450
|
+
return { ok: true, data: { result } };
|
|
451
|
+
} catch (err) {
|
|
452
|
+
return structuredErrorResult(err);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** `bridge-screenshot` relay op handler — captures the live play-mode game
|
|
457
|
+
* view and returns it in the same `{base64, mimeType}` shape
|
|
458
|
+
* `capture-viewport` already uses. #146: the capture is now the FULL game
|
|
459
|
+
* stack — canvas(es) plus the DOM UI layers (React adapter roots,
|
|
460
|
+
* react-world layers) composited via `capturePlayComposite` — because a
|
|
461
|
+
* canvas-only frame of a game whose score/health lives in the HUD is not
|
|
462
|
+
* "what the game looks like". When the composite leg can't run (no
|
|
463
|
+
* container, no DOM Image/XMLSerializer — e.g. the node-side relay tests —
|
|
464
|
+
* or a rasterization failure), it degrades to the original canvas-only
|
|
465
|
+
* `toDataURL` capture (readable because every runtime-mounted canvas sets
|
|
466
|
+
* `preserveDrawingBuffer: true` — `create-runtime.ts`, D5 §1/§4), marked
|
|
467
|
+
* honestly with `composite: false`. A caller (`RelayTransport.screenshot`
|
|
468
|
+
* in `@vgai/probe`) writes the PNG to disk — this relay op stays a pure
|
|
469
|
+
* "hand back the pixels" primitive, matching `bridge-call`'s own
|
|
470
|
+
* session-generic, probe-lifecycle-free shape. */
|
|
471
|
+
async function handleBridgeScreenshot(cmd: EditorCommand): Promise<CommandResult> {
|
|
472
|
+
if (!isPlayModeActive()) return notPlayingResult();
|
|
473
|
+
const canvas = getPlayCanvas();
|
|
474
|
+
// A paused canvas is provably stale, but a React-only root has no canvas:
|
|
475
|
+
// its DOM is self-driven and `capturePlayComposite` has its own bounded
|
|
476
|
+
// hidden-tab paint fallback. Do not apply a canvas freshness rule to the
|
|
477
|
+
// DOM-only adapter surface.
|
|
478
|
+
const runtime = getPlayRuntimeAccess();
|
|
479
|
+
if (canvas && runtime?.loop.liveness === 'hidden-paused') {
|
|
480
|
+
if (cmd['refreshHiddenFrame'] === true && runtime.runTicks) {
|
|
481
|
+
try {
|
|
482
|
+
runtime.runTicks(1, { render: 'last' });
|
|
483
|
+
} catch (err) {
|
|
484
|
+
return structuredErrorResult(err);
|
|
485
|
+
}
|
|
486
|
+
} else {
|
|
487
|
+
return {
|
|
488
|
+
ok: false,
|
|
489
|
+
error:
|
|
490
|
+
'cannot capture a fresh game frame while the editor tab is hidden — focus/open the editor tab and retry, or request a deterministic hidden-frame refresh',
|
|
491
|
+
data: { code: 'BRIDGE_SCREENSHOT_STALE' },
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
const container = getGameContainer();
|
|
496
|
+
if (
|
|
497
|
+
container &&
|
|
498
|
+
typeof Image !== 'undefined' &&
|
|
499
|
+
typeof XMLSerializer !== 'undefined' &&
|
|
500
|
+
typeof container.getBoundingClientRect === 'function'
|
|
501
|
+
) {
|
|
502
|
+
try {
|
|
503
|
+
const composite = await capturePlayComposite(container);
|
|
504
|
+
if (composite.layers.canvases === 0 && composite.layers.domOverlays === 0) {
|
|
505
|
+
throw new Error('composite screenshot: no game layers');
|
|
506
|
+
}
|
|
507
|
+
return {
|
|
508
|
+
ok: true,
|
|
509
|
+
data: {
|
|
510
|
+
base64: composite.base64,
|
|
511
|
+
mimeType: composite.mimeType,
|
|
512
|
+
composite: true,
|
|
513
|
+
layers: composite.layers,
|
|
514
|
+
},
|
|
515
|
+
};
|
|
516
|
+
} catch {
|
|
517
|
+
// Degrade to the canvas-only leg below — a HUD-less capture beats no
|
|
518
|
+
// capture, and `composite: false` keeps the degradation visible.
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
// A native React game has no canvas by design. The composite leg above is
|
|
522
|
+
// therefore the PRIMARY capture path for a React-only root, not an
|
|
523
|
+
// enhancement after a canvas precondition. Only fail once both the full
|
|
524
|
+
// game-stack capture and the canvas fallback are unavailable.
|
|
525
|
+
if (!canvas) {
|
|
526
|
+
return {
|
|
527
|
+
ok: false,
|
|
528
|
+
error: 'no play-mode game surface is mounted yet',
|
|
529
|
+
data: { code: 'BRIDGE_SCREENSHOT_UNAVAILABLE' },
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
try {
|
|
533
|
+
const dataUrl = canvas.toDataURL('image/png');
|
|
534
|
+
const comma = dataUrl.indexOf(',');
|
|
535
|
+
const base64 = comma >= 0 ? dataUrl.slice(comma + 1) : dataUrl;
|
|
536
|
+
return { ok: true, data: { base64, mimeType: 'image/png', composite: false } };
|
|
537
|
+
} catch (err) {
|
|
538
|
+
// e.g. a tainted canvas (a cross-origin texture loaded with no CORS
|
|
539
|
+
// headers, which WebGL refuses to read back even with
|
|
540
|
+
// preserveDrawingBuffer:true) — a distinct, rarer failure from "no
|
|
541
|
+
// canvas at all" above, surfaced honestly rather than swallowed.
|
|
542
|
+
return {
|
|
543
|
+
ok: false,
|
|
544
|
+
error: `bridge-screenshot: canvas capture failed — ${err instanceof Error ? err.message : String(err)}`,
|
|
545
|
+
data: { code: 'BRIDGE_SCREENSHOT_UNAVAILABLE' },
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* `page-script` relay op handler — Wave-2 "the playwright shim in the editor
|
|
552
|
+
* page + script-mode wire op" (`docs/SHARED-SESSION-SPEC.md` §7). Standalone
|
|
553
|
+
* command (like `bridge-screenshot`), NOT a `bridge-call` method: this is
|
|
554
|
+
* editor-page UI automation, not the game-debug seam, so it deliberately
|
|
555
|
+
* stays outside `dispatchBridgeMethod`'s switch — the bridge↔wire parity gate
|
|
556
|
+
* (`bridge-wire-parity.test.ts`) only walks THAT switch, and must never need
|
|
557
|
+
* to know about this op.
|
|
558
|
+
*
|
|
559
|
+
* `cmd['src']` is a UI-automation step's `Function.prototype.toString()`
|
|
560
|
+
* source (`@vgai/probe`'s `RelayTransport.runPageScript` — see that file's
|
|
561
|
+
* doc comment for the client-facing half of the wire contract). Reconstructed
|
|
562
|
+
* here with `new Function` and run against a fresh `createPageShim` rooted at
|
|
563
|
+
* the SAME game container `bridge-screenshot`'s composite capture uses
|
|
564
|
+
* (`getGameContainer` — never editor chrome). `PageShimUnsupportedError`
|
|
565
|
+
* (thrown the instant the step touches an unimplemented `Page`/`Locator`
|
|
566
|
+
* member) is distinguished from any other step failure so a caller can tell
|
|
567
|
+
* "this shim doesn't support that yet" apart from "the step's own assertion
|
|
568
|
+
* failed" — both are structured failures, never a hang or a silent
|
|
569
|
+
* mis-report.
|
|
570
|
+
*/
|
|
571
|
+
async function handlePageScript(cmd: EditorCommand): Promise<CommandResult> {
|
|
572
|
+
if (!isPlayModeActive()) return notPlayingResult();
|
|
573
|
+
const src = cmd['src'];
|
|
574
|
+
if (typeof src !== 'string') {
|
|
575
|
+
return { ok: false, error: 'page-script requires a string "src" (the step\'s toString())' };
|
|
576
|
+
}
|
|
577
|
+
const container = getGameContainer();
|
|
578
|
+
if (!container) {
|
|
579
|
+
return {
|
|
580
|
+
ok: false,
|
|
581
|
+
error: 'page-script: no play-mode game container is mounted yet',
|
|
582
|
+
data: { code: 'PAGE_SCRIPT_ERROR' },
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
let step: (page: unknown) => unknown;
|
|
586
|
+
try {
|
|
587
|
+
// Reconstructing a wire-carried step's source is the documented wire
|
|
588
|
+
// contract (Wave-2 decision 2) — the same limitation class as
|
|
589
|
+
// Playwright's own evaluate serialization: closures over anything
|
|
590
|
+
// outside the step's own body do NOT survive the trip.
|
|
591
|
+
step = new Function('page', `return (${src})(page)`) as (page: unknown) => unknown;
|
|
592
|
+
} catch (err) {
|
|
593
|
+
return {
|
|
594
|
+
ok: false,
|
|
595
|
+
error: `page-script: failed to reconstruct the step function from source — ${
|
|
596
|
+
err instanceof Error ? err.message : String(err)
|
|
597
|
+
}`,
|
|
598
|
+
data: { code: 'PAGE_SCRIPT_ERROR' },
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
const page = createPageShim(container);
|
|
602
|
+
try {
|
|
603
|
+
const result = await step(page);
|
|
604
|
+
return { ok: true, data: { result: result === undefined ? null : result } };
|
|
605
|
+
} catch (err) {
|
|
606
|
+
if (err instanceof PageShimUnsupportedError) {
|
|
607
|
+
return {
|
|
608
|
+
ok: false,
|
|
609
|
+
error: err.message,
|
|
610
|
+
data: { code: 'PAGE_SHIM_UNSUPPORTED', member: err.member, supported: [...err.supported] },
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
return {
|
|
614
|
+
ok: false,
|
|
615
|
+
error: `page-script: step threw — ${err instanceof Error ? err.message : String(err)}`,
|
|
616
|
+
data: { code: 'PAGE_SCRIPT_ERROR' },
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
82
621
|
/** Validate the current scene document against the Zod schema. */
|
|
83
622
|
function validateCurrentScene(store: EditorStore): CommandResult {
|
|
84
623
|
const doc = store.getDocument();
|
|
@@ -93,6 +632,47 @@ function validateCurrentScene(store: EditorStore): CommandResult {
|
|
|
93
632
|
}
|
|
94
633
|
}
|
|
95
634
|
|
|
635
|
+
async function handleAssetPreviewCommand(
|
|
636
|
+
store: EditorStore,
|
|
637
|
+
cmd: EditorCommand,
|
|
638
|
+
): Promise<CommandResult> {
|
|
639
|
+
const assetPath = cmd['assetPath'];
|
|
640
|
+
const entityId = cmd['entityId'];
|
|
641
|
+
if ((typeof assetPath === 'string') === (typeof entityId === 'string')) {
|
|
642
|
+
return {
|
|
643
|
+
ok: false,
|
|
644
|
+
error: 'capture-asset-preview requires exactly one of assetPath or entityId.',
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
const width = cmd['width'];
|
|
648
|
+
const height = cmd['height'];
|
|
649
|
+
const background = cmd['background'];
|
|
650
|
+
if (
|
|
651
|
+
(width !== undefined && typeof width !== 'number') ||
|
|
652
|
+
(height !== undefined && typeof height !== 'number') ||
|
|
653
|
+
(background !== undefined && background !== 'neutral' && background !== 'transparent')
|
|
654
|
+
) {
|
|
655
|
+
return { ok: false, error: 'Invalid asset preview dimensions or background.' };
|
|
656
|
+
}
|
|
657
|
+
if (typeof entityId === 'string' && !store.objectMap.has(entityId)) {
|
|
658
|
+
return { ok: false, error: `Entity not found: ${entityId}` };
|
|
659
|
+
}
|
|
660
|
+
try {
|
|
661
|
+
const options = {
|
|
662
|
+
...(typeof width === 'number' ? { width } : {}),
|
|
663
|
+
...(typeof height === 'number' ? { height } : {}),
|
|
664
|
+
...(background ? { background: background as AssetPreviewBackground } : {}),
|
|
665
|
+
};
|
|
666
|
+
const capture =
|
|
667
|
+
typeof assetPath === 'string'
|
|
668
|
+
? await captureModelAssetPreview(assetPath, options)
|
|
669
|
+
: captureObjectAssetPreview(store.objectMap.get(entityId as string)!, options);
|
|
670
|
+
return { ok: true, data: { ...capture } };
|
|
671
|
+
} catch (err) {
|
|
672
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
96
676
|
/**
|
|
97
677
|
* Exported (alongside `collectState` above) so unit tests can dispatch
|
|
98
678
|
* commands directly against a headless `EditorStore`, with no SSE/server
|
|
@@ -107,6 +687,21 @@ export async function handleCommand(
|
|
|
107
687
|
switch (cmd['type']) {
|
|
108
688
|
// Play control
|
|
109
689
|
case 'play': {
|
|
690
|
+
// Fix for the ghost-runtime bug — deliberately no `isPlayModeActive()`
|
|
691
|
+
// guard here: `play` on an already-playing (or still-booting) session
|
|
692
|
+
// is a RESTART, not a no-op, matching the SDK's own documented contract
|
|
693
|
+
// (`play.start`'s summary: "Start (or restart) play mode" —
|
|
694
|
+
// `packages/vgai-sdk/src/play/lifecycle-operations.ts`). What used to
|
|
695
|
+
// make a restart unsafe was `enterPlayMode()` having no reentrancy
|
|
696
|
+
// guard for the async-boot window before it assigns `_session` — two
|
|
697
|
+
// overlapping calls ran their entire prologues concurrently and
|
|
698
|
+
// corrupted shared module state (proven by a real console-patching
|
|
699
|
+
// stack overflow — see `play-mode-reentry-ghost.test.ts`). That is
|
|
700
|
+
// fixed at the source in `enterPlayMode()` itself (a FIFO queue
|
|
701
|
+
// serializes every invocation), so every caller of THIS case — CLI,
|
|
702
|
+
// SDK, a UI Play-button click racing a relayed command — gets a safe,
|
|
703
|
+
// clean restart for free without needing a guard here too.
|
|
704
|
+
//
|
|
110
705
|
// Validate before play — enterPlayMode() also validates, but we need
|
|
111
706
|
// the error message to flow back to the SDK/CLI.
|
|
112
707
|
const validation = validateCurrentScene(store);
|
|
@@ -114,7 +709,11 @@ export async function handleCommand(
|
|
|
114
709
|
return validation;
|
|
115
710
|
}
|
|
116
711
|
try {
|
|
117
|
-
|
|
712
|
+
// D15/T-D15.6 (objection-4 fix) — `vgai play --seed <n>` relays as
|
|
713
|
+
// `cmd['seed']`; `enterPlayMode`'s explicit-config seed leg (beats
|
|
714
|
+
// manifest.determinism.defaultSeed/?vgai-seed=).
|
|
715
|
+
const seed = cmd['seed'];
|
|
716
|
+
await enterPlayMode(typeof seed === 'number' ? seed : undefined);
|
|
118
717
|
} catch (err) {
|
|
119
718
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
120
719
|
}
|
|
@@ -178,15 +777,35 @@ export async function handleCommand(
|
|
|
178
777
|
case 'viewport-tab':
|
|
179
778
|
store.setActiveViewportTab(cmd['tab'] as 'scene' | 'game');
|
|
180
779
|
break;
|
|
181
|
-
|
|
182
|
-
|
|
780
|
+
// W2: the legacy right-rail tab commands keep their key vocabulary but
|
|
781
|
+
// now drive the CENTER workspace documents (`asset-documents.tsx`).
|
|
782
|
+
// '__inspector__' ("show the Inspector") is a no-op now: the right rail
|
|
783
|
+
// ALWAYS shows the Inspector — nothing displaces it anymore (§10.3).
|
|
784
|
+
case 'active-tab': {
|
|
785
|
+
const key = cmd['key'] as string;
|
|
786
|
+
if (key !== '__inspector__') activateWorkspaceDocument(key);
|
|
183
787
|
break;
|
|
788
|
+
}
|
|
184
789
|
case 'open-asset-tab':
|
|
185
|
-
store
|
|
790
|
+
openAssetDocument(store, cmd['path'] as string, cmd['kind'] as AssetKind);
|
|
186
791
|
break;
|
|
187
|
-
case 'close-asset-tab':
|
|
188
|
-
store
|
|
792
|
+
case 'close-asset-tab': {
|
|
793
|
+
// Legacy key vocabulary: the deleted store-era `closeAssetTab` only
|
|
794
|
+
// ever acted on asset tabs and no-op'd for anything else (scene/game/
|
|
795
|
+
// story keys included). `closeWorkspaceDocument` itself has no such
|
|
796
|
+
// guard — it closes ANY registered id, pinned or not (`closeable:
|
|
797
|
+
// false` is only a UI-affordance rule, not a registry invariant; see
|
|
798
|
+
// workspace-document-registry.ts) — so this command must keep the
|
|
799
|
+
// no-op itself. `isAssetDocumentId` is exactly the legacy asset key
|
|
800
|
+
// vocabulary check (project asset path / `asset-lab:entity:<id>` /
|
|
801
|
+
// `online:<source>:<id>` — see asset-documents.tsx), so gating on it
|
|
802
|
+
// blocks the pinned `workspace:scene`/`workspace:game` ids (and any
|
|
803
|
+
// story `story:<path>#<name>` id) while preserving the real behavior
|
|
804
|
+
// for actual asset tabs.
|
|
805
|
+
const key = cmd['key'] as string;
|
|
806
|
+
if (isAssetDocumentId(key)) closeWorkspaceDocument(key);
|
|
189
807
|
break;
|
|
808
|
+
}
|
|
190
809
|
case 'toggle-command-palette':
|
|
191
810
|
store.toggleCommandPalette();
|
|
192
811
|
break;
|
|
@@ -194,7 +813,10 @@ export async function handleCommand(
|
|
|
194
813
|
window.dispatchEvent(new CustomEvent('editor:toggle-console'));
|
|
195
814
|
break;
|
|
196
815
|
case 'show-build':
|
|
197
|
-
|
|
816
|
+
// W3 build split: the config surface is the Build Profiles CENTER
|
|
817
|
+
// document (build-documents.tsx); the log streams into the Build
|
|
818
|
+
// Output utility. Same command vocabulary, new (semantic) home.
|
|
819
|
+
openBuildProfilesDocument(store);
|
|
198
820
|
break;
|
|
199
821
|
case 'capture-viewport': {
|
|
200
822
|
// Fresh, unthrottled on-demand capture (see EditorStore.captureViewportImage's
|
|
@@ -213,6 +835,8 @@ export async function handleCommand(
|
|
|
213
835
|
const base64 = comma >= 0 ? dataUrl.slice(comma + 1) : dataUrl;
|
|
214
836
|
return { ok: true, data: { base64, mimeType: 'image/png' } };
|
|
215
837
|
}
|
|
838
|
+
case 'capture-asset-preview':
|
|
839
|
+
return handleAssetPreviewCommand(store, cmd);
|
|
216
840
|
|
|
217
841
|
// Scene loading
|
|
218
842
|
case 'open-scene': {
|
|
@@ -266,16 +890,242 @@ export async function handleCommand(
|
|
|
266
890
|
if (store.snapEnabled !== (cmd['enabled'] as boolean)) store.toggleSnap();
|
|
267
891
|
break;
|
|
268
892
|
|
|
893
|
+
// Debug seam relay (Wave 5, docs/SYNTHETIC-PLAYER-SPEC.md §3.5): all six
|
|
894
|
+
// cases read/actuate the LIVE play session's game-scoped debug registry
|
|
895
|
+
// (`SystemAdapters.debug`, installed by `setActiveSystems` on play enter)
|
|
896
|
+
// or its InputManager/GameLoop (via `getPlayRuntimeAccess`). A stopped
|
|
897
|
+
// session answers with a structured failure, never a fabricated success;
|
|
898
|
+
// a running game that exposes NO debug adapter (a non-first-party mount)
|
|
899
|
+
// degrades to the honest empty/null shape.
|
|
900
|
+
case 'list-gameplay-state': {
|
|
901
|
+
const debug = getActiveSystems().debug;
|
|
902
|
+
if (!debug) {
|
|
903
|
+
return isPlayModeActive() ? { ok: true, data: { providers: [] } } : notPlayingResult();
|
|
904
|
+
}
|
|
905
|
+
return { ok: true, data: { providers: debug.providers() } };
|
|
906
|
+
}
|
|
907
|
+
case 'inspect-gameplay-state': {
|
|
908
|
+
const debug = getActiveSystems().debug;
|
|
909
|
+
if (!debug) {
|
|
910
|
+
return isPlayModeActive() ? { ok: true, data: { state: null } } : notPlayingResult();
|
|
911
|
+
}
|
|
912
|
+
const keys = cmd['keys'] as string[] | undefined;
|
|
913
|
+
try {
|
|
914
|
+
const state: Record<string, unknown> = {};
|
|
915
|
+
if (keys && keys.length > 0) {
|
|
916
|
+
for (const key of keys) state[key] = debug.state(key);
|
|
917
|
+
} else {
|
|
918
|
+
Object.assign(state, debug.stateAll());
|
|
919
|
+
}
|
|
920
|
+
return { ok: true, data: { state } };
|
|
921
|
+
} catch (err) {
|
|
922
|
+
return structuredErrorResult(err);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
case 'list-debug-commands': {
|
|
926
|
+
const debug = getActiveSystems().debug;
|
|
927
|
+
if (!debug) {
|
|
928
|
+
return isPlayModeActive() ? { ok: true, data: { commands: [] } } : notPlayingResult();
|
|
929
|
+
}
|
|
930
|
+
return { ok: true, data: { commands: debug.commands() } };
|
|
931
|
+
}
|
|
932
|
+
case 'invoke-debug-command': {
|
|
933
|
+
const debug = getActiveSystems().debug;
|
|
934
|
+
if (!debug) {
|
|
935
|
+
if (!isPlayModeActive()) return notPlayingResult();
|
|
936
|
+
// Honest not-registered shape: no adapter means NOTHING is registered.
|
|
937
|
+
return {
|
|
938
|
+
ok: false,
|
|
939
|
+
error: `debug: no command registered under "${cmd['name']}" — this game exposes no debug adapter`,
|
|
940
|
+
data: { code: 'DEBUG_COMMAND_NOT_REGISTERED', registered: [] },
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
try {
|
|
944
|
+
const result = await debug.invoke(cmd['name'] as string, (cmd['args'] as unknown[]) ?? []);
|
|
945
|
+
return { ok: true, data: { result } };
|
|
946
|
+
} catch (err) {
|
|
947
|
+
return structuredErrorResult(err);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
case 'inject-input': {
|
|
951
|
+
const runtime = getPlayRuntimeAccess();
|
|
952
|
+
if (!runtime) {
|
|
953
|
+
return { ok: false, error: 'not in play mode — start play before injecting input' };
|
|
954
|
+
}
|
|
955
|
+
// D15/T-D15.5 (review objection 2's fix) — resolved through the SAME
|
|
956
|
+
// per-world `DebugRegistry.getVirtualInputTarget(worldId?)` the debug
|
|
957
|
+
// bridge's `window.__vgai.input.*` calls, instead of the old
|
|
958
|
+
// `Game.input` (always the DEFAULT world only) — an explicit
|
|
959
|
+
// `cmd['worldId']` reaches that world specifically; omitted resolves
|
|
960
|
+
// to the same default both doors now share. Throws
|
|
961
|
+
// `DEBUG_INPUT_WORLD_NOT_FOUND` (caught below) for an explicit,
|
|
962
|
+
// unregistered world id.
|
|
963
|
+
let input: ReturnType<typeof runtime.getInputTarget>;
|
|
964
|
+
try {
|
|
965
|
+
input = runtime.getInputTarget(cmd['worldId'] as string | undefined);
|
|
966
|
+
} catch (err) {
|
|
967
|
+
return structuredErrorResult(err);
|
|
968
|
+
}
|
|
969
|
+
if (!input) {
|
|
970
|
+
return {
|
|
971
|
+
ok: false,
|
|
972
|
+
error: 'this play session has no first-party InputManager to inject into',
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
try {
|
|
976
|
+
// Action-level is primary (frozen decision, spec §3.2): the honest
|
|
977
|
+
// gated path — a gated actuation surfaces {delivered:false, reason},
|
|
978
|
+
// never a false ok. The four legacy kinds map to the persistent
|
|
979
|
+
// test-source injectors (read back only through declared test_*
|
|
980
|
+
// bindings, so `delivered` here means "the source was written").
|
|
981
|
+
if (cmd['kind'] === 'action') {
|
|
982
|
+
const atTick = cmd['atTick'];
|
|
983
|
+
// D15/T-D15.5 — `atTick` defers to InputManager.scheduleActionAtTick
|
|
984
|
+
// instead of an immediate setVirtualAction. scheduleActionAtTick
|
|
985
|
+
// throws InputActionError/a valueType mismatch (same as
|
|
986
|
+
// setVirtualAction) or InputTickError ('TICK_ALREADY_PASSED',
|
|
987
|
+
// data.currentTick) — all caught by this case's outer try/catch
|
|
988
|
+
// below via structuredErrorResult.
|
|
989
|
+
if (typeof atTick === 'number') {
|
|
990
|
+
input.scheduleActionAtTick(
|
|
991
|
+
atTick,
|
|
992
|
+
cmd['action'] as string,
|
|
993
|
+
cmd['value'] as boolean | number | { x: number; y: number },
|
|
994
|
+
);
|
|
995
|
+
return { ok: true, data: { scheduled: true, tick: atTick } };
|
|
996
|
+
}
|
|
997
|
+
const outcome = input.setVirtualAction(
|
|
998
|
+
cmd['action'] as string,
|
|
999
|
+
cmd['value'] as boolean | number | { x: number; y: number },
|
|
1000
|
+
);
|
|
1001
|
+
return { ok: true, data: outcome };
|
|
1002
|
+
}
|
|
1003
|
+
const sourceId = cmd['sourceId'] as string;
|
|
1004
|
+
switch (cmd['kind']) {
|
|
1005
|
+
case 'axis':
|
|
1006
|
+
input.injectAxis(sourceId, cmd['value'] as number);
|
|
1007
|
+
break;
|
|
1008
|
+
case 'vector2':
|
|
1009
|
+
input.injectVector2(sourceId, cmd['value'] as { x: number; y: number });
|
|
1010
|
+
break;
|
|
1011
|
+
case 'pointerDelta':
|
|
1012
|
+
input.injectPointerDelta(sourceId, cmd['value'] as { x: number; y: number });
|
|
1013
|
+
break;
|
|
1014
|
+
case 'pointerPosition':
|
|
1015
|
+
input.injectPointerPosition(sourceId, cmd['value'] as { x: number; y: number });
|
|
1016
|
+
break;
|
|
1017
|
+
default:
|
|
1018
|
+
return { ok: false, error: `unknown injection kind "${cmd['kind']}"` };
|
|
1019
|
+
}
|
|
1020
|
+
return { ok: true, data: { delivered: true } };
|
|
1021
|
+
} catch (err) {
|
|
1022
|
+
return structuredErrorResult(err);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
case 'set-time-scale': {
|
|
1026
|
+
const runtime = getPlayRuntimeAccess();
|
|
1027
|
+
if (!runtime) {
|
|
1028
|
+
return { ok: false, error: 'not in play mode — start play before setting time-scale' };
|
|
1029
|
+
}
|
|
1030
|
+
runtime.loop.timeScale = cmd['timeScale'] as number;
|
|
1031
|
+
// Read back after the loop's own [0,8] clamp so the caller sees the
|
|
1032
|
+
// value actually applied.
|
|
1033
|
+
return { ok: true, data: { timeScale: runtime.loop.timeScale } };
|
|
1034
|
+
}
|
|
1035
|
+
// D15/T-D15.6 — the `play.seed.set` relay case (`docs/D15-DETERMINISM-
|
|
1036
|
+
// DESIGN.md` §2.d): reaches the live session's `ctx.random.reseed(seed)`
|
|
1037
|
+
// through `getPlayRuntimeAccess()`'s `random` accessor. Gated on
|
|
1038
|
+
// `determinismDeclared` (the running project's manifest
|
|
1039
|
+
// `determinism.seededRandom`, per T4.1: reseeding a project with no
|
|
1040
|
+
// documented determinism contract would silently imply one exists) —
|
|
1041
|
+
// the structured `DETERMINISM_NOT_DECLARED` marker the SDK maps to its
|
|
1042
|
+
// own declared error code.
|
|
1043
|
+
case 'set-seed': {
|
|
1044
|
+
const runtime = getPlayRuntimeAccess();
|
|
1045
|
+
if (!runtime) {
|
|
1046
|
+
return { ok: false, error: 'not in play mode — start play before setting the seed' };
|
|
1047
|
+
}
|
|
1048
|
+
if (!runtime.determinismDeclared) {
|
|
1049
|
+
return {
|
|
1050
|
+
ok: false,
|
|
1051
|
+
error:
|
|
1052
|
+
"this project's manifest does not declare determinism.seededRandom — " +
|
|
1053
|
+
'play.seed.set has no documented contract to reseed against (D15)',
|
|
1054
|
+
data: { code: 'DETERMINISM_NOT_DECLARED' },
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
if (!runtime.random) {
|
|
1058
|
+
return {
|
|
1059
|
+
ok: false,
|
|
1060
|
+
error: 'this play session has no first-party ctx.random to reseed',
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
runtime.random.reseed(cmd['seed'] as number);
|
|
1064
|
+
// Read back the live, post-reseed value (mirrors set-time-scale's own
|
|
1065
|
+
// read-after-apply convention immediately above).
|
|
1066
|
+
return { ok: true, data: { seed: runtime.random.seed } };
|
|
1067
|
+
}
|
|
1068
|
+
// D15/T-D15.4 — the `play.runTicks` relay case (`docs/D15-DETERMINISM-
|
|
1069
|
+
// DESIGN.md` §2.b): reaches the live session's `GameInternal.runTicks`
|
|
1070
|
+
// through `getPlayRuntimeAccess()`'s `runTicks` accessor (itself backed
|
|
1071
|
+
// by the SAME `DebugRegistry.getRunTicksTarget()` the bridge's
|
|
1072
|
+
// `window.__vgai.runTicks` calls — one implementation, byte-identical
|
|
1073
|
+
// behavior across doors, D17). `runTicks` itself throws a structured
|
|
1074
|
+
// `DebugError` (`RUN_TICKS_PAUSED`) while paused — never a silent no-op —
|
|
1075
|
+
// which `structuredErrorResult` below carries through as `data.code`.
|
|
1076
|
+
case 'run-ticks': {
|
|
1077
|
+
const runtime = getPlayRuntimeAccess();
|
|
1078
|
+
if (!runtime) {
|
|
1079
|
+
return { ok: false, error: 'not in play mode — start play before running ticks' };
|
|
1080
|
+
}
|
|
1081
|
+
if (!runtime.runTicks) {
|
|
1082
|
+
return {
|
|
1083
|
+
ok: false,
|
|
1084
|
+
error: 'this play session has no first-party Game to run ticks on',
|
|
1085
|
+
data: { code: 'RUN_TICKS_UNAVAILABLE' },
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
try {
|
|
1089
|
+
const n = cmd['n'] as number;
|
|
1090
|
+
const render = cmd['render'] as 'last' | 'all' | 'none' | undefined;
|
|
1091
|
+
runtime.runTicks(n, render ? { render } : undefined);
|
|
1092
|
+
return { ok: true };
|
|
1093
|
+
} catch (err) {
|
|
1094
|
+
return structuredErrorResult(err);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// #140 — the session-generic debug-seam relay: see `dispatchBridgeMethod`'s
|
|
1099
|
+
// doc comment above for why this is a distinct primitive from the six
|
|
1100
|
+
// named debug-seam cases above it (list-gameplay-state etc.) rather than
|
|
1101
|
+
// a duplicate of them.
|
|
1102
|
+
case 'bridge-call':
|
|
1103
|
+
return handleBridgeCall(cmd);
|
|
1104
|
+
case 'bridge-screenshot':
|
|
1105
|
+
return handleBridgeScreenshot(cmd);
|
|
1106
|
+
|
|
1107
|
+
// Wave-2 "the playwright shim in the editor page + script-mode wire op"
|
|
1108
|
+
// (`docs/SHARED-SESSION-SPEC.md` §7) — a STANDALONE relay command (like
|
|
1109
|
+
// `bridge-screenshot` above), deliberately NOT a `bridge-call` method: it
|
|
1110
|
+
// is editor-page UI automation (the in-page Playwright shim), not the
|
|
1111
|
+
// game-debug seam the bridge↔wire parity gate governs. See
|
|
1112
|
+
// `handlePageScript`'s own doc comment.
|
|
1113
|
+
case 'page-script':
|
|
1114
|
+
return handlePageScript(cmd);
|
|
1115
|
+
|
|
1116
|
+
// Version-skew honesty (B3-followup false-ack fix + Wave 5's AC-A0.3):
|
|
1117
|
+
// every unrecognized command type used to fall through to `return {ok:
|
|
1118
|
+
// true}` below — a silent lie: the browser did NOTHING, but the SDK/CLI
|
|
1119
|
+
// caller was told it succeeded. Reporting a structured failure must hold
|
|
1120
|
+
// for ANY unrecognized `type`, not just the ones known about today. The
|
|
1121
|
+
// `data.code` marker is what the SDK maps to its `*_UNSUPPORTED` codes;
|
|
1122
|
+
// the prose is for humans only.
|
|
269
1123
|
default:
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
// ever sending a command that would land here. Reporting a structured
|
|
276
|
-
// failure — never a fabricated success — is the fix, and it must hold for
|
|
277
|
-
// ANY unrecognized `type`, not just the ones known about today.
|
|
278
|
-
return { ok: false, error: `Unknown editor command: ${String(cmd['type'])}` };
|
|
1124
|
+
return {
|
|
1125
|
+
ok: false,
|
|
1126
|
+
error: `unknown command type "${cmd['type']}" — editor page predates this CLI`,
|
|
1127
|
+
data: { code: 'UNKNOWN_COMMAND_TYPE' },
|
|
1128
|
+
};
|
|
279
1129
|
}
|
|
280
1130
|
|
|
281
1131
|
return { ok: true };
|
|
@@ -322,5 +1172,29 @@ export function connectCommandListener(store: EditorStore): () => void {
|
|
|
322
1172
|
window.location.reload();
|
|
323
1173
|
});
|
|
324
1174
|
|
|
325
|
-
|
|
1175
|
+
// #145 — presence freshness: the state snapshot the server holds is only
|
|
1176
|
+
// re-POSTed after commands, so visibility/focus changes between commands
|
|
1177
|
+
// would go stale. Report on the three events that change presence.
|
|
1178
|
+
const reportPresence = () => reportEditorState(collectState(store));
|
|
1179
|
+
document.addEventListener('visibilitychange', reportPresence);
|
|
1180
|
+
window.addEventListener('focus', reportPresence);
|
|
1181
|
+
window.addEventListener('blur', reportPresence);
|
|
1182
|
+
|
|
1183
|
+
// pageErrors freshness (target-blaster friction #3): a runtime error
|
|
1184
|
+
// between commands must reach the server snapshot too, or `vgai status`
|
|
1185
|
+
// reads stale-clean. Deferred a tick so ViewportPanel's own
|
|
1186
|
+
// error-capture listener (which feeds editorConsole — the list
|
|
1187
|
+
// collectPageErrors reads) runs FIRST regardless of registration order.
|
|
1188
|
+
const reportAfterError = () => setTimeout(reportPresence, 0);
|
|
1189
|
+
window.addEventListener('error', reportAfterError);
|
|
1190
|
+
window.addEventListener('unhandledrejection', reportAfterError);
|
|
1191
|
+
|
|
1192
|
+
return () => {
|
|
1193
|
+
document.removeEventListener('visibilitychange', reportPresence);
|
|
1194
|
+
window.removeEventListener('focus', reportPresence);
|
|
1195
|
+
window.removeEventListener('blur', reportPresence);
|
|
1196
|
+
window.removeEventListener('error', reportAfterError);
|
|
1197
|
+
window.removeEventListener('unhandledrejection', reportAfterError);
|
|
1198
|
+
source.close();
|
|
1199
|
+
};
|
|
326
1200
|
}
|