unreal-engine-mcp-server 0.2.1
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/.dockerignore +57 -0
- package/.env.production +25 -0
- package/.eslintrc.json +54 -0
- package/.github/workflows/publish-mcp.yml +75 -0
- package/Dockerfile +54 -0
- package/LICENSE +21 -0
- package/Public/icon.png +0 -0
- package/README.md +209 -0
- package/claude_desktop_config_example.json +13 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +7 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +484 -0
- package/dist/prompts/index.d.ts +14 -0
- package/dist/prompts/index.js +38 -0
- package/dist/python-utils.d.ts +29 -0
- package/dist/python-utils.js +54 -0
- package/dist/resources/actors.d.ts +13 -0
- package/dist/resources/actors.js +83 -0
- package/dist/resources/assets.d.ts +23 -0
- package/dist/resources/assets.js +245 -0
- package/dist/resources/levels.d.ts +17 -0
- package/dist/resources/levels.js +94 -0
- package/dist/tools/actors.d.ts +51 -0
- package/dist/tools/actors.js +459 -0
- package/dist/tools/animation.d.ts +196 -0
- package/dist/tools/animation.js +579 -0
- package/dist/tools/assets.d.ts +21 -0
- package/dist/tools/assets.js +304 -0
- package/dist/tools/audio.d.ts +170 -0
- package/dist/tools/audio.js +416 -0
- package/dist/tools/blueprint.d.ts +144 -0
- package/dist/tools/blueprint.js +652 -0
- package/dist/tools/build_environment_advanced.d.ts +66 -0
- package/dist/tools/build_environment_advanced.js +484 -0
- package/dist/tools/consolidated-tool-definitions.d.ts +2598 -0
- package/dist/tools/consolidated-tool-definitions.js +607 -0
- package/dist/tools/consolidated-tool-handlers.d.ts +2 -0
- package/dist/tools/consolidated-tool-handlers.js +1050 -0
- package/dist/tools/debug.d.ts +185 -0
- package/dist/tools/debug.js +265 -0
- package/dist/tools/editor.d.ts +88 -0
- package/dist/tools/editor.js +365 -0
- package/dist/tools/engine.d.ts +30 -0
- package/dist/tools/engine.js +36 -0
- package/dist/tools/foliage.d.ts +155 -0
- package/dist/tools/foliage.js +525 -0
- package/dist/tools/introspection.d.ts +98 -0
- package/dist/tools/introspection.js +683 -0
- package/dist/tools/landscape.d.ts +158 -0
- package/dist/tools/landscape.js +375 -0
- package/dist/tools/level.d.ts +110 -0
- package/dist/tools/level.js +362 -0
- package/dist/tools/lighting.d.ts +159 -0
- package/dist/tools/lighting.js +1179 -0
- package/dist/tools/materials.d.ts +34 -0
- package/dist/tools/materials.js +146 -0
- package/dist/tools/niagara.d.ts +145 -0
- package/dist/tools/niagara.js +289 -0
- package/dist/tools/performance.d.ts +163 -0
- package/dist/tools/performance.js +412 -0
- package/dist/tools/physics.d.ts +189 -0
- package/dist/tools/physics.js +784 -0
- package/dist/tools/rc.d.ts +110 -0
- package/dist/tools/rc.js +363 -0
- package/dist/tools/sequence.d.ts +112 -0
- package/dist/tools/sequence.js +675 -0
- package/dist/tools/tool-definitions.d.ts +4919 -0
- package/dist/tools/tool-definitions.js +891 -0
- package/dist/tools/tool-handlers.d.ts +47 -0
- package/dist/tools/tool-handlers.js +830 -0
- package/dist/tools/ui.d.ts +171 -0
- package/dist/tools/ui.js +337 -0
- package/dist/tools/visual.d.ts +29 -0
- package/dist/tools/visual.js +67 -0
- package/dist/types/env.d.ts +10 -0
- package/dist/types/env.js +18 -0
- package/dist/types/index.d.ts +323 -0
- package/dist/types/index.js +28 -0
- package/dist/types/tool-types.d.ts +274 -0
- package/dist/types/tool-types.js +13 -0
- package/dist/unreal-bridge.d.ts +126 -0
- package/dist/unreal-bridge.js +992 -0
- package/dist/utils/cache-manager.d.ts +64 -0
- package/dist/utils/cache-manager.js +176 -0
- package/dist/utils/error-handler.d.ts +66 -0
- package/dist/utils/error-handler.js +243 -0
- package/dist/utils/errors.d.ts +133 -0
- package/dist/utils/errors.js +256 -0
- package/dist/utils/http.d.ts +26 -0
- package/dist/utils/http.js +135 -0
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.js +32 -0
- package/dist/utils/normalize.d.ts +17 -0
- package/dist/utils/normalize.js +49 -0
- package/dist/utils/response-validator.d.ts +34 -0
- package/dist/utils/response-validator.js +121 -0
- package/dist/utils/safe-json.d.ts +4 -0
- package/dist/utils/safe-json.js +97 -0
- package/dist/utils/stdio-redirect.d.ts +2 -0
- package/dist/utils/stdio-redirect.js +20 -0
- package/dist/utils/validation.d.ts +50 -0
- package/dist/utils/validation.js +173 -0
- package/mcp-config-example.json +14 -0
- package/package.json +63 -0
- package/server.json +60 -0
- package/src/cli.ts +7 -0
- package/src/index.ts +543 -0
- package/src/prompts/index.ts +51 -0
- package/src/python/editor_compat.py +181 -0
- package/src/python-utils.ts +57 -0
- package/src/resources/actors.ts +92 -0
- package/src/resources/assets.ts +251 -0
- package/src/resources/levels.ts +83 -0
- package/src/tools/actors.ts +480 -0
- package/src/tools/animation.ts +713 -0
- package/src/tools/assets.ts +305 -0
- package/src/tools/audio.ts +548 -0
- package/src/tools/blueprint.ts +736 -0
- package/src/tools/build_environment_advanced.ts +526 -0
- package/src/tools/consolidated-tool-definitions.ts +619 -0
- package/src/tools/consolidated-tool-handlers.ts +1093 -0
- package/src/tools/debug.ts +368 -0
- package/src/tools/editor.ts +360 -0
- package/src/tools/engine.ts +32 -0
- package/src/tools/foliage.ts +652 -0
- package/src/tools/introspection.ts +778 -0
- package/src/tools/landscape.ts +523 -0
- package/src/tools/level.ts +410 -0
- package/src/tools/lighting.ts +1316 -0
- package/src/tools/materials.ts +148 -0
- package/src/tools/niagara.ts +312 -0
- package/src/tools/performance.ts +549 -0
- package/src/tools/physics.ts +924 -0
- package/src/tools/rc.ts +437 -0
- package/src/tools/sequence.ts +791 -0
- package/src/tools/tool-definitions.ts +907 -0
- package/src/tools/tool-handlers.ts +941 -0
- package/src/tools/ui.ts +499 -0
- package/src/tools/visual.ts +60 -0
- package/src/types/env.ts +27 -0
- package/src/types/index.ts +414 -0
- package/src/types/tool-types.ts +343 -0
- package/src/unreal-bridge.ts +1118 -0
- package/src/utils/cache-manager.ts +213 -0
- package/src/utils/error-handler.ts +320 -0
- package/src/utils/errors.ts +312 -0
- package/src/utils/http.ts +184 -0
- package/src/utils/logger.ts +30 -0
- package/src/utils/normalize.ts +54 -0
- package/src/utils/response-validator.ts +145 -0
- package/src/utils/safe-json.ts +112 -0
- package/src/utils/stdio-redirect.ts +18 -0
- package/src/utils/validation.ts +212 -0
- package/tsconfig.json +33 -0
package/src/tools/ui.ts
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
// UI tools for Unreal Engine
|
|
2
|
+
import { UnrealBridge } from '../unreal-bridge.js';
|
|
3
|
+
|
|
4
|
+
export class UITools {
|
|
5
|
+
constructor(private bridge: UnrealBridge) {}
|
|
6
|
+
|
|
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
|
+
// Create widget blueprint
|
|
21
|
+
async createWidget(params: {
|
|
22
|
+
name: string;
|
|
23
|
+
type?: 'HUD' | 'Menu' | 'Inventory' | 'Dialog' | 'Custom';
|
|
24
|
+
savePath?: string;
|
|
25
|
+
}) {
|
|
26
|
+
const path = params.savePath || '/Game/UI/Widgets';
|
|
27
|
+
const py = `
|
|
28
|
+
import unreal
|
|
29
|
+
import json
|
|
30
|
+
name = r"${params.name}"
|
|
31
|
+
path = r"${path}"
|
|
32
|
+
try:
|
|
33
|
+
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
|
|
34
|
+
try:
|
|
35
|
+
factory = unreal.WidgetBlueprintFactory()
|
|
36
|
+
except Exception:
|
|
37
|
+
factory = None
|
|
38
|
+
if not factory:
|
|
39
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'WidgetBlueprintFactory unavailable'}))
|
|
40
|
+
else:
|
|
41
|
+
# Try setting parent_class in a version-tolerant way
|
|
42
|
+
try:
|
|
43
|
+
factory.parent_class = unreal.UserWidget
|
|
44
|
+
except Exception:
|
|
45
|
+
try:
|
|
46
|
+
factory.set_editor_property('parent_class', unreal.UserWidget)
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
asset = asset_tools.create_asset(asset_name=name, package_path=path, asset_class=unreal.WidgetBlueprint, factory=factory)
|
|
50
|
+
if asset:
|
|
51
|
+
unreal.EditorAssetLibrary.save_asset(f"{path}/{name}")
|
|
52
|
+
print('RESULT:' + json.dumps({'success': True}))
|
|
53
|
+
else:
|
|
54
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'Failed to create WidgetBlueprint'}))
|
|
55
|
+
except Exception as e:
|
|
56
|
+
print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
|
|
57
|
+
`.trim();
|
|
58
|
+
try {
|
|
59
|
+
const resp = await this.bridge.executePython(py);
|
|
60
|
+
const out = typeof resp === 'string' ? resp : JSON.stringify(resp);
|
|
61
|
+
const m = out.match(/RESULT:({.*})/);
|
|
62
|
+
if (m) { try { const parsed = JSON.parse(m[1]); return parsed.success ? { success: true, message: 'Widget blueprint created' } : { success: false, error: parsed.error }; } catch {} }
|
|
63
|
+
return { success: true, message: 'Widget blueprint creation attempted' };
|
|
64
|
+
} catch (e) {
|
|
65
|
+
return { success: false, error: `Failed to create widget blueprint: ${e}` };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Add widget component
|
|
70
|
+
async addWidgetComponent(params: {
|
|
71
|
+
widgetName: string;
|
|
72
|
+
componentType: 'Button' | 'Text' | 'Image' | 'ProgressBar' | 'Slider' | 'CheckBox' | 'ComboBox' | 'TextBox' | 'ScrollBox' | 'Canvas' | 'VerticalBox' | 'HorizontalBox' | 'Grid' | 'Overlay';
|
|
73
|
+
componentName: string;
|
|
74
|
+
slot?: {
|
|
75
|
+
position?: [number, number];
|
|
76
|
+
size?: [number, number];
|
|
77
|
+
anchor?: [number, number, number, number];
|
|
78
|
+
alignment?: [number, number];
|
|
79
|
+
};
|
|
80
|
+
}) {
|
|
81
|
+
const commands = [];
|
|
82
|
+
|
|
83
|
+
commands.push(`AddWidgetComponent ${params.widgetName} ${params.componentType} ${params.componentName}`);
|
|
84
|
+
|
|
85
|
+
if (params.slot) {
|
|
86
|
+
if (params.slot.position) {
|
|
87
|
+
commands.push(`SetWidgetPosition ${params.widgetName}.${params.componentName} ${params.slot.position.join(' ')}`);
|
|
88
|
+
}
|
|
89
|
+
if (params.slot.size) {
|
|
90
|
+
commands.push(`SetWidgetSize ${params.widgetName}.${params.componentName} ${params.slot.size.join(' ')}`);
|
|
91
|
+
}
|
|
92
|
+
if (params.slot.anchor) {
|
|
93
|
+
commands.push(`SetWidgetAnchor ${params.widgetName}.${params.componentName} ${params.slot.anchor.join(' ')}`);
|
|
94
|
+
}
|
|
95
|
+
if (params.slot.alignment) {
|
|
96
|
+
commands.push(`SetWidgetAlignment ${params.widgetName}.${params.componentName} ${params.slot.alignment.join(' ')}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (const cmd of commands) {
|
|
101
|
+
await this.bridge.executeConsoleCommand(cmd);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { success: true, message: `Component ${params.componentName} added to widget` };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Set text
|
|
108
|
+
async setWidgetText(params: {
|
|
109
|
+
widgetName: string;
|
|
110
|
+
componentName: string;
|
|
111
|
+
text: string;
|
|
112
|
+
fontSize?: number;
|
|
113
|
+
color?: [number, number, number, number];
|
|
114
|
+
fontFamily?: string;
|
|
115
|
+
}) {
|
|
116
|
+
const commands = [];
|
|
117
|
+
|
|
118
|
+
commands.push(`SetWidgetText ${params.widgetName}.${params.componentName} "${params.text}"`);
|
|
119
|
+
|
|
120
|
+
if (params.fontSize !== undefined) {
|
|
121
|
+
commands.push(`SetWidgetFontSize ${params.widgetName}.${params.componentName} ${params.fontSize}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (params.color) {
|
|
125
|
+
commands.push(`SetWidgetTextColor ${params.widgetName}.${params.componentName} ${params.color.join(' ')}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (params.fontFamily) {
|
|
129
|
+
commands.push(`SetWidgetFont ${params.widgetName}.${params.componentName} ${params.fontFamily}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for (const cmd of commands) {
|
|
133
|
+
await this.bridge.executeConsoleCommand(cmd);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { success: true, message: 'Widget text updated' };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Set image
|
|
140
|
+
async setWidgetImage(params: {
|
|
141
|
+
widgetName: string;
|
|
142
|
+
componentName: string;
|
|
143
|
+
imagePath: string;
|
|
144
|
+
tint?: [number, number, number, number];
|
|
145
|
+
sizeToContent?: boolean;
|
|
146
|
+
}) {
|
|
147
|
+
const commands = [];
|
|
148
|
+
|
|
149
|
+
commands.push(`SetWidgetImage ${params.widgetName}.${params.componentName} ${params.imagePath}`);
|
|
150
|
+
|
|
151
|
+
if (params.tint) {
|
|
152
|
+
commands.push(`SetWidgetImageTint ${params.widgetName}.${params.componentName} ${params.tint.join(' ')}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (params.sizeToContent !== undefined) {
|
|
156
|
+
commands.push(`SetWidgetSizeToContent ${params.widgetName}.${params.componentName} ${params.sizeToContent}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (const cmd of commands) {
|
|
160
|
+
await this.bridge.executeConsoleCommand(cmd);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return { success: true, message: 'Widget image updated' };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Create HUD
|
|
167
|
+
async createHUD(params: {
|
|
168
|
+
name: string;
|
|
169
|
+
elements?: Array<{
|
|
170
|
+
type: 'HealthBar' | 'AmmoCounter' | 'Score' | 'Timer' | 'Minimap' | 'Crosshair';
|
|
171
|
+
position: [number, number];
|
|
172
|
+
size?: [number, number];
|
|
173
|
+
}>;
|
|
174
|
+
}) {
|
|
175
|
+
const commands = [];
|
|
176
|
+
|
|
177
|
+
commands.push(`CreateHUDClass ${params.name}`);
|
|
178
|
+
|
|
179
|
+
if (params.elements) {
|
|
180
|
+
for (const element of params.elements) {
|
|
181
|
+
const size = element.size || [100, 50];
|
|
182
|
+
commands.push(`AddHUDElement ${params.name} ${element.type} ${element.position.join(' ')} ${size.join(' ')}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for (const cmd of commands) {
|
|
187
|
+
await this.bridge.executeConsoleCommand(cmd);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { success: true, message: `HUD ${params.name} created` };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Show/Hide widget
|
|
194
|
+
async setWidgetVisibility(params: {
|
|
195
|
+
widgetName: string;
|
|
196
|
+
visible: boolean;
|
|
197
|
+
playerIndex?: number;
|
|
198
|
+
}) {
|
|
199
|
+
const playerIndex = params.playerIndex ?? 0;
|
|
200
|
+
const command = params.visible
|
|
201
|
+
? `ShowWidget ${params.widgetName} ${playerIndex}`
|
|
202
|
+
: `HideWidget ${params.widgetName} ${playerIndex}`;
|
|
203
|
+
|
|
204
|
+
return this.bridge.executeConsoleCommand(command);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Add widget to viewport
|
|
208
|
+
async addWidgetToViewport(params: {
|
|
209
|
+
widgetClass: string;
|
|
210
|
+
zOrder?: number;
|
|
211
|
+
playerIndex?: number;
|
|
212
|
+
}) {
|
|
213
|
+
const zOrder = params.zOrder ?? 0;
|
|
214
|
+
const playerIndex = params.playerIndex ?? 0;
|
|
215
|
+
|
|
216
|
+
// Use Python API to create and add widget to viewport
|
|
217
|
+
const py = `
|
|
218
|
+
import unreal
|
|
219
|
+
import json
|
|
220
|
+
widget_path = r"${params.widgetClass}"
|
|
221
|
+
z_order = ${zOrder}
|
|
222
|
+
player_index = ${playerIndex}
|
|
223
|
+
try:
|
|
224
|
+
# Load the widget blueprint class
|
|
225
|
+
if not unreal.EditorAssetLibrary.does_asset_exist(widget_path):
|
|
226
|
+
print('RESULT:' + json.dumps({'success': False, 'error': f'Widget class not found: {widget_path}'}))
|
|
227
|
+
else:
|
|
228
|
+
widget_bp = unreal.EditorAssetLibrary.load_asset(widget_path)
|
|
229
|
+
if not widget_bp:
|
|
230
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'Failed to load widget blueprint'}))
|
|
231
|
+
else:
|
|
232
|
+
# Get the generated class from the widget blueprint
|
|
233
|
+
widget_class = widget_bp.generated_class() if hasattr(widget_bp, 'generated_class') else widget_bp
|
|
234
|
+
|
|
235
|
+
# Get the world and player controller
|
|
236
|
+
world = unreal.EditorLevelLibrary.get_editor_world()
|
|
237
|
+
if not world:
|
|
238
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'No editor world available'}))
|
|
239
|
+
else:
|
|
240
|
+
# Try to get player controller
|
|
241
|
+
try:
|
|
242
|
+
player_controller = unreal.GameplayStatics.get_player_controller(world, player_index)
|
|
243
|
+
except Exception:
|
|
244
|
+
player_controller = None
|
|
245
|
+
|
|
246
|
+
if not player_controller:
|
|
247
|
+
# If no player controller in PIE, try to get the first one or create a dummy
|
|
248
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'No player controller available. Run in PIE mode first.'}))
|
|
249
|
+
else:
|
|
250
|
+
# Create the widget
|
|
251
|
+
widget = unreal.WidgetBlueprintLibrary.create(world, widget_class, player_controller)
|
|
252
|
+
if widget:
|
|
253
|
+
# Add to viewport
|
|
254
|
+
widget.add_to_viewport(z_order)
|
|
255
|
+
print('RESULT:' + json.dumps({'success': True}))
|
|
256
|
+
else:
|
|
257
|
+
print('RESULT:' + json.dumps({'success': False, 'error': 'Failed to create widget instance'}))
|
|
258
|
+
except Exception as e:
|
|
259
|
+
print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
|
|
260
|
+
`.trim();
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
const resp = await this.bridge.executePython(py);
|
|
264
|
+
const out = typeof resp === 'string' ? resp : JSON.stringify(resp);
|
|
265
|
+
const m = out.match(/RESULT:({.*})/);
|
|
266
|
+
if (m) {
|
|
267
|
+
try {
|
|
268
|
+
const parsed = JSON.parse(m[1]);
|
|
269
|
+
return parsed.success
|
|
270
|
+
? { success: true, message: `Widget added to viewport with z-order ${zOrder}` }
|
|
271
|
+
: { success: false, error: parsed.error };
|
|
272
|
+
} catch {}
|
|
273
|
+
}
|
|
274
|
+
return { success: true, message: 'Widget add to viewport attempted' };
|
|
275
|
+
} catch (e) {
|
|
276
|
+
return { success: false, error: `Failed to add widget to viewport: ${e}` };
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Remove widget from viewport
|
|
281
|
+
async removeWidgetFromViewport(params: {
|
|
282
|
+
widgetName: string;
|
|
283
|
+
playerIndex?: number;
|
|
284
|
+
}) {
|
|
285
|
+
const playerIndex = params.playerIndex ?? 0;
|
|
286
|
+
const command = `RemoveWidgetFromViewport ${params.widgetName} ${playerIndex}`;
|
|
287
|
+
return this.bridge.executeConsoleCommand(command);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Create menu
|
|
291
|
+
async createMenu(params: {
|
|
292
|
+
name: string;
|
|
293
|
+
menuType: 'Main' | 'Pause' | 'Settings' | 'Inventory';
|
|
294
|
+
buttons?: Array<{
|
|
295
|
+
text: string;
|
|
296
|
+
action: string;
|
|
297
|
+
position?: [number, number];
|
|
298
|
+
}>;
|
|
299
|
+
}) {
|
|
300
|
+
const commands = [];
|
|
301
|
+
|
|
302
|
+
commands.push(`CreateMenuWidget ${params.name} ${params.menuType}`);
|
|
303
|
+
|
|
304
|
+
if (params.buttons) {
|
|
305
|
+
for (const button of params.buttons) {
|
|
306
|
+
const pos = button.position || [0, 0];
|
|
307
|
+
commands.push(`AddMenuButton ${params.name} "${button.text}" ${button.action} ${pos.join(' ')}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
for (const cmd of commands) {
|
|
312
|
+
await this.bridge.executeConsoleCommand(cmd);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return { success: true, message: `Menu ${params.name} created` };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Set widget animation
|
|
319
|
+
async createWidgetAnimation(params: {
|
|
320
|
+
widgetName: string;
|
|
321
|
+
animationName: string;
|
|
322
|
+
duration: number;
|
|
323
|
+
tracks?: Array<{
|
|
324
|
+
componentName: string;
|
|
325
|
+
property: 'Position' | 'Scale' | 'Rotation' | 'Opacity' | 'Color';
|
|
326
|
+
keyframes: Array<{
|
|
327
|
+
time: number;
|
|
328
|
+
value: number | [number, number] | [number, number, number] | [number, number, number, number];
|
|
329
|
+
}>;
|
|
330
|
+
}>;
|
|
331
|
+
}) {
|
|
332
|
+
const commands = [];
|
|
333
|
+
|
|
334
|
+
commands.push(`CreateWidgetAnimation ${params.widgetName} ${params.animationName} ${params.duration}`);
|
|
335
|
+
|
|
336
|
+
if (params.tracks) {
|
|
337
|
+
for (const track of params.tracks) {
|
|
338
|
+
commands.push(`AddAnimationTrack ${params.widgetName}.${params.animationName} ${track.componentName} ${track.property}`);
|
|
339
|
+
|
|
340
|
+
for (const keyframe of track.keyframes) {
|
|
341
|
+
const value = Array.isArray(keyframe.value) ? keyframe.value.join(' ') : keyframe.value;
|
|
342
|
+
commands.push(`AddAnimationKeyframe ${params.widgetName}.${params.animationName} ${track.componentName} ${keyframe.time} ${value}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
for (const cmd of commands) {
|
|
348
|
+
await this.bridge.executeConsoleCommand(cmd);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return { success: true, message: `Animation ${params.animationName} created` };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Play widget animation
|
|
355
|
+
async playWidgetAnimation(params: {
|
|
356
|
+
widgetName: string;
|
|
357
|
+
animationName: string;
|
|
358
|
+
playMode?: 'Forward' | 'Reverse' | 'PingPong';
|
|
359
|
+
loops?: number;
|
|
360
|
+
}) {
|
|
361
|
+
const playMode = params.playMode || 'Forward';
|
|
362
|
+
const loops = params.loops ?? 1;
|
|
363
|
+
|
|
364
|
+
const command = `PlayWidgetAnimation ${params.widgetName} ${params.animationName} ${playMode} ${loops}`;
|
|
365
|
+
return this.bridge.executeConsoleCommand(command);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Set widget style
|
|
369
|
+
async setWidgetStyle(params: {
|
|
370
|
+
widgetName: string;
|
|
371
|
+
componentName: string;
|
|
372
|
+
style: {
|
|
373
|
+
backgroundColor?: [number, number, number, number];
|
|
374
|
+
borderColor?: [number, number, number, number];
|
|
375
|
+
borderWidth?: number;
|
|
376
|
+
padding?: [number, number, number, number];
|
|
377
|
+
margin?: [number, number, number, number];
|
|
378
|
+
};
|
|
379
|
+
}) {
|
|
380
|
+
const commands = [];
|
|
381
|
+
|
|
382
|
+
if (params.style.backgroundColor) {
|
|
383
|
+
commands.push(`SetWidgetBackgroundColor ${params.widgetName}.${params.componentName} ${params.style.backgroundColor.join(' ')}`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (params.style.borderColor) {
|
|
387
|
+
commands.push(`SetWidgetBorderColor ${params.widgetName}.${params.componentName} ${params.style.borderColor.join(' ')}`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (params.style.borderWidth !== undefined) {
|
|
391
|
+
commands.push(`SetWidgetBorderWidth ${params.widgetName}.${params.componentName} ${params.style.borderWidth}`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (params.style.padding) {
|
|
395
|
+
commands.push(`SetWidgetPadding ${params.widgetName}.${params.componentName} ${params.style.padding.join(' ')}`);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (params.style.margin) {
|
|
399
|
+
commands.push(`SetWidgetMargin ${params.widgetName}.${params.componentName} ${params.style.margin.join(' ')}`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
for (const cmd of commands) {
|
|
403
|
+
await this.bridge.executeConsoleCommand(cmd);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return { success: true, message: 'Widget style updated' };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Bind widget event
|
|
410
|
+
async bindWidgetEvent(params: {
|
|
411
|
+
widgetName: string;
|
|
412
|
+
componentName: string;
|
|
413
|
+
eventType: 'OnClicked' | 'OnPressed' | 'OnReleased' | 'OnHovered' | 'OnUnhovered' | 'OnTextChanged' | 'OnTextCommitted' | 'OnValueChanged';
|
|
414
|
+
functionName: string;
|
|
415
|
+
}) {
|
|
416
|
+
const command = `BindWidgetEvent ${params.widgetName}.${params.componentName} ${params.eventType} ${params.functionName}`;
|
|
417
|
+
return this.bridge.executeConsoleCommand(command);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Set input mode
|
|
421
|
+
async setInputMode(params: {
|
|
422
|
+
mode: 'GameOnly' | 'UIOnly' | 'GameAndUI';
|
|
423
|
+
showCursor?: boolean;
|
|
424
|
+
lockCursor?: boolean;
|
|
425
|
+
}) {
|
|
426
|
+
const commands = [];
|
|
427
|
+
|
|
428
|
+
commands.push(`SetInputMode ${params.mode}`);
|
|
429
|
+
|
|
430
|
+
if (params.showCursor !== undefined) {
|
|
431
|
+
commands.push(`ShowMouseCursor ${params.showCursor}`);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (params.lockCursor !== undefined) {
|
|
435
|
+
commands.push(`SetMouseLockMode ${params.lockCursor}`);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
for (const cmd of commands) {
|
|
439
|
+
await this.bridge.executeConsoleCommand(cmd);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return { success: true, message: `Input mode set to ${params.mode}` };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Create tooltip
|
|
446
|
+
async createTooltip(params: {
|
|
447
|
+
widgetName: string;
|
|
448
|
+
componentName: string;
|
|
449
|
+
text: string;
|
|
450
|
+
delay?: number;
|
|
451
|
+
}) {
|
|
452
|
+
const delay = params.delay ?? 0.5;
|
|
453
|
+
const command = `SetWidgetTooltip ${params.widgetName}.${params.componentName} "${params.text}" ${delay}`;
|
|
454
|
+
return this.bridge.executeConsoleCommand(command);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Create drag and drop
|
|
458
|
+
async setupDragDrop(params: {
|
|
459
|
+
widgetName: string;
|
|
460
|
+
componentName: string;
|
|
461
|
+
dragVisual?: string;
|
|
462
|
+
dropTargets?: string[];
|
|
463
|
+
}) {
|
|
464
|
+
const commands = [];
|
|
465
|
+
|
|
466
|
+
commands.push(`EnableDragDrop ${params.widgetName}.${params.componentName}`);
|
|
467
|
+
|
|
468
|
+
if (params.dragVisual) {
|
|
469
|
+
commands.push(`SetDragVisual ${params.widgetName}.${params.componentName} ${params.dragVisual}`);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (params.dropTargets) {
|
|
473
|
+
for (const target of params.dropTargets) {
|
|
474
|
+
commands.push(`AddDropTarget ${params.widgetName}.${params.componentName} ${target}`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
for (const cmd of commands) {
|
|
479
|
+
await this.bridge.executeConsoleCommand(cmd);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return { success: true, message: 'Drag and drop configured' };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Create notification
|
|
486
|
+
async showNotification(params: {
|
|
487
|
+
text: string;
|
|
488
|
+
duration?: number;
|
|
489
|
+
type?: 'Info' | 'Success' | 'Warning' | 'Error';
|
|
490
|
+
position?: 'TopLeft' | 'TopCenter' | 'TopRight' | 'BottomLeft' | 'BottomCenter' | 'BottomRight';
|
|
491
|
+
}) {
|
|
492
|
+
const duration = params.duration ?? 3.0;
|
|
493
|
+
const type = params.type || 'Info';
|
|
494
|
+
const position = params.position || 'TopRight';
|
|
495
|
+
|
|
496
|
+
const command = `ShowNotification "${params.text}" ${duration} ${type} ${position}`;
|
|
497
|
+
return this.bridge.executeConsoleCommand(command);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { UnrealBridge } from '../unreal-bridge.js';
|
|
2
|
+
import { loadEnv } from '../types/env.js';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
export class VisualTools {
|
|
7
|
+
private env = loadEnv();
|
|
8
|
+
constructor(private bridge: UnrealBridge) {}
|
|
9
|
+
|
|
10
|
+
// Take a screenshot of viewport (high res or standard). Returns path and base64 (truncated)
|
|
11
|
+
async takeScreenshot(params: { resolution?: string }) {
|
|
12
|
+
const res = params.resolution && /^\d+x\d+$/i.test(params.resolution) ? params.resolution : '';
|
|
13
|
+
const cmd = res ? `HighResShot ${res}` : 'HighResShot';
|
|
14
|
+
try {
|
|
15
|
+
await this.bridge.executeConsoleCommand(cmd);
|
|
16
|
+
// Give the engine a moment to write the file
|
|
17
|
+
await new Promise(r => setTimeout(r, 1200));
|
|
18
|
+
const p = await this.findLatestScreenshot();
|
|
19
|
+
if (!p) return { success: true, message: 'Screenshot triggered, but could not locate output file' };
|
|
20
|
+
let b64: string | undefined;
|
|
21
|
+
try {
|
|
22
|
+
const buf = fs.readFileSync(p);
|
|
23
|
+
// Limit to ~1MB to avoid huge responses
|
|
24
|
+
const max = 1024 * 1024;
|
|
25
|
+
b64 = buf.length > max ? buf.subarray(0, max).toString('base64') : buf.toString('base64');
|
|
26
|
+
} catch {}
|
|
27
|
+
return { success: true, imagePath: p, imageBase64: b64 };
|
|
28
|
+
} catch (err: any) {
|
|
29
|
+
return { success: false, error: String(err?.message || err) };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private async findLatestScreenshot(): Promise<string | null> {
|
|
34
|
+
// Try env override, otherwise look in common UE Saved/Screenshots folder under project
|
|
35
|
+
const candidates: string[] = [];
|
|
36
|
+
if (this.env.UE_SCREENSHOT_DIR) candidates.push(this.env.UE_SCREENSHOT_DIR);
|
|
37
|
+
if (this.env.UE_PROJECT_PATH) {
|
|
38
|
+
const projectDir = path.dirname(this.env.UE_PROJECT_PATH);
|
|
39
|
+
candidates.push(path.join(projectDir, 'Saved', 'Screenshots'));
|
|
40
|
+
candidates.push(path.join(projectDir, 'Saved', 'Screenshots', 'Windows'));
|
|
41
|
+
candidates.push(path.join(projectDir, 'Saved', 'Screenshots', 'WindowsEditor'));
|
|
42
|
+
}
|
|
43
|
+
// Fallback: common locations
|
|
44
|
+
candidates.push(path.join(process.cwd(), 'Saved', 'Screenshots'));
|
|
45
|
+
candidates.push(path.join(process.cwd(), 'Saved', 'Screenshots', 'Windows'));
|
|
46
|
+
candidates.push(path.join(process.cwd(), 'Saved', 'Screenshots', 'WindowsEditor'));
|
|
47
|
+
let latest: { path: string; mtime: number } | null = null;
|
|
48
|
+
for (const c of candidates) {
|
|
49
|
+
try {
|
|
50
|
+
const entries = fs.readdirSync(c).map(f => path.join(c, f));
|
|
51
|
+
for (const fp of entries) {
|
|
52
|
+
if (!/\.png$/i.test(fp)) continue;
|
|
53
|
+
const st = fs.statSync(fp);
|
|
54
|
+
if (!latest || st.mtimeMs > latest.mtime) latest = { path: fp, mtime: st.mtimeMs };
|
|
55
|
+
}
|
|
56
|
+
} catch {}
|
|
57
|
+
}
|
|
58
|
+
return latest?.path || null;
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/types/env.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface Env {
|
|
2
|
+
UE_HOST: string;
|
|
3
|
+
UE_RC_WS_PORT: number;
|
|
4
|
+
UE_RC_HTTP_PORT: number;
|
|
5
|
+
UE_PROJECT_PATH?: string;
|
|
6
|
+
UE_EDITOR_EXE?: string;
|
|
7
|
+
UE_SCREENSHOT_DIR?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function loadEnv(): Env {
|
|
11
|
+
const host = process.env.UE_HOST || '127.0.0.1';
|
|
12
|
+
// Note: UE5 default is HTTP on 30010, WebSocket on 30020
|
|
13
|
+
const wsPort = Number(process.env.UE_RC_WS_PORT || process.env.UE_REMOTE_CONTROL_WS_PORT || 30020);
|
|
14
|
+
const httpPort = Number(process.env.UE_RC_HTTP_PORT || process.env.UE_REMOTE_CONTROL_HTTP_PORT || 30010);
|
|
15
|
+
const projectPath = process.env.UE_PROJECT_PATH;
|
|
16
|
+
const editorExe = process.env.UE_EDITOR_EXE;
|
|
17
|
+
const screenshotDir = process.env.UE_SCREENSHOT_DIR;
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
UE_HOST: host,
|
|
21
|
+
UE_RC_WS_PORT: wsPort,
|
|
22
|
+
UE_RC_HTTP_PORT: httpPort,
|
|
23
|
+
UE_PROJECT_PATH: projectPath,
|
|
24
|
+
UE_EDITOR_EXE: editorExe,
|
|
25
|
+
UE_SCREENSHOT_DIR: screenshotDir,
|
|
26
|
+
};
|
|
27
|
+
}
|