| name | run-falcon-port |
| description | Build, run, screenshot, and drive the FRUA Atari Falcon030/TT030 port (frua.prg) in the Hatari emulator. Use when asked to run, build, boot, screenshot, or interact with the game/engine, or run its test suite. |
This repo cross-compiles frua.prg (a Forgotten Realms: Unlimited Adventures
port) for the Atari Falcon030/TT030 and runs it inside the Hatari emulator.
There is no native Linux binary — you drive it through Hatari under a virtual X
server. The handle is .claude/skills/run-falcon-port/driver.sh: it builds,
boots Hatari under Xvfb, screenshots the Falcon display, and sends keystrokes.
It is a thin headless wrapper over the project's real harness,
tools/hatari_ui.sh.
All paths below are relative to the repo root.
Prerequisites
This box is Linux Mint 22.2 (Ubuntu 24.04 base) → apt. (An earlier revision
of this skill said Arch/pacman; that was wrong for this machine.)
sudo apt install hatari xvfb imagemagick x11-utils xdotool python3
- Falcon TOS ROM ships with the
hatari package at
/usr/share/hatari/TOSv4.04.img (the Makefile/harness default).
- Cross toolchain — the hard prerequisite.
make needs the private
m68k-atari-mint GCC with an added soft-float m68020-60 multilib
(the FPU-less Falcon030 target). It is NOT a stock package; build it per
docs/toolchain-softfloat-020.md into ~/opt/cross-mint (the Makefile's
TOOLROOT default). Verify it resolves: m68k-atari-mint-gcc --version.
- Game data is copyrighted and git-ignored (
data/). It is staged at
data/work/gamedata here. run/screenshot need it; without it the engine
boots "with no resources" (a near-empty screen). Staging is documented in
docs/mac-release.md (make gamedata DSN=HEIRS.DSN).
Newer Hatari (2.6.1) — already built at ~/opt/hatari
Ubuntu/Mint ship 2.4.1. A 2.6.1 build lives at ~/opt/hatari/bin/hatari and
driver.sh picks it up automatically (override with HATARI_BIN). Nothing to do
unless it's missing.
If you need to rebuild it: Debian's own 2.6.1 .deb will NOT run here (it wants
glibc 2.42; Mint has 2.39), so build from source — and sudo needs a password on
this box, so get the dev headers WITHOUT root by unpacking the debs into a
sysroot:
apt-get download libsdl2-dev zlib1g-dev libpng-dev
for d in *.deb; do dpkg-deb -x "$d" sysroot; done
ln -sf /usr/lib/x86_64-linux-gnu/libSDL2-2.0.so.0.3000.0 sysroot/usr/lib/x86_64-linux-gnu/libSDL2.so
ln -sf /usr/lib/x86_64-linux-gnu/libz.so.1.3 sysroot/usr/lib/x86_64-linux-gnu/libz.so
curl -LO https://github.com/hatari/hatari/archive/refs/tags/v2.6.1.tar.gz
cmake -S hatari-2.6.1 -B build -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=$HOME/opt/hatari \
-DCMAKE_LIBRARY_PATH=$PWD/sysroot/usr/lib/x86_64-linux-gnu \
-DCMAKE_C_FLAGS="-I$PWD/sysroot/usr/include/x86_64-linux-gnu -I$PWD/sysroot/usr/include"
cmake --build build -j"$(nproc)" && cmake --install build
The -I .../x86_64-linux-gnu is load-bearing: Ubuntu's SDL_config.h is a shim
that includes SDL2/_real_SDL_config.h from the arch-specific include dir, which
GCC only searches by default under /usr.
Build
make
Output: frua.prg (an Atari ST M68K executable). Confirm 020 codegen took:
m68k-atari-mint-objdump -d src/engine/boot.o | grep -cE 'muls\.l|bfextu|bfins'
Run / screenshot (agent path) — USE THIS
driver.sh auto-starts a persistent Xvfb on :99 when no usable DISPLAY
exists, then delegates to tools/hatari_ui.sh. One-shot build→boot→screenshot:
.claude/skills/run-falcon-port/driver.sh smoke /tmp/frua.png
Or step by step (each call is a separate invocation; Hatari persists between
them via state in /tmp/frua-ui):
D=.claude/skills/run-falcon-port/driver.sh
"$D" start
"$D" dump /tmp/frua.png
"$D" shots /tmp/frua.png
"$D" key e
"$D" click 150 298
"$D" drag 85 65 85 92
"$D" beginplay
"$D" wait 'regex' [n]
"$D" log
"$D" dbg '<cmd>'
"$D" quit
"$D" stop
Sound (agent path) — how to check audio without ears
A screenshot can't tell you whether the DMA path is silent, so driver.sh sound
records the emulator's audio and reports it:
"$D" sound /tmp/frua-sound.wav
It builds the FRUA_SNDTEST harness (src/engine/boot.c — fires four sfx
through jt52 right after boot, and logs the two gates -806 / -17444 so a
silent run says WHICH gate closed), boots with --avirecord, quits Hatari
gracefully, and runs tools/avi_audio.py to print every burst of sound:
4 burst(s) of sound:
#1 start 33.33s dur 0.98s peak 7872 ~ 386 Hz dominant
...
Four bursts = the sfx path is live end-to-end. NONE — the DMA path produced silence (exit 1) = it is not. Afterwards run plain make to get the stock
binary back (the harness build is left in place).
Sound gotchas, all of which cost real time to find:
- Quit Hatari, don't kill it. An
--avirecord AVI is finalized on close;
stop SIGKILLs, leaving RIFF sizes at 0 and the file unparseable. Use quit
(needs --confirm-quit off, which sound passes).
- Record at real speed (
--fast-forward no), or the samples are dropped.
- Hatari writes UNPADDED AVI chunks, against the spec — a spec-conformant
parser desyncs a byte on the first odd-sized chunk and finds no audio at all.
tools/avi_audio.py resyncs; don't hand this AVI to a strict reader.
"main: sound chip locked" in the log is success — it's the engine
reporting Locksnd() worked, not an error.
Debugger (dbg). Stock Hatari 2.4.1+ speaks the hatari-debug protocol
over the cmd-fifo — no fork needed. Boot with HATARI_BIN=hrdb "$D" start to
auto-load frua.prg's symbol table, then reference engine names directly:
HATARI_BIN=hrdb "$D" start
"$D" dbg 'd _l28d4'
"$D" dbg 'm _jt272'
"$D" dbg 'e _g_a5_below+12288'
"$D" dbg 'r'
"$D" dbg 'b pc=_jt272'
"$D" dbg 'b'
Syntax notes (stock Hatari): m/d take a bare symbol or hex — NOT sym+off
arithmetic; use e sym+off to get the address, then m $hex. b alone lists
breakpoints; b pc=_sym sets one. Output is captured from the stdout log. To
catch a crash PC, boot with HATARI_ARGS="--debug" so a CPU exception drops
into the debugger, then dbg 'r'. (Avoid --trace cpu_exception — it floods
on every TOS TRAP and makes boot impractically slow.)
Then view the PNG with the Read tool. A correct boot looks like the main menu:
title "UNLIMITED ADVENTURES", "CURRENT GAME DESIGN: HEIRS.DSN", and a 2-column
button grid (PLAY THE GAME … QUIT FROM GAME), over a Hatari status bar reading
"14MB Falcon, TOS 4.04" (the dev default; FRUA_MEM=4 or 1 selects the
memory-fit configurations — the shipping floor is 4MB today, 1MB the goal).
- Prefer
dump over shots/shot. dump triggers Hatari's built-in
screendump over the control FIFO (hatari-shortcut screenshot; the launch sets
--screenshot-dir $STATE/shots --crop yes). It needs no X window grab or
imagemagick, gives a clean statusbar-cropped 688x490 PNG, and works in
fullscreen (where the X-grab breaks). shots (the X-grab) still works and is
the fallback. Both wait for a fresh frame.
- Env:
GEMDOS_DIR (C: mount, default data/work/gamedata), FALCON_TOS,
FRUA_XVFB_DISPLAY (default :99), or set DISPLAY to reuse a real X server.
Run the TT030 variant (same binary, TT-low letterbox)
The same frua.prg runs on an emulated TT (the _VDO cookie picks the
TT-shifter backend: 320x480 TT low, the 320x200 frame line-doubled into a
40-line letterbox). Boot it through this same driver with overrides —
HATARI_ARGS is applied last, so it overrides --machine:
export FALCON_TOS=~/opt/etos512us.img
export HATARI_ARGS="--machine tt --dsp none"
"$D" start
TT-specific input gotcha: Hatari's absolute click targeting desyncs from the
engine (engine clamps its pointer to 320x200; Hatari's model spans 320x480).
Re-sync BEFORE every targeted click with two dead-zone clicks, then click at
(2*engine_x, engine_y) window coordinates (x doubled, y RAW engine, not
the on-screen 2*y+40):
"$D" click 638 478 && sleep 1
"$D" click 1 1 && sleep 1
"$D" click 460 179
Keys need no translation. Sound works on the TT too (the STE-DMA ring path
in sound_falcon.c, picked by the _VDO cookie at 25033 Hz). To capture
audio, boot with --fast-forward no in HATARI_ARGS (fast-forward garbles
Hatari's sound) and record the pulse monitor:
parec -d "$(pactl get-default-sink).monitor" --file-format=wav out.wav.
Run the mono ST/STE variant (BWMODE, B&W)
The black-and-white build (make run-mono = ST High + mono monitor) is a
DIFFERENT binary — a bare 68000 with -DFRUA_BWMODE — plus an ST machine on a
mono monitor. Build it, then boot through this driver with overrides:
make CPU68K=68000 EXTRA_CFLAGS='-DFRUA_BWMODE'
export FALCON_TOS=/usr/share/hatari/tos206us.img
export FRUA_MEM=4
export HATARI_ARGS='--machine st --monitor mono --dsp none'
"$D" start
"$D" dump /tmp/mono.png
Two overrides that bite (both cost time here):
- The TOS goes in
FALCON_TOS, NOT in HATARI_ARGS. Hatari validates
--tos at PARSE time, and the harness's hardcoded --tos $FALCON_TOS comes
first — so a later --tos in HATARI_ARGS never runs; Hatari dies on the
first (here nonexistent) TOSv4.04.img. Point FALCON_TOS at the ST ROM.
(--machine, --monitor, --dsp are fine to override via HATARI_ARGS;
they are last-wins, no file check.)
FRUA_MEM=4 — its default is 14 MB, which is illegal on an ST and aborts
the boot.
Verified (2026-07-18) to menu: modal up; the conout log shows the native mono
path — Atari ST high (640x400 mono, 2x2 dither) and sthigh: 640x400 mono, ENGINE B&W 480x300 1:1 up — and the status bar reads 8MHz(CE)/- 4MB ST(WS3), TOS 2.06, MONO 71 Hz. The menu renders as white outlined chips on black. Run a
plain make afterwards to restore the default Falcon binary.
Run (human path)
On a machine with a real display, the Makefile boots Hatari in a window:
make run
make run-game DSN=HEIRS.DSN
Useless headless (a window opens and waits) — use the driver instead.
Test
make test
make test-slow
make test-slow needs DISPLAY — under headless, run it after the driver has
brought up Xvfb, e.g. DISPLAY=:99 make test-slow.
Gotchas
- NEVER
pkill -f hatari. -f matches the full command line, and your
launching shell contains the word "hatari" — you kill your own shell and every
command silently exits 1. Use pkill -9 -x hatari (exact name). The driver's
stop does this for you.
- A SIGKILL'd driver orphans Hatari.
start blocks until the menu is up; if
your outer command times out mid-start, the disowned Hatari keeps running
and holds the C: mount, stalling the next boot. Give start ≥150s of headroom,
and pkill -9 -x hatari before retrying if a boot hangs.
- Drive the main menu with KEYBOARD ACCELERATORS —
driver.sh key <letter>,
no mouse needed. Each menu button has a letter accelerator (first letter of
the item): key e = EDIT MODULES, key p = PLAY THE GAME, etc. Inside
dialogs the nav buttons take accels too (e.g. key n = NEXT page, key p =
PREV). This is the RELIABLE way to reach the design editor and other "mouse-
only"-looking screens. Verified: key e opens the record editor, key n
pages it. Prefer this over the mouse.
- Synthetic MOUSE clicks DO inject —
driver.sh click <x> <y> [btn] works
because start bakes in --mousewarp no (with warp ON, Hatari runs the host
pointer relative/grabbed and absolute XTEST positioning is consumed, so clicks
land nowhere; warp OFF, XTEST button events register — same path as key).
Verified live 2026-07-18: clicking the GEO map editor's 3D view moves the edit
cursor; clicking a menu button navigates. Prefer keyboard accels anyway
(above) for reaching screens — they're drift-free — and use clicks for the
in-view work keys can't do (editor cell placement). Click caveats:
- Targeting is only stable WITHIN one video mode. A mode change (menu →
editor → GEM desktop) makes Hatari destroy+recreate its SDL window, so the
window id churns (the driver re-finds it per call; a cached id goes stale
→
BadWindow) and the screenshot size shifts (menu 688×526, GEM 648×602) so
screenshot↔window pixel mapping is no longer 1:1. Re-screenshot after any
transition and read coordinates off THAT frame.
- IKBD rate-limits synthesized motion, so a long jump can fire mid-travel; the
click/drag helpers do a double-move + settle to absorb it.
- FILE / MAP / UTILITIES in the editor are Mac press-DRAG menus — they drop
only while the button is held and select on release over an item. An atomic
click can't drive them; use driver.sh drag <x1> <y1> <x2> <y2> [btn]
(press at the title, travel to the item in steps, release). Verify the
selection by its EFFECT (a dialog, a state change, a saved file), not by trying
to photograph the dropped menu.
- NEVER screenshot while a mouse button is held. A held button under Hatari's
SDL grab + a screenshot tool competing for the X server DEADLOCKS the whole
:99 display (even xdotool mouseup/xwininfo then block); recovery is
pkill -9 -x hatari. So: don't try to capture a menu mid-hold — always let
drag complete (it releases) and screenshot after. If the display ever hangs,
a stuck held button is the first suspect: pkill -9 -x hatari clears the grab.
- Headless GEO-editor testing recipe: reach it by keyboard (
key e → Edit
Modules, key Return → Open the highlighted module — reliable), click inside
the 3D map view for cell edits, drag the FILE/MAP/UTILITIES menus, then
assert by saving and re-reading: drive FILE → Save, and parse the written
GEO<NNN>.DAT offline with tools/geo.py (Geo.parse) — a file roundtrip is a
far more robust assertion than a pixel diff of the mouse-driven view.
shots prints a harmless [[: arithmetic syntax error line under Xvfb
(the magick compare AE metric trips set -o pipefail when frames differ).
It self-recovers and still saves a correct settled frame — ignore the stderr.
- ★★
shots CANNOT see a transient message — it skips them by construction.
FRUA prints messages ("BORIS CANNOT DO MAGIC", "EMPTY!", "PIOUS BUYS 20
ARROWS") on row 24, dwells ~1s (l4bac -> jt476), then repaints. shots
waits for the frame to settle, so it grabs the screen after the message
is gone — and the command bar it finds there looks like nothing ever happened.
This cost months: "CAST/INV/shop messages are invisible" was carried as a
known P1 bug that never existed. For anything transient use shot
(immediate) ~1s after the keypress, and build -DFRUA_MSGTRACE to log the
exact string and the dwell. shots remains correct for STATIC screens.
SDL_VIDEODRIVER=x11 is load-bearing for screenshots; the harness sets it.
- Boot speed varies with host load (~12s here at
--fast-forward); the
"Your system is too slow … sound samples" warning is cosmetic.
Troubleshooting
| Symptom | Fix |
|---|
make fails: command not found / wrong multilib | Cross toolchain missing or stock. Build the soft-float m68020-60 toolchain per docs/toolchain-softfloat-020.md; check m68k-atari-mint-gcc --version. |
| boot hangs, never logs "menu: modal up" | Stale Hatari holding C:. pkill -9 -x hatari, then retry start. |
no Hatari window found / empty (~358 B) PNG | Xvfb not up or grab too early. Driver auto-starts Xvfb on :99; re-run shots (it retries). |
| screenshot is a half-drawn/black play screen | Use shots, not shot; it waits for the frame to settle. |
| a transient message never appears ("CAST does nothing") | You used shots. It waits for the frame to SETTLE, so it skips transient text BY CONSTRUCTION. Use shot (immediate) within ~1s of the keypress. See the Gotcha below. |
make run-game says gamedata not found | Copyrighted data/frua-mac/joined isn't unpacked. See docs/mac-release.md. |