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 +1 -1
- package/README.md +1 -2
- package/dist/index.js +16 -18
- 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 +46 -3
- package/dist/tools/consolidated-tool-handlers.js +327 -717
- package/dist/tools/debug.js +4 -6
- package/dist/tools/rc.js +2 -2
- package/dist/tools/sequence.js +21 -2
- package/dist/utils/response-validator.d.ts +6 -1
- package/dist/utils/response-validator.js +41 -14
- package/package.json +4 -4
- package/server.json +2 -2
- package/src/index.ts +18 -19
- 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 +46 -3
- package/src/tools/consolidated-tool-handlers.ts +313 -746
- package/src/tools/debug.ts +4 -6
- package/src/tools/rc.ts +2 -2
- package/src/tools/sequence.ts +21 -2
- package/src/utils/response-validator.ts +46 -18
- package/dist/tools/tool-definitions.d.ts +0 -4919
- package/dist/tools/tool-definitions.js +0 -1065
- package/dist/tools/tool-handlers.d.ts +0 -47
- package/dist/tools/tool-handlers.js +0 -863
- package/src/tools/tool-definitions.ts +0 -1081
- package/src/tools/tool-handlers.ts +0 -973
package/.env.production
CHANGED
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
|
|
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
|
-
//
|
|
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.
|
|
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 =
|
|
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 -
|
|
396
|
+
// Handle tool listing - consolidated tools only
|
|
398
397
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
399
|
-
log.info(
|
|
398
|
+
log.info('Serving consolidated tools');
|
|
400
399
|
return {
|
|
401
|
-
tools:
|
|
400
|
+
tools: consolidatedToolDefinitions
|
|
402
401
|
};
|
|
403
402
|
});
|
|
404
|
-
// Handle tool calls -
|
|
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
|
-
//
|
|
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
|
|
18
|
-
*
|
|
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>;
|
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())
|