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.
- package/.env.production +1 -1
- package/.github/copilot-instructions.md +45 -0
- package/.github/workflows/publish-mcp.yml +1 -1
- package/README.md +22 -7
- package/dist/index.js +137 -46
- 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.d.ts +3 -2
- package/dist/resources/assets.js +117 -109
- package/dist/resources/levels.d.ts +21 -3
- package/dist/resources/levels.js +31 -56
- 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 +58 -46
- 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 +236 -87
- package/dist/tools/consolidated-tool-definitions.d.ts +232 -15
- package/dist/tools/consolidated-tool-definitions.js +124 -255
- package/dist/tools/consolidated-tool-handlers.js +749 -766
- package/dist/tools/debug.d.ts +72 -10
- package/dist/tools/debug.js +170 -36
- 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 +98 -24
- package/dist/tools/sequence.d.ts +1 -0
- package/dist/tools/sequence.js +146 -24
- 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.d.ts +6 -1
- package/dist/utils/response-validator.js +66 -13
- 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 +11 -10
- package/server.json +37 -14
- package/src/index.ts +146 -50
- package/src/prompts/index.ts +211 -13
- package/src/resources/actors.ts +59 -44
- package/src/resources/assets.ts +123 -102
- package/src/resources/levels.ts +37 -47
- package/src/tools/actors.ts +269 -313
- package/src/tools/animation.ts +556 -539
- package/src/tools/assets.ts +59 -45
- package/src/tools/audio.ts +507 -113
- package/src/tools/blueprint.ts +778 -462
- package/src/tools/build_environment_advanced.ts +312 -106
- package/src/tools/consolidated-tool-definitions.ts +136 -267
- package/src/tools/consolidated-tool-handlers.ts +871 -795
- package/src/tools/debug.ts +179 -38
- 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 +103 -25
- package/src/tools/sequence.ts +157 -30
- 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 +68 -17
- 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/tools/tool-definitions.d.ts +0 -4919
- package/dist/tools/tool-definitions.js +0 -1065
- package/dist/tools/tool-handlers.d.ts +0 -47
- package/dist/tools/tool-handlers.js +0 -863
- 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/tools/tool-definitions.ts +0 -1081
- package/src/tools/tool-handlers.ts +0 -973
- 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/ui.ts
CHANGED
|
@@ -1,22 +1,10 @@
|
|
|
1
1
|
// UI tools for Unreal Engine
|
|
2
2
|
import { UnrealBridge } from '../unreal-bridge.js';
|
|
3
|
+
import { bestEffortInterpretedText, interpretStandardResult } from '../utils/result-helpers.js';
|
|
3
4
|
|
|
4
5
|
export class UITools {
|
|
5
6
|
constructor(private bridge: UnrealBridge) {}
|
|
6
7
|
|
|
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
|
-
Command: command,
|
|
14
|
-
SpecificPlayer: null
|
|
15
|
-
},
|
|
16
|
-
generateTransaction: false
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
|
|
20
8
|
// Create widget blueprint
|
|
21
9
|
async createWidget(params: {
|
|
22
10
|
name: string;
|
|
@@ -57,10 +45,20 @@ except Exception as e:
|
|
|
57
45
|
`.trim();
|
|
58
46
|
try {
|
|
59
47
|
const resp = await this.bridge.executePython(py);
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
48
|
+
const interpreted = interpretStandardResult(resp, {
|
|
49
|
+
successMessage: 'Widget blueprint created',
|
|
50
|
+
failureMessage: 'Failed to create WidgetBlueprint'
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (interpreted.success) {
|
|
54
|
+
return { success: true, message: interpreted.message };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
success: false,
|
|
59
|
+
error: interpreted.error ?? 'Failed to create widget blueprint',
|
|
60
|
+
details: bestEffortInterpretedText(interpreted)
|
|
61
|
+
};
|
|
64
62
|
} catch (e) {
|
|
65
63
|
return { success: false, error: `Failed to create widget blueprint: ${e}` };
|
|
66
64
|
}
|
|
@@ -78,7 +76,7 @@ except Exception as e:
|
|
|
78
76
|
alignment?: [number, number];
|
|
79
77
|
};
|
|
80
78
|
}) {
|
|
81
|
-
|
|
79
|
+
const commands: string[] = [];
|
|
82
80
|
|
|
83
81
|
commands.push(`AddWidgetComponent ${params.widgetName} ${params.componentType} ${params.componentName}`);
|
|
84
82
|
|
|
@@ -97,9 +95,7 @@ except Exception as e:
|
|
|
97
95
|
}
|
|
98
96
|
}
|
|
99
97
|
|
|
100
|
-
|
|
101
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
102
|
-
}
|
|
98
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
103
99
|
|
|
104
100
|
return { success: true, message: `Component ${params.componentName} added to widget` };
|
|
105
101
|
}
|
|
@@ -113,7 +109,7 @@ except Exception as e:
|
|
|
113
109
|
color?: [number, number, number, number];
|
|
114
110
|
fontFamily?: string;
|
|
115
111
|
}) {
|
|
116
|
-
|
|
112
|
+
const commands: string[] = [];
|
|
117
113
|
|
|
118
114
|
commands.push(`SetWidgetText ${params.widgetName}.${params.componentName} "${params.text}"`);
|
|
119
115
|
|
|
@@ -129,9 +125,7 @@ except Exception as e:
|
|
|
129
125
|
commands.push(`SetWidgetFont ${params.widgetName}.${params.componentName} ${params.fontFamily}`);
|
|
130
126
|
}
|
|
131
127
|
|
|
132
|
-
|
|
133
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
134
|
-
}
|
|
128
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
135
129
|
|
|
136
130
|
return { success: true, message: 'Widget text updated' };
|
|
137
131
|
}
|
|
@@ -144,7 +138,7 @@ except Exception as e:
|
|
|
144
138
|
tint?: [number, number, number, number];
|
|
145
139
|
sizeToContent?: boolean;
|
|
146
140
|
}) {
|
|
147
|
-
|
|
141
|
+
const commands: string[] = [];
|
|
148
142
|
|
|
149
143
|
commands.push(`SetWidgetImage ${params.widgetName}.${params.componentName} ${params.imagePath}`);
|
|
150
144
|
|
|
@@ -156,9 +150,7 @@ except Exception as e:
|
|
|
156
150
|
commands.push(`SetWidgetSizeToContent ${params.widgetName}.${params.componentName} ${params.sizeToContent}`);
|
|
157
151
|
}
|
|
158
152
|
|
|
159
|
-
|
|
160
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
161
|
-
}
|
|
153
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
162
154
|
|
|
163
155
|
return { success: true, message: 'Widget image updated' };
|
|
164
156
|
}
|
|
@@ -172,7 +164,7 @@ except Exception as e:
|
|
|
172
164
|
size?: [number, number];
|
|
173
165
|
}>;
|
|
174
166
|
}) {
|
|
175
|
-
|
|
167
|
+
const commands: string[] = [];
|
|
176
168
|
|
|
177
169
|
commands.push(`CreateHUDClass ${params.name}`);
|
|
178
170
|
|
|
@@ -183,9 +175,7 @@ except Exception as e:
|
|
|
183
175
|
}
|
|
184
176
|
}
|
|
185
177
|
|
|
186
|
-
|
|
187
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
188
|
-
}
|
|
178
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
189
179
|
|
|
190
180
|
return { success: true, message: `HUD ${params.name} created` };
|
|
191
181
|
}
|
|
@@ -197,11 +187,49 @@ except Exception as e:
|
|
|
197
187
|
playerIndex?: number;
|
|
198
188
|
}) {
|
|
199
189
|
const playerIndex = params.playerIndex ?? 0;
|
|
190
|
+
const widgetName = params.widgetName?.trim();
|
|
191
|
+
if (!widgetName) {
|
|
192
|
+
return { success: false, error: 'widgetName is required' };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const verifyScript = `
|
|
196
|
+
import unreal, json
|
|
197
|
+
name = r"${widgetName}"
|
|
198
|
+
candidates = []
|
|
199
|
+
if name.startswith('/Game/'):
|
|
200
|
+
candidates.append(name)
|
|
201
|
+
else:
|
|
202
|
+
candidates.append(f"/Game/UI/Widgets/{name}")
|
|
203
|
+
candidates.append(f"/Game/{name}")
|
|
204
|
+
|
|
205
|
+
found_path = ''
|
|
206
|
+
for path in candidates:
|
|
207
|
+
if unreal.EditorAssetLibrary.does_asset_exist(path):
|
|
208
|
+
found_path = path
|
|
209
|
+
break
|
|
210
|
+
|
|
211
|
+
print('RESULT:' + json.dumps({'success': bool(found_path), 'path': found_path, 'candidates': candidates}))
|
|
212
|
+
`.trim();
|
|
213
|
+
|
|
214
|
+
const verify = await this.bridge.executePythonWithResult(verifyScript);
|
|
215
|
+
if (!verify?.success) {
|
|
216
|
+
return { success: false, error: `Widget asset not found for ${widgetName}` };
|
|
217
|
+
}
|
|
218
|
+
|
|
200
219
|
const command = params.visible
|
|
201
|
-
? `ShowWidget ${
|
|
202
|
-
: `HideWidget ${
|
|
203
|
-
|
|
204
|
-
|
|
220
|
+
? `ShowWidget ${widgetName} ${playerIndex}`
|
|
221
|
+
: `HideWidget ${widgetName} ${playerIndex}`;
|
|
222
|
+
|
|
223
|
+
const raw = await this.bridge.executeConsoleCommand(command);
|
|
224
|
+
const summary = this.bridge.summarizeConsoleCommand(command, raw);
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
success: true,
|
|
228
|
+
message: params.visible ? `Widget ${widgetName} show command issued` : `Widget ${widgetName} hide command issued`,
|
|
229
|
+
command: summary.command,
|
|
230
|
+
output: summary.output || undefined,
|
|
231
|
+
logLines: summary.logLines?.length ? summary.logLines : undefined
|
|
232
|
+
};
|
|
205
233
|
}
|
|
206
234
|
|
|
207
235
|
// Add widget to viewport
|
|
@@ -232,10 +260,20 @@ try:
|
|
|
232
260
|
# Get the generated class from the widget blueprint
|
|
233
261
|
widget_class = widget_bp.generated_class() if hasattr(widget_bp, 'generated_class') else widget_bp
|
|
234
262
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
263
|
+
# Get the world and player controller via modern subsystems
|
|
264
|
+
world = None
|
|
265
|
+
try:
|
|
266
|
+
world = unreal.EditorUtilityLibrary.get_editor_world()
|
|
267
|
+
except Exception:
|
|
268
|
+
pass
|
|
269
|
+
|
|
270
|
+
if not world:
|
|
271
|
+
editor_subsystem = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
|
|
272
|
+
if editor_subsystem and hasattr(editor_subsystem, 'get_editor_world'):
|
|
273
|
+
world = editor_subsystem.get_editor_world()
|
|
274
|
+
|
|
275
|
+
if not world:
|
|
276
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'No editor world available. Start a PIE session or enable Editor Scripting Utilities.'}))
|
|
239
277
|
else:
|
|
240
278
|
# Try to get player controller
|
|
241
279
|
try:
|
|
@@ -261,17 +299,20 @@ except Exception as e:
|
|
|
261
299
|
|
|
262
300
|
try {
|
|
263
301
|
const resp = await this.bridge.executePython(py);
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
: { success: false, error: parsed.error };
|
|
272
|
-
} catch {}
|
|
302
|
+
const interpreted = interpretStandardResult(resp, {
|
|
303
|
+
successMessage: `Widget added to viewport with z-order ${zOrder}`,
|
|
304
|
+
failureMessage: 'Failed to add widget to viewport'
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
if (interpreted.success) {
|
|
308
|
+
return { success: true, message: interpreted.message };
|
|
273
309
|
}
|
|
274
|
-
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
success: false,
|
|
313
|
+
error: interpreted.error ?? 'Failed to add widget to viewport',
|
|
314
|
+
details: bestEffortInterpretedText(interpreted)
|
|
315
|
+
};
|
|
275
316
|
} catch (e) {
|
|
276
317
|
return { success: false, error: `Failed to add widget to viewport: ${e}` };
|
|
277
318
|
}
|
|
@@ -297,7 +338,7 @@ except Exception as e:
|
|
|
297
338
|
position?: [number, number];
|
|
298
339
|
}>;
|
|
299
340
|
}) {
|
|
300
|
-
|
|
341
|
+
const commands: string[] = [];
|
|
301
342
|
|
|
302
343
|
commands.push(`CreateMenuWidget ${params.name} ${params.menuType}`);
|
|
303
344
|
|
|
@@ -308,9 +349,7 @@ except Exception as e:
|
|
|
308
349
|
}
|
|
309
350
|
}
|
|
310
351
|
|
|
311
|
-
|
|
312
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
313
|
-
}
|
|
352
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
314
353
|
|
|
315
354
|
return { success: true, message: `Menu ${params.name} created` };
|
|
316
355
|
}
|
|
@@ -329,7 +368,7 @@ except Exception as e:
|
|
|
329
368
|
}>;
|
|
330
369
|
}>;
|
|
331
370
|
}) {
|
|
332
|
-
|
|
371
|
+
const commands: string[] = [];
|
|
333
372
|
|
|
334
373
|
commands.push(`CreateWidgetAnimation ${params.widgetName} ${params.animationName} ${params.duration}`);
|
|
335
374
|
|
|
@@ -344,9 +383,7 @@ except Exception as e:
|
|
|
344
383
|
}
|
|
345
384
|
}
|
|
346
385
|
|
|
347
|
-
|
|
348
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
349
|
-
}
|
|
386
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
350
387
|
|
|
351
388
|
return { success: true, message: `Animation ${params.animationName} created` };
|
|
352
389
|
}
|
|
@@ -377,7 +414,7 @@ except Exception as e:
|
|
|
377
414
|
margin?: [number, number, number, number];
|
|
378
415
|
};
|
|
379
416
|
}) {
|
|
380
|
-
|
|
417
|
+
const commands: string[] = [];
|
|
381
418
|
|
|
382
419
|
if (params.style.backgroundColor) {
|
|
383
420
|
commands.push(`SetWidgetBackgroundColor ${params.widgetName}.${params.componentName} ${params.style.backgroundColor.join(' ')}`);
|
|
@@ -399,9 +436,7 @@ except Exception as e:
|
|
|
399
436
|
commands.push(`SetWidgetMargin ${params.widgetName}.${params.componentName} ${params.style.margin.join(' ')}`);
|
|
400
437
|
}
|
|
401
438
|
|
|
402
|
-
|
|
403
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
404
|
-
}
|
|
439
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
405
440
|
|
|
406
441
|
return { success: true, message: 'Widget style updated' };
|
|
407
442
|
}
|
|
@@ -423,7 +458,7 @@ except Exception as e:
|
|
|
423
458
|
showCursor?: boolean;
|
|
424
459
|
lockCursor?: boolean;
|
|
425
460
|
}) {
|
|
426
|
-
|
|
461
|
+
const commands: string[] = [];
|
|
427
462
|
|
|
428
463
|
commands.push(`SetInputMode ${params.mode}`);
|
|
429
464
|
|
|
@@ -435,9 +470,7 @@ except Exception as e:
|
|
|
435
470
|
commands.push(`SetMouseLockMode ${params.lockCursor}`);
|
|
436
471
|
}
|
|
437
472
|
|
|
438
|
-
|
|
439
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
440
|
-
}
|
|
473
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
441
474
|
|
|
442
475
|
return { success: true, message: `Input mode set to ${params.mode}` };
|
|
443
476
|
}
|
|
@@ -475,9 +508,7 @@ except Exception as e:
|
|
|
475
508
|
}
|
|
476
509
|
}
|
|
477
510
|
|
|
478
|
-
|
|
479
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
480
|
-
}
|
|
511
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
481
512
|
|
|
482
513
|
return { success: true, message: 'Drag and drop configured' };
|
|
483
514
|
}
|
package/src/tools/visual.ts
CHANGED
|
@@ -1,60 +1,281 @@
|
|
|
1
1
|
import { UnrealBridge } from '../unreal-bridge.js';
|
|
2
2
|
import { loadEnv } from '../types/env.js';
|
|
3
|
-
import
|
|
3
|
+
import { Logger } from '../utils/logger.js';
|
|
4
|
+
import { coerceStringArray, interpretStandardResult } from '../utils/result-helpers.js';
|
|
5
|
+
import { promises as fs } from 'fs';
|
|
4
6
|
import path from 'path';
|
|
5
7
|
|
|
6
8
|
export class VisualTools {
|
|
7
9
|
private env = loadEnv();
|
|
10
|
+
private log = new Logger('VisualTools');
|
|
8
11
|
constructor(private bridge: UnrealBridge) {}
|
|
9
12
|
|
|
10
13
|
// Take a screenshot of viewport (high res or standard). Returns path and base64 (truncated)
|
|
11
14
|
async takeScreenshot(params: { resolution?: string }) {
|
|
12
15
|
const res = params.resolution && /^\d+x\d+$/i.test(params.resolution) ? params.resolution : '';
|
|
13
|
-
|
|
16
|
+
const primaryCommand = res ? `HighResShot ${res}` : 'Shot';
|
|
17
|
+
const captureStartedAt = Date.now();
|
|
14
18
|
try {
|
|
15
|
-
await this.bridge.executeConsoleCommand(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
const firstAttempt = await this.bridge.executeConsoleCommand(primaryCommand);
|
|
20
|
+
const firstSummary = this.bridge.summarizeConsoleCommand(primaryCommand, firstAttempt);
|
|
21
|
+
|
|
22
|
+
if (!res) {
|
|
23
|
+
const output = (firstSummary.output || '').toLowerCase();
|
|
24
|
+
const badInput = output.includes('bad input') || output.includes('unrecognized command');
|
|
25
|
+
const hasErrorLine = firstSummary.logLines?.some(line => /error:/i.test(line));
|
|
26
|
+
if (badInput || hasErrorLine) {
|
|
27
|
+
this.log.debug(`Screenshot primary command reported an error (${firstSummary.output || 'no output'})`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Give the engine a moment to write the file (UE can flush asynchronously)
|
|
31
|
+
await new Promise(r => setTimeout(r, 1200));
|
|
32
|
+
const p = await this.findLatestScreenshot(captureStartedAt);
|
|
33
|
+
if (!p) {
|
|
34
|
+
this.log.warn('Screenshot captured but output file was not found');
|
|
35
|
+
return { success: true, message: 'Screenshot triggered, but could not locate output file' };
|
|
36
|
+
}
|
|
20
37
|
let b64: string | undefined;
|
|
38
|
+
let truncated = false;
|
|
39
|
+
let sizeBytes: number | undefined;
|
|
40
|
+
let mime: string | undefined;
|
|
21
41
|
try {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
const max = 1024 * 1024;
|
|
25
|
-
|
|
42
|
+
const stat = await fs.stat(p);
|
|
43
|
+
sizeBytes = stat.size;
|
|
44
|
+
const max = 1024 * 1024; // 1 MiB cap for inline payloads
|
|
45
|
+
if (stat.size <= max) {
|
|
46
|
+
const buf = await fs.readFile(p);
|
|
47
|
+
b64 = buf.toString('base64');
|
|
48
|
+
} else {
|
|
49
|
+
truncated = true;
|
|
50
|
+
}
|
|
51
|
+
const ext = path.extname(p).toLowerCase();
|
|
52
|
+
if (ext === '.png') mime = 'image/png';
|
|
53
|
+
else if (ext === '.jpg' || ext === '.jpeg') mime = 'image/jpeg';
|
|
54
|
+
else if (ext === '.bmp') mime = 'image/bmp';
|
|
26
55
|
} catch {}
|
|
27
|
-
return {
|
|
56
|
+
return {
|
|
57
|
+
success: true,
|
|
58
|
+
imagePath: p.replace(/\\/g, '/'),
|
|
59
|
+
imageMimeType: mime,
|
|
60
|
+
imageSizeBytes: sizeBytes,
|
|
61
|
+
imageBase64: b64,
|
|
62
|
+
imageBase64Truncated: truncated
|
|
63
|
+
};
|
|
28
64
|
} catch (err: any) {
|
|
29
65
|
return { success: false, error: String(err?.message || err) };
|
|
30
66
|
}
|
|
31
67
|
}
|
|
32
68
|
|
|
33
|
-
private async
|
|
69
|
+
private async getEngineScreenshotDirectories(): Promise<string[]> {
|
|
70
|
+
const python = `
|
|
71
|
+
import unreal
|
|
72
|
+
import json
|
|
73
|
+
import os
|
|
74
|
+
|
|
75
|
+
result = {
|
|
76
|
+
"success": True,
|
|
77
|
+
"message": "",
|
|
78
|
+
"error": "",
|
|
79
|
+
"directories": [],
|
|
80
|
+
"warnings": [],
|
|
81
|
+
"details": []
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
def finalize():
|
|
85
|
+
data = dict(result)
|
|
86
|
+
if data.get("success"):
|
|
87
|
+
if not data.get("message"):
|
|
88
|
+
data["message"] = "Collected screenshot directories"
|
|
89
|
+
data.pop("error", None)
|
|
90
|
+
else:
|
|
91
|
+
if not data.get("error"):
|
|
92
|
+
data["error"] = data.get("message") or "Failed to collect screenshot directories"
|
|
93
|
+
if not data.get("message"):
|
|
94
|
+
data["message"] = data["error"]
|
|
95
|
+
if not data.get("warnings"):
|
|
96
|
+
data.pop("warnings", None)
|
|
97
|
+
if not data.get("details"):
|
|
98
|
+
data.pop("details", None)
|
|
99
|
+
if not data.get("directories"):
|
|
100
|
+
data.pop("directories", None)
|
|
101
|
+
return data
|
|
102
|
+
|
|
103
|
+
def add_path(candidate, note=None):
|
|
104
|
+
if not candidate:
|
|
105
|
+
return
|
|
106
|
+
try:
|
|
107
|
+
abs_path = os.path.abspath(os.path.normpath(candidate))
|
|
108
|
+
except Exception as normalize_error:
|
|
109
|
+
result["warnings"].append(f"Failed to normalize path {candidate}: {normalize_error}")
|
|
110
|
+
return
|
|
111
|
+
if abs_path not in result["directories"]:
|
|
112
|
+
result["directories"].append(abs_path)
|
|
113
|
+
if note:
|
|
114
|
+
result["details"].append(f"{note}: {abs_path}")
|
|
115
|
+
else:
|
|
116
|
+
result["details"].append(f"Discovered screenshot directory: {abs_path}")
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
automation_dir = unreal.AutomationLibrary.get_screenshot_directory()
|
|
120
|
+
add_path(automation_dir, "Automation screenshot directory")
|
|
121
|
+
except Exception as automation_error:
|
|
122
|
+
result["warnings"].append(f"Automation screenshot directory unavailable: {automation_error}")
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
project_saved = unreal.Paths.project_saved_dir()
|
|
126
|
+
add_path(project_saved, "Project Saved directory")
|
|
127
|
+
if project_saved:
|
|
128
|
+
add_path(os.path.join(project_saved, 'Screenshots'), "Project Saved screenshots")
|
|
129
|
+
add_path(os.path.join(project_saved, 'Screenshots', 'Windows'), "Project Saved Windows screenshots")
|
|
130
|
+
add_path(os.path.join(project_saved, 'Screenshots', 'WindowsEditor'), "Project Saved WindowsEditor screenshots")
|
|
131
|
+
try:
|
|
132
|
+
platform_name = unreal.SystemLibrary.get_platform_user_name()
|
|
133
|
+
if platform_name:
|
|
134
|
+
add_path(os.path.join(project_saved, 'Screenshots', platform_name), f"Project Saved screenshots for {platform_name}")
|
|
135
|
+
add_path(os.path.join(project_saved, 'Screenshots', f"{platform_name}Editor"), f"Project Saved editor screenshots for {platform_name}")
|
|
136
|
+
except Exception as platform_error:
|
|
137
|
+
result["warnings"].append(f"Failed to resolve platform-specific screenshot directories: {platform_error}")
|
|
138
|
+
except Exception as saved_error:
|
|
139
|
+
result["warnings"].append(f"Project Saved directory unavailable: {saved_error}")
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
project_file = unreal.Paths.get_project_file_path()
|
|
143
|
+
if project_file:
|
|
144
|
+
project_dir = os.path.dirname(project_file)
|
|
145
|
+
add_path(os.path.join(project_dir, 'Saved', 'Screenshots'), "Project directory screenshots")
|
|
146
|
+
add_path(os.path.join(project_dir, 'Saved', 'Screenshots', 'Windows'), "Project directory Windows screenshots")
|
|
147
|
+
add_path(os.path.join(project_dir, 'Saved', 'Screenshots', 'WindowsEditor'), "Project directory WindowsEditor screenshots")
|
|
148
|
+
except Exception as project_error:
|
|
149
|
+
result["warnings"].append(f"Project directory screenshots unavailable: {project_error}")
|
|
150
|
+
|
|
151
|
+
if not result["directories"]:
|
|
152
|
+
result["warnings"].append("No screenshot directories discovered")
|
|
153
|
+
|
|
154
|
+
print('RESULT:' + json.dumps(finalize()))
|
|
155
|
+
`.trim()
|
|
156
|
+
.replace(/\r?\n/g, '\n');
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const response = await this.bridge.executePython(python);
|
|
160
|
+
const interpreted = interpretStandardResult(response, {
|
|
161
|
+
successMessage: 'Collected screenshot directories',
|
|
162
|
+
failureMessage: 'Failed to collect screenshot directories'
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
if (interpreted.details) {
|
|
166
|
+
for (const entry of interpreted.details) {
|
|
167
|
+
this.log.debug(entry);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (interpreted.warnings) {
|
|
172
|
+
for (const warning of interpreted.warnings) {
|
|
173
|
+
this.log.debug(`Screenshot directory warning: ${warning}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const directories = coerceStringArray(interpreted.payload.directories);
|
|
178
|
+
if (directories?.length) {
|
|
179
|
+
return directories;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!interpreted.success && interpreted.error) {
|
|
183
|
+
this.log.warn(`Screenshot path probe failed: ${interpreted.error}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (interpreted.rawText) {
|
|
187
|
+
try {
|
|
188
|
+
const fallback = JSON.parse(interpreted.rawText);
|
|
189
|
+
const fallbackDirs = coerceStringArray(fallback);
|
|
190
|
+
if (fallbackDirs?.length) {
|
|
191
|
+
return fallbackDirs;
|
|
192
|
+
}
|
|
193
|
+
} catch {}
|
|
194
|
+
}
|
|
195
|
+
} catch (err) {
|
|
196
|
+
this.log.debug('Screenshot path probe failed', err);
|
|
197
|
+
}
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private async findLatestScreenshot(since?: number): Promise<string | null> {
|
|
34
202
|
// Try env override, otherwise look in common UE Saved/Screenshots folder under project
|
|
35
203
|
const candidates: string[] = [];
|
|
36
|
-
|
|
204
|
+
const seen = new Set<string>();
|
|
205
|
+
const addCandidate = (candidate?: string | null) => {
|
|
206
|
+
if (!candidate) return;
|
|
207
|
+
const normalized = path.isAbsolute(candidate)
|
|
208
|
+
? path.normalize(candidate)
|
|
209
|
+
: path.resolve(candidate);
|
|
210
|
+
if (!seen.has(normalized)) {
|
|
211
|
+
seen.add(normalized);
|
|
212
|
+
candidates.push(normalized);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
if (this.env.UE_SCREENSHOT_DIR) addCandidate(this.env.UE_SCREENSHOT_DIR);
|
|
37
217
|
if (this.env.UE_PROJECT_PATH) {
|
|
38
218
|
const projectDir = path.dirname(this.env.UE_PROJECT_PATH);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
219
|
+
addCandidate(path.join(projectDir, 'Saved', 'Screenshots'));
|
|
220
|
+
addCandidate(path.join(projectDir, 'Saved', 'Screenshots', 'Windows'));
|
|
221
|
+
addCandidate(path.join(projectDir, 'Saved', 'Screenshots', 'WindowsEditor'));
|
|
42
222
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
223
|
+
|
|
224
|
+
const engineDirs = await this.getEngineScreenshotDirectories();
|
|
225
|
+
for (const dir of engineDirs) {
|
|
226
|
+
addCandidate(dir);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Fallback: common locations relative to current working directory
|
|
230
|
+
addCandidate(path.join(process.cwd(), 'Saved', 'Screenshots'));
|
|
231
|
+
addCandidate(path.join(process.cwd(), 'Saved', 'Screenshots', 'Windows'));
|
|
232
|
+
addCandidate(path.join(process.cwd(), 'Saved', 'Screenshots', 'WindowsEditor'));
|
|
233
|
+
|
|
234
|
+
const searchDirs = new Set<string>();
|
|
235
|
+
const queue: string[] = [...candidates];
|
|
236
|
+
while (queue.length) {
|
|
237
|
+
const candidate = queue.pop();
|
|
238
|
+
if (!candidate || searchDirs.has(candidate)) continue;
|
|
239
|
+
searchDirs.add(candidate);
|
|
49
240
|
try {
|
|
50
|
-
const entries = fs.
|
|
51
|
-
for (const
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
241
|
+
const entries = await fs.readdir(candidate, { withFileTypes: true });
|
|
242
|
+
for (const entry of entries) {
|
|
243
|
+
if (entry.isDirectory()) {
|
|
244
|
+
queue.push(path.join(candidate, entry.name));
|
|
245
|
+
}
|
|
55
246
|
}
|
|
56
247
|
} catch {}
|
|
57
248
|
}
|
|
58
|
-
|
|
249
|
+
|
|
250
|
+
let latest: { path: string; mtime: number } | null = null;
|
|
251
|
+
let latestSince: { path: string; mtime: number } | null = null;
|
|
252
|
+
const cutoff = since ? since - 2000 : undefined; // allow slight clock drift
|
|
253
|
+
|
|
254
|
+
for (const dirPath of searchDirs) {
|
|
255
|
+
let entries: string[];
|
|
256
|
+
try {
|
|
257
|
+
entries = await fs.readdir(dirPath);
|
|
258
|
+
} catch {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
for (const entry of entries) {
|
|
262
|
+
const fp = path.join(dirPath, entry);
|
|
263
|
+
if (!/\.(png|jpg|jpeg|bmp)$/i.test(fp)) continue;
|
|
264
|
+
try {
|
|
265
|
+
const st = await fs.stat(fp);
|
|
266
|
+
const info = { path: fp, mtime: st.mtimeMs };
|
|
267
|
+
if (!latest || info.mtime > latest.mtime) {
|
|
268
|
+
latest = info;
|
|
269
|
+
}
|
|
270
|
+
if (cutoff !== undefined && st.mtimeMs >= cutoff) {
|
|
271
|
+
if (!latestSince || info.mtime > latestSince.mtime) {
|
|
272
|
+
latestSince = info;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
} catch {}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const chosen = latestSince || latest;
|
|
279
|
+
return chosen?.path || null;
|
|
59
280
|
}
|
|
60
281
|
}
|
package/src/types/tool-types.ts
CHANGED
|
@@ -332,12 +332,3 @@ export type GetToolResponse<T extends ToolName> = ToolResponseMap[T];
|
|
|
332
332
|
// Helper type for getting parameters by tool name
|
|
333
333
|
export type GetToolParams<T extends keyof ConsolidatedToolParams> = ConsolidatedToolParams[T];
|
|
334
334
|
|
|
335
|
-
// Export a type guard to check if a response is successful
|
|
336
|
-
export function isSuccessResponse(response: BaseToolResponse): response is BaseToolResponse & { success: true } {
|
|
337
|
-
return response.success === true;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Export a type guard to check if a response has an error
|
|
341
|
-
export function isErrorResponse(response: BaseToolResponse): response is BaseToolResponse & { error: string } {
|
|
342
|
-
return response.success === false && typeof response.error === 'string';
|
|
343
|
-
}
|