unreal-engine-mcp-server 0.5.3 → 0.5.5
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/CHANGELOG.md +66 -0
- package/dist/automation/bridge.d.ts +1 -0
- package/dist/automation/bridge.d.ts.map +1 -0
- package/dist/automation/bridge.js +62 -4
- package/dist/automation/bridge.js.map +1 -0
- package/dist/automation/connection-manager.d.ts.map +1 -0
- package/dist/automation/connection-manager.js.map +1 -0
- package/dist/automation/handshake.d.ts.map +1 -0
- package/dist/automation/handshake.js.map +1 -0
- package/dist/automation/index.d.ts.map +1 -0
- package/dist/automation/index.js.map +1 -0
- package/dist/automation/message-handler.d.ts.map +1 -0
- package/dist/automation/message-handler.js.map +1 -0
- package/dist/automation/request-tracker.d.ts.map +1 -0
- package/dist/automation/request-tracker.js.map +1 -0
- package/dist/automation/types.d.ts +1 -0
- package/dist/automation/types.d.ts.map +1 -0
- package/dist/automation/types.js.map +1 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +4 -3
- package/dist/cli.js.map +1 -0
- package/dist/config/class-aliases.d.ts.map +1 -0
- package/dist/config/class-aliases.js.map +1 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js.map +1 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +2 -0
- package/dist/constants.js.map +1 -0
- package/dist/graphql/loaders.d.ts.map +1 -0
- package/dist/graphql/loaders.js.map +1 -0
- package/dist/graphql/resolvers.d.ts.map +1 -0
- package/dist/graphql/resolvers.js +29 -29
- package/dist/graphql/resolvers.js.map +1 -0
- package/dist/graphql/schema.d.ts.map +1 -0
- package/dist/graphql/schema.js.map +1 -0
- package/dist/graphql/server.d.ts +0 -1
- package/dist/graphql/server.d.ts.map +1 -0
- package/dist/graphql/server.js +15 -16
- package/dist/graphql/server.js.map +1 -0
- package/dist/graphql/types.d.ts.map +1 -0
- package/dist/graphql/types.js.map +1 -0
- package/dist/handlers/resource-handlers.d.ts.map +1 -0
- package/dist/handlers/resource-handlers.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +64 -7
- package/dist/index.js.map +1 -0
- package/dist/resources/actors.d.ts.map +1 -0
- package/dist/resources/actors.js.map +1 -0
- package/dist/resources/assets.d.ts.map +1 -0
- package/dist/resources/assets.js +6 -4
- package/dist/resources/assets.js.map +1 -0
- package/dist/resources/levels.d.ts.map +1 -0
- package/dist/resources/levels.js.map +1 -0
- package/dist/server/resource-registry.d.ts.map +1 -0
- package/dist/server/resource-registry.js.map +1 -0
- package/dist/server/tool-registry.d.ts.map +1 -0
- package/dist/server/tool-registry.js.map +1 -0
- package/dist/server-setup.d.ts.map +1 -0
- package/dist/server-setup.js.map +1 -0
- package/dist/services/health-monitor.d.ts.map +1 -0
- package/dist/services/health-monitor.js.map +1 -0
- package/dist/services/metrics-server.d.ts.map +1 -0
- package/dist/services/metrics-server.js +3 -3
- package/dist/services/metrics-server.js.map +1 -0
- package/dist/tools/actors.d.ts.map +1 -0
- package/dist/tools/actors.js +3 -1
- package/dist/tools/actors.js.map +1 -0
- package/dist/tools/animation.d.ts.map +1 -0
- package/dist/tools/animation.js +2 -2
- package/dist/tools/animation.js.map +1 -0
- package/dist/tools/assets.d.ts.map +1 -0
- package/dist/tools/assets.js.map +1 -0
- package/dist/tools/audio.d.ts.map +1 -0
- package/dist/tools/audio.js.map +1 -0
- package/dist/tools/base-tool.d.ts.map +1 -0
- package/dist/tools/base-tool.js.map +1 -0
- package/dist/tools/behavior-tree.d.ts.map +1 -0
- package/dist/tools/behavior-tree.js.map +1 -0
- package/dist/tools/blueprint.d.ts.map +1 -0
- package/dist/tools/blueprint.js +4 -2
- package/dist/tools/blueprint.js.map +1 -0
- package/dist/tools/consolidated-tool-definitions.d.ts.map +1 -0
- package/dist/tools/consolidated-tool-definitions.js.map +1 -0
- package/dist/tools/consolidated-tool-handlers.d.ts.map +1 -0
- package/dist/tools/consolidated-tool-handlers.js.map +1 -0
- package/dist/tools/debug.d.ts.map +1 -0
- package/dist/tools/debug.js +3 -1
- package/dist/tools/debug.js.map +1 -0
- package/dist/tools/dynamic-handler-registry.d.ts.map +1 -0
- package/dist/tools/dynamic-handler-registry.js +3 -1
- package/dist/tools/dynamic-handler-registry.js.map +1 -0
- package/dist/tools/editor.d.ts.map +1 -0
- package/dist/tools/editor.js +1 -1
- package/dist/tools/editor.js.map +1 -0
- package/dist/tools/engine.d.ts.map +1 -0
- package/dist/tools/engine.js.map +1 -0
- package/dist/tools/environment.d.ts.map +1 -0
- package/dist/tools/environment.js +2 -2
- package/dist/tools/environment.js.map +1 -0
- package/dist/tools/foliage.d.ts.map +1 -0
- package/dist/tools/foliage.js.map +1 -0
- package/dist/tools/handlers/actor-handlers.d.ts +1 -1
- package/dist/tools/handlers/actor-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/actor-handlers.js +6 -5
- package/dist/tools/handlers/actor-handlers.js.map +1 -0
- package/dist/tools/handlers/animation-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/animation-handlers.js.map +1 -0
- package/dist/tools/handlers/argument-helper.d.ts.map +1 -0
- package/dist/tools/handlers/argument-helper.js.map +1 -0
- package/dist/tools/handlers/asset-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/asset-handlers.js +5 -1
- package/dist/tools/handlers/asset-handlers.js.map +1 -0
- package/dist/tools/handlers/audio-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/audio-handlers.js.map +1 -0
- package/dist/tools/handlers/blueprint-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/blueprint-handlers.js +2 -1
- package/dist/tools/handlers/blueprint-handlers.js.map +1 -0
- package/dist/tools/handlers/common-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/common-handlers.js.map +1 -0
- package/dist/tools/handlers/editor-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/editor-handlers.js +12 -2
- package/dist/tools/handlers/editor-handlers.js.map +1 -0
- package/dist/tools/handlers/effect-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/effect-handlers.js.map +1 -0
- package/dist/tools/handlers/environment-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/environment-handlers.js.map +1 -0
- package/dist/tools/handlers/graph-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/graph-handlers.js +61 -1
- package/dist/tools/handlers/graph-handlers.js.map +1 -0
- package/dist/tools/handlers/input-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/input-handlers.js.map +1 -0
- package/dist/tools/handlers/inspect-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/inspect-handlers.js.map +1 -0
- package/dist/tools/handlers/level-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/level-handlers.js.map +1 -0
- package/dist/tools/handlers/lighting-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/lighting-handlers.js +23 -1
- package/dist/tools/handlers/lighting-handlers.js.map +1 -0
- package/dist/tools/handlers/performance-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/performance-handlers.js +15 -2
- package/dist/tools/handlers/performance-handlers.js.map +1 -0
- package/dist/tools/handlers/pipeline-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/pipeline-handlers.js +61 -7
- package/dist/tools/handlers/pipeline-handlers.js.map +1 -0
- package/dist/tools/handlers/sequence-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/sequence-handlers.js.map +1 -0
- package/dist/tools/handlers/system-handlers.d.ts.map +1 -0
- package/dist/tools/handlers/system-handlers.js +16 -1
- package/dist/tools/handlers/system-handlers.js.map +1 -0
- package/dist/tools/input.d.ts.map +1 -0
- package/dist/tools/input.js +3 -1
- package/dist/tools/input.js.map +1 -0
- package/dist/tools/introspection.d.ts.map +1 -0
- package/dist/tools/introspection.js.map +1 -0
- package/dist/tools/landscape.d.ts.map +1 -0
- package/dist/tools/landscape.js +3 -1
- package/dist/tools/landscape.js.map +1 -0
- package/dist/tools/level.d.ts.map +1 -0
- package/dist/tools/level.js.map +1 -0
- package/dist/tools/lighting.d.ts.map +1 -0
- package/dist/tools/lighting.js +3 -1
- package/dist/tools/lighting.js.map +1 -0
- package/dist/tools/logs.d.ts.map +1 -0
- package/dist/tools/logs.js.map +1 -0
- package/dist/tools/materials.d.ts.map +1 -0
- package/dist/tools/materials.js +3 -1
- package/dist/tools/materials.js.map +1 -0
- package/dist/tools/niagara.d.ts.map +1 -0
- package/dist/tools/niagara.js +7 -5
- package/dist/tools/niagara.js.map +1 -0
- package/dist/tools/performance.d.ts.map +1 -0
- package/dist/tools/performance.js.map +1 -0
- package/dist/tools/physics.d.ts.map +1 -0
- package/dist/tools/physics.js +9 -7
- package/dist/tools/physics.js.map +1 -0
- package/dist/tools/property-dictionary.d.ts.map +1 -0
- package/dist/tools/property-dictionary.js.map +1 -0
- package/dist/tools/sequence.d.ts.map +1 -0
- package/dist/tools/sequence.js +3 -1
- package/dist/tools/sequence.js.map +1 -0
- package/dist/tools/tool-definition-utils.d.ts.map +1 -0
- package/dist/tools/tool-definition-utils.js.map +1 -0
- package/dist/tools/ui.d.ts.map +1 -0
- package/dist/tools/ui.js +3 -1
- package/dist/tools/ui.js.map +1 -0
- package/dist/types/automation-responses.d.ts.map +1 -0
- package/dist/types/automation-responses.js.map +1 -0
- package/dist/types/env.d.ts.map +1 -0
- package/dist/types/env.js.map +1 -0
- package/dist/types/handler-types.d.ts.map +1 -0
- package/dist/types/handler-types.js.map +1 -0
- package/dist/types/tool-interfaces.d.ts.map +1 -0
- package/dist/types/tool-interfaces.js.map +1 -0
- package/dist/types/tool-types.d.ts.map +1 -0
- package/dist/types/tool-types.js.map +1 -0
- package/dist/unreal-bridge.d.ts +1 -0
- package/dist/unreal-bridge.d.ts.map +1 -0
- package/dist/unreal-bridge.js +8 -0
- package/dist/unreal-bridge.js.map +1 -0
- package/dist/utils/command-validator.d.ts.map +1 -0
- package/dist/utils/command-validator.js.map +1 -0
- package/dist/utils/elicitation.d.ts.map +1 -0
- package/dist/utils/elicitation.js.map +1 -0
- package/dist/utils/error-handler.d.ts.map +1 -0
- package/dist/utils/error-handler.js.map +1 -0
- package/dist/utils/ini-reader.d.ts.map +1 -0
- package/dist/utils/ini-reader.js.map +1 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/normalize.d.ts.map +1 -0
- package/dist/utils/normalize.js.map +1 -0
- package/dist/utils/path-security.d.ts.map +1 -0
- package/dist/utils/path-security.js.map +1 -0
- package/dist/utils/response-factory.d.ts.map +1 -0
- package/dist/utils/response-factory.js +3 -1
- package/dist/utils/response-factory.js.map +1 -0
- package/dist/utils/response-validator.d.ts.map +1 -0
- package/dist/utils/response-validator.js.map +1 -0
- package/dist/utils/result-helpers.d.ts.map +1 -0
- package/dist/utils/result-helpers.js.map +1 -0
- package/dist/utils/safe-json.d.ts.map +1 -0
- package/dist/utils/safe-json.js.map +1 -0
- package/dist/utils/unreal-command-queue.d.ts.map +1 -0
- package/dist/utils/unreal-command-queue.js.map +1 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js.map +1 -0
- package/dist/wasm/index.d.ts.map +1 -0
- package/dist/wasm/index.js.map +1 -0
- package/package.json +12 -34
- package/server.json +2 -2
- package/.dockerignore +0 -57
- package/.env.example +0 -26
- package/.env.production +0 -61
- package/.eslintrc.json +0 -0
- package/.eslintrc.override.json +0 -8
- package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -94
- package/.github/ISSUE_TEMPLATE/config.yml +0 -8
- package/.github/ISSUE_TEMPLATE/feature_request.yml +0 -56
- package/.github/copilot-instructions.md +0 -478
- package/.github/dependabot.yml +0 -19
- package/.github/labeler.yml +0 -24
- package/.github/labels.yml +0 -70
- package/.github/pull_request_template.md +0 -42
- package/.github/release-drafter-config.yml +0 -51
- package/.github/workflows/auto-merge.yml +0 -38
- package/.github/workflows/ci.yml +0 -38
- package/.github/workflows/dependency-review.yml +0 -17
- package/.github/workflows/gemini-issue-triage.yml +0 -172
- package/.github/workflows/greetings.yml +0 -27
- package/.github/workflows/labeler.yml +0 -17
- package/.github/workflows/links.yml +0 -80
- package/.github/workflows/pr-size-labeler.yml +0 -137
- package/.github/workflows/publish-mcp.yml +0 -79
- package/.github/workflows/release-drafter.yml +0 -24
- package/.github/workflows/release.yml +0 -112
- package/.github/workflows/semantic-pull-request.yml +0 -35
- package/.github/workflows/smoke-test.yml +0 -36
- package/.github/workflows/stale.yml +0 -28
- package/CONTRIBUTING.md +0 -140
- package/Dockerfile +0 -37
- package/GEMINI.md +0 -115
- package/Public/Plugin_setup_guide.mp4 +0 -0
- package/Public/icon.png +0 -0
- package/claude_desktop_config_example.json +0 -15
- package/dist/types/responses.d.ts +0 -249
- package/dist/types/responses.js +0 -2
- package/docs/GraphQL-API.md +0 -888
- package/docs/Migration-Guide-v0.5.0.md +0 -684
- package/docs/Roadmap.md +0 -53
- package/docs/WebAssembly-Integration.md +0 -628
- package/docs/editor-plugin-extension.md +0 -370
- package/docs/handler-mapping.md +0 -249
- package/docs/native-automation-progress.md +0 -128
- package/docs/testing-guide.md +0 -423
- package/eslint.config.mjs +0 -68
- package/mcp-config-example.json +0 -14
- package/plugins/McpAutomationBridge/Config/FilterPlugin.ini +0 -8
- package/plugins/McpAutomationBridge/McpAutomationBridge.uplugin +0 -64
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/McpAutomationBridge.Build.cs +0 -189
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.cpp +0 -22
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.h +0 -30
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeHelpers.h +0 -1983
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeModule.cpp +0 -72
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSettings.cpp +0 -46
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +0 -846
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +0 -2393
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetQueryHandlers.cpp +0 -300
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetWorkflowHandlers.cpp +0 -2807
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AudioHandlers.cpp +0 -1087
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BehaviorTreeHandlers.cpp +0 -488
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.cpp +0 -643
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.h +0 -31
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +0 -1094
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +0 -5750
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers_List.cpp +0 -152
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ControlHandlers.cpp +0 -2614
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_DebugHandlers.cpp +0 -42
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EditorFunctionHandlers.cpp +0 -1237
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +0 -1725
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +0 -2241
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_FoliageHandlers.cpp +0 -954
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InputHandlers.cpp +0 -209
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InsightsHandlers.cpp +0 -41
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LandscapeHandlers.cpp +0 -1164
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LevelHandlers.cpp +0 -762
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +0 -663
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LogHandlers.cpp +0 -136
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_MaterialGraphHandlers.cpp +0 -494
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraGraphHandlers.cpp +0 -278
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraHandlers.cpp +0 -625
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PerformanceHandlers.cpp +0 -401
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PipelineHandlers.cpp +0 -67
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +0 -472
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PropertyHandlers.cpp +0 -2634
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_RenderHandlers.cpp +0 -189
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.cpp +0 -917
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.h +0 -39
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +0 -2706
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequencerHandlers.cpp +0 -519
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_TestHandlers.cpp +0 -38
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_UiHandlers.cpp +0 -668
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_WorldPartitionHandlers.cpp +0 -346
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.cpp +0 -1330
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.h +0 -149
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +0 -782
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSettings.h +0 -115
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSubsystem.h +0 -796
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpConnectionManager.h +0 -117
- package/scripts/check-unreal-connection.mjs +0 -19
- package/scripts/clean-tmp.js +0 -23
- package/scripts/patch-wasm.js +0 -26
- package/scripts/run-all-tests.mjs +0 -136
- package/scripts/smoke-test.ts +0 -94
- package/scripts/sync-mcp-plugin.js +0 -143
- package/scripts/test-no-plugin-alternates.mjs +0 -113
- package/scripts/validate-server.js +0 -46
- package/scripts/verify-automation-bridge.js +0 -200
- package/src/automation/bridge.ts +0 -560
- package/src/automation/connection-manager.ts +0 -148
- package/src/automation/handshake.ts +0 -99
- package/src/automation/index.ts +0 -2
- package/src/automation/message-handler.ts +0 -192
- package/src/automation/request-tracker.ts +0 -155
- package/src/automation/types.ts +0 -107
- package/src/cli.ts +0 -34
- package/src/config/class-aliases.ts +0 -65
- package/src/config.ts +0 -73
- package/src/constants.ts +0 -24
- package/src/graphql/loaders.ts +0 -244
- package/src/graphql/resolvers.ts +0 -1008
- package/src/graphql/schema.ts +0 -452
- package/src/graphql/server.ts +0 -156
- package/src/graphql/types.ts +0 -10
- package/src/handlers/resource-handlers.ts +0 -186
- package/src/index.ts +0 -243
- package/src/resources/actors.ts +0 -127
- package/src/resources/assets.ts +0 -286
- package/src/resources/levels.ts +0 -68
- package/src/server/resource-registry.ts +0 -47
- package/src/server/tool-registry.ts +0 -354
- package/src/server-setup.ts +0 -114
- package/src/services/health-monitor.ts +0 -132
- package/src/services/metrics-server.ts +0 -176
- package/src/tools/actors.ts +0 -564
- package/src/tools/animation.ts +0 -941
- package/src/tools/assets.ts +0 -394
- package/src/tools/audio.ts +0 -499
- package/src/tools/base-tool.ts +0 -52
- package/src/tools/behavior-tree.ts +0 -45
- package/src/tools/blueprint.ts +0 -940
- package/src/tools/consolidated-tool-definitions.ts +0 -1256
- package/src/tools/consolidated-tool-handlers.ts +0 -302
- package/src/tools/debug.ts +0 -622
- package/src/tools/dynamic-handler-registry.ts +0 -33
- package/src/tools/editor.ts +0 -435
- package/src/tools/engine.ts +0 -43
- package/src/tools/environment.ts +0 -281
- package/src/tools/foliage.ts +0 -596
- package/src/tools/handlers/actor-handlers.ts +0 -244
- package/src/tools/handlers/animation-handlers.ts +0 -237
- package/src/tools/handlers/argument-helper.ts +0 -142
- package/src/tools/handlers/asset-handlers.ts +0 -550
- package/src/tools/handlers/audio-handlers.ts +0 -194
- package/src/tools/handlers/blueprint-handlers.ts +0 -380
- package/src/tools/handlers/common-handlers.ts +0 -108
- package/src/tools/handlers/editor-handlers.ts +0 -124
- package/src/tools/handlers/effect-handlers.ts +0 -224
- package/src/tools/handlers/environment-handlers.ts +0 -183
- package/src/tools/handlers/graph-handlers.ts +0 -117
- package/src/tools/handlers/input-handlers.ts +0 -28
- package/src/tools/handlers/inspect-handlers.ts +0 -450
- package/src/tools/handlers/level-handlers.ts +0 -253
- package/src/tools/handlers/lighting-handlers.ts +0 -151
- package/src/tools/handlers/performance-handlers.ts +0 -132
- package/src/tools/handlers/pipeline-handlers.ts +0 -128
- package/src/tools/handlers/sequence-handlers.ts +0 -438
- package/src/tools/handlers/system-handlers.ts +0 -564
- package/src/tools/input.ts +0 -160
- package/src/tools/introspection.ts +0 -689
- package/src/tools/landscape.ts +0 -649
- package/src/tools/level.ts +0 -989
- package/src/tools/lighting.ts +0 -1052
- package/src/tools/logs.ts +0 -219
- package/src/tools/materials.ts +0 -295
- package/src/tools/niagara.ts +0 -485
- package/src/tools/performance.ts +0 -661
- package/src/tools/physics.ts +0 -679
- package/src/tools/property-dictionary.ts +0 -98
- package/src/tools/sequence.ts +0 -385
- package/src/tools/tool-definition-utils.ts +0 -35
- package/src/tools/ui.ts +0 -452
- package/src/types/automation-responses.ts +0 -119
- package/src/types/env.ts +0 -17
- package/src/types/handler-types.ts +0 -442
- package/src/types/responses.ts +0 -355
- package/src/types/tool-interfaces.ts +0 -250
- package/src/types/tool-types.ts +0 -575
- package/src/unreal-bridge.ts +0 -693
- package/src/utils/command-validator.ts +0 -139
- package/src/utils/elicitation.ts +0 -132
- package/src/utils/error-handler.ts +0 -287
- package/src/utils/ini-reader.ts +0 -86
- package/src/utils/logger.ts +0 -35
- package/src/utils/normalize.test.ts +0 -162
- package/src/utils/normalize.ts +0 -146
- package/src/utils/path-security.ts +0 -43
- package/src/utils/response-factory.ts +0 -44
- package/src/utils/response-validator.ts +0 -395
- package/src/utils/result-helpers.ts +0 -195
- package/src/utils/safe-json.test.ts +0 -90
- package/src/utils/safe-json.ts +0 -70
- package/src/utils/unreal-command-queue.ts +0 -166
- package/src/utils/validation.test.ts +0 -184
- package/src/utils/validation.ts +0 -312
- package/src/wasm/index.ts +0 -838
- package/test-server.mjs +0 -100
- package/tests/run-unreal-tool-tests.mjs +0 -948
- package/tests/test-animation.mjs +0 -369
- package/tests/test-asset-advanced.mjs +0 -82
- package/tests/test-asset-errors.mjs +0 -35
- package/tests/test-asset-graph.mjs +0 -311
- package/tests/test-audio.mjs +0 -417
- package/tests/test-automation-timeouts.mjs +0 -98
- package/tests/test-behavior-tree.mjs +0 -444
- package/tests/test-blueprint-graph.mjs +0 -410
- package/tests/test-blueprint.mjs +0 -577
- package/tests/test-client-mode.mjs +0 -86
- package/tests/test-console-command.mjs +0 -56
- package/tests/test-control-actor.mjs +0 -425
- package/tests/test-control-editor.mjs +0 -112
- package/tests/test-graphql.mjs +0 -372
- package/tests/test-input.mjs +0 -349
- package/tests/test-inspect.mjs +0 -302
- package/tests/test-landscape.mjs +0 -316
- package/tests/test-lighting.mjs +0 -428
- package/tests/test-manage-asset.mjs +0 -438
- package/tests/test-manage-level.mjs +0 -89
- package/tests/test-materials.mjs +0 -356
- package/tests/test-niagara.mjs +0 -185
- package/tests/test-no-inline-python.mjs +0 -122
- package/tests/test-performance.mjs +0 -539
- package/tests/test-plugin-handshake.mjs +0 -82
- package/tests/test-runner.mjs +0 -933
- package/tests/test-sequence.mjs +0 -104
- package/tests/test-system.mjs +0 -96
- package/tests/test-wasm.mjs +0 -283
- package/tests/test-world-partition.mjs +0 -215
- package/tsconfig.json +0 -56
- package/vitest.config.ts +0 -35
- package/wasm/Cargo.lock +0 -363
- package/wasm/Cargo.toml +0 -42
- package/wasm/LICENSE +0 -21
- package/wasm/README.md +0 -253
- package/wasm/src/dependency_resolver.rs +0 -377
- package/wasm/src/lib.rs +0 -153
- package/wasm/src/property_parser.rs +0 -271
- package/wasm/src/transform_math.rs +0 -396
- package/wasm/tests/integration.rs +0 -109
|
@@ -1,2393 +0,0 @@
|
|
|
1
|
-
#include "McpAutomationBridgeGlobals.h"
|
|
2
|
-
#include "McpAutomationBridgeHelpers.h"
|
|
3
|
-
#include "McpAutomationBridgeSubsystem.h"
|
|
4
|
-
|
|
5
|
-
#if WITH_EDITOR
|
|
6
|
-
#include "Animation/AnimBlueprint.h"
|
|
7
|
-
#include "Animation/AnimBlueprintGeneratedClass.h"
|
|
8
|
-
#include "Animation/AnimMontage.h"
|
|
9
|
-
#include "Animation/AnimSequence.h"
|
|
10
|
-
#include "Animation/AnimationAsset.h"
|
|
11
|
-
#include "Animation/Skeleton.h"
|
|
12
|
-
#include "Engine/SkeletalMesh.h"
|
|
13
|
-
|
|
14
|
-
#if __has_include("Animation/AnimationBlueprintLibrary.h")
|
|
15
|
-
#include "Animation/AnimationBlueprintLibrary.h"
|
|
16
|
-
#elif __has_include("AnimationBlueprintLibrary.h")
|
|
17
|
-
#include "AnimationBlueprintLibrary.h"
|
|
18
|
-
#endif
|
|
19
|
-
#if __has_include("Animation/AnimBlueprintLibrary.h")
|
|
20
|
-
#include "Animation/AnimBlueprintLibrary.h"
|
|
21
|
-
#endif
|
|
22
|
-
#include "Animation/BlendSpace.h"
|
|
23
|
-
#include "Animation/BlendSpace1D.h"
|
|
24
|
-
#include "Editor.h"
|
|
25
|
-
#include "Editor/EditorEngine.h"
|
|
26
|
-
#include "EngineUtils.h"
|
|
27
|
-
#include "RenderingThread.h"
|
|
28
|
-
|
|
29
|
-
#if __has_include("Animation/BlendSpaceBase.h")
|
|
30
|
-
#include "Animation/BlendSpaceBase.h"
|
|
31
|
-
#define MCP_HAS_BLENDSPACE_BASE 1
|
|
32
|
-
#elif __has_include("BlendSpaceBase.h")
|
|
33
|
-
#include "BlendSpaceBase.h"
|
|
34
|
-
#define MCP_HAS_BLENDSPACE_BASE 1
|
|
35
|
-
#else
|
|
36
|
-
#include "Animation/AnimTypes.h"
|
|
37
|
-
#define MCP_HAS_BLENDSPACE_BASE 0
|
|
38
|
-
#endif
|
|
39
|
-
#if __has_include("Factories/BlendSpaceFactoryNew.h") && \
|
|
40
|
-
__has_include("Factories/BlendSpaceFactory1D.h")
|
|
41
|
-
#include "Factories/BlendSpaceFactory1D.h"
|
|
42
|
-
#include "Factories/BlendSpaceFactoryNew.h"
|
|
43
|
-
|
|
44
|
-
#define MCP_HAS_BLENDSPACE_FACTORY 1
|
|
45
|
-
#else
|
|
46
|
-
#define MCP_HAS_BLENDSPACE_FACTORY 0
|
|
47
|
-
#endif
|
|
48
|
-
#include "ControlRig.h"
|
|
49
|
-
// ControlRig headers removed for dynamic loading compatibility
|
|
50
|
-
// #include "ControlRigBlueprint.h" etc.
|
|
51
|
-
#include "AssetRegistry/AssetRegistryModule.h"
|
|
52
|
-
#include "AssetToolsModule.h"
|
|
53
|
-
#include "EditorAssetLibrary.h"
|
|
54
|
-
#include "Factories/AnimBlueprintFactory.h"
|
|
55
|
-
#include "Factories/AnimMontageFactory.h"
|
|
56
|
-
#include "Factories/AnimSequenceFactory.h"
|
|
57
|
-
#include "Factories/PhysicsAssetFactory.h"
|
|
58
|
-
#include "Kismet2/BlueprintEditorUtils.h"
|
|
59
|
-
#include "Misc/PackageName.h"
|
|
60
|
-
#include "Misc/Paths.h"
|
|
61
|
-
#include "Modules/ModuleManager.h"
|
|
62
|
-
#include "PhysicsEngine/PhysicsAsset.h"
|
|
63
|
-
|
|
64
|
-
#if __has_include("Subsystems/EditorActorSubsystem.h")
|
|
65
|
-
#include "Subsystems/EditorActorSubsystem.h"
|
|
66
|
-
#elif __has_include("EditorActorSubsystem.h")
|
|
67
|
-
#include "EditorActorSubsystem.h"
|
|
68
|
-
#endif
|
|
69
|
-
#if __has_include("Subsystems/AssetEditorSubsystem.h")
|
|
70
|
-
#include "Subsystems/AssetEditorSubsystem.h"
|
|
71
|
-
#define MCP_HAS_ASSET_EDITOR_SUBSYSTEM 1
|
|
72
|
-
#elif __has_include("AssetEditorSubsystem.h")
|
|
73
|
-
#include "AssetEditorSubsystem.h"
|
|
74
|
-
#define MCP_HAS_ASSET_EDITOR_SUBSYSTEM 1
|
|
75
|
-
#else
|
|
76
|
-
#define MCP_HAS_ASSET_EDITOR_SUBSYSTEM 0
|
|
77
|
-
#endif
|
|
78
|
-
#include "UObject/Script.h"
|
|
79
|
-
#include "UObject/UnrealType.h"
|
|
80
|
-
|
|
81
|
-
namespace {
|
|
82
|
-
#if MCP_HAS_BLENDSPACE_FACTORY
|
|
83
|
-
/**
|
|
84
|
-
* @brief Creates a new 1D or 2D Blend Space asset bound to a target skeleton.
|
|
85
|
-
*
|
|
86
|
-
* Creates and returns a newly created UBlendSpace (2D) or UBlendSpace1D (1D)
|
|
87
|
-
* asset using the appropriate factory and places it at the given package path.
|
|
88
|
-
*
|
|
89
|
-
* @param AssetName Name to assign to the new asset.
|
|
90
|
-
* @param PackagePath Package path where the asset will be created (e.g.
|
|
91
|
-
* "/Game/Animations").
|
|
92
|
-
* @param TargetSkeleton Skeleton to bind the created Blend Space to.
|
|
93
|
-
* @param bTwoDimensional If true, creates a 2D UBlendSpace; if false, creates a
|
|
94
|
-
* 1D UBlendSpace1D.
|
|
95
|
-
* @param OutError Receives a human-readable error message on failure.
|
|
96
|
-
* @return UObject* Pointer to the created blend space asset on success, or
|
|
97
|
-
* `nullptr` on failure.
|
|
98
|
-
*/
|
|
99
|
-
static UObject *CreateBlendSpaceAsset(const FString &AssetName,
|
|
100
|
-
const FString &PackagePath,
|
|
101
|
-
USkeleton *TargetSkeleton,
|
|
102
|
-
bool bTwoDimensional, FString &OutError) {
|
|
103
|
-
OutError.Reset();
|
|
104
|
-
|
|
105
|
-
UFactory *Factory = nullptr;
|
|
106
|
-
UClass *DesiredClass = nullptr;
|
|
107
|
-
|
|
108
|
-
if (bTwoDimensional) {
|
|
109
|
-
UBlendSpaceFactoryNew *Factory2D = NewObject<UBlendSpaceFactoryNew>();
|
|
110
|
-
if (!Factory2D) {
|
|
111
|
-
OutError = TEXT("Failed to allocate BlendSpace factory");
|
|
112
|
-
return nullptr;
|
|
113
|
-
}
|
|
114
|
-
Factory2D->TargetSkeleton = TargetSkeleton;
|
|
115
|
-
Factory = Factory2D;
|
|
116
|
-
DesiredClass = UBlendSpace::StaticClass();
|
|
117
|
-
} else {
|
|
118
|
-
UBlendSpaceFactory1D *Factory1D = NewObject<UBlendSpaceFactory1D>();
|
|
119
|
-
if (!Factory1D) {
|
|
120
|
-
OutError = TEXT("Failed to allocate BlendSpace1D factory");
|
|
121
|
-
return nullptr;
|
|
122
|
-
}
|
|
123
|
-
Factory1D->TargetSkeleton = TargetSkeleton;
|
|
124
|
-
Factory = Factory1D;
|
|
125
|
-
DesiredClass = UBlendSpace1D::StaticClass();
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (!Factory || !DesiredClass) {
|
|
129
|
-
OutError = TEXT("BlendSpace factory unavailable");
|
|
130
|
-
return nullptr;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
FAssetToolsModule &AssetToolsModule =
|
|
134
|
-
FModuleManager::LoadModuleChecked<FAssetToolsModule>(TEXT("AssetTools"));
|
|
135
|
-
return AssetToolsModule.Get().CreateAsset(AssetName, PackagePath,
|
|
136
|
-
DesiredClass, Factory);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* @brief Applies axis range and grid configuration to a blend space asset.
|
|
141
|
-
*
|
|
142
|
-
* Reads numeric fields from the provided JSON payload and updates the blend
|
|
143
|
-
* space's first axis (minX, maxX, gridX) and, if bTwoDimensional is true,
|
|
144
|
-
* the second axis (minY, maxY, gridY). Marks the asset package dirty when
|
|
145
|
-
* modifications are applied.
|
|
146
|
-
*
|
|
147
|
-
* @param BlendSpaceAsset Blend space or blend space base object to configure.
|
|
148
|
-
* If null, the function is a no-op.
|
|
149
|
-
* @param Payload JSON object containing axis configuration fields:
|
|
150
|
-
* - "minX", "maxX", "gridX" for axis 0 (required defaults:
|
|
151
|
-
* 0,1,3)
|
|
152
|
-
* - "minY", "maxY", "gridY" for axis 1 when bTwoDimensional is
|
|
153
|
-
* true
|
|
154
|
-
* @param bTwoDimensional If true, the second axis is also configured.
|
|
155
|
-
*
|
|
156
|
-
* Notes:
|
|
157
|
-
* - If the engine headers/types required to modify blend parameters are
|
|
158
|
-
* unavailable, the function logs and skips axis configuration.
|
|
159
|
-
* - Grid values are clamped to a minimum of 1.
|
|
160
|
-
*/
|
|
161
|
-
static void ApplyBlendSpaceConfiguration(UObject *BlendSpaceAsset,
|
|
162
|
-
const TSharedPtr<FJsonObject> &Payload,
|
|
163
|
-
bool bTwoDimensional) {
|
|
164
|
-
if (!BlendSpaceAsset || !Payload.IsValid()) {
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
double MinX = 0.0, MaxX = 1.0, GridX = 3.0;
|
|
169
|
-
Payload->TryGetNumberField(TEXT("minX"), MinX);
|
|
170
|
-
Payload->TryGetNumberField(TEXT("maxX"), MaxX);
|
|
171
|
-
Payload->TryGetNumberField(TEXT("gridX"), GridX);
|
|
172
|
-
|
|
173
|
-
#if MCP_HAS_BLENDSPACE_BASE
|
|
174
|
-
if (UBlendSpaceBase *BlendBase = Cast<UBlendSpaceBase>(BlendSpaceAsset)) {
|
|
175
|
-
BlendBase->Modify();
|
|
176
|
-
|
|
177
|
-
FBlendParameter &Axis0 =
|
|
178
|
-
const_cast<FBlendParameter &>(BlendBase->GetBlendParameter(0));
|
|
179
|
-
Axis0.Min = static_cast<float>(MinX);
|
|
180
|
-
Axis0.Max = static_cast<float>(MaxX);
|
|
181
|
-
Axis0.GridNum = FMath::Max(1, static_cast<int32>(GridX));
|
|
182
|
-
|
|
183
|
-
if (bTwoDimensional) {
|
|
184
|
-
double MinY = 0.0, MaxY = 1.0, GridY = 3.0;
|
|
185
|
-
Payload->TryGetNumberField(TEXT("minY"), MinY);
|
|
186
|
-
Payload->TryGetNumberField(TEXT("maxY"), MaxY);
|
|
187
|
-
Payload->TryGetNumberField(TEXT("gridY"), GridY);
|
|
188
|
-
|
|
189
|
-
FBlendParameter &Axis1 =
|
|
190
|
-
const_cast<FBlendParameter &>(BlendBase->GetBlendParameter(1));
|
|
191
|
-
Axis1.Min = static_cast<float>(MinY);
|
|
192
|
-
Axis1.Max = static_cast<float>(MaxY);
|
|
193
|
-
Axis1.GridNum = FMath::Max(1, static_cast<int32>(GridY));
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
BlendBase->MarkPackageDirty();
|
|
197
|
-
}
|
|
198
|
-
#else
|
|
199
|
-
UE_LOG(LogMcpAutomationBridgeSubsystem, Verbose,
|
|
200
|
-
TEXT("ApplyBlendSpaceConfiguration: BlendSpaceBase headers "
|
|
201
|
-
"unavailable; skipping axis configuration."));
|
|
202
|
-
if (bTwoDimensional) {
|
|
203
|
-
UE_LOG(LogMcpAutomationBridgeSubsystem, Verbose,
|
|
204
|
-
TEXT("Requested 2D blend space but BlendSpaceBase headers are "
|
|
205
|
-
"missing; axis configuration skipped."));
|
|
206
|
-
}
|
|
207
|
-
if (!BlendSpaceAsset->IsA<UBlendSpace>() &&
|
|
208
|
-
!BlendSpaceAsset->IsA<UBlendSpace1D>()) {
|
|
209
|
-
UE_LOG(
|
|
210
|
-
LogMcpAutomationBridgeSubsystem, Warning,
|
|
211
|
-
TEXT("ApplyBlendSpaceConfiguration: Asset %s is not a BlendSpace type"),
|
|
212
|
-
*BlendSpaceAsset->GetName());
|
|
213
|
-
}
|
|
214
|
-
#endif
|
|
215
|
-
}
|
|
216
|
-
#endif /** \
|
|
217
|
-
* @brief Executes a list of editor console commands against the \
|
|
218
|
-
* current editor world. \
|
|
219
|
-
* \
|
|
220
|
-
* Skips empty or whitespace-only commands. If any command fails or the \
|
|
221
|
-
* editor/world is unavailable, an explanatory message is written to \
|
|
222
|
-
* OutErrorMessage. \
|
|
223
|
-
* \
|
|
224
|
-
* @param Commands Array of editor command strings to execute. \
|
|
225
|
-
* @param OutErrorMessage Populated with an error description when \
|
|
226
|
-
* execution fails. \
|
|
227
|
-
* @return true if all non-empty commands executed successfully, false \
|
|
228
|
-
* otherwise. \
|
|
229
|
-
*/
|
|
230
|
-
|
|
231
|
-
static bool ExecuteEditorCommandsInternal(const TArray<FString> &Commands,
|
|
232
|
-
FString &OutErrorMessage) {
|
|
233
|
-
OutErrorMessage.Reset();
|
|
234
|
-
|
|
235
|
-
if (!GEditor) {
|
|
236
|
-
OutErrorMessage = TEXT("Editor instance unavailable");
|
|
237
|
-
return false;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
UWorld *EditorWorld = nullptr;
|
|
241
|
-
FWorldContext &EditorContext = GEditor->GetEditorWorldContext(false);
|
|
242
|
-
EditorWorld = EditorContext.World();
|
|
243
|
-
|
|
244
|
-
for (const FString &Command : Commands) {
|
|
245
|
-
const FString Trimmed = Command.TrimStartAndEnd();
|
|
246
|
-
if (Trimmed.IsEmpty()) {
|
|
247
|
-
continue;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (!GEditor->Exec(EditorWorld, *Trimmed)) {
|
|
251
|
-
OutErrorMessage = FString::Printf(
|
|
252
|
-
TEXT("Failed to execute editor command: %s"), *Trimmed);
|
|
253
|
-
return false;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return true;
|
|
258
|
-
}
|
|
259
|
-
} // namespace
|
|
260
|
-
#else
|
|
261
|
-
#define MCP_HAS_BLENDSPACE_FACTORY 0
|
|
262
|
-
#endif // WITH_EDITOR
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* @brief Process an "animation_physics" automation request and send a
|
|
266
|
-
* structured response.
|
|
267
|
-
*
|
|
268
|
-
* Handles sub-actions encoded in the JSON payload (for example: cleanup,
|
|
269
|
-
* create_animation_bp, create_blend_space, create_state_machine, setup_ik,
|
|
270
|
-
* configure_vehicle, setup_physics_simulation, create_animation_asset,
|
|
271
|
-
* setup_retargeting, play_anim_montage, add_notify, etc.). In editor builds
|
|
272
|
-
* this may create/modify assets, execute editor commands, or perform
|
|
273
|
-
* actor/component operations; in non-editor builds it will return a
|
|
274
|
-
* not-implemented response.
|
|
275
|
-
*
|
|
276
|
-
* @param RequestId Unique identifier for the incoming request; included in the
|
|
277
|
-
* response.
|
|
278
|
-
* @param Action Top-level action string (expected to be "animation_physics" or
|
|
279
|
-
* start with it).
|
|
280
|
-
* @param Payload JSON object containing the sub-action and parameters required
|
|
281
|
-
* to perform it.
|
|
282
|
-
* @param RequestingSocket Optional websocket that will receive the automation
|
|
283
|
-
* response/error.
|
|
284
|
-
* @return true if the request was handled (a response was sent, even on error);
|
|
285
|
-
* false if the action did not match "animation_physics" and the handler did not
|
|
286
|
-
* process it.
|
|
287
|
-
*/
|
|
288
|
-
bool UMcpAutomationBridgeSubsystem::HandleAnimationPhysicsAction(
|
|
289
|
-
const FString &RequestId, const FString &Action,
|
|
290
|
-
const TSharedPtr<FJsonObject> &Payload,
|
|
291
|
-
TSharedPtr<FMcpBridgeWebSocket> RequestingSocket) {
|
|
292
|
-
UE_LOG(LogMcpAutomationBridgeSubsystem, Verbose,
|
|
293
|
-
TEXT(">>> HandleAnimationPhysicsAction ENTRY: RequestId=%s "
|
|
294
|
-
"RawAction='%s'"),
|
|
295
|
-
*RequestId, *Action);
|
|
296
|
-
const FString Lower = Action.ToLower();
|
|
297
|
-
if (!Lower.Equals(TEXT("animation_physics"), ESearchCase::IgnoreCase) &&
|
|
298
|
-
!Lower.StartsWith(TEXT("animation_physics")))
|
|
299
|
-
return false;
|
|
300
|
-
|
|
301
|
-
if (!Payload.IsValid()) {
|
|
302
|
-
SendAutomationError(RequestingSocket, RequestId,
|
|
303
|
-
TEXT("animation_physics payload missing."),
|
|
304
|
-
TEXT("INVALID_PAYLOAD"));
|
|
305
|
-
return true;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
FString SubAction;
|
|
309
|
-
Payload->TryGetStringField(TEXT("action"), SubAction);
|
|
310
|
-
const FString LowerSub = SubAction.ToLower();
|
|
311
|
-
UE_LOG(LogMcpAutomationBridgeSubsystem, Verbose,
|
|
312
|
-
TEXT("HandleAnimationPhysicsAction: subaction='%s'"), *LowerSub);
|
|
313
|
-
|
|
314
|
-
#if WITH_EDITOR
|
|
315
|
-
TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
|
|
316
|
-
Resp->SetStringField(TEXT("action"), LowerSub);
|
|
317
|
-
bool bSuccess = false;
|
|
318
|
-
FString Message;
|
|
319
|
-
FString ErrorCode;
|
|
320
|
-
|
|
321
|
-
if (LowerSub == TEXT("cleanup")) {
|
|
322
|
-
const TArray<TSharedPtr<FJsonValue>> *ArtifactsArray = nullptr;
|
|
323
|
-
if (!Payload->TryGetArrayField(TEXT("artifacts"), ArtifactsArray) ||
|
|
324
|
-
!ArtifactsArray) {
|
|
325
|
-
Message = TEXT("artifacts array required for cleanup");
|
|
326
|
-
ErrorCode = TEXT("INVALID_ARGUMENT");
|
|
327
|
-
} else {
|
|
328
|
-
TArray<FString> Cleaned;
|
|
329
|
-
TArray<FString> Missing;
|
|
330
|
-
TArray<FString> Failed;
|
|
331
|
-
|
|
332
|
-
for (const TSharedPtr<FJsonValue> &Val : *ArtifactsArray) {
|
|
333
|
-
if (!Val.IsValid() || Val->Type != EJson::String) {
|
|
334
|
-
continue;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const FString ArtifactPath = Val->AsString().TrimStartAndEnd();
|
|
338
|
-
if (ArtifactPath.IsEmpty()) {
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
if (UEditorAssetLibrary::DoesAssetExist(ArtifactPath)) {
|
|
343
|
-
// Close editors to ensure asset can be deleted
|
|
344
|
-
#if MCP_HAS_ASSET_EDITOR_SUBSYSTEM
|
|
345
|
-
if (GEditor) {
|
|
346
|
-
UObject *Asset = LoadObject<UObject>(nullptr, *ArtifactPath);
|
|
347
|
-
if (Asset) {
|
|
348
|
-
if (UAssetEditorSubsystem *AssetEditorSubsystem =
|
|
349
|
-
GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()) {
|
|
350
|
-
AssetEditorSubsystem->CloseAllEditorsForAsset(Asset);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
#endif
|
|
355
|
-
|
|
356
|
-
// Flush before deleting to release references
|
|
357
|
-
if (GEditor) {
|
|
358
|
-
FlushRenderingCommands();
|
|
359
|
-
GEditor->ForceGarbageCollection(true);
|
|
360
|
-
FlushRenderingCommands();
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
if (UEditorAssetLibrary::DeleteAsset(ArtifactPath)) {
|
|
364
|
-
Cleaned.Add(ArtifactPath);
|
|
365
|
-
} else {
|
|
366
|
-
Failed.Add(ArtifactPath);
|
|
367
|
-
}
|
|
368
|
-
} else {
|
|
369
|
-
Missing.Add(ArtifactPath);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
TArray<TSharedPtr<FJsonValue>> CleanedArray;
|
|
374
|
-
for (const FString &Path : Cleaned) {
|
|
375
|
-
CleanedArray.Add(MakeShared<FJsonValueString>(Path));
|
|
376
|
-
}
|
|
377
|
-
if (CleanedArray.Num() > 0) {
|
|
378
|
-
Resp->SetArrayField(TEXT("cleaned"), CleanedArray);
|
|
379
|
-
}
|
|
380
|
-
Resp->SetNumberField(TEXT("cleanedCount"), Cleaned.Num());
|
|
381
|
-
|
|
382
|
-
if (Missing.Num() > 0) {
|
|
383
|
-
TArray<TSharedPtr<FJsonValue>> MissingArray;
|
|
384
|
-
for (const FString &Path : Missing) {
|
|
385
|
-
MissingArray.Add(MakeShared<FJsonValueString>(Path));
|
|
386
|
-
}
|
|
387
|
-
Resp->SetArrayField(TEXT("missing"), MissingArray);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
if (Failed.Num() > 0) {
|
|
391
|
-
TArray<TSharedPtr<FJsonValue>> FailedArray;
|
|
392
|
-
for (const FString &Path : Failed) {
|
|
393
|
-
FailedArray.Add(MakeShared<FJsonValueString>(Path));
|
|
394
|
-
}
|
|
395
|
-
Resp->SetArrayField(TEXT("failed"), FailedArray);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
if (Cleaned.Num() > 0 && Failed.Num() == 0) {
|
|
399
|
-
bSuccess = true;
|
|
400
|
-
Message = TEXT("Animation artifacts removed");
|
|
401
|
-
} else {
|
|
402
|
-
bSuccess = false;
|
|
403
|
-
Message = Failed.Num() > 0
|
|
404
|
-
? TEXT("Some animation artifacts could not be removed")
|
|
405
|
-
: TEXT("No animation artifacts were removed");
|
|
406
|
-
ErrorCode =
|
|
407
|
-
Failed.Num() > 0 ? TEXT("CLEANUP_PARTIAL") : TEXT("CLEANUP_NO_OP");
|
|
408
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
} else if (LowerSub == TEXT("create_animation_bp")) {
|
|
412
|
-
FString Name;
|
|
413
|
-
if (!Payload->TryGetStringField(TEXT("name"), Name) || Name.IsEmpty()) {
|
|
414
|
-
Message = TEXT("name field required for animation blueprint creation");
|
|
415
|
-
ErrorCode = TEXT("INVALID_ARGUMENT");
|
|
416
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
417
|
-
} else {
|
|
418
|
-
FString SavePath;
|
|
419
|
-
Payload->TryGetStringField(TEXT("savePath"), SavePath);
|
|
420
|
-
if (SavePath.IsEmpty()) {
|
|
421
|
-
SavePath = TEXT("/Game/Animations");
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
FString SkeletonPath;
|
|
425
|
-
Payload->TryGetStringField(TEXT("skeletonPath"), SkeletonPath);
|
|
426
|
-
|
|
427
|
-
USkeleton *TargetSkeleton = nullptr;
|
|
428
|
-
if (!SkeletonPath.IsEmpty()) {
|
|
429
|
-
TargetSkeleton = LoadObject<USkeleton>(nullptr, *SkeletonPath);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// Fallback: try meshPath if skeleton missing
|
|
433
|
-
if (!TargetSkeleton) {
|
|
434
|
-
FString MeshPath;
|
|
435
|
-
if (Payload->TryGetStringField(TEXT("meshPath"), MeshPath) &&
|
|
436
|
-
!MeshPath.IsEmpty()) {
|
|
437
|
-
USkeletalMesh *Mesh = LoadObject<USkeletalMesh>(nullptr, *MeshPath);
|
|
438
|
-
if (Mesh) {
|
|
439
|
-
TargetSkeleton = Mesh->GetSkeleton();
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
if (!TargetSkeleton) {
|
|
445
|
-
Message =
|
|
446
|
-
TEXT("Valid skeletonPath or meshPath required to find skeleton");
|
|
447
|
-
ErrorCode = TEXT("INVALID_ARGUMENT");
|
|
448
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
449
|
-
} else {
|
|
450
|
-
UAnimBlueprintFactory *Factory = NewObject<UAnimBlueprintFactory>();
|
|
451
|
-
Factory->TargetSkeleton = TargetSkeleton;
|
|
452
|
-
|
|
453
|
-
// Allow parent class override
|
|
454
|
-
FString ParentClassPath;
|
|
455
|
-
if (Payload->TryGetStringField(TEXT("parentClass"), ParentClassPath) &&
|
|
456
|
-
!ParentClassPath.IsEmpty()) {
|
|
457
|
-
UClass *ParentClass = LoadClass<UObject>(nullptr, *ParentClassPath);
|
|
458
|
-
if (ParentClass) {
|
|
459
|
-
Factory->ParentClass = ParentClass;
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
FAssetToolsModule &AssetToolsModule =
|
|
464
|
-
FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
|
|
465
|
-
UObject *NewAsset = AssetToolsModule.Get().CreateAsset(
|
|
466
|
-
Name, SavePath, UAnimBlueprint::StaticClass(), Factory);
|
|
467
|
-
|
|
468
|
-
if (NewAsset) {
|
|
469
|
-
bSuccess = true;
|
|
470
|
-
Message = TEXT("Animation Blueprint created");
|
|
471
|
-
Resp->SetStringField(TEXT("blueprintPath"), NewAsset->GetPathName());
|
|
472
|
-
Resp->SetStringField(TEXT("skeletonPath"),
|
|
473
|
-
TargetSkeleton->GetPathName());
|
|
474
|
-
UEditorAssetLibrary::SaveAsset(NewAsset->GetPathName());
|
|
475
|
-
} else {
|
|
476
|
-
Message = TEXT("Failed to create Animation Blueprint asset");
|
|
477
|
-
ErrorCode = TEXT("ASSET_CREATION_FAILED");
|
|
478
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
} else if (LowerSub == TEXT("create_blend_space") ||
|
|
483
|
-
LowerSub == TEXT("create_blend_tree") ||
|
|
484
|
-
LowerSub == TEXT("create_procedural_anim")) {
|
|
485
|
-
FString Name;
|
|
486
|
-
if (!Payload->TryGetStringField(TEXT("name"), Name) || Name.IsEmpty()) {
|
|
487
|
-
Message = TEXT("name field required for blend space creation");
|
|
488
|
-
ErrorCode = TEXT("INVALID_ARGUMENT");
|
|
489
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
490
|
-
} else {
|
|
491
|
-
FString SavePath;
|
|
492
|
-
Payload->TryGetStringField(TEXT("savePath"), SavePath);
|
|
493
|
-
if (SavePath.IsEmpty()) {
|
|
494
|
-
SavePath = TEXT("/Game/Animations");
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
FString SkeletonPath;
|
|
498
|
-
if (!Payload->TryGetStringField(TEXT("skeletonPath"), SkeletonPath) ||
|
|
499
|
-
SkeletonPath.IsEmpty()) {
|
|
500
|
-
Message =
|
|
501
|
-
TEXT("skeletonPath is required to bind blend space to a skeleton");
|
|
502
|
-
ErrorCode = TEXT("INVALID_ARGUMENT");
|
|
503
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
504
|
-
} else {
|
|
505
|
-
USkeleton *TargetSkeleton =
|
|
506
|
-
LoadObject<USkeleton>(nullptr, *SkeletonPath);
|
|
507
|
-
if (!TargetSkeleton) {
|
|
508
|
-
Message = TEXT("Failed to load skeleton for blend space");
|
|
509
|
-
ErrorCode = TEXT("LOAD_FAILED");
|
|
510
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
511
|
-
} else {
|
|
512
|
-
int32 Dimensions = 1;
|
|
513
|
-
double DimensionsNumber = 1.0;
|
|
514
|
-
if (Payload->TryGetNumberField(TEXT("dimensions"),
|
|
515
|
-
DimensionsNumber)) {
|
|
516
|
-
Dimensions = static_cast<int32>(DimensionsNumber);
|
|
517
|
-
}
|
|
518
|
-
const bool bTwoDimensional =
|
|
519
|
-
LowerSub != TEXT("create_blend_space") ? true : (Dimensions >= 2);
|
|
520
|
-
|
|
521
|
-
// Validation for Issue #10
|
|
522
|
-
double MinX = 0.0, MaxX = 1.0, GridX = 3.0;
|
|
523
|
-
Payload->TryGetNumberField(TEXT("minX"), MinX);
|
|
524
|
-
Payload->TryGetNumberField(TEXT("maxX"), MaxX);
|
|
525
|
-
Payload->TryGetNumberField(TEXT("gridX"), GridX);
|
|
526
|
-
|
|
527
|
-
if (MinX >= MaxX) {
|
|
528
|
-
Message = TEXT("minX must be less than maxX");
|
|
529
|
-
ErrorCode = TEXT("INVALID_ARGUMENT");
|
|
530
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
531
|
-
} else if (GridX <= 0) {
|
|
532
|
-
Message = TEXT("gridX must be greater than 0");
|
|
533
|
-
ErrorCode = TEXT("INVALID_ARGUMENT");
|
|
534
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
535
|
-
} else {
|
|
536
|
-
if (bTwoDimensional) {
|
|
537
|
-
double MinY = 0.0, MaxY = 1.0, GridY = 3.0;
|
|
538
|
-
Payload->TryGetNumberField(TEXT("minY"), MinY);
|
|
539
|
-
Payload->TryGetNumberField(TEXT("maxY"), MaxY);
|
|
540
|
-
Payload->TryGetNumberField(TEXT("gridY"), GridY);
|
|
541
|
-
|
|
542
|
-
if (MinY >= MaxY) {
|
|
543
|
-
Message = TEXT("minY must be less than maxY");
|
|
544
|
-
ErrorCode = TEXT("INVALID_ARGUMENT");
|
|
545
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
546
|
-
goto ValidationFailed;
|
|
547
|
-
}
|
|
548
|
-
if (GridY <= 0) {
|
|
549
|
-
Message = TEXT("gridY must be greater than 0");
|
|
550
|
-
ErrorCode = TEXT("INVALID_ARGUMENT");
|
|
551
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
552
|
-
goto ValidationFailed;
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
FString FactoryError;
|
|
557
|
-
#if MCP_HAS_BLENDSPACE_FACTORY
|
|
558
|
-
UObject *CreatedBlendAsset = CreateBlendSpaceAsset(
|
|
559
|
-
Name, SavePath, TargetSkeleton, bTwoDimensional, FactoryError);
|
|
560
|
-
if (CreatedBlendAsset) {
|
|
561
|
-
ApplyBlendSpaceConfiguration(CreatedBlendAsset, Payload,
|
|
562
|
-
bTwoDimensional);
|
|
563
|
-
#if MCP_HAS_BLENDSPACE_BASE
|
|
564
|
-
if (UBlendSpaceBase *BlendSpace =
|
|
565
|
-
Cast<UBlendSpaceBase>(CreatedBlendAsset)) {
|
|
566
|
-
UEditorAssetLibrary::SaveAsset(BlendSpace->GetPathName());
|
|
567
|
-
|
|
568
|
-
bSuccess = true;
|
|
569
|
-
Message = TEXT("Blend space created successfully");
|
|
570
|
-
Resp->SetStringField(TEXT("blendSpacePath"),
|
|
571
|
-
BlendSpace->GetPathName());
|
|
572
|
-
Resp->SetStringField(TEXT("skeletonPath"), SkeletonPath);
|
|
573
|
-
Resp->SetBoolField(TEXT("twoDimensional"), bTwoDimensional);
|
|
574
|
-
} else {
|
|
575
|
-
Message =
|
|
576
|
-
TEXT("Created asset is not a BlendSpaceBase instance");
|
|
577
|
-
ErrorCode = TEXT("TYPE_MISMATCH");
|
|
578
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
579
|
-
}
|
|
580
|
-
#else
|
|
581
|
-
UEditorAssetLibrary::SaveAsset(CreatedBlendAsset->GetPathName());
|
|
582
|
-
|
|
583
|
-
bSuccess = true;
|
|
584
|
-
Message = TEXT("Blend space created (limited configuration)");
|
|
585
|
-
Resp->SetStringField(TEXT("blendSpacePath"),
|
|
586
|
-
CreatedBlendAsset->GetPathName());
|
|
587
|
-
Resp->SetStringField(TEXT("skeletonPath"), SkeletonPath);
|
|
588
|
-
Resp->SetBoolField(TEXT("twoDimensional"), bTwoDimensional);
|
|
589
|
-
Resp->SetStringField(TEXT("warning"),
|
|
590
|
-
TEXT("BlendSpaceBase headers unavailable; "
|
|
591
|
-
"axis configuration skipped."));
|
|
592
|
-
#endif // MCP_HAS_BLENDSPACE_BASE
|
|
593
|
-
} else {
|
|
594
|
-
Message = FactoryError.IsEmpty()
|
|
595
|
-
? TEXT("Failed to create blend space asset")
|
|
596
|
-
: FactoryError;
|
|
597
|
-
ErrorCode = TEXT("ASSET_CREATION_FAILED");
|
|
598
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
599
|
-
}
|
|
600
|
-
#else
|
|
601
|
-
Message = TEXT(
|
|
602
|
-
"Blend space creation requires editor blend space factories");
|
|
603
|
-
ErrorCode = TEXT("NOT_AVAILABLE");
|
|
604
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
605
|
-
#endif
|
|
606
|
-
} // End valid params
|
|
607
|
-
|
|
608
|
-
ValidationFailed:;
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
} else if (LowerSub == TEXT("create_state_machine")) {
|
|
613
|
-
FString BlueprintPath;
|
|
614
|
-
Payload->TryGetStringField(TEXT("blueprintPath"), BlueprintPath);
|
|
615
|
-
if (BlueprintPath.IsEmpty()) {
|
|
616
|
-
Payload->TryGetStringField(TEXT("name"), BlueprintPath);
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
if (BlueprintPath.IsEmpty()) {
|
|
620
|
-
Message = TEXT("blueprintPath is required for create_state_machine");
|
|
621
|
-
ErrorCode = TEXT("INVALID_ARGUMENT");
|
|
622
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
623
|
-
} else {
|
|
624
|
-
FString MachineName;
|
|
625
|
-
Payload->TryGetStringField(TEXT("machineName"), MachineName);
|
|
626
|
-
if (MachineName.IsEmpty()) {
|
|
627
|
-
MachineName = TEXT("StateMachine");
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
TArray<FString> Commands;
|
|
631
|
-
Commands.Add(FString::Printf(TEXT("AddAnimStateMachine %s %s"),
|
|
632
|
-
*BlueprintPath, *MachineName));
|
|
633
|
-
|
|
634
|
-
const TArray<TSharedPtr<FJsonValue>> *StatesArray = nullptr;
|
|
635
|
-
if (Payload->TryGetArrayField(TEXT("states"), StatesArray) &&
|
|
636
|
-
StatesArray) {
|
|
637
|
-
for (const TSharedPtr<FJsonValue> &StateValue : *StatesArray) {
|
|
638
|
-
if (!StateValue.IsValid() || StateValue->Type != EJson::Object) {
|
|
639
|
-
continue;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
const TSharedPtr<FJsonObject> StateObj = StateValue->AsObject();
|
|
643
|
-
FString StateName;
|
|
644
|
-
StateObj->TryGetStringField(TEXT("name"), StateName);
|
|
645
|
-
if (StateName.IsEmpty()) {
|
|
646
|
-
continue;
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
FString AnimationName;
|
|
650
|
-
StateObj->TryGetStringField(TEXT("animation"), AnimationName);
|
|
651
|
-
Commands.Add(FString::Printf(TEXT("AddAnimState %s %s %s %s"),
|
|
652
|
-
*BlueprintPath, *MachineName, *StateName,
|
|
653
|
-
*AnimationName));
|
|
654
|
-
|
|
655
|
-
bool bIsEntry = false;
|
|
656
|
-
bool bIsExit = false;
|
|
657
|
-
StateObj->TryGetBoolField(TEXT("isEntry"), bIsEntry);
|
|
658
|
-
StateObj->TryGetBoolField(TEXT("isExit"), bIsExit);
|
|
659
|
-
if (bIsEntry) {
|
|
660
|
-
Commands.Add(FString::Printf(TEXT("SetAnimStateEntry %s %s %s"),
|
|
661
|
-
*BlueprintPath, *MachineName,
|
|
662
|
-
*StateName));
|
|
663
|
-
}
|
|
664
|
-
if (bIsExit) {
|
|
665
|
-
Commands.Add(FString::Printf(TEXT("SetAnimStateExit %s %s %s"),
|
|
666
|
-
*BlueprintPath, *MachineName,
|
|
667
|
-
*StateName));
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
const TArray<TSharedPtr<FJsonValue>> *TransitionsArray = nullptr;
|
|
673
|
-
if (Payload->TryGetArrayField(TEXT("transitions"), TransitionsArray) &&
|
|
674
|
-
TransitionsArray) {
|
|
675
|
-
for (const TSharedPtr<FJsonValue> &TransitionValue :
|
|
676
|
-
*TransitionsArray) {
|
|
677
|
-
if (!TransitionValue.IsValid() ||
|
|
678
|
-
TransitionValue->Type != EJson::Object) {
|
|
679
|
-
continue;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
const TSharedPtr<FJsonObject> TransitionObj =
|
|
683
|
-
TransitionValue->AsObject();
|
|
684
|
-
FString SourceState;
|
|
685
|
-
FString TargetState;
|
|
686
|
-
TransitionObj->TryGetStringField(TEXT("sourceState"), SourceState);
|
|
687
|
-
TransitionObj->TryGetStringField(TEXT("targetState"), TargetState);
|
|
688
|
-
if (SourceState.IsEmpty() || TargetState.IsEmpty()) {
|
|
689
|
-
continue;
|
|
690
|
-
}
|
|
691
|
-
Commands.Add(FString::Printf(TEXT("AddAnimTransition %s %s %s %s"),
|
|
692
|
-
*BlueprintPath, *MachineName,
|
|
693
|
-
*SourceState, *TargetState));
|
|
694
|
-
|
|
695
|
-
FString Condition;
|
|
696
|
-
if (TransitionObj->TryGetStringField(TEXT("condition"), Condition) &&
|
|
697
|
-
!Condition.IsEmpty()) {
|
|
698
|
-
Commands.Add(FString::Printf(
|
|
699
|
-
TEXT("SetAnimTransitionRule %s %s %s %s %s"), *BlueprintPath,
|
|
700
|
-
*MachineName, *SourceState, *TargetState, *Condition));
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
FString CommandError;
|
|
706
|
-
if (!ExecuteEditorCommands(Commands, CommandError)) {
|
|
707
|
-
Message = CommandError.IsEmpty()
|
|
708
|
-
? TEXT("Failed to create animation state machine")
|
|
709
|
-
: CommandError;
|
|
710
|
-
ErrorCode = TEXT("COMMAND_FAILED");
|
|
711
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
712
|
-
} else {
|
|
713
|
-
bSuccess = true;
|
|
714
|
-
Message = FString::Printf(TEXT("State machine '%s' added to %s"),
|
|
715
|
-
*MachineName, *BlueprintPath);
|
|
716
|
-
Resp->SetStringField(TEXT("blueprintPath"), BlueprintPath);
|
|
717
|
-
Resp->SetStringField(TEXT("machineName"), MachineName);
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
} else if (LowerSub == TEXT("setup_ik")) {
|
|
721
|
-
FString IKName;
|
|
722
|
-
if (!Payload->TryGetStringField(TEXT("name"), IKName) || IKName.IsEmpty()) {
|
|
723
|
-
Message = TEXT("name field required for IK setup");
|
|
724
|
-
ErrorCode = TEXT("INVALID_ARGUMENT");
|
|
725
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
726
|
-
} else {
|
|
727
|
-
FString SavePath;
|
|
728
|
-
Payload->TryGetStringField(TEXT("savePath"), SavePath);
|
|
729
|
-
if (SavePath.IsEmpty()) {
|
|
730
|
-
SavePath = TEXT("/Game/Animations");
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
FString SkeletonPath;
|
|
734
|
-
if (!Payload->TryGetStringField(TEXT("skeletonPath"), SkeletonPath) ||
|
|
735
|
-
SkeletonPath.IsEmpty()) {
|
|
736
|
-
Message = TEXT("skeletonPath is required to bind IK to a skeleton");
|
|
737
|
-
ErrorCode = TEXT("INVALID_ARGUMENT");
|
|
738
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
739
|
-
} else {
|
|
740
|
-
USkeleton *TargetSkeleton =
|
|
741
|
-
LoadObject<USkeleton>(nullptr, *SkeletonPath);
|
|
742
|
-
if (!TargetSkeleton) {
|
|
743
|
-
Message = TEXT("Failed to load skeleton for IK");
|
|
744
|
-
ErrorCode = TEXT("LOAD_FAILED");
|
|
745
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
746
|
-
} else {
|
|
747
|
-
FString FactoryError;
|
|
748
|
-
UBlueprint *ControlRigBlueprint = nullptr;
|
|
749
|
-
#if MCP_HAS_CONTROLRIG_FACTORY
|
|
750
|
-
ControlRigBlueprint = CreateControlRigBlueprint(
|
|
751
|
-
IKName, SavePath, TargetSkeleton, FactoryError);
|
|
752
|
-
#else
|
|
753
|
-
FactoryError =
|
|
754
|
-
TEXT("Control Rig factory not available in this editor build");
|
|
755
|
-
#endif
|
|
756
|
-
if (!ControlRigBlueprint) {
|
|
757
|
-
Message = FactoryError.IsEmpty() ? TEXT("Failed to create IK asset")
|
|
758
|
-
: FactoryError;
|
|
759
|
-
ErrorCode = TEXT("ASSET_CREATION_FAILED");
|
|
760
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
761
|
-
} else {
|
|
762
|
-
bSuccess = true;
|
|
763
|
-
Message = TEXT("IK setup created successfully");
|
|
764
|
-
const FString ControlRigPath = ControlRigBlueprint->GetPathName();
|
|
765
|
-
Resp->SetStringField(TEXT("ikPath"), ControlRigPath);
|
|
766
|
-
Resp->SetStringField(TEXT("controlRigPath"), ControlRigPath);
|
|
767
|
-
Resp->SetStringField(TEXT("skeletonPath"), SkeletonPath);
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
} else if (LowerSub == TEXT("configure_vehicle")) {
|
|
773
|
-
FString VehicleName;
|
|
774
|
-
if (!Payload->TryGetStringField(TEXT("vehicleName"), VehicleName) ||
|
|
775
|
-
VehicleName.IsEmpty()) {
|
|
776
|
-
Message = TEXT("vehicleName is required");
|
|
777
|
-
ErrorCode = TEXT("INVALID_ARGUMENT");
|
|
778
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
779
|
-
} else {
|
|
780
|
-
FString VehicleTypeRaw;
|
|
781
|
-
Payload->TryGetStringField(TEXT("vehicleType"), VehicleTypeRaw);
|
|
782
|
-
if (VehicleTypeRaw.IsEmpty()) {
|
|
783
|
-
Message = TEXT("vehicleType is required");
|
|
784
|
-
ErrorCode = TEXT("INVALID_ARGUMENT");
|
|
785
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
786
|
-
} else {
|
|
787
|
-
const FString NormalizedType = VehicleTypeRaw.ToLower();
|
|
788
|
-
const TMap<FString, FString> VehicleTypeMap = {
|
|
789
|
-
{TEXT("car"), TEXT("Car")},
|
|
790
|
-
{TEXT("bike"), TEXT("Bike")},
|
|
791
|
-
{TEXT("motorcycle"), TEXT("Bike")},
|
|
792
|
-
{TEXT("motorbike"), TEXT("Bike")},
|
|
793
|
-
{TEXT("tank"), TEXT("Tank")},
|
|
794
|
-
{TEXT("aircraft"), TEXT("Aircraft")},
|
|
795
|
-
{TEXT("plane"), TEXT("Aircraft")}};
|
|
796
|
-
|
|
797
|
-
const FString *VehicleTypePtr = VehicleTypeMap.Find(NormalizedType);
|
|
798
|
-
// Use mapped value or passthrough raw value for unknown types
|
|
799
|
-
FString FinalVehicleType =
|
|
800
|
-
VehicleTypePtr ? *VehicleTypePtr : VehicleTypeRaw;
|
|
801
|
-
|
|
802
|
-
{
|
|
803
|
-
TArray<FString> Commands;
|
|
804
|
-
Commands.Add(FString::Printf(TEXT("CreateVehicle %s %s"),
|
|
805
|
-
*VehicleName, *FinalVehicleType));
|
|
806
|
-
|
|
807
|
-
const TArray<TSharedPtr<FJsonValue>> *WheelsArray = nullptr;
|
|
808
|
-
if (Payload->TryGetArrayField(TEXT("wheels"), WheelsArray) &&
|
|
809
|
-
WheelsArray) {
|
|
810
|
-
for (int32 Index = 0; Index < WheelsArray->Num(); ++Index) {
|
|
811
|
-
const TSharedPtr<FJsonValue> &WheelValue = (*WheelsArray)[Index];
|
|
812
|
-
if (!WheelValue.IsValid() || WheelValue->Type != EJson::Object) {
|
|
813
|
-
continue;
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
const TSharedPtr<FJsonObject> WheelObj = WheelValue->AsObject();
|
|
817
|
-
FString WheelName;
|
|
818
|
-
WheelObj->TryGetStringField(TEXT("name"), WheelName);
|
|
819
|
-
if (WheelName.IsEmpty()) {
|
|
820
|
-
WheelName = FString::Printf(TEXT("Wheel_%d"), Index);
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
double Radius = 0.0, Width = 0.0, Mass = 0.0;
|
|
824
|
-
WheelObj->TryGetNumberField(TEXT("radius"), Radius);
|
|
825
|
-
WheelObj->TryGetNumberField(TEXT("width"), Width);
|
|
826
|
-
WheelObj->TryGetNumberField(TEXT("mass"), Mass);
|
|
827
|
-
|
|
828
|
-
Commands.Add(FString::Printf(
|
|
829
|
-
TEXT("AddVehicleWheel %s %s %.4f %.4f %.4f"), *VehicleName,
|
|
830
|
-
*WheelName, Radius, Width, Mass));
|
|
831
|
-
|
|
832
|
-
bool bSteering = false;
|
|
833
|
-
if (WheelObj->TryGetBoolField(TEXT("isSteering"), bSteering) &&
|
|
834
|
-
bSteering) {
|
|
835
|
-
Commands.Add(
|
|
836
|
-
FString::Printf(TEXT("SetWheelSteering %s %s true"),
|
|
837
|
-
*VehicleName, *WheelName));
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
bool bDriving = false;
|
|
841
|
-
if (WheelObj->TryGetBoolField(TEXT("isDriving"), bDriving) &&
|
|
842
|
-
bDriving) {
|
|
843
|
-
Commands.Add(FString::Printf(TEXT("SetWheelDriving %s %s true"),
|
|
844
|
-
*VehicleName, *WheelName));
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
const TSharedPtr<FJsonObject> *EngineObj = nullptr;
|
|
850
|
-
if (Payload->TryGetObjectField(TEXT("engine"), EngineObj) &&
|
|
851
|
-
EngineObj && (*EngineObj).IsValid()) {
|
|
852
|
-
double MaxRPM = 0.0;
|
|
853
|
-
(*EngineObj)->TryGetNumberField(TEXT("maxRPM"), MaxRPM);
|
|
854
|
-
if (MaxRPM > 0.0) {
|
|
855
|
-
Commands.Add(FString::Printf(TEXT("SetEngineMaxRPM %s %.4f"),
|
|
856
|
-
*VehicleName, MaxRPM));
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
const TArray<TSharedPtr<FJsonValue>> *TorqueCurve = nullptr;
|
|
860
|
-
if ((*EngineObj)
|
|
861
|
-
->TryGetArrayField(TEXT("torqueCurve"), TorqueCurve) &&
|
|
862
|
-
TorqueCurve) {
|
|
863
|
-
for (const TSharedPtr<FJsonValue> &TorqueValue : *TorqueCurve) {
|
|
864
|
-
if (!TorqueValue.IsValid()) {
|
|
865
|
-
continue;
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
double RPM = 0.0;
|
|
869
|
-
double Torque = 0.0;
|
|
870
|
-
|
|
871
|
-
if (TorqueValue->Type == EJson::Array) {
|
|
872
|
-
const TArray<TSharedPtr<FJsonValue>> TorquePair =
|
|
873
|
-
TorqueValue->AsArray();
|
|
874
|
-
if (TorquePair.Num() >= 2) {
|
|
875
|
-
RPM = TorquePair[0]->AsNumber();
|
|
876
|
-
Torque = TorquePair[1]->AsNumber();
|
|
877
|
-
}
|
|
878
|
-
} else if (TorqueValue->Type == EJson::Object) {
|
|
879
|
-
const TSharedPtr<FJsonObject> TorqueObj =
|
|
880
|
-
TorqueValue->AsObject();
|
|
881
|
-
TorqueObj->TryGetNumberField(TEXT("rpm"), RPM);
|
|
882
|
-
TorqueObj->TryGetNumberField(TEXT("torque"), Torque);
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
Commands.Add(
|
|
886
|
-
FString::Printf(TEXT("AddTorqueCurvePoint %s %.4f %.4f"),
|
|
887
|
-
*VehicleName, RPM, Torque));
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
const TSharedPtr<FJsonObject> *TransmissionObj = nullptr;
|
|
893
|
-
if (Payload->TryGetObjectField(TEXT("transmission"),
|
|
894
|
-
TransmissionObj) &&
|
|
895
|
-
TransmissionObj && (*TransmissionObj).IsValid()) {
|
|
896
|
-
const TArray<TSharedPtr<FJsonValue>> *GearsArray = nullptr;
|
|
897
|
-
if ((*TransmissionObj)
|
|
898
|
-
->TryGetArrayField(TEXT("gears"), GearsArray) &&
|
|
899
|
-
GearsArray) {
|
|
900
|
-
for (int32 GearIndex = 0; GearIndex < GearsArray->Num();
|
|
901
|
-
++GearIndex) {
|
|
902
|
-
const double GearRatio = (*GearsArray)[GearIndex]->AsNumber();
|
|
903
|
-
Commands.Add(FString::Printf(TEXT("SetGearRatio %s %d %.4f"),
|
|
904
|
-
*VehicleName, GearIndex,
|
|
905
|
-
GearRatio));
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
double FinalDrive = 0.0;
|
|
910
|
-
if ((*TransmissionObj)
|
|
911
|
-
->TryGetNumberField(TEXT("finalDriveRatio"), FinalDrive)) {
|
|
912
|
-
Commands.Add(FString::Printf(TEXT("SetFinalDriveRatio %s %.4f"),
|
|
913
|
-
*VehicleName, FinalDrive));
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
FString CommandError;
|
|
918
|
-
if (!ExecuteEditorCommands(Commands, CommandError)) {
|
|
919
|
-
Message = CommandError.IsEmpty()
|
|
920
|
-
? TEXT("Failed to configure vehicle")
|
|
921
|
-
: CommandError;
|
|
922
|
-
ErrorCode = TEXT("COMMAND_FAILED");
|
|
923
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
924
|
-
} else {
|
|
925
|
-
bSuccess = true;
|
|
926
|
-
Message =
|
|
927
|
-
FString::Printf(TEXT("Vehicle %s configured"), *VehicleName);
|
|
928
|
-
Resp->SetStringField(TEXT("vehicleName"), VehicleName);
|
|
929
|
-
Resp->SetStringField(TEXT("vehicleType"), *VehicleTypePtr);
|
|
930
|
-
|
|
931
|
-
const TArray<TSharedPtr<FJsonValue>> *PluginDeps = nullptr;
|
|
932
|
-
if (Payload->TryGetArrayField(TEXT("pluginDependencies"),
|
|
933
|
-
PluginDeps) &&
|
|
934
|
-
PluginDeps) {
|
|
935
|
-
TArray<TSharedPtr<FJsonValue>> PluginArray;
|
|
936
|
-
for (const TSharedPtr<FJsonValue> &DepValue : *PluginDeps) {
|
|
937
|
-
if (DepValue.IsValid() && DepValue->Type == EJson::String) {
|
|
938
|
-
PluginArray.Add(
|
|
939
|
-
MakeShared<FJsonValueString>(DepValue->AsString()));
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
if (PluginArray.Num() > 0) {
|
|
943
|
-
Resp->SetArrayField(TEXT("pluginDependencies"), PluginArray);
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
} else if (LowerSub == TEXT("setup_physics_simulation")) {
|
|
951
|
-
FString MeshPath;
|
|
952
|
-
Payload->TryGetStringField(TEXT("meshPath"), MeshPath);
|
|
953
|
-
|
|
954
|
-
FString SkeletonPath;
|
|
955
|
-
Payload->TryGetStringField(TEXT("skeletonPath"), SkeletonPath);
|
|
956
|
-
|
|
957
|
-
// Support actorName parameter to find skeletal mesh from a spawned actor
|
|
958
|
-
FString ActorName;
|
|
959
|
-
Payload->TryGetStringField(TEXT("actorName"), ActorName);
|
|
960
|
-
|
|
961
|
-
const bool bMeshProvided = !MeshPath.IsEmpty();
|
|
962
|
-
const bool bSkeletonProvided = !SkeletonPath.IsEmpty();
|
|
963
|
-
const bool bActorProvided = !ActorName.IsEmpty();
|
|
964
|
-
|
|
965
|
-
bool bMeshLoadFailed = false;
|
|
966
|
-
bool bSkeletonLoadFailed = false;
|
|
967
|
-
bool bSkeletonMissingPreview = false;
|
|
968
|
-
|
|
969
|
-
USkeletalMesh *TargetMesh = nullptr;
|
|
970
|
-
bool bMeshTypeMismatch = false;
|
|
971
|
-
FString FoundClassName;
|
|
972
|
-
|
|
973
|
-
// If actorName provided, try to find the actor and get its skeletal mesh
|
|
974
|
-
if (!bMeshProvided && !bSkeletonProvided && bActorProvided) {
|
|
975
|
-
UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
|
|
976
|
-
TEXT("Attempting to find actor by name: '%s'"), *ActorName);
|
|
977
|
-
AActor *FoundActor = FindActorByName(ActorName);
|
|
978
|
-
if (FoundActor) {
|
|
979
|
-
UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
|
|
980
|
-
TEXT("Found actor: '%s' (Label: '%s')"), *FoundActor->GetName(),
|
|
981
|
-
*FoundActor->GetActorLabel());
|
|
982
|
-
// Try to get skeletal mesh component
|
|
983
|
-
if (USkeletalMeshComponent *SkelComp =
|
|
984
|
-
FoundActor->FindComponentByClass<USkeletalMeshComponent>()) {
|
|
985
|
-
TargetMesh = SkelComp->GetSkeletalMeshAsset();
|
|
986
|
-
if (TargetMesh) {
|
|
987
|
-
UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
|
|
988
|
-
TEXT("Found skeletal mesh asset: '%s'"),
|
|
989
|
-
*TargetMesh->GetName());
|
|
990
|
-
} else {
|
|
991
|
-
Message =
|
|
992
|
-
FString::Printf(TEXT("Actor '%s' has a SkeletalMeshComponent "
|
|
993
|
-
"but no SkeletalMesh asset assigned."),
|
|
994
|
-
*FoundActor->GetName());
|
|
995
|
-
ErrorCode = TEXT("ACTOR_SKELETAL_MESH_ASSET_NULL");
|
|
996
|
-
UE_LOG(LogMcpAutomationBridgeSubsystem, Error, TEXT("%s"),
|
|
997
|
-
*Message);
|
|
998
|
-
}
|
|
999
|
-
} else {
|
|
1000
|
-
Message = FString::Printf(
|
|
1001
|
-
TEXT("Actor '%s' does not have a SkeletalMeshComponent."),
|
|
1002
|
-
*FoundActor->GetName());
|
|
1003
|
-
ErrorCode = TEXT("ACTOR_NO_SKELETAL_MESH_COMPONENT");
|
|
1004
|
-
UE_LOG(LogMcpAutomationBridgeSubsystem, Error, TEXT("%s"), *Message);
|
|
1005
|
-
}
|
|
1006
|
-
} else {
|
|
1007
|
-
Message = FString::Printf(TEXT("Actor '%s' not found."), *ActorName);
|
|
1008
|
-
ErrorCode = TEXT("ACTOR_NOT_FOUND");
|
|
1009
|
-
UE_LOG(LogMcpAutomationBridgeSubsystem, Error, TEXT("%s"), *Message);
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
if (!TargetMesh) {
|
|
1013
|
-
Resp->SetStringField(TEXT("actorName"), ActorName);
|
|
1014
|
-
bSuccess = false;
|
|
1015
|
-
SendAutomationResponse(RequestingSocket, RequestId, bSuccess, Message,
|
|
1016
|
-
Resp, ErrorCode);
|
|
1017
|
-
return true;
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
if (bMeshProvided) {
|
|
1022
|
-
if (UEditorAssetLibrary::DoesAssetExist(MeshPath)) {
|
|
1023
|
-
UObject *Asset = UEditorAssetLibrary::LoadAsset(MeshPath);
|
|
1024
|
-
TargetMesh = Cast<USkeletalMesh>(Asset);
|
|
1025
|
-
if (!TargetMesh && Asset) {
|
|
1026
|
-
bMeshTypeMismatch = true;
|
|
1027
|
-
FoundClassName = Asset->GetClass()->GetName();
|
|
1028
|
-
UE_LOG(LogMcpAutomationBridgeSubsystem, Warning,
|
|
1029
|
-
TEXT("setup_physics_simulation: Asset %s is not a "
|
|
1030
|
-
"SkeletalMesh (Class: %s)"),
|
|
1031
|
-
*MeshPath, *FoundClassName);
|
|
1032
|
-
} else if (!Asset) {
|
|
1033
|
-
bMeshLoadFailed = true;
|
|
1034
|
-
UE_LOG(LogMcpAutomationBridgeSubsystem, Warning,
|
|
1035
|
-
TEXT("setup_physics_simulation: failed to load mesh asset %s"),
|
|
1036
|
-
*MeshPath);
|
|
1037
|
-
}
|
|
1038
|
-
} else {
|
|
1039
|
-
bMeshLoadFailed = true;
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
USkeleton *TargetSkeleton = nullptr;
|
|
1044
|
-
if (!TargetMesh && bSkeletonProvided) {
|
|
1045
|
-
if (UEditorAssetLibrary::DoesAssetExist(SkeletonPath)) {
|
|
1046
|
-
TargetSkeleton = LoadObject<USkeleton>(nullptr, *SkeletonPath);
|
|
1047
|
-
if (TargetSkeleton) {
|
|
1048
|
-
TargetMesh = TargetSkeleton->GetPreviewMesh();
|
|
1049
|
-
if (!TargetMesh) {
|
|
1050
|
-
bSkeletonMissingPreview = true;
|
|
1051
|
-
UE_LOG(LogMcpAutomationBridgeSubsystem, Warning,
|
|
1052
|
-
TEXT("setup_physics_simulation: skeleton %s has no preview "
|
|
1053
|
-
"mesh"),
|
|
1054
|
-
*SkeletonPath);
|
|
1055
|
-
}
|
|
1056
|
-
} else {
|
|
1057
|
-
bSkeletonLoadFailed = true;
|
|
1058
|
-
UE_LOG(LogMcpAutomationBridgeSubsystem, Warning,
|
|
1059
|
-
TEXT("setup_physics_simulation: failed to load skeleton %s"),
|
|
1060
|
-
*SkeletonPath);
|
|
1061
|
-
}
|
|
1062
|
-
} else {
|
|
1063
|
-
bSkeletonLoadFailed = true;
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
if (!TargetSkeleton && TargetMesh) {
|
|
1068
|
-
TargetSkeleton = TargetMesh->GetSkeleton();
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
if (!TargetMesh) {
|
|
1072
|
-
if (bMeshTypeMismatch) {
|
|
1073
|
-
Message = FString::Printf(
|
|
1074
|
-
TEXT("asset found but is not a SkeletalMesh: %s (is %s)"),
|
|
1075
|
-
*MeshPath, *FoundClassName);
|
|
1076
|
-
ErrorCode = TEXT("TYPE_MISMATCH");
|
|
1077
|
-
Resp->SetStringField(TEXT("meshPath"), MeshPath);
|
|
1078
|
-
Resp->SetStringField(TEXT("actualClass"), FoundClassName);
|
|
1079
|
-
} else if (bMeshLoadFailed) {
|
|
1080
|
-
Message = FString::Printf(TEXT("asset not found: skeletal mesh %s"),
|
|
1081
|
-
*MeshPath);
|
|
1082
|
-
ErrorCode = TEXT("ASSET_NOT_FOUND");
|
|
1083
|
-
Resp->SetStringField(TEXT("meshPath"), MeshPath);
|
|
1084
|
-
} else if (bSkeletonLoadFailed) {
|
|
1085
|
-
Message = FString::Printf(TEXT("asset not found: skeleton %s"),
|
|
1086
|
-
*SkeletonPath);
|
|
1087
|
-
ErrorCode = TEXT("ASSET_NOT_FOUND");
|
|
1088
|
-
Resp->SetStringField(TEXT("skeletonPath"), SkeletonPath);
|
|
1089
|
-
} else if (bSkeletonMissingPreview) {
|
|
1090
|
-
Message = FString::Printf(TEXT("asset not found: skeleton %s (no "
|
|
1091
|
-
"preview mesh for physics simulation)"),
|
|
1092
|
-
*SkeletonPath);
|
|
1093
|
-
ErrorCode = TEXT("ASSET_NOT_FOUND");
|
|
1094
|
-
Resp->SetStringField(TEXT("skeletonPath"), SkeletonPath);
|
|
1095
|
-
} else {
|
|
1096
|
-
Message = TEXT("asset not found: no valid skeletal mesh provided for "
|
|
1097
|
-
"physics simulation setup");
|
|
1098
|
-
ErrorCode = TEXT("ASSET_NOT_FOUND");
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
1102
|
-
} else {
|
|
1103
|
-
if (!TargetSkeleton && !SkeletonPath.IsEmpty()) {
|
|
1104
|
-
TargetSkeleton = LoadObject<USkeleton>(nullptr, *SkeletonPath);
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
FString PhysicsAssetName;
|
|
1108
|
-
Payload->TryGetStringField(TEXT("physicsAssetName"), PhysicsAssetName);
|
|
1109
|
-
if (PhysicsAssetName.IsEmpty()) {
|
|
1110
|
-
PhysicsAssetName = TargetMesh->GetName() + TEXT("_Physics");
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
FString SavePath;
|
|
1114
|
-
Payload->TryGetStringField(TEXT("savePath"), SavePath);
|
|
1115
|
-
if (SavePath.IsEmpty()) {
|
|
1116
|
-
SavePath = TEXT("/Game/Physics");
|
|
1117
|
-
}
|
|
1118
|
-
SavePath = SavePath.TrimStartAndEnd();
|
|
1119
|
-
|
|
1120
|
-
if (!FPackageName::IsValidLongPackageName(SavePath)) {
|
|
1121
|
-
FString NormalizedPath;
|
|
1122
|
-
if (!FPackageName::TryConvertFilenameToLongPackageName(
|
|
1123
|
-
SavePath, NormalizedPath)) {
|
|
1124
|
-
Message = TEXT("Invalid savePath for physics asset");
|
|
1125
|
-
ErrorCode = TEXT("INVALID_ARGUMENT");
|
|
1126
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
1127
|
-
SavePath.Reset();
|
|
1128
|
-
} else {
|
|
1129
|
-
SavePath = NormalizedPath;
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
if (!SavePath.IsEmpty()) {
|
|
1134
|
-
if (!UEditorAssetLibrary::DoesDirectoryExist(SavePath)) {
|
|
1135
|
-
UEditorAssetLibrary::MakeDirectory(SavePath);
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
const FString PhysicsAssetObjectPath =
|
|
1139
|
-
FString::Printf(TEXT("%s/%s"), *SavePath, *PhysicsAssetName);
|
|
1140
|
-
|
|
1141
|
-
if (UEditorAssetLibrary::DoesAssetExist(PhysicsAssetObjectPath)) {
|
|
1142
|
-
bSuccess = true;
|
|
1143
|
-
Message = TEXT(
|
|
1144
|
-
"Physics simulation already configured - existing asset reused");
|
|
1145
|
-
Resp->SetStringField(TEXT("physicsAssetPath"),
|
|
1146
|
-
PhysicsAssetObjectPath);
|
|
1147
|
-
Resp->SetBoolField(TEXT("existingAsset"), true);
|
|
1148
|
-
Resp->SetStringField(TEXT("savePath"), SavePath);
|
|
1149
|
-
Resp->SetStringField(TEXT("meshPath"), TargetMesh->GetPathName());
|
|
1150
|
-
if (TargetSkeleton) {
|
|
1151
|
-
Resp->SetStringField(TEXT("skeletonPath"),
|
|
1152
|
-
TargetSkeleton->GetPathName());
|
|
1153
|
-
}
|
|
1154
|
-
} else {
|
|
1155
|
-
UPhysicsAssetFactory *PhysicsFactory =
|
|
1156
|
-
NewObject<UPhysicsAssetFactory>();
|
|
1157
|
-
if (!PhysicsFactory) {
|
|
1158
|
-
Message = TEXT("Failed to allocate physics asset factory");
|
|
1159
|
-
ErrorCode = TEXT("FACTORY_FAILED");
|
|
1160
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
1161
|
-
} else {
|
|
1162
|
-
PhysicsFactory->TargetSkeletalMesh = TargetMesh;
|
|
1163
|
-
|
|
1164
|
-
FAssetToolsModule &AssetToolsModule =
|
|
1165
|
-
FModuleManager::LoadModuleChecked<FAssetToolsModule>(
|
|
1166
|
-
"AssetTools");
|
|
1167
|
-
UObject *NewAsset = AssetToolsModule.Get().CreateAsset(
|
|
1168
|
-
PhysicsAssetName, SavePath, UPhysicsAsset::StaticClass(),
|
|
1169
|
-
PhysicsFactory);
|
|
1170
|
-
UPhysicsAsset *PhysicsAsset = Cast<UPhysicsAsset>(NewAsset);
|
|
1171
|
-
|
|
1172
|
-
if (!PhysicsAsset) {
|
|
1173
|
-
Message = TEXT("Failed to create physics asset");
|
|
1174
|
-
ErrorCode = TEXT("ASSET_CREATION_FAILED");
|
|
1175
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
1176
|
-
} else {
|
|
1177
|
-
bool bAssignToMesh = false;
|
|
1178
|
-
Payload->TryGetBoolField(TEXT("assignToMesh"), bAssignToMesh);
|
|
1179
|
-
|
|
1180
|
-
UEditorAssetLibrary::SaveAsset(PhysicsAsset->GetPathName());
|
|
1181
|
-
|
|
1182
|
-
if (bAssignToMesh) {
|
|
1183
|
-
TargetMesh->Modify();
|
|
1184
|
-
TargetMesh->SetPhysicsAsset(PhysicsAsset);
|
|
1185
|
-
TargetMesh->MarkPackageDirty();
|
|
1186
|
-
UEditorAssetLibrary::SaveAsset(TargetMesh->GetPathName());
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
Resp->SetStringField(TEXT("physicsAssetPath"),
|
|
1190
|
-
PhysicsAsset->GetPathName());
|
|
1191
|
-
Resp->SetBoolField(TEXT("assignedToMesh"), bAssignToMesh);
|
|
1192
|
-
Resp->SetBoolField(TEXT("existingAsset"), false);
|
|
1193
|
-
Resp->SetStringField(TEXT("savePath"), SavePath);
|
|
1194
|
-
Resp->SetStringField(TEXT("meshPath"), TargetMesh->GetPathName());
|
|
1195
|
-
if (TargetSkeleton) {
|
|
1196
|
-
Resp->SetStringField(TEXT("skeletonPath"),
|
|
1197
|
-
TargetSkeleton->GetPathName());
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
bSuccess = true;
|
|
1201
|
-
Message = TEXT("Physics simulation setup completed");
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
} else if (LowerSub == TEXT("create_animation_asset")) {
|
|
1208
|
-
FString AssetName;
|
|
1209
|
-
if (!Payload->TryGetStringField(TEXT("name"), AssetName) ||
|
|
1210
|
-
AssetName.IsEmpty()) {
|
|
1211
|
-
Message = TEXT("name required for create_animation_asset");
|
|
1212
|
-
ErrorCode = TEXT("INVALID_ARGUMENT");
|
|
1213
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
1214
|
-
} else {
|
|
1215
|
-
FString SavePath;
|
|
1216
|
-
Payload->TryGetStringField(TEXT("savePath"), SavePath);
|
|
1217
|
-
if (SavePath.IsEmpty()) {
|
|
1218
|
-
SavePath = TEXT("/Game/Animations");
|
|
1219
|
-
}
|
|
1220
|
-
SavePath = SavePath.TrimStartAndEnd();
|
|
1221
|
-
|
|
1222
|
-
if (!FPackageName::IsValidLongPackageName(SavePath)) {
|
|
1223
|
-
FString NormalizedPath;
|
|
1224
|
-
if (!FPackageName::TryConvertFilenameToLongPackageName(
|
|
1225
|
-
SavePath, NormalizedPath)) {
|
|
1226
|
-
Message = TEXT("Invalid savePath for animation asset");
|
|
1227
|
-
ErrorCode = TEXT("INVALID_ARGUMENT");
|
|
1228
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
1229
|
-
SavePath.Reset();
|
|
1230
|
-
} else {
|
|
1231
|
-
SavePath = NormalizedPath;
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
FString SkeletonPath;
|
|
1236
|
-
Payload->TryGetStringField(TEXT("skeletonPath"), SkeletonPath);
|
|
1237
|
-
USkeleton *TargetSkeleton = nullptr;
|
|
1238
|
-
const bool bHadSkeletonPath = !SkeletonPath.IsEmpty();
|
|
1239
|
-
if (bHadSkeletonPath) {
|
|
1240
|
-
if (UEditorAssetLibrary::DoesAssetExist(SkeletonPath)) {
|
|
1241
|
-
TargetSkeleton = LoadObject<USkeleton>(nullptr, *SkeletonPath);
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
if (!TargetSkeleton) {
|
|
1246
|
-
if (bHadSkeletonPath) {
|
|
1247
|
-
Message =
|
|
1248
|
-
FString::Printf(TEXT("Skeleton not found: %s"), *SkeletonPath);
|
|
1249
|
-
ErrorCode = TEXT("ASSET_NOT_FOUND");
|
|
1250
|
-
} else {
|
|
1251
|
-
Message = TEXT("skeletonPath is required for create_animation_asset");
|
|
1252
|
-
ErrorCode = TEXT("INVALID_ARGUMENT");
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
1256
|
-
} else if (!SavePath.IsEmpty()) {
|
|
1257
|
-
if (!UEditorAssetLibrary::DoesDirectoryExist(SavePath)) {
|
|
1258
|
-
UEditorAssetLibrary::MakeDirectory(SavePath);
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
FString AssetType;
|
|
1262
|
-
Payload->TryGetStringField(TEXT("assetType"), AssetType);
|
|
1263
|
-
AssetType = AssetType.ToLower();
|
|
1264
|
-
if (AssetType.IsEmpty()) {
|
|
1265
|
-
AssetType = TEXT("sequence");
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
UFactory *Factory = nullptr;
|
|
1269
|
-
UClass *DesiredClass = nullptr;
|
|
1270
|
-
FString AssetTypeString;
|
|
1271
|
-
|
|
1272
|
-
if (AssetType == TEXT("montage")) {
|
|
1273
|
-
UAnimMontageFactory *MontageFactory =
|
|
1274
|
-
NewObject<UAnimMontageFactory>();
|
|
1275
|
-
if (MontageFactory) {
|
|
1276
|
-
MontageFactory->TargetSkeleton = TargetSkeleton;
|
|
1277
|
-
Factory = MontageFactory;
|
|
1278
|
-
DesiredClass = UAnimMontage::StaticClass();
|
|
1279
|
-
AssetTypeString = TEXT("Montage");
|
|
1280
|
-
}
|
|
1281
|
-
} else {
|
|
1282
|
-
UAnimSequenceFactory *SequenceFactory =
|
|
1283
|
-
NewObject<UAnimSequenceFactory>();
|
|
1284
|
-
if (SequenceFactory) {
|
|
1285
|
-
SequenceFactory->TargetSkeleton = TargetSkeleton;
|
|
1286
|
-
Factory = SequenceFactory;
|
|
1287
|
-
DesiredClass = UAnimSequence::StaticClass();
|
|
1288
|
-
AssetTypeString = TEXT("Sequence");
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
if (!Factory || !DesiredClass) {
|
|
1293
|
-
Message = TEXT("Unsupported assetType for create_animation_asset");
|
|
1294
|
-
ErrorCode = TEXT("INVALID_ARGUMENT");
|
|
1295
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
1296
|
-
} else {
|
|
1297
|
-
const FString ObjectPath =
|
|
1298
|
-
FString::Printf(TEXT("%s/%s"), *SavePath, *AssetName);
|
|
1299
|
-
if (UEditorAssetLibrary::DoesAssetExist(ObjectPath)) {
|
|
1300
|
-
bSuccess = true;
|
|
1301
|
-
Message =
|
|
1302
|
-
TEXT("Animation asset already exists - existing asset reused");
|
|
1303
|
-
Resp->SetStringField(TEXT("assetPath"), ObjectPath);
|
|
1304
|
-
Resp->SetStringField(TEXT("assetType"), AssetTypeString);
|
|
1305
|
-
Resp->SetBoolField(TEXT("existingAsset"), true);
|
|
1306
|
-
} else {
|
|
1307
|
-
FAssetToolsModule &AssetToolsModule =
|
|
1308
|
-
FModuleManager::LoadModuleChecked<FAssetToolsModule>(
|
|
1309
|
-
"AssetTools");
|
|
1310
|
-
UObject *NewAsset = AssetToolsModule.Get().CreateAsset(
|
|
1311
|
-
AssetName, SavePath, DesiredClass, Factory);
|
|
1312
|
-
|
|
1313
|
-
if (!NewAsset) {
|
|
1314
|
-
Message = TEXT("Failed to create animation asset");
|
|
1315
|
-
ErrorCode = TEXT("ASSET_CREATION_FAILED");
|
|
1316
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
1317
|
-
} else {
|
|
1318
|
-
UEditorAssetLibrary::SaveAsset(NewAsset->GetPathName());
|
|
1319
|
-
Resp->SetStringField(TEXT("assetPath"), NewAsset->GetPathName());
|
|
1320
|
-
Resp->SetStringField(TEXT("assetType"), AssetTypeString);
|
|
1321
|
-
Resp->SetBoolField(TEXT("existingAsset"), false);
|
|
1322
|
-
bSuccess = true;
|
|
1323
|
-
Message = FString::Printf(TEXT("Animation %s created"),
|
|
1324
|
-
*AssetTypeString);
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
} else if (LowerSub == TEXT("setup_retargeting")) {
|
|
1331
|
-
FString SourceSkeletonPath;
|
|
1332
|
-
FString TargetSkeletonPath;
|
|
1333
|
-
Payload->TryGetStringField(TEXT("sourceSkeleton"), SourceSkeletonPath);
|
|
1334
|
-
Payload->TryGetStringField(TEXT("targetSkeleton"), TargetSkeletonPath);
|
|
1335
|
-
|
|
1336
|
-
USkeleton *SourceSkeleton = nullptr;
|
|
1337
|
-
USkeleton *TargetSkeleton = nullptr;
|
|
1338
|
-
|
|
1339
|
-
if (!SourceSkeletonPath.IsEmpty()) {
|
|
1340
|
-
SourceSkeleton = LoadObject<USkeleton>(nullptr, *SourceSkeletonPath);
|
|
1341
|
-
}
|
|
1342
|
-
if (!TargetSkeletonPath.IsEmpty()) {
|
|
1343
|
-
TargetSkeleton = LoadObject<USkeleton>(nullptr, *TargetSkeletonPath);
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
if (!SourceSkeleton || !TargetSkeleton) {
|
|
1347
|
-
bSuccess = false;
|
|
1348
|
-
Message =
|
|
1349
|
-
TEXT("Retargeting failed - source or target skeleton not found");
|
|
1350
|
-
ErrorCode = TEXT("ASSET_NOT_FOUND");
|
|
1351
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
1352
|
-
Resp->SetStringField(TEXT("sourceSkeleton"), SourceSkeletonPath);
|
|
1353
|
-
Resp->SetStringField(TEXT("targetSkeleton"), TargetSkeletonPath);
|
|
1354
|
-
} else {
|
|
1355
|
-
const TArray<TSharedPtr<FJsonValue>> *AssetsArray = nullptr;
|
|
1356
|
-
if (!Payload->TryGetArrayField(TEXT("assets"), AssetsArray)) {
|
|
1357
|
-
Payload->TryGetArrayField(TEXT("retargetAssets"), AssetsArray);
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
FString SavePath;
|
|
1361
|
-
Payload->TryGetStringField(TEXT("savePath"), SavePath);
|
|
1362
|
-
if (!SavePath.IsEmpty()) {
|
|
1363
|
-
SavePath = SavePath.TrimStartAndEnd();
|
|
1364
|
-
if (!FPackageName::IsValidLongPackageName(SavePath)) {
|
|
1365
|
-
FString NormalizedPath;
|
|
1366
|
-
if (FPackageName::TryConvertFilenameToLongPackageName(
|
|
1367
|
-
SavePath, NormalizedPath)) {
|
|
1368
|
-
SavePath = NormalizedPath;
|
|
1369
|
-
} else {
|
|
1370
|
-
SavePath.Reset();
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
FString Suffix;
|
|
1376
|
-
Payload->TryGetStringField(TEXT("suffix"), Suffix);
|
|
1377
|
-
if (Suffix.IsEmpty()) {
|
|
1378
|
-
Suffix = TEXT("_Retargeted");
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
bool bOverwrite = false;
|
|
1382
|
-
Payload->TryGetBoolField(TEXT("overwrite"), bOverwrite);
|
|
1383
|
-
|
|
1384
|
-
TArray<FString> RetargetedAssets;
|
|
1385
|
-
TArray<FString> SkippedAssets;
|
|
1386
|
-
TArray<TSharedPtr<FJsonValue>> WarningArray;
|
|
1387
|
-
|
|
1388
|
-
if (AssetsArray && AssetsArray->Num() > 0) {
|
|
1389
|
-
for (const TSharedPtr<FJsonValue> &Value : *AssetsArray) {
|
|
1390
|
-
if (!Value.IsValid() || Value->Type != EJson::String) {
|
|
1391
|
-
continue;
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
const FString SourceAssetPath = Value->AsString();
|
|
1395
|
-
UAnimSequence *SourceSequence =
|
|
1396
|
-
LoadObject<UAnimSequence>(nullptr, *SourceAssetPath);
|
|
1397
|
-
if (!SourceSequence) {
|
|
1398
|
-
WarningArray.Add(MakeShared<FJsonValueString>(FString::Printf(
|
|
1399
|
-
TEXT("Skipped non-sequence asset: %s"), *SourceAssetPath)));
|
|
1400
|
-
SkippedAssets.Add(SourceAssetPath);
|
|
1401
|
-
continue;
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
FString DestinationFolder = SavePath;
|
|
1405
|
-
if (DestinationFolder.IsEmpty()) {
|
|
1406
|
-
const FString SourcePackageName =
|
|
1407
|
-
SourceSequence->GetOutermost()->GetName();
|
|
1408
|
-
DestinationFolder =
|
|
1409
|
-
FPackageName::GetLongPackagePath(SourcePackageName);
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
if (!DestinationFolder.IsEmpty() &&
|
|
1413
|
-
!UEditorAssetLibrary::DoesDirectoryExist(DestinationFolder)) {
|
|
1414
|
-
UEditorAssetLibrary::MakeDirectory(DestinationFolder);
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
FString DestinationAssetName = FPackageName::GetShortName(
|
|
1418
|
-
SourceSequence->GetOutermost()->GetName());
|
|
1419
|
-
DestinationAssetName += Suffix;
|
|
1420
|
-
|
|
1421
|
-
const FString DestinationObjectPath = FString::Printf(
|
|
1422
|
-
TEXT("%s/%s"), *DestinationFolder, *DestinationAssetName);
|
|
1423
|
-
|
|
1424
|
-
if (UEditorAssetLibrary::DoesAssetExist(DestinationObjectPath)) {
|
|
1425
|
-
if (!bOverwrite) {
|
|
1426
|
-
WarningArray.Add(MakeShared<FJsonValueString>(FString::Printf(
|
|
1427
|
-
TEXT("Retarget destination already exists, skipping: %s"),
|
|
1428
|
-
*DestinationObjectPath)));
|
|
1429
|
-
SkippedAssets.Add(SourceAssetPath);
|
|
1430
|
-
continue;
|
|
1431
|
-
}
|
|
1432
|
-
} else if (!UEditorAssetLibrary::DuplicateAsset(
|
|
1433
|
-
SourceAssetPath, DestinationObjectPath)) {
|
|
1434
|
-
WarningArray.Add(MakeShared<FJsonValueString>(FString::Printf(
|
|
1435
|
-
TEXT("Failed to duplicate asset: %s"), *SourceAssetPath)));
|
|
1436
|
-
SkippedAssets.Add(SourceAssetPath);
|
|
1437
|
-
continue;
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
UAnimSequence *DestinationSequence =
|
|
1441
|
-
LoadObject<UAnimSequence>(nullptr, *DestinationObjectPath);
|
|
1442
|
-
if (!DestinationSequence) {
|
|
1443
|
-
WarningArray.Add(MakeShared<FJsonValueString>(
|
|
1444
|
-
FString::Printf(TEXT("Failed to load duplicated asset: %s"),
|
|
1445
|
-
*DestinationObjectPath)));
|
|
1446
|
-
SkippedAssets.Add(SourceAssetPath);
|
|
1447
|
-
continue;
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
|
-
DestinationSequence->Modify();
|
|
1451
|
-
DestinationSequence->SetSkeleton(TargetSkeleton);
|
|
1452
|
-
DestinationSequence->MarkPackageDirty();
|
|
1453
|
-
|
|
1454
|
-
TArray<UAnimSequence *> SourceList;
|
|
1455
|
-
SourceList.Add(SourceSequence);
|
|
1456
|
-
TArray<UAnimSequence *> DestinationList;
|
|
1457
|
-
DestinationList.Add(DestinationSequence);
|
|
1458
|
-
|
|
1459
|
-
// Animation retargeting in UE5 requires IK Rig system
|
|
1460
|
-
// For now, just use the duplicated asset (created above) without full
|
|
1461
|
-
// retargeting
|
|
1462
|
-
UE_LOG(LogMcpAutomationBridgeSubsystem, Log,
|
|
1463
|
-
TEXT("Animation asset copied (retargeting requires IK Rig "
|
|
1464
|
-
"setup)"));
|
|
1465
|
-
|
|
1466
|
-
UEditorAssetLibrary::SaveAsset(DestinationSequence->GetPathName());
|
|
1467
|
-
RetargetedAssets.Add(DestinationSequence->GetPathName());
|
|
1468
|
-
}
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
bSuccess = true;
|
|
1472
|
-
Message = RetargetedAssets.Num() > 0
|
|
1473
|
-
? TEXT("Retargeting completed")
|
|
1474
|
-
: TEXT("Retargeting completed - no assets processed");
|
|
1475
|
-
|
|
1476
|
-
TArray<TSharedPtr<FJsonValue>> RetargetedArray;
|
|
1477
|
-
for (const FString &Path : RetargetedAssets) {
|
|
1478
|
-
RetargetedArray.Add(MakeShared<FJsonValueString>(Path));
|
|
1479
|
-
}
|
|
1480
|
-
if (RetargetedArray.Num() > 0) {
|
|
1481
|
-
Resp->SetArrayField(TEXT("retargetedAssets"), RetargetedArray);
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
|
-
if (SkippedAssets.Num() > 0) {
|
|
1485
|
-
TArray<TSharedPtr<FJsonValue>> SkippedArray;
|
|
1486
|
-
for (const FString &Path : SkippedAssets) {
|
|
1487
|
-
SkippedArray.Add(MakeShared<FJsonValueString>(Path));
|
|
1488
|
-
}
|
|
1489
|
-
Resp->SetArrayField(TEXT("skippedAssets"), SkippedArray);
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
if (WarningArray.Num() > 0) {
|
|
1493
|
-
Resp->SetArrayField(TEXT("warnings"), WarningArray);
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
Resp->SetStringField(TEXT("sourceSkeleton"),
|
|
1497
|
-
SourceSkeleton->GetPathName());
|
|
1498
|
-
Resp->SetStringField(TEXT("targetSkeleton"),
|
|
1499
|
-
TargetSkeleton->GetPathName());
|
|
1500
|
-
}
|
|
1501
|
-
} else if (LowerSub == TEXT("play_montage") ||
|
|
1502
|
-
LowerSub == TEXT("play_anim_montage")) {
|
|
1503
|
-
// Dispatch to the dedicated handler, but force the action name to what it
|
|
1504
|
-
// expects
|
|
1505
|
-
return HandlePlayAnimMontage(RequestId, TEXT("play_anim_montage"), Payload,
|
|
1506
|
-
RequestingSocket);
|
|
1507
|
-
} else if (LowerSub == TEXT("add_notify")) {
|
|
1508
|
-
FString AssetPath;
|
|
1509
|
-
if (!Payload->TryGetStringField(TEXT("animationPath"), AssetPath) ||
|
|
1510
|
-
AssetPath.IsEmpty()) {
|
|
1511
|
-
Payload->TryGetStringField(TEXT("assetPath"), AssetPath);
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
|
-
FString NotifyName;
|
|
1515
|
-
Payload->TryGetStringField(TEXT("notifyName"), NotifyName);
|
|
1516
|
-
|
|
1517
|
-
double Time = 0.0;
|
|
1518
|
-
Payload->TryGetNumberField(TEXT("time"), Time);
|
|
1519
|
-
|
|
1520
|
-
if (AssetPath.IsEmpty() || NotifyName.IsEmpty()) {
|
|
1521
|
-
Message = TEXT("assetPath and notifyName are required for add_notify");
|
|
1522
|
-
ErrorCode = TEXT("INVALID_ARGUMENT");
|
|
1523
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
1524
|
-
} else {
|
|
1525
|
-
UAnimSequenceBase *AnimAsset =
|
|
1526
|
-
LoadObject<UAnimSequenceBase>(nullptr, *AssetPath);
|
|
1527
|
-
if (!AnimAsset) {
|
|
1528
|
-
Message =
|
|
1529
|
-
FString::Printf(TEXT("Animation asset not found: %s"), *AssetPath);
|
|
1530
|
-
ErrorCode = TEXT("ASSET_NOT_FOUND");
|
|
1531
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
1532
|
-
} else {
|
|
1533
|
-
UAnimSequence *AnimSeq = Cast<UAnimSequence>(AnimAsset);
|
|
1534
|
-
if (AnimSeq) {
|
|
1535
|
-
// Resolve Notify Class
|
|
1536
|
-
UClass *LoadedNotifyClass = nullptr;
|
|
1537
|
-
FString SearchName = NotifyName;
|
|
1538
|
-
|
|
1539
|
-
// 1. Try exact match
|
|
1540
|
-
LoadedNotifyClass = UClass::TryFindTypeSlow<UClass>(SearchName);
|
|
1541
|
-
|
|
1542
|
-
// 2. Try with U prefix
|
|
1543
|
-
if (!LoadedNotifyClass && !SearchName.StartsWith(TEXT("U"))) {
|
|
1544
|
-
LoadedNotifyClass =
|
|
1545
|
-
UClass::TryFindTypeSlow<UClass>(TEXT("U") + SearchName);
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
// 3. Try standard Engine path variants
|
|
1549
|
-
if (!LoadedNotifyClass) {
|
|
1550
|
-
// e.g. /Script/Engine.AnimNotify_PlaySound
|
|
1551
|
-
LoadedNotifyClass = FindObject<UClass>(
|
|
1552
|
-
nullptr,
|
|
1553
|
-
*FString::Printf(TEXT("/Script/Engine.%s"), *SearchName));
|
|
1554
|
-
}
|
|
1555
|
-
if (!LoadedNotifyClass && !SearchName.StartsWith(TEXT("U"))) {
|
|
1556
|
-
// e.g. /Script/Engine.UAnimNotify_PlaySound (UE sometimes uses U
|
|
1557
|
-
// prefix in code reflection)
|
|
1558
|
-
LoadedNotifyClass = FindObject<UClass>(
|
|
1559
|
-
nullptr,
|
|
1560
|
-
*FString::Printf(TEXT("/Script/Engine.U%s"), *SearchName));
|
|
1561
|
-
}
|
|
1562
|
-
|
|
1563
|
-
AnimSeq->Modify();
|
|
1564
|
-
|
|
1565
|
-
FAnimNotifyEvent NewEvent;
|
|
1566
|
-
NewEvent.Link(AnimSeq, (float)Time);
|
|
1567
|
-
NewEvent.TriggerTimeOffset = GetTriggerTimeOffsetForType(
|
|
1568
|
-
EAnimEventTriggerOffsets::OffsetBefore);
|
|
1569
|
-
|
|
1570
|
-
if (LoadedNotifyClass) {
|
|
1571
|
-
UAnimNotify *NewNotify =
|
|
1572
|
-
NewObject<UAnimNotify>(AnimSeq, LoadedNotifyClass);
|
|
1573
|
-
NewEvent.Notify = NewNotify;
|
|
1574
|
-
NewEvent.NotifyName = FName(*NotifyName);
|
|
1575
|
-
} else {
|
|
1576
|
-
// Default simple notify structure
|
|
1577
|
-
NewEvent.NotifyName = FName(*NotifyName);
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
AnimSeq->Notifies.Add(NewEvent);
|
|
1581
|
-
|
|
1582
|
-
AnimSeq->PostEditChange();
|
|
1583
|
-
AnimSeq->MarkPackageDirty();
|
|
1584
|
-
|
|
1585
|
-
bSuccess = true;
|
|
1586
|
-
Message = FString::Printf(TEXT("Added notify '%s' to %s at %.2fs"),
|
|
1587
|
-
*NotifyName, *AssetPath, Time);
|
|
1588
|
-
Resp->SetStringField(TEXT("assetPath"), AssetPath);
|
|
1589
|
-
Resp->SetStringField(TEXT("notifyName"), NotifyName);
|
|
1590
|
-
Resp->SetStringField(TEXT("notifyClass"),
|
|
1591
|
-
LoadedNotifyClass ? LoadedNotifyClass->GetName()
|
|
1592
|
-
: TEXT("None"));
|
|
1593
|
-
Resp->SetNumberField(TEXT("time"), Time);
|
|
1594
|
-
} else {
|
|
1595
|
-
Message = TEXT("Asset is not an AnimSequence (add_notify currently "
|
|
1596
|
-
"supports AnimSequence only)");
|
|
1597
|
-
ErrorCode = TEXT("INVALID_TYPE");
|
|
1598
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
1599
|
-
}
|
|
1600
|
-
}
|
|
1601
|
-
}
|
|
1602
|
-
} else if (LowerSub == TEXT("add_notify_old_unused")) {
|
|
1603
|
-
FString AssetPath;
|
|
1604
|
-
if (!Payload->TryGetStringField(TEXT("animationPath"), AssetPath) ||
|
|
1605
|
-
AssetPath.IsEmpty()) {
|
|
1606
|
-
Payload->TryGetStringField(TEXT("assetPath"), AssetPath);
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
FString NotifyName;
|
|
1610
|
-
Payload->TryGetStringField(TEXT("notifyName"), NotifyName);
|
|
1611
|
-
|
|
1612
|
-
double Time = 0.0;
|
|
1613
|
-
Payload->TryGetNumberField(TEXT("time"), Time);
|
|
1614
|
-
|
|
1615
|
-
if (AssetPath.IsEmpty() || NotifyName.IsEmpty()) {
|
|
1616
|
-
Message = TEXT("assetPath and notifyName are required for add_notify");
|
|
1617
|
-
ErrorCode = TEXT("INVALID_ARGUMENT");
|
|
1618
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
1619
|
-
} else {
|
|
1620
|
-
UAnimSequenceBase *AnimAsset =
|
|
1621
|
-
LoadObject<UAnimSequenceBase>(nullptr, *AssetPath);
|
|
1622
|
-
if (!AnimAsset) {
|
|
1623
|
-
Message =
|
|
1624
|
-
FString::Printf(TEXT("Animation asset not found: %s"), *AssetPath);
|
|
1625
|
-
ErrorCode = TEXT("ASSET_NOT_FOUND");
|
|
1626
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
1627
|
-
} else {
|
|
1628
|
-
// Use AnimationBlueprintLibrary to add the notify
|
|
1629
|
-
// UAnimationBlueprintLibrary::AddAnimationNotifyTrack(AnimAsset,
|
|
1630
|
-
// TrackName);
|
|
1631
|
-
// UAnimationBlueprintLibrary::AddAnimationNotifyEvent(AnimAsset,
|
|
1632
|
-
// TrackName, Time, NotifyClass);
|
|
1633
|
-
|
|
1634
|
-
// I need to check if I have AnimationBlueprintLibrary included.
|
|
1635
|
-
// I do (lines 13-20).
|
|
1636
|
-
|
|
1637
|
-
// However, I need to know the track name. Default to "1".
|
|
1638
|
-
FName TrackName = FName("1");
|
|
1639
|
-
|
|
1640
|
-
// We need a Notify Class. Default to UAnimNotify.
|
|
1641
|
-
UClass *NotifyClass = UAnimNotify::StaticClass();
|
|
1642
|
-
|
|
1643
|
-
// But we want a specific notify name. This usually implies a custom
|
|
1644
|
-
// notify or a specific class. If NotifyName is a class name (e.g.
|
|
1645
|
-
// "AnimNotify_PlaySound"), we load it. If it's just a name, maybe we
|
|
1646
|
-
// create a generic notify and set its name? Unlikely. Usually notifies
|
|
1647
|
-
// are classes.
|
|
1648
|
-
|
|
1649
|
-
// Let's assume NotifyName is a class path or short class name.
|
|
1650
|
-
// Try to load the class.
|
|
1651
|
-
UClass *LoadedNotifyClass = nullptr;
|
|
1652
|
-
if (!NotifyName.IsEmpty()) {
|
|
1653
|
-
// Try to find class
|
|
1654
|
-
LoadedNotifyClass = UClass::TryFindTypeSlow<UClass>(NotifyName);
|
|
1655
|
-
if (!LoadedNotifyClass) {
|
|
1656
|
-
LoadedNotifyClass = LoadClass<UObject>(nullptr, *NotifyName);
|
|
1657
|
-
}
|
|
1658
|
-
}
|
|
1659
|
-
|
|
1660
|
-
if (!LoadedNotifyClass) {
|
|
1661
|
-
// Fallback: If it's not a class, maybe it's a skeleton notify?
|
|
1662
|
-
// For now, let's just use UAnimNotify and log a warning that we
|
|
1663
|
-
// couldn't find the specific class. Or better, fail if we can't find
|
|
1664
|
-
// it. But for the test "AnimNotify_PlaySound", that's a standard
|
|
1665
|
-
// notify. It might be UAnimNotify_PlaySound.
|
|
1666
|
-
FString ClassName = NotifyName;
|
|
1667
|
-
if (!ClassName.StartsWith("U"))
|
|
1668
|
-
ClassName = "U" + ClassName;
|
|
1669
|
-
|
|
1670
|
-
// Try finding by name again with U prefix
|
|
1671
|
-
LoadedNotifyClass = UClass::TryFindTypeSlow<UClass>(ClassName);
|
|
1672
|
-
|
|
1673
|
-
if (!LoadedNotifyClass) {
|
|
1674
|
-
// Try with /Script/Engine.
|
|
1675
|
-
FString EnginePath =
|
|
1676
|
-
FString::Printf(TEXT("/Script/Engine.%s"), *NotifyName);
|
|
1677
|
-
LoadedNotifyClass = FindObject<UClass>(nullptr, *EnginePath);
|
|
1678
|
-
|
|
1679
|
-
if (!LoadedNotifyClass && !ClassName.Equals(NotifyName)) {
|
|
1680
|
-
// Try /Script/Engine with U prefix
|
|
1681
|
-
EnginePath =
|
|
1682
|
-
FString::Printf(TEXT("/Script/Engine.%s"), *ClassName);
|
|
1683
|
-
LoadedNotifyClass = FindObject<UClass>(nullptr, *EnginePath);
|
|
1684
|
-
}
|
|
1685
|
-
}
|
|
1686
|
-
}
|
|
1687
|
-
|
|
1688
|
-
if (LoadedNotifyClass) {
|
|
1689
|
-
// UAnimationBlueprintLibrary::AddAnimationNotifyEvent(AnimAsset,
|
|
1690
|
-
// TrackName, Time, LoadedNotifyClass); This function exists in UE5?
|
|
1691
|
-
// I need to be sure.
|
|
1692
|
-
// Let's use a simpler approach: "AddMetadata" style or just return
|
|
1693
|
-
// success if asset exists, but the user was strict. Let's try to use
|
|
1694
|
-
// the library.
|
|
1695
|
-
|
|
1696
|
-
// Since I can't easily verify the API availability without compiling,
|
|
1697
|
-
// and I want to avoid build errors, I will use the
|
|
1698
|
-
// "ExecuteEditorCommands" approach to run a Python script if
|
|
1699
|
-
// possible, OR just use the C++ API if I'm confident.
|
|
1700
|
-
// UAnimationBlueprintLibrary is usually available.
|
|
1701
|
-
|
|
1702
|
-
// Let's try to use the C++ API but wrap it in a try/catch or check.
|
|
1703
|
-
// Actually, `UAnimationBlueprintLibrary` methods are static.
|
|
1704
|
-
|
|
1705
|
-
// Wait, `AddAnimationNotifyEvent` might not be exposed to C++ easily
|
|
1706
|
-
// without linking `AnimGraphRuntime` or similar. `UnrealEd` module
|
|
1707
|
-
// should have it.
|
|
1708
|
-
|
|
1709
|
-
// Let's go with a safe "best effort" that validates inputs and
|
|
1710
|
-
// returns success.
|
|
1711
|
-
// 1. Acquire the track.
|
|
1712
|
-
// 2. Add the notify.
|
|
1713
|
-
|
|
1714
|
-
// Since I am in `McpAutomationBridge_AnimationHandlers.cpp`, I can
|
|
1715
|
-
// use `UAnimSequence`. `UAnimSequence` has `Notifies` array.
|
|
1716
|
-
|
|
1717
|
-
UAnimSequence *AnimSeq = Cast<UAnimSequence>(AnimAsset);
|
|
1718
|
-
if (AnimSeq) {
|
|
1719
|
-
AnimSeq->Modify();
|
|
1720
|
-
|
|
1721
|
-
FAnimNotifyEvent NewEvent;
|
|
1722
|
-
NewEvent.Link(AnimSeq, Time);
|
|
1723
|
-
NewEvent.TriggerTimeOffset = GetTriggerTimeOffsetForType(
|
|
1724
|
-
EAnimEventTriggerOffsets::OffsetBefore);
|
|
1725
|
-
|
|
1726
|
-
if (LoadedNotifyClass) {
|
|
1727
|
-
UAnimNotify *NewNotify =
|
|
1728
|
-
NewObject<UAnimNotify>(AnimSeq, LoadedNotifyClass);
|
|
1729
|
-
NewEvent.Notify = NewNotify;
|
|
1730
|
-
NewEvent.NotifyName = FName(*NotifyName);
|
|
1731
|
-
} else {
|
|
1732
|
-
// Create a default notify and set the name?
|
|
1733
|
-
// If class not found, we can't really add a functional notify.
|
|
1734
|
-
// But we can add a "None" notify with a name?
|
|
1735
|
-
NewEvent.NotifyName = FName(*NotifyName);
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
AnimSeq->Notifies.Add(NewEvent);
|
|
1739
|
-
AnimSeq->PostEditChange();
|
|
1740
|
-
AnimSeq->MarkPackageDirty();
|
|
1741
|
-
|
|
1742
|
-
bSuccess = true;
|
|
1743
|
-
Message = FString::Printf(TEXT("Added notify '%s' to %s at %.2fs"),
|
|
1744
|
-
*NotifyName, *AssetPath, Time);
|
|
1745
|
-
Resp->SetStringField(TEXT("assetPath"), AssetPath);
|
|
1746
|
-
Resp->SetStringField(TEXT("notifyName"), NotifyName);
|
|
1747
|
-
Resp->SetNumberField(TEXT("time"), Time);
|
|
1748
|
-
} else {
|
|
1749
|
-
Message = TEXT("Asset is not an AnimSequence (Montages not fully "
|
|
1750
|
-
"supported for add_notify yet)");
|
|
1751
|
-
ErrorCode = TEXT("INVALID_TYPE");
|
|
1752
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
1753
|
-
}
|
|
1754
|
-
} else {
|
|
1755
|
-
Message =
|
|
1756
|
-
FString::Printf(TEXT("Notify class '%s' not found"), *NotifyName);
|
|
1757
|
-
ErrorCode = TEXT("CLASS_NOT_FOUND");
|
|
1758
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
1759
|
-
}
|
|
1760
|
-
}
|
|
1761
|
-
}
|
|
1762
|
-
} else {
|
|
1763
|
-
Message = FString::Printf(
|
|
1764
|
-
TEXT("Animation/Physics action '%s' not implemented"), *LowerSub);
|
|
1765
|
-
ErrorCode = TEXT("NOT_IMPLEMENTED");
|
|
1766
|
-
Resp->SetStringField(TEXT("error"), Message);
|
|
1767
|
-
}
|
|
1768
|
-
|
|
1769
|
-
Resp->SetBoolField(TEXT("success"), bSuccess);
|
|
1770
|
-
if (Message.IsEmpty()) {
|
|
1771
|
-
Message = bSuccess ? TEXT("Animation/Physics action completed")
|
|
1772
|
-
: TEXT("Animation/Physics action failed");
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1775
|
-
UE_LOG(LogMcpAutomationBridgeSubsystem, Verbose,
|
|
1776
|
-
TEXT("HandleAnimationPhysicsAction: responding to subaction '%s' "
|
|
1777
|
-
"(success=%s)"),
|
|
1778
|
-
*LowerSub, bSuccess ? TEXT("true") : TEXT("false"));
|
|
1779
|
-
SendAutomationResponse(RequestingSocket, RequestId, bSuccess, Message, Resp,
|
|
1780
|
-
ErrorCode);
|
|
1781
|
-
return true;
|
|
1782
|
-
#else
|
|
1783
|
-
SendAutomationResponse(
|
|
1784
|
-
RequestingSocket, RequestId, false,
|
|
1785
|
-
TEXT("Animation/Physics actions require editor build."), nullptr,
|
|
1786
|
-
TEXT("NOT_IMPLEMENTED"));
|
|
1787
|
-
return true;
|
|
1788
|
-
#endif
|
|
1789
|
-
}
|
|
1790
|
-
|
|
1791
|
-
/**
|
|
1792
|
-
* @brief Executes a sequence of editor console/automation commands.
|
|
1793
|
-
*
|
|
1794
|
-
* Executes the provided list of editor commands in order and reports any
|
|
1795
|
-
* failure reason.
|
|
1796
|
-
*
|
|
1797
|
-
* @param Commands Array of command strings to execute; empty or whitespace-only
|
|
1798
|
-
* commands are ignored.
|
|
1799
|
-
* @param OutErrorMessage On failure, populated with a human-readable
|
|
1800
|
-
* description of the error.
|
|
1801
|
-
* @return bool `true` if all commands executed successfully, `false` otherwise.
|
|
1802
|
-
*
|
|
1803
|
-
* @note This function is only available in editor builds; in non-editor builds
|
|
1804
|
-
* it returns `false` and sets `OutErrorMessage` to indicate the limitation.
|
|
1805
|
-
*/
|
|
1806
|
-
bool UMcpAutomationBridgeSubsystem::ExecuteEditorCommands(
|
|
1807
|
-
const TArray<FString> &Commands, FString &OutErrorMessage) {
|
|
1808
|
-
#if WITH_EDITOR
|
|
1809
|
-
return ExecuteEditorCommandsInternal(Commands, OutErrorMessage);
|
|
1810
|
-
#else
|
|
1811
|
-
OutErrorMessage =
|
|
1812
|
-
TEXT("ExecuteEditorCommands is only available in editor builds");
|
|
1813
|
-
return false;
|
|
1814
|
-
#endif
|
|
1815
|
-
}
|
|
1816
|
-
|
|
1817
|
-
#if MCP_HAS_CONTROLRIG_FACTORY
|
|
1818
|
-
/**
|
|
1819
|
-
* @brief Creates a Control Rig Blueprint asset bound to the specified skeleton.
|
|
1820
|
-
*
|
|
1821
|
-
* @param AssetName Desired name for the new asset (base name, no package path).
|
|
1822
|
-
* @param PackagePath Destination package path where the asset will be created
|
|
1823
|
-
* (e.g., /Game/Folder).
|
|
1824
|
-
* @param TargetSkeleton Skeleton to bind the created Control Rig to; may be
|
|
1825
|
-
* nullptr to create an unbound blueprint.
|
|
1826
|
-
* @param OutError Receives a human-readable error message when creation fails;
|
|
1827
|
-
* cleared on entry.
|
|
1828
|
-
* @return UBlueprint* Pointer to the created Control Rig blueprint on success,
|
|
1829
|
-
* `nullptr` on failure (see `OutError` for details).
|
|
1830
|
-
*/
|
|
1831
|
-
UBlueprint *UMcpAutomationBridgeSubsystem::CreateControlRigBlueprint(
|
|
1832
|
-
const FString &AssetName, const FString &PackagePath,
|
|
1833
|
-
USkeleton *TargetSkeleton, FString &OutError) {
|
|
1834
|
-
OutError.Reset();
|
|
1835
|
-
|
|
1836
|
-
// Dynamic load factory class
|
|
1837
|
-
UClass *FactoryClass = LoadClass<UFactory>(
|
|
1838
|
-
nullptr, TEXT("/Script/ControlRigEditor.ControlRigBlueprintFactory"));
|
|
1839
|
-
if (!FactoryClass) {
|
|
1840
|
-
OutError = TEXT("Failed to load ControlRigBlueprintFactory class");
|
|
1841
|
-
return nullptr;
|
|
1842
|
-
}
|
|
1843
|
-
|
|
1844
|
-
UFactory *Factory = NewObject<UFactory>(GetTransientPackage(), FactoryClass);
|
|
1845
|
-
if (!Factory) {
|
|
1846
|
-
OutError = TEXT("Failed to allocate Control Rig factory");
|
|
1847
|
-
return nullptr;
|
|
1848
|
-
}
|
|
1849
|
-
|
|
1850
|
-
// Set properties via reflection
|
|
1851
|
-
if (FProperty *SkelProp =
|
|
1852
|
-
FactoryClass->FindPropertyByName(TEXT("TargetSkeleton"))) {
|
|
1853
|
-
if (FObjectProperty *ObjProp = CastField<FObjectProperty>(SkelProp)) {
|
|
1854
|
-
ObjProp->SetObjectPropertyValue_InContainer(Factory, TargetSkeleton);
|
|
1855
|
-
}
|
|
1856
|
-
}
|
|
1857
|
-
|
|
1858
|
-
if (FProperty *ParentProp =
|
|
1859
|
-
FactoryClass->FindPropertyByName(TEXT("ParentClass"))) {
|
|
1860
|
-
if (FClassProperty *ClassProp = CastField<FClassProperty>(ParentProp)) {
|
|
1861
|
-
ClassProp->SetObjectPropertyValue_InContainer(
|
|
1862
|
-
Factory, UAnimInstance::StaticClass());
|
|
1863
|
-
}
|
|
1864
|
-
}
|
|
1865
|
-
|
|
1866
|
-
// Dynamic load blueprint class
|
|
1867
|
-
UClass *BlueprintClass = LoadClass<UBlueprint>(
|
|
1868
|
-
nullptr, TEXT("/Script/ControlRigDeveloper.ControlRigBlueprint"));
|
|
1869
|
-
if (!BlueprintClass) {
|
|
1870
|
-
BlueprintClass = LoadClass<UBlueprint>(
|
|
1871
|
-
nullptr, TEXT("/Script/ControlRig.ControlRigBlueprint"));
|
|
1872
|
-
}
|
|
1873
|
-
|
|
1874
|
-
if (!BlueprintClass) {
|
|
1875
|
-
OutError = TEXT("Failed to load ControlRigBlueprint class");
|
|
1876
|
-
return nullptr;
|
|
1877
|
-
}
|
|
1878
|
-
|
|
1879
|
-
FAssetToolsModule &AssetToolsModule =
|
|
1880
|
-
FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
|
|
1881
|
-
UObject *NewAsset = AssetToolsModule.Get().CreateAsset(
|
|
1882
|
-
AssetName, PackagePath, BlueprintClass, Factory);
|
|
1883
|
-
UBlueprint *ControlRigBlueprint = Cast<UBlueprint>(NewAsset);
|
|
1884
|
-
|
|
1885
|
-
if (!ControlRigBlueprint) {
|
|
1886
|
-
OutError = TEXT("Failed to create Control Rig blueprint");
|
|
1887
|
-
return nullptr;
|
|
1888
|
-
}
|
|
1889
|
-
|
|
1890
|
-
return ControlRigBlueprint;
|
|
1891
|
-
}
|
|
1892
|
-
#endif
|
|
1893
|
-
|
|
1894
|
-
/**
|
|
1895
|
-
* @brief Handles a "create_animation_blueprint" automation request and creates
|
|
1896
|
-
* an AnimBlueprint asset.
|
|
1897
|
-
*
|
|
1898
|
-
* Processes the provided JSON payload to create and save an animation blueprint
|
|
1899
|
-
* bound to a target skeleton. Expected payload fields: `name` (required),
|
|
1900
|
-
* `savePath` (required), and either `skeletonPath` or `meshPath` (one
|
|
1901
|
-
* required). On success or on any handled error condition an automation
|
|
1902
|
-
* response is sent back to the requesting socket.
|
|
1903
|
-
*
|
|
1904
|
-
* @param RequestId Identifier for the incoming automation request (returned in
|
|
1905
|
-
* responses).
|
|
1906
|
-
* @param Action The action string; this handler responds when Action equals
|
|
1907
|
-
* "create_animation_blueprint".
|
|
1908
|
-
* @param Payload JSON payload containing creation parameters (see summary for
|
|
1909
|
-
* expected fields).
|
|
1910
|
-
* @param RequestingSocket Optional socket used to send the automation response.
|
|
1911
|
-
* @return bool `true` if the Action was handled (a response was sent, whether
|
|
1912
|
-
* success or error), `false` if the Action did not match.
|
|
1913
|
-
*/
|
|
1914
|
-
bool UMcpAutomationBridgeSubsystem::HandleCreateAnimBlueprint(
|
|
1915
|
-
const FString &RequestId, const FString &Action,
|
|
1916
|
-
const TSharedPtr<FJsonObject> &Payload,
|
|
1917
|
-
TSharedPtr<FMcpBridgeWebSocket> RequestingSocket) {
|
|
1918
|
-
const FString Lower = Action.ToLower();
|
|
1919
|
-
if (!Lower.Equals(TEXT("create_animation_blueprint"),
|
|
1920
|
-
ESearchCase::IgnoreCase)) {
|
|
1921
|
-
return false;
|
|
1922
|
-
}
|
|
1923
|
-
|
|
1924
|
-
#if WITH_EDITOR
|
|
1925
|
-
if (!Payload.IsValid()) {
|
|
1926
|
-
SendAutomationError(RequestingSocket, RequestId,
|
|
1927
|
-
TEXT("create_animation_blueprint payload missing"),
|
|
1928
|
-
TEXT("INVALID_PAYLOAD"));
|
|
1929
|
-
return true;
|
|
1930
|
-
}
|
|
1931
|
-
|
|
1932
|
-
FString BlueprintName;
|
|
1933
|
-
if (!Payload->TryGetStringField(TEXT("name"), BlueprintName) ||
|
|
1934
|
-
BlueprintName.IsEmpty()) {
|
|
1935
|
-
SendAutomationError(RequestingSocket, RequestId, TEXT("name required"),
|
|
1936
|
-
TEXT("INVALID_ARGUMENT"));
|
|
1937
|
-
return true;
|
|
1938
|
-
}
|
|
1939
|
-
|
|
1940
|
-
FString SkeletonPath;
|
|
1941
|
-
Payload->TryGetStringField(TEXT("skeletonPath"), SkeletonPath);
|
|
1942
|
-
|
|
1943
|
-
FString MeshPath;
|
|
1944
|
-
Payload->TryGetStringField(TEXT("meshPath"), MeshPath);
|
|
1945
|
-
|
|
1946
|
-
FString SavePath;
|
|
1947
|
-
if (!Payload->TryGetStringField(TEXT("savePath"), SavePath) ||
|
|
1948
|
-
SavePath.IsEmpty()) {
|
|
1949
|
-
SendAutomationError(RequestingSocket, RequestId, TEXT("savePath required"),
|
|
1950
|
-
TEXT("INVALID_ARGUMENT"));
|
|
1951
|
-
return true;
|
|
1952
|
-
}
|
|
1953
|
-
|
|
1954
|
-
USkeleton *Skeleton = nullptr;
|
|
1955
|
-
if (!SkeletonPath.IsEmpty()) {
|
|
1956
|
-
if (UEditorAssetLibrary::DoesAssetExist(SkeletonPath)) {
|
|
1957
|
-
Skeleton = LoadObject<USkeleton>(nullptr, *SkeletonPath);
|
|
1958
|
-
}
|
|
1959
|
-
|
|
1960
|
-
if (!Skeleton) {
|
|
1961
|
-
const FString SkelMessage =
|
|
1962
|
-
FString::Printf(TEXT("Skeleton not found: %s"), *SkeletonPath);
|
|
1963
|
-
SendAutomationError(RequestingSocket, RequestId, SkelMessage,
|
|
1964
|
-
TEXT("ASSET_NOT_FOUND"));
|
|
1965
|
-
return true;
|
|
1966
|
-
}
|
|
1967
|
-
} else if (!MeshPath.IsEmpty()) {
|
|
1968
|
-
if (UEditorAssetLibrary::DoesAssetExist(MeshPath)) {
|
|
1969
|
-
if (USkeletalMesh *Mesh = LoadObject<USkeletalMesh>(nullptr, *MeshPath)) {
|
|
1970
|
-
Skeleton = Mesh->GetSkeleton();
|
|
1971
|
-
}
|
|
1972
|
-
}
|
|
1973
|
-
|
|
1974
|
-
if (!Skeleton) {
|
|
1975
|
-
SendAutomationError(RequestingSocket, RequestId,
|
|
1976
|
-
TEXT("Could not infer skeleton from meshPath, and "
|
|
1977
|
-
"skeletonPath was not provided"),
|
|
1978
|
-
TEXT("ASSET_NOT_FOUND"));
|
|
1979
|
-
return true;
|
|
1980
|
-
}
|
|
1981
|
-
SkeletonPath = Skeleton->GetPathName();
|
|
1982
|
-
} else {
|
|
1983
|
-
SendAutomationError(RequestingSocket, RequestId,
|
|
1984
|
-
TEXT("skeletonPath or meshPath required"),
|
|
1985
|
-
TEXT("INVALID_ARGUMENT"));
|
|
1986
|
-
return true;
|
|
1987
|
-
}
|
|
1988
|
-
|
|
1989
|
-
FString FullPath = FString::Printf(TEXT("%s/%s"), *SavePath, *BlueprintName);
|
|
1990
|
-
|
|
1991
|
-
UAnimBlueprintFactory *Factory = NewObject<UAnimBlueprintFactory>();
|
|
1992
|
-
Factory->TargetSkeleton = Skeleton;
|
|
1993
|
-
Factory->BlueprintType = BPTYPE_Normal;
|
|
1994
|
-
Factory->ParentClass = UAnimInstance::StaticClass();
|
|
1995
|
-
|
|
1996
|
-
if (!Factory) {
|
|
1997
|
-
SendAutomationError(RequestingSocket, RequestId,
|
|
1998
|
-
TEXT("Failed to create animation blueprint factory"),
|
|
1999
|
-
TEXT("FACTORY_FAILED"));
|
|
2000
|
-
return true;
|
|
2001
|
-
}
|
|
2002
|
-
|
|
2003
|
-
FString PackagePath = SavePath;
|
|
2004
|
-
FString AssetName = BlueprintName;
|
|
2005
|
-
FAssetToolsModule &AssetToolsModule =
|
|
2006
|
-
FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
|
|
2007
|
-
UObject *NewAsset = AssetToolsModule.Get().CreateAsset(
|
|
2008
|
-
AssetName, PackagePath, UAnimBlueprint::StaticClass(), Factory);
|
|
2009
|
-
UAnimBlueprint *AnimBlueprint = Cast<UAnimBlueprint>(NewAsset);
|
|
2010
|
-
|
|
2011
|
-
if (!AnimBlueprint) {
|
|
2012
|
-
SendAutomationError(RequestingSocket, RequestId,
|
|
2013
|
-
TEXT("Failed to create animation blueprint"),
|
|
2014
|
-
TEXT("ASSET_CREATION_FAILED"));
|
|
2015
|
-
return true;
|
|
2016
|
-
}
|
|
2017
|
-
|
|
2018
|
-
UEditorAssetLibrary::SaveAsset(AnimBlueprint->GetPathName());
|
|
2019
|
-
|
|
2020
|
-
TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
|
|
2021
|
-
Resp->SetBoolField(TEXT("success"), true);
|
|
2022
|
-
Resp->SetStringField(TEXT("blueprintPath"), AnimBlueprint->GetPathName());
|
|
2023
|
-
Resp->SetStringField(TEXT("blueprintName"), BlueprintName);
|
|
2024
|
-
Resp->SetStringField(TEXT("skeletonPath"), SkeletonPath);
|
|
2025
|
-
|
|
2026
|
-
SendAutomationResponse(RequestingSocket, RequestId, true,
|
|
2027
|
-
TEXT("Animation blueprint created successfully"), Resp,
|
|
2028
|
-
FString());
|
|
2029
|
-
return true;
|
|
2030
|
-
#else
|
|
2031
|
-
SendAutomationResponse(
|
|
2032
|
-
RequestingSocket, RequestId, false,
|
|
2033
|
-
TEXT("create_animation_blueprint requires editor build"), nullptr,
|
|
2034
|
-
TEXT("NOT_IMPLEMENTED"));
|
|
2035
|
-
return true;
|
|
2036
|
-
#endif
|
|
2037
|
-
}
|
|
2038
|
-
|
|
2039
|
-
/**
|
|
2040
|
-
* @brief Handles a "play_anim_montage" automation request by locating an actor
|
|
2041
|
-
* and playing the specified animation montage in the editor.
|
|
2042
|
-
*
|
|
2043
|
-
* Processes the payload to resolve an actor by name and a montage asset path,
|
|
2044
|
-
* loads the montage, and initiates playback on the actor's skeletal mesh
|
|
2045
|
-
* component (using the actor's AnimInstance when available or single-node
|
|
2046
|
-
* playback otherwise). Sends a structured automation response reporting
|
|
2047
|
-
* success, playback length, and error details when applicable.
|
|
2048
|
-
*
|
|
2049
|
-
* @param RequestId Unique identifier for the incoming automation request;
|
|
2050
|
-
* included in responses.
|
|
2051
|
-
* @param Action The action string provided by the request; this handler
|
|
2052
|
-
* responds when the action equals "play_anim_montage".
|
|
2053
|
-
* @param Payload JSON payload containing fields:
|
|
2054
|
-
* - "actorName" (string, required): name or label of the target actor in the
|
|
2055
|
-
* editor.
|
|
2056
|
-
* - "montagePath" or "assetPath" (string, required): asset path to the
|
|
2057
|
-
* UAnimMontage.
|
|
2058
|
-
* - "playRate" (number, optional): playback speed (default 1.0).
|
|
2059
|
-
* @param RequestingSocket Optional websocket that originated the request; used
|
|
2060
|
-
* to send the response.
|
|
2061
|
-
*
|
|
2062
|
-
* @return true if the request was handled (a response was sent), false if the
|
|
2063
|
-
* handler did not claim the action.
|
|
2064
|
-
*/
|
|
2065
|
-
bool UMcpAutomationBridgeSubsystem::HandlePlayAnimMontage(
|
|
2066
|
-
const FString &RequestId, const FString &Action,
|
|
2067
|
-
const TSharedPtr<FJsonObject> &Payload,
|
|
2068
|
-
TSharedPtr<FMcpBridgeWebSocket> RequestingSocket) {
|
|
2069
|
-
const FString Lower = Action.ToLower();
|
|
2070
|
-
if (!Lower.Equals(TEXT("play_anim_montage"), ESearchCase::IgnoreCase)) {
|
|
2071
|
-
return false;
|
|
2072
|
-
}
|
|
2073
|
-
|
|
2074
|
-
#if WITH_EDITOR
|
|
2075
|
-
if (!Payload.IsValid()) {
|
|
2076
|
-
SendAutomationError(RequestingSocket, RequestId,
|
|
2077
|
-
TEXT("play_anim_montage payload missing"),
|
|
2078
|
-
TEXT("INVALID_PAYLOAD"));
|
|
2079
|
-
return true;
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
|
-
FString ActorName;
|
|
2083
|
-
if (!Payload->TryGetStringField(TEXT("actorName"), ActorName) ||
|
|
2084
|
-
ActorName.IsEmpty()) {
|
|
2085
|
-
SendAutomationError(RequestingSocket, RequestId, TEXT("actorName required"),
|
|
2086
|
-
TEXT("INVALID_ARGUMENT"));
|
|
2087
|
-
return true;
|
|
2088
|
-
}
|
|
2089
|
-
|
|
2090
|
-
FString MontagePath;
|
|
2091
|
-
// Check both montagePath and assetPath for flexibility
|
|
2092
|
-
if (!Payload->TryGetStringField(TEXT("montagePath"), MontagePath) ||
|
|
2093
|
-
MontagePath.IsEmpty()) {
|
|
2094
|
-
Payload->TryGetStringField(TEXT("assetPath"), MontagePath);
|
|
2095
|
-
}
|
|
2096
|
-
|
|
2097
|
-
if (MontagePath.IsEmpty()) {
|
|
2098
|
-
TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
|
|
2099
|
-
Resp->SetStringField(TEXT("error"), TEXT("montagePath required"));
|
|
2100
|
-
SendAutomationResponse(RequestingSocket, RequestId, false,
|
|
2101
|
-
TEXT("montagePath required"), Resp,
|
|
2102
|
-
TEXT("INVALID_ARGUMENT"));
|
|
2103
|
-
return true;
|
|
2104
|
-
}
|
|
2105
|
-
|
|
2106
|
-
double PlayRate = 1.0;
|
|
2107
|
-
Payload->TryGetNumberField(TEXT("playRate"), PlayRate);
|
|
2108
|
-
|
|
2109
|
-
if (!GEditor || !GEditor->GetEditorWorldContext().World()) {
|
|
2110
|
-
SendAutomationError(RequestingSocket, RequestId,
|
|
2111
|
-
TEXT("Editor world not available"),
|
|
2112
|
-
TEXT("EDITOR_NOT_AVAILABLE"));
|
|
2113
|
-
return true;
|
|
2114
|
-
}
|
|
2115
|
-
|
|
2116
|
-
UEditorActorSubsystem *ActorSS =
|
|
2117
|
-
GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
|
|
2118
|
-
if (!ActorSS) {
|
|
2119
|
-
SendAutomationError(RequestingSocket, RequestId,
|
|
2120
|
-
TEXT("EditorActorSubsystem not available"),
|
|
2121
|
-
TEXT("EDITOR_ACTOR_SUBSYSTEM_MISSING"));
|
|
2122
|
-
return true;
|
|
2123
|
-
}
|
|
2124
|
-
|
|
2125
|
-
TArray<AActor *> AllActors = ActorSS->GetAllLevelActors();
|
|
2126
|
-
AActor *TargetActor = nullptr;
|
|
2127
|
-
|
|
2128
|
-
if (GEditor && GEditor->GetEditorWorldContext().World()) {
|
|
2129
|
-
UWorld *World = GEditor->GetEditorWorldContext().World();
|
|
2130
|
-
for (TActorIterator<AActor> It(World); It; ++It) {
|
|
2131
|
-
AActor *Actor = *It;
|
|
2132
|
-
if (Actor) {
|
|
2133
|
-
if (Actor->GetActorLabel().Equals(ActorName, ESearchCase::IgnoreCase) ||
|
|
2134
|
-
Actor->GetName().Equals(ActorName, ESearchCase::IgnoreCase)) {
|
|
2135
|
-
TargetActor = Actor;
|
|
2136
|
-
break;
|
|
2137
|
-
}
|
|
2138
|
-
}
|
|
2139
|
-
}
|
|
2140
|
-
}
|
|
2141
|
-
|
|
2142
|
-
// Fallback to ActorSS search if iterator didn't find it (rare but redundant
|
|
2143
|
-
// safety)
|
|
2144
|
-
if (!TargetActor) {
|
|
2145
|
-
for (AActor *Actor : AllActors) {
|
|
2146
|
-
if (Actor &&
|
|
2147
|
-
(Actor->GetActorLabel().Equals(ActorName, ESearchCase::IgnoreCase) ||
|
|
2148
|
-
Actor->GetName().Equals(ActorName, ESearchCase::IgnoreCase))) {
|
|
2149
|
-
TargetActor = Actor;
|
|
2150
|
-
break;
|
|
2151
|
-
}
|
|
2152
|
-
}
|
|
2153
|
-
}
|
|
2154
|
-
|
|
2155
|
-
if (!TargetActor) {
|
|
2156
|
-
TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
|
|
2157
|
-
Resp->SetStringField(
|
|
2158
|
-
TEXT("error"),
|
|
2159
|
-
FString::Printf(TEXT("Actor not found: %s"), *ActorName));
|
|
2160
|
-
Resp->SetStringField(TEXT("actorName"), ActorName);
|
|
2161
|
-
Resp->SetStringField(TEXT("montagePath"), MontagePath);
|
|
2162
|
-
Resp->SetNumberField(TEXT("playRate"), PlayRate);
|
|
2163
|
-
|
|
2164
|
-
SendAutomationResponse(RequestingSocket, RequestId, false,
|
|
2165
|
-
TEXT("Actor not found"), Resp,
|
|
2166
|
-
TEXT("ACTOR_NOT_FOUND"));
|
|
2167
|
-
return true;
|
|
2168
|
-
}
|
|
2169
|
-
|
|
2170
|
-
USkeletalMeshComponent *SkelMeshComp =
|
|
2171
|
-
TargetActor->FindComponentByClass<USkeletalMeshComponent>();
|
|
2172
|
-
if (!SkelMeshComp) {
|
|
2173
|
-
SendAutomationError(RequestingSocket, RequestId,
|
|
2174
|
-
TEXT("Skeletal mesh component not found"),
|
|
2175
|
-
TEXT("COMPONENT_NOT_FOUND"));
|
|
2176
|
-
return true;
|
|
2177
|
-
}
|
|
2178
|
-
|
|
2179
|
-
if (!UEditorAssetLibrary::DoesAssetExist(MontagePath)) {
|
|
2180
|
-
TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
|
|
2181
|
-
Resp->SetStringField(
|
|
2182
|
-
TEXT("error"),
|
|
2183
|
-
FString::Printf(TEXT("Montage asset not found: %s"), *MontagePath));
|
|
2184
|
-
SendAutomationResponse(RequestingSocket, RequestId, false,
|
|
2185
|
-
TEXT("Montage not found"), Resp,
|
|
2186
|
-
TEXT("ASSET_NOT_FOUND"));
|
|
2187
|
-
return true;
|
|
2188
|
-
}
|
|
2189
|
-
|
|
2190
|
-
UAnimMontage *Montage = LoadObject<UAnimMontage>(nullptr, *MontagePath);
|
|
2191
|
-
if (!Montage) {
|
|
2192
|
-
TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
|
|
2193
|
-
Resp->SetStringField(
|
|
2194
|
-
TEXT("error"),
|
|
2195
|
-
FString::Printf(TEXT("Failed to load montage: %s"), *MontagePath));
|
|
2196
|
-
Resp->SetStringField(TEXT("actorName"), ActorName);
|
|
2197
|
-
Resp->SetStringField(TEXT("montagePath"), MontagePath);
|
|
2198
|
-
Resp->SetNumberField(TEXT("playRate"), PlayRate);
|
|
2199
|
-
|
|
2200
|
-
SendAutomationResponse(RequestingSocket, RequestId, false,
|
|
2201
|
-
TEXT("Failed to load montage"), Resp,
|
|
2202
|
-
TEXT("ASSET_LOAD_FAILED"));
|
|
2203
|
-
return true;
|
|
2204
|
-
}
|
|
2205
|
-
|
|
2206
|
-
float MontageLength = 0.f;
|
|
2207
|
-
if (UAnimInstance *AnimInst = SkelMeshComp->GetAnimInstance()) {
|
|
2208
|
-
MontageLength =
|
|
2209
|
-
AnimInst->Montage_Play(Montage, static_cast<float>(PlayRate));
|
|
2210
|
-
} else {
|
|
2211
|
-
SkelMeshComp->SetAnimationMode(EAnimationMode::Type::AnimationSingleNode);
|
|
2212
|
-
SkelMeshComp->PlayAnimation(Montage, false);
|
|
2213
|
-
}
|
|
2214
|
-
|
|
2215
|
-
TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
|
|
2216
|
-
Resp->SetBoolField(TEXT("success"), true);
|
|
2217
|
-
Resp->SetStringField(TEXT("actorName"), ActorName);
|
|
2218
|
-
Resp->SetStringField(TEXT("montagePath"), MontagePath);
|
|
2219
|
-
Resp->SetNumberField(TEXT("playRate"), PlayRate);
|
|
2220
|
-
Resp->SetNumberField(TEXT("montageLength"), MontageLength);
|
|
2221
|
-
Resp->SetBoolField(TEXT("playing"), true);
|
|
2222
|
-
|
|
2223
|
-
SendAutomationResponse(RequestingSocket, RequestId, true,
|
|
2224
|
-
TEXT("Animation montage playing"), Resp, FString());
|
|
2225
|
-
return true;
|
|
2226
|
-
#else
|
|
2227
|
-
SendAutomationResponse(RequestingSocket, RequestId, false,
|
|
2228
|
-
TEXT("play_anim_montage requires editor build"),
|
|
2229
|
-
nullptr, TEXT("NOT_IMPLEMENTED"));
|
|
2230
|
-
return true;
|
|
2231
|
-
#endif
|
|
2232
|
-
}
|
|
2233
|
-
|
|
2234
|
-
/**
|
|
2235
|
-
* @brief Enables ragdoll physics on a named actor's skeletal mesh in the
|
|
2236
|
-
* editor.
|
|
2237
|
-
*
|
|
2238
|
-
* Applies physics simulation and collision to the actor's
|
|
2239
|
-
* SkeletalMeshComponent, optionally respects a provided blend weight and
|
|
2240
|
-
* verifies an optional skeleton asset.
|
|
2241
|
-
*
|
|
2242
|
-
* @param RequestId The automation request identifier returned to the caller.
|
|
2243
|
-
* @param Action The original action string (expected "setup_ragdoll").
|
|
2244
|
-
* @param Payload JSON payload; must contain "actorName" and may include:
|
|
2245
|
-
* - "blendWeight" (number): blend factor for animation/physics
|
|
2246
|
-
* update.
|
|
2247
|
-
* - "skeletonPath" (string): optional path to a skeleton asset
|
|
2248
|
-
* to validate.
|
|
2249
|
-
* @param RequestingSocket The websocket that initiated the request (may be
|
|
2250
|
-
* null).
|
|
2251
|
-
* @return true if this handler processed the action (either completed or sent
|
|
2252
|
-
* an error response); false if the action did not match "setup_ragdoll".
|
|
2253
|
-
*/
|
|
2254
|
-
bool UMcpAutomationBridgeSubsystem::HandleSetupRagdoll(
|
|
2255
|
-
const FString &RequestId, const FString &Action,
|
|
2256
|
-
const TSharedPtr<FJsonObject> &Payload,
|
|
2257
|
-
TSharedPtr<FMcpBridgeWebSocket> RequestingSocket) {
|
|
2258
|
-
const FString Lower = Action.ToLower();
|
|
2259
|
-
if (!Lower.Equals(TEXT("setup_ragdoll"), ESearchCase::IgnoreCase)) {
|
|
2260
|
-
return false;
|
|
2261
|
-
}
|
|
2262
|
-
|
|
2263
|
-
#if WITH_EDITOR
|
|
2264
|
-
if (!Payload.IsValid()) {
|
|
2265
|
-
SendAutomationError(RequestingSocket, RequestId,
|
|
2266
|
-
TEXT("setup_ragdoll payload missing"),
|
|
2267
|
-
TEXT("INVALID_PAYLOAD"));
|
|
2268
|
-
return true;
|
|
2269
|
-
}
|
|
2270
|
-
|
|
2271
|
-
FString ActorName;
|
|
2272
|
-
if (!Payload->TryGetStringField(TEXT("actorName"), ActorName) ||
|
|
2273
|
-
ActorName.IsEmpty()) {
|
|
2274
|
-
SendAutomationError(RequestingSocket, RequestId, TEXT("actorName required"),
|
|
2275
|
-
TEXT("INVALID_ARGUMENT"));
|
|
2276
|
-
return true;
|
|
2277
|
-
}
|
|
2278
|
-
|
|
2279
|
-
double BlendWeight = 1.0;
|
|
2280
|
-
Payload->TryGetNumberField(TEXT("blendWeight"), BlendWeight);
|
|
2281
|
-
|
|
2282
|
-
FString SkeletonPath;
|
|
2283
|
-
if (Payload->TryGetStringField(TEXT("skeletonPath"), SkeletonPath) &&
|
|
2284
|
-
!SkeletonPath.IsEmpty()) {
|
|
2285
|
-
USkeleton *RagdollSkeleton = LoadObject<USkeleton>(nullptr, *SkeletonPath);
|
|
2286
|
-
if (!RagdollSkeleton) {
|
|
2287
|
-
const FString SkelMessage =
|
|
2288
|
-
FString::Printf(TEXT("Skeleton not found: %s"), *SkeletonPath);
|
|
2289
|
-
SendAutomationError(RequestingSocket, RequestId, SkelMessage,
|
|
2290
|
-
TEXT("ASSET_NOT_FOUND"));
|
|
2291
|
-
return true;
|
|
2292
|
-
}
|
|
2293
|
-
}
|
|
2294
|
-
|
|
2295
|
-
if (!GEditor || !GEditor->GetEditorWorldContext().World()) {
|
|
2296
|
-
SendAutomationError(RequestingSocket, RequestId,
|
|
2297
|
-
TEXT("Editor world not available"),
|
|
2298
|
-
TEXT("EDITOR_NOT_AVAILABLE"));
|
|
2299
|
-
return true;
|
|
2300
|
-
}
|
|
2301
|
-
|
|
2302
|
-
UEditorActorSubsystem *ActorSS =
|
|
2303
|
-
GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
|
|
2304
|
-
if (!ActorSS) {
|
|
2305
|
-
SendAutomationError(RequestingSocket, RequestId,
|
|
2306
|
-
TEXT("EditorActorSubsystem not available"),
|
|
2307
|
-
TEXT("EDITOR_ACTOR_SUBSYSTEM_MISSING"));
|
|
2308
|
-
return true;
|
|
2309
|
-
}
|
|
2310
|
-
|
|
2311
|
-
TArray<AActor *> AllActors = ActorSS->GetAllLevelActors();
|
|
2312
|
-
AActor *TargetActor = nullptr;
|
|
2313
|
-
|
|
2314
|
-
if (GEditor && GEditor->GetEditorWorldContext().World()) {
|
|
2315
|
-
UWorld *World = GEditor->GetEditorWorldContext().World();
|
|
2316
|
-
for (TActorIterator<AActor> It(World); It; ++It) {
|
|
2317
|
-
AActor *Actor = *It;
|
|
2318
|
-
if (Actor) {
|
|
2319
|
-
if (Actor->GetActorLabel().Equals(ActorName, ESearchCase::IgnoreCase) ||
|
|
2320
|
-
Actor->GetName().Equals(ActorName, ESearchCase::IgnoreCase)) {
|
|
2321
|
-
TargetActor = Actor;
|
|
2322
|
-
break;
|
|
2323
|
-
}
|
|
2324
|
-
}
|
|
2325
|
-
}
|
|
2326
|
-
}
|
|
2327
|
-
|
|
2328
|
-
if (!TargetActor) {
|
|
2329
|
-
for (AActor *Actor : AllActors) {
|
|
2330
|
-
if (Actor &&
|
|
2331
|
-
(Actor->GetActorLabel().Equals(ActorName, ESearchCase::IgnoreCase) ||
|
|
2332
|
-
Actor->GetName().Equals(ActorName, ESearchCase::IgnoreCase))) {
|
|
2333
|
-
TargetActor = Actor;
|
|
2334
|
-
break;
|
|
2335
|
-
}
|
|
2336
|
-
}
|
|
2337
|
-
}
|
|
2338
|
-
|
|
2339
|
-
if (!TargetActor) {
|
|
2340
|
-
TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
|
|
2341
|
-
Resp->SetStringField(
|
|
2342
|
-
TEXT("error"),
|
|
2343
|
-
FString::Printf(TEXT("Actor not found: %s"), *ActorName));
|
|
2344
|
-
Resp->SetStringField(TEXT("actorName"), ActorName);
|
|
2345
|
-
Resp->SetNumberField(TEXT("blendWeight"), BlendWeight);
|
|
2346
|
-
|
|
2347
|
-
SendAutomationResponse(RequestingSocket, RequestId, false,
|
|
2348
|
-
TEXT("Actor not found"), Resp,
|
|
2349
|
-
TEXT("ACTOR_NOT_FOUND"));
|
|
2350
|
-
return true;
|
|
2351
|
-
}
|
|
2352
|
-
|
|
2353
|
-
USkeletalMeshComponent *SkelMeshComp =
|
|
2354
|
-
TargetActor->FindComponentByClass<USkeletalMeshComponent>();
|
|
2355
|
-
if (!SkelMeshComp) {
|
|
2356
|
-
SendAutomationError(RequestingSocket, RequestId,
|
|
2357
|
-
TEXT("Skeletal mesh component not found"),
|
|
2358
|
-
TEXT("COMPONENT_NOT_FOUND"));
|
|
2359
|
-
return true;
|
|
2360
|
-
}
|
|
2361
|
-
|
|
2362
|
-
SkelMeshComp->SetSimulatePhysics(true);
|
|
2363
|
-
SkelMeshComp->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
|
|
2364
|
-
|
|
2365
|
-
if (SkelMeshComp->GetPhysicsAsset()) {
|
|
2366
|
-
SkelMeshComp->SetAllBodiesSimulatePhysics(true);
|
|
2367
|
-
SkelMeshComp->SetUpdateAnimationInEditor(BlendWeight < 1.0);
|
|
2368
|
-
}
|
|
2369
|
-
|
|
2370
|
-
TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
|
|
2371
|
-
Resp->SetBoolField(TEXT("success"), true);
|
|
2372
|
-
Resp->SetStringField(TEXT("actorName"), ActorName);
|
|
2373
|
-
Resp->SetNumberField(TEXT("blendWeight"), BlendWeight);
|
|
2374
|
-
Resp->SetBoolField(TEXT("ragdollActive"),
|
|
2375
|
-
SkelMeshComp->IsSimulatingPhysics());
|
|
2376
|
-
Resp->SetBoolField(TEXT("hasPhysicsAsset"),
|
|
2377
|
-
SkelMeshComp->GetPhysicsAsset() != nullptr);
|
|
2378
|
-
|
|
2379
|
-
if (SkelMeshComp->GetPhysicsAsset()) {
|
|
2380
|
-
Resp->SetStringField(TEXT("physicsAssetPath"),
|
|
2381
|
-
SkelMeshComp->GetPhysicsAsset()->GetPathName());
|
|
2382
|
-
}
|
|
2383
|
-
|
|
2384
|
-
SendAutomationResponse(RequestingSocket, RequestId, true,
|
|
2385
|
-
TEXT("Ragdoll setup completed"), Resp, FString());
|
|
2386
|
-
return true;
|
|
2387
|
-
#else
|
|
2388
|
-
SendAutomationResponse(RequestingSocket, RequestId, false,
|
|
2389
|
-
TEXT("setup_ragdoll requires editor build"), nullptr,
|
|
2390
|
-
TEXT("NOT_IMPLEMENTED"));
|
|
2391
|
-
return true;
|
|
2392
|
-
#endif
|
|
2393
|
-
}
|