Skip to main content
Glama
debug.ts19 kB
// Debug visualization tools for Unreal Engine import { UnrealBridge } from '../unreal-bridge.js'; import { bestEffortInterpretedText, coerceString, interpretStandardResult } from '../utils/result-helpers.js'; import { parseStandardResult } from '../utils/python-output.js'; export class DebugVisualizationTools { constructor(private bridge: UnrealBridge) {} // Helper to draw via Python SystemLibrary with the editor world private async pyDraw(scriptBody: string, meta?: { action: string; params?: Record<string, unknown> }) { const action = (meta?.action || 'debug_draw').replace(/\\/g, '\\\\').replace(/'/g, "\\'"); const payloadObject = meta?.params ?? {}; const payloadJson = JSON.stringify(payloadObject).replace(/\\/g, '\\\\').replace(/'/g, "\\'"); const indentedBody = scriptBody .split(/\r?\n/) .map(line => ` ${line}`) .join('\n'); const script = ` import unreal import json payload = json.loads('${payloadJson}') ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem) if not ues: raise Exception('UnrealEditorSubsystem not available') world = ues.get_editor_world() if not world: raise Exception('Editor world unavailable') try: ${indentedBody} print('DEBUG_DRAW:' + json.dumps({'action': '${action}', 'params': payload})) print('RESULT:' + json.dumps({'success': True, 'action': '${action}', 'params': payload})) except Exception as e: print('DEBUG_DRAW_ERROR:' + str(e)) print('RESULT:' + json.dumps({'success': False, 'action': '${action}', 'error': str(e)})) `.trim() .replace(/\r?\n/g, '\n'); try { const response = await this.bridge.executePython(script); let interpreted = interpretStandardResult(response, { successMessage: `${action} executed`, failureMessage: `${action} failed` }); const parsed = parseStandardResult(response); const parsedPayload = parsed.data ?? {}; const parsedSuccessValue = (parsedPayload as any).success; const normalizedSuccess = typeof parsedSuccessValue === 'string' ? ['true', '1', 'yes'].includes(parsedSuccessValue.toLowerCase()) : parsedSuccessValue === true; if (!interpreted.success && normalizedSuccess) { interpreted = { ...interpreted, success: true, error: undefined, message: interpreted.message || `${action} executed`, payload: { ...parsedPayload } }; } const finalSuccess = interpreted.success || normalizedSuccess; const resolvedAction = coerceString(interpreted.payload.action) ?? action; const fallbackOutput = typeof parsed.text === 'string' ? parsed.text : ''; const rawOutput = bestEffortInterpretedText(interpreted) ?? (fallbackOutput ? fallbackOutput : undefined); if (finalSuccess) { return { ...interpreted.payload, success: true, action: resolvedAction, warnings: interpreted.warnings, details: interpreted.details, rawOutput, raw: parsed.raw ?? interpreted.raw }; } return { success: false, action: resolvedAction, error: interpreted.error ?? `${resolvedAction} failed`, warnings: interpreted.warnings, details: interpreted.details, rawOutput, raw: parsed.raw ?? interpreted.raw }; } catch (e) { return { success: false, error: String(e), action: meta?.action || 'debug_draw' }; } } // Draw debug line using Python SystemLibrary async drawDebugLine(params: { start: [number, number, number]; end: [number, number, number]; color?: [number, number, number, number]; duration?: number; thickness?: number; }) { const color = params.color || [255, 0, 0, 255]; const duration = params.duration ?? 5.0; const thickness = params.thickness ?? 1.0; const [sr, sg, sb, sa] = color; const [sx, sy, sz] = params.start; const [ex, ey, ez] = params.end; const script = ` start = unreal.Vector(${sx}, ${sy}, ${sz}) end = unreal.Vector(${ex}, ${ey}, ${ez}) color = unreal.LinearColor(${sr}/255.0, ${sg}/255.0, ${sb}/255.0, ${sa}/255.0) unreal.SystemLibrary.draw_debug_line(world, start, end, color, ${duration}, ${thickness}) `; return this.pyDraw(script, { action: 'debug_line', params: { start: params.start, end: params.end, color, duration, thickness } }); } // Draw debug box using Python SystemLibrary async drawDebugBox(params: { center: [number, number, number]; extent: [number, number, number]; color?: [number, number, number, number]; rotation?: [number, number, number]; duration?: number; thickness?: number; }) { const color = params.color || [0, 255, 0, 255]; const rotation = params.rotation || [0, 0, 0]; const duration = params.duration ?? 5.0; const thickness = params.thickness ?? 1.0; const [cr, cg, cb, ca] = color; const [cx, cy, cz] = params.center; const [ex, ey, ez] = params.extent; const [rp, ry, rr] = rotation; const script = ` center = unreal.Vector(${cx}, ${cy}, ${cz}) extent = unreal.Vector(${ex}, ${ey}, ${ez}) rot = unreal.Rotator(${rp}, ${ry}, ${rr}) color = unreal.LinearColor(${cr}/255.0, ${cg}/255.0, ${cb}/255.0, ${ca}/255.0) unreal.SystemLibrary.draw_debug_box(world, center, extent, color, rot, ${duration}, ${thickness}) `; return this.pyDraw(script, { action: 'debug_box', params: { center: params.center, extent: params.extent, rotation, color, duration, thickness } }); } // Draw debug sphere using Python SystemLibrary async drawDebugSphere(params: { center: [number, number, number]; radius: number; segments?: number; color?: [number, number, number, number]; duration?: number; thickness?: number; }) { const segments = params.segments ?? 12; const color = params.color || [0, 0, 255, 255]; const duration = params.duration ?? 5.0; const thickness = params.thickness ?? 1.0; const [cr, cg, cb, ca] = color; const [cx, cy, cz] = params.center; const script = ` center = unreal.Vector(${cx}, ${cy}, ${cz}) color = unreal.LinearColor(${cr}/255.0, ${cg}/255.0, ${cb}/255.0, ${ca}/255.0) unreal.SystemLibrary.draw_debug_sphere(world, center, ${params.radius}, ${segments}, color, ${duration}, ${thickness}) `; return this.pyDraw(script, { action: 'debug_sphere', params: { center: params.center, radius: params.radius, segments, color, duration, thickness } }); } // The rest keep console-command fallbacks or editor helpers as before async drawDebugCapsule(params: { center: [number, number, number]; halfHeight: number; radius: number; rotation?: [number, number, number]; color?: [number, number, number, number]; duration?: number; }) { const rotation = params.rotation || [0, 0, 0]; const color = params.color || [255, 255, 0, 255]; const duration = params.duration || 5.0; const [cx, cy, cz] = params.center; const [rp, ry, rr] = rotation; const [cr, cg, cb, ca] = color; const script = `\ncenter = unreal.Vector(${cx}, ${cy}, ${cz})\nrot = unreal.Rotator(${rp}, ${ry}, ${rr})\ncolor = unreal.LinearColor(${cr}/255.0, ${cg}/255.0, ${cb}/255.0, ${ca}/255.0)\nunreal.SystemLibrary.draw_debug_capsule(world, center, ${params.halfHeight}, ${params.radius}, rot, color, ${duration}, 1.0)\n`; return this.pyDraw(script, { action: 'debug_capsule', params: { center: params.center, halfHeight: params.halfHeight, radius: params.radius, rotation, color, duration } }); } async drawDebugCone(params: { origin: [number, number, number]; direction: [number, number, number]; length: number; angleWidth: number; angleHeight: number; numSides?: number; color?: [number, number, number, number]; duration?: number; }) { const color = params.color || [255, 0, 255, 255]; const duration = params.duration || 5.0; const [ox, oy, oz] = params.origin; const [dx, dy, dz] = params.direction; const [cr, cg, cb, ca] = color; const script = `\norigin = unreal.Vector(${ox}, ${oy}, ${oz})\ndir = unreal.Vector(${dx}, ${dy}, ${dz})\ncolor = unreal.LinearColor(${cr}/255.0, ${cg}/255.0, ${cb}/255.0, ${ca}/255.0)\nunreal.SystemLibrary.draw_debug_cone(world, origin, dir, ${params.length}, ${params.angleWidth}, ${params.angleHeight}, ${params.numSides || 12}, color, ${duration}, 1.0)\n`; return this.pyDraw(script, { action: 'debug_cone', params: { origin: params.origin, direction: params.direction, length: params.length, angleWidth: params.angleWidth, angleHeight: params.angleHeight, numSides: params.numSides || 12, color, duration } }); } async drawDebugString(params: { location: [number, number, number]; text: string; color?: [number, number, number, number]; duration?: number; fontSize?: number; }) { const color = params.color || [255, 255, 255, 255]; const duration = params.duration || 5.0; const [x, y, z] = params.location; const [r, g, b, a] = color; const script = `\nloc = unreal.Vector(${x}, ${y}, ${z})\ncolor = unreal.LinearColor(${r}/255.0, ${g}/255.0, ${b}/255.0, ${a}/255.0)\nunreal.SystemLibrary.draw_debug_string(world, loc, "${params.text.replace(/"/g, '\\"')}", None, color, ${duration})\n`; return this.pyDraw(script, { action: 'debug_string', params: { location: params.location, text: params.text, color, duration, fontSize: params.fontSize } }); } async drawDebugArrow(params: { start: [number, number, number]; end: [number, number, number]; arrowSize?: number; color?: [number, number, number, number]; duration?: number; thickness?: number; }) { const color = params.color || [0, 255, 255, 255]; const duration = params.duration || 5.0; const thickness = params.thickness || 2.0; const [sx, sy, sz] = params.start; const [ex, ey, ez] = params.end; const [r, g, b, a] = color; const script = `\nstart = unreal.Vector(${sx}, ${sy}, ${sz})\nend = unreal.Vector(${ex}, ${ey}, ${ez})\ncolor = unreal.LinearColor(${r}/255.0, ${g}/255.0, ${b}/255.0, ${a}/255.0)\nunreal.SystemLibrary.draw_debug_arrow(world, start, end, ${params.arrowSize || 10.0}, color, ${duration}, ${thickness})\n`; return this.pyDraw(script, { action: 'debug_arrow', params: { start: params.start, end: params.end, arrowSize: params.arrowSize || 10.0, color, duration, thickness } }); } async drawDebugPoint(params: { location: [number, number, number]; size?: number; color?: [number, number, number, number]; duration?: number; }) { const size = params.size || 10.0; const color = params.color || [255, 255, 255, 255]; const duration = params.duration || 5.0; const [x, y, z] = params.location; const [r, g, b, a] = color; const script = `\nloc = unreal.Vector(${x}, ${y}, ${z})\ncolor = unreal.LinearColor(${r}/255.0, ${g}/255.0, ${b}/255.0, ${a}/255.0)\nunreal.SystemLibrary.draw_debug_point(world, loc, ${size}, color, ${duration})\n`; return this.pyDraw(script, { action: 'debug_point', params: { location: params.location, size, color, duration } }); } async drawDebugCoordinateSystem(params: { location: [number, number, number]; rotation?: [number, number, number]; scale?: number; duration?: number; thickness?: number; }) { const rotation = params.rotation || [0, 0, 0]; const scale = params.scale || 100.0; const duration = params.duration || 5.0; const thickness = params.thickness || 2.0; const command = `DrawDebugCoordinateSystem ${params.location.join(' ')} ${rotation.join(' ')} ${scale} ${duration} ${thickness}`; return this.bridge.executeConsoleCommand(command); } async drawDebugFrustum(params: { origin: [number, number, number]; rotation: [number, number, number]; fov: number; aspectRatio?: number; nearPlane?: number; farPlane?: number; color?: [number, number, number, number]; duration?: number; }) { const aspectRatio = params.aspectRatio || 1.77; const nearPlane = params.nearPlane || 10.0; const farPlane = params.farPlane || 1000.0; const color = params.color || [128, 128, 255, 255]; const duration = params.duration || 5.0; const [ox, oy, oz] = params.origin; const [rp, ry, rr] = params.rotation; const [r, g, b, a] = color; const script = `\norigin = unreal.Vector(${ox}, ${oy}, ${oz})\nrot = unreal.Rotator(${rp}, ${ry}, ${rr})\ncolor = unreal.LinearColor(${r}/255.0, ${g}/255.0, ${b}/255.0, ${a}/255.0)\nunreal.SystemLibrary.draw_debug_frustum(world, origin, rot, ${params.fov}, ${aspectRatio}, ${nearPlane}, ${farPlane}, color, ${duration})\n`; return this.pyDraw(script, { action: 'debug_frustum', params: { origin: params.origin, rotation: params.rotation, fov: params.fov, aspectRatio, nearPlane, farPlane, color, duration } }); } async clearDebugDrawings() { return this.bridge.executeConsoleCommand('FlushPersistentDebugLines'); } async showCollision(params: { enabled: boolean; type?: 'Simple' | 'Complex' | 'Both'; }) { const commands: string[] = []; if (params.enabled) { const typeCmd = params.type === 'Simple' ? '1' : params.type === 'Complex' ? '2' : '3'; commands.push(`show Collision ${typeCmd}`); } else { commands.push('show Collision 0'); } await this.bridge.executeConsoleCommands(commands); return { success: true, message: `Collision visualization ${params.enabled ? 'enabled' : 'disabled'}` }; } async showBounds(params: { enabled: boolean; }) { const command = params.enabled ? 'show Bounds' : 'show Bounds 0'; return this.bridge.executeConsoleCommand(command); } async setViewMode(params: { mode: 'Lit' | 'Unlit' | 'Wireframe' | 'DetailLighting' | 'LightingOnly' | 'LightComplexity' | 'ShaderComplexity' | 'LightmapDensity' | 'StationaryLightOverlap' | 'ReflectionOverride' | 'CollisionPawn' | 'CollisionVisibility'; }) { // Map non-viewmode requests to appropriate show flags for safety if (params.mode === 'CollisionPawn' || params.mode === 'CollisionVisibility') { // Use collision visualization instead of viewmode (UE doesn't have these as view modes) await this.showCollision({ enabled: true, type: 'Both' }); return { success: true, message: 'Collision visualization enabled (use show flags, not viewmode)' } as any; } const VALID_VIEWMODES = new Set([ 'Lit', 'Unlit', 'Wireframe', 'DetailLighting', 'LightingOnly', 'LightComplexity', 'ShaderComplexity', 'LightmapDensity', 'StationaryLightOverlap', 'ReflectionOverride' ]); if (!VALID_VIEWMODES.has(params.mode)) { // Fallback to Lit if unknown await this.bridge.executeConsoleCommand('viewmode Lit'); return { success: false, warning: `Unknown or unsupported viewmode '${params.mode}'. Reverted to Lit.` } as any; } const UNSAFE_VIEWMODES = [ 'LightComplexity', 'ShaderComplexity', 'LightmapDensity', 'StationaryLightOverlap' ]; if (UNSAFE_VIEWMODES.includes(params.mode)) { console.error(`⚠️ Viewmode '${params.mode}' may be unstable in some UE configurations.`); try { await this.bridge.executeConsoleCommand('stop'); } catch {} await new Promise(resolve => setTimeout(resolve, 100)); } try { const command = `viewmode ${params.mode}`; const result = await this.bridge.executeConsoleCommand(command); if (UNSAFE_VIEWMODES.includes(params.mode)) { setTimeout(async () => { try { await this.bridge.executeConsoleCommand('stat unit'); } catch { await this.bridge.executeConsoleCommand('viewmode Lit'); } }, 2000); } return { ...result, warning: UNSAFE_VIEWMODES.includes(params.mode) ? `Viewmode '${params.mode}' applied. This mode may be unstable.` : undefined }; } catch (error) { await this.bridge.executeConsoleCommand('viewmode Lit'); throw new Error(`Failed to set viewmode '${params.mode}': ${error}. Reverted to Lit mode.`); } } async showDebugInfo(params: { category: 'AI' | 'Animation' | 'Audio' | 'Collision' | 'Camera' | 'Game' | 'Hitboxes' | 'Input' | 'Net' | 'Physics' | 'Slate' | 'Streaming' | 'Particles' | 'Navigation'; enabled: boolean; }) { const command = `showdebug ${params.enabled ? params.category : 'None'}`; return this.bridge.executeConsoleCommand(command); } async showActorNames(params: { enabled: boolean; }) { const command = params.enabled ? 'show ActorNames' : 'show ActorNames 0'; return this.bridge.executeConsoleCommand(command); } async drawDebugPath(params: { points: Array<[number, number, number]>; color?: [number, number, number, number]; duration?: number; thickness?: number; }) { const color = params.color || [255, 128, 0, 255]; const duration = params.duration || 5.0; const thickness = params.thickness || 2.0; for (let i = 0; i < params.points.length - 1; i++) { const start = params.points[i]; const end = params.points[i + 1]; await this.drawDebugLine({ start, end, color, duration, thickness }); } return { success: true, message: `Debug path drawn with ${params.points.length} points` }; } async showNavigationMesh(params: { enabled: boolean; }) { const command = params.enabled ? 'show Navigation' : 'show Navigation 0'; return this.bridge.executeConsoleCommand(command); } async enableOnScreenMessages(params: { enabled: boolean; key?: number; message?: string; duration?: number; color?: [number, number, number, number]; }) { if (params.enabled && params.message) { const key = params.key || -1; const duration = params.duration || 5.0; const color = params.color || [255, 255, 255, 255]; const command = `ke * DisplayDebugMessage ${key} "${params.message}" ${duration} ${color.join(' ')}`; return this.bridge.executeConsoleCommand(command); } else { return this.bridge.executeConsoleCommand('DisableAllScreenMessages'); } } async showSkeletalMeshBones(params: { actorName: string; enabled: boolean; }) { const command = params.enabled ? `ShowDebugSkelMesh ${params.actorName}` : `HideDebugSkelMesh ${params.actorName}`; return this.bridge.executeConsoleCommand(command); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ChiR24/Unreal_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server