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/src/resources/assets.ts
CHANGED
|
@@ -10,11 +10,35 @@ export class AssetResources {
|
|
|
10
10
|
return page !== undefined ? `${dir}::${recursive ? 1 : 0}::${page}` : `${dir}::${recursive ? 1 : 0}`;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
// Normalize UE content paths:
|
|
14
|
+
// - Map '/Content' -> '/Game'
|
|
15
|
+
// - Ensure forward slashes
|
|
16
|
+
private normalizeDir(dir: string): string {
|
|
17
|
+
try {
|
|
18
|
+
if (!dir || typeof dir !== 'string') return '/Game';
|
|
19
|
+
let d = dir.replace(/\\/g, '/');
|
|
20
|
+
if (!d.startsWith('/')) d = '/' + d;
|
|
21
|
+
if (d.toLowerCase().startsWith('/content')) {
|
|
22
|
+
d = '/Game' + d.substring('/Content'.length);
|
|
23
|
+
}
|
|
24
|
+
// Collapse multiple slashes
|
|
25
|
+
d = d.replace(/\/+/g, '/');
|
|
26
|
+
// Remove trailing slash except root
|
|
27
|
+
if (d.length > 1) d = d.replace(/\/$/, '');
|
|
28
|
+
return d;
|
|
29
|
+
} catch {
|
|
30
|
+
return '/Game';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
13
34
|
async list(dir = '/Game', _recursive = false, limit = 50) {
|
|
14
35
|
// ALWAYS use non-recursive listing to show only immediate children
|
|
15
36
|
// This prevents timeouts and makes navigation clearer
|
|
16
37
|
_recursive = false; // Force non-recursive
|
|
17
38
|
|
|
39
|
+
// Normalize directory first
|
|
40
|
+
dir = this.normalizeDir(dir);
|
|
41
|
+
|
|
18
42
|
// Cache fast-path
|
|
19
43
|
try {
|
|
20
44
|
const key = this.makeKey(dir, false);
|
|
@@ -50,7 +74,8 @@ export class AssetResources {
|
|
|
50
74
|
const safePageSize = Math.min(pageSize, 50);
|
|
51
75
|
const offset = page * safePageSize;
|
|
52
76
|
|
|
53
|
-
//
|
|
77
|
+
// Normalize directory and check cache for this specific page
|
|
78
|
+
dir = this.normalizeDir(dir);
|
|
54
79
|
const cacheKey = this.makeKey(dir, recursive, page);
|
|
55
80
|
const cached = this.cache.get(cacheKey);
|
|
56
81
|
if (cached && (Date.now() - cached.timestamp) < this.ttlMs) {
|
|
@@ -102,8 +127,8 @@ export class AssetResources {
|
|
|
102
127
|
}
|
|
103
128
|
|
|
104
129
|
/**
|
|
105
|
-
* Directory-based listing
|
|
106
|
-
*
|
|
130
|
+
* Directory-based listing of immediate children using AssetRegistry.
|
|
131
|
+
* Returns both subfolders and assets at the given path.
|
|
107
132
|
*/
|
|
108
133
|
private async listDirectoryOnly(dir: string, _recursive: boolean, limit: number) {
|
|
109
134
|
// Always return only immediate children to avoid timeout and improve navigation
|
|
@@ -112,65 +137,43 @@ export class AssetResources {
|
|
|
112
137
|
import unreal
|
|
113
138
|
import json
|
|
114
139
|
|
|
115
|
-
_dir = r"${dir}"
|
|
140
|
+
_dir = r"${this.normalizeDir(dir)}"
|
|
116
141
|
|
|
117
142
|
try:
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
# Add folders first
|
|
144
|
-
for folder in sorted(immediate_folders):
|
|
145
|
-
result.append({
|
|
146
|
-
'n': folder,
|
|
147
|
-
'p': _dir + '/' + folder,
|
|
148
|
-
'c': 'Folder',
|
|
149
|
-
'isFolder': True
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
# Add immediate assets (limit to prevent socket issues)
|
|
153
|
-
for asset_path in immediate_assets[:min(${limit}, len(immediate_assets))]:
|
|
154
|
-
name = asset_path.split('/')[-1].split('.')[0]
|
|
155
|
-
result.append({
|
|
156
|
-
'n': name,
|
|
157
|
-
'p': asset_path,
|
|
158
|
-
'c': 'Asset'
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
# Always showing immediate children only
|
|
162
|
-
note = f'Showing immediate children of {_dir} ({len(immediate_folders)} folders, {len(immediate_assets)} files)'
|
|
163
|
-
|
|
143
|
+
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
|
144
|
+
# Immediate subfolders
|
|
145
|
+
sub_paths = ar.get_sub_paths(_dir, False)
|
|
146
|
+
folders_list = []
|
|
147
|
+
for p in sub_paths:
|
|
148
|
+
try:
|
|
149
|
+
name = p.split('/')[-1]
|
|
150
|
+
folders_list.append({'n': name, 'p': p})
|
|
151
|
+
except Exception:
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
# Immediate assets at this path
|
|
155
|
+
assets_data = ar.get_assets_by_path(_dir, False)
|
|
156
|
+
assets = []
|
|
157
|
+
for a in assets_data[:${limit}]:
|
|
158
|
+
try:
|
|
159
|
+
assets.append({
|
|
160
|
+
'n': str(a.asset_name),
|
|
161
|
+
'p': str(a.object_path),
|
|
162
|
+
'c': str(a.asset_class)
|
|
163
|
+
})
|
|
164
|
+
except Exception:
|
|
165
|
+
pass
|
|
166
|
+
|
|
164
167
|
print("RESULT:" + json.dumps({
|
|
165
|
-
'success': True,
|
|
166
|
-
'
|
|
167
|
-
'
|
|
168
|
-
'
|
|
169
|
-
'
|
|
170
|
-
'
|
|
168
|
+
'success': True,
|
|
169
|
+
'path': _dir,
|
|
170
|
+
'folders': len(folders_list),
|
|
171
|
+
'files': len(assets),
|
|
172
|
+
'folders_list': folders_list,
|
|
173
|
+
'assets': assets
|
|
171
174
|
}))
|
|
172
175
|
except Exception as e:
|
|
173
|
-
print("RESULT:" + json.dumps({'success': False, 'error': str(e), '
|
|
176
|
+
print("RESULT:" + json.dumps({'success': False, 'error': str(e), 'path': _dir}))
|
|
174
177
|
`.trim();
|
|
175
178
|
|
|
176
179
|
const resp = await this.bridge.executePython(py);
|
|
@@ -188,21 +191,37 @@ except Exception as e:
|
|
|
188
191
|
try {
|
|
189
192
|
const parsed = JSON.parse(m[1]);
|
|
190
193
|
if (parsed.success) {
|
|
191
|
-
//
|
|
192
|
-
const
|
|
194
|
+
// Map folders and assets to a clear response
|
|
195
|
+
const foldersArr = Array.isArray(parsed.folders_list) ? parsed.folders_list.map((f: any) => ({
|
|
196
|
+
Name: f.n,
|
|
197
|
+
Path: f.p,
|
|
198
|
+
Class: 'Folder',
|
|
199
|
+
isFolder: true
|
|
200
|
+
})) : [];
|
|
201
|
+
|
|
202
|
+
const assetsArr = Array.isArray(parsed.assets) ? parsed.assets.map((a: any) => ({
|
|
193
203
|
Name: a.n,
|
|
194
204
|
Path: a.p,
|
|
195
|
-
Class: a.c,
|
|
196
|
-
isFolder:
|
|
197
|
-
}));
|
|
198
|
-
|
|
205
|
+
Class: a.c || 'Asset',
|
|
206
|
+
isFolder: false
|
|
207
|
+
})) : [];
|
|
208
|
+
|
|
209
|
+
const total = foldersArr.length + assetsArr.length;
|
|
210
|
+
const summary = {
|
|
211
|
+
total,
|
|
212
|
+
folders: foldersArr.length,
|
|
213
|
+
assets: assetsArr.length
|
|
214
|
+
};
|
|
215
|
+
|
|
199
216
|
return {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
217
|
+
success: true,
|
|
218
|
+
path: parsed.path || this.normalizeDir(dir),
|
|
219
|
+
summary,
|
|
220
|
+
foldersList: foldersArr,
|
|
221
|
+
assets: assetsArr,
|
|
222
|
+
count: total,
|
|
223
|
+
note: `Immediate children of ${parsed.path || this.normalizeDir(dir)}: ${foldersArr.length} folder(s), ${assetsArr.length} asset(s)`,
|
|
224
|
+
method: 'asset_registry_listing'
|
|
206
225
|
};
|
|
207
226
|
}
|
|
208
227
|
} catch {}
|
|
@@ -213,10 +232,13 @@ except Exception as e:
|
|
|
213
232
|
|
|
214
233
|
// Fallback: return empty with explanation
|
|
215
234
|
return {
|
|
235
|
+
success: true,
|
|
236
|
+
path: this.normalizeDir(dir),
|
|
237
|
+
summary: { total: 0, folders: 0, assets: 0 },
|
|
238
|
+
foldersList: [],
|
|
216
239
|
assets: [],
|
|
217
|
-
warning: '
|
|
218
|
-
|
|
219
|
-
method: 'directory_timeout_fallback'
|
|
240
|
+
warning: 'No items at this path or failed to query AssetRegistry.',
|
|
241
|
+
method: 'asset_registry_fallback'
|
|
220
242
|
};
|
|
221
243
|
}
|
|
222
244
|
|
|
@@ -226,9 +248,11 @@ except Exception as e:
|
|
|
226
248
|
return false;
|
|
227
249
|
}
|
|
228
250
|
|
|
251
|
+
// Normalize asset path (support users passing /Content/...)
|
|
252
|
+
const ap = this.normalizeDir(assetPath);
|
|
229
253
|
const py = `
|
|
230
254
|
import unreal
|
|
231
|
-
apath = r"${
|
|
255
|
+
apath = r"${ap}"
|
|
232
256
|
try:
|
|
233
257
|
exists = unreal.EditorAssetLibrary.does_asset_exist(apath)
|
|
234
258
|
print("RESULT:{'success': True, 'exists': %s}" % ('True' if exists else 'False'))
|
package/src/resources/levels.ts
CHANGED
|
@@ -56,9 +56,9 @@ export class LevelResources {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
async saveCurrentLevel() {
|
|
59
|
-
//
|
|
59
|
+
// Strict modern API: require LevelEditorSubsystem
|
|
60
60
|
try {
|
|
61
|
-
const py = '\nimport unreal, json\ntry:\n les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)\n if les
|
|
61
|
+
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();
|
|
62
62
|
const resp: any = await this.bridge.executePython(py);
|
|
63
63
|
// Handle LogOutput format from executePython
|
|
64
64
|
let out = '';
|
package/src/tools/assets.ts
CHANGED
|
@@ -7,8 +7,12 @@ export class AssetTools {
|
|
|
7
7
|
|
|
8
8
|
async importAsset(sourcePath: string, destinationPath: string) {
|
|
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
|
|
|
13
17
|
// Create test FBX file if it's a test file
|
|
14
18
|
if (sourcePath.includes('test_model.fbx')) {
|
|
@@ -53,17 +53,8 @@ try:
|
|
|
53
53
|
proc_mesh_comp = proc_actor.get_component_by_class(unreal.ProceduralMeshComponent)
|
|
54
54
|
except:
|
|
55
55
|
# Fallback: Create empty actor and add ProceduralMeshComponent
|
|
56
|
-
|
|
57
|
-
|
|
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()
|
|
56
|
+
# If spawning ProceduralMeshActor failed, surface a clear error about the plugin requirement
|
|
57
|
+
raise Exception("Failed to spawn ProceduralMeshActor. Ensure the 'Procedural Mesh Component' plugin is enabled and available.")
|
|
67
58
|
|
|
68
59
|
if proc_mesh_comp:
|
|
69
60
|
# Generate terrain mesh
|
|
@@ -193,6 +184,10 @@ try:
|
|
|
193
184
|
subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
|
|
194
185
|
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
|
|
195
186
|
|
|
187
|
+
# Validate Procedural Foliage plugin/classes are available
|
|
188
|
+
if not hasattr(unreal, 'ProceduralFoliageVolume') or not hasattr(unreal, 'ProceduralFoliageSpawner'):
|
|
189
|
+
raise Exception("Procedural Foliage plugin not available. Please enable the 'Procedural Foliage' plugin and try again.")
|
|
190
|
+
|
|
196
191
|
# Create ProceduralFoliageVolume
|
|
197
192
|
volume_actor = subsys.spawn_actor_from_class(
|
|
198
193
|
unreal.ProceduralFoliageVolume,
|
|
@@ -200,7 +195,7 @@ try:
|
|
|
200
195
|
unreal.Rotator(0, 0, 0)
|
|
201
196
|
)
|
|
202
197
|
volume_actor.set_actor_label(f"{name}_ProceduralFoliageVolume")
|
|
203
|
-
volume_actor.set_actor_scale3d(bounds_size / 100) # Scale is in meters
|
|
198
|
+
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
199
|
|
|
205
200
|
# Get the procedural component
|
|
206
201
|
proc_comp = volume_actor.procedural_component
|
|
@@ -231,13 +226,14 @@ try:
|
|
|
231
226
|
)
|
|
232
227
|
|
|
233
228
|
if spawner:
|
|
234
|
-
# Configure spawner
|
|
235
|
-
spawner.random_seed
|
|
236
|
-
spawner.tile_size
|
|
229
|
+
# Configure spawner (use set_editor_property for read-only attributes)
|
|
230
|
+
spawner.set_editor_property('random_seed', seed)
|
|
231
|
+
spawner.set_editor_property('tile_size', max(bounds_size.x, bounds_size.y))
|
|
237
232
|
|
|
238
233
|
# Create foliage types
|
|
239
234
|
foliage_types = []
|
|
240
|
-
|
|
235
|
+
ft_input = json.loads(r'''${JSON.stringify(params.foliageTypes)}''')
|
|
236
|
+
for ft_params in ft_input:
|
|
241
237
|
# Load mesh
|
|
242
238
|
mesh = unreal.EditorAssetLibrary.load_asset(ft_params['meshPath'])
|
|
243
239
|
if mesh:
|
|
@@ -256,26 +252,26 @@ try:
|
|
|
256
252
|
ft_asset = unreal.FoliageType_InstancedStaticMesh()
|
|
257
253
|
|
|
258
254
|
if ft_asset:
|
|
259
|
-
# Configure foliage type
|
|
260
|
-
ft_asset.mesh
|
|
261
|
-
ft_asset.density
|
|
262
|
-
ft_asset.random_yaw
|
|
263
|
-
ft_asset.align_to_normal
|
|
255
|
+
# Configure foliage type (use set_editor_property)
|
|
256
|
+
ft_asset.set_editor_property('mesh', mesh)
|
|
257
|
+
ft_asset.set_editor_property('density', ft_params.get('density', 1.0))
|
|
258
|
+
ft_asset.set_editor_property('random_yaw', ft_params.get('randomYaw', True))
|
|
259
|
+
ft_asset.set_editor_property('align_to_normal', ft_params.get('alignToNormal', True))
|
|
264
260
|
|
|
265
261
|
min_scale = ft_params.get('minScale', 0.8)
|
|
266
262
|
max_scale = ft_params.get('maxScale', 1.2)
|
|
267
|
-
ft_asset.scale_x
|
|
268
|
-
ft_asset.scale_y
|
|
269
|
-
ft_asset.scale_z
|
|
263
|
+
ft_asset.set_editor_property('scale_x', unreal.FloatInterval(min_scale, max_scale))
|
|
264
|
+
ft_asset.set_editor_property('scale_y', unreal.FloatInterval(min_scale, max_scale))
|
|
265
|
+
ft_asset.set_editor_property('scale_z', unreal.FloatInterval(min_scale, max_scale))
|
|
270
266
|
|
|
271
|
-
ft_obj.foliage_type_object
|
|
267
|
+
ft_obj.set_editor_property('foliage_type_object', ft_asset)
|
|
272
268
|
foliage_types.append(ft_obj)
|
|
273
269
|
|
|
274
270
|
# Set foliage types on spawner
|
|
275
|
-
spawner.foliage_types
|
|
271
|
+
spawner.set_editor_property('foliage_types', foliage_types)
|
|
276
272
|
|
|
277
273
|
# Assign spawner to component
|
|
278
|
-
proc_comp.foliage_spawner
|
|
274
|
+
proc_comp.set_editor_property('foliage_spawner', spawner)
|
|
279
275
|
|
|
280
276
|
# Save spawner asset
|
|
281
277
|
unreal.EditorAssetLibrary.save_asset(spawner.get_path_name())
|
|
@@ -455,23 +451,31 @@ try:
|
|
|
455
451
|
# Load mesh
|
|
456
452
|
mesh = unreal.EditorAssetLibrary.load_asset(mesh_path)
|
|
457
453
|
if mesh:
|
|
458
|
-
# Configure grass type
|
|
454
|
+
# Configure grass type (use set_editor_property)
|
|
459
455
|
grass_variety = unreal.GrassVariety()
|
|
460
|
-
grass_variety.grass_mesh
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
grass_variety.
|
|
465
|
-
grass_variety.
|
|
466
|
-
grass_variety.
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
grass_variety.
|
|
471
|
-
|
|
472
|
-
|
|
456
|
+
grass_variety.set_editor_property('grass_mesh', mesh)
|
|
457
|
+
# GrassDensity is PerPlatformFloat in UE5+; set via struct instance
|
|
458
|
+
pp_density = unreal.PerPlatformFloat()
|
|
459
|
+
pp_density.set_editor_property('Default', float(density * 100.0))
|
|
460
|
+
grass_variety.set_editor_property('grass_density', pp_density)
|
|
461
|
+
grass_variety.set_editor_property('use_grid', True)
|
|
462
|
+
grass_variety.set_editor_property('placement_jitter', 1.0)
|
|
463
|
+
# Set cull distances as PerPlatformInt and LOD as int (engine uses mixed types here)
|
|
464
|
+
pp_start = unreal.PerPlatformInt()
|
|
465
|
+
pp_start.set_editor_property('Default', 10000)
|
|
466
|
+
grass_variety.set_editor_property('start_cull_distance', pp_start)
|
|
467
|
+
pp_end = unreal.PerPlatformInt()
|
|
468
|
+
pp_end.set_editor_property('Default', 20000)
|
|
469
|
+
grass_variety.set_editor_property('end_cull_distance', pp_end)
|
|
470
|
+
grass_variety.set_editor_property('min_lod', -1)
|
|
471
|
+
grass_variety.set_editor_property('scaling', unreal.GrassScaling.UNIFORM)
|
|
472
|
+
grass_variety.set_editor_property('scale_x', unreal.FloatInterval(min_scale, max_scale))
|
|
473
|
+
grass_variety.set_editor_property('scale_y', unreal.FloatInterval(min_scale, max_scale))
|
|
474
|
+
grass_variety.set_editor_property('scale_z', unreal.FloatInterval(min_scale, max_scale))
|
|
475
|
+
grass_variety.set_editor_property('random_rotation', True)
|
|
476
|
+
grass_variety.set_editor_property('align_to_surface', True)
|
|
473
477
|
|
|
474
|
-
grass_type.grass_varieties
|
|
478
|
+
grass_type.set_editor_property('grass_varieties', [grass_variety])
|
|
475
479
|
|
|
476
480
|
# Save asset
|
|
477
481
|
unreal.EditorAssetLibrary.save_asset(grass_type.get_path_name())
|