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,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
|
+
}
|