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
@@ -7,26 +7,393 @@
7
7
  #include "Engine/World.h"
8
8
  #include "EngineUtils.h"
9
9
  #include "Landscape.h"
10
+ #include "LandscapeDataAccess.h"
11
+ // #939: FLandscapeEditDataInterface, the same height/weight accessor
12
+ // landscape(sculpt) and landscape(paint_layer) write through, so a sample reads
13
+ // back exactly what a paint wrote.
14
+ #include "LandscapeEdit.h"
10
15
  #include "LandscapeEditTypes.h"
11
16
  #include "LandscapeProxy.h"
12
17
  #include "LandscapeStreamingProxy.h"
13
18
  #include "LandscapeInfo.h"
14
19
  #include "LandscapeComponent.h"
20
+ #include "LandscapeHeightfieldCollisionComponent.h"
15
21
  #include "LandscapeSplineActor.h"
16
22
  #include "LandscapeSplinesComponent.h"
17
23
  #include "LandscapeSplineControlPoint.h"
18
24
  #include "LandscapeSplineSegment.h"
25
+ #include "CollisionQueryParams.h"
26
+ #include "Engine/HitResult.h"
27
+ #include "PhysicalMaterials/PhysicalMaterial.h"
19
28
  #include "Kismet/KismetSystemLibrary.h"
20
29
  #include "Misc/FileHelper.h"
30
+ #include "Misc/SecureHash.h"
31
+ #include "HAL/IConsoleManager.h"
32
+ #include "HAL/PlatformProcess.h"
33
+ #include "HAL/PlatformTime.h"
21
34
  #include "Materials/MaterialInterface.h"
22
35
  #include "EditorScriptingUtilities/Public/EditorAssetLibrary.h"
36
+ #include "RenderingThread.h"
23
37
  #include "Components/PrimitiveComponent.h"
38
+ #include "PhysicsEngine/BodyInstance.h"
24
39
  #include "LandscapeLayerInfoObject.h"
25
40
  #include "UObject/Package.h"
41
+ #include "UObject/UnrealType.h"
26
42
  #include "AssetRegistry/AssetRegistryModule.h"
27
43
  #include "AssetToolsModule.h"
28
44
  #include "IAssetTools.h"
29
45
 
46
+ #if WITH_DEV_AUTOMATION_TESTS
47
+ #include "Misc/AutomationTest.h"
48
+ #endif
49
+
50
+ #if UE_MCP_HAS_5_8_API
51
+
52
+ namespace
53
+ {
54
+ constexpr float HeightCollisionToleranceCm = 0.25f;
55
+
56
+ struct FLandscapeHeightCollisionSignature
57
+ {
58
+ ULandscapeHeightfieldCollisionComponent* Collision = nullptr;
59
+ FString ComponentPath;
60
+ int64 RawElementCount = 0;
61
+ int32 ComplexSampleCount = 0;
62
+ int32 CollisionSizeVerts = 0;
63
+ int32 SimpleSampleCount = 0;
64
+ int32 SimpleCollisionSizeVerts = 0;
65
+ FSHAHash RawHash;
66
+ FSHAHash LiveHash;
67
+ FTransform ComponentTransform;
68
+ float RawMinWorldZ = 0.0f;
69
+ float RawMaxWorldZ = 0.0f;
70
+ float LiveMinWorldZ = 0.0f;
71
+ float LiveMaxWorldZ = 0.0f;
72
+ };
73
+
74
+ bool ReadCollisionSampleInfo(
75
+ const ULandscapeHeightfieldCollisionComponent* Collision,
76
+ ULandscapeHeightfieldCollisionComponent::FCollisionSampleInfo& OutInfo,
77
+ FString& OutError)
78
+ {
79
+ // GetCollisionSampleInfo is public but not LANDSCAPE_API, so calling it
80
+ // from an external plugin compiles and then fails to link. Reproduce its
81
+ // four-line calculation from the reflected size properties instead.
82
+ static const FIntProperty* CollisionSizeQuadsProperty =
83
+ FindFProperty<FIntProperty>(
84
+ ULandscapeHeightfieldCollisionComponent::StaticClass(),
85
+ TEXT("CollisionSizeQuads"));
86
+ static const FIntProperty* SimpleCollisionSizeQuadsProperty =
87
+ FindFProperty<FIntProperty>(
88
+ ULandscapeHeightfieldCollisionComponent::StaticClass(),
89
+ TEXT("SimpleCollisionSizeQuads"));
90
+ if (!CollisionSizeQuadsProperty || !SimpleCollisionSizeQuadsProperty)
91
+ {
92
+ OutError = TEXT("Landscape collision size properties are unavailable");
93
+ return false;
94
+ }
95
+
96
+ const int32 CollisionSizeQuads =
97
+ CollisionSizeQuadsProperty->GetPropertyValue_InContainer(Collision);
98
+ const int32 SimpleCollisionSizeQuads =
99
+ SimpleCollisionSizeQuadsProperty->GetPropertyValue_InContainer(Collision);
100
+ if (CollisionSizeQuads <= 0 || SimpleCollisionSizeQuads < 0)
101
+ {
102
+ OutError = FString::Printf(
103
+ TEXT("Invalid landscape collision quad dimensions: complex=%d, simple=%d"),
104
+ CollisionSizeQuads, SimpleCollisionSizeQuads);
105
+ return false;
106
+ }
107
+
108
+ OutInfo.CollisionSizeVerts = CollisionSizeQuads + 1;
109
+ OutInfo.SimpleCollisionSizeVerts =
110
+ SimpleCollisionSizeQuads > 0 ? SimpleCollisionSizeQuads + 1 : 0;
111
+ OutInfo.NumSamples = FMath::Square(OutInfo.CollisionSizeVerts);
112
+ OutInfo.NumSimpleSamples = FMath::Square(OutInfo.SimpleCollisionSizeVerts);
113
+ return true;
114
+ }
115
+
116
+ bool BuildHeightCollisionSignature(
117
+ TConstArrayView<uint16> RawHeights,
118
+ int32 ComplexSampleCount,
119
+ int32 CollisionSizeVerts,
120
+ int32 SimpleSampleCount,
121
+ int32 SimpleCollisionSizeVerts,
122
+ TConstArrayView<float> LiveWorldHeights,
123
+ const FTransform& ComponentTransform,
124
+ FLandscapeHeightCollisionSignature& OutSignature,
125
+ FString& OutError)
126
+ {
127
+ if (ComplexSampleCount <= 0 || CollisionSizeVerts <= 1 ||
128
+ ComplexSampleCount != CollisionSizeVerts * CollisionSizeVerts)
129
+ {
130
+ OutError = TEXT("Invalid landscape collision sample dimensions");
131
+ return false;
132
+ }
133
+ if (SimpleSampleCount < 0 || SimpleCollisionSizeVerts < 0 ||
134
+ (SimpleSampleCount == 0) != (SimpleCollisionSizeVerts == 0) ||
135
+ (SimpleSampleCount > 0 && SimpleSampleCount != SimpleCollisionSizeVerts * SimpleCollisionSizeVerts))
136
+ {
137
+ OutError = TEXT("Invalid simple landscape collision sample dimensions");
138
+ return false;
139
+ }
140
+ const int32 TotalSampleCount = ComplexSampleCount + SimpleSampleCount;
141
+ if (RawHeights.Num() != TotalSampleCount)
142
+ {
143
+ OutError = FString::Printf(
144
+ TEXT("Raw collision height data has %d element(s), expected %d complex + simple samples"),
145
+ RawHeights.Num(), TotalSampleCount);
146
+ return false;
147
+ }
148
+ if (LiveWorldHeights.Num() != TotalSampleCount)
149
+ {
150
+ OutError = FString::Printf(
151
+ TEXT("Live collision heightfields returned %d sample(s), expected %d"),
152
+ LiveWorldHeights.Num(), TotalSampleCount);
153
+ return false;
154
+ }
155
+
156
+ OutSignature.RawElementCount = RawHeights.Num();
157
+ OutSignature.ComplexSampleCount = ComplexSampleCount;
158
+ OutSignature.CollisionSizeVerts = CollisionSizeVerts;
159
+ OutSignature.SimpleSampleCount = SimpleSampleCount;
160
+ OutSignature.SimpleCollisionSizeVerts = SimpleCollisionSizeVerts;
161
+ OutSignature.ComponentTransform = ComponentTransform;
162
+ OutSignature.RawHash = FSHA1::HashBuffer(
163
+ RawHeights.GetData(), static_cast<uint64>(RawHeights.Num()) * sizeof(uint16));
164
+
165
+ FSHA1 LiveHasher;
166
+ OutSignature.RawMinWorldZ = TNumericLimits<float>::Max();
167
+ OutSignature.RawMaxWorldZ = TNumericLimits<float>::Lowest();
168
+ OutSignature.LiveMinWorldZ = TNumericLimits<float>::Max();
169
+ OutSignature.LiveMaxWorldZ = TNumericLimits<float>::Lowest();
170
+ for (int32 Index = 0; Index < TotalSampleCount; ++Index)
171
+ {
172
+ const float ExpectedWorldZ = static_cast<float>(ComponentTransform.TransformPosition(
173
+ FVector(0.0, 0.0, LandscapeDataAccess::GetLocalHeight(RawHeights[Index]))).Z);
174
+ const float LiveWorldZ = LiveWorldHeights[Index];
175
+ if (!FMath::IsFinite(ExpectedWorldZ) || !FMath::IsFinite(LiveWorldZ) ||
176
+ !FMath::IsNearlyEqual(ExpectedWorldZ, LiveWorldZ, HeightCollisionToleranceCm))
177
+ {
178
+ OutError = FString::Printf(
179
+ TEXT("Raw/live collision height mismatch at sample %d: raw predicts %.3f cm, live heightfield reports %.3f cm"),
180
+ Index, ExpectedWorldZ, LiveWorldZ);
181
+ return false;
182
+ }
183
+
184
+ OutSignature.RawMinWorldZ = FMath::Min(OutSignature.RawMinWorldZ, ExpectedWorldZ);
185
+ OutSignature.RawMaxWorldZ = FMath::Max(OutSignature.RawMaxWorldZ, ExpectedWorldZ);
186
+ OutSignature.LiveMinWorldZ = FMath::Min(OutSignature.LiveMinWorldZ, LiveWorldZ);
187
+ OutSignature.LiveMaxWorldZ = FMath::Max(OutSignature.LiveMaxWorldZ, LiveWorldZ);
188
+
189
+ // Hash at 0.1 cm precision. A recreated Chaos heightfield can differ by a
190
+ // tiny floating-point amount while still representing the same uint16
191
+ // source height, but a flattened or shifted field must never compare equal.
192
+ const int64 QuantizedHeight = FMath::RoundToInt64(static_cast<double>(LiveWorldZ) * 10.0);
193
+ LiveHasher.Update(QuantizedHeight);
194
+ }
195
+ OutSignature.LiveHash = LiveHasher.Finalize();
196
+ return true;
197
+ }
198
+
199
+ bool CaptureHeightCollisionSignature(
200
+ ULandscapeHeightfieldCollisionComponent* Collision,
201
+ FLandscapeHeightCollisionSignature& OutSignature,
202
+ FString& OutError)
203
+ {
204
+ if (!Collision)
205
+ {
206
+ OutError = TEXT("Landscape collision component is null");
207
+ return false;
208
+ }
209
+ const FBodyInstance* BodyInstance = Collision->GetBodyInstance();
210
+ if (!Collision->IsRegistered() || !Collision->IsPhysicsStateCreated() ||
211
+ !BodyInstance || !BodyInstance->IsValidBodyInstance())
212
+ {
213
+ OutError = FString::Printf(
214
+ TEXT("%s does not have a registered, valid physics body"), *Collision->GetPathName());
215
+ return false;
216
+ }
217
+
218
+ ULandscapeHeightfieldCollisionComponent::FCollisionSampleInfo SampleInfo;
219
+ if (!ReadCollisionSampleInfo(Collision, SampleInfo, OutError))
220
+ {
221
+ return false;
222
+ }
223
+ const int64 ExpectedRawElements = static_cast<int64>(SampleInfo.NumSamples) + SampleInfo.NumSimpleSamples;
224
+ const int64 RawElementCount = Collision->CollisionHeightData.GetElementCount();
225
+ if (ExpectedRawElements <= 0 || RawElementCount != ExpectedRawElements)
226
+ {
227
+ OutError = FString::Printf(
228
+ TEXT("%s has %lld raw collision height element(s), expected %lld"),
229
+ *Collision->GetPathName(), RawElementCount, ExpectedRawElements);
230
+ return false;
231
+ }
232
+
233
+ TArray<float> LiveWorldHeights;
234
+ LiveWorldHeights.SetNumUninitialized(static_cast<int32>(ExpectedRawElements));
235
+ if (!Collision->FillHeightTile(
236
+ MakeArrayView(LiveWorldHeights.GetData(), SampleInfo.NumSamples),
237
+ 0,
238
+ SampleInfo.CollisionSizeVerts))
239
+ {
240
+ OutError = FString::Printf(
241
+ TEXT("%s has no readable live complex collision heightfield"), *Collision->GetPathName());
242
+ return false;
243
+ }
244
+ if (SampleInfo.NumSimpleSamples > 0)
245
+ {
246
+ const FTransform& WorldTransform = Collision->GetComponentTransform();
247
+ for (int32 Y = 0; Y < SampleInfo.SimpleCollisionSizeVerts; ++Y)
248
+ {
249
+ for (int32 X = 0; X < SampleInfo.SimpleCollisionSizeVerts; ++X)
250
+ {
251
+ const TOptional<float> SimpleLocalHeight =
252
+ Collision->GetHeight(X, Y, EHeightfieldSource::Simple);
253
+ if (!SimpleLocalHeight.IsSet())
254
+ {
255
+ OutError = FString::Printf(
256
+ TEXT("%s has no readable live simple collision heightfield"),
257
+ *Collision->GetPathName());
258
+ return false;
259
+ }
260
+ const int32 SampleIndex = SampleInfo.NumSamples +
261
+ Y * SampleInfo.SimpleCollisionSizeVerts + X;
262
+ LiveWorldHeights[SampleIndex] = static_cast<float>(
263
+ WorldTransform.TransformPositionNoScale(
264
+ FVector(0.0, 0.0, SimpleLocalHeight.GetValue())).Z);
265
+ }
266
+ }
267
+ }
268
+
269
+ const uint16* RawHeightData = static_cast<const uint16*>(Collision->CollisionHeightData.LockReadOnly());
270
+ if (!RawHeightData)
271
+ {
272
+ Collision->CollisionHeightData.Unlock();
273
+ OutError = FString::Printf(
274
+ TEXT("%s raw collision height data could not be locked"), *Collision->GetPathName());
275
+ return false;
276
+ }
277
+
278
+ OutSignature.Collision = Collision;
279
+ OutSignature.ComponentPath = Collision->GetPathName();
280
+ const bool bBuilt = BuildHeightCollisionSignature(
281
+ MakeArrayView(RawHeightData, static_cast<int32>(RawElementCount)),
282
+ SampleInfo.NumSamples,
283
+ SampleInfo.CollisionSizeVerts,
284
+ SampleInfo.NumSimpleSamples,
285
+ SampleInfo.SimpleCollisionSizeVerts,
286
+ LiveWorldHeights,
287
+ Collision->GetComponentTransform(),
288
+ OutSignature,
289
+ OutError);
290
+ Collision->CollisionHeightData.Unlock();
291
+ return bBuilt;
292
+ }
293
+
294
+ bool MatchesHeightCollisionSignature(
295
+ const FLandscapeHeightCollisionSignature& Baseline,
296
+ const FLandscapeHeightCollisionSignature& Current,
297
+ FString& OutError)
298
+ {
299
+ if (Baseline.RawElementCount != Current.RawElementCount || Baseline.RawHash != Current.RawHash)
300
+ {
301
+ OutError = FString::Printf(
302
+ TEXT("%s raw collision height data changed"), *Baseline.ComponentPath);
303
+ return false;
304
+ }
305
+ if (Baseline.ComplexSampleCount != Current.ComplexSampleCount ||
306
+ Baseline.CollisionSizeVerts != Current.CollisionSizeVerts ||
307
+ Baseline.SimpleSampleCount != Current.SimpleSampleCount ||
308
+ Baseline.SimpleCollisionSizeVerts != Current.SimpleCollisionSizeVerts ||
309
+ !Baseline.ComponentTransform.Equals(Current.ComponentTransform, 1.e-6f))
310
+ {
311
+ OutError = FString::Printf(
312
+ TEXT("%s collision dimensions or transform changed"), *Baseline.ComponentPath);
313
+ return false;
314
+ }
315
+ if (Baseline.LiveHash != Current.LiveHash ||
316
+ !FMath::IsNearlyEqual(Baseline.LiveMinWorldZ, Current.LiveMinWorldZ, HeightCollisionToleranceCm) ||
317
+ !FMath::IsNearlyEqual(Baseline.LiveMaxWorldZ, Current.LiveMaxWorldZ, HeightCollisionToleranceCm))
318
+ {
319
+ OutError = FString::Printf(
320
+ TEXT("%s live collision heightfield changed (baseline %.3f..%.3f cm, current %.3f..%.3f cm)"),
321
+ *Baseline.ComponentPath,
322
+ Baseline.LiveMinWorldZ, Baseline.LiveMaxWorldZ,
323
+ Current.LiveMinWorldZ, Current.LiveMaxWorldZ);
324
+ return false;
325
+ }
326
+ return true;
327
+ }
328
+
329
+ bool ValidateLandscapeCollisionPreflight(
330
+ int32 LandscapeComponents,
331
+ int32 CollisionComponents,
332
+ int32 CapturedHeightComponents,
333
+ int32 PendingLayerUpdateComponents,
334
+ bool bLandscapeLayersUpToDate,
335
+ bool bTextureResourcesReady,
336
+ FString& OutError)
337
+ {
338
+ if (CollisionComponents == 0)
339
+ {
340
+ OutError = TEXT("No registered landscape collision components were available to refresh");
341
+ return false;
342
+ }
343
+ if (CollisionComponents != LandscapeComponents ||
344
+ CapturedHeightComponents != CollisionComponents)
345
+ {
346
+ OutError = FString::Printf(
347
+ TEXT("Loaded proxy coverage is incomplete: %d of %d landscape component(s) have registered, captured collision"),
348
+ CapturedHeightComponents, LandscapeComponents);
349
+ return false;
350
+ }
351
+ if (!bLandscapeLayersUpToDate || PendingLayerUpdateComponents > 0)
352
+ {
353
+ OutError = TEXT("The parent landscape has pending edit-layer updates. A later PreSave could replace collision heights; let the landscape finish updating and validate it before retrying.");
354
+ return false;
355
+ }
356
+ if (!bTextureResourcesReady)
357
+ {
358
+ OutError = TEXT("The parent landscape could not make its texture resources resident");
359
+ return false;
360
+ }
361
+ return true;
362
+ }
363
+
364
+ class FScopedLandscapePhysicalMaterialRecreateMode
365
+ {
366
+ public:
367
+ FScopedLandscapePhysicalMaterialRecreateMode()
368
+ {
369
+ Variable = IConsoleManager::Get().FindConsoleVariable(
370
+ TEXT("landscape.ApplyPhysicalMaterialChangesImmediately"));
371
+ if (Variable)
372
+ {
373
+ Variable->Set(0, ECVF_SetByTemp, OverrideTag);
374
+ bEffective = Variable->GetInt() == 0;
375
+ }
376
+ }
377
+
378
+ ~FScopedLandscapePhysicalMaterialRecreateMode()
379
+ {
380
+ if (Variable)
381
+ {
382
+ Variable->Unset(ECVF_SetByTemp, OverrideTag);
383
+ }
384
+ }
385
+
386
+ bool IsValid() const { return Variable != nullptr && bEffective; }
387
+
388
+ private:
389
+ static inline const FName OverrideTag = TEXT("UE_MCP_LandscapePhysicalMaterialCollision");
390
+ IConsoleVariable* Variable = nullptr;
391
+ bool bEffective = false;
392
+ };
393
+ }
394
+
395
+ #endif // UE_MCP_HAS_5_8_API
396
+
30
397
  void FLandscapeHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
31
398
  {
32
399
  Registry.RegisterHandler(TEXT("get_landscape_info"), &GetLandscapeInfo);
@@ -42,6 +409,7 @@ void FLandscapeHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
42
409
  // #733: World Partition landscape streaming-proxy enumeration + spatial lookup.
43
410
  Registry.RegisterHandler(TEXT("list_landscape_proxies"), &ListLandscapeProxies);
44
411
  Registry.RegisterHandler(TEXT("find_landscape_proxy_at"), &FindLandscapeProxyAt);
412
+ Registry.RegisterHandlerWithTimeout(TEXT("refresh_landscape_physical_material_collision"), &RefreshPhysicalMaterialCollision, 600.0f);
45
413
  Registry.RegisterHandlerWithTimeout(TEXT("sculpt_landscape"), &Sculpt, 120.0f);
46
414
  Registry.RegisterHandlerWithTimeout(TEXT("paint_landscape_layer"), &PaintLayer, 120.0f);
47
415
  }
@@ -140,61 +508,295 @@ TSharedPtr<FJsonValue> FLandscapeHandlers::ListLandscapeLayers(const TSharedPtr<
140
508
  return MCPResult(Result);
141
509
  }
142
510
 
511
+ // landscape(sample): height AND paint-layer weights at a world XY (#939).
512
+ //
513
+ // The action advertised "Params: x, y" and the handler required an object
514
+ // called `point`, so BOTH documented call shapes failed with "Missing 'point'
515
+ // parameter" and there was no working call at all. It also never returned a
516
+ // single layer weight despite advertising "height/layers", which left reading a
517
+ // weight to rendering the weightmap into a render target and reading that back.
518
+ //
519
+ // Both halves are fixed here. The position is accepted in every shape the
520
+ // category already uses (x/y, point{x,y}, worldX/worldY, all named in the
521
+ // schema), and the weights come from the same FLandscapeEditDataInterface that
522
+ // landscape(paint_layer) writes through, so a sample taken after a paint reads
523
+ // back the number that was painted. No edit layer is selected, which is what
524
+ // makes the read the MERGED result the renderer and the physics use, rather
525
+ // than one layer's contribution to it.
143
526
  TSharedPtr<FJsonValue> FLandscapeHandlers::SampleLandscape(const TSharedPtr<FJsonObject>& Params)
144
527
  {
528
+ // Position, in every shape the schema names. `point` may carry a z, which is
529
+ // only ever used as the origin of the confirmation trace: the surface height
530
+ // is what this action answers, so it is never an input to itself.
531
+ double WorldX = 0.0;
532
+ double WorldY = 0.0;
533
+ double TraceOriginZ = 0.0;
534
+ bool bHavePosition = false;
535
+
145
536
  const TSharedPtr<FJsonObject>* PointObj = nullptr;
146
- if (!Params->TryGetObjectField(TEXT("point"), PointObj))
537
+ if (Params->TryGetObjectField(TEXT("point"), PointObj) && PointObj && PointObj->IsValid())
147
538
  {
148
- return MCPError(TEXT("Missing 'point' parameter"));
539
+ (*PointObj)->TryGetNumberField(TEXT("x"), WorldX);
540
+ (*PointObj)->TryGetNumberField(TEXT("y"), WorldY);
541
+ (*PointObj)->TryGetNumberField(TEXT("z"), TraceOriginZ);
542
+ bHavePosition = true;
543
+ }
544
+ else if (Params->HasField(TEXT("x")) && Params->HasField(TEXT("y")))
545
+ {
546
+ WorldX = OptionalNumber(Params, TEXT("x"), 0.0);
547
+ WorldY = OptionalNumber(Params, TEXT("y"), 0.0);
548
+ bHavePosition = true;
549
+ }
550
+ else if (Params->HasField(TEXT("worldX")) && Params->HasField(TEXT("worldY")))
551
+ {
552
+ // The names landscape(find_proxy_at) takes. A caller moving between the
553
+ // two actions should not have to rename the same two numbers.
554
+ WorldX = OptionalNumber(Params, TEXT("worldX"), 0.0);
555
+ WorldY = OptionalNumber(Params, TEXT("worldY"), 0.0);
556
+ bHavePosition = true;
557
+ }
558
+ if (!bHavePosition)
559
+ {
560
+ return MCPError(TEXT("Missing sample position. Pass x and y, or point {x, y}, or worldX and worldY (all world space)."));
149
561
  }
150
-
151
- FVector Point;
152
- Point.X = (*PointObj)->GetNumberField(TEXT("x"));
153
- Point.Y = (*PointObj)->GetNumberField(TEXT("y"));
154
- Point.Z = (*PointObj)->GetNumberField(TEXT("z"));
155
562
 
156
563
  REQUIRE_EDITOR_WORLD(World);
157
564
 
158
- // Find the first landscape and sample height
565
+ // Which landscape. A label picks one explicitly; without one, every proxy in
566
+ // the world contributes its ULandscapeInfo and the point decides between
567
+ // them, because a map with two landscapes has no single right default.
568
+ // #983: actorPath narrows to exactly one proxy when several share a label,
569
+ // which is what a World Partition map full of streaming proxies looks like.
570
+ const FString ActorLabel = OptionalString(Params, TEXT("actorLabel"));
571
+ const FString ActorPath = OptionalString(Params, TEXT("actorPath"));
572
+ // Normalised the same way MCPFindActorByPath normalises, so the export-text
573
+ // form (Actor'/Game/...') resolves here exactly as it does on every other
574
+ // action rather than silently matching nothing (#983).
575
+ const FString WantedPath = ActorPath.IsEmpty()
576
+ ? FString()
577
+ : FPackageName::ExportTextPathToObjectPath(ActorPath).TrimStartAndEnd();
578
+ TArray<ULandscapeInfo*> Candidates;
159
579
  for (TActorIterator<ALandscapeProxy> It(World); It; ++It)
160
580
  {
161
- ALandscapeProxy* Landscape = *It;
162
- if (!Landscape) continue;
581
+ ALandscapeProxy* Proxy = *It;
582
+ if (!Proxy) continue;
583
+ if (!WantedPath.IsEmpty() && !Proxy->GetPathName().Equals(WantedPath, ESearchCase::IgnoreCase)) continue;
584
+ if (!ActorLabel.IsEmpty() && !Proxy->GetActorLabel().Equals(ActorLabel, ESearchCase::IgnoreCase)) continue;
585
+ ULandscapeInfo* ProxyInfo = Proxy->GetLandscapeInfo();
586
+ if (!ProxyInfo) continue;
587
+ if (Candidates.Contains(ProxyInfo)) continue;
588
+ Candidates.Add(ProxyInfo);
589
+ }
590
+ if (Candidates.Num() == 0)
591
+ {
592
+ if (ActorLabel.IsEmpty() && ActorPath.IsEmpty())
593
+ {
594
+ return MCPError(TEXT("No landscape in the current level. Create one with landscape(create)."));
595
+ }
596
+ return MCPError(FString::Printf(
597
+ TEXT("No landscape actor matched '%s' (nothing with a registered LandscapeInfo matched)"),
598
+ ActorPath.IsEmpty() ? *ActorLabel : *ActorPath));
599
+ }
600
+
601
+ // Pick the landscape whose quad extent actually covers the point. With one
602
+ // landscape this is the only candidate and the extent test still reports
603
+ // whether the point is on it, which is the difference between "weight is
604
+ // zero here" and "this position is not on the landscape at all".
605
+ ULandscapeInfo* Info = nullptr;
606
+ FTransform LandscapeToWorld;
607
+ FVector LocalPoint = FVector::ZeroVector;
608
+ FIntRect Extent(0, 0, 0, 0);
609
+ for (ULandscapeInfo* Candidate : Candidates)
610
+ {
611
+ ALandscapeProxy* Reference = Candidate->GetLandscapeProxy();
612
+ if (!Reference) continue;
613
+ FIntRect CandidateExtent;
614
+ if (!Candidate->GetLandscapeExtent(CandidateExtent)) continue;
615
+
616
+ const FTransform CandidateTransform = Reference->ActorToWorld();
617
+ const FVector CandidateLocal =
618
+ CandidateTransform.InverseTransformPosition(FVector(WorldX, WorldY, 0.0));
619
+ // RoundToInt32, not RoundToInt: the double overload of the latter
620
+ // returns int64 and would narrow on the way into these.
621
+ const int32 QuadX = FMath::RoundToInt32(CandidateLocal.X);
622
+ const int32 QuadY = FMath::RoundToInt32(CandidateLocal.Y);
623
+ const bool bCovers =
624
+ QuadX >= CandidateExtent.Min.X && QuadX <= CandidateExtent.Max.X &&
625
+ QuadY >= CandidateExtent.Min.Y && QuadY <= CandidateExtent.Max.Y;
626
+
627
+ // Remember the first candidate either way, so a point off every
628
+ // landscape still reports which one it was measured against.
629
+ if (!Info || bCovers)
630
+ {
631
+ Info = Candidate;
632
+ LandscapeToWorld = CandidateTransform;
633
+ LocalPoint = CandidateLocal;
634
+ Extent = CandidateExtent;
635
+ }
636
+ if (bCovers) break;
637
+ }
638
+ if (!Info)
639
+ {
640
+ return MCPError(TEXT("Landscape found but its LandscapeInfo has no registered extent yet (are its components loaded?)"));
641
+ }
163
642
 
164
- // Use line trace to get the landscape height at the given point
165
- FVector TraceStart(Point.X, Point.Y, Point.Z + 100000.0f);
166
- FVector TraceEnd(Point.X, Point.Y, Point.Z - 100000.0f);
643
+ ALandscapeProxy* ReferenceProxy = Info->GetLandscapeProxy();
644
+ const int32 QuadX = FMath::RoundToInt32(LocalPoint.X);
645
+ const int32 QuadY = FMath::RoundToInt32(LocalPoint.Y);
646
+ const bool bInBounds =
647
+ QuadX >= Extent.Min.X && QuadX <= Extent.Max.X &&
648
+ QuadY >= Extent.Min.Y && QuadY <= Extent.Max.Y;
167
649
 
168
- FHitResult HitResult;
169
- FCollisionQueryParams QueryParams;
170
- QueryParams.bTraceComplex = true;
650
+ auto Result = MCPSuccess();
651
+ Result->SetStringField(TEXT("landscape"), ReferenceProxy ? ReferenceProxy->GetActorLabel() : FString());
652
+ TSharedPtr<FJsonObject> QuadObj = MakeShared<FJsonObject>();
653
+ QuadObj->SetNumberField(TEXT("x"), QuadX);
654
+ QuadObj->SetNumberField(TEXT("y"), QuadY);
655
+ Result->SetObjectField(TEXT("quad"), QuadObj);
656
+ Result->SetBoolField(TEXT("inBounds"), bInBounds);
657
+ TSharedPtr<FJsonObject> ExtentObj = MakeShared<FJsonObject>();
658
+ ExtentObj->SetNumberField(TEXT("minX"), Extent.Min.X);
659
+ ExtentObj->SetNumberField(TEXT("minY"), Extent.Min.Y);
660
+ ExtentObj->SetNumberField(TEXT("maxX"), Extent.Max.X);
661
+ ExtentObj->SetNumberField(TEXT("maxY"), Extent.Max.Y);
662
+ Result->SetObjectField(TEXT("quadExtent"), ExtentObj);
663
+
664
+ if (!bInBounds)
665
+ {
666
+ Result->SetBoolField(TEXT("hit"), false);
667
+ Result->SetStringField(TEXT("reason"),
668
+ TEXT("The world position is outside this landscape's loaded quad extent, so height and weights would both read as zero rather than as measurements. On a World Partition map check landscape(find_proxy_at) first."));
669
+ return MCPResult(Result);
670
+ }
671
+
672
+ // One edit-data interface for the height and every layer, with no edit layer
673
+ // selected so the read is the merged result rather than one layer's own
674
+ // contribution to it.
675
+ FLandscapeEditDataInterface EditData(Info);
171
676
 
172
- if (World->LineTraceSingleByChannel(HitResult, TraceStart, TraceEnd, ECC_WorldStatic, QueryParams))
677
+ uint16 RawHeight = 0;
678
+ {
679
+ int32 X1 = QuadX, Y1 = QuadY, X2 = QuadX, Y2 = QuadY;
680
+ // GetHeightData rewrites the rect with the range it could actually
681
+ // serve, so an unloaded component comes back as an inverted rect rather
682
+ // than as a zero sample that reads like real data.
683
+ EditData.GetHeightData(X1, Y1, X2, Y2, &RawHeight, 0);
684
+ if (X2 < X1 || Y2 < Y1)
173
685
  {
174
- if (HitResult.GetActor() && HitResult.GetActor()->IsA(ALandscapeProxy::StaticClass()))
686
+ Result->SetBoolField(TEXT("hit"), false);
687
+ Result->SetStringField(TEXT("reason"),
688
+ TEXT("No landscape height data at this position (the covering component is not loaded). Pin it with level(load_actor_descs) first."));
689
+ return MCPResult(Result);
690
+ }
691
+ }
692
+
693
+ const double LocalHeight = LandscapeDataAccess::GetLocalHeight(RawHeight);
694
+ const FVector SurfaceWorld = LandscapeToWorld.TransformPosition(
695
+ FVector(static_cast<double>(QuadX), static_cast<double>(QuadY), static_cast<double>(LocalHeight)));
696
+
697
+ Result->SetBoolField(TEXT("hit"), true);
698
+ Result->SetNumberField(TEXT("height"), SurfaceWorld.Z);
699
+ Result->SetNumberField(TEXT("rawHeight"), RawHeight);
700
+ TSharedPtr<FJsonObject> LocationObj = MakeShared<FJsonObject>();
701
+ LocationObj->SetNumberField(TEXT("x"), SurfaceWorld.X);
702
+ LocationObj->SetNumberField(TEXT("y"), SurfaceWorld.Y);
703
+ LocationObj->SetNumberField(TEXT("z"), SurfaceWorld.Z);
704
+ Result->SetObjectField(TEXT("location"), LocationObj);
705
+
706
+ // Paint-layer weights, the half that had no read at all. Each layer is
707
+ // reported both as the raw 0..255 weightmap byte and as the 0..1 fraction,
708
+ // because the editor shows one and a material reads the other.
709
+ if (OptionalBool(Params, TEXT("includeLayers"), true))
710
+ {
711
+ const FString LayerFilter = OptionalString(Params, TEXT("layerName"));
712
+ TArray<TSharedPtr<FJsonValue>> LayerArray;
713
+ double TotalWeight = 0.0;
714
+ FString DominantLayer;
715
+ double DominantWeight = -1.0;
716
+ TArray<FString> KnownLayers;
717
+
718
+ for (const FLandscapeInfoLayerSettings& LayerSettings : Info->Layers)
719
+ {
720
+ ULandscapeLayerInfoObject* LayerInfo = LayerSettings.LayerInfoObj;
721
+ if (!LayerInfo) continue;
722
+ const FString LayerName = LayerSettings.GetLayerName().ToString();
723
+ KnownLayers.Add(LayerName);
724
+ if (!LayerFilter.IsEmpty() && !LayerName.Equals(LayerFilter, ESearchCase::IgnoreCase)) continue;
725
+
726
+ uint8 RawWeight = 0;
727
+ int32 X1 = QuadX, Y1 = QuadY, X2 = QuadX, Y2 = QuadY;
728
+ EditData.GetWeightData(LayerInfo, X1, Y1, X2, Y2, &RawWeight, 0);
729
+ if (X2 < X1 || Y2 < Y1) continue;
730
+
731
+ const double Weight = static_cast<double>(RawWeight) / 255.0;
732
+ TotalWeight += Weight;
733
+ if (Weight > DominantWeight)
734
+ {
735
+ DominantWeight = Weight;
736
+ DominantLayer = LayerName;
737
+ }
738
+
739
+ TSharedPtr<FJsonObject> LayerObj = MakeShared<FJsonObject>();
740
+ LayerObj->SetStringField(TEXT("name"), LayerName);
741
+ LayerObj->SetNumberField(TEXT("weight"), Weight);
742
+ LayerObj->SetNumberField(TEXT("weight255"), RawWeight);
743
+ LayerObj->SetStringField(TEXT("layerInfo"), LayerInfo->GetPathName());
744
+ if (UPhysicalMaterial* PhysMat = LayerInfo->GetPhysicalMaterial())
175
745
  {
176
- auto Result = MCPSuccess();
177
- Result->SetNumberField(TEXT("height"), HitResult.Location.Z);
178
- TSharedPtr<FJsonObject> HitPoint = MakeShared<FJsonObject>();
179
- HitPoint->SetNumberField(TEXT("x"), HitResult.Location.X);
180
- HitPoint->SetNumberField(TEXT("y"), HitResult.Location.Y);
181
- HitPoint->SetNumberField(TEXT("z"), HitResult.Location.Z);
182
- Result->SetObjectField(TEXT("hitLocation"), HitPoint);
183
-
184
- TSharedPtr<FJsonObject> Normal = MakeShared<FJsonObject>();
185
- Normal->SetNumberField(TEXT("x"), HitResult.Normal.X);
186
- Normal->SetNumberField(TEXT("y"), HitResult.Normal.Y);
187
- Normal->SetNumberField(TEXT("z"), HitResult.Normal.Z);
188
- Result->SetObjectField(TEXT("normal"), Normal);
189
-
190
- Result->SetBoolField(TEXT("hit"), true);
191
- return MCPResult(Result);
746
+ LayerObj->SetStringField(TEXT("physicalMaterial"), PhysMat->GetPathName());
192
747
  }
748
+ LayerArray.Add(MakeShared<FJsonValueObject>(LayerObj));
749
+ }
750
+
751
+ Result->SetArrayField(TEXT("layers"), LayerArray);
752
+ Result->SetNumberField(TEXT("layerCount"), LayerArray.Num());
753
+ Result->SetNumberField(TEXT("totalWeight"), TotalWeight);
754
+ if (!DominantLayer.IsEmpty())
755
+ {
756
+ Result->SetStringField(TEXT("dominantLayer"), DominantLayer);
757
+ Result->SetNumberField(TEXT("dominantWeight"), DominantWeight);
758
+ }
759
+ if (!LayerFilter.IsEmpty() && LayerArray.Num() == 0)
760
+ {
761
+ Result->SetStringField(TEXT("layerNote"), FString::Printf(
762
+ TEXT("No layer named '%s' is registered on this landscape. Registered layers: [%s]. Add one with landscape(add_layer_info)."),
763
+ *LayerFilter, *FString::Join(KnownLayers, TEXT(", "))));
193
764
  }
194
765
  }
195
766
 
196
- auto Result = MCPSuccess();
197
- Result->SetBoolField(TEXT("hit"), false);
767
+ // A confirmation trace against the collision heightfield, kept because it is
768
+ // the only source of a surface normal and because a disagreement between it
769
+ // and the heightmap is itself the answer to "is my collision stale".
770
+ {
771
+ const double TraceZ = FMath::Max(TraceOriginZ, SurfaceWorld.Z);
772
+ const FVector TraceStart(SurfaceWorld.X, SurfaceWorld.Y, TraceZ + 100000.0);
773
+ const FVector TraceEnd(SurfaceWorld.X, SurfaceWorld.Y, TraceZ - 200000.0);
774
+ FHitResult HitResult;
775
+ FCollisionQueryParams QueryParams(SCENE_QUERY_STAT(MCPLandscapeSample), /*bTraceComplex*/ true);
776
+ const bool bTraceHit =
777
+ World->LineTraceSingleByChannel(HitResult, TraceStart, TraceEnd, ECC_WorldStatic, QueryParams) &&
778
+ HitResult.GetActor() != nullptr &&
779
+ HitResult.GetActor()->IsA(ALandscapeProxy::StaticClass());
780
+ Result->SetBoolField(TEXT("traceHit"), bTraceHit);
781
+ if (bTraceHit)
782
+ {
783
+ TSharedPtr<FJsonObject> HitPoint = MakeShared<FJsonObject>();
784
+ HitPoint->SetNumberField(TEXT("x"), HitResult.Location.X);
785
+ HitPoint->SetNumberField(TEXT("y"), HitResult.Location.Y);
786
+ HitPoint->SetNumberField(TEXT("z"), HitResult.Location.Z);
787
+ Result->SetObjectField(TEXT("hitLocation"), HitPoint);
788
+ Result->SetNumberField(TEXT("traceHeight"), HitResult.Location.Z);
789
+
790
+ TSharedPtr<FJsonObject> Normal = MakeShared<FJsonObject>();
791
+ Normal->SetNumberField(TEXT("x"), HitResult.ImpactNormal.X);
792
+ Normal->SetNumberField(TEXT("y"), HitResult.ImpactNormal.Y);
793
+ Normal->SetNumberField(TEXT("z"), HitResult.ImpactNormal.Z);
794
+ Result->SetObjectField(TEXT("normal"), Normal);
795
+ }
796
+ }
797
+
798
+ Result->SetStringField(TEXT("note"),
799
+ TEXT("height and layer weights come from the landscape's own height and weight data (the merged result of every edit layer), so they are exact and do not depend on collision. traceHeight and normal come from a collision trace and are absent when collision is unbuilt or the proxy is streamed out."));
198
800
  return MCPResult(Result);
199
801
  }
200
802
 
@@ -876,3 +1478,653 @@ TSharedPtr<FJsonValue> FLandscapeHandlers::FindLandscapeProxyAt(const TSharedPtr
876
1478
  Result->SetStringField(TEXT("note"), TEXT("No loaded proxy covers this position; the covering proxy is likely streamed out, so weight/height readbacks here are ambiguous."));
877
1479
  return MCPResult(Result);
878
1480
  }
1481
+
1482
+ // Refresh physical-material collision data after a LayerInfo PhysMaterial edit.
1483
+ // LayerInfo PostEditChange requests a full edit-layer update as well as an
1484
+ // immediate collision recreation. This batch path deliberately separates those
1485
+ // operations: it refuses pending edit-layer work, updates layer/material data,
1486
+ // waits for the material build, and recreates collision exactly once. Complete
1487
+ // raw, complex-live, and simple-live heightfield signatures guard the mutation.
1488
+ // Package saving is deliberately outside this action. Only loaded streaming
1489
+ // proxies can be acted on; unloaded World Partition actors do not exist here.
1490
+ TSharedPtr<FJsonValue> FLandscapeHandlers::RefreshPhysicalMaterialCollision(const TSharedPtr<FJsonObject>& Params)
1491
+ {
1492
+ #if !UE_MCP_HAS_5_8_API
1493
+ return MCPError(TEXT("Landscape physical-material collision refresh requires Unreal Engine 5.8 or newer"));
1494
+ #else
1495
+ const double StartedAt = FPlatformTime::Seconds();
1496
+ if (!GEditor)
1497
+ {
1498
+ return MCPError(TEXT("Editor not available"));
1499
+ }
1500
+ if (GEditor->PlayWorld != nullptr || GEditor->bIsSimulatingInEditor)
1501
+ {
1502
+ return MCPError(TEXT("Stop PIE or SIE before refreshing landscape physical-material collision"));
1503
+ }
1504
+
1505
+ REQUIRE_EDITOR_WORLD(World);
1506
+ if (World->WorldType != EWorldType::Editor)
1507
+ {
1508
+ return MCPError(TEXT("Physical-material collision refresh requires the current editor world"));
1509
+ }
1510
+ if (!World->IsPartitionedWorld())
1511
+ {
1512
+ return MCPError(TEXT("The current editor world is not a World Partition map"));
1513
+ }
1514
+
1515
+ const int32 MaxActors = OptionalInt(Params, TEXT("maxActors"), 256);
1516
+ if (MaxActors < 1 || MaxActors > 1024)
1517
+ {
1518
+ return MCPError(TEXT("'maxActors' must be between 1 and 1024"));
1519
+ }
1520
+ if (OptionalBool(Params, TEXT("save"), false))
1521
+ {
1522
+ return MCPError(TEXT("save_not_supported: this action is in-memory only. Unreal Landscape PreSave can mutate pending edit-layer collision data, so persistence requires a separate validated workflow."));
1523
+ }
1524
+
1525
+ TSet<FString> WantedLabels;
1526
+ const bool bHasLabelFilter = Params->HasField(TEXT("actorLabels"));
1527
+ if (bHasLabelFilter)
1528
+ {
1529
+ const TArray<TSharedPtr<FJsonValue>>* Values = nullptr;
1530
+ if (!Params->TryGetArrayField(TEXT("actorLabels"), Values) || !Values || Values->IsEmpty() || Values->Num() > 256)
1531
+ {
1532
+ return MCPError(TEXT("'actorLabels' must be a non-empty array of at most 256 strings"));
1533
+ }
1534
+ for (const TSharedPtr<FJsonValue>& Value : *Values)
1535
+ {
1536
+ FString Label;
1537
+ if (!Value.IsValid() || !Value->TryGetString(Label) || Label.IsEmpty())
1538
+ {
1539
+ return MCPError(TEXT("Every 'actorLabels' entry must be a non-empty string"));
1540
+ }
1541
+ WantedLabels.Add(Label.ToLower());
1542
+ }
1543
+ }
1544
+
1545
+ TSet<FGuid> WantedGuids;
1546
+ const bool bHasGuidFilter = Params->HasField(TEXT("guids"));
1547
+ if (bHasGuidFilter)
1548
+ {
1549
+ const TArray<TSharedPtr<FJsonValue>>* Values = nullptr;
1550
+ if (!Params->TryGetArrayField(TEXT("guids"), Values) || !Values || Values->IsEmpty() || Values->Num() > 256)
1551
+ {
1552
+ return MCPError(TEXT("'guids' must be a non-empty array of at most 256 GUID strings"));
1553
+ }
1554
+ for (const TSharedPtr<FJsonValue>& Value : *Values)
1555
+ {
1556
+ FString GuidText;
1557
+ FGuid Guid;
1558
+ if (!Value.IsValid() || !Value->TryGetString(GuidText) || !FGuid::Parse(GuidText, Guid))
1559
+ {
1560
+ return MCPError(FString::Printf(TEXT("Invalid actor GUID: '%s'"), *GuidText));
1561
+ }
1562
+ WantedGuids.Add(Guid);
1563
+ }
1564
+ }
1565
+
1566
+ FBox FilterBounds(ForceInit);
1567
+ const bool bHasBoundsFilter = Params->HasField(TEXT("bounds"));
1568
+ if (bHasBoundsFilter)
1569
+ {
1570
+ const TSharedPtr<FJsonObject>* BoundsObject = nullptr;
1571
+ const TSharedPtr<FJsonObject>* MinObject = nullptr;
1572
+ const TSharedPtr<FJsonObject>* MaxObject = nullptr;
1573
+ if (!Params->TryGetObjectField(TEXT("bounds"), BoundsObject) || !BoundsObject ||
1574
+ !(*BoundsObject)->TryGetObjectField(TEXT("min"), MinObject) || !MinObject ||
1575
+ !(*BoundsObject)->TryGetObjectField(TEXT("max"), MaxObject) || !MaxObject)
1576
+ {
1577
+ return MCPError(TEXT("'bounds' must be {min:{x,y,z}, max:{x,y,z}}"));
1578
+ }
1579
+
1580
+ auto ReadVector = [](const TSharedPtr<FJsonObject>& Object, FVector& Out) -> bool
1581
+ {
1582
+ double X = 0.0, Y = 0.0, Z = 0.0;
1583
+ if (!Object->TryGetNumberField(TEXT("x"), X) ||
1584
+ !Object->TryGetNumberField(TEXT("y"), Y) ||
1585
+ !Object->TryGetNumberField(TEXT("z"), Z) ||
1586
+ !FMath::IsFinite(X) || !FMath::IsFinite(Y) || !FMath::IsFinite(Z))
1587
+ {
1588
+ return false;
1589
+ }
1590
+ Out = FVector(X, Y, Z);
1591
+ return true;
1592
+ };
1593
+
1594
+ FVector Min, Max;
1595
+ if (!ReadVector(*MinObject, Min) || !ReadVector(*MaxObject, Max))
1596
+ {
1597
+ return MCPError(TEXT("Every bounds min/max coordinate must be a finite number"));
1598
+ }
1599
+ FilterBounds = FBox(FVector::Min(Min, Max), FVector::Max(Min, Max));
1600
+ }
1601
+
1602
+ TArray<ALandscapeStreamingProxy*> LoadedProxies;
1603
+ TArray<ALandscapeStreamingProxy*> Matches;
1604
+ for (TActorIterator<ALandscapeStreamingProxy> It(World); It; ++It)
1605
+ {
1606
+ ALandscapeStreamingProxy* Proxy = *It;
1607
+ if (!Proxy || Proxy->GetWorld() != World) continue;
1608
+ LoadedProxies.Add(Proxy);
1609
+
1610
+ if (bHasLabelFilter && !WantedLabels.Contains(Proxy->GetActorLabel().ToLower())) continue;
1611
+ if (bHasGuidFilter && !WantedGuids.Contains(Proxy->GetActorGuid())) continue;
1612
+ if (bHasBoundsFilter)
1613
+ {
1614
+ FVector Origin, Extent;
1615
+ Proxy->GetActorBounds(false, Origin, Extent);
1616
+ if (!FBox::BuildAABB(Origin, Extent).Intersect(FilterBounds)) continue;
1617
+ }
1618
+ Matches.Add(Proxy);
1619
+ }
1620
+
1621
+ if (Matches.Num() > MaxActors)
1622
+ {
1623
+ return MCPError(FString::Printf(
1624
+ TEXT("%d loaded LandscapeStreamingProxy actors matched, above the maxActors limit of %d. Narrow actorLabels/guids/bounds or raise maxActors deliberately."),
1625
+ Matches.Num(), MaxActors));
1626
+ }
1627
+ for (ALandscapeStreamingProxy* Proxy : Matches)
1628
+ {
1629
+ if (UPackage* Package = Proxy ? Proxy->GetPackage() : nullptr)
1630
+ {
1631
+ // A later external save would fully load the package first. Establish the
1632
+ // same object residency before any safety baseline or layer-state test.
1633
+ Package->FullyLoad();
1634
+ }
1635
+ }
1636
+
1637
+ struct FRefreshEntry
1638
+ {
1639
+ TSharedPtr<FJsonObject> Json;
1640
+ ALandscapeStreamingProxy* Proxy = nullptr;
1641
+ ALandscape* ParentLandscape = nullptr;
1642
+ FString PackagePath;
1643
+ FString Error;
1644
+ TArray<FLandscapeHeightCollisionSignature> HeightSignatures;
1645
+ int32 LandscapeComponents = 0;
1646
+ int32 CollisionComponents = 0;
1647
+ int32 IneligibleCollisionComponents = 0;
1648
+ int32 ComponentsWithPendingLayerUpdates = 0;
1649
+ int32 OutdatedBefore = 0;
1650
+ int32 OutdatedAfter = 0;
1651
+ bool bTextureResourcesReady = false;
1652
+ bool bLandscapeLayersUpToDate = false;
1653
+ bool bHeightCollisionVerified = false;
1654
+ bool bRefreshed = false;
1655
+ };
1656
+
1657
+ TArray<FRefreshEntry> Entries;
1658
+ TArray<FString> PackagePaths;
1659
+ TMap<ALandscape*, bool> TextureResourcesReady;
1660
+ TMap<ALandscape*, bool> LandscapeLayersUpToDate;
1661
+ for (ALandscapeStreamingProxy* Proxy : Matches)
1662
+ {
1663
+ ALandscape* ParentLandscape = Proxy ? Proxy->GetLandscapeActor() : nullptr;
1664
+ if (ParentLandscape && !LandscapeLayersUpToDate.Contains(ParentLandscape))
1665
+ {
1666
+ // A LayerInfo PostEditChange requests Update_All. PreSave forcibly
1667
+ // flushes that work; on a stale edit-layer stack this can replace valid
1668
+ // collision heights while the package is already being serialized.
1669
+ // Refuse that state instead of discovering it after overwriting the OFPA
1670
+ // package.
1671
+ LandscapeLayersUpToDate.Add(ParentLandscape, ParentLandscape->IsUpToDate());
1672
+ }
1673
+ if (ParentLandscape && LandscapeLayersUpToDate.FindRef(ParentLandscape) &&
1674
+ !TextureResourcesReady.Contains(ParentLandscape))
1675
+ {
1676
+ // BuildPhysicalMaterial depends on resident weightmaps. Do this once per
1677
+ // parent landscape and wait, instead of letting each proxy make a
1678
+ // best-effort non-blocking request in the same frame.
1679
+ TextureResourcesReady.Add(ParentLandscape, ParentLandscape->PrepareTextureResources(true));
1680
+ }
1681
+ }
1682
+ for (TPair<ALandscape*, bool>& Pair : LandscapeLayersUpToDate)
1683
+ {
1684
+ // Texture preparation is allowed to service queued landscape work. Capture
1685
+ // the state that actually exists immediately before the height baseline.
1686
+ Pair.Value = Pair.Key && Pair.Key->IsUpToDate();
1687
+ }
1688
+
1689
+ int32 Refreshed = 0;
1690
+ int32 CollisionComponentsRefreshed = 0;
1691
+ int32 OutdatedBefore = 0;
1692
+
1693
+ for (ALandscapeStreamingProxy* Proxy : Matches)
1694
+ {
1695
+ FRefreshEntry& Entry = Entries.AddDefaulted_GetRef();
1696
+ Entry.Proxy = Proxy;
1697
+ Entry.ParentLandscape = Proxy->GetLandscapeActor();
1698
+ Entry.bLandscapeLayersUpToDate = Entry.ParentLandscape && LandscapeLayersUpToDate.FindRef(Entry.ParentLandscape);
1699
+ Entry.bTextureResourcesReady = Entry.ParentLandscape && TextureResourcesReady.FindRef(Entry.ParentLandscape);
1700
+ Entry.Json = MakeShared<FJsonObject>();
1701
+ Entry.Json->SetStringField(TEXT("label"), Proxy->GetActorLabel());
1702
+ Entry.Json->SetStringField(TEXT("guid"), Proxy->GetActorGuid().ToString());
1703
+ Entry.Json->SetBoolField(TEXT("landscapeLayersUpToDate"), Entry.bLandscapeLayersUpToDate);
1704
+ Entry.Json->SetBoolField(TEXT("textureResourcesReady"), Entry.bTextureResourcesReady);
1705
+
1706
+ if (UPackage* Package = Proxy->GetPackage())
1707
+ {
1708
+ Entry.PackagePath = Package->GetName();
1709
+ Entry.Json->SetStringField(TEXT("package"), Entry.PackagePath);
1710
+ PackagePaths.AddUnique(Entry.PackagePath);
1711
+ }
1712
+
1713
+ TArray<ULandscapeComponent*> Components;
1714
+ Proxy->GetComponents<ULandscapeComponent>(Components);
1715
+ for (ULandscapeComponent* Component : Components)
1716
+ {
1717
+ if (!Component) continue;
1718
+ ++Entry.LandscapeComponents;
1719
+ if (Component && Component->GetLayerUpdateFlagPerMode() != 0)
1720
+ {
1721
+ ++Entry.ComponentsWithPendingLayerUpdates;
1722
+ }
1723
+ if (Component && Component->IsRegistered() && Component->GetCollisionComponent())
1724
+ {
1725
+ ++Entry.CollisionComponents;
1726
+ FLandscapeHeightCollisionSignature Signature;
1727
+ FString SignatureError;
1728
+ if (CaptureHeightCollisionSignature(Component->GetCollisionComponent(), Signature, SignatureError))
1729
+ {
1730
+ Entry.HeightSignatures.Add(MoveTemp(Signature));
1731
+ }
1732
+ else if (Entry.Error.IsEmpty())
1733
+ {
1734
+ Entry.Error = FString::Printf(TEXT("Height collision preflight failed: %s"), *SignatureError);
1735
+ }
1736
+ }
1737
+ else
1738
+ {
1739
+ ++Entry.IneligibleCollisionComponents;
1740
+ }
1741
+ }
1742
+ Entry.Json->SetNumberField(TEXT("landscapeComponents"), Entry.LandscapeComponents);
1743
+ Entry.Json->SetNumberField(TEXT("collisionComponents"), Entry.CollisionComponents);
1744
+ Entry.Json->SetNumberField(TEXT("ineligibleCollisionComponents"), Entry.IneligibleCollisionComponents);
1745
+ Entry.Json->SetNumberField(TEXT("componentsWithPendingLayerUpdates"), Entry.ComponentsWithPendingLayerUpdates);
1746
+ Entry.Json->SetNumberField(TEXT("heightCollisionComponentsCaptured"), Entry.HeightSignatures.Num());
1747
+ if (Entry.Error.IsEmpty())
1748
+ {
1749
+ ValidateLandscapeCollisionPreflight(
1750
+ Entry.LandscapeComponents,
1751
+ Entry.CollisionComponents,
1752
+ Entry.HeightSignatures.Num(),
1753
+ Entry.ComponentsWithPendingLayerUpdates,
1754
+ Entry.bLandscapeLayersUpToDate,
1755
+ Entry.bTextureResourcesReady,
1756
+ Entry.Error);
1757
+ }
1758
+ if (!Entry.Error.IsEmpty())
1759
+ {
1760
+ Entry.Json->SetBoolField(TEXT("refreshed"), false);
1761
+ continue;
1762
+ }
1763
+
1764
+ }
1765
+
1766
+ const bool bBatchHeightPreflightPassed =
1767
+ Entries.Num() == Matches.Num() &&
1768
+ !Entries.ContainsByPredicate([](const FRefreshEntry& Entry)
1769
+ {
1770
+ return !Entry.Error.IsEmpty();
1771
+ });
1772
+ if (!bBatchHeightPreflightPassed)
1773
+ {
1774
+ // The selected proxy set is one safety unit. Updating the apparently-good
1775
+ // subset would leave the loaded landscape internally inconsistent and can
1776
+ // reproduce the stale-cache failure on the next save/reload.
1777
+ for (FRefreshEntry& Entry : Entries)
1778
+ {
1779
+ if (Entry.Error.IsEmpty())
1780
+ {
1781
+ Entry.Error = TEXT("Another matched proxy failed loaded/registered height-collision preflight; no matched proxy was mutated");
1782
+ Entry.Json->SetBoolField(TEXT("refreshed"), false);
1783
+ }
1784
+ }
1785
+ }
1786
+
1787
+ FScopedLandscapePhysicalMaterialRecreateMode RecreateMode;
1788
+ if (!RecreateMode.IsValid())
1789
+ {
1790
+ return MCPError(TEXT("Required console variable landscape.ApplyPhysicalMaterialChangesImmediately was unavailable or could not be forced to zero; a higher-priority override may be active"));
1791
+ }
1792
+
1793
+ for (FRefreshEntry& Entry : Entries)
1794
+ {
1795
+ if (!Entry.Error.IsEmpty()) continue;
1796
+ TArray<ULandscapeComponent*> Components;
1797
+ Entry.Proxy->GetComponents<ULandscapeComponent>(Components);
1798
+ for (ULandscapeComponent* Component : Components)
1799
+ {
1800
+ if (Component && Component->IsRegistered() && Component->GetCollisionComponent())
1801
+ {
1802
+ // UpdateCollisionLayerData invalidates the physical-material task but,
1803
+ // unlike ChangedPhysMaterial, intentionally does not tear down physics
1804
+ // before the asynchronous material output is ready.
1805
+ Component->UpdateCollisionLayerData();
1806
+ }
1807
+ }
1808
+ Entry.OutdatedBefore = Entry.Proxy->GetOudatedPhysicalMaterialComponentsCount();
1809
+ Entry.Json->SetNumberField(TEXT("outdatedBefore"), Entry.OutdatedBefore);
1810
+ // The return type changed between supported UE versions; ignoring it is
1811
+ // intentional. The exported outdated count below is the stable readback.
1812
+ Entry.Proxy->BuildPhysicalMaterial();
1813
+ Entry.bRefreshed = true;
1814
+ Entry.Json->SetBoolField(TEXT("refreshed"), true);
1815
+ ++Refreshed;
1816
+ CollisionComponentsRefreshed += Entry.CollisionComponents;
1817
+ OutdatedBefore += Entry.OutdatedBefore;
1818
+ }
1819
+
1820
+ // BuildPhysicalMaterial advances an asynchronous GPU readback. Re-enter the
1821
+ // exported build method after flushing render commands until it finalizes
1822
+ // every matched proxy, or until there is enough time left to return a useful
1823
+ // failure report before the handler's 600-second bridge timeout. Time spent
1824
+ // preparing texture resources counts too.
1825
+ const double BuildDeadline = StartedAt + 420.0;
1826
+ bool bBuildTimedOut = false;
1827
+ while (Refreshed > 0)
1828
+ {
1829
+ int32 Remaining = 0;
1830
+ for (const FRefreshEntry& Entry : Entries)
1831
+ {
1832
+ if (Entry.bRefreshed) Remaining += Entry.Proxy->GetOudatedPhysicalMaterialComponentsCount();
1833
+ }
1834
+ if (Remaining == 0) break;
1835
+ if (FPlatformTime::Seconds() >= BuildDeadline)
1836
+ {
1837
+ bBuildTimedOut = true;
1838
+ break;
1839
+ }
1840
+
1841
+ FlushRenderingCommands();
1842
+ for (FRefreshEntry& Entry : Entries)
1843
+ {
1844
+ if (Entry.bRefreshed && Entry.Proxy->GetOudatedPhysicalMaterialComponentsCount() > 0)
1845
+ {
1846
+ Entry.Proxy->BuildPhysicalMaterial();
1847
+ }
1848
+ }
1849
+ FPlatformProcess::Sleep(0.001f);
1850
+ }
1851
+ if (Refreshed > 0) FlushRenderingCommands();
1852
+
1853
+ int32 CollisionComponentsRecreateRequested = 0;
1854
+ int32 CollisionComponentsRecreatedAfterBuild = 0;
1855
+ int32 CollisionComponentsUnchangedAfterBuild = 0;
1856
+ int32 OutdatedAfterBuild = 0;
1857
+ auto VerifyEntryHeightCollision = [](
1858
+ FRefreshEntry& Entry,
1859
+ const TCHAR* Phase,
1860
+ bool bWriteJsonField) -> bool
1861
+ {
1862
+ for (const FLandscapeHeightCollisionSignature& Baseline : Entry.HeightSignatures)
1863
+ {
1864
+ if (!IsValid(Baseline.Collision))
1865
+ {
1866
+ Entry.Error = FString::Printf(
1867
+ TEXT("Height collision verification failed %s: %s is no longer valid"),
1868
+ Phase, *Baseline.ComponentPath);
1869
+ if (bWriteJsonField) Entry.Json->SetBoolField(TEXT("heightCollisionVerified"), false);
1870
+ return false;
1871
+ }
1872
+
1873
+ FLandscapeHeightCollisionSignature Current;
1874
+ FString SignatureError;
1875
+ if (!CaptureHeightCollisionSignature(Baseline.Collision, Current, SignatureError) ||
1876
+ !MatchesHeightCollisionSignature(Baseline, Current, SignatureError))
1877
+ {
1878
+ Entry.Error = FString::Printf(
1879
+ TEXT("Height collision verification failed %s: %s"), Phase, *SignatureError);
1880
+ if (bWriteJsonField) Entry.Json->SetBoolField(TEXT("heightCollisionVerified"), false);
1881
+ return false;
1882
+ }
1883
+ }
1884
+ if (bWriteJsonField) Entry.Json->SetBoolField(TEXT("heightCollisionVerified"), true);
1885
+ return true;
1886
+ };
1887
+
1888
+ for (FRefreshEntry& Entry : Entries)
1889
+ {
1890
+ if (!Entry.bRefreshed) continue;
1891
+ Entry.OutdatedAfter = Entry.Proxy->GetOudatedPhysicalMaterialComponentsCount();
1892
+ Entry.Json->SetNumberField(TEXT("outdatedAfterBuild"), Entry.OutdatedAfter);
1893
+ Entry.Json->SetBoolField(TEXT("physicalMaterialCurrentAfterBuild"), Entry.OutdatedAfter == 0);
1894
+ OutdatedAfterBuild += Entry.OutdatedAfter;
1895
+ if (Entry.OutdatedAfter == 0)
1896
+ {
1897
+ // Prove that the material build did not touch raw or live heights before
1898
+ // tearing down the valid heightfield. The scoped CVar above prevents the
1899
+ // engine finalizer from recreating it early.
1900
+ if (!VerifyEntryHeightCollision(Entry, TEXT("after the physical-material build"), false))
1901
+ {
1902
+ continue;
1903
+ }
1904
+
1905
+ // Recreate exactly once, after both dominant-layer and rendered physical
1906
+ // material data are current. RecreateCollision's return value only says
1907
+ // whether a request was made; the full live height tile below is proof
1908
+ // that a valid, unchanged geometry was produced.
1909
+ int32 EntryRecreateRequested = 0;
1910
+ int32 EntryRecreated = 0;
1911
+ int32 EntryUnchanged = 0;
1912
+ for (const FLandscapeHeightCollisionSignature& Baseline : Entry.HeightSignatures)
1913
+ {
1914
+ if (IsValid(Baseline.Collision) && Baseline.Collision->IsRegistered())
1915
+ {
1916
+ ++EntryRecreateRequested;
1917
+ if (Baseline.Collision->RecreateCollision()) ++EntryRecreated;
1918
+ else ++EntryUnchanged;
1919
+ }
1920
+ }
1921
+ Entry.Json->SetNumberField(TEXT("collisionRecreateRequestedAfterBuild"), EntryRecreateRequested);
1922
+ Entry.Json->SetNumberField(TEXT("collisionRecreatedAfterBuild"), EntryRecreated);
1923
+ Entry.Json->SetNumberField(TEXT("collisionUnchangedAfterBuild"), EntryUnchanged);
1924
+ CollisionComponentsRecreateRequested += EntryRecreateRequested;
1925
+ CollisionComponentsRecreatedAfterBuild += EntryRecreated;
1926
+ CollisionComponentsUnchangedAfterBuild += EntryUnchanged;
1927
+ Entry.bHeightCollisionVerified =
1928
+ EntryRecreateRequested == Entry.HeightSignatures.Num() &&
1929
+ EntryRecreated == Entry.HeightSignatures.Num() &&
1930
+ VerifyEntryHeightCollision(Entry, TEXT("after the final collision recreation"), true);
1931
+ if (!Entry.bHeightCollisionVerified && Entry.Error.IsEmpty())
1932
+ {
1933
+ Entry.Error = TEXT("Not every captured collision component was explicitly recreated for the new physical-material mapping");
1934
+ Entry.Json->SetBoolField(TEXT("heightCollisionVerified"), false);
1935
+ }
1936
+ }
1937
+ else
1938
+ {
1939
+ Entry.Error = FString::Printf(
1940
+ TEXT("Physical-material build did not flush %d outdated component(s)%s"),
1941
+ Entry.OutdatedAfter, bBuildTimedOut ? TEXT(" before the timeout") : TEXT(""));
1942
+ }
1943
+ }
1944
+
1945
+ // Read back after the build/recreation so the response reflects the final
1946
+ // in-memory derived state. Persistence is deliberately unsupported here:
1947
+ // ALandscapeProxy::PreSave can mutate edit-layer collision data.
1948
+ int32 OutdatedAfter = 0;
1949
+ int32 PhysicalMaterialsCurrent = 0;
1950
+ for (FRefreshEntry& Entry : Entries)
1951
+ {
1952
+ if (!Entry.bRefreshed) continue;
1953
+ Entry.OutdatedAfter = Entry.Proxy->GetOudatedPhysicalMaterialComponentsCount();
1954
+ Entry.Json->SetNumberField(TEXT("outdatedAfter"), Entry.OutdatedAfter);
1955
+ const bool bPhysicalMaterialCurrent = Entry.OutdatedAfter == 0;
1956
+ Entry.Json->SetBoolField(TEXT("physicalMaterialCurrent"), bPhysicalMaterialCurrent);
1957
+ OutdatedAfter += Entry.OutdatedAfter;
1958
+ if (bPhysicalMaterialCurrent)
1959
+ {
1960
+ ++PhysicalMaterialsCurrent;
1961
+ }
1962
+ else if (Entry.Error.IsEmpty())
1963
+ {
1964
+ Entry.Error = FString::Printf(
1965
+ TEXT("Physical-material state has %d outdated component(s) after the build"),
1966
+ Entry.OutdatedAfter);
1967
+ }
1968
+ }
1969
+ const bool bBatchAccepted = !Matches.IsEmpty() && Entries.Num() == Matches.Num() &&
1970
+ !Entries.ContainsByPredicate([](const FRefreshEntry& Entry)
1971
+ {
1972
+ return !Entry.Error.IsEmpty();
1973
+ });
1974
+ if (!bBatchAccepted)
1975
+ {
1976
+ for (FRefreshEntry& Entry : Entries)
1977
+ {
1978
+ if (Entry.Error.IsEmpty())
1979
+ {
1980
+ Entry.Error = TEXT("Another matched proxy failed the post-build safety check; the matched batch was not accepted and must not be saved");
1981
+ }
1982
+ }
1983
+ }
1984
+
1985
+ TArray<TSharedPtr<FJsonValue>> ActorResults;
1986
+ TArray<FString> FailedPackagePaths;
1987
+ int32 Failed = 0;
1988
+ int32 HeightCollisionComponentsVerified = 0;
1989
+ for (FRefreshEntry& Entry : Entries)
1990
+ {
1991
+ Entry.Json->SetBoolField(TEXT("saved"), false);
1992
+ if (Entry.bHeightCollisionVerified)
1993
+ {
1994
+ HeightCollisionComponentsVerified += Entry.HeightSignatures.Num();
1995
+ }
1996
+ if (!Entry.Error.IsEmpty())
1997
+ {
1998
+ ++Failed;
1999
+ Entry.Json->SetStringField(TEXT("error"), Entry.Error);
2000
+ if (!Entry.PackagePath.IsEmpty()) FailedPackagePaths.AddUnique(Entry.PackagePath);
2001
+ }
2002
+ ActorResults.Add(MakeShared<FJsonValueObject>(Entry.Json));
2003
+ }
2004
+
2005
+ auto ToJsonStrings = [](const TArray<FString>& Strings)
2006
+ {
2007
+ TArray<TSharedPtr<FJsonValue>> Values;
2008
+ Values.Reserve(Strings.Num());
2009
+ for (const FString& String : Strings) Values.Add(MakeShared<FJsonValueString>(String));
2010
+ return Values;
2011
+ };
2012
+
2013
+ auto Result = MCPSuccess();
2014
+ if (Failed > 0 || Matches.IsEmpty()) Result->SetBoolField(TEXT("success"), false);
2015
+ if (Refreshed > 0) MCPSetUpdated(Result);
2016
+ Result->SetStringField(TEXT("world"), World->GetPathName());
2017
+ Result->SetBoolField(TEXT("batchAccepted"), bBatchAccepted);
2018
+ Result->SetBoolField(TEXT("saveSupported"), false);
2019
+ Result->SetBoolField(TEXT("saveRequested"), false);
2020
+ Result->SetNumberField(TEXT("maxActors"), MaxActors);
2021
+ Result->SetNumberField(TEXT("loaded"), LoadedProxies.Num());
2022
+ Result->SetNumberField(TEXT("matched"), Matches.Num());
2023
+ Result->SetNumberField(TEXT("refreshed"), Refreshed);
2024
+ Result->SetNumberField(TEXT("collisionComponentsRefreshed"), CollisionComponentsRefreshed);
2025
+ Result->SetNumberField(TEXT("collisionComponentsRecreateRequested"), CollisionComponentsRecreateRequested);
2026
+ Result->SetNumberField(TEXT("collisionComponentsRecreatedAfterBuild"), CollisionComponentsRecreatedAfterBuild);
2027
+ Result->SetNumberField(TEXT("collisionComponentsUnchangedAfterBuild"), CollisionComponentsUnchangedAfterBuild);
2028
+ Result->SetNumberField(TEXT("heightCollisionComponentsVerified"), HeightCollisionComponentsVerified);
2029
+ Result->SetNumberField(TEXT("physicalMaterialsCurrent"), PhysicalMaterialsCurrent);
2030
+ Result->SetNumberField(TEXT("outdatedBefore"), OutdatedBefore);
2031
+ Result->SetNumberField(TEXT("outdatedAfterBuild"), OutdatedAfterBuild);
2032
+ Result->SetNumberField(TEXT("outdatedAfter"), OutdatedAfter);
2033
+ Result->SetBoolField(TEXT("buildTimedOut"), bBuildTimedOut);
2034
+ Result->SetNumberField(TEXT("textureResourceParents"), TextureResourcesReady.Num());
2035
+ int32 ReadyParents = 0;
2036
+ for (const TPair<ALandscape*, bool>& Pair : TextureResourcesReady) ReadyParents += Pair.Value ? 1 : 0;
2037
+ Result->SetNumberField(TEXT("textureResourceParentsReady"), ReadyParents);
2038
+ int32 UpToDateParents = 0;
2039
+ for (const TPair<ALandscape*, bool>& Pair : LandscapeLayersUpToDate) UpToDateParents += Pair.Value ? 1 : 0;
2040
+ Result->SetNumberField(TEXT("landscapeLayerParents"), LandscapeLayersUpToDate.Num());
2041
+ Result->SetNumberField(TEXT("landscapeLayerParentsUpToDate"), UpToDateParents);
2042
+ Result->SetNumberField(TEXT("saved"), 0);
2043
+ Result->SetNumberField(TEXT("failed"), Failed);
2044
+ Result->SetArrayField(TEXT("packagePaths"), ToJsonStrings(PackagePaths));
2045
+ Result->SetArrayField(TEXT("savedPackagePaths"), TArray<TSharedPtr<FJsonValue>>());
2046
+ Result->SetArrayField(TEXT("failedPackagePaths"), ToJsonStrings(FailedPackagePaths));
2047
+ Result->SetArrayField(TEXT("actors"), ActorResults);
2048
+ FString Note;
2049
+ if (Matches.IsEmpty())
2050
+ {
2051
+ Note = TEXT("No loaded LandscapeStreamingProxy actor matched. Unloaded proxies were not changed; pin them first with level(load_actor_descs), then rerun this action.");
2052
+ }
2053
+ else if (Failed > 0)
2054
+ {
2055
+ Note = FString::Printf(TEXT("%d of %d matched proxies failed; inspect the per-actor errors. Unloaded proxies were not changed."), Failed, Matches.Num());
2056
+ }
2057
+ else
2058
+ {
2059
+ Note = TEXT("Collision and physical-material data were rebuilt in memory and matched packages may now be dirty. No packages were saved. Unloaded proxies were not changed.");
2060
+ }
2061
+ Result->SetStringField(TEXT("note"), Note);
2062
+ return MCPResult(Result);
2063
+ #endif // UE_MCP_HAS_5_8_API
2064
+ }
2065
+
2066
+ #if WITH_DEV_AUTOMATION_TESTS && UE_MCP_HAS_5_8_API
2067
+
2068
+ IMPLEMENT_SIMPLE_AUTOMATION_TEST(
2069
+ FMCPLandscapePhysicalMaterialCollisionHeightSafetyTest,
2070
+ "UE.MCP.Landscape.PhysicalMaterialCollision.HeightSafety",
2071
+ EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
2072
+
2073
+ bool FMCPLandscapePhysicalMaterialCollisionHeightSafetyTest::RunTest(const FString& Parameters)
2074
+ {
2075
+ const TArray<uint16> TerrainRaw = {
2076
+ 32768, 32896, 32640, 33024,
2077
+ 32768, 32960, 32576, 33152
2078
+ };
2079
+ const TArray<float> TerrainLive = {
2080
+ 100.0f, 200.0f, 0.0f, 300.0f,
2081
+ 100.0f, 250.0f, -50.0f, 400.0f
2082
+ };
2083
+ const FTransform Transform(FRotator::ZeroRotator, FVector(0.0, 0.0, 100.0), FVector(100.0));
2084
+
2085
+ FLandscapeHeightCollisionSignature Baseline;
2086
+ FString Error;
2087
+ TestTrue(TEXT("varied raw terrain agrees with its complex and simple live heightfields"),
2088
+ BuildHeightCollisionSignature(TerrainRaw, 4, 2, 4, 2, TerrainLive, Transform, Baseline, Error));
2089
+ TestTrue(TEXT("baseline reports a non-flat height range"),
2090
+ Baseline.LiveMaxWorldZ > Baseline.LiveMinWorldZ);
2091
+
2092
+ const TArray<uint16> FlatRaw = {
2093
+ 32768, 32768, 32768, 32768,
2094
+ 32768, 32768, 32768, 32768
2095
+ };
2096
+ FLandscapeHeightCollisionSignature Invalid;
2097
+ Error.Reset();
2098
+ TestFalse(TEXT("flat raw data cannot masquerade as the original live terrain"),
2099
+ BuildHeightCollisionSignature(FlatRaw, 4, 2, 4, 2, TerrainLive, Transform, Invalid, Error));
2100
+ TestTrue(TEXT("raw/live mismatch names the unsafe sample"), Error.Contains(TEXT("mismatch at sample")));
2101
+
2102
+ TArray<float> FlatSimpleLive = TerrainLive;
2103
+ for (int32 Index = 4; Index < FlatSimpleLive.Num(); ++Index) FlatSimpleLive[Index] = 100.0f;
2104
+ Error.Reset();
2105
+ TestFalse(TEXT("flattened simple live collision is rejected"),
2106
+ BuildHeightCollisionSignature(TerrainRaw, 4, 2, 4, 2, FlatSimpleLive, Transform, Invalid, Error));
2107
+
2108
+ const TArray<float> FlatLive = {
2109
+ 100.0f, 100.0f, 100.0f, 100.0f,
2110
+ 100.0f, 100.0f, 100.0f, 100.0f
2111
+ };
2112
+ FLandscapeHeightCollisionSignature Flattened;
2113
+ Error.Reset();
2114
+ TestTrue(TEXT("internally consistent flat collision can be signed"),
2115
+ BuildHeightCollisionSignature(FlatRaw, 4, 2, 4, 2, FlatLive, Transform, Flattened, Error));
2116
+ TestFalse(TEXT("a flattened raw/live pair cannot match the terrain baseline"),
2117
+ MatchesHeightCollisionSignature(Baseline, Flattened, Error));
2118
+
2119
+ FString PreflightError;
2120
+ TestFalse(TEXT("pending edit-layer work fails preflight"),
2121
+ ValidateLandscapeCollisionPreflight(1, 1, 1, 1, true, true, PreflightError));
2122
+ TestTrue(TEXT("pending-work failure explains PreSave risk"),
2123
+ PreflightError.Contains(TEXT("pending edit-layer updates")));
2124
+ PreflightError.Reset();
2125
+ TestTrue(TEXT("complete idle collision coverage passes preflight"),
2126
+ ValidateLandscapeCollisionPreflight(1, 1, 1, 0, true, true, PreflightError));
2127
+ return true;
2128
+ }
2129
+
2130
+ #endif // WITH_DEV_AUTOMATION_TESTS && UE_MCP_HAS_5_8_API