182 lines
7.4 KiB
Bash
Executable file
182 lines
7.4 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# oracle.sh - play the test probes (tests/probes.py) and the game sessions
|
|
# (tests/GAME/*.scr, for each game in games/GAME) on Sierra's own
|
|
# interpreter and on JoeyAGI, and compare the frames.
|
|
#
|
|
# Sierra's interpreter is the black box the probes were checked against:
|
|
# each probe game is written beside the interpreter files of the copy of
|
|
# King's Quest III in games/kq3 (AGI, AGIDATA.OVL, the *.OVL drivers,
|
|
# KQ3.COM -- never distributed), or of the game ORACLE_INTERP names. A
|
|
# session plays a copy of its own game with that game's interpreter,
|
|
# started by the launcher it shipped with (GAME.COM, SIERRA.COM, or the
|
|
# game's .EXE). Each runs in DOSBox Staging as a fast 386 (dosDrive.py)
|
|
# under a private Xvfb and fed the probe's script with xdotool; JoeyAGI
|
|
# plays the same script on the host (build/tests/agiHost, built by
|
|
# scripts/test-agi.sh). tests/tools/compareFrames.py then compares each
|
|
# shot, reading text with each interpreter's own font.
|
|
#
|
|
# A probe whose script records (shake) is compared by what
|
|
# tests/tools/shakeMeasure.py reads out of the two recordings.
|
|
#
|
|
# DOSBox runs in real time. dosDrive.py keeps every step of a script at
|
|
# its own time from the moment DOSBox switches to the game's graphics mode
|
|
# (the host harness's frame 0), so the two sides stay in step however long
|
|
# the screenshots take; a shot of something in motion can still fall in a
|
|
# different part of a cycle. The probes that matter record their results
|
|
# into variables and print them, which does not depend on timing.
|
|
#
|
|
# The follow probe's second follower takes random detours, so its x2/y2
|
|
# rows differ from run to run on Sierra's side; everything else matches.
|
|
#
|
|
# Usage: scripts/oracle.sh [PROBE | GAME-SESSION ...] every probe and
|
|
# session when none is named
|
|
# Env: ORACLE_INTERP=GAME runs the probes on games/GAME's interpreter
|
|
# (a v2 game; default kq3)
|
|
# Output: build/oracle/PROBE/{oracle,joey}/ and a diff image per shot
|
|
# Needs: games/kq3 (or games/ORACLE_INTERP) with Sierra's interpreter,
|
|
# Xvfb, xdotool, python3 + PIL, JoeyLib's DOSBox Staging
|
|
# (./install.sh install emulators)
|
|
set -uo pipefail
|
|
|
|
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
JOEYLIB=$(cd "${JOEYLIB:-$repo/../joeylib}" && pwd) || { echo "oracle: no JoeyLib checkout (set JOEYLIB)" >&2; exit 2; }
|
|
export JOEYLIB
|
|
games=$repo/games
|
|
sierra=$games/${ORACLE_INTERP:-kq3}
|
|
tools=$repo/tests/tools
|
|
build=$repo/build/oracle
|
|
agiHost=$repo/build/tests/agiHost
|
|
parallel=${ORACLE_JOBS:-4}
|
|
|
|
# The launcher Sierra shipped with game $2 (games/$2) in folder $1. A v3
|
|
# game runs as <prefix>.COM, the name its installer gives SIERRA.COM (the
|
|
# interpreter takes its file prefix, GR of GRDIR, from it), so in a copy
|
|
# of one that has only SIERRA.COM that file is made. Black Cauldron 2.10's
|
|
# interpreter is one SIERRA.EXE; a release with an installer also has
|
|
# SETUP.EXE and the like, so SIERRA.EXE comes before any other .EXE.
|
|
launcher() {
|
|
local dir=$1
|
|
local id=$2
|
|
local exe
|
|
local d
|
|
|
|
if [[ ! -f $dir/LOGDIR ]]; then
|
|
for d in "$dir"/*DIR; do
|
|
[[ -f $d ]] || continue
|
|
if [[ ! -f ${d%DIR}.COM && -f $dir/SIERRA.COM ]]; then
|
|
cp "$dir/SIERRA.COM" "${d%DIR}.COM"
|
|
fi
|
|
if [[ -f ${d%DIR}.COM ]]; then
|
|
basename "${d%DIR}.COM"
|
|
return 0
|
|
fi
|
|
done
|
|
fi
|
|
for exe in "$dir/${id^^}.COM" "$dir/SIERRA.COM" "$dir/SIERRA.EXE" "$dir"/*.[Ee][Xx][Ee]; do
|
|
if [[ -f $exe ]]; then
|
|
basename "$exe"
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
[[ -f $sierra/AGIDATA.OVL ]] && launcher "$sierra" "$(basename "$sierra")" >/dev/null || { echo "oracle: needs Sierra's interpreter in $sierra" >&2; exit 2; }
|
|
for tool in Xvfb xdotool python3; do
|
|
command -v "$tool" >/dev/null || { echo "oracle: needs $tool" >&2; exit 2; }
|
|
done
|
|
[[ -x $agiHost ]] || "$repo/scripts/test-agi.sh" font >/dev/null || true
|
|
[[ -x $agiHost ]] || { echo "oracle: build/tests/agiHost missing (scripts/test-agi.sh builds it)" >&2; exit 1; }
|
|
|
|
# Every session of a game that ships Sierra's interpreter (AGIDATA.OVL;
|
|
# Space Quest 0's is a Windows program and has no part here), but those
|
|
# that use the mouse, which Sierra's DOS interpreters have none of, and
|
|
# those marked "# JoeyAGI only" (where JoeyAGI skips copy protection).
|
|
sessions=$(for f in "$repo"/tests/*/*.scr; do
|
|
g=$(basename "$(dirname "$f")")
|
|
[[ -f $f && -f $games/$g/AGIDATA.OVL ]] || continue
|
|
grep -qE '^((mouse|click)( |$)|# JoeyAGI only)' "$f" && continue
|
|
echo "$g-$(basename "${f%.scr}")"
|
|
done)
|
|
probes=${*:-$(python3 "$repo/tests/probes.py" list) $sessions}
|
|
mkdir -p "$build"
|
|
|
|
# One probe on both interpreters.
|
|
one() {
|
|
local name=$1
|
|
local game=$build/$name/game
|
|
local run
|
|
local script
|
|
local exe
|
|
|
|
rm -rf "${build:?}/$name"
|
|
mkdir -p "$build/$name/oracle" "$build/$name/joey"
|
|
script=$repo/tests/${name%%-*}/${name#*-}.scr
|
|
if [[ $name == *-* && -f $script ]]; then
|
|
cp -r "$games/${name%%-*}" "$game"
|
|
exe=$(launcher "$game" "${name%%-*}")
|
|
else
|
|
python3 "$repo/tests/probes.py" build "$build/$name/.probe" "$name" || return 1
|
|
mv "$build/$name/.probe/$name" "$game"
|
|
rmdir "$build/$name/.probe"
|
|
exe=$(launcher "$sierra" "$(basename "$sierra")")
|
|
cp "$sierra"/*.OVL "$sierra"/HGC_FONT "$sierra/$exe" "$game/"
|
|
if [[ -f $sierra/AGI ]]; then
|
|
cp "$sierra/AGI" "$game/"
|
|
fi
|
|
script=$game/probe.scr
|
|
fi
|
|
timeout -s KILL 900 python3 "$tools/dosDrive.py" "$game" "$exe" "$build/$name/oracle" "$script" \
|
|
--set joysticktype=none > "$build/$name/oracle.log" 2>&1
|
|
run=$(mktemp -d "$build/run.XXXXXX")
|
|
ln -s "$game" "$run/DATA"
|
|
mkdir "$run/SAVES"
|
|
(cd "$run" && timeout -s KILL 900 "$agiHost" "$script" "$build/$name/joey") > "$build/$name/joey.log" 2>&1
|
|
rm -rf "$run"
|
|
}
|
|
export -f one launcher
|
|
export build repo games sierra tools agiHost
|
|
|
|
# The fonts first: every comparison reads text with them.
|
|
one font
|
|
printf '%s\n' $probes | grep -vx font | xargs -P "$parallel" -I{} bash -c 'one {}'
|
|
|
|
fontArgs=(--fonts "$build/font/oracle/font.png" "$build/font/joey/font.ppm")
|
|
total=0
|
|
differ=0
|
|
for name in $probes; do
|
|
echo "=== $name"
|
|
for shot in "$build/$name"/oracle/*.png; do
|
|
[[ -f $shot ]] || continue
|
|
base=$(basename "${shot%.png}")
|
|
joey=$build/$name/joey/$base.ppm
|
|
total=$((total + 1))
|
|
if [[ ! -f $joey ]]; then
|
|
echo " $base: JoeyAGI took no such shot"
|
|
differ=$((differ + 1))
|
|
continue
|
|
fi
|
|
if result=$(python3 "$tools/compareFrames.py" "$shot" "$joey" "$build/$name/diff-$base.png" "${fontArgs[@]}"); then
|
|
echo " $base: same"
|
|
else
|
|
echo " $base: DIFFERS $result"
|
|
differ=$((differ + 1))
|
|
fi
|
|
done
|
|
# Recordings (shake.screen): the shakes read out of both.
|
|
for video in "$build/$name"/oracle/*.avi; do
|
|
[[ -f $video ]] || continue
|
|
base=$(basename "${video%.avi}")
|
|
total=$((total + 1))
|
|
if result=$(python3 "$tools/shakeMeasure.py" "$video" "$build/$name/joey/$base.frames"); then
|
|
echo " $base (recording): same"
|
|
else
|
|
echo " $base (recording): DIFFERS"
|
|
differ=$((differ + 1))
|
|
fi
|
|
echo "$result" | sed 's/^/ /'
|
|
done
|
|
done
|
|
echo
|
|
echo "oracle: $((total - differ)) of $total shots match (diff images in $build/<probe>/)"
|
|
[[ $differ == 0 ]]
|