#!/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-dos-mouse.sh - Headless mouse acceptance gate for the DOS port. # The port has the mouse driver call it on every move and button change # (INT 33h function 0Ch, a DPMI real-mode callback, src/dos/input.c); a # callback that never runs leaves the pointer where it started. Runs # KEYS.EXE in DOSBox Staging under Xvfb with the mouse captured from the # start, moves it with xdotool (real X pointer motion), and decodes KEYS's # mouse verification scanline (row 199: 5A5A, the button flags, X, Y as # nibble pixels) from a raw screenshot after each step. Covered: the start # at the stage's middle, a move each way on both axes, clamping at all # four edges, an odd column, and press and release of the left and right # buttons. # DOSBox scales host motion into mickeys its own way, so a move is checked # by its direction and only the clamps by position, and a one-pixel nudge # by landing on an odd column: in mode 13h the driver steps X by 2 in a # 640-wide virtual screen, which the port halves, so every column counts. # # Usage: scripts/verify-dos-mouse.sh # Requires: DOSBox Staging (install.sh install emulator:dos), Xvfb, xdotool, python3 + PIL; # build/dos/bin/KEYS.EXE (make dos). set -uo pipefail repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) source "$repo/scripts/lib/joeyEnv.sh" source "$repo/scripts/lib/dosboxKeys.sh" joeyNeed DOSBOX dosbox bin_dir=$repo/build/dos/bin conf=$repo/scripts/dosbox-386sx16.conf label=verify-dos-mouse [ -f "$bin_dir/KEYS.EXE" ] || { echo "$label: build KEYS.EXE first (make dos)" >&2; exit 2; } work=$(mktemp -d -t joeylib-dos-mouse.XXXXXX) display_num=$((RANDOM % 500 + 300)) cleanup() { joeyStop "${dosbox_pid:-}" [ -n "${xvfb_pid:-}" ] && kill "$xvfb_pid" 2>/dev/null rm -rf "$work" } trap cleanup EXIT mkdir -p "$work/shots" dosboxKeysMapper "$work/mapper.txt" Xvfb ":$display_num" -screen 0 1024x768x24 >/dev/null 2>&1 & xvfb_pid=$! sleep 1 XDG_CONFIG_HOME="$work/xdg" DISPLAY=":$display_num" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \ --conf "$conf" \ --set "capture_dir=$work/shots" \ --set "mapperfile=$work/mapper.txt" \ --set "mouse_capture=onstart" \ -c "MOUNT C $bin_dir" -c "C:" -c "KEYS.EXE" \ >"$work/dosbox.log" 2>&1 & dosbox_pid=$! # Let DOS boot and KEYS reach its poll loop on the simulated 386SX-16. sleep 12 win=$(joeyFindWindow ":$display_num" "$dosbox_pid") if [ -z "$win" ]; then echo "$label: FAIL - DOSBox window not found" >&2 exit 1 fi DISPLAY=":$display_num" xdotool windowactivate --sync "$win" 2>/dev/null || true # The mouse row of a fresh screenshot as "flags x y" (hex flags). readRow() { local png="$work/$1.png" dosboxShot ":$display_num" "$win" "$work/shots" "$png" || return 1 python3 - "$png" <<'PY' import sys from PIL import Image img = Image.open(sys.argv[1]).convert("RGB") w, h = img.size sx, sy = w // 320, h // 200 if sx < 1 or sy < 1 or w % 320 or h % 200: sys.exit(1) def dac8(c4): v6 = ((c4 << 2) | (c4 >> 2)) & 0x3F return (v6 << 2) | (v6 >> 4) # KEYS palette 0 (as verify-dos-input.sh decodes it). pal12 = [(i << 8) | (i << 4) | i for i in range(16)] pal12[0], pal12[1], pal12[2], pal12[3] = 0x000, 0x333, 0x0F0, 0xFFF pal12[15] = 0xF00 palRgb = [(dac8((v >> 8) & 0xF), dac8((v >> 4) & 0xF), dac8(v & 0xF)) for v in pal12] def pix(px): r, g, b = img.getpixel((px * sx, 199 * sy)) return min(range(16), key=lambda n: (r - palRgb[n][0]) ** 2 + (g - palRgb[n][1]) ** 2 + (b - palRgb[n][2]) ** 2) nib = [pix(i) for i in range(16)] words = [(nib[4 * k] << 12) | (nib[4 * k + 1] << 8) | (nib[4 * k + 2] << 4) | nib[4 * k + 3] for k in range(4)] if words[0] != 0x5A5A: sys.exit(1) print("%04X %d %d" % (words[1], words[2], words[3])) PY } fails=() step() { local tag="$1" local row shift if [ $# -gt 0 ]; then DISPLAY=":$display_num" xdotool "$@" sleep 1 fi if ! row=$(readRow "$tag"); then fails+=("$tag: no mouse row in the screenshot") row="---- -1 -1" fi read -r flags mx my <<< "$row" echo " $tag: flags=$flags x=$mx y=$my" } step start [ "$flags $mx $my" = "1000 160 100" ] || fails+=("start: expected flags 1000 at 160,100") step upLeft mousemove_relative -- -60 -40 [ "$flags" = "1000" ] && [ "$mx" -lt 160 ] && [ "$my" -lt 100 ] || fails+=("a move up and left did not arrive") lastX=$mx lastY=$my step downRight mousemove_relative -- 120 80 [ "$flags" = "1000" ] && [ "$mx" -gt "$lastX" ] && [ "$my" -gt "$lastY" ] || fails+=("a move down and right did not arrive") step topLeft mousemove_relative -- -2000 -2000 [ "$flags $mx $my" = "1000 0 0" ] || fails+=("not held at the top left (0,0)") step nudge mousemove_relative -- 1 0 [ "$flags" = "1000" ] && [ $((mx % 2)) -eq 1 ] || fails+=("a one-pixel nudge did not reach an odd column") step bottomRight mousemove_relative -- 2000 2000 [ "$flags $mx $my" = "1000 319 199" ] || fails+=("not held at the bottom right (319,199)") step leftDown mousedown 1 [ "$flags" = "1100" ] || fails+=("the left button down not seen") step leftUp mouseup 1 [ "$flags" = "1000" ] || fails+=("the left button up not seen") step rightDown mousedown 3 [ "$flags" = "1010" ] || fails+=("the right button down not seen") step rightUp mouseup 3 [ "$flags" = "1000" ] || fails+=("the right button up not seen") if [ ${#fails[@]} -eq 0 ]; then echo "$label: PASS (moves both ways, all four edges, every column, both buttons down and up)" exit 0 fi for f in "${fails[@]}"; do echo "$label: FAIL - $f" >&2 done exit 1