496 lines
17 KiB
Bash
Executable file
496 lines
17 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
#
|
|
# joey-run: run a built JoeyLib program in the right emulator.
|
|
#
|
|
# Usage:
|
|
# ./joey-run [options] PROGRAM [-- ARGS...]
|
|
#
|
|
# PROGRAM is any of:
|
|
# a program file build/dos/bin/DRAW.EXE, build/atarist/bin/DRAW.PRG,
|
|
# build/amiga/bin/Draw, build/x68000/bin/DRAW.X,
|
|
# build/iigs/bin/DRAW
|
|
# an app folder build/amiga/bin/Taxi (the program plus its DATA)
|
|
# an IIgs disk image build/iigs/bin/STAXI.2mg (make iigs-game-disk), or
|
|
# a data disk such as joey.2mg (boots GS/OS with it)
|
|
# an example name draw, staxi, uber, ... together with --port
|
|
# The machine is recognised from the file itself.
|
|
#
|
|
# Options:
|
|
# --port PORT iigs, amiga, atarist, dos or x68000
|
|
# --data DIR copy DIR's files into the program's DATA folder for
|
|
# this run (for example an AGI game's files)
|
|
# --headless no window and no sound, for scripts and CI; stops
|
|
# after 60 seconds unless --seconds says otherwise
|
|
# --seconds N stop after N seconds
|
|
# --snapshot FILE save a PNG of the screen when the run stops
|
|
# (needs --seconds or --headless)
|
|
# --machine rom01 IIgs: the ROM 01 machine instead of ROM 03
|
|
# --ram SIZE IIgs: memory size for MAME (default 4M; AGI wants 8M)
|
|
# --debug IIgs and X68000: start MAME's debugger
|
|
# -h, --help this text
|
|
#
|
|
# Arguments after -- are passed to the program (DOS only: the other
|
|
# machines' autostart cannot pass arguments).
|
|
#
|
|
# The emulators and firmware come from ./install.sh
|
|
# (install emulator:PORT). Interactive runs use the program's own folder
|
|
# or disk image, so saves and logs persist; --headless and --data runs
|
|
# work on a private copy. Nothing is written to your home directory.
|
|
|
|
set -euo pipefail
|
|
|
|
repo="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
# shellcheck source=scripts/lib/joeyEnv.sh
|
|
source "$repo/scripts/lib/joeyEnv.sh"
|
|
|
|
port=""
|
|
dataDir=""
|
|
headless=0
|
|
seconds=""
|
|
snapshot=""
|
|
machine="apple2gs"
|
|
ramSize="4M"
|
|
debug=0
|
|
program=""
|
|
progArgs=()
|
|
emuPid=""
|
|
xvfbPid=""
|
|
work=""
|
|
|
|
|
|
usage() {
|
|
sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//'
|
|
}
|
|
|
|
|
|
fail() {
|
|
echo "joey-run: $*" >&2
|
|
exit 1
|
|
}
|
|
|
|
|
|
cleanup() {
|
|
joeyStop "$emuPid"
|
|
if [ -n "$xvfbPid" ]; then
|
|
kill "$xvfbPid" 2>/dev/null || true
|
|
fi
|
|
if [ -n "$work" ]; then
|
|
rm -rf "$work"
|
|
fi
|
|
}
|
|
|
|
|
|
# ---- finding the program ----------------------------------------------------
|
|
|
|
# magicPort FILE -- print the machine FILE was built for, or nothing.
|
|
magicPort() {
|
|
local file="$1"
|
|
local magic
|
|
case "$file" in
|
|
*.2mg|*.2MG|*.po|*.PO|*.hdv|*.HDV)
|
|
printf 'iigs\n'
|
|
return 0
|
|
;;
|
|
esac
|
|
magic="$(head -c 16 "$file" | od -An -tx1 | tr -d ' \n')"
|
|
case "$magic" in
|
|
000003f3*) printf 'amiga\n' ;;
|
|
601a*) printf 'atarist\n' ;;
|
|
4d5a*) printf 'dos\n' ;;
|
|
4855*) printf 'x68000\n' ;;
|
|
*)
|
|
# Apple IIgs OMF: segment header byte 14 (NUMLEN) is 4 and
|
|
# byte 15 (VERSION) is 2.
|
|
if [ "${magic:28:4}" = "0402" ]; then
|
|
printf 'iigs\n'
|
|
fi
|
|
;;
|
|
esac
|
|
}
|
|
|
|
|
|
# folderProgram DIR -- the one program file inside an app folder.
|
|
folderProgram() {
|
|
local file
|
|
local found=""
|
|
while IFS= read -r -d '' file; do
|
|
if [ -n "$(magicPort "$file")" ]; then
|
|
[ -z "$found" ] || fail "$1 holds more than one program; name the file"
|
|
found="$file"
|
|
fi
|
|
done < <(find "$1" -maxdepth 1 -type f -print0)
|
|
[ -n "$found" ] || fail "no program found in $1"
|
|
printf '%s\n' "$found"
|
|
}
|
|
|
|
|
|
# ---- shared run plumbing ------------------------------------------------------
|
|
|
|
# stageFolder DEST -- copy the program's folder (its files and DATA tree)
|
|
# plus --data into DEST, for runs that must not touch the build output.
|
|
stageFolder() {
|
|
local dest="$1"
|
|
local progDir
|
|
progDir="$(dirname "$program")"
|
|
mkdir -p "$dest"
|
|
find "$progDir" -maxdepth 1 -type f -exec cp -t "$dest" {} +
|
|
if [ -d "$progDir/DATA" ]; then
|
|
cp -r "$progDir/DATA" "$dest/"
|
|
fi
|
|
if [ -n "$dataDir" ]; then
|
|
mkdir -p "$dest/DATA"
|
|
cp -r "$dataDir/." "$dest/DATA/"
|
|
fi
|
|
}
|
|
|
|
|
|
# startDisplay -- a virtual X display for --headless SDL emulators.
|
|
startDisplay() {
|
|
local display
|
|
display=":$((300 + RANDOM % 500))"
|
|
Xvfb "$display" -screen 0 1024x768x24 -nolisten tcp >/dev/null 2>&1 &
|
|
xvfbPid=$!
|
|
sleep 1
|
|
export DISPLAY="$display"
|
|
export SDL_AUDIODRIVER=dummy
|
|
}
|
|
|
|
|
|
# waitOrStop -- wait for the emulator, or stop it after --seconds (taking
|
|
# the --snapshot first).
|
|
waitOrStop() {
|
|
local waited=0
|
|
local win
|
|
if [ -z "$seconds" ]; then
|
|
wait "$emuPid" || true
|
|
emuPid=""
|
|
return 0
|
|
fi
|
|
while [ "$waited" -lt "$seconds" ] && kill -0 "$emuPid" 2>/dev/null; do
|
|
sleep 1
|
|
waited=$((waited + 1))
|
|
done
|
|
if [ -n "$snapshot" ]; then
|
|
if [ "$headless" = "1" ]; then
|
|
import -window root "$snapshot"
|
|
elif win="$(joeyFindWindow "$DISPLAY" "$emuPid")"; then
|
|
import -window "$win" "$snapshot"
|
|
else
|
|
echo "joey-run: no emulator window to snapshot" >&2
|
|
fi
|
|
fi
|
|
}
|
|
|
|
|
|
# mameStopScript FILE -- MAME Lua that snapshots (if asked) and exits after
|
|
# --seconds of emulated time.
|
|
mameStopScript() {
|
|
cat > "$1" <<LUA
|
|
local done = false
|
|
emu.register_frame_done(function()
|
|
if not done and manager.machine.time:as_double() >= $seconds then
|
|
done = true
|
|
if $([ -n "$snapshot" ] && echo true || echo false) then
|
|
manager.machine.video:snapshot()
|
|
end
|
|
manager.machine:exit()
|
|
end
|
|
end)
|
|
LUA
|
|
}
|
|
|
|
|
|
# runMame DRIVER ARGS... -- start MAME with the per-run directories, the
|
|
# headless/debug/stop options, then collect the snapshot.
|
|
runMame() {
|
|
local driver="$1"
|
|
shift
|
|
local args=(-skip_gameinfo -cfg_directory "$work/cfg" -nvram_directory "$work/nvram"
|
|
-snapshot_directory "$work/snap" -snapname "shot")
|
|
if [ "$headless" = "1" ]; then
|
|
args+=(-video none -sound none -nothrottle)
|
|
export SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy QT_QPA_PLATFORM=offscreen
|
|
else
|
|
args+=(-window)
|
|
fi
|
|
if [ "$debug" = "1" ]; then
|
|
args+=(-debug)
|
|
fi
|
|
if [ -n "$seconds" ]; then
|
|
mameStopScript "$work/stop.lua"
|
|
args+=(-autoboot_script "$work/stop.lua")
|
|
fi
|
|
(cd "$work" && exec "$MAME" "$driver" "$@" "${args[@]}") &
|
|
emuPid=$!
|
|
wait "$emuPid" || true
|
|
emuPid=""
|
|
if [ -n "$snapshot" ]; then
|
|
if [ -f "$work/snap/shot.png" ]; then
|
|
cp "$work/snap/shot.png" "$snapshot"
|
|
else
|
|
echo "joey-run: MAME saved no snapshot" >&2
|
|
fi
|
|
fi
|
|
}
|
|
|
|
|
|
# ---- one function per machine -------------------------------------------------
|
|
|
|
runIigs() {
|
|
local name
|
|
local img
|
|
local bytes
|
|
local media=()
|
|
joeyNeed MAME mame
|
|
joeyNeed IIGS_ROMPATH iigsFirmware
|
|
[ -z "$dataDir" ] || fail "--data is not supported on the IIgs; build a game disk (make -f make/iigs.mk iigs-game-disk-agi for AGI)"
|
|
case "$program" in
|
|
*.2mg|*.2MG|*.po|*.PO|*.hdv|*.HDV)
|
|
joeyNeed CADIUS cadius
|
|
img="$program"
|
|
if [ "$headless" = "1" ]; then
|
|
img="$work/$(basename "$program")"
|
|
cp "$program" "$img"
|
|
fi
|
|
bytes="$(stat -c %s "$img")"
|
|
if "$CADIUS" CATALOG "$img" 2>/dev/null | grep -qE '^ ProDOS +SYS'; then
|
|
# A bootable game disk: it launches its program by itself.
|
|
if [ "$bytes" -le 900000 ]; then
|
|
media=(-flop3 "$img")
|
|
else
|
|
media=(-sl7 cffa2 -hard1 "$img")
|
|
fi
|
|
else
|
|
# A data disk: boot GS/OS beside it and pick in the Finder.
|
|
joeyNeed GSOS_SYSTEM_DISK iigsFirmware
|
|
cp "$GSOS_SYSTEM_DISK" "$work/boot.po"
|
|
if [ "$bytes" -le 900000 ]; then
|
|
media=(-flop3 "$work/boot.po" -flop4 "$img")
|
|
else
|
|
media=(-flop3 "$work/boot.po" -sl7 cffa2 -hard1 "$img")
|
|
fi
|
|
echo "joey-run: $(basename "$img") is not bootable; GS/OS boots beside it -- open it in the Finder."
|
|
fi
|
|
;;
|
|
*)
|
|
# A bare program: pack it onto a data disk and boot a GS/OS
|
|
# whose startup program launches it (the developer path; needs
|
|
# the IIgs toolchain).
|
|
joeyNeed LLVM816_ROOT toolchain:iigs
|
|
joeyNeed CADIUS cadius
|
|
joeyNeed IIGS_CLANG_BUILD toolchain:iigs
|
|
joeyNeed GSOS_SYSTEM_DISK iigsFirmware
|
|
name="$(basename "$program")"
|
|
BINDIR="$(dirname "$program")" JOEY_DISK_EXAMPLES="$name" \
|
|
"$repo/scripts/make-iigs-disk.sh" "$work/data.2mg" >/dev/null
|
|
"$repo/scripts/make-iigs-boot-disk.sh" "$work/boot.po" "$name" >/dev/null
|
|
media=(-flop3 "$work/boot.po" -flop4 "$work/data.2mg")
|
|
;;
|
|
esac
|
|
echo "joey-run: booting the IIgs ($machine); the program comes up in about a minute. Close MAME to stop."
|
|
runMame "$machine" -ramsize "$ramSize" -rompath "$IIGS_ROMPATH" "${media[@]}"
|
|
}
|
|
|
|
|
|
runAmiga() {
|
|
local name
|
|
local hd="$work/hd"
|
|
local args=()
|
|
joeyNeed FSUAE fsuae
|
|
name="$(basename "$program")"
|
|
# FS-UAE boots the folder as a hard drive and writes to it, so it is
|
|
# always staged. s/startup-sequence runs the program.
|
|
stageFolder "$hd"
|
|
mkdir -p "$hd/s"
|
|
printf ':%s\n' "$name" > "$hd/s/startup-sequence"
|
|
# Kickstart 3.1 has no serial.device in ROM; AmigaOS loads it from
|
|
# DEVS: (SYS:Devs), so a program that opens the serial port needs it on
|
|
# this boot drive. Harmless for everything else.
|
|
if [ -n "${AMIGA_SERIAL_DEVICE:-}" ] && [ -f "$AMIGA_SERIAL_DEVICE" ]; then
|
|
mkdir -p "$hd/Devs"
|
|
cp "$AMIGA_SERIAL_DEVICE" "$hd/Devs/serial.device"
|
|
else
|
|
echo "joey-run: no serial.device installed (./install.sh install amigaSerial); serial will not open on the Amiga" >&2
|
|
fi
|
|
args=(--amiga_model=A500 --fast_memory=2048
|
|
--hard_drive_0="$hd" --hard_drive_0_label=JOEYLIB
|
|
--automatic_input_grab=0 --middle_click_ungrab=1 --mouse_integration=1
|
|
--floppy_drive_speed=800 --base_dir="$work/fs-uae")
|
|
if [ -n "${KICKSTART_ROM:-}" ] && [ -f "$KICKSTART_ROM" ]; then
|
|
args+=(--kickstart_file="$KICKSTART_ROM")
|
|
else
|
|
echo "joey-run: no Kickstart installed (./install.sh install kickstart); booting FS-UAE's AROS, which may not run every program" >&2
|
|
fi
|
|
if [ "$headless" = "1" ]; then
|
|
startDisplay
|
|
# A real gamepad crashes FS-UAE when there is no screen to grab.
|
|
args+=(--joystick_port_0=none --joystick_port_1=none)
|
|
export ALSOFT_DRIVERS=null
|
|
fi
|
|
echo "joey-run: booting the Amiga; $name starts from the JOEYLIB hard drive. Close FS-UAE to stop."
|
|
"$FSUAE" "${args[@]}" >"$work/fs-uae.log" 2>&1 &
|
|
emuPid=$!
|
|
waitOrStop
|
|
# Keep the program's log next to it, as a direct run would.
|
|
find "$hd" -maxdepth 1 -iname 'joeylog.txt' -exec cp {} "$(dirname "$program")/" \; 2>/dev/null || true
|
|
}
|
|
|
|
|
|
runAtarist() {
|
|
local dir
|
|
local args=()
|
|
joeyNeed HATARI hatari
|
|
joeyNeed EMUTOS_IMG emutos
|
|
dir="$(dirname "$program")"
|
|
if [ "$headless" = "1" ] || [ -n "$dataDir" ]; then
|
|
stageFolder "$work/hd"
|
|
dir="$work/hd"
|
|
fi
|
|
args=(--tos "$EMUTOS_IMG" --harddrive "$dir" --gemdos-drive C --auto "C:\\$(basename "$program")")
|
|
if [ "$headless" = "1" ]; then
|
|
startDisplay
|
|
args+=(--sound off)
|
|
fi
|
|
echo "joey-run: booting the Atari ST (EmuTOS); $(basename "$program") starts from drive C. Close Hatari to stop."
|
|
"$HATARI" "${args[@]}" >"$work/hatari.log" 2>&1 &
|
|
emuPid=$!
|
|
waitOrStop
|
|
}
|
|
|
|
|
|
runDos() {
|
|
local dir
|
|
local command
|
|
local args=()
|
|
joeyNeed DOSBOX dosbox
|
|
dir="$(dirname "$program")"
|
|
if [ "$headless" = "1" ] || [ -n "$dataDir" ]; then
|
|
stageFolder "$work/hd"
|
|
dir="$work/hd"
|
|
fi
|
|
command="$(basename "$program")"
|
|
if [ ${#progArgs[@]} -gt 0 ]; then
|
|
command="$command ${progArgs[*]}"
|
|
fi
|
|
args=("${JOEY_DOSBOX_ISOLATE[@]}" --conf "$repo/scripts/dosbox-386sx16.conf"
|
|
--set "mouse_capture=seamless" -c "MOUNT C \"$dir\"" -c "C:" -c "$command")
|
|
if [ "$headless" = "1" ]; then
|
|
startDisplay
|
|
else
|
|
# Leave the program's last screen up until a key is pressed.
|
|
args+=(-c "pause")
|
|
fi
|
|
args+=(--exit)
|
|
echo "joey-run: booting DOS (386SX-16); $command runs from drive C. Close DOSBox to stop."
|
|
XDG_CONFIG_HOME="$work/xdg" "$DOSBOX" "${args[@]}" >"$work/dosbox.log" 2>&1 &
|
|
emuPid=$!
|
|
waitOrStop
|
|
}
|
|
|
|
|
|
runX68000() {
|
|
local name
|
|
local stage="$work/files"
|
|
joeyNeed MAME mame
|
|
joeyNeed X68K_DIR x68kFirmware
|
|
joeyNeedFile "$X68K_DIR/roms/x68000/cgrom.dat" x68kCgrom
|
|
# shellcheck source=scripts/lib/x68kDisk.sh
|
|
source "$repo/scripts/lib/x68kDisk.sh"
|
|
name="$(basename "$program")"
|
|
# Human68k boots from a trimmed copy of the system disk whose
|
|
# AUTOEXEC.BAT runs the program from the data disk in drive B.
|
|
x68kBootDisk "$work/boot.xdf" 'B:' "B:\\$name" || fail "cannot build the boot disk"
|
|
x68kDataDisk "$work/data.xdf" || fail "cannot build the data disk"
|
|
x68kXdf add "$work/data.xdf" "$program" "$name" >/dev/null || fail "cannot copy $name onto the data disk"
|
|
rm -rf "$stage"
|
|
mkdir -p "$stage"
|
|
if [ -d "$(dirname "$program")/DATA" ]; then
|
|
cp -r "$(dirname "$program")/DATA" "$stage/"
|
|
fi
|
|
if [ -n "$dataDir" ]; then
|
|
mkdir -p "$stage/DATA"
|
|
cp -r "$dataDir/." "$stage/DATA/"
|
|
fi
|
|
if [ -d "$stage/DATA" ]; then
|
|
x68kAddTree "$work/data.xdf" "$stage/DATA" DATA || fail "the DATA files do not fit on the 1232 KB data disk"
|
|
fi
|
|
echo "joey-run: booting the X68000 (Human68k 3.02); $name runs from drive B. Close MAME to stop."
|
|
runMame x68000 -bios ipl10 -rompath "$X68K_DIR/roms" -flop1 "$work/boot.xdf" -flop2 "$work/data.xdf"
|
|
}
|
|
|
|
|
|
# ---- main ---------------------------------------------------------------------
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--port) port="${2:?--port needs iigs, amiga, atarist, dos or x68000}"; shift ;;
|
|
--data) dataDir="${2:?--data needs a folder}"; shift ;;
|
|
--headless) headless=1 ;;
|
|
--seconds) seconds="${2:?--seconds needs a number}"; shift ;;
|
|
--snapshot) snapshot="${2:?--snapshot needs a file name}"; shift ;;
|
|
--machine)
|
|
case "${2:-}" in
|
|
rom01) machine="apple2gsr1" ;;
|
|
rom03) machine="apple2gs" ;;
|
|
*) fail "--machine takes rom01 or rom03" ;;
|
|
esac
|
|
shift
|
|
;;
|
|
--ram) ramSize="${2:?--ram needs a size such as 4M}"; shift ;;
|
|
--debug) debug=1 ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
--) shift; progArgs=("$@"); break ;;
|
|
-*) fail "unknown option $1 (see: joey-run --help)" ;;
|
|
*)
|
|
[ -z "$program" ] || fail "one program at a time (got '$program' and '$1')"
|
|
program="$1"
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
[ -n "$program" ] || { usage; exit 2; }
|
|
[[ "$seconds" =~ ^[0-9]*$ ]] || fail "--seconds takes a whole number"
|
|
if [ "$headless" = "1" ] && [ -z "$seconds" ]; then
|
|
seconds=60
|
|
fi
|
|
if [ -n "$snapshot" ]; then
|
|
[ -n "$seconds" ] || fail "--snapshot needs --seconds (or --headless)"
|
|
snapshot="$(realpath -m "$snapshot")"
|
|
fi
|
|
if [ -n "$dataDir" ]; then
|
|
[ -d "$dataDir" ] || fail "--data: no such folder $dataDir"
|
|
dataDir="$(cd "$dataDir" && pwd)"
|
|
fi
|
|
|
|
# Resolve PROGRAM to a file and its machine.
|
|
if [ -d "$program" ]; then
|
|
program="$(folderProgram "$program")"
|
|
elif [ ! -e "$program" ]; then
|
|
[ -n "$port" ] || fail "$program is not a file; for an example name add --port"
|
|
program="$(joeyExampleProgram "$program" "$port")" || exit 1
|
|
[ -e "$program" ] || fail "$program is not built (make $port)"
|
|
fi
|
|
program="$(cd "$(dirname "$program")" && pwd)/$(basename "$program")"
|
|
detected="$(magicPort "$program")"
|
|
[ -n "$detected" ] || fail "cannot tell which machine $program is for"
|
|
if [ -n "$port" ] && [ "$port" != "$detected" ]; then
|
|
fail "$program is a $detected program, not $port"
|
|
fi
|
|
if [ ${#progArgs[@]} -gt 0 ] && [ "$detected" != "dos" ]; then
|
|
fail "program arguments only work on DOS"
|
|
fi
|
|
|
|
work="$(mktemp -d -t joey-run.XXXXXX)"
|
|
trap cleanup EXIT INT TERM
|
|
|
|
case "$detected" in
|
|
iigs) runIigs ;;
|
|
amiga) runAmiga ;;
|
|
atarist) runAtarist ;;
|
|
dos) runDos ;;
|
|
x68000) runX68000 ;;
|
|
esac
|
|
if [ -n "$snapshot" ] && [ -f "$snapshot" ]; then
|
|
echo "joey-run: snapshot saved to $snapshot"
|
|
fi
|