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:
parent
850181d8a9
commit
65c380555f
3 changed files with 89 additions and 45 deletions
1
agi.h
1
agi.h
|
|
@ -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);
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
75
agiRes.c
75
agiRes.c
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue