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,3061 @@
1
+ // UE 5.8 Control Rig editing through Sequencer.
2
+ //
3
+ // The LevelSequence is the mutable workspace. Input AnimSequences and Control
4
+ // Rig assets are never changed; bake always creates a separate AnimSequence.
5
+
6
+ #include "AnimationHandlers.h"
7
+
8
+ #include "HandlerUtils.h"
9
+
10
+ namespace
11
+ {
12
+ TSharedPtr<FJsonValue> ControlRigSequencerUnsupported()
13
+ {
14
+ auto Result = MakeShared<FJsonObject>();
15
+ Result->SetBoolField(TEXT("success"), false);
16
+ Result->SetStringField(TEXT("errorCode"), TEXT("unsupported_engine_version"));
17
+ Result->SetStringField(TEXT("error"), TEXT("Control Rig Sequencer authoring requires Unreal Engine 5.8 or newer"));
18
+ return MCPResult(Result);
19
+ }
20
+ }
21
+
22
+ #if UE_MCP_HAS_5_8_API
23
+
24
+ #include "HandlerAssetCreate.h"
25
+
26
+ #include "Algo/Reverse.h"
27
+ #include "AnimPose.h"
28
+ #include "FABRIK.h"
29
+ #include "Animation/AnimSequence.h"
30
+ #include "Animation/Skeleton.h"
31
+ #include "Components/SkeletalMeshComponent.h"
32
+ #include "ControlRig.h"
33
+ #include "ControlRigSequencerEditorLibrary.h"
34
+ #include "EditorAssetLibrary.h"
35
+ #include "Editor.h"
36
+ #include "Engine/Blueprint.h"
37
+ #include "Engine/SkeletalMesh.h"
38
+ #include "Engine/SkeletalMeshSocket.h"
39
+ #include "Animation/SkeletalMeshActor.h"
40
+ #include "Exporters/AnimSeqExportOption.h"
41
+ #include "IControlRigObjectBinding.h"
42
+ #include "ILevelSequenceEditorToolkit.h"
43
+ #include "ISequencer.h"
44
+ #include "LevelSequence.h"
45
+ #include "LevelSequenceEditorBlueprintLibrary.h"
46
+ #include "Misc/PackageName.h"
47
+ #include "MovieScene.h"
48
+ #include "MovieSceneBindingProxy.h"
49
+ #include "MovieSceneObjectBindingID.h"
50
+ #include "MovieSceneSpawnable.h"
51
+ #if WITH_DEV_AUTOMATION_TESTS
52
+ #include "Misc/AutomationTest.h"
53
+ #endif
54
+ #include "Rigs/FKControlRig.h"
55
+ #include "Rigs/RigHierarchy.h"
56
+ #include "ScopedTransaction.h"
57
+ #include "Sections/MovieSceneSkeletalAnimationSection.h"
58
+ #include "Sequencer/MovieSceneControlRigParameterSection.h"
59
+ #include "Sequencer/MovieSceneControlRigParameterTrack.h"
60
+ #include "Tracks/MovieSceneSkeletalAnimationTrack.h"
61
+ #include "Tracks/MovieSceneSpawnTrack.h"
62
+ #include "Subsystems/AssetEditorSubsystem.h"
63
+ #include "Units/Execution/RigUnit_InverseExecution.h"
64
+
65
+ namespace
66
+ {
67
+ constexpr int32 ControlRigSequencerMaxFrames = 100000;
68
+
69
+ struct FControlRigSequenceSession
70
+ {
71
+ ULevelSequence* Sequence = nullptr;
72
+ UMovieScene* MovieScene = nullptr;
73
+ FGuid BindingGuid;
74
+ UMovieSceneControlRigParameterTrack* Track = nullptr;
75
+ UMovieSceneControlRigParameterSection* Section = nullptr;
76
+ UControlRig* ControlRig = nullptr;
77
+ FString SequencePath;
78
+ FString BindingTag;
79
+ };
80
+
81
+ struct FControlRigTransformPatch
82
+ {
83
+ bool bTranslation = false;
84
+ bool bRotation = false;
85
+ bool bScale = false;
86
+ FVector Translation = FVector::ZeroVector;
87
+ FQuat Rotation = FQuat::Identity;
88
+ FVector Scale = FVector::OneVector;
89
+ };
90
+
91
+ enum class EControlRigPreparedValueType : uint8
92
+ {
93
+ Transform,
94
+ Bool,
95
+ Float,
96
+ Integer,
97
+ };
98
+
99
+ struct FControlRigPreparedWrite
100
+ {
101
+ FName Control;
102
+ TArray<FFrameNumber> Frames;
103
+ TArray<FTransform> Before;
104
+ TArray<FTransform> After;
105
+ EControlRigTransformSpace Space = EControlRigTransformSpace::Local;
106
+ FString Op;
107
+ EControlRigPreparedValueType ValueType = EControlRigPreparedValueType::Transform;
108
+ bool BoolValue = false;
109
+ float FloatValue = 0.0f;
110
+ int32 IntValue = 0;
111
+ };
112
+
113
+ struct FControlRigContactMetrics
114
+ {
115
+ double MaxPositionErrorCm = 0.0;
116
+ double MaxRotationErrorDegrees = 0.0;
117
+ int32 WorstPositionFrame = 0;
118
+ int32 WorstRotationFrame = 0;
119
+ FTransform WorstPositionExpected = FTransform::Identity;
120
+ FTransform WorstPositionActual = FTransform::Identity;
121
+ };
122
+
123
+ struct FControlRigContactStabilizerQA
124
+ {
125
+ FName Control;
126
+ ERigControlType ControlType = ERigControlType::Transform;
127
+ TArray<FTransform> Expected;
128
+ FControlRigContactMetrics Metrics;
129
+ };
130
+
131
+ struct FControlRigPreparedContactQA
132
+ {
133
+ int32 OperationIndex = INDEX_NONE;
134
+ FName Control;
135
+ FName DrivenReference;
136
+ ERigControlType ControlType = ERigControlType::Transform;
137
+ bool bHasDrivenReference = false;
138
+ bool bUsedFkRotationChain = false;
139
+ bool bCheckRotation = false;
140
+ int32 FullWeightFrameCount = 0;
141
+ double PositionToleranceCm = 0.1;
142
+ double RotationToleranceDegrees = 0.5;
143
+ TArray<FFrameNumber> Frames;
144
+ TArray<FTransform> ExpectedSubject;
145
+ FControlRigContactMetrics Metrics;
146
+ TArray<FControlRigContactStabilizerQA> Stabilizers;
147
+ };
148
+
149
+ class FControlRigSequenceFocusGuard
150
+ {
151
+ public:
152
+ explicit FControlRigSequenceFocusGuard(ULevelSequence* InSequence)
153
+ : Previous(ULevelSequenceEditorBlueprintLibrary::GetCurrentLevelSequence())
154
+ , Target(InSequence)
155
+ {
156
+ if (Target && ULevelSequenceEditorBlueprintLibrary::GetFocusedLevelSequence() != Target)
157
+ {
158
+ bChanged = true;
159
+ bReady = ULevelSequenceEditorBlueprintLibrary::OpenLevelSequence(Target);
160
+ }
161
+ else
162
+ {
163
+ bReady = Target != nullptr;
164
+ }
165
+ if (bReady)
166
+ {
167
+ ULevelSequenceEditorBlueprintLibrary::RefreshCurrentLevelSequence();
168
+ ULevelSequenceEditorBlueprintLibrary::ForceUpdate();
169
+ bReady = ULevelSequenceEditorBlueprintLibrary::GetCurrentLevelSequence() == Target
170
+ && ULevelSequenceEditorBlueprintLibrary::GetFocusedLevelSequence() == Target;
171
+ }
172
+ }
173
+
174
+ ~FControlRigSequenceFocusGuard()
175
+ {
176
+ if (!bChanged)
177
+ {
178
+ return;
179
+ }
180
+ if (Previous)
181
+ {
182
+ ULevelSequenceEditorBlueprintLibrary::OpenLevelSequence(Previous);
183
+ }
184
+ else
185
+ {
186
+ ULevelSequenceEditorBlueprintLibrary::CloseLevelSequence();
187
+ }
188
+ }
189
+
190
+ bool IsReady() const { return bReady; }
191
+
192
+ private:
193
+ TObjectPtr<ULevelSequence> Previous;
194
+ TObjectPtr<ULevelSequence> Target;
195
+ bool bChanged = false;
196
+ bool bReady = false;
197
+ };
198
+
199
+ bool ControlRigSequencerSplitAssetPath(
200
+ const FString& InPath,
201
+ FString& OutPackagePath,
202
+ FString& OutName,
203
+ FString& OutError)
204
+ {
205
+ FString PackageName = InPath;
206
+ PackageName.TrimStartAndEndInline();
207
+ int32 DotIndex = INDEX_NONE;
208
+ if (PackageName.FindLastChar(TEXT('.'), DotIndex))
209
+ {
210
+ PackageName.LeftInline(DotIndex);
211
+ }
212
+ FText InvalidReason;
213
+ if (!FPackageName::IsValidLongPackageName(PackageName, true, &InvalidReason))
214
+ {
215
+ OutError = FString::Printf(TEXT("Invalid asset path '%s': %s"), *InPath, *InvalidReason.ToString());
216
+ return false;
217
+ }
218
+ if (MCPIsProtectedAssetPath(PackageName))
219
+ {
220
+ OutError = FString::Printf(TEXT("Refusing to create or edit protected asset path '%s'"), *PackageName);
221
+ return false;
222
+ }
223
+ OutName = FPackageName::GetLongPackageAssetName(PackageName);
224
+ OutPackagePath = FPackageName::GetLongPackagePath(PackageName);
225
+ if (OutName.IsEmpty() || OutPackagePath.IsEmpty())
226
+ {
227
+ OutError = FString::Printf(TEXT("Asset path must include a package and asset name: '%s'"), *InPath);
228
+ return false;
229
+ }
230
+ return true;
231
+ }
232
+
233
+ bool ControlRigSequencerReadRate(
234
+ const TSharedPtr<FJsonObject>& Params,
235
+ const TCHAR* Field,
236
+ const FFrameRate& Default,
237
+ FFrameRate& OutRate,
238
+ FString& OutError)
239
+ {
240
+ OutRate = Default;
241
+ const TSharedPtr<FJsonValue>* Value = Params->Values.Find(Field);
242
+ if (!Value || !Value->IsValid() || (*Value)->IsNull())
243
+ {
244
+ return true;
245
+ }
246
+ if ((*Value)->Type == EJson::Number)
247
+ {
248
+ const double Number = (*Value)->AsNumber();
249
+ if (!FMath::IsFinite(Number) || Number <= 0.0 || Number > static_cast<double>(MAX_int32))
250
+ {
251
+ OutError = FString::Printf(TEXT("'%s' must be a positive frame rate"), Field);
252
+ return false;
253
+ }
254
+ OutRate = FFrameRate(FMath::RoundToInt(Number), 1);
255
+ return OutRate.IsValid();
256
+ }
257
+ if ((*Value)->Type != EJson::Object)
258
+ {
259
+ OutError = FString::Printf(TEXT("'%s' must be a number or {numerator, denominator}"), Field);
260
+ return false;
261
+ }
262
+ const TSharedPtr<FJsonObject> Object = (*Value)->AsObject();
263
+ double Numerator = 0.0;
264
+ double Denominator = 1.0;
265
+ if (!Object.IsValid() || !Object->TryGetNumberField(TEXT("numerator"), Numerator))
266
+ {
267
+ OutError = FString::Printf(TEXT("'%s.numerator' is required"), Field);
268
+ return false;
269
+ }
270
+ Object->TryGetNumberField(TEXT("denominator"), Denominator);
271
+ if (!FMath::IsFinite(Numerator) || !FMath::IsFinite(Denominator)
272
+ || Numerator <= 0.0 || Denominator <= 0.0
273
+ || Numerator > static_cast<double>(MAX_int32) || Denominator > static_cast<double>(MAX_int32))
274
+ {
275
+ OutError = FString::Printf(TEXT("'%s' numerator and denominator must be positive integers"), Field);
276
+ return false;
277
+ }
278
+ OutRate = FFrameRate(FMath::RoundToInt(Numerator), FMath::RoundToInt(Denominator));
279
+ if (!OutRate.IsValid())
280
+ {
281
+ OutError = FString::Printf(TEXT("'%s' is not a valid frame rate"), Field);
282
+ return false;
283
+ }
284
+ return true;
285
+ }
286
+
287
+ TSharedPtr<FJsonObject> ControlRigSequencerRateJson(const FFrameRate& Rate)
288
+ {
289
+ auto Object = MakeShared<FJsonObject>();
290
+ Object->SetNumberField(TEXT("numerator"), Rate.Numerator);
291
+ Object->SetNumberField(TEXT("denominator"), Rate.Denominator);
292
+ Object->SetNumberField(TEXT("decimal"), Rate.AsDecimal());
293
+ return Object;
294
+ }
295
+
296
+ bool ControlRigSequencerReadVector(
297
+ const TSharedPtr<FJsonObject>& Parent,
298
+ const TCHAR* Field,
299
+ FVector& OutValue,
300
+ FString& OutError)
301
+ {
302
+ const TSharedPtr<FJsonObject>* Object = nullptr;
303
+ if (!Parent->TryGetObjectField(Field, Object) || !Object || !Object->IsValid())
304
+ {
305
+ OutError = FString::Printf(TEXT("'%s' must be an object with x, y and z"), Field);
306
+ return false;
307
+ }
308
+ double X = 0.0;
309
+ double Y = 0.0;
310
+ double Z = 0.0;
311
+ if (!(*Object)->TryGetNumberField(TEXT("x"), X)
312
+ || !(*Object)->TryGetNumberField(TEXT("y"), Y)
313
+ || !(*Object)->TryGetNumberField(TEXT("z"), Z)
314
+ || !FMath::IsFinite(X) || !FMath::IsFinite(Y) || !FMath::IsFinite(Z))
315
+ {
316
+ OutError = FString::Printf(TEXT("'%s' must contain finite x, y and z numbers"), Field);
317
+ return false;
318
+ }
319
+ OutValue = FVector(X, Y, Z);
320
+ return true;
321
+ }
322
+
323
+ bool ControlRigSequencerReadRotation(
324
+ const TSharedPtr<FJsonObject>& Parent,
325
+ FQuat& OutValue,
326
+ FString& OutError)
327
+ {
328
+ const TSharedPtr<FJsonObject>* Quaternion = nullptr;
329
+ if (Parent->TryGetObjectField(TEXT("rotation"), Quaternion) && Quaternion && Quaternion->IsValid())
330
+ {
331
+ double X = 0.0;
332
+ double Y = 0.0;
333
+ double Z = 0.0;
334
+ double W = 0.0;
335
+ if (!(*Quaternion)->TryGetNumberField(TEXT("x"), X)
336
+ || !(*Quaternion)->TryGetNumberField(TEXT("y"), Y)
337
+ || !(*Quaternion)->TryGetNumberField(TEXT("z"), Z)
338
+ || !(*Quaternion)->TryGetNumberField(TEXT("w"), W)
339
+ || !FMath::IsFinite(X) || !FMath::IsFinite(Y)
340
+ || !FMath::IsFinite(Z) || !FMath::IsFinite(W))
341
+ {
342
+ OutError = TEXT("'rotation' must contain finite x, y, z and w numbers");
343
+ return false;
344
+ }
345
+ OutValue = FQuat(X, Y, Z, W);
346
+ if (OutValue.SizeSquared() <= UE_SMALL_NUMBER)
347
+ {
348
+ OutError = TEXT("'rotation' quaternion must have non-zero length");
349
+ return false;
350
+ }
351
+ OutValue.Normalize();
352
+ return true;
353
+ }
354
+
355
+ const TSharedPtr<FJsonObject>* Euler = nullptr;
356
+ if (!Parent->TryGetObjectField(TEXT("rotationDegrees"), Euler) || !Euler || !Euler->IsValid())
357
+ {
358
+ OutError = TEXT("Expected 'rotation' quaternion or 'rotationDegrees'");
359
+ return false;
360
+ }
361
+ double Pitch = 0.0;
362
+ double Yaw = 0.0;
363
+ double Roll = 0.0;
364
+ if (!(*Euler)->TryGetNumberField(TEXT("pitch"), Pitch)
365
+ || !(*Euler)->TryGetNumberField(TEXT("yaw"), Yaw)
366
+ || !(*Euler)->TryGetNumberField(TEXT("roll"), Roll)
367
+ || !FMath::IsFinite(Pitch) || !FMath::IsFinite(Yaw) || !FMath::IsFinite(Roll))
368
+ {
369
+ OutError = TEXT("'rotationDegrees' must contain finite pitch, yaw and roll numbers");
370
+ return false;
371
+ }
372
+ OutValue = FRotator(Pitch, Yaw, Roll).Quaternion();
373
+ OutValue.Normalize();
374
+ return true;
375
+ }
376
+
377
+ bool ControlRigSequencerReadNormalizedQuaternion(
378
+ const TSharedPtr<FJsonObject>& Parent,
379
+ FQuat& OutValue,
380
+ FString& OutError)
381
+ {
382
+ const TSharedPtr<FJsonObject>* Quaternion = nullptr;
383
+ if (!Parent->TryGetObjectField(TEXT("rotationQuaternion"), Quaternion)
384
+ || !Quaternion || !Quaternion->IsValid())
385
+ {
386
+ OutError = TEXT("'rotationQuaternion' must be an object with finite x, y, z and w numbers");
387
+ return false;
388
+ }
389
+ double X = 0.0;
390
+ double Y = 0.0;
391
+ double Z = 0.0;
392
+ double W = 0.0;
393
+ if (!(*Quaternion)->TryGetNumberField(TEXT("x"), X)
394
+ || !(*Quaternion)->TryGetNumberField(TEXT("y"), Y)
395
+ || !(*Quaternion)->TryGetNumberField(TEXT("z"), Z)
396
+ || !(*Quaternion)->TryGetNumberField(TEXT("w"), W)
397
+ || !FMath::IsFinite(X) || !FMath::IsFinite(Y)
398
+ || !FMath::IsFinite(Z) || !FMath::IsFinite(W))
399
+ {
400
+ OutError = TEXT("'rotationQuaternion' must contain finite x, y, z and w numbers");
401
+ return false;
402
+ }
403
+ OutValue = FQuat(X, Y, Z, W);
404
+ const double Length = OutValue.Size();
405
+ if (Length <= UE_SMALL_NUMBER)
406
+ {
407
+ OutError = TEXT("'rotationQuaternion' must have non-zero length");
408
+ return false;
409
+ }
410
+ constexpr double NormalizedTolerance = 1e-3;
411
+ if (FMath::Abs(Length - 1.0) > NormalizedTolerance)
412
+ {
413
+ OutError = FString::Printf(
414
+ TEXT("'rotationQuaternion' must be normalized within %.4f (length was %.8f)"),
415
+ NormalizedTolerance, Length);
416
+ return false;
417
+ }
418
+ OutValue.Normalize();
419
+ return true;
420
+ }
421
+
422
+ bool ControlRigSequencerReadTransformPatch(
423
+ const TSharedPtr<FJsonObject>& Object,
424
+ bool bRequireAny,
425
+ FControlRigTransformPatch& OutPatch,
426
+ FString& OutError)
427
+ {
428
+ if (!Object.IsValid())
429
+ {
430
+ OutError = TEXT("Transform must be an object");
431
+ return false;
432
+ }
433
+ if (Object->HasField(TEXT("translation")))
434
+ {
435
+ if (!ControlRigSequencerReadVector(Object, TEXT("translation"), OutPatch.Translation, OutError)) return false;
436
+ OutPatch.bTranslation = true;
437
+ }
438
+ else if (Object->HasField(TEXT("translationCm")))
439
+ {
440
+ if (!ControlRigSequencerReadVector(Object, TEXT("translationCm"), OutPatch.Translation, OutError)) return false;
441
+ OutPatch.bTranslation = true;
442
+ }
443
+ if (Object->HasField(TEXT("rotation")) || Object->HasField(TEXT("rotationDegrees")))
444
+ {
445
+ if (!ControlRigSequencerReadRotation(Object, OutPatch.Rotation, OutError)) return false;
446
+ OutPatch.bRotation = true;
447
+ }
448
+ if (Object->HasField(TEXT("scale")))
449
+ {
450
+ if (!ControlRigSequencerReadVector(Object, TEXT("scale"), OutPatch.Scale, OutError)) return false;
451
+ OutPatch.bScale = true;
452
+ }
453
+ else if (Object->HasField(TEXT("scaleMultiplier")))
454
+ {
455
+ if (!ControlRigSequencerReadVector(Object, TEXT("scaleMultiplier"), OutPatch.Scale, OutError)) return false;
456
+ OutPatch.bScale = true;
457
+ }
458
+ if (bRequireAny && !OutPatch.bTranslation && !OutPatch.bRotation && !OutPatch.bScale)
459
+ {
460
+ OutError = TEXT("Transform must specify translation, rotation/rotationDegrees, or scale");
461
+ return false;
462
+ }
463
+ return true;
464
+ }
465
+
466
+ TSharedPtr<FJsonObject> ControlRigSequencerTransformJson(const FTransform& Transform)
467
+ {
468
+ auto Object = MakeShared<FJsonObject>();
469
+ const FVector Translation = Transform.GetTranslation();
470
+ const FQuat Rotation = Transform.GetRotation().GetNormalized();
471
+ const FRotator Euler = Rotation.Rotator();
472
+ const FVector Scale = Transform.GetScale3D();
473
+
474
+ auto TranslationObject = MakeShared<FJsonObject>();
475
+ TranslationObject->SetNumberField(TEXT("x"), Translation.X);
476
+ TranslationObject->SetNumberField(TEXT("y"), Translation.Y);
477
+ TranslationObject->SetNumberField(TEXT("z"), Translation.Z);
478
+ Object->SetObjectField(TEXT("translation"), TranslationObject);
479
+
480
+ auto RotationObject = MakeShared<FJsonObject>();
481
+ RotationObject->SetNumberField(TEXT("x"), Rotation.X);
482
+ RotationObject->SetNumberField(TEXT("y"), Rotation.Y);
483
+ RotationObject->SetNumberField(TEXT("z"), Rotation.Z);
484
+ RotationObject->SetNumberField(TEXT("w"), Rotation.W);
485
+ Object->SetObjectField(TEXT("rotation"), RotationObject);
486
+
487
+ auto EulerObject = MakeShared<FJsonObject>();
488
+ EulerObject->SetNumberField(TEXT("pitch"), Euler.Pitch);
489
+ EulerObject->SetNumberField(TEXT("yaw"), Euler.Yaw);
490
+ EulerObject->SetNumberField(TEXT("roll"), Euler.Roll);
491
+ Object->SetObjectField(TEXT("rotationDegrees"), EulerObject);
492
+
493
+ auto ScaleObject = MakeShared<FJsonObject>();
494
+ ScaleObject->SetNumberField(TEXT("x"), Scale.X);
495
+ ScaleObject->SetNumberField(TEXT("y"), Scale.Y);
496
+ ScaleObject->SetNumberField(TEXT("z"), Scale.Z);
497
+ Object->SetObjectField(TEXT("scale"), ScaleObject);
498
+ return Object;
499
+ }
500
+
501
+ bool ControlRigSequencerReadFrames(
502
+ const TSharedPtr<FJsonObject>& Object,
503
+ TArray<FFrameNumber>& OutFrames,
504
+ FString& OutError)
505
+ {
506
+ double SingleFrame = 0.0;
507
+ if (Object->TryGetNumberField(TEXT("frame"), SingleFrame))
508
+ {
509
+ if (!FMath::IsFinite(SingleFrame)
510
+ || !FMath::IsNearlyEqual(SingleFrame, FMath::RoundToDouble(SingleFrame))
511
+ || SingleFrame < static_cast<double>(MIN_int32) || SingleFrame > static_cast<double>(MAX_int32))
512
+ {
513
+ OutError = TEXT("'frame' must be an integer");
514
+ return false;
515
+ }
516
+ OutFrames.Add(FFrameNumber(static_cast<int32>(FMath::RoundToInt(SingleFrame))));
517
+ }
518
+
519
+ const TArray<TSharedPtr<FJsonValue>>* Frames = nullptr;
520
+ if (Object->TryGetArrayField(TEXT("frames"), Frames) && Frames)
521
+ {
522
+ for (const TSharedPtr<FJsonValue>& FrameValue : *Frames)
523
+ {
524
+ if (!FrameValue.IsValid() || FrameValue->Type != EJson::Number)
525
+ {
526
+ OutError = TEXT("Every item in 'frames' must be an integer");
527
+ return false;
528
+ }
529
+ const double Number = FrameValue->AsNumber();
530
+ if (!FMath::IsFinite(Number)
531
+ || !FMath::IsNearlyEqual(Number, FMath::RoundToDouble(Number))
532
+ || Number < static_cast<double>(MIN_int32) || Number > static_cast<double>(MAX_int32))
533
+ {
534
+ OutError = TEXT("Every item in 'frames' must be an integer");
535
+ return false;
536
+ }
537
+ OutFrames.AddUnique(FFrameNumber(static_cast<int32>(FMath::RoundToInt(Number))));
538
+ }
539
+ }
540
+ if (OutFrames.IsEmpty())
541
+ {
542
+ OutError = TEXT("Specify 'frame' or a non-empty 'frames' array");
543
+ return false;
544
+ }
545
+ OutFrames.Sort([](FFrameNumber A, FFrameNumber B) { return A.Value < B.Value; });
546
+ return true;
547
+ }
548
+
549
+ bool ControlRigSequencerReadSpace(
550
+ const FString& InSpace,
551
+ EControlRigTransformSpace& OutSpace,
552
+ FString& OutCanonical,
553
+ FString& OutError)
554
+ {
555
+ if (InSpace.IsEmpty() || InSpace.Equals(TEXT("local"), ESearchCase::IgnoreCase))
556
+ {
557
+ OutSpace = EControlRigTransformSpace::Local;
558
+ OutCanonical = TEXT("local");
559
+ return true;
560
+ }
561
+ if (InSpace.Equals(TEXT("global"), ESearchCase::IgnoreCase)
562
+ || InSpace.Equals(TEXT("component"), ESearchCase::IgnoreCase))
563
+ {
564
+ OutSpace = EControlRigTransformSpace::Global;
565
+ OutCanonical = TEXT("component");
566
+ return true;
567
+ }
568
+ OutError = TEXT("'space' must be 'local', 'component', or 'global'");
569
+ return false;
570
+ }
571
+
572
+ void ControlRigSequencerDisplayRange(UMovieScene* MovieScene, int32& OutStart, int32& OutEndExclusive)
573
+ {
574
+ const TRange<FFrameNumber> Range = MovieScene->GetPlaybackRange();
575
+ const FFrameRate TickRate = MovieScene->GetTickResolution();
576
+ const FFrameRate DisplayRate = MovieScene->GetDisplayRate();
577
+ OutStart = FFrameRate::TransformTime(FFrameTime(Range.GetLowerBoundValue()), TickRate, DisplayRate).RoundToFrame().Value;
578
+ OutEndExclusive = FFrameRate::TransformTime(FFrameTime(Range.GetUpperBoundValue()), TickRate, DisplayRate).RoundToFrame().Value;
579
+ }
580
+
581
+ bool ControlRigSequencerResolveSession(
582
+ const TSharedPtr<FJsonObject>& Params,
583
+ FControlRigSequenceSession& OutSession,
584
+ FString& OutError)
585
+ {
586
+ if (!Params->TryGetStringField(TEXT("sequencePath"), OutSession.SequencePath) || OutSession.SequencePath.IsEmpty())
587
+ {
588
+ OutError = TEXT("Missing 'sequencePath' parameter");
589
+ return false;
590
+ }
591
+ if (!Params->TryGetStringField(TEXT("bindingTag"), OutSession.BindingTag) || OutSession.BindingTag.IsEmpty())
592
+ {
593
+ OutError = TEXT("Missing 'bindingTag' parameter");
594
+ return false;
595
+ }
596
+
597
+ OutSession.Sequence = Cast<ULevelSequence>(UEditorAssetLibrary::LoadAsset(OutSession.SequencePath));
598
+ if (!OutSession.Sequence)
599
+ {
600
+ OutError = FString::Printf(TEXT("LevelSequence not found: %s"), *OutSession.SequencePath);
601
+ return false;
602
+ }
603
+ OutSession.MovieScene = OutSession.Sequence->GetMovieScene();
604
+ if (!OutSession.MovieScene)
605
+ {
606
+ OutError = TEXT("LevelSequence has no MovieScene");
607
+ return false;
608
+ }
609
+
610
+ const FMovieSceneObjectBindingIDs* Tagged = OutSession.MovieScene->AllTaggedBindings().Find(FName(*OutSession.BindingTag));
611
+ if (!Tagged || Tagged->IDs.IsEmpty())
612
+ {
613
+ OutError = FString::Printf(TEXT("Binding tag '%s' was not found"), *OutSession.BindingTag);
614
+ return false;
615
+ }
616
+ for (const FMovieSceneObjectBindingID& ID : Tagged->IDs)
617
+ {
618
+ if (OutSession.MovieScene->FindBinding(ID.GetGuid()))
619
+ {
620
+ if (OutSession.BindingGuid.IsValid() && OutSession.BindingGuid != ID.GetGuid())
621
+ {
622
+ OutError = FString::Printf(TEXT("Binding tag '%s' resolves to more than one binding"), *OutSession.BindingTag);
623
+ return false;
624
+ }
625
+ OutSession.BindingGuid = ID.GetGuid();
626
+ }
627
+ }
628
+ if (!OutSession.BindingGuid.IsValid())
629
+ {
630
+ OutError = FString::Printf(TEXT("Binding tag '%s' has no binding in this sequence"), *OutSession.BindingTag);
631
+ return false;
632
+ }
633
+
634
+ const TArray<UMovieSceneTrack*> Tracks = OutSession.MovieScene->FindTracks(
635
+ UMovieSceneControlRigParameterTrack::StaticClass(), OutSession.BindingGuid, NAME_None);
636
+ for (UMovieSceneTrack* Candidate : Tracks)
637
+ {
638
+ auto* RigTrack = Cast<UMovieSceneControlRigParameterTrack>(Candidate);
639
+ if (!RigTrack || !RigTrack->GetControlRig())
640
+ {
641
+ continue;
642
+ }
643
+ if (OutSession.Track)
644
+ {
645
+ OutError = FString::Printf(TEXT("Binding '%s' has more than one Control Rig track"), *OutSession.BindingTag);
646
+ return false;
647
+ }
648
+ OutSession.Track = RigTrack;
649
+ OutSession.ControlRig = RigTrack->GetControlRig();
650
+ }
651
+ if (!OutSession.Track || !OutSession.ControlRig)
652
+ {
653
+ OutError = FString::Printf(TEXT("Binding '%s' has no Control Rig track"), *OutSession.BindingTag);
654
+ return false;
655
+ }
656
+
657
+ OutSession.Section = Cast<UMovieSceneControlRigParameterSection>(OutSession.Track->GetSectionToKey());
658
+ if (!OutSession.Section)
659
+ {
660
+ for (UMovieSceneSection* Candidate : OutSession.Track->GetAllSections())
661
+ {
662
+ OutSession.Section = Cast<UMovieSceneControlRigParameterSection>(Candidate);
663
+ if (OutSession.Section) break;
664
+ }
665
+ }
666
+ if (!OutSession.Section)
667
+ {
668
+ OutError = TEXT("Control Rig track has no parameter section");
669
+ return false;
670
+ }
671
+ return true;
672
+ }
673
+
674
+ bool ControlRigSequencerIsTransformControl(const FRigControlElement* Control)
675
+ {
676
+ if (!Control) return false;
677
+ switch (Control->Settings.ControlType)
678
+ {
679
+ case ERigControlType::Position:
680
+ case ERigControlType::Scale:
681
+ case ERigControlType::Rotator:
682
+ case ERigControlType::Transform:
683
+ case ERigControlType::TransformNoScale:
684
+ case ERigControlType::EulerTransform:
685
+ return true;
686
+ default:
687
+ return false;
688
+ }
689
+ }
690
+
691
+ bool ControlRigSequencerIsFloatControl(const FRigControlElement* Control)
692
+ {
693
+ return Control && (Control->Settings.ControlType == ERigControlType::Float
694
+ || Control->Settings.ControlType == ERigControlType::ScaleFloat);
695
+ }
696
+
697
+ bool ControlRigSequencerIsEnumOption(const UEnum* Enum, int32 Index)
698
+ {
699
+ return Enum && Index >= 0 && Index < Enum->NumEnums()
700
+ && !Enum->HasMetaData(TEXT("Hidden"), Index)
701
+ && !(Enum->ContainsExistingMax() && Index == Enum->NumEnums() - 1);
702
+ }
703
+
704
+ int32 ControlRigSequencerFindEnumOption(const UEnum* Enum, int32 Value)
705
+ {
706
+ if (!Enum) return INDEX_NONE;
707
+ for (int32 Index = 0; Index < Enum->NumEnums(); ++Index)
708
+ {
709
+ if (ControlRigSequencerIsEnumOption(Enum, Index) && Enum->GetValueByIndex(Index) == Value)
710
+ {
711
+ return Index;
712
+ }
713
+ }
714
+ return INDEX_NONE;
715
+ }
716
+
717
+ bool ControlRigSequencerIsValidEnumValue(const UEnum* Enum, int32 Value)
718
+ {
719
+ return ControlRigSequencerFindEnumOption(Enum, Value) != INDEX_NONE;
720
+ }
721
+
722
+ TArray<TSharedPtr<FJsonValue>> ControlRigSequencerControlsJson(
723
+ UControlRig* ControlRig,
724
+ const TArray<FName>* ControlFilter = nullptr)
725
+ {
726
+ TArray<TSharedPtr<FJsonValue>> Result;
727
+ if (!ControlRig || !ControlRig->GetHierarchy()) return Result;
728
+
729
+ TArray<FRigControlElement*> Controls = ControlRig->GetHierarchy()->GetControls();
730
+ Controls.Sort([](const FRigControlElement& A, const FRigControlElement& B)
731
+ {
732
+ return A.GetFName().LexicalLess(B.GetFName());
733
+ });
734
+ for (const FRigControlElement* Control : Controls)
735
+ {
736
+ if (!Control) continue;
737
+ if (ControlFilter && !ControlFilter->Contains(Control->GetFName())) continue;
738
+ auto Object = MakeShared<FJsonObject>();
739
+ Object->SetStringField(TEXT("name"), Control->GetFName().ToString());
740
+ const FString ControlType = StaticEnum<ERigControlType>()->GetNameStringByValue(
741
+ static_cast<int64>(Control->Settings.ControlType));
742
+ Object->SetStringField(TEXT("type"), ControlType);
743
+ Object->SetStringField(TEXT("controlType"), ControlType);
744
+ Object->SetBoolField(TEXT("transformControl"), ControlRigSequencerIsTransformControl(Control));
745
+ Object->SetBoolField(TEXT("animatable"), Control->Settings.IsAnimatable());
746
+ if (const UEnum* ControlEnum = Control->Settings.ControlEnum.Get())
747
+ {
748
+ Object->SetStringField(TEXT("enumName"), ControlEnum->GetName());
749
+ Object->SetStringField(TEXT("enumPath"), ControlEnum->GetPathName());
750
+ TArray<TSharedPtr<FJsonValue>> Options;
751
+ for (int32 Index = 0; Index < ControlEnum->NumEnums(); ++Index)
752
+ {
753
+ if (!ControlRigSequencerIsEnumOption(ControlEnum, Index)) continue;
754
+ auto Option = MakeShared<FJsonObject>();
755
+ Option->SetStringField(TEXT("name"), ControlEnum->GetNameStringByIndex(Index));
756
+ Option->SetStringField(TEXT("displayName"), ControlEnum->GetDisplayNameTextByIndex(Index).ToString());
757
+ Option->SetNumberField(TEXT("value"), ControlEnum->GetValueByIndex(Index));
758
+ Options.Add(MakeShared<FJsonValueObject>(Option));
759
+ }
760
+ Object->SetArrayField(TEXT("enumOptions"), Options);
761
+ }
762
+ Result.Add(MakeShared<FJsonValueObject>(Object));
763
+ }
764
+ return Result;
765
+ }
766
+
767
+ TSharedPtr<FJsonObject> ControlRigSequencerSessionJson(const FControlRigSequenceSession& Session)
768
+ {
769
+ auto Result = MCPSuccess();
770
+ Result->SetStringField(TEXT("sequencePath"), Session.Sequence->GetPathName());
771
+ Result->SetStringField(TEXT("bindingTag"), Session.BindingTag);
772
+ Result->SetStringField(TEXT("bindingGuid"), Session.BindingGuid.ToString());
773
+ Result->SetStringField(TEXT("trackName"), Session.Track->GetTrackName().ToString());
774
+ Result->SetStringField(TEXT("trackPath"), Session.Track->GetPathName());
775
+ Result->SetStringField(TEXT("sectionPath"), Session.Section->GetPathName());
776
+ Result->SetStringField(TEXT("controlRigClass"), Session.ControlRig->GetClass()->GetPathName());
777
+ Result->SetBoolField(TEXT("layered"), Session.ControlRig->IsAdditive());
778
+ Result->SetObjectField(TEXT("displayRate"), ControlRigSequencerRateJson(Session.MovieScene->GetDisplayRate()));
779
+ Result->SetObjectField(TEXT("tickResolution"), ControlRigSequencerRateJson(Session.MovieScene->GetTickResolution()));
780
+ int32 StartFrame = 0;
781
+ int32 EndFrameExclusive = 0;
782
+ ControlRigSequencerDisplayRange(Session.MovieScene, StartFrame, EndFrameExclusive);
783
+ Result->SetNumberField(TEXT("startFrame"), StartFrame);
784
+ Result->SetNumberField(TEXT("endFrameExclusive"), EndFrameExclusive);
785
+ TArray<TSharedPtr<FJsonValue>> Controls = ControlRigSequencerControlsJson(Session.ControlRig);
786
+ Result->SetNumberField(TEXT("controlCount"), Controls.Num());
787
+ Result->SetArrayField(TEXT("controls"), Controls);
788
+ return Result;
789
+ }
790
+
791
+ FTransform ControlRigSequencerApplySetPatch(const FTransform& Base, const FControlRigTransformPatch& Patch)
792
+ {
793
+ FTransform Result = Base;
794
+ if (Patch.bTranslation) Result.SetTranslation(Patch.Translation);
795
+ if (Patch.bRotation) Result.SetRotation(Patch.Rotation.GetNormalized());
796
+ if (Patch.bScale) Result.SetScale3D(Patch.Scale);
797
+ return Result;
798
+ }
799
+
800
+ FTransform ControlRigSequencerApplyOffset(
801
+ const FTransform& Base,
802
+ const FControlRigTransformPatch& Patch,
803
+ double Weight,
804
+ EControlRigTransformSpace Space)
805
+ {
806
+ FTransform Result = Base;
807
+ if (Patch.bTranslation)
808
+ {
809
+ Result.AddToTranslation(Patch.Translation * Weight);
810
+ }
811
+ if (Patch.bRotation)
812
+ {
813
+ const FQuat Delta = FQuat::Slerp(FQuat::Identity, Patch.Rotation, Weight).GetNormalized();
814
+ const FQuat Rotation = Space == EControlRigTransformSpace::Local
815
+ ? Result.GetRotation() * Delta
816
+ : Delta * Result.GetRotation();
817
+ Result.SetRotation(Rotation.GetNormalized());
818
+ }
819
+ if (Patch.bScale)
820
+ {
821
+ const FVector Multiplier = FMath::Lerp(FVector::OneVector, Patch.Scale, Weight);
822
+ Result.SetScale3D(Result.GetScale3D() * Multiplier);
823
+ }
824
+ return Result;
825
+ }
826
+
827
+ bool ControlRigSequencerTransformMatches(
828
+ const FTransform& Expected,
829
+ const FTransform& Actual,
830
+ ERigControlType ControlType)
831
+ {
832
+ const bool bTranslationMatches = Expected.GetTranslation().Equals(Actual.GetTranslation(), 0.01);
833
+ const bool bScaleMatches = Expected.GetScale3D().Equals(Actual.GetScale3D(), 0.001);
834
+ const double RotationDot = FMath::Abs(
835
+ Expected.GetRotation().GetNormalized() | Actual.GetRotation().GetNormalized());
836
+ const bool bRotationMatches = RotationDot >= FMath::Cos(FMath::DegreesToRadians(0.1) * 0.5);
837
+ switch (ControlType)
838
+ {
839
+ case ERigControlType::Position: return bTranslationMatches;
840
+ case ERigControlType::Scale: return bScaleMatches;
841
+ case ERigControlType::Rotator: return bRotationMatches;
842
+ case ERigControlType::TransformNoScale: return bTranslationMatches && bRotationMatches;
843
+ default: return bTranslationMatches && bRotationMatches && bScaleMatches;
844
+ }
845
+ }
846
+
847
+ bool ControlRigSequencerPatchAffectsControl(
848
+ const FControlRigTransformPatch& Patch,
849
+ ERigControlType ControlType)
850
+ {
851
+ switch (ControlType)
852
+ {
853
+ case ERigControlType::Position: return Patch.bTranslation;
854
+ case ERigControlType::Scale: return Patch.bScale;
855
+ case ERigControlType::Rotator: return Patch.bRotation;
856
+ case ERigControlType::TransformNoScale: return Patch.bTranslation || Patch.bRotation;
857
+ default: return Patch.bTranslation || Patch.bRotation || Patch.bScale;
858
+ }
859
+ }
860
+
861
+ bool ControlRigSequencerControlHasTranslation(ERigControlType ControlType)
862
+ {
863
+ switch (ControlType)
864
+ {
865
+ case ERigControlType::Position:
866
+ case ERigControlType::Transform:
867
+ case ERigControlType::TransformNoScale:
868
+ case ERigControlType::EulerTransform:
869
+ return true;
870
+ default:
871
+ return false;
872
+ }
873
+ }
874
+
875
+ bool ControlRigSequencerControlHasRotation(ERigControlType ControlType)
876
+ {
877
+ switch (ControlType)
878
+ {
879
+ case ERigControlType::Rotator:
880
+ case ERigControlType::Transform:
881
+ case ERigControlType::TransformNoScale:
882
+ case ERigControlType::EulerTransform:
883
+ return true;
884
+ default:
885
+ return false;
886
+ }
887
+ }
888
+
889
+ double ControlRigSequencerSmoothStep(double Value)
890
+ {
891
+ const double T = FMath::Clamp(Value, 0.0, 1.0);
892
+ return T * T * (3.0 - 2.0 * T);
893
+ }
894
+
895
+ double ControlRigSequencerContactWeight(int32 Index, int32 FrameCount, int32 BlendIn, int32 BlendOut)
896
+ {
897
+ double Weight = 1.0;
898
+ if (BlendIn > 0)
899
+ {
900
+ Weight = FMath::Min(Weight, ControlRigSequencerSmoothStep(
901
+ static_cast<double>(Index) / static_cast<double>(BlendIn)));
902
+ }
903
+ if (BlendOut > 0)
904
+ {
905
+ Weight = FMath::Min(Weight, ControlRigSequencerSmoothStep(
906
+ static_cast<double>(FrameCount - 1 - Index) / static_cast<double>(BlendOut)));
907
+ }
908
+ return Weight;
909
+ }
910
+
911
+ FTransform ControlRigSequencerBlendContactTransform(
912
+ const FTransform& Before,
913
+ const FTransform& Target,
914
+ double Weight,
915
+ bool bBlendRotation)
916
+ {
917
+ FTransform Result = Before;
918
+ Result.SetTranslation(FMath::Lerp(Before.GetTranslation(), Target.GetTranslation(), Weight));
919
+ if (bBlendRotation)
920
+ {
921
+ const FQuat From = Before.GetRotation().GetNormalized();
922
+ FQuat To = Target.GetRotation().GetNormalized();
923
+ if ((From | To) < 0.0)
924
+ {
925
+ To = FQuat(-To.X, -To.Y, -To.Z, -To.W);
926
+ }
927
+ Result.SetRotation(FQuat::Slerp(From, To, Weight).GetNormalized());
928
+ }
929
+ Result.SetScale3D(Before.GetScale3D());
930
+ return Result;
931
+ }
932
+
933
+ bool ControlRigSequencerSolveRotationChain(
934
+ const TArray<FTransform>& SourceGlobals,
935
+ const FTransform& TargetEnd,
936
+ bool bTargetRotation,
937
+ TArray<FTransform>& OutGlobals,
938
+ double& OutPositionErrorCm)
939
+ {
940
+ if (SourceGlobals.Num() < 2)
941
+ {
942
+ return false;
943
+ }
944
+
945
+ TArray<FFABRIKChainLink> Links;
946
+ Links.Reserve(SourceGlobals.Num());
947
+ double MaximumReach = 0.0;
948
+ for (int32 Index = 0; Index < SourceGlobals.Num(); ++Index)
949
+ {
950
+ const FVector Position = SourceGlobals[Index].GetTranslation();
951
+ const double Length = Index == 0
952
+ ? 0.0
953
+ : FVector::Distance(Position, SourceGlobals[Index - 1].GetTranslation());
954
+ MaximumReach += Length;
955
+ Links.Emplace(Position, Length, Index, Index);
956
+ }
957
+
958
+ AnimationCore::SolveFabrik(
959
+ Links, TargetEnd.GetTranslation(), MaximumReach, 0.001, 32);
960
+
961
+ OutGlobals = SourceGlobals;
962
+ for (int32 Index = 0; Index < Links.Num(); ++Index)
963
+ {
964
+ OutGlobals[Index].SetTranslation(Links[Index].Position);
965
+ if (Index + 1 < Links.Num())
966
+ {
967
+ const FVector SourceDirection =
968
+ SourceGlobals[Index + 1].GetTranslation() - SourceGlobals[Index].GetTranslation();
969
+ const FVector SolvedDirection = Links[Index + 1].Position - Links[Index].Position;
970
+ if (!SourceDirection.IsNearlyZero() && !SolvedDirection.IsNearlyZero())
971
+ {
972
+ const FQuat Delta = FQuat::FindBetweenNormals(
973
+ SourceDirection.GetSafeNormal(), SolvedDirection.GetSafeNormal());
974
+ OutGlobals[Index].SetRotation(
975
+ (Delta * SourceGlobals[Index].GetRotation()).GetNormalized());
976
+ }
977
+ }
978
+ else if (bTargetRotation)
979
+ {
980
+ OutGlobals[Index].SetRotation(TargetEnd.GetRotation().GetNormalized());
981
+ }
982
+ }
983
+
984
+ OutPositionErrorCm = FVector::Distance(
985
+ OutGlobals.Last().GetTranslation(), TargetEnd.GetTranslation());
986
+ return !OutGlobals.ContainsByPredicate([](const FTransform& Transform)
987
+ {
988
+ return Transform.ContainsNaN();
989
+ });
990
+ }
991
+
992
+ bool ControlRigSequencerRegisterWriteFrames(
993
+ TSet<FString>& WrittenKeys,
994
+ FName Control,
995
+ const TArray<FFrameNumber>& Frames,
996
+ int32 OperationIndex,
997
+ FString& OutError)
998
+ {
999
+ for (const FFrameNumber Frame : Frames)
1000
+ {
1001
+ const FString Key = FString::Printf(
1002
+ TEXT("%s|%d"), *Control.ToString().ToLower(), Frame.Value);
1003
+ if (WrittenKeys.Contains(Key))
1004
+ {
1005
+ OutError = FString::Printf(
1006
+ TEXT("operations[%d] overlaps another edit at %s frame %d"),
1007
+ OperationIndex, *Control.ToString(), Frame.Value);
1008
+ return false;
1009
+ }
1010
+ WrittenKeys.Add(Key);
1011
+ }
1012
+ return true;
1013
+ }
1014
+
1015
+ USkeletalMeshComponent* ControlRigSequencerBoundSkeletalMesh(UControlRig* ControlRig)
1016
+ {
1017
+ if (!ControlRig) return nullptr;
1018
+ const TSharedPtr<IControlRigObjectBinding> ObjectBinding = ControlRig->GetObjectBinding();
1019
+ return ObjectBinding.IsValid()
1020
+ ? Cast<USkeletalMeshComponent>(ObjectBinding->GetBoundObject())
1021
+ : nullptr;
1022
+ }
1023
+
1024
+ bool ControlRigSequencerSampleReferenceTransforms(
1025
+ const FControlRigSequenceSession& Session,
1026
+ FName Reference,
1027
+ const TArray<FFrameNumber>& Frames,
1028
+ TArray<FTransform>& OutTransforms,
1029
+ FString& OutError)
1030
+ {
1031
+ USkeletalMeshComponent* Component = ControlRigSequencerBoundSkeletalMesh(Session.ControlRig);
1032
+ if (!Component)
1033
+ {
1034
+ OutError = TEXT("The Control Rig is not bound to a skeletal mesh component");
1035
+ return false;
1036
+ }
1037
+ if (!Component->DoesSocketExist(Reference))
1038
+ {
1039
+ OutError = FString::Printf(TEXT("Driven bone or socket was not found: %s"), *Reference.ToString());
1040
+ return false;
1041
+ }
1042
+ UMovieSceneSkeletalAnimationSection* SourceSection = nullptr;
1043
+ for (UMovieSceneTrack* Track : Session.MovieScene->FindTracks(
1044
+ UMovieSceneSkeletalAnimationTrack::StaticClass(), Session.BindingGuid, NAME_None))
1045
+ {
1046
+ for (UMovieSceneSection* Section : Track->GetAllSections())
1047
+ {
1048
+ auto* Candidate = Cast<UMovieSceneSkeletalAnimationSection>(Section);
1049
+ if (!Candidate || !Candidate->Params.Animation) continue;
1050
+ if (SourceSection && SourceSection != Candidate)
1051
+ {
1052
+ OutError = TEXT("contact_lock requires one source animation section in its edit session");
1053
+ return false;
1054
+ }
1055
+ SourceSection = Candidate;
1056
+ }
1057
+ }
1058
+ if (!SourceSection || !SourceSection->Params.Animation)
1059
+ {
1060
+ OutError = TEXT("The Control Rig edit session has no source animation section");
1061
+ return false;
1062
+ }
1063
+
1064
+ FAnimPoseEvaluationOptions Options;
1065
+ Options.EvaluationType = EAnimDataEvalType::Raw;
1066
+ Options.OptionalSkeletalMesh = Component->GetSkeletalMeshAsset();
1067
+ Options.bShouldRetarget = true;
1068
+ const bool bSocket = Component->GetSocketByName(Reference) != nullptr;
1069
+ OutTransforms.Init(FTransform::Identity, Frames.Num());
1070
+ for (int32 Index = 0; Index < Frames.Num(); ++Index)
1071
+ {
1072
+ const FFrameTime TickTime = FFrameRate::TransformTime(
1073
+ FFrameTime(Frames[Index]),
1074
+ Session.MovieScene->GetDisplayRate(),
1075
+ Session.MovieScene->GetTickResolution());
1076
+ const double AnimationTime = SourceSection->MapTimeToAnimation(
1077
+ TickTime, Session.MovieScene->GetTickResolution());
1078
+ FAnimPose Pose;
1079
+ UAnimPoseExtensions::GetAnimPoseAtTime(
1080
+ SourceSection->Params.Animation, AnimationTime, Options, Pose);
1081
+ OutTransforms[Index] = bSocket
1082
+ ? UAnimPoseExtensions::GetSocketPose(Pose, Reference, EAnimPoseSpaces::World)
1083
+ : UAnimPoseExtensions::GetBonePose(Pose, Reference, EAnimPoseSpaces::World);
1084
+ if (OutTransforms[Index].ContainsNaN())
1085
+ {
1086
+ OutError = FString::Printf(
1087
+ TEXT("Driven reference %s evaluated to an invalid component-space transform"),
1088
+ *Reference.ToString());
1089
+ return false;
1090
+ }
1091
+ }
1092
+ if (OutTransforms.IsEmpty())
1093
+ {
1094
+ OutError = TEXT("contact_lock requires at least one frame");
1095
+ return false;
1096
+ }
1097
+ return true;
1098
+ }
1099
+
1100
+ double ControlRigSequencerRotationErrorDegrees(const FQuat& Expected, const FQuat& Actual)
1101
+ {
1102
+ const double Dot = FMath::Clamp(
1103
+ FMath::Abs(Expected.GetNormalized() | Actual.GetNormalized()), 0.0, 1.0);
1104
+ return FMath::RadiansToDegrees(2.0 * FMath::Acos(Dot));
1105
+ }
1106
+
1107
+ void ControlRigSequencerMeasureContact(
1108
+ const TArray<FFrameNumber>& Frames,
1109
+ const TArray<FTransform>& Expected,
1110
+ const TArray<FTransform>& Actual,
1111
+ bool bCheckPosition,
1112
+ bool bCheckRotation,
1113
+ FControlRigContactMetrics& OutMetrics)
1114
+ {
1115
+ for (int32 Index = 0; Index < Frames.Num(); ++Index)
1116
+ {
1117
+ if (bCheckPosition)
1118
+ {
1119
+ const double Error = FVector::Distance(
1120
+ Expected[Index].GetTranslation(), Actual[Index].GetTranslation());
1121
+ if (Index == 0 || Error > OutMetrics.MaxPositionErrorCm)
1122
+ {
1123
+ OutMetrics.MaxPositionErrorCm = Error;
1124
+ OutMetrics.WorstPositionFrame = Frames[Index].Value;
1125
+ OutMetrics.WorstPositionExpected = Expected[Index];
1126
+ OutMetrics.WorstPositionActual = Actual[Index];
1127
+ }
1128
+ }
1129
+ if (bCheckRotation)
1130
+ {
1131
+ const double Error = ControlRigSequencerRotationErrorDegrees(
1132
+ Expected[Index].GetRotation(), Actual[Index].GetRotation());
1133
+ if (Index == 0 || Error > OutMetrics.MaxRotationErrorDegrees)
1134
+ {
1135
+ OutMetrics.MaxRotationErrorDegrees = Error;
1136
+ OutMetrics.WorstRotationFrame = Frames[Index].Value;
1137
+ }
1138
+ }
1139
+ }
1140
+ }
1141
+
1142
+ TSharedPtr<FJsonObject> ControlRigSequencerContactMetricsJson(
1143
+ const FControlRigContactMetrics& Metrics,
1144
+ bool bCheckPosition,
1145
+ bool bCheckRotation)
1146
+ {
1147
+ auto Object = MakeShared<FJsonObject>();
1148
+ if (bCheckPosition)
1149
+ {
1150
+ Object->SetNumberField(TEXT("maxPositionErrorCm"), Metrics.MaxPositionErrorCm);
1151
+ Object->SetNumberField(TEXT("worstPositionFrame"), Metrics.WorstPositionFrame);
1152
+ Object->SetObjectField(
1153
+ TEXT("worstPositionExpected"), ControlRigSequencerTransformJson(Metrics.WorstPositionExpected));
1154
+ Object->SetObjectField(
1155
+ TEXT("worstPositionActual"), ControlRigSequencerTransformJson(Metrics.WorstPositionActual));
1156
+ }
1157
+ if (bCheckRotation)
1158
+ {
1159
+ Object->SetNumberField(TEXT("maxRotationErrorDegrees"), Metrics.MaxRotationErrorDegrees);
1160
+ Object->SetNumberField(TEXT("worstRotationFrame"), Metrics.WorstRotationFrame);
1161
+ }
1162
+ return Object;
1163
+ }
1164
+ }
1165
+
1166
+ #if WITH_DEV_AUTOMATION_TESTS
1167
+ IMPLEMENT_SIMPLE_AUTOMATION_TEST(
1168
+ FUEMCPControlRigContactRotationChainTest,
1169
+ "UE_MCP.Animation.ControlRig.ContactRotationChain",
1170
+ EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
1171
+
1172
+ bool FUEMCPControlRigContactRotationChainTest::RunTest(const FString& Parameters)
1173
+ {
1174
+ (void)Parameters;
1175
+ const TArray<FTransform> SourceGlobals{
1176
+ FTransform(FQuat::Identity, FVector(0.0, 0.0, 0.0)),
1177
+ FTransform(FQuat::Identity, FVector(10.0, 0.0, 0.0)),
1178
+ FTransform(FQuat::Identity, FVector(20.0, 0.0, 0.0)),
1179
+ };
1180
+ const FTransform TargetEnd(FQuat::Identity, FVector(10.0, 10.0, 0.0));
1181
+ TArray<FTransform> SolvedGlobals;
1182
+ double PositionErrorCm = 0.0;
1183
+ TestTrue(
1184
+ TEXT("Rotation chain solves a reachable contact"),
1185
+ ControlRigSequencerSolveRotationChain(
1186
+ SourceGlobals, TargetEnd, false, SolvedGlobals, PositionErrorCm));
1187
+ TestTrue(TEXT("Solved contact is within one millimetre"), PositionErrorCm <= 0.1);
1188
+ TestTrue(
1189
+ TEXT("Driver translation remains unchanged"),
1190
+ SolvedGlobals[0].GetTranslation().Equals(SourceGlobals[0].GetTranslation(), UE_KINDA_SMALL_NUMBER));
1191
+ TestTrue(
1192
+ TEXT("First local bone translation remains unchanged"),
1193
+ SolvedGlobals[1].GetRelativeTransform(SolvedGlobals[0]).GetTranslation().Equals(
1194
+ SourceGlobals[1].GetRelativeTransform(SourceGlobals[0]).GetTranslation(), 0.001));
1195
+ TestTrue(
1196
+ TEXT("Second local bone translation remains unchanged"),
1197
+ SolvedGlobals[2].GetRelativeTransform(SolvedGlobals[1]).GetTranslation().Equals(
1198
+ SourceGlobals[2].GetRelativeTransform(SourceGlobals[1]).GetTranslation(), 0.001));
1199
+ return true;
1200
+ }
1201
+ #endif
1202
+
1203
+ #endif // UE_MCP_HAS_5_8_API
1204
+
1205
+ TSharedPtr<FJsonValue> FAnimationHandlers::BeginControlRigEdit(const TSharedPtr<FJsonObject>& Params)
1206
+ {
1207
+ #if !UE_MCP_HAS_5_8_API
1208
+ return ControlRigSequencerUnsupported();
1209
+ #else
1210
+ FString SequencePath;
1211
+ FString SkeletalMeshPath;
1212
+ FString SourceAnimationPath;
1213
+ if (auto Error = RequireString(Params, TEXT("sequencePath"), SequencePath)) return Error;
1214
+ if (auto Error = RequireString(Params, TEXT("skeletalMeshPath"), SkeletalMeshPath)) return Error;
1215
+ if (auto Error = RequireString(Params, TEXT("sourceAnimationPath"), SourceAnimationPath)) return Error;
1216
+
1217
+ FString PackagePath;
1218
+ FString AssetName;
1219
+ FString Error;
1220
+ if (!ControlRigSequencerSplitAssetPath(SequencePath, PackagePath, AssetName, Error)) return MCPError(Error);
1221
+
1222
+ USkeletalMesh* SkeletalMesh = Cast<USkeletalMesh>(UEditorAssetLibrary::LoadAsset(SkeletalMeshPath));
1223
+ if (!SkeletalMesh) return MCPError(FString::Printf(TEXT("SkeletalMesh not found: %s"), *SkeletalMeshPath));
1224
+ UAnimSequence* SourceAnimation = Cast<UAnimSequence>(UEditorAssetLibrary::LoadAsset(SourceAnimationPath));
1225
+ if (!SourceAnimation) return MCPError(FString::Printf(TEXT("AnimSequence not found: %s"), *SourceAnimationPath));
1226
+ if (!SkeletalMesh->GetSkeleton() || !SourceAnimation->GetSkeleton()
1227
+ || !SkeletalMesh->GetSkeleton()->IsCompatibleForEditor(SourceAnimation->GetSkeleton()))
1228
+ {
1229
+ return MCPError(TEXT("Source animation is not compatible with the skeletal mesh. Retarget it before beginning a Control Rig edit."));
1230
+ }
1231
+ if (SourceAnimation->GetAdditiveAnimType() != AAT_None)
1232
+ {
1233
+ return MCPError(TEXT("Additive source animations require an explicit base pose. Flatten the additive with its intended base before beginning a Control Rig edit."));
1234
+ }
1235
+ const double SourceRateScale = static_cast<double>(SourceAnimation->RateScale);
1236
+ if (!FMath::IsFinite(SourceRateScale) || SourceRateScale == 0.0)
1237
+ {
1238
+ return MCPError(TEXT("Source animation RateScale must be finite and non-zero before beginning a Control Rig edit"));
1239
+ }
1240
+ const float RawTimelinePlayRate = static_cast<float>(1.0 / SourceRateScale);
1241
+ if (!FMath::IsFinite(RawTimelinePlayRate))
1242
+ {
1243
+ return MCPError(TEXT("Source animation RateScale cannot be converted to a finite Sequencer play rate"));
1244
+ }
1245
+
1246
+ const FString RigMode = OptionalString(Params, TEXT("rigMode"), TEXT("fk")).ToLower();
1247
+ UClass* ControlRigClass = nullptr;
1248
+ if (RigMode == TEXT("fk"))
1249
+ {
1250
+ ControlRigClass = UFKControlRig::StaticClass();
1251
+ }
1252
+ else if (RigMode == TEXT("asset"))
1253
+ {
1254
+ FString ControlRigPath;
1255
+ if (auto RigError = RequireString(Params, TEXT("controlRigPath"), ControlRigPath)) return RigError;
1256
+ UBlueprint* Blueprint = Cast<UBlueprint>(UEditorAssetLibrary::LoadAsset(ControlRigPath));
1257
+ if (!Blueprint || !Blueprint->GeneratedClass || !Blueprint->GeneratedClass->IsChildOf(UControlRig::StaticClass()))
1258
+ {
1259
+ return MCPError(FString::Printf(TEXT("Control Rig asset is invalid or has no generated Control Rig class: %s"), *ControlRigPath));
1260
+ }
1261
+ ControlRigClass = Blueprint->GeneratedClass;
1262
+ UControlRig* DefaultRig = Cast<UControlRig>(ControlRigClass->GetDefaultObject());
1263
+ const FName LegacyInverse(TEXT("Inverse"));
1264
+ if (!DefaultRig || !(DefaultRig->SupportsEvent(FRigUnit_InverseExecution::EventName) || DefaultRig->SupportsEvent(LegacyInverse)))
1265
+ {
1266
+ return MCPError(TEXT("Control Rig must support inverse execution before an AnimSequence can be baked into it"));
1267
+ }
1268
+ }
1269
+ else
1270
+ {
1271
+ return MCPError(TEXT("'rigMode' must be 'fk' or 'asset'"));
1272
+ }
1273
+
1274
+ FFrameRate DisplayRate;
1275
+ if (!ControlRigSequencerReadRate(Params, TEXT("displayRate"), SourceAnimation->GetSamplingFrameRate(), DisplayRate, Error))
1276
+ {
1277
+ return MCPError(Error);
1278
+ }
1279
+ double StartNumber = 0.0;
1280
+ Params->TryGetNumberField(TEXT("startFrame"), StartNumber);
1281
+ if (!FMath::IsFinite(StartNumber)
1282
+ || !FMath::IsNearlyEqual(StartNumber, FMath::RoundToDouble(StartNumber))
1283
+ || StartNumber < static_cast<double>(MIN_int32) || StartNumber > static_cast<double>(MAX_int32))
1284
+ {
1285
+ return MCPError(TEXT("'startFrame' must be an integer"));
1286
+ }
1287
+ const int32 StartFrame = static_cast<int32>(FMath::RoundToInt(StartNumber));
1288
+ const int32 DefaultDuration = FMath::Max(1, static_cast<int32>(FMath::RoundToInt(SourceAnimation->GetPlayLength() * DisplayRate.AsDecimal())));
1289
+ double EndNumber = static_cast<double>(StartFrame) + static_cast<double>(DefaultDuration);
1290
+ Params->TryGetNumberField(TEXT("endFrame"), EndNumber);
1291
+ if (!FMath::IsFinite(EndNumber)
1292
+ || !FMath::IsNearlyEqual(EndNumber, FMath::RoundToDouble(EndNumber))
1293
+ || EndNumber < static_cast<double>(MIN_int32) || EndNumber > static_cast<double>(MAX_int32))
1294
+ {
1295
+ return MCPError(TEXT("'endFrame' must be an integer"));
1296
+ }
1297
+ const int32 EndFrameExclusive = static_cast<int32>(FMath::RoundToInt(EndNumber));
1298
+ if (EndFrameExclusive <= StartFrame) return MCPError(TEXT("'endFrame' must be greater than 'startFrame'"));
1299
+ if (EndFrameExclusive == MAX_int32) return MCPError(TEXT("'endFrame' is too large"));
1300
+ if (static_cast<int64>(EndFrameExclusive) - static_cast<int64>(StartFrame) > ControlRigSequencerMaxFrames)
1301
+ {
1302
+ return MCPError(FString::Printf(TEXT("Control Rig edit sessions are limited to %d frames"), ControlRigSequencerMaxFrames));
1303
+ }
1304
+
1305
+ const FString OnConflict = OptionalString(Params, TEXT("onConflict"), TEXT("error")).ToLower();
1306
+ if (OnConflict != TEXT("skip") && OnConflict != TEXT("error"))
1307
+ {
1308
+ return MCPError(TEXT("'onConflict' must be 'skip' or 'error'"));
1309
+ }
1310
+ const FString BindingTag = OptionalString(Params, TEXT("bindingTag"), FString::Printf(TEXT("Codex_%s"), *SkeletalMesh->GetName()));
1311
+ if (BindingTag.IsEmpty()) return MCPError(TEXT("'bindingTag' must not be empty"));
1312
+ const bool bLayered = OptionalBool(Params, TEXT("layered"), false);
1313
+
1314
+ ULevelSequence* Sequence = Cast<ULevelSequence>(UEditorAssetLibrary::LoadAsset(SequencePath));
1315
+ const bool bCreated = Sequence == nullptr;
1316
+ if (Sequence && OnConflict == TEXT("error"))
1317
+ {
1318
+ return MCPError(FString::Printf(TEXT("LevelSequence already exists: %s"), *SequencePath));
1319
+ }
1320
+ if (Sequence && OnConflict == TEXT("skip"))
1321
+ {
1322
+ FControlRigSequenceFocusGuard Focus(Sequence);
1323
+ if (!Focus.IsReady()) return MCPError(TEXT("Could not focus the existing LevelSequence in Sequencer"));
1324
+ FControlRigSequenceSession Session;
1325
+ auto ResolveParams = MakeShared<FJsonObject>();
1326
+ ResolveParams->SetStringField(TEXT("sequencePath"), SequencePath);
1327
+ ResolveParams->SetStringField(TEXT("bindingTag"), BindingTag);
1328
+ if (!ControlRigSequencerResolveSession(ResolveParams, Session, Error)) return MCPError(Error);
1329
+ auto Result = ControlRigSequencerSessionJson(Session);
1330
+ MCPSetExisted(Result);
1331
+ return MCPResult(Result);
1332
+ }
1333
+ if (!Sequence)
1334
+ {
1335
+ auto Created = MCPCreateAssetIdempotentNewObject<ULevelSequence>(AssetName, PackagePath, TEXT("error"), TEXT("LevelSequence"));
1336
+ if (Created.EarlyReturn) return Created.EarlyReturn;
1337
+ Sequence = Created.Asset;
1338
+ Sequence->Initialize();
1339
+ }
1340
+
1341
+ UMovieScene* MovieScene = Sequence->GetMovieScene();
1342
+ if (!MovieScene)
1343
+ {
1344
+ if (bCreated) UEditorAssetLibrary::DeleteAsset(PackagePath + TEXT("/") + AssetName);
1345
+ return MCPError(TEXT("LevelSequence has no MovieScene"));
1346
+ }
1347
+
1348
+ bool bBakeSucceeded = false;
1349
+ FString BakeError;
1350
+ FGuid BindingGuid;
1351
+ {
1352
+ const FScopedTransaction Transaction(NSLOCTEXT("UE_MCP", "BeginControlRigEdit", "Begin Control Rig Edit"));
1353
+ Sequence->Modify();
1354
+ MovieScene->Modify();
1355
+ MovieScene->SetDisplayRate(DisplayRate);
1356
+ MovieScene->SetTickResolutionDirectly(DisplayRate);
1357
+ MovieScene->SetPlaybackRange(TRange<FFrameNumber>(FFrameNumber(StartFrame), FFrameNumber(EndFrameExclusive)));
1358
+
1359
+ ASkeletalMeshActor* ActorTemplate = DuplicateObject<ASkeletalMeshActor>(
1360
+ GetMutableDefault<ASkeletalMeshActor>(), Sequence,
1361
+ MakeUniqueObjectName(Sequence, ASkeletalMeshActor::StaticClass(), TEXT("CodexSkeletalMesh")));
1362
+ if (!ActorTemplate || !ActorTemplate->GetSkeletalMeshComponent())
1363
+ {
1364
+ BakeError = TEXT("Failed to create the LevelSequence skeletal mesh spawnable template");
1365
+ }
1366
+ else
1367
+ {
1368
+ ActorTemplate->SetFlags(RF_Transactional);
1369
+ ActorTemplate->GetSkeletalMeshComponent()->SetSkeletalMeshAsset(SkeletalMesh);
1370
+ BindingGuid = MovieScene->AddSpawnable(ActorTemplate->GetName(), *ActorTemplate);
1371
+ auto* SpawnTrack = BindingGuid.IsValid()
1372
+ ? MovieScene->AddTrack<UMovieSceneSpawnTrack>(BindingGuid)
1373
+ : nullptr;
1374
+ UMovieSceneSection* SpawnSection = SpawnTrack ? SpawnTrack->CreateNewSection() : nullptr;
1375
+ if (!BindingGuid.IsValid() || !SpawnTrack || !SpawnSection)
1376
+ {
1377
+ BakeError = TEXT("Failed to create the LevelSequence skeletal mesh spawnable binding");
1378
+ }
1379
+ else
1380
+ {
1381
+ SpawnTrack->AddSection(*SpawnSection);
1382
+ MovieScene->TagBinding(FName(*BindingTag), UE::MovieScene::FFixedObjectBindingID(BindingGuid, MovieSceneSequenceID::Root));
1383
+
1384
+ auto* AnimationTrack = Cast<UMovieSceneSkeletalAnimationTrack>(
1385
+ MovieScene->AddTrack(UMovieSceneSkeletalAnimationTrack::StaticClass(), BindingGuid));
1386
+ auto* AnimationSection = AnimationTrack
1387
+ ? Cast<UMovieSceneSkeletalAnimationSection>(AnimationTrack->CreateNewSection())
1388
+ : nullptr;
1389
+ if (!AnimationTrack || !AnimationSection)
1390
+ {
1391
+ BakeError = TEXT("Failed to create the source skeletal animation track");
1392
+ }
1393
+ else
1394
+ {
1395
+ AnimationSection->Params.Animation = SourceAnimation;
1396
+ // Sequencer multiplies section PlayRate by the AnimSequence RateScale.
1397
+ // Cancel the asset rate so the edit session sees the raw source timeline once.
1398
+ AnimationSection->Params.PlayRate = RawTimelinePlayRate;
1399
+ // Unreal's AnimSequence exporter samples the exact playback end as its final key.
1400
+ // Keep one support frame on the source/rig sections so that sample evaluates the
1401
+ // animation instead of falling outside every section and snapping to reference pose.
1402
+ AnimationSection->SetRange(TRange<FFrameNumber>(
1403
+ FFrameNumber(StartFrame), FFrameNumber(EndFrameExclusive + 1)));
1404
+ AnimationTrack->AddSection(*AnimationSection);
1405
+
1406
+ FControlRigSequenceFocusGuard Focus(Sequence);
1407
+ if (!Focus.IsReady())
1408
+ {
1409
+ BakeError = TEXT("Could not focus the LevelSequence in Sequencer");
1410
+ }
1411
+ else
1412
+ {
1413
+ const FMovieSceneObjectBindingID ObjectBinding(
1414
+ UE::MovieScene::FFixedObjectBindingID(BindingGuid, MovieSceneSequenceID::Root));
1415
+ if (ULevelSequenceEditorBlueprintLibrary::GetBoundObjects(ObjectBinding).IsEmpty())
1416
+ {
1417
+ BakeError = TEXT("Sequencer did not instantiate the skeletal mesh spawnable for baking");
1418
+ }
1419
+ else
1420
+ {
1421
+ UAnimSeqExportOption* ExportOptions = NewObject<UAnimSeqExportOption>();
1422
+ bBakeSucceeded = UControlRigSequencerEditorLibrary::BakeToControlRig(
1423
+ GetEditorWorld(), Sequence, ControlRigClass, ExportOptions,
1424
+ false, 0.001f, FMovieSceneBindingProxy(BindingGuid, Sequence), true);
1425
+ if (!bBakeSucceeded)
1426
+ {
1427
+ BakeError = TEXT("Unreal failed to bake the source animation to the Control Rig");
1428
+ }
1429
+ else
1430
+ {
1431
+ const TArray<UMovieSceneTrack*> RigTracks = MovieScene->FindTracks(
1432
+ UMovieSceneControlRigParameterTrack::StaticClass(), BindingGuid, NAME_None);
1433
+ if (RigTracks.Num() != 1)
1434
+ {
1435
+ bBakeSucceeded = false;
1436
+ BakeError = TEXT("Bake did not produce exactly one Control Rig track");
1437
+ }
1438
+ else if (bLayered)
1439
+ {
1440
+ auto* RigTrack = Cast<UMovieSceneControlRigParameterTrack>(RigTracks[0]);
1441
+ if (!RigTrack || !UControlRigSequencerEditorLibrary::SetControlRigLayeredMode(RigTrack, true))
1442
+ {
1443
+ bBakeSucceeded = false;
1444
+ BakeError = TEXT("Unreal could not convert the Control Rig track to layered mode");
1445
+ }
1446
+ else
1447
+ {
1448
+ // Layer conversion clears the baked Control Rig keys. The source track
1449
+ // must be active so the now-empty rig section is an additive edit layer.
1450
+ AnimationTrack->SetEvalDisabled(false);
1451
+ }
1452
+ }
1453
+ if (bBakeSucceeded)
1454
+ {
1455
+ auto* RigTrack = Cast<UMovieSceneControlRigParameterTrack>(RigTracks[0]);
1456
+ const TArray<UMovieSceneSection*> RigSections = RigTrack ? RigTrack->GetAllSections() : TArray<UMovieSceneSection*>();
1457
+ if (!RigTrack || RigSections.Num() != 1 || !RigSections[0])
1458
+ {
1459
+ bBakeSucceeded = false;
1460
+ BakeError = TEXT("Bake did not produce exactly one Control Rig section");
1461
+ }
1462
+ else
1463
+ {
1464
+ RigSections[0]->SetEndFrame(TRangeBound<FFrameNumber>::Exclusive(FFrameNumber(EndFrameExclusive + 1)));
1465
+ }
1466
+ }
1467
+ }
1468
+ }
1469
+ }
1470
+ }
1471
+ }
1472
+ }
1473
+ }
1474
+
1475
+ if (!bBakeSucceeded)
1476
+ {
1477
+ if (bCreated)
1478
+ {
1479
+ UEditorAssetLibrary::DeleteAsset(PackagePath + TEXT("/") + AssetName);
1480
+ }
1481
+ return MCPError(BakeError.IsEmpty() ? TEXT("Failed to begin Control Rig edit") : BakeError);
1482
+ }
1483
+
1484
+ if (!UEditorAssetLibrary::SaveLoadedAsset(Sequence, false))
1485
+ {
1486
+ if (bCreated) UEditorAssetLibrary::DeleteAsset(PackagePath + TEXT("/") + AssetName);
1487
+ return MCPError(TEXT("Control Rig edit was created in memory but the LevelSequence could not be saved"));
1488
+ }
1489
+ FControlRigSequenceFocusGuard Focus(Sequence);
1490
+ if (!Focus.IsReady()) return MCPError(TEXT("Control Rig edit was created but the LevelSequence could not be focused for inspection"));
1491
+ FControlRigSequenceSession Session;
1492
+ auto ResolveParams = MakeShared<FJsonObject>();
1493
+ ResolveParams->SetStringField(TEXT("sequencePath"), Sequence->GetPathName());
1494
+ ResolveParams->SetStringField(TEXT("bindingTag"), BindingTag);
1495
+ if (!ControlRigSequencerResolveSession(ResolveParams, Session, Error)) return MCPError(Error);
1496
+ auto Result = ControlRigSequencerSessionJson(Session);
1497
+ Result->SetStringField(TEXT("sourceAnimationPath"), SourceAnimation->GetPathName());
1498
+ Result->SetStringField(TEXT("skeletalMeshPath"), SkeletalMesh->GetPathName());
1499
+ Result->SetStringField(TEXT("rigMode"), RigMode);
1500
+ Result->SetBoolField(TEXT("layered"), bLayered);
1501
+ MCPSetCreated(Result);
1502
+ MCPSetDeleteAssetRollback(Result, Sequence->GetPathName());
1503
+ return MCPResult(Result);
1504
+ #endif
1505
+ }
1506
+
1507
+ TSharedPtr<FJsonValue> FAnimationHandlers::ReadControlRigEdit(const TSharedPtr<FJsonObject>& Params)
1508
+ {
1509
+ #if !UE_MCP_HAS_5_8_API
1510
+ return ControlRigSequencerUnsupported();
1511
+ #else
1512
+ FString SequencePath;
1513
+ if (auto Error = RequireString(Params, TEXT("sequencePath"), SequencePath)) return Error;
1514
+ ULevelSequence* Sequence = Cast<ULevelSequence>(UEditorAssetLibrary::LoadAsset(SequencePath));
1515
+ if (!Sequence) return MCPError(FString::Printf(TEXT("LevelSequence not found: %s"), *SequencePath));
1516
+ FControlRigSequenceFocusGuard Focus(Sequence);
1517
+ if (!Focus.IsReady()) return MCPError(TEXT("Could not focus the LevelSequence in Sequencer"));
1518
+
1519
+ FControlRigSequenceSession Session;
1520
+ FString Error;
1521
+ if (!ControlRigSequencerResolveSession(Params, Session, Error)) return MCPError(Error);
1522
+ EControlRigTransformSpace Space;
1523
+ FString CanonicalSpace;
1524
+ if (!ControlRigSequencerReadSpace(OptionalString(Params, TEXT("space"), TEXT("local")), Space, CanonicalSpace, Error))
1525
+ {
1526
+ return MCPError(Error);
1527
+ }
1528
+
1529
+ TArray<FName> ControlNames;
1530
+ TArray<FName> TransformControlNames;
1531
+ TArray<FName> BoolControlNames;
1532
+ TArray<FName> FloatControlNames;
1533
+ TArray<FName> IntControlNames;
1534
+ const TArray<TSharedPtr<FJsonValue>>* RequestedNames = nullptr;
1535
+ if (Params->TryGetArrayField(TEXT("controlNames"), RequestedNames) && RequestedNames)
1536
+ {
1537
+ for (const TSharedPtr<FJsonValue>& Value : *RequestedNames)
1538
+ {
1539
+ if (!Value.IsValid() || Value->Type != EJson::String || Value->AsString().IsEmpty())
1540
+ {
1541
+ return MCPError(TEXT("Every item in 'controlNames' must be a non-empty string"));
1542
+ }
1543
+ const FName Name(*Value->AsString());
1544
+ FRigControlElement* Control = Session.ControlRig->FindControl(Name);
1545
+ if (!Control)
1546
+ {
1547
+ return MCPError(FString::Printf(TEXT("Control not found: %s"), *Name.ToString()));
1548
+ }
1549
+ ControlNames.AddUnique(Name);
1550
+ if (ControlRigSequencerIsTransformControl(Control))
1551
+ {
1552
+ TransformControlNames.AddUnique(Name);
1553
+ }
1554
+ else if (Control->Settings.ControlType == ERigControlType::Bool)
1555
+ {
1556
+ BoolControlNames.AddUnique(Name);
1557
+ }
1558
+ else if (ControlRigSequencerIsFloatControl(Control))
1559
+ {
1560
+ FloatControlNames.AddUnique(Name);
1561
+ }
1562
+ else if (Control->Settings.ControlType == ERigControlType::Integer)
1563
+ {
1564
+ IntControlNames.AddUnique(Name);
1565
+ }
1566
+ else
1567
+ {
1568
+ return MCPError(FString::Printf(TEXT("Control type is not readable by this action: %s"), *Name.ToString()));
1569
+ }
1570
+ }
1571
+ }
1572
+ else
1573
+ {
1574
+ for (FRigControlElement* Control : Session.ControlRig->GetHierarchy()->GetControls())
1575
+ {
1576
+ if (ControlRigSequencerIsTransformControl(Control))
1577
+ {
1578
+ ControlNames.Add(Control->GetFName());
1579
+ TransformControlNames.Add(Control->GetFName());
1580
+ }
1581
+ else if (Control->Settings.ControlType == ERigControlType::Bool)
1582
+ {
1583
+ ControlNames.Add(Control->GetFName());
1584
+ BoolControlNames.Add(Control->GetFName());
1585
+ }
1586
+ else if (ControlRigSequencerIsFloatControl(Control))
1587
+ {
1588
+ ControlNames.Add(Control->GetFName());
1589
+ FloatControlNames.Add(Control->GetFName());
1590
+ }
1591
+ else if (Control->Settings.ControlType == ERigControlType::Integer)
1592
+ {
1593
+ ControlNames.Add(Control->GetFName());
1594
+ IntControlNames.Add(Control->GetFName());
1595
+ }
1596
+ }
1597
+ ControlNames.Sort(FNameLexicalLess());
1598
+ TransformControlNames.Sort(FNameLexicalLess());
1599
+ BoolControlNames.Sort(FNameLexicalLess());
1600
+ FloatControlNames.Sort(FNameLexicalLess());
1601
+ IntControlNames.Sort(FNameLexicalLess());
1602
+ }
1603
+ if (ControlNames.IsEmpty()) return MCPError(TEXT("No readable controls were requested or available"));
1604
+
1605
+ TArray<FFrameNumber> Frames;
1606
+ if (Params->HasField(TEXT("frame")) || Params->HasField(TEXT("frames")))
1607
+ {
1608
+ if (!ControlRigSequencerReadFrames(Params, Frames, Error)) return MCPError(Error);
1609
+ }
1610
+ else
1611
+ {
1612
+ int32 Start = 0;
1613
+ int32 EndExclusive = 0;
1614
+ ControlRigSequencerDisplayRange(Session.MovieScene, Start, EndExclusive);
1615
+ Frames.Add(FFrameNumber(Start));
1616
+ if (EndExclusive - 1 != Start) Frames.Add(FFrameNumber(EndExclusive - 1));
1617
+ }
1618
+
1619
+ int32 Start = 0;
1620
+ int32 EndExclusive = 0;
1621
+ ControlRigSequencerDisplayRange(Session.MovieScene, Start, EndExclusive);
1622
+ for (FFrameNumber Frame : Frames)
1623
+ {
1624
+ if (Frame.Value < Start || Frame.Value >= EndExclusive)
1625
+ {
1626
+ return MCPError(FString::Printf(TEXT("Frame %d is outside [%d, %d)"), Frame.Value, Start, EndExclusive));
1627
+ }
1628
+ }
1629
+
1630
+ TArray<FArrayOfRigControlTransforms> Values;
1631
+ if (!TransformControlNames.IsEmpty())
1632
+ {
1633
+ Values = UControlRigSequencerEditorLibrary::BatchGetControlTransforms(
1634
+ Session.Sequence, Session.ControlRig, TransformControlNames, Frames, Space, EMovieSceneTimeUnit::DisplayRate);
1635
+ if (Values.Num() != TransformControlNames.Num()) return MCPError(TEXT("Unreal could not evaluate every requested Control Rig transform"));
1636
+ }
1637
+
1638
+ auto Result = ControlRigSequencerSessionJson(Session);
1639
+ if (RequestedNames)
1640
+ {
1641
+ TArray<TSharedPtr<FJsonValue>> RequestedControls = ControlRigSequencerControlsJson(
1642
+ Session.ControlRig, &ControlNames);
1643
+ Result->SetNumberField(TEXT("controlCount"), RequestedControls.Num());
1644
+ Result->SetArrayField(TEXT("controls"), RequestedControls);
1645
+ }
1646
+ Result->SetStringField(TEXT("space"), CanonicalSpace);
1647
+ TArray<TSharedPtr<FJsonValue>> Samples;
1648
+ for (const FArrayOfRigControlTransforms& Value : Values)
1649
+ {
1650
+ if (Value.Transforms.Num() != Frames.Num()) return MCPError(TEXT("Control Rig transform evaluation returned an incomplete frame set"));
1651
+ auto ControlObject = MakeShared<FJsonObject>();
1652
+ ControlObject->SetStringField(TEXT("control"), Value.ControlName.ToString());
1653
+ TArray<TSharedPtr<FJsonValue>> FrameSamples;
1654
+ for (int32 Index = 0; Index < Frames.Num(); ++Index)
1655
+ {
1656
+ auto FrameObject = MakeShared<FJsonObject>();
1657
+ FrameObject->SetNumberField(TEXT("frame"), Frames[Index].Value);
1658
+ FrameObject->SetObjectField(TEXT("transform"), ControlRigSequencerTransformJson(Value.Transforms[Index]));
1659
+ FrameSamples.Add(MakeShared<FJsonValueObject>(FrameObject));
1660
+ }
1661
+ ControlObject->SetArrayField(TEXT("samples"), FrameSamples);
1662
+ Samples.Add(MakeShared<FJsonValueObject>(ControlObject));
1663
+ }
1664
+ for (const FName ControlName : BoolControlNames)
1665
+ {
1666
+ const TArray<bool> BoolValues = UControlRigSequencerEditorLibrary::GetLocalControlRigBools(
1667
+ Session.Sequence, Session.ControlRig, ControlName, Frames, EMovieSceneTimeUnit::DisplayRate);
1668
+ if (BoolValues.Num() != Frames.Num()) return MCPError(TEXT("Control Rig bool evaluation returned an incomplete frame set"));
1669
+ auto ControlObject = MakeShared<FJsonObject>();
1670
+ ControlObject->SetStringField(TEXT("control"), ControlName.ToString());
1671
+ ControlObject->SetStringField(TEXT("valueType"), TEXT("bool"));
1672
+ TArray<TSharedPtr<FJsonValue>> FrameSamples;
1673
+ for (int32 Index = 0; Index < Frames.Num(); ++Index)
1674
+ {
1675
+ auto FrameObject = MakeShared<FJsonObject>();
1676
+ FrameObject->SetNumberField(TEXT("frame"), Frames[Index].Value);
1677
+ FrameObject->SetBoolField(TEXT("value"), BoolValues[Index]);
1678
+ FrameSamples.Add(MakeShared<FJsonValueObject>(FrameObject));
1679
+ }
1680
+ ControlObject->SetArrayField(TEXT("samples"), FrameSamples);
1681
+ Samples.Add(MakeShared<FJsonValueObject>(ControlObject));
1682
+ }
1683
+ for (const FName ControlName : FloatControlNames)
1684
+ {
1685
+ const TArray<float> FloatValues = UControlRigSequencerEditorLibrary::GetLocalControlRigFloats(
1686
+ Session.Sequence, Session.ControlRig, ControlName, Frames, EMovieSceneTimeUnit::DisplayRate);
1687
+ if (FloatValues.Num() != Frames.Num()) return MCPError(TEXT("Control Rig float evaluation returned an incomplete frame set"));
1688
+ auto ControlObject = MakeShared<FJsonObject>();
1689
+ ControlObject->SetStringField(TEXT("control"), ControlName.ToString());
1690
+ ControlObject->SetStringField(TEXT("valueType"), TEXT("float"));
1691
+ TArray<TSharedPtr<FJsonValue>> FrameSamples;
1692
+ for (int32 Index = 0; Index < Frames.Num(); ++Index)
1693
+ {
1694
+ auto FrameObject = MakeShared<FJsonObject>();
1695
+ FrameObject->SetNumberField(TEXT("frame"), Frames[Index].Value);
1696
+ FrameObject->SetNumberField(TEXT("value"), FloatValues[Index]);
1697
+ FrameSamples.Add(MakeShared<FJsonValueObject>(FrameObject));
1698
+ }
1699
+ ControlObject->SetArrayField(TEXT("samples"), FrameSamples);
1700
+ Samples.Add(MakeShared<FJsonValueObject>(ControlObject));
1701
+ }
1702
+ for (const FName ControlName : IntControlNames)
1703
+ {
1704
+ const TArray<int32> IntValues = UControlRigSequencerEditorLibrary::GetLocalControlRigInts(
1705
+ Session.Sequence, Session.ControlRig, ControlName, Frames, EMovieSceneTimeUnit::DisplayRate);
1706
+ if (IntValues.Num() != Frames.Num()) return MCPError(TEXT("Control Rig integer evaluation returned an incomplete frame set"));
1707
+ const FRigControlElement* Control = Session.ControlRig->FindControl(ControlName);
1708
+ const UEnum* ControlEnum = Control ? Control->Settings.ControlEnum.Get() : nullptr;
1709
+ auto ControlObject = MakeShared<FJsonObject>();
1710
+ ControlObject->SetStringField(TEXT("control"), ControlName.ToString());
1711
+ ControlObject->SetStringField(TEXT("valueType"), ControlEnum ? TEXT("enum") : TEXT("int"));
1712
+ TArray<TSharedPtr<FJsonValue>> FrameSamples;
1713
+ for (int32 Index = 0; Index < Frames.Num(); ++Index)
1714
+ {
1715
+ auto FrameObject = MakeShared<FJsonObject>();
1716
+ FrameObject->SetNumberField(TEXT("frame"), Frames[Index].Value);
1717
+ FrameObject->SetNumberField(TEXT("value"), IntValues[Index]);
1718
+ const int32 EnumIndex = ControlRigSequencerFindEnumOption(ControlEnum, IntValues[Index]);
1719
+ if (EnumIndex != INDEX_NONE)
1720
+ {
1721
+ FrameObject->SetStringField(TEXT("enumOption"), ControlEnum->GetNameStringByIndex(EnumIndex));
1722
+ }
1723
+ FrameSamples.Add(MakeShared<FJsonValueObject>(FrameObject));
1724
+ }
1725
+ ControlObject->SetArrayField(TEXT("samples"), FrameSamples);
1726
+ Samples.Add(MakeShared<FJsonValueObject>(ControlObject));
1727
+ }
1728
+ Result->SetArrayField(TEXT("samples"), Samples);
1729
+ return MCPResult(Result);
1730
+ #endif
1731
+ }
1732
+
1733
+ TSharedPtr<FJsonValue> FAnimationHandlers::ApplyControlRigEdits(const TSharedPtr<FJsonObject>& Params)
1734
+ {
1735
+ #if !UE_MCP_HAS_5_8_API
1736
+ return ControlRigSequencerUnsupported();
1737
+ #else
1738
+ FString SequencePath;
1739
+ if (auto Error = RequireString(Params, TEXT("sequencePath"), SequencePath)) return Error;
1740
+ if (MCPIsProtectedAssetPath(SequencePath))
1741
+ {
1742
+ return MCPError(FString::Printf(TEXT("Protected asset cannot be modified: %s"), *SequencePath));
1743
+ }
1744
+ ULevelSequence* Sequence = Cast<ULevelSequence>(UEditorAssetLibrary::LoadAsset(SequencePath));
1745
+ if (!Sequence) return MCPError(FString::Printf(TEXT("LevelSequence not found: %s"), *SequencePath));
1746
+ FControlRigSequenceFocusGuard Focus(Sequence);
1747
+ if (!Focus.IsReady()) return MCPError(TEXT("Could not focus the LevelSequence in Sequencer"));
1748
+
1749
+ FControlRigSequenceSession Session;
1750
+ FString Error;
1751
+ if (!ControlRigSequencerResolveSession(Params, Session, Error)) return MCPError(Error);
1752
+ if (Session.Section->GetDoNotKey()) return MCPError(TEXT("The resolved Control Rig section is marked Do Not Key"));
1753
+ const TArray<TSharedPtr<FJsonValue>>* Operations = nullptr;
1754
+ if (!Params->TryGetArrayField(TEXT("operations"), Operations) || !Operations || Operations->IsEmpty())
1755
+ {
1756
+ return MCPError(TEXT("'operations' must be a non-empty array"));
1757
+ }
1758
+
1759
+ int32 RangeStart = 0;
1760
+ int32 RangeEndExclusive = 0;
1761
+ ControlRigSequencerDisplayRange(Session.MovieScene, RangeStart, RangeEndExclusive);
1762
+ TArray<FControlRigPreparedWrite> Prepared;
1763
+ TArray<FControlRigPreparedContactQA> PreparedContacts;
1764
+ TSet<FString> WrittenKeys;
1765
+
1766
+ for (int32 OperationIndex = 0; OperationIndex < Operations->Num(); ++OperationIndex)
1767
+ {
1768
+ const TSharedPtr<FJsonObject> Operation = (*Operations)[OperationIndex].IsValid()
1769
+ ? (*Operations)[OperationIndex]->AsObject() : nullptr;
1770
+ if (!Operation.IsValid()) return MCPError(FString::Printf(TEXT("operations[%d] must be an object"), OperationIndex));
1771
+ FString Op;
1772
+ FString ControlString;
1773
+ if (!Operation->TryGetStringField(TEXT("op"), Op) || Op.IsEmpty())
1774
+ return MCPError(FString::Printf(TEXT("operations[%d].op is required"), OperationIndex));
1775
+ if (!Operation->TryGetStringField(TEXT("control"), ControlString) || ControlString.IsEmpty())
1776
+ return MCPError(FString::Printf(TEXT("operations[%d].control is required"), OperationIndex));
1777
+ Op.ToLowerInline();
1778
+ const FName ControlName(*ControlString);
1779
+ FRigControlElement* Control = Session.ControlRig->FindControl(ControlName);
1780
+ if (!Control) return MCPError(FString::Printf(TEXT("Control not found: %s"), *ControlString));
1781
+ if (!Control->Settings.IsAnimatable()) return MCPError(FString::Printf(TEXT("Control is not animatable: %s"), *ControlString));
1782
+ const bool bSetOperation = Op == TEXT("set");
1783
+ const bool bSetKeysOperation = Op == TEXT("set_keys");
1784
+ const bool bOffsetOperation = Op == TEXT("offset");
1785
+ const bool bContactOperation = Op == TEXT("contact_lock");
1786
+ const bool bBoolOperation = Op == TEXT("set_bool");
1787
+ const bool bFloatOperation = Op == TEXT("set_float");
1788
+ const bool bIntOperation = Op == TEXT("set_int");
1789
+ if (!bSetOperation && !bSetKeysOperation && !bOffsetOperation && !bContactOperation
1790
+ && !bBoolOperation && !bFloatOperation && !bIntOperation)
1791
+ {
1792
+ return MCPError(FString::Printf(
1793
+ TEXT("operations[%d].op must be 'set_keys', 'set', 'offset', 'contact_lock', 'set_bool', 'set_float', or 'set_int'"),
1794
+ OperationIndex));
1795
+ }
1796
+ if (bBoolOperation)
1797
+ {
1798
+ if (Control->Settings.ControlType != ERigControlType::Bool)
1799
+ return MCPError(FString::Printf(TEXT("Bool control not found: %s"), *ControlString));
1800
+ }
1801
+ else if (bFloatOperation)
1802
+ {
1803
+ if (!ControlRigSequencerIsFloatControl(Control))
1804
+ return MCPError(FString::Printf(TEXT("Float control not found: %s"), *ControlString));
1805
+ }
1806
+ else if (bIntOperation)
1807
+ {
1808
+ if (Control->Settings.ControlType != ERigControlType::Integer)
1809
+ return MCPError(FString::Printf(TEXT("Integer control not found: %s"), *ControlString));
1810
+ }
1811
+ else if (!ControlRigSequencerIsTransformControl(Control))
1812
+ {
1813
+ return MCPError(FString::Printf(TEXT("Transform control not found: %s"), *ControlString));
1814
+ }
1815
+ else if (bContactOperation && !ControlRigSequencerControlHasTranslation(Control->Settings.ControlType))
1816
+ {
1817
+ return MCPError(FString::Printf(
1818
+ TEXT("contact_lock driver control must support translation: %s"), *ControlString));
1819
+ }
1820
+
1821
+ FControlRigPreparedWrite Write;
1822
+ Write.Control = ControlName;
1823
+ Write.Op = Op;
1824
+ Write.ValueType = bBoolOperation ? EControlRigPreparedValueType::Bool
1825
+ : bFloatOperation ? EControlRigPreparedValueType::Float
1826
+ : bIntOperation ? EControlRigPreparedValueType::Integer
1827
+ : EControlRigPreparedValueType::Transform;
1828
+ TArray<FTransform> AbsoluteKeyTransforms;
1829
+ if (Write.ValueType == EControlRigPreparedValueType::Transform)
1830
+ {
1831
+ if (bContactOperation)
1832
+ {
1833
+ if (Operation->HasField(TEXT("space")))
1834
+ return MCPError(FString::Printf(TEXT("operations[%d].contact_lock always uses component space"), OperationIndex));
1835
+ Write.Space = EControlRigTransformSpace::Global;
1836
+ }
1837
+ else
1838
+ {
1839
+ FString CanonicalSpace;
1840
+ if (!ControlRigSequencerReadSpace(OptionalString(Operation, TEXT("space"), TEXT("local")), Write.Space, CanonicalSpace, Error))
1841
+ return MCPError(FString::Printf(TEXT("operations[%d]: %s"), OperationIndex, *Error));
1842
+ }
1843
+ }
1844
+
1845
+ if (bSetKeysOperation)
1846
+ {
1847
+ const TArray<TSharedPtr<FJsonValue>>* Keys = nullptr;
1848
+ if (!Operation->TryGetArrayField(TEXT("keys"), Keys) || !Keys || Keys->IsEmpty())
1849
+ return MCPError(FString::Printf(TEXT("operations[%d].keys must be a non-empty array"), OperationIndex));
1850
+ int32 PreviousFrame = 0;
1851
+ FQuat PreviousRotation = FQuat::Identity;
1852
+ for (int32 KeyIndex = 0; KeyIndex < Keys->Num(); ++KeyIndex)
1853
+ {
1854
+ const TSharedPtr<FJsonValue>& KeyValue = (*Keys)[KeyIndex];
1855
+ if (!KeyValue.IsValid() || KeyValue->Type != EJson::Object)
1856
+ return MCPError(FString::Printf(TEXT("operations[%d].keys[%d] must be an object"), OperationIndex, KeyIndex));
1857
+ const TSharedPtr<FJsonObject> KeyObject = KeyValue->AsObject();
1858
+ double FrameNumber = 0.0;
1859
+ if (!KeyObject.IsValid()
1860
+ || !KeyObject->TryGetNumberField(TEXT("frame"), FrameNumber)
1861
+ || !FMath::IsFinite(FrameNumber)
1862
+ || !FMath::IsNearlyEqual(FrameNumber, FMath::RoundToDouble(FrameNumber))
1863
+ || FrameNumber < static_cast<double>(MIN_int32)
1864
+ || FrameNumber > static_cast<double>(MAX_int32))
1865
+ {
1866
+ return MCPError(FString::Printf(TEXT("operations[%d].keys[%d].frame must be a 32-bit integer"), OperationIndex, KeyIndex));
1867
+ }
1868
+ const int32 Frame = static_cast<int32>(FMath::RoundToInt(FrameNumber));
1869
+ if (KeyIndex > 0 && Frame <= PreviousFrame)
1870
+ {
1871
+ return MCPError(FString::Printf(
1872
+ TEXT("operations[%d].keys frames must be strictly increasing and unique"),
1873
+ OperationIndex));
1874
+ }
1875
+ PreviousFrame = Frame;
1876
+
1877
+ const TSharedPtr<FJsonObject>* TransformObject = nullptr;
1878
+ if (!KeyObject->TryGetObjectField(TEXT("transform"), TransformObject)
1879
+ || !TransformObject || !TransformObject->IsValid())
1880
+ {
1881
+ return MCPError(FString::Printf(TEXT("operations[%d].keys[%d].transform must be an object"), OperationIndex, KeyIndex));
1882
+ }
1883
+ if ((*TransformObject)->Values.Num() != 3
1884
+ || !(*TransformObject)->HasField(TEXT("translation"))
1885
+ || !(*TransformObject)->HasField(TEXT("rotationQuaternion"))
1886
+ || !(*TransformObject)->HasField(TEXT("scale")))
1887
+ {
1888
+ return MCPError(FString::Printf(
1889
+ TEXT("operations[%d].keys[%d].transform must contain exactly translation, rotationQuaternion and scale"),
1890
+ OperationIndex, KeyIndex));
1891
+ }
1892
+ FVector Translation;
1893
+ FQuat Rotation;
1894
+ FVector Scale;
1895
+ if (!ControlRigSequencerReadVector(*TransformObject, TEXT("translation"), Translation, Error)
1896
+ || !ControlRigSequencerReadNormalizedQuaternion(*TransformObject, Rotation, Error)
1897
+ || !ControlRigSequencerReadVector(*TransformObject, TEXT("scale"), Scale, Error))
1898
+ {
1899
+ return MCPError(FString::Printf(TEXT("operations[%d].keys[%d].transform: %s"), OperationIndex, KeyIndex, *Error));
1900
+ }
1901
+ if (KeyIndex > 0 && (PreviousRotation | Rotation) < 0.0)
1902
+ {
1903
+ Rotation = FQuat(-Rotation.X, -Rotation.Y, -Rotation.Z, -Rotation.W);
1904
+ }
1905
+ PreviousRotation = Rotation;
1906
+ Write.Frames.Add(FFrameNumber(Frame));
1907
+ AbsoluteKeyTransforms.Add(FTransform(Rotation, Translation, Scale));
1908
+ }
1909
+ }
1910
+ else if (bSetOperation || bBoolOperation || bFloatOperation || bIntOperation)
1911
+ {
1912
+ if (!ControlRigSequencerReadFrames(Operation, Write.Frames, Error))
1913
+ return MCPError(FString::Printf(TEXT("operations[%d]: %s"), OperationIndex, *Error));
1914
+ if (bBoolOperation && !Operation->TryGetBoolField(TEXT("value"), Write.BoolValue))
1915
+ return MCPError(FString::Printf(TEXT("operations[%d].value must be a boolean"), OperationIndex));
1916
+ if (bFloatOperation)
1917
+ {
1918
+ double Value = 0.0;
1919
+ if (!Operation->TryGetNumberField(TEXT("value"), Value) || !FMath::IsFinite(Value))
1920
+ return MCPError(FString::Printf(TEXT("operations[%d].value must be a finite number"), OperationIndex));
1921
+ Write.FloatValue = static_cast<float>(Value);
1922
+ if (!FMath::IsFinite(Write.FloatValue))
1923
+ return MCPError(FString::Printf(TEXT("operations[%d].value is outside the float range"), OperationIndex));
1924
+ }
1925
+ if (bIntOperation)
1926
+ {
1927
+ double Value = 0.0;
1928
+ if (!Operation->TryGetNumberField(TEXT("value"), Value)
1929
+ || !FMath::IsFinite(Value)
1930
+ || !FMath::IsNearlyEqual(Value, FMath::RoundToDouble(Value))
1931
+ || Value < static_cast<double>(MIN_int32)
1932
+ || Value > static_cast<double>(MAX_int32))
1933
+ {
1934
+ return MCPError(FString::Printf(TEXT("operations[%d].value must be a 32-bit integer"), OperationIndex));
1935
+ }
1936
+ Write.IntValue = static_cast<int32>(FMath::RoundToInt(Value));
1937
+ if (const UEnum* ControlEnum = Control->Settings.ControlEnum.Get())
1938
+ {
1939
+ if (Write.IntValue < 0 || Write.IntValue > MAX_uint8)
1940
+ {
1941
+ return MCPError(FString::Printf(
1942
+ TEXT("operations[%d].value is outside the byte range used by Sequencer enum channels"),
1943
+ OperationIndex));
1944
+ }
1945
+ if (!ControlRigSequencerIsValidEnumValue(ControlEnum, Write.IntValue))
1946
+ {
1947
+ return MCPError(FString::Printf(
1948
+ TEXT("operations[%d].value is not a selectable option in enum %s"),
1949
+ OperationIndex, *ControlEnum->GetPathName()));
1950
+ }
1951
+ }
1952
+ }
1953
+ }
1954
+ else if (bOffsetOperation || bContactOperation)
1955
+ {
1956
+ double StartNumber = 0.0;
1957
+ double EndNumber = 0.0;
1958
+ if (!Operation->TryGetNumberField(TEXT("startFrame"), StartNumber)
1959
+ || !Operation->TryGetNumberField(TEXT("endFrame"), EndNumber)
1960
+ || !FMath::IsFinite(StartNumber) || !FMath::IsFinite(EndNumber)
1961
+ || !FMath::IsNearlyEqual(StartNumber, FMath::RoundToDouble(StartNumber))
1962
+ || !FMath::IsNearlyEqual(EndNumber, FMath::RoundToDouble(EndNumber))
1963
+ || StartNumber < static_cast<double>(MIN_int32) || StartNumber > static_cast<double>(MAX_int32)
1964
+ || EndNumber < static_cast<double>(MIN_int32) || EndNumber > static_cast<double>(MAX_int32))
1965
+ {
1966
+ return MCPError(FString::Printf(TEXT("operations[%d] requires integer startFrame and endFrame"), OperationIndex));
1967
+ }
1968
+ const int32 Start = static_cast<int32>(FMath::RoundToInt(StartNumber));
1969
+ const int32 End = static_cast<int32>(FMath::RoundToInt(EndNumber));
1970
+ if (End < Start) return MCPError(FString::Printf(TEXT("operations[%d].endFrame must be at least startFrame"), OperationIndex));
1971
+ if (Start < RangeStart || End >= RangeEndExclusive)
1972
+ {
1973
+ return MCPError(FString::Printf(
1974
+ TEXT("operations[%d] range [%d, %d] is outside [%d, %d)"),
1975
+ OperationIndex, Start, End, RangeStart, RangeEndExclusive));
1976
+ }
1977
+ const int64 FrameCount = static_cast<int64>(End) - static_cast<int64>(Start) + 1;
1978
+ if (FrameCount > ControlRigSequencerMaxFrames)
1979
+ {
1980
+ return MCPError(FString::Printf(
1981
+ TEXT("operations[%d] is limited to %d frames"),
1982
+ OperationIndex, ControlRigSequencerMaxFrames));
1983
+ }
1984
+ Write.Frames.Reserve(static_cast<int32>(FrameCount));
1985
+ for (int64 Frame = Start; Frame <= End; ++Frame) Write.Frames.Add(FFrameNumber(static_cast<int32>(Frame)));
1986
+ }
1987
+
1988
+ for (FFrameNumber Frame : Write.Frames)
1989
+ {
1990
+ if (Frame.Value < RangeStart || Frame.Value >= RangeEndExclusive)
1991
+ return MCPError(FString::Printf(TEXT("operations[%d] frame %d is outside [%d, %d)"), OperationIndex, Frame.Value, RangeStart, RangeEndExclusive));
1992
+ }
1993
+ if (!ControlRigSequencerRegisterWriteFrames(WrittenKeys, ControlName, Write.Frames, OperationIndex, Error))
1994
+ return MCPError(Error);
1995
+
1996
+ if (Write.ValueType == EControlRigPreparedValueType::Bool)
1997
+ {
1998
+ const TArray<bool> Existing = UControlRigSequencerEditorLibrary::GetLocalControlRigBools(
1999
+ Session.Sequence, Session.ControlRig, ControlName, Write.Frames, EMovieSceneTimeUnit::DisplayRate);
2000
+ if (Existing.Num() != Write.Frames.Num())
2001
+ return MCPError(FString::Printf(TEXT("Could not sample %s before applying operations[%d]"), *ControlString, OperationIndex));
2002
+ }
2003
+ else if (Write.ValueType == EControlRigPreparedValueType::Float)
2004
+ {
2005
+ const TArray<float> Existing = UControlRigSequencerEditorLibrary::GetLocalControlRigFloats(
2006
+ Session.Sequence, Session.ControlRig, ControlName, Write.Frames, EMovieSceneTimeUnit::DisplayRate);
2007
+ if (Existing.Num() != Write.Frames.Num())
2008
+ return MCPError(FString::Printf(TEXT("Could not sample %s before applying operations[%d]"), *ControlString, OperationIndex));
2009
+ }
2010
+ else if (Write.ValueType == EControlRigPreparedValueType::Integer)
2011
+ {
2012
+ const TArray<int32> Existing = UControlRigSequencerEditorLibrary::GetLocalControlRigInts(
2013
+ Session.Sequence, Session.ControlRig, ControlName, Write.Frames, EMovieSceneTimeUnit::DisplayRate);
2014
+ if (Existing.Num() != Write.Frames.Num())
2015
+ return MCPError(FString::Printf(TEXT("Could not sample %s before applying operations[%d]"), *ControlString, OperationIndex));
2016
+ }
2017
+ else
2018
+ {
2019
+ if (bContactOperation)
2020
+ {
2021
+ const TSharedPtr<FJsonObject>* TargetObject = nullptr;
2022
+ if (!Operation->TryGetObjectField(TEXT("target"), TargetObject)
2023
+ || !TargetObject || !TargetObject->IsValid())
2024
+ {
2025
+ return MCPError(FString::Printf(TEXT("operations[%d].target must be an object"), OperationIndex));
2026
+ }
2027
+ const bool bTargetRotation = (*TargetObject)->HasField(TEXT("rotationQuaternion"));
2028
+ if (!(*TargetObject)->HasField(TEXT("translation"))
2029
+ || (*TargetObject)->Values.Num() != (bTargetRotation ? 2 : 1))
2030
+ {
2031
+ return MCPError(FString::Printf(
2032
+ TEXT("operations[%d].target must contain translation and optional rotationQuaternion only"),
2033
+ OperationIndex));
2034
+ }
2035
+ if (bTargetRotation && !ControlRigSequencerControlHasRotation(Control->Settings.ControlType))
2036
+ {
2037
+ return MCPError(FString::Printf(
2038
+ TEXT("contact_lock driver control must support rotation when target.rotationQuaternion is set: %s"),
2039
+ *ControlString));
2040
+ }
2041
+
2042
+ FVector TargetTranslation;
2043
+ FQuat TargetRotation = FQuat::Identity;
2044
+ if (!ControlRigSequencerReadVector(*TargetObject, TEXT("translation"), TargetTranslation, Error)
2045
+ || (bTargetRotation && !ControlRigSequencerReadNormalizedQuaternion(*TargetObject, TargetRotation, Error)))
2046
+ {
2047
+ return MCPError(FString::Printf(TEXT("operations[%d].target: %s"), OperationIndex, *Error));
2048
+ }
2049
+
2050
+ auto ReadNonNegativeFrameCount = [&](const TCHAR* Field, int32& OutValue) -> bool
2051
+ {
2052
+ OutValue = 0;
2053
+ const TSharedPtr<FJsonValue>* Value = Operation->Values.Find(Field);
2054
+ if (!Value || !Value->IsValid() || (*Value)->IsNull()) return true;
2055
+ if ((*Value)->Type != EJson::Number)
2056
+ {
2057
+ Error = FString::Printf(TEXT("operations[%d].%s must be a non-negative integer"), OperationIndex, Field);
2058
+ return false;
2059
+ }
2060
+ const double Number = (*Value)->AsNumber();
2061
+ if (!FMath::IsFinite(Number)
2062
+ || !FMath::IsNearlyEqual(Number, FMath::RoundToDouble(Number))
2063
+ || Number < 0.0 || Number > static_cast<double>(MAX_int32))
2064
+ {
2065
+ Error = FString::Printf(TEXT("operations[%d].%s must be a non-negative integer"), OperationIndex, Field);
2066
+ return false;
2067
+ }
2068
+ OutValue = static_cast<int32>(FMath::RoundToInt(Number));
2069
+ return true;
2070
+ };
2071
+ int32 BlendIn = 0;
2072
+ int32 BlendOut = 0;
2073
+ if (!ReadNonNegativeFrameCount(TEXT("blendInFrames"), BlendIn)
2074
+ || !ReadNonNegativeFrameCount(TEXT("blendOutFrames"), BlendOut))
2075
+ {
2076
+ return MCPError(Error);
2077
+ }
2078
+ const int64 IntervalCount = static_cast<int64>(Write.Frames.Last().Value)
2079
+ - static_cast<int64>(Write.Frames[0].Value);
2080
+ if (static_cast<int64>(BlendIn) + static_cast<int64>(BlendOut) > IntervalCount)
2081
+ {
2082
+ return MCPError(FString::Printf(
2083
+ TEXT("operations[%d] blends must leave at least one fully constrained frame"),
2084
+ OperationIndex));
2085
+ }
2086
+
2087
+ auto ReadTolerance = [&](const TCHAR* Field, double Default, double Maximum, double& OutValue) -> bool
2088
+ {
2089
+ OutValue = Default;
2090
+ const TSharedPtr<FJsonValue>* Value = Operation->Values.Find(Field);
2091
+ if (!Value || !Value->IsValid() || (*Value)->IsNull()) return true;
2092
+ if ((*Value)->Type != EJson::Number)
2093
+ {
2094
+ Error = FString::Printf(TEXT("operations[%d].%s must be a positive finite number"), OperationIndex, Field);
2095
+ return false;
2096
+ }
2097
+ const double Number = (*Value)->AsNumber();
2098
+ if (!FMath::IsFinite(Number) || Number <= 0.0 || Number > Maximum)
2099
+ {
2100
+ Error = FString::Printf(
2101
+ TEXT("operations[%d].%s must be greater than zero and at most %.3f"),
2102
+ OperationIndex, Field, Maximum);
2103
+ return false;
2104
+ }
2105
+ OutValue = Number;
2106
+ return true;
2107
+ };
2108
+ double PositionToleranceCm = 0.1;
2109
+ double RotationToleranceDegrees = 0.5;
2110
+ if (!ReadTolerance(TEXT("positionToleranceCm"), 0.1, 100.0, PositionToleranceCm)
2111
+ || !ReadTolerance(TEXT("rotationToleranceDegrees"), 0.5, 180.0, RotationToleranceDegrees))
2112
+ {
2113
+ return MCPError(Error);
2114
+ }
2115
+
2116
+ FString DrivenReferenceString;
2117
+ const bool bHasDrivenReference = Operation->HasField(TEXT("drivenReference"));
2118
+ if (bHasDrivenReference
2119
+ && (!Operation->TryGetStringField(TEXT("drivenReference"), DrivenReferenceString)
2120
+ || DrivenReferenceString.IsEmpty()))
2121
+ {
2122
+ return MCPError(FString::Printf(
2123
+ TEXT("operations[%d].drivenReference must be a non-empty bone or socket name"),
2124
+ OperationIndex));
2125
+ }
2126
+ const FName DrivenReference(*DrivenReferenceString);
2127
+
2128
+ bool bUseFkRotationChain = false;
2129
+ TArray<int32> FkChainBoneIndices;
2130
+ TArray<FName> FkChainControls;
2131
+ int32 FkChainWriteCount = 0;
2132
+ if (bHasDrivenReference && Cast<UFKControlRig>(Session.ControlRig))
2133
+ {
2134
+ const UFKControlRig* FkRig = CastChecked<UFKControlRig>(Session.ControlRig);
2135
+ USkeletalMeshComponent* Component = ControlRigSequencerBoundSkeletalMesh(Session.ControlRig);
2136
+ USkeletalMesh* Mesh = Component ? Component->GetSkeletalMeshAsset() : nullptr;
2137
+ USkeleton* Skeleton = Mesh ? Mesh->GetSkeleton() : nullptr;
2138
+ if (!Mesh || !Skeleton)
2139
+ {
2140
+ return MCPError(TEXT("FK contact_lock requires a bound skeletal mesh and skeleton"));
2141
+ }
2142
+
2143
+ const FReferenceSkeleton& ReferenceSkeleton = Mesh->GetRefSkeleton();
2144
+ const FName DriverBone = UFKControlRig::GetControlTargetName(
2145
+ ControlName, ERigElementType::Bone);
2146
+ const int32 DriverBoneIndex = ReferenceSkeleton.FindBoneIndex(DriverBone);
2147
+ bool bDrivenReferenceIsSocket = false;
2148
+ int32 DrivenBoneIndex = ReferenceSkeleton.FindBoneIndex(DrivenReference);
2149
+ if (DrivenBoneIndex == INDEX_NONE)
2150
+ {
2151
+ const USkeletalMeshSocket* Socket = Component->GetSocketByName(DrivenReference);
2152
+ bDrivenReferenceIsSocket = Socket != nullptr;
2153
+ DrivenBoneIndex = Socket
2154
+ ? ReferenceSkeleton.FindBoneIndex(Socket->BoneName)
2155
+ : INDEX_NONE;
2156
+ }
2157
+ if (DriverBoneIndex == INDEX_NONE || DrivenBoneIndex == INDEX_NONE)
2158
+ {
2159
+ return MCPError(FString::Printf(
2160
+ TEXT("FK contact_lock could not resolve driver %s and driven reference %s to bones"),
2161
+ *ControlString, *DrivenReferenceString));
2162
+ }
2163
+
2164
+ const int32 SkeletonDriverIndex = Skeleton->GetSkeletonBoneIndexFromMeshBoneIndex(
2165
+ Mesh, DriverBoneIndex);
2166
+ bUseFkRotationChain = SkeletonDriverIndex != INDEX_NONE
2167
+ && Skeleton->GetBoneTranslationRetargetingMode(SkeletonDriverIndex)
2168
+ == EBoneTranslationRetargetingMode::Skeleton;
2169
+ if (bUseFkRotationChain)
2170
+ {
2171
+ if (FkRig->GetApplyMode() != EControlRigFKRigExecuteMode::Replace)
2172
+ {
2173
+ return MCPError(
2174
+ TEXT("contact_lock_runtime_translation_unsupported: FK rotation-chain contact requires Replace apply mode"));
2175
+ }
2176
+ if (bDrivenReferenceIsSocket)
2177
+ {
2178
+ return MCPError(FString::Printf(
2179
+ TEXT("contact_lock_runtime_translation_unsupported: FK rotation-chain contact currently requires a driven bone; socket %s requires an asset Control Rig"),
2180
+ *DrivenReferenceString));
2181
+ }
2182
+ for (int32 BoneIndex = DrivenBoneIndex;
2183
+ BoneIndex != INDEX_NONE;
2184
+ BoneIndex = ReferenceSkeleton.GetParentIndex(BoneIndex))
2185
+ {
2186
+ FkChainBoneIndices.Add(BoneIndex);
2187
+ if (BoneIndex == DriverBoneIndex) break;
2188
+ }
2189
+ if (FkChainBoneIndices.IsEmpty() || FkChainBoneIndices.Last() != DriverBoneIndex)
2190
+ {
2191
+ return MCPError(FString::Printf(
2192
+ TEXT("FK contact_lock driver bone %s must be an ancestor of %s"),
2193
+ *DriverBone.ToString(),
2194
+ *ReferenceSkeleton.GetBoneName(DrivenBoneIndex).ToString()));
2195
+ }
2196
+ Algo::Reverse(FkChainBoneIndices);
2197
+ if (FkChainBoneIndices.Num() < 2)
2198
+ {
2199
+ return MCPError(FString::Printf(
2200
+ TEXT("contact_lock_runtime_translation_unsupported: FK bone %s ignores animation translation and has no descendant rotation chain"),
2201
+ *DriverBone.ToString()));
2202
+ }
2203
+ for (const int32 BoneIndex : FkChainBoneIndices)
2204
+ {
2205
+ const FName ChainControl = UFKControlRig::GetControlName(
2206
+ ReferenceSkeleton.GetBoneName(BoneIndex), ERigElementType::Bone);
2207
+ const FRigControlElement* ChainElement = Session.ControlRig->FindControl(ChainControl);
2208
+ if (!ChainElement || !ChainElement->Settings.IsAnimatable()
2209
+ || !ControlRigSequencerControlHasRotation(ChainElement->Settings.ControlType))
2210
+ {
2211
+ return MCPError(FString::Printf(
2212
+ TEXT("FK contact_lock rotation-chain control is unavailable: %s"),
2213
+ *ChainControl.ToString()));
2214
+ }
2215
+ FkChainControls.Add(ChainControl);
2216
+ }
2217
+ // A position-only contact needs rotations through the end bone's parent.
2218
+ // Key the end control only when the caller explicitly requests orientation.
2219
+ FkChainWriteCount = bTargetRotation
2220
+ ? FkChainControls.Num()
2221
+ : FkChainControls.Num() - 1;
2222
+ for (int32 ChainIndex = 1; ChainIndex < FkChainWriteCount; ++ChainIndex)
2223
+ {
2224
+ if (!ControlRigSequencerRegisterWriteFrames(
2225
+ WrittenKeys, FkChainControls[ChainIndex], Write.Frames, OperationIndex, Error))
2226
+ {
2227
+ return MCPError(Error);
2228
+ }
2229
+ }
2230
+ }
2231
+ }
2232
+
2233
+ TArray<FName> StabilizerNames;
2234
+ const TSharedPtr<FJsonValue>* StabilizerValue = Operation->Values.Find(TEXT("stabilizeControls"));
2235
+ if (StabilizerValue && StabilizerValue->IsValid() && !(*StabilizerValue)->IsNull())
2236
+ {
2237
+ const TArray<TSharedPtr<FJsonValue>>* StabilizerValues = nullptr;
2238
+ if (!Operation->TryGetArrayField(TEXT("stabilizeControls"), StabilizerValues) || !StabilizerValues)
2239
+ {
2240
+ return MCPError(FString::Printf(TEXT("operations[%d].stabilizeControls must be an array"), OperationIndex));
2241
+ }
2242
+ if (bUseFkRotationChain && !StabilizerValues->IsEmpty())
2243
+ {
2244
+ return MCPError(FString::Printf(
2245
+ TEXT("operations[%d] cannot combine FK rotation-chain contact with stabilizer controls"),
2246
+ OperationIndex));
2247
+ }
2248
+ if (StabilizerValues->Num() > 8)
2249
+ {
2250
+ return MCPError(FString::Printf(TEXT("operations[%d] supports at most 8 stabilizer controls"), OperationIndex));
2251
+ }
2252
+ for (int32 StabilizerIndex = 0; StabilizerIndex < StabilizerValues->Num(); ++StabilizerIndex)
2253
+ {
2254
+ const TSharedPtr<FJsonValue>& Value = (*StabilizerValues)[StabilizerIndex];
2255
+ if (!Value.IsValid() || Value->Type != EJson::String || Value->AsString().IsEmpty())
2256
+ {
2257
+ return MCPError(FString::Printf(
2258
+ TEXT("operations[%d].stabilizeControls[%d] must be a non-empty string"),
2259
+ OperationIndex, StabilizerIndex));
2260
+ }
2261
+ const FName StabilizerName(*Value->AsString());
2262
+ if (StabilizerName == ControlName || StabilizerNames.Contains(StabilizerName))
2263
+ {
2264
+ return MCPError(FString::Printf(
2265
+ TEXT("operations[%d] stabilizers must be unique and cannot include the driver control"),
2266
+ OperationIndex));
2267
+ }
2268
+ FRigControlElement* Stabilizer = Session.ControlRig->FindControl(StabilizerName);
2269
+ if (!Stabilizer || !Stabilizer->Settings.IsAnimatable()
2270
+ || !ControlRigSequencerIsTransformControl(Stabilizer)
2271
+ || (!ControlRigSequencerControlHasTranslation(Stabilizer->Settings.ControlType)
2272
+ && !ControlRigSequencerControlHasRotation(Stabilizer->Settings.ControlType)))
2273
+ {
2274
+ return MCPError(FString::Printf(
2275
+ TEXT("contact_lock stabilizer must be an animatable position and/or rotation control: %s"),
2276
+ *StabilizerName.ToString()));
2277
+ }
2278
+ if (!ControlRigSequencerRegisterWriteFrames(
2279
+ WrittenKeys, StabilizerName, Write.Frames, OperationIndex, Error))
2280
+ {
2281
+ return MCPError(Error);
2282
+ }
2283
+ StabilizerNames.Add(StabilizerName);
2284
+ }
2285
+ }
2286
+
2287
+ const int32 ContactControlCount = bUseFkRotationChain
2288
+ ? FkChainWriteCount
2289
+ : 1;
2290
+ const int64 CellCount = static_cast<int64>(Write.Frames.Num())
2291
+ * static_cast<int64>(StabilizerNames.Num() + ContactControlCount);
2292
+ if (CellCount > ControlRigSequencerMaxFrames)
2293
+ {
2294
+ return MCPError(FString::Printf(
2295
+ TEXT("operations[%d] is limited to %d contact control-frame cells"),
2296
+ OperationIndex, ControlRigSequencerMaxFrames));
2297
+ }
2298
+
2299
+ TArray<FName> ContactControls{ControlName};
2300
+ ContactControls.Append(StabilizerNames);
2301
+ const TArray<FArrayOfRigControlTransforms> Existing =
2302
+ UControlRigSequencerEditorLibrary::BatchGetControlTransforms(
2303
+ Session.Sequence, Session.ControlRig, ContactControls, Write.Frames,
2304
+ EControlRigTransformSpace::Global, EMovieSceneTimeUnit::DisplayRate);
2305
+ if (Existing.Num() != ContactControls.Num())
2306
+ {
2307
+ return MCPError(FString::Printf(
2308
+ TEXT("Could not sample every contact_lock control before operations[%d]"),
2309
+ OperationIndex));
2310
+ }
2311
+ TMap<FName, const FArrayOfRigControlTransforms*> ExistingByControl;
2312
+ for (const FArrayOfRigControlTransforms& Values : Existing)
2313
+ {
2314
+ if (Values.Transforms.Num() != Write.Frames.Num())
2315
+ {
2316
+ return MCPError(FString::Printf(
2317
+ TEXT("Could not sample every contact_lock frame before operations[%d]"),
2318
+ OperationIndex));
2319
+ }
2320
+ ExistingByControl.Add(Values.ControlName, &Values);
2321
+ }
2322
+ const FArrayOfRigControlTransforms* const* DriverValues = ExistingByControl.Find(ControlName);
2323
+ if (!DriverValues || !*DriverValues)
2324
+ {
2325
+ return MCPError(FString::Printf(TEXT("Could not sample contact_lock driver %s"), *ControlString));
2326
+ }
2327
+ Write.Before = (*DriverValues)->Transforms;
2328
+ Write.After.SetNum(Write.Frames.Num());
2329
+
2330
+ TArray<TArray<FTransform>> FkSourceChainGlobals;
2331
+ if (bUseFkRotationChain)
2332
+ {
2333
+ const TArray<FArrayOfRigControlTransforms> GlobalChainValues =
2334
+ UControlRigSequencerEditorLibrary::BatchGetControlTransforms(
2335
+ Session.Sequence, Session.ControlRig, FkChainControls, Write.Frames,
2336
+ EControlRigTransformSpace::Global, EMovieSceneTimeUnit::DisplayRate);
2337
+ if (GlobalChainValues.Num() != FkChainControls.Num())
2338
+ {
2339
+ return MCPError(FString::Printf(
2340
+ TEXT("Could not sample every FK contact rotation-chain control before operations[%d]"),
2341
+ OperationIndex));
2342
+ }
2343
+ TMap<FName, const FArrayOfRigControlTransforms*> GlobalValuesByControl;
2344
+ for (const FArrayOfRigControlTransforms& Values : GlobalChainValues)
2345
+ {
2346
+ if (Values.Transforms.Num() != Write.Frames.Num())
2347
+ {
2348
+ return MCPError(FString::Printf(
2349
+ TEXT("Could not sample every FK contact rotation-chain frame before operations[%d]"),
2350
+ OperationIndex));
2351
+ }
2352
+ GlobalValuesByControl.Add(Values.ControlName, &Values);
2353
+ }
2354
+ FkSourceChainGlobals.SetNum(FkChainControls.Num());
2355
+ for (int32 ChainIndex = 0; ChainIndex < FkChainControls.Num(); ++ChainIndex)
2356
+ {
2357
+ const FArrayOfRigControlTransforms* const* Values =
2358
+ GlobalValuesByControl.Find(FkChainControls[ChainIndex]);
2359
+ if (!Values || !*Values)
2360
+ {
2361
+ return MCPError(FString::Printf(
2362
+ TEXT("Could not sample FK contact rotation-chain control %s"),
2363
+ *FkChainControls[ChainIndex].ToString()));
2364
+ }
2365
+ FkSourceChainGlobals[ChainIndex] = (*Values)->Transforms;
2366
+ }
2367
+ }
2368
+
2369
+ TArray<FTransform> SubjectBefore = Write.Before;
2370
+ if (bUseFkRotationChain)
2371
+ {
2372
+ // FK control globals include their initial offset and coincide with the
2373
+ // evaluated bone component transforms. Use them so an existing rig layer
2374
+ // is part of the solve instead of resampling only the source animation.
2375
+ SubjectBefore = FkSourceChainGlobals.Last();
2376
+ }
2377
+ else if (bHasDrivenReference
2378
+ && !ControlRigSequencerSampleReferenceTransforms(
2379
+ Session, DrivenReference, Write.Frames, SubjectBefore, Error))
2380
+ {
2381
+ return MCPError(FString::Printf(TEXT("operations[%d]: %s"), OperationIndex, *Error));
2382
+ }
2383
+
2384
+ FControlRigPreparedContactQA ContactQA;
2385
+ ContactQA.OperationIndex = OperationIndex;
2386
+ ContactQA.Control = ControlName;
2387
+ ContactQA.DrivenReference = DrivenReference;
2388
+ ContactQA.ControlType = Control->Settings.ControlType;
2389
+ ContactQA.bHasDrivenReference = bHasDrivenReference;
2390
+ ContactQA.bCheckRotation = bTargetRotation;
2391
+ ContactQA.PositionToleranceCm = PositionToleranceCm;
2392
+ ContactQA.RotationToleranceDegrees = RotationToleranceDegrees;
2393
+ ContactQA.Frames = Write.Frames;
2394
+ ContactQA.ExpectedSubject.SetNum(Write.Frames.Num());
2395
+
2396
+ TArray<double> Weights;
2397
+ Weights.SetNum(Write.Frames.Num());
2398
+ for (int32 Index = 0; Index < Write.Frames.Num(); ++Index)
2399
+ {
2400
+ const double Weight = ControlRigSequencerContactWeight(
2401
+ Index, Write.Frames.Num(), BlendIn, BlendOut);
2402
+ Weights[Index] = Weight;
2403
+ if (Weight >= 1.0 - UE_DOUBLE_SMALL_NUMBER) ++ContactQA.FullWeightFrameCount;
2404
+
2405
+ FTransform SubjectTarget = SubjectBefore[Index];
2406
+ SubjectTarget.SetTranslation(TargetTranslation);
2407
+ if (bTargetRotation) SubjectTarget.SetRotation(TargetRotation);
2408
+ ContactQA.ExpectedSubject[Index] = ControlRigSequencerBlendContactTransform(
2409
+ SubjectBefore[Index], SubjectTarget, Weight, bTargetRotation);
2410
+
2411
+ if (bUseFkRotationChain)
2412
+ {
2413
+ continue;
2414
+ }
2415
+ if (bHasDrivenReference)
2416
+ {
2417
+ const FTransform SubjectRelativeToDriver =
2418
+ SubjectBefore[Index].GetRelativeTransform(Write.Before[Index]);
2419
+ FTransform DriverTarget = SubjectRelativeToDriver.GetRelativeTransformReverse(
2420
+ ContactQA.ExpectedSubject[Index]);
2421
+ DriverTarget.SetScale3D(Write.Before[Index].GetScale3D());
2422
+ if (!ControlRigSequencerControlHasRotation(Control->Settings.ControlType))
2423
+ {
2424
+ DriverTarget.SetRotation(Write.Before[Index].GetRotation());
2425
+ }
2426
+ if (DriverTarget.ContainsNaN())
2427
+ {
2428
+ return MCPError(FString::Printf(
2429
+ TEXT("operations[%d] produced an invalid driver transform at frame %d"),
2430
+ OperationIndex, Write.Frames[Index].Value));
2431
+ }
2432
+ Write.After[Index] = DriverTarget;
2433
+ }
2434
+ else
2435
+ {
2436
+ Write.After[Index] = ContactQA.ExpectedSubject[Index];
2437
+ }
2438
+ }
2439
+
2440
+ TArray<FControlRigPreparedWrite> FkChainWrites;
2441
+ if (bUseFkRotationChain)
2442
+ {
2443
+ ContactQA.bUsedFkRotationChain = true;
2444
+ const TArray<FArrayOfRigControlTransforms> LocalControlValues =
2445
+ UControlRigSequencerEditorLibrary::BatchGetControlTransforms(
2446
+ Session.Sequence, Session.ControlRig, FkChainControls, Write.Frames,
2447
+ EControlRigTransformSpace::Local, EMovieSceneTimeUnit::DisplayRate);
2448
+ if (LocalControlValues.Num() != FkChainControls.Num())
2449
+ {
2450
+ return MCPError(FString::Printf(
2451
+ TEXT("Could not sample every FK contact rotation-chain control before operations[%d]"),
2452
+ OperationIndex));
2453
+ }
2454
+ TMap<FName, const FArrayOfRigControlTransforms*> LocalValuesByControl;
2455
+ for (const FArrayOfRigControlTransforms& Values : LocalControlValues)
2456
+ {
2457
+ if (Values.Transforms.Num() != Write.Frames.Num())
2458
+ {
2459
+ return MCPError(FString::Printf(
2460
+ TEXT("Could not sample every FK contact rotation-chain frame before operations[%d]"),
2461
+ OperationIndex));
2462
+ }
2463
+ LocalValuesByControl.Add(Values.ControlName, &Values);
2464
+ }
2465
+
2466
+ FkChainWrites.SetNum(FkChainWriteCount);
2467
+ TArray<FTransform> ControlOffsets;
2468
+ ControlOffsets.SetNum(FkChainWriteCount);
2469
+ for (int32 ChainIndex = 0; ChainIndex < FkChainWriteCount; ++ChainIndex)
2470
+ {
2471
+ const FArrayOfRigControlTransforms* const* LocalValues =
2472
+ LocalValuesByControl.Find(FkChainControls[ChainIndex]);
2473
+ FRigControlElement* ChainControl = Session.ControlRig->FindControl(FkChainControls[ChainIndex]);
2474
+ if (!LocalValues || !*LocalValues || !ChainControl)
2475
+ {
2476
+ return MCPError(FString::Printf(
2477
+ TEXT("Could not prepare FK contact rotation-chain control %s"),
2478
+ *FkChainControls[ChainIndex].ToString()));
2479
+ }
2480
+ FControlRigPreparedWrite& ChainWrite = FkChainWrites[ChainIndex];
2481
+ ChainWrite.Control = FkChainControls[ChainIndex];
2482
+ ChainWrite.Op = Op;
2483
+ ChainWrite.Space = EControlRigTransformSpace::Local;
2484
+ ChainWrite.ValueType = EControlRigPreparedValueType::Transform;
2485
+ ChainWrite.Frames = Write.Frames;
2486
+ ChainWrite.Before = (*LocalValues)->Transforms;
2487
+ ChainWrite.After.SetNum(Write.Frames.Num());
2488
+ ControlOffsets[ChainIndex] = Session.ControlRig->GetHierarchy()->GetControlOffsetTransform(
2489
+ ChainControl, ERigTransformType::InitialLocal);
2490
+ }
2491
+
2492
+ TArray<FTransform> PredictedSubjects;
2493
+ PredictedSubjects.SetNum(Write.Frames.Num());
2494
+ for (int32 FrameIndex = 0; FrameIndex < Write.Frames.Num(); ++FrameIndex)
2495
+ {
2496
+ TArray<FTransform> SourceGlobals;
2497
+ SourceGlobals.Reserve(FkChainBoneIndices.Num());
2498
+ for (const TArray<FTransform>& BoneSamples : FkSourceChainGlobals)
2499
+ {
2500
+ SourceGlobals.Add(BoneSamples[FrameIndex]);
2501
+ }
2502
+
2503
+ const FTransform SubjectRelativeToEnd =
2504
+ SubjectBefore[FrameIndex].GetRelativeTransform(SourceGlobals.Last());
2505
+ const FTransform TargetEnd = SubjectRelativeToEnd.GetRelativeTransformReverse(
2506
+ ContactQA.ExpectedSubject[FrameIndex]);
2507
+ TArray<FTransform> SolvedGlobals;
2508
+ double SolverPositionErrorCm = 0.0;
2509
+ if (!ControlRigSequencerSolveRotationChain(
2510
+ SourceGlobals, TargetEnd, bTargetRotation, SolvedGlobals, SolverPositionErrorCm)
2511
+ || !FMath::IsFinite(SolverPositionErrorCm))
2512
+ {
2513
+ return MCPError(FString::Printf(
2514
+ TEXT("operations[%d] could not solve the FK contact rotation chain at frame %d"),
2515
+ OperationIndex, Write.Frames[FrameIndex].Value));
2516
+ }
2517
+
2518
+ const FTransform SourceDriverLocal =
2519
+ FkChainWrites[0].Before[FrameIndex] * ControlOffsets[0];
2520
+ FTransform SourceParent =
2521
+ SourceDriverLocal.GetRelativeTransformReverse(SourceGlobals[0]);
2522
+ FTransform DesiredParent = SourceParent;
2523
+ for (int32 ChainIndex = 0; ChainIndex < FkChainBoneIndices.Num(); ++ChainIndex)
2524
+ {
2525
+ const FTransform SourceLocal = SourceGlobals[ChainIndex].GetRelativeTransform(SourceParent);
2526
+ FTransform DesiredLocal = SourceLocal;
2527
+ if (ChainIndex < FkChainWriteCount)
2528
+ {
2529
+ DesiredLocal = SolvedGlobals[ChainIndex].GetRelativeTransform(DesiredParent);
2530
+ // Skeleton-retargeted FK translations are discarded during playback. Keep the
2531
+ // source local lengths and express the contact correction in rotations only.
2532
+ DesiredLocal.SetTranslation(SourceLocal.GetTranslation());
2533
+ DesiredLocal.SetScale3D(SourceLocal.GetScale3D());
2534
+ DesiredLocal.NormalizeRotation();
2535
+ FkChainWrites[ChainIndex].After[FrameIndex] =
2536
+ DesiredLocal.GetRelativeTransform(ControlOffsets[ChainIndex]);
2537
+ }
2538
+ const FTransform DesiredGlobal = DesiredLocal * DesiredParent;
2539
+ SourceParent = SourceGlobals[ChainIndex];
2540
+ DesiredParent = DesiredGlobal;
2541
+ SolvedGlobals[ChainIndex] = DesiredGlobal;
2542
+ }
2543
+ PredictedSubjects[FrameIndex] = SubjectRelativeToEnd * SolvedGlobals.Last();
2544
+ }
2545
+
2546
+ ControlRigSequencerMeasureContact(
2547
+ Write.Frames, ContactQA.ExpectedSubject, PredictedSubjects,
2548
+ true, bTargetRotation, ContactQA.Metrics);
2549
+ if (ContactQA.Metrics.MaxPositionErrorCm > PositionToleranceCm
2550
+ || (bTargetRotation
2551
+ && ContactQA.Metrics.MaxRotationErrorDegrees > RotationToleranceDegrees))
2552
+ {
2553
+ return MCPError(FString::Printf(
2554
+ TEXT("contact_constraint_tolerance_exceeded: operations[%d] FK rotation-chain residual was %.4f cm at frame %d and %.4f degrees at frame %d"),
2555
+ OperationIndex,
2556
+ ContactQA.Metrics.MaxPositionErrorCm,
2557
+ ContactQA.Metrics.WorstPositionFrame,
2558
+ ContactQA.Metrics.MaxRotationErrorDegrees,
2559
+ ContactQA.Metrics.WorstRotationFrame));
2560
+ }
2561
+ }
2562
+
2563
+ for (const FName StabilizerName : StabilizerNames)
2564
+ {
2565
+ const FArrayOfRigControlTransforms* const* StabilizerValues = ExistingByControl.Find(StabilizerName);
2566
+ FRigControlElement* Stabilizer = Session.ControlRig->FindControl(StabilizerName);
2567
+ if (!StabilizerValues || !*StabilizerValues || !Stabilizer)
2568
+ {
2569
+ return MCPError(FString::Printf(TEXT("Could not prepare stabilizer %s"), *StabilizerName.ToString()));
2570
+ }
2571
+ FControlRigPreparedWrite StabilizerWrite;
2572
+ StabilizerWrite.Control = StabilizerName;
2573
+ StabilizerWrite.Op = Op;
2574
+ StabilizerWrite.Space = EControlRigTransformSpace::Global;
2575
+ StabilizerWrite.ValueType = EControlRigPreparedValueType::Transform;
2576
+ StabilizerWrite.Frames = Write.Frames;
2577
+ StabilizerWrite.Before = (*StabilizerValues)->Transforms;
2578
+ StabilizerWrite.After.SetNum(Write.Frames.Num());
2579
+ const FTransform Anchor = StabilizerWrite.Before[0];
2580
+ const bool bStabilizeRotation =
2581
+ ControlRigSequencerControlHasRotation(Stabilizer->Settings.ControlType);
2582
+ for (int32 Index = 0; Index < Write.Frames.Num(); ++Index)
2583
+ {
2584
+ StabilizerWrite.After[Index] = ControlRigSequencerBlendContactTransform(
2585
+ StabilizerWrite.Before[Index], Anchor, Weights[Index], bStabilizeRotation);
2586
+ }
2587
+
2588
+ FControlRigContactStabilizerQA StabilizerQA;
2589
+ StabilizerQA.Control = StabilizerName;
2590
+ StabilizerQA.ControlType = Stabilizer->Settings.ControlType;
2591
+ StabilizerQA.Expected = StabilizerWrite.After;
2592
+ ContactQA.Stabilizers.Add(MoveTemp(StabilizerQA));
2593
+ Prepared.Add(MoveTemp(StabilizerWrite));
2594
+ }
2595
+
2596
+ PreparedContacts.Add(MoveTemp(ContactQA));
2597
+ if (bUseFkRotationChain)
2598
+ {
2599
+ Prepared.Append(MoveTemp(FkChainWrites));
2600
+ }
2601
+ else
2602
+ {
2603
+ Prepared.Add(MoveTemp(Write));
2604
+ }
2605
+ continue;
2606
+ }
2607
+
2608
+ const TArray<FName> OneControl{ControlName};
2609
+ const TArray<FArrayOfRigControlTransforms> Existing = UControlRigSequencerEditorLibrary::BatchGetControlTransforms(
2610
+ Session.Sequence, Session.ControlRig, OneControl, Write.Frames, Write.Space, EMovieSceneTimeUnit::DisplayRate);
2611
+ if (Existing.Num() != 1 || Existing[0].Transforms.Num() != Write.Frames.Num())
2612
+ return MCPError(FString::Printf(TEXT("Could not sample %s before applying operations[%d]"), *ControlString, OperationIndex));
2613
+ Write.Before = Existing[0].Transforms;
2614
+ Write.After = Write.Before;
2615
+
2616
+ if (Op == TEXT("set"))
2617
+ {
2618
+ const TArray<TSharedPtr<FJsonValue>>* TransformValues = nullptr;
2619
+ const TSharedPtr<FJsonObject>* SingleTransform = nullptr;
2620
+ if (Operation->TryGetArrayField(TEXT("transforms"), TransformValues) && TransformValues)
2621
+ {
2622
+ if (TransformValues->Num() != Write.Frames.Num())
2623
+ return MCPError(FString::Printf(TEXT("operations[%d].transforms must match the frame count"), OperationIndex));
2624
+ for (int32 Index = 0; Index < TransformValues->Num(); ++Index)
2625
+ {
2626
+ const TSharedPtr<FJsonObject> TransformObject = (*TransformValues)[Index].IsValid()
2627
+ ? (*TransformValues)[Index]->AsObject() : nullptr;
2628
+ FControlRigTransformPatch Patch;
2629
+ if (!ControlRigSequencerReadTransformPatch(TransformObject, true, Patch, Error))
2630
+ return MCPError(FString::Printf(TEXT("operations[%d].transforms[%d]: %s"), OperationIndex, Index, *Error));
2631
+ Write.After[Index] = ControlRigSequencerApplySetPatch(Write.Before[Index], Patch);
2632
+ }
2633
+ }
2634
+ else if (Operation->TryGetObjectField(TEXT("transform"), SingleTransform) && SingleTransform && SingleTransform->IsValid())
2635
+ {
2636
+ FControlRigTransformPatch Patch;
2637
+ if (!ControlRigSequencerReadTransformPatch(*SingleTransform, true, Patch, Error))
2638
+ return MCPError(FString::Printf(TEXT("operations[%d].transform: %s"), OperationIndex, *Error));
2639
+ for (int32 Index = 0; Index < Write.After.Num(); ++Index)
2640
+ Write.After[Index] = ControlRigSequencerApplySetPatch(Write.Before[Index], Patch);
2641
+ }
2642
+ else
2643
+ {
2644
+ return MCPError(FString::Printf(TEXT("operations[%d] requires 'transform' or 'transforms'"), OperationIndex));
2645
+ }
2646
+ }
2647
+ else if (Op == TEXT("set_keys"))
2648
+ {
2649
+ if (AbsoluteKeyTransforms.Num() != Write.Frames.Num())
2650
+ return MCPError(FString::Printf(TEXT("operations[%d].keys could not be prepared"), OperationIndex));
2651
+ Write.After = MoveTemp(AbsoluteKeyTransforms);
2652
+ }
2653
+ else
2654
+ {
2655
+ FControlRigTransformPatch Patch;
2656
+ if (!ControlRigSequencerReadTransformPatch(Operation, true, Patch, Error))
2657
+ return MCPError(FString::Printf(TEXT("operations[%d]: %s"), OperationIndex, *Error));
2658
+ if (!ControlRigSequencerPatchAffectsControl(Patch, Control->Settings.ControlType))
2659
+ {
2660
+ return MCPError(FString::Printf(
2661
+ TEXT("operations[%d] does not change a channel supported by %s"),
2662
+ OperationIndex, *ControlString));
2663
+ }
2664
+ const int32 BlendIn = FMath::Max(0, OptionalInt(Operation, TEXT("blendInFrames"), 0));
2665
+ const int32 BlendOut = FMath::Max(0, OptionalInt(Operation, TEXT("blendOutFrames"), 0));
2666
+ for (int32 Index = 0; Index < Write.After.Num(); ++Index)
2667
+ {
2668
+ double Weight = 1.0;
2669
+ if (BlendIn > 0) Weight = FMath::Min(Weight, static_cast<double>(Index) / static_cast<double>(BlendIn));
2670
+ if (BlendOut > 0) Weight = FMath::Min(Weight, static_cast<double>(Write.After.Num() - 1 - Index) / static_cast<double>(BlendOut));
2671
+ Write.After[Index] = ControlRigSequencerApplyOffset(Write.Before[Index], Patch, FMath::Clamp(Weight, 0.0, 1.0), Write.Space);
2672
+ }
2673
+ }
2674
+ }
2675
+ Prepared.Add(MoveTemp(Write));
2676
+ }
2677
+
2678
+ // All controls, frames and payloads have been resolved and sampled. Only now
2679
+ // do we create keys in the LevelSequence.
2680
+ bool bApplyFailed = false;
2681
+ FString ApplyError;
2682
+ {
2683
+ const FScopedTransaction Transaction(NSLOCTEXT("UE_MCP", "ApplyControlRigEdits", "Apply Control Rig Edits"));
2684
+ Session.Sequence->Modify();
2685
+ Session.MovieScene->Modify();
2686
+ Session.Section->Modify();
2687
+ for (const FControlRigPreparedWrite& Write : Prepared)
2688
+ {
2689
+ if (Write.ValueType == EControlRigPreparedValueType::Bool)
2690
+ {
2691
+ TArray<bool> Values;
2692
+ Values.Init(Write.BoolValue, Write.Frames.Num());
2693
+ Session.Track->SetSectionToKey(Session.Section, Write.Control);
2694
+ UControlRigSequencerEditorLibrary::SetLocalControlRigBools(
2695
+ Session.Sequence, Session.ControlRig, Write.Control, Write.Frames, Values,
2696
+ EMovieSceneTimeUnit::DisplayRate);
2697
+ const TArray<bool> Actual = UControlRigSequencerEditorLibrary::GetLocalControlRigBools(
2698
+ Session.Sequence, Session.ControlRig, Write.Control, Write.Frames,
2699
+ EMovieSceneTimeUnit::DisplayRate);
2700
+ if (Actual != Values)
2701
+ {
2702
+ bApplyFailed = true;
2703
+ ApplyError = FString::Printf(TEXT("Unreal failed while applying the prevalidated '%s' edit to %s"), *Write.Op, *Write.Control.ToString());
2704
+ break;
2705
+ }
2706
+ continue;
2707
+ }
2708
+ if (Write.ValueType == EControlRigPreparedValueType::Float)
2709
+ {
2710
+ TArray<float> Values;
2711
+ Values.Init(Write.FloatValue, Write.Frames.Num());
2712
+ Session.Track->SetSectionToKey(Session.Section, Write.Control);
2713
+ UControlRigSequencerEditorLibrary::SetLocalControlRigFloats(
2714
+ Session.Sequence, Session.ControlRig, Write.Control, Write.Frames, Values,
2715
+ EMovieSceneTimeUnit::DisplayRate);
2716
+ const TArray<float> Actual = UControlRigSequencerEditorLibrary::GetLocalControlRigFloats(
2717
+ Session.Sequence, Session.ControlRig, Write.Control, Write.Frames,
2718
+ EMovieSceneTimeUnit::DisplayRate);
2719
+ bool bMatches = Actual.Num() == Values.Num();
2720
+ for (int32 Index = 0; bMatches && Index < Values.Num(); ++Index)
2721
+ {
2722
+ bMatches = FMath::IsNearlyEqual(Actual[Index], Values[Index]);
2723
+ }
2724
+ if (!bMatches)
2725
+ {
2726
+ bApplyFailed = true;
2727
+ ApplyError = FString::Printf(TEXT("Unreal failed while applying the prevalidated '%s' edit to %s"), *Write.Op, *Write.Control.ToString());
2728
+ break;
2729
+ }
2730
+ continue;
2731
+ }
2732
+ if (Write.ValueType == EControlRigPreparedValueType::Integer)
2733
+ {
2734
+ TArray<int32> Values;
2735
+ Values.Init(Write.IntValue, Write.Frames.Num());
2736
+ Session.Track->SetSectionToKey(Session.Section, Write.Control);
2737
+ UControlRigSequencerEditorLibrary::SetLocalControlRigInts(
2738
+ Session.Sequence, Session.ControlRig, Write.Control, Write.Frames, Values,
2739
+ EMovieSceneTimeUnit::DisplayRate);
2740
+ const TArray<int32> Actual = UControlRigSequencerEditorLibrary::GetLocalControlRigInts(
2741
+ Session.Sequence, Session.ControlRig, Write.Control, Write.Frames,
2742
+ EMovieSceneTimeUnit::DisplayRate);
2743
+ if (Actual != Values)
2744
+ {
2745
+ bApplyFailed = true;
2746
+ ApplyError = FString::Printf(TEXT("Unreal failed while applying the prevalidated '%s' edit to %s"), *Write.Op, *Write.Control.ToString());
2747
+ break;
2748
+ }
2749
+ continue;
2750
+ }
2751
+ FArrayOfRigControlTransforms Values;
2752
+ Values.ControlName = Write.Control;
2753
+ Values.Transforms = Write.After;
2754
+ Session.Track->SetSectionToKey(Session.Section, Write.Control);
2755
+ if (!UControlRigSequencerEditorLibrary::BatchSetControlTransforms(
2756
+ Session.Sequence, Session.ControlRig, {Values}, Write.Frames, Write.Space,
2757
+ Session.Section, EMovieSceneTimeUnit::DisplayRate))
2758
+ {
2759
+ bApplyFailed = true;
2760
+ ApplyError = FString::Printf(TEXT("Unreal failed while applying the prevalidated '%s' edit to %s"), *Write.Op, *Write.Control.ToString());
2761
+ break;
2762
+ }
2763
+ {
2764
+ const TArray<FName> OneControl{Write.Control};
2765
+ const TArray<FArrayOfRigControlTransforms> Actual = UControlRigSequencerEditorLibrary::BatchGetControlTransforms(
2766
+ Session.Sequence, Session.ControlRig, OneControl, Write.Frames, Write.Space,
2767
+ EMovieSceneTimeUnit::DisplayRate);
2768
+ const FRigControlElement* Control = Session.ControlRig->FindControl(Write.Control);
2769
+ bool bMatches = Control && Actual.Num() == 1 && Actual[0].Transforms.Num() == Write.After.Num();
2770
+ for (int32 Index = 0; bMatches && Index < Write.After.Num(); ++Index)
2771
+ {
2772
+ bMatches = ControlRigSequencerTransformMatches(
2773
+ Write.After[Index], Actual[0].Transforms[Index], Control->Settings.ControlType);
2774
+ }
2775
+ if (!bMatches)
2776
+ {
2777
+ bApplyFailed = true;
2778
+ ApplyError = FString::Printf(TEXT("Unreal readback did not match the '%s' keys applied to %s"), *Write.Op, *Write.Control.ToString());
2779
+ break;
2780
+ }
2781
+ }
2782
+ }
2783
+
2784
+ if (!bApplyFailed)
2785
+ {
2786
+ for (FControlRigPreparedContactQA& Contact : PreparedContacts)
2787
+ {
2788
+ if (!Contact.bHasDrivenReference)
2789
+ {
2790
+ const TArray<FArrayOfRigControlTransforms> Actual =
2791
+ UControlRigSequencerEditorLibrary::BatchGetControlTransforms(
2792
+ Session.Sequence, Session.ControlRig, {Contact.Control}, Contact.Frames,
2793
+ EControlRigTransformSpace::Global, EMovieSceneTimeUnit::DisplayRate);
2794
+ if (Actual.Num() != 1 || Actual[0].Transforms.Num() != Contact.Frames.Num())
2795
+ {
2796
+ bApplyFailed = true;
2797
+ ApplyError = FString::Printf(
2798
+ TEXT("Could not perform final contact_lock readback for %s"),
2799
+ *Contact.Control.ToString());
2800
+ break;
2801
+ }
2802
+ ControlRigSequencerMeasureContact(
2803
+ Contact.Frames, Contact.ExpectedSubject, Actual[0].Transforms,
2804
+ true, Contact.bCheckRotation, Contact.Metrics);
2805
+ if (Contact.Metrics.MaxPositionErrorCm > Contact.PositionToleranceCm
2806
+ || (Contact.bCheckRotation
2807
+ && Contact.Metrics.MaxRotationErrorDegrees > Contact.RotationToleranceDegrees))
2808
+ {
2809
+ bApplyFailed = true;
2810
+ ApplyError = FString::Printf(
2811
+ TEXT("contact_constraint_tolerance_exceeded: operations[%d] %s residual was %.4f cm at frame %d and %.4f degrees at frame %d"),
2812
+ Contact.OperationIndex,
2813
+ *Contact.Control.ToString(),
2814
+ Contact.Metrics.MaxPositionErrorCm,
2815
+ Contact.Metrics.WorstPositionFrame,
2816
+ Contact.Metrics.MaxRotationErrorDegrees,
2817
+ Contact.Metrics.WorstRotationFrame);
2818
+ break;
2819
+ }
2820
+ }
2821
+
2822
+ if (!Contact.Stabilizers.IsEmpty())
2823
+ {
2824
+ TArray<FName> StabilizerNames;
2825
+ for (const FControlRigContactStabilizerQA& Stabilizer : Contact.Stabilizers)
2826
+ {
2827
+ StabilizerNames.Add(Stabilizer.Control);
2828
+ }
2829
+ const TArray<FArrayOfRigControlTransforms> ActualStabilizers =
2830
+ UControlRigSequencerEditorLibrary::BatchGetControlTransforms(
2831
+ Session.Sequence, Session.ControlRig, StabilizerNames, Contact.Frames,
2832
+ EControlRigTransformSpace::Global, EMovieSceneTimeUnit::DisplayRate);
2833
+ if (ActualStabilizers.Num() != Contact.Stabilizers.Num())
2834
+ {
2835
+ bApplyFailed = true;
2836
+ ApplyError = FString::Printf(
2837
+ TEXT("Could not perform final contact_lock stabilizer readback for operations[%d]"),
2838
+ Contact.OperationIndex);
2839
+ break;
2840
+ }
2841
+ TMap<FName, const FArrayOfRigControlTransforms*> ActualByControl;
2842
+ for (const FArrayOfRigControlTransforms& Values : ActualStabilizers)
2843
+ {
2844
+ ActualByControl.Add(Values.ControlName, &Values);
2845
+ }
2846
+ for (FControlRigContactStabilizerQA& Stabilizer : Contact.Stabilizers)
2847
+ {
2848
+ const FArrayOfRigControlTransforms* const* ActualValues =
2849
+ ActualByControl.Find(Stabilizer.Control);
2850
+ if (!ActualValues || !*ActualValues
2851
+ || (*ActualValues)->Transforms.Num() != Contact.Frames.Num())
2852
+ {
2853
+ bApplyFailed = true;
2854
+ ApplyError = FString::Printf(
2855
+ TEXT("Could not perform final contact_lock readback for stabilizer %s"),
2856
+ *Stabilizer.Control.ToString());
2857
+ break;
2858
+ }
2859
+ const bool bCheckPosition =
2860
+ ControlRigSequencerControlHasTranslation(Stabilizer.ControlType);
2861
+ const bool bCheckRotation =
2862
+ ControlRigSequencerControlHasRotation(Stabilizer.ControlType);
2863
+ ControlRigSequencerMeasureContact(
2864
+ Contact.Frames, Stabilizer.Expected, (*ActualValues)->Transforms,
2865
+ bCheckPosition, bCheckRotation, Stabilizer.Metrics);
2866
+ if ((bCheckPosition
2867
+ && Stabilizer.Metrics.MaxPositionErrorCm > Contact.PositionToleranceCm)
2868
+ || (bCheckRotation
2869
+ && Stabilizer.Metrics.MaxRotationErrorDegrees > Contact.RotationToleranceDegrees))
2870
+ {
2871
+ bApplyFailed = true;
2872
+ ApplyError = FString::Printf(
2873
+ TEXT("contact_constraint_tolerance_exceeded: operations[%d] stabilizer %s residual was %.4f cm and %.4f degrees"),
2874
+ Contact.OperationIndex, *Stabilizer.Control.ToString(),
2875
+ Stabilizer.Metrics.MaxPositionErrorCm,
2876
+ Stabilizer.Metrics.MaxRotationErrorDegrees);
2877
+ break;
2878
+ }
2879
+ }
2880
+ if (bApplyFailed) break;
2881
+ }
2882
+ }
2883
+ }
2884
+ }
2885
+ if (bApplyFailed)
2886
+ {
2887
+ const bool bRolledBack = GEditor && GEditor->UndoTransaction();
2888
+ if (!bRolledBack)
2889
+ {
2890
+ return MCPError(ApplyError + TEXT("; the editor transaction could not be rolled back"));
2891
+ }
2892
+ return MCPError(ApplyError);
2893
+ }
2894
+ Session.Sequence->MarkPackageDirty();
2895
+ if (!UEditorAssetLibrary::SaveLoadedAsset(Session.Sequence, false))
2896
+ {
2897
+ const bool bRolledBack = GEditor && GEditor->UndoTransaction();
2898
+ return MCPError(bRolledBack
2899
+ ? TEXT("Control Rig edits could not be saved and were rolled back")
2900
+ : TEXT("Control Rig edits could not be saved and the editor transaction could not be rolled back"));
2901
+ }
2902
+
2903
+ auto Result = MCPSuccess();
2904
+ Result->SetStringField(TEXT("sequencePath"), Session.Sequence->GetPathName());
2905
+ Result->SetStringField(TEXT("bindingTag"), Session.BindingTag);
2906
+ Result->SetNumberField(TEXT("appliedOperationCount"), Operations->Num());
2907
+ Result->SetNumberField(TEXT("keyedControlWriteCount"), Prepared.Num());
2908
+ int32 KeyedSamples = 0;
2909
+ TArray<TSharedPtr<FJsonValue>> Applied;
2910
+ for (const FControlRigPreparedWrite& Write : Prepared)
2911
+ {
2912
+ KeyedSamples += Write.Frames.Num();
2913
+ auto Object = MakeShared<FJsonObject>();
2914
+ Object->SetStringField(TEXT("op"), Write.Op);
2915
+ Object->SetStringField(TEXT("control"), Write.Control.ToString());
2916
+ Object->SetNumberField(TEXT("keyedFrameCount"), Write.Frames.Num());
2917
+ Applied.Add(MakeShared<FJsonValueObject>(Object));
2918
+ }
2919
+ Result->SetNumberField(TEXT("keyedSampleCount"), KeyedSamples);
2920
+ Result->SetArrayField(TEXT("applied"), Applied);
2921
+ TArray<TSharedPtr<FJsonValue>> ContactResults;
2922
+ for (const FControlRigPreparedContactQA& Contact : PreparedContacts)
2923
+ {
2924
+ auto Object = Contact.bHasDrivenReference
2925
+ ? MakeShared<FJsonObject>()
2926
+ : ControlRigSequencerContactMetricsJson(Contact.Metrics, true, Contact.bCheckRotation);
2927
+ Object->SetNumberField(TEXT("operationIndex"), Contact.OperationIndex);
2928
+ Object->SetStringField(TEXT("control"), Contact.Control.ToString());
2929
+ if (Contact.bHasDrivenReference)
2930
+ {
2931
+ Object->SetStringField(TEXT("drivenReference"), Contact.DrivenReference.ToString());
2932
+ Object->SetStringField(TEXT("verification"), TEXT("bake_and_analyze_required"));
2933
+ if (Contact.bUsedFkRotationChain)
2934
+ {
2935
+ Object->SetStringField(TEXT("solver"), TEXT("fk_rotation_chain"));
2936
+ Object->SetObjectField(
2937
+ TEXT("preBakePrediction"),
2938
+ ControlRigSequencerContactMetricsJson(Contact.Metrics, true, Contact.bCheckRotation));
2939
+ }
2940
+ }
2941
+ Object->SetNumberField(TEXT("frameCount"), Contact.Frames.Num());
2942
+ Object->SetNumberField(TEXT("fullWeightFrameCount"), Contact.FullWeightFrameCount);
2943
+ Object->SetNumberField(TEXT("positionToleranceCm"), Contact.PositionToleranceCm);
2944
+ if (Contact.bCheckRotation)
2945
+ {
2946
+ Object->SetNumberField(TEXT("rotationToleranceDegrees"), Contact.RotationToleranceDegrees);
2947
+ }
2948
+ TArray<TSharedPtr<FJsonValue>> StabilizerResults;
2949
+ for (const FControlRigContactStabilizerQA& Stabilizer : Contact.Stabilizers)
2950
+ {
2951
+ const bool bCheckPosition =
2952
+ ControlRigSequencerControlHasTranslation(Stabilizer.ControlType);
2953
+ const bool bCheckRotation =
2954
+ ControlRigSequencerControlHasRotation(Stabilizer.ControlType);
2955
+ auto StabilizerObject = ControlRigSequencerContactMetricsJson(
2956
+ Stabilizer.Metrics, bCheckPosition, bCheckRotation);
2957
+ StabilizerObject->SetStringField(TEXT("control"), Stabilizer.Control.ToString());
2958
+ StabilizerResults.Add(MakeShared<FJsonValueObject>(StabilizerObject));
2959
+ }
2960
+ Object->SetArrayField(TEXT("stabilizers"), StabilizerResults);
2961
+ Object->SetBoolField(TEXT("keyReadbackPassed"), true);
2962
+ if (!Contact.bHasDrivenReference) Object->SetBoolField(TEXT("passed"), true);
2963
+ ContactResults.Add(MakeShared<FJsonValueObject>(Object));
2964
+ }
2965
+ Result->SetArrayField(TEXT("contactQa"), ContactResults);
2966
+ MCPSetUpdated(Result);
2967
+ return MCPResult(Result);
2968
+ #endif
2969
+ }
2970
+
2971
+ TSharedPtr<FJsonValue> FAnimationHandlers::BakeControlRigEdit(const TSharedPtr<FJsonObject>& Params)
2972
+ {
2973
+ #if !UE_MCP_HAS_5_8_API
2974
+ return ControlRigSequencerUnsupported();
2975
+ #else
2976
+ FString SequencePath;
2977
+ FString OutputAssetPath;
2978
+ if (auto Error = RequireString(Params, TEXT("sequencePath"), SequencePath)) return Error;
2979
+ if (auto Error = RequireString(Params, TEXT("outputAssetPath"), OutputAssetPath)) return Error;
2980
+
2981
+ FString PackagePath;
2982
+ FString AssetName;
2983
+ FString Error;
2984
+ if (!ControlRigSequencerSplitAssetPath(OutputAssetPath, PackagePath, AssetName, Error)) return MCPError(Error);
2985
+ const FString OnConflict = OptionalString(Params, TEXT("onConflict"), TEXT("error")).ToLower();
2986
+ if (OnConflict != TEXT("skip") && OnConflict != TEXT("error"))
2987
+ return MCPError(TEXT("'onConflict' must be 'skip' or 'error'; bake never overwrites an AnimSequence"));
2988
+ if (UObject* Existing = UEditorAssetLibrary::LoadAsset(OutputAssetPath))
2989
+ {
2990
+ if (OnConflict == TEXT("error")) return MCPError(FString::Printf(TEXT("Output asset already exists: %s"), *OutputAssetPath));
2991
+ if (!Existing->IsA<UAnimSequence>()) return MCPError(FString::Printf(TEXT("Existing output is not an AnimSequence: %s"), *OutputAssetPath));
2992
+ auto Result = MCPSuccess();
2993
+ MCPSetExisted(Result);
2994
+ Result->SetStringField(TEXT("outputAssetPath"), Existing->GetPathName());
2995
+ return MCPResult(Result);
2996
+ }
2997
+
2998
+ ULevelSequence* Sequence = Cast<ULevelSequence>(UEditorAssetLibrary::LoadAsset(SequencePath));
2999
+ if (!Sequence) return MCPError(FString::Printf(TEXT("LevelSequence not found: %s"), *SequencePath));
3000
+ FControlRigSequenceFocusGuard Focus(Sequence);
3001
+ if (!Focus.IsReady()) return MCPError(TEXT("Could not focus the LevelSequence in Sequencer"));
3002
+ FControlRigSequenceSession Session;
3003
+ if (!ControlRigSequencerResolveSession(Params, Session, Error)) return MCPError(Error);
3004
+
3005
+ FFrameRate FrameRate;
3006
+ if (!ControlRigSequencerReadRate(Params, TEXT("frameRate"), Session.MovieScene->GetDisplayRate(), FrameRate, Error))
3007
+ return MCPError(Error);
3008
+ const bool bReduceKeys = OptionalBool(Params, TEXT("reduceKeys"), false);
3009
+ const double Tolerance = OptionalNumber(Params, TEXT("tolerance"), 0.001);
3010
+ if (!FMath::IsFinite(Tolerance) || Tolerance < 0.0)
3011
+ return MCPError(TEXT("'tolerance' must be a finite non-negative number"));
3012
+ if (bReduceKeys)
3013
+ {
3014
+ return MCPError(TEXT("'reduceKeys' is not supported by AnimSequence export in this vertical slice; bake with reduceKeys=false"));
3015
+ }
3016
+ const bool bCreateLink = OptionalBool(Params, TEXT("createLink"), false);
3017
+ if (bCreateLink)
3018
+ {
3019
+ return MCPError(TEXT("'createLink' is not supported because Unreal mutates both linked assets; bake with createLink=false"));
3020
+ }
3021
+
3022
+ auto Created = MCPCreateAssetIdempotentNewObject<UAnimSequence>(AssetName, PackagePath, TEXT("error"), TEXT("AnimSequence"));
3023
+ if (Created.EarlyReturn) return Created.EarlyReturn;
3024
+ UAnimSequence* Output = Created.Asset;
3025
+ UAnimSeqExportOption* ExportOptions = NewObject<UAnimSeqExportOption>();
3026
+ ExportOptions->bUseCustomFrameRate = true;
3027
+ ExportOptions->CustomFrameRate = FrameRate;
3028
+ ExportOptions->bExportTransforms = true;
3029
+ ExportOptions->bExportMorphTargets = true;
3030
+ ExportOptions->bExportAttributeCurves = true;
3031
+ ExportOptions->bExportMaterialCurves = true;
3032
+
3033
+ const bool bExported = UControlRigSequencerEditorLibrary::ExportAnimSequenceFromSequencer(
3034
+ Output, ExportOptions, FMovieSceneBindingProxy(Session.BindingGuid, Session.Sequence), false);
3035
+ if (!bExported)
3036
+ {
3037
+ UEditorAssetLibrary::DeleteAsset(PackagePath + TEXT("/") + AssetName);
3038
+ return MCPError(TEXT("Unreal failed to export the Control Rig edit to an AnimSequence"));
3039
+ }
3040
+ Output->MarkPackageDirty();
3041
+ if (!UEditorAssetLibrary::SaveLoadedAsset(Output, false))
3042
+ {
3043
+ UEditorAssetLibrary::DeleteAsset(PackagePath + TEXT("/") + AssetName);
3044
+ return MCPError(TEXT("AnimSequence export completed in memory but the output asset could not be saved"));
3045
+ }
3046
+
3047
+ auto Result = MCPSuccess();
3048
+ MCPSetCreated(Result);
3049
+ Result->SetStringField(TEXT("sequencePath"), Session.Sequence->GetPathName());
3050
+ Result->SetStringField(TEXT("bindingTag"), Session.BindingTag);
3051
+ Result->SetStringField(TEXT("outputAssetPath"), Output->GetPathName());
3052
+ if (Output->GetSkeleton()) Result->SetStringField(TEXT("skeletonPath"), Output->GetSkeleton()->GetPathName());
3053
+ Result->SetObjectField(TEXT("frameRate"), ControlRigSequencerRateJson(FrameRate));
3054
+ Result->SetNumberField(TEXT("sampledKeyCount"), Output->GetNumberOfSampledKeys());
3055
+ Result->SetNumberField(TEXT("durationSeconds"), Output->GetPlayLength());
3056
+ Result->SetBoolField(TEXT("createdLink"), false);
3057
+ Result->SetBoolField(TEXT("sourceAnimationModified"), false);
3058
+ MCPSetDeleteAssetRollback(Result, Output->GetPathName());
3059
+ return MCPResult(Result);
3060
+ #endif
3061
+ }