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,36 +1,16 @@
1
- // Lighting tools for Unreal Engine
1
+ // Lighting tools for Unreal Engine using Automation Bridge
2
2
  import { UnrealBridge } from '../unreal-bridge.js';
3
- import { parseStandardResult } from '../utils/python-output.js';
4
- import { escapePythonString } from '../utils/python.js';
3
+ import { AutomationBridge } from '../automation/index.js';
4
+ import { ensureVector3 } from '../utils/validation.js';
5
+ import { wasmIntegration } from '../wasm/index.js';
5
6
 
6
7
  export class LightingTools {
7
- constructor(private bridge: UnrealBridge) {}
8
+ constructor(private bridge: UnrealBridge, private automationBridge?: AutomationBridge) { }
8
9
 
9
- private ensurePythonSpawnSucceeded(label: string, result: any) {
10
- let logs = '';
11
- if (Array.isArray(result?.LogOutput)) {
12
- logs = result.LogOutput.map((l: any) => String(l.Output || '')).join('');
13
- } else if (typeof result === 'string') {
14
- logs = result;
15
- }
16
-
17
- // If Python reported a traceback or explicit failure, propagate as error
18
- if (/Traceback|Error:|Failed to spawn/i.test(logs)) {
19
- throw new Error(`Unreal reported error spawning '${label}': ${logs}`);
20
- }
21
-
22
- // If script executed (ReturnValue true) and no error patterns, treat as success
23
- const executed = result?.ReturnValue === true || result?.ReturnValue === 'true';
24
- if (executed) return;
10
+ setAutomationBridge(automationBridge?: AutomationBridge) { this.automationBridge = automationBridge; }
25
11
 
26
- // Fallback: if no ReturnValue but success-like logs exist, accept
27
- if (/spawned/i.test(logs)) return;
28
12
 
29
- // Otherwise, uncertain
30
- throw new Error(`Uncertain spawn result for '${label}'. Engine logs:\n${logs}`);
31
- }
32
-
33
- private normalizeName(value: unknown, fallback?: string): string {
13
+ private normalizeName(value: unknown, defaultName?: string): string {
34
14
  if (typeof value === 'string') {
35
15
  const trimmed = value.trim();
36
16
  if (trimmed.length > 0) {
@@ -38,14 +18,75 @@ export class LightingTools {
38
18
  }
39
19
  }
40
20
 
41
- if (typeof fallback === 'string') {
42
- const trimmedFallback = fallback.trim();
43
- if (trimmedFallback.length > 0) {
44
- return trimmedFallback;
21
+ if (typeof defaultName === 'string') {
22
+ const trimmedDefault = defaultName.trim();
23
+ if (trimmedDefault.length > 0) {
24
+ return trimmedDefault;
45
25
  }
46
26
  }
47
27
 
48
- throw new Error('Invalid name: must be a non-empty string');
28
+ // Auto-generate if no name is provided
29
+ return `Light_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
30
+ }
31
+
32
+ /**
33
+ * Spawn a light actor using the Automation Bridge.
34
+ * @param lightClass The Unreal light class name (e.g. 'DirectionalLight', 'PointLight')
35
+ * @param params Light spawn parameters
36
+ */
37
+ private async spawnLightViaAutomation(
38
+ lightClass: string,
39
+ params: {
40
+ name: string;
41
+ location?: [number, number, number];
42
+ rotation?: [number, number, number] | { pitch: number, yaw: number, roll: number };
43
+ properties?: Record<string, any>;
44
+ }
45
+ ) {
46
+ if (!this.automationBridge) {
47
+ throw new Error('Automation Bridge not available. Cannot spawn lights without plugin support.');
48
+ }
49
+
50
+ try {
51
+ const payload: Record<string, any> = {
52
+ lightClass,
53
+ name: params.name,
54
+ };
55
+
56
+ if (params.location) {
57
+ // Use WASM vectorAdd for light location processing
58
+ const zeroVector: [number, number, number] = [0, 0, 0];
59
+ const processedLocation = wasmIntegration.vectorAdd(zeroVector, params.location);
60
+ console.error('[WASM] Using vectorAdd for light positioning');
61
+ payload.location = { x: processedLocation[0], y: processedLocation[1], z: processedLocation[2] };
62
+ }
63
+
64
+ if (params.rotation) {
65
+ if (Array.isArray(params.rotation)) {
66
+ payload.rotation = { pitch: params.rotation[0], yaw: params.rotation[1], roll: params.rotation[2] };
67
+ } else {
68
+ payload.rotation = params.rotation;
69
+ }
70
+ }
71
+
72
+ if (params.properties) {
73
+ payload.properties = params.properties;
74
+ }
75
+
76
+ const response = await this.automationBridge.sendAutomationRequest('spawn_light', payload, {
77
+ timeoutMs: 60000
78
+ });
79
+
80
+ if (response.success === false) {
81
+ throw new Error(response.error || response.message || 'Failed to spawn light');
82
+ }
83
+
84
+ return response;
85
+ } catch (error) {
86
+ throw new Error(
87
+ `Failed to spawn ${lightClass}: ${error instanceof Error ? error.message : String(error)}`
88
+ );
89
+ }
49
90
  }
50
91
 
51
92
  // Create directional light
@@ -53,13 +94,17 @@ export class LightingTools {
53
94
  name: string;
54
95
  intensity?: number;
55
96
  color?: [number, number, number];
56
- rotation?: [number, number, number];
97
+ rotation?: [number, number, number] | { pitch: number, yaw: number, roll: number };
57
98
  castShadows?: boolean;
58
99
  temperature?: number;
100
+ useAsAtmosphereSunLight?: boolean;
101
+ properties?: Record<string, any>;
59
102
  }) {
60
103
  const name = this.normalizeName(params.name);
61
- const escapedName = escapePythonString(name);
62
-
104
+ if (!this.automationBridge) {
105
+ throw new Error('Automation Bridge required for light spawning');
106
+ }
107
+
63
108
  // Validate numeric parameters
64
109
  if (params.intensity !== undefined) {
65
110
  if (typeof params.intensity !== 'number' || !isFinite(params.intensity)) {
@@ -69,13 +114,13 @@ export class LightingTools {
69
114
  throw new Error('Invalid intensity: must be non-negative');
70
115
  }
71
116
  }
72
-
117
+
73
118
  if (params.temperature !== undefined) {
74
119
  if (typeof params.temperature !== 'number' || !isFinite(params.temperature)) {
75
120
  throw new Error(`Invalid temperature value: ${params.temperature}`);
76
121
  }
77
122
  }
78
-
123
+
79
124
  // Validate arrays
80
125
  if (params.color !== undefined) {
81
126
  if (!Array.isArray(params.color) || params.color.length !== 3) {
@@ -87,77 +132,53 @@ export class LightingTools {
87
132
  }
88
133
  }
89
134
  }
90
-
135
+
91
136
  if (params.rotation !== undefined) {
92
- if (!Array.isArray(params.rotation) || params.rotation.length !== 3) {
93
- throw new Error('Invalid rotation: must be an array [pitch,yaw,roll]');
94
- }
95
- for (const r of params.rotation) {
96
- if (typeof r !== 'number' || !isFinite(r)) {
97
- throw new Error('Invalid rotation component: must be finite numbers');
137
+ if (Array.isArray(params.rotation)) {
138
+ if (params.rotation.length !== 3) {
139
+ throw new Error('Invalid rotation: must be an array [pitch,yaw,roll]');
140
+ }
141
+ for (const r of params.rotation) {
142
+ if (typeof r !== 'number' || !isFinite(r)) {
143
+ throw new Error('Invalid rotation component: must be finite numbers');
144
+ }
98
145
  }
99
146
  }
100
147
  }
101
-
148
+
102
149
  const rot = params.rotation || [0, 0, 0];
103
-
104
- // Build property setters
105
- const propSetters: string[] = [];
150
+
151
+ // Build properties for the light
152
+ const properties: Record<string, any> = params.properties || {};
106
153
  if (params.intensity !== undefined) {
107
- propSetters.push(` light_component.set_intensity(${params.intensity})`);
154
+ properties.intensity = params.intensity;
108
155
  }
109
156
  if (params.color) {
110
- propSetters.push(` light_component.set_light_color(unreal.LinearColor(${params.color[0]}, ${params.color[1]}, ${params.color[2]}, 1.0))`);
157
+ properties.color = { r: params.color[0], g: params.color[1], b: params.color[2], a: 1.0 };
111
158
  }
112
159
  if (params.castShadows !== undefined) {
113
- propSetters.push(` light_component.set_cast_shadows(${params.castShadows ? 'True' : 'False'})`);
160
+ properties.castShadows = params.castShadows;
114
161
  }
115
162
  if (params.temperature !== undefined) {
116
- propSetters.push(` light_component.set_temperature(${params.temperature})`);
117
- }
118
-
119
- const propertiesCode = propSetters.length > 0
120
- ? propSetters.join('\n')
121
- : ' pass # No additional properties';
122
-
123
- const pythonScript = `
124
- import unreal
125
-
126
- # Get editor subsystem
127
- editor_actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
128
-
129
- # Spawn the directional light
130
- directional_light_class = unreal.DirectionalLight
131
- spawn_location = unreal.Vector(0, 0, 500)
132
- spawn_rotation = unreal.Rotator(${rot[0]}, ${rot[1]}, ${rot[2]})
133
-
134
- # Spawn the actor
135
- spawned_light = editor_actor_subsystem.spawn_actor_from_class(
136
- directional_light_class,
137
- spawn_location,
138
- spawn_rotation
139
- )
140
-
141
- if spawned_light:
142
- # Set the label/name
143
- spawned_light.set_actor_label("${escapedName}")
144
-
145
- # Get the light component
146
- light_component = spawned_light.get_component_by_class(unreal.DirectionalLightComponent)
147
-
148
- if light_component:
149
- ${propertiesCode}
150
-
151
- print("Directional light '${escapedName}' spawned")
152
- else:
153
- print("Failed to spawn directional light '${escapedName}'")
154
- `;
155
-
156
- // Execute the Python script via bridge (UE 5.6-compatible)
157
- const result = await this.bridge.executePython(pythonScript);
158
-
159
- this.ensurePythonSpawnSucceeded(name, result);
160
- return { success: true, message: `Directional light '${name}' spawned` };
163
+ properties.temperature = params.temperature;
164
+ }
165
+ if (params.useAsAtmosphereSunLight !== undefined) {
166
+ properties.useAsAtmosphereSunLight = params.useAsAtmosphereSunLight;
167
+ }
168
+
169
+ try {
170
+ await this.spawnLightViaAutomation('DirectionalLight', {
171
+ name,
172
+ location: [0, 0, 500],
173
+ rotation: rot,
174
+ properties
175
+ });
176
+
177
+ return { success: true, message: `Directional light '${name}' spawned` };
178
+ } catch (e: any) {
179
+ // Don't mask errors as "not implemented" - report the actual error from the bridge
180
+ return { success: false, error: `Failed to create directional light: ${e?.message ?? e}` } as any;
181
+ }
161
182
  }
162
183
 
163
184
  // Create point light
@@ -169,25 +190,27 @@ else:
169
190
  color?: [number, number, number];
170
191
  falloffExponent?: number;
171
192
  castShadows?: boolean;
193
+ rotation?: [number, number, number] | { pitch: number, yaw: number, roll: number };
172
194
  }) {
173
195
  const name = this.normalizeName(params.name);
174
- const escapedName = escapePythonString(name);
175
-
196
+ if (!this.automationBridge) {
197
+ throw new Error('Automation Bridge required for light spawning');
198
+ }
199
+
200
+ // Validate location array
176
201
  // Validate location array
177
202
  if (params.location !== undefined) {
178
- if (!Array.isArray(params.location) || params.location.length !== 3) {
179
- throw new Error('Invalid location: must be an array [x,y,z]');
180
- }
181
- for (const l of params.location) {
182
- if (typeof l !== 'number' || !isFinite(l)) {
183
- throw new Error('Invalid location component: must be finite numbers');
184
- }
203
+ // Ensure location is valid array [x,y,z]
204
+ try {
205
+ params.location = ensureVector3(params.location, 'location');
206
+ } catch (e) {
207
+ throw new Error(`Invalid location: ${e instanceof Error ? e.message : String(e)}`);
185
208
  }
186
209
  }
187
-
210
+
188
211
  // Default location if not provided
189
212
  const location = params.location || [0, 0, 0];
190
-
213
+
191
214
  // Validate numeric parameters
192
215
  if (params.intensity !== undefined) {
193
216
  if (typeof params.intensity !== 'number' || !isFinite(params.intensity)) {
@@ -210,7 +233,7 @@ else:
210
233
  throw new Error(`Invalid falloffExponent value: ${params.falloffExponent}`);
211
234
  }
212
235
  }
213
-
236
+
214
237
  // Validate color array
215
238
  if (params.color !== undefined) {
216
239
  if (!Array.isArray(params.color) || params.color.length !== 3) {
@@ -222,74 +245,45 @@ else:
222
245
  }
223
246
  }
224
247
  }
225
-
226
- // Build property setters
227
- const propSetters: string[] = [];
248
+
249
+ // Build properties for the light
250
+ const properties: Record<string, any> = {};
228
251
  if (params.intensity !== undefined) {
229
- propSetters.push(` light_component.set_intensity(${params.intensity})`);
252
+ properties.intensity = params.intensity;
230
253
  }
231
254
  if (params.radius !== undefined) {
232
- propSetters.push(` light_component.set_attenuation_radius(${params.radius})`);
255
+ properties.attenuationRadius = params.radius;
233
256
  }
234
257
  if (params.color) {
235
- propSetters.push(` light_component.set_light_color(unreal.LinearColor(${params.color[0]}, ${params.color[1]}, ${params.color[2]}, 1.0))`);
258
+ properties.color = { r: params.color[0], g: params.color[1], b: params.color[2], a: 1.0 };
236
259
  }
237
260
  if (params.castShadows !== undefined) {
238
- propSetters.push(` light_component.set_cast_shadows(${params.castShadows ? 'True' : 'False'})`);
261
+ properties.castShadows = params.castShadows;
239
262
  }
240
263
  if (params.falloffExponent !== undefined) {
241
- propSetters.push(` light_component.set_light_falloff_exponent(${params.falloffExponent})`);
242
- }
243
-
244
- const propertiesCode = propSetters.length > 0
245
- ? propSetters.join('\n')
246
- : ' pass # No additional properties';
247
-
248
- const pythonScript = `
249
- import unreal
250
-
251
- # Get editor subsystem
252
- editor_actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
253
-
254
- # Spawn the point light
255
- point_light_class = unreal.PointLight
256
- spawn_location = unreal.Vector(${location[0]}, ${location[1]}, ${location[2]})
257
- spawn_rotation = unreal.Rotator(0, 0, 0)
258
-
259
- # Spawn the actor
260
- spawned_light = editor_actor_subsystem.spawn_actor_from_class(
261
- point_light_class,
262
- spawn_location,
263
- spawn_rotation
264
- )
265
-
266
- if spawned_light:
267
- # Set the label/name
268
- spawned_light.set_actor_label("${escapedName}")
269
-
270
- # Get the light component
271
- light_component = spawned_light.get_component_by_class(unreal.PointLightComponent)
272
-
273
- if light_component:
274
- ${propertiesCode}
275
-
276
- print(f"Point light '${escapedName}' spawned at {spawn_location.x}, {spawn_location.y}, {spawn_location.z}")
277
- else:
278
- print("Failed to spawn point light '${escapedName}'")
279
- `;
280
-
281
- // Execute the Python script via bridge (UE 5.6-compatible)
282
- const result = await this.bridge.executePython(pythonScript);
283
-
284
- this.ensurePythonSpawnSucceeded(name, result);
285
- return { success: true, message: `Point light '${name}' spawned at ${location.join(', ')}` };
264
+ properties.lightFalloffExponent = params.falloffExponent;
265
+ }
266
+
267
+ try {
268
+ await this.spawnLightViaAutomation('PointLight', {
269
+ name,
270
+ location,
271
+ rotation: params.rotation,
272
+ properties
273
+ });
274
+
275
+ return { success: true, message: `Point light '${name}' spawned at ${location.join(', ')}` };
276
+ } catch (e: any) {
277
+ // Don't mask errors as "not implemented" - report the actual error from the bridge
278
+ return { success: false, error: `Failed to create point light: ${e?.message ?? e}` } as any;
279
+ }
286
280
  }
287
281
 
288
282
  // Create spot light
289
283
  async createSpotLight(params: {
290
284
  name: string;
291
285
  location: [number, number, number];
292
- rotation: [number, number, number];
286
+ rotation: [number, number, number] | { pitch: number, yaw: number, roll: number };
293
287
  intensity?: number;
294
288
  innerCone?: number;
295
289
  outerCone?: number;
@@ -298,8 +292,10 @@ else:
298
292
  castShadows?: boolean;
299
293
  }) {
300
294
  const name = this.normalizeName(params.name);
301
- const escapedName = escapePythonString(name);
302
-
295
+ if (!this.automationBridge) {
296
+ throw new Error('Automation Bridge required for light spawning');
297
+ }
298
+
303
299
  // Validate required location and rotation arrays
304
300
  if (!params.location || !Array.isArray(params.location) || params.location.length !== 3) {
305
301
  throw new Error('Invalid location: must be an array [x,y,z]');
@@ -309,16 +305,21 @@ else:
309
305
  throw new Error('Invalid location component: must be finite numbers');
310
306
  }
311
307
  }
312
-
313
- if (!params.rotation || !Array.isArray(params.rotation) || params.rotation.length !== 3) {
314
- throw new Error('Invalid rotation: must be an array [pitch,yaw,roll]');
308
+
309
+ if (!params.rotation) {
310
+ throw new Error('Rotation is required');
315
311
  }
316
- for (const r of params.rotation) {
317
- if (typeof r !== 'number' || !isFinite(r)) {
318
- throw new Error('Invalid rotation component: must be finite numbers');
312
+ if (Array.isArray(params.rotation)) {
313
+ if (params.rotation.length !== 3) {
314
+ throw new Error('Invalid rotation: must be an array [pitch,yaw,roll]');
315
+ }
316
+ for (const r of params.rotation) {
317
+ if (typeof r !== 'number' || !isFinite(r)) {
318
+ throw new Error('Invalid rotation component: must be finite numbers');
319
+ }
319
320
  }
320
321
  }
321
-
322
+
322
323
  // Validate optional numeric parameters
323
324
  if (params.intensity !== undefined) {
324
325
  if (typeof params.intensity !== 'number' || !isFinite(params.intensity)) {
@@ -328,7 +329,7 @@ else:
328
329
  throw new Error('Invalid intensity: must be non-negative');
329
330
  }
330
331
  }
331
-
332
+
332
333
  if (params.innerCone !== undefined) {
333
334
  if (typeof params.innerCone !== 'number' || !isFinite(params.innerCone)) {
334
335
  throw new Error(`Invalid innerCone value: ${params.innerCone}`);
@@ -337,7 +338,7 @@ else:
337
338
  throw new Error('Invalid innerCone: must be between 0 and 180 degrees');
338
339
  }
339
340
  }
340
-
341
+
341
342
  if (params.outerCone !== undefined) {
342
343
  if (typeof params.outerCone !== 'number' || !isFinite(params.outerCone)) {
343
344
  throw new Error(`Invalid outerCone value: ${params.outerCone}`);
@@ -346,7 +347,7 @@ else:
346
347
  throw new Error('Invalid outerCone: must be between 0 and 180 degrees');
347
348
  }
348
349
  }
349
-
350
+
350
351
  if (params.radius !== undefined) {
351
352
  if (typeof params.radius !== 'number' || !isFinite(params.radius)) {
352
353
  throw new Error(`Invalid radius value: ${params.radius}`);
@@ -355,7 +356,7 @@ else:
355
356
  throw new Error('Invalid radius: must be non-negative');
356
357
  }
357
358
  }
358
-
359
+
359
360
  // Validate color array
360
361
  if (params.color !== undefined) {
361
362
  if (!Array.isArray(params.color) || params.color.length !== 3) {
@@ -367,83 +368,58 @@ else:
367
368
  }
368
369
  }
369
370
  }
370
- // Build property setters
371
- const propSetters: string[] = [];
371
+ // Build properties for the light
372
+ const properties: Record<string, any> = {};
372
373
  if (params.intensity !== undefined) {
373
- propSetters.push(` light_component.set_intensity(${params.intensity})`);
374
+ properties.intensity = params.intensity;
374
375
  }
375
376
  if (params.innerCone !== undefined) {
376
- propSetters.push(` light_component.set_inner_cone_angle(${params.innerCone})`);
377
+ properties.innerConeAngle = params.innerCone;
377
378
  }
378
379
  if (params.outerCone !== undefined) {
379
- propSetters.push(` light_component.set_outer_cone_angle(${params.outerCone})`);
380
+ properties.outerConeAngle = params.outerCone;
380
381
  }
381
382
  if (params.radius !== undefined) {
382
- propSetters.push(` light_component.set_attenuation_radius(${params.radius})`);
383
+ properties.attenuationRadius = params.radius;
383
384
  }
384
385
  if (params.color) {
385
- propSetters.push(` light_component.set_light_color(unreal.LinearColor(${params.color[0]}, ${params.color[1]}, ${params.color[2]}, 1.0))`);
386
+ properties.color = { r: params.color[0], g: params.color[1], b: params.color[2], a: 1.0 };
386
387
  }
387
388
  if (params.castShadows !== undefined) {
388
- propSetters.push(` light_component.set_cast_shadows(${params.castShadows ? 'True' : 'False'})`);
389
- }
390
-
391
- const propertiesCode = propSetters.length > 0
392
- ? propSetters.join('\n')
393
- : ' pass # No additional properties';
394
-
395
- const pythonScript = `
396
- import unreal
397
-
398
- # Get editor subsystem
399
- editor_actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
400
-
401
- # Spawn the spot light
402
- spot_light_class = unreal.SpotLight
403
- spawn_location = unreal.Vector(${params.location[0]}, ${params.location[1]}, ${params.location[2]})
404
- spawn_rotation = unreal.Rotator(${params.rotation[0]}, ${params.rotation[1]}, ${params.rotation[2]})
405
-
406
- # Spawn the actor
407
- spawned_light = editor_actor_subsystem.spawn_actor_from_class(
408
- spot_light_class,
409
- spawn_location,
410
- spawn_rotation
411
- )
412
-
413
- if spawned_light:
414
- # Set the label/name
415
- spawned_light.set_actor_label("${escapedName}")
416
-
417
- # Get the light component
418
- light_component = spawned_light.get_component_by_class(unreal.SpotLightComponent)
419
-
420
- if light_component:
421
- ${propertiesCode}
422
-
423
- print(f"Spot light '${escapedName}' spawned at {spawn_location.x}, {spawn_location.y}, {spawn_location.z}")
424
- else:
425
- print("Failed to spawn spot light '${escapedName}'")
426
- `;
427
-
428
- // Execute the Python script via bridge (UE 5.6-compatible)
429
- const result = await this.bridge.executePython(pythonScript);
430
-
431
- this.ensurePythonSpawnSucceeded(name, result);
432
- return { success: true, message: `Spot light '${name}' spawned at ${params.location.join(', ')}` };
389
+ properties.castShadows = params.castShadows;
390
+ }
391
+
392
+ try {
393
+ await this.spawnLightViaAutomation('SpotLight', {
394
+ name,
395
+ location: params.location,
396
+ rotation: params.rotation,
397
+ properties
398
+ });
399
+
400
+ return { success: true, message: `Spot light '${name}' spawned at ${params.location.join(', ')}` };
401
+ } catch (e: any) {
402
+ // Don't mask errors as "not implemented" - report the actual error from the bridge
403
+ return { success: false, error: `Failed to create spot light: ${e?.message ?? e}` } as any;
404
+ }
433
405
  }
434
406
 
435
407
  // Create rect light
436
408
  async createRectLight(params: {
437
409
  name: string;
438
410
  location: [number, number, number];
439
- rotation: [number, number, number];
411
+ rotation: [number, number, number] | { pitch: number, yaw: number, roll: number };
440
412
  width?: number;
441
413
  height?: number;
442
414
  intensity?: number;
443
415
  color?: [number, number, number];
416
+ castShadows?: boolean;
444
417
  }) {
418
+
445
419
  const name = this.normalizeName(params.name);
446
- const escapedName = escapePythonString(name);
420
+ if (!this.automationBridge) {
421
+ throw new Error('Automation Bridge required for light spawning');
422
+ }
447
423
 
448
424
  // Validate required location and rotation arrays
449
425
  if (!params.location || !Array.isArray(params.location) || params.location.length !== 3) {
@@ -454,16 +430,21 @@ else:
454
430
  throw new Error('Invalid location component: must be finite numbers');
455
431
  }
456
432
  }
457
-
458
- if (!params.rotation || !Array.isArray(params.rotation) || params.rotation.length !== 3) {
459
- throw new Error('Invalid rotation: must be an array [pitch,yaw,roll]');
433
+
434
+ if (!params.rotation) {
435
+ throw new Error('Rotation is required');
460
436
  }
461
- for (const r of params.rotation) {
462
- if (typeof r !== 'number' || !isFinite(r)) {
463
- throw new Error('Invalid rotation component: must be finite numbers');
437
+ if (Array.isArray(params.rotation)) {
438
+ if (params.rotation.length !== 3) {
439
+ throw new Error('Invalid rotation: must be an array [pitch,yaw,roll]');
440
+ }
441
+ for (const r of params.rotation) {
442
+ if (typeof r !== 'number' || !isFinite(r)) {
443
+ throw new Error('Invalid rotation component: must be finite numbers');
444
+ }
464
445
  }
465
446
  }
466
-
447
+
467
448
  // Validate optional numeric parameters
468
449
  if (params.width !== undefined) {
469
450
  if (typeof params.width !== 'number' || !isFinite(params.width)) {
@@ -473,7 +454,7 @@ else:
473
454
  throw new Error('Invalid width: must be positive');
474
455
  }
475
456
  }
476
-
457
+
477
458
  if (params.height !== undefined) {
478
459
  if (typeof params.height !== 'number' || !isFinite(params.height)) {
479
460
  throw new Error(`Invalid height value: ${params.height}`);
@@ -482,7 +463,7 @@ else:
482
463
  throw new Error('Invalid height: must be positive');
483
464
  }
484
465
  }
485
-
466
+
486
467
  if (params.intensity !== undefined) {
487
468
  if (typeof params.intensity !== 'number' || !isFinite(params.intensity)) {
488
469
  throw new Error(`Invalid intensity value: ${params.intensity}`);
@@ -491,7 +472,7 @@ else:
491
472
  throw new Error('Invalid intensity: must be non-negative');
492
473
  }
493
474
  }
494
-
475
+
495
476
  // Validate color array
496
477
  if (params.color !== undefined) {
497
478
  if (!Array.isArray(params.color) || params.color.length !== 3) {
@@ -503,63 +484,77 @@ else:
503
484
  }
504
485
  }
505
486
  }
506
- // Build property setters
507
- const propSetters: string[] = [];
487
+ // Build properties for the light
488
+ const properties: Record<string, any> = {};
508
489
  if (params.intensity !== undefined) {
509
- propSetters.push(` light_component.set_intensity(${params.intensity})`);
490
+ properties.intensity = params.intensity;
510
491
  }
511
492
  if (params.color) {
512
- propSetters.push(` light_component.set_light_color(unreal.LinearColor(${params.color[0]}, ${params.color[1]}, ${params.color[2]}, 1.0))`);
493
+ properties.color = { r: params.color[0], g: params.color[1], b: params.color[2], a: 1.0 };
513
494
  }
514
495
  if (params.width !== undefined) {
515
- propSetters.push(` light_component.set_source_width(${params.width})`);
496
+ properties.sourceWidth = params.width;
516
497
  }
517
498
  if (params.height !== undefined) {
518
- propSetters.push(` light_component.set_source_height(${params.height})`);
519
- }
520
-
521
- const propertiesCode = propSetters.length > 0
522
- ? propSetters.join('\n')
523
- : ' pass # No additional properties';
524
-
525
- const pythonScript = `
526
- import unreal
527
-
528
- # Get editor subsystem
529
- editor_actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
530
-
531
- # Spawn the rect light
532
- rect_light_class = unreal.RectLight
533
- spawn_location = unreal.Vector(${params.location[0]}, ${params.location[1]}, ${params.location[2]})
534
- spawn_rotation = unreal.Rotator(${params.rotation[0]}, ${params.rotation[1]}, ${params.rotation[2]})
535
-
536
- # Spawn the actor
537
- spawned_light = editor_actor_subsystem.spawn_actor_from_class(
538
- rect_light_class,
539
- spawn_location,
540
- spawn_rotation
541
- )
542
-
543
- if spawned_light:
544
- # Set the label/name
545
- spawned_light.set_actor_label("${escapedName}")
546
-
547
- # Get the light component
548
- light_component = spawned_light.get_component_by_class(unreal.RectLightComponent)
549
-
550
- if light_component:
551
- ${propertiesCode}
552
-
553
- print(f"Rect light '${escapedName}' spawned at {spawn_location.x}, {spawn_location.y}, {spawn_location.z}")
554
- else:
555
- print("Failed to spawn rect light '${escapedName}'")
556
- `;
557
-
558
- // Execute the Python script via bridge (UE 5.6-compatible)
559
- const result = await this.bridge.executePython(pythonScript);
560
-
561
- this.ensurePythonSpawnSucceeded(name, result);
562
- return { success: true, message: `Rect light '${name}' spawned at ${params.location.join(', ')}` };
499
+ properties.sourceHeight = params.height;
500
+ }
501
+
502
+ try {
503
+ await this.spawnLightViaAutomation('RectLight', {
504
+ name,
505
+ location: params.location,
506
+ rotation: params.rotation,
507
+ properties
508
+ });
509
+
510
+ return { success: true, message: `Rect light '${name}' spawned at ${params.location.join(', ')}` };
511
+ } catch (e: any) {
512
+ // Don't mask errors as "not implemented" - report the actual error from the bridge
513
+ return { success: false, error: `Failed to create rect light: ${e?.message ?? e}` } as any;
514
+ }
515
+ }
516
+
517
+ /**
518
+ * Create dynamic light
519
+ */
520
+ async createDynamicLight(params: {
521
+ name?: string;
522
+ lightType?: 'Point' | 'Spot' | 'Directional' | 'Rect' | string;
523
+ location?: [number, number, number] | { x: number; y: number; z: number };
524
+ rotation?: [number, number, number];
525
+ intensity?: number;
526
+ color?: [number, number, number, number] | { r: number; g: number; b: number; a?: number };
527
+ pulse?: { enabled?: boolean; frequency?: number };
528
+ }) {
529
+ try {
530
+ const name = typeof params.name === 'string' && params.name.trim().length > 0 ? params.name.trim() : `DynamicLight_${Date.now() % 10000}`;
531
+ const lightTypeRaw = typeof params.lightType === 'string' && params.lightType.trim().length > 0 ? params.lightType.trim() : 'Point';
532
+ const location = Array.isArray(params.location) ? { x: params.location[0], y: params.location[1], z: params.location[2] } : (params.location || { x: 0, y: 0, z: 100 });
533
+
534
+ // C++ plugin does not strictly implement 'create_dynamic_light' action; it supports 'spawn_light'.
535
+ // However, we rely on the specific helper methods below which correctly map to 'spawn_light'
536
+ // with the appropriate class and properties.
537
+
538
+ const toArray3 = (loc: any): [number, number, number] => Array.isArray(loc)
539
+ ? [Number(loc[0]) || 0, Number(loc[1]) || 0, Number(loc[2]) || 0]
540
+ : [Number(loc?.x) || 0, Number(loc?.y) || 0, Number(loc?.z) || 0];
541
+ const locArr = toArray3(location);
542
+ const typeNorm = (lightTypeRaw || 'Point').toLowerCase();
543
+
544
+ switch (typeNorm) {
545
+ case 'directional': case 'directionallight':
546
+ return await this.createDirectionalLight({ name, intensity: params.intensity, color: Array.isArray(params.color) ? [params.color[0], params.color[1], params.color[2]] as any : (params.color ? [params.color.r, params.color.g, params.color.b] : undefined), rotation: params.rotation as any });
547
+ case 'spot': case 'spotlight':
548
+ return await this.createSpotLight({ name, location: locArr, rotation: params.rotation as any, intensity: params.intensity, innerCone: undefined, outerCone: undefined, color: Array.isArray(params.color) ? params.color as any : (params.color ? [params.color.r, params.color.g, params.color.b] : undefined) });
549
+ case 'rect': case 'rectlight':
550
+ return await this.createRectLight({ name, location: locArr, rotation: params.rotation as any, width: undefined, height: undefined, intensity: params.intensity, color: Array.isArray(params.color) ? params.color as any : (params.color ? [params.color.r, params.color.g, params.color.b] : undefined) });
551
+ case 'point': default:
552
+ return await this.createPointLight({ name, location: locArr, intensity: params.intensity, radius: undefined, color: Array.isArray(params.color) ? params.color as any : (params.color ? [params.color.r, params.color.g, params.color.b] : undefined), castShadows: undefined });
553
+ }
554
+
555
+ } catch (err) {
556
+ return { success: false, error: `Failed to create dynamic light: ${err}` };
557
+ }
563
558
  }
564
559
 
565
560
  // Create sky light
@@ -569,210 +564,107 @@ else:
569
564
  cubemapPath?: string;
570
565
  intensity?: number;
571
566
  recapture?: boolean;
567
+ location?: [number, number, number];
568
+ rotation?: [number, number, number] | { pitch: number, yaw: number, roll: number };
569
+ realTimeCapture?: boolean;
570
+ castShadows?: boolean;
571
+ color?: [number, number, number];
572
572
  }) {
573
573
  const name = this.normalizeName(params.name);
574
- const escapedName = escapePythonString(name);
575
- const sourceTypeRaw = typeof params.sourceType === 'string' ? params.sourceType.trim() : undefined;
576
- const normalizedSourceType = sourceTypeRaw
577
- ? sourceTypeRaw.toLowerCase() === 'specifiedcubemap'
578
- ? 'SpecifiedCubemap'
579
- : sourceTypeRaw.toLowerCase() === 'capturedscene'
580
- ? 'CapturedScene'
581
- : undefined
582
- : undefined;
583
- const cubemapPath = typeof params.cubemapPath === 'string' ? params.cubemapPath.trim() : undefined;
584
-
585
- if (normalizedSourceType === 'SpecifiedCubemap' && (!cubemapPath || cubemapPath.length === 0)) {
574
+ if (params.sourceType === 'SpecifiedCubemap' && (!params.cubemapPath || params.cubemapPath.trim().length === 0)) {
586
575
  const message = 'cubemapPath is required when sourceType is SpecifiedCubemap';
587
576
  return { success: false, error: message, message };
588
577
  }
589
- const escapedCubemapPath = cubemapPath ? escapePythonString(cubemapPath) : '';
590
- const python = `
591
- import unreal
592
- import json
593
578
 
594
- result = {
595
- "success": False,
596
- "message": "",
597
- "error": "",
598
- "warnings": []
599
- }
579
+ if (!this.automationBridge) {
580
+ throw new Error('Automation Bridge required for sky light creation');
581
+ }
582
+
583
+ try {
584
+ const properties: Record<string, any> = {};
585
+ if (params.intensity !== undefined) properties.Intensity = params.intensity;
586
+ if (params.castShadows !== undefined) properties.CastShadows = params.castShadows;
587
+ if (params.realTimeCapture !== undefined) properties.RealTimeCapture = params.realTimeCapture;
588
+ if (params.color) properties.LightColor = { r: params.color[0], g: params.color[1], b: params.color[2], a: 1.0 };
589
+
590
+ const payload: Record<string, any> = {
591
+ name,
592
+ sourceType: params.sourceType || 'CapturedScene',
593
+ location: params.location,
594
+ rotation: params.rotation,
595
+ properties
596
+ };
597
+
598
+ if (params.cubemapPath) {
599
+ payload.cubemapPath = params.cubemapPath;
600
+ }
601
+ if (params.intensity !== undefined) {
602
+ payload.intensity = params.intensity;
603
+ }
604
+ if (params.recapture) {
605
+ payload.recapture = params.recapture;
606
+ }
607
+
608
+ const response = await this.automationBridge.sendAutomationRequest('spawn_sky_light', payload, {
609
+ timeoutMs: 60000
610
+ });
611
+
612
+ if (response.success === false) {
613
+ return {
614
+ success: false,
615
+ error: response.error || response.message || 'Failed to create sky light'
616
+ };
617
+ }
600
618
 
601
- def add_warning(text):
602
- if text:
603
- result["warnings"].append(str(text))
604
-
605
- def finish():
606
- if result["success"]:
607
- if not result["message"]:
608
- result["message"] = "Sky light ensured"
609
- result.pop("error", None)
610
- else:
611
- if not result["error"]:
612
- result["error"] = result["message"] or "Failed to ensure sky light"
613
- if not result["message"]:
614
- result["message"] = result["error"]
615
- if not result["warnings"]:
616
- result.pop("warnings", None)
617
- print('RESULT:' + json.dumps(result))
618
-
619
- try:
620
- actor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
621
- if not actor_sub:
622
- result["error"] = "EditorActorSubsystem unavailable"
623
- finish()
624
- raise SystemExit(0)
625
-
626
- spawn_location = unreal.Vector(0.0, 0.0, 500.0)
627
- spawn_rotation = unreal.Rotator(0.0, 0.0, 0.0)
628
-
629
- actor = None
630
- try:
631
- for candidate in actor_sub.get_all_level_actors():
632
- try:
633
- if candidate.get_class().get_name() == 'SkyLight':
634
- actor = candidate
635
- break
636
- except Exception:
637
- continue
638
- except Exception:
639
- pass
640
-
641
- if actor is None:
642
- actor = actor_sub.spawn_actor_from_class(unreal.SkyLight, spawn_location, spawn_rotation)
643
-
644
- if not actor:
645
- result["error"] = "Failed to spawn SkyLight actor"
646
- finish()
647
- raise SystemExit(0)
648
-
649
- try:
650
- actor.set_actor_label("${escapedName}")
651
- except Exception:
652
- pass
653
-
654
- comp = actor.get_component_by_class(unreal.SkyLightComponent)
655
- if not comp:
656
- result["error"] = "SkyLight component missing"
657
- finish()
658
- raise SystemExit(0)
659
-
660
- ${params.intensity !== undefined ? `
661
- try:
662
- comp.set_intensity(${params.intensity})
663
- except Exception:
664
- try:
665
- comp.set_editor_property('intensity', ${params.intensity})
666
- except Exception:
667
- add_warning('Unable to set intensity property')
668
- ` : ''}
669
-
670
- source_type = ${normalizedSourceType ? `'${normalizedSourceType}'` : 'None'}
671
- if source_type:
672
- try:
673
- comp.set_editor_property('source_type', getattr(unreal.SkyLightSourceType, source_type))
674
- except Exception:
675
- try:
676
- comp.source_type = getattr(unreal.SkyLightSourceType, source_type)
677
- except Exception:
678
- add_warning(f"Unable to set source type {source_type}")
679
-
680
- if source_type == 'SpecifiedCubemap':
681
- path = "${escapedCubemapPath}"
682
- if not path:
683
- result["error"] = "cubemapPath is required when sourceType is SpecifiedCubemap"
684
- finish()
685
- raise SystemExit(0)
686
- try:
687
- exists = unreal.EditorAssetLibrary.does_asset_exist(path)
688
- except Exception:
689
- exists = False
690
- if not exists:
691
- result["error"] = f"Cubemap asset not found: {path}"
692
- finish()
693
- raise SystemExit(0)
694
- try:
695
- cube = unreal.EditorAssetLibrary.load_asset(path)
696
- except Exception as load_err:
697
- result["error"] = f"Failed to load cubemap asset: {load_err}"
698
- finish()
699
- raise SystemExit(0)
700
- if not cube:
701
- result["error"] = f"Cubemap asset could not be loaded: {path}"
702
- finish()
703
- raise SystemExit(0)
704
- try:
705
- if hasattr(comp, 'set_cubemap'):
706
- comp.set_cubemap(cube)
707
- else:
708
- comp.set_editor_property('cubemap', cube)
709
- except Exception as assign_err:
710
- result["error"] = f"Failed to assign cubemap: {assign_err}"
711
- finish()
712
- raise SystemExit(0)
713
-
714
- if ${params.recapture ? 'True' : 'False'}:
715
- try:
716
- comp.recapture_sky()
717
- except Exception as recapture_err:
718
- add_warning(f"Recapture failed: {recapture_err}")
719
-
720
- result["success"] = True
721
- result["message"] = "Sky light ensured"
722
- finish()
723
-
724
- except SystemExit:
725
- pass
726
- except Exception as run_err:
727
- result["error"] = str(run_err)
728
- finish()
729
- `.trim();
730
- const resp = await this.bridge.executePython(python);
731
- const parsed = parseStandardResult(resp).data;
732
- if (parsed) {
733
- if (parsed.success) {
734
619
  return {
735
620
  success: true,
736
- message: parsed.message ?? 'Sky light ensured',
737
- warnings: Array.isArray(parsed.warnings) && parsed.warnings.length > 0 ? parsed.warnings : undefined
621
+ message: response.message || 'Sky light created',
622
+ ...(response.result || {})
623
+ };
624
+ } catch (error) {
625
+ return {
626
+ success: false,
627
+ error: `Failed to create sky light: ${error instanceof Error ? error.message : String(error)}`
738
628
  };
739
629
  }
740
- return {
741
- success: false,
742
- error: parsed.error ?? parsed.message ?? 'Failed to ensure sky light',
743
- warnings: Array.isArray(parsed.warnings) && parsed.warnings.length > 0 ? parsed.warnings : undefined
744
- };
745
- }
746
- return { success: true, message: 'Sky light ensured' };
747
630
  }
748
631
 
749
632
  // Remove duplicate SkyLights and keep only one (named target label)
750
633
  async ensureSingleSkyLight(params?: { name?: string; recapture?: boolean }) {
751
- const fallbackName = 'MCP_Test_Sky';
752
- const name = this.normalizeName(params?.name, fallbackName);
753
- const escapedName = escapePythonString(name);
634
+ const defaultName = 'MCP_Test_Sky';
635
+ const name = this.normalizeName(params?.name, defaultName);
754
636
  const recapture = !!params?.recapture;
755
- const py = `\nimport unreal, json\nactor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)\nactors = actor_sub.get_all_level_actors() if actor_sub else []\nskies = []\nfor a in actors:\n try:\n if a.get_class().get_name() == 'SkyLight':\n skies.append(a)\n except Exception: pass\nkeep = None\n# Prefer one with matching label; otherwise keep the first\nfor a in skies:\n try:\n label = a.get_actor_label()\n if label == "${escapedName}":\n keep = a\n break\n except Exception: pass\nif keep is None and len(skies) > 0:\n keep = skies[0]\n# Rename the kept one if needed\nif keep is not None:\n try: keep.set_actor_label("${escapedName}")\n except Exception: pass\n# Destroy all others using the correct non-deprecated API\nremoved = 0\nfor a in skies:\n if keep is not None and a == keep:\n continue\n try:\n # Use EditorActorSubsystem.destroy_actor instead of deprecated EditorLevelLibrary\n actor_sub.destroy_actor(a)\n removed += 1\n except Exception: pass\n# Optionally recapture\nif keep is not None and ${recapture ? 'True' : 'False'}:\n try:\n comp = keep.get_component_by_class(unreal.SkyLightComponent)\n if comp: comp.recapture_sky()\n except Exception: pass\nprint('RESULT:' + json.dumps({'success': True, 'removed': removed, 'kept': True if keep else False}))\n`.trim();
756
-
757
- const resp = await this.bridge.executePython(py);
758
- let out = '';
759
- if (resp?.LogOutput && Array.isArray((resp as any).LogOutput)) {
760
- out = (resp as any).LogOutput.map((l: any) => l.Output || '').join('');
761
- } else if (typeof resp === 'string') {
762
- out = resp;
763
- } else {
764
- out = JSON.stringify(resp);
765
- }
766
- const m = out.match(/RESULT:({.*})/);
767
- if (m) {
768
- try {
769
- const parsed = JSON.parse(m[1]);
770
- if (parsed.success) {
771
- return { success: true, removed: parsed.removed, message: `Ensured single SkyLight (removed ${parsed.removed})` };
772
- }
773
- } catch {}
637
+
638
+ if (!this.automationBridge) {
639
+ throw new Error('Automation Bridge required for sky light management');
640
+ }
641
+
642
+ try {
643
+ const response = await this.automationBridge.sendAutomationRequest('ensure_single_sky_light', {
644
+ name,
645
+ recapture
646
+ }, {
647
+ timeoutMs: 60000
648
+ });
649
+
650
+ if (response.success === false) {
651
+ return {
652
+ success: false,
653
+ error: response.error || response.message || 'Failed to ensure single sky light'
654
+ };
655
+ }
656
+
657
+ return {
658
+ success: true,
659
+ message: response.message || `Ensured single SkyLight (removed ${(response.result as any)?.removed || 0})`,
660
+ ...(response.result || {})
661
+ };
662
+ } catch (error) {
663
+ return {
664
+ success: false,
665
+ error: `Failed to ensure single sky light: ${error instanceof Error ? error.message : String(error)}`
666
+ };
774
667
  }
775
- return { success: true, message: 'Ensured single SkyLight' };
776
668
  }
777
669
 
778
670
  // Setup global illumination
@@ -782,8 +674,22 @@ except Exception as run_err:
782
674
  indirectLightingIntensity?: number;
783
675
  bounces?: number;
784
676
  }) {
677
+ if (this.automationBridge) {
678
+ try {
679
+ const response = await this.automationBridge.sendAutomationRequest('setup_global_illumination', {
680
+ method: params.method,
681
+ quality: params.quality,
682
+ indirectLightingIntensity: params.indirectLightingIntensity,
683
+ bounces: params.bounces
684
+ });
685
+ if (response.success) return { success: true, message: 'Global illumination configured via bridge', ...(response.result || {}) };
686
+ } catch (_e) {
687
+ // Fallback to console commands
688
+ }
689
+ }
690
+
785
691
  const commands = [];
786
-
692
+
787
693
  switch (params.method) {
788
694
  case 'Lightmass':
789
695
  commands.push('r.DynamicGlobalIlluminationMethod 0');
@@ -798,25 +704,25 @@ except Exception as run_err:
798
704
  commands.push('r.DynamicGlobalIlluminationMethod 3');
799
705
  break;
800
706
  }
801
-
707
+
802
708
  if (params.quality) {
803
709
  const qualityMap = { 'Low': 0, 'Medium': 1, 'High': 2, 'Epic': 3 };
804
710
  commands.push(`r.Lumen.Quality ${qualityMap[params.quality]}`);
805
711
  }
806
-
712
+
807
713
  if (params.indirectLightingIntensity !== undefined) {
808
714
  commands.push(`r.IndirectLightingIntensity ${params.indirectLightingIntensity}`);
809
715
  }
810
-
716
+
811
717
  if (params.bounces !== undefined) {
812
718
  commands.push(`r.Lumen.MaxReflectionBounces ${params.bounces}`);
813
719
  }
814
-
720
+
815
721
  for (const cmd of commands) {
816
722
  await this.bridge.executeConsoleCommand(cmd);
817
723
  }
818
-
819
- return { success: true, message: 'Global illumination configured' };
724
+
725
+ return { success: true, message: 'Global illumination configured (console)' };
820
726
  }
821
727
 
822
728
  // Configure shadows
@@ -827,529 +733,174 @@ except Exception as run_err:
827
733
  contactShadows?: boolean;
828
734
  rayTracedShadows?: boolean;
829
735
  }) {
736
+ if (this.automationBridge) {
737
+ try {
738
+ const response = await this.automationBridge.sendAutomationRequest('configure_shadows', {
739
+ shadowQuality: params.shadowQuality,
740
+ cascadedShadows: params.cascadedShadows,
741
+ shadowDistance: params.shadowDistance,
742
+ contactShadows: params.contactShadows,
743
+ rayTracedShadows: params.rayTracedShadows,
744
+ virtualShadowMaps: params.rayTracedShadows // Map to VSM for C++ handler
745
+ });
746
+ if (response.success) return { success: true, message: 'Shadow settings configured via bridge', ...(response.result || {}) };
747
+ } catch (_e) {
748
+ // Fallback
749
+ }
750
+ }
751
+
830
752
  const commands = [];
831
-
753
+
832
754
  if (params.shadowQuality) {
833
755
  const qualityMap = { 'Low': 0, 'Medium': 1, 'High': 2, 'Epic': 3 };
834
756
  commands.push(`r.ShadowQuality ${qualityMap[params.shadowQuality]}`);
835
757
  }
836
-
758
+
837
759
  if (params.cascadedShadows !== undefined) {
838
760
  commands.push(`r.Shadow.CSM.MaxCascades ${params.cascadedShadows ? 4 : 1}`);
839
761
  }
840
-
762
+
841
763
  if (params.shadowDistance !== undefined) {
842
764
  commands.push(`r.Shadow.DistanceScale ${params.shadowDistance}`);
843
765
  }
844
-
766
+
845
767
  if (params.contactShadows !== undefined) {
846
768
  commands.push(`r.ContactShadows ${params.contactShadows ? 1 : 0}`);
847
769
  }
848
-
770
+
849
771
  if (params.rayTracedShadows !== undefined) {
850
772
  commands.push(`r.RayTracing.Shadows ${params.rayTracedShadows ? 1 : 0}`);
851
773
  }
852
-
774
+
853
775
  for (const cmd of commands) {
854
776
  await this.bridge.executeConsoleCommand(cmd);
855
777
  }
856
-
857
- return { success: true, message: 'Shadow settings configured' };
778
+
779
+ return { success: true, message: 'Shadow settings configured (console)' };
858
780
  }
859
781
 
860
- // Build lighting (Python-based)
782
+ // Build lighting
861
783
  async buildLighting(params: {
862
784
  quality?: 'Preview' | 'Medium' | 'High' | 'Production';
863
- buildOnlySelected?: boolean; // ignored in Python path
785
+ buildOnlySelected?: boolean;
864
786
  buildReflectionCaptures?: boolean;
787
+ levelPath?: string;
865
788
  }) {
866
- const q = params.quality || 'High';
867
- const qualityMap: Record<string, string> = {
868
- 'Preview': 'QUALITY_PREVIEW',
869
- 'Medium': 'QUALITY_MEDIUM',
870
- 'High': 'QUALITY_HIGH',
871
- 'Production': 'QUALITY_PRODUCTION'
872
- };
873
- const qualityEnum = qualityMap[q] || 'QUALITY_HIGH';
874
-
875
- // First try to ensure precomputed lighting is allowed and force-no-precomputed is disabled, then save changes
876
- const disablePrecomputedPy = `
877
- import unreal, json
878
-
879
- messages = []
880
-
881
- # Precheck: verify project supports static lighting (Support Static Lighting)
882
- try:
883
- rs = unreal.get_default_object(unreal.RendererSettings)
884
- support_static = False
885
- try:
886
- support_static = bool(rs.get_editor_property('bSupportStaticLighting'))
887
- except Exception:
888
- try:
889
- support_static = bool(rs.get_editor_property('support_static_lighting'))
890
- except Exception:
891
- support_static = False
892
- if not support_static:
893
- print('RESULT:' + json.dumps({
894
- 'success': False,
895
- 'status': 'staticDisabled',
896
- 'error': 'Project has Support Static Lighting disabled (r.AllowStaticLighting=0). Enable Project Settings -> Rendering -> Support Static Lighting and restart the editor.'
897
- }))
898
- raise SystemExit(0)
899
- else:
900
- messages.append('Support Static Lighting is enabled')
901
- except Exception as e:
902
- messages.append(f'Precheck failed: {e}')
903
-
904
- # Ensure runtime CVar does not force disable precomputed lighting
905
- try:
906
- unreal.SystemLibrary.execute_console_command(None, 'r.ForceNoPrecomputedLighting 0')
907
- messages.append('Set r.ForceNoPrecomputedLighting 0')
908
- except Exception as e:
909
- messages.append(f'r.ForceNoPrecomputedLighting failed: {e}')
910
-
911
- # Temporarily disable source control prompts to avoid checkout dialogs during automated saves
912
- try:
913
- prefs = unreal.SourceControlPreferences()
914
- try:
915
- prefs.set_enable_source_control(False)
916
- except Exception:
917
- try:
918
- prefs.enable_source_control = False
919
- except Exception:
920
- pass
921
- messages.append('Disabled Source Control for this session')
922
- except Exception as e:
923
- messages.append(f'SourceControlPreferences modify failed: {e}')
924
-
925
- try:
926
- ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
927
- les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
928
- world = ues.get_editor_world() if ues else None
929
-
930
- if world:
931
- world_settings = world.get_world_settings()
932
- if world_settings:
933
- # Mark for modification
934
- try:
935
- world_settings.modify()
936
- except Exception:
937
- pass
938
-
939
- # Try all known variants of the property name
940
- for prop in ['force_no_precomputed_lighting', 'bForceNoPrecomputedLighting']:
941
- try:
942
- world_settings.set_editor_property(prop, False)
943
- messages.append(f"Set WorldSettings.{prop}=False")
944
- except Exception as e:
945
- messages.append(f"Failed setting {prop}: {e}")
946
-
947
- # Also update the Class Default Object (CDO) to help persistence in some versions
948
- try:
949
- ws_class = world_settings.get_class()
950
- ws_cdo = unreal.get_default_object(ws_class)
951
- if ws_cdo:
952
- try:
953
- ws_cdo.set_editor_property('bForceNoPrecomputedLighting', False)
954
- messages.append('Set CDO bForceNoPrecomputedLighting=False')
955
- except Exception:
956
- pass
957
- try:
958
- ws_cdo.set_editor_property('force_no_precomputed_lighting', False)
959
- messages.append('Set CDO force_no_precomputed_lighting=False')
960
- except Exception:
961
- pass
962
- except Exception as e:
963
- messages.append(f'CDO update failed: {e}')
964
-
965
- # Apply and save level to persist change
966
- try:
967
- if hasattr(world_settings, 'post_edit_change'):
968
- world_settings.post_edit_change()
969
- except Exception:
970
- pass
971
-
972
- # Save current level/package
973
- try:
974
- wp = world.get_path_name()
975
- pkg_path = wp.split('.')[0] if '.' in wp else wp
976
- unreal.EditorAssetLibrary.save_asset(pkg_path)
977
- messages.append(f'Saved world asset: {pkg_path}')
978
- except Exception as e:
979
- messages.append(f'Failed to save world asset: {e}')
980
-
981
- # Secondary save method
982
- try:
983
- if les:
984
- les.save_current_level()
985
- messages.append('LevelEditorSubsystem.save_current_level called')
986
- except Exception as e:
987
- messages.append(f'save_current_level failed: {e}')
988
-
989
- # Verify final value(s)
990
- try:
991
- force_val = None
992
- bforce_val = None
993
- try:
994
- force_val = bool(world_settings.get_editor_property('force_no_precomputed_lighting'))
995
- except Exception:
996
- pass
997
- try:
998
- bforce_val = bool(world_settings.get_editor_property('bForceNoPrecomputedLighting'))
999
- except Exception:
1000
- pass
1001
- messages.append(f'Verify WorldSettings.force_no_precomputed_lighting={force_val}')
1002
- messages.append(f'Verify WorldSettings.bForceNoPrecomputedLighting={bforce_val}')
1003
- except Exception as e:
1004
- messages.append(f'Verify failed: {e}')
1005
- except Exception as e:
1006
- messages.append(f'World modification failed: {e}')
1007
-
1008
- print('RESULT:' + json.dumps({'success': True, 'messages': messages, 'flags': {
1009
- 'force_no_precomputed_lighting': force_val if 'force_val' in locals() else None,
1010
- 'bForceNoPrecomputedLighting': bforce_val if 'bforce_val' in locals() else None
1011
- }}))
1012
- `.trim();
1013
-
1014
- // Execute the disable script first and parse messages for diagnostics
1015
- const preResp = await this.bridge.executePython(disablePrecomputedPy);
789
+ if (!this.automationBridge) {
790
+ throw new Error('Automation Bridge required for lighting build');
791
+ }
792
+
1016
793
  try {
1017
- const preOut = typeof preResp === 'string' ? preResp : JSON.stringify(preResp);
1018
- const pm = preOut.match(/RESULT:({.*})/);
1019
- if (pm) {
1020
- try {
1021
- const preJson = JSON.parse(pm[1]);
1022
- if (preJson && preJson.success === false && preJson.status === 'staticDisabled') {
1023
- return { success: false, error: preJson.error } as any;
1024
- }
1025
- if (preJson && preJson.flags) {
1026
- const f = preJson.flags as any;
1027
- if (f.bForceNoPrecomputedLighting === true || f.force_no_precomputed_lighting === true) {
1028
- return {
1029
- success: false,
1030
- error: 'WorldSettings.bForceNoPrecomputedLighting is true. Unreal will skip static lighting builds. Please uncheck "Force No Precomputed Lighting" in this level\'s World Settings (or enable Support Static Lighting in Project Settings) and retry. If using source control, check out the map asset first.'
1031
- } as any;
1032
- }
1033
- }
1034
- } catch {}
794
+ const response = await this.automationBridge.sendAutomationRequest('bake_lightmap', {
795
+ quality: params.quality || 'High',
796
+ buildOnlySelected: params.buildOnlySelected || false,
797
+ buildReflectionCaptures: params.buildReflectionCaptures !== false,
798
+ levelPath: params.levelPath
799
+ }, {
800
+ timeoutMs: 300000 // 5 minutes for lighting builds
801
+ });
802
+
803
+ if (response.success === false) {
804
+ return {
805
+ success: false,
806
+ error: response.error || response.message || 'Failed to build lighting'
807
+ };
1035
808
  }
1036
- } catch {}
1037
-
1038
- // Small delay to ensure settings are applied
1039
- await new Promise(resolve => setTimeout(resolve, 150));
1040
-
1041
- // Now execute the lighting build
1042
- const py = `
1043
- import unreal
1044
- import json
1045
-
1046
- try:
1047
- les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
1048
- if les:
1049
- # Build light maps with specified quality and reflection captures option
1050
- les.build_light_maps(unreal.LightingBuildQuality.${qualityEnum}, ${params.buildReflectionCaptures !== false ? 'True' : 'False'})
1051
- print('RESULT:' + json.dumps({'success': True, 'message': 'Lighting build started via LevelEditorSubsystem'}))
1052
- else:
1053
- # Fallback: Try using console command if subsystem not available
1054
- try:
1055
- unreal.SystemLibrary.execute_console_command(None, 'BuildLighting Quality=${q}')
1056
- ${params.buildReflectionCaptures ? "unreal.SystemLibrary.execute_console_command(None, 'BuildReflectionCaptures')" : ''}
1057
- print('RESULT:' + json.dumps({'success': True, 'message': 'Lighting build started via console command (fallback)'}))
1058
- except Exception as e2:
1059
- print('RESULT:' + json.dumps({'success': False, 'error': f'Build failed: {str(e2)}'}))
1060
- except Exception as e:
1061
- print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
1062
- `.trim();
1063
- const resp = await this.bridge.executePython(py);
1064
- const out = typeof resp === 'string' ? resp : JSON.stringify(resp);
1065
- const m = out.match(/RESULT:({.*})/);
1066
- if (m) { try { const parsed = JSON.parse(m[1]); return parsed.success ? { success: true, message: parsed.message } : { success: false, error: parsed.error }; } catch {} }
1067
- return { success: true, message: 'Lighting build started' };
809
+
810
+ return {
811
+ success: true,
812
+ message: response.message || 'Lighting build started',
813
+ ...(response.result || {})
814
+ } as any;
815
+ } catch (error) {
816
+ return {
817
+ success: false,
818
+ error: `Failed to build lighting: ${error instanceof Error ? error.message : String(error)}`
819
+ } as any;
820
+ }
1068
821
  }
1069
822
 
1070
- // Create a new level with proper lighting settings as workaround
823
+ // Create a new level with proper lighting settings
1071
824
  async createLightingEnabledLevel(params?: {
1072
825
  levelName?: string;
1073
826
  copyActors?: boolean;
1074
827
  useTemplate?: boolean;
1075
828
  }) {
1076
829
  const levelName = params?.levelName || 'LightingEnabledLevel';
1077
- const py = `
1078
- import unreal
1079
- import json
1080
-
1081
- def create_lighting_enabled_level():
1082
- """Create a new level with lighting enabled"""
1083
- try:
1084
- les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
1085
- ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
1086
- actor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
1087
- editor_asset = unreal.EditorAssetLibrary
1088
-
1089
- if not les or not ues:
1090
- return {'success': False, 'error': 'Required subsystems not available'}
1091
-
1092
- # Store current actors if we need to copy them
1093
- actors_to_copy = []
1094
- if ${params?.copyActors ? 'True' : 'False'}:
1095
- current_world = ues.get_editor_world()
1096
- if current_world:
1097
- all_actors = actor_sub.get_all_level_actors()
1098
- # Filter out unnecessary actors - only copy static meshes and important gameplay actors
1099
- for actor in all_actors:
1100
- if actor:
1101
- class_name = actor.get_class().get_name()
1102
- # Only copy specific actor types
1103
- if class_name in ['StaticMeshActor', 'SkeletalMeshActor', 'Blueprint', 'Actor']:
1104
- try:
1105
- actor_data = {
1106
- 'class': actor.get_class(),
1107
- 'location': actor.get_actor_location(),
1108
- 'rotation': actor.get_actor_rotation(),
1109
- 'scale': actor.get_actor_scale3d(),
1110
- 'label': actor.get_actor_label()
1111
- }
1112
- # Check if actor has a static mesh component
1113
- mesh_comp = actor.get_component_by_class(unreal.StaticMeshComponent)
1114
- if mesh_comp:
1115
- mesh = mesh_comp.get_editor_property('static_mesh')
1116
- if mesh:
1117
- actor_data['mesh'] = mesh
1118
- actors_to_copy.append(actor_data)
1119
- except:
1120
- pass
1121
- print(f'Stored {len(actors_to_copy)} actors to copy')
1122
-
1123
- # Create new level with proper template or blank
1124
- level_name_str = "${levelName}"
1125
- level_path = f'/Game/Maps/{level_name_str}'
1126
-
1127
- # Try different approaches to create a level with lighting enabled
1128
- level_created = False
1129
-
1130
- # Method 1: Try using the Default template (not Blank)
1131
- try:
1132
- # The Default template should have lighting enabled
1133
- template_path = '/Engine/Maps/Templates/Template_Default'
1134
- if editor_asset.does_asset_exist(template_path):
1135
- les.new_level_from_template(level_path, template_path)
1136
- print(f'Created level from Default template: {level_path}')
1137
- level_created = True
1138
- except:
1139
- pass
1140
-
1141
- # Method 2: Try TimeOfDay template
1142
- if not level_created:
1143
- try:
1144
- template_path = '/Engine/Maps/Templates/TimeOfDay'
1145
- if editor_asset.does_asset_exist(template_path):
1146
- les.new_level_from_template(level_path, template_path)
1147
- print(f'Created level from TimeOfDay template: {level_path}')
1148
- level_created = True
1149
- except:
1150
- pass
1151
-
1152
- # Method 3: Create blank and manually configure
1153
- if not level_created:
1154
- les.new_level(level_path, False)
1155
- print(f'Created new blank level: {level_path}')
1156
- level_created = True
1157
-
1158
- # CRITICAL: Force disable ForceNoPrecomputedLighting using all possible methods
1159
- new_world = ues.get_editor_world()
1160
- if new_world:
1161
- new_ws = new_world.get_world_settings()
1162
- if new_ws:
1163
- # Method 1: Direct property modification
1164
- for prop in ['force_no_precomputed_lighting', 'bForceNoPrecomputedLighting',
1165
- 'ForceNoPrecomputedLighting', 'bforce_no_precomputed_lighting']:
1166
- try:
1167
- new_ws.set_editor_property(prop, False)
1168
- except:
1169
- pass
1170
-
1171
- # Method 2: Modify via reflection
1172
- try:
1173
- # Access the property through the class default object
1174
- ws_class = new_ws.get_class()
1175
- ws_cdo = unreal.get_default_object(ws_class)
1176
- if ws_cdo:
1177
- ws_cdo.set_editor_property('force_no_precomputed_lighting', False)
1178
- ws_cdo.set_editor_property('bForceNoPrecomputedLighting', False)
1179
- except:
1180
- pass
1181
-
1182
- # Method 3: Override with Lightmass settings
1183
- try:
1184
- # Create proper Lightmass settings
1185
- lightmass_settings = unreal.LightmassWorldInfoSettings()
1186
- lightmass_settings.static_lighting_level_scale = 1.0
1187
- lightmass_settings.num_indirect_lighting_bounces = 3
1188
- lightmass_settings.use_ambient_occlusion = True
1189
- lightmass_settings.generate_ambient_occlusion_material_mask = False
1190
-
1191
- new_ws.set_editor_property('lightmass_settings', lightmass_settings)
1192
- except:
1193
- pass
1194
-
1195
- # Method 4: Force save and reload to apply changes
1196
- try:
1197
- # Mark the world settings as dirty
1198
- new_ws.modify()
1199
- # Save immediately
1200
- les.save_current_level()
1201
- # Force update
1202
- new_world.force_update_level_bounds()
1203
- except:
1204
- pass
1205
-
1206
- # Verify the setting
1207
- try:
1208
- val = new_ws.get_editor_property('force_no_precomputed_lighting')
1209
- print(f'New level force_no_precomputed_lighting: {val}')
1210
- if val:
1211
- print('WARNING: ForceNoPrecomputedLighting is persistent - project setting override detected')
1212
- print('WORKAROUND: Will use dynamic lighting only')
1213
- except:
1214
- pass
1215
-
1216
- # Copy actors if requested
1217
- if actors_to_copy and actor_sub:
1218
- print('Copying actors to new level...')
1219
- copied = 0
1220
- for actor_data in actors_to_copy:
1221
- try:
1222
- # Spawn a static mesh actor if we have mesh data
1223
- if 'mesh' in actor_data:
1224
- # Create a proper static mesh actor
1225
- spawned = actor_sub.spawn_actor_from_class(
1226
- unreal.StaticMeshActor,
1227
- actor_data['location'],
1228
- actor_data['rotation']
1229
- )
1230
- if spawned:
1231
- spawned.set_actor_scale3d(actor_data['scale'])
1232
- spawned.set_actor_label(actor_data['label'])
1233
- # Set the static mesh
1234
- mesh_comp = spawned.get_component_by_class(unreal.StaticMeshComponent)
1235
- if mesh_comp:
1236
- mesh_comp.set_static_mesh(actor_data['mesh'])
1237
- copied += 1
1238
- else:
1239
- # Spawn regular actor
1240
- spawned = actor_sub.spawn_actor_from_class(
1241
- actor_data['class'],
1242
- actor_data['location'],
1243
- actor_data['rotation']
1244
- )
1245
- if spawned:
1246
- spawned.set_actor_scale3d(actor_data['scale'])
1247
- spawned.set_actor_label(actor_data['label'])
1248
- copied += 1
1249
- except Exception as e:
1250
- pass # Silently skip failed copies
1251
- print(f'Successfully copied {copied} actors')
1252
-
1253
- # Add essential lighting actors if not using template
1254
- if not use_template:
1255
- # Add a directional light for sun
1256
- light = actor_sub.spawn_actor_from_class(
1257
- unreal.DirectionalLight,
1258
- unreal.Vector(0, 0, 500),
1259
- unreal.Rotator(-45, 45, 0)
1260
- )
1261
- if light:
1262
- light.set_actor_label('Sun_Light')
1263
- light_comp = light.get_component_by_class(unreal.DirectionalLightComponent)
1264
- if light_comp:
1265
- light_comp.set_intensity(3.14159) # Pi lux for realistic sun
1266
- light_comp.set_light_color(unreal.LinearColor(1, 0.95, 0.8, 1))
1267
- print('Added directional light')
1268
-
1269
- # Add sky light for ambient
1270
- sky = actor_sub.spawn_actor_from_class(
1271
- unreal.SkyLight,
1272
- unreal.Vector(0, 0, 300),
1273
- unreal.Rotator(0, 0, 0)
1274
- )
1275
- if sky:
1276
- sky.set_actor_label('Sky_Light')
1277
- sky_comp = sky.get_component_by_class(unreal.SkyLightComponent)
1278
- if sky_comp:
1279
- sky_comp.set_intensity(1.0)
1280
- print('Added sky light')
1281
-
1282
- # Add sky atmosphere for realistic sky
1283
- atmosphere = actor_sub.spawn_actor_from_class(
1284
- unreal.SkyAtmosphere,
1285
- unreal.Vector(0, 0, 0),
1286
- unreal.Rotator(0, 0, 0)
1287
- )
1288
- if atmosphere:
1289
- atmosphere.set_actor_label('Sky_Atmosphere')
1290
- print('Added sky atmosphere')
1291
-
1292
- # Save the new level
1293
- les.save_current_level()
1294
- print('New level saved')
1295
-
830
+
831
+ if (!this.automationBridge) {
832
+ throw new Error('Automation Bridge not available. Level creation requires plugin support.');
833
+ }
834
+
835
+ try {
836
+ const response = await this.automationBridge.sendAutomationRequest('create_lighting_enabled_level', {
837
+ levelName,
838
+ copyActors: params?.copyActors === true,
839
+ useTemplate: params?.useTemplate === true,
840
+ path: params?.levelName ? `/Game/Maps/${params.levelName}` : undefined // Ensure path is sent
841
+ }, {
842
+ timeoutMs: 120000 // 2 minutes for level creation
843
+ });
844
+
845
+ if (response.success === false) {
1296
846
  return {
1297
- 'success': True,
1298
- 'message': f'Created new level "{level_name_str}" with lighting enabled',
1299
- 'path': level_path
1300
- }
1301
-
1302
- except Exception as e:
1303
- return {'success': False, 'error': str(e)}
1304
-
1305
- result = create_lighting_enabled_level()
1306
- print('RESULT:' + json.dumps(result))
1307
- `.trim();
1308
-
1309
- const resp = await this.bridge.executePython(py);
1310
- const out = typeof resp === 'string' ? resp : JSON.stringify(resp);
1311
- const m = out.match(/RESULT:({.*})/);
1312
- if (m) {
1313
- try {
1314
- const parsed = JSON.parse(m[1]);
1315
- return parsed;
1316
- } catch {}
1317
- }
1318
- return { success: true, message: 'New level creation attempted' };
847
+ success: false,
848
+ error: response.error || response.message || 'Failed to create level'
849
+ };
850
+ }
851
+
852
+ return {
853
+ success: true,
854
+ message: response.message || `Created new level "${levelName}" with lighting enabled`,
855
+ ...(response.result || {})
856
+ };
857
+ } catch (error) {
858
+ return {
859
+ success: false,
860
+ error: `Failed to create lighting-enabled level: ${error}`
861
+ };
862
+ }
1319
863
  }
1320
864
 
1321
- // Create lightmass importance volume via Python
865
+ // Create lightmass importance volume
1322
866
  async createLightmassVolume(params: {
1323
867
  name: string;
1324
868
  location: [number, number, number];
1325
869
  size: [number, number, number];
1326
870
  }) {
1327
871
  const name = this.normalizeName(params.name);
1328
- const escapedName = escapePythonString(name);
1329
- const [lx, ly, lz] = params.location;
1330
- const [sx, sy, sz] = params.size;
1331
- const py = `
1332
- import unreal
1333
- editor_actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
1334
- loc = unreal.Vector(${lx}, ${ly}, ${lz})
1335
- rot = unreal.Rotator(0,0,0)
1336
- actor = editor_actor_subsystem.spawn_actor_from_class(unreal.LightmassImportanceVolume, loc, rot)
1337
- if actor:
1338
- try: actor.set_actor_label("${escapedName}")
1339
- except Exception: pass
1340
- # Best-effort: set actor scale to approximate size
1341
- try:
1342
- actor.set_actor_scale3d(unreal.Vector(max(${sx}/100.0, 0.1), max(${sy}/100.0, 0.1), max(${sz}/100.0, 0.1)))
1343
- except Exception: pass
1344
- print("RESULT:{'success': True}")
1345
- else:
1346
- print("RESULT:{'success': False, 'error': 'Failed to spawn LightmassImportanceVolume'}")
1347
- `.trim();
1348
- const resp = await this.bridge.executePython(py);
1349
- const out = typeof resp === 'string' ? resp : JSON.stringify(resp);
1350
- const m = out.match(/RESULT:({.*})/);
1351
- if (m) { try { const parsed = JSON.parse(m[1].replace(/'/g, '"')); return parsed.success ? { success: true, message: `LightmassImportanceVolume '${name}' created` } : { success: false, error: parsed.error }; } catch {} }
1352
- return { success: true, message: 'LightmassImportanceVolume creation attempted' };
872
+
873
+ if (!this.automationBridge) {
874
+ throw new Error('Automation Bridge not available. Lightmass volume creation requires plugin support.');
875
+ }
876
+
877
+ try {
878
+ const response = await this.automationBridge.sendAutomationRequest('create_lightmass_volume', {
879
+ name,
880
+ location: { x: params.location[0], y: params.location[1], z: params.location[2] },
881
+ size: { x: params.size[0], y: params.size[1], z: params.size[2] }
882
+ }, {
883
+ timeoutMs: 60000
884
+ });
885
+
886
+ if (response.success === false) {
887
+ return {
888
+ success: false,
889
+ error: response.error || response.message || 'Failed to create lightmass volume'
890
+ };
891
+ }
892
+
893
+ return {
894
+ success: true,
895
+ message: `LightmassImportanceVolume '${name}' created`,
896
+ ...(response.result || {})
897
+ };
898
+ } catch (error) {
899
+ return {
900
+ success: false,
901
+ error: `Failed to create lightmass volume: ${error}`
902
+ };
903
+ }
1353
904
  }
1354
905
 
1355
906
  // Set exposure
@@ -1359,27 +910,41 @@ else:
1359
910
  minBrightness?: number;
1360
911
  maxBrightness?: number;
1361
912
  }) {
913
+ if (this.automationBridge) {
914
+ try {
915
+ const response = await this.automationBridge.sendAutomationRequest('set_exposure', {
916
+ method: params.method,
917
+ compensationValue: params.compensationValue,
918
+ minBrightness: params.minBrightness,
919
+ maxBrightness: params.maxBrightness
920
+ });
921
+ if (response.success) return { success: true, message: 'Exposure settings updated via bridge', ...(response.result || {}) };
922
+ } catch (_e) {
923
+ // Fallback
924
+ }
925
+ }
926
+
1362
927
  const commands = [];
1363
-
928
+
1364
929
  commands.push(`r.EyeAdaptation.ExposureMethod ${params.method === 'Manual' ? 0 : 1}`);
1365
-
930
+
1366
931
  if (params.compensationValue !== undefined) {
1367
932
  commands.push(`r.EyeAdaptation.ExposureCompensation ${params.compensationValue}`);
1368
933
  }
1369
-
934
+
1370
935
  if (params.minBrightness !== undefined) {
1371
936
  commands.push(`r.EyeAdaptation.MinBrightness ${params.minBrightness}`);
1372
937
  }
1373
-
938
+
1374
939
  if (params.maxBrightness !== undefined) {
1375
940
  commands.push(`r.EyeAdaptation.MaxBrightness ${params.maxBrightness}`);
1376
941
  }
1377
-
942
+
1378
943
  for (const cmd of commands) {
1379
944
  await this.bridge.executeConsoleCommand(cmd);
1380
945
  }
1381
-
1382
- return { success: true, message: 'Exposure settings updated' };
946
+
947
+ return { success: true, message: 'Exposure settings updated (console)' };
1383
948
  }
1384
949
 
1385
950
  // Set ambient occlusion
@@ -1389,31 +954,45 @@ else:
1389
954
  radius?: number;
1390
955
  quality?: 'Low' | 'Medium' | 'High';
1391
956
  }) {
957
+ if (this.automationBridge) {
958
+ try {
959
+ const response = await this.automationBridge.sendAutomationRequest('set_ambient_occlusion', {
960
+ enabled: params.enabled,
961
+ intensity: params.intensity,
962
+ radius: params.radius,
963
+ quality: params.quality
964
+ });
965
+ if (response.success) return { success: true, message: 'Ambient occlusion configured via bridge', ...(response.result || {}) };
966
+ } catch (_e) {
967
+ // Fallback
968
+ }
969
+ }
970
+
1392
971
  const commands = [];
1393
-
972
+
1394
973
  commands.push(`r.AmbientOcclusion.Enabled ${params.enabled ? 1 : 0}`);
1395
-
974
+
1396
975
  if (params.intensity !== undefined) {
1397
976
  commands.push(`r.AmbientOcclusion.Intensity ${params.intensity}`);
1398
977
  }
1399
-
978
+
1400
979
  if (params.radius !== undefined) {
1401
980
  commands.push(`r.AmbientOcclusion.Radius ${params.radius}`);
1402
981
  }
1403
-
982
+
1404
983
  if (params.quality) {
1405
984
  const qualityMap = { 'Low': 0, 'Medium': 1, 'High': 2 };
1406
985
  commands.push(`r.AmbientOcclusion.Quality ${qualityMap[params.quality]}`);
1407
986
  }
1408
-
987
+
1409
988
  for (const cmd of commands) {
1410
989
  await this.bridge.executeConsoleCommand(cmd);
1411
990
  }
1412
-
1413
- return { success: true, message: 'Ambient occlusion configured' };
991
+
992
+ return { success: true, message: 'Ambient occlusion configured (console)' };
1414
993
  }
1415
994
 
1416
- // Setup volumetric fog (prefer Python to adjust fog actor/component)
995
+ // Setup volumetric fog
1417
996
  async setupVolumetricFog(params: {
1418
997
  enabled: boolean;
1419
998
  density?: number;
@@ -1423,15 +1002,40 @@ else:
1423
1002
  // Enable/disable global volumetric fog via CVar
1424
1003
  await this.bridge.executeConsoleCommand(`r.VolumetricFog ${params.enabled ? 1 : 0}`);
1425
1004
 
1426
- const py = `\nimport unreal\ntry:\n actor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)\n actors = actor_sub.get_all_level_actors() if actor_sub else []\n fog = None\n for a in actors:\n try:\n if a.get_class().get_name() == 'ExponentialHeightFog':\n fog = a\n break\n except Exception: pass\n if fog:\n comp = fog.get_component_by_class(unreal.ExponentialHeightFogComponent)\n if comp:\n ${params.density !== undefined ? `\n try: comp.set_fog_density(${params.density})\n except Exception: comp.set_editor_property('fog_density', ${params.density})\n ` : ''}
1427
- ${params.scatteringIntensity !== undefined ? `\n try: comp.set_fog_max_opacity(${Math.min(Math.max(params.scatteringIntensity,0),1)})\n except Exception: pass\n ` : ''}
1428
- ${params.fogHeight !== undefined ? `\n try:\n L = fog.get_actor_location()\n fog.set_actor_location(unreal.Vector(L.x, L.y, ${params.fogHeight}))\n except Exception: pass\n ` : ''}
1429
- print("RESULT:{'success': True}")\nexcept Exception as e:\n print("RESULT:{'success': False, 'error': '%s'}" % str(e))\n`.trim();
1005
+ if (!this.automationBridge) {
1006
+ return {
1007
+ success: true,
1008
+ message: 'Volumetric fog console setting applied (plugin required for fog actor adjustment)'
1009
+ };
1010
+ }
1011
+
1012
+ try {
1013
+ const response = await this.automationBridge.sendAutomationRequest('setup_volumetric_fog', {
1014
+ enabled: params.enabled,
1015
+ density: params.density,
1016
+ scatteringIntensity: params.scatteringIntensity,
1017
+ fogHeight: params.fogHeight
1018
+ }, {
1019
+ timeoutMs: 60000
1020
+ });
1021
+
1022
+ if (response.success === false) {
1023
+ return {
1024
+ success: false,
1025
+ error: response.error || response.message || 'Failed to configure volumetric fog'
1026
+ };
1027
+ }
1430
1028
 
1431
- const resp = await this.bridge.executePython(py);
1432
- const out = typeof resp === 'string' ? resp : JSON.stringify(resp);
1433
- const m = out.match(/RESULT:({.*})/);
1434
- if (m) { try { const parsed = JSON.parse(m[1].replace(/'/g, '"')); return parsed.success ? { success: true, message: 'Volumetric fog configured' } : { success: false, error: parsed.error }; } catch {} }
1435
- return { success: true, message: 'Volumetric fog configured' };
1029
+ return {
1030
+ success: true,
1031
+ message: 'Volumetric fog configured',
1032
+ ...(response.result || {})
1033
+ };
1034
+ } catch (error) {
1035
+ return {
1036
+ success: false,
1037
+ error: `Failed to setup volumetric fog: ${error}`
1038
+ };
1039
+ }
1436
1040
  }
1437
1041
  }