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,334 +1,79 @@
1
- import WebSocket from 'ws';
2
- import { createHttpClient } from './utils/http.js';
3
1
  import { Logger } from './utils/logger.js';
4
- import { loadEnv } from './types/env.js';
5
2
  import { ErrorHandler } from './utils/error-handler.js';
6
-
7
- // RcMessage interface reserved for future WebSocket message handling
8
- // interface RcMessage {
9
- // MessageName: string;
10
- // Parameters?: any;
11
- // }
12
-
13
- interface RcCallBody {
14
- objectPath: string; // e.g. "/Script/UnrealEd.Default__EditorAssetLibrary"
15
- functionName: string; // e.g. "ListAssets"
16
- parameters?: Record<string, any>;
17
- generateTransaction?: boolean;
18
- }
19
-
20
- interface CommandQueueItem {
21
- command: () => Promise<any>;
22
- resolve: (value: any) => void;
23
- reject: (reason?: any) => void;
24
- priority: number;
25
- retryCount?: number;
26
- }
27
-
28
- interface PythonScriptTemplate {
29
- name: string;
30
- script: string;
31
- params?: Record<string, any>;
32
- }
3
+ import type { AutomationBridge } from './automation/index.js';
4
+ import { DEFAULT_AUTOMATION_HOST, DEFAULT_AUTOMATION_PORT } from './constants.js';
5
+ import { UnrealCommandQueue } from './utils/unreal-command-queue.js';
6
+ import { CommandValidator } from './utils/command-validator.js';
33
7
 
34
8
  export class UnrealBridge {
35
- private ws?: WebSocket;
36
- private http = createHttpClient('');
37
- private env = loadEnv();
38
9
  private log = new Logger('UnrealBridge');
39
10
  private connected = false;
40
- private reconnectTimer?: NodeJS.Timeout;
41
- private reconnectAttempts = 0;
42
- private readonly MAX_RECONNECT_ATTEMPTS = 5;
43
- private readonly BASE_RECONNECT_DELAY = 1000;
44
- private autoReconnectEnabled = false; // disabled by default to prevent looping retries
45
- private engineVersionCache?: { value: { version: string; major: number; minor: number; patch: number; isUE56OrAbove: boolean }; timestamp: number };
46
- private readonly ENGINE_VERSION_TTL_MS = 5 * 60 * 1000;
47
-
48
- // WebSocket health monitoring (best practice from WebSocket optimization guides)
49
- private lastPongReceived = 0;
50
- private pingInterval?: NodeJS.Timeout;
51
- private readonly PING_INTERVAL_MS = 30000; // 30 seconds
52
- private readonly PONG_TIMEOUT_MS = 10000; // 10 seconds
53
-
54
- // Command queue for throttling
55
- private commandQueue: CommandQueueItem[] = [];
56
- private isProcessing = false;
57
- private readonly MIN_COMMAND_DELAY = 100; // Increased to prevent console spam
58
- private readonly MAX_COMMAND_DELAY = 500; // Maximum delay for heavy operations
59
- private readonly STAT_COMMAND_DELAY = 300; // Special delay for stat commands to avoid warnings
60
- private lastCommandTime = 0;
61
- private lastStatCommandTime = 0; // Track stat commands separately
62
-
63
- // Console object cache to reduce FindConsoleObject warnings
64
- private consoleObjectCache = new Map<string, any>();
65
- private readonly CONSOLE_CACHE_TTL = 300000; // 5 minutes TTL for cached objects
66
- private pluginStatusCache = new Map<string, { enabled: boolean; timestamp: number }>();
67
- private readonly PLUGIN_CACHE_TTL = 5 * 60 * 1000;
68
-
69
- // Unsafe viewmodes that can cause crashes or instability via visualizeBuffer
70
- private readonly UNSAFE_VIEWMODES = [
71
- 'BaseColor', 'WorldNormal', 'Metallic', 'Specular',
72
- 'Roughness',
73
- 'SubsurfaceColor',
74
- 'Opacity',
75
- 'LightComplexity', 'LightmapDensity',
76
- 'StationaryLightOverlap', 'CollisionPawn', 'CollisionVisibility'
77
- ];
78
- private readonly HARD_BLOCKED_VIEWMODES = new Set([
79
- 'BaseColor', 'WorldNormal', 'Metallic', 'Specular', 'Roughness', 'SubsurfaceColor', 'Opacity'
80
- ]);
81
- private readonly VIEWMODE_ALIASES = new Map<string, string>([
82
- ['lit', 'Lit'],
83
- ['unlit', 'Unlit'],
84
- ['wireframe', 'Wireframe'],
85
- ['brushwireframe', 'BrushWireframe'],
86
- ['brush_wireframe', 'BrushWireframe'],
87
- ['detaillighting', 'DetailLighting'],
88
- ['detail_lighting', 'DetailLighting'],
89
- ['lightingonly', 'LightingOnly'],
90
- ['lighting_only', 'LightingOnly'],
91
- ['lightonly', 'LightingOnly'],
92
- ['light_only', 'LightingOnly'],
93
- ['lightcomplexity', 'LightComplexity'],
94
- ['light_complexity', 'LightComplexity'],
95
- ['shadercomplexity', 'ShaderComplexity'],
96
- ['shader_complexity', 'ShaderComplexity'],
97
- ['lightmapdensity', 'LightmapDensity'],
98
- ['lightmap_density', 'LightmapDensity'],
99
- ['stationarylightoverlap', 'StationaryLightOverlap'],
100
- ['stationary_light_overlap', 'StationaryLightOverlap'],
101
- ['reflectionoverride', 'ReflectionOverride'],
102
- ['reflection_override', 'ReflectionOverride'],
103
- ['texeldensity', 'TexelDensity'],
104
- ['texel_density', 'TexelDensity'],
105
- ['vertexcolor', 'VertexColor'],
106
- ['vertex_color', 'VertexColor'],
107
- ['litdetail', 'DetailLighting'],
108
- ['lit_only', 'LightingOnly']
109
- ]);
110
-
111
- // Python script templates for EditorLevelLibrary access
112
- private readonly PYTHON_TEMPLATES: Record<string, PythonScriptTemplate> = {
113
- GET_ALL_ACTORS: {
114
- name: 'get_all_actors',
115
- script: `
116
- import unreal
117
- import json
118
-
119
- # Use EditorActorSubsystem instead of deprecated EditorLevelLibrary
120
- try:
121
- subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
122
- if subsys:
123
- actors = subsys.get_all_level_actors()
124
- result = [{'name': a.get_name(), 'class': a.get_class().get_name(), 'path': a.get_path_name()} for a in actors]
125
- print(f"RESULT:{json.dumps(result)}")
126
- else:
127
- print("RESULT:[]")
128
- except Exception as e:
129
- print(f"RESULT:{json.dumps({'error': str(e)})}")
130
- `.trim()
131
- },
132
- SPAWN_ACTOR_AT_LOCATION: {
133
- name: 'spawn_actor',
134
- script: `
135
- import unreal
136
- import json
137
-
138
- location = unreal.Vector({x}, {y}, {z})
139
- rotation = unreal.Rotator({pitch}, {yaw}, {roll})
140
-
141
- try:
142
- # Use EditorActorSubsystem instead of deprecated EditorLevelLibrary
143
- subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
144
- if subsys:
145
- # Try to load asset class
146
- actor_class = unreal.EditorAssetLibrary.load_asset("{class_path}")
147
- if actor_class:
148
- spawned = subsys.spawn_actor_from_object(actor_class, location, rotation)
149
- if spawned:
150
- print(f"RESULT:{json.dumps({'success': True, 'actor': spawned.get_name(), 'location': [{x}, {y}, {z}]}})}")
151
- else:
152
- print(f"RESULT:{json.dumps({'success': False, 'error': 'Failed to spawn actor'})}")
153
- else:
154
- print(f"RESULT:{json.dumps({'success': False, 'error': 'Failed to load actor class: {class_path}'})}")
155
- else:
156
- print(f"RESULT:{json.dumps({'success': False, 'error': 'EditorActorSubsystem not available'})}")
157
- except Exception as e:
158
- print(f"RESULT:{json.dumps({'success': False, 'error': str(e)})}")
159
- `.trim()
160
- },
161
- DELETE_ACTOR: {
162
- name: 'delete_actor',
163
- script: `
164
- import unreal
165
- import json
166
-
167
- try:
168
- subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
169
- if subsys:
170
- actors = subsys.get_all_level_actors()
171
- found = False
172
- for actor in actors:
173
- if not actor:
174
- continue
175
- label = actor.get_actor_label()
176
- name = actor.get_name()
177
- if label == "{actor_name}" or name == "{actor_name}" or label.lower().startswith("{actor_name}".lower()+"_"):
178
- success = subsys.destroy_actor(actor)
179
- print(f"RESULT:{json.dumps({'success': success, 'deleted': label})}")
180
- found = True
181
- break
182
- if not found:
183
- print(f"RESULT:{json.dumps({'success': False, 'error': 'Actor not found: {actor_name}'})}")
184
- else:
185
- print(f"RESULT:{json.dumps({'success': False, 'error': 'EditorActorSubsystem not available'})}")
186
- except Exception as e:
187
- print(f"RESULT:{json.dumps({'success': False, 'error': str(e)})}")
188
- `.trim()
189
- },
190
- CREATE_ASSET: {
191
- name: 'create_asset',
192
- script: `
193
- import unreal
194
- import json
195
-
196
- try:
197
- asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
198
- if asset_tools:
199
- # Create factory based on asset type
200
- factory_class = getattr(unreal, '{factory_class}', None)
201
- asset_class = getattr(unreal, '{asset_class}', None)
202
-
203
- if factory_class and asset_class:
204
- factory = factory_class()
205
- # Clean up the path - remove trailing slashes and normalize
206
- package_path = "{package_path}".rstrip('/').replace('//', '/')
207
-
208
- # Ensure package path is valid (starts with /Game or /Engine)
209
- if not package_path.startswith('/Game') and not package_path.startswith('/Engine'):
210
- if not package_path.startswith('/'):
211
- package_path = f"/Game/{package_path}"
212
- else:
213
- package_path = f"/Game{package_path}"
214
-
215
- # Create full asset path for verification
216
- full_asset_path = f"{package_path}/{asset_name}" if package_path != "/Game" else f"/Game/{asset_name}"
217
-
218
- # Create the asset with cleaned path
219
- asset = asset_tools.create_asset("{asset_name}", package_path, asset_class, factory)
220
- if asset:
221
- # Save the asset
222
- saved = unreal.EditorAssetLibrary.save_asset(asset.get_path_name())
223
- # Enhanced verification with retry logic
224
- asset_path = asset.get_path_name()
225
- verification_attempts = 0
226
- max_verification_attempts = 5
227
- asset_verified = False
228
-
229
- while verification_attempts < max_verification_attempts and not asset_verified:
230
- verification_attempts += 1
231
- # Wait a bit for the asset to be fully saved
232
- import time
233
- time.sleep(0.1)
234
-
235
- # Check if asset exists
236
- asset_exists = unreal.EditorAssetLibrary.does_asset_exist(asset_path)
237
-
238
- if asset_exists:
239
- asset_verified = True
240
- elif verification_attempts < max_verification_attempts:
241
- # Try to reload the asset registry
242
- try:
243
- unreal.AssetRegistryHelpers.get_asset_registry().scan_modified_asset_files([asset_path])
244
- except:
245
- pass
246
-
247
- if asset_verified:
248
- print(f"RESULT:{json.dumps({'success': saved, 'path': asset_path, 'verified': True})}")
249
- else:
250
- print(f"RESULT:{json.dumps({'success': saved, 'path': asset_path, 'warning': 'Asset created but verification pending'})}")
251
- else:
252
- print(f"RESULT:{json.dumps({'success': False, 'error': 'Failed to create asset'})}")
253
- else:
254
- print(f"RESULT:{json.dumps({'success': False, 'error': 'Invalid factory or asset class'})}")
255
- else:
256
- print(f"RESULT:{json.dumps({'success': False, 'error': 'AssetToolsHelpers not available'})}")
257
- except Exception as e:
258
- print(f"RESULT:{json.dumps({'success': False, 'error': str(e)})}")
259
- `.trim()
260
- },
261
- SET_VIEWPORT_CAMERA: {
262
- name: 'set_viewport_camera',
263
- script: `
264
- import unreal
265
- import json
266
-
267
- location = unreal.Vector({x}, {y}, {z})
268
- rotation = unreal.Rotator({pitch}, {yaw}, {roll})
269
-
270
- try:
271
- # Use UnrealEditorSubsystem for viewport operations (UE5.1+)
272
- ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
273
- les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
274
-
275
- if ues:
276
- ues.set_level_viewport_camera_info(location, rotation)
277
- try:
278
- if les:
279
- les.editor_invalidate_viewports()
280
- except Exception:
281
- pass
282
- print(f"RESULT:{json.dumps({'success': True, 'location': [{x}, {y}, {z}], 'rotation': [{pitch}, {yaw}, {roll}]}})}")
283
- else:
284
- print(f"RESULT:{json.dumps({'success': False, 'error': 'UnrealEditorSubsystem not available'})}")
285
- except Exception as e:
286
- print(f"RESULT:{json.dumps({'success': False, 'error': str(e)})}")
287
- `.trim()
288
- },
289
- BUILD_LIGHTING: {
290
- name: 'build_lighting',
291
- script: `
292
- import unreal
293
- import json
294
-
295
- try:
296
- les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
297
- if les:
298
- # Use UE 5.6 enhanced lighting quality settings
299
- quality_map = {
300
- 'Preview': unreal.LightingBuildQuality.PREVIEW,
301
- 'Medium': unreal.LightingBuildQuality.MEDIUM,
302
- 'High': unreal.LightingBuildQuality.HIGH,
303
- 'Production': unreal.LightingBuildQuality.PRODUCTION
304
- }
305
- q = quality_map.get('{quality}', unreal.LightingBuildQuality.PREVIEW)
306
- les.build_light_maps(q, True)
307
- print(f"RESULT:{json.dumps({'success': True, 'quality': '{quality}', 'method': 'LevelEditorSubsystem'})}")
308
- else:
309
- print(f"RESULT:{json.dumps({'success': False, 'error': 'LevelEditorSubsystem not available'})}")
310
- except Exception as e:
311
- print(f"RESULT:{json.dumps({'success': False, 'error': str(e)})}")
312
- `.trim()
313
- },
314
- SAVE_ALL_DIRTY_PACKAGES: {
315
- name: 'save_dirty_packages',
316
- script: `
317
- import unreal
318
- import json
319
-
320
- try:
321
- # Use UE 5.6 enhanced saving with better error handling
322
- saved = unreal.EditorLoadingAndSavingUtils.save_dirty_packages(True, True)
323
- print(f"RESULT:{json.dumps({'success': bool(saved), 'saved_count': saved if isinstance(saved, int) else 0, 'message': 'All dirty packages saved'})}")
324
- except Exception as e:
325
- print(f"RESULT:{json.dumps({'success': False, 'error': str(e), 'message': 'Failed to save dirty packages'})}")
326
- `.trim()
327
- }
11
+ private automationBridge?: AutomationBridge;
12
+ private automationBridgeListeners?: {
13
+ connected: (info: any) => void;
14
+ disconnected: (info: any) => void;
15
+ handshakeFailed: (info: any) => void;
328
16
  };
329
17
 
18
+ // Command queue for throttling
19
+ private commandQueue = new UnrealCommandQueue();
20
+
330
21
  get isConnected() { return this.connected; }
331
-
22
+
23
+ setAutomationBridge(automationBridge?: AutomationBridge): void {
24
+ if (this.automationBridge && this.automationBridgeListeners) {
25
+ this.automationBridge.off('connected', this.automationBridgeListeners.connected);
26
+ this.automationBridge.off('disconnected', this.automationBridgeListeners.disconnected);
27
+ this.automationBridge.off('handshakeFailed', this.automationBridgeListeners.handshakeFailed);
28
+ }
29
+
30
+ this.automationBridge = automationBridge;
31
+ this.automationBridgeListeners = undefined;
32
+
33
+ if (!automationBridge) {
34
+ this.connected = false;
35
+ return;
36
+ }
37
+
38
+ const onConnected = (info: any) => {
39
+ this.connected = true;
40
+ this.log.debug('Automation bridge connected', info);
41
+ };
42
+
43
+ const onDisconnected = (info: any) => {
44
+ this.connected = false;
45
+ this.log.debug('Automation bridge disconnected', info);
46
+ };
47
+
48
+ const onHandshakeFailed = (info: any) => {
49
+ this.connected = false;
50
+ this.log.warn('Automation bridge handshake failed', info);
51
+ };
52
+
53
+ automationBridge.on('connected', onConnected);
54
+ automationBridge.on('disconnected', onDisconnected);
55
+ automationBridge.on('handshakeFailed', onHandshakeFailed);
56
+
57
+ this.automationBridgeListeners = {
58
+ connected: onConnected,
59
+ disconnected: onDisconnected,
60
+ handshakeFailed: onHandshakeFailed
61
+ };
62
+
63
+ this.connected = automationBridge.isConnected();
64
+ }
65
+
66
+ /**
67
+ * Get the automation bridge instance safely.
68
+ * Throws if not configured, but does not check connection status (use isConnected for that).
69
+ */
70
+ getAutomationBridge(): AutomationBridge {
71
+ if (!this.automationBridge) {
72
+ throw new Error('Automation bridge is not configured');
73
+ }
74
+ return this.automationBridge;
75
+ }
76
+
332
77
  /**
333
78
  * Attempt to connect with exponential backoff retry strategy
334
79
  * Uses optimized retry pattern from TypeScript best practices
@@ -339,36 +84,54 @@ except Exception as e:
339
84
  */
340
85
  private connectPromise?: Promise<void>;
341
86
 
342
- async tryConnect(maxAttempts: number = 3, timeoutMs: number = 5000, retryDelayMs: number = 2000): Promise<boolean> {
343
- if (this.connected) return true;
87
+ async tryConnect(maxAttempts: number = 3, timeoutMs: number = 15000, retryDelayMs: number = 3000): Promise<boolean> {
88
+ if (process.env.MOCK_UNREAL_CONNECTION === 'true') {
89
+ this.log.info('🔌 MOCK MODE: Simulating active connection');
90
+ this.connected = true;
91
+ return true;
92
+ }
93
+
94
+ if (this.connected && this.automationBridge?.isConnected()) {
95
+ return true;
96
+ }
97
+
98
+ if (!this.automationBridge) {
99
+ this.log.warn('Automation bridge is not configured; cannot establish connection.');
100
+ return false;
101
+ }
102
+
103
+ if (this.automationBridge.isConnected()) {
104
+ this.connected = true;
105
+ return true;
106
+ }
344
107
 
345
108
  if (this.connectPromise) {
346
109
  try {
347
110
  await this.connectPromise;
348
- } catch {
349
- // swallow, we'll return connected flag
350
- }
111
+ } catch { }
351
112
  return this.connected;
352
113
  }
353
114
 
354
- // Use ErrorHandler's retryWithBackoff for consistent retry behavior
355
115
  this.connectPromise = ErrorHandler.retryWithBackoff(
356
- () => this.connect(timeoutMs),
116
+ () => {
117
+ const envTimeout = process.env.UNREAL_CONNECTION_TIMEOUT ? parseInt(process.env.UNREAL_CONNECTION_TIMEOUT, 10) : 30000;
118
+ const actualTimeout = envTimeout > 0 ? envTimeout : timeoutMs;
119
+ return this.connect(actualTimeout);
120
+ },
357
121
  {
358
- maxRetries: maxAttempts - 1,
122
+ maxRetries: Math.max(0, maxAttempts - 1),
359
123
  initialDelay: retryDelayMs,
360
124
  maxDelay: 10000,
361
125
  backoffMultiplier: 1.5,
362
- shouldRetry: (error) => {
363
- // Only retry on connection-related errors
126
+ shouldRetry: (error: any) => {
364
127
  const msg = (error as Error)?.message?.toLowerCase() || '';
365
- return msg.includes('timeout') || msg.includes('connection') || msg.includes('econnrefused');
128
+ return msg.includes('timeout') || msg.includes('connect') || msg.includes('automation');
366
129
  }
367
130
  }
368
- ).then(() => {
369
- // Success
370
- }).catch((err) => {
371
- this.log.warn(`Connection failed after ${maxAttempts} attempts:`, err.message);
131
+ ).catch((err: any) => {
132
+ this.log.warn(`Automation bridge connection failed after ${maxAttempts} attempts:`, err.message);
133
+ this.log.warn('⚠️ Ensure Unreal Editor is running with MCP Automation Bridge plugin enabled');
134
+ this.log.warn(`⚠️ Plugin should listen on ws://${DEFAULT_AUTOMATION_HOST}:${DEFAULT_AUTOMATION_PORT} for MCP server connections`);
372
135
  });
373
136
 
374
137
  try {
@@ -377,553 +140,351 @@ except Exception as e:
377
140
  this.connectPromise = undefined;
378
141
  }
379
142
 
143
+ this.connected = this.automationBridge?.isConnected() ?? false;
380
144
  return this.connected;
381
145
  }
382
146
 
383
- async connect(timeoutMs: number = 5000): Promise<void> {
384
- // If already connected and socket is open, do nothing
385
- if (this.connected && this.ws && this.ws.readyState === WebSocket.OPEN) {
386
- this.log.debug('connect() called but already connected; skipping');
147
+ async connect(timeoutMs: number = 15000): Promise<void> {
148
+ const automationBridge = this.automationBridge;
149
+ if (!automationBridge) {
150
+ throw new Error('Automation bridge not configured');
151
+ }
152
+
153
+ if (automationBridge.isConnected()) {
154
+ this.connected = true;
387
155
  return;
388
156
  }
389
157
 
390
- const wsUrl = `ws://${this.env.UE_HOST}:${this.env.UE_RC_WS_PORT}`;
391
- const httpBase = `http://${this.env.UE_HOST}:${this.env.UE_RC_HTTP_PORT}`;
392
- this.http = createHttpClient(httpBase);
158
+ // Start the bridge connection if it's not active
159
+ // This supports lazy connection where the bridge doesn't start until a tool is used
160
+ automationBridge.start();
393
161
 
394
- this.log.debug(`Connecting to UE Remote Control: ${wsUrl}`);
395
- this.ws = new WebSocket(wsUrl);
162
+ const success = await this.waitForAutomationConnection(timeoutMs);
163
+ if (!success) {
164
+ throw new Error('Automation bridge connection timeout');
165
+ }
166
+
167
+ this.connected = true;
168
+ }
169
+
170
+ private async waitForAutomationConnection(timeoutMs: number): Promise<boolean> {
171
+ const automationBridge = this.automationBridge;
172
+ if (!automationBridge) {
173
+ return false;
174
+ }
175
+
176
+ if (automationBridge.isConnected()) {
177
+ return true;
178
+ }
396
179
 
397
- await new Promise<void>((resolve, reject) => {
398
- if (!this.ws) return reject(new Error('WS not created'));
399
-
400
- // Guard against double-resolution/rejection
180
+ return new Promise<boolean>((resolve) => {
401
181
  let settled = false;
402
- const safeResolve = () => { if (!settled) { settled = true; resolve(); } };
403
- const safeReject = (err: Error) => { if (!settled) { settled = true; reject(err); } };
404
-
405
- // Setup timeout
406
- const timeout = setTimeout(() => {
407
- this.log.warn(`Connection timeout after ${timeoutMs}ms`);
408
- if (this.ws) {
409
- try {
410
- // Attach a temporary error handler to avoid unhandled 'error' events on abort
411
- this.ws.on('error', () => {});
412
- // Prefer graceful close; terminate as a fallback
413
- if (this.ws.readyState === WebSocket.CONNECTING || this.ws.readyState === WebSocket.OPEN) {
414
- try { this.ws.close(); } catch {}
415
- try { this.ws.terminate(); } catch {}
416
- }
417
- } finally {
418
- try { this.ws.removeAllListeners(); } catch {}
419
- this.ws = undefined;
420
- }
182
+
183
+ const cleanup = () => {
184
+ if (settled) {
185
+ return;
421
186
  }
422
- safeReject(new Error('Connection timeout: Unreal Engine may not be running or Remote Control is not enabled'));
423
- }, timeoutMs);
424
-
425
- // Success handler
426
- const onOpen = () => {
427
- clearTimeout(timeout);
428
- this.connected = true;
429
- this.log.info('Connected to Unreal Remote Control');
430
- this.startCommandProcessor(); // Start command processor on connect
431
- safeResolve();
187
+ settled = true;
188
+ automationBridge.off('connected', onConnected);
189
+ automationBridge.off('handshakeFailed', onHandshakeFailed);
190
+ automationBridge.off('error', onError);
191
+ automationBridge.off('disconnected', onDisconnected);
192
+ clearTimeout(timer);
432
193
  };
433
-
434
- // Error handler
435
- const onError = (err: Error) => {
436
- clearTimeout(timeout);
437
- // Keep error logs concise to avoid stack spam when UE is not running
438
- this.log.debug(`WebSocket error during connect: ${(err && (err as any).code) || ''} ${err.message}`);
439
- if (this.ws) {
440
- try {
441
- // Attach a temporary error handler to avoid unhandled 'error' events while aborting
442
- this.ws.on('error', () => {});
443
- if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
444
- try { this.ws.close(); } catch {}
445
- try { this.ws.terminate(); } catch {}
446
- }
447
- } finally {
448
- try { this.ws.removeAllListeners(); } catch {}
449
- this.ws = undefined;
450
- }
451
- }
452
- safeReject(new Error(`Failed to connect: ${err.message}`));
194
+
195
+ const onConnected = (info: any) => {
196
+ cleanup();
197
+ this.log.debug('Automation bridge connected while waiting', info);
198
+ resolve(true);
453
199
  };
454
-
455
- // Close handler (if closed before open)
456
- const onClose = () => {
457
- if (!this.connected) {
458
- clearTimeout(timeout);
459
- safeReject(new Error('Connection closed before establishing'));
460
- } else {
461
- // Normal close after connection was established
462
- this.connected = false;
463
- this.ws = undefined;
464
- this.log.warn('WebSocket closed');
465
- if (this.autoReconnectEnabled) {
466
- this.scheduleReconnect();
467
- }
468
- }
200
+
201
+ const onHandshakeFailed = (info: any) => {
202
+ this.log.warn('Automation bridge handshake failed while waiting', info);
203
+ // We don't resolve false immediately here? The original code didn't.
204
+ // But handshake failed usually means we should stop waiting.
205
+ cleanup();
206
+ resolve(false);
469
207
  };
470
-
471
- // Message handler (currently best-effort logging)
472
- const onMessage = (raw: WebSocket.RawData) => {
473
- try {
474
- const msg = JSON.parse(String(raw));
475
- this.log.debug('WS message', msg);
476
- } catch (_e) {
477
- // Noise reduction: keep at debug and do nothing on parse errors
478
- }
208
+
209
+ const onError = (err: any) => {
210
+ this.log.warn('Automation bridge error while waiting', err);
211
+ cleanup();
212
+ resolve(false);
479
213
  };
480
-
481
- // Attach listeners
482
- this.ws.once('open', onOpen);
483
- this.ws.once('error', onError);
484
- this.ws.on('close', onClose);
485
- this.ws.on('message', onMessage);
486
- });
487
- }
488
214
 
215
+ const onDisconnected = (info: any) => {
216
+ this.log.warn('Automation bridge disconnected while waiting', info);
217
+ cleanup();
218
+ resolve(false);
219
+ };
489
220
 
490
- async httpCall<T = any>(path: string, method: 'GET' | 'POST' | 'PUT' = 'POST', body?: any): Promise<T> {
491
- // Guard: if not connected, do not attempt HTTP
492
- if (!this.connected) {
493
- throw new Error('Not connected to Unreal Engine');
494
- }
221
+ const timer = setTimeout(() => {
222
+ cleanup();
223
+ resolve(false);
224
+ }, Math.max(0, timeoutMs));
225
+
226
+ automationBridge.on('connected', onConnected);
227
+ automationBridge.on('handshakeFailed', onHandshakeFailed);
228
+ automationBridge.on('error', onError);
229
+ automationBridge.on('disconnected', onDisconnected);
230
+ });
231
+ }
495
232
 
496
- const url = path.startsWith('/') ? path : `/${path}`;
497
- const started = Date.now();
498
-
499
- // Fix Content-Length header issue - ensure body is properly handled
500
- let payload = body;
501
- if ((payload === undefined || payload === null) && method !== 'GET') {
502
- payload = {};
233
+ async getObjectProperty(params: {
234
+ objectPath: string;
235
+ propertyName: string;
236
+ timeoutMs?: number;
237
+ allowAlternate?: boolean;
238
+ }): Promise<Record<string, any>> {
239
+ const { objectPath, propertyName, timeoutMs } = params;
240
+ if (!objectPath || typeof objectPath !== 'string') {
241
+ throw new Error('Invalid objectPath: must be a non-empty string');
503
242
  }
504
-
505
- // Add timeout wrapper to prevent hanging - adjust based on operation type
506
- let CALL_TIMEOUT = 10000; // Default 10 seconds timeout
507
- const longRunningTimeout = 10 * 60 * 1000; // 10 minutes for heavy editor jobs
508
-
509
- // Use payload contents to detect long-running editor operations
510
- let payloadSignature = '';
511
- if (typeof payload === 'string') {
512
- payloadSignature = payload;
513
- } else if (payload && typeof payload === 'object') {
514
- try {
515
- payloadSignature = JSON.stringify(payload);
516
- } catch {
517
- payloadSignature = '';
518
- }
243
+ if (!propertyName || typeof propertyName !== 'string') {
244
+ throw new Error('Invalid propertyName: must be a non-empty string');
519
245
  }
520
246
 
521
- // Allow explicit override via meta property when provided
522
- let sanitizedPayload = payload;
523
- if (payload && typeof payload === 'object' && '__callTimeoutMs' in payload) {
524
- const overrideRaw = (payload as any).__callTimeoutMs;
525
- const overrideMs = typeof overrideRaw === 'number'
526
- ? overrideRaw
527
- : Number.parseInt(String(overrideRaw), 10);
528
- if (Number.isFinite(overrideMs) && overrideMs > 0) {
529
- CALL_TIMEOUT = Math.max(CALL_TIMEOUT, overrideMs);
530
- }
531
- sanitizedPayload = { ...(payload as any) };
532
- delete (sanitizedPayload as any).__callTimeoutMs;
533
- }
247
+ const bridge = this.automationBridge;
534
248
 
535
- // For heavy operations, use longer timeout based on URL or payload signature
536
- if (url.includes('build') || url.includes('create') || url.includes('asset')) {
537
- CALL_TIMEOUT = Math.max(CALL_TIMEOUT, 30000); // 30 seconds for heavy operations
538
- }
539
- if (url.includes('light') || url.includes('BuildLighting')) {
540
- CALL_TIMEOUT = Math.max(CALL_TIMEOUT, 60000); // Base 60 seconds for lighting builds
249
+ if (process.env.MOCK_UNREAL_CONNECTION === 'true') {
250
+ return {
251
+ success: true,
252
+ objectPath,
253
+ propertyName,
254
+ value: 'MockValue',
255
+ propertyValue: 'MockValue',
256
+ transport: 'mock_bridge',
257
+ message: 'Mock property read successful'
258
+ };
541
259
  }
542
260
 
543
- if (payloadSignature) {
544
- const longRunningPatterns = [
545
- /build_light_maps/i,
546
- /lightingbuildquality/i,
547
- /editorbuildlibrary/i,
548
- /buildlighting/i,
549
- /"command"\s*:\s*"buildlighting/i
550
- ];
551
- if (longRunningPatterns.some(pattern => pattern.test(payloadSignature))) {
552
- if (CALL_TIMEOUT < longRunningTimeout) {
553
- this.log.debug(`Detected long-running lighting operation, extending HTTP timeout to ${longRunningTimeout}ms`);
554
- }
555
- CALL_TIMEOUT = Math.max(CALL_TIMEOUT, longRunningTimeout);
556
- }
557
- }
558
-
559
- // CRITICAL: Intercept and block dangerous console commands at HTTP level
560
- if (url === '/remote/object/call' && (payload as any)?.functionName === 'ExecuteConsoleCommand') {
561
- const command = (payload as any)?.parameters?.Command;
562
- if (command && typeof command === 'string') {
563
- const cmdLower = command.trim().toLowerCase();
564
-
565
- // List of commands that cause crashes
566
- const crashCommands = [
567
- 'buildpaths', // Causes access violation 0x0000000000000060
568
- 'rebuildnavigation', // Can crash without nav system
569
- 'buildhierarchicallod', // Can crash without proper setup
570
- 'buildlandscapeinfo', // Can crash without landscape
571
- 'rebuildselectednavigation' // Nav-related crash
572
- ];
573
-
574
- // Check if this is a crash-inducing command
575
- if (crashCommands.some(dangerous => cmdLower === dangerous || cmdLower.startsWith(dangerous + ' '))) {
576
- this.log.warn(`BLOCKED dangerous command that causes crashes: ${command}`);
577
- // Return a safe error response instead of executing
578
- return {
579
- success: false,
580
- error: `Command '${command}' blocked: This command can cause Unreal Engine to crash. Use the Python API alternatives instead.`
581
- } as any;
582
- }
583
-
584
- // Also block other dangerous commands
585
- const dangerousPatterns = [
586
- 'quit', 'exit', 'r.gpucrash', 'debug crash',
587
- 'viewmode visualizebuffer' // These can crash in certain states
588
- ];
589
-
590
- if (dangerousPatterns.some(pattern => cmdLower.includes(pattern))) {
591
- this.log.warn(`BLOCKED potentially dangerous command: ${command}`);
592
- return {
593
- success: false,
594
- error: `Command '${command}' blocked for safety.`
595
- } as any;
596
- }
597
- }
261
+ if (!bridge || typeof bridge.sendAutomationRequest !== 'function') {
262
+ return {
263
+ success: false,
264
+ objectPath,
265
+ propertyName,
266
+ error: 'Automation bridge not connected',
267
+ transport: 'automation_bridge'
268
+ };
598
269
  }
599
-
600
- // Retry logic with exponential backoff and timeout
601
- let lastError: any;
602
- for (let attempt = 0; attempt < 3; attempt++) {
603
- try {
604
- // For GET requests, send payload as query parameters (not in body)
605
- const config: any = { url, method, timeout: CALL_TIMEOUT };
606
- if (method === 'GET' && sanitizedPayload && typeof sanitizedPayload === 'object') {
607
- config.params = sanitizedPayload;
608
- } else if (sanitizedPayload !== undefined) {
609
- config.data = sanitizedPayload;
610
- }
611
270
 
612
- // Wrap with timeout promise to ensure we don't hang
613
- const requestPromise = this.http.request<T>(config);
614
- const resp = await new Promise<Awaited<typeof requestPromise>>((resolve, reject) => {
615
- const timer = setTimeout(() => {
616
- const err = new Error(`Request timeout after ${CALL_TIMEOUT}ms`);
617
- (err as any).code = 'UE_HTTP_TIMEOUT';
618
- reject(err);
619
- }, CALL_TIMEOUT);
620
- requestPromise.then(result => {
621
- clearTimeout(timer);
622
- resolve(result);
623
- }).catch(err => {
624
- clearTimeout(timer);
625
- reject(err);
626
- });
627
- });
628
- const ms = Date.now() - started;
629
-
630
- // Add connection health check for long-running requests
631
- if (ms > 5000) {
632
- this.log.debug(`[HTTP ${method}] ${url} -> ${ms}ms (long request)`);
633
- } else {
634
- this.log.debug(`[HTTP ${method}] ${url} -> ${ms}ms`);
635
- }
271
+ try {
272
+ const response = await bridge.sendAutomationRequest(
273
+ 'get_object_property',
274
+ {
275
+ objectPath,
276
+ propertyName
277
+ },
278
+ timeoutMs ? { timeoutMs } : undefined
279
+ );
636
280
 
637
- return resp.data;
638
- } catch (error: any) {
639
- lastError = error;
640
- const delay = Math.min(1000 * Math.pow(2, attempt), 5000); // Exponential backoff with 5s max
641
-
642
- // Log timeout errors specifically
643
- if (error.message?.includes('timeout')) {
644
- this.log.debug(`HTTP request timed out (attempt ${attempt + 1}/3): ${url}`);
645
- }
646
-
647
- if (attempt < 2) {
648
- this.log.debug(`HTTP request failed (attempt ${attempt + 1}/3), retrying in ${delay}ms...`);
649
- await new Promise(resolve => setTimeout(resolve, delay));
650
-
651
- // If connection error, try to reconnect
652
- if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT' || error.message?.includes('timeout')) {
653
- if (this.autoReconnectEnabled) {
654
- this.scheduleReconnect();
655
- }
281
+ const success = response.success !== false;
282
+ const rawResult =
283
+ response.result && typeof response.result === 'object'
284
+ ? { ...(response.result as Record<string, unknown>) }
285
+ : response.result;
286
+ const value =
287
+ (rawResult as any)?.value ??
288
+ (rawResult as any)?.propertyValue ??
289
+ (success ? rawResult : undefined);
290
+
291
+ if (success) {
292
+ return {
293
+ success: true,
294
+ objectPath,
295
+ propertyName,
296
+ value,
297
+ propertyValue: value,
298
+ transport: 'automation_bridge',
299
+ message: response.message,
300
+ warnings: Array.isArray((rawResult as any)?.warnings)
301
+ ? (rawResult as any).warnings
302
+ : undefined,
303
+ raw: rawResult,
304
+ bridge: {
305
+ requestId: response.requestId,
306
+ success: true,
307
+ error: response.error
656
308
  }
657
- }
309
+ };
658
310
  }
311
+
312
+ return {
313
+ success: false,
314
+ objectPath,
315
+ propertyName,
316
+ error: response.error || response.message || 'AUTOMATION_BRIDGE_FAILURE',
317
+ transport: 'automation_bridge',
318
+ raw: rawResult,
319
+ bridge: {
320
+ requestId: response.requestId,
321
+ success: false,
322
+ error: response.error
323
+ }
324
+ };
325
+ } catch (err) {
326
+ const message = err instanceof Error ? err.message : String(err);
327
+ return {
328
+ success: false,
329
+ objectPath,
330
+ propertyName,
331
+ error: message,
332
+ transport: 'automation_bridge'
333
+ };
659
334
  }
660
-
661
- throw lastError;
662
335
  }
663
336
 
664
- private parsePythonJsonResult<T = any>(raw: any): T | null {
665
- if (!raw) {
666
- return null;
337
+ async setObjectProperty(params: {
338
+ objectPath: string;
339
+ propertyName: string;
340
+ value: unknown;
341
+ markDirty?: boolean;
342
+ timeoutMs?: number;
343
+ allowAlternate?: boolean;
344
+ }): Promise<Record<string, any>> {
345
+ const { objectPath, propertyName, value, markDirty, timeoutMs } = params;
346
+ if (!objectPath || typeof objectPath !== 'string') {
347
+ throw new Error('Invalid objectPath: must be a non-empty string');
667
348
  }
668
-
669
- const fragments: string[] = [];
670
-
671
- if (typeof raw === 'string') {
672
- fragments.push(raw);
349
+ if (!propertyName || typeof propertyName !== 'string') {
350
+ throw new Error('Invalid propertyName: must be a non-empty string');
673
351
  }
674
352
 
675
- if (typeof raw?.Output === 'string') {
676
- fragments.push(raw.Output);
677
- }
353
+ const bridge = this.automationBridge;
678
354
 
679
- if (typeof raw?.ReturnValue === 'string') {
680
- fragments.push(raw.ReturnValue);
355
+ if (process.env.MOCK_UNREAL_CONNECTION === 'true') {
356
+ return {
357
+ success: true,
358
+ objectPath,
359
+ propertyName,
360
+ message: 'Mock property set successful',
361
+ transport: 'mock_bridge'
362
+ };
681
363
  }
682
364
 
683
- if (Array.isArray(raw?.LogOutput)) {
684
- for (const entry of raw.LogOutput) {
685
- if (!entry) continue;
686
- if (typeof entry === 'string') {
687
- fragments.push(entry);
688
- } else if (typeof entry?.Output === 'string') {
689
- fragments.push(entry.Output);
690
- }
691
- }
365
+ if (!bridge || typeof bridge.sendAutomationRequest !== 'function') {
366
+ return {
367
+ success: false,
368
+ objectPath,
369
+ propertyName,
370
+ error: 'Automation bridge not connected',
371
+ transport: 'automation_bridge'
372
+ };
692
373
  }
693
374
 
694
- const combined = fragments.join('\n');
695
- const match = combined.match(/RESULT:(\{.*\}|\[.*\])/s);
696
- if (!match) {
697
- return null;
375
+ const payload: Record<string, unknown> = {
376
+ objectPath,
377
+ propertyName,
378
+ value
379
+ };
380
+ if (markDirty !== undefined) {
381
+ payload.markDirty = Boolean(markDirty);
698
382
  }
699
383
 
700
384
  try {
701
- return JSON.parse(match[1]);
702
- } catch {
703
- return null;
704
- }
705
- }
706
-
707
- async ensurePluginsEnabled(pluginNames: string[], context?: string): Promise<string[]> {
708
- if (!pluginNames || pluginNames.length === 0) {
709
- return [];
710
- }
711
-
712
- const now = Date.now();
713
- const pluginsToCheck = pluginNames.filter((name) => {
714
- const cached = this.pluginStatusCache.get(name);
715
- if (!cached) return true;
716
- if (now - cached.timestamp > this.PLUGIN_CACHE_TTL) {
717
- this.pluginStatusCache.delete(name);
718
- return true;
719
- }
720
- return false;
721
- });
722
-
723
- if (pluginsToCheck.length > 0) {
724
- const python = `
725
- import unreal
726
- import json
727
-
728
- plugins = ${JSON.stringify(pluginsToCheck)}
729
- status = {}
730
-
731
- def get_plugin_manager():
732
- try:
733
- return unreal.PluginManager.get()
734
- except AttributeError:
735
- return None
736
- except Exception:
737
- return None
738
-
739
- def get_plugins_subsystem():
740
- try:
741
- return unreal.get_editor_subsystem(unreal.PluginsEditorSubsystem)
742
- except AttributeError:
743
- pass
744
- except Exception:
745
- pass
746
- try:
747
- return unreal.PluginsSubsystem()
748
- except Exception:
749
- return None
750
-
751
- pm = get_plugin_manager()
752
- ps = get_plugins_subsystem()
753
-
754
- def is_enabled(plugin_name):
755
- if pm:
756
- try:
757
- if pm.is_plugin_enabled(plugin_name):
758
- return True
759
- except Exception:
760
- try:
761
- plugin = pm.find_plugin(plugin_name)
762
- if plugin and plugin.is_enabled():
763
- return True
764
- except Exception:
765
- pass
766
- if ps:
767
- try:
768
- return bool(ps.is_plugin_enabled(plugin_name))
769
- except Exception:
770
- try:
771
- plugin = ps.find_plugin(plugin_name)
772
- if plugin and plugin.is_enabled():
773
- return True
774
- except Exception:
775
- pass
776
- return False
777
-
778
- for plugin_name in plugins:
779
- enabled = False
780
- try:
781
- enabled = is_enabled(plugin_name)
782
- except Exception:
783
- enabled = False
784
- status[plugin_name] = bool(enabled)
785
-
786
- print('RESULT:' + json.dumps(status))
787
- `.trim();
385
+ const response = await bridge.sendAutomationRequest(
386
+ 'set_object_property',
387
+ payload,
388
+ timeoutMs ? { timeoutMs } : undefined
389
+ );
788
390
 
789
- try {
790
- const response = await this.executePython(python);
791
- const parsed = this.parsePythonJsonResult<Record<string, boolean>>(response);
792
- if (parsed) {
793
- for (const [name, enabled] of Object.entries(parsed)) {
794
- this.pluginStatusCache.set(name, { enabled: Boolean(enabled), timestamp: now });
391
+ const success = response.success !== false;
392
+ const rawResult =
393
+ response.result && typeof response.result === 'object'
394
+ ? { ...(response.result as Record<string, unknown>) }
395
+ : response.result;
396
+
397
+ if (success) {
398
+ return {
399
+ success: true,
400
+ objectPath,
401
+ propertyName,
402
+ message:
403
+ response.message ||
404
+ (typeof (rawResult as any)?.message === 'string' ? (rawResult as any).message : undefined),
405
+ transport: 'automation_bridge',
406
+ raw: rawResult,
407
+ bridge: {
408
+ requestId: response.requestId,
409
+ success: true,
410
+ error: response.error
795
411
  }
796
- } else {
797
- this.log.warn('Failed to parse plugin status response', { context, pluginsToCheck });
798
- }
799
- } catch (error) {
800
- this.log.warn('Plugin status check failed', { context, pluginsToCheck, error: (error as Error)?.message ?? error });
801
- }
802
- }
803
-
804
- for (const name of pluginNames) {
805
- if (!this.pluginStatusCache.has(name)) {
806
- this.pluginStatusCache.set(name, { enabled: false, timestamp: now });
412
+ };
807
413
  }
808
- }
809
414
 
810
- const missing = pluginNames.filter((name) => !this.pluginStatusCache.get(name)?.enabled);
811
- if (missing.length && context) {
812
- this.log.warn(`Missing required Unreal plugins for ${context}: ${missing.join(', ')}`);
415
+ return {
416
+ success: false,
417
+ objectPath,
418
+ propertyName,
419
+ error: response.error || response.message || 'AUTOMATION_BRIDGE_FAILURE',
420
+ transport: 'automation_bridge',
421
+ raw: rawResult,
422
+ bridge: {
423
+ requestId: response.requestId,
424
+ success: false,
425
+ error: response.error
426
+ }
427
+ };
428
+ } catch (err) {
429
+ const message = err instanceof Error ? err.message : String(err);
430
+ return {
431
+ success: false,
432
+ objectPath,
433
+ propertyName,
434
+ error: message,
435
+ transport: 'automation_bridge'
436
+ };
813
437
  }
814
- return missing;
815
- }
816
-
817
- // Generic function call via Remote Control HTTP API
818
- async call(body: RcCallBody): Promise<any> {
819
- if (!this.connected) throw new Error('Not connected to Unreal Engine');
820
- // Using HTTP endpoint /remote/object/call
821
- const result = await this.httpCall<any>('/remote/object/call', 'PUT', {
822
- generateTransaction: false,
823
- ...body
824
- });
825
- return result;
826
- }
827
-
828
- async getExposed(): Promise<any> {
829
- if (!this.connected) throw new Error('Not connected to Unreal Engine');
830
- return this.httpCall('/remote/preset', 'GET');
831
438
  }
832
439
 
833
440
  // Execute a console command safely with validation and throttling
834
- async executeConsoleCommand(command: string, options: { allowPython?: boolean } = {}): Promise<any> {
835
- if (!this.connected) {
836
- throw new Error('Not connected to Unreal Engine');
837
- }
838
- const { allowPython = false } = options;
839
- // Validate command is not empty
840
- if (!command || typeof command !== 'string') {
841
- throw new Error('Invalid command: must be a non-empty string');
441
+ async executeConsoleCommand(command: string, _options: Record<string, never> = {}): Promise<any> {
442
+ const automationAvailable = Boolean(
443
+ this.automationBridge && typeof this.automationBridge.sendAutomationRequest === 'function'
444
+ );
445
+ if (!automationAvailable) {
446
+ throw new Error('Automation bridge not connected');
842
447
  }
843
-
448
+
449
+ // Validate command
450
+ CommandValidator.validate(command);
844
451
  const cmdTrimmed = command.trim();
845
452
  if (cmdTrimmed.length === 0) {
846
- // Return success for empty commands to match UE behavior
847
453
  return { success: true, message: 'Empty command ignored' };
848
454
  }
849
455
 
850
- if (cmdTrimmed.includes('\n') || cmdTrimmed.includes('\r')) {
851
- throw new Error('Multi-line console commands are not allowed. Send one command per call.');
456
+ if (CommandValidator.isLikelyInvalid(cmdTrimmed)) {
457
+ this.log.warn(`Command appears invalid: ${cmdTrimmed}`);
852
458
  }
853
459
 
854
- const cmdLower = cmdTrimmed.toLowerCase();
460
+ const priority = CommandValidator.getPriority(cmdTrimmed);
855
461
 
856
- if (!allowPython && (cmdLower === 'py' || cmdLower.startsWith('py '))) {
857
- throw new Error('Python console commands are blocked from external calls for safety.');
858
- }
859
-
860
- // Check for dangerous commands
861
- const dangerousCommands = [
862
- 'quit', 'exit', 'delete', 'destroy', 'kill', 'crash',
863
- 'viewmode visualizebuffer basecolor',
864
- 'viewmode visualizebuffer worldnormal',
865
- 'r.gpucrash',
866
- 'buildpaths', // Can cause access violation if nav system not initialized
867
- 'rebuildnavigation' // Can also crash without proper nav setup
868
- ];
869
- if (dangerousCommands.some(dangerous => cmdLower.includes(dangerous))) {
870
- throw new Error(`Dangerous command blocked: ${command}`);
871
- }
462
+ const executeCommand = async (): Promise<any> => {
463
+ if (process.env.MOCK_UNREAL_CONNECTION === 'true') {
464
+ this.log.info(`[MOCK] Executing console command: ${cmdTrimmed}`);
465
+ return { success: true, message: `Mock execution of '${cmdTrimmed}' successful`, transport: 'mock_bridge' };
466
+ }
872
467
 
873
- const forbiddenTokens = [
874
- 'rm ', 'rm-', 'del ', 'format ', 'shutdown', 'reboot',
875
- 'rmdir', 'mklink', 'copy ', 'move ', 'start "', 'system(',
876
- 'import os', 'import subprocess', 'subprocess.', 'os.system',
877
- 'exec(', 'eval(', '__import__', 'import sys', 'import importlib',
878
- 'with open', 'open('
879
- ];
468
+ if (!this.automationBridge || !this.automationBridge.isConnected()) {
469
+ throw new Error('Automation bridge not connected');
470
+ }
880
471
 
881
- if (cmdLower.includes('&&') || cmdLower.includes('||')) {
882
- throw new Error('Command chaining with && or || is blocked for safety.');
883
- }
472
+ const pluginResp: any = await this.automationBridge.sendAutomationRequest(
473
+ 'console_command',
474
+ { command: cmdTrimmed },
475
+ { timeoutMs: 30000 }
476
+ );
477
+
478
+ if (pluginResp && pluginResp.success) {
479
+ return { ...(pluginResp as any), transport: 'automation_bridge' };
480
+ }
481
+
482
+ const errMsg = pluginResp?.message || pluginResp?.error || 'Plugin execution failed';
483
+ throw new Error(errMsg);
484
+ };
884
485
 
885
- if (forbiddenTokens.some(token => cmdLower.includes(token))) {
886
- throw new Error(`Command contains unsafe token and was blocked: ${command}`);
887
- }
888
-
889
- // Determine priority based on command type
890
- let priority = 7; // Default priority
891
-
892
- if (command.includes('BuildLighting') || command.includes('BuildPaths')) {
893
- priority = 1; // Heavy operation
894
- } else if (command.includes('summon') || command.includes('spawn')) {
895
- priority = 5; // Medium operation
896
- } else if (command.startsWith('stat') || command.startsWith('show')) {
897
- priority = 9; // Light operation
898
- }
899
-
900
- // Known invalid command patterns
901
- const invalidPatterns = [
902
- /^\d+$/, // Just numbers
903
- /^invalid_command/i,
904
- /^this_is_not_a_valid/i,
905
- ];
906
-
907
- const isLikelyInvalid = invalidPatterns.some(pattern => pattern.test(cmdTrimmed));
908
- if (isLikelyInvalid) {
909
- this.log.warn(`Command appears invalid: ${cmdTrimmed}`);
910
- }
911
-
912
486
  try {
913
- const result = await this.executeThrottledCommand(
914
- () => this.httpCall('/remote/object/call', 'PUT', {
915
- objectPath: '/Script/Engine.Default__KismetSystemLibrary',
916
- functionName: 'ExecuteConsoleCommand',
917
- parameters: {
918
- WorldContextObject: null,
919
- Command: cmdTrimmed,
920
- SpecificPlayer: null
921
- },
922
- generateTransaction: false
923
- }),
924
- priority
925
- );
926
-
487
+ const result = await this.executeThrottledCommand(executeCommand, priority);
927
488
  return result;
928
489
  } catch (error) {
929
490
  this.log.error(`Console command failed: ${cmdTrimmed}`, error);
@@ -931,50 +492,8 @@ print('RESULT:' + json.dumps(status))
931
492
  }
932
493
  }
933
494
 
934
- summarizeConsoleCommand(command: string, response: any) {
935
- const trimmedCommand = command.trim();
936
- const logLines = Array.isArray(response?.LogOutput)
937
- ? (response.LogOutput as any[]).map(entry => {
938
- if (entry === null || entry === undefined) {
939
- return '';
940
- }
941
- if (typeof entry === 'string') {
942
- return entry;
943
- }
944
- return typeof entry.Output === 'string' ? entry.Output : '';
945
- }).filter(Boolean)
946
- : [];
947
-
948
- let output = logLines.join('\n').trim();
949
- if (!output) {
950
- if (typeof response === 'string') {
951
- output = response.trim();
952
- } else if (response && typeof response === 'object') {
953
- if (typeof response.Output === 'string') {
954
- output = response.Output.trim();
955
- } else if ('result' in response && response.result !== undefined) {
956
- output = String(response.result).trim();
957
- } else if ('ReturnValue' in response && typeof response.ReturnValue === 'string') {
958
- output = response.ReturnValue.trim();
959
- }
960
- }
961
- }
962
-
963
- const returnValue = response && typeof response === 'object' && 'ReturnValue' in response
964
- ? (response as any).ReturnValue
965
- : undefined;
966
-
967
- return {
968
- command: trimmedCommand,
969
- output,
970
- logLines,
971
- returnValue,
972
- raw: response
973
- };
974
- }
975
-
976
495
  async executeConsoleCommands(
977
- commands: Iterable<string | { command: string; priority?: number; allowPython?: boolean }>,
496
+ commands: Iterable<string | { command: string; priority?: number }>,
978
497
  options: { continueOnError?: boolean; delayMs?: number } = {}
979
498
  ): Promise<any[]> {
980
499
  const { continueOnError = false, delayMs = 0 } = options;
@@ -987,9 +506,7 @@ print('RESULT:' + json.dumps(status))
987
506
  continue;
988
507
  }
989
508
  try {
990
- const result = await this.executeConsoleCommand(command, {
991
- allowPython: Boolean(descriptor.allowPython)
992
- });
509
+ const result = await this.executeConsoleCommand(command);
993
510
  results.push(result);
994
511
  } catch (error) {
995
512
  if (!continueOnError) {
@@ -1007,482 +524,106 @@ print('RESULT:' + json.dumps(status))
1007
524
  return results;
1008
525
  }
1009
526
 
1010
- // Try to execute a Python command via the PythonScriptPlugin, fallback to `py` console command.
1011
- async executePython(command: string): Promise<any> {
1012
- if (!this.connected) {
1013
- throw new Error('Not connected to Unreal Engine');
527
+ async executeEditorFunction(
528
+ functionName: string,
529
+ params?: Record<string, any>,
530
+ _options?: { timeoutMs?: number }
531
+ ): Promise<any> {
532
+ if (process.env.MOCK_UNREAL_CONNECTION === 'true') {
533
+ return { success: true, result: { status: 'mock_success', function: functionName } };
1014
534
  }
1015
- const isMultiLine = /[\r\n]/.test(command) || command.includes(';');
1016
- try {
1017
- // Use ExecutePythonCommandEx with appropriate mode based on content
1018
- return await this.httpCall('/remote/object/call', 'PUT', {
1019
- objectPath: '/Script/PythonScriptPlugin.Default__PythonScriptLibrary',
1020
- functionName: 'ExecutePythonCommandEx',
1021
- parameters: {
1022
- PythonCommand: command,
1023
- ExecutionMode: isMultiLine ? 'ExecuteFile' : 'ExecuteStatement',
1024
- FileExecutionScope: 'Private'
1025
- },
1026
- generateTransaction: false
1027
- });
1028
- } catch {
1029
- try {
1030
- // Fallback to ExecutePythonCommand (more tolerant for multi-line)
1031
- return await this.httpCall('/remote/object/call', 'PUT', {
1032
- objectPath: '/Script/PythonScriptPlugin.Default__PythonScriptLibrary',
1033
- functionName: 'ExecutePythonCommand',
1034
- parameters: {
1035
- Command: command
1036
- },
1037
- generateTransaction: false
1038
- });
1039
- } catch {
1040
- // Final fallback: execute via console py command
1041
- this.log.warn('PythonScriptLibrary not available or failed, falling back to console `py` command');
1042
-
1043
- // For simple single-line commands
1044
- if (!isMultiLine) {
1045
- return await this.executeConsoleCommand(`py ${command}`, { allowPython: true });
1046
- }
1047
-
1048
- // For multi-line scripts, try to execute as a block
1049
- try {
1050
- // Try executing as a single exec block
1051
- // Properly escape the script for Python exec
1052
- const escapedScript = command
1053
- .replace(/\\/g, '\\\\')
1054
- .replace(/"/g, '\\"')
1055
- .replace(/\n/g, '\\n')
1056
- .replace(/\r/g, '');
1057
- return await this.executeConsoleCommand(`py exec("${escapedScript}")`, { allowPython: true });
1058
- } catch {
1059
- // If that fails, break into smaller chunks
1060
- try {
1061
- // First ensure unreal is imported
1062
- await this.executeConsoleCommand('py import unreal');
1063
-
1064
- // For complex multi-line scripts, execute in logical chunks
1065
- const commandWithoutImport = command.replace(/^\s*import\s+unreal\s*;?\s*/m, '');
1066
-
1067
- // Split by semicolons first, then by newlines
1068
- const statements = commandWithoutImport
1069
- .split(/[;\n]/)
1070
- .map(s => s.trim())
1071
- .filter(s => s.length > 0 && !s.startsWith('#'));
1072
-
1073
- let result = null;
1074
- for (const stmt of statements) {
1075
- // Skip if statement is too long for console
1076
- if (stmt.length > 200) {
1077
- // Try to execute as a single exec block
1078
- const miniScript = `exec("""${stmt.replace(/"/g, '\\"')}""")`;
1079
- result = await this.executeConsoleCommand(`py ${miniScript}`, { allowPython: true });
1080
- } else {
1081
- result = await this.executeConsoleCommand(`py ${stmt}`, { allowPython: true });
1082
- }
1083
- // Small delay between commands
1084
- await new Promise(resolve => setTimeout(resolve, 30));
1085
- }
1086
-
1087
- return result;
1088
- } catch {
1089
- // Final fallback: execute line by line
1090
- const lines = command.split('\n').filter(line => line.trim().length > 0);
1091
- let result = null;
1092
-
1093
- for (const line of lines) {
1094
- // Skip comments
1095
- if (line.trim().startsWith('#')) {
1096
- continue;
1097
- }
1098
- result = await this.executeConsoleCommand(`py ${line.trim()}`, { allowPython: true });
1099
- // Small delay between commands to ensure execution order
1100
- await new Promise(resolve => setTimeout(resolve, 50));
1101
- }
1102
-
1103
- return result;
1104
- }
1105
- }
1106
- }
1107
- }
1108
- }
1109
-
1110
- // Allow callers to enable/disable auto-reconnect behavior
1111
- setAutoReconnectEnabled(enabled: boolean): void {
1112
- this.autoReconnectEnabled = enabled;
1113
- }
1114
535
 
1115
- // Connection recovery
1116
- private scheduleReconnect(): void {
1117
- if (!this.autoReconnectEnabled) {
1118
- this.log.info('Auto-reconnect disabled; not scheduling reconnection');
1119
- return;
1120
- }
1121
- if (this.reconnectTimer || this.connected) {
1122
- return;
1123
- }
1124
-
1125
- if (this.reconnectAttempts >= this.MAX_RECONNECT_ATTEMPTS) {
1126
- this.log.error('Max reconnection attempts reached. Please check Unreal Engine.');
1127
- return;
1128
- }
1129
-
1130
- // Exponential backoff with jitter
1131
- const delay = Math.min(
1132
- this.BASE_RECONNECT_DELAY * Math.pow(2, this.reconnectAttempts) + Math.random() * 1000,
1133
- 30000 // Max 30 seconds
1134
- );
1135
-
1136
- this.log.debug(`Scheduling reconnection attempt ${this.reconnectAttempts + 1}/${this.MAX_RECONNECT_ATTEMPTS} in ${Math.round(delay)}ms`);
1137
-
1138
- this.reconnectTimer = setTimeout(async () => {
1139
- this.reconnectTimer = undefined;
1140
- this.reconnectAttempts++;
1141
-
1142
- try {
1143
- await this.connect();
1144
- this.reconnectAttempts = 0;
1145
- this.log.info('Successfully reconnected to Unreal Engine');
1146
- } catch (err) {
1147
- this.log.warn('Reconnection attempt failed:', err);
1148
- this.scheduleReconnect();
1149
- }
1150
- }, delay);
1151
- }
1152
-
1153
- // Graceful shutdown
1154
- async disconnect(): Promise<void> {
1155
- if (this.reconnectTimer) {
1156
- clearTimeout(this.reconnectTimer);
1157
- this.reconnectTimer = undefined;
1158
- }
1159
-
1160
- if (this.ws) {
1161
- try {
1162
- // Avoid unhandled error during shutdown
1163
- this.ws.on('error', () => {});
1164
- try { this.ws.close(); } catch {}
1165
- try { this.ws.terminate(); } catch {}
1166
- } finally {
1167
- try { this.ws.removeAllListeners(); } catch {}
1168
- this.ws = undefined;
1169
- }
1170
- }
1171
-
1172
- this.connected = false;
1173
- }
1174
-
1175
- /**
1176
- * Enhanced Editor Function Access
1177
- * Use Python scripting as a bridge to access modern Editor Subsystem functions
1178
- */
1179
- async executeEditorFunction(functionName: string, params?: Record<string, any>): Promise<any> {
1180
- const template = this.PYTHON_TEMPLATES[functionName];
1181
- if (!template) {
1182
- throw new Error(`Unknown editor function: ${functionName}`);
536
+ if (!this.automationBridge || typeof this.automationBridge.sendAutomationRequest !== 'function') {
537
+ return { success: false, error: 'AUTOMATION_BRIDGE_UNAVAILABLE' };
1183
538
  }
1184
539
 
1185
- let script = template.script;
1186
-
1187
- // Replace parameters in the script
1188
- if (params) {
1189
- for (const [key, value] of Object.entries(params)) {
1190
- const placeholder = `{${key}}`;
1191
- script = script.replace(new RegExp(placeholder, 'g'), String(value));
1192
- }
1193
- }
540
+ const resp: any = await this.automationBridge.sendAutomationRequest('execute_editor_function', {
541
+ functionName,
542
+ params: params ?? {}
543
+ }, _options?.timeoutMs ? { timeoutMs: _options.timeoutMs } : undefined);
1194
544
 
1195
- try {
1196
- // Execute Python script with result parsing
1197
- const result = await this.executePythonWithResult(script);
1198
- return result;
1199
- } catch (error) {
1200
- this.log.error(`Failed to execute editor function ${functionName}:`, error);
1201
-
1202
- // Fallback to console command if Python fails
1203
- return this.executeFallbackCommand(functionName, params);
1204
- }
545
+ return resp && resp.success !== false ? (resp.result ?? resp) : resp;
1205
546
  }
1206
547
 
1207
- /**
1208
- * Execute Python script and parse the result
1209
- */
1210
- // Expose for internal consumers (resources) that want parsed RESULT blocks
1211
- public async executePythonWithResult(script: string): Promise<any> {
1212
- try {
1213
- // Wrap script to capture output so we can parse RESULT: lines reliably
1214
- const wrappedScript = `
1215
- import sys
1216
- import io
1217
- old_stdout = sys.stdout
1218
- sys.stdout = buffer = io.StringIO()
1219
- try:
1220
- ${script.split('\n').join('\n ')}
1221
- finally:
1222
- output = buffer.getvalue()
1223
- sys.stdout = old_stdout
1224
- if output:
1225
- print(output)
1226
- `.trim()
1227
- .replace(/\r?\n/g, '\n');
1228
-
1229
- const response = await this.executePython(wrappedScript);
1230
-
1231
- // Extract textual output from various response shapes
1232
- let out = '';
1233
- try {
1234
- if (response && typeof response === 'string') {
1235
- out = response;
1236
- } else if (response && typeof response === 'object') {
1237
- if (Array.isArray((response as any).LogOutput)) {
1238
- out = (response as any).LogOutput.map((l: any) => l.Output || '').join('');
1239
- } else if (typeof (response as any).Output === 'string') {
1240
- out = (response as any).Output;
1241
- } else if (typeof (response as any).result === 'string') {
1242
- out = (response as any).result;
1243
- } else {
1244
- out = JSON.stringify(response);
1245
- }
1246
- }
1247
- } catch {
1248
- out = String(response || '');
1249
- }
1250
-
1251
- // Robust RESULT parsing with bracket matching (handles nested objects)
1252
- const marker = 'RESULT:';
1253
- const idx = out.lastIndexOf(marker);
1254
- if (idx !== -1) {
1255
- // Find first '{' after the marker
1256
- let i = idx + marker.length;
1257
- while (i < out.length && out[i] !== '{') i++;
1258
- if (i < out.length && out[i] === '{') {
1259
- let depth = 0;
1260
- let inStr = false;
1261
- let esc = false;
1262
- let j = i;
1263
- for (; j < out.length; j++) {
1264
- const ch = out[j];
1265
- if (esc) { esc = false; continue; }
1266
- if (ch === '\\') { esc = true; continue; }
1267
- if (ch === '"') { inStr = !inStr; continue; }
1268
- if (!inStr) {
1269
- if (ch === '{') depth++;
1270
- else if (ch === '}') { depth--; if (depth === 0) { j++; break; } }
1271
- }
1272
- }
1273
- const jsonStr = out.slice(i, j);
1274
- try { return JSON.parse(jsonStr); } catch {}
1275
- }
1276
- }
1277
-
1278
- // Fallback to previous regex approach (best-effort)
1279
- const matches = Array.from(out.matchAll(/RESULT:({[\s\S]*})/g));
1280
- if (matches.length > 0) {
1281
- const last = matches[matches.length - 1][1];
1282
- try { return JSON.parse(last); } catch { return { raw: last }; }
1283
- }
1284
-
1285
- // If no RESULT: marker, return the best-effort textual output or original response
1286
- return typeof response !== 'undefined' ? response : out;
1287
- } catch {
1288
- this.log.warn('Python execution failed, trying direct execution');
1289
- return this.executePython(script);
1290
- }
1291
- }
1292
-
1293
- /**
1294
- * Get the Unreal Engine version via Python and parse major/minor/patch.
1295
- */
548
+ /** Get Unreal Engine version */
1296
549
  async getEngineVersion(): Promise<{ version: string; major: number; minor: number; patch: number; isUE56OrAbove: boolean; }> {
1297
- const now = Date.now();
1298
- if (this.engineVersionCache && now - this.engineVersionCache.timestamp < this.ENGINE_VERSION_TTL_MS) {
1299
- return this.engineVersionCache.value;
550
+ if (process.env.MOCK_UNREAL_CONNECTION === 'true') {
551
+ return { version: '5.6.0-Mock', major: 5, minor: 6, patch: 0, isUE56OrAbove: true };
1300
552
  }
1301
553
 
554
+ const bridge = this.getAutomationBridge();
1302
555
  try {
1303
- const script = `
1304
- import unreal, json, re
1305
- ver = str(unreal.SystemLibrary.get_engine_version())
1306
- m = re.match(r'^(\\d+)\\.(\\d+)\\.(\\d+)', ver)
1307
- major = int(m.group(1)) if m else 0
1308
- minor = int(m.group(2)) if m else 0
1309
- patch = int(m.group(3)) if m else 0
1310
- print('RESULT:' + json.dumps({'version': ver, 'major': major, 'minor': minor, 'patch': patch}))
1311
- `.trim();
1312
- const result = await this.executePythonWithResult(script);
1313
- const version = String(result?.version ?? 'unknown');
1314
- const major = Number(result?.major ?? 0) || 0;
1315
- const minor = Number(result?.minor ?? 0) || 0;
1316
- const patch = Number(result?.patch ?? 0) || 0;
1317
- const isUE56OrAbove = major > 5 || (major === 5 && minor >= 6);
1318
- const value = { version, major, minor, patch, isUE56OrAbove };
1319
- this.engineVersionCache = { value, timestamp: now };
1320
- return value;
556
+ const resp: any = await bridge.sendAutomationRequest(
557
+ 'system_control',
558
+ { action: 'get_engine_version' },
559
+ { timeoutMs: 15000 }
560
+ );
561
+ const raw = resp && typeof resp.result === 'object'
562
+ ? (resp.result as any)
563
+ : (resp?.result ?? resp ?? {});
564
+ const version = typeof raw.version === 'string' ? raw.version : 'unknown';
565
+ const major = typeof raw.major === 'number' ? raw.major : 0;
566
+ const minor = typeof raw.minor === 'number' ? raw.minor : 0;
567
+ const patch = typeof raw.patch === 'number' ? raw.patch : 0;
568
+ const isUE56OrAbove =
569
+ typeof raw.isUE56OrAbove === 'boolean'
570
+ ? raw.isUE56OrAbove
571
+ : (major > 5 || (major === 5 && minor >= 6));
572
+ return { version, major, minor, patch, isUE56OrAbove };
1321
573
  } catch (error) {
1322
- this.log.warn('Failed to get engine version via Python', error);
1323
- const fallback = { version: 'unknown', major: 0, minor: 0, patch: 0, isUE56OrAbove: false };
1324
- this.engineVersionCache = { value: fallback, timestamp: now };
1325
- return fallback;
1326
- }
1327
- }
1328
-
1329
- /**
1330
- * Query feature flags (Python availability, editor subsystems) via Python.
1331
- */
1332
- async getFeatureFlags(): Promise<{ pythonEnabled: boolean; subsystems: { unrealEditor: boolean; levelEditor: boolean; editorActor: boolean; } }> {
1333
- try {
1334
- const script = `
1335
- import unreal, json
1336
- flags = {}
1337
- # Python plugin availability (class exists)
1338
- try:
1339
- _ = unreal.PythonScriptLibrary
1340
- flags['pythonEnabled'] = True
1341
- except Exception:
1342
- flags['pythonEnabled'] = False
1343
- # Editor subsystems
1344
- try:
1345
- flags['unrealEditor'] = bool(unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem))
1346
- except Exception:
1347
- flags['unrealEditor'] = False
1348
- try:
1349
- flags['levelEditor'] = bool(unreal.get_editor_subsystem(unreal.LevelEditorSubsystem))
1350
- except Exception:
1351
- flags['levelEditor'] = False
1352
- try:
1353
- flags['editorActor'] = bool(unreal.get_editor_subsystem(unreal.EditorActorSubsystem))
1354
- except Exception:
1355
- flags['editorActor'] = False
1356
- print('RESULT:' + json.dumps(flags))
1357
- `.trim();
1358
- const res = await this.executePythonWithResult(script);
574
+ this.log.warn('getEngineVersion failed', error);
1359
575
  return {
1360
- pythonEnabled: Boolean(res?.pythonEnabled),
1361
- subsystems: {
1362
- unrealEditor: Boolean(res?.unrealEditor),
1363
- levelEditor: Boolean(res?.levelEditor),
1364
- editorActor: Boolean(res?.editorActor)
1365
- }
576
+ version: 'unknown',
577
+ major: 0,
578
+ minor: 0,
579
+ patch: 0,
580
+ isUE56OrAbove: false
1366
581
  };
1367
- } catch (e) {
1368
- this.log.warn('Failed to get feature flags via Python', e);
1369
- return { pythonEnabled: false, subsystems: { unrealEditor: false, levelEditor: false, editorActor: false } };
1370
- }
1371
- }
1372
-
1373
- /**
1374
- * Fallback commands when Python is not available
1375
- */
1376
- private async executeFallbackCommand(functionName: string, params?: Record<string, any>): Promise<any> {
1377
- switch (functionName) {
1378
- case 'SPAWN_ACTOR_AT_LOCATION':
1379
- return this.executeConsoleCommand(
1380
- `summon ${params?.class_path || 'StaticMeshActor'} ${params?.x || 0} ${params?.y || 0} ${params?.z || 0}`
1381
- );
1382
-
1383
- case 'DELETE_ACTOR':
1384
- // Use Python-based deletion to avoid unsafe console command and improve reliability
1385
- return this.executePythonWithResult(this.PYTHON_TEMPLATES.DELETE_ACTOR.script.replace('{actor_name}', String(params?.actor_name || '')));
1386
-
1387
- case 'BUILD_LIGHTING':
1388
- return this.executeConsoleCommand('BuildLighting');
1389
-
1390
- default:
1391
- throw new Error(`No fallback available for ${functionName}`);
1392
582
  }
1393
583
  }
1394
584
 
1395
- /**
1396
- * SOLUTION 2: Safe ViewMode Switching
1397
- * Prevent crashes by validating and safely switching viewmodes
1398
- */
1399
- async setSafeViewMode(mode: string): Promise<any> {
1400
- const acceptedModes = Array.from(new Set(this.VIEWMODE_ALIASES.values())).sort();
1401
-
1402
- if (typeof mode !== 'string') {
1403
- return {
1404
- success: false,
1405
- error: 'View mode must be provided as a string',
1406
- acceptedModes
1407
- };
1408
- }
1409
-
1410
- const key = mode.trim().toLowerCase().replace(/[\s_-]+/g, '');
1411
- if (!key) {
585
+ /** Query feature flags */
586
+ async getFeatureFlags(): Promise<{ subsystems: { unrealEditor: boolean; levelEditor: boolean; editorActor: boolean; } }> {
587
+ if (process.env.MOCK_UNREAL_CONNECTION === 'true') {
1412
588
  return {
1413
- success: false,
1414
- error: 'View mode cannot be empty',
1415
- acceptedModes
589
+ subsystems: {
590
+ unrealEditor: true,
591
+ levelEditor: true,
592
+ editorActor: true
593
+ }
1416
594
  };
1417
595
  }
1418
596
 
1419
- const targetMode = this.VIEWMODE_ALIASES.get(key);
1420
- if (!targetMode) {
597
+ const bridge = this.getAutomationBridge();
598
+ try {
599
+ const resp: any = await bridge.sendAutomationRequest(
600
+ 'system_control',
601
+ { action: 'get_feature_flags' },
602
+ { timeoutMs: 15000 }
603
+ );
604
+ const raw = resp && typeof resp.result === 'object'
605
+ ? (resp.result as any)
606
+ : (resp?.result ?? resp ?? {});
607
+ const subs = raw && typeof raw.subsystems === 'object'
608
+ ? (raw.subsystems as any)
609
+ : {};
1421
610
  return {
1422
- success: false,
1423
- error: `Unknown view mode '${mode}'`,
1424
- acceptedModes
611
+ subsystems: {
612
+ unrealEditor: Boolean(subs.unrealEditor),
613
+ levelEditor: Boolean(subs.levelEditor),
614
+ editorActor: Boolean(subs.editorActor)
615
+ }
1425
616
  };
1426
- }
1427
-
1428
- if (this.HARD_BLOCKED_VIEWMODES.has(targetMode)) {
1429
- this.log.warn(`Viewmode '${targetMode}' is blocked for safety. Using alternative.`);
1430
- const alternative = this.getSafeAlternative(targetMode);
1431
- const altCommand = `viewmode ${alternative}`;
1432
- const altResult = await this.executeConsoleCommand(altCommand);
1433
- const altSummary = this.summarizeConsoleCommand(altCommand, altResult);
617
+ } catch (error) {
618
+ this.log.warn('getFeatureFlags failed', error);
1434
619
  return {
1435
- ...altSummary,
1436
- success: false,
1437
- requestedMode: targetMode,
1438
- viewMode: alternative,
1439
- message: `View mode '${targetMode}' is unsafe in remote sessions. Switched to '${alternative}'.`,
1440
- alternative
620
+ subsystems: {
621
+ unrealEditor: false,
622
+ levelEditor: false,
623
+ editorActor: false
624
+ }
1441
625
  };
1442
626
  }
1443
-
1444
- const command = `viewmode ${targetMode}`;
1445
- const rawResult = await this.executeConsoleCommand(command);
1446
- const summary = this.summarizeConsoleCommand(command, rawResult);
1447
- const response: any = {
1448
- ...summary,
1449
- success: summary.returnValue !== false,
1450
- requestedMode: targetMode,
1451
- viewMode: targetMode,
1452
- message: `View mode set to ${targetMode}`
1453
- };
1454
-
1455
- if (this.UNSAFE_VIEWMODES.includes(targetMode)) {
1456
- response.warning = `View mode '${targetMode}' may be unstable on some engine versions.`;
1457
- }
1458
-
1459
- if (summary.output && /unknown|invalid/i.test(summary.output)) {
1460
- response.success = false;
1461
- response.error = summary.output;
1462
- }
1463
-
1464
- return response;
1465
- }
1466
-
1467
- /**
1468
- * Get safe alternative for unsafe viewmodes
1469
- */
1470
- private getSafeAlternative(unsafeMode: string): string {
1471
- const alternatives: Record<string, string> = {
1472
- 'BaseColor': 'Unlit',
1473
- 'WorldNormal': 'Lit',
1474
- 'Metallic': 'Lit',
1475
- 'Specular': 'Lit',
1476
- 'Roughness': 'Lit',
1477
- 'SubsurfaceColor': 'Lit',
1478
- 'Opacity': 'Lit',
1479
- 'LightComplexity': 'LightingOnly',
1480
- 'ShaderComplexity': 'Wireframe',
1481
- 'CollisionPawn': 'Wireframe',
1482
- 'CollisionVisibility': 'Wireframe'
1483
- };
1484
-
1485
- return alternatives[unsafeMode] || 'Lit';
1486
627
  }
1487
628
 
1488
629
  /**
@@ -1490,182 +631,10 @@ print('RESULT:' + json.dumps(flags))
1490
631
  * Prevent rapid command execution that can overwhelm the engine
1491
632
  */
1492
633
  private async executeThrottledCommand<T>(
1493
- command: () => Promise<T>,
634
+ command: () => Promise<T>,
1494
635
  priority: number = 5
1495
636
  ): Promise<T> {
1496
- return new Promise((resolve, reject) => {
1497
- this.commandQueue.push({
1498
- command,
1499
- resolve,
1500
- reject,
1501
- priority
1502
- });
1503
-
1504
- // Sort by priority (lower number = higher priority)
1505
- this.commandQueue.sort((a, b) => a.priority - b.priority);
1506
-
1507
- // Process queue if not already processing
1508
- if (!this.isProcessing) {
1509
- this.processCommandQueue();
1510
- }
1511
- });
1512
- }
1513
-
1514
- /**
1515
- * Process command queue with appropriate delays
1516
- */
1517
- private async processCommandQueue(): Promise<void> {
1518
- if (this.isProcessing || this.commandQueue.length === 0) {
1519
- return;
1520
- }
1521
-
1522
- this.isProcessing = true;
1523
-
1524
- while (this.commandQueue.length > 0) {
1525
- const item = this.commandQueue.shift();
1526
- if (!item) continue; // Skip if undefined
1527
-
1528
- // Calculate delay based on time since last command
1529
- const timeSinceLastCommand = Date.now() - this.lastCommandTime;
1530
- const requiredDelay = this.calculateDelay(item.priority);
1531
-
1532
- if (timeSinceLastCommand < requiredDelay) {
1533
- await this.delay(requiredDelay - timeSinceLastCommand);
1534
- }
1535
-
1536
- try {
1537
- const result = await item.command();
1538
- item.resolve(result);
1539
- } catch (error: any) {
1540
- // Retry logic for transient failures
1541
- const msg = (error?.message || String(error)).toLowerCase();
1542
- const notConnected = msg.includes('not connected to unreal');
1543
- if (item.retryCount === undefined) {
1544
- item.retryCount = 0;
1545
- }
1546
-
1547
- if (!notConnected && item.retryCount < 3) {
1548
- item.retryCount++;
1549
- this.log.warn(`Command failed, retrying (${item.retryCount}/3)`);
1550
-
1551
- // Re-add to queue with increased priority
1552
- this.commandQueue.unshift({
1553
- command: item.command,
1554
- resolve: item.resolve,
1555
- reject: item.reject,
1556
- priority: Math.max(1, item.priority - 1),
1557
- retryCount: item.retryCount
1558
- });
1559
-
1560
- // Add extra delay before retry
1561
- await this.delay(500);
1562
- } else {
1563
- item.reject(error);
1564
- }
1565
- }
1566
-
1567
- this.lastCommandTime = Date.now();
1568
- }
1569
-
1570
- this.isProcessing = false;
1571
- }
1572
-
1573
- /**
1574
- * Calculate appropriate delay based on command priority and type
1575
- */
1576
- private calculateDelay(priority: number): number {
1577
- // Priority 1-3: Heavy operations (asset creation, lighting build)
1578
- if (priority <= 3) {
1579
- return this.MAX_COMMAND_DELAY;
1580
- }
1581
- // Priority 4-6: Medium operations (actor spawning, material changes)
1582
- else if (priority <= 6) {
1583
- return 200;
1584
- }
1585
- // Priority 8: Stat commands - need special handling
1586
- else if (priority === 8) {
1587
- // Check time since last stat command to avoid FindConsoleObject warnings
1588
- const timeSinceLastStat = Date.now() - this.lastStatCommandTime;
1589
- if (timeSinceLastStat < this.STAT_COMMAND_DELAY) {
1590
- return this.STAT_COMMAND_DELAY;
1591
- }
1592
- this.lastStatCommandTime = Date.now();
1593
- return 150;
1594
- }
1595
- // Priority 7,9-10: Light operations (console commands, queries)
1596
- else {
1597
- // For light operations, add some jitter to prevent thundering herd
1598
- const baseDelay = this.MIN_COMMAND_DELAY;
1599
- const jitter = Math.random() * 50; // Add up to 50ms random jitter
1600
- return baseDelay + jitter;
1601
- }
1602
- }
1603
-
1604
- /**
1605
- * SOLUTION 4: Enhanced Asset Creation
1606
- * Use Python scripting for complex asset creation that requires editor scripting
1607
- */
1608
- async createComplexAsset(assetType: string, params: Record<string, any>): Promise<any> {
1609
- const assetCreators: Record<string, string> = {
1610
- 'Material': 'MaterialFactoryNew',
1611
- 'MaterialInstance': 'MaterialInstanceConstantFactoryNew',
1612
- 'Blueprint': 'BlueprintFactory',
1613
- 'AnimationBlueprint': 'AnimBlueprintFactory',
1614
- 'ControlRig': 'ControlRigBlueprintFactory',
1615
- 'NiagaraSystem': 'NiagaraSystemFactoryNew',
1616
- 'NiagaraEmitter': 'NiagaraEmitterFactoryNew',
1617
- 'LandscapeGrassType': 'LandscapeGrassTypeFactory',
1618
- 'PhysicsAsset': 'PhysicsAssetFactory'
1619
- };
1620
-
1621
- const factoryClass = assetCreators[assetType];
1622
- if (!factoryClass) {
1623
- throw new Error(`Unknown asset type: ${assetType}`);
1624
- }
1625
-
1626
- const createParams = {
1627
- factory_class: factoryClass,
1628
- asset_class: `unreal.${assetType}`,
1629
- asset_name: params.name || `New${assetType}`,
1630
- package_path: params.path || '/Game/CreatedAssets',
1631
- ...params
1632
- };
1633
-
1634
- return this.executeEditorFunction('CREATE_ASSET', createParams);
1635
- }
1636
-
1637
- /**
1638
- * Start the command processor
1639
- */
1640
- private startCommandProcessor(): void {
1641
- // Periodic queue processing to handle any stuck commands
1642
- setInterval(() => {
1643
- if (!this.isProcessing && this.commandQueue.length > 0) {
1644
- this.processCommandQueue();
1645
- }
1646
- }, 1000);
1647
-
1648
- // Clean console cache every 5 minutes
1649
- setInterval(() => {
1650
- this.cleanConsoleCache();
1651
- }, this.CONSOLE_CACHE_TTL);
1652
- }
1653
-
1654
- /**
1655
- * Clean expired entries from console object cache
1656
- */
1657
- private cleanConsoleCache(): void {
1658
- const now = Date.now();
1659
- let cleaned = 0;
1660
- for (const [key, value] of this.consoleObjectCache.entries()) {
1661
- if (now - (value.timestamp || 0) > this.CONSOLE_CACHE_TTL) {
1662
- this.consoleObjectCache.delete(key);
1663
- cleaned++;
1664
- }
1665
- }
1666
- if (cleaned > 0) {
1667
- this.log.debug(`Cleaned ${cleaned} expired console cache entries`);
1668
- }
637
+ return this.commandQueue.execute(command, priority);
1669
638
  }
1670
639
 
1671
640
  /**
@@ -1674,63 +643,4 @@ print('RESULT:' + json.dumps(flags))
1674
643
  private delay(ms: number): Promise<void> {
1675
644
  return new Promise(resolve => setTimeout(resolve, ms));
1676
645
  }
1677
-
1678
- /**
1679
- * Batch command execution with proper delays
1680
- */
1681
- async executeBatch(commands: Array<{ command: string; priority?: number }>): Promise<any[]> {
1682
- return this.executeConsoleCommands(commands.map(cmd => cmd.command));
1683
- }
1684
-
1685
- /**
1686
- * Get safe console commands for common operations
1687
- */
1688
- getSafeCommands(): Record<string, string> {
1689
- return {
1690
- // Health check (safe, no side effects)
1691
- 'HealthCheck': 'echo MCP Server Health Check',
1692
-
1693
- // Performance monitoring (safe)
1694
- 'ShowFPS': 'stat unit', // Use 'stat unit' instead of 'stat fps'
1695
- 'ShowMemory': 'stat memory',
1696
- 'ShowGame': 'stat game',
1697
- 'ShowRendering': 'stat scenerendering',
1698
- 'ClearStats': 'stat none',
1699
-
1700
- // Safe viewmodes
1701
- 'ViewLit': 'viewmode lit',
1702
- 'ViewUnlit': 'viewmode unlit',
1703
- 'ViewWireframe': 'viewmode wireframe',
1704
- 'ViewDetailLighting': 'viewmode detaillighting',
1705
- 'ViewLightingOnly': 'viewmode lightingonly',
1706
-
1707
- // Safe show flags
1708
- 'ShowBounds': 'show bounds',
1709
- 'ShowCollision': 'show collision',
1710
- 'ShowNavigation': 'show navigation',
1711
- 'ShowFog': 'show fog',
1712
- 'ShowGrid': 'show grid',
1713
-
1714
- // PIE controls
1715
- 'PlayInEditor': 'play',
1716
- 'StopPlay': 'stop',
1717
- 'PausePlay': 'pause',
1718
-
1719
- // Time control
1720
- 'SlowMotion': 'slomo 0.5',
1721
- 'NormalSpeed': 'slomo 1',
1722
- 'FastForward': 'slomo 2',
1723
-
1724
- // Camera controls
1725
- 'CameraSpeed1': 'camspeed 1',
1726
- 'CameraSpeed4': 'camspeed 4',
1727
- 'CameraSpeed8': 'camspeed 8',
1728
-
1729
- // Rendering quality (safe)
1730
- 'LowQuality': 'sg.ViewDistanceQuality 0',
1731
- 'MediumQuality': 'sg.ViewDistanceQuality 1',
1732
- 'HighQuality': 'sg.ViewDistanceQuality 2',
1733
- 'EpicQuality': 'sg.ViewDistanceQuality 3'
1734
- };
1735
- }
1736
646
  }