"""Execution control tools: launch, attach, continue, step, quit.""" import time from ..dosbox import DOSBoxConfig from ..fonts import ( FONTS_DIR, fonts_installed, get_font_path, list_fonts, ) from ..fonts import ( download_fonts as _download_fonts, ) from ..gdb_client import GDBError from ..state import client, manager from ..utils import format_address from .peripheral import keyboard_send as _keyboard_send # Font metadata for the fonts_list() tool _FONT_INFO = { "Px437_IBM_VGA_8x16": { "description": "Classic IBM VGA 8x16", "best_for": "Standard DOS text (80x25)", }, "Px437_IBM_VGA_9x16": { "description": "IBM VGA 9x16 (wider)", "best_for": "Better readability", }, "Px437_IBM_EGA_8x14": { "description": "IBM EGA 8x14", "best_for": "80x25 with smaller font", }, "Px437_IBM_CGA": { "description": "IBM CGA", "best_for": "40-column mode, retro look", }, "Px437_IBM_MDA": { "description": "IBM MDA (Monochrome)", "best_for": "Word processing style", }, "PxPlus_IBM_VGA_8x16": { "description": "VGA 8x16 + Unicode", "best_for": "Extended character support", }, "PxPlus_IBM_VGA_9x16": { "description": "VGA 9x16 + Unicode", "best_for": "Extended + better readability", }, } def launch( binary_path: str | None = None, gdb_port: int = 1234, qmp_port: int = 4444, use_docker: bool | None = None, cycles: str = "auto", memsize: int = 16, joystick: str = "auto", parallel1: str = "disabled", ) -> dict: """Launch DOSBox-X with GDB debugging and QMP control enabled. Automatically uses Docker if native DOSBox-X is not installed. Args: binary_path: Path to DOS binary to run (optional) gdb_port: Port for GDB stub (default: 1234) qmp_port: Port for QMP control - screenshots, keyboard, mouse (default: 4444) use_docker: Force Docker (True), force native (False), or auto-detect (None/default) cycles: CPU cycles setting (auto, max, or number) memsize: Conventional memory in MB (default: 16) joystick: Joystick type - auto, none, 2axis, 4axis (default: auto) parallel1: Parallel port 1 - disabled, file, printer (default: disabled) Use "file" to capture print output to capture directory Returns: Status dict with connection details Example: launch("/path/to/GAME.EXE") # Auto-detects native vs Docker launch("/path/to/GAME.EXE", joystick="2axis") # With joystick launch("/path/to/GAME.EXE", parallel1="file") # Capture printer output """ config = DOSBoxConfig( gdb_port=gdb_port, gdb_enabled=True, qmp_port=qmp_port, qmp_enabled=True, cycles=cycles, memsize=memsize, joysticktype=joystick, parallel1=parallel1, ) launch_method = None try: # Auto-detect: try native first, fallback to Docker if use_docker is None: if manager._find_dosbox(): launch_method = "native" manager.launch_native(binary_path=binary_path, config=config) else: # Native not available, try Docker launch_method = "docker" manager.launch_docker(binary_path=binary_path, config=config) elif use_docker: launch_method = "docker" manager.launch_docker(binary_path=binary_path, config=config) else: launch_method = "native" manager.launch_native(binary_path=binary_path, config=config) return { "success": True, "message": f"DOSBox-X launched successfully ({launch_method})", "launch_method": launch_method, "gdb_host": "localhost", "gdb_port": gdb_port, "qmp_port": qmp_port, "pid": manager.pid, "hint": f"Use attach() to connect to debugger on port {gdb_port}. QMP (screenshots/keyboard) on port {qmp_port}.", } except Exception as e: return { "success": False, "error": str(e), "hint": "Ensure Docker is installed and the dosbox-mcp image is built (make build)", } def attach( host: str = "localhost", port: int | None = None, auto_keypress: str | None = None, keypress_delay_ms: int = 500, ) -> dict: """Connect to a running DOSBox-X GDB stub. Args: host: Hostname or IP (default: localhost) port: GDB port (default: uses port from launch(), or 1234) auto_keypress: Optional key(s) to send after attaching (e.g., "enter", "space") Useful for dismissing splash screens automatically. keypress_delay_ms: Delay after connection before sending keypress (default: 500ms) Returns: Connection status and initial register state Examples: attach() # Auto-detect port from launch() attach("localhost", 1234) # Explicit port attach(auto_keypress="enter") # Dismiss splash screen with Enter """ # Use manager's port if not specified, fallback to 1234 if port is None: port = manager.gdb_port if manager.running else 1234 try: client.connect(host, port) # Get initial state regs = client.read_registers() stop = client.get_stop_reason() result = { "success": True, "message": f"Connected to {host}:{port}", "gdb_port": port, "stop_reason": stop.reason.name.lower(), "cs_ip": f"{regs.cs:04x}:{regs.ip:04x}", "physical_address": f"{regs.cs_ip:05x}", } # Send keypress to dismiss splash screen if requested if auto_keypress: time.sleep(keypress_delay_ms / 1000.0) key_result = _keyboard_send(auto_keypress) result["keypress_sent"] = auto_keypress result["keypress_result"] = key_result.get("success", False) if not key_result.get("success"): result["keypress_error"] = key_result.get("error") return result except GDBError as e: return { "success": False, "error": str(e), } def continue_execution(timeout: float | None = None) -> dict: """Continue execution until breakpoint or signal. Args: timeout: Optional timeout in seconds Returns: Stop event info (reason, address, breakpoint hit) """ try: event = client.continue_execution(timeout=timeout) regs = client.read_registers() return { "success": True, "stop_reason": event.reason.name.lower(), "address": format_address(event.address, "both"), "breakpoint_id": event.breakpoint_id, "signal": event.signal, "cs_ip": f"{regs.cs:04x}:{regs.ip:04x}", } except GDBError as e: return { "success": False, "error": str(e), } def step(count: int = 1) -> dict: """Step one or more instructions. Args: count: Number of instructions to step (default: 1) Returns: New register state after stepping """ try: event = client.step(count) regs = client.read_registers() return { "success": True, "stop_reason": event.reason.name.lower(), "cs_ip": f"{regs.cs:04x}:{regs.ip:04x}", "physical_address": f"{regs.cs_ip:05x}", "stepped": count, } except GDBError as e: return { "success": False, "error": str(e), } def step_over() -> dict: """Step over a CALL instruction (execute subroutine and stop after return). Returns: New register state after step-over """ try: event = client.step_over() regs = client.read_registers() return { "success": True, "stop_reason": event.reason.name.lower(), "cs_ip": f"{regs.cs:04x}:{regs.ip:04x}", "physical_address": f"{regs.cs_ip:05x}", } except GDBError as e: return { "success": False, "error": str(e), } def quit() -> dict: """Stop DOSBox-X and clean up. Returns: Shutdown status """ try: if client.connected: try: client.kill() except GDBError: client.disconnect() if manager.running: manager.stop() return { "success": True, "message": "DOSBox-X stopped and cleaned up", } except Exception as e: return { "success": False, "error": str(e), } def fonts_list() -> dict: """List available TrueType fonts for DOSBox-X TTF output. These fonts from The Ultimate Oldschool PC Font Pack provide authentic DOS-era rendering. Fonts are downloaded on first use. Use fonts_download() to pre-download fonts, or they will be auto-downloaded when you specify a font in launch(). Returns: Dictionary with available fonts and installation status Example: fonts_list() fonts_download() # Optional: pre-download launch(binary_path="/dos/RIPTERM.EXE", ttf_font="Px437_IBM_VGA_9x16") """ installed = fonts_installed() fonts = [] for name in list_fonts(): info = _FONT_INFO.get(name, {}) path = get_font_path(name) fonts.append( { "name": name, "description": info.get("description", ""), "best_for": info.get("best_for", ""), "installed": path is not None, } ) return { "fonts": fonts, "fonts_installed": installed, "cache_dir": str(FONTS_DIR), "recommended": "Px437_IBM_VGA_9x16", "usage_hint": "Fonts auto-download on use. Use fonts_download() to pre-install.", "license": "CC BY-SA 4.0 - The Ultimate Oldschool PC Font Pack by VileR", "source": "https://int10h.org/oldschool-pc-fonts/", } def fonts_download(force: bool = False) -> dict: """Download IBM PC TrueType fonts for DOSBox-X TTF output. Downloads fonts from The Ultimate Oldschool PC Font Pack (int10h.org) and caches them locally. Fonts are also auto-downloaded when needed. Args: force: Re-download even if fonts already exist Returns: Status dict with download result and cache location Example: fonts_download() # Download fonts fonts_list() # See installed fonts """ return _download_fonts(force=force)