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,900 @@
1
+ // Copyright Epic Games, Inc. All Rights Reserved.
2
+
3
+ #include "AnimationHandlers.h"
4
+ #include "HandlerUtils.h"
5
+
6
+ #if UE_MCP_HAS_5_8_API
7
+ #include "Editor.h"
8
+ #include "EditorAssetLibrary.h"
9
+ #include "Engine/SkeletalMesh.h"
10
+ #include "Rig/IKRigDataTypes.h"
11
+ #include "Rig/IKRigDefinition.h"
12
+ #include "Rig/IKRigSkeleton.h"
13
+ #include "Rig/Solvers/IKRigFullBodyIK.h"
14
+ #include "RigEditor/IKRigAutoCharacterizer.h"
15
+ #include "RigEditor/IKRigController.h"
16
+ #include "ScopedTransaction.h"
17
+ #include "StructUtils/InstancedStruct.h"
18
+ #endif
19
+
20
+ namespace UE_MCP_IKRigAuthoring
21
+ {
22
+ static TSharedPtr<FJsonValue> Error(
23
+ const FString& Code,
24
+ const FString& Message,
25
+ const TOptional<bool>& RollbackSucceeded = TOptional<bool>())
26
+ {
27
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
28
+ Result->SetBoolField(TEXT("success"), false);
29
+ Result->SetStringField(TEXT("errorCode"), Code);
30
+ Result->SetStringField(TEXT("error"), Message);
31
+ Result->SetBoolField(TEXT("rollbackSafe"), !RollbackSucceeded.IsSet() || RollbackSucceeded.GetValue());
32
+ if (RollbackSucceeded.IsSet())
33
+ {
34
+ Result->SetBoolField(TEXT("rollbackAttempted"), true);
35
+ Result->SetBoolField(TEXT("rollbackSucceeded"), RollbackSucceeded.GetValue());
36
+ }
37
+ return MCPResult(Result);
38
+ }
39
+
40
+ #if UE_MCP_HAS_5_8_API
41
+
42
+ constexpr int32 MaxChains = 256;
43
+ constexpr int32 MaxGoals = 256;
44
+ constexpr int32 MaxExclusions = 2048;
45
+
46
+ struct FChainRequest
47
+ {
48
+ FName Name;
49
+ FName StartBone;
50
+ FName EndBone;
51
+ FName Goal;
52
+ };
53
+
54
+ struct FGoalRequest
55
+ {
56
+ FName Name;
57
+ FName Bone;
58
+ TOptional<float> PositionAlpha;
59
+ TOptional<float> RotationAlpha;
60
+ TOptional<int32> ChainDepth;
61
+ TOptional<float> StrengthAlpha;
62
+ TOptional<float> PullChainAlpha;
63
+ TOptional<float> PinRotation;
64
+ };
65
+
66
+ struct FFullBodyRequest
67
+ {
68
+ bool bPresent = false;
69
+ TOptional<int32> SolverIndex;
70
+ FName RootBone;
71
+ TOptional<bool> bEnabled;
72
+ TArray<FGoalRequest> Goals;
73
+ };
74
+
75
+ struct FExclusionRequest
76
+ {
77
+ FName Bone;
78
+ bool bExcluded = false;
79
+ };
80
+
81
+ static bool ReadRequiredString(
82
+ const TSharedPtr<FJsonObject>& Object,
83
+ const TCHAR* Field,
84
+ FString& Out,
85
+ FString& OutError)
86
+ {
87
+ if (!Object.IsValid() || !Object->TryGetStringField(Field, Out))
88
+ {
89
+ OutError = FString::Printf(TEXT("'%s' must be a string"), Field);
90
+ return false;
91
+ }
92
+ Out.TrimStartAndEndInline();
93
+ if (Out.IsEmpty())
94
+ {
95
+ OutError = FString::Printf(TEXT("'%s' must not be empty"), Field);
96
+ return false;
97
+ }
98
+ return true;
99
+ }
100
+
101
+ static bool ReadOptionalUnitFloat(
102
+ const TSharedPtr<FJsonObject>& Object,
103
+ const TCHAR* Field,
104
+ TOptional<float>& Out,
105
+ FString& OutError)
106
+ {
107
+ if (!Object->HasField(Field))
108
+ {
109
+ return true;
110
+ }
111
+ double Value = 0.0;
112
+ if (!Object->TryGetNumberField(Field, Value) || !FMath::IsFinite(Value) || Value < 0.0 || Value > 1.0)
113
+ {
114
+ OutError = FString::Printf(TEXT("'%s' must be a finite number in [0, 1]"), Field);
115
+ return false;
116
+ }
117
+ Out = static_cast<float>(Value);
118
+ return true;
119
+ }
120
+
121
+ static bool ReadOptionalNonNegativeInteger(
122
+ const TSharedPtr<FJsonObject>& Object,
123
+ const TCHAR* Field,
124
+ TOptional<int32>& Out,
125
+ FString& OutError)
126
+ {
127
+ if (!Object->HasField(Field))
128
+ {
129
+ return true;
130
+ }
131
+ double Value = 0.0;
132
+ if (!Object->TryGetNumberField(Field, Value) || !FMath::IsFinite(Value) ||
133
+ Value < 0.0 || Value > static_cast<double>(MAX_int32) || Value != FMath::TruncToDouble(Value))
134
+ {
135
+ OutError = FString::Printf(TEXT("'%s' must be a non-negative integer"), Field);
136
+ return false;
137
+ }
138
+ Out = static_cast<int32>(Value);
139
+ return true;
140
+ }
141
+
142
+ static bool ReadIKRigOptionalBool(
143
+ const TSharedPtr<FJsonObject>& Object,
144
+ const TCHAR* Field,
145
+ TOptional<bool>& Out,
146
+ FString& OutError)
147
+ {
148
+ if (!Object->HasField(Field))
149
+ {
150
+ return true;
151
+ }
152
+ bool Value = false;
153
+ if (!Object->TryGetBoolField(Field, Value))
154
+ {
155
+ OutError = FString::Printf(TEXT("'%s' must be a boolean"), Field);
156
+ return false;
157
+ }
158
+ Out = Value;
159
+ return true;
160
+ }
161
+
162
+ static bool IsFullBodySolver(const UIKRigController* Controller, const int32 SolverIndex)
163
+ {
164
+ FInstancedStruct* SolverStruct = Controller->GetSolverStructAtIndex(SolverIndex);
165
+ return SolverStruct && SolverStruct->GetScriptStruct() == FIKRigFullBodyIKSolver::StaticStruct();
166
+ }
167
+
168
+ static FIKRigFullBodyIKSolver* GetFullBodySolver(const UIKRigController* Controller, const int32 SolverIndex)
169
+ {
170
+ return IsFullBodySolver(Controller, SolverIndex)
171
+ ? static_cast<FIKRigFullBodyIKSolver*>(Controller->GetSolverAtIndex(SolverIndex))
172
+ : nullptr;
173
+ }
174
+
175
+ static bool GoalNameIsSafe(const FString& Name)
176
+ {
177
+ FString Sanitized = Name;
178
+ UIKRigController::SanitizeGoalName(Sanitized);
179
+ return Sanitized == Name && !FName(*Name).IsNone();
180
+ }
181
+
182
+ #endif
183
+ }
184
+
185
+ TSharedPtr<FJsonValue> FAnimationHandlers::ConfigureIKRig(const TSharedPtr<FJsonObject>& Params)
186
+ {
187
+ #if !UE_MCP_HAS_5_8_API
188
+ return UE_MCP_IKRigAuthoring::Error(
189
+ TEXT("unsupported_engine_version"),
190
+ TEXT("IK Rig authoring requires Unreal Engine 5.8 or newer"));
191
+ #else
192
+ using namespace UE_MCP_IKRigAuthoring;
193
+ using UE_MCP_IKRigAuthoring::Error;
194
+
195
+ if (!Params.IsValid())
196
+ {
197
+ return Error(TEXT("invalid_params"), TEXT("Parameters are required"));
198
+ }
199
+
200
+ FString ParseError;
201
+ FString RigPath;
202
+ if (!ReadRequiredString(Params, TEXT("rigPath"), RigPath, ParseError))
203
+ {
204
+ return Error(TEXT("invalid_params"), ParseError);
205
+ }
206
+ if (MCPIsProtectedAssetPath(RigPath))
207
+ {
208
+ return Error(TEXT("protected_asset"), FString::Printf(TEXT("Protected asset cannot be modified: %s"), *RigPath));
209
+ }
210
+
211
+ FString AutoSetup;
212
+ if (Params->HasField(TEXT("autoSetup")))
213
+ {
214
+ if (!Params->TryGetStringField(TEXT("autoSetup"), AutoSetup) ||
215
+ (AutoSetup != TEXT("retarget") && AutoSetup != TEXT("full_body")))
216
+ {
217
+ return Error(TEXT("invalid_params"), TEXT("'autoSetup' must be 'retarget' or 'full_body'"));
218
+ }
219
+ }
220
+ const bool bAutoRetarget = AutoSetup == TEXT("retarget") || AutoSetup == TEXT("full_body");
221
+ const bool bAutoFullBody = AutoSetup == TEXT("full_body");
222
+
223
+ TOptional<FName> RetargetRoot;
224
+ if (Params->HasField(TEXT("retargetRoot")))
225
+ {
226
+ FString Value;
227
+ if (!ReadRequiredString(Params, TEXT("retargetRoot"), Value, ParseError))
228
+ {
229
+ return Error(TEXT("invalid_params"), ParseError);
230
+ }
231
+ RetargetRoot = FName(*Value);
232
+ }
233
+
234
+ TOptional<FName> RootMotionBone;
235
+ if (Params->HasField(TEXT("rootMotionBone")))
236
+ {
237
+ FString Value;
238
+ if (!ReadRequiredString(Params, TEXT("rootMotionBone"), Value, ParseError))
239
+ {
240
+ return Error(TEXT("invalid_params"), ParseError);
241
+ }
242
+ RootMotionBone = FName(*Value);
243
+ }
244
+
245
+ TArray<FChainRequest> Chains;
246
+ TSet<FName> RequestedChainNames;
247
+ if (Params->HasField(TEXT("chains")))
248
+ {
249
+ const TArray<TSharedPtr<FJsonValue>>* Values = nullptr;
250
+ if (!Params->TryGetArrayField(TEXT("chains"), Values) || !Values || Values->Num() > MaxChains)
251
+ {
252
+ return Error(TEXT("invalid_params"), FString::Printf(TEXT("'chains' must be an array with at most %d entries"), MaxChains));
253
+ }
254
+ for (int32 Index = 0; Index < Values->Num(); ++Index)
255
+ {
256
+ if (!(*Values)[Index].IsValid() || (*Values)[Index]->Type != EJson::Object)
257
+ {
258
+ return Error(TEXT("invalid_params"), FString::Printf(TEXT("chains[%d] must be an object"), Index));
259
+ }
260
+ const TSharedPtr<FJsonObject> Object = (*Values)[Index]->AsObject();
261
+ FString Name;
262
+ FString StartBone;
263
+ FString EndBone;
264
+ if (!ReadRequiredString(Object, TEXT("name"), Name, ParseError) ||
265
+ !ReadRequiredString(Object, TEXT("startBone"), StartBone, ParseError) ||
266
+ !ReadRequiredString(Object, TEXT("endBone"), EndBone, ParseError))
267
+ {
268
+ return Error(TEXT("invalid_params"), FString::Printf(TEXT("chains[%d]: %s"), Index, *ParseError));
269
+ }
270
+ FString Goal;
271
+ if (Object->HasField(TEXT("goal")) && !Object->TryGetStringField(TEXT("goal"), Goal))
272
+ {
273
+ return Error(TEXT("invalid_params"), FString::Printf(TEXT("chains[%d].goal must be a string"), Index));
274
+ }
275
+ Goal.TrimStartAndEndInline();
276
+ FChainRequest Request{FName(*Name), FName(*StartBone), FName(*EndBone), Goal.IsEmpty() ? NAME_None : FName(*Goal)};
277
+ if (Request.Name.IsNone() || RequestedChainNames.Contains(Request.Name))
278
+ {
279
+ return Error(TEXT("invalid_params"), FString::Printf(TEXT("chains[%d].name must be non-empty and unique"), Index));
280
+ }
281
+ RequestedChainNames.Add(Request.Name);
282
+ Chains.Add(Request);
283
+ }
284
+ }
285
+
286
+ FFullBodyRequest FullBody;
287
+ if (Params->HasField(TEXT("fullBodyIK")))
288
+ {
289
+ const TSharedPtr<FJsonObject>* ObjectPtr = nullptr;
290
+ if (!Params->TryGetObjectField(TEXT("fullBodyIK"), ObjectPtr) || !ObjectPtr || !ObjectPtr->IsValid())
291
+ {
292
+ return Error(TEXT("invalid_params"), TEXT("'fullBodyIK' must be an object"));
293
+ }
294
+ const TSharedPtr<FJsonObject> Object = *ObjectPtr;
295
+ FullBody.bPresent = true;
296
+ FString RootBone;
297
+ if (!ReadRequiredString(Object, TEXT("rootBone"), RootBone, ParseError))
298
+ {
299
+ return Error(TEXT("invalid_params"), FString::Printf(TEXT("fullBodyIK: %s"), *ParseError));
300
+ }
301
+ FullBody.RootBone = FName(*RootBone);
302
+ if (!ReadOptionalNonNegativeInteger(Object, TEXT("solverIndex"), FullBody.SolverIndex, ParseError) ||
303
+ !ReadIKRigOptionalBool(Object, TEXT("enabled"), FullBody.bEnabled, ParseError))
304
+ {
305
+ return Error(TEXT("invalid_params"), FString::Printf(TEXT("fullBodyIK: %s"), *ParseError));
306
+ }
307
+
308
+ const TArray<TSharedPtr<FJsonValue>>* GoalValues = nullptr;
309
+ if (!Object->TryGetArrayField(TEXT("goals"), GoalValues) || !GoalValues || GoalValues->Num() > MaxGoals)
310
+ {
311
+ return Error(TEXT("invalid_params"), FString::Printf(TEXT("fullBodyIK.goals must be an array with at most %d entries"), MaxGoals));
312
+ }
313
+ TSet<FName> RequestedGoalNames;
314
+ for (int32 Index = 0; Index < GoalValues->Num(); ++Index)
315
+ {
316
+ if (!(*GoalValues)[Index].IsValid() || (*GoalValues)[Index]->Type != EJson::Object)
317
+ {
318
+ return Error(TEXT("invalid_params"), FString::Printf(TEXT("fullBodyIK.goals[%d] must be an object"), Index));
319
+ }
320
+ const TSharedPtr<FJsonObject> GoalObject = (*GoalValues)[Index]->AsObject();
321
+ FString Name;
322
+ FString Bone;
323
+ if (!ReadRequiredString(GoalObject, TEXT("name"), Name, ParseError) ||
324
+ !ReadRequiredString(GoalObject, TEXT("bone"), Bone, ParseError))
325
+ {
326
+ return Error(TEXT("invalid_params"), FString::Printf(TEXT("fullBodyIK.goals[%d]: %s"), Index, *ParseError));
327
+ }
328
+ if (!GoalNameIsSafe(Name))
329
+ {
330
+ return Error(TEXT("invalid_params"), FString::Printf(TEXT("fullBodyIK.goals[%d].name is not a native-safe IK goal name"), Index));
331
+ }
332
+ FGoalRequest Request;
333
+ Request.Name = FName(*Name);
334
+ Request.Bone = FName(*Bone);
335
+ if (RequestedGoalNames.Contains(Request.Name))
336
+ {
337
+ return Error(TEXT("invalid_params"), FString::Printf(TEXT("Duplicate fullBodyIK goal '%s'"), *Name));
338
+ }
339
+ RequestedGoalNames.Add(Request.Name);
340
+ if (!ReadOptionalUnitFloat(GoalObject, TEXT("positionAlpha"), Request.PositionAlpha, ParseError) ||
341
+ !ReadOptionalUnitFloat(GoalObject, TEXT("rotationAlpha"), Request.RotationAlpha, ParseError) ||
342
+ !ReadOptionalNonNegativeInteger(GoalObject, TEXT("chainDepth"), Request.ChainDepth, ParseError) ||
343
+ !ReadOptionalUnitFloat(GoalObject, TEXT("strengthAlpha"), Request.StrengthAlpha, ParseError) ||
344
+ !ReadOptionalUnitFloat(GoalObject, TEXT("pullChainAlpha"), Request.PullChainAlpha, ParseError) ||
345
+ !ReadOptionalUnitFloat(GoalObject, TEXT("pinRotation"), Request.PinRotation, ParseError))
346
+ {
347
+ return Error(TEXT("invalid_params"), FString::Printf(TEXT("fullBodyIK.goals[%d]: %s"), Index, *ParseError));
348
+ }
349
+ FullBody.Goals.Add(Request);
350
+ }
351
+ }
352
+
353
+ TArray<FExclusionRequest> Exclusions;
354
+ TSet<FName> RequestedExclusionBones;
355
+ if (Params->HasField(TEXT("exclusions")))
356
+ {
357
+ const TArray<TSharedPtr<FJsonValue>>* Values = nullptr;
358
+ if (!Params->TryGetArrayField(TEXT("exclusions"), Values) || !Values || Values->Num() > MaxExclusions)
359
+ {
360
+ return Error(TEXT("invalid_params"), FString::Printf(TEXT("'exclusions' must be an array with at most %d entries"), MaxExclusions));
361
+ }
362
+ for (int32 Index = 0; Index < Values->Num(); ++Index)
363
+ {
364
+ if (!(*Values)[Index].IsValid() || (*Values)[Index]->Type != EJson::Object)
365
+ {
366
+ return Error(TEXT("invalid_params"), FString::Printf(TEXT("exclusions[%d] must be an object"), Index));
367
+ }
368
+ const TSharedPtr<FJsonObject> Object = (*Values)[Index]->AsObject();
369
+ FString Bone;
370
+ bool bExcluded = false;
371
+ if (!ReadRequiredString(Object, TEXT("bone"), Bone, ParseError) ||
372
+ !Object->TryGetBoolField(TEXT("excluded"), bExcluded))
373
+ {
374
+ return Error(TEXT("invalid_params"), FString::Printf(TEXT("exclusions[%d] requires string 'bone' and boolean 'excluded'"), Index));
375
+ }
376
+ const FName BoneName(*Bone);
377
+ if (RequestedExclusionBones.Contains(BoneName))
378
+ {
379
+ return Error(TEXT("invalid_params"), FString::Printf(TEXT("Duplicate exclusion bone '%s'"), *Bone));
380
+ }
381
+ RequestedExclusionBones.Add(BoneName);
382
+ Exclusions.Add({BoneName, bExcluded});
383
+ }
384
+ }
385
+
386
+ if (!bAutoRetarget && !RetargetRoot.IsSet() && !RootMotionBone.IsSet() &&
387
+ !Params->HasField(TEXT("chains")) && !FullBody.bPresent && !Params->HasField(TEXT("exclusions")))
388
+ {
389
+ return Error(TEXT("invalid_params"), TEXT("No IK Rig configuration was requested"));
390
+ }
391
+
392
+ UIKRigDefinition* Rig = LoadAssetByPath<UIKRigDefinition>(RigPath);
393
+ if (!Rig)
394
+ {
395
+ return Error(TEXT("asset_not_found"), FString::Printf(TEXT("IK Rig not found: %s"), *RigPath));
396
+ }
397
+ UIKRigController* Controller = UIKRigController::GetController(Rig);
398
+ if (!Controller)
399
+ {
400
+ return Error(TEXT("controller_unavailable"), FString::Printf(TEXT("Could not acquire the native IK Rig controller for %s"), *RigPath));
401
+ }
402
+ USkeletalMesh* Mesh = Controller->GetSkeletalMesh();
403
+ const FIKRigSkeleton& Skeleton = Controller->GetIKRigSkeleton();
404
+ if (!Mesh || Skeleton.BoneNames.IsEmpty() || !Controller->IsSkeletalMeshCompatible(Mesh))
405
+ {
406
+ return Error(TEXT("incompatible_rig"), TEXT("The IK Rig must have a non-empty compatible skeletal mesh before it can be configured"));
407
+ }
408
+
409
+ auto RequireBone = [&Skeleton](const FName Bone, const FString& Context, FString& OutError) -> bool
410
+ {
411
+ if (Bone.IsNone() || Skeleton.GetBoneIndexFromName(Bone) == INDEX_NONE)
412
+ {
413
+ OutError = FString::Printf(TEXT("%s references unknown bone '%s'"), *Context, *Bone.ToString());
414
+ return false;
415
+ }
416
+ return true;
417
+ };
418
+
419
+ if ((RetargetRoot.IsSet() && !RequireBone(RetargetRoot.GetValue(), TEXT("retargetRoot"), ParseError)) ||
420
+ (RootMotionBone.IsSet() && !RequireBone(RootMotionBone.GetValue(), TEXT("rootMotionBone"), ParseError)) ||
421
+ (FullBody.bPresent && !RequireBone(FullBody.RootBone, TEXT("fullBodyIK.rootBone"), ParseError)))
422
+ {
423
+ return Error(TEXT("invalid_bone"), ParseError);
424
+ }
425
+
426
+ TMap<FName, FName> AvailableGoals;
427
+ for (const UIKRigEffectorGoal* Goal : Controller->GetAllGoals())
428
+ {
429
+ if (Goal)
430
+ {
431
+ AvailableGoals.Add(Goal->GoalName, Goal->BoneName);
432
+ }
433
+ }
434
+
435
+ FAutoCharacterizeResults AutoResults;
436
+ if (bAutoRetarget)
437
+ {
438
+ Controller->AutoGenerateRetargetDefinition(AutoResults);
439
+ if (!AutoResults.bUsedTemplate)
440
+ {
441
+ return Error(TEXT("auto_setup_unavailable"), TEXT("Unreal could not match this skeleton to a native IK Rig template"));
442
+ }
443
+ const FRetargetDefinition& Definition = AutoResults.AutoRetargetDefinition.RetargetDefinition;
444
+ if (!RequireBone(Definition.PelvisBone, TEXT("autoSetup retarget root"), ParseError))
445
+ {
446
+ return Error(TEXT("invalid_bone"), ParseError);
447
+ }
448
+ for (const FBoneChain& Chain : Definition.BoneChains)
449
+ {
450
+ if (Chain.ChainName.IsNone() ||
451
+ !RequireBone(Chain.StartBone.BoneName, FString::Printf(TEXT("auto chain '%s' start"), *Chain.ChainName.ToString()), ParseError) ||
452
+ !RequireBone(Chain.EndBone.BoneName, FString::Printf(TEXT("auto chain '%s' end"), *Chain.ChainName.ToString()), ParseError) ||
453
+ !Skeleton.IsBoneInDirectLineage(Chain.EndBone.BoneName, Chain.StartBone.BoneName))
454
+ {
455
+ return Error(TEXT("invalid_chain"), ParseError.IsEmpty()
456
+ ? FString::Printf(TEXT("Auto chain '%s' has invalid ancestry"), *Chain.ChainName.ToString())
457
+ : ParseError);
458
+ }
459
+ if (bAutoFullBody && !Chain.IKGoalName.IsNone())
460
+ {
461
+ FString GoalName = Chain.IKGoalName.ToString();
462
+ if (!GoalNameIsSafe(GoalName))
463
+ {
464
+ return Error(TEXT("invalid_goal"), FString::Printf(TEXT("Auto goal '%s' is not a native-safe goal name"), *GoalName));
465
+ }
466
+ AvailableGoals.Add(Chain.IKGoalName, Chain.EndBone.BoneName);
467
+ }
468
+ }
469
+ }
470
+
471
+ if (bAutoFullBody &&
472
+ (Controller->GetNumSolvers() != 0 || !Controller->GetAllGoals().IsEmpty() ||
473
+ !Controller->GetRetargetChains().IsEmpty() || !Skeleton.ExcludedBones.IsEmpty() ||
474
+ !Controller->GetRetargetRoot().IsNone() || !Controller->GetRootMotionBone().IsNone()))
475
+ {
476
+ return Error(TEXT("non_empty_rig"), TEXT("autoSetup 'full_body' is only safe on an empty IK Rig definition"));
477
+ }
478
+
479
+ for (const FGoalRequest& Goal : FullBody.Goals)
480
+ {
481
+ if (!RequireBone(Goal.Bone, FString::Printf(TEXT("goal '%s'"), *Goal.Name.ToString()), ParseError))
482
+ {
483
+ return Error(TEXT("invalid_bone"), ParseError);
484
+ }
485
+ if (Goal.ChainDepth.IsSet() && Goal.ChainDepth.GetValue() > Skeleton.BoneNames.Num())
486
+ {
487
+ return Error(TEXT("invalid_range"), FString::Printf(TEXT("Goal '%s' chainDepth exceeds the skeleton bone count"), *Goal.Name.ToString()));
488
+ }
489
+ AvailableGoals.Add(Goal.Name, Goal.Bone);
490
+ }
491
+
492
+ for (const FChainRequest& Chain : Chains)
493
+ {
494
+ if (!RequireBone(Chain.StartBone, FString::Printf(TEXT("chain '%s' start"), *Chain.Name.ToString()), ParseError) ||
495
+ !RequireBone(Chain.EndBone, FString::Printf(TEXT("chain '%s' end"), *Chain.Name.ToString()), ParseError))
496
+ {
497
+ return Error(TEXT("invalid_bone"), ParseError);
498
+ }
499
+ if (!Skeleton.IsBoneInDirectLineage(Chain.EndBone, Chain.StartBone))
500
+ {
501
+ return Error(TEXT("invalid_chain"), FString::Printf(TEXT("Chain '%s' end bone must descend from its start bone"), *Chain.Name.ToString()));
502
+ }
503
+ if (!Chain.Goal.IsNone() && !AvailableGoals.Contains(Chain.Goal))
504
+ {
505
+ return Error(TEXT("dangling_goal"), FString::Printf(TEXT("Chain '%s' references missing goal '%s'"), *Chain.Name.ToString(), *Chain.Goal.ToString()));
506
+ }
507
+ }
508
+
509
+ TSet<FName> RequiredFBIKBones;
510
+ for (int32 Index = 0; Index < Controller->GetNumSolvers(); ++Index)
511
+ {
512
+ FIKRigFullBodyIKSolver* ExistingSolver = GetFullBodySolver(Controller, Index);
513
+ if (!ExistingSolver) continue;
514
+ const FName ExistingRoot = Controller->GetStartBone(Index);
515
+ if (!ExistingRoot.IsNone()) RequiredFBIKBones.Add(ExistingRoot);
516
+ TSet<FName> ConnectedGoals;
517
+ ExistingSolver->GetRequiredGoals(ConnectedGoals);
518
+ for (const FName GoalName : ConnectedGoals)
519
+ {
520
+ if (const FName* GoalBone = AvailableGoals.Find(GoalName)) RequiredFBIKBones.Add(*GoalBone);
521
+ }
522
+ }
523
+ if (FullBody.bPresent)
524
+ {
525
+ RequiredFBIKBones.Add(FullBody.RootBone);
526
+ for (const FGoalRequest& Goal : FullBody.Goals) RequiredFBIKBones.Add(Goal.Bone);
527
+ }
528
+ if (bAutoFullBody)
529
+ {
530
+ RequiredFBIKBones.Add(AutoResults.AutoRetargetDefinition.RetargetDefinition.PelvisBone);
531
+ for (const FBoneChain& Chain : AutoResults.AutoRetargetDefinition.RetargetDefinition.BoneChains)
532
+ {
533
+ if (const FName* GoalBone = AvailableGoals.Find(Chain.IKGoalName)) RequiredFBIKBones.Add(*GoalBone);
534
+ }
535
+ }
536
+
537
+ for (const FExclusionRequest& Exclusion : Exclusions)
538
+ {
539
+ if (!RequireBone(Exclusion.Bone, TEXT("exclusions"), ParseError))
540
+ {
541
+ return Error(TEXT("invalid_bone"), ParseError);
542
+ }
543
+ if (Exclusion.bExcluded && RequiredFBIKBones.Contains(Exclusion.Bone))
544
+ {
545
+ return Error(TEXT("invalid_exclusion"), FString::Printf(TEXT("Bone '%s' cannot be excluded while an FBIK solver requires it as a root or goal bone"), *Exclusion.Bone.ToString()));
546
+ }
547
+ }
548
+
549
+ int32 SolverIndex = INDEX_NONE;
550
+ bool bCreateSolver = false;
551
+ if (FullBody.bPresent || bAutoFullBody)
552
+ {
553
+ if (bAutoFullBody)
554
+ {
555
+ SolverIndex = 0;
556
+ if (FullBody.SolverIndex.IsSet() && FullBody.SolverIndex.GetValue() != 0)
557
+ {
558
+ return Error(TEXT("invalid_solver"), TEXT("autoSetup 'full_body' creates its FBIK solver at index 0"));
559
+ }
560
+ }
561
+ else if (FullBody.SolverIndex.IsSet())
562
+ {
563
+ SolverIndex = FullBody.SolverIndex.GetValue();
564
+ if (SolverIndex >= Controller->GetNumSolvers() || !IsFullBodySolver(Controller, SolverIndex))
565
+ {
566
+ return Error(TEXT("invalid_solver"), FString::Printf(TEXT("Solver index %d is not an existing Full Body IK solver"), SolverIndex));
567
+ }
568
+ }
569
+ else
570
+ {
571
+ for (int32 Index = 0; Index < Controller->GetNumSolvers(); ++Index)
572
+ {
573
+ if (IsFullBodySolver(Controller, Index))
574
+ {
575
+ SolverIndex = Index;
576
+ break;
577
+ }
578
+ }
579
+ if (SolverIndex == INDEX_NONE)
580
+ {
581
+ SolverIndex = Controller->GetNumSolvers();
582
+ bCreateSolver = true;
583
+ }
584
+ }
585
+
586
+ const FName SolverRoot = FullBody.bPresent
587
+ ? FullBody.RootBone
588
+ : AutoResults.AutoRetargetDefinition.RetargetDefinition.PelvisBone;
589
+ TSet<FName> ConnectedGoals;
590
+ if (!bCreateSolver && !bAutoFullBody)
591
+ {
592
+ if (FIKRigFullBodyIKSolver* ExistingSolver = GetFullBodySolver(Controller, SolverIndex))
593
+ {
594
+ ExistingSolver->GetRequiredGoals(ConnectedGoals);
595
+ }
596
+ }
597
+ if (bAutoFullBody)
598
+ {
599
+ for (const FBoneChain& Chain : AutoResults.AutoRetargetDefinition.RetargetDefinition.BoneChains)
600
+ {
601
+ if (!Chain.IKGoalName.IsNone()) ConnectedGoals.Add(Chain.IKGoalName);
602
+ }
603
+ }
604
+ for (const FGoalRequest& Goal : FullBody.Goals) ConnectedGoals.Add(Goal.Name);
605
+ for (const FName GoalName : ConnectedGoals)
606
+ {
607
+ const FName* GoalBone = AvailableGoals.Find(GoalName);
608
+ if (!GoalBone)
609
+ {
610
+ return Error(TEXT("dangling_goal"), FString::Printf(TEXT("FBIK solver references missing goal '%s'"), *GoalName.ToString()));
611
+ }
612
+ if (!Skeleton.IsBoneInDirectLineage(*GoalBone, SolverRoot))
613
+ {
614
+ return Error(TEXT("invalid_ancestry"), FString::Printf(TEXT("Goal bone '%s' must descend from FBIK root '%s'"), *GoalBone->ToString(), *SolverRoot.ToString()));
615
+ }
616
+ }
617
+ }
618
+
619
+ const int32 BeforeChainCount = Controller->GetRetargetChains().Num();
620
+ const int32 BeforeGoalCount = Controller->GetAllGoals().Num();
621
+ const int32 BeforeSolverCount = Controller->GetNumSolvers();
622
+ const TSet<FName> BeforeExcluded(Skeleton.ExcludedBones);
623
+ UPackage* Package = Rig->GetOutermost();
624
+ const bool bWasDirty = Package && Package->IsDirty();
625
+ bool bFailed = false;
626
+ FString ApplyError;
627
+ TArray<FString> Warnings;
628
+
629
+ {
630
+ FScopedTransaction Transaction(NSLOCTEXT("UE_MCP", "ConfigureIKRig", "Configure IK Rig"));
631
+ FScopedReinitializeIKRig Reinitialize(Controller, true);
632
+ Rig->Modify();
633
+
634
+ if (bAutoRetarget && !Controller->ApplyAutoGeneratedRetargetDefinition())
635
+ {
636
+ bFailed = true;
637
+ ApplyError = TEXT("Native auto retarget setup failed");
638
+ }
639
+ if (!bFailed && bAutoFullBody && !Controller->ApplyAutoFBIK())
640
+ {
641
+ bFailed = true;
642
+ ApplyError = TEXT("Native auto Full Body IK setup failed");
643
+ }
644
+ if (!bFailed && RetargetRoot.IsSet() && Controller->GetRetargetRoot() != RetargetRoot.GetValue() &&
645
+ !Controller->SetRetargetRoot(RetargetRoot.GetValue()))
646
+ {
647
+ bFailed = true;
648
+ ApplyError = TEXT("Failed to set retargetRoot");
649
+ }
650
+ if (!bFailed && RootMotionBone.IsSet() && Controller->GetRootMotionBone() != RootMotionBone.GetValue() &&
651
+ !Controller->SetRootMotionBone(RootMotionBone.GetValue()))
652
+ {
653
+ bFailed = true;
654
+ ApplyError = TEXT("Failed to set rootMotionBone");
655
+ }
656
+
657
+ for (const FGoalRequest& Request : FullBody.Goals)
658
+ {
659
+ if (bFailed) break;
660
+ UIKRigEffectorGoal* Goal = Controller->GetGoal(Request.Name);
661
+ if (!Goal)
662
+ {
663
+ if (Controller->AddNewGoal(Request.Name, Request.Bone) != Request.Name)
664
+ {
665
+ bFailed = true;
666
+ ApplyError = FString::Printf(TEXT("Failed to create goal '%s'"), *Request.Name.ToString());
667
+ break;
668
+ }
669
+ Goal = Controller->GetGoal(Request.Name);
670
+ }
671
+ else if (Goal->BoneName != Request.Bone && !Controller->SetGoalBone(Request.Name, Request.Bone))
672
+ {
673
+ bFailed = true;
674
+ ApplyError = FString::Printf(TEXT("Failed to move goal '%s'"), *Request.Name.ToString());
675
+ break;
676
+ }
677
+ Goal = Controller->GetGoal(Request.Name);
678
+ const bool bCoreSettingsChanged = Goal &&
679
+ ((Request.PositionAlpha.IsSet() && !FMath::IsNearlyEqual(Goal->PositionAlpha, Request.PositionAlpha.GetValue())) ||
680
+ (Request.RotationAlpha.IsSet() && !FMath::IsNearlyEqual(Goal->RotationAlpha, Request.RotationAlpha.GetValue())));
681
+ if (!Goal || (bCoreSettingsChanged && !Controller->ModifyGoal(Request.Name)))
682
+ {
683
+ bFailed = true;
684
+ ApplyError = FString::Printf(TEXT("Failed to modify goal '%s'"), *Request.Name.ToString());
685
+ break;
686
+ }
687
+ if (Request.PositionAlpha.IsSet()) Goal->PositionAlpha = Request.PositionAlpha.GetValue();
688
+ if (Request.RotationAlpha.IsSet()) Goal->RotationAlpha = Request.RotationAlpha.GetValue();
689
+ }
690
+
691
+ for (const FChainRequest& Request : Chains)
692
+ {
693
+ if (bFailed) break;
694
+ const FBoneChain* Existing = Controller->GetRetargetChainByName(Request.Name);
695
+ if (!Existing)
696
+ {
697
+ if (Controller->AddRetargetChain(Request.Name, Request.StartBone, Request.EndBone, Request.Goal) != Request.Name)
698
+ {
699
+ bFailed = true;
700
+ ApplyError = FString::Printf(TEXT("Failed to create chain '%s'"), *Request.Name.ToString());
701
+ }
702
+ continue;
703
+ }
704
+ if (Existing->StartBone.BoneName != Request.StartBone && !Controller->SetRetargetChainStartBone(Request.Name, Request.StartBone))
705
+ {
706
+ bFailed = true;
707
+ ApplyError = FString::Printf(TEXT("Failed to set chain '%s' start bone"), *Request.Name.ToString());
708
+ break;
709
+ }
710
+ Existing = Controller->GetRetargetChainByName(Request.Name);
711
+ if (Existing->EndBone.BoneName != Request.EndBone && !Controller->SetRetargetChainEndBone(Request.Name, Request.EndBone))
712
+ {
713
+ bFailed = true;
714
+ ApplyError = FString::Printf(TEXT("Failed to set chain '%s' end bone"), *Request.Name.ToString());
715
+ break;
716
+ }
717
+ Existing = Controller->GetRetargetChainByName(Request.Name);
718
+ if (Existing->IKGoalName != Request.Goal && !Controller->SetRetargetChainGoal(Request.Name, Request.Goal))
719
+ {
720
+ bFailed = true;
721
+ ApplyError = FString::Printf(TEXT("Failed to set chain '%s' goal"), *Request.Name.ToString());
722
+ }
723
+ }
724
+
725
+ if (!bFailed && (FullBody.bPresent || bAutoFullBody))
726
+ {
727
+ if (bCreateSolver)
728
+ {
729
+ const int32 AddedIndex = Controller->AddSolver(FIKRigFullBodyIKSolver::StaticStruct());
730
+ if (AddedIndex != SolverIndex)
731
+ {
732
+ bFailed = true;
733
+ ApplyError = TEXT("Failed to create the Full Body IK solver at the predicted index");
734
+ }
735
+ }
736
+ FIKRigFullBodyIKSolver* Solver = bFailed ? nullptr : GetFullBodySolver(Controller, SolverIndex);
737
+ if (!Solver)
738
+ {
739
+ bFailed = true;
740
+ ApplyError = TEXT("Full Body IK solver readback failed");
741
+ }
742
+ if (!bFailed && FullBody.bPresent)
743
+ {
744
+ if (Controller->GetStartBone(SolverIndex) != FullBody.RootBone && !Controller->SetStartBone(FullBody.RootBone, SolverIndex))
745
+ {
746
+ bFailed = true;
747
+ ApplyError = TEXT("Failed to set the Full Body IK root bone");
748
+ }
749
+ if (!bFailed && FullBody.bEnabled.IsSet() && Controller->GetSolverEnabled(SolverIndex) != FullBody.bEnabled.GetValue() &&
750
+ !Controller->SetSolverEnabled(SolverIndex, FullBody.bEnabled.GetValue()))
751
+ {
752
+ bFailed = true;
753
+ ApplyError = TEXT("Failed to set the Full Body IK enabled state");
754
+ }
755
+ for (const FGoalRequest& Request : FullBody.Goals)
756
+ {
757
+ if (bFailed) break;
758
+ if (!Controller->IsGoalConnectedToSolver(Request.Name, SolverIndex) &&
759
+ !Controller->ConnectGoalToSolver(Request.Name, SolverIndex))
760
+ {
761
+ bFailed = true;
762
+ ApplyError = FString::Printf(TEXT("Failed to connect goal '%s' to the Full Body IK solver"), *Request.Name.ToString());
763
+ break;
764
+ }
765
+ FIKRigFBIKGoalSettings* Settings = static_cast<FIKRigFBIKGoalSettings*>(Solver->GetGoalSettings(Request.Name));
766
+ if (!Settings)
767
+ {
768
+ bFailed = true;
769
+ ApplyError = FString::Printf(TEXT("Full Body IK settings are missing for goal '%s'"), *Request.Name.ToString());
770
+ break;
771
+ }
772
+ if (Request.ChainDepth.IsSet()) Settings->ChainDepth = Request.ChainDepth.GetValue();
773
+ if (Request.StrengthAlpha.IsSet()) Settings->StrengthAlpha = Request.StrengthAlpha.GetValue();
774
+ if (Request.PullChainAlpha.IsSet()) Settings->PullChainAlpha = Request.PullChainAlpha.GetValue();
775
+ if (Request.PinRotation.IsSet()) Settings->PinRotation = Request.PinRotation.GetValue();
776
+ }
777
+ }
778
+ }
779
+
780
+ for (const FExclusionRequest& Request : Exclusions)
781
+ {
782
+ if (bFailed) break;
783
+ if (Controller->GetBoneExcluded(Request.Bone) != Request.bExcluded &&
784
+ !Controller->SetBoneExcluded(Request.Bone, Request.bExcluded))
785
+ {
786
+ bFailed = true;
787
+ ApplyError = FString::Printf(TEXT("Failed to set exclusion for bone '%s'"), *Request.Bone.ToString());
788
+ }
789
+ }
790
+
791
+ if (!bFailed && RetargetRoot.IsSet() && Controller->GetRetargetRoot() != RetargetRoot.GetValue())
792
+ {
793
+ bFailed = true;
794
+ ApplyError = TEXT("retargetRoot readback did not match");
795
+ }
796
+ if (!bFailed && RootMotionBone.IsSet() && Controller->GetRootMotionBone() != RootMotionBone.GetValue())
797
+ {
798
+ bFailed = true;
799
+ ApplyError = TEXT("rootMotionBone readback did not match");
800
+ }
801
+ for (const FChainRequest& Request : Chains)
802
+ {
803
+ if (bFailed) break;
804
+ const FBoneChain* Chain = Controller->GetRetargetChainByName(Request.Name);
805
+ TSet<int32> ChainIndices;
806
+ if (!Chain || Chain->StartBone.BoneName != Request.StartBone || Chain->EndBone.BoneName != Request.EndBone ||
807
+ Chain->IKGoalName != Request.Goal || !Controller->ValidateChain(Request.Name, &Skeleton, ChainIndices))
808
+ {
809
+ bFailed = true;
810
+ ApplyError = FString::Printf(TEXT("Chain '%s' failed native readback validation"), *Request.Name.ToString());
811
+ }
812
+ }
813
+ for (const FGoalRequest& Request : FullBody.Goals)
814
+ {
815
+ if (bFailed) break;
816
+ const UIKRigEffectorGoal* Goal = Controller->GetGoal(Request.Name);
817
+ FIKRigFullBodyIKSolver* Solver = GetFullBodySolver(Controller, SolverIndex);
818
+ const FIKRigFBIKGoalSettings* Settings = Solver
819
+ ? static_cast<const FIKRigFBIKGoalSettings*>(Solver->GetGoalSettings(Request.Name))
820
+ : nullptr;
821
+ if (!Goal || Goal->BoneName != Request.Bone || !Controller->IsGoalConnectedToSolver(Request.Name, SolverIndex) || !Settings ||
822
+ (Request.PositionAlpha.IsSet() && !FMath::IsNearlyEqual(Goal->PositionAlpha, Request.PositionAlpha.GetValue())) ||
823
+ (Request.RotationAlpha.IsSet() && !FMath::IsNearlyEqual(Goal->RotationAlpha, Request.RotationAlpha.GetValue())) ||
824
+ (Request.ChainDepth.IsSet() && Settings->ChainDepth != Request.ChainDepth.GetValue()) ||
825
+ (Request.StrengthAlpha.IsSet() && !FMath::IsNearlyEqual(Settings->StrengthAlpha, Request.StrengthAlpha.GetValue())) ||
826
+ (Request.PullChainAlpha.IsSet() && !FMath::IsNearlyEqual(Settings->PullChainAlpha, Request.PullChainAlpha.GetValue())) ||
827
+ (Request.PinRotation.IsSet() && !FMath::IsNearlyEqual(Settings->PinRotation, Request.PinRotation.GetValue())))
828
+ {
829
+ bFailed = true;
830
+ ApplyError = FString::Printf(TEXT("Goal '%s' failed native readback validation"), *Request.Name.ToString());
831
+ }
832
+ }
833
+ for (const FExclusionRequest& Request : Exclusions)
834
+ {
835
+ if (!bFailed && Controller->GetBoneExcluded(Request.Bone) != Request.bExcluded)
836
+ {
837
+ bFailed = true;
838
+ ApplyError = FString::Printf(TEXT("Exclusion for bone '%s' failed readback"), *Request.Bone.ToString());
839
+ }
840
+ }
841
+ }
842
+
843
+ auto Undo = [&]() -> bool
844
+ {
845
+ const bool bUndone = GEditor && GEditor->UndoTransaction();
846
+ if (bUndone && Package)
847
+ {
848
+ Package->SetDirtyFlag(bWasDirty);
849
+ FScopedReinitializeIKRig Refresh(Controller, true);
850
+ }
851
+ return bUndone;
852
+ };
853
+
854
+ if (bFailed)
855
+ {
856
+ const bool bUndone = Undo();
857
+ return Error(TEXT("mutation_failed"), ApplyError, bUndone);
858
+ }
859
+
860
+ Rig->MarkPackageDirty();
861
+ if (!UEditorAssetLibrary::SaveLoadedAsset(Rig, false))
862
+ {
863
+ const bool bUndone = Undo();
864
+ return Error(TEXT("save_failed"), TEXT("IK Rig changes could not be saved"), bUndone);
865
+ }
866
+
867
+ const FIKRigSkeleton& FinalSkeleton = Controller->GetIKRigSkeleton();
868
+ const TSet<FName> FinalExcluded(FinalSkeleton.ExcludedBones);
869
+ int32 ExclusionsChanged = 0;
870
+ for (const FName Bone : BeforeExcluded) if (!FinalExcluded.Contains(Bone)) ++ExclusionsChanged;
871
+ for (const FName Bone : FinalExcluded) if (!BeforeExcluded.Contains(Bone)) ++ExclusionsChanged;
872
+
873
+ TSharedPtr<FJsonObject> Result = MCPSuccess();
874
+ MCPSetUpdated(Result);
875
+ Result->SetStringField(TEXT("rigPath"), Rig->GetPathName());
876
+ Result->SetBoolField(TEXT("validated"), true);
877
+ Result->SetBoolField(TEXT("rollbackSafe"), true);
878
+ Result->SetBoolField(TEXT("autoSetupApplied"), bAutoRetarget);
879
+ if (!AutoSetup.IsEmpty()) Result->SetStringField(TEXT("autoSetup"), AutoSetup);
880
+ Result->SetStringField(TEXT("retargetRoot"), Controller->GetRetargetRoot().ToString());
881
+ Result->SetStringField(TEXT("rootMotionBone"), Controller->GetRootMotionBone().ToString());
882
+ Result->SetNumberField(TEXT("chainsCreated"), FMath::Max(0, Controller->GetRetargetChains().Num() - BeforeChainCount));
883
+ Result->SetNumberField(TEXT("chainsUpserted"), Chains.Num());
884
+ Result->SetNumberField(TEXT("goalsCreated"), FMath::Max(0, Controller->GetAllGoals().Num() - BeforeGoalCount));
885
+ Result->SetNumberField(TEXT("goalsUpserted"), FullBody.Goals.Num());
886
+ Result->SetNumberField(TEXT("exclusionsChanged"), ExclusionsChanged);
887
+ Result->SetNumberField(TEXT("chainCount"), Controller->GetRetargetChains().Num());
888
+ Result->SetNumberField(TEXT("goalCount"), Controller->GetAllGoals().Num());
889
+ Result->SetNumberField(TEXT("solverCount"), Controller->GetNumSolvers());
890
+ if (SolverIndex != INDEX_NONE)
891
+ {
892
+ Result->SetNumberField(TEXT("fullBodyIKSolverIndex"), SolverIndex);
893
+ Result->SetBoolField(TEXT("fullBodyIKSolverCreated"), Controller->GetNumSolvers() > BeforeSolverCount);
894
+ }
895
+ TArray<TSharedPtr<FJsonValue>> WarningValues;
896
+ for (const FString& Warning : Warnings) WarningValues.Add(MakeShared<FJsonValueString>(Warning));
897
+ Result->SetArrayField(TEXT("warnings"), WarningValues);
898
+ return MCPResult(Result);
899
+ #endif
900
+ }