unreal-engine-mcp-server 0.4.7 → 0.5.1

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