// 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. A directory entry with no resource in the // game's files (agiResPresent) is listed as absent, not malformed: Sierra's // interpreter reads a resource only when a script loads it, and shipped // games list some they do not hold (King's Quest 1's sounds 34 to 37). // // 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 SOUND_TYPE_BYTES 2u #define SOUND_IIGS_SAMPLE 1u #define SOUND_IIGS_MIDI 2u #define SOUND_PC 8u #define RLE_COLOR_SHIFT 4u #define RLE_COUNT_MASK 0x0Fu #define MAX_CEL_PIXELS (255u * 255u) // What the lines call each resource type, by AgiResTypeE. static const char *const kTypeNames[AGI_RES_COUNT] = { "logic", "picture", "view", "sound" }; // ----- 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, uint16_t *truncated); 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 should end (FF FF) inside the resource. One that runs // to the resource's end instead is listed as truncated, not malformed: // King's Quest III 2.14 shipped its sound 36 cut to 1035 of its 2281 bytes, // and the player ends a voice where the resource ends. A PC sound's // first word is 8, its first voice's offset; anything else is another // machine's sound and only listed: an Apple IIgs sample or MIDI (first word 1 // or 2; King's Quest IV's IIgs release has nothing else), or an unknown kind // (Demo 3's PC data carries sounds 209 to 225 starting 0x0101, which no // logic plays). static bool testSound(uint16_t n, uint16_t *truncated) { uint8_t *data; uint16_t length; uint16_t pos; uint16_t notes[SOUND_VOICES]; uint16_t type; uint8_t v; bool cut; data = agiResLoad(&gAgi->game, AGI_RES_SOUND, n, &length); type = (data != NULL && length >= SOUND_TYPE_BYTES) ? (uint16_t)(data[0] | ((uint16_t)data[1] << 8)) : 0u; if (data != NULL && type != SOUND_PC) { printf("sound %u %s %u bytes\n", (unsigned)n, (type == SOUND_IIGS_SAMPLE) ? "iigs sample" : ((type == SOUND_IIGS_MIDI) ? "iigs midi" : "not a pc sound"), (unsigned)length); jlFree(data); return true; } if (data == NULL || length < SOUND_VOICES * 2u) { fprintf(stderr, "sound %u: does not load\n", (unsigned)n); jlFree(data); return false; } cut = 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 to the end of the resource\n", (unsigned)n, (unsigned)v); cut = true; } } jlFree(data); if (cut) { (*truncated)++; } printf("sound %u%s notes %u %u %u %u\n", (unsigned)n, cut ? " truncated" : "", (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]; uint16_t absent; uint16_t truncated; uint8_t t; bool ok; memset(&cfg, 0, sizeof(cfg)); cfg.noAudio = true; 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)); absent = 0u; truncated = 0u; ok = true; for (n = 0u; n < AGI_MAX_RESOURCES; n++) { for (t = 0u; t < AGI_RES_COUNT; t++) { if (n >= gAgi->game.resCount[t] || gAgi->game.resDir[t][n].volume >= AGI_MAX_VOLUMES) { continue; } if (!agiResPresent(&gAgi->game, (AgiResTypeE)t, n)) { fprintf(stderr, "%s %u: listed, but not in the game's files\n", kTypeNames[t], (unsigned)n); printf("%s %u absent\n", kTypeNames[t], (unsigned)n); absent++; continue; } switch ((AgiResTypeE)t) { case AGI_RES_LOGIC: ok = testLogic(n) && ok; break; case AGI_RES_PIC: ok = testPicture(n, visual, priority) && ok; break; case AGI_RES_VIEW: ok = testView(n, pixels) && ok; break; case AGI_RES_SOUND: default: ok = testSound(n, &truncated) && ok; break; } counts[t]++; } } fprintf(stderr, "%u logics, %u pictures, %u views, %u sounds, %u absent, %u truncated%s\n", (unsigned)counts[AGI_RES_LOGIC], (unsigned)counts[AGI_RES_PIC], (unsigned)counts[AGI_RES_VIEW], (unsigned)counts[AGI_RES_SOUND], (unsigned)absent, (unsigned)truncated, ok ? "" : " -- MALFORMED (see above)"); return ok ? 0 : 1; }