309 lines
8.4 KiB
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;
|
|
}
|