// 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 #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]; }