unreal-engine-mcp-server 0.4.7 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (438) hide show
  1. package/.env.example +26 -0
  2. package/.env.production +38 -7
  3. package/.eslintrc.json +0 -54
  4. package/.eslintrc.override.json +8 -0
  5. package/.github/ISSUE_TEMPLATE/bug_report.yml +94 -0
  6. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  7. package/.github/ISSUE_TEMPLATE/feature_request.yml +56 -0
  8. package/.github/copilot-instructions.md +478 -45
  9. package/.github/dependabot.yml +19 -0
  10. package/.github/labeler.yml +24 -0
  11. package/.github/labels.yml +70 -0
  12. package/.github/pull_request_template.md +42 -0
  13. package/.github/release-drafter.yml +148 -0
  14. package/.github/workflows/auto-merge.yml +38 -0
  15. package/.github/workflows/ci.yml +38 -0
  16. package/.github/workflows/dependency-review.yml +17 -0
  17. package/.github/workflows/gemini-issue-triage.yml +172 -0
  18. package/.github/workflows/greetings.yml +23 -0
  19. package/.github/workflows/labeler.yml +16 -0
  20. package/.github/workflows/links.yml +80 -0
  21. package/.github/workflows/pr-size-labeler.yml +137 -0
  22. package/.github/workflows/publish-mcp.yml +12 -7
  23. package/.github/workflows/release-drafter.yml +23 -0
  24. package/.github/workflows/release.yml +112 -0
  25. package/.github/workflows/semantic-pull-request.yml +35 -0
  26. package/.github/workflows/smoke-test.yml +36 -0
  27. package/.github/workflows/stale.yml +28 -0
  28. package/CHANGELOG.md +267 -31
  29. package/CONTRIBUTING.md +140 -0
  30. package/README.md +166 -71
  31. package/claude_desktop_config_example.json +7 -6
  32. package/dist/automation/bridge.d.ts +50 -0
  33. package/dist/automation/bridge.js +452 -0
  34. package/dist/automation/connection-manager.d.ts +23 -0
  35. package/dist/automation/connection-manager.js +107 -0
  36. package/dist/automation/handshake.d.ts +11 -0
  37. package/dist/automation/handshake.js +89 -0
  38. package/dist/automation/index.d.ts +3 -0
  39. package/dist/automation/index.js +3 -0
  40. package/dist/automation/message-handler.d.ts +12 -0
  41. package/dist/automation/message-handler.js +149 -0
  42. package/dist/automation/request-tracker.d.ts +25 -0
  43. package/dist/automation/request-tracker.js +98 -0
  44. package/dist/automation/types.d.ts +130 -0
  45. package/dist/automation/types.js +2 -0
  46. package/dist/cli.js +32 -5
  47. package/dist/config.d.ts +27 -0
  48. package/dist/config.js +60 -0
  49. package/dist/constants.d.ts +12 -0
  50. package/dist/constants.js +12 -0
  51. package/dist/graphql/resolvers.d.ts +268 -0
  52. package/dist/graphql/resolvers.js +743 -0
  53. package/dist/graphql/schema.d.ts +5 -0
  54. package/dist/graphql/schema.js +437 -0
  55. package/dist/graphql/server.d.ts +26 -0
  56. package/dist/graphql/server.js +115 -0
  57. package/dist/graphql/types.d.ts +7 -0
  58. package/dist/graphql/types.js +2 -0
  59. package/dist/handlers/resource-handlers.d.ts +20 -0
  60. package/dist/handlers/resource-handlers.js +180 -0
  61. package/dist/index.d.ts +31 -18
  62. package/dist/index.js +119 -619
  63. package/dist/prompts/index.js +4 -4
  64. package/dist/resources/actors.d.ts +17 -12
  65. package/dist/resources/actors.js +56 -76
  66. package/dist/resources/assets.d.ts +6 -14
  67. package/dist/resources/assets.js +115 -147
  68. package/dist/resources/levels.d.ts +13 -13
  69. package/dist/resources/levels.js +25 -34
  70. package/dist/server/resource-registry.d.ts +20 -0
  71. package/dist/server/resource-registry.js +37 -0
  72. package/dist/server/tool-registry.d.ts +23 -0
  73. package/dist/server/tool-registry.js +322 -0
  74. package/dist/server-setup.d.ts +21 -0
  75. package/dist/server-setup.js +111 -0
  76. package/dist/services/health-monitor.d.ts +34 -0
  77. package/dist/services/health-monitor.js +105 -0
  78. package/dist/services/metrics-server.d.ts +11 -0
  79. package/dist/services/metrics-server.js +105 -0
  80. package/dist/tools/actors.d.ts +147 -9
  81. package/dist/tools/actors.js +350 -311
  82. package/dist/tools/animation.d.ts +135 -4
  83. package/dist/tools/animation.js +510 -411
  84. package/dist/tools/assets.d.ts +117 -19
  85. package/dist/tools/assets.js +259 -284
  86. package/dist/tools/audio.d.ts +102 -42
  87. package/dist/tools/audio.js +272 -685
  88. package/dist/tools/base-tool.d.ts +17 -0
  89. package/dist/tools/base-tool.js +46 -0
  90. package/dist/tools/behavior-tree.d.ts +94 -0
  91. package/dist/tools/behavior-tree.js +39 -0
  92. package/dist/tools/blueprint/helpers.d.ts +29 -0
  93. package/dist/tools/blueprint/helpers.js +182 -0
  94. package/dist/tools/blueprint.d.ts +228 -118
  95. package/dist/tools/blueprint.js +685 -832
  96. package/dist/tools/consolidated-tool-definitions.d.ts +5462 -1781
  97. package/dist/tools/consolidated-tool-definitions.js +829 -496
  98. package/dist/tools/consolidated-tool-handlers.d.ts +2 -1
  99. package/dist/tools/consolidated-tool-handlers.js +211 -1026
  100. package/dist/tools/debug.d.ts +143 -85
  101. package/dist/tools/debug.js +234 -180
  102. package/dist/tools/dynamic-handler-registry.d.ts +11 -0
  103. package/dist/tools/dynamic-handler-registry.js +101 -0
  104. package/dist/tools/editor.d.ts +139 -18
  105. package/dist/tools/editor.js +239 -244
  106. package/dist/tools/engine.d.ts +10 -4
  107. package/dist/tools/engine.js +13 -5
  108. package/dist/tools/environment.d.ts +36 -0
  109. package/dist/tools/environment.js +267 -0
  110. package/dist/tools/foliage.d.ts +105 -14
  111. package/dist/tools/foliage.js +219 -331
  112. package/dist/tools/handlers/actor-handlers.d.ts +3 -0
  113. package/dist/tools/handlers/actor-handlers.js +232 -0
  114. package/dist/tools/handlers/animation-handlers.d.ts +3 -0
  115. package/dist/tools/handlers/animation-handlers.js +185 -0
  116. package/dist/tools/handlers/argument-helper.d.ts +16 -0
  117. package/dist/tools/handlers/argument-helper.js +80 -0
  118. package/dist/tools/handlers/asset-handlers.d.ts +3 -0
  119. package/dist/tools/handlers/asset-handlers.js +496 -0
  120. package/dist/tools/handlers/audio-handlers.d.ts +3 -0
  121. package/dist/tools/handlers/audio-handlers.js +166 -0
  122. package/dist/tools/handlers/blueprint-handlers.d.ts +4 -0
  123. package/dist/tools/handlers/blueprint-handlers.js +358 -0
  124. package/dist/tools/handlers/common-handlers.d.ts +14 -0
  125. package/dist/tools/handlers/common-handlers.js +56 -0
  126. package/dist/tools/handlers/editor-handlers.d.ts +3 -0
  127. package/dist/tools/handlers/editor-handlers.js +119 -0
  128. package/dist/tools/handlers/effect-handlers.d.ts +3 -0
  129. package/dist/tools/handlers/effect-handlers.js +171 -0
  130. package/dist/tools/handlers/environment-handlers.d.ts +3 -0
  131. package/dist/tools/handlers/environment-handlers.js +170 -0
  132. package/dist/tools/handlers/graph-handlers.d.ts +3 -0
  133. package/dist/tools/handlers/graph-handlers.js +90 -0
  134. package/dist/tools/handlers/input-handlers.d.ts +3 -0
  135. package/dist/tools/handlers/input-handlers.js +21 -0
  136. package/dist/tools/handlers/inspect-handlers.d.ts +3 -0
  137. package/dist/tools/handlers/inspect-handlers.js +383 -0
  138. package/dist/tools/handlers/level-handlers.d.ts +3 -0
  139. package/dist/tools/handlers/level-handlers.js +237 -0
  140. package/dist/tools/handlers/lighting-handlers.d.ts +3 -0
  141. package/dist/tools/handlers/lighting-handlers.js +144 -0
  142. package/dist/tools/handlers/performance-handlers.d.ts +3 -0
  143. package/dist/tools/handlers/performance-handlers.js +130 -0
  144. package/dist/tools/handlers/pipeline-handlers.d.ts +3 -0
  145. package/dist/tools/handlers/pipeline-handlers.js +110 -0
  146. package/dist/tools/handlers/sequence-handlers.d.ts +3 -0
  147. package/dist/tools/handlers/sequence-handlers.js +376 -0
  148. package/dist/tools/handlers/system-handlers.d.ts +4 -0
  149. package/dist/tools/handlers/system-handlers.js +506 -0
  150. package/dist/tools/input.d.ts +19 -0
  151. package/dist/tools/input.js +89 -0
  152. package/dist/tools/introspection.d.ts +103 -40
  153. package/dist/tools/introspection.js +425 -568
  154. package/dist/tools/landscape.d.ts +97 -36
  155. package/dist/tools/landscape.js +280 -409
  156. package/dist/tools/level.d.ts +130 -10
  157. package/dist/tools/level.js +639 -675
  158. package/dist/tools/lighting.d.ts +77 -38
  159. package/dist/tools/lighting.js +441 -943
  160. package/dist/tools/logs.d.ts +3 -3
  161. package/dist/tools/logs.js +5 -57
  162. package/dist/tools/materials.d.ts +91 -24
  163. package/dist/tools/materials.js +190 -118
  164. package/dist/tools/niagara.d.ts +149 -39
  165. package/dist/tools/niagara.js +232 -182
  166. package/dist/tools/performance.d.ts +27 -12
  167. package/dist/tools/performance.js +204 -122
  168. package/dist/tools/physics.d.ts +32 -77
  169. package/dist/tools/physics.js +171 -582
  170. package/dist/tools/property-dictionary.d.ts +13 -0
  171. package/dist/tools/property-dictionary.js +82 -0
  172. package/dist/tools/sequence.d.ts +73 -48
  173. package/dist/tools/sequence.js +196 -748
  174. package/dist/tools/tool-definition-utils.d.ts +59 -0
  175. package/dist/tools/tool-definition-utils.js +35 -0
  176. package/dist/tools/ui.d.ts +66 -34
  177. package/dist/tools/ui.js +134 -214
  178. package/dist/types/env.d.ts +0 -3
  179. package/dist/types/env.js +0 -7
  180. package/dist/types/tool-interfaces.d.ts +898 -0
  181. package/dist/types/tool-interfaces.js +2 -0
  182. package/dist/types/tool-types.d.ts +183 -19
  183. package/dist/types/tool-types.js +0 -4
  184. package/dist/unreal-bridge.d.ts +24 -131
  185. package/dist/unreal-bridge.js +364 -1506
  186. package/dist/utils/command-validator.d.ts +9 -0
  187. package/dist/utils/command-validator.js +67 -0
  188. package/dist/utils/elicitation.d.ts +1 -1
  189. package/dist/utils/elicitation.js +12 -15
  190. package/dist/utils/error-handler.d.ts +2 -51
  191. package/dist/utils/error-handler.js +11 -87
  192. package/dist/utils/ini-reader.d.ts +3 -0
  193. package/dist/utils/ini-reader.js +69 -0
  194. package/dist/utils/logger.js +9 -6
  195. package/dist/utils/normalize.d.ts +3 -0
  196. package/dist/utils/normalize.js +56 -0
  197. package/dist/utils/response-factory.d.ts +7 -0
  198. package/dist/utils/response-factory.js +33 -0
  199. package/dist/utils/response-validator.d.ts +3 -24
  200. package/dist/utils/response-validator.js +130 -81
  201. package/dist/utils/result-helpers.d.ts +4 -5
  202. package/dist/utils/result-helpers.js +15 -16
  203. package/dist/utils/safe-json.js +5 -11
  204. package/dist/utils/unreal-command-queue.d.ts +24 -0
  205. package/dist/utils/unreal-command-queue.js +120 -0
  206. package/dist/utils/validation.d.ts +0 -40
  207. package/dist/utils/validation.js +1 -78
  208. package/dist/wasm/index.d.ts +70 -0
  209. package/dist/wasm/index.js +535 -0
  210. package/docs/GraphQL-API.md +888 -0
  211. package/docs/Migration-Guide-v0.5.0.md +692 -0
  212. package/docs/Roadmap.md +53 -0
  213. package/docs/WebAssembly-Integration.md +628 -0
  214. package/docs/editor-plugin-extension.md +370 -0
  215. package/docs/handler-mapping.md +242 -0
  216. package/docs/native-automation-progress.md +128 -0
  217. package/docs/testing-guide.md +423 -0
  218. package/mcp-config-example.json +6 -6
  219. package/package.json +60 -27
  220. package/plugins/McpAutomationBridge/Config/FilterPlugin.ini +8 -0
  221. package/plugins/McpAutomationBridge/McpAutomationBridge.uplugin +64 -0
  222. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/McpAutomationBridge.Build.cs +189 -0
  223. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.cpp +22 -0
  224. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.h +30 -0
  225. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeHelpers.h +1983 -0
  226. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeModule.cpp +72 -0
  227. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSettings.cpp +46 -0
  228. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +581 -0
  229. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +2394 -0
  230. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetQueryHandlers.cpp +300 -0
  231. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetWorkflowHandlers.cpp +2807 -0
  232. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AudioHandlers.cpp +1087 -0
  233. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BehaviorTreeHandlers.cpp +488 -0
  234. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.cpp +643 -0
  235. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.h +31 -0
  236. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +1184 -0
  237. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +5652 -0
  238. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers_List.cpp +152 -0
  239. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ControlHandlers.cpp +2614 -0
  240. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_DebugHandlers.cpp +42 -0
  241. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EditorFunctionHandlers.cpp +1237 -0
  242. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +1701 -0
  243. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +2145 -0
  244. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_FoliageHandlers.cpp +954 -0
  245. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InputHandlers.cpp +209 -0
  246. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InsightsHandlers.cpp +41 -0
  247. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LandscapeHandlers.cpp +1164 -0
  248. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LevelHandlers.cpp +762 -0
  249. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +634 -0
  250. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LogHandlers.cpp +136 -0
  251. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_MaterialGraphHandlers.cpp +494 -0
  252. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraGraphHandlers.cpp +278 -0
  253. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraHandlers.cpp +625 -0
  254. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PerformanceHandlers.cpp +401 -0
  255. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PipelineHandlers.cpp +67 -0
  256. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +735 -0
  257. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PropertyHandlers.cpp +2634 -0
  258. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_RenderHandlers.cpp +189 -0
  259. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.cpp +917 -0
  260. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.h +39 -0
  261. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +2670 -0
  262. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequencerHandlers.cpp +519 -0
  263. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_TestHandlers.cpp +38 -0
  264. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_UiHandlers.cpp +668 -0
  265. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_WorldPartitionHandlers.cpp +346 -0
  266. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.cpp +1330 -0
  267. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.h +149 -0
  268. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +783 -0
  269. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSettings.h +115 -0
  270. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSubsystem.h +796 -0
  271. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpConnectionManager.h +117 -0
  272. package/scripts/check-unreal-connection.mjs +19 -0
  273. package/scripts/clean-tmp.js +23 -0
  274. package/scripts/patch-wasm.js +26 -0
  275. package/scripts/run-all-tests.mjs +131 -0
  276. package/scripts/smoke-test.ts +94 -0
  277. package/scripts/sync-mcp-plugin.js +143 -0
  278. package/scripts/test-no-plugin-alternates.mjs +113 -0
  279. package/scripts/validate-server.js +46 -0
  280. package/scripts/verify-automation-bridge.js +200 -0
  281. package/server.json +57 -21
  282. package/src/automation/bridge.ts +558 -0
  283. package/src/automation/connection-manager.ts +130 -0
  284. package/src/automation/handshake.ts +99 -0
  285. package/src/automation/index.ts +2 -0
  286. package/src/automation/message-handler.ts +167 -0
  287. package/src/automation/request-tracker.ts +123 -0
  288. package/src/automation/types.ts +107 -0
  289. package/src/cli.ts +33 -6
  290. package/src/config.ts +73 -0
  291. package/src/constants.ts +12 -0
  292. package/src/graphql/resolvers.ts +1010 -0
  293. package/src/graphql/schema.ts +452 -0
  294. package/src/graphql/server.ts +154 -0
  295. package/src/graphql/types.ts +7 -0
  296. package/src/handlers/resource-handlers.ts +186 -0
  297. package/src/index.ts +152 -663
  298. package/src/prompts/index.ts +4 -4
  299. package/src/resources/actors.ts +58 -76
  300. package/src/resources/assets.ts +147 -134
  301. package/src/resources/levels.ts +28 -33
  302. package/src/server/resource-registry.ts +47 -0
  303. package/src/server/tool-registry.ts +354 -0
  304. package/src/server-setup.ts +148 -0
  305. package/src/services/health-monitor.ts +132 -0
  306. package/src/services/metrics-server.ts +142 -0
  307. package/src/tools/actors.ts +417 -322
  308. package/src/tools/animation.ts +671 -461
  309. package/src/tools/assets.ts +353 -289
  310. package/src/tools/audio.ts +323 -766
  311. package/src/tools/base-tool.ts +52 -0
  312. package/src/tools/behavior-tree.ts +45 -0
  313. package/src/tools/blueprint/helpers.ts +189 -0
  314. package/src/tools/blueprint.ts +787 -965
  315. package/src/tools/consolidated-tool-definitions.ts +993 -515
  316. package/src/tools/consolidated-tool-handlers.ts +272 -1139
  317. package/src/tools/debug.ts +292 -187
  318. package/src/tools/dynamic-handler-registry.ts +151 -0
  319. package/src/tools/editor.ts +309 -246
  320. package/src/tools/engine.ts +14 -3
  321. package/src/tools/environment.ts +287 -0
  322. package/src/tools/foliage.ts +314 -379
  323. package/src/tools/handlers/actor-handlers.ts +271 -0
  324. package/src/tools/handlers/animation-handlers.ts +237 -0
  325. package/src/tools/handlers/argument-helper.ts +142 -0
  326. package/src/tools/handlers/asset-handlers.ts +532 -0
  327. package/src/tools/handlers/audio-handlers.ts +194 -0
  328. package/src/tools/handlers/blueprint-handlers.ts +380 -0
  329. package/src/tools/handlers/common-handlers.ts +87 -0
  330. package/src/tools/handlers/editor-handlers.ts +123 -0
  331. package/src/tools/handlers/effect-handlers.ts +220 -0
  332. package/src/tools/handlers/environment-handlers.ts +183 -0
  333. package/src/tools/handlers/graph-handlers.ts +116 -0
  334. package/src/tools/handlers/input-handlers.ts +28 -0
  335. package/src/tools/handlers/inspect-handlers.ts +450 -0
  336. package/src/tools/handlers/level-handlers.ts +252 -0
  337. package/src/tools/handlers/lighting-handlers.ts +147 -0
  338. package/src/tools/handlers/performance-handlers.ts +132 -0
  339. package/src/tools/handlers/pipeline-handlers.ts +127 -0
  340. package/src/tools/handlers/sequence-handlers.ts +415 -0
  341. package/src/tools/handlers/system-handlers.ts +564 -0
  342. package/src/tools/input.ts +101 -0
  343. package/src/tools/introspection.ts +493 -584
  344. package/src/tools/landscape.ts +394 -489
  345. package/src/tools/level.ts +752 -694
  346. package/src/tools/lighting.ts +583 -984
  347. package/src/tools/logs.ts +9 -57
  348. package/src/tools/materials.ts +231 -121
  349. package/src/tools/niagara.ts +293 -168
  350. package/src/tools/performance.ts +320 -168
  351. package/src/tools/physics.ts +268 -613
  352. package/src/tools/property-dictionary.ts +98 -0
  353. package/src/tools/sequence.ts +255 -815
  354. package/src/tools/tool-definition-utils.ts +35 -0
  355. package/src/tools/ui.ts +207 -283
  356. package/src/types/env.ts +0 -10
  357. package/src/types/tool-interfaces.ts +250 -0
  358. package/src/types/tool-types.ts +243 -21
  359. package/src/unreal-bridge.ts +460 -1550
  360. package/src/utils/command-validator.ts +75 -0
  361. package/src/utils/elicitation.ts +10 -7
  362. package/src/utils/error-handler.ts +14 -90
  363. package/src/utils/ini-reader.ts +86 -0
  364. package/src/utils/logger.ts +8 -3
  365. package/src/utils/normalize.ts +60 -0
  366. package/src/utils/response-factory.ts +39 -0
  367. package/src/utils/response-validator.ts +176 -56
  368. package/src/utils/result-helpers.ts +21 -19
  369. package/src/utils/safe-json.ts +14 -11
  370. package/src/utils/unreal-command-queue.ts +152 -0
  371. package/src/utils/validation.ts +4 -1
  372. package/src/wasm/index.ts +838 -0
  373. package/test-server.mjs +100 -0
  374. package/tests/run-unreal-tool-tests.mjs +242 -14
  375. package/tests/test-animation.mjs +44 -0
  376. package/tests/test-asset-advanced.mjs +82 -0
  377. package/tests/test-asset-errors.mjs +35 -0
  378. package/tests/test-audio.mjs +219 -0
  379. package/tests/test-automation-timeouts.mjs +98 -0
  380. package/tests/test-behavior-tree.mjs +261 -0
  381. package/tests/test-blueprint-events.mjs +35 -0
  382. package/tests/test-blueprint-graph.mjs +79 -0
  383. package/tests/test-blueprint.mjs +577 -0
  384. package/tests/test-client-mode.mjs +86 -0
  385. package/tests/test-console-command.mjs +56 -0
  386. package/tests/test-control-actor.mjs +425 -0
  387. package/tests/test-control-editor.mjs +80 -0
  388. package/tests/test-extra-tools.mjs +38 -0
  389. package/tests/test-graphql.mjs +322 -0
  390. package/tests/test-inspect.mjs +72 -0
  391. package/tests/test-landscape.mjs +60 -0
  392. package/tests/test-manage-asset.mjs +438 -0
  393. package/tests/test-manage-level.mjs +70 -0
  394. package/tests/test-materials.mjs +356 -0
  395. package/tests/test-niagara.mjs +185 -0
  396. package/tests/test-no-inline-python.mjs +122 -0
  397. package/tests/test-plugin-handshake.mjs +82 -0
  398. package/tests/test-render.mjs +33 -0
  399. package/tests/test-runner.mjs +933 -0
  400. package/tests/test-search-assets.mjs +66 -0
  401. package/tests/test-sequence.mjs +68 -0
  402. package/tests/test-system.mjs +57 -0
  403. package/tests/test-wasm.mjs +193 -0
  404. package/tests/test-world-partition.mjs +215 -0
  405. package/tsconfig.json +3 -3
  406. package/wasm/Cargo.lock +363 -0
  407. package/wasm/Cargo.toml +42 -0
  408. package/wasm/LICENSE +21 -0
  409. package/wasm/README.md +253 -0
  410. package/wasm/src/dependency_resolver.rs +377 -0
  411. package/wasm/src/lib.rs +153 -0
  412. package/wasm/src/property_parser.rs +271 -0
  413. package/wasm/src/transform_math.rs +396 -0
  414. package/wasm/tests/integration.rs +109 -0
  415. package/.github/workflows/smithery-build.yml +0 -29
  416. package/dist/tools/build_environment_advanced.d.ts +0 -65
  417. package/dist/tools/build_environment_advanced.js +0 -633
  418. package/dist/tools/rc.d.ts +0 -110
  419. package/dist/tools/rc.js +0 -437
  420. package/dist/tools/visual.d.ts +0 -40
  421. package/dist/tools/visual.js +0 -282
  422. package/dist/utils/http.d.ts +0 -6
  423. package/dist/utils/http.js +0 -151
  424. package/dist/utils/python-output.d.ts +0 -18
  425. package/dist/utils/python-output.js +0 -290
  426. package/dist/utils/python.d.ts +0 -2
  427. package/dist/utils/python.js +0 -4
  428. package/dist/utils/stdio-redirect.d.ts +0 -2
  429. package/dist/utils/stdio-redirect.js +0 -20
  430. package/docs/unreal-tool-test-cases.md +0 -574
  431. package/smithery.yaml +0 -29
  432. package/src/tools/build_environment_advanced.ts +0 -732
  433. package/src/tools/rc.ts +0 -515
  434. package/src/tools/visual.ts +0 -281
  435. package/src/utils/http.ts +0 -187
  436. package/src/utils/python-output.ts +0 -351
  437. package/src/utils/python.ts +0 -3
  438. package/src/utils/stdio-redirect.ts +0 -18
@@ -1,36 +1,15 @@
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
5
 
6
6
  export class LightingTools {
7
- constructor(private bridge: UnrealBridge) {}
7
+ constructor(private bridge: UnrealBridge, private automationBridge?: AutomationBridge) { }
8
8
 
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;
9
+ setAutomationBridge(automationBridge?: AutomationBridge) { this.automationBridge = automationBridge; }
25
10
 
26
- // Fallback: if no ReturnValue but success-like logs exist, accept
27
- if (/spawned/i.test(logs)) return;
28
11
 
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 {
12
+ private normalizeName(value: unknown, defaultName?: string): string {
34
13
  if (typeof value === 'string') {
35
14
  const trimmed = value.trim();
36
15
  if (trimmed.length > 0) {
@@ -38,14 +17,71 @@ export class LightingTools {
38
17
  }
39
18
  }
40
19
 
41
- if (typeof fallback === 'string') {
42
- const trimmedFallback = fallback.trim();
43
- if (trimmedFallback.length > 0) {
44
- return trimmedFallback;
20
+ if (typeof defaultName === 'string') {
21
+ const trimmedDefault = defaultName.trim();
22
+ if (trimmedDefault.length > 0) {
23
+ return trimmedDefault;
45
24
  }
46
25
  }
47
26
 
48
- throw new Error('Invalid name: must be a non-empty string');
27
+ // Auto-generate if no name is provided
28
+ return `Light_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
29
+ }
30
+
31
+ /**
32
+ * Spawn a light actor using the Automation Bridge.
33
+ * @param lightClass The Unreal light class name (e.g. 'DirectionalLight', 'PointLight')
34
+ * @param params Light spawn parameters
35
+ */
36
+ private async spawnLightViaAutomation(
37
+ lightClass: string,
38
+ params: {
39
+ name: string;
40
+ location?: [number, number, number];
41
+ rotation?: [number, number, number] | { pitch: number, yaw: number, roll: number };
42
+ properties?: Record<string, any>;
43
+ }
44
+ ) {
45
+ if (!this.automationBridge) {
46
+ throw new Error('Automation Bridge not available. Cannot spawn lights without plugin support.');
47
+ }
48
+
49
+ try {
50
+ const payload: Record<string, any> = {
51
+ lightClass,
52
+ name: params.name,
53
+ };
54
+
55
+ if (params.location) {
56
+ payload.location = { x: params.location[0], y: params.location[1], z: params.location[2] };
57
+ }
58
+
59
+ if (params.rotation) {
60
+ if (Array.isArray(params.rotation)) {
61
+ payload.rotation = { pitch: params.rotation[0], yaw: params.rotation[1], roll: params.rotation[2] };
62
+ } else {
63
+ payload.rotation = params.rotation;
64
+ }
65
+ }
66
+
67
+ if (params.properties) {
68
+ payload.properties = params.properties;
69
+ }
70
+
71
+ const response = await this.automationBridge.sendAutomationRequest('spawn_light', payload, {
72
+ timeoutMs: 60000
73
+ });
74
+
75
+ if (response.success === false) {
76
+ throw new Error(response.error || response.message || 'Failed to spawn light');
77
+ }
78
+
79
+ return response;
80
+ } catch (error) {
81
+ throw new Error(
82
+ `Failed to spawn ${lightClass}: ${error instanceof Error ? error.message : String(error)}`
83
+ );
84
+ }
49
85
  }
50
86
 
51
87
  // Create directional light
@@ -53,13 +89,17 @@ export class LightingTools {
53
89
  name: string;
54
90
  intensity?: number;
55
91
  color?: [number, number, number];
56
- rotation?: [number, number, number];
92
+ rotation?: [number, number, number] | { pitch: number, yaw: number, roll: number };
57
93
  castShadows?: boolean;
58
94
  temperature?: number;
95
+ useAsAtmosphereSunLight?: boolean;
96
+ properties?: Record<string, any>;
59
97
  }) {
60
98
  const name = this.normalizeName(params.name);
61
- const escapedName = escapePythonString(name);
62
-
99
+ if (!this.automationBridge) {
100
+ throw new Error('Automation Bridge required for light spawning');
101
+ }
102
+
63
103
  // Validate numeric parameters
64
104
  if (params.intensity !== undefined) {
65
105
  if (typeof params.intensity !== 'number' || !isFinite(params.intensity)) {
@@ -69,13 +109,13 @@ export class LightingTools {
69
109
  throw new Error('Invalid intensity: must be non-negative');
70
110
  }
71
111
  }
72
-
112
+
73
113
  if (params.temperature !== undefined) {
74
114
  if (typeof params.temperature !== 'number' || !isFinite(params.temperature)) {
75
115
  throw new Error(`Invalid temperature value: ${params.temperature}`);
76
116
  }
77
117
  }
78
-
118
+
79
119
  // Validate arrays
80
120
  if (params.color !== undefined) {
81
121
  if (!Array.isArray(params.color) || params.color.length !== 3) {
@@ -87,77 +127,53 @@ export class LightingTools {
87
127
  }
88
128
  }
89
129
  }
90
-
130
+
91
131
  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');
132
+ if (Array.isArray(params.rotation)) {
133
+ if (params.rotation.length !== 3) {
134
+ throw new Error('Invalid rotation: must be an array [pitch,yaw,roll]');
135
+ }
136
+ for (const r of params.rotation) {
137
+ if (typeof r !== 'number' || !isFinite(r)) {
138
+ throw new Error('Invalid rotation component: must be finite numbers');
139
+ }
98
140
  }
99
141
  }
100
142
  }
101
-
143
+
102
144
  const rot = params.rotation || [0, 0, 0];
103
-
104
- // Build property setters
105
- const propSetters: string[] = [];
145
+
146
+ // Build properties for the light
147
+ const properties: Record<string, any> = params.properties || {};
106
148
  if (params.intensity !== undefined) {
107
- propSetters.push(` light_component.set_intensity(${params.intensity})`);
149
+ properties.intensity = params.intensity;
108
150
  }
109
151
  if (params.color) {
110
- propSetters.push(` light_component.set_light_color(unreal.LinearColor(${params.color[0]}, ${params.color[1]}, ${params.color[2]}, 1.0))`);
152
+ properties.color = { r: params.color[0], g: params.color[1], b: params.color[2], a: 1.0 };
111
153
  }
112
154
  if (params.castShadows !== undefined) {
113
- propSetters.push(` light_component.set_cast_shadows(${params.castShadows ? 'True' : 'False'})`);
155
+ properties.castShadows = params.castShadows;
114
156
  }
115
157
  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` };
158
+ properties.temperature = params.temperature;
159
+ }
160
+ if (params.useAsAtmosphereSunLight !== undefined) {
161
+ properties.useAsAtmosphereSunLight = params.useAsAtmosphereSunLight;
162
+ }
163
+
164
+ try {
165
+ await this.spawnLightViaAutomation('DirectionalLight', {
166
+ name,
167
+ location: [0, 0, 500],
168
+ rotation: rot,
169
+ properties
170
+ });
171
+
172
+ return { success: true, message: `Directional light '${name}' spawned` };
173
+ } catch (e: any) {
174
+ // Don't mask errors as "not implemented" - report the actual error from the bridge
175
+ return { success: false, error: `Failed to create directional light: ${e?.message ?? e}` } as any;
176
+ }
161
177
  }
162
178
 
163
179
  // Create point light
@@ -169,25 +185,27 @@ else:
169
185
  color?: [number, number, number];
170
186
  falloffExponent?: number;
171
187
  castShadows?: boolean;
188
+ rotation?: [number, number, number] | { pitch: number, yaw: number, roll: number };
172
189
  }) {
173
190
  const name = this.normalizeName(params.name);
174
- const escapedName = escapePythonString(name);
175
-
191
+ if (!this.automationBridge) {
192
+ throw new Error('Automation Bridge required for light spawning');
193
+ }
194
+
195
+ // Validate location array
176
196
  // Validate location array
177
197
  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
- }
198
+ // Ensure location is valid array [x,y,z]
199
+ try {
200
+ params.location = ensureVector3(params.location, 'location');
201
+ } catch (e) {
202
+ throw new Error(`Invalid location: ${e instanceof Error ? e.message : String(e)}`);
185
203
  }
186
204
  }
187
-
205
+
188
206
  // Default location if not provided
189
207
  const location = params.location || [0, 0, 0];
190
-
208
+
191
209
  // Validate numeric parameters
192
210
  if (params.intensity !== undefined) {
193
211
  if (typeof params.intensity !== 'number' || !isFinite(params.intensity)) {
@@ -210,7 +228,7 @@ else:
210
228
  throw new Error(`Invalid falloffExponent value: ${params.falloffExponent}`);
211
229
  }
212
230
  }
213
-
231
+
214
232
  // Validate color array
215
233
  if (params.color !== undefined) {
216
234
  if (!Array.isArray(params.color) || params.color.length !== 3) {
@@ -222,74 +240,45 @@ else:
222
240
  }
223
241
  }
224
242
  }
225
-
226
- // Build property setters
227
- const propSetters: string[] = [];
243
+
244
+ // Build properties for the light
245
+ const properties: Record<string, any> = {};
228
246
  if (params.intensity !== undefined) {
229
- propSetters.push(` light_component.set_intensity(${params.intensity})`);
247
+ properties.intensity = params.intensity;
230
248
  }
231
249
  if (params.radius !== undefined) {
232
- propSetters.push(` light_component.set_attenuation_radius(${params.radius})`);
250
+ properties.attenuationRadius = params.radius;
233
251
  }
234
252
  if (params.color) {
235
- propSetters.push(` light_component.set_light_color(unreal.LinearColor(${params.color[0]}, ${params.color[1]}, ${params.color[2]}, 1.0))`);
253
+ properties.color = { r: params.color[0], g: params.color[1], b: params.color[2], a: 1.0 };
236
254
  }
237
255
  if (params.castShadows !== undefined) {
238
- propSetters.push(` light_component.set_cast_shadows(${params.castShadows ? 'True' : 'False'})`);
256
+ properties.castShadows = params.castShadows;
239
257
  }
240
258
  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(', ')}` };
259
+ properties.lightFalloffExponent = params.falloffExponent;
260
+ }
261
+
262
+ try {
263
+ await this.spawnLightViaAutomation('PointLight', {
264
+ name,
265
+ location,
266
+ rotation: params.rotation,
267
+ properties
268
+ });
269
+
270
+ return { success: true, message: `Point light '${name}' spawned at ${location.join(', ')}` };
271
+ } catch (e: any) {
272
+ // Don't mask errors as "not implemented" - report the actual error from the bridge
273
+ return { success: false, error: `Failed to create point light: ${e?.message ?? e}` } as any;
274
+ }
286
275
  }
287
276
 
288
277
  // Create spot light
289
278
  async createSpotLight(params: {
290
279
  name: string;
291
280
  location: [number, number, number];
292
- rotation: [number, number, number];
281
+ rotation: [number, number, number] | { pitch: number, yaw: number, roll: number };
293
282
  intensity?: number;
294
283
  innerCone?: number;
295
284
  outerCone?: number;
@@ -298,8 +287,10 @@ else:
298
287
  castShadows?: boolean;
299
288
  }) {
300
289
  const name = this.normalizeName(params.name);
301
- const escapedName = escapePythonString(name);
302
-
290
+ if (!this.automationBridge) {
291
+ throw new Error('Automation Bridge required for light spawning');
292
+ }
293
+
303
294
  // Validate required location and rotation arrays
304
295
  if (!params.location || !Array.isArray(params.location) || params.location.length !== 3) {
305
296
  throw new Error('Invalid location: must be an array [x,y,z]');
@@ -309,16 +300,21 @@ else:
309
300
  throw new Error('Invalid location component: must be finite numbers');
310
301
  }
311
302
  }
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]');
303
+
304
+ if (!params.rotation) {
305
+ throw new Error('Rotation is required');
315
306
  }
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');
307
+ if (Array.isArray(params.rotation)) {
308
+ if (params.rotation.length !== 3) {
309
+ throw new Error('Invalid rotation: must be an array [pitch,yaw,roll]');
310
+ }
311
+ for (const r of params.rotation) {
312
+ if (typeof r !== 'number' || !isFinite(r)) {
313
+ throw new Error('Invalid rotation component: must be finite numbers');
314
+ }
319
315
  }
320
316
  }
321
-
317
+
322
318
  // Validate optional numeric parameters
323
319
  if (params.intensity !== undefined) {
324
320
  if (typeof params.intensity !== 'number' || !isFinite(params.intensity)) {
@@ -328,7 +324,7 @@ else:
328
324
  throw new Error('Invalid intensity: must be non-negative');
329
325
  }
330
326
  }
331
-
327
+
332
328
  if (params.innerCone !== undefined) {
333
329
  if (typeof params.innerCone !== 'number' || !isFinite(params.innerCone)) {
334
330
  throw new Error(`Invalid innerCone value: ${params.innerCone}`);
@@ -337,7 +333,7 @@ else:
337
333
  throw new Error('Invalid innerCone: must be between 0 and 180 degrees');
338
334
  }
339
335
  }
340
-
336
+
341
337
  if (params.outerCone !== undefined) {
342
338
  if (typeof params.outerCone !== 'number' || !isFinite(params.outerCone)) {
343
339
  throw new Error(`Invalid outerCone value: ${params.outerCone}`);
@@ -346,7 +342,7 @@ else:
346
342
  throw new Error('Invalid outerCone: must be between 0 and 180 degrees');
347
343
  }
348
344
  }
349
-
345
+
350
346
  if (params.radius !== undefined) {
351
347
  if (typeof params.radius !== 'number' || !isFinite(params.radius)) {
352
348
  throw new Error(`Invalid radius value: ${params.radius}`);
@@ -355,7 +351,7 @@ else:
355
351
  throw new Error('Invalid radius: must be non-negative');
356
352
  }
357
353
  }
358
-
354
+
359
355
  // Validate color array
360
356
  if (params.color !== undefined) {
361
357
  if (!Array.isArray(params.color) || params.color.length !== 3) {
@@ -367,83 +363,58 @@ else:
367
363
  }
368
364
  }
369
365
  }
370
- // Build property setters
371
- const propSetters: string[] = [];
366
+ // Build properties for the light
367
+ const properties: Record<string, any> = {};
372
368
  if (params.intensity !== undefined) {
373
- propSetters.push(` light_component.set_intensity(${params.intensity})`);
369
+ properties.intensity = params.intensity;
374
370
  }
375
371
  if (params.innerCone !== undefined) {
376
- propSetters.push(` light_component.set_inner_cone_angle(${params.innerCone})`);
372
+ properties.innerConeAngle = params.innerCone;
377
373
  }
378
374
  if (params.outerCone !== undefined) {
379
- propSetters.push(` light_component.set_outer_cone_angle(${params.outerCone})`);
375
+ properties.outerConeAngle = params.outerCone;
380
376
  }
381
377
  if (params.radius !== undefined) {
382
- propSetters.push(` light_component.set_attenuation_radius(${params.radius})`);
378
+ properties.attenuationRadius = params.radius;
383
379
  }
384
380
  if (params.color) {
385
- propSetters.push(` light_component.set_light_color(unreal.LinearColor(${params.color[0]}, ${params.color[1]}, ${params.color[2]}, 1.0))`);
381
+ properties.color = { r: params.color[0], g: params.color[1], b: params.color[2], a: 1.0 };
386
382
  }
387
383
  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(', ')}` };
384
+ properties.castShadows = params.castShadows;
385
+ }
386
+
387
+ try {
388
+ await this.spawnLightViaAutomation('SpotLight', {
389
+ name,
390
+ location: params.location,
391
+ rotation: params.rotation,
392
+ properties
393
+ });
394
+
395
+ return { success: true, message: `Spot light '${name}' spawned at ${params.location.join(', ')}` };
396
+ } catch (e: any) {
397
+ // Don't mask errors as "not implemented" - report the actual error from the bridge
398
+ return { success: false, error: `Failed to create spot light: ${e?.message ?? e}` } as any;
399
+ }
433
400
  }
434
401
 
435
402
  // Create rect light
436
403
  async createRectLight(params: {
437
404
  name: string;
438
405
  location: [number, number, number];
439
- rotation: [number, number, number];
406
+ rotation: [number, number, number] | { pitch: number, yaw: number, roll: number };
440
407
  width?: number;
441
408
  height?: number;
442
409
  intensity?: number;
443
410
  color?: [number, number, number];
411
+ castShadows?: boolean;
444
412
  }) {
413
+
445
414
  const name = this.normalizeName(params.name);
446
- const escapedName = escapePythonString(name);
415
+ if (!this.automationBridge) {
416
+ throw new Error('Automation Bridge required for light spawning');
417
+ }
447
418
 
448
419
  // Validate required location and rotation arrays
449
420
  if (!params.location || !Array.isArray(params.location) || params.location.length !== 3) {
@@ -454,16 +425,21 @@ else:
454
425
  throw new Error('Invalid location component: must be finite numbers');
455
426
  }
456
427
  }
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]');
428
+
429
+ if (!params.rotation) {
430
+ throw new Error('Rotation is required');
460
431
  }
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');
432
+ if (Array.isArray(params.rotation)) {
433
+ if (params.rotation.length !== 3) {
434
+ throw new Error('Invalid rotation: must be an array [pitch,yaw,roll]');
435
+ }
436
+ for (const r of params.rotation) {
437
+ if (typeof r !== 'number' || !isFinite(r)) {
438
+ throw new Error('Invalid rotation component: must be finite numbers');
439
+ }
464
440
  }
465
441
  }
466
-
442
+
467
443
  // Validate optional numeric parameters
468
444
  if (params.width !== undefined) {
469
445
  if (typeof params.width !== 'number' || !isFinite(params.width)) {
@@ -473,7 +449,7 @@ else:
473
449
  throw new Error('Invalid width: must be positive');
474
450
  }
475
451
  }
476
-
452
+
477
453
  if (params.height !== undefined) {
478
454
  if (typeof params.height !== 'number' || !isFinite(params.height)) {
479
455
  throw new Error(`Invalid height value: ${params.height}`);
@@ -482,7 +458,7 @@ else:
482
458
  throw new Error('Invalid height: must be positive');
483
459
  }
484
460
  }
485
-
461
+
486
462
  if (params.intensity !== undefined) {
487
463
  if (typeof params.intensity !== 'number' || !isFinite(params.intensity)) {
488
464
  throw new Error(`Invalid intensity value: ${params.intensity}`);
@@ -491,7 +467,7 @@ else:
491
467
  throw new Error('Invalid intensity: must be non-negative');
492
468
  }
493
469
  }
494
-
470
+
495
471
  // Validate color array
496
472
  if (params.color !== undefined) {
497
473
  if (!Array.isArray(params.color) || params.color.length !== 3) {
@@ -503,63 +479,77 @@ else:
503
479
  }
504
480
  }
505
481
  }
506
- // Build property setters
507
- const propSetters: string[] = [];
482
+ // Build properties for the light
483
+ const properties: Record<string, any> = {};
508
484
  if (params.intensity !== undefined) {
509
- propSetters.push(` light_component.set_intensity(${params.intensity})`);
485
+ properties.intensity = params.intensity;
510
486
  }
511
487
  if (params.color) {
512
- propSetters.push(` light_component.set_light_color(unreal.LinearColor(${params.color[0]}, ${params.color[1]}, ${params.color[2]}, 1.0))`);
488
+ properties.color = { r: params.color[0], g: params.color[1], b: params.color[2], a: 1.0 };
513
489
  }
514
490
  if (params.width !== undefined) {
515
- propSetters.push(` light_component.set_source_width(${params.width})`);
491
+ properties.sourceWidth = params.width;
516
492
  }
517
493
  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(', ')}` };
494
+ properties.sourceHeight = params.height;
495
+ }
496
+
497
+ try {
498
+ await this.spawnLightViaAutomation('RectLight', {
499
+ name,
500
+ location: params.location,
501
+ rotation: params.rotation,
502
+ properties
503
+ });
504
+
505
+ return { success: true, message: `Rect light '${name}' spawned at ${params.location.join(', ')}` };
506
+ } catch (e: any) {
507
+ // Don't mask errors as "not implemented" - report the actual error from the bridge
508
+ return { success: false, error: `Failed to create rect light: ${e?.message ?? e}` } as any;
509
+ }
510
+ }
511
+
512
+ /**
513
+ * Create dynamic light
514
+ */
515
+ async createDynamicLight(params: {
516
+ name?: string;
517
+ lightType?: 'Point' | 'Spot' | 'Directional' | 'Rect' | string;
518
+ location?: [number, number, number] | { x: number; y: number; z: number };
519
+ rotation?: [number, number, number];
520
+ intensity?: number;
521
+ color?: [number, number, number, number] | { r: number; g: number; b: number; a?: number };
522
+ pulse?: { enabled?: boolean; frequency?: number };
523
+ }) {
524
+ try {
525
+ const name = typeof params.name === 'string' && params.name.trim().length > 0 ? params.name.trim() : `DynamicLight_${Date.now() % 10000}`;
526
+ const lightTypeRaw = typeof params.lightType === 'string' && params.lightType.trim().length > 0 ? params.lightType.trim() : 'Point';
527
+ 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 });
528
+
529
+ // C++ plugin does not strictly implement 'create_dynamic_light' action; it supports 'spawn_light'.
530
+ // However, we rely on the specific helper methods below which correctly map to 'spawn_light'
531
+ // with the appropriate class and properties.
532
+
533
+ const toArray3 = (loc: any): [number, number, number] => Array.isArray(loc)
534
+ ? [Number(loc[0]) || 0, Number(loc[1]) || 0, Number(loc[2]) || 0]
535
+ : [Number(loc?.x) || 0, Number(loc?.y) || 0, Number(loc?.z) || 0];
536
+ const locArr = toArray3(location);
537
+ const typeNorm = (lightTypeRaw || 'Point').toLowerCase();
538
+
539
+ switch (typeNorm) {
540
+ case 'directional': case 'directionallight':
541
+ 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 });
542
+ case 'spot': case 'spotlight':
543
+ 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) });
544
+ case 'rect': case 'rectlight':
545
+ 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) });
546
+ case 'point': default:
547
+ 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 });
548
+ }
549
+
550
+ } catch (err) {
551
+ return { success: false, error: `Failed to create dynamic light: ${err}` };
552
+ }
563
553
  }
564
554
 
565
555
  // Create sky light
@@ -569,210 +559,107 @@ else:
569
559
  cubemapPath?: string;
570
560
  intensity?: number;
571
561
  recapture?: boolean;
562
+ location?: [number, number, number];
563
+ rotation?: [number, number, number] | { pitch: number, yaw: number, roll: number };
564
+ realTimeCapture?: boolean;
565
+ castShadows?: boolean;
566
+ color?: [number, number, number];
572
567
  }) {
573
568
  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)) {
569
+ if (params.sourceType === 'SpecifiedCubemap' && (!params.cubemapPath || params.cubemapPath.trim().length === 0)) {
586
570
  const message = 'cubemapPath is required when sourceType is SpecifiedCubemap';
587
571
  return { success: false, error: message, message };
588
572
  }
589
- const escapedCubemapPath = cubemapPath ? escapePythonString(cubemapPath) : '';
590
- const python = `
591
- import unreal
592
- import json
593
573
 
594
- result = {
595
- "success": False,
596
- "message": "",
597
- "error": "",
598
- "warnings": []
599
- }
574
+ if (!this.automationBridge) {
575
+ throw new Error('Automation Bridge required for sky light creation');
576
+ }
577
+
578
+ try {
579
+ const properties: Record<string, any> = {};
580
+ if (params.intensity !== undefined) properties.Intensity = params.intensity;
581
+ if (params.castShadows !== undefined) properties.CastShadows = params.castShadows;
582
+ if (params.realTimeCapture !== undefined) properties.RealTimeCapture = params.realTimeCapture;
583
+ if (params.color) properties.LightColor = { r: params.color[0], g: params.color[1], b: params.color[2], a: 1.0 };
584
+
585
+ const payload: Record<string, any> = {
586
+ name,
587
+ sourceType: params.sourceType || 'CapturedScene',
588
+ location: params.location,
589
+ rotation: params.rotation,
590
+ properties
591
+ };
592
+
593
+ if (params.cubemapPath) {
594
+ payload.cubemapPath = params.cubemapPath;
595
+ }
596
+ if (params.intensity !== undefined) {
597
+ payload.intensity = params.intensity;
598
+ }
599
+ if (params.recapture) {
600
+ payload.recapture = params.recapture;
601
+ }
602
+
603
+ const response = await this.automationBridge.sendAutomationRequest('spawn_sky_light', payload, {
604
+ timeoutMs: 60000
605
+ });
606
+
607
+ if (response.success === false) {
608
+ return {
609
+ success: false,
610
+ error: response.error || response.message || 'Failed to create sky light'
611
+ };
612
+ }
600
613
 
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
614
  return {
735
615
  success: true,
736
- message: parsed.message ?? 'Sky light ensured',
737
- warnings: Array.isArray(parsed.warnings) && parsed.warnings.length > 0 ? parsed.warnings : undefined
616
+ message: response.message || 'Sky light created',
617
+ ...(response.result || {})
618
+ };
619
+ } catch (error) {
620
+ return {
621
+ success: false,
622
+ error: `Failed to create sky light: ${error instanceof Error ? error.message : String(error)}`
738
623
  };
739
624
  }
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
625
  }
748
626
 
749
627
  // Remove duplicate SkyLights and keep only one (named target label)
750
628
  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);
629
+ const defaultName = 'MCP_Test_Sky';
630
+ const name = this.normalizeName(params?.name, defaultName);
754
631
  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 {}
632
+
633
+ if (!this.automationBridge) {
634
+ throw new Error('Automation Bridge required for sky light management');
635
+ }
636
+
637
+ try {
638
+ const response = await this.automationBridge.sendAutomationRequest('ensure_single_sky_light', {
639
+ name,
640
+ recapture
641
+ }, {
642
+ timeoutMs: 60000
643
+ });
644
+
645
+ if (response.success === false) {
646
+ return {
647
+ success: false,
648
+ error: response.error || response.message || 'Failed to ensure single sky light'
649
+ };
650
+ }
651
+
652
+ return {
653
+ success: true,
654
+ message: response.message || `Ensured single SkyLight (removed ${(response.result as any)?.removed || 0})`,
655
+ ...(response.result || {})
656
+ };
657
+ } catch (error) {
658
+ return {
659
+ success: false,
660
+ error: `Failed to ensure single sky light: ${error instanceof Error ? error.message : String(error)}`
661
+ };
774
662
  }
775
- return { success: true, message: 'Ensured single SkyLight' };
776
663
  }
777
664
 
778
665
  // Setup global illumination
@@ -782,8 +669,22 @@ except Exception as run_err:
782
669
  indirectLightingIntensity?: number;
783
670
  bounces?: number;
784
671
  }) {
672
+ if (this.automationBridge) {
673
+ try {
674
+ const response = await this.automationBridge.sendAutomationRequest('setup_global_illumination', {
675
+ method: params.method,
676
+ quality: params.quality,
677
+ indirectLightingIntensity: params.indirectLightingIntensity,
678
+ bounces: params.bounces
679
+ });
680
+ if (response.success) return { success: true, message: 'Global illumination configured via bridge', ...(response.result || {}) };
681
+ } catch (_e) {
682
+ // Fallback to console commands
683
+ }
684
+ }
685
+
785
686
  const commands = [];
786
-
687
+
787
688
  switch (params.method) {
788
689
  case 'Lightmass':
789
690
  commands.push('r.DynamicGlobalIlluminationMethod 0');
@@ -798,25 +699,25 @@ except Exception as run_err:
798
699
  commands.push('r.DynamicGlobalIlluminationMethod 3');
799
700
  break;
800
701
  }
801
-
702
+
802
703
  if (params.quality) {
803
704
  const qualityMap = { 'Low': 0, 'Medium': 1, 'High': 2, 'Epic': 3 };
804
705
  commands.push(`r.Lumen.Quality ${qualityMap[params.quality]}`);
805
706
  }
806
-
707
+
807
708
  if (params.indirectLightingIntensity !== undefined) {
808
709
  commands.push(`r.IndirectLightingIntensity ${params.indirectLightingIntensity}`);
809
710
  }
810
-
711
+
811
712
  if (params.bounces !== undefined) {
812
713
  commands.push(`r.Lumen.MaxReflectionBounces ${params.bounces}`);
813
714
  }
814
-
715
+
815
716
  for (const cmd of commands) {
816
717
  await this.bridge.executeConsoleCommand(cmd);
817
718
  }
818
-
819
- return { success: true, message: 'Global illumination configured' };
719
+
720
+ return { success: true, message: 'Global illumination configured (console)' };
820
721
  }
821
722
 
822
723
  // Configure shadows
@@ -827,529 +728,174 @@ except Exception as run_err:
827
728
  contactShadows?: boolean;
828
729
  rayTracedShadows?: boolean;
829
730
  }) {
731
+ if (this.automationBridge) {
732
+ try {
733
+ const response = await this.automationBridge.sendAutomationRequest('configure_shadows', {
734
+ shadowQuality: params.shadowQuality,
735
+ cascadedShadows: params.cascadedShadows,
736
+ shadowDistance: params.shadowDistance,
737
+ contactShadows: params.contactShadows,
738
+ rayTracedShadows: params.rayTracedShadows,
739
+ virtualShadowMaps: params.rayTracedShadows // Map to VSM for C++ handler
740
+ });
741
+ if (response.success) return { success: true, message: 'Shadow settings configured via bridge', ...(response.result || {}) };
742
+ } catch (_e) {
743
+ // Fallback
744
+ }
745
+ }
746
+
830
747
  const commands = [];
831
-
748
+
832
749
  if (params.shadowQuality) {
833
750
  const qualityMap = { 'Low': 0, 'Medium': 1, 'High': 2, 'Epic': 3 };
834
751
  commands.push(`r.ShadowQuality ${qualityMap[params.shadowQuality]}`);
835
752
  }
836
-
753
+
837
754
  if (params.cascadedShadows !== undefined) {
838
755
  commands.push(`r.Shadow.CSM.MaxCascades ${params.cascadedShadows ? 4 : 1}`);
839
756
  }
840
-
757
+
841
758
  if (params.shadowDistance !== undefined) {
842
759
  commands.push(`r.Shadow.DistanceScale ${params.shadowDistance}`);
843
760
  }
844
-
761
+
845
762
  if (params.contactShadows !== undefined) {
846
763
  commands.push(`r.ContactShadows ${params.contactShadows ? 1 : 0}`);
847
764
  }
848
-
765
+
849
766
  if (params.rayTracedShadows !== undefined) {
850
767
  commands.push(`r.RayTracing.Shadows ${params.rayTracedShadows ? 1 : 0}`);
851
768
  }
852
-
769
+
853
770
  for (const cmd of commands) {
854
771
  await this.bridge.executeConsoleCommand(cmd);
855
772
  }
856
-
857
- return { success: true, message: 'Shadow settings configured' };
773
+
774
+ return { success: true, message: 'Shadow settings configured (console)' };
858
775
  }
859
776
 
860
- // Build lighting (Python-based)
777
+ // Build lighting
861
778
  async buildLighting(params: {
862
779
  quality?: 'Preview' | 'Medium' | 'High' | 'Production';
863
- buildOnlySelected?: boolean; // ignored in Python path
780
+ buildOnlySelected?: boolean;
864
781
  buildReflectionCaptures?: boolean;
782
+ levelPath?: string;
865
783
  }) {
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);
784
+ if (!this.automationBridge) {
785
+ throw new Error('Automation Bridge required for lighting build');
786
+ }
787
+
1016
788
  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 {}
789
+ const response = await this.automationBridge.sendAutomationRequest('bake_lightmap', {
790
+ quality: params.quality || 'High',
791
+ buildOnlySelected: params.buildOnlySelected || false,
792
+ buildReflectionCaptures: params.buildReflectionCaptures !== false,
793
+ levelPath: params.levelPath
794
+ }, {
795
+ timeoutMs: 300000 // 5 minutes for lighting builds
796
+ });
797
+
798
+ if (response.success === false) {
799
+ return {
800
+ success: false,
801
+ error: response.error || response.message || 'Failed to build lighting'
802
+ };
1035
803
  }
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' };
804
+
805
+ return {
806
+ success: true,
807
+ message: response.message || 'Lighting build started',
808
+ ...(response.result || {})
809
+ } as any;
810
+ } catch (error) {
811
+ return {
812
+ success: false,
813
+ error: `Failed to build lighting: ${error instanceof Error ? error.message : String(error)}`
814
+ } as any;
815
+ }
1068
816
  }
1069
817
 
1070
- // Create a new level with proper lighting settings as workaround
818
+ // Create a new level with proper lighting settings
1071
819
  async createLightingEnabledLevel(params?: {
1072
820
  levelName?: string;
1073
821
  copyActors?: boolean;
1074
822
  useTemplate?: boolean;
1075
823
  }) {
1076
824
  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
-
825
+
826
+ if (!this.automationBridge) {
827
+ throw new Error('Automation Bridge not available. Level creation requires plugin support.');
828
+ }
829
+
830
+ try {
831
+ const response = await this.automationBridge.sendAutomationRequest('create_lighting_enabled_level', {
832
+ levelName,
833
+ copyActors: params?.copyActors === true,
834
+ useTemplate: params?.useTemplate === true,
835
+ path: params?.levelName ? `/Game/Maps/${params.levelName}` : undefined // Ensure path is sent
836
+ }, {
837
+ timeoutMs: 120000 // 2 minutes for level creation
838
+ });
839
+
840
+ if (response.success === false) {
1296
841
  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' };
842
+ success: false,
843
+ error: response.error || response.message || 'Failed to create level'
844
+ };
845
+ }
846
+
847
+ return {
848
+ success: true,
849
+ message: response.message || `Created new level "${levelName}" with lighting enabled`,
850
+ ...(response.result || {})
851
+ };
852
+ } catch (error) {
853
+ return {
854
+ success: false,
855
+ error: `Failed to create lighting-enabled level: ${error}`
856
+ };
857
+ }
1319
858
  }
1320
859
 
1321
- // Create lightmass importance volume via Python
860
+ // Create lightmass importance volume
1322
861
  async createLightmassVolume(params: {
1323
862
  name: string;
1324
863
  location: [number, number, number];
1325
864
  size: [number, number, number];
1326
865
  }) {
1327
866
  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' };
867
+
868
+ if (!this.automationBridge) {
869
+ throw new Error('Automation Bridge not available. Lightmass volume creation requires plugin support.');
870
+ }
871
+
872
+ try {
873
+ const response = await this.automationBridge.sendAutomationRequest('create_lightmass_volume', {
874
+ name,
875
+ location: { x: params.location[0], y: params.location[1], z: params.location[2] },
876
+ size: { x: params.size[0], y: params.size[1], z: params.size[2] }
877
+ }, {
878
+ timeoutMs: 60000
879
+ });
880
+
881
+ if (response.success === false) {
882
+ return {
883
+ success: false,
884
+ error: response.error || response.message || 'Failed to create lightmass volume'
885
+ };
886
+ }
887
+
888
+ return {
889
+ success: true,
890
+ message: `LightmassImportanceVolume '${name}' created`,
891
+ ...(response.result || {})
892
+ };
893
+ } catch (error) {
894
+ return {
895
+ success: false,
896
+ error: `Failed to create lightmass volume: ${error}`
897
+ };
898
+ }
1353
899
  }
1354
900
 
1355
901
  // Set exposure
@@ -1359,27 +905,41 @@ else:
1359
905
  minBrightness?: number;
1360
906
  maxBrightness?: number;
1361
907
  }) {
908
+ if (this.automationBridge) {
909
+ try {
910
+ const response = await this.automationBridge.sendAutomationRequest('set_exposure', {
911
+ method: params.method,
912
+ compensationValue: params.compensationValue,
913
+ minBrightness: params.minBrightness,
914
+ maxBrightness: params.maxBrightness
915
+ });
916
+ if (response.success) return { success: true, message: 'Exposure settings updated via bridge', ...(response.result || {}) };
917
+ } catch (_e) {
918
+ // Fallback
919
+ }
920
+ }
921
+
1362
922
  const commands = [];
1363
-
923
+
1364
924
  commands.push(`r.EyeAdaptation.ExposureMethod ${params.method === 'Manual' ? 0 : 1}`);
1365
-
925
+
1366
926
  if (params.compensationValue !== undefined) {
1367
927
  commands.push(`r.EyeAdaptation.ExposureCompensation ${params.compensationValue}`);
1368
928
  }
1369
-
929
+
1370
930
  if (params.minBrightness !== undefined) {
1371
931
  commands.push(`r.EyeAdaptation.MinBrightness ${params.minBrightness}`);
1372
932
  }
1373
-
933
+
1374
934
  if (params.maxBrightness !== undefined) {
1375
935
  commands.push(`r.EyeAdaptation.MaxBrightness ${params.maxBrightness}`);
1376
936
  }
1377
-
937
+
1378
938
  for (const cmd of commands) {
1379
939
  await this.bridge.executeConsoleCommand(cmd);
1380
940
  }
1381
-
1382
- return { success: true, message: 'Exposure settings updated' };
941
+
942
+ return { success: true, message: 'Exposure settings updated (console)' };
1383
943
  }
1384
944
 
1385
945
  // Set ambient occlusion
@@ -1389,31 +949,45 @@ else:
1389
949
  radius?: number;
1390
950
  quality?: 'Low' | 'Medium' | 'High';
1391
951
  }) {
952
+ if (this.automationBridge) {
953
+ try {
954
+ const response = await this.automationBridge.sendAutomationRequest('set_ambient_occlusion', {
955
+ enabled: params.enabled,
956
+ intensity: params.intensity,
957
+ radius: params.radius,
958
+ quality: params.quality
959
+ });
960
+ if (response.success) return { success: true, message: 'Ambient occlusion configured via bridge', ...(response.result || {}) };
961
+ } catch (_e) {
962
+ // Fallback
963
+ }
964
+ }
965
+
1392
966
  const commands = [];
1393
-
967
+
1394
968
  commands.push(`r.AmbientOcclusion.Enabled ${params.enabled ? 1 : 0}`);
1395
-
969
+
1396
970
  if (params.intensity !== undefined) {
1397
971
  commands.push(`r.AmbientOcclusion.Intensity ${params.intensity}`);
1398
972
  }
1399
-
973
+
1400
974
  if (params.radius !== undefined) {
1401
975
  commands.push(`r.AmbientOcclusion.Radius ${params.radius}`);
1402
976
  }
1403
-
977
+
1404
978
  if (params.quality) {
1405
979
  const qualityMap = { 'Low': 0, 'Medium': 1, 'High': 2 };
1406
980
  commands.push(`r.AmbientOcclusion.Quality ${qualityMap[params.quality]}`);
1407
981
  }
1408
-
982
+
1409
983
  for (const cmd of commands) {
1410
984
  await this.bridge.executeConsoleCommand(cmd);
1411
985
  }
1412
-
1413
- return { success: true, message: 'Ambient occlusion configured' };
986
+
987
+ return { success: true, message: 'Ambient occlusion configured (console)' };
1414
988
  }
1415
989
 
1416
- // Setup volumetric fog (prefer Python to adjust fog actor/component)
990
+ // Setup volumetric fog
1417
991
  async setupVolumetricFog(params: {
1418
992
  enabled: boolean;
1419
993
  density?: number;
@@ -1423,15 +997,40 @@ else:
1423
997
  // Enable/disable global volumetric fog via CVar
1424
998
  await this.bridge.executeConsoleCommand(`r.VolumetricFog ${params.enabled ? 1 : 0}`);
1425
999
 
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();
1000
+ if (!this.automationBridge) {
1001
+ return {
1002
+ success: true,
1003
+ message: 'Volumetric fog console setting applied (plugin required for fog actor adjustment)'
1004
+ };
1005
+ }
1006
+
1007
+ try {
1008
+ const response = await this.automationBridge.sendAutomationRequest('setup_volumetric_fog', {
1009
+ enabled: params.enabled,
1010
+ density: params.density,
1011
+ scatteringIntensity: params.scatteringIntensity,
1012
+ fogHeight: params.fogHeight
1013
+ }, {
1014
+ timeoutMs: 60000
1015
+ });
1016
+
1017
+ if (response.success === false) {
1018
+ return {
1019
+ success: false,
1020
+ error: response.error || response.message || 'Failed to configure volumetric fog'
1021
+ };
1022
+ }
1430
1023
 
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' };
1024
+ return {
1025
+ success: true,
1026
+ message: 'Volumetric fog configured',
1027
+ ...(response.result || {})
1028
+ };
1029
+ } catch (error) {
1030
+ return {
1031
+ success: false,
1032
+ error: `Failed to setup volumetric fog: ${error}`
1033
+ };
1034
+ }
1436
1035
  }
1437
1036
  }