unreal-engine-mcp-server 0.4.0 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.production +1 -1
- package/.github/copilot-instructions.md +45 -0
- package/.github/workflows/publish-mcp.yml +3 -2
- package/README.md +21 -5
- package/dist/index.js +124 -31
- package/dist/prompts/index.d.ts +10 -3
- package/dist/prompts/index.js +186 -7
- package/dist/resources/actors.d.ts +19 -1
- package/dist/resources/actors.js +55 -64
- package/dist/resources/assets.js +46 -62
- package/dist/resources/levels.d.ts +21 -3
- package/dist/resources/levels.js +29 -54
- package/dist/tools/actors.d.ts +3 -14
- package/dist/tools/actors.js +246 -302
- package/dist/tools/animation.d.ts +57 -102
- package/dist/tools/animation.js +429 -450
- package/dist/tools/assets.d.ts +13 -2
- package/dist/tools/assets.js +52 -44
- package/dist/tools/audio.d.ts +22 -13
- package/dist/tools/audio.js +467 -121
- package/dist/tools/blueprint.d.ts +32 -13
- package/dist/tools/blueprint.js +699 -448
- package/dist/tools/build_environment_advanced.d.ts +0 -1
- package/dist/tools/build_environment_advanced.js +190 -45
- package/dist/tools/consolidated-tool-definitions.js +78 -252
- package/dist/tools/consolidated-tool-handlers.js +506 -133
- package/dist/tools/debug.d.ts +72 -10
- package/dist/tools/debug.js +167 -31
- package/dist/tools/editor.d.ts +9 -2
- package/dist/tools/editor.js +30 -44
- package/dist/tools/foliage.d.ts +34 -15
- package/dist/tools/foliage.js +97 -107
- package/dist/tools/introspection.js +19 -21
- package/dist/tools/landscape.d.ts +1 -2
- package/dist/tools/landscape.js +311 -168
- package/dist/tools/level.d.ts +3 -28
- package/dist/tools/level.js +642 -192
- package/dist/tools/lighting.d.ts +14 -3
- package/dist/tools/lighting.js +236 -123
- package/dist/tools/materials.d.ts +25 -7
- package/dist/tools/materials.js +102 -79
- package/dist/tools/niagara.d.ts +10 -12
- package/dist/tools/niagara.js +74 -94
- package/dist/tools/performance.d.ts +12 -4
- package/dist/tools/performance.js +38 -79
- package/dist/tools/physics.d.ts +34 -10
- package/dist/tools/physics.js +364 -292
- package/dist/tools/rc.js +97 -23
- package/dist/tools/sequence.d.ts +1 -0
- package/dist/tools/sequence.js +125 -22
- package/dist/tools/ui.d.ts +31 -4
- package/dist/tools/ui.js +83 -66
- package/dist/tools/visual.d.ts +11 -0
- package/dist/tools/visual.js +245 -30
- package/dist/types/tool-types.d.ts +0 -6
- package/dist/types/tool-types.js +1 -8
- package/dist/unreal-bridge.d.ts +32 -2
- package/dist/unreal-bridge.js +621 -127
- package/dist/utils/elicitation.d.ts +57 -0
- package/dist/utils/elicitation.js +104 -0
- package/dist/utils/error-handler.d.ts +0 -33
- package/dist/utils/error-handler.js +4 -111
- package/dist/utils/http.d.ts +2 -22
- package/dist/utils/http.js +12 -75
- package/dist/utils/normalize.d.ts +4 -4
- package/dist/utils/normalize.js +15 -7
- package/dist/utils/python-output.d.ts +18 -0
- package/dist/utils/python-output.js +290 -0
- package/dist/utils/python.d.ts +2 -0
- package/dist/utils/python.js +4 -0
- package/dist/utils/response-validator.js +28 -2
- package/dist/utils/result-helpers.d.ts +27 -0
- package/dist/utils/result-helpers.js +147 -0
- package/dist/utils/safe-json.d.ts +0 -2
- package/dist/utils/safe-json.js +0 -43
- package/dist/utils/validation.d.ts +16 -0
- package/dist/utils/validation.js +70 -7
- package/mcp-config-example.json +2 -2
- package/package.json +10 -9
- package/server.json +37 -14
- package/src/index.ts +130 -33
- package/src/prompts/index.ts +211 -13
- package/src/resources/actors.ts +59 -44
- package/src/resources/assets.ts +48 -51
- package/src/resources/levels.ts +35 -45
- package/src/tools/actors.ts +269 -313
- package/src/tools/animation.ts +556 -539
- package/src/tools/assets.ts +53 -43
- package/src/tools/audio.ts +507 -113
- package/src/tools/blueprint.ts +778 -462
- package/src/tools/build_environment_advanced.ts +266 -64
- package/src/tools/consolidated-tool-definitions.ts +90 -264
- package/src/tools/consolidated-tool-handlers.ts +630 -121
- package/src/tools/debug.ts +176 -33
- package/src/tools/editor.ts +35 -37
- package/src/tools/foliage.ts +110 -104
- package/src/tools/introspection.ts +24 -22
- package/src/tools/landscape.ts +334 -181
- package/src/tools/level.ts +683 -182
- package/src/tools/lighting.ts +244 -123
- package/src/tools/materials.ts +114 -83
- package/src/tools/niagara.ts +87 -81
- package/src/tools/performance.ts +49 -88
- package/src/tools/physics.ts +393 -299
- package/src/tools/rc.ts +102 -24
- package/src/tools/sequence.ts +136 -28
- package/src/tools/ui.ts +101 -70
- package/src/tools/visual.ts +250 -29
- package/src/types/tool-types.ts +0 -9
- package/src/unreal-bridge.ts +658 -140
- package/src/utils/elicitation.ts +129 -0
- package/src/utils/error-handler.ts +4 -159
- package/src/utils/http.ts +16 -115
- package/src/utils/normalize.ts +20 -10
- package/src/utils/python-output.ts +351 -0
- package/src/utils/python.ts +3 -0
- package/src/utils/response-validator.ts +25 -2
- package/src/utils/result-helpers.ts +193 -0
- package/src/utils/safe-json.ts +0 -50
- package/src/utils/validation.ts +94 -7
- package/tests/run-unreal-tool-tests.mjs +720 -0
- package/tsconfig.json +2 -2
- package/dist/python-utils.d.ts +0 -29
- package/dist/python-utils.js +0 -54
- package/dist/types/index.d.ts +0 -323
- package/dist/types/index.js +0 -28
- package/dist/utils/cache-manager.d.ts +0 -64
- package/dist/utils/cache-manager.js +0 -176
- package/dist/utils/errors.d.ts +0 -133
- package/dist/utils/errors.js +0 -256
- package/src/python/editor_compat.py +0 -181
- package/src/python-utils.ts +0 -57
- package/src/types/index.ts +0 -414
- package/src/utils/cache-manager.ts +0 -213
- package/src/utils/errors.ts +0 -312
package/src/tools/level.ts
CHANGED
|
@@ -1,23 +1,15 @@
|
|
|
1
1
|
// Level management tools for Unreal Engine
|
|
2
2
|
import { UnrealBridge } from '../unreal-bridge.js';
|
|
3
|
+
import {
|
|
4
|
+
coerceBoolean,
|
|
5
|
+
coerceNumber,
|
|
6
|
+
coerceString,
|
|
7
|
+
interpretStandardResult
|
|
8
|
+
} from '../utils/result-helpers.js';
|
|
3
9
|
|
|
4
10
|
export class LevelTools {
|
|
5
11
|
constructor(private bridge: UnrealBridge) {}
|
|
6
12
|
|
|
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
13
|
// Load level (using LevelEditorSubsystem to avoid crashes)
|
|
22
14
|
async loadLevel(params: {
|
|
23
15
|
levelPath: string;
|
|
@@ -25,52 +17,177 @@ export class LevelTools {
|
|
|
25
17
|
position?: [number, number, number];
|
|
26
18
|
}) {
|
|
27
19
|
if (params.streaming) {
|
|
28
|
-
|
|
29
|
-
|
|
20
|
+
const python = `
|
|
21
|
+
import unreal
|
|
22
|
+
import json
|
|
23
|
+
|
|
24
|
+
result = {
|
|
25
|
+
"success": False,
|
|
26
|
+
"message": "",
|
|
27
|
+
"error": "",
|
|
28
|
+
"details": [],
|
|
29
|
+
"warnings": []
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
|
|
34
|
+
world = ues.get_editor_world() if ues else None
|
|
35
|
+
if world:
|
|
36
|
+
try:
|
|
37
|
+
unreal.EditorLevelUtils.add_level_to_world(world, r"${params.levelPath}", unreal.LevelStreamingKismet)
|
|
38
|
+
result["success"] = True
|
|
39
|
+
result["message"] = "Streaming level added"
|
|
40
|
+
result["details"].append("Streaming level added via EditorLevelUtils")
|
|
41
|
+
except Exception as add_error:
|
|
42
|
+
result["error"] = f"Failed to add streaming level: {add_error}"
|
|
43
|
+
else:
|
|
44
|
+
result["error"] = "No editor world available"
|
|
45
|
+
except Exception as outer_error:
|
|
46
|
+
result["error"] = f"Streaming level operation failed: {outer_error}"
|
|
47
|
+
|
|
48
|
+
if result["success"]:
|
|
49
|
+
if not result["message"]:
|
|
50
|
+
result["message"] = "Streaming level added"
|
|
51
|
+
else:
|
|
52
|
+
if not result["error"]:
|
|
53
|
+
result["error"] = result["message"] or "Failed to add streaming level"
|
|
54
|
+
if not result["message"]:
|
|
55
|
+
result["message"] = result["error"]
|
|
56
|
+
|
|
57
|
+
if not result["warnings"]:
|
|
58
|
+
result.pop("warnings")
|
|
59
|
+
if not result["details"]:
|
|
60
|
+
result.pop("details")
|
|
61
|
+
if result.get("error") is None:
|
|
62
|
+
result.pop("error")
|
|
63
|
+
|
|
64
|
+
print("RESULT:" + json.dumps(result))
|
|
65
|
+
`.trim();
|
|
66
|
+
|
|
30
67
|
try {
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
68
|
+
const response = await this.bridge.executePython(python);
|
|
69
|
+
const interpreted = interpretStandardResult(response, {
|
|
70
|
+
successMessage: 'Streaming level added',
|
|
71
|
+
failureMessage: 'Failed to add streaming level'
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (interpreted.success) {
|
|
75
|
+
const result: Record<string, unknown> = {
|
|
76
|
+
success: true,
|
|
77
|
+
message: interpreted.message
|
|
78
|
+
};
|
|
79
|
+
if (interpreted.warnings?.length) {
|
|
80
|
+
result.warnings = interpreted.warnings;
|
|
81
|
+
}
|
|
82
|
+
if (interpreted.details?.length) {
|
|
83
|
+
result.details = interpreted.details;
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
35
87
|
} catch {}
|
|
36
|
-
|
|
88
|
+
|
|
37
89
|
return this.bridge.executeConsoleCommand(`LoadStreamLevel ${params.levelPath}`);
|
|
38
90
|
} else {
|
|
39
|
-
// Use LevelEditorSubsystem.load_level() to avoid crashes
|
|
40
|
-
// This properly handles the WorldContext and avoids the assertion failure
|
|
41
91
|
const python = `
|
|
42
92
|
import unreal
|
|
93
|
+
import json
|
|
94
|
+
|
|
95
|
+
result = {
|
|
96
|
+
"success": False,
|
|
97
|
+
"message": "",
|
|
98
|
+
"error": "",
|
|
99
|
+
"warnings": [],
|
|
100
|
+
"details": [],
|
|
101
|
+
"level": r"${params.levelPath}"
|
|
102
|
+
}
|
|
103
|
+
|
|
43
104
|
try:
|
|
44
|
-
|
|
105
|
+
level_path = r"${params.levelPath}"
|
|
106
|
+
asset_path = level_path
|
|
107
|
+
try:
|
|
108
|
+
tail = asset_path.rsplit('/', 1)[-1]
|
|
109
|
+
if '.' not in tail:
|
|
110
|
+
asset_path = f"{asset_path}.{tail}"
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
asset_exists = False
|
|
115
|
+
try:
|
|
116
|
+
asset_exists = unreal.EditorAssetLibrary.does_asset_exist(asset_path)
|
|
117
|
+
except Exception:
|
|
118
|
+
asset_exists = False
|
|
119
|
+
|
|
120
|
+
if not asset_exists:
|
|
121
|
+
result["error"] = f"Level not found: {asset_path}"
|
|
122
|
+
else:
|
|
45
123
|
les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
|
|
46
124
|
if les:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
success =
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
125
|
+
success = les.load_level(level_path)
|
|
126
|
+
if success:
|
|
127
|
+
result["success"] = True
|
|
128
|
+
result["message"] = "Level loaded successfully"
|
|
129
|
+
result["details"].append("Level loaded via LevelEditorSubsystem")
|
|
130
|
+
else:
|
|
131
|
+
result["error"] = "Failed to load level"
|
|
54
132
|
else:
|
|
55
|
-
|
|
56
|
-
except Exception as
|
|
57
|
-
|
|
133
|
+
result["error"] = "LevelEditorSubsystem not available"
|
|
134
|
+
except Exception as err:
|
|
135
|
+
result["error"] = f"Failed to load level: {err}"
|
|
136
|
+
|
|
137
|
+
if result["success"]:
|
|
138
|
+
if not result["message"]:
|
|
139
|
+
result["message"] = "Level loaded successfully"
|
|
140
|
+
else:
|
|
141
|
+
if not result["error"]:
|
|
142
|
+
result["error"] = "Failed to load level"
|
|
143
|
+
if not result["message"]:
|
|
144
|
+
result["message"] = result["error"]
|
|
145
|
+
|
|
146
|
+
if not result["warnings"]:
|
|
147
|
+
result.pop("warnings")
|
|
148
|
+
if not result["details"]:
|
|
149
|
+
result.pop("details")
|
|
150
|
+
if result.get("error") is None:
|
|
151
|
+
result.pop("error")
|
|
152
|
+
|
|
153
|
+
print("RESULT:" + json.dumps(result))
|
|
58
154
|
`.trim();
|
|
59
|
-
|
|
155
|
+
|
|
60
156
|
try {
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
157
|
+
const response = await this.bridge.executePython(python);
|
|
158
|
+
const interpreted = interpretStandardResult(response, {
|
|
159
|
+
successMessage: `Level ${params.levelPath} loaded`,
|
|
160
|
+
failureMessage: `Failed to load level ${params.levelPath}`
|
|
161
|
+
});
|
|
162
|
+
const payloadLevel = coerceString(interpreted.payload.level) ?? params.levelPath;
|
|
163
|
+
|
|
164
|
+
if (interpreted.success) {
|
|
165
|
+
const result: Record<string, unknown> = {
|
|
166
|
+
success: true,
|
|
167
|
+
message: interpreted.message,
|
|
168
|
+
level: payloadLevel
|
|
169
|
+
};
|
|
170
|
+
if (interpreted.warnings?.length) {
|
|
171
|
+
result.warnings = interpreted.warnings;
|
|
172
|
+
}
|
|
173
|
+
if (interpreted.details?.length) {
|
|
174
|
+
result.details = interpreted.details;
|
|
175
|
+
}
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const failure: Record<string, unknown> = {
|
|
180
|
+
success: false,
|
|
181
|
+
error: interpreted.error || interpreted.message,
|
|
182
|
+
level: payloadLevel
|
|
183
|
+
};
|
|
184
|
+
if (interpreted.warnings?.length) {
|
|
185
|
+
failure.warnings = interpreted.warnings;
|
|
71
186
|
}
|
|
72
|
-
|
|
73
|
-
|
|
187
|
+
if (interpreted.details?.length) {
|
|
188
|
+
failure.details = interpreted.details;
|
|
189
|
+
}
|
|
190
|
+
return failure;
|
|
74
191
|
} catch (e) {
|
|
75
192
|
return { success: false, error: `Failed to load level: ${e}` };
|
|
76
193
|
}
|
|
@@ -82,70 +199,193 @@ except Exception as e:
|
|
|
82
199
|
levelName?: string;
|
|
83
200
|
savePath?: string;
|
|
84
201
|
}) {
|
|
85
|
-
// Use Python EditorLevelLibrary.save_current_level for reliability
|
|
86
202
|
const python = `
|
|
87
203
|
import unreal
|
|
204
|
+
import json
|
|
205
|
+
|
|
206
|
+
result = {
|
|
207
|
+
"success": False,
|
|
208
|
+
"message": "",
|
|
209
|
+
"error": "",
|
|
210
|
+
"warnings": [],
|
|
211
|
+
"details": [],
|
|
212
|
+
"skipped": False,
|
|
213
|
+
"reason": ""
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
def print_result(payload):
|
|
217
|
+
data = dict(payload)
|
|
218
|
+
if data.get("skipped") and not data.get("message"):
|
|
219
|
+
data["message"] = data.get("reason") or "Level save skipped"
|
|
220
|
+
if data.get("success") and not data.get("message"):
|
|
221
|
+
data["message"] = "Level saved"
|
|
222
|
+
if not data.get("success"):
|
|
223
|
+
if not data.get("error"):
|
|
224
|
+
data["error"] = data.get("message") or "Failed to save level"
|
|
225
|
+
if not data.get("message"):
|
|
226
|
+
data["message"] = data.get("error") or "Failed to save level"
|
|
227
|
+
if data.get("success"):
|
|
228
|
+
data.pop("error", None)
|
|
229
|
+
if not data.get("warnings"):
|
|
230
|
+
data.pop("warnings", None)
|
|
231
|
+
if not data.get("details"):
|
|
232
|
+
data.pop("details", None)
|
|
233
|
+
if not data.get("skipped"):
|
|
234
|
+
data.pop("skipped", None)
|
|
235
|
+
data.pop("reason", None)
|
|
236
|
+
else:
|
|
237
|
+
if not data.get("reason"):
|
|
238
|
+
data.pop("reason", None)
|
|
239
|
+
print("RESULT:" + json.dumps(data))
|
|
240
|
+
|
|
88
241
|
try:
|
|
89
|
-
|
|
242
|
+
# Attempt to reduce source control prompts (best-effort, may be a no-op depending on UE version)
|
|
243
|
+
try:
|
|
244
|
+
prefs = unreal.SourceControlPreferences()
|
|
245
|
+
muted = False
|
|
90
246
|
try:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
prefs.set_enable_source_control(False)
|
|
94
|
-
except Exception:
|
|
95
|
-
try:
|
|
96
|
-
prefs.enable_source_control = False
|
|
97
|
-
except Exception:
|
|
98
|
-
pass
|
|
247
|
+
prefs.set_enable_source_control(False)
|
|
248
|
+
muted = True
|
|
99
249
|
except Exception:
|
|
100
|
-
|
|
250
|
+
try:
|
|
251
|
+
prefs.enable_source_control = False
|
|
252
|
+
muted = True
|
|
253
|
+
except Exception:
|
|
254
|
+
muted = False
|
|
255
|
+
if muted:
|
|
256
|
+
result["details"].append("Source control prompts disabled")
|
|
257
|
+
except Exception:
|
|
258
|
+
pass
|
|
101
259
|
|
|
102
|
-
|
|
260
|
+
# Determine if level is dirty and save via LevelEditorSubsystem when possible
|
|
261
|
+
world = None
|
|
262
|
+
try:
|
|
263
|
+
world = unreal.EditorSubsystemLibrary.get_editor_world()
|
|
264
|
+
except Exception:
|
|
103
265
|
try:
|
|
104
|
-
|
|
266
|
+
ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
|
|
267
|
+
world = ues.get_editor_world() if ues else None
|
|
268
|
+
except Exception:
|
|
269
|
+
world = None
|
|
270
|
+
|
|
271
|
+
pkg_path = None
|
|
272
|
+
try:
|
|
273
|
+
if world is not None:
|
|
274
|
+
full = world.get_path_name()
|
|
275
|
+
pkg_path = full.split('.')[0] if '.' in full else full
|
|
276
|
+
if pkg_path:
|
|
277
|
+
result["details"].append(f"Detected level package: {pkg_path}")
|
|
278
|
+
except Exception:
|
|
279
|
+
pkg_path = None
|
|
280
|
+
|
|
281
|
+
skip_save = False
|
|
282
|
+
try:
|
|
283
|
+
is_dirty = None
|
|
284
|
+
if pkg_path:
|
|
285
|
+
editor_asset_lib = getattr(unreal, 'EditorAssetLibrary', None)
|
|
286
|
+
if editor_asset_lib and hasattr(editor_asset_lib, 'is_asset_dirty'):
|
|
105
287
|
try:
|
|
106
|
-
|
|
107
|
-
except Exception:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
pkg_path = None
|
|
288
|
+
is_dirty = editor_asset_lib.is_asset_dirty(pkg_path)
|
|
289
|
+
except Exception as check_error:
|
|
290
|
+
result["warnings"].append(f"EditorAssetLibrary.is_asset_dirty failed: {check_error}")
|
|
291
|
+
is_dirty = None
|
|
292
|
+
if is_dirty is None:
|
|
293
|
+
# Fallback: attempt to inspect the current level package
|
|
113
294
|
try:
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
295
|
+
ell = getattr(unreal, 'EditorLevelLibrary', None)
|
|
296
|
+
level = ell.get_current_level() if ell and hasattr(ell, 'get_current_level') else None
|
|
297
|
+
package = level.get_outermost() if level and hasattr(level, 'get_outermost') else None
|
|
298
|
+
if package and hasattr(package, 'is_dirty'):
|
|
299
|
+
is_dirty = package.is_dirty()
|
|
300
|
+
except Exception as fallback_error:
|
|
301
|
+
result["warnings"].append(f"Fallback dirty check failed: {fallback_error}")
|
|
302
|
+
if is_dirty is False:
|
|
303
|
+
result["success"] = True
|
|
304
|
+
result["skipped"] = True
|
|
305
|
+
result["reason"] = "Level not dirty"
|
|
306
|
+
result["message"] = "Level save skipped"
|
|
307
|
+
skip_save = True
|
|
308
|
+
elif is_dirty is None and pkg_path:
|
|
309
|
+
result["warnings"].append("Unable to determine level dirty state; attempting save anyway")
|
|
310
|
+
except Exception as dirty_error:
|
|
311
|
+
result["warnings"].append(f"Failed to check level dirty state: {dirty_error}")
|
|
124
312
|
|
|
125
|
-
|
|
313
|
+
if not skip_save:
|
|
126
314
|
saved = False
|
|
127
315
|
try:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
316
|
+
les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
|
|
317
|
+
if les:
|
|
318
|
+
les.save_current_level()
|
|
319
|
+
saved = True
|
|
320
|
+
result["details"].append("Level saved via LevelEditorSubsystem")
|
|
321
|
+
except Exception as save_error:
|
|
322
|
+
result["error"] = f"Level save failed: {save_error}"
|
|
323
|
+
saved = False
|
|
324
|
+
|
|
134
325
|
if not saved:
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
326
|
+
raise Exception('LevelEditorSubsystem not available')
|
|
327
|
+
|
|
328
|
+
result["success"] = True
|
|
329
|
+
if not result["message"]:
|
|
330
|
+
result["message"] = "Level saved"
|
|
331
|
+
except Exception as err:
|
|
332
|
+
result["error"] = str(err)
|
|
333
|
+
|
|
334
|
+
print_result(result)
|
|
140
335
|
`.trim();
|
|
336
|
+
|
|
141
337
|
try {
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
338
|
+
const response = await this.bridge.executePython(python);
|
|
339
|
+
const interpreted = interpretStandardResult(response, {
|
|
340
|
+
successMessage: 'Level saved',
|
|
341
|
+
failureMessage: 'Failed to save level'
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
if (interpreted.success) {
|
|
345
|
+
const result: Record<string, unknown> = {
|
|
346
|
+
success: true,
|
|
347
|
+
message: interpreted.message
|
|
348
|
+
};
|
|
349
|
+
const skipped = coerceBoolean(interpreted.payload.skipped);
|
|
350
|
+
if (typeof skipped === 'boolean') {
|
|
351
|
+
result.skipped = skipped;
|
|
352
|
+
}
|
|
353
|
+
const reason = coerceString(interpreted.payload.reason);
|
|
354
|
+
if (reason) {
|
|
355
|
+
result.reason = reason;
|
|
356
|
+
}
|
|
357
|
+
if (interpreted.warnings?.length) {
|
|
358
|
+
result.warnings = interpreted.warnings;
|
|
359
|
+
}
|
|
360
|
+
if (interpreted.details?.length) {
|
|
361
|
+
result.details = interpreted.details;
|
|
362
|
+
}
|
|
363
|
+
return result;
|
|
147
364
|
}
|
|
148
|
-
|
|
365
|
+
|
|
366
|
+
const failure: Record<string, unknown> = {
|
|
367
|
+
success: false,
|
|
368
|
+
error: interpreted.error || interpreted.message
|
|
369
|
+
};
|
|
370
|
+
if (interpreted.message && interpreted.message !== failure.error) {
|
|
371
|
+
failure.message = interpreted.message;
|
|
372
|
+
}
|
|
373
|
+
const skippedFailure = coerceBoolean(interpreted.payload.skipped);
|
|
374
|
+
if (typeof skippedFailure === 'boolean') {
|
|
375
|
+
failure.skipped = skippedFailure;
|
|
376
|
+
}
|
|
377
|
+
const failureReason = coerceString(interpreted.payload.reason);
|
|
378
|
+
if (failureReason) {
|
|
379
|
+
failure.reason = failureReason;
|
|
380
|
+
}
|
|
381
|
+
if (interpreted.warnings?.length) {
|
|
382
|
+
failure.warnings = interpreted.warnings;
|
|
383
|
+
}
|
|
384
|
+
if (interpreted.details?.length) {
|
|
385
|
+
failure.details = interpreted.details;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return failure;
|
|
149
389
|
} catch (e) {
|
|
150
390
|
return { success: false, error: `Failed to save level: ${e}` };
|
|
151
391
|
}
|
|
@@ -160,24 +400,91 @@ except Exception as e:
|
|
|
160
400
|
const basePath = params.savePath || '/Game/Maps';
|
|
161
401
|
const isPartitioned = true; // default to World Partition for UE5
|
|
162
402
|
const fullPath = `${basePath}/${params.levelName}`;
|
|
163
|
-
const
|
|
403
|
+
const python = `
|
|
164
404
|
import unreal
|
|
405
|
+
import json
|
|
406
|
+
|
|
407
|
+
result = {
|
|
408
|
+
"success": False,
|
|
409
|
+
"message": "",
|
|
410
|
+
"error": "",
|
|
411
|
+
"warnings": [],
|
|
412
|
+
"details": [],
|
|
413
|
+
"path": r"${fullPath}",
|
|
414
|
+
"partitioned": ${isPartitioned ? 'True' : 'False'}
|
|
415
|
+
}
|
|
416
|
+
|
|
165
417
|
try:
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
418
|
+
les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
|
|
419
|
+
if les:
|
|
420
|
+
les.new_level(r"${fullPath}", ${isPartitioned ? 'True' : 'False'})
|
|
421
|
+
result["success"] = True
|
|
422
|
+
result["message"] = "Level created"
|
|
423
|
+
result["details"].append("Level created via LevelEditorSubsystem.new_level")
|
|
424
|
+
else:
|
|
425
|
+
result["error"] = "LevelEditorSubsystem not available"
|
|
426
|
+
except Exception as err:
|
|
427
|
+
result["error"] = f"Level creation failed: {err}"
|
|
428
|
+
|
|
429
|
+
if result["success"]:
|
|
430
|
+
if not result["message"]:
|
|
431
|
+
result["message"] = "Level created"
|
|
432
|
+
else:
|
|
433
|
+
if not result["error"]:
|
|
434
|
+
result["error"] = "Failed to create level"
|
|
435
|
+
if not result["message"]:
|
|
436
|
+
result["message"] = result["error"]
|
|
437
|
+
|
|
438
|
+
if not result["warnings"]:
|
|
439
|
+
result.pop("warnings")
|
|
440
|
+
if not result["details"]:
|
|
441
|
+
result.pop("details")
|
|
442
|
+
if result.get("error") is None:
|
|
443
|
+
result.pop("error")
|
|
444
|
+
|
|
445
|
+
print("RESULT:" + json.dumps(result))
|
|
174
446
|
`.trim();
|
|
447
|
+
|
|
175
448
|
try {
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
449
|
+
const response = await this.bridge.executePython(python);
|
|
450
|
+
const interpreted = interpretStandardResult(response, {
|
|
451
|
+
successMessage: 'Level created',
|
|
452
|
+
failureMessage: 'Failed to create level'
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
const path = coerceString(interpreted.payload.path) ?? fullPath;
|
|
456
|
+
const partitioned = coerceBoolean(interpreted.payload.partitioned, isPartitioned) ?? isPartitioned;
|
|
457
|
+
|
|
458
|
+
if (interpreted.success) {
|
|
459
|
+
const result: Record<string, unknown> = {
|
|
460
|
+
success: true,
|
|
461
|
+
message: interpreted.message,
|
|
462
|
+
path,
|
|
463
|
+
partitioned
|
|
464
|
+
};
|
|
465
|
+
if (interpreted.warnings?.length) {
|
|
466
|
+
result.warnings = interpreted.warnings;
|
|
467
|
+
}
|
|
468
|
+
if (interpreted.details?.length) {
|
|
469
|
+
result.details = interpreted.details;
|
|
470
|
+
}
|
|
471
|
+
return result;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const failure: Record<string, unknown> = {
|
|
475
|
+
success: false,
|
|
476
|
+
error: interpreted.error || interpreted.message,
|
|
477
|
+
path,
|
|
478
|
+
partitioned
|
|
479
|
+
};
|
|
480
|
+
if (interpreted.warnings?.length) {
|
|
481
|
+
failure.warnings = interpreted.warnings;
|
|
482
|
+
}
|
|
483
|
+
if (interpreted.details?.length) {
|
|
484
|
+
failure.details = interpreted.details;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return failure;
|
|
181
488
|
} catch (e) {
|
|
182
489
|
return { success: false, error: `Failed to create level: ${e}` };
|
|
183
490
|
}
|
|
@@ -190,18 +497,157 @@ except Exception as e:
|
|
|
190
497
|
shouldBeVisible: boolean;
|
|
191
498
|
position?: [number, number, number];
|
|
192
499
|
}) {
|
|
193
|
-
const
|
|
500
|
+
const python = `
|
|
501
|
+
import unreal
|
|
502
|
+
import json
|
|
503
|
+
|
|
504
|
+
result = {
|
|
505
|
+
"success": False,
|
|
506
|
+
"message": "",
|
|
507
|
+
"error": "",
|
|
508
|
+
"warnings": [],
|
|
509
|
+
"details": [],
|
|
510
|
+
"level": "${params.levelName}",
|
|
511
|
+
"loaded": ${params.shouldBeLoaded ? 'True' : 'False'},
|
|
512
|
+
"visible": ${params.shouldBeVisible ? 'True' : 'False'}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
try:
|
|
516
|
+
ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
|
|
517
|
+
world = ues.get_editor_world() if ues else None
|
|
518
|
+
if world:
|
|
519
|
+
updated = False
|
|
520
|
+
streaming_levels = []
|
|
521
|
+
try:
|
|
522
|
+
if hasattr(world, 'get_streaming_levels'):
|
|
523
|
+
streaming_levels = list(world.get_streaming_levels() or [])
|
|
524
|
+
except Exception as primary_error:
|
|
525
|
+
result["warnings"].append(f"get_streaming_levels unavailable: {primary_error}")
|
|
526
|
+
|
|
527
|
+
if not streaming_levels:
|
|
528
|
+
try:
|
|
529
|
+
if hasattr(world, 'get_level_streaming_levels'):
|
|
530
|
+
streaming_levels = list(world.get_level_streaming_levels() or [])
|
|
531
|
+
except Exception as alt_error:
|
|
532
|
+
result["warnings"].append(f"get_level_streaming_levels unavailable: {alt_error}")
|
|
533
|
+
|
|
534
|
+
if not streaming_levels:
|
|
535
|
+
try:
|
|
536
|
+
fallback_levels = getattr(world, 'streaming_levels', None)
|
|
537
|
+
if fallback_levels is not None:
|
|
538
|
+
streaming_levels = list(fallback_levels)
|
|
539
|
+
except Exception as attr_error:
|
|
540
|
+
result["warnings"].append(f"streaming_levels attribute unavailable: {attr_error}")
|
|
541
|
+
|
|
542
|
+
if not streaming_levels:
|
|
543
|
+
result["error"] = "Streaming levels unavailable"
|
|
544
|
+
else:
|
|
545
|
+
for streaming_level in streaming_levels:
|
|
546
|
+
try:
|
|
547
|
+
name = None
|
|
548
|
+
if hasattr(streaming_level, 'get_world_asset_package_name'):
|
|
549
|
+
name = streaming_level.get_world_asset_package_name()
|
|
550
|
+
if not name:
|
|
551
|
+
try:
|
|
552
|
+
name = str(streaming_level.get_editor_property('world_asset'))
|
|
553
|
+
except Exception:
|
|
554
|
+
name = None
|
|
555
|
+
|
|
556
|
+
if name and name.endswith('/${params.levelName}'):
|
|
557
|
+
try:
|
|
558
|
+
streaming_level.set_should_be_loaded(${params.shouldBeLoaded ? 'True' : 'False'})
|
|
559
|
+
except Exception as load_error:
|
|
560
|
+
result["warnings"].append(f"Failed to set loaded flag: {load_error}")
|
|
561
|
+
try:
|
|
562
|
+
streaming_level.set_should_be_visible(${params.shouldBeVisible ? 'True' : 'False'})
|
|
563
|
+
except Exception as visible_error:
|
|
564
|
+
result["warnings"].append(f"Failed to set visibility: {visible_error}")
|
|
565
|
+
updated = True
|
|
566
|
+
break
|
|
567
|
+
except Exception as iteration_error:
|
|
568
|
+
result["warnings"].append(f"Streaming level iteration error: {iteration_error}")
|
|
569
|
+
|
|
570
|
+
if updated:
|
|
571
|
+
result["success"] = True
|
|
572
|
+
result["message"] = "Streaming level updated"
|
|
573
|
+
result["details"].append("Streaming level flags updated for editor world")
|
|
574
|
+
else:
|
|
575
|
+
result["error"] = "Streaming level not found"
|
|
576
|
+
else:
|
|
577
|
+
result["error"] = "No editor world available"
|
|
578
|
+
except Exception as err:
|
|
579
|
+
result["error"] = f"Streaming level update failed: {err}"
|
|
580
|
+
|
|
581
|
+
if result["success"]:
|
|
582
|
+
if not result["message"]:
|
|
583
|
+
result["message"] = "Streaming level updated"
|
|
584
|
+
else:
|
|
585
|
+
if not result["error"]:
|
|
586
|
+
result["error"] = "Streaming level update failed"
|
|
587
|
+
if not result["message"]:
|
|
588
|
+
result["message"] = result["error"]
|
|
589
|
+
|
|
590
|
+
if not result["warnings"]:
|
|
591
|
+
result.pop("warnings")
|
|
592
|
+
if not result["details"]:
|
|
593
|
+
result.pop("details")
|
|
594
|
+
if result.get("error") is None:
|
|
595
|
+
result.pop("error")
|
|
596
|
+
|
|
597
|
+
print("RESULT:" + json.dumps(result))
|
|
598
|
+
`.trim();
|
|
599
|
+
|
|
194
600
|
try {
|
|
195
|
-
const
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
601
|
+
const response = await this.bridge.executePython(python);
|
|
602
|
+
const interpreted = interpretStandardResult(response, {
|
|
603
|
+
successMessage: 'Streaming level updated',
|
|
604
|
+
failureMessage: 'Streaming level update failed'
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
const levelName = coerceString(interpreted.payload.level) ?? params.levelName;
|
|
608
|
+
const loaded = coerceBoolean(interpreted.payload.loaded, params.shouldBeLoaded) ?? params.shouldBeLoaded;
|
|
609
|
+
const visible = coerceBoolean(interpreted.payload.visible, params.shouldBeVisible) ?? params.shouldBeVisible;
|
|
610
|
+
|
|
611
|
+
if (interpreted.success) {
|
|
612
|
+
const result: Record<string, unknown> = {
|
|
613
|
+
success: true,
|
|
614
|
+
message: interpreted.message,
|
|
615
|
+
level: levelName,
|
|
616
|
+
loaded,
|
|
617
|
+
visible
|
|
618
|
+
};
|
|
619
|
+
if (interpreted.warnings?.length) {
|
|
620
|
+
result.warnings = interpreted.warnings;
|
|
621
|
+
}
|
|
622
|
+
if (interpreted.details?.length) {
|
|
623
|
+
result.details = interpreted.details;
|
|
624
|
+
}
|
|
625
|
+
return result;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const failure: Record<string, unknown> = {
|
|
629
|
+
success: false,
|
|
630
|
+
error: interpreted.error || interpreted.message || 'Streaming level update failed',
|
|
631
|
+
level: levelName,
|
|
632
|
+
loaded,
|
|
633
|
+
visible
|
|
634
|
+
};
|
|
635
|
+
if (interpreted.message && interpreted.message !== failure.error) {
|
|
636
|
+
failure.message = interpreted.message;
|
|
637
|
+
}
|
|
638
|
+
if (interpreted.warnings?.length) {
|
|
639
|
+
failure.warnings = interpreted.warnings;
|
|
640
|
+
}
|
|
641
|
+
if (interpreted.details?.length) {
|
|
642
|
+
failure.details = interpreted.details;
|
|
643
|
+
}
|
|
644
|
+
return failure;
|
|
645
|
+
} catch {
|
|
646
|
+
const loadCmd = params.shouldBeLoaded ? 'Load' : 'Unload';
|
|
647
|
+
const visCmd = params.shouldBeVisible ? 'Show' : 'Hide';
|
|
648
|
+
const command = `StreamLevel ${params.levelName} ${loadCmd} ${visCmd}`;
|
|
649
|
+
return this.bridge.executeConsoleCommand(command);
|
|
650
|
+
}
|
|
205
651
|
}
|
|
206
652
|
|
|
207
653
|
// World composition
|
|
@@ -211,7 +657,7 @@ except Exception as e:
|
|
|
211
657
|
distanceStreaming?: boolean;
|
|
212
658
|
streamingDistance?: number;
|
|
213
659
|
}) {
|
|
214
|
-
|
|
660
|
+
const commands: string[] = [];
|
|
215
661
|
|
|
216
662
|
if (params.enableComposition) {
|
|
217
663
|
commands.push('EnableWorldComposition');
|
|
@@ -225,9 +671,7 @@ except Exception as e:
|
|
|
225
671
|
commands.push('DisableWorldComposition');
|
|
226
672
|
}
|
|
227
673
|
|
|
228
|
-
|
|
229
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
230
|
-
}
|
|
674
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
231
675
|
|
|
232
676
|
return { success: true, message: 'World composition configured' };
|
|
233
677
|
}
|
|
@@ -264,7 +708,7 @@ except Exception as e:
|
|
|
264
708
|
defaultPawn?: string;
|
|
265
709
|
killZ?: number;
|
|
266
710
|
}) {
|
|
267
|
-
|
|
711
|
+
const commands: string[] = [];
|
|
268
712
|
|
|
269
713
|
if (params.gravity !== undefined) {
|
|
270
714
|
commands.push(`SetWorldGravity ${params.gravity}`);
|
|
@@ -282,9 +726,7 @@ except Exception as e:
|
|
|
282
726
|
commands.push(`SetKillZ ${params.killZ}`);
|
|
283
727
|
}
|
|
284
728
|
|
|
285
|
-
|
|
286
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
287
|
-
}
|
|
729
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
288
730
|
|
|
289
731
|
return { success: true, message: 'World settings updated' };
|
|
290
732
|
}
|
|
@@ -303,68 +745,126 @@ except Exception as e:
|
|
|
303
745
|
rebuildAll?: boolean;
|
|
304
746
|
selectedOnly?: boolean;
|
|
305
747
|
}) {
|
|
306
|
-
|
|
307
|
-
const py = `
|
|
748
|
+
const python = `
|
|
308
749
|
import unreal
|
|
309
750
|
import json
|
|
751
|
+
|
|
752
|
+
result = {
|
|
753
|
+
"success": False,
|
|
754
|
+
"message": "",
|
|
755
|
+
"error": "",
|
|
756
|
+
"warnings": [],
|
|
757
|
+
"details": [],
|
|
758
|
+
"rebuildAll": ${params.rebuildAll ? 'True' : 'False'},
|
|
759
|
+
"selectedOnly": ${params.selectedOnly ? 'True' : 'False'},
|
|
760
|
+
"selectionCount": 0
|
|
761
|
+
}
|
|
762
|
+
|
|
310
763
|
try:
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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'}))
|
|
764
|
+
nav_system = unreal.EditorSubsystemLibrary.get_editor_subsystem(unreal.NavigationSystemV1)
|
|
765
|
+
if not nav_system:
|
|
766
|
+
ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
|
|
767
|
+
world = ues.get_editor_world() if ues else None
|
|
768
|
+
nav_system = unreal.NavigationSystemV1.get_navigation_system(world) if world else None
|
|
769
|
+
|
|
770
|
+
if nav_system:
|
|
771
|
+
if ${params.rebuildAll ? 'True' : 'False'}:
|
|
772
|
+
nav_system.navigation_build_async()
|
|
773
|
+
result["success"] = True
|
|
774
|
+
result["message"] = "Navigation rebuild started"
|
|
775
|
+
result["details"].append("Triggered full navigation rebuild")
|
|
339
776
|
else:
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
777
|
+
actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
|
|
778
|
+
selected_actors = actor_subsystem.get_selected_level_actors() if actor_subsystem else []
|
|
779
|
+
result["selectionCount"] = len(selected_actors) if selected_actors else 0
|
|
780
|
+
|
|
781
|
+
if ${params.selectedOnly ? 'True' : 'False'} and selected_actors:
|
|
782
|
+
for actor in selected_actors:
|
|
783
|
+
nav_system.update_nav_octree(actor)
|
|
784
|
+
result["success"] = True
|
|
785
|
+
result["message"] = f"Navigation updated for {len(selected_actors)} actors"
|
|
786
|
+
result["details"].append("Updated nav octree for selected actors")
|
|
787
|
+
elif selected_actors:
|
|
788
|
+
for actor in selected_actors:
|
|
789
|
+
nav_system.update_nav_octree(actor)
|
|
790
|
+
nav_system.update(0.0)
|
|
791
|
+
result["success"] = True
|
|
792
|
+
result["message"] = f"Navigation updated for {len(selected_actors)} actors"
|
|
793
|
+
result["details"].append("Updated nav octree and performed incremental update")
|
|
794
|
+
else:
|
|
795
|
+
nav_system.update(0.0)
|
|
796
|
+
result["success"] = True
|
|
797
|
+
result["message"] = "Navigation incremental update performed"
|
|
798
|
+
result["details"].append("No selected actors; performed incremental update")
|
|
799
|
+
else:
|
|
800
|
+
result["error"] = "Navigation system not available. Add a NavMeshBoundsVolume to the level first."
|
|
801
|
+
except AttributeError as attr_error:
|
|
802
|
+
result["error"] = f"Navigation API not available: {attr_error}"
|
|
803
|
+
except Exception as err:
|
|
804
|
+
result["error"] = f"Navigation build failed: {err}"
|
|
805
|
+
|
|
806
|
+
if result["success"]:
|
|
807
|
+
if not result["message"]:
|
|
808
|
+
result["message"] = "Navigation build started"
|
|
809
|
+
else:
|
|
810
|
+
if not result["error"]:
|
|
811
|
+
result["error"] = result["message"] or "Navigation build failed"
|
|
812
|
+
if not result["message"]:
|
|
813
|
+
result["message"] = result["error"]
|
|
814
|
+
|
|
815
|
+
if not result["warnings"]:
|
|
816
|
+
result.pop("warnings")
|
|
817
|
+
if not result["details"]:
|
|
818
|
+
result.pop("details")
|
|
819
|
+
if result.get("error") is None:
|
|
820
|
+
result.pop("error")
|
|
821
|
+
|
|
822
|
+
if not result.get("selectionCount"):
|
|
823
|
+
result.pop("selectionCount", None)
|
|
824
|
+
|
|
825
|
+
print("RESULT:" + json.dumps(result))
|
|
347
826
|
`.trim();
|
|
348
|
-
|
|
827
|
+
|
|
349
828
|
try {
|
|
350
|
-
const
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
829
|
+
const response = await this.bridge.executePython(python);
|
|
830
|
+
const interpreted = interpretStandardResult(response, {
|
|
831
|
+
successMessage: params.rebuildAll ? 'Navigation rebuild started' : 'Navigation update started',
|
|
832
|
+
failureMessage: 'Navigation build failed'
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
const result: Record<string, unknown> = interpreted.success
|
|
836
|
+
? { success: true, message: interpreted.message }
|
|
837
|
+
: { success: false, error: interpreted.error || interpreted.message };
|
|
838
|
+
|
|
839
|
+
const rebuildAll = coerceBoolean(interpreted.payload.rebuildAll, params.rebuildAll);
|
|
840
|
+
const selectedOnly = coerceBoolean(interpreted.payload.selectedOnly, params.selectedOnly);
|
|
841
|
+
if (typeof rebuildAll === 'boolean') {
|
|
842
|
+
result.rebuildAll = rebuildAll;
|
|
843
|
+
} else if (typeof params.rebuildAll === 'boolean') {
|
|
844
|
+
result.rebuildAll = params.rebuildAll;
|
|
845
|
+
}
|
|
846
|
+
if (typeof selectedOnly === 'boolean') {
|
|
847
|
+
result.selectedOnly = selectedOnly;
|
|
848
|
+
} else if (typeof params.selectedOnly === 'boolean') {
|
|
849
|
+
result.selectedOnly = params.selectedOnly;
|
|
360
850
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
851
|
+
|
|
852
|
+
const selectionCount = coerceNumber(interpreted.payload.selectionCount);
|
|
853
|
+
if (typeof selectionCount === 'number') {
|
|
854
|
+
result.selectionCount = selectionCount;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
if (interpreted.warnings?.length) {
|
|
858
|
+
result.warnings = interpreted.warnings;
|
|
859
|
+
}
|
|
860
|
+
if (interpreted.details?.length) {
|
|
861
|
+
result.details = interpreted.details;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
return result;
|
|
364
865
|
} catch (e) {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
success: false,
|
|
866
|
+
return {
|
|
867
|
+
success: false,
|
|
368
868
|
error: `Navigation build not available: ${e}. Please ensure a NavMeshBoundsVolume exists in the level.`
|
|
369
869
|
};
|
|
370
870
|
}
|
|
@@ -407,4 +907,5 @@ except Exception as e:
|
|
|
407
907
|
const command = `SetLevelLOD ${params.levelName} ${params.lodLevel} ${params.distance}`;
|
|
408
908
|
return this.bridge.executeConsoleCommand(command);
|
|
409
909
|
}
|
|
910
|
+
|
|
410
911
|
}
|