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.
Files changed (155) hide show
  1. package/.dockerignore +57 -0
  2. package/.env.production +25 -0
  3. package/.eslintrc.json +54 -0
  4. package/.github/workflows/publish-mcp.yml +75 -0
  5. package/Dockerfile +54 -0
  6. package/LICENSE +21 -0
  7. package/Public/icon.png +0 -0
  8. package/README.md +209 -0
  9. package/claude_desktop_config_example.json +13 -0
  10. package/dist/cli.d.ts +3 -0
  11. package/dist/cli.js +7 -0
  12. package/dist/index.d.ts +31 -0
  13. package/dist/index.js +484 -0
  14. package/dist/prompts/index.d.ts +14 -0
  15. package/dist/prompts/index.js +38 -0
  16. package/dist/python-utils.d.ts +29 -0
  17. package/dist/python-utils.js +54 -0
  18. package/dist/resources/actors.d.ts +13 -0
  19. package/dist/resources/actors.js +83 -0
  20. package/dist/resources/assets.d.ts +23 -0
  21. package/dist/resources/assets.js +245 -0
  22. package/dist/resources/levels.d.ts +17 -0
  23. package/dist/resources/levels.js +94 -0
  24. package/dist/tools/actors.d.ts +51 -0
  25. package/dist/tools/actors.js +459 -0
  26. package/dist/tools/animation.d.ts +196 -0
  27. package/dist/tools/animation.js +579 -0
  28. package/dist/tools/assets.d.ts +21 -0
  29. package/dist/tools/assets.js +304 -0
  30. package/dist/tools/audio.d.ts +170 -0
  31. package/dist/tools/audio.js +416 -0
  32. package/dist/tools/blueprint.d.ts +144 -0
  33. package/dist/tools/blueprint.js +652 -0
  34. package/dist/tools/build_environment_advanced.d.ts +66 -0
  35. package/dist/tools/build_environment_advanced.js +484 -0
  36. package/dist/tools/consolidated-tool-definitions.d.ts +2598 -0
  37. package/dist/tools/consolidated-tool-definitions.js +607 -0
  38. package/dist/tools/consolidated-tool-handlers.d.ts +2 -0
  39. package/dist/tools/consolidated-tool-handlers.js +1050 -0
  40. package/dist/tools/debug.d.ts +185 -0
  41. package/dist/tools/debug.js +265 -0
  42. package/dist/tools/editor.d.ts +88 -0
  43. package/dist/tools/editor.js +365 -0
  44. package/dist/tools/engine.d.ts +30 -0
  45. package/dist/tools/engine.js +36 -0
  46. package/dist/tools/foliage.d.ts +155 -0
  47. package/dist/tools/foliage.js +525 -0
  48. package/dist/tools/introspection.d.ts +98 -0
  49. package/dist/tools/introspection.js +683 -0
  50. package/dist/tools/landscape.d.ts +158 -0
  51. package/dist/tools/landscape.js +375 -0
  52. package/dist/tools/level.d.ts +110 -0
  53. package/dist/tools/level.js +362 -0
  54. package/dist/tools/lighting.d.ts +159 -0
  55. package/dist/tools/lighting.js +1179 -0
  56. package/dist/tools/materials.d.ts +34 -0
  57. package/dist/tools/materials.js +146 -0
  58. package/dist/tools/niagara.d.ts +145 -0
  59. package/dist/tools/niagara.js +289 -0
  60. package/dist/tools/performance.d.ts +163 -0
  61. package/dist/tools/performance.js +412 -0
  62. package/dist/tools/physics.d.ts +189 -0
  63. package/dist/tools/physics.js +784 -0
  64. package/dist/tools/rc.d.ts +110 -0
  65. package/dist/tools/rc.js +363 -0
  66. package/dist/tools/sequence.d.ts +112 -0
  67. package/dist/tools/sequence.js +675 -0
  68. package/dist/tools/tool-definitions.d.ts +4919 -0
  69. package/dist/tools/tool-definitions.js +891 -0
  70. package/dist/tools/tool-handlers.d.ts +47 -0
  71. package/dist/tools/tool-handlers.js +830 -0
  72. package/dist/tools/ui.d.ts +171 -0
  73. package/dist/tools/ui.js +337 -0
  74. package/dist/tools/visual.d.ts +29 -0
  75. package/dist/tools/visual.js +67 -0
  76. package/dist/types/env.d.ts +10 -0
  77. package/dist/types/env.js +18 -0
  78. package/dist/types/index.d.ts +323 -0
  79. package/dist/types/index.js +28 -0
  80. package/dist/types/tool-types.d.ts +274 -0
  81. package/dist/types/tool-types.js +13 -0
  82. package/dist/unreal-bridge.d.ts +126 -0
  83. package/dist/unreal-bridge.js +992 -0
  84. package/dist/utils/cache-manager.d.ts +64 -0
  85. package/dist/utils/cache-manager.js +176 -0
  86. package/dist/utils/error-handler.d.ts +66 -0
  87. package/dist/utils/error-handler.js +243 -0
  88. package/dist/utils/errors.d.ts +133 -0
  89. package/dist/utils/errors.js +256 -0
  90. package/dist/utils/http.d.ts +26 -0
  91. package/dist/utils/http.js +135 -0
  92. package/dist/utils/logger.d.ts +12 -0
  93. package/dist/utils/logger.js +32 -0
  94. package/dist/utils/normalize.d.ts +17 -0
  95. package/dist/utils/normalize.js +49 -0
  96. package/dist/utils/response-validator.d.ts +34 -0
  97. package/dist/utils/response-validator.js +121 -0
  98. package/dist/utils/safe-json.d.ts +4 -0
  99. package/dist/utils/safe-json.js +97 -0
  100. package/dist/utils/stdio-redirect.d.ts +2 -0
  101. package/dist/utils/stdio-redirect.js +20 -0
  102. package/dist/utils/validation.d.ts +50 -0
  103. package/dist/utils/validation.js +173 -0
  104. package/mcp-config-example.json +14 -0
  105. package/package.json +63 -0
  106. package/server.json +60 -0
  107. package/src/cli.ts +7 -0
  108. package/src/index.ts +543 -0
  109. package/src/prompts/index.ts +51 -0
  110. package/src/python/editor_compat.py +181 -0
  111. package/src/python-utils.ts +57 -0
  112. package/src/resources/actors.ts +92 -0
  113. package/src/resources/assets.ts +251 -0
  114. package/src/resources/levels.ts +83 -0
  115. package/src/tools/actors.ts +480 -0
  116. package/src/tools/animation.ts +713 -0
  117. package/src/tools/assets.ts +305 -0
  118. package/src/tools/audio.ts +548 -0
  119. package/src/tools/blueprint.ts +736 -0
  120. package/src/tools/build_environment_advanced.ts +526 -0
  121. package/src/tools/consolidated-tool-definitions.ts +619 -0
  122. package/src/tools/consolidated-tool-handlers.ts +1093 -0
  123. package/src/tools/debug.ts +368 -0
  124. package/src/tools/editor.ts +360 -0
  125. package/src/tools/engine.ts +32 -0
  126. package/src/tools/foliage.ts +652 -0
  127. package/src/tools/introspection.ts +778 -0
  128. package/src/tools/landscape.ts +523 -0
  129. package/src/tools/level.ts +410 -0
  130. package/src/tools/lighting.ts +1316 -0
  131. package/src/tools/materials.ts +148 -0
  132. package/src/tools/niagara.ts +312 -0
  133. package/src/tools/performance.ts +549 -0
  134. package/src/tools/physics.ts +924 -0
  135. package/src/tools/rc.ts +437 -0
  136. package/src/tools/sequence.ts +791 -0
  137. package/src/tools/tool-definitions.ts +907 -0
  138. package/src/tools/tool-handlers.ts +941 -0
  139. package/src/tools/ui.ts +499 -0
  140. package/src/tools/visual.ts +60 -0
  141. package/src/types/env.ts +27 -0
  142. package/src/types/index.ts +414 -0
  143. package/src/types/tool-types.ts +343 -0
  144. package/src/unreal-bridge.ts +1118 -0
  145. package/src/utils/cache-manager.ts +213 -0
  146. package/src/utils/error-handler.ts +320 -0
  147. package/src/utils/errors.ts +312 -0
  148. package/src/utils/http.ts +184 -0
  149. package/src/utils/logger.ts +30 -0
  150. package/src/utils/normalize.ts +54 -0
  151. package/src/utils/response-validator.ts +145 -0
  152. package/src/utils/safe-json.ts +112 -0
  153. package/src/utils/stdio-redirect.ts +18 -0
  154. package/src/utils/validation.ts +212 -0
  155. 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
+ }