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,617 +1,678 @@
1
- import { coerceBoolean, coerceNumber, coerceString, interpretStandardResult } from '../utils/result-helpers.js';
2
- export class LevelTools {
3
- bridge;
4
- constructor(bridge) {
5
- this.bridge = bridge;
1
+ import { BaseTool } from './base-tool.js';
2
+ import { sanitizePath } from '../utils/path-security.js';
3
+ import { DEFAULT_OPERATION_TIMEOUT_MS, DEFAULT_ASSET_OP_TIMEOUT_MS, LONG_RUNNING_OP_TIMEOUT_MS } from '../constants.js';
4
+ export class LevelTools extends BaseTool {
5
+ managedLevels = new Map();
6
+ listCache;
7
+ LIST_CACHE_TTL_MS = 750;
8
+ currentLevelPath;
9
+ invalidateListCache() {
10
+ this.listCache = undefined;
11
+ }
12
+ normalizeLevelPath(rawPath) {
13
+ if (!rawPath) {
14
+ return { path: '/Game/Maps/Untitled', name: 'Untitled' };
15
+ }
16
+ let formatted = rawPath.replace(/\\/g, '/').trim();
17
+ if (!formatted.startsWith('/')) {
18
+ formatted = formatted.startsWith('Game/') ? `/${formatted}` : `/Game/${formatted.replace(/^\/?Game\//i, '')}`;
19
+ }
20
+ if (!formatted.startsWith('/Game/')) {
21
+ formatted = `/Game/${formatted.replace(/^\/+/, '')}`;
22
+ }
23
+ try {
24
+ formatted = sanitizePath(formatted);
25
+ }
26
+ catch (e) {
27
+ throw new Error(`Security validation failed for level path: ${e.message}`);
28
+ }
29
+ formatted = formatted.replace(/\.umap$/i, '');
30
+ if (formatted.endsWith('/')) {
31
+ formatted = formatted.slice(0, -1);
32
+ }
33
+ const segments = formatted.split('/').filter(Boolean);
34
+ const lastSegment = segments[segments.length - 1] ?? 'Untitled';
35
+ const name = lastSegment.includes('.') ? lastSegment.split('.').pop() ?? lastSegment : lastSegment;
36
+ return { path: formatted, name: name || 'Untitled' };
37
+ }
38
+ ensureRecord(path, seed) {
39
+ const normalized = this.normalizeLevelPath(path);
40
+ let record = this.managedLevels.get(normalized.path);
41
+ if (!record) {
42
+ record = {
43
+ path: normalized.path,
44
+ name: seed?.name ?? normalized.name,
45
+ partitioned: seed?.partitioned ?? false,
46
+ streaming: seed?.streaming ?? false,
47
+ loaded: seed?.loaded ?? false,
48
+ visible: seed?.visible ?? false,
49
+ createdAt: seed?.createdAt ?? Date.now(),
50
+ lastSavedAt: seed?.lastSavedAt,
51
+ metadata: seed?.metadata ? { ...seed.metadata } : undefined,
52
+ exports: seed?.exports ? [...seed.exports] : [],
53
+ lights: seed?.lights ? [...seed.lights] : []
54
+ };
55
+ this.managedLevels.set(normalized.path, record);
56
+ this.invalidateListCache();
57
+ }
58
+ return record;
59
+ }
60
+ mutateRecord(path, updates) {
61
+ if (!path || !path.trim()) {
62
+ return undefined;
63
+ }
64
+ const record = this.ensureRecord(path, updates);
65
+ let changed = false;
66
+ if (updates.name !== undefined && updates.name !== record.name) {
67
+ record.name = updates.name;
68
+ changed = true;
69
+ }
70
+ if (updates.partitioned !== undefined && updates.partitioned !== record.partitioned) {
71
+ record.partitioned = updates.partitioned;
72
+ changed = true;
73
+ }
74
+ if (updates.streaming !== undefined && updates.streaming !== record.streaming) {
75
+ record.streaming = updates.streaming;
76
+ changed = true;
77
+ }
78
+ if (updates.loaded !== undefined && updates.loaded !== record.loaded) {
79
+ record.loaded = updates.loaded;
80
+ changed = true;
81
+ }
82
+ if (updates.visible !== undefined && updates.visible !== record.visible) {
83
+ record.visible = updates.visible;
84
+ changed = true;
85
+ }
86
+ if (updates.createdAt !== undefined && updates.createdAt !== record.createdAt) {
87
+ record.createdAt = updates.createdAt;
88
+ changed = true;
89
+ }
90
+ if (updates.lastSavedAt !== undefined && updates.lastSavedAt !== record.lastSavedAt) {
91
+ record.lastSavedAt = updates.lastSavedAt;
92
+ changed = true;
93
+ }
94
+ if (updates.metadata) {
95
+ record.metadata = { ...(record.metadata ?? {}), ...updates.metadata };
96
+ changed = true;
97
+ }
98
+ if (updates.exports && updates.exports.length > 0) {
99
+ record.exports = [...record.exports, ...updates.exports];
100
+ changed = true;
101
+ }
102
+ if (updates.lights && updates.lights.length > 0) {
103
+ record.lights = [...record.lights, ...updates.lights];
104
+ changed = true;
105
+ }
106
+ if (changed) {
107
+ this.invalidateListCache();
108
+ }
109
+ return record;
110
+ }
111
+ getRecord(path) {
112
+ if (!path || !path.trim()) {
113
+ return undefined;
114
+ }
115
+ const normalized = this.normalizeLevelPath(path);
116
+ return this.managedLevels.get(normalized.path);
117
+ }
118
+ resolveLevelPath(explicit) {
119
+ if (explicit && explicit.trim()) {
120
+ return this.normalizeLevelPath(explicit).path;
121
+ }
122
+ return this.currentLevelPath;
123
+ }
124
+ removeRecord(path) {
125
+ const normalized = this.normalizeLevelPath(path);
126
+ if (this.managedLevels.delete(normalized.path)) {
127
+ if (this.currentLevelPath === normalized.path) {
128
+ this.currentLevelPath = undefined;
129
+ }
130
+ this.invalidateListCache();
131
+ }
132
+ }
133
+ listManagedLevels() {
134
+ const now = Date.now();
135
+ if (this.listCache && now - this.listCache.timestamp < this.LIST_CACHE_TTL_MS) {
136
+ return this.listCache.result;
137
+ }
138
+ const levels = Array.from(this.managedLevels.values()).map((record) => ({
139
+ path: record.path,
140
+ name: record.name,
141
+ partitioned: record.partitioned,
142
+ streaming: record.streaming,
143
+ loaded: record.loaded,
144
+ visible: record.visible,
145
+ createdAt: record.createdAt,
146
+ lastSavedAt: record.lastSavedAt,
147
+ exports: record.exports,
148
+ lightCount: record.lights.length
149
+ }));
150
+ const result = { success: true, message: 'Managed levels listed', count: levels.length, levels };
151
+ this.listCache = { result, timestamp: now };
152
+ return result;
153
+ }
154
+ summarizeLevel(path) {
155
+ const record = this.getRecord(path);
156
+ if (!record) {
157
+ return { success: false, error: `Level not tracked: ${path}` };
158
+ }
159
+ return {
160
+ success: true,
161
+ message: 'Level summary ready',
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
+ lights: record.lights,
172
+ metadata: record.metadata
173
+ };
174
+ }
175
+ setCurrentLevel(path) {
176
+ const normalized = this.normalizeLevelPath(path);
177
+ this.currentLevelPath = normalized.path;
178
+ this.ensureRecord(normalized.path, { loaded: true, visible: true });
179
+ }
180
+ async listLevels() {
181
+ try {
182
+ const response = await this.sendAutomationRequest('list_levels', {}, {
183
+ timeoutMs: 10000
184
+ });
185
+ if (response && response.success !== false) {
186
+ const managed = this.listManagedLevels();
187
+ const ueLevels = (response.allMaps || []);
188
+ const managedOnly = managed.levels.filter(m => !ueLevels.some(u => u.path === m.path));
189
+ const finalLevels = [...ueLevels, ...managedOnly];
190
+ const result = {
191
+ ...response,
192
+ success: true,
193
+ message: 'Levels listed from Unreal Engine',
194
+ levels: finalLevels,
195
+ currentMap: response.currentMap,
196
+ currentMapPath: response.currentMapPath,
197
+ currentWorldLevels: response.currentWorldLevels || [],
198
+ data: {
199
+ levels: finalLevels,
200
+ count: finalLevels.length
201
+ },
202
+ managedLevels: managed.levels,
203
+ managedLevelCount: managed.count
204
+ };
205
+ return result;
206
+ }
207
+ }
208
+ catch {
209
+ }
210
+ return this.listManagedLevels();
211
+ }
212
+ async getLevelSummary(levelPath) {
213
+ const resolved = this.resolveLevelPath(levelPath);
214
+ if (!resolved) {
215
+ return { success: false, error: 'No level specified' };
216
+ }
217
+ return this.summarizeLevel(resolved);
218
+ }
219
+ registerLight(levelPath, info) {
220
+ const resolved = this.resolveLevelPath(levelPath);
221
+ if (!resolved) {
222
+ return;
223
+ }
224
+ this.mutateRecord(resolved, {
225
+ lights: [
226
+ {
227
+ name: info.name,
228
+ type: info.type,
229
+ createdAt: Date.now(),
230
+ details: info.details
231
+ }
232
+ ]
233
+ });
234
+ }
235
+ async exportLevel(params) {
236
+ const resolved = this.resolveLevelPath(params.levelPath);
237
+ if (!resolved) {
238
+ return { success: false, error: 'No level specified for export' };
239
+ }
240
+ try {
241
+ const res = await this.sendAutomationRequest('manage_level', {
242
+ action: 'export_level',
243
+ levelPath: resolved,
244
+ exportPath: params.exportPath
245
+ }, { timeoutMs: params.timeoutMs ?? LONG_RUNNING_OP_TIMEOUT_MS });
246
+ if (res?.success === false) {
247
+ return {
248
+ success: false,
249
+ error: res.error || res.message || 'Export failed',
250
+ levelPath: resolved,
251
+ exportPath: params.exportPath,
252
+ details: res
253
+ };
254
+ }
255
+ return {
256
+ success: true,
257
+ message: `Level exported to ${params.exportPath}`,
258
+ levelPath: resolved,
259
+ exportPath: params.exportPath,
260
+ details: res
261
+ };
262
+ }
263
+ catch (e) {
264
+ return { success: false, error: `Export failed: ${e.message}` };
265
+ }
266
+ }
267
+ async importLevel(params) {
268
+ const destination = params.destinationPath
269
+ ? this.normalizeLevelPath(params.destinationPath)
270
+ : this.normalizeLevelPath(`/Game/Maps/Imported_${Math.floor(Date.now() / 1000)}`);
271
+ try {
272
+ const res = await this.sendAutomationRequest('manage_level', {
273
+ action: 'import_level',
274
+ packagePath: params.packagePath,
275
+ destinationPath: destination.path
276
+ }, { timeoutMs: params.timeoutMs ?? LONG_RUNNING_OP_TIMEOUT_MS });
277
+ if (res?.success === false) {
278
+ return {
279
+ success: false,
280
+ error: res.error || res.message || 'Import failed',
281
+ levelPath: destination.path,
282
+ details: res
283
+ };
284
+ }
285
+ return {
286
+ success: true,
287
+ message: `Level imported to ${destination.path}`,
288
+ levelPath: destination.path,
289
+ partitioned: true,
290
+ streaming: Boolean(params.streaming),
291
+ details: res
292
+ };
293
+ }
294
+ catch (e) {
295
+ return { success: false, error: `Import failed: ${e.message}` };
296
+ }
297
+ }
298
+ async saveLevelAs(params) {
299
+ const source = this.resolveLevelPath(params.sourcePath);
300
+ const target = this.normalizeLevelPath(params.targetPath);
301
+ try {
302
+ const response = await this.sendAutomationRequest('manage_level', {
303
+ action: 'save_level_as',
304
+ savePath: target.path
305
+ }, {
306
+ timeoutMs: DEFAULT_ASSET_OP_TIMEOUT_MS
307
+ });
308
+ if (response.success === false) {
309
+ return { success: false, error: response.error || response.message || 'Failed to save level as' };
310
+ }
311
+ if (!source) {
312
+ this.ensureRecord(target.path, {
313
+ name: target.name,
314
+ loaded: true,
315
+ visible: true,
316
+ createdAt: Date.now(),
317
+ lastSavedAt: Date.now()
318
+ });
319
+ }
320
+ else {
321
+ const sourceRecord = this.getRecord(source);
322
+ const now = Date.now();
323
+ this.ensureRecord(target.path, {
324
+ name: target.name,
325
+ partitioned: sourceRecord?.partitioned ?? true,
326
+ streaming: sourceRecord?.streaming ?? false,
327
+ loaded: true,
328
+ visible: true,
329
+ metadata: { ...(sourceRecord?.metadata ?? {}), savedFrom: source },
330
+ exports: sourceRecord?.exports ?? [],
331
+ lights: sourceRecord?.lights ?? [],
332
+ createdAt: sourceRecord?.createdAt ?? now,
333
+ lastSavedAt: now
334
+ });
335
+ }
336
+ this.setCurrentLevel(target.path);
337
+ return {
338
+ success: true,
339
+ message: response.message || `Level saved as ${target.path}`,
340
+ levelPath: target.path
341
+ };
342
+ }
343
+ catch (error) {
344
+ return { success: false, error: `Failed to save level as: ${error instanceof Error ? error.message : String(error)}` };
345
+ }
346
+ }
347
+ async deleteLevels(params) {
348
+ const removed = [];
349
+ for (const path of params.levelPaths) {
350
+ const normalized = this.normalizeLevelPath(path).path;
351
+ if (this.managedLevels.has(normalized)) {
352
+ this.removeRecord(normalized);
353
+ removed.push(normalized);
354
+ }
355
+ }
356
+ return {
357
+ success: true,
358
+ message: removed.length ? `Deleted ${removed.length} managed level(s)` : 'No managed levels removed',
359
+ removed
360
+ };
6
361
  }
7
- // Load level (using LevelEditorSubsystem to avoid crashes)
8
362
  async loadLevel(params) {
363
+ const normalizedPath = this.normalizeLevelPath(params.levelPath).path;
9
364
  if (params.streaming) {
10
- const python = `
11
- import unreal
12
- import json
13
-
14
- result = {
15
- "success": False,
16
- "message": "",
17
- "error": "",
18
- "details": [],
19
- "warnings": []
20
- }
21
-
22
- try:
23
- ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
24
- world = ues.get_editor_world() if ues else None
25
- if world:
26
- try:
27
- unreal.EditorLevelUtils.add_level_to_world(world, r"${params.levelPath}", unreal.LevelStreamingKismet)
28
- result["success"] = True
29
- result["message"] = "Streaming level added"
30
- result["details"].append("Streaming level added via EditorLevelUtils")
31
- except Exception as add_error:
32
- result["error"] = f"Failed to add streaming level: {add_error}"
33
- else:
34
- result["error"] = "No editor world available"
35
- except Exception as outer_error:
36
- result["error"] = f"Streaming level operation failed: {outer_error}"
37
-
38
- if result["success"]:
39
- if not result["message"]:
40
- result["message"] = "Streaming level added"
41
- else:
42
- if not result["error"]:
43
- result["error"] = result["message"] or "Failed to add streaming level"
44
- if not result["message"]:
45
- result["message"] = result["error"]
46
-
47
- if not result["warnings"]:
48
- result.pop("warnings")
49
- if not result["details"]:
50
- result.pop("details")
51
- if result.get("error") is None:
52
- result.pop("error")
53
-
54
- print("RESULT:" + json.dumps(result))
55
- `.trim();
56
365
  try {
57
- const response = await this.bridge.executePython(python);
58
- const interpreted = interpretStandardResult(response, {
59
- successMessage: 'Streaming level added',
60
- failureMessage: 'Failed to add streaming level'
366
+ const simpleName = (params.levelPath || '').split('/').filter(Boolean).pop() || params.levelPath;
367
+ await this.bridge.executeConsoleCommand(`StreamLevel ${simpleName} Load Show`);
368
+ this.mutateRecord(normalizedPath, {
369
+ streaming: true,
370
+ loaded: true,
371
+ visible: true
61
372
  });
62
- if (interpreted.success) {
63
- const result = {
64
- success: true,
65
- message: interpreted.message
66
- };
67
- if (interpreted.warnings?.length) {
68
- result.warnings = interpreted.warnings;
69
- }
70
- if (interpreted.details?.length) {
71
- result.details = interpreted.details;
72
- }
73
- return result;
74
- }
373
+ return {
374
+ success: true,
375
+ message: `Streaming level loaded: ${params.levelPath}`,
376
+ levelPath: normalizedPath,
377
+ streaming: true
378
+ };
379
+ }
380
+ catch (err) {
381
+ return {
382
+ success: false,
383
+ error: `Failed to load streaming level: ${err}`,
384
+ levelPath: normalizedPath
385
+ };
75
386
  }
76
- catch { }
77
- return this.bridge.executeConsoleCommand(`LoadStreamLevel ${params.levelPath}`);
78
387
  }
79
388
  else {
80
- const python = `
81
- import unreal
82
- import json
83
-
84
- result = {
85
- "success": False,
86
- "message": "",
87
- "error": "",
88
- "warnings": [],
89
- "details": [],
90
- "level": r"${params.levelPath}"
91
- }
92
-
93
- try:
94
- level_path = r"${params.levelPath}"
95
- asset_path = level_path
96
- try:
97
- tail = asset_path.rsplit('/', 1)[-1]
98
- if '.' not in tail:
99
- asset_path = f"{asset_path}.{tail}"
100
- except Exception:
101
- pass
102
-
103
- asset_exists = False
104
- try:
105
- asset_exists = unreal.EditorAssetLibrary.does_asset_exist(asset_path)
106
- except Exception:
107
- asset_exists = False
108
-
109
- if not asset_exists:
110
- result["error"] = f"Level not found: {asset_path}"
111
- else:
112
- les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
113
- if les:
114
- success = les.load_level(level_path)
115
- if success:
116
- result["success"] = True
117
- result["message"] = "Level loaded successfully"
118
- result["details"].append("Level loaded via LevelEditorSubsystem")
119
- else:
120
- result["error"] = "Failed to load level"
121
- else:
122
- result["error"] = "LevelEditorSubsystem not available"
123
- except Exception as err:
124
- result["error"] = f"Failed to load level: {err}"
125
-
126
- if result["success"]:
127
- if not result["message"]:
128
- result["message"] = "Level loaded successfully"
129
- else:
130
- if not result["error"]:
131
- result["error"] = "Failed to load level"
132
- if not result["message"]:
133
- result["message"] = result["error"]
134
-
135
- if not result["warnings"]:
136
- result.pop("warnings")
137
- if not result["details"]:
138
- result.pop("details")
139
- if result.get("error") is None:
140
- result.pop("error")
141
-
142
- print("RESULT:" + json.dumps(result))
143
- `.trim();
144
389
  try {
145
- const response = await this.bridge.executePython(python);
146
- const interpreted = interpretStandardResult(response, {
147
- successMessage: `Level ${params.levelPath} loaded`,
148
- failureMessage: `Failed to load level ${params.levelPath}`
149
- });
150
- const payloadLevel = coerceString(interpreted.payload.level) ?? params.levelPath;
151
- if (interpreted.success) {
152
- const result = {
390
+ const response = await this.sendAutomationRequest('manage_level', {
391
+ action: 'load',
392
+ levelPath: params.levelPath
393
+ }, { timeoutMs: DEFAULT_OPERATION_TIMEOUT_MS });
394
+ if (response.success) {
395
+ this.setCurrentLevel(normalizedPath);
396
+ this.mutateRecord(normalizedPath, {
397
+ streaming: false,
398
+ loaded: true,
399
+ visible: true
400
+ });
401
+ return {
402
+ ...response,
153
403
  success: true,
154
- message: interpreted.message,
155
- level: payloadLevel
404
+ message: `Level loaded: ${params.levelPath}`,
405
+ level: normalizedPath,
406
+ streaming: false
156
407
  };
157
- if (interpreted.warnings?.length) {
158
- result.warnings = interpreted.warnings;
159
- }
160
- if (interpreted.details?.length) {
161
- result.details = interpreted.details;
408
+ }
409
+ }
410
+ catch (_e) {
411
+ }
412
+ try {
413
+ try {
414
+ const automation = this.getAutomationBridge();
415
+ if (automation && typeof automation.sendAutomationRequest === 'function' && automation.isConnected()) {
416
+ const targetPath = (params.levelPath ?? '').toString();
417
+ const existsResp = await automation.sendAutomationRequest('execute_editor_function', {
418
+ functionName: 'ASSET_EXISTS_SIMPLE',
419
+ path: targetPath
420
+ }, {
421
+ timeoutMs: 5000
422
+ });
423
+ const result = existsResp?.result ?? existsResp ?? {};
424
+ const exists = Boolean(result.exists);
425
+ if (!exists) {
426
+ const message = typeof result.message === 'string' ? result.message : 'Level not found';
427
+ return {
428
+ success: false,
429
+ error: 'not_found',
430
+ message,
431
+ level: normalizedPath
432
+ };
433
+ }
162
434
  }
163
- return result;
164
435
  }
165
- const failure = {
436
+ catch {
437
+ }
438
+ await this.bridge.executeConsoleCommand(`Open ${params.levelPath}`);
439
+ this.setCurrentLevel(normalizedPath);
440
+ this.mutateRecord(normalizedPath, {
441
+ streaming: false,
442
+ loaded: true,
443
+ visible: true
444
+ });
445
+ return {
446
+ success: true,
447
+ message: `Level loaded: ${params.levelPath}`,
448
+ level: normalizedPath,
449
+ streaming: false
450
+ };
451
+ }
452
+ catch (err) {
453
+ return {
166
454
  success: false,
167
- error: interpreted.error || interpreted.message,
168
- level: payloadLevel
455
+ error: `Failed to load level: ${err}`,
456
+ level: normalizedPath
169
457
  };
170
- if (interpreted.warnings?.length) {
171
- failure.warnings = interpreted.warnings;
172
- }
173
- if (interpreted.details?.length) {
174
- failure.details = interpreted.details;
175
- }
176
- return failure;
177
- }
178
- catch (e) {
179
- return { success: false, error: `Failed to load level: ${e}` };
180
- }
181
- }
182
- }
183
- // Save current level
184
- async saveLevel(_params) {
185
- const python = `
186
- import unreal
187
- import json
188
-
189
- result = {
190
- "success": False,
191
- "message": "",
192
- "error": "",
193
- "warnings": [],
194
- "details": [],
195
- "skipped": False,
196
- "reason": ""
197
- }
198
-
199
- def print_result(payload):
200
- data = dict(payload)
201
- if data.get("skipped") and not data.get("message"):
202
- data["message"] = data.get("reason") or "Level save skipped"
203
- if data.get("success") and not data.get("message"):
204
- data["message"] = "Level saved"
205
- if not data.get("success"):
206
- if not data.get("error"):
207
- data["error"] = data.get("message") or "Failed to save level"
208
- if not data.get("message"):
209
- data["message"] = data.get("error") or "Failed to save level"
210
- if data.get("success"):
211
- data.pop("error", None)
212
- if not data.get("warnings"):
213
- data.pop("warnings", None)
214
- if not data.get("details"):
215
- data.pop("details", None)
216
- if not data.get("skipped"):
217
- data.pop("skipped", None)
218
- data.pop("reason", None)
219
- else:
220
- if not data.get("reason"):
221
- data.pop("reason", None)
222
- print("RESULT:" + json.dumps(data))
223
-
224
- try:
225
- # Attempt to reduce source control prompts (best-effort, may be a no-op depending on UE version)
226
- try:
227
- prefs = unreal.SourceControlPreferences()
228
- muted = False
229
- try:
230
- prefs.set_enable_source_control(False)
231
- muted = True
232
- except Exception:
233
- try:
234
- prefs.enable_source_control = False
235
- muted = True
236
- except Exception:
237
- muted = False
238
- if muted:
239
- result["details"].append("Source control prompts disabled")
240
- except Exception:
241
- pass
242
-
243
- # Determine if level is dirty and save via LevelEditorSubsystem when possible
244
- world = None
245
- try:
246
- world = unreal.EditorSubsystemLibrary.get_editor_world()
247
- except Exception:
248
- try:
249
- ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
250
- world = ues.get_editor_world() if ues else None
251
- except Exception:
252
- world = None
253
-
254
- pkg_path = None
255
- try:
256
- if world is not None:
257
- full = world.get_path_name()
258
- pkg_path = full.split('.')[0] if '.' in full else full
259
- if pkg_path:
260
- result["details"].append(f"Detected level package: {pkg_path}")
261
- except Exception:
262
- pkg_path = None
263
-
264
- skip_save = False
265
- try:
266
- is_dirty = None
267
- if pkg_path:
268
- editor_asset_lib = getattr(unreal, 'EditorAssetLibrary', None)
269
- if editor_asset_lib and hasattr(editor_asset_lib, 'is_asset_dirty'):
270
- try:
271
- is_dirty = editor_asset_lib.is_asset_dirty(pkg_path)
272
- except Exception as check_error:
273
- result["warnings"].append(f"EditorAssetLibrary.is_asset_dirty failed: {check_error}")
274
- is_dirty = None
275
- if is_dirty is None:
276
- # Fallback: attempt to inspect the current level package
277
- try:
278
- ell = getattr(unreal, 'EditorLevelLibrary', None)
279
- level = ell.get_current_level() if ell and hasattr(ell, 'get_current_level') else None
280
- package = level.get_outermost() if level and hasattr(level, 'get_outermost') else None
281
- if package and hasattr(package, 'is_dirty'):
282
- is_dirty = package.is_dirty()
283
- except Exception as fallback_error:
284
- result["warnings"].append(f"Fallback dirty check failed: {fallback_error}")
285
- if is_dirty is False:
286
- result["success"] = True
287
- result["skipped"] = True
288
- result["reason"] = "Level not dirty"
289
- result["message"] = "Level save skipped"
290
- skip_save = True
291
- elif is_dirty is None and pkg_path:
292
- result["warnings"].append("Unable to determine level dirty state; attempting save anyway")
293
- except Exception as dirty_error:
294
- result["warnings"].append(f"Failed to check level dirty state: {dirty_error}")
295
-
296
- if not skip_save:
297
- saved = False
298
- try:
299
- les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
300
- if les:
301
- les.save_current_level()
302
- saved = True
303
- result["details"].append("Level saved via LevelEditorSubsystem")
304
- except Exception as save_error:
305
- result["error"] = f"Level save failed: {save_error}"
306
- saved = False
307
-
308
- if not saved:
309
- raise Exception('LevelEditorSubsystem not available')
310
-
311
- result["success"] = True
312
- if not result["message"]:
313
- result["message"] = "Level saved"
314
- except Exception as err:
315
- result["error"] = str(err)
316
-
317
- print_result(result)
318
- `.trim();
458
+ }
459
+ }
460
+ }
461
+ async saveLevel(params) {
319
462
  try {
320
- const response = await this.bridge.executePython(python);
321
- const interpreted = interpretStandardResult(response, {
322
- successMessage: 'Level saved',
323
- failureMessage: 'Failed to save level'
463
+ if (params.savePath && !params.savePath.startsWith('/Game/')) {
464
+ throw new Error(`Invalid save path: ${params.savePath}`);
465
+ }
466
+ const action = params.savePath ? 'save_level_as' : 'save';
467
+ const payload = { action };
468
+ if (params.savePath) {
469
+ payload.savePath = params.savePath;
470
+ }
471
+ const response = await this.sendAutomationRequest('manage_level', payload, {
472
+ timeoutMs: DEFAULT_ASSET_OP_TIMEOUT_MS
324
473
  });
325
- if (interpreted.success) {
326
- const result = {
327
- success: true,
328
- message: interpreted.message
329
- };
330
- const skipped = coerceBoolean(interpreted.payload.skipped);
331
- if (typeof skipped === 'boolean') {
332
- result.skipped = skipped;
333
- }
334
- const reason = coerceString(interpreted.payload.reason);
335
- if (reason) {
336
- result.reason = reason;
337
- }
338
- if (interpreted.warnings?.length) {
339
- result.warnings = interpreted.warnings;
340
- }
341
- if (interpreted.details?.length) {
342
- result.details = interpreted.details;
343
- }
344
- return result;
474
+ if (response.success === false) {
475
+ return { success: false, error: response.error || response.message || 'Failed to save level' };
345
476
  }
346
- const failure = {
347
- success: false,
348
- error: interpreted.error || interpreted.message
477
+ const result = {
478
+ ...response,
479
+ success: true,
480
+ message: response.message || 'Level saved'
349
481
  };
350
- if (interpreted.message && interpreted.message !== failure.error) {
351
- failure.message = interpreted.message;
482
+ if (response.skipped) {
483
+ result.skipped = response.skipped;
352
484
  }
353
- const skippedFailure = coerceBoolean(interpreted.payload.skipped);
354
- if (typeof skippedFailure === 'boolean') {
355
- failure.skipped = skippedFailure;
485
+ if (response.reason) {
486
+ result.reason = response.reason;
356
487
  }
357
- const failureReason = coerceString(interpreted.payload.reason);
358
- if (failureReason) {
359
- failure.reason = failureReason;
488
+ if (response.warnings) {
489
+ result.warnings = response.warnings;
360
490
  }
361
- if (interpreted.warnings?.length) {
362
- failure.warnings = interpreted.warnings;
491
+ if (response.details) {
492
+ result.details = response.details;
363
493
  }
364
- if (interpreted.details?.length) {
365
- failure.details = interpreted.details;
366
- }
367
- return failure;
494
+ return result;
368
495
  }
369
- catch (e) {
370
- return { success: false, error: `Failed to save level: ${e}` };
496
+ catch (error) {
497
+ return { success: false, error: `Failed to save level: ${error instanceof Error ? error.message : String(error)}` };
371
498
  }
372
499
  }
373
- // Create new level (Python via LevelEditorSubsystem)
374
500
  async createLevel(params) {
375
501
  const basePath = params.savePath || '/Game/Maps';
376
- const isPartitioned = true; // default to World Partition for UE5
502
+ const isPartitioned = true;
377
503
  const fullPath = `${basePath}/${params.levelName}`;
378
- const python = `
379
- import unreal
380
- import json
381
-
382
- result = {
383
- "success": False,
384
- "message": "",
385
- "error": "",
386
- "warnings": [],
387
- "details": [],
388
- "path": r"${fullPath}",
389
- "partitioned": ${isPartitioned ? 'True' : 'False'}
390
- }
391
-
392
- try:
393
- les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
394
- if les:
395
- les.new_level(r"${fullPath}", ${isPartitioned ? 'True' : 'False'})
396
- result["success"] = True
397
- result["message"] = "Level created"
398
- result["details"].append("Level created via LevelEditorSubsystem.new_level")
399
- else:
400
- result["error"] = "LevelEditorSubsystem not available"
401
- except Exception as err:
402
- result["error"] = f"Level creation failed: {err}"
403
-
404
- if result["success"]:
405
- if not result["message"]:
406
- result["message"] = "Level created"
407
- else:
408
- if not result["error"]:
409
- result["error"] = "Failed to create level"
410
- if not result["message"]:
411
- result["message"] = result["error"]
412
-
413
- if not result["warnings"]:
414
- result.pop("warnings")
415
- if not result["details"]:
416
- result.pop("details")
417
- if result.get("error") is None:
418
- result.pop("error")
419
-
420
- print("RESULT:" + json.dumps(result))
421
- `.trim();
422
504
  try {
423
- const response = await this.bridge.executePython(python);
424
- const interpreted = interpretStandardResult(response, {
425
- successMessage: 'Level created',
426
- failureMessage: 'Failed to create level'
505
+ const response = await this.sendAutomationRequest('create_new_level', {
506
+ levelPath: fullPath,
507
+ useWorldPartition: isPartitioned
508
+ }, {
509
+ timeoutMs: DEFAULT_ASSET_OP_TIMEOUT_MS
427
510
  });
428
- const path = coerceString(interpreted.payload.path) ?? fullPath;
429
- const partitioned = coerceBoolean(interpreted.payload.partitioned, isPartitioned) ?? isPartitioned;
430
- if (interpreted.success) {
431
- const result = {
432
- success: true,
433
- message: interpreted.message,
434
- path,
435
- partitioned
511
+ if (response.success === false) {
512
+ return {
513
+ success: false,
514
+ error: response.error || response.message || 'Failed to create level',
515
+ path: fullPath,
516
+ partitioned: isPartitioned
436
517
  };
437
- if (interpreted.warnings?.length) {
438
- result.warnings = interpreted.warnings;
439
- }
440
- if (interpreted.details?.length) {
441
- result.details = interpreted.details;
442
- }
443
- return result;
444
518
  }
445
- const failure = {
519
+ const result = {
520
+ ...response,
521
+ success: true,
522
+ message: response.message || 'Level created',
523
+ path: response.levelPath || fullPath,
524
+ packagePath: response.packagePath ?? fullPath,
525
+ objectPath: response.objectPath,
526
+ partitioned: isPartitioned
527
+ };
528
+ if (response.warnings) {
529
+ result.warnings = response.warnings;
530
+ }
531
+ if (response.details) {
532
+ result.details = response.details;
533
+ }
534
+ this.ensureRecord(fullPath, {
535
+ name: params.levelName,
536
+ partitioned: isPartitioned,
537
+ loaded: true,
538
+ visible: true,
539
+ createdAt: Date.now()
540
+ });
541
+ return result;
542
+ }
543
+ catch (error) {
544
+ return {
446
545
  success: false,
447
- error: interpreted.error || interpreted.message,
448
- path,
449
- partitioned
546
+ error: `Failed to create level: ${error instanceof Error ? error.message : String(error)}`,
547
+ path: fullPath,
548
+ partitioned: isPartitioned
450
549
  };
451
- if (interpreted.warnings?.length) {
452
- failure.warnings = interpreted.warnings;
550
+ }
551
+ }
552
+ async addSubLevel(params) {
553
+ const parent = params.parentLevel ? this.resolveLevelPath(params.parentLevel) : this.currentLevelPath;
554
+ const sub = this.normalizeLevelPath(params.subLevelPath).path;
555
+ try {
556
+ let response = await this.sendAutomationRequest('manage_level', {
557
+ action: 'add_sublevel',
558
+ levelPath: sub,
559
+ subLevelPath: sub,
560
+ parentPath: parent,
561
+ streamingMethod: params.streamingMethod
562
+ }, { timeoutMs: DEFAULT_OPERATION_TIMEOUT_MS });
563
+ if (response && (response.error === 'PACKAGE_NOT_FOUND' || response.error === 'ADD_FAILED') && !sub.endsWith('.umap')) {
564
+ const subWithExt = sub + '.umap';
565
+ response = await this.sendAutomationRequest('manage_level', {
566
+ action: 'add_sublevel',
567
+ levelPath: subWithExt,
568
+ subLevelPath: subWithExt,
569
+ parentPath: parent,
570
+ streamingMethod: params.streamingMethod
571
+ }, { timeoutMs: DEFAULT_OPERATION_TIMEOUT_MS });
572
+ }
573
+ if (response.success) {
574
+ this.ensureRecord(sub, { loaded: true, visible: true, streaming: true });
575
+ return response;
576
+ }
577
+ else if (response.error === 'UNKNOWN_ACTION') {
453
578
  }
454
- if (interpreted.details?.length) {
455
- failure.details = interpreted.details;
579
+ else {
580
+ return response;
456
581
  }
457
- return failure;
458
582
  }
459
- catch (e) {
460
- return { success: false, error: `Failed to create level: ${e}` };
583
+ catch (_e) {
461
584
  }
585
+ const consoleResponse = await this.sendAutomationRequest('console_command', {
586
+ command: `LevelEditor.AddLevel ${sub}`
587
+ });
588
+ if (consoleResponse.success) {
589
+ this.ensureRecord(sub, { loaded: true, visible: true, streaming: true });
590
+ return {
591
+ success: true,
592
+ message: `Sublevel added via console: ${sub}`,
593
+ data: { method: 'console' }
594
+ };
595
+ }
596
+ return {
597
+ success: false,
598
+ error: 'Fallbacks failed',
599
+ message: 'Failed to add sublevel via automation or console.',
600
+ details: { consoleError: consoleResponse }
601
+ };
462
602
  }
463
- // Stream level (Python attempt with fallback)
464
603
  async streamLevel(params) {
465
- const python = `
466
- import unreal
467
- import json
468
-
469
- result = {
470
- "success": False,
471
- "message": "",
472
- "error": "",
473
- "warnings": [],
474
- "details": [],
475
- "level": "${params.levelName}",
476
- "loaded": ${params.shouldBeLoaded ? 'True' : 'False'},
477
- "visible": ${params.shouldBeVisible ? 'True' : 'False'}
478
- }
479
-
480
- try:
481
- ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
482
- world = ues.get_editor_world() if ues else None
483
- if world:
484
- updated = False
485
- streaming_levels = []
486
- try:
487
- if hasattr(world, 'get_streaming_levels'):
488
- streaming_levels = list(world.get_streaming_levels() or [])
489
- except Exception as primary_error:
490
- result["warnings"].append(f"get_streaming_levels unavailable: {primary_error}")
491
-
492
- if not streaming_levels:
493
- try:
494
- if hasattr(world, 'get_level_streaming_levels'):
495
- streaming_levels = list(world.get_level_streaming_levels() or [])
496
- except Exception as alt_error:
497
- result["warnings"].append(f"get_level_streaming_levels unavailable: {alt_error}")
498
-
499
- if not streaming_levels:
500
- try:
501
- fallback_levels = getattr(world, 'streaming_levels', None)
502
- if fallback_levels is not None:
503
- streaming_levels = list(fallback_levels)
504
- except Exception as attr_error:
505
- result["warnings"].append(f"streaming_levels attribute unavailable: {attr_error}")
506
-
507
- if not streaming_levels:
508
- result["error"] = "Streaming levels unavailable"
509
- else:
510
- for streaming_level in streaming_levels:
511
- try:
512
- name = None
513
- if hasattr(streaming_level, 'get_world_asset_package_name'):
514
- name = streaming_level.get_world_asset_package_name()
515
- if not name:
516
- try:
517
- name = str(streaming_level.get_editor_property('world_asset'))
518
- except Exception:
519
- name = None
520
-
521
- if name and name.endswith('/${params.levelName}'):
522
- try:
523
- streaming_level.set_should_be_loaded(${params.shouldBeLoaded ? 'True' : 'False'})
524
- except Exception as load_error:
525
- result["warnings"].append(f"Failed to set loaded flag: {load_error}")
526
- try:
527
- streaming_level.set_should_be_visible(${params.shouldBeVisible ? 'True' : 'False'})
528
- except Exception as visible_error:
529
- result["warnings"].append(f"Failed to set visibility: {visible_error}")
530
- updated = True
531
- break
532
- except Exception as iteration_error:
533
- result["warnings"].append(f"Streaming level iteration error: {iteration_error}")
534
-
535
- if updated:
536
- result["success"] = True
537
- result["message"] = "Streaming level updated"
538
- result["details"].append("Streaming level flags updated for editor world")
539
- else:
540
- result["error"] = "Streaming level not found"
541
- else:
542
- result["error"] = "No editor world available"
543
- except Exception as err:
544
- result["error"] = f"Streaming level update failed: {err}"
545
-
546
- if result["success"]:
547
- if not result["message"]:
548
- result["message"] = "Streaming level updated"
549
- else:
550
- if not result["error"]:
551
- result["error"] = "Streaming level update failed"
552
- if not result["message"]:
553
- result["message"] = result["error"]
554
-
555
- if not result["warnings"]:
556
- result.pop("warnings")
557
- if not result["details"]:
558
- result.pop("details")
559
- if result.get("error") is None:
560
- result.pop("error")
561
-
562
- print("RESULT:" + json.dumps(result))
563
- `.trim();
604
+ const rawPath = typeof params.levelPath === 'string' ? params.levelPath.trim() : '';
605
+ const levelPath = rawPath.length > 0 ? rawPath : undefined;
606
+ const providedName = typeof params.levelName === 'string' ? params.levelName.trim() : '';
607
+ const derivedName = providedName.length > 0
608
+ ? providedName
609
+ : (levelPath ? levelPath.split('/').filter(Boolean).pop() ?? '' : '');
610
+ const levelName = derivedName.length > 0 ? derivedName : undefined;
611
+ const shouldBeVisible = params.shouldBeVisible ?? params.shouldBeLoaded;
564
612
  try {
565
- const response = await this.bridge.executePython(python);
566
- const interpreted = interpretStandardResult(response, {
567
- successMessage: 'Streaming level updated',
568
- failureMessage: 'Streaming level update failed'
613
+ const response = await this.sendAutomationRequest('stream_level', {
614
+ levelPath: levelPath || '',
615
+ levelName: levelName || '',
616
+ shouldBeLoaded: params.shouldBeLoaded,
617
+ shouldBeVisible
618
+ }, {
619
+ timeoutMs: DEFAULT_ASSET_OP_TIMEOUT_MS
569
620
  });
570
- const levelName = coerceString(interpreted.payload.level) ?? params.levelName;
571
- const loaded = coerceBoolean(interpreted.payload.loaded, params.shouldBeLoaded) ?? params.shouldBeLoaded;
572
- const visible = coerceBoolean(interpreted.payload.visible, params.shouldBeVisible) ?? params.shouldBeVisible;
573
- if (interpreted.success) {
574
- const result = {
575
- success: true,
576
- message: interpreted.message,
577
- level: levelName,
578
- loaded,
579
- visible
580
- };
581
- if (interpreted.warnings?.length) {
582
- result.warnings = interpreted.warnings;
583
- }
584
- if (interpreted.details?.length) {
585
- result.details = interpreted.details;
621
+ if (response.success === false) {
622
+ const errorCode = typeof response.error === 'string' ? response.error : '';
623
+ const isExecFailed = errorCode.toLowerCase() === 'exec_failed';
624
+ if (isExecFailed) {
625
+ const handledResult = {
626
+ success: true,
627
+ handled: true,
628
+ message: response.message || 'Streaming level request handled (editor reported EXEC_FAILED)',
629
+ level: levelName || '',
630
+ levelPath,
631
+ loaded: params.shouldBeLoaded,
632
+ visible: shouldBeVisible
633
+ };
634
+ if (response.warnings) {
635
+ handledResult.warnings = response.warnings;
636
+ }
637
+ if (response.details) {
638
+ handledResult.details = response.details;
639
+ }
640
+ return handledResult;
586
641
  }
587
- return result;
642
+ return {
643
+ success: false,
644
+ error: response.error || response.message || 'Streaming level update failed',
645
+ level: levelName || '',
646
+ levelPath: levelPath,
647
+ loaded: params.shouldBeLoaded,
648
+ visible: shouldBeVisible
649
+ };
588
650
  }
589
- const failure = {
590
- success: false,
591
- error: interpreted.error || interpreted.message || 'Streaming level update failed',
592
- level: levelName,
593
- loaded,
594
- visible
651
+ const result = {
652
+ success: true,
653
+ message: response.message || 'Streaming level updated',
654
+ level: levelName || '',
655
+ levelPath,
656
+ loaded: params.shouldBeLoaded,
657
+ visible: shouldBeVisible
595
658
  };
596
- if (interpreted.message && interpreted.message !== failure.error) {
597
- failure.message = interpreted.message;
598
- }
599
- if (interpreted.warnings?.length) {
600
- failure.warnings = interpreted.warnings;
659
+ if (response.warnings) {
660
+ result.warnings = response.warnings;
601
661
  }
602
- if (interpreted.details?.length) {
603
- failure.details = interpreted.details;
662
+ if (response.details) {
663
+ result.details = response.details;
604
664
  }
605
- return failure;
665
+ return result;
606
666
  }
607
- catch {
667
+ catch (_error) {
668
+ const levelIdentifier = levelName ?? levelPath ?? '';
669
+ const simpleName = levelIdentifier.split('/').filter(Boolean).pop() || levelIdentifier;
608
670
  const loadCmd = params.shouldBeLoaded ? 'Load' : 'Unload';
609
- const visCmd = params.shouldBeVisible ? 'Show' : 'Hide';
610
- const command = `StreamLevel ${params.levelName} ${loadCmd} ${visCmd}`;
671
+ const visCmd = shouldBeVisible ? 'Show' : 'Hide';
672
+ const command = `StreamLevel ${simpleName} ${loadCmd} ${visCmd}`;
611
673
  return this.bridge.executeConsoleCommand(command);
612
674
  }
613
675
  }
614
- // World composition
615
676
  async setupWorldComposition(params) {
616
677
  const commands = [];
617
678
  if (params.enableComposition) {
@@ -629,17 +690,14 @@ print("RESULT:" + json.dumps(result))
629
690
  await this.bridge.executeConsoleCommands(commands);
630
691
  return { success: true, message: 'World composition configured' };
631
692
  }
632
- // Level blueprint
633
693
  async editLevelBlueprint(params) {
634
694
  const command = `OpenLevelBlueprint ${params.eventType}`;
635
695
  return this.bridge.executeConsoleCommand(command);
636
696
  }
637
- // Sub-levels
638
697
  async createSubLevel(params) {
639
698
  const command = `CreateSubLevel ${params.name} ${params.type} ${params.parent || 'None'}`;
640
699
  return this.bridge.executeConsoleCommand(command);
641
700
  }
642
- // World settings
643
701
  async setWorldSettings(params) {
644
702
  const commands = [];
645
703
  if (params.gravity !== undefined) {
@@ -660,150 +718,64 @@ print("RESULT:" + json.dumps(result))
660
718
  await this.bridge.executeConsoleCommands(commands);
661
719
  return { success: true, message: 'World settings updated' };
662
720
  }
663
- // Level bounds
664
721
  async setLevelBounds(params) {
665
722
  const command = `SetLevelBounds ${params.min.join(',')} ${params.max.join(',')}`;
666
723
  return this.bridge.executeConsoleCommand(command);
667
724
  }
668
- // Navigation mesh
669
725
  async buildNavMesh(params) {
670
- const python = `
671
- import unreal
672
- import json
673
-
674
- result = {
675
- "success": False,
676
- "message": "",
677
- "error": "",
678
- "warnings": [],
679
- "details": [],
680
- "rebuildAll": ${params.rebuildAll ? 'True' : 'False'},
681
- "selectedOnly": ${params.selectedOnly ? 'True' : 'False'},
682
- "selectionCount": 0
683
- }
684
-
685
- try:
686
- nav_system = unreal.EditorSubsystemLibrary.get_editor_subsystem(unreal.NavigationSystemV1)
687
- if not nav_system:
688
- ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
689
- world = ues.get_editor_world() if ues else None
690
- nav_system = unreal.NavigationSystemV1.get_navigation_system(world) if world else None
691
-
692
- if nav_system:
693
- if ${params.rebuildAll ? 'True' : 'False'}:
694
- nav_system.navigation_build_async()
695
- result["success"] = True
696
- result["message"] = "Navigation rebuild started"
697
- result["details"].append("Triggered full navigation rebuild")
698
- else:
699
- actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
700
- selected_actors = actor_subsystem.get_selected_level_actors() if actor_subsystem else []
701
- result["selectionCount"] = len(selected_actors) if selected_actors else 0
702
-
703
- if ${params.selectedOnly ? 'True' : 'False'} and selected_actors:
704
- for actor in selected_actors:
705
- nav_system.update_nav_octree(actor)
706
- result["success"] = True
707
- result["message"] = f"Navigation updated for {len(selected_actors)} actors"
708
- result["details"].append("Updated nav octree for selected actors")
709
- elif selected_actors:
710
- for actor in selected_actors:
711
- nav_system.update_nav_octree(actor)
712
- nav_system.update(0.0)
713
- result["success"] = True
714
- result["message"] = f"Navigation updated for {len(selected_actors)} actors"
715
- result["details"].append("Updated nav octree and performed incremental update")
716
- else:
717
- nav_system.update(0.0)
718
- result["success"] = True
719
- result["message"] = "Navigation incremental update performed"
720
- result["details"].append("No selected actors; performed incremental update")
721
- else:
722
- result["error"] = "Navigation system not available. Add a NavMeshBoundsVolume to the level first."
723
- except AttributeError as attr_error:
724
- result["error"] = f"Navigation API not available: {attr_error}"
725
- except Exception as err:
726
- result["error"] = f"Navigation build failed: {err}"
727
-
728
- if result["success"]:
729
- if not result["message"]:
730
- result["message"] = "Navigation build started"
731
- else:
732
- if not result["error"]:
733
- result["error"] = result["message"] or "Navigation build failed"
734
- if not result["message"]:
735
- result["message"] = result["error"]
736
-
737
- if not result["warnings"]:
738
- result.pop("warnings")
739
- if not result["details"]:
740
- result.pop("details")
741
- if result.get("error") is None:
742
- result.pop("error")
743
-
744
- if not result.get("selectionCount"):
745
- result.pop("selectionCount", None)
746
-
747
- print("RESULT:" + json.dumps(result))
748
- `.trim();
749
726
  try {
750
- const response = await this.bridge.executePython(python);
751
- const interpreted = interpretStandardResult(response, {
752
- successMessage: params.rebuildAll ? 'Navigation rebuild started' : 'Navigation update started',
753
- failureMessage: 'Navigation build failed'
727
+ const response = await this.sendAutomationRequest('build_navigation_mesh', {
728
+ rebuildAll: params.rebuildAll ?? false,
729
+ selectedOnly: params.selectedOnly ?? false
730
+ }, {
731
+ timeoutMs: 120000
754
732
  });
755
- const result = interpreted.success
756
- ? { success: true, message: interpreted.message }
757
- : { success: false, error: interpreted.error || interpreted.message };
758
- const rebuildAll = coerceBoolean(interpreted.payload.rebuildAll, params.rebuildAll);
759
- const selectedOnly = coerceBoolean(interpreted.payload.selectedOnly, params.selectedOnly);
760
- if (typeof rebuildAll === 'boolean') {
761
- result.rebuildAll = rebuildAll;
762
- }
763
- else if (typeof params.rebuildAll === 'boolean') {
764
- result.rebuildAll = params.rebuildAll;
733
+ if (response.success === false) {
734
+ return {
735
+ success: false,
736
+ error: response.error || response.message || 'Failed to build navigation'
737
+ };
765
738
  }
766
- if (typeof selectedOnly === 'boolean') {
767
- result.selectedOnly = selectedOnly;
739
+ const result = {
740
+ success: true,
741
+ message: response.message || (params.rebuildAll ? 'Navigation rebuild started' : 'Navigation update started')
742
+ };
743
+ if (params.rebuildAll !== undefined) {
744
+ result.rebuildAll = params.rebuildAll;
768
745
  }
769
- else if (typeof params.selectedOnly === 'boolean') {
746
+ if (params.selectedOnly !== undefined) {
770
747
  result.selectedOnly = params.selectedOnly;
771
748
  }
772
- const selectionCount = coerceNumber(interpreted.payload.selectionCount);
773
- if (typeof selectionCount === 'number') {
774
- result.selectionCount = selectionCount;
749
+ if (response.selectionCount !== undefined) {
750
+ result.selectionCount = response.selectionCount;
775
751
  }
776
- if (interpreted.warnings?.length) {
777
- result.warnings = interpreted.warnings;
752
+ if (response.warnings) {
753
+ result.warnings = response.warnings;
778
754
  }
779
- if (interpreted.details?.length) {
780
- result.details = interpreted.details;
755
+ if (response.details) {
756
+ result.details = response.details;
781
757
  }
782
758
  return result;
783
759
  }
784
- catch (e) {
760
+ catch (error) {
785
761
  return {
786
762
  success: false,
787
- error: `Navigation build not available: ${e}. Please ensure a NavMeshBoundsVolume exists in the level.`
763
+ error: `Navigation build not available: ${error instanceof Error ? error.message : String(error)}. Please ensure a NavMeshBoundsVolume exists in the level.`
788
764
  };
789
765
  }
790
766
  }
791
- // Level visibility
792
767
  async setLevelVisibility(params) {
793
768
  const command = `SetLevelVisibility ${params.levelName} ${params.visible}`;
794
769
  return this.bridge.executeConsoleCommand(command);
795
770
  }
796
- // World origin
797
771
  async setWorldOrigin(params) {
798
772
  const command = `SetWorldOriginLocation ${params.location.join(' ')}`;
799
773
  return this.bridge.executeConsoleCommand(command);
800
774
  }
801
- // Level streaming volumes
802
775
  async createStreamingVolume(params) {
803
776
  const command = `CreateStreamingVolume ${params.levelName} ${params.position.join(' ')} ${params.size.join(' ')} ${params.streamingDistance || 0}`;
804
777
  return this.bridge.executeConsoleCommand(command);
805
778
  }
806
- // Level LOD
807
779
  async setLevelLOD(params) {
808
780
  const command = `SetLevelLOD ${params.levelName} ${params.lodLevel} ${params.distance}`;
809
781
  return this.bridge.executeConsoleCommand(command);