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,410 @@
1
+ // Level management tools for Unreal Engine
2
+ import { UnrealBridge } from '../unreal-bridge.js';
3
+
4
+ export class LevelTools {
5
+ constructor(private bridge: UnrealBridge) {}
6
+
7
+ // Execute console command
8
+ private async _executeCommand(command: string) {
9
+ return this.bridge.httpCall('/remote/object/call', 'PUT', {
10
+ objectPath: '/Script/Engine.Default__KismetSystemLibrary',
11
+ functionName: 'ExecuteConsoleCommand',
12
+ parameters: {
13
+ WorldContextObject: null,
14
+ Command: command,
15
+ SpecificPlayer: null
16
+ },
17
+ generateTransaction: false
18
+ });
19
+ }
20
+
21
+ // Load level (using LevelEditorSubsystem to avoid crashes)
22
+ async loadLevel(params: {
23
+ levelPath: string;
24
+ streaming?: boolean;
25
+ position?: [number, number, number];
26
+ }) {
27
+ if (params.streaming) {
28
+ // Try to add as streaming level
29
+ const py = `\nimport unreal\ntry:\n # Use UnrealEditorSubsystem to get editor world\n ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)\n world = ues.get_editor_world() if ues else None\n if world:\n unreal.EditorLevelUtils.add_level_to_world(world, r"${params.levelPath}", unreal.LevelStreamingKismet)\n print('RESULT:{\\'success\\': True}')\n else:\n print('RESULT:{\\'success\\': False, \\'error\\': \\'No editor world\\'}')\nexcept Exception as e:\n print('RESULT:{\\'success\\': False, \\'error\\': \\'%s\\'}' % str(e))\n`.trim();
30
+ try {
31
+ const resp = await this.bridge.executePython(py);
32
+ const out = typeof resp === 'string' ? resp : JSON.stringify(resp);
33
+ const m = out.match(/RESULT:({.*})/);
34
+ if (m) { try { const parsed = JSON.parse(m[1].replace(/'/g, '"')); if (parsed.success) return { success: true, message: 'Streaming level added' }; } catch {} }
35
+ } catch {}
36
+ // Fallback to console
37
+ return this.bridge.executeConsoleCommand(`LoadStreamLevel ${params.levelPath}`);
38
+ } else {
39
+ // Use LevelEditorSubsystem.load_level() to avoid crashes
40
+ // This properly handles the WorldContext and avoids the assertion failure
41
+ const python = `
42
+ import unreal
43
+ try:
44
+ # Use LevelEditorSubsystem which properly handles WorldContext
45
+ les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
46
+ if les:
47
+ # load_level closes the current level and opens a new one
48
+ # This is the proper way to switch levels in the editor
49
+ success = les.load_level(r"${params.levelPath}")
50
+ if success:
51
+ print('RESULT:{"success": true, "message": "Level loaded successfully"}')
52
+ else:
53
+ print('RESULT:{"success": false, "error": "Failed to load level"}')
54
+ else:
55
+ print('RESULT:{"success": false, "error": "LevelEditorSubsystem not available"}')
56
+ except Exception as e:
57
+ print('RESULT:{"success": false, "error": "' + str(e).replace('"','\\"'
58
+ `.trim();
59
+
60
+ try {
61
+ const resp = await this.bridge.executePython(python);
62
+ const out = typeof resp === 'string' ? resp : JSON.stringify(resp);
63
+ const m = out.match(/RESULT:({.*})/);
64
+ if (m) {
65
+ try {
66
+ const parsed = JSON.parse(m[1]);
67
+ return parsed.success
68
+ ? { success: true, message: parsed.message || `Level ${params.levelPath} loaded` }
69
+ : { success: false, error: parsed.error || 'Failed to load level' };
70
+ } catch {}
71
+ }
72
+ // If we get here but no error was thrown, assume success
73
+ return { success: true, message: `Level ${params.levelPath} loaded` };
74
+ } catch (e) {
75
+ return { success: false, error: `Failed to load level: ${e}` };
76
+ }
77
+ }
78
+ }
79
+
80
+ // Save current level
81
+ async saveLevel(_params: {
82
+ levelName?: string;
83
+ savePath?: string;
84
+ }) {
85
+ // Use Python EditorLevelLibrary.save_current_level for reliability
86
+ const python = `
87
+ import unreal
88
+ try:
89
+ # Attempt to reduce source control prompts (best-effort, may be a no-op depending on UE version)
90
+ try:
91
+ prefs = unreal.SourceControlPreferences()
92
+ try:
93
+ prefs.set_enable_source_control(False)
94
+ except Exception:
95
+ try:
96
+ prefs.enable_source_control = False
97
+ except Exception:
98
+ pass
99
+ except Exception:
100
+ pass
101
+
102
+ # Determine if level is dirty and save via LevelEditorSubsystem when possible
103
+ try:
104
+ world = None
105
+ try:
106
+ world = unreal.EditorSubsystemLibrary.get_editor_world()
107
+ except Exception:
108
+ try:
109
+ world = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem).get_editor_world()
110
+ except Exception:
111
+ world = None
112
+ pkg_path = None
113
+ try:
114
+ if world is not None:
115
+ full = world.get_path_name()
116
+ pkg_path = full.split('.')[0] if '.' in full else full
117
+ except Exception:
118
+ pkg_path = None
119
+ if pkg_path and not unreal.EditorAssetLibrary.is_asset_dirty(pkg_path):
120
+ print('RESULT:{"success": true, "skipped": true, "reason": "Level not dirty"}')
121
+ raise SystemExit(0)
122
+ except Exception:
123
+ pass
124
+
125
+ # Save using LevelEditorSubsystem to avoid deprecation
126
+ saved = False
127
+ try:
128
+ les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
129
+ if les:
130
+ les.save_current_level()
131
+ saved = True
132
+ except Exception:
133
+ pass
134
+ if not saved:
135
+ # No fallback available - LevelEditorSubsystem is required
136
+ raise Exception('LevelEditorSubsystem not available')
137
+ print('RESULT:{"success": true}')
138
+ except Exception as e:
139
+ print('RESULT:{"success": false, "error": "' + str(e).replace('"','\\"') + '"}')
140
+ `.trim();
141
+ try {
142
+ const resp = await this.bridge.executePython(python);
143
+ const out = typeof resp === 'string' ? resp : JSON.stringify(resp);
144
+ const m = out.match(/RESULT:({.*})/);
145
+ if (m) {
146
+ try { const parsed = JSON.parse(m[1]); return parsed.success ? { success: true, message: 'Level saved' } : { success: false, error: parsed.error }; } catch {}
147
+ }
148
+ return { success: true, message: 'Level saved' };
149
+ } catch (e) {
150
+ return { success: false, error: `Failed to save level: ${e}` };
151
+ }
152
+ }
153
+
154
+ // Create new level (Python via LevelEditorSubsystem)
155
+ async createLevel(params: {
156
+ levelName: string;
157
+ template?: 'Empty' | 'Default' | 'VR' | 'TimeOfDay';
158
+ savePath?: string;
159
+ }) {
160
+ const basePath = params.savePath || '/Game/Maps';
161
+ const isPartitioned = true; // default to World Partition for UE5
162
+ const fullPath = `${basePath}/${params.levelName}`;
163
+ const py = `
164
+ import unreal
165
+ try:
166
+ les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
167
+ if les:
168
+ les.new_level(r"${fullPath}", ${isPartitioned ? 'True' : 'False'})
169
+ print('RESULT:{"success": True, "message": "Level created"}')
170
+ else:
171
+ print('RESULT:{"success": False, "error": "LevelEditorSubsystem not available"}')
172
+ except Exception as e:
173
+ print('RESULT:{"success": False, "error": "' + str(e) + '"}')
174
+ `.trim();
175
+ try {
176
+ const resp = await this.bridge.executePython(py);
177
+ const out = typeof resp === 'string' ? resp : JSON.stringify(resp);
178
+ const m = out.match(/RESULT:({.*})/);
179
+ if (m) { try { const parsed = JSON.parse(m[1].replace(/'/g, '"')); return parsed.success ? { success: true, message: parsed.message } : { success: false, error: parsed.error }; } catch {} }
180
+ return { success: true, message: 'Level creation attempted' };
181
+ } catch (e) {
182
+ return { success: false, error: `Failed to create level: ${e}` };
183
+ }
184
+ }
185
+
186
+ // Stream level (Python attempt with fallback)
187
+ async streamLevel(params: {
188
+ levelName: string;
189
+ shouldBeLoaded: boolean;
190
+ shouldBeVisible: boolean;
191
+ position?: [number, number, number];
192
+ }) {
193
+ const py = `\nimport unreal\ntry:\n # Use UnrealEditorSubsystem to get editor world\n ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)\n world = ues.get_editor_world() if ues else None\n if world:\n # Find streaming level by name and set flags\n updated = False\n for sl in world.get_streaming_levels():\n try:\n name = sl.get_world_asset_package_name() if hasattr(sl, 'get_world_asset_package_name') else str(sl.get_editor_property('world_asset'))\n if name and name.endswith('/${params.levelName}'):\n try: sl.set_should_be_loaded(${params.shouldBeLoaded ? 'True' : 'False'})\n except Exception: pass\n try: sl.set_should_be_visible(${params.shouldBeVisible ? 'True' : 'False'})\n except Exception: pass\n updated = True\n break\n except Exception: pass\n print('RESULT:{\\'success\\': %s}' % ('True' if updated else 'False'))\n else:\n print('RESULT:{\\'success\\': False, \\'error\\': \\'No editor world\\'}')\nexcept Exception as e:\n print('RESULT:{\\'success\\': False, \\'error\\': \\'%s\\'}' % str(e))\n`.trim();
194
+ try {
195
+ const resp = await this.bridge.executePython(py);
196
+ const out = typeof resp === 'string' ? resp : JSON.stringify(resp);
197
+ const m = out.match(/RESULT:({.*})/);
198
+ if (m) { try { const parsed = JSON.parse(m[1].replace(/'/g, '"')); if (parsed.success) return { success: true, message: 'Streaming level updated' }; } catch {} }
199
+ } catch {}
200
+ // Fallback
201
+ const loadCmd = params.shouldBeLoaded ? 'Load' : 'Unload';
202
+ const visCmd = params.shouldBeVisible ? 'Show' : 'Hide';
203
+ const command = `StreamLevel ${params.levelName} ${loadCmd} ${visCmd}`;
204
+ return this.bridge.executeConsoleCommand(command);
205
+ }
206
+
207
+ // World composition
208
+ async setupWorldComposition(params: {
209
+ enableComposition: boolean;
210
+ tileSize?: number;
211
+ distanceStreaming?: boolean;
212
+ streamingDistance?: number;
213
+ }) {
214
+ const commands = [];
215
+
216
+ if (params.enableComposition) {
217
+ commands.push('EnableWorldComposition');
218
+ if (params.tileSize) {
219
+ commands.push(`SetWorldTileSize ${params.tileSize}`);
220
+ }
221
+ if (params.distanceStreaming) {
222
+ commands.push(`EnableDistanceStreaming ${params.streamingDistance || 5000}`);
223
+ }
224
+ } else {
225
+ commands.push('DisableWorldComposition');
226
+ }
227
+
228
+ for (const cmd of commands) {
229
+ await this.bridge.executeConsoleCommand(cmd);
230
+ }
231
+
232
+ return { success: true, message: 'World composition configured' };
233
+ }
234
+
235
+ // Level blueprint
236
+ async editLevelBlueprint(params: {
237
+ eventType: 'BeginPlay' | 'EndPlay' | 'Tick' | 'Custom';
238
+ customEventName?: string;
239
+ nodes?: Array<{
240
+ nodeType: string;
241
+ position: [number, number];
242
+ connections?: string[];
243
+ }>;
244
+ }) {
245
+ const command = `OpenLevelBlueprint ${params.eventType}`;
246
+ return this.bridge.executeConsoleCommand(command);
247
+ }
248
+
249
+ // Sub-levels
250
+ async createSubLevel(params: {
251
+ name: string;
252
+ type: 'Persistent' | 'Streaming' | 'Lighting' | 'Gameplay';
253
+ parent?: string;
254
+ }) {
255
+ const command = `CreateSubLevel ${params.name} ${params.type} ${params.parent || 'None'}`;
256
+ return this.bridge.executeConsoleCommand(command);
257
+ }
258
+
259
+ // World settings
260
+ async setWorldSettings(params: {
261
+ gravity?: number;
262
+ worldScale?: number;
263
+ gameMode?: string;
264
+ defaultPawn?: string;
265
+ killZ?: number;
266
+ }) {
267
+ const commands = [];
268
+
269
+ if (params.gravity !== undefined) {
270
+ commands.push(`SetWorldGravity ${params.gravity}`);
271
+ }
272
+ if (params.worldScale !== undefined) {
273
+ commands.push(`SetWorldToMeters ${params.worldScale}`);
274
+ }
275
+ if (params.gameMode) {
276
+ commands.push(`SetGameMode ${params.gameMode}`);
277
+ }
278
+ if (params.defaultPawn) {
279
+ commands.push(`SetDefaultPawn ${params.defaultPawn}`);
280
+ }
281
+ if (params.killZ !== undefined) {
282
+ commands.push(`SetKillZ ${params.killZ}`);
283
+ }
284
+
285
+ for (const cmd of commands) {
286
+ await this.bridge.executeConsoleCommand(cmd);
287
+ }
288
+
289
+ return { success: true, message: 'World settings updated' };
290
+ }
291
+
292
+ // Level bounds
293
+ async setLevelBounds(params: {
294
+ min: [number, number, number];
295
+ max: [number, number, number];
296
+ }) {
297
+ const command = `SetLevelBounds ${params.min.join(',')} ${params.max.join(',')}`;
298
+ return this.bridge.executeConsoleCommand(command);
299
+ }
300
+
301
+ // Navigation mesh
302
+ async buildNavMesh(params: {
303
+ rebuildAll?: boolean;
304
+ selectedOnly?: boolean;
305
+ }) {
306
+ // Use Python API for safer navigation mesh building to avoid crashes
307
+ const py = `
308
+ import unreal
309
+ import json
310
+ try:
311
+ # Check if navigation system exists first
312
+ nav_system = unreal.EditorSubsystemLibrary.get_editor_subsystem(unreal.NavigationSystemV1)
313
+ if not nav_system:
314
+ # Try alternative method
315
+ # Try to get world via UnrealEditorSubsystem
316
+ ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
317
+ world = ues.get_editor_world() if ues else None
318
+ nav_system = unreal.NavigationSystemV1.get_navigation_system(world) if world else None
319
+
320
+ if nav_system:
321
+ # Use the safe Python API method instead of console commands
322
+ if ${params.rebuildAll ? 'True' : 'False'}:
323
+ # Rebuild all navigation
324
+ nav_system.navigation_build_async()
325
+ print('RESULT:' + json.dumps({'success': True, 'message': 'Navigation rebuild started'}))
326
+ else:
327
+ # Update navigation for selected actors only
328
+ # Use EditorActorSubsystem to get selected actors
329
+ actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
330
+ selected_actors = actor_subsystem.get_selected_level_actors() if actor_subsystem else []
331
+ if selected_actors:
332
+ for actor in selected_actors:
333
+ nav_system.update_nav_octree(actor)
334
+ print('RESULT:' + json.dumps({'success': True, 'message': f'Navigation updated for {len(selected_actors)} actors'}))
335
+ else:
336
+ # If nothing selected, do a safe incremental update
337
+ nav_system.update(0.0)
338
+ print('RESULT:' + json.dumps({'success': True, 'message': 'Navigation incremental update performed'}))
339
+ else:
340
+ # Navigation system not available - likely no nav mesh in level
341
+ print('RESULT:' + json.dumps({'success': False, 'error': 'Navigation system not available. Add a NavMeshBoundsVolume to the level first.'}))
342
+ except AttributeError as e:
343
+ # Some methods might not be available in all UE versions
344
+ print('RESULT:' + json.dumps({'success': False, 'error': f'Navigation API not available: {str(e)}'}))
345
+ except Exception as e:
346
+ print('RESULT:' + json.dumps({'success': False, 'error': f'Navigation build failed: {str(e)}'}))
347
+ `.trim();
348
+
349
+ try {
350
+ const resp = await this.bridge.executePython(py);
351
+ const out = typeof resp === 'string' ? resp : JSON.stringify(resp);
352
+ const m = out.match(/RESULT:({.*})/);
353
+ if (m) {
354
+ try {
355
+ const parsed = JSON.parse(m[1]);
356
+ return parsed.success
357
+ ? { success: true, message: parsed.message }
358
+ : { success: false, error: parsed.error };
359
+ } catch {}
360
+ }
361
+
362
+ // Fallback message if no clear result
363
+ return { success: true, message: 'Navigation mesh build attempted' };
364
+ } catch (e) {
365
+ // If Python fails, return error instead of trying console command that crashes
366
+ return {
367
+ success: false,
368
+ error: `Navigation build not available: ${e}. Please ensure a NavMeshBoundsVolume exists in the level.`
369
+ };
370
+ }
371
+ }
372
+
373
+ // Level visibility
374
+ async setLevelVisibility(params: {
375
+ levelName: string;
376
+ visible: boolean;
377
+ }) {
378
+ const command = `SetLevelVisibility ${params.levelName} ${params.visible}`;
379
+ return this.bridge.executeConsoleCommand(command);
380
+ }
381
+
382
+ // World origin
383
+ async setWorldOrigin(params: {
384
+ location: [number, number, number];
385
+ }) {
386
+ const command = `SetWorldOriginLocation ${params.location.join(' ')}`;
387
+ return this.bridge.executeConsoleCommand(command);
388
+ }
389
+
390
+ // Level streaming volumes
391
+ async createStreamingVolume(params: {
392
+ levelName: string;
393
+ position: [number, number, number];
394
+ size: [number, number, number];
395
+ streamingDistance?: number;
396
+ }) {
397
+ const command = `CreateStreamingVolume ${params.levelName} ${params.position.join(' ')} ${params.size.join(' ')} ${params.streamingDistance || 0}`;
398
+ return this.bridge.executeConsoleCommand(command);
399
+ }
400
+
401
+ // Level LOD
402
+ async setLevelLOD(params: {
403
+ levelName: string;
404
+ lodLevel: number;
405
+ distance: number;
406
+ }) {
407
+ const command = `SetLevelLOD ${params.levelName} ${params.lodLevel} ${params.distance}`;
408
+ return this.bridge.executeConsoleCommand(command);
409
+ }
410
+ }