// 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 #include #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; } }