33 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) run as a black box for everything the specifications leave open. No ScummVM, NAGI or Sarien source was read.
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 the two random generators differ.
- 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 checkpasses: host 28/28 (resources, 23 probes, 4 KQ3 sessions), 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 colours, 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 modelled;
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 behaviour 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 |
| 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 | 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"), else the AGI file, else 2.435; 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; 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 |
| Display on planar machines | Picture updates go out as colour runs through jlFillRect (two jlDrawPixel calls per AGI pixel before) | host frames identical either way; ST/Amiga/X68000 snapshots |
Bugs found and fixed in this dive, with causes
- 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). - ST, Amiga, X68000: picture output. Planar stages have no chunky bytes, and the fallback drew two jlDrawPixel calls per AGI pixel. Now runs of one colour 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.)
- 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 colours are used as given; clear.lines and clear.text.rect on the text screen take the colour as a PC attribute byte (so 0..15 clear to black). The old rule (background | 8) was wrong.
- Sound on at start. Sierra's interpreter sets f9 itself (its status line reads "Sound:on" in a game that never touches f9).
- 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.
- 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.
- 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.
- reposition / reposition.to skip the object's step in that cycle.
- 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).
- 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.
- 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.
- Resource memory grew without bound (every logic and view ever loaded stayed); now freed at new.room.
- Games without SNDDIR (or any empty directory) failed to start.
- 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.
- 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.
- shake.screen was a pause. It now shakes as Sierra's does (below).
- print.at at column 0 or 1 drew a clipped window and erased it whole; Sierra's interpreter draws and erases it displaced (below).
- 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).
- set.cursor.char mid-line echoed from the end of the typed text; Sierra's echo keeps its own position (below).
- 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.
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 behaviour 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, so whether the quirks differ between versions could not be measured: all three are on for 2.000 to 2.999 (every version JoeyAGI plays), and narrowing a range is a one-line change once another interpreter is measured. Off, each falls back to the plain behaviour (a clipped window, an echo from the end of the typed text, an exact line).
- 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.
winedgeprobe: nine windows at columns 0 to 3, one to three lines, on a coloured 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 "*").
cursorprobe: 20/20 frames identical. - AGI_QUIRK_LINE_BYTE_ERROR -- picture lines (found by the
winedgepicture): 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).
What remains, most important first
- 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.
- 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.
- 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.
- 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.
- 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).
- Random sequences differ from Sierra's (wander, follow.ego detours, KQ3's Manannan schedule): expected, not a bug.
- Other interpreter versions. Only 2.435 was available to measure; the quirks are assumed for every 2.x interpreter (see below).
- The font is JoeyAGI's own, not the PC's ROM font.
- shake.screen on the IIgs takes 2.75 times as long as on Sierra's (see below); a JoeyLib display offset would fix it.
- 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 behaviour 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 | 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 colour 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; clears the input line | |
| 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 | |
| 0x7E | restore.game | 1 | works: JoeyAGI's own slot window; replays the picture script; FIXED (cycle ends, f12 reaches the next cycle) | |
| 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 |
| # | 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 |
Gates
make check (build every port, then scripts/verify.sh):
| Gate | Result |
|---|---|
host (scripts/test-agi.sh) |
28/28: resources-kq3 (the interpreter version and 472 lines of resource hashes), 23 probes (shake pins every frame of its recording), 4 KQ3 sessions (opening, play, saverestore, teleport) |
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 interpreter in
games/kq3, Xvfb, xdotool): probes 89 of 94 shots identical to Sierra's
(the rest: follow.ego's random detours, and the clock probe's 1-second
window one cycle long, shown on all four of its frames), including the
shake recording and the new winedge (18/18) and cursor (20/20) probes.
Its KQ3 sessions (several shots each) drift from Sierra's with real time
and with the screenshot key reaching the game (see "How it was checked"),
which is why the KQ3 checkpoints above were taken one screenshot a run.
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).