233 lines
8 KiB
C
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;
|
|
}
|