unreal-engine-mcp-server 0.3.1 → 0.4.0

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 CHANGED
@@ -19,7 +19,7 @@ LOG_LEVEL=info
19
19
 
20
20
  # Server Settings
21
21
  SERVER_NAME=unreal-engine-mcp
22
- SERVER_VERSION=0.3.1
22
+ SERVER_VERSION=0.4.0
23
23
 
24
24
  # Connection Settings
25
25
  MAX_RETRY_ATTEMPTS=3
package/README.md CHANGED
@@ -114,7 +114,7 @@ Then enable Python execution in: Edit > Project Settings > Plugins > Remote Cont
114
114
  }
115
115
  ```
116
116
 
117
- ## Available Tools (13 Consolidated)
117
+ ## Available Tools (13)
118
118
 
119
119
  | Tool | Description |
120
120
  |------|-------------|
@@ -161,7 +161,6 @@ Blueprints, Materials, Textures, Static/Skeletal Meshes, Levels, Sounds, Particl
161
161
  UE_HOST=127.0.0.1 # Unreal Engine host
162
162
  UE_RC_HTTP_PORT=30010 # Remote Control HTTP port
163
163
  UE_RC_WS_PORT=30020 # Remote Control WebSocket port
164
- USE_CONSOLIDATED_TOOLS=true # Use 13 consolidated tools (false = 37 individual)
165
164
  LOG_LEVEL=info # debug | info | warn | error
166
165
  ```
167
166
 
package/dist/index.js CHANGED
@@ -17,6 +17,7 @@ import { BlueprintTools } from './tools/blueprint.js';
17
17
  import { LevelTools } from './tools/level.js';
18
18
  import { LightingTools } from './tools/lighting.js';
19
19
  import { LandscapeTools } from './tools/landscape.js';
20
+ import { BuildEnvironmentAdvanced } from './tools/build_environment_advanced.js';
20
21
  import { FoliageTools } from './tools/foliage.js';
21
22
  import { DebugVisualizationTools } from './tools/debug.js';
22
23
  import { PerformanceTools } from './tools/performance.js';
@@ -27,8 +28,6 @@ import { SequenceTools } from './tools/sequence.js';
27
28
  import { IntrospectionTools } from './tools/introspection.js';
28
29
  import { VisualTools } from './tools/visual.js';
29
30
  import { EngineTools } from './tools/engine.js';
30
- import { toolDefinitions } from './tools/tool-definitions.js';
31
- import { handleToolCall } from './tools/tool-handlers.js';
32
31
  import { consolidatedToolDefinitions } from './tools/consolidated-tool-definitions.js';
33
32
  import { handleConsolidatedToolCall } from './tools/consolidated-tool-handlers.js';
34
33
  import { prompts } from './prompts/index.js';
@@ -56,14 +55,13 @@ let healthCheckTimer;
56
55
  let lastHealthSuccessAt = 0;
57
56
  // Configuration
58
57
  const CONFIG = {
59
- // Tool mode: true = consolidated (13 tools), false = individual (36+ tools)
60
- USE_CONSOLIDATED_TOOLS: process.env.USE_CONSOLIDATED_TOOLS !== 'false',
58
+ // Tooling: use consolidated tools only (13 tools)
61
59
  // Connection retry settings
62
60
  MAX_RETRY_ATTEMPTS: 3,
63
61
  RETRY_DELAY_MS: 2000,
64
62
  // Server info
65
63
  SERVER_NAME: 'unreal-engine-mcp',
66
- SERVER_VERSION: '0.3.1',
64
+ SERVER_VERSION: '0.4.0',
67
65
  // Monitoring
68
66
  HEALTH_CHECK_INTERVAL_MS: 30000 // 30 seconds
69
67
  };
@@ -123,7 +121,7 @@ export async function createServer() {
123
121
  bridge.setAutoReconnectEnabled(false);
124
122
  // Initialize response validation with schemas
125
123
  log.debug('Initializing response validation...');
126
- const toolDefs = CONFIG.USE_CONSOLIDATED_TOOLS ? consolidatedToolDefinitions : toolDefinitions;
124
+ const toolDefs = consolidatedToolDefinitions;
127
125
  toolDefs.forEach((tool) => {
128
126
  if (tool.outputSchema) {
129
127
  responseValidator.registerSchema(tool.name, tool.outputSchema);
@@ -196,6 +194,7 @@ export async function createServer() {
196
194
  const lightingTools = new LightingTools(bridge);
197
195
  const landscapeTools = new LandscapeTools(bridge);
198
196
  const foliageTools = new FoliageTools(bridge);
197
+ const buildEnvAdvanced = new BuildEnvironmentAdvanced(bridge);
199
198
  const debugTools = new DebugVisualizationTools(bridge);
200
199
  const performanceTools = new PerformanceTools(bridge);
201
200
  const audioTools = new AudioTools(bridge);
@@ -394,14 +393,14 @@ export async function createServer() {
394
393
  }
395
394
  throw new Error(`Unknown resource: ${uri}`);
396
395
  });
397
- // Handle tool listing - switch between consolidated (13) or individual (36) tools
396
+ // Handle tool listing - consolidated tools only
398
397
  server.setRequestHandler(ListToolsRequestSchema, async () => {
399
- log.info(`Serving ${CONFIG.USE_CONSOLIDATED_TOOLS ? 'consolidated' : 'individual'} tools`);
398
+ log.info('Serving consolidated tools');
400
399
  return {
401
- tools: CONFIG.USE_CONSOLIDATED_TOOLS ? consolidatedToolDefinitions : toolDefinitions
400
+ tools: consolidatedToolDefinitions
402
401
  };
403
402
  });
404
- // Handle tool calls - switch between consolidated (13) or individual (36) tools
403
+ // Handle tool calls - consolidated tools only (13)
405
404
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
406
405
  const { name, arguments: args } = request.params;
407
406
  const startTime = Date.now();
@@ -432,6 +431,7 @@ export async function createServer() {
432
431
  lightingTools,
433
432
  landscapeTools,
434
433
  foliageTools,
434
+ buildEnvAdvanced,
435
435
  debugTools,
436
436
  performanceTools,
437
437
  audioTools,
@@ -441,18 +441,16 @@ export async function createServer() {
441
441
  introspectionTools,
442
442
  visualTools,
443
443
  engineTools,
444
+ // Resources for listing and info
445
+ assetResources,
446
+ actorResources,
447
+ levelResources,
444
448
  bridge
445
449
  };
446
- // Use consolidated or individual handler based on configuration
450
+ // Execute consolidated tool handler
447
451
  try {
448
452
  log.debug(`Executing tool: ${name}`);
449
- let result;
450
- if (CONFIG.USE_CONSOLIDATED_TOOLS) {
451
- result = await handleConsolidatedToolCall(name, args, tools);
452
- }
453
- else {
454
- result = await handleToolCall(name, args, tools);
455
- }
453
+ let result = await handleConsolidatedToolCall(name, args, tools);
456
454
  log.debug(`Tool ${name} returned result`);
457
455
  // Clean the result to remove circular references
458
456
  result = cleanObject(result);
@@ -5,6 +5,7 @@ export declare class AssetResources {
5
5
  private cache;
6
6
  private get ttlMs();
7
7
  private makeKey;
8
+ private normalizeDir;
8
9
  list(dir?: string, _recursive?: boolean, limit?: number): Promise<any>;
9
10
  /**
10
11
  * List assets with pagination support
@@ -14,8 +15,8 @@ export declare class AssetResources {
14
15
  */
15
16
  listPaged(dir?: string, page?: number, pageSize?: number, recursive?: boolean): Promise<any>;
16
17
  /**
17
- * Directory-based listing for paths with too many assets
18
- * Shows only immediate children (folders and files) to avoid timeouts
18
+ * Directory-based listing of immediate children using AssetRegistry.
19
+ * Returns both subfolders and assets at the given path.
19
20
  */
20
21
  private listDirectoryOnly;
21
22
  find(assetPath: string): Promise<boolean>;
@@ -9,10 +9,36 @@ export class AssetResources {
9
9
  makeKey(dir, recursive, page) {
10
10
  return page !== undefined ? `${dir}::${recursive ? 1 : 0}::${page}` : `${dir}::${recursive ? 1 : 0}`;
11
11
  }
12
+ // Normalize UE content paths:
13
+ // - Map '/Content' -> '/Game'
14
+ // - Ensure forward slashes
15
+ normalizeDir(dir) {
16
+ try {
17
+ if (!dir || typeof dir !== 'string')
18
+ return '/Game';
19
+ let d = dir.replace(/\\/g, '/');
20
+ if (!d.startsWith('/'))
21
+ d = '/' + d;
22
+ if (d.toLowerCase().startsWith('/content')) {
23
+ d = '/Game' + d.substring('/Content'.length);
24
+ }
25
+ // Collapse multiple slashes
26
+ d = d.replace(/\/+/g, '/');
27
+ // Remove trailing slash except root
28
+ if (d.length > 1)
29
+ d = d.replace(/\/$/, '');
30
+ return d;
31
+ }
32
+ catch {
33
+ return '/Game';
34
+ }
35
+ }
12
36
  async list(dir = '/Game', _recursive = false, limit = 50) {
13
37
  // ALWAYS use non-recursive listing to show only immediate children
14
38
  // This prevents timeouts and makes navigation clearer
15
39
  _recursive = false; // Force non-recursive
40
+ // Normalize directory first
41
+ dir = this.normalizeDir(dir);
16
42
  // Cache fast-path
17
43
  try {
18
44
  const key = this.makeKey(dir, false);
@@ -45,7 +71,8 @@ export class AssetResources {
45
71
  // Ensure pageSize doesn't exceed safe limit
46
72
  const safePageSize = Math.min(pageSize, 50);
47
73
  const offset = page * safePageSize;
48
- // Check cache for this specific page
74
+ // Normalize directory and check cache for this specific page
75
+ dir = this.normalizeDir(dir);
49
76
  const cacheKey = this.makeKey(dir, recursive, page);
50
77
  const cached = this.cache.get(cacheKey);
51
78
  if (cached && (Date.now() - cached.timestamp) < this.ttlMs) {
@@ -91,8 +118,8 @@ export class AssetResources {
91
118
  };
92
119
  }
93
120
  /**
94
- * Directory-based listing for paths with too many assets
95
- * Shows only immediate children (folders and files) to avoid timeouts
121
+ * Directory-based listing of immediate children using AssetRegistry.
122
+ * Returns both subfolders and assets at the given path.
96
123
  */
97
124
  async listDirectoryOnly(dir, _recursive, limit) {
98
125
  // Always return only immediate children to avoid timeout and improve navigation
@@ -101,65 +128,43 @@ export class AssetResources {
101
128
  import unreal
102
129
  import json
103
130
 
104
- _dir = r"${dir}"
131
+ _dir = r"${this.normalizeDir(dir)}"
105
132
 
106
133
  try:
107
- # ALWAYS non-recursive - get only immediate children
108
- all_paths = unreal.EditorAssetLibrary.list_assets(_dir, False, False)
109
-
110
- # Organize into immediate children only
111
- immediate_folders = set()
112
- immediate_assets = []
113
-
114
- for path in all_paths:
115
- # Remove the base directory to get relative path
116
- relative = path.replace(_dir, '').strip('/')
117
- if not relative:
118
- continue
119
-
120
- # Split to check depth
121
- parts = relative.split('/')
122
-
123
- if len(parts) == 1:
124
- # This is an immediate child asset
125
- immediate_assets.append(path)
126
- elif len(parts) > 1:
127
- # This indicates a subfolder exists
128
- immediate_folders.add(parts[0])
129
-
130
- result = []
131
-
132
- # Add folders first
133
- for folder in sorted(immediate_folders):
134
- result.append({
135
- 'n': folder,
136
- 'p': _dir + '/' + folder,
137
- 'c': 'Folder',
138
- 'isFolder': True
139
- })
140
-
141
- # Add immediate assets (limit to prevent socket issues)
142
- for asset_path in immediate_assets[:min(${limit}, len(immediate_assets))]:
143
- name = asset_path.split('/')[-1].split('.')[0]
144
- result.append({
145
- 'n': name,
146
- 'p': asset_path,
147
- 'c': 'Asset'
148
- })
149
-
150
- # Always showing immediate children only
151
- note = f'Showing immediate children of {_dir} ({len(immediate_folders)} folders, {len(immediate_assets)} files)'
152
-
134
+ ar = unreal.AssetRegistryHelpers.get_asset_registry()
135
+ # Immediate subfolders
136
+ sub_paths = ar.get_sub_paths(_dir, False)
137
+ folders_list = []
138
+ for p in sub_paths:
139
+ try:
140
+ name = p.split('/')[-1]
141
+ folders_list.append({'n': name, 'p': p})
142
+ except Exception:
143
+ pass
144
+
145
+ # Immediate assets at this path
146
+ assets_data = ar.get_assets_by_path(_dir, False)
147
+ assets = []
148
+ for a in assets_data[:${limit}]:
149
+ try:
150
+ assets.append({
151
+ 'n': str(a.asset_name),
152
+ 'p': str(a.object_path),
153
+ 'c': str(a.asset_class)
154
+ })
155
+ except Exception:
156
+ pass
157
+
153
158
  print("RESULT:" + json.dumps({
154
- 'success': True,
155
- 'assets': result,
156
- 'count': len(result),
157
- 'folders': len(immediate_folders),
158
- 'files': len(immediate_assets),
159
- 'note': note
159
+ 'success': True,
160
+ 'path': _dir,
161
+ 'folders': len(folders_list),
162
+ 'files': len(assets),
163
+ 'folders_list': folders_list,
164
+ 'assets': assets
160
165
  }))
161
166
  except Exception as e:
162
- print("RESULT:" + json.dumps({'success': False, 'error': str(e), 'assets': []}))
167
+ print("RESULT:" + json.dumps({'success': False, 'error': str(e), 'path': _dir}))
163
168
  `.trim();
164
169
  const resp = await this.bridge.executePython(py);
165
170
  let output = '';
@@ -177,20 +182,34 @@ except Exception as e:
177
182
  try {
178
183
  const parsed = JSON.parse(m[1]);
179
184
  if (parsed.success) {
180
- // Transform to standard format
181
- const assets = parsed.assets.map((a) => ({
185
+ // Map folders and assets to a clear response
186
+ const foldersArr = Array.isArray(parsed.folders_list) ? parsed.folders_list.map((f) => ({
187
+ Name: f.n,
188
+ Path: f.p,
189
+ Class: 'Folder',
190
+ isFolder: true
191
+ })) : [];
192
+ const assetsArr = Array.isArray(parsed.assets) ? parsed.assets.map((a) => ({
182
193
  Name: a.n,
183
194
  Path: a.p,
184
- Class: a.c,
185
- isFolder: a.isFolder || false
186
- }));
195
+ Class: a.c || 'Asset',
196
+ isFolder: false
197
+ })) : [];
198
+ const total = foldersArr.length + assetsArr.length;
199
+ const summary = {
200
+ total,
201
+ folders: foldersArr.length,
202
+ assets: assetsArr.length
203
+ };
187
204
  return {
188
- assets,
189
- count: parsed.count,
190
- folders: parsed.folders,
191
- files: parsed.files,
192
- note: parsed.note,
193
- method: 'directory_listing'
205
+ success: true,
206
+ path: parsed.path || this.normalizeDir(dir),
207
+ summary,
208
+ foldersList: foldersArr,
209
+ assets: assetsArr,
210
+ count: total,
211
+ note: `Immediate children of ${parsed.path || this.normalizeDir(dir)}: ${foldersArr.length} folder(s), ${assetsArr.length} asset(s)`,
212
+ method: 'asset_registry_listing'
194
213
  };
195
214
  }
196
215
  }
@@ -202,10 +221,13 @@ except Exception as e:
202
221
  }
203
222
  // Fallback: return empty with explanation
204
223
  return {
224
+ success: true,
225
+ path: this.normalizeDir(dir),
226
+ summary: { total: 0, folders: 0, assets: 0 },
227
+ foldersList: [],
205
228
  assets: [],
206
- warning: 'Directory contains too many assets. Showing immediate children only.',
207
- suggestion: 'Navigate to specific subdirectories for detailed listings.',
208
- method: 'directory_timeout_fallback'
229
+ warning: 'No items at this path or failed to query AssetRegistry.',
230
+ method: 'asset_registry_fallback'
209
231
  };
210
232
  }
211
233
  async find(assetPath) {
@@ -213,9 +235,11 @@ except Exception as e:
213
235
  if (!assetPath || typeof assetPath !== 'string' || assetPath.trim() === '' || assetPath.endsWith('/')) {
214
236
  return false;
215
237
  }
238
+ // Normalize asset path (support users passing /Content/...)
239
+ const ap = this.normalizeDir(assetPath);
216
240
  const py = `
217
241
  import unreal
218
- apath = r"${assetPath}"
242
+ apath = r"${ap}"
219
243
  try:
220
244
  exists = unreal.EditorAssetLibrary.does_asset_exist(apath)
221
245
  print("RESULT:{'success': True, 'exists': %s}" % ('True' if exists else 'False'))
@@ -62,9 +62,9 @@ export class LevelResources {
62
62
  }
63
63
  }
64
64
  async saveCurrentLevel() {
65
- // Prefer Python save (or LevelEditorSubsystem) then fallback
65
+ // Strict modern API: require LevelEditorSubsystem
66
66
  try {
67
- const py = '\nimport unreal, json\ntry:\n les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)\n if les: les.save_current_level()\n else: unreal.EditorLevelLibrary.save_current_level()\n print(\'RESULT:\' + json.dumps({\'success\': True}))\nexcept Exception as e:\n print(\'RESULT:\' + json.dumps({\'success\': False, \'error\': str(e)}))\n'.trim();
67
+ const py = '\nimport unreal, json\ntry:\n les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)\n if not les:\n print(\'RESULT:\' + json.dumps({\'success\': False, \'error\': \'LevelEditorSubsystem not available\'}))\n else:\n les.save_current_level()\n print(\'RESULT:\' + json.dumps({\'success\': True}))\nexcept Exception as e:\n print(\'RESULT:\' + json.dumps({\'success\': False, \'error\': str(e)}))\n'.trim();
68
68
  const resp = await this.bridge.executePython(py);
69
69
  // Handle LogOutput format from executePython
70
70
  let out = '';
@@ -7,8 +7,12 @@ export class AssetTools {
7
7
  }
8
8
  async importAsset(sourcePath, destinationPath) {
9
9
  try {
10
- // Sanitize destination path (remove trailing slash)
11
- const cleanDest = destinationPath.replace(/\/$/, '');
10
+ // Sanitize destination path (remove trailing slash) and normalize UE path
11
+ let cleanDest = destinationPath.replace(/\/$/, '');
12
+ // Map /Content -> /Game for UE asset destinations
13
+ if (/^\/?content(\/|$)/i.test(cleanDest)) {
14
+ cleanDest = '/Game' + cleanDest.replace(/^\/?content/i, '');
15
+ }
12
16
  // Create test FBX file if it's a test file
13
17
  if (sourcePath.includes('test_model.fbx')) {
14
18
  // Create the file outside of Python, before import
@@ -45,17 +45,8 @@ try:
45
45
  proc_mesh_comp = proc_actor.get_component_by_class(unreal.ProceduralMeshComponent)
46
46
  except:
47
47
  # Fallback: Create empty actor and add ProceduralMeshComponent
48
- proc_actor = subsys.spawn_actor_from_class(
49
- unreal.Actor,
50
- location,
51
- unreal.Rotator(0, 0, 0)
52
- )
53
- proc_actor.set_actor_label(f"{name}_ProceduralTerrain")
54
-
55
- # Add procedural mesh component
56
- proc_mesh_comp = unreal.ProceduralMeshComponent()
57
- proc_actor.add_instance_component(proc_mesh_comp)
58
- proc_mesh_comp.register_component()
48
+ # If spawning ProceduralMeshActor failed, surface a clear error about the plugin requirement
49
+ raise Exception("Failed to spawn ProceduralMeshActor. Ensure the 'Procedural Mesh Component' plugin is enabled and available.")
59
50
 
60
51
  if proc_mesh_comp:
61
52
  # Generate terrain mesh
@@ -170,6 +161,10 @@ try:
170
161
  subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
171
162
  asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
172
163
 
164
+ # Validate Procedural Foliage plugin/classes are available
165
+ if not hasattr(unreal, 'ProceduralFoliageVolume') or not hasattr(unreal, 'ProceduralFoliageSpawner'):
166
+ raise Exception("Procedural Foliage plugin not available. Please enable the 'Procedural Foliage' plugin and try again.")
167
+
173
168
  # Create ProceduralFoliageVolume
174
169
  volume_actor = subsys.spawn_actor_from_class(
175
170
  unreal.ProceduralFoliageVolume,
@@ -177,7 +172,7 @@ try:
177
172
  unreal.Rotator(0, 0, 0)
178
173
  )
179
174
  volume_actor.set_actor_label(f"{name}_ProceduralFoliageVolume")
180
- volume_actor.set_actor_scale3d(bounds_size / 100) # Scale is in meters
175
+ 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
181
176
 
182
177
  # Get the procedural component
183
178
  proc_comp = volume_actor.procedural_component
@@ -208,13 +203,14 @@ try:
208
203
  )
209
204
 
210
205
  if spawner:
211
- # Configure spawner
212
- spawner.random_seed = seed
213
- spawner.tile_size = max(bounds_size.x, bounds_size.y)
206
+ # Configure spawner (use set_editor_property for read-only attributes)
207
+ spawner.set_editor_property('random_seed', seed)
208
+ spawner.set_editor_property('tile_size', max(bounds_size.x, bounds_size.y))
214
209
 
215
210
  # Create foliage types
216
211
  foliage_types = []
217
- for ft_params in ${JSON.stringify(params.foliageTypes)}:
212
+ ft_input = json.loads(r'''${JSON.stringify(params.foliageTypes)}''')
213
+ for ft_params in ft_input:
218
214
  # Load mesh
219
215
  mesh = unreal.EditorAssetLibrary.load_asset(ft_params['meshPath'])
220
216
  if mesh:
@@ -233,26 +229,26 @@ try:
233
229
  ft_asset = unreal.FoliageType_InstancedStaticMesh()
234
230
 
235
231
  if ft_asset:
236
- # Configure foliage type
237
- ft_asset.mesh = mesh
238
- ft_asset.density = ft_params.get('density', 1.0)
239
- ft_asset.random_yaw = ft_params.get('randomYaw', True)
240
- ft_asset.align_to_normal = ft_params.get('alignToNormal', True)
232
+ # Configure foliage type (use set_editor_property)
233
+ ft_asset.set_editor_property('mesh', mesh)
234
+ ft_asset.set_editor_property('density', ft_params.get('density', 1.0))
235
+ ft_asset.set_editor_property('random_yaw', ft_params.get('randomYaw', True))
236
+ ft_asset.set_editor_property('align_to_normal', ft_params.get('alignToNormal', True))
241
237
 
242
238
  min_scale = ft_params.get('minScale', 0.8)
243
239
  max_scale = ft_params.get('maxScale', 1.2)
244
- ft_asset.scale_x = unreal.FloatInterval(min_scale, max_scale)
245
- ft_asset.scale_y = unreal.FloatInterval(min_scale, max_scale)
246
- ft_asset.scale_z = unreal.FloatInterval(min_scale, max_scale)
240
+ ft_asset.set_editor_property('scale_x', unreal.FloatInterval(min_scale, max_scale))
241
+ ft_asset.set_editor_property('scale_y', unreal.FloatInterval(min_scale, max_scale))
242
+ ft_asset.set_editor_property('scale_z', unreal.FloatInterval(min_scale, max_scale))
247
243
 
248
- ft_obj.foliage_type_object = ft_asset
244
+ ft_obj.set_editor_property('foliage_type_object', ft_asset)
249
245
  foliage_types.append(ft_obj)
250
246
 
251
247
  # Set foliage types on spawner
252
- spawner.foliage_types = foliage_types
248
+ spawner.set_editor_property('foliage_types', foliage_types)
253
249
 
254
250
  # Assign spawner to component
255
- proc_comp.foliage_spawner = spawner
251
+ proc_comp.set_editor_property('foliage_spawner', spawner)
256
252
 
257
253
  # Save spawner asset
258
254
  unreal.EditorAssetLibrary.save_asset(spawner.get_path_name())
@@ -413,23 +409,31 @@ try:
413
409
  # Load mesh
414
410
  mesh = unreal.EditorAssetLibrary.load_asset(mesh_path)
415
411
  if mesh:
416
- # Configure grass type
412
+ # Configure grass type (use set_editor_property)
417
413
  grass_variety = unreal.GrassVariety()
418
- grass_variety.grass_mesh = mesh
419
- grass_variety.grass_density = density * 100 # Convert to per square meter
420
- grass_variety.use_grid = True
421
- grass_variety.placement_jitter = 1.0
422
- grass_variety.start_cull_distance = 10000
423
- grass_variety.end_cull_distance = 20000
424
- grass_variety.min_lod = -1
425
- grass_variety.scaling = unreal.GrassScaling.UNIFORM
426
- grass_variety.scale_x = unreal.FloatInterval(min_scale, max_scale)
427
- grass_variety.scale_y = unreal.FloatInterval(min_scale, max_scale)
428
- grass_variety.scale_z = unreal.FloatInterval(min_scale, max_scale)
429
- grass_variety.random_rotation = True
430
- grass_variety.align_to_surface = True
414
+ grass_variety.set_editor_property('grass_mesh', mesh)
415
+ # GrassDensity is PerPlatformFloat in UE5+; set via struct instance
416
+ pp_density = unreal.PerPlatformFloat()
417
+ pp_density.set_editor_property('Default', float(density * 100.0))
418
+ grass_variety.set_editor_property('grass_density', pp_density)
419
+ grass_variety.set_editor_property('use_grid', True)
420
+ grass_variety.set_editor_property('placement_jitter', 1.0)
421
+ # Set cull distances as PerPlatformInt and LOD as int (engine uses mixed types here)
422
+ pp_start = unreal.PerPlatformInt()
423
+ pp_start.set_editor_property('Default', 10000)
424
+ grass_variety.set_editor_property('start_cull_distance', pp_start)
425
+ pp_end = unreal.PerPlatformInt()
426
+ pp_end.set_editor_property('Default', 20000)
427
+ grass_variety.set_editor_property('end_cull_distance', pp_end)
428
+ grass_variety.set_editor_property('min_lod', -1)
429
+ grass_variety.set_editor_property('scaling', unreal.GrassScaling.UNIFORM)
430
+ grass_variety.set_editor_property('scale_x', unreal.FloatInterval(min_scale, max_scale))
431
+ grass_variety.set_editor_property('scale_y', unreal.FloatInterval(min_scale, max_scale))
432
+ grass_variety.set_editor_property('scale_z', unreal.FloatInterval(min_scale, max_scale))
433
+ grass_variety.set_editor_property('random_rotation', True)
434
+ grass_variety.set_editor_property('align_to_surface', True)
431
435
 
432
- grass_type.grass_varieties = [grass_variety]
436
+ grass_type.set_editor_property('grass_varieties', [grass_variety])
433
437
 
434
438
  # Save asset
435
439
  unreal.EditorAssetLibrary.save_asset(grass_type.get_path_name())