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
|
@@ -0,0 +1,2614 @@
|
|
|
1
|
+
#include "Async/Async.h"
|
|
2
|
+
#include "Dom/JsonObject.h"
|
|
3
|
+
#include "GameFramework/Actor.h"
|
|
4
|
+
#include "McpAutomationBridgeGlobals.h"
|
|
5
|
+
#include "McpAutomationBridgeHelpers.h"
|
|
6
|
+
#include "McpAutomationBridgeSubsystem.h"
|
|
7
|
+
#include "Misc/ScopeExit.h"
|
|
8
|
+
|
|
9
|
+
#if WITH_EDITOR
|
|
10
|
+
#include "EditorAssetLibrary.h"
|
|
11
|
+
#include "EngineUtils.h"
|
|
12
|
+
#if __has_include("Subsystems/EditorActorSubsystem.h")
|
|
13
|
+
#include "Subsystems/EditorActorSubsystem.h"
|
|
14
|
+
#elif __has_include("EditorActorSubsystem.h")
|
|
15
|
+
#include "EditorActorSubsystem.h"
|
|
16
|
+
#endif
|
|
17
|
+
#if __has_include("Subsystems/UnrealEditorSubsystem.h")
|
|
18
|
+
#include "Subsystems/UnrealEditorSubsystem.h"
|
|
19
|
+
#define MCP_HAS_UNREALEDITOR_SUBSYSTEM 1
|
|
20
|
+
#elif __has_include("UnrealEditorSubsystem.h")
|
|
21
|
+
#include "UnrealEditorSubsystem.h"
|
|
22
|
+
#define MCP_HAS_UNREALEDITOR_SUBSYSTEM 1
|
|
23
|
+
#endif
|
|
24
|
+
#if __has_include("Subsystems/LevelEditorSubsystem.h")
|
|
25
|
+
#include "Subsystems/LevelEditorSubsystem.h"
|
|
26
|
+
#define MCP_HAS_LEVELEDITOR_SUBSYSTEM 1
|
|
27
|
+
#elif __has_include("LevelEditorSubsystem.h")
|
|
28
|
+
#include "LevelEditorSubsystem.h"
|
|
29
|
+
#define MCP_HAS_LEVELEDITOR_SUBSYSTEM 1
|
|
30
|
+
#endif
|
|
31
|
+
#if __has_include("Subsystems/AssetEditorSubsystem.h")
|
|
32
|
+
#include "Subsystems/AssetEditorSubsystem.h"
|
|
33
|
+
#elif __has_include("AssetEditorSubsystem.h")
|
|
34
|
+
#include "AssetEditorSubsystem.h"
|
|
35
|
+
#endif
|
|
36
|
+
// Additional editor headers for viewport control
|
|
37
|
+
#include "Components/LightComponent.h"
|
|
38
|
+
#include "Editor.h"
|
|
39
|
+
#include "Modules/ModuleManager.h"
|
|
40
|
+
|
|
41
|
+
#if __has_include("LevelEditor.h")
|
|
42
|
+
#include "LevelEditor.h"
|
|
43
|
+
#define MCP_HAS_LEVEL_EDITOR_MODULE 1
|
|
44
|
+
#else
|
|
45
|
+
#define MCP_HAS_LEVEL_EDITOR_MODULE 0
|
|
46
|
+
#endif
|
|
47
|
+
#if __has_include("Settings/LevelEditorPlaySettings.h")
|
|
48
|
+
#include "Settings/LevelEditorPlaySettings.h"
|
|
49
|
+
#define MCP_HAS_LEVEL_EDITOR_PLAY_SETTINGS 1
|
|
50
|
+
#else
|
|
51
|
+
#define MCP_HAS_LEVEL_EDITOR_PLAY_SETTINGS 0
|
|
52
|
+
#endif
|
|
53
|
+
#include "Components/PrimitiveComponent.h"
|
|
54
|
+
#include "EditorViewportClient.h"
|
|
55
|
+
#include "Engine/Blueprint.h"
|
|
56
|
+
|
|
57
|
+
#if __has_include("FileHelpers.h")
|
|
58
|
+
#include "FileHelpers.h"
|
|
59
|
+
#endif
|
|
60
|
+
#include "Animation/SkeletalMeshActor.h"
|
|
61
|
+
#include "Components/ActorComponent.h"
|
|
62
|
+
#include "Components/SceneComponent.h"
|
|
63
|
+
#include "Components/StaticMeshComponent.h"
|
|
64
|
+
#include "Engine/EngineTypes.h"
|
|
65
|
+
#include "Engine/SkeletalMesh.h"
|
|
66
|
+
#include "Engine/StaticMesh.h"
|
|
67
|
+
#include "Engine/StaticMeshActor.h"
|
|
68
|
+
#include "Engine/World.h"
|
|
69
|
+
#include "Exporters/Exporter.h"
|
|
70
|
+
#include "Misc/OutputDevice.h"
|
|
71
|
+
|
|
72
|
+
#endif
|
|
73
|
+
|
|
74
|
+
// Helper class for capturing export output
|
|
75
|
+
/* UE5.6: Use built-in FStringOutputDevice from UnrealString.h */
|
|
76
|
+
|
|
77
|
+
// Helper functions
|
|
78
|
+
// (ExtractVectorField and ExtractRotatorField moved to
|
|
79
|
+
// McpAutomationBridgeHelpers.h)
|
|
80
|
+
|
|
81
|
+
AActor *UMcpAutomationBridgeSubsystem::FindActorByName(const FString &Target) {
|
|
82
|
+
#if WITH_EDITOR
|
|
83
|
+
if (Target.IsEmpty() || !GEditor)
|
|
84
|
+
return nullptr;
|
|
85
|
+
|
|
86
|
+
// Priority: PIE World if active
|
|
87
|
+
if (GEditor->PlayWorld) {
|
|
88
|
+
for (TActorIterator<AActor> It(GEditor->PlayWorld); It; ++It) {
|
|
89
|
+
AActor *A = *It;
|
|
90
|
+
if (!A)
|
|
91
|
+
continue;
|
|
92
|
+
if (A->GetActorLabel().Equals(Target, ESearchCase::IgnoreCase) ||
|
|
93
|
+
A->GetName().Equals(Target, ESearchCase::IgnoreCase) ||
|
|
94
|
+
A->GetPathName().Equals(Target, ESearchCase::IgnoreCase)) {
|
|
95
|
+
return A;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// If not found in PIE, do we fall back to Editor World?
|
|
99
|
+
// Probably not, because interacting with Editor world during PIE is
|
|
100
|
+
// confusing. But for "Editor subsystems" usage, we usually want Editor
|
|
101
|
+
// world. Let's fallback if not found, just in case.
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
UEditorActorSubsystem *ActorSS =
|
|
105
|
+
GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
|
|
106
|
+
if (!ActorSS)
|
|
107
|
+
return nullptr;
|
|
108
|
+
|
|
109
|
+
TArray<AActor *> AllActors = ActorSS->GetAllLevelActors();
|
|
110
|
+
AActor *ExactMatch = nullptr;
|
|
111
|
+
TArray<AActor *> FuzzyMatches;
|
|
112
|
+
|
|
113
|
+
for (AActor *A : AllActors) {
|
|
114
|
+
if (!A)
|
|
115
|
+
continue;
|
|
116
|
+
if (A->GetActorLabel().Equals(Target, ESearchCase::IgnoreCase) ||
|
|
117
|
+
A->GetName().Equals(Target, ESearchCase::IgnoreCase) ||
|
|
118
|
+
A->GetPathName().Equals(Target, ESearchCase::IgnoreCase)) {
|
|
119
|
+
ExactMatch = A;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
// Collect fuzzy matches
|
|
123
|
+
if (A->GetActorLabel().Contains(Target, ESearchCase::IgnoreCase)) {
|
|
124
|
+
FuzzyMatches.Add(A);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (ExactMatch) {
|
|
129
|
+
return ExactMatch;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// If no exact match, check fuzzy matches
|
|
133
|
+
if (FuzzyMatches.Num() == 1) {
|
|
134
|
+
return FuzzyMatches[0];
|
|
135
|
+
} else if (FuzzyMatches.Num() > 1) {
|
|
136
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Warning,
|
|
137
|
+
TEXT("FindActorByName: Ambiguous match for '%s'. Found %d matches."),
|
|
138
|
+
*Target, FuzzyMatches.Num());
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Fallback: try to load as asset if it looks like a path
|
|
142
|
+
if (Target.StartsWith(TEXT("/"))) {
|
|
143
|
+
if (UObject *Obj = UEditorAssetLibrary::LoadAsset(Target)) {
|
|
144
|
+
return Cast<AActor>(Obj);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
#endif
|
|
148
|
+
return nullptr;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlActorSpawn(
|
|
152
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
153
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
154
|
+
#if WITH_EDITOR
|
|
155
|
+
FString ClassPath;
|
|
156
|
+
Payload->TryGetStringField(TEXT("classPath"), ClassPath);
|
|
157
|
+
FString ActorName;
|
|
158
|
+
Payload->TryGetStringField(TEXT("actorName"), ActorName);
|
|
159
|
+
FVector Location =
|
|
160
|
+
ExtractVectorField(Payload, TEXT("location"), FVector::ZeroVector);
|
|
161
|
+
FRotator Rotation =
|
|
162
|
+
ExtractRotatorField(Payload, TEXT("rotation"), FRotator::ZeroRotator);
|
|
163
|
+
|
|
164
|
+
UClass *ResolvedClass = nullptr;
|
|
165
|
+
FString MeshPath;
|
|
166
|
+
Payload->TryGetStringField(TEXT("meshPath"), MeshPath);
|
|
167
|
+
UStaticMesh *ResolvedStaticMesh = nullptr;
|
|
168
|
+
USkeletalMesh *ResolvedSkeletalMesh = nullptr;
|
|
169
|
+
|
|
170
|
+
// Skip LoadAsset for script classes (e.g. /Script/Engine.CameraActor) to
|
|
171
|
+
// avoid LogEditorAssetSubsystem errors
|
|
172
|
+
if ((ClassPath.StartsWith(TEXT("/")) || ClassPath.Contains(TEXT("/"))) &&
|
|
173
|
+
!ClassPath.StartsWith(TEXT("/Script/"))) {
|
|
174
|
+
if (UObject *Loaded = UEditorAssetLibrary::LoadAsset(ClassPath)) {
|
|
175
|
+
if (UBlueprint *BP = Cast<UBlueprint>(Loaded))
|
|
176
|
+
ResolvedClass = BP->GeneratedClass;
|
|
177
|
+
else if (UClass *C = Cast<UClass>(Loaded))
|
|
178
|
+
ResolvedClass = C;
|
|
179
|
+
else if (UStaticMesh *Mesh = Cast<UStaticMesh>(Loaded))
|
|
180
|
+
ResolvedStaticMesh = Mesh;
|
|
181
|
+
else if (USkeletalMesh *SkelMesh = Cast<USkeletalMesh>(Loaded))
|
|
182
|
+
ResolvedSkeletalMesh = SkelMesh;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (!ResolvedClass && !ResolvedStaticMesh && !ResolvedSkeletalMesh)
|
|
186
|
+
ResolvedClass = ResolveClassByName(ClassPath);
|
|
187
|
+
|
|
188
|
+
// If explicit mesh path provided for a general spawn request
|
|
189
|
+
if (!ResolvedStaticMesh && !ResolvedSkeletalMesh && !MeshPath.IsEmpty()) {
|
|
190
|
+
if (UObject *MeshObj = UEditorAssetLibrary::LoadAsset(MeshPath)) {
|
|
191
|
+
ResolvedStaticMesh = Cast<UStaticMesh>(MeshObj);
|
|
192
|
+
if (!ResolvedStaticMesh)
|
|
193
|
+
ResolvedSkeletalMesh = Cast<USkeletalMesh>(MeshObj);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Force StaticMeshActor if we have a resolved mesh, regardless of class input
|
|
198
|
+
// (unless it's a specific subclass)
|
|
199
|
+
bool bSpawnStaticMeshActor = (ResolvedStaticMesh != nullptr);
|
|
200
|
+
bool bSpawnSkeletalMeshActor = (ResolvedSkeletalMesh != nullptr);
|
|
201
|
+
|
|
202
|
+
if (!bSpawnStaticMeshActor && !bSpawnSkeletalMeshActor && ResolvedClass) {
|
|
203
|
+
bSpawnStaticMeshActor =
|
|
204
|
+
ResolvedClass->IsChildOf(AStaticMeshActor::StaticClass());
|
|
205
|
+
if (!bSpawnStaticMeshActor)
|
|
206
|
+
bSpawnSkeletalMeshActor =
|
|
207
|
+
ResolvedClass->IsChildOf(ASkeletalMeshActor::StaticClass());
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Explicitly use StaticMeshActor class if we have a mesh but no class, or if
|
|
211
|
+
// we decided to spawn a static mesh actor
|
|
212
|
+
if (bSpawnStaticMeshActor && !ResolvedClass) {
|
|
213
|
+
ResolvedClass = AStaticMeshActor::StaticClass();
|
|
214
|
+
} else if (bSpawnSkeletalMeshActor && !ResolvedClass) {
|
|
215
|
+
ResolvedClass = ASkeletalMeshActor::StaticClass();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!ResolvedClass && !bSpawnStaticMeshActor && !bSpawnSkeletalMeshActor) {
|
|
219
|
+
const FString ErrorMsg =
|
|
220
|
+
FString::Printf(TEXT("Class not found: %s. Verify plugin is enabled if "
|
|
221
|
+
"using a plugin class."),
|
|
222
|
+
*ClassPath);
|
|
223
|
+
SendStandardErrorResponse(this, Socket, RequestId, TEXT("CLASS_NOT_FOUND"),
|
|
224
|
+
ErrorMsg);
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
UEditorActorSubsystem *ActorSS =
|
|
229
|
+
GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
|
|
230
|
+
AActor *Spawned = nullptr;
|
|
231
|
+
|
|
232
|
+
// Support PIE spawning
|
|
233
|
+
UWorld *TargetWorld = (GEditor->PlayWorld) ? GEditor->PlayWorld : nullptr;
|
|
234
|
+
|
|
235
|
+
if (TargetWorld) {
|
|
236
|
+
// PIE Path
|
|
237
|
+
FActorSpawnParameters SpawnParams;
|
|
238
|
+
SpawnParams.SpawnCollisionHandlingOverride =
|
|
239
|
+
ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
|
|
240
|
+
|
|
241
|
+
UClass *ClassToSpawn =
|
|
242
|
+
ResolvedClass
|
|
243
|
+
? ResolvedClass
|
|
244
|
+
: (bSpawnStaticMeshActor ? AStaticMeshActor::StaticClass()
|
|
245
|
+
: (bSpawnSkeletalMeshActor
|
|
246
|
+
? ASkeletalMeshActor::StaticClass()
|
|
247
|
+
: AActor::StaticClass()));
|
|
248
|
+
Spawned = TargetWorld->SpawnActor(ClassToSpawn, &Location, &Rotation,
|
|
249
|
+
SpawnParams);
|
|
250
|
+
|
|
251
|
+
if (Spawned) {
|
|
252
|
+
if (bSpawnStaticMeshActor) {
|
|
253
|
+
if (AStaticMeshActor *StaticMeshActor =
|
|
254
|
+
Cast<AStaticMeshActor>(Spawned)) {
|
|
255
|
+
if (UStaticMeshComponent *MeshComponent =
|
|
256
|
+
StaticMeshActor->GetStaticMeshComponent()) {
|
|
257
|
+
if (ResolvedStaticMesh) {
|
|
258
|
+
MeshComponent->SetStaticMesh(ResolvedStaticMesh);
|
|
259
|
+
}
|
|
260
|
+
MeshComponent->SetMobility(EComponentMobility::Movable);
|
|
261
|
+
// PIE actors don't need MarkRenderStateDirty in the same way, but
|
|
262
|
+
// it doesn't hurt
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
} else if (bSpawnSkeletalMeshActor) {
|
|
266
|
+
if (ASkeletalMeshActor *SkelActor = Cast<ASkeletalMeshActor>(Spawned)) {
|
|
267
|
+
if (USkeletalMeshComponent *SkelComp =
|
|
268
|
+
SkelActor->GetSkeletalMeshComponent()) {
|
|
269
|
+
if (ResolvedSkeletalMesh) {
|
|
270
|
+
SkelComp->SetSkeletalMesh(ResolvedSkeletalMesh);
|
|
271
|
+
}
|
|
272
|
+
SkelComp->SetMobility(EComponentMobility::Movable);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
} else {
|
|
278
|
+
// Editor Path
|
|
279
|
+
if (bSpawnStaticMeshActor) {
|
|
280
|
+
Spawned = ActorSS->SpawnActorFromClass(
|
|
281
|
+
ResolvedClass ? ResolvedClass : AStaticMeshActor::StaticClass(),
|
|
282
|
+
Location, Rotation);
|
|
283
|
+
if (Spawned) {
|
|
284
|
+
Spawned->SetActorLocationAndRotation(Location, Rotation, false, nullptr,
|
|
285
|
+
ETeleportType::TeleportPhysics);
|
|
286
|
+
if (AStaticMeshActor *StaticMeshActor =
|
|
287
|
+
Cast<AStaticMeshActor>(Spawned)) {
|
|
288
|
+
if (UStaticMeshComponent *MeshComponent =
|
|
289
|
+
StaticMeshActor->GetStaticMeshComponent()) {
|
|
290
|
+
if (ResolvedStaticMesh) {
|
|
291
|
+
MeshComponent->SetStaticMesh(ResolvedStaticMesh);
|
|
292
|
+
}
|
|
293
|
+
MeshComponent->SetMobility(EComponentMobility::Movable);
|
|
294
|
+
MeshComponent->MarkRenderStateDirty();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
} else if (bSpawnSkeletalMeshActor) {
|
|
299
|
+
Spawned = ActorSS->SpawnActorFromClass(
|
|
300
|
+
ResolvedClass ? ResolvedClass : ASkeletalMeshActor::StaticClass(),
|
|
301
|
+
Location, Rotation);
|
|
302
|
+
if (Spawned) {
|
|
303
|
+
Spawned->SetActorLocationAndRotation(Location, Rotation, false, nullptr,
|
|
304
|
+
ETeleportType::TeleportPhysics);
|
|
305
|
+
if (ASkeletalMeshActor *SkelActor = Cast<ASkeletalMeshActor>(Spawned)) {
|
|
306
|
+
if (USkeletalMeshComponent *SkelComp =
|
|
307
|
+
SkelActor->GetSkeletalMeshComponent()) {
|
|
308
|
+
if (ResolvedSkeletalMesh) {
|
|
309
|
+
SkelComp->SetSkeletalMesh(ResolvedSkeletalMesh);
|
|
310
|
+
}
|
|
311
|
+
SkelComp->SetMobility(EComponentMobility::Movable);
|
|
312
|
+
SkelComp->MarkRenderStateDirty();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
Spawned = ActorSS->SpawnActorFromClass(ResolvedClass, Location, Rotation);
|
|
318
|
+
if (Spawned) {
|
|
319
|
+
Spawned->SetActorLocationAndRotation(Location, Rotation, false, nullptr,
|
|
320
|
+
ETeleportType::TeleportPhysics);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (!Spawned) {
|
|
326
|
+
SendStandardErrorResponse(this, Socket, RequestId, TEXT("SPAWN_FAILED"),
|
|
327
|
+
TEXT("Failed to spawn actor"));
|
|
328
|
+
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (!ActorName.IsEmpty()) {
|
|
333
|
+
Spawned->SetActorLabel(ActorName);
|
|
334
|
+
} else {
|
|
335
|
+
// Auto-generate a friendly label from the mesh or class name
|
|
336
|
+
FString BaseName;
|
|
337
|
+
if (ResolvedStaticMesh) {
|
|
338
|
+
BaseName = ResolvedStaticMesh->GetName();
|
|
339
|
+
} else if (ResolvedSkeletalMesh) {
|
|
340
|
+
BaseName = ResolvedSkeletalMesh->GetName();
|
|
341
|
+
} else if (ResolvedClass) {
|
|
342
|
+
BaseName = ResolvedClass->GetName();
|
|
343
|
+
if (BaseName.EndsWith(TEXT("_C"))) {
|
|
344
|
+
BaseName.RemoveFromEnd(TEXT("_C"));
|
|
345
|
+
}
|
|
346
|
+
} else {
|
|
347
|
+
BaseName = TEXT("Actor");
|
|
348
|
+
}
|
|
349
|
+
Spawned->SetActorLabel(BaseName);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
353
|
+
Data->SetStringField(TEXT("id"), Spawned->GetActorLabel());
|
|
354
|
+
Data->SetStringField(TEXT("name"), Spawned->GetActorLabel());
|
|
355
|
+
Data->SetStringField(TEXT("objectPath"), Spawned->GetPathName());
|
|
356
|
+
// Provide the resolved class path useful for referencing
|
|
357
|
+
if (ResolvedClass)
|
|
358
|
+
Data->SetStringField(TEXT("classPath"), ResolvedClass->GetPathName());
|
|
359
|
+
else
|
|
360
|
+
Data->SetStringField(TEXT("classPath"), ClassPath);
|
|
361
|
+
|
|
362
|
+
if (ResolvedStaticMesh)
|
|
363
|
+
Data->SetStringField(TEXT("meshPath"), ResolvedStaticMesh->GetPathName());
|
|
364
|
+
else if (ResolvedSkeletalMesh)
|
|
365
|
+
Data->SetStringField(TEXT("meshPath"), ResolvedSkeletalMesh->GetPathName());
|
|
366
|
+
|
|
367
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
|
|
368
|
+
TEXT("ControlActor: Spawned actor '%s'"), *Spawned->GetActorLabel());
|
|
369
|
+
|
|
370
|
+
SendAutomationResponse(Socket, RequestId, true, TEXT("Actor spawned"), Data);
|
|
371
|
+
return true;
|
|
372
|
+
|
|
373
|
+
#else
|
|
374
|
+
return false;
|
|
375
|
+
#endif
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlActorSpawnBlueprint(
|
|
379
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
380
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
381
|
+
#if WITH_EDITOR
|
|
382
|
+
FString BlueprintPath;
|
|
383
|
+
Payload->TryGetStringField(TEXT("blueprintPath"), BlueprintPath);
|
|
384
|
+
if (BlueprintPath.IsEmpty()) {
|
|
385
|
+
SendAutomationResponse(Socket, RequestId, false,
|
|
386
|
+
TEXT("Blueprint path required"), nullptr,
|
|
387
|
+
TEXT("INVALID_ARGUMENT"));
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
FString ActorName;
|
|
392
|
+
Payload->TryGetStringField(TEXT("actorName"), ActorName);
|
|
393
|
+
FVector Location =
|
|
394
|
+
ExtractVectorField(Payload, TEXT("location"), FVector::ZeroVector);
|
|
395
|
+
FRotator Rotation =
|
|
396
|
+
ExtractRotatorField(Payload, TEXT("rotation"), FRotator::ZeroRotator);
|
|
397
|
+
|
|
398
|
+
UClass *ResolvedClass = nullptr;
|
|
399
|
+
|
|
400
|
+
// Prefer the same blueprint resolution heuristics used by manage_blueprint
|
|
401
|
+
// so that short names and package paths behave consistently.
|
|
402
|
+
FString NormalizedPath;
|
|
403
|
+
FString LoadError;
|
|
404
|
+
if (!BlueprintPath.IsEmpty()) {
|
|
405
|
+
UBlueprint *BlueprintAsset =
|
|
406
|
+
LoadBlueprintAsset(BlueprintPath, NormalizedPath, LoadError);
|
|
407
|
+
if (BlueprintAsset && BlueprintAsset->GeneratedClass) {
|
|
408
|
+
ResolvedClass = BlueprintAsset->GeneratedClass;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (!ResolvedClass && (BlueprintPath.StartsWith(TEXT("/")) ||
|
|
413
|
+
BlueprintPath.Contains(TEXT("/")))) {
|
|
414
|
+
if (UObject *Loaded = UEditorAssetLibrary::LoadAsset(BlueprintPath)) {
|
|
415
|
+
if (UBlueprint *BP = Cast<UBlueprint>(Loaded))
|
|
416
|
+
ResolvedClass = BP->GeneratedClass;
|
|
417
|
+
else if (UClass *C = Cast<UClass>(Loaded))
|
|
418
|
+
ResolvedClass = C;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (!ResolvedClass)
|
|
422
|
+
ResolvedClass = ResolveClassByName(BlueprintPath);
|
|
423
|
+
|
|
424
|
+
if (!ResolvedClass) {
|
|
425
|
+
TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
|
|
426
|
+
Resp->SetStringField(TEXT("error"), TEXT("Blueprint class not found"));
|
|
427
|
+
SendAutomationResponse(Socket, RequestId, false,
|
|
428
|
+
TEXT("Blueprint class not found"), Resp,
|
|
429
|
+
TEXT("CLASS_NOT_FOUND"));
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
UEditorActorSubsystem *ActorSS =
|
|
434
|
+
GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
|
|
435
|
+
|
|
436
|
+
// Debug log the received location
|
|
437
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
|
|
438
|
+
TEXT("spawn_blueprint: Location=(%f, %f, %f) Rotation=(%f, %f, %f)"),
|
|
439
|
+
Location.X, Location.Y, Location.Z, Rotation.Pitch, Rotation.Yaw,
|
|
440
|
+
Rotation.Roll);
|
|
441
|
+
|
|
442
|
+
AActor *Spawned = nullptr;
|
|
443
|
+
UWorld *TargetWorld = (GEditor->PlayWorld) ? GEditor->PlayWorld : nullptr;
|
|
444
|
+
|
|
445
|
+
if (TargetWorld) {
|
|
446
|
+
// PIE Path
|
|
447
|
+
FActorSpawnParameters SpawnParams;
|
|
448
|
+
SpawnParams.SpawnCollisionHandlingOverride =
|
|
449
|
+
ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
|
|
450
|
+
Spawned = TargetWorld->SpawnActor(ResolvedClass, &Location, &Rotation,
|
|
451
|
+
SpawnParams);
|
|
452
|
+
// Ensure physics/teleport if needed, though SpawnActor should handle it.
|
|
453
|
+
} else {
|
|
454
|
+
// Editor Path
|
|
455
|
+
Spawned = ActorSS->SpawnActorFromClass(ResolvedClass, Location, Rotation);
|
|
456
|
+
// Explicitly set location and rotation in case SpawnActorFromClass didn't
|
|
457
|
+
// apply them correctly (legacy fix)
|
|
458
|
+
if (Spawned) {
|
|
459
|
+
Spawned->SetActorLocationAndRotation(Location, Rotation, false, nullptr,
|
|
460
|
+
ETeleportType::TeleportPhysics);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (!Spawned) {
|
|
465
|
+
TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
|
|
466
|
+
Resp->SetStringField(TEXT("error"), TEXT("Failed to spawn blueprint"));
|
|
467
|
+
SendAutomationResponse(Socket, RequestId, false,
|
|
468
|
+
TEXT("Failed to spawn blueprint"), Resp,
|
|
469
|
+
TEXT("SPAWN_FAILED"));
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (!ActorName.IsEmpty())
|
|
474
|
+
Spawned->SetActorLabel(ActorName);
|
|
475
|
+
|
|
476
|
+
TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
|
|
477
|
+
Resp->SetBoolField(TEXT("success"), true);
|
|
478
|
+
Resp->SetStringField(TEXT("actorName"), Spawned->GetActorLabel());
|
|
479
|
+
Resp->SetStringField(TEXT("actorPath"), Spawned->GetPathName());
|
|
480
|
+
Resp->SetStringField(TEXT("classPath"), ResolvedClass->GetPathName());
|
|
481
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
|
|
482
|
+
TEXT("ControlActor: Spawned blueprint '%s'"),
|
|
483
|
+
*Spawned->GetActorLabel());
|
|
484
|
+
SendAutomationResponse(Socket, RequestId, true, TEXT("Blueprint spawned"),
|
|
485
|
+
Resp, FString());
|
|
486
|
+
return true;
|
|
487
|
+
#else
|
|
488
|
+
return false;
|
|
489
|
+
#endif
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlActorDelete(
|
|
493
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
494
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
495
|
+
#if WITH_EDITOR
|
|
496
|
+
TArray<FString> Targets;
|
|
497
|
+
const TArray<TSharedPtr<FJsonValue>> *NamesArray = nullptr;
|
|
498
|
+
if (Payload->TryGetArrayField(TEXT("actorNames"), NamesArray) && NamesArray) {
|
|
499
|
+
for (const TSharedPtr<FJsonValue> &Entry : *NamesArray) {
|
|
500
|
+
if (Entry.IsValid() && Entry->Type == EJson::String) {
|
|
501
|
+
const FString Value = Entry->AsString().TrimStartAndEnd();
|
|
502
|
+
if (!Value.IsEmpty())
|
|
503
|
+
Targets.AddUnique(Value);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
FString SingleName;
|
|
509
|
+
if (Targets.Num() == 0) {
|
|
510
|
+
Payload->TryGetStringField(TEXT("actorName"), SingleName);
|
|
511
|
+
if (!SingleName.IsEmpty())
|
|
512
|
+
Targets.AddUnique(SingleName);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (Targets.Num() == 0) {
|
|
516
|
+
SendStandardErrorResponse(this, Socket, RequestId, TEXT("INVALID_ARGUMENT"),
|
|
517
|
+
TEXT("actorName or actorNames required"));
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
UEditorActorSubsystem *ActorSS =
|
|
522
|
+
GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
|
|
523
|
+
TArray<FString> Deleted;
|
|
524
|
+
TArray<FString> Missing;
|
|
525
|
+
|
|
526
|
+
for (const FString &Name : Targets) {
|
|
527
|
+
AActor *Found = FindActorByName(Name);
|
|
528
|
+
if (!Found) {
|
|
529
|
+
Missing.Add(Name);
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
if (ActorSS->DestroyActor(Found)) {
|
|
533
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
|
|
534
|
+
TEXT("ControlActor: Deleted actor '%s'"), *Name);
|
|
535
|
+
Deleted.Add(Name);
|
|
536
|
+
} else
|
|
537
|
+
Missing.Add(Name);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const bool bAllDeleted = Missing.Num() == 0;
|
|
541
|
+
const bool bAnyDeleted = Deleted.Num() > 0;
|
|
542
|
+
TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
|
|
543
|
+
Resp->SetBoolField(TEXT("success"), bAllDeleted);
|
|
544
|
+
Resp->SetNumberField(TEXT("deletedCount"), Deleted.Num());
|
|
545
|
+
|
|
546
|
+
TArray<TSharedPtr<FJsonValue>> DeletedArray;
|
|
547
|
+
for (const FString &Name : Deleted)
|
|
548
|
+
DeletedArray.Add(MakeShared<FJsonValueString>(Name));
|
|
549
|
+
Resp->SetArrayField(TEXT("deleted"), DeletedArray);
|
|
550
|
+
|
|
551
|
+
if (Missing.Num() > 0) {
|
|
552
|
+
TArray<TSharedPtr<FJsonValue>> MissingArray;
|
|
553
|
+
for (const FString &Name : Missing)
|
|
554
|
+
MissingArray.Add(MakeShared<FJsonValueString>(Name));
|
|
555
|
+
Resp->SetArrayField(TEXT("missing"), MissingArray);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
FString Message;
|
|
559
|
+
FString ErrorCode;
|
|
560
|
+
if (!bAnyDeleted && Missing.Num() > 0) {
|
|
561
|
+
Message = TEXT("Actors not found");
|
|
562
|
+
ErrorCode = TEXT("NOT_FOUND");
|
|
563
|
+
} else {
|
|
564
|
+
Message = bAllDeleted ? TEXT("Actors deleted")
|
|
565
|
+
: TEXT("Some actors could not be deleted");
|
|
566
|
+
ErrorCode = bAllDeleted ? FString() : TEXT("DELETE_PARTIAL");
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (!bAllDeleted && Missing.Num() > 0 && !bAnyDeleted) {
|
|
570
|
+
SendStandardErrorResponse(this, Socket, RequestId, ErrorCode, Message);
|
|
571
|
+
} else {
|
|
572
|
+
SendStandardSuccessResponse(this, Socket, RequestId, Message, Resp);
|
|
573
|
+
}
|
|
574
|
+
return true;
|
|
575
|
+
#else
|
|
576
|
+
return false;
|
|
577
|
+
#endif
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlActorApplyForce(
|
|
581
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
582
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
583
|
+
#if WITH_EDITOR
|
|
584
|
+
FString TargetName;
|
|
585
|
+
Payload->TryGetStringField(TEXT("actorName"), TargetName);
|
|
586
|
+
FVector ForceVector =
|
|
587
|
+
ExtractVectorField(Payload, TEXT("force"), FVector::ZeroVector);
|
|
588
|
+
|
|
589
|
+
AActor *Found = FindActorByName(TargetName);
|
|
590
|
+
if (!Found) {
|
|
591
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
|
|
592
|
+
nullptr, TEXT("ACTOR_NOT_FOUND"));
|
|
593
|
+
return true;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
UPrimitiveComponent *Prim =
|
|
597
|
+
Found->FindComponentByClass<UPrimitiveComponent>();
|
|
598
|
+
if (!Prim) {
|
|
599
|
+
if (UStaticMeshComponent *SMC =
|
|
600
|
+
Found->FindComponentByClass<UStaticMeshComponent>())
|
|
601
|
+
Prim = SMC;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (!Prim) {
|
|
605
|
+
SendAutomationResponse(Socket, RequestId, false,
|
|
606
|
+
TEXT("No component to apply force"), nullptr,
|
|
607
|
+
TEXT("NO_COMPONENT"));
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (Prim->Mobility == EComponentMobility::Static)
|
|
612
|
+
Prim->SetMobility(EComponentMobility::Movable);
|
|
613
|
+
|
|
614
|
+
// Ensure collision is enabled for physics
|
|
615
|
+
if (Prim->GetCollisionEnabled() == ECollisionEnabled::NoCollision) {
|
|
616
|
+
Prim->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Check if collision geometry exists (common failure for empty
|
|
620
|
+
// StaticMeshActors)
|
|
621
|
+
if (UStaticMeshComponent *SMC = Cast<UStaticMeshComponent>(Prim)) {
|
|
622
|
+
if (!SMC->GetStaticMesh()) {
|
|
623
|
+
SendStandardErrorResponse(
|
|
624
|
+
this, Socket, RequestId, TEXT("PHYSICS_FAILED"),
|
|
625
|
+
TEXT("StaticMeshComponent has no StaticMesh assigned."), nullptr);
|
|
626
|
+
return true;
|
|
627
|
+
}
|
|
628
|
+
if (!SMC->GetStaticMesh()->GetBodySetup()) {
|
|
629
|
+
SendStandardErrorResponse(
|
|
630
|
+
this, Socket, RequestId, TEXT("PHYSICS_FAILED"),
|
|
631
|
+
TEXT("StaticMesh has no collision geometry (BodySetup is null)."),
|
|
632
|
+
nullptr);
|
|
633
|
+
return true;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (!Prim->IsSimulatingPhysics()) {
|
|
638
|
+
Prim->SetSimulatePhysics(true);
|
|
639
|
+
// Must recreate physics state for the body to be properly initialized in
|
|
640
|
+
// Editor
|
|
641
|
+
Prim->RecreatePhysicsState();
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
Prim->AddForce(ForceVector);
|
|
645
|
+
Prim->WakeAllRigidBodies();
|
|
646
|
+
Prim->MarkRenderStateDirty();
|
|
647
|
+
|
|
648
|
+
// Verify physics state
|
|
649
|
+
const bool bIsSimulating = Prim->IsSimulatingPhysics();
|
|
650
|
+
|
|
651
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
652
|
+
Data->SetBoolField(TEXT("simulating"), bIsSimulating);
|
|
653
|
+
TArray<TSharedPtr<FJsonValue>> Applied;
|
|
654
|
+
Applied.Add(MakeShared<FJsonValueNumber>(ForceVector.X));
|
|
655
|
+
Applied.Add(MakeShared<FJsonValueNumber>(ForceVector.Y));
|
|
656
|
+
Applied.Add(MakeShared<FJsonValueNumber>(ForceVector.Z));
|
|
657
|
+
Data->SetArrayField(TEXT("applied"), Applied);
|
|
658
|
+
Data->SetStringField(TEXT("actorName"), Found->GetActorLabel());
|
|
659
|
+
|
|
660
|
+
if (!bIsSimulating) {
|
|
661
|
+
FString FailureReason = TEXT("Failed to enable physics simulation.");
|
|
662
|
+
if (Prim->GetCollisionEnabled() == ECollisionEnabled::NoCollision) {
|
|
663
|
+
FailureReason += TEXT(" Collision is disabled.");
|
|
664
|
+
} else if (Prim->Mobility != EComponentMobility::Movable) {
|
|
665
|
+
FailureReason += TEXT(" Component is not Movable.");
|
|
666
|
+
}
|
|
667
|
+
SendStandardErrorResponse(this, Socket, RequestId, TEXT("PHYSICS_FAILED"),
|
|
668
|
+
FailureReason, Data);
|
|
669
|
+
return true;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
|
|
673
|
+
TEXT("ControlActor: Applied force to '%s'"), *Found->GetActorLabel());
|
|
674
|
+
SendStandardSuccessResponse(this, Socket, RequestId, TEXT("Force applied"),
|
|
675
|
+
Data);
|
|
676
|
+
return true;
|
|
677
|
+
#else
|
|
678
|
+
return false;
|
|
679
|
+
#endif
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlActorSetTransform(
|
|
683
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
684
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
685
|
+
#if WITH_EDITOR
|
|
686
|
+
FString TargetName;
|
|
687
|
+
Payload->TryGetStringField(TEXT("actorName"), TargetName);
|
|
688
|
+
if (TargetName.IsEmpty()) {
|
|
689
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("actorName required"),
|
|
690
|
+
nullptr, TEXT("INVALID_ARGUMENT"));
|
|
691
|
+
return true;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
AActor *Found = FindActorByName(TargetName);
|
|
695
|
+
if (!Found) {
|
|
696
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
|
|
697
|
+
nullptr, TEXT("ACTOR_NOT_FOUND"));
|
|
698
|
+
return true;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
FVector Location =
|
|
702
|
+
ExtractVectorField(Payload, TEXT("location"), Found->GetActorLocation());
|
|
703
|
+
FRotator Rotation =
|
|
704
|
+
ExtractRotatorField(Payload, TEXT("rotation"), Found->GetActorRotation());
|
|
705
|
+
FVector Scale =
|
|
706
|
+
ExtractVectorField(Payload, TEXT("scale"), Found->GetActorScale3D());
|
|
707
|
+
|
|
708
|
+
Found->Modify();
|
|
709
|
+
Found->SetActorLocation(Location, false, nullptr,
|
|
710
|
+
ETeleportType::TeleportPhysics);
|
|
711
|
+
Found->SetActorRotation(Rotation, ETeleportType::TeleportPhysics);
|
|
712
|
+
Found->SetActorScale3D(Scale);
|
|
713
|
+
Found->MarkComponentsRenderStateDirty();
|
|
714
|
+
Found->MarkPackageDirty();
|
|
715
|
+
|
|
716
|
+
// Verify transform
|
|
717
|
+
const FVector NewLoc = Found->GetActorLocation();
|
|
718
|
+
const FRotator NewRot = Found->GetActorRotation();
|
|
719
|
+
const FVector NewScale = Found->GetActorScale3D();
|
|
720
|
+
|
|
721
|
+
const bool bLocMatch = NewLoc.Equals(Location, 1.0f); // 1 unit tolerance
|
|
722
|
+
// Rotation comparison is tricky due to normalization, skipping strict check
|
|
723
|
+
// for now but logging if very different
|
|
724
|
+
const bool bScaleMatch = NewScale.Equals(Scale, 0.01f);
|
|
725
|
+
|
|
726
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
727
|
+
Data->SetStringField(TEXT("actorName"), Found->GetActorLabel());
|
|
728
|
+
|
|
729
|
+
auto MakeArray = [](const FVector &Vec) {
|
|
730
|
+
TArray<TSharedPtr<FJsonValue>> Arr;
|
|
731
|
+
Arr.Add(MakeShared<FJsonValueNumber>(Vec.X));
|
|
732
|
+
Arr.Add(MakeShared<FJsonValueNumber>(Vec.Y));
|
|
733
|
+
Arr.Add(MakeShared<FJsonValueNumber>(Vec.Z));
|
|
734
|
+
return Arr;
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
Data->SetArrayField(TEXT("location"), MakeArray(NewLoc));
|
|
738
|
+
Data->SetArrayField(TEXT("scale"), MakeArray(NewScale));
|
|
739
|
+
|
|
740
|
+
if (!bLocMatch || !bScaleMatch) {
|
|
741
|
+
SendStandardErrorResponse(this, Socket, RequestId,
|
|
742
|
+
TEXT("TRANSFORM_MISMATCH"),
|
|
743
|
+
TEXT("Failed to set transform exactly"), Data);
|
|
744
|
+
return true;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
|
|
748
|
+
TEXT("ControlActor: Set transform for '%s'"), *Found->GetActorLabel());
|
|
749
|
+
SendStandardSuccessResponse(this, Socket, RequestId,
|
|
750
|
+
TEXT("Actor transform updated"), Data);
|
|
751
|
+
return true;
|
|
752
|
+
#else
|
|
753
|
+
return false;
|
|
754
|
+
#endif
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlActorGetTransform(
|
|
758
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
759
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
760
|
+
#if WITH_EDITOR
|
|
761
|
+
FString TargetName;
|
|
762
|
+
Payload->TryGetStringField(TEXT("actorName"), TargetName);
|
|
763
|
+
if (TargetName.IsEmpty()) {
|
|
764
|
+
SendStandardErrorResponse(this, Socket, RequestId, TEXT("INVALID_ARGUMENT"),
|
|
765
|
+
TEXT("actorName required"));
|
|
766
|
+
return true;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
AActor *Found = FindActorByName(TargetName);
|
|
770
|
+
if (!Found) {
|
|
771
|
+
SendStandardErrorResponse(this, Socket, RequestId, TEXT("ACTOR_NOT_FOUND"),
|
|
772
|
+
TEXT("Actor not found"));
|
|
773
|
+
return true;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const FTransform Current = Found->GetActorTransform();
|
|
777
|
+
const FVector Location = Current.GetLocation();
|
|
778
|
+
const FRotator Rotation = Current.GetRotation().Rotator();
|
|
779
|
+
const FVector Scale = Current.GetScale3D();
|
|
780
|
+
|
|
781
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
782
|
+
|
|
783
|
+
auto MakeArray = [](const FVector &Vec) {
|
|
784
|
+
TArray<TSharedPtr<FJsonValue>> Arr;
|
|
785
|
+
Arr.Add(MakeShared<FJsonValueNumber>(Vec.X));
|
|
786
|
+
Arr.Add(MakeShared<FJsonValueNumber>(Vec.Y));
|
|
787
|
+
Arr.Add(MakeShared<FJsonValueNumber>(Vec.Z));
|
|
788
|
+
return Arr;
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
Data->SetArrayField(TEXT("location"), MakeArray(Location));
|
|
792
|
+
TArray<TSharedPtr<FJsonValue>> RotArray;
|
|
793
|
+
RotArray.Add(MakeShared<FJsonValueNumber>(Rotation.Pitch));
|
|
794
|
+
RotArray.Add(MakeShared<FJsonValueNumber>(Rotation.Yaw));
|
|
795
|
+
RotArray.Add(MakeShared<FJsonValueNumber>(Rotation.Roll));
|
|
796
|
+
Data->SetArrayField(TEXT("rotation"), RotArray);
|
|
797
|
+
Data->SetArrayField(TEXT("scale"), MakeArray(Scale));
|
|
798
|
+
|
|
799
|
+
SendStandardSuccessResponse(this, Socket, RequestId,
|
|
800
|
+
TEXT("Actor transform retrieved"), Data);
|
|
801
|
+
return true;
|
|
802
|
+
#else
|
|
803
|
+
return false;
|
|
804
|
+
#endif
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlActorSetVisibility(
|
|
808
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
809
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
810
|
+
#if WITH_EDITOR
|
|
811
|
+
FString TargetName;
|
|
812
|
+
Payload->TryGetStringField(TEXT("actorName"), TargetName);
|
|
813
|
+
if (TargetName.IsEmpty()) {
|
|
814
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("actorName required"),
|
|
815
|
+
nullptr, TEXT("INVALID_ARGUMENT"));
|
|
816
|
+
return true;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
bool bVisible = true;
|
|
820
|
+
if (Payload->HasField(TEXT("visible")))
|
|
821
|
+
Payload->TryGetBoolField(TEXT("visible"), bVisible);
|
|
822
|
+
|
|
823
|
+
AActor *Found = FindActorByName(TargetName);
|
|
824
|
+
if (!Found) {
|
|
825
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
|
|
826
|
+
nullptr, TEXT("ACTOR_NOT_FOUND"));
|
|
827
|
+
return true;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
Found->Modify();
|
|
831
|
+
Found->SetActorHiddenInGame(!bVisible);
|
|
832
|
+
Found->SetActorEnableCollision(bVisible);
|
|
833
|
+
|
|
834
|
+
for (UActorComponent *Comp : Found->GetComponents()) {
|
|
835
|
+
if (!Comp)
|
|
836
|
+
continue;
|
|
837
|
+
if (UPrimitiveComponent *Prim = Cast<UPrimitiveComponent>(Comp)) {
|
|
838
|
+
Prim->SetVisibility(bVisible, true);
|
|
839
|
+
Prim->SetHiddenInGame(!bVisible);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
Found->MarkComponentsRenderStateDirty();
|
|
844
|
+
Found->MarkPackageDirty();
|
|
845
|
+
|
|
846
|
+
// Verify visibility state
|
|
847
|
+
const bool bIsHidden = Found->IsHidden();
|
|
848
|
+
const bool bStateMatches = (bIsHidden == !bVisible);
|
|
849
|
+
|
|
850
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
851
|
+
Data->SetBoolField(TEXT("visible"), !bIsHidden);
|
|
852
|
+
Data->SetStringField(TEXT("actorName"), Found->GetActorLabel());
|
|
853
|
+
|
|
854
|
+
if (!bStateMatches) {
|
|
855
|
+
SendStandardErrorResponse(this, Socket, RequestId,
|
|
856
|
+
TEXT("VISIBILITY_MISMATCH"),
|
|
857
|
+
TEXT("Failed to set actor visibility"), Data);
|
|
858
|
+
return true;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
|
|
862
|
+
TEXT("ControlActor: Set visibility to %s for '%s'"),
|
|
863
|
+
bVisible ? TEXT("True") : TEXT("False"), *Found->GetActorLabel());
|
|
864
|
+
SendStandardSuccessResponse(this, Socket, RequestId,
|
|
865
|
+
TEXT("Actor visibility updated"), Data);
|
|
866
|
+
return true;
|
|
867
|
+
#else
|
|
868
|
+
return false;
|
|
869
|
+
#endif
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlActorAddComponent(
|
|
873
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
874
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
875
|
+
#if WITH_EDITOR
|
|
876
|
+
FString TargetName;
|
|
877
|
+
Payload->TryGetStringField(TEXT("actorName"), TargetName);
|
|
878
|
+
if (TargetName.IsEmpty()) {
|
|
879
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("actorName required"),
|
|
880
|
+
nullptr, TEXT("INVALID_ARGUMENT"));
|
|
881
|
+
return true;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
FString ComponentType;
|
|
885
|
+
Payload->TryGetStringField(TEXT("componentType"), ComponentType);
|
|
886
|
+
if (ComponentType.IsEmpty()) {
|
|
887
|
+
SendAutomationResponse(Socket, RequestId, false,
|
|
888
|
+
TEXT("componentType required"), nullptr,
|
|
889
|
+
TEXT("INVALID_ARGUMENT"));
|
|
890
|
+
return true;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
FString ComponentName;
|
|
894
|
+
Payload->TryGetStringField(TEXT("componentName"), ComponentName);
|
|
895
|
+
|
|
896
|
+
AActor *Found = FindActorByName(TargetName);
|
|
897
|
+
if (!Found) {
|
|
898
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
|
|
899
|
+
nullptr, TEXT("ACTOR_NOT_FOUND"));
|
|
900
|
+
return true;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
UClass *ComponentClass = ResolveClassByName(ComponentType);
|
|
904
|
+
if (!ComponentClass ||
|
|
905
|
+
!ComponentClass->IsChildOf(UActorComponent::StaticClass())) {
|
|
906
|
+
SendAutomationResponse(Socket, RequestId, false,
|
|
907
|
+
TEXT("Component class not found"), nullptr,
|
|
908
|
+
TEXT("CLASS_NOT_FOUND"));
|
|
909
|
+
return true;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
if (ComponentName.TrimStartAndEnd().IsEmpty())
|
|
913
|
+
ComponentName = FString::Printf(TEXT("%s_%d"), *ComponentClass->GetName(),
|
|
914
|
+
FMath::Rand());
|
|
915
|
+
|
|
916
|
+
FName DesiredName = FName(*ComponentName);
|
|
917
|
+
UActorComponent *NewComponent = NewObject<UActorComponent>(
|
|
918
|
+
Found, ComponentClass, DesiredName, RF_Transactional);
|
|
919
|
+
if (!NewComponent) {
|
|
920
|
+
SendAutomationResponse(Socket, RequestId, false,
|
|
921
|
+
TEXT("Failed to create component"), nullptr,
|
|
922
|
+
TEXT("CREATE_COMPONENT_FAILED"));
|
|
923
|
+
return true;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
Found->Modify();
|
|
927
|
+
NewComponent->SetFlags(RF_Transactional);
|
|
928
|
+
Found->AddInstanceComponent(NewComponent);
|
|
929
|
+
NewComponent->OnComponentCreated();
|
|
930
|
+
|
|
931
|
+
if (USceneComponent *SceneComp = Cast<USceneComponent>(NewComponent)) {
|
|
932
|
+
if (Found->GetRootComponent() && !SceneComp->GetAttachParent()) {
|
|
933
|
+
SceneComp->SetupAttachment(Found->GetRootComponent());
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Force lights to be movable to ensure they work without baking (Issue #6
|
|
938
|
+
// fix) We check for "LightComponent" class name to avoid dependency issues if
|
|
939
|
+
// header is obscure, but ULightComponent is standard.
|
|
940
|
+
if (NewComponent->IsA(ULightComponent::StaticClass())) {
|
|
941
|
+
if (USceneComponent *SC = Cast<USceneComponent>(NewComponent)) {
|
|
942
|
+
SC->SetMobility(EComponentMobility::Movable);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Special handling for StaticMeshComponent meshPath convenience
|
|
947
|
+
if (UStaticMeshComponent *SMC = Cast<UStaticMeshComponent>(NewComponent)) {
|
|
948
|
+
FString MeshPath;
|
|
949
|
+
if (Payload->TryGetStringField(TEXT("meshPath"), MeshPath) &&
|
|
950
|
+
!MeshPath.IsEmpty()) {
|
|
951
|
+
if (UObject *LoadedMesh = UEditorAssetLibrary::LoadAsset(MeshPath)) {
|
|
952
|
+
if (UStaticMesh *Mesh = Cast<UStaticMesh>(LoadedMesh)) {
|
|
953
|
+
SMC->SetStaticMesh(Mesh);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
TArray<FString> AppliedProperties;
|
|
960
|
+
TArray<FString> PropertyWarnings;
|
|
961
|
+
const TSharedPtr<FJsonObject> *PropertiesPtr = nullptr;
|
|
962
|
+
if (Payload->TryGetObjectField(TEXT("properties"), PropertiesPtr) &&
|
|
963
|
+
PropertiesPtr && (*PropertiesPtr).IsValid()) {
|
|
964
|
+
for (const auto &Pair : (*PropertiesPtr)->Values) {
|
|
965
|
+
FProperty *Property = ComponentClass->FindPropertyByName(*Pair.Key);
|
|
966
|
+
if (!Property) {
|
|
967
|
+
PropertyWarnings.Add(
|
|
968
|
+
FString::Printf(TEXT("Property not found: %s"), *Pair.Key));
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
FString ApplyError;
|
|
972
|
+
if (ApplyJsonValueToProperty(NewComponent, Property, Pair.Value,
|
|
973
|
+
ApplyError))
|
|
974
|
+
AppliedProperties.Add(Pair.Key);
|
|
975
|
+
else
|
|
976
|
+
PropertyWarnings.Add(FString::Printf(TEXT("Failed to set %s: %s"),
|
|
977
|
+
*Pair.Key, *ApplyError));
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
NewComponent->RegisterComponent();
|
|
982
|
+
if (USceneComponent *SceneComp = Cast<USceneComponent>(NewComponent))
|
|
983
|
+
SceneComp->UpdateComponentToWorld();
|
|
984
|
+
NewComponent->MarkPackageDirty();
|
|
985
|
+
Found->MarkPackageDirty();
|
|
986
|
+
|
|
987
|
+
TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
|
|
988
|
+
Resp->SetBoolField(TEXT("success"), true);
|
|
989
|
+
Resp->SetStringField(TEXT("componentName"), NewComponent->GetName());
|
|
990
|
+
Resp->SetStringField(TEXT("componentPath"), NewComponent->GetPathName());
|
|
991
|
+
Resp->SetStringField(TEXT("componentClass"), ComponentClass->GetPathName());
|
|
992
|
+
if (AppliedProperties.Num() > 0) {
|
|
993
|
+
TArray<TSharedPtr<FJsonValue>> PropsArray;
|
|
994
|
+
for (const FString &PropName : AppliedProperties)
|
|
995
|
+
PropsArray.Add(MakeShared<FJsonValueString>(PropName));
|
|
996
|
+
Resp->SetArrayField(TEXT("appliedProperties"), PropsArray);
|
|
997
|
+
}
|
|
998
|
+
if (PropertyWarnings.Num() > 0) {
|
|
999
|
+
TArray<TSharedPtr<FJsonValue>> WarnArray;
|
|
1000
|
+
for (const FString &Warning : PropertyWarnings)
|
|
1001
|
+
WarnArray.Add(MakeShared<FJsonValueString>(Warning));
|
|
1002
|
+
Resp->SetArrayField(TEXT("warnings"), WarnArray);
|
|
1003
|
+
}
|
|
1004
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
|
|
1005
|
+
TEXT("ControlActor: Added component '%s' to '%s'"),
|
|
1006
|
+
*NewComponent->GetName(), *Found->GetActorLabel());
|
|
1007
|
+
SendAutomationResponse(Socket, RequestId, true, TEXT("Component added"), Resp,
|
|
1008
|
+
FString());
|
|
1009
|
+
return true;
|
|
1010
|
+
#else
|
|
1011
|
+
return false;
|
|
1012
|
+
#endif
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlActorSetComponentProperties(
|
|
1016
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
1017
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
1018
|
+
#if WITH_EDITOR
|
|
1019
|
+
FString TargetName;
|
|
1020
|
+
Payload->TryGetStringField(TEXT("actorName"), TargetName);
|
|
1021
|
+
if (TargetName.IsEmpty()) {
|
|
1022
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("actorName required"),
|
|
1023
|
+
nullptr, TEXT("INVALID_ARGUMENT"));
|
|
1024
|
+
return true;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
FString ComponentName;
|
|
1028
|
+
Payload->TryGetStringField(TEXT("componentName"), ComponentName);
|
|
1029
|
+
if (ComponentName.IsEmpty()) {
|
|
1030
|
+
SendAutomationResponse(Socket, RequestId, false,
|
|
1031
|
+
TEXT("componentName required"), nullptr,
|
|
1032
|
+
TEXT("INVALID_ARGUMENT"));
|
|
1033
|
+
return true;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const TSharedPtr<FJsonObject> *PropertiesPtr = nullptr;
|
|
1037
|
+
if (!(Payload->TryGetObjectField(TEXT("properties"), PropertiesPtr) &&
|
|
1038
|
+
PropertiesPtr && PropertiesPtr->IsValid())) {
|
|
1039
|
+
SendAutomationResponse(Socket, RequestId, false,
|
|
1040
|
+
TEXT("properties object required"), nullptr,
|
|
1041
|
+
TEXT("INVALID_ARGUMENT"));
|
|
1042
|
+
return true;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
AActor *Found = FindActorByName(TargetName);
|
|
1046
|
+
if (!Found) {
|
|
1047
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
|
|
1048
|
+
nullptr, TEXT("ACTOR_NOT_FOUND"));
|
|
1049
|
+
return true;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
UActorComponent *TargetComponent = nullptr;
|
|
1053
|
+
for (UActorComponent *Comp : Found->GetComponents()) {
|
|
1054
|
+
if (!Comp)
|
|
1055
|
+
continue;
|
|
1056
|
+
if (Comp->GetName().Equals(ComponentName, ESearchCase::IgnoreCase)) {
|
|
1057
|
+
TargetComponent = Comp;
|
|
1058
|
+
break;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
if (!TargetComponent) {
|
|
1063
|
+
SendAutomationResponse(Socket, RequestId, false,
|
|
1064
|
+
TEXT("Component not found"), nullptr,
|
|
1065
|
+
TEXT("COMPONENT_NOT_FOUND"));
|
|
1066
|
+
return true;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
TArray<FString> AppliedProperties;
|
|
1070
|
+
TArray<FString> PropertyWarnings;
|
|
1071
|
+
UClass *ComponentClass = TargetComponent->GetClass();
|
|
1072
|
+
TargetComponent->Modify();
|
|
1073
|
+
|
|
1074
|
+
// PRIORITY: Apply Mobility FIRST.
|
|
1075
|
+
// Physics simulation fails if the component is generic "Static".
|
|
1076
|
+
// Scan for Mobility key case-insensitively to ensure we find it regardless of
|
|
1077
|
+
// JSON casing
|
|
1078
|
+
const TSharedPtr<FJsonValue> *MobilityVal = nullptr;
|
|
1079
|
+
FString MobilityKey;
|
|
1080
|
+
for (const auto &Pair : (*PropertiesPtr)->Values) {
|
|
1081
|
+
if (Pair.Key.Equals(TEXT("Mobility"), ESearchCase::IgnoreCase)) {
|
|
1082
|
+
MobilityVal = &Pair.Value;
|
|
1083
|
+
MobilityKey = Pair.Key;
|
|
1084
|
+
break;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
if (MobilityVal) {
|
|
1089
|
+
if (USceneComponent *SC = Cast<USceneComponent>(TargetComponent)) {
|
|
1090
|
+
FString EnumVal;
|
|
1091
|
+
if ((*MobilityVal)->TryGetString(EnumVal)) {
|
|
1092
|
+
// Parse enum string
|
|
1093
|
+
int64 Val =
|
|
1094
|
+
StaticEnum<EComponentMobility::Type>()->GetValueByNameString(
|
|
1095
|
+
EnumVal);
|
|
1096
|
+
if (Val != INDEX_NONE) {
|
|
1097
|
+
SC->SetMobility((EComponentMobility::Type)Val);
|
|
1098
|
+
AppliedProperties.Add(MobilityKey);
|
|
1099
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
|
|
1100
|
+
TEXT("Explicitly set Mobility to %s"), *EnumVal);
|
|
1101
|
+
}
|
|
1102
|
+
} else {
|
|
1103
|
+
double Val;
|
|
1104
|
+
if ((*MobilityVal)->TryGetNumber(Val)) {
|
|
1105
|
+
SC->SetMobility((EComponentMobility::Type)(int32)Val);
|
|
1106
|
+
AppliedProperties.Add(MobilityKey);
|
|
1107
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
|
|
1108
|
+
TEXT("Explicitly set Mobility to %d"), (int32)Val);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
for (const auto &Pair : (*PropertiesPtr)->Values) {
|
|
1115
|
+
// Skip Mobility as we already handled it
|
|
1116
|
+
if (Pair.Key.Equals(TEXT("Mobility"), ESearchCase::IgnoreCase))
|
|
1117
|
+
continue;
|
|
1118
|
+
|
|
1119
|
+
// Special handling for SimulatePhysics
|
|
1120
|
+
if (Pair.Key.Equals(TEXT("SimulatePhysics"), ESearchCase::IgnoreCase) ||
|
|
1121
|
+
Pair.Key.Equals(TEXT("bSimulatePhysics"), ESearchCase::IgnoreCase)) {
|
|
1122
|
+
if (UPrimitiveComponent *Prim =
|
|
1123
|
+
Cast<UPrimitiveComponent>(TargetComponent)) {
|
|
1124
|
+
bool bVal = false;
|
|
1125
|
+
if (Pair.Value->TryGetBool(bVal)) {
|
|
1126
|
+
Prim->SetSimulatePhysics(bVal);
|
|
1127
|
+
AppliedProperties.Add(Pair.Key);
|
|
1128
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
|
|
1129
|
+
TEXT("Explicitly set SimulatePhysics to %s"),
|
|
1130
|
+
bVal ? TEXT("True") : TEXT("False"));
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
FProperty *Property = ComponentClass->FindPropertyByName(*Pair.Key);
|
|
1137
|
+
if (!Property) {
|
|
1138
|
+
PropertyWarnings.Add(
|
|
1139
|
+
FString::Printf(TEXT("Property not found: %s"), *Pair.Key));
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
FString ApplyError;
|
|
1143
|
+
if (ApplyJsonValueToProperty(TargetComponent, Property, Pair.Value,
|
|
1144
|
+
ApplyError))
|
|
1145
|
+
AppliedProperties.Add(Pair.Key);
|
|
1146
|
+
else
|
|
1147
|
+
PropertyWarnings.Add(FString::Printf(TEXT("Failed to set %s: %s"),
|
|
1148
|
+
*Pair.Key, *ApplyError));
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
if (USceneComponent *SceneComponent =
|
|
1152
|
+
Cast<USceneComponent>(TargetComponent)) {
|
|
1153
|
+
SceneComponent->MarkRenderStateDirty();
|
|
1154
|
+
SceneComponent->UpdateComponentToWorld();
|
|
1155
|
+
}
|
|
1156
|
+
TargetComponent->MarkPackageDirty();
|
|
1157
|
+
|
|
1158
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
1159
|
+
if (AppliedProperties.Num() > 0) {
|
|
1160
|
+
TArray<TSharedPtr<FJsonValue>> PropsArray;
|
|
1161
|
+
for (const FString &PropName : AppliedProperties)
|
|
1162
|
+
PropsArray.Add(MakeShared<FJsonValueString>(PropName));
|
|
1163
|
+
Data->SetArrayField(TEXT("applied"), PropsArray);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
|
|
1167
|
+
TEXT("ControlActor: Updated properties for component '%s' on '%s'"),
|
|
1168
|
+
*TargetComponent->GetName(), *Found->GetActorLabel());
|
|
1169
|
+
|
|
1170
|
+
SendStandardSuccessResponse(this, Socket, RequestId,
|
|
1171
|
+
TEXT("Component properties updated"), Data,
|
|
1172
|
+
PropertyWarnings);
|
|
1173
|
+
return true;
|
|
1174
|
+
#else
|
|
1175
|
+
return false;
|
|
1176
|
+
#endif
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlActorGetComponents(
|
|
1180
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
1181
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
1182
|
+
#if WITH_EDITOR
|
|
1183
|
+
FString TargetName;
|
|
1184
|
+
Payload->TryGetStringField(TEXT("actorName"), TargetName);
|
|
1185
|
+
|
|
1186
|
+
// Also accept "objectPath" as an alias, common in inspections
|
|
1187
|
+
if (TargetName.IsEmpty()) {
|
|
1188
|
+
Payload->TryGetStringField(TEXT("objectPath"), TargetName);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
if (TargetName.IsEmpty()) {
|
|
1192
|
+
SendAutomationResponse(Socket, RequestId, false,
|
|
1193
|
+
TEXT("actorName or objectPath required"), nullptr,
|
|
1194
|
+
TEXT("INVALID_ARGUMENT"));
|
|
1195
|
+
return true;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
AActor *Found = FindActorByName(TargetName);
|
|
1199
|
+
// Fallback: Check if it's a Blueprint asset to inspect CDO components
|
|
1200
|
+
if (!Found) {
|
|
1201
|
+
if (UObject *Asset = UEditorAssetLibrary::LoadAsset(TargetName)) {
|
|
1202
|
+
if (UBlueprint *BP = Cast<UBlueprint>(Asset)) {
|
|
1203
|
+
if (BP->GeneratedClass) {
|
|
1204
|
+
Found = Cast<AActor>(BP->GeneratedClass->GetDefaultObject());
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
if (!Found) {
|
|
1211
|
+
SendAutomationResponse(Socket, RequestId, false,
|
|
1212
|
+
TEXT("Actor or Blueprint not found"), nullptr,
|
|
1213
|
+
TEXT("ACTOR_NOT_FOUND"));
|
|
1214
|
+
return true;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
TArray<TSharedPtr<FJsonValue>> ComponentsArray;
|
|
1218
|
+
for (UActorComponent *Comp : Found->GetComponents()) {
|
|
1219
|
+
if (!Comp)
|
|
1220
|
+
continue;
|
|
1221
|
+
TSharedPtr<FJsonObject> Entry = MakeShared<FJsonObject>();
|
|
1222
|
+
Entry->SetStringField(TEXT("name"), Comp->GetName());
|
|
1223
|
+
Entry->SetStringField(TEXT("class"), Comp->GetClass()
|
|
1224
|
+
? Comp->GetClass()->GetPathName()
|
|
1225
|
+
: TEXT(""));
|
|
1226
|
+
Entry->SetStringField(TEXT("path"), Comp->GetPathName());
|
|
1227
|
+
if (USceneComponent *SceneComp = Cast<USceneComponent>(Comp)) {
|
|
1228
|
+
FVector Loc = SceneComp->GetRelativeLocation();
|
|
1229
|
+
FRotator Rot = SceneComp->GetRelativeRotation();
|
|
1230
|
+
FVector Scale = SceneComp->GetRelativeScale3D();
|
|
1231
|
+
|
|
1232
|
+
TSharedPtr<FJsonObject> LocObj = MakeShared<FJsonObject>();
|
|
1233
|
+
LocObj->SetNumberField(TEXT("x"), Loc.X);
|
|
1234
|
+
LocObj->SetNumberField(TEXT("y"), Loc.Y);
|
|
1235
|
+
LocObj->SetNumberField(TEXT("z"), Loc.Z);
|
|
1236
|
+
Entry->SetObjectField(TEXT("relativeLocation"), LocObj);
|
|
1237
|
+
|
|
1238
|
+
TSharedPtr<FJsonObject> RotObj = MakeShared<FJsonObject>();
|
|
1239
|
+
RotObj->SetNumberField(TEXT("pitch"), Rot.Pitch);
|
|
1240
|
+
RotObj->SetNumberField(TEXT("yaw"), Rot.Yaw);
|
|
1241
|
+
RotObj->SetNumberField(TEXT("roll"), Rot.Roll);
|
|
1242
|
+
Entry->SetObjectField(TEXT("relativeRotation"), RotObj);
|
|
1243
|
+
|
|
1244
|
+
TSharedPtr<FJsonObject> ScaleObj = MakeShared<FJsonObject>();
|
|
1245
|
+
ScaleObj->SetNumberField(TEXT("x"), Scale.X);
|
|
1246
|
+
ScaleObj->SetNumberField(TEXT("y"), Scale.Y);
|
|
1247
|
+
ScaleObj->SetNumberField(TEXT("z"), Scale.Z);
|
|
1248
|
+
Entry->SetObjectField(TEXT("relativeScale"), ScaleObj);
|
|
1249
|
+
}
|
|
1250
|
+
ComponentsArray.Add(MakeShared<FJsonValueObject>(Entry));
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
1254
|
+
Data->SetArrayField(TEXT("components"), ComponentsArray);
|
|
1255
|
+
Data->SetNumberField(TEXT("count"), ComponentsArray.Num());
|
|
1256
|
+
SendAutomationResponse(Socket, RequestId, true,
|
|
1257
|
+
TEXT("Actor components retrieved"), Data);
|
|
1258
|
+
return true;
|
|
1259
|
+
#else
|
|
1260
|
+
return false;
|
|
1261
|
+
#endif
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlActorDuplicate(
|
|
1265
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
1266
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
1267
|
+
#if WITH_EDITOR
|
|
1268
|
+
FString TargetName;
|
|
1269
|
+
Payload->TryGetStringField(TEXT("actorName"), TargetName);
|
|
1270
|
+
if (TargetName.IsEmpty()) {
|
|
1271
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("actorName required"),
|
|
1272
|
+
nullptr, TEXT("INVALID_ARGUMENT"));
|
|
1273
|
+
return true;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
AActor *Found = FindActorByName(TargetName);
|
|
1277
|
+
if (!Found) {
|
|
1278
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
|
|
1279
|
+
nullptr, TEXT("ACTOR_NOT_FOUND"));
|
|
1280
|
+
return true;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
FVector Offset =
|
|
1284
|
+
ExtractVectorField(Payload, TEXT("offset"), FVector::ZeroVector);
|
|
1285
|
+
UEditorActorSubsystem *ActorSS =
|
|
1286
|
+
GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
|
|
1287
|
+
AActor *Duplicated =
|
|
1288
|
+
ActorSS->DuplicateActor(Found, Found->GetWorld(), Offset);
|
|
1289
|
+
if (!Duplicated) {
|
|
1290
|
+
SendAutomationResponse(Socket, RequestId, false,
|
|
1291
|
+
TEXT("Failed to duplicate actor"), nullptr,
|
|
1292
|
+
TEXT("DUPLICATE_FAILED"));
|
|
1293
|
+
return true;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
FString NewName;
|
|
1297
|
+
Payload->TryGetStringField(TEXT("newName"), NewName);
|
|
1298
|
+
if (!NewName.TrimStartAndEnd().IsEmpty())
|
|
1299
|
+
Duplicated->SetActorLabel(NewName);
|
|
1300
|
+
|
|
1301
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
1302
|
+
Data->SetStringField(TEXT("source"), Found->GetActorLabel());
|
|
1303
|
+
Data->SetStringField(TEXT("actorName"), Duplicated->GetActorLabel());
|
|
1304
|
+
Data->SetStringField(TEXT("actorPath"), Duplicated->GetPathName());
|
|
1305
|
+
|
|
1306
|
+
TArray<TSharedPtr<FJsonValue>> OffsetArray;
|
|
1307
|
+
OffsetArray.Add(MakeShared<FJsonValueNumber>(Offset.X));
|
|
1308
|
+
OffsetArray.Add(MakeShared<FJsonValueNumber>(Offset.Y));
|
|
1309
|
+
OffsetArray.Add(MakeShared<FJsonValueNumber>(Offset.Z));
|
|
1310
|
+
Data->SetArrayField(TEXT("offset"), OffsetArray);
|
|
1311
|
+
|
|
1312
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
|
|
1313
|
+
TEXT("ControlActor: Duplicated '%s' to '%s'"), *Found->GetActorLabel(),
|
|
1314
|
+
*Duplicated->GetActorLabel());
|
|
1315
|
+
SendStandardSuccessResponse(this, Socket, RequestId, TEXT("Actor duplicated"),
|
|
1316
|
+
Data);
|
|
1317
|
+
return true;
|
|
1318
|
+
#else
|
|
1319
|
+
return false;
|
|
1320
|
+
#endif
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlActorAttach(
|
|
1324
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
1325
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
1326
|
+
#if WITH_EDITOR
|
|
1327
|
+
FString ChildName;
|
|
1328
|
+
Payload->TryGetStringField(TEXT("childActor"), ChildName);
|
|
1329
|
+
FString ParentName;
|
|
1330
|
+
Payload->TryGetStringField(TEXT("parentActor"), ParentName);
|
|
1331
|
+
if (ChildName.IsEmpty() || ParentName.IsEmpty()) {
|
|
1332
|
+
SendAutomationResponse(Socket, RequestId, false,
|
|
1333
|
+
TEXT("childActor and parentActor required"), nullptr,
|
|
1334
|
+
TEXT("INVALID_ARGUMENT"));
|
|
1335
|
+
return true;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
AActor *Child = FindActorByName(ChildName);
|
|
1339
|
+
AActor *Parent = FindActorByName(ParentName);
|
|
1340
|
+
if (!Child || !Parent) {
|
|
1341
|
+
SendAutomationResponse(Socket, RequestId, false,
|
|
1342
|
+
TEXT("Child or parent actor not found"), nullptr,
|
|
1343
|
+
TEXT("ACTOR_NOT_FOUND"));
|
|
1344
|
+
return true;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
if (Child == Parent) {
|
|
1348
|
+
SendAutomationResponse(Socket, RequestId, false,
|
|
1349
|
+
TEXT("Cannot attach actor to itself"), nullptr,
|
|
1350
|
+
TEXT("CYCLE_DETECTED"));
|
|
1351
|
+
return true;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
USceneComponent *ChildRoot = Child->GetRootComponent();
|
|
1355
|
+
USceneComponent *ParentRoot = Parent->GetRootComponent();
|
|
1356
|
+
if (!ChildRoot || !ParentRoot) {
|
|
1357
|
+
SendAutomationResponse(Socket, RequestId, false,
|
|
1358
|
+
TEXT("Actor missing root component"), nullptr,
|
|
1359
|
+
TEXT("ROOT_MISSING"));
|
|
1360
|
+
return true;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
Child->Modify();
|
|
1364
|
+
ChildRoot->Modify();
|
|
1365
|
+
ChildRoot->AttachToComponent(ParentRoot,
|
|
1366
|
+
FAttachmentTransformRules::KeepWorldTransform);
|
|
1367
|
+
Child->SetOwner(Parent);
|
|
1368
|
+
Child->MarkPackageDirty();
|
|
1369
|
+
Parent->MarkPackageDirty();
|
|
1370
|
+
|
|
1371
|
+
// Verify attachment
|
|
1372
|
+
bool bAttached = false;
|
|
1373
|
+
if (Child->GetRootComponent() &&
|
|
1374
|
+
Child->GetRootComponent()->GetAttachParent() == ParentRoot) {
|
|
1375
|
+
bAttached = true;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
1379
|
+
Data->SetStringField(TEXT("child"), Child->GetActorLabel());
|
|
1380
|
+
Data->SetStringField(TEXT("parent"), Parent->GetActorLabel());
|
|
1381
|
+
Data->SetBoolField(TEXT("attached"), bAttached);
|
|
1382
|
+
|
|
1383
|
+
if (!bAttached) {
|
|
1384
|
+
SendStandardErrorResponse(this, Socket, RequestId, TEXT("ATTACH_FAILED"),
|
|
1385
|
+
TEXT("Failed to attach actor"), Data);
|
|
1386
|
+
return true;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
|
|
1390
|
+
TEXT("ControlActor: Attached '%s' to '%s'"), *Child->GetActorLabel(),
|
|
1391
|
+
*Parent->GetActorLabel());
|
|
1392
|
+
SendStandardSuccessResponse(this, Socket, RequestId, TEXT("Actor attached"),
|
|
1393
|
+
Data);
|
|
1394
|
+
return true;
|
|
1395
|
+
#else
|
|
1396
|
+
return false;
|
|
1397
|
+
#endif
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlActorDetach(
|
|
1401
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
1402
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
1403
|
+
#if WITH_EDITOR
|
|
1404
|
+
FString TargetName;
|
|
1405
|
+
Payload->TryGetStringField(TEXT("actorName"), TargetName);
|
|
1406
|
+
if (TargetName.IsEmpty()) {
|
|
1407
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("actorName required"),
|
|
1408
|
+
nullptr, TEXT("INVALID_ARGUMENT"));
|
|
1409
|
+
return true;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
AActor *Found = FindActorByName(TargetName);
|
|
1413
|
+
if (!Found) {
|
|
1414
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
|
|
1415
|
+
nullptr, TEXT("ACTOR_NOT_FOUND"));
|
|
1416
|
+
return true;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
USceneComponent *RootComp = Found->GetRootComponent();
|
|
1420
|
+
if (!RootComp || !RootComp->GetAttachParent()) {
|
|
1421
|
+
TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
|
|
1422
|
+
Resp->SetBoolField(TEXT("success"), true);
|
|
1423
|
+
Resp->SetStringField(TEXT("actorName"), Found->GetActorLabel());
|
|
1424
|
+
Resp->SetStringField(TEXT("note"), TEXT("Actor was not attached"));
|
|
1425
|
+
SendAutomationResponse(Socket, RequestId, true,
|
|
1426
|
+
TEXT("Actor already detached"), Resp, FString());
|
|
1427
|
+
return true;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
Found->Modify();
|
|
1431
|
+
RootComp->Modify();
|
|
1432
|
+
RootComp->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform);
|
|
1433
|
+
Found->SetOwner(nullptr);
|
|
1434
|
+
Found->MarkPackageDirty();
|
|
1435
|
+
|
|
1436
|
+
// Verify detachment
|
|
1437
|
+
const bool bDetached = (RootComp->GetAttachParent() == nullptr);
|
|
1438
|
+
|
|
1439
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
1440
|
+
Data->SetStringField(TEXT("actorName"), Found->GetActorLabel());
|
|
1441
|
+
Data->SetBoolField(TEXT("detached"), bDetached);
|
|
1442
|
+
|
|
1443
|
+
if (!bDetached) {
|
|
1444
|
+
SendStandardErrorResponse(this, Socket, RequestId, TEXT("DETACH_FAILED"),
|
|
1445
|
+
TEXT("Failed to detach actor"), Data);
|
|
1446
|
+
return true;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
|
|
1450
|
+
TEXT("ControlActor: Detached '%s'"), *Found->GetActorLabel());
|
|
1451
|
+
SendStandardSuccessResponse(this, Socket, RequestId, TEXT("Actor detached"),
|
|
1452
|
+
Data);
|
|
1453
|
+
return true;
|
|
1454
|
+
#else
|
|
1455
|
+
return false;
|
|
1456
|
+
#endif
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlActorFindByTag(
|
|
1460
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
1461
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
1462
|
+
#if WITH_EDITOR
|
|
1463
|
+
FString TagValue;
|
|
1464
|
+
Payload->TryGetStringField(TEXT("tag"), TagValue);
|
|
1465
|
+
if (TagValue.IsEmpty()) {
|
|
1466
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("tag required"),
|
|
1467
|
+
nullptr, TEXT("INVALID_ARGUMENT"));
|
|
1468
|
+
return true;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
FString MatchType;
|
|
1472
|
+
Payload->TryGetStringField(TEXT("matchType"), MatchType);
|
|
1473
|
+
MatchType = MatchType.ToLower();
|
|
1474
|
+
FName TagName(*TagValue);
|
|
1475
|
+
TArray<TSharedPtr<FJsonValue>> Matches;
|
|
1476
|
+
|
|
1477
|
+
UEditorActorSubsystem *ActorSS =
|
|
1478
|
+
GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
|
|
1479
|
+
TArray<AActor *> AllActors = ActorSS->GetAllLevelActors();
|
|
1480
|
+
for (AActor *Actor : AllActors) {
|
|
1481
|
+
if (!Actor)
|
|
1482
|
+
continue;
|
|
1483
|
+
bool bMatches = false;
|
|
1484
|
+
if (MatchType == TEXT("contains")) {
|
|
1485
|
+
for (const FName &Existing : Actor->Tags) {
|
|
1486
|
+
if (Existing.ToString().Contains(TagValue, ESearchCase::IgnoreCase)) {
|
|
1487
|
+
bMatches = true;
|
|
1488
|
+
break;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
} else {
|
|
1492
|
+
bMatches = Actor->ActorHasTag(TagName);
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
if (bMatches) {
|
|
1496
|
+
TSharedPtr<FJsonObject> Entry = MakeShared<FJsonObject>();
|
|
1497
|
+
Entry->SetStringField(TEXT("name"), Actor->GetActorLabel());
|
|
1498
|
+
Entry->SetStringField(TEXT("path"), Actor->GetPathName());
|
|
1499
|
+
Entry->SetStringField(TEXT("class"),
|
|
1500
|
+
Actor->GetClass() ? Actor->GetClass()->GetPathName()
|
|
1501
|
+
: TEXT(""));
|
|
1502
|
+
Matches.Add(MakeShared<FJsonValueObject>(Entry));
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
1507
|
+
Data->SetArrayField(TEXT("actors"), Matches);
|
|
1508
|
+
Data->SetNumberField(TEXT("count"), Matches.Num());
|
|
1509
|
+
SendStandardSuccessResponse(this, Socket, RequestId, TEXT("Actors found"),
|
|
1510
|
+
Data);
|
|
1511
|
+
return true;
|
|
1512
|
+
#else
|
|
1513
|
+
return false;
|
|
1514
|
+
#endif
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlActorAddTag(
|
|
1518
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
1519
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
1520
|
+
#if WITH_EDITOR
|
|
1521
|
+
FString TargetName;
|
|
1522
|
+
Payload->TryGetStringField(TEXT("actorName"), TargetName);
|
|
1523
|
+
FString TagValue;
|
|
1524
|
+
Payload->TryGetStringField(TEXT("tag"), TagValue);
|
|
1525
|
+
if (TargetName.IsEmpty() || TagValue.IsEmpty()) {
|
|
1526
|
+
SendAutomationResponse(Socket, RequestId, false,
|
|
1527
|
+
TEXT("actorName and tag required"), nullptr,
|
|
1528
|
+
TEXT("INVALID_ARGUMENT"));
|
|
1529
|
+
return true;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
AActor *Found = FindActorByName(TargetName);
|
|
1533
|
+
if (!Found) {
|
|
1534
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
|
|
1535
|
+
nullptr, TEXT("ACTOR_NOT_FOUND"));
|
|
1536
|
+
return true;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
const FName TagName(*TagValue);
|
|
1540
|
+
const bool bAlreadyHad = Found->Tags.Contains(TagName);
|
|
1541
|
+
|
|
1542
|
+
Found->Modify();
|
|
1543
|
+
Found->Tags.AddUnique(TagName);
|
|
1544
|
+
Found->MarkPackageDirty();
|
|
1545
|
+
|
|
1546
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
1547
|
+
Data->SetBoolField(TEXT("wasPresent"), bAlreadyHad);
|
|
1548
|
+
Data->SetStringField(TEXT("actorName"), Found->GetActorLabel());
|
|
1549
|
+
Data->SetStringField(TEXT("tag"), TagName.ToString());
|
|
1550
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
|
|
1551
|
+
TEXT("ControlActor: Added tag '%s' to '%s'"), *TagName.ToString(),
|
|
1552
|
+
*Found->GetActorLabel());
|
|
1553
|
+
SendStandardSuccessResponse(this, Socket, RequestId,
|
|
1554
|
+
TEXT("Tag applied to actor"), Data);
|
|
1555
|
+
return true;
|
|
1556
|
+
#else
|
|
1557
|
+
return false;
|
|
1558
|
+
#endif
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlActorFindByName(
|
|
1562
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
1563
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
1564
|
+
#if WITH_EDITOR
|
|
1565
|
+
FString Query;
|
|
1566
|
+
Payload->TryGetStringField(TEXT("name"), Query);
|
|
1567
|
+
if (Query.IsEmpty()) {
|
|
1568
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("name required"),
|
|
1569
|
+
nullptr, TEXT("INVALID_ARGUMENT"));
|
|
1570
|
+
return true;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
UEditorActorSubsystem *ActorSS =
|
|
1574
|
+
GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
|
|
1575
|
+
const TArray<AActor *> AllActors = ActorSS->GetAllLevelActors();
|
|
1576
|
+
TArray<TSharedPtr<FJsonValue>> Matches;
|
|
1577
|
+
for (AActor *Actor : AllActors) {
|
|
1578
|
+
if (!Actor)
|
|
1579
|
+
continue;
|
|
1580
|
+
const FString Label = Actor->GetActorLabel();
|
|
1581
|
+
const FString Name = Actor->GetName();
|
|
1582
|
+
const FString Path = Actor->GetPathName();
|
|
1583
|
+
const bool bMatches = Label.Contains(Query, ESearchCase::IgnoreCase) ||
|
|
1584
|
+
Name.Contains(Query, ESearchCase::IgnoreCase) ||
|
|
1585
|
+
Path.Contains(Query, ESearchCase::IgnoreCase);
|
|
1586
|
+
if (bMatches) {
|
|
1587
|
+
TSharedPtr<FJsonObject> Entry = MakeShared<FJsonObject>();
|
|
1588
|
+
Entry->SetStringField(TEXT("label"), Label);
|
|
1589
|
+
Entry->SetStringField(TEXT("name"), Name);
|
|
1590
|
+
Entry->SetStringField(TEXT("path"), Path);
|
|
1591
|
+
Entry->SetStringField(TEXT("class"),
|
|
1592
|
+
Actor->GetClass() ? Actor->GetClass()->GetPathName()
|
|
1593
|
+
: TEXT(""));
|
|
1594
|
+
Matches.Add(MakeShared<FJsonValueObject>(Entry));
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
1599
|
+
Data->SetNumberField(TEXT("count"), Matches.Num());
|
|
1600
|
+
Data->SetArrayField(TEXT("actors"), Matches);
|
|
1601
|
+
Data->SetStringField(TEXT("query"), Query);
|
|
1602
|
+
SendStandardSuccessResponse(this, Socket, RequestId,
|
|
1603
|
+
TEXT("Actor query executed"), Data);
|
|
1604
|
+
return true;
|
|
1605
|
+
#else
|
|
1606
|
+
return false;
|
|
1607
|
+
#endif
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlActorDeleteByTag(
|
|
1611
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
1612
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
1613
|
+
#if WITH_EDITOR
|
|
1614
|
+
FString TagValue;
|
|
1615
|
+
Payload->TryGetStringField(TEXT("tag"), TagValue);
|
|
1616
|
+
if (TagValue.IsEmpty()) {
|
|
1617
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("tag required"),
|
|
1618
|
+
nullptr, TEXT("INVALID_ARGUMENT"));
|
|
1619
|
+
return true;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
const FName TagName(*TagValue);
|
|
1623
|
+
UEditorActorSubsystem *ActorSS =
|
|
1624
|
+
GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
|
|
1625
|
+
const TArray<AActor *> AllActors = ActorSS->GetAllLevelActors();
|
|
1626
|
+
TArray<FString> Deleted;
|
|
1627
|
+
|
|
1628
|
+
for (AActor *Actor : AllActors) {
|
|
1629
|
+
if (!Actor)
|
|
1630
|
+
continue;
|
|
1631
|
+
if (Actor->ActorHasTag(TagName)) {
|
|
1632
|
+
const FString Label = Actor->GetActorLabel();
|
|
1633
|
+
if (ActorSS->DestroyActor(Actor))
|
|
1634
|
+
Deleted.Add(Label);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
1639
|
+
Data->SetStringField(TEXT("tag"), TagName.ToString());
|
|
1640
|
+
Data->SetNumberField(TEXT("deletedCount"), Deleted.Num());
|
|
1641
|
+
TArray<TSharedPtr<FJsonValue>> DeletedArray;
|
|
1642
|
+
for (const FString &Name : Deleted)
|
|
1643
|
+
DeletedArray.Add(MakeShared<FJsonValueString>(Name));
|
|
1644
|
+
Data->SetArrayField(TEXT("deleted"), DeletedArray);
|
|
1645
|
+
SendStandardSuccessResponse(this, Socket, RequestId,
|
|
1646
|
+
TEXT("Actors deleted by tag"), Data);
|
|
1647
|
+
return true;
|
|
1648
|
+
#else
|
|
1649
|
+
return false;
|
|
1650
|
+
#endif
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlActorSetBlueprintVariables(
|
|
1654
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
1655
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
1656
|
+
#if WITH_EDITOR
|
|
1657
|
+
FString TargetName;
|
|
1658
|
+
Payload->TryGetStringField(TEXT("actorName"), TargetName);
|
|
1659
|
+
if (TargetName.IsEmpty()) {
|
|
1660
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("actorName required"),
|
|
1661
|
+
nullptr, TEXT("INVALID_ARGUMENT"));
|
|
1662
|
+
return true;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
const TSharedPtr<FJsonObject> *VariablesPtr = nullptr;
|
|
1666
|
+
if (!(Payload->TryGetObjectField(TEXT("variables"), VariablesPtr) &&
|
|
1667
|
+
VariablesPtr && VariablesPtr->IsValid())) {
|
|
1668
|
+
SendAutomationResponse(Socket, RequestId, false,
|
|
1669
|
+
TEXT("variables object required"), nullptr,
|
|
1670
|
+
TEXT("INVALID_ARGUMENT"));
|
|
1671
|
+
return true;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
AActor *Found = FindActorByName(TargetName);
|
|
1675
|
+
if (!Found) {
|
|
1676
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
|
|
1677
|
+
nullptr, TEXT("ACTOR_NOT_FOUND"));
|
|
1678
|
+
return true;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
UClass *ActorClass = Found->GetClass();
|
|
1682
|
+
Found->Modify();
|
|
1683
|
+
TArray<FString> Applied;
|
|
1684
|
+
TArray<FString> Warnings;
|
|
1685
|
+
|
|
1686
|
+
for (const auto &Pair : (*VariablesPtr)->Values) {
|
|
1687
|
+
FProperty *Property = ActorClass->FindPropertyByName(*Pair.Key);
|
|
1688
|
+
if (!Property) {
|
|
1689
|
+
Warnings.Add(FString::Printf(TEXT("Property not found: %s"), *Pair.Key));
|
|
1690
|
+
continue;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
FString ApplyError;
|
|
1694
|
+
if (ApplyJsonValueToProperty(Found, Property, Pair.Value, ApplyError))
|
|
1695
|
+
Applied.Add(Pair.Key);
|
|
1696
|
+
else
|
|
1697
|
+
Warnings.Add(FString::Printf(TEXT("Failed to set %s: %s"), *Pair.Key,
|
|
1698
|
+
*ApplyError));
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
Found->MarkComponentsRenderStateDirty();
|
|
1702
|
+
Found->MarkPackageDirty();
|
|
1703
|
+
|
|
1704
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
1705
|
+
if (Applied.Num() > 0) {
|
|
1706
|
+
TArray<TSharedPtr<FJsonValue>> AppliedArray;
|
|
1707
|
+
for (const FString &Name : Applied)
|
|
1708
|
+
AppliedArray.Add(MakeShared<FJsonValueString>(Name));
|
|
1709
|
+
Data->SetArrayField(TEXT("updated"), AppliedArray);
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
SendStandardSuccessResponse(this, Socket, RequestId,
|
|
1713
|
+
TEXT("Variables updated"), Data, Warnings);
|
|
1714
|
+
return true;
|
|
1715
|
+
#else
|
|
1716
|
+
return false;
|
|
1717
|
+
#endif
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlActorCreateSnapshot(
|
|
1721
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
1722
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
1723
|
+
#if WITH_EDITOR
|
|
1724
|
+
FString TargetName;
|
|
1725
|
+
Payload->TryGetStringField(TEXT("actorName"), TargetName);
|
|
1726
|
+
if (TargetName.IsEmpty()) {
|
|
1727
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("actorName required"),
|
|
1728
|
+
nullptr, TEXT("INVALID_ARGUMENT"));
|
|
1729
|
+
return true;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
FString SnapshotName;
|
|
1733
|
+
Payload->TryGetStringField(TEXT("snapshotName"), SnapshotName);
|
|
1734
|
+
if (SnapshotName.IsEmpty()) {
|
|
1735
|
+
SendAutomationResponse(Socket, RequestId, false,
|
|
1736
|
+
TEXT("snapshotName required"), nullptr,
|
|
1737
|
+
TEXT("INVALID_ARGUMENT"));
|
|
1738
|
+
return true;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
AActor *Found = FindActorByName(TargetName);
|
|
1742
|
+
if (!Found) {
|
|
1743
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
|
|
1744
|
+
nullptr, TEXT("ACTOR_NOT_FOUND"));
|
|
1745
|
+
return true;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
const FString SnapshotKey =
|
|
1749
|
+
FString::Printf(TEXT("%s::%s"), *Found->GetPathName(), *SnapshotName);
|
|
1750
|
+
CachedActorSnapshots.Add(SnapshotKey, Found->GetActorTransform());
|
|
1751
|
+
|
|
1752
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
1753
|
+
Data->SetStringField(TEXT("snapshotName"), SnapshotName);
|
|
1754
|
+
Data->SetStringField(TEXT("actorName"), Found->GetActorLabel());
|
|
1755
|
+
SendStandardSuccessResponse(this, Socket, RequestId, TEXT("Snapshot created"),
|
|
1756
|
+
Data);
|
|
1757
|
+
return true;
|
|
1758
|
+
#else
|
|
1759
|
+
return false;
|
|
1760
|
+
#endif
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlActorRestoreSnapshot(
|
|
1764
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
1765
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
1766
|
+
#if WITH_EDITOR
|
|
1767
|
+
FString TargetName;
|
|
1768
|
+
Payload->TryGetStringField(TEXT("actorName"), TargetName);
|
|
1769
|
+
if (TargetName.IsEmpty()) {
|
|
1770
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("actorName required"),
|
|
1771
|
+
nullptr, TEXT("INVALID_ARGUMENT"));
|
|
1772
|
+
return true;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
FString SnapshotName;
|
|
1776
|
+
Payload->TryGetStringField(TEXT("snapshotName"), SnapshotName);
|
|
1777
|
+
if (SnapshotName.IsEmpty()) {
|
|
1778
|
+
SendAutomationResponse(Socket, RequestId, false,
|
|
1779
|
+
TEXT("snapshotName required"), nullptr,
|
|
1780
|
+
TEXT("INVALID_ARGUMENT"));
|
|
1781
|
+
return true;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
AActor *Found = FindActorByName(TargetName);
|
|
1785
|
+
if (!Found) {
|
|
1786
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
|
|
1787
|
+
nullptr, TEXT("ACTOR_NOT_FOUND"));
|
|
1788
|
+
return true;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
const FString SnapshotKey =
|
|
1792
|
+
FString::Printf(TEXT("%s::%s"), *Found->GetPathName(), *SnapshotName);
|
|
1793
|
+
if (!CachedActorSnapshots.Contains(SnapshotKey)) {
|
|
1794
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("Snapshot not found"),
|
|
1795
|
+
nullptr, TEXT("SNAPSHOT_NOT_FOUND"));
|
|
1796
|
+
return true;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
const FTransform &SavedTransform = CachedActorSnapshots[SnapshotKey];
|
|
1800
|
+
Found->Modify();
|
|
1801
|
+
Found->SetActorTransform(SavedTransform);
|
|
1802
|
+
Found->MarkComponentsRenderStateDirty();
|
|
1803
|
+
Found->MarkPackageDirty();
|
|
1804
|
+
|
|
1805
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
1806
|
+
Data->SetStringField(TEXT("snapshotName"), SnapshotName);
|
|
1807
|
+
Data->SetStringField(TEXT("actorName"), Found->GetActorLabel());
|
|
1808
|
+
SendStandardSuccessResponse(this, Socket, RequestId,
|
|
1809
|
+
TEXT("Snapshot restored"), Data);
|
|
1810
|
+
return true;
|
|
1811
|
+
#else
|
|
1812
|
+
return false;
|
|
1813
|
+
#endif
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlActorExport(
|
|
1817
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
1818
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
1819
|
+
#if WITH_EDITOR
|
|
1820
|
+
FString TargetName;
|
|
1821
|
+
Payload->TryGetStringField(TEXT("actorName"), TargetName);
|
|
1822
|
+
if (TargetName.IsEmpty()) {
|
|
1823
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("actorName required"),
|
|
1824
|
+
nullptr, TEXT("INVALID_ARGUMENT"));
|
|
1825
|
+
return true;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
AActor *Found = FindActorByName(TargetName);
|
|
1829
|
+
if (!Found) {
|
|
1830
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
|
|
1831
|
+
nullptr, TEXT("ACTOR_NOT_FOUND"));
|
|
1832
|
+
return true;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
FMcpOutputCapture OutputCapture;
|
|
1836
|
+
UExporter::ExportToOutputDevice(nullptr, Found, nullptr, OutputCapture,
|
|
1837
|
+
TEXT("T3D"), 0, 0, false);
|
|
1838
|
+
FString OutputString = FString::Join(OutputCapture.Consume(), TEXT("\n"));
|
|
1839
|
+
|
|
1840
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
1841
|
+
Data->SetStringField(TEXT("t3d"), OutputString);
|
|
1842
|
+
Data->SetStringField(TEXT("actorName"), Found->GetActorLabel());
|
|
1843
|
+
SendStandardSuccessResponse(this, Socket, RequestId, TEXT("Actor exported"),
|
|
1844
|
+
Data);
|
|
1845
|
+
return true;
|
|
1846
|
+
#else
|
|
1847
|
+
return false;
|
|
1848
|
+
#endif
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlActorGetBoundingBox(
|
|
1852
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
1853
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
1854
|
+
#if WITH_EDITOR
|
|
1855
|
+
FString TargetName;
|
|
1856
|
+
Payload->TryGetStringField(TEXT("actorName"), TargetName);
|
|
1857
|
+
if (TargetName.IsEmpty()) {
|
|
1858
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("actorName required"),
|
|
1859
|
+
nullptr, TEXT("INVALID_ARGUMENT"));
|
|
1860
|
+
return true;
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
AActor *Found = FindActorByName(TargetName);
|
|
1864
|
+
if (!Found) {
|
|
1865
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
|
|
1866
|
+
nullptr, TEXT("ACTOR_NOT_FOUND"));
|
|
1867
|
+
return true;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
FVector Origin, BoxExtent;
|
|
1871
|
+
Found->GetActorBounds(false, Origin, BoxExtent);
|
|
1872
|
+
|
|
1873
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
1874
|
+
|
|
1875
|
+
auto MakeArray = [](const FVector &Vec) {
|
|
1876
|
+
TArray<TSharedPtr<FJsonValue>> Arr;
|
|
1877
|
+
Arr.Add(MakeShared<FJsonValueNumber>(Vec.X));
|
|
1878
|
+
Arr.Add(MakeShared<FJsonValueNumber>(Vec.Y));
|
|
1879
|
+
Arr.Add(MakeShared<FJsonValueNumber>(Vec.Z));
|
|
1880
|
+
return Arr;
|
|
1881
|
+
};
|
|
1882
|
+
|
|
1883
|
+
Data->SetArrayField(TEXT("origin"), MakeArray(Origin));
|
|
1884
|
+
Data->SetArrayField(TEXT("extent"), MakeArray(BoxExtent));
|
|
1885
|
+
SendStandardSuccessResponse(this, Socket, RequestId,
|
|
1886
|
+
TEXT("Bounding box retrieved"), Data);
|
|
1887
|
+
return true;
|
|
1888
|
+
#else
|
|
1889
|
+
return false;
|
|
1890
|
+
#endif
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlActorGetMetadata(
|
|
1894
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
1895
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
1896
|
+
#if WITH_EDITOR
|
|
1897
|
+
FString TargetName;
|
|
1898
|
+
Payload->TryGetStringField(TEXT("actorName"), TargetName);
|
|
1899
|
+
if (TargetName.IsEmpty()) {
|
|
1900
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("actorName required"),
|
|
1901
|
+
nullptr, TEXT("INVALID_ARGUMENT"));
|
|
1902
|
+
return true;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
AActor *Found = FindActorByName(TargetName);
|
|
1906
|
+
if (!Found) {
|
|
1907
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
|
|
1908
|
+
nullptr, TEXT("ACTOR_NOT_FOUND"));
|
|
1909
|
+
return true;
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
1913
|
+
Data->SetStringField(TEXT("name"), Found->GetName());
|
|
1914
|
+
Data->SetStringField(TEXT("label"), Found->GetActorLabel());
|
|
1915
|
+
Data->SetStringField(TEXT("path"), Found->GetPathName());
|
|
1916
|
+
Data->SetStringField(TEXT("class"), Found->GetClass()
|
|
1917
|
+
? Found->GetClass()->GetPathName()
|
|
1918
|
+
: TEXT(""));
|
|
1919
|
+
|
|
1920
|
+
TArray<TSharedPtr<FJsonValue>> TagsArray;
|
|
1921
|
+
for (const FName &Tag : Found->Tags) {
|
|
1922
|
+
TagsArray.Add(MakeShared<FJsonValueString>(Tag.ToString()));
|
|
1923
|
+
}
|
|
1924
|
+
Data->SetArrayField(TEXT("tags"), TagsArray);
|
|
1925
|
+
|
|
1926
|
+
const FTransform Current = Found->GetActorTransform();
|
|
1927
|
+
auto MakeArray = [](const FVector &Vec) {
|
|
1928
|
+
TArray<TSharedPtr<FJsonValue>> Arr;
|
|
1929
|
+
Arr.Add(MakeShared<FJsonValueNumber>(Vec.X));
|
|
1930
|
+
Arr.Add(MakeShared<FJsonValueNumber>(Vec.Y));
|
|
1931
|
+
Arr.Add(MakeShared<FJsonValueNumber>(Vec.Z));
|
|
1932
|
+
return Arr;
|
|
1933
|
+
};
|
|
1934
|
+
Data->SetArrayField(TEXT("location"), MakeArray(Current.GetLocation()));
|
|
1935
|
+
|
|
1936
|
+
SendStandardSuccessResponse(this, Socket, RequestId,
|
|
1937
|
+
TEXT("Metadata retrieved"), Data);
|
|
1938
|
+
return true;
|
|
1939
|
+
#else
|
|
1940
|
+
return false;
|
|
1941
|
+
#endif
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlActorRemoveTag(
|
|
1945
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
1946
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
1947
|
+
#if WITH_EDITOR
|
|
1948
|
+
FString TargetName;
|
|
1949
|
+
Payload->TryGetStringField(TEXT("actorName"), TargetName);
|
|
1950
|
+
FString TagValue;
|
|
1951
|
+
Payload->TryGetStringField(TEXT("tag"), TagValue);
|
|
1952
|
+
if (TargetName.IsEmpty() || TagValue.IsEmpty()) {
|
|
1953
|
+
SendAutomationResponse(Socket, RequestId, false,
|
|
1954
|
+
TEXT("actorName and tag required"), nullptr,
|
|
1955
|
+
TEXT("INVALID_ARGUMENT"));
|
|
1956
|
+
return true;
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
AActor *Found = FindActorByName(TargetName);
|
|
1960
|
+
if (!Found) {
|
|
1961
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
|
|
1962
|
+
nullptr, TEXT("ACTOR_NOT_FOUND"));
|
|
1963
|
+
return true;
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
const FName TagName(*TagValue);
|
|
1967
|
+
if (!Found->Tags.Contains(TagName)) {
|
|
1968
|
+
// Idempotent success
|
|
1969
|
+
TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
|
|
1970
|
+
Resp->SetBoolField(TEXT("success"), true);
|
|
1971
|
+
Resp->SetBoolField(TEXT("wasPresent"), false);
|
|
1972
|
+
Resp->SetStringField(TEXT("actorName"), Found->GetActorLabel());
|
|
1973
|
+
Resp->SetStringField(TEXT("tag"), TagValue);
|
|
1974
|
+
SendAutomationResponse(Socket, RequestId, true,
|
|
1975
|
+
TEXT("Tag not present (idempotent)"), Resp,
|
|
1976
|
+
FString());
|
|
1977
|
+
return true;
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
Found->Modify();
|
|
1981
|
+
Found->Tags.Remove(TagName);
|
|
1982
|
+
Found->MarkPackageDirty();
|
|
1983
|
+
|
|
1984
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
1985
|
+
Data->SetBoolField(TEXT("wasPresent"), true);
|
|
1986
|
+
Data->SetStringField(TEXT("actorName"), Found->GetActorLabel());
|
|
1987
|
+
Data->SetStringField(TEXT("tag"), TagValue);
|
|
1988
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
|
|
1989
|
+
TEXT("ControlActor: Removed tag '%s' from '%s'"), *TagValue,
|
|
1990
|
+
*Found->GetActorLabel());
|
|
1991
|
+
SendStandardSuccessResponse(this, Socket, RequestId,
|
|
1992
|
+
TEXT("Tag removed from actor"), Data);
|
|
1993
|
+
return true;
|
|
1994
|
+
#else
|
|
1995
|
+
return false;
|
|
1996
|
+
#endif
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlActorAction(
|
|
2000
|
+
const FString &RequestId, const FString &Action,
|
|
2001
|
+
const TSharedPtr<FJsonObject> &Payload,
|
|
2002
|
+
TSharedPtr<FMcpBridgeWebSocket> RequestingSocket) {
|
|
2003
|
+
const FString Lower = Action.ToLower();
|
|
2004
|
+
if (!Lower.Equals(TEXT("control_actor"), ESearchCase::IgnoreCase) &&
|
|
2005
|
+
!Lower.StartsWith(TEXT("control_actor")))
|
|
2006
|
+
return false;
|
|
2007
|
+
if (!Payload.IsValid()) {
|
|
2008
|
+
SendAutomationError(RequestingSocket, RequestId,
|
|
2009
|
+
TEXT("control_actor payload missing."),
|
|
2010
|
+
TEXT("INVALID_PAYLOAD"));
|
|
2011
|
+
return true;
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
FString SubAction;
|
|
2015
|
+
Payload->TryGetStringField(TEXT("action"), SubAction);
|
|
2016
|
+
const FString LowerSub = SubAction.ToLower();
|
|
2017
|
+
|
|
2018
|
+
UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
|
|
2019
|
+
TEXT("HandleControlActorAction: %s RequestId=%s"), *LowerSub,
|
|
2020
|
+
*RequestId);
|
|
2021
|
+
|
|
2022
|
+
#if WITH_EDITOR
|
|
2023
|
+
if (!GEditor) {
|
|
2024
|
+
SendAutomationResponse(RequestingSocket, RequestId, false,
|
|
2025
|
+
TEXT("Editor not available"), nullptr,
|
|
2026
|
+
TEXT("EDITOR_NOT_AVAILABLE"));
|
|
2027
|
+
return true;
|
|
2028
|
+
}
|
|
2029
|
+
if (!GEditor->GetEditorSubsystem<UEditorActorSubsystem>()) {
|
|
2030
|
+
SendAutomationResponse(RequestingSocket, RequestId, false,
|
|
2031
|
+
TEXT("EditorActorSubsystem not available"), nullptr,
|
|
2032
|
+
TEXT("EDITOR_ACTOR_SUBSYSTEM_MISSING"));
|
|
2033
|
+
return true;
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
if (LowerSub == TEXT("spawn"))
|
|
2037
|
+
return HandleControlActorSpawn(RequestId, Payload, RequestingSocket);
|
|
2038
|
+
if (LowerSub == TEXT("spawn_blueprint"))
|
|
2039
|
+
return HandleControlActorSpawnBlueprint(RequestId, Payload,
|
|
2040
|
+
RequestingSocket);
|
|
2041
|
+
if (LowerSub == TEXT("delete") || LowerSub == TEXT("remove"))
|
|
2042
|
+
return HandleControlActorDelete(RequestId, Payload, RequestingSocket);
|
|
2043
|
+
if (LowerSub == TEXT("apply_force") ||
|
|
2044
|
+
LowerSub == TEXT("apply_force_to_actor"))
|
|
2045
|
+
return HandleControlActorApplyForce(RequestId, Payload, RequestingSocket);
|
|
2046
|
+
if (LowerSub == TEXT("set_transform") ||
|
|
2047
|
+
LowerSub == TEXT("set_actor_transform"))
|
|
2048
|
+
return HandleControlActorSetTransform(RequestId, Payload, RequestingSocket);
|
|
2049
|
+
if (LowerSub == TEXT("get_transform") ||
|
|
2050
|
+
LowerSub == TEXT("get_actor_transform"))
|
|
2051
|
+
return HandleControlActorGetTransform(RequestId, Payload, RequestingSocket);
|
|
2052
|
+
if (LowerSub == TEXT("set_visibility") ||
|
|
2053
|
+
LowerSub == TEXT("set_actor_visibility"))
|
|
2054
|
+
return HandleControlActorSetVisibility(RequestId, Payload,
|
|
2055
|
+
RequestingSocket);
|
|
2056
|
+
if (LowerSub == TEXT("add_component"))
|
|
2057
|
+
return HandleControlActorAddComponent(RequestId, Payload, RequestingSocket);
|
|
2058
|
+
if (LowerSub == TEXT("set_component_properties"))
|
|
2059
|
+
return HandleControlActorSetComponentProperties(RequestId, Payload,
|
|
2060
|
+
RequestingSocket);
|
|
2061
|
+
if (LowerSub == TEXT("get_components"))
|
|
2062
|
+
return HandleControlActorGetComponents(RequestId, Payload,
|
|
2063
|
+
RequestingSocket);
|
|
2064
|
+
if (LowerSub == TEXT("duplicate"))
|
|
2065
|
+
return HandleControlActorDuplicate(RequestId, Payload, RequestingSocket);
|
|
2066
|
+
if (LowerSub == TEXT("attach"))
|
|
2067
|
+
return HandleControlActorAttach(RequestId, Payload, RequestingSocket);
|
|
2068
|
+
if (LowerSub == TEXT("detach"))
|
|
2069
|
+
return HandleControlActorDetach(RequestId, Payload, RequestingSocket);
|
|
2070
|
+
if (LowerSub == TEXT("find_by_tag"))
|
|
2071
|
+
return HandleControlActorFindByTag(RequestId, Payload, RequestingSocket);
|
|
2072
|
+
if (LowerSub == TEXT("add_tag"))
|
|
2073
|
+
return HandleControlActorAddTag(RequestId, Payload, RequestingSocket);
|
|
2074
|
+
if (LowerSub == TEXT("remove_tag"))
|
|
2075
|
+
return HandleControlActorRemoveTag(RequestId, Payload, RequestingSocket);
|
|
2076
|
+
if (LowerSub == TEXT("find_by_name"))
|
|
2077
|
+
return HandleControlActorFindByName(RequestId, Payload, RequestingSocket);
|
|
2078
|
+
if (LowerSub == TEXT("delete_by_tag"))
|
|
2079
|
+
return HandleControlActorDeleteByTag(RequestId, Payload, RequestingSocket);
|
|
2080
|
+
if (LowerSub == TEXT("set_blueprint_variables"))
|
|
2081
|
+
return HandleControlActorSetBlueprintVariables(RequestId, Payload,
|
|
2082
|
+
RequestingSocket);
|
|
2083
|
+
if (LowerSub == TEXT("create_snapshot"))
|
|
2084
|
+
return HandleControlActorCreateSnapshot(RequestId, Payload,
|
|
2085
|
+
RequestingSocket);
|
|
2086
|
+
if (LowerSub == TEXT("restore_snapshot"))
|
|
2087
|
+
return HandleControlActorRestoreSnapshot(RequestId, Payload,
|
|
2088
|
+
RequestingSocket);
|
|
2089
|
+
if (LowerSub == TEXT("export"))
|
|
2090
|
+
return HandleControlActorExport(RequestId, Payload, RequestingSocket);
|
|
2091
|
+
if (LowerSub == TEXT("get_bounding_box"))
|
|
2092
|
+
return HandleControlActorGetBoundingBox(RequestId, Payload,
|
|
2093
|
+
RequestingSocket);
|
|
2094
|
+
if (LowerSub == TEXT("get_metadata"))
|
|
2095
|
+
return HandleControlActorGetMetadata(RequestId, Payload, RequestingSocket);
|
|
2096
|
+
if (LowerSub == TEXT("list") || LowerSub == TEXT("list_actors"))
|
|
2097
|
+
return HandleControlActorList(RequestId, Payload, RequestingSocket);
|
|
2098
|
+
if (LowerSub == TEXT("get") || LowerSub == TEXT("get_actor") ||
|
|
2099
|
+
LowerSub == TEXT("get_actor_by_name"))
|
|
2100
|
+
return HandleControlActorGet(RequestId, Payload, RequestingSocket);
|
|
2101
|
+
|
|
2102
|
+
SendAutomationResponse(
|
|
2103
|
+
RequestingSocket, RequestId, false,
|
|
2104
|
+
FString::Printf(TEXT("Unknown actor control action: %s"), *LowerSub),
|
|
2105
|
+
nullptr, TEXT("UNKNOWN_ACTION"));
|
|
2106
|
+
return true;
|
|
2107
|
+
#else
|
|
2108
|
+
SendAutomationResponse(RequestingSocket, RequestId, false,
|
|
2109
|
+
TEXT("Actor control requires editor build."), nullptr,
|
|
2110
|
+
TEXT("NOT_IMPLEMENTED"));
|
|
2111
|
+
return true;
|
|
2112
|
+
#endif
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlEditorPlay(
|
|
2116
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
2117
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
2118
|
+
#if WITH_EDITOR
|
|
2119
|
+
if (GEditor->PlayWorld) {
|
|
2120
|
+
TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
|
|
2121
|
+
Resp->SetBoolField(TEXT("success"), true);
|
|
2122
|
+
Resp->SetBoolField(TEXT("alreadyPlaying"), true);
|
|
2123
|
+
SendAutomationResponse(Socket, RequestId, true,
|
|
2124
|
+
TEXT("Play session already active"), Resp,
|
|
2125
|
+
FString());
|
|
2126
|
+
return true;
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
FRequestPlaySessionParams PlayParams;
|
|
2130
|
+
PlayParams.WorldType = EPlaySessionWorldType::PlayInEditor;
|
|
2131
|
+
#if MCP_HAS_LEVEL_EDITOR_PLAY_SETTINGS
|
|
2132
|
+
PlayParams.EditorPlaySettings = GetMutableDefault<ULevelEditorPlaySettings>();
|
|
2133
|
+
#endif
|
|
2134
|
+
#if MCP_HAS_LEVEL_EDITOR_MODULE
|
|
2135
|
+
if (FLevelEditorModule *LevelEditorModule =
|
|
2136
|
+
FModuleManager::GetModulePtr<FLevelEditorModule>(
|
|
2137
|
+
TEXT("LevelEditor"))) {
|
|
2138
|
+
TSharedPtr<IAssetViewport> DestinationViewport =
|
|
2139
|
+
LevelEditorModule->GetFirstActiveViewport();
|
|
2140
|
+
if (DestinationViewport.IsValid())
|
|
2141
|
+
PlayParams.DestinationSlateViewport = DestinationViewport;
|
|
2142
|
+
}
|
|
2143
|
+
#endif
|
|
2144
|
+
|
|
2145
|
+
GEditor->RequestPlaySession(PlayParams);
|
|
2146
|
+
TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
|
|
2147
|
+
Resp->SetBoolField(TEXT("success"), true);
|
|
2148
|
+
SendAutomationResponse(Socket, RequestId, true,
|
|
2149
|
+
TEXT("Play in Editor started"), Resp, FString());
|
|
2150
|
+
return true;
|
|
2151
|
+
#else
|
|
2152
|
+
return false;
|
|
2153
|
+
#endif
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlEditorStop(
|
|
2157
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
2158
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
2159
|
+
#if WITH_EDITOR
|
|
2160
|
+
if (!GEditor->PlayWorld) {
|
|
2161
|
+
TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
|
|
2162
|
+
Resp->SetBoolField(TEXT("success"), true);
|
|
2163
|
+
Resp->SetBoolField(TEXT("alreadyStopped"), true);
|
|
2164
|
+
SendAutomationResponse(Socket, RequestId, true,
|
|
2165
|
+
TEXT("Play session not active"), Resp, FString());
|
|
2166
|
+
return true;
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
GEditor->RequestEndPlayMap();
|
|
2170
|
+
TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
|
|
2171
|
+
Resp->SetBoolField(TEXT("success"), true);
|
|
2172
|
+
SendAutomationResponse(Socket, RequestId, true,
|
|
2173
|
+
TEXT("Play in Editor stopped"), Resp, FString());
|
|
2174
|
+
return true;
|
|
2175
|
+
#else
|
|
2176
|
+
return false;
|
|
2177
|
+
#endif
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlEditorEject(
|
|
2181
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
2182
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
2183
|
+
#if WITH_EDITOR
|
|
2184
|
+
if (!GEditor->PlayWorld) {
|
|
2185
|
+
TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
|
|
2186
|
+
Resp->SetBoolField(TEXT("success"), true);
|
|
2187
|
+
Resp->SetBoolField(TEXT("alreadyStopped"), true);
|
|
2188
|
+
SendAutomationResponse(Socket, RequestId, true,
|
|
2189
|
+
TEXT("Play session not active"), Resp, FString());
|
|
2190
|
+
return true;
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
GEditor->RequestEndPlayMap();
|
|
2194
|
+
TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
|
|
2195
|
+
Resp->SetBoolField(TEXT("success"), true);
|
|
2196
|
+
SendAutomationResponse(Socket, RequestId, true,
|
|
2197
|
+
TEXT("Play in Editor ejected"), Resp, FString());
|
|
2198
|
+
return true;
|
|
2199
|
+
#else
|
|
2200
|
+
return false;
|
|
2201
|
+
#endif
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlEditorPossess(
|
|
2205
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
2206
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
2207
|
+
#if WITH_EDITOR
|
|
2208
|
+
FString ActorName;
|
|
2209
|
+
Payload->TryGetStringField(TEXT("actorName"), ActorName);
|
|
2210
|
+
|
|
2211
|
+
// Also try "objectPath" as fallback since schema might use that
|
|
2212
|
+
if (ActorName.IsEmpty())
|
|
2213
|
+
Payload->TryGetStringField(TEXT("objectPath"), ActorName);
|
|
2214
|
+
|
|
2215
|
+
if (ActorName.IsEmpty()) {
|
|
2216
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("actorName required"),
|
|
2217
|
+
nullptr, TEXT("INVALID_ARGUMENT"));
|
|
2218
|
+
return true;
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
AActor *Found = FindActorByName(ActorName);
|
|
2222
|
+
if (!Found) {
|
|
2223
|
+
SendAutomationResponse(
|
|
2224
|
+
Socket, RequestId, false,
|
|
2225
|
+
FString::Printf(TEXT("Actor not found: %s"), *ActorName), nullptr,
|
|
2226
|
+
TEXT("ACTOR_NOT_FOUND"));
|
|
2227
|
+
return true;
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
if (GEditor) {
|
|
2231
|
+
GEditor->SelectNone(true, true, false);
|
|
2232
|
+
GEditor->SelectActor(Found, true, true, true);
|
|
2233
|
+
// 'POSSESS' command works on selected actor in PIE
|
|
2234
|
+
if (GEditor->PlayWorld) {
|
|
2235
|
+
GEditor->Exec(GEditor->PlayWorld, TEXT("POSSESS"));
|
|
2236
|
+
SendAutomationResponse(Socket, RequestId, true, TEXT("Possessed actor"),
|
|
2237
|
+
nullptr);
|
|
2238
|
+
} else {
|
|
2239
|
+
// If not in PIE, we can't possess
|
|
2240
|
+
SendAutomationResponse(Socket, RequestId, false,
|
|
2241
|
+
TEXT("Cannot possess actor while not in PIE"),
|
|
2242
|
+
nullptr, TEXT("NOT_IN_PIE"));
|
|
2243
|
+
}
|
|
2244
|
+
return true;
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("Editor not available"),
|
|
2248
|
+
nullptr, TEXT("EDITOR_NOT_AVAILABLE"));
|
|
2249
|
+
return true;
|
|
2250
|
+
#else
|
|
2251
|
+
return false;
|
|
2252
|
+
#endif
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlEditorFocusActor(
|
|
2256
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
2257
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
2258
|
+
#if WITH_EDITOR
|
|
2259
|
+
FString ActorName;
|
|
2260
|
+
Payload->TryGetStringField(TEXT("actorName"), ActorName);
|
|
2261
|
+
if (ActorName.IsEmpty()) {
|
|
2262
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("actorName required"),
|
|
2263
|
+
nullptr, TEXT("INVALID_ARGUMENT"));
|
|
2264
|
+
return true;
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
if (UEditorActorSubsystem *ActorSS =
|
|
2268
|
+
GEditor->GetEditorSubsystem<UEditorActorSubsystem>()) {
|
|
2269
|
+
TArray<AActor *> Actors = ActorSS->GetAllLevelActors();
|
|
2270
|
+
for (AActor *Actor : Actors) {
|
|
2271
|
+
if (!Actor)
|
|
2272
|
+
continue;
|
|
2273
|
+
if (Actor->GetActorLabel().Equals(ActorName, ESearchCase::IgnoreCase)) {
|
|
2274
|
+
GEditor->SelectNone(true, true, false);
|
|
2275
|
+
GEditor->SelectActor(Actor, true, true, true);
|
|
2276
|
+
GEditor->Exec(nullptr, TEXT("EDITORTEMPVIEWPORT"));
|
|
2277
|
+
GEditor->MoveViewportCamerasToActor(*Actor, false);
|
|
2278
|
+
SendAutomationResponse(Socket, RequestId, true,
|
|
2279
|
+
TEXT("Viewport focused on actor"), nullptr,
|
|
2280
|
+
FString());
|
|
2281
|
+
return true;
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
|
|
2285
|
+
nullptr, TEXT("ACTOR_NOT_FOUND"));
|
|
2286
|
+
return true;
|
|
2287
|
+
}
|
|
2288
|
+
return false;
|
|
2289
|
+
#else
|
|
2290
|
+
return false;
|
|
2291
|
+
#endif
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlEditorSetCamera(
|
|
2295
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
2296
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
2297
|
+
#if WITH_EDITOR
|
|
2298
|
+
const TSharedPtr<FJsonObject> *Loc = nullptr;
|
|
2299
|
+
FVector Location(0, 0, 0);
|
|
2300
|
+
FRotator Rotation(0, 0, 0);
|
|
2301
|
+
if (Payload->TryGetObjectField(TEXT("location"), Loc) && Loc &&
|
|
2302
|
+
(*Loc).IsValid())
|
|
2303
|
+
ReadVectorField(*Loc, TEXT(""), Location, Location);
|
|
2304
|
+
if (Payload->TryGetObjectField(TEXT("rotation"), Loc) && Loc &&
|
|
2305
|
+
(*Loc).IsValid())
|
|
2306
|
+
ReadRotatorField(*Loc, TEXT(""), Rotation, Rotation);
|
|
2307
|
+
|
|
2308
|
+
#if defined(MCP_HAS_UNREALEDITOR_SUBSYSTEM)
|
|
2309
|
+
if (UUnrealEditorSubsystem *UES =
|
|
2310
|
+
GEditor->GetEditorSubsystem<UUnrealEditorSubsystem>()) {
|
|
2311
|
+
UES->SetLevelViewportCameraInfo(Location, Rotation);
|
|
2312
|
+
#if defined(MCP_HAS_LEVELEDITOR_SUBSYSTEM)
|
|
2313
|
+
if (ULevelEditorSubsystem *LES =
|
|
2314
|
+
GEditor->GetEditorSubsystem<ULevelEditorSubsystem>())
|
|
2315
|
+
LES->EditorInvalidateViewports();
|
|
2316
|
+
#endif
|
|
2317
|
+
TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
|
|
2318
|
+
Resp->SetBoolField(TEXT("success"), true);
|
|
2319
|
+
SendAutomationResponse(Socket, RequestId, true, TEXT("Camera set"), Resp,
|
|
2320
|
+
FString());
|
|
2321
|
+
return true;
|
|
2322
|
+
}
|
|
2323
|
+
#endif
|
|
2324
|
+
if (FEditorViewportClient *ViewportClient =
|
|
2325
|
+
GEditor->GetActiveViewport()
|
|
2326
|
+
? (FEditorViewportClient *)GEditor->GetActiveViewport()
|
|
2327
|
+
->GetClient()
|
|
2328
|
+
: nullptr) {
|
|
2329
|
+
ViewportClient->SetViewLocation(Location);
|
|
2330
|
+
ViewportClient->SetViewRotation(Rotation);
|
|
2331
|
+
ViewportClient->Invalidate();
|
|
2332
|
+
TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
|
|
2333
|
+
Resp->SetBoolField(TEXT("success"), true);
|
|
2334
|
+
SendAutomationResponse(Socket, RequestId, true, TEXT("Camera set"), Resp,
|
|
2335
|
+
FString());
|
|
2336
|
+
return true;
|
|
2337
|
+
}
|
|
2338
|
+
return false;
|
|
2339
|
+
#else
|
|
2340
|
+
return false;
|
|
2341
|
+
#endif
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlEditorSetViewMode(
|
|
2345
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
2346
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
2347
|
+
#if WITH_EDITOR
|
|
2348
|
+
FString Mode;
|
|
2349
|
+
Payload->TryGetStringField(TEXT("viewMode"), Mode);
|
|
2350
|
+
FString LowerMode = Mode.ToLower();
|
|
2351
|
+
FString Chosen;
|
|
2352
|
+
if (LowerMode == TEXT("lit"))
|
|
2353
|
+
Chosen = TEXT("Lit");
|
|
2354
|
+
else if (LowerMode == TEXT("unlit"))
|
|
2355
|
+
Chosen = TEXT("Unlit");
|
|
2356
|
+
else if (LowerMode == TEXT("wireframe"))
|
|
2357
|
+
Chosen = TEXT("Wireframe");
|
|
2358
|
+
else if (LowerMode == TEXT("detaillighting"))
|
|
2359
|
+
Chosen = TEXT("DetailLighting");
|
|
2360
|
+
else if (LowerMode == TEXT("lightingonly"))
|
|
2361
|
+
Chosen = TEXT("LightingOnly");
|
|
2362
|
+
else if (LowerMode == TEXT("lightcomplexity"))
|
|
2363
|
+
Chosen = TEXT("LightComplexity");
|
|
2364
|
+
else if (LowerMode == TEXT("shadercomplexity"))
|
|
2365
|
+
Chosen = TEXT("ShaderComplexity");
|
|
2366
|
+
else if (LowerMode == TEXT("lightmapdensity"))
|
|
2367
|
+
Chosen = TEXT("LightmapDensity");
|
|
2368
|
+
else if (LowerMode == TEXT("stationarylightoverlap"))
|
|
2369
|
+
Chosen = TEXT("StationaryLightOverlap");
|
|
2370
|
+
else if (LowerMode == TEXT("reflectionoverride"))
|
|
2371
|
+
Chosen = TEXT("ReflectionOverride");
|
|
2372
|
+
else
|
|
2373
|
+
Chosen = Mode;
|
|
2374
|
+
|
|
2375
|
+
const FString Cmd = FString::Printf(TEXT("viewmode %s"), *Chosen);
|
|
2376
|
+
if (GEditor->Exec(nullptr, *Cmd)) {
|
|
2377
|
+
TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
|
|
2378
|
+
Resp->SetBoolField(TEXT("success"), true);
|
|
2379
|
+
Resp->SetStringField(TEXT("viewMode"), Chosen);
|
|
2380
|
+
SendAutomationResponse(Socket, RequestId, true, TEXT("View mode set"), Resp,
|
|
2381
|
+
FString());
|
|
2382
|
+
return true;
|
|
2383
|
+
}
|
|
2384
|
+
SendAutomationResponse(Socket, RequestId, false,
|
|
2385
|
+
TEXT("View mode command failed"), nullptr,
|
|
2386
|
+
TEXT("EXEC_FAILED"));
|
|
2387
|
+
return true;
|
|
2388
|
+
#else
|
|
2389
|
+
return false;
|
|
2390
|
+
#endif
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlEditorAction(
|
|
2394
|
+
const FString &RequestId, const FString &Action,
|
|
2395
|
+
const TSharedPtr<FJsonObject> &Payload,
|
|
2396
|
+
TSharedPtr<FMcpBridgeWebSocket> RequestingSocket) {
|
|
2397
|
+
const FString Lower = Action.ToLower();
|
|
2398
|
+
if (!Lower.Equals(TEXT("control_editor"), ESearchCase::IgnoreCase) &&
|
|
2399
|
+
!Lower.StartsWith(TEXT("control_editor")))
|
|
2400
|
+
return false;
|
|
2401
|
+
if (!Payload.IsValid()) {
|
|
2402
|
+
SendAutomationError(RequestingSocket, RequestId,
|
|
2403
|
+
TEXT("control_editor payload missing."),
|
|
2404
|
+
TEXT("INVALID_PAYLOAD"));
|
|
2405
|
+
return true;
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
FString SubAction;
|
|
2409
|
+
Payload->TryGetStringField(TEXT("action"), SubAction);
|
|
2410
|
+
const FString LowerSub = SubAction.ToLower();
|
|
2411
|
+
|
|
2412
|
+
#if WITH_EDITOR
|
|
2413
|
+
if (!GEditor) {
|
|
2414
|
+
SendAutomationResponse(RequestingSocket, RequestId, false,
|
|
2415
|
+
TEXT("Editor not available"), nullptr,
|
|
2416
|
+
TEXT("EDITOR_NOT_AVAILABLE"));
|
|
2417
|
+
return true;
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
if (LowerSub == TEXT("play"))
|
|
2421
|
+
return HandleControlEditorPlay(RequestId, Payload, RequestingSocket);
|
|
2422
|
+
if (LowerSub == TEXT("stop"))
|
|
2423
|
+
return HandleControlEditorStop(RequestId, Payload, RequestingSocket);
|
|
2424
|
+
if (LowerSub == TEXT("eject"))
|
|
2425
|
+
return HandleControlEditorEject(RequestId, Payload, RequestingSocket);
|
|
2426
|
+
if (LowerSub == TEXT("possess"))
|
|
2427
|
+
return HandleControlEditorPossess(RequestId, Payload, RequestingSocket);
|
|
2428
|
+
if (LowerSub == TEXT("focus_actor"))
|
|
2429
|
+
return HandleControlEditorFocusActor(RequestId, Payload, RequestingSocket);
|
|
2430
|
+
if (LowerSub == TEXT("set_camera") ||
|
|
2431
|
+
LowerSub == TEXT("set_camera_position") ||
|
|
2432
|
+
LowerSub == TEXT("set_viewport_camera"))
|
|
2433
|
+
return HandleControlEditorSetCamera(RequestId, Payload, RequestingSocket);
|
|
2434
|
+
if (LowerSub == TEXT("set_view_mode"))
|
|
2435
|
+
return HandleControlEditorSetViewMode(RequestId, Payload, RequestingSocket);
|
|
2436
|
+
if (LowerSub == TEXT("open_asset"))
|
|
2437
|
+
return HandleControlEditorOpenAsset(RequestId, Payload, RequestingSocket);
|
|
2438
|
+
|
|
2439
|
+
SendAutomationResponse(
|
|
2440
|
+
RequestingSocket, RequestId, false,
|
|
2441
|
+
FString::Printf(TEXT("Unknown editor control action: %s"), *LowerSub),
|
|
2442
|
+
nullptr, TEXT("UNKNOWN_ACTION"));
|
|
2443
|
+
return true;
|
|
2444
|
+
#else
|
|
2445
|
+
SendAutomationResponse(RequestingSocket, RequestId, false,
|
|
2446
|
+
TEXT("Editor control requires editor build."), nullptr,
|
|
2447
|
+
TEXT("NOT_IMPLEMENTED"));
|
|
2448
|
+
return true;
|
|
2449
|
+
#endif
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlEditorOpenAsset(
|
|
2453
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
2454
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
2455
|
+
#if WITH_EDITOR
|
|
2456
|
+
FString AssetPath;
|
|
2457
|
+
Payload->TryGetStringField(TEXT("assetPath"), AssetPath);
|
|
2458
|
+
if (AssetPath.IsEmpty()) {
|
|
2459
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("assetPath required"),
|
|
2460
|
+
nullptr, TEXT("INVALID_ARGUMENT"));
|
|
2461
|
+
return true;
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
if (!GEditor) {
|
|
2465
|
+
SendAutomationResponse(Socket, RequestId, false,
|
|
2466
|
+
TEXT("Editor not available"), nullptr,
|
|
2467
|
+
TEXT("EDITOR_NOT_AVAILABLE"));
|
|
2468
|
+
return true;
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
UAssetEditorSubsystem *AssetEditorSS =
|
|
2472
|
+
GEditor->GetEditorSubsystem<UAssetEditorSubsystem>();
|
|
2473
|
+
if (!AssetEditorSS) {
|
|
2474
|
+
SendAutomationResponse(Socket, RequestId, false,
|
|
2475
|
+
TEXT("AssetEditorSubsystem not available"), nullptr,
|
|
2476
|
+
TEXT("SUBSYSTEM_MISSING"));
|
|
2477
|
+
return true;
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
if (!UEditorAssetLibrary::DoesAssetExist(AssetPath)) {
|
|
2481
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("Asset not found"),
|
|
2482
|
+
nullptr, TEXT("ASSET_NOT_FOUND"));
|
|
2483
|
+
return true;
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
UObject *Asset = UEditorAssetLibrary::LoadAsset(AssetPath);
|
|
2487
|
+
if (!Asset) {
|
|
2488
|
+
SendAutomationResponse(Socket, RequestId, false,
|
|
2489
|
+
TEXT("Failed to load asset"), nullptr,
|
|
2490
|
+
TEXT("LOAD_FAILED"));
|
|
2491
|
+
return true;
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
const bool bOpened = AssetEditorSS->OpenEditorForAsset(Asset);
|
|
2495
|
+
|
|
2496
|
+
TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
|
|
2497
|
+
Resp->SetBoolField(TEXT("success"), bOpened);
|
|
2498
|
+
Resp->SetStringField(TEXT("assetPath"), AssetPath);
|
|
2499
|
+
|
|
2500
|
+
if (bOpened) {
|
|
2501
|
+
SendAutomationResponse(Socket, RequestId, true, TEXT("Asset opened"), Resp,
|
|
2502
|
+
FString());
|
|
2503
|
+
} else {
|
|
2504
|
+
SendAutomationResponse(Socket, RequestId, false,
|
|
2505
|
+
TEXT("Failed to open asset editor"), Resp,
|
|
2506
|
+
TEXT("OPEN_FAILED"));
|
|
2507
|
+
}
|
|
2508
|
+
return true;
|
|
2509
|
+
#else
|
|
2510
|
+
return false;
|
|
2511
|
+
#endif
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlActorList(
|
|
2515
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
2516
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
2517
|
+
#if WITH_EDITOR
|
|
2518
|
+
FString Filter;
|
|
2519
|
+
Payload->TryGetStringField(TEXT("filter"), Filter);
|
|
2520
|
+
|
|
2521
|
+
UEditorActorSubsystem *ActorSS =
|
|
2522
|
+
GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
|
|
2523
|
+
if (!ActorSS) {
|
|
2524
|
+
SendAutomationResponse(Socket, RequestId, false,
|
|
2525
|
+
TEXT("EditorActorSubsystem unavailable"), nullptr,
|
|
2526
|
+
TEXT("SUBSYSTEM_MISSING"));
|
|
2527
|
+
return true;
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
const TArray<AActor *> &AllActors = ActorSS->GetAllLevelActors();
|
|
2531
|
+
TArray<TSharedPtr<FJsonValue>> ActorsArray;
|
|
2532
|
+
|
|
2533
|
+
for (AActor *Actor : AllActors) {
|
|
2534
|
+
if (!Actor)
|
|
2535
|
+
continue;
|
|
2536
|
+
const FString Label = Actor->GetActorLabel();
|
|
2537
|
+
const FString Name = Actor->GetName();
|
|
2538
|
+
if (!Filter.IsEmpty() && !Label.Contains(Filter) && !Name.Contains(Filter))
|
|
2539
|
+
continue;
|
|
2540
|
+
|
|
2541
|
+
TSharedPtr<FJsonObject> Entry = MakeShared<FJsonObject>();
|
|
2542
|
+
Entry->SetStringField(TEXT("label"), Label);
|
|
2543
|
+
Entry->SetStringField(TEXT("name"), Name);
|
|
2544
|
+
Entry->SetStringField(TEXT("path"), Actor->GetPathName());
|
|
2545
|
+
Entry->SetStringField(TEXT("class"), Actor->GetClass()
|
|
2546
|
+
? Actor->GetClass()->GetPathName()
|
|
2547
|
+
: TEXT(""));
|
|
2548
|
+
ActorsArray.Add(MakeShared<FJsonValueObject>(Entry));
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
2552
|
+
Data->SetArrayField(TEXT("actors"), ActorsArray);
|
|
2553
|
+
Data->SetNumberField(TEXT("count"), ActorsArray.Num());
|
|
2554
|
+
if (!Filter.IsEmpty())
|
|
2555
|
+
Data->SetStringField(TEXT("filter"), Filter);
|
|
2556
|
+
SendStandardSuccessResponse(this, Socket, RequestId, TEXT("Actors listed"),
|
|
2557
|
+
Data);
|
|
2558
|
+
return true;
|
|
2559
|
+
#else
|
|
2560
|
+
return false;
|
|
2561
|
+
#endif
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
bool UMcpAutomationBridgeSubsystem::HandleControlActorGet(
|
|
2565
|
+
const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
|
|
2566
|
+
TSharedPtr<FMcpBridgeWebSocket> Socket) {
|
|
2567
|
+
#if WITH_EDITOR
|
|
2568
|
+
FString TargetName;
|
|
2569
|
+
Payload->TryGetStringField(TEXT("actorName"), TargetName);
|
|
2570
|
+
if (TargetName.IsEmpty()) {
|
|
2571
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("actorName required"),
|
|
2572
|
+
nullptr, TEXT("INVALID_ARGUMENT"));
|
|
2573
|
+
return true;
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
AActor *Found = FindActorByName(TargetName);
|
|
2577
|
+
if (!Found) {
|
|
2578
|
+
SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
|
|
2579
|
+
nullptr, TEXT("ACTOR_NOT_FOUND"));
|
|
2580
|
+
return true;
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
const FTransform Current = Found->GetActorTransform();
|
|
2584
|
+
TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
|
|
2585
|
+
Data->SetStringField(TEXT("name"), Found->GetName());
|
|
2586
|
+
Data->SetStringField(TEXT("label"), Found->GetActorLabel());
|
|
2587
|
+
Data->SetStringField(TEXT("path"), Found->GetPathName());
|
|
2588
|
+
Data->SetStringField(TEXT("class"), Found->GetClass()
|
|
2589
|
+
? Found->GetClass()->GetPathName()
|
|
2590
|
+
: TEXT(""));
|
|
2591
|
+
|
|
2592
|
+
TArray<TSharedPtr<FJsonValue>> TagsArray;
|
|
2593
|
+
for (const FName &Tag : Found->Tags) {
|
|
2594
|
+
TagsArray.Add(MakeShared<FJsonValueString>(Tag.ToString()));
|
|
2595
|
+
}
|
|
2596
|
+
Data->SetArrayField(TEXT("tags"), TagsArray);
|
|
2597
|
+
|
|
2598
|
+
auto MakeArray = [](const FVector &Vec) -> TArray<TSharedPtr<FJsonValue>> {
|
|
2599
|
+
TArray<TSharedPtr<FJsonValue>> Arr;
|
|
2600
|
+
Arr.Add(MakeShared<FJsonValueNumber>(Vec.X));
|
|
2601
|
+
Arr.Add(MakeShared<FJsonValueNumber>(Vec.Y));
|
|
2602
|
+
Arr.Add(MakeShared<FJsonValueNumber>(Vec.Z));
|
|
2603
|
+
return Arr;
|
|
2604
|
+
};
|
|
2605
|
+
Data->SetArrayField(TEXT("location"), MakeArray(Current.GetLocation()));
|
|
2606
|
+
Data->SetArrayField(TEXT("scale"), MakeArray(Current.GetScale3D()));
|
|
2607
|
+
|
|
2608
|
+
SendStandardSuccessResponse(this, Socket, RequestId, TEXT("Actor retrieved"),
|
|
2609
|
+
Data);
|
|
2610
|
+
return true;
|
|
2611
|
+
#else
|
|
2612
|
+
return false;
|
|
2613
|
+
#endif
|
|
2614
|
+
}
|