ue-mcp 1.2.3 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/README.md +4 -3
  2. package/dist/action-class.js +56 -3
  3. package/dist/action-class.js.map +1 -1
  4. package/dist/bridge-timeouts.d.ts +64 -0
  5. package/dist/bridge-timeouts.js +118 -0
  6. package/dist/bridge-timeouts.js.map +1 -0
  7. package/dist/bridge.js +10 -2
  8. package/dist/bridge.js.map +1 -1
  9. package/dist/editor-control.d.ts +72 -5
  10. package/dist/editor-control.js +164 -140
  11. package/dist/editor-control.js.map +1 -1
  12. package/dist/editor-target.d.ts +55 -1
  13. package/dist/editor-target.js +107 -10
  14. package/dist/editor-target.js.map +1 -1
  15. package/dist/engine-observer.d.ts +59 -5
  16. package/dist/engine-observer.js +249 -29
  17. package/dist/engine-observer.js.map +1 -1
  18. package/dist/engine-root.d.ts +91 -0
  19. package/dist/engine-root.js +448 -0
  20. package/dist/engine-root.js.map +1 -0
  21. package/dist/epic-enrich.js +16 -1
  22. package/dist/epic-enrich.js.map +1 -1
  23. package/dist/feedback-cli.js +4 -0
  24. package/dist/feedback-cli.js.map +1 -1
  25. package/dist/feedback-deferred.d.ts +9 -0
  26. package/dist/feedback-deferred.js +2 -0
  27. package/dist/feedback-deferred.js.map +1 -1
  28. package/dist/feedback-fallback.d.ts +99 -0
  29. package/dist/feedback-fallback.js +173 -0
  30. package/dist/feedback-fallback.js.map +1 -0
  31. package/dist/flow/write-methods.js +20 -0
  32. package/dist/flow/write-methods.js.map +1 -1
  33. package/dist/index.js +41 -2
  34. package/dist/index.js.map +1 -1
  35. package/dist/lean-context.js +8 -3
  36. package/dist/lean-context.js.map +1 -1
  37. package/dist/locking.js +11 -2
  38. package/dist/locking.js.map +1 -1
  39. package/dist/plugin/loader.d.ts +4 -0
  40. package/dist/plugin/loader.js +22 -8
  41. package/dist/plugin/loader.js.map +1 -1
  42. package/dist/plugin/manifest.d.ts +72 -52
  43. package/dist/plugin/manifest.js +118 -21
  44. package/dist/plugin/manifest.js.map +1 -1
  45. package/dist/plugin-cli.js +16 -3
  46. package/dist/plugin-cli.js.map +1 -1
  47. package/dist/project.d.ts +11 -0
  48. package/dist/project.js +23 -2
  49. package/dist/project.js.map +1 -1
  50. package/dist/python-gate.d.ts +95 -0
  51. package/dist/python-gate.js +175 -0
  52. package/dist/python-gate.js.map +1 -0
  53. package/dist/tool-counts.json +16 -16
  54. package/dist/tools/animation.js +288 -24
  55. package/dist/tools/animation.js.map +1 -1
  56. package/dist/tools/asset.js +79 -14
  57. package/dist/tools/asset.js.map +1 -1
  58. package/dist/tools/audio.js +59 -5
  59. package/dist/tools/audio.js.map +1 -1
  60. package/dist/tools/blueprint.js +29 -12
  61. package/dist/tools/blueprint.js.map +1 -1
  62. package/dist/tools/chooser.js +4 -1
  63. package/dist/tools/chooser.js.map +1 -1
  64. package/dist/tools/editor.js +81 -43
  65. package/dist/tools/editor.js.map +1 -1
  66. package/dist/tools/feedback.js +214 -48
  67. package/dist/tools/feedback.js.map +1 -1
  68. package/dist/tools/foliage.js +27 -1
  69. package/dist/tools/foliage.js.map +1 -1
  70. package/dist/tools/gameplay.js +38 -11
  71. package/dist/tools/gameplay.js.map +1 -1
  72. package/dist/tools/gas.js +17 -5
  73. package/dist/tools/gas.js.map +1 -1
  74. package/dist/tools/landscape.js +22 -7
  75. package/dist/tools/landscape.js.map +1 -1
  76. package/dist/tools/level.js +322 -51
  77. package/dist/tools/level.js.map +1 -1
  78. package/dist/tools/material.js +10 -4
  79. package/dist/tools/material.js.map +1 -1
  80. package/dist/tools/niagara.js +3 -2
  81. package/dist/tools/niagara.js.map +1 -1
  82. package/dist/tools/pcg.js +6 -5
  83. package/dist/tools/pcg.js.map +1 -1
  84. package/dist/tools/plugins.js +2 -0
  85. package/dist/tools/plugins.js.map +1 -1
  86. package/dist/tools/project.js +47 -20
  87. package/dist/tools/project.js.map +1 -1
  88. package/dist/types.d.ts +24 -0
  89. package/dist/types.js +46 -3
  90. package/dist/types.js.map +1 -1
  91. package/dist/ue-mcp.default.yml +493 -125
  92. package/package.json +2 -2
  93. package/plugin/ue_mcp_bridge/NATIVE_RUNTIME_VISIBILITY.md +53 -0
  94. package/plugin/ue_mcp_bridge/NATIVE_STATIC_MESH_USAGE_PROGRESS.md +115 -0
  95. package/plugin/ue_mcp_bridge/README_MassHandlers.md +97 -0
  96. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/BridgeServer.cpp +14 -0
  97. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/GameThreadExecutor.cpp +21 -4
  98. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/GameThreadExecutor.h +9 -0
  99. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/HandlerJsonProperty.h +395 -69
  100. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers.cpp +79 -3
  101. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers.h +25 -0
  102. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_ControlRig.cpp +3 -3
  103. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_ControlRigSequencer.cpp +3061 -0
  104. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_IKRetargeterAuthoring.cpp +961 -0
  105. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_IKRigAuthoring.cpp +900 -0
  106. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Pose.cpp +769 -0
  107. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Sequence.cpp +44 -3
  108. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_SkeletalLive.cpp +217 -11
  109. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_StateMachine.cpp +551 -24
  110. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Validation.cpp +735 -0
  111. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers.cpp +352 -73
  112. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers.h +8 -0
  113. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkProperties.cpp +1 -1
  114. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkRead.cpp +503 -0
  115. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkRead.h +21 -0
  116. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkUpsert.cpp +2 -2
  117. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Enum.cpp +2 -2
  118. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Geometry.cpp +902 -0
  119. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Geometry.h +33 -0
  120. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Import.cpp +478 -134
  121. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Mesh.cpp +5 -5
  122. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_MeshBoolean.cpp +933 -0
  123. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_MeshBoolean.h +35 -0
  124. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Redirectors.cpp +478 -0
  125. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_RenderTarget.cpp +1 -1
  126. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Sockets.cpp +4 -4
  127. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Struct.cpp +2 -2
  128. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Subobject.cpp +319 -0
  129. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AudioHandlers_SoundCue.cpp +3 -3
  130. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers.cpp +482 -35
  131. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers.h +10 -0
  132. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Collision.cpp +502 -0
  133. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Collision.h +43 -0
  134. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Functions.cpp +89 -7
  135. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Graph.cpp +17 -51
  136. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Internal.h +107 -0
  137. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Properties.cpp +10 -10
  138. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Search.cpp +648 -0
  139. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DemoHandlers.cpp +5 -1
  140. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DiffHandlers.cpp +252 -1
  141. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DiffHandlers.h +10 -2
  142. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers.cpp +252 -41
  143. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers.h +4 -0
  144. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_Build.cpp +250 -27
  145. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_PIE.cpp +303 -125
  146. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_PIERuntime.cpp +73 -50
  147. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_RuntimeVisibility.cpp +757 -0
  148. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers.cpp +11 -2
  149. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers.h +3 -0
  150. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers_Batch.cpp +419 -0
  151. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers.cpp +39 -161
  152. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers.h +66 -1
  153. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_BTAuthoring.cpp +1323 -0
  154. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_BehaviorTree.cpp +1211 -0
  155. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.cpp +2 -0
  156. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.h +60 -0
  157. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers_Runtime.cpp +363 -12
  158. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/HandlerEditorState.h +84 -0
  159. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.cpp +1288 -36
  160. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.h +3 -0
  161. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers_Sculpt.cpp +41 -16
  162. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers.cpp +959 -213
  163. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers.h +66 -0
  164. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_BatchWrite.cpp +1571 -0
  165. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Convert.cpp +316 -0
  166. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Inspection.cpp +280 -0
  167. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_InstanceProjection.cpp +666 -0
  168. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_InstanceProjection_Internal.h +63 -0
  169. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Lights.cpp +43 -12
  170. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_MultiLevel.cpp +13 -47
  171. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_PostProcess.cpp +527 -0
  172. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Query.cpp +1169 -0
  173. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Refresh.cpp +723 -0
  174. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Save.cpp +325 -0
  175. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Spatial.cpp +360 -0
  176. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Trace.cpp +178 -58
  177. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Transient.cpp +337 -0
  178. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Volumes.cpp +25 -12
  179. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_WorldPartition.cpp +10 -9
  180. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_WorldPartitionSettings.cpp +686 -0
  181. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MassHandlers.cpp +467 -0
  182. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MassHandlers.h +22 -0
  183. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers.cpp +452 -175
  184. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers.h +5 -0
  185. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers_Build.cpp +597 -0
  186. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/NiagaraHandlers.cpp +17 -15
  187. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PCGHandlers.cpp +38 -168
  188. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PhysicsHandlers.cpp +39 -30
  189. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.cpp +392 -78
  190. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.h +2 -0
  191. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers_Internal.h +83 -0
  192. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SkeletalMeshHandlers.cpp +200 -0
  193. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SkeletalMeshHandlers.h +15 -0
  194. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SplineHandlers.cpp +19 -12
  195. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/StateTreeHandlers.cpp +31 -1
  196. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers.cpp +307 -69
  197. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers.h +84 -0
  198. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers_Extraction.cpp +8 -41
  199. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers_Properties.cpp +33 -52
  200. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/ActorSelectorTests.cpp +344 -0
  201. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AnimationControlRigTimelineTests.cpp +60 -0
  202. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AnimationPoseTests.cpp +115 -0
  203. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetBulkReadTests.cpp +177 -0
  204. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetDeleteGuardTests.cpp +276 -0
  205. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetPathResolutionTests.cpp +243 -0
  206. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetPropertyPersistTests.cpp +213 -0
  207. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetSubobjectTests.cpp +235 -0
  208. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AutomationRunnerTests.cpp +72 -0
  209. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BehaviorTreeAuthoringTests.cpp +526 -0
  210. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BehaviorTreeNodeTests.cpp +334 -0
  211. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BlueprintHandlerSurfaceTests.cpp +362 -0
  212. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BridgeProtocolTests.cpp +14 -0
  213. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/CollisionQueryTests.cpp +288 -0
  214. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/DataTableRowWriteTests.cpp +453 -0
  215. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/EditorFunctionCallTests.cpp +223 -0
  216. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/FoliageBatchTests.cpp +147 -0
  217. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/GameThreadGateTests.cpp +82 -0
  218. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/GasLiveAttributeTests.cpp +183 -0
  219. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LandscapeSampleTests.cpp +119 -0
  220. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelBatchWriteTests.cpp +176 -0
  221. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelComponentTreeInstanceCountTests.cpp +189 -0
  222. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelConvertBrushesTests.cpp +100 -0
  223. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelInstanceProjectionTests.cpp +265 -0
  224. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelPostProcessOverrideTests.cpp +258 -0
  225. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelQueryComponentsTests.cpp +195 -0
  226. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelRefreshTests.cpp +142 -0
  227. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelSaveReportTests.cpp +125 -0
  228. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelScriptHandlersTests.cpp +42 -0
  229. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelStaticMeshUsageTests.cpp +412 -0
  230. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelTraceWorldScopeTests.cpp +133 -0
  231. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelTransientActorTests.cpp +205 -0
  232. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelWorldPartitionSettingsTests.cpp +168 -0
  233. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MCPInstancedStructPathTestTypes.h +51 -0
  234. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MCPInstancedStructPathTests.cpp +379 -0
  235. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MaterialBuildTests.cpp +238 -0
  236. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MeshBooleanTests.cpp +170 -0
  237. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MeshGeometryTests.cpp +116 -0
  238. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/PackageSaveExtensionTests.cpp +161 -0
  239. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/PackageWriteGuardTests.cpp +253 -0
  240. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/RuntimeVisibilityTests.cpp +143 -0
  241. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SequencerHandlerTests.cpp +660 -0
  242. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SequencerScrubTests.cpp +50 -0
  243. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SkeletonDiffTests.cpp +685 -0
  244. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/WidgetBlueprintResolveTests.cpp +145 -0
  245. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/UE_MCP_Bridge.cpp +80 -0
  246. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerFunctionCall.h +509 -0
  247. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerQuery.h +407 -0
  248. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerUtils.h +1008 -77
  249. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/UE_MCP_Bridge.Build.cs +87 -0
  250. package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Private/MCPEngineStatus.cpp +152 -14
  251. package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Private/Tests/MCPEngineStatusTests.cpp +65 -0
  252. package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Public/MCPEngineStatus.h +32 -1
  253. package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/UE_MCP_BridgeStatus.Build.cs +2 -0
  254. package/skills/ue-mcp-animation/SKILL.md +154 -0
  255. package/skills/ue-mcp-blueprint/SKILL.md +71 -0
@@ -2,71 +2,96 @@ import { z } from "zod";
2
2
  import { categoryTool, bp } from "../types.js";
3
3
  import { Vec3, Rotator, Color } from "../schemas.js";
4
4
  export const levelTool = categoryTool("level", "Level actors, selection, components, level management, volumes, lights, and splines.", {
5
- get_outliner: bp("List actors (each row includes editorHidden - hidden in the viewport but still rendering in game). Params: classFilter?, nameFilter?, editorHidden? (filter to only hidden/only visible), world? (editor|pie|auto), limit?, includeStreaming? (#717)", "get_world_outliner"),
5
+ get_outliner: bp("List actors (each row includes editorHidden - hidden in the viewport but still rendering in game, and folderPath). classFilter is a case-sensitive SUBSTRING over the class name by default; pass exactClass=true to require the exact class instead. folderPath matches one World Outliner folder exactly and folderPathPrefix matches that folder and everything nested under it, so narrowing a folder to a single class no longer needs a get_actor_details round trip per entry (#911). Params: classFilter?, exactClass?, nameFilter? (case-sensitive substring over the internal name OR the label), folderPath?, folderPathPrefix?, editorHidden? (filter to only hidden/only visible), world? (editor|pie|auto), limit?, includeStreaming? (#717)", "get_world_outliner"),
6
6
  set_editor_visibility: bp("Bulk set editor-only visibility (temporarily-hidden-in-editor) on actors. Targets actorLabels[] or all=true. Params: hidden (required; true=hide, false=show), actorLabels?, all? (#717)", "set_editor_visibility", (p) => ({ hidden: p.hidden, actorLabels: p.actorLabels, all: p.all })),
7
7
  place_actor: bp("Spawn actor. Pass world:pie to spawn into the running PIE world (#585). Params: actorClass, label?, location?, rotation?, scale?, staticMesh?, material?, world? (editor|pie)", "place_actor"),
8
- delete_actor: bp("Remove actor. Params: actorLabel", "delete_actor"),
9
- get_actor_details: bp("Inspect actor. Params: actorLabel OR actorPath, includeProperties?, propertyName?, world? (editor|pie)", "get_actor_details"),
10
- move_actor: bp("Transform actor. Pass world:pie to move a live PIE actor (resolves labels/names from get_outliner {world:pie}) (#586). Params: actorLabel, location?, rotation?, scale?, world? (editor|pie)", "move_actor", (p) => ({ actorLabel: p.actorLabel, location: p.location, rotation: p.rotation, scale: p.scale, world: p.world, pieInstance: p.pieInstance })),
11
- aim_actor_at: bp("Rotate an actor so its forward (+X) points at a target. Params: actorLabel, targetPoint (Vec3) OR targetActor (label), roll? (default 0), world? (editor|pie) (#566)", "aim_actor_at", (p) => ({ actorLabel: p.actorLabel, target: p.targetPoint, targetActor: p.targetActor, roll: p.roll, world: p.world, pieInstance: p.pieInstance })),
8
+ delete_actor: bp("Remove actor. Params: actorLabel OR actorPath. A label matching several actors is refused with the candidates rather than deleting one of them (#983)", "delete_actor"),
9
+ get_actor_details: bp("Inspect actor. With includeProperties=true, a UPROPERTY declared as a C-style FIXED ARRAY (int32 Foo[3], one FProperty with ArrayDim 3) comes back as a JSON ARRAY of its elements plus arrayDim, instead of element 0 presented as the whole value. That mattered on RecastNavMesh NavMeshResolutionParams, whose three entries are the Low, Default and High generation tiers, and where reporting only Low sent a user tuning cell sizes against numbers the engine was not using (#927). Params: actorLabel OR actorPath, includeProperties?, propertyName?, world? (editor|pie)", "get_actor_details"),
10
+ move_actor: bp("Transform actor. Pass world:pie to move a live PIE actor (resolves labels/names from get_outliner {world:pie}) (#586). Params: actorLabel OR actorPath, location?, rotation?, scale?, world? (editor|pie)", "move_actor", (p) => ({ actorLabel: p.actorLabel, actorPath: p.actorPath, location: p.location, rotation: p.rotation, scale: p.scale, world: p.world, pieInstance: p.pieInstance })),
11
+ aim_actor_at: bp("Rotate an actor so its forward (+X) points at a target. Params: actorLabel OR actorPath, targetPoint (Vec3) OR targetActor (label) OR targetActorPath, roll? (default 0), world? (editor|pie) (#566/#983)", "aim_actor_at", (p) => ({ actorLabel: p.actorLabel, actorPath: p.actorPath, target: p.targetPoint, targetActor: p.targetActor, targetActorPath: p.targetActorPath, roll: p.roll, world: p.world, pieInstance: p.pieInstance })),
12
12
  nav_project_point: bp("Project a world point onto the navmesh. Returns onNavMesh + projectedLocation. Params: point (Vec3), extent? (Vec3, default 100), world? (editor|pie) (#585)", "nav_project_point", (p) => ({ point: p.point, extent: p.extent, world: p.world, pieInstance: p.pieInstance })),
13
- select: bp("Select actors. Params: actorLabels[]", "select_actors"),
13
+ select: bp("Select actors. A label naming several actors selects all of them; selectedPaths reports exactly which. Params: actorLabels[] and/or actorPaths[] (#983)", "select_actors"),
14
14
  get_selected: bp("Get selection", "get_selected_actors"),
15
- add_component: bp("Add component to actor. Params: actorLabel, componentClass, componentName?", "add_component_to_actor"),
16
- remove_component: bp("Remove instance component from a level actor by name. Idempotent: returns alreadyDeleted=true if no matching component exists. Params: actorLabel, componentName (#426)", "remove_component_from_actor", (p) => ({ actorLabel: p.actorLabel, componentName: p.componentName })),
17
- set_component_property: bp("Set component prop. Pass value=null to clear a TObjectPtr/SoftObject/WeakObject/UClass/Interface reference (#420). Resolves inherited/SCS components on placed Blueprint instances case-insensitively, and refreshes the scene transform after RelativeLocation/RelativeRotation/RelativeScale3D writes (#539). Pass world='pie' (with optional pieInstance) to write to a LIVE PIE component rather than the editor one (#763). Params: actorLabel, componentName, propertyName, value, world?, pieInstance?", "set_component_property"),
18
- get_component_details: bp("Read a placed actor's component transforms. With componentName returns that component's relative+world location/rotation/scale, class, and attach parent; without it lists every component. With includeValues=true also dumps arbitrary UPROPERTY values (custom fields, CharacterMovement MaxWalkSpeed, etc.); world='pie' reads the live PIE instance (#539/#584). Params: actorLabel, componentName?, includeValues?, propertyNames? (filter), world? (editor|pie)", "get_component_details", (p) => ({ actorLabel: p.actorLabel, componentName: p.componentName, includeValues: p.includeValues, propertyNames: p.propertyNames, world: p.world, pieInstance: p.pieInstance })),
15
+ add_component: bp("Add component to actor. Params: actorLabel OR actorPath, componentClass, componentName?", "add_component_to_actor"),
16
+ remove_component: bp("Remove instance component from a level actor by name. Idempotent: returns alreadyDeleted=true if no matching component exists. Params: actorLabel OR actorPath, componentName (#426)", "remove_component_from_actor", (p) => ({ actorLabel: p.actorLabel, actorPath: p.actorPath, componentName: p.componentName })),
17
+ set_component_property: bp("Set component prop. A C-style FIXED ARRAY element is addressed by index in the dotted path ('Foo[2].Bar'); without an index every write lands on element 0 (#927). Pass value=null to clear a TObjectPtr/SoftObject/WeakObject/UClass/Interface reference (#420). Resolves inherited/SCS components on placed Blueprint instances case-insensitively, and refreshes the scene transform after RelativeLocation/RelativeRotation/RelativeScale3D writes (#539). Pass world='pie' (with optional pieInstance) to write to a LIVE PIE component rather than the editor one (#763). Params: actorLabel OR actorPath, componentName, propertyName, value, world?, pieInstance?", "set_component_property"),
18
+ nudge_component: bp("Make a small spatial adjustment to an exact named SceneComponent without editing raw relative Euler angles. For visual tuning, normalize the user's request as target + reference frame + viewpoint + operation + amount + constraints. Character left/right means actor-relative; viewer-relative directions must be stated explicitly; clockwise/counterclockwise requires a viewpoint. Infer obvious context and echo the normalized interpretation before applying; ask one concise question only when the frame or viewpoint cannot be resolved. Translation uses forward/right/up centimetres in frame=world|actor|parent|component. Rotation is one axis-angle quaternion delta in that frame, so attached/socketed components avoid gimbal-like pitch/roll drift. Unspecified location, rotation and scale are preserved. After a visual nudge, verify with a fixed-pose close packet: for character body attachments, stop actor/root movement and freeze the parent skeletal pose before the baseline, then hold both across every probe and capture; light the contact area from the front, capture front/side/top/three-quarter views with the target filling roughly 50-75% of the frame, and reject dark or wide-only evidence. Returns before/after relative and world transforms, resolved world deltas, and exact rollback data. Params: actorLabel OR actorPath, componentName, frame? (default actor), translationDelta? ({forwardCm?,rightCm?,upCm?}), axisRotation? ({axis:forward|right|up,degrees}), scaleMultiplier? (>0), world? (editor|pie), pieInstance?", "nudge_component", (p) => ({ actorLabel: p.actorLabel, actorPath: p.actorPath, componentName: p.componentName, frame: p.frame, translationDelta: p.translationDelta, axisRotation: p.axisRotation, scaleMultiplier: p.scaleMultiplier, world: p.world, pieInstance: p.pieInstance })),
19
+ get_component_details: bp("Read a placed actor's component transforms. With componentName returns that component's relative+world location/rotation/scale, class, and attach parent; without it lists every component. With includeValues=true also dumps arbitrary UPROPERTY values (custom fields, CharacterMovement MaxWalkSpeed, etc.); world='pie' reads the live PIE instance (#539/#584). Params: actorLabel OR actorPath, componentName?, includeValues?, propertyNames? (filter), world? (editor|pie)", "get_component_details", (p) => ({ actorLabel: p.actorLabel, actorPath: p.actorPath, componentName: p.componentName, includeValues: p.includeValues, propertyNames: p.propertyNames, world: p.world, pieInstance: p.pieInstance })),
19
20
  get_current: bp("Get current level name and path", "get_current_level"),
20
21
  load: bp("Load level. Params: levelPath", "load_level"),
21
- save: bp("Save current level", "save_current_level"),
22
+ clear_level_script: bp("Preview or remove every node and member variable from the currently loaded persistent level's Level Blueprint. Defaults to dryRun=true. Actors are untouched; save=true saves only the current level after a successful compile. Params: dryRun?, save?", "clear_level_script", (p) => ({ dryRun: p.dryRun, save: p.save })),
23
+ save: bp("Save the level currently being edited, and say what happened to each package. Takes the same package path editor(save_dirty) uses, so the two can no longer disagree about whether one package was written: the old action returned a bare {success:false, error:'Failed to save current level'} on a map that save_dirty then wrote seconds later, and a false failure makes an agent redo work that was already on disk (#964). On a World Partition or one-file-per-actor map the actors live in their own packages, so those are saved too and each is reported separately. A package that was already clean is reported as skipped, not as a failure. On failure the error names the package, the resolved file, whether that file exists and is read-only, the engine's ESavePackageResult and the error/warning lines the save itself emitted. Saving during PIE is refused by name rather than failing silently. Params: force? (write even a clean package, default false), includeExternalActors? (default true)", "save_level", (p) => ({ force: p.force, includeExternalActors: p.includeExternalActors })),
22
24
  list: bp("List levels. Params: directory?, recursive?", "list_levels"),
23
25
  create: bp("Create new level. Params: levelPath?, templateLevel?", "create_new_level"),
24
26
  spawn_volume: bp("Place volume. Params: volumeType, location?, extent?, label?", "spawn_volume"),
25
27
  list_volumes: bp("List volumes. Params: volumeType?", "list_volumes"),
26
- set_volume_properties: bp("Edit volume. Params: actorLabel, properties", "set_volume_properties"),
28
+ set_volume_properties: bp("Edit volume. Params: actorLabel OR actorPath, properties", "set_volume_properties"),
27
29
  spawn_light: bp("Place light. Params: lightType (point|spot|directional|rect|sky), location?, rotation?, intensity?, color? ({r,g,b} 0-255), attenuationRadius? (point/spot/rect only; #723), mobility? (static|stationary|movable; default movable so the light renders without a build), label? (#331/#310)", "spawn_light"),
28
- set_light_properties: bp("Edit a light OR SkyLight (intensity/color now work on SkyLight too, #608). Params: actorLabel, intensity?, color?, rotation? (DirectionalLight sun angle), mobility? (static|stationary|movable), recaptureSky?, volumetricScatteringIntensity?, sourceRadius? (point/spot), innerConeAngle?/outerConeAngle? (spot)", "set_light_properties"),
29
- set_fog_properties: bp("Edit ExponentialHeightFog incl. volumetric fog (#608). Params: actorLabel?, fogDensity?, fogHeightFalloff?, startDistance?, fogInscatteringColor?, enableVolumetricFog?, volumetricFogScatteringDistribution?, volumetricFogExtinctionScale?, volumetricFogDistance?, volumetricFogAlbedo?", "set_fog_properties"),
30
+ set_light_properties: bp("Edit a light OR SkyLight (intensity/color now work on SkyLight too, #608). Params: actorLabel OR actorPath, intensity?, color?, rotation? (DirectionalLight sun angle), mobility? (static|stationary|movable), recaptureSky?, volumetricScatteringIntensity?, sourceRadius? (point/spot), innerConeAngle?/outerConeAngle? (spot)", "set_light_properties"),
31
+ set_fog_properties: bp("Edit ExponentialHeightFog incl. volumetric fog (#608). Params: actorLabel? OR actorPath?, fogDensity?, fogHeightFalloff?, startDistance?, fogInscatteringColor?, enableVolumetricFog?, volumetricFogScatteringDistribution?, volumetricFogExtinctionScale?, volumetricFogDistance?, volumetricFogAlbedo?", "set_fog_properties"),
30
32
  get_actors_by_class: bp("List actors by class. Matches Blueprint subclasses of a native base via IsChildOf when className resolves to a UClass (short name, /Script path, or BP class path), else falls back to name-substring. Each actor includes location/rotation/scale. Params: className, world? (editor|pie), matchSubclasses? (default true), includeTransforms? (default true) (#675)", "get_actors_by_class", (p) => ({ className: p.className, world: p.world, pieInstance: p.pieInstance, matchSubclasses: p.matchSubclasses, includeTransforms: p.includeTransforms })),
31
- get_actors_by_component_class: bp("List actors that own a component of a given class (exact or substring match). Returns each actor plus its matchedComponents. Params: componentClass, world? (editor|pie) (#582)", "get_actors_by_component_class", (p) => ({ componentClass: p.componentClass, className: p.className, world: p.world, pieInstance: p.pieInstance })),
33
+ get_actors_by_component_class: bp("List actors that own a component of a given class (exact or substring match). Returns each actor plus its matchedComponents. For a whole-map question use level(query_components), which filters and counts in the editor instead of returning every candidate. Params: componentClass, world? (editor|pie) (#582)", "get_actors_by_component_class", (p) => ({ componentClass: p.componentClass, className: p.className, world: p.world, pieInstance: p.pieInstance })),
34
+ query_components: {
35
+ description: "Ask one level-wide question about components and get the answer, not the candidates. Selection, projection, predicates, grouping and counts all evaluate in the editor, so a 4,000 actor map costs one call instead of thousands. Computed (not raw) values: shadow.effectiveCastShadow crosses CastShadow with visibility, bHiddenInGame, owner hidden and bCastHiddenShadow, so a hidden debug marker with CastShadow=true is correctly NOT a shadow caster; every mobility and collision enum comes back as a clean name (Movable, QueryAndPhysics), never as EComponentMobility.MOVABLE; navigation.effectiveNavRelevant crosses bCanEverAffectNavigation with collision and custom navigable geometry. Never writes, and reports dirtiedPackages so you can see that. Field groups for 'fields' and for dot paths: transform, bounds, localBounds (unscaled, includes UBoxComponent BoxExtent), shadow, nanite, navigation, tick, materials (per slot: effective path, componentOverride vs meshDefault, slot name/index, isNull, isWorldGrid), mesh (adds instanceCount + hierarchical for an ISM/HISM, #986), decal, health (roll-up incl. suspect). A dot path used in where/groupBy/countBy turns its own group on. groupBy and countBy COUNT COMPONENTS, not instances: one HISM holding 5,000 instances counts as 1, so for per-mesh placement totals use level(summarize_static_mesh_usage), which sums instances and counts distinct actors. Params: componentClass?, actorClass?, matchSubclasses? (default true), componentNameContains?, actorLabelPrefix?, actorLabelContains?, actorTag?, folderPath?, folderPathPrefix?, fields? (string[]), propertyNames? (string[], projected component UPROPERTYs under props.*), where? ([{field, op, value}]; ops eq/ne/lt/lte/gt/gte/contains/notContains/startsWith/endsWith/in/notIn/exists/notExists/isNull/isNotNull/isTrue/isFalse), whereMode? (all|any, default all), suspectOnly?, groupBy?, countBy? (string[]), sampleLimit?, countOnly?, limit? (default 200, max 2000), startIndex?, duplicateTransformTolerance? (cm; reports duplicateTransformGroups), levelPath? (temporarily opens another map: refuses if anything is dirty and restores the map that was open), world? (editor|pie) (#910/#943/#912)",
36
+ bridge: "query_components",
37
+ timeoutMs: 300_000,
38
+ mapParams: (p) => ({
39
+ componentClass: p.componentClass, actorClass: p.actorClass, matchSubclasses: p.matchSubclasses,
40
+ componentNameContains: p.componentNameContains, actorLabelPrefix: p.actorLabelPrefix,
41
+ actorLabelContains: p.actorLabelContains, actorTag: p.actorTag,
42
+ folderPath: p.folderPath, folderPathPrefix: p.folderPathPrefix,
43
+ fields: p.fields, propertyNames: p.propertyNames, where: p.where, whereMode: p.whereMode,
44
+ suspectOnly: p.suspectOnly, groupBy: p.groupBy, countBy: p.countBy, sampleLimit: p.sampleLimit,
45
+ countOnly: p.countOnly, limit: p.limit, startIndex: p.startIndex,
46
+ duplicateTransformTolerance: p.duplicateTransformTolerance,
47
+ levelPath: p.levelPath, world: p.world, pieInstance: p.pieInstance,
48
+ }),
49
+ },
32
50
  count_actors_by_class: bp("Histogram of actor classes in the level (sorted desc). Params: world? (editor|pie), topN? (#146)", "count_actors_by_class", (p) => ({ world: p.world, pieInstance: p.pieInstance, topN: p.topN })),
33
51
  get_runtime_virtual_texture_summary: bp("List RuntimeVirtualTextureVolume actors + their bound VirtualTexture assets (#150)", "get_runtime_virtual_texture_summary"),
34
- set_water_body_property: bp("Set a property on an actor's WaterBodyComponent (ShapeDilation, WaterLevel, etc.). Params: actorLabel, propertyName, value. Requires Water plugin. For spline edits use level(set_spline_points) (#151)", "set_water_body_property", (p) => ({ actorLabel: p.actorLabel, propertyName: p.propertyName, value: p.value })),
52
+ set_water_body_property: bp("Set a property on an actor's WaterBodyComponent (ShapeDilation, WaterLevel, etc.). Params: actorLabel OR actorPath, propertyName, value. Requires Water plugin. For spline edits use level(set_spline_points) (#151)", "set_water_body_property", (p) => ({ actorLabel: p.actorLabel, actorPath: p.actorPath, propertyName: p.propertyName, value: p.value })),
35
53
  build_lighting: bp("Build lights. Params: quality?", "build_lighting"),
36
- get_spline_info: bp("Read a spline component's points, closedLoop, length, and per-point tangents/types. Works in editor or PIE (#553). Optional componentName picks a specific (custom) spline; projectPoint (Vec3) returns the closest location, inputKey, distanceAlongSpline, distanceToSpline, and tangent (#555). Params: actorLabel, componentName?, world? (editor|pie), projectPoint?", "get_spline_info", (p) => ({ actorLabel: p.actorLabel, componentName: p.componentName, world: p.world, pieInstance: p.pieInstance, projectPoint: p.projectPoint })),
37
- set_spline_points: bp("Set spline points. Params: actorLabel, points[], closedLoop?", "set_spline_points"),
38
- set_actor_material: bp("Set material on actor. Params: actorLabel, materialPath, slotIndex?", "set_actor_material"),
54
+ get_spline_info: bp("Read a spline component's points, closedLoop, length, and per-point tangents/types. Works in editor or PIE (#553). Optional componentName picks a specific (custom) spline; projectPoint (Vec3) returns the closest location, inputKey, distanceAlongSpline, distanceToSpline, and tangent (#555). Params: actorLabel OR actorPath, componentName?, world? (editor|pie), projectPoint?", "get_spline_info", (p) => ({ actorLabel: p.actorLabel, actorPath: p.actorPath, componentName: p.componentName, world: p.world, pieInstance: p.pieInstance, projectPoint: p.projectPoint })),
55
+ set_spline_points: bp("Set spline points. Params: actorLabel OR actorPath, points[], closedLoop?", "set_spline_points"),
56
+ set_actor_material: bp("Set material on actor. Params: actorLabel OR actorPath, materialPath, slotIndex?", "set_actor_material"),
39
57
  get_world_settings: bp("Read world settings (GameMode, KillZ, gravity, etc.)", "get_world_settings"),
40
58
  set_world_settings: bp("Set world settings. Params: defaultGameMode?, killZ?, globalGravityZ?, enableWorldBoundsChecks?", "set_world_settings"),
41
59
  get_actor_bounds: bp("Get actor AABB. Params: actorLabel. Returns origin + extent (#188)", "get_actor_bounds"),
42
- get_component_tree: bp("Deep component-tree dump for an actor. Returns per-component: name, class, attachParent, attachSocket, mobility, visibility, relative+world transforms, tags. For PrimitiveComponents adds collisionProfile/collisionEnabled/bounds/castShadow. For StaticMeshComponent adds staticMesh + materials[]. For SkeletalMeshComponent adds skeletalMesh + skeleton + materials[]. For NiagaraComponent adds niagara{asset,active,visible} and for AudioComponent audio{sound,playing} - runtime FX state in PIE (#581). Params: actorLabel | actorPath, world? (editor|pie), componentClass? (substring filter), includeProperties? (dump UPROPERTY name/type/value per component) (#240/#241/#302/#320/#370/#353/#581)", "get_component_tree", (p) => ({ actorLabel: p.actorLabel, actorPath: p.actorPath, world: p.world, pieInstance: p.pieInstance, componentClass: p.componentClass, includeProperties: p.includeProperties })),
43
- get_relative_transform: bp("Compute target's transform in reference's local space (location/rotation/scale). Common dungeon/calibration workflow. Params: target (actor label), reference (actor label), world? (#386/#387)", "get_relative_transform", (p) => ({ targetLabel: p.target, referenceLabel: p.reference, target: p.target, reference: p.reference, world: p.world, pieInstance: p.pieInstance })),
60
+ get_component_tree: bp("Deep component-tree dump for an actor. Returns per-component: name, class, attachParent, attachSocket, mobility, visibility, relative+world transforms, tags. For PrimitiveComponents adds collisionProfile/collisionEnabled/bounds/castShadow. For StaticMeshComponent adds staticMesh + materials[], and for an ISM/HISM also instanceCount plus instanced{hierarchical,numCustomDataFloats} - the number you need before deciding whether to touch an ISM at all, since a component holding three instances and one holding three hundred thousand were otherwise the same row (#986). For SkeletalMeshComponent adds skeletalMesh + skeleton + materials[]. For NiagaraComponent adds niagara{asset,active,visible} and for AudioComponent audio{sound,playing} - runtime FX state in PIE (#581). Params: actorLabel OR actorPath, world? (editor|pie), componentClass? (substring filter), includeProperties? (dump UPROPERTY name/type/value per component) (#240/#241/#302/#320/#370/#353/#581)", "get_component_tree", (p) => ({ actorLabel: p.actorLabel, actorPath: p.actorPath, world: p.world, pieInstance: p.pieInstance, componentClass: p.componentClass, includeProperties: p.includeProperties })),
61
+ get_relative_transform: bp("Compute target's transform in reference's local space (location/rotation/scale). Common dungeon/calibration workflow. Params: target/targetLabel (actor label) OR targetPath, reference/referenceLabel OR referencePath, world? (#386/#387/#983)", "get_relative_transform", (p) => ({ targetLabel: p.targetLabel ?? p.target, targetPath: p.targetPath, referenceLabel: p.referenceLabel ?? p.reference, referencePath: p.referencePath, target: p.target, reference: p.reference, world: p.world, pieInstance: p.pieInstance })),
44
62
  resolve_actor: bp("Resolve internal/runtime actor name to editor label. Params: internalName (e.g. StaticMeshActor_141). Returns actorLabel, actorPath, className, location (#178)", "resolve_actor"),
45
- set_actor_property: bp("Set per-instance UPROPERTY on a level actor. Params: actorLabel ('WorldSettings' targets the world settings actor), propertyName (dotted paths like 'Foo.Bar' supported), value (string/number/bool/object/array; a label string resolves to an AActor* ref, and a JSON array of labels populates a TArray of actor refs #538), force? (bypass EditDefaultsOnly), world? (editor|pie) (#202/#230)", "set_actor_property", (p) => ({ actorLabel: p.actorLabel, propertyName: p.propertyName, value: p.value, force: p.force, world: p.world, pieInstance: p.pieInstance })),
46
- read_actor_motion: bp("Snapshot motion telemetry for one or many actors: location, rotation, velocity, scale, angularVelocity (when simulating physics), grounded + distanceToGround (downward 200u trace). Defaults to the PIE world with editor fallback. Loop at your sample interval for long telemetry probes. Params: actorLabel? OR actorLabels (string[]), world? ('pie'|'editor'), pieInstance? (#453)", "read_actor_motion", (p) => ({ actorLabel: p.actorLabel, actorLabels: p.actorLabels, world: p.world, pieInstance: p.pieInstance })),
47
- add_hismc_instances: bp("Bulk-add transforms to a HISMC / ISMC component on an actor (Python add_instance crashes on UE 5.7; this is the C++ path). Params: actorLabel, componentName? (default: first ISMC/HISMC found), transforms ([{location: {x,y,z}, rotation?: {pitch,yaw,roll}, scale?: {x,y,z}}]), worldSpace? (default true) (#434)", "add_hismc_instances", (p) => ({ actorLabel: p.actorLabel, componentName: p.componentName, transforms: p.transforms, worldSpace: p.worldSpace })),
48
- get_instance_transforms: bp("Read every instance transform on an actor's ISMC/HISMC. Params: actorLabel, componentName? (default first), worldSpace? (default true). Returns instances[{index, location, rotation, scale}] (#697)", "get_instance_transforms", (p) => ({ actorLabel: p.actorLabel, componentName: p.componentName, worldSpace: p.worldSpace })),
49
- update_instance_transform: bp("Update a single ISMC/HISMC instance by index; unspecified location/rotation/scale are preserved. Params: actorLabel, componentName?, index, location?, rotation?, scale?, worldSpace? (default true) (#697)", "update_instance_transform", (p) => ({ actorLabel: p.actorLabel, componentName: p.componentName, index: p.index, location: p.location, rotation: p.rotation, scale: p.scale, worldSpace: p.worldSpace })),
50
- remove_instance: bp("Remove a single ISMC/HISMC instance by index. Params: actorLabel, componentName?, index (#697)", "remove_instance", (p) => ({ actorLabel: p.actorLabel, componentName: p.componentName, index: p.index })),
63
+ set_actor_property: bp("Set per-instance UPROPERTY on a level actor. Params: actorLabel OR actorPath ('WorldSettings' as the label targets the world settings actor), propertyName (dotted paths like 'Foo.Bar' supported, and a C-style FIXED ARRAY element is addressed by index: 'NavMeshResolutionParams[1].CellSize' reaches the Default tier rather than always writing element 0 (#927)), value (string/number/bool/object/array; a label string resolves to an AActor* ref, and a JSON array of labels populates a TArray of actor refs #538), force? (bypass EditDefaultsOnly), world? (editor|pie) (#202/#230)", "set_actor_property", (p) => ({ actorLabel: p.actorLabel, actorPath: p.actorPath, propertyName: p.propertyName, value: p.value, force: p.force, world: p.world, pieInstance: p.pieInstance })),
64
+ read_actor_motion: bp("Snapshot motion telemetry for one or many actors: location, rotation, velocity, scale, angularVelocity (when simulating physics), grounded + distanceToGround (downward 200u trace). Defaults to the PIE world with editor fallback. Loop at your sample interval for long telemetry probes. Params: actorLabel? OR actorLabels (string[]) OR actorPath? OR actorPaths (string[]), world? ('pie'|'editor'), pieInstance? (#453)", "read_actor_motion", (p) => ({ actorLabel: p.actorLabel, actorPath: p.actorPath, actorLabels: p.actorLabels, actorPaths: p.actorPaths, world: p.world, pieInstance: p.pieInstance })),
65
+ add_hismc_instances: bp("Bulk-add transforms to a HISMC / ISMC component on an actor (Python add_instance crashes on UE 5.7; this is the C++ path). Params: actorLabel OR actorPath, componentName? (default: first ISMC/HISMC found), transforms ([{location: {x,y,z}, rotation?: {pitch,yaw,roll}, scale?: {x,y,z}}]), worldSpace? (default true) (#434)", "add_hismc_instances", (p) => ({ actorLabel: p.actorLabel, actorPath: p.actorPath, componentName: p.componentName, transforms: p.transforms, worldSpace: p.worldSpace })),
66
+ get_instance_transforms: bp("Read every instance transform on an actor's ISMC/HISMC. Params: actorLabel OR actorPath, componentName? (default first), worldSpace? (default true). Returns instances[{index, location, rotation, scale}] (#697)", "get_instance_transforms", (p) => ({ actorLabel: p.actorLabel, actorPath: p.actorPath, componentName: p.componentName, worldSpace: p.worldSpace })),
67
+ update_instance_transform: bp("Update a single ISMC/HISMC instance by index; unspecified location/rotation/scale are preserved. Params: actorLabel OR actorPath, componentName?, index, location?, rotation?, scale?, worldSpace? (default true) (#697)", "update_instance_transform", (p) => ({ actorLabel: p.actorLabel, actorPath: p.actorPath, componentName: p.componentName, index: p.index, location: p.location, rotation: p.rotation, scale: p.scale, worldSpace: p.worldSpace })),
68
+ remove_instance: bp("Remove a single ISMC/HISMC instance by index. Params: actorLabel OR actorPath, componentName?, index (#697)", "remove_instance", (p) => ({ actorLabel: p.actorLabel, actorPath: p.actorPath, componentName: p.componentName, index: p.index })),
51
69
  set_nanite_settings: bp("Enable/disable + force-build Nanite on a UStaticMesh asset (rebuilds render data immediately, not on next cook). Params: assetPath, enabled? (default true), positionPrecision? (#696)", "set_nanite_settings", (p) => ({ assetPath: p.assetPath, enabled: p.enabled, positionPrecision: p.positionPrecision })),
52
70
  get_nanite_info: bp("Read a UStaticMesh's Nanite state (naniteEnabled, positionPrecision, numLODs). Params: assetPath (#696)", "get_nanite_info", (p) => ({ assetPath: p.assetPath })),
53
- add_post_process_blendable: bp("Add a material blendable (post-process material or MID, e.g. a toon/outline pass) to a PostProcessVolume's WeightedBlendables. Params: actorLabel (the PPV), materialPath, weight? (default 1) (#666)", "add_post_process_blendable", (p) => ({ actorLabel: p.actorLabel, materialPath: p.materialPath, weight: p.weight })),
54
- export_actor_fbx: bp("Export an actor's skeletal/static mesh to FBX and write a metadata sidecar JSON (actor transform, mesh, materials, skeleton) for a downstream bridge (e.g. MetaTailor). Params: actorLabel, outputPath (.fbx). Returns fbx + metadata paths (#637)", "export_actor_fbx", (p) => ({ actorLabel: p.actorLabel, outputPath: p.outputPath })),
55
- spawn_skeletal_mesh_actor: bp("Spawn a SkeletalMeshActor for visual/deform verification. Optional per-slot materials and single-node animation preview (animSequence + loop). Returns actorLabel + boxExtent. The animSequence is written to the component's editable AnimationData, so it persists across a level save/reload instead of only driving the runtime preview and coming back in A-pose (#790). Params: skeletalMesh, label?, location?, rotation?, scale?, materials? (path[]), animSequence?, loop? (#679/#677)", "spawn_skeletal_mesh_actor", (p) => ({ skeletalMesh: p.skeletalMesh, label: p.label, location: p.location, rotation: p.rotation, scale: p.scale, materials: p.materials, animSequence: p.animSequence, loop: p.loop, onConflict: p.onConflict })),
56
- delete_actors: bp("Bulk-delete actors. Params: at least one of labelPrefix, className, tag; dryRun? to preview. Returns matched/deleted/labels (#220)", "delete_actors", (p) => ({ labelPrefix: p.labelPrefix, className: p.className, tag: p.tag, dryRun: p.dryRun })),
71
+ add_post_process_blendable: bp("Add a material blendable (post-process material or MID, e.g. a toon/outline pass) to a PostProcessVolume's WeightedBlendables. Params: actorLabel OR actorPath (the PPV), materialPath, weight? (default 1) (#666)", "add_post_process_blendable", (p) => ({ actorLabel: p.actorLabel, actorPath: p.actorPath, materialPath: p.materialPath, weight: p.weight })),
72
+ set_post_process_settings: bp("Write post-process settings AND enable the bOverride_<Name> flag for every key given. Inside FPostProcessSettings each value is gated by its own override bit, so writing AutoExposureMinBrightness through the generic property setter stores the number, leaves the bit off, and the engine keeps using the default while the details panel shows the value you asked for. Nothing errors. That is the trap this closes: the auto-enable IS the action. Works off the struct's reflected fields, so every setting is reachable, not just exposure, and a key that has no override bit (WeightedBlendables) says so instead of implying one was enabled. Every key is resolved BEFORE anything is written, so a typo fails the call rather than half-applying it, and each key reports previousValue, newValue, overrideWasEnabled and overrideEnabled read back from the struct. Finds the settings on the actor or on one of its components; a camera with several camera components needs componentName (or propertyName) and is refused by listing the candidates rather than by picking one. The level is left dirty and is NOT saved. Params: actorLabel OR actorPath, settings (object of settingName -> value), enableOverrides? (default true), componentName?, propertyName? (#950)", "set_post_process_settings", (p) => ({ actorLabel: p.actorLabel, actorPath: p.actorPath, settings: p.settings, enableOverrides: p.enableOverrides, componentName: p.componentName, propertyName: p.propertyName })),
73
+ get_post_process_settings: bp("Read post-process settings with the override bit that decides whether each one applies. onlyOverridden answers 'is this volume actually overriding exposure' directly, instead of dumping the whole FPostProcessSettings as one ExportText blob to parse. Each row is name, type, value, overridden (the bOverride_<Name> bit) and hasOverrideFlag; the bits themselves are not listed as settings of their own, because a caller who writes the bit without the value has changed nothing. Also returns totalSettings and overriddenCount, so 'nothing is overridden here' is a fact rather than an empty list. Params: actorLabel OR actorPath, onlyOverridden? (default false), names? (string[] exact names), nameContains? (substring filter), componentName?, propertyName? (#950)", "get_post_process_settings", (p) => ({ actorLabel: p.actorLabel, actorPath: p.actorPath, onlyOverridden: p.onlyOverridden, names: p.names, nameContains: p.nameContains, componentName: p.componentName, propertyName: p.propertyName })),
74
+ set_fixed_exposure: bp("Pin exposure on a post-process volume or camera in one call. Writes the same value to AutoExposureMinBrightness and AutoExposureMaxBrightness, which is how the engine disables eye adaptation, and enables both override flags so the values actually apply. Params: actorLabel OR actorPath, exposure (the fixed adaptation brightness), bias? (AutoExposureBias, also override-enabled), componentName?, propertyName? (#950)", "set_fixed_exposure", (p) => ({ actorLabel: p.actorLabel, actorPath: p.actorPath, exposure: p.exposure, bias: p.bias, componentName: p.componentName, propertyName: p.propertyName })),
75
+ export_actor_fbx: bp("Export an actor's skeletal/static mesh to FBX and write a metadata sidecar JSON (actor transform, mesh, materials, skeleton) for a downstream bridge (e.g. MetaTailor). Params: actorLabel OR actorPath, outputPath (.fbx). Returns fbx + metadata paths (#637)", "export_actor_fbx", (p) => ({ actorLabel: p.actorLabel, actorPath: p.actorPath, outputPath: p.outputPath })),
76
+ spawn_skeletal_mesh_actor: bp("Spawn a SkeletalMeshActor for visual/deform verification. Optional per-slot COMPONENT material overrides (they write the component's OverrideMaterials, not the mesh ASSET's slots, which material(build_material) writes) and single-node animation preview (animSequence + loop). Each entry in materials[] comes back with ok, and a path that does not load or a slot index past the mesh's slot count is reported rather than silently dropped, because a silently dropped assignment reads as one that worked. To dress an actor that is already placed, or to put one material on every slot, use level(set_component_materials) (#946). Returns actorLabel + boxExtent + materialSlotCount. The animSequence is written to the component's editable AnimationData, so it persists across a level save/reload instead of only driving the runtime preview and coming back in A-pose (#790). Params: skeletalMesh, label?, location?, rotation?, scale?, materials? (path[]), animSequence?, loop? (#679/#677)", "spawn_skeletal_mesh_actor", (p) => ({ skeletalMesh: p.skeletalMesh, label: p.label, location: p.location, rotation: p.rotation, scale: p.scale, materials: p.materials, animSequence: p.animSequence, loop: p.loop, onConflict: p.onConflict })),
77
+ delete_actors: bp("Bulk-delete actors. The filter names say exactly what they match, because two level actions taking similar-sounding filters with different semantics is how a caller ends up trusting a wrong zero (#963): labelPrefix is a CASE-SENSITIVE PREFIX over the EDITOR LABEL, labelContains a case-insensitive SUBSTRING over the label, nameContains a case-insensitive substring over the INTERNAL name, className a case-sensitive substring over the class name. get_outliner's nameFilter is looser than any of them (label OR internal name, substring), so a string that selects a set there can select a different set here. When nothing matches, the response explains it rather than returning a bare zero: zeroMatchHint reports how many actors the same string WOULD have matched under the other semantics with samples, and a World Partition map says that only loaded actors were enumerated. classPathContains is a case-insensitive substring of the generated UClass path (e.g. /SurvivalGameKitV2/), so a whole vendor folder can be selected without listing every Blueprint class, and classPathContainsAny is the same match against any of several substrings (#924). Params: at least one of labelPrefix, labelContains, nameContains, className, tag, classPathContains, classPathContainsAny; dryRun? to preview. Returns matched/deleted/labels/classPaths (#220/#924/#963)", "delete_actors", (p) => ({ labelPrefix: p.labelPrefix, labelContains: p.labelContains, nameContains: p.nameContains, className: p.className, tag: p.tag, classPathContains: p.classPathContains, classPathContainsAny: p.classPathContainsAny, dryRun: p.dryRun })),
57
78
  delete_exact_labeled_actors_in_levels: bp("Delete actors by EXACT label across several map packages in one call, without leaving the maps open or dirty. Previews by default (dryRun defaults to TRUE, the opposite of delete_actors), and every level is loaded and preflighted before anything is deleted, so a bad label in the last map cannot half-commit the first. Refuses the whole request on duplicate labels, a class that does not match expectedClassPath, actors living in a streaming sublevel, WorldSettings/default brush/level blueprint, World Partition maps, a read-only .umap, or an editor that is already dirty. Saves only the maps it actually changed, then returns to the map that was open. Limits: 16 levels, 256 labels per level. Params: levels[] ({levelPath, actorLabels[], expectedClassPath?}), dryRun? (default true), onMissing? (error|ignore, default error), restoreOriginalLevel? (default true)", "delete_exact_labeled_actors_in_levels", (p) => ({ levels: p.levels, dryRun: p.dryRun, onMissing: p.onMissing, restoreOriginalLevel: p.restoreOriginalLevel })),
58
79
  list_actor_descs: bp("World Partition ONLY: list on-disk actor descriptors, including actors whose cell is not loaded and which every other actor query therefore reports as absent. Returns guid/label/class/bounds/runtimeGrid/dataLayers plus a loaded flag, and separate loadedMatches/unloadedMatches counts so 'not streamed in' is distinguishable from 'does not exist'. Needs UE 5.5 or newer. Params: filter? (case-insensitive over label/name/class/path), className?, guids?, bounds? ({min:{x,y,z},max:{x,y,z}} intersection test), loadedOnly?, unloadedOnly?, limit? (default 500) (#746)", "list_actor_descs", (p) => ({ filter: p.filter, className: p.className, guids: p.guids, bounds: p.bounds, loadedOnly: p.loadedOnly, unloadedOnly: p.unloadedOnly, limit: p.limit })),
59
80
  load_actor_descs: bp("World Partition ONLY: pin matched actors so they become resident and the normal actor actions can reach them, or unpin to release. Pinning is used rather than transient streaming because a transient load is dropped again by the streaming system mid-measurement. Takes the same filters as list_actor_descs and refuses to act on more than maxActors (default 256) so an unfiltered call cannot pin a whole map by accident. Reports residentAfter, read back from the world rather than assumed. Needs UE 5.5 or newer. Params: mode? (pin|unpin, default pin), filter?, className?, guids?, bounds?, maxActors?, dryRun? (#746)", "load_actor_descs", (p) => ({ mode: p.mode, filter: p.filter, className: p.className, guids: p.guids, bounds: p.bounds, maxActors: p.maxActors, dryRun: p.dryRun })),
81
+ get_world_partition_settings: bp("World Partition ONLY: the streaming knobs on the open map, plus the runtime cell transformer stack. Returns the world partition and runtime hash classes, whether streaming is enabled, the default HLOD layer, and grids[] with each grid's name, CellSize, LoadingRange and a dotted path rooted at the world partition. Grids are found by REFLECTION rather than against a fixed class, so this answers for the RuntimeHashSet layout and the older spatial hash alike. cellTransformers[] lists each stack entry with its class, its instancePath and the current values of the instanced transformer object's editable properties, which no read exposed. Params: none (#985)", "get_world_partition_settings"),
82
+ set_world_partition_settings: bp("World Partition ONLY: write the streaming knobs. cellSize and loadingRange are the shorthand for the primary tuning pair and pick the map's only grid automatically, or name one with grid (its name) or gridPath (the dotted path from get_world_partition_settings). settings takes any dotted path rooted at the world partition, which is how the INSTANCED runtime cell transformer object is configured ('RuntimeCellsTransformerStack[0].Instance.SomeProperty') - that object is read-only through Python and was previously set by hand in the editor UI. Every path is resolved BEFORE anything is written, so a bad path fails the call rather than leaving the map streaming on a mixture nobody asked for, and each write reports previousValue and the value read back. Streaming settings only apply to cells generated after the change, so rebuild the streaming data before measuring. The map package is left dirty and is NOT saved. Params: cellSize?, loadingRange?, grid? (name), gridPath?, settings? (object of dotted path -> value) (#985)", "set_world_partition_settings", (p) => ({ cellSize: p.cellSize, loadingRange: p.loadingRange, grid: p.grid, gridPath: p.gridPath, settings: p.settings })),
83
+ add_runtime_cell_transformer: bp("World Partition ONLY: append a WorldPartitionRuntimeCellTransformer to RuntimeCellsTransformerStack and configure its instanced object in the same call. Adding the entry without being able to write the instance would leave the manual editor step exactly where it was, so the instance is created here and properties are applied to it. skipIfPresent (default true) makes a rerun a no-op that reports the existing index instead of stacking duplicates. position inserts at an index rather than appending, because the stack is ordered. Params: transformerClass (short name, Module.Class or /Script path), properties? (object applied to the instance), position? (default -1 = append), skipIfPresent? (default true) (#985)", "add_runtime_cell_transformer", (p) => ({ transformerClass: p.transformerClass, properties: p.properties, position: p.position, skipIfPresent: p.skipIfPresent })),
84
+ set_actor_hlod_layer: bp("Assign (or clear) the per-actor HLODLayer override across every actor a selector picks. Assigning one across 295 InstancedFoliageActors was a Python loop. Goes through the engine's own AActor::SetHLODLayer rather than writing the private property directly, so the result behaves like an edit made in the details panel. hlodLayer=null clears the override. An actor already carrying the requested layer is reported as unchanged rather than counted as an update, so a rerun is readable. Optionally sets bEnableAutoLODGeneration in the same pass, since the layer only matters when the actor is allowed to build HLODs at all. On a World Partition map this dirties one package per actor and does NOT save. Params: hlodLayer (asset path or null), at least one of actorLabels[]/labelPrefix/labelContains/tag/classFilter/folderPath/folderPathPrefix, matchSubclasses? (default true), enableAutoLODGeneration?, dryRun?, transactionLabel? (#985)", "set_actor_hlod_layer", (p) => ({ hlodLayer: p.hlodLayer, actorLabels: p.actorLabels, labelPrefix: p.labelPrefix, labelContains: p.labelContains, tag: p.tag, classFilter: p.classFilter, folderPath: p.folderPath, folderPathPrefix: p.folderPathPrefix, matchSubclasses: p.matchSubclasses, enableAutoLODGeneration: p.enableAutoLODGeneration, dryRun: p.dryRun, transactionLabel: p.transactionLabel })),
60
85
  set_actor_folder_path: bp("Assign World Outliner folder paths in bulk. Filter with actorLabels (exact), labelPrefix, className or tag; pass an empty folderPath to move actors back to the root. Runs in one transaction so the whole move is a single undo, reads each write back (verified), and reports missingLabels for names that matched no actor. Editor-only organisation: the level is left dirty and is NOT saved. Params: folderPath, actorLabels?, labelPrefix?, className?, tag?, dryRun?, transactionLabel? (#767)", "set_actor_folder_path", (p) => ({ folderPath: p.folderPath, actorLabels: p.actorLabels, labelPrefix: p.labelPrefix, className: p.className, tag: p.tag, dryRun: p.dryRun, transactionLabel: p.transactionLabel })),
61
- add_actor_tag: bp("Append a tag to an actor's Tags array. Params: actorLabel, tag (#219)", "add_actor_tag", (p) => ({ actorLabel: p.actorLabel, tag: p.tag })),
62
- remove_actor_tag: bp("Remove a tag from an actor's Tags array. Params: actorLabel, tag (#219)", "remove_actor_tag", (p) => ({ actorLabel: p.actorLabel, tag: p.tag })),
63
- set_actor_tags: bp("Replace an actor's Tags array. Params: actorLabel, tags[] (#219)", "set_actor_tags", (p) => ({ actorLabel: p.actorLabel, tags: p.tags })),
64
- list_actor_tags: bp("List an actor's Tags. Params: actorLabel (#219)", "list_actor_tags", (p) => ({ actorLabel: p.actorLabel })),
65
- attach_actor: bp("Attach actor as child. Params: childLabel, parentLabel, attachRule? (KeepWorld|KeepRelative|SnapToTarget; default KeepWorld), socketName? (#205)", "attach_actor", (p) => ({ childLabel: p.childLabel, parentLabel: p.parentLabel, attachRule: p.attachRule, socketName: p.socketName })),
66
- detach_actor: bp("Detach actor from parent. Params: childLabel (#205)", "detach_actor", (p) => ({ childLabel: p.childLabel })),
67
- attach_component: bp("Attach an actor root or exact named child SceneComponent to an actor root or exact named parent SceneComponent. A non-root child changes only component hierarchy, not actor parentage. An already-matching attachment is a no-op and does not reapply transform rules. Returns verified resolved component and socket details; native attachment failure is an error. Params: childLabel, parentLabel, childComponentName? (default child root), parentComponentName? (default parent root), socketName?, attachRule? (KeepWorld|KeepRelative|SnapToTarget; default KeepWorld), weldSimulatedBodies? (default false)", "attach_component", (p) => ({ childLabel: p.childLabel, parentLabel: p.parentLabel, childComponentName: p.childComponentName, parentComponentName: p.parentComponentName, socketName: p.socketName, attachRule: p.attachRule, weldSimulatedBodies: p.weldSimulatedBodies })),
68
- detach_component: bp("Detach an actor root or exact named child SceneComponent while preserving world transform. Omitted childComponentName selects the actor root. An already-detached component is a verified no-op. Returns previous parent component/socket details plus alreadyDetached and detachmentChanged. Params: childLabel, childComponentName?", "detach_component", (p) => ({ childLabel: p.childLabel, childComponentName: p.childComponentName })),
69
- set_actor_mobility: bp("Set actor root component Mobility. Params: actorLabel, mobility (static|stationary|movable) (#205)", "set_actor_mobility", (p) => ({ actorLabel: p.actorLabel, mobility: p.mobility })),
86
+ add_actor_tag: bp("Append a tag to an actor's Tags array. Params: actorLabel OR actorPath, tag (#219)", "add_actor_tag", (p) => ({ actorLabel: p.actorLabel, actorPath: p.actorPath, tag: p.tag })),
87
+ remove_actor_tag: bp("Remove a tag from an actor's Tags array. Params: actorLabel OR actorPath, tag (#219)", "remove_actor_tag", (p) => ({ actorLabel: p.actorLabel, actorPath: p.actorPath, tag: p.tag })),
88
+ set_actor_tags: bp("Replace an actor's Tags array. Params: actorLabel OR actorPath, tags[] (#219)", "set_actor_tags", (p) => ({ actorLabel: p.actorLabel, actorPath: p.actorPath, tags: p.tags })),
89
+ list_actor_tags: bp("List an actor's Tags. Params: actorLabel OR actorPath (#219/#983)", "list_actor_tags", (p) => ({ actorLabel: p.actorLabel, actorPath: p.actorPath })),
90
+ attach_actor: bp("Attach actor as child. Params: childLabel OR childPath, parentLabel OR parentPath, attachRule? (KeepWorld|KeepRelative|SnapToTarget; default KeepWorld), socketName? (#205)", "attach_actor", (p) => ({ childLabel: p.childLabel, childPath: p.childPath, parentLabel: p.parentLabel, parentPath: p.parentPath, attachRule: p.attachRule, socketName: p.socketName })),
91
+ detach_actor: bp("Detach actor from parent. Params: childLabel OR childPath (#205/#983)", "detach_actor", (p) => ({ childLabel: p.childLabel, childPath: p.childPath })),
92
+ attach_component: bp("Attach an actor root or exact named child SceneComponent to an actor root or exact named parent SceneComponent. A non-root child changes only component hierarchy, not actor parentage. An already-matching attachment is a no-op and does not reapply transform rules. Returns verified resolved component and socket details; native attachment failure is an error. Params: childLabel OR childPath, parentLabel OR parentPath, childComponentName? (default child root), parentComponentName? (default parent root), socketName?, attachRule? (KeepWorld|KeepRelative|SnapToTarget; default KeepWorld), weldSimulatedBodies? (default false)", "attach_component", (p) => ({ childLabel: p.childLabel, childPath: p.childPath, parentLabel: p.parentLabel, parentPath: p.parentPath, childComponentName: p.childComponentName, parentComponentName: p.parentComponentName, socketName: p.socketName, attachRule: p.attachRule, weldSimulatedBodies: p.weldSimulatedBodies })),
93
+ detach_component: bp("Detach an actor root or exact named child SceneComponent while preserving world transform. Omitted childComponentName selects the actor root. An already-detached component is a verified no-op. Returns previous parent component/socket details plus alreadyDetached and detachmentChanged. Params: childLabel OR childPath, childComponentName?", "detach_component", (p) => ({ childLabel: p.childLabel, childPath: p.childPath, childComponentName: p.childComponentName })),
94
+ set_actor_mobility: bp("Set actor root component Mobility. Params: actorLabel OR actorPath, mobility (static|stationary|movable) (#205)", "set_actor_mobility", (p) => ({ actorLabel: p.actorLabel, actorPath: p.actorPath, mobility: p.mobility })),
70
95
  get_current_edit_level: bp("Read the active edit-target sub-level (#204)", "get_current_edit_level"),
71
96
  set_current_edit_level: bp("Set the active edit-target sub-level so subsequent spawns land in it. Params: levelName (e.g. SubLevel_A) (#204)", "set_current_edit_level", (p) => ({ levelName: p.levelName, levelPath: p.levelPath })),
72
97
  list_streaming_sublevels: bp("List streaming sub-levels with transform + initially-loaded/visible flags (#206)", "list_streaming_sublevels"),
@@ -74,10 +99,107 @@ export const levelTool = categoryTool("level", "Level actors, selection, compone
74
99
  remove_streaming_sublevel: bp("Remove a streaming sub-level. Params: levelName | levelPath (#206)", "remove_streaming_sublevel", (p) => ({ levelName: p.levelName, levelPath: p.levelPath })),
75
100
  set_streaming_sublevel_properties: bp("Update sub-level transform/visibility flags. Params: levelName | levelPath, location?, initiallyLoaded?, initiallyVisible?, editorVisible? (#206)", "set_streaming_sublevel_properties", (p) => ({ levelName: p.levelName, levelPath: p.levelPath, location: p.location, initiallyLoaded: p.initiallyLoaded, initiallyVisible: p.initiallyVisible, editorVisible: p.editorVisible })),
76
101
  spawn_grid: bp("Batch-spawn StaticMeshActors on a grid. Params: staticMesh, min, max (Vec3 bounds), countX?, countY?, countZ?, jitter?, labelPrefix? (#203)", "spawn_grid", (p) => ({ staticMesh: p.staticMesh, min: p.min, max: p.max, countX: p.countX, countY: p.countY, countZ: p.countZ, jitter: p.jitter, labelPrefix: p.labelPrefix })),
77
- batch_translate: bp("Translate a set of actors by an offset. Params: offset (Vec3), actorLabels[] OR tag (#203)", "batch_translate", (p) => ({ offset: p.offset, actorLabels: p.actorLabels, tag: p.tag })),
102
+ batch_translate: bp("Translate a set of actors by an offset. Params: offset (Vec3), actorLabels[] and/or actorPaths[] OR tag (#203/#983)", "batch_translate", (p) => ({ offset: p.offset, actorLabels: p.actorLabels, actorPaths: p.actorPaths, tag: p.tag })),
78
103
  place_actors_batch: bp("Bulk-spawn StaticMeshActors with per-instance mesh + transform. Params: actors[]: [{staticMesh, location?, rotation?, scale?, label?}]. Mesh loads cached per path. Returns spawned/failedMesh/failedSpawn counts + labels (#264)", "place_actors_batch", (p) => ({ actors: p.actors })),
79
- line_trace: bp("Line trace in the editor world. Traces simple collision by default, the same as a gameplay trace, so the result matches what the running game hits; pass traceComplex to trace per-triangle instead. channel picks the collision channel, including channels this project renamed in its collision settings. The response echoes traceComplex and channel, and faceIndex is only present on a complex hit. Returns hit + actorLabel/actorClass/componentName/componentClass/location/impactPoint/normal/distance/faceIndex/boneName/physicalMaterial. Params: start (Vec3), end? (Vec3) OR direction? (Vec3) + distance? (default 200000), traceComplex? (default false), channel? (default Visibility), ignoreActors? (array of labels) (#420, #807)", "line_trace", (p) => ({ start: p.start, end: p.end, direction: p.direction, distance: p.distance, traceComplex: p.traceComplex, channel: p.channel, ignoreActors: p.ignoreActors })),
80
- snap_actor_to_floor: bp("Snap an actor's bounds-bottom to the first downward line-trace hit. Equivalent of the End-key shortcut, works on arbitrary geometry (not just Landscape). Params: actorLabel, floorOffset? (added to impact Z, default 0), maxDistance? (default 100000) (#419)", "snap_actor_to_floor", (p) => ({ actorLabel: p.actorLabel, floorOffset: p.floorOffset, maxDistance: p.maxDistance })),
104
+ batch_set_properties: {
105
+ description: "Set the same properties on every actor an editor-side SELECTOR picks. batch_translate needs an explicit label list, which for auto-numbered labels is a payload you have to generate first; this takes tag / classFilter / labelPrefix / labelContains / folderPath instead, so '190 SpotLights tagged LichtTest' is one call. Property writes delegate to set_actor_property, so dotted paths, actor-label object references and TArray-of-actor-labels all behave identically to the single-actor action. The whole batch is ONE undo transaction. dryRun resolves every property against every matched actor's class and reports currentValue without writing. The level is left dirty and is NOT saved. Params: properties (object of propertyName -> value, max 32), at least one of actorLabels[]/labelPrefix/labelContains/tag/classFilter/folderPath/folderPathPrefix, matchSubclasses? (default true), dryRun?, force? (bypass EditDefaultsOnly), transactionLabel? (#984)",
106
+ bridge: "batch_set_actor_properties",
107
+ timeoutMs: 300_000,
108
+ mapParams: (p) => ({
109
+ properties: p.properties, actorLabels: p.actorLabels, labelPrefix: p.labelPrefix,
110
+ labelContains: p.labelContains, tag: p.tag, classFilter: p.classFilter,
111
+ folderPath: p.folderPath, folderPathPrefix: p.folderPathPrefix,
112
+ matchSubclasses: p.matchSubclasses, dryRun: p.dryRun, force: p.force,
113
+ transactionLabel: p.transactionLabel,
114
+ }),
115
+ },
116
+ bulk_set_component_property: {
117
+ description: "Set one property on the SAME NAMED COMPONENT across every actor a selector picks (the real case: NavModifierComponent.AreaClass on dozens of placed StaticMeshActors). Delegates to set_component_property per actor, so value semantics match exactly, including JSON null to clear an object reference. An actor that has no component of that name is reported as no_such_component rather than counted as a failure or silently skipped. One undo transaction. The level is left dirty and is NOT saved. Params: componentName, propertyName, value, at least one of actorLabels[]/labelPrefix/labelContains/tag/classFilter/folderPath/folderPathPrefix, matchSubclasses? (default true), dryRun?, transactionLabel? (#941)",
118
+ bridge: "bulk_set_component_property",
119
+ timeoutMs: 300_000,
120
+ mapParams: (p) => ({
121
+ componentName: p.componentName, propertyName: p.propertyName, value: p.value,
122
+ actorLabels: p.actorLabels, labelPrefix: p.labelPrefix, labelContains: p.labelContains,
123
+ tag: p.tag, classFilter: p.classFilter, folderPath: p.folderPath,
124
+ folderPathPrefix: p.folderPathPrefix, matchSubclasses: p.matchSubclasses,
125
+ dryRun: p.dryRun, transactionLabel: p.transactionLabel,
126
+ }),
127
+ },
128
+ remove_components_by_class: {
129
+ description: "Remove every instance component of a class across the actors a selector picks. remove_component takes one name at a time, which cannot reach 230 orphaned SplineMeshComponents whose names are auto-generated (SplineMeshComponent_0, _1, ...). dryRun DEFAULTS TO TRUE here, unlike delete_actors: this deletes components you cannot enumerate by name, so you see the list first. Only INSTANCE components are removed; a native or SCS component belongs to the class and is reported under skippedNonInstanceComponents rather than destroyed, because destroying it would come back on the next construction rerun. Omitting every actor selector means every loaded actor, which is a legitimate request and the reason for the dryRun default. One undo transaction. Params: componentClass, actorClassFilter? (or classFilter), componentNameContains?, matchComponentSubclasses? (default true), actorLabels?/labelPrefix?/labelContains?/tag?/folderPath?/folderPathPrefix?, dryRun? (default TRUE), save? (default false; the level is otherwise left dirty), transactionLabel? (#907)",
130
+ bridge: "remove_components_by_class",
131
+ timeoutMs: 300_000,
132
+ mapParams: (p) => ({
133
+ componentClass: p.componentClass, actorClassFilter: p.actorClassFilter,
134
+ classFilter: p.classFilter, componentNameContains: p.componentNameContains,
135
+ matchComponentSubclasses: p.matchComponentSubclasses,
136
+ actorLabels: p.actorLabels, labelPrefix: p.labelPrefix, labelContains: p.labelContains,
137
+ tag: p.tag, folderPath: p.folderPath, folderPathPrefix: p.folderPathPrefix,
138
+ matchSubclasses: p.matchSubclasses, dryRun: p.dryRun, save: p.save,
139
+ transactionLabel: p.transactionLabel,
140
+ }),
141
+ },
142
+ spawn_actors_batch: {
143
+ description: "Bulk-spawn actors of ANY class with per-instance properties, from explicit transforms or from positions DERIVED in the editor. place_actors_batch is StaticMeshActor only and spawn_light takes one position. Exactly one source: instances[] ({location?, rotation?, scale?, label?, properties?}); fromComponents ({componentClass?, componentNameContains?, actor selector, offset?, extentFraction?, space?, inheritRotation?}) which places one actor per matched component at boundsOrigin + boxExtent*extentFraction + offset; or alongSpline ({actorLabel, componentName?, spacing (cm), startDistance?, endDistance?, offset?, alignToTangent? (default true)}). fromComponents defaults to space='local' on purpose: a component's WORLD bounds are axis-aligned, so 'the top of the mesh' computed from them is not on a rotated lamp post, while local bounds carry the component's own orientation. That is what makes 'a SpotLight at 72 percent of each of 190 lamp posts' one call instead of 190 bounds reads and 190 spawns. dryRun returns every computed transform without spawning. maxSpawn (default 500) refuses a plan bigger than you meant. The level is left dirty and is NOT saved. Params: actorClass, exactly one of instances[]/fromComponents/alongSpline, properties? (applied to every spawn; per-instance properties win), labelPrefix?, dryRun?, maxSpawn?, transactionLabel? (#987)",
144
+ bridge: "spawn_actors_batch",
145
+ timeoutMs: 300_000,
146
+ mapParams: (p) => ({
147
+ actorClass: p.actorClass, instances: p.instances, fromComponents: p.fromComponents,
148
+ alongSpline: p.alongSpline, properties: p.properties, labelPrefix: p.labelPrefix,
149
+ dryRun: p.dryRun, maxSpawn: p.maxSpawn, transactionLabel: p.transactionLabel,
150
+ }),
151
+ },
152
+ set_component_materials: {
153
+ description: "Set per-slot COMPONENT material overrides on placed actors. This is the other half of material assignment and the half that had no action: material(build_material) and asset(set_property) write the mesh ASSET's slots, while a placed actor usually needs the instance override the details panel edits (OverrideMaterials). set_actor_material does one slot on whichever primitive component it finds first, so dressing a placed skeletal mesh meant a Python loop over get_num_materials(). Exactly one of: materials[] (index = slot; a null or empty entry clears just that slot's override), material (one path applied to EVERY slot), clearOverrides=true (drop every override so the asset's own slots show through). Every slot comes back with its previous and new path AND whether each is a componentOverride or the meshDefault, read back rather than echoed. Every material path is resolved before anything is written, so a bad path fails the call instead of half-applying it. Works on any UMeshComponent (static or skeletal). One undo transaction; the LEVEL is dirtied and NOT saved, the mesh asset is untouched. Params: at least one of actorLabels[]/labelPrefix/labelContains/tag/classFilter/folderPath/folderPathPrefix, componentName? (default the actor's first mesh component), materials? OR material? OR clearOverrides?, dryRun?, transactionLabel? (#946)",
154
+ bridge: "set_component_materials",
155
+ timeoutMs: 300_000,
156
+ mapParams: (p) => ({
157
+ actorLabels: p.actorLabels, labelPrefix: p.labelPrefix, labelContains: p.labelContains,
158
+ tag: p.tag, classFilter: p.classFilter, folderPath: p.folderPath,
159
+ folderPathPrefix: p.folderPathPrefix, matchSubclasses: p.matchSubclasses,
160
+ componentName: p.componentName, materials: p.materials, material: p.material,
161
+ clearOverrides: p.clearOverrides, dryRun: p.dryRun, transactionLabel: p.transactionLabel,
162
+ }),
163
+ },
164
+ spawn_transient_actor: bp("Spawn an actor for VERIFICATION that cannot be saved into the map. Checking anything that only exists on a live actor needs a subject, and spawning a normal one and forgetting to delete it is how a SceneCapture2D ended up committed to source control. So this actor is transient by construction: RF_Transient (the package serializer skips it), bTemporaryEditorActor (an editor preview actor, kept out of the level's persistent actor list), no external actor package on a World Partition map, and its label is set with bMarkDirty=false because SetActorLabel dirties the map by default. It carries a UEMCP_TransientVerification tag so it can be listed and so destroy refuses anything it did not create, and the response reports dirtiedPackages rather than promising nothing was dirtied. initialize controls how far towards a running state it is taken, because the EDITOR WORLD HAS NOT BEGUN PLAY and a spawned actor therefore gets neither InitializeComponent nor BeginPlay: 'none' spawns only, 'construction' (default) reruns construction scripts, 'beginPlay' also registers components and dispatches the begin-play cycle on this actor, which is what a component doing its setup in BeginPlay (a GAS ability system component's attribute-set scan, for one) needs before it can be read. Each returned component reports registered and initialized so the caller can see what actually happened. Destroy it with destroy_transient_actor. Params: actorClass, location?, rotation?, scale?, label?, initialize? (none|construction|beginPlay), hideFromOutliner?, world? (editor|pie) (#956)", "spawn_transient_actor", (p) => ({ actorClass: p.actorClass, location: p.location, rotation: p.rotation, scale: p.scale, label: p.label, initialize: p.initialize, hideFromOutliner: p.hideFromOutliner, world: p.world, pieInstance: p.pieInstance })),
165
+ destroy_transient_actor: bp("Destroy an actor created by spawn_transient_actor. It will ONLY destroy an actor carrying the UEMCP_TransientVerification tag and RF_Transient: a label or path that resolves to a real level actor is refused by name rather than deleted, because this is the cleanup call and a mistyped label at cleanup time is how real work disappears. Use delete_actor or delete_actors for a real level actor. Params: actorPath OR actorLabel OR all=true (every transient verification actor in this world), world? (editor|pie) (#956)", "destroy_transient_actor", (p) => ({ actorPath: p.actorPath, actorLabel: p.actorLabel, all: p.all, world: p.world, pieInstance: p.pieInstance })),
166
+ list_transient_actors: bp("List the verification actors spawned by spawn_transient_actor that are still in the world, so nothing is left behind by accident. They cannot be saved into the map and do not survive a map reload, but they are live until destroyed. Params: world? (editor|pie) (#956)", "list_transient_actors", (p) => ({ world: p.world, pieInstance: p.pieInstance })),
167
+ convert_brushes_to_static_mesh: {
168
+ description: "Convert BSP brushes into StaticMeshActors via UEditorActorSubsystem::ConvertActors, generating the static meshes into destinationPath. Finishing a blockout pass otherwise meant doing it by hand. dryRun DEFAULTS TO TRUE, because conversion DESTROYS the source brushes and there is no way back to a builder brush once the map is saved. Every candidate the selector picked comes back with a verdict, so nothing is silently dropped: the default builder brush, brush shapes, volumes (both derive from ABrush and neither is geometry a player sees) and subtractive brushes (they carve space and have no surface, so converting one yields an empty mesh that looks like success) are all excluded unless you opt in by name. classFilter is EXACT by default here. Reports convertedFrom (recorded before the call, since the actors are gone after it), newActorLabels and newStaticMeshes. Params: actorLabels[] OR folderPath (+ recursiveFolder?, default true), classFilter?, exactClass? (default true), destinationPath? (default /Game/Meshes/Converted), dryRun? (default TRUE), allowSubtractive?, includeVolumes? (#911)",
169
+ bridge: "convert_brushes_to_static_mesh",
170
+ timeoutMs: 600_000,
171
+ mapParams: (p) => ({
172
+ actorLabels: p.actorLabels, folderPath: p.folderPath, recursiveFolder: p.recursiveFolder,
173
+ classFilter: p.classFilter, exactClass: p.exactClass, destinationPath: p.destinationPath,
174
+ dryRun: p.dryRun, allowSubtractive: p.allowSubtractive, includeVolumes: p.includeVolumes,
175
+ }),
176
+ },
177
+ rerun_construction: {
178
+ description: "Re-run the construction script on PLACED actor instances, so an SCS or construction-script change reaches the copies already in the level instead of only new ones. blueprint(run_construction_script) builds a TEMPORARY actor and throws it away, and Python cannot do this at all because Actor.rerun_construction_scripts() is missing from the wrapper. Requires actorLabels or className: this destroys and rebuilds every generated component on each match, so it will not run against a whole level implicitly. Reports reran, failed[], samples[], and how many matches have no user construction script (their generated components are still rebuilt, but a caller expecting a script change to land needs to know). Leaves the level dirty and does NOT save. Params: actorLabels? (string[]) and/or className? (Blueprint or native class), matchSubclasses? (default true), world? (editor|pie) (#944)",
179
+ bridge: "rerun_construction_scripts",
180
+ timeoutMs: 300_000,
181
+ mapParams: (p) => ({
182
+ actorLabels: p.actorLabels, className: p.className,
183
+ matchSubclasses: p.matchSubclasses, world: p.world, pieInstance: p.pieInstance,
184
+ }),
185
+ },
186
+ recreate_physics_state: {
187
+ description: "Rebuild the physics and collision state on the components a selector picks, after the mesh data underneath them changed. Stale collision makes line_trace report a MISS through solid geometry, which is the worst kind of wrong answer because nothing about it looks wrong. dryRun DEFAULTS TO TRUE and is the preflight: it lists which components would be rebuilt and what their collision enabled state and profile are right now. Requires at least one bound (actorLabels, labelPrefix, tag, classFilter, componentClass, componentNameContains) and never refreshes a whole world implicitly. Returns per-component before/after collision state plus a post-refresh collisionSummary histogram. Params: actorLabels?, labelPrefix?, tag?, classFilter?, componentClass?, componentNameContains?, dryRun? (default TRUE), maxComponents? (default 2000), world? (editor|pie) (#915)",
188
+ bridge: "recreate_physics_state",
189
+ timeoutMs: 300_000,
190
+ mapParams: (p) => ({
191
+ actorLabels: p.actorLabels, labelPrefix: p.labelPrefix, tag: p.tag,
192
+ classFilter: p.classFilter, componentClass: p.componentClass,
193
+ componentNameContains: p.componentNameContains, dryRun: p.dryRun,
194
+ maxComponents: p.maxComponents, world: p.world, pieInstance: p.pieInstance,
195
+ }),
196
+ },
197
+ test_component_overlap: bp("Do two placed components overlap? Returns the boolean plus both sides' local bounds, world bounds and world transforms, and separation (when apart) or penetration depth (when overlapping). For a UBoxComponent it also returns boxExtentUnscaled, the authored BoxExtent the details panel shows, which no other read exposed. method=OBB (default) runs a separating-axis test on the components' oriented local bounds, so a rotated box is tested as a rotated box; method=AABB compares the axis-aligned world bounds and adds a per-axis gap. Deliberately geometric rather than a physics overlap query: a physics answer depends on collision state, and stale collision state is what makes a trace lie (see recreate_physics_state). Omitting a componentName selects that actor's root component. Params: actorLabelA OR actorPathA, actorLabelB OR actorPathB, componentNameA?, componentNameB?, method? (OBB|AABB), world? (editor|pie) (#914)", "test_component_overlap", (p) => ({ actorLabelA: p.actorLabelA, actorPathA: p.actorPathA, actorLabelB: p.actorLabelB, actorPathB: p.actorPathB, componentNameA: p.componentNameA, componentNameB: p.componentNameB, method: p.method, world: p.world, pieInstance: p.pieInstance })),
198
+ line_trace: bp("Line trace. world selects which world is traced: 'editor' (default) or 'pie' for the LIVE running PIE world, which is the only way to check geometry that exists once play has begun (#933). Traces simple collision by default, the same as a gameplay trace, so the result matches what the running game hits; pass traceComplex to trace per-triangle instead. channel picks the collision channel, including channels this project renamed in its collision settings. The response echoes traceComplex, channel and the world that actually answered, and faceIndex is only present on a complex hit. Returns hit + actorLabel/actorClass/componentName/componentClass/location/impactPoint/normal/distance/faceIndex/boneName/physicalMaterial. Params: start (Vec3), end? (Vec3) OR direction? (Vec3) + distance? (default 200000), traceComplex? (default false), channel? (default Visibility), ignoreActors? (array of labels), world? (editor|pie), pieInstance? (#420, #807, #933)", "line_trace", (p) => ({ start: p.start, end: p.end, direction: p.direction, distance: p.distance, traceComplex: p.traceComplex, channel: p.channel, ignoreActors: p.ignoreActors, world: p.world, pieInstance: p.pieInstance })),
199
+ bulk_line_trace: bp("Batch of line traces. Each traces[] item uses the same semantics as line_trace (start + end or direction+distance, simple collision by default, optional traceComplex/channel/ignoreActors). The top-level world applies to the WHOLE batch, so one call cannot straddle the editor and PIE worlds and present the results as comparable. Caps at 256. Returns results[] in traces[] order, one per item, each with the same fields as line_trace (or success=false + error for that item). Params: traces[], world? (editor|pie), pieInstance? (#933)", "bulk_line_trace", (p) => ({ traces: p.traces, world: p.world, pieInstance: p.pieInstance })),
200
+ snap_instances_to_surface: bp("Project ISMC/HISMC instances down onto surface geometry, preserving each instance's rotation and scale. Dry-run by default: preflights every requested instance and reports what would move before anything does, then applies successful projections in one transaction with rollback. Filter the surfaces traced against with surfaceActorClass/surfaceActorLabels so instances do not snap onto each other. onMiss controls whether a miss aborts the batch or is skipped. Does not save packages. Params: actorLabel OR actorPath, componentName?, instanceIndices? (omit for all), maxInstances?, traceStartOffset?, traceDistance?, surfaceOffset?, channel?, traceComplex?, surfaceActorClass?, surfaceActorLabels?, onMiss? ('abort'|'skip'), dryRun? (default true) (#905)", "snap_instances_to_surface", (p) => ({ actorLabel: p.actorLabel, actorPath: p.actorPath, componentName: p.componentName, instanceIndices: p.instanceIndices, maxInstances: p.maxInstances, traceStartOffset: p.traceStartOffset, traceDistance: p.traceDistance, surfaceOffset: p.surfaceOffset, channel: p.channel, traceComplex: p.traceComplex, surfaceActorClass: p.surfaceActorClass, surfaceActorLabels: p.surfaceActorLabels, onMiss: p.onMiss, dryRun: p.dryRun })),
201
+ summarize_static_mesh_usage: bp("Read-only inventory of which static meshes are actually placed in the loaded world, aggregated in one game-thread scan instead of walking every actor and component. This is the action for placement counts, and query_components is not a substitute for it: grouping there only increments, so one HISM holding 5,000 instances counts as 1, while this SUMS real ISM/HISM instances and counts distinct actors per mesh, and it sorts every aggregate before truncating so the top N is the true top N with an exact totalUniqueMeshes. Distinguishes actors, components and placements. Results are bounded; full-scan totals are preserved alongside the truncated rows. Loads nothing and saves nothing. Params: world? (editor|pie), maxResults?, includeOccurrences?, maxOccurrences? (#903)", "summarize_static_mesh_usage", (p) => ({ world: p.world, maxResults: p.maxResults, includeOccurrences: p.includeOccurrences, maxOccurrences: p.maxOccurrences })),
202
+ snap_actor_to_floor: bp("Snap an actor's bounds-bottom to the first downward line-trace hit. Equivalent of the End-key shortcut, works on arbitrary geometry (not just Landscape). The actor lookup and the downward trace both run in the world named by world, so a PIE snap measures against PIE geometry (#933). Params: actorLabel OR actorPath, floorOffset? (added to impact Z, default 0), maxDistance? (default 100000), world? (editor|pie), pieInstance? (#419, #933)", "snap_actor_to_floor", (p) => ({ actorLabel: p.actorLabel, actorPath: p.actorPath, floorOffset: p.floorOffset, maxDistance: p.maxDistance, world: p.world, pieInstance: p.pieInstance })),
81
203
  }, undefined, // actionDocs auto-generated from descriptions
82
204
  {
83
205
  actorLabel: z.string().optional(), actorLabels: z.array(z.string()).optional(),
@@ -89,13 +211,104 @@ export const levelTool = categoryTool("level", "Level actors, selection, compone
89
211
  projectPoint: Vec3.optional().describe("get_spline_info: world-space point to project onto the spline (#555)"),
90
212
  targetActor: z.string().optional().describe("aim_actor_at: label of the actor to look at (alternative to targetPoint)"),
91
213
  roll: z.number().optional().describe("aim_actor_at: roll angle in degrees (default 0)"),
92
- properties: z.record(z.unknown()).optional(),
214
+ properties: z.record(z.unknown()).optional().describe("batch writes: propertyName -> value. add_runtime_cell_transformer: properties applied to the newly created transformer INSTANCE (#985)"),
93
215
  classFilter: z.string().optional(), nameFilter: z.string().optional(),
94
216
  editorHidden: z.boolean().optional().describe("get_outliner: filter to only editor-hidden (true) / only visible (false) actors (#717)"),
95
217
  includeStreaming: z.boolean().optional().describe("get_outliner: include World Partition streaming-proxy / HLOD actors (default false)"),
96
218
  hidden: z.boolean().optional().describe("set_editor_visibility: true = hide in editor, false = show (#717)"),
97
219
  all: z.boolean().optional().describe("set_editor_visibility: target every actor (#717)"),
98
220
  componentClass: z.string().optional(), componentName: z.string().optional(),
221
+ frame: z.enum(["world", "actor", "parent", "component"]).optional().describe("nudge_component: coordinate frame for translation and axis rotation (default actor)"),
222
+ translationDelta: z.object({
223
+ forwardCm: z.number().finite().optional(),
224
+ rightCm: z.number().finite().optional(),
225
+ upCm: z.number().finite().optional(),
226
+ }).optional().describe("nudge_component: frame-relative translation in centimetres"),
227
+ axisRotation: z.object({
228
+ axis: z.enum(["forward", "right", "up"]),
229
+ degrees: z.number().finite(),
230
+ }).optional().describe("nudge_component: quaternion rotation about one frame-relative axis"),
231
+ scaleMultiplier: z.number().finite().positive().optional().describe("nudge_component: uniform relative-scale multiplier (>0)"),
232
+ // query_components (#910/#943/#912)
233
+ componentNameContains: z.string().optional().describe("query_components: case-insensitive substring over the component's instance name"),
234
+ actorLabelPrefix: z.string().optional().describe("query_components: case-sensitive prefix over the actor's EDITOR LABEL"),
235
+ actorLabelContains: z.string().optional().describe("query_components: case-insensitive substring over the actor's EDITOR LABEL"),
236
+ actorTag: z.string().optional().describe("query_components: actor must carry this tag"),
237
+ folderPathPrefix: z.string().optional().describe("query_components: World Outliner folder prefix, e.g. 'Gameplay/'"),
238
+ fields: z.array(z.string()).optional().describe("query_components: field groups to project - transform, bounds, localBounds, shadow, nanite, navigation, tick, materials, mesh, decal, health"),
239
+ where: z.array(z.object({
240
+ field: z.string().describe("Dot path into the row, e.g. shadow.effectiveCastShadow or props.CullDistance"),
241
+ op: z.string().optional().describe("eq (default), ne, lt, lte, gt, gte, contains, notContains, startsWith, endsWith, in, notIn, exists, notExists, isNull, isNotNull, isTrue, isFalse"),
242
+ value: z.unknown().optional(),
243
+ })).optional().describe("query_components: predicates evaluated in the editor (max 24)"),
244
+ whereMode: z.string().optional().describe("query_components: all (default) or any"),
245
+ suspectOnly: z.boolean().optional().describe("query_components: shorthand for where health.suspect isTrue"),
246
+ groupBy: z.string().optional().describe("query_components: dot path to group matches by; returns key/count/samples (max 200 groups)"),
247
+ countBy: z.array(z.string()).optional().describe("query_components: dot paths to build value histograms for (max 8 paths, 64 values each)"),
248
+ sampleLimit: z.number().optional().describe("query_components: sample labels per group (default 5, max 25)"),
249
+ countOnly: z.boolean().optional().describe("query_components: return aggregates without rows"),
250
+ startIndex: z.number().optional().describe("query_components: first row index for paging; rows are sorted deterministically so the index is stable across calls"),
251
+ duplicateTransformTolerance: z.number().optional().describe("query_components: cm bucket size for duplicate-transform detection (#912)"),
252
+ // Batch writes (#984/#941/#907/#987). The selector vocabulary is shared,
253
+ // and the names say what they match: labelPrefix is a case-sensitive
254
+ // prefix over the EDITOR LABEL, labelContains a case-insensitive substring
255
+ // over the same label.
256
+ labelContains: z.string().optional().describe("delete_actors / batch writes: case-insensitive substring over the actor's EDITOR LABEL"),
257
+ nameContains: z.string().optional().describe("delete_actors: case-insensitive substring over the actor's INTERNAL name (e.g. StaticMeshActor_141), which is not the same thing as its label (#963)"),
258
+ save: z.boolean().optional().describe("clear_level_script: save only the current level after clearing and compiling. remove_components_by_class: save the level after a committed removal. Default false for both."),
259
+ actorClassFilter: z.string().optional().describe("remove_components_by_class: restrict to actors of this class (alias of classFilter)"),
260
+ matchComponentSubclasses: z.boolean().optional().describe("remove_components_by_class: also match subclasses of componentClass (default true)"),
261
+ instances: z.array(z.object({
262
+ location: Vec3.optional(),
263
+ rotation: Rotator.optional(),
264
+ scale: Vec3.optional(),
265
+ label: z.string().optional(),
266
+ properties: z.record(z.unknown()).optional(),
267
+ })).optional().describe("spawn_actors_batch: explicit per-instance transforms and property overrides (#987)"),
268
+ fromComponents: z.object({
269
+ componentClass: z.string().optional(),
270
+ componentNameContains: z.string().optional(),
271
+ actorLabels: z.array(z.string()).optional(),
272
+ labelPrefix: z.string().optional(),
273
+ labelContains: z.string().optional(),
274
+ tag: z.string().optional(),
275
+ classFilter: z.string().optional(),
276
+ folderPath: z.string().optional(),
277
+ folderPathPrefix: z.string().optional(),
278
+ matchSubclasses: z.boolean().optional(),
279
+ space: z.string().optional().describe("local (default, respects the component's orientation) or world (axis-aligned bounds)"),
280
+ offset: Vec3.optional(),
281
+ extentFraction: Vec3.optional().describe("Fraction of the box extent to add per axis, e.g. {z: 0.72} for 72 percent of the way up"),
282
+ inheritRotation: z.boolean().optional(),
283
+ }).optional().describe("spawn_actors_batch: derive one spawn per matched component from its bounds (#987)"),
284
+ alongSpline: z.object({
285
+ actorLabel: z.string(),
286
+ componentName: z.string().optional(),
287
+ spacing: z.number().describe("Distance between spawns in cm (minimum 1)"),
288
+ startDistance: z.number().optional(),
289
+ endDistance: z.number().optional(),
290
+ offset: Vec3.optional(),
291
+ alignToTangent: z.boolean().optional(),
292
+ }).optional().describe("spawn_actors_batch: scatter along a spline by distance (#987)"),
293
+ maxSpawn: z.number().optional().describe("spawn_actors_batch: refuse a plan larger than this (default 500, max 5000)"),
294
+ // Refresh and inspect (#944/#915/#914)
295
+ maxComponents: z.number().optional().describe("recreate_physics_state: refuse to rebuild more than this many components (default 2000, max 20000)"),
296
+ actorLabelA: z.string().optional().describe("test_component_overlap: first actor's editor label"),
297
+ actorLabelB: z.string().optional().describe("test_component_overlap: second actor's editor label"),
298
+ componentNameA: z.string().optional().describe("test_component_overlap: component on actor A; omitted selects its root component"),
299
+ componentNameB: z.string().optional().describe("test_component_overlap: component on actor B; omitted selects its root component"),
300
+ method: z.string().optional().describe("test_component_overlap: OBB (oriented local bounds, default) or AABB (axis-aligned world bounds)"),
301
+ // Brush conversion + the exact class filter the folder queries lacked (#911)
302
+ exactClass: z.boolean().optional().describe("get_outliner: require classFilter to be the exact class name rather than a substring. convert_brushes_to_static_mesh: same, defaults to true there"),
303
+ recursiveFolder: z.boolean().optional().describe("convert_brushes_to_static_mesh: also take brushes in folders nested under folderPath (default true)"),
304
+ destinationPath: z.string().optional().describe("convert_brushes_to_static_mesh: content path the generated static meshes are written to (default /Game/Meshes/Converted)"),
305
+ allowSubtractive: z.boolean().optional().describe("convert_brushes_to_static_mesh: opt in to converting subtractive brushes, which have no surface of their own"),
306
+ includeVolumes: z.boolean().optional().describe("convert_brushes_to_static_mesh: opt in to converting volume brushes, which are collision rather than visible geometry"),
307
+ // Component material overrides (#946) and transient verification actors (#956)
308
+ materials: z.array(z.string()).optional().describe("spawn_skeletal_mesh_actor / set_component_materials: per-slot material paths; index is the slot. In set_component_materials a null or empty entry clears that slot's override (#946)"),
309
+ clearOverrides: z.boolean().optional().describe("set_component_materials: drop every component override so the mesh asset's own slots show through (#946)"),
310
+ initialize: z.string().optional().describe("spawn_transient_actor: none | construction (default) | beginPlay. The editor world has not begun play, so nothing runs InitializeComponent or BeginPlay unless you ask for it (#956)"),
311
+ hideFromOutliner: z.boolean().optional().describe("spawn_transient_actor: keep the actor out of the World Outliner (default false, so you can see what was made)"),
99
312
  transforms: z.array(z.record(z.unknown())).optional().describe("add_hismc_instances transform list (#434)"),
100
313
  worldSpace: z.boolean().optional().describe("add_hismc_instances/instance ops: treat transforms as world-space (default true) (#434)"),
101
314
  index: z.number().optional().describe("update_instance_transform/remove_instance: instance index (#697)"),
@@ -104,12 +317,11 @@ export const levelTool = categoryTool("level", "Level actors, selection, compone
104
317
  positionPrecision: z.number().optional().describe("set_nanite_settings: Nanite position precision (#696)"),
105
318
  outputPath: z.string().optional().describe("export_actor_fbx: output .fbx path (#637)"),
106
319
  skeletalMesh: z.string().optional().describe("spawn_skeletal_mesh_actor: USkeletalMesh path (#679)"),
107
- materials: z.array(z.string()).optional().describe("spawn_skeletal_mesh_actor: per-slot material paths (#679)"),
108
320
  animSequence: z.string().optional().describe("spawn_skeletal_mesh_actor: single-node preview animation (#679)"),
109
321
  loop: z.boolean().optional().describe("spawn_skeletal_mesh_actor: loop the preview animation (#679)"),
110
322
  onConflict: z.string().optional().describe("spawn_skeletal_mesh_actor: label conflict policy (skip|error)"),
111
323
  includeValues: z.boolean().optional().describe("get_component_details: dump arbitrary UPROPERTY values (#584)"),
112
- propertyNames: z.array(z.string()).optional().describe("get_component_details: restrict includeValues to these property names (#584)"),
324
+ propertyNames: z.array(z.string()).optional().describe("get_component_details: restrict includeValues to these property names (#584). query_components: component UPROPERTY names to project under props.* (max 32)"),
113
325
  matchSubclasses: z.boolean().optional().describe("get_actors_by_class: match Blueprint subclasses via IsChildOf (default true) (#675)"),
114
326
  includeTransforms: z.boolean().optional().describe("get_actors_by_class: include per-actor location/rotation/scale (default true) (#675)"),
115
327
  propertyName: z.string().optional(), value: z.unknown().optional(),
@@ -129,9 +341,45 @@ export const levelTool = categoryTool("level", "Level actors, selection, compone
129
341
  staticMesh: z.string().optional().describe("Asset path for static mesh (e.g. /Engine/BasicShapes/Cube.Cube)"),
130
342
  material: z.string().optional().describe("Asset path for material to apply at slot 0"),
131
343
  weight: z.number().optional().describe("add_post_process_blendable: blend weight (default 1)"),
344
+ // World Partition streaming (#985). cellSize and loadingRange are the two
345
+ // knobs; `settings` is the general escape hatch because the reachable set
346
+ // depends on which runtime hash the map uses, and the handler resolves
347
+ // each path against the world partition's own reflection.
348
+ cellSize: z.number().optional().describe("set_world_partition_settings: streaming cell size in world centimetres (#985)"),
349
+ loadingRange: z.number().optional().describe("set_world_partition_settings: streaming loading range in world centimetres (#985)"),
350
+ grid: z.string().optional().describe("set_world_partition_settings: which streaming grid to write, by name (#985)"),
351
+ gridPath: z.string().optional().describe("set_world_partition_settings: which streaming grid to write, by the dotted path get_world_partition_settings reports (#985)"),
352
+ transformerClass: z.string().optional().describe("add_runtime_cell_transformer: a WorldPartitionRuntimeCellTransformer subclass, by short name, Module.Class or /Script path (#985)"),
353
+ position: z.number().optional().describe("add_runtime_cell_transformer: index to insert at; -1 (default) appends (#985)"),
354
+ skipIfPresent: z.boolean().optional().describe("add_runtime_cell_transformer: report the existing entry instead of stacking a duplicate (default true) (#985)"),
355
+ hlodLayer: z.string().nullable().optional().describe("set_actor_hlod_layer: HLODLayer asset path, or null to clear the per-actor override (#985)"),
356
+ enableAutoLODGeneration: z.boolean().optional().describe("set_actor_hlod_layer: also set bEnableAutoLODGeneration on each matched actor (#985)"),
357
+ // Post-process settings (#950). `settings` is a free-form map because
358
+ // FPostProcessSettings has hundreds of fields and the handler resolves each
359
+ // key against the struct's own reflection, which is the only list that
360
+ // cannot go stale against the engine version in front of it.
361
+ settings: z.record(z.unknown()).optional().describe("set_post_process_settings: settingName -> value; each key's bOverride_<Name> flag is enabled too, which is what makes the value apply (#950). set_world_partition_settings: dotted path -> value, rooted at the world partition object (#985)"),
362
+ enableOverrides: z.boolean().optional().describe("set_post_process_settings: enable each written setting's bOverride_<Name> flag (default true; false writes the value and leaves it inert) (#950)"),
363
+ onlyOverridden: z.boolean().optional().describe("get_post_process_settings: return only settings whose bOverride_<Name> flag is on (#950)"),
364
+ names: z.array(z.string()).optional().describe("get_post_process_settings: exact setting names to return (#950)"),
365
+ exposure: z.number().optional().describe("set_fixed_exposure: the fixed adaptation brightness written to both AutoExposureMinBrightness and AutoExposureMaxBrightness (#950)"),
366
+ bias: z.number().optional().describe("set_fixed_exposure: AutoExposureBias (exposure compensation), also override-enabled (#950)"),
132
367
  materialPath: z.string().optional().describe("Material asset path for set_actor_material"),
133
368
  slotIndex: z.number().optional().describe("Material slot index (default 0)"),
134
- actorPath: z.string().optional().describe("Full actor object path; alternative to actorLabel"),
369
+ // #983: editor labels are not unique. actorPath is the unambiguous
370
+ // selector, it wins when both are given, and any action that resolves a
371
+ // label matching more than one actor refuses with the candidate paths
372
+ // rather than editing whichever one came first. Every action that returns
373
+ // an actor returns its actorPath, so the retry is a copy of one field.
374
+ actorPath: z.string().optional().describe("Full actor object path (e.g. /Game/Maps/Map.Map:PersistentLevel.BP_Road_3). The unambiguous selector, and it wins over actorLabel when both are given. Editor labels are NOT unique, and a label matching several actors is refused with the candidates rather than resolved at random (#983)"),
375
+ actorPaths: z.array(z.string()).optional().describe("select / read_actor_motion / batch_translate: full actor object paths, the unambiguous form of actorLabels (#983)"),
376
+ targetPath: z.string().optional().describe("get_relative_transform: full object path of the target actor; alternative to targetLabel/target (#983)"),
377
+ referencePath: z.string().optional().describe("get_relative_transform: full object path of the reference actor; alternative to referenceLabel/reference (#983)"),
378
+ targetActorPath: z.string().optional().describe("aim_actor_at: full object path of the actor to aim at; alternative to targetActor (#983)"),
379
+ childPath: z.string().optional().describe("attach_actor / attach_component / detach_actor / detach_component: full object path of the child actor; alternative to childLabel (#983)"),
380
+ parentPath: z.string().optional().describe("attach_actor / attach_component: full object path of the parent actor; alternative to parentLabel (#983)"),
381
+ actorPathA: z.string().optional().describe("component_overlap: full object path of the first actor; alternative to actorLabelA (#983)"),
382
+ actorPathB: z.string().optional().describe("component_overlap: full object path of the second actor; alternative to actorLabelB (#983)"),
135
383
  includeProperties: z.boolean().optional().describe("Include reflected UPROPERTY values in get_actor_details"),
136
384
  world: z.string().optional().describe("World scope: editor (default) or pie"),
137
385
  className: z.string().optional().describe("Class name filter for get_actors_by_class"),
@@ -159,17 +407,40 @@ export const levelTool = categoryTool("level", "Level actors, selection, compone
159
407
  end: Vec3.optional().describe("line_trace: ray end (use this OR direction+distance)"),
160
408
  direction: Vec3.optional().describe("line_trace: ray direction (unit-normalized internally)"),
161
409
  distance: z.number().optional().describe("line_trace: ray length when direction is supplied (default 200000)"),
162
- traceComplex: z.boolean().optional().describe("line_trace: trace per-triangle (complex) collision instead of the simple collision a gameplay trace uses (default false) (#807)"),
410
+ traceComplex: z.boolean().optional().describe("line_trace / snap_instances_to_surface: trace per-triangle (complex) collision instead of the simple collision a gameplay trace uses (default false) (#807)"),
163
411
  channel: z.string().optional().describe("line_trace: collision channel to trace on, e.g. Visibility, Camera, WorldStatic, Pawn, or a channel renamed in this project's collision settings (default Visibility) (#807)"),
164
412
  ignoreActors: z.array(z.string()).optional().describe("line_trace: actor labels to skip"),
413
+ traces: z.array(z.object({
414
+ start: Vec3.describe("ray start"),
415
+ end: Vec3.optional().describe("ray end (use this OR direction+distance)"),
416
+ direction: Vec3.optional().describe("ray direction (unit-normalized internally)"),
417
+ distance: z.number().optional().describe("ray length when direction is supplied (default 200000)"),
418
+ traceComplex: z.boolean().optional().describe("trace per-triangle (complex) collision instead of simple collision (default false)"),
419
+ channel: z.string().optional().describe("collision channel (default Visibility)"),
420
+ ignoreActors: z.array(z.string()).optional().describe("actor labels to skip"),
421
+ })).min(1).max(256).optional().describe("bulk_line_trace: per-item line traces (max 256), each with the same fields as line_trace"),
422
+ instanceIndices: z.array(z.number().int().nonnegative()).optional().describe("snap_instances_to_surface: instance indices to project; omit to take every instance on the component"),
423
+ traceStartOffset: z.number().optional().describe("snap_instances_to_surface: height above each instance to begin the downward trace"),
424
+ traceDistance: z.number().optional().describe("snap_instances_to_surface: maximum downward trace length"),
425
+ surfaceOffset: z.number().optional().describe("snap_instances_to_surface: offset added along the surface normal after the hit"),
426
+ surfaceActorClass: z.string().optional().describe("snap_instances_to_surface: only accept hits on actors of this class"),
427
+ surfaceActorLabels: z.array(z.string()).optional().describe("snap_instances_to_surface: only accept hits on actors with these labels"),
428
+ onMiss: z.enum(["abort", "skip"]).optional().describe("snap_instances_to_surface: whether an instance with no hit aborts the batch or is skipped"),
429
+ maxInstances: z.number().int().positive().optional().describe("snap_instances_to_surface: cap on instances processed in one call"),
430
+ maxResults: z.number().int().positive().optional().describe("summarize_static_mesh_usage: cap on result rows; full-scan totals are still reported"),
431
+ includeOccurrences: z.boolean().optional().describe("summarize_static_mesh_usage: include example actor/component occurrences per mesh"),
432
+ maxOccurrences: z.number().int().positive().optional().describe("summarize_static_mesh_usage: cap on occurrence examples per mesh"),
165
433
  floorOffset: z.number().optional().describe("snap_actor_to_floor: vertical offset added to impact Z"),
166
434
  maxDistance: z.number().optional().describe("snap_actor_to_floor: line-trace length downward (default 100000)"),
167
- force: z.boolean().optional().describe("set_actor_property: bypass EditDefaultsOnly to write per-instance overrides"),
168
- labelPrefix: z.string().optional().describe("delete_actors / set_actor_folder_path: actor label prefix filter"),
435
+ force: z.boolean().optional().describe("set_actor_property: bypass EditDefaultsOnly to write per-instance overrides. save: write a package even when it is already clean (#964)"),
436
+ includeExternalActors: z.boolean().optional().describe("save: also save the level's loaded external actor/folder packages, which is where a World Partition map keeps its edits (default true) (#964)"),
437
+ classPathContains: z.string().optional().describe("delete_actors: case-insensitive substring of the generated UClass path, e.g. /SurvivalGameKitV2/"),
438
+ classPathContainsAny: z.array(z.string()).optional().describe("delete_actors: match if the generated UClass path contains any of these case-insensitive substrings"),
439
+ labelPrefix: z.string().optional().describe("delete_actors / set_actor_folder_path / batch writes: CASE-SENSITIVE PREFIX over the actor's EDITOR LABEL (not the internal name, and not a substring)"),
169
440
  tag: z.string().optional(),
170
441
  pieInstance: z.number().optional().describe("Which PIE world to target when world='pie': 0 = server/primary, 1..N = clients. See editor(list_pie_instances) (#778)"),
171
442
  tags: z.array(z.string()).optional(),
172
- dryRun: z.boolean().optional().describe("delete_actors / set_actor_folder_path: report matches without writing. delete_exact_labeled_actors_in_levels: same, but defaults to true"),
443
+ dryRun: z.boolean().optional().describe("clear_level_script / delete_actors / set_actor_folder_path: report matches without writing. clear_level_script and delete_exact_labeled_actors_in_levels default to true"),
173
444
  levels: z.array(z.object({
174
445
  levelPath: z.string().describe("Long package name of the .umap, e.g. /Game/Maps/Arena"),
175
446
  actorLabels: z.array(z.string()).describe("Exact editor labels to delete in that map (max 256, no duplicates)"),