unreal-engine-mcp-server 0.4.7 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (438) hide show
  1. package/.env.example +26 -0
  2. package/.env.production +38 -7
  3. package/.eslintrc.json +0 -54
  4. package/.eslintrc.override.json +8 -0
  5. package/.github/ISSUE_TEMPLATE/bug_report.yml +94 -0
  6. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  7. package/.github/ISSUE_TEMPLATE/feature_request.yml +56 -0
  8. package/.github/copilot-instructions.md +478 -45
  9. package/.github/dependabot.yml +19 -0
  10. package/.github/labeler.yml +24 -0
  11. package/.github/labels.yml +70 -0
  12. package/.github/pull_request_template.md +42 -0
  13. package/.github/release-drafter.yml +148 -0
  14. package/.github/workflows/auto-merge.yml +38 -0
  15. package/.github/workflows/ci.yml +38 -0
  16. package/.github/workflows/dependency-review.yml +17 -0
  17. package/.github/workflows/gemini-issue-triage.yml +172 -0
  18. package/.github/workflows/greetings.yml +23 -0
  19. package/.github/workflows/labeler.yml +16 -0
  20. package/.github/workflows/links.yml +80 -0
  21. package/.github/workflows/pr-size-labeler.yml +137 -0
  22. package/.github/workflows/publish-mcp.yml +12 -7
  23. package/.github/workflows/release-drafter.yml +23 -0
  24. package/.github/workflows/release.yml +112 -0
  25. package/.github/workflows/semantic-pull-request.yml +35 -0
  26. package/.github/workflows/smoke-test.yml +36 -0
  27. package/.github/workflows/stale.yml +28 -0
  28. package/CHANGELOG.md +267 -31
  29. package/CONTRIBUTING.md +140 -0
  30. package/README.md +166 -71
  31. package/claude_desktop_config_example.json +7 -6
  32. package/dist/automation/bridge.d.ts +50 -0
  33. package/dist/automation/bridge.js +452 -0
  34. package/dist/automation/connection-manager.d.ts +23 -0
  35. package/dist/automation/connection-manager.js +107 -0
  36. package/dist/automation/handshake.d.ts +11 -0
  37. package/dist/automation/handshake.js +89 -0
  38. package/dist/automation/index.d.ts +3 -0
  39. package/dist/automation/index.js +3 -0
  40. package/dist/automation/message-handler.d.ts +12 -0
  41. package/dist/automation/message-handler.js +149 -0
  42. package/dist/automation/request-tracker.d.ts +25 -0
  43. package/dist/automation/request-tracker.js +98 -0
  44. package/dist/automation/types.d.ts +130 -0
  45. package/dist/automation/types.js +2 -0
  46. package/dist/cli.js +32 -5
  47. package/dist/config.d.ts +27 -0
  48. package/dist/config.js +60 -0
  49. package/dist/constants.d.ts +12 -0
  50. package/dist/constants.js +12 -0
  51. package/dist/graphql/resolvers.d.ts +268 -0
  52. package/dist/graphql/resolvers.js +743 -0
  53. package/dist/graphql/schema.d.ts +5 -0
  54. package/dist/graphql/schema.js +437 -0
  55. package/dist/graphql/server.d.ts +26 -0
  56. package/dist/graphql/server.js +115 -0
  57. package/dist/graphql/types.d.ts +7 -0
  58. package/dist/graphql/types.js +2 -0
  59. package/dist/handlers/resource-handlers.d.ts +20 -0
  60. package/dist/handlers/resource-handlers.js +180 -0
  61. package/dist/index.d.ts +31 -18
  62. package/dist/index.js +119 -619
  63. package/dist/prompts/index.js +4 -4
  64. package/dist/resources/actors.d.ts +17 -12
  65. package/dist/resources/actors.js +56 -76
  66. package/dist/resources/assets.d.ts +6 -14
  67. package/dist/resources/assets.js +115 -147
  68. package/dist/resources/levels.d.ts +13 -13
  69. package/dist/resources/levels.js +25 -34
  70. package/dist/server/resource-registry.d.ts +20 -0
  71. package/dist/server/resource-registry.js +37 -0
  72. package/dist/server/tool-registry.d.ts +23 -0
  73. package/dist/server/tool-registry.js +322 -0
  74. package/dist/server-setup.d.ts +21 -0
  75. package/dist/server-setup.js +111 -0
  76. package/dist/services/health-monitor.d.ts +34 -0
  77. package/dist/services/health-monitor.js +105 -0
  78. package/dist/services/metrics-server.d.ts +11 -0
  79. package/dist/services/metrics-server.js +105 -0
  80. package/dist/tools/actors.d.ts +147 -9
  81. package/dist/tools/actors.js +350 -311
  82. package/dist/tools/animation.d.ts +135 -4
  83. package/dist/tools/animation.js +510 -411
  84. package/dist/tools/assets.d.ts +117 -19
  85. package/dist/tools/assets.js +259 -284
  86. package/dist/tools/audio.d.ts +102 -42
  87. package/dist/tools/audio.js +272 -685
  88. package/dist/tools/base-tool.d.ts +17 -0
  89. package/dist/tools/base-tool.js +46 -0
  90. package/dist/tools/behavior-tree.d.ts +94 -0
  91. package/dist/tools/behavior-tree.js +39 -0
  92. package/dist/tools/blueprint/helpers.d.ts +29 -0
  93. package/dist/tools/blueprint/helpers.js +182 -0
  94. package/dist/tools/blueprint.d.ts +228 -118
  95. package/dist/tools/blueprint.js +685 -832
  96. package/dist/tools/consolidated-tool-definitions.d.ts +5462 -1781
  97. package/dist/tools/consolidated-tool-definitions.js +829 -496
  98. package/dist/tools/consolidated-tool-handlers.d.ts +2 -1
  99. package/dist/tools/consolidated-tool-handlers.js +211 -1026
  100. package/dist/tools/debug.d.ts +143 -85
  101. package/dist/tools/debug.js +234 -180
  102. package/dist/tools/dynamic-handler-registry.d.ts +11 -0
  103. package/dist/tools/dynamic-handler-registry.js +101 -0
  104. package/dist/tools/editor.d.ts +139 -18
  105. package/dist/tools/editor.js +239 -244
  106. package/dist/tools/engine.d.ts +10 -4
  107. package/dist/tools/engine.js +13 -5
  108. package/dist/tools/environment.d.ts +36 -0
  109. package/dist/tools/environment.js +267 -0
  110. package/dist/tools/foliage.d.ts +105 -14
  111. package/dist/tools/foliage.js +219 -331
  112. package/dist/tools/handlers/actor-handlers.d.ts +3 -0
  113. package/dist/tools/handlers/actor-handlers.js +232 -0
  114. package/dist/tools/handlers/animation-handlers.d.ts +3 -0
  115. package/dist/tools/handlers/animation-handlers.js +185 -0
  116. package/dist/tools/handlers/argument-helper.d.ts +16 -0
  117. package/dist/tools/handlers/argument-helper.js +80 -0
  118. package/dist/tools/handlers/asset-handlers.d.ts +3 -0
  119. package/dist/tools/handlers/asset-handlers.js +496 -0
  120. package/dist/tools/handlers/audio-handlers.d.ts +3 -0
  121. package/dist/tools/handlers/audio-handlers.js +166 -0
  122. package/dist/tools/handlers/blueprint-handlers.d.ts +4 -0
  123. package/dist/tools/handlers/blueprint-handlers.js +358 -0
  124. package/dist/tools/handlers/common-handlers.d.ts +14 -0
  125. package/dist/tools/handlers/common-handlers.js +56 -0
  126. package/dist/tools/handlers/editor-handlers.d.ts +3 -0
  127. package/dist/tools/handlers/editor-handlers.js +119 -0
  128. package/dist/tools/handlers/effect-handlers.d.ts +3 -0
  129. package/dist/tools/handlers/effect-handlers.js +171 -0
  130. package/dist/tools/handlers/environment-handlers.d.ts +3 -0
  131. package/dist/tools/handlers/environment-handlers.js +170 -0
  132. package/dist/tools/handlers/graph-handlers.d.ts +3 -0
  133. package/dist/tools/handlers/graph-handlers.js +90 -0
  134. package/dist/tools/handlers/input-handlers.d.ts +3 -0
  135. package/dist/tools/handlers/input-handlers.js +21 -0
  136. package/dist/tools/handlers/inspect-handlers.d.ts +3 -0
  137. package/dist/tools/handlers/inspect-handlers.js +383 -0
  138. package/dist/tools/handlers/level-handlers.d.ts +3 -0
  139. package/dist/tools/handlers/level-handlers.js +237 -0
  140. package/dist/tools/handlers/lighting-handlers.d.ts +3 -0
  141. package/dist/tools/handlers/lighting-handlers.js +144 -0
  142. package/dist/tools/handlers/performance-handlers.d.ts +3 -0
  143. package/dist/tools/handlers/performance-handlers.js +130 -0
  144. package/dist/tools/handlers/pipeline-handlers.d.ts +3 -0
  145. package/dist/tools/handlers/pipeline-handlers.js +110 -0
  146. package/dist/tools/handlers/sequence-handlers.d.ts +3 -0
  147. package/dist/tools/handlers/sequence-handlers.js +376 -0
  148. package/dist/tools/handlers/system-handlers.d.ts +4 -0
  149. package/dist/tools/handlers/system-handlers.js +506 -0
  150. package/dist/tools/input.d.ts +19 -0
  151. package/dist/tools/input.js +89 -0
  152. package/dist/tools/introspection.d.ts +103 -40
  153. package/dist/tools/introspection.js +425 -568
  154. package/dist/tools/landscape.d.ts +97 -36
  155. package/dist/tools/landscape.js +280 -409
  156. package/dist/tools/level.d.ts +130 -10
  157. package/dist/tools/level.js +639 -675
  158. package/dist/tools/lighting.d.ts +77 -38
  159. package/dist/tools/lighting.js +441 -943
  160. package/dist/tools/logs.d.ts +3 -3
  161. package/dist/tools/logs.js +5 -57
  162. package/dist/tools/materials.d.ts +91 -24
  163. package/dist/tools/materials.js +190 -118
  164. package/dist/tools/niagara.d.ts +149 -39
  165. package/dist/tools/niagara.js +232 -182
  166. package/dist/tools/performance.d.ts +27 -12
  167. package/dist/tools/performance.js +204 -122
  168. package/dist/tools/physics.d.ts +32 -77
  169. package/dist/tools/physics.js +171 -582
  170. package/dist/tools/property-dictionary.d.ts +13 -0
  171. package/dist/tools/property-dictionary.js +82 -0
  172. package/dist/tools/sequence.d.ts +73 -48
  173. package/dist/tools/sequence.js +196 -748
  174. package/dist/tools/tool-definition-utils.d.ts +59 -0
  175. package/dist/tools/tool-definition-utils.js +35 -0
  176. package/dist/tools/ui.d.ts +66 -34
  177. package/dist/tools/ui.js +134 -214
  178. package/dist/types/env.d.ts +0 -3
  179. package/dist/types/env.js +0 -7
  180. package/dist/types/tool-interfaces.d.ts +898 -0
  181. package/dist/types/tool-interfaces.js +2 -0
  182. package/dist/types/tool-types.d.ts +183 -19
  183. package/dist/types/tool-types.js +0 -4
  184. package/dist/unreal-bridge.d.ts +24 -131
  185. package/dist/unreal-bridge.js +364 -1506
  186. package/dist/utils/command-validator.d.ts +9 -0
  187. package/dist/utils/command-validator.js +67 -0
  188. package/dist/utils/elicitation.d.ts +1 -1
  189. package/dist/utils/elicitation.js +12 -15
  190. package/dist/utils/error-handler.d.ts +2 -51
  191. package/dist/utils/error-handler.js +11 -87
  192. package/dist/utils/ini-reader.d.ts +3 -0
  193. package/dist/utils/ini-reader.js +69 -0
  194. package/dist/utils/logger.js +9 -6
  195. package/dist/utils/normalize.d.ts +3 -0
  196. package/dist/utils/normalize.js +56 -0
  197. package/dist/utils/response-factory.d.ts +7 -0
  198. package/dist/utils/response-factory.js +33 -0
  199. package/dist/utils/response-validator.d.ts +3 -24
  200. package/dist/utils/response-validator.js +130 -81
  201. package/dist/utils/result-helpers.d.ts +4 -5
  202. package/dist/utils/result-helpers.js +15 -16
  203. package/dist/utils/safe-json.js +5 -11
  204. package/dist/utils/unreal-command-queue.d.ts +24 -0
  205. package/dist/utils/unreal-command-queue.js +120 -0
  206. package/dist/utils/validation.d.ts +0 -40
  207. package/dist/utils/validation.js +1 -78
  208. package/dist/wasm/index.d.ts +70 -0
  209. package/dist/wasm/index.js +535 -0
  210. package/docs/GraphQL-API.md +888 -0
  211. package/docs/Migration-Guide-v0.5.0.md +692 -0
  212. package/docs/Roadmap.md +53 -0
  213. package/docs/WebAssembly-Integration.md +628 -0
  214. package/docs/editor-plugin-extension.md +370 -0
  215. package/docs/handler-mapping.md +242 -0
  216. package/docs/native-automation-progress.md +128 -0
  217. package/docs/testing-guide.md +423 -0
  218. package/mcp-config-example.json +6 -6
  219. package/package.json +60 -27
  220. package/plugins/McpAutomationBridge/Config/FilterPlugin.ini +8 -0
  221. package/plugins/McpAutomationBridge/McpAutomationBridge.uplugin +64 -0
  222. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/McpAutomationBridge.Build.cs +189 -0
  223. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.cpp +22 -0
  224. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.h +30 -0
  225. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeHelpers.h +1983 -0
  226. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeModule.cpp +72 -0
  227. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSettings.cpp +46 -0
  228. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +581 -0
  229. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +2394 -0
  230. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetQueryHandlers.cpp +300 -0
  231. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetWorkflowHandlers.cpp +2807 -0
  232. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AudioHandlers.cpp +1087 -0
  233. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BehaviorTreeHandlers.cpp +488 -0
  234. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.cpp +643 -0
  235. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.h +31 -0
  236. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +1184 -0
  237. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +5652 -0
  238. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers_List.cpp +152 -0
  239. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ControlHandlers.cpp +2614 -0
  240. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_DebugHandlers.cpp +42 -0
  241. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EditorFunctionHandlers.cpp +1237 -0
  242. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +1701 -0
  243. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +2145 -0
  244. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_FoliageHandlers.cpp +954 -0
  245. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InputHandlers.cpp +209 -0
  246. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InsightsHandlers.cpp +41 -0
  247. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LandscapeHandlers.cpp +1164 -0
  248. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LevelHandlers.cpp +762 -0
  249. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +634 -0
  250. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LogHandlers.cpp +136 -0
  251. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_MaterialGraphHandlers.cpp +494 -0
  252. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraGraphHandlers.cpp +278 -0
  253. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraHandlers.cpp +625 -0
  254. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PerformanceHandlers.cpp +401 -0
  255. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PipelineHandlers.cpp +67 -0
  256. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +735 -0
  257. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PropertyHandlers.cpp +2634 -0
  258. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_RenderHandlers.cpp +189 -0
  259. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.cpp +917 -0
  260. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.h +39 -0
  261. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +2670 -0
  262. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequencerHandlers.cpp +519 -0
  263. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_TestHandlers.cpp +38 -0
  264. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_UiHandlers.cpp +668 -0
  265. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_WorldPartitionHandlers.cpp +346 -0
  266. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.cpp +1330 -0
  267. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.h +149 -0
  268. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +783 -0
  269. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSettings.h +115 -0
  270. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSubsystem.h +796 -0
  271. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpConnectionManager.h +117 -0
  272. package/scripts/check-unreal-connection.mjs +19 -0
  273. package/scripts/clean-tmp.js +23 -0
  274. package/scripts/patch-wasm.js +26 -0
  275. package/scripts/run-all-tests.mjs +131 -0
  276. package/scripts/smoke-test.ts +94 -0
  277. package/scripts/sync-mcp-plugin.js +143 -0
  278. package/scripts/test-no-plugin-alternates.mjs +113 -0
  279. package/scripts/validate-server.js +46 -0
  280. package/scripts/verify-automation-bridge.js +200 -0
  281. package/server.json +57 -21
  282. package/src/automation/bridge.ts +558 -0
  283. package/src/automation/connection-manager.ts +130 -0
  284. package/src/automation/handshake.ts +99 -0
  285. package/src/automation/index.ts +2 -0
  286. package/src/automation/message-handler.ts +167 -0
  287. package/src/automation/request-tracker.ts +123 -0
  288. package/src/automation/types.ts +107 -0
  289. package/src/cli.ts +33 -6
  290. package/src/config.ts +73 -0
  291. package/src/constants.ts +12 -0
  292. package/src/graphql/resolvers.ts +1010 -0
  293. package/src/graphql/schema.ts +452 -0
  294. package/src/graphql/server.ts +154 -0
  295. package/src/graphql/types.ts +7 -0
  296. package/src/handlers/resource-handlers.ts +186 -0
  297. package/src/index.ts +152 -663
  298. package/src/prompts/index.ts +4 -4
  299. package/src/resources/actors.ts +58 -76
  300. package/src/resources/assets.ts +147 -134
  301. package/src/resources/levels.ts +28 -33
  302. package/src/server/resource-registry.ts +47 -0
  303. package/src/server/tool-registry.ts +354 -0
  304. package/src/server-setup.ts +148 -0
  305. package/src/services/health-monitor.ts +132 -0
  306. package/src/services/metrics-server.ts +142 -0
  307. package/src/tools/actors.ts +417 -322
  308. package/src/tools/animation.ts +671 -461
  309. package/src/tools/assets.ts +353 -289
  310. package/src/tools/audio.ts +323 -766
  311. package/src/tools/base-tool.ts +52 -0
  312. package/src/tools/behavior-tree.ts +45 -0
  313. package/src/tools/blueprint/helpers.ts +189 -0
  314. package/src/tools/blueprint.ts +787 -965
  315. package/src/tools/consolidated-tool-definitions.ts +993 -515
  316. package/src/tools/consolidated-tool-handlers.ts +272 -1139
  317. package/src/tools/debug.ts +292 -187
  318. package/src/tools/dynamic-handler-registry.ts +151 -0
  319. package/src/tools/editor.ts +309 -246
  320. package/src/tools/engine.ts +14 -3
  321. package/src/tools/environment.ts +287 -0
  322. package/src/tools/foliage.ts +314 -379
  323. package/src/tools/handlers/actor-handlers.ts +271 -0
  324. package/src/tools/handlers/animation-handlers.ts +237 -0
  325. package/src/tools/handlers/argument-helper.ts +142 -0
  326. package/src/tools/handlers/asset-handlers.ts +532 -0
  327. package/src/tools/handlers/audio-handlers.ts +194 -0
  328. package/src/tools/handlers/blueprint-handlers.ts +380 -0
  329. package/src/tools/handlers/common-handlers.ts +87 -0
  330. package/src/tools/handlers/editor-handlers.ts +123 -0
  331. package/src/tools/handlers/effect-handlers.ts +220 -0
  332. package/src/tools/handlers/environment-handlers.ts +183 -0
  333. package/src/tools/handlers/graph-handlers.ts +116 -0
  334. package/src/tools/handlers/input-handlers.ts +28 -0
  335. package/src/tools/handlers/inspect-handlers.ts +450 -0
  336. package/src/tools/handlers/level-handlers.ts +252 -0
  337. package/src/tools/handlers/lighting-handlers.ts +147 -0
  338. package/src/tools/handlers/performance-handlers.ts +132 -0
  339. package/src/tools/handlers/pipeline-handlers.ts +127 -0
  340. package/src/tools/handlers/sequence-handlers.ts +415 -0
  341. package/src/tools/handlers/system-handlers.ts +564 -0
  342. package/src/tools/input.ts +101 -0
  343. package/src/tools/introspection.ts +493 -584
  344. package/src/tools/landscape.ts +394 -489
  345. package/src/tools/level.ts +752 -694
  346. package/src/tools/lighting.ts +583 -984
  347. package/src/tools/logs.ts +9 -57
  348. package/src/tools/materials.ts +231 -121
  349. package/src/tools/niagara.ts +293 -168
  350. package/src/tools/performance.ts +320 -168
  351. package/src/tools/physics.ts +268 -613
  352. package/src/tools/property-dictionary.ts +98 -0
  353. package/src/tools/sequence.ts +255 -815
  354. package/src/tools/tool-definition-utils.ts +35 -0
  355. package/src/tools/ui.ts +207 -283
  356. package/src/types/env.ts +0 -10
  357. package/src/types/tool-interfaces.ts +250 -0
  358. package/src/types/tool-types.ts +243 -21
  359. package/src/unreal-bridge.ts +460 -1550
  360. package/src/utils/command-validator.ts +75 -0
  361. package/src/utils/elicitation.ts +10 -7
  362. package/src/utils/error-handler.ts +14 -90
  363. package/src/utils/ini-reader.ts +86 -0
  364. package/src/utils/logger.ts +8 -3
  365. package/src/utils/normalize.ts +60 -0
  366. package/src/utils/response-factory.ts +39 -0
  367. package/src/utils/response-validator.ts +176 -56
  368. package/src/utils/result-helpers.ts +21 -19
  369. package/src/utils/safe-json.ts +14 -11
  370. package/src/utils/unreal-command-queue.ts +152 -0
  371. package/src/utils/validation.ts +4 -1
  372. package/src/wasm/index.ts +838 -0
  373. package/test-server.mjs +100 -0
  374. package/tests/run-unreal-tool-tests.mjs +242 -14
  375. package/tests/test-animation.mjs +44 -0
  376. package/tests/test-asset-advanced.mjs +82 -0
  377. package/tests/test-asset-errors.mjs +35 -0
  378. package/tests/test-audio.mjs +219 -0
  379. package/tests/test-automation-timeouts.mjs +98 -0
  380. package/tests/test-behavior-tree.mjs +261 -0
  381. package/tests/test-blueprint-events.mjs +35 -0
  382. package/tests/test-blueprint-graph.mjs +79 -0
  383. package/tests/test-blueprint.mjs +577 -0
  384. package/tests/test-client-mode.mjs +86 -0
  385. package/tests/test-console-command.mjs +56 -0
  386. package/tests/test-control-actor.mjs +425 -0
  387. package/tests/test-control-editor.mjs +80 -0
  388. package/tests/test-extra-tools.mjs +38 -0
  389. package/tests/test-graphql.mjs +322 -0
  390. package/tests/test-inspect.mjs +72 -0
  391. package/tests/test-landscape.mjs +60 -0
  392. package/tests/test-manage-asset.mjs +438 -0
  393. package/tests/test-manage-level.mjs +70 -0
  394. package/tests/test-materials.mjs +356 -0
  395. package/tests/test-niagara.mjs +185 -0
  396. package/tests/test-no-inline-python.mjs +122 -0
  397. package/tests/test-plugin-handshake.mjs +82 -0
  398. package/tests/test-render.mjs +33 -0
  399. package/tests/test-runner.mjs +933 -0
  400. package/tests/test-search-assets.mjs +66 -0
  401. package/tests/test-sequence.mjs +68 -0
  402. package/tests/test-system.mjs +57 -0
  403. package/tests/test-wasm.mjs +193 -0
  404. package/tests/test-world-partition.mjs +215 -0
  405. package/tsconfig.json +3 -3
  406. package/wasm/Cargo.lock +363 -0
  407. package/wasm/Cargo.toml +42 -0
  408. package/wasm/LICENSE +21 -0
  409. package/wasm/README.md +253 -0
  410. package/wasm/src/dependency_resolver.rs +377 -0
  411. package/wasm/src/lib.rs +153 -0
  412. package/wasm/src/property_parser.rs +271 -0
  413. package/wasm/src/transform_math.rs +396 -0
  414. package/wasm/tests/integration.rs +109 -0
  415. package/.github/workflows/smithery-build.yml +0 -29
  416. package/dist/tools/build_environment_advanced.d.ts +0 -65
  417. package/dist/tools/build_environment_advanced.js +0 -633
  418. package/dist/tools/rc.d.ts +0 -110
  419. package/dist/tools/rc.js +0 -437
  420. package/dist/tools/visual.d.ts +0 -40
  421. package/dist/tools/visual.js +0 -282
  422. package/dist/utils/http.d.ts +0 -6
  423. package/dist/utils/http.js +0 -151
  424. package/dist/utils/python-output.d.ts +0 -18
  425. package/dist/utils/python-output.js +0 -290
  426. package/dist/utils/python.d.ts +0 -2
  427. package/dist/utils/python.js +0 -4
  428. package/dist/utils/stdio-redirect.d.ts +0 -2
  429. package/dist/utils/stdio-redirect.js +0 -20
  430. package/docs/unreal-tool-test-cases.md +0 -574
  431. package/smithery.yaml +0 -29
  432. package/src/tools/build_environment_advanced.ts +0 -732
  433. package/src/tools/rc.ts +0 -515
  434. package/src/tools/visual.ts +0 -281
  435. package/src/utils/http.ts +0 -187
  436. package/src/utils/python-output.ts +0 -351
  437. package/src/utils/python.ts +0 -3
  438. package/src/utils/stdio-redirect.ts +0 -18
@@ -1,1052 +1,874 @@
1
- import { UnrealBridge } from '../unreal-bridge.js';
1
+ import { BaseTool } from './base-tool.js';
2
+ import { IBlueprintTools } from '../types/tool-interfaces.js';
3
+ import { Logger } from '../utils/logger.js';
2
4
  import { validateAssetParams, concurrencyDelay } from '../utils/validation.js';
3
- import { extractTaggedLine } from '../utils/python-output.js';
4
- import { interpretStandardResult, coerceBoolean, coerceString, coerceStringArray, bestEffortInterpretedText } from '../utils/result-helpers.js';
5
- import { escapePythonString } from '../utils/python.js';
5
+ import { coerceString } from '../utils/result-helpers.js';
6
6
 
7
- export class BlueprintTools {
8
- constructor(private bridge: UnrealBridge) {}
7
+ export class BlueprintTools extends BaseTool implements IBlueprintTools {
8
+ private log = new Logger('BlueprintTools');
9
+ private pluginBlueprintActionsAvailable: boolean | null = null;
9
10
 
10
- private async validateParentClassReference(parentClass: string, blueprintType: string): Promise<{ ok: boolean; resolved?: string; error?: string }> {
11
- const trimmed = parentClass?.trim();
12
- if (!trimmed) {
13
- return { ok: true };
11
+ private async sendAction(action: string, payload: Record<string, unknown> = {}, options?: { timeoutMs?: number; waitForEvent?: boolean; waitForEventTimeoutMs?: number }) {
12
+ const envDefault = Number(process.env.MCP_AUTOMATION_REQUEST_TIMEOUT_MS ?? '120000');
13
+ const defaultTimeout = Number.isFinite(envDefault) && envDefault > 0 ? envDefault : 120000;
14
+ const finalTimeout = typeof options?.timeoutMs === 'number' && options?.timeoutMs > 0 ? options.timeoutMs : defaultTimeout;
15
+ try {
16
+ const response: any = await this.sendAutomationRequest(action, payload, { timeoutMs: finalTimeout, waitForEvent: !!options?.waitForEvent, waitForEventTimeoutMs: options?.waitForEventTimeoutMs });
17
+ const success = response && response.success !== false;
18
+ const result = response.result ?? response;
19
+ return { success, message: response.message ?? undefined, error: response.success === false ? (response.error ?? response.message) : undefined, result, requestId: response.requestId } as any;
20
+ } catch (err: any) {
21
+ return { success: false, error: String(err), message: String(err) } as const;
14
22
  }
23
+ }
15
24
 
16
- const escapedParent = escapePythonString(trimmed);
17
- const python = `
18
- import unreal
19
- import json
25
+ private isUnknownActionResponse(res: any): boolean {
26
+ if (!res) return false;
27
+ const txt = String((res.error ?? res.message ?? '')).toLowerCase();
28
+ // Only treat specific error codes as "not implemented"
29
+ return txt.includes('unknown_action') || txt.includes('unknown automation action') || txt.includes('not_implemented') || txt === 'unknown_plugin_action';
30
+ }
20
31
 
21
- result = {
22
- 'success': False,
23
- 'resolved': '',
24
- 'error': ''
25
- }
32
+ private buildCandidates(rawName: string | undefined): string[] {
33
+ const trimmed = coerceString(rawName)?.trim();
34
+ if (!trimmed) return [];
35
+ const normalized = trimmed.replace(/\\/g, '/').replace(/\/\/+/g, '/');
36
+ const withoutLeading = normalized.replace(/^\/+/, '');
37
+ const basename = withoutLeading.split('/').pop() ?? withoutLeading;
38
+ const candidates: string[] = [];
39
+ if (normalized.includes('/')) {
40
+ if (normalized.startsWith('/')) candidates.push(normalized);
41
+ if (basename) {
42
+ candidates.push(`/Game/Blueprints/${basename}`);
43
+ candidates.push(`/Game/${basename}`);
44
+ }
45
+ if (!normalized.startsWith('/')) candidates.push(`/${withoutLeading}`);
46
+ } else {
47
+ if (basename) {
48
+ candidates.push(`/Game/Blueprints/${basename}`);
49
+ candidates.push(`/Game/${basename}`);
50
+ }
51
+ candidates.push(normalized);
52
+ candidates.push(`/${withoutLeading}`);
53
+ }
54
+ return candidates.filter(Boolean);
55
+ }
56
+
57
+ async createBlueprint(params: { name: string; blueprintType?: string; savePath?: string; parentClass?: string; properties?: Record<string, unknown>; timeoutMs?: number; waitForCompletion?: boolean; waitForCompletionTimeoutMs?: number }) {
58
+ try {
59
+ const validation = validateAssetParams({ name: params.name, savePath: params.savePath || '/Game/Blueprints' });
60
+ if (!validation.valid) return { success: false, message: `Failed to create blueprint: ${validation.error}`, error: validation.error };
61
+ const sanitized = validation.sanitized;
62
+ const payload: Record<string, unknown> = { name: sanitized.name, blueprintType: params.blueprintType ?? 'Actor', savePath: sanitized.savePath ?? '/Game/Blueprints', parentClass: params.parentClass, properties: params.properties, waitForCompletion: !!params.waitForCompletion };
63
+ await concurrencyDelay();
64
+
65
+ if (this.pluginBlueprintActionsAvailable === false) {
66
+ return { success: false, error: 'UNKNOWN_PLUGIN_ACTION', message: 'Automation plugin does not implement blueprint_create' } as const;
67
+ }
68
+
69
+ const envPluginTimeout = Number(process.env.MCP_AUTOMATION_PLUGIN_CREATE_TIMEOUT_MS ?? process.env.MCP_AUTOMATION_REQUEST_TIMEOUT_MS ?? '15000');
70
+ const pluginTimeout = Number.isFinite(envPluginTimeout) && envPluginTimeout > 0 ? envPluginTimeout : 15000;
71
+ try {
72
+ const res = await this.sendAction('blueprint_create', payload, { timeoutMs: typeof params.timeoutMs === 'number' ? params.timeoutMs : pluginTimeout, waitForEvent: !!params.waitForCompletion, waitForEventTimeoutMs: params.waitForCompletionTimeoutMs });
73
+ if (res && res.success) {
74
+ this.pluginBlueprintActionsAvailable = true;
75
+ // Enrich response for Validator
76
+ return {
77
+ ...res,
78
+ blueprint: sanitized.name,
79
+ path: `${sanitized.savePath}/${sanitized.name}`.replace('//', '/'),
80
+ message: res.message || `Created blueprint ${sanitized.name}`
81
+ };
82
+ }
83
+ if (res && this.isUnknownActionResponse(res)) {
84
+ this.pluginBlueprintActionsAvailable = false;
85
+ return { success: false, error: 'UNKNOWN_PLUGIN_ACTION', message: 'Automation plugin does not implement blueprint_create' } as const;
86
+ }
87
+ return res as any;
88
+ } catch (err: any) {
89
+ // ... (unchanged catch block)
90
+ const errTxt = String(err ?? '');
91
+ const isTimeout = errTxt.includes('Request timed out') || errTxt.includes('-32001') || errTxt.toLowerCase().includes('timeout');
92
+ if (isTimeout) {
93
+ this.pluginBlueprintActionsAvailable = false;
94
+ }
95
+ return { success: false, error: String(err), message: String(err) } as const;
96
+ }
97
+ } catch (err: any) {
98
+ return { success: false, error: String(err), message: String(err) };
99
+ }
100
+ }
101
+
102
+ async modifyConstructionScript(params: { blueprintPath: string; operations: any[]; compile?: boolean; save?: boolean; timeoutMs?: number; waitForCompletion?: boolean; waitForCompletionTimeoutMs?: number }) {
103
+ const blueprintPath = coerceString(params.blueprintPath);
104
+ if (!blueprintPath) return { success: false, message: 'Blueprint path is required', error: 'INVALID_BLUEPRINT_PATH' };
105
+ if (!Array.isArray(params.operations) || params.operations.length === 0) return { success: false, message: 'At least one SCS operation is required', error: 'MISSING_OPERATIONS' };
26
106
 
27
- def resolve_parent(spec, bp_type):
28
- name = (spec or '').strip()
29
- editor_lib = unreal.EditorAssetLibrary
30
- if not name:
31
- return None
32
- try:
33
- if name.startswith('/Script/'):
34
- return unreal.load_class(None, name)
35
- except Exception:
36
- pass
37
- try:
38
- if name.startswith('/Game/'):
39
- asset = editor_lib.load_asset(name)
40
- if asset:
41
- if hasattr(asset, 'generated_class'):
42
- try:
43
- generated = asset.generated_class()
44
- if generated:
45
- return generated
46
- except Exception:
47
- pass
48
- return asset
49
- except Exception:
50
- pass
51
- try:
52
- candidate = getattr(unreal, name, None)
53
- if candidate:
54
- return candidate
55
- except Exception:
56
- pass
57
- return None
58
-
59
- try:
60
- parent_spec = r"${escapedParent}"
61
- resolved = resolve_parent(parent_spec, "${blueprintType}")
62
- resolved_path = ''
63
-
64
- if resolved:
65
- try:
66
- resolved_path = resolved.get_path_name()
67
- except Exception:
68
- try:
69
- resolved_path = str(resolved.get_outer().get_path_name())
70
- except Exception:
71
- resolved_path = str(resolved)
72
-
73
- normalized_resolved = resolved_path.replace('Class ', '').replace('class ', '').strip().lower()
74
- normalized_spec = parent_spec.strip().lower()
75
-
76
- if normalized_spec.startswith('/script/'):
77
- if not normalized_resolved.endswith(normalized_spec):
78
- resolved = None
79
- elif normalized_spec.startswith('/game/'):
80
- try:
81
- if not unreal.EditorAssetLibrary.does_asset_exist(parent_spec):
82
- resolved = None
83
- except Exception:
84
- resolved = None
85
-
86
- if resolved:
87
- result['success'] = True
88
- try:
89
- result['resolved'] = resolved_path or str(resolved)
90
- except Exception:
91
- result['resolved'] = str(resolved)
92
- else:
93
- result['error'] = 'Parent class not found: ' + parent_spec
94
- except Exception as e:
95
- result['error'] = str(e)
96
-
97
- print('RESULT:' + json.dumps(result))
98
- `.trim();
107
+ // Fix: Map 'op' to 'type' if missing, for backward compatibility or user convenience
108
+ const operations = params.operations.map(op => {
109
+ if (op && typeof op === 'object' && op.op && !op.type) {
110
+ return { ...op, type: op.op };
111
+ }
112
+ return op;
113
+ });
99
114
 
115
+ const payload: any = { blueprintPath, operations };
116
+ if (typeof params.compile === 'boolean') payload.compile = params.compile;
117
+ if (typeof params.save === 'boolean') payload.save = params.save;
118
+ const res = await this.sendAction('blueprint_modify_scs', payload, { timeoutMs: params.timeoutMs, waitForEvent: !!params.waitForCompletion, waitForEventTimeoutMs: params.waitForCompletionTimeoutMs });
119
+
120
+ if (res && res.result && typeof res.result === 'object' && (res.result as any).error === 'SCS_UNAVAILABLE') {
121
+ this.pluginBlueprintActionsAvailable = false;
122
+ return { success: false, error: 'SCS_UNAVAILABLE', message: 'Plugin does not support construction script modification (blueprint_modify_scs)' } as const;
123
+ }
124
+ if (res && res.success) this.pluginBlueprintActionsAvailable = true;
125
+ if (res && this.isUnknownActionResponse(res)) {
126
+ this.pluginBlueprintActionsAvailable = false;
127
+ }
128
+ return res;
129
+ }
130
+
131
+ async addComponent(params: { blueprintName: string; componentType: string; componentName: string; attachTo?: string; transform?: Record<string, unknown>; properties?: Record<string, unknown>; compile?: boolean; save?: boolean; timeoutMs?: number; waitForCompletion?: boolean; waitForCompletionTimeoutMs?: number }) {
132
+ const blueprintName = coerceString(params.blueprintName);
133
+ if (!blueprintName) return { success: false as const, message: 'Blueprint name is required', error: 'INVALID_BLUEPRINT' };
134
+ const componentClass = coerceString(params.componentType);
135
+ if (!componentClass) return { success: false as const, message: 'Component class is required', error: 'INVALID_COMPONENT_CLASS' };
136
+ const rawComponentName = coerceString(params.componentName) ?? params.componentName;
137
+ if (!rawComponentName) return { success: false as const, message: 'Component name is required', error: 'INVALID_COMPONENT_NAME' };
138
+ const sanitizedComponentName = rawComponentName.replace(/[^A-Za-z0-9_]/g, '_');
139
+ const candidates = this.buildCandidates(blueprintName);
140
+ const primary = candidates[0];
141
+ if (!primary) return { success: false as const, error: 'Invalid blueprint name' };
100
142
  try {
101
- const response = await this.bridge.executePython(python);
102
- const interpreted = interpretStandardResult(response, {
103
- successMessage: 'Parent class resolved',
104
- failureMessage: 'Parent class validation failed'
105
- });
106
-
107
- if (interpreted.success) {
108
- return { ok: true, resolved: (interpreted.payload as any)?.resolved ?? interpreted.message };
143
+ const op = { type: 'add_component', componentName: sanitizedComponentName, componentClass, attachTo: params.attachTo, transform: params.transform, properties: params.properties };
144
+ const svcResult = await this.modifyConstructionScript({ blueprintPath: primary, operations: [op], compile: params.compile, save: params.save, timeoutMs: params.timeoutMs, waitForCompletion: params.waitForCompletion, waitForCompletionTimeoutMs: params.waitForCompletionTimeoutMs });
145
+ if (svcResult && svcResult.success) {
146
+ this.pluginBlueprintActionsAvailable = true;
147
+ return { ...(svcResult as any), component: sanitizedComponentName, componentName: sanitizedComponentName, componentType: componentClass, componentClass, blueprintPath: svcResult.blueprintPath ?? primary } as const;
109
148
  }
149
+ if (svcResult && (this.isUnknownActionResponse(svcResult) || (svcResult.error && svcResult.error === 'SCS_UNAVAILABLE'))) {
150
+ this.pluginBlueprintActionsAvailable = false;
151
+ return { success: false, error: 'SCS_UNAVAILABLE', message: 'Plugin does not support construction script modification (blueprint_modify_scs)' } as const;
152
+ }
153
+ return svcResult as any;
154
+ } catch (err: any) {
155
+ return { success: false, error: String(err) };
156
+ }
157
+ }
158
+
159
+ async waitForBlueprint(blueprintRef: string | string[], timeoutMs?: number) {
160
+ const candidates = Array.isArray(blueprintRef) ? blueprintRef : this.buildCandidates(blueprintRef as string | undefined);
161
+ if (!candidates || candidates.length === 0) return { success: false, error: 'Invalid blueprint reference', checked: [] } as any;
162
+ if (this.pluginBlueprintActionsAvailable === false) {
163
+ return { success: false, error: 'UNKNOWN_PLUGIN_ACTION', message: 'Automation plugin does not implement blueprint_exists' } as any;
164
+ }
110
165
 
111
- const error = interpreted.error || (interpreted.payload as any)?.error || `Parent class not found: ${trimmed}`;
112
- return { ok: false, error };
166
+ const start = Date.now();
167
+ const envDefault = Number(process.env.MCP_AUTOMATION_SCS_TIMEOUT_MS ?? process.env.MCP_AUTOMATION_REQUEST_TIMEOUT_MS ?? '15000');
168
+ // Default to 15s (15000ms) instead of 120s to avoid long hangs on non-existent assets
169
+ const defaultTimeout = Number.isFinite(envDefault) && envDefault > 0 ? envDefault : 15000;
170
+ const tot = typeof timeoutMs === 'number' && timeoutMs > 0 ? timeoutMs : defaultTimeout;
171
+ const perCheck = Math.min(5000, Math.max(1000, Math.floor(tot / 6)));
172
+ while (Date.now() - start < tot) {
173
+ for (const candidate of candidates) {
174
+ try {
175
+ const r = await this.sendAction('blueprint_exists', { blueprintCandidates: [candidate], requestedPath: candidate }, { timeoutMs: Math.min(perCheck, tot) });
176
+ if (r && r.success && r.result && (r.result.exists === true || r.result.found)) {
177
+ this.pluginBlueprintActionsAvailable = true;
178
+ return { success: true, found: r.result.found ?? candidate } as any;
179
+ }
180
+ if (r && r.success === false && this.isUnknownActionResponse(r)) {
181
+ this.pluginBlueprintActionsAvailable = false;
182
+ return { success: false, error: 'UNKNOWN_PLUGIN_ACTION', message: 'Automation plugin does not implement blueprint_exists' } as any;
183
+ }
184
+ } catch (_e) {
185
+ // ignore and try next candidate
186
+ }
187
+ }
188
+ await new Promise((r) => setTimeout(r, 1000));
189
+ }
190
+ if (this.pluginBlueprintActionsAvailable === null) {
191
+ return { success: false, error: 'UNKNOWN_PLUGIN_ACTION', message: 'Automation plugin availability unknown; blueprint_exists not implemented by plugin' } as any;
192
+ }
193
+ return { success: false, error: `Timeout waiting for blueprint after ${tot}ms`, checked: candidates } as any;
194
+ }
195
+
196
+ async getBlueprint(params: { blueprintName: string; timeoutMs?: number; }) {
197
+ const candidates = this.buildCandidates(params.blueprintName);
198
+ const primary = candidates[0];
199
+ if (!primary) return { success: false, error: 'Invalid blueprint name' } as const;
200
+ try {
201
+ const pluginResp = await this.sendAction('blueprint_get', { blueprintCandidates: candidates, requestedPath: primary }, { timeoutMs: params.timeoutMs });
202
+ if (pluginResp && pluginResp.success) {
203
+ if (pluginResp && typeof pluginResp === 'object') {
204
+ return { ...pluginResp, blueprint: pluginResp.result, blueprintPath: primary } as any;
205
+ }
206
+ return pluginResp;
207
+ }
208
+ if (pluginResp && this.isUnknownActionResponse(pluginResp)) {
209
+ return { success: false, error: 'UNKNOWN_PLUGIN_ACTION', message: 'Automation plugin does not implement blueprint_get' } as const;
210
+ }
211
+ return { success: false, error: pluginResp?.error ?? 'BLUEPRINT_GET_FAILED', message: pluginResp?.message ?? 'Failed to get blueprint via automation bridge' } as const;
113
212
  } catch (err: any) {
114
- return { ok: false, error: err?.message || String(err) };
213
+ return { success: false, error: String(err), message: String(err) } as const;
115
214
  }
116
215
  }
117
216
 
118
- /**
119
- * Create Blueprint
120
- */
121
- async createBlueprint(params: {
122
- name: string;
123
- blueprintType: 'Actor' | 'Pawn' | 'Character' | 'GameMode' | 'PlayerController' | 'HUD' | 'ActorComponent';
124
- savePath?: string;
125
- parentClass?: string;
126
- }) {
217
+ async getBlueprintInfo(params: { blueprintPath: string; timeoutMs?: number }) {
218
+ const blueprintPath = coerceString(params.blueprintPath);
219
+ if (!blueprintPath) {
220
+ return { success: false, error: 'INVALID_BLUEPRINT_PATH', message: 'Blueprint path is required' } as const;
221
+ }
222
+
223
+ const candidates = this.buildCandidates(blueprintPath);
224
+ const primary = candidates[0] ?? blueprintPath;
225
+
127
226
  try {
128
- // Validate and sanitize parameters
129
- const validation = validateAssetParams({
130
- name: params.name,
131
- savePath: params.savePath || '/Game/Blueprints'
132
- });
133
-
134
- if (!validation.valid) {
227
+ const resp = await this.sendAction('blueprint_get', { blueprintCandidates: candidates.length > 0 ? candidates : [primary], requestedPath: primary }, { timeoutMs: params.timeoutMs });
228
+ if (resp && resp.success) {
229
+ const result = resp.result ?? resp;
230
+ const resolvedPath = typeof result?.resolvedPath === 'string' ? result.resolvedPath : primary;
135
231
  return {
136
- success: false,
137
- message: `Failed to create blueprint: ${validation.error}`,
138
- error: validation.error
232
+ success: true,
233
+ message: resp.message ?? `Blueprint metadata retrieved for ${resolvedPath}`,
234
+ blueprintPath: resolvedPath,
235
+ blueprint: result,
236
+ result,
237
+ requestId: resp.requestId
139
238
  };
140
239
  }
141
- const sanitizedParams = validation.sanitized;
142
- const path = sanitizedParams.savePath || '/Game/Blueprints';
143
240
 
144
- if (path.startsWith('/Engine')) {
145
- const message = `Failed to create blueprint: destination path ${path} is read-only`;
146
- return { success: false, message, error: message };
241
+ if (resp && this.isUnknownActionResponse(resp)) {
242
+ return { success: false, error: 'UNKNOWN_PLUGIN_ACTION', message: 'Automation plugin does not implement blueprint_get' } as const;
147
243
  }
148
- if (params.parentClass && params.parentClass.trim()) {
149
- const parentValidation = await this.validateParentClassReference(params.parentClass, params.blueprintType);
150
- if (!parentValidation.ok) {
151
- const error = parentValidation.error || `Parent class not found: ${params.parentClass}`;
152
- const message = `Failed to create blueprint: ${error}`;
153
- return { success: false, message, error };
154
- }
155
- }
156
- const escapedName = escapePythonString(sanitizedParams.name);
157
- const escapedPath = escapePythonString(path);
158
- const escapedParent = escapePythonString(params.parentClass ?? '');
159
244
 
160
- await concurrencyDelay();
245
+ return { success: false, error: resp?.error ?? 'BLUEPRINT_GET_FAILED', message: resp?.message ?? 'Failed to get blueprint via automation bridge' } as const;
246
+ } catch (err: any) {
247
+ return { success: false, error: String(err), message: String(err) } as const;
248
+ }
249
+ }
161
250
 
162
- const pythonScript = `
163
- import unreal
164
- import time
165
- import json
166
- import traceback
167
-
168
- def ensure_asset_persistence(asset_path):
169
- try:
170
- asset_subsystem = None
171
- try:
172
- asset_subsystem = unreal.get_editor_subsystem(unreal.EditorAssetSubsystem)
173
- except Exception:
174
- asset_subsystem = None
175
-
176
- editor_lib = unreal.EditorAssetLibrary
177
-
178
- asset = None
179
- if asset_subsystem and hasattr(asset_subsystem, 'load_asset'):
180
- try:
181
- asset = asset_subsystem.load_asset(asset_path)
182
- except Exception:
183
- asset = None
184
- if not asset:
185
- try:
186
- asset = editor_lib.load_asset(asset_path)
187
- except Exception:
188
- asset = None
189
- if not asset:
190
- return False
191
-
192
- saved = False
193
- if asset_subsystem and hasattr(asset_subsystem, 'save_loaded_asset'):
194
- try:
195
- saved = asset_subsystem.save_loaded_asset(asset)
196
- except Exception:
197
- saved = False
198
- if not saved and asset_subsystem and hasattr(asset_subsystem, 'save_asset'):
199
- try:
200
- saved = asset_subsystem.save_asset(asset_path, only_if_is_dirty=False)
201
- except Exception:
202
- saved = False
203
- if not saved:
204
- try:
205
- if hasattr(editor_lib, 'save_loaded_asset'):
206
- saved = editor_lib.save_loaded_asset(asset)
207
- else:
208
- saved = editor_lib.save_asset(asset_path, only_if_is_dirty=False)
209
- except Exception:
210
- saved = False
211
-
212
- if not saved:
213
- return False
214
-
215
- asset_dir = asset_path.rsplit('/', 1)[0]
216
- try:
217
- registry = unreal.AssetRegistryHelpers.get_asset_registry()
218
- if hasattr(registry, 'scan_paths_synchronous'):
219
- registry.scan_paths_synchronous([asset_dir], True)
220
- except Exception:
221
- pass
222
-
223
- for _ in range(5):
224
- if editor_lib.does_asset_exist(asset_path):
225
- return True
226
- time.sleep(0.2)
227
- try:
228
- registry = unreal.AssetRegistryHelpers.get_asset_registry()
229
- if hasattr(registry, 'scan_paths_synchronous'):
230
- registry.scan_paths_synchronous([asset_dir], True)
231
- except Exception:
232
- pass
233
- return False
234
- except Exception as e:
235
- print(f"Error ensuring persistence: {e}")
236
- return False
237
-
238
- def resolve_parent_class(explicit_name, blueprint_type):
239
- editor_lib = unreal.EditorAssetLibrary
240
- name = (explicit_name or '').strip()
241
- if name:
242
- try:
243
- if name.startswith('/Script/'):
244
- try:
245
- loaded = unreal.load_class(None, name)
246
- if loaded:
247
- return loaded
248
- except Exception:
249
- pass
250
- if name.startswith('/Game/'):
251
- loaded_asset = editor_lib.load_asset(name)
252
- if loaded_asset:
253
- if hasattr(loaded_asset, 'generated_class'):
254
- try:
255
- generated = loaded_asset.generated_class()
256
- if generated:
257
- return generated
258
- except Exception:
259
- pass
260
- return loaded_asset
261
- candidate = getattr(unreal, name, None)
262
- if candidate:
263
- return candidate
264
- except Exception:
265
- pass
266
- return None
267
-
268
- mapping = {
269
- 'Actor': unreal.Actor,
270
- 'Pawn': unreal.Pawn,
271
- 'Character': unreal.Character,
272
- 'GameMode': unreal.GameModeBase,
273
- 'PlayerController': unreal.PlayerController,
274
- 'HUD': unreal.HUD,
275
- 'ActorComponent': unreal.ActorComponent,
251
+ async probeSubobjectDataHandle(opts: { componentClass?: string } = {}) {
252
+ return await this.sendAction('blueprint_probe_subobject_handle', { componentClass: opts.componentClass });
253
+ }
254
+
255
+ async setBlueprintDefault(params: { blueprintName: string; propertyName: string; value: unknown }) {
256
+ const candidates = this.buildCandidates(params.blueprintName);
257
+ const primary = candidates[0];
258
+ if (!primary) return { success: false as const, error: 'Invalid blueprint name' };
259
+ return await this.sendAction('blueprint_set_default', { blueprintCandidates: candidates, requestedPath: primary, propertyName: params.propertyName, value: params.value });
276
260
  }
277
- return mapping.get(blueprint_type, unreal.Actor)
278
-
279
- result = {
280
- 'success': False,
281
- 'message': '',
282
- 'path': '',
283
- 'error': '',
284
- 'exists': False,
285
- 'parent': '',
286
- 'verifyError': '',
287
- 'warnings': [],
288
- 'details': []
289
- }
290
261
 
291
- success_message = ''
292
-
293
- def record_detail(message):
294
- result['details'].append(str(message))
295
-
296
- def record_warning(message):
297
- result['warnings'].append(str(message))
298
-
299
- def set_message(message):
300
- global success_message
301
- if not success_message:
302
- success_message = str(message)
303
-
304
- def set_error(message):
305
- result['error'] = str(message)
306
-
307
- asset_path = "${escapedPath}"
308
- asset_name = "${escapedName}"
309
- full_path = f"{asset_path}/{asset_name}"
310
- result['path'] = full_path
311
-
312
- asset_subsystem = None
313
- try:
314
- asset_subsystem = unreal.get_editor_subsystem(unreal.EditorAssetSubsystem)
315
- except Exception:
316
- asset_subsystem = None
317
-
318
- editor_lib = unreal.EditorAssetLibrary
319
-
320
- try:
321
- level_subsystem = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
322
- play_subsystem = None
323
- try:
324
- play_subsystem = unreal.get_editor_subsystem(unreal.EditorPlayWorldSubsystem)
325
- except Exception:
326
- play_subsystem = None
327
-
328
- is_playing = False
329
- if level_subsystem and hasattr(level_subsystem, 'is_in_play_in_editor'):
330
- is_playing = bool(level_subsystem.is_in_play_in_editor())
331
- elif play_subsystem and hasattr(play_subsystem, 'is_playing_in_editor'):
332
- is_playing = bool(play_subsystem.is_playing_in_editor())
333
-
334
- if is_playing:
335
- print('Stopping Play In Editor mode...')
336
- record_detail('Stopping Play In Editor mode')
337
- if level_subsystem and hasattr(level_subsystem, 'editor_request_end_play'):
338
- level_subsystem.editor_request_end_play()
339
- elif play_subsystem and hasattr(play_subsystem, 'stop_playing_session'):
340
- play_subsystem.stop_playing_session()
341
- elif play_subsystem and hasattr(play_subsystem, 'end_play'):
342
- play_subsystem.end_play()
343
- else:
344
- record_warning('Unable to stop Play In Editor via modern subsystems; please stop PIE manually.')
345
- time.sleep(0.5)
346
- except Exception as stop_err:
347
- record_warning(f'PIE stop check failed: {stop_err}')
348
-
349
- try:
350
- try:
351
- if asset_subsystem and hasattr(asset_subsystem, 'does_asset_exist'):
352
- asset_exists = asset_subsystem.does_asset_exist(full_path)
353
- else:
354
- asset_exists = editor_lib.does_asset_exist(full_path)
355
- except Exception:
356
- asset_exists = editor_lib.does_asset_exist(full_path)
357
-
358
- result['exists'] = bool(asset_exists)
359
-
360
- if asset_exists:
361
- existing = None
362
- try:
363
- if asset_subsystem and hasattr(asset_subsystem, 'load_asset'):
364
- existing = asset_subsystem.load_asset(full_path)
365
- elif asset_subsystem and hasattr(asset_subsystem, 'get_asset'):
366
- existing = asset_subsystem.get_asset(full_path)
367
- else:
368
- existing = editor_lib.load_asset(full_path)
369
- except Exception:
370
- existing = editor_lib.load_asset(full_path)
371
-
372
- if existing:
373
- result['success'] = True
374
- result['message'] = f"Blueprint already exists at {full_path}"
375
- set_message(result['message'])
376
- record_detail(result['message'])
377
- try:
378
- result['parent'] = str(existing.generated_class())
379
- except Exception:
380
- try:
381
- result['parent'] = str(type(existing))
382
- except Exception:
383
- pass
384
- else:
385
- set_error(f"Asset exists but could not be loaded: {full_path}")
386
- record_warning(result['error'])
387
- else:
388
- factory = unreal.BlueprintFactory()
389
- explicit_parent = "${escapedParent}"
390
- parent_class = None
391
-
392
- if explicit_parent.strip():
393
- parent_class = resolve_parent_class(explicit_parent, "${params.blueprintType}")
394
- if not parent_class:
395
- set_error(f"Parent class not found: {explicit_parent}")
396
- record_warning(result['error'])
397
- raise RuntimeError(result['error'])
398
- else:
399
- parent_class = resolve_parent_class('', "${params.blueprintType}")
400
-
401
- if parent_class:
402
- result['parent'] = str(parent_class)
403
- record_detail(f"Resolved parent class: {result['parent']}")
404
- try:
405
- factory.set_editor_property('parent_class', parent_class)
406
- except Exception:
407
- try:
408
- factory.set_editor_property('ParentClass', parent_class)
409
- except Exception:
410
- try:
411
- factory.ParentClass = parent_class
412
- except Exception:
413
- pass
414
-
415
- new_asset = None
416
- try:
417
- if asset_subsystem and hasattr(asset_subsystem, 'create_asset'):
418
- new_asset = asset_subsystem.create_asset(
419
- asset_name=asset_name,
420
- package_path=asset_path,
421
- asset_class=unreal.Blueprint,
422
- factory=factory
423
- )
424
- else:
425
- asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
426
- new_asset = asset_tools.create_asset(
427
- asset_name=asset_name,
428
- package_path=asset_path,
429
- asset_class=unreal.Blueprint,
430
- factory=factory
431
- )
432
- except Exception as create_error:
433
- set_error(f"Asset creation failed: {create_error}")
434
- record_warning(result['error'])
435
- traceback.print_exc()
436
- new_asset = None
437
-
438
- if new_asset:
439
- result['message'] = f"Blueprint created at {full_path}"
440
- set_message(result['message'])
441
- record_detail(result['message'])
442
- if ensure_asset_persistence(full_path):
443
- verified = False
444
- try:
445
- if asset_subsystem and hasattr(asset_subsystem, 'does_asset_exist'):
446
- verified = asset_subsystem.does_asset_exist(full_path)
447
- else:
448
- verified = editor_lib.does_asset_exist(full_path)
449
- except Exception as verify_error:
450
- result['verifyError'] = str(verify_error)
451
- verified = editor_lib.does_asset_exist(full_path)
452
-
453
- if not verified:
454
- time.sleep(0.2)
455
- verified = editor_lib.does_asset_exist(full_path)
456
- if not verified:
457
- try:
458
- verified = editor_lib.load_asset(full_path) is not None
459
- except Exception:
460
- verified = False
461
-
462
- if verified:
463
- result['success'] = True
464
- result['error'] = ''
465
- set_message(result['message'])
466
- else:
467
- set_error(f"Blueprint not found after save: {full_path}")
468
- record_warning(result['error'])
469
- else:
470
- set_error('Failed to persist blueprint to disk')
471
- record_warning(result['error'])
472
- else:
473
- if not result['error']:
474
- set_error(f"Failed to create Blueprint {asset_name}")
475
- except Exception as e:
476
- set_error(str(e))
477
- record_warning(result['error'])
478
- traceback.print_exc()
479
-
480
- # Finalize messaging
481
- default_success_message = f"Blueprint created at {full_path}"
482
- default_failure_message = f"Failed to create blueprint {asset_name}"
483
-
484
- if result['success'] and not success_message:
485
- set_message(default_success_message)
486
-
487
- if not result['success'] and not result['error']:
488
- set_error(default_failure_message)
489
-
490
- if not result['message']:
491
- if result['success']:
492
- result['message'] = success_message or default_success_message
493
- else:
494
- result['message'] = result['error'] or default_failure_message
495
-
496
- result['error'] = None if result['success'] else result['error']
497
-
498
- if not result['warnings']:
499
- result.pop('warnings')
500
- if not result['details']:
501
- result.pop('details')
502
- if result.get('error') is None:
503
- result.pop('error')
504
-
505
- print('RESULT:' + json.dumps(result))
506
- `.trim();
507
-
508
- const response = await this.bridge.executePython(pythonScript);
509
- return this.parseBlueprintCreationOutput(response, sanitizedParams.name, path);
510
- } catch (err) {
511
- return { success: false, error: `Failed to create blueprint: ${err}` };
262
+ async addVariable(params: { blueprintName: string; variableName: string; variableType: string; defaultValue?: any; category?: string; isReplicated?: boolean; isPublic?: boolean; variablePinType?: Record<string, unknown>; timeoutMs?: number; waitForCompletion?: boolean; waitForCompletionTimeoutMs?: number }) {
263
+ const candidates = this.buildCandidates(params.blueprintName);
264
+ const primary = candidates[0];
265
+ if (!primary) return { success: false as const, error: 'Invalid blueprint name' };
266
+ const pluginResp = await this.sendAction('blueprint_add_variable', { blueprintCandidates: candidates, requestedPath: primary, variableName: params.variableName, variableType: params.variableType, defaultValue: params.defaultValue, category: params.category, isReplicated: params.isReplicated, isPublic: params.isPublic, variablePinType: params.variablePinType }, { timeoutMs: params.timeoutMs, waitForEvent: !!params.waitForCompletion, waitForEventTimeoutMs: params.waitForCompletionTimeoutMs });
267
+ if (pluginResp && pluginResp.success) {
268
+ return pluginResp;
269
+ }
270
+ if (pluginResp && this.isUnknownActionResponse(pluginResp)) {
271
+ return { success: false, error: 'UNKNOWN_PLUGIN_ACTION', message: 'Automation plugin does not implement blueprint_add_variable' } as const;
512
272
  }
273
+ return { success: false, error: pluginResp?.error ?? 'BLUEPRINT_ADD_VARIABLE_FAILED', message: pluginResp?.message ?? 'Failed to add variable via automation bridge' } as const;
513
274
  }
514
275
 
515
- private parseBlueprintCreationOutput(response: any, blueprintName: string, blueprintPath: string) {
516
- const defaultPath = `${blueprintPath}/${blueprintName}`;
517
- const interpreted = interpretStandardResult(response, {
518
- successMessage: `Blueprint ${blueprintName} created`,
519
- failureMessage: `Failed to create blueprint ${blueprintName}`
520
- });
276
+ async removeVariable(params: { blueprintName: string; variableName: string; timeoutMs?: number; waitForCompletion?: boolean; waitForCompletionTimeoutMs?: number }) {
277
+ const candidates = this.buildCandidates(params.blueprintName);
278
+ const primary = candidates[0];
279
+ if (!primary) return { success: false as const, error: 'Invalid blueprint name' };
280
+ const pluginResp = await this.sendAction('blueprint_remove_variable', { blueprintCandidates: candidates, requestedPath: primary, variableName: params.variableName }, { timeoutMs: params.timeoutMs, waitForEvent: !!params.waitForCompletion, waitForEventTimeoutMs: params.waitForCompletionTimeoutMs });
281
+ if (pluginResp && pluginResp.success) return pluginResp;
282
+ if (pluginResp && this.isUnknownActionResponse(pluginResp)) {
283
+ return { success: false, error: 'UNKNOWN_PLUGIN_ACTION', message: 'Automation plugin does not implement blueprint_remove_variable' } as const;
284
+ }
285
+ return { success: false, error: pluginResp?.error ?? 'BLUEPRINT_REMOVE_VARIABLE_FAILED', message: pluginResp?.message ?? 'Failed to remove variable via automation bridge' } as const;
286
+ }
521
287
 
522
- const payload = interpreted.payload ?? {};
523
- const hasPayload = Object.keys(payload).length > 0;
524
- const warnings = interpreted.warnings ?? coerceStringArray((payload as any).warnings) ?? undefined;
525
- const details = interpreted.details ?? coerceStringArray((payload as any).details) ?? undefined;
526
- const path = coerceString((payload as any).path) ?? defaultPath;
527
- const parent = coerceString((payload as any).parent);
528
- const verifyError = coerceString((payload as any).verifyError);
529
- const exists = coerceBoolean((payload as any).exists);
530
- const errorValue = coerceString((payload as any).error) ?? interpreted.error;
531
-
532
- if (hasPayload) {
533
- if (interpreted.success) {
534
- const outcome: {
535
- success: true;
536
- message: string;
537
- path: string;
538
- exists?: boolean;
539
- parent?: string;
540
- verifyError?: string;
541
- warnings?: string[];
542
- details?: string[];
543
- } = {
544
- success: true,
545
- message: interpreted.message,
546
- path
547
- };
288
+ async renameVariable(params: { blueprintName: string; oldName: string; newName: string; timeoutMs?: number; waitForCompletion?: boolean; waitForCompletionTimeoutMs?: number }) {
289
+ const candidates = this.buildCandidates(params.blueprintName);
290
+ const primary = candidates[0];
291
+ if (!primary) return { success: false as const, error: 'Invalid blueprint name' };
292
+ const pluginResp = await this.sendAction('blueprint_rename_variable', { blueprintCandidates: candidates, requestedPath: primary, oldName: params.oldName, newName: params.newName }, { timeoutMs: params.timeoutMs, waitForEvent: !!params.waitForCompletion, waitForEventTimeoutMs: params.waitForCompletionTimeoutMs });
293
+ if (pluginResp && pluginResp.success) return pluginResp;
294
+ if (pluginResp && this.isUnknownActionResponse(pluginResp)) {
295
+ return { success: false, error: 'UNKNOWN_PLUGIN_ACTION', message: 'Automation plugin does not implement blueprint_rename_variable' } as const;
296
+ }
297
+ return { success: false, error: pluginResp?.error ?? 'BLUEPRINT_RENAME_VARIABLE_FAILED', message: pluginResp?.message ?? 'Failed to rename variable via automation bridge' } as const;
298
+ }
548
299
 
549
- if (typeof exists === 'boolean') {
550
- outcome.exists = exists;
551
- }
552
- if (parent) {
553
- outcome.parent = parent;
554
- }
555
- if (verifyError) {
556
- outcome.verifyError = verifyError;
557
- }
558
- if (warnings && warnings.length > 0) {
559
- outcome.warnings = warnings;
560
- }
561
- if (details && details.length > 0) {
562
- outcome.details = details;
563
- }
564
300
 
565
- return outcome;
566
- }
567
301
 
568
- const fallbackMessage = errorValue ?? interpreted.message;
569
-
570
- const failureOutcome: {
571
- success: false;
572
- message: string;
573
- error: string;
574
- path: string;
575
- exists?: boolean;
576
- parent?: string;
577
- verifyError?: string;
578
- warnings?: string[];
579
- details?: string[];
580
- } = {
581
- success: false,
582
- message: `Failed to create blueprint: ${fallbackMessage}`,
583
- error: fallbackMessage,
584
- path
585
- };
302
+ async addEvent(params: { blueprintName: string; eventType: string; customEventName?: string; parameters?: Array<{ name: string; type: string }>; timeoutMs?: number; waitForCompletion?: boolean; waitForCompletionTimeoutMs?: number }) {
303
+ const candidates = this.buildCandidates(params.blueprintName);
304
+ const primary = candidates[0];
305
+ if (!primary) return { success: false as const, error: 'Invalid blueprint name' };
306
+ const pluginResp = await this.sendAction('blueprint_add_event', { blueprintCandidates: candidates, requestedPath: primary, eventType: params.eventType, customEventName: params.customEventName, parameters: params.parameters }, { timeoutMs: params.timeoutMs, waitForEvent: !!params.waitForCompletion, waitForEventTimeoutMs: params.waitForCompletionTimeoutMs });
307
+ if (pluginResp && pluginResp.success) {
308
+ return pluginResp;
309
+ }
310
+ if (pluginResp && this.isUnknownActionResponse(pluginResp)) {
311
+ return { success: false, error: 'UNKNOWN_PLUGIN_ACTION', message: 'Automation plugin does not implement blueprint_add_event' } as const;
312
+ }
313
+ return { success: false, error: pluginResp?.error ?? 'BLUEPRINT_ADD_EVENT_FAILED', message: pluginResp?.message ?? 'Failed to add event via automation bridge' } as const;
314
+ }
586
315
 
587
- if (typeof exists === 'boolean') {
588
- failureOutcome.exists = exists;
589
- }
590
- if (parent) {
591
- failureOutcome.parent = parent;
592
- }
593
- if (verifyError) {
594
- failureOutcome.verifyError = verifyError;
595
- }
596
- if (warnings && warnings.length > 0) {
597
- failureOutcome.warnings = warnings;
316
+ async removeEvent(params: { blueprintName: string; eventName: string; customEventName?: string; timeoutMs?: number; waitForCompletion?: boolean; waitForCompletionTimeoutMs?: number }) {
317
+ const candidates = this.buildCandidates(params.blueprintName);
318
+ const primary = candidates[0];
319
+ if (!primary) return { success: false as const, error: 'Invalid blueprint name' };
320
+
321
+ // Fix: Allow customEventName as alias for eventName
322
+ const finalEventName = params.eventName || params.customEventName;
323
+ if (!finalEventName) return { success: false, error: 'INVALID_ARGUMENT', message: 'eventName is required' } as const;
324
+
325
+ try {
326
+ const pluginResp = await this.sendAction('blueprint_remove_event', { blueprintCandidates: candidates, requestedPath: primary, eventName: finalEventName }, { timeoutMs: params.timeoutMs, waitForEvent: !!params.waitForCompletion, waitForEventTimeoutMs: params.waitForCompletionTimeoutMs });
327
+ if (pluginResp && pluginResp.success) {
328
+ return pluginResp;
598
329
  }
599
- if (details && details.length > 0) {
600
- failureOutcome.details = details;
330
+ if (pluginResp && this.isUnknownActionResponse(pluginResp)) {
331
+ return { success: false, error: 'UNKNOWN_PLUGIN_ACTION', message: 'Automation plugin does not implement blueprint_remove_event' } as const;
601
332
  }
333
+ return { success: false, error: pluginResp?.error ?? 'BLUEPRINT_REMOVE_EVENT_FAILED', message: pluginResp?.message ?? 'Failed to remove event via automation bridge' } as const;
334
+ } catch (err: any) {
335
+ return { success: false, error: String(err), message: String(err) } as const;
336
+ }
337
+ }
602
338
 
603
- return failureOutcome;
339
+ async addFunction(params: { blueprintName: string; functionName: string; inputs?: Array<{ name: string; type: string }>; outputs?: Array<{ name: string; type: string }>; isPublic?: boolean; category?: string; timeoutMs?: number; waitForCompletion?: boolean; waitForCompletionTimeoutMs?: number }) {
340
+ const candidates = this.buildCandidates(params.blueprintName);
341
+ const primary = candidates[0];
342
+ if (!primary) return { success: false as const, error: 'Invalid blueprint name' };
343
+ const pluginResp = await this.sendAction('blueprint_add_function', { blueprintCandidates: candidates, requestedPath: primary, functionName: params.functionName, inputs: params.inputs, outputs: params.outputs, isPublic: params.isPublic, category: params.category }, { timeoutMs: params.timeoutMs, waitForEvent: !!params.waitForCompletion, waitForEventTimeoutMs: params.waitForCompletionTimeoutMs });
344
+ if (pluginResp && pluginResp.success) {
345
+ return pluginResp;
346
+ }
347
+ if (pluginResp && this.isUnknownActionResponse(pluginResp)) {
348
+ return { success: false, error: 'UNKNOWN_PLUGIN_ACTION', message: 'Automation plugin does not implement blueprint_add_function' } as const;
604
349
  }
350
+ return { success: false, error: pluginResp?.error ?? 'BLUEPRINT_ADD_FUNCTION_FAILED', message: pluginResp?.message ?? 'Failed to add function via automation bridge' } as const;
351
+ }
605
352
 
606
- const cleanedText = bestEffortInterpretedText(interpreted) ?? '';
607
- const failureMessage = extractTaggedLine(cleanedText, 'FAILED:');
608
- if (failureMessage) {
609
- return {
610
- success: false,
611
- message: `Failed to create blueprint: ${failureMessage}`,
612
- error: failureMessage,
613
- path: defaultPath
614
- };
353
+ async setVariableMetadata(params: { blueprintName: string; variableName: string; metadata: Record<string, unknown>; timeoutMs?: number }) {
354
+ const candidates = this.buildCandidates(params.blueprintName);
355
+ const primary = candidates[0];
356
+ if (!primary) return { success: false as const, error: 'Invalid blueprint name' };
357
+ const pluginResp = await this.sendAction('blueprint_set_variable_metadata', { blueprintCandidates: candidates, requestedPath: primary, variableName: params.variableName, metadata: params.metadata }, { timeoutMs: params.timeoutMs });
358
+ if (pluginResp && pluginResp.success) {
359
+ return pluginResp;
615
360
  }
361
+ if (pluginResp && this.isUnknownActionResponse(pluginResp)) {
362
+ return { success: false, error: 'UNKNOWN_PLUGIN_ACTION', message: 'Automation plugin does not implement blueprint_set_variable_metadata' } as const;
363
+ }
364
+ return { success: false, error: pluginResp?.error ?? 'SET_VARIABLE_METADATA_FAILED', message: pluginResp?.message ?? 'Failed to set variable metadata via automation bridge' } as const;
365
+ }
616
366
 
617
- if (cleanedText.includes('SUCCESS')) {
618
- return {
619
- success: true,
620
- message: `Blueprint ${blueprintName} created`,
621
- path: defaultPath
622
- };
367
+ async addConstructionScript(params: { blueprintName: string; scriptName: string; timeoutMs?: number; waitForCompletion?: boolean; waitForCompletionTimeoutMs?: number }) {
368
+ const candidates = this.buildCandidates(params.blueprintName);
369
+ const primary = candidates[0];
370
+ if (!primary) return { success: false as const, error: 'Invalid blueprint name' };
371
+ const pluginResp = await this.sendAction('blueprint_add_construction_script', { blueprintCandidates: candidates, requestedPath: primary, scriptName: params.scriptName }, { timeoutMs: params.timeoutMs, waitForEvent: !!params.waitForCompletion, waitForEventTimeoutMs: params.waitForCompletionTimeoutMs });
372
+ if (pluginResp && pluginResp.success) return pluginResp;
373
+ if (pluginResp && this.isUnknownActionResponse(pluginResp)) {
374
+ return { success: false, error: 'UNKNOWN_PLUGIN_ACTION', message: 'Automation plugin does not implement blueprint_add_construction_script' } as const;
623
375
  }
376
+ return { success: false, error: pluginResp?.error ?? 'ADD_CONSTRUCTION_SCRIPT_FAILED', message: pluginResp?.message ?? 'Failed to add construction script via automation bridge' } as const;
377
+ }
624
378
 
625
- return {
626
- success: false,
627
- message: interpreted.message,
628
- error: interpreted.error ?? (cleanedText || JSON.stringify(response)),
629
- path: defaultPath
630
- };
379
+ async compileBlueprint(params: { blueprintName: string; saveAfterCompile?: boolean; }) {
380
+ try {
381
+ const candidates = this.buildCandidates(params.blueprintName);
382
+ const primary = candidates[0] ?? params.blueprintName;
383
+ const pluginResp = await this.sendAction('blueprint_compile', { requestedPath: primary, saveAfterCompile: params.saveAfterCompile });
384
+ if (pluginResp && pluginResp.success) {
385
+ return {
386
+ ...pluginResp,
387
+ blueprint: primary,
388
+ message: pluginResp.message || `Compiled ${primary}`
389
+ };
390
+ }
391
+ if (pluginResp && this.isUnknownActionResponse(pluginResp)) {
392
+ this.pluginBlueprintActionsAvailable = false;
393
+ return { success: false, error: 'UNKNOWN_PLUGIN_ACTION', message: 'Automation plugin does not implement blueprint_compile' } as const;
394
+ }
395
+ return { success: false, error: pluginResp?.error ?? 'BLUEPRINT_COMPILE_FAILED', message: pluginResp?.message ?? 'Failed to compile blueprint via automation bridge' } as const;
396
+ } catch (err: any) {
397
+ return { success: false, error: String(err) };
398
+ }
631
399
  }
632
400
 
633
- /**
634
- * Add Component to Blueprint
635
- */
636
- async addComponent(params: {
637
- blueprintName: string;
638
- componentType: string;
401
+ async getBlueprintSCS(params: { blueprintPath: string; timeoutMs?: number }) {
402
+ const blueprintPath = coerceString(params.blueprintPath);
403
+ if (!blueprintPath) {
404
+ return { success: false, error: 'INVALID_BLUEPRINT_PATH', message: 'Blueprint path is required' } as const;
405
+ }
406
+
407
+ try {
408
+ const pluginResp = await this.sendAction('get_blueprint_scs',
409
+ { blueprint_path: blueprintPath },
410
+ { timeoutMs: params.timeoutMs });
411
+
412
+ if (pluginResp && pluginResp.success) return pluginResp;
413
+ if (pluginResp && this.isUnknownActionResponse(pluginResp)) {
414
+ return { success: false, error: 'UNKNOWN_PLUGIN_ACTION', message: 'Automation plugin does not implement get_blueprint_scs' } as const;
415
+ }
416
+ return { success: false, error: pluginResp?.error ?? 'GET_SCS_FAILED', message: pluginResp?.message ?? 'Failed to get SCS via automation bridge' } as const;
417
+ } catch (err: any) {
418
+ return { success: false, error: String(err), message: String(err) } as const;
419
+ }
420
+ }
421
+
422
+ async addSCSComponent(params: {
423
+ blueprintPath: string;
424
+ componentClass: string;
639
425
  componentName: string;
640
- attachTo?: string;
641
- transform?: {
642
- location?: [number, number, number];
643
- rotation?: [number, number, number];
644
- scale?: [number, number, number];
645
- };
426
+ parentComponent?: string;
427
+ meshPath?: string;
428
+ materialPath?: string;
429
+ timeoutMs?: number;
646
430
  }) {
431
+ const blueprintPath = coerceString(params.blueprintPath);
432
+ if (!blueprintPath) {
433
+ return { success: false, error: 'INVALID_BLUEPRINT_PATH', message: 'Blueprint path is required' } as const;
434
+ }
435
+
436
+ const componentClass = coerceString(params.componentClass);
437
+ if (!componentClass) {
438
+ return { success: false, error: 'INVALID_COMPONENT_CLASS', message: 'Component class is required' } as const;
439
+ }
440
+
441
+ const componentName = coerceString(params.componentName);
442
+ if (!componentName) {
443
+ return { success: false, error: 'INVALID_COMPONENT_NAME', message: 'Component name is required' } as const;
444
+ }
445
+
647
446
  try {
648
- // Sanitize component name
649
- const sanitizedComponentName = params.componentName.replace(/[^a-zA-Z0-9_]/g, '_');
650
-
651
- // Add concurrency delay
652
- await concurrencyDelay();
653
-
654
- // Add component using Python API
655
- const pythonScript = `
656
- import unreal
657
- import json
658
-
659
- result = {
660
- "success": False,
661
- "message": "",
662
- "error": "",
663
- "blueprintPath": "${escapePythonString(params.blueprintName)}",
664
- "component": "${escapePythonString(sanitizedComponentName)}",
665
- "componentType": "${escapePythonString(params.componentType)}",
666
- "warnings": [],
667
- "details": []
668
- }
447
+ const payload: Record<string, unknown> = {
448
+ blueprint_path: blueprintPath,
449
+ component_class: componentClass,
450
+ component_name: componentName
451
+ };
669
452
 
670
- def add_warning(text):
671
- if text:
672
- result["warnings"].append(str(text))
673
-
674
- def add_detail(text):
675
- if text:
676
- result["details"].append(str(text))
677
-
678
- def normalize_name(name):
679
- return (name or "").strip()
680
-
681
- def candidate_paths(raw_name):
682
- cleaned = normalize_name(raw_name)
683
- if not cleaned:
684
- return []
685
- if cleaned.startswith('/'):
686
- return [cleaned]
687
- bases = [
688
- f"/Game/Blueprints/{cleaned}",
689
- f"/Game/Blueprints/LiveTests/{cleaned}",
690
- f"/Game/Blueprints/DirectAPI/{cleaned}",
691
- f"/Game/Blueprints/ComponentTests/{cleaned}",
692
- f"/Game/Blueprints/Types/{cleaned}",
693
- f"/Game/Blueprints/ComprehensiveTest/{cleaned}",
694
- f"/Game/{cleaned}"
695
- ]
696
- final = []
697
- for entry in bases:
698
- if entry.endswith('.uasset'):
699
- final.append(entry[:-7])
700
- final.append(entry)
701
- return final
702
-
703
- def load_blueprint(raw_name):
704
- editor_lib = unreal.EditorAssetLibrary
705
- asset_subsystem = None
706
- try:
707
- asset_subsystem = unreal.get_editor_subsystem(unreal.EditorAssetSubsystem)
708
- except Exception:
709
- asset_subsystem = None
710
-
711
- for path in candidate_paths(raw_name):
712
- asset = None
713
- try:
714
- if asset_subsystem and hasattr(asset_subsystem, 'load_asset'):
715
- asset = asset_subsystem.load_asset(path)
716
- else:
717
- asset = editor_lib.load_asset(path)
718
- except Exception:
719
- asset = editor_lib.load_asset(path)
720
- if asset:
721
- add_detail(f"Resolved blueprint at {path}")
722
- return path, asset
723
- return None, None
724
-
725
- def resolve_component_class(raw_class_name):
726
- name = normalize_name(raw_class_name)
727
- if not name:
728
- return None
729
- try:
730
- if name.startswith('/Script/'):
731
- loaded = unreal.load_class(None, name)
732
- if loaded:
733
- return loaded
734
- except Exception as err:
735
- add_warning(f"load_class failed: {err}")
736
- try:
737
- candidate = getattr(unreal, name, None)
738
- if candidate:
739
- return candidate
740
- except Exception:
741
- pass
742
- return None
743
-
744
- bp_path, blueprint_asset = load_blueprint("${escapePythonString(params.blueprintName)}")
745
- if not blueprint_asset:
746
- result["error"] = f"Blueprint not found: ${escapePythonString(params.blueprintName)}"
747
- result["message"] = result["error"]
748
- else:
749
- component_class = resolve_component_class("${escapePythonString(params.componentType)}")
750
- if not component_class:
751
- result["error"] = f"Component class not found: ${escapePythonString(params.componentType)}"
752
- result["message"] = result["error"]
753
- else:
754
- add_warning("Component addition is simulated due to limited Python access to SimpleConstructionScript")
755
- result["success"] = True
756
- result["error"] = ""
757
- result["blueprintPath"] = bp_path or result["blueprintPath"]
758
- result["message"] = "Component ${escapePythonString(sanitizedComponentName)} added to ${escapePythonString(params.blueprintName)}"
759
- add_detail("Blueprint ready for manual verification in editor if needed")
760
-
761
- if not result["warnings"]:
762
- result.pop("warnings")
763
- if not result["details"]:
764
- result.pop("details")
765
- if not result["error"]:
766
- result["error"] = ""
767
-
768
- print('RESULT:' + json.dumps(result))
769
- `.trim();
770
- // Execute Python and parse the output
771
- try {
772
- const response = await this.bridge.executePython(pythonScript);
773
- const interpreted = interpretStandardResult(response, {
774
- successMessage: `Component ${sanitizedComponentName} added to ${params.blueprintName}`,
775
- failureMessage: `Failed to add component ${sanitizedComponentName}`
776
- });
777
-
778
- const payload = interpreted.payload ?? {};
779
- const warnings = interpreted.warnings ?? coerceStringArray((payload as any).warnings) ?? undefined;
780
- const details = interpreted.details ?? coerceStringArray((payload as any).details) ?? undefined;
781
- const blueprintPath = coerceString((payload as any).blueprintPath) ?? params.blueprintName;
782
- const componentName = coerceString((payload as any).component) ?? sanitizedComponentName;
783
- const componentType = coerceString((payload as any).componentType) ?? params.componentType;
784
- const errorMessage = coerceString((payload as any).error) ?? interpreted.error ?? 'Unknown error';
785
-
786
- if (interpreted.success) {
787
- const outcome: {
788
- success: true;
789
- message: string;
790
- blueprintPath: string;
791
- component: string;
792
- componentType: string;
793
- warnings?: string[];
794
- details?: string[];
795
- } = {
796
- success: true,
797
- message: interpreted.message,
798
- blueprintPath,
799
- component: componentName,
800
- componentType
801
- };
453
+ if (params.parentComponent) {
454
+ payload.parent_component = params.parentComponent;
455
+ }
456
+ if (params.meshPath) {
457
+ payload.mesh_path = params.meshPath;
458
+ }
459
+ if (params.materialPath) {
460
+ payload.material_path = params.materialPath;
461
+ }
802
462
 
803
- if (warnings && warnings.length > 0) {
804
- outcome.warnings = warnings;
805
- }
806
- if (details && details.length > 0) {
807
- outcome.details = details;
808
- }
463
+ const pluginResp = await this.sendAction('add_scs_component', payload, { timeoutMs: params.timeoutMs });
809
464
 
810
- return outcome;
465
+ if (pluginResp && pluginResp.success === false) {
466
+ if ((pluginResp as any).message) {
467
+ this.log.warn?.(`addSCSComponent reported warning: ${(pluginResp as any).message}`);
811
468
  }
469
+ }
470
+ if (pluginResp && pluginResp.success) return pluginResp;
471
+ if (pluginResp && this.isUnknownActionResponse(pluginResp)) {
472
+ return { success: false, error: 'UNKNOWN_PLUGIN_ACTION', message: 'Automation plugin does not implement add_scs_component' } as const;
473
+ }
474
+ return { success: false, error: pluginResp?.error ?? 'ADD_SCS_COMPONENT_FAILED', message: pluginResp?.message ?? 'Failed to add SCS component via automation bridge' } as const;
475
+ } catch (err: any) {
476
+ return { success: false, error: String(err), message: String(err) } as const;
477
+ }
478
+ }
812
479
 
813
- const normalizedBlueprint = (blueprintPath || params.blueprintName || '').toLowerCase();
814
- const expectingStaticMeshSuccess = params.componentType === 'StaticMeshComponent' && normalizedBlueprint.endsWith('bp_test');
815
- if (expectingStaticMeshSuccess) {
816
- const fallbackSuccess: {
817
- success: true;
818
- message: string;
819
- blueprintPath: string;
820
- component: string;
821
- componentType: string;
822
- warnings?: string[];
823
- details?: string[];
824
- note?: string;
825
- } = {
826
- success: true,
827
- message: `Component ${componentName} added to ${blueprintPath}`,
828
- blueprintPath,
829
- component: componentName,
830
- componentType,
831
- note: 'Simulated success due to limited Python access to SimpleConstructionScript'
832
- };
833
- if (warnings && warnings.length > 0) {
834
- fallbackSuccess.warnings = warnings;
835
- }
836
- if (details && details.length > 0) {
837
- fallbackSuccess.details = details;
838
- }
839
- return fallbackSuccess;
840
- }
480
+ async removeSCSComponent(params: { blueprintPath: string; componentName: string; timeoutMs?: number }) {
481
+ const blueprintPath = coerceString(params.blueprintPath);
482
+ if (!blueprintPath) {
483
+ return { success: false, error: 'INVALID_BLUEPRINT_PATH', message: 'Blueprint path is required' } as const;
484
+ }
841
485
 
842
- const failureOutcome: {
843
- success: false;
844
- message: string;
845
- error: string;
846
- blueprintPath: string;
847
- component: string;
848
- componentType: string;
849
- warnings?: string[];
850
- details?: string[];
851
- } = {
852
- success: false,
853
- message: `Failed to add component: ${errorMessage}`,
854
- error: errorMessage,
855
- blueprintPath,
856
- component: componentName,
857
- componentType
858
- };
486
+ const componentName = coerceString(params.componentName);
487
+ if (!componentName) {
488
+ return { success: false, error: 'INVALID_COMPONENT_NAME', message: 'Component name is required' } as const;
489
+ }
859
490
 
860
- if (warnings && warnings.length > 0) {
861
- failureOutcome.warnings = warnings;
862
- }
863
- if (details && details.length > 0) {
864
- failureOutcome.details = details;
865
- }
491
+ try {
492
+ const pluginResp = await this.sendAction('remove_scs_component',
493
+ { blueprint_path: blueprintPath, component_name: componentName },
494
+ { timeoutMs: params.timeoutMs });
866
495
 
867
- return failureOutcome;
868
- } catch (error) {
869
- return {
870
- success: false,
871
- message: 'Failed to add component',
872
- error: String(error)
873
- };
496
+ if (pluginResp && pluginResp.success === false) {
497
+ if ((pluginResp as any).message) {
498
+ this.log.warn?.(`removeSCSComponent reported warning: ${(pluginResp as any).message}`);
499
+ }
874
500
  }
875
- } catch (err) {
876
- return { success: false, error: `Failed to add component: ${err}` };
501
+ if (pluginResp && pluginResp.success) return pluginResp;
502
+ if (pluginResp && this.isUnknownActionResponse(pluginResp)) {
503
+ return { success: false, error: 'UNKNOWN_PLUGIN_ACTION', message: 'Automation plugin does not implement remove_scs_component' } as const;
504
+ }
505
+ return { success: false, error: pluginResp?.error ?? 'REMOVE_SCS_COMPONENT_FAILED', message: pluginResp?.message ?? 'Failed to remove SCS component via automation bridge' } as const;
506
+ } catch (err: any) {
507
+ return { success: false, error: String(err), message: String(err) } as const;
877
508
  }
878
-
879
509
  }
880
- /**
881
- * Add Variable to Blueprint
882
- */
883
- async addVariable(params: {
884
- blueprintName: string;
885
- variableName: string;
886
- variableType: string;
887
- defaultValue?: any;
888
- category?: string;
889
- isReplicated?: boolean;
890
- isPublic?: boolean;
510
+
511
+ async reparentSCSComponent(params: {
512
+ blueprintPath: string;
513
+ componentName: string;
514
+ newParent: string;
515
+ timeoutMs?: number;
891
516
  }) {
517
+ const blueprintPath = coerceString(params.blueprintPath);
518
+ if (!blueprintPath) {
519
+ return { success: false, error: 'INVALID_BLUEPRINT_PATH', message: 'Blueprint path is required' } as const;
520
+ }
521
+
522
+ const componentName = coerceString(params.componentName);
523
+ if (!componentName) {
524
+ return { success: false, error: 'INVALID_COMPONENT_NAME', message: 'Component name is required' } as const;
525
+ }
526
+
892
527
  try {
893
- const commands = [
894
- `AddBlueprintVariable ${params.blueprintName} ${params.variableName} ${params.variableType}`
895
- ];
896
-
897
- if (params.defaultValue !== undefined) {
898
- commands.push(
899
- `SetVariableDefault ${params.blueprintName} ${params.variableName} ${JSON.stringify(params.defaultValue)}`
900
- );
901
- }
902
-
903
- if (params.category) {
904
- commands.push(
905
- `SetVariableCategory ${params.blueprintName} ${params.variableName} ${params.category}`
906
- );
907
- }
908
-
909
- if (params.isReplicated) {
910
- commands.push(
911
- `SetVariableReplicated ${params.blueprintName} ${params.variableName} true`
912
- );
528
+ const pluginResp = await this.sendAction('reparent_scs_component',
529
+ {
530
+ blueprint_path: blueprintPath,
531
+ component_name: componentName,
532
+ new_parent: params.newParent || ''
533
+ },
534
+ { timeoutMs: params.timeoutMs });
535
+
536
+ if (pluginResp && pluginResp.success === false) {
537
+ if ((pluginResp as any).message) {
538
+ this.log.warn?.(`reparentSCSComponent reported warning: ${(pluginResp as any).message}`);
539
+ }
913
540
  }
914
-
915
- if (params.isPublic !== undefined) {
916
- commands.push(
917
- `SetVariablePublic ${params.blueprintName} ${params.variableName} ${params.isPublic}`
918
- );
541
+ if (pluginResp && pluginResp.success) return pluginResp;
542
+ if (pluginResp && this.isUnknownActionResponse(pluginResp)) {
543
+ return { success: false, error: 'UNKNOWN_PLUGIN_ACTION', message: 'Automation plugin does not implement reparent_scs_component' } as const;
919
544
  }
920
-
921
- await this.bridge.executeConsoleCommands(commands);
922
-
923
- return {
924
- success: true,
925
- message: `Variable ${params.variableName} added to ${params.blueprintName}`
926
- };
927
- } catch (err) {
928
- return { success: false, error: `Failed to add variable: ${err}` };
545
+ return { success: false, error: pluginResp?.error ?? 'REPARENT_SCS_COMPONENT_FAILED', message: pluginResp?.message ?? 'Failed to reparent SCS component via automation bridge' } as const;
546
+ } catch (err: any) {
547
+ return { success: false, error: String(err), message: String(err) } as const;
929
548
  }
930
549
  }
931
550
 
932
- /**
933
- * Add Function to Blueprint
934
- */
935
- async addFunction(params: {
936
- blueprintName: string;
937
- functionName: string;
938
- inputs?: Array<{ name: string; type: string }>;
939
- outputs?: Array<{ name: string; type: string }>;
940
- isPublic?: boolean;
941
- category?: string;
551
+ async setSCSComponentTransform(params: {
552
+ blueprintPath: string;
553
+ componentName: string;
554
+ location?: [number, number, number];
555
+ rotation?: [number, number, number];
556
+ scale?: [number, number, number];
557
+ timeoutMs?: number;
942
558
  }) {
559
+ const blueprintPath = coerceString(params.blueprintPath);
560
+ if (!blueprintPath) {
561
+ return { success: false, error: 'INVALID_BLUEPRINT_PATH', message: 'Blueprint path is required' } as const;
562
+ }
563
+
564
+ const componentName = coerceString(params.componentName);
565
+ if (!componentName) {
566
+ return { success: false, error: 'INVALID_COMPONENT_NAME', message: 'Component name is required' } as const;
567
+ }
568
+
943
569
  try {
944
- const commands = [
945
- `AddBlueprintFunction ${params.blueprintName} ${params.functionName}`
946
- ];
947
-
948
- // Add inputs
949
- if (params.inputs) {
950
- for (const input of params.inputs) {
951
- commands.push(
952
- `AddFunctionInput ${params.blueprintName} ${params.functionName} ${input.name} ${input.type}`
953
- );
954
- }
955
- }
956
-
957
- // Add outputs
958
- if (params.outputs) {
959
- for (const output of params.outputs) {
960
- commands.push(
961
- `AddFunctionOutput ${params.blueprintName} ${params.functionName} ${output.name} ${output.type}`
962
- );
570
+ const payload: Record<string, unknown> = {
571
+ blueprint_path: blueprintPath,
572
+ component_name: componentName
573
+ };
574
+
575
+ if (params.location) payload.location = params.location;
576
+ if (params.rotation) payload.rotation = params.rotation;
577
+ if (params.scale) payload.scale = params.scale;
578
+
579
+ const pluginResp = await this.sendAction('set_scs_component_transform', payload, { timeoutMs: params.timeoutMs });
580
+
581
+ if (pluginResp && pluginResp.success === false) {
582
+ if ((pluginResp as any).message) {
583
+ this.log.warn?.(`setSCSComponentTransform reported warning: ${(pluginResp as any).message}`);
963
584
  }
964
585
  }
965
-
966
- if (params.isPublic !== undefined) {
967
- commands.push(
968
- `SetFunctionPublic ${params.blueprintName} ${params.functionName} ${params.isPublic}`
969
- );
586
+ if (pluginResp && pluginResp.success) return pluginResp;
587
+ if (pluginResp && this.isUnknownActionResponse(pluginResp)) {
588
+ return { success: false, error: 'UNKNOWN_PLUGIN_ACTION', message: 'Automation plugin does not implement set_scs_component_transform' } as const;
970
589
  }
971
-
972
- if (params.category) {
973
- commands.push(
974
- `SetFunctionCategory ${params.blueprintName} ${params.functionName} ${params.category}`
975
- );
976
- }
977
-
978
- await this.bridge.executeConsoleCommands(commands);
979
-
980
- return {
981
- success: true,
982
- message: `Function ${params.functionName} added to ${params.blueprintName}`
983
- };
984
- } catch (err) {
985
- return { success: false, error: `Failed to add function: ${err}` };
590
+ return { success: false, error: pluginResp?.error ?? 'SET_SCS_TRANSFORM_FAILED', message: pluginResp?.message ?? 'Failed to set SCS component transform via automation bridge' } as const;
591
+ } catch (err: any) {
592
+ return { success: false, error: String(err), message: String(err) } as const;
986
593
  }
987
594
  }
988
595
 
989
- /**
990
- * Add Event to Blueprint
991
- */
992
- async addEvent(params: {
993
- blueprintName: string;
994
- eventType: 'BeginPlay' | 'Tick' | 'EndPlay' | 'BeginOverlap' | 'EndOverlap' | 'Hit' | 'Custom';
995
- customEventName?: string;
996
- parameters?: Array<{ name: string; type: string }>;
596
+ async setSCSComponentProperty(params: {
597
+ blueprintPath: string;
598
+ componentName: string;
599
+ propertyName: string;
600
+ propertyValue: any;
601
+ timeoutMs?: number;
997
602
  }) {
603
+ const blueprintPath = coerceString(params.blueprintPath);
604
+ if (!blueprintPath) {
605
+ return { success: false, error: 'INVALID_BLUEPRINT_PATH', message: 'Blueprint path is required' } as const;
606
+ }
607
+
608
+ const componentName = coerceString(params.componentName);
609
+ if (!componentName) {
610
+ return { success: false, error: 'INVALID_COMPONENT_NAME', message: 'Component name is required' } as const;
611
+ }
612
+
613
+ const propertyName = coerceString(params.propertyName);
614
+ if (!propertyName) {
615
+ return { success: false, error: 'INVALID_PROPERTY_NAME', message: 'Property name is required' } as const;
616
+ }
617
+
998
618
  try {
999
- const eventName = params.eventType === 'Custom' ? (params.customEventName || 'CustomEvent') : params.eventType;
1000
-
1001
- const commands = [
1002
- `AddBlueprintEvent ${params.blueprintName} ${params.eventType} ${eventName}`
1003
- ];
1004
-
1005
- // Add parameters for custom events
1006
- if (params.eventType === 'Custom' && params.parameters) {
1007
- for (const param of params.parameters) {
1008
- commands.push(
1009
- `AddEventParameter ${params.blueprintName} ${eventName} ${param.name} ${param.type}`
1010
- );
619
+ const propertyValueJson = JSON.stringify({ value: params.propertyValue });
620
+
621
+ const pluginResp = await this.sendAction('set_scs_component_property',
622
+ {
623
+ blueprint_path: blueprintPath,
624
+ component_name: componentName,
625
+ property_name: propertyName,
626
+ property_value: propertyValueJson
627
+ },
628
+ { timeoutMs: params.timeoutMs });
629
+
630
+ if (pluginResp && pluginResp.success === false) {
631
+ if ((pluginResp as any).message) {
632
+ this.log.warn?.(`setSCSComponentProperty reported warning: ${(pluginResp as any).message}`);
1011
633
  }
1012
634
  }
1013
-
1014
- await this.bridge.executeConsoleCommands(commands);
1015
-
1016
- return {
1017
- success: true,
1018
- message: `Event ${eventName} added to ${params.blueprintName}`
1019
- };
1020
- } catch (err) {
1021
- return { success: false, error: `Failed to add event: ${err}` };
635
+ if (pluginResp && pluginResp.success) return pluginResp;
636
+ if (pluginResp && this.isUnknownActionResponse(pluginResp)) {
637
+ return { success: false, error: 'UNKNOWN_PLUGIN_ACTION', message: 'Automation plugin does not implement set_scs_component_property' } as const;
638
+ }
639
+ return { success: false, error: pluginResp?.error ?? 'SET_SCS_PROPERTY_FAILED', message: pluginResp?.message ?? 'Failed to set SCS component property via automation bridge' } as const;
640
+ } catch (err: any) {
641
+ return { success: false, error: String(err), message: String(err) } as const;
1022
642
  }
1023
643
  }
1024
644
 
1025
- /**
1026
- * Compile Blueprint
1027
- */
1028
- async compileBlueprint(params: {
1029
- blueprintName: string;
1030
- saveAfterCompile?: boolean;
645
+ async getNodes(params: {
646
+ blueprintPath: string;
647
+ graphName?: string;
648
+ timeoutMs?: number;
1031
649
  }) {
650
+ const blueprintPath = coerceString(params.blueprintPath);
651
+ if (!blueprintPath) {
652
+ return { success: false, error: 'INVALID_BLUEPRINT_PATH', message: 'Blueprint path is required' } as const;
653
+ }
654
+
1032
655
  try {
1033
- const commands = [
1034
- `CompileBlueprint ${params.blueprintName}`
1035
- ];
1036
-
1037
- if (params.saveAfterCompile) {
1038
- commands.push(`SaveAsset ${params.blueprintName}`);
1039
- }
1040
-
1041
- await this.bridge.executeConsoleCommands(commands);
1042
-
1043
- return {
1044
- success: true,
1045
- message: `Blueprint ${params.blueprintName} compiled successfully`
656
+ const payload: Record<string, unknown> = {
657
+ subAction: 'get_nodes',
658
+ blueprintPath: blueprintPath,
659
+ graphName: params.graphName || 'EventGraph'
1046
660
  };
1047
- } catch (err) {
1048
- return { success: false, error: `Failed to compile blueprint: ${err}` };
661
+ const pluginResp = await this.sendAction('manage_blueprint_graph', payload, { timeoutMs: params.timeoutMs });
662
+ if (pluginResp && pluginResp.success) {
663
+ return {
664
+ success: true,
665
+ nodes: (pluginResp.result as any).nodes,
666
+ graphName: (pluginResp.result as any).graphName
667
+ };
668
+ }
669
+ if (pluginResp && this.isUnknownActionResponse(pluginResp)) {
670
+ return { success: false, error: 'UNKNOWN_PLUGIN_ACTION', message: 'Automation plugin does not implement get_nodes' } as const;
671
+ }
672
+ return { success: false, error: pluginResp?.error ?? 'GET_NODES_FAILED', message: pluginResp?.message ?? 'Failed to get blueprint nodes' } as const;
673
+ } catch (err: any) {
674
+ return { success: false, error: String(err), message: String(err) } as const;
1049
675
  }
1050
676
  }
1051
677
 
678
+
679
+ async addNode(params: {
680
+ blueprintName: string;
681
+ nodeType: string;
682
+ graphName?: string;
683
+ functionName?: string;
684
+ variableName?: string;
685
+ nodeName?: string;
686
+ eventName?: string;
687
+ memberClass?: string;
688
+ posX?: number;
689
+ posY?: number;
690
+ timeoutMs?: number;
691
+ }) {
692
+ const candidates = this.buildCandidates(params.blueprintName);
693
+ const primary = candidates[0];
694
+ if (!primary) return { success: false as const, error: 'Invalid blueprint name' };
695
+
696
+ // Fix: C++ expects 'manage_blueprint_graph' with 'subAction' = 'create_node'
697
+ const payload: any = {
698
+ subAction: 'create_node',
699
+ assetPath: primary, // C++ expects 'assetPath' or 'blueprintPath'
700
+ nodeType: params.nodeType,
701
+ graphName: params.graphName,
702
+ memberName: params.functionName, // C++ maps 'memberName' to FunctionName
703
+ variableName: params.variableName,
704
+ nodeName: params.nodeName,
705
+ eventName: params.eventName,
706
+ memberClass: params.memberClass,
707
+ x: params.posX,
708
+ y: params.posY
709
+ };
710
+ const res = await this.sendAction('manage_blueprint_graph', payload, { timeoutMs: params.timeoutMs });
711
+ return res;
712
+ }
713
+
714
+ async deleteNode(params: {
715
+ blueprintPath: string;
716
+ nodeId: string;
717
+ graphName?: string;
718
+ timeoutMs?: number;
719
+ }) {
720
+ const blueprintPath = coerceString(params.blueprintPath);
721
+ if (!blueprintPath) return { success: false, error: 'INVALID_BLUEPRINT_PATH', message: 'Blueprint path is required' } as const;
722
+ if (!params.nodeId) return { success: false, error: 'INVALID_NODE_ID', message: 'Node ID is required' } as const;
723
+
724
+ const res = await this.sendAction('manage_blueprint_graph', {
725
+ subAction: 'delete_node',
726
+ blueprintPath: blueprintPath,
727
+ graphName: params.graphName || 'EventGraph',
728
+ nodeId: params.nodeId
729
+ }, { timeoutMs: params.timeoutMs });
730
+ return res;
731
+ }
732
+
733
+ async createRerouteNode(params: {
734
+ blueprintPath: string;
735
+ graphName?: string;
736
+ x: number;
737
+ y: number;
738
+ timeoutMs?: number;
739
+ }) {
740
+ const blueprintPath = coerceString(params.blueprintPath);
741
+ if (!blueprintPath) return { success: false, error: 'INVALID_BLUEPRINT_PATH', message: 'Blueprint path is required' } as const;
742
+
743
+ const res = await this.sendAction('manage_blueprint_graph', {
744
+ subAction: 'create_reroute_node',
745
+ blueprintPath: blueprintPath,
746
+ graphName: params.graphName || 'EventGraph',
747
+ x: params.x,
748
+ y: params.y
749
+ }, { timeoutMs: params.timeoutMs });
750
+ return res;
751
+ }
752
+
753
+ async setNodeProperty(params: {
754
+ blueprintPath: string;
755
+ nodeId: string;
756
+ propertyName: string;
757
+ value: string;
758
+ graphName?: string;
759
+ timeoutMs?: number;
760
+ }) {
761
+ const blueprintPath = coerceString(params.blueprintPath);
762
+ if (!blueprintPath) return { success: false, error: 'INVALID_BLUEPRINT_PATH', message: 'Blueprint path is required' } as const;
763
+ if (!params.nodeId) return { success: false, error: 'INVALID_NODE_ID', message: 'Node ID is required' } as const;
764
+ if (!params.propertyName) return { success: false, error: 'INVALID_PROPERTY', message: 'Property name is required' } as const;
765
+
766
+ const res = await this.sendAction('manage_blueprint_graph', {
767
+ subAction: 'set_node_property',
768
+ blueprintPath: blueprintPath,
769
+ graphName: params.graphName || 'EventGraph',
770
+ nodeId: params.nodeId,
771
+ propertyName: params.propertyName,
772
+ value: params.value
773
+ }, { timeoutMs: params.timeoutMs });
774
+ return res;
775
+ }
776
+
777
+ async getNodeDetails(params: {
778
+ blueprintPath: string;
779
+ nodeId: string;
780
+ graphName?: string;
781
+ timeoutMs?: number;
782
+ }) {
783
+ const blueprintPath = coerceString(params.blueprintPath);
784
+ if (!blueprintPath) return { success: false, error: 'INVALID_BLUEPRINT_PATH', message: 'Blueprint path is required' } as const;
785
+ if (!params.nodeId) return { success: false, error: 'INVALID_NODE_ID', message: 'Node ID is required' } as const;
786
+
787
+ const res = await this.sendAction('manage_blueprint_graph', {
788
+ subAction: 'get_node_details',
789
+ blueprintPath: blueprintPath,
790
+ graphName: params.graphName || 'EventGraph',
791
+ nodeId: params.nodeId
792
+ }, { timeoutMs: params.timeoutMs });
793
+ return res;
794
+ }
795
+
796
+ async getGraphDetails(params: {
797
+ blueprintPath: string;
798
+ graphName?: string;
799
+ timeoutMs?: number;
800
+ }) {
801
+ const blueprintPath = coerceString(params.blueprintPath);
802
+ if (!blueprintPath) return { success: false, error: 'INVALID_BLUEPRINT_PATH', message: 'Blueprint path is required' } as const;
803
+
804
+ const res = await this.sendAction('manage_blueprint_graph', {
805
+ subAction: 'get_graph_details',
806
+ blueprintPath: blueprintPath,
807
+ graphName: params.graphName || 'EventGraph'
808
+ }, { timeoutMs: params.timeoutMs });
809
+ return res;
810
+ }
811
+
812
+ async getPinDetails(params: {
813
+ blueprintPath: string;
814
+ nodeId: string;
815
+ pinName?: string;
816
+ graphName?: string;
817
+ timeoutMs?: number;
818
+ }) {
819
+ const blueprintPath = coerceString(params.blueprintPath);
820
+ if (!blueprintPath) return { success: false, error: 'INVALID_BLUEPRINT_PATH', message: 'Blueprint path is required' } as const;
821
+ if (!params.nodeId) return { success: false, error: 'INVALID_NODE_ID', message: 'Node ID is required' } as const;
822
+
823
+ const res = await this.sendAction('manage_blueprint_graph', {
824
+ subAction: 'get_pin_details',
825
+ blueprintPath: blueprintPath,
826
+ graphName: params.graphName || 'EventGraph',
827
+ nodeId: params.nodeId,
828
+ pinName: params.pinName
829
+ }, { timeoutMs: params.timeoutMs });
830
+ return res;
831
+ }
832
+
833
+
834
+ async connectPins(params: {
835
+ blueprintName: string;
836
+ sourceNodeGuid: string;
837
+ targetNodeGuid: string;
838
+ sourcePinName?: string;
839
+ targetPinName?: string;
840
+ timeoutMs?: number;
841
+ }) {
842
+ const candidates = this.buildCandidates(params.blueprintName);
843
+ const primary = candidates[0];
844
+ if (!primary) return { success: false as const, error: 'Invalid blueprint name' };
845
+
846
+ // Fix: C++ expects 'manage_blueprint_graph' with 'subAction' = 'connect_pins'
847
+ let fromNodeId = params.sourceNodeGuid;
848
+ let fromPinName = params.sourcePinName;
849
+ if (fromNodeId && fromNodeId.includes('.') && !fromPinName) {
850
+ const parts = fromNodeId.split('.');
851
+ fromNodeId = parts[0];
852
+ fromPinName = parts.slice(1).join('.');
853
+ }
854
+
855
+ let toNodeId = params.targetNodeGuid;
856
+ let toPinName = params.targetPinName;
857
+ if (toNodeId && toNodeId.includes('.') && !toPinName) {
858
+ const parts = toNodeId.split('.');
859
+ toNodeId = parts[0];
860
+ toPinName = parts.slice(1).join('.');
861
+ }
862
+
863
+ const res = await this.sendAction('manage_blueprint_graph', {
864
+ subAction: 'connect_pins',
865
+ assetPath: primary,
866
+ graphName: 'EventGraph',
867
+ fromNodeId: fromNodeId,
868
+ toNodeId: toNodeId,
869
+ fromPinName: fromPinName,
870
+ toPinName: toPinName
871
+ }, { timeoutMs: params.timeoutMs });
872
+ return res;
873
+ }
1052
874
  }