#!/usr/bin/env bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT # # verify-atarist-joy.sh - Joystick gate for the Atari ST and STe builds: # both ports, every direction, the fire button and every release, driven # through Hatari's keyboard joysticks and read back from what examples/joy # draws. # # The port (src/atarist/input.c) decodes the IKBD's joystick packets: port 1, # the dedicated stick port, is JOYSTICK_0 (stick A) and port 0, the mouse # port, JOYSTICK_1 (stick B); one fire button each, both always connected. # The port keeps TOS's IKBD mode, in which port 0 is the mouse's: the IKBD # reports no stick there, so B must stay at rest whatever is held on it, and # port 1's fire, the same line as the mouse's right button, arrives as that # button. # Hatari's [Joystick0] and [Joystick1] (a config file of the gate's own) are # keyboard joysticks on host keys, which xdotool holds on a private Xvfb # display; Hatari's own screenshots (its command FIFO's screenshot shortcut) # go through scripts/lib/joyScreen.py (scripts/lib/joyGate.sh). Escape must # then end JOY. # # Usage: scripts/verify-atarist-joy.sh [atarist|ste] [MACHINE] # (default atarist on an st; ste on an ste) # Env: JOY_SHOTS also keep the screenshots in this folder # Requires: Hatari, EmuTOS, Xvfb, xdotool, python3 + PIL; # build//bin/JOY.PRG (make atarist / make ste). set -uo pipefail repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) source "$repo/scripts/lib/joeyEnv.sh" source "$repo/scripts/lib/joyGate.sh" joeyNeed HATARI hatari joeyNeed EMUTOS_IMG emutos build="${1:-atarist}" # The ST build writes 3-bit colours on any machine (joyScreen.py --st). case "$build" in atarist) machine="${2:-st}"; JOY_SCREEN_ARGS=(--st) ;; ste) machine="${2:-ste}" ;; *) echo "verify-atarist-joy: build must be atarist or ste" >&2; exit 2 ;; esac JOY_LABEL="verify-$build-joy" program="$repo/build/$build/bin/JOY.PRG" wall=300 # hard cap on Hatari, whatever happens to this script readySeconds=90 # EmuTOS booting and JOY drawing shotSeconds=5 # the screenshot shortcut until Hatari has written the file exitSeconds=10 # Escape until JOY's screen is gone [ -f "$program" ] || { echo "$JOY_LABEL: build JOY.PRG first (make $build)" >&2; exit 2; } JOY_WORK=$(mktemp -d -t joeylib-st-joy.XXXXXX) guard="" hatari="" xvfb="" display="" win="" cleanup() { joeyStop "$hatari" "$guard" "$xvfb" 2> /dev/null rm -rf "$JOY_WORK" } trap cleanup EXIT # Control, its host key (xdotool's name) and Hatari's name for the key. # Hatari's Joystick1 is ST port 1 (stick A), Joystick0 port 0 (stick B). keys=( "A-left j J" "A-right l L" "A-up i I" "A-down k K" "A-button0 a A" "B-left f F" "B-right h H" "B-up t T" "B-down g G" "B-button0 q Q" ) # key CONTROL -- the host key for a control. key() { local entry local control local host for entry in "${keys[@]}"; do read -r control host _ <<< "$entry" if [ "$control" = "$1" ]; then printf '%s\n' "$host" return 0 fi done return 1 } # hatariKey CONTROL -- Hatari's name for the control's key. hatariKey() { local entry local control local name for entry in "${keys[@]}"; do read -r control _ name <<< "$entry" if [ "$control" = "$1" ]; then printf '%s\n' "$name" return 0 fi done return 1 } # hold keydown|keyup CONTROL... hold() { local action="$1" local control local host shift for control in "$@"; do host=$(key "$control") || { joyFail "no key for $control"; continue; } DISPLAY="$display" xdotool "$action" "$host" done } joyPress() { hold keydown "$@" } joyRelease() { hold keyup "$@" } # Hatari makes the file before it writes it: it is done when it has bytes # and one more poll adds none. joyShot() { local before local found local size="" local deadline before=$(find "$JOY_WORK/shots" -name '*.png' | wc -l) timeout 5 bash -c 'echo "hatari-shortcut screenshot" > "$1"' _ "$JOY_WORK/cmd.fifo" || return 1 deadline=$((SECONDS + shotSeconds)) while [ "$SECONDS" -lt "$deadline" ]; do sleep 0.2 [ "$(find "$JOY_WORK/shots" -name '*.png' | wc -l)" -gt "$before" ] || continue found=$(ls -t "$JOY_WORK/shots"/*.png | head -1) [ -s "$found" ] || continue if [ "$(stat -c %s "$found")" = "$size" ]; then mv "$found" "$1" return 0 fi size=$(stat -c %s "$found") done return 1 } # joyStage -- whether a screenshot now shows JOY's stage. joyStage() { joyShot "$JOY_WORK/probe.png" && python3 "$repo/scripts/lib/joyScreen.py" "$JOY_WORK/probe.png" "${JOY_SCREEN_ARGS[@]}" > /dev/null 2>&1 } # The keyboard joysticks: Joystick1 (port 1) stick A, Joystick0 stick B. { for port in 1 0; do stick=$([ "$port" = 1 ] && echo A || echo B) printf '[Joystick%s]\nnJoystickMode = 2\nkUp = %s\nkDown = %s\nkLeft = %s\nkRight = %s\nkFire = %s\n\n' "$port" \ "$(hatariKey "$stick-up")" "$(hatariKey "$stick-down")" "$(hatariKey "$stick-left")" \ "$(hatariKey "$stick-right")" "$(hatariKey "$stick-button0")" done } > "$JOY_WORK/joy.cfg" display=":$((RANDOM % 400 + 5100))" Xvfb "$display" -screen 0 1024x768x24 -nolisten tcp > /dev/null 2>&1 & xvfb=$! sleep 1 mkdir -p "$JOY_WORK/shots" "$JOY_WORK/home" (cd "$JOY_WORK" && exec env HOME="$JOY_WORK/home" XDG_CONFIG_HOME="$JOY_WORK/home" DISPLAY="$display" SDL_AUDIODRIVER=dummy \ timeout -s KILL "$wall" "$HATARI" --configfile "$JOY_WORK/joy.cfg" --machine "$machine" --tos "$EMUTOS_IMG" \ --harddrive "$(dirname "$program")" --gemdos-drive C --auto "C:\\$(basename "$program")" \ --sound off --statusbar false --crop true --screenshot-dir "$JOY_WORK/shots" \ --cmd-fifo "$JOY_WORK/cmd.fifo") > "$JOY_WORK/hatari.log" 2>&1 & guard=$! hatari=$(joeyChild "$guard") ready=0 for ((waited = 0; waited < readySeconds; waited += 2)); do if [ -p "$JOY_WORK/cmd.fifo" ] && joyStage; then ready=1 break fi joeyAlive "$guard" || break sleep 2 done if [ "$ready" != 1 ]; then echo "$JOY_LABEL: FAIL - Hatari with JOY did not come up (log follows)" >&2 grep -v '^Screen dump saved' "$JOY_WORK/hatari.log" | tail -20 >&2 if [ -n "${JOY_SHOTS:-}" ] && [ -f "$JOY_WORK/probe.png" ]; then mkdir -p "$JOY_SHOTS" cp "$JOY_WORK/probe.png" "$JOY_SHOTS/not-ready.png" fi exit 1 fi # Xvfb has no window manager: the keyboard goes to the focused window, and # xdotool's keys (XTEST, which SDL takes) to the one that has it. win=$(joeyFindWindow "$display" "$hatari") if [ -n "$win" ]; then DISPLAY="$display" xdotool windowfocus --sync "$win" 2> /dev/null || true DISPLAY="$display" xdotool mousemove --window "$win" 20 20 fi joyCheck "rest" "A=$JOY_REST B=$JOY_REST" # Stick A's steps (the first seven), then B held: the mouse's port. joyRunSteps "${JOY_STEPS_ONE_BUTTON[@]:0:7}" "B held: port 0 is the mouse's|B-left B-up B-button0|REST|REST" DISPLAY="$display" xdotool key Escape gone=0 for ((waited = 0; waited < exitSeconds; waited++)); do sleep 1 if ! joyStage; then gone=1 break fi done [ "$gone" = 1 ] || joyFail "Escape did not end JOY" joyResult "$JOY_LABEL" "$machine: port 1 every direction, the fire button and every release; port 0 left to the mouse; Escape ends JOY"