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/dist/tools/level.js
CHANGED
|
@@ -1,79 +1,179 @@
|
|
|
1
|
+
import { coerceBoolean, coerceNumber, coerceString, interpretStandardResult } from '../utils/result-helpers.js';
|
|
1
2
|
export class LevelTools {
|
|
2
3
|
bridge;
|
|
3
4
|
constructor(bridge) {
|
|
4
5
|
this.bridge = bridge;
|
|
5
6
|
}
|
|
6
|
-
// Execute console command
|
|
7
|
-
async _executeCommand(command) {
|
|
8
|
-
return this.bridge.httpCall('/remote/object/call', 'PUT', {
|
|
9
|
-
objectPath: '/Script/Engine.Default__KismetSystemLibrary',
|
|
10
|
-
functionName: 'ExecuteConsoleCommand',
|
|
11
|
-
parameters: {
|
|
12
|
-
WorldContextObject: null,
|
|
13
|
-
Command: command,
|
|
14
|
-
SpecificPlayer: null
|
|
15
|
-
},
|
|
16
|
-
generateTransaction: false
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
7
|
// Load level (using LevelEditorSubsystem to avoid crashes)
|
|
20
8
|
async loadLevel(params) {
|
|
21
9
|
if (params.streaming) {
|
|
22
|
-
|
|
23
|
-
|
|
10
|
+
const python = `
|
|
11
|
+
import unreal
|
|
12
|
+
import json
|
|
13
|
+
|
|
14
|
+
result = {
|
|
15
|
+
"success": False,
|
|
16
|
+
"message": "",
|
|
17
|
+
"error": "",
|
|
18
|
+
"details": [],
|
|
19
|
+
"warnings": []
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
|
|
24
|
+
world = ues.get_editor_world() if ues else None
|
|
25
|
+
if world:
|
|
26
|
+
try:
|
|
27
|
+
unreal.EditorLevelUtils.add_level_to_world(world, r"${params.levelPath}", unreal.LevelStreamingKismet)
|
|
28
|
+
result["success"] = True
|
|
29
|
+
result["message"] = "Streaming level added"
|
|
30
|
+
result["details"].append("Streaming level added via EditorLevelUtils")
|
|
31
|
+
except Exception as add_error:
|
|
32
|
+
result["error"] = f"Failed to add streaming level: {add_error}"
|
|
33
|
+
else:
|
|
34
|
+
result["error"] = "No editor world available"
|
|
35
|
+
except Exception as outer_error:
|
|
36
|
+
result["error"] = f"Streaming level operation failed: {outer_error}"
|
|
37
|
+
|
|
38
|
+
if result["success"]:
|
|
39
|
+
if not result["message"]:
|
|
40
|
+
result["message"] = "Streaming level added"
|
|
41
|
+
else:
|
|
42
|
+
if not result["error"]:
|
|
43
|
+
result["error"] = result["message"] or "Failed to add streaming level"
|
|
44
|
+
if not result["message"]:
|
|
45
|
+
result["message"] = result["error"]
|
|
46
|
+
|
|
47
|
+
if not result["warnings"]:
|
|
48
|
+
result.pop("warnings")
|
|
49
|
+
if not result["details"]:
|
|
50
|
+
result.pop("details")
|
|
51
|
+
if result.get("error") is None:
|
|
52
|
+
result.pop("error")
|
|
53
|
+
|
|
54
|
+
print("RESULT:" + json.dumps(result))
|
|
55
|
+
`.trim();
|
|
24
56
|
try {
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
57
|
+
const response = await this.bridge.executePython(python);
|
|
58
|
+
const interpreted = interpretStandardResult(response, {
|
|
59
|
+
successMessage: 'Streaming level added',
|
|
60
|
+
failureMessage: 'Failed to add streaming level'
|
|
61
|
+
});
|
|
62
|
+
if (interpreted.success) {
|
|
63
|
+
const result = {
|
|
64
|
+
success: true,
|
|
65
|
+
message: interpreted.message
|
|
66
|
+
};
|
|
67
|
+
if (interpreted.warnings?.length) {
|
|
68
|
+
result.warnings = interpreted.warnings;
|
|
69
|
+
}
|
|
70
|
+
if (interpreted.details?.length) {
|
|
71
|
+
result.details = interpreted.details;
|
|
33
72
|
}
|
|
34
|
-
|
|
73
|
+
return result;
|
|
35
74
|
}
|
|
36
75
|
}
|
|
37
76
|
catch { }
|
|
38
|
-
// Fallback to console
|
|
39
77
|
return this.bridge.executeConsoleCommand(`LoadStreamLevel ${params.levelPath}`);
|
|
40
78
|
}
|
|
41
79
|
else {
|
|
42
|
-
// Use LevelEditorSubsystem.load_level() to avoid crashes
|
|
43
|
-
// This properly handles the WorldContext and avoids the assertion failure
|
|
44
80
|
const python = `
|
|
45
81
|
import unreal
|
|
82
|
+
import json
|
|
83
|
+
|
|
84
|
+
result = {
|
|
85
|
+
"success": False,
|
|
86
|
+
"message": "",
|
|
87
|
+
"error": "",
|
|
88
|
+
"warnings": [],
|
|
89
|
+
"details": [],
|
|
90
|
+
"level": r"${params.levelPath}"
|
|
91
|
+
}
|
|
92
|
+
|
|
46
93
|
try:
|
|
47
|
-
|
|
94
|
+
level_path = r"${params.levelPath}"
|
|
95
|
+
asset_path = level_path
|
|
96
|
+
try:
|
|
97
|
+
tail = asset_path.rsplit('/', 1)[-1]
|
|
98
|
+
if '.' not in tail:
|
|
99
|
+
asset_path = f"{asset_path}.{tail}"
|
|
100
|
+
except Exception:
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
asset_exists = False
|
|
104
|
+
try:
|
|
105
|
+
asset_exists = unreal.EditorAssetLibrary.does_asset_exist(asset_path)
|
|
106
|
+
except Exception:
|
|
107
|
+
asset_exists = False
|
|
108
|
+
|
|
109
|
+
if not asset_exists:
|
|
110
|
+
result["error"] = f"Level not found: {asset_path}"
|
|
111
|
+
else:
|
|
48
112
|
les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
|
|
49
113
|
if les:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
success =
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
114
|
+
success = les.load_level(level_path)
|
|
115
|
+
if success:
|
|
116
|
+
result["success"] = True
|
|
117
|
+
result["message"] = "Level loaded successfully"
|
|
118
|
+
result["details"].append("Level loaded via LevelEditorSubsystem")
|
|
119
|
+
else:
|
|
120
|
+
result["error"] = "Failed to load level"
|
|
57
121
|
else:
|
|
58
|
-
|
|
59
|
-
except Exception as
|
|
60
|
-
|
|
122
|
+
result["error"] = "LevelEditorSubsystem not available"
|
|
123
|
+
except Exception as err:
|
|
124
|
+
result["error"] = f"Failed to load level: {err}"
|
|
125
|
+
|
|
126
|
+
if result["success"]:
|
|
127
|
+
if not result["message"]:
|
|
128
|
+
result["message"] = "Level loaded successfully"
|
|
129
|
+
else:
|
|
130
|
+
if not result["error"]:
|
|
131
|
+
result["error"] = "Failed to load level"
|
|
132
|
+
if not result["message"]:
|
|
133
|
+
result["message"] = result["error"]
|
|
134
|
+
|
|
135
|
+
if not result["warnings"]:
|
|
136
|
+
result.pop("warnings")
|
|
137
|
+
if not result["details"]:
|
|
138
|
+
result.pop("details")
|
|
139
|
+
if result.get("error") is None:
|
|
140
|
+
result.pop("error")
|
|
141
|
+
|
|
142
|
+
print("RESULT:" + json.dumps(result))
|
|
61
143
|
`.trim();
|
|
62
144
|
try {
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
145
|
+
const response = await this.bridge.executePython(python);
|
|
146
|
+
const interpreted = interpretStandardResult(response, {
|
|
147
|
+
successMessage: `Level ${params.levelPath} loaded`,
|
|
148
|
+
failureMessage: `Failed to load level ${params.levelPath}`
|
|
149
|
+
});
|
|
150
|
+
const payloadLevel = coerceString(interpreted.payload.level) ?? params.levelPath;
|
|
151
|
+
if (interpreted.success) {
|
|
152
|
+
const result = {
|
|
153
|
+
success: true,
|
|
154
|
+
message: interpreted.message,
|
|
155
|
+
level: payloadLevel
|
|
156
|
+
};
|
|
157
|
+
if (interpreted.warnings?.length) {
|
|
158
|
+
result.warnings = interpreted.warnings;
|
|
72
159
|
}
|
|
73
|
-
|
|
160
|
+
if (interpreted.details?.length) {
|
|
161
|
+
result.details = interpreted.details;
|
|
162
|
+
}
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
const failure = {
|
|
166
|
+
success: false,
|
|
167
|
+
error: interpreted.error || interpreted.message,
|
|
168
|
+
level: payloadLevel
|
|
169
|
+
};
|
|
170
|
+
if (interpreted.warnings?.length) {
|
|
171
|
+
failure.warnings = interpreted.warnings;
|
|
74
172
|
}
|
|
75
|
-
|
|
76
|
-
|
|
173
|
+
if (interpreted.details?.length) {
|
|
174
|
+
failure.details = interpreted.details;
|
|
175
|
+
}
|
|
176
|
+
return failure;
|
|
77
177
|
}
|
|
78
178
|
catch (e) {
|
|
79
179
|
return { success: false, error: `Failed to load level: ${e}` };
|
|
@@ -82,74 +182,189 @@ except Exception as e:
|
|
|
82
182
|
}
|
|
83
183
|
// Save current level
|
|
84
184
|
async saveLevel(_params) {
|
|
85
|
-
// Use Python EditorLevelLibrary.save_current_level for reliability
|
|
86
185
|
const python = `
|
|
87
186
|
import unreal
|
|
187
|
+
import json
|
|
188
|
+
|
|
189
|
+
result = {
|
|
190
|
+
"success": False,
|
|
191
|
+
"message": "",
|
|
192
|
+
"error": "",
|
|
193
|
+
"warnings": [],
|
|
194
|
+
"details": [],
|
|
195
|
+
"skipped": False,
|
|
196
|
+
"reason": ""
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
def print_result(payload):
|
|
200
|
+
data = dict(payload)
|
|
201
|
+
if data.get("skipped") and not data.get("message"):
|
|
202
|
+
data["message"] = data.get("reason") or "Level save skipped"
|
|
203
|
+
if data.get("success") and not data.get("message"):
|
|
204
|
+
data["message"] = "Level saved"
|
|
205
|
+
if not data.get("success"):
|
|
206
|
+
if not data.get("error"):
|
|
207
|
+
data["error"] = data.get("message") or "Failed to save level"
|
|
208
|
+
if not data.get("message"):
|
|
209
|
+
data["message"] = data.get("error") or "Failed to save level"
|
|
210
|
+
if data.get("success"):
|
|
211
|
+
data.pop("error", None)
|
|
212
|
+
if not data.get("warnings"):
|
|
213
|
+
data.pop("warnings", None)
|
|
214
|
+
if not data.get("details"):
|
|
215
|
+
data.pop("details", None)
|
|
216
|
+
if not data.get("skipped"):
|
|
217
|
+
data.pop("skipped", None)
|
|
218
|
+
data.pop("reason", None)
|
|
219
|
+
else:
|
|
220
|
+
if not data.get("reason"):
|
|
221
|
+
data.pop("reason", None)
|
|
222
|
+
print("RESULT:" + json.dumps(data))
|
|
223
|
+
|
|
88
224
|
try:
|
|
89
|
-
|
|
225
|
+
# Attempt to reduce source control prompts (best-effort, may be a no-op depending on UE version)
|
|
226
|
+
try:
|
|
227
|
+
prefs = unreal.SourceControlPreferences()
|
|
228
|
+
muted = False
|
|
90
229
|
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
|
|
230
|
+
prefs.set_enable_source_control(False)
|
|
231
|
+
muted = True
|
|
99
232
|
except Exception:
|
|
100
|
-
|
|
233
|
+
try:
|
|
234
|
+
prefs.enable_source_control = False
|
|
235
|
+
muted = True
|
|
236
|
+
except Exception:
|
|
237
|
+
muted = False
|
|
238
|
+
if muted:
|
|
239
|
+
result["details"].append("Source control prompts disabled")
|
|
240
|
+
except Exception:
|
|
241
|
+
pass
|
|
101
242
|
|
|
102
|
-
|
|
243
|
+
# Determine if level is dirty and save via LevelEditorSubsystem when possible
|
|
244
|
+
world = None
|
|
245
|
+
try:
|
|
246
|
+
world = unreal.EditorSubsystemLibrary.get_editor_world()
|
|
247
|
+
except Exception:
|
|
103
248
|
try:
|
|
104
|
-
|
|
249
|
+
ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
|
|
250
|
+
world = ues.get_editor_world() if ues else None
|
|
251
|
+
except Exception:
|
|
252
|
+
world = None
|
|
253
|
+
|
|
254
|
+
pkg_path = None
|
|
255
|
+
try:
|
|
256
|
+
if world is not None:
|
|
257
|
+
full = world.get_path_name()
|
|
258
|
+
pkg_path = full.split('.')[0] if '.' in full else full
|
|
259
|
+
if pkg_path:
|
|
260
|
+
result["details"].append(f"Detected level package: {pkg_path}")
|
|
261
|
+
except Exception:
|
|
262
|
+
pkg_path = None
|
|
263
|
+
|
|
264
|
+
skip_save = False
|
|
265
|
+
try:
|
|
266
|
+
is_dirty = None
|
|
267
|
+
if pkg_path:
|
|
268
|
+
editor_asset_lib = getattr(unreal, 'EditorAssetLibrary', None)
|
|
269
|
+
if editor_asset_lib and hasattr(editor_asset_lib, 'is_asset_dirty'):
|
|
105
270
|
try:
|
|
106
|
-
|
|
107
|
-
except Exception:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
pkg_path = None
|
|
271
|
+
is_dirty = editor_asset_lib.is_asset_dirty(pkg_path)
|
|
272
|
+
except Exception as check_error:
|
|
273
|
+
result["warnings"].append(f"EditorAssetLibrary.is_asset_dirty failed: {check_error}")
|
|
274
|
+
is_dirty = None
|
|
275
|
+
if is_dirty is None:
|
|
276
|
+
# Fallback: attempt to inspect the current level package
|
|
113
277
|
try:
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
278
|
+
ell = getattr(unreal, 'EditorLevelLibrary', None)
|
|
279
|
+
level = ell.get_current_level() if ell and hasattr(ell, 'get_current_level') else None
|
|
280
|
+
package = level.get_outermost() if level and hasattr(level, 'get_outermost') else None
|
|
281
|
+
if package and hasattr(package, 'is_dirty'):
|
|
282
|
+
is_dirty = package.is_dirty()
|
|
283
|
+
except Exception as fallback_error:
|
|
284
|
+
result["warnings"].append(f"Fallback dirty check failed: {fallback_error}")
|
|
285
|
+
if is_dirty is False:
|
|
286
|
+
result["success"] = True
|
|
287
|
+
result["skipped"] = True
|
|
288
|
+
result["reason"] = "Level not dirty"
|
|
289
|
+
result["message"] = "Level save skipped"
|
|
290
|
+
skip_save = True
|
|
291
|
+
elif is_dirty is None and pkg_path:
|
|
292
|
+
result["warnings"].append("Unable to determine level dirty state; attempting save anyway")
|
|
293
|
+
except Exception as dirty_error:
|
|
294
|
+
result["warnings"].append(f"Failed to check level dirty state: {dirty_error}")
|
|
124
295
|
|
|
125
|
-
|
|
296
|
+
if not skip_save:
|
|
126
297
|
saved = False
|
|
127
298
|
try:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
299
|
+
les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
|
|
300
|
+
if les:
|
|
301
|
+
les.save_current_level()
|
|
302
|
+
saved = True
|
|
303
|
+
result["details"].append("Level saved via LevelEditorSubsystem")
|
|
304
|
+
except Exception as save_error:
|
|
305
|
+
result["error"] = f"Level save failed: {save_error}"
|
|
306
|
+
saved = False
|
|
307
|
+
|
|
134
308
|
if not saved:
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
309
|
+
raise Exception('LevelEditorSubsystem not available')
|
|
310
|
+
|
|
311
|
+
result["success"] = True
|
|
312
|
+
if not result["message"]:
|
|
313
|
+
result["message"] = "Level saved"
|
|
314
|
+
except Exception as err:
|
|
315
|
+
result["error"] = str(err)
|
|
316
|
+
|
|
317
|
+
print_result(result)
|
|
140
318
|
`.trim();
|
|
141
319
|
try {
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
320
|
+
const response = await this.bridge.executePython(python);
|
|
321
|
+
const interpreted = interpretStandardResult(response, {
|
|
322
|
+
successMessage: 'Level saved',
|
|
323
|
+
failureMessage: 'Failed to save level'
|
|
324
|
+
});
|
|
325
|
+
if (interpreted.success) {
|
|
326
|
+
const result = {
|
|
327
|
+
success: true,
|
|
328
|
+
message: interpreted.message
|
|
329
|
+
};
|
|
330
|
+
const skipped = coerceBoolean(interpreted.payload.skipped);
|
|
331
|
+
if (typeof skipped === 'boolean') {
|
|
332
|
+
result.skipped = skipped;
|
|
333
|
+
}
|
|
334
|
+
const reason = coerceString(interpreted.payload.reason);
|
|
335
|
+
if (reason) {
|
|
336
|
+
result.reason = reason;
|
|
149
337
|
}
|
|
150
|
-
|
|
338
|
+
if (interpreted.warnings?.length) {
|
|
339
|
+
result.warnings = interpreted.warnings;
|
|
340
|
+
}
|
|
341
|
+
if (interpreted.details?.length) {
|
|
342
|
+
result.details = interpreted.details;
|
|
343
|
+
}
|
|
344
|
+
return result;
|
|
345
|
+
}
|
|
346
|
+
const failure = {
|
|
347
|
+
success: false,
|
|
348
|
+
error: interpreted.error || interpreted.message
|
|
349
|
+
};
|
|
350
|
+
if (interpreted.message && interpreted.message !== failure.error) {
|
|
351
|
+
failure.message = interpreted.message;
|
|
352
|
+
}
|
|
353
|
+
const skippedFailure = coerceBoolean(interpreted.payload.skipped);
|
|
354
|
+
if (typeof skippedFailure === 'boolean') {
|
|
355
|
+
failure.skipped = skippedFailure;
|
|
356
|
+
}
|
|
357
|
+
const failureReason = coerceString(interpreted.payload.reason);
|
|
358
|
+
if (failureReason) {
|
|
359
|
+
failure.reason = failureReason;
|
|
360
|
+
}
|
|
361
|
+
if (interpreted.warnings?.length) {
|
|
362
|
+
failure.warnings = interpreted.warnings;
|
|
151
363
|
}
|
|
152
|
-
|
|
364
|
+
if (interpreted.details?.length) {
|
|
365
|
+
failure.details = interpreted.details;
|
|
366
|
+
}
|
|
367
|
+
return failure;
|
|
153
368
|
}
|
|
154
369
|
catch (e) {
|
|
155
370
|
return { success: false, error: `Failed to save level: ${e}` };
|
|
@@ -160,30 +375,86 @@ except Exception as e:
|
|
|
160
375
|
const basePath = params.savePath || '/Game/Maps';
|
|
161
376
|
const isPartitioned = true; // default to World Partition for UE5
|
|
162
377
|
const fullPath = `${basePath}/${params.levelName}`;
|
|
163
|
-
const
|
|
378
|
+
const python = `
|
|
164
379
|
import unreal
|
|
380
|
+
import json
|
|
381
|
+
|
|
382
|
+
result = {
|
|
383
|
+
"success": False,
|
|
384
|
+
"message": "",
|
|
385
|
+
"error": "",
|
|
386
|
+
"warnings": [],
|
|
387
|
+
"details": [],
|
|
388
|
+
"path": r"${fullPath}",
|
|
389
|
+
"partitioned": ${isPartitioned ? 'True' : 'False'}
|
|
390
|
+
}
|
|
391
|
+
|
|
165
392
|
try:
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
393
|
+
les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
|
|
394
|
+
if les:
|
|
395
|
+
les.new_level(r"${fullPath}", ${isPartitioned ? 'True' : 'False'})
|
|
396
|
+
result["success"] = True
|
|
397
|
+
result["message"] = "Level created"
|
|
398
|
+
result["details"].append("Level created via LevelEditorSubsystem.new_level")
|
|
399
|
+
else:
|
|
400
|
+
result["error"] = "LevelEditorSubsystem not available"
|
|
401
|
+
except Exception as err:
|
|
402
|
+
result["error"] = f"Level creation failed: {err}"
|
|
403
|
+
|
|
404
|
+
if result["success"]:
|
|
405
|
+
if not result["message"]:
|
|
406
|
+
result["message"] = "Level created"
|
|
407
|
+
else:
|
|
408
|
+
if not result["error"]:
|
|
409
|
+
result["error"] = "Failed to create level"
|
|
410
|
+
if not result["message"]:
|
|
411
|
+
result["message"] = result["error"]
|
|
412
|
+
|
|
413
|
+
if not result["warnings"]:
|
|
414
|
+
result.pop("warnings")
|
|
415
|
+
if not result["details"]:
|
|
416
|
+
result.pop("details")
|
|
417
|
+
if result.get("error") is None:
|
|
418
|
+
result.pop("error")
|
|
419
|
+
|
|
420
|
+
print("RESULT:" + json.dumps(result))
|
|
174
421
|
`.trim();
|
|
175
422
|
try {
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
423
|
+
const response = await this.bridge.executePython(python);
|
|
424
|
+
const interpreted = interpretStandardResult(response, {
|
|
425
|
+
successMessage: 'Level created',
|
|
426
|
+
failureMessage: 'Failed to create level'
|
|
427
|
+
});
|
|
428
|
+
const path = coerceString(interpreted.payload.path) ?? fullPath;
|
|
429
|
+
const partitioned = coerceBoolean(interpreted.payload.partitioned, isPartitioned) ?? isPartitioned;
|
|
430
|
+
if (interpreted.success) {
|
|
431
|
+
const result = {
|
|
432
|
+
success: true,
|
|
433
|
+
message: interpreted.message,
|
|
434
|
+
path,
|
|
435
|
+
partitioned
|
|
436
|
+
};
|
|
437
|
+
if (interpreted.warnings?.length) {
|
|
438
|
+
result.warnings = interpreted.warnings;
|
|
439
|
+
}
|
|
440
|
+
if (interpreted.details?.length) {
|
|
441
|
+
result.details = interpreted.details;
|
|
183
442
|
}
|
|
184
|
-
|
|
443
|
+
return result;
|
|
185
444
|
}
|
|
186
|
-
|
|
445
|
+
const failure = {
|
|
446
|
+
success: false,
|
|
447
|
+
error: interpreted.error || interpreted.message,
|
|
448
|
+
path,
|
|
449
|
+
partitioned
|
|
450
|
+
};
|
|
451
|
+
if (interpreted.warnings?.length) {
|
|
452
|
+
failure.warnings = interpreted.warnings;
|
|
453
|
+
}
|
|
454
|
+
if (interpreted.details?.length) {
|
|
455
|
+
failure.details = interpreted.details;
|
|
456
|
+
}
|
|
457
|
+
return failure;
|
|
187
458
|
}
|
|
188
459
|
catch (e) {
|
|
189
460
|
return { success: false, error: `Failed to create level: ${e}` };
|
|
@@ -191,26 +462,154 @@ except Exception as e:
|
|
|
191
462
|
}
|
|
192
463
|
// Stream level (Python attempt with fallback)
|
|
193
464
|
async streamLevel(params) {
|
|
194
|
-
const
|
|
465
|
+
const python = `
|
|
466
|
+
import unreal
|
|
467
|
+
import json
|
|
468
|
+
|
|
469
|
+
result = {
|
|
470
|
+
"success": False,
|
|
471
|
+
"message": "",
|
|
472
|
+
"error": "",
|
|
473
|
+
"warnings": [],
|
|
474
|
+
"details": [],
|
|
475
|
+
"level": "${params.levelName}",
|
|
476
|
+
"loaded": ${params.shouldBeLoaded ? 'True' : 'False'},
|
|
477
|
+
"visible": ${params.shouldBeVisible ? 'True' : 'False'}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
try:
|
|
481
|
+
ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
|
|
482
|
+
world = ues.get_editor_world() if ues else None
|
|
483
|
+
if world:
|
|
484
|
+
updated = False
|
|
485
|
+
streaming_levels = []
|
|
486
|
+
try:
|
|
487
|
+
if hasattr(world, 'get_streaming_levels'):
|
|
488
|
+
streaming_levels = list(world.get_streaming_levels() or [])
|
|
489
|
+
except Exception as primary_error:
|
|
490
|
+
result["warnings"].append(f"get_streaming_levels unavailable: {primary_error}")
|
|
491
|
+
|
|
492
|
+
if not streaming_levels:
|
|
493
|
+
try:
|
|
494
|
+
if hasattr(world, 'get_level_streaming_levels'):
|
|
495
|
+
streaming_levels = list(world.get_level_streaming_levels() or [])
|
|
496
|
+
except Exception as alt_error:
|
|
497
|
+
result["warnings"].append(f"get_level_streaming_levels unavailable: {alt_error}")
|
|
498
|
+
|
|
499
|
+
if not streaming_levels:
|
|
500
|
+
try:
|
|
501
|
+
fallback_levels = getattr(world, 'streaming_levels', None)
|
|
502
|
+
if fallback_levels is not None:
|
|
503
|
+
streaming_levels = list(fallback_levels)
|
|
504
|
+
except Exception as attr_error:
|
|
505
|
+
result["warnings"].append(f"streaming_levels attribute unavailable: {attr_error}")
|
|
506
|
+
|
|
507
|
+
if not streaming_levels:
|
|
508
|
+
result["error"] = "Streaming levels unavailable"
|
|
509
|
+
else:
|
|
510
|
+
for streaming_level in streaming_levels:
|
|
511
|
+
try:
|
|
512
|
+
name = None
|
|
513
|
+
if hasattr(streaming_level, 'get_world_asset_package_name'):
|
|
514
|
+
name = streaming_level.get_world_asset_package_name()
|
|
515
|
+
if not name:
|
|
516
|
+
try:
|
|
517
|
+
name = str(streaming_level.get_editor_property('world_asset'))
|
|
518
|
+
except Exception:
|
|
519
|
+
name = None
|
|
520
|
+
|
|
521
|
+
if name and name.endswith('/${params.levelName}'):
|
|
522
|
+
try:
|
|
523
|
+
streaming_level.set_should_be_loaded(${params.shouldBeLoaded ? 'True' : 'False'})
|
|
524
|
+
except Exception as load_error:
|
|
525
|
+
result["warnings"].append(f"Failed to set loaded flag: {load_error}")
|
|
526
|
+
try:
|
|
527
|
+
streaming_level.set_should_be_visible(${params.shouldBeVisible ? 'True' : 'False'})
|
|
528
|
+
except Exception as visible_error:
|
|
529
|
+
result["warnings"].append(f"Failed to set visibility: {visible_error}")
|
|
530
|
+
updated = True
|
|
531
|
+
break
|
|
532
|
+
except Exception as iteration_error:
|
|
533
|
+
result["warnings"].append(f"Streaming level iteration error: {iteration_error}")
|
|
534
|
+
|
|
535
|
+
if updated:
|
|
536
|
+
result["success"] = True
|
|
537
|
+
result["message"] = "Streaming level updated"
|
|
538
|
+
result["details"].append("Streaming level flags updated for editor world")
|
|
539
|
+
else:
|
|
540
|
+
result["error"] = "Streaming level not found"
|
|
541
|
+
else:
|
|
542
|
+
result["error"] = "No editor world available"
|
|
543
|
+
except Exception as err:
|
|
544
|
+
result["error"] = f"Streaming level update failed: {err}"
|
|
545
|
+
|
|
546
|
+
if result["success"]:
|
|
547
|
+
if not result["message"]:
|
|
548
|
+
result["message"] = "Streaming level updated"
|
|
549
|
+
else:
|
|
550
|
+
if not result["error"]:
|
|
551
|
+
result["error"] = "Streaming level update failed"
|
|
552
|
+
if not result["message"]:
|
|
553
|
+
result["message"] = result["error"]
|
|
554
|
+
|
|
555
|
+
if not result["warnings"]:
|
|
556
|
+
result.pop("warnings")
|
|
557
|
+
if not result["details"]:
|
|
558
|
+
result.pop("details")
|
|
559
|
+
if result.get("error") is None:
|
|
560
|
+
result.pop("error")
|
|
561
|
+
|
|
562
|
+
print("RESULT:" + json.dumps(result))
|
|
563
|
+
`.trim();
|
|
195
564
|
try {
|
|
196
|
-
const
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
565
|
+
const response = await this.bridge.executePython(python);
|
|
566
|
+
const interpreted = interpretStandardResult(response, {
|
|
567
|
+
successMessage: 'Streaming level updated',
|
|
568
|
+
failureMessage: 'Streaming level update failed'
|
|
569
|
+
});
|
|
570
|
+
const levelName = coerceString(interpreted.payload.level) ?? params.levelName;
|
|
571
|
+
const loaded = coerceBoolean(interpreted.payload.loaded, params.shouldBeLoaded) ?? params.shouldBeLoaded;
|
|
572
|
+
const visible = coerceBoolean(interpreted.payload.visible, params.shouldBeVisible) ?? params.shouldBeVisible;
|
|
573
|
+
if (interpreted.success) {
|
|
574
|
+
const result = {
|
|
575
|
+
success: true,
|
|
576
|
+
message: interpreted.message,
|
|
577
|
+
level: levelName,
|
|
578
|
+
loaded,
|
|
579
|
+
visible
|
|
580
|
+
};
|
|
581
|
+
if (interpreted.warnings?.length) {
|
|
582
|
+
result.warnings = interpreted.warnings;
|
|
583
|
+
}
|
|
584
|
+
if (interpreted.details?.length) {
|
|
585
|
+
result.details = interpreted.details;
|
|
204
586
|
}
|
|
205
|
-
|
|
587
|
+
return result;
|
|
588
|
+
}
|
|
589
|
+
const failure = {
|
|
590
|
+
success: false,
|
|
591
|
+
error: interpreted.error || interpreted.message || 'Streaming level update failed',
|
|
592
|
+
level: levelName,
|
|
593
|
+
loaded,
|
|
594
|
+
visible
|
|
595
|
+
};
|
|
596
|
+
if (interpreted.message && interpreted.message !== failure.error) {
|
|
597
|
+
failure.message = interpreted.message;
|
|
598
|
+
}
|
|
599
|
+
if (interpreted.warnings?.length) {
|
|
600
|
+
failure.warnings = interpreted.warnings;
|
|
206
601
|
}
|
|
602
|
+
if (interpreted.details?.length) {
|
|
603
|
+
failure.details = interpreted.details;
|
|
604
|
+
}
|
|
605
|
+
return failure;
|
|
606
|
+
}
|
|
607
|
+
catch {
|
|
608
|
+
const loadCmd = params.shouldBeLoaded ? 'Load' : 'Unload';
|
|
609
|
+
const visCmd = params.shouldBeVisible ? 'Show' : 'Hide';
|
|
610
|
+
const command = `StreamLevel ${params.levelName} ${loadCmd} ${visCmd}`;
|
|
611
|
+
return this.bridge.executeConsoleCommand(command);
|
|
207
612
|
}
|
|
208
|
-
catch { }
|
|
209
|
-
// Fallback
|
|
210
|
-
const loadCmd = params.shouldBeLoaded ? 'Load' : 'Unload';
|
|
211
|
-
const visCmd = params.shouldBeVisible ? 'Show' : 'Hide';
|
|
212
|
-
const command = `StreamLevel ${params.levelName} ${loadCmd} ${visCmd}`;
|
|
213
|
-
return this.bridge.executeConsoleCommand(command);
|
|
214
613
|
}
|
|
215
614
|
// World composition
|
|
216
615
|
async setupWorldComposition(params) {
|
|
@@ -227,9 +626,7 @@ except Exception as e:
|
|
|
227
626
|
else {
|
|
228
627
|
commands.push('DisableWorldComposition');
|
|
229
628
|
}
|
|
230
|
-
|
|
231
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
232
|
-
}
|
|
629
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
233
630
|
return { success: true, message: 'World composition configured' };
|
|
234
631
|
}
|
|
235
632
|
// Level blueprint
|
|
@@ -260,9 +657,7 @@ except Exception as e:
|
|
|
260
657
|
if (params.killZ !== undefined) {
|
|
261
658
|
commands.push(`SetKillZ ${params.killZ}`);
|
|
262
659
|
}
|
|
263
|
-
|
|
264
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
265
|
-
}
|
|
660
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
266
661
|
return { success: true, message: 'World settings updated' };
|
|
267
662
|
}
|
|
268
663
|
// Level bounds
|
|
@@ -272,66 +667,121 @@ except Exception as e:
|
|
|
272
667
|
}
|
|
273
668
|
// Navigation mesh
|
|
274
669
|
async buildNavMesh(params) {
|
|
275
|
-
|
|
276
|
-
const py = `
|
|
670
|
+
const python = `
|
|
277
671
|
import unreal
|
|
278
672
|
import json
|
|
673
|
+
|
|
674
|
+
result = {
|
|
675
|
+
"success": False,
|
|
676
|
+
"message": "",
|
|
677
|
+
"error": "",
|
|
678
|
+
"warnings": [],
|
|
679
|
+
"details": [],
|
|
680
|
+
"rebuildAll": ${params.rebuildAll ? 'True' : 'False'},
|
|
681
|
+
"selectedOnly": ${params.selectedOnly ? 'True' : 'False'},
|
|
682
|
+
"selectionCount": 0
|
|
683
|
+
}
|
|
684
|
+
|
|
279
685
|
try:
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
# Rebuild all navigation
|
|
293
|
-
nav_system.navigation_build_async()
|
|
294
|
-
print('RESULT:' + json.dumps({'success': True, 'message': 'Navigation rebuild started'}))
|
|
295
|
-
else:
|
|
296
|
-
# Update navigation for selected actors only
|
|
297
|
-
# Use EditorActorSubsystem to get selected actors
|
|
298
|
-
actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
|
|
299
|
-
selected_actors = actor_subsystem.get_selected_level_actors() if actor_subsystem else []
|
|
300
|
-
if selected_actors:
|
|
301
|
-
for actor in selected_actors:
|
|
302
|
-
nav_system.update_nav_octree(actor)
|
|
303
|
-
print('RESULT:' + json.dumps({'success': True, 'message': f'Navigation updated for {len(selected_actors)} actors'}))
|
|
304
|
-
else:
|
|
305
|
-
# If nothing selected, do a safe incremental update
|
|
306
|
-
nav_system.update(0.0)
|
|
307
|
-
print('RESULT:' + json.dumps({'success': True, 'message': 'Navigation incremental update performed'}))
|
|
686
|
+
nav_system = unreal.EditorSubsystemLibrary.get_editor_subsystem(unreal.NavigationSystemV1)
|
|
687
|
+
if not nav_system:
|
|
688
|
+
ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
|
|
689
|
+
world = ues.get_editor_world() if ues else None
|
|
690
|
+
nav_system = unreal.NavigationSystemV1.get_navigation_system(world) if world else None
|
|
691
|
+
|
|
692
|
+
if nav_system:
|
|
693
|
+
if ${params.rebuildAll ? 'True' : 'False'}:
|
|
694
|
+
nav_system.navigation_build_async()
|
|
695
|
+
result["success"] = True
|
|
696
|
+
result["message"] = "Navigation rebuild started"
|
|
697
|
+
result["details"].append("Triggered full navigation rebuild")
|
|
308
698
|
else:
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
699
|
+
actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
|
|
700
|
+
selected_actors = actor_subsystem.get_selected_level_actors() if actor_subsystem else []
|
|
701
|
+
result["selectionCount"] = len(selected_actors) if selected_actors else 0
|
|
702
|
+
|
|
703
|
+
if ${params.selectedOnly ? 'True' : 'False'} and selected_actors:
|
|
704
|
+
for actor in selected_actors:
|
|
705
|
+
nav_system.update_nav_octree(actor)
|
|
706
|
+
result["success"] = True
|
|
707
|
+
result["message"] = f"Navigation updated for {len(selected_actors)} actors"
|
|
708
|
+
result["details"].append("Updated nav octree for selected actors")
|
|
709
|
+
elif selected_actors:
|
|
710
|
+
for actor in selected_actors:
|
|
711
|
+
nav_system.update_nav_octree(actor)
|
|
712
|
+
nav_system.update(0.0)
|
|
713
|
+
result["success"] = True
|
|
714
|
+
result["message"] = f"Navigation updated for {len(selected_actors)} actors"
|
|
715
|
+
result["details"].append("Updated nav octree and performed incremental update")
|
|
716
|
+
else:
|
|
717
|
+
nav_system.update(0.0)
|
|
718
|
+
result["success"] = True
|
|
719
|
+
result["message"] = "Navigation incremental update performed"
|
|
720
|
+
result["details"].append("No selected actors; performed incremental update")
|
|
721
|
+
else:
|
|
722
|
+
result["error"] = "Navigation system not available. Add a NavMeshBoundsVolume to the level first."
|
|
723
|
+
except AttributeError as attr_error:
|
|
724
|
+
result["error"] = f"Navigation API not available: {attr_error}"
|
|
725
|
+
except Exception as err:
|
|
726
|
+
result["error"] = f"Navigation build failed: {err}"
|
|
727
|
+
|
|
728
|
+
if result["success"]:
|
|
729
|
+
if not result["message"]:
|
|
730
|
+
result["message"] = "Navigation build started"
|
|
731
|
+
else:
|
|
732
|
+
if not result["error"]:
|
|
733
|
+
result["error"] = result["message"] or "Navigation build failed"
|
|
734
|
+
if not result["message"]:
|
|
735
|
+
result["message"] = result["error"]
|
|
736
|
+
|
|
737
|
+
if not result["warnings"]:
|
|
738
|
+
result.pop("warnings")
|
|
739
|
+
if not result["details"]:
|
|
740
|
+
result.pop("details")
|
|
741
|
+
if result.get("error") is None:
|
|
742
|
+
result.pop("error")
|
|
743
|
+
|
|
744
|
+
if not result.get("selectionCount"):
|
|
745
|
+
result.pop("selectionCount", None)
|
|
746
|
+
|
|
747
|
+
print("RESULT:" + json.dumps(result))
|
|
316
748
|
`.trim();
|
|
317
749
|
try {
|
|
318
|
-
const
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
750
|
+
const response = await this.bridge.executePython(python);
|
|
751
|
+
const interpreted = interpretStandardResult(response, {
|
|
752
|
+
successMessage: params.rebuildAll ? 'Navigation rebuild started' : 'Navigation update started',
|
|
753
|
+
failureMessage: 'Navigation build failed'
|
|
754
|
+
});
|
|
755
|
+
const result = interpreted.success
|
|
756
|
+
? { success: true, message: interpreted.message }
|
|
757
|
+
: { success: false, error: interpreted.error || interpreted.message };
|
|
758
|
+
const rebuildAll = coerceBoolean(interpreted.payload.rebuildAll, params.rebuildAll);
|
|
759
|
+
const selectedOnly = coerceBoolean(interpreted.payload.selectedOnly, params.selectedOnly);
|
|
760
|
+
if (typeof rebuildAll === 'boolean') {
|
|
761
|
+
result.rebuildAll = rebuildAll;
|
|
762
|
+
}
|
|
763
|
+
else if (typeof params.rebuildAll === 'boolean') {
|
|
764
|
+
result.rebuildAll = params.rebuildAll;
|
|
765
|
+
}
|
|
766
|
+
if (typeof selectedOnly === 'boolean') {
|
|
767
|
+
result.selectedOnly = selectedOnly;
|
|
768
|
+
}
|
|
769
|
+
else if (typeof params.selectedOnly === 'boolean') {
|
|
770
|
+
result.selectedOnly = params.selectedOnly;
|
|
771
|
+
}
|
|
772
|
+
const selectionCount = coerceNumber(interpreted.payload.selectionCount);
|
|
773
|
+
if (typeof selectionCount === 'number') {
|
|
774
|
+
result.selectionCount = selectionCount;
|
|
775
|
+
}
|
|
776
|
+
if (interpreted.warnings?.length) {
|
|
777
|
+
result.warnings = interpreted.warnings;
|
|
778
|
+
}
|
|
779
|
+
if (interpreted.details?.length) {
|
|
780
|
+
result.details = interpreted.details;
|
|
329
781
|
}
|
|
330
|
-
|
|
331
|
-
return { success: true, message: 'Navigation mesh build attempted' };
|
|
782
|
+
return result;
|
|
332
783
|
}
|
|
333
784
|
catch (e) {
|
|
334
|
-
// If Python fails, return error instead of trying console command that crashes
|
|
335
785
|
return {
|
|
336
786
|
success: false,
|
|
337
787
|
error: `Navigation build not available: ${e}. Please ensure a NavMeshBoundsVolume exists in the level.`
|