// 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, DIR (GRDIR, // MH2DIR), after four little-endian offsets to them, and the volumes in // 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 #include #include #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 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; }