unreal-engine-mcp-server 0.3.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/.env.production +1 -1
  2. package/.github/copilot-instructions.md +45 -0
  3. package/.github/workflows/publish-mcp.yml +1 -1
  4. package/README.md +22 -7
  5. package/dist/index.js +137 -46
  6. package/dist/prompts/index.d.ts +10 -3
  7. package/dist/prompts/index.js +186 -7
  8. package/dist/resources/actors.d.ts +19 -1
  9. package/dist/resources/actors.js +55 -64
  10. package/dist/resources/assets.d.ts +3 -2
  11. package/dist/resources/assets.js +117 -109
  12. package/dist/resources/levels.d.ts +21 -3
  13. package/dist/resources/levels.js +31 -56
  14. package/dist/tools/actors.d.ts +3 -14
  15. package/dist/tools/actors.js +246 -302
  16. package/dist/tools/animation.d.ts +57 -102
  17. package/dist/tools/animation.js +429 -450
  18. package/dist/tools/assets.d.ts +13 -2
  19. package/dist/tools/assets.js +58 -46
  20. package/dist/tools/audio.d.ts +22 -13
  21. package/dist/tools/audio.js +467 -121
  22. package/dist/tools/blueprint.d.ts +32 -13
  23. package/dist/tools/blueprint.js +699 -448
  24. package/dist/tools/build_environment_advanced.d.ts +0 -1
  25. package/dist/tools/build_environment_advanced.js +236 -87
  26. package/dist/tools/consolidated-tool-definitions.d.ts +232 -15
  27. package/dist/tools/consolidated-tool-definitions.js +124 -255
  28. package/dist/tools/consolidated-tool-handlers.js +749 -766
  29. package/dist/tools/debug.d.ts +72 -10
  30. package/dist/tools/debug.js +170 -36
  31. package/dist/tools/editor.d.ts +9 -2
  32. package/dist/tools/editor.js +30 -44
  33. package/dist/tools/foliage.d.ts +34 -15
  34. package/dist/tools/foliage.js +97 -107
  35. package/dist/tools/introspection.js +19 -21
  36. package/dist/tools/landscape.d.ts +1 -2
  37. package/dist/tools/landscape.js +311 -168
  38. package/dist/tools/level.d.ts +3 -28
  39. package/dist/tools/level.js +642 -192
  40. package/dist/tools/lighting.d.ts +14 -3
  41. package/dist/tools/lighting.js +236 -123
  42. package/dist/tools/materials.d.ts +25 -7
  43. package/dist/tools/materials.js +102 -79
  44. package/dist/tools/niagara.d.ts +10 -12
  45. package/dist/tools/niagara.js +74 -94
  46. package/dist/tools/performance.d.ts +12 -4
  47. package/dist/tools/performance.js +38 -79
  48. package/dist/tools/physics.d.ts +34 -10
  49. package/dist/tools/physics.js +364 -292
  50. package/dist/tools/rc.js +98 -24
  51. package/dist/tools/sequence.d.ts +1 -0
  52. package/dist/tools/sequence.js +146 -24
  53. package/dist/tools/ui.d.ts +31 -4
  54. package/dist/tools/ui.js +83 -66
  55. package/dist/tools/visual.d.ts +11 -0
  56. package/dist/tools/visual.js +245 -30
  57. package/dist/types/tool-types.d.ts +0 -6
  58. package/dist/types/tool-types.js +1 -8
  59. package/dist/unreal-bridge.d.ts +32 -2
  60. package/dist/unreal-bridge.js +621 -127
  61. package/dist/utils/elicitation.d.ts +57 -0
  62. package/dist/utils/elicitation.js +104 -0
  63. package/dist/utils/error-handler.d.ts +0 -33
  64. package/dist/utils/error-handler.js +4 -111
  65. package/dist/utils/http.d.ts +2 -22
  66. package/dist/utils/http.js +12 -75
  67. package/dist/utils/normalize.d.ts +4 -4
  68. package/dist/utils/normalize.js +15 -7
  69. package/dist/utils/python-output.d.ts +18 -0
  70. package/dist/utils/python-output.js +290 -0
  71. package/dist/utils/python.d.ts +2 -0
  72. package/dist/utils/python.js +4 -0
  73. package/dist/utils/response-validator.d.ts +6 -1
  74. package/dist/utils/response-validator.js +66 -13
  75. package/dist/utils/result-helpers.d.ts +27 -0
  76. package/dist/utils/result-helpers.js +147 -0
  77. package/dist/utils/safe-json.d.ts +0 -2
  78. package/dist/utils/safe-json.js +0 -43
  79. package/dist/utils/validation.d.ts +16 -0
  80. package/dist/utils/validation.js +70 -7
  81. package/mcp-config-example.json +2 -2
  82. package/package.json +11 -10
  83. package/server.json +37 -14
  84. package/src/index.ts +146 -50
  85. package/src/prompts/index.ts +211 -13
  86. package/src/resources/actors.ts +59 -44
  87. package/src/resources/assets.ts +123 -102
  88. package/src/resources/levels.ts +37 -47
  89. package/src/tools/actors.ts +269 -313
  90. package/src/tools/animation.ts +556 -539
  91. package/src/tools/assets.ts +59 -45
  92. package/src/tools/audio.ts +507 -113
  93. package/src/tools/blueprint.ts +778 -462
  94. package/src/tools/build_environment_advanced.ts +312 -106
  95. package/src/tools/consolidated-tool-definitions.ts +136 -267
  96. package/src/tools/consolidated-tool-handlers.ts +871 -795
  97. package/src/tools/debug.ts +179 -38
  98. package/src/tools/editor.ts +35 -37
  99. package/src/tools/foliage.ts +110 -104
  100. package/src/tools/introspection.ts +24 -22
  101. package/src/tools/landscape.ts +334 -181
  102. package/src/tools/level.ts +683 -182
  103. package/src/tools/lighting.ts +244 -123
  104. package/src/tools/materials.ts +114 -83
  105. package/src/tools/niagara.ts +87 -81
  106. package/src/tools/performance.ts +49 -88
  107. package/src/tools/physics.ts +393 -299
  108. package/src/tools/rc.ts +103 -25
  109. package/src/tools/sequence.ts +157 -30
  110. package/src/tools/ui.ts +101 -70
  111. package/src/tools/visual.ts +250 -29
  112. package/src/types/tool-types.ts +0 -9
  113. package/src/unreal-bridge.ts +658 -140
  114. package/src/utils/elicitation.ts +129 -0
  115. package/src/utils/error-handler.ts +4 -159
  116. package/src/utils/http.ts +16 -115
  117. package/src/utils/normalize.ts +20 -10
  118. package/src/utils/python-output.ts +351 -0
  119. package/src/utils/python.ts +3 -0
  120. package/src/utils/response-validator.ts +68 -17
  121. package/src/utils/result-helpers.ts +193 -0
  122. package/src/utils/safe-json.ts +0 -50
  123. package/src/utils/validation.ts +94 -7
  124. package/tests/run-unreal-tool-tests.mjs +720 -0
  125. package/tsconfig.json +2 -2
  126. package/dist/python-utils.d.ts +0 -29
  127. package/dist/python-utils.js +0 -54
  128. package/dist/tools/tool-definitions.d.ts +0 -4919
  129. package/dist/tools/tool-definitions.js +0 -1065
  130. package/dist/tools/tool-handlers.d.ts +0 -47
  131. package/dist/tools/tool-handlers.js +0 -863
  132. package/dist/types/index.d.ts +0 -323
  133. package/dist/types/index.js +0 -28
  134. package/dist/utils/cache-manager.d.ts +0 -64
  135. package/dist/utils/cache-manager.js +0 -176
  136. package/dist/utils/errors.d.ts +0 -133
  137. package/dist/utils/errors.js +0 -256
  138. package/src/python/editor_compat.py +0 -181
  139. package/src/python-utils.ts +0 -57
  140. package/src/tools/tool-definitions.ts +0 -1081
  141. package/src/tools/tool-handlers.ts +0 -973
  142. package/src/types/index.ts +0 -414
  143. package/src/utils/cache-manager.ts +0 -213
  144. package/src/utils/errors.ts +0 -312
@@ -1,4 +1,5 @@
1
1
  import { UnrealBridge } from '../unreal-bridge.js';
2
+ import { coerceBoolean, coerceString, interpretStandardResult } from '../utils/result-helpers.js';
2
3
 
3
4
  export class AssetResources {
4
5
  constructor(private bridge: UnrealBridge) {}
@@ -10,11 +11,35 @@ export class AssetResources {
10
11
  return page !== undefined ? `${dir}::${recursive ? 1 : 0}::${page}` : `${dir}::${recursive ? 1 : 0}`;
11
12
  }
12
13
 
14
+ // Normalize UE content paths:
15
+ // - Map '/Content' -> '/Game'
16
+ // - Ensure forward slashes
17
+ private normalizeDir(dir: string): string {
18
+ try {
19
+ if (!dir || typeof dir !== 'string') return '/Game';
20
+ let d = dir.replace(/\\/g, '/');
21
+ if (!d.startsWith('/')) d = '/' + d;
22
+ if (d.toLowerCase().startsWith('/content')) {
23
+ d = '/Game' + d.substring('/Content'.length);
24
+ }
25
+ // Collapse multiple slashes
26
+ d = d.replace(/\/+/g, '/');
27
+ // Remove trailing slash except root
28
+ if (d.length > 1) d = d.replace(/\/$/, '');
29
+ return d;
30
+ } catch {
31
+ return '/Game';
32
+ }
33
+ }
34
+
13
35
  async list(dir = '/Game', _recursive = false, limit = 50) {
14
36
  // ALWAYS use non-recursive listing to show only immediate children
15
37
  // This prevents timeouts and makes navigation clearer
16
38
  _recursive = false; // Force non-recursive
17
39
 
40
+ // Normalize directory first
41
+ dir = this.normalizeDir(dir);
42
+
18
43
  // Cache fast-path
19
44
  try {
20
45
  const key = this.makeKey(dir, false);
@@ -50,7 +75,8 @@ export class AssetResources {
50
75
  const safePageSize = Math.min(pageSize, 50);
51
76
  const offset = page * safePageSize;
52
77
 
53
- // Check cache for this specific page
78
+ // Normalize directory and check cache for this specific page
79
+ dir = this.normalizeDir(dir);
54
80
  const cacheKey = this.makeKey(dir, recursive, page);
55
81
  const cached = this.cache.get(cacheKey);
56
82
  if (cached && (Date.now() - cached.timestamp) < this.ttlMs) {
@@ -102,8 +128,8 @@ export class AssetResources {
102
128
  }
103
129
 
104
130
  /**
105
- * Directory-based listing for paths with too many assets
106
- * Shows only immediate children (folders and files) to avoid timeouts
131
+ * Directory-based listing of immediate children using AssetRegistry.
132
+ * Returns both subfolders and assets at the given path.
107
133
  */
108
134
  private async listDirectoryOnly(dir: string, _recursive: boolean, limit: number) {
109
135
  // Always return only immediate children to avoid timeout and improve navigation
@@ -112,100 +138,91 @@ export class AssetResources {
112
138
  import unreal
113
139
  import json
114
140
 
115
- _dir = r"${dir}"
141
+ _dir = r"${this.normalizeDir(dir)}"
116
142
 
117
143
  try:
118
- # ALWAYS non-recursive - get only immediate children
119
- all_paths = unreal.EditorAssetLibrary.list_assets(_dir, False, False)
120
-
121
- # Organize into immediate children only
122
- immediate_folders = set()
123
- immediate_assets = []
124
-
125
- for path in all_paths:
126
- # Remove the base directory to get relative path
127
- relative = path.replace(_dir, '').strip('/')
128
- if not relative:
129
- continue
130
-
131
- # Split to check depth
132
- parts = relative.split('/')
133
-
134
- if len(parts) == 1:
135
- # This is an immediate child asset
136
- immediate_assets.append(path)
137
- elif len(parts) > 1:
138
- # This indicates a subfolder exists
139
- immediate_folders.add(parts[0])
140
-
141
- result = []
142
-
143
- # Add folders first
144
- for folder in sorted(immediate_folders):
145
- result.append({
146
- 'n': folder,
147
- 'p': _dir + '/' + folder,
148
- 'c': 'Folder',
149
- 'isFolder': True
150
- })
151
-
152
- # Add immediate assets (limit to prevent socket issues)
153
- for asset_path in immediate_assets[:min(${limit}, len(immediate_assets))]:
154
- name = asset_path.split('/')[-1].split('.')[0]
155
- result.append({
156
- 'n': name,
157
- 'p': asset_path,
158
- 'c': 'Asset'
159
- })
160
-
161
- # Always showing immediate children only
162
- note = f'Showing immediate children of {_dir} ({len(immediate_folders)} folders, {len(immediate_assets)} files)'
163
-
144
+ ar = unreal.AssetRegistryHelpers.get_asset_registry()
145
+ # Immediate subfolders
146
+ sub_paths = ar.get_sub_paths(_dir, False)
147
+ folders_list = []
148
+ for p in sub_paths:
149
+ try:
150
+ name = p.split('/')[-1]
151
+ folders_list.append({'n': name, 'p': p})
152
+ except Exception:
153
+ pass
154
+
155
+ # Immediate assets at this path
156
+ assets_data = ar.get_assets_by_path(_dir, False)
157
+ assets = []
158
+ for a in assets_data[:${limit}]:
159
+ try:
160
+ assets.append({
161
+ 'n': str(a.asset_name),
162
+ 'p': str(a.object_path),
163
+ 'c': str(a.asset_class)
164
+ })
165
+ except Exception:
166
+ pass
167
+
164
168
  print("RESULT:" + json.dumps({
165
- 'success': True,
166
- 'assets': result,
167
- 'count': len(result),
168
- 'folders': len(immediate_folders),
169
- 'files': len(immediate_assets),
170
- 'note': note
169
+ 'success': True,
170
+ 'path': _dir,
171
+ 'folders': len(folders_list),
172
+ 'files': len(assets),
173
+ 'folders_list': folders_list,
174
+ 'assets': assets
171
175
  }))
172
176
  except Exception as e:
173
- print("RESULT:" + json.dumps({'success': False, 'error': str(e), 'assets': []}))
177
+ print("RESULT:" + json.dumps({'success': False, 'error': str(e), 'path': _dir}))
174
178
  `.trim();
175
179
 
176
180
  const resp = await this.bridge.executePython(py);
177
- let output = '';
178
- if (resp?.LogOutput && Array.isArray(resp.LogOutput)) {
179
- output = resp.LogOutput.map((l: any) => l.Output || '').join('');
180
- } else if (typeof resp === 'string') {
181
- output = resp;
182
- } else {
183
- output = JSON.stringify(resp);
184
- }
185
-
186
- const m = output.match(/RESULT:({.*})/);
187
- if (m) {
188
- try {
189
- const parsed = JSON.parse(m[1]);
190
- if (parsed.success) {
191
- // Transform to standard format
192
- const assets = parsed.assets.map((a: any) => ({
193
- Name: a.n,
194
- Path: a.p,
195
- Class: a.c,
196
- isFolder: a.isFolder || false
197
- }));
198
-
199
- return {
200
- assets,
201
- count: parsed.count,
202
- folders: parsed.folders,
203
- files: parsed.files,
204
- note: parsed.note,
205
- method: 'directory_listing'
206
- };
207
- }
208
- } catch {}
181
+ const interpreted = interpretStandardResult(resp, {
182
+ successMessage: 'Directory contents retrieved',
183
+ failureMessage: 'Failed to list directory contents'
184
+ });
185
+
186
+ if (interpreted.success) {
187
+ const payload = interpreted.payload as Record<string, unknown>;
188
+
189
+ const foldersArr = Array.isArray(payload.folders_list)
190
+ ? payload.folders_list.map((f: any) => ({
191
+ Name: coerceString(f?.n) ?? '',
192
+ Path: coerceString(f?.p) ?? '',
193
+ Class: 'Folder',
194
+ isFolder: true
195
+ }))
196
+ : [];
197
+
198
+ const assetsArr = Array.isArray(payload.assets)
199
+ ? payload.assets.map((a: any) => ({
200
+ Name: coerceString(a?.n) ?? '',
201
+ Path: coerceString(a?.p) ?? '',
202
+ Class: coerceString(a?.c) ?? 'Asset',
203
+ isFolder: false
204
+ }))
205
+ : [];
206
+
207
+ const total = foldersArr.length + assetsArr.length;
208
+ const summary = {
209
+ total,
210
+ folders: foldersArr.length,
211
+ assets: assetsArr.length
212
+ };
213
+
214
+ const resolvedPath = coerceString(payload.path) ?? this.normalizeDir(dir);
215
+
216
+ return {
217
+ success: true,
218
+ path: resolvedPath,
219
+ summary,
220
+ foldersList: foldersArr,
221
+ assets: assetsArr,
222
+ count: total,
223
+ note: `Immediate children of ${resolvedPath}: ${foldersArr.length} folder(s), ${assetsArr.length} asset(s)`,
224
+ method: 'asset_registry_listing'
225
+ };
209
226
  }
210
227
  } catch (err: any) {
211
228
  console.warn('Engine asset listing failed:', err.message);
@@ -213,10 +230,13 @@ except Exception as e:
213
230
 
214
231
  // Fallback: return empty with explanation
215
232
  return {
233
+ success: true,
234
+ path: this.normalizeDir(dir),
235
+ summary: { total: 0, folders: 0, assets: 0 },
236
+ foldersList: [],
216
237
  assets: [],
217
- warning: 'Directory contains too many assets. Showing immediate children only.',
218
- suggestion: 'Navigate to specific subdirectories for detailed listings.',
219
- method: 'directory_timeout_fallback'
238
+ warning: 'No items at this path or failed to query AssetRegistry.',
239
+ method: 'asset_registry_fallback'
220
240
  };
221
241
  }
222
242
 
@@ -226,9 +246,11 @@ except Exception as e:
226
246
  return false;
227
247
  }
228
248
 
249
+ // Normalize asset path (support users passing /Content/...)
250
+ const ap = this.normalizeDir(assetPath);
229
251
  const py = `
230
252
  import unreal
231
- apath = r"${assetPath}"
253
+ apath = r"${ap}"
232
254
  try:
233
255
  exists = unreal.EditorAssetLibrary.does_asset_exist(apath)
234
256
  print("RESULT:{'success': True, 'exists': %s}" % ('True' if exists else 'False'))
@@ -236,16 +258,15 @@ except Exception as e:
236
258
  print("RESULT:{'success': False, 'error': '" + str(e) + "'}")
237
259
  `.trim();
238
260
  const resp = await this.bridge.executePython(py);
239
- let output = '';
240
- if (resp?.LogOutput && Array.isArray(resp.LogOutput)) output = resp.LogOutput.map((l: any) => l.Output || '').join('');
241
- else if (typeof resp === 'string') output = resp; else output = JSON.stringify(resp);
242
- const m = output.match(/RESULT:({.*})/);
243
- if (m) {
244
- try {
245
- const parsed = JSON.parse(m[1].replace(/'/g, '"'));
246
- if (parsed.success) return !!parsed.exists;
247
- } catch {}
261
+ const interpreted = interpretStandardResult(resp, {
262
+ successMessage: 'Asset existence verified',
263
+ failureMessage: 'Failed to verify asset existence'
264
+ });
265
+
266
+ if (interpreted.success) {
267
+ return coerceBoolean(interpreted.payload.exists, false) ?? false;
248
268
  }
269
+
249
270
  return false;
250
271
  }
251
272
  }
@@ -1,4 +1,5 @@
1
1
  import { UnrealBridge } from '../unreal-bridge.js';
2
+ import { coerceString, interpretStandardResult } from '../utils/result-helpers.js';
2
3
 
3
4
  export class LevelResources {
4
5
  constructor(private bridge: UnrealBridge) {}
@@ -8,22 +9,20 @@ export class LevelResources {
8
9
  try {
9
10
  const py = '\nimport unreal, json\ntry:\n # Use UnrealEditorSubsystem instead of deprecated EditorLevelLibrary\n editor_subsys = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)\n world = editor_subsys.get_editor_world()\n name = world.get_name() if world else \'None\'\n path = world.get_path_name() if world else \'None\'\n print(\'RESULT:\' + json.dumps({\'success\': True, \'name\': name, \'path\': path}))\nexcept Exception as e:\n print(\'RESULT:\' + json.dumps({\'success\': False, \'error\': str(e)}))\n'.trim();
10
11
  const resp: any = await this.bridge.executePython(py);
11
- // Handle LogOutput format from executePython
12
- let out = '';
13
- if (resp?.LogOutput && Array.isArray(resp.LogOutput)) {
14
- out = resp.LogOutput.map((log: any) => log.Output || '').join('');
15
- } else if (typeof resp === 'string') {
16
- out = resp;
17
- } else {
18
- out = JSON.stringify(resp);
19
- }
20
- const m = out.match(/RESULT:({.*})/);
21
- if (m) {
22
- const parsed = JSON.parse(m[1]);
23
- if (parsed.success) return parsed;
12
+ const interpreted = interpretStandardResult(resp, {
13
+ successMessage: 'Retrieved current level',
14
+ failureMessage: 'Failed to get current level'
15
+ });
16
+
17
+ if (interpreted.success) {
18
+ return {
19
+ success: true,
20
+ name: coerceString(interpreted.payload.name) ?? coerceString(interpreted.payload.level_name) ?? 'None',
21
+ path: coerceString(interpreted.payload.path) ?? 'None'
22
+ };
24
23
  }
25
- // If Python failed, return error
26
- return { error: 'Failed to get current level', success: false };
24
+
25
+ return { success: false, error: interpreted.error ?? interpreted.message };
27
26
  } catch (err) {
28
27
  return { error: `Failed to get current level: ${err}`, success: false };
29
28
  }
@@ -34,48 +33,39 @@ export class LevelResources {
34
33
  try {
35
34
  const py = '\nimport unreal, json\ntry:\n # Use UnrealEditorSubsystem instead of deprecated EditorLevelLibrary\n editor_subsys = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)\n world = editor_subsys.get_editor_world()\n path = world.get_path_name() if world else \'\'\n print(\'RESULT:\' + json.dumps({\'success\': True, \'path\': path}))\nexcept Exception as e:\n print(\'RESULT:\' + json.dumps({\'success\': False, \'error\': str(e)}))\n'.trim();
36
35
  const resp: any = await this.bridge.executePython(py);
37
- // Handle LogOutput format from executePython
38
- let out = '';
39
- if (resp?.LogOutput && Array.isArray(resp.LogOutput)) {
40
- out = resp.LogOutput.map((log: any) => log.Output || '').join('');
41
- } else if (typeof resp === 'string') {
42
- out = resp;
43
- } else {
44
- out = JSON.stringify(resp);
45
- }
46
- const m = out.match(/RESULT:({.*})/);
47
- if (m) {
48
- const parsed = JSON.parse(m[1]);
49
- if (parsed.success) return parsed;
36
+ const interpreted = interpretStandardResult(resp, {
37
+ successMessage: 'Retrieved level path',
38
+ failureMessage: 'Failed to get level name'
39
+ });
40
+
41
+ if (interpreted.success) {
42
+ return {
43
+ success: true,
44
+ path: coerceString(interpreted.payload.path) ?? ''
45
+ };
50
46
  }
51
- // If Python failed, return error
52
- return { error: 'Failed to get level name', success: false };
47
+
48
+ return { success: false, error: interpreted.error ?? interpreted.message };
53
49
  } catch (err) {
54
50
  return { error: `Failed to get level name: ${err}`, success: false };
55
51
  }
56
52
  }
57
53
 
58
54
  async saveCurrentLevel() {
59
- // Prefer Python save (or LevelEditorSubsystem) then fallback
55
+ // Strict modern API: require LevelEditorSubsystem
60
56
  try {
61
- const py = '\nimport unreal, json\ntry:\n les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)\n if les: les.save_current_level()\n else: unreal.EditorLevelLibrary.save_current_level()\n print(\'RESULT:\' + json.dumps({\'success\': True}))\nexcept Exception as e:\n print(\'RESULT:\' + json.dumps({\'success\': False, \'error\': str(e)}))\n'.trim();
57
+ const py = '\nimport unreal, json\ntry:\n les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)\n if not les:\n print(\'RESULT:\' + json.dumps({\'success\': False, \'error\': \'LevelEditorSubsystem not available\'}))\n else:\n les.save_current_level()\n print(\'RESULT:\' + json.dumps({\'success\': True}))\nexcept Exception as e:\n print(\'RESULT:\' + json.dumps({\'success\': False, \'error\': str(e)}))\n'.trim();
62
58
  const resp: any = await this.bridge.executePython(py);
63
- // Handle LogOutput format from executePython
64
- let out = '';
65
- if (resp?.LogOutput && Array.isArray(resp.LogOutput)) {
66
- out = resp.LogOutput.map((log: any) => log.Output || '').join('');
67
- } else if (typeof resp === 'string') {
68
- out = resp;
69
- } else {
70
- out = JSON.stringify(resp);
71
- }
72
- const m = out.match(/RESULT:({.*})/);
73
- if (m) {
74
- const parsed = JSON.parse(m[1]);
75
- if (parsed.success) return { success: true, message: 'Level saved' };
59
+ const interpreted = interpretStandardResult(resp, {
60
+ successMessage: 'Level saved',
61
+ failureMessage: 'Failed to save level'
62
+ });
63
+
64
+ if (interpreted.success) {
65
+ return { success: true, message: interpreted.message };
76
66
  }
77
- // If Python failed, return error
78
- return { error: 'Failed to save level', success: false };
67
+
68
+ return { success: false, error: interpreted.error ?? interpreted.message };
79
69
  } catch (err) {
80
70
  return { error: `Failed to save level: ${err}`, success: false };
81
71
  }