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.
Files changed (135) hide show
  1. package/.env.production +1 -1
  2. package/.github/copilot-instructions.md +45 -0
  3. package/.github/workflows/publish-mcp.yml +3 -2
  4. package/README.md +21 -5
  5. package/dist/index.js +124 -31
  6. package/dist/prompts/index.d.ts +10 -3
  7. package/dist/prompts/index.js +186 -7
  8. package/dist/resources/actors.d.ts +19 -1
  9. package/dist/resources/actors.js +55 -64
  10. package/dist/resources/assets.js +46 -62
  11. package/dist/resources/levels.d.ts +21 -3
  12. package/dist/resources/levels.js +29 -54
  13. package/dist/tools/actors.d.ts +3 -14
  14. package/dist/tools/actors.js +246 -302
  15. package/dist/tools/animation.d.ts +57 -102
  16. package/dist/tools/animation.js +429 -450
  17. package/dist/tools/assets.d.ts +13 -2
  18. package/dist/tools/assets.js +52 -44
  19. package/dist/tools/audio.d.ts +22 -13
  20. package/dist/tools/audio.js +467 -121
  21. package/dist/tools/blueprint.d.ts +32 -13
  22. package/dist/tools/blueprint.js +699 -448
  23. package/dist/tools/build_environment_advanced.d.ts +0 -1
  24. package/dist/tools/build_environment_advanced.js +190 -45
  25. package/dist/tools/consolidated-tool-definitions.js +78 -252
  26. package/dist/tools/consolidated-tool-handlers.js +506 -133
  27. package/dist/tools/debug.d.ts +72 -10
  28. package/dist/tools/debug.js +167 -31
  29. package/dist/tools/editor.d.ts +9 -2
  30. package/dist/tools/editor.js +30 -44
  31. package/dist/tools/foliage.d.ts +34 -15
  32. package/dist/tools/foliage.js +97 -107
  33. package/dist/tools/introspection.js +19 -21
  34. package/dist/tools/landscape.d.ts +1 -2
  35. package/dist/tools/landscape.js +311 -168
  36. package/dist/tools/level.d.ts +3 -28
  37. package/dist/tools/level.js +642 -192
  38. package/dist/tools/lighting.d.ts +14 -3
  39. package/dist/tools/lighting.js +236 -123
  40. package/dist/tools/materials.d.ts +25 -7
  41. package/dist/tools/materials.js +102 -79
  42. package/dist/tools/niagara.d.ts +10 -12
  43. package/dist/tools/niagara.js +74 -94
  44. package/dist/tools/performance.d.ts +12 -4
  45. package/dist/tools/performance.js +38 -79
  46. package/dist/tools/physics.d.ts +34 -10
  47. package/dist/tools/physics.js +364 -292
  48. package/dist/tools/rc.js +97 -23
  49. package/dist/tools/sequence.d.ts +1 -0
  50. package/dist/tools/sequence.js +125 -22
  51. package/dist/tools/ui.d.ts +31 -4
  52. package/dist/tools/ui.js +83 -66
  53. package/dist/tools/visual.d.ts +11 -0
  54. package/dist/tools/visual.js +245 -30
  55. package/dist/types/tool-types.d.ts +0 -6
  56. package/dist/types/tool-types.js +1 -8
  57. package/dist/unreal-bridge.d.ts +32 -2
  58. package/dist/unreal-bridge.js +621 -127
  59. package/dist/utils/elicitation.d.ts +57 -0
  60. package/dist/utils/elicitation.js +104 -0
  61. package/dist/utils/error-handler.d.ts +0 -33
  62. package/dist/utils/error-handler.js +4 -111
  63. package/dist/utils/http.d.ts +2 -22
  64. package/dist/utils/http.js +12 -75
  65. package/dist/utils/normalize.d.ts +4 -4
  66. package/dist/utils/normalize.js +15 -7
  67. package/dist/utils/python-output.d.ts +18 -0
  68. package/dist/utils/python-output.js +290 -0
  69. package/dist/utils/python.d.ts +2 -0
  70. package/dist/utils/python.js +4 -0
  71. package/dist/utils/response-validator.js +28 -2
  72. package/dist/utils/result-helpers.d.ts +27 -0
  73. package/dist/utils/result-helpers.js +147 -0
  74. package/dist/utils/safe-json.d.ts +0 -2
  75. package/dist/utils/safe-json.js +0 -43
  76. package/dist/utils/validation.d.ts +16 -0
  77. package/dist/utils/validation.js +70 -7
  78. package/mcp-config-example.json +2 -2
  79. package/package.json +10 -9
  80. package/server.json +37 -14
  81. package/src/index.ts +130 -33
  82. package/src/prompts/index.ts +211 -13
  83. package/src/resources/actors.ts +59 -44
  84. package/src/resources/assets.ts +48 -51
  85. package/src/resources/levels.ts +35 -45
  86. package/src/tools/actors.ts +269 -313
  87. package/src/tools/animation.ts +556 -539
  88. package/src/tools/assets.ts +53 -43
  89. package/src/tools/audio.ts +507 -113
  90. package/src/tools/blueprint.ts +778 -462
  91. package/src/tools/build_environment_advanced.ts +266 -64
  92. package/src/tools/consolidated-tool-definitions.ts +90 -264
  93. package/src/tools/consolidated-tool-handlers.ts +630 -121
  94. package/src/tools/debug.ts +176 -33
  95. package/src/tools/editor.ts +35 -37
  96. package/src/tools/foliage.ts +110 -104
  97. package/src/tools/introspection.ts +24 -22
  98. package/src/tools/landscape.ts +334 -181
  99. package/src/tools/level.ts +683 -182
  100. package/src/tools/lighting.ts +244 -123
  101. package/src/tools/materials.ts +114 -83
  102. package/src/tools/niagara.ts +87 -81
  103. package/src/tools/performance.ts +49 -88
  104. package/src/tools/physics.ts +393 -299
  105. package/src/tools/rc.ts +102 -24
  106. package/src/tools/sequence.ts +136 -28
  107. package/src/tools/ui.ts +101 -70
  108. package/src/tools/visual.ts +250 -29
  109. package/src/types/tool-types.ts +0 -9
  110. package/src/unreal-bridge.ts +658 -140
  111. package/src/utils/elicitation.ts +129 -0
  112. package/src/utils/error-handler.ts +4 -159
  113. package/src/utils/http.ts +16 -115
  114. package/src/utils/normalize.ts +20 -10
  115. package/src/utils/python-output.ts +351 -0
  116. package/src/utils/python.ts +3 -0
  117. package/src/utils/response-validator.ts +25 -2
  118. package/src/utils/result-helpers.ts +193 -0
  119. package/src/utils/safe-json.ts +0 -50
  120. package/src/utils/validation.ts +94 -7
  121. package/tests/run-unreal-tool-tests.mjs +720 -0
  122. package/tsconfig.json +2 -2
  123. package/dist/python-utils.d.ts +0 -29
  124. package/dist/python-utils.js +0 -54
  125. package/dist/types/index.d.ts +0 -323
  126. package/dist/types/index.js +0 -28
  127. package/dist/utils/cache-manager.d.ts +0 -64
  128. package/dist/utils/cache-manager.js +0 -176
  129. package/dist/utils/errors.d.ts +0 -133
  130. package/dist/utils/errors.js +0 -256
  131. package/src/python/editor_compat.py +0 -181
  132. package/src/python-utils.ts +0 -57
  133. package/src/types/index.ts +0 -414
  134. package/src/utils/cache-manager.ts +0 -213
  135. package/src/utils/errors.ts +0 -312
@@ -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
- // Try Python API with World Partition support for UE 5.6
38
- try {
39
- const pythonScript = `
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
- # Get the editor world using the proper subsystem
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
- editor_subsystem = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
46
- world = editor_subsystem.get_editor_world() if hasattr(editor_subsystem, 'get_editor_world') else None
47
- except:
48
- # Fallback for older API
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
- world = unreal.EditorLevelLibrary.get_editor_world()
51
- except:
52
- world = None
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
- is_world_partition = False
55
- data_layer_manager = None
56
-
57
- if world:
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
- # Check if World Partition is enabled (UE 5.6)
60
- world_partition = world.get_world_partition()
61
- is_world_partition = world_partition is not None
62
- if is_world_partition:
63
- # Get Data Layer Manager for UE 5.6
64
- data_layer_manager = unreal.WorldPartitionBlueprintLibrary.get_data_layer_manager(world)
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
- # Try to create a basic landscape using available API
69
- try:
70
- # Use EditorLevelLibrary to spawn a landscape actor
71
- location = unreal.Vector(${params.location?.[0] || 0}, ${params.location?.[1] || 0}, ${params.location?.[2] || 0})
72
- rotation = unreal.Rotator(0, 0, 0)
73
-
74
- # Check if LandscapeSubsystem is available (not LandscapeEditorSubsystem)
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
- # Landscape actors cannot be properly spawned via Python API
86
- # The component registration issues are inherent to how landscapes work
87
- # Direct users to the proper workflow
88
- result = {
89
- "success": False,
90
- "error": "Landscape creation is not supported via Python API",
91
- "suggestion": "Please use Landscape Mode in the Unreal Editor toolbar:",
92
- "steps": [
93
- "1. Click 'Modes' dropdown in toolbar",
94
- "2. Select 'Landscape'",
95
- "3. Configure size and materials",
96
- "4. Click 'Create' to generate landscape"
97
- ],
98
- "world_partition": is_world_partition
99
- }
100
-
101
- print(f'RESULT:{json.dumps(result)}')
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
- print(f'RESULT:{{"success": false, "error": "{str(e)}"}}')
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 output = typeof response === 'string' ? response : JSON.stringify(response);
108
- const match = output.match(/RESULT:({.*})/);
109
-
110
- if (match) {
111
- try {
112
- const result = JSON.parse(match[1]);
113
- if (result.world_partition) {
114
- result.message = 'World Partition detected. Manual landscape creation required in editor.';
115
- }
116
- return result;
117
- } catch {}
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
- } catch {
120
- // Continue to fallback
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
- const commands = [];
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
- for (const cmd of commands) {
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
- const commands = [];
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
- for (const cmd of commands) {
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
- const commands = [];
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
- for (const cmd of commands) {
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
- const commands = [];
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
- for (const cmd of commands) {
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
- const commands = [];
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
- for (const cmd of commands) {
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
- const commands = [];
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
- for (const cmd of commands) {
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
- const pythonScript = `
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
- # Get the landscape actor
383
- actors = unreal.EditorLevelLibrary.get_all_level_actors()
384
- landscape = None
385
-
386
- for actor in actors:
387
- if actor.get_name() == "${params.landscapeName}" or actor.get_actor_label() == "${params.landscapeName}":
388
- if isinstance(actor, unreal.LandscapeProxy) or isinstance(actor, unreal.Landscape):
389
- landscape = actor
390
- break
391
-
392
- if not landscape:
393
- print('RESULT:{"success": false, "error": "Landscape not found"}')
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
- changes_made = []
396
-
397
- # Configure spatial loading (UE 5.6)
398
- if ${params.enableSpatialLoading !== undefined ? 'True' : 'False'}:
399
- try:
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
- print(f'RESULT:{{"success": false, "error": "{str(e)}"}}')
581
+ result = {'success': False, 'error': str(e)}
582
+
583
+ print('RESULT:' + json.dumps(result))
431
584
  `.trim();
432
585
 
433
- const response = await this.bridge.executePython(pythonScript);
434
- const output = typeof response === 'string' ? response : JSON.stringify(response);
435
- const match = output.match(/RESULT:({.*})/);
436
-
437
- if (match) {
438
- try {
439
- return JSON.parse(match[1]);
440
- } catch {}
441
- }
442
-
443
- return { success: true, message: 'World Partition configuration attempted' };
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
- for (const cmd of commands) {
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
- for (const cmd of commands) {
507
- await this.bridge.executeConsoleCommand(cmd);
508
- }
661
+ await this.bridge.executeConsoleCommands(commands);
509
662
 
510
663
  return {
511
664
  success: true,