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/dist/tools/ui.js
CHANGED
|
@@ -1,20 +1,9 @@
|
|
|
1
|
+
import { bestEffortInterpretedText, interpretStandardResult } from '../utils/result-helpers.js';
|
|
1
2
|
export class UITools {
|
|
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
|
-
Command: command,
|
|
13
|
-
SpecificPlayer: null
|
|
14
|
-
},
|
|
15
|
-
generateTransaction: false
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
7
|
// Create widget blueprint
|
|
19
8
|
async createWidget(params) {
|
|
20
9
|
const path = params.savePath || '/Game/UI/Widgets';
|
|
@@ -51,16 +40,18 @@ except Exception as e:
|
|
|
51
40
|
`.trim();
|
|
52
41
|
try {
|
|
53
42
|
const resp = await this.bridge.executePython(py);
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
catch { }
|
|
43
|
+
const interpreted = interpretStandardResult(resp, {
|
|
44
|
+
successMessage: 'Widget blueprint created',
|
|
45
|
+
failureMessage: 'Failed to create WidgetBlueprint'
|
|
46
|
+
});
|
|
47
|
+
if (interpreted.success) {
|
|
48
|
+
return { success: true, message: interpreted.message };
|
|
62
49
|
}
|
|
63
|
-
return {
|
|
50
|
+
return {
|
|
51
|
+
success: false,
|
|
52
|
+
error: interpreted.error ?? 'Failed to create widget blueprint',
|
|
53
|
+
details: bestEffortInterpretedText(interpreted)
|
|
54
|
+
};
|
|
64
55
|
}
|
|
65
56
|
catch (e) {
|
|
66
57
|
return { success: false, error: `Failed to create widget blueprint: ${e}` };
|
|
@@ -84,9 +75,7 @@ except Exception as e:
|
|
|
84
75
|
commands.push(`SetWidgetAlignment ${params.widgetName}.${params.componentName} ${params.slot.alignment.join(' ')}`);
|
|
85
76
|
}
|
|
86
77
|
}
|
|
87
|
-
|
|
88
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
89
|
-
}
|
|
78
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
90
79
|
return { success: true, message: `Component ${params.componentName} added to widget` };
|
|
91
80
|
}
|
|
92
81
|
// Set text
|
|
@@ -102,9 +91,7 @@ except Exception as e:
|
|
|
102
91
|
if (params.fontFamily) {
|
|
103
92
|
commands.push(`SetWidgetFont ${params.widgetName}.${params.componentName} ${params.fontFamily}`);
|
|
104
93
|
}
|
|
105
|
-
|
|
106
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
107
|
-
}
|
|
94
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
108
95
|
return { success: true, message: 'Widget text updated' };
|
|
109
96
|
}
|
|
110
97
|
// Set image
|
|
@@ -117,9 +104,7 @@ except Exception as e:
|
|
|
117
104
|
if (params.sizeToContent !== undefined) {
|
|
118
105
|
commands.push(`SetWidgetSizeToContent ${params.widgetName}.${params.componentName} ${params.sizeToContent}`);
|
|
119
106
|
}
|
|
120
|
-
|
|
121
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
122
|
-
}
|
|
107
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
123
108
|
return { success: true, message: 'Widget image updated' };
|
|
124
109
|
}
|
|
125
110
|
// Create HUD
|
|
@@ -132,18 +117,50 @@ except Exception as e:
|
|
|
132
117
|
commands.push(`AddHUDElement ${params.name} ${element.type} ${element.position.join(' ')} ${size.join(' ')}`);
|
|
133
118
|
}
|
|
134
119
|
}
|
|
135
|
-
|
|
136
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
137
|
-
}
|
|
120
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
138
121
|
return { success: true, message: `HUD ${params.name} created` };
|
|
139
122
|
}
|
|
140
123
|
// Show/Hide widget
|
|
141
124
|
async setWidgetVisibility(params) {
|
|
142
125
|
const playerIndex = params.playerIndex ?? 0;
|
|
126
|
+
const widgetName = params.widgetName?.trim();
|
|
127
|
+
if (!widgetName) {
|
|
128
|
+
return { success: false, error: 'widgetName is required' };
|
|
129
|
+
}
|
|
130
|
+
const verifyScript = `
|
|
131
|
+
import unreal, json
|
|
132
|
+
name = r"${widgetName}"
|
|
133
|
+
candidates = []
|
|
134
|
+
if name.startswith('/Game/'):
|
|
135
|
+
candidates.append(name)
|
|
136
|
+
else:
|
|
137
|
+
candidates.append(f"/Game/UI/Widgets/{name}")
|
|
138
|
+
candidates.append(f"/Game/{name}")
|
|
139
|
+
|
|
140
|
+
found_path = ''
|
|
141
|
+
for path in candidates:
|
|
142
|
+
if unreal.EditorAssetLibrary.does_asset_exist(path):
|
|
143
|
+
found_path = path
|
|
144
|
+
break
|
|
145
|
+
|
|
146
|
+
print('RESULT:' + json.dumps({'success': bool(found_path), 'path': found_path, 'candidates': candidates}))
|
|
147
|
+
`.trim();
|
|
148
|
+
const verify = await this.bridge.executePythonWithResult(verifyScript);
|
|
149
|
+
if (!verify?.success) {
|
|
150
|
+
return { success: false, error: `Widget asset not found for ${widgetName}` };
|
|
151
|
+
}
|
|
143
152
|
const command = params.visible
|
|
144
|
-
? `ShowWidget ${
|
|
145
|
-
: `HideWidget ${
|
|
146
|
-
|
|
153
|
+
? `ShowWidget ${widgetName} ${playerIndex}`
|
|
154
|
+
: `HideWidget ${widgetName} ${playerIndex}`;
|
|
155
|
+
const raw = await this.bridge.executeConsoleCommand(command);
|
|
156
|
+
const summary = this.bridge.summarizeConsoleCommand(command, raw);
|
|
157
|
+
return {
|
|
158
|
+
success: true,
|
|
159
|
+
message: params.visible ? `Widget ${widgetName} show command issued` : `Widget ${widgetName} hide command issued`,
|
|
160
|
+
command: summary.command,
|
|
161
|
+
output: summary.output || undefined,
|
|
162
|
+
logLines: summary.logLines?.length ? summary.logLines : undefined
|
|
163
|
+
};
|
|
147
164
|
}
|
|
148
165
|
// Add widget to viewport
|
|
149
166
|
async addWidgetToViewport(params) {
|
|
@@ -168,10 +185,20 @@ try:
|
|
|
168
185
|
# Get the generated class from the widget blueprint
|
|
169
186
|
widget_class = widget_bp.generated_class() if hasattr(widget_bp, 'generated_class') else widget_bp
|
|
170
187
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
188
|
+
# Get the world and player controller via modern subsystems
|
|
189
|
+
world = None
|
|
190
|
+
try:
|
|
191
|
+
world = unreal.EditorUtilityLibrary.get_editor_world()
|
|
192
|
+
except Exception:
|
|
193
|
+
pass
|
|
194
|
+
|
|
195
|
+
if not world:
|
|
196
|
+
editor_subsystem = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
|
|
197
|
+
if editor_subsystem and hasattr(editor_subsystem, 'get_editor_world'):
|
|
198
|
+
world = editor_subsystem.get_editor_world()
|
|
199
|
+
|
|
200
|
+
if not world:
|
|
201
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'No editor world available. Start a PIE session or enable Editor Scripting Utilities.'}))
|
|
175
202
|
else:
|
|
176
203
|
# Try to get player controller
|
|
177
204
|
try:
|
|
@@ -196,18 +223,18 @@ except Exception as e:
|
|
|
196
223
|
`.trim();
|
|
197
224
|
try {
|
|
198
225
|
const resp = await this.bridge.executePython(py);
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
? { success: true, message: `Widget added to viewport with z-order ${zOrder}` }
|
|
206
|
-
: { success: false, error: parsed.error };
|
|
207
|
-
}
|
|
208
|
-
catch { }
|
|
226
|
+
const interpreted = interpretStandardResult(resp, {
|
|
227
|
+
successMessage: `Widget added to viewport with z-order ${zOrder}`,
|
|
228
|
+
failureMessage: 'Failed to add widget to viewport'
|
|
229
|
+
});
|
|
230
|
+
if (interpreted.success) {
|
|
231
|
+
return { success: true, message: interpreted.message };
|
|
209
232
|
}
|
|
210
|
-
return {
|
|
233
|
+
return {
|
|
234
|
+
success: false,
|
|
235
|
+
error: interpreted.error ?? 'Failed to add widget to viewport',
|
|
236
|
+
details: bestEffortInterpretedText(interpreted)
|
|
237
|
+
};
|
|
211
238
|
}
|
|
212
239
|
catch (e) {
|
|
213
240
|
return { success: false, error: `Failed to add widget to viewport: ${e}` };
|
|
@@ -229,9 +256,7 @@ except Exception as e:
|
|
|
229
256
|
commands.push(`AddMenuButton ${params.name} "${button.text}" ${button.action} ${pos.join(' ')}`);
|
|
230
257
|
}
|
|
231
258
|
}
|
|
232
|
-
|
|
233
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
234
|
-
}
|
|
259
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
235
260
|
return { success: true, message: `Menu ${params.name} created` };
|
|
236
261
|
}
|
|
237
262
|
// Set widget animation
|
|
@@ -247,9 +272,7 @@ except Exception as e:
|
|
|
247
272
|
}
|
|
248
273
|
}
|
|
249
274
|
}
|
|
250
|
-
|
|
251
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
252
|
-
}
|
|
275
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
253
276
|
return { success: true, message: `Animation ${params.animationName} created` };
|
|
254
277
|
}
|
|
255
278
|
// Play widget animation
|
|
@@ -277,9 +300,7 @@ except Exception as e:
|
|
|
277
300
|
if (params.style.margin) {
|
|
278
301
|
commands.push(`SetWidgetMargin ${params.widgetName}.${params.componentName} ${params.style.margin.join(' ')}`);
|
|
279
302
|
}
|
|
280
|
-
|
|
281
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
282
|
-
}
|
|
303
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
283
304
|
return { success: true, message: 'Widget style updated' };
|
|
284
305
|
}
|
|
285
306
|
// Bind widget event
|
|
@@ -297,9 +318,7 @@ except Exception as e:
|
|
|
297
318
|
if (params.lockCursor !== undefined) {
|
|
298
319
|
commands.push(`SetMouseLockMode ${params.lockCursor}`);
|
|
299
320
|
}
|
|
300
|
-
|
|
301
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
302
|
-
}
|
|
321
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
303
322
|
return { success: true, message: `Input mode set to ${params.mode}` };
|
|
304
323
|
}
|
|
305
324
|
// Create tooltip
|
|
@@ -320,9 +339,7 @@ except Exception as e:
|
|
|
320
339
|
commands.push(`AddDropTarget ${params.widgetName}.${params.componentName} ${target}`);
|
|
321
340
|
}
|
|
322
341
|
}
|
|
323
|
-
|
|
324
|
-
await this.bridge.executeConsoleCommand(cmd);
|
|
325
|
-
}
|
|
342
|
+
await this.bridge.executeConsoleCommands(commands);
|
|
326
343
|
return { success: true, message: 'Drag and drop configured' };
|
|
327
344
|
}
|
|
328
345
|
// Create notification
|
package/dist/tools/visual.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { UnrealBridge } from '../unreal-bridge.js';
|
|
|
2
2
|
export declare class VisualTools {
|
|
3
3
|
private bridge;
|
|
4
4
|
private env;
|
|
5
|
+
private log;
|
|
5
6
|
constructor(bridge: UnrealBridge);
|
|
6
7
|
takeScreenshot(params: {
|
|
7
8
|
resolution?: string;
|
|
@@ -9,12 +10,18 @@ export declare class VisualTools {
|
|
|
9
10
|
success: boolean;
|
|
10
11
|
message: string;
|
|
11
12
|
imagePath?: undefined;
|
|
13
|
+
imageMimeType?: undefined;
|
|
14
|
+
imageSizeBytes?: undefined;
|
|
12
15
|
imageBase64?: undefined;
|
|
16
|
+
imageBase64Truncated?: undefined;
|
|
13
17
|
error?: undefined;
|
|
14
18
|
} | {
|
|
15
19
|
success: boolean;
|
|
16
20
|
imagePath: string;
|
|
21
|
+
imageMimeType: string | undefined;
|
|
22
|
+
imageSizeBytes: number | undefined;
|
|
17
23
|
imageBase64: string | undefined;
|
|
24
|
+
imageBase64Truncated: boolean;
|
|
18
25
|
message?: undefined;
|
|
19
26
|
error?: undefined;
|
|
20
27
|
} | {
|
|
@@ -22,8 +29,12 @@ export declare class VisualTools {
|
|
|
22
29
|
error: string;
|
|
23
30
|
message?: undefined;
|
|
24
31
|
imagePath?: undefined;
|
|
32
|
+
imageMimeType?: undefined;
|
|
33
|
+
imageSizeBytes?: undefined;
|
|
25
34
|
imageBase64?: undefined;
|
|
35
|
+
imageBase64Truncated?: undefined;
|
|
26
36
|
}>;
|
|
37
|
+
private getEngineScreenshotDirectories;
|
|
27
38
|
private findLatestScreenshot;
|
|
28
39
|
}
|
|
29
40
|
//# sourceMappingURL=visual.d.ts.map
|
package/dist/tools/visual.js
CHANGED
|
@@ -1,67 +1,282 @@
|
|
|
1
1
|
import { loadEnv } from '../types/env.js';
|
|
2
|
-
import
|
|
2
|
+
import { Logger } from '../utils/logger.js';
|
|
3
|
+
import { coerceStringArray, interpretStandardResult } from '../utils/result-helpers.js';
|
|
4
|
+
import { promises as fs } from 'fs';
|
|
3
5
|
import path from 'path';
|
|
4
6
|
export class VisualTools {
|
|
5
7
|
bridge;
|
|
6
8
|
env = loadEnv();
|
|
9
|
+
log = new Logger('VisualTools');
|
|
7
10
|
constructor(bridge) {
|
|
8
11
|
this.bridge = bridge;
|
|
9
12
|
}
|
|
10
13
|
// Take a screenshot of viewport (high res or standard). Returns path and base64 (truncated)
|
|
11
14
|
async takeScreenshot(params) {
|
|
12
15
|
const res = params.resolution && /^\d+x\d+$/i.test(params.resolution) ? params.resolution : '';
|
|
13
|
-
const
|
|
16
|
+
const primaryCommand = res ? `HighResShot ${res}` : 'Shot';
|
|
17
|
+
const captureStartedAt = Date.now();
|
|
14
18
|
try {
|
|
15
|
-
await this.bridge.executeConsoleCommand(
|
|
16
|
-
|
|
19
|
+
const firstAttempt = await this.bridge.executeConsoleCommand(primaryCommand);
|
|
20
|
+
const firstSummary = this.bridge.summarizeConsoleCommand(primaryCommand, firstAttempt);
|
|
21
|
+
if (!res) {
|
|
22
|
+
const output = (firstSummary.output || '').toLowerCase();
|
|
23
|
+
const badInput = output.includes('bad input') || output.includes('unrecognized command');
|
|
24
|
+
const hasErrorLine = firstSummary.logLines?.some(line => /error:/i.test(line));
|
|
25
|
+
if (badInput || hasErrorLine) {
|
|
26
|
+
this.log.debug(`Screenshot primary command reported an error (${firstSummary.output || 'no output'})`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Give the engine a moment to write the file (UE can flush asynchronously)
|
|
17
30
|
await new Promise(r => setTimeout(r, 1200));
|
|
18
|
-
const p = await this.findLatestScreenshot();
|
|
19
|
-
if (!p)
|
|
31
|
+
const p = await this.findLatestScreenshot(captureStartedAt);
|
|
32
|
+
if (!p) {
|
|
33
|
+
this.log.warn('Screenshot captured but output file was not found');
|
|
20
34
|
return { success: true, message: 'Screenshot triggered, but could not locate output file' };
|
|
35
|
+
}
|
|
21
36
|
let b64;
|
|
37
|
+
let truncated = false;
|
|
38
|
+
let sizeBytes;
|
|
39
|
+
let mime;
|
|
22
40
|
try {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
const max = 1024 * 1024;
|
|
26
|
-
|
|
41
|
+
const stat = await fs.stat(p);
|
|
42
|
+
sizeBytes = stat.size;
|
|
43
|
+
const max = 1024 * 1024; // 1 MiB cap for inline payloads
|
|
44
|
+
if (stat.size <= max) {
|
|
45
|
+
const buf = await fs.readFile(p);
|
|
46
|
+
b64 = buf.toString('base64');
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
truncated = true;
|
|
50
|
+
}
|
|
51
|
+
const ext = path.extname(p).toLowerCase();
|
|
52
|
+
if (ext === '.png')
|
|
53
|
+
mime = 'image/png';
|
|
54
|
+
else if (ext === '.jpg' || ext === '.jpeg')
|
|
55
|
+
mime = 'image/jpeg';
|
|
56
|
+
else if (ext === '.bmp')
|
|
57
|
+
mime = 'image/bmp';
|
|
27
58
|
}
|
|
28
59
|
catch { }
|
|
29
|
-
return {
|
|
60
|
+
return {
|
|
61
|
+
success: true,
|
|
62
|
+
imagePath: p.replace(/\\/g, '/'),
|
|
63
|
+
imageMimeType: mime,
|
|
64
|
+
imageSizeBytes: sizeBytes,
|
|
65
|
+
imageBase64: b64,
|
|
66
|
+
imageBase64Truncated: truncated
|
|
67
|
+
};
|
|
30
68
|
}
|
|
31
69
|
catch (err) {
|
|
32
70
|
return { success: false, error: String(err?.message || err) };
|
|
33
71
|
}
|
|
34
72
|
}
|
|
35
|
-
async
|
|
73
|
+
async getEngineScreenshotDirectories() {
|
|
74
|
+
const python = `
|
|
75
|
+
import unreal
|
|
76
|
+
import json
|
|
77
|
+
import os
|
|
78
|
+
|
|
79
|
+
result = {
|
|
80
|
+
"success": True,
|
|
81
|
+
"message": "",
|
|
82
|
+
"error": "",
|
|
83
|
+
"directories": [],
|
|
84
|
+
"warnings": [],
|
|
85
|
+
"details": []
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
def finalize():
|
|
89
|
+
data = dict(result)
|
|
90
|
+
if data.get("success"):
|
|
91
|
+
if not data.get("message"):
|
|
92
|
+
data["message"] = "Collected screenshot directories"
|
|
93
|
+
data.pop("error", None)
|
|
94
|
+
else:
|
|
95
|
+
if not data.get("error"):
|
|
96
|
+
data["error"] = data.get("message") or "Failed to collect screenshot directories"
|
|
97
|
+
if not data.get("message"):
|
|
98
|
+
data["message"] = data["error"]
|
|
99
|
+
if not data.get("warnings"):
|
|
100
|
+
data.pop("warnings", None)
|
|
101
|
+
if not data.get("details"):
|
|
102
|
+
data.pop("details", None)
|
|
103
|
+
if not data.get("directories"):
|
|
104
|
+
data.pop("directories", None)
|
|
105
|
+
return data
|
|
106
|
+
|
|
107
|
+
def add_path(candidate, note=None):
|
|
108
|
+
if not candidate:
|
|
109
|
+
return
|
|
110
|
+
try:
|
|
111
|
+
abs_path = os.path.abspath(os.path.normpath(candidate))
|
|
112
|
+
except Exception as normalize_error:
|
|
113
|
+
result["warnings"].append(f"Failed to normalize path {candidate}: {normalize_error}")
|
|
114
|
+
return
|
|
115
|
+
if abs_path not in result["directories"]:
|
|
116
|
+
result["directories"].append(abs_path)
|
|
117
|
+
if note:
|
|
118
|
+
result["details"].append(f"{note}: {abs_path}")
|
|
119
|
+
else:
|
|
120
|
+
result["details"].append(f"Discovered screenshot directory: {abs_path}")
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
automation_dir = unreal.AutomationLibrary.get_screenshot_directory()
|
|
124
|
+
add_path(automation_dir, "Automation screenshot directory")
|
|
125
|
+
except Exception as automation_error:
|
|
126
|
+
result["warnings"].append(f"Automation screenshot directory unavailable: {automation_error}")
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
project_saved = unreal.Paths.project_saved_dir()
|
|
130
|
+
add_path(project_saved, "Project Saved directory")
|
|
131
|
+
if project_saved:
|
|
132
|
+
add_path(os.path.join(project_saved, 'Screenshots'), "Project Saved screenshots")
|
|
133
|
+
add_path(os.path.join(project_saved, 'Screenshots', 'Windows'), "Project Saved Windows screenshots")
|
|
134
|
+
add_path(os.path.join(project_saved, 'Screenshots', 'WindowsEditor'), "Project Saved WindowsEditor screenshots")
|
|
135
|
+
try:
|
|
136
|
+
platform_name = unreal.SystemLibrary.get_platform_user_name()
|
|
137
|
+
if platform_name:
|
|
138
|
+
add_path(os.path.join(project_saved, 'Screenshots', platform_name), f"Project Saved screenshots for {platform_name}")
|
|
139
|
+
add_path(os.path.join(project_saved, 'Screenshots', f"{platform_name}Editor"), f"Project Saved editor screenshots for {platform_name}")
|
|
140
|
+
except Exception as platform_error:
|
|
141
|
+
result["warnings"].append(f"Failed to resolve platform-specific screenshot directories: {platform_error}")
|
|
142
|
+
except Exception as saved_error:
|
|
143
|
+
result["warnings"].append(f"Project Saved directory unavailable: {saved_error}")
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
project_file = unreal.Paths.get_project_file_path()
|
|
147
|
+
if project_file:
|
|
148
|
+
project_dir = os.path.dirname(project_file)
|
|
149
|
+
add_path(os.path.join(project_dir, 'Saved', 'Screenshots'), "Project directory screenshots")
|
|
150
|
+
add_path(os.path.join(project_dir, 'Saved', 'Screenshots', 'Windows'), "Project directory Windows screenshots")
|
|
151
|
+
add_path(os.path.join(project_dir, 'Saved', 'Screenshots', 'WindowsEditor'), "Project directory WindowsEditor screenshots")
|
|
152
|
+
except Exception as project_error:
|
|
153
|
+
result["warnings"].append(f"Project directory screenshots unavailable: {project_error}")
|
|
154
|
+
|
|
155
|
+
if not result["directories"]:
|
|
156
|
+
result["warnings"].append("No screenshot directories discovered")
|
|
157
|
+
|
|
158
|
+
print('RESULT:' + json.dumps(finalize()))
|
|
159
|
+
`.trim()
|
|
160
|
+
.replace(/\r?\n/g, '\n');
|
|
161
|
+
try {
|
|
162
|
+
const response = await this.bridge.executePython(python);
|
|
163
|
+
const interpreted = interpretStandardResult(response, {
|
|
164
|
+
successMessage: 'Collected screenshot directories',
|
|
165
|
+
failureMessage: 'Failed to collect screenshot directories'
|
|
166
|
+
});
|
|
167
|
+
if (interpreted.details) {
|
|
168
|
+
for (const entry of interpreted.details) {
|
|
169
|
+
this.log.debug(entry);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (interpreted.warnings) {
|
|
173
|
+
for (const warning of interpreted.warnings) {
|
|
174
|
+
this.log.debug(`Screenshot directory warning: ${warning}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const directories = coerceStringArray(interpreted.payload.directories);
|
|
178
|
+
if (directories?.length) {
|
|
179
|
+
return directories;
|
|
180
|
+
}
|
|
181
|
+
if (!interpreted.success && interpreted.error) {
|
|
182
|
+
this.log.warn(`Screenshot path probe failed: ${interpreted.error}`);
|
|
183
|
+
}
|
|
184
|
+
if (interpreted.rawText) {
|
|
185
|
+
try {
|
|
186
|
+
const fallback = JSON.parse(interpreted.rawText);
|
|
187
|
+
const fallbackDirs = coerceStringArray(fallback);
|
|
188
|
+
if (fallbackDirs?.length) {
|
|
189
|
+
return fallbackDirs;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch { }
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
this.log.debug('Screenshot path probe failed', err);
|
|
197
|
+
}
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
async findLatestScreenshot(since) {
|
|
36
201
|
// Try env override, otherwise look in common UE Saved/Screenshots folder under project
|
|
37
202
|
const candidates = [];
|
|
203
|
+
const seen = new Set();
|
|
204
|
+
const addCandidate = (candidate) => {
|
|
205
|
+
if (!candidate)
|
|
206
|
+
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
|
+
};
|
|
38
215
|
if (this.env.UE_SCREENSHOT_DIR)
|
|
39
|
-
|
|
216
|
+
addCandidate(this.env.UE_SCREENSHOT_DIR);
|
|
40
217
|
if (this.env.UE_PROJECT_PATH) {
|
|
41
218
|
const projectDir = path.dirname(this.env.UE_PROJECT_PATH);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
219
|
+
addCandidate(path.join(projectDir, 'Saved', 'Screenshots'));
|
|
220
|
+
addCandidate(path.join(projectDir, 'Saved', 'Screenshots', 'Windows'));
|
|
221
|
+
addCandidate(path.join(projectDir, 'Saved', 'Screenshots', 'WindowsEditor'));
|
|
45
222
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
223
|
+
const engineDirs = await this.getEngineScreenshotDirectories();
|
|
224
|
+
for (const dir of engineDirs) {
|
|
225
|
+
addCandidate(dir);
|
|
226
|
+
}
|
|
227
|
+
// Fallback: common locations relative to current working directory
|
|
228
|
+
addCandidate(path.join(process.cwd(), 'Saved', 'Screenshots'));
|
|
229
|
+
addCandidate(path.join(process.cwd(), 'Saved', 'Screenshots', 'Windows'));
|
|
230
|
+
addCandidate(path.join(process.cwd(), 'Saved', 'Screenshots', 'WindowsEditor'));
|
|
231
|
+
const searchDirs = new Set();
|
|
232
|
+
const queue = [...candidates];
|
|
233
|
+
while (queue.length) {
|
|
234
|
+
const candidate = queue.pop();
|
|
235
|
+
if (!candidate || searchDirs.has(candidate))
|
|
236
|
+
continue;
|
|
237
|
+
searchDirs.add(candidate);
|
|
52
238
|
try {
|
|
53
|
-
const entries = fs.
|
|
54
|
-
for (const
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (!latest || st.mtimeMs > latest.mtime)
|
|
59
|
-
latest = { path: fp, mtime: st.mtimeMs };
|
|
239
|
+
const entries = await fs.readdir(candidate, { withFileTypes: true });
|
|
240
|
+
for (const entry of entries) {
|
|
241
|
+
if (entry.isDirectory()) {
|
|
242
|
+
queue.push(path.join(candidate, entry.name));
|
|
243
|
+
}
|
|
60
244
|
}
|
|
61
245
|
}
|
|
62
246
|
catch { }
|
|
63
247
|
}
|
|
64
|
-
|
|
248
|
+
let latest = null;
|
|
249
|
+
let latestSince = null;
|
|
250
|
+
const cutoff = since ? since - 2000 : undefined; // allow slight clock drift
|
|
251
|
+
for (const dirPath of searchDirs) {
|
|
252
|
+
let entries;
|
|
253
|
+
try {
|
|
254
|
+
entries = await fs.readdir(dirPath);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
for (const entry of entries) {
|
|
260
|
+
const fp = path.join(dirPath, entry);
|
|
261
|
+
if (!/\.(png|jpg|jpeg|bmp)$/i.test(fp))
|
|
262
|
+
continue;
|
|
263
|
+
try {
|
|
264
|
+
const st = await fs.stat(fp);
|
|
265
|
+
const info = { path: fp, mtime: st.mtimeMs };
|
|
266
|
+
if (!latest || info.mtime > latest.mtime) {
|
|
267
|
+
latest = info;
|
|
268
|
+
}
|
|
269
|
+
if (cutoff !== undefined && st.mtimeMs >= cutoff) {
|
|
270
|
+
if (!latestSince || info.mtime > latestSince.mtime) {
|
|
271
|
+
latestSince = info;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
catch { }
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const chosen = latestSince || latest;
|
|
279
|
+
return chosen?.path || null;
|
|
65
280
|
}
|
|
66
281
|
}
|
|
67
282
|
//# sourceMappingURL=visual.js.map
|
|
@@ -265,10 +265,4 @@ export interface ToolResponseMap {
|
|
|
265
265
|
export type ToolName = keyof ToolResponseMap;
|
|
266
266
|
export type GetToolResponse<T extends ToolName> = ToolResponseMap[T];
|
|
267
267
|
export type GetToolParams<T extends keyof ConsolidatedToolParams> = ConsolidatedToolParams[T];
|
|
268
|
-
export declare function isSuccessResponse(response: BaseToolResponse): response is BaseToolResponse & {
|
|
269
|
-
success: true;
|
|
270
|
-
};
|
|
271
|
-
export declare function isErrorResponse(response: BaseToolResponse): response is BaseToolResponse & {
|
|
272
|
-
error: string;
|
|
273
|
-
};
|
|
274
268
|
//# sourceMappingURL=tool-types.d.ts.map
|
package/dist/types/tool-types.js
CHANGED
|
@@ -2,12 +2,5 @@
|
|
|
2
2
|
* Auto-generated TypeScript types from tool schemas
|
|
3
3
|
* This provides type safety and IntelliSense support
|
|
4
4
|
*/
|
|
5
|
-
|
|
6
|
-
export function isSuccessResponse(response) {
|
|
7
|
-
return response.success === true;
|
|
8
|
-
}
|
|
9
|
-
// Export a type guard to check if a response has an error
|
|
10
|
-
export function isErrorResponse(response) {
|
|
11
|
-
return response.success === false && typeof response.error === 'string';
|
|
12
|
-
}
|
|
5
|
+
export {};
|
|
13
6
|
//# sourceMappingURL=tool-types.js.map
|