American spelling, aligned declarations. Resources a directory lists but the game's files lack count as absent, as with Sierra's interpreter.

This commit is contained in:
Scott Duensing 2026-10-19 17:11:42 -05:00
parent 850181d8a9
commit 65c380555f
3 changed files with 89 additions and 45 deletions

1
agi.h
View file

@ -464,6 +464,7 @@ bool agiQuirk(AgiQuirkE quirk);
void agiResClose(AgiGameT *game); void agiResClose(AgiGameT *game);
uint8_t *agiResLoad(const AgiGameT *game, AgiResTypeE type, uint16_t index, uint16_t *outLength); uint8_t *agiResLoad(const AgiGameT *game, AgiResTypeE type, uint16_t index, uint16_t *outLength);
bool agiResOpen(AgiGameT *game); 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); uint16_t agiWordMatch(const AgiGameT *game, const char *text, uint8_t *outLen);

View file

@ -64,6 +64,7 @@ static uint16_t detectVersion(void);
static void loadDirectory(AgiGameT *game, const char *fileName, AgiResTypeE type); static void loadDirectory(AgiGameT *game, const char *fileName, AgiResTypeE type);
static uint8_t *loadFile(const char *name, uint16_t maxBytes, uint16_t *outLength); static uint8_t *loadFile(const char *name, uint16_t maxBytes, uint16_t *outLength);
static bool loadObjects(AgiGameT *game); static bool loadObjects(AgiGameT *game);
static bool resHeaderRead(const AgiGameT *game, AgiResTypeE type, uint16_t index, uint8_t *header);
// ----- Tables ----- // ----- 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) ----- // ----- Public API (alphabetical) -----
// Undo Sierra's XOR with "Avis Durgan" (OBJECT, and LOGIC messages from // 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) { uint8_t *agiResLoad(const AgiGameT *game, AgiResTypeE type, uint16_t index, uint16_t *outLength) {
const AgiResEntryT *entry; FILE *fp;
FILE *fp; uint8_t header[VOL_HEADER_BYTES];
uint8_t header[VOL_HEADER_BYTES]; uint8_t *payload;
uint8_t *payload; uint16_t length;
uint16_t length;
long offset;
if (index >= game->resCount[type]) { if (!resHeaderRead(game, type, index, header)) {
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) {
return NULL; return NULL;
} }
// v3 resources are LZW-compressed and flag it in the volume byte. // 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; return NULL;
} }
fp = game->volFiles[game->resDir[type][index].volume];
length = (uint16_t)(header[3] | ((uint16_t)header[4] << 8)); length = (uint16_t)(header[3] | ((uint16_t)header[4] << 8));
if (length == 0u) { if (length == 0u) {
return NULL; 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 // 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 // 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 // ("pick up"). text is lower case with single spaces. Returns the word's

View file

@ -5,7 +5,10 @@
// hashes, a view's loop and cel counts with a hash of its decoded cels, a // 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 // 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 // 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/. // Run on the host (BLANK port) from a folder holding DATA/.
@ -29,6 +32,10 @@
#define MAX_CEL_PIXELS (255u * 255u) #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 ----- // ----- Prototypes -----
static uint32_t fnv(uint32_t hash, const uint8_t *p, uint32_t length); static uint32_t fnv(uint32_t hash, const uint8_t *p, uint32_t length);
@ -185,6 +192,8 @@ int main(void) {
uint8_t *pixels; uint8_t *pixels;
uint16_t n; uint16_t n;
uint16_t counts[AGI_RES_COUNT]; uint16_t counts[AGI_RES_COUNT];
uint16_t absent;
uint8_t t;
bool ok; bool ok;
memset(&cfg, 0, sizeof(cfg)); memset(&cfg, 0, sizeof(cfg));
@ -209,26 +218,37 @@ int main(void) {
return 1; return 1;
} }
memset(counts, 0, sizeof(counts)); memset(counts, 0, sizeof(counts));
ok = true; absent = 0u;
ok = true;
for (n = 0u; n < AGI_MAX_RESOURCES; n++) { 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) { for (t = 0u; t < AGI_RES_COUNT; t++) {
ok = testLogic(n) && ok; if (n >= gAgi->game.resCount[t] || gAgi->game.resDir[t][n].volume >= AGI_MAX_VOLUMES) {
counts[AGI_RES_LOGIC]++; continue;
} }
if (n < gAgi->game.resCount[AGI_RES_PIC] && gAgi->game.resDir[AGI_RES_PIC][n].volume < AGI_MAX_VOLUMES) { if (!agiResPresent(&gAgi->game, (AgiResTypeE)t, n)) {
ok = testPicture(n, visual, priority) && ok; fprintf(stderr, "%s %u: listed, but not in the game's files\n", kTypeNames[t], (unsigned)n);
counts[AGI_RES_PIC]++; printf("%s %u absent\n", kTypeNames[t], (unsigned)n);
} absent++;
if (n < gAgi->game.resCount[AGI_RES_VIEW] && gAgi->game.resDir[AGI_RES_VIEW][n].volume < AGI_MAX_VOLUMES) { continue;
ok = testView(n, pixels) && ok; }
counts[AGI_RES_VIEW]++; switch ((AgiResTypeE)t) {
} case AGI_RES_LOGIC:
if (n < gAgi->game.resCount[AGI_RES_SOUND] && gAgi->game.resDir[AGI_RES_SOUND][n].volume < AGI_MAX_VOLUMES) { ok = testLogic(n) && ok;
ok = testSound(n) && ok; break;
counts[AGI_RES_SOUND]++; 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], 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)");
(unsigned)counts[AGI_RES_VIEW], (unsigned)counts[AGI_RES_SOUND], ok ? "" : " -- MALFORMED (see above)");
return ok ? 0 : 1; return ok ? 0 : 1;
} }