#!/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-iigs-joy.sh - Joystick gate for the IIgs: the stick on the game # port, every direction, both buttons and every release, driven through # MAME's joystick and read back from what examples/joy draws. # # The port (src/iigs/input.c) reads paddles 0 and 1 (the asm in # joeyDraw.s) and the buttons at $C061/$C062 as JL_JOYSTICK_0 (stick A); until # a program calls jlJoystickReset a paddle reads as a digital direction. # The IIgs exposes one stick: B must read unplugged throughout. MAME's # joystick (-gameio joy) is held from Lua at each step's paddle positions # (0 and 255 full, 128 at rest) and buttons, and a snapshot taken once JOY # has drawn it; the snapshots, lines doubled (MAME's IIgs frame has # double-width pixels), then go through scripts/lib/joyScreen.py # (scripts/lib/joyGate.sh) in order. Escape must then end JOY. # # The program boots from a GS/OS floppy whose launcher starts it, with its # data disk beside it (scripts/make-iigs-boot-disk.sh, make-iigs-disk.sh), # as joey-run boots it. # # Usage: scripts/verify-iigs-joy.sh # Env: JOY_SHOTS also keep the screenshots in this folder # Requires: MAME, the IIgs firmware and GS/OS, the IIgs toolchain (the disk # scripts), ImageMagick, python3 + PIL; build/iigs/bin/JOY # (make iigs-disk). set -uo pipefail repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) source "$repo/scripts/lib/joeyEnv.sh" source "$repo/scripts/lib/joyGate.sh" joeyNeed MAME mame joeyNeed IIGS_ROMPATH iigsFirmware joeyNeed GSOS_SYSTEM_DISK iigsFirmware joeyNeed CADIUS cadius JOY_LABEL=verify-iigs-joy JOY_REST_B="$JOY_ABSENT" # The checks read snapshots MAME has already taken: no waiting, and none # taken again. JOY_PRESS_SECONDS=0 JOY_SHOT_TRIES=1 program="$repo/build/iigs/bin/JOY" wall=300 # hard cap on MAME, whatever happens to this script readySeconds=80 # emulated: GS/OS booting and JOY drawing settleSeconds=0.5 # emulated: a change until JOY has drawn it exitSeconds=3 # emulated: Escape until JOY's screen is gone paddleLow=0 paddleRest=128 paddleHigh=255 [ -f "$program" ] || { echo "$JOY_LABEL: build JOY first (make iigs-disk)" >&2; exit 2; } JOY_WORK=$(mktemp -d -t joeylib-iigs-joy.XXXXXX) mame="" cleanup() { joeyStop "$mame" 2> /dev/null rm -rf "$JOY_WORK" } trap cleanup EXIT # The steps checked: stick A's, B left alone. steps=("${JOY_STEPS_TWO_STICKS[@]:0:9}") # state CONTROL... -- the Lua table of paddle X, paddle Y, button 0 and # button 1 with the controls held. state() { local x=$paddleRest local y=$paddleRest local b0=0 local b1=0 local control for control in "$@"; do case "$control" in A-left) x=$paddleLow ;; A-right) x=$paddleHigh ;; A-up) y=$paddleLow ;; A-down) y=$paddleHigh ;; A-button0) b0=1 ;; A-button1) b1=1 ;; *) joyFail "no control $control on the IIgs" ;; esac done printf '{%s, %s, %s, %s}' "$x" "$y" "$b0" "$b1" } # The snapshots, in the order the checks take them: rest, then for each # step held and released. shotIndex=0 joyPress() { : } joyRelease() { : } joyShot() { local shot shot=$(ls "$JOY_WORK"/snap/*.png 2> /dev/null | sort | sed -n "$((shotIndex + 1))p") shotIndex=$((shotIndex + 1)) [ -n "$shot" ] || return 1 convert "$shot" -sample 100%x200% "$1" } states="$(state)" for step in "${steps[@]}"; do IFS='|' read -r _ held _ _ <<< "$step" read -r -a controls <<< "$held" states="$states, $(state "${controls[@]}"), $(state)" done mkdir -p "$JOY_WORK/cfg" "$JOY_WORK/nvram" "$JOY_WORK/snap" BINDIR="$(dirname "$program")" JOEY_DISK_EXAMPLES="$(basename "$program")" \ "$repo/scripts/make-iigs-disk.sh" "$JOY_WORK/data.2mg" > "$JOY_WORK/disk.log" 2>&1 || { echo "$JOY_LABEL: FAIL - could not build the data disk" >&2; tail -5 "$JOY_WORK/disk.log" >&2; exit 1; } "$repo/scripts/make-iigs-boot-disk.sh" "$JOY_WORK/boot.po" "$(basename "$program")" > "$JOY_WORK/boot.log" 2>&1 || { echo "$JOY_LABEL: FAIL - could not build the boot disk" >&2; tail -5 "$JOY_WORK/boot.log" >&2; exit 1; } cat > "$JOY_WORK/joy.lua" << LUA local states = { $states } local readyAt = $readySeconds local settle = $settleSeconds local exitAfter = $exitSeconds -- A field of an ioport whose name contains pattern, or nil. local function field(tag, pattern) local p = manager.machine.ioport.ports[tag] if not p then return nil end for name, f in pairs(p.fields) do if name:find(pattern, 1, true) then return f end end return nil end -- The field of an ioport that drives mask, or nil. local function maskField(tag, mask) local p = manager.machine.ioport.ports[tag] if not p then return nil end for _, f in pairs(p.fields) do if f.mask == mask then return f end end return nil end -- A field named exactly name on any port whose tag starts with prefix. local function key(prefix, name) for tag, p in pairs(manager.machine.ioport.ports) do if tag:sub(1, #prefix) == prefix and p.fields[name] then return p.fields[name] end end return nil end local fx = field(":gameio:joy:joystick_1_x", "") local fy = field(":gameio:joy:joystick_1_y", "") -- The game port's switches SW0 and SW1: bits 4 and 5 of MAME's button -- port (its player numbers do not follow the switches). local fb0 = maskField(":gameio:joy:joystick_buttons", 0x10) local fb1 = maskField(":gameio:joy:joystick_buttons", 0x20) local esc = key(":macadb:", "Esc") if not (fx and fy and fb0 and fb1 and esc) then for tag, p in pairs(manager.machine.ioport.ports) do for name, f in pairs(p.fields) do if tag:find(":gameio:", 1, true) or name:lower():find("esc", 1, true) then print(string.format("JOYGATE field %s %s mask %x", tag, name, f.mask)) end end end print("JOYGATE: MAME's joystick or Escape key not found") manager.machine:exit() return end local i = 0 local nextAt = readyAt local phase = "apply" local escAt = nil emu.register_frame_done(function() local t = manager.machine.time:as_double() if t < nextAt then return end if phase == "apply" then i = i + 1 if i > #states then esc:set_value(1) escAt = t phase = "escape" nextAt = t + 0.5 return end local s = states[i] fx:set_value(s[1]); fy:set_value(s[2]); fb0:set_value(s[3]); fb1:set_value(s[4]) phase = "shoot" nextAt = t + settle elseif phase == "shoot" then manager.machine.video:snapshot() phase = "apply" nextAt = t elseif phase == "escape" then esc:set_value(0) phase = "after" nextAt = escAt + exitAfter elseif phase == "after" then manager.machine.video:snapshot() manager.machine:exit() end end) LUA (cd "$JOY_WORK" && exec env SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy timeout -s KILL "$wall" "$MAME" apple2gs \ -rompath "$IIGS_ROMPATH" -flop3 "$JOY_WORK/boot.po" -flop4 "$JOY_WORK/data.2mg" -gameio joy \ -skip_gameinfo -cfg_directory "$JOY_WORK/cfg" -nvram_directory "$JOY_WORK/nvram" \ -snapshot_directory "$JOY_WORK/snap" -snapname "shot%i" \ -video none -sound none -nothrottle -autoboot_script "$JOY_WORK/joy.lua") > "$JOY_WORK/mame.log" 2>&1 & mame=$! wait "$mame" mame="" if grep -q '^JOYGATE' "$JOY_WORK/mame.log"; then echo "$JOY_LABEL: FAIL - $(grep '^JOYGATE:' "$JOY_WORK/mame.log")" >&2 grep '^JOYGATE field' "$JOY_WORK/mame.log" >&2 exit 1 fi joyCheck "rest" "A=$JOY_REST B=$JOY_REST_B" joyRunSteps "${steps[@]}" # The last snapshot, after Escape: JOY gone. if joyShot "$JOY_WORK/after.png" && python3 "$repo/scripts/lib/joyScreen.py" "$JOY_WORK/after.png" > /dev/null 2>&1; then joyFail "Escape did not end JOY" fi joyResult "$JOY_LABEL" "the game port's stick, every direction, both buttons and every release, B unplugged; Escape ends JOY"