joeylib2/tests/agi/disasmAgi.c

413 lines
15 KiB
C

// AGI v2 LOGIC disassembler.
//
// Walks one LOGIC resource and prints the bytecode in a human-readable
// form, with names for action opcodes, test commands, common variables
// and flags. Intended as a debugging aid while filling out the VM:
// dump KQ3's logic.0 + logic.45 to identify which stub opcodes are
// gating title-screen progression.
//
// Usage:
// disasmAgi <game-directory> <logic-id> [end-pc]
//
// `end-pc` caps the walk at a given offset; useful for spelunking
// through huge logic.0 without dumping the whole thing.
#include "agi.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define AGI_LOGIC_HDR_BYTES 2u
#define ACTION_UNKNOWN 0xFFu
#define MAX_TEST 0x12u
// ----- Prototypes -----
static const char *actionName(uint8_t op);
static uint16_t disasmAction(const uint8_t *code, uint16_t pc, uint16_t length, uint8_t op);
static uint16_t disasmIf(const uint8_t *code, uint16_t pc, uint16_t length);
static uint16_t disasmTest(const uint8_t *code, uint16_t pc, uint16_t length);
static const char *testName(uint8_t op);
static int walk(const uint8_t *code, uint16_t length, uint16_t endPc);
// AGI v2 action-opcode names. Indexed by opcode id; NULL means "no
// such opcode" (matches the VM's ACTION_UNKNOWN sentinel).
static const char *kActionNames[256] = {
/* 0x00 */ "return", "increment", "decrement", "assignn",
/* 0x04 */ "assignv", "addn", "addv", "subn",
/* 0x08 */ "subv", "lindirectv", "rindirect", "lindirectn",
/* 0x0C */ "set", "reset", "toggle", "setv",
/* 0x10 */ "resetv", "togglev", "new.room", "new.room.v",
/* 0x14 */ "load.logic", "load.logic.v", "call", "call.v",
/* 0x18 */ "load.pic", "draw.pic", "show.pic", "discard.pic",
/* 0x1C */ "overlay.pic", "show.pri.screen", "load.view", "load.view.v",
/* 0x20 */ "discard.view", "animate.obj", "unanimate.all", "draw",
/* 0x24 */ "erase", "position", "position.v", "get.posn",
/* 0x28 */ "reposition", "set.view", "set.view.v", "set.loop",
/* 0x2C */ "set.loop.v", "fix.loop", "release.loop", "set.cel",
/* 0x30 */ "set.cel.v", "last.cel", "current.cel", "current.loop",
/* 0x34 */ "current.view", "number.of.loops", "set.priority", "set.priority.v",
/* 0x38 */ "release.priority", "get.priority", "stop.update", "start.update",
/* 0x3C */ "force.update", "ignore.horizon", "observe.horizon", "set.horizon",
/* 0x40 */ "object.on.water", "object.on.land", "object.on.anything", "ignore.objs",
/* 0x44 */ "observe.objs", "distance", "stop.cycling", "start.cycling",
/* 0x48 */ "normal.cycle", "end.of.loop", "reverse.cycle", "reverse.loop",
/* 0x4C */ "cycle.time", "stop.motion", "start.motion", "step.size",
/* 0x50 */ "step.time", "move.obj", "move.obj.v", "follow.ego",
/* 0x54 */ "wander", "normal.motion", "set.dir", "get.dir",
/* 0x58 */ "ignore.blocks", "observe.blocks", "block", "unblock",
/* 0x5C */ "get", "get.v", "drop", "put",
/* 0x60 */ "put.v", "get.room.v", "load.sound", "sound",
/* 0x64 */ "stop.sound", "print", "print.v", "display",
/* 0x68 */ "display.v", "clear.lines", "text.screen", "graphics",
/* 0x6C */ "set.cursor.char", "set.text.attribute", "shake.screen", "configure.screen",
/* 0x70 */ "status.line.on", "status.line.off", "set.string", "get.string",
/* 0x74 */ "word.to.string", "parse", "get.num", "prevent.input",
/* 0x78 */ "accept.input", "set.key", "add.to.pic", "add.to.pic.v",
/* 0x7C */ "status", "save.game", "restore.game", "init.disk",
/* 0x80 */ "restart.game", "show.obj", "random", "program.control",
/* 0x84 */ "player.control", "obj.status.v", "quit", "show.mem",
/* 0x88 */ "pause", "echo.line", "cancel.line", "init.joy",
/* 0x8C */ "toggle.monitor", "version", "script.size", "set.game.id",
/* 0x90 */ "log", "set.scan.start", "reset.scan.start", "reposition.to",
/* 0x94 */ "reposition.to.v", "trace.on", "trace.info", "print.at",
/* 0x98 */ "print.at.v", "discard.view.v", "clear.text.rect", "set.upper.left",
/* 0x9C */ "set.menu", "set.menu.item", "submit.menu", "enable.item",
/* 0xA0 */ "disable.item", "menu.input", "show.obj.v", "open.dialogue",
/* 0xA4 */ "close.dialogue", "mul.n", "mul.v", "div.n",
/* 0xA8 */ "div.v", "close.window", "set.simple", "push.script",
/* 0xAC */ "pop.script", "hold.key", "set.pri.base", "discard.sound",
/* 0xB0 */ "hide.mouse", "allow.menu", "show.mouse", "fence.mouse",
/* 0xB4 */ "mouse.posn", "release.key", "adj.ego.move.to.x.y",
/* 0xB7..0xFD */ NULL
// 0xFE = goto, 0xFF = if -- handled separately
};
// AGI v2 action arg byte counts (matches kActionArgBytes in agiVm.c).
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
};
static const char *kTestNames[MAX_TEST + 1u] = {
/* 0x00 */ NULL,
/* 0x01 */ "equaln",
/* 0x02 */ "equalv",
/* 0x03 */ "lessn",
/* 0x04 */ "lessv",
/* 0x05 */ "greatern",
/* 0x06 */ "greaterv",
/* 0x07 */ "isset",
/* 0x08 */ "issetv",
/* 0x09 */ "has",
/* 0x0A */ "obj.in.room",
/* 0x0B */ "posn",
/* 0x0C */ "controller",
/* 0x0D */ "have.key",
/* 0x0E */ "said",
/* 0x0F */ "compare.strings",
/* 0x10 */ "obj.in.box",
/* 0x11 */ "center.posn",
/* 0x12 */ "right.posn"
};
static const uint8_t kTestArgBytes[MAX_TEST + 1u] = {
0u, 2u, 2u, 2u, 2u, 2u, 2u, 1u, 1u, 1u, 2u, 5u, 1u, 0u, 0u /*said*/, 2u, 5u, 5u, 5u
};
// ----- Helpers (alphabetical) -----
static const char *actionName(uint8_t op) {
if (op == 0xFFu) {
return "if";
}
if (op == 0xFEu) {
return "goto";
}
if (kActionNames[op] != NULL) {
return kActionNames[op];
}
return "???";
}
static uint16_t disasmAction(const uint8_t *code, uint16_t pc, uint16_t length, uint8_t op) {
uint8_t args;
uint8_t i;
uint16_t here;
here = (uint16_t)(pc - 1u);
args = kActionArgBytes[op];
if (args == ACTION_UNKNOWN) {
printf(" %04X: %02X ???\n", (unsigned)here, (unsigned)op);
return length;
}
if ((uint32_t)pc + (uint32_t)args > (uint32_t)length) {
printf(" %04X: %02X truncated args\n", (unsigned)here, (unsigned)op);
return length;
}
printf(" %04X: %s", (unsigned)here, actionName(op));
if (args == 0u) {
printf("()\n");
} else {
printf("(");
for (i = 0u; i < args; i++) {
if (i > 0u) {
printf(", ");
}
printf("%u", (unsigned)code[pc + i]);
}
printf(")\n");
}
return (uint16_t)(pc + args);
}
static uint16_t disasmIf(const uint8_t *code, uint16_t pc, uint16_t length) {
uint16_t here;
uint8_t testOp;
bool first;
bool notNext;
bool inOr;
uint16_t skip;
uint16_t target;
here = (uint16_t)(pc - 1u);
printf(" %04X: if (", (unsigned)here);
first = true;
notNext = false;
inOr = false;
for (;;) {
if (pc >= length) {
printf(" <truncated>)\n");
return length;
}
testOp = code[pc];
pc = (uint16_t)(pc + 1u);
if (testOp == 0xFFu) {
// End of test list.
break;
}
if (testOp == 0xFCu) {
if (inOr) {
printf(")");
inOr = false;
} else {
if (!first) {
printf(" && ");
}
printf("(");
inOr = true;
first = true;
}
continue;
}
if (testOp == 0xFDu) {
notNext = true;
continue;
}
if (!first) {
printf(inOr ? " || " : " && ");
}
first = false;
if (notNext) {
printf("!");
notNext = false;
}
pc = disasmTest(code, pc, length);
}
if (inOr) {
printf(")");
}
if ((uint32_t)pc + 2u > (uint32_t)length) {
printf(") <truncated skip>\n");
return length;
}
skip = (uint16_t)(code[pc] | ((uint16_t)code[pc + 1u] << 8));
target = (uint16_t)(pc + 2u + skip);
printf(") -> %04X\n", (unsigned)target);
return (uint16_t)(pc + 2u);
}
static uint16_t disasmTest(const uint8_t *code, uint16_t pc, uint16_t length) {
uint8_t args;
uint8_t i;
uint8_t testOp;
uint8_t wordCount;
uint16_t word;
testOp = code[pc - 1u];
if (testOp == 0u || testOp > MAX_TEST) {
printf("???_test_0x%02X(", (unsigned)testOp);
return pc;
}
if (testOp == 0x0Eu) {
// said(word, word, ...)
if (pc >= length) {
printf("said(<truncated>");
return length;
}
wordCount = code[pc];
pc = (uint16_t)(pc + 1u);
printf("said(");
for (i = 0u; i < wordCount; i++) {
if (i > 0u) {
printf(", ");
}
if ((uint32_t)pc + 2u > (uint32_t)length) {
printf("<truncated>");
return length;
}
word = (uint16_t)(code[pc] | ((uint16_t)code[pc + 1u] << 8));
printf("w%u", (unsigned)word);
pc = (uint16_t)(pc + 2u);
}
printf(")");
return pc;
}
args = kTestArgBytes[testOp];
printf("%s(", testName(testOp));
if ((uint32_t)pc + (uint32_t)args > (uint32_t)length) {
printf("<truncated>)");
return length;
}
for (i = 0u; i < args; i++) {
if (i > 0u) {
printf(", ");
}
printf("%u", (unsigned)code[pc + i]);
}
printf(")");
return (uint16_t)(pc + args);
}
static const char *testName(uint8_t op) {
if (op > MAX_TEST || kTestNames[op] == NULL) {
return "???";
}
return kTestNames[op];
}
static int walk(const uint8_t *code, uint16_t length, uint16_t endPc) {
uint16_t pc;
uint8_t op;
int16_t gotoOff;
uint16_t target;
pc = 0u;
while (pc < length && pc < endPc) {
op = code[pc];
pc = (uint16_t)(pc + 1u);
if (op == 0xFFu) {
pc = disasmIf(code, pc, length);
continue;
}
if (op == 0xFEu) {
if ((uint32_t)pc + 2u > (uint32_t)length) {
printf(" %04X: goto <truncated>\n", (unsigned)(pc - 1u));
return 1;
}
gotoOff = (int16_t)(code[pc] | ((uint16_t)code[pc + 1u] << 8));
target = (uint16_t)((int32_t)pc + 2 + (int32_t)gotoOff);
printf(" %04X: goto %04X\n", (unsigned)(pc - 1u), (unsigned)target);
pc = (uint16_t)(pc + 2u);
continue;
}
pc = disasmAction(code, pc, length, op);
if (op == 0x00u) {
// return -- end of logic. Keep walking past it; some
// logics have unreachable handlers below the top-level
// return that callers jump into via goto.
}
}
return 0;
}
// ----- main -----
int main(int argc, char **argv) {
AgiGameT game;
uint16_t logicId;
uint8_t *bytes;
uint16_t length;
uint16_t bytecodeLen;
uint16_t endPc;
int ret;
if (argc < 3 || argc > 4) {
fprintf(stderr, "usage: %s <game-directory> <logic-id> [end-pc]\n", argv[0]);
return 2;
}
if (!agiResOpen(&game, argv[1])) {
fprintf(stderr, "FAIL: agiResOpen rejected '%s'\n", argv[1]);
return 1;
}
logicId = (uint16_t)atoi(argv[2]);
if (logicId >= game.resCount[AGI_RES_LOGIC]) {
fprintf(stderr, "FAIL: no LOGIC at id %u\n", (unsigned)logicId);
agiResClose(&game);
return 1;
}
bytes = agiResLoad(&game, AGI_RES_LOGIC, logicId, &length);
if (bytes == NULL) {
fprintf(stderr, "FAIL: LOGIC %u failed to load\n", (unsigned)logicId);
agiResClose(&game);
return 1;
}
if (length < AGI_LOGIC_HDR_BYTES) {
fprintf(stderr, "FAIL: LOGIC %u truncated header\n", (unsigned)logicId);
free(bytes);
agiResClose(&game);
return 1;
}
bytecodeLen = (uint16_t)(bytes[0] | ((uint16_t)bytes[1] << 8));
if ((uint32_t)bytecodeLen + AGI_LOGIC_HDR_BYTES > (uint32_t)length) {
fprintf(stderr, "FAIL: LOGIC %u bytecode length exceeds resource\n", (unsigned)logicId);
free(bytes);
agiResClose(&game);
return 1;
}
endPc = (argc == 4) ? (uint16_t)atoi(argv[3]) : bytecodeLen;
if (endPc > bytecodeLen) {
endPc = bytecodeLen;
}
printf("LOGIC %u: %u bytes resource, %u bytes bytecode\n",
(unsigned)logicId, (unsigned)length, (unsigned)bytecodeLen);
ret = walk(bytes + AGI_LOGIC_HDR_BYTES, bytecodeLen, endPc);
free(bytes);
agiResClose(&game);
return ret;
}