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
@@ -28,6 +28,7 @@
28
28
  #include "Framework/Docking/TabManager.h"
29
29
  #include "Widgets/Docking/SDockTab.h"
30
30
  #include "ISettingsModule.h"
31
+ #include "Interfaces/IMainFrameModule.h"
31
32
  #include "Modules/ModuleManager.h"
32
33
  #include "Misc/ConfigCacheIni.h"
33
34
  #include "Misc/ConfigContext.h"
@@ -41,6 +42,7 @@
41
42
  #include "RenderingThread.h"
42
43
  #include "Misc/AutomationTest.h"
43
44
  #include "HAL/PlatformProcess.h"
45
+ #include "HAL/PlatformTime.h"
44
46
  #include "Slate/SceneViewport.h"
45
47
  #include "HAL/PlatformMemory.h"
46
48
  #include "Misc/App.h"
@@ -246,6 +248,7 @@ void FEditorHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
246
248
  // order in BridgeServer.cpp, which is not a contract.
247
249
  Registry.RegisterHandler(TEXT("save_dirty"), &SaveDirty);
248
250
  Registry.RegisterHandler(TEXT("list_dirty_packages"), &ListDirtyPackages);
251
+ Registry.RegisterHandler(TEXT("get_world_state"), &GetWorldState);
249
252
  Registry.RegisterHandler(TEXT("request_editor_shutdown"), &RequestEditorShutdown);
250
253
  Registry.RegisterHandler(TEXT("list_pie_instances"), &ListPIEInstances);
251
254
  Registry.RegisterHandler(TEXT("invoke_object_function"), &InvokeObjectFunction);
@@ -254,6 +257,8 @@ void FEditorHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
254
257
  Registry.RegisterHandler(TEXT("read_bone_transforms"), &ReadBoneTransforms);
255
258
  Registry.RegisterHandler(TEXT("teleport_runtime_actor"), &TeleportRuntimeActor);
256
259
  Registry.RegisterHandler(TEXT("set_movement_mode"), &SetMovementMode);
260
+ Registry.RegisterHandler(TEXT("set_runtime_visibility"), &SetRuntimeVisibility);
261
+ Registry.RegisterHandler(TEXT("restore_runtime_visibility"), &RestoreRuntimeVisibility);
257
262
  // #802: resolve a live instance path, and write to a live instance.
258
263
  Registry.RegisterHandler(TEXT("find_object"), &FindLiveObjects);
259
264
  Registry.RegisterHandler(TEXT("set_object_property"), &SetObjectProperty);
@@ -1163,7 +1168,11 @@ TSharedPtr<FJsonValue> FEditorHandlers::HitTestViewportPixel(const TSharedPtr<FJ
1163
1168
  {
1164
1169
  FString Label;
1165
1170
  if (!V->TryGetString(Label)) continue;
1166
- if (AActor* A = FindActorByLabel(World, Label)) Query.AddIgnoredActor(A);
1171
+ // #983: an ignore list is the plural case, so a label naming
1172
+ // several actors ignores all of them.
1173
+ TArray<AActor*> Matches;
1174
+ MCPCollectActorsByToken(World, Label, EMCPActorMatch::LabelNameOrPath, Matches);
1175
+ for (AActor* A : Matches) Query.AddIgnoredActor(A);
1167
1176
  }
1168
1177
  }
1169
1178
 
@@ -1190,6 +1199,7 @@ TSharedPtr<FJsonValue> FEditorHandlers::HitTestViewportPixel(const TSharedPtr<FJ
1190
1199
  AActor* HitActor = Hit.GetActor();
1191
1200
  UPrimitiveComponent* HitComp = Hit.GetComponent();
1192
1201
  if (HitActor) Result->SetStringField(TEXT("actorLabel"), HitActor->GetActorLabel());
1202
+ if (HitActor) Result->SetStringField(TEXT("actorPath"), HitActor->GetPathName());
1193
1203
  if (HitActor) Result->SetStringField(TEXT("actorClass"), HitActor->GetClass()->GetName());
1194
1204
  if (HitComp)
1195
1205
  {
@@ -1433,8 +1443,18 @@ TSharedPtr<FJsonValue> FEditorHandlers::GetBuildStatus(const TSharedPtr<FJsonObj
1433
1443
  }
1434
1444
  TSharedPtr<FJsonValue> FEditorHandlers::CaptureScreenshot(const TSharedPtr<FJsonObject>& Params)
1435
1445
  {
1436
- FString Filename;
1437
- if (auto Err = RequireString(Params, TEXT("filename"), Filename)) return Err;
1446
+ // #966: capture_scene_png names this parameter `outputPath` and this action
1447
+ // named it `filename`, for the same thing, so passing one to the other
1448
+ // failed on a call that was otherwise correct. Both are accepted by both.
1449
+ FString Filename = OptionalString(Params, TEXT("filename"));
1450
+ if (Filename.IsEmpty())
1451
+ {
1452
+ Filename = OptionalString(Params, TEXT("outputPath"));
1453
+ }
1454
+ if (Filename.IsEmpty())
1455
+ {
1456
+ return MCPError(TEXT("Missing 'filename' (also accepted as 'outputPath'): where to write the image"));
1457
+ }
1438
1458
 
1439
1459
  // Ensure the filename has a proper extension
1440
1460
  if (!Filename.EndsWith(TEXT(".png")) && !Filename.EndsWith(TEXT(".jpg")) && !Filename.EndsWith(TEXT(".bmp")))
@@ -1899,6 +1919,71 @@ TSharedPtr<FJsonValue> FEditorHandlers::ListDirtyPackages(const TSharedPtr<FJson
1899
1919
  return MCPResult(Result);
1900
1920
  }
1901
1921
 
1922
+ // #920 / #921: one read that answers "which world is open, and what is unsaved"
1923
+ // together. level(get_current) and list_dirty_packages are two calls, so the
1924
+ // editor can change between them and neither answer proves which world the
1925
+ // other described. Both reports were filed independently by the same team,
1926
+ // which is a fair signal that two calls is genuinely not good enough for a QA
1927
+ // gate. This runs in one game-thread dispatch, so the pair is consistent.
1928
+ TSharedPtr<FJsonValue> FEditorHandlers::GetWorldState(const TSharedPtr<FJsonObject>& Params)
1929
+ {
1930
+ if (!GEditor)
1931
+ {
1932
+ return MCPError(TEXT("Editor not available"));
1933
+ }
1934
+
1935
+ UWorld* EditorWorld = GEditor->GetEditorWorldContext().World();
1936
+ if (!EditorWorld)
1937
+ {
1938
+ // Fail closed. An empty answer here would read as "nothing is open and
1939
+ // nothing is dirty", which is the reassuring shape of a wrong answer.
1940
+ return MCPError(TEXT("No editor world is currently resolvable, so world state cannot be reported"));
1941
+ }
1942
+
1943
+ auto Result = MCPSuccess();
1944
+
1945
+ UPackage* WorldPackage = EditorWorld->GetOutermost();
1946
+ Result->SetStringField(TEXT("editorWorldName"), EditorWorld->GetName());
1947
+ Result->SetStringField(TEXT("editorWorldPackage"), WorldPackage ? WorldPackage->GetName() : FString());
1948
+ Result->SetBoolField(TEXT("worldPackageDirty"), WorldPackage != nullptr && WorldPackage->IsDirty());
1949
+
1950
+ if (ULevel* Persistent = EditorWorld->PersistentLevel)
1951
+ {
1952
+ if (UPackage* LevelPackage = Persistent->GetOutermost())
1953
+ {
1954
+ Result->SetStringField(TEXT("persistentLevelPackage"), LevelPackage->GetName());
1955
+ }
1956
+ }
1957
+
1958
+ // PIE and SIE are distinct states and a caller acting on the wrong one gets
1959
+ // a confidently wrong result, so report both rather than one "inPIE" flag.
1960
+ const bool bSimulating = GEditor->bIsSimulatingInEditor;
1961
+ const bool bPlayWorld = GEditor->PlayWorld != nullptr;
1962
+ Result->SetBoolField(TEXT("playInEditor"), bPlayWorld && !bSimulating);
1963
+ Result->SetBoolField(TEXT("simulateInEditor"), bSimulating);
1964
+ Result->SetStringField(TEXT("mode"), bSimulating ? TEXT("simulate") : (bPlayWorld ? TEXT("play") : TEXT("editor")));
1965
+
1966
+ const FDirtyEditorPackages Dirty = CollectDirtyEditorPackages();
1967
+ TArray<FString> All;
1968
+ All.Reserve(Dirty.Content.Num() + Dirty.Maps.Num());
1969
+ All.Append(Dirty.Content);
1970
+ All.Append(Dirty.Maps);
1971
+ All.Sort();
1972
+
1973
+ TArray<TSharedPtr<FJsonValue>> DirtyJson;
1974
+ DirtyJson.Reserve(All.Num());
1975
+ for (const FString& PackageName : All)
1976
+ {
1977
+ DirtyJson.Add(MakeShared<FJsonValueString>(PackageName));
1978
+ }
1979
+ Result->SetArrayField(TEXT("dirtyPackages"), DirtyJson);
1980
+ Result->SetNumberField(TEXT("dirtyPackageCount"), All.Num());
1981
+ Result->SetNumberField(TEXT("dirtyContentCount"), Dirty.Content.Num());
1982
+ Result->SetNumberField(TEXT("dirtyMapCount"), Dirty.Maps.Num());
1983
+
1984
+ return MCPResult(Result);
1985
+ }
1986
+
1902
1987
  TSharedPtr<FJsonValue> FEditorHandlers::RequestEditorShutdown(const TSharedPtr<FJsonObject>& Params)
1903
1988
  {
1904
1989
  if (!GEditor)
@@ -1956,7 +2041,8 @@ TSharedPtr<FJsonValue> FEditorHandlers::RequestEditorShutdown(const TSharedPtr<F
1956
2041
  {
1957
2042
  return true;
1958
2043
  }
1959
- UKismetSystemLibrary::QuitEditor();
2044
+ IMainFrameModule& MainFrameModule = FModuleManager::LoadModuleChecked<IMainFrameModule>(TEXT("MainFrame"));
2045
+ MainFrameModule.RequestCloseEditor();
1960
2046
  return false;
1961
2047
  }),
1962
2048
  1.0f);
@@ -1969,15 +2055,14 @@ TSharedPtr<FJsonValue> FEditorHandlers::RequestEditorShutdown(const TSharedPtr<F
1969
2055
  TSharedPtr<FJsonValue> FEditorHandlers::FocusViewportOnActor(const TSharedPtr<FJsonObject>& Params)
1970
2056
  {
1971
2057
  FString ActorLabel;
1972
- if (auto Err = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return Err;
2058
+ if (auto Err = RequireStringAlt(Params, TEXT("actorLabel"), TEXT("actorPath"), ActorLabel)) return Err;
1973
2059
 
1974
2060
  REQUIRE_EDITOR_WORLD(World);
1975
2061
 
1976
- AActor* TargetActor = FindActorByLabel(World, ActorLabel);
1977
- if (!TargetActor)
1978
- {
1979
- return MCPError(FString::Printf(TEXT("Actor '%s' not found"), *ActorLabel));
1980
- }
2062
+ TSharedPtr<FJsonValue> ActorErr;
2063
+ AActor* TargetActor = MCPResolveActor(World, Params, ActorErr);
2064
+ if (!TargetActor) return ActorErr;
2065
+ ActorLabel = TargetActor->GetActorLabel();
1981
2066
 
1982
2067
  // Get the viewport client
1983
2068
  FLevelEditorViewportClient* ViewportClient = GCurrentLevelEditingViewportClient;
@@ -2020,6 +2105,7 @@ TSharedPtr<FJsonValue> FEditorHandlers::FocusViewportOnActor(const TSharedPtr<FJ
2020
2105
  auto Result = MCPSuccess();
2021
2106
  Result->SetObjectField(TEXT("viewLocation"), LocObj);
2022
2107
  Result->SetStringField(TEXT("actorLabel"), ActorLabel);
2108
+ Result->SetStringField(TEXT("actorPath"), TargetActor->GetPathName());
2023
2109
  return MCPResult(Result);
2024
2110
  }
2025
2111
  TSharedPtr<FJsonValue> FEditorHandlers::CreateNewLevel(const TSharedPtr<FJsonObject>& Params)
@@ -2450,8 +2536,18 @@ TSharedPtr<FJsonValue> FEditorHandlers::CaptureScenePng(const TSharedPtr<FJsonOb
2450
2536
  UWorld* World = ResolveWorldFromParams(Params, *WorldScope);
2451
2537
  if (!World) return MCPError(FString::Printf(TEXT("World not available for scope '%s'"), *WorldScope));
2452
2538
 
2453
- FString OutputPath;
2454
- if (auto Err = RequireString(Params, TEXT("outputPath"), OutputPath)) return Err;
2539
+ // #966: capture_screenshot names this parameter `filename` and this action
2540
+ // named it `outputPath`, for the same thing, so passing one to the other
2541
+ // failed on a call that was otherwise correct. Both are accepted by both.
2542
+ FString OutputPath = OptionalString(Params, TEXT("outputPath"));
2543
+ if (OutputPath.IsEmpty())
2544
+ {
2545
+ OutputPath = OptionalString(Params, TEXT("filename"));
2546
+ }
2547
+ if (OutputPath.IsEmpty())
2548
+ {
2549
+ return MCPError(TEXT("Missing 'outputPath' (also accepted as 'filename'): where to write the PNG"));
2550
+ }
2455
2551
 
2456
2552
  // Resolution
2457
2553
  int32 Width = OptionalInt(Params, TEXT("width"), 1280);
@@ -2467,10 +2563,18 @@ TSharedPtr<FJsonValue> FEditorHandlers::CaptureScenePng(const TSharedPtr<FJsonOb
2467
2563
  // #599: focusActorLabel frames the capture on a specific actor by computing
2468
2564
  // a camera position from the actor's bounds and looking at its center.
2469
2565
  const FString FocusLabel = OptionalString(Params, TEXT("focusActorLabel"));
2470
- if (!FocusLabel.IsEmpty())
2471
- {
2472
- AActor* Focus = FindActorByLabelNameOrPath(World, FocusLabel);
2473
- if (!Focus) return MCPError(FString::Printf(TEXT("focusActorLabel not found: %s"), *FocusLabel));
2566
+ const FString FocusPath = OptionalString(Params, TEXT("focusActorPath"));
2567
+ if (!FocusLabel.IsEmpty() || !FocusPath.IsEmpty())
2568
+ {
2569
+ // #983: framing on the wrong copy of a duplicated label is a capture
2570
+ // that quietly shows somewhere else entirely.
2571
+ FMCPActorSelector FocusSel;
2572
+ FocusSel.LabelKey = TEXT("focusActorLabel");
2573
+ FocusSel.PathKey = TEXT("focusActorPath");
2574
+ FocusSel.Match = EMCPActorMatch::LabelNameOrPath;
2575
+ TSharedPtr<FJsonValue> FocusErr;
2576
+ AActor* Focus = MCPResolveActor(World, Params, FocusErr, FocusSel);
2577
+ if (!Focus) return FocusErr;
2474
2578
  FVector Origin, Extent;
2475
2579
  Focus->GetActorBounds(false, Origin, Extent);
2476
2580
  const double Radius = FMath::Max(Extent.Size(), 50.0);
@@ -2483,27 +2587,69 @@ TSharedPtr<FJsonValue> FEditorHandlers::CaptureScenePng(const TSharedPtr<FJsonOb
2483
2587
  Rotation = (Origin - Location).Rotation();
2484
2588
  }
2485
2589
 
2486
- // Find or spawn the reusable capture actor.
2590
+ // #966: the capture actor used to be spawned once and LEFT IN THE LEVEL.
2591
+ // It is a level actor, so it was saved into the map and committed to source
2592
+ // control as though somebody had authored it, and list_dirty_packages
2593
+ // reported nothing while it sat there, so the usual "did I change
2594
+ // anything" check missed it entirely. A capture must leave the level
2595
+ // exactly as it found it.
2596
+ //
2597
+ // Two halves. First, sweep any debris earlier builds left behind, so a
2598
+ // project that already has one is cleaned by the next capture rather than
2599
+ // by hand.
2487
2600
  static const FString CaptureLabel = TEXT("__ClaudeSceneCapture");
2488
- ASceneCapture2D* CaptureActor = nullptr;
2489
- for (TActorIterator<ASceneCapture2D> It(World); It; ++It)
2601
+ int32 RemovedStrayCaptures = 0;
2490
2602
  {
2491
- if (It->GetActorLabel() == CaptureLabel)
2603
+ TArray<ASceneCapture2D*> Strays;
2604
+ for (TActorIterator<ASceneCapture2D> It(World); It; ++It)
2492
2605
  {
2493
- CaptureActor = *It;
2494
- break;
2606
+ if (It->GetActorLabel() == CaptureLabel)
2607
+ {
2608
+ Strays.Add(*It);
2609
+ }
2610
+ }
2611
+ for (ASceneCapture2D* Stray : Strays)
2612
+ {
2613
+ // These ones ARE in the map, so their removal is a real edit and
2614
+ // goes through Modify(): the level has to become dirty, or the
2615
+ // debris comes straight back on the next load.
2616
+ if (IsValid(Stray) && World->DestroyActor(Stray))
2617
+ {
2618
+ ++RemovedStrayCaptures;
2619
+ }
2495
2620
  }
2496
2621
  }
2497
- if (!CaptureActor)
2622
+
2623
+ // Second, this capture's own actor: transient, outside the scene outliner,
2624
+ // in no actor package of its own, and destroyed on every exit from here.
2625
+ // The transient flag alone is not enough, because the debris that prompted
2626
+ // this was visible in the outliner and in the map's actor list either way.
2627
+ FActorSpawnParameters SpawnParams;
2628
+ SpawnParams.ObjectFlags |= RF_Transient;
2629
+ #if WITH_EDITOR
2630
+ SpawnParams.bTemporaryEditorActor = true;
2631
+ SpawnParams.bHideFromSceneOutliner = true;
2632
+ SpawnParams.bCreateActorPackage = false;
2633
+ #endif
2634
+ ASceneCapture2D* CaptureActor = World->SpawnActor<ASceneCapture2D>(ASceneCapture2D::StaticClass(), Location, Rotation, SpawnParams);
2635
+ if (!CaptureActor) return MCPError(TEXT("Failed to spawn SceneCapture2D actor"));
2636
+ CaptureActor->SetActorHiddenInGame(true);
2637
+
2638
+ ON_SCOPE_EXIT
2498
2639
  {
2499
- FActorSpawnParameters SpawnParams;
2500
- SpawnParams.ObjectFlags |= RF_Transient;
2501
- CaptureActor = World->SpawnActor<ASceneCapture2D>(ASceneCapture2D::StaticClass(), Location, Rotation, SpawnParams);
2502
- if (!CaptureActor) return MCPError(TEXT("Failed to spawn SceneCapture2D actor"));
2503
- CaptureActor->SetActorLabel(CaptureLabel);
2504
- CaptureActor->SetActorHiddenInGame(true);
2505
- }
2506
- CaptureActor->SetActorLocationAndRotation(Location, Rotation);
2640
+ if (IsValid(CaptureActor))
2641
+ {
2642
+ // Drop the render target first: the component holds the only
2643
+ // reference keeping it alive past this call.
2644
+ if (USceneCaptureComponent2D* Dying = CaptureActor->GetCaptureComponent2D())
2645
+ {
2646
+ Dying->TextureTarget = nullptr;
2647
+ }
2648
+ // bShouldModifyLevel=false: this actor was never part of the map,
2649
+ // so removing it is not an edit and must not dirty the package.
2650
+ World->DestroyActor(CaptureActor, /*bNetForce*/ false, /*bShouldModifyLevel*/ false);
2651
+ }
2652
+ };
2507
2653
 
2508
2654
  USceneCaptureComponent2D* Comp = CaptureActor->GetCaptureComponent2D();
2509
2655
  if (!Comp) return MCPError(TEXT("SceneCapture2D has no capture component"));
@@ -2557,7 +2703,13 @@ TSharedPtr<FJsonValue> FEditorHandlers::CaptureScenePng(const TSharedPtr<FJsonOb
2557
2703
  Result->SetNumberField(TEXT("width"), Width);
2558
2704
  Result->SetNumberField(TEXT("height"), Height);
2559
2705
  Result->SetNumberField(TEXT("sizeBytes"), (double)Size);
2560
- Result->SetStringField(TEXT("actorLabel"), CaptureLabel);
2706
+ // The capture actor is gone by the time this reaches the caller, so say so
2707
+ // rather than naming an actor they could go and look for.
2708
+ Result->SetBoolField(TEXT("captureActorRemoved"), true);
2709
+ if (RemovedStrayCaptures > 0)
2710
+ {
2711
+ Result->SetNumberField(TEXT("strayCaptureActorsRemoved"), RemovedStrayCaptures);
2712
+ }
2561
2713
  return MCPResult(Result);
2562
2714
  }
2563
2715
 
@@ -2625,12 +2777,27 @@ TSharedPtr<FJsonValue> FEditorHandlers::ListFunctionLibraries(const TSharedPtr<F
2625
2777
  // Headlessly run registered Automation tests whose name matches a filter and
2626
2778
  // report pass/fail + errors. Uses FAutomationTestFramework's synchronous
2627
2779
  // StartTestByName/StopTest, which fully runs non-latent tests. Latent tests
2628
- // (that enqueue cross-frame commands) have their queued commands flushed on a
2629
- // best-effort tick loop with a timeout.
2780
+ // (that enqueue cross-frame commands) get a bounded drain, and one that has not
2781
+ // finished when the budget runs out is abandoned and reported, never stopped
2782
+ // mid-queue.
2783
+ //
2784
+ // #993: StopTest asserts on LatentCommands.IsEmpty() inside InternalStopTest,
2785
+ // and this handler used to call it unconditionally after a drain loop that gave
2786
+ // up after a fixed iteration count. A CQTest test that starts multi-client PIE
2787
+ // still had commands queued at that point, so the assert fired and took the
2788
+ // editor down: the caller saw a WebSocket close 1006, not a test result. A
2789
+ // handler must never take the editor down.
2630
2790
  TSharedPtr<FJsonValue> FEditorHandlers::RunAutomationTests(const TSharedPtr<FJsonObject>& Params)
2631
2791
  {
2632
2792
  const FString NameFilter = OptionalString(Params, TEXT("filter"));
2633
2793
  const int32 MaxTests = OptionalInt(Params, TEXT("maxTests"), 50);
2794
+ // How long one test's latent queue may take. The handler occupies the game
2795
+ // thread for its whole run, so this is deliberately small by default: a
2796
+ // latent command that needs real frames (PIE, anything waiting on a world
2797
+ // tick) cannot make progress from in here however long it is given, and
2798
+ // waiting longer only holds the editor still for longer.
2799
+ const double LatentBudgetSeconds = FMath::Clamp(
2800
+ OptionalNumber(Params, TEXT("latentTimeoutSeconds"), 5.0), 0.0, 120.0);
2634
2801
 
2635
2802
  // #765: this handler runs tests SYNCHRONOUSLY via StartTestByName inside a
2636
2803
  // single tick, so it never depended on the interactive-frame-rate gate that
@@ -2663,7 +2830,7 @@ TSharedPtr<FJsonValue> FEditorHandlers::RunAutomationTests(const TSharedPtr<FJso
2663
2830
  Framework.GetValidTestNames(AllTests);
2664
2831
 
2665
2832
  TArray<TSharedPtr<FJsonValue>> Results;
2666
- int32 Ran = 0, Passed = 0, Failed = 0;
2833
+ int32 Ran = 0, Passed = 0, Failed = 0, Abandoned = 0;
2667
2834
  for (const FAutomationTestInfo& Info : AllTests)
2668
2835
  {
2669
2836
  const FString TestName = Info.GetTestName();
@@ -2677,21 +2844,57 @@ TSharedPtr<FJsonValue> FEditorHandlers::RunAutomationTests(const TSharedPtr<FJso
2677
2844
 
2678
2845
  Framework.StartTestByName(TestName, /*RoleIndex*/ 0);
2679
2846
 
2680
- // Flush latent commands (best effort, bounded) so latent tests finish.
2681
- int32 Guard = 0;
2682
- while (!Framework.ExecuteLatentCommands() && Guard++ < 1000)
2847
+ // Drain the latent queue, bounded by wall clock rather than by a fixed
2848
+ // iteration count. The old bound was 1000 iterations of a zero-length
2849
+ // sleep, which elapses in no time at all and gave a command that polls
2850
+ // for a condition no real chance to reach one. Network commands are
2851
+ // pumped alongside, because the framework's own per-frame path does and
2852
+ // a latent queue behind an undelivered network command never empties.
2853
+ const double DrainStartedAt = FPlatformTime::Seconds();
2854
+ while (!Framework.ExecuteLatentCommands()
2855
+ && (FPlatformTime::Seconds() - DrainStartedAt) < LatentBudgetSeconds)
2683
2856
  {
2684
- FPlatformProcess::Sleep(0.0f);
2857
+ Framework.ExecuteNetworkCommands();
2858
+ FPlatformProcess::Sleep(0.001f);
2859
+ }
2860
+
2861
+ // #993: StopTest asserts that the latent queue is empty. Anything still
2862
+ // queued here needs frames this handler cannot give it, because the
2863
+ // handler owns the game thread for its whole run and the engine loop
2864
+ // only advances once it returns. Clear the queue and record the test as
2865
+ // abandoned; calling StopTest with commands outstanding is what took
2866
+ // the editor down.
2867
+ const bool bAbandoned = !Framework.IsLatentCommandQueueEmpty();
2868
+ if (bAbandoned)
2869
+ {
2870
+ Framework.DequeueAllCommands();
2685
2871
  }
2686
2872
 
2687
2873
  FAutomationTestExecutionInfo ExecInfo;
2688
- const bool bPassed = Framework.StopTest(ExecInfo);
2874
+ const bool bStopReportedPass = Framework.StopTest(ExecInfo);
2875
+ const bool bPassed = bStopReportedPass && !bAbandoned;
2689
2876
  ++Ran;
2690
- if (bPassed) ++Passed; else ++Failed;
2877
+ if (bAbandoned) ++Abandoned;
2878
+ else if (bPassed) ++Passed;
2879
+ else ++Failed;
2880
+
2881
+ // An abandoned test may have left PIE running, and leaving it up would
2882
+ // silently change what every later call in this session sees.
2883
+ if (bAbandoned && GEditor && (GEditor->PlayWorld != nullptr || GEditor->bIsSimulatingInEditor))
2884
+ {
2885
+ GEditor->RequestEndPlayMap();
2886
+ }
2691
2887
 
2692
2888
  TSharedPtr<FJsonObject> R = MakeShared<FJsonObject>();
2693
2889
  R->SetStringField(TEXT("test"), DisplayName.IsEmpty() ? TestName : DisplayName);
2694
2890
  R->SetBoolField(TEXT("passed"), bPassed);
2891
+ R->SetBoolField(TEXT("abandoned"), bAbandoned);
2892
+ if (bAbandoned)
2893
+ {
2894
+ R->SetStringField(TEXT("abandonedReason"), FString::Printf(
2895
+ TEXT("Latent commands were still queued after %.1fs. They need engine frames, which cannot happen while this handler holds the game thread, so the test was abandoned rather than stopped mid-queue (stopping mid-queue asserts and terminates the editor). Run this one from the editor's Automation window, or with -ExecCmds=\"Automation RunTests %s\" at launch."),
2896
+ LatentBudgetSeconds, *TestName));
2897
+ }
2695
2898
  R->SetNumberField(TEXT("errors"), ExecInfo.GetErrorTotal());
2696
2899
  R->SetNumberField(TEXT("warnings"), ExecInfo.GetWarningTotal());
2697
2900
  if (ExecInfo.GetErrorTotal() > 0)
@@ -2715,6 +2918,14 @@ TSharedPtr<FJsonValue> FEditorHandlers::RunAutomationTests(const TSharedPtr<FJso
2715
2918
  Result->SetNumberField(TEXT("ran"), Ran);
2716
2919
  Result->SetNumberField(TEXT("passed"), Passed);
2717
2920
  Result->SetNumberField(TEXT("failed"), Failed);
2921
+ Result->SetNumberField(TEXT("abandoned"), Abandoned);
2922
+ Result->SetNumberField(TEXT("latentTimeoutSeconds"), LatentBudgetSeconds);
2923
+ if (Abandoned > 0)
2924
+ {
2925
+ Result->SetStringField(TEXT("warning"), FString::Printf(
2926
+ TEXT("%d test(s) were abandoned with latent commands still queued. They need engine frames, and this handler holds the game thread for its whole run, so it cannot supply any. Raising latentTimeoutSeconds helps a test that only polls; a test that starts PIE needs the editor's Automation window or -ExecCmds=\"Automation RunTests <name>\" at launch. Nothing was stopped mid-queue, which is what used to terminate the editor (#993)."),
2927
+ Abandoned));
2928
+ }
2718
2929
  Result->SetArrayField(TEXT("results"), Results);
2719
2930
  return MCPResult(Result);
2720
2931
  }
@@ -144,6 +144,7 @@ private:
144
144
  static TSharedPtr<FJsonValue> SaveDirty(const TSharedPtr<FJsonObject>& Params);
145
145
  // #340: enumerate currently-dirty content/map packages
146
146
  static TSharedPtr<FJsonValue> ListDirtyPackages(const TSharedPtr<FJsonObject>& Params);
147
+ static TSharedPtr<FJsonValue> GetWorldState(const TSharedPtr<FJsonObject>& Params);
147
148
  // Gracefully close the editor after dirty-package and PIE safety checks.
148
149
  static TSharedPtr<FJsonValue> RequestEditorShutdown(const TSharedPtr<FJsonObject>& Params);
149
150
  // PIE runtime inspection/control (#739/#756/#757/#761/#764/#770/#777/#778).
@@ -154,6 +155,9 @@ private:
154
155
  static TSharedPtr<FJsonValue> ReadBoneTransforms(const TSharedPtr<FJsonObject>& Params);
155
156
  static TSharedPtr<FJsonValue> TeleportRuntimeActor(const TSharedPtr<FJsonObject>& Params);
156
157
  static TSharedPtr<FJsonValue> SetMovementMode(const TSharedPtr<FJsonObject>& Params);
158
+ // Bounded, reversible visibility changes on live PIE actors/components.
159
+ static TSharedPtr<FJsonValue> SetRuntimeVisibility(const TSharedPtr<FJsonObject>& Params);
160
+ static TSharedPtr<FJsonValue> RestoreRuntimeVisibility(const TSharedPtr<FJsonObject>& Params);
157
161
  // #802: locate live UObject instances, and write to one.
158
162
  static TSharedPtr<FJsonValue> FindLiveObjects(const TSharedPtr<FJsonObject>& Params);
159
163
  static TSharedPtr<FJsonValue> SetObjectProperty(const TSharedPtr<FJsonObject>& Params);