joeyagi/agiVm.c
2026-10-06 16:59:41 -05:00

1483 lines
48 KiB
C

// The LOGIC interpreter (AGI specification chapters 4 and 6).
//
// A logic resource is a 16-bit code length, the code, then the messages:
// a count, an end offset and an offset per message (counted from the byte
// after the count), the text XORed with "Avis Durgan" running across the
// whole block. Messages are decrypted when a logic loads so they can be
// used in place.
//
// Code bytes below 0xFC are commands; 0xFF opens an if, whose tests run to
// the next 0xFF and are followed by the 16-bit distance to skip when they
// are false; 0xFE is a jump by a signed 16-bit distance (else and goto).
// Inside an if, 0xFD negates the next test and 0xFC brackets an or-group.
// Tests are evaluated left to right and stop as soon as the answer is
// known, as in Sierra's interpreter -- said() and have.key() have side
// effects, so this matters.
//
// call() runs a logic as a subroutine from its scan start (set.scan.start
// remembers where); a logic call() had to load counts as unloaded again
// after, forgetting its scan start (its bytes stay cached until new.room
// frees every logic but 0). new.room, restart, restore and quit stop all
// logic at once (gAgi->abortLogic).
#include "agi.h"
#include <stddef.h>
#include <string.h>
#include "joey/core.h"
#include "joey/debug.h"
#define LOGIC_HEADER_BYTES 2u
#define MSG_HEADER_BYTES 3u
#define MSG_OFFSET_BYTES 2u
#define MSG_OFFSET_BASE 1u
#define SAID_WORD_BYTES 2u
#define CODE_IF 0xFFu
#define CODE_GOTO 0xFEu
#define CODE_NOT 0xFDu
#define CODE_OR 0xFCu
#define MAX_ARGS AGI_SCRIPT_ARGS
#define MESSAGE_MAX 512u
#define DISTANCE_FAR 255u
#define DISTANCE_MAX 254u
// Logics calling logics nest only a few deep; a deeper chain is a logic
// calling itself round in a loop (KQ3's room 43 entered the wrong way),
// which Sierra's interpreter hangs on. JoeyAGI stops the chain instead.
#define CALL_DEPTH_MAX 32u
// Action commands, by number (specification 6.2).
#define CMD_RETURN 0x00u
#define CMD_INCREMENT 0x01u
#define CMD_DECREMENT 0x02u
#define CMD_ASSIGNN 0x03u
#define CMD_ASSIGNV 0x04u
#define CMD_ADDN 0x05u
#define CMD_ADDV 0x06u
#define CMD_SUBN 0x07u
#define CMD_SUBV 0x08u
#define CMD_LINDIRECTV 0x09u
#define CMD_RINDIRECT 0x0Au
#define CMD_LINDIRECTN 0x0Bu
#define CMD_SET 0x0Cu
#define CMD_RESET 0x0Du
#define CMD_TOGGLE 0x0Eu
#define CMD_SET_V 0x0Fu
#define CMD_RESET_V 0x10u
#define CMD_TOGGLE_V 0x11u
#define CMD_NEW_ROOM 0x12u
#define CMD_NEW_ROOM_V 0x13u
#define CMD_LOAD_LOGICS 0x14u
#define CMD_LOAD_LOGICS_V 0x15u
#define CMD_CALL 0x16u
#define CMD_CALL_V 0x17u
#define CMD_LOAD_PIC 0x18u
#define CMD_DRAW_PIC 0x19u
#define CMD_SHOW_PIC 0x1Au
#define CMD_DISCARD_PIC 0x1Bu
#define CMD_OVERLAY_PIC 0x1Cu
#define CMD_SHOW_PRI_SCREEN 0x1Du
#define CMD_LOAD_VIEW 0x1Eu
#define CMD_LOAD_VIEW_V 0x1Fu
#define CMD_DISCARD_VIEW 0x20u
#define CMD_ANIMATE_OBJ 0x21u
#define CMD_UNANIMATE_ALL 0x22u
#define CMD_DRAW 0x23u
#define CMD_ERASE 0x24u
#define CMD_POSITION 0x25u
#define CMD_POSITION_V 0x26u
#define CMD_GET_POSN 0x27u
#define CMD_REPOSITION 0x28u
#define CMD_SET_VIEW 0x29u
#define CMD_SET_VIEW_V 0x2Au
#define CMD_SET_LOOP 0x2Bu
#define CMD_SET_LOOP_V 0x2Cu
#define CMD_FIX_LOOP 0x2Du
#define CMD_RELEASE_LOOP 0x2Eu
#define CMD_SET_CEL 0x2Fu
#define CMD_SET_CEL_V 0x30u
#define CMD_LAST_CEL 0x31u
#define CMD_CURRENT_CEL 0x32u
#define CMD_CURRENT_LOOP 0x33u
#define CMD_CURRENT_VIEW 0x34u
#define CMD_NUMBER_OF_LOOPS 0x35u
#define CMD_SET_PRIORITY 0x36u
#define CMD_SET_PRIORITY_V 0x37u
#define CMD_RELEASE_PRIORITY 0x38u
#define CMD_GET_PRIORITY 0x39u
#define CMD_STOP_UPDATE 0x3Au
#define CMD_START_UPDATE 0x3Bu
#define CMD_FORCE_UPDATE 0x3Cu
#define CMD_IGNORE_HORIZON 0x3Du
#define CMD_OBSERVE_HORIZON 0x3Eu
#define CMD_SET_HORIZON 0x3Fu
#define CMD_OBJECT_ON_WATER 0x40u
#define CMD_OBJECT_ON_LAND 0x41u
#define CMD_OBJECT_ON_ANYTHING 0x42u
#define CMD_IGNORE_OBJS 0x43u
#define CMD_OBSERVE_OBJS 0x44u
#define CMD_DISTANCE 0x45u
#define CMD_STOP_CYCLING 0x46u
#define CMD_START_CYCLING 0x47u
#define CMD_NORMAL_CYCLE 0x48u
#define CMD_END_OF_LOOP 0x49u
#define CMD_REVERSE_CYCLE 0x4Au
#define CMD_REVERSE_LOOP 0x4Bu
#define CMD_CYCLE_TIME 0x4Cu
#define CMD_STOP_MOTION 0x4Du
#define CMD_START_MOTION 0x4Eu
#define CMD_STEP_SIZE 0x4Fu
#define CMD_STEP_TIME 0x50u
#define CMD_MOVE_OBJ 0x51u
#define CMD_MOVE_OBJ_V 0x52u
#define CMD_FOLLOW_EGO 0x53u
#define CMD_WANDER 0x54u
#define CMD_NORMAL_MOTION 0x55u
#define CMD_SET_DIR 0x56u
#define CMD_GET_DIR 0x57u
#define CMD_IGNORE_BLOCKS 0x58u
#define CMD_OBSERVE_BLOCKS 0x59u
#define CMD_BLOCK 0x5Au
#define CMD_UNBLOCK 0x5Bu
#define CMD_GET 0x5Cu
#define CMD_GET_V 0x5Du
#define CMD_DROP 0x5Eu
#define CMD_PUT 0x5Fu
#define CMD_PUT_V 0x60u
#define CMD_GET_ROOM_V 0x61u
#define CMD_LOAD_SOUND 0x62u
#define CMD_SOUND 0x63u
#define CMD_STOP_SOUND 0x64u
#define CMD_PRINT 0x65u
#define CMD_PRINT_V 0x66u
#define CMD_DISPLAY 0x67u
#define CMD_DISPLAY_V 0x68u
#define CMD_CLEAR_LINES 0x69u
#define CMD_TEXT_SCREEN 0x6Au
#define CMD_GRAPHICS 0x6Bu
#define CMD_SET_CURSOR_CHAR 0x6Cu
#define CMD_SET_TEXT_ATTRIBUTE 0x6Du
#define CMD_SHAKE_SCREEN 0x6Eu
#define CMD_CONFIGURE_SCREEN 0x6Fu
#define CMD_STATUS_LINE_ON 0x70u
#define CMD_STATUS_LINE_OFF 0x71u
#define CMD_SET_STRING 0x72u
#define CMD_GET_STRING 0x73u
#define CMD_WORD_TO_STRING 0x74u
#define CMD_PARSE 0x75u
#define CMD_GET_NUM 0x76u
#define CMD_PREVENT_INPUT 0x77u
#define CMD_ACCEPT_INPUT 0x78u
#define CMD_SET_KEY 0x79u
#define CMD_ADD_TO_PIC 0x7Au
#define CMD_ADD_TO_PIC_V 0x7Bu
#define CMD_STATUS 0x7Cu
#define CMD_SAVE_GAME 0x7Du
#define CMD_RESTORE_GAME 0x7Eu
#define CMD_INIT_DISK 0x7Fu
#define CMD_RESTART_GAME 0x80u
#define CMD_SHOW_OBJ 0x81u
#define CMD_RANDOM 0x82u
#define CMD_PROGRAM_CONTROL 0x83u
#define CMD_PLAYER_CONTROL 0x84u
#define CMD_OBJ_STATUS_V 0x85u
#define CMD_QUIT 0x86u
#define CMD_SHOW_MEM 0x87u
#define CMD_PAUSE 0x88u
#define CMD_ECHO_LINE 0x89u
#define CMD_CANCEL_LINE 0x8Au
#define CMD_INIT_JOY 0x8Bu
#define CMD_TOGGLE_MONITOR 0x8Cu
#define CMD_VERSION 0x8Du
#define CMD_SCRIPT_SIZE 0x8Eu
#define CMD_SET_GAME_ID 0x8Fu
#define CMD_LOG 0x90u
#define CMD_SET_SCAN_START 0x91u
#define CMD_RESET_SCAN_START 0x92u
#define CMD_REPOSITION_TO 0x93u
#define CMD_REPOSITION_TO_V 0x94u
#define CMD_TRACE_ON 0x95u
#define CMD_TRACE_INFO 0x96u
#define CMD_PRINT_AT 0x97u
#define CMD_PRINT_AT_V 0x98u
#define CMD_DISCARD_VIEW_V 0x99u
#define CMD_CLEAR_TEXT_RECT 0x9Au
#define CMD_SET_UPPER_LEFT 0x9Bu
#define CMD_SET_MENU 0x9Cu
#define CMD_SET_MENU_ITEM 0x9Du
#define CMD_SUBMIT_MENU 0x9Eu
#define CMD_ENABLE_ITEM 0x9Fu
#define CMD_DISABLE_ITEM 0xA0u
#define CMD_MENU_INPUT 0xA1u
#define CMD_SHOW_OBJ_V 0xA2u
#define CMD_OPEN_DIALOGUE 0xA3u
#define CMD_CLOSE_DIALOGUE 0xA4u
#define CMD_MUL_N 0xA5u
#define CMD_MUL_V 0xA6u
#define CMD_DIV_N 0xA7u
#define CMD_DIV_V 0xA8u
#define CMD_CLOSE_WINDOW 0xA9u
#define CMD_COUNT 0xAAu
// Test commands (specification 6.2).
#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_ISSET_V 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_COUNT 0x13u
typedef struct {
const uint8_t *code;
uint16_t length;
uint16_t pc;
uint8_t logic;
bool done;
} VmFrameT;
// ----- Prototypes -----
static void cmdMisc(VmFrameT *f, uint8_t op, const uint8_t *a);
static void cmdObject(VmFrameT *f, uint8_t op, const uint8_t *a);
static void cmdText(VmFrameT *f, uint8_t op, const uint8_t *a);
static void cmdVars(VmFrameT *f, uint8_t op, const uint8_t *a);
static uint16_t codeLength(const uint8_t *raw);
static bool compareStrings(const char *a, const char *b);
static void decryptMessages(uint8_t *raw, uint16_t length);
static bool evalCondition(VmFrameT *f);
static bool evalTest(VmFrameT *f, uint8_t test);
static void execute(VmFrameT *f);
static AgiObjectT *object(uint8_t n);
static bool posnTest(uint8_t test, const uint8_t *a);
static void printMessage(const char *msg, int16_t row, int16_t col, uint8_t width);
static uint8_t readByte(VmFrameT *f);
static uint16_t readWord(VmFrameT *f);
static void redrawObjects(uint8_t n, uint16_t setFlags, uint16_t clearFlags);
static bool saidMatch(VmFrameT *f);
static void skipTest(VmFrameT *f, uint8_t test);
// ----- Command tables -----
// Argument bytes per action command (specification 6.2).
static const uint8_t kActionArgs[CMD_COUNT] = {
0, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, // 0x00
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, // 0x10
1, 1, 0, 1, 1, 3, 3, 3, 3, 2, 2, 2, 2, 1, 1, 2, // 0x20
2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 1, 1, 1, 1, 1, 1, // 0x30
1, 1, 1, 1, 1, 3, 1, 1, 1, 2, 1, 2, 2, 1, 1, 2, // 0x40
2, 5, 5, 3, 1, 1, 2, 2, 1, 1, 4, 0, 1, 1, 1, 2, // 0x50
2, 2, 1, 2, 0, 1, 1, 3, 3, 3, 0, 0, 1, 2, 1, 3, // 0x60
0, 0, 2, 5, 2, 1, 2, 0, 0, 3, 7, 7, 0, 0, 0, 0, // 0x70
0, 1, 3, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, // 0x80
1, 0, 0, 3, 3, 0, 3, 4, 4, 1, 5, 2, 1, 2, 0, 1, // 0x90
1, 0, 1, 0, 0, 2, 2, 2, 2, 0 // 0xA0
};
// Argument bytes per test command (said is variable).
static const uint8_t kTestArgs[TEST_COUNT] = {
0, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 5, 1, 0, 0, 2, 5, 5, 5
};
// ----- Module state -----
// How deep call() has nested (a logic that calls itself forever stops).
static uint8_t gCallDepth;
// ----- Internal helpers (alphabetical) -----
// The rest: system, screen, menu and arithmetic commands (0x7C..0xA9).
static void cmdMisc(VmFrameT *f, uint8_t op, const uint8_t *a) {
switch (op) {
case CMD_STATUS:
textInventory();
break;
case CMD_SAVE_GAME:
saveGame();
break;
case CMD_RESTORE_GAME:
if (restoreGame()) {
gAgi->abortLogic = true;
}
break;
case CMD_RESTART_GAME:
agiRestartGame();
break;
case CMD_SHOW_OBJ:
textShowObject(a[0]);
break;
case CMD_SHOW_OBJ_V:
textShowObject(gAgi->vars[a[0]]);
break;
case CMD_RANDOM:
if (a[1] >= a[0]) {
gAgi->vars[a[2]] = (uint8_t)(a[0] + jlRandomRange((uint16_t)(a[1] - a[0] + 1u)));
} else {
gAgi->vars[a[2]] = a[0];
}
break;
case CMD_PROGRAM_CONTROL:
gAgi->playerControl = false;
break;
case CMD_PLAYER_CONTROL:
gAgi->playerControl = true;
gAgi->objects[AGI_EGO].motion = AGI_MOTION_NORMAL;
break;
case CMD_OBJ_STATUS_V:
agiObjectStatus(gAgi->vars[a[0]]);
break;
case CMD_QUIT:
agiQuit(a[0] != 0u);
break;
case CMD_SHOW_MEM:
textMessageBox("Memory is not a problem here.");
break;
case CMD_PAUSE:
textMessageBox(" Game paused.\nPress Enter to continue.");
break;
case CMD_ECHO_LINE:
if (gAgi->inputLen == 0u) {
strcpy(gAgi->inputLine, gAgi->lastLine);
gAgi->inputLen = (uint8_t)strlen(gAgi->inputLine);
textDrawInputLine();
}
break;
case CMD_CANCEL_LINE:
gAgi->inputLine[0] = '\0';
gAgi->inputLen = 0u;
textDrawInputLine();
break;
case CMD_VERSION:
textMessageBox("JoeyAGI\n\nan AGI interpreter for JoeyLib");
break;
case CMD_SET_SCAN_START:
gAgi->logicScanStart[f->logic] = f->pc;
break;
case CMD_RESET_SCAN_START:
gAgi->logicScanStart[f->logic] = 0u;
break;
case CMD_REPOSITION_TO:
case CMD_REPOSITION_TO_V: {
AgiObjectT *o = object(a[0]);
if (o != NULL) {
// It stays where it was put for this cycle's step.
o->x = (op == CMD_REPOSITION_TO) ? a[1] : gAgi->vars[a[1]];
o->y = (op == CMD_REPOSITION_TO) ? a[2] : gAgi->vars[a[2]];
o->flags = (uint16_t)(o->flags | OBJ_SKIP_STEP);
objFixPosition(a[0]);
}
break;
}
case CMD_PRINT_AT:
printMessage(vmMessage(gAgi->currentLogic, a[0]), a[1], a[2], a[3]);
break;
case CMD_PRINT_AT_V:
printMessage(vmMessage(gAgi->currentLogic, gAgi->vars[a[0]]), a[1], a[2], a[3]);
break;
case CMD_DISCARD_VIEW_V:
saveScriptAdd(op, gAgi->vars[a[0]]);
break;
case CMD_CLEAR_TEXT_RECT:
textClearRect(a[0], a[1], a[2], a[3], a[4]);
break;
case CMD_SET_MENU:
textMenuAdd(vmMessage(gAgi->currentLogic, a[0]));
break;
case CMD_SET_MENU_ITEM:
textMenuAddItem(vmMessage(gAgi->currentLogic, a[0]), a[1]);
break;
case CMD_SUBMIT_MENU:
gAgi->menuSubmitted = true;
break;
case CMD_ENABLE_ITEM:
case CMD_DISABLE_ITEM: {
uint8_t i;
for (i = 0u; i < gAgi->menuItemCount; i++) {
if (gAgi->menuItems[i].controller == a[0]) {
gAgi->menuItems[i].enabled = (op == CMD_ENABLE_ITEM);
}
}
break;
}
case CMD_MENU_INPUT:
if (gAgi->flags[FLAG_MENU] != 0u) {
gAgi->menuRequested = true;
}
break;
case CMD_MUL_N:
gAgi->vars[a[0]] = (uint8_t)(gAgi->vars[a[0]] * a[1]);
break;
case CMD_MUL_V:
gAgi->vars[a[0]] = (uint8_t)(gAgi->vars[a[0]] * gAgi->vars[a[1]]);
break;
case CMD_DIV_N:
if (a[1] != 0u) {
gAgi->vars[a[0]] = (uint8_t)(gAgi->vars[a[0]] / a[1]);
}
break;
case CMD_DIV_V:
if (gAgi->vars[a[1]] != 0u) {
gAgi->vars[a[0]] = (uint8_t)(gAgi->vars[a[0]] / gAgi->vars[a[1]]);
}
break;
case CMD_CLOSE_WINDOW:
textCloseWindow();
break;
default:
// init.disk, init.joy, toggle.monitor, script.size, set.game.id,
// log, trace.on, trace.info, set.upper.left, open.dialogue and
// close.dialogue have nothing to do here.
break;
}
}
// Object commands (0x1E..0x5B).
static void cmdObject(VmFrameT *f, uint8_t op, const uint8_t *a) {
AgiObjectT *o;
(void)f;
o = object(a[0]);
switch (op) {
case CMD_LOAD_VIEW:
case CMD_LOAD_VIEW_V: {
uint8_t id = (op == CMD_LOAD_VIEW) ? a[0] : gAgi->vars[a[0]];
(void)agiViewGet(id);
saveScriptAdd(CMD_LOAD_VIEW, id);
break;
}
case CMD_DISCARD_VIEW:
saveScriptAdd(op, a[0]);
break;
case CMD_ANIMATE_OBJ:
if (o != NULL) {
objAnimate(a[0]);
}
break;
case CMD_UNANIMATE_ALL:
objUnanimateAll();
break;
case CMD_DRAW:
if (o != NULL && (o->flags & OBJ_DRAWN) == 0u && o->view != NULL) {
o->flags = (uint16_t)(o->flags | OBJ_ANIMATED);
objFixPosition(a[0]);
o->prevX = o->x;
o->prevY = o->y;
redrawObjects(a[0], (uint16_t)(OBJ_DRAWN | OBJ_UPDATE), 0u);
}
break;
case CMD_ERASE:
if (o != NULL && (o->flags & OBJ_DRAWN) != 0u) {
redrawObjects(a[0], 0u, OBJ_DRAWN);
}
break;
case CMD_POSITION:
case CMD_POSITION_V:
if (o != NULL) {
o->x = (op == CMD_POSITION) ? a[1] : gAgi->vars[a[1]];
o->y = (op == CMD_POSITION) ? a[2] : gAgi->vars[a[2]];
o->prevX = o->x;
o->prevY = o->y;
}
break;
case CMD_GET_POSN:
if (o != NULL) {
gAgi->vars[a[1]] = (uint8_t)o->x;
gAgi->vars[a[2]] = (uint8_t)o->y;
}
break;
case CMD_REPOSITION:
if (o != NULL) {
o->x = (int16_t)(o->x + (int8_t)gAgi->vars[a[1]]);
o->y = (int16_t)(o->y + (int8_t)gAgi->vars[a[2]]);
o->flags = (uint16_t)(o->flags | OBJ_SKIP_STEP);
objFixPosition(a[0]);
}
break;
case CMD_SET_VIEW:
case CMD_SET_VIEW_V:
if (o != NULL) {
(void)objSetView(a[0], (op == CMD_SET_VIEW) ? a[1] : gAgi->vars[a[1]]);
}
break;
case CMD_SET_LOOP:
case CMD_SET_LOOP_V:
if (o != NULL) {
objSetLoop(a[0], (op == CMD_SET_LOOP) ? a[1] : gAgi->vars[a[1]]);
}
break;
case CMD_FIX_LOOP:
if (o != NULL) {
o->flags = (uint16_t)(o->flags | OBJ_FIX_LOOP);
}
break;
case CMD_RELEASE_LOOP:
if (o != NULL) {
o->flags = (uint16_t)(o->flags & (uint16_t)~OBJ_FIX_LOOP);
}
break;
case CMD_SET_CEL:
case CMD_SET_CEL_V:
if (o != NULL) {
objSetCel(a[0], (op == CMD_SET_CEL) ? a[1] : gAgi->vars[a[1]]);
}
break;
case CMD_LAST_CEL:
if (o != NULL && o->view != NULL && o->loop < o->view->loopCount) {
gAgi->vars[a[1]] = (uint8_t)(o->view->loops[o->loop].celCount - 1u);
}
break;
case CMD_CURRENT_CEL:
gAgi->vars[a[1]] = (o != NULL) ? o->cel : 0u;
break;
case CMD_CURRENT_LOOP:
gAgi->vars[a[1]] = (o != NULL) ? o->loop : 0u;
break;
case CMD_CURRENT_VIEW:
gAgi->vars[a[1]] = (o != NULL) ? o->viewId : 0u;
break;
case CMD_NUMBER_OF_LOOPS:
gAgi->vars[a[1]] = (o != NULL && o->view != NULL) ? o->view->loopCount : 0u;
break;
case CMD_SET_PRIORITY:
case CMD_SET_PRIORITY_V:
if (o != NULL) {
o->priority = (op == CMD_SET_PRIORITY) ? a[1] : gAgi->vars[a[1]];
o->flags = (uint16_t)(o->flags | OBJ_FIX_PRIORITY);
}
break;
case CMD_RELEASE_PRIORITY:
if (o != NULL) {
o->flags = (uint16_t)(o->flags & (uint16_t)~OBJ_FIX_PRIORITY);
}
break;
case CMD_GET_PRIORITY:
gAgi->vars[a[1]] = (o != NULL) ? o->priority : 0u;
break;
case CMD_STOP_UPDATE:
if (o != NULL && (o->flags & OBJ_UPDATE) != 0u) {
redrawObjects(a[0], 0u, OBJ_UPDATE);
}
break;
case CMD_START_UPDATE:
if (o != NULL && (o->flags & OBJ_UPDATE) == 0u) {
redrawObjects(a[0], OBJ_UPDATE, 0u);
}
break;
case CMD_FORCE_UPDATE:
if (o != NULL) {
redrawObjects(a[0], 0u, 0u);
}
break;
case CMD_IGNORE_HORIZON:
if (o != NULL) {
o->flags = (uint16_t)(o->flags | OBJ_IGNORE_HORIZON);
}
break;
case CMD_OBSERVE_HORIZON:
if (o != NULL) {
o->flags = (uint16_t)(o->flags & (uint16_t)~OBJ_IGNORE_HORIZON);
}
break;
case CMD_SET_HORIZON:
gAgi->horizon = a[0];
break;
case CMD_OBJECT_ON_WATER:
case CMD_OBJECT_ON_LAND:
case CMD_OBJECT_ON_ANYTHING:
if (o != NULL) {
o->flags = (uint16_t)(o->flags & (uint16_t)~(OBJ_ON_WATER | OBJ_ON_LAND));
if (op == CMD_OBJECT_ON_WATER) {
o->flags = (uint16_t)(o->flags | OBJ_ON_WATER);
} else if (op == CMD_OBJECT_ON_LAND) {
o->flags = (uint16_t)(o->flags | OBJ_ON_LAND);
}
}
break;
case CMD_IGNORE_OBJS:
if (o != NULL) {
o->flags = (uint16_t)(o->flags | OBJ_IGNORE_OBJS);
}
break;
case CMD_OBSERVE_OBJS:
if (o != NULL) {
o->flags = (uint16_t)(o->flags & (uint16_t)~OBJ_IGNORE_OBJS);
}
break;
case CMD_DISTANCE: {
const AgiObjectT *p = object(a[1]);
uint16_t d;
d = DISTANCE_FAR;
if (o != NULL && p != NULL && (o->flags & OBJ_DRAWN) != 0u && (p->flags & OBJ_DRAWN) != 0u) {
d = (uint16_t)(((o->x > p->x) ? o->x - p->x : p->x - o->x) + ((o->y > p->y) ? o->y - p->y : p->y - o->y));
if (d > DISTANCE_MAX) {
d = DISTANCE_MAX;
}
}
gAgi->vars[a[2]] = (uint8_t)d;
break;
}
case CMD_STOP_CYCLING:
if (o != NULL) {
o->flags = (uint16_t)(o->flags & (uint16_t)~OBJ_CYCLING);
}
break;
case CMD_START_CYCLING:
if (o != NULL) {
o->flags = (uint16_t)(o->flags | OBJ_CYCLING);
}
break;
case CMD_NORMAL_CYCLE:
case CMD_REVERSE_CYCLE:
if (o != NULL) {
o->cycleMode = (op == CMD_NORMAL_CYCLE) ? AGI_CYCLE_NORMAL : AGI_CYCLE_REVERSE;
o->flags = (uint16_t)(o->flags | OBJ_CYCLING);
}
break;
case CMD_END_OF_LOOP:
case CMD_REVERSE_LOOP:
if (o != NULL) {
o->cycleMode = (op == CMD_END_OF_LOOP) ? AGI_CYCLE_END_LOOP : AGI_CYCLE_REVERSE_LOOP;
o->loopFlag = a[1];
gAgi->flags[a[1]] = 0u;
o->flags = (uint16_t)(o->flags | OBJ_CYCLING);
if ((o->flags & OBJ_DRAWN) != 0u) {
o->flags = (uint16_t)(o->flags | OBJ_SKIP_CYCLE);
}
}
break;
case CMD_CYCLE_TIME:
if (o != NULL) {
o->cycleTime = gAgi->vars[a[1]];
o->cycleCount = o->cycleTime;
}
break;
case CMD_STOP_MOTION:
if (o != NULL) {
o->direction = AGI_DIR_NONE;
o->motion = AGI_MOTION_NORMAL;
if (a[0] == AGI_EGO) {
gAgi->vars[VAR_EGO_DIR] = AGI_DIR_NONE;
gAgi->playerControl = false;
}
}
break;
case CMD_START_MOTION:
if (o != NULL) {
o->motion = AGI_MOTION_NORMAL;
if (a[0] == AGI_EGO) {
gAgi->vars[VAR_EGO_DIR] = AGI_DIR_NONE;
gAgi->playerControl = true;
}
}
break;
case CMD_STEP_SIZE:
if (o != NULL) {
o->stepSize = gAgi->vars[a[1]];
}
break;
case CMD_STEP_TIME:
if (o != NULL) {
o->stepTime = gAgi->vars[a[1]];
o->stepCount = o->stepTime;
}
break;
case CMD_MOVE_OBJ:
if (o != NULL) {
objMoveTo(a[0], a[1], a[2], a[3], a[4]);
}
break;
case CMD_MOVE_OBJ_V:
if (o != NULL) {
objMoveTo(a[0], gAgi->vars[a[1]], gAgi->vars[a[2]], gAgi->vars[a[3]], a[4]);
}
break;
case CMD_FOLLOW_EGO:
if (o != NULL) {
o->motion = AGI_MOTION_FOLLOW;
o->randomSteps = 0u;
o->followDistance = (a[1] > o->stepSize) ? a[1] : o->stepSize;
o->motionFlag = a[2];
gAgi->flags[a[2]] = 0u;
}
break;
case CMD_WANDER:
if (o != NULL) {
o->motion = AGI_MOTION_WANDER;
o->randomSteps = 0u;
if (a[0] == AGI_EGO) {
gAgi->playerControl = false;
}
}
break;
case CMD_NORMAL_MOTION:
if (o != NULL) {
o->motion = AGI_MOTION_NORMAL;
}
break;
case CMD_SET_DIR:
if (o != NULL) {
o->direction = gAgi->vars[a[1]];
}
break;
case CMD_GET_DIR:
gAgi->vars[a[1]] = (o != NULL) ? o->direction : 0u;
break;
case CMD_IGNORE_BLOCKS:
if (o != NULL) {
o->flags = (uint16_t)(o->flags | OBJ_IGNORE_BLOCKS);
}
break;
case CMD_OBSERVE_BLOCKS:
if (o != NULL) {
o->flags = (uint16_t)(o->flags & (uint16_t)~OBJ_IGNORE_BLOCKS);
}
break;
case CMD_BLOCK:
gAgi->blockActive = true;
gAgi->blockX1 = a[0];
gAgi->blockY1 = a[1];
gAgi->blockX2 = a[2];
gAgi->blockY2 = a[3];
break;
case CMD_UNBLOCK:
gAgi->blockActive = false;
break;
default:
break;
}
}
// Text, sound, inventory and input commands (0x5C..0x7B).
static void cmdText(VmFrameT *f, uint8_t op, const uint8_t *a) {
char text[MESSAGE_MAX];
(void)f;
switch (op) {
case CMD_GET:
gAgi->itemRoom[a[0]] = AGI_ITEM_CARRIED;
break;
case CMD_GET_V:
gAgi->itemRoom[gAgi->vars[a[0]]] = AGI_ITEM_CARRIED;
break;
case CMD_DROP:
gAgi->itemRoom[a[0]] = 0u;
break;
case CMD_PUT:
gAgi->itemRoom[a[0]] = gAgi->vars[a[1]];
break;
case CMD_PUT_V:
gAgi->itemRoom[gAgi->vars[a[0]]] = gAgi->vars[a[1]];
break;
case CMD_GET_ROOM_V:
gAgi->vars[a[1]] = gAgi->itemRoom[gAgi->vars[a[0]]];
break;
case CMD_LOAD_SOUND:
saveScriptAdd(op, a[0]);
break;
case CMD_SOUND:
soundStart(a[0], a[1]);
break;
case CMD_STOP_SOUND:
soundStop();
break;
case CMD_PRINT:
printMessage(vmMessage(gAgi->currentLogic, a[0]), -1, -1, 0u);
break;
case CMD_PRINT_V:
printMessage(vmMessage(gAgi->currentLogic, gAgi->vars[a[0]]), -1, -1, 0u);
break;
case CMD_DISPLAY:
textExpand(vmMessage(gAgi->currentLogic, a[2]), text, sizeof(text));
textDisplay(a[0], a[1], text);
break;
case CMD_DISPLAY_V:
textExpand(vmMessage(gAgi->currentLogic, gAgi->vars[a[2]]), text, sizeof(text));
textDisplay(gAgi->vars[a[0]], gAgi->vars[a[1]], text);
break;
case CMD_CLEAR_LINES:
textClearLines(a[0], a[1], a[2]);
break;
case CMD_TEXT_SCREEN:
textTextMode();
break;
case CMD_GRAPHICS:
textGraphicsMode();
break;
case CMD_SET_CURSOR_CHAR:
gAgi->cursorChar = vmMessage(gAgi->currentLogic, a[0])[0];
break;
case CMD_SET_TEXT_ATTRIBUTE:
textSetAttribute(a[0], a[1]);
break;
case CMD_SHAKE_SCREEN:
gfxShakeScreen(a[0]);
break;
case CMD_CONFIGURE_SCREEN:
gAgi->picRow = a[0];
gAgi->inputRow = a[1];
gAgi->statusRow = a[2];
break;
case CMD_STATUS_LINE_ON:
gAgi->statusOn = true;
textDrawStatus();
break;
case CMD_STATUS_LINE_OFF:
gAgi->statusOn = false;
textClearLines(gAgi->statusRow, gAgi->statusRow, AGI_COLOR_BLACK);
break;
case CMD_SET_STRING:
if (a[0] < AGI_MAX_STRINGS) {
strncpy(gAgi->strings[a[0]], vmMessage(gAgi->currentLogic, a[1]), AGI_STRING_LEN);
gAgi->strings[a[0]][AGI_STRING_LEN] = '\0';
}
break;
case CMD_GET_STRING:
if (a[0] < AGI_MAX_STRINGS) {
textExpand(vmMessage(gAgi->currentLogic, a[1]), text, sizeof(text));
(void)inputGetString(gAgi->strings[a[0]], (uint8_t)((a[4] > AGI_STRING_LEN) ? AGI_STRING_LEN : a[4]), a[2], a[3], text);
}
break;
case CMD_WORD_TO_STRING:
if (a[0] < AGI_MAX_STRINGS && a[1] >= 1u && a[1] <= gAgi->wordCount) {
strcpy(gAgi->strings[a[0]], gAgi->wordText[a[1] - 1u]);
}
break;
case CMD_PARSE:
if (a[0] < AGI_MAX_STRINGS) {
inputParse(gAgi->strings[a[0]]);
}
break;
case CMD_GET_NUM:
textExpand(vmMessage(gAgi->currentLogic, a[0]), text, sizeof(text));
if (!inputGetNumber(text, &gAgi->vars[a[1]])) {
gAgi->vars[a[1]] = 0u;
}
break;
case CMD_PREVENT_INPUT:
gAgi->inputOn = false;
textDrawInputLine();
break;
case CMD_ACCEPT_INPUT:
gAgi->inputOn = true;
textDrawInputLine();
break;
case CMD_SET_KEY:
if (gAgi->keyMapCount < AGI_MAX_KEYMAPS) {
gAgi->keyMaps[gAgi->keyMapCount].key = (uint16_t)(a[0] | ((uint16_t)a[1] << 8));
gAgi->keyMaps[gAgi->keyMapCount].controller = a[2];
gAgi->keyMapCount++;
}
break;
case CMD_ADD_TO_PIC:
gfxAddToPic(a[0], a[1], a[2], a[3], a[4], a[5], a[6]);
saveScriptAddArgs(op, a);
break;
case CMD_ADD_TO_PIC_V: {
uint8_t v[MAX_ARGS];
uint8_t i;
for (i = 0u; i < MAX_ARGS; i++) {
v[i] = gAgi->vars[a[i]];
}
gfxAddToPic(v[0], v[1], v[2], v[3], v[4], v[5], v[6]);
saveScriptAddArgs(CMD_ADD_TO_PIC, v);
break;
}
default:
break;
}
}
// Variables, flags, rooms, logics and pictures (0x00..0x1D).
static void cmdVars(VmFrameT *f, uint8_t op, const uint8_t *a) {
uint8_t *v;
v = gAgi->vars;
switch (op) {
case CMD_RETURN:
f->done = true;
break;
case CMD_INCREMENT:
if (v[a[0]] != UINT8_MAX) {
v[a[0]]++;
}
break;
case CMD_DECREMENT:
if (v[a[0]] != 0u) {
v[a[0]]--;
}
break;
case CMD_ASSIGNN:
v[a[0]] = a[1];
break;
case CMD_ASSIGNV:
v[a[0]] = v[a[1]];
break;
case CMD_ADDN:
v[a[0]] = (uint8_t)(v[a[0]] + a[1]);
break;
case CMD_ADDV:
v[a[0]] = (uint8_t)(v[a[0]] + v[a[1]]);
break;
case CMD_SUBN:
v[a[0]] = (uint8_t)(v[a[0]] - a[1]);
break;
case CMD_SUBV:
v[a[0]] = (uint8_t)(v[a[0]] - v[a[1]]);
break;
case CMD_LINDIRECTV:
v[v[a[0]]] = v[a[1]];
break;
case CMD_RINDIRECT:
v[a[0]] = v[v[a[1]]];
break;
case CMD_LINDIRECTN:
v[v[a[0]]] = a[1];
break;
case CMD_SET:
gAgi->flags[a[0]] = 1u;
break;
case CMD_RESET:
gAgi->flags[a[0]] = 0u;
break;
case CMD_TOGGLE:
gAgi->flags[a[0]] = (uint8_t)(gAgi->flags[a[0]] ^ 1u);
break;
case CMD_SET_V:
gAgi->flags[v[a[0]]] = 1u;
break;
case CMD_RESET_V:
gAgi->flags[v[a[0]]] = 0u;
break;
case CMD_TOGGLE_V:
gAgi->flags[v[a[0]]] = (uint8_t)(gAgi->flags[v[a[0]]] ^ 1u);
break;
case CMD_NEW_ROOM:
agiNewRoom(a[0]);
break;
case CMD_NEW_ROOM_V:
agiNewRoom(v[a[0]]);
break;
case CMD_LOAD_LOGICS:
case CMD_LOAD_LOGICS_V: {
uint8_t id = (op == CMD_LOAD_LOGICS) ? a[0] : v[a[0]];
if (vmLoadLogic(id)) {
gAgi->logicLoaded[id] = true;
}
saveScriptAdd(CMD_LOAD_LOGICS, id);
break;
}
case CMD_CALL:
vmCallLogic(a[0]);
break;
case CMD_CALL_V:
vmCallLogic(v[a[0]]);
break;
case CMD_LOAD_PIC:
saveScriptAdd(op, v[a[0]]);
break;
case CMD_DRAW_PIC:
gfxDrawPic(v[a[0]], true);
saveScriptAdd(op, v[a[0]]);
break;
case CMD_SHOW_PIC:
gAgi->windowOpen = false;
gfxShowPic();
break;
case CMD_DISCARD_PIC:
saveScriptAdd(op, v[a[0]]);
break;
case CMD_OVERLAY_PIC:
gfxDrawPic(v[a[0]], false);
saveScriptAdd(op, v[a[0]]);
break;
case CMD_SHOW_PRI_SCREEN:
gfxShowPriority();
break;
default:
break;
}
}
// A logic resource's code length, its first (little-endian) word.
static uint16_t codeLength(const uint8_t *raw) {
return (uint16_t)(raw[0] | ((uint16_t)raw[1] << 8));
}
// compare.strings: equal ignoring case, spaces and punctuation.
static bool compareStrings(const char *a, const char *b) {
char ca;
char cb;
for (;;) {
while (*a != '\0' && !((*a >= 'a' && *a <= 'z') || (*a >= 'A' && *a <= 'Z') || (*a >= '0' && *a <= '9'))) {
a++;
}
while (*b != '\0' && !((*b >= 'a' && *b <= 'z') || (*b >= 'A' && *b <= 'Z') || (*b >= '0' && *b <= '9'))) {
b++;
}
if (*a == '\0' || *b == '\0') {
return *a == *b;
}
ca = (char)((*a >= 'A' && *a <= 'Z') ? *a - 'A' + 'a' : *a);
cb = (char)((*b >= 'A' && *b <= 'Z') ? *b - 'A' + 'a' : *b);
if (ca != cb) {
return false;
}
a++;
b++;
}
}
// Decrypt a logic's message block in place: the key runs on from the
// first message's text to the end of the resource.
static void decryptMessages(uint8_t *raw, uint16_t length) {
uint16_t codeLen;
uint16_t sec;
uint8_t count;
uint16_t text;
codeLen = codeLength(raw);
sec = (uint16_t)(LOGIC_HEADER_BYTES + codeLen);
if ((uint32_t)sec + MSG_HEADER_BYTES > length) {
return;
}
count = raw[sec];
text = (uint16_t)(sec + MSG_HEADER_BYTES + (uint16_t)count * MSG_OFFSET_BYTES);
if (text < length) {
agiDecrypt(&raw[text], (uint16_t)(length - text));
}
}
// An if's tests. Leaves pc after the closing 0xFF.
static bool evalCondition(VmFrameT *f) {
uint8_t b;
bool negate;
bool inOr;
bool orResult;
bool result;
bool skipping;
negate = false;
inOr = false;
orResult = false;
result = true;
// Once the answer is known the remaining tests are only skipped.
skipping = false;
for (;;) {
b = readByte(f);
if (f->done) {
return false;
}
if (b == CODE_IF) {
return result;
}
if (b == CODE_NOT) {
negate = !negate;
continue;
}
if (b == CODE_OR) {
if (!inOr) {
inOr = true;
orResult = false;
} else {
inOr = false;
if (!skipping && !orResult) {
result = false;
skipping = true;
}
}
continue;
}
if (skipping || (inOr && orResult)) {
skipTest(f, b);
negate = false;
continue;
}
if (evalTest(f, b) != negate) {
if (inOr) {
orResult = true;
}
} else if (!inOr) {
result = false;
skipping = true;
}
negate = false;
}
}
static bool evalTest(VmFrameT *f, uint8_t test) {
uint8_t a[MAX_ARGS];
uint8_t i;
if (test == TEST_SAID) {
return saidMatch(f);
}
if (test == 0u || test >= TEST_COUNT) {
f->done = true;
return false;
}
for (i = 0u; i < kTestArgs[test]; i++) {
a[i] = readByte(f);
}
switch (test) {
case TEST_EQUALN: return gAgi->vars[a[0]] == a[1];
case TEST_EQUALV: return gAgi->vars[a[0]] == gAgi->vars[a[1]];
case TEST_LESSN: return gAgi->vars[a[0]] < a[1];
case TEST_LESSV: return gAgi->vars[a[0]] < gAgi->vars[a[1]];
case TEST_GREATERN: return gAgi->vars[a[0]] > a[1];
case TEST_GREATERV: return gAgi->vars[a[0]] > gAgi->vars[a[1]];
case TEST_ISSET: return gAgi->flags[a[0]] != 0u;
case TEST_ISSET_V: return gAgi->flags[gAgi->vars[a[0]]] != 0u;
case TEST_HAS: return gAgi->itemRoom[a[0]] == AGI_ITEM_CARRIED;
case TEST_OBJ_IN_ROOM: return gAgi->itemRoom[a[0]] == gAgi->vars[a[1]];
case TEST_CONTROLLER: return gAgi->controllers[a[0]] != 0u;
case TEST_HAVE_KEY: return inputHaveKey();
case TEST_COMPARE_STRINGS:
return a[0] < AGI_MAX_STRINGS && a[1] < AGI_MAX_STRINGS && compareStrings(gAgi->strings[a[0]], gAgi->strings[a[1]]);
default: return posnTest(test, a);
}
}
// Run a logic frame until it returns or logic is aborted.
static void execute(VmFrameT *f) {
uint8_t op;
uint8_t a[MAX_ARGS];
uint8_t i;
uint16_t skip;
while (!f->done && !gAgi->abortLogic) {
op = readByte(f);
if (f->done) {
break;
}
if (op == CODE_IF) {
bool cond;
cond = evalCondition(f);
skip = readWord(f);
if (!cond && !f->done) {
f->pc = (uint16_t)(f->pc + skip);
}
continue;
}
if (op == CODE_GOTO) {
skip = readWord(f);
f->pc = (uint16_t)(f->pc + skip);
continue;
}
if (op >= CMD_COUNT || kActionArgs[op] > MAX_ARGS) {
jlLogF("logic %u: unknown command 0x%02X at %u", (unsigned)f->logic, (unsigned)op, (unsigned)(f->pc - 1u));
f->done = true;
break;
}
for (i = 0u; i < kActionArgs[op]; i++) {
a[i] = readByte(f);
}
if (op <= CMD_SHOW_PRI_SCREEN) {
cmdVars(f, op, a);
} else if (op <= CMD_UNBLOCK) {
cmdObject(f, op, a);
} else if (op <= CMD_ADD_TO_PIC_V) {
cmdText(f, op, a);
} else {
cmdMisc(f, op, a);
}
}
}
static AgiObjectT *object(uint8_t n) {
if (n >= gAgi->objectCount) {
return NULL;
}
return &gAgi->objects[n];
}
// posn, obj.in.box, center.posn and right.posn: a point of the object's
// baseline (its left end, both ends, middle, right end) in a box.
static bool posnTest(uint8_t test, const uint8_t *a) {
const AgiObjectT *o;
int16_t x1;
int16_t x2;
o = object(a[0]);
if (o == NULL) {
return false;
}
x1 = o->x;
x2 = o->x;
if (test == TEST_OBJ_IN_BOX) {
x2 = (int16_t)(o->x + (int16_t)o->width - 1);
} else if (test == TEST_CENTER_POSN) {
x1 = (int16_t)(o->x + (int16_t)(o->width / 2u));
x2 = x1;
} else if (test == TEST_RIGHT_POSN) {
x1 = (int16_t)(o->x + (int16_t)o->width - 1);
x2 = x1;
}
return x1 >= (int16_t)a[1] && o->y >= (int16_t)a[2] && x2 <= (int16_t)a[3] && o->y <= (int16_t)a[4];
}
// print and print.at: a message window that waits for Enter or Escape
// (or v21 half-seconds) unless f15 says to leave it up.
static void printMessage(const char *msg, int16_t row, int16_t col, uint8_t width) {
char text[MESSAGE_MAX];
textExpand(msg, text, sizeof(text));
textPrint(text, row, col, width);
if (gAgi->flags[FLAG_WINDOW_STAYS] == 0u) {
(void)inputWaitPrintClose(gAgi->vars[VAR_WINDOW_TIMER]);
textCloseWindow();
}
}
static uint8_t readByte(VmFrameT *f) {
if (f->pc >= f->length) {
f->done = true;
return 0u;
}
return f->code[f->pc++];
}
static uint16_t readWord(VmFrameT *f) {
uint8_t lo;
uint8_t hi;
lo = readByte(f);
hi = readByte(f);
return (uint16_t)(lo | ((uint16_t)hi << 8));
}
// Change an object's drawn/updated flags with it off the screen, then
// draw everything again in order.
static void redrawObjects(uint8_t n, uint16_t setFlags, uint16_t clearFlags) {
AgiObjectT *o;
o = &gAgi->objects[n];
gfxEraseAll();
o->flags = (uint16_t)((o->flags | setFlags) & (uint16_t)~clearFlags);
gfxDrawStatic();
gfxPresentDirty();
}
// said(): the typed words against a pattern (specification 3.8): word 1
// matches any word, 9999 the rest of the line. A match sets f4 so later
// said() tests see the input as taken. An unknown word (v9) only matches
// those two, as in Sierra's interpreter.
static bool saidMatch(VmFrameT *f) {
uint8_t count;
uint8_t i;
uint16_t w;
uint8_t matched;
bool ok;
count = readByte(f);
ok = gAgi->flags[FLAG_ENTERED] != 0u && gAgi->flags[FLAG_SAID_OK] == 0u;
matched = 0u;
for (i = 0u; i < count; i++) {
w = readWord(f);
if (!ok) {
continue;
}
if (w == AGI_WORD_REST) {
matched = gAgi->wordCount;
continue;
}
if (matched >= gAgi->wordCount || (w != AGI_WORD_ANY && w != gAgi->wordIds[matched])) {
ok = false;
continue;
}
matched++;
}
if (!ok || matched != gAgi->wordCount) {
return false;
}
gAgi->flags[FLAG_SAID_OK] = 1u;
return true;
}
static void skipTest(VmFrameT *f, uint8_t test) {
uint8_t count;
if (test == TEST_SAID) {
count = readByte(f);
f->pc = (uint16_t)(f->pc + (uint16_t)count * SAID_WORD_BYTES);
return;
}
if (test < TEST_COUNT) {
f->pc = (uint16_t)(f->pc + kTestArgs[test]);
}
}
// ----- Public API (alphabetical) -----
// Run a logic from its scan start. One that was not loaded is loaded for
// the call and unloaded after.
void vmCallLogic(uint8_t logicId) {
VmFrameT frame;
uint8_t *raw;
uint8_t caller;
bool temporary;
if (gAgi->abortLogic) {
return;
}
if (gCallDepth >= CALL_DEPTH_MAX) {
jlLogF("call: logic %u nested too deep", (unsigned)logicId);
return;
}
temporary = !gAgi->logicLoaded[logicId];
if (!vmLoadLogic(logicId)) {
jlLogF("call: no logic %u", (unsigned)logicId);
return;
}
raw = gAgi->logicRaw[logicId];
frame.code = raw + LOGIC_HEADER_BYTES;
frame.length = codeLength(raw);
frame.pc = gAgi->logicScanStart[logicId];
frame.logic = logicId;
frame.done = false;
caller = gAgi->currentLogic;
gAgi->currentLogic = logicId;
gCallDepth++;
execute(&frame);
gCallDepth--;
gAgi->currentLogic = caller;
if (temporary) {
gAgi->logicScanStart[logicId] = 0u;
}
}
// new.room: forget every logic but logic 0 (and their scan starts) and
// give their memory back, as Sierra's interpreter does; the new room loads
// what it uses. Only called between cycles, with no logic running.
void vmDiscardLogics(void) {
uint16_t i;
for (i = 1u; i < AGI_MAX_RESOURCES; i++) {
gAgi->logicLoaded[i] = false;
gAgi->logicScanStart[i] = 0u;
jlFree(gAgi->logicRaw[i]);
gAgi->logicRaw[i] = NULL;
gAgi->logicLength[i] = 0u;
}
}
// Make sure a logic's resource is in memory (they stay cached).
bool vmLoadLogic(uint8_t logicId) {
uint8_t *raw;
uint16_t length;
uint16_t codeLen;
if (gAgi->logicRaw[logicId] != NULL) {
return true;
}
raw = agiResLoad(&gAgi->game, AGI_RES_LOGIC, logicId, &length);
if (raw == NULL) {
return false;
}
codeLen = codeLength(raw);
if (length < LOGIC_HEADER_BYTES || (uint32_t)LOGIC_HEADER_BYTES + codeLen > length) {
jlFree(raw);
return false;
}
decryptMessages(raw, length);
gAgi->logicRaw[logicId] = raw;
gAgi->logicLength[logicId] = length;
return true;
}
// A logic's message, decrypted; "" when it has none by that number.
const char *vmMessage(uint8_t logicId, uint8_t msgId) {
const uint8_t *raw;
uint16_t length;
uint16_t sec;
uint16_t off;
if (!vmLoadLogic(logicId)) {
return "";
}
raw = gAgi->logicRaw[logicId];
length = gAgi->logicLength[logicId];
sec = (uint16_t)(LOGIC_HEADER_BYTES + codeLength(raw));
if (msgId == 0u || (uint32_t)sec + MSG_HEADER_BYTES > length || msgId > raw[sec]) {
return "";
}
off = (uint16_t)(sec + MSG_HEADER_BYTES + (uint16_t)(msgId - 1u) * MSG_OFFSET_BYTES);
if ((uint32_t)off + 1u >= length) {
return "";
}
off = (uint16_t)(raw[off] | ((uint16_t)raw[off + 1u] << 8));
if (off == 0u || (uint32_t)sec + MSG_OFFSET_BASE + off >= length) {
return "";
}
return (const char *)&raw[sec + MSG_OFFSET_BASE + off];
}
// Rebuild one recorded picture command (restore).
void vmReplay(const AgiScriptEntryT *e) {
const uint8_t *a = e->args;
switch (e->op) {
case CMD_LOAD_LOGICS:
(void)vmLoadLogic(a[0]);
break;
case CMD_LOAD_VIEW:
(void)agiViewGet(a[0]);
break;
case CMD_DRAW_PIC:
case CMD_OVERLAY_PIC:
gfxDrawPic(a[0], e->op == CMD_DRAW_PIC);
break;
case CMD_ADD_TO_PIC:
gfxAddToPic(a[0], a[1], a[2], a[3], a[4], a[5], a[6]);
break;
default:
break;
}
}