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