From 96c72ca4baa87c1fab8f7c0fe292aa8963bcde73 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Mon, 5 Oct 2026 18:50:09 -0500 Subject: [PATCH] Build standalone against JoeyLib. --- .gitignore | 15 ++++++ Makefile | 28 ++++++++++++ README.md | 79 ++++++++++++++++++++++++++++++++ games/README.md | 49 ++++++++++++++++++++ program.mk | 32 +++++++++++++ scripts/make-agi-iigs-disk.sh | 14 ++++-- scripts/test-agi.sh | 61 +++++++++++++------------ scripts/verify.sh | 86 +++++++++++++++++++++++++++++++++++ tests/testAgiPic.c | 2 +- tests/testAgiRes.c | 2 +- tests/testAgiView.c | 2 +- tests/testAgiVm.c | 2 +- 12 files changed, 335 insertions(+), 37 deletions(-) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 games/README.md create mode 100644 program.mk create mode 100755 scripts/verify.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1ca0f70 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Everything make writes: programs, disk images, the host tests. +build/ + +# The games the interpreter plays: Sierra's (or other authors') files, +# never committed. games/README.md says what goes there. +games/* +!games/README.md + +__pycache__/ + +# Editor / OS cruft +*.swp +*.swo +*~ +.DS_Store diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3c0cb3e --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ +# JoeyAGI, a Sierra AGI v2 interpreter built with JoeyLib: a JoeyLib +# checkout (JOEYLIB, beside this one by default) owns the toolchains, the +# library and every target's rules, and program.mk describes the +# interpreter to them. +# +# make every target whose toolchain is installed +# make one target: iigs amiga atarist dos x68000 dvx +# make iigs-game-disk-agi the bootable IIgs volume with a game on it +# (games/kq3; AGI_GAME=NAME for games/NAME) +# make check build, then run the gates (scripts/verify.sh) +# make clean remove build/ +# make help JoeyLib's project targets +# +# README.md covers setting up JoeyLib, the game files and running. + +JOEYLIB ?= ../joeylib +include $(JOEYLIB)/make/project.mk + +include $(JOEY_PROJECT)/program.mk + +# The author DVX's resource list names. +DVX_AUTHOR := Scott Duensing + +include $(JOEYLIB)/make/programs.mk + +.PHONY: check +check: all + JOEYLIB="$(abspath $(JOEYLIB))" $(JOEY_PROJECT)/scripts/verify.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..3fec65c --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# JoeyAGI + +An interpreter for Sierra's Adventure Game Interpreter (AGI) v2 games -- +King's Quest III and its contemporaries -- written from the public AGI +format descriptions on [JoeyLib](../joeylib), so it runs on every machine +JoeyLib targets: the Apple IIgs, Commodore Amiga, Atari ST, MS-DOS, Sharp +X68000 and DVX. It decodes the games' own resource files (LOGIC, PICTURE, +VIEW, SOUND, OBJECT, WORDS.TOK); it ships no game. + +## Setting up + +JoeyAGI builds with JoeyLib, which owns the cross compilers, the library +and every machine's build rules. Check JoeyLib out beside this repository +and install its toolchains (its README covers the installer): + +``` +games/ + joeylib/ a JoeyLib checkout, set up with ./install.sh + joeyagi/ this repository +``` + +```console +$ cd ../joeylib +$ ./install.sh install toolchains # the cross compilers +$ ./install.sh install emulators # to run it and for the gates +``` + +`JOEYLIB=PATH` on the make command line (or in the environment, for the +scripts) points at a checkout somewhere else. + +## Games + +The interpreter needs an AGI v2 game's files. Put each game in its own +folder under `games/` (`games/kq3/`, ...), which git ignores; +games/README.md lists the files and where legitimate copies come from. + +## Building + +```console +$ make # every machine whose toolchain is installed +$ make dos # one: iigs amiga atarist dos x68000 dvx +$ make iigs-game-disk-agi # the bootable IIgs volume, with games/kq3 on it +$ make clean +``` + +The programs land in `build//bin/`: `AGI` (IIgs), `Agi` (Amiga), +`AGI.PRG`, `AGI.EXE`, `AGI.X`, and on DVX `apps/joeylib/agi/`. +`make iigs-game-disk-agi` writes `build/iigs/bin/AGI.2mg`, a 2 MB bootable +volume holding the interpreter and the game (`AGI_GAME=NAME` puts +`games/NAME` on it instead). JoeyLib's library is built on demand in the +JoeyLib checkout's own `build/`. + +## Running + +The interpreter reads the game from its `DATA` folder; JoeyLib's `joey-run` +copies a game there for the run: + +```console +$ ../joeylib/joey-run --project . --port dos --data games/kq3 agi +$ ../joeylib/joey-run --ram 8M build/iigs/bin/AGI.2mg +``` + +The IIgs volume boots off a CFFA2 card in MAME and wants 8 MB of memory. + +## The gates + +```console +$ make check # build, then every gate +$ scripts/verify.sh host # or name the ones to run +``` + +| Gate | What it proves | +|------|----------------| +| `host` | The resource loader, the PICTURE and VIEW decoders, the LOGIC VM and the VM pipeline, built for the host against JoeyLib's headers and run on every game in `games/` (`scripts/test-agi.sh`) | +| `disk` | `AGI.2mg` with King's Quest III boots on a IIgs, launches the interpreter and draws | + +`scripts/verify.sh` exits 0 when every gate passed, 1 when one failed, and 3 +when none failed but one was skipped (no game in `games/`, or the IIgs +toolchain or emulator missing). diff --git a/games/README.md b/games/README.md new file mode 100644 index 0000000..a232eff --- /dev/null +++ b/games/README.md @@ -0,0 +1,49 @@ +# AGI game data + +Everything in this folder except this file is ignored by git. No game's +files are, or should ever be, committed to the repository. + +Place each game in its own subdirectory: + +``` +games/ + agidemo/ # Sierra's promotional AGIDEMO (freely redistributable) + kq3/ # Local copy extracted from your licensed King's Quest 3 + ... +``` + +`scripts/test-agi.sh` runs the host tests against every subdirectory that +holds a `LOGDIR`, and `make iigs-game-disk-agi` puts `kq3` (or +`AGI_GAME=NAME`) on the bootable IIgs volume. Everywhere else the +interpreter reads the game from its `DATA` folder; `joey-run --data +games/kq3` copies a game there for one run. + +## Required files per game + +For an AGI v2 game directory the loader needs: + +- `LOGDIR` - logic script index +- `PICDIR` - picture (room background) index +- `VIEWDIR` - view (sprite/animation) index +- `SNDDIR` - sound index +- `VOL.0` - resource volume 0 (always present) +- `VOL.1`..`VOL.N` - additional resource volumes (game-dependent) +- `OBJECT` - inventory table +- `WORDS.TOK` - parser dictionary + +File names are uppercase by AGI convention; the loader looks them up +literally so the directory should match. + +## Sourcing AGI data + +This repository never ships copyrighted Sierra game data. Two legitimate +sources for development and testing: + +1. Sierra's freely-distributed `AGIDEMO` promotional disk - + redistributable since the late 1980s. +2. Fan-made AGI titles authored with AGI Studio - many are released + under explicit redistribution licenses. + +To test against a commercial Sierra game you legally own, extract +the data files from your own copy and drop them in a new sibling +directory here. Do not commit them. diff --git a/program.mk b/program.mk new file mode 100644 index 0000000..5b28559 --- /dev/null +++ b/program.mk @@ -0,0 +1,32 @@ +# The AGI interpreter, described for JoeyLib's make/programs.mk (which lists +# what each variable means). Included by the Makefile beside it, on JoeyLib's +# make/project.mk. + +AGI_HOME := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))) + +PROGRAMS += AGI +AGI_SRCS := $(addprefix $(AGI_HOME)/,agi.c agiRes.c agiPic.c agiView.c agiVm.c agiObj.c agiText.c) +AGI_TITLE := AGI +AGI_HELP := A Sierra AGI interpreter. The files of the game it plays go in its DATA folder. + +# IIgs: large overflow segments. --segment-cap bounds the ENTRY bank (which +# also holds rodata + bss + heap); overflow banks carry text only, so capping +# them the same way wasted ~50K per bank. With this AGI links 5 bank-aligned +# segments instead of 19, which matters because it already needs -ramsize 4M. +# Verified by running KQ3 headless and diffing AGI's own joeylog.txt against +# the 19-segment build: identical apart from the embedded build timestamp. +# See JoeyLib's make/support/iigs/clang-build.sh for the measurements. +AGI_IIGS_OVERFLOW_CAP := 0xF000 + +ifeq ($(PLATFORM),iigs) +# AGI's bootable IIgs volume needs a game's resources (games/kq3, King's +# Quest III, by default; AGI_GAME=NAME for games/NAME), which +# make-agi-iigs-disk.sh lays out, so it cannot use the generic one-program +# data disk. It also wants a bigger Memory Manager pool: run it with +# -ramsize 8M. +AGI_GAME ?= kq3 +.PHONY: iigs-game-disk-agi +iigs-game-disk-agi: $(call programBin,AGI) + BINDIR="$(BINDIR)" JOEYLIB="$(REPO_DIR)" $(AGI_HOME)/scripts/make-agi-iigs-disk.sh $(BINDIR)/AGI-data.2mg $(AGI_GAME) + $(REPO_DIR)/scripts/make-iigs-game-disk.sh $(BINDIR)/AGI.2mg AGI AGI $(BINDIR)/AGI-data.2mg +endif diff --git a/scripts/make-agi-iigs-disk.sh b/scripts/make-agi-iigs-disk.sh index a24b672..ea39d66 100755 --- a/scripts/make-agi-iigs-disk.sh +++ b/scripts/make-agi-iigs-disk.sh @@ -10,19 +10,23 @@ # has no such cap, so the whole payload lands on one 2MB ProDOS volume. # # Usage: scripts/make-agi-iigs-disk.sh [game-name] -# game-name : subdirectory under examples/agi/gamedata/ (default kq3) -# Requires: ./install.sh install toolchain:iigs; built build/iigs/bin/AGI. +# game-name : subdirectory under games/ (default kq3) +# Env: BINDIR the built AGI's folder (default build/iigs/bin) +# JOEYLIB the JoeyLib checkout (default ../joeylib) +# Requires: JoeyLib's ./install.sh install toolchain:iigs; build/iigs/bin/AGI +# built (make iigs). set -euo pipefail repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) -source "$repo/scripts/lib/joeyEnv.sh" +JOEYLIB=${JOEYLIB:-$repo/../joeylib} +source "$JOEYLIB/scripts/lib/joeyEnv.sh" joeyNeed LLVM816_ROOT toolchain:iigs joeyNeed CADIUS cadius BINDIR="${BINDIR:-$repo/build/iigs/bin}" out=${1:?usage: make-agi-iigs-disk.sh [game]} game=${2:-kq3} -gamedata=$repo/examples/agi/gamedata/$game +gamedata=$repo/games/$game agiBin=$BINDIR/AGI VOL=AGI @@ -31,7 +35,7 @@ VOL=AGI # prefix to the volume it launched from, and ProDOS lookups are # case-insensitive, so the uppercase names match the lowercase fopen paths. [ -x "$CADIUS" ] || { echo "make-agi-iigs-disk.sh: cadius not found at $CADIUS" >&2; exit 2; } -[ -f "$agiBin" ] || { echo "make-agi-iigs-disk.sh: $agiBin not built (make -f make/iigs.mk $agiBin)" >&2; exit 1; } +[ -f "$agiBin" ] || { echo "make-agi-iigs-disk.sh: $agiBin not built (make iigs)" >&2; exit 1; } [ -d "$gamedata" ] || { echo "make-agi-iigs-disk.sh: $gamedata is not an AGI v2 game directory" >&2; exit 1; } [ -f "$gamedata/LOGDIR" ] || { echo "make-agi-iigs-disk.sh: $gamedata has no LOGDIR -- not AGI v2" >&2; exit 1; } diff --git a/scripts/test-agi.sh b/scripts/test-agi.sh index b92fc04..227f76e 100755 --- a/scripts/test-agi.sh +++ b/scripts/test-agi.sh @@ -1,9 +1,9 @@ #!/usr/bin/env bash # Host-side regression tests for the AGI port. # -# Compiles tests/agi/testAgiRes.c + examples/agi/agiRes.c with the -# host gcc and runs it against every game directory in -# examples/agi/gamedata/. Each game is exercised in isolation; the +# Compiles the tests in tests/ with the interpreter sources they cover, +# using the host gcc and JoeyLib's headers, and runs them against every +# game directory in games/. Each game is exercised in isolation; the # script aggregates pass/fail counts and exits non-zero if any test # fails. # @@ -15,13 +15,17 @@ # depend on capturing video / log output through DOSBox or MAME. # # Usage: -# scripts/test-agi.sh # test every subdir of gamedata/ +# scripts/test-agi.sh # test every subdir of games/ # scripts/test-agi.sh kq3 agidemo # test only the named subdirs +# Env: +# JOEYLIB the JoeyLib checkout (default ../joeylib) set -euo pipefail repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) -gamedata=$repo/examples/agi/gamedata +JOEYLIB=${JOEYLIB:-$repo/../joeylib} +[[ -f $JOEYLIB/include/joey/joey.h ]] || { echo "test-agi: no JoeyLib checkout at $JOEYLIB (set JOEYLIB)" >&2; exit 2; } +gamedata=$repo/games build=$repo/build/tests testRes=$build/testAgiRes testPic=$build/testAgiPic @@ -40,7 +44,7 @@ mkdir -p "$build" # the AGI parsers, never the JoeyLib HAL), but joey/platform.h #errors # unless exactly one is defined. DOS is the closest fit for a Linux # host (chunky, little-endian, x86 lineage). -hostCflags=(-std=c99 -Wall -Wextra -Werror -O2 -DJOEYLIB_PLATFORM_DOS -I "$repo/include" -I "$repo/examples/agi" -I "$repo/src/core") +hostCflags=(-std=c99 -Wall -Wextra -Werror -O2 -DJOEYLIB_PLATFORM_DOS -I "$JOEYLIB/include" -I "$repo" -I "$JOEYLIB/src/core") stubSrc=$build/hostStubs.c cat > "$stubSrc" <<'EOF' #include "joey/draw.h" @@ -85,45 +89,45 @@ EOF echo "Building host tests..." gcc "${hostCflags[@]}" \ - "$repo/tests/agi/testAgiRes.c" \ - "$repo/examples/agi/agiRes.c" \ + "$repo/tests/testAgiRes.c" \ + "$repo/agiRes.c" \ "$stubSrc" \ -o "$testRes" gcc "${hostCflags[@]}" \ - "$repo/tests/agi/testAgiPic.c" \ - "$repo/examples/agi/agiRes.c" \ - "$repo/examples/agi/agiPic.c" \ + "$repo/tests/testAgiPic.c" \ + "$repo/agiRes.c" \ + "$repo/agiPic.c" \ "$stubSrc" \ -o "$testPic" gcc "${hostCflags[@]}" \ - "$repo/tests/agi/testAgiView.c" \ - "$repo/examples/agi/agiRes.c" \ - "$repo/examples/agi/agiPic.c" \ - "$repo/examples/agi/agiView.c" \ + "$repo/tests/testAgiView.c" \ + "$repo/agiRes.c" \ + "$repo/agiPic.c" \ + "$repo/agiView.c" \ "$stubSrc" \ -o "$testView" gcc "${hostCflags[@]}" \ - "$repo/tests/agi/testAgiVm.c" \ - "$repo/examples/agi/agiRes.c" \ - "$repo/examples/agi/agiVm.c" \ - "$repo/examples/agi/agiObj.c" \ + "$repo/tests/testAgiVm.c" \ + "$repo/agiRes.c" \ + "$repo/agiVm.c" \ + "$repo/agiObj.c" \ "$stubSrc" \ -o "$testVm" gcc "${hostCflags[@]}" \ - "$repo/tests/agi/testAgiPipeline.c" \ - "$repo/examples/agi/agiRes.c" \ - "$repo/examples/agi/agiVm.c" \ - "$repo/examples/agi/agiObj.c" \ + "$repo/tests/testAgiPipeline.c" \ + "$repo/agiRes.c" \ + "$repo/agiVm.c" \ + "$repo/agiObj.c" \ "$stubSrc" \ -o "$testPipeline" # Pick the game directories to exercise. With no args, every -# subdirectory of examples/agi/gamedata that contains LOGDIR is a -# candidate (skips stray README files etc). +# subdirectory of games/ that contains LOGDIR is a candidate (skips +# stray README files etc). if [[ $# -gt 0 ]]; then games=("$@") else @@ -142,9 +146,10 @@ fi if [[ ${#games[@]} -eq 0 ]]; then echo echo "No AGI game directories found under $gamedata" - echo "Drop an AGI v2 game in examples/agi/gamedata// and rerun." - echo "See examples/agi/gamedata/README.md for the required files." - exit 0 + echo "Drop an AGI v2 game in games// and rerun." + echo "See games/README.md for the required files." + # Nothing was tested: not a pass (a gate runner reads 2 as skipped). + exit 2 fi passed=0 diff --git a/scripts/verify.sh b/scripts/verify.sh new file mode 100755 index 0000000..03f4d54 --- /dev/null +++ b/scripts/verify.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# verify.sh - JoeyAGI's gates, run one after another, with a summary. +# +# host the resource loader, PIC and VIEW decoders, the LOGIC VM and the +# VM pipeline, built for the host and run against every game in +# games/ (scripts/test-agi.sh) +# disk the bootable IIgs volume with games/kq3 on it boots on a CFFA2 +# card, launches the interpreter and draws (make +# iigs-game-disk-agi, then JoeyLib's verify-iigs-game-disk.sh with +# the 8 MB the interpreter wants) +# +# Usage: scripts/verify.sh [gate...] every gate when none is named +# Env: JOEYLIB the JoeyLib checkout (default ../joeylib) +# The disk gate checks what is built: run make first (make check does both). +# +# Exit codes: +# 0 every gate passed +# 1 a gate failed +# 3 none failed, but one was skipped for a missing prerequisite (an +# emulator, the IIgs toolchain, no game in games/) -- not 0, so a run +# that tested nothing cannot look green +# A gate exits 2 to say it was skipped. +set -uo pipefail + +repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +JOEYLIB=$(cd "${JOEYLIB:-$repo/../joeylib}" && pwd) || { echo "verify: no JoeyLib checkout (set JOEYLIB)" >&2; exit 2; } +export JOEYLIB +diskGame=kq3 + +# name command...: every gate, in run order. +GATES=$(cat <&2; exit 2; }; make -s iigs-game-disk-agi AGI_GAME=$diskGame || exit 1; $JOEYLIB/scripts/verify-iigs-game-disk.sh build/iigs/bin/AGI.2mg 2 8M +EOF +) + +names=$(echo "$GATES" | awk '{ print $1 }' | tr '\n' ' ') +for want in "$@"; do + case " $names " in + *" $want "*) ;; + *) echo "verify: unknown gate '$want' (choose from: $names)" >&2; exit 2 ;; + esac +done +selected=${*:-$names} + +gateNames=() +gateVerdicts=() +gateSeconds=() +passed=0 +failed=0 +skipped=0 + +while read -r name cmd; do + case " $selected " in *" $name "*) ;; *) continue ;; esac + echo + echo "=== verify: $name ($cmd) ===" + start=$SECONDS + ( cd "$repo" && bash -c "$cmd" ) < /dev/null + case $? in + 0) verdict=PASS; passed=$((passed + 1)) ;; + 2) verdict="SKIP (prerequisite missing)"; skipped=$((skipped + 1)) ;; + *) verdict=FAIL; failed=$((failed + 1)) ;; + esac + gateNames+=("$name") + gateVerdicts+=("$verdict") + gateSeconds+=("$((SECONDS - start))") +done <<< "$GATES" + +echo +echo "================ verify summary ================" +for i in "${!gateNames[@]}"; do + printf " %-10s %-28s %4ds\n" "${gateNames[$i]}" "${gateVerdicts[$i]}" "${gateSeconds[$i]}" +done +echo " ----" +printf " %d passed, %d failed, %d skipped\n" "$passed" "$failed" "$skipped" + +if [ "$failed" -gt 0 ]; then + echo "verify: FAIL" >&2 + exit 1 +fi +if [ "$skipped" -gt 0 ]; then + echo "verify: INCOMPLETE - some gates skipped (see above)" >&2 + exit 3 +fi +echo "verify: PASS (all $passed gates)" +exit 0 diff --git a/tests/testAgiPic.c b/tests/testAgiPic.c index c18eb52..5aeed38 100644 --- a/tests/testAgiPic.c +++ b/tests/testAgiPic.c @@ -1,4 +1,4 @@ -// Host-side test for examples/agi/agiPic.c. +// Host-side test for agiPic.c. // // Loads a single PIC resource from a game directory, decodes it, and // writes two image files: a color PPM for the visual plane (palette diff --git a/tests/testAgiRes.c b/tests/testAgiRes.c index b56cc58..7776fef 100644 --- a/tests/testAgiRes.c +++ b/tests/testAgiRes.c @@ -1,4 +1,4 @@ -// Host-side regression test for examples/agi/agiRes.c. +// Host-side regression test for agiRes.c. // // The AGI resource loader is plain stdio C with no JoeyLib runtime // dependencies, so we can compile and exercise it on the build host diff --git a/tests/testAgiView.c b/tests/testAgiView.c index 9c332e0..74c3ad4 100644 --- a/tests/testAgiView.c +++ b/tests/testAgiView.c @@ -1,4 +1,4 @@ -// Host-side test for examples/agi/agiView.c. +// Host-side test for agiView.c. // // Loads a single VIEW resource, parses it, dumps the loop/cel // structure, and writes cel 0 of every loop as a PPM image. The diff --git a/tests/testAgiVm.c b/tests/testAgiVm.c index b0948f2..330403c 100644 --- a/tests/testAgiVm.c +++ b/tests/testAgiVm.c @@ -1,4 +1,4 @@ -// Host-side test for examples/agi/agiVm.c (Phase 1 sub-A). +// Host-side test for agiVm.c (Phase 1 sub-A). // // Loads a LOGIC resource (default 0), runs the VM until it halts, // and prints the halt reason plus any non-zero vars/flags. Since