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

198 lines
6.5 KiB
C

// AGI v2 VIEW parsing (AGI specification chapter 8).
//
// A view is a header (loop count, description offset, a loop offset per
// loop), each loop a cel count and cel offsets relative to the loop, each
// cel a width, height and a byte holding the transparent colour (low
// nibble) and the mirroring bits (high nibble), then run-length rows: a
// byte of colour (high nibble) and run length (low nibble), 0 ending a
// row. Loops that mirror each other share their cels; a cel whose
// mirroring bit is set is drawn flipped in every loop except the one its
// loop field names.
#include "agi.h"
#include <stddef.h>
#include "joey/core.h"
#define VIEW_LOOP_COUNT_BYTE 2u
#define VIEW_DESC_OFFSET_BYTE 3u
#define VIEW_LOOP_TABLE_BYTE 5u
#define CEL_HEADER_BYTES 3u
#define CEL_TRANSPARENT_MASK 0x0Fu
#define CEL_MIRRORED 0x80u
#define CEL_MIRROR_LOOP_SHIFT 4u
#define CEL_MIRROR_LOOP_MASK 0x07u
// ----- Prototypes -----
static bool celCountOf(const uint8_t *raw, uint16_t length, uint8_t loopCount, uint16_t *outCels);
static bool inUse(const AgiViewT *view);
static AgiViewT *load(const AgiGameT *game, uint8_t viewId);
static uint16_t readLe16(const uint8_t *p);
// ----- Internal helpers (alphabetical) -----
// Check the loop table and every cel header against the resource's length
// and count the cels, so the view can be built in one allocation.
static bool celCountOf(const uint8_t *raw, uint16_t length, uint8_t loopCount, uint16_t *outCels) {
uint8_t loop;
uint8_t cel;
uint8_t count;
uint16_t loopOffset;
uint16_t celOffset;
*outCels = 0u;
for (loop = 0u; loop < loopCount; loop++) {
loopOffset = readLe16(&raw[VIEW_LOOP_TABLE_BYTE + (uint16_t)loop * 2u]);
if (loopOffset >= length) {
return false;
}
count = raw[loopOffset];
if ((uint32_t)loopOffset + 1u + (uint32_t)count * 2u > length) {
return false;
}
for (cel = 0u; cel < count; cel++) {
celOffset = (uint16_t)(loopOffset + readLe16(&raw[loopOffset + 1u + (uint16_t)cel * 2u]));
if ((uint32_t)celOffset + CEL_HEADER_BYTES > length) {
return false;
}
}
*outCels = (uint16_t)(*outCels + count);
}
return true;
}
static bool inUse(const AgiViewT *view) {
uint8_t i;
for (i = 0u; i < gAgi->objectCount; i++) {
if (gAgi->objects[i].view == view) {
return true;
}
}
return false;
}
// The view, its loop table and every cel header share one allocation (on
// the IIgs each is a Memory Manager handle); the cels point into raw.
static AgiViewT *load(const AgiGameT *game, uint8_t viewId) {
AgiViewT *view;
AgiCelT *cels;
uint8_t *raw;
uint16_t length;
uint16_t celTotal;
uint16_t loopOffset;
uint16_t celOffset;
uint16_t descOffset;
uint8_t loopCount;
uint8_t loop;
uint8_t cel;
raw = agiResLoad(game, AGI_RES_VIEW, viewId, &length);
if (raw == NULL) {
return NULL;
}
loopCount = (length < VIEW_LOOP_TABLE_BYTE) ? 0u : raw[VIEW_LOOP_COUNT_BYTE];
if (length < VIEW_LOOP_TABLE_BYTE || (uint32_t)VIEW_LOOP_TABLE_BYTE + (uint32_t)loopCount * 2u > length ||
!celCountOf(raw, length, loopCount, &celTotal)) {
jlFree(raw);
return NULL;
}
view = (AgiViewT *)jlAlloc((uint32_t)sizeof(AgiViewT) + (uint32_t)loopCount * sizeof(AgiLoopT) + (uint32_t)celTotal * sizeof(AgiCelT));
if (view == NULL) {
jlFree(raw);
return NULL;
}
view->raw = raw;
view->rawLength = length;
view->loopCount = loopCount;
view->loops = (AgiLoopT *)(view + 1);
view->description = NULL;
descOffset = readLe16(&raw[VIEW_DESC_OFFSET_BYTE]);
if (descOffset != 0u && descOffset < length) {
view->description = (const char *)&raw[descOffset];
}
cels = (AgiCelT *)(view->loops + loopCount);
for (loop = 0u; loop < loopCount; loop++) {
loopOffset = readLe16(&raw[VIEW_LOOP_TABLE_BYTE + (uint16_t)loop * 2u]);
view->loops[loop].celCount = raw[loopOffset];
view->loops[loop].cels = cels;
for (cel = 0u; cel < view->loops[loop].celCount; cel++) {
celOffset = (uint16_t)(loopOffset + readLe16(&raw[loopOffset + 1u + (uint16_t)cel * 2u]));
cels->width = raw[celOffset];
cels->height = raw[celOffset + 1u];
cels->transparent = (uint8_t)(raw[celOffset + 2u] & CEL_TRANSPARENT_MASK);
cels->mirrorFlags = (uint8_t)(raw[celOffset + 2u] & (uint8_t)~CEL_TRANSPARENT_MASK);
cels->rle = &raw[celOffset + CEL_HEADER_BYTES];
cels++;
}
}
return view;
}
static uint16_t readLe16(const uint8_t *p) {
return (uint16_t)(p[0] | ((uint16_t)p[1] << 8));
}
// ----- Public API (alphabetical) -----
// Look up a cel, clamping the loop and cel to the view's range. *outMirror
// says whether it is drawn flipped in this loop.
const AgiCelT *agiViewCel(const AgiViewT *view, uint8_t loop, uint8_t cel, bool *outMirror) {
const AgiCelT *c;
*outMirror = false;
if (view == NULL || view->loopCount == 0u) {
return NULL;
}
if (loop >= view->loopCount) {
loop = (uint8_t)(view->loopCount - 1u);
}
if (view->loops[loop].celCount == 0u) {
return NULL;
}
if (cel >= view->loops[loop].celCount) {
cel = (uint8_t)(view->loops[loop].celCount - 1u);
}
c = &view->loops[loop].cels[cel];
if ((c->mirrorFlags & CEL_MIRRORED) != 0u &&
((c->mirrorFlags >> CEL_MIRROR_LOOP_SHIFT) & CEL_MIRROR_LOOP_MASK) != loop) {
*outMirror = true;
}
return c;
}
// new.room: free the views no object still shows (an unanimated object
// keeps its view for the next room, as ego does), as Sierra's interpreter
// frees the old room's resources.
void agiViewDiscardUnused(void) {
uint16_t i;
for (i = 0u; i < AGI_MAX_RESOURCES; i++) {
if (gAgi->views[i] != NULL && !inUse(gAgi->views[i])) {
jlFree(gAgi->views[i]->raw);
jlFree(gAgi->views[i]);
gAgi->views[i] = NULL;
}
}
}
// A view from the cache, loaded on first use; NULL when the game has none
// by that number. Within a room views stay loaded (discard.view is a
// no-op).
const AgiViewT *agiViewGet(uint8_t viewId) {
if (gAgi->views[viewId] == NULL) {
gAgi->views[viewId] = load(&gAgi->game, viewId);
}
return gAgi->views[viewId];
}