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