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,1169 @@
1
+ // level(query_components) - one editor-side component query for whole-map
2
+ // questions (#910, #943, #912).
3
+ //
4
+ // The complaint these three issues share is not a missing field. It is that
5
+ // answering a level-wide question meant shipping every candidate out to the
6
+ // client and filtering there: get_actors_by_component_class returned over a
7
+ // megabyte on a real map, and get_nanite_info answers one mesh per call. So
8
+ // the selector, the projection, the predicates, the grouping and the counts
9
+ // all run here, and only the answer crosses the wire.
10
+ //
11
+ // Three things this deliberately does NOT do:
12
+ //
13
+ // - It never writes. No MarkPackageDirty, no SetX, no property assignment.
14
+ // A map-wide audit that dirties 3,900 actors is worse than no audit
15
+ // (#912). The only state it can touch is the open map, and only when the
16
+ // caller passes levelPath: then it refuses outright if anything is
17
+ // already dirty, and it puts the original map back afterwards.
18
+ // - It never returns an unbounded payload. Rows are capped, groups are
19
+ // capped, value histograms are capped, and every cap reports itself.
20
+ // - It never reports a raw flag where the flag is not the answer. CastShadow
21
+ // true on a hidden debug marker is not a shadow caster, and
22
+ // str(mobility) == "EComponentMobility.MOVABLE" is why a substring test
23
+ // for "Movable" silently matched nothing. Both are computed here.
24
+
25
+ #include "LevelHandlers.h"
26
+
27
+ #include "HandlerRegistry.h"
28
+ #include "HandlerQuery.h"
29
+ #include "HandlerUtils.h"
30
+ #include "HandlerEditorState.h"
31
+
32
+ #include "Components/ActorComponent.h"
33
+ #include "Components/BoxComponent.h"
34
+ #include "Components/DecalComponent.h"
35
+ #include "Components/HierarchicalInstancedStaticMeshComponent.h"
36
+ #include "Components/InstancedStaticMeshComponent.h"
37
+ #include "Components/MeshComponent.h"
38
+ #include "Components/PrimitiveComponent.h"
39
+ #include "Components/SceneComponent.h"
40
+ #include "Components/SkinnedMeshComponent.h"
41
+ #include "Components/StaticMeshComponent.h"
42
+ #include "Dom/JsonObject.h"
43
+ #include "Dom/JsonValue.h"
44
+ #include "Editor.h"
45
+ #include "Engine/EngineTypes.h"
46
+ #include "Engine/StaticMesh.h"
47
+ #include "Engine/World.h"
48
+ #include "EngineUtils.h"
49
+ #include "GameFramework/Actor.h"
50
+ #include "GameFramework/Pawn.h"
51
+ #include "Materials/MaterialInterface.h"
52
+ #include "UObject/EnumProperty.h"
53
+ #include "UObject/UnrealType.h"
54
+
55
+ namespace
56
+ {
57
+ // ── Bounds ──────────────────────────────────────────────────────────────
58
+ //
59
+ // Every one of these exists because an unbounded version of this query is
60
+ // what the issues are complaining about.
61
+ constexpr int32 MCPQueryDefaultRowLimit = 200;
62
+ constexpr int32 MCPQueryMaxRowLimit = 2000;
63
+ /** Rows kept in memory for sorting and paging. `matched` stays exact past
64
+ * this; only the retained window is capped. */
65
+ constexpr int32 MCPQueryMaxRetainedRows = 20000;
66
+ constexpr int32 MCPQueryMaxGroups = 200;
67
+ constexpr int32 MCPQueryDefaultGroupSamples = 5;
68
+ constexpr int32 MCPQueryMaxGroupSamples = 25;
69
+ constexpr int32 MCPQueryMaxCountByPaths = 8;
70
+ constexpr int32 MCPQueryMaxCountByValues = 64;
71
+ constexpr int32 MCPQueryMaxProjectedProperties = 32;
72
+ constexpr int32 MCPQueryMaxPredicates = 24;
73
+ constexpr int32 MCPQueryMaxMaterialSlots = 64;
74
+ constexpr int32 MCPQueryMaxDuplicateGroups = 100;
75
+
76
+ const TCHAR* const MCPQueryWorldGridMaterial = TEXT("/Engine/EngineMaterials/WorldGridMaterial");
77
+
78
+ // ── Clean enum names ────────────────────────────────────────────────────
79
+ //
80
+ // #943's reporter lost a whole result set to `str(mobility)` producing
81
+ // "EComponentMobility.MOVABLE" and a case-sensitive test for "Movable"
82
+ // matching none of it. These switches are what a caller compares against.
83
+
84
+ const TCHAR* MCPQueryMobilityName(EComponentMobility::Type Mobility)
85
+ {
86
+ switch (Mobility)
87
+ {
88
+ case EComponentMobility::Static: return TEXT("Static");
89
+ case EComponentMobility::Stationary: return TEXT("Stationary");
90
+ case EComponentMobility::Movable: return TEXT("Movable");
91
+ default: return TEXT("Unknown");
92
+ }
93
+ }
94
+
95
+ const TCHAR* MCPQueryCollisionEnabledName(ECollisionEnabled::Type Value)
96
+ {
97
+ switch (Value)
98
+ {
99
+ case ECollisionEnabled::NoCollision: return TEXT("NoCollision");
100
+ case ECollisionEnabled::QueryOnly: return TEXT("QueryOnly");
101
+ case ECollisionEnabled::PhysicsOnly: return TEXT("PhysicsOnly");
102
+ case ECollisionEnabled::QueryAndPhysics: return TEXT("QueryAndPhysics");
103
+ case ECollisionEnabled::ProbeOnly: return TEXT("ProbeOnly");
104
+ case ECollisionEnabled::QueryAndProbe: return TEXT("QueryAndProbe");
105
+ default: return TEXT("Unknown");
106
+ }
107
+ }
108
+
109
+ const TCHAR* MCPQueryNavGeometryName(EHasCustomNavigableGeometry::Type Value)
110
+ {
111
+ switch (Value)
112
+ {
113
+ case EHasCustomNavigableGeometry::No: return TEXT("No");
114
+ case EHasCustomNavigableGeometry::Yes: return TEXT("Yes");
115
+ case EHasCustomNavigableGeometry::EvenIfNotCollidable: return TEXT("EvenIfNotCollidable");
116
+ case EHasCustomNavigableGeometry::DontExport: return TEXT("DontExport");
117
+ default: return TEXT("Unknown");
118
+ }
119
+ }
120
+
121
+ // ── JSON shaping ────────────────────────────────────────────────────────
122
+
123
+ TSharedPtr<FJsonObject> MCPQueryVec(const FVector& V)
124
+ {
125
+ TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
126
+ Obj->SetNumberField(TEXT("x"), V.X);
127
+ Obj->SetNumberField(TEXT("y"), V.Y);
128
+ Obj->SetNumberField(TEXT("z"), V.Z);
129
+ return Obj;
130
+ }
131
+
132
+ TSharedPtr<FJsonObject> MCPQueryRot(const FRotator& R)
133
+ {
134
+ TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
135
+ Obj->SetNumberField(TEXT("pitch"), R.Pitch);
136
+ Obj->SetNumberField(TEXT("yaw"), R.Yaw);
137
+ Obj->SetNumberField(TEXT("roll"), R.Roll);
138
+ return Obj;
139
+ }
140
+
141
+ // ── Projection ──────────────────────────────────────────────────────────
142
+
143
+ struct FMCPQueryFields
144
+ {
145
+ bool bTransform = false;
146
+ bool bBounds = false;
147
+ bool bLocalBounds = false;
148
+ bool bShadow = false;
149
+ bool bNanite = false;
150
+ bool bNavigation = false;
151
+ bool bTick = false;
152
+ bool bMaterials = false;
153
+ bool bMesh = false;
154
+ bool bDecal = false;
155
+ bool bHealth = false;
156
+ };
157
+
158
+ /** Every field name this action understands, so an unknown one is an error
159
+ * the caller sees rather than a silently empty projection. */
160
+ bool MCPQueryApplyFieldName(const FString& Name, FMCPQueryFields& Fields)
161
+ {
162
+ if (Name == TEXT("transform")) { Fields.bTransform = true; return true; }
163
+ if (Name == TEXT("bounds")) { Fields.bBounds = true; return true; }
164
+ if (Name == TEXT("localBounds")) { Fields.bLocalBounds = true; return true; }
165
+ if (Name == TEXT("shadow")) { Fields.bShadow = true; return true; }
166
+ if (Name == TEXT("nanite")) { Fields.bNanite = true; return true; }
167
+ if (Name == TEXT("navigation")) { Fields.bNavigation = true; return true; }
168
+ if (Name == TEXT("tick")) { Fields.bTick = true; return true; }
169
+ if (Name == TEXT("materials")) { Fields.bMaterials = true; return true; }
170
+ if (Name == TEXT("mesh")) { Fields.bMesh = true; return true; }
171
+ if (Name == TEXT("decal")) { Fields.bDecal = true; return true; }
172
+ if (Name == TEXT("health"))
173
+ {
174
+ // health is derived from the others, so asking for it turns on
175
+ // everything it reads rather than reporting false negatives.
176
+ Fields.bHealth = true;
177
+ Fields.bBounds = true;
178
+ Fields.bTransform = true;
179
+ Fields.bMesh = true;
180
+ Fields.bDecal = true;
181
+ Fields.bMaterials = true;
182
+ return true;
183
+ }
184
+ return false;
185
+ }
186
+
187
+ /** The asset a component renders, if it renders one. */
188
+ UObject* MCPQueryMeshAsset(UActorComponent* Component, FString& OutKind, bool& bOutIsMeshComponent)
189
+ {
190
+ bOutIsMeshComponent = false;
191
+ OutKind.Reset();
192
+ if (UStaticMeshComponent* StaticMeshComponent = Cast<UStaticMeshComponent>(Component))
193
+ {
194
+ bOutIsMeshComponent = true;
195
+ OutKind = TEXT("StaticMesh");
196
+ return StaticMeshComponent->GetStaticMesh();
197
+ }
198
+ if (USkinnedMeshComponent* SkinnedComponent = Cast<USkinnedMeshComponent>(Component))
199
+ {
200
+ bOutIsMeshComponent = true;
201
+ OutKind = TEXT("SkinnedAsset");
202
+ return SkinnedComponent->GetSkinnedAsset();
203
+ }
204
+ return nullptr;
205
+ }
206
+
207
+ /** Read a UPROPERTY off a component by name without linking its module.
208
+ * NavModifierComponent lives in NavigationSystem and its AreaClass is the
209
+ * one field #943 needs; reflection reaches it on any engine version and
210
+ * degrades to absent rather than to a link error. */
211
+ TSharedPtr<FJsonValue> MCPQueryReflectProperty(UObject* Object, const TCHAR* PropertyName)
212
+ {
213
+ if (!Object)
214
+ {
215
+ return nullptr;
216
+ }
217
+ FProperty* Property = Object->GetClass()->FindPropertyByName(FName(PropertyName));
218
+ if (!Property)
219
+ {
220
+ return nullptr;
221
+ }
222
+ return MCPQuery::PropertyToJson(Property, Property->ContainerPtrToValuePtr<void>(Object));
223
+ }
224
+
225
+ struct FMCPQueryRow
226
+ {
227
+ FString SortKey;
228
+ TSharedPtr<FJsonObject> Object;
229
+ FIntVector QuantizedLocation = FIntVector::ZeroValue;
230
+ bool bHasTransform = false;
231
+ };
232
+ }
233
+
234
+ // ---------------------------------------------------------------------------
235
+ // query_components
236
+ // ---------------------------------------------------------------------------
237
+ TSharedPtr<FJsonValue> FLevelHandlers::QueryComponents(const TSharedPtr<FJsonObject>& Params)
238
+ {
239
+ MCP_CHECK_GAME_THREAD();
240
+
241
+ // ── Parameters ──────────────────────────────────────────────────────────
242
+ const FString WorldScope = OptionalString(Params, TEXT("world"), TEXT("editor"));
243
+ const FString LevelPath = OptionalString(Params, TEXT("levelPath"));
244
+
245
+ const FString ComponentClassSpec = OptionalString(Params, TEXT("componentClass"));
246
+ const FString ActorClassSpec = OptionalString(Params, TEXT("actorClass"));
247
+ const bool bMatchSubclasses = OptionalBool(Params, TEXT("matchSubclasses"), true);
248
+ const FString ComponentNameContains = OptionalString(Params, TEXT("componentNameContains"));
249
+ const FString ActorLabelPrefix = OptionalString(Params, TEXT("actorLabelPrefix"));
250
+ const FString ActorLabelContains = OptionalString(Params, TEXT("actorLabelContains"));
251
+ const FString ActorTag = OptionalString(Params, TEXT("actorTag"));
252
+ const FString FolderPath = OptionalString(Params, TEXT("folderPath"));
253
+ const FString FolderPathPrefix = OptionalString(Params, TEXT("folderPathPrefix"));
254
+
255
+ const FString WhereMode = OptionalString(Params, TEXT("whereMode"), TEXT("all")).ToLower();
256
+ if (WhereMode != TEXT("all") && WhereMode != TEXT("any"))
257
+ {
258
+ return MCPError(TEXT("'whereMode' must be either 'all' or 'any'"));
259
+ }
260
+
261
+ const bool bCountOnly = OptionalBool(Params, TEXT("countOnly"), false);
262
+ const bool bSuspectOnly = OptionalBool(Params, TEXT("suspectOnly"), false);
263
+ const FString GroupBy = OptionalString(Params, TEXT("groupBy"));
264
+ const int32 GroupSamples = FMath::Clamp(
265
+ OptionalInt(Params, TEXT("sampleLimit"), MCPQueryDefaultGroupSamples), 0, MCPQueryMaxGroupSamples);
266
+ const int32 Limit = FMath::Clamp(
267
+ OptionalInt(Params, TEXT("limit"), MCPQueryDefaultRowLimit), 0, MCPQueryMaxRowLimit);
268
+ const int32 StartIndex = FMath::Max(0, OptionalInt(Params, TEXT("startIndex"), 0));
269
+ const double DuplicateTolerance = OptionalNumber(Params, TEXT("duplicateTransformTolerance"), 0.0);
270
+
271
+ // Projection.
272
+ FMCPQueryFields Fields;
273
+ bool bAnyFieldRequested = false;
274
+ {
275
+ const TArray<TSharedPtr<FJsonValue>>* FieldValues = nullptr;
276
+ if (Params->TryGetArrayField(TEXT("fields"), FieldValues) && FieldValues)
277
+ {
278
+ for (const FString& Name : JsonArrayToStringList(FieldValues))
279
+ {
280
+ if (!MCPQueryApplyFieldName(Name, Fields))
281
+ {
282
+ return MCPError(FString::Printf(
283
+ TEXT("Unknown 'fields' entry '%s'. Valid: transform, bounds, localBounds, shadow, nanite, navigation, tick, materials, mesh, decal, health"),
284
+ *Name));
285
+ }
286
+ bAnyFieldRequested = true;
287
+ }
288
+ }
289
+ }
290
+ if (bSuspectOnly)
291
+ {
292
+ MCPQueryApplyFieldName(TEXT("health"), Fields);
293
+ bAnyFieldRequested = true;
294
+ }
295
+ if (!bAnyFieldRequested)
296
+ {
297
+ // A caller who names no fields gets identity plus transform: enough to
298
+ // act on a row, small enough that an unfiltered query is not the
299
+ // megabyte payload these issues are about.
300
+ Fields.bTransform = true;
301
+ }
302
+
303
+ TArray<FString> ProjectedProperties;
304
+ {
305
+ const TArray<TSharedPtr<FJsonValue>>* PropertyValues = nullptr;
306
+ if (Params->TryGetArrayField(TEXT("propertyNames"), PropertyValues) && PropertyValues)
307
+ {
308
+ ProjectedProperties = JsonArrayToStringList(PropertyValues);
309
+ }
310
+ }
311
+ if (ProjectedProperties.Num() > MCPQueryMaxProjectedProperties)
312
+ {
313
+ return MCPError(FString::Printf(
314
+ TEXT("'propertyNames' exceeds the maximum of %d entries"), MCPQueryMaxProjectedProperties));
315
+ }
316
+
317
+ // Predicates. Parsing and evaluation live in the shared query header, so
318
+ // this action and asset(bulk_read_properties) cannot drift apart on what
319
+ // an operator name means.
320
+ TArray<MCPQuery::FPredicate> Predicates;
321
+ {
322
+ const TArray<TSharedPtr<FJsonValue>>* WhereValues = nullptr;
323
+ Params->TryGetArrayField(TEXT("where"), WhereValues);
324
+ FString ParseError;
325
+ if (!MCPQuery::ParsePredicates(WhereValues, MCPQueryMaxPredicates, Predicates, ParseError))
326
+ {
327
+ return MCPError(ParseError);
328
+ }
329
+ }
330
+
331
+ if (bSuspectOnly)
332
+ {
333
+ MCPQuery::FPredicate Suspect;
334
+ Suspect.Field = TEXT("health.suspect");
335
+ Suspect.Op = TEXT("isTrue");
336
+ Predicates.Add(MoveTemp(Suspect));
337
+ // suspectOnly is an AND on top of whatever else was asked for, so an
338
+ // `any` request keeps its own semantics only among its own predicates.
339
+ // Rather than silently changing that, refuse the ambiguous combination.
340
+ if (WhereMode == TEXT("any") && Predicates.Num() > 1)
341
+ {
342
+ return MCPError(TEXT("'suspectOnly' cannot be combined with whereMode 'any'; add {field:'health.suspect', op:'isTrue'} to 'where' instead"));
343
+ }
344
+ }
345
+
346
+ // Value histograms.
347
+ TArray<FString> CountByPaths;
348
+ {
349
+ const TArray<TSharedPtr<FJsonValue>>* CountByValues = nullptr;
350
+ if (Params->TryGetArrayField(TEXT("countBy"), CountByValues) && CountByValues)
351
+ {
352
+ CountByPaths = JsonArrayToStringList(CountByValues);
353
+ }
354
+ }
355
+ if (CountByPaths.Num() > MCPQueryMaxCountByPaths)
356
+ {
357
+ return MCPError(FString::Printf(
358
+ TEXT("'countBy' exceeds the maximum of %d paths"), MCPQueryMaxCountByPaths));
359
+ }
360
+
361
+ // A predicate on `shadow.effectiveCastShadow` with `shadow` left out of
362
+ // `fields` would resolve to an absent field and match nothing, and a
363
+ // confident zero is the worst answer this action can give. So a path that
364
+ // names a field group turns that group on.
365
+ {
366
+ auto EnableGroupNamedBy = [&Fields](const FString& Path)
367
+ {
368
+ FString Head;
369
+ FString Tail;
370
+ MCPQueryApplyFieldName(Path.Split(TEXT("."), &Head, &Tail) ? Head : Path, Fields);
371
+ };
372
+ for (const MCPQuery::FPredicate& Predicate : Predicates) EnableGroupNamedBy(Predicate.Field);
373
+ for (const FString& CountPath : CountByPaths) EnableGroupNamedBy(CountPath);
374
+ if (!GroupBy.IsEmpty()) EnableGroupNamedBy(GroupBy);
375
+ }
376
+
377
+ // ── Class resolution ────────────────────────────────────────────────────
378
+ UClass* ComponentClass = nullptr;
379
+ FString ComponentClassFallback;
380
+ if (!ComponentClassSpec.IsEmpty())
381
+ {
382
+ ComponentClass = MCPResolveClassOfType(ComponentClassSpec, UActorComponent::StaticClass(), true);
383
+ if (!ComponentClass)
384
+ {
385
+ // A substring is a legitimate way to ask this question and the
386
+ // existing get_actors_by_component_class accepts one, so keep it
387
+ // working rather than failing a call that used to succeed.
388
+ ComponentClassFallback = ComponentClassSpec;
389
+ }
390
+ }
391
+ UClass* ActorClass = nullptr;
392
+ FString ActorClassFallback;
393
+ if (!ActorClassSpec.IsEmpty())
394
+ {
395
+ ActorClass = MCPResolveClassOfType(ActorClassSpec, AActor::StaticClass(), true);
396
+ if (!ActorClass)
397
+ {
398
+ ActorClassFallback = ActorClassSpec;
399
+ }
400
+ }
401
+
402
+ // ── World selection, and the map-restore contract (#912) ────────────────
403
+ //
404
+ // Reading another map means opening it. Opening it while the editor has
405
+ // unsaved work either discards that work or raises a modal save prompt the
406
+ // bridge cannot answer, so this refuses instead. And whatever happens, the
407
+ // map that was open when the call arrived is the map that is open when it
408
+ // returns.
409
+ FString RestoreLevelPath;
410
+ TArray<FString> InitialDirtyPackages;
411
+ if (!LevelPath.IsEmpty())
412
+ {
413
+ if (!GEditor)
414
+ {
415
+ return MCPError(TEXT("GEditor is not available"));
416
+ }
417
+ if (GEditor->PlayWorld != nullptr || GEditor->bIsSimulatingInEditor)
418
+ {
419
+ return MCPError(TEXT("Stop PIE or SIE before querying another level"));
420
+ }
421
+ if (!WorldScope.Equals(TEXT("editor"), ESearchCase::IgnoreCase))
422
+ {
423
+ return MCPError(TEXT("'levelPath' queries the editor world; drop 'world' or set it to 'editor'"));
424
+ }
425
+ if (!MCPEditorState::IsExistingMapPackage(LevelPath))
426
+ {
427
+ return MCPError(FString::Printf(TEXT("Level package was not found as a .umap: %s"), *LevelPath));
428
+ }
429
+
430
+ MCPEditorState::CollectDirtyEditorPackageNames(InitialDirtyPackages);
431
+ if (!InitialDirtyPackages.IsEmpty())
432
+ {
433
+ auto DirtyResult = MCPSuccess();
434
+ DirtyResult->SetBoolField(TEXT("success"), false);
435
+ DirtyResult->SetStringField(
436
+ TEXT("error"),
437
+ TEXT("Refusing to open another level while content or map packages are dirty"));
438
+ DirtyResult->SetArrayField(TEXT("dirtyPackages"), MCPStringListToJson(InitialDirtyPackages));
439
+ return MCPResult(DirtyResult);
440
+ }
441
+
442
+ const FString CurrentLevelPath = MCPEditorState::CurrentEditorLevelPackageName();
443
+ if (CurrentLevelPath != LevelPath)
444
+ {
445
+ if (!MCPEditorState::IsExistingMapPackage(CurrentLevelPath))
446
+ {
447
+ return MCPError(FString::Printf(
448
+ TEXT("The currently open level cannot be restored from package path '%s', so this refuses to leave it"),
449
+ *CurrentLevelPath));
450
+ }
451
+ TSharedPtr<FJsonObject> LoadParams = MakeShared<FJsonObject>();
452
+ LoadParams->SetStringField(TEXT("levelPath"), LevelPath);
453
+ const TSharedPtr<FJsonValue> LoadResult = FLevelHandlers::LoadLevel(LoadParams);
454
+ if (!LoadResult.IsValid() || LoadResult->Type != EJson::Object ||
455
+ !LoadResult->AsObject()->GetBoolField(TEXT("success")))
456
+ {
457
+ return MCPError(FString::Printf(TEXT("Failed to open level '%s'"), *LevelPath));
458
+ }
459
+ RestoreLevelPath = CurrentLevelPath;
460
+ }
461
+ }
462
+
463
+ // Everything below runs against this world. RestoreLevelPath is put back
464
+ // on every exit path from here on.
465
+ UWorld* World = LevelPath.IsEmpty()
466
+ ? ResolveWorldFromParams(Params, *WorldScope)
467
+ : GetEditorWorld();
468
+
469
+ auto RestoreLevel = [&RestoreLevelPath]()
470
+ {
471
+ if (RestoreLevelPath.IsEmpty())
472
+ {
473
+ return;
474
+ }
475
+ TSharedPtr<FJsonObject> RestoreParams = MakeShared<FJsonObject>();
476
+ RestoreParams->SetStringField(TEXT("levelPath"), RestoreLevelPath);
477
+ FLevelHandlers::LoadLevel(RestoreParams);
478
+ };
479
+
480
+ if (!World)
481
+ {
482
+ RestoreLevel();
483
+ return MCPError(FString::Printf(TEXT("World not available for scope '%s'"), *WorldScope));
484
+ }
485
+
486
+ // ── Scan ────────────────────────────────────────────────────────────────
487
+ int32 ScannedActors = 0;
488
+ int32 ScannedComponents = 0;
489
+ int32 Matched = 0;
490
+ TArray<FMCPQueryRow> Rows;
491
+ TMap<FString, int32> GroupCounts;
492
+ TMap<FString, TArray<FString>> GroupSamplesByKey;
493
+ TArray<FString> GroupOrder;
494
+ TMap<FString, TMap<FString, int32>> CountByHistograms;
495
+ TMap<FIntVector, int32> DuplicateCounts;
496
+ TMap<FIntVector, TArray<FString>> DuplicateSamples;
497
+ bool bGroupsTruncated = false;
498
+
499
+ const double DuplicateQuantum = DuplicateTolerance > 0.0 ? DuplicateTolerance : 0.0;
500
+
501
+ for (TActorIterator<AActor> ActorIt(World); ActorIt; ++ActorIt)
502
+ {
503
+ AActor* Actor = *ActorIt;
504
+ if (!Actor)
505
+ {
506
+ continue;
507
+ }
508
+ ++ScannedActors;
509
+
510
+ if (ActorClass)
511
+ {
512
+ const bool bActorMatches = bMatchSubclasses ? Actor->IsA(ActorClass) : (Actor->GetClass() == ActorClass);
513
+ if (!bActorMatches) continue;
514
+ }
515
+ else if (!ActorClassFallback.IsEmpty() &&
516
+ !Actor->GetClass()->GetName().Contains(ActorClassFallback, ESearchCase::IgnoreCase))
517
+ {
518
+ continue;
519
+ }
520
+
521
+ const FString ActorLabel = Actor->GetActorLabel();
522
+ if (!ActorLabelPrefix.IsEmpty() && !ActorLabel.StartsWith(ActorLabelPrefix, ESearchCase::CaseSensitive)) continue;
523
+ if (!ActorLabelContains.IsEmpty() && !ActorLabel.Contains(ActorLabelContains, ESearchCase::IgnoreCase)) continue;
524
+ if (!ActorTag.IsEmpty() && !Actor->ActorHasTag(FName(*ActorTag))) continue;
525
+
526
+ const FString ActorFolder = Actor->GetFolderPath().ToString();
527
+ if (!FolderPath.IsEmpty() && !ActorFolder.Equals(FolderPath, ESearchCase::IgnoreCase)) continue;
528
+ if (!FolderPathPrefix.IsEmpty() && !ActorFolder.StartsWith(FolderPathPrefix, ESearchCase::IgnoreCase)) continue;
529
+
530
+ const bool bOwnerHidden = Actor->IsHidden();
531
+ const APawn* OwnerPawn = Cast<APawn>(Actor);
532
+
533
+ TArray<UActorComponent*> Components;
534
+ Actor->GetComponents(Components);
535
+ for (UActorComponent* Component : Components)
536
+ {
537
+ if (!Component)
538
+ {
539
+ continue;
540
+ }
541
+ ++ScannedComponents;
542
+
543
+ if (ComponentClass)
544
+ {
545
+ const bool bMatches = bMatchSubclasses
546
+ ? Component->IsA(ComponentClass)
547
+ : (Component->GetClass() == ComponentClass);
548
+ if (!bMatches) continue;
549
+ }
550
+ else if (!ComponentClassFallback.IsEmpty() &&
551
+ !Component->GetClass()->GetName().Contains(ComponentClassFallback, ESearchCase::IgnoreCase))
552
+ {
553
+ continue;
554
+ }
555
+
556
+ const FString ComponentName = Component->GetName();
557
+ if (!ComponentNameContains.IsEmpty() &&
558
+ !ComponentName.Contains(ComponentNameContains, ESearchCase::IgnoreCase))
559
+ {
560
+ continue;
561
+ }
562
+
563
+ USceneComponent* SceneComponent = Cast<USceneComponent>(Component);
564
+ UPrimitiveComponent* Primitive = Cast<UPrimitiveComponent>(Component);
565
+
566
+ TSharedPtr<FJsonObject> Row = MakeShared<FJsonObject>();
567
+ Row->SetStringField(TEXT("actorLabel"), ActorLabel);
568
+ Row->SetStringField(TEXT("actorName"), Actor->GetName());
569
+ Row->SetStringField(TEXT("actorClass"), Actor->GetClass()->GetName());
570
+ Row->SetStringField(TEXT("actorPath"), Actor->GetPathName());
571
+ Row->SetStringField(TEXT("folderPath"), ActorFolder);
572
+ Row->SetStringField(TEXT("componentName"), ComponentName);
573
+ Row->SetStringField(TEXT("componentClass"), Component->GetClass()->GetName());
574
+
575
+ FVector WorldScale = FVector::OneVector;
576
+ bool bHasTransform = false;
577
+ FVector WorldLocation = FVector::ZeroVector;
578
+ if (SceneComponent)
579
+ {
580
+ bHasTransform = true;
581
+ const FTransform ComponentTransform = SceneComponent->GetComponentTransform();
582
+ WorldLocation = ComponentTransform.GetLocation();
583
+ WorldScale = ComponentTransform.GetScale3D();
584
+ if (Fields.bTransform)
585
+ {
586
+ TSharedPtr<FJsonObject> TransformObject = MakeShared<FJsonObject>();
587
+ TransformObject->SetObjectField(TEXT("location"), MCPQueryVec(WorldLocation));
588
+ TransformObject->SetObjectField(TEXT("rotation"), MCPQueryRot(ComponentTransform.Rotator()));
589
+ TransformObject->SetObjectField(TEXT("scale"), MCPQueryVec(WorldScale));
590
+ TransformObject->SetNumberField(TEXT("minAbsScale"),
591
+ FMath::Min3(FMath::Abs(WorldScale.X), FMath::Abs(WorldScale.Y), FMath::Abs(WorldScale.Z)));
592
+ Row->SetObjectField(TEXT("transform"), TransformObject);
593
+ }
594
+ }
595
+
596
+ bool bBoundsZero = false;
597
+ bool bBoundsInvalid = false;
598
+ if (SceneComponent && (Fields.bBounds || Fields.bHealth))
599
+ {
600
+ const FBoxSphereBounds ComponentBounds = SceneComponent->GetBounds();
601
+ const FVector Extent = ComponentBounds.BoxExtent;
602
+ bBoundsZero = Extent.IsNearlyZero() && FMath::IsNearlyZero(ComponentBounds.SphereRadius);
603
+ bBoundsInvalid =
604
+ Extent.ContainsNaN() || ComponentBounds.Origin.ContainsNaN() ||
605
+ !FMath::IsFinite(ComponentBounds.SphereRadius) ||
606
+ Extent.X < 0.0 || Extent.Y < 0.0 || Extent.Z < 0.0;
607
+ if (Fields.bBounds)
608
+ {
609
+ TSharedPtr<FJsonObject> BoundsObject = MakeShared<FJsonObject>();
610
+ BoundsObject->SetObjectField(TEXT("origin"), MCPQueryVec(ComponentBounds.Origin));
611
+ BoundsObject->SetObjectField(TEXT("boxExtent"), MCPQueryVec(Extent));
612
+ BoundsObject->SetNumberField(TEXT("sphereRadius"), ComponentBounds.SphereRadius);
613
+ BoundsObject->SetBoolField(TEXT("zero"), bBoundsZero);
614
+ BoundsObject->SetBoolField(TEXT("invalid"), bBoundsInvalid);
615
+ Row->SetObjectField(TEXT("bounds"), BoundsObject);
616
+ }
617
+ }
618
+
619
+ if (SceneComponent && Fields.bLocalBounds)
620
+ {
621
+ // #914: the local, unscaled bounds. A caller comparing two
622
+ // shapes needs the authored extent, not the extent times the
623
+ // component scale, and the scaled one is all the existing
624
+ // reads expose.
625
+ const FBoxSphereBounds LocalBounds = SceneComponent->GetLocalBounds();
626
+ TSharedPtr<FJsonObject> LocalObject = MakeShared<FJsonObject>();
627
+ LocalObject->SetObjectField(TEXT("origin"), MCPQueryVec(LocalBounds.Origin));
628
+ LocalObject->SetObjectField(TEXT("boxExtent"), MCPQueryVec(LocalBounds.BoxExtent));
629
+ LocalObject->SetNumberField(TEXT("sphereRadius"), LocalBounds.SphereRadius);
630
+ if (const UBoxComponent* BoxComponent = Cast<UBoxComponent>(Component))
631
+ {
632
+ // #914: UBoxComponent.BoxExtent itself, which is the
633
+ // authored value the details panel shows and is not
634
+ // derivable from the scaled bounds.
635
+ LocalObject->SetObjectField(TEXT("boxExtent_unscaled"),
636
+ MCPQueryVec(BoxComponent->GetUnscaledBoxExtent()));
637
+ }
638
+ Row->SetObjectField(TEXT("localBounds"), LocalObject);
639
+ }
640
+
641
+ // ── #910: effective shadow casting ──────────────────────────────
642
+ if (Primitive && Fields.bShadow)
643
+ {
644
+ const bool bVisibleFlag = Primitive->GetVisibleFlag();
645
+ const bool bVisible = Primitive->IsVisible();
646
+ const bool bHiddenInGame = Primitive->bHiddenInGame;
647
+ const bool bRenderedInGame = bVisible && !bHiddenInGame && !bOwnerHidden;
648
+ const bool bCastShadowFlag = Primitive->CastShadow != 0;
649
+ const bool bCastHidden = Primitive->bCastHiddenShadow != 0;
650
+ const bool bEffective = bCastShadowFlag && (bRenderedInGame || bCastHidden);
651
+
652
+ FString Reason;
653
+ if (bEffective)
654
+ {
655
+ Reason = bRenderedInGame ? TEXT("rendered and CastShadow") : TEXT("hidden but bCastHiddenShadow");
656
+ }
657
+ else if (!bCastShadowFlag)
658
+ {
659
+ Reason = TEXT("CastShadow is false");
660
+ }
661
+ else if (!bVisible)
662
+ {
663
+ Reason = TEXT("component is not visible");
664
+ }
665
+ else if (bHiddenInGame)
666
+ {
667
+ Reason = TEXT("bHiddenInGame and not bCastHiddenShadow");
668
+ }
669
+ else
670
+ {
671
+ Reason = TEXT("owning actor is hidden and not bCastHiddenShadow");
672
+ }
673
+
674
+ TSharedPtr<FJsonObject> ShadowObject = MakeShared<FJsonObject>();
675
+ ShadowObject->SetBoolField(TEXT("castShadow"), bCastShadowFlag);
676
+ ShadowObject->SetBoolField(TEXT("castHiddenShadow"), bCastHidden);
677
+ ShadowObject->SetBoolField(TEXT("castDynamicShadow"), Primitive->bCastDynamicShadow != 0);
678
+ ShadowObject->SetBoolField(TEXT("castStaticShadow"), Primitive->bCastStaticShadow != 0);
679
+ ShadowObject->SetBoolField(TEXT("visibleFlag"), bVisibleFlag);
680
+ ShadowObject->SetBoolField(TEXT("visible"), bVisible);
681
+ ShadowObject->SetBoolField(TEXT("hiddenInGame"), bHiddenInGame);
682
+ ShadowObject->SetBoolField(TEXT("ownerHidden"), bOwnerHidden);
683
+ ShadowObject->SetBoolField(TEXT("renderedInGame"), bRenderedInGame);
684
+ ShadowObject->SetBoolField(TEXT("effectiveCastShadow"), bEffective);
685
+ ShadowObject->SetStringField(TEXT("reason"), Reason);
686
+ ShadowObject->SetStringField(TEXT("mobility"), MCPQueryMobilityName(Primitive->Mobility));
687
+ Row->SetObjectField(TEXT("shadow"), ShadowObject);
688
+ }
689
+
690
+ // ── #910: Nanite state, without a call per mesh ─────────────────
691
+ if (Fields.bNanite)
692
+ {
693
+ TSharedPtr<FJsonObject> NaniteObject = MakeShared<FJsonObject>();
694
+ UStaticMeshComponent* StaticMeshComponent = Cast<UStaticMeshComponent>(Component);
695
+ UStaticMesh* StaticMesh = StaticMeshComponent ? StaticMeshComponent->GetStaticMesh() : nullptr;
696
+ const bool bMeshNanite = StaticMesh ? StaticMesh->IsNaniteEnabled() : false;
697
+ const bool bDisallow = StaticMeshComponent ? StaticMeshComponent->IsDisallowNanite() : false;
698
+ const bool bForceDisable = StaticMeshComponent ? StaticMeshComponent->IsForceDisableNanite() : false;
699
+ NaniteObject->SetStringField(TEXT("staticMeshPath"), StaticMesh ? StaticMesh->GetPathName() : FString());
700
+ NaniteObject->SetBoolField(TEXT("meshNaniteEnabled"), bMeshNanite);
701
+ NaniteObject->SetBoolField(TEXT("componentDisallowNanite"), bDisallow);
702
+ NaniteObject->SetBoolField(TEXT("componentForceDisableNanite"), bForceDisable);
703
+ NaniteObject->SetBoolField(TEXT("effectiveNanite"), bMeshNanite && !bDisallow && !bForceDisable);
704
+ Row->SetObjectField(TEXT("nanite"), NaniteObject);
705
+ }
706
+
707
+ // ── #943: navigation relevance ──────────────────────────────────
708
+ if (Fields.bNavigation)
709
+ {
710
+ TSharedPtr<FJsonObject> NavObject = MakeShared<FJsonObject>();
711
+ const bool bCanAffectNav = Component->CanEverAffectNavigation();
712
+ NavObject->SetBoolField(TEXT("canEverAffectNavigation"), bCanAffectNav);
713
+ NavObject->SetStringField(TEXT("mobility"),
714
+ SceneComponent ? MCPQueryMobilityName(SceneComponent->Mobility) : TEXT("None"));
715
+
716
+ ECollisionEnabled::Type CollisionEnabled = ECollisionEnabled::NoCollision;
717
+ if (Primitive)
718
+ {
719
+ CollisionEnabled = Primitive->GetCollisionEnabled();
720
+ NavObject->SetStringField(TEXT("collisionProfile"),
721
+ Primitive->GetCollisionProfileName().ToString());
722
+ NavObject->SetStringField(TEXT("hasCustomNavigableGeometry"),
723
+ MCPQueryNavGeometryName(Primitive->HasCustomNavigableGeometry()));
724
+ }
725
+ NavObject->SetStringField(TEXT("collisionEnabled"), MCPQueryCollisionEnabledName(CollisionEnabled));
726
+
727
+ const bool bCollides =
728
+ CollisionEnabled == ECollisionEnabled::QueryOnly ||
729
+ CollisionEnabled == ECollisionEnabled::QueryAndPhysics ||
730
+ CollisionEnabled == ECollisionEnabled::QueryAndProbe;
731
+
732
+ // NavModifierComponent lives in a module this file does not
733
+ // link, so its AreaClass is read reflectively (#943).
734
+ if (const TSharedPtr<FJsonValue> AreaClass = MCPQueryReflectProperty(Component, TEXT("AreaClass")))
735
+ {
736
+ NavObject->SetField(TEXT("navAreaClass"), AreaClass);
737
+ }
738
+ if (const TSharedPtr<FJsonValue> AreaClassToReplace =
739
+ MCPQueryReflectProperty(Component, TEXT("AreaClassToReplace")))
740
+ {
741
+ NavObject->SetField(TEXT("navAreaClassToReplace"), AreaClassToReplace);
742
+ }
743
+ if (OwnerPawn)
744
+ {
745
+ NavObject->SetBoolField(TEXT("ownerCanAffectNavigationGeneration"),
746
+ OwnerPawn->bCanAffectNavigationGeneration != 0);
747
+ }
748
+
749
+ const EHasCustomNavigableGeometry::Type CustomGeometry = Primitive
750
+ ? Primitive->HasCustomNavigableGeometry()
751
+ : EHasCustomNavigableGeometry::No;
752
+ // EvenIfNotCollidable is the case that makes a raw collision
753
+ // check the wrong answer: the component exports navigation
754
+ // geometry with no collision at all.
755
+ const bool bCustomExports =
756
+ CustomGeometry == EHasCustomNavigableGeometry::Yes ||
757
+ CustomGeometry == EHasCustomNavigableGeometry::EvenIfNotCollidable;
758
+ const bool bEffectiveNav = bCanAffectNav &&
759
+ CustomGeometry != EHasCustomNavigableGeometry::DontExport &&
760
+ (bCollides || bCustomExports);
761
+ NavObject->SetBoolField(TEXT("effectiveNavRelevant"), bEffectiveNav);
762
+ NavObject->SetStringField(TEXT("reason"),
763
+ !bCanAffectNav ? TEXT("bCanEverAffectNavigation is false")
764
+ : bEffectiveNav ? TEXT("navigation relevant")
765
+ : TEXT("no query collision and no custom navigable geometry"));
766
+ Row->SetObjectField(TEXT("navigation"), NavObject);
767
+ }
768
+
769
+ // ── #912: tick settings ─────────────────────────────────────────
770
+ if (Fields.bTick)
771
+ {
772
+ TSharedPtr<FJsonObject> TickObject = MakeShared<FJsonObject>();
773
+ TickObject->SetBoolField(TEXT("componentCanEverTick"), Component->PrimaryComponentTick.bCanEverTick);
774
+ TickObject->SetBoolField(TEXT("componentTickEnabled"), Component->PrimaryComponentTick.IsTickFunctionEnabled());
775
+ TickObject->SetNumberField(TEXT("componentTickInterval"), Component->PrimaryComponentTick.TickInterval);
776
+ TickObject->SetBoolField(TEXT("actorCanEverTick"), Actor->PrimaryActorTick.bCanEverTick);
777
+ TickObject->SetBoolField(TEXT("actorTickEnabled"), Actor->PrimaryActorTick.IsTickFunctionEnabled());
778
+ TickObject->SetNumberField(TEXT("actorTickInterval"), Actor->PrimaryActorTick.TickInterval);
779
+ Row->SetObjectField(TEXT("tick"), TickObject);
780
+ }
781
+
782
+ // ── #912: the rendered asset ────────────────────────────────────
783
+ bool bIsMeshComponent = false;
784
+ FString MeshKind;
785
+ UObject* MeshAsset = MCPQueryMeshAsset(Component, MeshKind, bIsMeshComponent);
786
+ const bool bMissingMesh = bIsMeshComponent && MeshAsset == nullptr;
787
+ if (Fields.bMesh && bIsMeshComponent)
788
+ {
789
+ TSharedPtr<FJsonObject> MeshObject = MakeShared<FJsonObject>();
790
+ MeshObject->SetStringField(TEXT("assetKind"), MeshKind);
791
+ if (MeshAsset)
792
+ {
793
+ MeshObject->SetStringField(TEXT("assetPath"), MeshAsset->GetPathName());
794
+ }
795
+ else
796
+ {
797
+ MeshObject->SetField(TEXT("assetPath"), MakeShared<FJsonValueNull>());
798
+ }
799
+ MeshObject->SetBoolField(TEXT("missing"), bMissingMesh);
800
+ // #986: how many instances an ISM/HISM holds. Without it a
801
+ // component holding three instances and one holding three
802
+ // hundred thousand are the same row, and the decision about
803
+ // whether to touch it is made blind. Note this is a projected
804
+ // value, not an aggregate: groupBy/countBy count COMPONENTS, so
805
+ // use level(summarize_static_mesh_usage) for placement totals.
806
+ if (const UInstancedStaticMeshComponent* ISMC = Cast<UInstancedStaticMeshComponent>(Component))
807
+ {
808
+ MeshObject->SetNumberField(TEXT("instanceCount"), ISMC->GetInstanceCount());
809
+ MeshObject->SetBoolField(TEXT("hierarchical"),
810
+ ISMC->IsA<UHierarchicalInstancedStaticMeshComponent>());
811
+ }
812
+ Row->SetObjectField(TEXT("mesh"), MeshObject);
813
+ }
814
+
815
+ // ── #912: decal material ────────────────────────────────────────
816
+ bool bMissingDecalMaterial = false;
817
+ if (UDecalComponent* Decal = Cast<UDecalComponent>(Component))
818
+ {
819
+ UMaterialInterface* DecalMaterial = Decal->GetDecalMaterial();
820
+ bMissingDecalMaterial = DecalMaterial == nullptr;
821
+ if (Fields.bDecal)
822
+ {
823
+ TSharedPtr<FJsonObject> DecalObject = MakeShared<FJsonObject>();
824
+ if (DecalMaterial)
825
+ {
826
+ DecalObject->SetStringField(TEXT("materialPath"), DecalMaterial->GetPathName());
827
+ }
828
+ else
829
+ {
830
+ DecalObject->SetField(TEXT("materialPath"), MakeShared<FJsonValueNull>());
831
+ }
832
+ DecalObject->SetBoolField(TEXT("missing"), bMissingDecalMaterial);
833
+ Row->SetObjectField(TEXT("decal"), DecalObject);
834
+ }
835
+ }
836
+
837
+ // ── #912: effective material slots ──────────────────────────────
838
+ int32 NullMaterialSlots = 0;
839
+ int32 WorldGridMaterialSlots = 0;
840
+ if (UMeshComponent* MeshComponent = Cast<UMeshComponent>(Component))
841
+ {
842
+ const int32 SlotCount = FMath::Min(MeshComponent->GetNumMaterials(), MCPQueryMaxMaterialSlots);
843
+ const TArray<FName> SlotNames = MeshComponent->GetMaterialSlotNames();
844
+ TArray<TSharedPtr<FJsonValue>> SlotArray;
845
+ for (int32 SlotIndex = 0; SlotIndex < SlotCount; ++SlotIndex)
846
+ {
847
+ UMaterialInterface* Effective = MeshComponent->GetMaterial(SlotIndex);
848
+ // UMeshComponent::GetMaterial consults OverrideMaterials
849
+ // first, so a non-null entry there is exactly what makes
850
+ // this slot an instance override rather than the asset's
851
+ // own assignment.
852
+ const bool bOverride =
853
+ MeshComponent->OverrideMaterials.IsValidIndex(SlotIndex) &&
854
+ MeshComponent->OverrideMaterials[SlotIndex] != nullptr;
855
+ const FString Path = Effective ? Effective->GetPathName() : FString();
856
+ const bool bIsNull = Effective == nullptr;
857
+ const bool bIsWorldGrid = !bIsNull && Path.StartsWith(MCPQueryWorldGridMaterial);
858
+ if (bIsNull) ++NullMaterialSlots;
859
+ if (bIsWorldGrid) ++WorldGridMaterialSlots;
860
+
861
+ if (Fields.bMaterials)
862
+ {
863
+ TSharedPtr<FJsonObject> SlotObject = MakeShared<FJsonObject>();
864
+ SlotObject->SetNumberField(TEXT("slotIndex"), SlotIndex);
865
+ SlotObject->SetStringField(TEXT("slotName"),
866
+ SlotNames.IsValidIndex(SlotIndex) ? SlotNames[SlotIndex].ToString() : FString());
867
+ if (bIsNull)
868
+ {
869
+ SlotObject->SetField(TEXT("materialPath"), MakeShared<FJsonValueNull>());
870
+ }
871
+ else
872
+ {
873
+ SlotObject->SetStringField(TEXT("materialPath"), Path);
874
+ }
875
+ SlotObject->SetStringField(TEXT("source"),
876
+ bOverride ? TEXT("componentOverride") : (bIsNull ? TEXT("none") : TEXT("meshDefault")));
877
+ SlotObject->SetBoolField(TEXT("isNull"), bIsNull);
878
+ SlotObject->SetBoolField(TEXT("isWorldGrid"), bIsWorldGrid);
879
+ SlotArray.Add(MakeShared<FJsonValueObject>(SlotObject));
880
+ }
881
+ }
882
+ if (Fields.bMaterials)
883
+ {
884
+ Row->SetArrayField(TEXT("materials"), SlotArray);
885
+ Row->SetNumberField(TEXT("materialSlotCount"), MeshComponent->GetNumMaterials());
886
+ }
887
+ }
888
+
889
+ // ── #912: the health roll-up ────────────────────────────────────
890
+ const bool bZeroScale = bHasTransform && (
891
+ FMath::IsNearlyZero(WorldScale.X) ||
892
+ FMath::IsNearlyZero(WorldScale.Y) ||
893
+ FMath::IsNearlyZero(WorldScale.Z));
894
+ const bool bNegativeScale = bHasTransform &&
895
+ (WorldScale.X < 0.0 || WorldScale.Y < 0.0 || WorldScale.Z < 0.0);
896
+ if (Fields.bHealth)
897
+ {
898
+ const bool bSuspect =
899
+ bBoundsZero || bBoundsInvalid || bZeroScale || bMissingMesh ||
900
+ bMissingDecalMaterial || NullMaterialSlots > 0 || WorldGridMaterialSlots > 0;
901
+ TSharedPtr<FJsonObject> HealthObject = MakeShared<FJsonObject>();
902
+ HealthObject->SetBoolField(TEXT("boundsZero"), bBoundsZero);
903
+ HealthObject->SetBoolField(TEXT("boundsInvalid"), bBoundsInvalid);
904
+ HealthObject->SetBoolField(TEXT("zeroScale"), bZeroScale);
905
+ HealthObject->SetBoolField(TEXT("negativeScale"), bNegativeScale);
906
+ HealthObject->SetBoolField(TEXT("missingMesh"), bMissingMesh);
907
+ HealthObject->SetBoolField(TEXT("missingDecalMaterial"), bMissingDecalMaterial);
908
+ HealthObject->SetNumberField(TEXT("nullMaterialSlotCount"), NullMaterialSlots);
909
+ HealthObject->SetNumberField(TEXT("worldGridMaterialSlotCount"), WorldGridMaterialSlots);
910
+ HealthObject->SetBoolField(TEXT("suspect"), bSuspect);
911
+ Row->SetObjectField(TEXT("health"), HealthObject);
912
+ }
913
+
914
+ // ── Requested UPROPERTY projection ──────────────────────────────
915
+ if (ProjectedProperties.Num() > 0)
916
+ {
917
+ TSharedPtr<FJsonObject> PropsObject = MakeShared<FJsonObject>();
918
+ for (const FString& PropertyName : ProjectedProperties)
919
+ {
920
+ FProperty* Property = Component->GetClass()->FindPropertyByName(FName(*PropertyName));
921
+ if (!Property)
922
+ {
923
+ continue;
924
+ }
925
+ PropsObject->SetField(PropertyName,
926
+ MCPQuery::PropertyToJson(Property, Property->ContainerPtrToValuePtr<void>(Component)));
927
+ }
928
+ Row->SetObjectField(TEXT("props"), PropsObject);
929
+ }
930
+
931
+ // ── Predicates, evaluated here rather than in the client ────────
932
+ if (!MCPQuery::EvaluateAll(Predicates, Row, WhereMode == TEXT("all")))
933
+ {
934
+ continue;
935
+ }
936
+
937
+ ++Matched;
938
+
939
+ // Grouping and histograms count every match, including the ones
940
+ // past the retention cap, so an aggregate is never a partial
941
+ // answer dressed up as a total.
942
+ if (!GroupBy.IsEmpty())
943
+ {
944
+ const FString Key = MCPQuery::ValueKey(MCPQuery::ResolvePath(Row, GroupBy));
945
+ if (int32* Existing = GroupCounts.Find(Key))
946
+ {
947
+ ++(*Existing);
948
+ TArray<FString>& Samples = GroupSamplesByKey.FindOrAdd(Key);
949
+ if (Samples.Num() < GroupSamples)
950
+ {
951
+ Samples.Add(FString::Printf(TEXT("%s.%s"), *ActorLabel, *ComponentName));
952
+ }
953
+ }
954
+ else if (GroupCounts.Num() < MCPQueryMaxGroups)
955
+ {
956
+ GroupCounts.Add(Key, 1);
957
+ GroupOrder.Add(Key);
958
+ TArray<FString>& Samples = GroupSamplesByKey.FindOrAdd(Key);
959
+ if (Samples.Num() < GroupSamples)
960
+ {
961
+ Samples.Add(FString::Printf(TEXT("%s.%s"), *ActorLabel, *ComponentName));
962
+ }
963
+ }
964
+ else
965
+ {
966
+ bGroupsTruncated = true;
967
+ }
968
+ }
969
+
970
+ for (const FString& CountPath : CountByPaths)
971
+ {
972
+ TMap<FString, int32>& Histogram = CountByHistograms.FindOrAdd(CountPath);
973
+ const FString Key = MCPQuery::ValueKey(MCPQuery::ResolvePath(Row, CountPath));
974
+ if (int32* Existing = Histogram.Find(Key))
975
+ {
976
+ ++(*Existing);
977
+ }
978
+ else if (Histogram.Num() < MCPQueryMaxCountByValues)
979
+ {
980
+ Histogram.Add(Key, 1);
981
+ }
982
+ }
983
+
984
+ FIntVector Quantized = FIntVector::ZeroValue;
985
+ if (DuplicateQuantum > 0.0 && bHasTransform)
986
+ {
987
+ Quantized = FIntVector(
988
+ FMath::RoundToInt(WorldLocation.X / DuplicateQuantum),
989
+ FMath::RoundToInt(WorldLocation.Y / DuplicateQuantum),
990
+ FMath::RoundToInt(WorldLocation.Z / DuplicateQuantum));
991
+ int32& Count = DuplicateCounts.FindOrAdd(Quantized);
992
+ ++Count;
993
+ TArray<FString>& Samples = DuplicateSamples.FindOrAdd(Quantized);
994
+ if (Samples.Num() < FMath::Max(GroupSamples, 2))
995
+ {
996
+ Samples.Add(FString::Printf(TEXT("%s.%s"), *ActorLabel, *ComponentName));
997
+ }
998
+ }
999
+
1000
+ if (!bCountOnly && Rows.Num() < MCPQueryMaxRetainedRows)
1001
+ {
1002
+ FMCPQueryRow NewRow;
1003
+ NewRow.SortKey = FString::Printf(TEXT("%s|%s"), *Actor->GetPathName(), *ComponentName);
1004
+ NewRow.Object = Row;
1005
+ NewRow.QuantizedLocation = Quantized;
1006
+ NewRow.bHasTransform = bHasTransform;
1007
+ Rows.Add(MoveTemp(NewRow));
1008
+ }
1009
+ }
1010
+ }
1011
+
1012
+ // ── Response ────────────────────────────────────────────────────────────
1013
+ const bool bRetentionTruncated = !bCountOnly && Matched > Rows.Num();
1014
+
1015
+ // Deterministic order, so `startIndex` means the same thing on the next call.
1016
+ // TActorIterator order is not a contract.
1017
+ Rows.Sort([](const FMCPQueryRow& A, const FMCPQueryRow& B) { return A.SortKey < B.SortKey; });
1018
+
1019
+ TArray<TSharedPtr<FJsonValue>> RowArray;
1020
+ if (!bCountOnly)
1021
+ {
1022
+ const int32 First = FMath::Min(StartIndex, Rows.Num());
1023
+ const int32 Last = FMath::Min(First + Limit, Rows.Num());
1024
+ for (int32 Index = First; Index < Last; ++Index)
1025
+ {
1026
+ FMCPQueryRow& Row = Rows[Index];
1027
+ if (DuplicateQuantum > 0.0 && Row.bHasTransform)
1028
+ {
1029
+ const int32* Count = DuplicateCounts.Find(Row.QuantizedLocation);
1030
+ const bool bDuplicate = Count && *Count > 1;
1031
+ if (Row.Object->HasTypedField<EJson::Object>(TEXT("health")))
1032
+ {
1033
+ Row.Object->GetObjectField(TEXT("health"))->SetBoolField(TEXT("duplicateTransform"), bDuplicate);
1034
+ }
1035
+ else
1036
+ {
1037
+ Row.Object->SetBoolField(TEXT("duplicateTransform"), bDuplicate);
1038
+ }
1039
+ }
1040
+ RowArray.Add(MakeShared<FJsonValueObject>(Row.Object));
1041
+ }
1042
+ }
1043
+
1044
+ auto Result = MCPSuccess();
1045
+ Result->SetStringField(TEXT("worldName"), World->GetName());
1046
+ Result->SetStringField(TEXT("levelPath"), MCPEditorState::CurrentEditorLevelPackageName());
1047
+ Result->SetNumberField(TEXT("scannedActors"), ScannedActors);
1048
+ Result->SetNumberField(TEXT("scannedComponents"), ScannedComponents);
1049
+ Result->SetNumberField(TEXT("matched"), Matched);
1050
+ Result->SetNumberField(TEXT("returned"), RowArray.Num());
1051
+ Result->SetNumberField(TEXT("startIndex"), StartIndex);
1052
+ Result->SetNumberField(TEXT("limit"), Limit);
1053
+ Result->SetBoolField(TEXT("countOnly"), bCountOnly);
1054
+ Result->SetBoolField(TEXT("retentionTruncated"), bRetentionTruncated);
1055
+ if (bRetentionTruncated)
1056
+ {
1057
+ Result->SetStringField(TEXT("retentionNote"), FString::Printf(
1058
+ TEXT("Only the first %d matches were kept for paging; 'matched' is exact. Narrow the filters or use countOnly/groupBy."),
1059
+ MCPQueryMaxRetainedRows));
1060
+ }
1061
+ const int32 NextStartIndex = StartIndex + RowArray.Num();
1062
+ Result->SetBoolField(TEXT("hasMore"), !bCountOnly && NextStartIndex < Rows.Num());
1063
+ if (!bCountOnly && NextStartIndex < Rows.Num())
1064
+ {
1065
+ Result->SetNumberField(TEXT("nextStartIndex"), NextStartIndex);
1066
+ }
1067
+ if (!bCountOnly)
1068
+ {
1069
+ Result->SetArrayField(TEXT("rows"), RowArray);
1070
+ }
1071
+
1072
+ if (!GroupBy.IsEmpty())
1073
+ {
1074
+ GroupOrder.Sort([&GroupCounts](const FString& A, const FString& B)
1075
+ {
1076
+ const int32 CountA = GroupCounts.FindChecked(A);
1077
+ const int32 CountB = GroupCounts.FindChecked(B);
1078
+ return CountA != CountB ? CountA > CountB : A < B;
1079
+ });
1080
+ TArray<TSharedPtr<FJsonValue>> GroupArray;
1081
+ for (const FString& Key : GroupOrder)
1082
+ {
1083
+ TSharedPtr<FJsonObject> GroupObject = MakeShared<FJsonObject>();
1084
+ GroupObject->SetStringField(TEXT("key"), Key);
1085
+ GroupObject->SetNumberField(TEXT("count"), GroupCounts.FindChecked(Key));
1086
+ if (const TArray<FString>* Samples = GroupSamplesByKey.Find(Key))
1087
+ {
1088
+ GroupObject->SetArrayField(TEXT("samples"), MCPStringListToJson(*Samples));
1089
+ }
1090
+ GroupArray.Add(MakeShared<FJsonValueObject>(GroupObject));
1091
+ }
1092
+ Result->SetStringField(TEXT("groupBy"), GroupBy);
1093
+ Result->SetArrayField(TEXT("groups"), GroupArray);
1094
+ Result->SetBoolField(TEXT("groupsTruncated"), bGroupsTruncated);
1095
+ }
1096
+
1097
+ if (CountByPaths.Num() > 0)
1098
+ {
1099
+ TSharedPtr<FJsonObject> CountsObject = MakeShared<FJsonObject>();
1100
+ for (const FString& CountPath : CountByPaths)
1101
+ {
1102
+ TSharedPtr<FJsonObject> Histogram = MakeShared<FJsonObject>();
1103
+ if (const TMap<FString, int32>* Values = CountByHistograms.Find(CountPath))
1104
+ {
1105
+ for (const TPair<FString, int32>& Pair : *Values)
1106
+ {
1107
+ Histogram->SetNumberField(Pair.Key, Pair.Value);
1108
+ }
1109
+ }
1110
+ CountsObject->SetObjectField(CountPath, Histogram);
1111
+ }
1112
+ Result->SetObjectField(TEXT("counts"), CountsObject);
1113
+ }
1114
+
1115
+ if (DuplicateQuantum > 0.0)
1116
+ {
1117
+ TArray<TSharedPtr<FJsonValue>> DuplicateArray;
1118
+ int32 DuplicateComponentCount = 0;
1119
+ for (const TPair<FIntVector, int32>& Pair : DuplicateCounts)
1120
+ {
1121
+ if (Pair.Value < 2)
1122
+ {
1123
+ continue;
1124
+ }
1125
+ DuplicateComponentCount += Pair.Value;
1126
+ if (DuplicateArray.Num() >= MCPQueryMaxDuplicateGroups)
1127
+ {
1128
+ continue;
1129
+ }
1130
+ TSharedPtr<FJsonObject> DuplicateObject = MakeShared<FJsonObject>();
1131
+ DuplicateObject->SetObjectField(TEXT("location"), MCPQueryVec(FVector(
1132
+ Pair.Key.X * DuplicateQuantum,
1133
+ Pair.Key.Y * DuplicateQuantum,
1134
+ Pair.Key.Z * DuplicateQuantum)));
1135
+ DuplicateObject->SetNumberField(TEXT("count"), Pair.Value);
1136
+ if (const TArray<FString>* Samples = DuplicateSamples.Find(Pair.Key))
1137
+ {
1138
+ DuplicateObject->SetArrayField(TEXT("samples"), MCPStringListToJson(*Samples));
1139
+ }
1140
+ DuplicateArray.Add(MakeShared<FJsonValueObject>(DuplicateObject));
1141
+ }
1142
+ Result->SetNumberField(TEXT("duplicateTransformTolerance"), DuplicateQuantum);
1143
+ Result->SetNumberField(TEXT("duplicateTransformComponents"), DuplicateComponentCount);
1144
+ Result->SetArrayField(TEXT("duplicateTransformGroups"), DuplicateArray);
1145
+ }
1146
+
1147
+ // The read half of the #912 contract, asserted rather than assumed: if
1148
+ // anything about this call dirtied a package, say so instead of leaving a
1149
+ // map-wide audit as an unexplained unsaved-changes marker.
1150
+ TArray<FString> DirtyAfterQuery;
1151
+ MCPEditorState::CollectDirtyEditorPackageNames(DirtyAfterQuery);
1152
+ for (const FString& Already : InitialDirtyPackages)
1153
+ {
1154
+ DirtyAfterQuery.Remove(Already);
1155
+ }
1156
+ Result->SetBoolField(TEXT("dirtiedPackages"), DirtyAfterQuery.Num() > 0);
1157
+ if (DirtyAfterQuery.Num() > 0)
1158
+ {
1159
+ Result->SetArrayField(TEXT("dirtyPackages"), MCPStringListToJson(DirtyAfterQuery));
1160
+ }
1161
+
1162
+ if (!RestoreLevelPath.IsEmpty())
1163
+ {
1164
+ Result->SetStringField(TEXT("restoredLevelPath"), RestoreLevelPath);
1165
+ }
1166
+ RestoreLevel();
1167
+
1168
+ return MCPResult(Result);
1169
+ }