/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "agi/agi.h" #include "agi/words.h" #include "common/textconsole.h" namespace Agi { Words::Words(AgiEngine *vm) : _vm(vm), _hasExtendedCharacters(false) { clearEgoWords(); } Words::~Words() { clearEgoWords(); } int Words::loadDictionary_v1(Common::SeekableReadStream &stream) { char str[64]; int k; debug(0, "Loading dictionary"); // Loop through alphabet, as words in the dictionary file are sorted by // first character stream.seek(26 * 2, SEEK_CUR); do { // Read next word for (k = 0; k < ARRAYSIZE(str) - 1; k++) { str[k] = stream.readByte(); if (str[k] == 0 || (uint8)str[k] == 0xFF) break; } // Store word in dictionary if (k > 0) { WordEntry newWord; newWord.word = Common::String(str, k + 1); newWord.id = stream.readUint16LE(); _dictionary[str[0]].push_back(newWord); } } while ((uint8)str[0] != 0xFF); return errOK; } int Words::loadDictionary(const char *fname) { Common::File fp; if (!fp.open(fname)) { warning("loadDictionary: can't open %s", fname); // FIXME return errOK; // err_BadFileOpen } debug(0, "Loading dictionary: %s", fname); return loadDictionary(fp); } /** * Load all words from WORDS.TOK into the dictionary. * * Note that this parser handles words that start with a digit. These appear in * fan games because AGI Studio allowed them and placed them at the start of the * 'A' section. These words had no effect because the interpreter only matched * user input that began with A-Z, and the matching logic happened to skip words * until it reached one with the expected first letter. In the past, these words * caused problems for our parser. See bugs #6415, #15000 */ int Words::loadDictionary(Common::SeekableReadStream &stream) { // Read words for each letter (A-Z) const uint32 start = stream.pos(); for (int i = 0; i < 26; i++) { // Read letter index and seek to first word stream.seek(start + i * 2); int offset = stream.readUint16BE(); if (offset == 0) { continue; // no words } stream.seek(start + offset); // Read all words in this letter's section char str[64] = { 0 }; int prevWordLength = 0; int k = stream.readByte(); // copy-count of first word while (!stream.eos() && !stream.err() && k <= prevWordLength) { // Read word char c = 0; while (!(c & 0x80) && k < ARRAYSIZE(str) - 1) { c = stream.readByte(); str[k++] = (c ^ 0x7F) & 0x7F; } str[k] = 0; // Read word id uint16 wordId = stream.readUint16BE(); if (stream.eos() || stream.err()) { break; } // Store word in dictionary WordEntry newWord; newWord.word = Common::String(str, k); newWord.id = wordId; _dictionary[str[0]].push_back(newWord); // Read next word's copy count, or this letter's zero terminator. // Stop on zero if the word we read begins with the expected letter, // otherwise this is a fan game and we just read a word that starts // with a digit at the start of the 'A' section. Bugs #6413, #15000 k = stream.readByte(); if (k == 0 && str[0] == 'a' + i) { break; } prevWordLength = k; } } return errOK; } int Words::loadExtendedDictionary(const char *fname) { Common::File fp; if (!fp.open(fname)) { warning("loadWords: can't open %s", fname); // FIXME return errOK; // err_BadFileOpen } debug(0, "Loading extended dictionary: %s", fname); // skip the header fp.readString('\n'); while (!fp.eos() && !fp.err()) { WordEntry newWord; newWord.word = fp.readString(); newWord.id = atoi(fp.readString('\n').c_str()); if (!newWord.word.empty()) { _dictionary[(byte)newWord.word[0]].push_back(newWord); } } _hasExtendedCharacters = true; return errOK; } void Words::unloadDictionary() { _dictionary.clear(); } void Words::clearEgoWords() { for (int16 wordNr = 0; wordNr < MAX_WORDS; wordNr++) { _egoWords[wordNr].id = 0; _egoWords[wordNr].word.clear(); } _egoWordCount = 0; } static bool isCharSeparator(const char curChar) { switch (curChar) { case ' ': case ',': case '.': case '?': case '!': case '(': case ')': case ';': case ':': case '[': case ']': case '{': case '}': return true; default: return false; } } static bool isCharInvalid(const char curChar) { switch (curChar) { case 0x27: // ' case 0x60: // ` case '-': case '\\': case '"': return true; default: return false; } } void Words::cleanUpInput(const char *rawUserInput, Common::String &cleanInput) { cleanInput.clear(); byte curChar = *rawUserInput; while (curChar) { // skip separators / invalid characters if (isCharSeparator(curChar) || isCharInvalid(curChar)) { rawUserInput++; curChar = *rawUserInput; } else { do { if (!isCharInvalid(curChar)) { // not invalid char, add it to the cleaned up input cleanInput += curChar; } rawUserInput++; curChar = *rawUserInput; if (isCharSeparator(curChar)) { cleanInput += ' '; break; } } while (curChar); } } if (cleanInput.hasSuffix(" ")) { // ends with a space? remove it cleanInput.deleteLastChar(); } } int16 Words::findWordInDictionary(const Common::String &userInputLowercase, uint16 userInputLen, uint16 userInputPos, uint16 &foundWordLen) { uint16 userInputLeft = userInputLen - userInputPos; uint16 wordStartPos = userInputPos; int16 wordId = DICTIONARY_RESULT_UNKNOWN; byte firstChar = userInputLowercase[userInputPos]; foundWordLen = 0; const byte lastCharInAbc = _hasExtendedCharacters ? 0xff : 'z'; // Words normally have to start with a letter. // ENHANCEMENT: Fan games and translations include words that start with a // digit, even though the original interpreter ignored them. We allow input // words to start with a digit if the dictionary contains such a word. if (('a' <= firstChar && firstChar <= lastCharInAbc) || ('0' <= firstChar && firstChar <= '9' && _dictionary.contains(firstChar))) { if (((userInputPos + 1) < userInputLen) && (userInputLowercase[userInputPos + 1] == ' ')) { // current word is 1 char only? if ((firstChar == 'a') || (firstChar == 'i')) { // and it's "a" or "i"? -> then set current type to ignore wordId = DICTIONARY_RESULT_IGNORE; } } const Common::Array &words = _dictionary.getValOrDefault(firstChar); for (const WordEntry &wordEntry : words) { uint16 dictionaryWordLen = wordEntry.word.size(); if (dictionaryWordLen <= userInputLeft) { // dictionary word is longer or same length as the remaining user input uint16 curCompareLeft = dictionaryWordLen; uint16 dictionaryWordPos = 0; userInputPos = wordStartPos; while (curCompareLeft) { byte curUserInputChar = userInputLowercase[userInputPos]; byte curDictionaryChar = wordEntry.word[dictionaryWordPos]; if (curUserInputChar != curDictionaryChar) break; userInputPos++; dictionaryWordPos++; curCompareLeft--; } if (!curCompareLeft) { // check, if there is also nothing more of user input left or if a space the follow-up char? if ((userInputPos >= userInputLen) || (userInputLowercase[userInputPos] == ' ')) { // so fully matched, remember match wordId = wordEntry.id; foundWordLen = dictionaryWordLen; // perfect match? -> exit loop if (userInputLeft == foundWordLen) { // perfect match -> break break; } } } } } } if (foundWordLen == 0) { userInputPos = wordStartPos; while (userInputPos < userInputLen) { if (userInputLowercase[userInputPos] == ' ') { break; } userInputPos++; } foundWordLen = userInputPos - wordStartPos; } return wordId; } void Words::parseUsingDictionary(const char *rawUserInput) { assert(rawUserInput); debugC(2, kDebugLevelScripts, "parse: userinput = \"%s\"", rawUserInput); // Reset result clearEgoWords(); // clean up user input Common::String userInput; cleanUpInput(rawUserInput, userInput); // Sierra compared independent of upper case and lower case Common::String userInputLowercase = userInput; userInputLowercase.toLowercase(); if (_vm->getLanguage() == Common::RU_RUS) { convertRussianUserInput(userInputLowercase); } if (handleSpeedCommands(userInputLowercase)) { return; } uint16 wordCount = 0; uint16 userInputPos = 0; uint16 userInputLen = userInput.size(); const char *userInputPtr = userInput.c_str(); while (userInputPos < userInputLen) { // Skip trailing space if (userInput[userInputPos] == ' ') userInputPos++; uint16 foundWordPos = userInputPos; uint16 foundWordLen = 0; int16 foundWordId = findWordInDictionary(userInputLowercase, userInputLen, userInputPos, foundWordLen); if (foundWordId != DICTIONARY_RESULT_IGNORE) { // word not supposed to get ignored // add it now if (foundWordId != DICTIONARY_RESULT_UNKNOWN) { // known word _egoWords[wordCount].id = foundWordId; } _egoWords[wordCount].word = Common::String(userInputPtr + foundWordPos, foundWordLen); debugC(2, kDebugLevelScripts, "found word %s (id %d)", _egoWords[wordCount].word.c_str(), _egoWords[wordCount].id); wordCount++; if (foundWordId == DICTIONARY_RESULT_UNKNOWN) { // unknown word _vm->setVar(VM_VAR_WORD_NOT_FOUND, wordCount); break; // and exit now } } userInputPos += foundWordLen; } _egoWordCount = wordCount; debugC(4, kDebugLevelScripts, "ego word count = %d", _egoWordCount); if (_egoWordCount > 0) { _vm->setFlag(VM_FLAG_ENTERED_CLI, true); } else { _vm->setFlag(VM_FLAG_ENTERED_CLI, false); } _vm->setFlag(VM_FLAG_SAID_ACCEPTED_INPUT, false); } uint16 Words::getEgoWordCount() const { return _egoWordCount; } const char *Words::getEgoWord(int16 wordNr) const { assert(wordNr >= 0 && wordNr < MAX_WORDS); return _egoWords[wordNr].word.c_str(); } uint16 Words::getEgoWordId(int16 wordNr) const { assert(wordNr >= 0 && wordNr < MAX_WORDS); return _egoWords[wordNr].id; } bool Words::handleSpeedCommands(const Common::String &userInputLowercase) { // We add speed controls to games that didn't originally have them. // Apple II games had no speed controls, the interpreter ran as fast as it could. // Some Apple IIgs games had speed controls, others didn't. We override the // the speed that the game requests with `timeDelayOverwrite`. switch (_vm->getPlatform()) { case Common::kPlatformApple2: case Common::kPlatformApple2GS: if (userInputLowercase == "fastest") { _vm->_game.setSpeedLevel(0); return true; } else if (userInputLowercase == "fast") { _vm->_game.setSpeedLevel(1); return true; } else if (userInputLowercase == "normal") { _vm->_game.setSpeedLevel(2); return true; } else if (userInputLowercase == "slow") { _vm->_game.setSpeedLevel(3); return true; } break; default: break; } return false; } void Words::convertRussianUserInput(Common::String &userInputLowercase) { const char *conv = // АБВГДЕЖЗИЙКЛМНОП "abvgdewziiklmnop" // 80 // РСТУФХЦЧШЩЪЫЬЭЮЯ "rstufxcyhhjijeuq" // 90 // абвгдежзийклмноп "abvgdewziiklmnop" // a0 " " // b0 " " // c0 " " // d0 // рстуфхцчшщъыьэюя "rstufxcyhhjijeuq" // e0 // Ее "ee ";// f0 Common::String tr; for (uint i = 0; i < userInputLowercase.size(); i++) { if ((byte)userInputLowercase[i] >= 0x80) { tr += conv[(byte)userInputLowercase[i] - 0x80]; } else { tr += (byte)userInputLowercase[i]; } } userInputLowercase = tr; } } // End of namespace Agi