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
@@ -8,6 +8,7 @@
8
8
  #include "HandlerUtils.h"
9
9
  #include "HandlerAssetCreate.h"
10
10
  #include "HandlerJsonProperty.h"
11
+ #include "JsonSerializer.h"
11
12
  #include "AssetToolsModule.h"
12
13
  #include "IAssetTools.h"
13
14
  #include "AssetImportTask.h"
@@ -143,11 +144,13 @@ namespace
143
144
  TSharedPtr<FJsonValue> LoadCurveTable(const TSharedPtr<FJsonObject>& Params, UCurveTable*& OutTable, FString& OutAssetPath)
144
145
  {
145
146
  if (auto Err = RequireStringAlt(Params, TEXT("path"), TEXT("assetPath"), OutAssetPath)) return Err;
146
- UObject* Asset = UEditorAssetLibrary::LoadAsset(OutAssetPath);
147
+ TSharedPtr<FJsonValue> LoadError;
148
+ UObject* Asset = MCPRequireAssetObject(OutAssetPath, LoadError);
149
+ if (!Asset) return LoadError;
147
150
  OutTable = Cast<UCurveTable>(Asset);
148
151
  if (!OutTable)
149
152
  {
150
- return MCPError(FString::Printf(TEXT("Asset is not a CurveTable: %s"), *OutAssetPath));
153
+ return MCPAssetWrongTypeError(OutAssetPath, Asset, TEXT("CurveTable"));
151
154
  }
152
155
  return nullptr;
153
156
  }
@@ -333,7 +336,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::ImportSkeletalMesh(const TSharedPtr<FJson
333
336
  FString SkeletonPath;
334
337
  if (Params->TryGetStringField(TEXT("skeletonPath"), SkeletonPath) && !SkeletonPath.IsEmpty())
335
338
  {
336
- USkeleton* Skeleton = LoadObject<USkeleton>(nullptr, *SkeletonPath);
339
+ USkeleton* Skeleton = LoadAssetByPath<USkeleton>(SkeletonPath);
337
340
  if (Skeleton)
338
341
  {
339
342
  ImportUI->Skeleton = Skeleton;
@@ -470,10 +473,10 @@ TSharedPtr<FJsonValue> FAssetHandlers::ImportAnimation(const TSharedPtr<FJsonObj
470
473
  return MCPError(FString::Printf(TEXT("File not found: %s"), *FileName));
471
474
  }
472
475
 
473
- USkeleton* Skeleton = LoadObject<USkeleton>(nullptr, *SkeletonPath);
476
+ USkeleton* Skeleton = LoadAssetByPath<USkeleton>(SkeletonPath);
474
477
  if (!Skeleton)
475
478
  {
476
- return MCPError(FString::Printf(TEXT("Skeleton not found: %s"), *SkeletonPath));
479
+ return MCPAssetLoadError(SkeletonPath, TEXT("Skeleton"));
477
480
  }
478
481
 
479
482
  UFbxFactory* FbxFactory = NewObject<UFbxFactory>();
@@ -577,16 +580,14 @@ TSharedPtr<FJsonValue> FAssetHandlers::ListTextureProperties(const TSharedPtr<FJ
577
580
  FString AssetPath;
578
581
  if (auto Err = RequireStringAlt(Params, TEXT("assetPath"), TEXT("path"), AssetPath)) return Err;
579
582
 
580
- UObject* Asset = UEditorAssetLibrary::LoadAsset(AssetPath);
581
- if (!Asset)
582
- {
583
- return MCPError(FString::Printf(TEXT("Asset not found: %s"), *AssetPath));
584
- }
583
+ TSharedPtr<FJsonValue> LoadError;
584
+ UObject* Asset = MCPRequireAssetObject(AssetPath, LoadError);
585
+ if (!Asset) return LoadError;
585
586
 
586
587
  UTexture2D* Texture = Cast<UTexture2D>(Asset);
587
588
  if (!Texture)
588
589
  {
589
- return MCPError(FString::Printf(TEXT("Asset is not a Texture2D: %s"), *AssetPath));
590
+ return MCPAssetWrongTypeError(AssetPath, Asset, TEXT("Texture2D"));
590
591
  }
591
592
 
592
593
  auto Result = MCPSuccess();
@@ -662,16 +663,14 @@ TSharedPtr<FJsonValue> FAssetHandlers::SetTextureProperties(const TSharedPtr<FJs
662
663
  FString AssetPath;
663
664
  if (auto Err = RequireStringAlt(Params, TEXT("assetPath"), TEXT("path"), AssetPath)) return Err;
664
665
 
665
- UObject* Asset = UEditorAssetLibrary::LoadAsset(AssetPath);
666
- if (!Asset)
667
- {
668
- return MCPError(FString::Printf(TEXT("Asset not found: %s"), *AssetPath));
669
- }
666
+ TSharedPtr<FJsonValue> LoadError;
667
+ UObject* Asset = MCPRequireAssetObject(AssetPath, LoadError);
668
+ if (!Asset) return LoadError;
670
669
 
671
670
  UTexture2D* Texture = Cast<UTexture2D>(Asset);
672
671
  if (!Texture)
673
672
  {
674
- return MCPError(FString::Printf(TEXT("Asset is not a Texture2D: %s"), *AssetPath));
673
+ return MCPAssetWrongTypeError(AssetPath, Asset, TEXT("Texture2D"));
675
674
  }
676
675
 
677
676
  // Capture previous values for self-inverse rollback. Use reflection paths
@@ -1062,7 +1061,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::CreateCurveTable(const TSharedPtr<FJsonOb
1062
1061
  {
1063
1062
  FString ExistingAssetPath;
1064
1063
  ExistingObj->TryGetStringField(TEXT("path"), ExistingAssetPath);
1065
- if (UCurveTable* Existing = LoadObject<UCurveTable>(nullptr, *ExistingAssetPath))
1064
+ if (UCurveTable* Existing = LoadAssetByPath<UCurveTable>(ExistingAssetPath))
1066
1065
  {
1067
1066
  ExistingObj->SetStringField(TEXT("assetPath"), Existing->GetPathName());
1068
1067
  ExistingObj->SetStringField(TEXT("curveType"), CurveTableModeName(Existing->GetCurveTableMode()));
@@ -1552,7 +1551,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::CreateDataTable(const TSharedPtr<FJsonObj
1552
1551
 
1553
1552
  // Find the row struct type
1554
1553
  UScriptStruct* ScriptStruct = nullptr;
1555
- ScriptStruct = LoadObject<UScriptStruct>(nullptr, *RowStruct);
1554
+ ScriptStruct = LoadAssetByPath<UScriptStruct>(RowStruct);
1556
1555
  if (!ScriptStruct)
1557
1556
  {
1558
1557
  for (TObjectIterator<UScriptStruct> It; It; ++It)
@@ -1583,7 +1582,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::CreateDataTable(const TSharedPtr<FJsonObj
1583
1582
  {
1584
1583
  FString ExistingAssetPath;
1585
1584
  ExistingObj->TryGetStringField(TEXT("path"), ExistingAssetPath);
1586
- if (UDataTable* Existing = LoadObject<UDataTable>(nullptr, *ExistingAssetPath))
1585
+ if (UDataTable* Existing = LoadAssetByPath<UDataTable>(ExistingAssetPath))
1587
1586
  {
1588
1587
  ExistingObj->SetStringField(TEXT("assetPath"), Existing->GetPathName());
1589
1588
  ExistingObj->SetStringField(TEXT("rowStruct"), Existing->RowStruct ? Existing->RowStruct->GetName() : TEXT(""));
@@ -1612,22 +1611,40 @@ TSharedPtr<FJsonValue> FAssetHandlers::CreateDataTable(const TSharedPtr<FJsonObj
1612
1611
  }
1613
1612
 
1614
1613
 
1615
- TSharedPtr<FJsonValue> FAssetHandlers::ReadDataTable(const TSharedPtr<FJsonObject>& Params)
1614
+ namespace
1616
1615
  {
1617
- FString AssetPath;
1618
- if (auto Err = RequireStringAlt(Params, TEXT("path"), TEXT("assetPath"), AssetPath)) return Err;
1619
-
1620
- UObject* Asset = UEditorAssetLibrary::LoadAsset(AssetPath);
1621
- if (!Asset)
1616
+ // #930: resolve the DataTable parameter the way asset(read) resolves any
1617
+ // asset, so the type-specific actions succeed on every path form the
1618
+ // generic reader already opens. Returns a ready-to-return error, or an
1619
+ // unset pointer on success with OutAssetPath and OutTable filled in.
1620
+ //
1621
+ // The "not a DataTable" message names the class that was found, because
1622
+ // the old wording was also what a caller saw when the path resolved to
1623
+ // nothing at all.
1624
+ TSharedPtr<FJsonValue> LoadDataTableParam(
1625
+ const TSharedPtr<FJsonObject>& Params,
1626
+ FString& OutAssetPath,
1627
+ UDataTable*& OutTable)
1622
1628
  {
1623
- return MCPError(FString::Printf(TEXT("Asset not found: %s"), *AssetPath));
1624
- }
1629
+ if (auto Err = RequireStringAlt(Params, TEXT("path"), TEXT("assetPath"), OutAssetPath)) return Err;
1625
1630
 
1626
- UDataTable* DataTable = Cast<UDataTable>(Asset);
1627
- if (!DataTable)
1628
- {
1629
- return MCPError(FString::Printf(TEXT("Asset is not a DataTable: %s"), *AssetPath));
1631
+ TSharedPtr<FJsonValue> LoadError;
1632
+ UObject* Asset = MCPRequireAssetObject(OutAssetPath, LoadError);
1633
+ if (!Asset) return LoadError;
1634
+ OutTable = Cast<UDataTable>(Asset);
1635
+ if (!OutTable)
1636
+ {
1637
+ return MCPAssetWrongTypeError(OutAssetPath, Asset, TEXT("DataTable"));
1638
+ }
1639
+ return nullptr;
1630
1640
  }
1641
+ }
1642
+
1643
+ TSharedPtr<FJsonValue> FAssetHandlers::ReadDataTable(const TSharedPtr<FJsonObject>& Params)
1644
+ {
1645
+ FString AssetPath;
1646
+ UDataTable* DataTable = nullptr;
1647
+ if (auto Err = LoadDataTableParam(Params, AssetPath, DataTable)) return Err;
1631
1648
 
1632
1649
  FString RowFilter = OptionalString(Params, TEXT("rowFilter"));
1633
1650
 
@@ -1697,19 +1714,8 @@ TSharedPtr<FJsonValue> FAssetHandlers::ReadDataTable(const TSharedPtr<FJsonObjec
1697
1714
  TSharedPtr<FJsonValue> FAssetHandlers::ReimportDataTable(const TSharedPtr<FJsonObject>& Params)
1698
1715
  {
1699
1716
  FString AssetPath;
1700
- if (auto Err = RequireStringAlt(Params, TEXT("path"), TEXT("assetPath"), AssetPath)) return Err;
1701
-
1702
- UObject* Asset = UEditorAssetLibrary::LoadAsset(AssetPath);
1703
- if (!Asset)
1704
- {
1705
- return MCPError(FString::Printf(TEXT("Asset not found: %s"), *AssetPath));
1706
- }
1707
-
1708
- UDataTable* DataTable = Cast<UDataTable>(Asset);
1709
- if (!DataTable)
1710
- {
1711
- return MCPError(FString::Printf(TEXT("Asset is not a DataTable: %s"), *AssetPath));
1712
- }
1717
+ UDataTable* DataTable = nullptr;
1718
+ if (auto Err = LoadDataTableParam(Params, AssetPath, DataTable)) return Err;
1713
1719
 
1714
1720
  // Get JSON string from either inline jsonString or from a file path
1715
1721
  FString JsonString;
@@ -1767,7 +1773,8 @@ TSharedPtr<FJsonValue> FAssetHandlers::ReimportDataTable(const TSharedPtr<FJsonO
1767
1773
  TSharedPtr<FJsonValue> FAssetHandlers::SetDataTableRow(const TSharedPtr<FJsonObject>& Params)
1768
1774
  {
1769
1775
  FString AssetPath;
1770
- if (auto Err = RequireStringAlt(Params, TEXT("path"), TEXT("assetPath"), AssetPath)) return Err;
1776
+ UDataTable* DataTable = nullptr;
1777
+ if (auto Err = LoadDataTableParam(Params, AssetPath, DataTable)) return Err;
1771
1778
  FString RowName;
1772
1779
  if (auto Err = RequireString(Params, TEXT("rowName"), RowName)) return Err;
1773
1780
 
@@ -1785,12 +1792,6 @@ TSharedPtr<FJsonValue> FAssetHandlers::SetDataTableRow(const TSharedPtr<FJsonObj
1785
1792
  return MCPError(TEXT("Missing 'row' (or 'fields'/'data') JSON object with the row struct fields"));
1786
1793
  }
1787
1794
 
1788
- UObject* Asset = UEditorAssetLibrary::LoadAsset(AssetPath);
1789
- UDataTable* DataTable = Cast<UDataTable>(Asset);
1790
- if (!DataTable)
1791
- {
1792
- return MCPError(FString::Printf(TEXT("Asset is not a DataTable: %s"), *AssetPath));
1793
- }
1794
1795
  const UScriptStruct* RowStruct = DataTable->GetRowStruct();
1795
1796
  if (!RowStruct)
1796
1797
  {
@@ -1798,35 +1799,19 @@ TSharedPtr<FJsonValue> FAssetHandlers::SetDataTableRow(const TSharedPtr<FJsonObj
1798
1799
  }
1799
1800
 
1800
1801
  const FName RowKey(*RowName);
1801
- const TMap<FName, uint8*>& RowMap = DataTable->GetRowMap();
1802
- const bool bExisted = RowMap.Contains(RowKey);
1803
-
1804
- // Snapshot the prior row (if any) for rollback / idempotency.
1805
- FString PrevExport;
1806
- if (bExisted)
1807
- {
1808
- uint8* PrevPtr = *RowMap.Find(RowKey);
1809
- RowStruct->ExportText(PrevExport, PrevPtr, PrevPtr, nullptr, PPF_None, nullptr);
1810
- }
1811
-
1812
- // Allocate a row buffer and apply fields via MCPJsonProperty so dicts/
1813
- // arrays/asset paths/gameplay tags all work.
1814
- const int32 StructSize = RowStruct->GetStructureSize();
1815
- const int32 MinAlign = RowStruct->GetMinAlignment();
1816
- uint8* NewRow = (uint8*)FMemory::Malloc(StructSize, MinAlign);
1817
- RowStruct->InitializeStruct(NewRow);
1818
-
1819
- // Seed from the prior row so partial JSON only updates the named fields.
1820
- if (bExisted)
1821
- {
1822
- uint8* PrevPtr = *RowMap.Find(RowKey);
1823
- RowStruct->CopyScriptStruct(NewRow, PrevPtr);
1824
- }
1825
-
1826
- FString SetErr;
1827
- bool bOk = true;
1802
+ uint8* const* ExistingRow = DataTable->GetRowMap().Find(RowKey);
1803
+ const bool bExisted = ExistingRow != nullptr && *ExistingRow != nullptr;
1804
+
1805
+ // Resolve every named field before a single byte is written. An unknown
1806
+ // field name used to be discovered half way through the loop, after
1807
+ // earlier fields had already been applied.
1808
+ TArray<TPair<FProperty*, TSharedPtr<FJsonValue>>> Writes;
1809
+ Writes.Reserve((*RowObj)->Values.Num());
1828
1810
  for (const auto& Pair : (*RowObj)->Values)
1829
1811
  {
1812
+ // FJsonObject::Values is not keyed by FString on UE 5.8, so the key has
1813
+ // to be materialised before it can be compared with a property name.
1814
+ // FString(*Pair.Key) is the idiom the rest of this module already uses.
1830
1815
  const FString FieldName(*Pair.Key);
1831
1816
  FProperty* FieldProp = nullptr;
1832
1817
  for (TFieldIterator<FProperty> It(RowStruct); It; ++It)
@@ -1839,31 +1824,151 @@ TSharedPtr<FJsonValue> FAssetHandlers::SetDataTableRow(const TSharedPtr<FJsonObj
1839
1824
  }
1840
1825
  if (!FieldProp)
1841
1826
  {
1842
- SetErr = FString::Printf(TEXT("row struct '%s' has no field '%s'"), *RowStruct->GetName(), *FieldName);
1843
- bOk = false;
1844
- break;
1827
+ return MCPError(FString::Printf(
1828
+ TEXT("row struct '%s' has no field '%s'"), *RowStruct->GetName(), *Pair.Key));
1845
1829
  }
1846
- void* FieldAddr = FieldProp->ContainerPtrToValuePtr<void>(NewRow);
1830
+ Writes.Emplace(FieldProp, Pair.Value);
1831
+ }
1832
+
1833
+ const int32 StructSize = RowStruct->GetStructureSize();
1834
+ const int32 MinAlign = RowStruct->GetMinAlignment();
1835
+
1836
+ // Snapshot the prior row: it is both the rollback record handed back to
1837
+ // the caller and the copy this handler restores if a field write fails
1838
+ // part way through.
1839
+ //
1840
+ // The rollback record carries the prior values of exactly the fields
1841
+ // about to change, as a row payload for set_datatable_row, which is a
1842
+ // method that exists and now merges in place. It used to name
1843
+ // set_datatable_row_raw, which is registered nowhere, so a flow running
1844
+ // with rollback_on_failure could not restore a row at all; and it carried
1845
+ // whole-struct export text produced with the value as its own defaults,
1846
+ // which exports every field as unchanged and yields "()".
1847
+ TSharedPtr<FJsonObject> PrevFields = MakeShared<FJsonObject>();
1848
+ uint8* Backup = nullptr;
1849
+ if (bExisted)
1850
+ {
1851
+ Backup = (uint8*)FMemory::Malloc(StructSize, MinAlign);
1852
+ RowStruct->InitializeStruct(Backup);
1853
+ RowStruct->CopyScriptStruct(Backup, *ExistingRow);
1854
+ for (const TPair<FProperty*, TSharedPtr<FJsonValue>>& Write : Writes)
1855
+ {
1856
+ const void* FieldAddr = Write.Key->ContainerPtrToValuePtr<void>(Backup);
1857
+ PrevFields->SetField(
1858
+ Write.Key->GetAuthoredName(),
1859
+ FMCPJsonSerializer::SerializeValue(FieldAddr, Write.Key));
1860
+ }
1861
+ }
1862
+
1863
+ // #929: an existing row is edited in the memory the table already owns.
1864
+ // Only the named fields are touched, so every other field keeps the exact
1865
+ // bytes it had, whether or not its UPROPERTY declares a default.
1866
+ //
1867
+ // The previous shape reallocated the row (RemoveRow followed by AddRow)
1868
+ // and relied on a CopyScriptStruct seed to carry the untouched fields
1869
+ // across. That seed is one line away from being lost, it moved the row to
1870
+ // the end of the row map on every edit, and UDataTable::RemoveRow closes
1871
+ // an FScopedDataTableChange whose destructor calls
1872
+ // HandleDataTableChanged(NAME_None), which runs
1873
+ // FTableRowBase::OnDataTableChanged against *every* row in the table
1874
+ // rather than the one being edited. A single-cell write should not reach
1875
+ // the other rows at all.
1876
+ uint8* RowData = bExisted ? *ExistingRow : (uint8*)FMemory::Malloc(StructSize, MinAlign);
1877
+ if (!bExisted)
1878
+ {
1879
+ RowStruct->InitializeStruct(RowData);
1880
+ }
1881
+
1882
+ FString SetErr;
1883
+ bool bOk = true;
1884
+ for (const TPair<FProperty*, TSharedPtr<FJsonValue>>& Write : Writes)
1885
+ {
1886
+ void* FieldAddr = Write.Key->ContainerPtrToValuePtr<void>(RowData);
1847
1887
  FString E;
1848
- if (!MCPJsonProperty::SetJsonOnProperty(FieldProp, FieldAddr, Pair.Value, E))
1888
+ if (!MCPJsonProperty::SetJsonOnProperty(Write.Key, FieldAddr, Write.Value, E))
1849
1889
  {
1850
- SetErr = FString::Printf(TEXT("%s: %s"), *FieldName, *E);
1890
+ SetErr = FString::Printf(TEXT("%s: %s"), *Write.Key->GetAuthoredName(), *E);
1851
1891
  bOk = false;
1852
1892
  break;
1853
1893
  }
1854
1894
  }
1855
1895
  if (!bOk)
1856
1896
  {
1857
- RowStruct->DestroyStruct(NewRow);
1858
- FMemory::Free(NewRow);
1897
+ if (bExisted)
1898
+ {
1899
+ // Put the row back exactly as it was found.
1900
+ RowStruct->CopyScriptStruct(RowData, Backup);
1901
+ RowStruct->DestroyStruct(Backup);
1902
+ FMemory::Free(Backup);
1903
+ }
1904
+ else
1905
+ {
1906
+ RowStruct->DestroyStruct(RowData);
1907
+ FMemory::Free(RowData);
1908
+ }
1859
1909
  return MCPError(SetErr);
1860
1910
  }
1861
1911
 
1862
- // AddRow takes the struct buffer ownership (copies, manages lifetime).
1863
- DataTable->RemoveRow(RowKey);
1864
- DataTable->AddRow(RowKey, *reinterpret_cast<FTableRowBase*>(NewRow));
1865
- RowStruct->DestroyStruct(NewRow);
1866
- FMemory::Free(NewRow);
1912
+ if (!bExisted)
1913
+ {
1914
+ // AddRow copies the buffer and owns the copy from here on.
1915
+ DataTable->AddRow(RowKey, *reinterpret_cast<FTableRowBase*>(RowData));
1916
+ RowStruct->DestroyStruct(RowData);
1917
+ FMemory::Free(RowData);
1918
+ RowData = nullptr;
1919
+ }
1920
+
1921
+ // #935: read the row back out of the table and check that every field the
1922
+ // caller named holds what was asked for, before anything is saved. This
1923
+ // reads the table's own memory, not the buffer that was written, so a
1924
+ // value lost in the copy into the table is caught here too.
1925
+ uint8* const* StoredRow = DataTable->GetRowMap().Find(RowKey);
1926
+ if (!StoredRow || !*StoredRow)
1927
+ {
1928
+ if (bExisted)
1929
+ {
1930
+ RowStruct->DestroyStruct(Backup);
1931
+ FMemory::Free(Backup);
1932
+ }
1933
+ return MCPError(FString::Printf(
1934
+ TEXT("Row '%s' is missing from '%s' after the write. Nothing was saved."),
1935
+ *RowName, *AssetPath));
1936
+ }
1937
+ for (const TPair<FProperty*, TSharedPtr<FJsonValue>>& Write : Writes)
1938
+ {
1939
+ const void* FieldAddr = Write.Key->ContainerPtrToValuePtr<void>(*StoredRow);
1940
+ FString Detail;
1941
+ if (MCPJsonProperty::VerifyJsonOnProperty(Write.Key, FieldAddr, Write.Value, Detail))
1942
+ {
1943
+ continue;
1944
+ }
1945
+
1946
+ // Undo the whole call. A verified-bad write is not a partial success.
1947
+ if (bExisted)
1948
+ {
1949
+ RowStruct->CopyScriptStruct(*StoredRow, Backup);
1950
+ RowStruct->DestroyStruct(Backup);
1951
+ FMemory::Free(Backup);
1952
+ DataTable->HandleDataTableChanged(RowKey);
1953
+ }
1954
+ else
1955
+ {
1956
+ DataTable->RemoveRow(RowKey);
1957
+ }
1958
+ return MCPError(FString::Printf(
1959
+ TEXT("Field '%s' on row '%s' did not store the requested value: %s. The row was left unchanged."),
1960
+ *Write.Key->GetAuthoredName(), *RowName, *Detail));
1961
+ }
1962
+
1963
+ if (bExisted)
1964
+ {
1965
+ RowStruct->DestroyStruct(Backup);
1966
+ FMemory::Free(Backup);
1967
+ Backup = nullptr;
1968
+ // The table's own row memory was edited, so name the row that changed
1969
+ // and only its hook runs.
1970
+ DataTable->HandleDataTableChanged(RowKey);
1971
+ }
1867
1972
 
1868
1973
  DataTable->MarkPackageDirty();
1869
1974
  UEditorAssetLibrary::SaveLoadedAsset(DataTable, /*bOnlyIfIsDirty*/ true);
@@ -1880,8 +1985,8 @@ TSharedPtr<FJsonValue> FAssetHandlers::SetDataTableRow(const TSharedPtr<FJsonObj
1880
1985
  TSharedPtr<FJsonObject> Payload = MakeShared<FJsonObject>();
1881
1986
  Payload->SetStringField(TEXT("assetPath"), AssetPath);
1882
1987
  Payload->SetStringField(TEXT("rowName"), RowName);
1883
- Payload->SetStringField(TEXT("rowExport"), PrevExport);
1884
- MCPSetRollback(Result, TEXT("set_datatable_row_raw"), Payload);
1988
+ Payload->SetObjectField(TEXT("row"), PrevFields);
1989
+ MCPSetRollback(Result, TEXT("set_datatable_row"), Payload);
1885
1990
  }
1886
1991
  else
1887
1992
  {
@@ -1899,16 +2004,11 @@ TSharedPtr<FJsonValue> FAssetHandlers::SetDataTableRow(const TSharedPtr<FJsonObj
1899
2004
  TSharedPtr<FJsonValue> FAssetHandlers::RemoveDataTableRow(const TSharedPtr<FJsonObject>& Params)
1900
2005
  {
1901
2006
  FString AssetPath;
1902
- if (auto Err = RequireStringAlt(Params, TEXT("path"), TEXT("assetPath"), AssetPath)) return Err;
2007
+ UDataTable* DataTable = nullptr;
2008
+ if (auto Err = LoadDataTableParam(Params, AssetPath, DataTable)) return Err;
1903
2009
  FString RowName;
1904
2010
  if (auto Err = RequireString(Params, TEXT("rowName"), RowName)) return Err;
1905
2011
 
1906
- UObject* Asset = UEditorAssetLibrary::LoadAsset(AssetPath);
1907
- UDataTable* DataTable = Cast<UDataTable>(Asset);
1908
- if (!DataTable)
1909
- {
1910
- return MCPError(FString::Printf(TEXT("Asset is not a DataTable: %s"), *AssetPath));
1911
- }
1912
2012
  const FName RowKey(*RowName);
1913
2013
  if (!DataTable->GetRowMap().Contains(RowKey))
1914
2014
  {
@@ -1936,12 +2036,11 @@ TSharedPtr<FJsonValue> FAssetHandlers::RemoveDataTableRow(const TSharedPtr<FJson
1936
2036
  TSharedPtr<FJsonValue> FAssetHandlers::GetDataTableRow(const TSharedPtr<FJsonObject>& Params)
1937
2037
  {
1938
2038
  FString AssetPath;
1939
- if (auto Err = RequireStringAlt(Params, TEXT("path"), TEXT("assetPath"), AssetPath)) return Err;
2039
+ UDataTable* DataTable = nullptr;
2040
+ if (auto Err = LoadDataTableParam(Params, AssetPath, DataTable)) return Err;
1940
2041
  FString RowName;
1941
2042
  if (auto Err = RequireString(Params, TEXT("rowName"), RowName)) return Err;
1942
2043
 
1943
- UDataTable* DataTable = Cast<UDataTable>(UEditorAssetLibrary::LoadAsset(AssetPath));
1944
- if (!DataTable) return MCPError(FString::Printf(TEXT("Asset is not a DataTable: %s"), *AssetPath));
1945
2044
  const UScriptStruct* RowStruct = DataTable->GetRowStruct();
1946
2045
  if (!RowStruct) return MCPError(TEXT("DataTable has no row struct"));
1947
2046
 
@@ -1968,8 +2067,8 @@ TSharedPtr<FJsonValue> FAssetHandlers::GetDataTableRow(const TSharedPtr<FJsonObj
1968
2067
  }
1969
2068
 
1970
2069
  // #535: write a single field on a single row. Thin wrapper over the row-merge
1971
- // upsert (SetDataTableRow), which seeds from the existing row so only the named
1972
- // cell changes. Params: assetPath, rowName, fieldName, value.
2070
+ // upsert (SetDataTableRow), which edits the existing row in place so only the
2071
+ // named cell changes. Params: assetPath, rowName, fieldName, value.
1973
2072
  TSharedPtr<FJsonValue> FAssetHandlers::SetDataTableCell(const TSharedPtr<FJsonObject>& Params)
1974
2073
  {
1975
2074
  FString FieldName;
@@ -1983,11 +2082,10 @@ TSharedPtr<FJsonValue> FAssetHandlers::SetDataTableCell(const TSharedPtr<FJsonOb
1983
2082
  // Build a one-field row object and delegate to the row upsert, which
1984
2083
  // requires the row to exist for a cell edit (no accidental row creation).
1985
2084
  FString AssetPath;
1986
- if (auto Err = RequireStringAlt(Params, TEXT("path"), TEXT("assetPath"), AssetPath)) return Err;
2085
+ UDataTable* DataTable = nullptr;
2086
+ if (auto Err = LoadDataTableParam(Params, AssetPath, DataTable)) return Err;
1987
2087
  FString RowName;
1988
2088
  if (auto Err = RequireString(Params, TEXT("rowName"), RowName)) return Err;
1989
- UDataTable* DataTable = Cast<UDataTable>(UEditorAssetLibrary::LoadAsset(AssetPath));
1990
- if (!DataTable) return MCPError(FString::Printf(TEXT("Asset is not a DataTable: %s"), *AssetPath));
1991
2089
  if (!DataTable->GetRowMap().Contains(FName(*RowName)))
1992
2090
  {
1993
2091
  return MCPError(FString::Printf(TEXT("Row not found: %s (use set_datatable_row to create it)"), *RowName));
@@ -2007,14 +2105,13 @@ TSharedPtr<FJsonValue> FAssetHandlers::SetDataTableCell(const TSharedPtr<FJsonOb
2007
2105
  TSharedPtr<FJsonValue> FAssetHandlers::RenameDataTableRow(const TSharedPtr<FJsonObject>& Params)
2008
2106
  {
2009
2107
  FString AssetPath;
2010
- if (auto Err = RequireStringAlt(Params, TEXT("path"), TEXT("assetPath"), AssetPath)) return Err;
2108
+ UDataTable* DataTable = nullptr;
2109
+ if (auto Err = LoadDataTableParam(Params, AssetPath, DataTable)) return Err;
2011
2110
  FString OldName;
2012
2111
  if (auto Err = RequireStringAlt(Params, TEXT("oldName"), TEXT("rowName"), OldName)) return Err;
2013
2112
  FString NewName;
2014
2113
  if (auto Err = RequireString(Params, TEXT("newName"), NewName)) return Err;
2015
2114
 
2016
- UDataTable* DataTable = Cast<UDataTable>(UEditorAssetLibrary::LoadAsset(AssetPath));
2017
- if (!DataTable) return MCPError(FString::Printf(TEXT("Asset is not a DataTable: %s"), *AssetPath));
2018
2115
  const UScriptStruct* RowStruct = DataTable->GetRowStruct();
2019
2116
  if (!RowStruct) return MCPError(TEXT("DataTable has no row struct"));
2020
2117
 
@@ -2063,7 +2160,8 @@ TSharedPtr<FJsonValue> FAssetHandlers::RenameDataTableRow(const TSharedPtr<FJson
2063
2160
  TSharedPtr<FJsonValue> FAssetHandlers::FillDataTableFromJson(const TSharedPtr<FJsonObject>& Params)
2064
2161
  {
2065
2162
  FString AssetPath;
2066
- if (auto Err = RequireStringAlt(Params, TEXT("path"), TEXT("assetPath"), AssetPath)) return Err;
2163
+ UDataTable* DataTable = nullptr;
2164
+ if (auto Err = LoadDataTableParam(Params, AssetPath, DataTable)) return Err;
2067
2165
 
2068
2166
  const TSharedPtr<FJsonObject>* RowsObj = nullptr;
2069
2167
  TSharedPtr<FJsonObject> ParsedRows;
@@ -2085,8 +2183,6 @@ TSharedPtr<FJsonValue> FAssetHandlers::FillDataTableFromJson(const TSharedPtr<FJ
2085
2183
  return MCPError(TEXT("Missing 'rows' object (or 'jsonString') mapping rowName -> {field: value}"));
2086
2184
  }
2087
2185
 
2088
- UDataTable* DataTable = Cast<UDataTable>(UEditorAssetLibrary::LoadAsset(AssetPath));
2089
- if (!DataTable) return MCPError(FString::Printf(TEXT("Asset is not a DataTable: %s"), *AssetPath));
2090
2186
  const UScriptStruct* RowStruct = DataTable->GetRowStruct();
2091
2187
  if (!RowStruct) return MCPError(TEXT("DataTable has no row struct"));
2092
2188
 
@@ -2132,16 +2228,14 @@ namespace
2132
2228
  {
2133
2229
  if (auto Err = RequireStringAlt(Params, TEXT("path"), TEXT("assetPath"), OutAssetPath)) return Err;
2134
2230
 
2135
- UObject* Asset = UEditorAssetLibrary::LoadAsset(OutAssetPath);
2136
- if (!Asset)
2137
- {
2138
- return MCPError(FString::Printf(TEXT("Asset not found: %s"), *OutAssetPath));
2139
- }
2231
+ TSharedPtr<FJsonValue> LoadError;
2232
+ UObject* Asset = MCPRequireAssetObject(OutAssetPath, LoadError);
2233
+ if (!Asset) return LoadError;
2140
2234
 
2141
2235
  OutStringTable = Cast<UStringTable>(Asset);
2142
2236
  if (!OutStringTable)
2143
2237
  {
2144
- return MCPError(FString::Printf(TEXT("Asset is not a StringTable: %s"), *OutAssetPath));
2238
+ return MCPAssetWrongTypeError(OutAssetPath, Asset, TEXT("StringTable"));
2145
2239
  }
2146
2240
  return nullptr;
2147
2241
  }
@@ -2222,7 +2316,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::CreateStringTable(const TSharedPtr<FJsonO
2222
2316
  {
2223
2317
  FString ExistingAssetPath;
2224
2318
  ExistingObj->TryGetStringField(TEXT("path"), ExistingAssetPath);
2225
- if (UStringTable* Existing = LoadObject<UStringTable>(nullptr, *ExistingAssetPath))
2319
+ if (UStringTable* Existing = LoadAssetByPath<UStringTable>(ExistingAssetPath))
2226
2320
  {
2227
2321
  SetStringTableInfoFields(ExistingObj, Existing);
2228
2322
  }
@@ -2481,6 +2575,256 @@ TSharedPtr<FJsonValue> FAssetHandlers::ImportStringTable(const TSharedPtr<FJsonO
2481
2575
  return MCPResult(Result);
2482
2576
  }
2483
2577
 
2578
+ // ---------------------------------------------------------------------------
2579
+ // import_stringtable_csv (#978)
2580
+ //
2581
+ // String Tables could be created and inspected but not refreshed from a CSV
2582
+ // source with any confidence, so a user kept the CSV as the canonical copy and
2583
+ // ran a custom editor script instead. This is the same engine importer
2584
+ // FStringTable exposes, wrapped so the caller gets the three things the script
2585
+ // was written to provide: the CSV is proved to parse and to carry the expected
2586
+ // keys BEFORE the asset is touched, the result names exactly which keys were
2587
+ // added, updated and removed, and the save is reported rather than assumed.
2588
+ // ---------------------------------------------------------------------------
2589
+
2590
+ namespace
2591
+ {
2592
+ /** Every key and source string in a table, as a plain map. */
2593
+ void MCPSnapshotStringTable(const UStringTable* Table, TMap<FString, FString>& Out)
2594
+ {
2595
+ Out.Reset();
2596
+ if (!Table) return;
2597
+ Table->GetStringTable()->EnumerateKeysAndSourceStrings(
2598
+ [&Out](const FTextKey& Key, const FString& SourceString) -> bool
2599
+ {
2600
+ Out.Add(Key.ToString(), SourceString);
2601
+ return true;
2602
+ });
2603
+ }
2604
+
2605
+ /** Run the engine's CSV importer over Table, on whichever engine this is. */
2606
+ bool MCPImportStringTableCsvFile(UStringTable* Table, const FString& FilePath)
2607
+ {
2608
+ if (!Table) return false;
2609
+ #if UE_MCP_HAS_5_8_API
2610
+ return Table->GetMutableStringTable()->ImportStringsFromCSVFile(FilePath);
2611
+ #else
2612
+ return Table->GetMutableStringTable()->ImportStrings(FilePath);
2613
+ #endif
2614
+ }
2615
+
2616
+ TArray<TSharedPtr<FJsonValue>> MCPSortedKeysToJson(const TArray<FString>& Keys)
2617
+ {
2618
+ TArray<FString> Sorted = Keys;
2619
+ Sorted.Sort();
2620
+ TArray<TSharedPtr<FJsonValue>> Out;
2621
+ Out.Reserve(Sorted.Num());
2622
+ for (const FString& Key : Sorted) Out.Add(MakeShared<FJsonValueString>(Key));
2623
+ return Out;
2624
+ }
2625
+ }
2626
+
2627
+ TSharedPtr<FJsonValue> FAssetHandlers::ImportStringTableCsv(const TSharedPtr<FJsonObject>& Params)
2628
+ {
2629
+ MCP_CHECK_GAME_THREAD();
2630
+
2631
+ FString AssetPath;
2632
+ UStringTable* StringTable = nullptr;
2633
+ if (auto Err = LoadStringTableAsset(Params, AssetPath, StringTable)) return Err;
2634
+
2635
+ if (MCPIsProtectedAssetPath(AssetPath))
2636
+ {
2637
+ return MCPError(FString::Printf(
2638
+ TEXT("'%s' is on a protected mount (/Engine/, /Script/, /Memory/, /Temp/), which the bridge never writes to."),
2639
+ *AssetPath));
2640
+ }
2641
+
2642
+ FString CsvPath;
2643
+ if (auto Err = RequireStringAlt(Params, TEXT("csvPath"), TEXT("filePath"), CsvPath)) return Err;
2644
+ // A relative path is read against the project directory, not the editor's
2645
+ // working directory, which is not something a caller can predict.
2646
+ if (FPaths::IsRelative(CsvPath))
2647
+ {
2648
+ CsvPath = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir(), CsvPath);
2649
+ }
2650
+ if (!FPaths::FileExists(CsvPath))
2651
+ {
2652
+ return MCPError(FString::Printf(TEXT("CSV file not found: %s"), *CsvPath));
2653
+ }
2654
+
2655
+ const bool bReplaceExisting = OptionalBool(Params, TEXT("replaceExisting"), false);
2656
+ const bool bRequireExactKeys = OptionalBool(Params, TEXT("requireExactKeys"), false);
2657
+ const bool bSave = OptionalBool(Params, TEXT("save"), true);
2658
+
2659
+ TArray<FString> ExpectedKeys;
2660
+ const TArray<TSharedPtr<FJsonValue>>* ExpectedField = nullptr;
2661
+ if (Params->TryGetArrayField(TEXT("expectedKeys"), ExpectedField) && ExpectedField)
2662
+ {
2663
+ ExpectedKeys = JsonArrayToStringList(ExpectedField);
2664
+ }
2665
+
2666
+ // Parse and validate against a throwaway table first, so a malformed CSV or
2667
+ // a key set that does not match expectations never reaches the asset.
2668
+ UStringTable* Staged = NewObject<UStringTable>(GetTransientPackage(), NAME_None, RF_Transient);
2669
+ if (!Staged)
2670
+ {
2671
+ return MCPError(TEXT("Could not create the staging StringTable used to validate the CSV."));
2672
+ }
2673
+ const FGCRootScope KeepStagedAlive(Staged);
2674
+ if (!MCPImportStringTableCsvFile(Staged, CsvPath))
2675
+ {
2676
+ return MCPError(FString::Printf(
2677
+ TEXT("The engine's String Table importer rejected '%s'. Nothing was changed. ")
2678
+ TEXT("The file must be a CSV whose first column is the entry key; the editor log carries the parse error."),
2679
+ *CsvPath));
2680
+ }
2681
+
2682
+ TMap<FString, FString> Incoming;
2683
+ MCPSnapshotStringTable(Staged, Incoming);
2684
+ if (Incoming.Num() == 0)
2685
+ {
2686
+ return MCPError(FString::Printf(
2687
+ TEXT("'%s' parsed but carried no entries, so nothing was changed."), *CsvPath));
2688
+ }
2689
+
2690
+ if (ExpectedKeys.Num() > 0)
2691
+ {
2692
+ TArray<FString> MissingKeys;
2693
+ for (const FString& Expected : ExpectedKeys)
2694
+ {
2695
+ if (!Incoming.Contains(Expected)) MissingKeys.Add(Expected);
2696
+ }
2697
+ TArray<FString> UnexpectedKeys;
2698
+ if (bRequireExactKeys)
2699
+ {
2700
+ for (const TPair<FString, FString>& Entry : Incoming)
2701
+ {
2702
+ if (!ExpectedKeys.Contains(Entry.Key)) UnexpectedKeys.Add(Entry.Key);
2703
+ }
2704
+ }
2705
+ if (MissingKeys.Num() > 0 || UnexpectedKeys.Num() > 0)
2706
+ {
2707
+ TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
2708
+ Obj->SetBoolField(TEXT("success"), false);
2709
+ Obj->SetStringField(TEXT("error"), FString::Printf(
2710
+ TEXT("'%s' does not match the expected key set, so the String Table was not touched: %d expected key(s) missing%s."),
2711
+ *CsvPath, MissingKeys.Num(),
2712
+ bRequireExactKeys
2713
+ ? *FString::Printf(TEXT(", %d unexpected"), UnexpectedKeys.Num())
2714
+ : TEXT("")));
2715
+ Obj->SetStringField(TEXT("reason"), TEXT("key_set_mismatch"));
2716
+ Obj->SetStringField(TEXT("assetPath"), AssetPath);
2717
+ Obj->SetStringField(TEXT("csvPath"), CsvPath);
2718
+ Obj->SetNumberField(TEXT("csvKeyCount"), Incoming.Num());
2719
+ Obj->SetArrayField(TEXT("missingKeys"), MCPSortedKeysToJson(MissingKeys));
2720
+ Obj->SetArrayField(TEXT("unexpectedKeys"), MCPSortedKeysToJson(UnexpectedKeys));
2721
+ return MakeShared<FJsonValueObject>(Obj);
2722
+ }
2723
+ }
2724
+
2725
+ TMap<FString, FString> Before;
2726
+ MCPSnapshotStringTable(StringTable, Before);
2727
+
2728
+ // The merge runs first and the pruning second, so a parse that somehow
2729
+ // fails on the second pass cannot leave the table emptied.
2730
+ StringTable->Modify(true);
2731
+ if (!MCPImportStringTableCsvFile(StringTable, CsvPath))
2732
+ {
2733
+ return MCPError(FString::Printf(
2734
+ TEXT("'%s' parsed into the staging table but not into '%s'. The table is unchanged apart from any partial merge the importer applied; re-read it before retrying."),
2735
+ *CsvPath, *AssetPath));
2736
+ }
2737
+
2738
+ TArray<FString> RemovedKeys;
2739
+ if (bReplaceExisting)
2740
+ {
2741
+ for (const TPair<FString, FString>& Entry : Before)
2742
+ {
2743
+ if (!Incoming.Contains(Entry.Key))
2744
+ {
2745
+ StringTable->GetMutableStringTable()->RemoveSourceString(FTextKey(Entry.Key));
2746
+ RemovedKeys.Add(Entry.Key);
2747
+ }
2748
+ }
2749
+ }
2750
+
2751
+ TMap<FString, FString> After;
2752
+ MCPSnapshotStringTable(StringTable, After);
2753
+
2754
+ TArray<FString> AddedKeys;
2755
+ TArray<FString> UpdatedKeys;
2756
+ for (const TPair<FString, FString>& Entry : After)
2757
+ {
2758
+ if (const FString* Previous = Before.Find(Entry.Key))
2759
+ {
2760
+ if (!Previous->Equals(Entry.Value, ESearchCase::CaseSensitive))
2761
+ {
2762
+ UpdatedKeys.Add(Entry.Key);
2763
+ }
2764
+ }
2765
+ else
2766
+ {
2767
+ AddedKeys.Add(Entry.Key);
2768
+ }
2769
+ }
2770
+
2771
+ UPackage* Package = StringTable->GetOutermost();
2772
+ bool bPersisted = false;
2773
+ FString PersistReason;
2774
+ if (bSave)
2775
+ {
2776
+ bPersisted = SaveAssetPackage(StringTable);
2777
+ if (!bPersisted)
2778
+ {
2779
+ PersistReason = FString::Printf(
2780
+ TEXT("The editor refused to write '%s'. The entries are in memory only."),
2781
+ Package ? *Package->GetName() : *AssetPath);
2782
+ }
2783
+ }
2784
+ else
2785
+ {
2786
+ PersistReason = TEXT("save=false was requested, so the imported entries are in memory only until the package is saved.");
2787
+ }
2788
+
2789
+ auto Result = MCPSuccess();
2790
+ MCPSetUpdated(Result);
2791
+ SetStringTableInfoFields(Result, StringTable);
2792
+ Result->SetStringField(TEXT("assetPath"), AssetPath);
2793
+ Result->SetStringField(TEXT("csvPath"), CsvPath);
2794
+ Result->SetBoolField(TEXT("replaceExisting"), bReplaceExisting);
2795
+ Result->SetNumberField(TEXT("csvKeyCount"), Incoming.Num());
2796
+ Result->SetNumberField(TEXT("entryCountBefore"), Before.Num());
2797
+ Result->SetNumberField(TEXT("entryCountAfter"), After.Num());
2798
+ Result->SetArrayField(TEXT("addedKeys"), MCPSortedKeysToJson(AddedKeys));
2799
+ Result->SetArrayField(TEXT("updatedKeys"), MCPSortedKeysToJson(UpdatedKeys));
2800
+ Result->SetArrayField(TEXT("removedKeys"), MCPSortedKeysToJson(RemovedKeys));
2801
+ TArray<FString> AllKeys;
2802
+ After.GetKeys(AllKeys);
2803
+ Result->SetArrayField(TEXT("keys"), MCPSortedKeysToJson(AllKeys));
2804
+ if (ExpectedKeys.Num() > 0)
2805
+ {
2806
+ Result->SetNumberField(TEXT("expectedKeyCount"), ExpectedKeys.Num());
2807
+ Result->SetBoolField(TEXT("expectedKeysPresent"), true);
2808
+ }
2809
+ Result->SetBoolField(TEXT("persisted"), bPersisted);
2810
+ Result->SetBoolField(TEXT("saved"), bPersisted);
2811
+ if (Package)
2812
+ {
2813
+ Result->SetStringField(TEXT("packageName"), Package->GetName());
2814
+ Result->SetBoolField(TEXT("packageDirty"), Package->IsDirty());
2815
+ }
2816
+ if (!bPersisted)
2817
+ {
2818
+ Result->SetStringField(TEXT("persistError"), PersistReason);
2819
+ if (bSave)
2820
+ {
2821
+ Result->SetBoolField(TEXT("success"), false);
2822
+ Result->SetStringField(TEXT("error"), PersistReason);
2823
+ }
2824
+ }
2825
+ return MCPResult(Result);
2826
+ }
2827
+
2484
2828
  // --- Reimport ---------------------------------------------------------
2485
2829
 
2486
2830
 
@@ -2491,11 +2835,9 @@ TSharedPtr<FJsonValue> FAssetHandlers::ReimportAsset(const TSharedPtr<FJsonObjec
2491
2835
  FString AssetPath;
2492
2836
  if (auto Err = RequireStringAlt(Params, TEXT("assetPath"), TEXT("path"), AssetPath)) return Err;
2493
2837
 
2494
- UObject* Asset = LoadObject<UObject>(nullptr, *AssetPath);
2495
- if (!Asset)
2496
- {
2497
- return MCPError(FString::Printf(TEXT("Asset not found: %s"), *AssetPath));
2498
- }
2838
+ TSharedPtr<FJsonValue> LoadError;
2839
+ UObject* Asset = MCPRequireAssetObject(AssetPath, LoadError);
2840
+ if (!Asset) return LoadError;
2499
2841
 
2500
2842
  // Optionally override the source file path
2501
2843
  FString NewSourcePath;
@@ -2563,7 +2905,9 @@ TSharedPtr<FJsonValue> FAssetHandlers::ExportAsset(const TSharedPtr<FJsonObject>
2563
2905
  FString OutputPath;
2564
2906
  if (auto Err = RequireString(Params, TEXT("outputPath"), OutputPath)) return Err;
2565
2907
 
2566
- UObject* Asset = UEditorAssetLibrary::LoadAsset(AssetPath);
2908
+ // #930: same resolution as asset(read), which opened assets this action
2909
+ // reported as missing.
2910
+ UObject* Asset = MCPLoadAssetObject(AssetPath);
2567
2911
  if (!Asset)
2568
2912
  {
2569
2913
  return MCPError(FString::Printf(TEXT("Asset not found: %s"), *AssetPath));
@@ -2613,8 +2957,8 @@ TSharedPtr<FJsonValue> FAssetHandlers::ExportTexture(const TSharedPtr<FJsonObjec
2613
2957
  FString OutputPath;
2614
2958
  if (auto Err = RequireStringAlt(Params, TEXT("outputPath"), TEXT("filePath"), OutputPath)) return Err;
2615
2959
 
2616
- UTexture2D* Texture = Cast<UTexture2D>(UEditorAssetLibrary::LoadAsset(AssetPath));
2617
- if (!Texture) return MCPError(FString::Printf(TEXT("Texture2D not found: %s"), *AssetPath));
2960
+ UTexture2D* Texture = LoadAssetByPath<UTexture2D>(AssetPath);
2961
+ if (!Texture) return MCPAssetLoadError(AssetPath, TEXT("Texture2D"));
2618
2962
 
2619
2963
  FString AbsPath = OutputPath;
2620
2964
  if (FPaths::IsRelative(AbsPath)) AbsPath = FPaths::Combine(FPaths::ProjectDir(), AbsPath);
@@ -2651,10 +2995,10 @@ TSharedPtr<FJsonValue> FAssetHandlers::CompareTextures(const TSharedPtr<FJsonObj
2651
2995
  if (auto Err = RequireStringAlt(Params, TEXT("assetPathA"), TEXT("a"), PathA)) return Err;
2652
2996
  if (auto Err = RequireStringAlt(Params, TEXT("assetPathB"), TEXT("b"), PathB)) return Err;
2653
2997
 
2654
- UTexture2D* A = Cast<UTexture2D>(UEditorAssetLibrary::LoadAsset(PathA));
2655
- UTexture2D* B = Cast<UTexture2D>(UEditorAssetLibrary::LoadAsset(PathB));
2656
- if (!A) return MCPError(FString::Printf(TEXT("Texture2D not found: %s"), *PathA));
2657
- if (!B) return MCPError(FString::Printf(TEXT("Texture2D not found: %s"), *PathB));
2998
+ UTexture2D* A = LoadAssetByPath<UTexture2D>(PathA);
2999
+ UTexture2D* B = LoadAssetByPath<UTexture2D>(PathB);
3000
+ if (!A) return MCPAssetLoadError(PathA, TEXT("Texture2D"));
3001
+ if (!B) return MCPAssetLoadError(PathB, TEXT("Texture2D"));
2658
3002
 
2659
3003
  const bool bSameDims = A->GetSizeX() == B->GetSizeX() && A->GetSizeY() == B->GetSizeY();
2660
3004
  const bool bSameFormat = A->GetPixelFormat() == B->GetPixelFormat();