unreal-engine-mcp-server 0.4.7 → 0.5.1

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 (454) hide show
  1. package/.env.example +26 -0
  2. package/.env.production +38 -7
  3. package/.eslintrc.json +0 -54
  4. package/.eslintrc.override.json +8 -0
  5. package/.github/ISSUE_TEMPLATE/bug_report.yml +94 -0
  6. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  7. package/.github/ISSUE_TEMPLATE/feature_request.yml +56 -0
  8. package/.github/copilot-instructions.md +478 -45
  9. package/.github/dependabot.yml +19 -0
  10. package/.github/labeler.yml +24 -0
  11. package/.github/labels.yml +70 -0
  12. package/.github/pull_request_template.md +42 -0
  13. package/.github/release-drafter-config.yml +51 -0
  14. package/.github/workflows/auto-merge.yml +38 -0
  15. package/.github/workflows/ci.yml +38 -0
  16. package/.github/workflows/dependency-review.yml +17 -0
  17. package/.github/workflows/gemini-issue-triage.yml +172 -0
  18. package/.github/workflows/greetings.yml +27 -0
  19. package/.github/workflows/labeler.yml +17 -0
  20. package/.github/workflows/links.yml +80 -0
  21. package/.github/workflows/pr-size-labeler.yml +137 -0
  22. package/.github/workflows/publish-mcp.yml +13 -7
  23. package/.github/workflows/release-drafter.yml +23 -0
  24. package/.github/workflows/release.yml +112 -0
  25. package/.github/workflows/semantic-pull-request.yml +35 -0
  26. package/.github/workflows/smoke-test.yml +36 -0
  27. package/.github/workflows/stale.yml +28 -0
  28. package/CHANGELOG.md +338 -31
  29. package/CONTRIBUTING.md +140 -0
  30. package/GEMINI.md +115 -0
  31. package/Public/Plugin_setup_guide.mp4 +0 -0
  32. package/README.md +189 -128
  33. package/claude_desktop_config_example.json +7 -6
  34. package/dist/automation/bridge.d.ts +50 -0
  35. package/dist/automation/bridge.js +452 -0
  36. package/dist/automation/connection-manager.d.ts +23 -0
  37. package/dist/automation/connection-manager.js +107 -0
  38. package/dist/automation/handshake.d.ts +11 -0
  39. package/dist/automation/handshake.js +89 -0
  40. package/dist/automation/index.d.ts +3 -0
  41. package/dist/automation/index.js +3 -0
  42. package/dist/automation/message-handler.d.ts +12 -0
  43. package/dist/automation/message-handler.js +149 -0
  44. package/dist/automation/request-tracker.d.ts +25 -0
  45. package/dist/automation/request-tracker.js +98 -0
  46. package/dist/automation/types.d.ts +130 -0
  47. package/dist/automation/types.js +2 -0
  48. package/dist/cli.js +32 -5
  49. package/dist/config.d.ts +26 -0
  50. package/dist/config.js +59 -0
  51. package/dist/constants.d.ts +16 -0
  52. package/dist/constants.js +16 -0
  53. package/dist/graphql/loaders.d.ts +64 -0
  54. package/dist/graphql/loaders.js +117 -0
  55. package/dist/graphql/resolvers.d.ts +268 -0
  56. package/dist/graphql/resolvers.js +746 -0
  57. package/dist/graphql/schema.d.ts +5 -0
  58. package/dist/graphql/schema.js +437 -0
  59. package/dist/graphql/server.d.ts +26 -0
  60. package/dist/graphql/server.js +117 -0
  61. package/dist/graphql/types.d.ts +9 -0
  62. package/dist/graphql/types.js +2 -0
  63. package/dist/handlers/resource-handlers.d.ts +20 -0
  64. package/dist/handlers/resource-handlers.js +180 -0
  65. package/dist/index.d.ts +33 -18
  66. package/dist/index.js +130 -619
  67. package/dist/resources/actors.d.ts +17 -12
  68. package/dist/resources/actors.js +56 -76
  69. package/dist/resources/assets.d.ts +6 -14
  70. package/dist/resources/assets.js +115 -147
  71. package/dist/resources/levels.d.ts +13 -13
  72. package/dist/resources/levels.js +25 -34
  73. package/dist/server/resource-registry.d.ts +20 -0
  74. package/dist/server/resource-registry.js +37 -0
  75. package/dist/server/tool-registry.d.ts +23 -0
  76. package/dist/server/tool-registry.js +322 -0
  77. package/dist/server-setup.d.ts +20 -0
  78. package/dist/server-setup.js +71 -0
  79. package/dist/services/health-monitor.d.ts +34 -0
  80. package/dist/services/health-monitor.js +105 -0
  81. package/dist/services/metrics-server.d.ts +11 -0
  82. package/dist/services/metrics-server.js +105 -0
  83. package/dist/tools/actors.d.ts +163 -9
  84. package/dist/tools/actors.js +356 -311
  85. package/dist/tools/animation.d.ts +135 -4
  86. package/dist/tools/animation.js +510 -411
  87. package/dist/tools/assets.d.ts +75 -29
  88. package/dist/tools/assets.js +265 -284
  89. package/dist/tools/audio.d.ts +102 -42
  90. package/dist/tools/audio.js +272 -685
  91. package/dist/tools/base-tool.d.ts +17 -0
  92. package/dist/tools/base-tool.js +46 -0
  93. package/dist/tools/behavior-tree.d.ts +94 -0
  94. package/dist/tools/behavior-tree.js +39 -0
  95. package/dist/tools/blueprint.d.ts +208 -126
  96. package/dist/tools/blueprint.js +685 -832
  97. package/dist/tools/consolidated-tool-definitions.d.ts +5462 -1781
  98. package/dist/tools/consolidated-tool-definitions.js +829 -496
  99. package/dist/tools/consolidated-tool-handlers.d.ts +2 -1
  100. package/dist/tools/consolidated-tool-handlers.js +198 -1027
  101. package/dist/tools/debug.d.ts +143 -85
  102. package/dist/tools/debug.js +234 -180
  103. package/dist/tools/dynamic-handler-registry.d.ts +13 -0
  104. package/dist/tools/dynamic-handler-registry.js +23 -0
  105. package/dist/tools/editor.d.ts +30 -83
  106. package/dist/tools/editor.js +247 -244
  107. package/dist/tools/engine.d.ts +10 -4
  108. package/dist/tools/engine.js +13 -5
  109. package/dist/tools/environment.d.ts +30 -0
  110. package/dist/tools/environment.js +267 -0
  111. package/dist/tools/foliage.d.ts +65 -99
  112. package/dist/tools/foliage.js +221 -331
  113. package/dist/tools/handlers/actor-handlers.d.ts +3 -0
  114. package/dist/tools/handlers/actor-handlers.js +227 -0
  115. package/dist/tools/handlers/animation-handlers.d.ts +3 -0
  116. package/dist/tools/handlers/animation-handlers.js +185 -0
  117. package/dist/tools/handlers/argument-helper.d.ts +16 -0
  118. package/dist/tools/handlers/argument-helper.js +80 -0
  119. package/dist/tools/handlers/asset-handlers.d.ts +3 -0
  120. package/dist/tools/handlers/asset-handlers.js +496 -0
  121. package/dist/tools/handlers/audio-handlers.d.ts +3 -0
  122. package/dist/tools/handlers/audio-handlers.js +166 -0
  123. package/dist/tools/handlers/blueprint-handlers.d.ts +4 -0
  124. package/dist/tools/handlers/blueprint-handlers.js +358 -0
  125. package/dist/tools/handlers/common-handlers.d.ts +14 -0
  126. package/dist/tools/handlers/common-handlers.js +56 -0
  127. package/dist/tools/handlers/editor-handlers.d.ts +3 -0
  128. package/dist/tools/handlers/editor-handlers.js +119 -0
  129. package/dist/tools/handlers/effect-handlers.d.ts +3 -0
  130. package/dist/tools/handlers/effect-handlers.js +171 -0
  131. package/dist/tools/handlers/environment-handlers.d.ts +3 -0
  132. package/dist/tools/handlers/environment-handlers.js +170 -0
  133. package/dist/tools/handlers/graph-handlers.d.ts +3 -0
  134. package/dist/tools/handlers/graph-handlers.js +90 -0
  135. package/dist/tools/handlers/input-handlers.d.ts +3 -0
  136. package/dist/tools/handlers/input-handlers.js +21 -0
  137. package/dist/tools/handlers/inspect-handlers.d.ts +3 -0
  138. package/dist/tools/handlers/inspect-handlers.js +383 -0
  139. package/dist/tools/handlers/level-handlers.d.ts +3 -0
  140. package/dist/tools/handlers/level-handlers.js +237 -0
  141. package/dist/tools/handlers/lighting-handlers.d.ts +3 -0
  142. package/dist/tools/handlers/lighting-handlers.js +144 -0
  143. package/dist/tools/handlers/performance-handlers.d.ts +3 -0
  144. package/dist/tools/handlers/performance-handlers.js +130 -0
  145. package/dist/tools/handlers/pipeline-handlers.d.ts +3 -0
  146. package/dist/tools/handlers/pipeline-handlers.js +110 -0
  147. package/dist/tools/handlers/sequence-handlers.d.ts +3 -0
  148. package/dist/tools/handlers/sequence-handlers.js +376 -0
  149. package/dist/tools/handlers/system-handlers.d.ts +4 -0
  150. package/dist/tools/handlers/system-handlers.js +506 -0
  151. package/dist/tools/input.d.ts +19 -0
  152. package/dist/tools/input.js +89 -0
  153. package/dist/tools/introspection.d.ts +103 -40
  154. package/dist/tools/introspection.js +425 -568
  155. package/dist/tools/landscape.d.ts +54 -93
  156. package/dist/tools/landscape.js +284 -409
  157. package/dist/tools/level.d.ts +66 -27
  158. package/dist/tools/level.js +647 -675
  159. package/dist/tools/lighting.d.ts +77 -38
  160. package/dist/tools/lighting.js +445 -943
  161. package/dist/tools/logs.d.ts +3 -3
  162. package/dist/tools/logs.js +5 -57
  163. package/dist/tools/materials.d.ts +91 -24
  164. package/dist/tools/materials.js +194 -118
  165. package/dist/tools/niagara.d.ts +149 -39
  166. package/dist/tools/niagara.js +267 -182
  167. package/dist/tools/performance.d.ts +27 -13
  168. package/dist/tools/performance.js +203 -122
  169. package/dist/tools/physics.d.ts +32 -77
  170. package/dist/tools/physics.js +175 -582
  171. package/dist/tools/property-dictionary.d.ts +13 -0
  172. package/dist/tools/property-dictionary.js +82 -0
  173. package/dist/tools/sequence.d.ts +85 -60
  174. package/dist/tools/sequence.js +208 -747
  175. package/dist/tools/tool-definition-utils.d.ts +59 -0
  176. package/dist/tools/tool-definition-utils.js +35 -0
  177. package/dist/tools/ui.d.ts +64 -34
  178. package/dist/tools/ui.js +134 -214
  179. package/dist/types/automation-responses.d.ts +115 -0
  180. package/dist/types/automation-responses.js +2 -0
  181. package/dist/types/env.d.ts +0 -3
  182. package/dist/types/env.js +0 -7
  183. package/dist/types/responses.d.ts +249 -0
  184. package/dist/types/responses.js +2 -0
  185. package/dist/types/tool-interfaces.d.ts +898 -0
  186. package/dist/types/tool-interfaces.js +2 -0
  187. package/dist/types/tool-types.d.ts +183 -19
  188. package/dist/types/tool-types.js +0 -4
  189. package/dist/unreal-bridge.d.ts +24 -131
  190. package/dist/unreal-bridge.js +364 -1506
  191. package/dist/utils/command-validator.d.ts +9 -0
  192. package/dist/utils/command-validator.js +68 -0
  193. package/dist/utils/elicitation.d.ts +1 -1
  194. package/dist/utils/elicitation.js +12 -15
  195. package/dist/utils/error-handler.d.ts +2 -51
  196. package/dist/utils/error-handler.js +11 -87
  197. package/dist/utils/ini-reader.d.ts +3 -0
  198. package/dist/utils/ini-reader.js +69 -0
  199. package/dist/utils/logger.js +9 -6
  200. package/dist/utils/normalize.d.ts +3 -0
  201. package/dist/utils/normalize.js +56 -0
  202. package/dist/utils/path-security.d.ts +2 -0
  203. package/dist/utils/path-security.js +24 -0
  204. package/dist/utils/response-factory.d.ts +7 -0
  205. package/dist/utils/response-factory.js +27 -0
  206. package/dist/utils/response-validator.d.ts +3 -24
  207. package/dist/utils/response-validator.js +130 -81
  208. package/dist/utils/result-helpers.d.ts +4 -5
  209. package/dist/utils/result-helpers.js +15 -16
  210. package/dist/utils/safe-json.js +5 -11
  211. package/dist/utils/unreal-command-queue.d.ts +24 -0
  212. package/dist/utils/unreal-command-queue.js +120 -0
  213. package/dist/utils/validation.d.ts +0 -40
  214. package/dist/utils/validation.js +1 -78
  215. package/dist/wasm/index.d.ts +70 -0
  216. package/dist/wasm/index.js +535 -0
  217. package/docs/GraphQL-API.md +888 -0
  218. package/docs/Migration-Guide-v0.5.0.md +684 -0
  219. package/docs/Roadmap.md +53 -0
  220. package/docs/WebAssembly-Integration.md +628 -0
  221. package/docs/editor-plugin-extension.md +370 -0
  222. package/docs/handler-mapping.md +242 -0
  223. package/docs/native-automation-progress.md +128 -0
  224. package/docs/testing-guide.md +423 -0
  225. package/mcp-config-example.json +6 -6
  226. package/package.json +67 -28
  227. package/plugins/McpAutomationBridge/Config/FilterPlugin.ini +8 -0
  228. package/plugins/McpAutomationBridge/McpAutomationBridge.uplugin +64 -0
  229. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/McpAutomationBridge.Build.cs +189 -0
  230. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.cpp +22 -0
  231. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.h +30 -0
  232. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeHelpers.h +1983 -0
  233. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeModule.cpp +72 -0
  234. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSettings.cpp +46 -0
  235. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +581 -0
  236. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +2394 -0
  237. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetQueryHandlers.cpp +300 -0
  238. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetWorkflowHandlers.cpp +2807 -0
  239. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AudioHandlers.cpp +1087 -0
  240. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BehaviorTreeHandlers.cpp +488 -0
  241. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.cpp +643 -0
  242. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.h +31 -0
  243. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +1184 -0
  244. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +5652 -0
  245. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers_List.cpp +152 -0
  246. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ControlHandlers.cpp +2614 -0
  247. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_DebugHandlers.cpp +42 -0
  248. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EditorFunctionHandlers.cpp +1237 -0
  249. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +1701 -0
  250. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +2145 -0
  251. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_FoliageHandlers.cpp +954 -0
  252. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InputHandlers.cpp +209 -0
  253. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InsightsHandlers.cpp +41 -0
  254. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LandscapeHandlers.cpp +1164 -0
  255. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LevelHandlers.cpp +762 -0
  256. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +634 -0
  257. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LogHandlers.cpp +136 -0
  258. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_MaterialGraphHandlers.cpp +494 -0
  259. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraGraphHandlers.cpp +278 -0
  260. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraHandlers.cpp +625 -0
  261. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PerformanceHandlers.cpp +401 -0
  262. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PipelineHandlers.cpp +67 -0
  263. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +735 -0
  264. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PropertyHandlers.cpp +2634 -0
  265. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_RenderHandlers.cpp +189 -0
  266. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.cpp +917 -0
  267. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.h +39 -0
  268. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +2670 -0
  269. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequencerHandlers.cpp +519 -0
  270. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_TestHandlers.cpp +38 -0
  271. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_UiHandlers.cpp +668 -0
  272. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_WorldPartitionHandlers.cpp +346 -0
  273. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.cpp +1330 -0
  274. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.h +149 -0
  275. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +783 -0
  276. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSettings.h +115 -0
  277. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSubsystem.h +796 -0
  278. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpConnectionManager.h +117 -0
  279. package/scripts/check-unreal-connection.mjs +19 -0
  280. package/scripts/clean-tmp.js +23 -0
  281. package/scripts/patch-wasm.js +26 -0
  282. package/scripts/run-all-tests.mjs +136 -0
  283. package/scripts/smoke-test.ts +94 -0
  284. package/scripts/sync-mcp-plugin.js +143 -0
  285. package/scripts/test-no-plugin-alternates.mjs +113 -0
  286. package/scripts/validate-server.js +46 -0
  287. package/scripts/verify-automation-bridge.js +200 -0
  288. package/server.json +58 -21
  289. package/src/automation/bridge.ts +558 -0
  290. package/src/automation/connection-manager.ts +130 -0
  291. package/src/automation/handshake.ts +99 -0
  292. package/src/automation/index.ts +2 -0
  293. package/src/automation/message-handler.ts +167 -0
  294. package/src/automation/request-tracker.ts +123 -0
  295. package/src/automation/types.ts +107 -0
  296. package/src/cli.ts +33 -6
  297. package/src/config.ts +73 -0
  298. package/src/constants.ts +19 -0
  299. package/src/graphql/loaders.ts +244 -0
  300. package/src/graphql/resolvers.ts +1008 -0
  301. package/src/graphql/schema.ts +452 -0
  302. package/src/graphql/server.ts +156 -0
  303. package/src/graphql/types.ts +10 -0
  304. package/src/handlers/resource-handlers.ts +186 -0
  305. package/src/index.ts +166 -664
  306. package/src/resources/actors.ts +58 -76
  307. package/src/resources/assets.ts +148 -134
  308. package/src/resources/levels.ts +28 -33
  309. package/src/server/resource-registry.ts +47 -0
  310. package/src/server/tool-registry.ts +354 -0
  311. package/src/server-setup.ts +114 -0
  312. package/src/services/health-monitor.ts +132 -0
  313. package/src/services/metrics-server.ts +142 -0
  314. package/src/tools/actors.ts +426 -323
  315. package/src/tools/animation.ts +672 -461
  316. package/src/tools/assets.ts +364 -289
  317. package/src/tools/audio.ts +323 -766
  318. package/src/tools/base-tool.ts +52 -0
  319. package/src/tools/behavior-tree.ts +45 -0
  320. package/src/tools/blueprint.ts +792 -970
  321. package/src/tools/consolidated-tool-definitions.ts +993 -515
  322. package/src/tools/consolidated-tool-handlers.ts +258 -1146
  323. package/src/tools/debug.ts +292 -187
  324. package/src/tools/dynamic-handler-registry.ts +33 -0
  325. package/src/tools/editor.ts +329 -253
  326. package/src/tools/engine.ts +14 -3
  327. package/src/tools/environment.ts +281 -0
  328. package/src/tools/foliage.ts +330 -392
  329. package/src/tools/handlers/actor-handlers.ts +265 -0
  330. package/src/tools/handlers/animation-handlers.ts +237 -0
  331. package/src/tools/handlers/argument-helper.ts +142 -0
  332. package/src/tools/handlers/asset-handlers.ts +532 -0
  333. package/src/tools/handlers/audio-handlers.ts +194 -0
  334. package/src/tools/handlers/blueprint-handlers.ts +380 -0
  335. package/src/tools/handlers/common-handlers.ts +87 -0
  336. package/src/tools/handlers/editor-handlers.ts +123 -0
  337. package/src/tools/handlers/effect-handlers.ts +220 -0
  338. package/src/tools/handlers/environment-handlers.ts +183 -0
  339. package/src/tools/handlers/graph-handlers.ts +116 -0
  340. package/src/tools/handlers/input-handlers.ts +28 -0
  341. package/src/tools/handlers/inspect-handlers.ts +450 -0
  342. package/src/tools/handlers/level-handlers.ts +252 -0
  343. package/src/tools/handlers/lighting-handlers.ts +147 -0
  344. package/src/tools/handlers/performance-handlers.ts +132 -0
  345. package/src/tools/handlers/pipeline-handlers.ts +127 -0
  346. package/src/tools/handlers/sequence-handlers.ts +415 -0
  347. package/src/tools/handlers/system-handlers.ts +564 -0
  348. package/src/tools/input.ts +101 -0
  349. package/src/tools/introspection.ts +493 -584
  350. package/src/tools/landscape.ts +418 -507
  351. package/src/tools/level.ts +786 -708
  352. package/src/tools/lighting.ts +588 -984
  353. package/src/tools/logs.ts +9 -57
  354. package/src/tools/materials.ts +237 -121
  355. package/src/tools/niagara.ts +335 -168
  356. package/src/tools/performance.ts +320 -169
  357. package/src/tools/physics.ts +274 -613
  358. package/src/tools/property-dictionary.ts +98 -0
  359. package/src/tools/sequence.ts +276 -820
  360. package/src/tools/tool-definition-utils.ts +35 -0
  361. package/src/tools/ui.ts +205 -283
  362. package/src/types/automation-responses.ts +119 -0
  363. package/src/types/env.ts +0 -10
  364. package/src/types/responses.ts +355 -0
  365. package/src/types/tool-interfaces.ts +250 -0
  366. package/src/types/tool-types.ts +243 -21
  367. package/src/unreal-bridge.ts +460 -1550
  368. package/src/utils/command-validator.ts +76 -0
  369. package/src/utils/elicitation.ts +10 -7
  370. package/src/utils/error-handler.ts +14 -90
  371. package/src/utils/ini-reader.ts +86 -0
  372. package/src/utils/logger.ts +8 -3
  373. package/src/utils/normalize.test.ts +162 -0
  374. package/src/utils/normalize.ts +60 -0
  375. package/src/utils/path-security.ts +43 -0
  376. package/src/utils/response-factory.ts +44 -0
  377. package/src/utils/response-validator.ts +176 -56
  378. package/src/utils/result-helpers.ts +21 -19
  379. package/src/utils/safe-json.test.ts +90 -0
  380. package/src/utils/safe-json.ts +14 -11
  381. package/src/utils/unreal-command-queue.ts +152 -0
  382. package/src/utils/validation.test.ts +184 -0
  383. package/src/utils/validation.ts +4 -1
  384. package/src/wasm/index.ts +838 -0
  385. package/test-server.mjs +100 -0
  386. package/tests/run-unreal-tool-tests.mjs +242 -14
  387. package/tests/test-animation.mjs +369 -0
  388. package/tests/test-asset-advanced.mjs +82 -0
  389. package/tests/test-asset-errors.mjs +35 -0
  390. package/tests/test-asset-graph.mjs +311 -0
  391. package/tests/test-audio.mjs +417 -0
  392. package/tests/test-automation-timeouts.mjs +98 -0
  393. package/tests/test-behavior-tree.mjs +444 -0
  394. package/tests/test-blueprint-graph.mjs +410 -0
  395. package/tests/test-blueprint.mjs +577 -0
  396. package/tests/test-client-mode.mjs +86 -0
  397. package/tests/test-console-command.mjs +56 -0
  398. package/tests/test-control-actor.mjs +425 -0
  399. package/tests/test-control-editor.mjs +112 -0
  400. package/tests/test-graphql.mjs +372 -0
  401. package/tests/test-input.mjs +349 -0
  402. package/tests/test-inspect.mjs +302 -0
  403. package/tests/test-landscape.mjs +316 -0
  404. package/tests/test-lighting.mjs +428 -0
  405. package/tests/test-manage-asset.mjs +438 -0
  406. package/tests/test-manage-level.mjs +89 -0
  407. package/tests/test-materials.mjs +356 -0
  408. package/tests/test-niagara.mjs +185 -0
  409. package/tests/test-no-inline-python.mjs +122 -0
  410. package/tests/test-performance.mjs +539 -0
  411. package/tests/test-plugin-handshake.mjs +82 -0
  412. package/tests/test-runner.mjs +933 -0
  413. package/tests/test-sequence.mjs +104 -0
  414. package/tests/test-system.mjs +96 -0
  415. package/tests/test-wasm.mjs +283 -0
  416. package/tests/test-world-partition.mjs +215 -0
  417. package/tsconfig.json +3 -3
  418. package/vitest.config.ts +35 -0
  419. package/wasm/Cargo.lock +363 -0
  420. package/wasm/Cargo.toml +42 -0
  421. package/wasm/LICENSE +21 -0
  422. package/wasm/README.md +253 -0
  423. package/wasm/src/dependency_resolver.rs +377 -0
  424. package/wasm/src/lib.rs +153 -0
  425. package/wasm/src/property_parser.rs +271 -0
  426. package/wasm/src/transform_math.rs +396 -0
  427. package/wasm/tests/integration.rs +109 -0
  428. package/.github/workflows/smithery-build.yml +0 -29
  429. package/dist/prompts/index.d.ts +0 -21
  430. package/dist/prompts/index.js +0 -217
  431. package/dist/tools/build_environment_advanced.d.ts +0 -65
  432. package/dist/tools/build_environment_advanced.js +0 -633
  433. package/dist/tools/rc.d.ts +0 -110
  434. package/dist/tools/rc.js +0 -437
  435. package/dist/tools/visual.d.ts +0 -40
  436. package/dist/tools/visual.js +0 -282
  437. package/dist/utils/http.d.ts +0 -6
  438. package/dist/utils/http.js +0 -151
  439. package/dist/utils/python-output.d.ts +0 -18
  440. package/dist/utils/python-output.js +0 -290
  441. package/dist/utils/python.d.ts +0 -2
  442. package/dist/utils/python.js +0 -4
  443. package/dist/utils/stdio-redirect.d.ts +0 -2
  444. package/dist/utils/stdio-redirect.js +0 -20
  445. package/docs/unreal-tool-test-cases.md +0 -574
  446. package/smithery.yaml +0 -29
  447. package/src/prompts/index.ts +0 -249
  448. package/src/tools/build_environment_advanced.ts +0 -732
  449. package/src/tools/rc.ts +0 -515
  450. package/src/tools/visual.ts +0 -281
  451. package/src/utils/http.ts +0 -187
  452. package/src/utils/python-output.ts +0 -351
  453. package/src/utils/python.ts +0 -3
  454. package/src/utils/stdio-redirect.ts +0 -18
@@ -0,0 +1,1701 @@
1
+ #include "DrawDebugHelpers.h"
2
+ #include "McpAutomationBridgeGlobals.h"
3
+ #include "McpAutomationBridgeHelpers.h"
4
+ #include "McpAutomationBridgeSubsystem.h"
5
+
6
+ #if WITH_EDITOR
7
+ #include "EditorAssetLibrary.h"
8
+ #if __has_include("Subsystems/EditorActorSubsystem.h")
9
+ #include "Subsystems/EditorActorSubsystem.h"
10
+ #elif __has_include("EditorActorSubsystem.h")
11
+ #include "EditorActorSubsystem.h"
12
+ #endif
13
+ #if __has_include("NiagaraActor.h")
14
+ #include "NiagaraActor.h"
15
+ #endif
16
+ #if __has_include("NiagaraComponent.h")
17
+ #include "NiagaraComponent.h"
18
+ #endif
19
+ #if __has_include("NiagaraSystem.h")
20
+ #include "NiagaraFunctionLibrary.h"
21
+ #include "NiagaraSystem.h"
22
+ #include "NiagaraSystemFactoryNew.h"
23
+ #endif
24
+ #if __has_include("Engine/PointLight.h")
25
+ #include "Engine/PointLight.h"
26
+ #endif
27
+ #if __has_include("Engine/SpotLight.h")
28
+ #include "Engine/SpotLight.h"
29
+ #endif
30
+ #if __has_include("Engine/DirectionalLight.h")
31
+ #include "Engine/DirectionalLight.h"
32
+ #endif
33
+ #if __has_include("Engine/RectLight.h")
34
+ #include "Engine/RectLight.h"
35
+ #endif
36
+ #if __has_include("Components/LightComponent.h")
37
+ #include "Components/LightComponent.h"
38
+ #endif
39
+ #if __has_include("Components/PointLightComponent.h")
40
+ #include "Components/PointLightComponent.h"
41
+ #endif
42
+ #if __has_include("Components/SpotLightComponent.h")
43
+ #include "Components/SpotLightComponent.h"
44
+ #endif
45
+ #if __has_include("Components/RectLightComponent.h")
46
+ #include "Components/RectLightComponent.h"
47
+ #endif
48
+ #if __has_include("Components/DirectionalLightComponent.h")
49
+ #include "Components/DirectionalLightComponent.h"
50
+ #endif
51
+ #endif
52
+
53
+ bool UMcpAutomationBridgeSubsystem::HandleEffectAction(
54
+ const FString &RequestId, const FString &Action,
55
+ const TSharedPtr<FJsonObject> &Payload,
56
+ TSharedPtr<FMcpBridgeWebSocket> RequestingSocket) {
57
+ const FString Lower = Action.ToLower();
58
+ const bool bIsCreateEffect = Lower.Equals(TEXT("create_effect")) ||
59
+ Lower.StartsWith(TEXT("create_effect"));
60
+ if (!bIsCreateEffect && !Lower.StartsWith(TEXT("spawn_")) &&
61
+ !Lower.Equals(TEXT("set_niagara_parameter")) &&
62
+ !Lower.Equals(TEXT("clear_debug_shapes")))
63
+ return false;
64
+
65
+ TSharedPtr<FJsonObject> LocalPayload =
66
+ Payload.IsValid() ? Payload : MakeShared<FJsonObject>();
67
+
68
+ auto SendResponse = [&](bool bOk, const FString &Msg,
69
+ const TSharedPtr<FJsonObject> &ResObj,
70
+ const FString &ErrCode = FString()) {
71
+ SendAutomationResponse(RequestingSocket, RequestId, bOk, Msg, ResObj,
72
+ ErrCode);
73
+ };
74
+
75
+ // Handle create_effect tool with sub-actions
76
+ if (Lower.Equals(TEXT("clear_debug_shapes"))) {
77
+ #if WITH_EDITOR
78
+ if (GEditor && GEditor->GetEditorWorldContext().World()) {
79
+ FlushPersistentDebugLines(GEditor->GetEditorWorldContext().World());
80
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
81
+ Resp->SetBoolField(TEXT("success"), true);
82
+ SendAutomationResponse(RequestingSocket, RequestId, true,
83
+ TEXT("Debug shapes cleared"), Resp);
84
+ return true;
85
+ } else {
86
+ SendAutomationResponse(RequestingSocket, RequestId, false,
87
+ TEXT("Editor world not available"), nullptr,
88
+ TEXT("NO_WORLD"));
89
+ return true;
90
+ }
91
+ #else
92
+ SendAutomationResponse(RequestingSocket, RequestId, false,
93
+ TEXT("Debug shape clearing requires editor build"),
94
+ nullptr, TEXT("NOT_IMPLEMENTED"));
95
+ return true;
96
+ #endif
97
+ }
98
+
99
+ if (bIsCreateEffect || Lower.Equals(TEXT("create_niagara_system"))) {
100
+ FString SubAction;
101
+ LocalPayload->TryGetStringField(TEXT("action"), SubAction);
102
+
103
+ if (Lower.Equals(TEXT("create_niagara_system"))) {
104
+ SubAction = TEXT("create_niagara_system");
105
+ }
106
+
107
+ // Fallback: if action field in payload is empty, check if the top-level
108
+ // Action is a specific tool (e.g. set_niagara_parameter) and use that as
109
+ // sub-action.
110
+ if (SubAction.IsEmpty() &&
111
+ !Action.Equals(TEXT("create_effect"), ESearchCase::IgnoreCase)) {
112
+ SubAction = Action;
113
+ }
114
+
115
+ const FString LowerSub = SubAction.ToLower();
116
+
117
+ // Handle particle spawning
118
+ if (LowerSub == TEXT("particle")) {
119
+ FString Preset;
120
+ LocalPayload->TryGetStringField(TEXT("preset"), Preset);
121
+ if (Preset.IsEmpty()) {
122
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
123
+ Resp->SetBoolField(TEXT("success"), false);
124
+ Resp->SetStringField(
125
+ TEXT("error"),
126
+ TEXT("preset parameter required for particle spawning"));
127
+ SendAutomationResponse(RequestingSocket, RequestId, false,
128
+ TEXT("Preset path required"), Resp,
129
+ TEXT("INVALID_ARGUMENT"));
130
+ return true;
131
+ }
132
+
133
+ // Location and optional rotation/scale
134
+ FVector Loc(0, 0, 0);
135
+ if (LocalPayload->HasField(TEXT("location"))) {
136
+ const TSharedPtr<FJsonValue> LocVal =
137
+ LocalPayload->TryGetField(TEXT("location"));
138
+ if (LocVal.IsValid()) {
139
+ if (LocVal->Type == EJson::Array) {
140
+ const TArray<TSharedPtr<FJsonValue>> &Arr = LocVal->AsArray();
141
+ if (Arr.Num() >= 3)
142
+ Loc =
143
+ FVector((float)Arr[0]->AsNumber(), (float)Arr[1]->AsNumber(),
144
+ (float)Arr[2]->AsNumber());
145
+ } else if (LocVal->Type == EJson::Object) {
146
+ const TSharedPtr<FJsonObject> O = LocVal->AsObject();
147
+ if (O.IsValid())
148
+ Loc = FVector(
149
+ (float)(O->HasField(TEXT("x")) ? O->GetNumberField(TEXT("x"))
150
+ : 0.0),
151
+ (float)(O->HasField(TEXT("y")) ? O->GetNumberField(TEXT("y"))
152
+ : 0.0),
153
+ (float)(O->HasField(TEXT("z")) ? O->GetNumberField(TEXT("z"))
154
+ : 0.0));
155
+ }
156
+ }
157
+ }
158
+
159
+ // Rotation may be an array
160
+ TArray<double> RotArr = {0, 0, 0};
161
+ const TArray<TSharedPtr<FJsonValue>> *RA = nullptr;
162
+ if (LocalPayload->TryGetArrayField(TEXT("rotation"), RA) && RA &&
163
+ RA->Num() >= 3) {
164
+ RotArr[0] = (*RA)[0]->AsNumber();
165
+ RotArr[1] = (*RA)[1]->AsNumber();
166
+ RotArr[2] = (*RA)[2]->AsNumber();
167
+ }
168
+
169
+ // Scale may be an array or a single numeric value
170
+ TArray<double> ScaleArr = {1, 1, 1};
171
+ const TArray<TSharedPtr<FJsonValue>> *ScaleJsonArr = nullptr;
172
+ if (LocalPayload->TryGetArrayField(TEXT("scale"), ScaleJsonArr) &&
173
+ ScaleJsonArr && ScaleJsonArr->Num() >= 3) {
174
+ ScaleArr[0] = (*ScaleJsonArr)[0]->AsNumber();
175
+ ScaleArr[1] = (*ScaleJsonArr)[1]->AsNumber();
176
+ ScaleArr[2] = (*ScaleJsonArr)[2]->AsNumber();
177
+ } else if (LocalPayload->TryGetNumberField(TEXT("scale"), ScaleArr[0])) {
178
+ ScaleArr[1] = ScaleArr[2] = ScaleArr[0];
179
+ }
180
+
181
+ const bool bAutoDestroy =
182
+ LocalPayload->HasField(TEXT("autoDestroy"))
183
+ ? LocalPayload->GetBoolField(TEXT("autoDestroy"))
184
+ : false;
185
+
186
+ #if WITH_EDITOR
187
+ if (!GEditor) {
188
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
189
+ Resp->SetBoolField(TEXT("success"), false);
190
+ Resp->SetStringField(TEXT("error"), TEXT("Editor not available"));
191
+ SendAutomationResponse(RequestingSocket, RequestId, false,
192
+ TEXT("Editor not available"), Resp,
193
+ TEXT("EDITOR_NOT_AVAILABLE"));
194
+ return true;
195
+ }
196
+ UEditorActorSubsystem *ActorSS =
197
+ GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
198
+ if (!ActorSS) {
199
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
200
+ Resp->SetBoolField(TEXT("success"), false);
201
+ Resp->SetStringField(TEXT("error"),
202
+ TEXT("EditorActorSubsystem not available"));
203
+ SendAutomationResponse(RequestingSocket, RequestId, false,
204
+ TEXT("EditorActorSubsystem not available"), Resp,
205
+ TEXT("EDITOR_ACTOR_SUBSYSTEM_MISSING"));
206
+ return true;
207
+ }
208
+
209
+ UObject *ParticleObj = UEditorAssetLibrary::LoadAsset(Preset);
210
+ if (!ParticleObj) {
211
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
212
+ Resp->SetBoolField(TEXT("success"), false);
213
+ Resp->SetStringField(TEXT("error"),
214
+ TEXT("Particle preset asset not found"));
215
+ Resp->SetStringField(TEXT("preset"), Preset);
216
+ SendAutomationResponse(RequestingSocket, RequestId, false,
217
+ TEXT("Particle preset not found"), Resp,
218
+ TEXT("PRESET_NOT_FOUND"));
219
+ return true;
220
+ }
221
+
222
+ const FRotator SpawnRot(static_cast<float>(RotArr[0]),
223
+ static_cast<float>(RotArr[1]),
224
+ static_cast<float>(RotArr[2]));
225
+ AActor *Spawned = SpawnActorInActiveWorld<AActor>(
226
+ ANiagaraActor::StaticClass(), Loc, SpawnRot);
227
+ if (!Spawned) {
228
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
229
+ Resp->SetBoolField(TEXT("success"), false);
230
+ Resp->SetStringField(TEXT("error"),
231
+ TEXT("Failed to spawn particle actor"));
232
+ SendAutomationResponse(RequestingSocket, RequestId, false,
233
+ TEXT("Failed to spawn particle actor"), Resp,
234
+ TEXT("SPAWN_FAILED"));
235
+ return true;
236
+ }
237
+
238
+ UNiagaraComponent *NiComp =
239
+ Spawned->FindComponentByClass<UNiagaraComponent>();
240
+ if (NiComp && ParticleObj->IsA<UNiagaraSystem>()) {
241
+ NiComp->SetAsset(Cast<UNiagaraSystem>(ParticleObj));
242
+ NiComp->SetWorldScale3D(FVector(ScaleArr[0], ScaleArr[1], ScaleArr[2]));
243
+ NiComp->Activate(true);
244
+ }
245
+
246
+ Spawned->SetActorLabel(FString::Printf(
247
+ TEXT("Particle_%s_%lld"), *FPackageName::GetShortName(Preset),
248
+ FDateTime::Now().ToUnixTimestamp()));
249
+
250
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
251
+ Resp->SetBoolField(TEXT("success"), true);
252
+ Resp->SetStringField(TEXT("particlePath"), Preset);
253
+ Resp->SetStringField(TEXT("actorName"), Spawned->GetActorLabel());
254
+ Resp->SetNumberField(TEXT("actorId"), Spawned->GetUniqueID());
255
+ SendAutomationResponse(RequestingSocket, RequestId, true,
256
+ TEXT("Particle preset spawned"), Resp, FString());
257
+ return true;
258
+ #else
259
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
260
+ Resp->SetBoolField(TEXT("success"), false);
261
+ Resp->SetStringField(TEXT("error"),
262
+ TEXT("Particle spawning requires editor build"));
263
+ Resp->SetStringField(TEXT("preset"), Preset);
264
+ SendAutomationResponse(
265
+ RequestingSocket, RequestId, false,
266
+ TEXT("Particle spawning not available in non-editor build"), Resp,
267
+ TEXT("NOT_AVAILABLE"));
268
+ return true;
269
+ #endif
270
+ }
271
+ // Handle create_niagara_system
272
+ else if (LowerSub == TEXT("create_niagara_system")) {
273
+ FString Name;
274
+ LocalPayload->TryGetStringField(TEXT("name"), Name);
275
+ FString Path;
276
+ LocalPayload->TryGetStringField(TEXT("path"), Path);
277
+
278
+ if (Name.IsEmpty() || Path.IsEmpty()) {
279
+ SendAutomationError(RequestingSocket, RequestId,
280
+ TEXT("name and path required"),
281
+ TEXT("INVALID_ARGUMENT"));
282
+ return true;
283
+ }
284
+
285
+ // Basic asset creation logic (requires UNiagaraSystemFactoryNew or
286
+ // similar) Since we are inside EffectHandlers, usually we spawn things.
287
+ // creation might belong in AssetHandlers But per plan, we implement it
288
+ // here to unblock.
289
+
290
+ UPackage *Package = CreatePackage(*Path);
291
+ if (!Package) {
292
+ SendAutomationError(RequestingSocket, RequestId,
293
+ TEXT("Failed to create package"),
294
+ TEXT("CREATE_FAILED"));
295
+ return true;
296
+ }
297
+
298
+ UNiagaraSystemFactoryNew *Factory = NewObject<UNiagaraSystemFactoryNew>();
299
+ UNiagaraSystem *NewSystem =
300
+ Cast<UNiagaraSystem>(Factory->FactoryCreateNew(
301
+ UNiagaraSystem::StaticClass(), Package, *Name,
302
+ RF_Public | RF_Standalone, nullptr, nullptr));
303
+
304
+ if (NewSystem) {
305
+ FAssetRegistryModule::AssetCreated(NewSystem);
306
+ Package->MarkPackageDirty();
307
+ // Return validity check
308
+ FString AssetPath = NewSystem->GetPathName();
309
+ // If it's something like /Game/Path/Asset.Asset, try to simplify for
310
+ // user convenience But LoadAsset works with the full Object path or
311
+ // Package path. Let's ensure we save it properly.
312
+ UEditorAssetLibrary::SaveAsset(AssetPath);
313
+
314
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
315
+ Resp->SetBoolField(TEXT("success"), true);
316
+ Resp->SetStringField(TEXT("assetPath"), AssetPath);
317
+ Resp->SetStringField(TEXT("packageName"), Package->GetPathName());
318
+
319
+ SendAutomationResponse(RequestingSocket, RequestId, true,
320
+ TEXT("Niagara System created"), Resp);
321
+ } else {
322
+ SendAutomationError(RequestingSocket, RequestId,
323
+ TEXT("Factory failed to create system"),
324
+ TEXT("CREATE_FAILED"));
325
+ }
326
+ return true;
327
+ }
328
+
329
+ // Handle debug shapes
330
+ if (LowerSub == TEXT("debug_shape")) {
331
+ FString ShapeType;
332
+ LocalPayload->TryGetStringField(TEXT("shapeType"), ShapeType);
333
+ if (ShapeType.IsEmpty()) {
334
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
335
+ Resp->SetBoolField(TEXT("success"), false);
336
+ Resp->SetStringField(
337
+ TEXT("error"),
338
+ TEXT("shapeType parameter required for debug shape drawing"));
339
+ SendAutomationResponse(RequestingSocket, RequestId, false,
340
+ TEXT("shapeType required"), Resp,
341
+ TEXT("INVALID_ARGUMENT"));
342
+ return true;
343
+ }
344
+
345
+ // Location
346
+ FVector Loc(0, 0, 0);
347
+ if (LocalPayload->HasField(TEXT("location"))) {
348
+ const TSharedPtr<FJsonValue> LocVal =
349
+ LocalPayload->TryGetField(TEXT("location"));
350
+ if (LocVal.IsValid()) {
351
+ if (LocVal->Type == EJson::Array) {
352
+ const TArray<TSharedPtr<FJsonValue>> &Arr = LocVal->AsArray();
353
+ if (Arr.Num() >= 3)
354
+ Loc =
355
+ FVector((float)Arr[0]->AsNumber(), (float)Arr[1]->AsNumber(),
356
+ (float)Arr[2]->AsNumber());
357
+ } else if (LocVal->Type == EJson::Object) {
358
+ const TSharedPtr<FJsonObject> O = LocVal->AsObject();
359
+ if (O.IsValid())
360
+ Loc = FVector(
361
+ (float)(O->HasField(TEXT("x")) ? O->GetNumberField(TEXT("x"))
362
+ : 0.0),
363
+ (float)(O->HasField(TEXT("y")) ? O->GetNumberField(TEXT("y"))
364
+ : 0.0),
365
+ (float)(O->HasField(TEXT("z")) ? O->GetNumberField(TEXT("z"))
366
+ : 0.0));
367
+ }
368
+ }
369
+ }
370
+
371
+ // Color (default: red)
372
+ TArray<double> ColorArr = {255, 0, 0, 255};
373
+ const TArray<TSharedPtr<FJsonValue>> *ColorJsonArr = nullptr;
374
+ if (LocalPayload->TryGetArrayField(TEXT("color"), ColorJsonArr) &&
375
+ ColorJsonArr && ColorJsonArr->Num() >= 4) {
376
+ ColorArr[0] = (*ColorJsonArr)[0]->AsNumber();
377
+ ColorArr[1] = (*ColorJsonArr)[1]->AsNumber();
378
+ ColorArr[2] = (*ColorJsonArr)[2]->AsNumber();
379
+ ColorArr[3] = (*ColorJsonArr)[3]->AsNumber();
380
+ }
381
+
382
+ // Duration (default: 5.0 seconds)
383
+ const float Duration =
384
+ LocalPayload->HasField(TEXT("duration"))
385
+ ? (float)LocalPayload->GetNumberField(TEXT("duration"))
386
+ : 5.0f;
387
+
388
+ // Size/Radius (default: 100.0)
389
+ const float Size = LocalPayload->HasField(TEXT("size"))
390
+ ? (float)LocalPayload->GetNumberField(TEXT("size"))
391
+ : 100.0f;
392
+
393
+ // Thickness for lines (default: 2.0)
394
+ const float Thickness =
395
+ LocalPayload->HasField(TEXT("thickness"))
396
+ ? (float)LocalPayload->GetNumberField(TEXT("thickness"))
397
+ : 2.0f;
398
+
399
+ #if WITH_EDITOR
400
+ if (!GEditor) {
401
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
402
+ Resp->SetBoolField(TEXT("success"), false);
403
+ Resp->SetStringField(TEXT("error"),
404
+ TEXT("Editor not available for debug drawing"));
405
+ SendAutomationResponse(RequestingSocket, RequestId, false,
406
+ TEXT("Editor not available"), Resp,
407
+ TEXT("EDITOR_NOT_AVAILABLE"));
408
+ return true;
409
+ }
410
+
411
+ // Get the current world for debug drawing
412
+ UWorld *World = GEditor->GetEditorWorldContext().World();
413
+ if (!World) {
414
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
415
+ Resp->SetBoolField(TEXT("success"), false);
416
+ Resp->SetStringField(TEXT("error"),
417
+ TEXT("No world available for debug drawing"));
418
+ SendAutomationResponse(RequestingSocket, RequestId, false,
419
+ TEXT("No world available"), Resp,
420
+ TEXT("NO_WORLD"));
421
+ return true;
422
+ }
423
+
424
+ const FColor DebugColor((uint8)ColorArr[0], (uint8)ColorArr[1],
425
+ (uint8)ColorArr[2], (uint8)ColorArr[3]);
426
+ const FString LowerShapeType = ShapeType.ToLower();
427
+
428
+ if (LowerShapeType == TEXT("sphere")) {
429
+ DrawDebugSphere(World, Loc, Size, 16, DebugColor, false, Duration, 0,
430
+ Thickness);
431
+ } else if (LowerShapeType == TEXT("box")) {
432
+ FVector BoxSize = FVector(Size);
433
+ if (LocalPayload->HasField(TEXT("boxSize"))) {
434
+ const TArray<TSharedPtr<FJsonValue>> *BoxSizeArr = nullptr;
435
+ if (LocalPayload->TryGetArrayField(TEXT("boxSize"), BoxSizeArr) &&
436
+ BoxSizeArr && BoxSizeArr->Num() >= 3) {
437
+ BoxSize = FVector((float)(*BoxSizeArr)[0]->AsNumber(),
438
+ (float)(*BoxSizeArr)[1]->AsNumber(),
439
+ (float)(*BoxSizeArr)[2]->AsNumber());
440
+ }
441
+ }
442
+ DrawDebugBox(World, Loc, BoxSize, FRotator::ZeroRotator.Quaternion(),
443
+ DebugColor, false, Duration, 0, Thickness);
444
+ } else if (LowerShapeType == TEXT("circle")) {
445
+ DrawDebugCircle(World, Loc, Size, 32, DebugColor, false, Duration, 0,
446
+ Thickness, FVector::UpVector);
447
+ } else if (LowerShapeType == TEXT("line")) {
448
+ FVector EndLoc = Loc + FVector(100, 0, 0);
449
+ if (LocalPayload->HasField(TEXT("endLocation"))) {
450
+ const TSharedPtr<FJsonValue> EndVal =
451
+ LocalPayload->TryGetField(TEXT("endLocation"));
452
+ if (EndVal.IsValid()) {
453
+ if (EndVal->Type == EJson::Array) {
454
+ const TArray<TSharedPtr<FJsonValue>> &Arr = EndVal->AsArray();
455
+ if (Arr.Num() >= 3)
456
+ EndLoc = FVector((float)Arr[0]->AsNumber(),
457
+ (float)Arr[1]->AsNumber(),
458
+ (float)Arr[2]->AsNumber());
459
+ } else if (EndVal->Type == EJson::Object) {
460
+ const TSharedPtr<FJsonObject> O = EndVal->AsObject();
461
+ if (O.IsValid())
462
+ EndLoc = FVector((float)(O->HasField(TEXT("x"))
463
+ ? O->GetNumberField(TEXT("x"))
464
+ : 0.0),
465
+ (float)(O->HasField(TEXT("y"))
466
+ ? O->GetNumberField(TEXT("y"))
467
+ : 0.0),
468
+ (float)(O->HasField(TEXT("z"))
469
+ ? O->GetNumberField(TEXT("z"))
470
+ : 0.0));
471
+ }
472
+ }
473
+ }
474
+ DrawDebugLine(World, Loc, EndLoc, DebugColor, false, Duration, 0,
475
+ Thickness);
476
+ } else if (LowerShapeType == TEXT("point")) {
477
+ DrawDebugPoint(World, Loc, Size, DebugColor, false, Duration);
478
+ } else if (LowerShapeType == TEXT("coordinate")) {
479
+ FRotator Rot = FRotator::ZeroRotator;
480
+ if (LocalPayload->HasField(TEXT("rotation"))) {
481
+ const TArray<TSharedPtr<FJsonValue>> *RotArr = nullptr;
482
+ if (LocalPayload->TryGetArrayField(TEXT("rotation"), RotArr) &&
483
+ RotArr && RotArr->Num() >= 3) {
484
+ Rot = FRotator((float)(*RotArr)[0]->AsNumber(),
485
+ (float)(*RotArr)[1]->AsNumber(),
486
+ (float)(*RotArr)[2]->AsNumber());
487
+ }
488
+ }
489
+ DrawDebugCoordinateSystem(World, Loc, Rot, Size, false, Duration, 0,
490
+ Thickness);
491
+ } else if (LowerShapeType == TEXT("cylinder")) {
492
+ FVector EndLoc = Loc + FVector(0, 0, 100);
493
+ if (LocalPayload->HasField(TEXT("endLocation"))) {
494
+ const TSharedPtr<FJsonValue> EndVal =
495
+ LocalPayload->TryGetField(TEXT("endLocation"));
496
+ if (EndVal.IsValid()) {
497
+ if (EndVal->Type == EJson::Array) {
498
+ const TArray<TSharedPtr<FJsonValue>> &Arr = EndVal->AsArray();
499
+ if (Arr.Num() >= 3)
500
+ EndLoc = FVector((float)Arr[0]->AsNumber(),
501
+ (float)Arr[1]->AsNumber(),
502
+ (float)Arr[2]->AsNumber());
503
+ } else if (EndVal->Type == EJson::Object) {
504
+ const TSharedPtr<FJsonObject> O = EndVal->AsObject();
505
+ if (O.IsValid())
506
+ EndLoc = FVector((float)(O->HasField(TEXT("x"))
507
+ ? O->GetNumberField(TEXT("x"))
508
+ : 0.0),
509
+ (float)(O->HasField(TEXT("y"))
510
+ ? O->GetNumberField(TEXT("y"))
511
+ : 0.0),
512
+ (float)(O->HasField(TEXT("z"))
513
+ ? O->GetNumberField(TEXT("z"))
514
+ : 0.0));
515
+ }
516
+ }
517
+ }
518
+ DrawDebugCylinder(World, Loc, EndLoc, Size, 16, DebugColor, false,
519
+ Duration, 0, Thickness);
520
+ } else if (LowerShapeType == TEXT("cone")) {
521
+ FVector Direction = FVector::UpVector;
522
+ if (LocalPayload->HasField(TEXT("direction"))) {
523
+ const TSharedPtr<FJsonValue> DirVal =
524
+ LocalPayload->TryGetField(TEXT("direction"));
525
+ if (DirVal.IsValid()) {
526
+ if (DirVal->Type == EJson::Array) {
527
+ const TArray<TSharedPtr<FJsonValue>> &Arr = DirVal->AsArray();
528
+ if (Arr.Num() >= 3)
529
+ Direction = FVector((float)Arr[0]->AsNumber(),
530
+ (float)Arr[1]->AsNumber(),
531
+ (float)Arr[2]->AsNumber());
532
+ } else if (DirVal->Type == EJson::Object) {
533
+ const TSharedPtr<FJsonObject> O = DirVal->AsObject();
534
+ if (O.IsValid())
535
+ Direction = FVector((float)(O->HasField(TEXT("x"))
536
+ ? O->GetNumberField(TEXT("x"))
537
+ : 0.0),
538
+ (float)(O->HasField(TEXT("y"))
539
+ ? O->GetNumberField(TEXT("y"))
540
+ : 0.0),
541
+ (float)(O->HasField(TEXT("z"))
542
+ ? O->GetNumberField(TEXT("z"))
543
+ : 0.0));
544
+ }
545
+ }
546
+ }
547
+ float Length = 100.0f;
548
+ if (LocalPayload->HasField(TEXT("length"))) {
549
+ Length = (float)LocalPayload->GetNumberField(TEXT("length"));
550
+ }
551
+ // Default to a 45 degree cone if not specified
552
+ float AngleWidth = FMath::DegreesToRadians(45.0f);
553
+ float AngleHeight = FMath::DegreesToRadians(45.0f);
554
+
555
+ if (LocalPayload->HasField(TEXT("angle"))) {
556
+ float AngleDeg = (float)LocalPayload->GetNumberField(TEXT("angle"));
557
+ AngleWidth = AngleHeight = FMath::DegreesToRadians(AngleDeg);
558
+ }
559
+
560
+ DrawDebugCone(World, Loc, Direction, Length, AngleWidth, AngleHeight,
561
+ 16, DebugColor, false, Duration, 0, Thickness);
562
+ } else if (LowerShapeType == TEXT("capsule")) {
563
+ FQuat Rot = FQuat::Identity;
564
+ if (LocalPayload->HasField(TEXT("rotation"))) {
565
+ const TArray<TSharedPtr<FJsonValue>> *RotArr = nullptr;
566
+ if (LocalPayload->TryGetArrayField(TEXT("rotation"), RotArr) &&
567
+ RotArr && RotArr->Num() >= 3) {
568
+ Rot = FRotator((float)(*RotArr)[0]->AsNumber(),
569
+ (float)(*RotArr)[1]->AsNumber(),
570
+ (float)(*RotArr)[2]->AsNumber())
571
+ .Quaternion();
572
+ }
573
+ }
574
+ float HalfHeight = Size; // Default if not specified
575
+ if (LocalPayload->HasField(TEXT("halfHeight"))) {
576
+ HalfHeight = (float)LocalPayload->GetNumberField(TEXT("halfHeight"));
577
+ }
578
+ DrawDebugCapsule(World, Loc, HalfHeight, Size, Rot, DebugColor, false,
579
+ Duration, 0, Thickness);
580
+ } else if (LowerShapeType == TEXT("arrow")) {
581
+ FVector EndLoc = Loc + FVector(100, 0, 0);
582
+ if (LocalPayload->HasField(TEXT("endLocation"))) {
583
+ // ... parsing logic same as line ...
584
+ const TSharedPtr<FJsonValue> EndVal =
585
+ LocalPayload->TryGetField(TEXT("endLocation"));
586
+ if (EndVal.IsValid()) {
587
+ if (EndVal->Type == EJson::Array) {
588
+ const TArray<TSharedPtr<FJsonValue>> &Arr = EndVal->AsArray();
589
+ if (Arr.Num() >= 3)
590
+ EndLoc = FVector((float)Arr[0]->AsNumber(),
591
+ (float)Arr[1]->AsNumber(),
592
+ (float)Arr[2]->AsNumber());
593
+ } else if (EndVal->Type == EJson::Object) {
594
+ const TSharedPtr<FJsonObject> O = EndVal->AsObject();
595
+ if (O.IsValid())
596
+ EndLoc = FVector((float)(O->HasField(TEXT("x"))
597
+ ? O->GetNumberField(TEXT("x"))
598
+ : 0.0),
599
+ (float)(O->HasField(TEXT("y"))
600
+ ? O->GetNumberField(TEXT("y"))
601
+ : 0.0),
602
+ (float)(O->HasField(TEXT("z"))
603
+ ? O->GetNumberField(TEXT("z"))
604
+ : 0.0));
605
+ }
606
+ }
607
+ }
608
+ float ArrowSize = Size > 0 ? Size : 10.0f;
609
+ DrawDebugDirectionalArrow(World, Loc, EndLoc, ArrowSize, DebugColor,
610
+ false, Duration, 0, Thickness);
611
+ } else if (LowerShapeType == TEXT("plane")) {
612
+ // Draw a simple plane using a box with 0 height or DrawDebugSolidPlane
613
+ // if available but DrawDebugBox is safer for wireframe Using Box with
614
+ // minimal Z thickness
615
+ FVector BoxSize = FVector(Size, Size, 1.0f);
616
+ if (LocalPayload->HasField(TEXT("boxSize"))) {
617
+ // ... parsing ...
618
+ }
619
+ FQuat Rot = FQuat::Identity;
620
+ if (LocalPayload->HasField(TEXT("rotation"))) {
621
+ const TArray<TSharedPtr<FJsonValue>> *RotArr = nullptr;
622
+ if (LocalPayload->TryGetArrayField(TEXT("rotation"), RotArr) &&
623
+ RotArr && RotArr->Num() >= 3) {
624
+ Rot = FRotator((float)(*RotArr)[0]->AsNumber(),
625
+ (float)(*RotArr)[1]->AsNumber(),
626
+ (float)(*RotArr)[2]->AsNumber())
627
+ .Quaternion();
628
+ }
629
+ }
630
+ DrawDebugBox(World, Loc, BoxSize, Rot, DebugColor, false, Duration, 0,
631
+ Thickness);
632
+ } else {
633
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
634
+ Resp->SetBoolField(TEXT("success"), false);
635
+ Resp->SetStringField(
636
+ TEXT("error"),
637
+ FString::Printf(TEXT("Unsupported shape type: %s"), *ShapeType));
638
+ Resp->SetStringField(
639
+ TEXT("supportedShapes"),
640
+ TEXT("sphere, box, circle, line, point, coordinate, cylinder, "
641
+ "cone, capsule, arrow, plane"));
642
+ SendAutomationResponse(RequestingSocket, RequestId, false,
643
+ TEXT("Unsupported shape type"), Resp,
644
+ TEXT("UNSUPPORTED_SHAPE"));
645
+ return true;
646
+ }
647
+
648
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
649
+ Resp->SetBoolField(TEXT("success"), true);
650
+ Resp->SetStringField(TEXT("shapeType"), ShapeType);
651
+ Resp->SetStringField(
652
+ TEXT("location"),
653
+ FString::Printf(TEXT("%.2f,%.2f,%.2f"), Loc.X, Loc.Y, Loc.Z));
654
+ Resp->SetNumberField(TEXT("duration"), Duration);
655
+ SendAutomationResponse(RequestingSocket, RequestId, true,
656
+ TEXT("Debug shape drawn"), Resp, FString());
657
+ return true;
658
+ #else
659
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
660
+ Resp->SetBoolField(TEXT("success"), false);
661
+ Resp->SetStringField(TEXT("error"),
662
+ TEXT("Debug shape drawing requires editor build"));
663
+ Resp->SetStringField(TEXT("shapeType"), ShapeType);
664
+ SendAutomationResponse(
665
+ RequestingSocket, RequestId, false,
666
+ TEXT("Debug shape drawing not available in non-editor build"), Resp,
667
+ TEXT("NOT_AVAILABLE"));
668
+ return true;
669
+ #endif
670
+ }
671
+
672
+ // Handle niagara sub-action (delegates to existing spawn_niagara logic)
673
+ if (LowerSub == TEXT("niagara") || LowerSub == TEXT("spawn_niagara")) {
674
+ // Reuse logic below
675
+ } else if (LowerSub.Equals(TEXT("set_niagara_parameter"))) {
676
+ FString SystemName;
677
+ LocalPayload->TryGetStringField(TEXT("systemName"), SystemName);
678
+ FString ParameterName;
679
+ LocalPayload->TryGetStringField(TEXT("parameterName"), ParameterName);
680
+ FString ParameterType;
681
+ LocalPayload->TryGetStringField(TEXT("parameterType"), ParameterType);
682
+ if (ParameterName.IsEmpty()) {
683
+ SendAutomationResponse(RequestingSocket, RequestId, false,
684
+ TEXT("parameterName required"), nullptr,
685
+ TEXT("INVALID_ARGUMENT"));
686
+ return true;
687
+ }
688
+ if (ParameterType.IsEmpty())
689
+ ParameterType = TEXT("Float");
690
+
691
+ UE_LOG(
692
+ LogMcpAutomationBridgeSubsystem, Verbose,
693
+ TEXT("SetNiagaraParameter: Looking for actor '%s' to set param '%s'"),
694
+ *SystemName, *ParameterName);
695
+
696
+ #if WITH_EDITOR
697
+ if (!GEditor) {
698
+ SendAutomationResponse(RequestingSocket, RequestId, false,
699
+ TEXT("Editor not available"), nullptr,
700
+ TEXT("EDITOR_NOT_AVAILABLE"));
701
+ return true;
702
+ }
703
+ UEditorActorSubsystem *ActorSS =
704
+ GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
705
+ if (!ActorSS) {
706
+ SendAutomationResponse(RequestingSocket, RequestId, false,
707
+ TEXT("EditorActorSubsystem not available"),
708
+ nullptr, TEXT("EDITOR_ACTOR_SUBSYSTEM_MISSING"));
709
+ return true;
710
+ }
711
+
712
+ const FName ParamName(*ParameterName);
713
+ const TSharedPtr<FJsonValue> ValueField =
714
+ LocalPayload->TryGetField(TEXT("value"));
715
+
716
+ TArray<AActor *> AllActors = ActorSS->GetAllLevelActors();
717
+ bool bApplied = false;
718
+
719
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Verbose,
720
+ TEXT("SetNiagaraParameter: Looking for actor '%s'"), *SystemName);
721
+
722
+ bool bActorFound = false;
723
+ bool bComponentFound = false;
724
+
725
+ for (AActor *Actor : AllActors) {
726
+ if (!Actor)
727
+ continue;
728
+ if (!Actor->GetActorLabel().Equals(SystemName, ESearchCase::IgnoreCase))
729
+ continue;
730
+
731
+ bActorFound = true;
732
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Verbose,
733
+ TEXT("SetNiagaraParameter: Found actor '%s'"), *SystemName);
734
+ UNiagaraComponent *NiComp =
735
+ Actor->FindComponentByClass<UNiagaraComponent>();
736
+ if (!NiComp) {
737
+ UE_LOG(
738
+ LogMcpAutomationBridgeSubsystem, Warning,
739
+ TEXT("SetNiagaraParameter: Actor '%s' has no NiagaraComponent"),
740
+ *SystemName);
741
+ // Keep looking? No, actor label is unique-ish. But let's
742
+ // assume unique.
743
+ // But maybe we should break if we found the actor but no component?
744
+ bComponentFound = false;
745
+ break;
746
+ }
747
+ bComponentFound = true;
748
+
749
+ if (ParameterType.Equals(TEXT("Float"), ESearchCase::IgnoreCase)) {
750
+ double NumberValue = 0.0;
751
+ bool bHasNumber =
752
+ LocalPayload->TryGetNumberField(TEXT("value"), NumberValue);
753
+ if (!bHasNumber && ValueField.IsValid()) {
754
+ if (ValueField->Type == EJson::Number) {
755
+ NumberValue = ValueField->AsNumber();
756
+ bHasNumber = true;
757
+ } else if (ValueField->Type == EJson::Object) {
758
+ const TSharedPtr<FJsonObject> Obj = ValueField->AsObject();
759
+ if (Obj.IsValid())
760
+ bHasNumber = Obj->TryGetNumberField(TEXT("v"), NumberValue);
761
+ }
762
+ }
763
+ if (bHasNumber) {
764
+ NiComp->SetVariableFloat(ParamName,
765
+ static_cast<float>(NumberValue));
766
+ bApplied = true;
767
+ }
768
+ } else if (ParameterType.Equals(TEXT("Vector"),
769
+ ESearchCase::IgnoreCase)) {
770
+ const TSharedPtr<FJsonValue> Val =
771
+ LocalPayload->TryGetField(TEXT("value"));
772
+ UE_LOG(
773
+ LogMcpAutomationBridgeSubsystem, Display,
774
+ TEXT("SetNiagaraParameter: DEBUG - Processing Vector for '%s'"),
775
+ *ParamName.ToString());
776
+
777
+ const TArray<TSharedPtr<FJsonValue>> *ArrValue = nullptr;
778
+ const TSharedPtr<FJsonObject> *ObjValue = nullptr;
779
+ if (LocalPayload->TryGetArrayField(TEXT("value"), ArrValue) &&
780
+ ArrValue && ArrValue->Num() >= 3) {
781
+ const float X = static_cast<float>((*ArrValue)[0]->AsNumber());
782
+ const float Y = static_cast<float>((*ArrValue)[1]->AsNumber());
783
+ const float Z = static_cast<float>((*ArrValue)[2]->AsNumber());
784
+ NiComp->SetVariableVec3(ParamName, FVector(X, Y, Z));
785
+ bApplied = true;
786
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
787
+ TEXT("SetNiagaraParameter: DEBUG - Applied Vector from "
788
+ "Array: %f, %f, %f"),
789
+ X, Y, Z);
790
+ } else if (LocalPayload->TryGetObjectField(TEXT("value"), ObjValue) &&
791
+ ObjValue) {
792
+ double VX = 0, VY = 0, VZ = 0;
793
+ (*ObjValue)->TryGetNumberField(TEXT("x"), VX);
794
+ (*ObjValue)->TryGetNumberField(TEXT("y"), VY);
795
+ (*ObjValue)->TryGetNumberField(TEXT("z"), VZ);
796
+ NiComp->SetVariableVec3(ParamName,
797
+ FVector((float)VX, (float)VY, (float)VZ));
798
+ bApplied = true;
799
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
800
+ TEXT("SetNiagaraParameter: DEBUG - Applied Vector from "
801
+ "Object: %f, %f, %f"),
802
+ VX, VY, VZ);
803
+ } else {
804
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Warning,
805
+ TEXT("SetNiagaraParameter: DEBUG - Failed to parse Vector "
806
+ "value."));
807
+ }
808
+ } else if (ParameterType.Equals(TEXT("Color"),
809
+ ESearchCase::IgnoreCase)) {
810
+ const TArray<TSharedPtr<FJsonValue>> *ArrValue = nullptr;
811
+ if (LocalPayload->TryGetArrayField(TEXT("value"), ArrValue) &&
812
+ ArrValue && ArrValue->Num() >= 3) {
813
+ const float R = static_cast<float>((*ArrValue)[0]->AsNumber());
814
+ const float G = static_cast<float>((*ArrValue)[1]->AsNumber());
815
+ const float B = static_cast<float>((*ArrValue)[2]->AsNumber());
816
+ const float Alpha =
817
+ ArrValue->Num() > 3
818
+ ? static_cast<float>((*ArrValue)[3]->AsNumber())
819
+ : 1.0f;
820
+ NiComp->SetVariableLinearColor(ParamName,
821
+ FLinearColor(R, G, B, Alpha));
822
+ bApplied = true;
823
+ }
824
+ } else if (ParameterType.Equals(TEXT("Bool"),
825
+ ESearchCase::IgnoreCase)) {
826
+ bool bValue = false;
827
+ bool bHasBool = LocalPayload->TryGetBoolField(TEXT("value"), bValue);
828
+ if (bHasBool) {
829
+ NiComp->SetVariableBool(ParamName, bValue);
830
+ bApplied = true;
831
+ }
832
+ }
833
+
834
+ // If we found the actor and component but failed to apply, we stop
835
+ // searching.
836
+ break;
837
+ }
838
+
839
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
840
+ Resp->SetBoolField(TEXT("success"), bApplied);
841
+ Resp->SetBoolField(TEXT("applied"), bApplied);
842
+ Resp->SetStringField(TEXT("actorName"), SystemName);
843
+ Resp->SetStringField(TEXT("parameterName"), ParameterName);
844
+ Resp->SetStringField(TEXT("parameterType"), ParameterType);
845
+
846
+ if (bApplied) {
847
+ SendAutomationResponse(RequestingSocket, RequestId, true,
848
+ TEXT("Niagara parameter set"), Resp, FString());
849
+ } else {
850
+ FString ErrMsg = TEXT("Niagara parameter not applied");
851
+ FString ErrCode = TEXT("SET_NIAGARA_PARAM_FAILED");
852
+
853
+ if (!bActorFound) {
854
+ ErrMsg = FString::Printf(TEXT("Actor '%s' not found"), *SystemName);
855
+ ErrCode = TEXT("ACTOR_NOT_FOUND");
856
+ } else if (!bComponentFound) {
857
+ ErrMsg = FString::Printf(TEXT("Actor '%s' has no Niagara component"),
858
+ *SystemName);
859
+ ErrCode = TEXT("COMPONENT_NOT_FOUND");
860
+ } else {
861
+ // Check common failure reasons
862
+ // Invalid Type?
863
+ if (!ParameterType.Equals(TEXT("Float"), ESearchCase::IgnoreCase) &&
864
+ !ParameterType.Equals(TEXT("Vector"), ESearchCase::IgnoreCase) &&
865
+ !ParameterType.Equals(TEXT("Color"), ESearchCase::IgnoreCase) &&
866
+ !ParameterType.Equals(TEXT("Bool"), ESearchCase::IgnoreCase)) {
867
+ ErrMsg = FString::Printf(TEXT("Invalid parameter type: %s"),
868
+ *ParameterType);
869
+ ErrCode = TEXT("INVALID_ARGUMENT");
870
+ }
871
+ }
872
+
873
+ SendAutomationResponse(RequestingSocket, RequestId, false, ErrMsg, Resp,
874
+ ErrCode);
875
+ }
876
+ return true;
877
+ #else
878
+ SendAutomationResponse(
879
+ RequestingSocket, RequestId, false,
880
+ TEXT("set_niagara_parameter requires editor build."), nullptr,
881
+ TEXT("NOT_IMPLEMENTED"));
882
+ return true;
883
+ #endif
884
+ } else if (LowerSub.Equals(TEXT("activate_niagara"))) {
885
+ FString SystemName;
886
+ LocalPayload->TryGetStringField(TEXT("systemName"), SystemName);
887
+ bool bReset = LocalPayload->HasField(TEXT("reset"))
888
+ ? LocalPayload->GetBoolField(TEXT("reset"))
889
+ : true;
890
+
891
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Verbose,
892
+ TEXT("ActivateNiagara: Looking for actor '%s'"), *SystemName);
893
+
894
+ #if WITH_EDITOR
895
+ UEditorActorSubsystem *ActorSS =
896
+ GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
897
+ TArray<AActor *> AllActors = ActorSS->GetAllLevelActors();
898
+ bool bFound = false;
899
+ for (AActor *Actor : AllActors) {
900
+ if (!Actor)
901
+ continue;
902
+ if (!Actor->GetActorLabel().Equals(SystemName, ESearchCase::IgnoreCase))
903
+ continue;
904
+
905
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Verbose,
906
+ TEXT("ActivateNiagara: Found actor '%s'"), *SystemName);
907
+ UNiagaraComponent *NiComp =
908
+ Actor->FindComponentByClass<UNiagaraComponent>();
909
+ if (!NiComp)
910
+ continue;
911
+
912
+ NiComp->Activate(bReset);
913
+ bFound = true;
914
+ break;
915
+ }
916
+ if (bFound) {
917
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
918
+ Resp->SetBoolField(TEXT("success"), true);
919
+ Resp->SetStringField(TEXT("actorName"), SystemName);
920
+ Resp->SetBoolField(TEXT("active"), true);
921
+ SendAutomationResponse(RequestingSocket, RequestId, true,
922
+ TEXT("Niagara system activated."), Resp);
923
+ } else
924
+ SendAutomationResponse(RequestingSocket, RequestId, false,
925
+ TEXT("Niagara system not found."), nullptr,
926
+ TEXT("SYSTEM_NOT_FOUND"));
927
+ return true;
928
+ #else
929
+ SendAutomationResponse(RequestingSocket, RequestId, false,
930
+ TEXT("activate_niagara requires editor build."),
931
+ nullptr, TEXT("NOT_IMPLEMENTED"));
932
+ return true;
933
+ #endif
934
+ } else if (LowerSub.Equals(TEXT("deactivate_niagara"))) {
935
+ FString SystemName;
936
+ LocalPayload->TryGetStringField(TEXT("systemName"), SystemName);
937
+ if (SystemName.IsEmpty())
938
+ LocalPayload->TryGetStringField(TEXT("actorName"), SystemName);
939
+
940
+ #if WITH_EDITOR
941
+ UEditorActorSubsystem *ActorSS =
942
+ GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
943
+ TArray<AActor *> AllActors = ActorSS->GetAllLevelActors();
944
+ bool bFound = false;
945
+ for (AActor *Actor : AllActors) {
946
+ if (!Actor)
947
+ continue;
948
+ if (!Actor->GetActorLabel().Equals(SystemName, ESearchCase::IgnoreCase))
949
+ continue;
950
+
951
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Verbose,
952
+ TEXT("DeactivateNiagara: Found actor '%s'"), *SystemName);
953
+ UNiagaraComponent *NiComp =
954
+ Actor->FindComponentByClass<UNiagaraComponent>();
955
+ if (!NiComp)
956
+ continue;
957
+
958
+ NiComp->Deactivate();
959
+ bFound = true;
960
+ break;
961
+ }
962
+ if (bFound) {
963
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
964
+ Resp->SetBoolField(TEXT("success"), true);
965
+ Resp->SetStringField(TEXT("actorName"), SystemName);
966
+ Resp->SetBoolField(TEXT("active"), false);
967
+ SendAutomationResponse(RequestingSocket, RequestId, true,
968
+ TEXT("Niagara system deactivated."), Resp);
969
+ } else
970
+ SendAutomationResponse(RequestingSocket, RequestId, false,
971
+ TEXT("Niagara system not found."), nullptr,
972
+ TEXT("SYSTEM_NOT_FOUND"));
973
+ return true;
974
+ #else
975
+ SendAutomationResponse(RequestingSocket, RequestId, false,
976
+ TEXT("deactivate_niagara requires editor build."),
977
+ nullptr, TEXT("NOT_IMPLEMENTED"));
978
+ return true;
979
+ #endif
980
+ } else if (LowerSub.Equals(TEXT("advance_simulation"))) {
981
+ FString SystemName;
982
+ LocalPayload->TryGetStringField(TEXT("systemName"), SystemName);
983
+ if (SystemName.IsEmpty())
984
+ LocalPayload->TryGetStringField(TEXT("actorName"), SystemName);
985
+
986
+ double DeltaTime = 0.1;
987
+ LocalPayload->TryGetNumberField(TEXT("deltaTime"), DeltaTime);
988
+ int32 Steps = 1;
989
+ LocalPayload->TryGetNumberField(TEXT("steps"), Steps);
990
+
991
+ #if WITH_EDITOR
992
+ UEditorActorSubsystem *ActorSS =
993
+ GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
994
+ TArray<AActor *> AllActors = ActorSS->GetAllLevelActors();
995
+ bool bFound = false;
996
+ for (AActor *Actor : AllActors) {
997
+ if (!Actor)
998
+ continue;
999
+ if (!Actor->GetActorLabel().Equals(SystemName, ESearchCase::IgnoreCase))
1000
+ continue;
1001
+
1002
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Verbose,
1003
+ TEXT("AdvanceSimulation: Found actor '%s'"), *SystemName);
1004
+ UNiagaraComponent *NiComp =
1005
+ Actor->FindComponentByClass<UNiagaraComponent>();
1006
+ if (!NiComp)
1007
+ continue;
1008
+
1009
+ for (int i = 0; i < Steps; i++) {
1010
+ NiComp->AdvanceSimulation(Steps, DeltaTime);
1011
+ }
1012
+ bFound = true;
1013
+ break;
1014
+ }
1015
+ if (bFound) {
1016
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
1017
+ Resp->SetBoolField(TEXT("success"), true);
1018
+ Resp->SetStringField(TEXT("actorName"), SystemName);
1019
+ Resp->SetNumberField(TEXT("steps"), Steps);
1020
+ SendAutomationResponse(RequestingSocket, RequestId, true,
1021
+ TEXT("Niagara simulation advanced."), Resp);
1022
+ } else
1023
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1024
+ TEXT("Niagara system not found."), nullptr,
1025
+ TEXT("SYSTEM_NOT_FOUND"));
1026
+ return true;
1027
+ #else
1028
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1029
+ TEXT("advance_simulation requires editor build."),
1030
+ nullptr, TEXT("NOT_IMPLEMENTED"));
1031
+ return true;
1032
+ #endif
1033
+ } else if (LowerSub.Equals(TEXT("create_dynamic_light"))) {
1034
+ FString LightName;
1035
+ LocalPayload->TryGetStringField(TEXT("lightName"), LightName);
1036
+ FString LightType;
1037
+ LocalPayload->TryGetStringField(TEXT("lightType"), LightType);
1038
+ if (LightType.IsEmpty())
1039
+ LightType = TEXT("Point");
1040
+
1041
+ // location
1042
+ FVector Loc(0, 0, 0);
1043
+ if (LocalPayload->HasField(TEXT("location"))) {
1044
+ const TSharedPtr<FJsonValue> LocVal =
1045
+ LocalPayload->TryGetField(TEXT("location"));
1046
+ if (LocVal.IsValid()) {
1047
+ if (LocVal->Type == EJson::Array) {
1048
+ const TArray<TSharedPtr<FJsonValue>> &Arr = LocVal->AsArray();
1049
+ if (Arr.Num() >= 3)
1050
+ Loc =
1051
+ FVector((float)Arr[0]->AsNumber(), (float)Arr[1]->AsNumber(),
1052
+ (float)Arr[2]->AsNumber());
1053
+ } else if (LocVal->Type == EJson::Object) {
1054
+ const TSharedPtr<FJsonObject> O = LocVal->AsObject();
1055
+ if (O.IsValid())
1056
+ Loc = FVector(
1057
+ (float)(O->HasField(TEXT("x")) ? O->GetNumberField(TEXT("x"))
1058
+ : 0.0),
1059
+ (float)(O->HasField(TEXT("y")) ? O->GetNumberField(TEXT("y"))
1060
+ : 0.0),
1061
+ (float)(O->HasField(TEXT("z")) ? O->GetNumberField(TEXT("z"))
1062
+ : 0.0));
1063
+ }
1064
+ }
1065
+ }
1066
+
1067
+ double Intensity = 0.0;
1068
+ LocalPayload->TryGetNumberField(TEXT("intensity"), Intensity);
1069
+ // color can be array or object
1070
+ bool bHasColor = false;
1071
+ double Cr = 1.0, Cg = 1.0, Cb = 1.0, Ca = 1.0;
1072
+ if (LocalPayload->HasField(TEXT("color"))) {
1073
+ const TArray<TSharedPtr<FJsonValue>> *ColArr = nullptr;
1074
+ if (LocalPayload->TryGetArrayField(TEXT("color"), ColArr) && ColArr &&
1075
+ ColArr->Num() >= 3) {
1076
+ bHasColor = true;
1077
+ Cr = (*ColArr)[0]->AsNumber();
1078
+ Cg = (*ColArr)[1]->AsNumber();
1079
+ Cb = (*ColArr)[2]->AsNumber();
1080
+ Ca = (ColArr->Num() > 3) ? (*ColArr)[3]->AsNumber() : 1.0;
1081
+ } else {
1082
+ const TSharedPtr<FJsonObject> *CO = nullptr;
1083
+ if (LocalPayload->TryGetObjectField(TEXT("color"), CO) && CO &&
1084
+ (*CO).IsValid()) {
1085
+ bHasColor = true;
1086
+ Cr = (*CO)->HasField(TEXT("r")) ? (*CO)->GetNumberField(TEXT("r"))
1087
+ : Cr;
1088
+ Cg = (*CO)->HasField(TEXT("g")) ? (*CO)->GetNumberField(TEXT("g"))
1089
+ : Cg;
1090
+ Cb = (*CO)->HasField(TEXT("b")) ? (*CO)->GetNumberField(TEXT("b"))
1091
+ : Cb;
1092
+ Ca = (*CO)->HasField(TEXT("a")) ? (*CO)->GetNumberField(TEXT("a"))
1093
+ : Ca;
1094
+ }
1095
+ }
1096
+ }
1097
+
1098
+ // pulse param optional
1099
+ bool bPulseEnabled = false;
1100
+ double PulseFreq = 1.0;
1101
+ if (LocalPayload->HasField(TEXT("pulse"))) {
1102
+ const TSharedPtr<FJsonObject> *P = nullptr;
1103
+ if (LocalPayload->TryGetObjectField(TEXT("pulse"), P) && P &&
1104
+ (*P).IsValid()) {
1105
+ (*P)->TryGetBoolField(TEXT("enabled"), bPulseEnabled);
1106
+ (*P)->TryGetNumberField(TEXT("frequency"), PulseFreq);
1107
+ }
1108
+ }
1109
+
1110
+ #if WITH_EDITOR
1111
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
1112
+ if (!GEditor) {
1113
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1114
+ TEXT("Editor not available"), nullptr,
1115
+ TEXT("EDITOR_NOT_AVAILABLE"));
1116
+ return true;
1117
+ }
1118
+ UEditorActorSubsystem *ActorSS =
1119
+ GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
1120
+ if (!ActorSS) {
1121
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1122
+ TEXT("EditorActorSubsystem not available"),
1123
+ nullptr, TEXT("EDITOR_ACTOR_SUBSYSTEM_MISSING"));
1124
+ return true;
1125
+ }
1126
+
1127
+ UClass *ChosenClass = APointLight::StaticClass();
1128
+ UClass *CompClass = UPointLightComponent::StaticClass();
1129
+ FString LT = LightType.ToLower();
1130
+ if (LT == TEXT("spot") || LT == TEXT("spotlight")) {
1131
+ ChosenClass = ASpotLight::StaticClass();
1132
+ CompClass = USpotLightComponent::StaticClass();
1133
+ } else if (LT == TEXT("directional") || LT == TEXT("directionallight")) {
1134
+ ChosenClass = ADirectionalLight::StaticClass();
1135
+ CompClass = UDirectionalLightComponent::StaticClass();
1136
+ } else if (LT == TEXT("rect") || LT == TEXT("rectlight")) {
1137
+ ChosenClass = ARectLight::StaticClass();
1138
+ CompClass = URectLightComponent::StaticClass();
1139
+ }
1140
+
1141
+ AActor *Spawned = SpawnActorInActiveWorld<AActor>(ChosenClass, Loc,
1142
+ FRotator::ZeroRotator);
1143
+ if (!Spawned) {
1144
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1145
+ TEXT("Failed to spawn light actor"), nullptr,
1146
+ TEXT("CREATE_DYNAMIC_LIGHT_FAILED"));
1147
+ return true;
1148
+ }
1149
+
1150
+ UActorComponent *C = Spawned->GetComponentByClass(CompClass);
1151
+ if (C) {
1152
+ if (ULightComponent *LC = Cast<ULightComponent>(C)) {
1153
+ LC->SetIntensity(static_cast<float>(Intensity));
1154
+ if (bHasColor) {
1155
+ LC->SetLightColor(
1156
+ FLinearColor(static_cast<float>(Cr), static_cast<float>(Cg),
1157
+ static_cast<float>(Cb), static_cast<float>(Ca)));
1158
+ }
1159
+ }
1160
+ }
1161
+
1162
+ if (!LightName.IsEmpty()) {
1163
+ Spawned->SetActorLabel(LightName);
1164
+ }
1165
+ if (bPulseEnabled) {
1166
+ Spawned->Tags.Add(
1167
+ FName(*FString::Printf(TEXT("MCP_PULSE:%g"), PulseFreq)));
1168
+ }
1169
+
1170
+ Resp->SetBoolField(TEXT("success"), true);
1171
+ Resp->SetStringField(TEXT("actor"), Spawned->GetActorLabel());
1172
+ SendAutomationResponse(RequestingSocket, RequestId, true,
1173
+ TEXT("Dynamic light created"), Resp, FString());
1174
+ return true;
1175
+ #else
1176
+ SendAutomationResponse(
1177
+ RequestingSocket, RequestId, false,
1178
+ TEXT("create_dynamic_light requires editor build."), nullptr,
1179
+ TEXT("NOT_IMPLEMENTED"));
1180
+ return true;
1181
+ #endif
1182
+ } else if (LowerSub.Equals(TEXT("cleanup"))) {
1183
+ FString Filter;
1184
+ LocalPayload->TryGetStringField(TEXT("filter"), Filter);
1185
+ if (Filter.IsEmpty()) {
1186
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
1187
+ Resp->SetNumberField(TEXT("removed"), 0);
1188
+ SendAutomationResponse(RequestingSocket, RequestId, true,
1189
+ TEXT("Cleanup skipped (empty filter)"), Resp,
1190
+ FString());
1191
+ return true;
1192
+ }
1193
+ #if WITH_EDITOR
1194
+ if (!GEditor) {
1195
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1196
+ TEXT("Editor not available"), nullptr,
1197
+ TEXT("EDITOR_NOT_AVAILABLE"));
1198
+ return true;
1199
+ }
1200
+ UEditorActorSubsystem *ActorSS =
1201
+ GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
1202
+ if (!ActorSS) {
1203
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1204
+ TEXT("EditorActorSubsystem not available"),
1205
+ nullptr, TEXT("EDITOR_ACTOR_SUBSYSTEM_MISSING"));
1206
+ return true;
1207
+ }
1208
+ TArray<AActor *> Actors = ActorSS->GetAllLevelActors();
1209
+ TArray<FString> Removed;
1210
+ for (AActor *A : Actors) {
1211
+ if (!A)
1212
+ continue;
1213
+ FString Label = A->GetActorLabel();
1214
+ if (Label.IsEmpty())
1215
+ continue;
1216
+ if (!Label.StartsWith(Filter, ESearchCase::IgnoreCase))
1217
+ continue;
1218
+ bool bDel = ActorSS->DestroyActor(A);
1219
+ if (bDel)
1220
+ Removed.Add(Label);
1221
+ }
1222
+ TArray<TSharedPtr<FJsonValue>> Arr;
1223
+ for (const FString &S : Removed)
1224
+ Arr.Add(MakeShared<FJsonValueString>(S));
1225
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
1226
+ Resp->SetArrayField(TEXT("removedActors"), Arr);
1227
+ Resp->SetNumberField(TEXT("removed"), Removed.Num());
1228
+ SendAutomationResponse(
1229
+ RequestingSocket, RequestId, true,
1230
+ FString::Printf(TEXT("Cleanup completed (removed=%d)"),
1231
+ Removed.Num()),
1232
+ Resp, FString());
1233
+ return true;
1234
+ #else
1235
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1236
+ TEXT("cleanup requires editor build."), nullptr,
1237
+ TEXT("NOT_IMPLEMENTED"));
1238
+ return true;
1239
+ #endif
1240
+ }
1241
+ }
1242
+
1243
+ // Spawn Niagara system in-level as a NiagaraActor (editor-only)
1244
+ bool bSpawnNiagara = Lower.Equals(TEXT("spawn_niagara"));
1245
+ if (bIsCreateEffect) {
1246
+ FString Sub;
1247
+ LocalPayload->TryGetStringField(TEXT("action"), Sub);
1248
+ FString LowerSub = Sub.ToLower();
1249
+ if (LowerSub == TEXT("niagara") || LowerSub == TEXT("spawn_niagara"))
1250
+ bSpawnNiagara = true;
1251
+ // If SubAction is empty and Action is create_effect, we fallthrough to
1252
+ // legacy behavior below
1253
+ }
1254
+
1255
+ if (bSpawnNiagara) {
1256
+ FString SystemPath;
1257
+ LocalPayload->TryGetStringField(TEXT("systemPath"), SystemPath);
1258
+ if (SystemPath.IsEmpty()) {
1259
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1260
+ TEXT("systemPath required"), nullptr,
1261
+ TEXT("INVALID_ARGUMENT"));
1262
+ return true;
1263
+ }
1264
+
1265
+ // Guard against non-existent assets to prevent LoadPackage warnings
1266
+ if (!UEditorAssetLibrary::DoesAssetExist(SystemPath)) {
1267
+ SendAutomationResponse(
1268
+ RequestingSocket, RequestId, false,
1269
+ FString::Printf(TEXT("Niagara system asset not found: %s"),
1270
+ *SystemPath),
1271
+ nullptr, TEXT("SYSTEM_NOT_FOUND"));
1272
+ return true;
1273
+ }
1274
+
1275
+ // Location and optional rotation/scale
1276
+ FVector Loc(0, 0, 0);
1277
+ if (LocalPayload->HasField(TEXT("location"))) {
1278
+ const TSharedPtr<FJsonValue> LocVal =
1279
+ LocalPayload->TryGetField(TEXT("location"));
1280
+ if (LocVal.IsValid()) {
1281
+ if (LocVal->Type == EJson::Array) {
1282
+ const TArray<TSharedPtr<FJsonValue>> &Arr = LocVal->AsArray();
1283
+ if (Arr.Num() >= 3)
1284
+ Loc = FVector((float)Arr[0]->AsNumber(), (float)Arr[1]->AsNumber(),
1285
+ (float)Arr[2]->AsNumber());
1286
+ } else if (LocVal->Type == EJson::Object) {
1287
+ const TSharedPtr<FJsonObject> O = LocVal->AsObject();
1288
+ if (O.IsValid())
1289
+ Loc = FVector(
1290
+ (float)(O->HasField(TEXT("x")) ? O->GetNumberField(TEXT("x"))
1291
+ : 0.0),
1292
+ (float)(O->HasField(TEXT("y")) ? O->GetNumberField(TEXT("y"))
1293
+ : 0.0),
1294
+ (float)(O->HasField(TEXT("z")) ? O->GetNumberField(TEXT("z"))
1295
+ : 0.0));
1296
+ }
1297
+ }
1298
+ }
1299
+
1300
+ // Rotation may be an array
1301
+ TArray<double> RotArr = {0, 0, 0};
1302
+ const TArray<TSharedPtr<FJsonValue>> *RA = nullptr;
1303
+ if (LocalPayload->TryGetArrayField(TEXT("rotation"), RA) && RA &&
1304
+ RA->Num() >= 3) {
1305
+ RotArr[0] = (*RA)[0]->AsNumber();
1306
+ RotArr[1] = (*RA)[1]->AsNumber();
1307
+ RotArr[2] = (*RA)[2]->AsNumber();
1308
+ }
1309
+
1310
+ // Scale may be an array or a single numeric value
1311
+ TArray<double> ScaleArr = {1, 1, 1};
1312
+ const TArray<TSharedPtr<FJsonValue>> *ScaleJsonArr = nullptr;
1313
+ if (LocalPayload->TryGetArrayField(TEXT("scale"), ScaleJsonArr) &&
1314
+ ScaleJsonArr && ScaleJsonArr->Num() >= 3) {
1315
+ ScaleArr[0] = (*ScaleJsonArr)[0]->AsNumber();
1316
+ ScaleArr[1] = (*ScaleJsonArr)[1]->AsNumber();
1317
+ ScaleArr[2] = (*ScaleJsonArr)[2]->AsNumber();
1318
+ } else if (LocalPayload->TryGetNumberField(TEXT("scale"), ScaleArr[0])) {
1319
+ ScaleArr[1] = ScaleArr[2] = ScaleArr[0];
1320
+ }
1321
+
1322
+ const bool bAutoDestroy =
1323
+ LocalPayload->HasField(TEXT("autoDestroy"))
1324
+ ? LocalPayload->GetBoolField(TEXT("autoDestroy"))
1325
+ : false;
1326
+ FString AttachToActor;
1327
+ LocalPayload->TryGetStringField(TEXT("attachToActor"), AttachToActor);
1328
+
1329
+ #if WITH_EDITOR
1330
+ if (!GEditor) {
1331
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1332
+ TEXT("Editor not available"), nullptr,
1333
+ TEXT("EDITOR_NOT_AVAILABLE"));
1334
+ return true;
1335
+ }
1336
+ UEditorActorSubsystem *ActorSS =
1337
+ GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
1338
+ if (!ActorSS) {
1339
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1340
+ TEXT("EditorActorSubsystem not available"),
1341
+ nullptr, TEXT("EDITOR_ACTOR_SUBSYSTEM_MISSING"));
1342
+ return true;
1343
+ }
1344
+
1345
+ UObject *NiagObj = UEditorAssetLibrary::LoadAsset(SystemPath);
1346
+ if (!NiagObj) {
1347
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
1348
+ Resp->SetBoolField(TEXT("success"), false);
1349
+ Resp->SetStringField(TEXT("error"),
1350
+ TEXT("Niagara system asset not found"));
1351
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1352
+ TEXT("Niagara system not found"), Resp,
1353
+ TEXT("SYSTEM_NOT_FOUND"));
1354
+ return true;
1355
+ }
1356
+
1357
+ const FRotator SpawnRot(static_cast<float>(RotArr[0]),
1358
+ static_cast<float>(RotArr[1]),
1359
+ static_cast<float>(RotArr[2]));
1360
+ AActor *Spawned = SpawnActorInActiveWorld<AActor>(
1361
+ ANiagaraActor::StaticClass(), Loc, SpawnRot);
1362
+ if (!Spawned) {
1363
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1364
+ TEXT("Failed to spawn NiagaraActor"), nullptr,
1365
+ TEXT("SPAWN_FAILED"));
1366
+ return true;
1367
+ }
1368
+
1369
+ UNiagaraComponent *NiComp =
1370
+ Spawned->FindComponentByClass<UNiagaraComponent>();
1371
+ if (NiComp && NiagObj->IsA<UNiagaraSystem>()) {
1372
+ NiComp->SetAsset(Cast<UNiagaraSystem>(NiagObj));
1373
+ NiComp->SetWorldScale3D(FVector(ScaleArr[0], ScaleArr[1], ScaleArr[2]));
1374
+ NiComp->Activate(true); // Set to true
1375
+ }
1376
+
1377
+ if (!AttachToActor.IsEmpty()) {
1378
+ AActor *Parent = nullptr;
1379
+ TArray<AActor *> AllActors = ActorSS->GetAllLevelActors();
1380
+ for (AActor *A : AllActors) {
1381
+ if (A &&
1382
+ A->GetActorLabel().Equals(AttachToActor, ESearchCase::IgnoreCase)) {
1383
+ Parent = A;
1384
+ break;
1385
+ }
1386
+ }
1387
+ if (Parent) {
1388
+ Spawned->AttachToActor(Parent,
1389
+ FAttachmentTransformRules::KeepWorldTransform);
1390
+ }
1391
+ }
1392
+
1393
+ // Set actor label
1394
+ FString Name;
1395
+ LocalPayload->TryGetStringField(TEXT("name"), Name);
1396
+ if (Name.IsEmpty())
1397
+ LocalPayload->TryGetStringField(TEXT("actorName"), Name);
1398
+
1399
+ if (!Name.IsEmpty()) {
1400
+ Spawned->SetActorLabel(Name);
1401
+ } else {
1402
+ Spawned->SetActorLabel(FString::Printf(
1403
+ TEXT("Niagara_%lld"), FDateTime::Now().ToUnixTimestamp()));
1404
+ }
1405
+
1406
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
1407
+ TEXT("spawn_niagara: Spawned actor '%s' (ID: %u)"),
1408
+ *Spawned->GetActorLabel(), Spawned->GetUniqueID());
1409
+
1410
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
1411
+ Resp->SetBoolField(TEXT("success"), true);
1412
+ Resp->SetStringField(TEXT("actor"), Spawned->GetActorLabel());
1413
+ SendAutomationResponse(RequestingSocket, RequestId, true,
1414
+ TEXT("Niagara spawned"), Resp, FString());
1415
+ return true;
1416
+ #else
1417
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1418
+ TEXT("spawn_niagara requires editor build."),
1419
+ nullptr, TEXT("NOT_IMPLEMENTED"));
1420
+ return true;
1421
+ #endif
1422
+ }
1423
+
1424
+ // CLEANUP EFFECTS - remove actors whose label starts with the provided filter
1425
+ // (editor-only)
1426
+ bool bCleanup = Lower.Equals(TEXT("cleanup"));
1427
+ if (bIsCreateEffect) {
1428
+ FString Sub;
1429
+ LocalPayload->TryGetStringField(TEXT("action"), Sub);
1430
+ if (Sub.ToLower() == TEXT("cleanup"))
1431
+ bCleanup = true;
1432
+ }
1433
+
1434
+ if (bCleanup) {
1435
+ FString Filter;
1436
+ LocalPayload->TryGetStringField(TEXT("filter"), Filter);
1437
+ // Allow empty filter as a no-op success
1438
+ if (Filter.IsEmpty()) {
1439
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
1440
+ Resp->SetNumberField(TEXT("removed"), 0);
1441
+ SendAutomationResponse(RequestingSocket, RequestId, true,
1442
+ TEXT("Cleanup skipped (empty filter)"), Resp,
1443
+ FString());
1444
+ return true;
1445
+ }
1446
+ #if WITH_EDITOR
1447
+ if (!GEditor) {
1448
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1449
+ TEXT("Editor not available"), nullptr,
1450
+ TEXT("EDITOR_NOT_AVAILABLE"));
1451
+ return true;
1452
+ }
1453
+ UEditorActorSubsystem *ActorSS =
1454
+ GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
1455
+ if (!ActorSS) {
1456
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1457
+ TEXT("EditorActorSubsystem not available"),
1458
+ nullptr, TEXT("EDITOR_ACTOR_SUBSYSTEM_MISSING"));
1459
+ return true;
1460
+ }
1461
+ TArray<AActor *> Actors = ActorSS->GetAllLevelActors();
1462
+ TArray<FString> Removed;
1463
+ for (AActor *A : Actors) {
1464
+ if (!A) {
1465
+ continue;
1466
+ }
1467
+ FString Label = A->GetActorLabel();
1468
+ if (Label.IsEmpty()) {
1469
+ continue;
1470
+ }
1471
+ if (!Label.StartsWith(Filter, ESearchCase::IgnoreCase)) {
1472
+ continue;
1473
+ }
1474
+ bool bDel = ActorSS->DestroyActor(A);
1475
+ if (bDel) {
1476
+ Removed.Add(Label);
1477
+ }
1478
+ }
1479
+ TArray<TSharedPtr<FJsonValue>> Arr;
1480
+ for (const FString &S : Removed) {
1481
+ Arr.Add(MakeShared<FJsonValueString>(S));
1482
+ }
1483
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
1484
+ Resp->SetArrayField(TEXT("removedActors"), Arr);
1485
+ Resp->SetNumberField(TEXT("removed"), Removed.Num());
1486
+ SendAutomationResponse(
1487
+ RequestingSocket, RequestId, true,
1488
+ FString::Printf(TEXT("Cleanup completed (removed=%d)"), Removed.Num()),
1489
+ Resp, FString());
1490
+ return true;
1491
+ #else
1492
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1493
+ TEXT("cleanup requires editor build."), nullptr,
1494
+ TEXT("NOT_IMPLEMENTED"));
1495
+ return true;
1496
+ #endif
1497
+ }
1498
+
1499
+ // STUB HANDLERS FOR TEST COVERAGE - NOW IMPLEMENTED
1500
+ bool bCreateRibbon = Lower.Equals(TEXT("create_niagara_ribbon"));
1501
+ bool bCreateFog = Lower.Equals(TEXT("create_volumetric_fog"));
1502
+ bool bCreateTrail = Lower.Equals(TEXT("create_particle_trail"));
1503
+ bool bCreateEnv = Lower.Equals(TEXT("create_environment_effect"));
1504
+ bool bCreateImpact = Lower.Equals(TEXT("create_impact_effect"));
1505
+
1506
+ if (bIsCreateEffect) {
1507
+ FString Sub;
1508
+ LocalPayload->TryGetStringField(TEXT("action"), Sub);
1509
+ FString LSub = Sub.ToLower();
1510
+ if (LSub == TEXT("create_niagara_ribbon"))
1511
+ bCreateRibbon = true;
1512
+ if (LSub == TEXT("create_volumetric_fog"))
1513
+ bCreateFog = true;
1514
+ if (LSub == TEXT("create_particle_trail"))
1515
+ bCreateTrail = true;
1516
+ if (LSub == TEXT("create_environment_effect"))
1517
+ bCreateEnv = true;
1518
+ if (LSub == TEXT("create_impact_effect"))
1519
+ bCreateImpact = true;
1520
+ }
1521
+
1522
+ if (bCreateRibbon) {
1523
+ // Require systemPath
1524
+ return CreateNiagaraEffect(RequestId, Payload, RequestingSocket,
1525
+ TEXT("create_niagara_ribbon"), FString());
1526
+ }
1527
+ if (bCreateFog) {
1528
+ return CreateNiagaraEffect(RequestId, Payload, RequestingSocket,
1529
+ TEXT("create_volumetric_fog"), FString());
1530
+ }
1531
+ if (bCreateTrail) {
1532
+ return CreateNiagaraEffect(RequestId, Payload, RequestingSocket,
1533
+ TEXT("create_particle_trail"), FString());
1534
+ }
1535
+ if (bCreateEnv) {
1536
+ return CreateNiagaraEffect(RequestId, Payload, RequestingSocket,
1537
+ TEXT("create_environment_effect"), FString());
1538
+ }
1539
+ if (bCreateImpact) {
1540
+ return CreateNiagaraEffect(RequestId, Payload, RequestingSocket,
1541
+ TEXT("create_impact_effect"), FString());
1542
+ }
1543
+
1544
+ return false;
1545
+ }
1546
+
1547
+ // Helper function to create Niagara effects with default systems
1548
+ bool UMcpAutomationBridgeSubsystem::CreateNiagaraEffect(
1549
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
1550
+ TSharedPtr<FMcpBridgeWebSocket> RequestingSocket, const FString &EffectName,
1551
+ const FString &DefaultSystemPath) {
1552
+ #if WITH_EDITOR
1553
+ if (!GEditor) {
1554
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
1555
+ Resp->SetBoolField(TEXT("success"), false);
1556
+ Resp->SetStringField(TEXT("error"), TEXT("Editor not available"));
1557
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1558
+ TEXT("Editor not available"), Resp,
1559
+ TEXT("EDITOR_NOT_AVAILABLE"));
1560
+ return true;
1561
+ }
1562
+ UEditorActorSubsystem *ActorSS =
1563
+ GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
1564
+ if (!ActorSS) {
1565
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
1566
+ Resp->SetBoolField(TEXT("success"), false);
1567
+ Resp->SetStringField(TEXT("error"),
1568
+ TEXT("EditorActorSubsystem not available"));
1569
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1570
+ TEXT("EditorActorSubsystem not available"), Resp,
1571
+ TEXT("EDITOR_ACTOR_SUBSYSTEM_MISSING"));
1572
+ return true;
1573
+ }
1574
+
1575
+ // Get custom system path or use default
1576
+ FString SystemPath = DefaultSystemPath;
1577
+ Payload->TryGetStringField(TEXT("systemPath"), SystemPath);
1578
+
1579
+ if (SystemPath.IsEmpty()) {
1580
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
1581
+ Resp->SetBoolField(TEXT("success"), false);
1582
+ Resp->SetStringField(
1583
+ TEXT("error"),
1584
+ FString::Printf(TEXT("systemPath is required for %s. Please provide a "
1585
+ "valid asset path (e.g. /Game/Effects/MySystem)"),
1586
+ *EffectName));
1587
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1588
+ TEXT("systemPath required"), Resp,
1589
+ TEXT("INVALID_ARGUMENT"));
1590
+ return true;
1591
+ }
1592
+
1593
+ // Location
1594
+ FVector Loc(0, 0, 0);
1595
+ if (Payload->HasField(TEXT("location"))) {
1596
+ const TSharedPtr<FJsonValue> LocVal =
1597
+ Payload->TryGetField(TEXT("location"));
1598
+ if (LocVal.IsValid()) {
1599
+ if (LocVal->Type == EJson::Array) {
1600
+ const TArray<TSharedPtr<FJsonValue>> &Arr = LocVal->AsArray();
1601
+ if (Arr.Num() >= 3)
1602
+ Loc = FVector((float)Arr[0]->AsNumber(), (float)Arr[1]->AsNumber(),
1603
+ (float)Arr[2]->AsNumber());
1604
+ } else if (LocVal->Type == EJson::Object) {
1605
+ const TSharedPtr<FJsonObject> O = LocVal->AsObject();
1606
+ if (O.IsValid())
1607
+ Loc = FVector(
1608
+ (float)(O->HasField(TEXT("x")) ? O->GetNumberField(TEXT("x"))
1609
+ : 0.0),
1610
+ (float)(O->HasField(TEXT("y")) ? O->GetNumberField(TEXT("y"))
1611
+ : 0.0),
1612
+ (float)(O->HasField(TEXT("z")) ? O->GetNumberField(TEXT("z"))
1613
+ : 0.0));
1614
+ }
1615
+ }
1616
+ }
1617
+
1618
+ // Load the Niagara system
1619
+ if (!UEditorAssetLibrary::DoesAssetExist(SystemPath)) {
1620
+ SendAutomationResponse(
1621
+ RequestingSocket, RequestId, false,
1622
+ FString::Printf(TEXT("Niagara system asset not found: %s"),
1623
+ *SystemPath),
1624
+ nullptr, TEXT("SYSTEM_NOT_FOUND"));
1625
+ return true;
1626
+ }
1627
+
1628
+ UObject *NiagObj = UEditorAssetLibrary::LoadAsset(SystemPath);
1629
+ if (!NiagObj) {
1630
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
1631
+ Resp->SetBoolField(TEXT("success"), false);
1632
+ Resp->SetStringField(TEXT("error"), TEXT("Niagara system asset not found"));
1633
+ Resp->SetStringField(TEXT("systemPath"), SystemPath);
1634
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1635
+ TEXT("Niagara system not found"), Resp,
1636
+ TEXT("SYSTEM_NOT_FOUND"));
1637
+ return true;
1638
+ }
1639
+
1640
+ // Spawn the actor
1641
+ AActor *Spawned = SpawnActorInActiveWorld<AActor>(
1642
+ ANiagaraActor::StaticClass(), Loc, FRotator::ZeroRotator);
1643
+ if (!Spawned) {
1644
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
1645
+ Resp->SetBoolField(TEXT("success"), false);
1646
+ Resp->SetStringField(TEXT("error"), TEXT("Failed to spawn Niagara actor"));
1647
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1648
+ TEXT("Failed to spawn Niagara actor"), Resp,
1649
+ TEXT("SPAWN_FAILED"));
1650
+ return true;
1651
+ }
1652
+
1653
+ // Configure the Niagara component
1654
+ UNiagaraComponent *NiComp =
1655
+ Spawned->FindComponentByClass<UNiagaraComponent>();
1656
+ if (NiComp && NiagObj->IsA<UNiagaraSystem>()) {
1657
+ NiComp->SetAsset(Cast<UNiagaraSystem>(NiagObj));
1658
+ NiComp->Activate(true);
1659
+ }
1660
+
1661
+ // Set actor label
1662
+ FString Name;
1663
+ Payload->TryGetStringField(TEXT("name"), Name);
1664
+ if (Name.IsEmpty())
1665
+ Payload->TryGetStringField(TEXT("actorName"), Name);
1666
+
1667
+ if (!Name.IsEmpty()) {
1668
+ Spawned->SetActorLabel(Name);
1669
+ } else {
1670
+ Spawned->SetActorLabel(FString::Printf(
1671
+ TEXT("%s_%lld"), *EffectName.Replace(TEXT("create_"), TEXT("")),
1672
+ FDateTime::Now().ToUnixTimestamp()));
1673
+ }
1674
+
1675
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Verbose,
1676
+ TEXT("CreateNiagaraEffect: Spawned actor '%s' (ID: %u)"),
1677
+ *Spawned->GetActorLabel(), Spawned->GetUniqueID());
1678
+
1679
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
1680
+ Resp->SetBoolField(TEXT("success"), true);
1681
+ Resp->SetStringField(TEXT("effectType"), EffectName);
1682
+ Resp->SetStringField(TEXT("systemPath"), SystemPath);
1683
+ Resp->SetStringField(TEXT("actorName"), Spawned->GetActorLabel());
1684
+ Resp->SetNumberField(TEXT("actorId"), Spawned->GetUniqueID());
1685
+ SendAutomationResponse(
1686
+ RequestingSocket, RequestId, true,
1687
+ FString::Printf(TEXT("%s created successfully"), *EffectName), Resp,
1688
+ FString());
1689
+ return true;
1690
+ #else
1691
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
1692
+ Resp->SetBoolField(TEXT("success"), false);
1693
+ Resp->SetStringField(TEXT("error"),
1694
+ TEXT("Effect creation requires editor build"));
1695
+ SendAutomationResponse(
1696
+ RequestingSocket, RequestId, false,
1697
+ TEXT("Effect creation not available in non-editor build"), Resp,
1698
+ TEXT("NOT_AVAILABLE"));
1699
+ return true;
1700
+ #endif
1701
+ }