unreal-engine-mcp-server 0.3.1 → 0.4.3
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/.env.production +1 -1
- package/.github/copilot-instructions.md +45 -0
- package/.github/workflows/publish-mcp.yml +1 -1
- package/README.md +22 -7
- package/dist/index.js +137 -46
- package/dist/prompts/index.d.ts +10 -3
- package/dist/prompts/index.js +186 -7
- package/dist/resources/actors.d.ts +19 -1
- package/dist/resources/actors.js +55 -64
- package/dist/resources/assets.d.ts +3 -2
- package/dist/resources/assets.js +117 -109
- package/dist/resources/levels.d.ts +21 -3
- package/dist/resources/levels.js +31 -56
- package/dist/tools/actors.d.ts +3 -14
- package/dist/tools/actors.js +246 -302
- package/dist/tools/animation.d.ts +57 -102
- package/dist/tools/animation.js +429 -450
- package/dist/tools/assets.d.ts +13 -2
- package/dist/tools/assets.js +58 -46
- package/dist/tools/audio.d.ts +22 -13
- package/dist/tools/audio.js +467 -121
- package/dist/tools/blueprint.d.ts +32 -13
- package/dist/tools/blueprint.js +699 -448
- package/dist/tools/build_environment_advanced.d.ts +0 -1
- package/dist/tools/build_environment_advanced.js +236 -87
- package/dist/tools/consolidated-tool-definitions.d.ts +232 -15
- package/dist/tools/consolidated-tool-definitions.js +124 -255
- package/dist/tools/consolidated-tool-handlers.js +749 -766
- package/dist/tools/debug.d.ts +72 -10
- package/dist/tools/debug.js +170 -36
- package/dist/tools/editor.d.ts +9 -2
- package/dist/tools/editor.js +30 -44
- package/dist/tools/foliage.d.ts +34 -15
- package/dist/tools/foliage.js +97 -107
- package/dist/tools/introspection.js +19 -21
- package/dist/tools/landscape.d.ts +1 -2
- package/dist/tools/landscape.js +311 -168
- package/dist/tools/level.d.ts +3 -28
- package/dist/tools/level.js +642 -192
- package/dist/tools/lighting.d.ts +14 -3
- package/dist/tools/lighting.js +236 -123
- package/dist/tools/materials.d.ts +25 -7
- package/dist/tools/materials.js +102 -79
- package/dist/tools/niagara.d.ts +10 -12
- package/dist/tools/niagara.js +74 -94
- package/dist/tools/performance.d.ts +12 -4
- package/dist/tools/performance.js +38 -79
- package/dist/tools/physics.d.ts +34 -10
- package/dist/tools/physics.js +364 -292
- package/dist/tools/rc.js +98 -24
- package/dist/tools/sequence.d.ts +1 -0
- package/dist/tools/sequence.js +146 -24
- package/dist/tools/ui.d.ts +31 -4
- package/dist/tools/ui.js +83 -66
- package/dist/tools/visual.d.ts +11 -0
- package/dist/tools/visual.js +245 -30
- package/dist/types/tool-types.d.ts +0 -6
- package/dist/types/tool-types.js +1 -8
- package/dist/unreal-bridge.d.ts +32 -2
- package/dist/unreal-bridge.js +621 -127
- package/dist/utils/elicitation.d.ts +57 -0
- package/dist/utils/elicitation.js +104 -0
- package/dist/utils/error-handler.d.ts +0 -33
- package/dist/utils/error-handler.js +4 -111
- package/dist/utils/http.d.ts +2 -22
- package/dist/utils/http.js +12 -75
- package/dist/utils/normalize.d.ts +4 -4
- package/dist/utils/normalize.js +15 -7
- package/dist/utils/python-output.d.ts +18 -0
- package/dist/utils/python-output.js +290 -0
- package/dist/utils/python.d.ts +2 -0
- package/dist/utils/python.js +4 -0
- package/dist/utils/response-validator.d.ts +6 -1
- package/dist/utils/response-validator.js +66 -13
- package/dist/utils/result-helpers.d.ts +27 -0
- package/dist/utils/result-helpers.js +147 -0
- package/dist/utils/safe-json.d.ts +0 -2
- package/dist/utils/safe-json.js +0 -43
- package/dist/utils/validation.d.ts +16 -0
- package/dist/utils/validation.js +70 -7
- package/mcp-config-example.json +2 -2
- package/package.json +11 -10
- package/server.json +37 -14
- package/src/index.ts +146 -50
- package/src/prompts/index.ts +211 -13
- package/src/resources/actors.ts +59 -44
- package/src/resources/assets.ts +123 -102
- package/src/resources/levels.ts +37 -47
- package/src/tools/actors.ts +269 -313
- package/src/tools/animation.ts +556 -539
- package/src/tools/assets.ts +59 -45
- package/src/tools/audio.ts +507 -113
- package/src/tools/blueprint.ts +778 -462
- package/src/tools/build_environment_advanced.ts +312 -106
- package/src/tools/consolidated-tool-definitions.ts +136 -267
- package/src/tools/consolidated-tool-handlers.ts +871 -795
- package/src/tools/debug.ts +179 -38
- package/src/tools/editor.ts +35 -37
- package/src/tools/foliage.ts +110 -104
- package/src/tools/introspection.ts +24 -22
- package/src/tools/landscape.ts +334 -181
- package/src/tools/level.ts +683 -182
- package/src/tools/lighting.ts +244 -123
- package/src/tools/materials.ts +114 -83
- package/src/tools/niagara.ts +87 -81
- package/src/tools/performance.ts +49 -88
- package/src/tools/physics.ts +393 -299
- package/src/tools/rc.ts +103 -25
- package/src/tools/sequence.ts +157 -30
- package/src/tools/ui.ts +101 -70
- package/src/tools/visual.ts +250 -29
- package/src/types/tool-types.ts +0 -9
- package/src/unreal-bridge.ts +658 -140
- package/src/utils/elicitation.ts +129 -0
- package/src/utils/error-handler.ts +4 -159
- package/src/utils/http.ts +16 -115
- package/src/utils/normalize.ts +20 -10
- package/src/utils/python-output.ts +351 -0
- package/src/utils/python.ts +3 -0
- package/src/utils/response-validator.ts +68 -17
- package/src/utils/result-helpers.ts +193 -0
- package/src/utils/safe-json.ts +0 -50
- package/src/utils/validation.ts +94 -7
- package/tests/run-unreal-tool-tests.mjs +720 -0
- package/tsconfig.json +2 -2
- package/dist/python-utils.d.ts +0 -29
- package/dist/python-utils.js +0 -54
- package/dist/tools/tool-definitions.d.ts +0 -4919
- package/dist/tools/tool-definitions.js +0 -1065
- package/dist/tools/tool-handlers.d.ts +0 -47
- package/dist/tools/tool-handlers.js +0 -863
- package/dist/types/index.d.ts +0 -323
- package/dist/types/index.js +0 -28
- package/dist/utils/cache-manager.d.ts +0 -64
- package/dist/utils/cache-manager.js +0 -176
- package/dist/utils/errors.d.ts +0 -133
- package/dist/utils/errors.js +0 -256
- package/src/python/editor_compat.py +0 -181
- package/src/python-utils.ts +0 -57
- package/src/tools/tool-definitions.ts +0 -1081
- package/src/tools/tool-handlers.ts +0 -973
- package/src/types/index.ts +0 -414
- package/src/utils/cache-manager.ts +0 -213
- package/src/utils/errors.ts +0 -312
package/src/tools/lighting.ts
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
// Lighting tools for Unreal Engine
|
|
2
2
|
import { UnrealBridge } from '../unreal-bridge.js';
|
|
3
|
+
import { parseStandardResult } from '../utils/python-output.js';
|
|
4
|
+
import { escapePythonString } from '../utils/python.js';
|
|
3
5
|
|
|
4
6
|
export class LightingTools {
|
|
5
7
|
constructor(private bridge: UnrealBridge) {}
|
|
6
8
|
|
|
7
|
-
// Helper to safely escape strings for Python
|
|
8
|
-
private escapePythonString(str: string): string {
|
|
9
|
-
// Escape backslashes first, then quotes
|
|
10
|
-
return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
11
|
-
}
|
|
12
|
-
|
|
13
9
|
private ensurePythonSpawnSucceeded(label: string, result: any) {
|
|
14
10
|
let logs = '';
|
|
15
11
|
if (Array.isArray(result?.LogOutput)) {
|
|
@@ -34,18 +30,22 @@ export class LightingTools {
|
|
|
34
30
|
throw new Error(`Uncertain spawn result for '${label}'. Engine logs:\n${logs}`);
|
|
35
31
|
}
|
|
36
32
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
33
|
+
private normalizeName(value: unknown, fallback?: string): string {
|
|
34
|
+
if (typeof value === 'string') {
|
|
35
|
+
const trimmed = value.trim();
|
|
36
|
+
if (trimmed.length > 0) {
|
|
37
|
+
return trimmed;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (typeof fallback === 'string') {
|
|
42
|
+
const trimmedFallback = fallback.trim();
|
|
43
|
+
if (trimmedFallback.length > 0) {
|
|
44
|
+
return trimmedFallback;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
throw new Error('Invalid name: must be a non-empty string');
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
// Create directional light
|
|
@@ -57,10 +57,8 @@ export class LightingTools {
|
|
|
57
57
|
castShadows?: boolean;
|
|
58
58
|
temperature?: number;
|
|
59
59
|
}) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
throw new Error('Invalid name: must be a non-empty string');
|
|
63
|
-
}
|
|
60
|
+
const name = this.normalizeName(params.name);
|
|
61
|
+
const escapedName = escapePythonString(name);
|
|
64
62
|
|
|
65
63
|
// Validate numeric parameters
|
|
66
64
|
if (params.intensity !== undefined) {
|
|
@@ -135,31 +133,31 @@ spawn_rotation = unreal.Rotator(${rot[0]}, ${rot[1]}, ${rot[2]})
|
|
|
135
133
|
|
|
136
134
|
# Spawn the actor
|
|
137
135
|
spawned_light = editor_actor_subsystem.spawn_actor_from_class(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
136
|
+
directional_light_class,
|
|
137
|
+
spawn_location,
|
|
138
|
+
spawn_rotation
|
|
141
139
|
)
|
|
142
140
|
|
|
143
141
|
if spawned_light:
|
|
144
|
-
|
|
145
|
-
|
|
142
|
+
# Set the label/name
|
|
143
|
+
spawned_light.set_actor_label("${escapedName}")
|
|
146
144
|
|
|
147
|
-
|
|
148
|
-
|
|
145
|
+
# Get the light component
|
|
146
|
+
light_component = spawned_light.get_component_by_class(unreal.DirectionalLightComponent)
|
|
149
147
|
|
|
150
|
-
|
|
148
|
+
if light_component:
|
|
151
149
|
${propertiesCode}
|
|
152
150
|
|
|
153
|
-
|
|
151
|
+
print("Directional light '${escapedName}' spawned")
|
|
154
152
|
else:
|
|
155
|
-
|
|
153
|
+
print("Failed to spawn directional light '${escapedName}'")
|
|
156
154
|
`;
|
|
157
155
|
|
|
158
156
|
// Execute the Python script via bridge (UE 5.6-compatible)
|
|
159
157
|
const result = await this.bridge.executePython(pythonScript);
|
|
160
158
|
|
|
161
|
-
this.ensurePythonSpawnSucceeded(
|
|
162
|
-
return { success: true, message: `Directional light '${
|
|
159
|
+
this.ensurePythonSpawnSucceeded(name, result);
|
|
160
|
+
return { success: true, message: `Directional light '${name}' spawned` };
|
|
163
161
|
}
|
|
164
162
|
|
|
165
163
|
// Create point light
|
|
@@ -172,10 +170,8 @@ else:
|
|
|
172
170
|
falloffExponent?: number;
|
|
173
171
|
castShadows?: boolean;
|
|
174
172
|
}) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
throw new Error('Invalid name: must be a non-empty string');
|
|
178
|
-
}
|
|
173
|
+
const name = this.normalizeName(params.name);
|
|
174
|
+
const escapedName = escapePythonString(name);
|
|
179
175
|
|
|
180
176
|
// Validate location array
|
|
181
177
|
if (params.location !== undefined) {
|
|
@@ -262,31 +258,31 @@ spawn_rotation = unreal.Rotator(0, 0, 0)
|
|
|
262
258
|
|
|
263
259
|
# Spawn the actor
|
|
264
260
|
spawned_light = editor_actor_subsystem.spawn_actor_from_class(
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
261
|
+
point_light_class,
|
|
262
|
+
spawn_location,
|
|
263
|
+
spawn_rotation
|
|
268
264
|
)
|
|
269
265
|
|
|
270
266
|
if spawned_light:
|
|
271
|
-
|
|
272
|
-
|
|
267
|
+
# Set the label/name
|
|
268
|
+
spawned_light.set_actor_label("${escapedName}")
|
|
273
269
|
|
|
274
|
-
|
|
275
|
-
|
|
270
|
+
# Get the light component
|
|
271
|
+
light_component = spawned_light.get_component_by_class(unreal.PointLightComponent)
|
|
276
272
|
|
|
277
|
-
|
|
273
|
+
if light_component:
|
|
278
274
|
${propertiesCode}
|
|
279
275
|
|
|
280
|
-
|
|
276
|
+
print(f"Point light '${escapedName}' spawned at {spawn_location.x}, {spawn_location.y}, {spawn_location.z}")
|
|
281
277
|
else:
|
|
282
|
-
|
|
278
|
+
print("Failed to spawn point light '${escapedName}'")
|
|
283
279
|
`;
|
|
284
280
|
|
|
285
281
|
// Execute the Python script via bridge (UE 5.6-compatible)
|
|
286
282
|
const result = await this.bridge.executePython(pythonScript);
|
|
287
283
|
|
|
288
|
-
this.ensurePythonSpawnSucceeded(
|
|
289
|
-
return { success: true, message: `Point light '${
|
|
284
|
+
this.ensurePythonSpawnSucceeded(name, result);
|
|
285
|
+
return { success: true, message: `Point light '${name}' spawned at ${location.join(', ')}` };
|
|
290
286
|
}
|
|
291
287
|
|
|
292
288
|
// Create spot light
|
|
@@ -301,10 +297,8 @@ else:
|
|
|
301
297
|
color?: [number, number, number];
|
|
302
298
|
castShadows?: boolean;
|
|
303
299
|
}) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
throw new Error('Invalid name: must be a non-empty string');
|
|
307
|
-
}
|
|
300
|
+
const name = this.normalizeName(params.name);
|
|
301
|
+
const escapedName = escapePythonString(name);
|
|
308
302
|
|
|
309
303
|
// Validate required location and rotation arrays
|
|
310
304
|
if (!params.location || !Array.isArray(params.location) || params.location.length !== 3) {
|
|
@@ -418,7 +412,7 @@ spawned_light = editor_actor_subsystem.spawn_actor_from_class(
|
|
|
418
412
|
|
|
419
413
|
if spawned_light:
|
|
420
414
|
# Set the label/name
|
|
421
|
-
spawned_light.set_actor_label("${
|
|
415
|
+
spawned_light.set_actor_label("${escapedName}")
|
|
422
416
|
|
|
423
417
|
# Get the light component
|
|
424
418
|
light_component = spawned_light.get_component_by_class(unreal.SpotLightComponent)
|
|
@@ -426,16 +420,16 @@ if spawned_light:
|
|
|
426
420
|
if light_component:
|
|
427
421
|
${propertiesCode}
|
|
428
422
|
|
|
429
|
-
print(f"Spot light '${
|
|
423
|
+
print(f"Spot light '${escapedName}' spawned at {spawn_location.x}, {spawn_location.y}, {spawn_location.z}")
|
|
430
424
|
else:
|
|
431
|
-
print("Failed to spawn spot light '${
|
|
425
|
+
print("Failed to spawn spot light '${escapedName}'")
|
|
432
426
|
`;
|
|
433
427
|
|
|
434
428
|
// Execute the Python script via bridge (UE 5.6-compatible)
|
|
435
|
-
|
|
429
|
+
const result = await this.bridge.executePython(pythonScript);
|
|
436
430
|
|
|
437
|
-
|
|
438
|
-
|
|
431
|
+
this.ensurePythonSpawnSucceeded(name, result);
|
|
432
|
+
return { success: true, message: `Spot light '${name}' spawned at ${params.location.join(', ')}` };
|
|
439
433
|
}
|
|
440
434
|
|
|
441
435
|
// Create rect light
|
|
@@ -448,11 +442,9 @@ else:
|
|
|
448
442
|
intensity?: number;
|
|
449
443
|
color?: [number, number, number];
|
|
450
444
|
}) {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}
|
|
455
|
-
|
|
445
|
+
const name = this.normalizeName(params.name);
|
|
446
|
+
const escapedName = escapePythonString(name);
|
|
447
|
+
|
|
456
448
|
// Validate required location and rotation arrays
|
|
457
449
|
if (!params.location || !Array.isArray(params.location) || params.location.length !== 3) {
|
|
458
450
|
throw new Error('Invalid location: must be an array [x,y,z]');
|
|
@@ -549,25 +541,25 @@ spawned_light = editor_actor_subsystem.spawn_actor_from_class(
|
|
|
549
541
|
)
|
|
550
542
|
|
|
551
543
|
if spawned_light:
|
|
552
|
-
|
|
553
|
-
|
|
544
|
+
# Set the label/name
|
|
545
|
+
spawned_light.set_actor_label("${escapedName}")
|
|
554
546
|
|
|
555
|
-
|
|
556
|
-
|
|
547
|
+
# Get the light component
|
|
548
|
+
light_component = spawned_light.get_component_by_class(unreal.RectLightComponent)
|
|
557
549
|
|
|
558
|
-
|
|
550
|
+
if light_component:
|
|
559
551
|
${propertiesCode}
|
|
560
552
|
|
|
561
|
-
|
|
553
|
+
print(f"Rect light '${escapedName}' spawned at {spawn_location.x}, {spawn_location.y}, {spawn_location.z}")
|
|
562
554
|
else:
|
|
563
|
-
|
|
555
|
+
print("Failed to spawn rect light '${escapedName}'")
|
|
564
556
|
`;
|
|
565
557
|
|
|
566
558
|
// Execute the Python script via bridge (UE 5.6-compatible)
|
|
567
559
|
const result = await this.bridge.executePython(pythonScript);
|
|
568
560
|
|
|
569
|
-
this.ensurePythonSpawnSucceeded(
|
|
570
|
-
return { success: true, message: `Rect light '${
|
|
561
|
+
this.ensurePythonSpawnSucceeded(name, result);
|
|
562
|
+
return { success: true, message: `Rect light '${name}' spawned at ${params.location.join(', ')}` };
|
|
571
563
|
}
|
|
572
564
|
|
|
573
565
|
// Create sky light
|
|
@@ -578,62 +570,189 @@ else:
|
|
|
578
570
|
intensity?: number;
|
|
579
571
|
recapture?: boolean;
|
|
580
572
|
}) {
|
|
581
|
-
const
|
|
573
|
+
const name = this.normalizeName(params.name);
|
|
574
|
+
const escapedName = escapePythonString(name);
|
|
575
|
+
const sourceTypeRaw = typeof params.sourceType === 'string' ? params.sourceType.trim() : undefined;
|
|
576
|
+
const normalizedSourceType = sourceTypeRaw
|
|
577
|
+
? sourceTypeRaw.toLowerCase() === 'specifiedcubemap'
|
|
578
|
+
? 'SpecifiedCubemap'
|
|
579
|
+
: sourceTypeRaw.toLowerCase() === 'capturedscene'
|
|
580
|
+
? 'CapturedScene'
|
|
581
|
+
: undefined
|
|
582
|
+
: undefined;
|
|
583
|
+
const cubemapPath = typeof params.cubemapPath === 'string' ? params.cubemapPath.trim() : undefined;
|
|
584
|
+
|
|
585
|
+
if (normalizedSourceType === 'SpecifiedCubemap' && (!cubemapPath || cubemapPath.length === 0)) {
|
|
586
|
+
const message = 'cubemapPath is required when sourceType is SpecifiedCubemap';
|
|
587
|
+
return { success: false, error: message, message };
|
|
588
|
+
}
|
|
589
|
+
const escapedCubemapPath = cubemapPath ? escapePythonString(cubemapPath) : '';
|
|
590
|
+
const python = `
|
|
582
591
|
import unreal
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
592
|
+
import json
|
|
593
|
+
|
|
594
|
+
result = {
|
|
595
|
+
"success": False,
|
|
596
|
+
"message": "",
|
|
597
|
+
"error": "",
|
|
598
|
+
"warnings": []
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
def add_warning(text):
|
|
602
|
+
if text:
|
|
603
|
+
result["warnings"].append(str(text))
|
|
604
|
+
|
|
605
|
+
def finish():
|
|
606
|
+
if result["success"]:
|
|
607
|
+
if not result["message"]:
|
|
608
|
+
result["message"] = "Sky light ensured"
|
|
609
|
+
result.pop("error", None)
|
|
610
|
+
else:
|
|
611
|
+
if not result["error"]:
|
|
612
|
+
result["error"] = result["message"] or "Failed to ensure sky light"
|
|
613
|
+
if not result["message"]:
|
|
614
|
+
result["message"] = result["error"]
|
|
615
|
+
if not result["warnings"]:
|
|
616
|
+
result.pop("warnings", None)
|
|
617
|
+
print('RESULT:' + json.dumps(result))
|
|
618
|
+
|
|
588
619
|
try:
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
620
|
+
actor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
|
|
621
|
+
if not actor_sub:
|
|
622
|
+
result["error"] = "EditorActorSubsystem unavailable"
|
|
623
|
+
finish()
|
|
624
|
+
raise SystemExit(0)
|
|
625
|
+
|
|
626
|
+
spawn_location = unreal.Vector(0.0, 0.0, 500.0)
|
|
627
|
+
spawn_rotation = unreal.Rotator(0.0, 0.0, 0.0)
|
|
628
|
+
|
|
629
|
+
actor = None
|
|
630
|
+
try:
|
|
631
|
+
for candidate in actor_sub.get_all_level_actors():
|
|
632
|
+
try:
|
|
633
|
+
if candidate.get_class().get_name() == 'SkyLight':
|
|
634
|
+
actor = candidate
|
|
635
|
+
break
|
|
636
|
+
except Exception:
|
|
637
|
+
continue
|
|
638
|
+
except Exception:
|
|
639
|
+
pass
|
|
640
|
+
|
|
641
|
+
if actor is None:
|
|
642
|
+
actor = actor_sub.spawn_actor_from_class(unreal.SkyLight, spawn_location, spawn_rotation)
|
|
643
|
+
|
|
644
|
+
if not actor:
|
|
645
|
+
result["error"] = "Failed to spawn SkyLight actor"
|
|
646
|
+
finish()
|
|
647
|
+
raise SystemExit(0)
|
|
648
|
+
|
|
649
|
+
try:
|
|
650
|
+
actor.set_actor_label("${escapedName}")
|
|
651
|
+
except Exception:
|
|
652
|
+
pass
|
|
653
|
+
|
|
654
|
+
comp = actor.get_component_by_class(unreal.SkyLightComponent)
|
|
655
|
+
if not comp:
|
|
656
|
+
result["error"] = "SkyLight component missing"
|
|
657
|
+
finish()
|
|
658
|
+
raise SystemExit(0)
|
|
659
|
+
|
|
660
|
+
${params.intensity !== undefined ? `
|
|
661
|
+
try:
|
|
662
|
+
comp.set_intensity(${params.intensity})
|
|
663
|
+
except Exception:
|
|
601
664
|
try:
|
|
602
|
-
|
|
603
|
-
except Exception:
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
665
|
+
comp.set_editor_property('intensity', ${params.intensity})
|
|
666
|
+
except Exception:
|
|
667
|
+
add_warning('Unable to set intensity property')
|
|
668
|
+
` : ''}
|
|
669
|
+
|
|
670
|
+
source_type = ${normalizedSourceType ? `'${normalizedSourceType}'` : 'None'}
|
|
671
|
+
if source_type:
|
|
672
|
+
try:
|
|
673
|
+
comp.set_editor_property('source_type', getattr(unreal.SkyLightSourceType, source_type))
|
|
674
|
+
except Exception:
|
|
675
|
+
try:
|
|
676
|
+
comp.source_type = getattr(unreal.SkyLightSourceType, source_type)
|
|
677
|
+
except Exception:
|
|
678
|
+
add_warning(f"Unable to set source type {source_type}")
|
|
679
|
+
|
|
680
|
+
if source_type == 'SpecifiedCubemap':
|
|
681
|
+
path = "${escapedCubemapPath}"
|
|
682
|
+
if not path:
|
|
683
|
+
result["error"] = "cubemapPath is required when sourceType is SpecifiedCubemap"
|
|
684
|
+
finish()
|
|
685
|
+
raise SystemExit(0)
|
|
686
|
+
try:
|
|
687
|
+
exists = unreal.EditorAssetLibrary.does_asset_exist(path)
|
|
688
|
+
except Exception:
|
|
689
|
+
exists = False
|
|
690
|
+
if not exists:
|
|
691
|
+
result["error"] = f"Cubemap asset not found: {path}"
|
|
692
|
+
finish()
|
|
693
|
+
raise SystemExit(0)
|
|
694
|
+
try:
|
|
695
|
+
cube = unreal.EditorAssetLibrary.load_asset(path)
|
|
696
|
+
except Exception as load_err:
|
|
697
|
+
result["error"] = f"Failed to load cubemap asset: {load_err}"
|
|
698
|
+
finish()
|
|
699
|
+
raise SystemExit(0)
|
|
700
|
+
if not cube:
|
|
701
|
+
result["error"] = f"Cubemap asset could not be loaded: {path}"
|
|
702
|
+
finish()
|
|
703
|
+
raise SystemExit(0)
|
|
704
|
+
try:
|
|
705
|
+
if hasattr(comp, 'set_cubemap'):
|
|
706
|
+
comp.set_cubemap(cube)
|
|
707
|
+
else:
|
|
708
|
+
comp.set_editor_property('cubemap', cube)
|
|
709
|
+
except Exception as assign_err:
|
|
710
|
+
result["error"] = f"Failed to assign cubemap: {assign_err}"
|
|
711
|
+
finish()
|
|
712
|
+
raise SystemExit(0)
|
|
713
|
+
|
|
714
|
+
if ${params.recapture ? 'True' : 'False'}:
|
|
715
|
+
try:
|
|
716
|
+
comp.recapture_sky()
|
|
717
|
+
except Exception as recapture_err:
|
|
718
|
+
add_warning(f"Recapture failed: {recapture_err}")
|
|
719
|
+
|
|
720
|
+
result["success"] = True
|
|
721
|
+
result["message"] = "Sky light ensured"
|
|
722
|
+
finish()
|
|
723
|
+
|
|
724
|
+
except SystemExit:
|
|
725
|
+
pass
|
|
726
|
+
except Exception as run_err:
|
|
727
|
+
result["error"] = str(run_err)
|
|
728
|
+
finish()
|
|
624
729
|
`.trim();
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
if (
|
|
629
|
-
|
|
730
|
+
const resp = await this.bridge.executePython(python);
|
|
731
|
+
const parsed = parseStandardResult(resp).data;
|
|
732
|
+
if (parsed) {
|
|
733
|
+
if (parsed.success) {
|
|
734
|
+
return {
|
|
735
|
+
success: true,
|
|
736
|
+
message: parsed.message ?? 'Sky light ensured',
|
|
737
|
+
warnings: Array.isArray(parsed.warnings) && parsed.warnings.length > 0 ? parsed.warnings : undefined
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
return {
|
|
741
|
+
success: false,
|
|
742
|
+
error: parsed.error ?? parsed.message ?? 'Failed to ensure sky light',
|
|
743
|
+
warnings: Array.isArray(parsed.warnings) && parsed.warnings.length > 0 ? parsed.warnings : undefined
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
return { success: true, message: 'Sky light ensured' };
|
|
630
747
|
}
|
|
631
748
|
|
|
632
749
|
// Remove duplicate SkyLights and keep only one (named target label)
|
|
633
750
|
async ensureSingleSkyLight(params?: { name?: string; recapture?: boolean }) {
|
|
634
|
-
|
|
751
|
+
const fallbackName = 'MCP_Test_Sky';
|
|
752
|
+
const name = this.normalizeName(params?.name, fallbackName);
|
|
753
|
+
const escapedName = escapePythonString(name);
|
|
635
754
|
const recapture = !!params?.recapture;
|
|
636
|
-
const py = `\nimport unreal, json\nactor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)\nactors = actor_sub.get_all_level_actors() if actor_sub else []\nskies = []\nfor a in actors:\n try:\n if a.get_class().get_name() == 'SkyLight':\n skies.append(a)\n except Exception: pass\nkeep = None\n# Prefer one with matching label; otherwise keep the first\nfor a in skies:\n try:\n label = a.get_actor_label()\n if label ==
|
|
755
|
+
const py = `\nimport unreal, json\nactor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)\nactors = actor_sub.get_all_level_actors() if actor_sub else []\nskies = []\nfor a in actors:\n try:\n if a.get_class().get_name() == 'SkyLight':\n skies.append(a)\n except Exception: pass\nkeep = None\n# Prefer one with matching label; otherwise keep the first\nfor a in skies:\n try:\n label = a.get_actor_label()\n if label == "${escapedName}":\n keep = a\n break\n except Exception: pass\nif keep is None and len(skies) > 0:\n keep = skies[0]\n# Rename the kept one if needed\nif keep is not None:\n try: keep.set_actor_label("${escapedName}")\n except Exception: pass\n# Destroy all others using the correct non-deprecated API\nremoved = 0\nfor a in skies:\n if keep is not None and a == keep:\n continue\n try:\n # Use EditorActorSubsystem.destroy_actor instead of deprecated EditorLevelLibrary\n actor_sub.destroy_actor(a)\n removed += 1\n except Exception: pass\n# Optionally recapture\nif keep is not None and ${recapture ? 'True' : 'False'}:\n try:\n comp = keep.get_component_by_class(unreal.SkyLightComponent)\n if comp: comp.recapture_sky()\n except Exception: pass\nprint('RESULT:' + json.dumps({'success': True, 'removed': removed, 'kept': True if keep else False}))\n`.trim();
|
|
637
756
|
|
|
638
757
|
const resp = await this.bridge.executePython(py);
|
|
639
758
|
let out = '';
|
|
@@ -1205,6 +1324,8 @@ print('RESULT:' + json.dumps(result))
|
|
|
1205
1324
|
location: [number, number, number];
|
|
1206
1325
|
size: [number, number, number];
|
|
1207
1326
|
}) {
|
|
1327
|
+
const name = this.normalizeName(params.name);
|
|
1328
|
+
const escapedName = escapePythonString(name);
|
|
1208
1329
|
const [lx, ly, lz] = params.location;
|
|
1209
1330
|
const [sx, sy, sz] = params.size;
|
|
1210
1331
|
const py = `
|
|
@@ -1214,7 +1335,7 @@ loc = unreal.Vector(${lx}, ${ly}, ${lz})
|
|
|
1214
1335
|
rot = unreal.Rotator(0,0,0)
|
|
1215
1336
|
actor = editor_actor_subsystem.spawn_actor_from_class(unreal.LightmassImportanceVolume, loc, rot)
|
|
1216
1337
|
if actor:
|
|
1217
|
-
|
|
1338
|
+
try: actor.set_actor_label("${escapedName}")
|
|
1218
1339
|
except Exception: pass
|
|
1219
1340
|
# Best-effort: set actor scale to approximate size
|
|
1220
1341
|
try:
|
|
@@ -1227,7 +1348,7 @@ else:
|
|
|
1227
1348
|
const resp = await this.bridge.executePython(py);
|
|
1228
1349
|
const out = typeof resp === 'string' ? resp : JSON.stringify(resp);
|
|
1229
1350
|
const m = out.match(/RESULT:({.*})/);
|
|
1230
|
-
|
|
1351
|
+
if (m) { try { const parsed = JSON.parse(m[1].replace(/'/g, '"')); return parsed.success ? { success: true, message: `LightmassImportanceVolume '${name}' created` } : { success: false, error: parsed.error }; } catch {} }
|
|
1231
1352
|
return { success: true, message: 'LightmassImportanceVolume creation attempted' };
|
|
1232
1353
|
}
|
|
1233
1354
|
|