Install Recipes
An install recipe is a TOML file that describes how a DOS game gets
installed: where the disk images are, which installer to run, which
prompts appear on screen, and which keys answer them. Give the recipe
to the automation layer and the whole installation runs unattended,
from the first INSTALL.EXE to the verified files on the C drive,
including disk swaps in the middle.
The source distribution includes a sample implementation: the Python
scripts under tests/integration/ read these recipes, generate a Lua
script from them, and drive the emulator through the REST API.
The reference implementation serves two purposes. It demonstrates how launcher software can automate DOS installations through the public API, and it forms the project's end-to-end integration test suite. Every bundled recipe is executed against every build to verify that automation, disk swapping, input injection, and application control continue to work correctly.
The included toolchain
You do not have to write recipes from scratch. The source tree ships a
small toolchain around them, including a macro recorder: install the
game once by hand while the recorder captures every keystroke, then
replay that recording on any machine, or use it as the basis for a
recipe. All files live in tests/integration/.
| Tool | What it does | File |
|---|---|---|
| Macro recorder | Records a manual installation: starts the emulator with the disks mounted, captures all input with frame timing plus a ZMBV video, and pins the settings replay determinism depends on | record_install.py |
| Session recorder | Same recording mechanism for a free-form session instead of an install | record_session.py |
| Install replayer | Replays a recorded installation in a visible window | replay_install.py |
| Recording replayer | Replays any recording JSON through the API | replay_recording.py |
| Recipe test runner | Runs every bundled recipe end to end; doubles as the integration test suite | test_e2e_installs.py |
| Recipe engine | Turns a recipe into a Lua script and drives the emulator through the REST API; usable as a library | e2e_helpers.py |
| API client | Plain Python client for the REST API, used by all the tools above | dosbox_client.py |
Recipes are deliberately independent of that sample implementation. Every step type maps to a documented Lua function or REST API call, so a launcher written in any language can consume the same files. The mapping is listed below.
The format is young and will grow with the project. What's documented here is what the current release supports.
A complete example
This is a working recipe for a multi-disk floppy installer, trimmed from the bundled One Must Fall 2097 recipe:
[game]
name = "One Must Fall 2097"
slug = "one-must-fall-2097"
media = "floppy"
license = "commercial"
[discs]
images = ["disk1.img", "disk2.img", "disk3.img", "disk4.img", "disk5.img"]
[installer]
path = "INSTALL.EXE"
source_drive = "A"
target_drive = "C"
[settings]
cpu_cycles = "24000"
[prompts]
sequence = [
{ wait = "Which program do you want to install", key = "enter" },
{ wait = "Drive to install to", key = "enter" },
{ wait = "Create it", key = "enter" },
{ wait = "Please insert disk 2", action = "swap:2", key = "enter" },
{ wait = "Please insert disk 3", action = "swap:3", key = "enter" },
{ wait = "read the instructions", key = "n" },
]
[verify]
files = ["C:\\OMF\\OMF.EXE", "C:\\OMF\\SETUP.EXE"]
Read the sequence top to bottom: wait until the given text appears on screen, then answer it. The automation never types blind; every answer is gated on the screen actually showing the prompt it belongs to.
Sections
[game]
| Key | What |
|---|---|
name | Display name of the game |
slug | Short identifier, used for directories and artifacts |
media | floppy, cdrom-iso, cdrom-cue, booter, or zip |
license | Licensing note for the media, e.g. commercial or shareware |
The media type decides how the installation starts. Floppy and CD
recipes switch to the source drive and run the installer. A booter
recipe boots the disk image directly (for games that never see DOS).
A zip recipe starts on the target drive, for games that just get
unpacked rather than installed.
[source]
| Key | What |
|---|---|
url | Where the disk images can be downloaded |
sha256 | Optional checksum of the download |
The sample implementation fetches missing media from this URL before a test run. If you already have the images locally, the section is informational.
[discs]
images lists the disk image files in insertion order. The first image
is mounted at the start; the rest are brought in by swap actions in
the prompt sequence.
[installer]
| Key | What |
|---|---|
path | Installer executable on the source drive, e.g. INSTALL.EXE |
source_drive | Drive the media is mounted on, usually A or D |
target_drive | Drive the game installs to, usually C |
[settings]
Optional emulator settings for this game, written into the config for the run. Currently honored keys and the config sections they land in:
| Key | Section |
|---|---|
cpu_cycles, cpu_cycles_protected, cpu_throttle, cpu_type | [cpu] |
keyboard_layout | [dos] |
machine, memsize | [dosbox] |
output | [sdl] |
joysticktype | [joystick] |
Two of these matter for determinism and are pinned even if you omit
them: cpu_cycles (the default auto ramps with host load, so
recipes run at a fixed rate, 12000 unless specified) and
keyboard_layout (defaults to us, because recorded scancodes only
reproduce the same characters under the same layout).
[prompts]
sequence is the heart of the recipe: an ordered list of steps, each a
small table. The available step keys:
| Key | What |
|---|---|
wait = "text" | Wait until the text appears on screen (Lua pattern, 1800 frame timeout) |
key = "enter" | Press and release one key. Named keys (enter, esc, up, down, left, right, space, tab, backspace), single characters, or raw KBD_* names |
type = "C:\\GAME" | Type a string followed by Enter |
action = "swap:N" | Request a swap to disc number N (1-based index into images) |
pause = 70 | Wait a fixed number of frames (70 frames is one second) |
change_drive = "C" | Switch to another drive at the DOS prompt |
repeat = "wait", until = "gfx" | Loop until the video mode changes: gfx waits for a graphics mode, text for text mode. Instead of "wait", a key name presses that key each round |
Keys can be combined in one step. A step like
{ wait = "Insert disk 2", action = "swap:2", key = "enter" } waits
for the prompt, requests the swap, then confirms with Enter, in that
order.
The repeat/until pair covers graphical setup programs that a text
wait cannot see: rather than hardcoding how many screens an installer
shows, loop until it drops back to text mode. Menu counts and ordering
vary between installer versions, so a terminating condition is more
robust than a fixed list.
[verify]
files lists paths that must exist after the installation, as seen
from inside DOS. This is the pass/fail criterion: if the installer
finished but these files are missing, the run failed.
[capture]
screenshot_at lists prompt indices (0-based position in the
sequence) at which the screen text is captured into the script output,
useful for debugging a recipe or documenting an install.
How steps map to Lua
Recipes are convertible to plain Lua by design. The sample implementation generates exactly these calls:
| Recipe step | Lua |
|---|---|
wait | dosbox.wait_for_text(pattern, 1800) |
key | dosbox.key("KBD_enter", true), a short wait, then dosbox.key("KBD_enter", false) |
type | dosbox.type(text .. "\n") |
pause | dosbox.wait_frames(n) |
change_drive | dosbox.type("C:\n") |
repeat/until | a while dosbox.is_text_mode() do ... end loop |
action = "swap:N" | dosbox.output["swap_0"] = N, then the controlling side calls POST /api/v1/drive/swap |
The swap is the one step that needs a partner outside the script: the
Lua sandbox cannot change mounts itself. The script publishes the swap
request through its output table, the controller polls
GET /api/v1/script/status, performs the swap over the REST API, and
the script continues. Everything else runs entirely inside the
emulator.
The included scripts
The source tree ships the sample implementation under
tests/integration/:
record_install.pywalks an installer with you at the keyboard and records every input with frame timing. Recordings replay deterministically; they're how recipes for stubborn installers get bootstrapped.replay_install.pyandreplay_recording.pyreplay such a recording against a fresh instance.test_e2e_installs.pyis the end-to-end suite: for every bundled recipe it starts the emulator, runs the generated script, performs the disk swaps, and checks the[verify]files.e2e_helpers.pyholds the manifest parser and the Lua generator, the part you'd port if you want recipe support in your own launcher.
Run a single recipe end to end, with the window visible:
DOSBOX_VISIBLE=1 python -m pytest tests/integration/test_e2e_installs.py -k "one-must-fall" -v -sWriting your own
Start from a bundled recipe with the same media type. Run the
installer once by hand and write down every prompt exactly as it
appears on screen; the wait texts must match what the screen reader
sees. When a prompt sequence misbehaves, capture the screen text at the
failing step (screenshot_at) and compare it with your pattern. For
graphical setup tools, reach for the repeat/until loop and fixed
pause steps rather than trying to read pixels: a graphics-mode screen
has no text to wait for.