Scripting

A script is a JSON file of steps that drives a machine with no window and nobody watching. Hand one to any Emu198x binary with --script and it boots the hardware, works down the file, writes out whatever it was told to write, and prints a report of what happened. Run the same file tomorrow and the machine does the same thing, frame for frame.

That last part is the reason to bother. A screenshot from a session someone drove by hand is an anecdote — nobody else can check it. A screenshot from a script comes with the file that produced it, and anyone holding the same firmware gets the same picture. Every Spectrum boot capture on the system matrix — one for the machine and one for each of its variants — was made this way.

The mental model

A script or MCP?

Both drive the same session through the same verbs. What separates them is who decides the next step.

Write a script when you know the whole run before it starts: boot a machine for a screenshot, load a tape and capture what it paints, run a test image and save the result. The file is one artefact you can commit, diff and hand to someone else.

Reach for MCP when the next step depends on what the last one returned — chasing a bug, walking a program's memory, anything where you would want to look before choosing. A script cannot look.

What works today

All 30 machines take --script PATH, and --script implies headless: no window opens. The step vocabulary is 47 actions and it is the same 47 on every binary, down to the spelling. What varies between machines is which of them a machine can carry out.

NeedSteps
Advance executionrun_frames, run_ticks, step, run_until_pc, run_until_any_pc, run_until_line, run_until_mem_change
Wait for a statewait_for_boot, wait_for_query_contains, wait_for_query_bool
Inspectquery_paths, query, query_cpu, query_ay, memory_read, disasm, port_read
Change memory and portspoke_byte, poke_word, port_write
Watch for writeswatch_memory_start, watch_memory_log, watch_memory_clear, watch_ay_start, watch_ay_log, watch_ay_clear
Inputpress_key, press_keys, type_string, input
Media and stateload_media, media_transport, autoload_tape, load_basic_program, load_snapshot, save_snapshot, set_machine, reset
Capturesave_screenshot, start_audio_recording, stop_audio_recording, save_audio_capture, clear_audio_capture, start_video_recording, stop_video_recording
Source-level debuggingload_debug_info, debug_symbol

Writing a script

The file is a JSON array. Each step is an object with an action and whatever that action needs. Here is one that boots a machine, reads its text screen back, and saves a PNG — put it in boot.json:

[
  {"action":"wait_for_boot","max_frames":300},
  {"action":"query","path":"screen.text.lines"},
  {"action":"save_screenshot","path":"boot.png"}
]

Run it against a Spectrum:

./emu198x-spectrum --script boot.json

The machine needs its firmware first, found the same way a window finds it — the getting started page covers where each one looks. Nothing prints while it runs; when it finishes, boot.png is on disk and one JSON report is on standard output. Trimmed to the part worth reading:

{"kind":"wait_for_boot","frames":87,"reached":24321024,"reason":"found copyright banner on row 23","row":23}
{"kind":"query","result":{"path":"screen.text.lines","value":[ ... ,"© 1982 Sinclair Research Ltd    "]}}

Every step that has something to report answers with what the machine did rather than a promise that it did something. wait_for_boot stopped on frame 87 and says why it believed the machine had booted.

That same file runs on a different machine without a word changed, because nothing in it names Spectrum hardware:

./emu198x-c64 --script boot.json

A C64 boots to READY. instead of a copyright line, so the text that comes back differs — but the steps, the report and the PNG all arrive the same way.

Typing at the machine

Input is where a script starts to look like someone at the keyboard. This one waits for the Spectrum to boot, works out two plus two on it, reads the answer off the screen, and photographs the result:

[
  {"action":"wait_for_boot","max_frames":300},
  {"action":"press_key","key":"P"},
  {"action":"type_string","text":"2"},
  {"action":"press_keys","keys":["SymbolShift","K"]},
  {"action":"type_string","text":"2\n"},
  {"action":"query","path":"screen.text.lines"},
  {"action":"save_screenshot","path":"sum.png"}
]

Each of those steps is the real keyboard matrix, not a shortcut around it. Pressing P at a 48K BASIC prompt gives the whole PRINT keyword, which is why the script never spells it out. press_keys holds Symbol Shift and K together to reach the + sign, because that is where the plus lives on a rubber-key Spectrum. The trailing \n is Enter.

{"kind":"press_key","key":"P","hold_frames":3,"reached":25439232}
{"kind":"press_keys","keys":["SymbolShift","K"],"hold_frames":3,"reached":30471168}
{"kind":"query","result":{"path":"screen.text.lines","value":["4                               ", ... ,"0 OK, 0:1                       "]}}

A key is held for three frames and released, the way a finger does, and the screen says 4. Nothing in that run needed a person, and running it again produces the same four.

Finding out what a machine accepts

Three things are worth knowing before writing a longer script, and all three are questions the binary will answer.

Its headless flags. Plain --help prints the flags for the window and stops there. The automation flags live behind a second help screen:

./emu198x-spectrum --headless --help

Those flags differ sharply between machines, and several are shorthand for a step. On the Spectrum, --tape game.tap is the same thing as a load_media step on slot tape-1, and the help says so.

Two machines have no second help screen: the ZX80 and the ZX81 reject --headless outright. Their scripting works — --script takes the same vocabulary there as everywhere else — so a script written for another machine still runs. Their flags are readable only from the source.

Its query paths. A query_paths step lists every path the machine publishes, and takes an optional prefix to keep the answer short. Ask before hard-coding: a Spectrum publishes 65 paths and a C64 well over twice that, and the names past cpu., run., session. and capture. are the hardware's own.

Its step vocabulary. No file lists it, so the shortest route is to be wrong on purpose. Feed the binary a step that cannot exist and the rejection names all 47:

echo '[{"action":"tell_me_the_steps"}]' > probe.json
./emu198x-spectrum --script probe.json

Getting real software to load

Inserting a tape or a disk is one step. Persuading the machine to read it is the machine's own ritual, and it differs on every one of them. These were each worked out by loading commercial software and watching what happened, so they are what worked rather than what ought to.

What each machine needs after load_media
MachineThe ritualThe part that catches people
ZX Spectrumautoload_tape on tape-1The only machine with no --screenshot flag, so its captures go through a save_screenshot step.
Commodore 64--disk and --autoload-disk, then type_string "RUN\n"Wait for drive8.motor_on to go true and then false. A fixed frame count is unusable — a real 1541 load runs anywhere from 5,000 to 15,000 frames depending on the title.
Amiga--disk; it boots itselfBudget about 1,500 frames before anything appears. A blank frame at 300 is a machine still booting, not a broken one.
BBC Micro*TAPE, then CHAIN""--sideways 12=basic.rom is required. The runtime installs only the MOS, so without it there is no BASIC and therefore no CHAIN to type.
Acorn ElectronCHAIN""Type the quotes as press_keys ["shift", "2"]. type_string drops a " without saying so, and reports it as typed.
Acorn Atom*LOAD"NAME", then load_media, then a bare returnThe COS prints PLAY TAPE and waits for a keypress — that return is the whole trick. The name has to be read out of the UEF; an empty one is refused.
Dragon 32CLOADM then EXEC, or CLOAD then RUNWhich pair depends on whether the tape holds machine code or BASIC. The wrong one gives ?FM ERROR.
ZX81LOAD "", then media_transport, then R to runTwo traps. Keys need hold_frames: 20 — the default is dropped by the keyboard scan. And --ram-bytes 16384, or every 16K title fails looking exactly like a bad tape.
Amstrad CPCRUN", then return at the promptSame quote problem as the Electron. Tapes are .cdt only; a .wav is refused.

Two things worth knowing before you spend an afternoon on a machine that is behaving strangely. media_transport works on the Spectrum and is refused by the BBC, Electron and Dragon, so on those three the deck free-runs and a script cannot stop or rewind it. And a tape that appears to stall may only be slow: re-run at a much higher frame budget before concluding anything, because a load in progress and a machine that has hung look identical from outside.

What you need that we cannot give you

Firmware and software, the same as any other way of running these machines. Emu198x ships no ROMs, no BIOS images, and no disks, tapes or cartridges; the downloads page sets out the position on that. A machine that needs firmware fails before the first step runs, and names the file it went looking for.

That travels with the script. A script that loads media names a path on your disk, so handing the file to someone else hands them the steps and not the tape. Scripts that lean only on firmware — a boot capture, a BASIC session — are the ones that survive the trip.

What does not work yet