unreal-engine-mcp-server 0.4.7 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (438) 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.yml +148 -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 +23 -0
  19. package/.github/workflows/labeler.yml +16 -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 +12 -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 +267 -31
  29. package/CONTRIBUTING.md +140 -0
  30. package/README.md +166 -71
  31. package/claude_desktop_config_example.json +7 -6
  32. package/dist/automation/bridge.d.ts +50 -0
  33. package/dist/automation/bridge.js +452 -0
  34. package/dist/automation/connection-manager.d.ts +23 -0
  35. package/dist/automation/connection-manager.js +107 -0
  36. package/dist/automation/handshake.d.ts +11 -0
  37. package/dist/automation/handshake.js +89 -0
  38. package/dist/automation/index.d.ts +3 -0
  39. package/dist/automation/index.js +3 -0
  40. package/dist/automation/message-handler.d.ts +12 -0
  41. package/dist/automation/message-handler.js +149 -0
  42. package/dist/automation/request-tracker.d.ts +25 -0
  43. package/dist/automation/request-tracker.js +98 -0
  44. package/dist/automation/types.d.ts +130 -0
  45. package/dist/automation/types.js +2 -0
  46. package/dist/cli.js +32 -5
  47. package/dist/config.d.ts +27 -0
  48. package/dist/config.js +60 -0
  49. package/dist/constants.d.ts +12 -0
  50. package/dist/constants.js +12 -0
  51. package/dist/graphql/resolvers.d.ts +268 -0
  52. package/dist/graphql/resolvers.js +743 -0
  53. package/dist/graphql/schema.d.ts +5 -0
  54. package/dist/graphql/schema.js +437 -0
  55. package/dist/graphql/server.d.ts +26 -0
  56. package/dist/graphql/server.js +115 -0
  57. package/dist/graphql/types.d.ts +7 -0
  58. package/dist/graphql/types.js +2 -0
  59. package/dist/handlers/resource-handlers.d.ts +20 -0
  60. package/dist/handlers/resource-handlers.js +180 -0
  61. package/dist/index.d.ts +31 -18
  62. package/dist/index.js +119 -619
  63. package/dist/prompts/index.js +4 -4
  64. package/dist/resources/actors.d.ts +17 -12
  65. package/dist/resources/actors.js +56 -76
  66. package/dist/resources/assets.d.ts +6 -14
  67. package/dist/resources/assets.js +115 -147
  68. package/dist/resources/levels.d.ts +13 -13
  69. package/dist/resources/levels.js +25 -34
  70. package/dist/server/resource-registry.d.ts +20 -0
  71. package/dist/server/resource-registry.js +37 -0
  72. package/dist/server/tool-registry.d.ts +23 -0
  73. package/dist/server/tool-registry.js +322 -0
  74. package/dist/server-setup.d.ts +21 -0
  75. package/dist/server-setup.js +111 -0
  76. package/dist/services/health-monitor.d.ts +34 -0
  77. package/dist/services/health-monitor.js +105 -0
  78. package/dist/services/metrics-server.d.ts +11 -0
  79. package/dist/services/metrics-server.js +105 -0
  80. package/dist/tools/actors.d.ts +147 -9
  81. package/dist/tools/actors.js +350 -311
  82. package/dist/tools/animation.d.ts +135 -4
  83. package/dist/tools/animation.js +510 -411
  84. package/dist/tools/assets.d.ts +117 -19
  85. package/dist/tools/assets.js +259 -284
  86. package/dist/tools/audio.d.ts +102 -42
  87. package/dist/tools/audio.js +272 -685
  88. package/dist/tools/base-tool.d.ts +17 -0
  89. package/dist/tools/base-tool.js +46 -0
  90. package/dist/tools/behavior-tree.d.ts +94 -0
  91. package/dist/tools/behavior-tree.js +39 -0
  92. package/dist/tools/blueprint/helpers.d.ts +29 -0
  93. package/dist/tools/blueprint/helpers.js +182 -0
  94. package/dist/tools/blueprint.d.ts +228 -118
  95. package/dist/tools/blueprint.js +685 -832
  96. package/dist/tools/consolidated-tool-definitions.d.ts +5462 -1781
  97. package/dist/tools/consolidated-tool-definitions.js +829 -496
  98. package/dist/tools/consolidated-tool-handlers.d.ts +2 -1
  99. package/dist/tools/consolidated-tool-handlers.js +211 -1026
  100. package/dist/tools/debug.d.ts +143 -85
  101. package/dist/tools/debug.js +234 -180
  102. package/dist/tools/dynamic-handler-registry.d.ts +11 -0
  103. package/dist/tools/dynamic-handler-registry.js +101 -0
  104. package/dist/tools/editor.d.ts +139 -18
  105. package/dist/tools/editor.js +239 -244
  106. package/dist/tools/engine.d.ts +10 -4
  107. package/dist/tools/engine.js +13 -5
  108. package/dist/tools/environment.d.ts +36 -0
  109. package/dist/tools/environment.js +267 -0
  110. package/dist/tools/foliage.d.ts +105 -14
  111. package/dist/tools/foliage.js +219 -331
  112. package/dist/tools/handlers/actor-handlers.d.ts +3 -0
  113. package/dist/tools/handlers/actor-handlers.js +232 -0
  114. package/dist/tools/handlers/animation-handlers.d.ts +3 -0
  115. package/dist/tools/handlers/animation-handlers.js +185 -0
  116. package/dist/tools/handlers/argument-helper.d.ts +16 -0
  117. package/dist/tools/handlers/argument-helper.js +80 -0
  118. package/dist/tools/handlers/asset-handlers.d.ts +3 -0
  119. package/dist/tools/handlers/asset-handlers.js +496 -0
  120. package/dist/tools/handlers/audio-handlers.d.ts +3 -0
  121. package/dist/tools/handlers/audio-handlers.js +166 -0
  122. package/dist/tools/handlers/blueprint-handlers.d.ts +4 -0
  123. package/dist/tools/handlers/blueprint-handlers.js +358 -0
  124. package/dist/tools/handlers/common-handlers.d.ts +14 -0
  125. package/dist/tools/handlers/common-handlers.js +56 -0
  126. package/dist/tools/handlers/editor-handlers.d.ts +3 -0
  127. package/dist/tools/handlers/editor-handlers.js +119 -0
  128. package/dist/tools/handlers/effect-handlers.d.ts +3 -0
  129. package/dist/tools/handlers/effect-handlers.js +171 -0
  130. package/dist/tools/handlers/environment-handlers.d.ts +3 -0
  131. package/dist/tools/handlers/environment-handlers.js +170 -0
  132. package/dist/tools/handlers/graph-handlers.d.ts +3 -0
  133. package/dist/tools/handlers/graph-handlers.js +90 -0
  134. package/dist/tools/handlers/input-handlers.d.ts +3 -0
  135. package/dist/tools/handlers/input-handlers.js +21 -0
  136. package/dist/tools/handlers/inspect-handlers.d.ts +3 -0
  137. package/dist/tools/handlers/inspect-handlers.js +383 -0
  138. package/dist/tools/handlers/level-handlers.d.ts +3 -0
  139. package/dist/tools/handlers/level-handlers.js +237 -0
  140. package/dist/tools/handlers/lighting-handlers.d.ts +3 -0
  141. package/dist/tools/handlers/lighting-handlers.js +144 -0
  142. package/dist/tools/handlers/performance-handlers.d.ts +3 -0
  143. package/dist/tools/handlers/performance-handlers.js +130 -0
  144. package/dist/tools/handlers/pipeline-handlers.d.ts +3 -0
  145. package/dist/tools/handlers/pipeline-handlers.js +110 -0
  146. package/dist/tools/handlers/sequence-handlers.d.ts +3 -0
  147. package/dist/tools/handlers/sequence-handlers.js +376 -0
  148. package/dist/tools/handlers/system-handlers.d.ts +4 -0
  149. package/dist/tools/handlers/system-handlers.js +506 -0
  150. package/dist/tools/input.d.ts +19 -0
  151. package/dist/tools/input.js +89 -0
  152. package/dist/tools/introspection.d.ts +103 -40
  153. package/dist/tools/introspection.js +425 -568
  154. package/dist/tools/landscape.d.ts +97 -36
  155. package/dist/tools/landscape.js +280 -409
  156. package/dist/tools/level.d.ts +130 -10
  157. package/dist/tools/level.js +639 -675
  158. package/dist/tools/lighting.d.ts +77 -38
  159. package/dist/tools/lighting.js +441 -943
  160. package/dist/tools/logs.d.ts +3 -3
  161. package/dist/tools/logs.js +5 -57
  162. package/dist/tools/materials.d.ts +91 -24
  163. package/dist/tools/materials.js +190 -118
  164. package/dist/tools/niagara.d.ts +149 -39
  165. package/dist/tools/niagara.js +232 -182
  166. package/dist/tools/performance.d.ts +27 -12
  167. package/dist/tools/performance.js +204 -122
  168. package/dist/tools/physics.d.ts +32 -77
  169. package/dist/tools/physics.js +171 -582
  170. package/dist/tools/property-dictionary.d.ts +13 -0
  171. package/dist/tools/property-dictionary.js +82 -0
  172. package/dist/tools/sequence.d.ts +73 -48
  173. package/dist/tools/sequence.js +196 -748
  174. package/dist/tools/tool-definition-utils.d.ts +59 -0
  175. package/dist/tools/tool-definition-utils.js +35 -0
  176. package/dist/tools/ui.d.ts +66 -34
  177. package/dist/tools/ui.js +134 -214
  178. package/dist/types/env.d.ts +0 -3
  179. package/dist/types/env.js +0 -7
  180. package/dist/types/tool-interfaces.d.ts +898 -0
  181. package/dist/types/tool-interfaces.js +2 -0
  182. package/dist/types/tool-types.d.ts +183 -19
  183. package/dist/types/tool-types.js +0 -4
  184. package/dist/unreal-bridge.d.ts +24 -131
  185. package/dist/unreal-bridge.js +364 -1506
  186. package/dist/utils/command-validator.d.ts +9 -0
  187. package/dist/utils/command-validator.js +67 -0
  188. package/dist/utils/elicitation.d.ts +1 -1
  189. package/dist/utils/elicitation.js +12 -15
  190. package/dist/utils/error-handler.d.ts +2 -51
  191. package/dist/utils/error-handler.js +11 -87
  192. package/dist/utils/ini-reader.d.ts +3 -0
  193. package/dist/utils/ini-reader.js +69 -0
  194. package/dist/utils/logger.js +9 -6
  195. package/dist/utils/normalize.d.ts +3 -0
  196. package/dist/utils/normalize.js +56 -0
  197. package/dist/utils/response-factory.d.ts +7 -0
  198. package/dist/utils/response-factory.js +33 -0
  199. package/dist/utils/response-validator.d.ts +3 -24
  200. package/dist/utils/response-validator.js +130 -81
  201. package/dist/utils/result-helpers.d.ts +4 -5
  202. package/dist/utils/result-helpers.js +15 -16
  203. package/dist/utils/safe-json.js +5 -11
  204. package/dist/utils/unreal-command-queue.d.ts +24 -0
  205. package/dist/utils/unreal-command-queue.js +120 -0
  206. package/dist/utils/validation.d.ts +0 -40
  207. package/dist/utils/validation.js +1 -78
  208. package/dist/wasm/index.d.ts +70 -0
  209. package/dist/wasm/index.js +535 -0
  210. package/docs/GraphQL-API.md +888 -0
  211. package/docs/Migration-Guide-v0.5.0.md +692 -0
  212. package/docs/Roadmap.md +53 -0
  213. package/docs/WebAssembly-Integration.md +628 -0
  214. package/docs/editor-plugin-extension.md +370 -0
  215. package/docs/handler-mapping.md +242 -0
  216. package/docs/native-automation-progress.md +128 -0
  217. package/docs/testing-guide.md +423 -0
  218. package/mcp-config-example.json +6 -6
  219. package/package.json +60 -27
  220. package/plugins/McpAutomationBridge/Config/FilterPlugin.ini +8 -0
  221. package/plugins/McpAutomationBridge/McpAutomationBridge.uplugin +64 -0
  222. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/McpAutomationBridge.Build.cs +189 -0
  223. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.cpp +22 -0
  224. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.h +30 -0
  225. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeHelpers.h +1983 -0
  226. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeModule.cpp +72 -0
  227. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSettings.cpp +46 -0
  228. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +581 -0
  229. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +2394 -0
  230. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetQueryHandlers.cpp +300 -0
  231. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetWorkflowHandlers.cpp +2807 -0
  232. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AudioHandlers.cpp +1087 -0
  233. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BehaviorTreeHandlers.cpp +488 -0
  234. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.cpp +643 -0
  235. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.h +31 -0
  236. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +1184 -0
  237. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +5652 -0
  238. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers_List.cpp +152 -0
  239. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ControlHandlers.cpp +2614 -0
  240. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_DebugHandlers.cpp +42 -0
  241. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EditorFunctionHandlers.cpp +1237 -0
  242. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +1701 -0
  243. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +2145 -0
  244. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_FoliageHandlers.cpp +954 -0
  245. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InputHandlers.cpp +209 -0
  246. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InsightsHandlers.cpp +41 -0
  247. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LandscapeHandlers.cpp +1164 -0
  248. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LevelHandlers.cpp +762 -0
  249. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +634 -0
  250. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LogHandlers.cpp +136 -0
  251. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_MaterialGraphHandlers.cpp +494 -0
  252. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraGraphHandlers.cpp +278 -0
  253. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraHandlers.cpp +625 -0
  254. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PerformanceHandlers.cpp +401 -0
  255. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PipelineHandlers.cpp +67 -0
  256. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +735 -0
  257. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PropertyHandlers.cpp +2634 -0
  258. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_RenderHandlers.cpp +189 -0
  259. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.cpp +917 -0
  260. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.h +39 -0
  261. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +2670 -0
  262. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequencerHandlers.cpp +519 -0
  263. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_TestHandlers.cpp +38 -0
  264. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_UiHandlers.cpp +668 -0
  265. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_WorldPartitionHandlers.cpp +346 -0
  266. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.cpp +1330 -0
  267. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.h +149 -0
  268. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +783 -0
  269. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSettings.h +115 -0
  270. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSubsystem.h +796 -0
  271. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpConnectionManager.h +117 -0
  272. package/scripts/check-unreal-connection.mjs +19 -0
  273. package/scripts/clean-tmp.js +23 -0
  274. package/scripts/patch-wasm.js +26 -0
  275. package/scripts/run-all-tests.mjs +131 -0
  276. package/scripts/smoke-test.ts +94 -0
  277. package/scripts/sync-mcp-plugin.js +143 -0
  278. package/scripts/test-no-plugin-alternates.mjs +113 -0
  279. package/scripts/validate-server.js +46 -0
  280. package/scripts/verify-automation-bridge.js +200 -0
  281. package/server.json +57 -21
  282. package/src/automation/bridge.ts +558 -0
  283. package/src/automation/connection-manager.ts +130 -0
  284. package/src/automation/handshake.ts +99 -0
  285. package/src/automation/index.ts +2 -0
  286. package/src/automation/message-handler.ts +167 -0
  287. package/src/automation/request-tracker.ts +123 -0
  288. package/src/automation/types.ts +107 -0
  289. package/src/cli.ts +33 -6
  290. package/src/config.ts +73 -0
  291. package/src/constants.ts +12 -0
  292. package/src/graphql/resolvers.ts +1010 -0
  293. package/src/graphql/schema.ts +452 -0
  294. package/src/graphql/server.ts +154 -0
  295. package/src/graphql/types.ts +7 -0
  296. package/src/handlers/resource-handlers.ts +186 -0
  297. package/src/index.ts +152 -663
  298. package/src/prompts/index.ts +4 -4
  299. package/src/resources/actors.ts +58 -76
  300. package/src/resources/assets.ts +147 -134
  301. package/src/resources/levels.ts +28 -33
  302. package/src/server/resource-registry.ts +47 -0
  303. package/src/server/tool-registry.ts +354 -0
  304. package/src/server-setup.ts +148 -0
  305. package/src/services/health-monitor.ts +132 -0
  306. package/src/services/metrics-server.ts +142 -0
  307. package/src/tools/actors.ts +417 -322
  308. package/src/tools/animation.ts +671 -461
  309. package/src/tools/assets.ts +353 -289
  310. package/src/tools/audio.ts +323 -766
  311. package/src/tools/base-tool.ts +52 -0
  312. package/src/tools/behavior-tree.ts +45 -0
  313. package/src/tools/blueprint/helpers.ts +189 -0
  314. package/src/tools/blueprint.ts +787 -965
  315. package/src/tools/consolidated-tool-definitions.ts +993 -515
  316. package/src/tools/consolidated-tool-handlers.ts +272 -1139
  317. package/src/tools/debug.ts +292 -187
  318. package/src/tools/dynamic-handler-registry.ts +151 -0
  319. package/src/tools/editor.ts +309 -246
  320. package/src/tools/engine.ts +14 -3
  321. package/src/tools/environment.ts +287 -0
  322. package/src/tools/foliage.ts +314 -379
  323. package/src/tools/handlers/actor-handlers.ts +271 -0
  324. package/src/tools/handlers/animation-handlers.ts +237 -0
  325. package/src/tools/handlers/argument-helper.ts +142 -0
  326. package/src/tools/handlers/asset-handlers.ts +532 -0
  327. package/src/tools/handlers/audio-handlers.ts +194 -0
  328. package/src/tools/handlers/blueprint-handlers.ts +380 -0
  329. package/src/tools/handlers/common-handlers.ts +87 -0
  330. package/src/tools/handlers/editor-handlers.ts +123 -0
  331. package/src/tools/handlers/effect-handlers.ts +220 -0
  332. package/src/tools/handlers/environment-handlers.ts +183 -0
  333. package/src/tools/handlers/graph-handlers.ts +116 -0
  334. package/src/tools/handlers/input-handlers.ts +28 -0
  335. package/src/tools/handlers/inspect-handlers.ts +450 -0
  336. package/src/tools/handlers/level-handlers.ts +252 -0
  337. package/src/tools/handlers/lighting-handlers.ts +147 -0
  338. package/src/tools/handlers/performance-handlers.ts +132 -0
  339. package/src/tools/handlers/pipeline-handlers.ts +127 -0
  340. package/src/tools/handlers/sequence-handlers.ts +415 -0
  341. package/src/tools/handlers/system-handlers.ts +564 -0
  342. package/src/tools/input.ts +101 -0
  343. package/src/tools/introspection.ts +493 -584
  344. package/src/tools/landscape.ts +394 -489
  345. package/src/tools/level.ts +752 -694
  346. package/src/tools/lighting.ts +583 -984
  347. package/src/tools/logs.ts +9 -57
  348. package/src/tools/materials.ts +231 -121
  349. package/src/tools/niagara.ts +293 -168
  350. package/src/tools/performance.ts +320 -168
  351. package/src/tools/physics.ts +268 -613
  352. package/src/tools/property-dictionary.ts +98 -0
  353. package/src/tools/sequence.ts +255 -815
  354. package/src/tools/tool-definition-utils.ts +35 -0
  355. package/src/tools/ui.ts +207 -283
  356. package/src/types/env.ts +0 -10
  357. package/src/types/tool-interfaces.ts +250 -0
  358. package/src/types/tool-types.ts +243 -21
  359. package/src/unreal-bridge.ts +460 -1550
  360. package/src/utils/command-validator.ts +75 -0
  361. package/src/utils/elicitation.ts +10 -7
  362. package/src/utils/error-handler.ts +14 -90
  363. package/src/utils/ini-reader.ts +86 -0
  364. package/src/utils/logger.ts +8 -3
  365. package/src/utils/normalize.ts +60 -0
  366. package/src/utils/response-factory.ts +39 -0
  367. package/src/utils/response-validator.ts +176 -56
  368. package/src/utils/result-helpers.ts +21 -19
  369. package/src/utils/safe-json.ts +14 -11
  370. package/src/utils/unreal-command-queue.ts +152 -0
  371. package/src/utils/validation.ts +4 -1
  372. package/src/wasm/index.ts +838 -0
  373. package/test-server.mjs +100 -0
  374. package/tests/run-unreal-tool-tests.mjs +242 -14
  375. package/tests/test-animation.mjs +44 -0
  376. package/tests/test-asset-advanced.mjs +82 -0
  377. package/tests/test-asset-errors.mjs +35 -0
  378. package/tests/test-audio.mjs +219 -0
  379. package/tests/test-automation-timeouts.mjs +98 -0
  380. package/tests/test-behavior-tree.mjs +261 -0
  381. package/tests/test-blueprint-events.mjs +35 -0
  382. package/tests/test-blueprint-graph.mjs +79 -0
  383. package/tests/test-blueprint.mjs +577 -0
  384. package/tests/test-client-mode.mjs +86 -0
  385. package/tests/test-console-command.mjs +56 -0
  386. package/tests/test-control-actor.mjs +425 -0
  387. package/tests/test-control-editor.mjs +80 -0
  388. package/tests/test-extra-tools.mjs +38 -0
  389. package/tests/test-graphql.mjs +322 -0
  390. package/tests/test-inspect.mjs +72 -0
  391. package/tests/test-landscape.mjs +60 -0
  392. package/tests/test-manage-asset.mjs +438 -0
  393. package/tests/test-manage-level.mjs +70 -0
  394. package/tests/test-materials.mjs +356 -0
  395. package/tests/test-niagara.mjs +185 -0
  396. package/tests/test-no-inline-python.mjs +122 -0
  397. package/tests/test-plugin-handshake.mjs +82 -0
  398. package/tests/test-render.mjs +33 -0
  399. package/tests/test-runner.mjs +933 -0
  400. package/tests/test-search-assets.mjs +66 -0
  401. package/tests/test-sequence.mjs +68 -0
  402. package/tests/test-system.mjs +57 -0
  403. package/tests/test-wasm.mjs +193 -0
  404. package/tests/test-world-partition.mjs +215 -0
  405. package/tsconfig.json +3 -3
  406. package/wasm/Cargo.lock +363 -0
  407. package/wasm/Cargo.toml +42 -0
  408. package/wasm/LICENSE +21 -0
  409. package/wasm/README.md +253 -0
  410. package/wasm/src/dependency_resolver.rs +377 -0
  411. package/wasm/src/lib.rs +153 -0
  412. package/wasm/src/property_parser.rs +271 -0
  413. package/wasm/src/transform_math.rs +396 -0
  414. package/wasm/tests/integration.rs +109 -0
  415. package/.github/workflows/smithery-build.yml +0 -29
  416. package/dist/tools/build_environment_advanced.d.ts +0 -65
  417. package/dist/tools/build_environment_advanced.js +0 -633
  418. package/dist/tools/rc.d.ts +0 -110
  419. package/dist/tools/rc.js +0 -437
  420. package/dist/tools/visual.d.ts +0 -40
  421. package/dist/tools/visual.js +0 -282
  422. package/dist/utils/http.d.ts +0 -6
  423. package/dist/utils/http.js +0 -151
  424. package/dist/utils/python-output.d.ts +0 -18
  425. package/dist/utils/python-output.js +0 -290
  426. package/dist/utils/python.d.ts +0 -2
  427. package/dist/utils/python.js +0 -4
  428. package/dist/utils/stdio-redirect.d.ts +0 -2
  429. package/dist/utils/stdio-redirect.js +0 -20
  430. package/docs/unreal-tool-test-cases.md +0 -574
  431. package/smithery.yaml +0 -29
  432. package/src/tools/build_environment_advanced.ts +0 -732
  433. package/src/tools/rc.ts +0 -515
  434. package/src/tools/visual.ts +0 -281
  435. package/src/utils/http.ts +0 -187
  436. package/src/utils/python-output.ts +0 -351
  437. package/src/utils/python.ts +0 -3
  438. 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
+ }