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.
Files changed (243) hide show
  1. package/README.md +4 -3
  2. package/dist/action-class.js +56 -3
  3. package/dist/action-class.js.map +1 -1
  4. package/dist/bridge-timeouts.d.ts +64 -0
  5. package/dist/bridge-timeouts.js +118 -0
  6. package/dist/bridge-timeouts.js.map +1 -0
  7. package/dist/bridge.js +10 -2
  8. package/dist/bridge.js.map +1 -1
  9. package/dist/editor-control.d.ts +72 -5
  10. package/dist/editor-control.js +164 -140
  11. package/dist/editor-control.js.map +1 -1
  12. package/dist/editor-target.d.ts +55 -1
  13. package/dist/editor-target.js +107 -10
  14. package/dist/editor-target.js.map +1 -1
  15. package/dist/engine-observer.d.ts +59 -5
  16. package/dist/engine-observer.js +249 -29
  17. package/dist/engine-observer.js.map +1 -1
  18. package/dist/engine-root.d.ts +91 -0
  19. package/dist/engine-root.js +448 -0
  20. package/dist/engine-root.js.map +1 -0
  21. package/dist/epic-enrich.js +16 -1
  22. package/dist/epic-enrich.js.map +1 -1
  23. package/dist/feedback-cli.js +4 -0
  24. package/dist/feedback-cli.js.map +1 -1
  25. package/dist/feedback-deferred.d.ts +9 -0
  26. package/dist/feedback-deferred.js +2 -0
  27. package/dist/feedback-deferred.js.map +1 -1
  28. package/dist/feedback-fallback.d.ts +99 -0
  29. package/dist/feedback-fallback.js +173 -0
  30. package/dist/feedback-fallback.js.map +1 -0
  31. package/dist/flow/write-methods.js +20 -0
  32. package/dist/flow/write-methods.js.map +1 -1
  33. package/dist/lean-context.js +8 -3
  34. package/dist/lean-context.js.map +1 -1
  35. package/dist/locking.js +11 -2
  36. package/dist/locking.js.map +1 -1
  37. package/dist/project.d.ts +11 -0
  38. package/dist/project.js +23 -2
  39. package/dist/project.js.map +1 -1
  40. package/dist/python-gate.d.ts +95 -0
  41. package/dist/python-gate.js +175 -0
  42. package/dist/python-gate.js.map +1 -0
  43. package/dist/tool-counts.json +16 -16
  44. package/dist/tools/animation.js +288 -24
  45. package/dist/tools/animation.js.map +1 -1
  46. package/dist/tools/asset.js +79 -14
  47. package/dist/tools/asset.js.map +1 -1
  48. package/dist/tools/audio.js +59 -5
  49. package/dist/tools/audio.js.map +1 -1
  50. package/dist/tools/blueprint.js +29 -12
  51. package/dist/tools/blueprint.js.map +1 -1
  52. package/dist/tools/chooser.js +4 -1
  53. package/dist/tools/chooser.js.map +1 -1
  54. package/dist/tools/editor.js +81 -43
  55. package/dist/tools/editor.js.map +1 -1
  56. package/dist/tools/feedback.js +214 -48
  57. package/dist/tools/feedback.js.map +1 -1
  58. package/dist/tools/foliage.js +27 -1
  59. package/dist/tools/foliage.js.map +1 -1
  60. package/dist/tools/gameplay.js +38 -11
  61. package/dist/tools/gameplay.js.map +1 -1
  62. package/dist/tools/gas.js +17 -5
  63. package/dist/tools/gas.js.map +1 -1
  64. package/dist/tools/landscape.js +22 -7
  65. package/dist/tools/landscape.js.map +1 -1
  66. package/dist/tools/level.js +322 -51
  67. package/dist/tools/level.js.map +1 -1
  68. package/dist/tools/material.js +10 -4
  69. package/dist/tools/material.js.map +1 -1
  70. package/dist/tools/niagara.js +3 -2
  71. package/dist/tools/niagara.js.map +1 -1
  72. package/dist/tools/pcg.js +6 -5
  73. package/dist/tools/pcg.js.map +1 -1
  74. package/dist/tools/project.js +47 -20
  75. package/dist/tools/project.js.map +1 -1
  76. package/dist/types.d.ts +21 -0
  77. package/dist/types.js +46 -3
  78. package/dist/types.js.map +1 -1
  79. package/dist/ue-mcp.default.yml +493 -125
  80. package/package.json +2 -2
  81. package/plugin/ue_mcp_bridge/NATIVE_RUNTIME_VISIBILITY.md +53 -0
  82. package/plugin/ue_mcp_bridge/NATIVE_STATIC_MESH_USAGE_PROGRESS.md +115 -0
  83. package/plugin/ue_mcp_bridge/README_MassHandlers.md +97 -0
  84. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/BridgeServer.cpp +14 -0
  85. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/GameThreadExecutor.cpp +21 -4
  86. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/GameThreadExecutor.h +9 -0
  87. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/HandlerJsonProperty.h +395 -69
  88. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers.cpp +79 -3
  89. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers.h +25 -0
  90. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_ControlRig.cpp +3 -3
  91. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_ControlRigSequencer.cpp +3061 -0
  92. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_IKRetargeterAuthoring.cpp +961 -0
  93. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_IKRigAuthoring.cpp +900 -0
  94. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Pose.cpp +769 -0
  95. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Sequence.cpp +44 -3
  96. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_SkeletalLive.cpp +217 -11
  97. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_StateMachine.cpp +551 -24
  98. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Validation.cpp +735 -0
  99. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers.cpp +352 -73
  100. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers.h +8 -0
  101. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkProperties.cpp +1 -1
  102. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkRead.cpp +503 -0
  103. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkRead.h +21 -0
  104. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkUpsert.cpp +2 -2
  105. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Enum.cpp +2 -2
  106. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Geometry.cpp +902 -0
  107. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Geometry.h +33 -0
  108. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Import.cpp +478 -134
  109. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Mesh.cpp +5 -5
  110. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_MeshBoolean.cpp +933 -0
  111. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_MeshBoolean.h +35 -0
  112. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Redirectors.cpp +478 -0
  113. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_RenderTarget.cpp +1 -1
  114. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Sockets.cpp +4 -4
  115. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Struct.cpp +2 -2
  116. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Subobject.cpp +319 -0
  117. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AudioHandlers_SoundCue.cpp +3 -3
  118. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers.cpp +482 -35
  119. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers.h +10 -0
  120. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Collision.cpp +502 -0
  121. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Collision.h +43 -0
  122. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Functions.cpp +89 -7
  123. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Graph.cpp +17 -51
  124. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Internal.h +107 -0
  125. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Properties.cpp +10 -10
  126. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Search.cpp +648 -0
  127. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DemoHandlers.cpp +5 -1
  128. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DiffHandlers.cpp +252 -1
  129. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DiffHandlers.h +10 -2
  130. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers.cpp +252 -41
  131. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers.h +4 -0
  132. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_Build.cpp +250 -27
  133. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_PIE.cpp +303 -125
  134. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_PIERuntime.cpp +73 -50
  135. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_RuntimeVisibility.cpp +757 -0
  136. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers.cpp +11 -2
  137. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers.h +3 -0
  138. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers_Batch.cpp +419 -0
  139. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers.cpp +39 -161
  140. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers.h +66 -1
  141. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_BTAuthoring.cpp +1323 -0
  142. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_BehaviorTree.cpp +1211 -0
  143. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.cpp +2 -0
  144. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.h +60 -0
  145. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers_Runtime.cpp +363 -12
  146. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/HandlerEditorState.h +84 -0
  147. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.cpp +1288 -36
  148. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.h +3 -0
  149. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers_Sculpt.cpp +41 -16
  150. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers.cpp +959 -213
  151. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers.h +66 -0
  152. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_BatchWrite.cpp +1571 -0
  153. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Convert.cpp +316 -0
  154. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Inspection.cpp +280 -0
  155. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_InstanceProjection.cpp +666 -0
  156. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_InstanceProjection_Internal.h +63 -0
  157. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Lights.cpp +43 -12
  158. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_MultiLevel.cpp +13 -47
  159. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_PostProcess.cpp +527 -0
  160. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Query.cpp +1169 -0
  161. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Refresh.cpp +723 -0
  162. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Save.cpp +325 -0
  163. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Spatial.cpp +360 -0
  164. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Trace.cpp +178 -58
  165. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Transient.cpp +337 -0
  166. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Volumes.cpp +25 -12
  167. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_WorldPartition.cpp +10 -9
  168. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_WorldPartitionSettings.cpp +686 -0
  169. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MassHandlers.cpp +467 -0
  170. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MassHandlers.h +22 -0
  171. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers.cpp +452 -175
  172. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers.h +5 -0
  173. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers_Build.cpp +597 -0
  174. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/NiagaraHandlers.cpp +17 -15
  175. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PCGHandlers.cpp +38 -168
  176. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PhysicsHandlers.cpp +39 -30
  177. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.cpp +392 -78
  178. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.h +2 -0
  179. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers_Internal.h +83 -0
  180. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SkeletalMeshHandlers.cpp +200 -0
  181. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SkeletalMeshHandlers.h +15 -0
  182. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SplineHandlers.cpp +19 -12
  183. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/StateTreeHandlers.cpp +31 -1
  184. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers.cpp +307 -69
  185. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers.h +84 -0
  186. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers_Extraction.cpp +8 -41
  187. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers_Properties.cpp +33 -52
  188. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/ActorSelectorTests.cpp +344 -0
  189. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AnimationControlRigTimelineTests.cpp +60 -0
  190. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AnimationPoseTests.cpp +115 -0
  191. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetBulkReadTests.cpp +177 -0
  192. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetDeleteGuardTests.cpp +276 -0
  193. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetPathResolutionTests.cpp +243 -0
  194. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetPropertyPersistTests.cpp +213 -0
  195. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetSubobjectTests.cpp +235 -0
  196. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AutomationRunnerTests.cpp +72 -0
  197. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BehaviorTreeAuthoringTests.cpp +526 -0
  198. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BehaviorTreeNodeTests.cpp +334 -0
  199. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BlueprintHandlerSurfaceTests.cpp +362 -0
  200. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BridgeProtocolTests.cpp +14 -0
  201. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/CollisionQueryTests.cpp +288 -0
  202. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/DataTableRowWriteTests.cpp +453 -0
  203. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/EditorFunctionCallTests.cpp +223 -0
  204. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/FoliageBatchTests.cpp +147 -0
  205. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/GameThreadGateTests.cpp +82 -0
  206. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/GasLiveAttributeTests.cpp +183 -0
  207. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LandscapeSampleTests.cpp +119 -0
  208. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelBatchWriteTests.cpp +176 -0
  209. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelComponentTreeInstanceCountTests.cpp +189 -0
  210. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelConvertBrushesTests.cpp +100 -0
  211. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelInstanceProjectionTests.cpp +265 -0
  212. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelPostProcessOverrideTests.cpp +258 -0
  213. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelQueryComponentsTests.cpp +195 -0
  214. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelRefreshTests.cpp +142 -0
  215. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelSaveReportTests.cpp +125 -0
  216. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelScriptHandlersTests.cpp +42 -0
  217. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelStaticMeshUsageTests.cpp +412 -0
  218. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelTraceWorldScopeTests.cpp +133 -0
  219. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelTransientActorTests.cpp +205 -0
  220. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelWorldPartitionSettingsTests.cpp +168 -0
  221. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MCPInstancedStructPathTestTypes.h +51 -0
  222. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MCPInstancedStructPathTests.cpp +379 -0
  223. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MaterialBuildTests.cpp +238 -0
  224. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MeshBooleanTests.cpp +170 -0
  225. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MeshGeometryTests.cpp +116 -0
  226. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/PackageSaveExtensionTests.cpp +161 -0
  227. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/PackageWriteGuardTests.cpp +253 -0
  228. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/RuntimeVisibilityTests.cpp +143 -0
  229. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SequencerHandlerTests.cpp +660 -0
  230. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SequencerScrubTests.cpp +50 -0
  231. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SkeletonDiffTests.cpp +685 -0
  232. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/WidgetBlueprintResolveTests.cpp +145 -0
  233. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/UE_MCP_Bridge.cpp +80 -0
  234. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerFunctionCall.h +509 -0
  235. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerQuery.h +407 -0
  236. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerUtils.h +1008 -77
  237. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/UE_MCP_Bridge.Build.cs +87 -0
  238. package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Private/MCPEngineStatus.cpp +152 -14
  239. package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Private/Tests/MCPEngineStatusTests.cpp +65 -0
  240. package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Public/MCPEngineStatus.h +32 -1
  241. package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/UE_MCP_BridgeStatus.Build.cs +2 -0
  242. package/skills/ue-mcp-animation/SKILL.md +154 -0
  243. package/skills/ue-mcp-blueprint/SKILL.md +71 -0
@@ -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
+ }