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.
- package/.dockerignore +57 -0
- package/.env.production +25 -0
- package/.eslintrc.json +54 -0
- package/.github/workflows/publish-mcp.yml +75 -0
- package/Dockerfile +54 -0
- package/LICENSE +21 -0
- package/Public/icon.png +0 -0
- package/README.md +209 -0
- package/claude_desktop_config_example.json +13 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +7 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +484 -0
- package/dist/prompts/index.d.ts +14 -0
- package/dist/prompts/index.js +38 -0
- package/dist/python-utils.d.ts +29 -0
- package/dist/python-utils.js +54 -0
- package/dist/resources/actors.d.ts +13 -0
- package/dist/resources/actors.js +83 -0
- package/dist/resources/assets.d.ts +23 -0
- package/dist/resources/assets.js +245 -0
- package/dist/resources/levels.d.ts +17 -0
- package/dist/resources/levels.js +94 -0
- package/dist/tools/actors.d.ts +51 -0
- package/dist/tools/actors.js +459 -0
- package/dist/tools/animation.d.ts +196 -0
- package/dist/tools/animation.js +579 -0
- package/dist/tools/assets.d.ts +21 -0
- package/dist/tools/assets.js +304 -0
- package/dist/tools/audio.d.ts +170 -0
- package/dist/tools/audio.js +416 -0
- package/dist/tools/blueprint.d.ts +144 -0
- package/dist/tools/blueprint.js +652 -0
- package/dist/tools/build_environment_advanced.d.ts +66 -0
- package/dist/tools/build_environment_advanced.js +484 -0
- package/dist/tools/consolidated-tool-definitions.d.ts +2598 -0
- package/dist/tools/consolidated-tool-definitions.js +607 -0
- package/dist/tools/consolidated-tool-handlers.d.ts +2 -0
- package/dist/tools/consolidated-tool-handlers.js +1050 -0
- package/dist/tools/debug.d.ts +185 -0
- package/dist/tools/debug.js +265 -0
- package/dist/tools/editor.d.ts +88 -0
- package/dist/tools/editor.js +365 -0
- package/dist/tools/engine.d.ts +30 -0
- package/dist/tools/engine.js +36 -0
- package/dist/tools/foliage.d.ts +155 -0
- package/dist/tools/foliage.js +525 -0
- package/dist/tools/introspection.d.ts +98 -0
- package/dist/tools/introspection.js +683 -0
- package/dist/tools/landscape.d.ts +158 -0
- package/dist/tools/landscape.js +375 -0
- package/dist/tools/level.d.ts +110 -0
- package/dist/tools/level.js +362 -0
- package/dist/tools/lighting.d.ts +159 -0
- package/dist/tools/lighting.js +1179 -0
- package/dist/tools/materials.d.ts +34 -0
- package/dist/tools/materials.js +146 -0
- package/dist/tools/niagara.d.ts +145 -0
- package/dist/tools/niagara.js +289 -0
- package/dist/tools/performance.d.ts +163 -0
- package/dist/tools/performance.js +412 -0
- package/dist/tools/physics.d.ts +189 -0
- package/dist/tools/physics.js +784 -0
- package/dist/tools/rc.d.ts +110 -0
- package/dist/tools/rc.js +363 -0
- package/dist/tools/sequence.d.ts +112 -0
- package/dist/tools/sequence.js +675 -0
- package/dist/tools/tool-definitions.d.ts +4919 -0
- package/dist/tools/tool-definitions.js +891 -0
- package/dist/tools/tool-handlers.d.ts +47 -0
- package/dist/tools/tool-handlers.js +830 -0
- package/dist/tools/ui.d.ts +171 -0
- package/dist/tools/ui.js +337 -0
- package/dist/tools/visual.d.ts +29 -0
- package/dist/tools/visual.js +67 -0
- package/dist/types/env.d.ts +10 -0
- package/dist/types/env.js +18 -0
- package/dist/types/index.d.ts +323 -0
- package/dist/types/index.js +28 -0
- package/dist/types/tool-types.d.ts +274 -0
- package/dist/types/tool-types.js +13 -0
- package/dist/unreal-bridge.d.ts +126 -0
- package/dist/unreal-bridge.js +992 -0
- package/dist/utils/cache-manager.d.ts +64 -0
- package/dist/utils/cache-manager.js +176 -0
- package/dist/utils/error-handler.d.ts +66 -0
- package/dist/utils/error-handler.js +243 -0
- package/dist/utils/errors.d.ts +133 -0
- package/dist/utils/errors.js +256 -0
- package/dist/utils/http.d.ts +26 -0
- package/dist/utils/http.js +135 -0
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.js +32 -0
- package/dist/utils/normalize.d.ts +17 -0
- package/dist/utils/normalize.js +49 -0
- package/dist/utils/response-validator.d.ts +34 -0
- package/dist/utils/response-validator.js +121 -0
- package/dist/utils/safe-json.d.ts +4 -0
- package/dist/utils/safe-json.js +97 -0
- package/dist/utils/stdio-redirect.d.ts +2 -0
- package/dist/utils/stdio-redirect.js +20 -0
- package/dist/utils/validation.d.ts +50 -0
- package/dist/utils/validation.js +173 -0
- package/mcp-config-example.json +14 -0
- package/package.json +63 -0
- package/server.json +60 -0
- package/src/cli.ts +7 -0
- package/src/index.ts +543 -0
- package/src/prompts/index.ts +51 -0
- package/src/python/editor_compat.py +181 -0
- package/src/python-utils.ts +57 -0
- package/src/resources/actors.ts +92 -0
- package/src/resources/assets.ts +251 -0
- package/src/resources/levels.ts +83 -0
- package/src/tools/actors.ts +480 -0
- package/src/tools/animation.ts +713 -0
- package/src/tools/assets.ts +305 -0
- package/src/tools/audio.ts +548 -0
- package/src/tools/blueprint.ts +736 -0
- package/src/tools/build_environment_advanced.ts +526 -0
- package/src/tools/consolidated-tool-definitions.ts +619 -0
- package/src/tools/consolidated-tool-handlers.ts +1093 -0
- package/src/tools/debug.ts +368 -0
- package/src/tools/editor.ts +360 -0
- package/src/tools/engine.ts +32 -0
- package/src/tools/foliage.ts +652 -0
- package/src/tools/introspection.ts +778 -0
- package/src/tools/landscape.ts +523 -0
- package/src/tools/level.ts +410 -0
- package/src/tools/lighting.ts +1316 -0
- package/src/tools/materials.ts +148 -0
- package/src/tools/niagara.ts +312 -0
- package/src/tools/performance.ts +549 -0
- package/src/tools/physics.ts +924 -0
- package/src/tools/rc.ts +437 -0
- package/src/tools/sequence.ts +791 -0
- package/src/tools/tool-definitions.ts +907 -0
- package/src/tools/tool-handlers.ts +941 -0
- package/src/tools/ui.ts +499 -0
- package/src/tools/visual.ts +60 -0
- package/src/types/env.ts +27 -0
- package/src/types/index.ts +414 -0
- package/src/types/tool-types.ts +343 -0
- package/src/unreal-bridge.ts +1118 -0
- package/src/utils/cache-manager.ts +213 -0
- package/src/utils/error-handler.ts +320 -0
- package/src/utils/errors.ts +312 -0
- package/src/utils/http.ts +184 -0
- package/src/utils/logger.ts +30 -0
- package/src/utils/normalize.ts +54 -0
- package/src/utils/response-validator.ts +145 -0
- package/src/utils/safe-json.ts +112 -0
- package/src/utils/stdio-redirect.ts +18 -0
- package/src/utils/validation.ts +212 -0
- 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
|