Updates to match changes to JoeyLib. Some AGI fixes as well.

This commit is contained in:
Scott Duensing 2026-10-14 21:18:34 -05:00
parent 45a82bef2f
commit f9ef3e99c2
12 changed files with 111 additions and 84 deletions

View file

@ -4,12 +4,12 @@ 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,
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
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):
@ -72,7 +72,7 @@ $ scripts/verify.sh host # or name the ones to run
| Gate | What it proves |
|------|----------------|
| `host` | The whole interpreter, built for the build machine on JoeyLib's headless port, plays scripted sessions and every frame asked for must hash as expected: every resource of every game in `games/` loads and decodes (KQ3's pictures hash as Sierra's interpreter draws them); twenty probe games built from scratch by `tests/probes.py` (pictures, windows, motion, control lines, priority, the parser, menus, inventory, sound timing, ...); and, with `games/kq3`, four KQ3 sessions (the opening, play, save/restore/restart, debug teleports) (`scripts/test-agi.sh`) |
| `disk` | `AGI.2mg` with King's Quest III boots on a IIgs, launches the interpreter and draws |
| `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

3
agi.c
View file

@ -92,9 +92,6 @@ static void changeRoom(void) {
gAgi->vars[VAR_BAD_WORD] = 0u;
ego = &gAgi->objects[AGI_EGO];
gAgi->vars[VAR_EGO_VIEW] = ego->viewId;
ego->direction = AGI_DIR_NONE;
ego->motion = AGI_MOTION_NORMAL;
gAgi->vars[VAR_EGO_DIR] = AGI_DIR_NONE;
switch (gAgi->vars[VAR_EGO_EDGE]) {
case AGI_EDGE_TOP:
ego->y = AGI_MAX_Y;

3
agi.h
View file

@ -495,10 +495,11 @@ void gfxPresentDirty(void);
void gfxRedrawAll(void);
void gfxRedrawUpdating(void);
void gfxRestorePicture(void);
void gfxShakeScreen(uint8_t times);
void gfxShowObjectCel(uint8_t viewId);
void gfxShowPic(void);
void gfxShakeScreen(uint8_t times);
void gfxShowPriority(void);
void gfxStampObject(uint8_t n);
// ----- agiObj.c -----

View file

@ -49,7 +49,7 @@
#define SHAKE_BEATS_PER_UNIT 4u
#define MS_PER_SECOND 1000u
// A chunky surface's byte holds two pixels.
#define CHUNKY_PX_PER_BYTE (SURFACE_WIDTH / SURFACE_BYTES_PER_ROW)
#define CHUNKY_PX_PER_BYTE (JL_SURFACE_WIDTH / JL_SURFACE_BYTES_PER_ROW)
typedef struct {
@ -150,14 +150,14 @@ static void buildShifted(jlSurfaceT *shifted, const jlSurfaceT *normal) {
jlSurfaceCopy(shifted, normal);
jlSurfaceClear(shifted, AGI_COLOR_BLACK);
if (shifted->pixels != NULL && normal->pixels != NULL) {
for (y = SHAKE_DY; y < SURFACE_HEIGHT; y++) {
memcpy(&shifted->pixels[y * SURFACE_BYTES_PER_ROW + SHAKE_DX / CHUNKY_PX_PER_BYTE],
&normal->pixels[(uint16_t)(y - SHAKE_DY) * SURFACE_BYTES_PER_ROW],
SURFACE_BYTES_PER_ROW - SHAKE_DX / CHUNKY_PX_PER_BYTE);
for (y = SHAKE_DY; y < JL_SURFACE_HEIGHT; y++) {
memcpy(&shifted->pixels[y * JL_SURFACE_BYTES_PER_ROW + SHAKE_DX / CHUNKY_PX_PER_BYTE],
&normal->pixels[(uint16_t)(y - SHAKE_DY) * JL_SURFACE_BYTES_PER_ROW],
JL_SURFACE_BYTES_PER_ROW - SHAKE_DX / CHUNKY_PX_PER_BYTE);
}
return;
}
sprite = jlSpriteCreateFromSurface(normal, 0, 0, TILE_BLOCKS_PER_ROW, TILE_BLOCKS_PER_COL);
sprite = jlSpriteCreateFromSurface(normal, 0, 0, JL_TILE_BLOCKS_PER_ROW, JL_TILE_BLOCKS_PER_COL);
if (sprite != NULL) {
jlSpriteDraw(shifted, sprite, SHAKE_DX, SHAKE_DY);
jlSpriteDestroy(sprite);
@ -332,7 +332,7 @@ static void pushRect(const GfxRectT *r) {
}
for (y = y0; y < y1; y++) {
const uint8_t *src = &gVis[(uint16_t)y * AGI_WIDTH];
uint8_t *dst = &stage->pixels[(uint16_t)(screenY + y) * SURFACE_BYTES_PER_ROW];
uint8_t *dst = &stage->pixels[(uint16_t)(screenY + y) * JL_SURFACE_BYTES_PER_ROW];
for (x = x0; x < x1; x++) {
c = src[x];
@ -694,3 +694,28 @@ void gfxShowPriority(void) {
(void)inputWaitKey();
gfxRestorePicture();
}
// draw on an object never animated (measured on Sierra's interpreter): its
// cel goes into the picture once, at its baseline's priority, with the
// objects off. It is in neither blit list, so nothing erases it and it
// never moves; animate.obj leaves the image where it is.
void gfxStampObject(uint8_t n) {
AgiObjectT *o;
const AgiCelT *cel;
bool mirror;
int16_t top;
o = &gAgi->objects[n];
cel = agiViewCel(o->view, o->loop, o->cel, &mirror);
if (cel == NULL || cel->width == 0u || cel->height == 0u) {
return;
}
if ((o->flags & OBJ_FIX_PRIORITY) == 0u) {
o->priority = agiPriorityForY(o->y);
}
gfxEraseAll();
top = (int16_t)(o->y - (int16_t)cel->height + 1);
drawCel(cel, mirror, o->x, top, o->priority, false);
markDirty(o->x, top, cel->width, cel->height);
}

View file

@ -59,7 +59,7 @@ static const uint8_t kLetterScan[LETTER_COUNT] = {
};
static const jlKeyE kFunctionKeys[FKEY_COUNT] = {
KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6, KEY_F7, KEY_F8, KEY_F9, KEY_F10
JL_KEY_F1, JL_KEY_F2, JL_KEY_F3, JL_KEY_F4, JL_KEY_F5, JL_KEY_F6, JL_KEY_F7, JL_KEY_F8, JL_KEY_F9, JL_KEY_F10
};
@ -173,7 +173,7 @@ static void handleKey(uint16_t key) {
// The joystick, read as one of the eight directions.
static uint8_t joystickDirection(void) {
return objDirectionTo(0, 0, jlJoystickX(JOYSTICK_0), jlJoystickY(JOYSTICK_0), JOY_DEADZONE);
return objDirectionTo(0, 0, jlJoystickX(JL_JOYSTICK_0), jlJoystickY(JL_JOYSTICK_0), JOY_DEADZONE);
}
@ -372,8 +372,8 @@ void inputPoll(void) {
uint8_t dir;
jlInputPoll();
alt = jlKeyDown(KEY_LALT);
ctrl = jlKeyDown(KEY_LCTRL);
alt = jlKeyDown(JL_KEY_LALT);
ctrl = jlKeyDown(JL_KEY_LCTRL);
// A Ctrl or Alt chord is a command, not typing, whatever the port
// delivers as a character.
while ((ch = jlInputGetChar()) >= 0) {
@ -381,8 +381,8 @@ void inputPoll(void) {
enqueue((uint16_t)ch);
}
}
for (i = 0u; i < (uint8_t)(KEY_Z - KEY_A + 1); i++) {
if (jlKeyPressed((jlKeyE)(KEY_A + i))) {
for (i = 0u; i < (uint8_t)(JL_KEY_Z - JL_KEY_A + 1); i++) {
if (jlKeyPressed((jlKeyE)(JL_KEY_A + i))) {
if (alt) {
enqueue((uint16_t)((uint16_t)kLetterScan[i] << SCAN_SHIFT));
} else if (ctrl) {
@ -395,23 +395,23 @@ void inputPoll(void) {
enqueue((uint16_t)((uint16_t)(FKEY_SCAN_FIRST + i) << SCAN_SHIFT));
}
}
if (jlKeyPressed(KEY_UP)) {
if (jlKeyPressed(JL_KEY_UP)) {
enqueue(AGI_KEY_UP);
}
if (jlKeyPressed(KEY_DOWN)) {
if (jlKeyPressed(JL_KEY_DOWN)) {
enqueue(AGI_KEY_DOWN);
}
if (jlKeyPressed(KEY_LEFT)) {
if (jlKeyPressed(JL_KEY_LEFT)) {
enqueue(AGI_KEY_LEFT);
}
if (jlKeyPressed(KEY_RIGHT)) {
if (jlKeyPressed(JL_KEY_RIGHT)) {
enqueue(AGI_KEY_RIGHT);
}
if (jlJoystickConnected(JOYSTICK_0)) {
if (jlJoyPressed(JOYSTICK_0, JOY_BUTTON_0)) {
if (jlJoystickConnected(JL_JOYSTICK_0)) {
if (jlJoyPressed(JL_JOYSTICK_0, JL_JOY_BUTTON_0)) {
enqueue(AGI_KEY_JOY_BUTTON_1);
}
if (jlJoyPressed(JOYSTICK_0, JOY_BUTTON_1)) {
if (jlJoyPressed(JL_JOYSTICK_0, JL_JOY_BUTTON_1)) {
enqueue(AGI_KEY_JOY_BUTTON_2);
}
dir = joystickDirection();

View file

@ -543,7 +543,8 @@ bool objSetView(uint8_t n, uint8_t viewId) {
// unanimate.all (and new.room): every object off the screen and out of
// the interpreter's hands, with the default step and cycle settings. Its
// view, loop, cel and position stay (new.room places ego from them).
// view, loop, cel, position and direction stay (new.room places ego from
// them; measured: ego keeps walking the way it went once animated again).
void objUnanimateAll(void) {
uint8_t i;
AgiObjectT *o;
@ -552,7 +553,6 @@ void objUnanimateAll(void) {
for (i = 0u; i < AGI_MAX_OBJECTS; i++) {
o = &gAgi->objects[i];
o->flags = OBJ_SKIP_STEP;
o->direction = AGI_DIR_NONE;
o->motion = AGI_MOTION_NORMAL;
o->cycleMode = AGI_CYCLE_NORMAL;
o->stepSize = STEP_DEFAULT;

View file

@ -142,8 +142,8 @@ static void silence(void) {
// ----- Public API (alphabetical) -----
// jlInit has started audio.
void soundInit(void) {
(void)jlAudioInit();
silence();
}
@ -218,9 +218,9 @@ void soundPump(void) {
}
// jlShutdown stops audio.
void soundShutdown(void) {
release();
jlAudioShutdown();
}

View file

@ -182,7 +182,7 @@ static void copySpan(jlSurfaceT *dst, const jlSurfaceT *src, int16_t col, int16_
x = (int16_t)(col * AGI_SCREEN_PX_PER_AGI);
xEnd = (int16_t)(x + run * AGI_SCREEN_PX_PER_AGI);
if (dst->pixels != NULL && src->pixels != NULL) {
at = (uint16_t)((uint16_t)row * SURFACE_BYTES_PER_ROW + (uint16_t)col);
at = (uint16_t)((uint16_t)row * JL_SURFACE_BYTES_PER_ROW + (uint16_t)col);
memcpy(&dst->pixels[at], &src->pixels[at], (size_t)run);
surfaceMarkDirtyRect(dst, x, row, (int16_t)(xEnd - x), 1);
return;

View file

@ -480,10 +480,12 @@ static void cmdObject(VmFrameT *f, uint8_t op, const uint8_t *a) {
break;
case CMD_DRAW:
if (o != NULL && (o->flags & OBJ_DRAWN) == 0u && o->view != NULL) {
o->flags = (uint16_t)(o->flags | OBJ_ANIMATED);
objFixPosition(a[0]);
o->prevX = o->x;
o->prevY = o->y;
if ((o->flags & OBJ_ANIMATED) == 0u) {
gfxStampObject(a[0]);
}
redrawObjects(a[0], (uint16_t)(OBJ_DRAWN | OBJ_UPDATE), 0u);
}
break;

View file

@ -0,0 +1 @@
a 101c0a8c

View file

@ -183,34 +183,34 @@ static void hostWritePpm(const char *path);
// ----- Module state -----
static const HostKeyNameT kKeyNames[] = {
{ "Return", KEY_RETURN, JL_CHAR_RETURN },
{ "Escape", KEY_ESCAPE, JL_CHAR_ESCAPE },
{ "Tab", KEY_TAB, JL_CHAR_TAB },
{ "BackSpace", KEY_BACKSPACE, JL_CHAR_BACKSPACE },
{ "space", KEY_SPACE, ' ' },
{ "Up", KEY_UP, 0u },
{ "Down", KEY_DOWN, 0u },
{ "Left", KEY_LEFT, 0u },
{ "Right", KEY_RIGHT, 0u },
{ "F1", KEY_F1, 0u },
{ "F2", KEY_F2, 0u },
{ "F3", KEY_F3, 0u },
{ "F4", KEY_F4, 0u },
{ "F5", KEY_F5, 0u },
{ "F6", KEY_F6, 0u },
{ "F7", KEY_F7, 0u },
{ "F8", KEY_F8, 0u },
{ "F9", KEY_F9, 0u },
{ "F10", KEY_F10, 0u },
{ "ctrl", KEY_LCTRL, 0u },
{ "alt", KEY_LALT, 0u },
{ "shift", KEY_LSHIFT, 0u },
{ "period", KEY_NONE, '.' },
{ "comma", KEY_NONE, ',' },
{ "apostrophe", KEY_NONE, '\'' },
{ "minus", KEY_NONE, '-' },
{ "question", KEY_NONE, '?' },
{ "exclam", KEY_NONE, '!' }
{ "Return", JL_KEY_RETURN, JL_CHAR_RETURN },
{ "Escape", JL_KEY_ESCAPE, JL_CHAR_ESCAPE },
{ "Tab", JL_KEY_TAB, JL_CHAR_TAB },
{ "BackSpace", JL_KEY_BACKSPACE, JL_CHAR_BACKSPACE },
{ "space", JL_KEY_SPACE, ' ' },
{ "Up", JL_KEY_UP, 0u },
{ "Down", JL_KEY_DOWN, 0u },
{ "Left", JL_KEY_LEFT, 0u },
{ "Right", JL_KEY_RIGHT, 0u },
{ "F1", JL_KEY_F1, 0u },
{ "F2", JL_KEY_F2, 0u },
{ "F3", JL_KEY_F3, 0u },
{ "F4", JL_KEY_F4, 0u },
{ "F5", JL_KEY_F5, 0u },
{ "F6", JL_KEY_F6, 0u },
{ "F7", JL_KEY_F7, 0u },
{ "F8", JL_KEY_F8, 0u },
{ "F9", JL_KEY_F9, 0u },
{ "F10", JL_KEY_F10, 0u },
{ "ctrl", JL_KEY_LCTRL, 0u },
{ "alt", JL_KEY_LALT, 0u },
{ "shift", JL_KEY_LSHIFT, 0u },
{ "period", JL_KEY_NONE, '.' },
{ "comma", JL_KEY_NONE, ',' },
{ "apostrophe", JL_KEY_NONE, '\'' },
{ "minus", JL_KEY_NONE, '-' },
{ "question", JL_KEY_NONE, '?' },
{ "exclam", JL_KEY_NONE, '!' }
};
// The comparison operators of a condition, in HostOpE order.
@ -220,9 +220,9 @@ static FILE *gScript;
static char gOutDir[HOST_PATH_MAX];
static uint32_t gFrame;
static uint32_t gResumeFrame;
static uint32_t gHoldUntil[KEY_COUNT];
static bool gHeld[KEY_COUNT];
static uint8_t gDisplay[SURFACE_HEIGHT][SURFACE_WIDTH][3];
static uint32_t gHoldUntil[JL_KEY_COUNT];
static bool gHeld[JL_KEY_COUNT];
static uint8_t gDisplay[JL_SURFACE_HEIGHT][JL_SURFACE_WIDTH][3];
// Characters still to type for the current 'type' line, one per press.
static char gTypeBuf[HOST_LINE_MAX];
static long gLinePos;
@ -282,7 +282,7 @@ static uint16_t gGotoCount;
static bool hostClearStep(void) {
if (gAgi != NULL && gAgi->windowOpen && gClearTries < HOST_CLEAR_TRIES) {
gClearTries++;
hostPress(KEY_RETURN, JL_CHAR_RETURN, HOST_KEY_HOLD_FRAMES, HOST_KEY_GAP_FRAMES);
hostPress(JL_KEY_RETURN, JL_CHAR_RETURN, HOST_KEY_HOLD_FRAMES, HOST_KEY_GAP_FRAMES);
gResumeFrame = gFrame + HOST_CLEAR_GAP_FRAMES;
return false;
}
@ -417,13 +417,13 @@ static void hostGoto(const char *label) {
static jlKeyE hostEgoKey(uint8_t dir) {
switch (dir) {
case AGI_DIR_E:
return KEY_RIGHT;
return JL_KEY_RIGHT;
case AGI_DIR_S:
return KEY_DOWN;
return JL_KEY_DOWN;
case AGI_DIR_W:
return KEY_LEFT;
return JL_KEY_LEFT;
default:
return KEY_UP;
return JL_KEY_UP;
}
}
@ -470,32 +470,32 @@ static bool hostKeyLookup(const char *name, jlKeyE *outKey, uint8_t *outCh) {
}
c = name[0];
if (c >= 'a' && c <= 'z') {
*outKey = (jlKeyE)(KEY_A + (c - 'a'));
*outKey = (jlKeyE)(JL_KEY_A + (c - 'a'));
*outCh = (uint8_t)c;
return true;
}
if (c >= 'A' && c <= 'Z') {
*outKey = (jlKeyE)(KEY_A + (c - 'A'));
*outKey = (jlKeyE)(JL_KEY_A + (c - 'A'));
*outCh = (uint8_t)c;
return true;
}
if (c >= '0' && c <= '9') {
*outKey = (jlKeyE)(KEY_0 + (c - '0'));
*outKey = (jlKeyE)(JL_KEY_0 + (c - '0'));
*outCh = (uint8_t)c;
return true;
}
*outKey = KEY_NONE;
*outKey = JL_KEY_NONE;
*outCh = (uint8_t)c;
return true;
}
static void hostPress(jlKeyE key, uint8_t ch, uint32_t holdFrames, uint32_t gapFrames) {
if (key != KEY_NONE) {
if (key != JL_KEY_NONE) {
gHoldUntil[key] = gFrame + holdFrames;
}
// Like the real keyboards, a Ctrl or Alt chord types no character.
if (ch != 0u && !gHeld[KEY_LALT] && !gHeld[KEY_LCTRL]) {
if (ch != 0u && !gHeld[JL_KEY_LALT] && !gHeld[JL_KEY_LCTRL]) {
jlInputCharPush(ch);
}
gResumeFrame = gFrame + holdFrames + gapFrames;
@ -635,14 +635,14 @@ static void hostStep(void) {
one[1] = '\0';
gTypePos++;
if (one[0] == ' ') {
hostPress(KEY_SPACE, ' ', HOST_KEY_HOLD_FRAMES, HOST_KEY_GAP_FRAMES);
hostPress(JL_KEY_SPACE, ' ', HOST_KEY_HOLD_FRAMES, HOST_KEY_GAP_FRAMES);
} else if (hostKeyLookup(one, &key, &ch)) {
hostPress(key, ch, HOST_KEY_HOLD_FRAMES, HOST_KEY_GAP_FRAMES);
}
continue;
}
if (gSay == HOST_SAY_TYPING) {
hostPress(KEY_RETURN, JL_CHAR_RETURN, HOST_KEY_HOLD_FRAMES, HOST_KEY_GAP_FRAMES);
hostPress(JL_KEY_RETURN, JL_CHAR_RETURN, HOST_KEY_HOLD_FRAMES, HOST_KEY_GAP_FRAMES);
gSay = HOST_SAY_SETTLE;
continue;
}
@ -708,7 +708,7 @@ static void hostStep(void) {
}
hostPress(key, ch, (hold > 0.0) ? (uint32_t)(hold * HOST_FRAME_HZ + 0.5) : HOST_KEY_HOLD_FRAMES, HOST_KEY_GAP_FRAMES);
} else if (strcmp(op, "down") == 0 || strcmp(op, "up") == 0) {
if (!hostKeyLookup(arg, &key, &ch) || key == KEY_NONE) {
if (!hostKeyLookup(arg, &key, &ch) || key == JL_KEY_NONE) {
fprintf(stderr, "agiHost: unknown key '%s'\n", arg);
exit(2);
}
@ -895,7 +895,7 @@ static bool hostWaitStep(void) {
hostWritePpm(path);
}
gWindowSeen = false;
hostPress(KEY_RETURN, JL_CHAR_RETURN, HOST_KEY_HOLD_FRAMES, HOST_KEY_GAP_FRAMES);
hostPress(JL_KEY_RETURN, JL_CHAR_RETURN, HOST_KEY_HOLD_FRAMES, HOST_KEY_GAP_FRAMES);
}
return false;
}
@ -1046,7 +1046,7 @@ static void hostWritePpm(const char *path) {
fprintf(stderr, "agiHost: cannot write %s\n", path);
return;
}
fprintf(fp, "P6\n%d %d\n%u\n", SURFACE_WIDTH, SURFACE_HEIGHT, HOST_CHANNEL_MAX);
fprintf(fp, "P6\n%d %d\n%u\n", JL_SURFACE_WIDTH, JL_SURFACE_HEIGHT, HOST_CHANNEL_MAX);
fwrite(gDisplay, 1, sizeof(gDisplay), fp);
fclose(fp);
}
@ -1077,7 +1077,7 @@ void jlpInputInit(void) {
void jlpInputPoll(void) {
uint16_t k;
for (k = 0u; k < (uint16_t)KEY_COUNT; k++) {
for (k = 0u; k < (uint16_t)JL_KEY_COUNT; k++) {
gKeyState[k] = (uint8_t)((gHeld[k] || gFrame < gHoldUntil[k]) ? 1u : 0u);
}
}
@ -1100,10 +1100,10 @@ void jlpPresent(const jlSurfaceT *src) {
uint8_t nib;
uint16_t rgb;
for (y = 0u; y < SURFACE_HEIGHT; y++) {
for (y = 0u; y < JL_SURFACE_HEIGHT; y++) {
pal = src->palette[src->scb[y]];
for (x = 0u; x < SURFACE_WIDTH; x++) {
byte = src->pixels[(uint32_t)y * SURFACE_BYTES_PER_ROW + x / HOST_PIXELS_PER_BYTE];
for (x = 0u; x < JL_SURFACE_WIDTH; x++) {
byte = src->pixels[(uint32_t)y * JL_SURFACE_BYTES_PER_ROW + x / HOST_PIXELS_PER_BYTE];
nib = (uint8_t)((x & 1u) ? (byte & 0x0Fu) : (byte >> 4));
rgb = pal[nib];
gDisplay[y][x][0] = (uint8_t)(((rgb >> 8) & 0x0Fu) * HOST_NIBBLE_SCALE);

View file

@ -188,6 +188,7 @@ int main(void) {
bool ok;
memset(&cfg, 0, sizeof(cfg));
cfg.noAudio = true;
if (!jlInit(&cfg)) {
fprintf(stderr, "jlInit failed\n");
return 1;