#!/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-amiga-joy.sh - Joystick gate for the Amiga: both ports, every # direction, both buttons and every release, driven through FS-UAE's # joystick ports and read back from what examples/joy draws. # # The port (src/amiga/input.c) reads both ports through lowlevel.library's # ReadJoyPort: port 1, the joystick port, is JOYSTICK_0 (stick A) and port 0, # the mouse port, JOYSTICK_1 (stick B), each a stick while lowlevel.library # takes it for one; the red button is button 0 and the blue (a two-button # stick's second) button 1. lowlevel.library cannot tell an idle stick from # an empty port, so it reports a stick only once it has moved: both must # read unplugged until a tap on each, connected after. Kickstart 3.1 has no lowlevel.library in ROM, so # the boot drive carries Workbench's (component amigaLowlevel). FS-UAE's # ports are both in joystick mode, every line of each on a host key of its # own (keyboard_key_*), and xdotool holds the keys; FS-UAE's screenshots # (its screenshot action, on F11) go through scripts/lib/joyScreen.py # (scripts/lib/joyGate.sh). Escape must then end JOY. # # Usage: scripts/verify-amiga-joy.sh # Env: JOY_SHOTS also keep the screenshots in this folder # Requires: FS-UAE, the Kickstart, lowlevel.library (install emulator:amiga), # Xvfb, xdotool, python3 + PIL; build/amiga/bin/Joy (make amiga). set -uo pipefail repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) source "$repo/scripts/lib/joeyEnv.sh" source "$repo/scripts/lib/joyGate.sh" joeyNeed FSUAE fsuae source "$repo/scripts/lib/amigaGate.sh" amigaMachine joeyNeed AMIGA_LOWLEVEL_LIBRARY amigaLowlevel JOY_LABEL=verify-amiga-joy program="$repo/build/amiga/bin/Joy" wall=300 # hard cap on FS-UAE, whatever happens to this script readySeconds=90 # AmigaDOS booting and JOY drawing focusSeconds=30 # the full-screen window taking the keyboard shotSeconds=5 # the screenshot key until FS-UAE has written the file exitSeconds=10 # Escape until JOY's screen is gone shotKey=F11 [ -f "$program" ] || { echo "$JOY_LABEL: build Joy first (make amiga)" >&2; exit 2; } JOY_WORK=$(mktemp -d -t joeylib-amiga-joy.XXXXXX) guard="" fsuae="" xvfb="" display="" cleanup() { joeyStop "$fsuae" "$guard" "$xvfb" 2> /dev/null rm -rf "$JOY_WORK" } trap cleanup EXIT # Control, its host key (xdotool's name, and FS-UAE's in lower case) and # the FS-UAE action it drives. Stick A is Amiga port 1, stick B port 0. keys=( "A-left j action_joy_1_left" "A-right l action_joy_1_right" "A-up i action_joy_1_up" "A-down k action_joy_1_down" "A-button0 a action_joy_1_fire_button" "A-button1 s action_joy_1_2nd_button" "B-left f action_joy_0_left" "B-right h action_joy_0_right" "B-up t action_joy_0_up" "B-down g action_joy_0_down" "B-button0 q action_joy_0_fire_button" "B-button1 w action_joy_0_2nd_button" "shot $shotKey action_screenshot" ) # 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 } # 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 "$@" } # FS-UAE writes more than one picture of each screenshot; the one cropped # to the Amiga's display is the stage's. It 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 '*crop*.png' | wc -l) hold keydown shot sleep 0.3 hold keyup shot deadline=$((SECONDS + shotSeconds)) while [ "$SECONDS" -lt "$deadline" ]; do sleep 0.2 [ "$(find "$JOY_WORK/shots" -name '*crop*.png' | wc -l)" -gt "$before" ] || continue found=$(ls -t "$JOY_WORK"/shots/*crop*.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" > /dev/null 2>&1 } mkdir -p "$JOY_WORK/hd/s" "$JOY_WORK/hd/Libs" "$JOY_WORK/shots" cp "$program" "$JOY_WORK/hd/" cp "$AMIGA_LOWLEVEL_LIBRARY" "$JOY_WORK/hd/Libs/lowlevel.library" printf ':%s\n' "$(basename "$program")" > "$JOY_WORK/hd/s/startup-sequence" mapping=() for entry in "${keys[@]}"; do read -r _ host action <<< "$entry" mapping+=("--keyboard_key_${host,,}=$action") done display=":$((RANDOM % 400 + 5500))" Xvfb "$display" -screen 0 1024x768x24 -nolisten tcp > /dev/null 2>&1 & xvfb=$! sleep 1 # --fullscreen=1: under a WM-less Xvfb SDL takes the keyboard only once its # window has the focus, which a full-screen window takes by itself. DISPLAY="$display" ALSOFT_DRIVERS=null timeout -s KILL "$wall" "$FSUAE" \ "${AMIGA_MACHINE_ARGS[@]}" --fast_memory=2048 \ --hard_drive_0="$JOY_WORK/hd" --hard_drive_0_label=JOEYLIB \ --fullscreen=1 --initial_input_grab=1 --floppy_drive_speed=800 \ --base_dir="$JOY_WORK/fs-uae" --screenshots_output_dir="$JOY_WORK/shots" \ --joystick_port_0=none --joystick_port_0_mode=joystick \ --joystick_port_1=none --joystick_port_1_mode=joystick \ "${mapping[@]}" > "$JOY_WORK/fs-uae.log" 2>&1 & guard=$! fsuae=$(joeyChild "$guard") # XTEST keys land in whatever window has the focus, which the full-screen # window latches on its own schedule. win="" for ((waited = 0; waited < focusSeconds; waited++)); do [ -n "$win" ] || win=$(DISPLAY="$display" xdotool search --name -- "FS-UAE" 2> /dev/null | head -1) if [ -n "$win" ] && [ "$(DISPLAY="$display" xdotool getwindowfocus 2> /dev/null)" = "$win" ]; then break fi [ -n "$win" ] && DISPLAY="$display" xdotool windowfocus --sync "$win" 2> /dev/null sleep 1 done ready=0 for ((waited = 0; waited < readySeconds; waited += 2)); do if joyStage; then ready=1 break fi joeyAlive "$guard" || break sleep 2 done if [ "$ready" != 1 ]; then echo "$JOY_LABEL: FAIL - FS-UAE with JOY did not come up" >&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 joyCheck "before either stick moves" "A=$JOY_ABSENT B=$JOY_ABSENT" joyPress A-left B-left sleep "$JOY_PRESS_SECONDS" joyRelease A-left B-left sleep "$JOY_PRESS_SECONDS" joyCheck "rest" "A=$JOY_REST B=$JOY_REST" joyRunSteps "${JOY_STEPS_TWO_STICKS[@]}" DISPLAY="$display" xdotool keydown Escape sleep 0.5 DISPLAY="$display" xdotool keyup 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" "both ports unplugged until moved, then every direction, both buttons and every release; Escape ends JOY"