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