diff --git a/agi.h b/agi.h index 3cd0e64..0090b1f 100644 --- a/agi.h +++ b/agi.h @@ -464,6 +464,7 @@ bool agiQuirk(AgiQuirkE quirk); void agiResClose(AgiGameT *game); uint8_t *agiResLoad(const AgiGameT *game, AgiResTypeE type, uint16_t index, uint16_t *outLength); bool agiResOpen(AgiGameT *game); +bool agiResPresent(const AgiGameT *game, AgiResTypeE type, uint16_t index); uint16_t agiWordMatch(const AgiGameT *game, const char *text, uint8_t *outLen); diff --git a/agiRes.c b/agiRes.c index 25dac9a..c504230 100644 --- a/agiRes.c +++ b/agiRes.c @@ -64,6 +64,7 @@ static uint16_t detectVersion(void); static void loadDirectory(AgiGameT *game, const char *fileName, AgiResTypeE type); static uint8_t *loadFile(const char *name, uint16_t maxBytes, uint16_t *outLength); static bool loadObjects(AgiGameT *game); +static bool resHeaderRead(const AgiGameT *game, AgiResTypeE type, uint16_t index, uint8_t *header); // ----- Tables ----- @@ -237,6 +238,38 @@ static bool loadObjects(AgiGameT *game) { } +// Seek to resource index's header in its VOL file and read it: false when +// the number is past its directory or unused, or its entry names a VOL file +// the game lacks or a place in one that holds no header for that volume +// (12 34, then the entry's own volume). A directory can list a resource its +// VOL files do not hold -- King's Quest 1's sounds 34 to 37 point past the +// end of VOL.2, Space Quest 1's sound 0 into a VOL.15 -- which Sierra's +// interpreter, reading a resource only when a script loads it, meets only +// then. +static bool resHeaderRead(const AgiGameT *game, AgiResTypeE type, uint16_t index, uint8_t *header) { + const AgiResEntryT *entry; + FILE *fp; + long offset; + + if (index >= game->resCount[type]) { + return false; + } + entry = &game->resDir[type][index]; + if (entry->volume >= AGI_MAX_VOLUMES) { + return false; + } + fp = game->volFiles[entry->volume]; + if (fp == NULL) { + return false; + } + offset = ((long)entry->offsetHigh << DIR_OFFSET_HIGH_SHIFT) | (long)entry->offsetLow; + if (fseek(fp, offset, SEEK_SET) != 0 || fread(header, 1, VOL_HEADER_BYTES, fp) != VOL_HEADER_BYTES) { + return false; + } + return header[0] == VOL_SIG_0 && header[1] == VOL_SIG_1 && (header[2] & VOL_VOLUME_MASK) == entry->volume; +} + + // ----- Public API (alphabetical) ----- // Undo Sierra's XOR with "Avis Durgan" (OBJECT, and LOGIC messages from @@ -297,38 +330,19 @@ void agiResClose(AgiGameT *game) { uint8_t *agiResLoad(const AgiGameT *game, AgiResTypeE type, uint16_t index, uint16_t *outLength) { - const AgiResEntryT *entry; - FILE *fp; - uint8_t header[VOL_HEADER_BYTES]; - uint8_t *payload; - uint16_t length; - long offset; + FILE *fp; + uint8_t header[VOL_HEADER_BYTES]; + uint8_t *payload; + uint16_t length; - if (index >= game->resCount[type]) { - return NULL; - } - entry = &game->resDir[type][index]; - if (entry->volume >= AGI_MAX_VOLUMES) { - return NULL; - } - fp = game->volFiles[entry->volume]; - if (fp == NULL) { - return NULL; - } - offset = ((long)entry->offsetHigh << DIR_OFFSET_HIGH_SHIFT) | (long)entry->offsetLow; - if (fseek(fp, offset, SEEK_SET) != 0) { - return NULL; - } - if (fread(header, 1, VOL_HEADER_BYTES, fp) != VOL_HEADER_BYTES) { - return NULL; - } - if (header[0] != VOL_SIG_0 || header[1] != VOL_SIG_1) { + if (!resHeaderRead(game, type, index, header)) { return NULL; } // v3 resources are LZW-compressed and flag it in the volume byte. - if ((header[2] & VOL_COMPRESSED) != 0u || (header[2] & VOL_VOLUME_MASK) != entry->volume) { + if ((header[2] & VOL_COMPRESSED) != 0u) { return NULL; } + fp = game->volFiles[game->resDir[type][index].volume]; length = (uint16_t)(header[3] | ((uint16_t)header[4] << 8)); if (length == 0u) { return NULL; @@ -389,6 +403,15 @@ bool agiResOpen(AgiGameT *game) { } +// Whether resource index of type is in the game's files: its entry points at +// a resource header in its VOL file (resHeaderRead). +bool agiResPresent(const AgiGameT *game, AgiResTypeE type, uint16_t index) { + uint8_t header[VOL_HEADER_BYTES]; + + return resHeaderRead(game, type, index, header); +} + + // Find the longest dictionary entry that starts `text` and ends at a word // boundary (the end of text or a space). Entries can hold several words // ("pick up"). text is lower case with single spaces. Returns the word's diff --git a/tests/testResources.c b/tests/testResources.c index 84a01d1..bd4ee34 100644 --- a/tests/testResources.c +++ b/tests/testResources.c @@ -5,7 +5,10 @@ // 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. +// 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/. @@ -29,6 +32,10 @@ #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); @@ -185,6 +192,8 @@ int main(void) { uint8_t *pixels; uint16_t n; uint16_t counts[AGI_RES_COUNT]; + uint16_t absent; + uint8_t t; bool ok; memset(&cfg, 0, sizeof(cfg)); @@ -209,26 +218,37 @@ int main(void) { return 1; } memset(counts, 0, sizeof(counts)); - ok = true; + absent = 0u; + 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]++; + 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) && ok; + break; + } + counts[t]++; } } - 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)"); + fprintf(stderr, "%u logics, %u pictures, %u views, %u sounds, %u absent%s\n", (unsigned)counts[AGI_RES_LOGIC], (unsigned)counts[AGI_RES_PIC], (unsigned)counts[AGI_RES_VIEW], (unsigned)counts[AGI_RES_SOUND], (unsigned)absent, ok ? "" : " -- MALFORMED (see above)"); return ok ? 0 : 1; }