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,115 +1,246 @@
1
+ import { bestEffortInterpretedText, coerceBoolean, coerceString, interpretStandardResult } from '../utils/result-helpers.js';
2
+ import { ensureVector3 } from '../utils/validation.js';
3
+ import { escapePythonString } from '../utils/python.js';
1
4
  export class LandscapeTools {
2
5
  bridge;
3
6
  constructor(bridge) {
4
7
  this.bridge = bridge;
5
8
  }
6
- // Execute console command
7
- async _executeCommand(command) {
8
- return this.bridge.httpCall('/remote/object/call', 'PUT', {
9
- objectPath: '/Script/Engine.Default__KismetSystemLibrary',
10
- functionName: 'ExecuteConsoleCommand',
11
- parameters: {
12
- WorldContextObject: null,
13
- Command: command,
14
- SpecificPlayer: null
15
- },
16
- generateTransaction: false
17
- });
18
- }
19
9
  // Create landscape with World Partition support (UE 5.6)
20
10
  async createLandscape(params) {
21
- // Try Python API with World Partition support for UE 5.6
22
- try {
23
- const pythonScript = `
11
+ const name = params.name?.trim();
12
+ if (!name) {
13
+ return { success: false, error: 'Landscape name is required' };
14
+ }
15
+ if (typeof params.sizeX === 'number' && params.sizeX <= 0) {
16
+ return {
17
+ success: false,
18
+ error: 'Landscape sizeX must be a positive number'
19
+ };
20
+ }
21
+ if (typeof params.sizeY === 'number' && params.sizeY <= 0) {
22
+ return {
23
+ success: false,
24
+ error: 'Landscape sizeY must be a positive number'
25
+ };
26
+ }
27
+ const [locX, locY, locZ] = ensureVector3(params.location ?? [0, 0, 0], 'landscape location');
28
+ const sectionsPerComponent = Math.max(1, Math.floor(params.sectionsPerComponent ?? 1));
29
+ const quadsPerSection = Math.max(1, Math.floor(params.quadsPerSection ?? 63));
30
+ const componentCount = Math.max(1, Math.floor(params.componentCount ?? 1));
31
+ const defaultSize = 1000;
32
+ const scaleX = params.sizeX ? Math.max(0.1, params.sizeX / defaultSize) : 1;
33
+ const scaleY = params.sizeY ? Math.max(0.1, params.sizeY / defaultSize) : 1;
34
+ const escapedName = escapePythonString(name);
35
+ const escapedMaterial = params.materialPath && params.materialPath.trim().length > 0
36
+ ? escapePythonString(params.materialPath.trim())
37
+ : '';
38
+ const runtimeGridFlag = params.runtimeGrid ? 'True' : 'False';
39
+ const spatiallyLoadedFlag = params.isSpatiallyLoaded ? 'True' : 'False';
40
+ const runtimeGridValue = params.runtimeGrid ? escapePythonString(params.runtimeGrid.trim()) : '';
41
+ const dataLayerNames = Array.isArray(params.dataLayers)
42
+ ? params.dataLayers
43
+ .map(layer => layer?.trim())
44
+ .filter((layer) => Boolean(layer))
45
+ .map(layer => escapePythonString(layer))
46
+ : [];
47
+ const pythonScript = `
24
48
  import unreal
25
49
  import json
26
50
 
27
- # Get the editor world using the proper subsystem
51
+ result = {
52
+ "success": False,
53
+ "message": "",
54
+ "error": "",
55
+ "warnings": [],
56
+ "details": [],
57
+ "landscapeName": "",
58
+ "landscapeActor": "",
59
+ "worldPartition": False,
60
+ "runtimeGridRequested": ${runtimeGridFlag},
61
+ "spatiallyLoaded": ${spatiallyLoadedFlag}
62
+ }
63
+
28
64
  try:
29
- editor_subsystem = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
30
- world = editor_subsystem.get_editor_world() if hasattr(editor_subsystem, 'get_editor_world') else None
31
- except:
32
- # Fallback for older API
65
+ editor_subsystem = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
66
+ world = editor_subsystem.get_editor_world() if editor_subsystem and hasattr(editor_subsystem, 'get_editor_world') else None
67
+ data_layer_manager = None
68
+ if world:
33
69
  try:
34
- world = unreal.EditorLevelLibrary.get_editor_world()
35
- except:
36
- world = None
70
+ world_partition = world.get_world_partition()
71
+ result["worldPartition"] = world_partition is not None
72
+ if result["worldPartition"] and hasattr(unreal, "WorldPartitionBlueprintLibrary"):
73
+ data_layer_manager = unreal.WorldPartitionBlueprintLibrary.get_data_layer_manager(world)
74
+ except Exception as wp_error:
75
+ result["warnings"].append(f"Failed to inspect world partition: {wp_error}")
37
76
 
38
- is_world_partition = False
39
- data_layer_manager = None
40
-
41
- if world:
77
+ actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
78
+ if not actor_subsystem:
79
+ result["error"] = "EditorActorSubsystem unavailable"
80
+ else:
81
+ existing = None
42
82
  try:
43
- # Check if World Partition is enabled (UE 5.6)
44
- world_partition = world.get_world_partition()
45
- is_world_partition = world_partition is not None
46
- if is_world_partition:
47
- # Get Data Layer Manager for UE 5.6
48
- data_layer_manager = unreal.WorldPartitionBlueprintLibrary.get_data_layer_manager(world)
49
- except:
50
- pass
83
+ for actor in actor_subsystem.get_all_level_actors():
84
+ if actor and actor.get_actor_label() == "${escapedName}":
85
+ existing = actor
86
+ break
87
+ except Exception as scan_error:
88
+ result["warnings"].append(f"Actor scan failed: {scan_error}")
51
89
 
52
- # Try to create a basic landscape using available API
53
- try:
54
- # Use EditorLevelLibrary to spawn a landscape actor
55
- location = unreal.Vector(${params.location?.[0] || 0}, ${params.location?.[1] || 0}, ${params.location?.[2] || 0})
56
- rotation = unreal.Rotator(0, 0, 0)
57
-
58
- # Check if LandscapeSubsystem is available (not LandscapeEditorSubsystem)
59
- landscape_subsystem = None
60
- try:
61
- landscape_subsystem = unreal.get_editor_subsystem(unreal.LandscapeSubsystem)
62
- except:
90
+ if existing:
91
+ result["success"] = True
92
+ result["message"] = "Landscape already exists"
93
+ result["landscapeName"] = existing.get_actor_label()
94
+ try:
95
+ result["landscapeActor"] = existing.get_path_name()
96
+ except Exception:
63
97
  pass
64
-
65
- if landscape_subsystem:
66
- # Use subsystem if available
67
- result = {"success": False, "error": "LandscapeSubsystem API limited via Python", "world_partition": is_world_partition}
68
98
  else:
69
- # Landscape actors cannot be properly spawned via Python API
70
- # The component registration issues are inherent to how landscapes work
71
- # Direct users to the proper workflow
72
- result = {
73
- "success": False,
74
- "error": "Landscape creation is not supported via Python API",
75
- "suggestion": "Please use Landscape Mode in the Unreal Editor toolbar:",
76
- "steps": [
77
- "1. Click 'Modes' dropdown in toolbar",
78
- "2. Select 'Landscape'",
79
- "3. Configure size and materials",
80
- "4. Click 'Create' to generate landscape"
81
- ],
82
- "world_partition": is_world_partition
83
- }
84
-
85
- print(f'RESULT:{json.dumps(result)}')
99
+ landscape_class = getattr(unreal, "Landscape", None)
100
+ if not landscape_class:
101
+ result["error"] = "Landscape class unavailable"
102
+ else:
103
+ location = unreal.Vector(${locX}, ${locY}, ${locZ})
104
+ rotation = unreal.Rotator(0.0, 0.0, 0.0)
105
+ landscape_actor = actor_subsystem.spawn_actor_from_class(landscape_class, location, rotation)
106
+ if not landscape_actor:
107
+ result["error"] = "Failed to spawn landscape actor"
108
+ else:
109
+ try:
110
+ landscape_actor.set_actor_label("${escapedName}", True)
111
+ except TypeError:
112
+ landscape_actor.set_actor_label("${escapedName}")
113
+ except Exception as label_error:
114
+ result["warnings"].append(f"Failed to set landscape label: {label_error}")
115
+
116
+ try:
117
+ landscape_actor.set_actor_scale3d(unreal.Vector(${scaleX.toFixed(4)}, ${scaleY.toFixed(4)}, 1.0))
118
+ result["details"].append(f"Actor scale set to (${scaleX.toFixed(2)}, ${scaleY.toFixed(2)}, 1.0)")
119
+ except Exception as scale_error:
120
+ result["warnings"].append(f"Failed to set landscape scale: {scale_error}")
121
+
122
+ landscape_editor = None
123
+ try:
124
+ landscape_editor = unreal.get_editor_subsystem(unreal.LandscapeEditorSubsystem)
125
+ except Exception as editor_error:
126
+ result["warnings"].append(f"LandscapeEditorSubsystem unavailable: {editor_error}")
127
+
128
+ if landscape_editor:
129
+ try:
130
+ landscape_editor.set_component_size(${sectionsPerComponent}, ${quadsPerSection})
131
+ landscape_editor.set_component_count(${componentCount}, ${componentCount})
132
+ result["details"].append(f"Component size ${sectionsPerComponent}x${quadsPerSection}, count ${componentCount}x${componentCount}")
133
+ except Exception as config_error:
134
+ result["warnings"].append(f"Landscape configuration limited: {config_error}")
135
+
136
+ ${escapedMaterial ? `try:
137
+ material = unreal.EditorAssetLibrary.load_asset("${escapedMaterial}")
138
+ if material:
139
+ try:
140
+ landscape_actor.set_landscape_material(material)
141
+ except Exception:
142
+ landscape_actor.editor_set_landscape_material(material)
143
+ result["details"].append("Landscape material applied")
144
+ else:
145
+ result["warnings"].append("Landscape material asset not found: ${escapedMaterial}")
146
+ except Exception as material_error:
147
+ result["warnings"].append(f"Failed to apply landscape material: {material_error}")
148
+ ` : ''}
149
+ ${runtimeGridValue ? `if result["worldPartition"] and hasattr(unreal, "WorldPartitionBlueprintLibrary"):
150
+ try:
151
+ unreal.WorldPartitionBlueprintLibrary.set_actor_runtime_grid(landscape_actor, "${runtimeGridValue}")
152
+ result["details"].append("Runtime grid assigned: ${runtimeGridValue}")
153
+ except Exception as grid_error:
154
+ result["warnings"].append(f"Failed to assign runtime grid: {grid_error}")
155
+ ` : ''}
156
+ ${params.isSpatiallyLoaded ? `if result["worldPartition"] and hasattr(unreal, "WorldPartitionBlueprintLibrary"):
157
+ try:
158
+ unreal.WorldPartitionBlueprintLibrary.set_actor_spatially_loaded(landscape_actor, True)
159
+ result["details"].append("Actor marked as spatially loaded")
160
+ except Exception as spatial_error:
161
+ result["warnings"].append(f"Failed to mark as spatially loaded: {spatial_error}")
162
+ ` : ''}
163
+ ${dataLayerNames.length ? `if result["worldPartition"] and data_layer_manager:
164
+ for layer_name in ${JSON.stringify(dataLayerNames)}:
165
+ try:
166
+ data_layer = data_layer_manager.get_data_layer(layer_name)
167
+ if data_layer:
168
+ unreal.WorldPartitionBlueprintLibrary.add_actor_to_data_layer(landscape_actor, data_layer)
169
+ result["details"].append(f"Added to data layer {layer_name}")
170
+ else:
171
+ result["warnings"].append(f"Data layer not found: {layer_name}")
172
+ except Exception as data_layer_error:
173
+ result["warnings"].append(f"Failed to assign data layer {layer_name}: {data_layer_error}")
174
+ ` : ''}
175
+
176
+ try:
177
+ result["landscapeName"] = landscape_actor.get_actor_label()
178
+ result["landscapeActor"] = landscape_actor.get_path_name()
179
+ except Exception:
180
+ pass
181
+
182
+ result["success"] = True
183
+ result["message"] = "Landscape actor created"
86
184
  except Exception as e:
87
- print(f'RESULT:{{"success": false, "error": "{str(e)}"}}')
185
+ result["error"] = str(e)
186
+
187
+ if result.get("success"):
188
+ result.pop("error", None)
189
+ else:
190
+ if not result.get("error"):
191
+ result["error"] = "Failed to create landscape actor"
192
+ if not result.get("message"):
193
+ result["message"] = result["error"]
194
+
195
+ if not result.get("warnings"):
196
+ result.pop("warnings", None)
197
+ if not result.get("details"):
198
+ result.pop("details", None)
199
+
200
+ print("RESULT:" + json.dumps(result))
88
201
  `.trim();
202
+ try {
89
203
  const response = await this.bridge.executePython(pythonScript);
90
- const output = typeof response === 'string' ? response : JSON.stringify(response);
91
- const match = output.match(/RESULT:({.*})/);
92
- if (match) {
93
- try {
94
- const result = JSON.parse(match[1]);
95
- if (result.world_partition) {
96
- result.message = 'World Partition detected. Manual landscape creation required in editor.';
97
- }
98
- return result;
99
- }
100
- catch { }
204
+ const interpreted = interpretStandardResult(response, {
205
+ successMessage: 'Landscape actor created',
206
+ failureMessage: 'Failed to create landscape actor'
207
+ });
208
+ if (!interpreted.success) {
209
+ return {
210
+ success: false,
211
+ error: interpreted.error || interpreted.message
212
+ };
213
+ }
214
+ const result = {
215
+ success: true,
216
+ message: interpreted.message,
217
+ landscapeName: coerceString(interpreted.payload.landscapeName) ?? name,
218
+ worldPartition: coerceBoolean(interpreted.payload.worldPartition)
219
+ };
220
+ const actorPath = coerceString(interpreted.payload.landscapeActor);
221
+ if (actorPath) {
222
+ result.landscapeActor = actorPath;
101
223
  }
224
+ if (interpreted.warnings?.length) {
225
+ result.warnings = interpreted.warnings;
226
+ }
227
+ if (interpreted.details?.length) {
228
+ result.details = interpreted.details;
229
+ }
230
+ if (params.runtimeGrid) {
231
+ result.runtimeGrid = params.runtimeGrid;
232
+ }
233
+ if (typeof params.isSpatiallyLoaded === 'boolean') {
234
+ result.spatiallyLoaded = params.isSpatiallyLoaded;
235
+ }
236
+ return result;
102
237
  }
103
- catch {
104
- // Continue to fallback
238
+ catch (error) {
239
+ return {
240
+ success: false,
241
+ error: `Failed to create landscape actor: ${error}`
242
+ };
105
243
  }
106
- // Fallback message with World Partition info
107
- return {
108
- success: false,
109
- error: 'Landscape creation via API is limited. Please use the Unreal Editor UI to create landscapes.',
110
- worldPartitionSupport: params.enableWorldPartition ? 'Requested' : 'Not requested',
111
- suggestion: 'Use the Landscape Mode in the editor toolbar to create and configure landscapes'
112
- };
113
244
  }
114
245
  // Sculpt landscape
115
246
  async sculptLandscape(_params) {
@@ -129,9 +260,7 @@ except Exception as e:
129
260
  if (params.blendMode) {
130
261
  commands.push(`SetLayerBlendMode ${params.layerName} ${params.blendMode}`);
131
262
  }
132
- for (const cmd of commands) {
133
- await this.bridge.executeConsoleCommand(cmd);
134
- }
263
+ await this.bridge.executeConsoleCommands(commands);
135
264
  return { success: true, message: `Layer ${params.layerName} added to landscape` };
136
265
  }
137
266
  // Create landscape spline
@@ -150,9 +279,7 @@ except Exception as e:
150
279
  if (params.meshPath) {
151
280
  commands.push(`SetSplineMesh ${params.splineName} ${params.meshPath}`);
152
281
  }
153
- for (const cmd of commands) {
154
- await this.bridge.executeConsoleCommand(cmd);
155
- }
282
+ await this.bridge.executeConsoleCommands(commands);
156
283
  return { success: true, message: `Landscape spline ${params.splineName} created` };
157
284
  }
158
285
  // Import heightmap
@@ -179,9 +306,7 @@ except Exception as e:
179
306
  if (params.lodDistribution !== undefined) {
180
307
  commands.push(`SetLandscapeLODDistribution ${params.landscapeName} ${params.lodDistribution}`);
181
308
  }
182
- for (const cmd of commands) {
183
- await this.bridge.executeConsoleCommand(cmd);
184
- }
309
+ await this.bridge.executeConsoleCommands(commands);
185
310
  return { success: true, message: 'Landscape LOD settings updated' };
186
311
  }
187
312
  // Create landscape grass
@@ -197,9 +322,7 @@ except Exception as e:
197
322
  if (params.randomRotation !== undefined) {
198
323
  commands.push(`SetGrassRandomRotation ${params.grassType} ${params.randomRotation}`);
199
324
  }
200
- for (const cmd of commands) {
201
- await this.bridge.executeConsoleCommand(cmd);
202
- }
325
+ await this.bridge.executeConsoleCommands(commands);
203
326
  return { success: true, message: `Grass type ${params.grassType} created on landscape` };
204
327
  }
205
328
  // Landscape collision
@@ -212,9 +335,7 @@ except Exception as e:
212
335
  commands.push(`SetLandscapeSimpleCollision ${params.landscapeName} ${params.simpleCollision}`);
213
336
  }
214
337
  commands.push(`UpdateLandscapeCollision ${params.landscapeName}`);
215
- for (const cmd of commands) {
216
- await this.bridge.executeConsoleCommand(cmd);
217
- }
338
+ await this.bridge.executeConsoleCommands(commands);
218
339
  return { success: true, message: 'Landscape collision updated' };
219
340
  }
220
341
  // Retopologize landscape
@@ -227,9 +348,7 @@ except Exception as e:
227
348
  commands.push(`SetRetopologizePreserveDetails ${params.preserveDetails}`);
228
349
  }
229
350
  commands.push(`RetopologizeLandscape ${params.landscapeName}`);
230
- for (const cmd of commands) {
231
- await this.bridge.executeConsoleCommand(cmd);
232
- }
351
+ await this.bridge.executeConsoleCommands(commands);
233
352
  return { success: true, message: 'Landscape retopologized' };
234
353
  }
235
354
  // Create water body
@@ -245,68 +364,96 @@ except Exception as e:
245
364
  try {
246
365
  const pythonScript = `
247
366
  import unreal
367
+ import json
368
+
369
+ result = {'success': False, 'error': 'Landscape not found'}
248
370
 
249
371
  try:
250
- # Get the landscape actor
251
- actors = unreal.EditorLevelLibrary.get_all_level_actors()
252
- landscape = None
253
-
254
- for actor in actors:
255
- if actor.get_name() == "${params.landscapeName}" or actor.get_actor_label() == "${params.landscapeName}":
256
- if isinstance(actor, unreal.LandscapeProxy) or isinstance(actor, unreal.Landscape):
257
- landscape = actor
258
- break
259
-
260
- if not landscape:
261
- print('RESULT:{"success": false, "error": "Landscape not found"}')
372
+ # Get the landscape actor using modern EditorActorSubsystem
373
+ actors = []
374
+ try:
375
+ actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
376
+ if actor_subsystem and hasattr(actor_subsystem, 'get_all_level_actors'):
377
+ actors = actor_subsystem.get_all_level_actors()
378
+ except Exception:
379
+ actors = []
380
+ landscape = None
381
+
382
+ for actor in actors:
383
+ if actor.get_name() == "${params.landscapeName}" or actor.get_actor_label() == "${params.landscapeName}":
384
+ if isinstance(actor, unreal.LandscapeProxy) or isinstance(actor, unreal.Landscape):
385
+ landscape = actor
386
+ break
387
+
388
+ if landscape:
389
+ changes_made = []
390
+
391
+ # Configure spatial loading (UE 5.6)
392
+ if ${params.enableSpatialLoading !== undefined ? 'True' : 'False'}:
393
+ try:
394
+ landscape.set_editor_property('is_spatially_loaded', ${params.enableSpatialLoading || false})
395
+ changes_made.append("Spatial loading: ${params.enableSpatialLoading}")
396
+ except:
397
+ pass
398
+
399
+ # Set runtime grid (UE 5.6 World Partition)
400
+ if "${params.runtimeGrid || ''}":
401
+ try:
402
+ landscape.set_editor_property('runtime_grid', unreal.Name("${params.runtimeGrid}"))
403
+ changes_made.append("Runtime grid: ${params.runtimeGrid}")
404
+ except:
405
+ pass
406
+
407
+ # Configure data layers (UE 5.6)
408
+ if ${params.dataLayers ? 'True' : 'False'}:
409
+ try:
410
+ # Try modern subsystem first
411
+ try:
412
+ world = None
413
+ editor_subsystem = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
414
+ if editor_subsystem and hasattr(editor_subsystem, 'get_editor_world'):
415
+ world = editor_subsystem.get_editor_world()
416
+ if world is None:
417
+ world = unreal.EditorSubsystemLibrary.get_editor_world()
418
+ except Exception:
419
+ world = unreal.EditorSubsystemLibrary.get_editor_world()
420
+ data_layer_manager = unreal.WorldPartitionBlueprintLibrary.get_data_layer_manager(world)
421
+ if data_layer_manager:
422
+ # Note: Full data layer API requires additional setup
423
+ changes_made.append("Data layers: Requires manual configuration")
424
+ except:
425
+ pass
426
+
427
+ if changes_made:
428
+ result = {
429
+ 'success': True,
430
+ 'message': 'World Partition configured',
431
+ 'changes': changes_made
432
+ }
262
433
  else:
263
- changes_made = []
264
-
265
- # Configure spatial loading (UE 5.6)
266
- if ${params.enableSpatialLoading !== undefined ? 'True' : 'False'}:
267
- try:
268
- landscape.set_editor_property('is_spatially_loaded', ${params.enableSpatialLoading || false})
269
- changes_made.append("Spatial loading: ${params.enableSpatialLoading}")
270
- except:
271
- pass
272
-
273
- # Set runtime grid (UE 5.6 World Partition)
274
- if "${params.runtimeGrid || ''}":
275
- try:
276
- landscape.set_editor_property('runtime_grid', unreal.Name("${params.runtimeGrid}"))
277
- changes_made.append("Runtime grid: ${params.runtimeGrid}")
278
- except:
279
- pass
280
-
281
- # Configure data layers (UE 5.6)
282
- if ${params.dataLayers ? 'True' : 'False'}:
283
- try:
284
- world = unreal.EditorLevelLibrary.get_editor_world()
285
- data_layer_manager = unreal.WorldPartitionBlueprintLibrary.get_data_layer_manager(world)
286
- if data_layer_manager:
287
- # Note: Full data layer API requires additional setup
288
- changes_made.append("Data layers: Requires manual configuration")
289
- except:
290
- pass
291
-
292
- if changes_made:
293
- print('RESULT:{"success": true, "message": "World Partition configured", "changes": ' + str(changes_made).replace("'", '"') + '}')
294
- else:
295
- print('RESULT:{"success": false, "error": "No World Partition changes applied"}')
296
-
434
+ result = {
435
+ 'success': False,
436
+ 'error': 'No World Partition changes applied'
437
+ }
438
+
297
439
  except Exception as e:
298
- print(f'RESULT:{{"success": false, "error": "{str(e)}"}}')
440
+ result = {'success': False, 'error': str(e)}
441
+
442
+ print('RESULT:' + json.dumps(result))
299
443
  `.trim();
300
444
  const response = await this.bridge.executePython(pythonScript);
301
- const output = typeof response === 'string' ? response : JSON.stringify(response);
302
- const match = output.match(/RESULT:({.*})/);
303
- if (match) {
304
- try {
305
- return JSON.parse(match[1]);
306
- }
307
- catch { }
445
+ const interpreted = interpretStandardResult(response, {
446
+ successMessage: 'World Partition configuration attempted',
447
+ failureMessage: 'World Partition configuration failed'
448
+ });
449
+ if (interpreted.success) {
450
+ return interpreted.payload;
308
451
  }
309
- return { success: true, message: 'World Partition configuration attempted' };
452
+ return {
453
+ success: false,
454
+ error: interpreted.error ?? 'World Partition configuration failed',
455
+ details: bestEffortInterpretedText(interpreted)
456
+ };
310
457
  }
311
458
  catch (err) {
312
459
  return { success: false, error: `Failed to configure World Partition: ${err}` };
@@ -328,9 +475,7 @@ except Exception as e:
328
475
  }
329
476
  }
330
477
  // Execute commands
331
- for (const cmd of commands) {
332
- await this.bridge.executeConsoleCommand(cmd);
333
- }
478
+ await this.bridge.executeConsoleCommands(commands);
334
479
  return {
335
480
  success: true,
336
481
  message: `Data layers ${params.operation === 'add' ? 'added' : params.operation === 'remove' ? 'removed' : 'set'} for landscape`,
@@ -354,9 +499,7 @@ except Exception as e:
354
499
  // Debug visualization commands
355
500
  commands.push('wp.Runtime.ToggleDrawRuntimeHash2D'); // Show 2D grid
356
501
  try {
357
- for (const cmd of commands) {
358
- await this.bridge.executeConsoleCommand(cmd);
359
- }
502
+ await this.bridge.executeConsoleCommands(commands);
360
503
  return {
361
504
  success: true,
362
505
  message: 'Streaming cells configured for World Partition',
@@ -2,7 +2,6 @@ import { UnrealBridge } from '../unreal-bridge.js';
2
2
  export declare class LevelTools {
3
3
  private bridge;
4
4
  constructor(bridge: UnrealBridge);
5
- private _executeCommand;
6
5
  loadLevel(params: {
7
6
  levelPath: string;
8
7
  streaming?: boolean;
@@ -11,28 +10,12 @@ export declare class LevelTools {
11
10
  saveLevel(_params: {
12
11
  levelName?: string;
13
12
  savePath?: string;
14
- }): Promise<{
15
- success: boolean;
16
- message: string;
17
- error?: undefined;
18
- } | {
19
- success: boolean;
20
- error: any;
21
- message?: undefined;
22
- }>;
13
+ }): Promise<Record<string, unknown>>;
23
14
  createLevel(params: {
24
15
  levelName: string;
25
16
  template?: 'Empty' | 'Default' | 'VR' | 'TimeOfDay';
26
17
  savePath?: string;
27
- }): Promise<{
28
- success: boolean;
29
- message: any;
30
- error?: undefined;
31
- } | {
32
- success: boolean;
33
- error: any;
34
- message?: undefined;
35
- }>;
18
+ }): Promise<Record<string, unknown>>;
36
19
  streamLevel(params: {
37
20
  levelName: string;
38
21
  shouldBeLoaded: boolean;
@@ -79,15 +62,7 @@ export declare class LevelTools {
79
62
  buildNavMesh(params: {
80
63
  rebuildAll?: boolean;
81
64
  selectedOnly?: boolean;
82
- }): Promise<{
83
- success: boolean;
84
- message: any;
85
- error?: undefined;
86
- } | {
87
- success: boolean;
88
- error: any;
89
- message?: undefined;
90
- }>;
65
+ }): Promise<Record<string, unknown>>;
91
66
  setLevelVisibility(params: {
92
67
  levelName: string;
93
68
  visible: boolean;