// Host-side AGI pipeline test (Phase 1 sub-D). // // Wires up a real logic-resource cache + trace callbacks and runs // the VM in a simulated game-cycle loop: // * agiVmRun() // * NEW_ROOM -> update var[0]/var[1], reset flags, restart logic.0 // * RETURN -> count one cycle, restart logic.0 // * UNKNOWN_OP -> print and exit // Capped at MAX_CYCLES so a logic that always returns without // painting can't loop forever. // // What this lets us verify before touching any emulator: // - logic.0's prologue + IF branching against fresh game state // - room transition handling (new.room(N) loads logic N) // - room logic execution (load.pic/draw.pic/show.pic callbacks) // - call.logic / RETURN frame management // // Usage: // testAgiPipeline [max-cycles] #include "agi.h" #include #include #include #define MAX_CYCLES_DEFAULT 20u #define AGI_LOGIC_HDR_BYTES 2u typedef struct { uint8_t *raw; uint16_t rawLength; const uint8_t *bytecode; uint16_t bytecodeLength; } CachedLogicT; static AgiGameT gGame; static CachedLogicT gLogicCache[AGI_MAX_RESOURCES]; static uint32_t gTraceLoadPic; static uint32_t gTraceDrawPic; static uint32_t gTraceShowPic; static uint32_t gTraceDiscardPic; static uint32_t gTraceOverlayPic; static uint32_t gTraceFetchLogic; static void cbDiscardPic(void *ctx, uint8_t picId); static void cbDrawPic(void *ctx, uint8_t picId); static const uint8_t *cbFetchLogic(void *ctx, uint8_t logicId, uint16_t *outLength); static void cbLoadPic(void *ctx, uint8_t picId); static void cbOverlayPic(void *ctx, uint8_t picId); static void cbShowPic(void *ctx); static const char *haltName(AgiVmHaltE r); static void releaseCache(void); static void cbDiscardPic(void *ctx, uint8_t picId) { (void)ctx; gTraceDiscardPic++; printf(" [host] discardPic(%u)\n", (unsigned)picId); } static void cbDrawPic(void *ctx, uint8_t picId) { (void)ctx; gTraceDrawPic++; printf(" [host] drawPic(%u)\n", (unsigned)picId); } static const uint8_t *cbFetchLogic(void *ctx, uint8_t logicId, uint16_t *outLength) { CachedLogicT *slot; uint8_t *raw; uint16_t rawLength; uint16_t bcLen; (void)ctx; slot = &gLogicCache[logicId]; if (slot->bytecode != NULL) { *outLength = slot->bytecodeLength; return slot->bytecode; } if (logicId >= gGame.resCount[AGI_RES_LOGIC]) { return NULL; } raw = agiResLoad(&gGame, AGI_RES_LOGIC, (uint16_t)logicId, &rawLength); if (raw == NULL || rawLength < AGI_LOGIC_HDR_BYTES) { if (raw != NULL) { free(raw); } return NULL; } bcLen = (uint16_t)(raw[0] | ((uint16_t)raw[1] << 8)); if ((uint16_t)(AGI_LOGIC_HDR_BYTES + bcLen) > rawLength) { free(raw); return NULL; } slot->raw = raw; slot->rawLength = rawLength; slot->bytecode = raw + AGI_LOGIC_HDR_BYTES; slot->bytecodeLength = bcLen; gTraceFetchLogic++; printf(" [host] fetchLogic(%u) -> %u bytes (first load)\n", (unsigned)logicId, (unsigned)bcLen); *outLength = bcLen; return slot->bytecode; } static void cbLoadPic(void *ctx, uint8_t picId) { (void)ctx; gTraceLoadPic++; printf(" [host] loadPic(%u)\n", (unsigned)picId); } static void cbOverlayPic(void *ctx, uint8_t picId) { (void)ctx; gTraceOverlayPic++; printf(" [host] overlayPic(%u)\n", (unsigned)picId); } static void cbShowPic(void *ctx) { (void)ctx; gTraceShowPic++; printf(" [host] showPic\n"); } static const char *haltName(AgiVmHaltE r) { switch (r) { case AGI_VM_HALT_NONE: return "NONE"; case AGI_VM_HALT_RETURN: return "RETURN"; case AGI_VM_HALT_UNKNOWN_OP: return "UNKNOWN_OP"; case AGI_VM_HALT_TRUNCATED: return "TRUNCATED"; case AGI_VM_HALT_NEW_ROOM: return "NEW_ROOM"; case AGI_VM_HALT_NO_LOGIC: return "NO_LOGIC"; case AGI_VM_HALT_STACK_OVER: return "STACK_OVER"; default: return "?"; } } static void releaseCache(void) { uint16_t i; for (i = 0u; i < AGI_MAX_RESOURCES; i++) { if (gLogicCache[i].raw != NULL) { free(gLogicCache[i].raw); gLogicCache[i].raw = NULL; } } } int main(int argc, char **argv) { AgiVmT vm; AgiVmCallbacksT cb; uint16_t length; const uint8_t *logic0; uint32_t maxCycles; uint32_t cycles; AgiVmHaltE reason; uint8_t prevRoom; uint8_t i; if (argc < 2 || argc > 3) { fprintf(stderr, "usage: %s [max-cycles]\n", argv[0]); return 2; } maxCycles = (argc == 3) ? (uint32_t)atoi(argv[2]) : (uint32_t)MAX_CYCLES_DEFAULT; if (maxCycles == 0u) { maxCycles = (uint32_t)MAX_CYCLES_DEFAULT; } if (!agiResOpen(&gGame, argv[1])) { fprintf(stderr, "FAIL: agiResOpen rejected '%s'\n", argv[1]); return 1; } memset(gLogicCache, 0, sizeof(gLogicCache)); cb.fetchLogic = cbFetchLogic; cb.loadPic = cbLoadPic; cb.drawPic = cbDrawPic; cb.showPic = cbShowPic; cb.discardPic = cbDiscardPic; cb.overlayPic = cbOverlayPic; cb.haveKey = NULL; cb.fetchMessage = NULL; cb.addToPic = NULL; cb.soundDuration = NULL; cb.playSound = NULL; cb.stopSound = NULL; cb.ctx = NULL; agiVmInit(&vm); agiVmSetCallbacks(&vm, &cb); length = 0u; logic0 = cbFetchLogic(NULL, 0u, &length); if (logic0 == NULL) { fprintf(stderr, "FAIL: could not load logic.0\n"); releaseCache(); agiResClose(&gGame); return 1; } agiVmResetToLogic(&vm, logic0, length, 0u); cycles = 0u; for (;;) { if (cycles >= maxCycles) { printf(" (capped at %u cycles)\n", (unsigned)maxCycles); break; } cycles++; // Simulate one wall-clock second per cycle so per-second // game logic (countdowns, state machines, animation cadence) // advances under the host test without waiting in real time. // KQ3's title state machine in particular is gated entirely // on var 11 changing; without this tick logic.45's countdowns // never fire and the title appears to stall at 20+ cycles. agiVmTickSeconds(&vm, 1u); printf(" cycle %u: pc=%u/%u logic=%u callDepth=%u\n", (unsigned)cycles, (unsigned)vm.pc, (unsigned)vm.codeLength, (unsigned)vm.currentLogicId, (unsigned)vm.callDepth); reason = agiVmRun(&vm); printf(" halt=%s at pc=%u logic=%u", haltName(reason), (unsigned)vm.pc, (unsigned)vm.currentLogicId); if (reason == AGI_VM_HALT_UNKNOWN_OP) { printf(" opcode=0x%02X", (unsigned)vm.lastUnknownOp); } if (reason == AGI_VM_HALT_NEW_ROOM) { printf(" newRoom=%u", (unsigned)vm.newRoomId); } printf("\n"); if (reason == AGI_VM_HALT_NEW_ROOM) { prevRoom = vm.vars[0]; vm.vars[0] = vm.newRoomId; vm.vars[1] = prevRoom; // Reset most flags, then set flag 5 ("new room") to // signal the room logic that its init branch should // fire. In the real interpreter the engine also resets // a handful of cycle vars (input, signal, etc.) but for // the sub-D pipeline test the new-room flag is the only // one room logic actually checks. for (i = 0u; i < 32u; i++) { vm.flags[i] = 0u; } vm.flags[5] = 1u; agiVmResetToLogic(&vm, logic0, length, 0u); continue; } if (reason == AGI_VM_HALT_RETURN) { // End-of-cycle housekeeping the real interpreter would // perform: clear flag 5 (new.room) so subsequent cycles // don't re-run the room-init branch. vm.flags[5] = 0u; agiVmResetToLogic(&vm, logic0, length, 0u); continue; } break; } printf("===== trace =====\n"); printf(" fetchLogic: %u (first-loads only)\n", (unsigned)gTraceFetchLogic); printf(" loadPic: %u\n", (unsigned)gTraceLoadPic); printf(" drawPic: %u\n", (unsigned)gTraceDrawPic); printf(" showPic: %u\n", (unsigned)gTraceShowPic); printf(" discardPic: %u\n", (unsigned)gTraceDiscardPic); printf(" overlayPic: %u\n", (unsigned)gTraceOverlayPic); releaseCache(); agiResClose(&gGame); return 0; }