562 lines
22 KiB
C
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
|