joeyagi/agi.h
2026-10-06 16:59:41 -05:00

591 lines
18 KiB
C

// JoeyAGI: an interpreter for Sierra's Adventure Game Interpreter (AGI) v2
// games, written from the public AGI specifications on JoeyLib.
//
// One game runs at a time, so the interpreter's state is a single AgiStateT
// (gAgi), allocated at start-up with jlAlloc to keep it out of the IIgs's
// bank-0 BSS. The modules:
// agiRes.c resource directories, VOL files, WORDS.TOK, OBJECT
// agiPic.c PICTURE decoding into the picture buffers
// agiView.c VIEW parsing
// agiGfx.c the picture buffers, screen objects' blitting, the stage
// agiObj.c screen objects: cycling, motion, collisions
// agiText.c text: display, windows, status and input lines, menus
// agiInput.c the keyboard, the command line and the parser
// agiSound.c PCjr sound resources on JoeyLib's tone and noise voices
// agiSave.c save, restore and the picture script restore replays
// agiVm.c the LOGIC interpreter
// agi.c start-up and the interpreter cycle
#ifndef AGI_H
#define AGI_H
#include "joey/types.h"
#include "joey/surface.h"
#include <stdio.h>
// ----- Limits and the game's geometry -----
// Resource numbers are one byte; directories may name all 256.
#define AGI_MAX_RESOURCES 256u
// VOL.x files: the directory's volume nibble.
#define AGI_MAX_VOLUMES 16u
// The picture is 160x168 AGI pixels; each is two screen pixels wide, so
// it fills 320x168 of the 320x200 screen, below the status line.
#define AGI_WIDTH 160
#define AGI_HEIGHT 168
#define AGI_PIXELS ((uint16_t)AGI_WIDTH * (uint16_t)AGI_HEIGHT)
#define AGI_MAX_X (AGI_WIDTH - 1)
#define AGI_MAX_Y (AGI_HEIGHT - 1)
#define AGI_SCREEN_PX_PER_AGI 2
// The 40x25 text grid of 8x8 character cells.
#define AGI_TEXT_COLS 40u
#define AGI_TEXT_ROWS 25u
#define AGI_CELL_PX 8
// Colours. The picture starts white with priority 4 (red) everywhere.
#define AGI_COLOR_BLACK 0u
#define AGI_COLOR_RED 4u
#define AGI_COLOR_WHITE 15u
#define AGI_COLOR_COUNT 16u
#define AGI_COLOR_MASK 0x0Fu
#define AGI_PIC_BG_COLOR AGI_COLOR_WHITE
#define AGI_PIC_BG_PRIORITY 4u
// Priority values below 4 are control lines drawn into the priority
// screen: an unconditional barrier, a conditional barrier (blocks), a
// signal (alarm) line and water.
#define AGI_CTL_BARRIER 0u
#define AGI_CTL_BLOCK 1u
#define AGI_CTL_SIGNAL 2u
#define AGI_CTL_WATER 3u
#define AGI_PRIORITY_MIN 4u
#define AGI_PRIORITY_MAX 15u
// Priority bands: y below 48 is priority 4, then a band every 12 lines
// from priority 5 at y 48 to 14 at y 156..167.
#define AGI_PRI_BAND_TOP 48
#define AGI_PRI_BAND_HEIGHT 12
#define AGI_PRI_FIRST_BAND 5u
// The horizon new.room restores.
#define AGI_DEFAULT_HORIZON 36u
// Animated (screen) objects: OBJECT's header says how many a game uses.
#define AGI_MAX_OBJECTS 32u
#define AGI_EGO 0u
// Inventory items and the 40-character strings.
#define AGI_MAX_ITEMS 256u
#define AGI_ITEM_CARRIED 255u
#define AGI_MAX_STRINGS 24u
#define AGI_STRING_LEN 40u
// Variables, flags, controllers (set.key and menu items share one space).
#define AGI_NUM_VARS 256u
#define AGI_NUM_FLAGS 256u
#define AGI_NUM_CONTROLLERS 256u
#define AGI_MAX_KEYMAPS 64u
#define AGI_SCRIPT_MAX 200u
#define AGI_SCRIPT_ARGS 7u
// Menus.
#define AGI_MAX_MENUS 10u
#define AGI_MAX_MENU_ITEMS 48u
#define AGI_MENU_NAME_MAX 20u
#define AGI_MENU_ITEM_MAX 38u
// The command line and the parser.
#define AGI_INPUT_MAX 40u
#define AGI_MAX_WORDS 12u
#define AGI_WORD_MAX_LEN 40u
#define AGI_WORD_IGNORE 0u
#define AGI_WORD_ANY 1u
#define AGI_WORD_REST 9999u
#define AGI_WORD_NONE 0xFFFFu
#define AGI_WORDS_HEADER_BYTES 52u
#define AGI_WORDS_MAX 16384u
// Key codes as AGI sees them: the character, or the PC scan code in the
// high byte. Joystick buttons are 0x0101 and 0x0201.
#define AGI_KEY_NONE 0u
#define AGI_KEY_BACKSPACE 0x0008u
#define AGI_KEY_ENTER 0x000Du
#define AGI_KEY_ESCAPE 0x001Bu
#define AGI_KEY_UP 0x4800u
#define AGI_KEY_DOWN 0x5000u
#define AGI_KEY_LEFT 0x4B00u
#define AGI_KEY_RIGHT 0x4D00u
#define AGI_KEY_JOY_BUTTON_1 0x0101u
#define AGI_KEY_JOY_BUTTON_2 0x0201u
// A menu.input or inventory answer meaning "nothing chosen".
#define AGI_NO_CONTROLLER 0xFFu
// inputGetString with no position: on the input line.
#define AGI_NO_POSITION 0xFFu
// No set.cursor.char yet: the input line shows no cursor.
#define AGI_NO_CURSOR '\0'
// The font (agiFont.c): characters 0x20..0x7E, 8 rows each.
#define AGI_FONT_FIRST 0x20u
#define AGI_FONT_GLYPHS 95u
#define AGI_FONT_ROWS 8u
extern const uint8_t kAgiFont[AGI_FONT_GLYPHS][AGI_FONT_ROWS];
// ----- Reserved variables and flags (AGI specification 3.3 and 3.4) -----
#define VAR_ROOM 0u
#define VAR_PREV_ROOM 1u
#define VAR_EGO_EDGE 2u
#define VAR_SCORE 3u
#define VAR_OBJ_EDGE_OBJ 4u
#define VAR_OBJ_EDGE 5u
#define VAR_EGO_DIR 6u
#define VAR_MAX_SCORE 7u
#define VAR_FREE_PAGES 8u
#define VAR_BAD_WORD 9u
#define VAR_CYCLE_DELAY 10u
#define VAR_SECONDS 11u
#define VAR_MINUTES 12u
#define VAR_HOURS 13u
#define VAR_DAYS 14u
#define VAR_EGO_VIEW 16u
#define VAR_KEY 19u
#define VAR_COMPUTER 20u
#define VAR_WINDOW_TIMER 21u
#define VAR_SOUND_TYPE 22u
#define VAR_INPUT_MAX 24u
#define VAR_SELECTED_ITEM 25u
#define VAR_MONITOR 26u
#define FLAG_EGO_WATER 0u
#define FLAG_ENTERED 2u
#define FLAG_EGO_SIGNAL 3u
#define FLAG_SAID_OK 4u
#define FLAG_NEW_ROOM 5u
#define FLAG_RESTART 6u
#define FLAG_NO_SCRIPT 7u
#define FLAG_SOUND 9u
#define FLAG_FIRST_LOGIC0 11u
#define FLAG_RESTORED 12u
#define FLAG_ITEM_SELECT 13u
#define FLAG_MENU 14u
#define FLAG_WINDOW_STAYS 15u
// ----- Interpreter versions -----
// The Sierra interpreter a game shipped with, as major * 1000 + minor
// (2.435 is 2435), read from the game's AGIDATA.OVL (or AGI) when its
// folder has one; otherwise the version the quirks were measured on.
#define AGI_VERSION_MEASURED 2435u
// Behaviour of particular Sierra interpreter versions that games may lean
// on; agiQuirk says whether the game's version has one (the version ranges
// are agiRes.c's kQuirkVersions).
typedef enum {
// A window's x is a byte and the screen one line of 160-pixel rows:
// a box starting left of the screen starts 256 pixels on instead,
// and every span runs on into the next row.
AGI_QUIRK_WINDOW_BYTE_X = 0,
// A typed character first steps back over the cursor, whether or not
// one was drawn there.
AGI_QUIRK_ECHO_STEPS_BACK = 1,
// A picture line carries its error in a byte, losing steps once it
// passes 255.
AGI_QUIRK_LINE_BYTE_ERROR = 2,
AGI_QUIRK_COUNT = 3
} AgiQuirkE;
// ----- Resources (agiRes.c) -----
typedef enum {
AGI_RES_LOGIC = 0,
AGI_RES_PIC = 1,
AGI_RES_VIEW = 2,
AGI_RES_SOUND = 3,
AGI_RES_COUNT = 4
} AgiResTypeE;
typedef struct {
uint8_t volume;
uint8_t offsetHigh;
uint16_t offsetLow;
} AgiResEntryT;
typedef struct {
FILE *volFiles[AGI_MAX_VOLUMES];
uint16_t resCount[AGI_RES_COUNT];
AgiResEntryT resDir[AGI_RES_COUNT][AGI_MAX_RESOURCES];
uint8_t *words;
uint16_t wordsLen;
uint8_t *objectData;
uint16_t itemCount;
uint8_t maxObjects;
uint16_t interpreterVersion;
} AgiGameT;
// ----- Pictures and views -----
typedef struct {
uint8_t width;
uint8_t height;
uint8_t transparent;
uint8_t mirrorFlags;
const uint8_t *rle;
} AgiCelT;
typedef struct {
uint8_t celCount;
AgiCelT *cels;
} AgiLoopT;
typedef struct {
uint8_t *raw;
uint16_t rawLength;
uint8_t loopCount;
AgiLoopT *loops;
const char *description;
} AgiViewT;
// ----- Screen objects -----
typedef enum {
AGI_CYCLE_NORMAL = 0,
AGI_CYCLE_END_LOOP = 1,
AGI_CYCLE_REVERSE_LOOP = 2,
AGI_CYCLE_REVERSE = 3
} AgiCycleE;
typedef enum {
AGI_MOTION_NORMAL = 0,
AGI_MOTION_WANDER = 1,
AGI_MOTION_FOLLOW = 2,
AGI_MOTION_MOVE = 3
} AgiMotionE;
// Directions: 0 is still, 1..8 run clockwise from north.
#define AGI_DIR_NONE 0u
#define AGI_DIR_N 1u
#define AGI_DIR_NE 2u
#define AGI_DIR_E 3u
#define AGI_DIR_SE 4u
#define AGI_DIR_S 5u
#define AGI_DIR_SW 6u
#define AGI_DIR_W 7u
#define AGI_DIR_NW 8u
#define AGI_DIR_COUNT 9u
// Edges, as v2 and v5 report them.
#define AGI_EDGE_NONE 0u
#define AGI_EDGE_TOP 1u
#define AGI_EDGE_RIGHT 2u
#define AGI_EDGE_BOTTOM 3u
#define AGI_EDGE_LEFT 4u
#define OBJ_ANIMATED 0x0001u
#define OBJ_DRAWN 0x0002u
#define OBJ_UPDATE 0x0004u
#define OBJ_CYCLING 0x0008u
#define OBJ_FIX_LOOP 0x0010u
#define OBJ_FIX_PRIORITY 0x0020u
#define OBJ_IGNORE_HORIZON 0x0040u
#define OBJ_IGNORE_BLOCKS 0x0080u
#define OBJ_IGNORE_OBJS 0x0100u
#define OBJ_ON_WATER 0x0200u
#define OBJ_ON_LAND 0x0400u
// Measured from Sierra's interpreter: an object's first step after the
// game starts or the room changes is lost, as is the first cel change
// after end.of.loop or reverse.loop on an object already drawn.
#define OBJ_SKIP_STEP 0x0800u
#define OBJ_SKIP_CYCLE 0x1000u
typedef struct {
const AgiViewT *view;
uint8_t viewId;
uint8_t loop;
uint8_t cel;
uint8_t width;
uint8_t height;
uint8_t priority;
int16_t x;
int16_t y;
int16_t prevX;
int16_t prevY;
uint16_t flags;
uint8_t direction;
uint8_t stepSize;
uint8_t stepTime;
uint8_t stepCount;
uint8_t cycleTime;
uint8_t cycleCount;
uint8_t cycleMode;
uint8_t motion;
uint8_t loopFlag;
uint8_t motionFlag;
uint8_t targetX;
uint8_t targetY;
uint8_t savedStepSize;
uint8_t randomSteps;
uint8_t followDistance;
// Blit state: where the object was last drawn and the buffer bytes
// its cel covered there, restored when it is erased.
bool blitted;
int16_t blitX;
int16_t blitY;
uint8_t blitW;
uint8_t blitH;
uint8_t *saveUnder;
} AgiObjectT;
// ----- Text, menus, the parser -----
typedef struct {
uint16_t key;
uint8_t controller;
} AgiKeyMapT;
// One recorded picture-building command (the restore script): an AGI
// command number and its argument values (add.to.pic has seven).
typedef struct {
uint8_t op;
uint8_t args[AGI_SCRIPT_ARGS];
} AgiScriptEntryT;
typedef struct {
char text[AGI_MENU_ITEM_MAX + 1u];
uint8_t controller;
bool enabled;
} AgiMenuItemT;
typedef struct {
char name[AGI_MENU_NAME_MAX + 1u];
uint8_t firstItem;
uint8_t itemCount;
uint8_t col;
} AgiMenuT;
// ----- The interpreter state -----
typedef struct {
AgiGameT game;
uint8_t vars[AGI_NUM_VARS];
uint8_t flags[AGI_NUM_FLAGS];
char strings[AGI_MAX_STRINGS][AGI_STRING_LEN + 1u];
uint8_t controllers[AGI_NUM_CONTROLLERS];
AgiKeyMapT keyMaps[AGI_MAX_KEYMAPS];
uint8_t keyMapCount;
// The commands that built the current picture, replayed by restore.
AgiScriptEntryT script[AGI_SCRIPT_MAX];
uint8_t scriptCount;
// Logic resources (loaded on demand) and each one's scan start.
uint8_t *logicRaw[AGI_MAX_RESOURCES];
uint16_t logicLength[AGI_MAX_RESOURCES];
uint16_t logicScanStart[AGI_MAX_RESOURCES];
bool logicLoaded[AGI_MAX_RESOURCES];
uint8_t currentLogic;
bool abortLogic;
bool newRoomPending;
uint8_t newRoomNumber;
// Views loaded with load.view or on demand.
AgiViewT *views[AGI_MAX_RESOURCES];
// Screen objects.
AgiObjectT objects[AGI_MAX_OBJECTS];
uint8_t objectCount;
uint8_t horizon;
bool blockActive;
uint8_t blockX1;
uint8_t blockY1;
uint8_t blockX2;
uint8_t blockY2;
bool playerControl;
// Inventory: the room each item is in.
uint8_t itemRoom[AGI_MAX_ITEMS];
// Screen layout (configure.screen) and text state.
uint8_t picRow;
uint8_t inputRow;
uint8_t statusRow;
bool statusOn;
bool inputOn;
bool textMode;
uint8_t textFg;
uint8_t textBg;
char cursorChar;
bool windowOpen;
// The command line.
char inputLine[AGI_INPUT_MAX + 1u];
uint8_t inputLen;
char lastLine[AGI_INPUT_MAX + 1u];
uint16_t wordIds[AGI_MAX_WORDS];
char wordText[AGI_MAX_WORDS][AGI_WORD_MAX_LEN + 1u];
uint8_t wordCount;
// Menus.
AgiMenuT menus[AGI_MAX_MENUS];
AgiMenuItemT menuItems[AGI_MAX_MENU_ITEMS];
uint8_t menuCount;
uint8_t menuItemCount;
bool menuSubmitted;
bool menuRequested;
uint8_t menuLastMenu;
// Requests that end the cycle, and a restore that just did.
bool quitRequested;
bool restartRequested;
bool restored;
} AgiStateT;
extern AgiStateT *gAgi;
// ----- agiRes.c -----
void agiDecrypt(uint8_t *data, uint16_t length);
const char *agiItemName(const AgiGameT *game, uint8_t item);
uint8_t agiItemStartRoom(const AgiGameT *game, uint8_t item);
bool agiQuirk(AgiQuirkE quirk);
void agiResClose(AgiGameT *game);
uint8_t *agiResLoad(const AgiGameT *game, AgiResTypeE type, uint16_t index, uint16_t *outLength);
bool agiResOpen(AgiGameT *game);
uint16_t agiWordMatch(const AgiGameT *game, const char *text, uint8_t *outLen);
// ----- agiPic.c -----
bool agiPicDecode(uint8_t *visual, uint8_t *priority, const uint8_t *data, uint16_t length);
uint8_t agiPriorityForY(int16_t y);
int16_t agiYForPriority(uint8_t priority);
// ----- agiView.c -----
const AgiCelT *agiViewCel(const AgiViewT *view, uint8_t loop, uint8_t cel, bool *outMirror);
void agiViewDiscardUnused(void);
const AgiViewT *agiViewGet(uint8_t viewId);
// ----- agiGfx.c -----
void gfxAddToPic(uint8_t viewId, uint8_t loop, uint8_t cel, int16_t x, int16_t y, uint8_t priority, uint8_t margin);
uint8_t gfxControlAt(int16_t x, int16_t y);
void gfxDrawPic(uint8_t picId, bool clearFirst);
void gfxDrawStatic(void);
void gfxEraseAll(void);
void gfxEraseUpdating(void);
void gfxFree(void);
bool gfxInit(void);
void gfxPresentDirty(void);
void gfxRedrawAll(void);
void gfxRedrawUpdating(void);
void gfxRestorePicture(void);
void gfxShowObjectCel(uint8_t viewId);
void gfxShowPic(void);
void gfxShakeScreen(uint8_t times);
void gfxShowPriority(void);
// ----- agiObj.c -----
void objAnimate(uint8_t n);
void objClearAll(void);
uint8_t objDirectionTo(int16_t fromX, int16_t fromY, int16_t toX, int16_t toY, uint8_t slack);
void objFixPosition(uint8_t n);
void objMoveTo(uint8_t n, uint8_t x, uint8_t y, uint8_t step, uint8_t flag);
void objPlanMotion(void);
void objSetCel(uint8_t n, uint8_t cel);
void objSetLoop(uint8_t n, uint8_t loop);
bool objSetView(uint8_t n, uint8_t viewId);
void objUnanimateAll(void);
void objUpdateAll(void);
// ----- agiText.c -----
void textClearLines(uint8_t top, uint8_t bottom, uint8_t color);
void textClearRect(uint8_t top, uint8_t left, uint8_t bottom, uint8_t right, uint8_t color);
void textCloseWindow(void);
void textDisplay(uint8_t row, uint8_t col, const char *text);
void textDrawInputLine(void);
void textDrawStatus(void);
void textEchoInput(uint8_t oldLen);
void textExpand(const char *src, char *dst, uint16_t dstCap);
void textGraphicsMode(void);
void textInventory(void);
void textMenuAdd(const char *name);
void textMenuAddItem(const char *name, uint8_t controller);
uint8_t textMenuInput(void);
void textMessageBox(const char *text);
void textPrint(const char *text, int16_t row, int16_t col, uint8_t width);
void textSetAttribute(uint8_t fg, uint8_t bg);
void textShowObject(uint8_t viewId);
void textTextMode(void);
void textUpdateStatus(void);
// ----- agiInput.c -----
bool inputGetNumber(const char *prompt, uint8_t *outValue);
bool inputGetString(char *dst, uint8_t maxLen, uint8_t row, uint8_t col, const char *prompt);
bool inputHaveKey(void);
void inputParse(const char *line);
void inputPoll(void);
void inputProcess(void);
void inputReset(void);
uint16_t inputWaitKey(void);
bool inputWaitPrintClose(uint16_t timeoutHalfSeconds);
// ----- agiSound.c -----
void soundInit(void);
void soundPump(void);
void soundShutdown(void);
void soundStart(uint8_t soundId, uint8_t flag);
void soundStop(void);
// ----- agiSave.c -----
bool restoreGame(void);
void saveGame(void);
void saveScriptAdd(uint8_t op, uint8_t arg);
void saveScriptAddArgs(uint8_t op, const uint8_t *args);
void saveScriptClear(void);
// ----- agiVm.c -----
void vmCallLogic(uint8_t logicId);
void vmDiscardLogics(void);
bool vmLoadLogic(uint8_t logicId);
const char *vmMessage(uint8_t logicId, uint8_t msgId);
void vmReplay(const AgiScriptEntryT *e);
// ----- agi.c -----
void agiIdle(void);
void agiNewRoom(uint8_t room);
void agiObjectStatus(uint8_t n);
void agiQuit(bool immediately);
void agiResetState(void);
void agiRestartGame(void);
#endif