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
@@ -25,6 +25,58 @@ import * as path from "node:path";
25
25
  export function bridgeLockfilePath(projectDir) {
26
26
  return path.join(projectDir, "Saved", "UE_MCP_Bridge", "port.json");
27
27
  }
28
+ /** Where every live bridge publishes its own address, one file per process. */
29
+ export function bridgeInstancesDir(projectDir) {
30
+ return path.join(projectDir, "Saved", "UE_MCP_Bridge", "instances");
31
+ }
32
+ /**
33
+ * Every readable instance record for this project, newest first.
34
+ *
35
+ * Records are read, not trusted: the caller decides which pids are still alive.
36
+ * A record whose process is gone is exactly the artefact that outlives a crash,
37
+ * and treating it as an address would resurrect the bug the lockfile pid check
38
+ * exists to prevent (#819).
39
+ */
40
+ export function readBridgeInstanceRecords(projectDir) {
41
+ const dir = bridgeInstancesDir(projectDir);
42
+ let names;
43
+ try {
44
+ names = fs.readdirSync(dir);
45
+ }
46
+ catch {
47
+ return [];
48
+ }
49
+ const records = [];
50
+ for (const name of names) {
51
+ if (!name.toLowerCase().endsWith(".json"))
52
+ continue;
53
+ const recordPath = path.join(dir, name);
54
+ try {
55
+ const stat = fs.statSync(recordPath);
56
+ const parsed = JSON.parse(fs.readFileSync(recordPath, "utf-8"));
57
+ const pid = parsed.pid;
58
+ const port = parsed.port;
59
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0)
60
+ continue;
61
+ if (typeof port !== "number" || !Number.isInteger(port) || port <= 0 || port > 65535)
62
+ continue;
63
+ records.push({
64
+ port,
65
+ pid,
66
+ instanceId: typeof parsed.instanceId === "string" ? parsed.instanceId : null,
67
+ state: typeof parsed.state === "string" ? parsed.state : null,
68
+ startedAt: typeof parsed.startedAt === "string" ? parsed.startedAt : null,
69
+ recordPath,
70
+ writtenAtMs: stat.mtimeMs,
71
+ });
72
+ }
73
+ catch {
74
+ // A record being written right now, or one from a build that wrote a
75
+ // different shape. Neither is a reason to discard the others.
76
+ }
77
+ }
78
+ return records.sort((a, b) => b.writtenAtMs - a.writtenAtMs);
79
+ }
28
80
  /** Read a project's bridge lockfile, or null when it is absent or malformed. */
29
81
  export function readBridgeLockfileIn(projectDir) {
30
82
  const file = bridgeLockfilePath(projectDir);
@@ -45,12 +97,43 @@ export function readBridgeLockfileIn(projectDir) {
45
97
  return null;
46
98
  }
47
99
  }
100
+ /**
101
+ * A live editor's own published address for this project, newest first.
102
+ *
103
+ * #934: two editors of one project share a single `Saved/UE_MCP_Bridge/`, and
104
+ * the one that quits removes port.json on its way out. Only the instance that
105
+ * wrote it ever deletes it, so the survivor is left with no shared lockfile at
106
+ * all even though it is listening perfectly well, and recovery used to mean
107
+ * hand-writing port.json back. Its own `instances/<pid>.json` was on disk the
108
+ * whole time and names its port, so recovery is a directory read.
109
+ *
110
+ * `alive` is injected so the fallback is testable without real pids.
111
+ */
112
+ export function findLiveInstanceRecord(projectDir, alive = isPidAlive) {
113
+ for (const record of readBridgeInstanceRecords(projectDir)) {
114
+ // A bind-failed record exists to explain a failure, not to be dialled.
115
+ if (record.state === "bind-failed")
116
+ continue;
117
+ if (!alive(record.pid))
118
+ continue;
119
+ return record;
120
+ }
121
+ return null;
122
+ }
48
123
  /**
49
124
  * The bridge endpoint belonging to `projectDir`, or a reason naming the exact
50
125
  * file that was checked. Never returns a port that this project did not
51
126
  * publish for itself.
127
+ *
128
+ * The shared lockfile is still the first answer, and callers still check the
129
+ * pid it names for themselves. When there is no lockfile at all, a live
130
+ * instance record for this project is a strictly better answer than a refusal:
131
+ * the editor published it, for this project, and it names a port nobody else
132
+ * can overwrite (#934).
133
+ *
134
+ * `isAlive` is injected so the fallback is testable without real pids.
52
135
  */
53
- export function resolveBridgeTarget(projectDir) {
136
+ export function resolveBridgeTarget(projectDir, isAlive = isPidAlive) {
54
137
  if (!projectDir) {
55
138
  return {
56
139
  ok: false,
@@ -61,21 +144,35 @@ export function resolveBridgeTarget(projectDir) {
61
144
  }
62
145
  const lockfilePath = bridgeLockfilePath(projectDir);
63
146
  const lockfile = readBridgeLockfileIn(projectDir);
64
- if (!lockfile) {
147
+ if (lockfile) {
65
148
  return {
66
- ok: false,
149
+ ok: true,
150
+ port: lockfile.port,
151
+ pid: lockfile.pid,
152
+ writtenAtMs: lockfile.writtenAtMs,
153
+ lockfilePath,
154
+ source: "port.json",
155
+ };
156
+ }
157
+ const record = findLiveInstanceRecord(projectDir, isAlive);
158
+ if (record) {
159
+ return {
160
+ ok: true,
161
+ port: record.port,
162
+ pid: record.pid,
163
+ writtenAtMs: record.writtenAtMs,
67
164
  lockfilePath,
68
- reason: `No bridge port published at ${lockfilePath}. The editor writes that file while its bridge is listening ` +
69
- "and removes it when it exits, so either no editor is running for this project or its bridge never started. " +
70
- "Lifecycle actions do not guess a port, because a guessed port reaches whichever editor happens to hold it.",
165
+ source: "instance-record",
166
+ recordPath: record.recordPath,
71
167
  };
72
168
  }
73
169
  return {
74
- ok: true,
75
- port: lockfile.port,
76
- pid: lockfile.pid,
77
- writtenAtMs: lockfile.writtenAtMs,
170
+ ok: false,
78
171
  lockfilePath,
172
+ reason: `No bridge port published at ${lockfilePath}, and no live instance record under ${bridgeInstancesDir(projectDir)}. ` +
173
+ "The editor writes those files while its bridge is listening and removes them when it exits, so either no " +
174
+ "editor is running for this project or its bridge never started. " +
175
+ "Lifecycle actions do not guess a port, because a guessed port reaches whichever editor happens to hold it.",
79
176
  };
80
177
  }
81
178
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"editor-target.js","sourceRoot":"","sources":["../src/editor-target.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"editor-target.js","sourceRoot":"","sources":["../src/editor-target.ts"],"names":[],"mappings":"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"}
@@ -34,6 +34,29 @@ export interface EditorProcess {
34
34
  responding: boolean;
35
35
  windowTitle: string | null;
36
36
  }
37
+ /**
38
+ * The .uproject argument on a process command line, absolute and resolved.
39
+ *
40
+ * #967/#970/#965: this used to be one lazy regex over the whole command line,
41
+ * `[A-Za-z]:[\\/][^"]*?\.uproject`. `[^"]` matches spaces, so on the unquoted
42
+ * command line Windows reports for a spawned editor
43
+ *
44
+ * C:\...\UnrealEditor.exe C:\work\Demo\Demo.uproject
45
+ *
46
+ * the match started at the drive letter of the EXECUTABLE and ran all the way
47
+ * to the end, and every process came back with a "project path" that was the
48
+ * exe and the project concatenated. Nothing ever equalled that, so
49
+ * editorOwnsProject was false for the very editor that had the project open:
50
+ * stop_editor refused to stop a healthy editor while printing that same
51
+ * concatenation back as its evidence, and get_engine_state reported no process
52
+ * for a project whose editor was answering on the bridge.
53
+ *
54
+ * So: find where the .uproject argument ENDS (a token boundary, not just any
55
+ * occurrence of the extension), then walk back to the last place a path could
56
+ * have begun. That handles the quoted form, the unquoted form, `-Project=` with
57
+ * and without quotes, and POSIX roots, without assuming a path has no spaces.
58
+ */
59
+ export declare function extractProjectPath(commandLine: string): string | null;
37
60
  /**
38
61
  * Every UnrealEditor process on this machine, with enough detail to tell an
39
62
  * interactive editor for one project apart from a headless shard for another.
@@ -86,6 +109,18 @@ export interface LogState {
86
109
  * before the plugin module exists, which is where the "editor launched but the
87
110
  * bridge never came up" reports have always come from.
88
111
  */
112
+ /**
113
+ * The engine root the project was last actually opened with, from its own log.
114
+ *
115
+ * Unreal writes `LogInit: Base Directory: <engine>/Engine/Binaries/<Platform>/`
116
+ * in the first few lines of every run, which is the only durable record of
117
+ * which engine tree launched an editor. It survives the editor exiting, so it
118
+ * answers "which engine is this project on" while the editor is stopped, which
119
+ * is exactly when a full rebuild has to be resolved (#959, #974).
120
+ *
121
+ * Returns null when the log is absent, unreadable, or has no such line.
122
+ */
123
+ export declare function readEngineRootFromLog(projectPath: string | null | undefined): string | null;
89
124
  export declare function readLogState(projectPath: string | null | undefined, tailLines?: number): LogState;
90
125
  export interface NativeWindow {
91
126
  handle: string;
@@ -107,6 +142,8 @@ export declare function dialogLikeWindows(windows: NativeWindow[]): NativeWindow
107
142
  export interface EngineSnapshot {
108
143
  writtenAt?: string;
109
144
  ageSeconds?: number;
145
+ /** Which process this snapshot describes. Absent on plugin builds before #990. */
146
+ pid?: number;
110
147
  phase?: string;
111
148
  /**
112
149
  * Null until the engine loop starts ticking. Startup has no tick loop to
@@ -138,11 +175,6 @@ export interface EngineSnapshot {
138
175
  } | null;
139
176
  [key: string]: unknown;
140
177
  }
141
- /**
142
- * The snapshot the plugin flushes to disk from a dedicated writer thread. It
143
- * keeps updating while the game thread is inside a modal loop or a slow task,
144
- * which is precisely when a bridge request cannot be answered.
145
- */
146
178
  export declare function readEngineSnapshot(projectPath: string | null | undefined): EngineSnapshot | null;
147
179
  export interface EngineState {
148
180
  running: boolean;
@@ -150,6 +182,17 @@ export interface EngineState {
150
182
  log: LogState;
151
183
  snapshot: EngineSnapshot | null;
152
184
  dialogs: NativeWindow[];
185
+ /**
186
+ * #965: the out-of-process probe could not run. Reported on its own rather
187
+ * than folded into an empty `processes`, because "I could not look" and
188
+ * "there is no editor" are different answers and only one of them is a
189
+ * reason to conclude the editor is down.
190
+ */
191
+ processProbeFailed: boolean;
192
+ /** What `running` is asserted on. A bridge reply outranks the process table. */
193
+ runningEvidence: "bridge-snapshot" | "process-table" | "none";
194
+ /** Where `snapshot` came from. */
195
+ snapshotSource: "bridge" | "status.json" | "none";
153
196
  /** One line an agent can act on without reading any of the above. */
154
197
  summary: string;
155
198
  /** True when something is waiting on a human answer. */
@@ -163,3 +206,14 @@ export interface EngineState {
163
206
  export declare function readEngineState(projectPath: string | null | undefined, opts?: {
164
207
  probeWindows?: boolean;
165
208
  }): Promise<EngineState>;
209
+ /**
210
+ * Fold in a snapshot the editor served over its own bridge.
211
+ *
212
+ * #965: one report contained `"running": false, "processes": []` and, in the
213
+ * same object, a live snapshot with an uptime and a ticking game thread, served
214
+ * BY the editor over the bridge. An editor that answers a request is running;
215
+ * a process table that came back empty is a failed measurement, not a fact
216
+ * about the world. So a bridge reply sets `running` and the process probe's
217
+ * silence is reported as its own field.
218
+ */
219
+ export declare function withBridgeSnapshot(state: EngineState, snapshot: EngineSnapshot): EngineState;
@@ -44,11 +44,70 @@ function parseJsonLoose(raw) {
44
44
  }
45
45
  }
46
46
  const HEADLESS_FLAGS = ["-server", "-game", "-nullrhi", "-unattended", "-run=", "-buildmachine"];
47
- function extractProjectPath(commandLine) {
48
- const match = commandLine.match(/"?([A-Za-z]:[\\/][^"]*?\.uproject|\/[^"\s]*?\.uproject)"?/);
49
- if (!match)
47
+ /**
48
+ * Where a path argument can begin on a command line: the start of the string,
49
+ * or straight after a separator that cannot itself be part of a path (a space,
50
+ * a quote, or the `=` of `-Project=`). Matching the root marker rather than the
51
+ * whole token is what lets an unquoted path containing spaces still be found.
52
+ */
53
+ const PATH_ROOT_RE = /(?:^|[\s"'=])((?:[A-Za-z]:[\\/])|\/)/g;
54
+ /**
55
+ * The .uproject argument on a process command line, absolute and resolved.
56
+ *
57
+ * #967/#970/#965: this used to be one lazy regex over the whole command line,
58
+ * `[A-Za-z]:[\\/][^"]*?\.uproject`. `[^"]` matches spaces, so on the unquoted
59
+ * command line Windows reports for a spawned editor
60
+ *
61
+ * C:\...\UnrealEditor.exe C:\work\Demo\Demo.uproject
62
+ *
63
+ * the match started at the drive letter of the EXECUTABLE and ran all the way
64
+ * to the end, and every process came back with a "project path" that was the
65
+ * exe and the project concatenated. Nothing ever equalled that, so
66
+ * editorOwnsProject was false for the very editor that had the project open:
67
+ * stop_editor refused to stop a healthy editor while printing that same
68
+ * concatenation back as its evidence, and get_engine_state reported no process
69
+ * for a project whose editor was answering on the bridge.
70
+ *
71
+ * So: find where the .uproject argument ENDS (a token boundary, not just any
72
+ * occurrence of the extension), then walk back to the last place a path could
73
+ * have begun. That handles the quoted form, the unquoted form, `-Project=` with
74
+ * and without quotes, and POSIX roots, without assuming a path has no spaces.
75
+ */
76
+ export function extractProjectPath(commandLine) {
77
+ const lower = commandLine.toLowerCase();
78
+ const EXT = ".uproject";
79
+ let end = -1;
80
+ for (let at = lower.indexOf(EXT); at >= 0; at = lower.indexOf(EXT, at + EXT.length)) {
81
+ const after = commandLine[at + EXT.length];
82
+ if (after === undefined || /[\s"']/.test(after)) {
83
+ end = at + EXT.length;
84
+ break;
85
+ }
86
+ }
87
+ if (end < 0)
88
+ return null;
89
+ const head = commandLine.slice(0, end);
90
+ let start = -1;
91
+ PATH_ROOT_RE.lastIndex = 0;
92
+ for (let m = PATH_ROOT_RE.exec(head); m !== null; m = PATH_ROOT_RE.exec(head)) {
93
+ // m[0] may include the delimiter that proved this is a token boundary;
94
+ // the path itself starts where the captured root does.
95
+ start = m.index + m[0].length - m[1].length;
96
+ }
97
+ if (start < 0)
98
+ return null;
99
+ const raw = commandLine.slice(start, end).replace(/^["']|["']$/g, "").trim();
100
+ if (raw.length === 0)
50
101
  return null;
51
- return path.resolve(match[1]);
102
+ // A candidate that still swallowed the executable is the exact failure this
103
+ // function exists to prevent, so never return one. It can only happen when
104
+ // the project argument itself is relative, and there the last whitespace
105
+ // delimited token is the honest answer.
106
+ if (/\.exe(\s|$)/i.test(raw)) {
107
+ const token = raw.split(/\s+/).pop();
108
+ return token && token.toLowerCase().endsWith(EXT) ? path.resolve(token) : null;
109
+ }
110
+ return path.resolve(raw);
52
111
  }
53
112
  function classify(pid, commandLine, responding, windowTitle) {
54
113
  const lower = commandLine.toLowerCase();
@@ -89,40 +148,49 @@ let processCache = null;
89
148
  * interactive editor for one project apart from a headless shard for another.
90
149
  */
91
150
  export async function listEditorProcesses() {
151
+ return (await probeEditorProcesses()).processes;
152
+ }
153
+ async function probeEditorProcesses() {
92
154
  if (processCache && Date.now() - processCache.at < PROCESS_CACHE_MS) {
93
- return processCache.value;
155
+ return { processes: processCache.value, failed: processCache.failed };
94
156
  }
95
- const value = await queryEditorProcesses();
96
- processCache = { at: Date.now(), value };
97
- return value;
157
+ const probe = await queryEditorProcesses();
158
+ processCache = { at: Date.now(), value: probe.processes, failed: probe.failed };
159
+ return probe;
98
160
  }
99
161
  async function queryEditorProcesses() {
100
162
  try {
101
163
  if (IS_WINDOWS) {
102
164
  const raw = await powershell(WINDOWS_PROCESS_SCRIPT, 20000);
103
165
  const rows = parseJsonLoose(raw);
104
- return rows
105
- .filter((r) => typeof r?.pid === "number")
106
- .map((r) => classify(r.pid, r.cmd ?? "", r.responding !== false, r.title ?? null));
166
+ return {
167
+ processes: rows
168
+ .filter((r) => typeof r?.pid === "number")
169
+ .map((r) => classify(r.pid, r.cmd ?? "", r.responding !== false, r.title ?? null)),
170
+ failed: false,
171
+ };
107
172
  }
108
173
  // POSIX: `ps` gives the full argv, which carries the .uproject and the
109
174
  // headless flags. There is no cheap "responding" equivalent, so report
110
175
  // true and let the log-staleness signal stand in for a wedge.
111
176
  const { stdout } = await execFileAsync("ps", ["-eo", "pid=,args="], { timeout: 10000, maxBuffer: 4 * 1024 * 1024 });
112
- return stdout
113
- .split("\n")
114
- .map((line) => line.trim())
115
- .filter((line) => /UnrealEditor/.test(line) && !/\bgrep\b/.test(line))
116
- .map((line) => {
117
- const space = line.indexOf(" ");
118
- const pid = Number(line.slice(0, space));
119
- return classify(pid, line.slice(space + 1), true, null);
120
- })
121
- .filter((p) => Number.isFinite(p.pid));
177
+ return {
178
+ processes: stdout
179
+ .split("\n")
180
+ .map((line) => line.trim())
181
+ .filter((line) => /UnrealEditor/.test(line) && !/\bgrep\b/.test(line))
182
+ .map((line) => {
183
+ const space = line.indexOf(" ");
184
+ const pid = Number(line.slice(0, space));
185
+ return classify(pid, line.slice(space + 1), true, null);
186
+ })
187
+ .filter((p) => Number.isFinite(p.pid)),
188
+ failed: false,
189
+ };
122
190
  }
123
191
  catch (err) {
124
192
  log.debug("engine-observer", "process probe failed", err);
125
- return [];
193
+ return { processes: [], failed: true };
126
194
  }
127
195
  }
128
196
  /** Two .uproject paths that name the same file, whatever their spelling. */
@@ -223,6 +291,56 @@ function readTailBytes(file, bytes) {
223
291
  * before the plugin module exists, which is where the "editor launched but the
224
292
  * bridge never came up" reports have always come from.
225
293
  */
294
+ /**
295
+ * The engine root the project was last actually opened with, from its own log.
296
+ *
297
+ * Unreal writes `LogInit: Base Directory: <engine>/Engine/Binaries/<Platform>/`
298
+ * in the first few lines of every run, which is the only durable record of
299
+ * which engine tree launched an editor. It survives the editor exiting, so it
300
+ * answers "which engine is this project on" while the editor is stopped, which
301
+ * is exactly when a full rebuild has to be resolved (#959, #974).
302
+ *
303
+ * Returns null when the log is absent, unreadable, or has no such line.
304
+ */
305
+ export function readEngineRootFromLog(projectPath) {
306
+ if (!projectPath)
307
+ return null;
308
+ const file = projectLogPath(projectPath);
309
+ let head;
310
+ try {
311
+ const size = fs.statSync(file).size;
312
+ const length = Math.min(size, 64 * 1024);
313
+ if (length <= 0)
314
+ return null;
315
+ const buf = Buffer.alloc(length);
316
+ const fd = fs.openSync(file, "r");
317
+ try {
318
+ fs.readSync(fd, buf, 0, length, 0);
319
+ }
320
+ finally {
321
+ fs.closeSync(fd);
322
+ }
323
+ head = buf.toString("utf-8");
324
+ }
325
+ catch {
326
+ return null;
327
+ }
328
+ const match = head.match(/Log\w*:\s*(?:Base|Engine) Directory:\s*(.+)/i);
329
+ if (!match)
330
+ return null;
331
+ const directory = match[1].trim().replace(/[\\/]+$/, "");
332
+ if (!directory)
333
+ return null;
334
+ // Base Directory points inside Engine/Binaries/<Platform>; Engine Directory
335
+ // points at the Engine folder itself. Both sit under the root we want.
336
+ let current = path.resolve(directory);
337
+ while (current && current !== path.dirname(current)) {
338
+ if (path.basename(current).toLowerCase() === "engine")
339
+ return path.dirname(current);
340
+ current = path.dirname(current);
341
+ }
342
+ return null;
343
+ }
226
344
  export function readLogState(projectPath, tailLines = 25) {
227
345
  const empty = {
228
346
  logPath: null,
@@ -386,10 +504,7 @@ export function dialogLikeWindows(windows) {
386
504
  * keeps updating while the game thread is inside a modal loop or a slow task,
387
505
  * which is precisely when a bridge request cannot be answered.
388
506
  */
389
- export function readEngineSnapshot(projectPath) {
390
- if (!projectPath)
391
- return null;
392
- const file = path.join(path.dirname(projectPath), "Saved", "UE_MCP_Bridge", "status.json");
507
+ function readSnapshotFile(file) {
393
508
  // The writer publishes atomically (temp file + move) four times a second, so
394
509
  // a read can land in the instant between unlink and rename. One retry turns
395
510
  // that transient miss back into a hit; without it, callers see a null and
@@ -406,10 +521,79 @@ export function readEngineSnapshot(projectPath) {
406
521
  }
407
522
  return null;
408
523
  }
524
+ /**
525
+ * `status.<pid>.json` names, newest last-written first.
526
+ *
527
+ * #990: one project directory can hold the status of several processes, and a
528
+ * single shared `status.json` describes only whichever of them wrote last. The
529
+ * per-process files are the ones that describe each editor honestly. A file
530
+ * whose process has died stops being updated, so newest-wins picks a live
531
+ * editor over a crashed one without a process-table query.
532
+ */
533
+ function instanceStatusFiles(dir) {
534
+ try {
535
+ return fs
536
+ .readdirSync(dir)
537
+ .filter((name) => /^status\.\d+\.json$/i.test(name))
538
+ .map((name) => {
539
+ const file = path.join(dir, name);
540
+ let mtimeMs = 0;
541
+ try {
542
+ mtimeMs = fs.statSync(file).mtimeMs;
543
+ }
544
+ catch {
545
+ // Removed between readdir and stat; sorts last and reads as null.
546
+ }
547
+ return { file, mtimeMs };
548
+ })
549
+ .sort((a, b) => b.mtimeMs - a.mtimeMs)
550
+ .map((entry) => entry.file);
551
+ }
552
+ catch {
553
+ return [];
554
+ }
555
+ }
556
+ export function readEngineSnapshot(projectPath) {
557
+ if (!projectPath)
558
+ return null;
559
+ const dir = path.join(path.dirname(projectPath), "Saved", "UE_MCP_Bridge");
560
+ // Per-process first, shared second. The shared path is still written by
561
+ // current plugin builds and is the only thing an older one writes, so it
562
+ // stays the fallback rather than being dropped.
563
+ for (const file of instanceStatusFiles(dir)) {
564
+ const snapshot = readSnapshotFile(file);
565
+ if (snapshot)
566
+ return snapshot;
567
+ }
568
+ return readSnapshotFile(path.join(dir, "status.json"));
569
+ }
409
570
  function summarise(state) {
410
571
  const ours = state.processes;
572
+ if (ours.length === 0 && !state.running) {
573
+ return {
574
+ summary: state.processProbeFailed
575
+ ? `The process probe failed, so whether an editor is running for this project is unknown. Log phase: ${state.log.phase}.`
576
+ : `No editor process for this project. Log phase: ${state.log.phase}.`,
577
+ blocked: false,
578
+ };
579
+ }
580
+ // #965: an editor answered this request over the bridge while the process
581
+ // table came back empty, and the report said "No editor process for this
582
+ // project" in the same breath as it returned that editor's own snapshot.
583
+ // Whatever the process table managed to see, an editor that replied exists.
411
584
  if (ours.length === 0) {
412
- return { summary: `No editor process for this project. Log phase: ${state.log.phase}.`, blocked: false };
585
+ const modal = state.snapshot?.modal;
586
+ if (modal) {
587
+ return {
588
+ summary: `Editor answered over the bridge and is blocked on modal "${modal.title}": ${modal.message} [${(modal.buttons ?? []).join(", ")}]`,
589
+ blocked: true,
590
+ };
591
+ }
592
+ return {
593
+ summary: `Editor answered over the bridge (phase: ${state.snapshot?.phase ?? state.log.phase}), but the process probe ` +
594
+ `${state.processProbeFailed ? "failed" : "did not find it"}, so no pid or command line is available.`,
595
+ blocked: false,
596
+ };
413
597
  }
414
598
  if (state.dialogs.length > 0) {
415
599
  const d = state.dialogs[0];
@@ -452,14 +636,50 @@ function summarise(state) {
452
636
  * be turned on when something already looks wrong.
453
637
  */
454
638
  export async function readEngineState(projectPath, opts = {}) {
455
- const processes = await findInteractiveEditors(projectPath);
639
+ const probe = await probeEditorProcesses();
640
+ // Every interactive editor holding this .uproject open, not just one: two
641
+ // editors on one project is a legitimate state and hiding the second is how
642
+ // a stop lands on the wrong one (#965).
643
+ const processes = selectEditorsForProject(probe.processes, projectPath);
456
644
  const logState = readLogState(projectPath);
457
645
  const snapshot = readEngineSnapshot(projectPath);
458
646
  let dialogs = [];
459
647
  if (opts.probeWindows && processes.length > 0) {
460
648
  dialogs = dialogLikeWindows(await readNativeWindows(processes.map((p) => p.pid)));
461
649
  }
462
- const base = { running: processes.length > 0, processes, log: logState, snapshot, dialogs };
650
+ const base = {
651
+ running: processes.length > 0,
652
+ processes,
653
+ log: logState,
654
+ snapshot,
655
+ dialogs,
656
+ processProbeFailed: probe.failed,
657
+ runningEvidence: processes.length > 0 ? "process-table" : "none",
658
+ snapshotSource: snapshot ? "status.json" : "none",
659
+ };
660
+ return { ...base, ...summarise(base) };
661
+ }
662
+ /**
663
+ * Fold in a snapshot the editor served over its own bridge.
664
+ *
665
+ * #965: one report contained `"running": false, "processes": []` and, in the
666
+ * same object, a live snapshot with an uptime and a ticking game thread, served
667
+ * BY the editor over the bridge. An editor that answers a request is running;
668
+ * a process table that came back empty is a failed measurement, not a fact
669
+ * about the world. So a bridge reply sets `running` and the process probe's
670
+ * silence is reported as its own field.
671
+ */
672
+ export function withBridgeSnapshot(state, snapshot) {
673
+ const base = {
674
+ ...state,
675
+ // It was served in answer to this call, so its age is zero. Leaving it
676
+ // unset made the summary's freshness gate treat it as 999 seconds old and
677
+ // skip the modal and slow-task lines it was fetched to provide.
678
+ snapshot: { ...snapshot, ageSeconds: 0 },
679
+ snapshotSource: "bridge",
680
+ running: true,
681
+ runningEvidence: "bridge-snapshot",
682
+ };
463
683
  return { ...base, ...summarise(base) };
464
684
  }
465
685
  //# sourceMappingURL=engine-observer.js.map