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.
Files changed (155) hide show
  1. package/.dockerignore +57 -0
  2. package/.env.production +25 -0
  3. package/.eslintrc.json +54 -0
  4. package/.github/workflows/publish-mcp.yml +75 -0
  5. package/Dockerfile +54 -0
  6. package/LICENSE +21 -0
  7. package/Public/icon.png +0 -0
  8. package/README.md +209 -0
  9. package/claude_desktop_config_example.json +13 -0
  10. package/dist/cli.d.ts +3 -0
  11. package/dist/cli.js +7 -0
  12. package/dist/index.d.ts +31 -0
  13. package/dist/index.js +484 -0
  14. package/dist/prompts/index.d.ts +14 -0
  15. package/dist/prompts/index.js +38 -0
  16. package/dist/python-utils.d.ts +29 -0
  17. package/dist/python-utils.js +54 -0
  18. package/dist/resources/actors.d.ts +13 -0
  19. package/dist/resources/actors.js +83 -0
  20. package/dist/resources/assets.d.ts +23 -0
  21. package/dist/resources/assets.js +245 -0
  22. package/dist/resources/levels.d.ts +17 -0
  23. package/dist/resources/levels.js +94 -0
  24. package/dist/tools/actors.d.ts +51 -0
  25. package/dist/tools/actors.js +459 -0
  26. package/dist/tools/animation.d.ts +196 -0
  27. package/dist/tools/animation.js +579 -0
  28. package/dist/tools/assets.d.ts +21 -0
  29. package/dist/tools/assets.js +304 -0
  30. package/dist/tools/audio.d.ts +170 -0
  31. package/dist/tools/audio.js +416 -0
  32. package/dist/tools/blueprint.d.ts +144 -0
  33. package/dist/tools/blueprint.js +652 -0
  34. package/dist/tools/build_environment_advanced.d.ts +66 -0
  35. package/dist/tools/build_environment_advanced.js +484 -0
  36. package/dist/tools/consolidated-tool-definitions.d.ts +2598 -0
  37. package/dist/tools/consolidated-tool-definitions.js +607 -0
  38. package/dist/tools/consolidated-tool-handlers.d.ts +2 -0
  39. package/dist/tools/consolidated-tool-handlers.js +1050 -0
  40. package/dist/tools/debug.d.ts +185 -0
  41. package/dist/tools/debug.js +265 -0
  42. package/dist/tools/editor.d.ts +88 -0
  43. package/dist/tools/editor.js +365 -0
  44. package/dist/tools/engine.d.ts +30 -0
  45. package/dist/tools/engine.js +36 -0
  46. package/dist/tools/foliage.d.ts +155 -0
  47. package/dist/tools/foliage.js +525 -0
  48. package/dist/tools/introspection.d.ts +98 -0
  49. package/dist/tools/introspection.js +683 -0
  50. package/dist/tools/landscape.d.ts +158 -0
  51. package/dist/tools/landscape.js +375 -0
  52. package/dist/tools/level.d.ts +110 -0
  53. package/dist/tools/level.js +362 -0
  54. package/dist/tools/lighting.d.ts +159 -0
  55. package/dist/tools/lighting.js +1179 -0
  56. package/dist/tools/materials.d.ts +34 -0
  57. package/dist/tools/materials.js +146 -0
  58. package/dist/tools/niagara.d.ts +145 -0
  59. package/dist/tools/niagara.js +289 -0
  60. package/dist/tools/performance.d.ts +163 -0
  61. package/dist/tools/performance.js +412 -0
  62. package/dist/tools/physics.d.ts +189 -0
  63. package/dist/tools/physics.js +784 -0
  64. package/dist/tools/rc.d.ts +110 -0
  65. package/dist/tools/rc.js +363 -0
  66. package/dist/tools/sequence.d.ts +112 -0
  67. package/dist/tools/sequence.js +675 -0
  68. package/dist/tools/tool-definitions.d.ts +4919 -0
  69. package/dist/tools/tool-definitions.js +891 -0
  70. package/dist/tools/tool-handlers.d.ts +47 -0
  71. package/dist/tools/tool-handlers.js +830 -0
  72. package/dist/tools/ui.d.ts +171 -0
  73. package/dist/tools/ui.js +337 -0
  74. package/dist/tools/visual.d.ts +29 -0
  75. package/dist/tools/visual.js +67 -0
  76. package/dist/types/env.d.ts +10 -0
  77. package/dist/types/env.js +18 -0
  78. package/dist/types/index.d.ts +323 -0
  79. package/dist/types/index.js +28 -0
  80. package/dist/types/tool-types.d.ts +274 -0
  81. package/dist/types/tool-types.js +13 -0
  82. package/dist/unreal-bridge.d.ts +126 -0
  83. package/dist/unreal-bridge.js +992 -0
  84. package/dist/utils/cache-manager.d.ts +64 -0
  85. package/dist/utils/cache-manager.js +176 -0
  86. package/dist/utils/error-handler.d.ts +66 -0
  87. package/dist/utils/error-handler.js +243 -0
  88. package/dist/utils/errors.d.ts +133 -0
  89. package/dist/utils/errors.js +256 -0
  90. package/dist/utils/http.d.ts +26 -0
  91. package/dist/utils/http.js +135 -0
  92. package/dist/utils/logger.d.ts +12 -0
  93. package/dist/utils/logger.js +32 -0
  94. package/dist/utils/normalize.d.ts +17 -0
  95. package/dist/utils/normalize.js +49 -0
  96. package/dist/utils/response-validator.d.ts +34 -0
  97. package/dist/utils/response-validator.js +121 -0
  98. package/dist/utils/safe-json.d.ts +4 -0
  99. package/dist/utils/safe-json.js +97 -0
  100. package/dist/utils/stdio-redirect.d.ts +2 -0
  101. package/dist/utils/stdio-redirect.js +20 -0
  102. package/dist/utils/validation.d.ts +50 -0
  103. package/dist/utils/validation.js +173 -0
  104. package/mcp-config-example.json +14 -0
  105. package/package.json +63 -0
  106. package/server.json +60 -0
  107. package/src/cli.ts +7 -0
  108. package/src/index.ts +543 -0
  109. package/src/prompts/index.ts +51 -0
  110. package/src/python/editor_compat.py +181 -0
  111. package/src/python-utils.ts +57 -0
  112. package/src/resources/actors.ts +92 -0
  113. package/src/resources/assets.ts +251 -0
  114. package/src/resources/levels.ts +83 -0
  115. package/src/tools/actors.ts +480 -0
  116. package/src/tools/animation.ts +713 -0
  117. package/src/tools/assets.ts +305 -0
  118. package/src/tools/audio.ts +548 -0
  119. package/src/tools/blueprint.ts +736 -0
  120. package/src/tools/build_environment_advanced.ts +526 -0
  121. package/src/tools/consolidated-tool-definitions.ts +619 -0
  122. package/src/tools/consolidated-tool-handlers.ts +1093 -0
  123. package/src/tools/debug.ts +368 -0
  124. package/src/tools/editor.ts +360 -0
  125. package/src/tools/engine.ts +32 -0
  126. package/src/tools/foliage.ts +652 -0
  127. package/src/tools/introspection.ts +778 -0
  128. package/src/tools/landscape.ts +523 -0
  129. package/src/tools/level.ts +410 -0
  130. package/src/tools/lighting.ts +1316 -0
  131. package/src/tools/materials.ts +148 -0
  132. package/src/tools/niagara.ts +312 -0
  133. package/src/tools/performance.ts +549 -0
  134. package/src/tools/physics.ts +924 -0
  135. package/src/tools/rc.ts +437 -0
  136. package/src/tools/sequence.ts +791 -0
  137. package/src/tools/tool-definitions.ts +907 -0
  138. package/src/tools/tool-handlers.ts +941 -0
  139. package/src/tools/ui.ts +499 -0
  140. package/src/tools/visual.ts +60 -0
  141. package/src/types/env.ts +27 -0
  142. package/src/types/index.ts +414 -0
  143. package/src/types/tool-types.ts +343 -0
  144. package/src/unreal-bridge.ts +1118 -0
  145. package/src/utils/cache-manager.ts +213 -0
  146. package/src/utils/error-handler.ts +320 -0
  147. package/src/utils/errors.ts +312 -0
  148. package/src/utils/http.ts +184 -0
  149. package/src/utils/logger.ts +30 -0
  150. package/src/utils/normalize.ts +54 -0
  151. package/src/utils/response-validator.ts +145 -0
  152. package/src/utils/safe-json.ts +112 -0
  153. package/src/utils/stdio-redirect.ts +18 -0
  154. package/src/utils/validation.ts +212 -0
  155. package/tsconfig.json +33 -0
@@ -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
+ }
@@ -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
+ }