ue-mcp 1.2.4 → 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 (243) 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/lean-context.js +8 -3
  34. package/dist/lean-context.js.map +1 -1
  35. package/dist/locking.js +11 -2
  36. package/dist/locking.js.map +1 -1
  37. package/dist/project.d.ts +11 -0
  38. package/dist/project.js +23 -2
  39. package/dist/project.js.map +1 -1
  40. package/dist/python-gate.d.ts +95 -0
  41. package/dist/python-gate.js +175 -0
  42. package/dist/python-gate.js.map +1 -0
  43. package/dist/tool-counts.json +16 -16
  44. package/dist/tools/animation.js +288 -24
  45. package/dist/tools/animation.js.map +1 -1
  46. package/dist/tools/asset.js +79 -14
  47. package/dist/tools/asset.js.map +1 -1
  48. package/dist/tools/audio.js +59 -5
  49. package/dist/tools/audio.js.map +1 -1
  50. package/dist/tools/blueprint.js +29 -12
  51. package/dist/tools/blueprint.js.map +1 -1
  52. package/dist/tools/chooser.js +4 -1
  53. package/dist/tools/chooser.js.map +1 -1
  54. package/dist/tools/editor.js +81 -43
  55. package/dist/tools/editor.js.map +1 -1
  56. package/dist/tools/feedback.js +214 -48
  57. package/dist/tools/feedback.js.map +1 -1
  58. package/dist/tools/foliage.js +27 -1
  59. package/dist/tools/foliage.js.map +1 -1
  60. package/dist/tools/gameplay.js +38 -11
  61. package/dist/tools/gameplay.js.map +1 -1
  62. package/dist/tools/gas.js +17 -5
  63. package/dist/tools/gas.js.map +1 -1
  64. package/dist/tools/landscape.js +22 -7
  65. package/dist/tools/landscape.js.map +1 -1
  66. package/dist/tools/level.js +322 -51
  67. package/dist/tools/level.js.map +1 -1
  68. package/dist/tools/material.js +10 -4
  69. package/dist/tools/material.js.map +1 -1
  70. package/dist/tools/niagara.js +3 -2
  71. package/dist/tools/niagara.js.map +1 -1
  72. package/dist/tools/pcg.js +6 -5
  73. package/dist/tools/pcg.js.map +1 -1
  74. package/dist/tools/project.js +47 -20
  75. package/dist/tools/project.js.map +1 -1
  76. package/dist/types.d.ts +21 -0
  77. package/dist/types.js +46 -3
  78. package/dist/types.js.map +1 -1
  79. package/dist/ue-mcp.default.yml +493 -125
  80. package/package.json +2 -2
  81. package/plugin/ue_mcp_bridge/NATIVE_RUNTIME_VISIBILITY.md +53 -0
  82. package/plugin/ue_mcp_bridge/NATIVE_STATIC_MESH_USAGE_PROGRESS.md +115 -0
  83. package/plugin/ue_mcp_bridge/README_MassHandlers.md +97 -0
  84. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/BridgeServer.cpp +14 -0
  85. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/GameThreadExecutor.cpp +21 -4
  86. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/GameThreadExecutor.h +9 -0
  87. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/HandlerJsonProperty.h +395 -69
  88. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers.cpp +79 -3
  89. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers.h +25 -0
  90. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_ControlRig.cpp +3 -3
  91. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_ControlRigSequencer.cpp +3061 -0
  92. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_IKRetargeterAuthoring.cpp +961 -0
  93. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_IKRigAuthoring.cpp +900 -0
  94. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Pose.cpp +769 -0
  95. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Sequence.cpp +44 -3
  96. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_SkeletalLive.cpp +217 -11
  97. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_StateMachine.cpp +551 -24
  98. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Validation.cpp +735 -0
  99. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers.cpp +352 -73
  100. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers.h +8 -0
  101. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkProperties.cpp +1 -1
  102. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkRead.cpp +503 -0
  103. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkRead.h +21 -0
  104. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkUpsert.cpp +2 -2
  105. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Enum.cpp +2 -2
  106. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Geometry.cpp +902 -0
  107. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Geometry.h +33 -0
  108. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Import.cpp +478 -134
  109. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Mesh.cpp +5 -5
  110. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_MeshBoolean.cpp +933 -0
  111. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_MeshBoolean.h +35 -0
  112. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Redirectors.cpp +478 -0
  113. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_RenderTarget.cpp +1 -1
  114. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Sockets.cpp +4 -4
  115. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Struct.cpp +2 -2
  116. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Subobject.cpp +319 -0
  117. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AudioHandlers_SoundCue.cpp +3 -3
  118. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers.cpp +482 -35
  119. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers.h +10 -0
  120. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Collision.cpp +502 -0
  121. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Collision.h +43 -0
  122. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Functions.cpp +89 -7
  123. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Graph.cpp +17 -51
  124. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Internal.h +107 -0
  125. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Properties.cpp +10 -10
  126. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Search.cpp +648 -0
  127. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DemoHandlers.cpp +5 -1
  128. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DiffHandlers.cpp +252 -1
  129. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DiffHandlers.h +10 -2
  130. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers.cpp +252 -41
  131. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers.h +4 -0
  132. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_Build.cpp +250 -27
  133. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_PIE.cpp +303 -125
  134. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_PIERuntime.cpp +73 -50
  135. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_RuntimeVisibility.cpp +757 -0
  136. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers.cpp +11 -2
  137. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers.h +3 -0
  138. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers_Batch.cpp +419 -0
  139. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers.cpp +39 -161
  140. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers.h +66 -1
  141. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_BTAuthoring.cpp +1323 -0
  142. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_BehaviorTree.cpp +1211 -0
  143. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.cpp +2 -0
  144. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.h +60 -0
  145. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers_Runtime.cpp +363 -12
  146. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/HandlerEditorState.h +84 -0
  147. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.cpp +1288 -36
  148. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.h +3 -0
  149. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers_Sculpt.cpp +41 -16
  150. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers.cpp +959 -213
  151. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers.h +66 -0
  152. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_BatchWrite.cpp +1571 -0
  153. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Convert.cpp +316 -0
  154. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Inspection.cpp +280 -0
  155. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_InstanceProjection.cpp +666 -0
  156. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_InstanceProjection_Internal.h +63 -0
  157. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Lights.cpp +43 -12
  158. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_MultiLevel.cpp +13 -47
  159. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_PostProcess.cpp +527 -0
  160. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Query.cpp +1169 -0
  161. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Refresh.cpp +723 -0
  162. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Save.cpp +325 -0
  163. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Spatial.cpp +360 -0
  164. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Trace.cpp +178 -58
  165. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Transient.cpp +337 -0
  166. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Volumes.cpp +25 -12
  167. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_WorldPartition.cpp +10 -9
  168. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_WorldPartitionSettings.cpp +686 -0
  169. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MassHandlers.cpp +467 -0
  170. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MassHandlers.h +22 -0
  171. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers.cpp +452 -175
  172. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers.h +5 -0
  173. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers_Build.cpp +597 -0
  174. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/NiagaraHandlers.cpp +17 -15
  175. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PCGHandlers.cpp +38 -168
  176. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PhysicsHandlers.cpp +39 -30
  177. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.cpp +392 -78
  178. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.h +2 -0
  179. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers_Internal.h +83 -0
  180. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SkeletalMeshHandlers.cpp +200 -0
  181. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SkeletalMeshHandlers.h +15 -0
  182. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SplineHandlers.cpp +19 -12
  183. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/StateTreeHandlers.cpp +31 -1
  184. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers.cpp +307 -69
  185. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers.h +84 -0
  186. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers_Extraction.cpp +8 -41
  187. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers_Properties.cpp +33 -52
  188. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/ActorSelectorTests.cpp +344 -0
  189. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AnimationControlRigTimelineTests.cpp +60 -0
  190. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AnimationPoseTests.cpp +115 -0
  191. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetBulkReadTests.cpp +177 -0
  192. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetDeleteGuardTests.cpp +276 -0
  193. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetPathResolutionTests.cpp +243 -0
  194. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetPropertyPersistTests.cpp +213 -0
  195. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetSubobjectTests.cpp +235 -0
  196. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AutomationRunnerTests.cpp +72 -0
  197. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BehaviorTreeAuthoringTests.cpp +526 -0
  198. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BehaviorTreeNodeTests.cpp +334 -0
  199. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BlueprintHandlerSurfaceTests.cpp +362 -0
  200. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BridgeProtocolTests.cpp +14 -0
  201. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/CollisionQueryTests.cpp +288 -0
  202. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/DataTableRowWriteTests.cpp +453 -0
  203. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/EditorFunctionCallTests.cpp +223 -0
  204. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/FoliageBatchTests.cpp +147 -0
  205. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/GameThreadGateTests.cpp +82 -0
  206. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/GasLiveAttributeTests.cpp +183 -0
  207. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LandscapeSampleTests.cpp +119 -0
  208. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelBatchWriteTests.cpp +176 -0
  209. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelComponentTreeInstanceCountTests.cpp +189 -0
  210. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelConvertBrushesTests.cpp +100 -0
  211. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelInstanceProjectionTests.cpp +265 -0
  212. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelPostProcessOverrideTests.cpp +258 -0
  213. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelQueryComponentsTests.cpp +195 -0
  214. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelRefreshTests.cpp +142 -0
  215. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelSaveReportTests.cpp +125 -0
  216. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelScriptHandlersTests.cpp +42 -0
  217. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelStaticMeshUsageTests.cpp +412 -0
  218. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelTraceWorldScopeTests.cpp +133 -0
  219. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelTransientActorTests.cpp +205 -0
  220. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelWorldPartitionSettingsTests.cpp +168 -0
  221. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MCPInstancedStructPathTestTypes.h +51 -0
  222. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MCPInstancedStructPathTests.cpp +379 -0
  223. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MaterialBuildTests.cpp +238 -0
  224. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MeshBooleanTests.cpp +170 -0
  225. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MeshGeometryTests.cpp +116 -0
  226. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/PackageSaveExtensionTests.cpp +161 -0
  227. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/PackageWriteGuardTests.cpp +253 -0
  228. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/RuntimeVisibilityTests.cpp +143 -0
  229. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SequencerHandlerTests.cpp +660 -0
  230. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SequencerScrubTests.cpp +50 -0
  231. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SkeletonDiffTests.cpp +685 -0
  232. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/WidgetBlueprintResolveTests.cpp +145 -0
  233. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/UE_MCP_Bridge.cpp +80 -0
  234. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerFunctionCall.h +509 -0
  235. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerQuery.h +407 -0
  236. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerUtils.h +1008 -77
  237. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/UE_MCP_Bridge.Build.cs +87 -0
  238. package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Private/MCPEngineStatus.cpp +152 -14
  239. package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Private/Tests/MCPEngineStatusTests.cpp +65 -0
  240. package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Public/MCPEngineStatus.h +32 -1
  241. package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/UE_MCP_BridgeStatus.Build.cs +2 -0
  242. package/skills/ue-mcp-animation/SKILL.md +154 -0
  243. package/skills/ue-mcp-blueprint/SKILL.md +71 -0
@@ -14,6 +14,9 @@
14
14
  #include "Components/AudioComponent.h"
15
15
  #include "Sound/SoundBase.h"
16
16
  #include "Components/InstancedStaticMeshComponent.h"
17
+ // #986: get_component_tree distinguishes HISM from plain ISM, because per
18
+ // instance culling and LOD change what an edit to one costs.
19
+ #include "Components/HierarchicalInstancedStaticMeshComponent.h"
17
20
  #include "Engine/StaticMesh.h"
18
21
  #include "Engine/SkeletalMesh.h"
19
22
  #include "Animation/SkeletalMeshActor.h"
@@ -86,6 +89,12 @@
86
89
  #include "Engine/SkeletalMesh.h"
87
90
  #include "Materials/MaterialInterface.h"
88
91
  #include "HandlerJsonProperty.h"
92
+ #include "Engine/Blueprint.h"
93
+ #include "Engine/LevelScriptBlueprint.h"
94
+ #include "EdGraph/EdGraph.h"
95
+ #include "EdGraph/EdGraphNode.h"
96
+ #include "Kismet2/BlueprintEditorUtils.h"
97
+ #include "Kismet2/KismetEditorUtilities.h"
89
98
 
90
99
  void FLevelHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
91
100
  {
@@ -98,6 +107,10 @@ void FLevelHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
98
107
  Registry.RegisterHandler(TEXT("get_component_tree"), &GetComponentTree);
99
108
  Registry.RegisterHandler(TEXT("get_relative_transform"), &GetRelativeTransform);
100
109
  Registry.RegisterHandler(TEXT("get_current_level"), &GetCurrentLevel);
110
+ // #964: LevelHandlers_Save.cpp. Saves through the same package path
111
+ // editor(save_dirty) uses, so the two cannot disagree about one package,
112
+ // and reports the package, the file and the engine's own reason on failure.
113
+ Registry.RegisterHandler(TEXT("save_level"), &SaveLevel);
101
114
  Registry.RegisterHandler(TEXT("list_levels"), &ListLevels);
102
115
  Registry.RegisterHandler(TEXT("get_selected_actors"), &GetSelectedActors);
103
116
  Registry.RegisterHandler(TEXT("list_volumes"), &ListVolumes);
@@ -111,7 +124,9 @@ void FLevelHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
111
124
  Registry.RegisterHandler(TEXT("add_component_to_actor"), &AddComponentToActor);
112
125
  Registry.RegisterHandler(TEXT("remove_component_from_actor"), &RemoveComponentFromActor);
113
126
  Registry.RegisterHandler(TEXT("load_level"), &LoadLevel);
127
+ Registry.RegisterHandler(TEXT("clear_level_script"), &ClearLevelScript);
114
128
  Registry.RegisterHandler(TEXT("set_component_property"), &SetComponentProperty);
129
+ Registry.RegisterHandler(TEXT("nudge_component"), &NudgeComponent);
115
130
  Registry.RegisterHandler(TEXT("get_component_details"), &GetComponentDetails);
116
131
  Registry.RegisterHandler(TEXT("set_actor_material"), &SetActorMaterial);
117
132
  Registry.RegisterHandler(TEXT("set_volume_properties"), &SetVolumeProperties);
@@ -120,6 +135,12 @@ void FLevelHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
120
135
  Registry.RegisterHandler(TEXT("set_fog_properties"), &SetFogProperties);
121
136
  Registry.RegisterHandler(TEXT("get_actors_by_class"), &GetActorsByClass);
122
137
  Registry.RegisterHandler(TEXT("get_actors_by_component_class"), &GetActorsByComponentClass);
138
+ // Same budget as query_components, and for the same reason: this is a full
139
+ // TActorIterator pass on the game thread. It additionally sorts every actor
140
+ // by path name and sorts each actor's components, so it is the heavier of
141
+ // the two whole-map scans and had no business inheriting the 30 second
142
+ // default. Mirrored in src/bridge-timeouts.ts, which a parity test checks.
143
+ Registry.RegisterHandlerWithTimeout(TEXT("summarize_static_mesh_usage"), &SummarizeStaticMeshUsage, 300.0f);
123
144
  Registry.RegisterHandler(TEXT("count_actors_by_class"), &CountActorsByClass);
124
145
  Registry.RegisterHandler(TEXT("get_runtime_virtual_texture_summary"), &GetRVTSummary);
125
146
  Registry.RegisterHandler(TEXT("set_water_body_property"), &SetWaterBodyProperty);
@@ -127,6 +148,7 @@ void FLevelHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
127
148
  Registry.RegisterHandler(TEXT("resolve_actor"), &ResolveActor);
128
149
  Registry.RegisterHandler(TEXT("set_actor_property"), &SetActorProperty);
129
150
  Registry.RegisterHandler(TEXT("line_trace"), &LineTrace);
151
+ Registry.RegisterHandler(TEXT("bulk_line_trace"), &BulkLineTrace);
130
152
  // #453: per-actor motion snapshot for telemetry probes. Reads location,
131
153
  // rotation, velocity, angular velocity, scale, and ground state in one
132
154
  // call. Caller is expected to invoke at the desired sample interval.
@@ -139,6 +161,11 @@ void FLevelHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
139
161
  Registry.RegisterHandler(TEXT("get_instance_transforms"), &GetInstanceTransforms);
140
162
  Registry.RegisterHandler(TEXT("update_instance_transform"), &UpdateInstanceTransform);
141
163
  Registry.RegisterHandler(TEXT("remove_instance"), &RemoveInstance);
164
+ // Native bridge method only in this plugin-scoped change. It appears in
165
+ // get_bridge_capabilities.actions and can be called directly over JSON-RPC
166
+ // (or a UeMcpTask bridge.call). A first-class category action also requires a
167
+ // server schema wrapper, which intentionally lives outside this plugin.
168
+ Registry.RegisterHandlerWithTimeout(TEXT("snap_instances_to_surface"), &SnapInstancesToSurface, 300.0f);
142
169
  // #696: enable + force-build Nanite on a static mesh.
143
170
  Registry.RegisterHandler(TEXT("set_nanite_settings"), &SetNaniteSettings);
144
171
  Registry.RegisterHandler(TEXT("get_nanite_info"), &GetNaniteInfo);
@@ -147,6 +174,11 @@ void FLevelHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
147
174
  Registry.RegisterHandler(TEXT("place_skeletal_actor"), &SpawnSkeletalMeshActor);
148
175
  // #666: add a material blendable to a PostProcessVolume.
149
176
  Registry.RegisterHandler(TEXT("add_post_process_blendable"), &AddPostProcessBlendable);
177
+ // #950: LevelHandlers_PostProcess.cpp. The value half and the bOverride_ half
178
+ // of FPostProcessSettings, written together.
179
+ Registry.RegisterHandler(TEXT("set_post_process_settings"), &SetPostProcessSettings);
180
+ Registry.RegisterHandler(TEXT("get_post_process_settings"), &GetPostProcessSettings);
181
+ Registry.RegisterHandler(TEXT("set_fixed_exposure"), &SetFixedExposure);
150
182
  // #637: export a selected actor's mesh to FBX + metadata sidecar.
151
183
  Registry.RegisterHandler(TEXT("export_actor_fbx"), &ExportActorFbx);
152
184
  Registry.RegisterHandler(TEXT("snap_actor_to_floor"), &SnapActorToFloor);
@@ -155,6 +187,16 @@ void FLevelHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
155
187
  Registry.RegisterHandler(TEXT("set_actor_folder_path"), &SetActorFolderPath);
156
188
  Registry.RegisterHandler(TEXT("list_actor_descs"), &ListActorDescs);
157
189
  Registry.RegisterHandlerWithTimeout(TEXT("load_actor_descs"), &LoadActorDescs, 300.0f);
190
+ // #985: LevelHandlers_WorldPartitionSettings.cpp. The streaming knobs and
191
+ // the runtime cell transformer stack live with the other World Partition
192
+ // actions rather than in a category of their own.
193
+ Registry.RegisterHandler(TEXT("get_world_partition_settings"), &GetWorldPartitionSettings);
194
+ Registry.RegisterHandler(TEXT("set_world_partition_settings"), &SetWorldPartitionSettings);
195
+ Registry.RegisterHandler(TEXT("add_runtime_cell_transformer"), &AddRuntimeCellTransformer);
196
+ // #985: bulk HLOD layer assignment. A whole-map selector, so it takes the
197
+ // same 300 second budget as the other batch writes. Mirrored in
198
+ // src/bridge-timeouts.ts, which a parity test checks.
199
+ Registry.RegisterHandlerWithTimeout(TEXT("set_actor_hlod_layer"), &SetActorHLODLayer, 300.0f);
158
200
  Registry.RegisterHandler(TEXT("add_actor_tag"), &AddActorTag);
159
201
  Registry.RegisterHandler(TEXT("remove_actor_tag"), &RemoveActorTag);
160
202
  Registry.RegisterHandler(TEXT("set_actor_tags"), &SetActorTags);
@@ -173,6 +215,31 @@ void FLevelHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
173
215
  Registry.RegisterHandler(TEXT("spawn_grid"), &SpawnGrid);
174
216
  Registry.RegisterHandler(TEXT("batch_translate"), &BatchTranslate);
175
217
  Registry.RegisterHandler(TEXT("place_actors_batch"), &PlaceActorsBatch);
218
+ // #910/#943/#912: the general editor-side component query. A whole-map
219
+ // scan with a projection can take a while on a 4,000 actor level, so it
220
+ // gets its own timeout rather than the 30 second default.
221
+ Registry.RegisterHandlerWithTimeout(TEXT("query_components"), &QueryComponents, 300.0f);
222
+ // #984/#941/#907/#987: level-wide writes driven by an editor-side selector.
223
+ // Each can touch thousands of actors, so each gets its own timeout.
224
+ Registry.RegisterHandlerWithTimeout(TEXT("batch_set_actor_properties"), &BatchSetActorProperties, 300.0f);
225
+ Registry.RegisterHandlerWithTimeout(TEXT("bulk_set_component_property"), &BulkSetComponentProperty, 300.0f);
226
+ Registry.RegisterHandlerWithTimeout(TEXT("remove_components_by_class"), &RemoveComponentsByClass, 300.0f);
227
+ Registry.RegisterHandlerWithTimeout(TEXT("spawn_actors_batch"), &SpawnActorsBatch, 300.0f);
228
+ // #944/#915/#914: refresh state the editor is caching, and read the bounds
229
+ // a caller needs to check the result.
230
+ Registry.RegisterHandlerWithTimeout(TEXT("rerun_construction_scripts"), &RerunConstruction, 300.0f);
231
+ Registry.RegisterHandlerWithTimeout(TEXT("recreate_physics_state"), &RecreatePhysicsState, 300.0f);
232
+ Registry.RegisterHandler(TEXT("test_component_overlap"), &TestComponentOverlap);
233
+ // #911: BSP to StaticMesh. Generating meshes for hundreds of brushes takes
234
+ // far longer than the default handler timeout.
235
+ Registry.RegisterHandlerWithTimeout(TEXT("convert_brushes_to_static_mesh"), &ConvertBrushesToStaticMesh, 600.0f);
236
+ // #946: component-level material overrides on placed actors.
237
+ Registry.RegisterHandlerWithTimeout(TEXT("set_component_materials"), &SetComponentMaterials, 300.0f);
238
+ // #956: a transient verification subject, and the two actions that keep it
239
+ // from being left behind.
240
+ Registry.RegisterHandler(TEXT("spawn_transient_actor"), &SpawnTransientActor);
241
+ Registry.RegisterHandler(TEXT("destroy_transient_actor"), &DestroyTransientActor);
242
+ Registry.RegisterHandler(TEXT("list_transient_actors"), &ListTransientActors);
176
243
  }
177
244
 
178
245
  TSharedPtr<FJsonValue> FLevelHandlers::GetOutliner(const TSharedPtr<FJsonObject>& Params)
@@ -183,6 +250,13 @@ TSharedPtr<FJsonValue> FLevelHandlers::GetOutliner(const TSharedPtr<FJsonObject>
183
250
 
184
251
  FString ClassFilter = OptionalString(Params, TEXT("classFilter"));
185
252
  FString NameFilter = OptionalString(Params, TEXT("nameFilter"));
253
+ // #911: classFilter has always been a case-sensitive substring on the class
254
+ // name, which cannot express "only this exact class". Combined with the
255
+ // folder filters below, that is what forced a get_actor_details round trip
256
+ // per entry to narrow a folder to one class.
257
+ const bool bExactClass = OptionalBool(Params, TEXT("exactClass"), false);
258
+ const FString FolderPathFilter = OptionalString(Params, TEXT("folderPath"));
259
+ const FString FolderPathPrefixFilter = OptionalString(Params, TEXT("folderPathPrefix"));
186
260
  // Default 50 keeps us snappy on World Partition projects whose levels
187
261
  // contain hundreds of streaming-proxy / HLOD actors. Callers who need the
188
262
  // full list can pass a larger limit explicitly.
@@ -221,14 +295,36 @@ TSharedPtr<FJsonValue> FLevelHandlers::GetOutliner(const TSharedPtr<FJsonObject>
221
295
 
222
296
  FString ActorLabel = Actor->GetActorLabel();
223
297
 
224
- if (!ClassFilter.IsEmpty() && !ActorClass.Contains(ClassFilter))
298
+ if (!ClassFilter.IsEmpty())
225
299
  {
226
- continue;
300
+ const bool bClassMatches = bExactClass
301
+ ? ActorClass.Equals(ClassFilter, ESearchCase::IgnoreCase)
302
+ : ActorClass.Contains(ClassFilter);
303
+ if (!bClassMatches)
304
+ {
305
+ continue;
306
+ }
227
307
  }
228
308
  if (!NameFilter.IsEmpty() && !ActorName.Contains(NameFilter) && !ActorLabel.Contains(NameFilter))
229
309
  {
230
310
  continue;
231
311
  }
312
+ if (!FolderPathFilter.IsEmpty() || !FolderPathPrefixFilter.IsEmpty())
313
+ {
314
+ const FString Folder = Actor->GetFolderPath().ToString();
315
+ if (!FolderPathFilter.IsEmpty() && !Folder.Equals(FolderPathFilter, ESearchCase::IgnoreCase))
316
+ {
317
+ continue;
318
+ }
319
+ // A folder prefix matches the folder itself and everything nested
320
+ // under it, so "Gameplay" does not also match "GameplayOld".
321
+ if (!FolderPathPrefixFilter.IsEmpty() &&
322
+ !Folder.Equals(FolderPathPrefixFilter, ESearchCase::IgnoreCase) &&
323
+ !Folder.StartsWith(FolderPathPrefixFilter + TEXT("/"), ESearchCase::IgnoreCase))
324
+ {
325
+ continue;
326
+ }
327
+ }
232
328
 
233
329
  #if WITH_EDITOR
234
330
  const bool bEditorHidden = Actor->IsTemporarilyHiddenInEditor();
@@ -446,22 +542,29 @@ TSharedPtr<FJsonValue> FLevelHandlers::PlaceActor(const TSharedPtr<FJsonObject>&
446
542
 
447
543
  TSharedPtr<FJsonValue> FLevelHandlers::DeleteActor(const TSharedPtr<FJsonObject>& Params)
448
544
  {
449
- FString ActorLabel;
450
- if (auto Err = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return Err;
545
+ FString Selector;
546
+ if (auto Err = RequireStringAlt(Params, TEXT("actorLabel"), TEXT("actorPath"), Selector)) return Err;
451
547
 
452
548
  REQUIRE_EDITOR_WORLD(World);
453
549
 
454
- AActor* ActorToDelete = FindActorByLabel(World, ActorLabel);
550
+ // #983: a duplicate label is refused rather than deleted at random. The
551
+ // miss stays idempotent, but "already deleted" would be a lie when three
552
+ // actors carry the label and all three are still there.
553
+ TSharedPtr<FJsonValue> ActorErr;
554
+ AActor* ActorToDelete = MCPResolveActor(World, Params, ActorErr);
555
+ if (!ActorToDelete && MCPIsAmbiguousActorError(ActorErr)) return ActorErr;
455
556
 
456
557
  // Idempotent: deleting a non-existent actor is a no-op, not an error.
457
558
  if (!ActorToDelete)
458
559
  {
459
560
  auto Result = MCPSuccess();
460
- Result->SetStringField(TEXT("actorLabel"), ActorLabel);
561
+ Result->SetStringField(TEXT("actorLabel"), Selector);
461
562
  Result->SetBoolField(TEXT("alreadyDeleted"), true);
462
563
  return MCPResult(Result);
463
564
  }
464
565
 
566
+ const FString ActorLabel = ActorToDelete->GetActorLabel();
567
+
465
568
  World->DestroyActor(ActorToDelete);
466
569
 
467
570
  auto Result = MCPSuccess();
@@ -474,14 +577,8 @@ TSharedPtr<FJsonValue> FLevelHandlers::DeleteActor(const TSharedPtr<FJsonObject>
474
577
 
475
578
  TSharedPtr<FJsonValue> FLevelHandlers::GetActorDetails(const TSharedPtr<FJsonObject>& Params)
476
579
  {
477
- FString ActorLabel;
478
- FString ActorPath;
479
- bool bHasLabel = Params->TryGetStringField(TEXT("actorLabel"), ActorLabel);
480
- bool bHasPath = Params->TryGetStringField(TEXT("actorPath"), ActorPath);
481
- if (!bHasLabel && !bHasPath)
482
- {
483
- return MCPError(TEXT("Missing 'actorLabel' or 'actorPath' parameter"));
484
- }
580
+ FString Selector;
581
+ if (auto Err = RequireStringAlt(Params, TEXT("actorLabel"), TEXT("actorPath"), Selector)) return Err;
485
582
 
486
583
  // World selection: "editor" (default) or "pie" (#111)
487
584
  // #778: this hand-rolled loop took the FIRST PIE context, i.e. the server,
@@ -499,17 +596,26 @@ TSharedPtr<FJsonValue> FLevelHandlers::GetActorDetails(const TSharedPtr<FJsonObj
499
596
  if (!World) return MCPError(TEXT("No editor world available"));
500
597
  }
501
598
 
502
- AActor* Actor = FindActorByLabelOrPath(World, bHasLabel ? ActorLabel : FString(), bHasPath ? ActorPath : FString());
503
- if (!Actor)
504
- {
505
- return MCPError(FString::Printf(TEXT("Actor not found: %s"), bHasPath ? *ActorPath : *ActorLabel));
506
- }
599
+ // LabelOrName, not label alone: a caller often has an internal name rather
600
+ // than a label, because a PIE-spawned actor has no label worth guessing.
601
+ // The label tier is still exhausted first and the name tier refuses on
602
+ // ambiguity, so accepting the name cannot reintroduce a silent pick (#983).
603
+ FMCPActorSelector ActorSel;
604
+ ActorSel.Match = EMCPActorMatch::LabelOrName;
605
+ ActorSel.WorldLabel = World->IsGameWorld() ? TEXT("PIE") : TEXT("editor");
606
+ TSharedPtr<FJsonValue> ActorErr;
607
+ AActor* Actor = MCPResolveActor(World, Params, ActorErr, ActorSel);
608
+ if (!Actor) return ActorErr;
507
609
 
508
610
  auto Result = MCPSuccess();
509
611
  Result->SetStringField(TEXT("label"), Actor->GetActorLabel());
510
612
  Result->SetStringField(TEXT("name"), Actor->GetName());
511
613
  Result->SetStringField(TEXT("class"), Actor->GetClass()->GetName());
512
614
  Result->SetStringField(TEXT("path"), Actor->GetPathName());
615
+ // #983: the same value under the name the selector uses, so the round trip
616
+ // back into any actor-targeting action is a copy of one field.
617
+ Result->SetStringField(TEXT("actorPath"), Actor->GetPathName());
618
+ Result->SetStringField(TEXT("actorLabel"), Actor->GetActorLabel());
513
619
  Result->SetStringField(TEXT("folderPath"), Actor->GetFolderPath().ToString());
514
620
 
515
621
  FVector Location = Actor->GetActorLocation();
@@ -567,10 +673,26 @@ TSharedPtr<FJsonValue> FLevelHandlers::GetActorDetails(const TSharedPtr<FJsonObj
567
673
  P->SetStringField(TEXT("name"), Prop->GetName());
568
674
  P->SetStringField(TEXT("type"), Prop->GetCPPType());
569
675
 
570
- FString ValueStr;
571
- const void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(Actor);
572
- Prop->ExportText_Direct(ValueStr, ValuePtr, ValuePtr, Actor, PPF_None);
573
- P->SetStringField(TEXT("value"), ValueStr);
676
+ // #927: a UPROPERTY declared as a C-style fixed array, `int32 Foo[3]`,
677
+ // is ONE FProperty with ArrayDim == 3, not three properties. Exporting
678
+ // it without an index writes element 0 and stops, and the value then
679
+ // reads as an ordinary scalar with the remaining elements invisible.
680
+ //
681
+ // This is a general serialization bug, not a navmesh one. It was
682
+ // noticed on RecastNavMesh's NavMeshResolutionParams, a three-element
683
+ // fixed array holding the Low, Default and High generation tiers, and
684
+ // reporting only the Low tier as if it were the whole property sent a
685
+ // user tuning cell sizes against numbers Recast was not using. Any
686
+ // fixed array on any class had the same problem.
687
+ //
688
+ // MCPExportPropertyValue returns a JSON array of one string per
689
+ // element when ArrayDim > 1 and a plain string otherwise, so the two
690
+ // cases stay distinguishable rather than being conflated.
691
+ P->SetField(TEXT("value"), MCPExportPropertyValue(Prop, Actor));
692
+ if (MCPPropertyIsFixedArray(Prop))
693
+ {
694
+ P->SetNumberField(TEXT("arrayDim"), Prop->ArrayDim);
695
+ }
574
696
  PropsArr.Add(MakeShared<FJsonValueObject>(P));
575
697
  }
576
698
  Result->SetArrayField(TEXT("properties"), PropsArr);
@@ -596,14 +718,8 @@ TSharedPtr<FJsonValue> FLevelHandlers::GetActorDetails(const TSharedPtr<FJsonObj
596
718
  // - reflected UPROPERTY name/type/value when includeProperties=true
597
719
  TSharedPtr<FJsonValue> FLevelHandlers::GetComponentTree(const TSharedPtr<FJsonObject>& Params)
598
720
  {
599
- FString ActorLabel;
600
- FString ActorPath;
601
- const bool bHasLabel = Params->TryGetStringField(TEXT("actorLabel"), ActorLabel);
602
- const bool bHasPath = Params->TryGetStringField(TEXT("actorPath"), ActorPath);
603
- if (!bHasLabel && !bHasPath)
604
- {
605
- return MCPError(TEXT("Missing 'actorLabel' or 'actorPath' parameter"));
606
- }
721
+ FString Selector;
722
+ if (auto Err = RequireStringAlt(Params, TEXT("actorLabel"), TEXT("actorPath"), Selector)) return Err;
607
723
 
608
724
  const FString WorldScope = OptionalString(Params, TEXT("world"), TEXT("editor"));
609
725
  UWorld* World = ResolveWorldFromParams(Params, *WorldScope);
@@ -612,11 +728,16 @@ TSharedPtr<FJsonValue> FLevelHandlers::GetComponentTree(const TSharedPtr<FJsonOb
612
728
  return MCPError(FString::Printf(TEXT("World '%s' not available"), *WorldScope));
613
729
  }
614
730
 
615
- AActor* Actor = FindActorByLabelOrPath(World, bHasLabel ? ActorLabel : FString(), bHasPath ? ActorPath : FString());
616
- if (!Actor)
617
- {
618
- return MCPError(FString::Printf(TEXT("Actor not found: %s"), bHasPath ? *ActorPath : *ActorLabel));
619
- }
731
+ // LabelOrName, not label alone: a caller often has an internal name rather
732
+ // than a label, because a PIE-spawned actor has no label worth guessing.
733
+ // The label tier is still exhausted first and the name tier refuses on
734
+ // ambiguity, so accepting the name cannot reintroduce a silent pick (#983).
735
+ FMCPActorSelector ActorSel;
736
+ ActorSel.Match = EMCPActorMatch::LabelOrName;
737
+ ActorSel.WorldLabel = World->IsGameWorld() ? TEXT("PIE") : TEXT("editor");
738
+ TSharedPtr<FJsonValue> ActorErr;
739
+ AActor* Actor = MCPResolveActor(World, Params, ActorErr, ActorSel);
740
+ if (!Actor) return ActorErr;
620
741
 
621
742
  const bool bIncludeProperties = OptionalBool(Params, TEXT("includeProperties"));
622
743
  const FString PropertyFilter = OptionalString(Params, TEXT("componentClass"));
@@ -728,6 +849,24 @@ TSharedPtr<FJsonValue> FLevelHandlers::GetComponentTree(const TSharedPtr<FJsonOb
728
849
  Mats.Add(MakeShared<FJsonValueString>(Mat ? Mat->GetPathName() : TEXT("")));
729
850
  }
730
851
  C->SetArrayField(TEXT("materials"), Mats);
852
+
853
+ // #986: an ISM/HISM reported its class and its mesh and not
854
+ // how many instances it holds, which is the one number that
855
+ // decides whether to touch it at all. A component with three
856
+ // instances and one with three hundred thousand looked
857
+ // identical here, so the decision was made blind or cost a
858
+ // separate get_instance_transforms dump of every transform.
859
+ if (UInstancedStaticMeshComponent* ISMC = Cast<UInstancedStaticMeshComponent>(SMC))
860
+ {
861
+ C->SetNumberField(TEXT("instanceCount"), ISMC->GetInstanceCount());
862
+ TSharedPtr<FJsonObject> Instanced = MakeShared<FJsonObject>();
863
+ // HISM culls and LODs per instance and ISM does not, so
864
+ // the distinction changes what an edit costs.
865
+ Instanced->SetBoolField(TEXT("hierarchical"),
866
+ ISMC->IsA<UHierarchicalInstancedStaticMeshComponent>());
867
+ Instanced->SetNumberField(TEXT("numCustomDataFloats"), ISMC->NumCustomDataFloats);
868
+ C->SetObjectField(TEXT("instanced"), Instanced);
869
+ }
731
870
  }
732
871
  else if (USkeletalMeshComponent* SKMC = Cast<USkeletalMeshComponent>(PC))
733
872
  {
@@ -783,10 +922,15 @@ TSharedPtr<FJsonValue> FLevelHandlers::GetComponentTree(const TSharedPtr<FJsonOb
783
922
  TSharedPtr<FJsonObject> P = MakeShared<FJsonObject>();
784
923
  P->SetStringField(TEXT("name"), Prop->GetName());
785
924
  P->SetStringField(TEXT("type"), Prop->GetCPPType());
786
- FString ValueStr;
787
- const void* VP = Prop->ContainerPtrToValuePtr<void>(Comp);
788
- Prop->ExportText_Direct(ValueStr, VP, VP, Comp, PPF_None);
789
- P->SetStringField(TEXT("value"), ValueStr);
925
+ // #927: a fixed array is one FProperty with ArrayDim > 1, and
926
+ // exporting it without an index reports element 0 as though it
927
+ // were the whole value. Same helper as the actor dump, so the
928
+ // two cannot drift.
929
+ P->SetField(TEXT("value"), MCPExportPropertyValue(Prop, Comp));
930
+ if (MCPPropertyIsFixedArray(Prop))
931
+ {
932
+ P->SetNumberField(TEXT("arrayDim"), Prop->ArrayDim);
933
+ }
790
934
  Props.Add(MakeShared<FJsonValueObject>(P));
791
935
  }
792
936
  C->SetArrayField(TEXT("properties"), Props);
@@ -797,6 +941,7 @@ TSharedPtr<FJsonValue> FLevelHandlers::GetComponentTree(const TSharedPtr<FJsonOb
797
941
 
798
942
  auto Result = MCPSuccess();
799
943
  Result->SetStringField(TEXT("actorLabel"), Actor->GetActorLabel());
944
+ Result->SetStringField(TEXT("actorPath"), Actor->GetPathName());
800
945
  Result->SetStringField(TEXT("actorClass"), Actor->GetClass()->GetName());
801
946
  Result->SetNumberField(TEXT("componentCount"), CompArr.Num());
802
947
  Result->SetArrayField(TEXT("components"), CompArr);
@@ -809,19 +954,30 @@ TSharedPtr<FJsonValue> FLevelHandlers::GetComponentTree(const TSharedPtr<FJsonOb
809
954
  // required execute_python with MathLibrary.inverse_transform_location.
810
955
  TSharedPtr<FJsonValue> FLevelHandlers::GetRelativeTransform(const TSharedPtr<FJsonObject>& Params)
811
956
  {
812
- FString TargetLabel;
813
- if (auto Err = RequireStringAlt(Params, TEXT("targetLabel"), TEXT("target"), TargetLabel)) return Err;
814
- FString ReferenceLabel;
815
- if (auto Err = RequireStringAlt(Params, TEXT("referenceLabel"), TEXT("reference"), ReferenceLabel)) return Err;
816
-
817
957
  const FString WorldScope = OptionalString(Params, TEXT("world"), TEXT("editor"));
818
958
  UWorld* World = ResolveWorldFromParams(Params, *WorldScope);
819
959
  if (!World) return MCPError(FString::Printf(TEXT("World '%s' not available"), *WorldScope));
820
960
 
821
- AActor* TargetActor = FindActorByLabel(World, TargetLabel);
822
- AActor* ReferenceActor = FindActorByLabel(World, ReferenceLabel);
823
- if (!TargetActor) return MCPError(FString::Printf(TEXT("Target actor not found: %s"), *TargetLabel));
824
- if (!ReferenceActor) return MCPError(FString::Printf(TEXT("Reference actor not found: %s"), *ReferenceLabel));
961
+ // #983: both ends take a path. Two duplicated labels would otherwise
962
+ // produce a relative transform between whichever pair the actor iterator
963
+ // reached first, which is exactly the number this action exists to trust.
964
+ FMCPActorSelector TargetSel;
965
+ TargetSel.LabelKey = TEXT("targetLabel");
966
+ TargetSel.PathKey = TEXT("targetPath");
967
+ TargetSel.AltLabelKey = TEXT("target");
968
+ TSharedPtr<FJsonValue> ActorErr;
969
+ AActor* TargetActor = MCPResolveActor(World, Params, ActorErr, TargetSel);
970
+ if (!TargetActor) return ActorErr;
971
+
972
+ FMCPActorSelector ReferenceSel;
973
+ ReferenceSel.LabelKey = TEXT("referenceLabel");
974
+ ReferenceSel.PathKey = TEXT("referencePath");
975
+ ReferenceSel.AltLabelKey = TEXT("reference");
976
+ AActor* ReferenceActor = MCPResolveActor(World, Params, ActorErr, ReferenceSel);
977
+ if (!ReferenceActor) return ActorErr;
978
+
979
+ const FString TargetLabel = TargetActor->GetActorLabel();
980
+ const FString ReferenceLabel = ReferenceActor->GetActorLabel();
825
981
 
826
982
  const FTransform Target = TargetActor->GetActorTransform();
827
983
  const FTransform Reference = ReferenceActor->GetActorTransform();
@@ -844,7 +1000,9 @@ TSharedPtr<FJsonValue> FLevelHandlers::GetRelativeTransform(const TSharedPtr<FJs
844
1000
 
845
1001
  auto Result = MCPSuccess();
846
1002
  Result->SetStringField(TEXT("targetLabel"), TargetLabel);
1003
+ Result->SetStringField(TEXT("targetPath"), TargetActor->GetPathName());
847
1004
  Result->SetStringField(TEXT("referenceLabel"), ReferenceLabel);
1005
+ Result->SetStringField(TEXT("referencePath"), ReferenceActor->GetPathName());
848
1006
  Result->SetObjectField(TEXT("location"), MakeVec(Relative.GetLocation()));
849
1007
  Result->SetObjectField(TEXT("rotation"), MakeRot(Relative.GetRotation().Rotator()));
850
1008
  Result->SetObjectField(TEXT("scale"), MakeVec(Relative.GetScale3D()));
@@ -947,21 +1105,27 @@ TSharedPtr<FJsonValue> FLevelHandlers::GetSelectedActors(const TSharedPtr<FJsonO
947
1105
  }
948
1106
  TSharedPtr<FJsonValue> FLevelHandlers::MoveActor(const TSharedPtr<FJsonObject>& Params)
949
1107
  {
950
- FString ActorLabel;
951
- if (auto Err = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return Err;
1108
+ FString Selector;
1109
+ if (auto Err = RequireStringAlt(Params, TEXT("actorLabel"), TEXT("actorPath"), Selector)) return Err;
952
1110
 
953
1111
  // #586: support the PIE world so a label from get_outliner {world:pie}
954
- // resolves and the live actor moves. FindActorByLabelOrName also matches the
955
- // runtime instance name PIE shows.
1112
+ // resolves and the live actor moves. The resolver also matches the runtime
1113
+ // instance name PIE shows.
956
1114
  const FString WorldScope = OptionalString(Params, TEXT("world"), TEXT("editor"));
957
1115
  UWorld* World = ResolveWorldFromParams(Params, *WorldScope);
958
1116
  if (!World) return MCPError(TEXT("World not available"));
959
1117
 
960
- AActor* Actor = FindActorByLabelOrName(World, ActorLabel);
961
- if (!Actor)
962
- {
963
- return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
964
- }
1118
+ // LabelOrName, not label alone: a caller often has an internal name rather
1119
+ // than a label, because a PIE-spawned actor has no label worth guessing.
1120
+ // The label tier is still exhausted first and the name tier refuses on
1121
+ // ambiguity, so accepting the name cannot reintroduce a silent pick (#983).
1122
+ FMCPActorSelector ActorSel;
1123
+ ActorSel.Match = EMCPActorMatch::LabelOrName;
1124
+ ActorSel.WorldLabel = World->IsGameWorld() ? TEXT("PIE") : TEXT("editor");
1125
+ TSharedPtr<FJsonValue> ActorErr;
1126
+ AActor* Actor = MCPResolveActor(World, Params, ActorErr, ActorSel);
1127
+ if (!Actor) return ActorErr;
1128
+ const FString ActorLabel = Actor->GetActorLabel();
965
1129
 
966
1130
  // Capture previous transform for rollback.
967
1131
  const FVector PreviousLocation = Actor->GetActorLocation();
@@ -987,9 +1151,13 @@ TSharedPtr<FJsonValue> FLevelHandlers::MoveActor(const TSharedPtr<FJsonObject>&
987
1151
  Result->SetObjectField(TEXT("rotation"), MCPRotatorToJsonObject(Actor->GetActorRotation()));
988
1152
  Result->SetObjectField(TEXT("scale"), MCPVec3ToJsonObject(Actor->GetActorScale3D()));
989
1153
  Result->SetStringField(TEXT("actorLabel"), ActorLabel);
1154
+ Result->SetStringField(TEXT("actorPath"), Actor->GetPathName());
990
1155
 
991
- // Self-inverse: call move_actor with previous transform.
1156
+ // Self-inverse: call move_actor with previous transform. #983: the undo
1157
+ // travels by path, so replaying it cannot land on a different actor that
1158
+ // happens to share the label.
992
1159
  TSharedPtr<FJsonObject> Payload = MakeShared<FJsonObject>();
1160
+ Payload->SetStringField(TEXT("actorPath"), Actor->GetPathName());
993
1161
  Payload->SetStringField(TEXT("actorLabel"), ActorLabel);
994
1162
  Payload->SetObjectField(TEXT("location"), MCPVec3ToJsonObject(PreviousLocation));
995
1163
  Payload->SetObjectField(TEXT("rotation"), MCPRotatorToJsonObject(PreviousRotation));
@@ -1004,23 +1172,27 @@ TSharedPtr<FJsonValue> FLevelHandlers::MoveActor(const TSharedPtr<FJsonObject>&
1004
1172
  // reading two transforms and computing the look-at client-side.
1005
1173
  TSharedPtr<FJsonValue> FLevelHandlers::AimActorAt(const TSharedPtr<FJsonObject>& Params)
1006
1174
  {
1007
- FString ActorLabel;
1008
- if (auto Err = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return Err;
1175
+ FString Selector;
1176
+ if (auto Err = RequireStringAlt(Params, TEXT("actorLabel"), TEXT("actorPath"), Selector)) return Err;
1009
1177
 
1010
1178
  FString WorldScope = OptionalString(Params, TEXT("world"), TEXT("editor"));
1011
1179
  UWorld* World = ResolveWorldFromParams(Params, *WorldScope);
1012
1180
  if (!World) return MCPError(TEXT("World not available"));
1013
1181
 
1014
- AActor* Actor = FindActorByLabel(World, ActorLabel);
1015
- if (!Actor) return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
1182
+ TSharedPtr<FJsonValue> ActorErr;
1183
+ AActor* Actor = MCPResolveActor(World, Params, ActorErr);
1184
+ if (!Actor) return ActorErr;
1185
+ const FString ActorLabel = Actor->GetActorLabel();
1016
1186
 
1017
1187
  // Resolve the target point: an explicit target Vec3, or another actor's location.
1018
1188
  FVector TargetLocation;
1019
- const FString TargetActorLabel = OptionalString(Params, TEXT("targetActor"));
1020
- if (!TargetActorLabel.IsEmpty())
1189
+ if (Params->HasField(TEXT("targetActor")) || Params->HasField(TEXT("targetActorPath")))
1021
1190
  {
1022
- AActor* TargetActor = FindActorByLabel(World, TargetActorLabel);
1023
- if (!TargetActor) return MCPError(FString::Printf(TEXT("Target actor not found: %s"), *TargetActorLabel));
1191
+ FMCPActorSelector TargetSel;
1192
+ TargetSel.LabelKey = TEXT("targetActor");
1193
+ TargetSel.PathKey = TEXT("targetActorPath");
1194
+ AActor* TargetActor = MCPResolveActor(World, Params, ActorErr, TargetSel);
1195
+ if (!TargetActor) return ActorErr;
1024
1196
  TargetLocation = TargetActor->GetActorLocation();
1025
1197
  }
1026
1198
  else if (Params->HasField(TEXT("target")))
@@ -1029,7 +1201,7 @@ TSharedPtr<FJsonValue> FLevelHandlers::AimActorAt(const TSharedPtr<FJsonObject>&
1029
1201
  }
1030
1202
  else
1031
1203
  {
1032
- return MCPError(TEXT("Supply 'target' (Vec3) or 'targetActor' (label)"));
1204
+ return MCPError(TEXT("Supply 'target' (Vec3), 'targetActor' (label) or 'targetActorPath' (object path)"));
1033
1205
  }
1034
1206
 
1035
1207
  const FVector ActorLocation = Actor->GetActorLocation();
@@ -1048,11 +1220,14 @@ TSharedPtr<FJsonValue> FLevelHandlers::AimActorAt(const TSharedPtr<FJsonObject>&
1048
1220
  auto Result = MCPSuccess();
1049
1221
  MCPSetUpdated(Result);
1050
1222
  Result->SetStringField(TEXT("actorLabel"), ActorLabel);
1223
+ Result->SetStringField(TEXT("actorPath"), Actor->GetPathName());
1051
1224
  Result->SetObjectField(TEXT("rotation"), MCPRotatorToJsonObject(Actor->GetActorRotation()));
1052
1225
  Result->SetObjectField(TEXT("target"), MCPVec3ToJsonObject(TargetLocation));
1053
1226
 
1054
- // Rollback: restore the prior rotation via move_actor.
1227
+ // Rollback: restore the prior rotation via move_actor, by path so the undo
1228
+ // cannot land on a namesake (#983).
1055
1229
  TSharedPtr<FJsonObject> Payload = MakeShared<FJsonObject>();
1230
+ Payload->SetStringField(TEXT("actorPath"), Actor->GetPathName());
1056
1231
  Payload->SetStringField(TEXT("actorLabel"), ActorLabel);
1057
1232
  Payload->SetObjectField(TEXT("rotation"), MCPRotatorToJsonObject(PreviousRotation));
1058
1233
  MCPSetRollback(Result, TEXT("move_actor"), Payload);
@@ -1091,10 +1266,13 @@ TSharedPtr<FJsonValue> FLevelHandlers::NavProjectPoint(const TSharedPtr<FJsonObj
1091
1266
 
1092
1267
  TSharedPtr<FJsonValue> FLevelHandlers::SelectActors(const TSharedPtr<FJsonObject>& Params)
1093
1268
  {
1094
- const TArray<TSharedPtr<FJsonValue>>* ActorLabelsArray = nullptr;
1095
- if (!Params->TryGetArrayField(TEXT("actorLabels"), ActorLabelsArray))
1269
+ static const TArray<TSharedPtr<FJsonValue>> EmptySelection;
1270
+ const TArray<TSharedPtr<FJsonValue>>* ActorLabelsArray = &EmptySelection;
1271
+ const bool bHasLabels = Params->TryGetArrayField(TEXT("actorLabels"), ActorLabelsArray);
1272
+ if (!bHasLabels) ActorLabelsArray = &EmptySelection;
1273
+ if (!bHasLabels && !Params->HasField(TEXT("actorPaths")))
1096
1274
  {
1097
- return MCPError(TEXT("Missing 'actorLabels' parameter"));
1275
+ return MCPError(TEXT("Missing 'actorLabels' parameter (or 'actorPaths')"));
1098
1276
  }
1099
1277
 
1100
1278
  REQUIRE_EDITOR_WORLD(World);
@@ -1105,31 +1283,61 @@ TSharedPtr<FJsonValue> FLevelHandlers::SelectActors(const TSharedPtr<FJsonObject
1105
1283
  TArray<TSharedPtr<FJsonValue>> SelectedArray;
1106
1284
  TArray<TSharedPtr<FJsonValue>> NotFoundArray;
1107
1285
 
1286
+ // #983: selection is the plural case, so a label naming several actors
1287
+ // selects all of them rather than one at random. selectedPaths reports
1288
+ // exactly which, and is what a follow-up write should target.
1289
+ TArray<TSharedPtr<FJsonValue>> SelectedPathsArray;
1108
1290
  for (const TSharedPtr<FJsonValue>& LabelValue : *ActorLabelsArray)
1109
1291
  {
1110
1292
  FString Label = LabelValue->AsString();
1111
- if (AActor* Match = FindActorByLabel(World, Label))
1293
+ TArray<AActor*> Matches;
1294
+ MCPCollectActorsByToken(World, Label, EMCPActorMatch::Label, Matches);
1295
+ if (Matches.Num() == 0)
1296
+ {
1297
+ NotFoundArray.Add(MakeShared<FJsonValueString>(Label));
1298
+ continue;
1299
+ }
1300
+ for (AActor* Match : Matches)
1112
1301
  {
1113
1302
  GEditor->SelectActor(Match, true, true, true);
1114
- SelectedArray.Add(MakeShared<FJsonValueString>(Label));
1303
+ SelectedPathsArray.Add(MakeShared<FJsonValueString>(Match->GetPathName()));
1115
1304
  }
1116
- else
1305
+ SelectedArray.Add(MakeShared<FJsonValueString>(Label));
1306
+ }
1307
+
1308
+ // An explicit path list selects exactly what it names, with no label
1309
+ // resolution in the way at all.
1310
+ const TArray<TSharedPtr<FJsonValue>>* ActorPathsArray = nullptr;
1311
+ if (Params->TryGetArrayField(TEXT("actorPaths"), ActorPathsArray))
1312
+ {
1313
+ for (const TSharedPtr<FJsonValue>& PathValue : *ActorPathsArray)
1117
1314
  {
1118
- NotFoundArray.Add(MakeShared<FJsonValueString>(Label));
1315
+ const FString Path = PathValue->AsString();
1316
+ if (AActor* Match = MCPFindActorByPath(World, Path))
1317
+ {
1318
+ GEditor->SelectActor(Match, true, true, true);
1319
+ SelectedPathsArray.Add(MakeShared<FJsonValueString>(Match->GetPathName()));
1320
+ SelectedArray.Add(MakeShared<FJsonValueString>(Match->GetActorLabel()));
1321
+ }
1322
+ else
1323
+ {
1324
+ NotFoundArray.Add(MakeShared<FJsonValueString>(Path));
1325
+ }
1119
1326
  }
1120
1327
  }
1121
1328
 
1122
1329
  auto Result = MCPSuccess();
1123
1330
  Result->SetArrayField(TEXT("selected"), SelectedArray);
1331
+ Result->SetArrayField(TEXT("selectedPaths"), SelectedPathsArray);
1124
1332
  Result->SetArrayField(TEXT("notFound"), NotFoundArray);
1125
- Result->SetNumberField(TEXT("selectedCount"), SelectedArray.Num());
1333
+ Result->SetNumberField(TEXT("selectedCount"), SelectedPathsArray.Num());
1126
1334
 
1127
1335
  return MCPResult(Result);
1128
1336
  }
1129
1337
  TSharedPtr<FJsonValue> FLevelHandlers::AddComponentToActor(const TSharedPtr<FJsonObject>& Params)
1130
1338
  {
1131
1339
  FString ActorLabel;
1132
- if (auto Err = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return Err;
1340
+ if (auto Err = RequireStringAlt(Params, TEXT("actorLabel"), TEXT("actorPath"), ActorLabel)) return Err;
1133
1341
 
1134
1342
  FString ComponentClass;
1135
1343
  if (auto Err = RequireString(Params, TEXT("componentClass"), ComponentClass)) return Err;
@@ -1141,11 +1349,10 @@ TSharedPtr<FJsonValue> FLevelHandlers::AddComponentToActor(const TSharedPtr<FJso
1141
1349
 
1142
1350
  REQUIRE_EDITOR_WORLD(World);
1143
1351
 
1144
- AActor* Actor = FindActorByLabel(World, ActorLabel);
1145
- if (!Actor)
1146
- {
1147
- return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
1148
- }
1352
+ TSharedPtr<FJsonValue> ActorErr;
1353
+ AActor* Actor = MCPResolveActor(World, Params, ActorErr);
1354
+ if (!Actor) return ActorErr;
1355
+ ActorLabel = Actor->GetActorLabel();
1149
1356
 
1150
1357
  // Idempotency: check for an existing component with the same name on the actor.
1151
1358
  FName CompName = FName(*ComponentName);
@@ -1161,6 +1368,7 @@ TSharedPtr<FJsonValue> FLevelHandlers::AddComponentToActor(const TSharedPtr<FJso
1161
1368
  auto ExistingResult = MCPSuccess();
1162
1369
  MCPSetExisted(ExistingResult);
1163
1370
  ExistingResult->SetStringField(TEXT("actorLabel"), ActorLabel);
1371
+ ExistingResult->SetStringField(TEXT("actorPath"), Actor->GetPathName());
1164
1372
  ExistingResult->SetStringField(TEXT("componentName"), ComponentName);
1165
1373
  ExistingResult->SetStringField(TEXT("componentClass"), Existing->GetClass()->GetName());
1166
1374
  return MCPResult(ExistingResult);
@@ -1210,10 +1418,12 @@ TSharedPtr<FJsonValue> FLevelHandlers::AddComponentToActor(const TSharedPtr<FJso
1210
1418
  auto Result = MCPSuccess();
1211
1419
  MCPSetCreated(Result);
1212
1420
  Result->SetStringField(TEXT("actorLabel"), ActorLabel);
1421
+ Result->SetStringField(TEXT("actorPath"), Actor->GetPathName());
1213
1422
  Result->SetStringField(TEXT("componentName"), ComponentName);
1214
1423
  Result->SetStringField(TEXT("componentClass"), NewComponent->GetClass()->GetName());
1215
1424
 
1216
1425
  TSharedPtr<FJsonObject> Payload = MakeShared<FJsonObject>();
1426
+ Payload->SetStringField(TEXT("actorPath"), Actor->GetPathName());
1217
1427
  Payload->SetStringField(TEXT("actorLabel"), ActorLabel);
1218
1428
  Payload->SetStringField(TEXT("componentName"), ComponentName);
1219
1429
  MCPSetRollback(Result, TEXT("remove_component_from_actor"), Payload);
@@ -1225,14 +1435,16 @@ TSharedPtr<FJsonValue> FLevelHandlers::AddComponentToActor(const TSharedPtr<FJso
1225
1435
  TSharedPtr<FJsonValue> FLevelHandlers::RemoveComponentFromActor(const TSharedPtr<FJsonObject>& Params)
1226
1436
  {
1227
1437
  FString ActorLabel;
1228
- if (auto Err = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return Err;
1438
+ if (auto Err = RequireStringAlt(Params, TEXT("actorLabel"), TEXT("actorPath"), ActorLabel)) return Err;
1229
1439
  FString ComponentName;
1230
1440
  if (auto Err = RequireString(Params, TEXT("componentName"), ComponentName)) return Err;
1231
1441
 
1232
1442
  REQUIRE_EDITOR_WORLD(World);
1233
1443
 
1234
- AActor* Actor = FindActorByLabel(World, ActorLabel);
1235
- if (!Actor) return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
1444
+ TSharedPtr<FJsonValue> ActorErr;
1445
+ AActor* Actor = MCPResolveActor(World, Params, ActorErr);
1446
+ if (!Actor) return ActorErr;
1447
+ ActorLabel = Actor->GetActorLabel();
1236
1448
 
1237
1449
  const FName CompName(*ComponentName);
1238
1450
  UActorComponent* Target = nullptr;
@@ -1245,6 +1457,7 @@ TSharedPtr<FJsonValue> FLevelHandlers::RemoveComponentFromActor(const TSharedPtr
1245
1457
  {
1246
1458
  auto Noop = MCPSuccess();
1247
1459
  Noop->SetStringField(TEXT("actorLabel"), ActorLabel);
1460
+ Noop->SetStringField(TEXT("actorPath"), Actor->GetPathName());
1248
1461
  Noop->SetStringField(TEXT("componentName"), ComponentName);
1249
1462
  Noop->SetBoolField(TEXT("alreadyDeleted"), true);
1250
1463
  return MCPResult(Noop);
@@ -1258,6 +1471,7 @@ TSharedPtr<FJsonValue> FLevelHandlers::RemoveComponentFromActor(const TSharedPtr
1258
1471
 
1259
1472
  auto Result = MCPSuccess();
1260
1473
  Result->SetStringField(TEXT("actorLabel"), ActorLabel);
1474
+ Result->SetStringField(TEXT("actorPath"), Actor->GetPathName());
1261
1475
  Result->SetStringField(TEXT("componentName"), ComponentName);
1262
1476
  Result->SetStringField(TEXT("componentClass"), ComponentClass);
1263
1477
  Result->SetBoolField(TEXT("deleted"), true);
@@ -1318,6 +1532,168 @@ TSharedPtr<FJsonValue> FLevelHandlers::LoadLevel(const TSharedPtr<FJsonObject>&
1318
1532
  return MCPResult(Result);
1319
1533
  }
1320
1534
 
1535
+ int32 FLevelHandlers::ClearBlueprintGraphNodes(
1536
+ UBlueprint* Blueprint,
1537
+ bool bDryRun,
1538
+ TArray<TSharedPtr<FJsonValue>>& OutGraphs)
1539
+ {
1540
+ if (!Blueprint)
1541
+ {
1542
+ return 0;
1543
+ }
1544
+
1545
+ TArray<UEdGraph*> Graphs;
1546
+ Blueprint->GetAllGraphs(Graphs);
1547
+ int32 NodeCount = 0;
1548
+
1549
+ for (UEdGraph* Graph : Graphs)
1550
+ {
1551
+ if (!Graph || Graph->Nodes.IsEmpty())
1552
+ {
1553
+ continue;
1554
+ }
1555
+
1556
+ TArray<TSharedPtr<FJsonValue>> Nodes;
1557
+ TArray<UEdGraphNode*> GraphNodes;
1558
+ GraphNodes.Reserve(Graph->Nodes.Num());
1559
+ for (UEdGraphNode* Node : Graph->Nodes)
1560
+ {
1561
+ GraphNodes.Add(Node);
1562
+ }
1563
+ if (!bDryRun)
1564
+ {
1565
+ Graph->Modify();
1566
+ }
1567
+ for (UEdGraphNode* Node : GraphNodes)
1568
+ {
1569
+ if (!Node)
1570
+ {
1571
+ continue;
1572
+ }
1573
+
1574
+ auto NodeJson = MakeShared<FJsonObject>();
1575
+ NodeJson->SetStringField(TEXT("name"), Node->GetName());
1576
+ NodeJson->SetStringField(TEXT("classPath"), Node->GetClass()->GetPathName());
1577
+ Nodes.Add(MakeShared<FJsonValueObject>(NodeJson));
1578
+ ++NodeCount;
1579
+
1580
+ if (!bDryRun)
1581
+ {
1582
+ Node->Modify();
1583
+ FBlueprintEditorUtils::RemoveNode(Blueprint, Node, /*bDontRecompile*/ true);
1584
+ }
1585
+ }
1586
+
1587
+ auto GraphJson = MakeShared<FJsonObject>();
1588
+ GraphJson->SetStringField(TEXT("name"), Graph->GetName());
1589
+ GraphJson->SetNumberField(TEXT("nodeCount"), Nodes.Num());
1590
+ GraphJson->SetArrayField(TEXT("nodes"), Nodes);
1591
+ OutGraphs.Add(MakeShared<FJsonValueObject>(GraphJson));
1592
+ }
1593
+
1594
+ return NodeCount;
1595
+ }
1596
+
1597
+ TSharedPtr<FJsonValue> FLevelHandlers::ClearLevelScript(const TSharedPtr<FJsonObject>& Params)
1598
+ {
1599
+ if (!GEditor)
1600
+ {
1601
+ return MCPError(TEXT("GEditor not available"));
1602
+ }
1603
+
1604
+ UWorld* World = GEditor->GetEditorWorldContext().World();
1605
+ if (!World || !World->PersistentLevel)
1606
+ {
1607
+ return MCPError(TEXT("No persistent editor level is loaded"));
1608
+ }
1609
+
1610
+ const bool bDryRun = OptionalBool(Params, TEXT("dryRun"), true);
1611
+ const bool bSave = OptionalBool(Params, TEXT("save"), false);
1612
+ if (!bDryRun && bSave && World->GetOutermost()->IsDirty())
1613
+ {
1614
+ return MCPError(TEXT("Current level already has unsaved changes; save or discard them before clear_level_script with save=true"));
1615
+ }
1616
+ ULevelScriptBlueprint* LevelScript =
1617
+ World->PersistentLevel->GetLevelScriptBlueprint(/*bDontCreate*/ true);
1618
+
1619
+ TArray<TSharedPtr<FJsonValue>> Graphs;
1620
+ TArray<TSharedPtr<FJsonValue>> Variables;
1621
+ int32 NodeCount = 0;
1622
+ int32 VariableCount = 0;
1623
+ bool bCompileSucceeded = true;
1624
+ bool bSaved = false;
1625
+
1626
+ if (LevelScript)
1627
+ {
1628
+ TArray<FName> VariableNames;
1629
+ VariableNames.Reserve(LevelScript->NewVariables.Num());
1630
+ for (const FBPVariableDescription& Variable : LevelScript->NewVariables)
1631
+ {
1632
+ VariableNames.Add(Variable.VarName);
1633
+ Variables.Add(MakeShared<FJsonValueString>(Variable.VarName.ToString()));
1634
+ }
1635
+ VariableCount = VariableNames.Num();
1636
+
1637
+ if (bDryRun)
1638
+ {
1639
+ NodeCount = ClearBlueprintGraphNodes(LevelScript, true, Graphs);
1640
+ }
1641
+ else
1642
+ {
1643
+ const FScopedTransaction Transaction(
1644
+ NSLOCTEXT("UEMCPBridge", "ClearLevelScript", "MCP clear level script"));
1645
+ World->Modify();
1646
+ World->PersistentLevel->Modify();
1647
+ LevelScript->Modify();
1648
+ NodeCount = ClearBlueprintGraphNodes(LevelScript, false, Graphs);
1649
+ for (const FName VariableName : VariableNames)
1650
+ {
1651
+ FBlueprintEditorUtils::RemoveMemberVariable(LevelScript, VariableName);
1652
+ }
1653
+
1654
+ if (NodeCount > 0 || VariableCount > 0)
1655
+ {
1656
+ FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(LevelScript);
1657
+ FKismetEditorUtilities::CompileBlueprint(LevelScript);
1658
+ bCompileSucceeded = LevelScript->Status != BS_Error;
1659
+ World->MarkPackageDirty();
1660
+ if (!bCompileSucceeded)
1661
+ {
1662
+ return MCPError(TEXT("Level script nodes were cleared but compilation failed; the level was not saved and the change can be undone"));
1663
+ }
1664
+ }
1665
+
1666
+ if (bSave && (NodeCount > 0 || VariableCount > 0))
1667
+ {
1668
+ ULevelEditorSubsystem* LevelEditorSubsystem =
1669
+ GEditor->GetEditorSubsystem<ULevelEditorSubsystem>();
1670
+ if (!LevelEditorSubsystem)
1671
+ {
1672
+ return MCPError(TEXT("LevelEditorSubsystem not available; level was changed but not saved"));
1673
+ }
1674
+ bSaved = LevelEditorSubsystem->SaveCurrentLevel();
1675
+ if (!bSaved)
1676
+ {
1677
+ return MCPError(TEXT("Level script was cleared and compiled, but the current level could not be saved"));
1678
+ }
1679
+ }
1680
+ }
1681
+ }
1682
+
1683
+ auto Result = MCPSuccess();
1684
+ Result->SetStringField(TEXT("levelPath"), World->GetOutermost()->GetName());
1685
+ Result->SetBoolField(TEXT("dryRun"), bDryRun);
1686
+ Result->SetBoolField(TEXT("hasLevelScript"), LevelScript != nullptr);
1687
+ Result->SetNumberField(TEXT("graphCount"), Graphs.Num());
1688
+ Result->SetNumberField(TEXT("nodeCount"), NodeCount);
1689
+ Result->SetArrayField(TEXT("graphs"), Graphs);
1690
+ Result->SetNumberField(TEXT("variableCount"), VariableCount);
1691
+ Result->SetArrayField(TEXT("variables"), Variables);
1692
+ Result->SetBoolField(TEXT("compileSucceeded"), bCompileSucceeded);
1693
+ Result->SetBoolField(TEXT("saved"), bSaved);
1694
+ return MCPResult(Result);
1695
+ }
1696
+
1321
1697
  // Resolve a component on a placed actor by name, case-insensitively, across
1322
1698
  // all components GetComponents returns (which includes inherited/SCS
1323
1699
  // components on placed Blueprint instances). Empty name -> root component.
@@ -1380,7 +1756,7 @@ static UActorComponent* FindNamedComponentOnActor(AActor* Actor, const FString&
1380
1756
  TSharedPtr<FJsonValue> FLevelHandlers::SetComponentProperty(const TSharedPtr<FJsonObject>& Params)
1381
1757
  {
1382
1758
  FString ActorLabel;
1383
- if (auto Err = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return Err;
1759
+ if (auto Err = RequireStringAlt(Params, TEXT("actorLabel"), TEXT("actorPath"), ActorLabel)) return Err;
1384
1760
 
1385
1761
  FString ComponentName = OptionalString(Params, TEXT("componentName"));
1386
1762
 
@@ -1401,12 +1777,13 @@ TSharedPtr<FJsonValue> FLevelHandlers::SetComponentProperty(const TSharedPtr<FJs
1401
1777
  }
1402
1778
  const bool bRuntimeWorld = World->IsGameWorld();
1403
1779
 
1404
- AActor* TargetActor = FindActorByLabelNameOrPath(World, ActorLabel);
1405
- if (!TargetActor)
1406
- {
1407
- return MCPError(FString::Printf(TEXT("Actor not found in the %s world: %s"),
1408
- bRuntimeWorld ? TEXT("PIE") : TEXT("editor"), *ActorLabel));
1409
- }
1780
+ FMCPActorSelector ActorSel;
1781
+ ActorSel.Match = EMCPActorMatch::LabelNameOrPath;
1782
+ ActorSel.WorldLabel = bRuntimeWorld ? TEXT("PIE") : TEXT("editor");
1783
+ TSharedPtr<FJsonValue> ActorErr;
1784
+ AActor* TargetActor = MCPResolveActor(World, Params, ActorErr, ActorSel);
1785
+ if (!TargetActor) return ActorErr;
1786
+ ActorLabel = TargetActor->GetActorLabel();
1410
1787
 
1411
1788
  UActorComponent* TargetComp = FindComponentOnActor(TargetActor, ComponentName);
1412
1789
  if (!TargetComp)
@@ -1426,24 +1803,65 @@ TSharedPtr<FJsonValue> FLevelHandlers::SetComponentProperty(const TSharedPtr<FJs
1426
1803
  UStruct* CurrentStruct = TargetComp->GetClass();
1427
1804
  void* CurrentContainer = TargetComp;
1428
1805
  FProperty* Prop = nullptr;
1806
+ // #927: same fixed-array indexing as set_actor_property. A component
1807
+ // property declared `float Foo[4]` is one FProperty with ArrayDim 4, and
1808
+ // without an index every write lands on element 0.
1809
+ int32 LeafArrayIndex = 0;
1429
1810
  for (int32 i = 0; i < PathParts.Num(); ++i)
1430
1811
  {
1431
- FProperty* SegmentProp = CurrentStruct->FindPropertyByName(FName(*PathParts[i]));
1812
+ FString Token = PathParts[i];
1813
+ int32 SegmentIndex = 0;
1814
+ bool bHasSegmentIndex = false;
1815
+ {
1816
+ int32 OpenBracket = INDEX_NONE;
1817
+ int32 CloseBracket = INDEX_NONE;
1818
+ if (Token.FindChar(TEXT('['), OpenBracket) &&
1819
+ Token.FindChar(TEXT(']'), CloseBracket) &&
1820
+ CloseBracket > OpenBracket)
1821
+ {
1822
+ SegmentIndex = FCString::Atoi(*Token.Mid(OpenBracket + 1, CloseBracket - OpenBracket - 1));
1823
+ Token = Token.Left(OpenBracket);
1824
+ bHasSegmentIndex = true;
1825
+ }
1826
+ }
1827
+
1828
+ FProperty* SegmentProp = CurrentStruct->FindPropertyByName(FName(*Token));
1432
1829
  if (!SegmentProp)
1433
1830
  {
1434
- return MCPError(FString::Printf(TEXT("Property '%s' not found at '%s'"), *PathParts[i], *PropertyName));
1831
+ return MCPError(FString::Printf(TEXT("Property '%s' not found at '%s'"), *Token, *PropertyName));
1832
+ }
1833
+ if (bHasSegmentIndex)
1834
+ {
1835
+ if (CastField<FArrayProperty>(SegmentProp))
1836
+ {
1837
+ return MCPError(FString::Printf(
1838
+ TEXT("'%s' is a TArray. Indexing a dynamic array is not supported here; use asset(set_property) for dotted TArray paths. An index on this action addresses a C-style fixed array such as `float Foo[4]`."),
1839
+ *Token));
1840
+ }
1841
+ if (SegmentProp->ArrayDim <= 1)
1842
+ {
1843
+ return MCPError(FString::Printf(
1844
+ TEXT("'%s' is not a fixed array, so it cannot be indexed [%d]"), *Token, SegmentIndex));
1845
+ }
1846
+ if (SegmentIndex < 0 || SegmentIndex >= SegmentProp->ArrayDim)
1847
+ {
1848
+ return MCPError(FString::Printf(
1849
+ TEXT("Index %d is out of range on '%s', which has ArrayDim %d"),
1850
+ SegmentIndex, *Token, SegmentProp->ArrayDim));
1851
+ }
1435
1852
  }
1436
1853
  if (i < PathParts.Num() - 1)
1437
1854
  {
1438
1855
  if (FStructProperty* SP = CastField<FStructProperty>(SegmentProp))
1439
1856
  {
1440
- CurrentContainer = SP->ContainerPtrToValuePtr<void>(CurrentContainer);
1857
+ CurrentContainer = SP->ContainerPtrToValuePtr<void>(CurrentContainer, SegmentIndex);
1441
1858
  CurrentStruct = SP->Struct;
1442
1859
  }
1443
1860
  else if (FObjectProperty* OP = CastField<FObjectProperty>(SegmentProp))
1444
1861
  {
1445
1862
  // #305: descend through Instanced UObject sub-objects.
1446
- UObject* SubObject = OP->GetObjectPropertyValue(OP->ContainerPtrToValuePtr<void>(CurrentContainer));
1863
+ UObject* SubObject = OP->GetObjectPropertyValue(
1864
+ OP->ContainerPtrToValuePtr<void>(CurrentContainer, SegmentIndex));
1447
1865
  if (!SubObject)
1448
1866
  {
1449
1867
  return MCPError(FString::Printf(
@@ -1463,6 +1881,7 @@ TSharedPtr<FJsonValue> FLevelHandlers::SetComponentProperty(const TSharedPtr<FJs
1463
1881
  else
1464
1882
  {
1465
1883
  Prop = SegmentProp;
1884
+ LeafArrayIndex = SegmentIndex;
1466
1885
  }
1467
1886
  }
1468
1887
 
@@ -1472,7 +1891,7 @@ TSharedPtr<FJsonValue> FLevelHandlers::SetComponentProperty(const TSharedPtr<FJs
1472
1891
  return MCPError(TEXT("Missing 'value' parameter"));
1473
1892
  }
1474
1893
 
1475
- void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(CurrentContainer);
1894
+ void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(CurrentContainer, LeafArrayIndex);
1476
1895
 
1477
1896
  // Capture previous value as a string for self-inverse rollback.
1478
1897
  FString PreviousValueStr;
@@ -1509,9 +1928,21 @@ TSharedPtr<FJsonValue> FLevelHandlers::SetComponentProperty(const TSharedPtr<FJs
1509
1928
  // Skip obvious non-identifiers
1510
1929
  if (Token != TEXT("True") && Token != TEXT("False") && Token != TEXT("None") && !Token.IsNumeric())
1511
1930
  {
1512
- if (AActor* Resolved = FindActorByLabel(World, Token))
1931
+ // #983: a duplicated label here would wire the
1932
+ // struct's object reference to whichever namesake
1933
+ // the iterator reached first, so it is refused.
1934
+ // An unmatched token is left alone, as before: it
1935
+ // is probably an enum literal, not an actor.
1936
+ TArray<AActor*> TokenMatches;
1937
+ MCPCollectActorsByToken(World, Token, EMCPActorMatch::Label, TokenMatches);
1938
+ if (TokenMatches.Num() > 1)
1513
1939
  {
1514
- Result.Append(Resolved->GetPathName());
1940
+ return MCPAmbiguousActorError(
1941
+ Token, TEXT("value"), TEXT("actorPath"), TEXT("editor label"), TokenMatches);
1942
+ }
1943
+ if (TokenMatches.Num() == 1)
1944
+ {
1945
+ Result.Append(TokenMatches[0]->GetPathName());
1515
1946
  i = End;
1516
1947
  goto AppendDone;
1517
1948
  }
@@ -1571,12 +2002,15 @@ TSharedPtr<FJsonValue> FLevelHandlers::SetComponentProperty(const TSharedPtr<FJs
1571
2002
  auto Result = MCPSuccess();
1572
2003
  MCPSetUpdated(Result);
1573
2004
  Result->SetStringField(TEXT("actorLabel"), ActorLabel);
2005
+ Result->SetStringField(TEXT("actorPath"), TargetActor->GetPathName());
1574
2006
  Result->SetStringField(TEXT("componentClass"), TargetComp->GetClass()->GetName());
1575
2007
  Result->SetStringField(TEXT("propertyName"), PropertyName);
1576
2008
  Result->SetStringField(TEXT("previousValue"), PreviousValueStr);
1577
2009
 
1578
- // Self-inverse: same handler with previous value as string.
2010
+ // Self-inverse: same handler with previous value as string, addressed by
2011
+ // path so the undo cannot land on a namesake (#983).
1579
2012
  TSharedPtr<FJsonObject> Payload = MakeShared<FJsonObject>();
2013
+ Payload->SetStringField(TEXT("actorPath"), TargetActor->GetPathName());
1580
2014
  Payload->SetStringField(TEXT("actorLabel"), ActorLabel);
1581
2015
  if (!ComponentName.IsEmpty()) Payload->SetStringField(TEXT("componentName"), ComponentName);
1582
2016
  Payload->SetStringField(TEXT("propertyName"), PropertyName);
@@ -1593,7 +2027,7 @@ TSharedPtr<FJsonValue> FLevelHandlers::SetComponentProperty(const TSharedPtr<FJs
1593
2027
  TSharedPtr<FJsonValue> FLevelHandlers::GetComponentDetails(const TSharedPtr<FJsonObject>& Params)
1594
2028
  {
1595
2029
  FString ActorLabel;
1596
- if (auto Err = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return Err;
2030
+ if (auto Err = RequireStringAlt(Params, TEXT("actorLabel"), TEXT("actorPath"), ActorLabel)) return Err;
1597
2031
 
1598
2032
  const FString ComponentName = OptionalString(Params, TEXT("componentName"));
1599
2033
  // #584: optionally dump arbitrary UPROPERTY values (custom fields,
@@ -1608,11 +2042,13 @@ TSharedPtr<FJsonValue> FLevelHandlers::GetComponentDetails(const TSharedPtr<FJso
1608
2042
  UWorld* World = ResolveWorldFromParams(Params, *WorldScope);
1609
2043
  if (!World) return MCPError(FString::Printf(TEXT("World not available for scope '%s'"), *WorldScope));
1610
2044
 
1611
- AActor* TargetActor = FindActorByLabelNameOrPath(World, ActorLabel);
1612
- if (!TargetActor)
1613
- {
1614
- return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
1615
- }
2045
+ FMCPActorSelector ActorSel;
2046
+ ActorSel.Match = EMCPActorMatch::LabelNameOrPath;
2047
+ ActorSel.WorldLabel = World->IsGameWorld() ? TEXT("PIE") : TEXT("editor");
2048
+ TSharedPtr<FJsonValue> ActorErr;
2049
+ AActor* TargetActor = MCPResolveActor(World, Params, ActorErr, ActorSel);
2050
+ if (!TargetActor) return ActorErr;
2051
+ ActorLabel = TargetActor->GetActorLabel();
1616
2052
 
1617
2053
  auto DescribeComponent = [bIncludeValues, &PropFilter](UActorComponent* Comp) -> TSharedPtr<FJsonObject>
1618
2054
  {
@@ -1656,9 +2092,9 @@ TSharedPtr<FJsonValue> FLevelHandlers::GetComponentDetails(const TSharedPtr<FJso
1656
2092
  FProperty* Prop = *It;
1657
2093
  const FString PName = Prop->GetName();
1658
2094
  if (PropFilter.Num() > 0 && !PropFilter.Contains(PName)) continue;
1659
- FString Exported;
1660
- Prop->ExportText_Direct(Exported, Prop->ContainerPtrToValuePtr<void>(Comp), Prop->ContainerPtrToValuePtr<void>(Comp), Comp, PPF_None);
1661
- Values->SetStringField(PName, Exported);
2095
+ // #927: fixed arrays come back as a JSON array of elements
2096
+ // rather than as element 0 wearing the whole property's name.
2097
+ Values->SetField(PName, MCPExportPropertyValue(Prop, Comp));
1662
2098
  }
1663
2099
  Obj->SetObjectField(TEXT("values"), Values);
1664
2100
  }
@@ -1667,6 +2103,7 @@ TSharedPtr<FJsonValue> FLevelHandlers::GetComponentDetails(const TSharedPtr<FJso
1667
2103
 
1668
2104
  auto Result = MCPSuccess();
1669
2105
  Result->SetStringField(TEXT("actorLabel"), ActorLabel);
2106
+ Result->SetStringField(TEXT("actorPath"), TargetActor->GetPathName());
1670
2107
 
1671
2108
  if (!ComponentName.IsEmpty())
1672
2109
  {
@@ -1820,7 +2257,7 @@ TSharedPtr<FJsonValue> FLevelHandlers::SetWorldSettings(const TSharedPtr<FJsonOb
1820
2257
  TSharedPtr<FJsonValue> FLevelHandlers::SetActorMaterial(const TSharedPtr<FJsonObject>& Params)
1821
2258
  {
1822
2259
  FString ActorLabel;
1823
- if (auto Err = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return Err;
2260
+ if (auto Err = RequireStringAlt(Params, TEXT("actorLabel"), TEXT("actorPath"), ActorLabel)) return Err;
1824
2261
 
1825
2262
  FString MaterialPath;
1826
2263
  if (auto Err = RequireString(Params, TEXT("materialPath"), MaterialPath)) return Err;
@@ -1829,11 +2266,10 @@ TSharedPtr<FJsonValue> FLevelHandlers::SetActorMaterial(const TSharedPtr<FJsonOb
1829
2266
 
1830
2267
  REQUIRE_EDITOR_WORLD(World);
1831
2268
 
1832
- AActor* Actor = FindActorByLabel(World, ActorLabel);
1833
- if (!Actor)
1834
- {
1835
- return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
1836
- }
2269
+ TSharedPtr<FJsonValue> ActorErr;
2270
+ AActor* Actor = MCPResolveActor(World, Params, ActorErr);
2271
+ if (!Actor) return ActorErr;
2272
+ ActorLabel = Actor->GetActorLabel();
1837
2273
 
1838
2274
  UMaterialInterface* Material = LoadObject<UMaterialInterface>(nullptr, *MaterialPath);
1839
2275
  if (!Material)
@@ -1860,6 +2296,7 @@ TSharedPtr<FJsonValue> FLevelHandlers::SetActorMaterial(const TSharedPtr<FJsonOb
1860
2296
  auto Result = MCPSuccess();
1861
2297
  MCPSetUpdated(Result);
1862
2298
  Result->SetStringField(TEXT("actorLabel"), ActorLabel);
2299
+ Result->SetStringField(TEXT("actorPath"), Actor->GetPathName());
1863
2300
  Result->SetStringField(TEXT("materialPath"), MaterialPath);
1864
2301
  Result->SetNumberField(TEXT("slotIndex"), SlotIndex);
1865
2302
  Result->SetStringField(TEXT("previousMaterialPath"), PreviousMaterialPath);
@@ -1870,6 +2307,7 @@ TSharedPtr<FJsonValue> FLevelHandlers::SetActorMaterial(const TSharedPtr<FJsonOb
1870
2307
  if (!PreviousMaterialPath.IsEmpty())
1871
2308
  {
1872
2309
  TSharedPtr<FJsonObject> Payload = MakeShared<FJsonObject>();
2310
+ Payload->SetStringField(TEXT("actorPath"), Actor->GetPathName());
1873
2311
  Payload->SetStringField(TEXT("actorLabel"), ActorLabel);
1874
2312
  Payload->SetStringField(TEXT("materialPath"), PreviousMaterialPath);
1875
2313
  Payload->SetNumberField(TEXT("slotIndex"), SlotIndex);
@@ -2119,7 +2557,7 @@ TSharedPtr<FJsonValue> FLevelHandlers::GetRVTSummary(const TSharedPtr<FJsonObjec
2119
2557
  TSharedPtr<FJsonValue> FLevelHandlers::SetWaterBodyProperty(const TSharedPtr<FJsonObject>& Params)
2120
2558
  {
2121
2559
  FString ActorLabel;
2122
- if (auto Err = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return Err;
2560
+ if (auto Err = RequireStringAlt(Params, TEXT("actorLabel"), TEXT("actorPath"), ActorLabel)) return Err;
2123
2561
  FString PropertyName;
2124
2562
  if (auto Err = RequireString(Params, TEXT("propertyName"), PropertyName)) return Err;
2125
2563
 
@@ -2136,8 +2574,10 @@ TSharedPtr<FJsonValue> FLevelHandlers::SetWaterBodyProperty(const TSharedPtr<FJs
2136
2574
 
2137
2575
  REQUIRE_EDITOR_WORLD(World);
2138
2576
 
2139
- AActor* Actor = FindActorByLabel(World, ActorLabel);
2140
- if (!Actor) return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
2577
+ TSharedPtr<FJsonValue> ActorErr;
2578
+ AActor* Actor = MCPResolveActor(World, Params, ActorErr);
2579
+ if (!Actor) return ActorErr;
2580
+ ActorLabel = Actor->GetActorLabel();
2141
2581
 
2142
2582
  UClass* WBClass = LoadClass<UActorComponent>(nullptr, TEXT("/Script/Water.WaterBodyComponent"));
2143
2583
  if (!WBClass)
@@ -2170,6 +2610,7 @@ TSharedPtr<FJsonValue> FLevelHandlers::SetWaterBodyProperty(const TSharedPtr<FJs
2170
2610
  auto Result = MCPSuccess();
2171
2611
  MCPSetUpdated(Result);
2172
2612
  Result->SetStringField(TEXT("actorLabel"), ActorLabel);
2613
+ Result->SetStringField(TEXT("actorPath"), Actor->GetPathName());
2173
2614
  Result->SetStringField(TEXT("componentName"), WBComp->GetName());
2174
2615
  Result->SetStringField(TEXT("componentClass"), WBComp->GetClass()->GetName());
2175
2616
  Result->SetStringField(TEXT("propertyName"), PropertyName);
@@ -2179,21 +2620,23 @@ TSharedPtr<FJsonValue> FLevelHandlers::SetWaterBodyProperty(const TSharedPtr<FJs
2179
2620
 
2180
2621
  // ─── #188 get_actor_bounds ──────────────────────────────────────────
2181
2622
  // Returns the axis-aligned bounding box (origin + extent) for an actor
2182
- // found by its editor label.
2623
+ // named by its editor label or its object path.
2183
2624
  TSharedPtr<FJsonValue> FLevelHandlers::GetActorBounds(const TSharedPtr<FJsonObject>& Params)
2184
2625
  {
2185
2626
  FString ActorLabel;
2186
- if (auto Err = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return Err;
2627
+ if (auto Err = RequireStringAlt(Params, TEXT("actorLabel"), TEXT("actorPath"), ActorLabel)) return Err;
2187
2628
 
2188
2629
  const FString WorldScope = OptionalString(Params, TEXT("world"), TEXT("editor"));
2189
2630
  UWorld* World = ResolveWorldFromParams(Params, *WorldScope);
2190
2631
  if (!World) return MCPError(FString::Printf(TEXT("World not available for scope '%s'"), *WorldScope));
2191
2632
 
2192
- AActor* Actor = FindActorByLabelNameOrPath(World, ActorLabel);
2193
- if (!Actor)
2194
- {
2195
- return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
2196
- }
2633
+ FMCPActorSelector ActorSel;
2634
+ ActorSel.Match = EMCPActorMatch::LabelNameOrPath;
2635
+ ActorSel.WorldLabel = World->IsGameWorld() ? TEXT("PIE") : TEXT("editor");
2636
+ TSharedPtr<FJsonValue> ActorErr;
2637
+ AActor* Actor = MCPResolveActor(World, Params, ActorErr, ActorSel);
2638
+ if (!Actor) return ActorErr;
2639
+ ActorLabel = Actor->GetActorLabel();
2197
2640
 
2198
2641
  FVector Origin;
2199
2642
  FVector Extent;
@@ -2225,6 +2668,7 @@ TSharedPtr<FJsonValue> FLevelHandlers::GetActorBounds(const TSharedPtr<FJsonObje
2225
2668
 
2226
2669
  auto Result = MCPSuccess();
2227
2670
  Result->SetStringField(TEXT("actorLabel"), ActorLabel);
2671
+ Result->SetStringField(TEXT("actorPath"), Actor->GetPathName());
2228
2672
  Result->SetObjectField(TEXT("origin"), OriginObj);
2229
2673
  Result->SetObjectField(TEXT("extent"), ExtentObj);
2230
2674
  return MCPResult(Result);
@@ -2270,15 +2714,16 @@ TSharedPtr<FJsonValue> FLevelHandlers::ResolveActor(const TSharedPtr<FJsonObject
2270
2714
  }
2271
2715
 
2272
2716
  // #202/#230: generic per-instance UPROPERTY writer for level actors. Resolves
2273
- // the actor by label, walks dotted property paths, and routes the value
2274
- // through the recursive JSON setter so object refs / vectors / nested
2275
- // structs all apply. The optional `force` flag flips off the EditDefaultsOnly
2276
- // gate so per-instance overrides on EditDefaultsOnly properties go through
2277
- // (the per-instance value always existed - the editor UI just hides it).
2717
+ // the actor by label or object path, walks dotted property paths, and routes
2718
+ // the value through the recursive JSON setter so object refs / vectors /
2719
+ // nested structs all apply. The optional `force` flag flips off the
2720
+ // EditDefaultsOnly gate so per-instance overrides on EditDefaultsOnly
2721
+ // properties go through (the per-instance value always existed - the editor
2722
+ // UI just hides it).
2278
2723
  TSharedPtr<FJsonValue> FLevelHandlers::SetActorProperty(const TSharedPtr<FJsonObject>& Params)
2279
2724
  {
2280
2725
  FString ActorLabel;
2281
- if (auto Err = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return Err;
2726
+ if (auto Err = RequireStringAlt(Params, TEXT("actorLabel"), TEXT("actorPath"), ActorLabel)) return Err;
2282
2727
 
2283
2728
  FString PropertyName;
2284
2729
  if (auto Err = RequireString(Params, TEXT("propertyName"), PropertyName)) return Err;
@@ -2299,19 +2744,22 @@ TSharedPtr<FJsonValue> FLevelHandlers::SetActorProperty(const TSharedPtr<FJsonOb
2299
2744
  }
2300
2745
 
2301
2746
  AActor* TargetActor = nullptr;
2302
- const bool bWorldSettings = ActorLabel.Equals(TEXT("WorldSettings"), ESearchCase::IgnoreCase);
2747
+ const bool bWorldSettings =
2748
+ ActorLabel.Equals(TEXT("WorldSettings"), ESearchCase::IgnoreCase)
2749
+ && !Params->HasField(TEXT("actorPath"));
2303
2750
  if (bWorldSettings)
2304
2751
  {
2305
2752
  TargetActor = World->GetWorldSettings();
2753
+ if (!TargetActor) return MCPError(TEXT("World settings not available"));
2306
2754
  }
2307
2755
  else
2308
2756
  {
2309
- TargetActor = FindActorByLabel(World, ActorLabel);
2310
- }
2311
-
2312
- if (!TargetActor)
2313
- {
2314
- return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
2757
+ TSharedPtr<FJsonValue> ActorErr;
2758
+ FMCPActorSelector ActorSel;
2759
+ ActorSel.WorldLabel = World->IsGameWorld() ? TEXT("PIE") : TEXT("editor");
2760
+ TargetActor = MCPResolveActor(World, Params, ActorErr, ActorSel);
2761
+ if (!TargetActor) return ActorErr;
2762
+ ActorLabel = TargetActor->GetActorLabel();
2315
2763
  }
2316
2764
 
2317
2765
  TArray<FString> PathParts;
@@ -2321,15 +2769,62 @@ TSharedPtr<FJsonValue> FLevelHandlers::SetActorProperty(const TSharedPtr<FJsonOb
2321
2769
  UStruct* CurrentStruct = TargetActor->GetClass();
2322
2770
  void* CurrentContainer = TargetActor;
2323
2771
  FProperty* Prop = nullptr;
2772
+ // #927: the leaf element of a C-style fixed array, `int32 Foo[3]`. That is
2773
+ // ONE FProperty with ArrayDim == 3, so without an index the write lands on
2774
+ // element 0 and the other elements are unreachable. The read side has the
2775
+ // mirror of this bug; a read that shows three tiers and a write that can
2776
+ // only reach the first is not a usable pair.
2777
+ int32 LeafArrayIndex = 0;
2324
2778
  for (int32 i = 0; i < PathParts.Num(); ++i)
2325
2779
  {
2326
- FProperty* Seg = CurrentStruct->FindPropertyByName(FName(*PathParts[i]));
2327
- if (!Seg) return MCPError(FString::Printf(TEXT("Property '%s' not found at '%s'"), *PathParts[i], *PropertyName));
2780
+ FString Token = PathParts[i];
2781
+ int32 SegmentIndex = 0;
2782
+ bool bHasSegmentIndex = false;
2783
+ {
2784
+ int32 OpenBracket = INDEX_NONE;
2785
+ int32 CloseBracket = INDEX_NONE;
2786
+ if (Token.FindChar(TEXT('['), OpenBracket) &&
2787
+ Token.FindChar(TEXT(']'), CloseBracket) &&
2788
+ CloseBracket > OpenBracket)
2789
+ {
2790
+ SegmentIndex = FCString::Atoi(*Token.Mid(OpenBracket + 1, CloseBracket - OpenBracket - 1));
2791
+ Token = Token.Left(OpenBracket);
2792
+ bHasSegmentIndex = true;
2793
+ }
2794
+ }
2795
+
2796
+ FProperty* Seg = CurrentStruct->FindPropertyByName(FName(*Token));
2797
+ if (!Seg) return MCPError(FString::Printf(TEXT("Property '%s' not found at '%s'"), *Token, *PropertyName));
2798
+
2799
+ if (bHasSegmentIndex)
2800
+ {
2801
+ if (CastField<FArrayProperty>(Seg))
2802
+ {
2803
+ // A TArray element needs the shared resolver's array helper,
2804
+ // which this walker does not have. Say which action does
2805
+ // rather than writing element 0 and calling it a success.
2806
+ return MCPError(FString::Printf(
2807
+ TEXT("'%s' is a TArray. Indexing a dynamic array is not supported here; use asset(set_property) for dotted TArray paths. An index on this action addresses a C-style fixed array such as `int32 Foo[3]`."),
2808
+ *Token));
2809
+ }
2810
+ if (Seg->ArrayDim <= 1)
2811
+ {
2812
+ return MCPError(FString::Printf(
2813
+ TEXT("'%s' is not a fixed array, so it cannot be indexed [%d]"), *Token, SegmentIndex));
2814
+ }
2815
+ if (SegmentIndex < 0 || SegmentIndex >= Seg->ArrayDim)
2816
+ {
2817
+ return MCPError(FString::Printf(
2818
+ TEXT("Index %d is out of range on '%s', which has ArrayDim %d"),
2819
+ SegmentIndex, *Token, Seg->ArrayDim));
2820
+ }
2821
+ }
2822
+
2328
2823
  if (i < PathParts.Num() - 1)
2329
2824
  {
2330
2825
  if (FStructProperty* SP = CastField<FStructProperty>(Seg))
2331
2826
  {
2332
- CurrentContainer = SP->ContainerPtrToValuePtr<void>(CurrentContainer);
2827
+ CurrentContainer = SP->ContainerPtrToValuePtr<void>(CurrentContainer, SegmentIndex);
2333
2828
  CurrentStruct = SP->Struct;
2334
2829
  }
2335
2830
  // #305: descend through Instanced UObject sub-objects too. The path
@@ -2338,7 +2833,8 @@ TSharedPtr<FJsonValue> FLevelHandlers::SetActorProperty(const TSharedPtr<FJsonOb
2338
2833
  // rejection forced execute_python on every instanced-subobject write.
2339
2834
  else if (FObjectProperty* OP = CastField<FObjectProperty>(Seg))
2340
2835
  {
2341
- UObject* SubObject = OP->GetObjectPropertyValue(OP->ContainerPtrToValuePtr<void>(CurrentContainer));
2836
+ UObject* SubObject = OP->GetObjectPropertyValue(
2837
+ OP->ContainerPtrToValuePtr<void>(CurrentContainer, SegmentIndex));
2342
2838
  if (!SubObject)
2343
2839
  {
2344
2840
  return MCPError(FString::Printf(
@@ -2358,6 +2854,7 @@ TSharedPtr<FJsonValue> FLevelHandlers::SetActorProperty(const TSharedPtr<FJsonOb
2358
2854
  else
2359
2855
  {
2360
2856
  Prop = Seg;
2857
+ LeafArrayIndex = SegmentIndex;
2361
2858
  }
2362
2859
  }
2363
2860
 
@@ -2369,7 +2866,7 @@ TSharedPtr<FJsonValue> FLevelHandlers::SetActorProperty(const TSharedPtr<FJsonOb
2369
2866
  Prop->PropertyFlags &= ~CPF_DisableEditOnInstance;
2370
2867
  }
2371
2868
 
2372
- void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(CurrentContainer);
2869
+ void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(CurrentContainer, LeafArrayIndex);
2373
2870
 
2374
2871
  FString PrevValue;
2375
2872
  Prop->ExportText_Direct(PrevValue, ValuePtr, ValuePtr, TargetActor, PPF_None);
@@ -2377,15 +2874,30 @@ TSharedPtr<FJsonValue> FLevelHandlers::SetActorProperty(const TSharedPtr<FJsonOb
2377
2874
  TargetActor->Modify();
2378
2875
 
2379
2876
  // If the JSON value is a string and the property is an object reference,
2380
- // try resolving the string as an actor label first so callers can write
2381
- // {value: "Hopper_01"} for AHopper* references.
2877
+ // try resolving the string as an actor label or object path first, so
2878
+ // callers can write {value: "Hopper_01"} for AHopper* references and hand
2879
+ // back an actorPath when the label is not unique.
2382
2880
  TSharedPtr<FJsonValue> Value = *ValueField;
2383
2881
  if (Value->Type == EJson::String)
2384
2882
  {
2385
2883
  FString S = Value->AsString();
2386
2884
  if (FObjectProperty* OP = CastField<FObjectProperty>(Prop))
2387
2885
  {
2388
- AActor* RefActor = FindActorByLabel(World, S);
2886
+ // LabelNameOrPath, where this used to be label alone: the value
2887
+ // slot has to take an actorPath back, which is the whole point of
2888
+ // returning one. An actor object path contains ":PersistentLevel."
2889
+ // and so cannot collide with an asset path, and a value matching
2890
+ // nothing still falls through to the generic setter as before.
2891
+ TArray<AActor*> RefMatches;
2892
+ MCPCollectActorsByToken(World, S, EMCPActorMatch::LabelNameOrPath, RefMatches);
2893
+ // #983: wiring a reference to whichever namesake came first is the
2894
+ // silent wrong write this issue is about, so it is refused here too.
2895
+ if (RefMatches.Num() > 1)
2896
+ {
2897
+ Prop->PropertyFlags = OriginalFlags;
2898
+ return MCPAmbiguousActorError(S, TEXT("value"), TEXT("actorPath"), TEXT("editor label"), RefMatches);
2899
+ }
2900
+ AActor* RefActor = RefMatches.Num() == 1 ? RefMatches[0] : nullptr;
2389
2901
  if (RefActor && RefActor->IsA(OP->PropertyClass))
2390
2902
  {
2391
2903
  OP->SetObjectPropertyValue(ValuePtr, RefActor);
@@ -2423,7 +2935,16 @@ TSharedPtr<FJsonValue> FLevelHandlers::SetActorProperty(const TSharedPtr<FJsonOb
2423
2935
  {
2424
2936
  FString Label;
2425
2937
  (*Items)[i]->TryGetString(Label);
2426
- AActor* Ref = FindActorByLabel(World, Label);
2938
+ TArray<AActor*> ElementMatches;
2939
+ MCPCollectActorsByToken(World, Label, EMCPActorMatch::LabelNameOrPath, ElementMatches);
2940
+ if (ElementMatches.Num() > 1)
2941
+ {
2942
+ // #983: one ambiguous entry poisons the whole array,
2943
+ // so the write is refused before any element lands.
2944
+ Prop->PropertyFlags = OriginalFlags;
2945
+ return MCPAmbiguousActorError(Label, TEXT("value"), TEXT("actorPath"), TEXT("editor label"), ElementMatches);
2946
+ }
2947
+ AActor* Ref = ElementMatches.Num() == 1 ? ElementMatches[0] : nullptr;
2427
2948
  if (!Ref)
2428
2949
  {
2429
2950
  Prop->PropertyFlags = OriginalFlags;
@@ -2460,10 +2981,13 @@ WriteDone:
2460
2981
  auto Result = MCPSuccess();
2461
2982
  MCPSetUpdated(Result);
2462
2983
  Result->SetStringField(TEXT("actorLabel"), ActorLabel);
2984
+ Result->SetStringField(TEXT("actorPath"), TargetActor->GetPathName());
2463
2985
  Result->SetStringField(TEXT("propertyName"), PropertyName);
2464
2986
  Result->SetStringField(TEXT("previousValue"), PrevValue);
2465
2987
 
2988
+ // The undo travels by path so replaying it cannot land on a namesake (#983).
2466
2989
  TSharedPtr<FJsonObject> Payload = MakeShared<FJsonObject>();
2990
+ if (!bWorldSettings) Payload->SetStringField(TEXT("actorPath"), TargetActor->GetPathName());
2467
2991
  Payload->SetStringField(TEXT("actorLabel"), ActorLabel);
2468
2992
  Payload->SetStringField(TEXT("propertyName"), PropertyName);
2469
2993
  Payload->SetStringField(TEXT("value"), PrevValue);
@@ -2512,9 +3036,28 @@ TSharedPtr<FJsonValue> FLevelHandlers::ReadActorMotion(const TSharedPtr<FJsonObj
2512
3036
  FString L; if (V->TryGetString(L) && !L.IsEmpty()) Labels.Add(L);
2513
3037
  }
2514
3038
  }
2515
- if (Labels.Num() == 0)
3039
+ // #983: the same list spelled as object paths, which is what a caller
3040
+ // reaches for when several actors share a label. These are kept apart from
3041
+ // the label list and resolved by MCPFindActorByPath rather than folded into
3042
+ // the label token tier, so the export-text form and a case difference both
3043
+ // resolve here exactly as they do everywhere else.
3044
+ TArray<FString> Paths;
3045
+ FString SinglePath;
3046
+ if (Params->TryGetStringField(TEXT("actorPath"), SinglePath) && !SinglePath.IsEmpty())
2516
3047
  {
2517
- return MCPError(TEXT("Pass at least one of 'actorLabel' or 'actorLabels'"));
3048
+ Paths.Add(SinglePath);
3049
+ }
3050
+ const TArray<TSharedPtr<FJsonValue>>* PathsArr = nullptr;
3051
+ if (Params->TryGetArrayField(TEXT("actorPaths"), PathsArr) && PathsArr)
3052
+ {
3053
+ for (const TSharedPtr<FJsonValue>& V : *PathsArr)
3054
+ {
3055
+ FString P; if (V->TryGetString(P) && !P.IsEmpty()) Paths.Add(P);
3056
+ }
3057
+ }
3058
+ if (Labels.Num() == 0 && Paths.Num() == 0)
3059
+ {
3060
+ return MCPError(TEXT("Pass at least one of 'actorLabel', 'actorLabels', 'actorPath' or 'actorPaths'"));
2518
3061
  }
2519
3062
 
2520
3063
  auto VecToJson = [](const FVector& V) -> TSharedPtr<FJsonObject>
@@ -2532,16 +3075,37 @@ TSharedPtr<FJsonValue> FLevelHandlers::ReadActorMotion(const TSharedPtr<FJsonObj
2532
3075
 
2533
3076
  TArray<TSharedPtr<FJsonValue>> Samples;
2534
3077
  TArray<TSharedPtr<FJsonValue>> Missing;
3078
+ TArray<AActor*> Targets;
2535
3079
  for (const FString& Label : Labels)
2536
3080
  {
2537
- AActor* Actor = FindActorByLabel(TargetWorld, Label);
2538
- if (!Actor)
3081
+ // #983: this is a read, and a plural one, so a label naming several
3082
+ // actors samples all of them rather than one at random. Each row
3083
+ // carries actorPath, which is what a follow-up write should target.
3084
+ TArray<AActor*> Matches;
3085
+ MCPCollectActorsByToken(TargetWorld, Label, EMCPActorMatch::LabelNameOrPath, Matches);
3086
+ if (Matches.Num() == 0)
2539
3087
  {
2540
3088
  Missing.Add(MakeShared<FJsonValueString>(Label));
2541
3089
  continue;
2542
3090
  }
3091
+ for (AActor* Match : Matches) Targets.AddUnique(Match);
3092
+ }
3093
+ for (const FString& Path : Paths)
3094
+ {
3095
+ if (AActor* ByPath = MCPFindActorByPath(TargetWorld, Path))
3096
+ {
3097
+ Targets.AddUnique(ByPath);
3098
+ }
3099
+ else
3100
+ {
3101
+ Missing.Add(MakeShared<FJsonValueString>(Path));
3102
+ }
3103
+ }
3104
+ for (AActor* Actor : Targets)
3105
+ {
2543
3106
  TSharedPtr<FJsonObject> S = MakeShared<FJsonObject>();
2544
- S->SetStringField(TEXT("actorLabel"), Label);
3107
+ S->SetStringField(TEXT("actorLabel"), Actor->GetActorLabel());
3108
+ S->SetStringField(TEXT("actorPath"), Actor->GetPathName());
2545
3109
  S->SetStringField(TEXT("class"), Actor->GetClass()->GetName());
2546
3110
  S->SetObjectField(TEXT("location"), VecToJson(Actor->GetActorLocation()));
2547
3111
  S->SetObjectField(TEXT("rotation"), RotToJson(Actor->GetActorRotation()));
@@ -2599,10 +3163,12 @@ TSharedPtr<FJsonValue> FLevelHandlers::AddHismcInstances(const TSharedPtr<FJsonO
2599
3163
  {
2600
3164
  REQUIRE_EDITOR_WORLD(World);
2601
3165
  FString ActorLabel;
2602
- if (auto Err = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return Err;
3166
+ if (auto Err = RequireStringAlt(Params, TEXT("actorLabel"), TEXT("actorPath"), ActorLabel)) return Err;
2603
3167
 
2604
- AActor* Actor = FindActorByLabel(World, ActorLabel);
2605
- if (!Actor) return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
3168
+ TSharedPtr<FJsonValue> ActorErr;
3169
+ AActor* Actor = MCPResolveActor(World, Params, ActorErr);
3170
+ if (!Actor) return ActorErr;
3171
+ ActorLabel = Actor->GetActorLabel();
2606
3172
 
2607
3173
  FString ComponentName = OptionalString(Params, TEXT("componentName"));
2608
3174
  UInstancedStaticMeshComponent* ISMC = nullptr;
@@ -2683,6 +3249,7 @@ TSharedPtr<FJsonValue> FLevelHandlers::AddHismcInstances(const TSharedPtr<FJsonO
2683
3249
  auto Result = MCPSuccess();
2684
3250
  MCPSetCreated(Result);
2685
3251
  Result->SetStringField(TEXT("actorLabel"), ActorLabel);
3252
+ Result->SetStringField(TEXT("actorPath"), Actor->GetPathName());
2686
3253
  Result->SetStringField(TEXT("componentName"), ISMC->GetName());
2687
3254
  Result->SetStringField(TEXT("componentClass"), ISMC->GetClass()->GetName());
2688
3255
  Result->SetNumberField(TEXT("addedCount"), AddedIndices.Num());
@@ -2714,9 +3281,11 @@ TSharedPtr<FJsonValue> FLevelHandlers::GetInstanceTransforms(const TSharedPtr<FJ
2714
3281
  {
2715
3282
  REQUIRE_EDITOR_WORLD(World);
2716
3283
  FString ActorLabel;
2717
- if (auto Err = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return Err;
2718
- AActor* Actor = FindActorByLabel(World, ActorLabel);
2719
- if (!Actor) return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
3284
+ if (auto Err = RequireStringAlt(Params, TEXT("actorLabel"), TEXT("actorPath"), ActorLabel)) return Err;
3285
+ TSharedPtr<FJsonValue> ActorErr;
3286
+ AActor* Actor = MCPResolveActor(World, Params, ActorErr);
3287
+ if (!Actor) return ActorErr;
3288
+ ActorLabel = Actor->GetActorLabel();
2720
3289
 
2721
3290
  const FString ComponentName = OptionalString(Params, TEXT("componentName"));
2722
3291
  UInstancedStaticMeshComponent* ISMC = ResolveISMC(Actor, ComponentName);
@@ -2739,6 +3308,7 @@ TSharedPtr<FJsonValue> FLevelHandlers::GetInstanceTransforms(const TSharedPtr<FJ
2739
3308
 
2740
3309
  auto Result = MCPSuccess();
2741
3310
  Result->SetStringField(TEXT("actorLabel"), ActorLabel);
3311
+ Result->SetStringField(TEXT("actorPath"), Actor->GetPathName());
2742
3312
  Result->SetStringField(TEXT("componentName"), ISMC->GetName());
2743
3313
  Result->SetNumberField(TEXT("count"), Count);
2744
3314
  Result->SetBoolField(TEXT("worldSpace"), bWorldSpace);
@@ -2751,9 +3321,11 @@ TSharedPtr<FJsonValue> FLevelHandlers::UpdateInstanceTransform(const TSharedPtr<
2751
3321
  {
2752
3322
  REQUIRE_EDITOR_WORLD(World);
2753
3323
  FString ActorLabel;
2754
- if (auto Err = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return Err;
2755
- AActor* Actor = FindActorByLabel(World, ActorLabel);
2756
- if (!Actor) return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
3324
+ if (auto Err = RequireStringAlt(Params, TEXT("actorLabel"), TEXT("actorPath"), ActorLabel)) return Err;
3325
+ TSharedPtr<FJsonValue> ActorErr;
3326
+ AActor* Actor = MCPResolveActor(World, Params, ActorErr);
3327
+ if (!Actor) return ActorErr;
3328
+ ActorLabel = Actor->GetActorLabel();
2757
3329
 
2758
3330
  const FString ComponentName = OptionalString(Params, TEXT("componentName"));
2759
3331
  UInstancedStaticMeshComponent* ISMC = ResolveISMC(Actor, ComponentName);
@@ -2786,6 +3358,7 @@ TSharedPtr<FJsonValue> FLevelHandlers::UpdateInstanceTransform(const TSharedPtr<
2786
3358
  auto Result = MCPSuccess();
2787
3359
  MCPSetUpdated(Result);
2788
3360
  Result->SetStringField(TEXT("actorLabel"), ActorLabel);
3361
+ Result->SetStringField(TEXT("actorPath"), Actor->GetPathName());
2789
3362
  Result->SetStringField(TEXT("componentName"), ISMC->GetName());
2790
3363
  Result->SetNumberField(TEXT("index"), Index);
2791
3364
  return MCPResult(Result);
@@ -2796,9 +3369,11 @@ TSharedPtr<FJsonValue> FLevelHandlers::RemoveInstance(const TSharedPtr<FJsonObje
2796
3369
  {
2797
3370
  REQUIRE_EDITOR_WORLD(World);
2798
3371
  FString ActorLabel;
2799
- if (auto Err = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return Err;
2800
- AActor* Actor = FindActorByLabel(World, ActorLabel);
2801
- if (!Actor) return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
3372
+ if (auto Err = RequireStringAlt(Params, TEXT("actorLabel"), TEXT("actorPath"), ActorLabel)) return Err;
3373
+ TSharedPtr<FJsonValue> ActorErr;
3374
+ AActor* Actor = MCPResolveActor(World, Params, ActorErr);
3375
+ if (!Actor) return ActorErr;
3376
+ ActorLabel = Actor->GetActorLabel();
2802
3377
 
2803
3378
  const FString ComponentName = OptionalString(Params, TEXT("componentName"));
2804
3379
  UInstancedStaticMeshComponent* ISMC = ResolveISMC(Actor, ComponentName);
@@ -2818,6 +3393,7 @@ TSharedPtr<FJsonValue> FLevelHandlers::RemoveInstance(const TSharedPtr<FJsonObje
2818
3393
  auto Result = MCPSuccess();
2819
3394
  Result->SetBoolField(TEXT("removed"), bOk);
2820
3395
  Result->SetStringField(TEXT("actorLabel"), ActorLabel);
3396
+ Result->SetStringField(TEXT("actorPath"), Actor->GetPathName());
2821
3397
  Result->SetStringField(TEXT("componentName"), ISMC->GetName());
2822
3398
  Result->SetNumberField(TEXT("remainingInstances"), ISMC->GetInstanceCount());
2823
3399
  return MCPResult(Result);
@@ -2884,12 +3460,16 @@ TSharedPtr<FJsonValue> FLevelHandlers::ExportActorFbx(const TSharedPtr<FJsonObje
2884
3460
  {
2885
3461
  REQUIRE_EDITOR_WORLD(World);
2886
3462
  FString ActorLabel;
2887
- if (auto Err = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return Err;
3463
+ if (auto Err = RequireStringAlt(Params, TEXT("actorLabel"), TEXT("actorPath"), ActorLabel)) return Err;
2888
3464
  FString OutputPath;
2889
3465
  if (auto Err = RequireStringAlt(Params, TEXT("outputPath"), TEXT("filePath"), OutputPath)) return Err;
2890
3466
 
2891
- AActor* Actor = FindActorByLabelNameOrPath(World, ActorLabel);
2892
- if (!Actor) return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
3467
+ FMCPActorSelector ActorSel;
3468
+ ActorSel.Match = EMCPActorMatch::LabelNameOrPath;
3469
+ TSharedPtr<FJsonValue> ActorErr;
3470
+ AActor* Actor = MCPResolveActor(World, Params, ActorErr, ActorSel);
3471
+ if (!Actor) return ActorErr;
3472
+ ActorLabel = Actor->GetActorLabel();
2893
3473
 
2894
3474
  // Resolve the mesh asset from the actor (skeletal first, then static).
2895
3475
  UObject* MeshAsset = nullptr;
@@ -2999,25 +3579,56 @@ TSharedPtr<FJsonValue> FLevelHandlers::SpawnSkeletalMeshActor(const TSharedPtr<F
2999
3579
  if (!Actor) return MCPError(TEXT("Failed to spawn SkeletalMeshActor"));
3000
3580
  if (!Label.IsEmpty()) Actor->SetActorLabel(Label);
3001
3581
 
3582
+ // #946: per-slot COMPONENT material overrides, reported rather than
3583
+ // applied silently. These write the component's OverrideMaterials, which
3584
+ // is a different thing from the mesh ASSET's own slots: the asset is
3585
+ // untouched here. For an actor that is already placed, or to apply one
3586
+ // material to every slot, use level(set_component_materials).
3587
+ TArray<TSharedPtr<FJsonValue>> MaterialResults;
3588
+ int32 FailedMaterials = 0;
3002
3589
  USkeletalMeshComponent* Comp = Actor->GetSkeletalMeshComponent();
3003
3590
  if (Comp)
3004
3591
  {
3005
3592
  Comp->SetSkeletalMeshAsset(Mesh);
3006
3593
 
3007
- // Optional per-slot material overrides.
3008
3594
  const TArray<TSharedPtr<FJsonValue>>* Mats = nullptr;
3009
3595
  if (Params->TryGetArrayField(TEXT("materials"), Mats) && Mats)
3010
3596
  {
3597
+ const int32 SlotCount = Comp->GetNumMaterials();
3011
3598
  for (int32 i = 0; i < Mats->Num(); ++i)
3012
3599
  {
3013
3600
  FString MatPath;
3014
- if ((*Mats)[i].IsValid() && (*Mats)[i]->TryGetString(MatPath) && !MatPath.IsEmpty())
3601
+ const bool bHasPath =
3602
+ (*Mats)[i].IsValid() && (*Mats)[i]->TryGetString(MatPath) && !MatPath.IsEmpty();
3603
+ if (!bHasPath) continue;
3604
+
3605
+ TSharedPtr<FJsonObject> MatRow = MakeShared<FJsonObject>();
3606
+ MatRow->SetNumberField(TEXT("slotIndex"), i);
3607
+ MatRow->SetStringField(TEXT("materialPath"), MatPath);
3608
+
3609
+ // A slot index past the end, or a path that does not load,
3610
+ // used to be dropped on the floor. That reads as a successful
3611
+ // assignment that never happened.
3612
+ if (i >= SlotCount)
3015
3613
  {
3016
- if (UMaterialInterface* Mat = LoadAssetByPath<UMaterialInterface>(MatPath))
3017
- {
3018
- Comp->SetMaterial(i, Mat);
3019
- }
3614
+ MatRow->SetBoolField(TEXT("ok"), false);
3615
+ MatRow->SetStringField(TEXT("error"), FString::Printf(
3616
+ TEXT("slot %d is past the mesh's %d material slots"), i, SlotCount));
3617
+ ++FailedMaterials;
3618
+ }
3619
+ else if (UMaterialInterface* Mat = LoadAssetByPath<UMaterialInterface>(MatPath))
3620
+ {
3621
+ Comp->SetMaterial(i, Mat);
3622
+ MatRow->SetBoolField(TEXT("ok"), true);
3623
+ MatRow->SetStringField(TEXT("source"), TEXT("componentOverride"));
3624
+ }
3625
+ else
3626
+ {
3627
+ MatRow->SetBoolField(TEXT("ok"), false);
3628
+ MatRow->SetStringField(TEXT("error"), TEXT("material not found"));
3629
+ ++FailedMaterials;
3020
3630
  }
3631
+ MaterialResults.Add(MakeShared<FJsonValueObject>(MatRow));
3021
3632
  }
3022
3633
  }
3023
3634
 
@@ -3051,9 +3662,18 @@ TSharedPtr<FJsonValue> FLevelHandlers::SpawnSkeletalMeshActor(const TSharedPtr<F
3051
3662
 
3052
3663
  auto Result = MCPSuccess();
3053
3664
  MCPSetCreated(Result);
3665
+ Result->SetBoolField(TEXT("success"), FailedMaterials == 0);
3666
+ if (FailedMaterials > 0)
3667
+ {
3668
+ Result->SetStringField(TEXT("error"), FString::Printf(
3669
+ TEXT("The actor was spawned but %d material override(s) did not apply; see materials[]."),
3670
+ FailedMaterials));
3671
+ }
3054
3672
  Result->SetStringField(TEXT("actorLabel"), Actor->GetActorLabel());
3055
3673
  Result->SetStringField(TEXT("actorPath"), Actor->GetPathName());
3056
3674
  Result->SetStringField(TEXT("skeletalMesh"), Mesh->GetPathName());
3675
+ Result->SetNumberField(TEXT("materialSlotCount"), Comp ? Comp->GetNumMaterials() : 0);
3676
+ Result->SetArrayField(TEXT("materials"), MaterialResults);
3057
3677
  Result->SetObjectField(TEXT("location"), MCPVec3ToJsonObject(Loc));
3058
3678
  Result->SetObjectField(TEXT("boxExtent"), MCPVec3ToJsonObject(BoxExtent));
3059
3679
  TSharedPtr<FJsonObject> Rb = MakeShared<FJsonObject>();
@@ -3191,14 +3811,46 @@ TSharedPtr<FJsonValue> FLevelHandlers::DeleteActors(const TSharedPtr<FJsonObject
3191
3811
  {
3192
3812
  REQUIRE_EDITOR_WORLD(World);
3193
3813
 
3814
+ // #963: the filter names say what they match, and the two that were
3815
+ // previously reachable only through get_outliner's looser nameFilter are
3816
+ // now first class here. labelPrefix stays a CASE-SENSITIVE PREFIX over the
3817
+ // EDITOR LABEL, which is what it always was; labelContains and nameContains
3818
+ // are the substring forms, over the label and the internal name
3819
+ // respectively. Overloading one parameter to mean both is how a filter that
3820
+ // selects fifteen actors in one action selects none in another.
3194
3821
  const FString LabelPrefix = OptionalString(Params, TEXT("labelPrefix"));
3822
+ const FString LabelContains = OptionalString(Params, TEXT("labelContains"));
3823
+ const FString NameContains = OptionalString(Params, TEXT("nameContains"));
3195
3824
  const FString ClassName = OptionalString(Params, TEXT("className"));
3196
3825
  const FString Tag = OptionalString(Params, TEXT("tag"));
3197
3826
  const bool bDryRun = OptionalBool(Params, TEXT("dryRun"), false);
3198
3827
 
3199
- if (LabelPrefix.IsEmpty() && ClassName.IsEmpty() && Tag.IsEmpty())
3828
+ TArray<FString> ClassPathNeedles;
3829
+ const FString ClassPathContains = OptionalString(Params, TEXT("classPathContains"));
3830
+ if (!ClassPathContains.IsEmpty())
3200
3831
  {
3201
- return MCPError(TEXT("Provide at least one filter: labelPrefix, className, or tag"));
3832
+ ClassPathNeedles.Add(ClassPathContains);
3833
+ }
3834
+ const TArray<TSharedPtr<FJsonValue>>* ClassPathAny = nullptr;
3835
+ if (Params.IsValid() && Params->TryGetArrayField(TEXT("classPathContainsAny"), ClassPathAny) && ClassPathAny)
3836
+ {
3837
+ for (const TSharedPtr<FJsonValue>& Value : *ClassPathAny)
3838
+ {
3839
+ FString Needle;
3840
+ if (Value.IsValid() && Value->TryGetString(Needle) && !Needle.IsEmpty())
3841
+ {
3842
+ ClassPathNeedles.Add(Needle);
3843
+ }
3844
+ }
3845
+ }
3846
+
3847
+ // #924 added the class-path filters and #963 added the label/name ones. Both
3848
+ // are live, so the guard has to accept either family; requiring only one
3849
+ // family's filters would make the other silently unusable.
3850
+ if (LabelPrefix.IsEmpty() && LabelContains.IsEmpty() && NameContains.IsEmpty() &&
3851
+ ClassName.IsEmpty() && Tag.IsEmpty() && ClassPathNeedles.Num() == 0)
3852
+ {
3853
+ return MCPError(TEXT("Provide at least one filter: labelPrefix (case-sensitive prefix over the editor label), labelContains (case-insensitive substring over the label), nameContains (case-insensitive substring over the internal name), className, tag, classPathContains, or classPathContainsAny"));
3202
3854
  }
3203
3855
 
3204
3856
  TArray<AActor*> Matches;
@@ -3207,19 +3859,37 @@ TSharedPtr<FJsonValue> FLevelHandlers::DeleteActors(const TSharedPtr<FJsonObject
3207
3859
  AActor* A = *It;
3208
3860
  if (!A) continue;
3209
3861
  if (!LabelPrefix.IsEmpty() && !A->GetActorLabel().StartsWith(LabelPrefix)) continue;
3862
+ if (!LabelContains.IsEmpty() && !A->GetActorLabel().Contains(LabelContains, ESearchCase::IgnoreCase)) continue;
3863
+ if (!NameContains.IsEmpty() && !A->GetName().Contains(NameContains, ESearchCase::IgnoreCase)) continue;
3210
3864
  if (!ClassName.IsEmpty())
3211
3865
  {
3212
3866
  const FString CName = A->GetClass()->GetName();
3213
3867
  if (!CName.Contains(ClassName)) continue;
3214
3868
  }
3215
3869
  if (!Tag.IsEmpty() && !A->ActorHasTag(FName(*Tag))) continue;
3870
+ if (ClassPathNeedles.Num() > 0)
3871
+ {
3872
+ const FString ClassPath = A->GetClass()->GetPathName();
3873
+ bool bPathMatch = false;
3874
+ for (const FString& Needle : ClassPathNeedles)
3875
+ {
3876
+ if (ClassPath.Contains(Needle, ESearchCase::IgnoreCase))
3877
+ {
3878
+ bPathMatch = true;
3879
+ break;
3880
+ }
3881
+ }
3882
+ if (!bPathMatch) continue;
3883
+ }
3216
3884
  Matches.Add(A);
3217
3885
  }
3218
3886
 
3219
3887
  TArray<TSharedPtr<FJsonValue>> Labels;
3888
+ TArray<TSharedPtr<FJsonValue>> ClassPaths;
3220
3889
  for (AActor* A : Matches)
3221
3890
  {
3222
3891
  Labels.Add(MakeShared<FJsonValueString>(A->GetActorLabel()));
3892
+ ClassPaths.Add(MakeShared<FJsonValueString>(A->GetClass()->GetPathName()));
3223
3893
  }
3224
3894
 
3225
3895
  int32 Deleted = 0;
@@ -3246,17 +3916,38 @@ TSharedPtr<FJsonValue> FLevelHandlers::DeleteActors(const TSharedPtr<FJsonObject
3246
3916
  Result->SetNumberField(TEXT("matched"), Matches.Num());
3247
3917
  Result->SetNumberField(TEXT("deleted"), Deleted);
3248
3918
  Result->SetArrayField(TEXT("labels"), Labels);
3919
+ Result->SetArrayField(TEXT("classPaths"), ClassPaths);
3920
+
3921
+ // #963: a destructive action that matched nothing must not answer with a
3922
+ // bare success and a zero. A caller who trusts that concludes there is
3923
+ // nothing to delete and moves on, which is exactly what happened. Two
3924
+ // things can produce a wrong zero here, and the response now names both.
3925
+ if (Matches.IsEmpty())
3926
+ {
3927
+ Result->SetStringField(TEXT("zeroMatchNote"),
3928
+ TEXT("No actor matched. This is a filter result, not a statement that the actors do not exist."));
3929
+ const FString Needle = !LabelPrefix.IsEmpty()
3930
+ ? LabelPrefix
3931
+ : (!LabelContains.IsEmpty() ? LabelContains : NameContains);
3932
+ if (const TSharedPtr<FJsonObject> Hint = MCPDescribeZeroActorMatch(World, Needle))
3933
+ {
3934
+ Result->SetObjectField(TEXT("zeroMatchHint"), Hint);
3935
+ }
3936
+ MCPNoteLoadedOnlyEnumeration(World, Result);
3937
+ }
3249
3938
  return MCPResult(Result);
3250
3939
  }
3251
3940
 
3252
3941
  TSharedPtr<FJsonValue> FLevelHandlers::AddActorTag(const TSharedPtr<FJsonObject>& Params)
3253
3942
  {
3254
3943
  REQUIRE_EDITOR_WORLD(World);
3255
- FString ActorLabel; if (auto E = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return E;
3944
+ FString ActorLabel; if (auto E = RequireStringAlt(Params, TEXT("actorLabel"), TEXT("actorPath"), ActorLabel)) return E;
3256
3945
  FString Tag; if (auto E = RequireString(Params, TEXT("tag"), Tag)) return E;
3257
3946
 
3258
- AActor* A = FindActorByLabel(World, ActorLabel);
3259
- if (!A) return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
3947
+ TSharedPtr<FJsonValue> ActorErr;
3948
+ AActor* A = MCPResolveActor(World, Params, ActorErr);
3949
+ if (!A) return ActorErr;
3950
+ ActorLabel = A->GetActorLabel();
3260
3951
 
3261
3952
  const FName TagName(*Tag);
3262
3953
  const bool bAlreadyHad = A->Tags.Contains(TagName);
@@ -3269,6 +3960,7 @@ TSharedPtr<FJsonValue> FLevelHandlers::AddActorTag(const TSharedPtr<FJsonObject>
3269
3960
  auto Result = MCPSuccess();
3270
3961
  if (bAlreadyHad) MCPSetExisted(Result); else MCPSetUpdated(Result);
3271
3962
  Result->SetStringField(TEXT("actorLabel"), ActorLabel);
3963
+ Result->SetStringField(TEXT("actorPath"), A->GetPathName());
3272
3964
  Result->SetStringField(TEXT("tag"), Tag);
3273
3965
  TArray<TSharedPtr<FJsonValue>> TagsOut;
3274
3966
  for (const FName& T : A->Tags) TagsOut.Add(MakeShared<FJsonValueString>(T.ToString()));
@@ -3279,11 +3971,13 @@ TSharedPtr<FJsonValue> FLevelHandlers::AddActorTag(const TSharedPtr<FJsonObject>
3279
3971
  TSharedPtr<FJsonValue> FLevelHandlers::RemoveActorTag(const TSharedPtr<FJsonObject>& Params)
3280
3972
  {
3281
3973
  REQUIRE_EDITOR_WORLD(World);
3282
- FString ActorLabel; if (auto E = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return E;
3974
+ FString ActorLabel; if (auto E = RequireStringAlt(Params, TEXT("actorLabel"), TEXT("actorPath"), ActorLabel)) return E;
3283
3975
  FString Tag; if (auto E = RequireString(Params, TEXT("tag"), Tag)) return E;
3284
3976
 
3285
- AActor* A = FindActorByLabel(World, ActorLabel);
3286
- if (!A) return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
3977
+ TSharedPtr<FJsonValue> ActorErr;
3978
+ AActor* A = MCPResolveActor(World, Params, ActorErr);
3979
+ if (!A) return ActorErr;
3980
+ ActorLabel = A->GetActorLabel();
3287
3981
 
3288
3982
  const FName TagName(*Tag);
3289
3983
  const int32 Removed = A->Tags.Remove(TagName);
@@ -3296,6 +3990,7 @@ TSharedPtr<FJsonValue> FLevelHandlers::RemoveActorTag(const TSharedPtr<FJsonObje
3296
3990
  auto Result = MCPSuccess();
3297
3991
  if (Removed == 0) MCPSetExisted(Result); else MCPSetUpdated(Result);
3298
3992
  Result->SetStringField(TEXT("actorLabel"), ActorLabel);
3993
+ Result->SetStringField(TEXT("actorPath"), A->GetPathName());
3299
3994
  Result->SetStringField(TEXT("tag"), Tag);
3300
3995
  Result->SetNumberField(TEXT("removed"), Removed);
3301
3996
  return MCPResult(Result);
@@ -3304,10 +3999,12 @@ TSharedPtr<FJsonValue> FLevelHandlers::RemoveActorTag(const TSharedPtr<FJsonObje
3304
3999
  TSharedPtr<FJsonValue> FLevelHandlers::SetActorTags(const TSharedPtr<FJsonObject>& Params)
3305
4000
  {
3306
4001
  REQUIRE_EDITOR_WORLD(World);
3307
- FString ActorLabel; if (auto E = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return E;
4002
+ FString ActorLabel; if (auto E = RequireStringAlt(Params, TEXT("actorLabel"), TEXT("actorPath"), ActorLabel)) return E;
3308
4003
 
3309
- AActor* A = FindActorByLabel(World, ActorLabel);
3310
- if (!A) return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
4004
+ TSharedPtr<FJsonValue> ActorErr;
4005
+ AActor* A = MCPResolveActor(World, Params, ActorErr);
4006
+ if (!A) return ActorErr;
4007
+ ActorLabel = A->GetActorLabel();
3311
4008
 
3312
4009
  const TArray<TSharedPtr<FJsonValue>>* TagsArr = nullptr;
3313
4010
  if (!Params->TryGetArrayField(TEXT("tags"), TagsArr) || !TagsArr)
@@ -3330,6 +4027,7 @@ TSharedPtr<FJsonValue> FLevelHandlers::SetActorTags(const TSharedPtr<FJsonObject
3330
4027
  auto Result = MCPSuccess();
3331
4028
  MCPSetUpdated(Result);
3332
4029
  Result->SetStringField(TEXT("actorLabel"), ActorLabel);
4030
+ Result->SetStringField(TEXT("actorPath"), A->GetPathName());
3333
4031
  TArray<TSharedPtr<FJsonValue>> Out;
3334
4032
  for (const FName& T : A->Tags) Out.Add(MakeShared<FJsonValueString>(T.ToString()));
3335
4033
  Result->SetArrayField(TEXT("tags"), Out);
@@ -3339,13 +4037,16 @@ TSharedPtr<FJsonValue> FLevelHandlers::SetActorTags(const TSharedPtr<FJsonObject
3339
4037
  TSharedPtr<FJsonValue> FLevelHandlers::ListActorTags(const TSharedPtr<FJsonObject>& Params)
3340
4038
  {
3341
4039
  REQUIRE_EDITOR_WORLD(World);
3342
- FString ActorLabel; if (auto E = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return E;
4040
+ FString ActorLabel; if (auto E = RequireStringAlt(Params, TEXT("actorLabel"), TEXT("actorPath"), ActorLabel)) return E;
3343
4041
 
3344
- AActor* A = FindActorByLabel(World, ActorLabel);
3345
- if (!A) return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
4042
+ TSharedPtr<FJsonValue> ActorErr;
4043
+ AActor* A = MCPResolveActor(World, Params, ActorErr);
4044
+ if (!A) return ActorErr;
4045
+ ActorLabel = A->GetActorLabel();
3346
4046
 
3347
4047
  auto Result = MCPSuccess();
3348
4048
  Result->SetStringField(TEXT("actorLabel"), ActorLabel);
4049
+ Result->SetStringField(TEXT("actorPath"), A->GetPathName());
3349
4050
  TArray<TSharedPtr<FJsonValue>> Out;
3350
4051
  for (const FName& T : A->Tags) Out.Add(MakeShared<FJsonValueString>(T.ToString()));
3351
4052
  Result->SetArrayField(TEXT("tags"), Out);
@@ -3357,13 +4058,21 @@ TSharedPtr<FJsonValue> FLevelHandlers::ListActorTags(const TSharedPtr<FJsonObjec
3357
4058
  TSharedPtr<FJsonValue> FLevelHandlers::AttachActor(const TSharedPtr<FJsonObject>& Params)
3358
4059
  {
3359
4060
  REQUIRE_EDITOR_WORLD(World);
3360
- FString ChildLabel; if (auto E = RequireString(Params, TEXT("childLabel"), ChildLabel)) return E;
3361
- FString ParentLabel; if (auto E = RequireString(Params, TEXT("parentLabel"), ParentLabel)) return E;
3362
-
3363
- AActor* Child = FindActorByLabel(World, ChildLabel);
3364
- AActor* Parent = FindActorByLabel(World, ParentLabel);
3365
- if (!Child) return MCPError(FString::Printf(TEXT("Child actor not found: %s"), *ChildLabel));
3366
- if (!Parent) return MCPError(FString::Printf(TEXT("Parent actor not found: %s"), *ParentLabel));
4061
+ FString ChildLabel; if (auto E = RequireStringAlt(Params, TEXT("childLabel"), TEXT("childPath"), ChildLabel)) return E;
4062
+ FString ParentLabel; if (auto E = RequireStringAlt(Params, TEXT("parentLabel"), TEXT("parentPath"), ParentLabel)) return E;
4063
+
4064
+ // #983: both ends of an attachment take a path. Attaching to whichever
4065
+ // namesake the iterator reached first is how a prop ends up parented to a
4066
+ // building at the other end of the map.
4067
+ FMCPActorSelector ChildSel; ChildSel.LabelKey = TEXT("childLabel"); ChildSel.PathKey = TEXT("childPath");
4068
+ FMCPActorSelector ParentSel; ParentSel.LabelKey = TEXT("parentLabel"); ParentSel.PathKey = TEXT("parentPath");
4069
+ TSharedPtr<FJsonValue> ActorErr;
4070
+ AActor* Child = MCPResolveActor(World, Params, ActorErr, ChildSel);
4071
+ if (!Child) return ActorErr;
4072
+ AActor* Parent = MCPResolveActor(World, Params, ActorErr, ParentSel);
4073
+ if (!Parent) return ActorErr;
4074
+ ChildLabel = Child->GetActorLabel();
4075
+ ParentLabel = Parent->GetActorLabel();
3367
4076
 
3368
4077
  const FString RuleStr = OptionalString(Params, TEXT("attachRule"), TEXT("KeepWorld")).ToLower();
3369
4078
  EAttachmentRule Loc = EAttachmentRule::KeepWorld;
@@ -3380,10 +4089,13 @@ TSharedPtr<FJsonValue> FLevelHandlers::AttachActor(const TSharedPtr<FJsonObject>
3380
4089
  MCPSetUpdated(Result);
3381
4090
  Result->SetStringField(TEXT("childLabel"), ChildLabel);
3382
4091
  Result->SetStringField(TEXT("parentLabel"), ParentLabel);
4092
+ Result->SetStringField(TEXT("childPath"), Child->GetPathName());
4093
+ Result->SetStringField(TEXT("parentPath"), Parent->GetPathName());
3383
4094
  Result->SetBoolField(TEXT("attached"), bOk);
3384
4095
 
3385
4096
  TSharedPtr<FJsonObject> Payload = MakeShared<FJsonObject>();
3386
4097
  Payload->SetStringField(TEXT("childLabel"), ChildLabel);
4098
+ Payload->SetStringField(TEXT("childPath"), Child->GetPathName());
3387
4099
  MCPSetRollback(Result, TEXT("detach_actor"), Payload);
3388
4100
  return MCPResult(Result);
3389
4101
  }
@@ -3391,10 +4103,13 @@ TSharedPtr<FJsonValue> FLevelHandlers::AttachActor(const TSharedPtr<FJsonObject>
3391
4103
  TSharedPtr<FJsonValue> FLevelHandlers::DetachActor(const TSharedPtr<FJsonObject>& Params)
3392
4104
  {
3393
4105
  REQUIRE_EDITOR_WORLD(World);
3394
- FString ChildLabel; if (auto E = RequireString(Params, TEXT("childLabel"), ChildLabel)) return E;
4106
+ FString ChildLabel; if (auto E = RequireStringAlt(Params, TEXT("childLabel"), TEXT("childPath"), ChildLabel)) return E;
3395
4107
 
3396
- AActor* Child = FindActorByLabel(World, ChildLabel);
3397
- if (!Child) return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ChildLabel));
4108
+ FMCPActorSelector ChildSel; ChildSel.LabelKey = TEXT("childLabel"); ChildSel.PathKey = TEXT("childPath");
4109
+ TSharedPtr<FJsonValue> ActorErr;
4110
+ AActor* Child = MCPResolveActor(World, Params, ActorErr, ChildSel);
4111
+ if (!Child) return ActorErr;
4112
+ ChildLabel = Child->GetActorLabel();
3398
4113
 
3399
4114
  Child->Modify();
3400
4115
  Child->DetachFromActor(FDetachmentTransformRules(EDetachmentRule::KeepWorld, true));
@@ -3403,6 +4118,7 @@ TSharedPtr<FJsonValue> FLevelHandlers::DetachActor(const TSharedPtr<FJsonObject>
3403
4118
  auto Result = MCPSuccess();
3404
4119
  MCPSetUpdated(Result);
3405
4120
  Result->SetStringField(TEXT("childLabel"), ChildLabel);
4121
+ Result->SetStringField(TEXT("childPath"), Child->GetPathName());
3406
4122
  Result->SetBoolField(TEXT("detached"), true);
3407
4123
  return MCPResult(Result);
3408
4124
  }
@@ -3413,13 +4129,21 @@ TSharedPtr<FJsonValue> FLevelHandlers::DetachActor(const TSharedPtr<FJsonObject>
3413
4129
  TSharedPtr<FJsonValue> FLevelHandlers::AttachComponent(const TSharedPtr<FJsonObject>& Params)
3414
4130
  {
3415
4131
  REQUIRE_EDITOR_WORLD(World);
3416
- FString ChildLabel; if (auto E = RequireString(Params, TEXT("childLabel"), ChildLabel)) return E;
3417
- FString ParentLabel; if (auto E = RequireString(Params, TEXT("parentLabel"), ParentLabel)) return E;
3418
-
3419
- AActor* Child = FindActorByLabel(World, ChildLabel);
3420
- AActor* Parent = FindActorByLabel(World, ParentLabel);
3421
- if (!Child) return MCPError(FString::Printf(TEXT("Child actor not found: %s"), *ChildLabel));
3422
- if (!Parent) return MCPError(FString::Printf(TEXT("Parent actor not found: %s"), *ParentLabel));
4132
+ FString ChildLabel; if (auto E = RequireStringAlt(Params, TEXT("childLabel"), TEXT("childPath"), ChildLabel)) return E;
4133
+ FString ParentLabel; if (auto E = RequireStringAlt(Params, TEXT("parentLabel"), TEXT("parentPath"), ParentLabel)) return E;
4134
+
4135
+ // #983: both ends of an attachment take a path. Attaching to whichever
4136
+ // namesake the iterator reached first is how a prop ends up parented to a
4137
+ // building at the other end of the map.
4138
+ FMCPActorSelector ChildSel; ChildSel.LabelKey = TEXT("childLabel"); ChildSel.PathKey = TEXT("childPath");
4139
+ FMCPActorSelector ParentSel; ParentSel.LabelKey = TEXT("parentLabel"); ParentSel.PathKey = TEXT("parentPath");
4140
+ TSharedPtr<FJsonValue> ActorErr;
4141
+ AActor* Child = MCPResolveActor(World, Params, ActorErr, ChildSel);
4142
+ if (!Child) return ActorErr;
4143
+ AActor* Parent = MCPResolveActor(World, Params, ActorErr, ParentSel);
4144
+ if (!Parent) return ActorErr;
4145
+ ChildLabel = Child->GetActorLabel();
4146
+ ParentLabel = Parent->GetActorLabel();
3423
4147
 
3424
4148
  const FString ChildComponentSelector = OptionalString(Params, TEXT("childComponentName"));
3425
4149
  const FString ParentComponentSelector = OptionalString(Params, TEXT("parentComponentName"));
@@ -3645,10 +4369,13 @@ TSharedPtr<FJsonValue> FLevelHandlers::AttachComponent(const TSharedPtr<FJsonObj
3645
4369
  TSharedPtr<FJsonValue> FLevelHandlers::DetachComponent(const TSharedPtr<FJsonObject>& Params)
3646
4370
  {
3647
4371
  REQUIRE_EDITOR_WORLD(World);
3648
- FString ChildLabel; if (auto E = RequireString(Params, TEXT("childLabel"), ChildLabel)) return E;
4372
+ FString ChildLabel; if (auto E = RequireStringAlt(Params, TEXT("childLabel"), TEXT("childPath"), ChildLabel)) return E;
3649
4373
 
3650
- AActor* Child = FindActorByLabel(World, ChildLabel);
3651
- if (!Child) return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ChildLabel));
4374
+ FMCPActorSelector ChildSel; ChildSel.LabelKey = TEXT("childLabel"); ChildSel.PathKey = TEXT("childPath");
4375
+ TSharedPtr<FJsonValue> ActorErr;
4376
+ AActor* Child = MCPResolveActor(World, Params, ActorErr, ChildSel);
4377
+ if (!Child) return ActorErr;
4378
+ ChildLabel = Child->GetActorLabel();
3652
4379
 
3653
4380
  const FString ChildComponentSelector = OptionalString(Params, TEXT("childComponentName"));
3654
4381
  UActorComponent* ResolvedChildComponent = ChildComponentSelector.IsEmpty()
@@ -3708,11 +4435,13 @@ TSharedPtr<FJsonValue> FLevelHandlers::DetachComponent(const TSharedPtr<FJsonObj
3708
4435
  TSharedPtr<FJsonValue> FLevelHandlers::SetActorMobility(const TSharedPtr<FJsonObject>& Params)
3709
4436
  {
3710
4437
  REQUIRE_EDITOR_WORLD(World);
3711
- FString ActorLabel; if (auto E = RequireString(Params, TEXT("actorLabel"), ActorLabel)) return E;
4438
+ FString ActorLabel; if (auto E = RequireStringAlt(Params, TEXT("actorLabel"), TEXT("actorPath"), ActorLabel)) return E;
3712
4439
  FString MobilityStr; if (auto E = RequireString(Params, TEXT("mobility"), MobilityStr)) return E;
3713
4440
 
3714
- AActor* A = FindActorByLabel(World, ActorLabel);
3715
- if (!A) return MCPError(FString::Printf(TEXT("Actor not found: %s"), *ActorLabel));
4441
+ TSharedPtr<FJsonValue> ActorErr;
4442
+ AActor* A = MCPResolveActor(World, Params, ActorErr);
4443
+ if (!A) return ActorErr;
4444
+ ActorLabel = A->GetActorLabel();
3716
4445
  USceneComponent* Root = A->GetRootComponent();
3717
4446
  if (!Root) return MCPError(FString::Printf(TEXT("Actor '%s' has no root component"), *ActorLabel));
3718
4447
 
@@ -3731,12 +4460,14 @@ TSharedPtr<FJsonValue> FLevelHandlers::SetActorMobility(const TSharedPtr<FJsonOb
3731
4460
  auto Result = MCPSuccess();
3732
4461
  MCPSetUpdated(Result);
3733
4462
  Result->SetStringField(TEXT("actorLabel"), ActorLabel);
4463
+ Result->SetStringField(TEXT("actorPath"), A->GetPathName());
3734
4464
  Result->SetStringField(TEXT("mobility"), MobilityStr);
3735
4465
 
3736
4466
  const TCHAR* PrevStr = Prev == EComponentMobility::Movable ? TEXT("movable")
3737
4467
  : Prev == EComponentMobility::Stationary ? TEXT("stationary") : TEXT("static");
3738
4468
  TSharedPtr<FJsonObject> Payload = MakeShared<FJsonObject>();
3739
4469
  Payload->SetStringField(TEXT("actorLabel"), ActorLabel);
4470
+ Payload->SetStringField(TEXT("actorPath"), A->GetPathName());
3740
4471
  Payload->SetStringField(TEXT("mobility"), PrevStr);
3741
4472
  MCPSetRollback(Result, TEXT("set_actor_mobility"), Payload);
3742
4473
  return MCPResult(Result);
@@ -4021,7 +4752,22 @@ TSharedPtr<FJsonValue> FLevelHandlers::BatchTranslate(const TSharedPtr<FJsonObje
4021
4752
  {
4022
4753
  FString S; if (V.IsValid() && V->TryGetString(S))
4023
4754
  {
4024
- if (AActor* A = FindActorByLabel(World, S)) Targets.Add(A);
4755
+ // #983: a batch is the plural case, so a label naming several
4756
+ // actors moves all of them rather than one at random.
4757
+ TArray<AActor*> Matches;
4758
+ MCPCollectActorsByToken(World, S, EMCPActorMatch::Label, Matches);
4759
+ for (AActor* Match : Matches) Targets.Add(Match);
4760
+ }
4761
+ }
4762
+ }
4763
+ const TArray<TSharedPtr<FJsonValue>>* PathArr = nullptr;
4764
+ if (Params->TryGetArrayField(TEXT("actorPaths"), PathArr) && PathArr)
4765
+ {
4766
+ for (const auto& V : *PathArr)
4767
+ {
4768
+ FString S; if (V.IsValid() && V->TryGetString(S))
4769
+ {
4770
+ if (AActor* A = MCPFindActorByPath(World, S)) Targets.Add(A);
4025
4771
  }
4026
4772
  }
4027
4773
  }
@@ -4033,7 +4779,7 @@ TSharedPtr<FJsonValue> FLevelHandlers::BatchTranslate(const TSharedPtr<FJsonObje
4033
4779
  if (It->ActorHasTag(TagName)) Targets.Add(*It);
4034
4780
  }
4035
4781
  }
4036
- if (Targets.Num() == 0) return MCPError(TEXT("Provide actorLabels[] or tag matching at least one actor"));
4782
+ if (Targets.Num() == 0) return MCPError(TEXT("Provide actorLabels[], actorPaths[] or tag matching at least one actor"));
4037
4783
 
4038
4784
  for (AActor* A : Targets)
4039
4785
  {