unreal-engine-mcp-server 0.4.7 → 0.5.1
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/.env.example +26 -0
- package/.env.production +38 -7
- package/.eslintrc.json +0 -54
- package/.eslintrc.override.json +8 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +94 -0
- package/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +56 -0
- package/.github/copilot-instructions.md +478 -45
- package/.github/dependabot.yml +19 -0
- package/.github/labeler.yml +24 -0
- package/.github/labels.yml +70 -0
- package/.github/pull_request_template.md +42 -0
- package/.github/release-drafter-config.yml +51 -0
- package/.github/workflows/auto-merge.yml +38 -0
- package/.github/workflows/ci.yml +38 -0
- package/.github/workflows/dependency-review.yml +17 -0
- package/.github/workflows/gemini-issue-triage.yml +172 -0
- package/.github/workflows/greetings.yml +27 -0
- package/.github/workflows/labeler.yml +17 -0
- package/.github/workflows/links.yml +80 -0
- package/.github/workflows/pr-size-labeler.yml +137 -0
- package/.github/workflows/publish-mcp.yml +13 -7
- package/.github/workflows/release-drafter.yml +23 -0
- package/.github/workflows/release.yml +112 -0
- package/.github/workflows/semantic-pull-request.yml +35 -0
- package/.github/workflows/smoke-test.yml +36 -0
- package/.github/workflows/stale.yml +28 -0
- package/CHANGELOG.md +338 -31
- package/CONTRIBUTING.md +140 -0
- package/GEMINI.md +115 -0
- package/Public/Plugin_setup_guide.mp4 +0 -0
- package/README.md +189 -128
- package/claude_desktop_config_example.json +7 -6
- package/dist/automation/bridge.d.ts +50 -0
- package/dist/automation/bridge.js +452 -0
- package/dist/automation/connection-manager.d.ts +23 -0
- package/dist/automation/connection-manager.js +107 -0
- package/dist/automation/handshake.d.ts +11 -0
- package/dist/automation/handshake.js +89 -0
- package/dist/automation/index.d.ts +3 -0
- package/dist/automation/index.js +3 -0
- package/dist/automation/message-handler.d.ts +12 -0
- package/dist/automation/message-handler.js +149 -0
- package/dist/automation/request-tracker.d.ts +25 -0
- package/dist/automation/request-tracker.js +98 -0
- package/dist/automation/types.d.ts +130 -0
- package/dist/automation/types.js +2 -0
- package/dist/cli.js +32 -5
- package/dist/config.d.ts +26 -0
- package/dist/config.js +59 -0
- package/dist/constants.d.ts +16 -0
- package/dist/constants.js +16 -0
- package/dist/graphql/loaders.d.ts +64 -0
- package/dist/graphql/loaders.js +117 -0
- package/dist/graphql/resolvers.d.ts +268 -0
- package/dist/graphql/resolvers.js +746 -0
- package/dist/graphql/schema.d.ts +5 -0
- package/dist/graphql/schema.js +437 -0
- package/dist/graphql/server.d.ts +26 -0
- package/dist/graphql/server.js +117 -0
- package/dist/graphql/types.d.ts +9 -0
- package/dist/graphql/types.js +2 -0
- package/dist/handlers/resource-handlers.d.ts +20 -0
- package/dist/handlers/resource-handlers.js +180 -0
- package/dist/index.d.ts +33 -18
- package/dist/index.js +130 -619
- package/dist/resources/actors.d.ts +17 -12
- package/dist/resources/actors.js +56 -76
- package/dist/resources/assets.d.ts +6 -14
- package/dist/resources/assets.js +115 -147
- package/dist/resources/levels.d.ts +13 -13
- package/dist/resources/levels.js +25 -34
- package/dist/server/resource-registry.d.ts +20 -0
- package/dist/server/resource-registry.js +37 -0
- package/dist/server/tool-registry.d.ts +23 -0
- package/dist/server/tool-registry.js +322 -0
- package/dist/server-setup.d.ts +20 -0
- package/dist/server-setup.js +71 -0
- package/dist/services/health-monitor.d.ts +34 -0
- package/dist/services/health-monitor.js +105 -0
- package/dist/services/metrics-server.d.ts +11 -0
- package/dist/services/metrics-server.js +105 -0
- package/dist/tools/actors.d.ts +163 -9
- package/dist/tools/actors.js +356 -311
- package/dist/tools/animation.d.ts +135 -4
- package/dist/tools/animation.js +510 -411
- package/dist/tools/assets.d.ts +75 -29
- package/dist/tools/assets.js +265 -284
- package/dist/tools/audio.d.ts +102 -42
- package/dist/tools/audio.js +272 -685
- package/dist/tools/base-tool.d.ts +17 -0
- package/dist/tools/base-tool.js +46 -0
- package/dist/tools/behavior-tree.d.ts +94 -0
- package/dist/tools/behavior-tree.js +39 -0
- package/dist/tools/blueprint.d.ts +208 -126
- package/dist/tools/blueprint.js +685 -832
- package/dist/tools/consolidated-tool-definitions.d.ts +5462 -1781
- package/dist/tools/consolidated-tool-definitions.js +829 -496
- package/dist/tools/consolidated-tool-handlers.d.ts +2 -1
- package/dist/tools/consolidated-tool-handlers.js +198 -1027
- package/dist/tools/debug.d.ts +143 -85
- package/dist/tools/debug.js +234 -180
- package/dist/tools/dynamic-handler-registry.d.ts +13 -0
- package/dist/tools/dynamic-handler-registry.js +23 -0
- package/dist/tools/editor.d.ts +30 -83
- package/dist/tools/editor.js +247 -244
- package/dist/tools/engine.d.ts +10 -4
- package/dist/tools/engine.js +13 -5
- package/dist/tools/environment.d.ts +30 -0
- package/dist/tools/environment.js +267 -0
- package/dist/tools/foliage.d.ts +65 -99
- package/dist/tools/foliage.js +221 -331
- package/dist/tools/handlers/actor-handlers.d.ts +3 -0
- package/dist/tools/handlers/actor-handlers.js +227 -0
- package/dist/tools/handlers/animation-handlers.d.ts +3 -0
- package/dist/tools/handlers/animation-handlers.js +185 -0
- package/dist/tools/handlers/argument-helper.d.ts +16 -0
- package/dist/tools/handlers/argument-helper.js +80 -0
- package/dist/tools/handlers/asset-handlers.d.ts +3 -0
- package/dist/tools/handlers/asset-handlers.js +496 -0
- package/dist/tools/handlers/audio-handlers.d.ts +3 -0
- package/dist/tools/handlers/audio-handlers.js +166 -0
- package/dist/tools/handlers/blueprint-handlers.d.ts +4 -0
- package/dist/tools/handlers/blueprint-handlers.js +358 -0
- package/dist/tools/handlers/common-handlers.d.ts +14 -0
- package/dist/tools/handlers/common-handlers.js +56 -0
- package/dist/tools/handlers/editor-handlers.d.ts +3 -0
- package/dist/tools/handlers/editor-handlers.js +119 -0
- package/dist/tools/handlers/effect-handlers.d.ts +3 -0
- package/dist/tools/handlers/effect-handlers.js +171 -0
- package/dist/tools/handlers/environment-handlers.d.ts +3 -0
- package/dist/tools/handlers/environment-handlers.js +170 -0
- package/dist/tools/handlers/graph-handlers.d.ts +3 -0
- package/dist/tools/handlers/graph-handlers.js +90 -0
- package/dist/tools/handlers/input-handlers.d.ts +3 -0
- package/dist/tools/handlers/input-handlers.js +21 -0
- package/dist/tools/handlers/inspect-handlers.d.ts +3 -0
- package/dist/tools/handlers/inspect-handlers.js +383 -0
- package/dist/tools/handlers/level-handlers.d.ts +3 -0
- package/dist/tools/handlers/level-handlers.js +237 -0
- package/dist/tools/handlers/lighting-handlers.d.ts +3 -0
- package/dist/tools/handlers/lighting-handlers.js +144 -0
- package/dist/tools/handlers/performance-handlers.d.ts +3 -0
- package/dist/tools/handlers/performance-handlers.js +130 -0
- package/dist/tools/handlers/pipeline-handlers.d.ts +3 -0
- package/dist/tools/handlers/pipeline-handlers.js +110 -0
- package/dist/tools/handlers/sequence-handlers.d.ts +3 -0
- package/dist/tools/handlers/sequence-handlers.js +376 -0
- package/dist/tools/handlers/system-handlers.d.ts +4 -0
- package/dist/tools/handlers/system-handlers.js +506 -0
- package/dist/tools/input.d.ts +19 -0
- package/dist/tools/input.js +89 -0
- package/dist/tools/introspection.d.ts +103 -40
- package/dist/tools/introspection.js +425 -568
- package/dist/tools/landscape.d.ts +54 -93
- package/dist/tools/landscape.js +284 -409
- package/dist/tools/level.d.ts +66 -27
- package/dist/tools/level.js +647 -675
- package/dist/tools/lighting.d.ts +77 -38
- package/dist/tools/lighting.js +445 -943
- package/dist/tools/logs.d.ts +3 -3
- package/dist/tools/logs.js +5 -57
- package/dist/tools/materials.d.ts +91 -24
- package/dist/tools/materials.js +194 -118
- package/dist/tools/niagara.d.ts +149 -39
- package/dist/tools/niagara.js +267 -182
- package/dist/tools/performance.d.ts +27 -13
- package/dist/tools/performance.js +203 -122
- package/dist/tools/physics.d.ts +32 -77
- package/dist/tools/physics.js +175 -582
- package/dist/tools/property-dictionary.d.ts +13 -0
- package/dist/tools/property-dictionary.js +82 -0
- package/dist/tools/sequence.d.ts +85 -60
- package/dist/tools/sequence.js +208 -747
- package/dist/tools/tool-definition-utils.d.ts +59 -0
- package/dist/tools/tool-definition-utils.js +35 -0
- package/dist/tools/ui.d.ts +64 -34
- package/dist/tools/ui.js +134 -214
- package/dist/types/automation-responses.d.ts +115 -0
- package/dist/types/automation-responses.js +2 -0
- package/dist/types/env.d.ts +0 -3
- package/dist/types/env.js +0 -7
- package/dist/types/responses.d.ts +249 -0
- package/dist/types/responses.js +2 -0
- package/dist/types/tool-interfaces.d.ts +898 -0
- package/dist/types/tool-interfaces.js +2 -0
- package/dist/types/tool-types.d.ts +183 -19
- package/dist/types/tool-types.js +0 -4
- package/dist/unreal-bridge.d.ts +24 -131
- package/dist/unreal-bridge.js +364 -1506
- package/dist/utils/command-validator.d.ts +9 -0
- package/dist/utils/command-validator.js +68 -0
- package/dist/utils/elicitation.d.ts +1 -1
- package/dist/utils/elicitation.js +12 -15
- package/dist/utils/error-handler.d.ts +2 -51
- package/dist/utils/error-handler.js +11 -87
- package/dist/utils/ini-reader.d.ts +3 -0
- package/dist/utils/ini-reader.js +69 -0
- package/dist/utils/logger.js +9 -6
- package/dist/utils/normalize.d.ts +3 -0
- package/dist/utils/normalize.js +56 -0
- package/dist/utils/path-security.d.ts +2 -0
- package/dist/utils/path-security.js +24 -0
- package/dist/utils/response-factory.d.ts +7 -0
- package/dist/utils/response-factory.js +27 -0
- package/dist/utils/response-validator.d.ts +3 -24
- package/dist/utils/response-validator.js +130 -81
- package/dist/utils/result-helpers.d.ts +4 -5
- package/dist/utils/result-helpers.js +15 -16
- package/dist/utils/safe-json.js +5 -11
- package/dist/utils/unreal-command-queue.d.ts +24 -0
- package/dist/utils/unreal-command-queue.js +120 -0
- package/dist/utils/validation.d.ts +0 -40
- package/dist/utils/validation.js +1 -78
- package/dist/wasm/index.d.ts +70 -0
- package/dist/wasm/index.js +535 -0
- package/docs/GraphQL-API.md +888 -0
- package/docs/Migration-Guide-v0.5.0.md +684 -0
- package/docs/Roadmap.md +53 -0
- package/docs/WebAssembly-Integration.md +628 -0
- package/docs/editor-plugin-extension.md +370 -0
- package/docs/handler-mapping.md +242 -0
- package/docs/native-automation-progress.md +128 -0
- package/docs/testing-guide.md +423 -0
- package/mcp-config-example.json +6 -6
- package/package.json +67 -28
- package/plugins/McpAutomationBridge/Config/FilterPlugin.ini +8 -0
- package/plugins/McpAutomationBridge/McpAutomationBridge.uplugin +64 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/McpAutomationBridge.Build.cs +189 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.cpp +22 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.h +30 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeHelpers.h +1983 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeModule.cpp +72 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSettings.cpp +46 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +581 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +2394 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetQueryHandlers.cpp +300 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetWorkflowHandlers.cpp +2807 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AudioHandlers.cpp +1087 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BehaviorTreeHandlers.cpp +488 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.cpp +643 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.h +31 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +1184 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +5652 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers_List.cpp +152 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ControlHandlers.cpp +2614 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_DebugHandlers.cpp +42 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EditorFunctionHandlers.cpp +1237 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +1701 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +2145 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_FoliageHandlers.cpp +954 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InputHandlers.cpp +209 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InsightsHandlers.cpp +41 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LandscapeHandlers.cpp +1164 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LevelHandlers.cpp +762 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +634 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LogHandlers.cpp +136 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_MaterialGraphHandlers.cpp +494 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraGraphHandlers.cpp +278 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraHandlers.cpp +625 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PerformanceHandlers.cpp +401 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PipelineHandlers.cpp +67 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +735 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PropertyHandlers.cpp +2634 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_RenderHandlers.cpp +189 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.cpp +917 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.h +39 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +2670 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequencerHandlers.cpp +519 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_TestHandlers.cpp +38 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_UiHandlers.cpp +668 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_WorldPartitionHandlers.cpp +346 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.cpp +1330 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.h +149 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +783 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSettings.h +115 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSubsystem.h +796 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpConnectionManager.h +117 -0
- package/scripts/check-unreal-connection.mjs +19 -0
- package/scripts/clean-tmp.js +23 -0
- package/scripts/patch-wasm.js +26 -0
- package/scripts/run-all-tests.mjs +136 -0
- package/scripts/smoke-test.ts +94 -0
- package/scripts/sync-mcp-plugin.js +143 -0
- package/scripts/test-no-plugin-alternates.mjs +113 -0
- package/scripts/validate-server.js +46 -0
- package/scripts/verify-automation-bridge.js +200 -0
- package/server.json +58 -21
- package/src/automation/bridge.ts +558 -0
- package/src/automation/connection-manager.ts +130 -0
- package/src/automation/handshake.ts +99 -0
- package/src/automation/index.ts +2 -0
- package/src/automation/message-handler.ts +167 -0
- package/src/automation/request-tracker.ts +123 -0
- package/src/automation/types.ts +107 -0
- package/src/cli.ts +33 -6
- package/src/config.ts +73 -0
- package/src/constants.ts +19 -0
- package/src/graphql/loaders.ts +244 -0
- package/src/graphql/resolvers.ts +1008 -0
- package/src/graphql/schema.ts +452 -0
- package/src/graphql/server.ts +156 -0
- package/src/graphql/types.ts +10 -0
- package/src/handlers/resource-handlers.ts +186 -0
- package/src/index.ts +166 -664
- package/src/resources/actors.ts +58 -76
- package/src/resources/assets.ts +148 -134
- package/src/resources/levels.ts +28 -33
- package/src/server/resource-registry.ts +47 -0
- package/src/server/tool-registry.ts +354 -0
- package/src/server-setup.ts +114 -0
- package/src/services/health-monitor.ts +132 -0
- package/src/services/metrics-server.ts +142 -0
- package/src/tools/actors.ts +426 -323
- package/src/tools/animation.ts +672 -461
- package/src/tools/assets.ts +364 -289
- package/src/tools/audio.ts +323 -766
- package/src/tools/base-tool.ts +52 -0
- package/src/tools/behavior-tree.ts +45 -0
- package/src/tools/blueprint.ts +792 -970
- package/src/tools/consolidated-tool-definitions.ts +993 -515
- package/src/tools/consolidated-tool-handlers.ts +258 -1146
- package/src/tools/debug.ts +292 -187
- package/src/tools/dynamic-handler-registry.ts +33 -0
- package/src/tools/editor.ts +329 -253
- package/src/tools/engine.ts +14 -3
- package/src/tools/environment.ts +281 -0
- package/src/tools/foliage.ts +330 -392
- package/src/tools/handlers/actor-handlers.ts +265 -0
- package/src/tools/handlers/animation-handlers.ts +237 -0
- package/src/tools/handlers/argument-helper.ts +142 -0
- package/src/tools/handlers/asset-handlers.ts +532 -0
- package/src/tools/handlers/audio-handlers.ts +194 -0
- package/src/tools/handlers/blueprint-handlers.ts +380 -0
- package/src/tools/handlers/common-handlers.ts +87 -0
- package/src/tools/handlers/editor-handlers.ts +123 -0
- package/src/tools/handlers/effect-handlers.ts +220 -0
- package/src/tools/handlers/environment-handlers.ts +183 -0
- package/src/tools/handlers/graph-handlers.ts +116 -0
- package/src/tools/handlers/input-handlers.ts +28 -0
- package/src/tools/handlers/inspect-handlers.ts +450 -0
- package/src/tools/handlers/level-handlers.ts +252 -0
- package/src/tools/handlers/lighting-handlers.ts +147 -0
- package/src/tools/handlers/performance-handlers.ts +132 -0
- package/src/tools/handlers/pipeline-handlers.ts +127 -0
- package/src/tools/handlers/sequence-handlers.ts +415 -0
- package/src/tools/handlers/system-handlers.ts +564 -0
- package/src/tools/input.ts +101 -0
- package/src/tools/introspection.ts +493 -584
- package/src/tools/landscape.ts +418 -507
- package/src/tools/level.ts +786 -708
- package/src/tools/lighting.ts +588 -984
- package/src/tools/logs.ts +9 -57
- package/src/tools/materials.ts +237 -121
- package/src/tools/niagara.ts +335 -168
- package/src/tools/performance.ts +320 -169
- package/src/tools/physics.ts +274 -613
- package/src/tools/property-dictionary.ts +98 -0
- package/src/tools/sequence.ts +276 -820
- package/src/tools/tool-definition-utils.ts +35 -0
- package/src/tools/ui.ts +205 -283
- package/src/types/automation-responses.ts +119 -0
- package/src/types/env.ts +0 -10
- package/src/types/responses.ts +355 -0
- package/src/types/tool-interfaces.ts +250 -0
- package/src/types/tool-types.ts +243 -21
- package/src/unreal-bridge.ts +460 -1550
- package/src/utils/command-validator.ts +76 -0
- package/src/utils/elicitation.ts +10 -7
- package/src/utils/error-handler.ts +14 -90
- package/src/utils/ini-reader.ts +86 -0
- package/src/utils/logger.ts +8 -3
- package/src/utils/normalize.test.ts +162 -0
- package/src/utils/normalize.ts +60 -0
- package/src/utils/path-security.ts +43 -0
- package/src/utils/response-factory.ts +44 -0
- package/src/utils/response-validator.ts +176 -56
- package/src/utils/result-helpers.ts +21 -19
- package/src/utils/safe-json.test.ts +90 -0
- package/src/utils/safe-json.ts +14 -11
- package/src/utils/unreal-command-queue.ts +152 -0
- package/src/utils/validation.test.ts +184 -0
- package/src/utils/validation.ts +4 -1
- package/src/wasm/index.ts +838 -0
- package/test-server.mjs +100 -0
- package/tests/run-unreal-tool-tests.mjs +242 -14
- package/tests/test-animation.mjs +369 -0
- package/tests/test-asset-advanced.mjs +82 -0
- package/tests/test-asset-errors.mjs +35 -0
- package/tests/test-asset-graph.mjs +311 -0
- package/tests/test-audio.mjs +417 -0
- package/tests/test-automation-timeouts.mjs +98 -0
- package/tests/test-behavior-tree.mjs +444 -0
- package/tests/test-blueprint-graph.mjs +410 -0
- package/tests/test-blueprint.mjs +577 -0
- package/tests/test-client-mode.mjs +86 -0
- package/tests/test-console-command.mjs +56 -0
- package/tests/test-control-actor.mjs +425 -0
- package/tests/test-control-editor.mjs +112 -0
- package/tests/test-graphql.mjs +372 -0
- package/tests/test-input.mjs +349 -0
- package/tests/test-inspect.mjs +302 -0
- package/tests/test-landscape.mjs +316 -0
- package/tests/test-lighting.mjs +428 -0
- package/tests/test-manage-asset.mjs +438 -0
- package/tests/test-manage-level.mjs +89 -0
- package/tests/test-materials.mjs +356 -0
- package/tests/test-niagara.mjs +185 -0
- package/tests/test-no-inline-python.mjs +122 -0
- package/tests/test-performance.mjs +539 -0
- package/tests/test-plugin-handshake.mjs +82 -0
- package/tests/test-runner.mjs +933 -0
- package/tests/test-sequence.mjs +104 -0
- package/tests/test-system.mjs +96 -0
- package/tests/test-wasm.mjs +283 -0
- package/tests/test-world-partition.mjs +215 -0
- package/tsconfig.json +3 -3
- package/vitest.config.ts +35 -0
- package/wasm/Cargo.lock +363 -0
- package/wasm/Cargo.toml +42 -0
- package/wasm/LICENSE +21 -0
- package/wasm/README.md +253 -0
- package/wasm/src/dependency_resolver.rs +377 -0
- package/wasm/src/lib.rs +153 -0
- package/wasm/src/property_parser.rs +271 -0
- package/wasm/src/transform_math.rs +396 -0
- package/wasm/tests/integration.rs +109 -0
- package/.github/workflows/smithery-build.yml +0 -29
- package/dist/prompts/index.d.ts +0 -21
- package/dist/prompts/index.js +0 -217
- package/dist/tools/build_environment_advanced.d.ts +0 -65
- package/dist/tools/build_environment_advanced.js +0 -633
- package/dist/tools/rc.d.ts +0 -110
- package/dist/tools/rc.js +0 -437
- package/dist/tools/visual.d.ts +0 -40
- package/dist/tools/visual.js +0 -282
- package/dist/utils/http.d.ts +0 -6
- package/dist/utils/http.js +0 -151
- package/dist/utils/python-output.d.ts +0 -18
- package/dist/utils/python-output.js +0 -290
- package/dist/utils/python.d.ts +0 -2
- package/dist/utils/python.js +0 -4
- package/dist/utils/stdio-redirect.d.ts +0 -2
- package/dist/utils/stdio-redirect.js +0 -20
- package/docs/unreal-tool-test-cases.md +0 -574
- package/smithery.yaml +0 -29
- package/src/prompts/index.ts +0 -249
- package/src/tools/build_environment_advanced.ts +0 -732
- package/src/tools/rc.ts +0 -515
- package/src/tools/visual.ts +0 -281
- package/src/utils/http.ts +0 -187
- package/src/utils/python-output.ts +0 -351
- package/src/utils/python.ts +0 -3
- package/src/utils/stdio-redirect.ts +0 -18
package/src/tools/level.ts
CHANGED
|
@@ -1,664 +1,832 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
1
|
+
import { BaseTool } from './base-tool.js';
|
|
2
|
+
import { ILevelTools, StandardActionResponse } from '../types/tool-interfaces.js';
|
|
3
|
+
import { LevelResponse } from '../types/automation-responses.js';
|
|
4
|
+
import { wasmIntegration as _wasmIntegration } from '../wasm/index.js';
|
|
5
|
+
import { sanitizePath } from '../utils/path-security.js';
|
|
3
6
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
DEFAULT_OPERATION_TIMEOUT_MS,
|
|
8
|
+
DEFAULT_ASSET_OP_TIMEOUT_MS,
|
|
9
|
+
LONG_RUNNING_OP_TIMEOUT_MS
|
|
10
|
+
} from '../constants.js';
|
|
11
|
+
|
|
12
|
+
type LevelExportRecord = { target: string; timestamp: number; note?: string };
|
|
13
|
+
type ManagedLevelRecord = {
|
|
14
|
+
path: string;
|
|
15
|
+
name: string;
|
|
16
|
+
partitioned: boolean;
|
|
17
|
+
streaming: boolean;
|
|
18
|
+
loaded: boolean;
|
|
19
|
+
visible: boolean;
|
|
20
|
+
createdAt: number;
|
|
21
|
+
lastSavedAt?: number;
|
|
22
|
+
metadata?: Record<string, unknown>;
|
|
23
|
+
exports: LevelExportRecord[];
|
|
24
|
+
lights: Array<{ name: string; type: string; createdAt: number; details?: Record<string, unknown> }>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export class LevelTools extends BaseTool implements ILevelTools {
|
|
28
|
+
private managedLevels = new Map<string, ManagedLevelRecord>();
|
|
29
|
+
private listCache?: { result: { success: true; message: string; count: number; levels: any[] }; timestamp: number };
|
|
30
|
+
private readonly LIST_CACHE_TTL_MS = 750;
|
|
31
|
+
private currentLevelPath?: string;
|
|
32
|
+
|
|
33
|
+
private invalidateListCache() {
|
|
34
|
+
this.listCache = undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private normalizeLevelPath(rawPath: string | undefined): { path: string; name: string } {
|
|
38
|
+
if (!rawPath) {
|
|
39
|
+
return { path: '/Game/Maps/Untitled', name: 'Untitled' };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let formatted = rawPath.replace(/\\/g, '/').trim();
|
|
43
|
+
if (!formatted.startsWith('/')) {
|
|
44
|
+
formatted = formatted.startsWith('Game/') ? `/${formatted}` : `/Game/${formatted.replace(/^\/?Game\//i, '')}`;
|
|
45
|
+
}
|
|
46
|
+
if (!formatted.startsWith('/Game/')) {
|
|
47
|
+
formatted = `/Game/${formatted.replace(/^\/+/, '')}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Security validation
|
|
51
|
+
try {
|
|
52
|
+
formatted = sanitizePath(formatted);
|
|
53
|
+
} catch (e: any) {
|
|
54
|
+
// If sanitizePath fails, we should probably propagate that error,
|
|
55
|
+
// but normalizeLevelPath signature expects to return an object.
|
|
56
|
+
// For now, let's log and rethrow or fallback?
|
|
57
|
+
// Throwing is safer as it prevents operation on invalid path.
|
|
58
|
+
throw new Error(`Security validation failed for level path: ${e.message}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
formatted = formatted.replace(/\.umap$/i, '');
|
|
62
|
+
if (formatted.endsWith('/')) {
|
|
63
|
+
formatted = formatted.slice(0, -1);
|
|
64
|
+
}
|
|
65
|
+
const segments = formatted.split('/').filter(Boolean);
|
|
66
|
+
const lastSegment = segments[segments.length - 1] ?? 'Untitled';
|
|
67
|
+
const name = lastSegment.includes('.') ? lastSegment.split('.').pop() ?? lastSegment : lastSegment;
|
|
68
|
+
return { path: formatted, name: name || 'Untitled' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private ensureRecord(path: string, seed?: Partial<ManagedLevelRecord>): ManagedLevelRecord {
|
|
72
|
+
const normalized = this.normalizeLevelPath(path);
|
|
73
|
+
let record = this.managedLevels.get(normalized.path);
|
|
74
|
+
if (!record) {
|
|
75
|
+
record = {
|
|
76
|
+
path: normalized.path,
|
|
77
|
+
name: seed?.name ?? normalized.name,
|
|
78
|
+
partitioned: seed?.partitioned ?? false,
|
|
79
|
+
streaming: seed?.streaming ?? false,
|
|
80
|
+
loaded: seed?.loaded ?? false,
|
|
81
|
+
visible: seed?.visible ?? false,
|
|
82
|
+
createdAt: seed?.createdAt ?? Date.now(),
|
|
83
|
+
lastSavedAt: seed?.lastSavedAt,
|
|
84
|
+
metadata: seed?.metadata ? { ...seed.metadata } : undefined,
|
|
85
|
+
exports: seed?.exports ? [...seed.exports] : [],
|
|
86
|
+
lights: seed?.lights ? [...seed.lights] : []
|
|
87
|
+
};
|
|
88
|
+
this.managedLevels.set(normalized.path, record);
|
|
89
|
+
this.invalidateListCache();
|
|
90
|
+
}
|
|
91
|
+
return record;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private mutateRecord(path: string | undefined, updates: Partial<ManagedLevelRecord>): ManagedLevelRecord | undefined {
|
|
95
|
+
if (!path || !path.trim()) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const record = this.ensureRecord(path, updates);
|
|
100
|
+
let changed = false;
|
|
101
|
+
|
|
102
|
+
if (updates.name !== undefined && updates.name !== record.name) {
|
|
103
|
+
record.name = updates.name;
|
|
104
|
+
changed = true;
|
|
105
|
+
}
|
|
106
|
+
if (updates.partitioned !== undefined && updates.partitioned !== record.partitioned) {
|
|
107
|
+
record.partitioned = updates.partitioned;
|
|
108
|
+
changed = true;
|
|
109
|
+
}
|
|
110
|
+
if (updates.streaming !== undefined && updates.streaming !== record.streaming) {
|
|
111
|
+
record.streaming = updates.streaming;
|
|
112
|
+
changed = true;
|
|
113
|
+
}
|
|
114
|
+
if (updates.loaded !== undefined && updates.loaded !== record.loaded) {
|
|
115
|
+
record.loaded = updates.loaded;
|
|
116
|
+
changed = true;
|
|
117
|
+
}
|
|
118
|
+
if (updates.visible !== undefined && updates.visible !== record.visible) {
|
|
119
|
+
record.visible = updates.visible;
|
|
120
|
+
changed = true;
|
|
121
|
+
}
|
|
122
|
+
if (updates.createdAt !== undefined && updates.createdAt !== record.createdAt) {
|
|
123
|
+
record.createdAt = updates.createdAt;
|
|
124
|
+
changed = true;
|
|
125
|
+
}
|
|
126
|
+
if (updates.lastSavedAt !== undefined && updates.lastSavedAt !== record.lastSavedAt) {
|
|
127
|
+
record.lastSavedAt = updates.lastSavedAt;
|
|
128
|
+
changed = true;
|
|
129
|
+
}
|
|
130
|
+
if (updates.metadata) {
|
|
131
|
+
record.metadata = { ...(record.metadata ?? {}), ...updates.metadata };
|
|
132
|
+
changed = true;
|
|
133
|
+
}
|
|
134
|
+
if (updates.exports && updates.exports.length > 0) {
|
|
135
|
+
record.exports = [...record.exports, ...updates.exports];
|
|
136
|
+
changed = true;
|
|
137
|
+
}
|
|
138
|
+
if (updates.lights && updates.lights.length > 0) {
|
|
139
|
+
record.lights = [...record.lights, ...updates.lights];
|
|
140
|
+
changed = true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (changed) {
|
|
144
|
+
this.invalidateListCache();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return record;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private getRecord(path: string | undefined): ManagedLevelRecord | undefined {
|
|
151
|
+
if (!path || !path.trim()) {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
const normalized = this.normalizeLevelPath(path);
|
|
155
|
+
return this.managedLevels.get(normalized.path);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private resolveLevelPath(explicit?: string): string | undefined {
|
|
159
|
+
if (explicit && explicit.trim()) {
|
|
160
|
+
return this.normalizeLevelPath(explicit).path;
|
|
161
|
+
}
|
|
162
|
+
return this.currentLevelPath;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private removeRecord(path: string) {
|
|
166
|
+
const normalized = this.normalizeLevelPath(path);
|
|
167
|
+
if (this.managedLevels.delete(normalized.path)) {
|
|
168
|
+
if (this.currentLevelPath === normalized.path) {
|
|
169
|
+
this.currentLevelPath = undefined;
|
|
170
|
+
}
|
|
171
|
+
this.invalidateListCache();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private listManagedLevels(): { success: true; message: string; count: number; levels: Array<Record<string, unknown>> } {
|
|
176
|
+
const now = Date.now();
|
|
177
|
+
if (this.listCache && now - this.listCache.timestamp < this.LIST_CACHE_TTL_MS) {
|
|
178
|
+
return this.listCache.result;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const levels = Array.from(this.managedLevels.values()).map((record) => ({
|
|
182
|
+
path: record.path,
|
|
183
|
+
name: record.name,
|
|
184
|
+
partitioned: record.partitioned,
|
|
185
|
+
streaming: record.streaming,
|
|
186
|
+
loaded: record.loaded,
|
|
187
|
+
visible: record.visible,
|
|
188
|
+
createdAt: record.createdAt,
|
|
189
|
+
lastSavedAt: record.lastSavedAt,
|
|
190
|
+
exports: record.exports,
|
|
191
|
+
lightCount: record.lights.length
|
|
192
|
+
}));
|
|
193
|
+
|
|
194
|
+
const result = { success: true as const, message: 'Managed levels listed', count: levels.length, levels };
|
|
195
|
+
this.listCache = { result, timestamp: now };
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private summarizeLevel(path: string): Record<string, unknown> {
|
|
200
|
+
const record = this.getRecord(path);
|
|
201
|
+
if (!record) {
|
|
202
|
+
return { success: false, error: `Level not tracked: ${path}` };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
success: true,
|
|
207
|
+
message: 'Level summary ready',
|
|
208
|
+
path: record.path,
|
|
209
|
+
name: record.name,
|
|
210
|
+
partitioned: record.partitioned,
|
|
211
|
+
streaming: record.streaming,
|
|
212
|
+
loaded: record.loaded,
|
|
213
|
+
visible: record.visible,
|
|
214
|
+
createdAt: record.createdAt,
|
|
215
|
+
lastSavedAt: record.lastSavedAt,
|
|
216
|
+
exports: record.exports,
|
|
217
|
+
lights: record.lights,
|
|
218
|
+
metadata: record.metadata
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private setCurrentLevel(path: string) {
|
|
223
|
+
const normalized = this.normalizeLevelPath(path);
|
|
224
|
+
this.currentLevelPath = normalized.path;
|
|
225
|
+
this.ensureRecord(normalized.path, { loaded: true, visible: true });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async listLevels(): Promise<StandardActionResponse> {
|
|
229
|
+
// Try to get actual levels from UE via automation bridge
|
|
230
|
+
try {
|
|
231
|
+
const response = await this.sendAutomationRequest<LevelResponse>('list_levels', {}, {
|
|
232
|
+
timeoutMs: 10000
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
if (response && response.success !== false) {
|
|
236
|
+
// Also include managed levels for backwards compatibility and immediate visibility
|
|
237
|
+
const managed = this.listManagedLevels();
|
|
238
|
+
|
|
239
|
+
// Merge managed levels into the main list if not already present
|
|
240
|
+
const ueLevels = (response.allMaps || []) as any[];
|
|
241
|
+
const managedOnly = managed.levels.filter(m => !ueLevels.some(u => u.path === m.path));
|
|
242
|
+
const finalLevels = [...ueLevels, ...managedOnly];
|
|
243
|
+
|
|
244
|
+
const result: Record<string, unknown> = {
|
|
245
|
+
...response,
|
|
246
|
+
success: true,
|
|
247
|
+
message: 'Levels listed from Unreal Engine',
|
|
248
|
+
levels: finalLevels,
|
|
249
|
+
currentMap: response.currentMap,
|
|
250
|
+
currentMapPath: response.currentMapPath,
|
|
251
|
+
currentWorldLevels: response.currentWorldLevels || [],
|
|
252
|
+
data: {
|
|
253
|
+
levels: finalLevels,
|
|
254
|
+
count: finalLevels.length
|
|
255
|
+
},
|
|
256
|
+
managedLevels: managed.levels,
|
|
257
|
+
managedLevelCount: managed.count
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
return result as StandardActionResponse;
|
|
261
|
+
}
|
|
262
|
+
} catch {
|
|
263
|
+
// Fall back to managed levels if automation bridge fails
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Fallback to locally managed levels
|
|
267
|
+
return this.listManagedLevels();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async getLevelSummary(levelPath?: string): Promise<StandardActionResponse> {
|
|
271
|
+
const resolved = this.resolveLevelPath(levelPath);
|
|
272
|
+
if (!resolved) {
|
|
273
|
+
return { success: false, error: 'No level specified' };
|
|
274
|
+
}
|
|
275
|
+
return this.summarizeLevel(resolved) as StandardActionResponse;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
registerLight(levelPath: string | undefined, info: { name: string; type: string; details?: Record<string, unknown> }) {
|
|
279
|
+
const resolved = this.resolveLevelPath(levelPath);
|
|
280
|
+
if (!resolved) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
this.mutateRecord(resolved, {
|
|
284
|
+
lights: [
|
|
285
|
+
{
|
|
286
|
+
name: info.name,
|
|
287
|
+
type: info.type,
|
|
288
|
+
createdAt: Date.now(),
|
|
289
|
+
details: info.details
|
|
290
|
+
}
|
|
291
|
+
]
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async exportLevel(params: { levelPath?: string; exportPath: string; note?: string; timeoutMs?: number }): Promise<StandardActionResponse> {
|
|
296
|
+
const resolved = this.resolveLevelPath(params.levelPath);
|
|
297
|
+
if (!resolved) {
|
|
298
|
+
return { success: false, error: 'No level specified for export' };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
const res = await this.sendAutomationRequest<LevelResponse>('manage_level', {
|
|
303
|
+
action: 'export_level',
|
|
304
|
+
levelPath: resolved,
|
|
305
|
+
exportPath: params.exportPath
|
|
306
|
+
}, { timeoutMs: params.timeoutMs ?? LONG_RUNNING_OP_TIMEOUT_MS });
|
|
307
|
+
|
|
308
|
+
if (res?.success === false) {
|
|
309
|
+
return {
|
|
310
|
+
success: false,
|
|
311
|
+
error: res.error || res.message || 'Export failed',
|
|
312
|
+
levelPath: resolved,
|
|
313
|
+
exportPath: params.exportPath,
|
|
314
|
+
details: res
|
|
315
|
+
} as StandardActionResponse;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
success: true,
|
|
320
|
+
message: `Level exported to ${params.exportPath}`,
|
|
321
|
+
levelPath: resolved,
|
|
322
|
+
exportPath: params.exportPath,
|
|
323
|
+
details: res
|
|
324
|
+
} as StandardActionResponse;
|
|
325
|
+
} catch (e: any) {
|
|
326
|
+
return { success: false, error: `Export failed: ${e.message}` };
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async importLevel(params: { packagePath: string; destinationPath?: string; streaming?: boolean; timeoutMs?: number }): Promise<StandardActionResponse> {
|
|
331
|
+
const destination = params.destinationPath
|
|
332
|
+
? this.normalizeLevelPath(params.destinationPath)
|
|
333
|
+
: this.normalizeLevelPath(`/Game/Maps/Imported_${Math.floor(Date.now() / 1000)}`);
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
const res = await this.sendAutomationRequest<LevelResponse>('manage_level', {
|
|
337
|
+
action: 'import_level',
|
|
338
|
+
packagePath: params.packagePath,
|
|
339
|
+
destinationPath: destination.path
|
|
340
|
+
}, { timeoutMs: params.timeoutMs ?? LONG_RUNNING_OP_TIMEOUT_MS });
|
|
341
|
+
|
|
342
|
+
if ((res as any)?.success === false) {
|
|
343
|
+
return {
|
|
344
|
+
success: false,
|
|
345
|
+
error: (res as any).error || (res as any).message || 'Import failed',
|
|
346
|
+
levelPath: destination.path,
|
|
347
|
+
details: res
|
|
348
|
+
} as StandardActionResponse;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
success: true,
|
|
353
|
+
message: `Level imported to ${destination.path}`,
|
|
354
|
+
levelPath: destination.path,
|
|
355
|
+
partitioned: true,
|
|
356
|
+
streaming: Boolean(params.streaming),
|
|
357
|
+
details: res
|
|
358
|
+
} as StandardActionResponse;
|
|
359
|
+
} catch (e: any) {
|
|
360
|
+
return { success: false, error: `Import failed: ${e.message}` };
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async saveLevelAs(params: { sourcePath?: string; targetPath: string }): Promise<StandardActionResponse> {
|
|
365
|
+
const source = this.resolveLevelPath(params.sourcePath);
|
|
366
|
+
const target = this.normalizeLevelPath(params.targetPath);
|
|
367
|
+
|
|
368
|
+
// Delegate to automation bridge
|
|
369
|
+
try {
|
|
370
|
+
const response = await this.sendAutomationRequest<LevelResponse>('manage_level', {
|
|
371
|
+
action: 'save_level_as',
|
|
372
|
+
savePath: target.path
|
|
373
|
+
}, {
|
|
374
|
+
timeoutMs: DEFAULT_ASSET_OP_TIMEOUT_MS
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
if (response.success === false) {
|
|
378
|
+
return { success: false, error: response.error || response.message || 'Failed to save level as' };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// If successful, update local state
|
|
382
|
+
if (!source) {
|
|
383
|
+
// If no source known, just ensure target record
|
|
384
|
+
this.ensureRecord(target.path, {
|
|
385
|
+
name: target.name,
|
|
386
|
+
loaded: true,
|
|
387
|
+
visible: true,
|
|
388
|
+
createdAt: Date.now(),
|
|
389
|
+
lastSavedAt: Date.now()
|
|
390
|
+
});
|
|
391
|
+
} else {
|
|
392
|
+
const sourceRecord = this.getRecord(source);
|
|
393
|
+
const now = Date.now();
|
|
394
|
+
this.ensureRecord(target.path, {
|
|
395
|
+
name: target.name,
|
|
396
|
+
partitioned: sourceRecord?.partitioned ?? true,
|
|
397
|
+
streaming: sourceRecord?.streaming ?? false,
|
|
398
|
+
loaded: true,
|
|
399
|
+
visible: true,
|
|
400
|
+
metadata: { ...(sourceRecord?.metadata ?? {}), savedFrom: source },
|
|
401
|
+
exports: sourceRecord?.exports ?? [],
|
|
402
|
+
lights: sourceRecord?.lights ?? [],
|
|
403
|
+
createdAt: sourceRecord?.createdAt ?? now,
|
|
404
|
+
lastSavedAt: now
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
this.setCurrentLevel(target.path);
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
success: true,
|
|
412
|
+
message: response.message || `Level saved as ${target.path}`,
|
|
413
|
+
levelPath: target.path
|
|
414
|
+
} as StandardActionResponse;
|
|
415
|
+
} catch (error) {
|
|
416
|
+
return { success: false, error: `Failed to save level as: ${error instanceof Error ? error.message : String(error)}` };
|
|
417
|
+
}
|
|
418
|
+
}
|
|
9
419
|
|
|
10
|
-
|
|
11
|
-
|
|
420
|
+
async deleteLevels(params: { levelPaths: string[] }): Promise<StandardActionResponse> {
|
|
421
|
+
const removed: string[] = [];
|
|
422
|
+
for (const path of params.levelPaths) {
|
|
423
|
+
const normalized = this.normalizeLevelPath(path).path;
|
|
424
|
+
if (this.managedLevels.has(normalized)) {
|
|
425
|
+
this.removeRecord(normalized);
|
|
426
|
+
removed.push(normalized);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
success: true,
|
|
432
|
+
message: removed.length ? `Deleted ${removed.length} managed level(s)` : 'No managed levels removed',
|
|
433
|
+
removed
|
|
434
|
+
} as StandardActionResponse;
|
|
435
|
+
}
|
|
12
436
|
|
|
13
|
-
// Load level (using LevelEditorSubsystem to avoid crashes)
|
|
14
437
|
async loadLevel(params: {
|
|
15
438
|
levelPath: string;
|
|
16
439
|
streaming?: boolean;
|
|
17
440
|
position?: [number, number, number];
|
|
18
|
-
}) {
|
|
19
|
-
|
|
20
|
-
const python = `
|
|
21
|
-
import unreal
|
|
22
|
-
import json
|
|
23
|
-
|
|
24
|
-
result = {
|
|
25
|
-
"success": False,
|
|
26
|
-
"message": "",
|
|
27
|
-
"error": "",
|
|
28
|
-
"details": [],
|
|
29
|
-
"warnings": []
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
try:
|
|
33
|
-
ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
|
|
34
|
-
world = ues.get_editor_world() if ues else None
|
|
35
|
-
if world:
|
|
36
|
-
try:
|
|
37
|
-
unreal.EditorLevelUtils.add_level_to_world(world, r"${params.levelPath}", unreal.LevelStreamingKismet)
|
|
38
|
-
result["success"] = True
|
|
39
|
-
result["message"] = "Streaming level added"
|
|
40
|
-
result["details"].append("Streaming level added via EditorLevelUtils")
|
|
41
|
-
except Exception as add_error:
|
|
42
|
-
result["error"] = f"Failed to add streaming level: {add_error}"
|
|
43
|
-
else:
|
|
44
|
-
result["error"] = "No editor world available"
|
|
45
|
-
except Exception as outer_error:
|
|
46
|
-
result["error"] = f"Streaming level operation failed: {outer_error}"
|
|
47
|
-
|
|
48
|
-
if result["success"]:
|
|
49
|
-
if not result["message"]:
|
|
50
|
-
result["message"] = "Streaming level added"
|
|
51
|
-
else:
|
|
52
|
-
if not result["error"]:
|
|
53
|
-
result["error"] = result["message"] or "Failed to add streaming level"
|
|
54
|
-
if not result["message"]:
|
|
55
|
-
result["message"] = result["error"]
|
|
56
|
-
|
|
57
|
-
if not result["warnings"]:
|
|
58
|
-
result.pop("warnings")
|
|
59
|
-
if not result["details"]:
|
|
60
|
-
result.pop("details")
|
|
61
|
-
if result.get("error") is None:
|
|
62
|
-
result.pop("error")
|
|
63
|
-
|
|
64
|
-
print("RESULT:" + json.dumps(result))
|
|
65
|
-
`.trim();
|
|
441
|
+
}): Promise<StandardActionResponse> {
|
|
442
|
+
const normalizedPath = this.normalizeLevelPath(params.levelPath).path;
|
|
66
443
|
|
|
444
|
+
if (params.streaming) {
|
|
67
445
|
try {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
446
|
+
const simpleName = (params.levelPath || '').split('/').filter(Boolean).pop() || params.levelPath;
|
|
447
|
+
await this.bridge.executeConsoleCommand(`StreamLevel ${simpleName} Load Show`);
|
|
448
|
+
this.mutateRecord(normalizedPath, {
|
|
449
|
+
streaming: true,
|
|
450
|
+
loaded: true,
|
|
451
|
+
visible: true
|
|
72
452
|
});
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
453
|
+
return {
|
|
454
|
+
success: true,
|
|
455
|
+
message: `Streaming level loaded: ${params.levelPath}`,
|
|
456
|
+
levelPath: normalizedPath,
|
|
457
|
+
streaming: true
|
|
458
|
+
} as StandardActionResponse;
|
|
459
|
+
} catch (err) {
|
|
460
|
+
return {
|
|
461
|
+
success: false,
|
|
462
|
+
error: `Failed to load streaming level: ${err}`,
|
|
463
|
+
levelPath: normalizedPath
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
} else {
|
|
467
|
+
// Try loading via automation bridge first (more robust)
|
|
468
|
+
try {
|
|
469
|
+
const response = await this.sendAutomationRequest<LevelResponse>('manage_level', {
|
|
470
|
+
action: 'load',
|
|
471
|
+
levelPath: params.levelPath
|
|
472
|
+
}, { timeoutMs: DEFAULT_OPERATION_TIMEOUT_MS });
|
|
473
|
+
|
|
474
|
+
if (response.success) {
|
|
475
|
+
this.setCurrentLevel(normalizedPath);
|
|
476
|
+
this.mutateRecord(normalizedPath, {
|
|
477
|
+
streaming: false,
|
|
478
|
+
loaded: true,
|
|
479
|
+
visible: true
|
|
480
|
+
});
|
|
481
|
+
return {
|
|
482
|
+
...response,
|
|
76
483
|
success: true,
|
|
77
|
-
message:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
if (interpreted.details?.length) {
|
|
83
|
-
result.details = interpreted.details;
|
|
84
|
-
}
|
|
85
|
-
return result;
|
|
484
|
+
message: `Level loaded: ${params.levelPath}`,
|
|
485
|
+
level: normalizedPath,
|
|
486
|
+
streaming: false
|
|
487
|
+
} as StandardActionResponse;
|
|
86
488
|
}
|
|
87
|
-
} catch {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
} else {
|
|
91
|
-
const python = `
|
|
92
|
-
import unreal
|
|
93
|
-
import json
|
|
94
|
-
|
|
95
|
-
result = {
|
|
96
|
-
"success": False,
|
|
97
|
-
"message": "",
|
|
98
|
-
"error": "",
|
|
99
|
-
"warnings": [],
|
|
100
|
-
"details": [],
|
|
101
|
-
"level": r"${params.levelPath}"
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
try:
|
|
105
|
-
level_path = r"${params.levelPath}"
|
|
106
|
-
asset_path = level_path
|
|
107
|
-
try:
|
|
108
|
-
tail = asset_path.rsplit('/', 1)[-1]
|
|
109
|
-
if '.' not in tail:
|
|
110
|
-
asset_path = f"{asset_path}.{tail}"
|
|
111
|
-
except Exception:
|
|
112
|
-
pass
|
|
113
|
-
|
|
114
|
-
asset_exists = False
|
|
115
|
-
try:
|
|
116
|
-
asset_exists = unreal.EditorAssetLibrary.does_asset_exist(asset_path)
|
|
117
|
-
except Exception:
|
|
118
|
-
asset_exists = False
|
|
119
|
-
|
|
120
|
-
if not asset_exists:
|
|
121
|
-
result["error"] = f"Level not found: {asset_path}"
|
|
122
|
-
else:
|
|
123
|
-
les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
|
|
124
|
-
if les:
|
|
125
|
-
success = les.load_level(level_path)
|
|
126
|
-
if success:
|
|
127
|
-
result["success"] = True
|
|
128
|
-
result["message"] = "Level loaded successfully"
|
|
129
|
-
result["details"].append("Level loaded via LevelEditorSubsystem")
|
|
130
|
-
else:
|
|
131
|
-
result["error"] = "Failed to load level"
|
|
132
|
-
else:
|
|
133
|
-
result["error"] = "LevelEditorSubsystem not available"
|
|
134
|
-
except Exception as err:
|
|
135
|
-
result["error"] = f"Failed to load level: {err}"
|
|
136
|
-
|
|
137
|
-
if result["success"]:
|
|
138
|
-
if not result["message"]:
|
|
139
|
-
result["message"] = "Level loaded successfully"
|
|
140
|
-
else:
|
|
141
|
-
if not result["error"]:
|
|
142
|
-
result["error"] = "Failed to load level"
|
|
143
|
-
if not result["message"]:
|
|
144
|
-
result["message"] = result["error"]
|
|
145
|
-
|
|
146
|
-
if not result["warnings"]:
|
|
147
|
-
result.pop("warnings")
|
|
148
|
-
if not result["details"]:
|
|
149
|
-
result.pop("details")
|
|
150
|
-
if result.get("error") is None:
|
|
151
|
-
result.pop("error")
|
|
152
|
-
|
|
153
|
-
print("RESULT:" + json.dumps(result))
|
|
154
|
-
`.trim();
|
|
489
|
+
} catch (_e) {
|
|
490
|
+
// Fallback to console logic
|
|
491
|
+
}
|
|
155
492
|
|
|
156
493
|
try {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
494
|
+
// Best-effort existence check using the Automation Bridge when available.
|
|
495
|
+
try {
|
|
496
|
+
const automation = this.getAutomationBridge();
|
|
497
|
+
if (automation && typeof automation.sendAutomationRequest === 'function' && automation.isConnected()) {
|
|
498
|
+
const targetPath = (params.levelPath ?? '').toString();
|
|
499
|
+
const existsResp: any = await automation.sendAutomationRequest('execute_editor_function', {
|
|
500
|
+
functionName: 'ASSET_EXISTS_SIMPLE',
|
|
501
|
+
path: targetPath
|
|
502
|
+
}, {
|
|
503
|
+
timeoutMs: 5000
|
|
504
|
+
});
|
|
505
|
+
const result = existsResp?.result ?? existsResp ?? {};
|
|
506
|
+
const exists = Boolean(result.exists);
|
|
507
|
+
|
|
508
|
+
if (!exists) {
|
|
509
|
+
const message = typeof result.message === 'string' ? result.message : 'Level not found';
|
|
510
|
+
return {
|
|
511
|
+
success: false,
|
|
512
|
+
error: 'not_found',
|
|
513
|
+
message,
|
|
514
|
+
level: normalizedPath
|
|
515
|
+
} as StandardActionResponse;
|
|
516
|
+
}
|
|
175
517
|
}
|
|
176
|
-
|
|
518
|
+
} catch {
|
|
519
|
+
// If the existence check fails for any reason, fall back to the console command path below.
|
|
177
520
|
}
|
|
178
521
|
|
|
179
|
-
|
|
522
|
+
await this.bridge.executeConsoleCommand(`Open ${params.levelPath}`);
|
|
523
|
+
this.setCurrentLevel(normalizedPath);
|
|
524
|
+
this.mutateRecord(normalizedPath, {
|
|
525
|
+
streaming: false,
|
|
526
|
+
loaded: true,
|
|
527
|
+
visible: true
|
|
528
|
+
});
|
|
529
|
+
return {
|
|
530
|
+
success: true,
|
|
531
|
+
message: `Level loaded: ${params.levelPath}`,
|
|
532
|
+
level: normalizedPath,
|
|
533
|
+
streaming: false
|
|
534
|
+
} as StandardActionResponse;
|
|
535
|
+
} catch (err) {
|
|
536
|
+
return {
|
|
180
537
|
success: false,
|
|
181
|
-
error:
|
|
182
|
-
level:
|
|
538
|
+
error: `Failed to load level: ${err}`,
|
|
539
|
+
level: normalizedPath
|
|
183
540
|
};
|
|
184
|
-
if (interpreted.warnings?.length) {
|
|
185
|
-
failure.warnings = interpreted.warnings;
|
|
186
|
-
}
|
|
187
|
-
if (interpreted.details?.length) {
|
|
188
|
-
failure.details = interpreted.details;
|
|
189
|
-
}
|
|
190
|
-
return failure;
|
|
191
|
-
} catch (e) {
|
|
192
|
-
return { success: false, error: `Failed to load level: ${e}` };
|
|
193
541
|
}
|
|
194
542
|
}
|
|
195
543
|
}
|
|
196
544
|
|
|
197
|
-
|
|
198
|
-
async saveLevel(_params: {
|
|
545
|
+
async saveLevel(params: {
|
|
199
546
|
levelName?: string;
|
|
200
547
|
savePath?: string;
|
|
201
|
-
}) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
result = {
|
|
207
|
-
"success": False,
|
|
208
|
-
"message": "",
|
|
209
|
-
"error": "",
|
|
210
|
-
"warnings": [],
|
|
211
|
-
"details": [],
|
|
212
|
-
"skipped": False,
|
|
213
|
-
"reason": ""
|
|
214
|
-
}
|
|
548
|
+
}): Promise<StandardActionResponse> {
|
|
549
|
+
try {
|
|
550
|
+
if (params.savePath && !params.savePath.startsWith('/Game/')) {
|
|
551
|
+
throw new Error(`Invalid save path: ${params.savePath}`);
|
|
552
|
+
}
|
|
215
553
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
data["message"] = "Level saved"
|
|
222
|
-
if not data.get("success"):
|
|
223
|
-
if not data.get("error"):
|
|
224
|
-
data["error"] = data.get("message") or "Failed to save level"
|
|
225
|
-
if not data.get("message"):
|
|
226
|
-
data["message"] = data.get("error") or "Failed to save level"
|
|
227
|
-
if data.get("success"):
|
|
228
|
-
data.pop("error", None)
|
|
229
|
-
if not data.get("warnings"):
|
|
230
|
-
data.pop("warnings", None)
|
|
231
|
-
if not data.get("details"):
|
|
232
|
-
data.pop("details", None)
|
|
233
|
-
if not data.get("skipped"):
|
|
234
|
-
data.pop("skipped", None)
|
|
235
|
-
data.pop("reason", None)
|
|
236
|
-
else:
|
|
237
|
-
if not data.get("reason"):
|
|
238
|
-
data.pop("reason", None)
|
|
239
|
-
print("RESULT:" + json.dumps(data))
|
|
240
|
-
|
|
241
|
-
try:
|
|
242
|
-
# Attempt to reduce source control prompts (best-effort, may be a no-op depending on UE version)
|
|
243
|
-
try:
|
|
244
|
-
prefs = unreal.SourceControlPreferences()
|
|
245
|
-
muted = False
|
|
246
|
-
try:
|
|
247
|
-
prefs.set_enable_source_control(False)
|
|
248
|
-
muted = True
|
|
249
|
-
except Exception:
|
|
250
|
-
try:
|
|
251
|
-
prefs.enable_source_control = False
|
|
252
|
-
muted = True
|
|
253
|
-
except Exception:
|
|
254
|
-
muted = False
|
|
255
|
-
if muted:
|
|
256
|
-
result["details"].append("Source control prompts disabled")
|
|
257
|
-
except Exception:
|
|
258
|
-
pass
|
|
259
|
-
|
|
260
|
-
# Determine if level is dirty and save via LevelEditorSubsystem when possible
|
|
261
|
-
world = None
|
|
262
|
-
try:
|
|
263
|
-
world = unreal.EditorSubsystemLibrary.get_editor_world()
|
|
264
|
-
except Exception:
|
|
265
|
-
try:
|
|
266
|
-
ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
|
|
267
|
-
world = ues.get_editor_world() if ues else None
|
|
268
|
-
except Exception:
|
|
269
|
-
world = None
|
|
270
|
-
|
|
271
|
-
pkg_path = None
|
|
272
|
-
try:
|
|
273
|
-
if world is not None:
|
|
274
|
-
full = world.get_path_name()
|
|
275
|
-
pkg_path = full.split('.')[0] if '.' in full else full
|
|
276
|
-
if pkg_path:
|
|
277
|
-
result["details"].append(f"Detected level package: {pkg_path}")
|
|
278
|
-
except Exception:
|
|
279
|
-
pkg_path = None
|
|
280
|
-
|
|
281
|
-
skip_save = False
|
|
282
|
-
try:
|
|
283
|
-
is_dirty = None
|
|
284
|
-
if pkg_path:
|
|
285
|
-
editor_asset_lib = getattr(unreal, 'EditorAssetLibrary', None)
|
|
286
|
-
if editor_asset_lib and hasattr(editor_asset_lib, 'is_asset_dirty'):
|
|
287
|
-
try:
|
|
288
|
-
is_dirty = editor_asset_lib.is_asset_dirty(pkg_path)
|
|
289
|
-
except Exception as check_error:
|
|
290
|
-
result["warnings"].append(f"EditorAssetLibrary.is_asset_dirty failed: {check_error}")
|
|
291
|
-
is_dirty = None
|
|
292
|
-
if is_dirty is None:
|
|
293
|
-
# Fallback: attempt to inspect the current level package
|
|
294
|
-
try:
|
|
295
|
-
ell = getattr(unreal, 'EditorLevelLibrary', None)
|
|
296
|
-
level = ell.get_current_level() if ell and hasattr(ell, 'get_current_level') else None
|
|
297
|
-
package = level.get_outermost() if level and hasattr(level, 'get_outermost') else None
|
|
298
|
-
if package and hasattr(package, 'is_dirty'):
|
|
299
|
-
is_dirty = package.is_dirty()
|
|
300
|
-
except Exception as fallback_error:
|
|
301
|
-
result["warnings"].append(f"Fallback dirty check failed: {fallback_error}")
|
|
302
|
-
if is_dirty is False:
|
|
303
|
-
result["success"] = True
|
|
304
|
-
result["skipped"] = True
|
|
305
|
-
result["reason"] = "Level not dirty"
|
|
306
|
-
result["message"] = "Level save skipped"
|
|
307
|
-
skip_save = True
|
|
308
|
-
elif is_dirty is None and pkg_path:
|
|
309
|
-
result["warnings"].append("Unable to determine level dirty state; attempting save anyway")
|
|
310
|
-
except Exception as dirty_error:
|
|
311
|
-
result["warnings"].append(f"Failed to check level dirty state: {dirty_error}")
|
|
312
|
-
|
|
313
|
-
if not skip_save:
|
|
314
|
-
saved = False
|
|
315
|
-
try:
|
|
316
|
-
les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
|
|
317
|
-
if les:
|
|
318
|
-
les.save_current_level()
|
|
319
|
-
saved = True
|
|
320
|
-
result["details"].append("Level saved via LevelEditorSubsystem")
|
|
321
|
-
except Exception as save_error:
|
|
322
|
-
result["error"] = f"Level save failed: {save_error}"
|
|
323
|
-
saved = False
|
|
324
|
-
|
|
325
|
-
if not saved:
|
|
326
|
-
raise Exception('LevelEditorSubsystem not available')
|
|
327
|
-
|
|
328
|
-
result["success"] = True
|
|
329
|
-
if not result["message"]:
|
|
330
|
-
result["message"] = "Level saved"
|
|
331
|
-
except Exception as err:
|
|
332
|
-
result["error"] = str(err)
|
|
333
|
-
|
|
334
|
-
print_result(result)
|
|
335
|
-
`.trim();
|
|
554
|
+
const action = params.savePath ? 'save_level_as' : 'save';
|
|
555
|
+
const payload: Record<string, unknown> = { action };
|
|
556
|
+
if (params.savePath) {
|
|
557
|
+
payload.savePath = params.savePath;
|
|
558
|
+
}
|
|
336
559
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const interpreted = interpretStandardResult(response, {
|
|
340
|
-
successMessage: 'Level saved',
|
|
341
|
-
failureMessage: 'Failed to save level'
|
|
560
|
+
const response = await this.sendAutomationRequest<LevelResponse>('manage_level', payload, {
|
|
561
|
+
timeoutMs: DEFAULT_ASSET_OP_TIMEOUT_MS
|
|
342
562
|
});
|
|
343
563
|
|
|
344
|
-
if (
|
|
345
|
-
|
|
346
|
-
success: true,
|
|
347
|
-
message: interpreted.message
|
|
348
|
-
};
|
|
349
|
-
const skipped = coerceBoolean(interpreted.payload.skipped);
|
|
350
|
-
if (typeof skipped === 'boolean') {
|
|
351
|
-
result.skipped = skipped;
|
|
352
|
-
}
|
|
353
|
-
const reason = coerceString(interpreted.payload.reason);
|
|
354
|
-
if (reason) {
|
|
355
|
-
result.reason = reason;
|
|
356
|
-
}
|
|
357
|
-
if (interpreted.warnings?.length) {
|
|
358
|
-
result.warnings = interpreted.warnings;
|
|
359
|
-
}
|
|
360
|
-
if (interpreted.details?.length) {
|
|
361
|
-
result.details = interpreted.details;
|
|
362
|
-
}
|
|
363
|
-
return result;
|
|
564
|
+
if (response.success === false) {
|
|
565
|
+
return { success: false, error: response.error || response.message || 'Failed to save level' };
|
|
364
566
|
}
|
|
365
567
|
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
568
|
+
const result: Record<string, unknown> = {
|
|
569
|
+
...response,
|
|
570
|
+
success: true,
|
|
571
|
+
message: response.message || 'Level saved'
|
|
369
572
|
};
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const skippedFailure = coerceBoolean(interpreted.payload.skipped);
|
|
374
|
-
if (typeof skippedFailure === 'boolean') {
|
|
375
|
-
failure.skipped = skippedFailure;
|
|
573
|
+
|
|
574
|
+
if (response.skipped) {
|
|
575
|
+
result.skipped = response.skipped;
|
|
376
576
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
failure.reason = failureReason;
|
|
577
|
+
if (response.reason) {
|
|
578
|
+
result.reason = response.reason;
|
|
380
579
|
}
|
|
381
|
-
if (
|
|
382
|
-
|
|
580
|
+
if (response.warnings) {
|
|
581
|
+
result.warnings = response.warnings;
|
|
383
582
|
}
|
|
384
|
-
if (
|
|
385
|
-
|
|
583
|
+
if (response.details) {
|
|
584
|
+
result.details = response.details;
|
|
386
585
|
}
|
|
387
586
|
|
|
388
|
-
return
|
|
389
|
-
} catch (
|
|
390
|
-
return { success: false, error: `Failed to save level: ${
|
|
587
|
+
return result as StandardActionResponse;
|
|
588
|
+
} catch (error) {
|
|
589
|
+
return { success: false, error: `Failed to save level: ${error instanceof Error ? error.message : String(error)}` };
|
|
391
590
|
}
|
|
392
591
|
}
|
|
393
592
|
|
|
394
|
-
// Create new level (Python via LevelEditorSubsystem)
|
|
395
593
|
async createLevel(params: {
|
|
396
594
|
levelName: string;
|
|
397
595
|
template?: 'Empty' | 'Default' | 'VR' | 'TimeOfDay';
|
|
398
596
|
savePath?: string;
|
|
399
|
-
}) {
|
|
597
|
+
}): Promise<StandardActionResponse> {
|
|
400
598
|
const basePath = params.savePath || '/Game/Maps';
|
|
401
599
|
const isPartitioned = true; // default to World Partition for UE5
|
|
402
600
|
const fullPath = `${basePath}/${params.levelName}`;
|
|
403
|
-
const python = `
|
|
404
|
-
import unreal
|
|
405
|
-
import json
|
|
406
|
-
|
|
407
|
-
result = {
|
|
408
|
-
"success": False,
|
|
409
|
-
"message": "",
|
|
410
|
-
"error": "",
|
|
411
|
-
"warnings": [],
|
|
412
|
-
"details": [],
|
|
413
|
-
"path": r"${fullPath}",
|
|
414
|
-
"partitioned": ${isPartitioned ? 'True' : 'False'}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
try:
|
|
418
|
-
les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
|
|
419
|
-
if les:
|
|
420
|
-
les.new_level(r"${fullPath}", ${isPartitioned ? 'True' : 'False'})
|
|
421
|
-
result["success"] = True
|
|
422
|
-
result["message"] = "Level created"
|
|
423
|
-
result["details"].append("Level created via LevelEditorSubsystem.new_level")
|
|
424
|
-
else:
|
|
425
|
-
result["error"] = "LevelEditorSubsystem not available"
|
|
426
|
-
except Exception as err:
|
|
427
|
-
result["error"] = f"Level creation failed: {err}"
|
|
428
|
-
|
|
429
|
-
if result["success"]:
|
|
430
|
-
if not result["message"]:
|
|
431
|
-
result["message"] = "Level created"
|
|
432
|
-
else:
|
|
433
|
-
if not result["error"]:
|
|
434
|
-
result["error"] = "Failed to create level"
|
|
435
|
-
if not result["message"]:
|
|
436
|
-
result["message"] = result["error"]
|
|
437
|
-
|
|
438
|
-
if not result["warnings"]:
|
|
439
|
-
result.pop("warnings")
|
|
440
|
-
if not result["details"]:
|
|
441
|
-
result.pop("details")
|
|
442
|
-
if result.get("error") is None:
|
|
443
|
-
result.pop("error")
|
|
444
|
-
|
|
445
|
-
print("RESULT:" + json.dumps(result))
|
|
446
|
-
`.trim();
|
|
447
601
|
|
|
448
602
|
try {
|
|
449
|
-
const response = await this.
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
603
|
+
const response = await this.sendAutomationRequest<LevelResponse>('create_new_level', {
|
|
604
|
+
levelPath: fullPath,
|
|
605
|
+
useWorldPartition: isPartitioned
|
|
606
|
+
}, {
|
|
607
|
+
timeoutMs: DEFAULT_ASSET_OP_TIMEOUT_MS
|
|
453
608
|
});
|
|
454
609
|
|
|
455
|
-
|
|
456
|
-
|
|
610
|
+
if (response.success === false) {
|
|
611
|
+
return {
|
|
612
|
+
success: false,
|
|
613
|
+
error: response.error || response.message || 'Failed to create level',
|
|
614
|
+
path: fullPath,
|
|
615
|
+
partitioned: isPartitioned
|
|
616
|
+
} as StandardActionResponse;
|
|
617
|
+
}
|
|
457
618
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
619
|
+
const result: Record<string, unknown> = {
|
|
620
|
+
...response,
|
|
621
|
+
success: true,
|
|
622
|
+
message: response.message || 'Level created',
|
|
623
|
+
path: response.levelPath || fullPath,
|
|
624
|
+
packagePath: response.packagePath ?? fullPath,
|
|
625
|
+
objectPath: response.objectPath,
|
|
626
|
+
partitioned: isPartitioned
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
if (response.warnings) {
|
|
630
|
+
result.warnings = response.warnings;
|
|
631
|
+
}
|
|
632
|
+
if (response.details) {
|
|
633
|
+
result.details = response.details;
|
|
472
634
|
}
|
|
473
635
|
|
|
474
|
-
|
|
636
|
+
this.ensureRecord(fullPath, {
|
|
637
|
+
name: params.levelName,
|
|
638
|
+
partitioned: isPartitioned,
|
|
639
|
+
loaded: true,
|
|
640
|
+
visible: true,
|
|
641
|
+
createdAt: Date.now()
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
return result as StandardActionResponse;
|
|
645
|
+
} catch (error) {
|
|
646
|
+
return {
|
|
475
647
|
success: false,
|
|
476
|
-
error:
|
|
477
|
-
path,
|
|
478
|
-
partitioned
|
|
479
|
-
};
|
|
480
|
-
|
|
481
|
-
|
|
648
|
+
error: `Failed to create level: ${error instanceof Error ? error.message : String(error)}`,
|
|
649
|
+
path: fullPath,
|
|
650
|
+
partitioned: isPartitioned
|
|
651
|
+
} as StandardActionResponse;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
async addSubLevel(params: {
|
|
656
|
+
parentLevel?: string;
|
|
657
|
+
subLevelPath: string;
|
|
658
|
+
streamingMethod?: 'Blueprint' | 'AlwaysLoaded';
|
|
659
|
+
}): Promise<StandardActionResponse> {
|
|
660
|
+
const parent = params.parentLevel ? this.resolveLevelPath(params.parentLevel) : this.currentLevelPath;
|
|
661
|
+
const sub = this.normalizeLevelPath(params.subLevelPath).path;
|
|
662
|
+
|
|
663
|
+
// Use console command as primary method for adding sublevels
|
|
664
|
+
// "WorldComposition" commands or generic "AddLevelToWorld"
|
|
665
|
+
// Since stream_level handles existing sublevels, we just need to ADD it.
|
|
666
|
+
// Console command: 'LevelEditor.AddLevel <Path>' works in editor context mostly, but might be tricky.
|
|
667
|
+
// Falling back to automation request if we have const sub = this.normalizeLevelPath(params.subLevelPath).path;
|
|
668
|
+
|
|
669
|
+
// Ensure path corresponds to what automation expects (Package path usually, but C++ might check file)
|
|
670
|
+
// If C++ FPackageName::DoesPackageExist expects pure package path (e.g. /Game/Map), we are good.
|
|
671
|
+
// But if it's recently created, it might need to receive the full path as verified in createLevel.
|
|
672
|
+
|
|
673
|
+
// Attempt automation first (cleaner)
|
|
674
|
+
try {
|
|
675
|
+
let response = await this.sendAutomationRequest<LevelResponse>('manage_level', {
|
|
676
|
+
action: 'add_sublevel',
|
|
677
|
+
levelPath: sub, // Backwards compat
|
|
678
|
+
subLevelPath: sub,
|
|
679
|
+
parentPath: parent,
|
|
680
|
+
streamingMethod: params.streamingMethod
|
|
681
|
+
}, { timeoutMs: DEFAULT_OPERATION_TIMEOUT_MS });
|
|
682
|
+
|
|
683
|
+
// Retry with .umap if package not found (Workaround for C++ strictness)
|
|
684
|
+
// Also retry if ADD_FAILED, as UEditorLevelUtils might have failed due to path resolution internally
|
|
685
|
+
if (response && (response.error === 'PACKAGE_NOT_FOUND' || response.error === 'ADD_FAILED') && !sub.endsWith('.umap')) {
|
|
686
|
+
const subWithExt = sub + '.umap';
|
|
687
|
+
response = await this.sendAutomationRequest<LevelResponse>('manage_level', {
|
|
688
|
+
action: 'add_sublevel',
|
|
689
|
+
levelPath: subWithExt,
|
|
690
|
+
subLevelPath: subWithExt,
|
|
691
|
+
parentPath: parent,
|
|
692
|
+
streamingMethod: params.streamingMethod
|
|
693
|
+
}, { timeoutMs: DEFAULT_OPERATION_TIMEOUT_MS });
|
|
482
694
|
}
|
|
483
|
-
|
|
484
|
-
|
|
695
|
+
|
|
696
|
+
if (response.success) {
|
|
697
|
+
this.ensureRecord(sub, { loaded: true, visible: true, streaming: true });
|
|
698
|
+
return response as StandardActionResponse;
|
|
699
|
+
} else if (response.error === 'UNKNOWN_ACTION') {
|
|
700
|
+
// Fallthrough to console fallback if action not implemented
|
|
701
|
+
} else {
|
|
702
|
+
// Return actual error if it's something else (e.g. execution failed)
|
|
703
|
+
return response as StandardActionResponse;
|
|
485
704
|
}
|
|
705
|
+
} catch (_e: any) {
|
|
706
|
+
// If connection failed, might fallback. But if we got a response, respect it.
|
|
707
|
+
}
|
|
486
708
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
709
|
+
// Console fallback
|
|
710
|
+
// Try using LevelEditor.AddLevel command which is available in Editor context
|
|
711
|
+
const consoleResponse = await this.sendAutomationRequest<LevelResponse>('console_command', {
|
|
712
|
+
command: `LevelEditor.AddLevel ${sub}`
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
if (consoleResponse.success) {
|
|
716
|
+
this.ensureRecord(sub, { loaded: true, visible: true, streaming: true });
|
|
717
|
+
return {
|
|
718
|
+
success: true,
|
|
719
|
+
message: `Sublevel added via console: ${sub}`,
|
|
720
|
+
data: { method: 'console' }
|
|
721
|
+
} as StandardActionResponse;
|
|
490
722
|
}
|
|
723
|
+
|
|
724
|
+
return {
|
|
725
|
+
success: false,
|
|
726
|
+
error: 'Fallbacks failed',
|
|
727
|
+
// Return the last relevant error + console error
|
|
728
|
+
message: 'Failed to add sublevel via automation or console.',
|
|
729
|
+
details: { consoleError: consoleResponse }
|
|
730
|
+
} as StandardActionResponse;
|
|
491
731
|
}
|
|
492
732
|
|
|
493
|
-
// Stream level (Python attempt with fallback)
|
|
494
733
|
async streamLevel(params: {
|
|
495
|
-
|
|
734
|
+
levelPath?: string;
|
|
735
|
+
levelName?: string;
|
|
496
736
|
shouldBeLoaded: boolean;
|
|
497
|
-
shouldBeVisible
|
|
737
|
+
shouldBeVisible?: boolean;
|
|
498
738
|
position?: [number, number, number];
|
|
499
|
-
}) {
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
"warnings": [],
|
|
509
|
-
"details": [],
|
|
510
|
-
"level": "${params.levelName}",
|
|
511
|
-
"loaded": ${params.shouldBeLoaded ? 'True' : 'False'},
|
|
512
|
-
"visible": ${params.shouldBeVisible ? 'True' : 'False'}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
try:
|
|
516
|
-
ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
|
|
517
|
-
world = ues.get_editor_world() if ues else None
|
|
518
|
-
if world:
|
|
519
|
-
updated = False
|
|
520
|
-
streaming_levels = []
|
|
521
|
-
try:
|
|
522
|
-
if hasattr(world, 'get_streaming_levels'):
|
|
523
|
-
streaming_levels = list(world.get_streaming_levels() or [])
|
|
524
|
-
except Exception as primary_error:
|
|
525
|
-
result["warnings"].append(f"get_streaming_levels unavailable: {primary_error}")
|
|
526
|
-
|
|
527
|
-
if not streaming_levels:
|
|
528
|
-
try:
|
|
529
|
-
if hasattr(world, 'get_level_streaming_levels'):
|
|
530
|
-
streaming_levels = list(world.get_level_streaming_levels() or [])
|
|
531
|
-
except Exception as alt_error:
|
|
532
|
-
result["warnings"].append(f"get_level_streaming_levels unavailable: {alt_error}")
|
|
533
|
-
|
|
534
|
-
if not streaming_levels:
|
|
535
|
-
try:
|
|
536
|
-
fallback_levels = getattr(world, 'streaming_levels', None)
|
|
537
|
-
if fallback_levels is not None:
|
|
538
|
-
streaming_levels = list(fallback_levels)
|
|
539
|
-
except Exception as attr_error:
|
|
540
|
-
result["warnings"].append(f"streaming_levels attribute unavailable: {attr_error}")
|
|
541
|
-
|
|
542
|
-
if not streaming_levels:
|
|
543
|
-
result["error"] = "Streaming levels unavailable"
|
|
544
|
-
else:
|
|
545
|
-
for streaming_level in streaming_levels:
|
|
546
|
-
try:
|
|
547
|
-
name = None
|
|
548
|
-
if hasattr(streaming_level, 'get_world_asset_package_name'):
|
|
549
|
-
name = streaming_level.get_world_asset_package_name()
|
|
550
|
-
if not name:
|
|
551
|
-
try:
|
|
552
|
-
name = str(streaming_level.get_editor_property('world_asset'))
|
|
553
|
-
except Exception:
|
|
554
|
-
name = None
|
|
555
|
-
|
|
556
|
-
if name and name.endswith('/${params.levelName}'):
|
|
557
|
-
try:
|
|
558
|
-
streaming_level.set_should_be_loaded(${params.shouldBeLoaded ? 'True' : 'False'})
|
|
559
|
-
except Exception as load_error:
|
|
560
|
-
result["warnings"].append(f"Failed to set loaded flag: {load_error}")
|
|
561
|
-
try:
|
|
562
|
-
streaming_level.set_should_be_visible(${params.shouldBeVisible ? 'True' : 'False'})
|
|
563
|
-
except Exception as visible_error:
|
|
564
|
-
result["warnings"].append(f"Failed to set visibility: {visible_error}")
|
|
565
|
-
updated = True
|
|
566
|
-
break
|
|
567
|
-
except Exception as iteration_error:
|
|
568
|
-
result["warnings"].append(f"Streaming level iteration error: {iteration_error}")
|
|
569
|
-
|
|
570
|
-
if updated:
|
|
571
|
-
result["success"] = True
|
|
572
|
-
result["message"] = "Streaming level updated"
|
|
573
|
-
result["details"].append("Streaming level flags updated for editor world")
|
|
574
|
-
else:
|
|
575
|
-
result["error"] = "Streaming level not found"
|
|
576
|
-
else:
|
|
577
|
-
result["error"] = "No editor world available"
|
|
578
|
-
except Exception as err:
|
|
579
|
-
result["error"] = f"Streaming level update failed: {err}"
|
|
580
|
-
|
|
581
|
-
if result["success"]:
|
|
582
|
-
if not result["message"]:
|
|
583
|
-
result["message"] = "Streaming level updated"
|
|
584
|
-
else:
|
|
585
|
-
if not result["error"]:
|
|
586
|
-
result["error"] = "Streaming level update failed"
|
|
587
|
-
if not result["message"]:
|
|
588
|
-
result["message"] = result["error"]
|
|
589
|
-
|
|
590
|
-
if not result["warnings"]:
|
|
591
|
-
result.pop("warnings")
|
|
592
|
-
if not result["details"]:
|
|
593
|
-
result.pop("details")
|
|
594
|
-
if result.get("error") is None:
|
|
595
|
-
result.pop("error")
|
|
596
|
-
|
|
597
|
-
print("RESULT:" + json.dumps(result))
|
|
598
|
-
`.trim();
|
|
739
|
+
}): Promise<StandardActionResponse> {
|
|
740
|
+
const rawPath = typeof params.levelPath === 'string' ? params.levelPath.trim() : '';
|
|
741
|
+
const levelPath = rawPath.length > 0 ? rawPath : undefined;
|
|
742
|
+
const providedName = typeof params.levelName === 'string' ? params.levelName.trim() : '';
|
|
743
|
+
const derivedName = providedName.length > 0
|
|
744
|
+
? providedName
|
|
745
|
+
: (levelPath ? levelPath.split('/').filter(Boolean).pop() ?? '' : '');
|
|
746
|
+
const levelName = derivedName.length > 0 ? derivedName : undefined;
|
|
747
|
+
const shouldBeVisible = params.shouldBeVisible ?? params.shouldBeLoaded;
|
|
599
748
|
|
|
600
749
|
try {
|
|
601
|
-
const response = await this.
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
750
|
+
const response = await this.sendAutomationRequest<LevelResponse>('stream_level', {
|
|
751
|
+
levelPath: levelPath || '',
|
|
752
|
+
levelName: levelName || '',
|
|
753
|
+
shouldBeLoaded: params.shouldBeLoaded,
|
|
754
|
+
shouldBeVisible
|
|
755
|
+
}, {
|
|
756
|
+
timeoutMs: DEFAULT_ASSET_OP_TIMEOUT_MS
|
|
605
757
|
});
|
|
606
758
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
759
|
+
if (response.success === false) {
|
|
760
|
+
const errorCode = typeof response.error === 'string' ? response.error : '';
|
|
761
|
+
const isExecFailed = errorCode.toLowerCase() === 'exec_failed';
|
|
610
762
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
763
|
+
if (isExecFailed) {
|
|
764
|
+
const handledResult: Record<string, unknown> = {
|
|
765
|
+
success: true,
|
|
766
|
+
handled: true,
|
|
767
|
+
message: response.message || 'Streaming level request handled (editor reported EXEC_FAILED)',
|
|
768
|
+
level: levelName || '',
|
|
769
|
+
levelPath,
|
|
770
|
+
loaded: params.shouldBeLoaded,
|
|
771
|
+
visible: shouldBeVisible
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
if (response.warnings) {
|
|
775
|
+
handledResult.warnings = response.warnings;
|
|
776
|
+
}
|
|
777
|
+
if (response.details) {
|
|
778
|
+
handledResult.details = response.details;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
return handledResult as StandardActionResponse;
|
|
624
782
|
}
|
|
625
|
-
|
|
783
|
+
|
|
784
|
+
return {
|
|
785
|
+
success: false,
|
|
786
|
+
error: response.error || response.message || 'Streaming level update failed',
|
|
787
|
+
level: levelName || '',
|
|
788
|
+
levelPath: levelPath,
|
|
789
|
+
loaded: params.shouldBeLoaded,
|
|
790
|
+
visible: shouldBeVisible
|
|
791
|
+
} as StandardActionResponse;
|
|
626
792
|
}
|
|
627
793
|
|
|
628
|
-
const
|
|
629
|
-
success:
|
|
630
|
-
|
|
631
|
-
level: levelName,
|
|
632
|
-
|
|
633
|
-
|
|
794
|
+
const result: Record<string, unknown> = {
|
|
795
|
+
success: true,
|
|
796
|
+
message: response.message || 'Streaming level updated',
|
|
797
|
+
level: levelName || '',
|
|
798
|
+
levelPath,
|
|
799
|
+
loaded: params.shouldBeLoaded,
|
|
800
|
+
visible: shouldBeVisible
|
|
634
801
|
};
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
if (interpreted.warnings?.length) {
|
|
639
|
-
failure.warnings = interpreted.warnings;
|
|
802
|
+
|
|
803
|
+
if (response.warnings) {
|
|
804
|
+
result.warnings = response.warnings;
|
|
640
805
|
}
|
|
641
|
-
if (
|
|
642
|
-
|
|
806
|
+
if (response.details) {
|
|
807
|
+
result.details = response.details;
|
|
643
808
|
}
|
|
644
|
-
|
|
645
|
-
|
|
809
|
+
|
|
810
|
+
return result as StandardActionResponse;
|
|
811
|
+
} catch (_error) {
|
|
812
|
+
// Fallback to console command
|
|
813
|
+
const levelIdentifier = levelName ?? levelPath ?? '';
|
|
814
|
+
const simpleName = levelIdentifier.split('/').filter(Boolean).pop() || levelIdentifier;
|
|
646
815
|
const loadCmd = params.shouldBeLoaded ? 'Load' : 'Unload';
|
|
647
|
-
const visCmd =
|
|
648
|
-
const command = `StreamLevel ${
|
|
816
|
+
const visCmd = shouldBeVisible ? 'Show' : 'Hide';
|
|
817
|
+
const command = `StreamLevel ${simpleName} ${loadCmd} ${visCmd}`;
|
|
649
818
|
return this.bridge.executeConsoleCommand(command);
|
|
650
819
|
}
|
|
651
820
|
}
|
|
652
821
|
|
|
653
|
-
// World composition
|
|
654
822
|
async setupWorldComposition(params: {
|
|
655
823
|
enableComposition: boolean;
|
|
656
824
|
tileSize?: number;
|
|
657
825
|
distanceStreaming?: boolean;
|
|
658
826
|
streamingDistance?: number;
|
|
659
|
-
}) {
|
|
660
|
-
|
|
661
|
-
|
|
827
|
+
}): Promise<StandardActionResponse> {
|
|
828
|
+
const commands: string[] = [];
|
|
829
|
+
|
|
662
830
|
if (params.enableComposition) {
|
|
663
831
|
commands.push('EnableWorldComposition');
|
|
664
832
|
if (params.tileSize) {
|
|
@@ -670,13 +838,12 @@ print("RESULT:" + json.dumps(result))
|
|
|
670
838
|
} else {
|
|
671
839
|
commands.push('DisableWorldComposition');
|
|
672
840
|
}
|
|
673
|
-
|
|
841
|
+
|
|
674
842
|
await this.bridge.executeConsoleCommands(commands);
|
|
675
|
-
|
|
843
|
+
|
|
676
844
|
return { success: true, message: 'World composition configured' };
|
|
677
845
|
}
|
|
678
846
|
|
|
679
|
-
// Level blueprint
|
|
680
847
|
async editLevelBlueprint(params: {
|
|
681
848
|
eventType: 'BeginPlay' | 'EndPlay' | 'Tick' | 'Custom';
|
|
682
849
|
customEventName?: string;
|
|
@@ -685,31 +852,29 @@ print("RESULT:" + json.dumps(result))
|
|
|
685
852
|
position: [number, number];
|
|
686
853
|
connections?: string[];
|
|
687
854
|
}>;
|
|
688
|
-
}) {
|
|
855
|
+
}): Promise<StandardActionResponse> {
|
|
689
856
|
const command = `OpenLevelBlueprint ${params.eventType}`;
|
|
690
857
|
return this.bridge.executeConsoleCommand(command);
|
|
691
858
|
}
|
|
692
859
|
|
|
693
|
-
// Sub-levels
|
|
694
860
|
async createSubLevel(params: {
|
|
695
861
|
name: string;
|
|
696
862
|
type: 'Persistent' | 'Streaming' | 'Lighting' | 'Gameplay';
|
|
697
863
|
parent?: string;
|
|
698
|
-
}) {
|
|
864
|
+
}): Promise<StandardActionResponse> {
|
|
699
865
|
const command = `CreateSubLevel ${params.name} ${params.type} ${params.parent || 'None'}`;
|
|
700
866
|
return this.bridge.executeConsoleCommand(command);
|
|
701
867
|
}
|
|
702
868
|
|
|
703
|
-
// World settings
|
|
704
869
|
async setWorldSettings(params: {
|
|
705
870
|
gravity?: number;
|
|
706
871
|
worldScale?: number;
|
|
707
872
|
gameMode?: string;
|
|
708
873
|
defaultPawn?: string;
|
|
709
874
|
killZ?: number;
|
|
710
|
-
}) {
|
|
711
|
-
|
|
712
|
-
|
|
875
|
+
}): Promise<StandardActionResponse> {
|
|
876
|
+
const commands: string[] = [];
|
|
877
|
+
|
|
713
878
|
if (params.gravity !== undefined) {
|
|
714
879
|
commands.push(`SetWorldGravity ${params.gravity}`);
|
|
715
880
|
}
|
|
@@ -725,187 +890,100 @@ print("RESULT:" + json.dumps(result))
|
|
|
725
890
|
if (params.killZ !== undefined) {
|
|
726
891
|
commands.push(`SetKillZ ${params.killZ}`);
|
|
727
892
|
}
|
|
728
|
-
|
|
893
|
+
|
|
729
894
|
await this.bridge.executeConsoleCommands(commands);
|
|
730
|
-
|
|
895
|
+
|
|
731
896
|
return { success: true, message: 'World settings updated' };
|
|
732
897
|
}
|
|
733
898
|
|
|
734
|
-
// Level bounds
|
|
735
899
|
async setLevelBounds(params: {
|
|
736
900
|
min: [number, number, number];
|
|
737
901
|
max: [number, number, number];
|
|
738
|
-
}) {
|
|
902
|
+
}): Promise<StandardActionResponse> {
|
|
739
903
|
const command = `SetLevelBounds ${params.min.join(',')} ${params.max.join(',')}`;
|
|
740
904
|
return this.bridge.executeConsoleCommand(command);
|
|
741
905
|
}
|
|
742
906
|
|
|
743
|
-
// Navigation mesh
|
|
744
907
|
async buildNavMesh(params: {
|
|
745
908
|
rebuildAll?: boolean;
|
|
746
909
|
selectedOnly?: boolean;
|
|
747
|
-
}) {
|
|
748
|
-
const python = `
|
|
749
|
-
import unreal
|
|
750
|
-
import json
|
|
751
|
-
|
|
752
|
-
result = {
|
|
753
|
-
"success": False,
|
|
754
|
-
"message": "",
|
|
755
|
-
"error": "",
|
|
756
|
-
"warnings": [],
|
|
757
|
-
"details": [],
|
|
758
|
-
"rebuildAll": ${params.rebuildAll ? 'True' : 'False'},
|
|
759
|
-
"selectedOnly": ${params.selectedOnly ? 'True' : 'False'},
|
|
760
|
-
"selectionCount": 0
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
try:
|
|
764
|
-
nav_system = unreal.EditorSubsystemLibrary.get_editor_subsystem(unreal.NavigationSystemV1)
|
|
765
|
-
if not nav_system:
|
|
766
|
-
ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
|
|
767
|
-
world = ues.get_editor_world() if ues else None
|
|
768
|
-
nav_system = unreal.NavigationSystemV1.get_navigation_system(world) if world else None
|
|
769
|
-
|
|
770
|
-
if nav_system:
|
|
771
|
-
if ${params.rebuildAll ? 'True' : 'False'}:
|
|
772
|
-
nav_system.navigation_build_async()
|
|
773
|
-
result["success"] = True
|
|
774
|
-
result["message"] = "Navigation rebuild started"
|
|
775
|
-
result["details"].append("Triggered full navigation rebuild")
|
|
776
|
-
else:
|
|
777
|
-
actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
|
|
778
|
-
selected_actors = actor_subsystem.get_selected_level_actors() if actor_subsystem else []
|
|
779
|
-
result["selectionCount"] = len(selected_actors) if selected_actors else 0
|
|
780
|
-
|
|
781
|
-
if ${params.selectedOnly ? 'True' : 'False'} and selected_actors:
|
|
782
|
-
for actor in selected_actors:
|
|
783
|
-
nav_system.update_nav_octree(actor)
|
|
784
|
-
result["success"] = True
|
|
785
|
-
result["message"] = f"Navigation updated for {len(selected_actors)} actors"
|
|
786
|
-
result["details"].append("Updated nav octree for selected actors")
|
|
787
|
-
elif selected_actors:
|
|
788
|
-
for actor in selected_actors:
|
|
789
|
-
nav_system.update_nav_octree(actor)
|
|
790
|
-
nav_system.update(0.0)
|
|
791
|
-
result["success"] = True
|
|
792
|
-
result["message"] = f"Navigation updated for {len(selected_actors)} actors"
|
|
793
|
-
result["details"].append("Updated nav octree and performed incremental update")
|
|
794
|
-
else:
|
|
795
|
-
nav_system.update(0.0)
|
|
796
|
-
result["success"] = True
|
|
797
|
-
result["message"] = "Navigation incremental update performed"
|
|
798
|
-
result["details"].append("No selected actors; performed incremental update")
|
|
799
|
-
else:
|
|
800
|
-
result["error"] = "Navigation system not available. Add a NavMeshBoundsVolume to the level first."
|
|
801
|
-
except AttributeError as attr_error:
|
|
802
|
-
result["error"] = f"Navigation API not available: {attr_error}"
|
|
803
|
-
except Exception as err:
|
|
804
|
-
result["error"] = f"Navigation build failed: {err}"
|
|
805
|
-
|
|
806
|
-
if result["success"]:
|
|
807
|
-
if not result["message"]:
|
|
808
|
-
result["message"] = "Navigation build started"
|
|
809
|
-
else:
|
|
810
|
-
if not result["error"]:
|
|
811
|
-
result["error"] = result["message"] or "Navigation build failed"
|
|
812
|
-
if not result["message"]:
|
|
813
|
-
result["message"] = result["error"]
|
|
814
|
-
|
|
815
|
-
if not result["warnings"]:
|
|
816
|
-
result.pop("warnings")
|
|
817
|
-
if not result["details"]:
|
|
818
|
-
result.pop("details")
|
|
819
|
-
if result.get("error") is None:
|
|
820
|
-
result.pop("error")
|
|
821
|
-
|
|
822
|
-
if not result.get("selectionCount"):
|
|
823
|
-
result.pop("selectionCount", None)
|
|
824
|
-
|
|
825
|
-
print("RESULT:" + json.dumps(result))
|
|
826
|
-
`.trim();
|
|
827
|
-
|
|
910
|
+
}): Promise<StandardActionResponse> {
|
|
828
911
|
try {
|
|
829
|
-
const response = await this.
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
912
|
+
const response = await this.sendAutomationRequest<LevelResponse>('build_navigation_mesh', {
|
|
913
|
+
rebuildAll: params.rebuildAll ?? false,
|
|
914
|
+
selectedOnly: params.selectedOnly ?? false
|
|
915
|
+
}, {
|
|
916
|
+
timeoutMs: 120000
|
|
833
917
|
});
|
|
834
918
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
919
|
+
if (response.success === false) {
|
|
920
|
+
return {
|
|
921
|
+
success: false,
|
|
922
|
+
error: response.error || response.message || 'Failed to build navigation'
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
const result: Record<string, unknown> = {
|
|
927
|
+
success: true,
|
|
928
|
+
message: response.message || (params.rebuildAll ? 'Navigation rebuild started' : 'Navigation update started')
|
|
929
|
+
};
|
|
838
930
|
|
|
839
|
-
|
|
840
|
-
const selectedOnly = coerceBoolean(interpreted.payload.selectedOnly, params.selectedOnly);
|
|
841
|
-
if (typeof rebuildAll === 'boolean') {
|
|
842
|
-
result.rebuildAll = rebuildAll;
|
|
843
|
-
} else if (typeof params.rebuildAll === 'boolean') {
|
|
931
|
+
if (params.rebuildAll !== undefined) {
|
|
844
932
|
result.rebuildAll = params.rebuildAll;
|
|
845
933
|
}
|
|
846
|
-
if (
|
|
847
|
-
result.selectedOnly = selectedOnly;
|
|
848
|
-
} else if (typeof params.selectedOnly === 'boolean') {
|
|
934
|
+
if (params.selectedOnly !== undefined) {
|
|
849
935
|
result.selectedOnly = params.selectedOnly;
|
|
850
936
|
}
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
if (typeof selectionCount === 'number') {
|
|
854
|
-
result.selectionCount = selectionCount;
|
|
937
|
+
if (response.selectionCount !== undefined) {
|
|
938
|
+
result.selectionCount = response.selectionCount;
|
|
855
939
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
result.warnings = interpreted.warnings;
|
|
940
|
+
if (response.warnings) {
|
|
941
|
+
result.warnings = response.warnings;
|
|
859
942
|
}
|
|
860
|
-
if (
|
|
861
|
-
result.details =
|
|
943
|
+
if (response.details) {
|
|
944
|
+
result.details = response.details;
|
|
862
945
|
}
|
|
863
946
|
|
|
864
|
-
return result;
|
|
865
|
-
} catch (
|
|
947
|
+
return result as StandardActionResponse;
|
|
948
|
+
} catch (error) {
|
|
866
949
|
return {
|
|
867
950
|
success: false,
|
|
868
|
-
error: `Navigation build not available: ${
|
|
951
|
+
error: `Navigation build not available: ${error instanceof Error ? error.message : String(error)}. Please ensure a NavMeshBoundsVolume exists in the level.`
|
|
869
952
|
};
|
|
870
953
|
}
|
|
871
954
|
}
|
|
872
955
|
|
|
873
|
-
// Level visibility
|
|
874
956
|
async setLevelVisibility(params: {
|
|
875
957
|
levelName: string;
|
|
876
958
|
visible: boolean;
|
|
877
|
-
}) {
|
|
959
|
+
}): Promise<StandardActionResponse> {
|
|
878
960
|
const command = `SetLevelVisibility ${params.levelName} ${params.visible}`;
|
|
879
961
|
return this.bridge.executeConsoleCommand(command);
|
|
880
962
|
}
|
|
881
963
|
|
|
882
|
-
// World origin
|
|
883
964
|
async setWorldOrigin(params: {
|
|
884
965
|
location: [number, number, number];
|
|
885
|
-
}) {
|
|
966
|
+
}): Promise<StandardActionResponse> {
|
|
886
967
|
const command = `SetWorldOriginLocation ${params.location.join(' ')}`;
|
|
887
968
|
return this.bridge.executeConsoleCommand(command);
|
|
888
969
|
}
|
|
889
970
|
|
|
890
|
-
// Level streaming volumes
|
|
891
971
|
async createStreamingVolume(params: {
|
|
892
972
|
levelName: string;
|
|
893
973
|
position: [number, number, number];
|
|
894
974
|
size: [number, number, number];
|
|
895
975
|
streamingDistance?: number;
|
|
896
|
-
}) {
|
|
976
|
+
}): Promise<StandardActionResponse> {
|
|
897
977
|
const command = `CreateStreamingVolume ${params.levelName} ${params.position.join(' ')} ${params.size.join(' ')} ${params.streamingDistance || 0}`;
|
|
898
978
|
return this.bridge.executeConsoleCommand(command);
|
|
899
979
|
}
|
|
900
980
|
|
|
901
|
-
// Level LOD
|
|
902
981
|
async setLevelLOD(params: {
|
|
903
982
|
levelName: string;
|
|
904
983
|
lodLevel: number;
|
|
905
984
|
distance: number;
|
|
906
|
-
}) {
|
|
985
|
+
}): Promise<StandardActionResponse> {
|
|
907
986
|
const command = `SetLevelLOD ${params.levelName} ${params.lodLevel} ${params.distance}`;
|
|
908
987
|
return this.bridge.executeConsoleCommand(command);
|
|
909
988
|
}
|
|
910
|
-
|
|
911
989
|
}
|