joeylib2/examples/agi/agiRes.c

309 lines
8.4 KiB
C

// AGI v2 resource loader.
//
// Parses LOGDIR/PICDIR/VIEWDIR/SNDDIR into in-RAM index tables, keeps
// every VOL.x file open for the session, and exposes a single
// resource-by-id load primitive that hands back a freshly-allocated
// payload buffer.
//
// AGI v2 directory entry format (3 bytes, big-endian within the byte
// stream): byte0 high nibble is the volume number, the 20-bit value
// formed from (byte0 & 0x0F):byte1:byte2 is the file offset. All-FF
// means the slot is empty (no resource with that ID).
//
// VOL.x record header (5 bytes at the directory's offset): 0x12 0x34
// signature, then the volume number, then a little-endian 16-bit
// payload length. v2 payloads are stored raw; v3 sets bit 7 of the
// volume byte to flag LZW compression (not handled here).
#include "agi.h"
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// AGI directory files are flat arrays of 3-byte entries. The maximum
// directory length the loader will accept is bounded by AGI_MAX_-
// RESOURCES; larger files are reported as bad rather than truncated.
#define AGI_DIR_ENTRY_BYTES 3
// VOL.x resource records start with a 5-byte header.
#define AGI_VOL_HEADER_BYTES 5
#define AGI_VOL_SIG_0 0x12
#define AGI_VOL_SIG_1 0x34
#define AGI_VOL_COMPRESSED 0x80
#define AGI_VOL_VOLUME_MASK 0x7F
// Max directory + path string size. AGI filenames are 7 chars max;
// gameDir is application-controlled. 256 leaves comfortable margin.
#define AGI_PATH_MAX 256
// Empty-slot sentinel: must be >= AGI_MAX_VOLUMES so agiResLoad
// rejects empty entries. The on-disk empty marker is 0xFFFFFF (high
// nibble 0xF = volume 15); 15 is a legitimate volume number, so we
// don't reuse it as the sentinel.
#define AGI_DIR_EMPTY_VOLUME 0xFF
// IIgs 64 KB code-bank rule: park this TU in its own load segment
// so it doesn't pile onto _ROOT (which still holds main() and the
// ORCA-C startup). Cross-platform builds ignore this pragma.
#ifdef JOEYLIB_PLATFORM_IIGS
segment "AGIRES";
#endif
// ----- Prototypes -----
static bool buildPath(char *out, size_t outSize, const char *dir, const char *file);
static bool buildVolPath(char *out, size_t outSize, const char *dir, uint8_t volNum);
static bool loadDirectory(AgiGameT *game, AgiResTypeE type, const char *gameDir, const char *fileName);
static bool openVolumes(AgiGameT *game, const char *gameDir);
static const char *resTypeFileName(AgiResTypeE type);
// ----- Internal helpers (alphabetical) -----
static bool buildPath(char *out, size_t outSize, const char *dir, const char *file) {
size_t dirLen;
size_t fileLen;
dirLen = strlen(dir);
fileLen = strlen(file);
if (dirLen + 1 + fileLen + 1 > outSize) {
return false;
}
memcpy(out, dir, dirLen);
out[dirLen] = '/';
memcpy(out + dirLen + 1, file, fileLen);
out[dirLen + 1 + fileLen] = '\0';
return true;
}
static bool buildVolPath(char *out, size_t outSize, const char *dir, uint8_t volNum) {
size_t dirLen;
char *p;
dirLen = strlen(dir);
// "/VOL." + up to 2 digits + NUL = 8 chars max.
if (dirLen + 8 > outSize) {
return false;
}
memcpy(out, dir, dirLen);
p = out + dirLen;
*p++ = '/';
*p++ = 'V';
*p++ = 'O';
*p++ = 'L';
*p++ = '.';
if (volNum >= 10u) {
*p++ = (char)('0' + (volNum / 10u));
*p++ = (char)('0' + (volNum % 10u));
} else {
*p++ = (char)('0' + volNum);
}
*p = '\0';
return true;
}
static bool loadDirectory(AgiGameT *game, AgiResTypeE type, const char *gameDir, const char *fileName) {
char path[AGI_PATH_MAX];
FILE *fp;
long fileSize;
uint16_t entryCount;
uint16_t i;
uint8_t buf[AGI_DIR_ENTRY_BYTES];
uint8_t volNibble;
uint32_t offset;
if (!buildPath(path, AGI_PATH_MAX, gameDir, fileName)) {
return false;
}
fp = fopen(path, "rb");
if (fp == NULL) {
return false;
}
if (fseek(fp, 0L, SEEK_END) != 0) {
fclose(fp);
return false;
}
fileSize = ftell(fp);
if (fileSize < 0 || fileSize > (long)(AGI_MAX_RESOURCES * AGI_DIR_ENTRY_BYTES)) {
fclose(fp);
return false;
}
if ((fileSize % AGI_DIR_ENTRY_BYTES) != 0) {
fclose(fp);
return false;
}
if (fseek(fp, 0L, SEEK_SET) != 0) {
fclose(fp);
return false;
}
entryCount = (uint16_t)(fileSize / AGI_DIR_ENTRY_BYTES);
for (i = 0; i < entryCount; i++) {
if (fread(buf, 1, AGI_DIR_ENTRY_BYTES, fp) != AGI_DIR_ENTRY_BYTES) {
fclose(fp);
return false;
}
volNibble = (uint8_t)(buf[0] >> 4);
// 20-bit offset, cast to uint32_t before shift so ORCA-C's
// 16-bit int doesn't truncate the (& 0x0F) << 16 term.
offset = ((uint32_t)(buf[0] & 0x0F) << 16) | ((uint32_t)buf[1] << 8) | (uint32_t)buf[2];
if (buf[0] == 0xFF && buf[1] == 0xFF && buf[2] == 0xFF) {
game->resDir[type][i].volume = AGI_DIR_EMPTY_VOLUME;
game->resDir[type][i].offset = 0u;
} else {
game->resDir[type][i].volume = volNibble;
game->resDir[type][i].offset = offset;
}
}
fclose(fp);
game->resCount[type] = entryCount;
return true;
}
static bool openVolumes(AgiGameT *game, const char *gameDir) {
char path[AGI_PATH_MAX];
uint8_t v;
bool anyOpened;
anyOpened = false;
for (v = 0; v < AGI_MAX_VOLUMES; v++) {
if (!buildVolPath(path, AGI_PATH_MAX, gameDir, v)) {
return false;
}
game->volFiles[v] = fopen(path, "rb");
if (game->volFiles[v] != NULL) {
anyOpened = true;
}
}
return anyOpened;
}
static const char *resTypeFileName(AgiResTypeE type) {
switch (type) {
case AGI_RES_LOGIC: return "LOGDIR";
case AGI_RES_PIC: return "PICDIR";
case AGI_RES_VIEW: return "VIEWDIR";
case AGI_RES_SOUND: return "SNDDIR";
default: return NULL;
}
}
// ----- Public API (alphabetical) -----
void agiResClose(AgiGameT *game) {
uint8_t v;
uint8_t t;
for (v = 0; v < AGI_MAX_VOLUMES; v++) {
if (game->volFiles[v] != NULL) {
fclose(game->volFiles[v]);
game->volFiles[v] = NULL;
}
}
for (t = 0; t < AGI_RES_COUNT; t++) {
game->resCount[t] = 0u;
}
}
uint8_t *agiResLoad(const AgiGameT *game, AgiResTypeE type, uint16_t index, uint16_t *outLength) {
const AgiResEntryT *entry;
FILE *fp;
uint8_t header[AGI_VOL_HEADER_BYTES];
uint8_t *payload;
uint16_t length;
uint8_t flagsAndVolume;
if (outLength != NULL) {
*outLength = 0u;
}
if (type >= AGI_RES_COUNT) {
return NULL;
}
if (index >= game->resCount[type]) {
return NULL;
}
entry = &game->resDir[type][index];
if (entry->volume >= AGI_MAX_VOLUMES) {
return NULL;
}
fp = game->volFiles[entry->volume];
if (fp == NULL) {
return NULL;
}
if (fseek(fp, (long)entry->offset, SEEK_SET) != 0) {
return NULL;
}
if (fread(header, 1, AGI_VOL_HEADER_BYTES, fp) != AGI_VOL_HEADER_BYTES) {
return NULL;
}
if (header[0] != AGI_VOL_SIG_0 || header[1] != AGI_VOL_SIG_1) {
return NULL;
}
flagsAndVolume = header[2];
if (flagsAndVolume & AGI_VOL_COMPRESSED) {
// v3 LZW-compressed resource. Phase 2 work; not handled yet.
return NULL;
}
if ((flagsAndVolume & AGI_VOL_VOLUME_MASK) != entry->volume) {
return NULL;
}
length = (uint16_t)(((uint16_t)header[4] << 8) | (uint16_t)header[3]);
if (length == 0u) {
return NULL;
}
payload = (uint8_t *)malloc(length);
if (payload == NULL) {
return NULL;
}
if (fread(payload, 1, length, fp) != length) {
free(payload);
return NULL;
}
if (outLength != NULL) {
*outLength = length;
}
return payload;
}
bool agiResOpen(AgiGameT *game, const char *gameDir) {
uint8_t t;
memset(game, 0, sizeof(*game));
for (t = 0; t < AGI_RES_COUNT; t++) {
if (!loadDirectory(game, (AgiResTypeE)t, gameDir, resTypeFileName((AgiResTypeE)t))) {
agiResClose(game);
return false;
}
}
if (!openVolumes(game, gameDir)) {
agiResClose(game);
return false;
}
return true;
}