ue-mcp 1.2.4 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/action-class.js +56 -3
- package/dist/action-class.js.map +1 -1
- package/dist/bridge-timeouts.d.ts +64 -0
- package/dist/bridge-timeouts.js +118 -0
- package/dist/bridge-timeouts.js.map +1 -0
- package/dist/bridge.js +10 -2
- package/dist/bridge.js.map +1 -1
- package/dist/editor-control.d.ts +72 -5
- package/dist/editor-control.js +164 -140
- package/dist/editor-control.js.map +1 -1
- package/dist/editor-target.d.ts +55 -1
- package/dist/editor-target.js +107 -10
- package/dist/editor-target.js.map +1 -1
- package/dist/engine-observer.d.ts +59 -5
- package/dist/engine-observer.js +249 -29
- package/dist/engine-observer.js.map +1 -1
- package/dist/engine-root.d.ts +91 -0
- package/dist/engine-root.js +448 -0
- package/dist/engine-root.js.map +1 -0
- package/dist/epic-enrich.js +16 -1
- package/dist/epic-enrich.js.map +1 -1
- package/dist/feedback-cli.js +4 -0
- package/dist/feedback-cli.js.map +1 -1
- package/dist/feedback-deferred.d.ts +9 -0
- package/dist/feedback-deferred.js +2 -0
- package/dist/feedback-deferred.js.map +1 -1
- package/dist/feedback-fallback.d.ts +99 -0
- package/dist/feedback-fallback.js +173 -0
- package/dist/feedback-fallback.js.map +1 -0
- package/dist/flow/write-methods.js +20 -0
- package/dist/flow/write-methods.js.map +1 -1
- package/dist/lean-context.js +8 -3
- package/dist/lean-context.js.map +1 -1
- package/dist/locking.js +11 -2
- package/dist/locking.js.map +1 -1
- package/dist/project.d.ts +11 -0
- package/dist/project.js +23 -2
- package/dist/project.js.map +1 -1
- package/dist/python-gate.d.ts +95 -0
- package/dist/python-gate.js +175 -0
- package/dist/python-gate.js.map +1 -0
- package/dist/tool-counts.json +16 -16
- package/dist/tools/animation.js +288 -24
- package/dist/tools/animation.js.map +1 -1
- package/dist/tools/asset.js +79 -14
- package/dist/tools/asset.js.map +1 -1
- package/dist/tools/audio.js +59 -5
- package/dist/tools/audio.js.map +1 -1
- package/dist/tools/blueprint.js +29 -12
- package/dist/tools/blueprint.js.map +1 -1
- package/dist/tools/chooser.js +4 -1
- package/dist/tools/chooser.js.map +1 -1
- package/dist/tools/editor.js +81 -43
- package/dist/tools/editor.js.map +1 -1
- package/dist/tools/feedback.js +214 -48
- package/dist/tools/feedback.js.map +1 -1
- package/dist/tools/foliage.js +27 -1
- package/dist/tools/foliage.js.map +1 -1
- package/dist/tools/gameplay.js +38 -11
- package/dist/tools/gameplay.js.map +1 -1
- package/dist/tools/gas.js +17 -5
- package/dist/tools/gas.js.map +1 -1
- package/dist/tools/landscape.js +22 -7
- package/dist/tools/landscape.js.map +1 -1
- package/dist/tools/level.js +322 -51
- package/dist/tools/level.js.map +1 -1
- package/dist/tools/material.js +10 -4
- package/dist/tools/material.js.map +1 -1
- package/dist/tools/niagara.js +3 -2
- package/dist/tools/niagara.js.map +1 -1
- package/dist/tools/pcg.js +6 -5
- package/dist/tools/pcg.js.map +1 -1
- package/dist/tools/project.js +47 -20
- package/dist/tools/project.js.map +1 -1
- package/dist/types.d.ts +21 -0
- package/dist/types.js +46 -3
- package/dist/types.js.map +1 -1
- package/dist/ue-mcp.default.yml +493 -125
- package/package.json +2 -2
- package/plugin/ue_mcp_bridge/NATIVE_RUNTIME_VISIBILITY.md +53 -0
- package/plugin/ue_mcp_bridge/NATIVE_STATIC_MESH_USAGE_PROGRESS.md +115 -0
- package/plugin/ue_mcp_bridge/README_MassHandlers.md +97 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/BridgeServer.cpp +14 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/GameThreadExecutor.cpp +21 -4
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/GameThreadExecutor.h +9 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/HandlerJsonProperty.h +395 -69
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers.cpp +79 -3
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers.h +25 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_ControlRig.cpp +3 -3
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_ControlRigSequencer.cpp +3061 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_IKRetargeterAuthoring.cpp +961 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_IKRigAuthoring.cpp +900 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Pose.cpp +769 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Sequence.cpp +44 -3
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_SkeletalLive.cpp +217 -11
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_StateMachine.cpp +551 -24
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Validation.cpp +735 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers.cpp +352 -73
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers.h +8 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkProperties.cpp +1 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkRead.cpp +503 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkRead.h +21 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkUpsert.cpp +2 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Enum.cpp +2 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Geometry.cpp +902 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Geometry.h +33 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Import.cpp +478 -134
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Mesh.cpp +5 -5
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_MeshBoolean.cpp +933 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_MeshBoolean.h +35 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Redirectors.cpp +478 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_RenderTarget.cpp +1 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Sockets.cpp +4 -4
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Struct.cpp +2 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Subobject.cpp +319 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AudioHandlers_SoundCue.cpp +3 -3
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers.cpp +482 -35
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers.h +10 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Collision.cpp +502 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Collision.h +43 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Functions.cpp +89 -7
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Graph.cpp +17 -51
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Internal.h +107 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Properties.cpp +10 -10
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Search.cpp +648 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DemoHandlers.cpp +5 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DiffHandlers.cpp +252 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DiffHandlers.h +10 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers.cpp +252 -41
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers.h +4 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_Build.cpp +250 -27
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_PIE.cpp +303 -125
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_PIERuntime.cpp +73 -50
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_RuntimeVisibility.cpp +757 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers.cpp +11 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers.h +3 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers_Batch.cpp +419 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers.cpp +39 -161
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers.h +66 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_BTAuthoring.cpp +1323 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_BehaviorTree.cpp +1211 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.cpp +2 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.h +60 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers_Runtime.cpp +363 -12
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/HandlerEditorState.h +84 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.cpp +1288 -36
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.h +3 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers_Sculpt.cpp +41 -16
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers.cpp +959 -213
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers.h +66 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_BatchWrite.cpp +1571 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Convert.cpp +316 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Inspection.cpp +280 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_InstanceProjection.cpp +666 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_InstanceProjection_Internal.h +63 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Lights.cpp +43 -12
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_MultiLevel.cpp +13 -47
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_PostProcess.cpp +527 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Query.cpp +1169 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Refresh.cpp +723 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Save.cpp +325 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Spatial.cpp +360 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Trace.cpp +178 -58
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Transient.cpp +337 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Volumes.cpp +25 -12
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_WorldPartition.cpp +10 -9
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_WorldPartitionSettings.cpp +686 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MassHandlers.cpp +467 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MassHandlers.h +22 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers.cpp +452 -175
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers.h +5 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers_Build.cpp +597 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/NiagaraHandlers.cpp +17 -15
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PCGHandlers.cpp +38 -168
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PhysicsHandlers.cpp +39 -30
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.cpp +392 -78
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.h +2 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers_Internal.h +83 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SkeletalMeshHandlers.cpp +200 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SkeletalMeshHandlers.h +15 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SplineHandlers.cpp +19 -12
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/StateTreeHandlers.cpp +31 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers.cpp +307 -69
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers.h +84 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers_Extraction.cpp +8 -41
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers_Properties.cpp +33 -52
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/ActorSelectorTests.cpp +344 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AnimationControlRigTimelineTests.cpp +60 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AnimationPoseTests.cpp +115 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetBulkReadTests.cpp +177 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetDeleteGuardTests.cpp +276 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetPathResolutionTests.cpp +243 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetPropertyPersistTests.cpp +213 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetSubobjectTests.cpp +235 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AutomationRunnerTests.cpp +72 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BehaviorTreeAuthoringTests.cpp +526 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BehaviorTreeNodeTests.cpp +334 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BlueprintHandlerSurfaceTests.cpp +362 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BridgeProtocolTests.cpp +14 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/CollisionQueryTests.cpp +288 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/DataTableRowWriteTests.cpp +453 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/EditorFunctionCallTests.cpp +223 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/FoliageBatchTests.cpp +147 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/GameThreadGateTests.cpp +82 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/GasLiveAttributeTests.cpp +183 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LandscapeSampleTests.cpp +119 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelBatchWriteTests.cpp +176 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelComponentTreeInstanceCountTests.cpp +189 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelConvertBrushesTests.cpp +100 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelInstanceProjectionTests.cpp +265 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelPostProcessOverrideTests.cpp +258 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelQueryComponentsTests.cpp +195 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelRefreshTests.cpp +142 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelSaveReportTests.cpp +125 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelScriptHandlersTests.cpp +42 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelStaticMeshUsageTests.cpp +412 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelTraceWorldScopeTests.cpp +133 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelTransientActorTests.cpp +205 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelWorldPartitionSettingsTests.cpp +168 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MCPInstancedStructPathTestTypes.h +51 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MCPInstancedStructPathTests.cpp +379 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MaterialBuildTests.cpp +238 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MeshBooleanTests.cpp +170 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MeshGeometryTests.cpp +116 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/PackageSaveExtensionTests.cpp +161 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/PackageWriteGuardTests.cpp +253 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/RuntimeVisibilityTests.cpp +143 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SequencerHandlerTests.cpp +660 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SequencerScrubTests.cpp +50 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SkeletonDiffTests.cpp +685 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/WidgetBlueprintResolveTests.cpp +145 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/UE_MCP_Bridge.cpp +80 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerFunctionCall.h +509 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerQuery.h +407 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerUtils.h +1008 -77
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/UE_MCP_Bridge.Build.cs +87 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Private/MCPEngineStatus.cpp +152 -14
- package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Private/Tests/MCPEngineStatusTests.cpp +65 -0
- package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Public/MCPEngineStatus.h +32 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/UE_MCP_BridgeStatus.Build.cs +2 -0
- package/skills/ue-mcp-animation/SKILL.md +154 -0
- package/skills/ue-mcp-blueprint/SKILL.md +71 -0
package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_BTAuthoring.cpp
ADDED
|
@@ -0,0 +1,1323 @@
|
|
|
1
|
+
// BehaviorTree editor-graph authoring: add, reparent, reorder and remove nodes,
|
|
2
|
+
// then recompile the graph into the runnable tree (#889, #947).
|
|
3
|
+
//
|
|
4
|
+
// Why this is native rather than a Python or reflection recipe.
|
|
5
|
+
//
|
|
6
|
+
// A UBehaviorTree asset holds two representations. The runtime one is
|
|
7
|
+
// UBehaviorTree::RootNode, a UBTCompositeNode tree whose child links, execution
|
|
8
|
+
// indices, memory offsets and tree depths are all derived data. The authored
|
|
9
|
+
// one is UBehaviorTree::BTGraph, a UBehaviorTreeGraph of UEdGraphNodes. Only
|
|
10
|
+
// UBehaviorTreeGraph::UpdateAsset() turns the second into the first, and that
|
|
11
|
+
// class lives in the editor-only BehaviorTreeEditor module with no script
|
|
12
|
+
// exposure at all. Spawning UBTNode subclasses by reflection therefore produces
|
|
13
|
+
// objects that are never wired into a runnable tree: InitializeNode is never
|
|
14
|
+
// called, so every node keeps MAX_uint16 as its execution index and the tree
|
|
15
|
+
// refuses to run. The orchestration, not the node types, is the missing piece.
|
|
16
|
+
//
|
|
17
|
+
// The sequence mirrors what the BehaviorTree editor itself performs:
|
|
18
|
+
//
|
|
19
|
+
// 1. Get or create BTGraph, seeding it with the schema's default root node
|
|
20
|
+
// the way FBehaviorTreeEditor::RestoreBehaviorTree does.
|
|
21
|
+
// 2. Create the graph node, set its FGraphNodeClassData, and let
|
|
22
|
+
// PostPlacedNewNode spawn the UBTNode instance under the asset.
|
|
23
|
+
// 3. Link children through the schema's TryCreateConnection, so the BT
|
|
24
|
+
// schema's own rules (composite-only under root, single parent per child,
|
|
25
|
+
// cycle rejection) decide what is legal instead of hand-written pin
|
|
26
|
+
// arrays.
|
|
27
|
+
// 4. Attach decorators and services with AddSubNode. They are not children:
|
|
28
|
+
// the compiler reads them off the owning graph node's Decorators and
|
|
29
|
+
// Services arrays, and a decorator linked as a child would simply be
|
|
30
|
+
// dropped.
|
|
31
|
+
// 5. Write properties, resolve blackboard selectors with InitializeFromAsset.
|
|
32
|
+
// 6. UpdateAsset() last, which is the compile.
|
|
33
|
+
//
|
|
34
|
+
// Child execution order, the thing Selector and Sequence are defined by, is not
|
|
35
|
+
// stored as a list. BTGraphHelpers::CreateChildren sorts each output pin's
|
|
36
|
+
// LinkedTo by FCompareNodeXLocation before walking it, so a child's ordinal is
|
|
37
|
+
// its graph node's NodePosX. Reordering is therefore a position rewrite, done
|
|
38
|
+
// here by MCPBTALayoutChildren rather than by touching the link array, which
|
|
39
|
+
// the compiler would re-sort anyway.
|
|
40
|
+
//
|
|
41
|
+
// UBehaviorTreeGraph::AutoArrange() is deliberately never called: it reads
|
|
42
|
+
// DEPRECATED_NodeWidget, the Slate widget that exists only while the graph is
|
|
43
|
+
// open in an editor tab, and would null-dereference on a headless call.
|
|
44
|
+
|
|
45
|
+
#include "GameplayHandlers.h"
|
|
46
|
+
#include "HandlerRegistry.h"
|
|
47
|
+
#include "HandlerUtils.h"
|
|
48
|
+
|
|
49
|
+
#include "Dom/JsonObject.h"
|
|
50
|
+
#include "Dom/JsonValue.h"
|
|
51
|
+
#include "Misc/Guid.h"
|
|
52
|
+
#include "UObject/Class.h"
|
|
53
|
+
#include "UObject/UnrealType.h"
|
|
54
|
+
|
|
55
|
+
#include "EdGraph/EdGraph.h"
|
|
56
|
+
#include "EdGraph/EdGraphNode.h"
|
|
57
|
+
#include "EdGraph/EdGraphPin.h"
|
|
58
|
+
#include "EdGraph/EdGraphSchema.h"
|
|
59
|
+
#include "Kismet2/BlueprintEditorUtils.h"
|
|
60
|
+
|
|
61
|
+
#include "BehaviorTree/BehaviorTree.h"
|
|
62
|
+
#include "BehaviorTree/BehaviorTreeTypes.h"
|
|
63
|
+
#include "BehaviorTree/Blackboard/BlackboardKey.h"
|
|
64
|
+
#include "BehaviorTree/BlackboardData.h"
|
|
65
|
+
#include "BehaviorTree/BTNode.h"
|
|
66
|
+
#include "BehaviorTree/BTCompositeNode.h"
|
|
67
|
+
#include "BehaviorTree/BTTaskNode.h"
|
|
68
|
+
#include "BehaviorTree/BTDecorator.h"
|
|
69
|
+
#include "BehaviorTree/BTService.h"
|
|
70
|
+
#include "BehaviorTree/Composites/BTComposite_SimpleParallel.h"
|
|
71
|
+
#include "BehaviorTree/Tasks/BTTask_RunBehavior.h"
|
|
72
|
+
|
|
73
|
+
#include "AIGraphTypes.h"
|
|
74
|
+
#include "BehaviorTreeEditorTypes.h"
|
|
75
|
+
#include "BehaviorTreeGraph.h"
|
|
76
|
+
#include "BehaviorTreeGraphNode.h"
|
|
77
|
+
#include "BehaviorTreeGraphNode_Composite.h"
|
|
78
|
+
#include "BehaviorTreeGraphNode_Decorator.h"
|
|
79
|
+
#include "BehaviorTreeGraphNode_Root.h"
|
|
80
|
+
#include "BehaviorTreeGraphNode_Service.h"
|
|
81
|
+
#include "BehaviorTreeGraphNode_SimpleParallel.h"
|
|
82
|
+
#include "BehaviorTreeGraphNode_SubtreeTask.h"
|
|
83
|
+
#include "BehaviorTreeGraphNode_Task.h"
|
|
84
|
+
#include "EdGraphSchema_BehaviorTree.h"
|
|
85
|
+
|
|
86
|
+
namespace
|
|
87
|
+
{
|
|
88
|
+
// Horizontal spacing between siblings. Only the relative order matters to
|
|
89
|
+
// the compiler; the gap is what keeps the graph readable when a human opens
|
|
90
|
+
// the asset afterwards.
|
|
91
|
+
constexpr int32 MCPBTAChildSpacingX = 300;
|
|
92
|
+
|
|
93
|
+
// How far below its parent a newly placed node sits.
|
|
94
|
+
constexpr int32 MCPBTAChildOffsetY = 150;
|
|
95
|
+
|
|
96
|
+
// Depth cap for the graph walk. A BehaviorTree graph is a tree, but a
|
|
97
|
+
// hand-edited or partly corrupted asset is still one the bridge has to
|
|
98
|
+
// answer about, so the walk terminates rather than recursing forever.
|
|
99
|
+
constexpr int32 MCPBTAMaxDepth = 64;
|
|
100
|
+
|
|
101
|
+
// How many node references an ambiguity error lists back to the caller.
|
|
102
|
+
constexpr int32 MCPBTAMaxReported = 40;
|
|
103
|
+
|
|
104
|
+
FString MCPBTAGuidString(const UEdGraphNode* Node)
|
|
105
|
+
{
|
|
106
|
+
return Node ? Node->NodeGuid.ToString(EGuidFormats::Digits) : FString();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// One editor-graph node, with everything a caller needs to address it and
|
|
110
|
+
// everything the walk already established about where it sits.
|
|
111
|
+
struct FMCPBTAEntry
|
|
112
|
+
{
|
|
113
|
+
UBehaviorTreeGraphNode* Node = nullptr;
|
|
114
|
+
FString Category;
|
|
115
|
+
UBehaviorTreeGraphNode* Parent = nullptr;
|
|
116
|
+
/** Position among the parent's children / decorators / services. */
|
|
117
|
+
int32 IndexInParent = INDEX_NONE;
|
|
118
|
+
TArray<UBehaviorTreeGraphNode*> Children;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// The child graph nodes hanging off one node's output pins, in the order
|
|
122
|
+
// the compiler will read them: pins in declaration order, and within a pin,
|
|
123
|
+
// links sorted by node X position. A SimpleParallel declares two output
|
|
124
|
+
// pins (the main task, then the background branch), which is why the pin
|
|
125
|
+
// loop comes first and cannot be collapsed into a single pin lookup.
|
|
126
|
+
//
|
|
127
|
+
// The sort runs on a copy so that a read never reorders a caller's graph.
|
|
128
|
+
void MCPBTACollectChildren(UBehaviorTreeGraphNode* Node, TArray<UBehaviorTreeGraphNode*>& Out)
|
|
129
|
+
{
|
|
130
|
+
Out.Reset();
|
|
131
|
+
if (!Node) return;
|
|
132
|
+
|
|
133
|
+
for (UEdGraphPin* Pin : Node->Pins)
|
|
134
|
+
{
|
|
135
|
+
if (!Pin || Pin->Direction != EGPD_Output) continue;
|
|
136
|
+
|
|
137
|
+
TArray<UEdGraphPin*> Links = Pin->LinkedTo;
|
|
138
|
+
Links.Sort(FCompareNodeXLocation());
|
|
139
|
+
for (UEdGraphPin* Link : Links)
|
|
140
|
+
{
|
|
141
|
+
if (!Link) continue;
|
|
142
|
+
if (UBehaviorTreeGraphNode* Child = Cast<UBehaviorTreeGraphNode>(Link->GetOwningNode()))
|
|
143
|
+
{
|
|
144
|
+
Out.Add(Child);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// The graph's root node. UEdGraphSchema_BehaviorTree::CreateDefaultNodesForGraph
|
|
151
|
+
// places exactly one, and UpdateAsset takes the first it finds, so this
|
|
152
|
+
// matches what the compiler will treat as the root.
|
|
153
|
+
UBehaviorTreeGraphNode_Root* MCPBTAFindRoot(UBehaviorTreeGraph* Graph)
|
|
154
|
+
{
|
|
155
|
+
if (!Graph) return nullptr;
|
|
156
|
+
for (UEdGraphNode* Node : Graph->Nodes)
|
|
157
|
+
{
|
|
158
|
+
if (UBehaviorTreeGraphNode_Root* Root = Cast<UBehaviorTreeGraphNode_Root>(Node))
|
|
159
|
+
{
|
|
160
|
+
return Root;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return nullptr;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
void MCPBTAWalk(UBehaviorTreeGraphNode* Node, UBehaviorTreeGraphNode* Parent,
|
|
167
|
+
const FString& Category, int32 IndexInParent,
|
|
168
|
+
TArray<FMCPBTAEntry>& Out, TSet<UBehaviorTreeGraphNode*>& Seen, int32 Depth)
|
|
169
|
+
{
|
|
170
|
+
if (!Node || Depth > MCPBTAMaxDepth) return;
|
|
171
|
+
|
|
172
|
+
bool bAlreadySeen = false;
|
|
173
|
+
Seen.Add(Node, &bAlreadySeen);
|
|
174
|
+
if (bAlreadySeen) return;
|
|
175
|
+
|
|
176
|
+
FMCPBTAEntry Entry;
|
|
177
|
+
Entry.Node = Node;
|
|
178
|
+
Entry.Category = Category;
|
|
179
|
+
Entry.Parent = Parent;
|
|
180
|
+
Entry.IndexInParent = IndexInParent;
|
|
181
|
+
MCPBTACollectChildren(Node, Entry.Children);
|
|
182
|
+
const TArray<UBehaviorTreeGraphNode*> Children = Entry.Children;
|
|
183
|
+
Out.Add(MoveTemp(Entry));
|
|
184
|
+
|
|
185
|
+
for (int32 d = 0; d < Node->Decorators.Num(); ++d)
|
|
186
|
+
{
|
|
187
|
+
MCPBTAWalk(Node->Decorators[d], Node, TEXT("decorator"), d, Out, Seen, Depth + 1);
|
|
188
|
+
}
|
|
189
|
+
for (int32 s = 0; s < Node->Services.Num(); ++s)
|
|
190
|
+
{
|
|
191
|
+
MCPBTAWalk(Node->Services[s], Node, TEXT("service"), s, Out, Seen, Depth + 1);
|
|
192
|
+
}
|
|
193
|
+
for (int32 c = 0; c < Children.Num(); ++c)
|
|
194
|
+
{
|
|
195
|
+
UBehaviorTreeGraphNode* Child = Children[c];
|
|
196
|
+
const FString ChildCategory = (Child && Cast<UBTCompositeNode>(Child->NodeInstance))
|
|
197
|
+
? TEXT("composite")
|
|
198
|
+
: TEXT("task");
|
|
199
|
+
MCPBTAWalk(Child, Node, ChildCategory, c, Out, Seen, Depth + 1);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Every node the compiler can see, walked from the root, followed by any
|
|
204
|
+
// node the graph holds that the walk never reached. A disconnected node is
|
|
205
|
+
// real authoring state (it is what a half-built tree looks like), and a
|
|
206
|
+
// caller has to be able to address it in order to connect or delete it.
|
|
207
|
+
void MCPBTACollectGraph(UBehaviorTreeGraph* Graph, TArray<FMCPBTAEntry>& Out)
|
|
208
|
+
{
|
|
209
|
+
Out.Reset();
|
|
210
|
+
if (!Graph) return;
|
|
211
|
+
|
|
212
|
+
TSet<UBehaviorTreeGraphNode*> Seen;
|
|
213
|
+
if (UBehaviorTreeGraphNode_Root* Root = MCPBTAFindRoot(Graph))
|
|
214
|
+
{
|
|
215
|
+
MCPBTAWalk(Root, nullptr, TEXT("root"), INDEX_NONE, Out, Seen, 0);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
for (UEdGraphNode* EdNode : Graph->Nodes)
|
|
219
|
+
{
|
|
220
|
+
UBehaviorTreeGraphNode* Node = Cast<UBehaviorTreeGraphNode>(EdNode);
|
|
221
|
+
if (!Node || Seen.Contains(Node)) continue;
|
|
222
|
+
|
|
223
|
+
// An orphan has no walked position, so its category comes from what
|
|
224
|
+
// it is rather than from where it sits.
|
|
225
|
+
FString Category = TEXT("node");
|
|
226
|
+
if (Cast<UBTCompositeNode>(Node->NodeInstance)) Category = TEXT("composite");
|
|
227
|
+
else if (Cast<UBTTaskNode>(Node->NodeInstance)) Category = TEXT("task");
|
|
228
|
+
else if (Cast<UBTDecorator>(Node->NodeInstance)) Category = TEXT("decorator");
|
|
229
|
+
else if (Cast<UBTService>(Node->NodeInstance)) Category = TEXT("service");
|
|
230
|
+
|
|
231
|
+
MCPBTAWalk(Node, nullptr, Category, INDEX_NONE, Out, Seen, 0);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const FMCPBTAEntry* MCPBTAFind(const TArray<FMCPBTAEntry>& Entries, const UBehaviorTreeGraphNode* Node)
|
|
236
|
+
{
|
|
237
|
+
for (const FMCPBTAEntry& Entry : Entries)
|
|
238
|
+
{
|
|
239
|
+
if (Entry.Node == Node) return &Entry;
|
|
240
|
+
}
|
|
241
|
+
return nullptr;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// What a caller could have passed, for an error that has to say so.
|
|
245
|
+
FString MCPBTADescribe(const TArray<FMCPBTAEntry>& Entries)
|
|
246
|
+
{
|
|
247
|
+
TArray<FString> Parts;
|
|
248
|
+
for (const FMCPBTAEntry& Entry : Entries)
|
|
249
|
+
{
|
|
250
|
+
if (Parts.Num() >= MCPBTAMaxReported) break;
|
|
251
|
+
if (!Entry.Node) continue;
|
|
252
|
+
const UObject* Instance = Entry.Node->NodeInstance;
|
|
253
|
+
Parts.Add(FString::Printf(TEXT("%s (%s %s)"),
|
|
254
|
+
*MCPBTAGuidString(Entry.Node),
|
|
255
|
+
*Entry.Category,
|
|
256
|
+
Instance ? *Instance->GetClass()->GetName() : TEXT("root")));
|
|
257
|
+
}
|
|
258
|
+
FString Joined = FString::Join(Parts, TEXT(", "));
|
|
259
|
+
if (Entries.Num() > Parts.Num())
|
|
260
|
+
{
|
|
261
|
+
Joined += FString::Printf(TEXT(", and %d more"), Entries.Num() - Parts.Num());
|
|
262
|
+
}
|
|
263
|
+
return Joined;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Resolve a caller's node reference against the graph.
|
|
267
|
+
//
|
|
268
|
+
// Guid is the primary key, because it survives every recompile and is
|
|
269
|
+
// unaffected by inserting a sibling. The runtime address from
|
|
270
|
+
// read_behavior_tree_graph is accepted too, so a caller who just read the
|
|
271
|
+
// tree does not have to list the graph first, and it resolves by node
|
|
272
|
+
// instance identity rather than by re-deriving the address, which keeps the
|
|
273
|
+
// two surfaces from drifting. A bare name is last, and is rejected when it
|
|
274
|
+
// matches more than one node.
|
|
275
|
+
UBehaviorTreeGraphNode* MCPBTAResolve(
|
|
276
|
+
const TArray<FMCPBTAEntry>& Entries,
|
|
277
|
+
UBehaviorTree* Tree,
|
|
278
|
+
const FString& Reference,
|
|
279
|
+
FString& OutError)
|
|
280
|
+
{
|
|
281
|
+
const FString Spec = Reference.TrimStartAndEnd();
|
|
282
|
+
if (Spec.IsEmpty())
|
|
283
|
+
{
|
|
284
|
+
OutError = TEXT("empty node reference");
|
|
285
|
+
return nullptr;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (Spec.Equals(TEXT("root"), ESearchCase::IgnoreCase))
|
|
289
|
+
{
|
|
290
|
+
for (const FMCPBTAEntry& Entry : Entries)
|
|
291
|
+
{
|
|
292
|
+
if (Entry.Category == TEXT("root")) return Entry.Node;
|
|
293
|
+
}
|
|
294
|
+
OutError = TEXT("this graph has no root node");
|
|
295
|
+
return nullptr;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
FGuid Parsed;
|
|
299
|
+
if (FGuid::Parse(Spec, Parsed))
|
|
300
|
+
{
|
|
301
|
+
for (const FMCPBTAEntry& Entry : Entries)
|
|
302
|
+
{
|
|
303
|
+
if (Entry.Node && Entry.Node->NodeGuid == Parsed) return Entry.Node;
|
|
304
|
+
}
|
|
305
|
+
OutError = FString::Printf(TEXT("no graph node has guid %s"), *Spec);
|
|
306
|
+
return nullptr;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// A structural address like "Root.Children[0].Decorators[1]".
|
|
310
|
+
if (Spec.Contains(TEXT(".")) || Spec.Contains(TEXT("[")))
|
|
311
|
+
{
|
|
312
|
+
TMap<UBTNode*, FString> Addresses;
|
|
313
|
+
FGameplayHandlers::MapBTNodeAddresses(Tree, Addresses);
|
|
314
|
+
for (const FMCPBTAEntry& Entry : Entries)
|
|
315
|
+
{
|
|
316
|
+
UBTNode* Instance = Entry.Node ? Cast<UBTNode>(Entry.Node->NodeInstance) : nullptr;
|
|
317
|
+
const FString* Address = Instance ? Addresses.Find(Instance) : nullptr;
|
|
318
|
+
if (Address && Address->Equals(Spec, ESearchCase::IgnoreCase)) return Entry.Node;
|
|
319
|
+
}
|
|
320
|
+
OutError = FString::Printf(
|
|
321
|
+
TEXT("no compiled node sits at '%s'. Addresses come from read_behavior_tree_graph and only "
|
|
322
|
+
"cover nodes reachable from the root; use a guid from list_bt_graph_nodes otherwise."),
|
|
323
|
+
*Spec);
|
|
324
|
+
return nullptr;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
TArray<UBehaviorTreeGraphNode*> Matches;
|
|
328
|
+
for (const FMCPBTAEntry& Entry : Entries)
|
|
329
|
+
{
|
|
330
|
+
UBTNode* Instance = Entry.Node ? Cast<UBTNode>(Entry.Node->NodeInstance) : nullptr;
|
|
331
|
+
if (!Instance) continue;
|
|
332
|
+
if (Instance->NodeName.Equals(Spec, ESearchCase::IgnoreCase) ||
|
|
333
|
+
Instance->GetName().Equals(Spec, ESearchCase::IgnoreCase) ||
|
|
334
|
+
Instance->GetClass()->GetName().Equals(Spec, ESearchCase::IgnoreCase))
|
|
335
|
+
{
|
|
336
|
+
Matches.Add(Entry.Node);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (Matches.Num() == 1) return Matches[0];
|
|
340
|
+
if (Matches.Num() > 1)
|
|
341
|
+
{
|
|
342
|
+
TArray<FString> Guids;
|
|
343
|
+
for (UBehaviorTreeGraphNode* Match : Matches) Guids.Add(MCPBTAGuidString(Match));
|
|
344
|
+
OutError = FString::Printf(
|
|
345
|
+
TEXT("'%s' matches %d nodes; pass one of these guids instead: %s"),
|
|
346
|
+
*Spec, Matches.Num(), *FString::Join(Guids, TEXT(", ")));
|
|
347
|
+
return nullptr;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
OutError = FString::Printf(
|
|
351
|
+
TEXT("no graph node matched '%s'. Available: %s"), *Spec, *MCPBTADescribe(Entries));
|
|
352
|
+
return nullptr;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Get the asset's editor graph, creating and seeding it when the asset has
|
|
356
|
+
// never been opened in the BehaviorTree editor. gameplay(create_behavior_tree)
|
|
357
|
+
// makes a bare UBehaviorTree with no graph at all, so this is the normal
|
|
358
|
+
// path for a tree the bridge itself authored, not an edge case.
|
|
359
|
+
UBehaviorTreeGraph* MCPBTAGetOrCreateGraph(UBehaviorTree* Tree, bool& bOutCreated)
|
|
360
|
+
{
|
|
361
|
+
bOutCreated = false;
|
|
362
|
+
if (!Tree) return nullptr;
|
|
363
|
+
|
|
364
|
+
if (UBehaviorTreeGraph* Existing = Cast<UBehaviorTreeGraph>(Tree->BTGraph))
|
|
365
|
+
{
|
|
366
|
+
return Existing;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
Tree->Modify();
|
|
370
|
+
Tree->BTGraph = FBlueprintEditorUtils::CreateNewGraph(
|
|
371
|
+
Tree,
|
|
372
|
+
FName(TEXT("Behavior Tree")),
|
|
373
|
+
UBehaviorTreeGraph::StaticClass(),
|
|
374
|
+
UEdGraphSchema_BehaviorTree::StaticClass());
|
|
375
|
+
|
|
376
|
+
UBehaviorTreeGraph* Graph = Cast<UBehaviorTreeGraph>(Tree->BTGraph);
|
|
377
|
+
if (!Graph) return nullptr;
|
|
378
|
+
|
|
379
|
+
if (const UEdGraphSchema* Schema = Graph->GetSchema())
|
|
380
|
+
{
|
|
381
|
+
Schema->CreateDefaultNodesForGraph(*Graph);
|
|
382
|
+
}
|
|
383
|
+
Graph->OnCreated();
|
|
384
|
+
bOutCreated = true;
|
|
385
|
+
return Graph;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// The editor-graph node class that carries a given runtime node class.
|
|
389
|
+
//
|
|
390
|
+
// The engine's own menu builder compares class names exactly. IsChildOf is
|
|
391
|
+
// used here instead, because the distinction is structural rather than
|
|
392
|
+
// cosmetic: a SimpleParallel subclass still needs the two output pins that
|
|
393
|
+
// UBehaviorTreeGraphNode_SimpleParallel allocates, and giving it the plain
|
|
394
|
+
// composite node would leave its background branch unauthorable.
|
|
395
|
+
UClass* MCPBTAGraphNodeClassFor(UClass* RuntimeClass, const FString& Category)
|
|
396
|
+
{
|
|
397
|
+
if (Category == TEXT("composite"))
|
|
398
|
+
{
|
|
399
|
+
return RuntimeClass && RuntimeClass->IsChildOf(UBTComposite_SimpleParallel::StaticClass())
|
|
400
|
+
? UBehaviorTreeGraphNode_SimpleParallel::StaticClass()
|
|
401
|
+
: UBehaviorTreeGraphNode_Composite::StaticClass();
|
|
402
|
+
}
|
|
403
|
+
if (Category == TEXT("task"))
|
|
404
|
+
{
|
|
405
|
+
return RuntimeClass && RuntimeClass->IsChildOf(UBTTask_RunBehavior::StaticClass())
|
|
406
|
+
? UBehaviorTreeGraphNode_SubtreeTask::StaticClass()
|
|
407
|
+
: UBehaviorTreeGraphNode_Task::StaticClass();
|
|
408
|
+
}
|
|
409
|
+
if (Category == TEXT("decorator")) return UBehaviorTreeGraphNode_Decorator::StaticClass();
|
|
410
|
+
if (Category == TEXT("service")) return UBehaviorTreeGraphNode_Service::StaticClass();
|
|
411
|
+
return nullptr;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** The runtime base class a category requires, for validating nodeClass. */
|
|
415
|
+
UClass* MCPBTARuntimeBaseFor(const FString& Category)
|
|
416
|
+
{
|
|
417
|
+
if (Category == TEXT("composite")) return UBTCompositeNode::StaticClass();
|
|
418
|
+
if (Category == TEXT("task")) return UBTTaskNode::StaticClass();
|
|
419
|
+
if (Category == TEXT("decorator")) return UBTDecorator::StaticClass();
|
|
420
|
+
if (Category == TEXT("service")) return UBTService::StaticClass();
|
|
421
|
+
return nullptr;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** Which category a runtime node class belongs to, or empty. */
|
|
425
|
+
FString MCPBTACategoryOf(UClass* RuntimeClass)
|
|
426
|
+
{
|
|
427
|
+
if (!RuntimeClass) return FString();
|
|
428
|
+
if (RuntimeClass->IsChildOf(UBTCompositeNode::StaticClass())) return TEXT("composite");
|
|
429
|
+
if (RuntimeClass->IsChildOf(UBTTaskNode::StaticClass())) return TEXT("task");
|
|
430
|
+
if (RuntimeClass->IsChildOf(UBTDecorator::StaticClass())) return TEXT("decorator");
|
|
431
|
+
if (RuntimeClass->IsChildOf(UBTService::StaticClass())) return TEXT("service");
|
|
432
|
+
return FString();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Number of output pins a node declares. Two means a SimpleParallel. */
|
|
436
|
+
int32 MCPBTAOutputPinCount(const UBehaviorTreeGraphNode* Node)
|
|
437
|
+
{
|
|
438
|
+
int32 Count = 0;
|
|
439
|
+
if (!Node) return 0;
|
|
440
|
+
for (const UEdGraphPin* Pin : Node->Pins)
|
|
441
|
+
{
|
|
442
|
+
if (Pin && Pin->Direction == EGPD_Output) ++Count;
|
|
443
|
+
}
|
|
444
|
+
return Count;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Rewrite sibling X positions so that Ordered[i] is the i-th child at
|
|
448
|
+
// compile time. This is the whole of reordering: BTGraphHelpers::CreateChildren
|
|
449
|
+
// sorts each output pin's links by X, so the position IS the order and
|
|
450
|
+
// editing the link array would not survive the next compile.
|
|
451
|
+
void MCPBTALayoutChildren(UBehaviorTreeGraphNode* Parent, const TArray<UBehaviorTreeGraphNode*>& Ordered)
|
|
452
|
+
{
|
|
453
|
+
if (!Parent) return;
|
|
454
|
+
const int32 Count = Ordered.Num();
|
|
455
|
+
const int32 FirstX = Parent->NodePosX - ((Count - 1) * MCPBTAChildSpacingX) / 2;
|
|
456
|
+
for (int32 i = 0; i < Count; ++i)
|
|
457
|
+
{
|
|
458
|
+
UBehaviorTreeGraphNode* Child = Ordered[i];
|
|
459
|
+
if (!Child) continue;
|
|
460
|
+
Child->Modify();
|
|
461
|
+
Child->NodePosX = FirstX + i * MCPBTAChildSpacingX;
|
|
462
|
+
if (Child->NodePosY <= Parent->NodePosY)
|
|
463
|
+
{
|
|
464
|
+
Child->NodePosY = Parent->NodePosY + MCPBTAChildOffsetY;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Is Candidate at or below Node? Used before a reparent, because the
|
|
470
|
+
// schema's own cycle check walks input pins upward and cannot see the loop
|
|
471
|
+
// once the moved node's old parent link is gone.
|
|
472
|
+
bool MCPBTAIsInSubtree(UBehaviorTreeGraphNode* Node, UBehaviorTreeGraphNode* Candidate, int32 Depth = 0)
|
|
473
|
+
{
|
|
474
|
+
if (!Node || !Candidate || Depth > MCPBTAMaxDepth) return false;
|
|
475
|
+
if (Node == Candidate) return true;
|
|
476
|
+
|
|
477
|
+
TArray<UBehaviorTreeGraphNode*> Children;
|
|
478
|
+
MCPBTACollectChildren(Node, Children);
|
|
479
|
+
for (UBehaviorTreeGraphNode* Child : Children)
|
|
480
|
+
{
|
|
481
|
+
if (MCPBTAIsInSubtree(Child, Candidate, Depth + 1)) return true;
|
|
482
|
+
}
|
|
483
|
+
for (UBehaviorTreeGraphNode* Sub : Node->Decorators)
|
|
484
|
+
{
|
|
485
|
+
if (Sub == Candidate) return true;
|
|
486
|
+
}
|
|
487
|
+
for (UBehaviorTreeGraphNode* Sub : Node->Services)
|
|
488
|
+
{
|
|
489
|
+
if (Sub == Candidate) return true;
|
|
490
|
+
}
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Apply a properties map onto a node instance, reusing the same writer the
|
|
495
|
+
// scoped set_bt_node_property write uses. That matters for more than tidiness:
|
|
496
|
+
// on UE 5.8 BTTask_MoveTo::AcceptableRadius and BTTask_Wait::WaitTime are
|
|
497
|
+
// FValueOrBBKey_Float structs rather than floats, and the shared writer is
|
|
498
|
+
// where a bare JSON number is retargeted onto the struct's DefaultValue
|
|
499
|
+
// instead of failing as "ImportText failed for '60'".
|
|
500
|
+
bool MCPBTAApplyProperties(UBTNode* Instance, const TSharedPtr<FJsonObject>& Properties, FString& OutError)
|
|
501
|
+
{
|
|
502
|
+
if (!Instance || !Properties.IsValid()) return true;
|
|
503
|
+
for (const TPair<FString, TSharedPtr<FJsonValue>>& Pair : Properties->Values)
|
|
504
|
+
{
|
|
505
|
+
FString WriteError;
|
|
506
|
+
if (!FGameplayHandlers::WriteBTNodeProperty(Instance, Pair.Key, Pair.Value, WriteError))
|
|
507
|
+
{
|
|
508
|
+
OutError = FString::Printf(TEXT("%s on %s: %s"),
|
|
509
|
+
*Pair.Key, *Instance->GetClass()->GetName(), *WriteError);
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Point a node's FBlackboardKeySelector fields at named blackboard keys.
|
|
517
|
+
//
|
|
518
|
+
// A selector carries both the key name and the resolved key id, and only
|
|
519
|
+
// the name can be written directly. The id is filled in by
|
|
520
|
+
// InitializeFromAsset against the tree's blackboard, which is why the caller
|
|
521
|
+
// gets a clear error here when the key is not on the blackboard: the write
|
|
522
|
+
// would otherwise succeed and the node would silently never fire.
|
|
523
|
+
bool MCPBTAApplyBlackboardKeys(
|
|
524
|
+
UBTNode* Instance,
|
|
525
|
+
UBehaviorTree* Tree,
|
|
526
|
+
const TSharedPtr<FJsonObject>& Keys,
|
|
527
|
+
TArray<TSharedPtr<FJsonValue>>& OutApplied,
|
|
528
|
+
FString& OutError)
|
|
529
|
+
{
|
|
530
|
+
if (!Instance || !Keys.IsValid()) return true;
|
|
531
|
+
|
|
532
|
+
UClass* NodeClass = Instance->GetClass();
|
|
533
|
+
UBlackboardData* Blackboard = Tree ? Tree->BlackboardAsset.Get() : nullptr;
|
|
534
|
+
|
|
535
|
+
for (const TPair<FString, TSharedPtr<FJsonValue>>& Pair : Keys->Values)
|
|
536
|
+
{
|
|
537
|
+
FString KeyName;
|
|
538
|
+
if (!Pair.Value.IsValid() || !Pair.Value->TryGetString(KeyName))
|
|
539
|
+
{
|
|
540
|
+
OutError = FString::Printf(TEXT("blackboardKeys['%s'] must be a blackboard key name"), *Pair.Key);
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
FStructProperty* Prop = CastField<FStructProperty>(NodeClass->FindPropertyByName(FName(*Pair.Key)));
|
|
545
|
+
if (!Prop || Prop->Struct != FBlackboardKeySelector::StaticStruct())
|
|
546
|
+
{
|
|
547
|
+
OutError = FString::Printf(
|
|
548
|
+
TEXT("%s has no FBlackboardKeySelector property named '%s'"),
|
|
549
|
+
*NodeClass->GetName(), *Pair.Key);
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// FBlackboard::FKey has no IsValid(): it compares against the
|
|
554
|
+
// namespace sentinel, and does so in both the 16-bit default and
|
|
555
|
+
// the AI_BLACKBOARD_KEY_SIZE_8 legacy typedef.
|
|
556
|
+
if (Blackboard && Blackboard->GetKeyID(FName(*KeyName)) == FBlackboard::InvalidKey)
|
|
557
|
+
{
|
|
558
|
+
OutError = FString::Printf(
|
|
559
|
+
TEXT("blackboard %s has no key '%s' for %s.%s"),
|
|
560
|
+
*Blackboard->GetName(), *KeyName, *NodeClass->GetName(), *Pair.Key);
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
void* Addr = Prop->ContainerPtrToValuePtr<void>(Instance);
|
|
565
|
+
FBlackboardKeySelector* Selector = static_cast<FBlackboardKeySelector*>(Addr);
|
|
566
|
+
Selector->SelectedKeyName = FName(*KeyName);
|
|
567
|
+
Selector->InvalidateResolvedKey();
|
|
568
|
+
if (Blackboard)
|
|
569
|
+
{
|
|
570
|
+
Selector->ResolveSelectedKey(*Blackboard);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
TSharedPtr<FJsonObject> Entry = MakeShared<FJsonObject>();
|
|
574
|
+
Entry->SetStringField(TEXT("property"), Pair.Key);
|
|
575
|
+
Entry->SetStringField(TEXT("selectedKeyName"), KeyName);
|
|
576
|
+
Entry->SetBoolField(TEXT("resolved"), Selector->IsSet());
|
|
577
|
+
OutApplied.Add(MakeShared<FJsonValueObject>(Entry));
|
|
578
|
+
}
|
|
579
|
+
return true;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/** One editor-graph node as JSON. Addresses come out alongside the guid. */
|
|
583
|
+
TSharedPtr<FJsonObject> MCPBTADescribeEntry(
|
|
584
|
+
const FMCPBTAEntry& Entry,
|
|
585
|
+
const TMap<UBTNode*, FString>& Addresses)
|
|
586
|
+
{
|
|
587
|
+
TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
|
|
588
|
+
UBehaviorTreeGraphNode* Node = Entry.Node;
|
|
589
|
+
if (!Node) return Obj;
|
|
590
|
+
|
|
591
|
+
Obj->SetStringField(TEXT("guid"), MCPBTAGuidString(Node));
|
|
592
|
+
Obj->SetStringField(TEXT("category"), Entry.Category);
|
|
593
|
+
Obj->SetStringField(TEXT("parentGuid"), MCPBTAGuidString(Entry.Parent));
|
|
594
|
+
Obj->SetNumberField(TEXT("indexInParent"), Entry.IndexInParent);
|
|
595
|
+
Obj->SetStringField(TEXT("graphNodeClass"), Node->GetClass()->GetName());
|
|
596
|
+
Obj->SetNumberField(TEXT("posX"), Node->NodePosX);
|
|
597
|
+
Obj->SetNumberField(TEXT("posY"), Node->NodePosY);
|
|
598
|
+
Obj->SetBoolField(TEXT("injected"), Node->bInjectedNode != 0);
|
|
599
|
+
Obj->SetStringField(TEXT("errorMessage"), Node->ErrorMessage);
|
|
600
|
+
|
|
601
|
+
UBTNode* Instance = Cast<UBTNode>(Node->NodeInstance);
|
|
602
|
+
Obj->SetStringField(TEXT("class"), Instance ? Instance->GetClass()->GetName() : FString());
|
|
603
|
+
Obj->SetStringField(TEXT("classPath"), Instance ? Instance->GetClass()->GetPathName() : FString());
|
|
604
|
+
Obj->SetStringField(TEXT("nodeName"), Instance ? Instance->NodeName : FString());
|
|
605
|
+
Obj->SetStringField(TEXT("objectName"), Instance ? Instance->GetName() : FString());
|
|
606
|
+
|
|
607
|
+
// The address the read surface reports for this node, so a caller can
|
|
608
|
+
// hand a guid to the authoring calls and the same node's address to
|
|
609
|
+
// read_bt_node_properties without listing anything twice. Empty means
|
|
610
|
+
// the node is not currently reachable from the compiled root.
|
|
611
|
+
const FString* Address = Instance ? Addresses.Find(Instance) : nullptr;
|
|
612
|
+
Obj->SetStringField(TEXT("runtimePath"), Address ? *Address : FString());
|
|
613
|
+
|
|
614
|
+
// Generic because Decorators and Services are TArray<TObjectPtr<>>
|
|
615
|
+
// while the walked child list is a plain pointer array.
|
|
616
|
+
auto GuidArray = [](const auto& Nodes)
|
|
617
|
+
{
|
|
618
|
+
TArray<TSharedPtr<FJsonValue>> Out;
|
|
619
|
+
for (const UBehaviorTreeGraphNode* Item : Nodes)
|
|
620
|
+
{
|
|
621
|
+
if (Item) Out.Add(MakeShared<FJsonValueString>(MCPBTAGuidString(Item)));
|
|
622
|
+
}
|
|
623
|
+
return Out;
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
Obj->SetArrayField(TEXT("childGuids"), GuidArray(Entry.Children));
|
|
627
|
+
Obj->SetArrayField(TEXT("decoratorGuids"), GuidArray(Node->Decorators));
|
|
628
|
+
Obj->SetArrayField(TEXT("serviceGuids"), GuidArray(Node->Services));
|
|
629
|
+
return Obj;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** Recompile the graph into the runnable tree and persist the asset. */
|
|
633
|
+
bool MCPBTACompileAndSave(UBehaviorTree* Tree, UBehaviorTreeGraph* Graph)
|
|
634
|
+
{
|
|
635
|
+
if (!Graph || !Tree) return false;
|
|
636
|
+
Graph->Modify();
|
|
637
|
+
Graph->UpdateAsset();
|
|
638
|
+
// A BehaviorTree editor tab open on this asset is listening. Without the
|
|
639
|
+
// notification it keeps drawing the graph as it was before the call,
|
|
640
|
+
// which reads as the call having done nothing.
|
|
641
|
+
Graph->NotifyGraphChanged();
|
|
642
|
+
Tree->PostEditChange();
|
|
643
|
+
Tree->MarkPackageDirty();
|
|
644
|
+
return SaveAssetPackage(Tree);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/** Load the tree and its graph, or return the error a handler should emit. */
|
|
648
|
+
TSharedPtr<FJsonValue> MCPBTAOpen(
|
|
649
|
+
const TSharedPtr<FJsonObject>& Params,
|
|
650
|
+
bool bCreateGraph,
|
|
651
|
+
FString& OutAssetPath,
|
|
652
|
+
UBehaviorTree*& OutTree,
|
|
653
|
+
UBehaviorTreeGraph*& OutGraph,
|
|
654
|
+
bool& bOutGraphCreated)
|
|
655
|
+
{
|
|
656
|
+
OutTree = nullptr;
|
|
657
|
+
OutGraph = nullptr;
|
|
658
|
+
bOutGraphCreated = false;
|
|
659
|
+
|
|
660
|
+
if (auto Err = RequireStringAlt(Params, TEXT("assetPath"), TEXT("path"), OutAssetPath)) return Err;
|
|
661
|
+
|
|
662
|
+
OutTree = FGameplayHandlers::LoadBehaviorTree(OutAssetPath);
|
|
663
|
+
if (!OutTree)
|
|
664
|
+
{
|
|
665
|
+
return MCPError(FString::Printf(TEXT("BehaviorTree not found: %s"), *OutAssetPath));
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (bCreateGraph)
|
|
669
|
+
{
|
|
670
|
+
OutGraph = MCPBTAGetOrCreateGraph(OutTree, bOutGraphCreated);
|
|
671
|
+
if (!OutGraph)
|
|
672
|
+
{
|
|
673
|
+
return MCPError(FString::Printf(
|
|
674
|
+
TEXT("could not create an editor graph for %s"), *OutAssetPath));
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
else
|
|
678
|
+
{
|
|
679
|
+
OutGraph = Cast<UBehaviorTreeGraph>(OutTree->BTGraph);
|
|
680
|
+
}
|
|
681
|
+
return nullptr;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// -----------------------------------------------------------------
|
|
686
|
+
// list_bt_graph_nodes (#889)
|
|
687
|
+
// -----------------------------------------------------------------
|
|
688
|
+
|
|
689
|
+
TSharedPtr<FJsonValue> FGameplayHandlers::ListBTGraphNodes(const TSharedPtr<FJsonObject>& Params)
|
|
690
|
+
{
|
|
691
|
+
FString AssetPath;
|
|
692
|
+
UBehaviorTree* Tree = nullptr;
|
|
693
|
+
UBehaviorTreeGraph* Graph = nullptr;
|
|
694
|
+
bool bGraphCreated = false;
|
|
695
|
+
if (auto Err = MCPBTAOpen(Params, /*bCreateGraph*/ false, AssetPath, Tree, Graph, bGraphCreated)) return Err;
|
|
696
|
+
|
|
697
|
+
auto Result = MCPSuccess();
|
|
698
|
+
Result->SetStringField(TEXT("assetPath"), AssetPath);
|
|
699
|
+
Result->SetBoolField(TEXT("hasGraph"), Graph != nullptr);
|
|
700
|
+
|
|
701
|
+
if (!Graph)
|
|
702
|
+
{
|
|
703
|
+
// A tree created by gameplay(create_behavior_tree) and never opened has
|
|
704
|
+
// no graph yet. That is an answer, not a failure: add_bt_node seeds one.
|
|
705
|
+
Result->SetNumberField(TEXT("nodeCount"), 0);
|
|
706
|
+
Result->SetArrayField(TEXT("nodes"), TArray<TSharedPtr<FJsonValue>>());
|
|
707
|
+
Result->SetStringField(TEXT("note"),
|
|
708
|
+
TEXT("This BehaviorTree has no editor graph yet. add_bt_node creates one with its root node."));
|
|
709
|
+
return MCPResult(Result);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
TArray<FMCPBTAEntry> Entries;
|
|
713
|
+
MCPBTACollectGraph(Graph, Entries);
|
|
714
|
+
|
|
715
|
+
TMap<UBTNode*, FString> Addresses;
|
|
716
|
+
MapBTNodeAddresses(Tree, Addresses);
|
|
717
|
+
|
|
718
|
+
TArray<TSharedPtr<FJsonValue>> Nodes;
|
|
719
|
+
for (const FMCPBTAEntry& Entry : Entries)
|
|
720
|
+
{
|
|
721
|
+
Nodes.Add(MakeShared<FJsonValueObject>(MCPBTADescribeEntry(Entry, Addresses)));
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
Result->SetStringField(TEXT("rootGuid"), MCPBTAGuidString(MCPBTAFindRoot(Graph)));
|
|
725
|
+
Result->SetStringField(TEXT("blackboardAsset"),
|
|
726
|
+
Tree->BlackboardAsset ? Tree->BlackboardAsset->GetPathName() : FString());
|
|
727
|
+
Result->SetNumberField(TEXT("nodeCount"), Nodes.Num());
|
|
728
|
+
Result->SetArrayField(TEXT("nodes"), Nodes);
|
|
729
|
+
return MCPResult(Result);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// -----------------------------------------------------------------
|
|
733
|
+
// add_bt_node (#889)
|
|
734
|
+
// -----------------------------------------------------------------
|
|
735
|
+
|
|
736
|
+
TSharedPtr<FJsonValue> FGameplayHandlers::AddBTNode(const TSharedPtr<FJsonObject>& Params)
|
|
737
|
+
{
|
|
738
|
+
// The graph is seeded only after every argument has been checked. Seeding
|
|
739
|
+
// it is a change to the asset, and a call that is going to be rejected for
|
|
740
|
+
// a bad nodeClass should leave the tree exactly as it found it.
|
|
741
|
+
FString AssetPath;
|
|
742
|
+
UBehaviorTree* Tree = nullptr;
|
|
743
|
+
UBehaviorTreeGraph* Graph = nullptr;
|
|
744
|
+
bool bGraphCreated = false;
|
|
745
|
+
if (auto Err = MCPBTAOpen(Params, /*bCreateGraph*/ false, AssetPath, Tree, Graph, bGraphCreated)) return Err;
|
|
746
|
+
|
|
747
|
+
FString NodeClassSpec;
|
|
748
|
+
if (auto Err = RequireString(Params, TEXT("nodeClass"), NodeClassSpec)) return Err;
|
|
749
|
+
|
|
750
|
+
UClass* RuntimeClass = MCPResolveClass(NodeClassSpec.TrimStartAndEnd(), /*bAllowLoad*/ true);
|
|
751
|
+
if (!RuntimeClass)
|
|
752
|
+
{
|
|
753
|
+
return MCPError(FString::Printf(
|
|
754
|
+
TEXT("BT node class not found: %s. list_bt_node_classes enumerates every class this build offers."),
|
|
755
|
+
*NodeClassSpec));
|
|
756
|
+
}
|
|
757
|
+
if (RuntimeClass->HasAnyClassFlags(CLASS_Abstract))
|
|
758
|
+
{
|
|
759
|
+
return MCPError(FString::Printf(
|
|
760
|
+
TEXT("%s is abstract and cannot be placed. Pick one of its concrete subclasses."),
|
|
761
|
+
*RuntimeClass->GetName()));
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const FString DerivedCategory = MCPBTACategoryOf(RuntimeClass);
|
|
765
|
+
if (DerivedCategory.IsEmpty())
|
|
766
|
+
{
|
|
767
|
+
return MCPError(FString::Printf(
|
|
768
|
+
TEXT("%s is not a BehaviorTree node class. Expected a subclass of UBTCompositeNode, "
|
|
769
|
+
"UBTTaskNode, UBTDecorator or UBTService."),
|
|
770
|
+
*RuntimeClass->GetName()));
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
FString Category = OptionalString(Params, TEXT("nodeCategory")).TrimStartAndEnd().ToLower();
|
|
774
|
+
if (Category.IsEmpty())
|
|
775
|
+
{
|
|
776
|
+
Category = DerivedCategory;
|
|
777
|
+
}
|
|
778
|
+
else if (Category != DerivedCategory)
|
|
779
|
+
{
|
|
780
|
+
return MCPError(FString::Printf(
|
|
781
|
+
TEXT("nodeCategory '%s' does not match %s, which is a %s."),
|
|
782
|
+
*Category, *RuntimeClass->GetName(), *DerivedCategory));
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
UClass* GraphNodeClass = MCPBTAGraphNodeClassFor(RuntimeClass, Category);
|
|
786
|
+
if (!GraphNodeClass)
|
|
787
|
+
{
|
|
788
|
+
return MCPError(FString::Printf(TEXT("unsupported nodeCategory '%s'"), *Category));
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
Graph = MCPBTAGetOrCreateGraph(Tree, bGraphCreated);
|
|
792
|
+
if (!Graph)
|
|
793
|
+
{
|
|
794
|
+
return MCPError(FString::Printf(
|
|
795
|
+
TEXT("could not create an editor graph for %s"), *AssetPath));
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
TArray<FMCPBTAEntry> Entries;
|
|
799
|
+
MCPBTACollectGraph(Graph, Entries);
|
|
800
|
+
|
|
801
|
+
const FString ParentSpec = OptionalString(Params, TEXT("parent"), TEXT("root"));
|
|
802
|
+
FString ResolveError;
|
|
803
|
+
UBehaviorTreeGraphNode* Parent = MCPBTAResolve(Entries, Tree, ParentSpec, ResolveError);
|
|
804
|
+
if (!Parent) return MCPError(FString::Printf(TEXT("parent: %s"), *ResolveError));
|
|
805
|
+
|
|
806
|
+
const bool bParentIsRoot = Parent->IsA<UBehaviorTreeGraphNode_Root>();
|
|
807
|
+
const int32 RequestedIndex = OptionalInt(Params, TEXT("index"), INDEX_NONE);
|
|
808
|
+
|
|
809
|
+
if ((Category == TEXT("decorator") || Category == TEXT("service")) &&
|
|
810
|
+
(Parent->IsA<UBehaviorTreeGraphNode_Decorator>() || Parent->IsA<UBehaviorTreeGraphNode_Service>()))
|
|
811
|
+
{
|
|
812
|
+
return MCPError(TEXT(
|
|
813
|
+
"decorators and services attach to a composite, a task or the root, never to another subnode."));
|
|
814
|
+
}
|
|
815
|
+
if (Category == TEXT("service") && bParentIsRoot)
|
|
816
|
+
{
|
|
817
|
+
return MCPError(TEXT(
|
|
818
|
+
"the root node carries decorators only. Attach a service to the composite beneath it."));
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
Tree->Modify();
|
|
822
|
+
Graph->Modify();
|
|
823
|
+
|
|
824
|
+
UBehaviorTreeGraphNode* NewNode = nullptr;
|
|
825
|
+
int32 PlacedIndex = INDEX_NONE;
|
|
826
|
+
|
|
827
|
+
if (Category == TEXT("decorator") || Category == TEXT("service"))
|
|
828
|
+
{
|
|
829
|
+
// A subnode is owned by its parent graph node, not held in Graph->Nodes,
|
|
830
|
+
// so it is constructed against the graph and handed to AddSubNode, which
|
|
831
|
+
// does the guid, the instance spawn and the parent bookkeeping.
|
|
832
|
+
UBehaviorTreeGraphNode* SubNode = NewObject<UBehaviorTreeGraphNode>(Graph, GraphNodeClass);
|
|
833
|
+
SubNode->ClassData = FGraphNodeClassData(RuntimeClass, FString());
|
|
834
|
+
Parent->AddSubNode(SubNode, Graph);
|
|
835
|
+
NewNode = SubNode;
|
|
836
|
+
|
|
837
|
+
TArray<TObjectPtr<UBehaviorTreeGraphNode>>& Bucket =
|
|
838
|
+
(Category == TEXT("decorator")) ? Parent->Decorators : Parent->Services;
|
|
839
|
+
PlacedIndex = Bucket.IndexOfByKey(SubNode);
|
|
840
|
+
if (RequestedIndex >= 0 && PlacedIndex != INDEX_NONE)
|
|
841
|
+
{
|
|
842
|
+
const int32 Target = FMath::Clamp(RequestedIndex, 0, Bucket.Num() - 1);
|
|
843
|
+
Bucket.RemoveAt(PlacedIndex);
|
|
844
|
+
Bucket.Insert(SubNode, Target);
|
|
845
|
+
PlacedIndex = Target;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
else
|
|
849
|
+
{
|
|
850
|
+
if (bParentIsRoot && Category == TEXT("task"))
|
|
851
|
+
{
|
|
852
|
+
return MCPError(TEXT(
|
|
853
|
+
"the root node accepts a composite only. Add a Selector or Sequence first, then the task under it."));
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
UBehaviorTreeGraphNode* Created = nullptr;
|
|
857
|
+
{
|
|
858
|
+
FGraphNodeCreator<UBehaviorTreeGraphNode> Creator(*Graph);
|
|
859
|
+
Created = Creator.CreateNode(/*bSelectNewNode*/ false, GraphNodeClass);
|
|
860
|
+
if (Created)
|
|
861
|
+
{
|
|
862
|
+
// ClassData has to be in place before Finalize: PostPlacedNewNode
|
|
863
|
+
// reads it to spawn the UBTNode instance under the asset.
|
|
864
|
+
Created->ClassData = FGraphNodeClassData(RuntimeClass, FString());
|
|
865
|
+
}
|
|
866
|
+
Creator.Finalize();
|
|
867
|
+
}
|
|
868
|
+
if (!Created || !Created->NodeInstance)
|
|
869
|
+
{
|
|
870
|
+
if (Created) Created->DestroyNode();
|
|
871
|
+
return MCPError(FString::Printf(
|
|
872
|
+
TEXT("could not instance %s under %s."), *RuntimeClass->GetName(), *AssetPath));
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Which output pin. A SimpleParallel declares two (the main task, then
|
|
876
|
+
// the background branch), and index picks between them rather than
|
|
877
|
+
// ordering siblings, because each of those pins holds a single link.
|
|
878
|
+
const int32 OutputPinCount = MCPBTAOutputPinCount(Parent);
|
|
879
|
+
const int32 PinIndex = (OutputPinCount > 1 && RequestedIndex >= 0)
|
|
880
|
+
? FMath::Clamp(RequestedIndex, 0, OutputPinCount - 1)
|
|
881
|
+
: 0;
|
|
882
|
+
|
|
883
|
+
UEdGraphPin* ParentPin = Parent->GetOutputPin(PinIndex);
|
|
884
|
+
UEdGraphPin* ChildPin = Created->GetInputPin(0);
|
|
885
|
+
const UEdGraphSchema* Schema = Graph->GetSchema();
|
|
886
|
+
if (!ParentPin || !ChildPin || !Schema)
|
|
887
|
+
{
|
|
888
|
+
Created->DestroyNode();
|
|
889
|
+
return MCPError(TEXT("the parent or the new node has no pin to connect."));
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// The schema decides. Its CanCreateConnection is what enforces
|
|
893
|
+
// composite-only under the root, one parent per child, and no cycles,
|
|
894
|
+
// and its response is what says whether an existing link is replaced.
|
|
895
|
+
const FPinConnectionResponse Response = Schema->CanCreateConnection(ParentPin, ChildPin);
|
|
896
|
+
if (Response.Response == CONNECT_RESPONSE_DISALLOW || !Schema->TryCreateConnection(ParentPin, ChildPin))
|
|
897
|
+
{
|
|
898
|
+
const FString ParentName = Parent->NodeInstance
|
|
899
|
+
? Parent->NodeInstance->GetClass()->GetName()
|
|
900
|
+
: FString(TEXT("the root"));
|
|
901
|
+
const FString Reason = Response.Message.IsEmpty()
|
|
902
|
+
? FString(TEXT("the BehaviorTree schema rejected the link"))
|
|
903
|
+
: Response.Message.ToString();
|
|
904
|
+
Created->DestroyNode();
|
|
905
|
+
return MCPError(FString::Printf(
|
|
906
|
+
TEXT("%s cannot be a child of %s: %s"),
|
|
907
|
+
*RuntimeClass->GetName(), *ParentName, *Reason));
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
NewNode = Created;
|
|
911
|
+
|
|
912
|
+
Created->NodePosY = Parent->NodePosY + MCPBTAChildOffsetY;
|
|
913
|
+
if (OutputPinCount > 1)
|
|
914
|
+
{
|
|
915
|
+
Created->NodePosX = Parent->NodePosX + (PinIndex - 1) * MCPBTAChildSpacingX;
|
|
916
|
+
PlacedIndex = PinIndex;
|
|
917
|
+
}
|
|
918
|
+
else
|
|
919
|
+
{
|
|
920
|
+
TArray<UBehaviorTreeGraphNode*> Siblings;
|
|
921
|
+
MCPBTACollectChildren(Parent, Siblings);
|
|
922
|
+
Siblings.Remove(Created);
|
|
923
|
+
const int32 Target = (RequestedIndex >= 0)
|
|
924
|
+
? FMath::Clamp(RequestedIndex, 0, Siblings.Num())
|
|
925
|
+
: Siblings.Num();
|
|
926
|
+
Siblings.Insert(Created, Target);
|
|
927
|
+
MCPBTALayoutChildren(Parent, Siblings);
|
|
928
|
+
PlacedIndex = Target;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
UBTNode* Instance = Cast<UBTNode>(NewNode->NodeInstance);
|
|
933
|
+
if (!Instance)
|
|
934
|
+
{
|
|
935
|
+
NewNode->DestroyNode();
|
|
936
|
+
return MCPError(FString::Printf(
|
|
937
|
+
TEXT("%s produced no runtime node instance."), *RuntimeClass->GetName()));
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const FString NodeName = OptionalString(Params, TEXT("nodeName")).TrimStartAndEnd();
|
|
941
|
+
if (!NodeName.IsEmpty())
|
|
942
|
+
{
|
|
943
|
+
Instance->NodeName = NodeName;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
const TSharedPtr<FJsonObject>* PropertiesObj = nullptr;
|
|
947
|
+
if (Params->TryGetObjectField(TEXT("properties"), PropertiesObj) && PropertiesObj)
|
|
948
|
+
{
|
|
949
|
+
FString WriteError;
|
|
950
|
+
if (!MCPBTAApplyProperties(Instance, *PropertiesObj, WriteError))
|
|
951
|
+
{
|
|
952
|
+
// The node never became part of a compiled tree, so removing it
|
|
953
|
+
// leaves the asset exactly as the call found it.
|
|
954
|
+
NewNode->DestroyNode();
|
|
955
|
+
Graph->UpdateAsset();
|
|
956
|
+
return MCPError(WriteError);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
TArray<TSharedPtr<FJsonValue>> AppliedKeys;
|
|
961
|
+
const TSharedPtr<FJsonObject>* KeysObj = nullptr;
|
|
962
|
+
if (Params->TryGetObjectField(TEXT("blackboardKeys"), KeysObj) && KeysObj)
|
|
963
|
+
{
|
|
964
|
+
FString KeyError;
|
|
965
|
+
if (!MCPBTAApplyBlackboardKeys(Instance, Tree, *KeysObj, AppliedKeys, KeyError))
|
|
966
|
+
{
|
|
967
|
+
NewNode->DestroyNode();
|
|
968
|
+
Graph->UpdateAsset();
|
|
969
|
+
return MCPError(KeyError);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// Resolve the node's own selectors against the tree's blackboard before the
|
|
974
|
+
// compile, so a node that binds a key is already valid when UpdateAsset
|
|
975
|
+
// walks it. UpdateAsset re-runs this for every node afterwards; doing it
|
|
976
|
+
// here means a project node that inspects its own key during compile sees
|
|
977
|
+
// a resolved one.
|
|
978
|
+
Instance->InitializeFromAsset(*Tree);
|
|
979
|
+
|
|
980
|
+
const bool bSaved = MCPBTACompileAndSave(Tree, Graph);
|
|
981
|
+
|
|
982
|
+
TArray<FMCPBTAEntry> AfterEntries;
|
|
983
|
+
MCPBTACollectGraph(Graph, AfterEntries);
|
|
984
|
+
TMap<UBTNode*, FString> Addresses;
|
|
985
|
+
MapBTNodeAddresses(Tree, Addresses);
|
|
986
|
+
|
|
987
|
+
auto Result = MCPSuccess();
|
|
988
|
+
MCPSetCreated(Result);
|
|
989
|
+
Result->SetStringField(TEXT("assetPath"), AssetPath);
|
|
990
|
+
Result->SetBoolField(TEXT("graphCreated"), bGraphCreated);
|
|
991
|
+
Result->SetStringField(TEXT("guid"), MCPBTAGuidString(NewNode));
|
|
992
|
+
Result->SetStringField(TEXT("parentGuid"), MCPBTAGuidString(Parent));
|
|
993
|
+
Result->SetStringField(TEXT("category"), Category);
|
|
994
|
+
Result->SetStringField(TEXT("class"), RuntimeClass->GetName());
|
|
995
|
+
Result->SetStringField(TEXT("classPath"), RuntimeClass->GetPathName());
|
|
996
|
+
Result->SetNumberField(TEXT("index"), PlacedIndex);
|
|
997
|
+
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
998
|
+
if (AppliedKeys.Num() > 0) Result->SetArrayField(TEXT("blackboardKeys"), AppliedKeys);
|
|
999
|
+
|
|
1000
|
+
if (const FMCPBTAEntry* Entry = MCPBTAFind(AfterEntries, NewNode))
|
|
1001
|
+
{
|
|
1002
|
+
Result->SetObjectField(TEXT("node"), MCPBTADescribeEntry(*Entry, Addresses));
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// The compiled tree is what a caller checks the call against, so the node
|
|
1006
|
+
// count and the new node's address come back rather than being assumed.
|
|
1007
|
+
Result->SetNumberField(TEXT("compiledNodeCount"), Addresses.Num());
|
|
1008
|
+
|
|
1009
|
+
TSharedPtr<FJsonObject> Payload = MakeShared<FJsonObject>();
|
|
1010
|
+
Payload->SetStringField(TEXT("assetPath"), AssetPath);
|
|
1011
|
+
Payload->SetStringField(TEXT("node"), MCPBTAGuidString(NewNode));
|
|
1012
|
+
MCPSetRollback(Result, TEXT("remove_bt_node"), Payload);
|
|
1013
|
+
return MCPResult(Result);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// -----------------------------------------------------------------
|
|
1017
|
+
// move_bt_node - reparent and reorder (#947)
|
|
1018
|
+
// -----------------------------------------------------------------
|
|
1019
|
+
|
|
1020
|
+
TSharedPtr<FJsonValue> FGameplayHandlers::MoveBTNode(const TSharedPtr<FJsonObject>& Params)
|
|
1021
|
+
{
|
|
1022
|
+
FString AssetPath;
|
|
1023
|
+
UBehaviorTree* Tree = nullptr;
|
|
1024
|
+
UBehaviorTreeGraph* Graph = nullptr;
|
|
1025
|
+
bool bGraphCreated = false;
|
|
1026
|
+
if (auto Err = MCPBTAOpen(Params, /*bCreateGraph*/ false, AssetPath, Tree, Graph, bGraphCreated)) return Err;
|
|
1027
|
+
if (!Graph)
|
|
1028
|
+
{
|
|
1029
|
+
return MCPError(FString::Printf(
|
|
1030
|
+
TEXT("%s has no editor graph, so it has no nodes to move."), *AssetPath));
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
FString NodeSpec;
|
|
1034
|
+
if (auto Err = RequireStringAlt(Params, TEXT("node"), TEXT("nodePath"), NodeSpec)) return Err;
|
|
1035
|
+
|
|
1036
|
+
TArray<FMCPBTAEntry> Entries;
|
|
1037
|
+
MCPBTACollectGraph(Graph, Entries);
|
|
1038
|
+
|
|
1039
|
+
FString ResolveError;
|
|
1040
|
+
UBehaviorTreeGraphNode* Node = MCPBTAResolve(Entries, Tree, NodeSpec, ResolveError);
|
|
1041
|
+
if (!Node) return MCPError(FString::Printf(TEXT("node: %s"), *ResolveError));
|
|
1042
|
+
if (Node->IsA<UBehaviorTreeGraphNode_Root>())
|
|
1043
|
+
{
|
|
1044
|
+
return MCPError(TEXT("the root node has no parent and cannot be moved."));
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
const FMCPBTAEntry* Before = MCPBTAFind(Entries, Node);
|
|
1048
|
+
UBehaviorTreeGraphNode* OldParent = Before ? Before->Parent : nullptr;
|
|
1049
|
+
const int32 OldIndex = Before ? Before->IndexInParent : INDEX_NONE;
|
|
1050
|
+
const FString Category = Before ? Before->Category : FString();
|
|
1051
|
+
const bool bIsSubNode = (Category == TEXT("decorator") || Category == TEXT("service"));
|
|
1052
|
+
|
|
1053
|
+
const FString ParentSpec = OptionalString(Params, TEXT("parent")).TrimStartAndEnd();
|
|
1054
|
+
const bool bHasIndex = Params.IsValid() && Params->HasField(TEXT("index"));
|
|
1055
|
+
const int32 RequestedIndex = OptionalInt(Params, TEXT("index"), INDEX_NONE);
|
|
1056
|
+
if (ParentSpec.IsEmpty() && !bHasIndex)
|
|
1057
|
+
{
|
|
1058
|
+
return MCPError(TEXT("pass 'parent' to reconnect the node, 'index' to reorder it, or both."));
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
UBehaviorTreeGraphNode* NewParent = OldParent;
|
|
1062
|
+
if (!ParentSpec.IsEmpty())
|
|
1063
|
+
{
|
|
1064
|
+
NewParent = MCPBTAResolve(Entries, Tree, ParentSpec, ResolveError);
|
|
1065
|
+
if (!NewParent) return MCPError(FString::Printf(TEXT("parent: %s"), *ResolveError));
|
|
1066
|
+
}
|
|
1067
|
+
if (!NewParent)
|
|
1068
|
+
{
|
|
1069
|
+
return MCPError(TEXT(
|
|
1070
|
+
"this node is not attached to anything, so 'parent' has to say where it should go."));
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// The schema's cycle check walks input pins upward from the new parent. It
|
|
1074
|
+
// cannot see the loop that reparenting under a descendant would create,
|
|
1075
|
+
// because the moved node's own input link is about to be broken, so the
|
|
1076
|
+
// subtree test happens here instead.
|
|
1077
|
+
if (MCPBTAIsInSubtree(Node, NewParent))
|
|
1078
|
+
{
|
|
1079
|
+
return MCPError(TEXT("a node cannot be moved under itself or one of its own descendants."));
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
Tree->Modify();
|
|
1083
|
+
Graph->Modify();
|
|
1084
|
+
Node->Modify();
|
|
1085
|
+
|
|
1086
|
+
int32 PlacedIndex = INDEX_NONE;
|
|
1087
|
+
|
|
1088
|
+
if (bIsSubNode)
|
|
1089
|
+
{
|
|
1090
|
+
if (NewParent->IsA<UBehaviorTreeGraphNode_Decorator>() || NewParent->IsA<UBehaviorTreeGraphNode_Service>())
|
|
1091
|
+
{
|
|
1092
|
+
return MCPError(TEXT(
|
|
1093
|
+
"decorators and services attach to a composite, a task or the root, never to another subnode."));
|
|
1094
|
+
}
|
|
1095
|
+
if (Category == TEXT("service") && NewParent->IsA<UBehaviorTreeGraphNode_Root>())
|
|
1096
|
+
{
|
|
1097
|
+
return MCPError(TEXT(
|
|
1098
|
+
"the root node carries decorators only. Move the service onto the composite beneath it."));
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
if (OldParent && OldParent != NewParent)
|
|
1102
|
+
{
|
|
1103
|
+
OldParent->Modify();
|
|
1104
|
+
OldParent->RemoveSubNode(Node);
|
|
1105
|
+
NewParent->Modify();
|
|
1106
|
+
NewParent->AddSubNode(Node, Graph);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
TArray<TObjectPtr<UBehaviorTreeGraphNode>>& Bucket =
|
|
1110
|
+
(Category == TEXT("decorator")) ? NewParent->Decorators : NewParent->Services;
|
|
1111
|
+
PlacedIndex = Bucket.IndexOfByKey(Node);
|
|
1112
|
+
if (bHasIndex && RequestedIndex >= 0 && PlacedIndex != INDEX_NONE)
|
|
1113
|
+
{
|
|
1114
|
+
const int32 Target = FMath::Clamp(RequestedIndex, 0, Bucket.Num() - 1);
|
|
1115
|
+
Bucket.RemoveAt(PlacedIndex);
|
|
1116
|
+
Bucket.Insert(Node, Target);
|
|
1117
|
+
PlacedIndex = Target;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
else
|
|
1121
|
+
{
|
|
1122
|
+
const int32 OutputPinCount = MCPBTAOutputPinCount(NewParent);
|
|
1123
|
+
const int32 PinIndex = (OutputPinCount > 1 && RequestedIndex >= 0)
|
|
1124
|
+
? FMath::Clamp(RequestedIndex, 0, OutputPinCount - 1)
|
|
1125
|
+
: 0;
|
|
1126
|
+
|
|
1127
|
+
UEdGraphPin* ChildPin = Node->GetInputPin(0);
|
|
1128
|
+
UEdGraphPin* ParentPin = NewParent->GetOutputPin(PinIndex);
|
|
1129
|
+
const UEdGraphSchema* Schema = Graph->GetSchema();
|
|
1130
|
+
if (!ChildPin || !ParentPin || !Schema)
|
|
1131
|
+
{
|
|
1132
|
+
return MCPError(TEXT("the node or its new parent has no pin to connect."));
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
const bool bReparenting = (NewParent != OldParent) || (OutputPinCount > 1);
|
|
1136
|
+
if (bReparenting)
|
|
1137
|
+
{
|
|
1138
|
+
const FPinConnectionResponse Response = Schema->CanCreateConnection(ParentPin, ChildPin);
|
|
1139
|
+
if (Response.Response == CONNECT_RESPONSE_DISALLOW)
|
|
1140
|
+
{
|
|
1141
|
+
return MCPError(FString::Printf(
|
|
1142
|
+
TEXT("cannot reconnect: %s"), *Response.Message.ToString()));
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// Break the old link explicitly. CanCreateConnection reports
|
|
1146
|
+
// BREAK_OTHERS for the child's single input pin, but only once the
|
|
1147
|
+
// link is gone is the parent's child list correct for the reorder
|
|
1148
|
+
// that follows.
|
|
1149
|
+
ChildPin->BreakAllPinLinks(true);
|
|
1150
|
+
if (!Schema->TryCreateConnection(ParentPin, ChildPin))
|
|
1151
|
+
{
|
|
1152
|
+
const FString ParentName = NewParent->NodeInstance
|
|
1153
|
+
? NewParent->NodeInstance->GetClass()->GetName()
|
|
1154
|
+
: FString(TEXT("the root"));
|
|
1155
|
+
return MCPError(FString::Printf(
|
|
1156
|
+
TEXT("the BehaviorTree schema refused to link this node under %s."), *ParentName));
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
if (OutputPinCount > 1)
|
|
1161
|
+
{
|
|
1162
|
+
Node->NodePosX = NewParent->NodePosX + (PinIndex - 1) * MCPBTAChildSpacingX;
|
|
1163
|
+
Node->NodePosY = NewParent->NodePosY + MCPBTAChildOffsetY;
|
|
1164
|
+
PlacedIndex = PinIndex;
|
|
1165
|
+
}
|
|
1166
|
+
else
|
|
1167
|
+
{
|
|
1168
|
+
TArray<UBehaviorTreeGraphNode*> Siblings;
|
|
1169
|
+
MCPBTACollectChildren(NewParent, Siblings);
|
|
1170
|
+
Siblings.Remove(Node);
|
|
1171
|
+
const int32 Target = (bHasIndex && RequestedIndex >= 0)
|
|
1172
|
+
? FMath::Clamp(RequestedIndex, 0, Siblings.Num())
|
|
1173
|
+
: FMath::Min(OldIndex >= 0 ? OldIndex : Siblings.Num(), Siblings.Num());
|
|
1174
|
+
Siblings.Insert(Node, Target);
|
|
1175
|
+
MCPBTALayoutChildren(NewParent, Siblings);
|
|
1176
|
+
PlacedIndex = Target;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
const bool bSaved = MCPBTACompileAndSave(Tree, Graph);
|
|
1181
|
+
|
|
1182
|
+
TArray<FMCPBTAEntry> AfterEntries;
|
|
1183
|
+
MCPBTACollectGraph(Graph, AfterEntries);
|
|
1184
|
+
TMap<UBTNode*, FString> Addresses;
|
|
1185
|
+
MapBTNodeAddresses(Tree, Addresses);
|
|
1186
|
+
|
|
1187
|
+
auto Result = MCPSuccess();
|
|
1188
|
+
MCPSetUpdated(Result);
|
|
1189
|
+
Result->SetStringField(TEXT("assetPath"), AssetPath);
|
|
1190
|
+
Result->SetStringField(TEXT("guid"), MCPBTAGuidString(Node));
|
|
1191
|
+
Result->SetStringField(TEXT("category"), Category);
|
|
1192
|
+
Result->SetStringField(TEXT("previousParentGuid"), MCPBTAGuidString(OldParent));
|
|
1193
|
+
Result->SetNumberField(TEXT("previousIndex"), OldIndex);
|
|
1194
|
+
Result->SetStringField(TEXT("parentGuid"), MCPBTAGuidString(NewParent));
|
|
1195
|
+
Result->SetNumberField(TEXT("index"), PlacedIndex);
|
|
1196
|
+
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
1197
|
+
|
|
1198
|
+
if (const FMCPBTAEntry* Entry = MCPBTAFind(AfterEntries, Node))
|
|
1199
|
+
{
|
|
1200
|
+
Result->SetObjectField(TEXT("node"), MCPBTADescribeEntry(*Entry, Addresses));
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// The sibling order after the compile, which is the thing a Selector or a
|
|
1204
|
+
// Sequence is defined by and the thing this call exists to change.
|
|
1205
|
+
if (const FMCPBTAEntry* ParentEntry = MCPBTAFind(AfterEntries, NewParent))
|
|
1206
|
+
{
|
|
1207
|
+
TArray<TSharedPtr<FJsonValue>> Order;
|
|
1208
|
+
for (const UBehaviorTreeGraphNode* Child : ParentEntry->Children)
|
|
1209
|
+
{
|
|
1210
|
+
if (Child) Order.Add(MakeShared<FJsonValueString>(MCPBTAGuidString(Child)));
|
|
1211
|
+
}
|
|
1212
|
+
Result->SetArrayField(TEXT("siblingOrder"), Order);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
if (OldParent)
|
|
1216
|
+
{
|
|
1217
|
+
TSharedPtr<FJsonObject> Payload = MakeShared<FJsonObject>();
|
|
1218
|
+
Payload->SetStringField(TEXT("assetPath"), AssetPath);
|
|
1219
|
+
Payload->SetStringField(TEXT("node"), MCPBTAGuidString(Node));
|
|
1220
|
+
Payload->SetStringField(TEXT("parent"), MCPBTAGuidString(OldParent));
|
|
1221
|
+
Payload->SetNumberField(TEXT("index"), OldIndex);
|
|
1222
|
+
MCPSetRollback(Result, TEXT("move_bt_node"), Payload);
|
|
1223
|
+
}
|
|
1224
|
+
return MCPResult(Result);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// -----------------------------------------------------------------
|
|
1228
|
+
// remove_bt_node (#889)
|
|
1229
|
+
// -----------------------------------------------------------------
|
|
1230
|
+
|
|
1231
|
+
TSharedPtr<FJsonValue> FGameplayHandlers::RemoveBTNode(const TSharedPtr<FJsonObject>& Params)
|
|
1232
|
+
{
|
|
1233
|
+
FString AssetPath;
|
|
1234
|
+
UBehaviorTree* Tree = nullptr;
|
|
1235
|
+
UBehaviorTreeGraph* Graph = nullptr;
|
|
1236
|
+
bool bGraphCreated = false;
|
|
1237
|
+
if (auto Err = MCPBTAOpen(Params, /*bCreateGraph*/ false, AssetPath, Tree, Graph, bGraphCreated)) return Err;
|
|
1238
|
+
if (!Graph)
|
|
1239
|
+
{
|
|
1240
|
+
return MCPError(FString::Printf(
|
|
1241
|
+
TEXT("%s has no editor graph, so it has no nodes to remove."), *AssetPath));
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
FString NodeSpec;
|
|
1245
|
+
if (auto Err = RequireStringAlt(Params, TEXT("node"), TEXT("nodePath"), NodeSpec)) return Err;
|
|
1246
|
+
|
|
1247
|
+
TArray<FMCPBTAEntry> Entries;
|
|
1248
|
+
MCPBTACollectGraph(Graph, Entries);
|
|
1249
|
+
|
|
1250
|
+
FString ResolveError;
|
|
1251
|
+
UBehaviorTreeGraphNode* Node = MCPBTAResolve(Entries, Tree, NodeSpec, ResolveError);
|
|
1252
|
+
if (!Node) return MCPError(FString::Printf(TEXT("node: %s"), *ResolveError));
|
|
1253
|
+
if (Node->IsA<UBehaviorTreeGraphNode_Root>())
|
|
1254
|
+
{
|
|
1255
|
+
return MCPError(TEXT("the root node is part of every BehaviorTree graph and cannot be removed."));
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
const FMCPBTAEntry* Entry = MCPBTAFind(Entries, Node);
|
|
1259
|
+
const FString Category = Entry ? Entry->Category : FString();
|
|
1260
|
+
UBehaviorTreeGraphNode* Parent = Entry ? Entry->Parent : nullptr;
|
|
1261
|
+
|
|
1262
|
+
// Removing a composite removes the branch under it. Leaving the subtree
|
|
1263
|
+
// behind as loose graph nodes would make the next list call report nodes
|
|
1264
|
+
// that no longer run, which is worse than the deletion the caller asked for.
|
|
1265
|
+
TArray<UBehaviorTreeGraphNode*> Doomed;
|
|
1266
|
+
{
|
|
1267
|
+
TArray<UBehaviorTreeGraphNode*> Pending;
|
|
1268
|
+
Pending.Add(Node);
|
|
1269
|
+
TSet<UBehaviorTreeGraphNode*> Seen;
|
|
1270
|
+
while (Pending.Num() > 0 && Doomed.Num() < Graph->Nodes.Num() + 1)
|
|
1271
|
+
{
|
|
1272
|
+
UBehaviorTreeGraphNode* Current = Pending.Pop();
|
|
1273
|
+
if (!Current) continue;
|
|
1274
|
+
bool bAlreadySeen = false;
|
|
1275
|
+
Seen.Add(Current, &bAlreadySeen);
|
|
1276
|
+
if (bAlreadySeen) continue;
|
|
1277
|
+
|
|
1278
|
+
Doomed.Add(Current);
|
|
1279
|
+
TArray<UBehaviorTreeGraphNode*> Children;
|
|
1280
|
+
MCPBTACollectChildren(Current, Children);
|
|
1281
|
+
Pending.Append(Children);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
TArray<TSharedPtr<FJsonValue>> Removed;
|
|
1286
|
+
for (const UBehaviorTreeGraphNode* Doom : Doomed)
|
|
1287
|
+
{
|
|
1288
|
+
TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
|
|
1289
|
+
Obj->SetStringField(TEXT("guid"), MCPBTAGuidString(Doom));
|
|
1290
|
+
Obj->SetStringField(TEXT("class"),
|
|
1291
|
+
Doom && Doom->NodeInstance ? Doom->NodeInstance->GetClass()->GetName() : FString());
|
|
1292
|
+
Removed.Add(MakeShared<FJsonValueObject>(Obj));
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
Tree->Modify();
|
|
1296
|
+
Graph->Modify();
|
|
1297
|
+
if (Parent) Parent->Modify();
|
|
1298
|
+
|
|
1299
|
+
// Deepest first, so a parent is never destroyed while a child still points
|
|
1300
|
+
// at it. DestroyNode detaches a subnode from its owner and breaks a normal
|
|
1301
|
+
// node's links, and UpdateAsset's RemoveOrphanedNodes then moves the
|
|
1302
|
+
// stranded UBTNode instances out of the asset package.
|
|
1303
|
+
for (int32 i = Doomed.Num() - 1; i >= 0; --i)
|
|
1304
|
+
{
|
|
1305
|
+
if (Doomed[i]) Doomed[i]->DestroyNode();
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
const bool bSaved = MCPBTACompileAndSave(Tree, Graph);
|
|
1309
|
+
|
|
1310
|
+
TMap<UBTNode*, FString> Addresses;
|
|
1311
|
+
MapBTNodeAddresses(Tree, Addresses);
|
|
1312
|
+
|
|
1313
|
+
auto Result = MCPSuccess();
|
|
1314
|
+
MCPSetUpdated(Result);
|
|
1315
|
+
Result->SetStringField(TEXT("assetPath"), AssetPath);
|
|
1316
|
+
Result->SetStringField(TEXT("category"), Category);
|
|
1317
|
+
Result->SetStringField(TEXT("parentGuid"), MCPBTAGuidString(Parent));
|
|
1318
|
+
Result->SetNumberField(TEXT("removedCount"), Removed.Num());
|
|
1319
|
+
Result->SetArrayField(TEXT("removed"), Removed);
|
|
1320
|
+
Result->SetNumberField(TEXT("compiledNodeCount"), Addresses.Num());
|
|
1321
|
+
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
1322
|
+
return MCPResult(Result);
|
|
1323
|
+
}
|