ue-mcp 1.2.4 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. package/README.md +4 -3
  2. package/dist/action-class.js +56 -3
  3. package/dist/action-class.js.map +1 -1
  4. package/dist/bridge-timeouts.d.ts +64 -0
  5. package/dist/bridge-timeouts.js +118 -0
  6. package/dist/bridge-timeouts.js.map +1 -0
  7. package/dist/bridge.js +10 -2
  8. package/dist/bridge.js.map +1 -1
  9. package/dist/editor-control.d.ts +72 -5
  10. package/dist/editor-control.js +164 -140
  11. package/dist/editor-control.js.map +1 -1
  12. package/dist/editor-target.d.ts +55 -1
  13. package/dist/editor-target.js +107 -10
  14. package/dist/editor-target.js.map +1 -1
  15. package/dist/engine-observer.d.ts +59 -5
  16. package/dist/engine-observer.js +249 -29
  17. package/dist/engine-observer.js.map +1 -1
  18. package/dist/engine-root.d.ts +91 -0
  19. package/dist/engine-root.js +448 -0
  20. package/dist/engine-root.js.map +1 -0
  21. package/dist/epic-enrich.js +16 -1
  22. package/dist/epic-enrich.js.map +1 -1
  23. package/dist/feedback-cli.js +4 -0
  24. package/dist/feedback-cli.js.map +1 -1
  25. package/dist/feedback-deferred.d.ts +9 -0
  26. package/dist/feedback-deferred.js +2 -0
  27. package/dist/feedback-deferred.js.map +1 -1
  28. package/dist/feedback-fallback.d.ts +99 -0
  29. package/dist/feedback-fallback.js +173 -0
  30. package/dist/feedback-fallback.js.map +1 -0
  31. package/dist/flow/write-methods.js +20 -0
  32. package/dist/flow/write-methods.js.map +1 -1
  33. package/dist/lean-context.js +8 -3
  34. package/dist/lean-context.js.map +1 -1
  35. package/dist/locking.js +11 -2
  36. package/dist/locking.js.map +1 -1
  37. package/dist/project.d.ts +11 -0
  38. package/dist/project.js +23 -2
  39. package/dist/project.js.map +1 -1
  40. package/dist/python-gate.d.ts +95 -0
  41. package/dist/python-gate.js +175 -0
  42. package/dist/python-gate.js.map +1 -0
  43. package/dist/tool-counts.json +16 -16
  44. package/dist/tools/animation.js +288 -24
  45. package/dist/tools/animation.js.map +1 -1
  46. package/dist/tools/asset.js +79 -14
  47. package/dist/tools/asset.js.map +1 -1
  48. package/dist/tools/audio.js +59 -5
  49. package/dist/tools/audio.js.map +1 -1
  50. package/dist/tools/blueprint.js +29 -12
  51. package/dist/tools/blueprint.js.map +1 -1
  52. package/dist/tools/chooser.js +4 -1
  53. package/dist/tools/chooser.js.map +1 -1
  54. package/dist/tools/editor.js +81 -43
  55. package/dist/tools/editor.js.map +1 -1
  56. package/dist/tools/feedback.js +214 -48
  57. package/dist/tools/feedback.js.map +1 -1
  58. package/dist/tools/foliage.js +27 -1
  59. package/dist/tools/foliage.js.map +1 -1
  60. package/dist/tools/gameplay.js +38 -11
  61. package/dist/tools/gameplay.js.map +1 -1
  62. package/dist/tools/gas.js +17 -5
  63. package/dist/tools/gas.js.map +1 -1
  64. package/dist/tools/landscape.js +22 -7
  65. package/dist/tools/landscape.js.map +1 -1
  66. package/dist/tools/level.js +322 -51
  67. package/dist/tools/level.js.map +1 -1
  68. package/dist/tools/material.js +10 -4
  69. package/dist/tools/material.js.map +1 -1
  70. package/dist/tools/niagara.js +3 -2
  71. package/dist/tools/niagara.js.map +1 -1
  72. package/dist/tools/pcg.js +6 -5
  73. package/dist/tools/pcg.js.map +1 -1
  74. package/dist/tools/project.js +47 -20
  75. package/dist/tools/project.js.map +1 -1
  76. package/dist/types.d.ts +21 -0
  77. package/dist/types.js +46 -3
  78. package/dist/types.js.map +1 -1
  79. package/dist/ue-mcp.default.yml +493 -125
  80. package/package.json +2 -2
  81. package/plugin/ue_mcp_bridge/NATIVE_RUNTIME_VISIBILITY.md +53 -0
  82. package/plugin/ue_mcp_bridge/NATIVE_STATIC_MESH_USAGE_PROGRESS.md +115 -0
  83. package/plugin/ue_mcp_bridge/README_MassHandlers.md +97 -0
  84. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/BridgeServer.cpp +14 -0
  85. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/GameThreadExecutor.cpp +21 -4
  86. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/GameThreadExecutor.h +9 -0
  87. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/HandlerJsonProperty.h +395 -69
  88. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers.cpp +79 -3
  89. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers.h +25 -0
  90. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_ControlRig.cpp +3 -3
  91. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_ControlRigSequencer.cpp +3061 -0
  92. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_IKRetargeterAuthoring.cpp +961 -0
  93. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_IKRigAuthoring.cpp +900 -0
  94. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Pose.cpp +769 -0
  95. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Sequence.cpp +44 -3
  96. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_SkeletalLive.cpp +217 -11
  97. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_StateMachine.cpp +551 -24
  98. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Validation.cpp +735 -0
  99. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers.cpp +352 -73
  100. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers.h +8 -0
  101. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkProperties.cpp +1 -1
  102. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkRead.cpp +503 -0
  103. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkRead.h +21 -0
  104. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkUpsert.cpp +2 -2
  105. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Enum.cpp +2 -2
  106. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Geometry.cpp +902 -0
  107. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Geometry.h +33 -0
  108. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Import.cpp +478 -134
  109. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Mesh.cpp +5 -5
  110. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_MeshBoolean.cpp +933 -0
  111. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_MeshBoolean.h +35 -0
  112. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Redirectors.cpp +478 -0
  113. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_RenderTarget.cpp +1 -1
  114. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Sockets.cpp +4 -4
  115. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Struct.cpp +2 -2
  116. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Subobject.cpp +319 -0
  117. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AudioHandlers_SoundCue.cpp +3 -3
  118. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers.cpp +482 -35
  119. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers.h +10 -0
  120. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Collision.cpp +502 -0
  121. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Collision.h +43 -0
  122. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Functions.cpp +89 -7
  123. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Graph.cpp +17 -51
  124. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Internal.h +107 -0
  125. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Properties.cpp +10 -10
  126. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Search.cpp +648 -0
  127. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DemoHandlers.cpp +5 -1
  128. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DiffHandlers.cpp +252 -1
  129. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DiffHandlers.h +10 -2
  130. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers.cpp +252 -41
  131. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers.h +4 -0
  132. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_Build.cpp +250 -27
  133. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_PIE.cpp +303 -125
  134. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_PIERuntime.cpp +73 -50
  135. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_RuntimeVisibility.cpp +757 -0
  136. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers.cpp +11 -2
  137. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers.h +3 -0
  138. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers_Batch.cpp +419 -0
  139. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers.cpp +39 -161
  140. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers.h +66 -1
  141. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_BTAuthoring.cpp +1323 -0
  142. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_BehaviorTree.cpp +1211 -0
  143. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.cpp +2 -0
  144. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.h +60 -0
  145. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers_Runtime.cpp +363 -12
  146. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/HandlerEditorState.h +84 -0
  147. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.cpp +1288 -36
  148. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.h +3 -0
  149. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers_Sculpt.cpp +41 -16
  150. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers.cpp +959 -213
  151. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers.h +66 -0
  152. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_BatchWrite.cpp +1571 -0
  153. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Convert.cpp +316 -0
  154. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Inspection.cpp +280 -0
  155. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_InstanceProjection.cpp +666 -0
  156. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_InstanceProjection_Internal.h +63 -0
  157. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Lights.cpp +43 -12
  158. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_MultiLevel.cpp +13 -47
  159. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_PostProcess.cpp +527 -0
  160. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Query.cpp +1169 -0
  161. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Refresh.cpp +723 -0
  162. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Save.cpp +325 -0
  163. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Spatial.cpp +360 -0
  164. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Trace.cpp +178 -58
  165. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Transient.cpp +337 -0
  166. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Volumes.cpp +25 -12
  167. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_WorldPartition.cpp +10 -9
  168. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_WorldPartitionSettings.cpp +686 -0
  169. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MassHandlers.cpp +467 -0
  170. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MassHandlers.h +22 -0
  171. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers.cpp +452 -175
  172. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers.h +5 -0
  173. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers_Build.cpp +597 -0
  174. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/NiagaraHandlers.cpp +17 -15
  175. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PCGHandlers.cpp +38 -168
  176. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PhysicsHandlers.cpp +39 -30
  177. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.cpp +392 -78
  178. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.h +2 -0
  179. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers_Internal.h +83 -0
  180. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SkeletalMeshHandlers.cpp +200 -0
  181. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SkeletalMeshHandlers.h +15 -0
  182. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SplineHandlers.cpp +19 -12
  183. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/StateTreeHandlers.cpp +31 -1
  184. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers.cpp +307 -69
  185. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers.h +84 -0
  186. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers_Extraction.cpp +8 -41
  187. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers_Properties.cpp +33 -52
  188. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/ActorSelectorTests.cpp +344 -0
  189. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AnimationControlRigTimelineTests.cpp +60 -0
  190. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AnimationPoseTests.cpp +115 -0
  191. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetBulkReadTests.cpp +177 -0
  192. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetDeleteGuardTests.cpp +276 -0
  193. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetPathResolutionTests.cpp +243 -0
  194. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetPropertyPersistTests.cpp +213 -0
  195. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetSubobjectTests.cpp +235 -0
  196. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AutomationRunnerTests.cpp +72 -0
  197. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BehaviorTreeAuthoringTests.cpp +526 -0
  198. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BehaviorTreeNodeTests.cpp +334 -0
  199. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BlueprintHandlerSurfaceTests.cpp +362 -0
  200. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BridgeProtocolTests.cpp +14 -0
  201. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/CollisionQueryTests.cpp +288 -0
  202. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/DataTableRowWriteTests.cpp +453 -0
  203. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/EditorFunctionCallTests.cpp +223 -0
  204. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/FoliageBatchTests.cpp +147 -0
  205. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/GameThreadGateTests.cpp +82 -0
  206. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/GasLiveAttributeTests.cpp +183 -0
  207. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LandscapeSampleTests.cpp +119 -0
  208. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelBatchWriteTests.cpp +176 -0
  209. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelComponentTreeInstanceCountTests.cpp +189 -0
  210. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelConvertBrushesTests.cpp +100 -0
  211. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelInstanceProjectionTests.cpp +265 -0
  212. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelPostProcessOverrideTests.cpp +258 -0
  213. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelQueryComponentsTests.cpp +195 -0
  214. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelRefreshTests.cpp +142 -0
  215. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelSaveReportTests.cpp +125 -0
  216. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelScriptHandlersTests.cpp +42 -0
  217. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelStaticMeshUsageTests.cpp +412 -0
  218. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelTraceWorldScopeTests.cpp +133 -0
  219. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelTransientActorTests.cpp +205 -0
  220. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelWorldPartitionSettingsTests.cpp +168 -0
  221. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MCPInstancedStructPathTestTypes.h +51 -0
  222. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MCPInstancedStructPathTests.cpp +379 -0
  223. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MaterialBuildTests.cpp +238 -0
  224. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MeshBooleanTests.cpp +170 -0
  225. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MeshGeometryTests.cpp +116 -0
  226. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/PackageSaveExtensionTests.cpp +161 -0
  227. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/PackageWriteGuardTests.cpp +253 -0
  228. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/RuntimeVisibilityTests.cpp +143 -0
  229. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SequencerHandlerTests.cpp +660 -0
  230. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SequencerScrubTests.cpp +50 -0
  231. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SkeletonDiffTests.cpp +685 -0
  232. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/WidgetBlueprintResolveTests.cpp +145 -0
  233. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/UE_MCP_Bridge.cpp +80 -0
  234. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerFunctionCall.h +509 -0
  235. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerQuery.h +407 -0
  236. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerUtils.h +1008 -77
  237. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/UE_MCP_Bridge.Build.cs +87 -0
  238. package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Private/MCPEngineStatus.cpp +152 -14
  239. package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Private/Tests/MCPEngineStatusTests.cpp +65 -0
  240. package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Public/MCPEngineStatus.h +32 -1
  241. package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/UE_MCP_BridgeStatus.Build.cs +2 -0
  242. package/skills/ue-mcp-animation/SKILL.md +154 -0
  243. package/skills/ue-mcp-blueprint/SKILL.md +71 -0
@@ -33,6 +33,7 @@
33
33
  #include "Materials/MaterialExpressionFresnel.h"
34
34
  #include "Factories/MaterialInstanceConstantFactoryNew.h"
35
35
  #include "Factories/MaterialFactoryNew.h"
36
+ #include "MaterialEditingLibrary.h"
36
37
  #include "AssetToolsModule.h"
37
38
  #include "IAssetTools.h"
38
39
  #include "EditorScriptingUtilities/Public/EditorAssetLibrary.h"
@@ -87,6 +88,9 @@ void FMaterialHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
87
88
  Registry.RegisterHandler(TEXT("begin_material_transaction"), &BeginMaterialTransaction);
88
89
  Registry.RegisterHandler(TEXT("end_material_transaction"), &EndMaterialTransaction);
89
90
 
91
+ // #946: texture-set build with automatic virtual/UDIM sampler selection.
92
+ Registry.RegisterHandler(TEXT("build_material"), &BuildMaterial);
93
+
90
94
  Registry.RegisterHandler(TEXT("create_material_simple"), &CreateMaterialSimple);
91
95
  Registry.RegisterHandler(TEXT("set_material_usage"), &SetMaterialUsage);
92
96
 
@@ -162,6 +166,108 @@ namespace
162
166
  return Obj;
163
167
  }
164
168
 
169
+ // A colour object, in any of the spellings a client might use: {r,g,b,a},
170
+ // {R,G,B,A} or {x,y,z,w}. Missing components fall back to 0, with alpha 1.
171
+ bool TryReadMaterialColorObject(const TSharedPtr<FJsonObject>& Obj, FLinearColor& OutColor)
172
+ {
173
+ if (!Obj.IsValid()) return false;
174
+ double R = 0.0, G = 0.0, B = 0.0, A = 1.0;
175
+ bool bAny = false;
176
+ auto Pick = [&Obj, &bAny](const TCHAR* Lower, const TCHAR* Upper, const TCHAR* Alt, double& Slot)
177
+ {
178
+ double V = 0.0;
179
+ if (Obj->TryGetNumberField(Lower, V) || Obj->TryGetNumberField(Upper, V) || Obj->TryGetNumberField(Alt, V))
180
+ {
181
+ Slot = V;
182
+ bAny = true;
183
+ }
184
+ };
185
+ Pick(TEXT("r"), TEXT("R"), TEXT("x"), R);
186
+ Pick(TEXT("g"), TEXT("G"), TEXT("y"), G);
187
+ Pick(TEXT("b"), TEXT("B"), TEXT("z"), B);
188
+ Pick(TEXT("a"), TEXT("A"), TEXT("w"), A);
189
+ if (!bAny) return false;
190
+ OutColor = FLinearColor((float)R, (float)G, (float)B, (float)A);
191
+ return true;
192
+ }
193
+
194
+ // #952: a colour arrives in whatever shape the calling client serialised it
195
+ // in - an object {r,g,b,a}, an array [r,g,b,a], a re-encoded JSON string, or
196
+ // UE struct text "(R=..,G=..,B=..,A=..)". Accept every one of them under a
197
+ // single named field so no caller has to guess the wire format.
198
+ bool TryParseMaterialColorField(
199
+ const TSharedPtr<FJsonObject>& Params,
200
+ const TCHAR* FieldName,
201
+ FLinearColor& OutColor)
202
+ {
203
+ if (!Params.IsValid()) return false;
204
+
205
+ const TSharedPtr<FJsonObject>* AsObject = nullptr;
206
+ if (Params->TryGetObjectField(FieldName, AsObject) && AsObject)
207
+ {
208
+ return TryReadMaterialColorObject(*AsObject, OutColor);
209
+ }
210
+
211
+ const TArray<TSharedPtr<FJsonValue>>* AsArray = nullptr;
212
+ if (Params->TryGetArrayField(FieldName, AsArray) && AsArray && AsArray->Num() > 0)
213
+ {
214
+ double Components[4] = { 0.0, 0.0, 0.0, 1.0 };
215
+ for (int32 Index = 0; Index < AsArray->Num() && Index < 4; ++Index)
216
+ {
217
+ (*AsArray)[Index]->TryGetNumber(Components[Index]);
218
+ }
219
+ OutColor = FLinearColor((float)Components[0], (float)Components[1], (float)Components[2], (float)Components[3]);
220
+ return true;
221
+ }
222
+
223
+ FString AsString;
224
+ if (Params->TryGetStringField(FieldName, AsString) && !AsString.IsEmpty())
225
+ {
226
+ TSharedPtr<FJsonObject> Reparsed;
227
+ TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(AsString);
228
+ if (FJsonSerializer::Deserialize(Reader, Reparsed) && TryReadMaterialColorObject(Reparsed, OutColor))
229
+ {
230
+ return true;
231
+ }
232
+ FLinearColor Parsed;
233
+ if (Parsed.InitFromString(AsString))
234
+ {
235
+ OutColor = Parsed;
236
+ return true;
237
+ }
238
+ }
239
+
240
+ return false;
241
+ }
242
+
243
+ // #952: agents reach for `color` as readily as `value` when the parameter is
244
+ // a colour, and rejecting the synonym is indistinguishable from the whole
245
+ // action being unsupported. Try both, and report which one was honoured.
246
+ bool TryParseMaterialColorParam(
247
+ const TSharedPtr<FJsonObject>& Params,
248
+ FLinearColor& OutColor,
249
+ FString& OutSourceField,
250
+ bool bAllowTopLevelComponents = false)
251
+ {
252
+ static const TCHAR* Candidates[] = { TEXT("value"), TEXT("color"), TEXT("colour") };
253
+ for (const TCHAR* Candidate : Candidates)
254
+ {
255
+ if (TryParseMaterialColorField(Params, Candidate, OutColor))
256
+ {
257
+ OutSourceField = Candidate;
258
+ return true;
259
+ }
260
+ }
261
+ // Raw bridge callers reach the handler without the tool schema in front
262
+ // of them and have historically passed the components at the top level.
263
+ if (bAllowTopLevelComponents && TryReadMaterialColorObject(Params, OutColor))
264
+ {
265
+ OutSourceField = TEXT("(top-level components)");
266
+ return true;
267
+ }
268
+ return false;
269
+ }
270
+
165
271
  TSharedPtr<FJsonObject> MaterialInstanceOverrideCounts(UMaterialInstanceConstant* Instance)
166
272
  {
167
273
  TSharedPtr<FJsonObject> Counts = MakeShared<FJsonObject>();
@@ -248,6 +354,170 @@ namespace
248
354
  Result->SetObjectField(TEXT("overrideCounts"), MaterialInstanceOverrideCounts(Instance));
249
355
  Result->SetNumberField(TEXT("overrideCount"), CountTotalMaterialInstanceOverrides(Instance));
250
356
  }
357
+
358
+ // #952: `read` and `list_parameters` used to insist on a base UMaterial, so
359
+ // the asset an agent actually edits - the instance - could be written to and
360
+ // never read back, and verifying a write meant dropping to python. Report
361
+ // what the instance resolves to now, what its parent would give it, and
362
+ // which of the two the instance is actually overriding.
363
+ void AddMaterialInterfaceParameters(TSharedPtr<FJsonObject> Result, UMaterialInterface* Material)
364
+ {
365
+ if (!Result.IsValid() || !Material)
366
+ {
367
+ return;
368
+ }
369
+
370
+ UMaterialInstance* Instance = Cast<UMaterialInstance>(Material);
371
+ UMaterialInterface* Parent = nullptr;
372
+ if (Instance)
373
+ {
374
+ Parent = Instance->Parent;
375
+ }
376
+
377
+ {
378
+ TArray<FMaterialParameterInfo> Infos;
379
+ TArray<FGuid> Guids;
380
+ Material->GetAllScalarParameterInfo(Infos, Guids);
381
+ TArray<TSharedPtr<FJsonValue>> Entries;
382
+ for (int32 Index = 0; Index < Infos.Num(); ++Index)
383
+ {
384
+ const FMaterialParameterInfo& Info = Infos[Index];
385
+ TSharedPtr<FJsonObject> Obj = MaterialParameterInfoToJson(Info);
386
+
387
+ float Value = 0.0f;
388
+ if (Material->GetScalarParameterValue(Info, Value))
389
+ {
390
+ Obj->SetNumberField(TEXT("value"), Value);
391
+ }
392
+ float Default = 0.0f;
393
+ if (Parent && Parent->GetScalarParameterValue(Info, Default))
394
+ {
395
+ Obj->SetNumberField(TEXT("defaultValue"), Default);
396
+ }
397
+
398
+ bool bOverridden = false;
399
+ if (Instance)
400
+ {
401
+ for (const FScalarParameterValue& Override : Instance->ScalarParameterValues)
402
+ {
403
+ if (Override.ParameterInfo == Info) { bOverridden = true; break; }
404
+ }
405
+ }
406
+ Obj->SetBoolField(TEXT("overridden"), bOverridden);
407
+ if (Guids.IsValidIndex(Index))
408
+ {
409
+ Obj->SetStringField(TEXT("expressionGuid"), Guids[Index].ToString(EGuidFormats::DigitsWithHyphens));
410
+ }
411
+ Entries.Add(MakeShared<FJsonValueObject>(Obj));
412
+ }
413
+ Result->SetArrayField(TEXT("scalarParameters"), Entries);
414
+ }
415
+
416
+ {
417
+ TArray<FMaterialParameterInfo> Infos;
418
+ TArray<FGuid> Guids;
419
+ Material->GetAllVectorParameterInfo(Infos, Guids);
420
+ TArray<TSharedPtr<FJsonValue>> Entries;
421
+ for (int32 Index = 0; Index < Infos.Num(); ++Index)
422
+ {
423
+ const FMaterialParameterInfo& Info = Infos[Index];
424
+ TSharedPtr<FJsonObject> Obj = MaterialParameterInfoToJson(Info);
425
+
426
+ FLinearColor Value;
427
+ if (Material->GetVectorParameterValue(Info, Value))
428
+ {
429
+ Obj->SetObjectField(TEXT("value"), LinearColorToJson(Value));
430
+ }
431
+ FLinearColor Default;
432
+ if (Parent && Parent->GetVectorParameterValue(Info, Default))
433
+ {
434
+ Obj->SetObjectField(TEXT("defaultValue"), LinearColorToJson(Default));
435
+ }
436
+
437
+ bool bOverridden = false;
438
+ if (Instance)
439
+ {
440
+ for (const FVectorParameterValue& Override : Instance->VectorParameterValues)
441
+ {
442
+ if (Override.ParameterInfo == Info) { bOverridden = true; break; }
443
+ }
444
+ }
445
+ Obj->SetBoolField(TEXT("overridden"), bOverridden);
446
+ if (Guids.IsValidIndex(Index))
447
+ {
448
+ Obj->SetStringField(TEXT("expressionGuid"), Guids[Index].ToString(EGuidFormats::DigitsWithHyphens));
449
+ }
450
+ Entries.Add(MakeShared<FJsonValueObject>(Obj));
451
+ }
452
+ Result->SetArrayField(TEXT("vectorParameters"), Entries);
453
+ }
454
+
455
+ {
456
+ TArray<FMaterialParameterInfo> Infos;
457
+ TArray<FGuid> Guids;
458
+ Material->GetAllTextureParameterInfo(Infos, Guids);
459
+ TArray<TSharedPtr<FJsonValue>> Entries;
460
+ for (int32 Index = 0; Index < Infos.Num(); ++Index)
461
+ {
462
+ const FMaterialParameterInfo& Info = Infos[Index];
463
+ TSharedPtr<FJsonObject> Obj = MaterialParameterInfoToJson(Info);
464
+
465
+ UTexture* Value = nullptr;
466
+ if (Material->GetTextureParameterValue(Info, Value) && Value)
467
+ {
468
+ Obj->SetStringField(TEXT("value"), Value->GetPathName());
469
+ }
470
+ UTexture* Default = nullptr;
471
+ if (Parent && Parent->GetTextureParameterValue(Info, Default) && Default)
472
+ {
473
+ Obj->SetStringField(TEXT("defaultValue"), Default->GetPathName());
474
+ }
475
+
476
+ bool bOverridden = false;
477
+ if (Instance)
478
+ {
479
+ for (const FTextureParameterValue& Override : Instance->TextureParameterValues)
480
+ {
481
+ if (Override.ParameterInfo == Info) { bOverridden = true; break; }
482
+ }
483
+ }
484
+ Obj->SetBoolField(TEXT("overridden"), bOverridden);
485
+ if (Guids.IsValidIndex(Index))
486
+ {
487
+ Obj->SetStringField(TEXT("expressionGuid"), Guids[Index].ToString(EGuidFormats::DigitsWithHyphens));
488
+ }
489
+ Entries.Add(MakeShared<FJsonValueObject>(Obj));
490
+ }
491
+ Result->SetArrayField(TEXT("textureParameters"), Entries);
492
+ }
493
+ }
494
+
495
+ // Identity + shading summary shared by `read` and `list_parameters` when the
496
+ // asset is not a base UMaterial.
497
+ void SetMaterialInterfaceIdentityFields(TSharedPtr<FJsonObject> Result, UMaterialInterface* Material)
498
+ {
499
+ if (!Result.IsValid() || !Material)
500
+ {
501
+ return;
502
+ }
503
+
504
+ Result->SetStringField(TEXT("name"), Material->GetName());
505
+ Result->SetStringField(TEXT("path"), Material->GetPathName());
506
+ Result->SetStringField(TEXT("assetType"), Material->GetClass()->GetName());
507
+ if (UMaterialInstance* Instance = Cast<UMaterialInstance>(Material))
508
+ {
509
+ Result->SetStringField(TEXT("parentPath"), Instance->Parent ? Instance->Parent->GetPathName() : FString());
510
+ }
511
+ if (UMaterial* Base = Material->GetMaterial())
512
+ {
513
+ Result->SetStringField(TEXT("baseMaterialPath"), Base->GetPathName());
514
+ }
515
+ if (UMaterialInstanceConstant* Constant = Cast<UMaterialInstanceConstant>(Material))
516
+ {
517
+ Result->SetObjectField(TEXT("overrideCounts"), MaterialInstanceOverrideCounts(Constant));
518
+ Result->SetNumberField(TEXT("overrideCount"), CountTotalMaterialInstanceOverrides(Constant));
519
+ }
520
+ }
251
521
  }
252
522
 
253
523
  EMaterialShadingModel FMaterialHandlers::ParseShadingModel(const FString& ShadingModelStr)
@@ -433,12 +703,34 @@ TSharedPtr<FJsonValue> FMaterialHandlers::ReadMaterial(const TSharedPtr<FJsonObj
433
703
  UMaterial* Material = LoadMaterialFromPath(AssetPath);
434
704
  if (!Material)
435
705
  {
436
- return MCPError(FString::Printf(TEXT("Failed to load material at '%s'"), *AssetPath));
706
+ // #952: a MaterialInstance is a material as far as a caller is concerned.
707
+ // It has no expression graph of its own, so answer with what it does
708
+ // have: its lineage, its shading setup, and every parameter it resolves,
709
+ // with the ones it overrides marked.
710
+ UMaterialInterface* Interface = LoadAssetByPath<UMaterialInterface>(AssetPath);
711
+ if (!Interface)
712
+ {
713
+ return MCPError(FString::Printf(TEXT("Failed to load material or material instance at '%s'"), *AssetPath));
714
+ }
715
+
716
+ auto InstanceResult = MCPSuccess();
717
+ SetMaterialInterfaceIdentityFields(InstanceResult, Interface);
718
+ InstanceResult->SetStringField(TEXT("shadingModel"), ShadingModelToString(Interface->GetShadingModels().GetFirstShadingModel()));
719
+ InstanceResult->SetStringField(TEXT("blendMode"), StaticEnum<EBlendMode>()->GetNameStringByValue((int64)Interface->GetBlendMode()));
720
+ InstanceResult->SetBoolField(TEXT("twoSided"), Interface->IsTwoSided());
721
+ AddMaterialInterfaceParameters(InstanceResult, Interface);
722
+ InstanceResult->SetArrayField(TEXT("staticSwitches"), MaterialStaticSwitchesToJson(Interface));
723
+ // The graph lives on the base material; say so rather than returning an
724
+ // empty expression list that reads like a material with no nodes.
725
+ InstanceResult->SetStringField(TEXT("expressionsNote"),
726
+ TEXT("A MaterialInstance has no expression graph. Read baseMaterialPath for the graph."));
727
+ return MCPResult(InstanceResult);
437
728
  }
438
729
 
439
730
  auto Result = MCPSuccess();
440
731
  Result->SetStringField(TEXT("name"), Material->GetName());
441
732
  Result->SetStringField(TEXT("path"), Material->GetPathName());
733
+ Result->SetStringField(TEXT("assetType"), Material->GetClass()->GetName());
442
734
  Result->SetStringField(TEXT("shadingModel"), ShadingModelToString(Material->GetShadingModels().GetFirstShadingModel()));
443
735
  Result->SetStringField(TEXT("blendMode"), StaticEnum<EBlendMode>()->GetNameStringByValue((int64)Material->BlendMode));
444
736
  Result->SetBoolField(TEXT("twoSided"), Material->IsTwoSided());
@@ -1161,7 +1453,20 @@ TSharedPtr<FJsonValue> FMaterialHandlers::ListMaterialParameters(const TSharedPt
1161
1453
  UMaterial* Material = LoadMaterialFromPath(AssetPath);
1162
1454
  if (!Material)
1163
1455
  {
1164
- return MCPError(FString::Printf(TEXT("Failed to load material at '%s'"), *AssetPath));
1456
+ // #952: on a MaterialInstance report the resolved value of every
1457
+ // parameter, the parent's value, and which ones this instance overrides,
1458
+ // so a write can be verified without an execute_python round trip.
1459
+ UMaterialInterface* Interface = LoadAssetByPath<UMaterialInterface>(AssetPath);
1460
+ if (!Interface)
1461
+ {
1462
+ return MCPError(FString::Printf(TEXT("Failed to load material or material instance at '%s'"), *AssetPath));
1463
+ }
1464
+
1465
+ auto InstanceResult = MCPSuccess();
1466
+ SetMaterialInterfaceIdentityFields(InstanceResult, Interface);
1467
+ AddMaterialInterfaceParameters(InstanceResult, Interface);
1468
+ InstanceResult->SetArrayField(TEXT("staticSwitches"), MaterialStaticSwitchesToJson(Interface));
1469
+ return MCPResult(InstanceResult);
1165
1470
  }
1166
1471
 
1167
1472
  TArray<TSharedPtr<FJsonValue>> ScalarParams;
@@ -1210,6 +1515,7 @@ TSharedPtr<FJsonValue> FMaterialHandlers::ListMaterialParameters(const TSharedPt
1210
1515
  Result->SetArrayField(TEXT("vectorParameters"), VectorParams);
1211
1516
  Result->SetArrayField(TEXT("textureParameters"), TextureParams);
1212
1517
  Result->SetStringField(TEXT("path"), Material->GetPathName());
1518
+ Result->SetStringField(TEXT("assetType"), Material->GetClass()->GetName());
1213
1519
 
1214
1520
  return MCPResult(Result);
1215
1521
  }
@@ -1368,22 +1674,39 @@ TSharedPtr<FJsonValue> FMaterialHandlers::SetMaterialParameter(const TSharedPtr<
1368
1674
  }
1369
1675
  }
1370
1676
 
1677
+ // The association decides which parameter a name refers to once material
1678
+ // layers are in play, and it has to be identical on the write and on the
1679
+ // read-back or the verify step reads a different parameter than it wrote.
1680
+ const EMaterialParameterAssociation Association = ParseMaterialParameterAssociation(
1681
+ OptionalString(Params, TEXT("association"), TEXT("Global")));
1682
+ const FMaterialParameterInfo ParameterInfo(FName(*ParameterName), Association);
1683
+ const FString AssociationName = MaterialParameterAssociationToString(Association);
1684
+
1371
1685
  // Auto-detect parameter type if not provided
1372
1686
  if (ParameterType.IsEmpty())
1373
1687
  {
1374
1688
  // Check which parameter collections contain this name
1375
- FName ParamFName(*ParameterName);
1376
1689
  float ScalarVal;
1377
1690
  FLinearColor VectorVal;
1378
1691
  UTexture* TextureVal;
1379
- if (MaterialInstance->GetScalarParameterValue(ParamFName, ScalarVal))
1692
+ if (MaterialInstance->GetScalarParameterValue(ParameterInfo, ScalarVal))
1380
1693
  ParameterType = TEXT("scalar");
1381
- else if (MaterialInstance->GetVectorParameterValue(ParamFName, VectorVal))
1694
+ else if (MaterialInstance->GetVectorParameterValue(ParameterInfo, VectorVal))
1382
1695
  ParameterType = TEXT("vector");
1383
- else if (MaterialInstance->GetTextureParameterValue(ParamFName, TextureVal))
1696
+ else if (MaterialInstance->GetTextureParameterValue(ParameterInfo, TextureVal))
1384
1697
  ParameterType = TEXT("texture");
1385
1698
  else
1386
- ParameterType = TEXT("scalar"); // default fallback
1699
+ {
1700
+ // The parent does not declare the name, so nothing can be looked
1701
+ // up. Fall back on the shape of the payload: a colour is only ever
1702
+ // meant for a vector parameter, and guessing scalar there produced
1703
+ // the "missing value" rejection reported in #952.
1704
+ FLinearColor Probe;
1705
+ FString ProbeField;
1706
+ ParameterType = TryParseMaterialColorParam(Params, Probe, ProbeField)
1707
+ ? TEXT("vector")
1708
+ : TEXT("scalar");
1709
+ }
1387
1710
  }
1388
1711
 
1389
1712
  FString TypeLower = ParameterType.ToLower();
@@ -1397,11 +1720,12 @@ TSharedPtr<FJsonValue> FMaterialHandlers::SetMaterialParameter(const TSharedPtr<
1397
1720
  }
1398
1721
 
1399
1722
  float PrevScalar = 0.0f;
1400
- const bool bHadPrev = MaterialInstance->GetScalarParameterValue(FName(*ParameterName), PrevScalar);
1723
+ const bool bHadPrev = MaterialInstance->GetScalarParameterValue(ParameterInfo, PrevScalar);
1401
1724
 
1402
1725
  auto Result = MCPSuccess();
1403
1726
  Result->SetStringField(TEXT("parameterName"), ParameterName);
1404
1727
  Result->SetStringField(TEXT("parameterType"), TEXT("scalar"));
1728
+ Result->SetStringField(TEXT("association"), AssociationName);
1405
1729
  Result->SetNumberField(TEXT("value"), ScalarValue);
1406
1730
  Result->SetStringField(TEXT("path"), MaterialInstance->GetPathName());
1407
1731
 
@@ -1409,11 +1733,31 @@ TSharedPtr<FJsonValue> FMaterialHandlers::SetMaterialParameter(const TSharedPtr<
1409
1733
  {
1410
1734
  MCPSetExisted(Result);
1411
1735
  Result->SetBoolField(TEXT("updated"), false);
1736
+ Result->SetNumberField(TEXT("readBack"), PrevScalar);
1412
1737
  return MCPResult(Result);
1413
1738
  }
1414
1739
 
1415
- MaterialInstance->SetScalarParameterValueEditorOnly(FName(*ParameterName), static_cast<float>(ScalarValue));
1416
- MaterialInstance->MarkPackageDirty();
1740
+ MaterialInstance->Modify(true);
1741
+ const bool bApplied = UMaterialEditingLibrary::SetMaterialInstanceScalarParameterValue(
1742
+ MaterialInstance, FName(*ParameterName), static_cast<float>(ScalarValue), Association);
1743
+ if (!bApplied)
1744
+ {
1745
+ // The library declines a name the parent does not declare. Writing
1746
+ // the override anyway is what this handler has always done, and a
1747
+ // parameter added to the parent later then picks it up.
1748
+ MaterialInstance->SetScalarParameterValueEditorOnly(ParameterInfo, static_cast<float>(ScalarValue));
1749
+ UMaterialEditingLibrary::UpdateMaterialInstance(MaterialInstance);
1750
+ }
1751
+ Result->SetBoolField(TEXT("declaredByParent"), bApplied);
1752
+
1753
+ // Persist, then read the value straight back off the instance so the
1754
+ // caller never has to take "success" on trust (#952).
1755
+ Result->SetBoolField(TEXT("saved"), SaveAssetPackage(MaterialInstance));
1756
+ float ReadBack = 0.0f;
1757
+ if (MaterialInstance->GetScalarParameterValue(ParameterInfo, ReadBack))
1758
+ {
1759
+ Result->SetNumberField(TEXT("readBack"), ReadBack);
1760
+ }
1417
1761
 
1418
1762
  MCPSetUpdated(Result);
1419
1763
  if (bHadPrev)
@@ -1428,97 +1772,59 @@ TSharedPtr<FJsonValue> FMaterialHandlers::SetMaterialParameter(const TSharedPtr<
1428
1772
 
1429
1773
  return MCPResult(Result);
1430
1774
  }
1431
- else if (TypeLower == TEXT("vector"))
1775
+ else if (TypeLower == TEXT("vector") || TypeLower == TEXT("color") || TypeLower == TEXT("colour"))
1432
1776
  {
1433
- // #670: accept the vector payload as an object {r,g,b,a}, an array
1434
- // [r,g,b,a], or a double-encoded string ("{\"r\":..}" or "(R=..,G=..)").
1435
- double R = 0.0, G = 0.0, B = 0.0, A = 1.0;
1436
- bool bParsed = false;
1437
- const TSharedPtr<FJsonObject>* ValueObj = nullptr;
1438
- const TArray<TSharedPtr<FJsonValue>>* ValueArr = nullptr;
1439
- if (Params->TryGetObjectField(TEXT("value"), ValueObj) && ValueObj && (*ValueObj).IsValid())
1440
- {
1441
- (*ValueObj)->TryGetNumberField(TEXT("r"), R);
1442
- (*ValueObj)->TryGetNumberField(TEXT("g"), G);
1443
- (*ValueObj)->TryGetNumberField(TEXT("b"), B);
1444
- (*ValueObj)->TryGetNumberField(TEXT("a"), A);
1445
- bParsed = true;
1446
- }
1447
- else if (Params->TryGetArrayField(TEXT("value"), ValueArr) && ValueArr)
1777
+ // #670/#952: the payload may be an object {r,g,b,a}, an array [r,g,b,a],
1778
+ // a re-encoded JSON string or UE struct text, and it may arrive under
1779
+ // `value` or under `color`. Every one of those is a colour a caller
1780
+ // meant to set, so accept them all rather than making the caller guess.
1781
+ FLinearColor ColorValue = FLinearColor::White;
1782
+ FString SourceField;
1783
+ if (!TryParseMaterialColorParam(Params, ColorValue, SourceField))
1448
1784
  {
1449
- if (ValueArr->Num() > 0) (*ValueArr)[0]->TryGetNumber(R);
1450
- if (ValueArr->Num() > 1) (*ValueArr)[1]->TryGetNumber(G);
1451
- if (ValueArr->Num() > 2) (*ValueArr)[2]->TryGetNumber(B);
1452
- if (ValueArr->Num() > 3) (*ValueArr)[3]->TryGetNumber(A);
1453
- bParsed = true;
1454
- }
1455
- else
1456
- {
1457
- FString ValueStr;
1458
- if (Params->TryGetStringField(TEXT("value"), ValueStr) && !ValueStr.IsEmpty())
1459
- {
1460
- // Try JSON object first.
1461
- TSharedPtr<FJsonObject> Reparsed;
1462
- TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(ValueStr);
1463
- if (FJsonSerializer::Deserialize(Reader, Reparsed) && Reparsed.IsValid())
1464
- {
1465
- Reparsed->TryGetNumberField(TEXT("r"), R);
1466
- Reparsed->TryGetNumberField(TEXT("g"), G);
1467
- Reparsed->TryGetNumberField(TEXT("b"), B);
1468
- Reparsed->TryGetNumberField(TEXT("a"), A);
1469
- bParsed = true;
1470
- }
1471
- else
1472
- {
1473
- // Fall back to UE struct text "(R=..,G=..,B=..,A=..)".
1474
- FLinearColor Parsed;
1475
- if (Parsed.InitFromString(ValueStr))
1476
- {
1477
- R = Parsed.R; G = Parsed.G; B = Parsed.B; A = Parsed.A;
1478
- bParsed = true;
1479
- }
1480
- }
1481
- }
1482
- }
1483
- if (!bParsed)
1484
- {
1485
- return MCPError(TEXT("Missing/unparseable 'value' for vector parameter - pass {r,g,b,a}, [r,g,b,a], or '(R=..,G=..,B=..,A=..)'"));
1785
+ return MCPError(TEXT("Missing/unparseable colour for vector parameter - pass it as 'value' or 'color', shaped {r,g,b,a}, [r,g,b,a], or '(R=..,G=..,B=..,A=..)'"));
1486
1786
  }
1487
1787
 
1488
- FLinearColor ColorValue(R, G, B, A);
1489
1788
  FLinearColor PrevColor;
1490
- const bool bHadPrev = MaterialInstance->GetVectorParameterValue(FName(*ParameterName), PrevColor);
1491
-
1492
- TSharedPtr<FJsonObject> ValueResult = MakeShared<FJsonObject>();
1493
- ValueResult->SetNumberField(TEXT("r"), R);
1494
- ValueResult->SetNumberField(TEXT("g"), G);
1495
- ValueResult->SetNumberField(TEXT("b"), B);
1496
- ValueResult->SetNumberField(TEXT("a"), A);
1789
+ const bool bHadPrev = MaterialInstance->GetVectorParameterValue(ParameterInfo, PrevColor);
1497
1790
 
1498
1791
  auto Result = MCPSuccess();
1499
1792
  Result->SetStringField(TEXT("parameterName"), ParameterName);
1500
1793
  Result->SetStringField(TEXT("parameterType"), TEXT("vector"));
1501
- Result->SetObjectField(TEXT("value"), ValueResult);
1794
+ Result->SetStringField(TEXT("association"), AssociationName);
1795
+ Result->SetStringField(TEXT("valueField"), SourceField);
1796
+ Result->SetObjectField(TEXT("value"), LinearColorToJson(ColorValue));
1502
1797
  Result->SetStringField(TEXT("path"), MaterialInstance->GetPathName());
1503
1798
 
1504
1799
  if (bHadPrev && PrevColor.Equals(ColorValue))
1505
1800
  {
1506
1801
  MCPSetExisted(Result);
1507
1802
  Result->SetBoolField(TEXT("updated"), false);
1803
+ Result->SetObjectField(TEXT("readBack"), LinearColorToJson(PrevColor));
1508
1804
  return MCPResult(Result);
1509
1805
  }
1510
1806
 
1511
- MaterialInstance->SetVectorParameterValueEditorOnly(FName(*ParameterName), ColorValue);
1512
- MaterialInstance->MarkPackageDirty();
1807
+ MaterialInstance->Modify(true);
1808
+ const bool bApplied = UMaterialEditingLibrary::SetMaterialInstanceVectorParameterValue(
1809
+ MaterialInstance, FName(*ParameterName), ColorValue, Association);
1810
+ if (!bApplied)
1811
+ {
1812
+ MaterialInstance->SetVectorParameterValueEditorOnly(ParameterInfo, ColorValue);
1813
+ UMaterialEditingLibrary::UpdateMaterialInstance(MaterialInstance);
1814
+ }
1815
+ Result->SetBoolField(TEXT("declaredByParent"), bApplied);
1816
+
1817
+ Result->SetBoolField(TEXT("saved"), SaveAssetPackage(MaterialInstance));
1818
+ FLinearColor ReadBack;
1819
+ if (MaterialInstance->GetVectorParameterValue(ParameterInfo, ReadBack))
1820
+ {
1821
+ Result->SetObjectField(TEXT("readBack"), LinearColorToJson(ReadBack));
1822
+ }
1513
1823
 
1514
1824
  MCPSetUpdated(Result);
1515
1825
  if (bHadPrev)
1516
1826
  {
1517
- TSharedPtr<FJsonObject> PrevValueObj = MakeShared<FJsonObject>();
1518
- PrevValueObj->SetNumberField(TEXT("r"), PrevColor.R);
1519
- PrevValueObj->SetNumberField(TEXT("g"), PrevColor.G);
1520
- PrevValueObj->SetNumberField(TEXT("b"), PrevColor.B);
1521
- PrevValueObj->SetNumberField(TEXT("a"), PrevColor.A);
1827
+ TSharedPtr<FJsonObject> PrevValueObj = LinearColorToJson(PrevColor);
1522
1828
  TSharedPtr<FJsonObject> Payload = MakeShared<FJsonObject>();
1523
1829
  Payload->SetStringField(TEXT("path"), MaterialInstance->GetPathName());
1524
1830
  Payload->SetStringField(TEXT("parameterName"), ParameterName);
@@ -1533,6 +1839,10 @@ TSharedPtr<FJsonValue> FMaterialHandlers::SetMaterialParameter(const TSharedPtr<
1533
1839
  {
1534
1840
  FString TexturePath;
1535
1841
  if (!Params->TryGetStringField(TEXT("value"), TexturePath) || TexturePath.IsEmpty())
1842
+ {
1843
+ Params->TryGetStringField(TEXT("texturePath"), TexturePath);
1844
+ }
1845
+ if (TexturePath.IsEmpty())
1536
1846
  {
1537
1847
  return MCPError(TEXT("Missing 'value' string field (texture asset path) for texture parameter"));
1538
1848
  }
@@ -1549,11 +1859,12 @@ TSharedPtr<FJsonValue> FMaterialHandlers::SetMaterialParameter(const TSharedPtr<
1549
1859
  }
1550
1860
 
1551
1861
  UTexture* PrevTexture = nullptr;
1552
- const bool bHadPrev = MaterialInstance->GetTextureParameterValue(FName(*ParameterName), PrevTexture);
1862
+ const bool bHadPrev = MaterialInstance->GetTextureParameterValue(ParameterInfo, PrevTexture);
1553
1863
 
1554
1864
  auto Result = MCPSuccess();
1555
1865
  Result->SetStringField(TEXT("parameterName"), ParameterName);
1556
1866
  Result->SetStringField(TEXT("parameterType"), TEXT("texture"));
1867
+ Result->SetStringField(TEXT("association"), AssociationName);
1557
1868
  Result->SetStringField(TEXT("value"), Texture->GetPathName());
1558
1869
  Result->SetStringField(TEXT("path"), MaterialInstance->GetPathName());
1559
1870
 
@@ -1561,11 +1872,26 @@ TSharedPtr<FJsonValue> FMaterialHandlers::SetMaterialParameter(const TSharedPtr<
1561
1872
  {
1562
1873
  MCPSetExisted(Result);
1563
1874
  Result->SetBoolField(TEXT("updated"), false);
1875
+ Result->SetStringField(TEXT("readBack"), PrevTexture->GetPathName());
1564
1876
  return MCPResult(Result);
1565
1877
  }
1566
1878
 
1567
- MaterialInstance->SetTextureParameterValueEditorOnly(FName(*ParameterName), Texture);
1568
- MaterialInstance->MarkPackageDirty();
1879
+ MaterialInstance->Modify(true);
1880
+ const bool bApplied = UMaterialEditingLibrary::SetMaterialInstanceTextureParameterValue(
1881
+ MaterialInstance, FName(*ParameterName), Texture, Association);
1882
+ if (!bApplied)
1883
+ {
1884
+ MaterialInstance->SetTextureParameterValueEditorOnly(ParameterInfo, Texture);
1885
+ UMaterialEditingLibrary::UpdateMaterialInstance(MaterialInstance);
1886
+ }
1887
+ Result->SetBoolField(TEXT("declaredByParent"), bApplied);
1888
+
1889
+ Result->SetBoolField(TEXT("saved"), SaveAssetPackage(MaterialInstance));
1890
+ UTexture* ReadBack = nullptr;
1891
+ if (MaterialInstance->GetTextureParameterValue(ParameterInfo, ReadBack) && ReadBack)
1892
+ {
1893
+ Result->SetStringField(TEXT("readBack"), ReadBack->GetPathName());
1894
+ }
1569
1895
 
1570
1896
  MCPSetUpdated(Result);
1571
1897
  if (bHadPrev && PrevTexture)
@@ -1582,7 +1908,7 @@ TSharedPtr<FJsonValue> FMaterialHandlers::SetMaterialParameter(const TSharedPtr<
1582
1908
  }
1583
1909
  else
1584
1910
  {
1585
- return MCPError(FString::Printf(TEXT("Unknown parameterType '%s'. Use 'scalar', 'vector', or 'texture'."), *ParameterType));
1911
+ return MCPError(FString::Printf(TEXT("Unknown parameterType '%s'. Use 'scalar', 'vector' (alias 'color'), or 'texture'."), *ParameterType));
1586
1912
  }
1587
1913
  }
1588
1914
 
@@ -1745,15 +2071,15 @@ TSharedPtr<FJsonValue> FMaterialHandlers::BatchSetInstances(const TSharedPtr<FJs
1745
2071
  double V = 0; (*PObj)->TryGetNumberField(TEXT("value"), V);
1746
2072
  MIC->SetScalarParameterValueEditorOnly(FName(*PName), (float)V); ++ParamsSet;
1747
2073
  }
1748
- else if (PType == TEXT("vector") || PType == TEXT("color"))
2074
+ else if (PType == TEXT("vector") || PType == TEXT("color") || PType == TEXT("colour"))
1749
2075
  {
1750
- const TSharedPtr<FJsonObject>* CObj = nullptr;
1751
- if ((*PObj)->TryGetObjectField(TEXT("value"), CObj) && *CObj)
2076
+ // Same colour shapes the single-instance path accepts (#952),
2077
+ // so a batch is never fussier about the payload than one call.
2078
+ FLinearColor Color;
2079
+ FString SourceField;
2080
+ if (TryParseMaterialColorParam(*PObj, Color, SourceField))
1752
2081
  {
1753
- double R=0,G=0,B=0,A=1;
1754
- (*CObj)->TryGetNumberField(TEXT("r"), R); (*CObj)->TryGetNumberField(TEXT("g"), G);
1755
- (*CObj)->TryGetNumberField(TEXT("b"), B); (*CObj)->TryGetNumberField(TEXT("a"), A);
1756
- MIC->SetVectorParameterValueEditorOnly(FName(*PName), FLinearColor((float)R,(float)G,(float)B,(float)A)); ++ParamsSet;
2082
+ MIC->SetVectorParameterValueEditorOnly(FName(*PName), Color); ++ParamsSet;
1757
2083
  }
1758
2084
  }
1759
2085
  else if (PType == TEXT("texture"))
@@ -1958,12 +2284,22 @@ TSharedPtr<FJsonValue> FMaterialHandlers::SetExpressionValue(const TSharedPtr<FJ
1958
2284
  // Handle UMaterialExpressionConstant2Vector
1959
2285
  else if (UMaterialExpressionConstant2Vector* Const2Expr = Cast<UMaterialExpressionConstant2Vector>(Expression))
1960
2286
  {
1961
- double R = 0.0, G = 0.0;
1962
- if (Params->TryGetNumberField(TEXT("r"), R)) { Const2Expr->R = static_cast<float>(R); bValueSet = true; }
1963
- if (Params->TryGetNumberField(TEXT("g"), G)) { Const2Expr->G = static_cast<float>(G); bValueSet = true; }
1964
-
1965
- if (bValueSet)
2287
+ // #979: this branch used to read only top-level lowercase `r`/`g`, which
2288
+ // the tool schema does not declare, so a Constant2Vector had no reachable
2289
+ // write path at all while its 3- and 4-component siblings took a `value`
2290
+ // object. All three now share one reader, and `{x,y}` works as documented.
2291
+ FLinearColor Components;
2292
+ FString SourceField;
2293
+ if (TryParseMaterialColorParam(Params, Components, SourceField, /*bAllowTopLevelComponents*/ true))
1966
2294
  {
2295
+ Const2Expr->R = Components.R;
2296
+ Const2Expr->G = Components.G;
2297
+ bValueSet = true;
2298
+
2299
+ TSharedPtr<FJsonObject> ValueResult = MakeShared<FJsonObject>();
2300
+ ValueResult->SetNumberField(TEXT("r"), Const2Expr->R);
2301
+ ValueResult->SetNumberField(TEXT("g"), Const2Expr->G);
2302
+ Result->SetObjectField(TEXT("value"), ValueResult);
1967
2303
  Result->SetNumberField(TEXT("r"), Const2Expr->R);
1968
2304
  Result->SetNumberField(TEXT("g"), Const2Expr->G);
1969
2305
  }
@@ -1971,81 +2307,27 @@ TSharedPtr<FJsonValue> FMaterialHandlers::SetExpressionValue(const TSharedPtr<FJ
1971
2307
  // Handle UMaterialExpressionConstant3Vector - has FLinearColor Constant
1972
2308
  else if (UMaterialExpressionConstant3Vector* Const3Expr = Cast<UMaterialExpressionConstant3Vector>(Expression))
1973
2309
  {
1974
- // #444: accept {r,g,b,a} or {R,G,B,A} or {x,y,z,w} in value object,
1975
- // or top-level r/g/b/a/x/y/z/w fields.
1976
- auto ReadColor = [](const TSharedPtr<FJsonObject>& Obj, FLinearColor& Out) -> bool
1977
- {
1978
- double R = 0, G = 0, B = 0, A = 1; bool bAny = false;
1979
- auto Pick = [&](const TCHAR* L, const TCHAR* U, const TCHAR* Alt, double& Slot)
1980
- {
1981
- double V; if (Obj->TryGetNumberField(L, V) || Obj->TryGetNumberField(U, V) || Obj->TryGetNumberField(Alt, V)) { Slot = V; bAny = true; }
1982
- };
1983
- Pick(TEXT("r"), TEXT("R"), TEXT("x"), R);
1984
- Pick(TEXT("g"), TEXT("G"), TEXT("y"), G);
1985
- Pick(TEXT("b"), TEXT("B"), TEXT("z"), B);
1986
- Pick(TEXT("a"), TEXT("A"), TEXT("w"), A);
1987
- Out = FLinearColor((float)R, (float)G, (float)B, (float)A);
1988
- return bAny;
1989
- };
1990
- const TSharedPtr<FJsonObject>* ColorObj = nullptr;
1991
- FLinearColor Color = Const3Expr->Constant;
1992
- if (Params->TryGetObjectField(TEXT("value"), ColorObj) && *ColorObj && ReadColor(*ColorObj, Color))
1993
- {
1994
- Const3Expr->Constant = Color;
1995
- bValueSet = true;
1996
- }
1997
- else if (ReadColor(Params, Color))
2310
+ // #444/#979: accept {r,g,b,a}, {R,G,B,A} or {x,y,z,w} under `value` or
2311
+ // `color`, an array, UE struct text, or the components at the top level.
2312
+ FLinearColor Color;
2313
+ FString SourceField;
2314
+ if (TryParseMaterialColorParam(Params, Color, SourceField, /*bAllowTopLevelComponents*/ true))
1998
2315
  {
1999
2316
  Const3Expr->Constant = Color;
2000
2317
  bValueSet = true;
2001
- }
2002
- if (bValueSet)
2003
- {
2004
- TSharedPtr<FJsonObject> ColorResult = MakeShared<FJsonObject>();
2005
- ColorResult->SetNumberField(TEXT("r"), Const3Expr->Constant.R);
2006
- ColorResult->SetNumberField(TEXT("g"), Const3Expr->Constant.G);
2007
- ColorResult->SetNumberField(TEXT("b"), Const3Expr->Constant.B);
2008
- ColorResult->SetNumberField(TEXT("a"), Const3Expr->Constant.A);
2009
- Result->SetObjectField(TEXT("value"), ColorResult);
2318
+ Result->SetObjectField(TEXT("value"), LinearColorToJson(Const3Expr->Constant));
2010
2319
  }
2011
2320
  }
2012
2321
  // Handle UMaterialExpressionConstant4Vector
2013
2322
  else if (UMaterialExpressionConstant4Vector* Const4Expr = Cast<UMaterialExpressionConstant4Vector>(Expression))
2014
2323
  {
2015
- auto ReadColor4 = [](const TSharedPtr<FJsonObject>& Obj, FLinearColor& Out) -> bool
2016
- {
2017
- double R = 0, G = 0, B = 0, A = 1; bool bAny = false;
2018
- auto Pick = [&](const TCHAR* L, const TCHAR* U, const TCHAR* Alt, double& Slot)
2019
- {
2020
- double V; if (Obj->TryGetNumberField(L, V) || Obj->TryGetNumberField(U, V) || Obj->TryGetNumberField(Alt, V)) { Slot = V; bAny = true; }
2021
- };
2022
- Pick(TEXT("r"), TEXT("R"), TEXT("x"), R);
2023
- Pick(TEXT("g"), TEXT("G"), TEXT("y"), G);
2024
- Pick(TEXT("b"), TEXT("B"), TEXT("z"), B);
2025
- Pick(TEXT("a"), TEXT("A"), TEXT("w"), A);
2026
- Out = FLinearColor((float)R, (float)G, (float)B, (float)A);
2027
- return bAny;
2028
- };
2029
- const TSharedPtr<FJsonObject>* ColorObj = nullptr;
2030
- FLinearColor Color = Const4Expr->Constant;
2031
- if (Params->TryGetObjectField(TEXT("value"), ColorObj) && *ColorObj && ReadColor4(*ColorObj, Color))
2032
- {
2033
- Const4Expr->Constant = Color;
2034
- bValueSet = true;
2035
- }
2036
- else if (ReadColor4(Params, Color))
2324
+ FLinearColor Color;
2325
+ FString SourceField;
2326
+ if (TryParseMaterialColorParam(Params, Color, SourceField, /*bAllowTopLevelComponents*/ true))
2037
2327
  {
2038
2328
  Const4Expr->Constant = Color;
2039
2329
  bValueSet = true;
2040
- }
2041
- if (bValueSet)
2042
- {
2043
- TSharedPtr<FJsonObject> ColorResult = MakeShared<FJsonObject>();
2044
- ColorResult->SetNumberField(TEXT("r"), Const4Expr->Constant.R);
2045
- ColorResult->SetNumberField(TEXT("g"), Const4Expr->Constant.G);
2046
- ColorResult->SetNumberField(TEXT("b"), Const4Expr->Constant.B);
2047
- ColorResult->SetNumberField(TEXT("a"), Const4Expr->Constant.A);
2048
- Result->SetObjectField(TEXT("value"), ColorResult);
2330
+ Result->SetObjectField(TEXT("value"), LinearColorToJson(Const4Expr->Constant));
2049
2331
  }
2050
2332
  }
2051
2333
  // Handle UMaterialExpressionScalarParameter - has float DefaultValue
@@ -2070,23 +2352,13 @@ TSharedPtr<FJsonValue> FMaterialHandlers::SetExpressionValue(const TSharedPtr<FJ
2070
2352
  // Handle UMaterialExpressionVectorParameter - has FLinearColor DefaultValue
2071
2353
  else if (UMaterialExpressionVectorParameter* VectorParamExpr = Cast<UMaterialExpressionVectorParameter>(Expression))
2072
2354
  {
2073
- const TSharedPtr<FJsonObject>* ValueObj = nullptr;
2074
- if (Params->TryGetObjectField(TEXT("value"), ValueObj))
2355
+ FLinearColor Color;
2356
+ FString SourceField;
2357
+ if (TryParseMaterialColorParam(Params, Color, SourceField, /*bAllowTopLevelComponents*/ false))
2075
2358
  {
2076
- double R = 0.0, G = 0.0, B = 0.0, A = 1.0;
2077
- (*ValueObj)->TryGetNumberField(TEXT("r"), R);
2078
- (*ValueObj)->TryGetNumberField(TEXT("g"), G);
2079
- (*ValueObj)->TryGetNumberField(TEXT("b"), B);
2080
- (*ValueObj)->TryGetNumberField(TEXT("a"), A);
2081
- VectorParamExpr->DefaultValue = FLinearColor(R, G, B, A);
2359
+ VectorParamExpr->DefaultValue = Color;
2082
2360
  bValueSet = true;
2083
-
2084
- TSharedPtr<FJsonObject> ColorResult = MakeShared<FJsonObject>();
2085
- ColorResult->SetNumberField(TEXT("r"), R);
2086
- ColorResult->SetNumberField(TEXT("g"), G);
2087
- ColorResult->SetNumberField(TEXT("b"), B);
2088
- ColorResult->SetNumberField(TEXT("a"), A);
2089
- Result->SetObjectField(TEXT("value"), ColorResult);
2361
+ Result->SetObjectField(TEXT("value"), LinearColorToJson(VectorParamExpr->DefaultValue));
2090
2362
  }
2091
2363
 
2092
2364
  FString ParamName;
@@ -2244,9 +2516,14 @@ TSharedPtr<FJsonValue> FMaterialHandlers::SetExpressionValue(const TSharedPtr<FJ
2244
2516
  }
2245
2517
 
2246
2518
  Material->PostEditChange();
2247
- Material->MarkPackageDirty();
2248
2519
 
2520
+ // #979: the value was written in memory and the package only marked dirty,
2521
+ // so a caller who then asked something else to save it could be told the
2522
+ // save failed while the write had in fact landed - two answers, neither of
2523
+ // them the whole truth. Persist here, and report whether that worked
2524
+ // alongside the value that was written either way.
2249
2525
  MCPSetUpdated(Result);
2526
+ Result->SetBoolField(TEXT("saved"), SaveAssetPackage(Material));
2250
2527
  Result->SetStringField(TEXT("materialPath"), Material->GetPathName());
2251
2528
  Result->SetNumberField(TEXT("expressionIndex"), ExpressionIndex);
2252
2529
  Result->SetStringField(TEXT("expressionClass"), ExpressionClass);