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,735 @@
1
+ // Deterministic, data-first AnimSequence validation.
2
+ //
3
+ // Screenshots are useful review evidence, but exact pose samples are the
4
+ // source of truth for root motion, seams, bounds and numeric integrity.
5
+
6
+ #include "AnimationHandlers.h"
7
+
8
+ #include "HandlerUtils.h"
9
+
10
+ #include "Animation/AnimCurveTypes.h"
11
+ #include "Animation/AnimData/IAnimationDataModel.h"
12
+ #include "Animation/AnimSequence.h"
13
+ #include "Animation/AnimationPoseData.h"
14
+ #include "Animation/AttributesRuntime.h"
15
+ #include "Animation/Skeleton.h"
16
+ #include "BoneContainer.h"
17
+ #include "BoneIndices.h"
18
+ #include "BonePose.h"
19
+ #include "Dom/JsonObject.h"
20
+ #include "Dom/JsonValue.h"
21
+ #include "Engine/SkeletalMesh.h"
22
+ #include "HAL/FileManager.h"
23
+ #include "Misc/AutomationTest.h"
24
+ #include "Misc/FileHelper.h"
25
+ #include "Misc/MemStack.h"
26
+ #include "Misc/Paths.h"
27
+ #include "ReferenceSkeleton.h"
28
+ #include "Serialization/JsonSerializer.h"
29
+ #include "Serialization/JsonWriter.h"
30
+
31
+ namespace
32
+ {
33
+ TSharedPtr<FJsonObject> AnimQaVectorJson(const FVector& Value)
34
+ {
35
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
36
+ Result->SetNumberField(TEXT("x"), Value.X);
37
+ Result->SetNumberField(TEXT("y"), Value.Y);
38
+ Result->SetNumberField(TEXT("z"), Value.Z);
39
+ return Result;
40
+ }
41
+
42
+ TSharedPtr<FJsonObject> AnimQaQuatJson(const FQuat& Value)
43
+ {
44
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
45
+ Result->SetNumberField(TEXT("x"), Value.X);
46
+ Result->SetNumberField(TEXT("y"), Value.Y);
47
+ Result->SetNumberField(TEXT("z"), Value.Z);
48
+ Result->SetNumberField(TEXT("w"), Value.W);
49
+ return Result;
50
+ }
51
+
52
+ TSharedPtr<FJsonObject> AnimQaRotatorJson(const FRotator& Value)
53
+ {
54
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
55
+ Result->SetNumberField(TEXT("pitch"), Value.Pitch);
56
+ Result->SetNumberField(TEXT("yaw"), Value.Yaw);
57
+ Result->SetNumberField(TEXT("roll"), Value.Roll);
58
+ return Result;
59
+ }
60
+
61
+ TSharedPtr<FJsonObject> AnimQaTransformJson(const FTransform& Value)
62
+ {
63
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
64
+ Result->SetObjectField(TEXT("translation"), AnimQaVectorJson(Value.GetTranslation()));
65
+ Result->SetObjectField(TEXT("rotation"), AnimQaQuatJson(Value.GetRotation()));
66
+ Result->SetObjectField(TEXT("rotationDegrees"), AnimQaRotatorJson(Value.Rotator()));
67
+ Result->SetObjectField(TEXT("scale"), AnimQaVectorJson(Value.GetScale3D()));
68
+ return Result;
69
+ }
70
+
71
+ bool AnimQaRawTransformIsValid(const FTransform& Value)
72
+ {
73
+ const FVector Translation = Value.GetTranslation();
74
+ const FVector Scale = Value.GetScale3D();
75
+ const FQuat Rotation = Value.GetRotation();
76
+ return !Translation.ContainsNaN()
77
+ && !Scale.ContainsNaN()
78
+ && !Rotation.ContainsNaN()
79
+ && FMath::IsFinite(Translation.X) && FMath::IsFinite(Translation.Y) && FMath::IsFinite(Translation.Z)
80
+ && FMath::IsFinite(Scale.X) && FMath::IsFinite(Scale.Y) && FMath::IsFinite(Scale.Z)
81
+ && FMath::IsFinite(Rotation.X) && FMath::IsFinite(Rotation.Y)
82
+ && FMath::IsFinite(Rotation.Z) && FMath::IsFinite(Rotation.W)
83
+ && !Scale.IsNearlyZero()
84
+ && Rotation.SizeSquared() > SMALL_NUMBER;
85
+ }
86
+
87
+ bool AnimQaTransformIsFinite(const FTransform& Value)
88
+ {
89
+ return AnimQaRawTransformIsValid(Value)
90
+ && FMath::Abs(Value.GetRotation().SizeSquared() - 1.0) < 0.01;
91
+ }
92
+
93
+ double AnimQaQuatAngleDegrees(const FQuat& A, const FQuat& B)
94
+ {
95
+ const double Dot = FMath::Clamp(FMath::Abs(static_cast<double>(A | B)), 0.0, 1.0);
96
+ return FMath::RadiansToDegrees(2.0 * FMath::Acos(Dot));
97
+ }
98
+
99
+ bool AnimQaSerializeObject(const TSharedPtr<FJsonObject>& Object, FString& OutJson, bool bCondensed)
100
+ {
101
+ if (bCondensed)
102
+ {
103
+ TSharedRef<TJsonWriter<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>> Writer =
104
+ TJsonWriterFactory<TCHAR, TCondensedJsonPrintPolicy<TCHAR>>::Create(&OutJson);
105
+ return FJsonSerializer::Serialize(Object.ToSharedRef(), Writer);
106
+ }
107
+
108
+ TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutJson);
109
+ return FJsonSerializer::Serialize(Object.ToSharedRef(), Writer);
110
+ }
111
+
112
+ bool AnimQaResolveOutputDirectory(const FString& Requested, FString& OutDirectory, FString& OutError)
113
+ {
114
+ FString NormalizedRoot = FPaths::ConvertRelativePathToFull(
115
+ FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("Codex"), TEXT("AnimationQA")));
116
+ FPaths::NormalizeDirectoryName(NormalizedRoot);
117
+
118
+ FString Candidate;
119
+ if (Requested.IsEmpty())
120
+ {
121
+ OutDirectory.Reset();
122
+ return true;
123
+ }
124
+ if (FPaths::IsRelative(Requested))
125
+ {
126
+ FString ProjectRelativeCandidate = FPaths::ConvertRelativePathToFull(
127
+ FPaths::Combine(FPaths::ProjectDir(), Requested));
128
+ FPaths::NormalizeDirectoryName(ProjectRelativeCandidate);
129
+ Candidate = FPaths::IsSamePath(ProjectRelativeCandidate, NormalizedRoot)
130
+ || FPaths::IsUnderDirectory(ProjectRelativeCandidate, NormalizedRoot)
131
+ ? ProjectRelativeCandidate
132
+ : FPaths::Combine(NormalizedRoot, Requested);
133
+ }
134
+ else
135
+ {
136
+ Candidate = Requested;
137
+ }
138
+
139
+ Candidate = FPaths::ConvertRelativePathToFull(Candidate);
140
+ FPaths::NormalizeDirectoryName(Candidate);
141
+ if (!FPaths::IsSamePath(Candidate, NormalizedRoot)
142
+ && !FPaths::IsUnderDirectory(Candidate, NormalizedRoot))
143
+ {
144
+ OutError = FString::Printf(
145
+ TEXT("outputDirectory must resolve under '%s'"), *NormalizedRoot);
146
+ return false;
147
+ }
148
+
149
+ OutDirectory = Candidate;
150
+ return true;
151
+ }
152
+
153
+ void AnimQaAddDefaultBoneIfPresent(
154
+ const FReferenceSkeleton& RefSkeleton,
155
+ const TCHAR* BoneName,
156
+ TArray<int32>& BoneIndices)
157
+ {
158
+ const int32 Index = RefSkeleton.FindBoneIndex(FName(BoneName));
159
+ if (Index != INDEX_NONE)
160
+ {
161
+ BoneIndices.AddUnique(Index);
162
+ }
163
+ }
164
+ }
165
+
166
+ #if WITH_DEV_AUTOMATION_TESTS
167
+
168
+ IMPLEMENT_SIMPLE_AUTOMATION_TEST(
169
+ FAnimationAnalysisOutputDirectoryNormalizationTest,
170
+ "UE.MCP.Animation.Analysis.OutputDirectoryNormalization",
171
+ EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
172
+
173
+ bool FAnimationAnalysisOutputDirectoryNormalizationTest::RunTest(const FString& Parameters)
174
+ {
175
+ const FString Leaf = TEXT("throw_production_v003_full");
176
+ const FString QualifiedRequest = FPaths::Combine(
177
+ TEXT("Saved"), TEXT("Codex"), TEXT("AnimationQA"), Leaf);
178
+ FString RootRelativeOutput;
179
+ FString QualifiedOutput;
180
+ FString RootRelativeError;
181
+ FString QualifiedError;
182
+ TestTrue(
183
+ TEXT("a path relative to the AnimationQA root resolves"),
184
+ AnimQaResolveOutputDirectory(Leaf, RootRelativeOutput, RootRelativeError));
185
+ TestTrue(
186
+ TEXT("a Project/Saved-qualified path resolves"),
187
+ AnimQaResolveOutputDirectory(QualifiedRequest, QualifiedOutput, QualifiedError));
188
+ TestTrue(
189
+ TEXT("both forms resolve to the same directory exactly once"),
190
+ FPaths::IsSamePath(RootRelativeOutput, QualifiedOutput));
191
+ return true;
192
+ }
193
+
194
+ #endif
195
+
196
+ TSharedPtr<FJsonValue> FAnimationHandlers::AnalyzeAnimation(const TSharedPtr<FJsonObject>& Params)
197
+ {
198
+ FString AssetPath;
199
+ if (auto Error = RequireString(Params, TEXT("assetPath"), AssetPath)) return Error;
200
+
201
+ UAnimSequence* Sequence = LoadObject<UAnimSequence>(nullptr, *AssetPath);
202
+ if (!Sequence)
203
+ {
204
+ return MCPError(FString::Printf(TEXT("AnimSequence not found: %s"), *AssetPath));
205
+ }
206
+ USkeleton* Skeleton = Sequence->GetSkeleton();
207
+ if (!Skeleton)
208
+ {
209
+ return MCPError(TEXT("AnimSequence has no skeleton"));
210
+ }
211
+ if (Sequence->GetAdditiveAnimType() != AAT_None)
212
+ {
213
+ return MCPError(TEXT("Additive AnimSequences are not supported by analyze_animation v1; provide a baked full-pose sequence"));
214
+ }
215
+ const FString SkeletalMeshPath = OptionalString(Params, TEXT("skeletalMeshPath"));
216
+ USkeletalMesh* SkeletalMesh = nullptr;
217
+ if (!SkeletalMeshPath.IsEmpty())
218
+ {
219
+ SkeletalMesh = LoadObject<USkeletalMesh>(nullptr, *SkeletalMeshPath);
220
+ if (!SkeletalMesh)
221
+ {
222
+ return MCPError(FString::Printf(TEXT("SkeletalMesh not found: %s"), *SkeletalMeshPath));
223
+ }
224
+ if (!SkeletalMesh->GetSkeleton()
225
+ || !SkeletalMesh->GetSkeleton()->IsCompatibleForEditor(Skeleton))
226
+ {
227
+ return MCPError(FString::Printf(
228
+ TEXT("SkeletalMesh '%s' is not compatible with animation skeleton '%s'"),
229
+ *SkeletalMeshPath,
230
+ *Skeleton->GetPathName()));
231
+ }
232
+ }
233
+
234
+ const FReferenceSkeleton& RefSkeleton = SkeletalMesh
235
+ ? SkeletalMesh->GetRefSkeleton()
236
+ : Skeleton->GetReferenceSkeleton();
237
+ const int32 BoneCount = RefSkeleton.GetNum();
238
+ if (BoneCount <= 0)
239
+ {
240
+ return MCPError(TEXT("Skeleton has no bones"));
241
+ }
242
+
243
+ const IAnimationDataModel* DataModel = Sequence->GetDataModel();
244
+ if (!DataModel)
245
+ {
246
+ return MCPError(TEXT("AnimSequence has no animation data model"));
247
+ }
248
+ const FFrameRate SourceRate = DataModel->GetFrameRate();
249
+ const double SourceRateDecimal = SourceRate.AsDecimal();
250
+ if (!SourceRate.IsValid() || SourceRate.Numerator <= 0 || SourceRate.Denominator <= 0
251
+ || !FMath::IsFinite(SourceRateDecimal) || SourceRateDecimal <= 0.0)
252
+ {
253
+ return MCPError(TEXT("AnimSequence has an invalid frame rate"));
254
+ }
255
+ const int32 SourceFrameCount = DataModel->GetNumberOfFrames();
256
+ const double DurationSeconds = Sequence->GetPlayLength();
257
+ if (SourceFrameCount < 1 || !FMath::IsFinite(DurationSeconds) || DurationSeconds <= 0.0)
258
+ {
259
+ return MCPError(TEXT("AnimSequence has an invalid duration or frame count"));
260
+ }
261
+ const double RateScale = static_cast<double>(Sequence->RateScale);
262
+ if (!FMath::IsFinite(RateScale))
263
+ {
264
+ return MCPError(TEXT("AnimSequence has an invalid RateScale"));
265
+ }
266
+ const double PlaybackRateMagnitude = FMath::Abs(RateScale);
267
+ const bool bHasEffectiveTiming = PlaybackRateMagnitude > 0.0;
268
+ const double EffectiveDurationSeconds = bHasEffectiveTiming
269
+ ? DurationSeconds / PlaybackRateMagnitude
270
+ : 0.0;
271
+
272
+ TArray<TSharedPtr<FJsonValue>> NotifyValues;
273
+ NotifyValues.Reserve(Sequence->Notifies.Num());
274
+ for (const FAnimNotifyEvent& NotifyEvent : Sequence->Notifies)
275
+ {
276
+ const double RawTriggerTimeSeconds = static_cast<double>(NotifyEvent.GetTriggerTime());
277
+ if (!FMath::IsFinite(RawTriggerTimeSeconds))
278
+ {
279
+ return MCPError(TEXT("AnimSequence has a notify with an invalid trigger time"));
280
+ }
281
+
282
+ TSharedPtr<FJsonObject> NotifyObject = MakeShared<FJsonObject>();
283
+ NotifyObject->SetStringField(TEXT("name"), NotifyEvent.NotifyName.ToString());
284
+ NotifyObject->SetNumberField(TEXT("rawTriggerTimeSeconds"), RawTriggerTimeSeconds);
285
+ if (bHasEffectiveTiming)
286
+ {
287
+ NotifyObject->SetNumberField(
288
+ TEXT("effectiveTriggerTimeSeconds"),
289
+ RawTriggerTimeSeconds / PlaybackRateMagnitude);
290
+ }
291
+ else
292
+ {
293
+ NotifyObject->SetField(TEXT("effectiveTriggerTimeSeconds"), MakeShared<FJsonValueNull>());
294
+ }
295
+ NotifyValues.Add(MakeShared<FJsonValueObject>(NotifyObject));
296
+ }
297
+
298
+ TArray<int32> BoneIndices;
299
+ const TArray<TSharedPtr<FJsonValue>>* BoneNamesJson = nullptr;
300
+ if (Params->TryGetArrayField(TEXT("boneNames"), BoneNamesJson))
301
+ {
302
+ for (const TSharedPtr<FJsonValue>& Value : *BoneNamesJson)
303
+ {
304
+ FString BoneName;
305
+ if (!Value.IsValid() || !Value->TryGetString(BoneName))
306
+ {
307
+ return MCPError(TEXT("boneNames must contain only strings"));
308
+ }
309
+ const int32 BoneIndex = RefSkeleton.FindBoneIndex(FName(*BoneName));
310
+ if (BoneIndex == INDEX_NONE)
311
+ {
312
+ return MCPError(FString::Printf(TEXT("Bone not found on skeleton: %s"), *BoneName));
313
+ }
314
+ BoneIndices.AddUnique(BoneIndex);
315
+ }
316
+ }
317
+ else
318
+ {
319
+ AnimQaAddDefaultBoneIfPresent(RefSkeleton, TEXT("root"), BoneIndices);
320
+ AnimQaAddDefaultBoneIfPresent(RefSkeleton, TEXT("pelvis"), BoneIndices);
321
+ AnimQaAddDefaultBoneIfPresent(RefSkeleton, TEXT("head"), BoneIndices);
322
+ AnimQaAddDefaultBoneIfPresent(RefSkeleton, TEXT("hand_l"), BoneIndices);
323
+ AnimQaAddDefaultBoneIfPresent(RefSkeleton, TEXT("hand_r"), BoneIndices);
324
+ AnimQaAddDefaultBoneIfPresent(RefSkeleton, TEXT("foot_l"), BoneIndices);
325
+ AnimQaAddDefaultBoneIfPresent(RefSkeleton, TEXT("foot_r"), BoneIndices);
326
+ if (BoneIndices.IsEmpty())
327
+ {
328
+ for (int32 Index = 0; Index < FMath::Min(BoneCount, 16); ++Index)
329
+ {
330
+ BoneIndices.Add(Index);
331
+ }
332
+ }
333
+ }
334
+ if (BoneIndices.IsEmpty())
335
+ {
336
+ return MCPError(TEXT("No bones selected for analysis"));
337
+ }
338
+ if (BoneIndices.Num() > 256)
339
+ {
340
+ return MCPError(TEXT("analyze_animation supports at most 256 selected bones per call"));
341
+ }
342
+
343
+ const bool bLoop = OptionalBool(Params, TEXT("loop"), false);
344
+ TArray<int32> Frames;
345
+ const TArray<TSharedPtr<FJsonValue>>* FramesJson = nullptr;
346
+ if (Params->TryGetArrayField(TEXT("frames"), FramesJson))
347
+ {
348
+ if (FramesJson->Num() > 2401)
349
+ {
350
+ return MCPError(TEXT("analyze_animation supports at most 2401 explicit frames per call"));
351
+ }
352
+ for (const TSharedPtr<FJsonValue>& Value : *FramesJson)
353
+ {
354
+ double Number = 0.0;
355
+ if (!Value.IsValid() || !Value->TryGetNumber(Number) || !FMath::IsFinite(Number)
356
+ || Number < 0.0 || Number > SourceFrameCount
357
+ || !FMath::IsNearlyEqual(Number, FMath::RoundToDouble(Number)))
358
+ {
359
+ return MCPError(FString::Printf(
360
+ TEXT("frames must contain only integers in [0, %d]"), SourceFrameCount));
361
+ }
362
+ Frames.AddUnique(FMath::RoundToInt(Number));
363
+ }
364
+ }
365
+ else
366
+ {
367
+ double RequestedRate = SourceRateDecimal;
368
+ if (Params->HasField(TEXT("sampleRate"))
369
+ && (!Params->TryGetNumberField(TEXT("sampleRate"), RequestedRate)
370
+ || !FMath::IsFinite(RequestedRate) || RequestedRate < 1.0 || RequestedRate > 240.0))
371
+ {
372
+ return MCPError(TEXT("'sampleRate' must be a finite number in [1, 240]"));
373
+ }
374
+ const int32 SampleCount = FMath::Clamp(
375
+ FMath::CeilToInt(DurationSeconds * RequestedRate) + 1,
376
+ 2,
377
+ 2401);
378
+ for (int32 SampleIndex = 0; SampleIndex < SampleCount; ++SampleIndex)
379
+ {
380
+ const double Alpha = SampleCount > 1
381
+ ? static_cast<double>(SampleIndex) / static_cast<double>(SampleCount - 1)
382
+ : 0.0;
383
+ Frames.AddUnique(FMath::Clamp(FMath::RoundToInt(Alpha * SourceFrameCount), 0, SourceFrameCount));
384
+ }
385
+ }
386
+ if (Frames.IsEmpty())
387
+ {
388
+ return MCPError(TEXT("No frames selected for analysis"));
389
+ }
390
+ if (bLoop)
391
+ {
392
+ Frames.AddUnique(0);
393
+ Frames.AddUnique(SourceFrameCount);
394
+ }
395
+ Frames.Sort();
396
+ if (static_cast<int64>(Frames.Num()) * static_cast<int64>(BoneIndices.Num()) > 100000)
397
+ {
398
+ return MCPError(TEXT("analyze_animation supports at most 100000 bone-frame samples per call"));
399
+ }
400
+
401
+ TArray<FBoneIndexType> RequiredBoneIndices;
402
+ RequiredBoneIndices.Reserve(BoneCount);
403
+ for (int32 Index = 0; Index < BoneCount; ++Index)
404
+ {
405
+ RequiredBoneIndices.Add(static_cast<FBoneIndexType>(Index));
406
+ }
407
+ FBoneContainer RequiredBones;
408
+ RequiredBones.InitializeTo(
409
+ RequiredBoneIndices,
410
+ UE::Anim::FCurveFilterSettings(UE::Anim::ECurveFilterMode::DisallowAll),
411
+ SkeletalMesh ? static_cast<UObject&>(*SkeletalMesh) : static_cast<UObject&>(*Skeleton));
412
+ const auto CompactFromReferenceIndex = [&RequiredBones, SkeletalMesh](int32 Index)
413
+ {
414
+ return SkeletalMesh
415
+ ? RequiredBones.MakeCompactPoseIndex(FMeshPoseBoneIndex(Index))
416
+ : RequiredBones.GetCompactPoseIndexFromSkeletonPoseIndex(FSkeletonPoseBoneIndex(Index));
417
+ };
418
+ const auto ReferenceIndexFromCompact = [&RequiredBones, SkeletalMesh](FCompactPoseBoneIndex Index)
419
+ {
420
+ return SkeletalMesh
421
+ ? RequiredBones.MakeMeshPoseIndex(Index).GetInt()
422
+ : RequiredBones.GetSkeletonPoseIndexFromCompactPoseIndex(Index).GetInt();
423
+ };
424
+
425
+ bool bNumericIntegrity = true;
426
+ int32 InvalidTransformCount = 0;
427
+ FVector BoundsMin(UE_BIG_NUMBER, UE_BIG_NUMBER, UE_BIG_NUMBER);
428
+ FVector BoundsMax(-UE_BIG_NUMBER, -UE_BIG_NUMBER, -UE_BIG_NUMBER);
429
+ FVector FirstRoot = FVector::ZeroVector;
430
+ FVector LastRoot = FVector::ZeroVector;
431
+ FVector PreviousRoot = FVector::ZeroVector;
432
+ double PreviousTime = 0.0;
433
+ double MaxRootSpeed = 0.0;
434
+ bool bHasPreviousRoot = false;
435
+ TArray<FTransform> FirstLocalTransforms;
436
+ TArray<FTransform> LastLocalTransforms;
437
+ TArray<TSharedPtr<FJsonValue>> SampleValues;
438
+ TArray<FString> SampleLines;
439
+
440
+ for (int32 Frame : Frames)
441
+ {
442
+ FMemMark FrameMark(FMemStack::Get());
443
+ const double TimeSeconds = FMath::Clamp(
444
+ static_cast<double>(Frame) / SourceRateDecimal,
445
+ 0.0,
446
+ DurationSeconds);
447
+
448
+ FCompactPose CompactPose;
449
+ CompactPose.SetBoneContainer(&RequiredBones);
450
+ CompactPose.ResetToRefPose();
451
+ FBlendedCurve Curve;
452
+ Curve.InitFrom(RequiredBones);
453
+ UE::Anim::FStackAttributeContainer Attributes;
454
+ FAnimationPoseData PoseData(CompactPose, Curve, Attributes);
455
+ FAnimExtractContext ExtractContext(TimeSeconds, false);
456
+ #if WITH_EDITOR
457
+ ExtractContext.bIgnoreRootLock = true;
458
+ #endif
459
+ Sequence->GetAnimationPose(PoseData, ExtractContext);
460
+ for (const FCompactPoseBoneIndex BoneIndex : CompactPose.ForEachBoneIndex())
461
+ {
462
+ if (!AnimQaRawTransformIsValid(CompactPose[BoneIndex]))
463
+ {
464
+ const int32 ReferenceBoneIndex = ReferenceIndexFromCompact(BoneIndex);
465
+ return MCPError(FString::Printf(
466
+ TEXT("AnimSequence produced an invalid raw transform at frame %d for bone %s"),
467
+ Frame,
468
+ *RefSkeleton.GetBoneName(ReferenceBoneIndex).ToString()));
469
+ }
470
+ }
471
+ CompactPose.NormalizeRotations();
472
+
473
+ FCSPose<FCompactPose> ComponentPose;
474
+ ComponentPose.InitPose(CompactPose);
475
+
476
+ TSharedPtr<FJsonObject> SampleObject = MakeShared<FJsonObject>();
477
+ SampleObject->SetNumberField(TEXT("frame"), Frame);
478
+ SampleObject->SetNumberField(TEXT("timeSeconds"), TimeSeconds);
479
+ SampleObject->SetStringField(TEXT("timeRational"), FString::Printf(
480
+ TEXT("%lld/%d"),
481
+ static_cast<long long>(Frame) * static_cast<long long>(SourceRate.Denominator),
482
+ SourceRate.Numerator));
483
+
484
+ TArray<TSharedPtr<FJsonValue>> BoneValues;
485
+ TArray<FTransform> CurrentLocalTransforms;
486
+ CurrentLocalTransforms.Reserve(BoneIndices.Num());
487
+ for (int32 ReferenceBoneIndex : BoneIndices)
488
+ {
489
+ const FCompactPoseBoneIndex CompactIndex = CompactFromReferenceIndex(ReferenceBoneIndex);
490
+ if (CompactIndex == INDEX_NONE)
491
+ {
492
+ return MCPError(FString::Printf(
493
+ TEXT("Bone %s could not be mapped into the evaluated compact pose"),
494
+ *RefSkeleton.GetBoneName(ReferenceBoneIndex).ToString()));
495
+ }
496
+
497
+ const FTransform LocalTransform = CompactPose[CompactIndex];
498
+ const FTransform ComponentTransform = ComponentPose.GetComponentSpaceTransform(CompactIndex);
499
+ CurrentLocalTransforms.Add(LocalTransform);
500
+ if (!AnimQaTransformIsFinite(LocalTransform) || !AnimQaTransformIsFinite(ComponentTransform))
501
+ {
502
+ return MCPError(FString::Printf(
503
+ TEXT("AnimSequence produced an invalid evaluated transform at frame %d for bone %s"),
504
+ Frame,
505
+ *RefSkeleton.GetBoneName(ReferenceBoneIndex).ToString()));
506
+ }
507
+
508
+ const FVector Position = ComponentTransform.GetTranslation();
509
+ BoundsMin.X = FMath::Min(BoundsMin.X, Position.X);
510
+ BoundsMin.Y = FMath::Min(BoundsMin.Y, Position.Y);
511
+ BoundsMin.Z = FMath::Min(BoundsMin.Z, Position.Z);
512
+ BoundsMax.X = FMath::Max(BoundsMax.X, Position.X);
513
+ BoundsMax.Y = FMath::Max(BoundsMax.Y, Position.Y);
514
+ BoundsMax.Z = FMath::Max(BoundsMax.Z, Position.Z);
515
+
516
+ TSharedPtr<FJsonObject> BoneObject = MakeShared<FJsonObject>();
517
+ BoneObject->SetStringField(TEXT("name"), RefSkeleton.GetBoneName(ReferenceBoneIndex).ToString());
518
+ BoneObject->SetNumberField(TEXT("index"), ReferenceBoneIndex);
519
+ BoneObject->SetNumberField(TEXT("parentIndex"), RefSkeleton.GetParentIndex(ReferenceBoneIndex));
520
+ BoneObject->SetObjectField(TEXT("local"), AnimQaTransformJson(LocalTransform));
521
+ BoneObject->SetObjectField(TEXT("component"), AnimQaTransformJson(ComponentTransform));
522
+ BoneValues.Add(MakeShared<FJsonValueObject>(BoneObject));
523
+ }
524
+ SampleObject->SetArrayField(TEXT("bones"), BoneValues);
525
+
526
+ const FCompactPoseBoneIndex RootIndex = CompactFromReferenceIndex(0);
527
+ const FTransform RootTransform = ComponentPose.GetComponentSpaceTransform(RootIndex);
528
+ SampleObject->SetObjectField(TEXT("rootComponent"), AnimQaTransformJson(RootTransform));
529
+ const FVector RootPosition = RootTransform.GetTranslation();
530
+ if (!bHasPreviousRoot)
531
+ {
532
+ FirstRoot = RootPosition;
533
+ FirstLocalTransforms = CurrentLocalTransforms;
534
+ }
535
+ else
536
+ {
537
+ const double DeltaSeconds = TimeSeconds - PreviousTime;
538
+ if (DeltaSeconds > SMALL_NUMBER)
539
+ {
540
+ MaxRootSpeed = FMath::Max(
541
+ MaxRootSpeed,
542
+ static_cast<double>(FVector::Distance(RootPosition, PreviousRoot)) / DeltaSeconds);
543
+ }
544
+ }
545
+ bHasPreviousRoot = true;
546
+ PreviousRoot = RootPosition;
547
+ PreviousTime = TimeSeconds;
548
+ LastRoot = RootPosition;
549
+ LastLocalTransforms = CurrentLocalTransforms;
550
+
551
+ SampleValues.Add(MakeShared<FJsonValueObject>(SampleObject));
552
+ FString SampleJson;
553
+ if (!AnimQaSerializeObject(SampleObject, SampleJson, true))
554
+ {
555
+ return MCPError(FString::Printf(TEXT("Failed to serialize animation sample at frame %d"), Frame));
556
+ }
557
+ SampleLines.Add(MoveTemp(SampleJson));
558
+ }
559
+
560
+ double LoopMaxAngle = 0.0;
561
+ double LoopAngleSquaredSum = 0.0;
562
+ int32 LoopAngleCount = 0;
563
+ const bool bHasRootMotion = Sequence->HasRootMotion();
564
+ if (bLoop && FirstLocalTransforms.Num() == LastLocalTransforms.Num())
565
+ {
566
+ for (int32 Index = 0; Index < FirstLocalTransforms.Num(); ++Index)
567
+ {
568
+ if (bHasRootMotion && BoneIndices[Index] == 0)
569
+ {
570
+ continue;
571
+ }
572
+ const double Angle = AnimQaQuatAngleDegrees(
573
+ FirstLocalTransforms[Index].GetRotation(),
574
+ LastLocalTransforms[Index].GetRotation());
575
+ LoopMaxAngle = FMath::Max(LoopMaxAngle, Angle);
576
+ LoopAngleSquaredSum += Angle * Angle;
577
+ ++LoopAngleCount;
578
+ }
579
+ }
580
+ const double LoopRmsAngle = LoopAngleCount > 0
581
+ ? FMath::Sqrt(LoopAngleSquaredSum / static_cast<double>(LoopAngleCount))
582
+ : 0.0;
583
+ const double RootDisplacement = FVector::Distance(FirstRoot, LastRoot);
584
+ const double LoopRootTranslationError = bHasRootMotion ? 0.0 : RootDisplacement;
585
+ FAnimExtractContext RootMotionContext;
586
+ #if WITH_EDITOR
587
+ RootMotionContext.bIgnoreRootLock = true;
588
+ #endif
589
+ #if ENGINE_MAJOR_VERSION > 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 6)
590
+ const FTransform CycleRootMotion = Sequence->ExtractRootMotionFromRange(0.0, DurationSeconds, RootMotionContext);
591
+ #else
592
+ const FTransform CycleRootMotion = Sequence->ExtractRootMotionFromRange(0.0, DurationSeconds);
593
+ #endif
594
+ const bool bLoopWarn = bLoop && (LoopRootTranslationError > 1.0 || LoopMaxAngle > 5.0);
595
+
596
+ TSharedPtr<FJsonObject> Summary = MakeShared<FJsonObject>();
597
+ Summary->SetStringField(TEXT("status"), !bNumericIntegrity ? TEXT("fail") : (bLoopWarn ? TEXT("warn") : TEXT("pass")));
598
+ Summary->SetStringField(TEXT("confidence"), TEXT("high"));
599
+ Summary->SetBoolField(TEXT("numericIntegrity"), bNumericIntegrity);
600
+ Summary->SetNumberField(TEXT("invalidTransformCount"), InvalidTransformCount);
601
+ Summary->SetBoolField(TEXT("hasRootMotion"), bHasRootMotion);
602
+ Summary->SetNumberField(TEXT("rootDisplacementCm"), RootDisplacement);
603
+ Summary->SetNumberField(TEXT("maxRootSpeedCmPerSecond"), MaxRootSpeed);
604
+ Summary->SetObjectField(TEXT("boundsMinCm"), AnimQaVectorJson(BoundsMin));
605
+ Summary->SetObjectField(TEXT("boundsMaxCm"), AnimQaVectorJson(BoundsMax));
606
+ TSharedPtr<FJsonObject> LoopSummary = MakeShared<FJsonObject>();
607
+ LoopSummary->SetBoolField(TEXT("evaluated"), bLoop);
608
+ LoopSummary->SetBoolField(TEXT("rootExcludedFromPoseSeam"), bHasRootMotion);
609
+ LoopSummary->SetNumberField(TEXT("rootTranslationErrorCm"), bLoop ? LoopRootTranslationError : 0.0);
610
+ LoopSummary->SetNumberField(TEXT("jointAngleRmsDegrees"), bLoop ? LoopRmsAngle : 0.0);
611
+ LoopSummary->SetNumberField(TEXT("jointAngleMaxDegrees"), bLoop ? LoopMaxAngle : 0.0);
612
+ LoopSummary->SetObjectField(TEXT("cycleRootMotion"), AnimQaTransformJson(CycleRootMotion));
613
+ Summary->SetObjectField(TEXT("loopSeam"), LoopSummary);
614
+
615
+ FString OutputDirectory;
616
+ FString OutputError;
617
+ if (!AnimQaResolveOutputDirectory(OptionalString(Params, TEXT("outputDirectory")), OutputDirectory, OutputError))
618
+ {
619
+ return MCPError(OutputError);
620
+ }
621
+
622
+ TSharedPtr<FJsonObject> Manifest = MakeShared<FJsonObject>();
623
+ Manifest->SetStringField(TEXT("schema"), TEXT("ue-mcp://animation-validation/v1"));
624
+ Manifest->SetStringField(TEXT("assetPath"), AssetPath);
625
+ Manifest->SetStringField(TEXT("skeletonPath"), Skeleton->GetPathName());
626
+ if (!SkeletalMeshPath.IsEmpty()) Manifest->SetStringField(TEXT("skeletalMeshPath"), SkeletalMeshPath);
627
+ Manifest->SetNumberField(TEXT("durationSeconds"), DurationSeconds);
628
+ Manifest->SetNumberField(TEXT("rateScale"), RateScale);
629
+ if (bHasEffectiveTiming)
630
+ {
631
+ Manifest->SetNumberField(TEXT("effectiveDurationSeconds"), EffectiveDurationSeconds);
632
+ }
633
+ else
634
+ {
635
+ Manifest->SetField(TEXT("effectiveDurationSeconds"), MakeShared<FJsonValueNull>());
636
+ }
637
+ Manifest->SetArrayField(TEXT("notifies"), NotifyValues);
638
+ TSharedPtr<FJsonObject> FrameRateObject = MakeShared<FJsonObject>();
639
+ FrameRateObject->SetNumberField(TEXT("numerator"), SourceRate.Numerator);
640
+ FrameRateObject->SetNumberField(TEXT("denominator"), SourceRate.Denominator);
641
+ Manifest->SetObjectField(TEXT("displayRate"), FrameRateObject);
642
+ Manifest->SetNumberField(TEXT("sourceFrameCount"), SourceFrameCount);
643
+ Manifest->SetNumberField(TEXT("sampleCount"), Frames.Num());
644
+ Manifest->SetBoolField(TEXT("loop"), bLoop);
645
+ Manifest->SetStringField(TEXT("units"), TEXT("centimeters"));
646
+ Manifest->SetStringField(TEXT("handedness"), TEXT("left"));
647
+ Manifest->SetStringField(TEXT("forward"), TEXT("+X"));
648
+ Manifest->SetStringField(TEXT("right"), TEXT("+Y"));
649
+ Manifest->SetStringField(TEXT("up"), TEXT("+Z"));
650
+ Manifest->SetObjectField(TEXT("summary"), Summary);
651
+ Manifest->SetStringField(TEXT("samplesFile"), TEXT("samples.ndjson"));
652
+
653
+ FString ManifestPath;
654
+ FString SamplesPath;
655
+ if (!OutputDirectory.IsEmpty())
656
+ {
657
+ if (!IFileManager::Get().MakeDirectory(*OutputDirectory, true))
658
+ {
659
+ return MCPError(FString::Printf(TEXT("Failed to create output directory: %s"), *OutputDirectory));
660
+ }
661
+ ManifestPath = FPaths::Combine(OutputDirectory, TEXT("manifest.json"));
662
+ SamplesPath = FPaths::Combine(OutputDirectory, TEXT("samples.ndjson"));
663
+ if (IFileManager::Get().FileExists(*ManifestPath) || IFileManager::Get().FileExists(*SamplesPath))
664
+ {
665
+ return MCPError(FString::Printf(
666
+ TEXT("outputDirectory already contains animation validation artifacts: %s"),
667
+ *OutputDirectory));
668
+ }
669
+ const FString ManifestTempPath = ManifestPath + TEXT(".tmp");
670
+ const FString SamplesTempPath = SamplesPath + TEXT(".tmp");
671
+ IFileManager::Get().Delete(*ManifestTempPath, false, true);
672
+ IFileManager::Get().Delete(*SamplesTempPath, false, true);
673
+ FString ManifestJson;
674
+ if (!AnimQaSerializeObject(Manifest, ManifestJson, false))
675
+ {
676
+ return MCPError(TEXT("Failed to serialize animation validation manifest"));
677
+ }
678
+ const FString SamplesJson = FString::Join(SampleLines, TEXT("\n")) + TEXT("\n");
679
+ if (!FFileHelper::SaveStringToFile(
680
+ SamplesJson,
681
+ *SamplesTempPath,
682
+ FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
683
+ {
684
+ return MCPError(FString::Printf(TEXT("Failed to write samples: %s"), *SamplesPath));
685
+ }
686
+ if (!FFileHelper::SaveStringToFile(
687
+ ManifestJson,
688
+ *ManifestTempPath,
689
+ FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
690
+ {
691
+ IFileManager::Get().Delete(*SamplesTempPath, false, true);
692
+ return MCPError(FString::Printf(TEXT("Failed to write manifest: %s"), *ManifestPath));
693
+ }
694
+ if (!IFileManager::Get().Move(*SamplesPath, *SamplesTempPath, false, true))
695
+ {
696
+ IFileManager::Get().Delete(*ManifestTempPath, false, true);
697
+ IFileManager::Get().Delete(*SamplesTempPath, false, true);
698
+ return MCPError(FString::Printf(TEXT("Failed to commit samples: %s"), *SamplesPath));
699
+ }
700
+ if (!IFileManager::Get().Move(*ManifestPath, *ManifestTempPath, false, true))
701
+ {
702
+ IFileManager::Get().Delete(*ManifestTempPath, false, true);
703
+ IFileManager::Get().Delete(*SamplesPath, false, true);
704
+ return MCPError(FString::Printf(TEXT("Failed to commit manifest: %s"), *ManifestPath));
705
+ }
706
+ }
707
+
708
+ TSharedPtr<FJsonObject> Result = MCPSuccess();
709
+ Result->SetStringField(TEXT("assetPath"), AssetPath);
710
+ Result->SetStringField(TEXT("skeletonPath"), Skeleton->GetPathName());
711
+ if (!SkeletalMeshPath.IsEmpty()) Result->SetStringField(TEXT("skeletalMeshPath"), SkeletalMeshPath);
712
+ Result->SetNumberField(TEXT("durationSeconds"), DurationSeconds);
713
+ Result->SetNumberField(TEXT("rateScale"), RateScale);
714
+ if (bHasEffectiveTiming)
715
+ {
716
+ Result->SetNumberField(TEXT("effectiveDurationSeconds"), EffectiveDurationSeconds);
717
+ }
718
+ else
719
+ {
720
+ Result->SetField(TEXT("effectiveDurationSeconds"), MakeShared<FJsonValueNull>());
721
+ }
722
+ Result->SetArrayField(TEXT("notifies"), NotifyValues);
723
+ Result->SetNumberField(TEXT("sourceFrameCount"), SourceFrameCount);
724
+ Result->SetNumberField(TEXT("sampleCount"), Frames.Num());
725
+ Result->SetObjectField(TEXT("displayRate"), FrameRateObject);
726
+ Result->SetObjectField(TEXT("summary"), Summary);
727
+ Result->SetArrayField(TEXT("samples"), SampleValues);
728
+ if (!OutputDirectory.IsEmpty())
729
+ {
730
+ Result->SetStringField(TEXT("outputDirectory"), OutputDirectory);
731
+ Result->SetStringField(TEXT("manifestPath"), ManifestPath);
732
+ Result->SetStringField(TEXT("samplesPath"), SamplesPath);
733
+ }
734
+ return MCPResult(Result);
735
+ }