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,664 +1,832 @@
1
- // Level management tools for Unreal Engine
2
- import { UnrealBridge } from '../unreal-bridge.js';
1
+ import { BaseTool } from './base-tool.js';
2
+ import { ILevelTools, StandardActionResponse } from '../types/tool-interfaces.js';
3
+ import { LevelResponse } from '../types/automation-responses.js';
4
+ import { wasmIntegration as _wasmIntegration } from '../wasm/index.js';
5
+ import { sanitizePath } from '../utils/path-security.js';
3
6
  import {
4
- coerceBoolean,
5
- coerceNumber,
6
- coerceString,
7
- interpretStandardResult
8
- } from '../utils/result-helpers.js';
7
+ DEFAULT_OPERATION_TIMEOUT_MS,
8
+ DEFAULT_ASSET_OP_TIMEOUT_MS,
9
+ LONG_RUNNING_OP_TIMEOUT_MS
10
+ } from '../constants.js';
11
+
12
+ type LevelExportRecord = { target: string; timestamp: number; note?: string };
13
+ type ManagedLevelRecord = {
14
+ path: string;
15
+ name: string;
16
+ partitioned: boolean;
17
+ streaming: boolean;
18
+ loaded: boolean;
19
+ visible: boolean;
20
+ createdAt: number;
21
+ lastSavedAt?: number;
22
+ metadata?: Record<string, unknown>;
23
+ exports: LevelExportRecord[];
24
+ lights: Array<{ name: string; type: string; createdAt: number; details?: Record<string, unknown> }>;
25
+ };
26
+
27
+ export class LevelTools extends BaseTool implements ILevelTools {
28
+ private managedLevels = new Map<string, ManagedLevelRecord>();
29
+ private listCache?: { result: { success: true; message: string; count: number; levels: any[] }; timestamp: number };
30
+ private readonly LIST_CACHE_TTL_MS = 750;
31
+ private currentLevelPath?: string;
32
+
33
+ private invalidateListCache() {
34
+ this.listCache = undefined;
35
+ }
36
+
37
+ private normalizeLevelPath(rawPath: string | undefined): { path: string; name: string } {
38
+ if (!rawPath) {
39
+ return { path: '/Game/Maps/Untitled', name: 'Untitled' };
40
+ }
41
+
42
+ let formatted = rawPath.replace(/\\/g, '/').trim();
43
+ if (!formatted.startsWith('/')) {
44
+ formatted = formatted.startsWith('Game/') ? `/${formatted}` : `/Game/${formatted.replace(/^\/?Game\//i, '')}`;
45
+ }
46
+ if (!formatted.startsWith('/Game/')) {
47
+ formatted = `/Game/${formatted.replace(/^\/+/, '')}`;
48
+ }
49
+
50
+ // Security validation
51
+ try {
52
+ formatted = sanitizePath(formatted);
53
+ } catch (e: any) {
54
+ // If sanitizePath fails, we should probably propagate that error,
55
+ // but normalizeLevelPath signature expects to return an object.
56
+ // For now, let's log and rethrow or fallback?
57
+ // Throwing is safer as it prevents operation on invalid path.
58
+ throw new Error(`Security validation failed for level path: ${e.message}`);
59
+ }
60
+
61
+ formatted = formatted.replace(/\.umap$/i, '');
62
+ if (formatted.endsWith('/')) {
63
+ formatted = formatted.slice(0, -1);
64
+ }
65
+ const segments = formatted.split('/').filter(Boolean);
66
+ const lastSegment = segments[segments.length - 1] ?? 'Untitled';
67
+ const name = lastSegment.includes('.') ? lastSegment.split('.').pop() ?? lastSegment : lastSegment;
68
+ return { path: formatted, name: name || 'Untitled' };
69
+ }
70
+
71
+ private ensureRecord(path: string, seed?: Partial<ManagedLevelRecord>): ManagedLevelRecord {
72
+ const normalized = this.normalizeLevelPath(path);
73
+ let record = this.managedLevels.get(normalized.path);
74
+ if (!record) {
75
+ record = {
76
+ path: normalized.path,
77
+ name: seed?.name ?? normalized.name,
78
+ partitioned: seed?.partitioned ?? false,
79
+ streaming: seed?.streaming ?? false,
80
+ loaded: seed?.loaded ?? false,
81
+ visible: seed?.visible ?? false,
82
+ createdAt: seed?.createdAt ?? Date.now(),
83
+ lastSavedAt: seed?.lastSavedAt,
84
+ metadata: seed?.metadata ? { ...seed.metadata } : undefined,
85
+ exports: seed?.exports ? [...seed.exports] : [],
86
+ lights: seed?.lights ? [...seed.lights] : []
87
+ };
88
+ this.managedLevels.set(normalized.path, record);
89
+ this.invalidateListCache();
90
+ }
91
+ return record;
92
+ }
93
+
94
+ private mutateRecord(path: string | undefined, updates: Partial<ManagedLevelRecord>): ManagedLevelRecord | undefined {
95
+ if (!path || !path.trim()) {
96
+ return undefined;
97
+ }
98
+
99
+ const record = this.ensureRecord(path, updates);
100
+ let changed = false;
101
+
102
+ if (updates.name !== undefined && updates.name !== record.name) {
103
+ record.name = updates.name;
104
+ changed = true;
105
+ }
106
+ if (updates.partitioned !== undefined && updates.partitioned !== record.partitioned) {
107
+ record.partitioned = updates.partitioned;
108
+ changed = true;
109
+ }
110
+ if (updates.streaming !== undefined && updates.streaming !== record.streaming) {
111
+ record.streaming = updates.streaming;
112
+ changed = true;
113
+ }
114
+ if (updates.loaded !== undefined && updates.loaded !== record.loaded) {
115
+ record.loaded = updates.loaded;
116
+ changed = true;
117
+ }
118
+ if (updates.visible !== undefined && updates.visible !== record.visible) {
119
+ record.visible = updates.visible;
120
+ changed = true;
121
+ }
122
+ if (updates.createdAt !== undefined && updates.createdAt !== record.createdAt) {
123
+ record.createdAt = updates.createdAt;
124
+ changed = true;
125
+ }
126
+ if (updates.lastSavedAt !== undefined && updates.lastSavedAt !== record.lastSavedAt) {
127
+ record.lastSavedAt = updates.lastSavedAt;
128
+ changed = true;
129
+ }
130
+ if (updates.metadata) {
131
+ record.metadata = { ...(record.metadata ?? {}), ...updates.metadata };
132
+ changed = true;
133
+ }
134
+ if (updates.exports && updates.exports.length > 0) {
135
+ record.exports = [...record.exports, ...updates.exports];
136
+ changed = true;
137
+ }
138
+ if (updates.lights && updates.lights.length > 0) {
139
+ record.lights = [...record.lights, ...updates.lights];
140
+ changed = true;
141
+ }
142
+
143
+ if (changed) {
144
+ this.invalidateListCache();
145
+ }
146
+
147
+ return record;
148
+ }
149
+
150
+ private getRecord(path: string | undefined): ManagedLevelRecord | undefined {
151
+ if (!path || !path.trim()) {
152
+ return undefined;
153
+ }
154
+ const normalized = this.normalizeLevelPath(path);
155
+ return this.managedLevels.get(normalized.path);
156
+ }
157
+
158
+ private resolveLevelPath(explicit?: string): string | undefined {
159
+ if (explicit && explicit.trim()) {
160
+ return this.normalizeLevelPath(explicit).path;
161
+ }
162
+ return this.currentLevelPath;
163
+ }
164
+
165
+ private removeRecord(path: string) {
166
+ const normalized = this.normalizeLevelPath(path);
167
+ if (this.managedLevels.delete(normalized.path)) {
168
+ if (this.currentLevelPath === normalized.path) {
169
+ this.currentLevelPath = undefined;
170
+ }
171
+ this.invalidateListCache();
172
+ }
173
+ }
174
+
175
+ private listManagedLevels(): { success: true; message: string; count: number; levels: Array<Record<string, unknown>> } {
176
+ const now = Date.now();
177
+ if (this.listCache && now - this.listCache.timestamp < this.LIST_CACHE_TTL_MS) {
178
+ return this.listCache.result;
179
+ }
180
+
181
+ const levels = Array.from(this.managedLevels.values()).map((record) => ({
182
+ path: record.path,
183
+ name: record.name,
184
+ partitioned: record.partitioned,
185
+ streaming: record.streaming,
186
+ loaded: record.loaded,
187
+ visible: record.visible,
188
+ createdAt: record.createdAt,
189
+ lastSavedAt: record.lastSavedAt,
190
+ exports: record.exports,
191
+ lightCount: record.lights.length
192
+ }));
193
+
194
+ const result = { success: true as const, message: 'Managed levels listed', count: levels.length, levels };
195
+ this.listCache = { result, timestamp: now };
196
+ return result;
197
+ }
198
+
199
+ private summarizeLevel(path: string): Record<string, unknown> {
200
+ const record = this.getRecord(path);
201
+ if (!record) {
202
+ return { success: false, error: `Level not tracked: ${path}` };
203
+ }
204
+
205
+ return {
206
+ success: true,
207
+ message: 'Level summary ready',
208
+ path: record.path,
209
+ name: record.name,
210
+ partitioned: record.partitioned,
211
+ streaming: record.streaming,
212
+ loaded: record.loaded,
213
+ visible: record.visible,
214
+ createdAt: record.createdAt,
215
+ lastSavedAt: record.lastSavedAt,
216
+ exports: record.exports,
217
+ lights: record.lights,
218
+ metadata: record.metadata
219
+ };
220
+ }
221
+
222
+ private setCurrentLevel(path: string) {
223
+ const normalized = this.normalizeLevelPath(path);
224
+ this.currentLevelPath = normalized.path;
225
+ this.ensureRecord(normalized.path, { loaded: true, visible: true });
226
+ }
227
+
228
+ async listLevels(): Promise<StandardActionResponse> {
229
+ // Try to get actual levels from UE via automation bridge
230
+ try {
231
+ const response = await this.sendAutomationRequest<LevelResponse>('list_levels', {}, {
232
+ timeoutMs: 10000
233
+ });
234
+
235
+ if (response && response.success !== false) {
236
+ // Also include managed levels for backwards compatibility and immediate visibility
237
+ const managed = this.listManagedLevels();
238
+
239
+ // Merge managed levels into the main list if not already present
240
+ const ueLevels = (response.allMaps || []) as any[];
241
+ const managedOnly = managed.levels.filter(m => !ueLevels.some(u => u.path === m.path));
242
+ const finalLevels = [...ueLevels, ...managedOnly];
243
+
244
+ const result: Record<string, unknown> = {
245
+ ...response,
246
+ success: true,
247
+ message: 'Levels listed from Unreal Engine',
248
+ levels: finalLevels,
249
+ currentMap: response.currentMap,
250
+ currentMapPath: response.currentMapPath,
251
+ currentWorldLevels: response.currentWorldLevels || [],
252
+ data: {
253
+ levels: finalLevels,
254
+ count: finalLevels.length
255
+ },
256
+ managedLevels: managed.levels,
257
+ managedLevelCount: managed.count
258
+ };
259
+
260
+ return result as StandardActionResponse;
261
+ }
262
+ } catch {
263
+ // Fall back to managed levels if automation bridge fails
264
+ }
265
+
266
+ // Fallback to locally managed levels
267
+ return this.listManagedLevels();
268
+ }
269
+
270
+ async getLevelSummary(levelPath?: string): Promise<StandardActionResponse> {
271
+ const resolved = this.resolveLevelPath(levelPath);
272
+ if (!resolved) {
273
+ return { success: false, error: 'No level specified' };
274
+ }
275
+ return this.summarizeLevel(resolved) as StandardActionResponse;
276
+ }
277
+
278
+ registerLight(levelPath: string | undefined, info: { name: string; type: string; details?: Record<string, unknown> }) {
279
+ const resolved = this.resolveLevelPath(levelPath);
280
+ if (!resolved) {
281
+ return;
282
+ }
283
+ this.mutateRecord(resolved, {
284
+ lights: [
285
+ {
286
+ name: info.name,
287
+ type: info.type,
288
+ createdAt: Date.now(),
289
+ details: info.details
290
+ }
291
+ ]
292
+ });
293
+ }
294
+
295
+ async exportLevel(params: { levelPath?: string; exportPath: string; note?: string; timeoutMs?: number }): Promise<StandardActionResponse> {
296
+ const resolved = this.resolveLevelPath(params.levelPath);
297
+ if (!resolved) {
298
+ return { success: false, error: 'No level specified for export' };
299
+ }
300
+
301
+ try {
302
+ const res = await this.sendAutomationRequest<LevelResponse>('manage_level', {
303
+ action: 'export_level',
304
+ levelPath: resolved,
305
+ exportPath: params.exportPath
306
+ }, { timeoutMs: params.timeoutMs ?? LONG_RUNNING_OP_TIMEOUT_MS });
307
+
308
+ if (res?.success === false) {
309
+ return {
310
+ success: false,
311
+ error: res.error || res.message || 'Export failed',
312
+ levelPath: resolved,
313
+ exportPath: params.exportPath,
314
+ details: res
315
+ } as StandardActionResponse;
316
+ }
317
+
318
+ return {
319
+ success: true,
320
+ message: `Level exported to ${params.exportPath}`,
321
+ levelPath: resolved,
322
+ exportPath: params.exportPath,
323
+ details: res
324
+ } as StandardActionResponse;
325
+ } catch (e: any) {
326
+ return { success: false, error: `Export failed: ${e.message}` };
327
+ }
328
+ }
329
+
330
+ async importLevel(params: { packagePath: string; destinationPath?: string; streaming?: boolean; timeoutMs?: number }): Promise<StandardActionResponse> {
331
+ const destination = params.destinationPath
332
+ ? this.normalizeLevelPath(params.destinationPath)
333
+ : this.normalizeLevelPath(`/Game/Maps/Imported_${Math.floor(Date.now() / 1000)}`);
334
+
335
+ try {
336
+ const res = await this.sendAutomationRequest<LevelResponse>('manage_level', {
337
+ action: 'import_level',
338
+ packagePath: params.packagePath,
339
+ destinationPath: destination.path
340
+ }, { timeoutMs: params.timeoutMs ?? LONG_RUNNING_OP_TIMEOUT_MS });
341
+
342
+ if ((res as any)?.success === false) {
343
+ return {
344
+ success: false,
345
+ error: (res as any).error || (res as any).message || 'Import failed',
346
+ levelPath: destination.path,
347
+ details: res
348
+ } as StandardActionResponse;
349
+ }
350
+
351
+ return {
352
+ success: true,
353
+ message: `Level imported to ${destination.path}`,
354
+ levelPath: destination.path,
355
+ partitioned: true,
356
+ streaming: Boolean(params.streaming),
357
+ details: res
358
+ } as StandardActionResponse;
359
+ } catch (e: any) {
360
+ return { success: false, error: `Import failed: ${e.message}` };
361
+ }
362
+ }
363
+
364
+ async saveLevelAs(params: { sourcePath?: string; targetPath: string }): Promise<StandardActionResponse> {
365
+ const source = this.resolveLevelPath(params.sourcePath);
366
+ const target = this.normalizeLevelPath(params.targetPath);
367
+
368
+ // Delegate to automation bridge
369
+ try {
370
+ const response = await this.sendAutomationRequest<LevelResponse>('manage_level', {
371
+ action: 'save_level_as',
372
+ savePath: target.path
373
+ }, {
374
+ timeoutMs: DEFAULT_ASSET_OP_TIMEOUT_MS
375
+ });
376
+
377
+ if (response.success === false) {
378
+ return { success: false, error: response.error || response.message || 'Failed to save level as' };
379
+ }
380
+
381
+ // If successful, update local state
382
+ if (!source) {
383
+ // If no source known, just ensure target record
384
+ this.ensureRecord(target.path, {
385
+ name: target.name,
386
+ loaded: true,
387
+ visible: true,
388
+ createdAt: Date.now(),
389
+ lastSavedAt: Date.now()
390
+ });
391
+ } else {
392
+ const sourceRecord = this.getRecord(source);
393
+ const now = Date.now();
394
+ this.ensureRecord(target.path, {
395
+ name: target.name,
396
+ partitioned: sourceRecord?.partitioned ?? true,
397
+ streaming: sourceRecord?.streaming ?? false,
398
+ loaded: true,
399
+ visible: true,
400
+ metadata: { ...(sourceRecord?.metadata ?? {}), savedFrom: source },
401
+ exports: sourceRecord?.exports ?? [],
402
+ lights: sourceRecord?.lights ?? [],
403
+ createdAt: sourceRecord?.createdAt ?? now,
404
+ lastSavedAt: now
405
+ });
406
+ }
407
+
408
+ this.setCurrentLevel(target.path);
409
+
410
+ return {
411
+ success: true,
412
+ message: response.message || `Level saved as ${target.path}`,
413
+ levelPath: target.path
414
+ } as StandardActionResponse;
415
+ } catch (error) {
416
+ return { success: false, error: `Failed to save level as: ${error instanceof Error ? error.message : String(error)}` };
417
+ }
418
+ }
9
419
 
10
- export class LevelTools {
11
- constructor(private bridge: UnrealBridge) {}
420
+ async deleteLevels(params: { levelPaths: string[] }): Promise<StandardActionResponse> {
421
+ const removed: string[] = [];
422
+ for (const path of params.levelPaths) {
423
+ const normalized = this.normalizeLevelPath(path).path;
424
+ if (this.managedLevels.has(normalized)) {
425
+ this.removeRecord(normalized);
426
+ removed.push(normalized);
427
+ }
428
+ }
429
+
430
+ return {
431
+ success: true,
432
+ message: removed.length ? `Deleted ${removed.length} managed level(s)` : 'No managed levels removed',
433
+ removed
434
+ } as StandardActionResponse;
435
+ }
12
436
 
13
- // Load level (using LevelEditorSubsystem to avoid crashes)
14
437
  async loadLevel(params: {
15
438
  levelPath: string;
16
439
  streaming?: boolean;
17
440
  position?: [number, number, number];
18
- }) {
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();
441
+ }): Promise<StandardActionResponse> {
442
+ const normalizedPath = this.normalizeLevelPath(params.levelPath).path;
66
443
 
444
+ if (params.streaming) {
67
445
  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'
446
+ const simpleName = (params.levelPath || '').split('/').filter(Boolean).pop() || params.levelPath;
447
+ await this.bridge.executeConsoleCommand(`StreamLevel ${simpleName} Load Show`);
448
+ this.mutateRecord(normalizedPath, {
449
+ streaming: true,
450
+ loaded: true,
451
+ visible: true
72
452
  });
73
-
74
- if (interpreted.success) {
75
- const result: Record<string, unknown> = {
453
+ return {
454
+ success: true,
455
+ message: `Streaming level loaded: ${params.levelPath}`,
456
+ levelPath: normalizedPath,
457
+ streaming: true
458
+ } as StandardActionResponse;
459
+ } catch (err) {
460
+ return {
461
+ success: false,
462
+ error: `Failed to load streaming level: ${err}`,
463
+ levelPath: normalizedPath
464
+ };
465
+ }
466
+ } else {
467
+ // Try loading via automation bridge first (more robust)
468
+ try {
469
+ const response = await this.sendAutomationRequest<LevelResponse>('manage_level', {
470
+ action: 'load',
471
+ levelPath: params.levelPath
472
+ }, { timeoutMs: DEFAULT_OPERATION_TIMEOUT_MS });
473
+
474
+ if (response.success) {
475
+ this.setCurrentLevel(normalizedPath);
476
+ this.mutateRecord(normalizedPath, {
477
+ streaming: false,
478
+ loaded: true,
479
+ visible: true
480
+ });
481
+ return {
482
+ ...response,
76
483
  success: true,
77
- message: interpreted.message
78
- };
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;
484
+ message: `Level loaded: ${params.levelPath}`,
485
+ level: normalizedPath,
486
+ streaming: false
487
+ } as StandardActionResponse;
86
488
  }
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();
489
+ } catch (_e) {
490
+ // Fallback to console logic
491
+ }
155
492
 
156
493
  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;
494
+ // Best-effort existence check using the Automation Bridge when available.
495
+ try {
496
+ const automation = this.getAutomationBridge();
497
+ if (automation && typeof automation.sendAutomationRequest === 'function' && automation.isConnected()) {
498
+ const targetPath = (params.levelPath ?? '').toString();
499
+ const existsResp: any = await automation.sendAutomationRequest('execute_editor_function', {
500
+ functionName: 'ASSET_EXISTS_SIMPLE',
501
+ path: targetPath
502
+ }, {
503
+ timeoutMs: 5000
504
+ });
505
+ const result = existsResp?.result ?? existsResp ?? {};
506
+ const exists = Boolean(result.exists);
507
+
508
+ if (!exists) {
509
+ const message = typeof result.message === 'string' ? result.message : 'Level not found';
510
+ return {
511
+ success: false,
512
+ error: 'not_found',
513
+ message,
514
+ level: normalizedPath
515
+ } as StandardActionResponse;
516
+ }
175
517
  }
176
- return result;
518
+ } catch {
519
+ // If the existence check fails for any reason, fall back to the console command path below.
177
520
  }
178
521
 
179
- const failure: Record<string, unknown> = {
522
+ await this.bridge.executeConsoleCommand(`Open ${params.levelPath}`);
523
+ this.setCurrentLevel(normalizedPath);
524
+ this.mutateRecord(normalizedPath, {
525
+ streaming: false,
526
+ loaded: true,
527
+ visible: true
528
+ });
529
+ return {
530
+ success: true,
531
+ message: `Level loaded: ${params.levelPath}`,
532
+ level: normalizedPath,
533
+ streaming: false
534
+ } as StandardActionResponse;
535
+ } catch (err) {
536
+ return {
180
537
  success: false,
181
- error: interpreted.error || interpreted.message,
182
- level: payloadLevel
538
+ error: `Failed to load level: ${err}`,
539
+ level: normalizedPath
183
540
  };
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
541
  }
194
542
  }
195
543
  }
196
544
 
197
- // Save current level
198
- async saveLevel(_params: {
545
+ async saveLevel(params: {
199
546
  levelName?: string;
200
547
  savePath?: string;
201
- }) {
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
- }
548
+ }): Promise<StandardActionResponse> {
549
+ try {
550
+ if (params.savePath && !params.savePath.startsWith('/Game/')) {
551
+ throw new Error(`Invalid save path: ${params.savePath}`);
552
+ }
215
553
 
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();
554
+ const action = params.savePath ? 'save_level_as' : 'save';
555
+ const payload: Record<string, unknown> = { action };
556
+ if (params.savePath) {
557
+ payload.savePath = params.savePath;
558
+ }
336
559
 
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'
560
+ const response = await this.sendAutomationRequest<LevelResponse>('manage_level', payload, {
561
+ timeoutMs: DEFAULT_ASSET_OP_TIMEOUT_MS
342
562
  });
343
563
 
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;
564
+ if (response.success === false) {
565
+ return { success: false, error: response.error || response.message || 'Failed to save level' };
364
566
  }
365
567
 
366
- const failure: Record<string, unknown> = {
367
- success: false,
368
- error: interpreted.error || interpreted.message
568
+ const result: Record<string, unknown> = {
569
+ ...response,
570
+ success: true,
571
+ message: response.message || 'Level saved'
369
572
  };
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;
573
+
574
+ if (response.skipped) {
575
+ result.skipped = response.skipped;
376
576
  }
377
- const failureReason = coerceString(interpreted.payload.reason);
378
- if (failureReason) {
379
- failure.reason = failureReason;
577
+ if (response.reason) {
578
+ result.reason = response.reason;
380
579
  }
381
- if (interpreted.warnings?.length) {
382
- failure.warnings = interpreted.warnings;
580
+ if (response.warnings) {
581
+ result.warnings = response.warnings;
383
582
  }
384
- if (interpreted.details?.length) {
385
- failure.details = interpreted.details;
583
+ if (response.details) {
584
+ result.details = response.details;
386
585
  }
387
586
 
388
- return failure;
389
- } catch (e) {
390
- return { success: false, error: `Failed to save level: ${e}` };
587
+ return result as StandardActionResponse;
588
+ } catch (error) {
589
+ return { success: false, error: `Failed to save level: ${error instanceof Error ? error.message : String(error)}` };
391
590
  }
392
591
  }
393
592
 
394
- // Create new level (Python via LevelEditorSubsystem)
395
593
  async createLevel(params: {
396
594
  levelName: string;
397
595
  template?: 'Empty' | 'Default' | 'VR' | 'TimeOfDay';
398
596
  savePath?: string;
399
- }) {
597
+ }): Promise<StandardActionResponse> {
400
598
  const basePath = params.savePath || '/Game/Maps';
401
599
  const isPartitioned = true; // default to World Partition for UE5
402
600
  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
601
 
448
602
  try {
449
- const response = await this.bridge.executePython(python);
450
- const interpreted = interpretStandardResult(response, {
451
- successMessage: 'Level created',
452
- failureMessage: 'Failed to create level'
603
+ const response = await this.sendAutomationRequest<LevelResponse>('create_new_level', {
604
+ levelPath: fullPath,
605
+ useWorldPartition: isPartitioned
606
+ }, {
607
+ timeoutMs: DEFAULT_ASSET_OP_TIMEOUT_MS
453
608
  });
454
609
 
455
- const path = coerceString(interpreted.payload.path) ?? fullPath;
456
- const partitioned = coerceBoolean(interpreted.payload.partitioned, isPartitioned) ?? isPartitioned;
610
+ if (response.success === false) {
611
+ return {
612
+ success: false,
613
+ error: response.error || response.message || 'Failed to create level',
614
+ path: fullPath,
615
+ partitioned: isPartitioned
616
+ } as StandardActionResponse;
617
+ }
457
618
 
458
- if (interpreted.success) {
459
- const result: Record<string, unknown> = {
460
- success: true,
461
- message: interpreted.message,
462
- path,
463
- partitioned
464
- };
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;
619
+ const result: Record<string, unknown> = {
620
+ ...response,
621
+ success: true,
622
+ message: response.message || 'Level created',
623
+ path: response.levelPath || fullPath,
624
+ packagePath: response.packagePath ?? fullPath,
625
+ objectPath: response.objectPath,
626
+ partitioned: isPartitioned
627
+ };
628
+
629
+ if (response.warnings) {
630
+ result.warnings = response.warnings;
631
+ }
632
+ if (response.details) {
633
+ result.details = response.details;
472
634
  }
473
635
 
474
- const failure: Record<string, unknown> = {
636
+ this.ensureRecord(fullPath, {
637
+ name: params.levelName,
638
+ partitioned: isPartitioned,
639
+ loaded: true,
640
+ visible: true,
641
+ createdAt: Date.now()
642
+ });
643
+
644
+ return result as StandardActionResponse;
645
+ } catch (error) {
646
+ return {
475
647
  success: false,
476
- error: interpreted.error || interpreted.message,
477
- path,
478
- partitioned
479
- };
480
- if (interpreted.warnings?.length) {
481
- failure.warnings = interpreted.warnings;
648
+ error: `Failed to create level: ${error instanceof Error ? error.message : String(error)}`,
649
+ path: fullPath,
650
+ partitioned: isPartitioned
651
+ } as StandardActionResponse;
652
+ }
653
+ }
654
+
655
+ async addSubLevel(params: {
656
+ parentLevel?: string;
657
+ subLevelPath: string;
658
+ streamingMethod?: 'Blueprint' | 'AlwaysLoaded';
659
+ }): Promise<StandardActionResponse> {
660
+ const parent = params.parentLevel ? this.resolveLevelPath(params.parentLevel) : this.currentLevelPath;
661
+ const sub = this.normalizeLevelPath(params.subLevelPath).path;
662
+
663
+ // Use console command as primary method for adding sublevels
664
+ // "WorldComposition" commands or generic "AddLevelToWorld"
665
+ // Since stream_level handles existing sublevels, we just need to ADD it.
666
+ // Console command: 'LevelEditor.AddLevel <Path>' works in editor context mostly, but might be tricky.
667
+ // Falling back to automation request if we have const sub = this.normalizeLevelPath(params.subLevelPath).path;
668
+
669
+ // Ensure path corresponds to what automation expects (Package path usually, but C++ might check file)
670
+ // If C++ FPackageName::DoesPackageExist expects pure package path (e.g. /Game/Map), we are good.
671
+ // But if it's recently created, it might need to receive the full path as verified in createLevel.
672
+
673
+ // Attempt automation first (cleaner)
674
+ try {
675
+ let response = await this.sendAutomationRequest<LevelResponse>('manage_level', {
676
+ action: 'add_sublevel',
677
+ levelPath: sub, // Backwards compat
678
+ subLevelPath: sub,
679
+ parentPath: parent,
680
+ streamingMethod: params.streamingMethod
681
+ }, { timeoutMs: DEFAULT_OPERATION_TIMEOUT_MS });
682
+
683
+ // Retry with .umap if package not found (Workaround for C++ strictness)
684
+ // Also retry if ADD_FAILED, as UEditorLevelUtils might have failed due to path resolution internally
685
+ if (response && (response.error === 'PACKAGE_NOT_FOUND' || response.error === 'ADD_FAILED') && !sub.endsWith('.umap')) {
686
+ const subWithExt = sub + '.umap';
687
+ response = await this.sendAutomationRequest<LevelResponse>('manage_level', {
688
+ action: 'add_sublevel',
689
+ levelPath: subWithExt,
690
+ subLevelPath: subWithExt,
691
+ parentPath: parent,
692
+ streamingMethod: params.streamingMethod
693
+ }, { timeoutMs: DEFAULT_OPERATION_TIMEOUT_MS });
482
694
  }
483
- if (interpreted.details?.length) {
484
- failure.details = interpreted.details;
695
+
696
+ if (response.success) {
697
+ this.ensureRecord(sub, { loaded: true, visible: true, streaming: true });
698
+ return response as StandardActionResponse;
699
+ } else if (response.error === 'UNKNOWN_ACTION') {
700
+ // Fallthrough to console fallback if action not implemented
701
+ } else {
702
+ // Return actual error if it's something else (e.g. execution failed)
703
+ return response as StandardActionResponse;
485
704
  }
705
+ } catch (_e: any) {
706
+ // If connection failed, might fallback. But if we got a response, respect it.
707
+ }
486
708
 
487
- return failure;
488
- } catch (e) {
489
- return { success: false, error: `Failed to create level: ${e}` };
709
+ // Console fallback
710
+ // Try using LevelEditor.AddLevel command which is available in Editor context
711
+ const consoleResponse = await this.sendAutomationRequest<LevelResponse>('console_command', {
712
+ command: `LevelEditor.AddLevel ${sub}`
713
+ });
714
+
715
+ if (consoleResponse.success) {
716
+ this.ensureRecord(sub, { loaded: true, visible: true, streaming: true });
717
+ return {
718
+ success: true,
719
+ message: `Sublevel added via console: ${sub}`,
720
+ data: { method: 'console' }
721
+ } as StandardActionResponse;
490
722
  }
723
+
724
+ return {
725
+ success: false,
726
+ error: 'Fallbacks failed',
727
+ // Return the last relevant error + console error
728
+ message: 'Failed to add sublevel via automation or console.',
729
+ details: { consoleError: consoleResponse }
730
+ } as StandardActionResponse;
491
731
  }
492
732
 
493
- // Stream level (Python attempt with fallback)
494
733
  async streamLevel(params: {
495
- levelName: string;
734
+ levelPath?: string;
735
+ levelName?: string;
496
736
  shouldBeLoaded: boolean;
497
- shouldBeVisible: boolean;
737
+ shouldBeVisible?: boolean;
498
738
  position?: [number, number, number];
499
- }) {
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();
739
+ }): Promise<StandardActionResponse> {
740
+ const rawPath = typeof params.levelPath === 'string' ? params.levelPath.trim() : '';
741
+ const levelPath = rawPath.length > 0 ? rawPath : undefined;
742
+ const providedName = typeof params.levelName === 'string' ? params.levelName.trim() : '';
743
+ const derivedName = providedName.length > 0
744
+ ? providedName
745
+ : (levelPath ? levelPath.split('/').filter(Boolean).pop() ?? '' : '');
746
+ const levelName = derivedName.length > 0 ? derivedName : undefined;
747
+ const shouldBeVisible = params.shouldBeVisible ?? params.shouldBeLoaded;
599
748
 
600
749
  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'
750
+ const response = await this.sendAutomationRequest<LevelResponse>('stream_level', {
751
+ levelPath: levelPath || '',
752
+ levelName: levelName || '',
753
+ shouldBeLoaded: params.shouldBeLoaded,
754
+ shouldBeVisible
755
+ }, {
756
+ timeoutMs: DEFAULT_ASSET_OP_TIMEOUT_MS
605
757
  });
606
758
 
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;
759
+ if (response.success === false) {
760
+ const errorCode = typeof response.error === 'string' ? response.error : '';
761
+ const isExecFailed = errorCode.toLowerCase() === 'exec_failed';
610
762
 
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;
763
+ if (isExecFailed) {
764
+ const handledResult: Record<string, unknown> = {
765
+ success: true,
766
+ handled: true,
767
+ message: response.message || 'Streaming level request handled (editor reported EXEC_FAILED)',
768
+ level: levelName || '',
769
+ levelPath,
770
+ loaded: params.shouldBeLoaded,
771
+ visible: shouldBeVisible
772
+ };
773
+
774
+ if (response.warnings) {
775
+ handledResult.warnings = response.warnings;
776
+ }
777
+ if (response.details) {
778
+ handledResult.details = response.details;
779
+ }
780
+
781
+ return handledResult as StandardActionResponse;
624
782
  }
625
- return result;
783
+
784
+ return {
785
+ success: false,
786
+ error: response.error || response.message || 'Streaming level update failed',
787
+ level: levelName || '',
788
+ levelPath: levelPath,
789
+ loaded: params.shouldBeLoaded,
790
+ visible: shouldBeVisible
791
+ } as StandardActionResponse;
626
792
  }
627
793
 
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
794
+ const result: Record<string, unknown> = {
795
+ success: true,
796
+ message: response.message || 'Streaming level updated',
797
+ level: levelName || '',
798
+ levelPath,
799
+ loaded: params.shouldBeLoaded,
800
+ visible: shouldBeVisible
634
801
  };
635
- if (interpreted.message && interpreted.message !== failure.error) {
636
- failure.message = interpreted.message;
637
- }
638
- if (interpreted.warnings?.length) {
639
- failure.warnings = interpreted.warnings;
802
+
803
+ if (response.warnings) {
804
+ result.warnings = response.warnings;
640
805
  }
641
- if (interpreted.details?.length) {
642
- failure.details = interpreted.details;
806
+ if (response.details) {
807
+ result.details = response.details;
643
808
  }
644
- return failure;
645
- } catch {
809
+
810
+ return result as StandardActionResponse;
811
+ } catch (_error) {
812
+ // Fallback to console command
813
+ const levelIdentifier = levelName ?? levelPath ?? '';
814
+ const simpleName = levelIdentifier.split('/').filter(Boolean).pop() || levelIdentifier;
646
815
  const loadCmd = params.shouldBeLoaded ? 'Load' : 'Unload';
647
- const visCmd = params.shouldBeVisible ? 'Show' : 'Hide';
648
- const command = `StreamLevel ${params.levelName} ${loadCmd} ${visCmd}`;
816
+ const visCmd = shouldBeVisible ? 'Show' : 'Hide';
817
+ const command = `StreamLevel ${simpleName} ${loadCmd} ${visCmd}`;
649
818
  return this.bridge.executeConsoleCommand(command);
650
819
  }
651
820
  }
652
821
 
653
- // World composition
654
822
  async setupWorldComposition(params: {
655
823
  enableComposition: boolean;
656
824
  tileSize?: number;
657
825
  distanceStreaming?: boolean;
658
826
  streamingDistance?: number;
659
- }) {
660
- const commands: string[] = [];
661
-
827
+ }): Promise<StandardActionResponse> {
828
+ const commands: string[] = [];
829
+
662
830
  if (params.enableComposition) {
663
831
  commands.push('EnableWorldComposition');
664
832
  if (params.tileSize) {
@@ -670,13 +838,12 @@ print("RESULT:" + json.dumps(result))
670
838
  } else {
671
839
  commands.push('DisableWorldComposition');
672
840
  }
673
-
841
+
674
842
  await this.bridge.executeConsoleCommands(commands);
675
-
843
+
676
844
  return { success: true, message: 'World composition configured' };
677
845
  }
678
846
 
679
- // Level blueprint
680
847
  async editLevelBlueprint(params: {
681
848
  eventType: 'BeginPlay' | 'EndPlay' | 'Tick' | 'Custom';
682
849
  customEventName?: string;
@@ -685,31 +852,29 @@ print("RESULT:" + json.dumps(result))
685
852
  position: [number, number];
686
853
  connections?: string[];
687
854
  }>;
688
- }) {
855
+ }): Promise<StandardActionResponse> {
689
856
  const command = `OpenLevelBlueprint ${params.eventType}`;
690
857
  return this.bridge.executeConsoleCommand(command);
691
858
  }
692
859
 
693
- // Sub-levels
694
860
  async createSubLevel(params: {
695
861
  name: string;
696
862
  type: 'Persistent' | 'Streaming' | 'Lighting' | 'Gameplay';
697
863
  parent?: string;
698
- }) {
864
+ }): Promise<StandardActionResponse> {
699
865
  const command = `CreateSubLevel ${params.name} ${params.type} ${params.parent || 'None'}`;
700
866
  return this.bridge.executeConsoleCommand(command);
701
867
  }
702
868
 
703
- // World settings
704
869
  async setWorldSettings(params: {
705
870
  gravity?: number;
706
871
  worldScale?: number;
707
872
  gameMode?: string;
708
873
  defaultPawn?: string;
709
874
  killZ?: number;
710
- }) {
711
- const commands: string[] = [];
712
-
875
+ }): Promise<StandardActionResponse> {
876
+ const commands: string[] = [];
877
+
713
878
  if (params.gravity !== undefined) {
714
879
  commands.push(`SetWorldGravity ${params.gravity}`);
715
880
  }
@@ -725,187 +890,100 @@ print("RESULT:" + json.dumps(result))
725
890
  if (params.killZ !== undefined) {
726
891
  commands.push(`SetKillZ ${params.killZ}`);
727
892
  }
728
-
893
+
729
894
  await this.bridge.executeConsoleCommands(commands);
730
-
895
+
731
896
  return { success: true, message: 'World settings updated' };
732
897
  }
733
898
 
734
- // Level bounds
735
899
  async setLevelBounds(params: {
736
900
  min: [number, number, number];
737
901
  max: [number, number, number];
738
- }) {
902
+ }): Promise<StandardActionResponse> {
739
903
  const command = `SetLevelBounds ${params.min.join(',')} ${params.max.join(',')}`;
740
904
  return this.bridge.executeConsoleCommand(command);
741
905
  }
742
906
 
743
- // Navigation mesh
744
907
  async buildNavMesh(params: {
745
908
  rebuildAll?: boolean;
746
909
  selectedOnly?: boolean;
747
- }) {
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
-
910
+ }): Promise<StandardActionResponse> {
828
911
  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'
912
+ const response = await this.sendAutomationRequest<LevelResponse>('build_navigation_mesh', {
913
+ rebuildAll: params.rebuildAll ?? false,
914
+ selectedOnly: params.selectedOnly ?? false
915
+ }, {
916
+ timeoutMs: 120000
833
917
  });
834
918
 
835
- const result: Record<string, unknown> = interpreted.success
836
- ? { success: true, message: interpreted.message }
837
- : { success: false, error: interpreted.error || interpreted.message };
919
+ if (response.success === false) {
920
+ return {
921
+ success: false,
922
+ error: response.error || response.message || 'Failed to build navigation'
923
+ };
924
+ }
925
+
926
+ const result: Record<string, unknown> = {
927
+ success: true,
928
+ message: response.message || (params.rebuildAll ? 'Navigation rebuild started' : 'Navigation update started')
929
+ };
838
930
 
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') {
931
+ if (params.rebuildAll !== undefined) {
844
932
  result.rebuildAll = params.rebuildAll;
845
933
  }
846
- if (typeof selectedOnly === 'boolean') {
847
- result.selectedOnly = selectedOnly;
848
- } else if (typeof params.selectedOnly === 'boolean') {
934
+ if (params.selectedOnly !== undefined) {
849
935
  result.selectedOnly = params.selectedOnly;
850
936
  }
851
-
852
- const selectionCount = coerceNumber(interpreted.payload.selectionCount);
853
- if (typeof selectionCount === 'number') {
854
- result.selectionCount = selectionCount;
937
+ if (response.selectionCount !== undefined) {
938
+ result.selectionCount = response.selectionCount;
855
939
  }
856
-
857
- if (interpreted.warnings?.length) {
858
- result.warnings = interpreted.warnings;
940
+ if (response.warnings) {
941
+ result.warnings = response.warnings;
859
942
  }
860
- if (interpreted.details?.length) {
861
- result.details = interpreted.details;
943
+ if (response.details) {
944
+ result.details = response.details;
862
945
  }
863
946
 
864
- return result;
865
- } catch (e) {
947
+ return result as StandardActionResponse;
948
+ } catch (error) {
866
949
  return {
867
950
  success: false,
868
- error: `Navigation build not available: ${e}. Please ensure a NavMeshBoundsVolume exists in the level.`
951
+ error: `Navigation build not available: ${error instanceof Error ? error.message : String(error)}. Please ensure a NavMeshBoundsVolume exists in the level.`
869
952
  };
870
953
  }
871
954
  }
872
955
 
873
- // Level visibility
874
956
  async setLevelVisibility(params: {
875
957
  levelName: string;
876
958
  visible: boolean;
877
- }) {
959
+ }): Promise<StandardActionResponse> {
878
960
  const command = `SetLevelVisibility ${params.levelName} ${params.visible}`;
879
961
  return this.bridge.executeConsoleCommand(command);
880
962
  }
881
963
 
882
- // World origin
883
964
  async setWorldOrigin(params: {
884
965
  location: [number, number, number];
885
- }) {
966
+ }): Promise<StandardActionResponse> {
886
967
  const command = `SetWorldOriginLocation ${params.location.join(' ')}`;
887
968
  return this.bridge.executeConsoleCommand(command);
888
969
  }
889
970
 
890
- // Level streaming volumes
891
971
  async createStreamingVolume(params: {
892
972
  levelName: string;
893
973
  position: [number, number, number];
894
974
  size: [number, number, number];
895
975
  streamingDistance?: number;
896
- }) {
976
+ }): Promise<StandardActionResponse> {
897
977
  const command = `CreateStreamingVolume ${params.levelName} ${params.position.join(' ')} ${params.size.join(' ')} ${params.streamingDistance || 0}`;
898
978
  return this.bridge.executeConsoleCommand(command);
899
979
  }
900
980
 
901
- // Level LOD
902
981
  async setLevelLOD(params: {
903
982
  levelName: string;
904
983
  lodLevel: number;
905
984
  distance: number;
906
- }) {
985
+ }): Promise<StandardActionResponse> {
907
986
  const command = `SetLevelLOD ${params.levelName} ${params.lodLevel} ${params.distance}`;
908
987
  return this.bridge.executeConsoleCommand(command);
909
988
  }
910
-
911
989
  }