ue-mcp 1.2.3 → 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 (255) 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/index.js +41 -2
  34. package/dist/index.js.map +1 -1
  35. package/dist/lean-context.js +8 -3
  36. package/dist/lean-context.js.map +1 -1
  37. package/dist/locking.js +11 -2
  38. package/dist/locking.js.map +1 -1
  39. package/dist/plugin/loader.d.ts +4 -0
  40. package/dist/plugin/loader.js +22 -8
  41. package/dist/plugin/loader.js.map +1 -1
  42. package/dist/plugin/manifest.d.ts +72 -52
  43. package/dist/plugin/manifest.js +118 -21
  44. package/dist/plugin/manifest.js.map +1 -1
  45. package/dist/plugin-cli.js +16 -3
  46. package/dist/plugin-cli.js.map +1 -1
  47. package/dist/project.d.ts +11 -0
  48. package/dist/project.js +23 -2
  49. package/dist/project.js.map +1 -1
  50. package/dist/python-gate.d.ts +95 -0
  51. package/dist/python-gate.js +175 -0
  52. package/dist/python-gate.js.map +1 -0
  53. package/dist/tool-counts.json +16 -16
  54. package/dist/tools/animation.js +288 -24
  55. package/dist/tools/animation.js.map +1 -1
  56. package/dist/tools/asset.js +79 -14
  57. package/dist/tools/asset.js.map +1 -1
  58. package/dist/tools/audio.js +59 -5
  59. package/dist/tools/audio.js.map +1 -1
  60. package/dist/tools/blueprint.js +29 -12
  61. package/dist/tools/blueprint.js.map +1 -1
  62. package/dist/tools/chooser.js +4 -1
  63. package/dist/tools/chooser.js.map +1 -1
  64. package/dist/tools/editor.js +81 -43
  65. package/dist/tools/editor.js.map +1 -1
  66. package/dist/tools/feedback.js +214 -48
  67. package/dist/tools/feedback.js.map +1 -1
  68. package/dist/tools/foliage.js +27 -1
  69. package/dist/tools/foliage.js.map +1 -1
  70. package/dist/tools/gameplay.js +38 -11
  71. package/dist/tools/gameplay.js.map +1 -1
  72. package/dist/tools/gas.js +17 -5
  73. package/dist/tools/gas.js.map +1 -1
  74. package/dist/tools/landscape.js +22 -7
  75. package/dist/tools/landscape.js.map +1 -1
  76. package/dist/tools/level.js +322 -51
  77. package/dist/tools/level.js.map +1 -1
  78. package/dist/tools/material.js +10 -4
  79. package/dist/tools/material.js.map +1 -1
  80. package/dist/tools/niagara.js +3 -2
  81. package/dist/tools/niagara.js.map +1 -1
  82. package/dist/tools/pcg.js +6 -5
  83. package/dist/tools/pcg.js.map +1 -1
  84. package/dist/tools/plugins.js +2 -0
  85. package/dist/tools/plugins.js.map +1 -1
  86. package/dist/tools/project.js +47 -20
  87. package/dist/tools/project.js.map +1 -1
  88. package/dist/types.d.ts +24 -0
  89. package/dist/types.js +46 -3
  90. package/dist/types.js.map +1 -1
  91. package/dist/ue-mcp.default.yml +493 -125
  92. package/package.json +2 -2
  93. package/plugin/ue_mcp_bridge/NATIVE_RUNTIME_VISIBILITY.md +53 -0
  94. package/plugin/ue_mcp_bridge/NATIVE_STATIC_MESH_USAGE_PROGRESS.md +115 -0
  95. package/plugin/ue_mcp_bridge/README_MassHandlers.md +97 -0
  96. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/BridgeServer.cpp +14 -0
  97. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/GameThreadExecutor.cpp +21 -4
  98. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/GameThreadExecutor.h +9 -0
  99. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/HandlerJsonProperty.h +395 -69
  100. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers.cpp +79 -3
  101. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers.h +25 -0
  102. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_ControlRig.cpp +3 -3
  103. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_ControlRigSequencer.cpp +3061 -0
  104. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_IKRetargeterAuthoring.cpp +961 -0
  105. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_IKRigAuthoring.cpp +900 -0
  106. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Pose.cpp +769 -0
  107. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Sequence.cpp +44 -3
  108. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_SkeletalLive.cpp +217 -11
  109. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_StateMachine.cpp +551 -24
  110. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Validation.cpp +735 -0
  111. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers.cpp +352 -73
  112. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers.h +8 -0
  113. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkProperties.cpp +1 -1
  114. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkRead.cpp +503 -0
  115. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkRead.h +21 -0
  116. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkUpsert.cpp +2 -2
  117. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Enum.cpp +2 -2
  118. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Geometry.cpp +902 -0
  119. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Geometry.h +33 -0
  120. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Import.cpp +478 -134
  121. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Mesh.cpp +5 -5
  122. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_MeshBoolean.cpp +933 -0
  123. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_MeshBoolean.h +35 -0
  124. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Redirectors.cpp +478 -0
  125. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_RenderTarget.cpp +1 -1
  126. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Sockets.cpp +4 -4
  127. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Struct.cpp +2 -2
  128. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Subobject.cpp +319 -0
  129. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AudioHandlers_SoundCue.cpp +3 -3
  130. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers.cpp +482 -35
  131. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers.h +10 -0
  132. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Collision.cpp +502 -0
  133. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Collision.h +43 -0
  134. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Functions.cpp +89 -7
  135. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Graph.cpp +17 -51
  136. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Internal.h +107 -0
  137. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Properties.cpp +10 -10
  138. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Search.cpp +648 -0
  139. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DemoHandlers.cpp +5 -1
  140. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DiffHandlers.cpp +252 -1
  141. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DiffHandlers.h +10 -2
  142. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers.cpp +252 -41
  143. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers.h +4 -0
  144. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_Build.cpp +250 -27
  145. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_PIE.cpp +303 -125
  146. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_PIERuntime.cpp +73 -50
  147. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_RuntimeVisibility.cpp +757 -0
  148. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers.cpp +11 -2
  149. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers.h +3 -0
  150. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers_Batch.cpp +419 -0
  151. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers.cpp +39 -161
  152. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers.h +66 -1
  153. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_BTAuthoring.cpp +1323 -0
  154. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_BehaviorTree.cpp +1211 -0
  155. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.cpp +2 -0
  156. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.h +60 -0
  157. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers_Runtime.cpp +363 -12
  158. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/HandlerEditorState.h +84 -0
  159. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.cpp +1288 -36
  160. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.h +3 -0
  161. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers_Sculpt.cpp +41 -16
  162. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers.cpp +959 -213
  163. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers.h +66 -0
  164. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_BatchWrite.cpp +1571 -0
  165. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Convert.cpp +316 -0
  166. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Inspection.cpp +280 -0
  167. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_InstanceProjection.cpp +666 -0
  168. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_InstanceProjection_Internal.h +63 -0
  169. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Lights.cpp +43 -12
  170. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_MultiLevel.cpp +13 -47
  171. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_PostProcess.cpp +527 -0
  172. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Query.cpp +1169 -0
  173. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Refresh.cpp +723 -0
  174. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Save.cpp +325 -0
  175. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Spatial.cpp +360 -0
  176. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Trace.cpp +178 -58
  177. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Transient.cpp +337 -0
  178. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Volumes.cpp +25 -12
  179. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_WorldPartition.cpp +10 -9
  180. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_WorldPartitionSettings.cpp +686 -0
  181. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MassHandlers.cpp +467 -0
  182. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MassHandlers.h +22 -0
  183. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers.cpp +452 -175
  184. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers.h +5 -0
  185. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers_Build.cpp +597 -0
  186. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/NiagaraHandlers.cpp +17 -15
  187. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PCGHandlers.cpp +38 -168
  188. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PhysicsHandlers.cpp +39 -30
  189. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.cpp +392 -78
  190. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.h +2 -0
  191. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers_Internal.h +83 -0
  192. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SkeletalMeshHandlers.cpp +200 -0
  193. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SkeletalMeshHandlers.h +15 -0
  194. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SplineHandlers.cpp +19 -12
  195. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/StateTreeHandlers.cpp +31 -1
  196. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers.cpp +307 -69
  197. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers.h +84 -0
  198. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers_Extraction.cpp +8 -41
  199. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers_Properties.cpp +33 -52
  200. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/ActorSelectorTests.cpp +344 -0
  201. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AnimationControlRigTimelineTests.cpp +60 -0
  202. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AnimationPoseTests.cpp +115 -0
  203. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetBulkReadTests.cpp +177 -0
  204. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetDeleteGuardTests.cpp +276 -0
  205. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetPathResolutionTests.cpp +243 -0
  206. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetPropertyPersistTests.cpp +213 -0
  207. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetSubobjectTests.cpp +235 -0
  208. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AutomationRunnerTests.cpp +72 -0
  209. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BehaviorTreeAuthoringTests.cpp +526 -0
  210. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BehaviorTreeNodeTests.cpp +334 -0
  211. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BlueprintHandlerSurfaceTests.cpp +362 -0
  212. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BridgeProtocolTests.cpp +14 -0
  213. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/CollisionQueryTests.cpp +288 -0
  214. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/DataTableRowWriteTests.cpp +453 -0
  215. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/EditorFunctionCallTests.cpp +223 -0
  216. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/FoliageBatchTests.cpp +147 -0
  217. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/GameThreadGateTests.cpp +82 -0
  218. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/GasLiveAttributeTests.cpp +183 -0
  219. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LandscapeSampleTests.cpp +119 -0
  220. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelBatchWriteTests.cpp +176 -0
  221. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelComponentTreeInstanceCountTests.cpp +189 -0
  222. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelConvertBrushesTests.cpp +100 -0
  223. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelInstanceProjectionTests.cpp +265 -0
  224. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelPostProcessOverrideTests.cpp +258 -0
  225. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelQueryComponentsTests.cpp +195 -0
  226. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelRefreshTests.cpp +142 -0
  227. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelSaveReportTests.cpp +125 -0
  228. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelScriptHandlersTests.cpp +42 -0
  229. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelStaticMeshUsageTests.cpp +412 -0
  230. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelTraceWorldScopeTests.cpp +133 -0
  231. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelTransientActorTests.cpp +205 -0
  232. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelWorldPartitionSettingsTests.cpp +168 -0
  233. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MCPInstancedStructPathTestTypes.h +51 -0
  234. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MCPInstancedStructPathTests.cpp +379 -0
  235. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MaterialBuildTests.cpp +238 -0
  236. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MeshBooleanTests.cpp +170 -0
  237. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MeshGeometryTests.cpp +116 -0
  238. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/PackageSaveExtensionTests.cpp +161 -0
  239. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/PackageWriteGuardTests.cpp +253 -0
  240. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/RuntimeVisibilityTests.cpp +143 -0
  241. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SequencerHandlerTests.cpp +660 -0
  242. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SequencerScrubTests.cpp +50 -0
  243. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SkeletonDiffTests.cpp +685 -0
  244. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/WidgetBlueprintResolveTests.cpp +145 -0
  245. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/UE_MCP_Bridge.cpp +80 -0
  246. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerFunctionCall.h +509 -0
  247. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerQuery.h +407 -0
  248. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerUtils.h +1008 -77
  249. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/UE_MCP_Bridge.Build.cs +87 -0
  250. package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Private/MCPEngineStatus.cpp +152 -14
  251. package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Private/Tests/MCPEngineStatusTests.cpp +65 -0
  252. package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Public/MCPEngineStatus.h +32 -1
  253. package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/UE_MCP_BridgeStatus.Build.cs +2 -0
  254. package/skills/ue-mcp-animation/SKILL.md +154 -0
  255. package/skills/ue-mcp-blueprint/SKILL.md +71 -0
@@ -0,0 +1,1211 @@
1
+ // BehaviorTree inspection and node-level authoring.
2
+ //
3
+ // Split out of GameplayHandlers.cpp when the BT surface grew past "what asset
4
+ // is this" into reading node configuration and writing individual owned node
5
+ // subobjects (#887 crash fix, #888 decorator config, #919 filtered reads and
6
+ // scoped nested writes, #940 BTTask_MoveTo FilterClass).
7
+ //
8
+ // Everything here reads through UPROPERTY reflection rather than typed casts to
9
+ // concrete engine node classes. A project's own decorator that does not declare
10
+ // BlackboardKey, or an engine version that renames a field, is then described
11
+ // by what it does have instead of failing the whole read.
12
+
13
+ #include "GameplayHandlers.h"
14
+ #include "HandlerRegistry.h"
15
+ #include "HandlerUtils.h"
16
+ #include "HandlerJsonProperty.h"
17
+ #include "HandlerPropertyText.h"
18
+
19
+ #include "JsonObjectConverter.h"
20
+ #include "Modules/ModuleManager.h"
21
+ #include "AssetRegistry/AssetRegistryModule.h"
22
+ #include "AssetRegistry/AssetData.h"
23
+ #include "EditorScriptingUtilities/Public/EditorAssetLibrary.h"
24
+ #include "Dom/JsonObject.h"
25
+ #include "Dom/JsonValue.h"
26
+ #include "UObject/Class.h"
27
+ #include "UObject/UnrealType.h"
28
+ #include "UObject/PropertyPortFlags.h"
29
+ #include "UObject/TopLevelAssetPath.h"
30
+
31
+ #include "BehaviorTree/BehaviorTree.h"
32
+ #include "BehaviorTree/BlackboardData.h"
33
+ #include "BehaviorTree/Blackboard/BlackboardKeyType.h"
34
+ #include "BehaviorTree/BTNode.h"
35
+ #include "BehaviorTree/BTCompositeNode.h"
36
+ #include "BehaviorTree/BTTaskNode.h"
37
+ #include "BehaviorTree/BTDecorator.h"
38
+ #include "BehaviorTree/BTService.h"
39
+
40
+ namespace
41
+ {
42
+ // How deep a node walk goes before it stops. A BehaviorTree asset is a
43
+ // tree, but a corrupted one is still an asset the bridge has to answer
44
+ // about, and a handler must return a result rather than recurse forever.
45
+ constexpr int32 MCPBTMaxDepth = 64;
46
+
47
+ // Longest parent chain a Blackboard asset may declare before the walk stops.
48
+ constexpr int32 MCPBTMaxBlackboardParents = 32;
49
+
50
+ // How many node paths an ambiguous or empty selector lists back to the
51
+ // caller. Enough to pick from, short enough to read.
52
+ constexpr int32 MCPBTMaxReportedPaths = 40;
53
+
54
+ // "BlackboardKeyType_Object" reads as "Object" everywhere a key type is
55
+ // reported. The full class path travels alongside it.
56
+ FString MCPBTShortKeyTypeName(const UClass* KeyTypeClass)
57
+ {
58
+ if (!KeyTypeClass) return FString();
59
+ FString Name = KeyTypeClass->GetName();
60
+ const FString Prefix = TEXT("BlackboardKeyType_");
61
+ if (Name.StartsWith(Prefix)) Name = Name.RightChop(Prefix.Len());
62
+ return Name;
63
+ }
64
+
65
+ // One property value as JSON. Enum-valued bytes and enum-class properties
66
+ // are emitted as their enumerator name, which is what a caller reads and
67
+ // writes back; everything else goes through the JSON converter, with the
68
+ // export text as the last resort for a type it cannot express.
69
+ TSharedPtr<FJsonValue> MCPBTPropertyToJson(FProperty* Prop, const void* Addr)
70
+ {
71
+ if (!Prop || !Addr) return nullptr;
72
+
73
+ if (const FByteProperty* ByteProp = CastField<FByteProperty>(Prop))
74
+ {
75
+ if (UEnum* Enum = ByteProp->Enum)
76
+ {
77
+ const int64 Raw = ByteProp->GetSignedIntPropertyValue(Addr);
78
+ return MakeShared<FJsonValueString>(Enum->GetNameStringByValue(Raw));
79
+ }
80
+ }
81
+ if (const FEnumProperty* EnumProp = CastField<FEnumProperty>(Prop))
82
+ {
83
+ if (UEnum* Enum = EnumProp->GetEnum())
84
+ {
85
+ const int64 Raw = EnumProp->GetUnderlyingProperty()->GetSignedIntPropertyValue(Addr);
86
+ return MakeShared<FJsonValueString>(Enum->GetNameStringByValue(Raw));
87
+ }
88
+ }
89
+
90
+ TSharedPtr<FJsonValue> Json = FJsonObjectConverter::UPropertyToJsonValue(Prop, Addr, 0, 0);
91
+ if (Json.IsValid()) return Json;
92
+
93
+ FString Text;
94
+ Prop->ExportTextItem_Direct(Text, Addr, nullptr, nullptr, PPF_None);
95
+ return MakeShared<FJsonValueString>(Text);
96
+ }
97
+
98
+ // The DefaultValue field of a UE 5.8 FValueOrBBKey_* struct, or null when
99
+ // this is an ordinary struct.
100
+ //
101
+ // #940/#889: 5.8 replaced plain scalars on BT nodes (AcceptableRadius,
102
+ // WaitTime, FilterClass) with a struct pairing a DefaultValue against an
103
+ // optional blackboard Key that overrides it at runtime. A caller reading
104
+ // FilterClass with get_editor_property sees an empty struct, and a caller
105
+ // writing the scalar they see in the editor lands on the struct. Matched
106
+ // structurally, so a project's own subclass of the family is handled too.
107
+ FProperty* MCPBTValueOrBBKeyDefault(const FStructProperty* StructProp)
108
+ {
109
+ if (!StructProp || !StructProp->Struct) return nullptr;
110
+ if (!StructProp->Struct->GetSuperStruct()) return nullptr;
111
+ FProperty* Default = StructProp->Struct->FindPropertyByName(TEXT("DefaultValue"));
112
+ if (!Default) return nullptr;
113
+ if (!CastField<FNameProperty>(StructProp->Struct->FindPropertyByName(TEXT("Key")))) return nullptr;
114
+ return Default;
115
+ }
116
+
117
+ // A FValueOrBBKey_* value, unpacked into the two halves that matter: the
118
+ // literal DefaultValue and the blackboard Key that overrides it. The export
119
+ // text travels too, because that is the form a struct round trip needs and
120
+ // the form get_editor_property drops.
121
+ TSharedPtr<FJsonObject> MCPBTDescribeValueOrBBKey(FStructProperty* StructProp, void* Addr)
122
+ {
123
+ TSharedPtr<FJsonObject> Out = MakeShared<FJsonObject>();
124
+ if (!StructProp || !StructProp->Struct || !Addr) return Out;
125
+
126
+ Out->SetStringField(TEXT("struct"), StructProp->Struct->GetName());
127
+
128
+ if (FNameProperty* KeyProp = CastField<FNameProperty>(StructProp->Struct->FindPropertyByName(TEXT("Key"))))
129
+ {
130
+ const FName Key = KeyProp->GetPropertyValue(KeyProp->ContainerPtrToValuePtr<void>(Addr));
131
+ const FString KeyStr = Key.IsNone() ? FString() : Key.ToString();
132
+ Out->SetStringField(TEXT("key"), KeyStr);
133
+ Out->SetBoolField(TEXT("isBound"), !KeyStr.IsEmpty());
134
+ }
135
+
136
+ if (FProperty* DefaultProp = StructProp->Struct->FindPropertyByName(TEXT("DefaultValue")))
137
+ {
138
+ void* DefaultAddr = DefaultProp->ContainerPtrToValuePtr<void>(Addr);
139
+ TSharedPtr<FJsonValue> Json = MCPBTPropertyToJson(DefaultProp, DefaultAddr);
140
+ if (Json.IsValid()) Out->SetField(TEXT("defaultValue"), Json);
141
+ else Out->SetField(TEXT("defaultValue"), MakeShared<FJsonValueNull>());
142
+
143
+ if (FObjectPropertyBase* ObjProp = CastField<FObjectPropertyBase>(DefaultProp))
144
+ {
145
+ UObject* Value = ObjProp->GetObjectPropertyValue(DefaultAddr);
146
+ Out->SetStringField(TEXT("defaultValueName"), Value ? Value->GetName() : FString());
147
+ }
148
+ }
149
+
150
+ if (FObjectPropertyBase* BaseProp = CastField<FObjectPropertyBase>(StructProp->Struct->FindPropertyByName(TEXT("BaseClass"))))
151
+ {
152
+ UObject* Base = BaseProp->GetObjectPropertyValue(BaseProp->ContainerPtrToValuePtr<void>(Addr));
153
+ Out->SetStringField(TEXT("baseClass"), Base ? Base->GetPathName() : FString());
154
+ }
155
+
156
+ FString Text;
157
+ StructProp->ExportTextItem_Direct(Text, Addr, nullptr, nullptr, PPF_None);
158
+ Out->SetStringField(TEXT("text"), Text);
159
+ return Out;
160
+ }
161
+
162
+ // Which node kind this object is, in the vocabulary the BT editor uses.
163
+ FString MCPBTKind(const UBTNode* Node)
164
+ {
165
+ if (!Node) return FString();
166
+ if (Node->IsA<UBTCompositeNode>()) return TEXT("composite");
167
+ if (Node->IsA<UBTTaskNode>()) return TEXT("task");
168
+ if (Node->IsA<UBTDecorator>()) return TEXT("decorator");
169
+ if (Node->IsA<UBTService>()) return TEXT("service");
170
+ return TEXT("node");
171
+ }
172
+
173
+ // One node in a walked tree, with the structural address a caller passes
174
+ // back to target it: "Root.Children[1].Decorators[0]".
175
+ struct FMCPBTNodeRef
176
+ {
177
+ UBTNode* Node = nullptr;
178
+ FString Path;
179
+ FString ParentPath;
180
+ FString Kind;
181
+ };
182
+
183
+ void MCPBTCollectFrom(UBTNode* Node, const FString& Path, const FString& ParentPath,
184
+ TArray<FMCPBTNodeRef>& Out, TSet<UBTNode*>& Seen, int32 Depth)
185
+ {
186
+ if (!Node || Depth > MCPBTMaxDepth) return;
187
+
188
+ bool bAlreadySeen = false;
189
+ Seen.Add(Node, &bAlreadySeen);
190
+ if (bAlreadySeen) return;
191
+
192
+ FMCPBTNodeRef Ref;
193
+ Ref.Node = Node;
194
+ Ref.Path = Path;
195
+ Ref.ParentPath = ParentPath;
196
+ Ref.Kind = MCPBTKind(Node);
197
+ Out.Add(MoveTemp(Ref));
198
+
199
+ if (UBTCompositeNode* Comp = Cast<UBTCompositeNode>(Node))
200
+ {
201
+ for (int32 s = 0; s < Comp->Services.Num(); ++s)
202
+ {
203
+ if (UBTService* Svc = Comp->Services[s])
204
+ {
205
+ MCPBTCollectFrom(Svc, FString::Printf(TEXT("%s.Services[%d]"), *Path, s), Path, Out, Seen, Depth + 1);
206
+ }
207
+ }
208
+ for (int32 c = 0; c < Comp->Children.Num(); ++c)
209
+ {
210
+ const FBTCompositeChild& Child = Comp->Children[c];
211
+ const FString ChildPath = FString::Printf(TEXT("%s.Children[%d]"), *Path, c);
212
+ for (int32 d = 0; d < Child.Decorators.Num(); ++d)
213
+ {
214
+ if (UBTDecorator* Dec = Child.Decorators[d])
215
+ {
216
+ MCPBTCollectFrom(Dec, FString::Printf(TEXT("%s.Decorators[%d]"), *ChildPath, d), ChildPath, Out, Seen, Depth + 1);
217
+ }
218
+ }
219
+ if (Child.ChildComposite)
220
+ {
221
+ MCPBTCollectFrom(Child.ChildComposite, ChildPath, Path, Out, Seen, Depth + 1);
222
+ }
223
+ else if (Child.ChildTask)
224
+ {
225
+ MCPBTCollectFrom(Child.ChildTask, ChildPath, Path, Out, Seen, Depth + 1);
226
+ }
227
+ }
228
+ }
229
+ else if (UBTTaskNode* Task = Cast<UBTTaskNode>(Node))
230
+ {
231
+ for (int32 s = 0; s < Task->Services.Num(); ++s)
232
+ {
233
+ if (UBTService* Svc = Task->Services[s])
234
+ {
235
+ MCPBTCollectFrom(Svc, FString::Printf(TEXT("%s.Services[%d]"), *Path, s), Path, Out, Seen, Depth + 1);
236
+ }
237
+ }
238
+ }
239
+ }
240
+
241
+ void MCPBTCollectTree(UBehaviorTree* BT, TArray<FMCPBTNodeRef>& Out)
242
+ {
243
+ if (!BT) return;
244
+ TSet<UBTNode*> Seen;
245
+ for (int32 d = 0; d < BT->RootDecorators.Num(); ++d)
246
+ {
247
+ if (UBTDecorator* Dec = BT->RootDecorators[d])
248
+ {
249
+ MCPBTCollectFrom(Dec, FString::Printf(TEXT("RootDecorators[%d]"), d), FString(), Out, Seen, 0);
250
+ }
251
+ }
252
+ if (BT->RootNode)
253
+ {
254
+ MCPBTCollectFrom(BT->RootNode, TEXT("Root"), FString(), Out, Seen, 0);
255
+ }
256
+ }
257
+
258
+ // The property allow-list a caller passed as propertyNames, lowercased so
259
+ // the match ignores case the way FindPropertyByName does.
260
+ TSet<FString> MCPBTPropertyFilter(const TSharedPtr<FJsonObject>& Params)
261
+ {
262
+ TSet<FString> Filter;
263
+ const TArray<TSharedPtr<FJsonValue>>* Names = nullptr;
264
+ if (Params.IsValid() && Params->TryGetArrayField(TEXT("propertyNames"), Names) && Names)
265
+ {
266
+ for (const FString& Name : JsonArrayToStringList(Names))
267
+ {
268
+ const FString Trimmed = Name.TrimStartAndEnd();
269
+ if (!Trimmed.IsEmpty()) Filter.Add(Trimmed.ToLower());
270
+ }
271
+ }
272
+ return Filter;
273
+ }
274
+
275
+ // Node selection shared by the filtered read, the task inventory and the
276
+ // scoped write. Every supplied filter has to match.
277
+ struct FMCPBTSelector
278
+ {
279
+ UClass* NodeClass = nullptr;
280
+ FString NodeClassSpec;
281
+ FString NodeNameSpec;
282
+ FString NodePathSpec;
283
+ FString KindSpec;
284
+
285
+ bool IsEmpty() const
286
+ {
287
+ return NodeClassSpec.IsEmpty() && NodeNameSpec.IsEmpty() && NodePathSpec.IsEmpty() && KindSpec.IsEmpty();
288
+ }
289
+ };
290
+
291
+ FMCPBTSelector MCPBTReadSelector(const TSharedPtr<FJsonObject>& Params)
292
+ {
293
+ FMCPBTSelector Selector;
294
+ Selector.NodeClassSpec = OptionalString(Params, TEXT("nodeClass")).TrimStartAndEnd();
295
+ Selector.NodeNameSpec = OptionalString(Params, TEXT("nodeName")).TrimStartAndEnd();
296
+ Selector.NodePathSpec = OptionalString(Params, TEXT("nodePath")).TrimStartAndEnd();
297
+ Selector.KindSpec = OptionalString(Params, TEXT("kind")).TrimStartAndEnd();
298
+ if (!Selector.NodeClassSpec.IsEmpty())
299
+ {
300
+ Selector.NodeClass = MCPResolveClass(Selector.NodeClassSpec, /*bAllowLoad*/ true);
301
+ }
302
+ return Selector;
303
+ }
304
+
305
+ bool MCPBTSelectorMatches(const FMCPBTSelector& Selector, const FMCPBTNodeRef& Ref)
306
+ {
307
+ UBTNode* Node = Ref.Node;
308
+ if (!Node) return false;
309
+
310
+ if (!Selector.NodePathSpec.IsEmpty() && !Ref.Path.Equals(Selector.NodePathSpec, ESearchCase::IgnoreCase))
311
+ {
312
+ return false;
313
+ }
314
+ if (!Selector.KindSpec.IsEmpty() && !Ref.Kind.Equals(Selector.KindSpec, ESearchCase::IgnoreCase))
315
+ {
316
+ return false;
317
+ }
318
+ if (!Selector.NodeClassSpec.IsEmpty())
319
+ {
320
+ // A resolved class matches the whole subtree below it, so
321
+ // nodeClass="BTDecorator" selects every decorator. An unresolvable
322
+ // spec falls back to a substring of the class name, which is how a
323
+ // caller pastes a partial name out of the editor.
324
+ const bool bMatches = Selector.NodeClass
325
+ ? Node->IsA(Selector.NodeClass)
326
+ : Node->GetClass()->GetName().Contains(Selector.NodeClassSpec);
327
+ if (!bMatches) return false;
328
+ }
329
+ if (!Selector.NodeNameSpec.IsEmpty())
330
+ {
331
+ const FString ObjectName = Node->GetName();
332
+ const FString DisplayName = Node->NodeName;
333
+ const bool bMatches =
334
+ ObjectName.Equals(Selector.NodeNameSpec, ESearchCase::IgnoreCase) ||
335
+ DisplayName.Equals(Selector.NodeNameSpec, ESearchCase::IgnoreCase) ||
336
+ ObjectName.Contains(Selector.NodeNameSpec) ||
337
+ DisplayName.Contains(Selector.NodeNameSpec);
338
+ if (!bMatches) return false;
339
+ }
340
+ return true;
341
+ }
342
+
343
+ // The node addresses available on this asset, for an error that has to tell
344
+ // the caller what they could have asked for.
345
+ FString MCPBTDescribePaths(const TArray<FMCPBTNodeRef>& Nodes)
346
+ {
347
+ TArray<FString> Paths;
348
+ for (const FMCPBTNodeRef& Ref : Nodes)
349
+ {
350
+ if (Paths.Num() >= MCPBTMaxReportedPaths) break;
351
+ if (!Ref.Node) continue;
352
+ Paths.Add(FString::Printf(TEXT("%s (%s)"), *Ref.Path, *Ref.Node->GetClass()->GetName()));
353
+ }
354
+ FString Joined = FString::Join(Paths, TEXT(", "));
355
+ if (Nodes.Num() > Paths.Num())
356
+ {
357
+ Joined += FString::Printf(TEXT(", and %d more"), Nodes.Num() - Paths.Num());
358
+ }
359
+ return Joined;
360
+ }
361
+
362
+ // Load one BehaviorTree from a caller-supplied path.
363
+ UBehaviorTree* MCPBTLoad(const FString& AssetPath)
364
+ {
365
+ if (UBehaviorTree* Direct = LoadObject<UBehaviorTree>(nullptr, *AssetPath)) return Direct;
366
+ return Cast<UBehaviorTree>(UEditorAssetLibrary::LoadAsset(AssetPath));
367
+ }
368
+ }
369
+
370
+ // -----------------------------------------------------------------
371
+ // Shared primitives (#889)
372
+ // -----------------------------------------------------------------
373
+
374
+ UBehaviorTree* FGameplayHandlers::LoadBehaviorTree(const FString& AssetPath)
375
+ {
376
+ return MCPBTLoad(AssetPath);
377
+ }
378
+
379
+ void FGameplayHandlers::MapBTNodeAddresses(UBehaviorTree* Tree, TMap<UBTNode*, FString>& OutAddresses)
380
+ {
381
+ OutAddresses.Reset();
382
+ if (!Tree) return;
383
+
384
+ TArray<FMCPBTNodeRef> Nodes;
385
+ MCPBTCollectTree(Tree, Nodes);
386
+ for (const FMCPBTNodeRef& Ref : Nodes)
387
+ {
388
+ if (Ref.Node) OutAddresses.Add(Ref.Node, Ref.Path);
389
+ }
390
+ }
391
+
392
+ // -----------------------------------------------------------------
393
+ // Blackboard keys
394
+ // -----------------------------------------------------------------
395
+
396
+ TArray<TSharedPtr<FJsonValue>> FGameplayHandlers::DescribeBlackboardKeys(const UBlackboardData* Blackboard)
397
+ {
398
+ TArray<TSharedPtr<FJsonValue>> Out;
399
+ if (!Blackboard) return Out;
400
+
401
+ // #887: UBlackboardData::Keys is a TArray<FBlackboardEntry>, a struct
402
+ // array. The previous reader took each element's address, reinterpreted it
403
+ // as a UObject** and dereferenced the result, which is the entry's FName
404
+ // read as a pointer. Every blackboard with at least one key therefore
405
+ // access-violated the editor. FBlackboardEntry is read as the struct it is.
406
+ for (const FBlackboardEntry& Entry : Blackboard->Keys)
407
+ {
408
+ TSharedPtr<FJsonObject> KeyObj = MakeShared<FJsonObject>();
409
+ KeyObj->SetStringField(TEXT("name"), Entry.EntryName.ToString());
410
+
411
+ const UBlackboardKeyType* KeyType = Entry.KeyType.Get();
412
+ const UClass* KeyTypeClass = KeyType ? KeyType->GetClass() : nullptr;
413
+ KeyObj->SetStringField(TEXT("type"), MCPBTShortKeyTypeName(KeyTypeClass));
414
+ KeyObj->SetStringField(TEXT("typeClass"), KeyTypeClass ? KeyTypeClass->GetPathName() : FString());
415
+ KeyObj->SetBoolField(TEXT("instanceSynced"), Entry.bInstanceSynced != 0);
416
+ KeyObj->SetStringField(TEXT("owner"), Blackboard->GetPathName());
417
+ #if WITH_EDITORONLY_DATA
418
+ KeyObj->SetStringField(TEXT("description"), Entry.EntryDescription);
419
+ KeyObj->SetStringField(TEXT("category"), Entry.EntryCategory.ToString());
420
+ #endif
421
+ Out.Add(MakeShared<FJsonValueObject>(KeyObj));
422
+ }
423
+ return Out;
424
+ }
425
+
426
+ // -----------------------------------------------------------------
427
+ // Node description (#888 / #919 / #940)
428
+ // -----------------------------------------------------------------
429
+
430
+ TSharedPtr<FJsonObject> FGameplayHandlers::DescribeBTNode(
431
+ UBTNode* Node,
432
+ const FString& NodePath,
433
+ const FString& ParentPath,
434
+ bool bIncludeProperties,
435
+ bool bIncludeInherited,
436
+ const TSet<FString>& PropertyFilter)
437
+ {
438
+ TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
439
+ if (!Node) return Obj;
440
+
441
+ UClass* NodeClass = Node->GetClass();
442
+ Obj->SetStringField(TEXT("path"), NodePath);
443
+ Obj->SetStringField(TEXT("parentPath"), ParentPath);
444
+ Obj->SetStringField(TEXT("kind"), MCPBTKind(Node));
445
+ Obj->SetStringField(TEXT("class"), NodeClass->GetName());
446
+ Obj->SetStringField(TEXT("classPath"), NodeClass->GetPathName());
447
+ Obj->SetStringField(TEXT("objectName"), Node->GetName());
448
+ // `name` is what read_behavior_tree_graph has always reported for a node.
449
+ Obj->SetStringField(TEXT("name"), Node->GetName());
450
+ Obj->SetStringField(TEXT("nodeName"), Node->NodeName);
451
+ Obj->SetStringField(TEXT("staticDescription"), Node->GetStaticDescription());
452
+
453
+ // #888: the runtime configuration of a decorator is what answers "the tree
454
+ // is running but the wrong branch fires". BlackboardKey, the comparison
455
+ // operation, the abort mode and the observer mode are all protected C++
456
+ // fields, so reflection is the only path to them.
457
+ auto AddField = [&Obj, Node, NodeClass](const TCHAR* PropertyName, const TCHAR* JsonKey)
458
+ {
459
+ FProperty* Prop = NodeClass->FindPropertyByName(FName(PropertyName));
460
+ if (!Prop) return;
461
+ TSharedPtr<FJsonValue> Json = MCPBTPropertyToJson(Prop, Prop->ContainerPtrToValuePtr<void>(Node));
462
+ if (Json.IsValid()) Obj->SetField(JsonKey, Json);
463
+ };
464
+
465
+ if (FStructProperty* KeyProp = CastField<FStructProperty>(NodeClass->FindPropertyByName(TEXT("BlackboardKey"))))
466
+ {
467
+ void* KeyAddr = KeyProp->ContainerPtrToValuePtr<void>(Node);
468
+ TSharedPtr<FJsonObject> KeyObj = MakeShared<FJsonObject>();
469
+ if (FNameProperty* SelectedName = CastField<FNameProperty>(KeyProp->Struct->FindPropertyByName(TEXT("SelectedKeyName"))))
470
+ {
471
+ const FName Selected = SelectedName->GetPropertyValue(SelectedName->ContainerPtrToValuePtr<void>(KeyAddr));
472
+ KeyObj->SetStringField(TEXT("selectedKeyName"), Selected.IsNone() ? FString() : Selected.ToString());
473
+ }
474
+ if (FObjectPropertyBase* SelectedType = CastField<FObjectPropertyBase>(KeyProp->Struct->FindPropertyByName(TEXT("SelectedKeyType"))))
475
+ {
476
+ const UClass* TypeClass = Cast<UClass>(SelectedType->GetObjectPropertyValue(SelectedType->ContainerPtrToValuePtr<void>(KeyAddr)));
477
+ KeyObj->SetStringField(TEXT("selectedKeyType"), MCPBTShortKeyTypeName(TypeClass));
478
+ KeyObj->SetStringField(TEXT("selectedKeyTypeClass"), TypeClass ? TypeClass->GetPathName() : FString());
479
+ }
480
+ Obj->SetObjectField(TEXT("blackboardKey"), KeyObj);
481
+ }
482
+
483
+ if (Node->IsA<UBTDecorator>())
484
+ {
485
+ AddField(TEXT("FlowAbortMode"), TEXT("flowAbortMode"));
486
+ AddField(TEXT("bInverseCondition"), TEXT("inverseCondition"));
487
+ AddField(TEXT("NotifyObserver"), TEXT("notifyObserver"));
488
+ AddField(TEXT("BasicOperation"), TEXT("basicOperation"));
489
+ AddField(TEXT("ArithmeticOperation"), TEXT("arithmeticOperation"));
490
+ AddField(TEXT("TextOperation"), TEXT("textOperation"));
491
+ AddField(TEXT("IntValue"), TEXT("intValue"));
492
+ AddField(TEXT("FloatValue"), TEXT("floatValue"));
493
+ AddField(TEXT("StringValue"), TEXT("stringValue"));
494
+ }
495
+
496
+ if (!bIncludeProperties) return Obj;
497
+
498
+ // #919: the project-specific UPROPERTYs a custom task or composite carries.
499
+ // Without a filter the dump starts at the node's own class, because
500
+ // everything UBTNode declares (TreeAsset, ParentNode, NodeName) is tree
501
+ // plumbing this record already reports by name.
502
+ TSharedPtr<FJsonObject> Properties = MakeShared<FJsonObject>();
503
+ TArray<TSharedPtr<FJsonValue>> Skipped;
504
+ for (TFieldIterator<FProperty> It(NodeClass, EFieldIteratorFlags::IncludeSuper); It; ++It)
505
+ {
506
+ FProperty* Prop = *It;
507
+ if (!Prop || Prop->HasAnyPropertyFlags(CPF_Deprecated)) continue;
508
+
509
+ const FString PropertyName = Prop->GetName();
510
+ if (PropertyFilter.Num() > 0)
511
+ {
512
+ if (!PropertyFilter.Contains(PropertyName.ToLower())) continue;
513
+ }
514
+ else if (!bIncludeInherited)
515
+ {
516
+ UStruct* Owner = Prop->GetOwnerStruct();
517
+ if (!Owner || UBTNode::StaticClass()->IsChildOf(Owner)) continue;
518
+ }
519
+
520
+ void* Addr = Prop->ContainerPtrToValuePtr<void>(Node);
521
+ if (FStructProperty* StructProp = CastField<FStructProperty>(Prop))
522
+ {
523
+ if (MCPBTValueOrBBKeyDefault(StructProp))
524
+ {
525
+ Properties->SetObjectField(PropertyName, MCPBTDescribeValueOrBBKey(StructProp, Addr));
526
+ continue;
527
+ }
528
+ }
529
+
530
+ TSharedPtr<FJsonValue> Json = MCPBTPropertyToJson(Prop, Addr);
531
+ if (Json.IsValid()) Properties->SetField(PropertyName, Json);
532
+ else Skipped.Add(MakeShared<FJsonValueString>(PropertyName));
533
+ }
534
+ Obj->SetObjectField(TEXT("properties"), Properties);
535
+ if (Skipped.Num() > 0) Obj->SetArrayField(TEXT("skippedProperties"), Skipped);
536
+ return Obj;
537
+ }
538
+
539
+ // -----------------------------------------------------------------
540
+ // Node property read / write (#919 / #940)
541
+ // -----------------------------------------------------------------
542
+
543
+ TSharedPtr<FJsonValue> FGameplayHandlers::ReadBTNodeProperty(UBTNode* Node, const FString& PropertyPath, FString& OutError)
544
+ {
545
+ if (!Node) { OutError = TEXT("null node"); return nullptr; }
546
+ if (PropertyPath.IsEmpty()) { OutError = TEXT("empty property path"); return nullptr; }
547
+
548
+ FProperty* Prop = nullptr;
549
+ void* Addr = nullptr;
550
+ UObject* LeafOwner = nullptr;
551
+ if (!MCPJsonProperty::ResolveDottedPath(Node, PropertyPath, Prop, Addr, LeafOwner, OutError))
552
+ {
553
+ return nullptr;
554
+ }
555
+
556
+ if (FStructProperty* StructProp = CastField<FStructProperty>(Prop))
557
+ {
558
+ if (MCPBTValueOrBBKeyDefault(StructProp))
559
+ {
560
+ return MakeShared<FJsonValueObject>(MCPBTDescribeValueOrBBKey(StructProp, Addr));
561
+ }
562
+ }
563
+
564
+ TSharedPtr<FJsonValue> Json = MCPBTPropertyToJson(Prop, Addr);
565
+ if (!Json.IsValid())
566
+ {
567
+ OutError = FString::Printf(TEXT("property '%s' has no JSON representation"), *PropertyPath);
568
+ }
569
+ return Json;
570
+ }
571
+
572
+ bool FGameplayHandlers::WriteBTNodeProperty(UBTNode* Node, const FString& PropertyPath, const TSharedPtr<FJsonValue>& Value, FString& OutError)
573
+ {
574
+ if (!Node) { OutError = TEXT("null node"); return false; }
575
+ if (PropertyPath.IsEmpty()) { OutError = TEXT("empty property path"); return false; }
576
+ if (!Value.IsValid()) { OutError = TEXT("null value"); return false; }
577
+
578
+ FProperty* Prop = nullptr;
579
+ void* Addr = nullptr;
580
+ UObject* LeafOwner = nullptr;
581
+ if (!MCPJsonProperty::ResolveDottedPath(Node, PropertyPath, Prop, Addr, LeafOwner, OutError))
582
+ {
583
+ return false;
584
+ }
585
+
586
+ // #940/#889: a scalar aimed at a FValueOrBBKey_* field is the literal the
587
+ // editor shows, so it belongs on DefaultValue. An object value still writes
588
+ // the struct's own fields, which is how {"Key": "AcceptRadius"} binds the
589
+ // field to a blackboard key instead of pinning a literal.
590
+ if (FStructProperty* StructProp = CastField<FStructProperty>(Prop))
591
+ {
592
+ if (Value->Type != EJson::Object)
593
+ {
594
+ if (FProperty* DefaultProp = MCPBTValueOrBBKeyDefault(StructProp))
595
+ {
596
+ Addr = DefaultProp->ContainerPtrToValuePtr<void>(Addr);
597
+ Prop = DefaultProp;
598
+ }
599
+ }
600
+ }
601
+
602
+ // A class-valued leaf is resolved here rather than delegated, so a short
603
+ // class name, a /Script path and a Blueprint asset path all land, and so
604
+ // the class is checked against what the field will accept before it is
605
+ // written.
606
+ FObjectProperty* ObjLeaf = CastField<FObjectProperty>(Prop);
607
+ if (ObjLeaf && ObjLeaf->PropertyClass && ObjLeaf->PropertyClass->IsChildOf(UClass::StaticClass()))
608
+ {
609
+ FString Spec;
610
+ const bool bIsString = Value->TryGetString(Spec);
611
+ if (Value->Type == EJson::Null || (bIsString && (Spec.TrimStartAndEnd().IsEmpty() || Spec == TEXT("None"))))
612
+ {
613
+ ObjLeaf->SetObjectPropertyValue(Addr, nullptr);
614
+ return true;
615
+ }
616
+ if (bIsString)
617
+ {
618
+ UClass* Resolved = MCPResolveClass(Spec.TrimStartAndEnd(), /*bAllowLoad*/ true);
619
+ if (!Resolved)
620
+ {
621
+ OutError = FString::Printf(TEXT("class not found: %s"), *Spec);
622
+ return false;
623
+ }
624
+ if (FClassProperty* ClassLeaf = CastField<FClassProperty>(Prop))
625
+ {
626
+ if (ClassLeaf->MetaClass && !Resolved->IsChildOf(ClassLeaf->MetaClass))
627
+ {
628
+ OutError = FString::Printf(
629
+ TEXT("%s does not derive from %s, which '%s' requires"),
630
+ *Resolved->GetPathName(), *ClassLeaf->MetaClass->GetName(), *PropertyPath);
631
+ return false;
632
+ }
633
+ }
634
+ ObjLeaf->SetObjectPropertyValue(Addr, Resolved);
635
+ return true;
636
+ }
637
+ }
638
+
639
+ return MCPJsonProperty::SetJsonOnProperty(Prop, Addr, Value, OutError);
640
+ }
641
+
642
+ // -----------------------------------------------------------------
643
+ // get_behavior_tree_info (#887)
644
+ // -----------------------------------------------------------------
645
+
646
+ TSharedPtr<FJsonValue> FGameplayHandlers::GetBehaviorTreeInfo(const TSharedPtr<FJsonObject>& Params)
647
+ {
648
+ FString AssetPath;
649
+ if (auto Err = RequireStringAlt(Params, TEXT("assetPath"), TEXT("path"), AssetPath)) return Err;
650
+
651
+ UObject* Asset = UEditorAssetLibrary::LoadAsset(AssetPath);
652
+ if (!Asset)
653
+ {
654
+ return MCPError(FString::Printf(TEXT("BehaviorTree not found: %s"), *AssetPath));
655
+ }
656
+
657
+ UBehaviorTree* BT = Cast<UBehaviorTree>(Asset);
658
+ if (!BT)
659
+ {
660
+ return MCPError(FString::Printf(
661
+ TEXT("%s is a %s. get_behavior_tree_info reads a BehaviorTree asset - use asset(read_properties) for other types."),
662
+ *AssetPath, *Asset->GetClass()->GetName()));
663
+ }
664
+
665
+ auto Result = MCPSuccess();
666
+ Result->SetStringField(TEXT("path"), AssetPath);
667
+ Result->SetStringField(TEXT("name"), BT->GetName());
668
+ Result->SetStringField(TEXT("className"), BT->GetClass()->GetName());
669
+
670
+ UBlackboardData* Blackboard = BT->BlackboardAsset;
671
+ if (Blackboard)
672
+ {
673
+ Result->SetStringField(TEXT("blackboardAsset"), Blackboard->GetPathName());
674
+ Result->SetArrayField(TEXT("blackboardKeys"), DescribeBlackboardKeys(Blackboard));
675
+
676
+ // The parent chain contributes keys the tree can bind to just as well
677
+ // as its own, and the seen set keeps a mis-authored cycle finite.
678
+ TArray<TSharedPtr<FJsonValue>> Inherited;
679
+ TSet<UBlackboardData*> Visited;
680
+ Visited.Add(Blackboard);
681
+ UBlackboardData* Parent = Blackboard->Parent;
682
+ for (int32 Guard = 0; Parent && Guard < MCPBTMaxBlackboardParents; ++Guard)
683
+ {
684
+ bool bAlreadySeen = false;
685
+ Visited.Add(Parent, &bAlreadySeen);
686
+ if (bAlreadySeen) break;
687
+ Inherited.Append(DescribeBlackboardKeys(Parent));
688
+ Parent = Parent->Parent;
689
+ }
690
+ Result->SetArrayField(TEXT("inheritedBlackboardKeys"), Inherited);
691
+ }
692
+ else
693
+ {
694
+ Result->SetArrayField(TEXT("blackboardKeys"), TArray<TSharedPtr<FJsonValue>>());
695
+ }
696
+
697
+ if (BT->RootNode)
698
+ {
699
+ TSharedPtr<FJsonObject> Root = MakeShared<FJsonObject>();
700
+ Root->SetStringField(TEXT("path"), TEXT("Root"));
701
+ Root->SetStringField(TEXT("class"), BT->RootNode->GetClass()->GetName());
702
+ Root->SetStringField(TEXT("objectName"), BT->RootNode->GetName());
703
+ Root->SetStringField(TEXT("nodeName"), BT->RootNode->NodeName);
704
+ Result->SetObjectField(TEXT("rootNode"), Root);
705
+ }
706
+
707
+ TArray<FMCPBTNodeRef> Nodes;
708
+ MCPBTCollectTree(BT, Nodes);
709
+ Result->SetNumberField(TEXT("nodeCount"), Nodes.Num());
710
+ Result->SetNumberField(TEXT("rootDecoratorCount"), BT->RootDecorators.Num());
711
+ return MCPResult(Result);
712
+ }
713
+
714
+ // -----------------------------------------------------------------
715
+ // read_behavior_tree_graph (#124, decorator config added for #888)
716
+ // -----------------------------------------------------------------
717
+
718
+ TSharedPtr<FJsonValue> FGameplayHandlers::ReadBehaviorTreeGraph(const TSharedPtr<FJsonObject>& Params)
719
+ {
720
+ FString AssetPath;
721
+ if (auto Err = RequireStringAlt(Params, TEXT("assetPath"), TEXT("path"), AssetPath)) return Err;
722
+
723
+ UBehaviorTree* BT = MCPBTLoad(AssetPath);
724
+ if (!BT) return MCPError(FString::Printf(TEXT("BehaviorTree not found: %s"), *AssetPath));
725
+
726
+ const bool bIncludeProperties = OptionalBool(Params, TEXT("includeProperties"), false);
727
+ const bool bIncludeInherited = OptionalBool(Params, TEXT("includeInherited"), false);
728
+ const TSet<FString> Filter = MCPBTPropertyFilter(Params);
729
+
730
+ int32 DecoratorCount = 0;
731
+ int32 ServiceCount = 0;
732
+ TSet<UBTNode*> Seen;
733
+
734
+ auto Describe = [&](UBTNode* Node, const FString& Path, const FString& ParentPath)
735
+ {
736
+ return DescribeBTNode(Node, Path, ParentPath, bIncludeProperties, bIncludeInherited, Filter);
737
+ };
738
+
739
+ TFunction<TSharedPtr<FJsonObject>(UBTNode*, const FString&, const FString&, int32)> Walk;
740
+ Walk = [&](UBTNode* Node, const FString& Path, const FString& ParentPath, int32 Depth) -> TSharedPtr<FJsonObject>
741
+ {
742
+ if (!Node || Depth > MCPBTMaxDepth) return nullptr;
743
+ bool bAlreadySeen = false;
744
+ Seen.Add(Node, &bAlreadySeen);
745
+ if (bAlreadySeen) return nullptr;
746
+
747
+ TSharedPtr<FJsonObject> NObj = Describe(Node, Path, ParentPath);
748
+
749
+ if (UBTCompositeNode* Comp = Cast<UBTCompositeNode>(Node))
750
+ {
751
+ TArray<TSharedPtr<FJsonValue>> ChildrenArr;
752
+ for (int32 c = 0; c < Comp->Children.Num(); ++c)
753
+ {
754
+ const FBTCompositeChild& Child = Comp->Children[c];
755
+ const FString ChildPath = FString::Printf(TEXT("%s.Children[%d]"), *Path, c);
756
+
757
+ TSharedPtr<FJsonObject> ChildEntry = MakeShared<FJsonObject>();
758
+ ChildEntry->SetStringField(TEXT("path"), ChildPath);
759
+ if (Child.ChildComposite)
760
+ {
761
+ if (TSharedPtr<FJsonObject> Sub = Walk(Child.ChildComposite, ChildPath, Path, Depth + 1))
762
+ {
763
+ ChildEntry->SetObjectField(TEXT("child"), Sub);
764
+ }
765
+ }
766
+ else if (Child.ChildTask)
767
+ {
768
+ if (TSharedPtr<FJsonObject> Sub = Walk(Child.ChildTask, ChildPath, Path, Depth + 1))
769
+ {
770
+ ChildEntry->SetObjectField(TEXT("child"), Sub);
771
+ }
772
+ }
773
+
774
+ TArray<TSharedPtr<FJsonValue>> Decorators;
775
+ for (int32 d = 0; d < Child.Decorators.Num(); ++d)
776
+ {
777
+ UBTDecorator* Dec = Child.Decorators[d];
778
+ if (!Dec) continue;
779
+ const FString DecoratorPath = FString::Printf(TEXT("%s.Decorators[%d]"), *ChildPath, d);
780
+ Decorators.Add(MakeShared<FJsonValueObject>(Describe(Dec, DecoratorPath, ChildPath)));
781
+ ++DecoratorCount;
782
+ }
783
+ ChildEntry->SetArrayField(TEXT("decorators"), Decorators);
784
+ ChildrenArr.Add(MakeShared<FJsonValueObject>(ChildEntry));
785
+ }
786
+ NObj->SetArrayField(TEXT("children"), ChildrenArr);
787
+
788
+ TArray<TSharedPtr<FJsonValue>> Services;
789
+ for (int32 s = 0; s < Comp->Services.Num(); ++s)
790
+ {
791
+ UBTService* Svc = Comp->Services[s];
792
+ if (!Svc) continue;
793
+ Services.Add(MakeShared<FJsonValueObject>(Describe(Svc, FString::Printf(TEXT("%s.Services[%d]"), *Path, s), Path)));
794
+ ++ServiceCount;
795
+ }
796
+ NObj->SetArrayField(TEXT("services"), Services);
797
+ }
798
+ else if (UBTTaskNode* Task = Cast<UBTTaskNode>(Node))
799
+ {
800
+ TArray<TSharedPtr<FJsonValue>> Services;
801
+ for (int32 s = 0; s < Task->Services.Num(); ++s)
802
+ {
803
+ UBTService* Svc = Task->Services[s];
804
+ if (!Svc) continue;
805
+ Services.Add(MakeShared<FJsonValueObject>(Describe(Svc, FString::Printf(TEXT("%s.Services[%d]"), *Path, s), Path)));
806
+ ++ServiceCount;
807
+ }
808
+ NObj->SetArrayField(TEXT("services"), Services);
809
+ }
810
+ return NObj;
811
+ };
812
+
813
+ auto Result = MCPSuccess();
814
+ Result->SetStringField(TEXT("assetPath"), AssetPath);
815
+ Result->SetStringField(TEXT("name"), BT->GetName());
816
+ if (BT->BlackboardAsset)
817
+ {
818
+ Result->SetStringField(TEXT("blackboardAsset"), BT->BlackboardAsset->GetPathName());
819
+ Result->SetArrayField(TEXT("blackboardKeys"), DescribeBlackboardKeys(BT->BlackboardAsset));
820
+ }
821
+
822
+ TArray<TSharedPtr<FJsonValue>> RootDecorators;
823
+ for (int32 d = 0; d < BT->RootDecorators.Num(); ++d)
824
+ {
825
+ UBTDecorator* Dec = BT->RootDecorators[d];
826
+ if (!Dec) continue;
827
+ RootDecorators.Add(MakeShared<FJsonValueObject>(
828
+ Describe(Dec, FString::Printf(TEXT("RootDecorators[%d]"), d), FString())));
829
+ ++DecoratorCount;
830
+ }
831
+ Result->SetArrayField(TEXT("rootDecorators"), RootDecorators);
832
+
833
+ if (BT->RootNode)
834
+ {
835
+ if (TSharedPtr<FJsonObject> Root = Walk(BT->RootNode, TEXT("Root"), FString(), 0))
836
+ {
837
+ Result->SetObjectField(TEXT("root"), Root);
838
+ }
839
+ }
840
+
841
+ Result->SetNumberField(TEXT("decoratorCount"), DecoratorCount);
842
+ Result->SetNumberField(TEXT("serviceCount"), ServiceCount);
843
+ return MCPResult(Result);
844
+ }
845
+
846
+ // -----------------------------------------------------------------
847
+ // read_bt_node_properties (#919)
848
+ // -----------------------------------------------------------------
849
+
850
+ TSharedPtr<FJsonValue> FGameplayHandlers::ReadBTNodeProperties(const TSharedPtr<FJsonObject>& Params)
851
+ {
852
+ FString AssetPath;
853
+ if (auto Err = RequireStringAlt(Params, TEXT("assetPath"), TEXT("path"), AssetPath)) return Err;
854
+
855
+ UBehaviorTree* BT = MCPBTLoad(AssetPath);
856
+ if (!BT) return MCPError(FString::Printf(TEXT("BehaviorTree not found: %s"), *AssetPath));
857
+
858
+ TArray<FMCPBTNodeRef> Nodes;
859
+ MCPBTCollectTree(BT, Nodes);
860
+
861
+ const FMCPBTSelector Selector = MCPBTReadSelector(Params);
862
+ const bool bIncludeInherited = OptionalBool(Params, TEXT("includeInherited"), false);
863
+ const TSet<FString> Filter = MCPBTPropertyFilter(Params);
864
+
865
+ TArray<TSharedPtr<FJsonValue>> Out;
866
+ for (const FMCPBTNodeRef& Ref : Nodes)
867
+ {
868
+ if (!MCPBTSelectorMatches(Selector, Ref)) continue;
869
+ Out.Add(MakeShared<FJsonValueObject>(
870
+ DescribeBTNode(Ref.Node, Ref.Path, Ref.ParentPath, /*bIncludeProperties*/ true, bIncludeInherited, Filter)));
871
+ }
872
+
873
+ auto Result = MCPSuccess();
874
+ Result->SetStringField(TEXT("assetPath"), AssetPath);
875
+ Result->SetNumberField(TEXT("nodeCount"), Nodes.Num());
876
+ Result->SetNumberField(TEXT("matchCount"), Out.Num());
877
+ Result->SetArrayField(TEXT("nodes"), Out);
878
+ if (Out.Num() == 0 && Nodes.Num() > 0)
879
+ {
880
+ Result->SetStringField(TEXT("availableNodes"), MCPBTDescribePaths(Nodes));
881
+ }
882
+ return MCPResult(Result);
883
+ }
884
+
885
+ // -----------------------------------------------------------------
886
+ // list_bt_tasks (#940)
887
+ // -----------------------------------------------------------------
888
+
889
+ TSharedPtr<FJsonValue> FGameplayHandlers::ListBTTasks(const TSharedPtr<FJsonObject>& Params)
890
+ {
891
+ const FString AssetPath = OptionalString(Params, TEXT("assetPath")).TrimStartAndEnd();
892
+ FString Directory = OptionalString(Params, TEXT("directory")).TrimStartAndEnd();
893
+ const bool bRecursive = OptionalBool(Params, TEXT("recursive"), true);
894
+ const bool bFilterClassOnly = OptionalBool(Params, TEXT("filterClassOnly"), false);
895
+ const int32 Limit = FMath::Clamp(OptionalInt(Params, TEXT("limit"), 200), 1, 5000);
896
+
897
+ TArray<UBehaviorTree*> Trees;
898
+ TArray<TSharedPtr<FJsonValue>> Unloadable;
899
+
900
+ if (!AssetPath.IsEmpty())
901
+ {
902
+ UBehaviorTree* BT = MCPBTLoad(AssetPath);
903
+ if (!BT) return MCPError(FString::Printf(TEXT("BehaviorTree not found: %s"), *AssetPath));
904
+ Trees.Add(BT);
905
+ }
906
+ else
907
+ {
908
+ while (Directory.EndsWith(TEXT("/"))) Directory.LeftChopInline(1);
909
+
910
+ IAssetRegistry& AR = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry").Get();
911
+ TArray<FAssetData> Assets;
912
+ AR.GetAssetsByClass(FTopLevelAssetPath(TEXT("/Script/AIModule"), TEXT("BehaviorTree")), Assets, true);
913
+
914
+ for (const FAssetData& Data : Assets)
915
+ {
916
+ if (Trees.Num() >= Limit) break;
917
+ if (!Directory.IsEmpty())
918
+ {
919
+ const FString PackagePath = Data.PackagePath.ToString();
920
+ const bool bInScope = bRecursive
921
+ ? (PackagePath == Directory || PackagePath.StartsWith(Directory + TEXT("/")))
922
+ : (PackagePath == Directory);
923
+ if (!bInScope) continue;
924
+ }
925
+ if (UBehaviorTree* BT = Cast<UBehaviorTree>(Data.GetAsset()))
926
+ {
927
+ Trees.Add(BT);
928
+ }
929
+ else
930
+ {
931
+ Unloadable.Add(MakeShared<FJsonValueString>(Data.GetObjectPathString()));
932
+ }
933
+ }
934
+ }
935
+
936
+ const FMCPBTSelector Selector = MCPBTReadSelector(Params);
937
+ const FString TaskClassSpec = OptionalString(Params, TEXT("taskClass")).TrimStartAndEnd();
938
+ UClass* TaskClass = TaskClassSpec.IsEmpty() ? nullptr : MCPResolveClass(TaskClassSpec, /*bAllowLoad*/ true);
939
+
940
+ TArray<TSharedPtr<FJsonValue>> Out;
941
+ for (UBehaviorTree* BT : Trees)
942
+ {
943
+ TArray<FMCPBTNodeRef> Nodes;
944
+ MCPBTCollectTree(BT, Nodes);
945
+ for (const FMCPBTNodeRef& Ref : Nodes)
946
+ {
947
+ if (Ref.Kind != TEXT("task")) continue;
948
+ if (!MCPBTSelectorMatches(Selector, Ref)) continue;
949
+ if (!TaskClassSpec.IsEmpty())
950
+ {
951
+ const bool bMatches = TaskClass
952
+ ? Ref.Node->IsA(TaskClass)
953
+ : Ref.Node->GetClass()->GetName().Contains(TaskClassSpec);
954
+ if (!bMatches) continue;
955
+ }
956
+
957
+ UBTNode* Node = Ref.Node;
958
+ UClass* NodeClass = Node->GetClass();
959
+
960
+ TSharedPtr<FJsonObject> Entry = MakeShared<FJsonObject>();
961
+ Entry->SetStringField(TEXT("assetPath"), BT->GetPathName());
962
+ Entry->SetStringField(TEXT("path"), Ref.Path);
963
+ Entry->SetStringField(TEXT("parentPath"), Ref.ParentPath);
964
+ Entry->SetStringField(TEXT("class"), NodeClass->GetName());
965
+ Entry->SetStringField(TEXT("classPath"), NodeClass->GetPathName());
966
+ Entry->SetStringField(TEXT("objectName"), Node->GetName());
967
+ Entry->SetStringField(TEXT("name"), Node->GetName());
968
+ Entry->SetStringField(TEXT("nodeName"), Node->NodeName);
969
+ Entry->SetStringField(TEXT("staticDescription"), Node->GetStaticDescription());
970
+
971
+ // #940: on UE 5.8 FilterClass is a FValueOrBBKey_Class, so the
972
+ // literal lives in DefaultValue and a blackboard binding in Key.
973
+ // Earlier engines declare a bare TSubclassOf, which is read here
974
+ // into the same shape so a caller writes one thing either way.
975
+ bool bHasFilterClass = false;
976
+ FProperty* FilterProp = NodeClass->FindPropertyByName(TEXT("FilterClass"));
977
+ if (FStructProperty* FilterStruct = CastField<FStructProperty>(FilterProp))
978
+ {
979
+ if (MCPBTValueOrBBKeyDefault(FilterStruct))
980
+ {
981
+ Entry->SetObjectField(TEXT("filterClass"),
982
+ MCPBTDescribeValueOrBBKey(FilterStruct, FilterStruct->ContainerPtrToValuePtr<void>(Node)));
983
+ bHasFilterClass = true;
984
+ }
985
+ }
986
+ else if (FObjectPropertyBase* FilterObject = CastField<FObjectPropertyBase>(FilterProp))
987
+ {
988
+ UObject* Value = FilterObject->GetObjectPropertyValue(FilterObject->ContainerPtrToValuePtr<void>(Node));
989
+ TSharedPtr<FJsonObject> FilterJson = MakeShared<FJsonObject>();
990
+ FilterJson->SetStringField(TEXT("struct"), FString());
991
+ FilterJson->SetStringField(TEXT("key"), FString());
992
+ FilterJson->SetBoolField(TEXT("isBound"), false);
993
+ FilterJson->SetStringField(TEXT("defaultValue"), Value ? Value->GetPathName() : FString());
994
+ FilterJson->SetStringField(TEXT("defaultValueName"), Value ? Value->GetName() : FString());
995
+ Entry->SetObjectField(TEXT("filterClass"), FilterJson);
996
+ bHasFilterClass = true;
997
+ }
998
+ Entry->SetBoolField(TEXT("hasFilterClass"), bHasFilterClass);
999
+
1000
+ if (bFilterClassOnly && !bHasFilterClass) continue;
1001
+ Out.Add(MakeShared<FJsonValueObject>(Entry));
1002
+ }
1003
+ }
1004
+
1005
+ auto Result = MCPSuccess();
1006
+ Result->SetNumberField(TEXT("assetCount"), Trees.Num());
1007
+ Result->SetNumberField(TEXT("taskCount"), Out.Num());
1008
+ Result->SetArrayField(TEXT("tasks"), Out);
1009
+ if (Unloadable.Num() > 0) Result->SetArrayField(TEXT("unloadableAssets"), Unloadable);
1010
+ return MCPResult(Result);
1011
+ }
1012
+
1013
+ // -----------------------------------------------------------------
1014
+ // set_bt_node_property / set_bt_task_property (#919 / #940)
1015
+ // -----------------------------------------------------------------
1016
+
1017
+ TSharedPtr<FJsonValue> FGameplayHandlers::SetBTNodeProperty(const TSharedPtr<FJsonObject>& Params)
1018
+ {
1019
+ FString AssetPath;
1020
+ if (auto Err = RequireStringAlt(Params, TEXT("assetPath"), TEXT("path"), AssetPath)) return Err;
1021
+
1022
+ UBehaviorTree* BT = MCPBTLoad(AssetPath);
1023
+ if (!BT) return MCPError(FString::Printf(TEXT("BehaviorTree not found: %s"), *AssetPath));
1024
+
1025
+ TArray<FMCPBTNodeRef> Nodes;
1026
+ MCPBTCollectTree(BT, Nodes);
1027
+ if (Nodes.Num() == 0)
1028
+ {
1029
+ return MCPError(FString::Printf(TEXT("%s has no nodes to write to."), *AssetPath));
1030
+ }
1031
+
1032
+ const FMCPBTSelector Selector = MCPBTReadSelector(Params);
1033
+ if (Selector.IsEmpty())
1034
+ {
1035
+ return MCPError(FString::Printf(
1036
+ TEXT("Pass nodePath, nodeName, nodeClass or kind to pick the node to write. Available: %s"),
1037
+ *MCPBTDescribePaths(Nodes)));
1038
+ }
1039
+
1040
+ TArray<FMCPBTNodeRef> Matches;
1041
+ for (const FMCPBTNodeRef& Ref : Nodes)
1042
+ {
1043
+ if (MCPBTSelectorMatches(Selector, Ref)) Matches.Add(Ref);
1044
+ }
1045
+ if (Matches.Num() == 0)
1046
+ {
1047
+ return MCPError(FString::Printf(
1048
+ TEXT("No BT node matched. Available: %s"), *MCPBTDescribePaths(Nodes)));
1049
+ }
1050
+ if (Matches.Num() > 1)
1051
+ {
1052
+ return MCPError(FString::Printf(
1053
+ TEXT("%d BT nodes matched; the write targets exactly one. Narrow with nodePath. Matched: %s"),
1054
+ Matches.Num(), *MCPBTDescribePaths(Matches)));
1055
+ }
1056
+
1057
+ UBTNode* Node = Matches[0].Node;
1058
+
1059
+ // Gather the writes: a properties map of path to value, a single
1060
+ // property + value pair, or both.
1061
+ TArray<TPair<FString, TSharedPtr<FJsonValue>>> Writes;
1062
+ const TSharedPtr<FJsonObject>* PropertiesObj = nullptr;
1063
+ if (Params->TryGetObjectField(TEXT("properties"), PropertiesObj) && PropertiesObj && (*PropertiesObj).IsValid())
1064
+ {
1065
+ for (const auto& Pair : (*PropertiesObj)->Values)
1066
+ {
1067
+ Writes.Emplace(Pair.Key, Pair.Value);
1068
+ }
1069
+ }
1070
+ const FString SingleProperty = OptionalString(Params, TEXT("property")).TrimStartAndEnd();
1071
+ if (!SingleProperty.IsEmpty())
1072
+ {
1073
+ TSharedPtr<FJsonValue> SingleValue = Params->TryGetField(TEXT("value"));
1074
+ if (!SingleValue.IsValid())
1075
+ {
1076
+ return MCPError(TEXT("'property' needs a 'value' alongside it (pass null to clear an object reference)."));
1077
+ }
1078
+ Writes.Emplace(SingleProperty, SingleValue);
1079
+ }
1080
+ if (Writes.Num() == 0)
1081
+ {
1082
+ return MCPError(TEXT("Pass 'property' + 'value', or a 'properties' map of property path to value."));
1083
+ }
1084
+
1085
+ // Snapshot every target before touching anything: the export text restores
1086
+ // the value if a later write in the same call fails, and the JSON form is
1087
+ // what the caller sees as previousValue and what rollback replays.
1088
+ struct FMCPBTWriteSnapshot
1089
+ {
1090
+ FString PropertyPath;
1091
+ FString PreviousText;
1092
+ TSharedPtr<FJsonValue> PreviousJson;
1093
+ bool bValueOrBBKey = false;
1094
+ };
1095
+ TArray<FMCPBTWriteSnapshot> Snapshots;
1096
+ for (const TPair<FString, TSharedPtr<FJsonValue>>& Write : Writes)
1097
+ {
1098
+ FProperty* Prop = nullptr;
1099
+ void* Addr = nullptr;
1100
+ UObject* LeafOwner = nullptr;
1101
+ FString ResolveError;
1102
+ if (!MCPJsonProperty::ResolveDottedPath(Node, Write.Key, Prop, Addr, LeafOwner, ResolveError))
1103
+ {
1104
+ return MCPError(FString::Printf(
1105
+ TEXT("%s on %s (%s): %s"),
1106
+ *Write.Key, *Matches[0].Path, *Node->GetClass()->GetName(), *ResolveError));
1107
+ }
1108
+
1109
+ FMCPBTWriteSnapshot Snapshot;
1110
+ Snapshot.PropertyPath = Write.Key;
1111
+ Prop->ExportTextItem_Direct(Snapshot.PreviousText, Addr, nullptr, Node, PPF_None);
1112
+ FString ReadError;
1113
+ Snapshot.PreviousJson = ReadBTNodeProperty(Node, Write.Key, ReadError);
1114
+ if (FStructProperty* StructProp = CastField<FStructProperty>(Prop))
1115
+ {
1116
+ Snapshot.bValueOrBBKey = MCPBTValueOrBBKeyDefault(StructProp) != nullptr;
1117
+ }
1118
+ Snapshots.Add(MoveTemp(Snapshot));
1119
+ }
1120
+
1121
+ BT->Modify();
1122
+ Node->Modify();
1123
+
1124
+ for (int32 i = 0; i < Writes.Num(); ++i)
1125
+ {
1126
+ FString WriteError;
1127
+ if (WriteBTNodeProperty(Node, Writes[i].Key, Writes[i].Value, WriteError))
1128
+ {
1129
+ continue;
1130
+ }
1131
+
1132
+ // Put back every write this call already made. A partially applied
1133
+ // batch is worse than a rejected one: the caller would have to guess
1134
+ // which half landed.
1135
+ for (int32 j = i - 1; j >= 0; --j)
1136
+ {
1137
+ FProperty* Prop = nullptr;
1138
+ void* Addr = nullptr;
1139
+ UObject* LeafOwner = nullptr;
1140
+ FString ResolveError;
1141
+ if (MCPJsonProperty::ResolveDottedPath(Node, Snapshots[j].PropertyPath, Prop, Addr, LeafOwner, ResolveError))
1142
+ {
1143
+ FString RestoreError;
1144
+ MCPPropertyText::ImportTextIntoProperty(Prop, Addr, Snapshots[j].PreviousText, Node, RestoreError);
1145
+ }
1146
+ }
1147
+ return MCPError(FString::Printf(
1148
+ TEXT("%s on %s (%s): %s"),
1149
+ *Writes[i].Key, *Matches[0].Path, *Node->GetClass()->GetName(), *WriteError));
1150
+ }
1151
+
1152
+ Node->PostEditChange();
1153
+ BT->PostEditChange();
1154
+ const bool bSaved = SaveAssetPackage(BT);
1155
+
1156
+ // #940 asks for the write to be proved rather than asserted: every target
1157
+ // is read back off the node and reported next to what it held before.
1158
+ TArray<TSharedPtr<FJsonValue>> Applied;
1159
+ TSharedPtr<FJsonObject> RollbackProperties = MakeShared<FJsonObject>();
1160
+ for (const FMCPBTWriteSnapshot& Snapshot : Snapshots)
1161
+ {
1162
+ FString ReadError;
1163
+ TSharedPtr<FJsonValue> Current = ReadBTNodeProperty(Node, Snapshot.PropertyPath, ReadError);
1164
+
1165
+ TSharedPtr<FJsonObject> Entry = MakeShared<FJsonObject>();
1166
+ Entry->SetStringField(TEXT("property"), Snapshot.PropertyPath);
1167
+ if (Snapshot.PreviousJson.IsValid()) Entry->SetField(TEXT("previousValue"), Snapshot.PreviousJson);
1168
+ if (Current.IsValid()) Entry->SetField(TEXT("value"), Current);
1169
+ else if (!ReadError.IsEmpty()) Entry->SetStringField(TEXT("readBackError"), ReadError);
1170
+ Applied.Add(MakeShared<FJsonValueObject>(Entry));
1171
+
1172
+ // A FValueOrBBKey_* previous value is a description, not something the
1173
+ // setter accepts back, so rollback names its two halves directly.
1174
+ if (Snapshot.bValueOrBBKey && Snapshot.PreviousJson.IsValid() && Snapshot.PreviousJson->Type == EJson::Object)
1175
+ {
1176
+ const TSharedPtr<FJsonObject> Previous = Snapshot.PreviousJson->AsObject();
1177
+ if (const TSharedPtr<FJsonValue> DefaultValue = Previous->TryGetField(TEXT("defaultValue")))
1178
+ {
1179
+ RollbackProperties->SetField(Snapshot.PropertyPath + TEXT(".DefaultValue"), DefaultValue);
1180
+ }
1181
+ FString PreviousKey;
1182
+ Previous->TryGetStringField(TEXT("key"), PreviousKey);
1183
+ RollbackProperties->SetStringField(Snapshot.PropertyPath + TEXT(".Key"),
1184
+ PreviousKey.IsEmpty() ? FString(TEXT("None")) : PreviousKey);
1185
+ }
1186
+ else if (Snapshot.PreviousJson.IsValid())
1187
+ {
1188
+ RollbackProperties->SetField(Snapshot.PropertyPath, Snapshot.PreviousJson);
1189
+ }
1190
+ }
1191
+
1192
+ auto Result = MCPSuccess();
1193
+ MCPSetUpdated(Result);
1194
+ Result->SetStringField(TEXT("assetPath"), AssetPath);
1195
+ Result->SetStringField(TEXT("nodePath"), Matches[0].Path);
1196
+ Result->SetStringField(TEXT("nodeClass"), Node->GetClass()->GetName());
1197
+ Result->SetStringField(TEXT("objectName"), Node->GetName());
1198
+ Result->SetNumberField(TEXT("appliedCount"), Applied.Num());
1199
+ Result->SetArrayField(TEXT("applied"), Applied);
1200
+ Result->SetBoolField(TEXT("saved"), bSaved);
1201
+
1202
+ if (RollbackProperties->Values.Num() > 0)
1203
+ {
1204
+ TSharedPtr<FJsonObject> Payload = MakeShared<FJsonObject>();
1205
+ Payload->SetStringField(TEXT("assetPath"), AssetPath);
1206
+ Payload->SetStringField(TEXT("nodePath"), Matches[0].Path);
1207
+ Payload->SetObjectField(TEXT("properties"), RollbackProperties);
1208
+ MCPSetRollback(Result, TEXT("set_bt_node_property"), Payload);
1209
+ }
1210
+ return MCPResult(Result);
1211
+ }