unreal-engine-mcp-server 0.2.1

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