// 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];
    // WORDS.TOK dictionary, loaded whole. Prefix-compressed entries are
    // walked on demand by agiWordId (parse is infrequent -- once per typed
    // command -- so a flat scan beats building a hashed table).
    uint8_t       *words;
    uint32_t       wordsLen;
} AgiGameT;

// Reserved AGI word group IDs.
#define AGI_WORD_IGNORE     0u       // "a", "the", ... -- dropped by parse
#define AGI_WORD_ANYWORD    1u       // said() wildcard: matches any one word
#define AGI_WORD_REST       9999u    // said() wildcard: matches rest of line
#define AGI_WORD_UNKNOWN    0xFFFFu  // agiWordId: typed word not in dictionary

// WORDS.TOK layout + limits. The header is 26 big-endian 2-byte offsets
// (one per initial letter); word entries follow. AGI v2 dictionaries are a
// few KB (KQ3 is ~5.7 KB); 16 KB is ample headroom.
#define AGI_WORDS_HEADER_BYTES  52u
#define AGI_WORDS_MAX           16384u
#define AGI_WORD_MAX_LEN        40u

// Command-line parser limits + the reserved flag/var the interpreter uses
// to talk to logic scripts about input:
//   flag 2  -- set when the player has entered a command this cycle; said()
//              matches only while it is set and clears it on a match so only
//              the first matching said() in the logic scan fires.
//   var 9   -- 1-based position of a typed word that isn't in the dictionary.
#define AGI_INPUT_MAX          40u     // chars in the on-screen command line
#define AGI_MAX_PARSED         12u     // word IDs kept from one command
#define AGI_INPUT_ROW          24u     // bottom text row shows the command line
#define AGI_FLAG_INPUT         2u
#define AGI_VAR_UNKNOWN_WORD   9u


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

    // ----- parser / command input -----
    // inputLine is the raw text the player is typing; on Enter agi.c
    // tokenizes it against the dictionary into parsedWords[] (group IDs,
    // ignore-words dropped) and sets flag 2 for said() to test.
    char              inputLine[AGI_INPUT_MAX + 1u];
    uint8_t           inputLen;
    uint16_t          parsedWords[AGI_MAX_PARSED];
    uint8_t           parsedWordCount;

    // ----- 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 the AGI v2 game whose LOGDIR/PICDIR/VIEWDIR/SNDDIR and VOL.0..VOL.N
// files are staged under DATA/ (opened by bare name via jlDataOpen).
// 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);

// Look up a word in the loaded WORDS.TOK dictionary (case-insensitive) and
// return its AGI group ID, or AGI_WORD_UNKNOWN if the word is absent (or no
// dictionary loaded). Synonyms share a group ID; group 0 = ignore-words.
uint16_t agiWordId(const AgiGameT *game, const char *word);


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