773 lines
26 KiB
C
773 lines
26 KiB
C
// AGI resources: the four directories, the VOL files, WORDS.TOK and
|
|
// OBJECT (AGI specification chapters 5 and 10).
|
|
//
|
|
// A directory is a flat array of 3-byte entries: the high nibble of the
|
|
// first byte is the VOL file, the remaining 20 bits the offset into it;
|
|
// FF FF FF marks an unused number. A resource in a VOL file starts with a
|
|
// 5-byte header: 12 34, the volume, then the little-endian length.
|
|
//
|
|
// AGI v3 keeps the four directories in one file, <prefix>DIR (GRDIR,
|
|
// MH2DIR), after four little-endian offsets to them, and the volumes in
|
|
// <prefix>VOL.n. Its headers are 7 bytes: 12 34, the volume, the length,
|
|
// then the length stored. A stored length short of the length means LZW
|
|
// (expanded here); a picture whose volume byte has bit 7 set instead keeps
|
|
// the colors of F0 and F2 in a nibble each (spread back into bytes here),
|
|
// so the rest of the game sees v2 resources.
|
|
|
|
#include "agi.h"
|
|
|
|
#include <stddef.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include "joey/core.h"
|
|
#include "joey/file.h"
|
|
|
|
|
|
#define DIR_ENTRY_BYTES 3u
|
|
#define DIR_EMPTY_BYTE 0xFFu
|
|
#define DIR_EMPTY_VOLUME 0xFFu
|
|
#define DIR_VOLUME_SHIFT 4u
|
|
#define DIR_OFFSET_HIGH_MASK 0x0Fu
|
|
#define DIR_OFFSET_HIGH_SHIFT 16u
|
|
#define VOL_HEADER_V2_BYTES 5u
|
|
#define VOL_HEADER_V3_BYTES 7u
|
|
#define VOL_HEADER_MAX VOL_HEADER_V3_BYTES
|
|
#define VOL_SIG_0 0x12u
|
|
#define VOL_SIG_1 0x34u
|
|
#define VOL_VOLUME_BYTE 2u
|
|
#define VOL_LENGTH_BYTE 3u
|
|
#define VOL_STORED_BYTE 5u
|
|
#define VOL_NIBBLE_PICTURE 0x80u
|
|
#define VOL_VOLUME_MASK 0x7Fu
|
|
#define VOL_NAME_MAX (AGI_PREFIX_MAX + sizeof("VOL.15"))
|
|
#define DIR_NAME_MAX (AGI_PREFIX_MAX + sizeof("DIR"))
|
|
#define DECIMAL_BASE 10u
|
|
#define DIR_V3_OFFSETS_BYTES 8u
|
|
#define DIR_V3_MAX_BYTES (DIR_V3_OFFSETS_BYTES + AGI_RES_COUNT * AGI_MAX_RESOURCES * DIR_ENTRY_BYTES)
|
|
#define BYTE_BITS 8u
|
|
#define NIBBLE_BITS 4u
|
|
#define LOW_NIBBLE 0x0Fu
|
|
|
|
// AGI v3's LZW, as the data has it: codes come least significant bit
|
|
// first, 9 bits wide and growing a bit (to 11) as the next free code would
|
|
// need it; 0x100 starts the table over, 0x101 ends, and every stream
|
|
// starts with 0x100.
|
|
#define LZW_START_BITS 9u
|
|
#define LZW_MAX_BITS 11u
|
|
#define LZW_RESET 0x100u
|
|
#define LZW_END 0x101u
|
|
#define LZW_FIRST_FREE 0x102u
|
|
#define LZW_TABLE_SIZE (1u << LZW_MAX_BITS)
|
|
#define LZW_LITERAL_MAX 0xFFu
|
|
#define LZW_NONE 0xFFFFu
|
|
|
|
// The picture commands whose colors a v3 nibble picture packs.
|
|
#define PIC_SET_COLOR 0xF0u
|
|
#define PIC_SET_PRIORITY 0xF2u
|
|
#define PIC_END 0xFFu
|
|
|
|
// OBJECT: a 3-byte header (offset of the names, the number of animated
|
|
// objects) then a 3-byte entry per item (name offset, starting room).
|
|
// Offsets count from the first entry. The file is XORed with the same
|
|
// key as LOGIC messages.
|
|
#define OBJECT_HEADER_BYTES 3u
|
|
#define OBJECT_ENTRY_BYTES 3u
|
|
#define OBJECT_ROOM_BYTE 2u
|
|
#define OBJECT_MAX_BYTES 8192u
|
|
#define OBJECT_MAX_OBJS_BYTE 2u
|
|
|
|
// WORDS.TOK: 26 big-endian letter offsets, then entries of a prefix
|
|
// count, characters XORed with 0x7F (bit 7 marks the last) and a
|
|
// big-endian group number.
|
|
#define WORD_CHAR_XOR 0x7Fu
|
|
#define WORD_LAST_CHAR 0x80u
|
|
#define WORD_INDEX_BYTES 2u
|
|
|
|
#define CRYPT_KEY "Avis Durgan"
|
|
#define CRYPT_KEY_LEN 11u
|
|
|
|
// The interpreter's version string, "Version 2.435", in AGIDATA.OVL (or
|
|
// in the AGI executable of interpreters without one).
|
|
#define VERSION_TAG "Version "
|
|
#define VERSION_FILE_MAX 65535u
|
|
#define VERSION_MINOR_DIGITS 3u
|
|
#define VERSION_MAJOR_SCALE 1000u
|
|
|
|
|
|
// ----- Prototypes -----
|
|
|
|
static void buildVolName(const AgiGameT *game, char *out, uint8_t volNum);
|
|
static uint16_t detectVersion(void);
|
|
static uint8_t *expandNibblePicture(const uint8_t *in, uint16_t inLen, uint16_t *outLen);
|
|
static void fillDirectory(AgiGameT *game, AgiResTypeE type, const uint8_t *bytes, uint16_t length);
|
|
static void loadDirectory(AgiGameT *game, const char *fileName, AgiResTypeE type);
|
|
static void loadDirectoryV3(AgiGameT *game);
|
|
static uint8_t *loadFile(const char *name, uint16_t maxBytes, uint16_t *outLength);
|
|
static bool loadObjects(AgiGameT *game);
|
|
static bool lzwExpand(const uint8_t *in, uint16_t inLen, uint8_t *out, uint16_t outLen);
|
|
static uint8_t nibbleAt(const uint8_t *data, uint32_t k);
|
|
static uint16_t readLe16(const uint8_t *p);
|
|
static bool resHeaderRead(const AgiGameT *game, AgiResTypeE type, uint16_t index, uint8_t *header);
|
|
|
|
|
|
// ----- Tables -----
|
|
|
|
// The directory files, by AgiResTypeE.
|
|
static const char *const kDirNames[AGI_RES_COUNT] = { "LOGDIR", "PICDIR", "VIEWDIR", "SNDDIR" };
|
|
|
|
// Where the version string may be, in the order to look.
|
|
static const char *const kVersionFiles[] = { "AGIDATA.OVL", "AGI" };
|
|
|
|
// The v3 games' file-name prefixes, which also tell which game it is, and
|
|
// the interpreter to behave as when the folder names none (another
|
|
// machine's release: every King's Quest IV is 3.002.086's, the rest are
|
|
// taken as the last, 3.002.149). ScummVM finds the prefix by listing the
|
|
// folder for a file ending in DIR; JoeyLib lists no folders, so the known
|
|
// ones are tried.
|
|
typedef struct {
|
|
const char *prefix;
|
|
AgiGameIdE id;
|
|
uint16_t version;
|
|
} V3GameT;
|
|
|
|
static const V3GameT kV3Games[] = {
|
|
{ "KQ4", AGI_GAME_KQ4, AGI_VERSION_KQ4 },
|
|
{ "GR", AGI_GAME_GOLD_RUSH, AGI_VERSION_V3_ASSUMED },
|
|
{ "MH", AGI_GAME_MANHUNTER_1, AGI_VERSION_V3_ASSUMED },
|
|
{ "MH2", AGI_GAME_MANHUNTER_2, AGI_VERSION_V3_ASSUMED },
|
|
{ "BC", AGI_GAME_BLACK_CAULDRON, AGI_VERSION_V3_ASSUMED },
|
|
{ "DM", AGI_GAME_DEMO, AGI_VERSION_V3_ASSUMED }
|
|
};
|
|
|
|
// The interpreter versions each quirk is known in. Measured on 2.435
|
|
// (King's Quest III 2.00) only, the one Sierra interpreter at hand; they
|
|
// come from the PC interpreter's window, input-line and picture code, so
|
|
// they are assumed for every 2.x interpreter until one is measured
|
|
// otherwise.
|
|
typedef struct {
|
|
uint16_t first;
|
|
uint16_t last;
|
|
} QuirkRangeT;
|
|
|
|
static const QuirkRangeT kQuirkVersions[AGI_QUIRK_COUNT] = {
|
|
{ 2000u, 2999u }, // AGI_QUIRK_WINDOW_BYTE_X
|
|
{ 2000u, 2999u }, // AGI_QUIRK_ECHO_STEPS_BACK
|
|
{ 2000u, 2999u } // AGI_QUIRK_LINE_BYTE_ERROR
|
|
};
|
|
|
|
|
|
// ----- Internal helpers (alphabetical) -----
|
|
|
|
static void buildVolName(const AgiGameT *game, char *out, uint8_t volNum) {
|
|
(void)snprintf(out, VOL_NAME_MAX, "%sVOL.%u", game->prefix, (unsigned)volNum);
|
|
}
|
|
|
|
|
|
// The version Sierra's interpreter files in the game folder name, or
|
|
// AGI_VERSION_MEASURED when there are none.
|
|
static uint16_t detectVersion(void) {
|
|
uint8_t *data;
|
|
uint16_t length;
|
|
uint16_t i;
|
|
uint16_t j;
|
|
uint16_t major;
|
|
uint16_t minor;
|
|
uint8_t f;
|
|
|
|
for (f = 0u; f < sizeof(kVersionFiles) / sizeof(kVersionFiles[0]); f++) {
|
|
data = loadFile(kVersionFiles[f], VERSION_FILE_MAX, &length);
|
|
if (data == NULL) {
|
|
continue;
|
|
}
|
|
for (i = 0u; (uint32_t)i + sizeof(VERSION_TAG) + VERSION_MINOR_DIGITS + 1u < length; i++) {
|
|
if (memcmp(&data[i], VERSION_TAG, sizeof(VERSION_TAG) - 1u) != 0) {
|
|
continue;
|
|
}
|
|
j = (uint16_t)(i + sizeof(VERSION_TAG) - 1u);
|
|
if (data[j] < '0' || data[j] > '9' || data[j + 1u] != '.') {
|
|
continue;
|
|
}
|
|
major = (uint16_t)(data[j] - '0');
|
|
minor = 0u;
|
|
for (j = (uint16_t)(j + 2u); j < length && data[j] >= '0' && data[j] <= '9'; j++) {
|
|
minor = (uint16_t)(minor * DECIMAL_BASE + (uint16_t)(data[j] - '0'));
|
|
}
|
|
// AGI 3 names three parts, 3.002.149: the last one tells them
|
|
// apart (3149), as ScummVM numbers them.
|
|
if (j + 1u < length && data[j] == '.' && data[j + 1u] >= '0' && data[j + 1u] <= '9') {
|
|
minor = 0u;
|
|
for (j = (uint16_t)(j + 1u); j < length && data[j] >= '0' && data[j] <= '9'; j++) {
|
|
minor = (uint16_t)(minor * DECIMAL_BASE + (uint16_t)(data[j] - '0'));
|
|
}
|
|
}
|
|
jlFree(data);
|
|
return (uint16_t)(major * VERSION_MAJOR_SCALE + minor);
|
|
}
|
|
jlFree(data);
|
|
}
|
|
return AGI_VERSION_MEASURED;
|
|
}
|
|
|
|
|
|
// A v3 nibble picture spread back into bytes: each byte is two nibbles
|
|
// from wherever the last one ended, and the color after F0 or F2 one.
|
|
static uint8_t *expandNibblePicture(const uint8_t *in, uint16_t inLen, uint16_t *outLen) {
|
|
uint8_t *out;
|
|
uint32_t nibbles;
|
|
uint32_t k;
|
|
uint16_t n;
|
|
uint8_t b;
|
|
|
|
// Every output byte uses at least one nibble.
|
|
nibbles = (uint32_t)inLen * 2u;
|
|
if (nibbles > UINT16_MAX) {
|
|
nibbles = UINT16_MAX;
|
|
}
|
|
out = (uint8_t *)jlAlloc(nibbles);
|
|
if (out == NULL) {
|
|
return NULL;
|
|
}
|
|
n = 0u;
|
|
k = 0u;
|
|
while (k + 1u < nibbles) {
|
|
b = (uint8_t)((nibbleAt(in, k) << NIBBLE_BITS) | nibbleAt(in, k + 1u));
|
|
k += 2u;
|
|
out[n++] = b;
|
|
if (b == PIC_END) {
|
|
break;
|
|
}
|
|
if ((b == PIC_SET_COLOR || b == PIC_SET_PRIORITY) && k < nibbles) {
|
|
out[n++] = nibbleAt(in, k);
|
|
k++;
|
|
}
|
|
}
|
|
*outLen = n;
|
|
return out;
|
|
}
|
|
|
|
|
|
// The 3-byte entries of one directory.
|
|
static void fillDirectory(AgiGameT *game, AgiResTypeE type, const uint8_t *bytes, uint16_t length) {
|
|
uint16_t count;
|
|
uint16_t i;
|
|
|
|
count = (uint16_t)(length / DIR_ENTRY_BYTES);
|
|
if (count > AGI_MAX_RESOURCES) {
|
|
count = AGI_MAX_RESOURCES;
|
|
}
|
|
for (i = 0u; i < count; i++) {
|
|
const uint8_t *e = &bytes[i * DIR_ENTRY_BYTES];
|
|
AgiResEntryT *d = &game->resDir[type][i];
|
|
|
|
if (e[0] == DIR_EMPTY_BYTE && e[1] == DIR_EMPTY_BYTE && e[2] == DIR_EMPTY_BYTE) {
|
|
d->volume = DIR_EMPTY_VOLUME;
|
|
} else {
|
|
d->volume = (uint8_t)(e[0] >> DIR_VOLUME_SHIFT);
|
|
d->offsetHigh = (uint8_t)(e[0] & DIR_OFFSET_HIGH_MASK);
|
|
d->offsetLow = (uint16_t)(((uint16_t)e[1] << 8) | e[2]);
|
|
}
|
|
}
|
|
game->resCount[type] = count;
|
|
}
|
|
|
|
|
|
// A missing or empty directory just means none of that type (a game
|
|
// without sounds); the game needs logic 0 (agiResOpen).
|
|
static void loadDirectory(AgiGameT *game, const char *fileName, AgiResTypeE type) {
|
|
uint8_t *bytes;
|
|
uint16_t length;
|
|
|
|
bytes = loadFile(fileName, (uint16_t)(AGI_MAX_RESOURCES * DIR_ENTRY_BYTES), &length);
|
|
if (bytes == NULL) {
|
|
return;
|
|
}
|
|
fillDirectory(game, type, bytes, length);
|
|
jlFree(bytes);
|
|
}
|
|
|
|
|
|
// An AGI v3 game's <prefix>DIR: four offsets, then the logic, picture,
|
|
// view and sound directories, each running to the next (the last to the
|
|
// end of the file).
|
|
static void loadDirectoryV3(AgiGameT *game) {
|
|
char name[DIR_NAME_MAX];
|
|
uint8_t *bytes;
|
|
uint16_t length;
|
|
uint16_t start;
|
|
uint16_t end;
|
|
uint8_t p;
|
|
uint8_t t;
|
|
|
|
for (p = 0u; p < sizeof(kV3Games) / sizeof(kV3Games[0]); p++) {
|
|
(void)snprintf(name, sizeof(name), "%sDIR", kV3Games[p].prefix);
|
|
bytes = loadFile(name, (uint16_t)DIR_V3_MAX_BYTES, &length);
|
|
if (bytes == NULL) {
|
|
continue;
|
|
}
|
|
if (length >= DIR_V3_OFFSETS_BYTES) {
|
|
for (t = 0u; t < AGI_RES_COUNT; t++) {
|
|
start = readLe16(&bytes[t * 2u]);
|
|
end = (t + 1u < AGI_RES_COUNT) ? readLe16(&bytes[(t + 1u) * 2u]) : length;
|
|
if (start <= end && end <= length) {
|
|
fillDirectory(game, (AgiResTypeE)t, &bytes[start], (uint16_t)(end - start));
|
|
}
|
|
}
|
|
strcpy(game->prefix, kV3Games[p].prefix);
|
|
game->id = kV3Games[p].id;
|
|
game->interpreterVersion = kV3Games[p].version;
|
|
game->v3 = true;
|
|
}
|
|
jlFree(bytes);
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
// Read a whole data file, at most maxBytes of it, into a buffer of its
|
|
// own size (one fread: on the IIgs each call is a GS/OS round trip).
|
|
// Returns NULL when it is missing or empty.
|
|
static uint8_t *loadFile(const char *name, uint16_t maxBytes, uint16_t *outLength) {
|
|
FILE *fp;
|
|
uint8_t *buf;
|
|
long size;
|
|
|
|
fp = jlDataOpen(name, "rb");
|
|
if (fp == NULL) {
|
|
return NULL;
|
|
}
|
|
buf = NULL;
|
|
if (fseek(fp, 0L, SEEK_END) == 0) {
|
|
size = ftell(fp);
|
|
if (size > (long)maxBytes) {
|
|
size = (long)maxBytes;
|
|
}
|
|
if (size > 0L && fseek(fp, 0L, SEEK_SET) == 0) {
|
|
buf = (uint8_t *)jlAlloc((uint32_t)size);
|
|
}
|
|
if (buf != NULL && fread(buf, 1, (size_t)size, fp) != (size_t)size) {
|
|
jlFree(buf);
|
|
buf = NULL;
|
|
}
|
|
*outLength = (uint16_t)size;
|
|
}
|
|
fclose(fp);
|
|
return buf;
|
|
}
|
|
|
|
|
|
static bool loadObjects(AgiGameT *game) {
|
|
uint8_t *data;
|
|
uint16_t length;
|
|
uint16_t namesOffset;
|
|
|
|
data = loadFile("OBJECT", OBJECT_MAX_BYTES, &length);
|
|
if (data == NULL) {
|
|
return false;
|
|
}
|
|
if (length < OBJECT_HEADER_BYTES) {
|
|
jlFree(data);
|
|
return false;
|
|
}
|
|
// Most OBJECT files are XORed with the LOGIC messages' key, a few (the
|
|
// demos', the Christmas card's) not: encrypted, the first offset falls
|
|
// past the end of the file.
|
|
if (readLe16(data) > length) {
|
|
agiDecrypt(data, length);
|
|
}
|
|
namesOffset = readLe16(data);
|
|
game->objectData = data;
|
|
game->itemCount = (uint16_t)(namesOffset / OBJECT_ENTRY_BYTES);
|
|
if (game->itemCount > AGI_MAX_ITEMS) {
|
|
game->itemCount = AGI_MAX_ITEMS;
|
|
}
|
|
game->maxObjects = data[OBJECT_MAX_OBJS_BYTE];
|
|
if (game->maxObjects == 0u || game->maxObjects > AGI_MAX_OBJECTS) {
|
|
game->maxObjects = AGI_MAX_OBJECTS;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
// LZW-packed v3 data into exactly outLen bytes (any shortfall zeroed).
|
|
// False when the work tables cannot be had or the data makes no sense.
|
|
static bool lzwExpand(const uint8_t *in, uint16_t inLen, uint8_t *out, uint16_t outLen) {
|
|
uint16_t *prefix;
|
|
uint8_t *suffix;
|
|
uint8_t *stack;
|
|
uint32_t bits;
|
|
uint8_t bitCount;
|
|
uint8_t width;
|
|
uint16_t inPos;
|
|
uint16_t outPos;
|
|
uint16_t next;
|
|
uint16_t prev;
|
|
uint16_t code;
|
|
uint16_t cur;
|
|
uint16_t depth;
|
|
uint8_t first;
|
|
bool ok;
|
|
|
|
prefix = (uint16_t *)jlAlloc(LZW_TABLE_SIZE * sizeof(uint16_t));
|
|
suffix = (uint8_t *)jlAlloc(LZW_TABLE_SIZE);
|
|
stack = (uint8_t *)jlAlloc(LZW_TABLE_SIZE);
|
|
ok = prefix != NULL && suffix != NULL && stack != NULL;
|
|
memset(out, 0, outLen);
|
|
bits = 0u;
|
|
bitCount = 0u;
|
|
width = LZW_START_BITS;
|
|
inPos = 0u;
|
|
outPos = 0u;
|
|
next = LZW_FIRST_FREE;
|
|
prev = LZW_NONE;
|
|
first = 0u;
|
|
while (ok && outPos < outLen) {
|
|
while (bitCount < width && inPos < inLen) {
|
|
bits |= (uint32_t)in[inPos++] << bitCount;
|
|
bitCount = (uint8_t)(bitCount + BYTE_BITS);
|
|
}
|
|
if (bitCount < width) {
|
|
break;
|
|
}
|
|
code = (uint16_t)(bits & ((1u << width) - 1u));
|
|
bits >>= width;
|
|
bitCount = (uint8_t)(bitCount - width);
|
|
if (code == LZW_END) {
|
|
break;
|
|
}
|
|
if (code == LZW_RESET) {
|
|
width = LZW_START_BITS;
|
|
next = LZW_FIRST_FREE;
|
|
prev = LZW_NONE;
|
|
continue;
|
|
}
|
|
// The first code after a reset is a byte of its own.
|
|
if (prev == LZW_NONE) {
|
|
ok = code <= LZW_LITERAL_MAX;
|
|
if (ok) {
|
|
out[outPos++] = (uint8_t)code;
|
|
prev = code;
|
|
first = (uint8_t)code;
|
|
}
|
|
continue;
|
|
}
|
|
// The code's string, last byte first; the one code not yet in the
|
|
// table (next) is the previous string and its own first byte.
|
|
depth = 0u;
|
|
cur = code;
|
|
if (code >= next) {
|
|
ok = code == next;
|
|
stack[depth++] = first;
|
|
cur = prev;
|
|
}
|
|
while (ok && cur > LZW_LITERAL_MAX) {
|
|
ok = depth < LZW_TABLE_SIZE - 1u;
|
|
if (ok) {
|
|
stack[depth++] = suffix[cur];
|
|
cur = prefix[cur];
|
|
}
|
|
}
|
|
if (!ok) {
|
|
break;
|
|
}
|
|
stack[depth++] = (uint8_t)cur;
|
|
first = (uint8_t)cur;
|
|
while (depth > 0u && outPos < outLen) {
|
|
out[outPos++] = stack[--depth];
|
|
}
|
|
if (next < LZW_TABLE_SIZE) {
|
|
if (next >= (1u << width) - 1u && width < LZW_MAX_BITS) {
|
|
width++;
|
|
}
|
|
prefix[next] = prev;
|
|
suffix[next] = first;
|
|
next++;
|
|
}
|
|
prev = code;
|
|
}
|
|
jlFree(prefix);
|
|
jlFree(suffix);
|
|
jlFree(stack);
|
|
return ok;
|
|
}
|
|
|
|
|
|
// Nibble k of data, the high one of each byte first.
|
|
static uint8_t nibbleAt(const uint8_t *data, uint32_t k) {
|
|
return ((k & 1u) != 0u) ? (uint8_t)(data[k >> 1] & LOW_NIBBLE) : (uint8_t)(data[k >> 1] >> NIBBLE_BITS);
|
|
}
|
|
|
|
|
|
static uint16_t readLe16(const uint8_t *p) {
|
|
return (uint16_t)(p[0] | ((uint16_t)p[1] << 8));
|
|
}
|
|
|
|
|
|
// Seek to resource index's header in its VOL file and read it: false when
|
|
// the number is past its directory or unused, or its entry names a VOL file
|
|
// the game lacks or a place in one that holds no header for that volume
|
|
// (12 34, then the entry's own volume). A directory can list a resource its
|
|
// VOL files do not hold -- King's Quest 1's sounds 34 to 37 point past the
|
|
// end of VOL.2, Space Quest 1's sound 0 into a VOL.15 -- which Sierra's
|
|
// interpreter, reading a resource only when a script loads it, meets only
|
|
// then.
|
|
static bool resHeaderRead(const AgiGameT *game, AgiResTypeE type, uint16_t index, uint8_t *header) {
|
|
const AgiResEntryT *entry;
|
|
FILE *fp;
|
|
long offset;
|
|
size_t size;
|
|
|
|
if (index >= game->resCount[type]) {
|
|
return false;
|
|
}
|
|
entry = &game->resDir[type][index];
|
|
if (entry->volume >= AGI_MAX_VOLUMES) {
|
|
return false;
|
|
}
|
|
fp = game->volFiles[entry->volume];
|
|
if (fp == NULL) {
|
|
return false;
|
|
}
|
|
offset = ((long)entry->offsetHigh << DIR_OFFSET_HIGH_SHIFT) | (long)entry->offsetLow;
|
|
size = game->v3 ? VOL_HEADER_V3_BYTES : VOL_HEADER_V2_BYTES;
|
|
if (fseek(fp, offset, SEEK_SET) != 0 || fread(header, 1, size, fp) != size) {
|
|
return false;
|
|
}
|
|
return header[0] == VOL_SIG_0 && header[1] == VOL_SIG_1 && (header[VOL_VOLUME_BYTE] & VOL_VOLUME_MASK) == entry->volume;
|
|
}
|
|
|
|
|
|
// ----- Public API (alphabetical) -----
|
|
|
|
// Undo Sierra's XOR with "Avis Durgan" (OBJECT, and LOGIC messages from
|
|
// the start of their text).
|
|
void agiDecrypt(uint8_t *data, uint16_t length) {
|
|
uint16_t i;
|
|
|
|
for (i = 0u; i < length; i++) {
|
|
data[i] = (uint8_t)(data[i] ^ (uint8_t)CRYPT_KEY[i % CRYPT_KEY_LEN]);
|
|
}
|
|
}
|
|
|
|
|
|
const char *agiItemName(const AgiGameT *game, uint8_t item) {
|
|
const uint8_t *e;
|
|
uint16_t off;
|
|
|
|
if (game->objectData == NULL || item >= game->itemCount) {
|
|
return "";
|
|
}
|
|
e = &game->objectData[OBJECT_HEADER_BYTES + (uint16_t)item * OBJECT_ENTRY_BYTES];
|
|
off = (uint16_t)(e[0] | ((uint16_t)e[1] << 8));
|
|
return (const char *)&game->objectData[OBJECT_HEADER_BYTES + off];
|
|
}
|
|
|
|
|
|
uint8_t agiItemStartRoom(const AgiGameT *game, uint8_t item) {
|
|
if (game->objectData == NULL || item >= game->itemCount) {
|
|
return 0u;
|
|
}
|
|
return game->objectData[OBJECT_HEADER_BYTES + (uint16_t)item * OBJECT_ENTRY_BYTES + OBJECT_ROOM_BYTE];
|
|
}
|
|
|
|
|
|
// Whether the game's interpreter version has a quirk.
|
|
bool agiQuirk(AgiQuirkE quirk) {
|
|
uint16_t v;
|
|
|
|
v = gAgi->game.interpreterVersion;
|
|
return quirk < AGI_QUIRK_COUNT && v >= kQuirkVersions[quirk].first && v <= kQuirkVersions[quirk].last;
|
|
}
|
|
|
|
|
|
void agiResClose(AgiGameT *game) {
|
|
uint8_t v;
|
|
|
|
for (v = 0u; v < AGI_MAX_VOLUMES; v++) {
|
|
if (game->volFiles[v] != NULL) {
|
|
fclose(game->volFiles[v]);
|
|
game->volFiles[v] = NULL;
|
|
}
|
|
}
|
|
jlFree(game->words);
|
|
game->words = NULL;
|
|
jlFree(game->objectData);
|
|
game->objectData = NULL;
|
|
}
|
|
|
|
|
|
// A resource, as v2 stores it: v3's packed ones expanded (see the top).
|
|
uint8_t *agiResLoad(const AgiGameT *game, AgiResTypeE type, uint16_t index, uint16_t *outLength) {
|
|
FILE *fp;
|
|
uint8_t header[VOL_HEADER_MAX];
|
|
uint8_t *stored;
|
|
uint8_t *payload;
|
|
uint16_t length;
|
|
uint16_t storedLength;
|
|
|
|
if (!resHeaderRead(game, type, index, header)) {
|
|
return NULL;
|
|
}
|
|
fp = game->volFiles[game->resDir[type][index].volume];
|
|
length = readLe16(&header[VOL_LENGTH_BYTE]);
|
|
storedLength = game->v3 ? readLe16(&header[VOL_STORED_BYTE]) : length;
|
|
if (length == 0u || storedLength == 0u) {
|
|
return NULL;
|
|
}
|
|
stored = (uint8_t *)jlAlloc(storedLength);
|
|
if (stored == NULL) {
|
|
return NULL;
|
|
}
|
|
if (fread(stored, 1, storedLength, fp) != storedLength) {
|
|
jlFree(stored);
|
|
return NULL;
|
|
}
|
|
payload = stored;
|
|
if (game->v3 && (header[VOL_VOLUME_BYTE] & VOL_NIBBLE_PICTURE) != 0u) {
|
|
payload = expandNibblePicture(stored, storedLength, &length);
|
|
jlFree(stored);
|
|
} else if (storedLength != length) {
|
|
payload = (uint8_t *)jlAlloc(length);
|
|
if (payload != NULL && !lzwExpand(stored, storedLength, payload, length)) {
|
|
jlFree(payload);
|
|
payload = NULL;
|
|
}
|
|
jlFree(stored);
|
|
}
|
|
if (payload != NULL && outLength != NULL) {
|
|
*outLength = length;
|
|
}
|
|
return payload;
|
|
}
|
|
|
|
|
|
bool agiResOpen(AgiGameT *game) {
|
|
char name[VOL_NAME_MAX];
|
|
uint8_t t;
|
|
uint8_t v;
|
|
uint16_t version;
|
|
bool anyVolume;
|
|
|
|
memset(game, 0, sizeof(*game));
|
|
for (t = 0u; t < AGI_RES_COUNT; t++) {
|
|
loadDirectory(game, kDirNames[t], (AgiResTypeE)t);
|
|
}
|
|
if (game->resCount[AGI_RES_LOGIC] == 0u) {
|
|
loadDirectoryV3(game);
|
|
}
|
|
if (game->resCount[AGI_RES_LOGIC] == 0u) {
|
|
agiResClose(game);
|
|
return false;
|
|
}
|
|
anyVolume = false;
|
|
for (v = 0u; v < AGI_MAX_VOLUMES; v++) {
|
|
buildVolName(game, name, v);
|
|
game->volFiles[v] = jlDataOpen(name, "rb");
|
|
if (game->volFiles[v] != NULL) {
|
|
anyVolume = true;
|
|
}
|
|
}
|
|
if (!anyVolume) {
|
|
agiResClose(game);
|
|
return false;
|
|
}
|
|
// A game without WORDS.TOK just never matches said(); without OBJECT
|
|
// it has no inventory.
|
|
game->words = loadFile("WORDS.TOK", AGI_WORDS_MAX, &game->wordsLen);
|
|
if (game->words != NULL && game->wordsLen <= AGI_WORDS_HEADER_BYTES) {
|
|
jlFree(game->words);
|
|
game->words = NULL;
|
|
}
|
|
if (!loadObjects(game)) {
|
|
game->maxObjects = AGI_MAX_OBJECTS;
|
|
}
|
|
version = detectVersion();
|
|
if (!game->v3 || version >= AGI_VERSION_V3_FIRST) {
|
|
game->interpreterVersion = version;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
// Whether resource index of type is LZW-packed (v3), so its LOGIC messages
|
|
// are not encrypted.
|
|
bool agiResPacked(const AgiGameT *game, AgiResTypeE type, uint16_t index) {
|
|
uint8_t header[VOL_HEADER_MAX];
|
|
|
|
return game->v3 && resHeaderRead(game, type, index, header) && (header[VOL_VOLUME_BYTE] & VOL_NIBBLE_PICTURE) == 0u && readLe16(&header[VOL_LENGTH_BYTE]) != readLe16(&header[VOL_STORED_BYTE]);
|
|
}
|
|
|
|
|
|
// Whether resource index of type is in the game's files: its entry points at
|
|
// a resource header in its VOL file (resHeaderRead).
|
|
bool agiResPresent(const AgiGameT *game, AgiResTypeE type, uint16_t index) {
|
|
uint8_t header[VOL_HEADER_MAX];
|
|
|
|
return resHeaderRead(game, type, index, header);
|
|
}
|
|
|
|
|
|
// Find the longest dictionary entry that starts `text` and ends at a word
|
|
// boundary (the end of text or a space). Entries can hold several words
|
|
// ("pick up"). text is lower case with single spaces. Returns the word's
|
|
// group, or AGI_WORD_NONE, and its length in *outLen.
|
|
uint16_t agiWordMatch(const AgiGameT *game, const char *text, uint8_t *outLen) {
|
|
char cur[AGI_WORD_MAX_LEN + 1u];
|
|
const uint8_t *p;
|
|
const uint8_t *end;
|
|
uint16_t best;
|
|
uint8_t bestLen;
|
|
uint8_t len;
|
|
uint8_t letter;
|
|
uint16_t start;
|
|
|
|
*outLen = 0u;
|
|
if (game->words == NULL || text[0] < 'a' || text[0] > 'z') {
|
|
return AGI_WORD_NONE;
|
|
}
|
|
letter = (uint8_t)(text[0] - 'a');
|
|
start = (uint16_t)(((uint16_t)game->words[letter * WORD_INDEX_BYTES] << 8) | game->words[letter * WORD_INDEX_BYTES + 1u]);
|
|
if (start == 0u || start >= game->wordsLen) {
|
|
return AGI_WORD_NONE;
|
|
}
|
|
best = AGI_WORD_NONE;
|
|
bestLen = 0u;
|
|
p = game->words + start;
|
|
end = game->words + game->wordsLen;
|
|
while (p < end) {
|
|
uint8_t c;
|
|
uint16_t id;
|
|
|
|
len = *p++;
|
|
if (len > AGI_WORD_MAX_LEN) {
|
|
break;
|
|
}
|
|
do {
|
|
if (p >= end) {
|
|
*outLen = bestLen;
|
|
return best;
|
|
}
|
|
c = *p++;
|
|
if (len < AGI_WORD_MAX_LEN) {
|
|
cur[len] = (char)((c & (uint8_t)~WORD_LAST_CHAR) ^ WORD_CHAR_XOR);
|
|
len++;
|
|
}
|
|
} while ((c & WORD_LAST_CHAR) == 0u);
|
|
if (p + 1 >= end) {
|
|
break;
|
|
}
|
|
id = (uint16_t)(((uint16_t)p[0] << 8) | p[1]);
|
|
p += 2;
|
|
// Entries are sorted, so once the first letter changes nothing
|
|
// further can match.
|
|
if (cur[0] != text[0]) {
|
|
break;
|
|
}
|
|
if (len > bestLen && strncmp(cur, text, len) == 0 && (text[len] == '\0' || text[len] == ' ')) {
|
|
best = id;
|
|
bestLen = len;
|
|
}
|
|
}
|
|
*outLen = bestLen;
|
|
return best;
|
|
}
|