unreal-engine-mcp-server 0.2.1
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/.dockerignore +57 -0
- package/.env.production +25 -0
- package/.eslintrc.json +54 -0
- package/.github/workflows/publish-mcp.yml +75 -0
- package/Dockerfile +54 -0
- package/LICENSE +21 -0
- package/Public/icon.png +0 -0
- package/README.md +209 -0
- package/claude_desktop_config_example.json +13 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +7 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +484 -0
- package/dist/prompts/index.d.ts +14 -0
- package/dist/prompts/index.js +38 -0
- package/dist/python-utils.d.ts +29 -0
- package/dist/python-utils.js +54 -0
- package/dist/resources/actors.d.ts +13 -0
- package/dist/resources/actors.js +83 -0
- package/dist/resources/assets.d.ts +23 -0
- package/dist/resources/assets.js +245 -0
- package/dist/resources/levels.d.ts +17 -0
- package/dist/resources/levels.js +94 -0
- package/dist/tools/actors.d.ts +51 -0
- package/dist/tools/actors.js +459 -0
- package/dist/tools/animation.d.ts +196 -0
- package/dist/tools/animation.js +579 -0
- package/dist/tools/assets.d.ts +21 -0
- package/dist/tools/assets.js +304 -0
- package/dist/tools/audio.d.ts +170 -0
- package/dist/tools/audio.js +416 -0
- package/dist/tools/blueprint.d.ts +144 -0
- package/dist/tools/blueprint.js +652 -0
- package/dist/tools/build_environment_advanced.d.ts +66 -0
- package/dist/tools/build_environment_advanced.js +484 -0
- package/dist/tools/consolidated-tool-definitions.d.ts +2598 -0
- package/dist/tools/consolidated-tool-definitions.js +607 -0
- package/dist/tools/consolidated-tool-handlers.d.ts +2 -0
- package/dist/tools/consolidated-tool-handlers.js +1050 -0
- package/dist/tools/debug.d.ts +185 -0
- package/dist/tools/debug.js +265 -0
- package/dist/tools/editor.d.ts +88 -0
- package/dist/tools/editor.js +365 -0
- package/dist/tools/engine.d.ts +30 -0
- package/dist/tools/engine.js +36 -0
- package/dist/tools/foliage.d.ts +155 -0
- package/dist/tools/foliage.js +525 -0
- package/dist/tools/introspection.d.ts +98 -0
- package/dist/tools/introspection.js +683 -0
- package/dist/tools/landscape.d.ts +158 -0
- package/dist/tools/landscape.js +375 -0
- package/dist/tools/level.d.ts +110 -0
- package/dist/tools/level.js +362 -0
- package/dist/tools/lighting.d.ts +159 -0
- package/dist/tools/lighting.js +1179 -0
- package/dist/tools/materials.d.ts +34 -0
- package/dist/tools/materials.js +146 -0
- package/dist/tools/niagara.d.ts +145 -0
- package/dist/tools/niagara.js +289 -0
- package/dist/tools/performance.d.ts +163 -0
- package/dist/tools/performance.js +412 -0
- package/dist/tools/physics.d.ts +189 -0
- package/dist/tools/physics.js +784 -0
- package/dist/tools/rc.d.ts +110 -0
- package/dist/tools/rc.js +363 -0
- package/dist/tools/sequence.d.ts +112 -0
- package/dist/tools/sequence.js +675 -0
- package/dist/tools/tool-definitions.d.ts +4919 -0
- package/dist/tools/tool-definitions.js +891 -0
- package/dist/tools/tool-handlers.d.ts +47 -0
- package/dist/tools/tool-handlers.js +830 -0
- package/dist/tools/ui.d.ts +171 -0
- package/dist/tools/ui.js +337 -0
- package/dist/tools/visual.d.ts +29 -0
- package/dist/tools/visual.js +67 -0
- package/dist/types/env.d.ts +10 -0
- package/dist/types/env.js +18 -0
- package/dist/types/index.d.ts +323 -0
- package/dist/types/index.js +28 -0
- package/dist/types/tool-types.d.ts +274 -0
- package/dist/types/tool-types.js +13 -0
- package/dist/unreal-bridge.d.ts +126 -0
- package/dist/unreal-bridge.js +992 -0
- package/dist/utils/cache-manager.d.ts +64 -0
- package/dist/utils/cache-manager.js +176 -0
- package/dist/utils/error-handler.d.ts +66 -0
- package/dist/utils/error-handler.js +243 -0
- package/dist/utils/errors.d.ts +133 -0
- package/dist/utils/errors.js +256 -0
- package/dist/utils/http.d.ts +26 -0
- package/dist/utils/http.js +135 -0
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.js +32 -0
- package/dist/utils/normalize.d.ts +17 -0
- package/dist/utils/normalize.js +49 -0
- package/dist/utils/response-validator.d.ts +34 -0
- package/dist/utils/response-validator.js +121 -0
- package/dist/utils/safe-json.d.ts +4 -0
- package/dist/utils/safe-json.js +97 -0
- package/dist/utils/stdio-redirect.d.ts +2 -0
- package/dist/utils/stdio-redirect.js +20 -0
- package/dist/utils/validation.d.ts +50 -0
- package/dist/utils/validation.js +173 -0
- package/mcp-config-example.json +14 -0
- package/package.json +63 -0
- package/server.json +60 -0
- package/src/cli.ts +7 -0
- package/src/index.ts +543 -0
- package/src/prompts/index.ts +51 -0
- package/src/python/editor_compat.py +181 -0
- package/src/python-utils.ts +57 -0
- package/src/resources/actors.ts +92 -0
- package/src/resources/assets.ts +251 -0
- package/src/resources/levels.ts +83 -0
- package/src/tools/actors.ts +480 -0
- package/src/tools/animation.ts +713 -0
- package/src/tools/assets.ts +305 -0
- package/src/tools/audio.ts +548 -0
- package/src/tools/blueprint.ts +736 -0
- package/src/tools/build_environment_advanced.ts +526 -0
- package/src/tools/consolidated-tool-definitions.ts +619 -0
- package/src/tools/consolidated-tool-handlers.ts +1093 -0
- package/src/tools/debug.ts +368 -0
- package/src/tools/editor.ts +360 -0
- package/src/tools/engine.ts +32 -0
- package/src/tools/foliage.ts +652 -0
- package/src/tools/introspection.ts +778 -0
- package/src/tools/landscape.ts +523 -0
- package/src/tools/level.ts +410 -0
- package/src/tools/lighting.ts +1316 -0
- package/src/tools/materials.ts +148 -0
- package/src/tools/niagara.ts +312 -0
- package/src/tools/performance.ts +549 -0
- package/src/tools/physics.ts +924 -0
- package/src/tools/rc.ts +437 -0
- package/src/tools/sequence.ts +791 -0
- package/src/tools/tool-definitions.ts +907 -0
- package/src/tools/tool-handlers.ts +941 -0
- package/src/tools/ui.ts +499 -0
- package/src/tools/visual.ts +60 -0
- package/src/types/env.ts +27 -0
- package/src/types/index.ts +414 -0
- package/src/types/tool-types.ts +343 -0
- package/src/unreal-bridge.ts +1118 -0
- package/src/utils/cache-manager.ts +213 -0
- package/src/utils/error-handler.ts +320 -0
- package/src/utils/errors.ts +312 -0
- package/src/utils/http.ts +184 -0
- package/src/utils/logger.ts +30 -0
- package/src/utils/normalize.ts +54 -0
- package/src/utils/response-validator.ts +145 -0
- package/src/utils/safe-json.ts +112 -0
- package/src/utils/stdio-redirect.ts +18 -0
- package/src/utils/validation.ts +212 -0
- package/tsconfig.json +33 -0
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
// Foliage tools for Unreal Engine
|
|
2
|
+
import { UnrealBridge } from '../unreal-bridge.js';
|
|
3
|
+
|
|
4
|
+
export class FoliageTools {
|
|
5
|
+
constructor(private bridge: UnrealBridge) {}
|
|
6
|
+
|
|
7
|
+
// NOTE: We intentionally avoid issuing Unreal console commands here because
|
|
8
|
+
// they have proven unreliable and generate engine warnings (failed FindConsoleObject).
|
|
9
|
+
// Instead, we validate inputs and return structured results. Actual foliage
|
|
10
|
+
// authoring should be implemented via Python APIs in future iterations.
|
|
11
|
+
|
|
12
|
+
// Add foliage type via Python (creates FoliageType asset properly)
|
|
13
|
+
async addFoliageType(params: {
|
|
14
|
+
name: string;
|
|
15
|
+
meshPath: string;
|
|
16
|
+
density?: number;
|
|
17
|
+
radius?: number;
|
|
18
|
+
minScale?: number;
|
|
19
|
+
maxScale?: number;
|
|
20
|
+
alignToNormal?: boolean;
|
|
21
|
+
randomYaw?: boolean;
|
|
22
|
+
groundSlope?: number;
|
|
23
|
+
}) {
|
|
24
|
+
// Basic validation to prevent bad inputs like 'undefined' and empty strings
|
|
25
|
+
const errors: string[] = [];
|
|
26
|
+
const name = String(params?.name ?? '').trim();
|
|
27
|
+
const meshPath = String(params?.meshPath ?? '').trim();
|
|
28
|
+
|
|
29
|
+
if (!name || name.toLowerCase() === 'undefined' || name.toLowerCase() === 'any') {
|
|
30
|
+
errors.push(`Invalid foliage type name: '${params?.name}'`);
|
|
31
|
+
}
|
|
32
|
+
if (!meshPath || meshPath.toLowerCase() === 'undefined') {
|
|
33
|
+
errors.push(`Invalid meshPath: '${params?.meshPath}'`);
|
|
34
|
+
}
|
|
35
|
+
if (params?.density !== undefined) {
|
|
36
|
+
if (typeof params.density !== 'number' || !isFinite(params.density) || params.density < 0) {
|
|
37
|
+
errors.push(`Invalid density: '${params.density}' (must be non-negative finite number)`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (params?.minScale !== undefined || params?.maxScale !== undefined) {
|
|
41
|
+
const minS = params?.minScale ?? 1;
|
|
42
|
+
const maxS = params?.maxScale ?? 1;
|
|
43
|
+
if (typeof minS !== 'number' || typeof maxS !== 'number' || minS <= 0 || maxS <= 0 || maxS < minS) {
|
|
44
|
+
errors.push(`Invalid scale range: min=${params?.minScale}, max=${params?.maxScale}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (errors.length > 0) {
|
|
48
|
+
return { success: false, error: errors.join('; ') };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const py = `
|
|
52
|
+
import unreal, json
|
|
53
|
+
|
|
54
|
+
name = ${JSON.stringify(name)}
|
|
55
|
+
mesh_path = ${JSON.stringify(meshPath)}
|
|
56
|
+
fallback_mesh = '/Engine/EngineMeshes/Sphere'
|
|
57
|
+
package_path = '/Game/Foliage/Types'
|
|
58
|
+
|
|
59
|
+
res = {'success': False, 'created': False, 'asset_path': '', 'used_mesh': '', 'exists_after': False, 'method': '', 'note': ''}
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
# Ensure package directory
|
|
63
|
+
try:
|
|
64
|
+
if not unreal.EditorAssetLibrary.does_directory_exist(package_path):
|
|
65
|
+
unreal.EditorAssetLibrary.make_directory(package_path)
|
|
66
|
+
except Exception as e:
|
|
67
|
+
res['note'] += f"; make_directory failed: {e}"
|
|
68
|
+
|
|
69
|
+
# Load mesh or fallback
|
|
70
|
+
mesh = None
|
|
71
|
+
try:
|
|
72
|
+
if unreal.EditorAssetLibrary.does_asset_exist(mesh_path):
|
|
73
|
+
mesh = unreal.EditorAssetLibrary.load_asset(mesh_path)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
res['note'] += f"; could not check/load mesh_path: {e}"
|
|
76
|
+
|
|
77
|
+
if not mesh:
|
|
78
|
+
mesh = unreal.EditorAssetLibrary.load_asset(fallback_mesh)
|
|
79
|
+
res['note'] += '; fallback_mesh_used'
|
|
80
|
+
if mesh:
|
|
81
|
+
res['used_mesh'] = str(mesh.get_path_name())
|
|
82
|
+
|
|
83
|
+
# Create FoliageType asset using proper UE5 API
|
|
84
|
+
asset = None
|
|
85
|
+
try:
|
|
86
|
+
asset_path = f"{package_path}/{name}"
|
|
87
|
+
|
|
88
|
+
# Check if asset already exists
|
|
89
|
+
if unreal.EditorAssetLibrary.does_asset_exist(asset_path):
|
|
90
|
+
asset = unreal.EditorAssetLibrary.load_asset(asset_path)
|
|
91
|
+
res['note'] += '; loaded_existing'
|
|
92
|
+
else:
|
|
93
|
+
# Create FoliageType_InstancedStaticMesh using proper API
|
|
94
|
+
try:
|
|
95
|
+
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
|
|
96
|
+
|
|
97
|
+
# Try to create factory and set mesh property
|
|
98
|
+
factory = None
|
|
99
|
+
try:
|
|
100
|
+
factory = unreal.FoliageType_InstancedStaticMeshFactory()
|
|
101
|
+
# Try different property names for different UE versions
|
|
102
|
+
try:
|
|
103
|
+
factory.set_editor_property('mesh', mesh)
|
|
104
|
+
except:
|
|
105
|
+
try:
|
|
106
|
+
factory.set_editor_property('static_mesh', mesh)
|
|
107
|
+
except:
|
|
108
|
+
try:
|
|
109
|
+
factory.set_editor_property('source_mesh', mesh)
|
|
110
|
+
except:
|
|
111
|
+
pass # Factory will use default or no mesh
|
|
112
|
+
except:
|
|
113
|
+
res['note'] += '; factory_creation_failed'
|
|
114
|
+
factory = None
|
|
115
|
+
|
|
116
|
+
# Create the asset with or without factory
|
|
117
|
+
if factory:
|
|
118
|
+
asset = asset_tools.create_asset(
|
|
119
|
+
asset_name=name,
|
|
120
|
+
package_path=package_path,
|
|
121
|
+
asset_class=unreal.FoliageType_InstancedStaticMesh,
|
|
122
|
+
factory=factory
|
|
123
|
+
)
|
|
124
|
+
else:
|
|
125
|
+
# Try without factory
|
|
126
|
+
asset = asset_tools.create_asset(
|
|
127
|
+
asset_name=name,
|
|
128
|
+
package_path=package_path,
|
|
129
|
+
asset_class=unreal.FoliageType_InstancedStaticMesh,
|
|
130
|
+
factory=None
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if asset:
|
|
134
|
+
# Configure foliage properties
|
|
135
|
+
asset.set_editor_property('mesh', mesh)
|
|
136
|
+
if ${params.density !== undefined ? params.density : 1.0} >= 0:
|
|
137
|
+
asset.set_editor_property('density', ${params.density !== undefined ? params.density : 1.0})
|
|
138
|
+
if ${params.randomYaw === false ? 'False' : 'True'}:
|
|
139
|
+
asset.set_editor_property('random_yaw', True)
|
|
140
|
+
if ${params.alignToNormal === false ? 'False' : 'True'}:
|
|
141
|
+
asset.set_editor_property('align_to_normal', True)
|
|
142
|
+
|
|
143
|
+
# Set scale range
|
|
144
|
+
min_scale = ${params.minScale || 0.8}
|
|
145
|
+
max_scale = ${params.maxScale || 1.2}
|
|
146
|
+
asset.set_editor_property('scale_x', (min_scale, max_scale))
|
|
147
|
+
asset.set_editor_property('scale_y', (min_scale, max_scale))
|
|
148
|
+
asset.set_editor_property('scale_z', (min_scale, max_scale))
|
|
149
|
+
|
|
150
|
+
res['note'] += '; created_with_factory'
|
|
151
|
+
else:
|
|
152
|
+
res['note'] += '; factory_creation_failed'
|
|
153
|
+
except AttributeError:
|
|
154
|
+
# Fallback if factory doesn't exist - use base FoliageType
|
|
155
|
+
try:
|
|
156
|
+
asset = asset_tools.create_asset(
|
|
157
|
+
asset_name=name,
|
|
158
|
+
package_path=package_path,
|
|
159
|
+
asset_class=unreal.FoliageType,
|
|
160
|
+
factory=None
|
|
161
|
+
)
|
|
162
|
+
if asset:
|
|
163
|
+
res['note'] += '; created_base_foliage_type'
|
|
164
|
+
except Exception as e2:
|
|
165
|
+
res['note'] += f"; base_creation_failed: {e2}"
|
|
166
|
+
except Exception as e:
|
|
167
|
+
res['note'] += f"; factory_creation_failed: {e}"
|
|
168
|
+
asset = None
|
|
169
|
+
except Exception as e:
|
|
170
|
+
res['note'] += f"; create_asset failed: {e}"
|
|
171
|
+
asset = None
|
|
172
|
+
|
|
173
|
+
if asset and mesh:
|
|
174
|
+
try:
|
|
175
|
+
# Set the mesh property (different property names in different UE versions)
|
|
176
|
+
try:
|
|
177
|
+
asset.set_editor_property('mesh', mesh)
|
|
178
|
+
except:
|
|
179
|
+
try:
|
|
180
|
+
asset.set_editor_property('static_mesh', mesh)
|
|
181
|
+
except:
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
# Save the asset
|
|
185
|
+
unreal.EditorAssetLibrary.save_asset(asset.get_path_name())
|
|
186
|
+
res['asset_path'] = str(asset.get_path_name())
|
|
187
|
+
res['created'] = True
|
|
188
|
+
res['method'] = 'FoliageType_InstancedStaticMesh'
|
|
189
|
+
except Exception as e:
|
|
190
|
+
res['note'] += f"; set/save asset failed: {e}"
|
|
191
|
+
elif not asset:
|
|
192
|
+
res['note'] += "; asset creation returned None"
|
|
193
|
+
elif not mesh:
|
|
194
|
+
res['note'] += "; mesh object is None, cannot assign to foliage type"
|
|
195
|
+
|
|
196
|
+
# Verify existence
|
|
197
|
+
res['exists_after'] = unreal.EditorAssetLibrary.does_asset_exist(res['asset_path']) if res['asset_path'] else False
|
|
198
|
+
res['success'] = res['exists_after'] or res['created']
|
|
199
|
+
|
|
200
|
+
except Exception as e:
|
|
201
|
+
res['success'] = False
|
|
202
|
+
res['note'] += f"; fatal: {e}"
|
|
203
|
+
|
|
204
|
+
print('RESULT:' + json.dumps(res))
|
|
205
|
+
`.trim();
|
|
206
|
+
|
|
207
|
+
const pyResp = await this.bridge.executePython(py);
|
|
208
|
+
let out = '';
|
|
209
|
+
if (pyResp?.LogOutput && Array.isArray(pyResp.LogOutput)) out = pyResp.LogOutput.map((l: any) => l.Output || '').join('');
|
|
210
|
+
else if (typeof pyResp === 'string') out = pyResp; else out = JSON.stringify(pyResp);
|
|
211
|
+
const m = out.match(/RESULT:({.*})/);
|
|
212
|
+
if (m) {
|
|
213
|
+
try {
|
|
214
|
+
const parsed = JSON.parse(m[1]);
|
|
215
|
+
if (!parsed.success) {
|
|
216
|
+
return { success: false, error: parsed.note || 'Add foliage type failed' };
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
success: true,
|
|
220
|
+
created: parsed.created,
|
|
221
|
+
exists: parsed.exists_after,
|
|
222
|
+
method: parsed.method,
|
|
223
|
+
assetPath: parsed.asset_path,
|
|
224
|
+
usedMesh: parsed.used_mesh,
|
|
225
|
+
note: parsed.note,
|
|
226
|
+
message: parsed.exists_after ? `Foliage type '${name}' ready (${parsed.method || 'Unknown'})` : `Created foliage '${name}' but verification did not find it yet`
|
|
227
|
+
};
|
|
228
|
+
} catch {
|
|
229
|
+
return { success: false, error: 'Failed to parse Python result' };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return { success: false, error: 'No parseable result from Python' };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Paint foliage by placing HISM instances (editor-only)
|
|
236
|
+
async paintFoliage(params: {
|
|
237
|
+
foliageType: string;
|
|
238
|
+
position: [number, number, number];
|
|
239
|
+
brushSize?: number;
|
|
240
|
+
paintDensity?: number;
|
|
241
|
+
eraseMode?: boolean;
|
|
242
|
+
}) {
|
|
243
|
+
const errors: string[] = [];
|
|
244
|
+
const foliageType = String(params?.foliageType ?? '').trim();
|
|
245
|
+
const pos = Array.isArray(params?.position) ? params.position : [0,0,0];
|
|
246
|
+
|
|
247
|
+
if (!foliageType || foliageType.toLowerCase() === 'undefined' || foliageType.toLowerCase() === 'any') {
|
|
248
|
+
errors.push(`Invalid foliageType: '${params?.foliageType}'`);
|
|
249
|
+
}
|
|
250
|
+
if (!Array.isArray(pos) || pos.length !== 3 || pos.some(v => typeof v !== 'number' || !isFinite(v))) {
|
|
251
|
+
errors.push(`Invalid position: '${JSON.stringify(params?.position)}'`);
|
|
252
|
+
}
|
|
253
|
+
if (params?.brushSize !== undefined) {
|
|
254
|
+
if (typeof params.brushSize !== 'number' || !isFinite(params.brushSize) || params.brushSize < 0) {
|
|
255
|
+
errors.push(`Invalid brushSize: '${params.brushSize}' (must be non-negative finite number)`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (params?.paintDensity !== undefined) {
|
|
259
|
+
if (typeof params.paintDensity !== 'number' || !isFinite(params.paintDensity) || params.paintDensity < 0) {
|
|
260
|
+
errors.push(`Invalid paintDensity: '${params.paintDensity}' (must be non-negative finite number)`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (errors.length > 0) {
|
|
265
|
+
return { success: false, error: errors.join('; ') };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const brush = Number.isFinite(params.brushSize as number) ? (params.brushSize as number) : 300;
|
|
269
|
+
const py = `
|
|
270
|
+
import unreal, json, random, math
|
|
271
|
+
|
|
272
|
+
res = {'success': False, 'added': 0, 'actor': '', 'component': '', 'used_mesh': '', 'note': ''}
|
|
273
|
+
foliage_type_name = ${JSON.stringify(foliageType)}
|
|
274
|
+
px, py, pz = ${pos[0]}, ${pos[1]}, ${pos[2]}
|
|
275
|
+
radius = float(${brush}) / 2.0
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
actor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
|
|
279
|
+
all_actors = actor_sub.get_all_level_actors() if actor_sub else []
|
|
280
|
+
|
|
281
|
+
# Find or create a container actor
|
|
282
|
+
label = f"FoliageContainer_{foliage_type_name}"
|
|
283
|
+
container = None
|
|
284
|
+
for a in all_actors:
|
|
285
|
+
try:
|
|
286
|
+
if a.get_actor_label() == label:
|
|
287
|
+
container = a
|
|
288
|
+
break
|
|
289
|
+
except Exception:
|
|
290
|
+
pass
|
|
291
|
+
if not container:
|
|
292
|
+
# Spawn actor that can hold components
|
|
293
|
+
container = unreal.EditorLevelLibrary.spawn_actor_from_class(unreal.StaticMeshActor, unreal.Vector(px, py, pz))
|
|
294
|
+
try:
|
|
295
|
+
container.set_actor_label(label)
|
|
296
|
+
except Exception:
|
|
297
|
+
pass
|
|
298
|
+
|
|
299
|
+
# Resolve mesh from FoliageType asset
|
|
300
|
+
mesh = None
|
|
301
|
+
fol_asset_path = f"/Game/Foliage/Types/{foliage_type_name}.{foliage_type_name}"
|
|
302
|
+
if unreal.EditorAssetLibrary.does_asset_exist(fol_asset_path):
|
|
303
|
+
try:
|
|
304
|
+
ft_asset = unreal.EditorAssetLibrary.load_asset(fol_asset_path)
|
|
305
|
+
mesh = ft_asset.get_editor_property('mesh')
|
|
306
|
+
except Exception:
|
|
307
|
+
mesh = None
|
|
308
|
+
|
|
309
|
+
if not mesh:
|
|
310
|
+
mesh = unreal.EditorAssetLibrary.load_asset('/Engine/EngineMeshes/Sphere')
|
|
311
|
+
res['note'] += '; used_fallback_mesh'
|
|
312
|
+
|
|
313
|
+
if mesh:
|
|
314
|
+
res['used_mesh'] = str(mesh.get_path_name())
|
|
315
|
+
|
|
316
|
+
# Since HISM components and add_component don't work in this version,
|
|
317
|
+
# spawn individual StaticMeshActors for each instance
|
|
318
|
+
target_count = max(5, int(radius / 20.0))
|
|
319
|
+
added = 0
|
|
320
|
+
for i in range(target_count):
|
|
321
|
+
ang = random.random() * math.tau
|
|
322
|
+
r = random.random() * radius
|
|
323
|
+
x, y, z = px + math.cos(ang) * r, py + math.sin(ang) * r, pz
|
|
324
|
+
try:
|
|
325
|
+
# Spawn static mesh actor at position
|
|
326
|
+
inst_actor = unreal.EditorLevelLibrary.spawn_actor_from_class(
|
|
327
|
+
unreal.StaticMeshActor,
|
|
328
|
+
unreal.Vector(x, y, z),
|
|
329
|
+
unreal.Rotator(0, random.random()*360.0, 0)
|
|
330
|
+
)
|
|
331
|
+
if inst_actor and mesh:
|
|
332
|
+
# Set mesh on the actor's component
|
|
333
|
+
try:
|
|
334
|
+
mesh_comp = inst_actor.static_mesh_component
|
|
335
|
+
if mesh_comp:
|
|
336
|
+
mesh_comp.set_static_mesh(mesh)
|
|
337
|
+
inst_actor.set_actor_label(f"{foliage_type_name}_instance_{i}")
|
|
338
|
+
# Group under the container for organization
|
|
339
|
+
inst_actor.attach_to_actor(container, "", unreal.AttachmentRule.KEEP_WORLD, unreal.AttachmentRule.KEEP_WORLD, unreal.AttachmentRule.KEEP_WORLD, False)
|
|
340
|
+
added += 1
|
|
341
|
+
except Exception as e:
|
|
342
|
+
res['note'] += f"; instance_{i} setup failed: {e}"
|
|
343
|
+
except Exception as e:
|
|
344
|
+
res['note'] += f"; spawn instance_{i} failed: {e}"
|
|
345
|
+
|
|
346
|
+
res['added'] = added
|
|
347
|
+
res['actor'] = container.get_actor_label()
|
|
348
|
+
res['component'] = 'StaticMeshActors' # Using actors instead of components
|
|
349
|
+
res['success'] = True
|
|
350
|
+
except Exception as e:
|
|
351
|
+
res['success'] = False
|
|
352
|
+
res['note'] += f"; fatal: {e}"
|
|
353
|
+
|
|
354
|
+
print('RESULT:' + json.dumps(res))
|
|
355
|
+
`.trim();
|
|
356
|
+
|
|
357
|
+
const pyResp = await this.bridge.executePython(py);
|
|
358
|
+
let out = '';
|
|
359
|
+
if (pyResp?.LogOutput && Array.isArray(pyResp.LogOutput)) out = pyResp.LogOutput.map((l: any) => l.Output || '').join('');
|
|
360
|
+
else if (typeof pyResp === 'string') out = pyResp; else out = JSON.stringify(pyResp);
|
|
361
|
+
const m = out.match(/RESULT:({.*})/);
|
|
362
|
+
if (m) {
|
|
363
|
+
try {
|
|
364
|
+
const parsed = JSON.parse(m[1]);
|
|
365
|
+
if (!parsed.success) {
|
|
366
|
+
return { success: false, error: parsed.note || 'Paint foliage failed' };
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
success: true,
|
|
370
|
+
added: parsed.added,
|
|
371
|
+
actor: parsed.actor,
|
|
372
|
+
component: parsed.component,
|
|
373
|
+
usedMesh: parsed.used_mesh,
|
|
374
|
+
note: parsed.note,
|
|
375
|
+
message: `Painted ${parsed.added} instances for '${foliageType}' around (${pos[0]}, ${pos[1]}, ${pos[2]})`
|
|
376
|
+
};
|
|
377
|
+
} catch {
|
|
378
|
+
return { success: false, error: 'Failed to parse Python result' };
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return { success: false, error: 'No parseable result from Python' };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Create instanced mesh
|
|
385
|
+
async createInstancedMesh(params: {
|
|
386
|
+
name: string;
|
|
387
|
+
meshPath: string;
|
|
388
|
+
instances: Array<{
|
|
389
|
+
position: [number, number, number];
|
|
390
|
+
rotation?: [number, number, number];
|
|
391
|
+
scale?: [number, number, number];
|
|
392
|
+
}>;
|
|
393
|
+
enableCulling?: boolean;
|
|
394
|
+
cullDistance?: number;
|
|
395
|
+
}) {
|
|
396
|
+
const commands = [];
|
|
397
|
+
|
|
398
|
+
commands.push(`CreateInstancedStaticMesh ${params.name} ${params.meshPath}`);
|
|
399
|
+
|
|
400
|
+
for (const instance of params.instances) {
|
|
401
|
+
const rot = instance.rotation || [0, 0, 0];
|
|
402
|
+
const scale = instance.scale || [1, 1, 1];
|
|
403
|
+
commands.push(`AddInstance ${params.name} ${instance.position.join(' ')} ${rot.join(' ')} ${scale.join(' ')}`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (params.enableCulling !== undefined) {
|
|
407
|
+
commands.push(`SetInstanceCulling ${params.name} ${params.enableCulling}`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (params.cullDistance !== undefined) {
|
|
411
|
+
commands.push(`SetInstanceCullDistance ${params.name} ${params.cullDistance}`);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
for (const cmd of commands) {
|
|
415
|
+
await this.bridge.executeConsoleCommand(cmd);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return { success: true, message: `Instanced mesh ${params.name} created with ${params.instances.length} instances` };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Set foliage LOD
|
|
422
|
+
async setFoliageLOD(params: {
|
|
423
|
+
foliageType: string;
|
|
424
|
+
lodDistances?: number[];
|
|
425
|
+
screenSize?: number[];
|
|
426
|
+
}) {
|
|
427
|
+
const commands = [];
|
|
428
|
+
|
|
429
|
+
if (params.lodDistances) {
|
|
430
|
+
commands.push(`SetFoliageLODDistances ${params.foliageType} ${params.lodDistances.join(' ')}`);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (params.screenSize) {
|
|
434
|
+
commands.push(`SetFoliageLODScreenSize ${params.foliageType} ${params.screenSize.join(' ')}`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
for (const cmd of commands) {
|
|
438
|
+
await this.bridge.executeConsoleCommand(cmd);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return { success: true, message: 'Foliage LOD settings updated' };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Create procedural foliage
|
|
445
|
+
async createProceduralFoliage(params: {
|
|
446
|
+
volumeName: string;
|
|
447
|
+
position: [number, number, number];
|
|
448
|
+
size: [number, number, number];
|
|
449
|
+
foliageTypes: string[];
|
|
450
|
+
seed?: number;
|
|
451
|
+
tileSize?: number;
|
|
452
|
+
}) {
|
|
453
|
+
const commands = [];
|
|
454
|
+
|
|
455
|
+
commands.push(`CreateProceduralFoliageVolume ${params.volumeName} ${params.position.join(' ')} ${params.size.join(' ')}`);
|
|
456
|
+
|
|
457
|
+
for (const type of params.foliageTypes) {
|
|
458
|
+
commands.push(`AddProceduralFoliageType ${params.volumeName} ${type}`);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (params.seed !== undefined) {
|
|
462
|
+
commands.push(`SetProceduralSeed ${params.volumeName} ${params.seed}`);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (params.tileSize !== undefined) {
|
|
466
|
+
commands.push(`SetProceduralTileSize ${params.volumeName} ${params.tileSize}`);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
commands.push(`GenerateProceduralFoliage ${params.volumeName}`);
|
|
470
|
+
|
|
471
|
+
for (const cmd of commands) {
|
|
472
|
+
await this.bridge.executeConsoleCommand(cmd);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return { success: true, message: `Procedural foliage volume ${params.volumeName} created` };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Set foliage collision
|
|
479
|
+
async setFoliageCollision(params: {
|
|
480
|
+
foliageType: string;
|
|
481
|
+
collisionEnabled?: boolean;
|
|
482
|
+
collisionProfile?: string;
|
|
483
|
+
generateOverlapEvents?: boolean;
|
|
484
|
+
}) {
|
|
485
|
+
const commands = [];
|
|
486
|
+
|
|
487
|
+
if (params.collisionEnabled !== undefined) {
|
|
488
|
+
commands.push(`SetFoliageCollision ${params.foliageType} ${params.collisionEnabled}`);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (params.collisionProfile) {
|
|
492
|
+
commands.push(`SetFoliageCollisionProfile ${params.foliageType} ${params.collisionProfile}`);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (params.generateOverlapEvents !== undefined) {
|
|
496
|
+
commands.push(`SetFoliageOverlapEvents ${params.foliageType} ${params.generateOverlapEvents}`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
for (const cmd of commands) {
|
|
500
|
+
await this.bridge.executeConsoleCommand(cmd);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return { success: true, message: 'Foliage collision settings updated' };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Create grass system
|
|
507
|
+
async createGrassSystem(params: {
|
|
508
|
+
name: string;
|
|
509
|
+
grassTypes: Array<{
|
|
510
|
+
meshPath: string;
|
|
511
|
+
density: number;
|
|
512
|
+
minScale?: number;
|
|
513
|
+
maxScale?: number;
|
|
514
|
+
}>;
|
|
515
|
+
windStrength?: number;
|
|
516
|
+
windSpeed?: number;
|
|
517
|
+
}) {
|
|
518
|
+
const commands = [];
|
|
519
|
+
|
|
520
|
+
commands.push(`CreateGrassSystem ${params.name}`);
|
|
521
|
+
|
|
522
|
+
for (const grassType of params.grassTypes) {
|
|
523
|
+
const minScale = grassType.minScale || 0.8;
|
|
524
|
+
const maxScale = grassType.maxScale || 1.2;
|
|
525
|
+
commands.push(`AddGrassType ${params.name} ${grassType.meshPath} ${grassType.density} ${minScale} ${maxScale}`);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (params.windStrength !== undefined) {
|
|
529
|
+
commands.push(`SetGrassWindStrength ${params.name} ${params.windStrength}`);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (params.windSpeed !== undefined) {
|
|
533
|
+
commands.push(`SetGrassWindSpeed ${params.name} ${params.windSpeed}`);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
for (const cmd of commands) {
|
|
537
|
+
await this.bridge.executeConsoleCommand(cmd);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return { success: true, message: `Grass system ${params.name} created` };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Remove foliage instances
|
|
544
|
+
async removeFoliageInstances(params: {
|
|
545
|
+
foliageType: string;
|
|
546
|
+
position: [number, number, number];
|
|
547
|
+
radius: number;
|
|
548
|
+
}) {
|
|
549
|
+
const command = `RemoveFoliageInRadius ${params.foliageType} ${params.position.join(' ')} ${params.radius}`;
|
|
550
|
+
return this.bridge.executeConsoleCommand(command);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Select foliage instances
|
|
554
|
+
async selectFoliageInstances(params: {
|
|
555
|
+
foliageType: string;
|
|
556
|
+
position?: [number, number, number];
|
|
557
|
+
radius?: number;
|
|
558
|
+
selectAll?: boolean;
|
|
559
|
+
}) {
|
|
560
|
+
let command: string;
|
|
561
|
+
|
|
562
|
+
if (params.selectAll) {
|
|
563
|
+
command = `SelectAllFoliage ${params.foliageType}`;
|
|
564
|
+
} else if (params.position && params.radius) {
|
|
565
|
+
command = `SelectFoliageInRadius ${params.foliageType} ${params.position.join(' ')} ${params.radius}`;
|
|
566
|
+
} else {
|
|
567
|
+
command = `SelectFoliageType ${params.foliageType}`;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return this.bridge.executeConsoleCommand(command);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Update foliage instances
|
|
574
|
+
async updateFoliageInstances(params: {
|
|
575
|
+
foliageType: string;
|
|
576
|
+
updateTransforms?: boolean;
|
|
577
|
+
updateMesh?: boolean;
|
|
578
|
+
newMeshPath?: string;
|
|
579
|
+
}) {
|
|
580
|
+
const commands = [];
|
|
581
|
+
|
|
582
|
+
if (params.updateTransforms) {
|
|
583
|
+
commands.push(`UpdateFoliageTransforms ${params.foliageType}`);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (params.updateMesh && params.newMeshPath) {
|
|
587
|
+
commands.push(`UpdateFoliageMesh ${params.foliageType} ${params.newMeshPath}`);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
commands.push(`RefreshFoliage ${params.foliageType}`);
|
|
591
|
+
|
|
592
|
+
for (const cmd of commands) {
|
|
593
|
+
await this.bridge.executeConsoleCommand(cmd);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return { success: true, message: 'Foliage instances updated' };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Create foliage spawner
|
|
600
|
+
async createFoliageSpawner(params: {
|
|
601
|
+
name: string;
|
|
602
|
+
spawnArea: 'Landscape' | 'StaticMesh' | 'BSP' | 'Foliage' | 'All';
|
|
603
|
+
excludeAreas?: Array<[number, number, number, number]>; // [x, y, z, radius]
|
|
604
|
+
}) {
|
|
605
|
+
const commands = [];
|
|
606
|
+
|
|
607
|
+
commands.push(`CreateFoliageSpawner ${params.name} ${params.spawnArea}`);
|
|
608
|
+
|
|
609
|
+
if (params.excludeAreas) {
|
|
610
|
+
for (const area of params.excludeAreas) {
|
|
611
|
+
commands.push(`AddFoliageExclusionArea ${params.name} ${area.join(' ')}`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
for (const cmd of commands) {
|
|
616
|
+
await this.bridge.executeConsoleCommand(cmd);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return { success: true, message: `Foliage spawner ${params.name} created` };
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Optimize foliage
|
|
623
|
+
async optimizeFoliage(params: {
|
|
624
|
+
mergeInstances?: boolean;
|
|
625
|
+
generateClusters?: boolean;
|
|
626
|
+
clusterSize?: number;
|
|
627
|
+
reduceDrawCalls?: boolean;
|
|
628
|
+
}) {
|
|
629
|
+
const commands = [];
|
|
630
|
+
|
|
631
|
+
if (params.mergeInstances) {
|
|
632
|
+
commands.push('MergeFoliageInstances');
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (params.generateClusters) {
|
|
636
|
+
const size = params.clusterSize || 100;
|
|
637
|
+
commands.push(`GenerateFoliageClusters ${size}`);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (params.reduceDrawCalls) {
|
|
641
|
+
commands.push('OptimizeFoliageDrawCalls');
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
commands.push('RebuildFoliageTree');
|
|
645
|
+
|
|
646
|
+
for (const cmd of commands) {
|
|
647
|
+
await this.bridge.executeConsoleCommand(cmd);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return { success: true, message: 'Foliage optimized' };
|
|
651
|
+
}
|
|
652
|
+
}
|