unreal-engine-mcp-server 0.4.7 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (454) hide show
  1. package/.env.example +26 -0
  2. package/.env.production +38 -7
  3. package/.eslintrc.json +0 -54
  4. package/.eslintrc.override.json +8 -0
  5. package/.github/ISSUE_TEMPLATE/bug_report.yml +94 -0
  6. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  7. package/.github/ISSUE_TEMPLATE/feature_request.yml +56 -0
  8. package/.github/copilot-instructions.md +478 -45
  9. package/.github/dependabot.yml +19 -0
  10. package/.github/labeler.yml +24 -0
  11. package/.github/labels.yml +70 -0
  12. package/.github/pull_request_template.md +42 -0
  13. package/.github/release-drafter-config.yml +51 -0
  14. package/.github/workflows/auto-merge.yml +38 -0
  15. package/.github/workflows/ci.yml +38 -0
  16. package/.github/workflows/dependency-review.yml +17 -0
  17. package/.github/workflows/gemini-issue-triage.yml +172 -0
  18. package/.github/workflows/greetings.yml +27 -0
  19. package/.github/workflows/labeler.yml +17 -0
  20. package/.github/workflows/links.yml +80 -0
  21. package/.github/workflows/pr-size-labeler.yml +137 -0
  22. package/.github/workflows/publish-mcp.yml +13 -7
  23. package/.github/workflows/release-drafter.yml +23 -0
  24. package/.github/workflows/release.yml +112 -0
  25. package/.github/workflows/semantic-pull-request.yml +35 -0
  26. package/.github/workflows/smoke-test.yml +36 -0
  27. package/.github/workflows/stale.yml +28 -0
  28. package/CHANGELOG.md +338 -31
  29. package/CONTRIBUTING.md +140 -0
  30. package/GEMINI.md +115 -0
  31. package/Public/Plugin_setup_guide.mp4 +0 -0
  32. package/README.md +189 -128
  33. package/claude_desktop_config_example.json +7 -6
  34. package/dist/automation/bridge.d.ts +50 -0
  35. package/dist/automation/bridge.js +452 -0
  36. package/dist/automation/connection-manager.d.ts +23 -0
  37. package/dist/automation/connection-manager.js +107 -0
  38. package/dist/automation/handshake.d.ts +11 -0
  39. package/dist/automation/handshake.js +89 -0
  40. package/dist/automation/index.d.ts +3 -0
  41. package/dist/automation/index.js +3 -0
  42. package/dist/automation/message-handler.d.ts +12 -0
  43. package/dist/automation/message-handler.js +149 -0
  44. package/dist/automation/request-tracker.d.ts +25 -0
  45. package/dist/automation/request-tracker.js +98 -0
  46. package/dist/automation/types.d.ts +130 -0
  47. package/dist/automation/types.js +2 -0
  48. package/dist/cli.js +32 -5
  49. package/dist/config.d.ts +26 -0
  50. package/dist/config.js +59 -0
  51. package/dist/constants.d.ts +16 -0
  52. package/dist/constants.js +16 -0
  53. package/dist/graphql/loaders.d.ts +64 -0
  54. package/dist/graphql/loaders.js +117 -0
  55. package/dist/graphql/resolvers.d.ts +268 -0
  56. package/dist/graphql/resolvers.js +746 -0
  57. package/dist/graphql/schema.d.ts +5 -0
  58. package/dist/graphql/schema.js +437 -0
  59. package/dist/graphql/server.d.ts +26 -0
  60. package/dist/graphql/server.js +117 -0
  61. package/dist/graphql/types.d.ts +9 -0
  62. package/dist/graphql/types.js +2 -0
  63. package/dist/handlers/resource-handlers.d.ts +20 -0
  64. package/dist/handlers/resource-handlers.js +180 -0
  65. package/dist/index.d.ts +33 -18
  66. package/dist/index.js +130 -619
  67. package/dist/resources/actors.d.ts +17 -12
  68. package/dist/resources/actors.js +56 -76
  69. package/dist/resources/assets.d.ts +6 -14
  70. package/dist/resources/assets.js +115 -147
  71. package/dist/resources/levels.d.ts +13 -13
  72. package/dist/resources/levels.js +25 -34
  73. package/dist/server/resource-registry.d.ts +20 -0
  74. package/dist/server/resource-registry.js +37 -0
  75. package/dist/server/tool-registry.d.ts +23 -0
  76. package/dist/server/tool-registry.js +322 -0
  77. package/dist/server-setup.d.ts +20 -0
  78. package/dist/server-setup.js +71 -0
  79. package/dist/services/health-monitor.d.ts +34 -0
  80. package/dist/services/health-monitor.js +105 -0
  81. package/dist/services/metrics-server.d.ts +11 -0
  82. package/dist/services/metrics-server.js +105 -0
  83. package/dist/tools/actors.d.ts +163 -9
  84. package/dist/tools/actors.js +356 -311
  85. package/dist/tools/animation.d.ts +135 -4
  86. package/dist/tools/animation.js +510 -411
  87. package/dist/tools/assets.d.ts +75 -29
  88. package/dist/tools/assets.js +265 -284
  89. package/dist/tools/audio.d.ts +102 -42
  90. package/dist/tools/audio.js +272 -685
  91. package/dist/tools/base-tool.d.ts +17 -0
  92. package/dist/tools/base-tool.js +46 -0
  93. package/dist/tools/behavior-tree.d.ts +94 -0
  94. package/dist/tools/behavior-tree.js +39 -0
  95. package/dist/tools/blueprint.d.ts +208 -126
  96. package/dist/tools/blueprint.js +685 -832
  97. package/dist/tools/consolidated-tool-definitions.d.ts +5462 -1781
  98. package/dist/tools/consolidated-tool-definitions.js +829 -496
  99. package/dist/tools/consolidated-tool-handlers.d.ts +2 -1
  100. package/dist/tools/consolidated-tool-handlers.js +198 -1027
  101. package/dist/tools/debug.d.ts +143 -85
  102. package/dist/tools/debug.js +234 -180
  103. package/dist/tools/dynamic-handler-registry.d.ts +13 -0
  104. package/dist/tools/dynamic-handler-registry.js +23 -0
  105. package/dist/tools/editor.d.ts +30 -83
  106. package/dist/tools/editor.js +247 -244
  107. package/dist/tools/engine.d.ts +10 -4
  108. package/dist/tools/engine.js +13 -5
  109. package/dist/tools/environment.d.ts +30 -0
  110. package/dist/tools/environment.js +267 -0
  111. package/dist/tools/foliage.d.ts +65 -99
  112. package/dist/tools/foliage.js +221 -331
  113. package/dist/tools/handlers/actor-handlers.d.ts +3 -0
  114. package/dist/tools/handlers/actor-handlers.js +227 -0
  115. package/dist/tools/handlers/animation-handlers.d.ts +3 -0
  116. package/dist/tools/handlers/animation-handlers.js +185 -0
  117. package/dist/tools/handlers/argument-helper.d.ts +16 -0
  118. package/dist/tools/handlers/argument-helper.js +80 -0
  119. package/dist/tools/handlers/asset-handlers.d.ts +3 -0
  120. package/dist/tools/handlers/asset-handlers.js +496 -0
  121. package/dist/tools/handlers/audio-handlers.d.ts +3 -0
  122. package/dist/tools/handlers/audio-handlers.js +166 -0
  123. package/dist/tools/handlers/blueprint-handlers.d.ts +4 -0
  124. package/dist/tools/handlers/blueprint-handlers.js +358 -0
  125. package/dist/tools/handlers/common-handlers.d.ts +14 -0
  126. package/dist/tools/handlers/common-handlers.js +56 -0
  127. package/dist/tools/handlers/editor-handlers.d.ts +3 -0
  128. package/dist/tools/handlers/editor-handlers.js +119 -0
  129. package/dist/tools/handlers/effect-handlers.d.ts +3 -0
  130. package/dist/tools/handlers/effect-handlers.js +171 -0
  131. package/dist/tools/handlers/environment-handlers.d.ts +3 -0
  132. package/dist/tools/handlers/environment-handlers.js +170 -0
  133. package/dist/tools/handlers/graph-handlers.d.ts +3 -0
  134. package/dist/tools/handlers/graph-handlers.js +90 -0
  135. package/dist/tools/handlers/input-handlers.d.ts +3 -0
  136. package/dist/tools/handlers/input-handlers.js +21 -0
  137. package/dist/tools/handlers/inspect-handlers.d.ts +3 -0
  138. package/dist/tools/handlers/inspect-handlers.js +383 -0
  139. package/dist/tools/handlers/level-handlers.d.ts +3 -0
  140. package/dist/tools/handlers/level-handlers.js +237 -0
  141. package/dist/tools/handlers/lighting-handlers.d.ts +3 -0
  142. package/dist/tools/handlers/lighting-handlers.js +144 -0
  143. package/dist/tools/handlers/performance-handlers.d.ts +3 -0
  144. package/dist/tools/handlers/performance-handlers.js +130 -0
  145. package/dist/tools/handlers/pipeline-handlers.d.ts +3 -0
  146. package/dist/tools/handlers/pipeline-handlers.js +110 -0
  147. package/dist/tools/handlers/sequence-handlers.d.ts +3 -0
  148. package/dist/tools/handlers/sequence-handlers.js +376 -0
  149. package/dist/tools/handlers/system-handlers.d.ts +4 -0
  150. package/dist/tools/handlers/system-handlers.js +506 -0
  151. package/dist/tools/input.d.ts +19 -0
  152. package/dist/tools/input.js +89 -0
  153. package/dist/tools/introspection.d.ts +103 -40
  154. package/dist/tools/introspection.js +425 -568
  155. package/dist/tools/landscape.d.ts +54 -93
  156. package/dist/tools/landscape.js +284 -409
  157. package/dist/tools/level.d.ts +66 -27
  158. package/dist/tools/level.js +647 -675
  159. package/dist/tools/lighting.d.ts +77 -38
  160. package/dist/tools/lighting.js +445 -943
  161. package/dist/tools/logs.d.ts +3 -3
  162. package/dist/tools/logs.js +5 -57
  163. package/dist/tools/materials.d.ts +91 -24
  164. package/dist/tools/materials.js +194 -118
  165. package/dist/tools/niagara.d.ts +149 -39
  166. package/dist/tools/niagara.js +267 -182
  167. package/dist/tools/performance.d.ts +27 -13
  168. package/dist/tools/performance.js +203 -122
  169. package/dist/tools/physics.d.ts +32 -77
  170. package/dist/tools/physics.js +175 -582
  171. package/dist/tools/property-dictionary.d.ts +13 -0
  172. package/dist/tools/property-dictionary.js +82 -0
  173. package/dist/tools/sequence.d.ts +85 -60
  174. package/dist/tools/sequence.js +208 -747
  175. package/dist/tools/tool-definition-utils.d.ts +59 -0
  176. package/dist/tools/tool-definition-utils.js +35 -0
  177. package/dist/tools/ui.d.ts +64 -34
  178. package/dist/tools/ui.js +134 -214
  179. package/dist/types/automation-responses.d.ts +115 -0
  180. package/dist/types/automation-responses.js +2 -0
  181. package/dist/types/env.d.ts +0 -3
  182. package/dist/types/env.js +0 -7
  183. package/dist/types/responses.d.ts +249 -0
  184. package/dist/types/responses.js +2 -0
  185. package/dist/types/tool-interfaces.d.ts +898 -0
  186. package/dist/types/tool-interfaces.js +2 -0
  187. package/dist/types/tool-types.d.ts +183 -19
  188. package/dist/types/tool-types.js +0 -4
  189. package/dist/unreal-bridge.d.ts +24 -131
  190. package/dist/unreal-bridge.js +364 -1506
  191. package/dist/utils/command-validator.d.ts +9 -0
  192. package/dist/utils/command-validator.js +68 -0
  193. package/dist/utils/elicitation.d.ts +1 -1
  194. package/dist/utils/elicitation.js +12 -15
  195. package/dist/utils/error-handler.d.ts +2 -51
  196. package/dist/utils/error-handler.js +11 -87
  197. package/dist/utils/ini-reader.d.ts +3 -0
  198. package/dist/utils/ini-reader.js +69 -0
  199. package/dist/utils/logger.js +9 -6
  200. package/dist/utils/normalize.d.ts +3 -0
  201. package/dist/utils/normalize.js +56 -0
  202. package/dist/utils/path-security.d.ts +2 -0
  203. package/dist/utils/path-security.js +24 -0
  204. package/dist/utils/response-factory.d.ts +7 -0
  205. package/dist/utils/response-factory.js +27 -0
  206. package/dist/utils/response-validator.d.ts +3 -24
  207. package/dist/utils/response-validator.js +130 -81
  208. package/dist/utils/result-helpers.d.ts +4 -5
  209. package/dist/utils/result-helpers.js +15 -16
  210. package/dist/utils/safe-json.js +5 -11
  211. package/dist/utils/unreal-command-queue.d.ts +24 -0
  212. package/dist/utils/unreal-command-queue.js +120 -0
  213. package/dist/utils/validation.d.ts +0 -40
  214. package/dist/utils/validation.js +1 -78
  215. package/dist/wasm/index.d.ts +70 -0
  216. package/dist/wasm/index.js +535 -0
  217. package/docs/GraphQL-API.md +888 -0
  218. package/docs/Migration-Guide-v0.5.0.md +684 -0
  219. package/docs/Roadmap.md +53 -0
  220. package/docs/WebAssembly-Integration.md +628 -0
  221. package/docs/editor-plugin-extension.md +370 -0
  222. package/docs/handler-mapping.md +242 -0
  223. package/docs/native-automation-progress.md +128 -0
  224. package/docs/testing-guide.md +423 -0
  225. package/mcp-config-example.json +6 -6
  226. package/package.json +67 -28
  227. package/plugins/McpAutomationBridge/Config/FilterPlugin.ini +8 -0
  228. package/plugins/McpAutomationBridge/McpAutomationBridge.uplugin +64 -0
  229. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/McpAutomationBridge.Build.cs +189 -0
  230. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.cpp +22 -0
  231. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.h +30 -0
  232. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeHelpers.h +1983 -0
  233. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeModule.cpp +72 -0
  234. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSettings.cpp +46 -0
  235. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +581 -0
  236. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +2394 -0
  237. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetQueryHandlers.cpp +300 -0
  238. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetWorkflowHandlers.cpp +2807 -0
  239. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AudioHandlers.cpp +1087 -0
  240. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BehaviorTreeHandlers.cpp +488 -0
  241. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.cpp +643 -0
  242. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.h +31 -0
  243. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +1184 -0
  244. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +5652 -0
  245. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers_List.cpp +152 -0
  246. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ControlHandlers.cpp +2614 -0
  247. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_DebugHandlers.cpp +42 -0
  248. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EditorFunctionHandlers.cpp +1237 -0
  249. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +1701 -0
  250. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +2145 -0
  251. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_FoliageHandlers.cpp +954 -0
  252. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InputHandlers.cpp +209 -0
  253. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InsightsHandlers.cpp +41 -0
  254. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LandscapeHandlers.cpp +1164 -0
  255. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LevelHandlers.cpp +762 -0
  256. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +634 -0
  257. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LogHandlers.cpp +136 -0
  258. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_MaterialGraphHandlers.cpp +494 -0
  259. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraGraphHandlers.cpp +278 -0
  260. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraHandlers.cpp +625 -0
  261. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PerformanceHandlers.cpp +401 -0
  262. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PipelineHandlers.cpp +67 -0
  263. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +735 -0
  264. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PropertyHandlers.cpp +2634 -0
  265. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_RenderHandlers.cpp +189 -0
  266. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.cpp +917 -0
  267. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.h +39 -0
  268. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +2670 -0
  269. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequencerHandlers.cpp +519 -0
  270. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_TestHandlers.cpp +38 -0
  271. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_UiHandlers.cpp +668 -0
  272. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_WorldPartitionHandlers.cpp +346 -0
  273. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.cpp +1330 -0
  274. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.h +149 -0
  275. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +783 -0
  276. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSettings.h +115 -0
  277. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSubsystem.h +796 -0
  278. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpConnectionManager.h +117 -0
  279. package/scripts/check-unreal-connection.mjs +19 -0
  280. package/scripts/clean-tmp.js +23 -0
  281. package/scripts/patch-wasm.js +26 -0
  282. package/scripts/run-all-tests.mjs +136 -0
  283. package/scripts/smoke-test.ts +94 -0
  284. package/scripts/sync-mcp-plugin.js +143 -0
  285. package/scripts/test-no-plugin-alternates.mjs +113 -0
  286. package/scripts/validate-server.js +46 -0
  287. package/scripts/verify-automation-bridge.js +200 -0
  288. package/server.json +58 -21
  289. package/src/automation/bridge.ts +558 -0
  290. package/src/automation/connection-manager.ts +130 -0
  291. package/src/automation/handshake.ts +99 -0
  292. package/src/automation/index.ts +2 -0
  293. package/src/automation/message-handler.ts +167 -0
  294. package/src/automation/request-tracker.ts +123 -0
  295. package/src/automation/types.ts +107 -0
  296. package/src/cli.ts +33 -6
  297. package/src/config.ts +73 -0
  298. package/src/constants.ts +19 -0
  299. package/src/graphql/loaders.ts +244 -0
  300. package/src/graphql/resolvers.ts +1008 -0
  301. package/src/graphql/schema.ts +452 -0
  302. package/src/graphql/server.ts +156 -0
  303. package/src/graphql/types.ts +10 -0
  304. package/src/handlers/resource-handlers.ts +186 -0
  305. package/src/index.ts +166 -664
  306. package/src/resources/actors.ts +58 -76
  307. package/src/resources/assets.ts +148 -134
  308. package/src/resources/levels.ts +28 -33
  309. package/src/server/resource-registry.ts +47 -0
  310. package/src/server/tool-registry.ts +354 -0
  311. package/src/server-setup.ts +114 -0
  312. package/src/services/health-monitor.ts +132 -0
  313. package/src/services/metrics-server.ts +142 -0
  314. package/src/tools/actors.ts +426 -323
  315. package/src/tools/animation.ts +672 -461
  316. package/src/tools/assets.ts +364 -289
  317. package/src/tools/audio.ts +323 -766
  318. package/src/tools/base-tool.ts +52 -0
  319. package/src/tools/behavior-tree.ts +45 -0
  320. package/src/tools/blueprint.ts +792 -970
  321. package/src/tools/consolidated-tool-definitions.ts +993 -515
  322. package/src/tools/consolidated-tool-handlers.ts +258 -1146
  323. package/src/tools/debug.ts +292 -187
  324. package/src/tools/dynamic-handler-registry.ts +33 -0
  325. package/src/tools/editor.ts +329 -253
  326. package/src/tools/engine.ts +14 -3
  327. package/src/tools/environment.ts +281 -0
  328. package/src/tools/foliage.ts +330 -392
  329. package/src/tools/handlers/actor-handlers.ts +265 -0
  330. package/src/tools/handlers/animation-handlers.ts +237 -0
  331. package/src/tools/handlers/argument-helper.ts +142 -0
  332. package/src/tools/handlers/asset-handlers.ts +532 -0
  333. package/src/tools/handlers/audio-handlers.ts +194 -0
  334. package/src/tools/handlers/blueprint-handlers.ts +380 -0
  335. package/src/tools/handlers/common-handlers.ts +87 -0
  336. package/src/tools/handlers/editor-handlers.ts +123 -0
  337. package/src/tools/handlers/effect-handlers.ts +220 -0
  338. package/src/tools/handlers/environment-handlers.ts +183 -0
  339. package/src/tools/handlers/graph-handlers.ts +116 -0
  340. package/src/tools/handlers/input-handlers.ts +28 -0
  341. package/src/tools/handlers/inspect-handlers.ts +450 -0
  342. package/src/tools/handlers/level-handlers.ts +252 -0
  343. package/src/tools/handlers/lighting-handlers.ts +147 -0
  344. package/src/tools/handlers/performance-handlers.ts +132 -0
  345. package/src/tools/handlers/pipeline-handlers.ts +127 -0
  346. package/src/tools/handlers/sequence-handlers.ts +415 -0
  347. package/src/tools/handlers/system-handlers.ts +564 -0
  348. package/src/tools/input.ts +101 -0
  349. package/src/tools/introspection.ts +493 -584
  350. package/src/tools/landscape.ts +418 -507
  351. package/src/tools/level.ts +786 -708
  352. package/src/tools/lighting.ts +588 -984
  353. package/src/tools/logs.ts +9 -57
  354. package/src/tools/materials.ts +237 -121
  355. package/src/tools/niagara.ts +335 -168
  356. package/src/tools/performance.ts +320 -169
  357. package/src/tools/physics.ts +274 -613
  358. package/src/tools/property-dictionary.ts +98 -0
  359. package/src/tools/sequence.ts +276 -820
  360. package/src/tools/tool-definition-utils.ts +35 -0
  361. package/src/tools/ui.ts +205 -283
  362. package/src/types/automation-responses.ts +119 -0
  363. package/src/types/env.ts +0 -10
  364. package/src/types/responses.ts +355 -0
  365. package/src/types/tool-interfaces.ts +250 -0
  366. package/src/types/tool-types.ts +243 -21
  367. package/src/unreal-bridge.ts +460 -1550
  368. package/src/utils/command-validator.ts +76 -0
  369. package/src/utils/elicitation.ts +10 -7
  370. package/src/utils/error-handler.ts +14 -90
  371. package/src/utils/ini-reader.ts +86 -0
  372. package/src/utils/logger.ts +8 -3
  373. package/src/utils/normalize.test.ts +162 -0
  374. package/src/utils/normalize.ts +60 -0
  375. package/src/utils/path-security.ts +43 -0
  376. package/src/utils/response-factory.ts +44 -0
  377. package/src/utils/response-validator.ts +176 -56
  378. package/src/utils/result-helpers.ts +21 -19
  379. package/src/utils/safe-json.test.ts +90 -0
  380. package/src/utils/safe-json.ts +14 -11
  381. package/src/utils/unreal-command-queue.ts +152 -0
  382. package/src/utils/validation.test.ts +184 -0
  383. package/src/utils/validation.ts +4 -1
  384. package/src/wasm/index.ts +838 -0
  385. package/test-server.mjs +100 -0
  386. package/tests/run-unreal-tool-tests.mjs +242 -14
  387. package/tests/test-animation.mjs +369 -0
  388. package/tests/test-asset-advanced.mjs +82 -0
  389. package/tests/test-asset-errors.mjs +35 -0
  390. package/tests/test-asset-graph.mjs +311 -0
  391. package/tests/test-audio.mjs +417 -0
  392. package/tests/test-automation-timeouts.mjs +98 -0
  393. package/tests/test-behavior-tree.mjs +444 -0
  394. package/tests/test-blueprint-graph.mjs +410 -0
  395. package/tests/test-blueprint.mjs +577 -0
  396. package/tests/test-client-mode.mjs +86 -0
  397. package/tests/test-console-command.mjs +56 -0
  398. package/tests/test-control-actor.mjs +425 -0
  399. package/tests/test-control-editor.mjs +112 -0
  400. package/tests/test-graphql.mjs +372 -0
  401. package/tests/test-input.mjs +349 -0
  402. package/tests/test-inspect.mjs +302 -0
  403. package/tests/test-landscape.mjs +316 -0
  404. package/tests/test-lighting.mjs +428 -0
  405. package/tests/test-manage-asset.mjs +438 -0
  406. package/tests/test-manage-level.mjs +89 -0
  407. package/tests/test-materials.mjs +356 -0
  408. package/tests/test-niagara.mjs +185 -0
  409. package/tests/test-no-inline-python.mjs +122 -0
  410. package/tests/test-performance.mjs +539 -0
  411. package/tests/test-plugin-handshake.mjs +82 -0
  412. package/tests/test-runner.mjs +933 -0
  413. package/tests/test-sequence.mjs +104 -0
  414. package/tests/test-system.mjs +96 -0
  415. package/tests/test-wasm.mjs +283 -0
  416. package/tests/test-world-partition.mjs +215 -0
  417. package/tsconfig.json +3 -3
  418. package/vitest.config.ts +35 -0
  419. package/wasm/Cargo.lock +363 -0
  420. package/wasm/Cargo.toml +42 -0
  421. package/wasm/LICENSE +21 -0
  422. package/wasm/README.md +253 -0
  423. package/wasm/src/dependency_resolver.rs +377 -0
  424. package/wasm/src/lib.rs +153 -0
  425. package/wasm/src/property_parser.rs +271 -0
  426. package/wasm/src/transform_math.rs +396 -0
  427. package/wasm/tests/integration.rs +109 -0
  428. package/.github/workflows/smithery-build.yml +0 -29
  429. package/dist/prompts/index.d.ts +0 -21
  430. package/dist/prompts/index.js +0 -217
  431. package/dist/tools/build_environment_advanced.d.ts +0 -65
  432. package/dist/tools/build_environment_advanced.js +0 -633
  433. package/dist/tools/rc.d.ts +0 -110
  434. package/dist/tools/rc.js +0 -437
  435. package/dist/tools/visual.d.ts +0 -40
  436. package/dist/tools/visual.js +0 -282
  437. package/dist/utils/http.d.ts +0 -6
  438. package/dist/utils/http.js +0 -151
  439. package/dist/utils/python-output.d.ts +0 -18
  440. package/dist/utils/python-output.js +0 -290
  441. package/dist/utils/python.d.ts +0 -2
  442. package/dist/utils/python.js +0 -4
  443. package/dist/utils/stdio-redirect.d.ts +0 -2
  444. package/dist/utils/stdio-redirect.js +0 -20
  445. package/docs/unreal-tool-test-cases.md +0 -574
  446. package/smithery.yaml +0 -29
  447. package/src/prompts/index.ts +0 -249
  448. package/src/tools/build_environment_advanced.ts +0 -732
  449. package/src/tools/rc.ts +0 -515
  450. package/src/tools/visual.ts +0 -281
  451. package/src/utils/http.ts +0 -187
  452. package/src/utils/python-output.ts +0 -351
  453. package/src/utils/python.ts +0 -3
  454. package/src/utils/stdio-redirect.ts +0 -18
@@ -0,0 +1,1184 @@
1
+ #include "McpAutomationBridgeGlobals.h"
2
+ #include "McpAutomationBridgeHelpers.h"
3
+ #include "McpAutomationBridgeSubsystem.h"
4
+ #include "Misc/ScopeExit.h"
5
+
6
+ #if WITH_EDITOR
7
+ #include "EdGraph/EdGraph.h"
8
+ #include "EdGraph/EdGraphNode.h"
9
+ #include "EdGraph/EdGraphPin.h"
10
+ #include "EdGraph/EdGraphSchema.h"
11
+ #include "EdGraphNode_Comment.h"
12
+ #include "Engine/Blueprint.h"
13
+ #include "K2Node_BreakStruct.h"
14
+ #include "K2Node_CallFunction.h"
15
+ #include "K2Node_CommutativeAssociativeBinaryOperator.h"
16
+ #include "K2Node_CustomEvent.h"
17
+ #include "K2Node_DynamicCast.h"
18
+ #include "K2Node_Event.h"
19
+ #include "K2Node_ExecutionSequence.h"
20
+ #include "K2Node_FunctionEntry.h"
21
+ #include "K2Node_FunctionResult.h"
22
+ #include "K2Node_IfThenElse.h"
23
+ #include "K2Node_InputAxisEvent.h"
24
+ #include "K2Node_Knot.h"
25
+ #include "K2Node_Literal.h"
26
+ #include "K2Node_MakeArray.h"
27
+ #include "K2Node_MakeStruct.h"
28
+ #include "K2Node_PromotableOperator.h"
29
+ #include "K2Node_Select.h"
30
+ #include "K2Node_Self.h"
31
+ #include "K2Node_Timeline.h"
32
+ #include "K2Node_VariableGet.h"
33
+ #include "K2Node_VariableSet.h"
34
+ #include "Kismet/GameplayStatics.h"
35
+ #include "Kismet/KismetMathLibrary.h"
36
+ #include "Kismet/KismetSystemLibrary.h"
37
+ #include "Kismet2/BlueprintEditorUtils.h"
38
+ #include "Kismet2/KismetEditorUtilities.h"
39
+
40
+ #endif
41
+
42
+ /**
43
+ * Process a "manage_blueprint_graph" automation request to inspect or modify a Blueprint graph.
44
+ *
45
+ * The Payload JSON controls the specific operation via the "subAction" field (examples: create_node,
46
+ * connect_pins, get_nodes, break_pin_links, delete_node, create_reroute_node, set_node_property,
47
+ * get_node_details, get_graph_details, get_pin_details). In editor builds this function performs
48
+ * graph/blueprint lookups and edits; in non-editor builds it reports an editor-only error.
49
+ *
50
+ * @param RequestId Unique identifier for the automation request (used in responses).
51
+ * @param Action The requested action name; this handler only processes "manage_blueprint_graph".
52
+ * @param Payload JSON object containing action options such as "assetPath"/"blueprintPath", "graphName",
53
+ * "subAction" and subaction-specific fields (nodeType, nodeId, pin names, positions, etc.).
54
+ * @param RequestingSocket WebSocket used to send responses and errors back to the requester.
55
+ * @return `true` if the request was handled by this function (Action == "manage_blueprint_graph"), `false` otherwise.
56
+ */
57
+ bool UMcpAutomationBridgeSubsystem::HandleBlueprintGraphAction(
58
+ const FString &RequestId, const FString &Action,
59
+ const TSharedPtr<FJsonObject> &Payload,
60
+ TSharedPtr<FMcpBridgeWebSocket> RequestingSocket) {
61
+ if (Action != TEXT("manage_blueprint_graph")) {
62
+ return false;
63
+ }
64
+
65
+ #if WITH_EDITOR
66
+ if (!Payload.IsValid()) {
67
+ SendAutomationError(RequestingSocket, RequestId,
68
+ TEXT("Missing payload for blueprint graph action."),
69
+ TEXT("INVALID_PAYLOAD"));
70
+ return true;
71
+ }
72
+
73
+ FString AssetPath;
74
+ if (!Payload->TryGetStringField(TEXT("assetPath"), AssetPath) ||
75
+ AssetPath.IsEmpty()) {
76
+ // Allow callers to use "blueprintPath" (as exposed by the consolidated
77
+ // tool schema) as an alias for assetPath so tests and tools do not need
78
+ // to duplicate the same value under two keys.
79
+ FString BlueprintPath;
80
+ Payload->TryGetStringField(TEXT("blueprintPath"), BlueprintPath);
81
+ if (!BlueprintPath.IsEmpty()) {
82
+ AssetPath = BlueprintPath;
83
+ }
84
+ }
85
+
86
+ if (AssetPath.IsEmpty()) {
87
+ SendAutomationError(
88
+ RequestingSocket, RequestId,
89
+ TEXT("Missing 'assetPath' or 'blueprintPath' in payload."),
90
+ TEXT("INVALID_ARGUMENT"));
91
+ return true;
92
+ }
93
+
94
+ UBlueprint *Blueprint = LoadObject<UBlueprint>(nullptr, *AssetPath);
95
+ if (!Blueprint) {
96
+ SendAutomationError(
97
+ RequestingSocket, RequestId,
98
+ FString::Printf(TEXT("Could not load blueprint at path: %s"),
99
+ *AssetPath),
100
+ TEXT("ASSET_NOT_FOUND"));
101
+ return true;
102
+ }
103
+
104
+ FString GraphName;
105
+ Payload->TryGetStringField(TEXT("graphName"), GraphName);
106
+ UEdGraph *TargetGraph = nullptr;
107
+
108
+ // Find the target graph
109
+ if (GraphName.IsEmpty() ||
110
+ GraphName.Equals(TEXT("EventGraph"), ESearchCase::IgnoreCase)) {
111
+ // Default to the main ubergraph/event graph
112
+ if (Blueprint->UbergraphPages.Num() > 0) {
113
+ TargetGraph = Blueprint->UbergraphPages[0];
114
+ }
115
+ } else {
116
+ // Search in FunctionGraphs and UbergraphPages
117
+ for (UEdGraph *Graph : Blueprint->FunctionGraphs) {
118
+ if (Graph->GetName() == GraphName) {
119
+ TargetGraph = Graph;
120
+ break;
121
+ }
122
+ }
123
+ if (!TargetGraph) {
124
+ for (UEdGraph *Graph : Blueprint->UbergraphPages) {
125
+ if (Graph->GetName() == GraphName) {
126
+ TargetGraph = Graph;
127
+ break;
128
+ }
129
+ }
130
+ }
131
+ }
132
+
133
+ if (!TargetGraph) {
134
+ // Fallback: try finding by name in all graphs
135
+ TArray<UEdGraph *> AllGraphs;
136
+ Blueprint->GetAllGraphs(AllGraphs);
137
+ for (UEdGraph *Graph : AllGraphs) {
138
+ if (Graph->GetName() == GraphName) {
139
+ TargetGraph = Graph;
140
+ break;
141
+ }
142
+ }
143
+ }
144
+
145
+ if (!TargetGraph) {
146
+ SendAutomationError(
147
+ RequestingSocket, RequestId,
148
+ FString::Printf(TEXT("Could not find graph '%s' in blueprint."),
149
+ *GraphName),
150
+ TEXT("GRAPH_NOT_FOUND"));
151
+ return true;
152
+ }
153
+
154
+ const FString SubAction = Payload->GetStringField(TEXT("subAction"));
155
+
156
+ // Node identifier interoperability:
157
+ // - Prefer NodeGuid strings for stable references.
158
+ // - Accept node UObject names (e.g. "K2Node_Event_0") for clients that
159
+ // mistakenly pass nodeName where nodeId is expected.
160
+ auto FindNodeByIdOrName = [&](const FString &Id) -> UEdGraphNode * {
161
+ if (Id.IsEmpty()) {
162
+ return nullptr;
163
+ }
164
+
165
+ for (UEdGraphNode *Node : TargetGraph->Nodes) {
166
+ if (!Node) {
167
+ continue;
168
+ }
169
+
170
+ if (Node->NodeGuid.ToString().Equals(Id, ESearchCase::IgnoreCase) ||
171
+ Node->GetName().Equals(Id, ESearchCase::IgnoreCase)) {
172
+ return Node;
173
+ }
174
+ }
175
+
176
+ return nullptr;
177
+ };
178
+
179
+ if (SubAction == TEXT("create_node")) {
180
+ const FScopedTransaction Transaction(
181
+ FText::FromString(TEXT("Create Blueprint Node")));
182
+ Blueprint->Modify();
183
+ TargetGraph->Modify();
184
+
185
+ FString NodeType;
186
+ Payload->TryGetStringField(TEXT("nodeType"), NodeType);
187
+ float X = 0.0f;
188
+ float Y = 0.0f;
189
+ Payload->TryGetNumberField(TEXT("x"), X);
190
+ Payload->TryGetNumberField(TEXT("y"), Y);
191
+
192
+ // Helper to finalize and report
193
+ auto FinalizeAndReport = [&](auto &NodeCreator, UEdGraphNode *NewNode) {
194
+ if (NewNode) {
195
+ // Set position BEFORE finalization per FGraphNodeCreator pattern
196
+ NewNode->NodePosX = X;
197
+ NewNode->NodePosY = Y;
198
+
199
+ // Finalize() calls CreateNewGuid(), PostPlacedNewNode(), and
200
+ // AllocateDefaultPins() if pins are empty. Do NOT call
201
+ // AllocateDefaultPins() again after this!
202
+ NodeCreator.Finalize();
203
+
204
+ FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
205
+
206
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
207
+ Result->SetStringField(TEXT("nodeId"), NewNode->NodeGuid.ToString());
208
+ Result->SetStringField(TEXT("nodeName"), NewNode->GetName());
209
+ SendAutomationResponse(RequestingSocket, RequestId, true,
210
+ TEXT("Node created."), Result);
211
+ } else {
212
+ SendAutomationError(
213
+ RequestingSocket, RequestId,
214
+ TEXT("Failed to create node (unsupported type or internal error)."),
215
+ TEXT("CREATE_FAILED"));
216
+ }
217
+ };
218
+
219
+ // Map common Blueprint node names to their CallFunction equivalents
220
+ // This allows users to use nodeType="PrintString" instead of CallFunction
221
+ static TMap<FString, TTuple<FString, FString>> CommonFunctionNodes = {
222
+ {TEXT("PrintString"),
223
+ MakeTuple(TEXT("UKismetSystemLibrary"), TEXT("PrintString"))},
224
+ {TEXT("Print"),
225
+ MakeTuple(TEXT("UKismetSystemLibrary"), TEXT("PrintString"))},
226
+ {TEXT("PrintText"),
227
+ MakeTuple(TEXT("UKismetSystemLibrary"), TEXT("PrintText"))},
228
+ {TEXT("SetActorLocation"),
229
+ MakeTuple(TEXT("AActor"), TEXT("K2_SetActorLocation"))},
230
+ {TEXT("GetActorLocation"),
231
+ MakeTuple(TEXT("AActor"), TEXT("K2_GetActorLocation"))},
232
+ {TEXT("SetActorRotation"),
233
+ MakeTuple(TEXT("AActor"), TEXT("K2_SetActorRotation"))},
234
+ {TEXT("GetActorRotation"),
235
+ MakeTuple(TEXT("AActor"), TEXT("K2_GetActorRotation"))},
236
+ {TEXT("SetActorTransform"),
237
+ MakeTuple(TEXT("AActor"), TEXT("K2_SetActorTransform"))},
238
+ {TEXT("GetActorTransform"),
239
+ MakeTuple(TEXT("AActor"), TEXT("K2_GetActorTransform"))},
240
+ {TEXT("AddActorLocalOffset"),
241
+ MakeTuple(TEXT("AActor"), TEXT("K2_AddActorLocalOffset"))},
242
+ {TEXT("Delay"), MakeTuple(TEXT("UKismetSystemLibrary"), TEXT("Delay"))},
243
+ {TEXT("DestroyActor"),
244
+ MakeTuple(TEXT("AActor"), TEXT("K2_DestroyActor"))},
245
+ {TEXT("SpawnActor"),
246
+ MakeTuple(TEXT("UGameplayStatics"),
247
+ TEXT("BeginDeferredActorSpawnFromClass"))},
248
+ {TEXT("GetPlayerPawn"),
249
+ MakeTuple(TEXT("UGameplayStatics"), TEXT("GetPlayerPawn"))},
250
+ {TEXT("GetPlayerController"),
251
+ MakeTuple(TEXT("UGameplayStatics"), TEXT("GetPlayerController"))},
252
+ {TEXT("PlaySound"),
253
+ MakeTuple(TEXT("UGameplayStatics"), TEXT("PlaySound2D"))},
254
+ {TEXT("PlaySound2D"),
255
+ MakeTuple(TEXT("UGameplayStatics"), TEXT("PlaySound2D"))},
256
+ {TEXT("PlaySoundAtLocation"),
257
+ MakeTuple(TEXT("UGameplayStatics"), TEXT("PlaySoundAtLocation"))},
258
+ {TEXT("GetWorldDeltaSeconds"),
259
+ MakeTuple(TEXT("UGameplayStatics"), TEXT("GetWorldDeltaSeconds"))},
260
+ {TEXT("SetTimerByFunctionName"),
261
+ MakeTuple(TEXT("UKismetSystemLibrary"), TEXT("K2_SetTimer"))},
262
+ {TEXT("ClearTimer"),
263
+ MakeTuple(TEXT("UKismetSystemLibrary"), TEXT("K2_ClearTimer"))},
264
+ {TEXT("IsValid"),
265
+ MakeTuple(TEXT("UKismetSystemLibrary"), TEXT("IsValid"))},
266
+ {TEXT("IsValidClass"),
267
+ MakeTuple(TEXT("UKismetSystemLibrary"), TEXT("IsValidClass"))},
268
+ // Math Nodes
269
+ {TEXT("Add_IntInt"),
270
+ MakeTuple(TEXT("UKismetMathLibrary"), TEXT("Add_IntInt"))},
271
+ {TEXT("Subtract_IntInt"),
272
+ MakeTuple(TEXT("UKismetMathLibrary"), TEXT("Subtract_IntInt"))},
273
+ {TEXT("Multiply_IntInt"),
274
+ MakeTuple(TEXT("UKismetMathLibrary"), TEXT("Multiply_IntInt"))},
275
+ {TEXT("Divide_IntInt"),
276
+ MakeTuple(TEXT("UKismetMathLibrary"), TEXT("Divide_IntInt"))},
277
+ {TEXT("Add_DoubleDouble"),
278
+ MakeTuple(TEXT("UKismetMathLibrary"), TEXT("Add_DoubleDouble"))},
279
+ {TEXT("Subtract_DoubleDouble"),
280
+ MakeTuple(TEXT("UKismetMathLibrary"), TEXT("Subtract_DoubleDouble"))},
281
+ {TEXT("Multiply_DoubleDouble"),
282
+ MakeTuple(TEXT("UKismetMathLibrary"), TEXT("Multiply_DoubleDouble"))},
283
+ {TEXT("Divide_DoubleDouble"),
284
+ MakeTuple(TEXT("UKismetMathLibrary"), TEXT("Divide_DoubleDouble"))},
285
+ {TEXT("FTrunc"), MakeTuple(TEXT("UKismetMathLibrary"), TEXT("FTrunc"))},
286
+ // Vector Ops
287
+ {TEXT("MakeVector"),
288
+ MakeTuple(TEXT("UKismetMathLibrary"), TEXT("MakeVector"))},
289
+ {TEXT("BreakVector"),
290
+ MakeTuple(TEXT("UKismetMathLibrary"), TEXT("BreakVector"))},
291
+ // Actor/Component Ops
292
+ {TEXT("GetComponentByClass"),
293
+ MakeTuple(TEXT("AActor"), TEXT("GetComponentByClass"))},
294
+ // Timer
295
+ {TEXT("GetWorldTimerManager"),
296
+ MakeTuple(TEXT("UKismetSystemLibrary"), TEXT("K2_GetTimerManager"))}};
297
+
298
+ // Check if this is a common function node shortcut
299
+ if (const auto *FuncInfo = CommonFunctionNodes.Find(NodeType)) {
300
+ FString ClassName = FuncInfo->Get<0>();
301
+ FString FuncName = FuncInfo->Get<1>();
302
+
303
+ // Find the class and function BEFORE creating NodeCreator
304
+ // (FGraphNodeCreator asserts in destructor if not finalized)
305
+ UClass *Class = nullptr;
306
+ if (ClassName == TEXT("UKismetSystemLibrary")) {
307
+ Class = UKismetSystemLibrary::StaticClass();
308
+ } else if (ClassName == TEXT("UGameplayStatics")) {
309
+ Class = UGameplayStatics::StaticClass();
310
+ } else if (ClassName == TEXT("AActor")) {
311
+ Class = AActor::StaticClass();
312
+ } else if (ClassName == TEXT("UKismetMathLibrary")) {
313
+ Class = UKismetMathLibrary::StaticClass();
314
+ } else {
315
+ Class = ResolveUClass(ClassName);
316
+ }
317
+
318
+ UFunction *Func = nullptr;
319
+ if (Class) {
320
+ Func = Class->FindFunctionByName(*FuncName);
321
+ }
322
+
323
+ // Return early with error if function not found (before NodeCreator)
324
+ if (!Func) {
325
+ SendAutomationError(
326
+ RequestingSocket, RequestId,
327
+ FString::Printf(
328
+ TEXT("Could not find function '%s::%s' for node type '%s'"),
329
+ *ClassName, *FuncName, *NodeType),
330
+ TEXT("FUNCTION_NOT_FOUND"));
331
+ return true;
332
+ }
333
+
334
+ // Now safe to create NodeCreator since we know we'll finalize it
335
+ FGraphNodeCreator<UK2Node_CallFunction> NodeCreator(*TargetGraph);
336
+ UK2Node_CallFunction *CallFuncNode = NodeCreator.CreateNode(false);
337
+ CallFuncNode->SetFromFunction(Func);
338
+ FinalizeAndReport(NodeCreator, CallFuncNode);
339
+ return true;
340
+ }
341
+
342
+ // Basic node creation logic - this can be expanded significantly
343
+ if (NodeType == TEXT("InputAxisEvent")) {
344
+ FString InputAxisName;
345
+ Payload->TryGetStringField(TEXT("inputAxisName"), InputAxisName);
346
+
347
+ if (InputAxisName.IsEmpty()) {
348
+ SendAutomationError(RequestingSocket, RequestId,
349
+ TEXT("inputAxisName required"),
350
+ TEXT("INVALID_ARGUMENT"));
351
+ return true;
352
+ }
353
+
354
+ FGraphNodeCreator<UK2Node_InputAxisEvent> NodeCreator(*TargetGraph);
355
+ UK2Node_InputAxisEvent *InputNode = NodeCreator.CreateNode(false);
356
+ InputNode->InputAxisName = FName(*InputAxisName);
357
+
358
+ FinalizeAndReport(NodeCreator, InputNode);
359
+ } else if (NodeType == TEXT("CallFunction") ||
360
+ NodeType == TEXT("K2Node_CallFunction") ||
361
+ NodeType == TEXT("FunctionCall")) {
362
+ FString MemberName;
363
+ Payload->TryGetStringField(TEXT("memberName"), MemberName);
364
+ FString MemberClass;
365
+ Payload->TryGetStringField(TEXT("memberClass"),
366
+ MemberClass); // Optional, for static functions
367
+
368
+ UFunction *Func = nullptr;
369
+ if (!MemberClass.IsEmpty()) {
370
+ UClass *Class = ResolveUClass(MemberClass);
371
+ if (Class) {
372
+ Func = Class->FindFunctionByName(*MemberName);
373
+ }
374
+ } else {
375
+ // Try to find in blueprint context
376
+ Func = Blueprint->GeneratedClass->FindFunctionByName(*MemberName);
377
+ if (!Func) {
378
+ // Try global search if simple name, or check common libraries
379
+ Func = FindObject<UFunction>(nullptr, *MemberName);
380
+ if (!Func) {
381
+ // Fallback: Check common libraries
382
+ if (UClass *KSL = UKismetSystemLibrary::StaticClass())
383
+ Func = KSL->FindFunctionByName(*MemberName);
384
+ if (!Func)
385
+ if (UClass *GPS = UGameplayStatics::StaticClass())
386
+ Func = GPS->FindFunctionByName(*MemberName);
387
+ if (!Func)
388
+ if (UClass *KML = UKismetMathLibrary::StaticClass())
389
+ Func = KML->FindFunctionByName(*MemberName);
390
+ }
391
+ }
392
+ }
393
+
394
+ if (Func) {
395
+ FGraphNodeCreator<UK2Node_CallFunction> NodeCreator(*TargetGraph);
396
+ UK2Node_CallFunction *CallFuncNode = NodeCreator.CreateNode(false);
397
+ CallFuncNode->SetFromFunction(Func);
398
+ FinalizeAndReport(NodeCreator, CallFuncNode);
399
+ } else {
400
+ SendAutomationError(
401
+ RequestingSocket, RequestId,
402
+ FString::Printf(TEXT("Could not find function '%s'"), *MemberName),
403
+ TEXT("FUNCTION_NOT_FOUND"));
404
+ return true;
405
+ }
406
+ } else if (NodeType == TEXT("VariableGet")) {
407
+ FString VarName;
408
+ Payload->TryGetStringField(TEXT("variableName"), VarName);
409
+ FName VarFName(*VarName);
410
+
411
+ // Validation BEFORE creation
412
+ bool bFound = false;
413
+ for (const FBPVariableDescription &VarDesc : Blueprint->NewVariables) {
414
+ if (VarDesc.VarName == VarFName) {
415
+ bFound = true;
416
+ break;
417
+ }
418
+ }
419
+ if (!bFound && Blueprint->GeneratedClass &&
420
+ Blueprint->GeneratedClass->FindPropertyByName(VarFName)) {
421
+ bFound = true;
422
+ }
423
+
424
+ if (!bFound) {
425
+ SendAutomationError(
426
+ RequestingSocket, RequestId,
427
+ FString::Printf(TEXT("Could not find variable '%s'"), *VarName),
428
+ TEXT("VARIABLE_NOT_FOUND"));
429
+ return true;
430
+ }
431
+
432
+ FGraphNodeCreator<UK2Node_VariableGet> NodeCreator(*TargetGraph);
433
+ UK2Node_VariableGet *VarGet = NodeCreator.CreateNode(false);
434
+ VarGet->VariableReference.SetSelfMember(VarFName);
435
+ FinalizeAndReport(NodeCreator, VarGet);
436
+ } else if (NodeType == TEXT("VariableSet")) {
437
+ FString VarName;
438
+ Payload->TryGetStringField(TEXT("variableName"), VarName);
439
+ FName VarFName(*VarName);
440
+
441
+ // Validation BEFORE creation
442
+ bool bFound = false;
443
+ for (const FBPVariableDescription &VarDesc : Blueprint->NewVariables) {
444
+ if (VarDesc.VarName == VarFName) {
445
+ bFound = true;
446
+ break;
447
+ }
448
+ }
449
+ if (!bFound && Blueprint->GeneratedClass &&
450
+ Blueprint->GeneratedClass->FindPropertyByName(VarFName)) {
451
+ bFound = true;
452
+ }
453
+
454
+ if (!bFound) {
455
+ SendAutomationError(
456
+ RequestingSocket, RequestId,
457
+ FString::Printf(TEXT("Could not find variable '%s'"), *VarName),
458
+ TEXT("VARIABLE_NOT_FOUND"));
459
+ return true;
460
+ }
461
+
462
+ FGraphNodeCreator<UK2Node_VariableSet> NodeCreator(*TargetGraph);
463
+ UK2Node_VariableSet *VarSet = NodeCreator.CreateNode(false);
464
+ VarSet->VariableReference.SetSelfMember(VarFName);
465
+ FinalizeAndReport(NodeCreator, VarSet);
466
+ } else if (NodeType == TEXT("CustomEvent")) {
467
+ FString EventName;
468
+ Payload->TryGetStringField(TEXT("eventName"), EventName);
469
+
470
+ FGraphNodeCreator<UK2Node_CustomEvent> NodeCreator(*TargetGraph);
471
+ UK2Node_CustomEvent *EventNode = NodeCreator.CreateNode(false);
472
+
473
+ EventNode->CustomFunctionName = FName(*EventName);
474
+ FinalizeAndReport(NodeCreator, EventNode);
475
+ } else if (NodeType == TEXT("Event") || NodeType == TEXT("K2Node_Event")) {
476
+ FString EventName;
477
+ Payload->TryGetStringField(
478
+ TEXT("eventName"),
479
+ EventName); // e.g., "ReceiveBeginPlay", "ReceiveTick"
480
+ FString MemberClass;
481
+ Payload->TryGetStringField(TEXT("memberClass"),
482
+ MemberClass); // Optional class override
483
+
484
+ if (EventName.IsEmpty()) {
485
+ SendAutomationError(RequestingSocket, RequestId,
486
+ TEXT("eventName required for Event node"),
487
+ TEXT("INVALID_ARGUMENT"));
488
+ return true;
489
+ }
490
+
491
+ // Map common event name aliases to their actual function names
492
+ static TMap<FString, FString> EventNameAliases = {
493
+ {TEXT("BeginPlay"), TEXT("ReceiveBeginPlay")},
494
+ {TEXT("Tick"), TEXT("ReceiveTick")},
495
+ {TEXT("EndPlay"), TEXT("ReceiveEndPlay")},
496
+ {TEXT("ActorBeginOverlap"), TEXT("ReceiveActorBeginOverlap")},
497
+ {TEXT("ActorEndOverlap"), TEXT("ReceiveActorEndOverlap")},
498
+ {TEXT("Hit"), TEXT("ReceiveHit")},
499
+ {TEXT("BeginCursorOver"), TEXT("ReceiveBeginCursorOver")},
500
+ {TEXT("EndCursorOver"), TEXT("ReceiveEndCursorOver")},
501
+ {TEXT("Clicked"), TEXT("ReceiveClicked")},
502
+ {TEXT("Released"), TEXT("ReceiveReleased")},
503
+ {TEXT("Destroyed"), TEXT("ReceiveDestroyed")},
504
+ };
505
+
506
+ if (const FString *Alias = EventNameAliases.Find(EventName)) {
507
+ EventName = *Alias;
508
+ }
509
+
510
+ // Determine target class: use explicit MemberClass or search hierarchy
511
+ UClass *TargetClass = nullptr;
512
+ UFunction *EventFunc = nullptr;
513
+
514
+ if (!MemberClass.IsEmpty()) {
515
+ // Explicit class specified
516
+ TargetClass = ResolveUClass(MemberClass);
517
+ if (TargetClass) {
518
+ EventFunc = TargetClass->FindFunctionByName(*EventName);
519
+ }
520
+ } else {
521
+ // Search up the class hierarchy starting from the Blueprint's parent
522
+ // class. Events like ReceiveBeginPlay are defined in AActor, not in
523
+ // the generated Blueprint class.
524
+ UClass *SearchClass = Blueprint->ParentClass;
525
+ while (SearchClass && !EventFunc) {
526
+ EventFunc = SearchClass->FindFunctionByName(
527
+ *EventName, EIncludeSuperFlag::ExcludeSuper);
528
+ if (EventFunc) {
529
+ TargetClass = SearchClass;
530
+ break;
531
+ }
532
+ SearchClass = SearchClass->GetSuperClass();
533
+ }
534
+
535
+ // If not found in hierarchy, try the generated class
536
+ if (!EventFunc && Blueprint->GeneratedClass) {
537
+ EventFunc = Blueprint->GeneratedClass->FindFunctionByName(*EventName);
538
+ if (EventFunc) {
539
+ TargetClass = Blueprint->GeneratedClass;
540
+ }
541
+ }
542
+ }
543
+
544
+ if (EventFunc && TargetClass) {
545
+ FGraphNodeCreator<UK2Node_Event> NodeCreator(*TargetGraph);
546
+ UK2Node_Event *EventNode = NodeCreator.CreateNode(false);
547
+ EventNode->EventReference.SetFromField<UFunction>(EventFunc, false);
548
+ EventNode->bOverrideFunction = true;
549
+ FinalizeAndReport(NodeCreator, EventNode);
550
+ } else {
551
+ // Provide helpful error message
552
+ FString SearchedClasses;
553
+ UClass *C = Blueprint->ParentClass;
554
+ int ClassCount = 0;
555
+ while (C && ClassCount < 5) {
556
+ if (!SearchedClasses.IsEmpty())
557
+ SearchedClasses += TEXT(", ");
558
+ SearchedClasses += C->GetName();
559
+ C = C->GetSuperClass();
560
+ ClassCount++;
561
+ }
562
+ SendAutomationError(
563
+ RequestingSocket, RequestId,
564
+ FString::Printf(TEXT("Could not find event '%s'. Searched classes: "
565
+ "%s. Try using the full name like "
566
+ "'ReceiveBeginPlay' instead of 'BeginPlay'."),
567
+ *EventName, *SearchedClasses),
568
+ TEXT("EVENT_NOT_FOUND"));
569
+ }
570
+
571
+ } else if (NodeType == TEXT("Cast") ||
572
+ NodeType.StartsWith(TEXT("CastTo"))) {
573
+ FString TargetClassName;
574
+ Payload->TryGetStringField(TEXT("targetClass"), TargetClassName);
575
+
576
+ // If targetClass not specified, try to infer from nodeType
577
+ // "CastTo<ClassName>"
578
+ if (TargetClassName.IsEmpty() && NodeType.StartsWith(TEXT("CastTo"))) {
579
+ TargetClassName = NodeType.Mid(6); // Remove "CastTo" prefix
580
+ }
581
+
582
+ UClass *TargetClass = ResolveUClass(TargetClassName);
583
+ if (!TargetClass) {
584
+ SendAutomationError(
585
+ RequestingSocket, RequestId,
586
+ FString::Printf(
587
+ TEXT("Could not resolve target class '%s' for Cast node"),
588
+ *TargetClassName),
589
+ TEXT("CLASS_NOT_FOUND"));
590
+ return true;
591
+ }
592
+
593
+ FGraphNodeCreator<UK2Node_DynamicCast> NodeCreator(*TargetGraph);
594
+ UK2Node_DynamicCast *CastNode = NodeCreator.CreateNode(false);
595
+ CastNode->TargetType = TargetClass;
596
+ FinalizeAndReport(NodeCreator, CastNode);
597
+ } else if (NodeType == TEXT("Sequence")) {
598
+ FGraphNodeCreator<UK2Node_ExecutionSequence> NodeCreator(*TargetGraph);
599
+ UK2Node_ExecutionSequence *NewNode = NodeCreator.CreateNode(false);
600
+ FinalizeAndReport(NodeCreator, NewNode);
601
+ } else if (NodeType == TEXT("Branch") || NodeType == TEXT("IfThenElse") ||
602
+ NodeType == TEXT("K2Node_IfThenElse")) {
603
+ FGraphNodeCreator<UK2Node_IfThenElse> NodeCreator(*TargetGraph);
604
+ UK2Node_IfThenElse *BranchNode = NodeCreator.CreateNode(false);
605
+ FinalizeAndReport(NodeCreator, BranchNode);
606
+ } else if (NodeType == TEXT("Literal")) {
607
+ // Create a literal node that can hold an object reference. This is a
608
+ // fully functional K2 literal node that returns the referenced asset
609
+ // or object when executed in the graph.
610
+ FString LiteralType;
611
+ Payload->TryGetStringField(TEXT("literalType"), LiteralType);
612
+ LiteralType.TrimStartAndEndInline();
613
+ const FString LiteralTypeLower =
614
+ LiteralType.IsEmpty() ? TEXT("object") : LiteralType.ToLower();
615
+
616
+ if (LiteralTypeLower == TEXT("object") ||
617
+ LiteralTypeLower == TEXT("asset")) {
618
+ FString ObjectPath;
619
+ Payload->TryGetStringField(TEXT("objectPath"), ObjectPath);
620
+ if (ObjectPath.IsEmpty()) {
621
+ // As a convenience, allow callers to use assetPath as the
622
+ // literal source when objectPath is omitted.
623
+ Payload->TryGetStringField(TEXT("assetPath"), ObjectPath);
624
+ }
625
+
626
+ if (ObjectPath.IsEmpty()) {
627
+ SendAutomationError(RequestingSocket, RequestId,
628
+ TEXT("Literal object creation requires "
629
+ "'objectPath' or 'assetPath'."),
630
+ TEXT("INVALID_LITERAL"));
631
+ return true;
632
+ }
633
+
634
+ UObject *LoadedObject = LoadObject<UObject>(nullptr, *ObjectPath);
635
+ if (!LoadedObject) {
636
+ SendAutomationError(
637
+ RequestingSocket, RequestId,
638
+ FString::Printf(TEXT("Literal object not found at path '%s'"),
639
+ *ObjectPath),
640
+ TEXT("OBJECT_NOT_FOUND"));
641
+ return true;
642
+ }
643
+
644
+ // Create the node only after successful validation
645
+ FGraphNodeCreator<UK2Node_Literal> NodeCreator(*TargetGraph);
646
+ UK2Node_Literal *LiteralNode = NodeCreator.CreateNode(false);
647
+ if (!LiteralNode) {
648
+ SendAutomationError(RequestingSocket, RequestId,
649
+ TEXT("Failed to allocate Literal node."),
650
+ TEXT("CREATE_FAILED"));
651
+ return true;
652
+ }
653
+
654
+ // UK2Node_Literal stores the referenced UObject in a private
655
+ // member; use its public setter rather than touching the
656
+ // field directly so we respect engine encapsulation.
657
+ LiteralNode->SetObjectRef(LoadedObject);
658
+ FinalizeAndReport(NodeCreator, LiteralNode);
659
+ } else {
660
+ // Primitive literal support (float/int/bool/strings) can be
661
+ // added later by wiring value pins. For now, fail fast rather
662
+ // than pretending success.
663
+ SendAutomationError(
664
+ RequestingSocket, RequestId,
665
+ FString::Printf(TEXT("Unsupported literalType '%s' (only "
666
+ "'object'/'asset' supported)."),
667
+ *LiteralType),
668
+ TEXT("UNSUPPORTED_LITERAL_TYPE"));
669
+ return true;
670
+ }
671
+ } else if (NodeType == TEXT("Comment")) {
672
+ FGraphNodeCreator<UEdGraphNode_Comment> NodeCreator(*TargetGraph);
673
+ UEdGraphNode_Comment *CommentNode = NodeCreator.CreateNode(false);
674
+
675
+ FString CommentText;
676
+ if (Payload->TryGetStringField(TEXT("comment"), CommentText) &&
677
+ !CommentText.IsEmpty()) {
678
+ CommentNode->NodeComment = CommentText;
679
+ } else {
680
+ CommentNode->NodeComment = TEXT("Comment");
681
+ }
682
+
683
+ CommentNode->NodeWidth = 400;
684
+ CommentNode->NodeHeight = 100;
685
+
686
+ FinalizeAndReport(NodeCreator, CommentNode);
687
+ } else if (NodeType == TEXT("MakeArray")) {
688
+ FGraphNodeCreator<UK2Node_MakeArray> NodeCreator(*TargetGraph);
689
+ UK2Node_MakeArray *MakeArrayNode = NodeCreator.CreateNode(false);
690
+ FinalizeAndReport(NodeCreator, MakeArrayNode);
691
+ } else if (NodeType == TEXT("Return")) {
692
+ FGraphNodeCreator<UK2Node_FunctionResult> NodeCreator(*TargetGraph);
693
+ UK2Node_FunctionResult *ReturnNode = NodeCreator.CreateNode(false);
694
+ FinalizeAndReport(NodeCreator, ReturnNode);
695
+ } else if (NodeType == TEXT("Self")) {
696
+ FGraphNodeCreator<UK2Node_Self> NodeCreator(*TargetGraph);
697
+ UK2Node_Self *SelfNode = NodeCreator.CreateNode(false);
698
+ FinalizeAndReport(NodeCreator, SelfNode);
699
+ } else if (NodeType == TEXT("Select")) {
700
+ FGraphNodeCreator<UK2Node_Select> NodeCreator(*TargetGraph);
701
+ UK2Node_Select *SelectNode = NodeCreator.CreateNode(false);
702
+ FinalizeAndReport(NodeCreator, SelectNode);
703
+ } else if (NodeType == TEXT("Timeline")) {
704
+ FGraphNodeCreator<UK2Node_Timeline> NodeCreator(*TargetGraph);
705
+ UK2Node_Timeline *TimelineNode = NodeCreator.CreateNode(false);
706
+
707
+ FString TimelineName;
708
+ if (Payload->TryGetStringField(TEXT("timelineName"), TimelineName) &&
709
+ !TimelineName.IsEmpty()) {
710
+ TimelineNode->TimelineName = FName(*TimelineName);
711
+ }
712
+
713
+ FinalizeAndReport(NodeCreator, TimelineNode);
714
+ } else if (NodeType == TEXT("MakeStruct")) {
715
+ FString StructName;
716
+ Payload->TryGetStringField(TEXT("structName"), StructName);
717
+ if (StructName.IsEmpty()) {
718
+ SendAutomationError(RequestingSocket, RequestId,
719
+ TEXT("structName required for MakeStruct"),
720
+ TEXT("INVALID_ARGUMENT"));
721
+ return true;
722
+ }
723
+ UScriptStruct *Struct = FindObject<UScriptStruct>(nullptr, *StructName);
724
+ if (!Struct) {
725
+ SendAutomationError(RequestingSocket, RequestId,
726
+ TEXT("Struct not found"), TEXT("STRUCT_NOT_FOUND"));
727
+ return true;
728
+ }
729
+
730
+ FGraphNodeCreator<UK2Node_MakeStruct> NodeCreator(*TargetGraph);
731
+ UK2Node_MakeStruct *MakeStructNode = NodeCreator.CreateNode(false);
732
+ MakeStructNode->StructType = Struct;
733
+ FinalizeAndReport(NodeCreator, MakeStructNode);
734
+ } else if (NodeType == TEXT("BreakStruct")) {
735
+ FString StructName;
736
+ Payload->TryGetStringField(TEXT("structName"), StructName);
737
+ if (StructName.IsEmpty()) {
738
+ SendAutomationError(RequestingSocket, RequestId,
739
+ TEXT("structName required for BreakStruct"),
740
+ TEXT("INVALID_ARGUMENT"));
741
+ return true;
742
+ }
743
+ UScriptStruct *Struct = FindObject<UScriptStruct>(nullptr, *StructName);
744
+ if (!Struct) {
745
+ SendAutomationError(RequestingSocket, RequestId,
746
+ TEXT("Struct not found"), TEXT("STRUCT_NOT_FOUND"));
747
+ return true;
748
+ }
749
+
750
+ FGraphNodeCreator<UK2Node_BreakStruct> NodeCreator(*TargetGraph);
751
+ UK2Node_BreakStruct *BreakStructNode = NodeCreator.CreateNode(false);
752
+ BreakStructNode->StructType = Struct;
753
+ FinalizeAndReport(NodeCreator, BreakStructNode);
754
+ } else {
755
+ SendAutomationError(
756
+ RequestingSocket, RequestId,
757
+ TEXT("Failed to create node (unsupported type or internal error)."),
758
+ TEXT("CREATE_FAILED"));
759
+ }
760
+ return true;
761
+ } else if (SubAction == TEXT("connect_pins")) {
762
+ const FScopedTransaction Transaction(
763
+ FText::FromString(TEXT("Connect Blueprint Pins")));
764
+ Blueprint->Modify();
765
+ TargetGraph->Modify();
766
+
767
+ FString FromNodeId, FromPinName, ToNodeId, ToPinName;
768
+ Payload->TryGetStringField(TEXT("fromNodeId"), FromNodeId);
769
+ Payload->TryGetStringField(TEXT("fromPinName"), FromPinName);
770
+ Payload->TryGetStringField(TEXT("toNodeId"), ToNodeId);
771
+ Payload->TryGetStringField(TEXT("toPinName"), ToPinName);
772
+
773
+ UEdGraphNode *FromNode = FindNodeByIdOrName(FromNodeId);
774
+ UEdGraphNode *ToNode = FindNodeByIdOrName(ToNodeId);
775
+
776
+ if (!FromNode || !ToNode) {
777
+ SendAutomationError(RequestingSocket, RequestId,
778
+ TEXT("Could not find source or target node."),
779
+ TEXT("NODE_NOT_FOUND"));
780
+ return true;
781
+ }
782
+
783
+ // Handle PinName in format "NodeName.PinName"
784
+ FString FromPinClean = FromPinName;
785
+ if (FromPinName.Contains(TEXT("."))) {
786
+ FromPinName.Split(TEXT("."), nullptr, &FromPinClean);
787
+ }
788
+ FString ToPinClean = ToPinName;
789
+ if (ToPinName.Contains(TEXT("."))) {
790
+ ToPinName.Split(TEXT("."), nullptr, &ToPinClean);
791
+ }
792
+
793
+ UEdGraphPin *FromPin = FromNode->FindPin(*FromPinClean);
794
+ UEdGraphPin *ToPin = ToNode->FindPin(*ToPinClean);
795
+
796
+ if (!FromPin || !ToPin) {
797
+ SendAutomationError(RequestingSocket, RequestId,
798
+ TEXT("Could not find source or target pin."),
799
+ TEXT("PIN_NOT_FOUND"));
800
+ return true;
801
+ }
802
+
803
+ FromNode->Modify();
804
+ ToNode->Modify();
805
+
806
+ if (TargetGraph->GetSchema()->TryCreateConnection(FromPin, ToPin)) {
807
+ FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
808
+ SendAutomationResponse(RequestingSocket, RequestId, true,
809
+ TEXT("Pins connected."));
810
+ } else {
811
+ SendAutomationError(RequestingSocket, RequestId,
812
+ TEXT("Failed to connect pins (schema rejection)."),
813
+ TEXT("CONNECTION_FAILED"));
814
+ }
815
+ return true;
816
+ } else if (SubAction == TEXT("get_nodes")) {
817
+ TArray<TSharedPtr<FJsonValue>> NodesArray;
818
+
819
+ for (UEdGraphNode *Node : TargetGraph->Nodes) {
820
+ if (!Node)
821
+ continue;
822
+
823
+ TSharedPtr<FJsonObject> NodeObj = MakeShared<FJsonObject>();
824
+ NodeObj->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
825
+ NodeObj->SetStringField(TEXT("nodeName"), Node->GetName());
826
+ NodeObj->SetStringField(TEXT("nodeType"), Node->GetClass()->GetName());
827
+ NodeObj->SetStringField(
828
+ TEXT("nodeTitle"),
829
+ Node->GetNodeTitle(ENodeTitleType::ListView).ToString());
830
+ NodeObj->SetStringField(TEXT("comment"), Node->NodeComment);
831
+ NodeObj->SetNumberField(TEXT("x"), Node->NodePosX);
832
+ NodeObj->SetNumberField(TEXT("y"), Node->NodePosY);
833
+
834
+ TArray<TSharedPtr<FJsonValue>> PinsArray;
835
+ for (UEdGraphPin *Pin : Node->Pins) {
836
+ if (!Pin)
837
+ continue;
838
+
839
+ TSharedPtr<FJsonObject> PinObj = MakeShared<FJsonObject>();
840
+ PinObj->SetStringField(TEXT("pinName"), Pin->PinName.ToString());
841
+ PinObj->SetStringField(TEXT("pinType"),
842
+ Pin->PinType.PinCategory.ToString());
843
+ PinObj->SetStringField(TEXT("direction"), Pin->Direction == EGPD_Input
844
+ ? TEXT("Input")
845
+ : TEXT("Output"));
846
+
847
+ // Add pin sub-category object type if applicable
848
+ if (Pin->PinType.PinCategory == TEXT("object") ||
849
+ Pin->PinType.PinCategory == TEXT("class") ||
850
+ Pin->PinType.PinCategory == TEXT("struct")) {
851
+ if (Pin->PinType.PinSubCategoryObject.IsValid()) {
852
+ PinObj->SetStringField(
853
+ TEXT("pinSubType"),
854
+ Pin->PinType.PinSubCategoryObject->GetName());
855
+ }
856
+ }
857
+
858
+ TArray<TSharedPtr<FJsonValue>> LinkedToFileArray;
859
+ for (UEdGraphPin *LinkedPin : Pin->LinkedTo) {
860
+ if (LinkedPin && LinkedPin->GetOwningNode()) {
861
+ TSharedPtr<FJsonObject> LinkObj = MakeShared<FJsonObject>();
862
+ LinkObj->SetStringField(
863
+ TEXT("nodeId"),
864
+ LinkedPin->GetOwningNode()->NodeGuid.ToString());
865
+ LinkObj->SetStringField(TEXT("pinName"),
866
+ LinkedPin->PinName.ToString());
867
+ LinkedToFileArray.Add(MakeShared<FJsonValueObject>(LinkObj));
868
+ }
869
+ }
870
+ PinObj->SetArrayField(TEXT("linkedTo"), LinkedToFileArray);
871
+ PinsArray.Add(MakeShared<FJsonValueObject>(PinObj));
872
+ }
873
+ NodeObj->SetArrayField(TEXT("pins"), PinsArray);
874
+
875
+ NodesArray.Add(MakeShared<FJsonValueObject>(NodeObj));
876
+ }
877
+
878
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
879
+ Result->SetArrayField(TEXT("nodes"), NodesArray);
880
+ Result->SetStringField(TEXT("graphName"), TargetGraph->GetName());
881
+
882
+ SendAutomationResponse(RequestingSocket, RequestId, true,
883
+ TEXT("Nodes retrieved."), Result);
884
+ return true;
885
+ } else if (SubAction == TEXT("break_pin_links")) {
886
+ const FScopedTransaction Transaction(
887
+ FText::FromString(TEXT("Break Blueprint Pin Links")));
888
+ Blueprint->Modify();
889
+ TargetGraph->Modify();
890
+
891
+ FString NodeId, PinName;
892
+ Payload->TryGetStringField(TEXT("nodeId"), NodeId);
893
+ Payload->TryGetStringField(TEXT("pinName"), PinName);
894
+
895
+ UEdGraphNode *TargetNode = FindNodeByIdOrName(NodeId);
896
+
897
+ if (!TargetNode) {
898
+ SendAutomationError(RequestingSocket, RequestId, TEXT("Node not found."),
899
+ TEXT("NODE_NOT_FOUND"));
900
+ return true;
901
+ }
902
+
903
+ UEdGraphPin *Pin = TargetNode->FindPin(*PinName);
904
+ if (!Pin) {
905
+ SendAutomationError(RequestingSocket, RequestId, TEXT("Pin not found."),
906
+ TEXT("PIN_NOT_FOUND"));
907
+ return true;
908
+ }
909
+
910
+ TargetNode->Modify();
911
+ TargetGraph->GetSchema()->BreakPinLinks(*Pin, true);
912
+ FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
913
+ SendAutomationResponse(RequestingSocket, RequestId, true,
914
+ TEXT("Pin links broken."));
915
+ return true;
916
+ }
917
+
918
+ else if (SubAction == TEXT("delete_node")) {
919
+ const FScopedTransaction Transaction(
920
+ FText::FromString(TEXT("Delete Blueprint Node")));
921
+ Blueprint->Modify();
922
+ TargetGraph->Modify();
923
+
924
+ FString NodeId;
925
+ Payload->TryGetStringField(TEXT("nodeId"), NodeId);
926
+
927
+ UEdGraphNode *TargetNode = FindNodeByIdOrName(NodeId);
928
+
929
+ if (TargetNode) {
930
+ FBlueprintEditorUtils::RemoveNode(Blueprint, TargetNode, true);
931
+ SendAutomationResponse(RequestingSocket, RequestId, true,
932
+ TEXT("Node deleted."));
933
+ } else {
934
+ SendAutomationError(RequestingSocket, RequestId, TEXT("Node not found."),
935
+ TEXT("NODE_NOT_FOUND"));
936
+ }
937
+ return true;
938
+ } else if (SubAction == TEXT("create_reroute_node")) {
939
+ const FScopedTransaction Transaction(
940
+ FText::FromString(TEXT("Create Reroute Node")));
941
+ Blueprint->Modify();
942
+ TargetGraph->Modify();
943
+
944
+ float X = 0.0f;
945
+ float Y = 0.0f;
946
+ Payload->TryGetNumberField(TEXT("x"), X);
947
+ Payload->TryGetNumberField(TEXT("y"), Y);
948
+
949
+ FGraphNodeCreator<UK2Node_Knot> NodeCreator(*TargetGraph);
950
+ UK2Node_Knot *RerouteNode = NodeCreator.CreateNode(false);
951
+
952
+ RerouteNode->NodePosX = X;
953
+ RerouteNode->NodePosY = Y;
954
+
955
+ NodeCreator.Finalize();
956
+
957
+ FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
958
+
959
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
960
+ Result->SetStringField(TEXT("nodeId"), RerouteNode->NodeGuid.ToString());
961
+ SendAutomationResponse(RequestingSocket, RequestId, true,
962
+ TEXT("Reroute node created."), Result);
963
+ return true;
964
+ } else if (SubAction == TEXT("set_node_property")) {
965
+ const FScopedTransaction Transaction(
966
+ FText::FromString(TEXT("Set Blueprint Node Property")));
967
+ Blueprint->Modify();
968
+ TargetGraph->Modify();
969
+
970
+ // Generic property setter for common node properties used by tools.
971
+ FString NodeId;
972
+ Payload->TryGetStringField(TEXT("nodeId"), NodeId);
973
+ FString PropertyName;
974
+ Payload->TryGetStringField(TEXT("propertyName"), PropertyName);
975
+ FString Value;
976
+ Payload->TryGetStringField(TEXT("value"), Value);
977
+
978
+ UEdGraphNode *TargetNode = FindNodeByIdOrName(NodeId);
979
+
980
+ if (TargetNode) {
981
+ TargetNode->Modify();
982
+ bool bHandled = false;
983
+
984
+ if (PropertyName.Equals(TEXT("Comment"), ESearchCase::IgnoreCase) ||
985
+ PropertyName.Equals(TEXT("NodeComment"), ESearchCase::IgnoreCase)) {
986
+ TargetNode->NodeComment = Value;
987
+ bHandled = true;
988
+ } else if (PropertyName.Equals(TEXT("X"), ESearchCase::IgnoreCase) ||
989
+ PropertyName.Equals(TEXT("NodePosX"),
990
+ ESearchCase::IgnoreCase)) {
991
+ double NumValue = 0.0;
992
+ if (!Payload->TryGetNumberField(TEXT("value"), NumValue)) {
993
+ NumValue = FCString::Atod(*Value);
994
+ }
995
+ TargetNode->NodePosX = static_cast<float>(NumValue);
996
+ bHandled = true;
997
+ } else if (PropertyName.Equals(TEXT("Y"), ESearchCase::IgnoreCase) ||
998
+ PropertyName.Equals(TEXT("NodePosY"),
999
+ ESearchCase::IgnoreCase)) {
1000
+ double NumValue = 0.0;
1001
+ if (!Payload->TryGetNumberField(TEXT("value"), NumValue)) {
1002
+ NumValue = FCString::Atod(*Value);
1003
+ }
1004
+ TargetNode->NodePosY = static_cast<float>(NumValue);
1005
+ bHandled = true;
1006
+ } else if (PropertyName.Equals(TEXT("bCommentBubbleVisible"),
1007
+ ESearchCase::IgnoreCase)) {
1008
+ TargetNode->bCommentBubbleVisible = Value.ToBool();
1009
+ bHandled = true;
1010
+ } else if (PropertyName.Equals(TEXT("bCommentBubblePinned"),
1011
+ ESearchCase::IgnoreCase)) {
1012
+ TargetNode->bCommentBubblePinned = Value.ToBool();
1013
+ bHandled = true;
1014
+ }
1015
+
1016
+ if (bHandled) {
1017
+ TargetGraph->NotifyGraphChanged();
1018
+ FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
1019
+ SendAutomationResponse(RequestingSocket, RequestId, true,
1020
+ TEXT("Node property updated."));
1021
+ } else {
1022
+ SendAutomationError(
1023
+ RequestingSocket, RequestId,
1024
+ FString::Printf(TEXT("Unsupported node property '%s'"),
1025
+ *PropertyName),
1026
+ TEXT("PROPERTY_NOT_SUPPORTED"));
1027
+ }
1028
+ } else {
1029
+ SendAutomationError(RequestingSocket, RequestId, TEXT("Node not found."),
1030
+ TEXT("NODE_NOT_FOUND"));
1031
+ }
1032
+ return true;
1033
+ } else if (SubAction == TEXT("get_node_details")) {
1034
+ FString NodeId;
1035
+ Payload->TryGetStringField(TEXT("nodeId"), NodeId);
1036
+
1037
+ UEdGraphNode *TargetNode = FindNodeByIdOrName(NodeId);
1038
+
1039
+ if (TargetNode) {
1040
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
1041
+ Result->SetStringField(TEXT("nodeName"), TargetNode->GetName());
1042
+ Result->SetStringField(
1043
+ TEXT("nodeTitle"),
1044
+ TargetNode->GetNodeTitle(ENodeTitleType::ListView).ToString());
1045
+ Result->SetStringField(TEXT("nodeComment"), TargetNode->NodeComment);
1046
+ Result->SetNumberField(TEXT("x"), TargetNode->NodePosX);
1047
+ Result->SetNumberField(TEXT("y"), TargetNode->NodePosY);
1048
+
1049
+ TArray<TSharedPtr<FJsonValue>> Pins;
1050
+ for (UEdGraphPin *Pin : TargetNode->Pins) {
1051
+ TSharedPtr<FJsonObject> PinObj = MakeShared<FJsonObject>();
1052
+ PinObj->SetStringField(TEXT("pinName"), Pin->PinName.ToString());
1053
+ PinObj->SetStringField(TEXT("direction"), Pin->Direction == EGPD_Input
1054
+ ? TEXT("Input")
1055
+ : TEXT("Output"));
1056
+ PinObj->SetStringField(TEXT("pinType"),
1057
+ Pin->PinType.PinCategory.ToString());
1058
+ Pins.Add(MakeShared<FJsonValueObject>(PinObj));
1059
+ }
1060
+ Result->SetArrayField(TEXT("pins"), Pins);
1061
+
1062
+ SendAutomationResponse(RequestingSocket, RequestId, true,
1063
+ TEXT("Node details retrieved."), Result);
1064
+ } else {
1065
+ SendAutomationError(RequestingSocket, RequestId, TEXT("Node not found."),
1066
+ TEXT("NODE_NOT_FOUND"));
1067
+ }
1068
+ return true;
1069
+ } else if (SubAction == TEXT("get_graph_details")) {
1070
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
1071
+ Result->SetStringField(TEXT("graphName"), TargetGraph->GetName());
1072
+ Result->SetNumberField(TEXT("nodeCount"), TargetGraph->Nodes.Num());
1073
+
1074
+ TArray<TSharedPtr<FJsonValue>> Nodes;
1075
+ for (UEdGraphNode *Node : TargetGraph->Nodes) {
1076
+ TSharedPtr<FJsonObject> NodeObj = MakeShared<FJsonObject>();
1077
+ NodeObj->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
1078
+ NodeObj->SetStringField(TEXT("nodeName"), Node->GetName());
1079
+ NodeObj->SetStringField(
1080
+ TEXT("nodeTitle"),
1081
+ Node->GetNodeTitle(ENodeTitleType::ListView).ToString());
1082
+ Nodes.Add(MakeShared<FJsonValueObject>(NodeObj));
1083
+ }
1084
+ Result->SetArrayField(TEXT("nodes"), Nodes);
1085
+
1086
+ SendAutomationResponse(RequestingSocket, RequestId, true,
1087
+ TEXT("Graph details retrieved."), Result);
1088
+ return true;
1089
+ } else if (SubAction == TEXT("get_pin_details")) {
1090
+ FString NodeId;
1091
+ Payload->TryGetStringField(TEXT("nodeId"), NodeId);
1092
+ FString PinName;
1093
+ Payload->TryGetStringField(TEXT("pinName"), PinName);
1094
+
1095
+ UEdGraphNode *TargetNode = FindNodeByIdOrName(NodeId);
1096
+
1097
+ if (!TargetNode) {
1098
+ SendAutomationError(RequestingSocket, RequestId, TEXT("Node not found."),
1099
+ TEXT("NODE_NOT_FOUND"));
1100
+ return true;
1101
+ }
1102
+
1103
+ TArray<UEdGraphPin *> PinsToReport;
1104
+ if (!PinName.IsEmpty()) {
1105
+ UEdGraphPin *Pin = TargetNode->FindPin(*PinName);
1106
+ if (!Pin) {
1107
+ SendAutomationError(RequestingSocket, RequestId, TEXT("Pin not found."),
1108
+ TEXT("PIN_NOT_FOUND"));
1109
+ return true;
1110
+ }
1111
+ PinsToReport.Add(Pin);
1112
+ } else {
1113
+ PinsToReport = TargetNode->Pins;
1114
+ }
1115
+
1116
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
1117
+ Result->SetStringField(TEXT("nodeId"), NodeId);
1118
+
1119
+ TArray<TSharedPtr<FJsonValue>> PinsJson;
1120
+ for (UEdGraphPin *Pin : PinsToReport) {
1121
+ if (!Pin) {
1122
+ continue;
1123
+ }
1124
+
1125
+ TSharedPtr<FJsonObject> PinObj = MakeShared<FJsonObject>();
1126
+ PinObj->SetStringField(TEXT("pinName"), Pin->PinName.ToString());
1127
+ PinObj->SetStringField(TEXT("direction"), Pin->Direction == EGPD_Input
1128
+ ? TEXT("Input")
1129
+ : TEXT("Output"));
1130
+ PinObj->SetStringField(TEXT("pinType"),
1131
+ Pin->PinType.PinCategory.ToString());
1132
+
1133
+ if (Pin->LinkedTo.Num() > 0) {
1134
+ TArray<TSharedPtr<FJsonValue>> LinkedArray;
1135
+ for (UEdGraphPin *LinkedPin : Pin->LinkedTo) {
1136
+ if (!LinkedPin) {
1137
+ continue;
1138
+ }
1139
+ FString LinkedNodeId =
1140
+ LinkedPin->GetOwningNode()
1141
+ ? LinkedPin->GetOwningNode()->NodeGuid.ToString()
1142
+ : FString();
1143
+ const FString LinkedLabel =
1144
+ LinkedNodeId.IsEmpty()
1145
+ ? LinkedPin->PinName.ToString()
1146
+ : FString::Printf(TEXT("%s:%s"), *LinkedNodeId,
1147
+ *LinkedPin->PinName.ToString());
1148
+ LinkedArray.Add(MakeShared<FJsonValueString>(LinkedLabel));
1149
+ }
1150
+ PinObj->SetArrayField(TEXT("linkedTo"), LinkedArray);
1151
+ }
1152
+
1153
+ if (!Pin->DefaultValue.IsEmpty()) {
1154
+ PinObj->SetStringField(TEXT("defaultValue"), Pin->DefaultValue);
1155
+ } else if (!Pin->DefaultTextValue.IsEmptyOrWhitespace()) {
1156
+ PinObj->SetStringField(TEXT("defaultTextValue"),
1157
+ Pin->DefaultTextValue.ToString());
1158
+ } else if (Pin->DefaultObject) {
1159
+ PinObj->SetStringField(TEXT("defaultObjectPath"),
1160
+ Pin->DefaultObject->GetPathName());
1161
+ }
1162
+
1163
+ PinsJson.Add(MakeShared<FJsonValueObject>(PinObj));
1164
+ }
1165
+
1166
+ Result->SetArrayField(TEXT("pins"), PinsJson);
1167
+
1168
+ SendAutomationResponse(RequestingSocket, RequestId, true,
1169
+ TEXT("Pin details retrieved."), Result);
1170
+ return true;
1171
+ }
1172
+
1173
+ SendAutomationError(
1174
+ RequestingSocket, RequestId,
1175
+ FString::Printf(TEXT("Unknown subAction: %s"), *SubAction),
1176
+ TEXT("INVALID_SUBACTION"));
1177
+ return true;
1178
+ #else
1179
+ SendAutomationError(RequestingSocket, RequestId,
1180
+ TEXT("Blueprint graph actions are editor-only."),
1181
+ TEXT("EDITOR_ONLY"));
1182
+ return true;
1183
+ #endif
1184
+ }