unreal-engine-mcp-server 0.4.7 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (438) hide show
  1. package/.env.example +26 -0
  2. package/.env.production +38 -7
  3. package/.eslintrc.json +0 -54
  4. package/.eslintrc.override.json +8 -0
  5. package/.github/ISSUE_TEMPLATE/bug_report.yml +94 -0
  6. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  7. package/.github/ISSUE_TEMPLATE/feature_request.yml +56 -0
  8. package/.github/copilot-instructions.md +478 -45
  9. package/.github/dependabot.yml +19 -0
  10. package/.github/labeler.yml +24 -0
  11. package/.github/labels.yml +70 -0
  12. package/.github/pull_request_template.md +42 -0
  13. package/.github/release-drafter.yml +148 -0
  14. package/.github/workflows/auto-merge.yml +38 -0
  15. package/.github/workflows/ci.yml +38 -0
  16. package/.github/workflows/dependency-review.yml +17 -0
  17. package/.github/workflows/gemini-issue-triage.yml +172 -0
  18. package/.github/workflows/greetings.yml +23 -0
  19. package/.github/workflows/labeler.yml +16 -0
  20. package/.github/workflows/links.yml +80 -0
  21. package/.github/workflows/pr-size-labeler.yml +137 -0
  22. package/.github/workflows/publish-mcp.yml +12 -7
  23. package/.github/workflows/release-drafter.yml +23 -0
  24. package/.github/workflows/release.yml +112 -0
  25. package/.github/workflows/semantic-pull-request.yml +35 -0
  26. package/.github/workflows/smoke-test.yml +36 -0
  27. package/.github/workflows/stale.yml +28 -0
  28. package/CHANGELOG.md +267 -31
  29. package/CONTRIBUTING.md +140 -0
  30. package/README.md +166 -71
  31. package/claude_desktop_config_example.json +7 -6
  32. package/dist/automation/bridge.d.ts +50 -0
  33. package/dist/automation/bridge.js +452 -0
  34. package/dist/automation/connection-manager.d.ts +23 -0
  35. package/dist/automation/connection-manager.js +107 -0
  36. package/dist/automation/handshake.d.ts +11 -0
  37. package/dist/automation/handshake.js +89 -0
  38. package/dist/automation/index.d.ts +3 -0
  39. package/dist/automation/index.js +3 -0
  40. package/dist/automation/message-handler.d.ts +12 -0
  41. package/dist/automation/message-handler.js +149 -0
  42. package/dist/automation/request-tracker.d.ts +25 -0
  43. package/dist/automation/request-tracker.js +98 -0
  44. package/dist/automation/types.d.ts +130 -0
  45. package/dist/automation/types.js +2 -0
  46. package/dist/cli.js +32 -5
  47. package/dist/config.d.ts +27 -0
  48. package/dist/config.js +60 -0
  49. package/dist/constants.d.ts +12 -0
  50. package/dist/constants.js +12 -0
  51. package/dist/graphql/resolvers.d.ts +268 -0
  52. package/dist/graphql/resolvers.js +743 -0
  53. package/dist/graphql/schema.d.ts +5 -0
  54. package/dist/graphql/schema.js +437 -0
  55. package/dist/graphql/server.d.ts +26 -0
  56. package/dist/graphql/server.js +115 -0
  57. package/dist/graphql/types.d.ts +7 -0
  58. package/dist/graphql/types.js +2 -0
  59. package/dist/handlers/resource-handlers.d.ts +20 -0
  60. package/dist/handlers/resource-handlers.js +180 -0
  61. package/dist/index.d.ts +31 -18
  62. package/dist/index.js +119 -619
  63. package/dist/prompts/index.js +4 -4
  64. package/dist/resources/actors.d.ts +17 -12
  65. package/dist/resources/actors.js +56 -76
  66. package/dist/resources/assets.d.ts +6 -14
  67. package/dist/resources/assets.js +115 -147
  68. package/dist/resources/levels.d.ts +13 -13
  69. package/dist/resources/levels.js +25 -34
  70. package/dist/server/resource-registry.d.ts +20 -0
  71. package/dist/server/resource-registry.js +37 -0
  72. package/dist/server/tool-registry.d.ts +23 -0
  73. package/dist/server/tool-registry.js +322 -0
  74. package/dist/server-setup.d.ts +21 -0
  75. package/dist/server-setup.js +111 -0
  76. package/dist/services/health-monitor.d.ts +34 -0
  77. package/dist/services/health-monitor.js +105 -0
  78. package/dist/services/metrics-server.d.ts +11 -0
  79. package/dist/services/metrics-server.js +105 -0
  80. package/dist/tools/actors.d.ts +147 -9
  81. package/dist/tools/actors.js +350 -311
  82. package/dist/tools/animation.d.ts +135 -4
  83. package/dist/tools/animation.js +510 -411
  84. package/dist/tools/assets.d.ts +117 -19
  85. package/dist/tools/assets.js +259 -284
  86. package/dist/tools/audio.d.ts +102 -42
  87. package/dist/tools/audio.js +272 -685
  88. package/dist/tools/base-tool.d.ts +17 -0
  89. package/dist/tools/base-tool.js +46 -0
  90. package/dist/tools/behavior-tree.d.ts +94 -0
  91. package/dist/tools/behavior-tree.js +39 -0
  92. package/dist/tools/blueprint/helpers.d.ts +29 -0
  93. package/dist/tools/blueprint/helpers.js +182 -0
  94. package/dist/tools/blueprint.d.ts +228 -118
  95. package/dist/tools/blueprint.js +685 -832
  96. package/dist/tools/consolidated-tool-definitions.d.ts +5462 -1781
  97. package/dist/tools/consolidated-tool-definitions.js +829 -496
  98. package/dist/tools/consolidated-tool-handlers.d.ts +2 -1
  99. package/dist/tools/consolidated-tool-handlers.js +211 -1026
  100. package/dist/tools/debug.d.ts +143 -85
  101. package/dist/tools/debug.js +234 -180
  102. package/dist/tools/dynamic-handler-registry.d.ts +11 -0
  103. package/dist/tools/dynamic-handler-registry.js +101 -0
  104. package/dist/tools/editor.d.ts +139 -18
  105. package/dist/tools/editor.js +239 -244
  106. package/dist/tools/engine.d.ts +10 -4
  107. package/dist/tools/engine.js +13 -5
  108. package/dist/tools/environment.d.ts +36 -0
  109. package/dist/tools/environment.js +267 -0
  110. package/dist/tools/foliage.d.ts +105 -14
  111. package/dist/tools/foliage.js +219 -331
  112. package/dist/tools/handlers/actor-handlers.d.ts +3 -0
  113. package/dist/tools/handlers/actor-handlers.js +232 -0
  114. package/dist/tools/handlers/animation-handlers.d.ts +3 -0
  115. package/dist/tools/handlers/animation-handlers.js +185 -0
  116. package/dist/tools/handlers/argument-helper.d.ts +16 -0
  117. package/dist/tools/handlers/argument-helper.js +80 -0
  118. package/dist/tools/handlers/asset-handlers.d.ts +3 -0
  119. package/dist/tools/handlers/asset-handlers.js +496 -0
  120. package/dist/tools/handlers/audio-handlers.d.ts +3 -0
  121. package/dist/tools/handlers/audio-handlers.js +166 -0
  122. package/dist/tools/handlers/blueprint-handlers.d.ts +4 -0
  123. package/dist/tools/handlers/blueprint-handlers.js +358 -0
  124. package/dist/tools/handlers/common-handlers.d.ts +14 -0
  125. package/dist/tools/handlers/common-handlers.js +56 -0
  126. package/dist/tools/handlers/editor-handlers.d.ts +3 -0
  127. package/dist/tools/handlers/editor-handlers.js +119 -0
  128. package/dist/tools/handlers/effect-handlers.d.ts +3 -0
  129. package/dist/tools/handlers/effect-handlers.js +171 -0
  130. package/dist/tools/handlers/environment-handlers.d.ts +3 -0
  131. package/dist/tools/handlers/environment-handlers.js +170 -0
  132. package/dist/tools/handlers/graph-handlers.d.ts +3 -0
  133. package/dist/tools/handlers/graph-handlers.js +90 -0
  134. package/dist/tools/handlers/input-handlers.d.ts +3 -0
  135. package/dist/tools/handlers/input-handlers.js +21 -0
  136. package/dist/tools/handlers/inspect-handlers.d.ts +3 -0
  137. package/dist/tools/handlers/inspect-handlers.js +383 -0
  138. package/dist/tools/handlers/level-handlers.d.ts +3 -0
  139. package/dist/tools/handlers/level-handlers.js +237 -0
  140. package/dist/tools/handlers/lighting-handlers.d.ts +3 -0
  141. package/dist/tools/handlers/lighting-handlers.js +144 -0
  142. package/dist/tools/handlers/performance-handlers.d.ts +3 -0
  143. package/dist/tools/handlers/performance-handlers.js +130 -0
  144. package/dist/tools/handlers/pipeline-handlers.d.ts +3 -0
  145. package/dist/tools/handlers/pipeline-handlers.js +110 -0
  146. package/dist/tools/handlers/sequence-handlers.d.ts +3 -0
  147. package/dist/tools/handlers/sequence-handlers.js +376 -0
  148. package/dist/tools/handlers/system-handlers.d.ts +4 -0
  149. package/dist/tools/handlers/system-handlers.js +506 -0
  150. package/dist/tools/input.d.ts +19 -0
  151. package/dist/tools/input.js +89 -0
  152. package/dist/tools/introspection.d.ts +103 -40
  153. package/dist/tools/introspection.js +425 -568
  154. package/dist/tools/landscape.d.ts +97 -36
  155. package/dist/tools/landscape.js +280 -409
  156. package/dist/tools/level.d.ts +130 -10
  157. package/dist/tools/level.js +639 -675
  158. package/dist/tools/lighting.d.ts +77 -38
  159. package/dist/tools/lighting.js +441 -943
  160. package/dist/tools/logs.d.ts +3 -3
  161. package/dist/tools/logs.js +5 -57
  162. package/dist/tools/materials.d.ts +91 -24
  163. package/dist/tools/materials.js +190 -118
  164. package/dist/tools/niagara.d.ts +149 -39
  165. package/dist/tools/niagara.js +232 -182
  166. package/dist/tools/performance.d.ts +27 -12
  167. package/dist/tools/performance.js +204 -122
  168. package/dist/tools/physics.d.ts +32 -77
  169. package/dist/tools/physics.js +171 -582
  170. package/dist/tools/property-dictionary.d.ts +13 -0
  171. package/dist/tools/property-dictionary.js +82 -0
  172. package/dist/tools/sequence.d.ts +73 -48
  173. package/dist/tools/sequence.js +196 -748
  174. package/dist/tools/tool-definition-utils.d.ts +59 -0
  175. package/dist/tools/tool-definition-utils.js +35 -0
  176. package/dist/tools/ui.d.ts +66 -34
  177. package/dist/tools/ui.js +134 -214
  178. package/dist/types/env.d.ts +0 -3
  179. package/dist/types/env.js +0 -7
  180. package/dist/types/tool-interfaces.d.ts +898 -0
  181. package/dist/types/tool-interfaces.js +2 -0
  182. package/dist/types/tool-types.d.ts +183 -19
  183. package/dist/types/tool-types.js +0 -4
  184. package/dist/unreal-bridge.d.ts +24 -131
  185. package/dist/unreal-bridge.js +364 -1506
  186. package/dist/utils/command-validator.d.ts +9 -0
  187. package/dist/utils/command-validator.js +67 -0
  188. package/dist/utils/elicitation.d.ts +1 -1
  189. package/dist/utils/elicitation.js +12 -15
  190. package/dist/utils/error-handler.d.ts +2 -51
  191. package/dist/utils/error-handler.js +11 -87
  192. package/dist/utils/ini-reader.d.ts +3 -0
  193. package/dist/utils/ini-reader.js +69 -0
  194. package/dist/utils/logger.js +9 -6
  195. package/dist/utils/normalize.d.ts +3 -0
  196. package/dist/utils/normalize.js +56 -0
  197. package/dist/utils/response-factory.d.ts +7 -0
  198. package/dist/utils/response-factory.js +33 -0
  199. package/dist/utils/response-validator.d.ts +3 -24
  200. package/dist/utils/response-validator.js +130 -81
  201. package/dist/utils/result-helpers.d.ts +4 -5
  202. package/dist/utils/result-helpers.js +15 -16
  203. package/dist/utils/safe-json.js +5 -11
  204. package/dist/utils/unreal-command-queue.d.ts +24 -0
  205. package/dist/utils/unreal-command-queue.js +120 -0
  206. package/dist/utils/validation.d.ts +0 -40
  207. package/dist/utils/validation.js +1 -78
  208. package/dist/wasm/index.d.ts +70 -0
  209. package/dist/wasm/index.js +535 -0
  210. package/docs/GraphQL-API.md +888 -0
  211. package/docs/Migration-Guide-v0.5.0.md +692 -0
  212. package/docs/Roadmap.md +53 -0
  213. package/docs/WebAssembly-Integration.md +628 -0
  214. package/docs/editor-plugin-extension.md +370 -0
  215. package/docs/handler-mapping.md +242 -0
  216. package/docs/native-automation-progress.md +128 -0
  217. package/docs/testing-guide.md +423 -0
  218. package/mcp-config-example.json +6 -6
  219. package/package.json +60 -27
  220. package/plugins/McpAutomationBridge/Config/FilterPlugin.ini +8 -0
  221. package/plugins/McpAutomationBridge/McpAutomationBridge.uplugin +64 -0
  222. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/McpAutomationBridge.Build.cs +189 -0
  223. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.cpp +22 -0
  224. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.h +30 -0
  225. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeHelpers.h +1983 -0
  226. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeModule.cpp +72 -0
  227. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSettings.cpp +46 -0
  228. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +581 -0
  229. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +2394 -0
  230. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetQueryHandlers.cpp +300 -0
  231. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetWorkflowHandlers.cpp +2807 -0
  232. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AudioHandlers.cpp +1087 -0
  233. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BehaviorTreeHandlers.cpp +488 -0
  234. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.cpp +643 -0
  235. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.h +31 -0
  236. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +1184 -0
  237. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +5652 -0
  238. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers_List.cpp +152 -0
  239. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ControlHandlers.cpp +2614 -0
  240. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_DebugHandlers.cpp +42 -0
  241. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EditorFunctionHandlers.cpp +1237 -0
  242. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +1701 -0
  243. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +2145 -0
  244. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_FoliageHandlers.cpp +954 -0
  245. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InputHandlers.cpp +209 -0
  246. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InsightsHandlers.cpp +41 -0
  247. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LandscapeHandlers.cpp +1164 -0
  248. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LevelHandlers.cpp +762 -0
  249. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +634 -0
  250. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LogHandlers.cpp +136 -0
  251. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_MaterialGraphHandlers.cpp +494 -0
  252. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraGraphHandlers.cpp +278 -0
  253. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraHandlers.cpp +625 -0
  254. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PerformanceHandlers.cpp +401 -0
  255. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PipelineHandlers.cpp +67 -0
  256. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +735 -0
  257. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PropertyHandlers.cpp +2634 -0
  258. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_RenderHandlers.cpp +189 -0
  259. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.cpp +917 -0
  260. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.h +39 -0
  261. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +2670 -0
  262. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequencerHandlers.cpp +519 -0
  263. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_TestHandlers.cpp +38 -0
  264. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_UiHandlers.cpp +668 -0
  265. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_WorldPartitionHandlers.cpp +346 -0
  266. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.cpp +1330 -0
  267. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.h +149 -0
  268. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +783 -0
  269. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSettings.h +115 -0
  270. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSubsystem.h +796 -0
  271. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpConnectionManager.h +117 -0
  272. package/scripts/check-unreal-connection.mjs +19 -0
  273. package/scripts/clean-tmp.js +23 -0
  274. package/scripts/patch-wasm.js +26 -0
  275. package/scripts/run-all-tests.mjs +131 -0
  276. package/scripts/smoke-test.ts +94 -0
  277. package/scripts/sync-mcp-plugin.js +143 -0
  278. package/scripts/test-no-plugin-alternates.mjs +113 -0
  279. package/scripts/validate-server.js +46 -0
  280. package/scripts/verify-automation-bridge.js +200 -0
  281. package/server.json +57 -21
  282. package/src/automation/bridge.ts +558 -0
  283. package/src/automation/connection-manager.ts +130 -0
  284. package/src/automation/handshake.ts +99 -0
  285. package/src/automation/index.ts +2 -0
  286. package/src/automation/message-handler.ts +167 -0
  287. package/src/automation/request-tracker.ts +123 -0
  288. package/src/automation/types.ts +107 -0
  289. package/src/cli.ts +33 -6
  290. package/src/config.ts +73 -0
  291. package/src/constants.ts +12 -0
  292. package/src/graphql/resolvers.ts +1010 -0
  293. package/src/graphql/schema.ts +452 -0
  294. package/src/graphql/server.ts +154 -0
  295. package/src/graphql/types.ts +7 -0
  296. package/src/handlers/resource-handlers.ts +186 -0
  297. package/src/index.ts +152 -663
  298. package/src/prompts/index.ts +4 -4
  299. package/src/resources/actors.ts +58 -76
  300. package/src/resources/assets.ts +147 -134
  301. package/src/resources/levels.ts +28 -33
  302. package/src/server/resource-registry.ts +47 -0
  303. package/src/server/tool-registry.ts +354 -0
  304. package/src/server-setup.ts +148 -0
  305. package/src/services/health-monitor.ts +132 -0
  306. package/src/services/metrics-server.ts +142 -0
  307. package/src/tools/actors.ts +417 -322
  308. package/src/tools/animation.ts +671 -461
  309. package/src/tools/assets.ts +353 -289
  310. package/src/tools/audio.ts +323 -766
  311. package/src/tools/base-tool.ts +52 -0
  312. package/src/tools/behavior-tree.ts +45 -0
  313. package/src/tools/blueprint/helpers.ts +189 -0
  314. package/src/tools/blueprint.ts +787 -965
  315. package/src/tools/consolidated-tool-definitions.ts +993 -515
  316. package/src/tools/consolidated-tool-handlers.ts +272 -1139
  317. package/src/tools/debug.ts +292 -187
  318. package/src/tools/dynamic-handler-registry.ts +151 -0
  319. package/src/tools/editor.ts +309 -246
  320. package/src/tools/engine.ts +14 -3
  321. package/src/tools/environment.ts +287 -0
  322. package/src/tools/foliage.ts +314 -379
  323. package/src/tools/handlers/actor-handlers.ts +271 -0
  324. package/src/tools/handlers/animation-handlers.ts +237 -0
  325. package/src/tools/handlers/argument-helper.ts +142 -0
  326. package/src/tools/handlers/asset-handlers.ts +532 -0
  327. package/src/tools/handlers/audio-handlers.ts +194 -0
  328. package/src/tools/handlers/blueprint-handlers.ts +380 -0
  329. package/src/tools/handlers/common-handlers.ts +87 -0
  330. package/src/tools/handlers/editor-handlers.ts +123 -0
  331. package/src/tools/handlers/effect-handlers.ts +220 -0
  332. package/src/tools/handlers/environment-handlers.ts +183 -0
  333. package/src/tools/handlers/graph-handlers.ts +116 -0
  334. package/src/tools/handlers/input-handlers.ts +28 -0
  335. package/src/tools/handlers/inspect-handlers.ts +450 -0
  336. package/src/tools/handlers/level-handlers.ts +252 -0
  337. package/src/tools/handlers/lighting-handlers.ts +147 -0
  338. package/src/tools/handlers/performance-handlers.ts +132 -0
  339. package/src/tools/handlers/pipeline-handlers.ts +127 -0
  340. package/src/tools/handlers/sequence-handlers.ts +415 -0
  341. package/src/tools/handlers/system-handlers.ts +564 -0
  342. package/src/tools/input.ts +101 -0
  343. package/src/tools/introspection.ts +493 -584
  344. package/src/tools/landscape.ts +394 -489
  345. package/src/tools/level.ts +752 -694
  346. package/src/tools/lighting.ts +583 -984
  347. package/src/tools/logs.ts +9 -57
  348. package/src/tools/materials.ts +231 -121
  349. package/src/tools/niagara.ts +293 -168
  350. package/src/tools/performance.ts +320 -168
  351. package/src/tools/physics.ts +268 -613
  352. package/src/tools/property-dictionary.ts +98 -0
  353. package/src/tools/sequence.ts +255 -815
  354. package/src/tools/tool-definition-utils.ts +35 -0
  355. package/src/tools/ui.ts +207 -283
  356. package/src/types/env.ts +0 -10
  357. package/src/types/tool-interfaces.ts +250 -0
  358. package/src/types/tool-types.ts +243 -21
  359. package/src/unreal-bridge.ts +460 -1550
  360. package/src/utils/command-validator.ts +75 -0
  361. package/src/utils/elicitation.ts +10 -7
  362. package/src/utils/error-handler.ts +14 -90
  363. package/src/utils/ini-reader.ts +86 -0
  364. package/src/utils/logger.ts +8 -3
  365. package/src/utils/normalize.ts +60 -0
  366. package/src/utils/response-factory.ts +39 -0
  367. package/src/utils/response-validator.ts +176 -56
  368. package/src/utils/result-helpers.ts +21 -19
  369. package/src/utils/safe-json.ts +14 -11
  370. package/src/utils/unreal-command-queue.ts +152 -0
  371. package/src/utils/validation.ts +4 -1
  372. package/src/wasm/index.ts +838 -0
  373. package/test-server.mjs +100 -0
  374. package/tests/run-unreal-tool-tests.mjs +242 -14
  375. package/tests/test-animation.mjs +44 -0
  376. package/tests/test-asset-advanced.mjs +82 -0
  377. package/tests/test-asset-errors.mjs +35 -0
  378. package/tests/test-audio.mjs +219 -0
  379. package/tests/test-automation-timeouts.mjs +98 -0
  380. package/tests/test-behavior-tree.mjs +261 -0
  381. package/tests/test-blueprint-events.mjs +35 -0
  382. package/tests/test-blueprint-graph.mjs +79 -0
  383. package/tests/test-blueprint.mjs +577 -0
  384. package/tests/test-client-mode.mjs +86 -0
  385. package/tests/test-console-command.mjs +56 -0
  386. package/tests/test-control-actor.mjs +425 -0
  387. package/tests/test-control-editor.mjs +80 -0
  388. package/tests/test-extra-tools.mjs +38 -0
  389. package/tests/test-graphql.mjs +322 -0
  390. package/tests/test-inspect.mjs +72 -0
  391. package/tests/test-landscape.mjs +60 -0
  392. package/tests/test-manage-asset.mjs +438 -0
  393. package/tests/test-manage-level.mjs +70 -0
  394. package/tests/test-materials.mjs +356 -0
  395. package/tests/test-niagara.mjs +185 -0
  396. package/tests/test-no-inline-python.mjs +122 -0
  397. package/tests/test-plugin-handshake.mjs +82 -0
  398. package/tests/test-render.mjs +33 -0
  399. package/tests/test-runner.mjs +933 -0
  400. package/tests/test-search-assets.mjs +66 -0
  401. package/tests/test-sequence.mjs +68 -0
  402. package/tests/test-system.mjs +57 -0
  403. package/tests/test-wasm.mjs +193 -0
  404. package/tests/test-world-partition.mjs +215 -0
  405. package/tsconfig.json +3 -3
  406. package/wasm/Cargo.lock +363 -0
  407. package/wasm/Cargo.toml +42 -0
  408. package/wasm/LICENSE +21 -0
  409. package/wasm/README.md +253 -0
  410. package/wasm/src/dependency_resolver.rs +377 -0
  411. package/wasm/src/lib.rs +153 -0
  412. package/wasm/src/property_parser.rs +271 -0
  413. package/wasm/src/transform_math.rs +396 -0
  414. package/wasm/tests/integration.rs +109 -0
  415. package/.github/workflows/smithery-build.yml +0 -29
  416. package/dist/tools/build_environment_advanced.d.ts +0 -65
  417. package/dist/tools/build_environment_advanced.js +0 -633
  418. package/dist/tools/rc.d.ts +0 -110
  419. package/dist/tools/rc.js +0 -437
  420. package/dist/tools/visual.d.ts +0 -40
  421. package/dist/tools/visual.js +0 -282
  422. package/dist/utils/http.d.ts +0 -6
  423. package/dist/utils/http.js +0 -151
  424. package/dist/utils/python-output.d.ts +0 -18
  425. package/dist/utils/python-output.js +0 -290
  426. package/dist/utils/python.d.ts +0 -2
  427. package/dist/utils/python.js +0 -4
  428. package/dist/utils/stdio-redirect.d.ts +0 -2
  429. package/dist/utils/stdio-redirect.js +0 -20
  430. package/docs/unreal-tool-test-cases.md +0 -574
  431. package/smithery.yaml +0 -29
  432. package/src/tools/build_environment_advanced.ts +0 -732
  433. package/src/tools/rc.ts +0 -515
  434. package/src/tools/visual.ts +0 -281
  435. package/src/utils/http.ts +0 -187
  436. package/src/utils/python-output.ts +0 -351
  437. package/src/utils/python.ts +0 -3
  438. package/src/utils/stdio-redirect.ts +0 -18
@@ -1,397 +1,575 @@
1
- // Level management tools for Unreal Engine
2
- import { UnrealBridge } from '../unreal-bridge.js';
3
- import {
4
- coerceBoolean,
5
- coerceNumber,
6
- coerceString,
7
- interpretStandardResult
8
- } from '../utils/result-helpers.js';
9
-
10
- export class LevelTools {
11
- constructor(private bridge: UnrealBridge) {}
12
-
13
- // Load level (using LevelEditorSubsystem to avoid crashes)
1
+ import { BaseTool } from './base-tool.js';
2
+ import { ILevelTools } from '../types/tool-interfaces.js';
3
+
4
+ type LevelExportRecord = { target: string; timestamp: number; note?: string };
5
+ type ManagedLevelRecord = {
6
+ path: string;
7
+ name: string;
8
+ partitioned: boolean;
9
+ streaming: boolean;
10
+ loaded: boolean;
11
+ visible: boolean;
12
+ createdAt: number;
13
+ lastSavedAt?: number;
14
+ metadata?: Record<string, unknown>;
15
+ exports: LevelExportRecord[];
16
+ lights: Array<{ name: string; type: string; createdAt: number; details?: Record<string, unknown> }>;
17
+ };
18
+
19
+ export class LevelTools extends BaseTool implements ILevelTools {
20
+ private managedLevels = new Map<string, ManagedLevelRecord>();
21
+ private listCache?: { result: { success: true; message: string; count: number; levels: any[] }; timestamp: number };
22
+ private readonly LIST_CACHE_TTL_MS = 750;
23
+ private currentLevelPath?: string;
24
+
25
+ private invalidateListCache() {
26
+ this.listCache = undefined;
27
+ }
28
+
29
+ private normalizeLevelPath(rawPath: string | undefined): { path: string; name: string } {
30
+ if (!rawPath) {
31
+ return { path: '/Game/Maps/Untitled', name: 'Untitled' };
32
+ }
33
+
34
+ let formatted = rawPath.replace(/\\/g, '/').trim();
35
+ if (!formatted.startsWith('/')) {
36
+ formatted = formatted.startsWith('Game/') ? `/${formatted}` : `/Game/${formatted.replace(/^\/?Game\//i, '')}`;
37
+ }
38
+ if (!formatted.startsWith('/Game/')) {
39
+ formatted = `/Game/${formatted.replace(/^\/+/, '')}`;
40
+ }
41
+ formatted = formatted.replace(/\.umap$/i, '');
42
+ if (formatted.endsWith('/')) {
43
+ formatted = formatted.slice(0, -1);
44
+ }
45
+ const segments = formatted.split('/').filter(Boolean);
46
+ const lastSegment = segments[segments.length - 1] ?? 'Untitled';
47
+ const name = lastSegment.includes('.') ? lastSegment.split('.').pop() ?? lastSegment : lastSegment;
48
+ return { path: formatted, name: name || 'Untitled' };
49
+ }
50
+
51
+ private ensureRecord(path: string, seed?: Partial<ManagedLevelRecord>): ManagedLevelRecord {
52
+ const normalized = this.normalizeLevelPath(path);
53
+ let record = this.managedLevels.get(normalized.path);
54
+ if (!record) {
55
+ record = {
56
+ path: normalized.path,
57
+ name: seed?.name ?? normalized.name,
58
+ partitioned: seed?.partitioned ?? false,
59
+ streaming: seed?.streaming ?? false,
60
+ loaded: seed?.loaded ?? false,
61
+ visible: seed?.visible ?? false,
62
+ createdAt: seed?.createdAt ?? Date.now(),
63
+ lastSavedAt: seed?.lastSavedAt,
64
+ metadata: seed?.metadata ? { ...seed.metadata } : undefined,
65
+ exports: seed?.exports ? [...seed.exports] : [],
66
+ lights: seed?.lights ? [...seed.lights] : []
67
+ };
68
+ this.managedLevels.set(normalized.path, record);
69
+ this.invalidateListCache();
70
+ }
71
+ return record;
72
+ }
73
+
74
+ private mutateRecord(path: string | undefined, updates: Partial<ManagedLevelRecord>): ManagedLevelRecord | undefined {
75
+ if (!path || !path.trim()) {
76
+ return undefined;
77
+ }
78
+
79
+ const record = this.ensureRecord(path, updates);
80
+ let changed = false;
81
+
82
+ if (updates.name !== undefined && updates.name !== record.name) {
83
+ record.name = updates.name;
84
+ changed = true;
85
+ }
86
+ if (updates.partitioned !== undefined && updates.partitioned !== record.partitioned) {
87
+ record.partitioned = updates.partitioned;
88
+ changed = true;
89
+ }
90
+ if (updates.streaming !== undefined && updates.streaming !== record.streaming) {
91
+ record.streaming = updates.streaming;
92
+ changed = true;
93
+ }
94
+ if (updates.loaded !== undefined && updates.loaded !== record.loaded) {
95
+ record.loaded = updates.loaded;
96
+ changed = true;
97
+ }
98
+ if (updates.visible !== undefined && updates.visible !== record.visible) {
99
+ record.visible = updates.visible;
100
+ changed = true;
101
+ }
102
+ if (updates.createdAt !== undefined && updates.createdAt !== record.createdAt) {
103
+ record.createdAt = updates.createdAt;
104
+ changed = true;
105
+ }
106
+ if (updates.lastSavedAt !== undefined && updates.lastSavedAt !== record.lastSavedAt) {
107
+ record.lastSavedAt = updates.lastSavedAt;
108
+ changed = true;
109
+ }
110
+ if (updates.metadata) {
111
+ record.metadata = { ...(record.metadata ?? {}), ...updates.metadata };
112
+ changed = true;
113
+ }
114
+ if (updates.exports && updates.exports.length > 0) {
115
+ record.exports = [...record.exports, ...updates.exports];
116
+ changed = true;
117
+ }
118
+ if (updates.lights && updates.lights.length > 0) {
119
+ record.lights = [...record.lights, ...updates.lights];
120
+ changed = true;
121
+ }
122
+
123
+ if (changed) {
124
+ this.invalidateListCache();
125
+ }
126
+
127
+ return record;
128
+ }
129
+
130
+ private getRecord(path: string | undefined): ManagedLevelRecord | undefined {
131
+ if (!path || !path.trim()) {
132
+ return undefined;
133
+ }
134
+ const normalized = this.normalizeLevelPath(path);
135
+ return this.managedLevels.get(normalized.path);
136
+ }
137
+
138
+ private resolveLevelPath(explicit?: string): string | undefined {
139
+ if (explicit && explicit.trim()) {
140
+ return this.normalizeLevelPath(explicit).path;
141
+ }
142
+ return this.currentLevelPath;
143
+ }
144
+
145
+ private removeRecord(path: string) {
146
+ const normalized = this.normalizeLevelPath(path);
147
+ if (this.managedLevels.delete(normalized.path)) {
148
+ if (this.currentLevelPath === normalized.path) {
149
+ this.currentLevelPath = undefined;
150
+ }
151
+ this.invalidateListCache();
152
+ }
153
+ }
154
+
155
+ private listManagedLevels(): { success: true; message: string; count: number; levels: Array<Record<string, unknown>> } {
156
+ const now = Date.now();
157
+ if (this.listCache && now - this.listCache.timestamp < this.LIST_CACHE_TTL_MS) {
158
+ return this.listCache.result;
159
+ }
160
+
161
+ const levels = Array.from(this.managedLevels.values()).map((record) => ({
162
+ path: record.path,
163
+ name: record.name,
164
+ partitioned: record.partitioned,
165
+ streaming: record.streaming,
166
+ loaded: record.loaded,
167
+ visible: record.visible,
168
+ createdAt: record.createdAt,
169
+ lastSavedAt: record.lastSavedAt,
170
+ exports: record.exports,
171
+ lightCount: record.lights.length
172
+ }));
173
+
174
+ const result = { success: true as const, message: 'Managed levels listed', count: levels.length, levels };
175
+ this.listCache = { result, timestamp: now };
176
+ return result;
177
+ }
178
+
179
+ private summarizeLevel(path: string): Record<string, unknown> {
180
+ const record = this.getRecord(path);
181
+ if (!record) {
182
+ return { success: false, error: `Level not tracked: ${path}` };
183
+ }
184
+
185
+ return {
186
+ success: true,
187
+ message: 'Level summary ready',
188
+ path: record.path,
189
+ name: record.name,
190
+ partitioned: record.partitioned,
191
+ streaming: record.streaming,
192
+ loaded: record.loaded,
193
+ visible: record.visible,
194
+ createdAt: record.createdAt,
195
+ lastSavedAt: record.lastSavedAt,
196
+ exports: record.exports,
197
+ lights: record.lights,
198
+ metadata: record.metadata
199
+ };
200
+ }
201
+
202
+ private setCurrentLevel(path: string) {
203
+ const normalized = this.normalizeLevelPath(path);
204
+ this.currentLevelPath = normalized.path;
205
+ this.ensureRecord(normalized.path, { loaded: true, visible: true });
206
+ }
207
+
208
+ async listLevels() {
209
+ // Try to get actual levels from UE via automation bridge
210
+ try {
211
+ const response = await this.sendAutomationRequest('list_levels', {}, {
212
+ timeoutMs: 10000
213
+ });
214
+
215
+ if (response && response.success !== false) {
216
+ // Also include managed levels for backwards compatibility and immediate visibility
217
+ const managed = this.listManagedLevels();
218
+
219
+ // Merge managed levels into the main list if not already present
220
+ const ueLevels = (response.allMaps || []) as any[];
221
+ const managedOnly = managed.levels.filter(m => !ueLevels.some(u => u.path === m.path));
222
+ const finalLevels = [...ueLevels, ...managedOnly];
223
+
224
+ const result: Record<string, unknown> = {
225
+ success: true,
226
+ message: 'Levels listed from Unreal Engine',
227
+ levels: finalLevels,
228
+ currentMap: response.currentMap,
229
+ currentMapPath: response.currentMapPath,
230
+ currentWorldLevels: response.currentWorldLevels || [],
231
+ data: {
232
+ levels: finalLevels,
233
+ count: finalLevels.length
234
+ },
235
+ ...response,
236
+ managedLevels: managed.levels,
237
+ managedLevelCount: managed.count
238
+ };
239
+
240
+ return result;
241
+ }
242
+ } catch {
243
+ // Fall back to managed levels if automation bridge fails
244
+ }
245
+
246
+ // Fallback to locally managed levels
247
+ return this.listManagedLevels();
248
+ }
249
+
250
+ async getLevelSummary(levelPath?: string) {
251
+ const resolved = this.resolveLevelPath(levelPath);
252
+ if (!resolved) {
253
+ return { success: false, error: 'No level specified' };
254
+ }
255
+ return this.summarizeLevel(resolved);
256
+ }
257
+
258
+ registerLight(levelPath: string | undefined, info: { name: string; type: string; details?: Record<string, unknown> }) {
259
+ const resolved = this.resolveLevelPath(levelPath);
260
+ if (!resolved) {
261
+ return;
262
+ }
263
+ this.mutateRecord(resolved, {
264
+ lights: [
265
+ {
266
+ name: info.name,
267
+ type: info.type,
268
+ createdAt: Date.now(),
269
+ details: info.details
270
+ }
271
+ ]
272
+ });
273
+ }
274
+
275
+ async exportLevel(params: { levelPath?: string; exportPath: string; note?: string; timeoutMs?: number }) {
276
+ const resolved = this.resolveLevelPath(params.levelPath);
277
+ if (!resolved) {
278
+ return { success: false, error: 'No level specified for export' };
279
+ }
280
+
281
+ try {
282
+ const res = await this.sendAutomationRequest('manage_level', {
283
+ action: 'export_level',
284
+ levelPath: resolved,
285
+ exportPath: params.exportPath
286
+ }, { timeoutMs: params.timeoutMs ?? 300000 });
287
+
288
+ if ((res as any)?.success === false) {
289
+ return {
290
+ success: false,
291
+ error: (res as any).error || (res as any).message || 'Export failed',
292
+ levelPath: resolved,
293
+ exportPath: params.exportPath,
294
+ details: res
295
+ };
296
+ }
297
+
298
+ return {
299
+ success: true,
300
+ message: `Level exported to ${params.exportPath}`,
301
+ levelPath: resolved,
302
+ exportPath: params.exportPath,
303
+ details: res
304
+ };
305
+ } catch (e: any) {
306
+ return { success: false, error: `Export failed: ${e.message}` };
307
+ }
308
+ }
309
+
310
+ async importLevel(params: { packagePath: string; destinationPath?: string; streaming?: boolean; timeoutMs?: number }) {
311
+ const destination = params.destinationPath
312
+ ? this.normalizeLevelPath(params.destinationPath)
313
+ : this.normalizeLevelPath(`/Game/Maps/Imported_${Math.floor(Date.now() / 1000)}`);
314
+
315
+ try {
316
+ const res = await this.sendAutomationRequest('manage_level', {
317
+ action: 'import_level',
318
+ packagePath: params.packagePath,
319
+ destinationPath: destination.path
320
+ }, { timeoutMs: params.timeoutMs ?? 300000 });
321
+
322
+ if ((res as any)?.success === false) {
323
+ return {
324
+ success: false,
325
+ error: (res as any).error || (res as any).message || 'Import failed',
326
+ levelPath: destination.path,
327
+ details: res
328
+ };
329
+ }
330
+
331
+ return {
332
+ success: true,
333
+ message: `Level imported to ${destination.path}`,
334
+ levelPath: destination.path,
335
+ partitioned: true,
336
+ streaming: Boolean(params.streaming),
337
+ details: res
338
+ };
339
+ } catch (e: any) {
340
+ return { success: false, error: `Import failed: ${e.message}` };
341
+ }
342
+ }
343
+
344
+ async saveLevelAs(params: { sourcePath?: string; targetPath: string }) {
345
+ const source = this.resolveLevelPath(params.sourcePath);
346
+ const target = this.normalizeLevelPath(params.targetPath);
347
+
348
+ // Delegate to automation bridge
349
+ try {
350
+ const response = await this.sendAutomationRequest('manage_level', {
351
+ action: 'save_level_as',
352
+ savePath: target.path
353
+ }, {
354
+ timeoutMs: 60000
355
+ });
356
+
357
+ if (response.success === false) {
358
+ return { success: false, error: response.error || response.message || 'Failed to save level as' };
359
+ }
360
+
361
+ // If successful, update local state
362
+ if (!source) {
363
+ // If no source known, just ensure target record
364
+ this.ensureRecord(target.path, {
365
+ name: target.name,
366
+ loaded: true,
367
+ visible: true,
368
+ createdAt: Date.now(),
369
+ lastSavedAt: Date.now()
370
+ });
371
+ } else {
372
+ const sourceRecord = this.getRecord(source);
373
+ const now = Date.now();
374
+ this.ensureRecord(target.path, {
375
+ name: target.name,
376
+ partitioned: sourceRecord?.partitioned ?? true,
377
+ streaming: sourceRecord?.streaming ?? false,
378
+ loaded: true,
379
+ visible: true,
380
+ metadata: { ...(sourceRecord?.metadata ?? {}), savedFrom: source },
381
+ exports: sourceRecord?.exports ?? [],
382
+ lights: sourceRecord?.lights ?? [],
383
+ createdAt: sourceRecord?.createdAt ?? now,
384
+ lastSavedAt: now
385
+ });
386
+ }
387
+
388
+ this.setCurrentLevel(target.path);
389
+
390
+ return {
391
+ success: true,
392
+ message: response.message || `Level saved as ${target.path}`,
393
+ levelPath: target.path
394
+ };
395
+ } catch (error) {
396
+ return { success: false, error: `Failed to save level as: ${error instanceof Error ? error.message : String(error)}` };
397
+ }
398
+ }
399
+
400
+ async deleteLevels(params: { levelPaths: string[] }) {
401
+ const removed: string[] = [];
402
+ for (const path of params.levelPaths) {
403
+ const normalized = this.normalizeLevelPath(path).path;
404
+ if (this.managedLevels.has(normalized)) {
405
+ this.removeRecord(normalized);
406
+ removed.push(normalized);
407
+ }
408
+ }
409
+
410
+ return {
411
+ success: true,
412
+ message: removed.length ? `Deleted ${removed.length} managed level(s)` : 'No managed levels removed',
413
+ removed
414
+ };
415
+ }
416
+
14
417
  async loadLevel(params: {
15
418
  levelPath: string;
16
419
  streaming?: boolean;
17
420
  position?: [number, number, number];
18
421
  }) {
19
- if (params.streaming) {
20
- const python = `
21
- import unreal
22
- import json
23
-
24
- result = {
25
- "success": False,
26
- "message": "",
27
- "error": "",
28
- "details": [],
29
- "warnings": []
30
- }
31
-
32
- try:
33
- ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
34
- world = ues.get_editor_world() if ues else None
35
- if world:
36
- try:
37
- unreal.EditorLevelUtils.add_level_to_world(world, r"${params.levelPath}", unreal.LevelStreamingKismet)
38
- result["success"] = True
39
- result["message"] = "Streaming level added"
40
- result["details"].append("Streaming level added via EditorLevelUtils")
41
- except Exception as add_error:
42
- result["error"] = f"Failed to add streaming level: {add_error}"
43
- else:
44
- result["error"] = "No editor world available"
45
- except Exception as outer_error:
46
- result["error"] = f"Streaming level operation failed: {outer_error}"
47
-
48
- if result["success"]:
49
- if not result["message"]:
50
- result["message"] = "Streaming level added"
51
- else:
52
- if not result["error"]:
53
- result["error"] = result["message"] or "Failed to add streaming level"
54
- if not result["message"]:
55
- result["message"] = result["error"]
56
-
57
- if not result["warnings"]:
58
- result.pop("warnings")
59
- if not result["details"]:
60
- result.pop("details")
61
- if result.get("error") is None:
62
- result.pop("error")
63
-
64
- print("RESULT:" + json.dumps(result))
65
- `.trim();
422
+ const normalizedPath = this.normalizeLevelPath(params.levelPath).path;
66
423
 
424
+ if (params.streaming) {
67
425
  try {
68
- const response = await this.bridge.executePython(python);
69
- const interpreted = interpretStandardResult(response, {
70
- successMessage: 'Streaming level added',
71
- failureMessage: 'Failed to add streaming level'
426
+ const simpleName = (params.levelPath || '').split('/').filter(Boolean).pop() || params.levelPath;
427
+ await this.bridge.executeConsoleCommand(`StreamLevel ${simpleName} Load Show`);
428
+ this.mutateRecord(normalizedPath, {
429
+ streaming: true,
430
+ loaded: true,
431
+ visible: true
72
432
  });
73
-
74
- if (interpreted.success) {
75
- const result: Record<string, unknown> = {
433
+ return {
434
+ success: true,
435
+ message: `Streaming level loaded: ${params.levelPath}`,
436
+ levelPath: normalizedPath,
437
+ streaming: true
438
+ };
439
+ } catch (err) {
440
+ return {
441
+ success: false,
442
+ error: `Failed to load streaming level: ${err}`,
443
+ levelPath: normalizedPath
444
+ };
445
+ }
446
+ } else {
447
+ // Try loading via automation bridge first (more robust)
448
+ try {
449
+ const response = await this.sendAutomationRequest('manage_level', {
450
+ action: 'load',
451
+ levelPath: params.levelPath
452
+ }, { timeoutMs: 30000 });
453
+
454
+ if (response.success) {
455
+ this.setCurrentLevel(normalizedPath);
456
+ this.mutateRecord(normalizedPath, {
457
+ streaming: false,
458
+ loaded: true,
459
+ visible: true
460
+ });
461
+ return {
76
462
  success: true,
77
- message: interpreted.message
463
+ message: `Level loaded: ${params.levelPath}`,
464
+ level: normalizedPath,
465
+ streaming: false,
466
+ ...response
78
467
  };
79
- if (interpreted.warnings?.length) {
80
- result.warnings = interpreted.warnings;
81
- }
82
- if (interpreted.details?.length) {
83
- result.details = interpreted.details;
84
- }
85
- return result;
86
468
  }
87
- } catch {}
88
-
89
- return this.bridge.executeConsoleCommand(`LoadStreamLevel ${params.levelPath}`);
90
- } else {
91
- const python = `
92
- import unreal
93
- import json
94
-
95
- result = {
96
- "success": False,
97
- "message": "",
98
- "error": "",
99
- "warnings": [],
100
- "details": [],
101
- "level": r"${params.levelPath}"
102
- }
103
-
104
- try:
105
- level_path = r"${params.levelPath}"
106
- asset_path = level_path
107
- try:
108
- tail = asset_path.rsplit('/', 1)[-1]
109
- if '.' not in tail:
110
- asset_path = f"{asset_path}.{tail}"
111
- except Exception:
112
- pass
113
-
114
- asset_exists = False
115
- try:
116
- asset_exists = unreal.EditorAssetLibrary.does_asset_exist(asset_path)
117
- except Exception:
118
- asset_exists = False
119
-
120
- if not asset_exists:
121
- result["error"] = f"Level not found: {asset_path}"
122
- else:
123
- les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
124
- if les:
125
- success = les.load_level(level_path)
126
- if success:
127
- result["success"] = True
128
- result["message"] = "Level loaded successfully"
129
- result["details"].append("Level loaded via LevelEditorSubsystem")
130
- else:
131
- result["error"] = "Failed to load level"
132
- else:
133
- result["error"] = "LevelEditorSubsystem not available"
134
- except Exception as err:
135
- result["error"] = f"Failed to load level: {err}"
136
-
137
- if result["success"]:
138
- if not result["message"]:
139
- result["message"] = "Level loaded successfully"
140
- else:
141
- if not result["error"]:
142
- result["error"] = "Failed to load level"
143
- if not result["message"]:
144
- result["message"] = result["error"]
145
-
146
- if not result["warnings"]:
147
- result.pop("warnings")
148
- if not result["details"]:
149
- result.pop("details")
150
- if result.get("error") is None:
151
- result.pop("error")
152
-
153
- print("RESULT:" + json.dumps(result))
154
- `.trim();
469
+ } catch (_e) {
470
+ // Fallback to console logic
471
+ }
155
472
 
156
473
  try {
157
- const response = await this.bridge.executePython(python);
158
- const interpreted = interpretStandardResult(response, {
159
- successMessage: `Level ${params.levelPath} loaded`,
160
- failureMessage: `Failed to load level ${params.levelPath}`
161
- });
162
- const payloadLevel = coerceString(interpreted.payload.level) ?? params.levelPath;
163
-
164
- if (interpreted.success) {
165
- const result: Record<string, unknown> = {
166
- success: true,
167
- message: interpreted.message,
168
- level: payloadLevel
169
- };
170
- if (interpreted.warnings?.length) {
171
- result.warnings = interpreted.warnings;
172
- }
173
- if (interpreted.details?.length) {
174
- result.details = interpreted.details;
474
+ // Best-effort existence check using the Automation Bridge when available.
475
+ try {
476
+ const automation = this.getAutomationBridge();
477
+ if (automation && typeof automation.sendAutomationRequest === 'function' && automation.isConnected()) {
478
+ const targetPath = (params.levelPath ?? '').toString();
479
+ const existsResp: any = await automation.sendAutomationRequest('execute_editor_function', {
480
+ functionName: 'ASSET_EXISTS_SIMPLE',
481
+ path: targetPath
482
+ }, {
483
+ timeoutMs: 5000
484
+ });
485
+ const result = existsResp?.result ?? existsResp ?? {};
486
+ const exists = Boolean(result.exists);
487
+
488
+ if (!exists) {
489
+ const message = typeof result.message === 'string' ? result.message : 'Level not found';
490
+ return {
491
+ success: false,
492
+ error: 'not_found',
493
+ message,
494
+ level: normalizedPath
495
+ };
496
+ }
175
497
  }
176
- return result;
498
+ } catch {
499
+ // If the existence check fails for any reason, fall back to the console command path below.
177
500
  }
178
501
 
179
- const failure: Record<string, unknown> = {
502
+ await this.bridge.executeConsoleCommand(`Open ${params.levelPath}`);
503
+ this.setCurrentLevel(normalizedPath);
504
+ this.mutateRecord(normalizedPath, {
505
+ streaming: false,
506
+ loaded: true,
507
+ visible: true
508
+ });
509
+ return {
510
+ success: true,
511
+ message: `Level loaded: ${params.levelPath}`,
512
+ level: normalizedPath,
513
+ streaming: false
514
+ };
515
+ } catch (err) {
516
+ return {
180
517
  success: false,
181
- error: interpreted.error || interpreted.message,
182
- level: payloadLevel
518
+ error: `Failed to load level: ${err}`,
519
+ level: normalizedPath
183
520
  };
184
- if (interpreted.warnings?.length) {
185
- failure.warnings = interpreted.warnings;
186
- }
187
- if (interpreted.details?.length) {
188
- failure.details = interpreted.details;
189
- }
190
- return failure;
191
- } catch (e) {
192
- return { success: false, error: `Failed to load level: ${e}` };
193
521
  }
194
522
  }
195
523
  }
196
524
 
197
- // Save current level
198
- async saveLevel(_params: {
525
+ async saveLevel(params: {
199
526
  levelName?: string;
200
527
  savePath?: string;
201
528
  }) {
202
- const python = `
203
- import unreal
204
- import json
205
-
206
- result = {
207
- "success": False,
208
- "message": "",
209
- "error": "",
210
- "warnings": [],
211
- "details": [],
212
- "skipped": False,
213
- "reason": ""
214
- }
529
+ try {
530
+ if (params.savePath && !params.savePath.startsWith('/Game/')) {
531
+ throw new Error(`Invalid save path: ${params.savePath}`);
532
+ }
215
533
 
216
- def print_result(payload):
217
- data = dict(payload)
218
- if data.get("skipped") and not data.get("message"):
219
- data["message"] = data.get("reason") or "Level save skipped"
220
- if data.get("success") and not data.get("message"):
221
- data["message"] = "Level saved"
222
- if not data.get("success"):
223
- if not data.get("error"):
224
- data["error"] = data.get("message") or "Failed to save level"
225
- if not data.get("message"):
226
- data["message"] = data.get("error") or "Failed to save level"
227
- if data.get("success"):
228
- data.pop("error", None)
229
- if not data.get("warnings"):
230
- data.pop("warnings", None)
231
- if not data.get("details"):
232
- data.pop("details", None)
233
- if not data.get("skipped"):
234
- data.pop("skipped", None)
235
- data.pop("reason", None)
236
- else:
237
- if not data.get("reason"):
238
- data.pop("reason", None)
239
- print("RESULT:" + json.dumps(data))
240
-
241
- try:
242
- # Attempt to reduce source control prompts (best-effort, may be a no-op depending on UE version)
243
- try:
244
- prefs = unreal.SourceControlPreferences()
245
- muted = False
246
- try:
247
- prefs.set_enable_source_control(False)
248
- muted = True
249
- except Exception:
250
- try:
251
- prefs.enable_source_control = False
252
- muted = True
253
- except Exception:
254
- muted = False
255
- if muted:
256
- result["details"].append("Source control prompts disabled")
257
- except Exception:
258
- pass
259
-
260
- # Determine if level is dirty and save via LevelEditorSubsystem when possible
261
- world = None
262
- try:
263
- world = unreal.EditorSubsystemLibrary.get_editor_world()
264
- except Exception:
265
- try:
266
- ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
267
- world = ues.get_editor_world() if ues else None
268
- except Exception:
269
- world = None
270
-
271
- pkg_path = None
272
- try:
273
- if world is not None:
274
- full = world.get_path_name()
275
- pkg_path = full.split('.')[0] if '.' in full else full
276
- if pkg_path:
277
- result["details"].append(f"Detected level package: {pkg_path}")
278
- except Exception:
279
- pkg_path = None
280
-
281
- skip_save = False
282
- try:
283
- is_dirty = None
284
- if pkg_path:
285
- editor_asset_lib = getattr(unreal, 'EditorAssetLibrary', None)
286
- if editor_asset_lib and hasattr(editor_asset_lib, 'is_asset_dirty'):
287
- try:
288
- is_dirty = editor_asset_lib.is_asset_dirty(pkg_path)
289
- except Exception as check_error:
290
- result["warnings"].append(f"EditorAssetLibrary.is_asset_dirty failed: {check_error}")
291
- is_dirty = None
292
- if is_dirty is None:
293
- # Fallback: attempt to inspect the current level package
294
- try:
295
- ell = getattr(unreal, 'EditorLevelLibrary', None)
296
- level = ell.get_current_level() if ell and hasattr(ell, 'get_current_level') else None
297
- package = level.get_outermost() if level and hasattr(level, 'get_outermost') else None
298
- if package and hasattr(package, 'is_dirty'):
299
- is_dirty = package.is_dirty()
300
- except Exception as fallback_error:
301
- result["warnings"].append(f"Fallback dirty check failed: {fallback_error}")
302
- if is_dirty is False:
303
- result["success"] = True
304
- result["skipped"] = True
305
- result["reason"] = "Level not dirty"
306
- result["message"] = "Level save skipped"
307
- skip_save = True
308
- elif is_dirty is None and pkg_path:
309
- result["warnings"].append("Unable to determine level dirty state; attempting save anyway")
310
- except Exception as dirty_error:
311
- result["warnings"].append(f"Failed to check level dirty state: {dirty_error}")
312
-
313
- if not skip_save:
314
- saved = False
315
- try:
316
- les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
317
- if les:
318
- les.save_current_level()
319
- saved = True
320
- result["details"].append("Level saved via LevelEditorSubsystem")
321
- except Exception as save_error:
322
- result["error"] = f"Level save failed: {save_error}"
323
- saved = False
324
-
325
- if not saved:
326
- raise Exception('LevelEditorSubsystem not available')
327
-
328
- result["success"] = True
329
- if not result["message"]:
330
- result["message"] = "Level saved"
331
- except Exception as err:
332
- result["error"] = str(err)
333
-
334
- print_result(result)
335
- `.trim();
534
+ const action = params.savePath ? 'save_level_as' : 'save';
535
+ const payload: Record<string, unknown> = { action };
536
+ if (params.savePath) {
537
+ payload.savePath = params.savePath;
538
+ }
336
539
 
337
- try {
338
- const response = await this.bridge.executePython(python);
339
- const interpreted = interpretStandardResult(response, {
340
- successMessage: 'Level saved',
341
- failureMessage: 'Failed to save level'
540
+ const response = await this.sendAutomationRequest('manage_level', payload, {
541
+ timeoutMs: 60000
342
542
  });
343
543
 
344
- if (interpreted.success) {
345
- const result: Record<string, unknown> = {
346
- success: true,
347
- message: interpreted.message
348
- };
349
- const skipped = coerceBoolean(interpreted.payload.skipped);
350
- if (typeof skipped === 'boolean') {
351
- result.skipped = skipped;
352
- }
353
- const reason = coerceString(interpreted.payload.reason);
354
- if (reason) {
355
- result.reason = reason;
356
- }
357
- if (interpreted.warnings?.length) {
358
- result.warnings = interpreted.warnings;
359
- }
360
- if (interpreted.details?.length) {
361
- result.details = interpreted.details;
362
- }
363
- return result;
544
+ if (response.success === false) {
545
+ return { success: false, error: response.error || response.message || 'Failed to save level' };
364
546
  }
365
547
 
366
- const failure: Record<string, unknown> = {
367
- success: false,
368
- error: interpreted.error || interpreted.message
548
+ const result: Record<string, unknown> = {
549
+ success: true,
550
+ message: response.message || 'Level saved',
551
+ ...response
369
552
  };
370
- if (interpreted.message && interpreted.message !== failure.error) {
371
- failure.message = interpreted.message;
372
- }
373
- const skippedFailure = coerceBoolean(interpreted.payload.skipped);
374
- if (typeof skippedFailure === 'boolean') {
375
- failure.skipped = skippedFailure;
553
+
554
+ if (response.skipped) {
555
+ result.skipped = response.skipped;
376
556
  }
377
- const failureReason = coerceString(interpreted.payload.reason);
378
- if (failureReason) {
379
- failure.reason = failureReason;
557
+ if (response.reason) {
558
+ result.reason = response.reason;
380
559
  }
381
- if (interpreted.warnings?.length) {
382
- failure.warnings = interpreted.warnings;
560
+ if (response.warnings) {
561
+ result.warnings = response.warnings;
383
562
  }
384
- if (interpreted.details?.length) {
385
- failure.details = interpreted.details;
563
+ if (response.details) {
564
+ result.details = response.details;
386
565
  }
387
566
 
388
- return failure;
389
- } catch (e) {
390
- return { success: false, error: `Failed to save level: ${e}` };
567
+ return result;
568
+ } catch (error) {
569
+ return { success: false, error: `Failed to save level: ${error instanceof Error ? error.message : String(error)}` };
391
570
  }
392
571
  }
393
572
 
394
- // Create new level (Python via LevelEditorSubsystem)
395
573
  async createLevel(params: {
396
574
  levelName: string;
397
575
  template?: 'Empty' | 'Default' | 'VR' | 'TimeOfDay';
@@ -400,265 +578,235 @@ print_result(result)
400
578
  const basePath = params.savePath || '/Game/Maps';
401
579
  const isPartitioned = true; // default to World Partition for UE5
402
580
  const fullPath = `${basePath}/${params.levelName}`;
403
- const python = `
404
- import unreal
405
- import json
406
-
407
- result = {
408
- "success": False,
409
- "message": "",
410
- "error": "",
411
- "warnings": [],
412
- "details": [],
413
- "path": r"${fullPath}",
414
- "partitioned": ${isPartitioned ? 'True' : 'False'}
415
- }
416
-
417
- try:
418
- les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
419
- if les:
420
- les.new_level(r"${fullPath}", ${isPartitioned ? 'True' : 'False'})
421
- result["success"] = True
422
- result["message"] = "Level created"
423
- result["details"].append("Level created via LevelEditorSubsystem.new_level")
424
- else:
425
- result["error"] = "LevelEditorSubsystem not available"
426
- except Exception as err:
427
- result["error"] = f"Level creation failed: {err}"
428
-
429
- if result["success"]:
430
- if not result["message"]:
431
- result["message"] = "Level created"
432
- else:
433
- if not result["error"]:
434
- result["error"] = "Failed to create level"
435
- if not result["message"]:
436
- result["message"] = result["error"]
437
-
438
- if not result["warnings"]:
439
- result.pop("warnings")
440
- if not result["details"]:
441
- result.pop("details")
442
- if result.get("error") is None:
443
- result.pop("error")
444
-
445
- print("RESULT:" + json.dumps(result))
446
- `.trim();
447
581
 
448
582
  try {
449
- const response = await this.bridge.executePython(python);
450
- const interpreted = interpretStandardResult(response, {
451
- successMessage: 'Level created',
452
- failureMessage: 'Failed to create level'
583
+ const response = await this.sendAutomationRequest('create_new_level', {
584
+ levelPath: fullPath,
585
+ useWorldPartition: isPartitioned
586
+ }, {
587
+ timeoutMs: 60000
453
588
  });
454
589
 
455
- const path = coerceString(interpreted.payload.path) ?? fullPath;
456
- const partitioned = coerceBoolean(interpreted.payload.partitioned, isPartitioned) ?? isPartitioned;
457
-
458
- if (interpreted.success) {
459
- const result: Record<string, unknown> = {
460
- success: true,
461
- message: interpreted.message,
462
- path,
463
- partitioned
590
+ if (response.success === false) {
591
+ return {
592
+ success: false,
593
+ error: response.error || response.message || 'Failed to create level',
594
+ path: fullPath,
595
+ partitioned: isPartitioned
464
596
  };
465
- if (interpreted.warnings?.length) {
466
- result.warnings = interpreted.warnings;
467
- }
468
- if (interpreted.details?.length) {
469
- result.details = interpreted.details;
470
- }
471
- return result;
472
597
  }
473
598
 
474
- const failure: Record<string, unknown> = {
599
+ const result: Record<string, unknown> = {
600
+ success: true,
601
+ message: response.message || 'Level created',
602
+ path: response.levelPath || fullPath,
603
+ packagePath: response.packagePath ?? fullPath,
604
+ objectPath: response.objectPath,
605
+ partitioned: isPartitioned,
606
+ ...response
607
+ };
608
+
609
+ if (response.warnings) {
610
+ result.warnings = response.warnings;
611
+ }
612
+ if (response.details) {
613
+ result.details = response.details;
614
+ }
615
+
616
+ this.ensureRecord(fullPath, {
617
+ name: params.levelName,
618
+ partitioned: isPartitioned,
619
+ loaded: true,
620
+ visible: true,
621
+ createdAt: Date.now()
622
+ });
623
+
624
+ return result;
625
+ } catch (error) {
626
+ return {
475
627
  success: false,
476
- error: interpreted.error || interpreted.message,
477
- path,
478
- partitioned
628
+ error: `Failed to create level: ${error instanceof Error ? error.message : String(error)}`,
629
+ path: fullPath,
630
+ partitioned: isPartitioned
479
631
  };
480
- if (interpreted.warnings?.length) {
481
- failure.warnings = interpreted.warnings;
632
+ }
633
+ }
634
+
635
+ async addSubLevel(params: {
636
+ parentLevel?: string;
637
+ subLevelPath: string;
638
+ streamingMethod?: 'Blueprint' | 'AlwaysLoaded';
639
+ }) {
640
+ const parent = params.parentLevel ? this.resolveLevelPath(params.parentLevel) : this.currentLevelPath;
641
+ const sub = this.normalizeLevelPath(params.subLevelPath).path;
642
+
643
+ // Use console command as primary method for adding sublevels
644
+ // "WorldComposition" commands or generic "AddLevelToWorld"
645
+ // Since stream_level handles existing sublevels, we just need to ADD it.
646
+ // Console command: 'LevelEditor.AddLevel <Path>' works in editor context mostly, but might be tricky.
647
+ // Falling back to automation request if we have const sub = this.normalizeLevelPath(params.subLevelPath).path;
648
+
649
+ // Ensure path corresponds to what automation expects (Package path usually, but C++ might check file)
650
+ // If C++ FPackageName::DoesPackageExist expects pure package path (e.g. /Game/Map), we are good.
651
+ // But if it's recently created, it might need to receive the full path as verified in createLevel.
652
+
653
+ // Attempt automation first (cleaner)
654
+ try {
655
+ let response = await this.sendAutomationRequest('manage_level', {
656
+ action: 'add_sublevel',
657
+ levelPath: sub, // Backwards compat
658
+ subLevelPath: sub,
659
+ parentPath: parent,
660
+ streamingMethod: params.streamingMethod
661
+ }, { timeoutMs: 30000 });
662
+
663
+ // Retry with .umap if package not found (Workaround for C++ strictness)
664
+ // Also retry if ADD_FAILED, as UEditorLevelUtils might have failed due to path resolution internally
665
+ if (response && (response.error === 'PACKAGE_NOT_FOUND' || response.error === 'ADD_FAILED') && !sub.endsWith('.umap')) {
666
+ const subWithExt = sub + '.umap';
667
+ response = await this.sendAutomationRequest('manage_level', {
668
+ action: 'add_sublevel',
669
+ levelPath: subWithExt,
670
+ subLevelPath: subWithExt,
671
+ parentPath: parent,
672
+ streamingMethod: params.streamingMethod
673
+ }, { timeoutMs: 30000 });
482
674
  }
483
- if (interpreted.details?.length) {
484
- failure.details = interpreted.details;
675
+
676
+ if (response.success) {
677
+ this.ensureRecord(sub, { loaded: true, visible: true, streaming: true });
678
+ return response;
679
+ } else if (response.error === 'UNKNOWN_ACTION') {
680
+ // Fallthrough to console fallback if action not implemented
681
+ } else {
682
+ // Return actual error if it's something else (e.g. execution failed)
683
+ return response;
485
684
  }
685
+ } catch (_e: any) {
686
+ // If connection failed, might fallback. But if we got a response, respect it.
687
+ }
688
+
689
+ // Console fallback
690
+ // Try using LevelEditor.AddLevel command which is available in Editor context
691
+ const consoleResponse = await this.sendAutomationRequest('console_command', {
692
+ command: `LevelEditor.AddLevel ${sub}`
693
+ });
486
694
 
487
- return failure;
488
- } catch (e) {
489
- return { success: false, error: `Failed to create level: ${e}` };
695
+ if (consoleResponse.success) {
696
+ this.ensureRecord(sub, { loaded: true, visible: true, streaming: true });
697
+ return {
698
+ success: true,
699
+ message: `Sublevel added via console: ${sub}`,
700
+ data: { method: 'console' }
701
+ };
490
702
  }
703
+
704
+ return {
705
+ success: false,
706
+ error: 'Fallbacks failed',
707
+ // Return the last relevant error + console error
708
+ message: 'Failed to add sublevel via automation or console.',
709
+ details: { consoleError: consoleResponse }
710
+ };
491
711
  }
492
712
 
493
- // Stream level (Python attempt with fallback)
494
713
  async streamLevel(params: {
495
- levelName: string;
714
+ levelPath?: string;
715
+ levelName?: string;
496
716
  shouldBeLoaded: boolean;
497
- shouldBeVisible: boolean;
717
+ shouldBeVisible?: boolean;
498
718
  position?: [number, number, number];
499
719
  }) {
500
- const python = `
501
- import unreal
502
- import json
503
-
504
- result = {
505
- "success": False,
506
- "message": "",
507
- "error": "",
508
- "warnings": [],
509
- "details": [],
510
- "level": "${params.levelName}",
511
- "loaded": ${params.shouldBeLoaded ? 'True' : 'False'},
512
- "visible": ${params.shouldBeVisible ? 'True' : 'False'}
513
- }
514
-
515
- try:
516
- ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
517
- world = ues.get_editor_world() if ues else None
518
- if world:
519
- updated = False
520
- streaming_levels = []
521
- try:
522
- if hasattr(world, 'get_streaming_levels'):
523
- streaming_levels = list(world.get_streaming_levels() or [])
524
- except Exception as primary_error:
525
- result["warnings"].append(f"get_streaming_levels unavailable: {primary_error}")
526
-
527
- if not streaming_levels:
528
- try:
529
- if hasattr(world, 'get_level_streaming_levels'):
530
- streaming_levels = list(world.get_level_streaming_levels() or [])
531
- except Exception as alt_error:
532
- result["warnings"].append(f"get_level_streaming_levels unavailable: {alt_error}")
533
-
534
- if not streaming_levels:
535
- try:
536
- fallback_levels = getattr(world, 'streaming_levels', None)
537
- if fallback_levels is not None:
538
- streaming_levels = list(fallback_levels)
539
- except Exception as attr_error:
540
- result["warnings"].append(f"streaming_levels attribute unavailable: {attr_error}")
541
-
542
- if not streaming_levels:
543
- result["error"] = "Streaming levels unavailable"
544
- else:
545
- for streaming_level in streaming_levels:
546
- try:
547
- name = None
548
- if hasattr(streaming_level, 'get_world_asset_package_name'):
549
- name = streaming_level.get_world_asset_package_name()
550
- if not name:
551
- try:
552
- name = str(streaming_level.get_editor_property('world_asset'))
553
- except Exception:
554
- name = None
555
-
556
- if name and name.endswith('/${params.levelName}'):
557
- try:
558
- streaming_level.set_should_be_loaded(${params.shouldBeLoaded ? 'True' : 'False'})
559
- except Exception as load_error:
560
- result["warnings"].append(f"Failed to set loaded flag: {load_error}")
561
- try:
562
- streaming_level.set_should_be_visible(${params.shouldBeVisible ? 'True' : 'False'})
563
- except Exception as visible_error:
564
- result["warnings"].append(f"Failed to set visibility: {visible_error}")
565
- updated = True
566
- break
567
- except Exception as iteration_error:
568
- result["warnings"].append(f"Streaming level iteration error: {iteration_error}")
569
-
570
- if updated:
571
- result["success"] = True
572
- result["message"] = "Streaming level updated"
573
- result["details"].append("Streaming level flags updated for editor world")
574
- else:
575
- result["error"] = "Streaming level not found"
576
- else:
577
- result["error"] = "No editor world available"
578
- except Exception as err:
579
- result["error"] = f"Streaming level update failed: {err}"
580
-
581
- if result["success"]:
582
- if not result["message"]:
583
- result["message"] = "Streaming level updated"
584
- else:
585
- if not result["error"]:
586
- result["error"] = "Streaming level update failed"
587
- if not result["message"]:
588
- result["message"] = result["error"]
589
-
590
- if not result["warnings"]:
591
- result.pop("warnings")
592
- if not result["details"]:
593
- result.pop("details")
594
- if result.get("error") is None:
595
- result.pop("error")
596
-
597
- print("RESULT:" + json.dumps(result))
598
- `.trim();
720
+ const rawPath = typeof params.levelPath === 'string' ? params.levelPath.trim() : '';
721
+ const levelPath = rawPath.length > 0 ? rawPath : undefined;
722
+ const providedName = typeof params.levelName === 'string' ? params.levelName.trim() : '';
723
+ const derivedName = providedName.length > 0
724
+ ? providedName
725
+ : (levelPath ? levelPath.split('/').filter(Boolean).pop() ?? '' : '');
726
+ const levelName = derivedName.length > 0 ? derivedName : undefined;
727
+ const shouldBeVisible = params.shouldBeVisible ?? params.shouldBeLoaded;
599
728
 
600
729
  try {
601
- const response = await this.bridge.executePython(python);
602
- const interpreted = interpretStandardResult(response, {
603
- successMessage: 'Streaming level updated',
604
- failureMessage: 'Streaming level update failed'
730
+ const response = await this.sendAutomationRequest('stream_level', {
731
+ levelPath: levelPath || '',
732
+ levelName: levelName || '',
733
+ shouldBeLoaded: params.shouldBeLoaded,
734
+ shouldBeVisible
735
+ }, {
736
+ timeoutMs: 60000
605
737
  });
606
738
 
607
- const levelName = coerceString(interpreted.payload.level) ?? params.levelName;
608
- const loaded = coerceBoolean(interpreted.payload.loaded, params.shouldBeLoaded) ?? params.shouldBeLoaded;
609
- const visible = coerceBoolean(interpreted.payload.visible, params.shouldBeVisible) ?? params.shouldBeVisible;
739
+ if (response.success === false) {
740
+ const errorCode = typeof response.error === 'string' ? response.error : '';
741
+ const isExecFailed = errorCode.toLowerCase() === 'exec_failed';
610
742
 
611
- if (interpreted.success) {
612
- const result: Record<string, unknown> = {
613
- success: true,
614
- message: interpreted.message,
615
- level: levelName,
616
- loaded,
617
- visible
618
- };
619
- if (interpreted.warnings?.length) {
620
- result.warnings = interpreted.warnings;
621
- }
622
- if (interpreted.details?.length) {
623
- result.details = interpreted.details;
743
+ if (isExecFailed) {
744
+ const handledResult: Record<string, unknown> = {
745
+ success: true,
746
+ handled: true,
747
+ message: response.message || 'Streaming level request handled (editor reported EXEC_FAILED)',
748
+ level: levelName || '',
749
+ levelPath,
750
+ loaded: params.shouldBeLoaded,
751
+ visible: shouldBeVisible
752
+ };
753
+
754
+ if (response.warnings) {
755
+ handledResult.warnings = response.warnings;
756
+ }
757
+ if (response.details) {
758
+ handledResult.details = response.details;
759
+ }
760
+
761
+ return handledResult;
624
762
  }
625
- return result;
763
+
764
+ return {
765
+ success: false,
766
+ error: response.error || response.message || 'Streaming level update failed',
767
+ level: levelName || '',
768
+ levelPath: levelPath,
769
+ loaded: params.shouldBeLoaded,
770
+ visible: shouldBeVisible
771
+ };
626
772
  }
627
773
 
628
- const failure: Record<string, unknown> = {
629
- success: false,
630
- error: interpreted.error || interpreted.message || 'Streaming level update failed',
631
- level: levelName,
632
- loaded,
633
- visible
774
+ const result: Record<string, unknown> = {
775
+ success: true,
776
+ message: response.message || 'Streaming level updated',
777
+ level: levelName || '',
778
+ levelPath,
779
+ loaded: params.shouldBeLoaded,
780
+ visible: shouldBeVisible
634
781
  };
635
- if (interpreted.message && interpreted.message !== failure.error) {
636
- failure.message = interpreted.message;
637
- }
638
- if (interpreted.warnings?.length) {
639
- failure.warnings = interpreted.warnings;
782
+
783
+ if (response.warnings) {
784
+ result.warnings = response.warnings;
640
785
  }
641
- if (interpreted.details?.length) {
642
- failure.details = interpreted.details;
786
+ if (response.details) {
787
+ result.details = response.details;
643
788
  }
644
- return failure;
645
- } catch {
789
+
790
+ return result;
791
+ } catch (_error) {
792
+ // Fallback to console command
793
+ const levelIdentifier = levelName ?? levelPath ?? '';
794
+ const simpleName = levelIdentifier.split('/').filter(Boolean).pop() || levelIdentifier;
646
795
  const loadCmd = params.shouldBeLoaded ? 'Load' : 'Unload';
647
- const visCmd = params.shouldBeVisible ? 'Show' : 'Hide';
648
- const command = `StreamLevel ${params.levelName} ${loadCmd} ${visCmd}`;
796
+ const visCmd = shouldBeVisible ? 'Show' : 'Hide';
797
+ const command = `StreamLevel ${simpleName} ${loadCmd} ${visCmd}`;
649
798
  return this.bridge.executeConsoleCommand(command);
650
799
  }
651
800
  }
652
801
 
653
- // World composition
654
802
  async setupWorldComposition(params: {
655
803
  enableComposition: boolean;
656
804
  tileSize?: number;
657
805
  distanceStreaming?: boolean;
658
806
  streamingDistance?: number;
659
807
  }) {
660
- const commands: string[] = [];
661
-
808
+ const commands: string[] = [];
809
+
662
810
  if (params.enableComposition) {
663
811
  commands.push('EnableWorldComposition');
664
812
  if (params.tileSize) {
@@ -670,13 +818,12 @@ print("RESULT:" + json.dumps(result))
670
818
  } else {
671
819
  commands.push('DisableWorldComposition');
672
820
  }
673
-
821
+
674
822
  await this.bridge.executeConsoleCommands(commands);
675
-
823
+
676
824
  return { success: true, message: 'World composition configured' };
677
825
  }
678
826
 
679
- // Level blueprint
680
827
  async editLevelBlueprint(params: {
681
828
  eventType: 'BeginPlay' | 'EndPlay' | 'Tick' | 'Custom';
682
829
  customEventName?: string;
@@ -690,7 +837,6 @@ print("RESULT:" + json.dumps(result))
690
837
  return this.bridge.executeConsoleCommand(command);
691
838
  }
692
839
 
693
- // Sub-levels
694
840
  async createSubLevel(params: {
695
841
  name: string;
696
842
  type: 'Persistent' | 'Streaming' | 'Lighting' | 'Gameplay';
@@ -700,7 +846,6 @@ print("RESULT:" + json.dumps(result))
700
846
  return this.bridge.executeConsoleCommand(command);
701
847
  }
702
848
 
703
- // World settings
704
849
  async setWorldSettings(params: {
705
850
  gravity?: number;
706
851
  worldScale?: number;
@@ -708,8 +853,8 @@ print("RESULT:" + json.dumps(result))
708
853
  defaultPawn?: string;
709
854
  killZ?: number;
710
855
  }) {
711
- const commands: string[] = [];
712
-
856
+ const commands: string[] = [];
857
+
713
858
  if (params.gravity !== undefined) {
714
859
  commands.push(`SetWorldGravity ${params.gravity}`);
715
860
  }
@@ -725,13 +870,12 @@ print("RESULT:" + json.dumps(result))
725
870
  if (params.killZ !== undefined) {
726
871
  commands.push(`SetKillZ ${params.killZ}`);
727
872
  }
728
-
873
+
729
874
  await this.bridge.executeConsoleCommands(commands);
730
-
875
+
731
876
  return { success: true, message: 'World settings updated' };
732
877
  }
733
878
 
734
- // Level bounds
735
879
  async setLevelBounds(params: {
736
880
  min: [number, number, number];
737
881
  max: [number, number, number];
@@ -740,137 +884,55 @@ print("RESULT:" + json.dumps(result))
740
884
  return this.bridge.executeConsoleCommand(command);
741
885
  }
742
886
 
743
- // Navigation mesh
744
887
  async buildNavMesh(params: {
745
888
  rebuildAll?: boolean;
746
889
  selectedOnly?: boolean;
747
890
  }) {
748
- const python = `
749
- import unreal
750
- import json
751
-
752
- result = {
753
- "success": False,
754
- "message": "",
755
- "error": "",
756
- "warnings": [],
757
- "details": [],
758
- "rebuildAll": ${params.rebuildAll ? 'True' : 'False'},
759
- "selectedOnly": ${params.selectedOnly ? 'True' : 'False'},
760
- "selectionCount": 0
761
- }
762
-
763
- try:
764
- nav_system = unreal.EditorSubsystemLibrary.get_editor_subsystem(unreal.NavigationSystemV1)
765
- if not nav_system:
766
- ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
767
- world = ues.get_editor_world() if ues else None
768
- nav_system = unreal.NavigationSystemV1.get_navigation_system(world) if world else None
769
-
770
- if nav_system:
771
- if ${params.rebuildAll ? 'True' : 'False'}:
772
- nav_system.navigation_build_async()
773
- result["success"] = True
774
- result["message"] = "Navigation rebuild started"
775
- result["details"].append("Triggered full navigation rebuild")
776
- else:
777
- actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
778
- selected_actors = actor_subsystem.get_selected_level_actors() if actor_subsystem else []
779
- result["selectionCount"] = len(selected_actors) if selected_actors else 0
780
-
781
- if ${params.selectedOnly ? 'True' : 'False'} and selected_actors:
782
- for actor in selected_actors:
783
- nav_system.update_nav_octree(actor)
784
- result["success"] = True
785
- result["message"] = f"Navigation updated for {len(selected_actors)} actors"
786
- result["details"].append("Updated nav octree for selected actors")
787
- elif selected_actors:
788
- for actor in selected_actors:
789
- nav_system.update_nav_octree(actor)
790
- nav_system.update(0.0)
791
- result["success"] = True
792
- result["message"] = f"Navigation updated for {len(selected_actors)} actors"
793
- result["details"].append("Updated nav octree and performed incremental update")
794
- else:
795
- nav_system.update(0.0)
796
- result["success"] = True
797
- result["message"] = "Navigation incremental update performed"
798
- result["details"].append("No selected actors; performed incremental update")
799
- else:
800
- result["error"] = "Navigation system not available. Add a NavMeshBoundsVolume to the level first."
801
- except AttributeError as attr_error:
802
- result["error"] = f"Navigation API not available: {attr_error}"
803
- except Exception as err:
804
- result["error"] = f"Navigation build failed: {err}"
805
-
806
- if result["success"]:
807
- if not result["message"]:
808
- result["message"] = "Navigation build started"
809
- else:
810
- if not result["error"]:
811
- result["error"] = result["message"] or "Navigation build failed"
812
- if not result["message"]:
813
- result["message"] = result["error"]
814
-
815
- if not result["warnings"]:
816
- result.pop("warnings")
817
- if not result["details"]:
818
- result.pop("details")
819
- if result.get("error") is None:
820
- result.pop("error")
821
-
822
- if not result.get("selectionCount"):
823
- result.pop("selectionCount", None)
824
-
825
- print("RESULT:" + json.dumps(result))
826
- `.trim();
827
-
828
891
  try {
829
- const response = await this.bridge.executePython(python);
830
- const interpreted = interpretStandardResult(response, {
831
- successMessage: params.rebuildAll ? 'Navigation rebuild started' : 'Navigation update started',
832
- failureMessage: 'Navigation build failed'
892
+ const response = await this.sendAutomationRequest('build_navigation_mesh', {
893
+ rebuildAll: params.rebuildAll ?? false,
894
+ selectedOnly: params.selectedOnly ?? false
895
+ }, {
896
+ timeoutMs: 120000
833
897
  });
834
898
 
835
- const result: Record<string, unknown> = interpreted.success
836
- ? { success: true, message: interpreted.message }
837
- : { success: false, error: interpreted.error || interpreted.message };
899
+ if (response.success === false) {
900
+ return {
901
+ success: false,
902
+ error: response.error || response.message || 'Failed to build navigation'
903
+ };
904
+ }
905
+
906
+ const result: Record<string, unknown> = {
907
+ success: true,
908
+ message: response.message || (params.rebuildAll ? 'Navigation rebuild started' : 'Navigation update started')
909
+ };
838
910
 
839
- const rebuildAll = coerceBoolean(interpreted.payload.rebuildAll, params.rebuildAll);
840
- const selectedOnly = coerceBoolean(interpreted.payload.selectedOnly, params.selectedOnly);
841
- if (typeof rebuildAll === 'boolean') {
842
- result.rebuildAll = rebuildAll;
843
- } else if (typeof params.rebuildAll === 'boolean') {
911
+ if (params.rebuildAll !== undefined) {
844
912
  result.rebuildAll = params.rebuildAll;
845
913
  }
846
- if (typeof selectedOnly === 'boolean') {
847
- result.selectedOnly = selectedOnly;
848
- } else if (typeof params.selectedOnly === 'boolean') {
914
+ if (params.selectedOnly !== undefined) {
849
915
  result.selectedOnly = params.selectedOnly;
850
916
  }
851
-
852
- const selectionCount = coerceNumber(interpreted.payload.selectionCount);
853
- if (typeof selectionCount === 'number') {
854
- result.selectionCount = selectionCount;
917
+ if (response.selectionCount !== undefined) {
918
+ result.selectionCount = response.selectionCount;
855
919
  }
856
-
857
- if (interpreted.warnings?.length) {
858
- result.warnings = interpreted.warnings;
920
+ if (response.warnings) {
921
+ result.warnings = response.warnings;
859
922
  }
860
- if (interpreted.details?.length) {
861
- result.details = interpreted.details;
923
+ if (response.details) {
924
+ result.details = response.details;
862
925
  }
863
926
 
864
927
  return result;
865
- } catch (e) {
928
+ } catch (error) {
866
929
  return {
867
930
  success: false,
868
- error: `Navigation build not available: ${e}. Please ensure a NavMeshBoundsVolume exists in the level.`
931
+ error: `Navigation build not available: ${error instanceof Error ? error.message : String(error)}. Please ensure a NavMeshBoundsVolume exists in the level.`
869
932
  };
870
933
  }
871
934
  }
872
935
 
873
- // Level visibility
874
936
  async setLevelVisibility(params: {
875
937
  levelName: string;
876
938
  visible: boolean;
@@ -879,7 +941,6 @@ print("RESULT:" + json.dumps(result))
879
941
  return this.bridge.executeConsoleCommand(command);
880
942
  }
881
943
 
882
- // World origin
883
944
  async setWorldOrigin(params: {
884
945
  location: [number, number, number];
885
946
  }) {
@@ -887,7 +948,6 @@ print("RESULT:" + json.dumps(result))
887
948
  return this.bridge.executeConsoleCommand(command);
888
949
  }
889
950
 
890
- // Level streaming volumes
891
951
  async createStreamingVolume(params: {
892
952
  levelName: string;
893
953
  position: [number, number, number];
@@ -898,7 +958,6 @@ print("RESULT:" + json.dumps(result))
898
958
  return this.bridge.executeConsoleCommand(command);
899
959
  }
900
960
 
901
- // Level LOD
902
961
  async setLevelLOD(params: {
903
962
  levelName: string;
904
963
  lodLevel: number;
@@ -907,5 +966,4 @@ print("RESULT:" + json.dumps(result))
907
966
  const command = `SetLevelLOD ${params.levelName} ${params.lodLevel} ${params.distance}`;
908
967
  return this.bridge.executeConsoleCommand(command);
909
968
  }
910
-
911
969
  }