// 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 // 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