Skip to main content
Glama
visual.ts10.6 kB
import { UnrealBridge } from '../unreal-bridge.js'; import { loadEnv } from '../types/env.js'; import { Logger } from '../utils/logger.js'; import { coerceStringArray, interpretStandardResult } from '../utils/result-helpers.js'; import { promises as fs } from 'fs'; import path from 'path'; export class VisualTools { private env = loadEnv(); private log = new Logger('VisualTools'); constructor(private bridge: UnrealBridge) {} // Take a screenshot of viewport (high res or standard). Returns path and base64 (truncated) async takeScreenshot(params: { resolution?: string }) { const res = params.resolution && /^\d+x\d+$/i.test(params.resolution) ? params.resolution : ''; const primaryCommand = res ? `HighResShot ${res}` : 'Shot'; const captureStartedAt = Date.now(); try { const firstAttempt = await this.bridge.executeConsoleCommand(primaryCommand); const firstSummary = this.bridge.summarizeConsoleCommand(primaryCommand, firstAttempt); if (!res) { const output = (firstSummary.output || '').toLowerCase(); const badInput = output.includes('bad input') || output.includes('unrecognized command'); const hasErrorLine = firstSummary.logLines?.some(line => /error:/i.test(line)); if (badInput || hasErrorLine) { this.log.debug(`Screenshot primary command reported an error (${firstSummary.output || 'no output'})`); } } // Give the engine a moment to write the file (UE can flush asynchronously) await new Promise(r => setTimeout(r, 1200)); const p = await this.findLatestScreenshot(captureStartedAt); if (!p) { this.log.warn('Screenshot captured but output file was not found'); return { success: true, message: 'Screenshot triggered, but could not locate output file' }; } let b64: string | undefined; let truncated = false; let sizeBytes: number | undefined; let mime: string | undefined; try { const stat = await fs.stat(p); sizeBytes = stat.size; const max = 1024 * 1024; // 1 MiB cap for inline payloads if (stat.size <= max) { const buf = await fs.readFile(p); b64 = buf.toString('base64'); } else { truncated = true; } const ext = path.extname(p).toLowerCase(); if (ext === '.png') mime = 'image/png'; else if (ext === '.jpg' || ext === '.jpeg') mime = 'image/jpeg'; else if (ext === '.bmp') mime = 'image/bmp'; } catch {} return { success: true, imagePath: p.replace(/\\/g, '/'), imageMimeType: mime, imageSizeBytes: sizeBytes, imageBase64: b64, imageBase64Truncated: truncated }; } catch (err: any) { return { success: false, error: String(err?.message || err) }; } } private async getEngineScreenshotDirectories(): Promise<string[]> { const python = ` import unreal import json import os result = { "success": True, "message": "", "error": "", "directories": [], "warnings": [], "details": [] } def finalize(): data = dict(result) if data.get("success"): if not data.get("message"): data["message"] = "Collected screenshot directories" data.pop("error", None) else: if not data.get("error"): data["error"] = data.get("message") or "Failed to collect screenshot directories" if not data.get("message"): data["message"] = data["error"] if not data.get("warnings"): data.pop("warnings", None) if not data.get("details"): data.pop("details", None) if not data.get("directories"): data.pop("directories", None) return data def add_path(candidate, note=None): if not candidate: return try: abs_path = os.path.abspath(os.path.normpath(candidate)) except Exception as normalize_error: result["warnings"].append(f"Failed to normalize path {candidate}: {normalize_error}") return if abs_path not in result["directories"]: result["directories"].append(abs_path) if note: result["details"].append(f"{note}: {abs_path}") else: result["details"].append(f"Discovered screenshot directory: {abs_path}") try: automation_dir = unreal.AutomationLibrary.get_screenshot_directory() add_path(automation_dir, "Automation screenshot directory") except Exception as automation_error: result["warnings"].append(f"Automation screenshot directory unavailable: {automation_error}") try: project_saved = unreal.Paths.project_saved_dir() add_path(project_saved, "Project Saved directory") if project_saved: add_path(os.path.join(project_saved, 'Screenshots'), "Project Saved screenshots") add_path(os.path.join(project_saved, 'Screenshots', 'Windows'), "Project Saved Windows screenshots") add_path(os.path.join(project_saved, 'Screenshots', 'WindowsEditor'), "Project Saved WindowsEditor screenshots") try: platform_name = unreal.SystemLibrary.get_platform_user_name() if platform_name: add_path(os.path.join(project_saved, 'Screenshots', platform_name), f"Project Saved screenshots for {platform_name}") add_path(os.path.join(project_saved, 'Screenshots', f"{platform_name}Editor"), f"Project Saved editor screenshots for {platform_name}") except Exception as platform_error: result["warnings"].append(f"Failed to resolve platform-specific screenshot directories: {platform_error}") except Exception as saved_error: result["warnings"].append(f"Project Saved directory unavailable: {saved_error}") try: project_file = unreal.Paths.get_project_file_path() if project_file: project_dir = os.path.dirname(project_file) add_path(os.path.join(project_dir, 'Saved', 'Screenshots'), "Project directory screenshots") add_path(os.path.join(project_dir, 'Saved', 'Screenshots', 'Windows'), "Project directory Windows screenshots") add_path(os.path.join(project_dir, 'Saved', 'Screenshots', 'WindowsEditor'), "Project directory WindowsEditor screenshots") except Exception as project_error: result["warnings"].append(f"Project directory screenshots unavailable: {project_error}") if not result["directories"]: result["warnings"].append("No screenshot directories discovered") print('RESULT:' + json.dumps(finalize())) `.trim() .replace(/\r?\n/g, '\n'); try { const response = await this.bridge.executePython(python); const interpreted = interpretStandardResult(response, { successMessage: 'Collected screenshot directories', failureMessage: 'Failed to collect screenshot directories' }); if (interpreted.details) { for (const entry of interpreted.details) { this.log.debug(entry); } } if (interpreted.warnings) { for (const warning of interpreted.warnings) { this.log.debug(`Screenshot directory warning: ${warning}`); } } const directories = coerceStringArray(interpreted.payload.directories); if (directories?.length) { return directories; } if (!interpreted.success && interpreted.error) { this.log.warn(`Screenshot path probe failed: ${interpreted.error}`); } if (interpreted.rawText) { try { const fallback = JSON.parse(interpreted.rawText); const fallbackDirs = coerceStringArray(fallback); if (fallbackDirs?.length) { return fallbackDirs; } } catch {} } } catch (err) { this.log.debug('Screenshot path probe failed', err); } return []; } private async findLatestScreenshot(since?: number): Promise<string | null> { // Try env override, otherwise look in common UE Saved/Screenshots folder under project const candidates: string[] = []; const seen = new Set<string>(); const addCandidate = (candidate?: string | null) => { if (!candidate) return; const normalized = path.isAbsolute(candidate) ? path.normalize(candidate) : path.resolve(candidate); if (!seen.has(normalized)) { seen.add(normalized); candidates.push(normalized); } }; if (this.env.UE_SCREENSHOT_DIR) addCandidate(this.env.UE_SCREENSHOT_DIR); if (this.env.UE_PROJECT_PATH) { const projectDir = path.dirname(this.env.UE_PROJECT_PATH); addCandidate(path.join(projectDir, 'Saved', 'Screenshots')); addCandidate(path.join(projectDir, 'Saved', 'Screenshots', 'Windows')); addCandidate(path.join(projectDir, 'Saved', 'Screenshots', 'WindowsEditor')); } const engineDirs = await this.getEngineScreenshotDirectories(); for (const dir of engineDirs) { addCandidate(dir); } // Fallback: common locations relative to current working directory addCandidate(path.join(process.cwd(), 'Saved', 'Screenshots')); addCandidate(path.join(process.cwd(), 'Saved', 'Screenshots', 'Windows')); addCandidate(path.join(process.cwd(), 'Saved', 'Screenshots', 'WindowsEditor')); const searchDirs = new Set<string>(); const queue: string[] = [...candidates]; while (queue.length) { const candidate = queue.pop(); if (!candidate || searchDirs.has(candidate)) continue; searchDirs.add(candidate); try { const entries = await fs.readdir(candidate, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory()) { queue.push(path.join(candidate, entry.name)); } } } catch {} } let latest: { path: string; mtime: number } | null = null; let latestSince: { path: string; mtime: number } | null = null; const cutoff = since ? since - 2000 : undefined; // allow slight clock drift for (const dirPath of searchDirs) { let entries: string[]; try { entries = await fs.readdir(dirPath); } catch { continue; } for (const entry of entries) { const fp = path.join(dirPath, entry); if (!/\.(png|jpg|jpeg|bmp)$/i.test(fp)) continue; try { const st = await fs.stat(fp); const info = { path: fp, mtime: st.mtimeMs }; if (!latest || info.mtime > latest.mtime) { latest = info; } if (cutoff !== undefined && st.mtimeMs >= cutoff) { if (!latestSince || info.mtime > latestSince.mtime) { latestSince = info; } } } catch {} } } const chosen = latestSince || latest; return chosen?.path || null; } }

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