198 lines
6.5 KiB
C
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];
|
|
}
|