unreal-engine-mcp-server 0.3.0 → 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 +6 -1
- package/Dockerfile +11 -28
- package/README.md +1 -2
- package/dist/index.js +120 -54
- package/dist/resources/actors.js +71 -13
- package/dist/resources/assets.d.ts +3 -2
- package/dist/resources/assets.js +96 -72
- package/dist/resources/levels.js +2 -2
- package/dist/tools/assets.js +6 -2
- package/dist/tools/build_environment_advanced.js +46 -42
- package/dist/tools/consolidated-tool-definitions.d.ts +232 -15
- package/dist/tools/consolidated-tool-definitions.js +173 -8
- package/dist/tools/consolidated-tool-handlers.js +331 -718
- package/dist/tools/debug.js +4 -6
- package/dist/tools/rc.js +2 -2
- package/dist/tools/sequence.js +21 -2
- package/dist/unreal-bridge.d.ts +4 -1
- package/dist/unreal-bridge.js +211 -53
- package/dist/utils/http.js +4 -2
- package/dist/utils/response-validator.d.ts +6 -1
- package/dist/utils/response-validator.js +43 -15
- package/package.json +5 -5
- package/server.json +2 -2
- package/src/index.ts +120 -56
- package/src/resources/actors.ts +51 -13
- package/src/resources/assets.ts +97 -73
- package/src/resources/levels.ts +2 -2
- package/src/tools/assets.ts +6 -2
- package/src/tools/build_environment_advanced.ts +46 -42
- package/src/tools/consolidated-tool-definitions.ts +173 -8
- package/src/tools/consolidated-tool-handlers.ts +318 -747
- package/src/tools/debug.ts +4 -6
- package/src/tools/rc.ts +2 -2
- package/src/tools/sequence.ts +21 -2
- package/src/unreal-bridge.ts +163 -60
- package/src/utils/http.ts +7 -4
- package/src/utils/response-validator.ts +48 -19
- package/dist/tools/tool-definitions.d.ts +0 -4919
- package/dist/tools/tool-definitions.js +0 -1007
- package/dist/tools/tool-handlers.d.ts +0 -47
- package/dist/tools/tool-handlers.js +0 -863
- package/src/tools/tool-definitions.ts +0 -1023
- package/src/tools/tool-handlers.ts +0 -973
package/dist/resources/assets.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
95
|
-
*
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
'
|
|
156
|
-
'
|
|
157
|
-
'
|
|
158
|
-
'
|
|
159
|
-
'
|
|
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), '
|
|
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
|
-
//
|
|
181
|
-
const
|
|
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:
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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: '
|
|
207
|
-
|
|
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"${
|
|
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'))
|
package/dist/resources/levels.js
CHANGED
|
@@ -62,9 +62,9 @@ export class LevelResources {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
async saveCurrentLevel() {
|
|
65
|
-
//
|
|
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
|
|
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 = '';
|
package/dist/tools/assets.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
|
213
|
-
spawner.tile_size
|
|
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
|
-
|
|
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
|
|
238
|
-
ft_asset.density
|
|
239
|
-
ft_asset.random_yaw
|
|
240
|
-
ft_asset.align_to_normal
|
|
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
|
|
245
|
-
ft_asset.scale_y
|
|
246
|
-
ft_asset.scale_z
|
|
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
|
|
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
|
|
248
|
+
spawner.set_editor_property('foliage_types', foliage_types)
|
|
253
249
|
|
|
254
250
|
# Assign spawner to component
|
|
255
|
-
proc_comp.foliage_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
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
grass_variety.
|
|
423
|
-
grass_variety.
|
|
424
|
-
grass_variety.
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
grass_variety.
|
|
429
|
-
|
|
430
|
-
|
|
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
|
|
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())
|