// testResources: every resource of the game in DATA/ loads and decodes. // // Prints the interpreter version the game's files name, then one line per // resource to stdout -- a picture's visual and priority screens as FNV-1a // hashes, a view's loop and cel counts with a hash of its decoded cels, a // logic's code and message counts, a sound's note counts -- for // scripts/test-agi.sh to compare, and a summary line on stderr. Exits 1 // when a resource is malformed. // // Run on the host (BLANK port) from a folder holding DATA/. #include "agi.h" #include #include #include #include "joey/core.h" #define FNV_OFFSET_BASIS 2166136261u #define FNV_PRIME 16777619u #define LOGIC_HEADER_BYTES 2u #define SOUND_VOICES 4u #define SOUND_NOTE_BYTES 5u #define SOUND_END_BYTE 0xFFu #define RLE_COLOR_SHIFT 4u #define RLE_COUNT_MASK 0x0Fu #define MAX_CEL_PIXELS (255u * 255u) // ----- Prototypes ----- static uint32_t fnv(uint32_t hash, const uint8_t *p, uint32_t length); static bool testLogic(uint16_t n); static bool testPicture(uint16_t n, uint8_t *visual, uint8_t *priority); static bool testSound(uint16_t n); static bool testView(uint16_t n, uint8_t *pixels); // ----- Internal helpers (alphabetical) ----- static uint32_t fnv(uint32_t hash, const uint8_t *p, uint32_t length) { uint32_t i; for (i = 0u; i < length; i++) { hash = (hash ^ p[i]) * FNV_PRIME; } return hash; } static bool testLogic(uint16_t n) { const uint8_t *raw; uint16_t codeLen; if (!vmLoadLogic((uint8_t)n)) { fprintf(stderr, "logic %u: does not load\n", (unsigned)n); return false; } raw = gAgi->logicRaw[n]; codeLen = (uint16_t)(raw[0] | ((uint16_t)raw[1] << 8)); printf("logic %u code %u messages %u\n", (unsigned)n, (unsigned)codeLen, (unsigned)raw[LOGIC_HEADER_BYTES + codeLen]); return true; } static bool testPicture(uint16_t n, uint8_t *visual, uint8_t *priority) { uint8_t *data; uint16_t length; data = agiResLoad(&gAgi->game, AGI_RES_PIC, n, &length); if (data == NULL) { fprintf(stderr, "picture %u: does not load\n", (unsigned)n); return false; } memset(visual, AGI_PIC_BG_COLOR, AGI_PIXELS); memset(priority, AGI_PIC_BG_PRIORITY, AGI_PIXELS); if (!agiPicDecode(visual, priority, data, length)) { fprintf(stderr, "picture %u: does not decode\n", (unsigned)n); jlFree(data); return false; } jlFree(data); printf("picture %u visual %08x priority %08x\n", (unsigned)n, (unsigned)fnv(FNV_OFFSET_BASIS, visual, AGI_PIXELS), (unsigned)fnv(FNV_OFFSET_BASIS, priority, AGI_PIXELS)); return true; } // Every voice's notes must end (FF FF) inside the resource. static bool testSound(uint16_t n) { uint8_t *data; uint16_t length; uint16_t pos; uint16_t notes[SOUND_VOICES]; uint8_t v; data = agiResLoad(&gAgi->game, AGI_RES_SOUND, n, &length); if (data == NULL || length < SOUND_VOICES * 2u) { fprintf(stderr, "sound %u: does not load\n", (unsigned)n); jlFree(data); return false; } for (v = 0u; v < SOUND_VOICES; v++) { pos = (uint16_t)(data[v * 2u] | ((uint16_t)data[v * 2u + 1u] << 8)); notes[v] = 0u; while ((uint32_t)pos + 1u < length && !(data[pos] == SOUND_END_BYTE && data[pos + 1u] == SOUND_END_BYTE)) { pos = (uint16_t)(pos + SOUND_NOTE_BYTES); notes[v]++; } if ((uint32_t)pos + 1u >= length) { fprintf(stderr, "sound %u: voice %u runs off the end\n", (unsigned)n, (unsigned)v); jlFree(data); return false; } } jlFree(data); printf("sound %u notes %u %u %u %u\n", (unsigned)n, (unsigned)notes[0], (unsigned)notes[1], (unsigned)notes[2], (unsigned)notes[3]); return true; } // Decode every cel's runs: no row may run past the cel's width or the // resource's end. static bool testView(uint16_t n, uint8_t *pixels) { const AgiViewT *view; const AgiCelT *c; const uint8_t *rle; const uint8_t *end; uint32_t hash; uint16_t cels; uint8_t loop; uint8_t i; uint8_t row; uint8_t x; uint8_t run; view = agiViewGet((uint8_t)n); if (view == NULL) { fprintf(stderr, "view %u: does not load\n", (unsigned)n); return false; } end = view->raw + view->rawLength; hash = FNV_OFFSET_BASIS; cels = 0u; for (loop = 0u; loop < view->loopCount; loop++) { for (i = 0u; i < view->loops[loop].celCount; i++) { c = &view->loops[loop].cels[i]; rle = c->rle; memset(pixels, c->transparent, (size_t)c->width * c->height); for (row = 0u; row < c->height; row++) { x = 0u; while (rle < end && *rle != 0u) { run = (uint8_t)(*rle & RLE_COUNT_MASK); if ((uint16_t)x + run > c->width) { fprintf(stderr, "view %u loop %u cel %u: row %u is too wide\n", (unsigned)n, (unsigned)loop, (unsigned)i, (unsigned)row); return false; } memset(&pixels[(uint16_t)row * c->width + x], *rle >> RLE_COLOR_SHIFT, run); x = (uint8_t)(x + run); rle++; } if (rle >= end) { fprintf(stderr, "view %u loop %u cel %u: runs off the end\n", (unsigned)n, (unsigned)loop, (unsigned)i); return false; } rle++; } hash = fnv(hash, pixels, (uint32_t)c->width * c->height); cels++; } } printf("view %u loops %u cels %u pixels %08x\n", (unsigned)n, (unsigned)view->loopCount, (unsigned)cels, (unsigned)hash); return true; } // ----- Entry point ----- int main(void) { jlConfigT cfg; uint8_t *visual; uint8_t *priority; uint8_t *pixels; uint16_t n; uint16_t counts[AGI_RES_COUNT]; bool ok; memset(&cfg, 0, sizeof(cfg)); if (!jlInit(&cfg)) { fprintf(stderr, "jlInit failed\n"); return 1; } gAgi = (AgiStateT *)calloc(1u, sizeof(AgiStateT)); if (gAgi == NULL || !agiResOpen(&gAgi->game)) { fprintf(stderr, "no game in DATA/\n"); return 1; } // The Sierra interpreter version the game's files name (its quirks // follow it). printf("interpreter %u\n", (unsigned)gAgi->game.interpreterVersion); visual = (uint8_t *)malloc(AGI_PIXELS); priority = (uint8_t *)malloc(AGI_PIXELS); pixels = (uint8_t *)malloc(MAX_CEL_PIXELS); if (visual == NULL || priority == NULL || pixels == NULL) { fprintf(stderr, "out of memory\n"); return 1; } memset(counts, 0, sizeof(counts)); ok = true; for (n = 0u; n < AGI_MAX_RESOURCES; n++) { if (n < gAgi->game.resCount[AGI_RES_LOGIC] && gAgi->game.resDir[AGI_RES_LOGIC][n].volume < AGI_MAX_VOLUMES) { ok = testLogic(n) && ok; counts[AGI_RES_LOGIC]++; } if (n < gAgi->game.resCount[AGI_RES_PIC] && gAgi->game.resDir[AGI_RES_PIC][n].volume < AGI_MAX_VOLUMES) { ok = testPicture(n, visual, priority) && ok; counts[AGI_RES_PIC]++; } if (n < gAgi->game.resCount[AGI_RES_VIEW] && gAgi->game.resDir[AGI_RES_VIEW][n].volume < AGI_MAX_VOLUMES) { ok = testView(n, pixels) && ok; counts[AGI_RES_VIEW]++; } if (n < gAgi->game.resCount[AGI_RES_SOUND] && gAgi->game.resDir[AGI_RES_SOUND][n].volume < AGI_MAX_VOLUMES) { ok = testSound(n) && ok; counts[AGI_RES_SOUND]++; } } fprintf(stderr, "%u logics, %u pictures, %u views, %u sounds%s\n", (unsigned)counts[AGI_RES_LOGIC], (unsigned)counts[AGI_RES_PIC], (unsigned)counts[AGI_RES_VIEW], (unsigned)counts[AGI_RES_SOUND], ok ? "" : " -- MALFORMED (see above)"); return ok ? 0 : 1; }