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/.

ToolWhat it doesFile
Macro recorderRecords 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 onrecord_install.py
Session recorderSame recording mechanism for a free-form session instead of an installrecord_session.py
Install replayerReplays a recorded installation in a visible windowreplay_install.py
Recording replayerReplays any recording JSON through the APIreplay_recording.py
Recipe test runnerRuns every bundled recipe end to end; doubles as the integration test suitetest_e2e_installs.py
Recipe engineTurns a recipe into a Lua script and drives the emulator through the REST API; usable as a librarye2e_helpers.py
API clientPlain Python client for the REST API, used by all the tools abovedosbox_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]

KeyWhat
nameDisplay name of the game
slugShort identifier, used for directories and artifacts
mediafloppy, cdrom-iso, cdrom-cue, booter, or zip
licenseLicensing 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]

KeyWhat
urlWhere the disk images can be downloaded
sha256Optional 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]

KeyWhat
pathInstaller executable on the source drive, e.g. INSTALL.EXE
source_driveDrive the media is mounted on, usually A or D
target_driveDrive 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:

KeySection
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:

KeyWhat
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 = 70Wait 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 stepLua
waitdosbox.wait_for_text(pattern, 1800)
keydosbox.key("KBD_enter", true), a short wait, then dosbox.key("KBD_enter", false)
typedosbox.type(text .. "\n")
pausedosbox.wait_frames(n)
change_drivedosbox.type("C:\n")
repeat/untila 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.py walks 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.py and replay_recording.py replay such a recording against a fresh instance.
  • test_e2e_installs.py is 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.py holds 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 -s

Writing 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.