joeylib2/examples/agi/agi.h

562 lines
22 KiB
C

// JoeyLib AGI interpreter (Sierra Adventure Game Interpreter, v2).
//
// Reimplements the engine that ran Sierra's first wave of graphic
// adventures (KQ1-3, SQ1-2, PQ1, LSL1, Manhunter 1-2) on top of
// JoeyLib's surface/draw/input APIs. v2 format only for Phase 0; v3
// (LZW-compressed) lands in a later phase.
//
// This is a from-scratch implementation written against the public
// AGI format specification, not derived from any GPL'd reference.
#ifndef AGI_H
#define AGI_H
#include "joey/types.h"
#include "joey/surface.h"
#include <stdio.h>
// AGI resource directories are sized by the on-disk file length. The
// maximum index any single game references is well under 256 for the
// supported game set; oversize directories are rejected at load.
#define AGI_MAX_RESOURCES 256
// AGI VOL.x files are numbered 0..15 (4-bit field in the directory
// entry). One open FILE * per volume kept resident for the life of
// the game session.
#define AGI_MAX_VOLUMES 16
// AGI's native rendering surface. Visual and priority screens are
// each 160 wide; horizontal pixel doubling produces the 320-wide
// display. The vertical 168 leaves room above and below for the
// status line and command prompt in a 200-line display.
#define AGI_PIC_WIDTH 160
#define AGI_PIC_HEIGHT 168
#define AGI_PIC_PIXELS (AGI_PIC_WIDTH * AGI_PIC_HEIGHT)
// Flood-fill spread tests: a fill spreads through pixels matching the
// background color and stops at anything else. Visual default bg is
// 15 (white), priority default bg is 4 (red).
#define AGI_VISUAL_BG 15u
#define AGI_PRIORITY_BG 4u
// AGI 16-color CGA-derived palette in JoeyLib's $0RGB format (4-bit
// per channel). Entry 6 keeps the classic CGA "brown" tweak (R=A,
// G=5, B=0) so the muddy-yellow-on-CGA-monitors look matches the
// original render rather than reading as pure yellow-green.
extern const uint16_t kAgiPalette[16];
// Resource directory entry: which VOL.x file holds the resource and
// where in that file it starts. A 0xFF marker in volume means the
// directory slot is empty (game does not define this resource ID).
typedef struct {
uint8_t volume;
uint32_t offset;
} AgiResEntryT;
typedef enum {
AGI_RES_LOGIC = 0,
AGI_RES_PIC = 1,
AGI_RES_VIEW = 2,
AGI_RES_SOUND = 3,
AGI_RES_COUNT = 4
} AgiResTypeE;
// Open-game state: directory metadata + handles to every VOL.x file.
// Loading and freeing individual resource blobs is per-call so the
// working-set footprint stays small enough for stock IIgs / Amiga.
typedef struct {
FILE *volFiles[AGI_MAX_VOLUMES];
uint16_t resCount[AGI_RES_COUNT];
AgiResEntryT resDir[AGI_RES_COUNT][AGI_MAX_RESOURCES];
} AgiGameT;
// Decoded PIC working buffers. Both planes are 160x168 chunky 8bpp;
// only the low nibble (4 bits) of each byte carries actual color
// information. Buffers are heap-allocated so the binary's BSS
// footprint stays under what the IIgs stock heap can satisfy in
// one shot.
typedef struct {
uint8_t *visual;
uint8_t *priority;
} AgiPicT;
// VIEW resources hold one or more animation loops; each loop is a
// sequence of cels (frames). The parser walks the published format
// once and records pointers into the raw resource buffer so per-cel
// access is O(1) without re-parsing.
#define AGI_MAX_LOOPS_PER_VIEW 16u
#define AGI_MAX_CELS_PER_LOOP 32u
typedef struct {
uint8_t width;
uint8_t height;
uint8_t transparentColor;
uint8_t mirrored;
uint8_t mirrorSourceLoop;
const uint8_t *rleData;
} AgiCelInfoT;
typedef struct {
uint8_t celCount;
AgiCelInfoT cels[AGI_MAX_CELS_PER_LOOP];
} AgiLoopInfoT;
typedef struct {
uint8_t *raw;
uint16_t rawLength;
uint8_t loopCount;
AgiLoopInfoT loops[AGI_MAX_LOOPS_PER_VIEW];
} AgiViewT;
// AGI priority for an actor based on its feet (bottom-edge) Y.
// 0..47 is the sky band (priority 4); 48..167 is ground divided into
// 10 bands at priority 5..14 (12 pixels each). Priority 15 is "always
// on top" and isn't reached by the actor's natural Y.
uint8_t agiActorPriorityForY(int16_t y);
// LOGIC VM state. AGI's interpreter is a stack-of-logics bytecode
// VM with 256 byte-sized variables and 256 single-bit flags.
#define AGI_VM_NUM_VARS 256u
#define AGI_VM_NUM_FLAGS 256u
typedef enum {
AGI_VM_HALT_NONE = 0,
AGI_VM_HALT_RETURN = 1,
AGI_VM_HALT_UNKNOWN_OP = 2,
AGI_VM_HALT_TRUNCATED = 3,
AGI_VM_HALT_NEW_ROOM = 4,
AGI_VM_HALT_NO_LOGIC = 5,
AGI_VM_HALT_STACK_OVER = 6,
AGI_VM_HALT_PRINT_PENDING = 7, // print() popup waiting for ack
AGI_VM_HALT_QUIT = 8 // game logic asked to exit
} AgiVmHaltE;
#define AGI_VM_CALL_STACK_MAX 8u
typedef struct {
const uint8_t *code;
uint16_t codeLength;
uint16_t pc;
uint8_t logicId;
} AgiVmFrameT;
// ----- Animated objects -----
//
// AGI v2 supports up to 16 simultaneous animated objects ("screen
// objects"). Object 0 is the player/ego; the rest are NPCs, props,
// and incidental animations driven by the game logic. The state
// here mirrors the per-object fields the game scripts manipulate
// via animate.obj / set.view / set.cel / position / set.dir / etc.
#define AGI_MAX_OBJECTS 16u
#define AGI_OBJ_FLAG_ANIMATED 0x01u // animate.obj called
#define AGI_OBJ_FLAG_DRAWN 0x02u // draw() called, sprite is on-screen
#define AGI_OBJ_FLAG_CYCLING 0x04u // start.cycling, advances cel
#define AGI_OBJ_FLAG_UPDATING 0x08u // start.update, render allowed
#define AGI_OBJ_FLAG_LOOP_FIXED 0x10u // fix.loop disables auto-loop choice
#define AGI_OBJ_FLAG_PRI_FIXED 0x20u // set.priority overrides Y-band priority
#define AGI_OBJ_FLAG_HORIZON_IGN 0x40u // ignore.horizon
#define AGI_OBJ_FLAG_OBJS_IGN 0x80u // ignore.objs (collision)
typedef enum {
AGI_CYCLE_NORMAL = 0, // 0->1->2..->last->0->...
AGI_CYCLE_END_LOOP = 1, // play once forward, stop, set flag
AGI_CYCLE_REVERSE = 2, // last->..->1->0->last->...
AGI_CYCLE_REV_LOOP = 3 // play once backward, stop, set flag
} AgiCycleE;
typedef enum {
AGI_MOTION_NORMAL = 0, // moves on direction (vN+6 for ego)
AGI_MOTION_WANDER = 1, // random direction every few cycles
AGI_MOTION_FOLLOW_EGO = 2, // chases obj 0
AGI_MOTION_MOVE_OBJ = 3 // moving toward fixed (x,y)
} AgiMotionE;
typedef struct {
uint8_t viewId;
uint8_t loop;
uint8_t cel;
uint8_t priority; // 4..15; high nibble masking
int16_t x; // baseline (bottom-left) AGI x
int16_t y; // baseline (bottom) AGI y
int16_t prevX;
int16_t prevY;
uint8_t prevLoop;
uint8_t prevCel;
uint8_t prevView;
uint8_t direction; // 0=stop, 1..8 (up=1, clockwise)
uint8_t stepSize; // pixels per step
uint8_t stepTime; // VM cycles between steps
uint8_t stepTick; // counter toward stepTime
uint8_t cycleTime; // VM cycles between cel advances
uint8_t cycleTick; // counter toward cycleTime
AgiCycleE cycleMode;
uint8_t endOfLoopFlag; // flag id to set when end.of.loop fires
AgiMotionE motionType;
int16_t targetX; // for move.obj
int16_t targetY;
uint8_t moveStepBackup; // step size before move.obj started
uint8_t moveDoneFlag; // flag id set on move.obj completion
uint8_t wanderTick; // wander direction-change countdown
uint8_t followStep; // step.size override for follow.ego
uint8_t followDoneFlag;
uint8_t flags; // AGI_OBJ_FLAG_*
} AgiObjectT;
// ----- Text plane / status line -----
//
// AGI's text plane is 40 columns x 25 rows of 8x8 character cells.
// Status line (row 0) is reserved for game-managed status text;
// display() writes anywhere by row/col; print() pops a modal window.
#define AGI_TEXT_COLS 40u
#define AGI_TEXT_ROWS 25u
typedef struct {
char text[AGI_TEXT_COLS + 1u]; // NUL-terminated
uint8_t fg;
uint8_t bg;
} AgiTextLineT;
#define AGI_PRINT_MAX_LEN 240u
typedef struct {
bool active; // print modal on screen
uint16_t length;
char message[AGI_PRINT_MAX_LEN + 1u];
} AgiPrintModalT;
// ----- Controllers -----
//
// set.key binds a (key1, key2) pair to a controller id 0..49. When
// the host dispatches a key matching a binding, the controller's
// "fired this cycle" bit is set. The IF test controller(N) consumes
// the bit so the binding only fires once per press.
#define AGI_MAX_CONTROLLERS 50u
#define AGI_MAX_KEY_BINDINGS 64u
typedef struct {
uint8_t key1; // ASCII or extended scancode (low byte)
uint8_t key2; // extended high byte (0 for plain ASCII)
uint8_t controller;
uint8_t active;
} AgiKeyBindingT;
// ----- Menus -----
//
// set.menu / set.menu.item populate a 2D menu from message ids; the
// game can later disable individual items. menu.input pops it up;
// the user's choice fires the corresponding controller. Stored as
// state so menu.input can render and return a controller id.
#define AGI_MAX_MENUS 16u
#define AGI_MAX_MENU_ITEMS 16u
typedef struct {
uint8_t messageId; // logic 0 message id
uint8_t controller; // fired when item selected
uint8_t enabled;
} AgiMenuItemT;
typedef struct {
uint8_t nameMsgId;
uint8_t itemCount;
AgiMenuItemT items[AGI_MAX_MENU_ITEMS];
} AgiMenuT;
// ----- Strings -----
//
// set.string / get.string / word.to.string maintain a small pool of
// up to 24 game-defined strings. Each is 40 chars max (one screen row).
#define AGI_MAX_STRINGS 24u
#define AGI_STRING_LEN 40u
// Host callbacks invoked from the VM for side-effecting opcodes the
// host owns. The host owns the logic-resource cache and screen
// surfaces; the VM never touches either directly. fetchLogic returns
// the post-header bytecode pointer and its length (the 2-byte LE
// logic header has already been consumed by the host). NULL on
// missing logic causes the VM to halt with AGI_VM_HALT_NO_LOGIC.
//
// haveKey returns true if a keystroke is buffered, consuming it so
// the next call returns false until another key arrives. Used by
// the AGI test command have.key.
//
// fetchMessage returns the *decrypted* bytes of message `msgId`
// from logic `logicId`, or NULL if the message is missing. The
// returned pointer is valid until the next fetchMessage call (host
// may use a single shared scratch buffer). Used by display, print,
// print.at and friends.
typedef struct {
const uint8_t *(*fetchLogic)(void *ctx, uint8_t logicId, uint16_t *outBytecodeLength);
void (*loadPic)(void *ctx, uint8_t picId);
void (*drawPic)(void *ctx, uint8_t picId);
void (*showPic)(void *ctx);
void (*discardPic)(void *ctx, uint8_t picId);
void (*overlayPic)(void *ctx, uint8_t picId);
bool (*haveKey)(void *ctx);
const char *(*fetchMessage)(void *ctx, uint8_t logicId, uint8_t msgId);
void (*addToPic)(void *ctx, uint8_t viewId, uint8_t loop, uint8_t cel,
uint8_t x, uint8_t y, uint8_t pri, uint8_t margin);
// Return the playback duration of sound `soundId` in milliseconds,
// or 0 if the sound is missing/empty. Used only as a fallback
// duration cap when the host can't reliably report playback end;
// when isPlayingSound is wired, the VM ignores this and trusts
// the host's live state instead.
uint32_t (*soundDuration)(void *ctx, uint8_t soundId);
// Start playback of sound `soundId`. Any currently-playing AGI
// sound is replaced. The VM polls isPlayingSound each tick and
// sets the sound-done flag on the true->false transition.
void (*playSound)(void *ctx, uint8_t soundId);
// Stop playback immediately. Called by OP_STOP_SOUND (and any
// path that preempts the current sound).
void (*stopSound)(void *ctx);
// Live host-side playback state. Returns true while audio output
// is active for the currently-armed sound, false once the host
// has run all SND events to completion (or stopSound was called).
// Source of truth for sound completion: matches what the user
// actually hears, so the title sequence advances exactly when
// the music ends.
bool (*isPlayingSound)(void *ctx);
// View metadata for accurate cycling. AGI's cycle/loop opcodes
// (cycle.normal, end.of.loop, last.cel, number.of.loops...) need
// to know the cel count of the current view+loop and the loop
// count of the current view. Without these, end.of.loop fires
// its flag only at the AGI_MAX_CELS_PER_LOOP boundary -- and a
// sparkle that visually has 8 cels but is treated as 32 won't
// signal its end until 4x the intended time, breaking title
// sequences that arm end.of.loop expecting a tight cycle.
//
// Returns 0 if the view/loop is unloaded; callers treat 0 as
// "still running" so a not-yet-loaded view doesn't terminate
// early. Returns the loop count (>=1) or cel count (>=1) when
// the view is available.
uint8_t (*viewLoopCount)(void *ctx, uint8_t viewId);
uint8_t (*viewCelCount)(void *ctx, uint8_t viewId, uint8_t loopId);
void *ctx;
} AgiVmCallbacksT;
typedef struct {
// ----- core state -----
uint8_t vars[AGI_VM_NUM_VARS];
uint8_t flags[AGI_VM_NUM_FLAGS];
const uint8_t *code;
uint16_t codeLength;
uint16_t pc;
AgiVmHaltE haltReason;
uint8_t lastUnknownOp;
uint8_t newRoomId;
uint8_t currentLogicId;
uint8_t callDepth;
AgiVmFrameT callStack[AGI_VM_CALL_STACK_MAX];
AgiVmCallbacksT callbacks;
// ----- objects -----
AgiObjectT objects[AGI_MAX_OBJECTS];
// ----- text plane -----
AgiTextLineT textRows[AGI_TEXT_ROWS];
uint8_t textFg;
uint8_t textBg;
bool statusLineOn;
uint8_t statusLineRow; // usually 0
uint8_t horizon; // any obj y < horizon stops
bool programControl; // true => program controls ego (false = player)
bool acceptInput; // accept.input; toggled by accept/prevent
AgiPrintModalT printModal;
// ----- controllers + key bindings -----
AgiKeyBindingT keyBindings[AGI_MAX_KEY_BINDINGS];
uint8_t controllerFired[AGI_MAX_CONTROLLERS];
// ----- menus -----
AgiMenuT menus[AGI_MAX_MENUS];
uint8_t menuCount;
uint8_t menuOpen; // 0xFF = not open
// ----- strings -----
char strings[AGI_MAX_STRINGS][AGI_STRING_LEN + 1u];
// ----- sound playback state -----
// The VM polls callbacks.isPlayingSound() each tick; when it
// transitions from true to false, the sound-done flags fire.
// soundPlaying tracks whether we are currently waiting on an
// armed sound; soundDoneFlag is the per-sound user flag from
// OP_SOUND's second arg (0 = no user flag, just engine flag 9).
bool soundPlaying;
uint8_t soundDoneFlag;
} AgiVmT;
// ----- Resource loader (agiRes.c) -----
// Close all volume files; safe to call after a failed open.
void agiResClose(AgiGameT *game);
// Allocate and return a resource's raw bytes. Caller frees with
// free(). Returns NULL on any failure (slot empty, signature bad,
// I/O error, out of memory). On success *outLength receives the
// resource payload length in bytes.
uint8_t *agiResLoad(const AgiGameT *game, AgiResTypeE type, uint16_t index, uint16_t *outLength);
// Open an AGI v2 game directory. gameDir is a relative or absolute
// path to a directory containing LOGDIR/PICDIR/VIEWDIR/SNDDIR plus
// VOL.0..VOL.N. Returns true if every directory file and at least
// one VOL.* file opened. Partial-success on missing optional
// volumes; agiResLoad reports the missing-volume case per resource.
bool agiResOpen(AgiGameT *game, const char *gameDir);
// ----- Picture decoder (agiPic.c) -----
// Allocate the two 160x168 working buffers. Caller must agiPicFree
// before exit. Returns false if either malloc fails.
bool agiPicAlloc(AgiPicT *pic);
// Pixel-doubled blit of the visual plane onto the stage starting at
// (0, destY). 160 source columns become 320 stage columns; the rows
// copy 1:1 so the output occupies (0, destY) - (319, destY+167).
void agiPicBlit(const AgiPicT *pic, jlSurfaceT *stage, int16_t destY);
// Reset both planes to their AGI default backgrounds (15 / 4).
void agiPicClear(AgiPicT *pic);
// Decode an AGI v2 PIC resource into the working buffers. The buffers
// must already be cleared (agiPicClear) and allocated. Returns false
// if the byte stream is malformed (truncated argument, unknown
// opcode); on failure the buffers are left in a partial state.
bool agiPicDecode(AgiPicT *pic, const uint8_t *data, uint16_t length);
void agiPicFree(AgiPicT *pic);
// ----- View decoder (agiView.c) -----
// Parse an AGI VIEW resource. Takes ownership of `rawBytes` -- they
// must remain valid until agiViewFree (the view's cel pointers index
// into them). Returns false on malformed data; on failure the buffer
// is freed and `view` is left zeroed.
bool agiViewParse(AgiViewT *view, uint8_t *rawBytes, uint16_t length);
void agiViewFree(AgiViewT *view);
// Draw cel (loopIdx, celIdx) of `view` at AGI coordinate (x, y),
// where (x, y) is the bottom-left corner of the cel in 160x168 AGI
// space. Pixels are masked per-pixel by the priority plane: actor
// pixels are skipped where the picture priority exceeds actorPri.
// The blit is pixel-doubled horizontally onto the stage at
// (0, destY) -- pass the same destY used by agiPicBlit.
void agiViewDraw(const AgiViewT *view, uint8_t loopIdx, uint8_t celIdx,
int16_t x, int16_t y, uint8_t actorPri,
const uint8_t *picPriority,
jlSurfaceT *stage, int16_t destY);
// ----- LOGIC VM (agiVm.c) -----
// Reset every variable and flag to 0 and clear the halt state.
void agiVmInit(AgiVmT *vm);
// Point the VM at a logic resource's bytecode segment. AGI v2 logic
// resources start with a 2-byte little-endian length for the bytecode
// section followed by the bytecode itself (messages live past it).
// Returns false if the buffer is too short to contain a header.
bool agiVmLoadLogic(AgiVmT *vm, const uint8_t *resourceBytes, uint16_t resourceLength);
// Reset the VM's program counter to 0 against an already-loaded
// bytecode pointer. Used by the host after handling a halt (room
// transition, frame end) to re-run a logic from the top without
// re-parsing its resource header.
void agiVmResetToLogic(AgiVmT *vm, const uint8_t *bytecode, uint16_t bytecodeLength, uint8_t logicId);
// Run until the VM hits any halt condition (return, unknown opcode,
// truncated stream, or pending room transition). Returns the halt
// reason; details live in vm->lastUnknownOp / vm->newRoomId / vm->pc.
AgiVmHaltE agiVmRun(AgiVmT *vm);
// Install host callbacks. May be called before or after agiVmInit;
// pass NULL for any callback the host doesn't implement (the VM will
// treat that opcode as a no-op stub).
void agiVmSetCallbacks(AgiVmT *vm, const AgiVmCallbacksT *callbacks);
// Advance the AGI engine clock by `seconds` wall-clock seconds. Bumps
// v11 (seconds 0..59), v12 (minutes 0..59) and v13 (hours 0..23) with
// proper roll-over. Game logic uses these to detect per-second ticks
// (KQ3's logic.0 fires a per-second update branch when v11 changes,
// which sets flag 45 -- the gate the title countdown is waiting on).
// Safe to call with seconds=0 (no-op) and with any size up to UINT8_MAX.
void agiVmTickSeconds(AgiVmT *vm, uint8_t seconds);
// Per-VM-cycle ticker. Called by the host once per game-loop cycle
// (~6 Hz, the AGI interpreter cadence). Advances every animated
// object's cel cycling and motion. Honors per-object stop.cycling /
// stop.motion / stop.update flags. Sets end.of.loop / move.obj.done
// flags as those events fire.
void agiVmTickAnimation(AgiVmT *vm);
// Notify the VM that the host received key `keyCode` (low 7 bits =
// ASCII; high byte = extended scancode for arrow keys / Fn). Walks
// the key-binding table; if `keyCode` matches a set.key binding,
// the corresponding controller fires. Also dispatches to any open
// menu navigation. Called by the host once per pressed key.
void agiVmDispatchKey(AgiVmT *vm, uint8_t key1, uint8_t key2);
// Acknowledge an open print() modal so the VM can resume. Called by
// the host when the user presses ENTER / SPACE / etc. while
// vm->printModal.active is true. Clears the modal and the
// AGI_VM_HALT_PRINT_PENDING halt reason.
void agiVmAckPrint(AgiVmT *vm);
// ----- Object helpers (agiObj.c) -----
// Reset every object slot to the default empty state. Called by
// agiVmInit and again on every NEW_ROOM transition (matches AGI's
// canonical room-change reset).
void agiObjResetAll(AgiVmT *vm);
// Single-object reset (animate.obj reuses this).
void agiObjReset(AgiObjectT *obj);
// ----- Text helpers (agiText.c) -----
// Fetch the host font surface (built lazily on first call). Returned
// surface contains 96 ASCII glyphs (codes 32..127) laid out 16 wide
// by 6 tall. Caller must NOT free.
const jlSurfaceT *agiTextFontSurface(void);
// asciiMap suitable for jlDrawText against the font surface above.
const uint16_t *agiTextAsciiMap(void);
// Render the VM's text plane (status line + display rows + open
// print modal) onto `stage`. Called by host after the picture +
// objects are drawn.
void agiTextRender(const AgiVmT *vm, jlSurfaceT *stage);
// ----- PIC compositing (agiPic.c addendum) -----
// Bake a VIEW cel into the PIC visual + priority planes (the
// implementation of add.to.pic). priColor 4..15 sets the priority
// pixels; priColor == 0 means "use the priority of the actor's Y
// band" (AGI default). margin 0..3 reserves a control-line band at
// the cel's bottom so add.to.pic art can suggest a walkable edge;
// margin 4 means "no margin".
void agiPicAddView(AgiPicT *pic, const AgiViewT *view,
uint8_t loop, uint8_t cel,
int16_t x, int16_t y,
uint8_t priColor, uint8_t margin);
#endif