joeylib2/examples/agi/agiVm.c

2300 lines
84 KiB
C

// AGI v2 LOGIC VM, Phase 1 sub-A: bytecode dispatch skeleton.
//
// This pass implements the variable / flag opcodes (0x00-0x11) plus
// the basic header parser. Control flow (IF / ELSE / GOTO), test
// commands (equaln, isset, said, ...), and side-effecting opcodes
// (new.room, load.pic, animate.obj, ...) come in later sub-phases.
// Unknown opcodes halt cleanly with the offending byte recorded so
// host tests can report exactly where execution stopped.
//
// All algorithms here are re-implementations written against the
// published AGI v2 LOGIC bytecode specification; no third-party
// interpreter source is referenced.
#include "agi.h"
#include "joey/core.h"
#include "joey/debug.h"
#include <stddef.h>
#include <string.h>
// LOGIC resource layout: bytes 0-1 are the little-endian length of
// the bytecode section, followed by `bytecodeLen` bytes of bytecode,
// followed by the message table. The VM walks only the bytecode.
#define AGI_LOGIC_HDR_BYTES 2u
// Implemented action opcodes (sub-A: arithmetic and flags).
#define OP_RETURN 0x00u
#define OP_INCREMENT 0x01u
#define OP_DECREMENT 0x02u
#define OP_ASSIGNN 0x03u
#define OP_ASSIGNV 0x04u
#define OP_ADDN 0x05u
#define OP_ADDV 0x06u
#define OP_SUBN 0x07u
#define OP_SUBV 0x08u
#define OP_LINDIRECTV 0x09u
#define OP_RINDIRECT 0x0Au
#define OP_LINDIRECTN 0x0Bu
#define OP_SET 0x0Cu
#define OP_RESET 0x0Du
#define OP_TOGGLE 0x0Eu
#define OP_SETV 0x0Fu
#define OP_RESETV 0x10u
#define OP_TOGGLEV 0x11u
// Control flow (sub-B). 0xFF as an action opcode introduces an IF
// expression; the test list is terminated by an inner 0xFF, followed
// by a 2-byte LE skip offset that jumps past the IF body when the
// test is false. 0xFE is an unconditional relative GOTO (signed 16
// bit), used by the compiler to skip ELSE bodies and to form loops.
#define OP_GOTO 0xFEu
#define OP_IF 0xFFu
// Test command IDs (sub-B). Only valid inside an IF test list.
#define TEST_EQUALN 0x01u
#define TEST_EQUALV 0x02u
#define TEST_LESSN 0x03u
#define TEST_LESSV 0x04u
#define TEST_GREATERN 0x05u
#define TEST_GREATERV 0x06u
#define TEST_ISSET 0x07u
#define TEST_ISSETV 0x08u
#define TEST_HAS 0x09u
#define TEST_OBJ_IN_ROOM 0x0Au
#define TEST_POSN 0x0Bu
#define TEST_CONTROLLER 0x0Cu
#define TEST_HAVE_KEY 0x0Du
#define TEST_SAID 0x0Eu
#define TEST_COMPARE_STRINGS 0x0Fu
#define TEST_OBJ_IN_BOX 0x10u
#define TEST_CENTER_POSN 0x11u
#define TEST_RIGHT_POSN 0x12u
#define TEST_MAX 0x13u
// Markers inside an IF test list.
#define TEST_END_LIST 0xFFu
#define TEST_OR_TOGGLE 0xFCu
#define TEST_NOT 0xFDu
// Side-effecting opcodes wired to host callbacks in sub-D. Each has
// a real case in the dispatch switch; the host's callback table
// decides what (if anything) actually happens for the call.
#define OP_NEW_ROOM 0x12u
#define OP_NEW_ROOM_V 0x13u
#define OP_LOAD_LOGIC 0x14u
#define OP_LOAD_LOGIC_V 0x15u
#define OP_CALL 0x16u
#define OP_CALL_V 0x17u
#define OP_LOAD_PIC 0x18u
#define OP_DRAW_PIC 0x19u
#define OP_SHOW_PIC 0x1Au
#define OP_DISCARD_PIC 0x1Bu
#define OP_OVERLAY_PIC 0x1Cu
// Object-table opcodes (animate.obj, set.view, ..., release.priority).
// These mutate the per-object slots in vm->objects[N]; per-frame
// rendering and per-cycle animation tick are handled outside the VM.
#define OP_LOAD_VIEW 0x1Eu
#define OP_LOAD_VIEW_V 0x1Fu
#define OP_DISCARD_VIEW 0x20u
#define OP_ANIMATE_OBJ 0x21u
#define OP_UNANIMATE_ALL 0x22u
#define OP_DRAW 0x23u
#define OP_ERASE 0x24u
#define OP_POSITION 0x25u
#define OP_POSITION_V 0x26u
#define OP_GET_POSN 0x27u
#define OP_REPOSITION 0x28u
#define OP_SET_VIEW 0x29u
#define OP_SET_VIEW_V 0x2Au
#define OP_SET_LOOP 0x2Bu
#define OP_SET_LOOP_V 0x2Cu
#define OP_FIX_LOOP 0x2Du
#define OP_RELEASE_LOOP 0x2Eu
#define OP_SET_CEL 0x2Fu
#define OP_SET_CEL_V 0x30u
#define OP_LAST_CEL 0x31u
#define OP_CURRENT_CEL 0x32u
#define OP_CURRENT_LOOP 0x33u
#define OP_CURRENT_VIEW 0x34u
#define OP_NUMBER_OF_LOOPS 0x35u
#define OP_SET_PRIORITY 0x36u
#define OP_SET_PRIORITY_V 0x37u
#define OP_RELEASE_PRIORITY 0x38u
#define OP_GET_PRIORITY 0x39u
#define OP_STOP_UPDATE 0x3Au
#define OP_START_UPDATE 0x3Bu
#define OP_FORCE_UPDATE 0x3Cu
#define OP_IGNORE_HORIZON 0x3Du
#define OP_OBSERVE_HORIZON 0x3Eu
#define OP_SET_HORIZON 0x3Fu
#define OP_OBJECT_ON_WATER 0x40u
#define OP_OBJECT_ON_LAND 0x41u
#define OP_OBJECT_ON_ANYTHING 0x42u
#define OP_IGNORE_OBJS 0x43u
#define OP_OBSERVE_OBJS 0x44u
#define OP_DISTANCE 0x45u
// Animation cycling.
#define OP_STOP_CYCLING 0x46u
#define OP_START_CYCLING 0x47u
#define OP_NORMAL_CYCLE 0x48u
#define OP_END_OF_LOOP 0x49u
#define OP_REVERSE_CYCLE 0x4Au
#define OP_REVERSE_LOOP 0x4Bu
#define OP_CYCLE_TIME 0x4Cu
// Motion.
#define OP_STOP_MOTION 0x4Du
#define OP_START_MOTION 0x4Eu
#define OP_STEP_SIZE 0x4Fu
#define OP_STEP_TIME 0x50u
#define OP_MOVE_OBJ 0x51u
#define OP_MOVE_OBJ_V 0x52u
#define OP_FOLLOW_EGO 0x53u
#define OP_WANDER 0x54u
#define OP_NORMAL_MOTION 0x55u
#define OP_SET_DIR 0x56u
#define OP_GET_DIR 0x57u
// Object collision (most stub no-op; engine doesn't model blocking).
#define OP_IGNORE_BLOCKS 0x58u
#define OP_OBSERVE_BLOCKS 0x59u
#define OP_BLOCK 0x5Au
#define OP_UNBLOCK 0x5Bu
// Inventory.
#define OP_GET 0x5Cu
#define OP_GET_V 0x5Du
#define OP_DROP 0x5Eu
#define OP_PUT 0x5Fu
#define OP_PUT_V 0x60u
#define OP_GET_ROOM_V 0x61u
// Sound (stub for now; satisfies "sound finished" flag immediately).
#define OP_LOAD_SOUND 0x62u
#define OP_SOUND 0x63u
#define OP_STOP_SOUND 0x64u
// Text.
#define OP_PRINT 0x65u
#define OP_PRINT_V 0x66u
#define OP_DISPLAY 0x67u
#define OP_DISPLAY_V 0x68u
#define OP_CLEAR_LINES 0x69u
#define OP_TEXT_SCREEN 0x6Au
#define OP_GRAPHICS 0x6Bu
#define OP_SET_CURSOR_CHAR 0x6Cu
#define OP_SET_TEXT_ATTRIBUTE 0x6Du
#define OP_SHAKE_SCREEN 0x6Eu
#define OP_CONFIGURE_SCREEN 0x6Fu
#define OP_STATUS_LINE_ON 0x70u
#define OP_STATUS_LINE_OFF 0x71u
#define OP_SET_STRING 0x72u
#define OP_GET_STRING 0x73u
#define OP_WORD_TO_STRING 0x74u
#define OP_PARSE 0x75u
#define OP_GET_NUM 0x76u
// Input.
#define OP_PREVENT_INPUT 0x77u
#define OP_ACCEPT_INPUT 0x78u
#define OP_SET_KEY 0x79u
// Picture composition.
#define OP_ADD_TO_PIC 0x7Au
#define OP_ADD_TO_PIC_V 0x7Bu
// Misc.
#define OP_STATUS 0x7Cu
#define OP_SAVE_GAME 0x7Du
#define OP_RESTORE_GAME 0x7Eu
#define OP_INIT_DISK 0x7Fu
#define OP_RESTART_GAME 0x80u
#define OP_SHOW_OBJ 0x81u
#define OP_RANDOM 0x82u
#define OP_PROGRAM_CONTROL 0x83u
#define OP_PLAYER_CONTROL 0x84u
#define OP_OBJ_STATUS_V 0x85u
#define OP_QUIT 0x86u
#define OP_SHOW_MEM 0x87u
#define OP_PAUSE 0x88u
#define OP_ECHO_LINE 0x89u
#define OP_CANCEL_LINE 0x8Au
#define OP_INIT_JOY 0x8Bu
#define OP_TOGGLE_MONITOR 0x8Cu
#define OP_VERSION 0x8Du
#define OP_SCRIPT_SIZE 0x8Eu
#define OP_SET_GAME_ID 0x8Fu
#define OP_LOG 0x90u
#define OP_SET_SCAN_START 0x91u
#define OP_RESET_SCAN_START 0x92u
#define OP_REPOSITION_TO 0x93u
#define OP_REPOSITION_TO_V 0x94u
#define OP_TRACE_ON 0x95u
#define OP_TRACE_INFO 0x96u
#define OP_PRINT_AT 0x97u
#define OP_PRINT_AT_V 0x98u
#define OP_DISCARD_VIEW_V 0x99u
#define OP_CLEAR_TEXT_RECT 0x9Au
#define OP_SET_UPPER_LEFT 0x9Bu
#define OP_SET_MENU 0x9Cu
#define OP_SET_MENU_ITEM 0x9Du
#define OP_SUBMIT_MENU 0x9Eu
#define OP_ENABLE_ITEM 0x9Fu
#define OP_DISABLE_ITEM 0xA0u
#define OP_MENU_INPUT 0xA1u
#define OP_SHOW_OBJ_V 0xA2u
#define OP_OPEN_DIALOGUE 0xA3u
#define OP_CLOSE_DIALOGUE 0xA4u
#define OP_MUL_N 0xA5u
#define OP_MUL_V 0xA6u
#define OP_DIV_N 0xA7u
#define OP_DIV_V 0xA8u
#define OP_CLOSE_WINDOW 0xA9u
#define OP_SET_SIMPLE 0xAAu
// Special vars touched by the engine outside script control.
#define ENG_VAR_EGO_DIR 6u
#define ENG_VAR_EGO_VIEW 16u
// Inventory state lives in vm->vars[88..127] (40 slots) — each slot
// stores the room id where item N is currently (255 = ego inventory).
// AGI v2 fixes the inventory length to about 40 in most games; we
// reserve more (88..255) but cap at 64 items in practice.
#define INV_ROOM_INVENTORY 255u
// Sentinel value in kActionArgBytes meaning "no v2 opcode at this
// index"; the dispatcher halts when it sees this in the table.
#define ACTION_UNKNOWN 0xFFu
#ifdef JOEYLIB_PLATFORM_IIGS
segment "AGIVM";
#endif
// ----- Prototypes -----
static bool enterLogic(AgiVmT *vm, uint8_t logicId);
static bool evalOneTest(AgiVmT *vm, uint8_t testOp);
static bool evalTests(AgiVmT *vm);
static bool executeGoto(AgiVmT *vm);
static bool executeIf(AgiVmT *vm);
static void expandMessage(AgiVmT *vm, const char *src, char *dst, uint16_t dstCap);
static const char *fetchMessageOrEmpty(AgiVmT *vm, uint8_t logicId, uint8_t msgId);
static void openPrintModal(AgiVmT *vm, const char *expanded);
static bool popFrame(AgiVmT *vm);
static bool pushFrame(AgiVmT *vm);
static bool readByte(AgiVmT *vm, uint8_t *out);
static bool readU16(AgiVmT *vm, uint16_t *out);
static bool skipTestArgs(AgiVmT *vm, uint8_t testOp);
static void writeTextRow(AgiVmT *vm, uint8_t row, uint8_t col, const char *expanded);
// Per-action-opcode argument byte counts, indexed by opcode id. The
// real handlers (arithmetic / flags / IF / GOTO / new.room) consult
// this only as a sanity check; unrecognized opcodes (entries set to
// ACTION_UNKNOWN) halt the VM with AGI_VM_HALT_UNKNOWN_OP. Every other
// entry is treated as a stub: its argument bytes are consumed and no
// side effect is taken. This lets the VM walk arbitrary v2 logic
// without halting at every yet-to-be-implemented side-effecting
// command (load.pic / animate.obj / sound / ...).
static const uint8_t kActionArgBytes[256] = {
/* 0x00 */ 0u, 1u, 1u, 2u, 2u, 2u, 2u, 2u, 2u, 2u, 2u, 2u, 1u, 1u, 1u, 1u,
/* 0x10 */ 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 0u, 1u, 1u, 0u, 1u, 1u,
/* 0x20 */ 1u, 1u, 0u, 1u, 1u, 3u, 3u, 3u, 3u, 2u, 2u, 2u, 2u, 1u, 1u, 2u,
/* 0x30 */ 2u, 2u, 2u, 2u, 2u, 2u, 2u, 2u, 1u, 2u, 1u, 1u, 1u, 1u, 1u, 1u,
/* 0x40 */ 1u, 1u, 1u, 1u, 1u, 3u, 1u, 1u, 1u, 2u, 1u, 2u, 2u, 1u, 1u, 2u,
/* 0x50 */ 2u, 5u, 5u, 3u, 1u, 1u, 2u, 2u, 1u, 1u, 4u, 0u, 1u, 1u, 1u, 2u,
/* 0x60 */ 2u, 2u, 1u, 2u, 0u, 1u, 1u, 3u, 3u, 3u, 0u, 0u, 1u, 2u, 1u, 3u,
/* 0x70 */ 0u, 0u, 2u, 5u, 2u, 1u, 2u, 0u, 0u, 3u, 7u, 7u, 0u, 0u, 0u, 0u,
/* 0x80 */ 0u, 1u, 3u, 0u, 0u, 1u, 1u, 0u, 0u, 0u, 0u, 0u, 0u, 0u, 1u, 1u,
/* 0x90 */ 1u, 0u, 0u, 3u, 3u, 0u, 3u, 4u, 4u, 1u, 4u, 2u, 1u, 2u, 0u, 1u,
/* 0xA0 */ 1u, 0u, 1u, 0u, 0u, 2u, 2u, 2u, 2u, 0u, 1u, 0u, 0u, 0u, 1u, 1u,
/* 0xB0 */ 0u, 1u, 0u, 4u, 2u, 0u, 0u,
ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN,
/* 0xBA */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN,
/* 0xC0 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN,
/* 0xC8 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN,
/* 0xD0 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN,
/* 0xD8 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN,
/* 0xE0 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN,
/* 0xE8 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN,
/* 0xF0 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN,
/* 0xF8 */ ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN, ACTION_UNKNOWN
};
// Per-test-command argument byte counts, indexed by test opcode id.
// 0x0E (said) is variable-length and gets special-cased rather than
// indexed via this table.
static const uint8_t kTestArgBytes[TEST_MAX] = {
/* 0x00 */ 0u,
/* 0x01 equaln */ 2u,
/* 0x02 equalv */ 2u,
/* 0x03 lessn */ 2u,
/* 0x04 lessv */ 2u,
/* 0x05 greatern */ 2u,
/* 0x06 greaterv */ 2u,
/* 0x07 isset */ 1u,
/* 0x08 issetv */ 1u,
/* 0x09 has */ 1u,
/* 0x0A obj.in.room */ 2u,
/* 0x0B posn */ 5u,
/* 0x0C controller */ 1u,
/* 0x0D have.key */ 0u,
/* 0x0E said */ 0u,
/* 0x0F compare.strings */ 2u,
/* 0x10 obj.in.box */ 5u,
/* 0x11 center.posn */ 5u,
/* 0x12 right.posn */ 5u
};
// ----- Internal helpers (alphabetical) -----
// Switch the VM's running logic to `logicId`. Used by OP_CALL and
// OP_CALL_V after pushFrame has saved the caller's position. Returns
// false (and sets haltReason) if the host has no bytecode for the
// requested id.
static bool enterLogic(AgiVmT *vm, uint8_t logicId) {
const uint8_t *bytecode;
uint16_t length;
if (vm->callbacks.fetchLogic == NULL) {
vm->haltReason = AGI_VM_HALT_NO_LOGIC;
return false;
}
length = 0u;
bytecode = vm->callbacks.fetchLogic(vm->callbacks.ctx, logicId, &length);
if (bytecode == NULL || length == 0u) {
vm->haltReason = AGI_VM_HALT_NO_LOGIC;
return false;
}
vm->code = bytecode;
vm->codeLength = length;
vm->pc = 0u;
vm->currentLogicId = logicId;
return true;
}
// Evaluate a single test command, with pc currently positioned at
// the first argument byte. Always consumes the test's full argument
// list so the caller's stream position stays in sync, even when the
// test is stubbed to a constant false. Tests this sub-phase doesn't
// implement (objects, parser, controllers, key state) read their
// args and return false; the VM walks past the IF body as if the
// test evaluated against fresh game state.
static bool evalOneTest(AgiVmT *vm, uint8_t testOp) {
uint8_t args[5];
uint8_t i;
uint8_t argBytes;
if (testOp == 0u || testOp >= TEST_MAX) {
vm->lastUnknownOp = testOp;
vm->haltReason = AGI_VM_HALT_UNKNOWN_OP;
return false;
}
if (testOp == TEST_SAID) {
// 1-byte word count, followed by 2 bytes per word.
if (!skipTestArgs(vm, testOp)) {
return false;
}
return false;
}
argBytes = kTestArgBytes[testOp];
for (i = 0u; i < argBytes; i++) {
if (!readByte(vm, &args[i])) {
return false;
}
}
switch (testOp) {
case TEST_EQUALN: return vm->vars[args[0]] == args[1];
case TEST_EQUALV: return vm->vars[args[0]] == vm->vars[args[1]];
case TEST_LESSN: return vm->vars[args[0]] < args[1];
case TEST_LESSV: return vm->vars[args[0]] < vm->vars[args[1]];
case TEST_GREATERN: return vm->vars[args[0]] > args[1];
case TEST_GREATERV: return vm->vars[args[0]] > vm->vars[args[1]];
case TEST_ISSET: return vm->flags[args[0]] != 0u;
case TEST_ISSETV: return vm->flags[vm->vars[args[0]]] != 0u;
case TEST_HAVE_KEY:
if (vm->callbacks.haveKey != NULL) {
return vm->callbacks.haveKey(vm->callbacks.ctx);
}
return false;
case TEST_CONTROLLER: {
uint8_t c = args[0];
if (c < AGI_MAX_CONTROLLERS && vm->controllerFired[c] != 0u) {
vm->controllerFired[c] = 0u;
return true;
}
return false;
}
case TEST_POSN: {
uint8_t n = args[0];
int16_t x1 = (int16_t)args[1];
int16_t y1 = (int16_t)args[2];
int16_t x2 = (int16_t)args[3];
int16_t y2 = (int16_t)args[4];
const AgiObjectT *o;
if (n >= AGI_MAX_OBJECTS) return false;
o = &vm->objects[n];
return (o->x >= x1 && o->x <= x2 && o->y >= y1 && o->y <= y2);
}
case TEST_OBJ_IN_BOX: {
// Same shape as posn but checks the *entire* cel bbox is
// inside the box. Without view dimensions in the VM we
// approximate with point containment (== posn).
uint8_t n = args[0];
int16_t x1 = (int16_t)args[1];
int16_t y1 = (int16_t)args[2];
int16_t x2 = (int16_t)args[3];
int16_t y2 = (int16_t)args[4];
const AgiObjectT *o;
if (n >= AGI_MAX_OBJECTS) return false;
o = &vm->objects[n];
return (o->x >= x1 && o->x <= x2 && o->y >= y1 && o->y <= y2);
}
case TEST_CENTER_POSN: {
uint8_t n = args[0];
int16_t x1 = (int16_t)args[1];
int16_t y1 = (int16_t)args[2];
int16_t x2 = (int16_t)args[3];
int16_t y2 = (int16_t)args[4];
const AgiObjectT *o;
if (n >= AGI_MAX_OBJECTS) return false;
o = &vm->objects[n];
return (o->x >= x1 && o->x <= x2 && o->y >= y1 && o->y <= y2);
}
case TEST_RIGHT_POSN: {
uint8_t n = args[0];
int16_t x1 = (int16_t)args[1];
int16_t y1 = (int16_t)args[2];
int16_t x2 = (int16_t)args[3];
int16_t y2 = (int16_t)args[4];
const AgiObjectT *o;
if (n >= AGI_MAX_OBJECTS) return false;
o = &vm->objects[n];
return (o->x >= x1 && o->x <= x2 && o->y >= y1 && o->y <= y2);
}
case TEST_HAS:
// Inventory: var[88+arg] is the item room (255 = held).
// Without OBJECT-table parsing, default to "not held".
return false;
case TEST_OBJ_IN_ROOM:
// Same: defaults to false until OBJECT support lands.
return false;
default: return false;
}
}
// Walk the IF test list at pc. Default operator is AND; 0xFC toggles
// in/out of an OR group whose result is then ANDed into the running
// AND. 0xFD prefixes the next test with NOT. 0xFF terminates the
// list. Leaves pc positioned right after the terminator so the
// caller can read the 2-byte skip offset.
static bool evalTests(AgiVmT *vm) {
bool andResult;
bool orResult;
bool inOrGroup;
bool notNext;
bool testResult;
uint8_t op;
andResult = true;
orResult = false;
inOrGroup = false;
notNext = false;
for (;;) {
if (!readByte(vm, &op)) {
return false;
}
if (op == TEST_END_LIST) {
if (inOrGroup) {
andResult = (bool)(andResult && orResult);
}
return andResult;
}
if (op == TEST_OR_TOGGLE) {
if (inOrGroup) {
andResult = (bool)(andResult && orResult);
inOrGroup = false;
} else {
orResult = false;
inOrGroup = true;
}
continue;
}
if (op == TEST_NOT) {
notNext = (bool)!notNext;
continue;
}
testResult = evalOneTest(vm, op);
if (vm->haltReason != AGI_VM_HALT_NONE) {
return false;
}
if (notNext) {
testResult = (bool)!testResult;
notNext = false;
}
if (inOrGroup) {
orResult = (bool)(orResult || testResult);
} else {
andResult = (bool)(andResult && testResult);
}
}
}
// Unconditional relative jump. Offset is signed 16-bit; negative
// values allow backward jumps for loops. 16-bit arithmetic with
// separate positive / negative paths avoids dragging int32 into the
// 65816 build.
// Substitute AGI v2 message placeholders into an output buffer.
// Recognizes %v<num> (decimal var value), %0<num> (decimal var with
// leading zero pad), %s<num> (game string slot), %m<num> (recursive
// message lookup in current logic). Unknown sequences are passed
// through verbatim. Always NUL-terminates within dstCap.
static void expandMessage(AgiVmT *vm, const char *src, char *dst, uint16_t dstCap) {
uint16_t out;
uint8_t pad;
uint8_t varIdx;
uint8_t value;
uint8_t tmp[4];
int8_t digit;
char ch;
out = 0u;
if (dstCap == 0u) {
return;
}
while (src != NULL && *src != '\0' && out + 1u < dstCap) {
ch = *src++;
if (ch != '%') {
dst[out++] = ch;
continue;
}
if (*src == '\0') {
dst[out++] = '%';
break;
}
{
char tag = *src++;
uint8_t arg = 0u;
uint8_t digitsRead = 0u;
while (*src >= '0' && *src <= '9' && digitsRead < 3u) {
arg = (uint8_t)(arg * 10u + (uint8_t)(*src - '0'));
src++;
digitsRead++;
}
if (digitsRead == 0u) {
if (out + 2u < dstCap) {
dst[out++] = '%';
dst[out++] = tag;
}
continue;
}
switch (tag) {
case 'v': case '0':
pad = (tag == '0') ? 1u : 0u;
varIdx = arg;
value = vm->vars[varIdx];
digit = 0;
do {
tmp[digit++] = (uint8_t)(value % 10u);
value = (uint8_t)(value / 10u);
} while (value != 0u && digit < 3);
while (pad && digit < 3) {
tmp[digit++] = 0u;
}
while (digit > 0 && out + 1u < dstCap) {
digit--;
dst[out++] = (char)('0' + tmp[digit]);
}
break;
case 's':
if (arg < AGI_MAX_STRINGS) {
const char *s = vm->strings[arg];
while (*s != '\0' && out + 1u < dstCap) {
dst[out++] = *s++;
}
}
break;
case 'm': {
const char *nested = fetchMessageOrEmpty(vm, vm->currentLogicId, arg);
char scratch[AGI_PRINT_MAX_LEN + 1u];
expandMessage(vm, nested, scratch, sizeof(scratch));
{
const char *s = scratch;
while (*s != '\0' && out + 1u < dstCap) {
dst[out++] = *s++;
}
}
break;
}
case 'g':
case 'w':
// %g (logic 0 message — global) and %w (parser word)
// — fall back to no-op so the script doesn't see
// garbage substitutions.
break;
default:
if (out + 2u < dstCap) {
dst[out++] = '%';
dst[out++] = tag;
}
break;
}
}
}
dst[out] = '\0';
}
static bool executeGoto(AgiVmT *vm) {
uint16_t raw;
int16_t signedOffset;
uint16_t magnitude;
if (!readU16(vm, &raw)) {
return false;
}
signedOffset = (int16_t)raw;
if (signedOffset >= 0) {
magnitude = (uint16_t)signedOffset;
if (magnitude > (uint16_t)(vm->codeLength - vm->pc)) {
vm->haltReason = AGI_VM_HALT_TRUNCATED;
return false;
}
vm->pc = (uint16_t)(vm->pc + magnitude);
} else {
magnitude = (uint16_t)(-signedOffset);
if (magnitude > vm->pc) {
vm->haltReason = AGI_VM_HALT_TRUNCATED;
return false;
}
vm->pc = (uint16_t)(vm->pc - magnitude);
}
return true;
}
static bool executeIf(AgiVmT *vm) {
bool cond;
uint16_t skip;
cond = evalTests(vm);
if (vm->haltReason != AGI_VM_HALT_NONE) {
return false;
}
if (!readU16(vm, &skip)) {
return false;
}
if (!cond) {
if (skip > (uint16_t)(vm->codeLength - vm->pc)) {
vm->haltReason = AGI_VM_HALT_TRUNCATED;
return false;
}
vm->pc = (uint16_t)(vm->pc + skip);
}
return true;
}
static const char *fetchMessageOrEmpty(AgiVmT *vm, uint8_t logicId, uint8_t msgId) {
const char *msg;
if (vm->callbacks.fetchMessage == NULL) {
return "";
}
msg = vm->callbacks.fetchMessage(vm->callbacks.ctx, logicId, msgId);
return (msg != NULL) ? msg : "";
}
// AGI print() default text-window width: ~30 columns. Sierra's
// interpreter word-wraps the message at this width before measuring
// the window. Without wrapping a long single-line message (e.g.
// KQ3's "Gwydion is a lonely lad...") overflows the screen edge.
#define AGI_PRINT_WRAP_COLS 30u
static void openPrintModal(AgiVmT *vm, const char *expanded) {
uint16_t i;
uint16_t out;
uint16_t cap;
uint16_t lineStart; // index in printModal.message where current line begins
uint16_t lastSpace; // index of last space char in current line (0 = none yet)
uint16_t col; // visible columns on current line
cap = AGI_PRINT_MAX_LEN;
if (expanded == NULL) {
vm->printModal.message[0] = '\0';
vm->printModal.length = 0u;
vm->printModal.active = true;
vm->haltReason = AGI_VM_HALT_PRINT_PENDING;
return;
}
// Stream characters from expanded into printModal.message, breaking
// at AGI_PRINT_WRAP_COLS by replacing the last space on the line
// with '\n'. Preserves explicit '\n' from the source message.
out = 0u;
lineStart = 0u;
lastSpace = 0u;
col = 0u;
i = 0u;
while (expanded[i] != '\0' && out < cap - 1u) {
char c = expanded[i++];
if (c == '\n') {
vm->printModal.message[out++] = '\n';
lineStart = out;
lastSpace = 0u;
col = 0u;
continue;
}
vm->printModal.message[out++] = c;
if (c == ' ') {
lastSpace = (uint16_t)(out - 1u);
}
col++;
if (col >= AGI_PRINT_WRAP_COLS) {
if (lastSpace != 0u && lastSpace >= lineStart) {
// Replace the last space on this line with '\n' so
// the partial word that followed wraps to the next.
vm->printModal.message[lastSpace] = '\n';
col = (uint16_t)(out - lastSpace - 1u);
lineStart = (uint16_t)(lastSpace + 1u);
lastSpace = 0u;
} else {
// No space to break on: hard-wrap mid-token.
if (out < cap - 1u) {
vm->printModal.message[out++] = '\n';
}
lineStart = out;
lastSpace = 0u;
col = 0u;
}
}
}
vm->printModal.message[out] = '\0';
vm->printModal.length = out;
vm->printModal.active = true;
vm->haltReason = AGI_VM_HALT_PRINT_PENDING;
}
// Restore the most recently pushed frame as the active context.
// Caller (OP_RETURN at depth > 0) has already confirmed callDepth > 0.
static bool popFrame(AgiVmT *vm) {
const AgiVmFrameT *f;
vm->callDepth--;
f = &vm->callStack[vm->callDepth];
vm->code = f->code;
vm->codeLength = f->codeLength;
vm->pc = f->pc;
vm->currentLogicId = f->logicId;
return true;
}
// Save the active context onto the call stack so OP_CALL can switch
// to a different logic. Returns false (and halts) if the stack is
// already full -- AGI v2 games keep call depth shallow so a small
// fixed-size stack is sufficient.
static bool pushFrame(AgiVmT *vm) {
AgiVmFrameT *f;
if (vm->callDepth >= AGI_VM_CALL_STACK_MAX) {
vm->haltReason = AGI_VM_HALT_STACK_OVER;
return false;
}
f = &vm->callStack[vm->callDepth];
f->code = vm->code;
f->codeLength = vm->codeLength;
f->pc = vm->pc;
f->logicId = vm->currentLogicId;
vm->callDepth++;
return true;
}
static bool readByte(AgiVmT *vm, uint8_t *out) {
if (vm->pc >= vm->codeLength) {
vm->haltReason = AGI_VM_HALT_TRUNCATED;
return false;
}
*out = vm->code[vm->pc];
vm->pc++;
return true;
}
static bool readU16(AgiVmT *vm, uint16_t *out) {
uint8_t lo;
uint8_t hi;
if (!readByte(vm, &lo)) {
return false;
}
if (!readByte(vm, &hi)) {
return false;
}
*out = (uint16_t)(lo | ((uint16_t)hi << 8));
return true;
}
// Walk past the argument bytes of a test command without evaluating.
// For the variable-length `said` test, the first byte is the word
// count and the rest are 2 bytes per word.
static bool skipTestArgs(AgiVmT *vm, uint8_t testOp) {
uint8_t scratch;
uint8_t wordCount;
uint16_t toSkip;
uint16_t i;
if (testOp == TEST_SAID) {
if (!readByte(vm, &wordCount)) {
return false;
}
toSkip = (uint16_t)((uint16_t)wordCount * 2u);
for (i = 0u; i < toSkip; i++) {
if (!readByte(vm, &scratch)) {
return false;
}
}
return true;
}
if (testOp == 0u || testOp >= TEST_MAX) {
return false;
}
toSkip = (uint16_t)kTestArgBytes[testOp];
for (i = 0u; i < toSkip; i++) {
if (!readByte(vm, &scratch)) {
return false;
}
}
return true;
}
static void writeTextRow(AgiVmT *vm, uint8_t row, uint8_t col, const char *expanded) {
uint16_t i;
uint16_t off;
if (row >= AGI_TEXT_ROWS || col >= AGI_TEXT_COLS) {
return;
}
if (expanded == NULL) {
return;
}
// Pad text[0..col-1] with spaces. Otherwise the row starts with
// '\0' and agiTextRender skips the whole row -- display(r,c,m)
// with c>0 produces no visible text.
for (i = 0u; i < (uint16_t)col; i++) {
if (vm->textRows[row].text[i] == '\0') {
vm->textRows[row].text[i] = ' ';
}
}
off = col;
i = 0u;
while (expanded[i] != '\0' && off < AGI_TEXT_COLS) {
if (expanded[i] == '\n') {
// newline — clear rest of row, advance to next row col 0
while (off < AGI_TEXT_COLS) {
vm->textRows[row].text[off++] = ' ';
}
vm->textRows[row].text[AGI_TEXT_COLS] = '\0';
row++;
off = 0u;
if (row >= AGI_TEXT_ROWS) {
return;
}
} else {
vm->textRows[row].text[off++] = expanded[i];
}
i++;
}
vm->textRows[row].text[AGI_TEXT_COLS] = '\0';
vm->textRows[row].fg = vm->textFg;
vm->textRows[row].bg = vm->textBg;
}
// ----- Public API (alphabetical) -----
void agiVmAckPrint(AgiVmT *vm) {
vm->printModal.active = false;
vm->printModal.length = 0u;
vm->printModal.message[0] = '\0';
if (vm->haltReason == AGI_VM_HALT_PRINT_PENDING) {
vm->haltReason = AGI_VM_HALT_NONE;
}
}
void agiVmDispatchKey(AgiVmT *vm, uint8_t key1, uint8_t key2) {
uint8_t i;
for (i = 0u; i < (uint8_t)AGI_MAX_KEY_BINDINGS; i++) {
AgiKeyBindingT *b;
b = &vm->keyBindings[i];
if (b->active == 0u) {
continue;
}
if (b->key1 == key1 && b->key2 == key2) {
if (b->controller < AGI_MAX_CONTROLLERS) {
vm->controllerFired[b->controller] = 1u;
}
}
}
}
void agiVmInit(AgiVmT *vm) {
uint8_t i;
memset(vm, 0, sizeof(*vm));
vm->haltReason = AGI_VM_HALT_NONE;
vm->statusLineOn = false;
vm->statusLineRow = 0u;
vm->textFg = 15u;
vm->textBg = 0u;
vm->horizon = 36u; // canonical AGI default
vm->programControl = false;
vm->acceptInput = false;
vm->menuOpen = 0xFFu;
for (i = 0u; i < (uint8_t)AGI_TEXT_ROWS; i++) {
vm->textRows[i].text[0] = '\0';
vm->textRows[i].fg = 15u;
vm->textRows[i].bg = 0u;
}
agiObjResetAll(vm);
}
bool agiVmLoadLogic(AgiVmT *vm, const uint8_t *resourceBytes, uint16_t resourceLength) {
uint16_t bytecodeLen;
if (resourceLength < AGI_LOGIC_HDR_BYTES) {
return false;
}
bytecodeLen = (uint16_t)(resourceBytes[0] | ((uint16_t)resourceBytes[1] << 8));
if ((uint16_t)(AGI_LOGIC_HDR_BYTES + bytecodeLen) > resourceLength) {
return false;
}
vm->code = resourceBytes + AGI_LOGIC_HDR_BYTES;
vm->codeLength = bytecodeLen;
vm->pc = 0u;
vm->haltReason = AGI_VM_HALT_NONE;
vm->lastUnknownOp = 0u;
vm->newRoomId = 0u;
vm->currentLogicId = 0u;
vm->callDepth = 0u;
return true;
}
void agiVmResetToLogic(AgiVmT *vm, const uint8_t *bytecode, uint16_t bytecodeLength, uint8_t logicId) {
vm->code = bytecode;
vm->codeLength = bytecodeLength;
vm->pc = 0u;
vm->haltReason = AGI_VM_HALT_NONE;
vm->lastUnknownOp = 0u;
vm->newRoomId = 0u;
vm->currentLogicId = logicId;
vm->callDepth = 0u;
}
AgiVmHaltE agiVmRun(AgiVmT *vm) {
uint8_t op;
uint8_t a;
uint8_t b;
uint8_t c;
uint8_t d;
uint8_t e;
uint8_t f;
uint8_t g;
while (vm->haltReason == AGI_VM_HALT_NONE) {
if (!readByte(vm, &op)) {
// readByte sets haltReason = TRUNCATED on out-of-range.
return vm->haltReason;
}
switch (op) {
case OP_RETURN:
if (vm->callDepth > 0u) {
popFrame(vm);
} else {
vm->haltReason = AGI_VM_HALT_RETURN;
}
break;
case OP_INCREMENT:
if (!readByte(vm, &a)) {
return vm->haltReason;
}
// AGI spec: increment clamps at 255.
if (vm->vars[a] != 0xFFu) {
vm->vars[a] = (uint8_t)(vm->vars[a] + 1u);
}
break;
case OP_DECREMENT:
if (!readByte(vm, &a)) {
return vm->haltReason;
}
// AGI spec: decrement clamps at 0 (does NOT wrap).
// Sparkle counters like v221..v224 stay at 0 once
// assigned 0, which is how the wizard scene suppresses
// the logo-sparkle loop until sound 22 ends.
if (vm->vars[a] != 0u) {
vm->vars[a] = (uint8_t)(vm->vars[a] - 1u);
}
break;
case OP_ASSIGNN:
if (!readByte(vm, &a) || !readByte(vm, &b)) {
return vm->haltReason;
}
vm->vars[a] = b;
break;
case OP_ASSIGNV:
if (!readByte(vm, &a) || !readByte(vm, &b)) {
return vm->haltReason;
}
vm->vars[a] = vm->vars[b];
break;
case OP_ADDN:
if (!readByte(vm, &a) || !readByte(vm, &b)) {
return vm->haltReason;
}
vm->vars[a] = (uint8_t)(vm->vars[a] + b);
break;
case OP_ADDV:
if (!readByte(vm, &a) || !readByte(vm, &b)) {
return vm->haltReason;
}
vm->vars[a] = (uint8_t)(vm->vars[a] + vm->vars[b]);
break;
case OP_SUBN:
if (!readByte(vm, &a) || !readByte(vm, &b)) {
return vm->haltReason;
}
vm->vars[a] = (uint8_t)(vm->vars[a] - b);
break;
case OP_SUBV:
if (!readByte(vm, &a) || !readByte(vm, &b)) {
return vm->haltReason;
}
vm->vars[a] = (uint8_t)(vm->vars[a] - vm->vars[b]);
break;
case OP_LINDIRECTV:
// var[a] = var[var[b]]
if (!readByte(vm, &a) || !readByte(vm, &b)) {
return vm->haltReason;
}
vm->vars[a] = vm->vars[vm->vars[b]];
break;
case OP_RINDIRECT:
// var[var[a]] = var[b]
if (!readByte(vm, &a) || !readByte(vm, &b)) {
return vm->haltReason;
}
vm->vars[vm->vars[a]] = vm->vars[b];
break;
case OP_LINDIRECTN:
// var[var[a]] = b (immediate)
if (!readByte(vm, &a) || !readByte(vm, &b)) {
return vm->haltReason;
}
vm->vars[vm->vars[a]] = b;
break;
case OP_SET:
if (!readByte(vm, &a)) {
return vm->haltReason;
}
vm->flags[a] = 1u;
break;
case OP_RESET:
if (!readByte(vm, &a)) {
return vm->haltReason;
}
vm->flags[a] = 0u;
break;
case OP_TOGGLE:
if (!readByte(vm, &a)) {
return vm->haltReason;
}
vm->flags[a] = (uint8_t)(vm->flags[a] ? 0u : 1u);
break;
case OP_SETV:
if (!readByte(vm, &a)) {
return vm->haltReason;
}
vm->flags[vm->vars[a]] = 1u;
break;
case OP_RESETV:
if (!readByte(vm, &a)) {
return vm->haltReason;
}
vm->flags[vm->vars[a]] = 0u;
break;
case OP_TOGGLEV:
if (!readByte(vm, &a)) {
return vm->haltReason;
}
vm->flags[vm->vars[a]] = (uint8_t)(vm->flags[vm->vars[a]] ? 0u : 1u);
break;
case OP_IF:
if (!executeIf(vm)) {
return vm->haltReason;
}
break;
case OP_GOTO:
if (!executeGoto(vm)) {
return vm->haltReason;
}
break;
case OP_NEW_ROOM:
if (!readByte(vm, &a)) {
return vm->haltReason;
}
vm->newRoomId = a;
vm->haltReason = AGI_VM_HALT_NEW_ROOM;
jlLogF("OP_NEW_ROOM -> %u (v11=%u v12=%u)",
(unsigned)a, (unsigned)vm->vars[11], (unsigned)vm->vars[12]);
jlLogFlush();
break;
case OP_NEW_ROOM_V:
if (!readByte(vm, &a)) {
return vm->haltReason;
}
vm->newRoomId = vm->vars[a];
vm->haltReason = AGI_VM_HALT_NEW_ROOM;
jlLogF("OP_NEW_ROOM_V -> %u (v11=%u v12=%u)",
(unsigned)vm->newRoomId, (unsigned)vm->vars[11], (unsigned)vm->vars[12]);
jlLogFlush();
break;
case OP_LOAD_LOGIC:
case OP_LOAD_LOGIC_V:
// load.logic just primes the host's cache; the actual
// bytecode swap happens at OP_CALL time. We touch the
// callback so callers that prefetch can react.
if (!readByte(vm, &a)) {
return vm->haltReason;
}
if (vm->callbacks.fetchLogic != NULL) {
uint8_t logicId;
uint16_t ignore;
logicId = (op == OP_LOAD_LOGIC) ? a : vm->vars[a];
ignore = 0u;
(void)vm->callbacks.fetchLogic(vm->callbacks.ctx, logicId, &ignore);
}
break;
case OP_CALL:
case OP_CALL_V:
if (!readByte(vm, &a)) {
return vm->haltReason;
}
{
uint8_t logicId;
logicId = (op == OP_CALL) ? a : vm->vars[a];
if (!pushFrame(vm)) {
return vm->haltReason;
}
if (!enterLogic(vm, logicId)) {
// Roll back the pushed frame so RETURN doesn't
// pop garbage state.
vm->callDepth--;
return vm->haltReason;
}
}
break;
case OP_LOAD_PIC:
if (!readByte(vm, &a)) {
return vm->haltReason;
}
if (vm->callbacks.loadPic != NULL) {
vm->callbacks.loadPic(vm->callbacks.ctx, vm->vars[a]);
}
break;
case OP_DRAW_PIC:
if (!readByte(vm, &a)) {
return vm->haltReason;
}
if (vm->callbacks.drawPic != NULL) {
vm->callbacks.drawPic(vm->callbacks.ctx, vm->vars[a]);
}
break;
case OP_SHOW_PIC:
if (vm->callbacks.showPic != NULL) {
vm->callbacks.showPic(vm->callbacks.ctx);
}
break;
case OP_DISCARD_PIC:
if (!readByte(vm, &a)) {
return vm->haltReason;
}
if (vm->callbacks.discardPic != NULL) {
vm->callbacks.discardPic(vm->callbacks.ctx, vm->vars[a]);
}
break;
case OP_OVERLAY_PIC:
if (!readByte(vm, &a)) {
return vm->haltReason;
}
if (vm->callbacks.overlayPic != NULL) {
vm->callbacks.overlayPic(vm->callbacks.ctx, vm->vars[a]);
}
break;
// ----- view cache hints (no-op; host loads on demand) -----
case OP_LOAD_VIEW:
case OP_LOAD_VIEW_V:
case OP_DISCARD_VIEW:
case OP_DISCARD_VIEW_V:
if (!readByte(vm, &a)) { return vm->haltReason; }
break;
// ----- object table -----
case OP_ANIMATE_OBJ:
if (!readByte(vm, &a)) { return vm->haltReason; }
jlLogF("OP_ANIMATE_OBJ %u", (unsigned)a);
if (a < AGI_MAX_OBJECTS) {
agiObjReset(&vm->objects[a]);
vm->objects[a].flags |= AGI_OBJ_FLAG_ANIMATED | AGI_OBJ_FLAG_UPDATING | AGI_OBJ_FLAG_CYCLING;
}
break;
case OP_UNANIMATE_ALL:
jlLog("OP_UNANIMATE_ALL");
agiObjResetAll(vm);
break;
case OP_DRAW:
if (!readByte(vm, &a)) { return vm->haltReason; }
jlLogF("OP_DRAW %u", (unsigned)a);
if (a < AGI_MAX_OBJECTS) {
vm->objects[a].flags |= AGI_OBJ_FLAG_DRAWN;
}
break;
case OP_ERASE:
if (!readByte(vm, &a)) { return vm->haltReason; }
jlLogF("OP_ERASE %u", (unsigned)a);
if (a < AGI_MAX_OBJECTS) {
vm->objects[a].flags &= (uint8_t)~AGI_OBJ_FLAG_DRAWN;
}
break;
case OP_POSITION:
if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) {
return vm->haltReason;
}
jlLogF("OP_POSITION %u (%u,%u)", (unsigned)a, (unsigned)b, (unsigned)c);
if (a < AGI_MAX_OBJECTS) {
vm->objects[a].x = (int16_t)b;
vm->objects[a].y = (int16_t)c;
}
break;
case OP_POSITION_V:
case OP_REPOSITION_TO_V:
if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) {
return vm->haltReason;
}
if (a < AGI_MAX_OBJECTS) {
vm->objects[a].x = (int16_t)vm->vars[b];
vm->objects[a].y = (int16_t)vm->vars[c];
jlLogF("OP_POSITION_V/RTO_V %u <- v%u/v%u = (%d,%d)",
(unsigned)a, (unsigned)b, (unsigned)c,
(int)vm->objects[a].x, (int)vm->objects[a].y);
}
break;
case OP_GET_POSN:
if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) {
return vm->haltReason;
}
if (a < AGI_MAX_OBJECTS) {
vm->vars[b] = (uint8_t)vm->objects[a].x;
vm->vars[c] = (uint8_t)vm->objects[a].y;
}
break;
case OP_REPOSITION:
// reposition(N, vdx, vdy) — apply signed deltas (vars hold int8_t-style)
if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) {
return vm->haltReason;
}
if (a < AGI_MAX_OBJECTS) {
int8_t dx = (int8_t)vm->vars[b];
int8_t dy = (int8_t)vm->vars[c];
vm->objects[a].x = (int16_t)(vm->objects[a].x + dx);
vm->objects[a].y = (int16_t)(vm->objects[a].y + dy);
}
break;
case OP_REPOSITION_TO:
if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) {
return vm->haltReason;
}
jlLogF("OP_REPOSITION_TO %u (%u,%u)", (unsigned)a, (unsigned)b, (unsigned)c);
if (a < AGI_MAX_OBJECTS) {
vm->objects[a].x = (int16_t)b;
vm->objects[a].y = (int16_t)c;
}
break;
case OP_SET_VIEW:
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
jlLogF("OP_SET_VIEW obj=%u view=%u", (unsigned)a, (unsigned)b);
if (a < AGI_MAX_OBJECTS) {
vm->objects[a].viewId = b;
vm->objects[a].cel = 0u;
}
break;
case OP_SET_VIEW_V:
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
jlLogF("OP_SET_VIEW_V obj=%u view=v%u (=%u)",
(unsigned)a, (unsigned)b, (unsigned)vm->vars[b]);
if (a < AGI_MAX_OBJECTS) {
vm->objects[a].viewId = vm->vars[b];
vm->objects[a].cel = 0u;
}
break;
case OP_SET_LOOP:
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
jlLogF("OP_SET_LOOP obj=%u loop=%u", (unsigned)a, (unsigned)b);
if (a < AGI_MAX_OBJECTS) {
vm->objects[a].loop = b;
vm->objects[a].cel = 0u;
}
break;
case OP_SET_LOOP_V:
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
jlLogF("OP_SET_LOOP_V obj=%u loop=v%u (=%u)",
(unsigned)a, (unsigned)b, (unsigned)vm->vars[b]);
if (a < AGI_MAX_OBJECTS) {
vm->objects[a].loop = vm->vars[b];
vm->objects[a].cel = 0u;
}
break;
case OP_FIX_LOOP:
if (!readByte(vm, &a)) { return vm->haltReason; }
if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags |= AGI_OBJ_FLAG_LOOP_FIXED; }
break;
case OP_RELEASE_LOOP:
if (!readByte(vm, &a)) { return vm->haltReason; }
if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags &= (uint8_t)~AGI_OBJ_FLAG_LOOP_FIXED; }
break;
case OP_SET_CEL:
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
if (a < AGI_MAX_OBJECTS) { vm->objects[a].cel = b; vm->objects[a].cycleTick = 0u; }
break;
case OP_SET_CEL_V:
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
if (a < AGI_MAX_OBJECTS) { vm->objects[a].cel = vm->vars[b]; vm->objects[a].cycleTick = 0u; }
break;
case OP_LAST_CEL:
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
if (a < AGI_MAX_OBJECTS && vm->callbacks.viewCelCount != NULL) {
uint8_t c = vm->callbacks.viewCelCount(vm->callbacks.ctx,
vm->objects[a].viewId,
vm->objects[a].loop);
vm->vars[b] = (c > 0u) ? (uint8_t)(c - 1u) : 0u;
} else {
vm->vars[b] = 0u;
}
break;
case OP_CURRENT_CEL:
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
vm->vars[b] = (a < AGI_MAX_OBJECTS) ? vm->objects[a].cel : 0u;
break;
case OP_CURRENT_LOOP:
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
vm->vars[b] = (a < AGI_MAX_OBJECTS) ? vm->objects[a].loop : 0u;
break;
case OP_CURRENT_VIEW:
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
vm->vars[b] = (a < AGI_MAX_OBJECTS) ? vm->objects[a].viewId : 0u;
break;
case OP_NUMBER_OF_LOOPS:
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
if (a < AGI_MAX_OBJECTS && vm->callbacks.viewLoopCount != NULL) {
uint8_t c = vm->callbacks.viewLoopCount(vm->callbacks.ctx,
vm->objects[a].viewId);
vm->vars[b] = (c > 0u) ? c : 1u;
} else {
vm->vars[b] = 1u;
}
break;
case OP_SET_PRIORITY:
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
if (a < AGI_MAX_OBJECTS) {
vm->objects[a].priority = b;
vm->objects[a].flags |= AGI_OBJ_FLAG_PRI_FIXED;
}
break;
case OP_SET_PRIORITY_V:
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
if (a < AGI_MAX_OBJECTS) {
vm->objects[a].priority = vm->vars[b];
vm->objects[a].flags |= AGI_OBJ_FLAG_PRI_FIXED;
}
break;
case OP_RELEASE_PRIORITY:
if (!readByte(vm, &a)) { return vm->haltReason; }
if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags &= (uint8_t)~AGI_OBJ_FLAG_PRI_FIXED; }
break;
case OP_GET_PRIORITY:
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
vm->vars[b] = (a < AGI_MAX_OBJECTS) ? vm->objects[a].priority : 0u;
break;
case OP_STOP_UPDATE:
if (!readByte(vm, &a)) { return vm->haltReason; }
if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags &= (uint8_t)~AGI_OBJ_FLAG_UPDATING; }
break;
case OP_START_UPDATE:
if (!readByte(vm, &a)) { return vm->haltReason; }
if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags |= AGI_OBJ_FLAG_UPDATING; }
break;
case OP_FORCE_UPDATE:
if (!readByte(vm, &a)) { return vm->haltReason; }
// Force-redraw hint; we always redraw, so no-op apart from arg consume.
break;
case OP_IGNORE_HORIZON:
if (!readByte(vm, &a)) { return vm->haltReason; }
if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags |= AGI_OBJ_FLAG_HORIZON_IGN; }
break;
case OP_OBSERVE_HORIZON:
if (!readByte(vm, &a)) { return vm->haltReason; }
if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags &= (uint8_t)~AGI_OBJ_FLAG_HORIZON_IGN; }
break;
case OP_SET_HORIZON:
if (!readByte(vm, &a)) { return vm->haltReason; }
vm->horizon = a;
break;
case OP_OBJECT_ON_WATER:
case OP_OBJECT_ON_LAND:
case OP_OBJECT_ON_ANYTHING:
case OP_IGNORE_BLOCKS:
case OP_OBSERVE_BLOCKS:
if (!readByte(vm, &a)) { return vm->haltReason; }
break;
case OP_IGNORE_OBJS:
if (!readByte(vm, &a)) { return vm->haltReason; }
if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags |= AGI_OBJ_FLAG_OBJS_IGN; }
break;
case OP_OBSERVE_OBJS:
if (!readByte(vm, &a)) { return vm->haltReason; }
if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags &= (uint8_t)~AGI_OBJ_FLAG_OBJS_IGN; }
break;
case OP_DISTANCE: {
int16_t dx;
int16_t dy;
if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) {
return vm->haltReason;
}
if (a < AGI_MAX_OBJECTS && b < AGI_MAX_OBJECTS) {
dx = (int16_t)(vm->objects[a].x - vm->objects[b].x);
dy = (int16_t)(vm->objects[a].y - vm->objects[b].y);
if (dx < 0) { dx = (int16_t)(-dx); }
if (dy < 0) { dy = (int16_t)(-dy); }
vm->vars[c] = (uint8_t)((dx + dy > 255) ? 255 : (dx + dy));
} else {
vm->vars[c] = 255u;
}
break;
}
case OP_BLOCK:
// 4 args: (x1, y1, x2, y2)
if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d)) {
return vm->haltReason;
}
break;
case OP_UNBLOCK:
break;
// ----- animation cycling -----
case OP_STOP_CYCLING:
if (!readByte(vm, &a)) { return vm->haltReason; }
if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags &= (uint8_t)~AGI_OBJ_FLAG_CYCLING; }
break;
case OP_START_CYCLING:
if (!readByte(vm, &a)) { return vm->haltReason; }
if (a < AGI_MAX_OBJECTS) { vm->objects[a].flags |= AGI_OBJ_FLAG_CYCLING; }
break;
case OP_NORMAL_CYCLE:
if (!readByte(vm, &a)) { return vm->haltReason; }
if (a < AGI_MAX_OBJECTS) {
vm->objects[a].cycleMode = AGI_CYCLE_NORMAL;
vm->objects[a].endOfLoopFlag = 0u;
vm->objects[a].flags |= AGI_OBJ_FLAG_CYCLING;
}
break;
case OP_END_OF_LOOP:
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
if (a < AGI_MAX_OBJECTS) {
vm->objects[a].cycleMode = AGI_CYCLE_END_LOOP;
vm->objects[a].endOfLoopFlag = b;
vm->objects[a].flags |= AGI_OBJ_FLAG_CYCLING;
vm->flags[b] = 0u;
}
break;
case OP_REVERSE_CYCLE:
if (!readByte(vm, &a)) { return vm->haltReason; }
if (a < AGI_MAX_OBJECTS) {
vm->objects[a].cycleMode = AGI_CYCLE_REVERSE;
vm->objects[a].endOfLoopFlag = 0u;
vm->objects[a].flags |= AGI_OBJ_FLAG_CYCLING;
}
break;
case OP_REVERSE_LOOP:
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
if (a < AGI_MAX_OBJECTS) {
vm->objects[a].cycleMode = AGI_CYCLE_REV_LOOP;
vm->objects[a].endOfLoopFlag = b;
vm->objects[a].flags |= AGI_OBJ_FLAG_CYCLING;
vm->flags[b] = 0u;
}
break;
case OP_CYCLE_TIME:
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
if (a < AGI_MAX_OBJECTS) {
vm->objects[a].cycleTime = vm->vars[b];
if (vm->objects[a].cycleTime == 0u) { vm->objects[a].cycleTime = 1u; }
vm->objects[a].cycleTick = 0u;
}
break;
// ----- motion -----
case OP_STOP_MOTION:
if (!readByte(vm, &a)) { return vm->haltReason; }
if (a < AGI_MAX_OBJECTS) { vm->objects[a].direction = 0u; }
break;
case OP_START_MOTION:
if (!readByte(vm, &a)) { return vm->haltReason; }
if (a < AGI_MAX_OBJECTS) {
if (a == 0u) {
vm->objects[0].direction = vm->vars[ENG_VAR_EGO_DIR];
} else {
// Re-allow motion; direction stays whatever it was.
}
}
break;
case OP_STEP_SIZE:
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
if (a < AGI_MAX_OBJECTS) {
vm->objects[a].stepSize = vm->vars[b];
if (vm->objects[a].stepSize == 0u) { vm->objects[a].stepSize = 1u; }
}
break;
case OP_STEP_TIME:
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
if (a < AGI_MAX_OBJECTS) {
vm->objects[a].stepTime = vm->vars[b];
if (vm->objects[a].stepTime == 0u) { vm->objects[a].stepTime = 1u; }
vm->objects[a].stepTick = 0u;
}
break;
case OP_MOVE_OBJ:
// n, x, y, vstep, flag
if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d) || !readByte(vm, &e)) {
return vm->haltReason;
}
jlLogF("OP_MOVE_OBJ %u -> (%u,%u) step=%u doneFlag=%u",
(unsigned)a, (unsigned)b, (unsigned)c, (unsigned)d, (unsigned)e);
if (a < AGI_MAX_OBJECTS) {
vm->objects[a].targetX = (int16_t)b;
vm->objects[a].targetY = (int16_t)c;
vm->objects[a].moveStepBackup = vm->objects[a].stepSize;
vm->objects[a].stepSize = (d == 0u) ? vm->objects[a].stepSize : d;
vm->objects[a].moveDoneFlag = e;
vm->objects[a].motionType = AGI_MOTION_MOVE_OBJ;
vm->flags[e] = 0u;
}
break;
case OP_MOVE_OBJ_V:
if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d) || !readByte(vm, &e)) {
return vm->haltReason;
}
jlLogF("OP_MOVE_OBJ_V %u -> v%u/v%u=(%u,%u) vstep=v%u=%u doneFlag=%u",
(unsigned)a, (unsigned)b, (unsigned)c,
(unsigned)vm->vars[b], (unsigned)vm->vars[c],
(unsigned)d, (unsigned)vm->vars[d], (unsigned)e);
if (a < AGI_MAX_OBJECTS) {
vm->objects[a].targetX = (int16_t)vm->vars[b];
vm->objects[a].targetY = (int16_t)vm->vars[c];
vm->objects[a].moveStepBackup = vm->objects[a].stepSize;
vm->objects[a].stepSize = (vm->vars[d] == 0u) ? vm->objects[a].stepSize : vm->vars[d];
vm->objects[a].moveDoneFlag = e;
vm->objects[a].motionType = AGI_MOTION_MOVE_OBJ;
vm->flags[e] = 0u;
}
break;
case OP_FOLLOW_EGO:
if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) {
return vm->haltReason;
}
if (a < AGI_MAX_OBJECTS) {
vm->objects[a].followStep = b;
vm->objects[a].followDoneFlag = c;
vm->objects[a].motionType = AGI_MOTION_FOLLOW_EGO;
}
break;
case OP_WANDER:
if (!readByte(vm, &a)) { return vm->haltReason; }
if (a < AGI_MAX_OBJECTS) {
vm->objects[a].motionType = AGI_MOTION_WANDER;
vm->objects[a].wanderTick = 0u;
}
break;
case OP_NORMAL_MOTION:
if (!readByte(vm, &a)) { return vm->haltReason; }
if (a < AGI_MAX_OBJECTS) { vm->objects[a].motionType = AGI_MOTION_NORMAL; }
break;
case OP_SET_DIR:
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
jlLogF("OP_SET_DIR obj=%u dir=v%u (=%u)",
(unsigned)a, (unsigned)b, (unsigned)vm->vars[b]);
if (a < AGI_MAX_OBJECTS) {
vm->objects[a].direction = vm->vars[b];
if (a == 0u) { vm->vars[ENG_VAR_EGO_DIR] = vm->objects[0].direction; }
}
break;
case OP_GET_DIR:
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
vm->vars[b] = (a < AGI_MAX_OBJECTS) ? vm->objects[a].direction : 0u;
break;
// ----- inventory (state only; reads OBJECT data via host later) -----
case OP_GET:
case OP_DROP:
if (!readByte(vm, &a)) { return vm->haltReason; }
break;
case OP_GET_V:
if (!readByte(vm, &a)) { return vm->haltReason; }
break;
case OP_PUT:
case OP_PUT_V:
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
break;
case OP_GET_ROOM_V:
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
vm->vars[b] = 0u;
break;
// ----- sound (no-op; complete callback fires immediately) -----
case OP_LOAD_SOUND:
if (!readByte(vm, &a)) { return vm->haltReason; }
break;
case OP_SOUND:
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
jlLogF("OP_SOUND id=%u flag=%u (prev playing=%u prevFlag=%u)",
(unsigned)a, (unsigned)b,
(unsigned)vm->soundPlaying, (unsigned)vm->soundDoneFlag);
jlLogFlush();
// AGI v2 sound(N, F): both args are LITERAL.
// (No _V variant exists for sound; first arg is
// always the sound resource id, second is the
// completion flag id.) Don't index through vars.
//
// If a previous sound is still armed, fire its
// done flag now (the script's about to wait on the
// new sound; the old one's completion is the new
// sound's start, not whenever playback ended).
if (vm->soundPlaying) {
if (vm->soundDoneFlag != 0u) {
vm->flags[vm->soundDoneFlag] = 1u;
}
vm->flags[9] = 1u;
vm->soundPlaying = false;
}
if (vm->callbacks.playSound != NULL && vm->callbacks.isPlayingSound != NULL) {
vm->soundDoneFlag = b;
vm->flags[9] = 0u;
// ScummVM startSound does setFlagOrVar(_endflag,
// false) here -- without it the script's
// `while (!flag[b]) wait` loop sees a stale 1
// from a previous sound() and skips waiting.
vm->flags[b] = 0u;
vm->callbacks.playSound(vm->callbacks.ctx, a);
vm->soundPlaying = true;
} else {
// Legacy fire-immediately when the host can't
// report playback state. Keeps non-AGI uses of
// this VM (and any test harness without audio)
// from blocking forever on a wait-for-sound flag.
vm->flags[b] = 1u;
vm->flags[9] = 1u;
}
break;
case OP_STOP_SOUND:
jlLogF("OP_STOP_SOUND (playing=%u flag=%u)",
(unsigned)vm->soundPlaying,
(unsigned)vm->soundDoneFlag);
jlLogFlush();
if (vm->callbacks.stopSound != NULL) {
vm->callbacks.stopSound(vm->callbacks.ctx);
}
if (vm->soundPlaying) {
if (vm->soundDoneFlag != 0u) {
vm->flags[vm->soundDoneFlag] = 1u;
}
vm->flags[9] = 1u;
vm->soundPlaying = false;
}
break;
// ----- text -----
case OP_PRINT: {
char expanded[AGI_PRINT_MAX_LEN + 1u];
if (!readByte(vm, &a)) { return vm->haltReason; }
expandMessage(vm, fetchMessageOrEmpty(vm, vm->currentLogicId, a), expanded, sizeof(expanded));
openPrintModal(vm, expanded);
return vm->haltReason;
}
case OP_PRINT_V: {
char expanded[AGI_PRINT_MAX_LEN + 1u];
if (!readByte(vm, &a)) { return vm->haltReason; }
expandMessage(vm, fetchMessageOrEmpty(vm, vm->currentLogicId, vm->vars[a]), expanded, sizeof(expanded));
openPrintModal(vm, expanded);
return vm->haltReason;
}
case OP_DISPLAY: {
char expanded[AGI_PRINT_MAX_LEN + 1u];
if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) {
return vm->haltReason;
}
expandMessage(vm, fetchMessageOrEmpty(vm, vm->currentLogicId, c), expanded, sizeof(expanded));
jlLogF("OP_DISPLAY row=%u col=%u msg=%u text=\"%s\"",
(unsigned)a, (unsigned)b, (unsigned)c, expanded);
writeTextRow(vm, a, b, expanded);
break;
}
case OP_DISPLAY_V: {
char expanded[AGI_PRINT_MAX_LEN + 1u];
if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) {
return vm->haltReason;
}
expandMessage(vm, fetchMessageOrEmpty(vm, vm->currentLogicId, vm->vars[c]), expanded, sizeof(expanded));
writeTextRow(vm, vm->vars[a], vm->vars[b], expanded);
break;
}
case OP_CLEAR_LINES: {
uint8_t r;
if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) {
return vm->haltReason;
}
jlLogF("OP_CLEAR_LINES rows=%u..%u bg=%u",
(unsigned)a, (unsigned)b, (unsigned)c);
if (a >= AGI_TEXT_ROWS) a = AGI_TEXT_ROWS - 1u;
if (b >= AGI_TEXT_ROWS) b = AGI_TEXT_ROWS - 1u;
for (r = a; r <= b; r++) {
vm->textRows[r].text[0] = '\0';
vm->textRows[r].bg = c;
}
break;
}
case OP_CLEAR_TEXT_RECT: {
uint8_t r;
if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d) || !readByte(vm, &e)) {
return vm->haltReason;
}
if (c >= AGI_TEXT_ROWS) c = AGI_TEXT_ROWS - 1u;
for (r = a; r <= c; r++) {
vm->textRows[r].text[0] = '\0';
vm->textRows[r].bg = e;
}
break;
}
case OP_TEXT_SCREEN:
case OP_GRAPHICS:
break;
case OP_SET_CURSOR_CHAR:
if (!readByte(vm, &a)) { return vm->haltReason; }
break;
case OP_SET_TEXT_ATTRIBUTE:
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
vm->textFg = a;
vm->textBg = b;
break;
case OP_SHAKE_SCREEN:
if (!readByte(vm, &a)) { return vm->haltReason; }
break;
case OP_CONFIGURE_SCREEN:
if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) {
return vm->haltReason;
}
break;
case OP_STATUS_LINE_ON:
vm->statusLineOn = true;
break;
case OP_STATUS_LINE_OFF:
vm->statusLineOn = false;
break;
case OP_SET_STRING: {
char expanded[AGI_STRING_LEN + 1u];
uint8_t i;
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
if (a < AGI_MAX_STRINGS) {
expandMessage(vm, fetchMessageOrEmpty(vm, vm->currentLogicId, b), expanded, sizeof(expanded));
for (i = 0u; i < (uint8_t)AGI_STRING_LEN; i++) {
vm->strings[a][i] = expanded[i];
if (expanded[i] == '\0') break;
}
vm->strings[a][AGI_STRING_LEN] = '\0';
}
break;
}
case OP_GET_STRING:
// get.string(s, msgId, row, col, max) — parser input;
// no parser yet, just consume args.
if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d) || !readByte(vm, &e)) {
return vm->haltReason;
}
break;
case OP_WORD_TO_STRING:
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
break;
case OP_PARSE:
if (!readByte(vm, &a)) { return vm->haltReason; }
break;
case OP_GET_NUM:
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
vm->vars[b] = 0u;
break;
// ----- input -----
case OP_PREVENT_INPUT:
vm->acceptInput = false;
break;
case OP_ACCEPT_INPUT:
vm->acceptInput = true;
break;
case OP_SET_KEY: {
uint8_t i;
if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) {
return vm->haltReason;
}
for (i = 0u; i < (uint8_t)AGI_MAX_KEY_BINDINGS; i++) {
if (vm->keyBindings[i].active == 0u) {
vm->keyBindings[i].key1 = a;
vm->keyBindings[i].key2 = b;
vm->keyBindings[i].controller = c;
vm->keyBindings[i].active = 1u;
break;
}
}
break;
}
// ----- picture composition -----
case OP_ADD_TO_PIC:
if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d) || !readByte(vm, &e) || !readByte(vm, &f) || !readByte(vm, &g)) {
return vm->haltReason;
}
if (vm->callbacks.addToPic != NULL) {
vm->callbacks.addToPic(vm->callbacks.ctx, a, b, c, d, e, f, g);
}
break;
case OP_ADD_TO_PIC_V:
if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d) || !readByte(vm, &e) || !readByte(vm, &f) || !readByte(vm, &g)) {
return vm->haltReason;
}
if (vm->callbacks.addToPic != NULL) {
vm->callbacks.addToPic(vm->callbacks.ctx,
vm->vars[a], vm->vars[b], vm->vars[c],
vm->vars[d], vm->vars[e], vm->vars[f], vm->vars[g]);
}
break;
// ----- misc state / no-ops -----
case OP_STATUS:
break;
case OP_SAVE_GAME:
case OP_RESTORE_GAME:
break;
case OP_INIT_DISK:
break;
case OP_RESTART_GAME:
vm->haltReason = AGI_VM_HALT_QUIT;
return vm->haltReason;
case OP_SHOW_OBJ:
if (!readByte(vm, &a)) { return vm->haltReason; }
break;
case OP_RANDOM:
if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) {
return vm->haltReason;
}
{
uint16_t span;
span = (uint16_t)((b > a) ? (b - a + 1u) : 1u);
vm->vars[c] = (uint8_t)(a + (uint8_t)(jlRandomRange(span)));
}
break;
case OP_PROGRAM_CONTROL:
vm->programControl = true;
break;
case OP_PLAYER_CONTROL:
vm->programControl = false;
break;
case OP_OBJ_STATUS_V:
if (!readByte(vm, &a)) { return vm->haltReason; }
break;
case OP_QUIT:
if (!readByte(vm, &a)) { return vm->haltReason; }
vm->haltReason = AGI_VM_HALT_QUIT;
return vm->haltReason;
case OP_SHOW_MEM:
break;
case OP_PAUSE:
openPrintModal(vm, "Game Paused.\n\nPress ENTER to continue.");
return vm->haltReason;
case OP_ECHO_LINE:
case OP_CANCEL_LINE:
break;
case OP_INIT_JOY:
break;
case OP_TOGGLE_MONITOR:
break;
case OP_VERSION:
break;
case OP_SCRIPT_SIZE:
if (!readByte(vm, &a)) { return vm->haltReason; }
break;
case OP_SET_GAME_ID:
if (!readByte(vm, &a)) { return vm->haltReason; }
break;
case OP_LOG:
if (!readByte(vm, &a)) { return vm->haltReason; }
break;
case OP_SET_SCAN_START:
case OP_RESET_SCAN_START:
break;
case OP_TRACE_ON:
break;
case OP_TRACE_INFO:
if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c)) {
return vm->haltReason;
}
break;
case OP_PRINT_AT: {
char expanded[AGI_PRINT_MAX_LEN + 1u];
if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d)) {
return vm->haltReason;
}
expandMessage(vm, fetchMessageOrEmpty(vm, vm->currentLogicId, a), expanded, sizeof(expanded));
openPrintModal(vm, expanded);
return vm->haltReason;
}
case OP_PRINT_AT_V: {
char expanded[AGI_PRINT_MAX_LEN + 1u];
if (!readByte(vm, &a) || !readByte(vm, &b) || !readByte(vm, &c) || !readByte(vm, &d)) {
return vm->haltReason;
}
expandMessage(vm, fetchMessageOrEmpty(vm, vm->currentLogicId, vm->vars[a]), expanded, sizeof(expanded));
openPrintModal(vm, expanded);
return vm->haltReason;
}
case OP_SET_UPPER_LEFT:
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
break;
// ----- menus -----
case OP_SET_MENU:
if (!readByte(vm, &a)) { return vm->haltReason; }
if (vm->menuCount < AGI_MAX_MENUS) {
vm->menus[vm->menuCount].nameMsgId = a;
vm->menus[vm->menuCount].itemCount = 0u;
vm->menuCount++;
}
break;
case OP_SET_MENU_ITEM:
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
if (vm->menuCount > 0u) {
AgiMenuT *m = &vm->menus[vm->menuCount - 1u];
if (m->itemCount < AGI_MAX_MENU_ITEMS) {
m->items[m->itemCount].messageId = a;
m->items[m->itemCount].controller = b;
m->items[m->itemCount].enabled = 1u;
m->itemCount++;
}
}
break;
case OP_SUBMIT_MENU:
break;
case OP_ENABLE_ITEM:
case OP_DISABLE_ITEM: {
uint8_t m;
uint8_t i;
uint8_t enable;
if (!readByte(vm, &a)) { return vm->haltReason; }
enable = (op == OP_ENABLE_ITEM) ? 1u : 0u;
for (m = 0u; m < vm->menuCount; m++) {
for (i = 0u; i < vm->menus[m].itemCount; i++) {
if (vm->menus[m].items[i].controller == a) {
vm->menus[m].items[i].enabled = enable;
}
}
}
break;
}
case OP_MENU_INPUT:
// Real menu UI is not wired; just no-op so scripts don't stall.
break;
case OP_SHOW_OBJ_V:
if (!readByte(vm, &a)) { return vm->haltReason; }
break;
case OP_OPEN_DIALOGUE:
case OP_CLOSE_DIALOGUE:
break;
case OP_MUL_N:
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
vm->vars[a] = (uint8_t)(vm->vars[a] * b);
break;
case OP_MUL_V:
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
vm->vars[a] = (uint8_t)(vm->vars[a] * vm->vars[b]);
break;
case OP_DIV_N:
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
if (b != 0u) {
vm->vars[a] = (uint8_t)(vm->vars[a] / b);
}
break;
case OP_DIV_V:
if (!readByte(vm, &a) || !readByte(vm, &b)) { return vm->haltReason; }
if (vm->vars[b] != 0u) {
vm->vars[a] = (uint8_t)(vm->vars[a] / vm->vars[b]);
}
break;
case OP_CLOSE_WINDOW:
vm->printModal.active = false;
break;
case OP_SET_SIMPLE:
if (!readByte(vm, &a)) { return vm->haltReason; }
break;
default: {
uint8_t argBytes;
uint8_t i;
uint8_t scratch;
argBytes = kActionArgBytes[op];
if (argBytes == ACTION_UNKNOWN) {
vm->lastUnknownOp = op;
vm->haltReason = AGI_VM_HALT_UNKNOWN_OP;
vm->pc = (uint16_t)(vm->pc - 1u);
break;
}
for (i = 0u; i < argBytes; i++) {
if (!readByte(vm, &scratch)) {
return vm->haltReason;
}
}
break;
}
}
}
return vm->haltReason;
}
void agiVmSetCallbacks(AgiVmT *vm, const AgiVmCallbacksT *callbacks) {
if (callbacks == NULL) {
memset(&vm->callbacks, 0, sizeof(vm->callbacks));
} else {
vm->callbacks = *callbacks;
}
}
void agiVmTickSeconds(AgiVmT *vm, uint8_t seconds) {
uint16_t s;
uint16_t m;
uint16_t h;
uint16_t carry;
if (seconds == 0u) {
return;
}
// Sound completion is no longer driven from here -- the per-cycle
// poll in agiVmTickAnimation reads callbacks.isPlayingSound and
// fires the done flags on the live host->silent edge. Keeps the
// VM in lock-step with what the user actually hears.
s = (uint16_t)((uint16_t)vm->vars[11] + (uint16_t)seconds);
carry = (uint16_t)(s / 60u);
vm->vars[11] = (uint8_t)(s % 60u);
if (carry == 0u) {
return;
}
m = (uint16_t)((uint16_t)vm->vars[12] + carry);
carry = (uint16_t)(m / 60u);
vm->vars[12] = (uint8_t)(m % 60u);
if (carry == 0u) {
return;
}
h = (uint16_t)((uint16_t)vm->vars[13] + carry);
vm->vars[13] = (uint8_t)(h % 24u);
}