- Fonts downloaded from int10h.org on first use - Cached in platform-appropriate directory (~/.cache/mcdosbox-x/fonts) - Add fonts_download() MCP tool for explicit pre-download - Wheel size reduced from 473KB to 56KB (88% smaller) - 48 tools now registered
359 lines
11 KiB
Python
359 lines
11 KiB
Python
"""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)
|