joeyagi/STATUS.md
2026-10-21 01:16:19 -05:00

46 KiB

JoeyAGI status

Where JoeyAGI stands against Sierra's own interpreter, subsystem by subsystem and command by command, with the evidence. Written from a clean-room deep dive (October 2026): the public AGI specifications for the formats and commands, and Sierra's interpreter (King's Quest III 2.00, interpreter 2.435, in DOSBox Staging at 386SX-16 speed; a fast 386 since 2026-10-19) run as a black box for everything the specifications leave open. No ScummVM, NAGI or Sarien source was read until 2026-10-19; since then ScummVM's AGI engine is read for behavior, never copied (it is GPL), and Sierra's interpreter still decides.

In short

  • King's Quest III plays. The opening runs in step with Sierra's (title, credits over Manannan, the house, the story windows: checked with single screenshots at 9, 17 and 84 seconds); play starts in the hall with walking, typing, the parser and its unknown-word replies, the menus, the inventory screen, the F1 help screen, save, restore, restart and KQ3's debug mode and teleports all behaving as on Sierra's interpreter. Where frames still differ it is in things that move with time or chance (Manannan's sparkles, the cat, the chickens, wandering), because DOSBox runs in real time and the harness in virtual time, and each interpreter seeds its random numbers from its own clock (the generator itself is Sierra's: see "Random numbers").
  • Every picture is exact. All 97 KQ3 pictures (visual and priority, 194 frames) match Sierra's pixel for pixel, as does a synthetic picture using every drawing action and all 16 pens, and a long diagonal that shows Sierra's byte-sized line error (see "Interpreter versions and quirks").
  • shake.screen is Sierra's, measured frame by frame from a recording of Sierra's interpreter and reproduced beat for beat with JoeyLib's existing API (see "shake.screen").
  • Sierra's window and input-line quirks are reproduced: a print.at window at column 0 or 1 is drawn and only partly erased as Sierra's byte arithmetic does it, and set.cursor.char changed mid-line moves the echo as on Sierra's. Both, and the picture-line quirk, are gated on the interpreter version read from the game's AGIDATA.OVL.
  • 23 probe games, 93 frames and one recording: 89 match Sierra exactly. The others are follow.ego's random detours (its deterministic half matches) and the clock probe's four frames, which all show one measurement: a 1-second timed window that closes one cycle (50 ms) later than Sierra's (the 3-, 2- and half-second ones match).
  • All six ports build; the IIgs disk boots and draws (it was blank when the dive started), and the ST, Amiga and X68000 builds draw KQ3 correctly (screenshots/port-*.png).
  • Gates: make check passes: host 124/124 (see "Gates"), disk PASS.

Not verified: playing KQ3 to the end, death scenes, the later lands (Llewdor, the ship, Daventry) beyond their pictures and a few teleports, audio by ear, and speed on the 8 MHz machines.

How it was checked

Tool What it does
tests/hostPort.c, tests/agiHost.c The whole interpreter on the build machine (JoeyLib's BLANK port): virtual 60 Hz time, a scripted keyboard, every frame resolved through the palette and hashed (FNV-1a) or saved as PPM. Fully reproducible.
tests/agiGame.py, tests/probes.py Builds AGI games from scratch (LOGIC assembler from the spec's command table, PICTURE byte lists, VIEW from text art, SOUND, WORDS.TOK, OBJECT). Each probe records what the interpreter did cycle by cycle into variables and prints them, so its frames do not depend on timing.
scripts/oracle.sh Runs a probe (or a KQ3 session) on Sierra's interpreter in DOSBox under a private Xvfb, driven by xdotool, and on JoeyAGI, then compares every shot with tests/tools/compareFrames.py (EGA colors, text read with each interpreter's own font -- JoeyAGI's font is its own -- and Sierra's 40-column text mode compared cell by cell). A probe whose script says record is recorded instead: DOSBox's video capture (every 70 Hz frame) on Sierra's side, every frame on JoeyAGI's, compared by tests/tools/shakeMeasure.py.
scripts/test-agi.sh The host gate: resources, probes and KQ3 sessions against tests/expected/.

A caution learned on the way: DOSBox's screenshot key (Ctrl+F5) reaches the game as a key press, and KQ3's opening hurries along when it sees one. The KQ3 checkpoints below were each taken with a single screenshot.

The state found

The interpreter at the start (commit 96c72ca) was a VM/host split with many pieces stubbed: sound set its flag at once; save.game, restore.game, status, show.obj, menu.input, obj.status.v did nothing; restart.game quit; object collisions and block were not modeled; posn tests were approximations; ego stepped a fixed 2 pixels; and an argv "starting room" let you skip "a title screen the VM can't yet advance through". The IIgs disk gate passed, but on the IIgs the old engine was the one drawing.

Nearly every module was rewritten (agi.c, agiRes, agiPic, agiView, agiObj, agiText, agiVm) or written new (agiGfx, agiFont, agiInput, agiSound, agiSave), each behavior settled against the specifications and then against Sierra's interpreter.

Subsystems

Subsystem Status Evidence
Resources (directories, VOL, WORDS.TOK, OBJECT) Works. Files read whole into buffers of their own size from the Memory Manager on the IIgs; a missing or empty directory just means none of that type. resources-kq3: 125 logics, 97 pictures, 216 views, 34 sounds load and decode
AGI v3 (2026-10-20) Works: DIR (four directories in one file), VOL.n (prefix from the known games: KQ4, GR, MH, MH2, BC, DM), 7-byte headers, LZW (9 to 11 bits) and nibble pictures expanded at load, packed logics' messages not decrypted, OBJECT decrypted only when encrypted. Gold Rush (both copies), Manhunter 1 and 2 (3.02 and 3.03), Demo 3, Black Cauldron 2.10 (3.002.098), Gold Rush 3.0, and King's Quest IV (2.0, 2.2, and 2.3 for DOS, and the Apple IIgs release) load and start; on Sierra's interpreter (as .COM) the DOS ones start the same, King's Quest IV's past a manual question JoeyAGI skips. 3.002.086's rules (loops for any loop count, the x <= 0 left border, one-argument hide.mouse and hold.key) and v3's (draw reapplies loop and cel; new.room ends a walk to a click) are in resources for all of them; gr-start, grv201-start, mh1-start, mh2-start, mh2v303-start, bcv210-start, grv30-start, agidemo3-start, kq4-start, kq4v20-start, kq4v22-start, kq4gs-start
Mouse (2026-10-20) Works on the host (the DOS interpreters had none; this follows the Amiga, ST, IIgs, and Mac ones as ScummVM describes them): a JoeyLib sprite pointer drawn at each present and kept to fence.mouse's box; a left click on the menu bar opens the menus, in the picture walks ego to it (its baseline's middle; a direction key, or in v3 a new room, stops it); the menus follow the pointer and a click picks; a click closes a message window kq3-mouse (the pointer, a walk, the bar, a hovered item, a click choosing Help, a click closing it)
Game patches (agiPatch.c) King's Quest IV's zombie distance; the manual questions of King's Quest IV (rooms 141 to 143) and Gold Rush (room 125) skipped to where a right answer leads; Manhunter 1's Field Guide question (logic 159) patched as it loads into a jump to the right answer's path; Gold Rush's clock fed the real cycle rates kq4-start, kq4gs-start, grv201-start, mh1i102-start; the clock run at Normal, Fast, and Fastest on the host (10 game seconds in 10 seconds each)
PICTURE Exact: the spec's line (exact half rounds away from the start) with Sierra's byte-sized error term, corners, relative lines, scanline fill (visual, priority-only and both), solid and splatter pens of every size with the texture wrap at bit 254 194/194 KQ3 frames pixel-exact; pictures probe visual + priority exact (screenshots/probe-pictures-*.png); winedge probe's long diagonal
VIEW Works: loops, mirrored loops, cel transparency, descriptions; one allocation per view resources-kq3 (every cel decoded), setview, parser (show.obj)
Object drawing Exact: blits stacked with save-unders, not-updated objects under updated ones, sorted by baseline (or band top for fixed priority), control lines take the priority below them, add.to.pic margins (an outline of the control value from the baseline up to its band's top) priority probe incl. the priority screen exact
Motion and cycling Exact: step and cycle counters, the lost first step after draw/new.room/reposition, end.of.loop and reverse.loop flag timing, move.obj arrival (slack = step size), follow.ego distance. wander and follow.ego's detours are Sierra's code (read from 2.915): a heading kept about 255 cycles, a new one when an object's step leaves it standing. The cycle and motion modes share four bytes per object, as in Sierra's, so starting one overwrites what another left timing, motion, counters, redraw, steering, cycling exact; follow deterministic rows exact
Ego steering Exact: v6 steers ego before and after logic 0 under player and program control; set.dir on ego has no effect; arrow keys toggle v6 egodir, steering, ego exact
Control lines, collisions, position fixing Exact: barriers, conditional barriers and ignore.blocks, signal lines (f3), water/land, priority 15 ignoring control, the block rectangle, objects' baselines (touching counts, crossing counts), the spiral position fix control, collide exact
Windows and text Exact: print/print.at geometry and Sierra's wrap rule, display wrapping, text attributes in picture and text mode, clear.lines/clear.text.rect, the status line, timed windows (v21); print.at windows at columns 0 and 1 as Sierra's byte arithmetic draws and erases them; the screen under a window put back exactly to the pixel windows (12 frames), winedge (18 frames), textscreen, clock exact
Input line Exact: row 23 unless configure.screen moves it, no cursor until set.cursor.char, typed characters drawn in place, Enter and accept.input redraw it, prevent.input clears it; the echo keeps its own position as Sierra's does, so a cursor changed mid-line behaves as there parser probe exact (17 frames); cursor probe exact (20 frames)
shake.screen Exact: 4 beats per unit of the whole screen moved (8, 4) for 4 frames and back for 4, blocking shake recording: beats, frames and offset identical to Sierra's
Interpreter version Read from AGIDATA.OVL ("Version 2.435", "Version 3.002.149" as 3149), else the AGI file, else 2.435 (a v3 game 3149); the measured quirks follow it resources-kq3 (KQ3: 2435)
Parser and said Exact: separators, lower case, longest dictionary match incl. multi-word entries, ignored words, unknown words (stop, stay in the list for any-word/rest-of-line, v9 = position), all-ignored lines say nothing, v9 cleared each cycle parser exact; a 10-input x 9-pattern said() matrix matched Sierra
Menus Exact: bar, items, disabled items, keyboard navigation; kept across restart parser m01..m07 exact; screenshots/kq3-menu-after-restart.png
Inventory (status) and show.obj Exact layout (Sierra draws the inventory in text mode; compared cell by cell) parser p10, m05; screenshots/kq3-inventory-tab.png
Sound Flag timing exact: the flag is set when the first voice ends (Sierra's PC interpreter plays only that voice), whatever the other voices do; an empty first voice ends at once; f9 off sets it at once; stop.sound sets it. JoeyAGI plays all four voices on JoeyLib. sound probe exact (tunes of 3 to 240 ticks, 1 to 60 notes, long tails, no first voice); audio itself not compared (PC speaker vs. JoeyLib voices)
Clock (v11..v14), timed windows (v21) Clock exact; v21 windows of 3, 2 and 0.5 seconds measured identical to Sierra's against v11, the 1-second one one cycle longer clock probe; KQ3's once-a-second countdowns traced in step with Sierra's
have.key Works: a key this cycle that nothing else took, else (as Sierra's 2.915 does) one that arrived since the cycle's keys were handled, taken raw even if set.key maps it; a logic that asks again in the same cycle (KQ3's help screen spins on it) waits a frame per ask and takes any key pictures, priority, textscreen, ego probes; screenshots/kq3-help-f1.png
Save / restore / restart Work: restore rebuilds the picture from the script and ends the cycle so logic 0 sees f12 next (KQ3 relies on it); restart keeps menus and set.key mappings (KQ3 builds them only once) kq3-saverestore session; restart + menu matches Sierra
Memory Logics other than 0 are freed at new.room and so are the views no object shows, as Sierra frees a room's resources ASan run of every session clean
Picture output Every machine pushes a picture rectangle with one jlSurfacePasteIndexed call, each AGI pixel doubled (color runs through jlFillRect on the planar machines before, two jlDrawPixel calls per AGI pixel before that) IIgs frame outside a moving object byte-identical to the old code; JoeyLib's UBER indexed checks on every port

Bugs found and fixed in this dive, with causes

  1. IIgs: blank screen. Every resource came from malloc, and on the IIgs the C heap is the small window left in the entry bank (1926 bytes with 22 KB of BSS). Logic 0 could not be allocated, so nothing ran. All interpreter memory now comes from jlAlloc (the Memory Manager); files are read into buffers of their exact size; a view is one allocation; BSS fell from 22 KB to 4 KB (byte-sized fill stack, one shared line buffer, the picture script moved into the jlAlloc'd state).
  2. ST, Amiga, X68000: picture output. Planar stages have no chunky bytes, and the fallback drew two jlDrawPixel calls per AGI pixel. Then runs of one color per row through jlFillRect. (An 8x8 tile path was tried first and came out striped: jlTileT's bytes are in each port's own layout.) Since 2026-10-17 every machine pastes the rectangle with jlSurfacePasteIndexed (doubleWide), widened to whole 2-pixel pairs: IIgs picture pushes 11.8% -> ~4% of the CPU, X68000 ~4.7x cheaper per frame drawn.
  3. Text attributes. set.text.attribute in picture mode stores a background of white (or black); text.screen clears to that; on the text screen the colors are used as given; clear.lines and clear.text.rect on the text screen take the color as a PC attribute byte (so 0..15 clear to black). The old rule (background | 8) was wrong.
  4. Sound on at start. Sierra's interpreter sets f9 itself (its status line reads "Sound:on" in a game that never touches f9).
  5. Input line. Default row 23, not 22; no cursor before set.cursor.char; typing draws only the changed cell, so text displayed on the input row stays until Enter or accept.input redraws the line.
  6. Unknown words. said() failed whenever v9 was set; Sierra keeps the unknown word in the list, where any-word and rest-of-line match it, so "open the door now" matches said(open, rol). A line of only ignored words no longer sets f2, and v9 is cleared each cycle.
  7. Ego steering. v6 is copied to ego's direction before and after logic 0 in both control modes (the old code copied ego's direction into v6 under player control, so set.dir moved ego); v6 set by a logic now moves ego that same cycle.
  8. reposition / reposition.to skip the object's step in that cycle.
  9. Sound flag came after the longest voice; Sierra's comes after the first voice. This alone put KQ3's opening seconds behind (its title waits on the tune's flag).
  10. Restore let the rest of the cycle run (objects stepped once past the saved state) and cleared f12 before any logic saw it; KQ3's logic 0 tests f12 after a restore to fix up its menu.
  11. Restart wiped the menus and set.key mappings, but KQ3 builds both only on a first start (f6 clear), so after a restart Esc and the function keys did nothing.
  12. Resource memory grew without bound (every logic and view ever loaded stayed); now freed at new.room.
  13. Games without SNDDIR (or any empty directory) failed to start.
  14. Frame order. Each idle frame polled the keys, pumped the sound and ticked the clock before waiting for the vertical blank, so a cycle that started right after the wait saw them a frame stale: a short sound's flag arrived a cycle late. The frame now passes first.
  15. have.key let a frame pass every time a logic asked, and took any key that arrived then -- so a probe asking once a cycle swallowed the arrow key meant to steer ego. Now only a logic asking twice in one cycle waits.
  16. shake.screen was a pause. It now shakes as Sierra's does (below).
  17. print.at at column 0 or 1 drew a clipped window and erased it whole; Sierra's interpreter draws and erases it displaced (below).
  18. Window save-under went through jlSurfaceCopyRect, which widens a copy to 16-pixel groups, so closing a window also put back up to 15 stale pixels on each side: harmless while nothing beside an open window changed, wrong for the column-1 window, whose own frame and text sit there. Now exact: a chunky surface copies bytes (an AGI pixel each), a planar one copies the whole groups and the pixels at the ends one by one. Checked on all five machine ports against the host frame (0 pixels differ on ST, Amiga, X68000, DOS and IIgs).
  19. set.cursor.char mid-line echoed from the end of the typed text; Sierra's echo keeps its own position (below).
  20. Picture lines were exact to the fraction; Sierra's interpreter carries the error in a byte, which a line long on both sides overflows (below). No KQ3 picture has such a line.

Found 2026-10-19 by playing every v2 game's start on its own interpreter (scripts/oracle.sh GAME-start):

  1. Mixed-Up Mother Goose stayed black: logic 0 sets up only on f5 (the game starts as a room does), and it needs hold.key and set.simple (0xAA to 0xB6 were unknown commands).
  2. display() wrapped at 40 minus its column; Sierra's wraps at 40 counted from the text and goes on at column 0 (King's Quest 1's copyright line at column 1 is 39 characters on one row).
  3. %w was lower case; Sierra's keeps the word as typed (Space Quest 1: I don't understand "Wilco").
  4. get.string cleared its field in black; Sierra's draws the text and cursor in the current attribute (Mother Goose's name, black on its white window).
  5. f15 kept every window up; Sierra's clears it after the one print it covers, and show.pic clears it too (Larry's thanks after Alt+X waited on Sierra's, flashed past on JoeyAGI).
  6. A restore drew the input line while input was off, in the text attribute's background (a white bar under Mother Goose's picture); prevent.input now blanks it in black and nothing draws there until accept.input.

Found 2026-10-20 with the new games (the demos, the Christmas card, the fan games, and the v3 games):

  1. A loop of tests alone never ended: Manhunter 1 waits a second with "v11 = 0; while (v49 > v11)", and the clock moved only between cycles. A backward jump with no command since the last one now lets a frame pass, as Sierra's timer interrupt moves the clock while such a loop spins.
  2. The prompt was drawn raw; Sierra's expands string 0's %-codes (Gold Rush's is "%g69", ">").
  3. OBJECT was always decrypted; the demos' and the Christmas card's are not encrypted (encrypted, the first offset is past the file's end).
  4. have.key never saw a key set.key maps: Sierra's 2.915 reads the keyboard there and takes a key that came in after the cycle's keys were handled (Demo 2's Return at "Press any key", when it lands while the logics run). The first ask now polls; on the host, where keys arrive only between cycles, nothing changes.
  5. King's Quest IV's manual question is skipped (decision 3): a new.room to its question rooms 141 to 143 goes to room 96, where a right answer leads (agiPatch.c, the first game-specific change).

Found 2026-10-20 with the eXo releases (PLAN.md, "The games"):

  1. Gold Rush's clock stopped at Fast and Fastest in the cracked copy (games/gr): the crack put logic 73 in place of logic 129, the speed test that sets v165 and v167, so logic 0 copied 0 into v166. In the original the test counts a second of empty cycles at Fastest and takes a third and a quarter for a 1988 PC's playing rate; JoeyAGI holds Fastest to the frame rate, so that clock would run six times fast. Both copies now get the real rates every cycle (agiPatch.c).
  2. Gold Rush's quiz (the original's claim jumper, rooms 125 to 128) is skipped: logic 129's new.room(125) goes to room 73, where a right answer leads, which is also what the crack does.
  3. testResources failed King's Quest III 2.14, whose sound 36 shipped cut to 1035 of its 2281 bytes (the next resource follows it in VOL.1). Such a sound is now listed as truncated; the player already ends a voice where the resource ends.

Found 2026-10-21 with Sierra's DOS floppies (PLAN.md, "The games"):

  1. 2.089 words the status line its own way: "Score: 0 of 202" and "Sound is on" from column 27 (Space Quest 1.0X); 2.272 already has "Score:0 of 202" and "Sound:on". Gated on 2.089 and below.
  2. Manhunter 1 asks about its Field Guide in room 159 and quits on a wrong answer; games/mh1 is a cracked copy that never asks. The question's random(6,61,96) is patched as the logic loads into a jump to the right answer's path, the crack's own change (agiPatchLogic, which checks the bytes first). Manhunter 2's logic 159 already skips its question.
  3. oracle.sh picked the wrong program for a release with an installer: Gold Rush 3.0 ships GR.COM and no SIERRA.COM, so _MSTEST.EXE ran. An existing .COM is now taken first, and SIERRA.EXE (Black Cauldron 2.10's whole interpreter) before any other .EXE.

Fixed earlier in the dive (before the gates existed), all verified against Sierra's interpreter: the picture decoder (line algorithm, fill semantics, pens, splatter textures); window geometry and the wrap rule; display wrapping; the status line; menus; the inventory layout; show.obj placement; add.to.pic and its margin boxes; sprite priority masking; control-line blocking; object collisions; the position-fixing spiral; motion and cycle counter timing; move.obj arrival; follow.ego; set.view/set.loop resets of missing loops and cels; animate.obj leaving step and cycle settings alone; %-codes in messages (%v with |width, %s, %w, %0, %m, %g); the parser's longest match; the intro's timing; v21 window timeouts; a guard for KQ3's room 43 call.v(0) recursion (Sierra hangs there); Alt and Ctrl chords no longer type characters.

shake.screen

Measured on Sierra's interpreter from DOSBox's video capture, every 70 Hz VGA frame of probe games calling shake.screen(1), (2) and (3) (the shake probe) and (64), (65) and (0) (one-off probes); tests/tools/shakeMeasure.py reads the shakes out of a recording:

  • What moves: the whole screen -- picture, status line, displayed text and the input line -- by exactly 8 screen pixels right (4 AGI pixels) and 4 rows down, a plain shift; the uncovered strips are black. No other offset or direction occurs.
  • A beat is the moved screen for 4 frames, then the screen in place for 4, locked to the vertical blank (114 ms at 70 Hz).
  • Beats: 4 per unit: 1, 2, 3 gave 4, 8, 12 beats; 64 gave 256 and 65 gave 260, so the count is 16 bits, and shake.screen(0) ran past 1090 beats before the recording stopped (65536 beats, about two hours).
  • It blocks: no cycles run and nothing animates until it ends.

JoeyAGI (gfxShakeScreen in agiGfx.c, JoeyLib's existing API, no per-port code): it copies the stage to one surface and builds a second with that screen moved (8, 4) over black (a chunky surface moves its bytes; a planar one draws the screen as one 40x25-tile sprite at (8, 4)); each beat copies the moved one to the stage, presents and lets 4 display frames pass (timed with jlMillisElapsed against jlFrameHz, so a slow copy is not added on top), then the same with the screen in place. shake.screen(0) is 65536 beats, as on Sierra's. If the two surfaces cannot be allocated the shake is a pause of the same length. The host's recording reads the same as Sierra's: 4, 8 and 12 beats, moved 4 frames, in place 4 frames, offset (8, 4); the host gate pins every frame of it.

IIgs cost: a phase is a 32 KB jlSurfaceCopy into the stage (about 108 ms) and a full-screen present (about 74 ms): 11 frames (183 ms) where Sierra's takes 4, so a beat is 367 ms against Sierra's 133 ms at 60 Hz and a shake lasts 2.75 times as long. The other ports were not timed.

A JoeyLib addition that would make it much better (not made): a display offset -- show the stage moved by (dx, dy), uncovered edges black -- as one call (say jlStagePresentOffset(dx, dy)). Where the hardware scrolls it is free: Amiga bitplane pointers plus BPLCON1, the X68000's graphic scroll registers, the VGA CRTC start address; the ST (an STF's video base moves in 256-byte steps) and the IIgs (no hardware scroll) would present the stage shifted, one present's worth of copying instead of a 32 KB copy and a present -- about halving the IIgs's beat.

Interpreter versions and quirks

KQ3's AGIDATA.OVL names its interpreter ("Adventure Game Interpreter", "Version 2.435"); the AGI executable carries no readable version (2.00 is KQ3's own version, shown by the game). At startup detectVersion (agiRes.c) reads the version from AGIDATA.OVL, else from the AGI file, else takes 2.435, the version measured, into game.interpreterVersion; agiQuirk() answers from one table of version ranges (kQuirkVersions), the one place per-version behavior is decided. The IIgs disk now carries AGIDATA.OVL, and resources-kq3 checks KQ3 reads as 2435.

Only one Sierra interpreter, 2.435, was at hand when the three quirks below were measured, so all three are on for 2.000 to 2.999, and narrowing a range is a one-line change. Off, each falls back to the plain behavior (a clipped window, an echo from the end of the typed text, an exact line). Since 2026-10-21 games/ holds every DOS interpreter from 2.089 to 3.002.149 (seventeen), so the probes can now measure each (PLAN.md, Phase 0 item 2). Rules found since are gated where they act: 2.089's status line (bug 35), 2.272's motion rules (agiObj.c), the commands of 2.425 to 3.098 (agiVm.c), and 3.002.086's (agiObj.c, agiPic.c).

  • AGI_QUIRK_WINDOW_BYTE_X -- print.at at column 0 or 1. A window's box is x = 4 * col - 5 AGI pixels, y = 8 * row - 5 screen rows, 4 * width + 10 by 8 * lines + 10, the red frame one pixel in; its sides stop a row short of the top and bottom lines. Saving the screen under it, the white fill, the frame and putting the screen back all address the screen as one line of 160-pixel rows with x in a byte (row * 160 + (x & 255)), each span running on into the next row; the text is drawn where it belongs. At column 1 (x = -1, so 255) the save, fill and restore land a row down at x 95 and spill into the row below from x 0, while the frame (x + 1 = 0) and the text are in place: the window opens with its white running off to the right, and closing it erases only its leftmost 4 * width - 55 pixels, leaving the rest of the frame and text on screen. At column 0 (x = 251) the box and its top, bottom and left lines are drawn a row down, 91 in, and the right side and the text stay in place. Columns 2 and on are not affected. winedge probe: nine windows at columns 0 to 3, one to three lines, on a colored picture, open and closed: 18/18 frames identical.
  • AGI_QUIRK_ECHO_STEPS_BACK -- set.cursor.char mid-line. The input line echo keeps its own position: a redraw (Enter, accept.input) leaves it after the prompt, the text and the cursor; with a cursor set, a typed character first steps back one -- whether or not a cursor was drawn there -- then draws itself and the cursor; Backspace steps back two and draws the cursor and a blank (without a cursor: back one, a blank). So a cursor set mid-line makes the next character overwrite the last one typed (">w", then '' set and "yz" typed, shows ">yz" while the line holds "wyz"), a cursor taken away stays on the line (">pq_" then "r" shows ">pq_r"), and rubbing that line out with the cursor set runs back over the prompt (three Backspaces leave just "*"). cursor probe: 20/20 frames identical.
  • AGI_QUIRK_LINE_BYTE_ERROR -- picture lines (found by the winedge picture): the line's error term is a byte, so a line long on both sides loses steps when it passes 255: (0,0) to (159,167) stalls two rows in ten and ends at (134,167). No KQ3 picture has such a line (all 194 frames are unchanged).

Random numbers

Read from the 2.915 interpreter (Mixed-Up Mother Goose's AGI, an unencrypted MZ file; the code segment starts 512 bytes in):

  • The generator. seed = seed * 0x7C4D + 1 (16 bits); the number is the seed's two bytes XORed (0 to 255). A seed of 0 is replaced first by the BIOS tick count, so it seeds itself at the first use. JoeyAGI's agiRandom does the same with its own clock (virtual time in the host harness, so runs there repeat exactly).
  • random(min, max, v). The number modulo (max - min + 1) in 16 bits, plus min: a maximum below the minimum takes the number as it is. A span of 0 (max = min - 1) divides by zero in Sierra's; JoeyAGI takes the number.
  • wander. Each cycle the count (shared byte 0) runs down; when it had run out, or the object's last step left it standing, a new heading, rand % 9 (0 standing still), and, while the count is under 6, a new one, rand % 51. A count that ran out has wrapped to 255, so a heading lasts about 255 cycles unless the object is stopped.
  • follow.ego. Within the distance (byte 0) of ego's middle it stops and sets its flag (byte 1). The detour count (byte 2) starts at 255, meaning "head straight there". A step that left the object standing gives a heading, rand % 9 drawn until it is not 0, and a count, rand % (half the x and y distance + 1) drawn until it reaches the step size, spent a step size per cycle; Sierra's signed byte compare ends it early when the count is 128 or more.
  • Standing. At each step an object's place is compared with where the last step that moved it (or position, draw) left it; the same place sets its "didn't move" flag.

The 2.4xx interpreter files (AGI) are encrypted, and their loaders decrypt them; the same generator is assumed there.

What remains, most important first

  1. A full play-through. KQ3 has not been finished on JoeyAGI. The rooms, puzzles, spells, deaths and endings past the house are untested beyond their resources decoding and a few teleports.
  2. Speed on the 8 MHz machines. Not measured. The run-length output is much faster than before; picture decoding and flood fill run in C.
  3. Audio by ear. Note timing and the flag are exact; how the four voices and noise sound on each port's JoeyLib audio has not been judged.
  4. The IIgs beyond booting. The disk gate proves it boots and draws; the opening was checked by framebuffer dumps, play on the IIgs was not.
  5. Save/restore look. JoeyAGI has its own slot window (slots 1-9, a description) instead of Sierra's path prompt and list, and its own file format, which is per machine type (byte order and padding).
  6. Random sequences are made by Sierra's generator and spent as Sierra's spends them (wander, follow.ego's detours, random), but each run seeds it from its own clock, so a run on Sierra's interpreter and one on JoeyAGI only match by chance. The generator was read from the 2.915 interpreter; the 2.4xx interpreter files are encrypted and assumed the same.
  7. Other interpreter versions. Only 2.435 was available to measure; the quirks are assumed for every 2.x interpreter (see below).
  8. The font is JoeyAGI's own, not the PC's ROM font.
  9. shake.screen on the IIgs takes 2.75 times as long as on Sierra's (see below); a JoeyLib display offset would fix it.
  10. The flood fill's seed stack (1024 seeds) would truncate a fill that needs more; no KQ3 picture does.

Commands

Every AGI v2 command is implemented. "KQ3 uses" counts the command in KQ3's logics; "Probe" marks commands the probe games exercise against Sierra's interpreter (everything KQ3 uses is also exercised by the KQ3 sessions or checkpoints). FIXED marks behavior corrected in this dive's later half; the earlier rewrite touched nearly every command.

# Command KQ3 uses Probe Status
0x00 return 262 yes works
0x01 increment 186 yes works
0x02 decrement 144 works
0x03 assignn 2659 yes works
0x04 assignv 111 yes works
0x05 addn 106 works
0x06 addv 10 works
0x07 subn 29 yes works
0x08 subv 7 works
0x09 lindirectv 1 works
0x0A rindirect 1 works
0x0B lindirectn 1 works
0x0C set 923 yes works
0x0D reset 737 yes works
0x0E toggle 12 works
0x0F set.v 4 works
0x10 reset.v 5 works
0x11 toggle.v 0 works
0x12 new.room 263 works
0x13 new.room.v 14 works
0x14 load.logics 135 works
0x15 load.logics.v 1 works
0x16 call 253 works
0x17 call.v 5 works
0x18 load.pic 98 yes no-op (draw.pic loads)
0x19 draw.pic 102 yes works
0x1A show.pic 109 yes works
0x1B discard.pic 91 yes no-op (pictures are not kept)
0x1C overlay.pic 9 works
0x1D show.pri.screen 1 yes works
0x1E load.view 375 yes works
0x1F load.view.v 84 works
0x20 discard.view 11 no-op: views go at new.room
0x21 animate.obj 200 yes works
0x22 unanimate.all 2 works
0x23 draw 318 yes works
0x24 erase 153 yes works
0x25 position 306 yes works
0x26 position.v 112 works
0x27 get.posn 48 yes works
0x28 reposition 12 yes works; FIXED (skips that cycle's step)
0x29 set.view 477 yes works
0x2A set.view.v 84 works
0x2B set.loop 227 yes works
0x2C set.loop.v 7 works
0x2D fix.loop 44 yes works
0x2E release.loop 19 works
0x2F set.cel 219 yes works
0x30 set.cel.v 18 works
0x31 last.cel 15 works
0x32 current.cel 23 yes works
0x33 current.loop 11 yes works
0x34 current.view 8 yes works
0x35 number.of.loops 0 works
0x36 set.priority 246 yes works
0x37 set.priority.v 11 works
0x38 release.priority 59 works
0x39 get.priority 10 works
0x3A stop.update 83 yes works
0x3B start.update 72 works
0x3C force.update 19 works
0x3D ignore.horizon 59 works
0x3E observe.horizon 5 works
0x3F set.horizon 57 works
0x40 object.on.water 6 yes works
0x41 object.on.land 2 yes works
0x42 object.on.anything 1 works
0x43 ignore.objs 126 yes works
0x44 observe.objs 15 works
0x45 distance 23 works
0x46 stop.cycling 150 yes works
0x47 start.cycling 100 yes works
0x48 normal.cycle 1 works
0x49 end.of.loop 100 yes works
0x4A reverse.cycle 0 works
0x4B reverse.loop 23 yes works
0x4C cycle.time 139 yes works
0x4D stop.motion 95 yes works
0x4E start.motion 72 yes works
0x4F step.size 19 yes works
0x50 step.time 8 yes works
0x51 move.obj 155 yes works
0x52 move.obj.v 59 works
0x53 follow.ego 30 yes works
0x54 wander 39 works
0x55 normal.motion 35 works
0x56 set.dir 6 yes works; FIXED (no effect on ego: v6 steers it)
0x57 get.dir 4 yes works
0x58 ignore.blocks 127 yes works
0x59 observe.blocks 52 works
0x5A block 10 works
0x5B unblock 6 works
0x5C get 155 yes works
0x5D get.v 2 works
0x5E drop 80 works
0x5F put 30 works
0x60 put.v 10 works
0x61 get.room.v 12 works
0x62 load.sound 111 yes no-op (sound loads)
0x63 sound 125 yes works; FIXED (flag when the first voice ends)
0x64 stop.sound 19 yes works; flag set
0x65 print 1695 yes works
0x66 print.v 27 works
0x67 display 70 yes works
0x68 display.v 5 works
0x69 clear.lines 17 yes works; FIXED (text-screen color is an attribute byte)
0x6A text.screen 3 yes works; FIXED (clears to the attribute background)
0x6B graphics 3 yes works
0x6C set.cursor.char 1 yes works (no cursor before it: FIXED; changed mid-line moves the echo as Sierra's: FIXED)
0x6D set.text.attribute 31 yes works; FIXED (picture-mode background means white)
0x6E shake.screen 8 yes works; FIXED (measured: 4 beats per unit, (8, 4) for 4 frames and back for 4)
0x6F configure.screen 1 works
0x70 status.line.on 6 yes works
0x71 status.line.off 6 works
0x72 set.string 51 works
0x73 get.string 56 works
0x74 word.to.string 0 works
0x75 parse 0 works
0x76 get.num 13 works
0x77 prevent.input 75 works; blanks the input line in black, and nothing draws there until accept.input: FIXED (a restore drew it in the text attribute's background)
0x78 accept.input 45 yes works; redraws the input line
0x79 set.key 39 yes works
0x7A add.to.pic 66 yes works
0x7B add.to.pic.v 8 works
0x7C status 2 yes works: the inventory screen
0x7D save.game 1 works: JoeyAGI's own slot window and file format; under set.simple, no questions, as Sierra's (the slot with that description, else the first empty one)
0x7E restore.game 1 works: JoeyAGI's own slot window; replays the picture script; FIXED (cycle ends, f12 reaches the next cycle); under set.simple, Sierra's pointer list of descriptions
0x7F init.disk 0 no-op (nothing to do on these machines)
0x80 restart.game 1 works; FIXED (keeps menus and set.key)
0x81 show.obj 61 yes works
0x82 random 200 works
0x83 program.control 80 yes works
0x84 player.control 44 yes works
0x85 obj.status.v 2 works: a debug window
0x86 quit 3 works
0x87 show.mem 1 works: a JoeyAGI message
0x88 pause 1 works: a message window
0x89 echo.line 1 works
0x8A cancel.line 1 works
0x8B init.joy 1 no-op (nothing to do on these machines)
0x8C toggle.monitor 1 no-op (nothing to do on these machines)
0x8D version 2 works: a JoeyAGI message
0x8E script.size 1 no-op (nothing to do on these machines)
0x8F set.game.id 1 no-op (nothing to do on these machines)
0x90 log 8 no-op (nothing to do on these machines)
0x91 set.scan.start 22 works
0x92 reset.scan.start 18 works
0x93 reposition.to 49 yes works; FIXED (skips that cycle's step)
0x94 reposition.to.v 58 works; FIXED (as reposition.to)
0x95 trace.on 0 no-op (nothing to do on these machines)
0x96 trace.info 1 no-op (nothing to do on these machines)
0x97 print.at 31 yes works; FIXED (columns 0 and 1 drawn and erased as Sierra's byte arithmetic does)
0x98 print.at.v 2 works (as print.at)
0x99 discard.view.v 0 no-op: views go at new.room
0x9A clear.text.rect 0 works; FIXED (as clear.lines)
0x9B set.upper.left 0 no-op (nothing to do on these machines)
0x9C set.menu 5 yes works
0x9D set.menu.item 21 yes works
0x9E submit.menu 1 yes works
0x9F enable.item 1 works
0xA0 disable.item 3 yes works
0xA1 menu.input 1 yes works
0xA2 show.obj.v 1 works
0xA3 open.dialogue 16 no-op (nothing to do on these machines)
0xA4 close.dialogue 7 no-op (nothing to do on these machines)
0xA5 mul.n 0 works
0xA6 mul.v 0 works
0xA7 div.n 0 works
0xA8 div.v 0 works
0xA9 close.window 0 works
0xAA set.simple 0 works (2.425 on, not 2.440): save and restore by a description, as Sierra's (Mother Goose names the player's save)
0xAB push.script 0 no-op (as ScummVM; no game uses it)
0xAC pop.script 0 no-op (as ScummVM; no game uses it)
0xAD hold.key 0 works (2.425 on, not 2.440): ego walks only while a direction key is down
0xAE set.pri.base 0 works (2.425, and 2.936 on): a y-to-priority table from the base down
0xAF discard.sound 0 no-op (sounds are not kept)
0xB0 hide.mouse 0 works: the pointer hidden; one argument on 3.002.086
0xB1 allow.menu 0 works (3.098 on): a requested menu does not open
0xB2 show.mouse 0 works: the pointer shown again
0xB3 fence.mouse 0 works: the pointer kept to a box in picture coordinates
0xB4 get.mse.posn 0 works: the pointer in picture coordinates
0xB5 release.key 0 works (3.098 on): ends hold.key
0xB6 adj.ego.move.to.x.y 0 no-op (the Amiga and ST releases' two-argument form waits for Phase 5)
# Test KQ3 uses Probe Status
0x01 equaln 2281 yes works
0x02 equalv 35 works
0x03 lessn 157 yes works
0x04 lessv 12 works
0x05 greatern 449 works
0x06 greaterv 10 works
0x07 isset 1631 yes works
0x08 isset.v 1 works
0x09 has 362 works
0x0A obj.in.room 144 works
0x0B posn 307 works
0x0C controller 39 yes works
0x0D have.key 4 yes works
0x0E said 2991 yes works; FIXED (an unknown word only matches any-word and rest-of-line; all-ignored lines say nothing)
0x0F compare.strings 28 works
0x10 obj.in.box 5 works
0x11 center.posn 0 works
0x12 right.posn 0 works
0x13 in.motion.using.mouse 0 works: ego is walking to a click

Gates

make check (build every port, then scripts/verify.sh):

Gate Result
host (scripts/test-agi.sh) 124/124: resources for the 33 v2 and 13 v3 folders in games/ (every resource loads and decodes, a sound shipped cut short listed as truncated; KQ3's interpreter version and picture hashes are checked), 29 probes (shake pins every frame of its recording), 5 KQ3 sessions (opening, play, saverestore, teleport, mouse), and a start session for each of the other 44 game folders (tests/GAME/start.scr: titles, menus, the first room, typed answers)
disk (IIgs AGI.2mg on a CFFA2, MAME) PASS (boots, launches, draws: distinctNibbles=6)

scripts/oracle.sh (not part of make check; needs Sierra's interpreters in games/, Xvfb, xdotool), 2026-10-19: probes 97 of 103 shots identical to Sierra's, including the shake recording, winedge (18/18) and cursor (20/20). The rest: follow.ego's random detours, the clock probe's 1-second window one cycle long (all four of its frames), and egoblocked's ego shown in another cel of its walk (where it stands and what it recorded match).

The sessions (tests/GAME/*.scr, 81 shots) used to drift from Sierra's for two reasons, both fixed on 2026-10-19 in tests/tools/dosDrive.py:

  • Every screenshot's real cost was added to the script's waits (about 1.5 s over twelve shots). Now each step happens at its time from the interpreter's start (DOSBox logs its switch to a graphics mode), as the host harness's virtual clock does.
  • At 386SX-16 speed Sierra's interpreter spends real seconds drawing and loading, where the harness spends none (Black Cauldron's title put it 1.9 s behind). The oracle's DOSBox is now a fast 386 (50000 cycles/ms). The timing probe matches at that speed on 2.426, 2.435, and 2.439.

What still differs is chance (Manannan's orders, Larry's quiz question, the goose, follow.ego), sprites caught in another cel, Sierra's own save dialogs, and KQ3's opening, which each screenshot hurries along (the screenshot key reaches the game; see "How it was checked").

The sessions of the demos, the Christmas card, and two fan games (28 shots, 2026-10-19): 8 identical; the rest differ in color-cycled logos, the fireplace's flames, other things in motion, and AGI Mouse's pointer in The Lost Chapter (JoeyAGI's own pointer differs).

The eXo and floppy releases' start sessions (2026-10-20/21, scratch pair.sh), each on its own Sierra interpreter: King's Quest 1.0U (2.272), King's Quest 2.0F (2.425), King's Quest II 2.2 (2.426), Space Quest 1.1A (2.272) and 2.2 (2.917), Space Quest 1.0X (2.089, after fix 35), Space Quest II 2.0C, 2.0D, and 2.0F, King's Quest III 2.14, Police Quest 2.0E and 2.0G, Larry on 2.917, Black Cauldron 2.10 (3.002.098), Manhunter 2 3.03, and Gold Rush 3.0 agree with Sierra's but for chance (bullet holes, Larry's quiz question, the goose). Leads:

  • Titles that run ahead on Sierra's: King's Quest III 1.01 reaches its last credits window by 17 s, ours by 30 s, and every Space Quest 1 title is a screen ahead at 12 s. Probably the screenshot key, which reaches Sierra's interpreter and hurries a title that waits for a key (as with KQ3's opening); unconfirmed.
  • Gold Rush: Gold Rush 3.0 agrees through the speed test and Brooklyn Heights, but 6 s after the Return Sierra's Jerrod is further down the path than ours (gr-start's lead, now on a copy both run from the same start).

Sessions marked "JoeyAGI only", which oracle.sh leaves out, are where Sierra's interpreter asks a manual question JoeyAGI skips: Gold Rush 2.01 (the claim jumper), Manhunter 1's original data (the Field Guide), and King's Quest IV's three releases (a word from the manual, picked at random).

Screenshots

screenshots/ (git-ignored: they show Sierra's game) holds Sierra's frame and JoeyAGI's side by side for the probes (probe-*.png) and the KQ3 checkpoints (kq3-*.png), and the ports running KQ3 (port-*.png).