joeylib2/scripts/verify-psp-input.sh
2026-10-13 14:01:46 -05:00

174 lines
6.9 KiB
Bash
Executable file

#!/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-psp-input.sh - Input gate for the PSP: the PSP's own controls reach
# JoeyLib, pressed through PPSSPPHeadless's input script and read back from
# what the examples draw.
#
# The PSP has no keyboard and one set of controls: SDL3's PSP joystick, which
# the SDL3 backend opens as gamepad 0 (JOYSTICK_0: the d-pad or the analog
# nub, cross as button 0, circle as button 1), with Start as Return and
# Select as Escape. Two runs under PPSSPPHeadless (scripts/lib/pspGate.sh),
# each from its folder on a private memory stick with every memory access
# checked as a PSP checks it, each driven by an --input script on PPSSPP's
# emulated clock (installer/patches/ppsspp):
#
# JOY (examples/joy) each d-pad direction, two diagonals, each button
# and both together, the nub left and up, with a release after
# every press so a latched line shows up; after each, a snapshot of
# the PSP's screen, whose stage scripts/lib/joyScreen.py reads both
# sticks off (stick B must stay unplugged). Then Select: JOY must
# end by itself, as it does on Escape.
# KEYS (examples/keys) Start: the Return key's cell lights, and goes out
# again on release, and Start has typed one Return, read off KEYS's
# row of the characters it received (scripts/lib/keysRow.py). Then
# Select: KEYS must end by itself.
#
# Usage: scripts/verify-psp-input.sh
# Requires: PPSSPPHeadless, python3 with PIL; build/psp/bin/JOY and
# build/psp/bin/KEYS (make psp).
set -uo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
source "$repo/scripts/lib/joeyEnv.sh"
source "$repo/scripts/lib/pspGate.sh"
pspNeed
label=verify-psp-input
joyDir=$repo/build/psp/bin/JOY
keysDir=$repo/build/psp/bin/KEYS
for dir in "$joyDir" "$keysDir"; do
[ -f "$dir/EBOOT.PBP" ] || { echo "$label: build $(basename "$dir") first (make psp)" >&2; exit 2; }
done
python3 -c "import PIL" 2> /dev/null || { echo "$label: python3 has no PIL" >&2; exit 2; }
# Real seconds a run may take: the script is emulated time, which passes
# much faster than real time while the program waits.
runSeconds=60
# Emulated milliseconds: the first step (the program is up by then), and
# from each press to its snapshot and on to the next press.
startMs=1500
holdMs=400
stepMs=600
# KEYS's Return cell (examples/keys: column 2, row 4 of 28x26 cells on a
# 32x30 pitch from (2, 2)), at its centre, on the PSP's screen, where the
# backend centres the 320x200 stage at 1:1 in 480x272.
screenSize=480x272
stageX=80
stageY=36
returnX=$((stageX + 2 + 2 * 32 + 14))
returnY=$((stageY + 2 + 4 * 30 + 13))
returnLit="0 255 0"
returnUnlit="51 51 51"
# KEYS's row of every character it received (stage row 198), which must
# hold the one Return that Start typed.
typedRow=$((stageY + 198))
typedWant=$'\r'
rest=10000
unplugged=00000
# name, input script action, stick A's state (joyScreen.py's characters:
# connected, X, Y, button 0, button 1).
joySteps=(
"rest|buttons none|$rest"
"left|buttons left|1-000" "release|buttons none|$rest"
"right|buttons right|1+000" "release|buttons none|$rest"
"up|buttons up|10-00" "release|buttons none|$rest"
"down|buttons down|10+00" "release|buttons none|$rest"
"up-left|buttons up left|1--00" "release|buttons none|$rest"
"down-right|buttons down right|1++00" "release|buttons none|$rest"
"cross|buttons cross|10010" "release|buttons none|$rest"
"circle|buttons circle|10001" "release|buttons none|$rest"
"cross+circle|buttons cross circle|10011" "release|buttons none|$rest"
"nub left|analog -1 0|1-000" "nub centred|analog 0 0|$rest"
"nub up|analog 0 1|10-00" "nub centred|analog 0 0|$rest"
)
work=$(mktemp -d -t joeylib-psp-input.XXXXXX)
fails=""
cleanup() {
pspStop
rm -rf "$work"
}
trap cleanup EXIT
# fail WHAT -- note a failed check; the gate goes on to report the rest.
fail() {
echo "$label: FAIL - $1"
fails="${fails:+$fails; }$1"
}
# runScript DIR NAME -- run the program folder DIR from $work/NAME with
# $work/NAME/input.txt; fails unless it ended by itself, cleanly.
runScript() {
local run="$work/$2"
local folder
folder=$(pspStage "$run" "$1")
pspRun "$run" "$folder" "$runSeconds" --input="$run/input.txt"
pspWait
pspStrictCheck "$run" "$1" || fail "$2: a bad memory access"
if grep -q '^TIMEOUT' "$run/ppsspp.log"; then
fail "$2: did not end on Select (Escape)"
fi
}
# JOY: each step's action, then its snapshot holdMs later; Select last.
mkdir -p "$work/joy/shots"
at=$startMs
i=0
for step in "${joySteps[@]}"; do
IFS='|' read -r name action want <<< "$step"
printf '%d %s\n%d snapshot %s/joy/shots/%02d.png\n' "$at" "$action" "$((at + holdMs))" "$work" "$i" >> "$work/joy/input.txt"
at=$((at + stepMs))
i=$((i + 1))
done
printf '%d buttons select\n' "$at" >> "$work/joy/input.txt"
runScript "$joyDir" joy
i=0
for step in "${joySteps[@]}"; do
IFS='|' read -r name action want <<< "$step"
png=$(printf '%s/joy/shots/%02d.png' "$work" "$i")
i=$((i + 1))
if [ ! -f "$png" ]; then
fail "JOY $name: no snapshot"
continue
fi
got=$(python3 "$repo/scripts/lib/joyScreen.py" "$png" --scale 1)
echo "$label: JOY $name: $got"
[ "${got% scale=*}" = "A=$want B=$unplugged" ] || fail "JOY $name: read ${got% scale=*}, want A=$want B=$unplugged"
done
# KEYS: Start held, then let go, then Select.
mkdir -p "$work/keys/shots"
{
printf '%d buttons start\n' "$startMs"
printf '%d snapshot %s/keys/shots/start.png\n' "$((startMs + holdMs))" "$work"
printf '%d buttons none\n' "$((startMs + stepMs))"
printf '%d snapshot %s/keys/shots/release.png\n' "$((startMs + stepMs + holdMs))" "$work"
printf '%d buttons select\n' "$((startMs + 2 * stepMs))"
} > "$work/keys/input.txt"
runScript "$keysDir" keys
for shot in "start|$returnLit" "release|$returnUnlit"; do
IFS='|' read -r name want <<< "$shot"
png=$work/keys/shots/$name.png
if [ ! -f "$png" ]; then
fail "KEYS $name: no snapshot"
continue
fi
got=$(python3 -c "from PIL import Image; import sys; print(*Image.open(sys.argv[1]).convert('RGB').getpixel((int(sys.argv[2]), int(sys.argv[3]))))" "$png" "$returnX" "$returnY")
echo "$label: KEYS $name: Return cell $got"
[ "$got" = "$want" ] || fail "KEYS $name: the Return cell is $got, want $want"
typed=$(python3 "$repo/scripts/lib/keysRow.py" "$png" "$typedWant" --size "$screenSize" --at "$stageX,$typedRow" --scale 1) || fail "KEYS $name: Start did not type one Return"
echo "$label: KEYS $name: ${typed//$'\n'/; }"
done
if [ -n "$fails" ]; then
echo "$label: FAIL - $fails" >&2
exit 1
fi
echo "$label: PASS (JOY: ${#joySteps[@]} d-pad, button and nub states read back from the screen, stick B unplugged; Start held KEYS's Return and typed one; Select ended both)"