// UI tools for Unreal Engine
import { UnrealBridge } from '../unreal-bridge.js';
import { bestEffortInterpretedText, interpretStandardResult } from '../utils/result-helpers.js';
export class UITools {
constructor(private bridge: UnrealBridge) {}
// Create widget blueprint
async createWidget(params: {
name: string;
type?: 'HUD' | 'Menu' | 'Inventory' | 'Dialog' | 'Custom';
savePath?: string;
}) {
const path = params.savePath || '/Game/UI/Widgets';
const py = `
import unreal
import json
name = r"${params.name}"
path = r"${path}"
try:
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
try:
factory = unreal.WidgetBlueprintFactory()
except Exception:
factory = None
if not factory:
print('RESULT:' + json.dumps({'success': False, 'error': 'WidgetBlueprintFactory unavailable'}))
else:
# Try setting parent_class in a version-tolerant way
try:
factory.parent_class = unreal.UserWidget
except Exception:
try:
factory.set_editor_property('parent_class', unreal.UserWidget)
except Exception:
pass
asset = asset_tools.create_asset(asset_name=name, package_path=path, asset_class=unreal.WidgetBlueprint, factory=factory)
if asset:
unreal.EditorAssetLibrary.save_asset(f"{path}/{name}")
print('RESULT:' + json.dumps({'success': True}))
else:
print('RESULT:' + json.dumps({'success': False, 'error': 'Failed to create WidgetBlueprint'}))
except Exception as e:
print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
`.trim();
try {
const resp = await this.bridge.executePython(py);
const interpreted = interpretStandardResult(resp, {
successMessage: 'Widget blueprint created',
failureMessage: 'Failed to create WidgetBlueprint'
});
if (interpreted.success) {
return { success: true, message: interpreted.message };
}
return {
success: false,
error: interpreted.error ?? 'Failed to create widget blueprint',
details: bestEffortInterpretedText(interpreted)
};
} catch (e) {
return { success: false, error: `Failed to create widget blueprint: ${e}` };
}
}
// Add widget component
async addWidgetComponent(params: {
widgetName: string;
componentType: 'Button' | 'Text' | 'Image' | 'ProgressBar' | 'Slider' | 'CheckBox' | 'ComboBox' | 'TextBox' | 'ScrollBox' | 'Canvas' | 'VerticalBox' | 'HorizontalBox' | 'Grid' | 'Overlay';
componentName: string;
slot?: {
position?: [number, number];
size?: [number, number];
anchor?: [number, number, number, number];
alignment?: [number, number];
};
}) {
const commands: string[] = [];
commands.push(`AddWidgetComponent ${params.widgetName} ${params.componentType} ${params.componentName}`);
if (params.slot) {
if (params.slot.position) {
commands.push(`SetWidgetPosition ${params.widgetName}.${params.componentName} ${params.slot.position.join(' ')}`);
}
if (params.slot.size) {
commands.push(`SetWidgetSize ${params.widgetName}.${params.componentName} ${params.slot.size.join(' ')}`);
}
if (params.slot.anchor) {
commands.push(`SetWidgetAnchor ${params.widgetName}.${params.componentName} ${params.slot.anchor.join(' ')}`);
}
if (params.slot.alignment) {
commands.push(`SetWidgetAlignment ${params.widgetName}.${params.componentName} ${params.slot.alignment.join(' ')}`);
}
}
await this.bridge.executeConsoleCommands(commands);
return { success: true, message: `Component ${params.componentName} added to widget` };
}
// Set text
async setWidgetText(params: {
widgetName: string;
componentName: string;
text: string;
fontSize?: number;
color?: [number, number, number, number];
fontFamily?: string;
}) {
const commands: string[] = [];
commands.push(`SetWidgetText ${params.widgetName}.${params.componentName} "${params.text}"`);
if (params.fontSize !== undefined) {
commands.push(`SetWidgetFontSize ${params.widgetName}.${params.componentName} ${params.fontSize}`);
}
if (params.color) {
commands.push(`SetWidgetTextColor ${params.widgetName}.${params.componentName} ${params.color.join(' ')}`);
}
if (params.fontFamily) {
commands.push(`SetWidgetFont ${params.widgetName}.${params.componentName} ${params.fontFamily}`);
}
await this.bridge.executeConsoleCommands(commands);
return { success: true, message: 'Widget text updated' };
}
// Set image
async setWidgetImage(params: {
widgetName: string;
componentName: string;
imagePath: string;
tint?: [number, number, number, number];
sizeToContent?: boolean;
}) {
const commands: string[] = [];
commands.push(`SetWidgetImage ${params.widgetName}.${params.componentName} ${params.imagePath}`);
if (params.tint) {
commands.push(`SetWidgetImageTint ${params.widgetName}.${params.componentName} ${params.tint.join(' ')}`);
}
if (params.sizeToContent !== undefined) {
commands.push(`SetWidgetSizeToContent ${params.widgetName}.${params.componentName} ${params.sizeToContent}`);
}
await this.bridge.executeConsoleCommands(commands);
return { success: true, message: 'Widget image updated' };
}
// Create HUD
async createHUD(params: {
name: string;
elements?: Array<{
type: 'HealthBar' | 'AmmoCounter' | 'Score' | 'Timer' | 'Minimap' | 'Crosshair';
position: [number, number];
size?: [number, number];
}>;
}) {
const commands: string[] = [];
commands.push(`CreateHUDClass ${params.name}`);
if (params.elements) {
for (const element of params.elements) {
const size = element.size || [100, 50];
commands.push(`AddHUDElement ${params.name} ${element.type} ${element.position.join(' ')} ${size.join(' ')}`);
}
}
await this.bridge.executeConsoleCommands(commands);
return { success: true, message: `HUD ${params.name} created` };
}
// Show/Hide widget
async setWidgetVisibility(params: {
widgetName: string;
visible: boolean;
playerIndex?: number;
}) {
const playerIndex = params.playerIndex ?? 0;
const widgetName = params.widgetName?.trim();
if (!widgetName) {
return { success: false, error: 'widgetName is required' };
}
const verifyScript = `
import unreal, json
name = r"${widgetName}"
candidates = []
if name.startswith('/Game/'):
candidates.append(name)
else:
candidates.append(f"/Game/UI/Widgets/{name}")
candidates.append(f"/Game/{name}")
found_path = ''
for path in candidates:
if unreal.EditorAssetLibrary.does_asset_exist(path):
found_path = path
break
print('RESULT:' + json.dumps({'success': bool(found_path), 'path': found_path, 'candidates': candidates}))
`.trim();
const verify = await this.bridge.executePythonWithResult(verifyScript);
if (!verify?.success) {
return { success: false, error: `Widget asset not found for ${widgetName}` };
}
const command = params.visible
? `ShowWidget ${widgetName} ${playerIndex}`
: `HideWidget ${widgetName} ${playerIndex}`;
const raw = await this.bridge.executeConsoleCommand(command);
const summary = this.bridge.summarizeConsoleCommand(command, raw);
return {
success: true,
message: params.visible ? `Widget ${widgetName} show command issued` : `Widget ${widgetName} hide command issued`,
command: summary.command,
output: summary.output || undefined,
logLines: summary.logLines?.length ? summary.logLines : undefined
};
}
// Add widget to viewport
async addWidgetToViewport(params: {
widgetClass: string;
zOrder?: number;
playerIndex?: number;
}) {
const zOrder = params.zOrder ?? 0;
const playerIndex = params.playerIndex ?? 0;
// Use Python API to create and add widget to viewport
const py = `
import unreal
import json
widget_path = r"${params.widgetClass}"
z_order = ${zOrder}
player_index = ${playerIndex}
try:
# Load the widget blueprint class
if not unreal.EditorAssetLibrary.does_asset_exist(widget_path):
print('RESULT:' + json.dumps({'success': False, 'error': f'Widget class not found: {widget_path}'}))
else:
widget_bp = unreal.EditorAssetLibrary.load_asset(widget_path)
if not widget_bp:
print('RESULT:' + json.dumps({'success': False, 'error': 'Failed to load widget blueprint'}))
else:
# Get the generated class from the widget blueprint
widget_class = widget_bp.generated_class() if hasattr(widget_bp, 'generated_class') else widget_bp
# Get the world and player controller via modern subsystems
world = None
try:
world = unreal.EditorUtilityLibrary.get_editor_world()
except Exception:
pass
if not world:
editor_subsystem = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
if editor_subsystem and hasattr(editor_subsystem, 'get_editor_world'):
world = editor_subsystem.get_editor_world()
if not world:
print('RESULT:' + json.dumps({'success': False, 'error': 'No editor world available. Start a PIE session or enable Editor Scripting Utilities.'}))
else:
# Try to get player controller
try:
player_controller = unreal.GameplayStatics.get_player_controller(world, player_index)
except Exception:
player_controller = None
if not player_controller:
# If no player controller in PIE, try to get the first one or create a dummy
print('RESULT:' + json.dumps({'success': False, 'error': 'No player controller available. Run in PIE mode first.'}))
else:
# Create the widget
widget = unreal.WidgetBlueprintLibrary.create(world, widget_class, player_controller)
if widget:
# Add to viewport
widget.add_to_viewport(z_order)
print('RESULT:' + json.dumps({'success': True}))
else:
print('RESULT:' + json.dumps({'success': False, 'error': 'Failed to create widget instance'}))
except Exception as e:
print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
`.trim();
try {
const resp = await this.bridge.executePython(py);
const interpreted = interpretStandardResult(resp, {
successMessage: `Widget added to viewport with z-order ${zOrder}`,
failureMessage: 'Failed to add widget to viewport'
});
if (interpreted.success) {
return { success: true, message: interpreted.message };
}
return {
success: false,
error: interpreted.error ?? 'Failed to add widget to viewport',
details: bestEffortInterpretedText(interpreted)
};
} catch (e) {
return { success: false, error: `Failed to add widget to viewport: ${e}` };
}
}
// Remove widget from viewport
async removeWidgetFromViewport(params: {
widgetName: string;
playerIndex?: number;
}) {
const playerIndex = params.playerIndex ?? 0;
const command = `RemoveWidgetFromViewport ${params.widgetName} ${playerIndex}`;
return this.bridge.executeConsoleCommand(command);
}
// Create menu
async createMenu(params: {
name: string;
menuType: 'Main' | 'Pause' | 'Settings' | 'Inventory';
buttons?: Array<{
text: string;
action: string;
position?: [number, number];
}>;
}) {
const commands: string[] = [];
commands.push(`CreateMenuWidget ${params.name} ${params.menuType}`);
if (params.buttons) {
for (const button of params.buttons) {
const pos = button.position || [0, 0];
commands.push(`AddMenuButton ${params.name} "${button.text}" ${button.action} ${pos.join(' ')}`);
}
}
await this.bridge.executeConsoleCommands(commands);
return { success: true, message: `Menu ${params.name} created` };
}
// Set widget animation
async createWidgetAnimation(params: {
widgetName: string;
animationName: string;
duration: number;
tracks?: Array<{
componentName: string;
property: 'Position' | 'Scale' | 'Rotation' | 'Opacity' | 'Color';
keyframes: Array<{
time: number;
value: number | [number, number] | [number, number, number] | [number, number, number, number];
}>;
}>;
}) {
const commands: string[] = [];
commands.push(`CreateWidgetAnimation ${params.widgetName} ${params.animationName} ${params.duration}`);
if (params.tracks) {
for (const track of params.tracks) {
commands.push(`AddAnimationTrack ${params.widgetName}.${params.animationName} ${track.componentName} ${track.property}`);
for (const keyframe of track.keyframes) {
const value = Array.isArray(keyframe.value) ? keyframe.value.join(' ') : keyframe.value;
commands.push(`AddAnimationKeyframe ${params.widgetName}.${params.animationName} ${track.componentName} ${keyframe.time} ${value}`);
}
}
}
await this.bridge.executeConsoleCommands(commands);
return { success: true, message: `Animation ${params.animationName} created` };
}
// Play widget animation
async playWidgetAnimation(params: {
widgetName: string;
animationName: string;
playMode?: 'Forward' | 'Reverse' | 'PingPong';
loops?: number;
}) {
const playMode = params.playMode || 'Forward';
const loops = params.loops ?? 1;
const command = `PlayWidgetAnimation ${params.widgetName} ${params.animationName} ${playMode} ${loops}`;
return this.bridge.executeConsoleCommand(command);
}
// Set widget style
async setWidgetStyle(params: {
widgetName: string;
componentName: string;
style: {
backgroundColor?: [number, number, number, number];
borderColor?: [number, number, number, number];
borderWidth?: number;
padding?: [number, number, number, number];
margin?: [number, number, number, number];
};
}) {
const commands: string[] = [];
if (params.style.backgroundColor) {
commands.push(`SetWidgetBackgroundColor ${params.widgetName}.${params.componentName} ${params.style.backgroundColor.join(' ')}`);
}
if (params.style.borderColor) {
commands.push(`SetWidgetBorderColor ${params.widgetName}.${params.componentName} ${params.style.borderColor.join(' ')}`);
}
if (params.style.borderWidth !== undefined) {
commands.push(`SetWidgetBorderWidth ${params.widgetName}.${params.componentName} ${params.style.borderWidth}`);
}
if (params.style.padding) {
commands.push(`SetWidgetPadding ${params.widgetName}.${params.componentName} ${params.style.padding.join(' ')}`);
}
if (params.style.margin) {
commands.push(`SetWidgetMargin ${params.widgetName}.${params.componentName} ${params.style.margin.join(' ')}`);
}
await this.bridge.executeConsoleCommands(commands);
return { success: true, message: 'Widget style updated' };
}
// Bind widget event
async bindWidgetEvent(params: {
widgetName: string;
componentName: string;
eventType: 'OnClicked' | 'OnPressed' | 'OnReleased' | 'OnHovered' | 'OnUnhovered' | 'OnTextChanged' | 'OnTextCommitted' | 'OnValueChanged';
functionName: string;
}) {
const command = `BindWidgetEvent ${params.widgetName}.${params.componentName} ${params.eventType} ${params.functionName}`;
return this.bridge.executeConsoleCommand(command);
}
// Set input mode
async setInputMode(params: {
mode: 'GameOnly' | 'UIOnly' | 'GameAndUI';
showCursor?: boolean;
lockCursor?: boolean;
}) {
const commands: string[] = [];
commands.push(`SetInputMode ${params.mode}`);
if (params.showCursor !== undefined) {
commands.push(`ShowMouseCursor ${params.showCursor}`);
}
if (params.lockCursor !== undefined) {
commands.push(`SetMouseLockMode ${params.lockCursor}`);
}
await this.bridge.executeConsoleCommands(commands);
return { success: true, message: `Input mode set to ${params.mode}` };
}
// Create tooltip
async createTooltip(params: {
widgetName: string;
componentName: string;
text: string;
delay?: number;
}) {
const delay = params.delay ?? 0.5;
const command = `SetWidgetTooltip ${params.widgetName}.${params.componentName} "${params.text}" ${delay}`;
return this.bridge.executeConsoleCommand(command);
}
// Create drag and drop
async setupDragDrop(params: {
widgetName: string;
componentName: string;
dragVisual?: string;
dropTargets?: string[];
}) {
const commands = [];
commands.push(`EnableDragDrop ${params.widgetName}.${params.componentName}`);
if (params.dragVisual) {
commands.push(`SetDragVisual ${params.widgetName}.${params.componentName} ${params.dragVisual}`);
}
if (params.dropTargets) {
for (const target of params.dropTargets) {
commands.push(`AddDropTarget ${params.widgetName}.${params.componentName} ${target}`);
}
}
await this.bridge.executeConsoleCommands(commands);
return { success: true, message: 'Drag and drop configured' };
}
// Create notification
async showNotification(params: {
text: string;
duration?: number;
type?: 'Info' | 'Success' | 'Warning' | 'Error';
position?: 'TopLeft' | 'TopCenter' | 'TopRight' | 'BottomLeft' | 'BottomCenter' | 'BottomRight';
}) {
const duration = params.duration ?? 3.0;
const type = params.type || 'Info';
const position = params.position || 'TopRight';
const command = `ShowNotification "${params.text}" ${duration} ${type} ${position}`;
return this.bridge.executeConsoleCommand(command);
}
}