unreal-engine-mcp-server 0.4.6 → 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 +269 -22
  29. package/CONTRIBUTING.md +140 -0
  30. package/README.md +166 -72
  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 -604
  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 +5475 -1627
  97. package/dist/tools/consolidated-tool-definitions.js +829 -482
  98. package/dist/tools/consolidated-tool-handlers.d.ts +2 -1
  99. package/dist/tools/consolidated-tool-handlers.js +211 -1009
  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 +45 -0
  161. package/dist/tools/logs.js +210 -0
  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 +195 -11
  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 -649
  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 -500
  316. package/src/tools/consolidated-tool-handlers.ts +272 -1122
  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 +219 -0
  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 +250 -13
  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 -572
  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,2145 @@
1
+ #include "McpAutomationBridgeGlobals.h"
2
+ #include "McpAutomationBridgeHelpers.h"
3
+ #include "McpAutomationBridgeSubsystem.h"
4
+
5
+ #if WITH_EDITOR
6
+ #include "Editor.h"
7
+ #include "EditorAssetLibrary.h"
8
+
9
+ #if __has_include("Subsystems/EditorActorSubsystem.h")
10
+ #include "Subsystems/EditorActorSubsystem.h"
11
+ #elif __has_include("EditorActorSubsystem.h")
12
+ #include "EditorActorSubsystem.h"
13
+ #endif
14
+ #if __has_include("Subsystems/UnrealEditorSubsystem.h")
15
+ #include "Subsystems/UnrealEditorSubsystem.h"
16
+ #elif __has_include("UnrealEditorSubsystem.h")
17
+ #include "UnrealEditorSubsystem.h"
18
+ #endif
19
+ #if __has_include("Subsystems/LevelEditorSubsystem.h")
20
+ #include "Subsystems/LevelEditorSubsystem.h"
21
+ #elif __has_include("LevelEditorSubsystem.h")
22
+ #include "LevelEditorSubsystem.h"
23
+ #endif
24
+ #include "Components/DirectionalLightComponent.h"
25
+ #include "Components/SkyLightComponent.h"
26
+ #include "Developer/AssetTools/Public/AssetToolsModule.h"
27
+ #include "EditorValidatorSubsystem.h"
28
+ #include "Engine/Blueprint.h"
29
+ #include "Engine/DirectionalLight.h"
30
+ #include "Engine/SkyLight.h"
31
+ #include "EngineUtils.h"
32
+ #include "FileHelpers.h"
33
+ #include "GeneralProjectSettings.h"
34
+ #include "KismetProceduralMeshLibrary.h"
35
+ #include "Misc/FileHelper.h"
36
+ #include "NiagaraComponent.h"
37
+ #include "NiagaraSystem.h"
38
+ #include "ProceduralMeshComponent.h"
39
+
40
+ #endif
41
+
42
+ bool UMcpAutomationBridgeSubsystem::HandleBuildEnvironmentAction(
43
+ const FString &RequestId, const FString &Action,
44
+ const TSharedPtr<FJsonObject> &Payload,
45
+ TSharedPtr<FMcpBridgeWebSocket> RequestingSocket) {
46
+ const FString Lower = Action.ToLower();
47
+ if (!Lower.Equals(TEXT("build_environment"), ESearchCase::IgnoreCase) &&
48
+ !Lower.StartsWith(TEXT("build_environment")))
49
+ return false;
50
+
51
+ if (!Payload.IsValid()) {
52
+ SendAutomationError(RequestingSocket, RequestId,
53
+ TEXT("build_environment payload missing."),
54
+ TEXT("INVALID_PAYLOAD"));
55
+ return true;
56
+ }
57
+
58
+ FString SubAction;
59
+ Payload->TryGetStringField(TEXT("action"), SubAction);
60
+ const FString LowerSub = SubAction.ToLower();
61
+
62
+ // Fast-path foliage sub-actions to dedicated native handlers to avoid double
63
+ // responses
64
+ if (LowerSub == TEXT("add_foliage_instances")) {
65
+ // Transform from build_environment schema to foliage handler schema
66
+ FString FoliageTypePath;
67
+ Payload->TryGetStringField(TEXT("foliageType"), FoliageTypePath);
68
+ const TArray<TSharedPtr<FJsonValue>> *Transforms = nullptr;
69
+ Payload->TryGetArrayField(TEXT("transforms"), Transforms);
70
+ TSharedPtr<FJsonObject> FoliagePayload = MakeShared<FJsonObject>();
71
+ if (!FoliageTypePath.IsEmpty()) {
72
+ FoliagePayload->SetStringField(TEXT("foliageTypePath"), FoliageTypePath);
73
+ }
74
+ TArray<TSharedPtr<FJsonValue>> Locations;
75
+ if (Transforms) {
76
+ for (const TSharedPtr<FJsonValue> &V : *Transforms) {
77
+ if (!V.IsValid() || V->Type != EJson::Object)
78
+ continue;
79
+ const TSharedPtr<FJsonObject> *TObj = nullptr;
80
+ if (!V->TryGetObject(TObj) || !TObj)
81
+ continue;
82
+ const TSharedPtr<FJsonObject> *LocObj = nullptr;
83
+ if (!(*TObj)->TryGetObjectField(TEXT("location"), LocObj) || !LocObj)
84
+ continue;
85
+ double X = 0, Y = 0, Z = 0;
86
+ (*LocObj)->TryGetNumberField(TEXT("x"), X);
87
+ (*LocObj)->TryGetNumberField(TEXT("y"), Y);
88
+ (*LocObj)->TryGetNumberField(TEXT("z"), Z);
89
+ TSharedPtr<FJsonObject> L = MakeShared<FJsonObject>();
90
+ L->SetNumberField(TEXT("x"), X);
91
+ L->SetNumberField(TEXT("y"), Y);
92
+ L->SetNumberField(TEXT("z"), Z);
93
+ Locations.Add(MakeShared<FJsonValueObject>(L));
94
+ }
95
+ }
96
+ FoliagePayload->SetArrayField(TEXT("locations"), Locations);
97
+ return HandlePaintFoliage(RequestId, TEXT("paint_foliage"), FoliagePayload,
98
+ RequestingSocket);
99
+ } else if (LowerSub == TEXT("get_foliage_instances")) {
100
+ FString FoliageTypePath;
101
+ Payload->TryGetStringField(TEXT("foliageType"), FoliageTypePath);
102
+ TSharedPtr<FJsonObject> FoliagePayload = MakeShared<FJsonObject>();
103
+ if (!FoliageTypePath.IsEmpty()) {
104
+ FoliagePayload->SetStringField(TEXT("foliageTypePath"), FoliageTypePath);
105
+ }
106
+ return HandleGetFoliageInstances(RequestId, TEXT("get_foliage_instances"),
107
+ FoliagePayload, RequestingSocket);
108
+ } else if (LowerSub == TEXT("remove_foliage")) {
109
+ FString FoliageTypePath;
110
+ Payload->TryGetStringField(TEXT("foliageType"), FoliageTypePath);
111
+ bool bRemoveAll = false;
112
+ Payload->TryGetBoolField(TEXT("removeAll"), bRemoveAll);
113
+ TSharedPtr<FJsonObject> FoliagePayload = MakeShared<FJsonObject>();
114
+ if (!FoliageTypePath.IsEmpty()) {
115
+ FoliagePayload->SetStringField(TEXT("foliageTypePath"), FoliageTypePath);
116
+ }
117
+ FoliagePayload->SetBoolField(TEXT("removeAll"), bRemoveAll);
118
+ return HandleRemoveFoliage(RequestId, TEXT("remove_foliage"),
119
+ FoliagePayload, RequestingSocket);
120
+ }
121
+ // Dispatch landscape operations
122
+ else if (LowerSub == TEXT("paint_landscape") ||
123
+ LowerSub == TEXT("paint_landscape_layer")) {
124
+ return HandlePaintLandscapeLayer(RequestId, TEXT("paint_landscape_layer"),
125
+ Payload, RequestingSocket);
126
+ } else if (LowerSub == TEXT("sculpt_landscape")) {
127
+ return HandleSculptLandscape(RequestId, TEXT("sculpt_landscape"), Payload,
128
+ RequestingSocket);
129
+ } else if (LowerSub == TEXT("modify_heightmap")) {
130
+ return HandleModifyHeightmap(RequestId, TEXT("modify_heightmap"), Payload,
131
+ RequestingSocket);
132
+ } else if (LowerSub == TEXT("set_landscape_material")) {
133
+ return HandleSetLandscapeMaterial(RequestId, TEXT("set_landscape_material"),
134
+ Payload, RequestingSocket);
135
+ } else if (LowerSub == TEXT("create_landscape_grass_type")) {
136
+ return HandleCreateLandscapeGrassType(RequestId,
137
+ TEXT("create_landscape_grass_type"),
138
+ Payload, RequestingSocket);
139
+ } else if (LowerSub == TEXT("generate_lods")) {
140
+ return HandleGenerateLODs(RequestId, TEXT("generate_lods"), Payload,
141
+ RequestingSocket);
142
+ } else if (LowerSub == TEXT("bake_lightmap")) {
143
+ return HandleBakeLightmap(RequestId, TEXT("bake_lightmap"), Payload,
144
+ RequestingSocket);
145
+ }
146
+
147
+ #if WITH_EDITOR
148
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
149
+ Resp->SetStringField(TEXT("action"), LowerSub);
150
+ bool bSuccess = true;
151
+ FString Message =
152
+ FString::Printf(TEXT("Environment action '%s' completed"), *LowerSub);
153
+ FString ErrorCode;
154
+
155
+ if (LowerSub == TEXT("export_snapshot")) {
156
+ FString Path;
157
+ Payload->TryGetStringField(TEXT("path"), Path);
158
+ if (Path.IsEmpty()) {
159
+ bSuccess = false;
160
+ Message = TEXT("path required for export_snapshot");
161
+ ErrorCode = TEXT("INVALID_ARGUMENT");
162
+ Resp->SetStringField(TEXT("error"), Message);
163
+ } else {
164
+ TSharedPtr<FJsonObject> Snapshot = MakeShared<FJsonObject>();
165
+ Snapshot->SetStringField(TEXT("timestamp"),
166
+ FDateTime::UtcNow().ToString());
167
+ Snapshot->SetStringField(TEXT("type"), TEXT("environment_snapshot"));
168
+
169
+ FString JsonString;
170
+ TSharedRef<TJsonWriter<>> Writer =
171
+ TJsonWriterFactory<>::Create(&JsonString);
172
+ if (FJsonSerializer::Serialize(Snapshot.ToSharedRef(), Writer)) {
173
+ if (FFileHelper::SaveStringToFile(JsonString, *Path)) {
174
+ Resp->SetStringField(TEXT("exportPath"), Path);
175
+ Resp->SetStringField(TEXT("message"), TEXT("Snapshot exported"));
176
+ } else {
177
+ bSuccess = false;
178
+ Message = TEXT("Failed to write snapshot file");
179
+ ErrorCode = TEXT("WRITE_FAILED");
180
+ Resp->SetStringField(TEXT("error"), Message);
181
+ }
182
+ } else {
183
+ bSuccess = false;
184
+ Message = TEXT("Failed to serialize snapshot");
185
+ ErrorCode = TEXT("SERIALIZE_FAILED");
186
+ Resp->SetStringField(TEXT("error"), Message);
187
+ }
188
+ }
189
+ } else if (LowerSub == TEXT("import_snapshot")) {
190
+ FString Path;
191
+ Payload->TryGetStringField(TEXT("path"), Path);
192
+ if (Path.IsEmpty()) {
193
+ bSuccess = false;
194
+ Message = TEXT("path required for import_snapshot");
195
+ ErrorCode = TEXT("INVALID_ARGUMENT");
196
+ Resp->SetStringField(TEXT("error"), Message);
197
+ } else {
198
+ FString JsonString;
199
+ if (!FFileHelper::LoadFileToString(JsonString, *Path)) {
200
+ bSuccess = false;
201
+ Message = TEXT("Failed to read snapshot file");
202
+ ErrorCode = TEXT("LOAD_FAILED");
203
+ Resp->SetStringField(TEXT("error"), Message);
204
+ } else {
205
+ TSharedPtr<FJsonObject> SnapshotObj;
206
+ TSharedRef<TJsonReader<>> Reader =
207
+ TJsonReaderFactory<>::Create(JsonString);
208
+ if (!FJsonSerializer::Deserialize(Reader, SnapshotObj) ||
209
+ !SnapshotObj.IsValid()) {
210
+ bSuccess = false;
211
+ Message = TEXT("Failed to parse snapshot");
212
+ ErrorCode = TEXT("PARSE_FAILED");
213
+ Resp->SetStringField(TEXT("error"), Message);
214
+ } else {
215
+ Resp->SetObjectField(TEXT("snapshot"), SnapshotObj.ToSharedRef());
216
+ Resp->SetStringField(TEXT("message"), TEXT("Snapshot imported"));
217
+ }
218
+ }
219
+ }
220
+ } else if (LowerSub == TEXT("delete")) {
221
+ const TArray<TSharedPtr<FJsonValue>> *NamesArray = nullptr;
222
+ if (!Payload->TryGetArrayField(TEXT("names"), NamesArray) || !NamesArray) {
223
+ bSuccess = false;
224
+ Message = TEXT("names array required for delete");
225
+ ErrorCode = TEXT("INVALID_ARGUMENT");
226
+ Resp->SetStringField(TEXT("error"), Message);
227
+ } else if (!GEditor) {
228
+ bSuccess = false;
229
+ Message = TEXT("Editor not available");
230
+ ErrorCode = TEXT("EDITOR_NOT_AVAILABLE");
231
+ Resp->SetStringField(TEXT("error"), Message);
232
+ } else {
233
+ UEditorActorSubsystem *ActorSS =
234
+ GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
235
+ if (!ActorSS) {
236
+ bSuccess = false;
237
+ Message = TEXT("EditorActorSubsystem not available");
238
+ ErrorCode = TEXT("EDITOR_ACTOR_SUBSYSTEM_MISSING");
239
+ Resp->SetStringField(TEXT("error"), Message);
240
+ } else {
241
+ TArray<FString> Deleted;
242
+ TArray<FString> Missing;
243
+ for (const TSharedPtr<FJsonValue> &Val : *NamesArray) {
244
+ if (Val.IsValid() && Val->Type == EJson::String) {
245
+ FString Name = Val->AsString();
246
+ TArray<AActor *> AllActors = ActorSS->GetAllLevelActors();
247
+ bool bRemoved = false;
248
+ for (AActor *A : AllActors) {
249
+ if (A &&
250
+ A->GetActorLabel().Equals(Name, ESearchCase::IgnoreCase)) {
251
+ if (ActorSS->DestroyActor(A)) {
252
+ Deleted.Add(Name);
253
+ bRemoved = true;
254
+ }
255
+ break;
256
+ }
257
+ }
258
+ if (!bRemoved) {
259
+ Missing.Add(Name);
260
+ }
261
+ }
262
+ }
263
+
264
+ TArray<TSharedPtr<FJsonValue>> DeletedArray;
265
+ for (const FString &Name : Deleted) {
266
+ DeletedArray.Add(MakeShared<FJsonValueString>(Name));
267
+ }
268
+ Resp->SetArrayField(TEXT("deleted"), DeletedArray);
269
+ Resp->SetNumberField(TEXT("deletedCount"), Deleted.Num());
270
+
271
+ if (Missing.Num() > 0) {
272
+ TArray<TSharedPtr<FJsonValue>> MissingArray;
273
+ for (const FString &Name : Missing) {
274
+ MissingArray.Add(MakeShared<FJsonValueString>(Name));
275
+ }
276
+ Resp->SetArrayField(TEXT("missing"), MissingArray);
277
+ bSuccess = false;
278
+ Message = TEXT("Some environment actors could not be removed");
279
+ ErrorCode = TEXT("DELETE_PARTIAL");
280
+ Resp->SetStringField(TEXT("error"), Message);
281
+ } else {
282
+ Message = TEXT("Environment actors deleted");
283
+ }
284
+ }
285
+ }
286
+ } else if (LowerSub == TEXT("create_sky_sphere")) {
287
+ if (GEditor) {
288
+ UClass *SkySphereClass = LoadClass<AActor>(
289
+ nullptr, TEXT("/Script/Engine.Blueprint'/Engine/Maps/Templates/"
290
+ "SkySphere.SkySphere_C'"));
291
+ if (SkySphereClass) {
292
+ AActor *SkySphere = SpawnActorInActiveWorld<AActor>(
293
+ SkySphereClass, FVector::ZeroVector, FRotator::ZeroRotator,
294
+ TEXT("SkySphere"));
295
+ if (SkySphere) {
296
+ bSuccess = true;
297
+ Message = TEXT("Sky sphere created");
298
+ Resp->SetStringField(TEXT("actorName"), SkySphere->GetActorLabel());
299
+ }
300
+ }
301
+ }
302
+ if (!bSuccess) {
303
+ bSuccess = false;
304
+ Message = TEXT("Failed to create sky sphere");
305
+ ErrorCode = TEXT("CREATION_FAILED");
306
+ }
307
+ } else if (LowerSub == TEXT("set_time_of_day")) {
308
+ float TimeOfDay = 12.0f;
309
+ Payload->TryGetNumberField(TEXT("time"), TimeOfDay);
310
+
311
+ if (GEditor) {
312
+ UEditorActorSubsystem *ActorSS =
313
+ GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
314
+ if (ActorSS) {
315
+ for (AActor *Actor : ActorSS->GetAllLevelActors()) {
316
+ if (Actor->GetClass()->GetName().Contains(TEXT("SkySphere"))) {
317
+ UFunction *SetTimeFunction =
318
+ Actor->FindFunction(TEXT("SetTimeOfDay"));
319
+ if (SetTimeFunction) {
320
+ float TimeParam = TimeOfDay;
321
+ Actor->ProcessEvent(SetTimeFunction, &TimeParam);
322
+ bSuccess = true;
323
+ Message =
324
+ FString::Printf(TEXT("Time of day set to %.2f"), TimeOfDay);
325
+ break;
326
+ }
327
+ }
328
+ }
329
+ }
330
+ }
331
+ if (!bSuccess) {
332
+ bSuccess = false;
333
+ Message = TEXT("Sky sphere not found or time function not available");
334
+ ErrorCode = TEXT("SET_TIME_FAILED");
335
+ }
336
+ } else if (LowerSub == TEXT("create_fog_volume")) {
337
+ FVector Location(0, 0, 0);
338
+ Payload->TryGetNumberField(TEXT("x"), Location.X);
339
+ Payload->TryGetNumberField(TEXT("y"), Location.Y);
340
+ Payload->TryGetNumberField(TEXT("z"), Location.Z);
341
+
342
+ if (GEditor) {
343
+ UClass *FogClass = LoadClass<AActor>(
344
+ nullptr, TEXT("/Script/Engine.ExponentialHeightFog"));
345
+ if (FogClass) {
346
+ AActor *FogVolume = SpawnActorInActiveWorld<AActor>(
347
+ FogClass, Location, FRotator::ZeroRotator, TEXT("FogVolume"));
348
+ if (FogVolume) {
349
+ bSuccess = true;
350
+ Message = TEXT("Fog volume created");
351
+ Resp->SetStringField(TEXT("actorName"), FogVolume->GetActorLabel());
352
+ }
353
+ }
354
+ }
355
+ if (!bSuccess) {
356
+ bSuccess = false;
357
+ Message = TEXT("Failed to create fog volume");
358
+ ErrorCode = TEXT("CREATION_FAILED");
359
+ }
360
+ } else {
361
+ bSuccess = false;
362
+ Message = FString::Printf(TEXT("Environment action '%s' not implemented"),
363
+ *LowerSub);
364
+ ErrorCode = TEXT("NOT_IMPLEMENTED");
365
+ Resp->SetStringField(TEXT("error"), Message);
366
+ }
367
+
368
+ Resp->SetBoolField(TEXT("success"), bSuccess);
369
+ SendAutomationResponse(RequestingSocket, RequestId, bSuccess, Message, Resp,
370
+ ErrorCode);
371
+ return true;
372
+ #else
373
+ SendAutomationResponse(
374
+ RequestingSocket, RequestId, false,
375
+ TEXT("Environment building actions require editor build."), nullptr,
376
+ TEXT("NOT_IMPLEMENTED"));
377
+ return true;
378
+ #endif
379
+ }
380
+
381
+ bool UMcpAutomationBridgeSubsystem::HandleControlEnvironmentAction(
382
+ const FString &RequestId, const FString &Action,
383
+ const TSharedPtr<FJsonObject> &Payload,
384
+ TSharedPtr<FMcpBridgeWebSocket> RequestingSocket) {
385
+ const FString Lower = Action.ToLower();
386
+ if (!Lower.Equals(TEXT("control_environment"), ESearchCase::IgnoreCase) &&
387
+ !Lower.StartsWith(TEXT("control_environment"))) {
388
+ return false;
389
+ }
390
+
391
+ if (!Payload.IsValid()) {
392
+ SendAutomationError(RequestingSocket, RequestId,
393
+ TEXT("control_environment payload missing."),
394
+ TEXT("INVALID_PAYLOAD"));
395
+ return true;
396
+ }
397
+
398
+ FString SubAction;
399
+ Payload->TryGetStringField(TEXT("action"), SubAction);
400
+ const FString LowerSub = SubAction.ToLower();
401
+
402
+ #if WITH_EDITOR
403
+ auto SendResult = [&](bool bSuccess, const TCHAR *Message,
404
+ const FString &ErrorCode,
405
+ const TSharedPtr<FJsonObject> &Result) {
406
+ if (bSuccess) {
407
+ SendAutomationResponse(RequestingSocket, RequestId, true,
408
+ Message ? Message
409
+ : TEXT("Environment control succeeded."),
410
+ Result, FString());
411
+ } else {
412
+ SendAutomationResponse(RequestingSocket, RequestId, false,
413
+ Message ? Message
414
+ : TEXT("Environment control failed."),
415
+ Result, ErrorCode);
416
+ }
417
+ };
418
+
419
+ UWorld *World = nullptr;
420
+ if (GEditor) {
421
+ World = GEditor->GetEditorWorldContext().World();
422
+ }
423
+
424
+ if (!World) {
425
+ SendResult(false, TEXT("Editor world is unavailable"),
426
+ TEXT("WORLD_NOT_AVAILABLE"), nullptr);
427
+ return true;
428
+ }
429
+
430
+ auto FindFirstDirectionalLight = [&]() -> ADirectionalLight * {
431
+ for (TActorIterator<ADirectionalLight> It(World); It; ++It) {
432
+ if (ADirectionalLight *Light = *It) {
433
+ if (IsValid(Light)) {
434
+ return Light;
435
+ }
436
+ }
437
+ }
438
+ return nullptr;
439
+ };
440
+
441
+ auto FindFirstSkyLight = [&]() -> ASkyLight * {
442
+ for (TActorIterator<ASkyLight> It(World); It; ++It) {
443
+ if (ASkyLight *Sky = *It) {
444
+ if (IsValid(Sky)) {
445
+ return Sky;
446
+ }
447
+ }
448
+ }
449
+ return nullptr;
450
+ };
451
+
452
+ if (LowerSub == TEXT("set_time_of_day")) {
453
+ double Hour = 0.0;
454
+ const bool bHasHour = Payload->TryGetNumberField(TEXT("hour"), Hour);
455
+ if (!bHasHour) {
456
+ SendResult(false, TEXT("Missing hour parameter"),
457
+ TEXT("INVALID_ARGUMENT"), nullptr);
458
+ return true;
459
+ }
460
+
461
+ ADirectionalLight *SunLight = FindFirstDirectionalLight();
462
+ if (!SunLight) {
463
+ SendResult(false, TEXT("No directional light found"),
464
+ TEXT("SUN_NOT_FOUND"), nullptr);
465
+ return true;
466
+ }
467
+
468
+ const float ClampedHour =
469
+ FMath::Clamp(static_cast<float>(Hour), 0.0f, 24.0f);
470
+ const float SolarPitch = (ClampedHour / 24.0f) * 360.0f - 90.0f;
471
+
472
+ SunLight->Modify();
473
+ FRotator NewRotation = SunLight->GetActorRotation();
474
+ NewRotation.Pitch = SolarPitch;
475
+ SunLight->SetActorRotation(NewRotation);
476
+
477
+ if (UDirectionalLightComponent *LightComp =
478
+ Cast<UDirectionalLightComponent>(SunLight->GetLightComponent())) {
479
+ LightComp->MarkRenderStateDirty();
480
+ }
481
+
482
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
483
+ Result->SetNumberField(TEXT("hour"), ClampedHour);
484
+ Result->SetNumberField(TEXT("pitch"), SolarPitch);
485
+ Result->SetStringField(TEXT("actor"), SunLight->GetPathName());
486
+ SendResult(true, TEXT("Time of day updated"), FString(), Result);
487
+ return true;
488
+ }
489
+
490
+ if (LowerSub == TEXT("set_sun_intensity")) {
491
+ double Intensity = 0.0;
492
+ if (!Payload->TryGetNumberField(TEXT("intensity"), Intensity)) {
493
+ SendResult(false, TEXT("Missing intensity parameter"),
494
+ TEXT("INVALID_ARGUMENT"), nullptr);
495
+ return true;
496
+ }
497
+
498
+ ADirectionalLight *SunLight = FindFirstDirectionalLight();
499
+ if (!SunLight) {
500
+ SendResult(false, TEXT("No directional light found"),
501
+ TEXT("SUN_NOT_FOUND"), nullptr);
502
+ return true;
503
+ }
504
+
505
+ if (UDirectionalLightComponent *LightComp =
506
+ Cast<UDirectionalLightComponent>(SunLight->GetLightComponent())) {
507
+ LightComp->SetIntensity(static_cast<float>(Intensity));
508
+ LightComp->MarkRenderStateDirty();
509
+ }
510
+
511
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
512
+ Result->SetNumberField(TEXT("intensity"), Intensity);
513
+ Result->SetStringField(TEXT("actor"), SunLight->GetPathName());
514
+ SendResult(true, TEXT("Sun intensity updated"), FString(), Result);
515
+ return true;
516
+ }
517
+
518
+ if (LowerSub == TEXT("set_skylight_intensity")) {
519
+ double Intensity = 0.0;
520
+ if (!Payload->TryGetNumberField(TEXT("intensity"), Intensity)) {
521
+ SendResult(false, TEXT("Missing intensity parameter"),
522
+ TEXT("INVALID_ARGUMENT"), nullptr);
523
+ return true;
524
+ }
525
+
526
+ ASkyLight *SkyActor = FindFirstSkyLight();
527
+ if (!SkyActor) {
528
+ SendResult(false, TEXT("No skylight found"), TEXT("SKYLIGHT_NOT_FOUND"),
529
+ nullptr);
530
+ return true;
531
+ }
532
+
533
+ if (USkyLightComponent *SkyComp = SkyActor->GetLightComponent()) {
534
+ SkyComp->SetIntensity(static_cast<float>(Intensity));
535
+ SkyComp->MarkRenderStateDirty();
536
+ SkyActor->MarkComponentsRenderStateDirty();
537
+ }
538
+
539
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
540
+ Result->SetNumberField(TEXT("intensity"), Intensity);
541
+ Result->SetStringField(TEXT("actor"), SkyActor->GetPathName());
542
+ SendResult(true, TEXT("Skylight intensity updated"), FString(), Result);
543
+ return true;
544
+ }
545
+
546
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
547
+ Result->SetStringField(TEXT("action"), LowerSub);
548
+ SendResult(false, TEXT("Unsupported environment control action"),
549
+ TEXT("UNSUPPORTED_ACTION"), Result);
550
+ return true;
551
+ #else
552
+ SendAutomationResponse(RequestingSocket, RequestId, false,
553
+ TEXT("Environment control requires editor build"),
554
+ nullptr, TEXT("NOT_IMPLEMENTED"));
555
+ return true;
556
+ #endif
557
+ }
558
+
559
+ bool UMcpAutomationBridgeSubsystem::HandleSystemControlAction(
560
+ const FString &RequestId, const FString &Action,
561
+ const TSharedPtr<FJsonObject> &Payload,
562
+ TSharedPtr<FMcpBridgeWebSocket> RequestingSocket) {
563
+ const FString Lower = Action.ToLower();
564
+ if (!Lower.Equals(TEXT("system_control"), ESearchCase::IgnoreCase) &&
565
+ !Lower.StartsWith(TEXT("system_control")))
566
+ return false;
567
+
568
+ if (!Payload.IsValid()) {
569
+ SendAutomationResponse(RequestingSocket, RequestId, false,
570
+ TEXT("System control requires valid payload"),
571
+ nullptr, TEXT("INVALID_PAYLOAD"));
572
+ return true;
573
+ }
574
+
575
+ FString SubAction;
576
+ if (!Payload->TryGetStringField(TEXT("action"), SubAction)) {
577
+ SendAutomationResponse(RequestingSocket, RequestId, false,
578
+ TEXT("System control requires action parameter"),
579
+ nullptr, TEXT("INVALID_ARGUMENT"));
580
+ return true;
581
+ }
582
+
583
+ FString LowerSub = SubAction.ToLower();
584
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
585
+
586
+ // Profile commands
587
+ if (LowerSub == TEXT("profile")) {
588
+ FString ProfileType;
589
+ bool bEnabled = true;
590
+ Payload->TryGetStringField(TEXT("profileType"), ProfileType);
591
+ Payload->TryGetBoolField(TEXT("enabled"), bEnabled);
592
+
593
+ FString Command;
594
+ if (ProfileType.ToLower() == TEXT("cpu")) {
595
+ Command = bEnabled ? TEXT("stat cpu") : TEXT("stat cpu");
596
+ } else if (ProfileType.ToLower() == TEXT("gpu")) {
597
+ Command = bEnabled ? TEXT("stat gpu") : TEXT("stat gpu");
598
+ } else if (ProfileType.ToLower() == TEXT("memory")) {
599
+ Command = bEnabled ? TEXT("stat memory") : TEXT("stat memory");
600
+ } else if (ProfileType.ToLower() == TEXT("fps")) {
601
+ Command = bEnabled ? TEXT("stat fps") : TEXT("stat fps");
602
+ }
603
+
604
+ if (!Command.IsEmpty()) {
605
+ GEngine->Exec(nullptr, *Command);
606
+ Result->SetStringField(TEXT("command"), Command);
607
+ Result->SetBoolField(TEXT("enabled"), bEnabled);
608
+ SendAutomationResponse(
609
+ RequestingSocket, RequestId, true,
610
+ FString::Printf(TEXT("Executed profile command: %s"), *Command),
611
+ Result, FString());
612
+ return true;
613
+ }
614
+ }
615
+
616
+ // Show FPS
617
+ if (LowerSub == TEXT("show_fps")) {
618
+ bool bEnabled = true;
619
+ Payload->TryGetBoolField(TEXT("enabled"), bEnabled);
620
+
621
+ FString Command = bEnabled ? TEXT("stat fps") : TEXT("stat fps");
622
+ GEngine->Exec(nullptr, *Command);
623
+ Result->SetStringField(TEXT("command"), Command);
624
+ Result->SetBoolField(TEXT("enabled"), bEnabled);
625
+ SendAutomationResponse(
626
+ RequestingSocket, RequestId, true,
627
+ FString::Printf(TEXT("FPS display %s"),
628
+ bEnabled ? TEXT("enabled") : TEXT("disabled")),
629
+ Result, FString());
630
+ return true;
631
+ }
632
+
633
+ // Set quality
634
+ if (LowerSub == TEXT("set_quality")) {
635
+ FString Category;
636
+ int32 Level = 1;
637
+ Payload->TryGetStringField(TEXT("category"), Category);
638
+ Payload->TryGetNumberField(TEXT("level"), Level);
639
+
640
+ if (!Category.IsEmpty()) {
641
+ FString Command = FString::Printf(TEXT("sg.%s %d"), *Category, Level);
642
+ GEngine->Exec(nullptr, *Command);
643
+ Result->SetStringField(TEXT("command"), Command);
644
+ Result->SetStringField(TEXT("category"), Category);
645
+ Result->SetNumberField(TEXT("level"), Level);
646
+ SendAutomationResponse(
647
+ RequestingSocket, RequestId, true,
648
+ FString::Printf(TEXT("Set quality %s to %d"), *Category, Level),
649
+ Result, FString());
650
+ return true;
651
+ }
652
+ }
653
+
654
+ // Screenshot
655
+ if (LowerSub == TEXT("screenshot")) {
656
+ FString Filename = TEXT("screenshot");
657
+ Payload->TryGetStringField(TEXT("filename"), Filename);
658
+
659
+ FString Command = FString::Printf(TEXT("screenshot %s"), *Filename);
660
+ GEngine->Exec(nullptr, *Command);
661
+ Result->SetStringField(TEXT("command"), Command);
662
+ Result->SetStringField(TEXT("filename"), Filename);
663
+ SendAutomationResponse(
664
+ RequestingSocket, RequestId, true,
665
+ FString::Printf(TEXT("Screenshot captured: %s"), *Filename), Result,
666
+ FString());
667
+ return true;
668
+ }
669
+
670
+ if (LowerSub == TEXT("get_project_settings")) {
671
+ #if WITH_EDITOR
672
+ FString Category;
673
+ Payload->TryGetStringField(TEXT("category"), Category);
674
+ const FString LowerCategory = Category.ToLower();
675
+
676
+ const UGeneralProjectSettings *ProjectSettings =
677
+ GetDefault<UGeneralProjectSettings>();
678
+ TSharedPtr<FJsonObject> SettingsObj = MakeShared<FJsonObject>();
679
+ if (ProjectSettings) {
680
+ SettingsObj->SetStringField(TEXT("projectName"),
681
+ ProjectSettings->ProjectName);
682
+ SettingsObj->SetStringField(TEXT("companyName"),
683
+ ProjectSettings->CompanyName);
684
+ SettingsObj->SetStringField(TEXT("projectVersion"),
685
+ ProjectSettings->ProjectVersion);
686
+ SettingsObj->SetStringField(TEXT("description"),
687
+ ProjectSettings->Description);
688
+ }
689
+
690
+ TSharedPtr<FJsonObject> Out = MakeShared<FJsonObject>();
691
+ Out->SetStringField(TEXT("category"),
692
+ Category.IsEmpty() ? TEXT("Project") : Category);
693
+ Out->SetObjectField(TEXT("settings"), SettingsObj);
694
+
695
+ SendAutomationResponse(RequestingSocket, RequestId, true,
696
+ TEXT("Project settings retrieved"), Out, FString());
697
+ return true;
698
+ #else
699
+ SendAutomationResponse(RequestingSocket, RequestId, false,
700
+ TEXT("get_project_settings requires editor build"),
701
+ nullptr, TEXT("NOT_IMPLEMENTED"));
702
+ return true;
703
+ #endif
704
+ }
705
+
706
+ if (LowerSub == TEXT("get_engine_version")) {
707
+ #if WITH_EDITOR
708
+ const FEngineVersion &EngineVer = FEngineVersion::Current();
709
+ TSharedPtr<FJsonObject> Out = MakeShared<FJsonObject>();
710
+ Out->SetStringField(TEXT("version"), EngineVer.ToString());
711
+ Out->SetNumberField(TEXT("major"), EngineVer.GetMajor());
712
+ Out->SetNumberField(TEXT("minor"), EngineVer.GetMinor());
713
+ Out->SetNumberField(TEXT("patch"), EngineVer.GetPatch());
714
+ const bool bIs56OrAbove =
715
+ (EngineVer.GetMajor() > 5) ||
716
+ (EngineVer.GetMajor() == 5 && EngineVer.GetMinor() >= 6);
717
+ Out->SetBoolField(TEXT("isUE56OrAbove"), bIs56OrAbove);
718
+ SendAutomationResponse(RequestingSocket, RequestId, true,
719
+ TEXT("Engine version retrieved"), Out, FString());
720
+ return true;
721
+ #else
722
+ SendAutomationResponse(RequestingSocket, RequestId, false,
723
+ TEXT("get_engine_version requires editor build"),
724
+ nullptr, TEXT("NOT_IMPLEMENTED"));
725
+ return true;
726
+ #endif
727
+ }
728
+
729
+ if (LowerSub == TEXT("get_feature_flags")) {
730
+ #if WITH_EDITOR
731
+ bool bUnrealEditor = false;
732
+ bool bLevelEditor = false;
733
+ bool bEditorActor = false;
734
+
735
+ if (GEditor) {
736
+ if (UUnrealEditorSubsystem *UnrealEditorSS =
737
+ GEditor->GetEditorSubsystem<UUnrealEditorSubsystem>()) {
738
+ bUnrealEditor = true;
739
+ }
740
+ if (ULevelEditorSubsystem *LevelEditorSS =
741
+ GEditor->GetEditorSubsystem<ULevelEditorSubsystem>()) {
742
+ bLevelEditor = true;
743
+ }
744
+ if (UEditorActorSubsystem *ActorSS =
745
+ GEditor->GetEditorSubsystem<UEditorActorSubsystem>()) {
746
+ bEditorActor = true;
747
+ }
748
+ }
749
+
750
+ TSharedPtr<FJsonObject> SubsystemsObj = MakeShared<FJsonObject>();
751
+ SubsystemsObj->SetBoolField(TEXT("unrealEditor"), bUnrealEditor);
752
+ SubsystemsObj->SetBoolField(TEXT("levelEditor"), bLevelEditor);
753
+ SubsystemsObj->SetBoolField(TEXT("editorActor"), bEditorActor);
754
+
755
+ TSharedPtr<FJsonObject> Out = MakeShared<FJsonObject>();
756
+ Out->SetObjectField(TEXT("subsystems"), SubsystemsObj);
757
+
758
+ SendAutomationResponse(RequestingSocket, RequestId, true,
759
+ TEXT("Feature flags retrieved"), Out, FString());
760
+ return true;
761
+ #else
762
+ SendAutomationResponse(RequestingSocket, RequestId, false,
763
+ TEXT("get_feature_flags requires editor build"),
764
+ nullptr, TEXT("NOT_IMPLEMENTED"));
765
+ return true;
766
+ #endif
767
+ }
768
+
769
+ if (LowerSub == TEXT("set_project_setting")) {
770
+ #if WITH_EDITOR
771
+ FString Section;
772
+ FString Key;
773
+ FString Value;
774
+ FString ConfigName;
775
+
776
+ if (!Payload->TryGetStringField(TEXT("section"), Section) ||
777
+ !Payload->TryGetStringField(TEXT("key"), Key) ||
778
+ !Payload->TryGetStringField(TEXT("value"), Value)) {
779
+ SendAutomationResponse(RequestingSocket, RequestId, false,
780
+ TEXT("Missing section, key, or value"), nullptr,
781
+ TEXT("INVALID_ARGUMENT"));
782
+ return true;
783
+ }
784
+
785
+ // Default to GGameIni (DefaultGame.ini) but allow overrides
786
+ if (!Payload->TryGetStringField(TEXT("configName"), ConfigName) ||
787
+ ConfigName.IsEmpty()) {
788
+ ConfigName = GGameIni;
789
+ } else if (ConfigName == TEXT("Engine")) {
790
+ ConfigName = GEngineIni;
791
+ } else if (ConfigName == TEXT("Input")) {
792
+ ConfigName = GInputIni;
793
+ } else if (ConfigName == TEXT("Game")) {
794
+ ConfigName = GGameIni;
795
+ }
796
+
797
+ if (!GConfig) {
798
+ SendAutomationResponse(RequestingSocket, RequestId, false,
799
+ TEXT("GConfig not available"), nullptr,
800
+ TEXT("ENGINE_ERROR"));
801
+ return true;
802
+ }
803
+
804
+ GConfig->SetString(*Section, *Key, *Value, ConfigName);
805
+ GConfig->Flush(false, ConfigName);
806
+
807
+ SendAutomationResponse(
808
+ RequestingSocket, RequestId, true,
809
+ FString::Printf(TEXT("Project setting set: [%s] %s = %s"), *Section,
810
+ *Key, *Value),
811
+ nullptr);
812
+ return true;
813
+ #else
814
+ SendAutomationResponse(RequestingSocket, RequestId, false,
815
+ TEXT("set_project_setting requires editor build"),
816
+ nullptr, TEXT("NOT_IMPLEMENTED"));
817
+ return true;
818
+ #endif
819
+ }
820
+
821
+ if (LowerSub == TEXT("validate_assets")) {
822
+ #if WITH_EDITOR
823
+ const TArray<TSharedPtr<FJsonValue>> *PathsPtr = nullptr;
824
+ if (!Payload->TryGetArrayField(TEXT("paths"), PathsPtr) || !PathsPtr) {
825
+ SendAutomationResponse(RequestingSocket, RequestId, false,
826
+ TEXT("paths array required"), nullptr,
827
+ TEXT("INVALID_ARGUMENT"));
828
+ return true;
829
+ }
830
+
831
+ TArray<FString> AssetPaths;
832
+ for (const auto &Val : *PathsPtr) {
833
+ if (Val.IsValid() && Val->Type == EJson::String) {
834
+ AssetPaths.Add(Val->AsString());
835
+ }
836
+ }
837
+
838
+ if (AssetPaths.Num() == 0) {
839
+ SendAutomationResponse(RequestingSocket, RequestId, false,
840
+ TEXT("No paths provided"), nullptr,
841
+ TEXT("INVALID_ARGUMENT"));
842
+ return true;
843
+ }
844
+
845
+ if (GEditor) {
846
+ if (UEditorValidatorSubsystem *Validator =
847
+ GEditor->GetEditorSubsystem<UEditorValidatorSubsystem>()) {
848
+ FValidateAssetsSettings Settings;
849
+ Settings.bSkipExcludedDirectories = true;
850
+ Settings.bShowIfNoFailures = false;
851
+ Settings.ValidationUsecase = EDataValidationUsecase::Script;
852
+
853
+ TArray<FAssetData> AssetsToValidate;
854
+ for (const FString &Path : AssetPaths) {
855
+ // Simple logic: if it's a folder, list assets; if it's a file, try to
856
+ // find it. We assume anything without a dot is a folder, effectively.
857
+ // But UEditorAssetLibrary::ListAssets works recursively on module
858
+ // paths.
859
+ if (UEditorAssetLibrary::DoesDirectoryExist(Path)) {
860
+ TArray<FString> FoundAssets =
861
+ UEditorAssetLibrary::ListAssets(Path, true);
862
+ for (const FString &AssetPath : FoundAssets) {
863
+ FAssetData AssetData =
864
+ UEditorAssetLibrary::FindAssetData(AssetPath);
865
+ if (AssetData.IsValid()) {
866
+ AssetsToValidate.Add(AssetData);
867
+ }
868
+ }
869
+ } else {
870
+ FAssetData SpecificAsset = UEditorAssetLibrary::FindAssetData(Path);
871
+ if (SpecificAsset.IsValid()) {
872
+ AssetsToValidate.AddUnique(SpecificAsset);
873
+ }
874
+ }
875
+ }
876
+
877
+ if (AssetsToValidate.Num() == 0) {
878
+ Result->SetBoolField(TEXT("success"), true);
879
+ Result->SetStringField(TEXT("message"),
880
+ TEXT("No assets found to validate"));
881
+ SendAutomationResponse(RequestingSocket, RequestId, true,
882
+ TEXT("Validation skipped (no assets)"), Result,
883
+ FString());
884
+ return true;
885
+ }
886
+
887
+ FValidateAssetsResults ValidationResults;
888
+ int32 NumChecked = Validator->ValidateAssetsWithSettings(
889
+ AssetsToValidate, Settings, ValidationResults);
890
+
891
+ Result->SetNumberField(TEXT("checkedCount"), NumChecked);
892
+ Result->SetNumberField(TEXT("failedCount"),
893
+ ValidationResults.NumInvalid);
894
+ Result->SetNumberField(TEXT("warningCount"),
895
+ ValidationResults.NumWarnings);
896
+ Result->SetNumberField(TEXT("skippedCount"),
897
+ ValidationResults.NumSkipped);
898
+
899
+ bool bOverallSuccess = (ValidationResults.NumInvalid == 0);
900
+ Result->SetStringField(
901
+ TEXT("result"), bOverallSuccess ? TEXT("Valid") : TEXT("Invalid"));
902
+
903
+ SendAutomationResponse(RequestingSocket, RequestId, true,
904
+ bOverallSuccess ? TEXT("Validation Passed")
905
+ : TEXT("Validation Failed"),
906
+ Result, FString());
907
+ return true;
908
+ } else {
909
+ SendAutomationResponse(RequestingSocket, RequestId, false,
910
+ TEXT("EditorValidatorSubsystem not available"),
911
+ nullptr, TEXT("SUBSYSTEM_MISSING"));
912
+ return true;
913
+ }
914
+ }
915
+ return true;
916
+ #else
917
+ SendAutomationResponse(RequestingSocket, RequestId, false,
918
+ TEXT("validate_assets requires editor build"),
919
+ nullptr, TEXT("NOT_IMPLEMENTED"));
920
+ return true;
921
+ #endif
922
+ }
923
+
924
+ // Engine quit (disabled for safety)
925
+ if (LowerSub == TEXT("engine_quit")) {
926
+ SendAutomationResponse(RequestingSocket, RequestId, false,
927
+ TEXT("Engine quit command is disabled for safety"),
928
+ nullptr, TEXT("NOT_ALLOWED"));
929
+ return true;
930
+ }
931
+
932
+ // Unknown sub-action: return false to allow other handlers (e.g.
933
+ // HandleUiAction) to attempt handling it.
934
+ // NOTE: Simple return false is not enough if the dispatcher doesn't fallback.
935
+ // We explicitly try the UI handler here as system_control and ui actions
936
+ // overlap.
937
+ return HandleUiAction(RequestId, Action, Payload, RequestingSocket);
938
+ }
939
+
940
+ bool UMcpAutomationBridgeSubsystem::HandleConsoleCommandAction(
941
+ const FString &RequestId, const FString &Action,
942
+ const TSharedPtr<FJsonObject> &Payload,
943
+ TSharedPtr<FMcpBridgeWebSocket> RequestingSocket) {
944
+ if (!Action.Equals(TEXT("console_command"), ESearchCase::IgnoreCase)) {
945
+ return false;
946
+ }
947
+
948
+ if (!Payload.IsValid()) {
949
+ SendAutomationResponse(RequestingSocket, RequestId, false,
950
+ TEXT("Console command requires valid payload"),
951
+ nullptr, TEXT("INVALID_PAYLOAD"));
952
+ return true;
953
+ }
954
+
955
+ FString Command;
956
+ if (!Payload->TryGetStringField(TEXT("command"), Command)) {
957
+ SendAutomationResponse(RequestingSocket, RequestId, false,
958
+ TEXT("Console command requires command parameter"),
959
+ nullptr, TEXT("INVALID_ARGUMENT"));
960
+ return true;
961
+ }
962
+
963
+ // Block dangerous commands (Defense-in-Depth)
964
+ FString LowerCommand = Command.ToLower();
965
+
966
+ // 1. Explicit command blocking
967
+ TArray<FString> ExplicitBlockedCommands = {
968
+ TEXT("quit"), TEXT("exit"), TEXT("crash"), TEXT("shutdown"),
969
+ TEXT("restart"), TEXT("reboot"), TEXT("debug exec")};
970
+
971
+ for (const FString &Blocked : ExplicitBlockedCommands) {
972
+ if (LowerCommand.Equals(Blocked) ||
973
+ LowerCommand.StartsWith(Blocked + TEXT(" "))) {
974
+ SendAutomationResponse(
975
+ RequestingSocket, RequestId, false,
976
+ FString::Printf(TEXT("Command '%s' is explicitly blocked for safety"),
977
+ *Command),
978
+ nullptr, TEXT("COMMAND_BLOCKED"));
979
+ return true;
980
+ }
981
+ }
982
+
983
+ // 2. Token-based blocking (preventing system commands, file manipulation, and
984
+ // python hacks)
985
+ TArray<FString> ForbiddenTokens = {TEXT("rm "),
986
+ TEXT("rm-"),
987
+ TEXT("del "),
988
+ TEXT("format "),
989
+ TEXT("rmdir"),
990
+ TEXT("mklink"),
991
+ TEXT("copy "),
992
+ TEXT("move "),
993
+ TEXT("start \""),
994
+ TEXT("system("),
995
+ TEXT("import os"),
996
+ TEXT("import subprocess"),
997
+ TEXT("subprocess."),
998
+ TEXT("os.system"),
999
+ TEXT("exec("),
1000
+ TEXT("eval("),
1001
+ TEXT("__import__"),
1002
+ TEXT("import sys"),
1003
+ TEXT("import importlib"),
1004
+ TEXT("with open"),
1005
+ TEXT("open(")};
1006
+
1007
+ for (const FString &Token : ForbiddenTokens) {
1008
+ if (LowerCommand.Contains(Token)) {
1009
+ SendAutomationResponse(
1010
+ RequestingSocket, RequestId, false,
1011
+ FString::Printf(
1012
+ TEXT("Command '%s' contains forbidden token '%s' and is blocked"),
1013
+ *Command, *Token),
1014
+ nullptr, TEXT("COMMAND_BLOCKED"));
1015
+ return true;
1016
+ }
1017
+ }
1018
+
1019
+ // 3. Block Chaining
1020
+ if (LowerCommand.Contains(TEXT("&&")) || LowerCommand.Contains(TEXT("||"))) {
1021
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1022
+ TEXT("Command chaining is blocked for safety"),
1023
+ nullptr, TEXT("COMMAND_BLOCKED"));
1024
+ return true;
1025
+ }
1026
+
1027
+ // Execute the command
1028
+ try {
1029
+ UWorld *TargetWorld = nullptr;
1030
+ #if WITH_EDITOR
1031
+ if (GEditor) {
1032
+ // Prefer PIE world if active, otherwise Editor world
1033
+ TargetWorld = GEditor->PlayWorld;
1034
+ if (!TargetWorld) {
1035
+ TargetWorld = GEditor->GetEditorWorldContext().World();
1036
+ }
1037
+ }
1038
+ #endif
1039
+
1040
+ // Fallback to GWorld if no editor/PIE world found (e.g. game mode)
1041
+ if (!TargetWorld && GEngine) {
1042
+ // Note: In some contexts GWorld is a macro for a proxy, but here we need
1043
+ // a raw pointer. We'll rely on Exec handling nullptr if we really can't
1044
+ // find one, but explicitly passing the editor world fixes many "command
1045
+ // not handled" or crash issues.
1046
+ }
1047
+
1048
+ GEngine->Exec(TargetWorld, *Command);
1049
+
1050
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
1051
+ Result->SetStringField(TEXT("command"), Command);
1052
+ Result->SetBoolField(TEXT("executed"), true);
1053
+
1054
+ SendAutomationResponse(
1055
+ RequestingSocket, RequestId, true,
1056
+ FString::Printf(TEXT("Executed console command: %s"), *Command), Result,
1057
+ FString());
1058
+ return true;
1059
+ } catch (...) {
1060
+ SendAutomationResponse(
1061
+ RequestingSocket, RequestId, false,
1062
+ FString::Printf(TEXT("Failed to execute command: %s"), *Command),
1063
+ nullptr, TEXT("EXECUTION_FAILED"));
1064
+ return true;
1065
+ }
1066
+ }
1067
+
1068
+ bool UMcpAutomationBridgeSubsystem::HandleInspectAction(
1069
+ const FString &RequestId, const FString &Action,
1070
+ const TSharedPtr<FJsonObject> &Payload,
1071
+ TSharedPtr<FMcpBridgeWebSocket> RequestingSocket) {
1072
+ if (!Action.Equals(TEXT("inspect"), ESearchCase::IgnoreCase)) {
1073
+ return false;
1074
+ }
1075
+
1076
+ if (!Payload.IsValid()) {
1077
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1078
+ TEXT("Inspect action requires valid payload"),
1079
+ nullptr, TEXT("INVALID_PAYLOAD"));
1080
+ return true;
1081
+ }
1082
+
1083
+ FString SubAction;
1084
+ if (!Payload->TryGetStringField(TEXT("action"), SubAction)) {
1085
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1086
+ TEXT("Inspect action requires action parameter"),
1087
+ nullptr, TEXT("INVALID_ARGUMENT"));
1088
+ return true;
1089
+ }
1090
+
1091
+ FString LowerSub = SubAction.ToLower();
1092
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
1093
+
1094
+ // Inspect object
1095
+ if (LowerSub == TEXT("inspect_object")) {
1096
+ FString ObjectPath;
1097
+ if (!Payload->TryGetStringField(TEXT("objectPath"), ObjectPath)) {
1098
+ SendAutomationResponse(
1099
+ RequestingSocket, RequestId, false,
1100
+ TEXT("inspect_object requires objectPath parameter"), nullptr,
1101
+ TEXT("INVALID_ARGUMENT"));
1102
+ return true;
1103
+ }
1104
+
1105
+ UObject *TargetObject = FindObject<UObject>(nullptr, *ObjectPath);
1106
+
1107
+ // Compatibility: allow passing actor label/name/path as objectPath.
1108
+ // Many callers use simple names like "MyActor".
1109
+ if (!TargetObject) {
1110
+ if (AActor *FoundActor = FindActorByName(ObjectPath)) {
1111
+ TargetObject = FoundActor;
1112
+ ObjectPath = FoundActor->GetPathName();
1113
+ }
1114
+ }
1115
+ if (!TargetObject) {
1116
+ SendAutomationResponse(
1117
+ RequestingSocket, RequestId, false,
1118
+ FString::Printf(TEXT("Object not found: %s"), *ObjectPath), nullptr,
1119
+ TEXT("OBJECT_NOT_FOUND"));
1120
+ return true;
1121
+ }
1122
+
1123
+ Result->SetStringField(TEXT("objectPath"), ObjectPath);
1124
+ Result->SetStringField(TEXT("objectName"), TargetObject->GetName());
1125
+ Result->SetStringField(TEXT("objectClass"),
1126
+ TargetObject->GetClass()->GetName());
1127
+ Result->SetStringField(TEXT("objectType"),
1128
+ TargetObject->GetClass()->GetFName().ToString());
1129
+
1130
+ SendAutomationResponse(
1131
+ RequestingSocket, RequestId, true,
1132
+ FString::Printf(TEXT("Inspected object: %s"), *ObjectPath), Result,
1133
+ FString());
1134
+ return true;
1135
+ }
1136
+
1137
+ // Get property
1138
+ if (LowerSub == TEXT("get_property")) {
1139
+ FString ObjectPath;
1140
+ FString PropertyName;
1141
+
1142
+ if (!Payload->TryGetStringField(TEXT("objectPath"), ObjectPath) ||
1143
+ !Payload->TryGetStringField(TEXT("propertyName"), PropertyName)) {
1144
+ SendAutomationResponse(
1145
+ RequestingSocket, RequestId, false,
1146
+ TEXT("get_property requires objectPath and propertyName parameters"),
1147
+ nullptr, TEXT("INVALID_ARGUMENT"));
1148
+ return true;
1149
+ }
1150
+
1151
+ UObject *TargetObject = FindObject<UObject>(nullptr, *ObjectPath);
1152
+
1153
+ // Compatibility: allow passing actor label/name/path as objectPath.
1154
+ if (!TargetObject) {
1155
+ if (AActor *FoundActor = FindActorByName(ObjectPath)) {
1156
+ TargetObject = FoundActor;
1157
+ ObjectPath = FoundActor->GetPathName();
1158
+ }
1159
+ }
1160
+ if (!TargetObject) {
1161
+ SendAutomationResponse(
1162
+ RequestingSocket, RequestId, false,
1163
+ FString::Printf(TEXT("Object not found: %s"), *ObjectPath), nullptr,
1164
+ TEXT("OBJECT_NOT_FOUND"));
1165
+ return true;
1166
+ }
1167
+
1168
+ UClass *ObjectClass = TargetObject->GetClass();
1169
+ FProperty *Property = ObjectClass->FindPropertyByName(*PropertyName);
1170
+
1171
+ if (!Property) {
1172
+ SendAutomationResponse(
1173
+ RequestingSocket, RequestId, false,
1174
+ FString::Printf(TEXT("Property not found: %s"), *PropertyName),
1175
+ nullptr, TEXT("PROPERTY_NOT_FOUND"));
1176
+ return true;
1177
+ }
1178
+
1179
+ Result->SetStringField(TEXT("objectPath"), ObjectPath);
1180
+ Result->SetStringField(TEXT("propertyName"), PropertyName);
1181
+ Result->SetStringField(TEXT("propertyType"),
1182
+ Property->GetClass()->GetName());
1183
+
1184
+ // Return value as string for broad compatibility.
1185
+ FString ValueText;
1186
+ const void *ValuePtr = Property->ContainerPtrToValuePtr<void>(TargetObject);
1187
+ Property->ExportTextItem_Direct(ValueText, ValuePtr, nullptr, TargetObject,
1188
+ PPF_None);
1189
+ Result->SetStringField(TEXT("value"), ValueText);
1190
+
1191
+ SendAutomationResponse(RequestingSocket, RequestId, true,
1192
+ FString::Printf(TEXT("Retrieved property: %s.%s"),
1193
+ *ObjectPath, *PropertyName),
1194
+ Result, FString());
1195
+ return true;
1196
+ }
1197
+
1198
+ // Set property (simplified implementation)
1199
+ if (LowerSub == TEXT("set_property")) {
1200
+ FString ObjectPath;
1201
+ FString PropertyName;
1202
+
1203
+ if (!Payload->TryGetStringField(TEXT("objectPath"), ObjectPath) ||
1204
+ !Payload->TryGetStringField(TEXT("propertyName"), PropertyName)) {
1205
+ SendAutomationResponse(
1206
+ RequestingSocket, RequestId, false,
1207
+ TEXT("set_property requires objectPath and propertyName parameters"),
1208
+ nullptr, TEXT("INVALID_ARGUMENT"));
1209
+ return true;
1210
+ }
1211
+
1212
+ // Critical Property Protection
1213
+ TArray<FString> ProtectedProperties = {TEXT("Class"), TEXT("Outer"),
1214
+ TEXT("Archetype"), TEXT("Linker"),
1215
+ TEXT("LinkerIndex")};
1216
+ if (ProtectedProperties.Contains(PropertyName)) {
1217
+ SendAutomationResponse(
1218
+ RequestingSocket, RequestId, false,
1219
+ FString::Printf(
1220
+ TEXT("Modification of critical property '%s' is blocked"),
1221
+ *PropertyName),
1222
+ nullptr, TEXT("PROPERTY_BLOCKED"));
1223
+ return true;
1224
+ }
1225
+
1226
+ UObject *TargetObject = FindObject<UObject>(nullptr, *ObjectPath);
1227
+
1228
+ // Compatibility: allow passing actor label/name/path as objectPath.
1229
+ if (!TargetObject) {
1230
+ if (AActor *FoundActor = FindActorByName(ObjectPath)) {
1231
+ TargetObject = FoundActor;
1232
+ ObjectPath = FoundActor->GetPathName();
1233
+ }
1234
+ }
1235
+ if (!TargetObject) {
1236
+ SendAutomationResponse(
1237
+ RequestingSocket, RequestId, false,
1238
+ FString::Printf(TEXT("Object not found: %s"), *ObjectPath), nullptr,
1239
+ TEXT("OBJECT_NOT_FOUND"));
1240
+ return true;
1241
+ }
1242
+
1243
+ // Get the property value from payload
1244
+ FString PropertyValue;
1245
+ if (!Payload->TryGetStringField(TEXT("value"), PropertyValue)) {
1246
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1247
+ TEXT("set_property requires 'value' field"),
1248
+ nullptr, TEXT("INVALID_ARGUMENT"));
1249
+ return true;
1250
+ }
1251
+
1252
+ // Find the property using Unreal's reflection system
1253
+ FProperty *FoundProperty =
1254
+ TargetObject->GetClass()->FindPropertyByName(FName(*PropertyName));
1255
+ if (!FoundProperty) {
1256
+ SendAutomationResponse(
1257
+ RequestingSocket, RequestId, false,
1258
+ FString::Printf(TEXT("Property '%s' not found on object '%s'"),
1259
+ *PropertyName, *ObjectPath),
1260
+ nullptr, TEXT("PROPERTY_NOT_FOUND"));
1261
+ return true;
1262
+ }
1263
+
1264
+ // Set the property value based on type
1265
+ bool bSuccess = false;
1266
+ FString ErrorMessage;
1267
+
1268
+ if (FStrProperty *StrProp = CastField<FStrProperty>(FoundProperty)) {
1269
+ void *PropAddr = StrProp->ContainerPtrToValuePtr<void>(TargetObject);
1270
+ StrProp->SetPropertyValue(PropAddr, PropertyValue);
1271
+ bSuccess = true;
1272
+ } else if (FFloatProperty *FloatProp =
1273
+ CastField<FFloatProperty>(FoundProperty)) {
1274
+ void *PropAddr = FloatProp->ContainerPtrToValuePtr<void>(TargetObject);
1275
+ float Value = FCString::Atof(*PropertyValue);
1276
+ FloatProp->SetPropertyValue(PropAddr, Value);
1277
+ bSuccess = true;
1278
+ } else if (FDoubleProperty *DoubleProp =
1279
+ CastField<FDoubleProperty>(FoundProperty)) {
1280
+ void *PropAddr = DoubleProp->ContainerPtrToValuePtr<void>(TargetObject);
1281
+ double Value = FCString::Atod(*PropertyValue);
1282
+ DoubleProp->SetPropertyValue(PropAddr, Value);
1283
+ bSuccess = true;
1284
+ } else if (FIntProperty *IntProp = CastField<FIntProperty>(FoundProperty)) {
1285
+ void *PropAddr = IntProp->ContainerPtrToValuePtr<void>(TargetObject);
1286
+ int32 Value = FCString::Atoi(*PropertyValue);
1287
+ IntProp->SetPropertyValue(PropAddr, Value);
1288
+ bSuccess = true;
1289
+ } else if (FInt64Property *Int64Prop =
1290
+ CastField<FInt64Property>(FoundProperty)) {
1291
+ void *PropAddr = Int64Prop->ContainerPtrToValuePtr<void>(TargetObject);
1292
+ int64 Value = FCString::Atoi64(*PropertyValue);
1293
+ Int64Prop->SetPropertyValue(PropAddr, Value);
1294
+ bSuccess = true;
1295
+ } else if (FBoolProperty *BoolProp =
1296
+ CastField<FBoolProperty>(FoundProperty)) {
1297
+ void *PropAddr = BoolProp->ContainerPtrToValuePtr<void>(TargetObject);
1298
+ bool Value = PropertyValue.ToBool();
1299
+ BoolProp->SetPropertyValue(PropAddr, Value);
1300
+ bSuccess = true;
1301
+ } else if (FObjectProperty *ObjProp =
1302
+ CastField<FObjectProperty>(FoundProperty)) {
1303
+ // Try to find the object by path
1304
+ UObject *ObjValue = FindObject<UObject>(nullptr, *PropertyValue);
1305
+ if (ObjValue || PropertyValue.IsEmpty()) {
1306
+ void *PropAddr = ObjProp->ContainerPtrToValuePtr<void>(TargetObject);
1307
+ ObjProp->SetPropertyValue(PropAddr, ObjValue);
1308
+ bSuccess = true;
1309
+ } else {
1310
+ ErrorMessage = FString::Printf(
1311
+ TEXT("Object property requires valid object path, got: %s"),
1312
+ *PropertyValue);
1313
+ }
1314
+ } else {
1315
+ ErrorMessage =
1316
+ FString::Printf(TEXT("Property type '%s' not supported for setting"),
1317
+ *FoundProperty->GetClass()->GetName());
1318
+ }
1319
+
1320
+ if (bSuccess) {
1321
+ Result->SetStringField(TEXT("objectPath"), ObjectPath);
1322
+ Result->SetStringField(TEXT("propertyName"), PropertyName);
1323
+ Result->SetStringField(TEXT("value"), PropertyValue);
1324
+ SendAutomationResponse(RequestingSocket, RequestId, true,
1325
+ TEXT("Property set successfully"), Result,
1326
+ FString());
1327
+ } else {
1328
+ Result->SetStringField(TEXT("objectPath"), ObjectPath);
1329
+ Result->SetStringField(TEXT("propertyName"), PropertyName);
1330
+ Result->SetStringField(TEXT("error"), ErrorMessage);
1331
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1332
+ TEXT("Failed to set property"), Result,
1333
+ TEXT("PROPERTY_SET_FAILED"));
1334
+ }
1335
+ return true;
1336
+ }
1337
+
1338
+ // Get bounding box (get_bounding_box)
1339
+ if (LowerSub == TEXT("get_bounding_box")) {
1340
+ FString ActorName;
1341
+ Payload->TryGetStringField(TEXT("actorName"), ActorName);
1342
+ FString ObjectPath;
1343
+ Payload->TryGetStringField(TEXT("objectPath"), ObjectPath);
1344
+
1345
+ if (ActorName.IsEmpty() && ObjectPath.IsEmpty()) {
1346
+ SendAutomationResponse(
1347
+ RequestingSocket, RequestId, false,
1348
+ TEXT("get_bounding_box requires actorName or objectPath"), nullptr,
1349
+ TEXT("INVALID_ARGUMENT"));
1350
+ return true;
1351
+ }
1352
+
1353
+ AActor *TargetActor = nullptr;
1354
+ UPrimitiveComponent *PrimComp = nullptr;
1355
+
1356
+ #if WITH_EDITOR
1357
+ if (GEditor && !ActorName.IsEmpty()) {
1358
+ UEditorActorSubsystem *ActorSS =
1359
+ GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
1360
+ if (ActorSS) {
1361
+ TArray<AActor *> Actors = ActorSS->GetAllLevelActors();
1362
+ for (AActor *A : Actors) {
1363
+ if (A &&
1364
+ (A->GetActorLabel() == ActorName || A->GetName() == ActorName)) {
1365
+ TargetActor = A;
1366
+ break;
1367
+ }
1368
+ }
1369
+ }
1370
+ }
1371
+ #endif
1372
+
1373
+ if (!TargetActor && !ObjectPath.IsEmpty()) {
1374
+ UObject *Obj = FindObject<UObject>(nullptr, *ObjectPath);
1375
+ if (Obj) {
1376
+ if (AActor *A = Cast<AActor>(Obj)) {
1377
+ TargetActor = A;
1378
+ } else if (UPrimitiveComponent *PC = Cast<UPrimitiveComponent>(Obj)) {
1379
+ PrimComp = PC;
1380
+ }
1381
+ }
1382
+ }
1383
+
1384
+ FBox Box(ForceInit);
1385
+ bool bFound = false;
1386
+
1387
+ if (TargetActor) {
1388
+ Box = TargetActor->GetComponentsBoundingBox(true);
1389
+ bFound = true;
1390
+ } else if (PrimComp) {
1391
+ Box = PrimComp->Bounds.GetBox();
1392
+ bFound = true;
1393
+ }
1394
+
1395
+ if (bFound) {
1396
+ FVector Origin = Box.GetCenter();
1397
+ FVector Extent = Box.GetExtent();
1398
+ TSharedPtr<FJsonObject> BoxObj = MakeShared<FJsonObject>();
1399
+
1400
+ TSharedPtr<FJsonObject> OrgObj = MakeShared<FJsonObject>();
1401
+ OrgObj->SetNumberField(TEXT("x"), Origin.X);
1402
+ OrgObj->SetNumberField(TEXT("y"), Origin.Y);
1403
+ OrgObj->SetNumberField(TEXT("z"), Origin.Z);
1404
+ BoxObj->SetObjectField(TEXT("origin"), OrgObj);
1405
+
1406
+ TSharedPtr<FJsonObject> ExtObj = MakeShared<FJsonObject>();
1407
+ ExtObj->SetNumberField(TEXT("x"), Extent.X);
1408
+ ExtObj->SetNumberField(TEXT("y"), Extent.Y);
1409
+ ExtObj->SetNumberField(TEXT("z"), Extent.Z);
1410
+ BoxObj->SetObjectField(TEXT("extent"), ExtObj);
1411
+
1412
+ SendAutomationResponse(RequestingSocket, RequestId, true,
1413
+ TEXT("Bounding box retrieved"), BoxObj, FString());
1414
+ } else {
1415
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1416
+ TEXT("Object not found or has no bounds"), nullptr,
1417
+ TEXT("OBJECT_NOT_FOUND"));
1418
+ }
1419
+ return true;
1420
+ }
1421
+
1422
+ // Get components (get_components)
1423
+ if (LowerSub == TEXT("get_components")) {
1424
+ FString ObjectPath;
1425
+ if (!Payload->TryGetStringField(TEXT("objectPath"), ObjectPath)) {
1426
+ Payload->TryGetStringField(TEXT("actorName"), ObjectPath);
1427
+ }
1428
+
1429
+ if (ObjectPath.IsEmpty()) {
1430
+ SendAutomationResponse(
1431
+ RequestingSocket, RequestId, false,
1432
+ TEXT("get_components requires objectPath or actorName"), nullptr,
1433
+ TEXT("INVALID_ARGUMENT"));
1434
+ return true;
1435
+ }
1436
+
1437
+ AActor *FoundActor = FindActorByName(ObjectPath);
1438
+ if (!FoundActor) {
1439
+ if (UObject *Asset = UEditorAssetLibrary::LoadAsset(ObjectPath)) {
1440
+ if (UBlueprint *BP = Cast<UBlueprint>(Asset)) {
1441
+ if (BP->GeneratedClass) {
1442
+ FoundActor = Cast<AActor>(BP->GeneratedClass->GetDefaultObject());
1443
+ }
1444
+ }
1445
+ }
1446
+ }
1447
+
1448
+ if (!FoundActor) {
1449
+ SendAutomationResponse(
1450
+ RequestingSocket, RequestId, false,
1451
+ FString::Printf(TEXT("Actor or Blueprint not found: %s"),
1452
+ *ObjectPath),
1453
+ nullptr, TEXT("OBJECT_NOT_FOUND"));
1454
+ return true;
1455
+ }
1456
+
1457
+ TSharedPtr<FJsonObject> ComponentsObj = MakeShared<FJsonObject>();
1458
+ TArray<TSharedPtr<FJsonValue>> ComponentList;
1459
+
1460
+ for (UActorComponent *Comp : FoundActor->GetComponents()) {
1461
+ if (!Comp)
1462
+ continue;
1463
+ TSharedPtr<FJsonObject> CompData = MakeShared<FJsonObject>();
1464
+ CompData->SetStringField(TEXT("name"), Comp->GetName());
1465
+ CompData->SetStringField(TEXT("class"), Comp->GetClass()->GetName());
1466
+ CompData->SetStringField(TEXT("path"), Comp->GetPathName());
1467
+
1468
+ if (USceneComponent *SceneComp = Cast<USceneComponent>(Comp)) {
1469
+ CompData->SetBoolField(TEXT("isSceneComponent"), true);
1470
+ TSharedPtr<FJsonObject> LocObj = MakeShared<FJsonObject>();
1471
+ FVector Loc = SceneComp->GetRelativeLocation();
1472
+ LocObj->SetNumberField("x", Loc.X);
1473
+ LocObj->SetNumberField("y", Loc.Y);
1474
+ LocObj->SetNumberField("z", Loc.Z);
1475
+ CompData->SetObjectField("relativeLocation", LocObj);
1476
+ }
1477
+
1478
+ ComponentList.Add(MakeShared<FJsonValueObject>(CompData));
1479
+ }
1480
+
1481
+ TSharedPtr<FJsonObject> ComponentsResult = MakeShared<FJsonObject>();
1482
+ ComponentsResult->SetArrayField(TEXT("components"), ComponentList);
1483
+ ComponentsResult->SetNumberField(TEXT("count"), ComponentList.Num());
1484
+
1485
+ SendAutomationResponse(RequestingSocket, RequestId, true,
1486
+ TEXT("Actor components retrieved"), ComponentsResult,
1487
+ FString());
1488
+ return true;
1489
+ }
1490
+
1491
+ // Find by class (find_by_class)
1492
+ if (LowerSub == TEXT("find_by_class")) {
1493
+ #if WITH_EDITOR
1494
+ FString ClassName;
1495
+ Payload->TryGetStringField(TEXT("className"), ClassName);
1496
+ // Also accept classPath as alias
1497
+ if (ClassName.IsEmpty()) {
1498
+ Payload->TryGetStringField(TEXT("classPath"), ClassName);
1499
+ }
1500
+
1501
+ if (GEditor) {
1502
+ UEditorActorSubsystem *ActorSS =
1503
+ GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
1504
+ if (ActorSS) {
1505
+ TArray<AActor *> Actors = ActorSS->GetAllLevelActors();
1506
+ TArray<TSharedPtr<FJsonValue>> Matches;
1507
+
1508
+ // Normalize class name for matching
1509
+ FString SearchName = ClassName;
1510
+ FString SearchNameLower = ClassName.ToLower();
1511
+
1512
+ // Common prefix variants to try
1513
+ TArray<FString> SearchVariants;
1514
+ SearchVariants.Add(SearchName);
1515
+ if (!SearchName.StartsWith(TEXT("A")) &&
1516
+ !SearchName.Contains(TEXT("/"))) {
1517
+ SearchVariants.Add(TEXT("A") + SearchName); // AActor pattern
1518
+ }
1519
+ if (!SearchName.StartsWith(TEXT("U")) &&
1520
+ !SearchName.Contains(TEXT("/"))) {
1521
+ SearchVariants.Add(TEXT("U") + SearchName); // UObject pattern
1522
+ }
1523
+
1524
+ for (AActor *Actor : Actors) {
1525
+ if (!Actor)
1526
+ continue;
1527
+ FString ActorClassName = Actor->GetClass()->GetName();
1528
+ FString ActorClassPath = Actor->GetClass()->GetPathName();
1529
+ FString ActorClassLower = ActorClassName.ToLower();
1530
+
1531
+ bool bMatches = false;
1532
+ if (ClassName.IsEmpty()) {
1533
+ bMatches = true; // Return all actors if no filter
1534
+ } else {
1535
+ // Check all variants
1536
+ for (const FString &Variant : SearchVariants) {
1537
+ if (ActorClassName.Equals(Variant, ESearchCase::IgnoreCase) ||
1538
+ ActorClassName.Contains(Variant, ESearchCase::IgnoreCase) ||
1539
+ ActorClassPath.Contains(Variant, ESearchCase::IgnoreCase)) {
1540
+ bMatches = true;
1541
+ break;
1542
+ }
1543
+ }
1544
+ }
1545
+
1546
+ if (bMatches) {
1547
+ TSharedPtr<FJsonObject> Entry = MakeShared<FJsonObject>();
1548
+ Entry->SetStringField(TEXT("name"), Actor->GetActorLabel());
1549
+ Entry->SetStringField(TEXT("path"), Actor->GetPathName());
1550
+ Entry->SetStringField(TEXT("class"), ActorClassPath);
1551
+ Entry->SetStringField(TEXT("classShort"), ActorClassName);
1552
+ Matches.Add(MakeShared<FJsonValueObject>(Entry));
1553
+ }
1554
+ }
1555
+
1556
+ Result->SetBoolField(TEXT("success"), true);
1557
+ Result->SetArrayField(TEXT("actors"), Matches);
1558
+ Result->SetNumberField(TEXT("count"), Matches.Num());
1559
+ Result->SetStringField(TEXT("searchedFor"), ClassName);
1560
+ SendAutomationResponse(RequestingSocket, RequestId, true,
1561
+ TEXT("Found actors by class"), Result,
1562
+ FString());
1563
+ return true;
1564
+ }
1565
+ }
1566
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1567
+ TEXT("Editor not available"), nullptr,
1568
+ TEXT("EDITOR_NOT_AVAILABLE"));
1569
+ return true;
1570
+ #else
1571
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1572
+ TEXT("find_by_class requires editor build"), nullptr,
1573
+ TEXT("NOT_IMPLEMENTED"));
1574
+ return true;
1575
+ #endif
1576
+ }
1577
+
1578
+ // Inspect class (inspect_class)
1579
+ if (LowerSub == TEXT("inspect_class")) {
1580
+ FString ClassPath;
1581
+ if (!Payload->TryGetStringField(TEXT("classPath"), ClassPath)) {
1582
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1583
+ TEXT("classPath required"), nullptr,
1584
+ TEXT("INVALID_ARGUMENT"));
1585
+ return true;
1586
+ }
1587
+
1588
+ UClass *ResolvedClass = ResolveClassByName(ClassPath);
1589
+ if (!ResolvedClass) {
1590
+ // Try loading as asset
1591
+ if (UObject *Found =
1592
+ StaticLoadObject(UObject::StaticClass(), nullptr, *ClassPath)) {
1593
+ if (UBlueprint *BP = Cast<UBlueprint>(Found))
1594
+ ResolvedClass = BP->GeneratedClass;
1595
+ else if (UClass *C = Cast<UClass>(Found))
1596
+ ResolvedClass = C;
1597
+ }
1598
+ }
1599
+
1600
+ if (ResolvedClass) {
1601
+ Result->SetStringField(TEXT("className"), ResolvedClass->GetName());
1602
+ Result->SetStringField(TEXT("classPath"), ResolvedClass->GetPathName());
1603
+ if (ResolvedClass->GetSuperClass())
1604
+ Result->SetStringField(TEXT("parentClass"),
1605
+ ResolvedClass->GetSuperClass()->GetName());
1606
+
1607
+ // List properties
1608
+ TArray<TSharedPtr<FJsonValue>> Props;
1609
+ for (TFieldIterator<FProperty> PropIt(ResolvedClass); PropIt; ++PropIt) {
1610
+ FProperty *Prop = *PropIt;
1611
+ TSharedPtr<FJsonObject> P = MakeShared<FJsonObject>();
1612
+ P->SetStringField(TEXT("name"), Prop->GetName());
1613
+ P->SetStringField(TEXT("type"), Prop->GetClass()->GetName());
1614
+ Props.Add(MakeShared<FJsonValueObject>(P));
1615
+ }
1616
+ Result->SetArrayField(TEXT("properties"), Props);
1617
+
1618
+ SendAutomationResponse(RequestingSocket, RequestId, true,
1619
+ TEXT("Class inspected"), Result, FString());
1620
+ return true;
1621
+ }
1622
+
1623
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1624
+ TEXT("Class not found"), nullptr,
1625
+ TEXT("CLASS_NOT_FOUND"));
1626
+ return true;
1627
+ }
1628
+
1629
+ // Get components (get_components) - enumerate all components on an actor
1630
+ if (LowerSub == TEXT("get_components")) {
1631
+ #if WITH_EDITOR
1632
+ FString ActorName;
1633
+ Payload->TryGetStringField(TEXT("actorName"), ActorName);
1634
+ FString ObjectPath;
1635
+ Payload->TryGetStringField(TEXT("objectPath"), ObjectPath);
1636
+
1637
+ if (ActorName.IsEmpty() && ObjectPath.IsEmpty()) {
1638
+ SendAutomationResponse(
1639
+ RequestingSocket, RequestId, false,
1640
+ TEXT("get_components requires actorName or objectPath"), nullptr,
1641
+ TEXT("INVALID_ARGUMENT"));
1642
+ return true;
1643
+ }
1644
+
1645
+ AActor *TargetActor = nullptr;
1646
+ if (!ActorName.IsEmpty()) {
1647
+ TargetActor = FindActorByName(ActorName);
1648
+ }
1649
+ if (!TargetActor && !ObjectPath.IsEmpty()) {
1650
+ TargetActor = FindActorByName(ObjectPath);
1651
+ }
1652
+
1653
+ if (!TargetActor) {
1654
+ SendAutomationResponse(
1655
+ RequestingSocket, RequestId, false,
1656
+ FString::Printf(TEXT("Failed to get components for actor %s"),
1657
+ ActorName.IsEmpty() ? *ObjectPath : *ActorName),
1658
+ nullptr, TEXT("ACTOR_NOT_FOUND"));
1659
+ return true;
1660
+ }
1661
+
1662
+ TArray<TSharedPtr<FJsonValue>> ComponentsArray;
1663
+ for (UActorComponent *Comp : TargetActor->GetComponents()) {
1664
+ if (!Comp)
1665
+ continue;
1666
+ TSharedPtr<FJsonObject> Entry = MakeShared<FJsonObject>();
1667
+ Entry->SetStringField(TEXT("name"), Comp->GetName());
1668
+ Entry->SetStringField(TEXT("readableName"), Comp->GetReadableName());
1669
+ Entry->SetStringField(TEXT("class"), Comp->GetClass()
1670
+ ? Comp->GetClass()->GetPathName()
1671
+ : TEXT(""));
1672
+ Entry->SetStringField(TEXT("path"), Comp->GetPathName());
1673
+ if (USceneComponent *SceneComp = Cast<USceneComponent>(Comp)) {
1674
+ FVector Loc = SceneComp->GetRelativeLocation();
1675
+ FRotator Rot = SceneComp->GetRelativeRotation();
1676
+ FVector Scale = SceneComp->GetRelativeScale3D();
1677
+
1678
+ TSharedPtr<FJsonObject> LocObj = MakeShared<FJsonObject>();
1679
+ LocObj->SetNumberField(TEXT("x"), Loc.X);
1680
+ LocObj->SetNumberField(TEXT("y"), Loc.Y);
1681
+ LocObj->SetNumberField(TEXT("z"), Loc.Z);
1682
+ Entry->SetObjectField(TEXT("relativeLocation"), LocObj);
1683
+
1684
+ TSharedPtr<FJsonObject> RotObj = MakeShared<FJsonObject>();
1685
+ RotObj->SetNumberField(TEXT("pitch"), Rot.Pitch);
1686
+ RotObj->SetNumberField(TEXT("yaw"), Rot.Yaw);
1687
+ RotObj->SetNumberField(TEXT("roll"), Rot.Roll);
1688
+ Entry->SetObjectField(TEXT("relativeRotation"), RotObj);
1689
+
1690
+ TSharedPtr<FJsonObject> ScaleObj = MakeShared<FJsonObject>();
1691
+ ScaleObj->SetNumberField(TEXT("x"), Scale.X);
1692
+ ScaleObj->SetNumberField(TEXT("y"), Scale.Y);
1693
+ ScaleObj->SetNumberField(TEXT("z"), Scale.Z);
1694
+ Entry->SetObjectField(TEXT("relativeScale"), ScaleObj);
1695
+ }
1696
+ ComponentsArray.Add(MakeShared<FJsonValueObject>(Entry));
1697
+ }
1698
+
1699
+ Result->SetArrayField(TEXT("components"), ComponentsArray);
1700
+ Result->SetNumberField(TEXT("count"), ComponentsArray.Num());
1701
+ Result->SetStringField(TEXT("actorName"), TargetActor->GetActorLabel());
1702
+ SendAutomationResponse(RequestingSocket, RequestId, true,
1703
+ TEXT("Actor components retrieved"), Result,
1704
+ FString());
1705
+ return true;
1706
+ #else
1707
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1708
+ TEXT("get_components requires editor build"),
1709
+ nullptr, TEXT("NOT_IMPLEMENTED"));
1710
+ return true;
1711
+ #endif
1712
+ }
1713
+
1714
+ // Get component property (get_component_property)
1715
+ if (LowerSub == TEXT("get_component_property")) {
1716
+ #if WITH_EDITOR
1717
+ FString ActorName;
1718
+ Payload->TryGetStringField(TEXT("actorName"), ActorName);
1719
+ FString ObjectPath;
1720
+ Payload->TryGetStringField(TEXT("objectPath"), ObjectPath);
1721
+ FString ComponentName;
1722
+ Payload->TryGetStringField(TEXT("componentName"), ComponentName);
1723
+ FString PropertyName;
1724
+ Payload->TryGetStringField(TEXT("propertyName"), PropertyName);
1725
+
1726
+ if ((ActorName.IsEmpty() && ObjectPath.IsEmpty()) ||
1727
+ ComponentName.IsEmpty() || PropertyName.IsEmpty()) {
1728
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1729
+ TEXT("get_component_property requires "
1730
+ "actorName/objectPath, componentName, and "
1731
+ "propertyName"),
1732
+ nullptr, TEXT("INVALID_ARGUMENT"));
1733
+ return true;
1734
+ }
1735
+
1736
+ AActor *TargetActor = nullptr;
1737
+ if (!ActorName.IsEmpty()) {
1738
+ TargetActor = FindActorByName(ActorName);
1739
+ }
1740
+ if (!TargetActor && !ObjectPath.IsEmpty()) {
1741
+ TargetActor = FindActorByName(ObjectPath);
1742
+ }
1743
+
1744
+ if (!TargetActor) {
1745
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1746
+ TEXT("Actor not found"), nullptr,
1747
+ TEXT("ACTOR_NOT_FOUND"));
1748
+ return true;
1749
+ }
1750
+
1751
+ // Find component by name (fuzzy matching)
1752
+ UActorComponent *TargetComponent = nullptr;
1753
+ for (UActorComponent *Comp : TargetActor->GetComponents()) {
1754
+ if (!Comp)
1755
+ continue;
1756
+ if (Comp->GetName().Equals(ComponentName, ESearchCase::IgnoreCase) ||
1757
+ Comp->GetReadableName().Equals(ComponentName,
1758
+ ESearchCase::IgnoreCase) ||
1759
+ Comp->GetName().Contains(ComponentName, ESearchCase::IgnoreCase)) {
1760
+ TargetComponent = Comp;
1761
+ break;
1762
+ }
1763
+ }
1764
+
1765
+ if (!TargetComponent) {
1766
+ SendAutomationResponse(
1767
+ RequestingSocket, RequestId, false,
1768
+ FString::Printf(TEXT("Component not found on actor '%s': %s"),
1769
+ *TargetActor->GetActorLabel(), *ComponentName),
1770
+ nullptr, TEXT("COMPONENT_NOT_FOUND"));
1771
+ return true;
1772
+ }
1773
+
1774
+ FProperty *Property =
1775
+ TargetComponent->GetClass()->FindPropertyByName(*PropertyName);
1776
+ if (!Property) {
1777
+ SendAutomationResponse(
1778
+ RequestingSocket, RequestId, false,
1779
+ FString::Printf(TEXT("Property not found: %s"), *PropertyName),
1780
+ nullptr, TEXT("PROPERTY_NOT_FOUND"));
1781
+ return true;
1782
+ }
1783
+
1784
+ FString ValueText;
1785
+ const void *ValuePtr =
1786
+ Property->ContainerPtrToValuePtr<void>(TargetComponent);
1787
+ Property->ExportTextItem_Direct(ValueText, ValuePtr, nullptr,
1788
+ TargetComponent, PPF_None);
1789
+
1790
+ Result->SetStringField(TEXT("componentName"), TargetComponent->GetName());
1791
+ Result->SetStringField(TEXT("propertyName"), PropertyName);
1792
+ Result->SetStringField(TEXT("value"), ValueText);
1793
+ Result->SetStringField(TEXT("propertyType"),
1794
+ Property->GetClass()->GetName());
1795
+ SendAutomationResponse(RequestingSocket, RequestId, true,
1796
+ TEXT("Component property retrieved"), Result,
1797
+ FString());
1798
+ return true;
1799
+ #else
1800
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1801
+ TEXT("get_component_property requires editor build"),
1802
+ nullptr, TEXT("NOT_IMPLEMENTED"));
1803
+ return true;
1804
+ #endif
1805
+ }
1806
+
1807
+ // Set component property (set_component_property)
1808
+ if (LowerSub == TEXT("set_component_property")) {
1809
+ #if WITH_EDITOR
1810
+ FString ActorName;
1811
+ Payload->TryGetStringField(TEXT("actorName"), ActorName);
1812
+ FString ObjectPath;
1813
+ Payload->TryGetStringField(TEXT("objectPath"), ObjectPath);
1814
+ FString ComponentName;
1815
+ Payload->TryGetStringField(TEXT("componentName"), ComponentName);
1816
+ FString PropertyName;
1817
+ Payload->TryGetStringField(TEXT("propertyName"), PropertyName);
1818
+
1819
+ if ((ActorName.IsEmpty() && ObjectPath.IsEmpty()) ||
1820
+ ComponentName.IsEmpty() || PropertyName.IsEmpty()) {
1821
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1822
+ TEXT("set_component_property requires "
1823
+ "actorName/objectPath, componentName, and "
1824
+ "propertyName"),
1825
+ nullptr, TEXT("INVALID_ARGUMENT"));
1826
+ return true;
1827
+ }
1828
+
1829
+ AActor *TargetActor = nullptr;
1830
+ if (!ActorName.IsEmpty()) {
1831
+ TargetActor = FindActorByName(ActorName);
1832
+ }
1833
+ if (!TargetActor && !ObjectPath.IsEmpty()) {
1834
+ TargetActor = FindActorByName(ObjectPath);
1835
+ }
1836
+
1837
+ if (!TargetActor) {
1838
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1839
+ TEXT("Actor not found"), nullptr,
1840
+ TEXT("ACTOR_NOT_FOUND"));
1841
+ return true;
1842
+ }
1843
+
1844
+ // Find component by name (fuzzy matching)
1845
+ UActorComponent *TargetComponent = nullptr;
1846
+ for (UActorComponent *Comp : TargetActor->GetComponents()) {
1847
+ if (!Comp)
1848
+ continue;
1849
+ if (Comp->GetName().Equals(ComponentName, ESearchCase::IgnoreCase) ||
1850
+ Comp->GetReadableName().Equals(ComponentName,
1851
+ ESearchCase::IgnoreCase) ||
1852
+ Comp->GetName().Contains(ComponentName, ESearchCase::IgnoreCase)) {
1853
+ TargetComponent = Comp;
1854
+ break;
1855
+ }
1856
+ }
1857
+
1858
+ if (!TargetComponent) {
1859
+ SendAutomationResponse(
1860
+ RequestingSocket, RequestId, false,
1861
+ FString::Printf(TEXT("Component not found on actor '%s': %s"),
1862
+ *TargetActor->GetActorLabel(), *ComponentName),
1863
+ nullptr, TEXT("COMPONENT_NOT_FOUND"));
1864
+ return true;
1865
+ }
1866
+
1867
+ FString PropertyValue;
1868
+ if (!Payload->TryGetStringField(TEXT("value"), PropertyValue)) {
1869
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1870
+ TEXT("set_component_property requires 'value'"),
1871
+ nullptr, TEXT("INVALID_ARGUMENT"));
1872
+ return true;
1873
+ }
1874
+
1875
+ FProperty *FoundProperty =
1876
+ TargetComponent->GetClass()->FindPropertyByName(FName(*PropertyName));
1877
+ if (!FoundProperty) {
1878
+ SendAutomationResponse(
1879
+ RequestingSocket, RequestId, false,
1880
+ FString::Printf(TEXT("Property '%s' not found on component"),
1881
+ *PropertyName),
1882
+ nullptr, TEXT("PROPERTY_NOT_FOUND"));
1883
+ return true;
1884
+ }
1885
+
1886
+ bool bSuccess = false;
1887
+ FString ErrorMessage;
1888
+
1889
+ if (FStrProperty *StrProp = CastField<FStrProperty>(FoundProperty)) {
1890
+ void *PropAddr = StrProp->ContainerPtrToValuePtr<void>(TargetComponent);
1891
+ StrProp->SetPropertyValue(PropAddr, PropertyValue);
1892
+ bSuccess = true;
1893
+ } else if (FFloatProperty *FloatProp =
1894
+ CastField<FFloatProperty>(FoundProperty)) {
1895
+ void *PropAddr = FloatProp->ContainerPtrToValuePtr<void>(TargetComponent);
1896
+ float Value = FCString::Atof(*PropertyValue);
1897
+ FloatProp->SetPropertyValue(PropAddr, Value);
1898
+ bSuccess = true;
1899
+ } else if (FDoubleProperty *DoubleProp =
1900
+ CastField<FDoubleProperty>(FoundProperty)) {
1901
+ void *PropAddr =
1902
+ DoubleProp->ContainerPtrToValuePtr<void>(TargetComponent);
1903
+ double Value = FCString::Atod(*PropertyValue);
1904
+ DoubleProp->SetPropertyValue(PropAddr, Value);
1905
+ bSuccess = true;
1906
+ } else if (FIntProperty *IntProp = CastField<FIntProperty>(FoundProperty)) {
1907
+ void *PropAddr = IntProp->ContainerPtrToValuePtr<void>(TargetComponent);
1908
+ int32 Value = FCString::Atoi(*PropertyValue);
1909
+ IntProp->SetPropertyValue(PropAddr, Value);
1910
+ bSuccess = true;
1911
+ } else if (FBoolProperty *BoolProp =
1912
+ CastField<FBoolProperty>(FoundProperty)) {
1913
+ void *PropAddr = BoolProp->ContainerPtrToValuePtr<void>(TargetComponent);
1914
+ bool Value = PropertyValue.ToBool();
1915
+ BoolProp->SetPropertyValue(PropAddr, Value);
1916
+ bSuccess = true;
1917
+ } else {
1918
+ ErrorMessage =
1919
+ FString::Printf(TEXT("Property type '%s' not supported for setting"),
1920
+ *FoundProperty->GetClass()->GetName());
1921
+ }
1922
+
1923
+ if (bSuccess) {
1924
+ if (USceneComponent *SceneComponent =
1925
+ Cast<USceneComponent>(TargetComponent)) {
1926
+ SceneComponent->MarkRenderStateDirty();
1927
+ SceneComponent->UpdateComponentToWorld();
1928
+ }
1929
+ TargetComponent->MarkPackageDirty();
1930
+
1931
+ Result->SetStringField(TEXT("componentName"), TargetComponent->GetName());
1932
+ Result->SetStringField(TEXT("propertyName"), PropertyName);
1933
+ Result->SetStringField(TEXT("value"), PropertyValue);
1934
+ SendAutomationResponse(RequestingSocket, RequestId, true,
1935
+ TEXT("Component property set"), Result, FString());
1936
+ } else {
1937
+ Result->SetStringField(TEXT("error"), ErrorMessage);
1938
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1939
+ TEXT("Failed to set component property"), Result,
1940
+ TEXT("PROPERTY_SET_FAILED"));
1941
+ }
1942
+ return true;
1943
+ #else
1944
+ SendAutomationResponse(RequestingSocket, RequestId, false,
1945
+ TEXT("set_component_property requires editor build"),
1946
+ nullptr, TEXT("NOT_IMPLEMENTED"));
1947
+ return true;
1948
+ #endif
1949
+ }
1950
+
1951
+ return true;
1952
+ }
1953
+
1954
+ bool UMcpAutomationBridgeSubsystem::HandleCreateProceduralTerrain(
1955
+ const FString &RequestId, const FString &Action,
1956
+ const TSharedPtr<FJsonObject> &Payload,
1957
+ TSharedPtr<FMcpBridgeWebSocket> RequestingSocket) {
1958
+ const FString Lower = Action.ToLower();
1959
+ if (!Lower.Equals(TEXT("create_procedural_terrain"),
1960
+ ESearchCase::IgnoreCase)) {
1961
+ return false;
1962
+ }
1963
+
1964
+ #if WITH_EDITOR
1965
+ if (!Payload.IsValid()) {
1966
+ SendAutomationError(RequestingSocket, RequestId,
1967
+ TEXT("create_procedural_terrain payload missing"),
1968
+ TEXT("INVALID_PAYLOAD"));
1969
+ return true;
1970
+ }
1971
+
1972
+ FString Name;
1973
+ if (!Payload->TryGetStringField(TEXT("name"), Name) || Name.IsEmpty()) {
1974
+ SendAutomationError(RequestingSocket, RequestId, TEXT("name required"),
1975
+ TEXT("INVALID_ARGUMENT"));
1976
+ return true;
1977
+ }
1978
+
1979
+ FVector Location(0, 0, 0);
1980
+ const TArray<TSharedPtr<FJsonValue>> *LocArr = nullptr;
1981
+ if (Payload->TryGetArrayField(TEXT("location"), LocArr) && LocArr &&
1982
+ LocArr->Num() >= 3) {
1983
+ Location.X = (*LocArr)[0]->AsNumber();
1984
+ Location.Y = (*LocArr)[1]->AsNumber();
1985
+ Location.Z = (*LocArr)[2]->AsNumber();
1986
+ }
1987
+
1988
+ double SizeX = 2000.0;
1989
+ double SizeY = 2000.0;
1990
+ Payload->TryGetNumberField(TEXT("sizeX"), SizeX);
1991
+ Payload->TryGetNumberField(TEXT("sizeY"), SizeY);
1992
+
1993
+ int32 Subdivisions = 50;
1994
+ Payload->TryGetNumberField(TEXT("subdivisions"), Subdivisions);
1995
+ Subdivisions = FMath::Clamp(Subdivisions, 2, 255);
1996
+
1997
+ FString MaterialPath;
1998
+ Payload->TryGetStringField(TEXT("material"), MaterialPath);
1999
+
2000
+ if (!GEditor) {
2001
+ SendAutomationError(RequestingSocket, RequestId,
2002
+ TEXT("Editor not available"),
2003
+ TEXT("EDITOR_NOT_AVAILABLE"));
2004
+ return true;
2005
+ }
2006
+
2007
+ UEditorActorSubsystem *ActorSS =
2008
+ GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
2009
+ if (!ActorSS) {
2010
+ SendAutomationError(RequestingSocket, RequestId,
2011
+ TEXT("EditorActorSubsystem not available"),
2012
+ TEXT("EDITOR_ACTOR_SUBSYSTEM_MISSING"));
2013
+ return true;
2014
+ }
2015
+
2016
+ AActor *NewActor = SpawnActorInActiveWorld<AActor>(
2017
+ AActor::StaticClass(), Location, FRotator::ZeroRotator, Name);
2018
+ if (!NewActor) {
2019
+ SendAutomationError(RequestingSocket, RequestId,
2020
+ TEXT("Failed to spawn actor"), TEXT("SPAWN_FAILED"));
2021
+ return true;
2022
+ }
2023
+
2024
+ UProceduralMeshComponent *ProcMesh = NewObject<UProceduralMeshComponent>(
2025
+ NewActor, FName(TEXT("ProceduralTerrain")));
2026
+ if (!ProcMesh) {
2027
+ ActorSS->DestroyActor(NewActor);
2028
+ SendAutomationError(RequestingSocket, RequestId,
2029
+ TEXT("Failed to create ProceduralMeshComponent"),
2030
+ TEXT("COMPONENT_CREATION_FAILED"));
2031
+ return true;
2032
+ }
2033
+
2034
+ ProcMesh->RegisterComponent();
2035
+ NewActor->SetRootComponent(ProcMesh);
2036
+ NewActor->AddInstanceComponent(ProcMesh);
2037
+
2038
+ // Generate grid
2039
+ TArray<FVector> Vertices;
2040
+ TArray<int32> Triangles;
2041
+ TArray<FVector> Normals;
2042
+ TArray<FVector2D> UV0;
2043
+ TArray<FColor> VertexColors;
2044
+ TArray<FProcMeshTangent> Tangents;
2045
+
2046
+ const float StepX = SizeX / Subdivisions;
2047
+ const float StepY = SizeY / Subdivisions;
2048
+ const float UVStep = 1.0f / Subdivisions;
2049
+
2050
+ for (int32 Y = 0; Y <= Subdivisions; Y++) {
2051
+ for (int32 X = 0; X <= Subdivisions; X++) {
2052
+ float Z = 0.0f;
2053
+ // Simple sine wave terrain as default since we can't easily parse the
2054
+ // math string
2055
+ Z = FMath::Sin(X * 0.1f) * 50.0f + FMath::Cos(Y * 0.1f) * 30.0f;
2056
+
2057
+ Vertices.Add(FVector(X * StepX - SizeX / 2, Y * StepY - SizeY / 2, Z));
2058
+ Normals.Add(FVector(0, 0, 1)); // Simplified normal
2059
+ UV0.Add(FVector2D(X * UVStep, Y * UVStep));
2060
+ VertexColors.Add(FColor::White);
2061
+ Tangents.Add(FProcMeshTangent(1, 0, 0));
2062
+ }
2063
+ }
2064
+
2065
+ for (int32 Y = 0; Y < Subdivisions; Y++) {
2066
+ for (int32 X = 0; X < Subdivisions; X++) {
2067
+ int32 TopLeft = Y * (Subdivisions + 1) + X;
2068
+ int32 TopRight = TopLeft + 1;
2069
+ int32 BottomLeft = (Y + 1) * (Subdivisions + 1) + X;
2070
+ int32 BottomRight = BottomLeft + 1;
2071
+
2072
+ Triangles.Add(TopLeft);
2073
+ Triangles.Add(BottomLeft);
2074
+ Triangles.Add(TopRight);
2075
+
2076
+ Triangles.Add(TopRight);
2077
+ Triangles.Add(BottomLeft);
2078
+ Triangles.Add(BottomRight);
2079
+ }
2080
+ }
2081
+
2082
+ ProcMesh->CreateMeshSection(0, Vertices, Triangles, Normals, UV0,
2083
+ VertexColors, Tangents, true);
2084
+
2085
+ if (!MaterialPath.IsEmpty()) {
2086
+ UMaterialInterface *Mat =
2087
+ LoadObject<UMaterialInterface>(nullptr, *MaterialPath);
2088
+ if (Mat) {
2089
+ ProcMesh->SetMaterial(0, Mat);
2090
+ }
2091
+ }
2092
+
2093
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
2094
+ Resp->SetBoolField(TEXT("success"), true);
2095
+ Resp->SetStringField(TEXT("actor_name"), NewActor->GetActorLabel());
2096
+ Resp->SetNumberField(TEXT("vertices"), Vertices.Num());
2097
+ Resp->SetNumberField(TEXT("triangles"), Triangles.Num() / 3);
2098
+
2099
+ TSharedPtr<FJsonObject> SizeObj = MakeShared<FJsonObject>();
2100
+ SizeObj->SetNumberField(TEXT("x"), SizeX);
2101
+ SizeObj->SetNumberField(TEXT("y"), SizeY);
2102
+ Resp->SetObjectField(TEXT("size"), SizeObj);
2103
+ Resp->SetNumberField(TEXT("subdivisions"), Subdivisions);
2104
+
2105
+ SendAutomationResponse(RequestingSocket, RequestId, true,
2106
+ TEXT("Procedural terrain created"), Resp, FString());
2107
+ return true;
2108
+ #else
2109
+ SendAutomationResponse(
2110
+ RequestingSocket, RequestId, false,
2111
+ TEXT("create_procedural_terrain requires editor build."), nullptr,
2112
+ TEXT("NOT_IMPLEMENTED"));
2113
+ return true;
2114
+ #endif
2115
+ }
2116
+
2117
+ bool UMcpAutomationBridgeSubsystem::HandleBakeLightmap(
2118
+ const FString &RequestId, const FString &Action,
2119
+ const TSharedPtr<FJsonObject> &Payload,
2120
+ TSharedPtr<FMcpBridgeWebSocket> RequestingSocket) {
2121
+ const FString Lower = Action.ToLower();
2122
+ if (!Lower.Equals(TEXT("bake_lightmap"), ESearchCase::IgnoreCase)) {
2123
+ return false;
2124
+ }
2125
+
2126
+ #if WITH_EDITOR
2127
+ FString QualityStr = TEXT("Preview");
2128
+ if (Payload.IsValid())
2129
+ Payload->TryGetStringField(TEXT("quality"), QualityStr);
2130
+
2131
+ // Reuse HandleExecuteEditorFunction logic
2132
+ TSharedPtr<FJsonObject> P = MakeShared<FJsonObject>();
2133
+ P->SetStringField(TEXT("functionName"), TEXT("BUILD_LIGHTING"));
2134
+ P->SetStringField(TEXT("quality"), QualityStr);
2135
+
2136
+ return HandleExecuteEditorFunction(RequestId, TEXT("execute_editor_function"),
2137
+ P, RequestingSocket);
2138
+
2139
+ #else
2140
+ SendAutomationResponse(RequestingSocket, RequestId, false,
2141
+ TEXT("Requires editor"), nullptr,
2142
+ TEXT("NOT_IMPLEMENTED"));
2143
+ return true;
2144
+ #endif
2145
+ }