unreal-engine-mcp-server 0.3.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/.env.production +1 -1
  2. package/.github/copilot-instructions.md +45 -0
  3. package/.github/workflows/publish-mcp.yml +1 -1
  4. package/README.md +22 -7
  5. package/dist/index.js +137 -46
  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.d.ts +3 -2
  11. package/dist/resources/assets.js +117 -109
  12. package/dist/resources/levels.d.ts +21 -3
  13. package/dist/resources/levels.js +31 -56
  14. package/dist/tools/actors.d.ts +3 -14
  15. package/dist/tools/actors.js +246 -302
  16. package/dist/tools/animation.d.ts +57 -102
  17. package/dist/tools/animation.js +429 -450
  18. package/dist/tools/assets.d.ts +13 -2
  19. package/dist/tools/assets.js +58 -46
  20. package/dist/tools/audio.d.ts +22 -13
  21. package/dist/tools/audio.js +467 -121
  22. package/dist/tools/blueprint.d.ts +32 -13
  23. package/dist/tools/blueprint.js +699 -448
  24. package/dist/tools/build_environment_advanced.d.ts +0 -1
  25. package/dist/tools/build_environment_advanced.js +236 -87
  26. package/dist/tools/consolidated-tool-definitions.d.ts +232 -15
  27. package/dist/tools/consolidated-tool-definitions.js +124 -255
  28. package/dist/tools/consolidated-tool-handlers.js +749 -766
  29. package/dist/tools/debug.d.ts +72 -10
  30. package/dist/tools/debug.js +170 -36
  31. package/dist/tools/editor.d.ts +9 -2
  32. package/dist/tools/editor.js +30 -44
  33. package/dist/tools/foliage.d.ts +34 -15
  34. package/dist/tools/foliage.js +97 -107
  35. package/dist/tools/introspection.js +19 -21
  36. package/dist/tools/landscape.d.ts +1 -2
  37. package/dist/tools/landscape.js +311 -168
  38. package/dist/tools/level.d.ts +3 -28
  39. package/dist/tools/level.js +642 -192
  40. package/dist/tools/lighting.d.ts +14 -3
  41. package/dist/tools/lighting.js +236 -123
  42. package/dist/tools/materials.d.ts +25 -7
  43. package/dist/tools/materials.js +102 -79
  44. package/dist/tools/niagara.d.ts +10 -12
  45. package/dist/tools/niagara.js +74 -94
  46. package/dist/tools/performance.d.ts +12 -4
  47. package/dist/tools/performance.js +38 -79
  48. package/dist/tools/physics.d.ts +34 -10
  49. package/dist/tools/physics.js +364 -292
  50. package/dist/tools/rc.js +98 -24
  51. package/dist/tools/sequence.d.ts +1 -0
  52. package/dist/tools/sequence.js +146 -24
  53. package/dist/tools/ui.d.ts +31 -4
  54. package/dist/tools/ui.js +83 -66
  55. package/dist/tools/visual.d.ts +11 -0
  56. package/dist/tools/visual.js +245 -30
  57. package/dist/types/tool-types.d.ts +0 -6
  58. package/dist/types/tool-types.js +1 -8
  59. package/dist/unreal-bridge.d.ts +32 -2
  60. package/dist/unreal-bridge.js +621 -127
  61. package/dist/utils/elicitation.d.ts +57 -0
  62. package/dist/utils/elicitation.js +104 -0
  63. package/dist/utils/error-handler.d.ts +0 -33
  64. package/dist/utils/error-handler.js +4 -111
  65. package/dist/utils/http.d.ts +2 -22
  66. package/dist/utils/http.js +12 -75
  67. package/dist/utils/normalize.d.ts +4 -4
  68. package/dist/utils/normalize.js +15 -7
  69. package/dist/utils/python-output.d.ts +18 -0
  70. package/dist/utils/python-output.js +290 -0
  71. package/dist/utils/python.d.ts +2 -0
  72. package/dist/utils/python.js +4 -0
  73. package/dist/utils/response-validator.d.ts +6 -1
  74. package/dist/utils/response-validator.js +66 -13
  75. package/dist/utils/result-helpers.d.ts +27 -0
  76. package/dist/utils/result-helpers.js +147 -0
  77. package/dist/utils/safe-json.d.ts +0 -2
  78. package/dist/utils/safe-json.js +0 -43
  79. package/dist/utils/validation.d.ts +16 -0
  80. package/dist/utils/validation.js +70 -7
  81. package/mcp-config-example.json +2 -2
  82. package/package.json +11 -10
  83. package/server.json +37 -14
  84. package/src/index.ts +146 -50
  85. package/src/prompts/index.ts +211 -13
  86. package/src/resources/actors.ts +59 -44
  87. package/src/resources/assets.ts +123 -102
  88. package/src/resources/levels.ts +37 -47
  89. package/src/tools/actors.ts +269 -313
  90. package/src/tools/animation.ts +556 -539
  91. package/src/tools/assets.ts +59 -45
  92. package/src/tools/audio.ts +507 -113
  93. package/src/tools/blueprint.ts +778 -462
  94. package/src/tools/build_environment_advanced.ts +312 -106
  95. package/src/tools/consolidated-tool-definitions.ts +136 -267
  96. package/src/tools/consolidated-tool-handlers.ts +871 -795
  97. package/src/tools/debug.ts +179 -38
  98. package/src/tools/editor.ts +35 -37
  99. package/src/tools/foliage.ts +110 -104
  100. package/src/tools/introspection.ts +24 -22
  101. package/src/tools/landscape.ts +334 -181
  102. package/src/tools/level.ts +683 -182
  103. package/src/tools/lighting.ts +244 -123
  104. package/src/tools/materials.ts +114 -83
  105. package/src/tools/niagara.ts +87 -81
  106. package/src/tools/performance.ts +49 -88
  107. package/src/tools/physics.ts +393 -299
  108. package/src/tools/rc.ts +103 -25
  109. package/src/tools/sequence.ts +157 -30
  110. package/src/tools/ui.ts +101 -70
  111. package/src/tools/visual.ts +250 -29
  112. package/src/types/tool-types.ts +0 -9
  113. package/src/unreal-bridge.ts +658 -140
  114. package/src/utils/elicitation.ts +129 -0
  115. package/src/utils/error-handler.ts +4 -159
  116. package/src/utils/http.ts +16 -115
  117. package/src/utils/normalize.ts +20 -10
  118. package/src/utils/python-output.ts +351 -0
  119. package/src/utils/python.ts +3 -0
  120. package/src/utils/response-validator.ts +68 -17
  121. package/src/utils/result-helpers.ts +193 -0
  122. package/src/utils/safe-json.ts +0 -50
  123. package/src/utils/validation.ts +94 -7
  124. package/tests/run-unreal-tool-tests.mjs +720 -0
  125. package/tsconfig.json +2 -2
  126. package/dist/python-utils.d.ts +0 -29
  127. package/dist/python-utils.js +0 -54
  128. package/dist/tools/tool-definitions.d.ts +0 -4919
  129. package/dist/tools/tool-definitions.js +0 -1065
  130. package/dist/tools/tool-handlers.d.ts +0 -47
  131. package/dist/tools/tool-handlers.js +0 -863
  132. package/dist/types/index.d.ts +0 -323
  133. package/dist/types/index.js +0 -28
  134. package/dist/utils/cache-manager.d.ts +0 -64
  135. package/dist/utils/cache-manager.js +0 -176
  136. package/dist/utils/errors.d.ts +0 -133
  137. package/dist/utils/errors.js +0 -256
  138. package/src/python/editor_compat.py +0 -181
  139. package/src/python-utils.ts +0 -57
  140. package/src/tools/tool-definitions.ts +0 -1081
  141. package/src/tools/tool-handlers.ts +0 -973
  142. package/src/types/index.ts +0 -414
  143. package/src/utils/cache-manager.ts +0 -213
  144. package/src/utils/errors.ts +0 -312
@@ -1,4 +1,5 @@
1
1
  import { UnrealBridge } from '../unreal-bridge.js';
2
+ import { interpretStandardResult, coerceBoolean, coerceNumber, coerceString, coerceStringArray } from '../utils/result-helpers.js';
2
3
 
3
4
  /**
4
5
  * Advanced Build Environment Tools
@@ -53,17 +54,8 @@ try:
53
54
  proc_mesh_comp = proc_actor.get_component_by_class(unreal.ProceduralMeshComponent)
54
55
  except:
55
56
  # Fallback: Create empty actor and add ProceduralMeshComponent
56
- proc_actor = subsys.spawn_actor_from_class(
57
- unreal.Actor,
58
- location,
59
- unreal.Rotator(0, 0, 0)
60
- )
61
- proc_actor.set_actor_label(f"{name}_ProceduralTerrain")
62
-
63
- # Add procedural mesh component
64
- proc_mesh_comp = unreal.ProceduralMeshComponent()
65
- proc_actor.add_instance_component(proc_mesh_comp)
66
- proc_mesh_comp.register_component()
57
+ # If spawning ProceduralMeshActor failed, surface a clear error about the plugin requirement
58
+ raise Exception("Failed to spawn ProceduralMeshActor. Ensure the 'Procedural Mesh Component' plugin is enabled and available.")
67
59
 
68
60
  if proc_mesh_comp:
69
61
  # Generate terrain mesh
@@ -146,19 +138,85 @@ except Exception as e:
146
138
  print(f"RESULT:{json.dumps(result)}")
147
139
  `.trim();
148
140
 
149
- const response = await this.bridge.executePython(pythonScript);
150
- const output = this.parseResponse(response);
151
- const match = output.match(/RESULT:({.*})/);
152
-
153
- if (match) {
154
- try {
155
- return JSON.parse(match[1]);
156
- } catch (_e) {
157
- return { success: false, error: 'Failed to parse result', details: output };
158
- }
159
- }
160
-
161
- return { success: false, error: 'No result found', output };
141
+ const response = await this.bridge.executePython(pythonScript);
142
+ const interpreted = interpretStandardResult(response, {
143
+ successMessage: `Created procedural terrain '${params.name}'`,
144
+ failureMessage: `Failed to create procedural terrain '${params.name}'`
145
+ });
146
+
147
+ if (!interpreted.success) {
148
+ const failure: {
149
+ success: false;
150
+ error: string;
151
+ message: string;
152
+ warnings?: string[];
153
+ details?: string[];
154
+ payload?: Record<string, unknown>;
155
+ } = {
156
+ success: false,
157
+ error: interpreted.error ?? interpreted.message,
158
+ message: interpreted.message
159
+ };
160
+
161
+ if (interpreted.warnings) {
162
+ failure.warnings = interpreted.warnings;
163
+ }
164
+ if (interpreted.details) {
165
+ failure.details = interpreted.details;
166
+ }
167
+ if (interpreted.payload && Object.keys(interpreted.payload).length > 0) {
168
+ failure.payload = interpreted.payload;
169
+ }
170
+
171
+ return failure;
172
+ }
173
+
174
+ const payload = { ...interpreted.payload } as Record<string, unknown>;
175
+ const actorName = coerceString(payload.actor_name) ?? coerceString(payload.actorName);
176
+ const vertices = coerceNumber(payload.vertices);
177
+ const triangles = coerceNumber(payload.triangles);
178
+ const subdivisions = coerceNumber(payload.subdivisions);
179
+ const sizeArray = Array.isArray(payload.size)
180
+ ? (payload.size as unknown[]).map(entry => {
181
+ if (typeof entry === 'number' && Number.isFinite(entry)) {
182
+ return entry;
183
+ }
184
+ if (typeof entry === 'string') {
185
+ const parsed = Number(entry);
186
+ return Number.isFinite(parsed) ? parsed : undefined;
187
+ }
188
+ return undefined;
189
+ }).filter((entry): entry is number => typeof entry === 'number')
190
+ : undefined;
191
+
192
+ payload.success = true;
193
+ payload.message = interpreted.message;
194
+
195
+ if (actorName) {
196
+ payload.actor_name = actorName;
197
+ payload.actorName = actorName;
198
+ }
199
+ if (typeof vertices === 'number') {
200
+ payload.vertices = vertices;
201
+ }
202
+ if (typeof triangles === 'number') {
203
+ payload.triangles = triangles;
204
+ }
205
+ if (typeof subdivisions === 'number') {
206
+ payload.subdivisions = subdivisions;
207
+ }
208
+ if (sizeArray && sizeArray.length === 2) {
209
+ payload.size = sizeArray;
210
+ }
211
+
212
+ if (interpreted.warnings) {
213
+ payload.warnings = interpreted.warnings;
214
+ }
215
+ if (interpreted.details) {
216
+ payload.details = interpreted.details;
217
+ }
218
+
219
+ return payload as any;
162
220
  }
163
221
 
164
222
  /**
@@ -193,6 +251,10 @@ try:
193
251
  subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
194
252
  asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
195
253
 
254
+ # Validate Procedural Foliage plugin/classes are available
255
+ if not hasattr(unreal, 'ProceduralFoliageVolume') or not hasattr(unreal, 'ProceduralFoliageSpawner'):
256
+ raise Exception("Procedural Foliage plugin not available. Please enable the 'Procedural Foliage' plugin and try again.")
257
+
196
258
  # Create ProceduralFoliageVolume
197
259
  volume_actor = subsys.spawn_actor_from_class(
198
260
  unreal.ProceduralFoliageVolume,
@@ -200,7 +262,7 @@ try:
200
262
  unreal.Rotator(0, 0, 0)
201
263
  )
202
264
  volume_actor.set_actor_label(f"{name}_ProceduralFoliageVolume")
203
- volume_actor.set_actor_scale3d(bounds_size / 100) # Scale is in meters
265
+ volume_actor.set_actor_scale3d(unreal.Vector(bounds_size.x/100.0, bounds_size.y/100.0, bounds_size.z/100.0)) # Scale is in meters
204
266
 
205
267
  # Get the procedural component
206
268
  proc_comp = volume_actor.procedural_component
@@ -231,13 +293,14 @@ try:
231
293
  )
232
294
 
233
295
  if spawner:
234
- # Configure spawner
235
- spawner.random_seed = seed
236
- spawner.tile_size = max(bounds_size.x, bounds_size.y)
296
+ # Configure spawner (use set_editor_property for read-only attributes)
297
+ spawner.set_editor_property('random_seed', seed)
298
+ spawner.set_editor_property('tile_size', max(bounds_size.x, bounds_size.y))
237
299
 
238
300
  # Create foliage types
239
301
  foliage_types = []
240
- for ft_params in ${JSON.stringify(params.foliageTypes)}:
302
+ ft_input = json.loads(r'''${JSON.stringify(params.foliageTypes)}''')
303
+ for ft_params in ft_input:
241
304
  # Load mesh
242
305
  mesh = unreal.EditorAssetLibrary.load_asset(ft_params['meshPath'])
243
306
  if mesh:
@@ -256,26 +319,26 @@ try:
256
319
  ft_asset = unreal.FoliageType_InstancedStaticMesh()
257
320
 
258
321
  if ft_asset:
259
- # Configure foliage type
260
- ft_asset.mesh = mesh
261
- ft_asset.density = ft_params.get('density', 1.0)
262
- ft_asset.random_yaw = ft_params.get('randomYaw', True)
263
- ft_asset.align_to_normal = ft_params.get('alignToNormal', True)
322
+ # Configure foliage type (use set_editor_property)
323
+ ft_asset.set_editor_property('mesh', mesh)
324
+ ft_asset.set_editor_property('density', ft_params.get('density', 1.0))
325
+ ft_asset.set_editor_property('random_yaw', ft_params.get('randomYaw', True))
326
+ ft_asset.set_editor_property('align_to_normal', ft_params.get('alignToNormal', True))
264
327
 
265
328
  min_scale = ft_params.get('minScale', 0.8)
266
329
  max_scale = ft_params.get('maxScale', 1.2)
267
- ft_asset.scale_x = unreal.FloatInterval(min_scale, max_scale)
268
- ft_asset.scale_y = unreal.FloatInterval(min_scale, max_scale)
269
- ft_asset.scale_z = unreal.FloatInterval(min_scale, max_scale)
330
+ ft_asset.set_editor_property('scale_x', unreal.FloatInterval(min_scale, max_scale))
331
+ ft_asset.set_editor_property('scale_y', unreal.FloatInterval(min_scale, max_scale))
332
+ ft_asset.set_editor_property('scale_z', unreal.FloatInterval(min_scale, max_scale))
270
333
 
271
- ft_obj.foliage_type_object = ft_asset
334
+ ft_obj.set_editor_property('foliage_type_object', ft_asset)
272
335
  foliage_types.append(ft_obj)
273
336
 
274
337
  # Set foliage types on spawner
275
- spawner.foliage_types = foliage_types
338
+ spawner.set_editor_property('foliage_types', foliage_types)
276
339
 
277
340
  # Assign spawner to component
278
- proc_comp.foliage_spawner = spawner
341
+ proc_comp.set_editor_property('foliage_spawner', spawner)
279
342
 
280
343
  # Save spawner asset
281
344
  unreal.EditorAssetLibrary.save_asset(spawner.get_path_name())
@@ -309,19 +372,80 @@ except Exception as e:
309
372
  print(f"RESULT:{json.dumps(result)}")
310
373
  `.trim();
311
374
 
312
- const response = await this.bridge.executePython(pythonScript);
313
- const output = this.parseResponse(response);
314
- const match = output.match(/RESULT:({.*})/);
315
-
316
- if (match) {
317
- try {
318
- return JSON.parse(match[1]);
319
- } catch (_e) {
320
- return { success: false, error: 'Failed to parse result', details: output };
321
- }
322
- }
323
-
324
- return { success: false, error: 'No result found', output };
375
+ const response = await this.bridge.executePython(pythonScript);
376
+ const interpreted = interpretStandardResult(response, {
377
+ successMessage: `Created procedural foliage volume '${params.name}'`,
378
+ failureMessage: `Failed to create procedural foliage volume '${params.name}'`
379
+ });
380
+
381
+ if (!interpreted.success) {
382
+ const failure: {
383
+ success: false;
384
+ error: string;
385
+ message: string;
386
+ warnings?: string[];
387
+ details?: string[];
388
+ payload?: Record<string, unknown>;
389
+ } = {
390
+ success: false,
391
+ error: interpreted.error ?? interpreted.message,
392
+ message: interpreted.message
393
+ };
394
+
395
+ if (interpreted.warnings) {
396
+ failure.warnings = interpreted.warnings;
397
+ }
398
+ if (interpreted.details) {
399
+ failure.details = interpreted.details;
400
+ }
401
+ if (interpreted.payload && Object.keys(interpreted.payload).length > 0) {
402
+ failure.payload = interpreted.payload;
403
+ }
404
+
405
+ return failure;
406
+ }
407
+
408
+ const payload = { ...interpreted.payload } as Record<string, unknown>;
409
+ const volumeActor = coerceString(payload.volume_actor) ?? coerceString(payload.volumeActor);
410
+ const spawnerPath = coerceString(payload.spawner_path) ?? coerceString(payload.spawnerPath);
411
+ const foliageCount = coerceNumber(payload.foliage_types_count) ?? coerceNumber(payload.foliageTypesCount);
412
+ const resimulated = coerceBoolean(payload.resimulated);
413
+ const note = coerceString(payload.note);
414
+ const messages = coerceStringArray(payload.messages);
415
+
416
+ payload.success = true;
417
+ payload.message = interpreted.message;
418
+
419
+ if (volumeActor) {
420
+ payload.volume_actor = volumeActor;
421
+ payload.volumeActor = volumeActor;
422
+ }
423
+ if (spawnerPath) {
424
+ payload.spawner_path = spawnerPath;
425
+ payload.spawnerPath = spawnerPath;
426
+ }
427
+ if (typeof foliageCount === 'number') {
428
+ payload.foliage_types_count = foliageCount;
429
+ payload.foliageTypesCount = foliageCount;
430
+ }
431
+ if (typeof resimulated === 'boolean') {
432
+ payload.resimulated = resimulated;
433
+ }
434
+ if (note) {
435
+ payload.note = note;
436
+ }
437
+ if (messages && messages.length > 0) {
438
+ payload.messages = messages;
439
+ }
440
+
441
+ if (interpreted.warnings) {
442
+ payload.warnings = interpreted.warnings;
443
+ }
444
+ if (interpreted.details) {
445
+ payload.details = interpreted.details;
446
+ }
447
+
448
+ return payload as any;
325
449
  }
326
450
 
327
451
  /**
@@ -393,19 +517,59 @@ except Exception as e:
393
517
  print(f"RESULT:{json.dumps(result)}")
394
518
  `.trim();
395
519
 
396
- const response = await this.bridge.executePython(pythonScript);
397
- const output = this.parseResponse(response);
398
- const match = output.match(/RESULT:({.*})/);
399
-
400
- if (match) {
401
- try {
402
- return JSON.parse(match[1]);
403
- } catch (_e) {
404
- return { success: false, error: 'Failed to parse result', details: output };
405
- }
406
- }
407
-
408
- return { success: false, error: 'No result found', output };
520
+ const response = await this.bridge.executePython(pythonScript);
521
+ const interpreted = interpretStandardResult(response, {
522
+ successMessage: 'Foliage instances added',
523
+ failureMessage: 'Failed to add foliage instances'
524
+ });
525
+
526
+ if (!interpreted.success) {
527
+ const failure: {
528
+ success: false;
529
+ error: string;
530
+ message: string;
531
+ warnings?: string[];
532
+ details?: string[];
533
+ payload?: Record<string, unknown>;
534
+ } = {
535
+ success: false,
536
+ error: interpreted.error ?? interpreted.message,
537
+ message: interpreted.message
538
+ };
539
+
540
+ if (interpreted.warnings) {
541
+ failure.warnings = interpreted.warnings;
542
+ }
543
+ if (interpreted.details) {
544
+ failure.details = interpreted.details;
545
+ }
546
+ if (interpreted.payload && Object.keys(interpreted.payload).length > 0) {
547
+ failure.payload = interpreted.payload;
548
+ }
549
+
550
+ return failure;
551
+ }
552
+
553
+ const payload = { ...interpreted.payload } as Record<string, unknown>;
554
+ const count = coerceNumber(payload.instances_count) ?? coerceNumber(payload.instancesCount);
555
+ const message = coerceString(payload.message) ?? interpreted.message;
556
+
557
+ payload.success = true;
558
+ payload.message = message;
559
+
560
+ if (typeof count === 'number') {
561
+ payload.instances_count = count;
562
+ payload.instancesCount = count;
563
+ }
564
+
565
+ if (interpreted.warnings) {
566
+ payload.warnings = interpreted.warnings;
567
+ }
568
+ if (interpreted.details) {
569
+ payload.details = interpreted.details;
570
+ }
571
+
572
+ return payload as any;
409
573
  }
410
574
 
411
575
  /**
@@ -455,23 +619,31 @@ try:
455
619
  # Load mesh
456
620
  mesh = unreal.EditorAssetLibrary.load_asset(mesh_path)
457
621
  if mesh:
458
- # Configure grass type
622
+ # Configure grass type (use set_editor_property)
459
623
  grass_variety = unreal.GrassVariety()
460
- grass_variety.grass_mesh = mesh
461
- grass_variety.grass_density = density * 100 # Convert to per square meter
462
- grass_variety.use_grid = True
463
- grass_variety.placement_jitter = 1.0
464
- grass_variety.start_cull_distance = 10000
465
- grass_variety.end_cull_distance = 20000
466
- grass_variety.min_lod = -1
467
- grass_variety.scaling = unreal.GrassScaling.UNIFORM
468
- grass_variety.scale_x = unreal.FloatInterval(min_scale, max_scale)
469
- grass_variety.scale_y = unreal.FloatInterval(min_scale, max_scale)
470
- grass_variety.scale_z = unreal.FloatInterval(min_scale, max_scale)
471
- grass_variety.random_rotation = True
472
- grass_variety.align_to_surface = True
624
+ grass_variety.set_editor_property('grass_mesh', mesh)
625
+ # GrassDensity is PerPlatformFloat in UE5+; set via struct instance
626
+ pp_density = unreal.PerPlatformFloat()
627
+ pp_density.set_editor_property('Default', float(density * 100.0))
628
+ grass_variety.set_editor_property('grass_density', pp_density)
629
+ grass_variety.set_editor_property('use_grid', True)
630
+ grass_variety.set_editor_property('placement_jitter', 1.0)
631
+ # Set cull distances as PerPlatformInt and LOD as int (engine uses mixed types here)
632
+ pp_start = unreal.PerPlatformInt()
633
+ pp_start.set_editor_property('Default', 10000)
634
+ grass_variety.set_editor_property('start_cull_distance', pp_start)
635
+ pp_end = unreal.PerPlatformInt()
636
+ pp_end.set_editor_property('Default', 20000)
637
+ grass_variety.set_editor_property('end_cull_distance', pp_end)
638
+ grass_variety.set_editor_property('min_lod', -1)
639
+ grass_variety.set_editor_property('scaling', unreal.GrassScaling.UNIFORM)
640
+ grass_variety.set_editor_property('scale_x', unreal.FloatInterval(min_scale, max_scale))
641
+ grass_variety.set_editor_property('scale_y', unreal.FloatInterval(min_scale, max_scale))
642
+ grass_variety.set_editor_property('scale_z', unreal.FloatInterval(min_scale, max_scale))
643
+ grass_variety.set_editor_property('random_rotation', True)
644
+ grass_variety.set_editor_property('align_to_surface', True)
473
645
 
474
- grass_type.grass_varieties = [grass_variety]
646
+ grass_type.set_editor_property('grass_varieties', [grass_variety])
475
647
 
476
648
  # Save asset
477
649
  unreal.EditorAssetLibrary.save_asset(grass_type.get_path_name())
@@ -496,31 +668,65 @@ except Exception as e:
496
668
  print(f"RESULT:{json.dumps(result)}")
497
669
  `.trim();
498
670
 
499
- const response = await this.bridge.executePython(pythonScript);
500
- const output = this.parseResponse(response);
501
- const match = output.match(/RESULT:({.*})/);
502
-
503
- if (match) {
504
- try {
505
- return JSON.parse(match[1]);
506
- } catch (_e) {
507
- return { success: false, error: 'Failed to parse result', details: output };
508
- }
509
- }
510
-
511
- return { success: false, error: 'No result found', output };
512
- }
671
+ const response = await this.bridge.executePython(pythonScript);
672
+ const interpreted = interpretStandardResult(response, {
673
+ successMessage: `Created landscape grass type '${params.name}'`,
674
+ failureMessage: `Failed to create landscape grass type '${params.name}'`
675
+ });
676
+
677
+ if (!interpreted.success) {
678
+ const failure: {
679
+ success: false;
680
+ error: string;
681
+ message: string;
682
+ warnings?: string[];
683
+ details?: string[];
684
+ payload?: Record<string, unknown>;
685
+ } = {
686
+ success: false,
687
+ error: interpreted.error ?? interpreted.message,
688
+ message: interpreted.message
689
+ };
690
+
691
+ if (interpreted.warnings) {
692
+ failure.warnings = interpreted.warnings;
693
+ }
694
+ if (interpreted.details) {
695
+ failure.details = interpreted.details;
696
+ }
697
+ if (interpreted.payload && Object.keys(interpreted.payload).length > 0) {
698
+ failure.payload = interpreted.payload;
699
+ }
700
+
701
+ return failure;
702
+ }
703
+
704
+ const payload = { ...interpreted.payload } as Record<string, unknown>;
705
+ const assetPath = coerceString(payload.asset_path) ?? coerceString(payload.assetPath);
706
+ const note = coerceString(payload.note);
707
+ const messages = coerceStringArray(payload.messages);
708
+
709
+ payload.success = true;
710
+ payload.message = interpreted.message;
711
+
712
+ if (assetPath) {
713
+ payload.asset_path = assetPath;
714
+ payload.assetPath = assetPath;
715
+ }
716
+ if (note) {
717
+ payload.note = note;
718
+ }
719
+ if (messages && messages.length > 0) {
720
+ payload.messages = messages;
721
+ }
722
+
723
+ if (interpreted.warnings) {
724
+ payload.warnings = interpreted.warnings;
725
+ }
726
+ if (interpreted.details) {
727
+ payload.details = interpreted.details;
728
+ }
513
729
 
514
- private parseResponse(response: any): string {
515
- if (response && typeof response === 'object') {
516
- if (response.LogOutput && Array.isArray(response.LogOutput)) {
517
- return response.LogOutput.map((log: any) => log.Output || '').join('');
518
- } else if (response.CommandResult) {
519
- return response.CommandResult;
520
- } else if (response.ReturnValue) {
521
- return JSON.stringify(response);
522
- }
523
- }
524
- return typeof response === 'string' ? response : JSON.stringify(response);
730
+ return payload as any;
525
731
  }
526
732
  }