unreal-engine-mcp-server 0.4.0 → 0.4.4
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 +3 -2
- package/README.md +21 -5
- package/dist/index.js +124 -31
- 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.js +46 -62
- package/dist/resources/levels.d.ts +21 -3
- package/dist/resources/levels.js +29 -54
- 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 +52 -44
- 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 +190 -45
- package/dist/tools/consolidated-tool-definitions.js +78 -252
- package/dist/tools/consolidated-tool-handlers.js +506 -133
- package/dist/tools/debug.d.ts +72 -10
- package/dist/tools/debug.js +167 -31
- 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 +97 -23
- package/dist/tools/sequence.d.ts +1 -0
- package/dist/tools/sequence.js +125 -22
- 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.js +28 -2
- 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 +10 -9
- package/server.json +37 -14
- package/src/index.ts +130 -33
- package/src/prompts/index.ts +211 -13
- package/src/resources/actors.ts +59 -44
- package/src/resources/assets.ts +48 -51
- package/src/resources/levels.ts +35 -45
- package/src/tools/actors.ts +269 -313
- package/src/tools/animation.ts +556 -539
- package/src/tools/assets.ts +53 -43
- package/src/tools/audio.ts +507 -113
- package/src/tools/blueprint.ts +778 -462
- package/src/tools/build_environment_advanced.ts +266 -64
- package/src/tools/consolidated-tool-definitions.ts +90 -264
- package/src/tools/consolidated-tool-handlers.ts +630 -121
- package/src/tools/debug.ts +176 -33
- 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 +102 -24
- package/src/tools/sequence.ts +136 -28
- 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 +25 -2
- 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/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/types/index.ts +0 -414
- package/src/utils/cache-manager.ts +0 -213
- package/src/utils/errors.ts +0 -312
package/src/tools/landscape.ts
CHANGED
|
@@ -1,23 +1,12 @@
|
|
|
1
1
|
// Landscape tools for Unreal Engine with UE 5.6 World Partition support
|
|
2
2
|
import { UnrealBridge } from '../unreal-bridge.js';
|
|
3
|
+
import { bestEffortInterpretedText, coerceBoolean, coerceString, interpretStandardResult } from '../utils/result-helpers.js';
|
|
4
|
+
import { ensureVector3 } from '../utils/validation.js';
|
|
5
|
+
import { escapePythonString } from '../utils/python.js';
|
|
3
6
|
|
|
4
7
|
export class LandscapeTools {
|
|
5
8
|
constructor(private bridge: UnrealBridge) {}
|
|
6
9
|
|
|
7
|
-
// Execute console command
|
|
8
|
-
private async _executeCommand(command: string) {
|
|
9
|
-
return this.bridge.httpCall('/remote/object/call', 'PUT', {
|
|
10
|
-
objectPath: '/Script/Engine.Default__KismetSystemLibrary',
|
|
11
|
-
functionName: 'ExecuteConsoleCommand',
|
|
12
|
-
parameters: {
|
|
13
|
-
WorldContextObject: null,
|
|
14
|
-
Command: command,
|
|
15
|
-
SpecificPlayer: null
|
|
16
|
-
},
|
|
17
|
-
generateTransaction: false
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
|
|
21
10
|
// Create landscape with World Partition support (UE 5.6)
|
|
22
11
|
async createLandscape(params: {
|
|
23
12
|
name: string;
|
|
@@ -34,99 +23,250 @@ export class LandscapeTools {
|
|
|
34
23
|
isSpatiallyLoaded?: boolean;
|
|
35
24
|
dataLayers?: string[];
|
|
36
25
|
}) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
26
|
+
const name = params.name?.trim();
|
|
27
|
+
if (!name) {
|
|
28
|
+
return { success: false, error: 'Landscape name is required' };
|
|
29
|
+
}
|
|
30
|
+
if (typeof params.sizeX === 'number' && params.sizeX <= 0) {
|
|
31
|
+
return {
|
|
32
|
+
success: false,
|
|
33
|
+
error: 'Landscape sizeX must be a positive number'
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (typeof params.sizeY === 'number' && params.sizeY <= 0) {
|
|
37
|
+
return {
|
|
38
|
+
success: false,
|
|
39
|
+
error: 'Landscape sizeY must be a positive number'
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const [locX, locY, locZ] = ensureVector3(params.location ?? [0, 0, 0], 'landscape location');
|
|
44
|
+
const sectionsPerComponent = Math.max(1, Math.floor(params.sectionsPerComponent ?? 1));
|
|
45
|
+
const quadsPerSection = Math.max(1, Math.floor(params.quadsPerSection ?? 63));
|
|
46
|
+
const componentCount = Math.max(1, Math.floor(params.componentCount ?? 1));
|
|
47
|
+
|
|
48
|
+
const defaultSize = 1000;
|
|
49
|
+
const scaleX = params.sizeX ? Math.max(0.1, params.sizeX / defaultSize) : 1;
|
|
50
|
+
const scaleY = params.sizeY ? Math.max(0.1, params.sizeY / defaultSize) : 1;
|
|
51
|
+
|
|
52
|
+
const escapedName = escapePythonString(name);
|
|
53
|
+
const escapedMaterial =
|
|
54
|
+
params.materialPath && params.materialPath.trim().length > 0
|
|
55
|
+
? escapePythonString(params.materialPath.trim())
|
|
56
|
+
: '';
|
|
57
|
+
|
|
58
|
+
const runtimeGridFlag = params.runtimeGrid ? 'True' : 'False';
|
|
59
|
+
const spatiallyLoadedFlag = params.isSpatiallyLoaded ? 'True' : 'False';
|
|
60
|
+
const runtimeGridValue = params.runtimeGrid ? escapePythonString(params.runtimeGrid.trim()) : '';
|
|
61
|
+
const dataLayerNames = Array.isArray(params.dataLayers)
|
|
62
|
+
? params.dataLayers
|
|
63
|
+
.map(layer => layer?.trim())
|
|
64
|
+
.filter((layer): layer is string => Boolean(layer))
|
|
65
|
+
.map(layer => escapePythonString(layer))
|
|
66
|
+
: [];
|
|
67
|
+
|
|
68
|
+
const pythonScript = `
|
|
40
69
|
import unreal
|
|
41
70
|
import json
|
|
42
71
|
|
|
43
|
-
|
|
72
|
+
result = {
|
|
73
|
+
"success": False,
|
|
74
|
+
"message": "",
|
|
75
|
+
"error": "",
|
|
76
|
+
"warnings": [],
|
|
77
|
+
"details": [],
|
|
78
|
+
"landscapeName": "",
|
|
79
|
+
"landscapeActor": "",
|
|
80
|
+
"worldPartition": False,
|
|
81
|
+
"runtimeGridRequested": ${runtimeGridFlag},
|
|
82
|
+
"spatiallyLoaded": ${spatiallyLoadedFlag}
|
|
83
|
+
}
|
|
84
|
+
|
|
44
85
|
try:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
86
|
+
editor_subsystem = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
|
|
87
|
+
world = editor_subsystem.get_editor_world() if editor_subsystem and hasattr(editor_subsystem, 'get_editor_world') else None
|
|
88
|
+
data_layer_manager = None
|
|
89
|
+
if world:
|
|
49
90
|
try:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
91
|
+
world_partition = world.get_world_partition()
|
|
92
|
+
result["worldPartition"] = world_partition is not None
|
|
93
|
+
if result["worldPartition"] and hasattr(unreal, "WorldPartitionBlueprintLibrary"):
|
|
94
|
+
data_layer_manager = unreal.WorldPartitionBlueprintLibrary.get_data_layer_manager(world)
|
|
95
|
+
except Exception as wp_error:
|
|
96
|
+
result["warnings"].append(f"Failed to inspect world partition: {wp_error}")
|
|
53
97
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
98
|
+
actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
|
|
99
|
+
if not actor_subsystem:
|
|
100
|
+
result["error"] = "EditorActorSubsystem unavailable"
|
|
101
|
+
else:
|
|
102
|
+
existing = None
|
|
58
103
|
try:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
except:
|
|
66
|
-
pass
|
|
104
|
+
for actor in actor_subsystem.get_all_level_actors():
|
|
105
|
+
if actor and actor.get_actor_label() == "${escapedName}":
|
|
106
|
+
existing = actor
|
|
107
|
+
break
|
|
108
|
+
except Exception as scan_error:
|
|
109
|
+
result["warnings"].append(f"Actor scan failed: {scan_error}")
|
|
67
110
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
landscape_subsystem = None
|
|
76
|
-
try:
|
|
77
|
-
landscape_subsystem = unreal.get_editor_subsystem(unreal.LandscapeSubsystem)
|
|
78
|
-
except:
|
|
111
|
+
if existing:
|
|
112
|
+
result["success"] = True
|
|
113
|
+
result["message"] = "Landscape already exists"
|
|
114
|
+
result["landscapeName"] = existing.get_actor_label()
|
|
115
|
+
try:
|
|
116
|
+
result["landscapeActor"] = existing.get_path_name()
|
|
117
|
+
except Exception:
|
|
79
118
|
pass
|
|
80
|
-
|
|
81
|
-
if landscape_subsystem:
|
|
82
|
-
# Use subsystem if available
|
|
83
|
-
result = {"success": False, "error": "LandscapeSubsystem API limited via Python", "world_partition": is_world_partition}
|
|
84
119
|
else:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
"
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
120
|
+
landscape_class = getattr(unreal, "Landscape", None)
|
|
121
|
+
if not landscape_class:
|
|
122
|
+
result["error"] = "Landscape class unavailable"
|
|
123
|
+
else:
|
|
124
|
+
location = unreal.Vector(${locX}, ${locY}, ${locZ})
|
|
125
|
+
rotation = unreal.Rotator(0.0, 0.0, 0.0)
|
|
126
|
+
landscape_actor = actor_subsystem.spawn_actor_from_class(landscape_class, location, rotation)
|
|
127
|
+
if not landscape_actor:
|
|
128
|
+
result["error"] = "Failed to spawn landscape actor"
|
|
129
|
+
else:
|
|
130
|
+
try:
|
|
131
|
+
landscape_actor.set_actor_label("${escapedName}", True)
|
|
132
|
+
except TypeError:
|
|
133
|
+
landscape_actor.set_actor_label("${escapedName}")
|
|
134
|
+
except Exception as label_error:
|
|
135
|
+
result["warnings"].append(f"Failed to set landscape label: {label_error}")
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
landscape_actor.set_actor_scale3d(unreal.Vector(${scaleX.toFixed(4)}, ${scaleY.toFixed(4)}, 1.0))
|
|
139
|
+
result["details"].append(f"Actor scale set to (${scaleX.toFixed(2)}, ${scaleY.toFixed(2)}, 1.0)")
|
|
140
|
+
except Exception as scale_error:
|
|
141
|
+
result["warnings"].append(f"Failed to set landscape scale: {scale_error}")
|
|
142
|
+
|
|
143
|
+
landscape_editor = None
|
|
144
|
+
try:
|
|
145
|
+
landscape_editor = unreal.get_editor_subsystem(unreal.LandscapeEditorSubsystem)
|
|
146
|
+
except Exception as editor_error:
|
|
147
|
+
result["warnings"].append(f"LandscapeEditorSubsystem unavailable: {editor_error}")
|
|
148
|
+
|
|
149
|
+
if landscape_editor:
|
|
150
|
+
try:
|
|
151
|
+
landscape_editor.set_component_size(${sectionsPerComponent}, ${quadsPerSection})
|
|
152
|
+
landscape_editor.set_component_count(${componentCount}, ${componentCount})
|
|
153
|
+
result["details"].append(f"Component size ${sectionsPerComponent}x${quadsPerSection}, count ${componentCount}x${componentCount}")
|
|
154
|
+
except Exception as config_error:
|
|
155
|
+
result["warnings"].append(f"Landscape configuration limited: {config_error}")
|
|
156
|
+
|
|
157
|
+
${escapedMaterial ? `try:
|
|
158
|
+
material = unreal.EditorAssetLibrary.load_asset("${escapedMaterial}")
|
|
159
|
+
if material:
|
|
160
|
+
try:
|
|
161
|
+
landscape_actor.set_landscape_material(material)
|
|
162
|
+
except Exception:
|
|
163
|
+
landscape_actor.editor_set_landscape_material(material)
|
|
164
|
+
result["details"].append("Landscape material applied")
|
|
165
|
+
else:
|
|
166
|
+
result["warnings"].append("Landscape material asset not found: ${escapedMaterial}")
|
|
167
|
+
except Exception as material_error:
|
|
168
|
+
result["warnings"].append(f"Failed to apply landscape material: {material_error}")
|
|
169
|
+
` : ''}
|
|
170
|
+
${runtimeGridValue ? `if result["worldPartition"] and hasattr(unreal, "WorldPartitionBlueprintLibrary"):
|
|
171
|
+
try:
|
|
172
|
+
unreal.WorldPartitionBlueprintLibrary.set_actor_runtime_grid(landscape_actor, "${runtimeGridValue}")
|
|
173
|
+
result["details"].append("Runtime grid assigned: ${runtimeGridValue}")
|
|
174
|
+
except Exception as grid_error:
|
|
175
|
+
result["warnings"].append(f"Failed to assign runtime grid: {grid_error}")
|
|
176
|
+
` : ''}
|
|
177
|
+
${params.isSpatiallyLoaded ? `if result["worldPartition"] and hasattr(unreal, "WorldPartitionBlueprintLibrary"):
|
|
178
|
+
try:
|
|
179
|
+
unreal.WorldPartitionBlueprintLibrary.set_actor_spatially_loaded(landscape_actor, True)
|
|
180
|
+
result["details"].append("Actor marked as spatially loaded")
|
|
181
|
+
except Exception as spatial_error:
|
|
182
|
+
result["warnings"].append(f"Failed to mark as spatially loaded: {spatial_error}")
|
|
183
|
+
` : ''}
|
|
184
|
+
${dataLayerNames.length ? `if result["worldPartition"] and data_layer_manager:
|
|
185
|
+
for layer_name in ${JSON.stringify(dataLayerNames)}:
|
|
186
|
+
try:
|
|
187
|
+
data_layer = data_layer_manager.get_data_layer(layer_name)
|
|
188
|
+
if data_layer:
|
|
189
|
+
unreal.WorldPartitionBlueprintLibrary.add_actor_to_data_layer(landscape_actor, data_layer)
|
|
190
|
+
result["details"].append(f"Added to data layer {layer_name}")
|
|
191
|
+
else:
|
|
192
|
+
result["warnings"].append(f"Data layer not found: {layer_name}")
|
|
193
|
+
except Exception as data_layer_error:
|
|
194
|
+
result["warnings"].append(f"Failed to assign data layer {layer_name}: {data_layer_error}")
|
|
195
|
+
` : ''}
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
result["landscapeName"] = landscape_actor.get_actor_label()
|
|
199
|
+
result["landscapeActor"] = landscape_actor.get_path_name()
|
|
200
|
+
except Exception:
|
|
201
|
+
pass
|
|
202
|
+
|
|
203
|
+
result["success"] = True
|
|
204
|
+
result["message"] = "Landscape actor created"
|
|
102
205
|
except Exception as e:
|
|
103
|
-
|
|
206
|
+
result["error"] = str(e)
|
|
207
|
+
|
|
208
|
+
if result.get("success"):
|
|
209
|
+
result.pop("error", None)
|
|
210
|
+
else:
|
|
211
|
+
if not result.get("error"):
|
|
212
|
+
result["error"] = "Failed to create landscape actor"
|
|
213
|
+
if not result.get("message"):
|
|
214
|
+
result["message"] = result["error"]
|
|
215
|
+
|
|
216
|
+
if not result.get("warnings"):
|
|
217
|
+
result.pop("warnings", None)
|
|
218
|
+
if not result.get("details"):
|
|
219
|
+
result.pop("details", None)
|
|
220
|
+
|
|
221
|
+
print("RESULT:" + json.dumps(result))
|
|
104
222
|
`.trim();
|
|
105
|
-
|
|
223
|
+
|
|
224
|
+
try {
|
|
106
225
|
const response = await this.bridge.executePython(pythonScript);
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
226
|
+
const interpreted = interpretStandardResult(response, {
|
|
227
|
+
successMessage: 'Landscape actor created',
|
|
228
|
+
failureMessage: 'Failed to create landscape actor'
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
if (!interpreted.success) {
|
|
232
|
+
return {
|
|
233
|
+
success: false,
|
|
234
|
+
error: interpreted.error || interpreted.message
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const result: Record<string, unknown> = {
|
|
239
|
+
success: true,
|
|
240
|
+
message: interpreted.message,
|
|
241
|
+
landscapeName: coerceString(interpreted.payload.landscapeName) ?? name,
|
|
242
|
+
worldPartition: coerceBoolean(interpreted.payload.worldPartition)
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const actorPath = coerceString(interpreted.payload.landscapeActor);
|
|
246
|
+
if (actorPath) {
|
|
247
|
+
result.landscapeActor = actorPath;
|
|
118
248
|
}
|
|
119
|
-
|
|
120
|
-
|
|
249
|
+
if (interpreted.warnings?.length) {
|
|
250
|
+
result.warnings = interpreted.warnings;
|
|
251
|
+
}
|
|
252
|
+
if (interpreted.details?.length) {
|
|
253
|
+
result.details = interpreted.details;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (params.runtimeGrid) {
|
|
257
|
+
result.runtimeGrid = params.runtimeGrid;
|
|
258
|
+
}
|
|
259
|
+
if (typeof params.isSpatiallyLoaded === 'boolean') {
|
|
260
|
+
result.spatiallyLoaded = params.isSpatiallyLoaded;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return result;
|
|
264
|
+
} catch (error) {
|
|
265
|
+
return {
|
|
266
|
+
success: false,
|
|
267
|
+
error: `Failed to create landscape actor: ${error}`
|
|
268
|
+
};
|
|
121
269
|
}
|
|
122
|
-
|
|
123
|
-
// Fallback message with World Partition info
|
|
124
|
-
return {
|
|
125
|
-
success: false,
|
|
126
|
-
error: 'Landscape creation via API is limited. Please use the Unreal Editor UI to create landscapes.',
|
|
127
|
-
worldPartitionSupport: params.enableWorldPartition ? 'Requested' : 'Not requested',
|
|
128
|
-
suggestion: 'Use the Landscape Mode in the editor toolbar to create and configure landscapes'
|
|
129
|
-
};
|
|
130
270
|
}
|
|
131
271
|
|
|
132
272
|
// Sculpt landscape
|
|
@@ -160,7 +300,7 @@ except Exception as e:
|
|
|
160
300
|
weightMapPath?: string;
|
|
161
301
|
blendMode?: 'Weight' | 'Alpha';
|
|
162
302
|
}) {
|
|
163
|
-
|
|
303
|
+
const commands: string[] = [];
|
|
164
304
|
|
|
165
305
|
commands.push(`AddLandscapeLayer ${params.landscapeName} ${params.layerName}`);
|
|
166
306
|
|
|
@@ -172,9 +312,7 @@ except Exception as e:
|
|
|
172
312
|
commands.push(`SetLayerBlendMode ${params.layerName} ${params.blendMode}`);
|
|
173
313
|
}
|
|
174
314
|
|
|
175
|
-
|
|
176
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
177
|
-
}
|
|
315
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
178
316
|
|
|
179
317
|
return { success: true, message: `Layer ${params.layerName} added to landscape` };
|
|
180
318
|
}
|
|
@@ -188,7 +326,7 @@ except Exception as e:
|
|
|
188
326
|
falloffWidth?: number;
|
|
189
327
|
meshPath?: string;
|
|
190
328
|
}) {
|
|
191
|
-
|
|
329
|
+
const commands: string[] = [];
|
|
192
330
|
|
|
193
331
|
commands.push(`CreateLandscapeSpline ${params.landscapeName} ${params.splineName}`);
|
|
194
332
|
|
|
@@ -208,9 +346,7 @@ except Exception as e:
|
|
|
208
346
|
commands.push(`SetSplineMesh ${params.splineName} ${params.meshPath}`);
|
|
209
347
|
}
|
|
210
348
|
|
|
211
|
-
|
|
212
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
213
|
-
}
|
|
349
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
214
350
|
|
|
215
351
|
return { success: true, message: `Landscape spline ${params.splineName} created` };
|
|
216
352
|
}
|
|
@@ -246,7 +382,7 @@ except Exception as e:
|
|
|
246
382
|
forcedLOD?: number;
|
|
247
383
|
lodDistribution?: number;
|
|
248
384
|
}) {
|
|
249
|
-
|
|
385
|
+
const commands: string[] = [];
|
|
250
386
|
|
|
251
387
|
if (params.lodBias !== undefined) {
|
|
252
388
|
commands.push(`SetLandscapeLODBias ${params.landscapeName} ${params.lodBias}`);
|
|
@@ -260,9 +396,7 @@ except Exception as e:
|
|
|
260
396
|
commands.push(`SetLandscapeLODDistribution ${params.landscapeName} ${params.lodDistribution}`);
|
|
261
397
|
}
|
|
262
398
|
|
|
263
|
-
|
|
264
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
265
|
-
}
|
|
399
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
266
400
|
|
|
267
401
|
return { success: true, message: 'Landscape LOD settings updated' };
|
|
268
402
|
}
|
|
@@ -276,7 +410,7 @@ except Exception as e:
|
|
|
276
410
|
maxScale?: number;
|
|
277
411
|
randomRotation?: boolean;
|
|
278
412
|
}) {
|
|
279
|
-
|
|
413
|
+
const commands: string[] = [];
|
|
280
414
|
|
|
281
415
|
commands.push(`CreateLandscapeGrass ${params.landscapeName} ${params.grassType}`);
|
|
282
416
|
|
|
@@ -292,9 +426,7 @@ except Exception as e:
|
|
|
292
426
|
commands.push(`SetGrassRandomRotation ${params.grassType} ${params.randomRotation}`);
|
|
293
427
|
}
|
|
294
428
|
|
|
295
|
-
|
|
296
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
297
|
-
}
|
|
429
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
298
430
|
|
|
299
431
|
return { success: true, message: `Grass type ${params.grassType} created on landscape` };
|
|
300
432
|
}
|
|
@@ -305,7 +437,7 @@ except Exception as e:
|
|
|
305
437
|
collisionMipLevel?: number;
|
|
306
438
|
simpleCollision?: boolean;
|
|
307
439
|
}) {
|
|
308
|
-
|
|
440
|
+
const commands: string[] = [];
|
|
309
441
|
|
|
310
442
|
if (params.collisionMipLevel !== undefined) {
|
|
311
443
|
commands.push(`SetLandscapeCollisionMipLevel ${params.landscapeName} ${params.collisionMipLevel}`);
|
|
@@ -317,9 +449,7 @@ except Exception as e:
|
|
|
317
449
|
|
|
318
450
|
commands.push(`UpdateLandscapeCollision ${params.landscapeName}`);
|
|
319
451
|
|
|
320
|
-
|
|
321
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
322
|
-
}
|
|
452
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
323
453
|
|
|
324
454
|
return { success: true, message: 'Landscape collision updated' };
|
|
325
455
|
}
|
|
@@ -330,7 +460,7 @@ except Exception as e:
|
|
|
330
460
|
targetTriangleCount?: number;
|
|
331
461
|
preserveDetails?: boolean;
|
|
332
462
|
}) {
|
|
333
|
-
|
|
463
|
+
const commands: string[] = [];
|
|
334
464
|
|
|
335
465
|
if (params.targetTriangleCount !== undefined) {
|
|
336
466
|
commands.push(`SetRetopologizeTarget ${params.targetTriangleCount}`);
|
|
@@ -342,9 +472,7 @@ except Exception as e:
|
|
|
342
472
|
|
|
343
473
|
commands.push(`RetopologizeLandscape ${params.landscapeName}`);
|
|
344
474
|
|
|
345
|
-
|
|
346
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
347
|
-
}
|
|
475
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
348
476
|
|
|
349
477
|
return { success: true, message: 'Landscape retopologized' };
|
|
350
478
|
}
|
|
@@ -375,72 +503,101 @@ except Exception as e:
|
|
|
375
503
|
streamingDistance?: number;
|
|
376
504
|
}) {
|
|
377
505
|
try {
|
|
378
|
-
|
|
506
|
+
const pythonScript = `
|
|
379
507
|
import unreal
|
|
508
|
+
import json
|
|
509
|
+
|
|
510
|
+
result = {'success': False, 'error': 'Landscape not found'}
|
|
380
511
|
|
|
381
512
|
try:
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
513
|
+
# Get the landscape actor using modern EditorActorSubsystem
|
|
514
|
+
actors = []
|
|
515
|
+
try:
|
|
516
|
+
actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
|
|
517
|
+
if actor_subsystem and hasattr(actor_subsystem, 'get_all_level_actors'):
|
|
518
|
+
actors = actor_subsystem.get_all_level_actors()
|
|
519
|
+
except Exception:
|
|
520
|
+
actors = []
|
|
521
|
+
landscape = None
|
|
522
|
+
|
|
523
|
+
for actor in actors:
|
|
524
|
+
if actor.get_name() == "${params.landscapeName}" or actor.get_actor_label() == "${params.landscapeName}":
|
|
525
|
+
if isinstance(actor, unreal.LandscapeProxy) or isinstance(actor, unreal.Landscape):
|
|
526
|
+
landscape = actor
|
|
527
|
+
break
|
|
528
|
+
|
|
529
|
+
if landscape:
|
|
530
|
+
changes_made = []
|
|
531
|
+
|
|
532
|
+
# Configure spatial loading (UE 5.6)
|
|
533
|
+
if ${params.enableSpatialLoading !== undefined ? 'True' : 'False'}:
|
|
534
|
+
try:
|
|
535
|
+
landscape.set_editor_property('is_spatially_loaded', ${params.enableSpatialLoading || false})
|
|
536
|
+
changes_made.append("Spatial loading: ${params.enableSpatialLoading}")
|
|
537
|
+
except:
|
|
538
|
+
pass
|
|
539
|
+
|
|
540
|
+
# Set runtime grid (UE 5.6 World Partition)
|
|
541
|
+
if "${params.runtimeGrid || ''}":
|
|
542
|
+
try:
|
|
543
|
+
landscape.set_editor_property('runtime_grid', unreal.Name("${params.runtimeGrid}"))
|
|
544
|
+
changes_made.append("Runtime grid: ${params.runtimeGrid}")
|
|
545
|
+
except:
|
|
546
|
+
pass
|
|
547
|
+
|
|
548
|
+
# Configure data layers (UE 5.6)
|
|
549
|
+
if ${params.dataLayers ? 'True' : 'False'}:
|
|
550
|
+
try:
|
|
551
|
+
# Try modern subsystem first
|
|
552
|
+
try:
|
|
553
|
+
world = None
|
|
554
|
+
editor_subsystem = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
|
|
555
|
+
if editor_subsystem and hasattr(editor_subsystem, 'get_editor_world'):
|
|
556
|
+
world = editor_subsystem.get_editor_world()
|
|
557
|
+
if world is None:
|
|
558
|
+
world = unreal.EditorSubsystemLibrary.get_editor_world()
|
|
559
|
+
except Exception:
|
|
560
|
+
world = unreal.EditorSubsystemLibrary.get_editor_world()
|
|
561
|
+
data_layer_manager = unreal.WorldPartitionBlueprintLibrary.get_data_layer_manager(world)
|
|
562
|
+
if data_layer_manager:
|
|
563
|
+
# Note: Full data layer API requires additional setup
|
|
564
|
+
changes_made.append("Data layers: Requires manual configuration")
|
|
565
|
+
except:
|
|
566
|
+
pass
|
|
567
|
+
|
|
568
|
+
if changes_made:
|
|
569
|
+
result = {
|
|
570
|
+
'success': True,
|
|
571
|
+
'message': 'World Partition configured',
|
|
572
|
+
'changes': changes_made
|
|
573
|
+
}
|
|
394
574
|
else:
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
landscape.set_editor_property('is_spatially_loaded', ${params.enableSpatialLoading || false})
|
|
401
|
-
changes_made.append("Spatial loading: ${params.enableSpatialLoading}")
|
|
402
|
-
except:
|
|
403
|
-
pass
|
|
404
|
-
|
|
405
|
-
# Set runtime grid (UE 5.6 World Partition)
|
|
406
|
-
if "${params.runtimeGrid || ''}":
|
|
407
|
-
try:
|
|
408
|
-
landscape.set_editor_property('runtime_grid', unreal.Name("${params.runtimeGrid}"))
|
|
409
|
-
changes_made.append("Runtime grid: ${params.runtimeGrid}")
|
|
410
|
-
except:
|
|
411
|
-
pass
|
|
412
|
-
|
|
413
|
-
# Configure data layers (UE 5.6)
|
|
414
|
-
if ${params.dataLayers ? 'True' : 'False'}:
|
|
415
|
-
try:
|
|
416
|
-
world = unreal.EditorLevelLibrary.get_editor_world()
|
|
417
|
-
data_layer_manager = unreal.WorldPartitionBlueprintLibrary.get_data_layer_manager(world)
|
|
418
|
-
if data_layer_manager:
|
|
419
|
-
# Note: Full data layer API requires additional setup
|
|
420
|
-
changes_made.append("Data layers: Requires manual configuration")
|
|
421
|
-
except:
|
|
422
|
-
pass
|
|
423
|
-
|
|
424
|
-
if changes_made:
|
|
425
|
-
print('RESULT:{"success": true, "message": "World Partition configured", "changes": ' + str(changes_made).replace("'", '"') + '}')
|
|
426
|
-
else:
|
|
427
|
-
print('RESULT:{"success": false, "error": "No World Partition changes applied"}')
|
|
428
|
-
|
|
575
|
+
result = {
|
|
576
|
+
'success': False,
|
|
577
|
+
'error': 'No World Partition changes applied'
|
|
578
|
+
}
|
|
579
|
+
|
|
429
580
|
except Exception as e:
|
|
430
|
-
|
|
581
|
+
result = {'success': False, 'error': str(e)}
|
|
582
|
+
|
|
583
|
+
print('RESULT:' + json.dumps(result))
|
|
431
584
|
`.trim();
|
|
432
585
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
586
|
+
const response = await this.bridge.executePython(pythonScript);
|
|
587
|
+
const interpreted = interpretStandardResult(response, {
|
|
588
|
+
successMessage: 'World Partition configuration attempted',
|
|
589
|
+
failureMessage: 'World Partition configuration failed'
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
if (interpreted.success) {
|
|
593
|
+
return interpreted.payload as any;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return {
|
|
597
|
+
success: false,
|
|
598
|
+
error: interpreted.error ?? 'World Partition configuration failed',
|
|
599
|
+
details: bestEffortInterpretedText(interpreted)
|
|
600
|
+
};
|
|
444
601
|
} catch (err) {
|
|
445
602
|
return { success: false, error: `Failed to configure World Partition: ${err}` };
|
|
446
603
|
}
|
|
@@ -467,9 +624,7 @@ except Exception as e:
|
|
|
467
624
|
}
|
|
468
625
|
|
|
469
626
|
// Execute commands
|
|
470
|
-
|
|
471
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
472
|
-
}
|
|
627
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
473
628
|
|
|
474
629
|
return {
|
|
475
630
|
success: true,
|
|
@@ -503,9 +658,7 @@ except Exception as e:
|
|
|
503
658
|
commands.push('wp.Runtime.ToggleDrawRuntimeHash2D'); // Show 2D grid
|
|
504
659
|
|
|
505
660
|
try {
|
|
506
|
-
|
|
507
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
508
|
-
}
|
|
661
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
509
662
|
|
|
510
663
|
return {
|
|
511
664
|
success: true,
|