2300 lines
84 KiB
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);
|
|
}
|