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,3 +1,4 @@
1
+ import { coerceBoolean, coerceString, interpretStandardResult } from '../utils/result-helpers.js';
1
2
  export class AssetResources {
2
3
  bridge;
3
4
  constructor(bridge) {
@@ -9,10 +10,36 @@ export class AssetResources {
9
10
  makeKey(dir, recursive, page) {
10
11
  return page !== undefined ? `${dir}::${recursive ? 1 : 0}::${page}` : `${dir}::${recursive ? 1 : 0}`;
11
12
  }
13
+ // Normalize UE content paths:
14
+ // - Map '/Content' -> '/Game'
15
+ // - Ensure forward slashes
16
+ normalizeDir(dir) {
17
+ try {
18
+ if (!dir || typeof dir !== 'string')
19
+ return '/Game';
20
+ let d = dir.replace(/\\/g, '/');
21
+ if (!d.startsWith('/'))
22
+ d = '/' + d;
23
+ if (d.toLowerCase().startsWith('/content')) {
24
+ d = '/Game' + d.substring('/Content'.length);
25
+ }
26
+ // Collapse multiple slashes
27
+ d = d.replace(/\/+/g, '/');
28
+ // Remove trailing slash except root
29
+ if (d.length > 1)
30
+ d = d.replace(/\/$/, '');
31
+ return d;
32
+ }
33
+ catch {
34
+ return '/Game';
35
+ }
36
+ }
12
37
  async list(dir = '/Game', _recursive = false, limit = 50) {
13
38
  // ALWAYS use non-recursive listing to show only immediate children
14
39
  // This prevents timeouts and makes navigation clearer
15
40
  _recursive = false; // Force non-recursive
41
+ // Normalize directory first
42
+ dir = this.normalizeDir(dir);
16
43
  // Cache fast-path
17
44
  try {
18
45
  const key = this.makeKey(dir, false);
@@ -45,7 +72,8 @@ export class AssetResources {
45
72
  // Ensure pageSize doesn't exceed safe limit
46
73
  const safePageSize = Math.min(pageSize, 50);
47
74
  const offset = page * safePageSize;
48
- // Check cache for this specific page
75
+ // Normalize directory and check cache for this specific page
76
+ dir = this.normalizeDir(dir);
49
77
  const cacheKey = this.makeKey(dir, recursive, page);
50
78
  const cached = this.cache.get(cacheKey);
51
79
  if (cached && (Date.now() - cached.timestamp) < this.ttlMs) {
@@ -91,8 +119,8 @@ export class AssetResources {
91
119
  };
92
120
  }
93
121
  /**
94
- * Directory-based listing for paths with too many assets
95
- * Shows only immediate children (folders and files) to avoid timeouts
122
+ * Directory-based listing of immediate children using AssetRegistry.
123
+ * Returns both subfolders and assets at the given path.
96
124
  */
97
125
  async listDirectoryOnly(dir, _recursive, limit) {
98
126
  // Always return only immediate children to avoid timeout and improve navigation
@@ -101,100 +129,84 @@ export class AssetResources {
101
129
  import unreal
102
130
  import json
103
131
 
104
- _dir = r"${dir}"
132
+ _dir = r"${this.normalizeDir(dir)}"
105
133
 
106
134
  try:
107
- # ALWAYS non-recursive - get only immediate children
108
- all_paths = unreal.EditorAssetLibrary.list_assets(_dir, False, False)
109
-
110
- # Organize into immediate children only
111
- immediate_folders = set()
112
- immediate_assets = []
113
-
114
- for path in all_paths:
115
- # Remove the base directory to get relative path
116
- relative = path.replace(_dir, '').strip('/')
117
- if not relative:
118
- continue
119
-
120
- # Split to check depth
121
- parts = relative.split('/')
122
-
123
- if len(parts) == 1:
124
- # This is an immediate child asset
125
- immediate_assets.append(path)
126
- elif len(parts) > 1:
127
- # This indicates a subfolder exists
128
- immediate_folders.add(parts[0])
129
-
130
- result = []
131
-
132
- # Add folders first
133
- for folder in sorted(immediate_folders):
134
- result.append({
135
- 'n': folder,
136
- 'p': _dir + '/' + folder,
137
- 'c': 'Folder',
138
- 'isFolder': True
139
- })
140
-
141
- # Add immediate assets (limit to prevent socket issues)
142
- for asset_path in immediate_assets[:min(${limit}, len(immediate_assets))]:
143
- name = asset_path.split('/')[-1].split('.')[0]
144
- result.append({
145
- 'n': name,
146
- 'p': asset_path,
147
- 'c': 'Asset'
148
- })
149
-
150
- # Always showing immediate children only
151
- note = f'Showing immediate children of {_dir} ({len(immediate_folders)} folders, {len(immediate_assets)} files)'
152
-
135
+ ar = unreal.AssetRegistryHelpers.get_asset_registry()
136
+ # Immediate subfolders
137
+ sub_paths = ar.get_sub_paths(_dir, False)
138
+ folders_list = []
139
+ for p in sub_paths:
140
+ try:
141
+ name = p.split('/')[-1]
142
+ folders_list.append({'n': name, 'p': p})
143
+ except Exception:
144
+ pass
145
+
146
+ # Immediate assets at this path
147
+ assets_data = ar.get_assets_by_path(_dir, False)
148
+ assets = []
149
+ for a in assets_data[:${limit}]:
150
+ try:
151
+ assets.append({
152
+ 'n': str(a.asset_name),
153
+ 'p': str(a.object_path),
154
+ 'c': str(a.asset_class)
155
+ })
156
+ except Exception:
157
+ pass
158
+
153
159
  print("RESULT:" + json.dumps({
154
- 'success': True,
155
- 'assets': result,
156
- 'count': len(result),
157
- 'folders': len(immediate_folders),
158
- 'files': len(immediate_assets),
159
- 'note': note
160
+ 'success': True,
161
+ 'path': _dir,
162
+ 'folders': len(folders_list),
163
+ 'files': len(assets),
164
+ 'folders_list': folders_list,
165
+ 'assets': assets
160
166
  }))
161
167
  except Exception as e:
162
- print("RESULT:" + json.dumps({'success': False, 'error': str(e), 'assets': []}))
168
+ print("RESULT:" + json.dumps({'success': False, 'error': str(e), 'path': _dir}))
163
169
  `.trim();
164
170
  const resp = await this.bridge.executePython(py);
165
- let output = '';
166
- if (resp?.LogOutput && Array.isArray(resp.LogOutput)) {
167
- output = resp.LogOutput.map((l) => l.Output || '').join('');
168
- }
169
- else if (typeof resp === 'string') {
170
- output = resp;
171
- }
172
- else {
173
- output = JSON.stringify(resp);
174
- }
175
- const m = output.match(/RESULT:({.*})/);
176
- if (m) {
177
- try {
178
- const parsed = JSON.parse(m[1]);
179
- if (parsed.success) {
180
- // Transform to standard format
181
- const assets = parsed.assets.map((a) => ({
182
- Name: a.n,
183
- Path: a.p,
184
- Class: a.c,
185
- isFolder: a.isFolder || false
186
- }));
187
- return {
188
- assets,
189
- count: parsed.count,
190
- folders: parsed.folders,
191
- files: parsed.files,
192
- note: parsed.note,
193
- method: 'directory_listing'
194
- };
195
- }
196
- }
197
- catch { }
171
+ const interpreted = interpretStandardResult(resp, {
172
+ successMessage: 'Directory contents retrieved',
173
+ failureMessage: 'Failed to list directory contents'
174
+ });
175
+ if (interpreted.success) {
176
+ const payload = interpreted.payload;
177
+ const foldersArr = Array.isArray(payload.folders_list)
178
+ ? payload.folders_list.map((f) => ({
179
+ Name: coerceString(f?.n) ?? '',
180
+ Path: coerceString(f?.p) ?? '',
181
+ Class: 'Folder',
182
+ isFolder: true
183
+ }))
184
+ : [];
185
+ const assetsArr = Array.isArray(payload.assets)
186
+ ? payload.assets.map((a) => ({
187
+ Name: coerceString(a?.n) ?? '',
188
+ Path: coerceString(a?.p) ?? '',
189
+ Class: coerceString(a?.c) ?? 'Asset',
190
+ isFolder: false
191
+ }))
192
+ : [];
193
+ const total = foldersArr.length + assetsArr.length;
194
+ const summary = {
195
+ total,
196
+ folders: foldersArr.length,
197
+ assets: assetsArr.length
198
+ };
199
+ const resolvedPath = coerceString(payload.path) ?? this.normalizeDir(dir);
200
+ return {
201
+ success: true,
202
+ path: resolvedPath,
203
+ summary,
204
+ foldersList: foldersArr,
205
+ assets: assetsArr,
206
+ count: total,
207
+ note: `Immediate children of ${resolvedPath}: ${foldersArr.length} folder(s), ${assetsArr.length} asset(s)`,
208
+ method: 'asset_registry_listing'
209
+ };
198
210
  }
199
211
  }
200
212
  catch (err) {
@@ -202,10 +214,13 @@ except Exception as e:
202
214
  }
203
215
  // Fallback: return empty with explanation
204
216
  return {
217
+ success: true,
218
+ path: this.normalizeDir(dir),
219
+ summary: { total: 0, folders: 0, assets: 0 },
220
+ foldersList: [],
205
221
  assets: [],
206
- warning: 'Directory contains too many assets. Showing immediate children only.',
207
- suggestion: 'Navigate to specific subdirectories for detailed listings.',
208
- method: 'directory_timeout_fallback'
222
+ warning: 'No items at this path or failed to query AssetRegistry.',
223
+ method: 'asset_registry_fallback'
209
224
  };
210
225
  }
211
226
  async find(assetPath) {
@@ -213,9 +228,11 @@ except Exception as e:
213
228
  if (!assetPath || typeof assetPath !== 'string' || assetPath.trim() === '' || assetPath.endsWith('/')) {
214
229
  return false;
215
230
  }
231
+ // Normalize asset path (support users passing /Content/...)
232
+ const ap = this.normalizeDir(assetPath);
216
233
  const py = `
217
234
  import unreal
218
- apath = r"${assetPath}"
235
+ apath = r"${ap}"
219
236
  try:
220
237
  exists = unreal.EditorAssetLibrary.does_asset_exist(apath)
221
238
  print("RESULT:{'success': True, 'exists': %s}" % ('True' if exists else 'False'))
@@ -223,21 +240,12 @@ except Exception as e:
223
240
  print("RESULT:{'success': False, 'error': '" + str(e) + "'}")
224
241
  `.trim();
225
242
  const resp = await this.bridge.executePython(py);
226
- let output = '';
227
- if (resp?.LogOutput && Array.isArray(resp.LogOutput))
228
- output = resp.LogOutput.map((l) => l.Output || '').join('');
229
- else if (typeof resp === 'string')
230
- output = resp;
231
- else
232
- output = JSON.stringify(resp);
233
- const m = output.match(/RESULT:({.*})/);
234
- if (m) {
235
- try {
236
- const parsed = JSON.parse(m[1].replace(/'/g, '"'));
237
- if (parsed.success)
238
- return !!parsed.exists;
239
- }
240
- catch { }
243
+ const interpreted = interpretStandardResult(resp, {
244
+ successMessage: 'Asset existence verified',
245
+ failureMessage: 'Failed to verify asset existence'
246
+ });
247
+ if (interpreted.success) {
248
+ return coerceBoolean(interpreted.payload.exists, false) ?? false;
241
249
  }
242
250
  return false;
243
251
  }
@@ -2,15 +2,33 @@ import { UnrealBridge } from '../unreal-bridge.js';
2
2
  export declare class LevelResources {
3
3
  private bridge;
4
4
  constructor(bridge: UnrealBridge);
5
- getCurrentLevel(): Promise<any>;
6
- getLevelName(): Promise<any>;
5
+ getCurrentLevel(): Promise<{
6
+ success: boolean;
7
+ name: string;
8
+ path: string;
9
+ error?: undefined;
10
+ } | {
11
+ success: boolean;
12
+ error: string;
13
+ name?: undefined;
14
+ path?: undefined;
15
+ }>;
16
+ getLevelName(): Promise<{
17
+ success: boolean;
18
+ path: string;
19
+ error?: undefined;
20
+ } | {
21
+ success: boolean;
22
+ error: string;
23
+ path?: undefined;
24
+ }>;
7
25
  saveCurrentLevel(): Promise<{
8
26
  success: boolean;
9
27
  message: string;
10
28
  error?: undefined;
11
29
  } | {
12
- error: string;
13
30
  success: boolean;
31
+ error: string;
14
32
  message?: undefined;
15
33
  }>;
16
34
  }
@@ -1,3 +1,4 @@
1
+ import { coerceString, interpretStandardResult } from '../utils/result-helpers.js';
1
2
  export class LevelResources {
2
3
  bridge;
3
4
  constructor(bridge) {
@@ -8,25 +9,18 @@ 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 = 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) => log.Output || '').join('');
12
+ const interpreted = interpretStandardResult(resp, {
13
+ successMessage: 'Retrieved current level',
14
+ failureMessage: 'Failed to get current level'
15
+ });
16
+ if (interpreted.success) {
17
+ return {
18
+ success: true,
19
+ name: coerceString(interpreted.payload.name) ?? coerceString(interpreted.payload.level_name) ?? 'None',
20
+ path: coerceString(interpreted.payload.path) ?? 'None'
21
+ };
15
22
  }
16
- else if (typeof resp === 'string') {
17
- out = resp;
18
- }
19
- else {
20
- out = JSON.stringify(resp);
21
- }
22
- const m = out.match(/RESULT:({.*})/);
23
- if (m) {
24
- const parsed = JSON.parse(m[1]);
25
- if (parsed.success)
26
- return parsed;
27
- }
28
- // If Python failed, return error
29
- return { error: 'Failed to get current level', success: false };
23
+ return { success: false, error: interpreted.error ?? interpreted.message };
30
24
  }
31
25
  catch (err) {
32
26
  return { error: `Failed to get current level: ${err}`, success: false };
@@ -37,54 +31,35 @@ export class LevelResources {
37
31
  try {
38
32
  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();
39
33
  const resp = await this.bridge.executePython(py);
40
- // Handle LogOutput format from executePython
41
- let out = '';
42
- if (resp?.LogOutput && Array.isArray(resp.LogOutput)) {
43
- out = resp.LogOutput.map((log) => log.Output || '').join('');
44
- }
45
- else if (typeof resp === 'string') {
46
- out = resp;
47
- }
48
- else {
49
- out = JSON.stringify(resp);
34
+ const interpreted = interpretStandardResult(resp, {
35
+ successMessage: 'Retrieved level path',
36
+ failureMessage: 'Failed to get level name'
37
+ });
38
+ if (interpreted.success) {
39
+ return {
40
+ success: true,
41
+ path: coerceString(interpreted.payload.path) ?? ''
42
+ };
50
43
  }
51
- const m = out.match(/RESULT:({.*})/);
52
- if (m) {
53
- const parsed = JSON.parse(m[1]);
54
- if (parsed.success)
55
- return parsed;
56
- }
57
- // If Python failed, return error
58
- return { error: 'Failed to get level name', success: false };
44
+ return { success: false, error: interpreted.error ?? interpreted.message };
59
45
  }
60
46
  catch (err) {
61
47
  return { error: `Failed to get level name: ${err}`, success: false };
62
48
  }
63
49
  }
64
50
  async saveCurrentLevel() {
65
- // Prefer Python save (or LevelEditorSubsystem) then fallback
51
+ // Strict modern API: require LevelEditorSubsystem
66
52
  try {
67
- 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();
53
+ 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();
68
54
  const resp = await this.bridge.executePython(py);
69
- // Handle LogOutput format from executePython
70
- let out = '';
71
- if (resp?.LogOutput && Array.isArray(resp.LogOutput)) {
72
- out = resp.LogOutput.map((log) => log.Output || '').join('');
73
- }
74
- else if (typeof resp === 'string') {
75
- out = resp;
76
- }
77
- else {
78
- out = JSON.stringify(resp);
79
- }
80
- const m = out.match(/RESULT:({.*})/);
81
- if (m) {
82
- const parsed = JSON.parse(m[1]);
83
- if (parsed.success)
84
- return { success: true, message: 'Level saved' };
55
+ const interpreted = interpretStandardResult(resp, {
56
+ successMessage: 'Level saved',
57
+ failureMessage: 'Failed to save level'
58
+ });
59
+ if (interpreted.success) {
60
+ return { success: true, message: interpreted.message };
85
61
  }
86
- // If Python failed, return error
87
- return { error: 'Failed to save level', success: false };
62
+ return { success: false, error: interpreted.error ?? interpreted.message };
88
63
  }
89
64
  catch (err) {
90
65
  return { error: `Failed to save level: ${err}`, success: false };
@@ -14,20 +14,8 @@ export declare class ActorTools {
14
14
  yaw: number;
15
15
  roll: number;
16
16
  };
17
- }): Promise<any>;
18
- spawnViaPython(params: {
19
- classPath: string;
20
- location?: {
21
- x: number;
22
- y: number;
23
- z: number;
24
- };
25
- rotation?: {
26
- pitch: number;
27
- yaw: number;
28
- roll: number;
29
- };
30
- }): Promise<any>;
17
+ actorName?: string;
18
+ }): Promise<Record<string, unknown>>;
31
19
  spawnViaConsole(params: {
32
20
  classPath: string;
33
21
  location?: {
@@ -45,6 +33,7 @@ export declare class ActorTools {
45
33
  message: string;
46
34
  note: string;
47
35
  }>;
36
+ private getPythonSpawnHelper;
48
37
  private resolveActorClass;
49
38
  private getConsoleClassName;
50
39
  }