Build standalone against JoeyLib.

This commit is contained in:
Scott Duensing 2026-10-05 18:50:09 -05:00
parent 5a9db28d63
commit 96c72ca4ba
12 changed files with 335 additions and 37 deletions

15
.gitignore vendored Normal file
View file

@ -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

28
Makefile Normal file
View file

@ -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 <port> 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

79
README.md Normal file
View file

@ -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/<machine>/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).

49
games/README.md Normal file
View file

@ -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.

32
program.mk Normal file
View file

@ -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

View file

@ -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 <output.2mg> [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 <output.2mg> [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; }

View file

@ -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/<name>/ and rerun."
echo "See examples/agi/gamedata/README.md for the required files."
exit 0
echo "Drop an AGI v2 game in games/<name>/ 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

86
scripts/verify.sh Executable file
View file

@ -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 <<EOF
host scripts/test-agi.sh
disk [ -f games/$diskGame/LOGDIR ] || { echo "disk: no games/$diskGame to put on the volume" >&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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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