joeyagi/tests/testResources.c
2026-10-06 16:59:41 -05:00

233 lines
8 KiB
C

// 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}