Files
scummvm-cursorfix/engines/ags/engine/ac/parser.cpp
2026-02-02 04:50:13 +01:00

348 lines
9.9 KiB
C++

/* 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 <http://www.gnu.org/licenses/>.
*
*/
#include "ags/shared/ac/common.h"
#include "ags/shared/ac/game_setup_struct.h"
#include "ags/engine/ac/game_state.h"
#include "ags/engine/ac/parser.h"
#include "ags/engine/ac/string.h"
#include "ags/shared/ac/words_dictionary.h"
#include "ags/engine/debugging/debug_log.h"
#include "ags/shared/util/string.h"
#include "ags/shared/util/string_compat.h"
#include "ags/shared/debugging/out.h"
#include "ags/engine/script/script_api.h"
#include "ags/engine/script/script_runtime.h"
#include "ags/engine/ac/dynobj/script_string.h"
#include "ags/globals.h"
namespace AGS3 {
using namespace AGS::Shared;
int Parser_FindWordID(const char *wordToFind) {
return find_word_in_dictionary(wordToFind);
}
const char *Parser_SaidUnknownWord() {
if (_GP(play).bad_parsed_word[0] == 0)
return nullptr;
return CreateNewScriptString(_GP(play).bad_parsed_word);
}
void ParseText(const char *text) {
parse_sentence(text, &_GP(play).num_parsed_words, _GP(play).parsed_words, nullptr, 0);
}
// Said: call with argument for example "get apple"; we then check
// word by word if it matches (using dictonary ID equivalence to match
// synonyms). Returns 1 if it does, 0 if not.
int Said(const char *checkwords) {
int numword = 0;
short words[MAX_PARSED_WORDS];
return parse_sentence(checkwords, &numword, &words[0], _GP(play).parsed_words, _GP(play).num_parsed_words);
}
//=============================================================================
int find_word_in_dictionary(const char *lookfor) {
int j;
if (_GP(game).dict == nullptr)
return -1;
for (j = 0; j < _GP(game).dict->num_words; j++) {
if (ags_stricmp(lookfor, _GP(game).dict->word[j]) == 0) {
return _GP(game).dict->wordnum[j];
}
}
if (lookfor[0] != 0) {
// If the word wasn't found, but it ends in 'S', see if there's
// a non-plural version
const char *ptat = &lookfor[strlen(lookfor) - 1];
char lastletter = *ptat;
if ((lastletter == 's') || (lastletter == 'S') || (lastletter == '\'')) {
String singular = lookfor;
singular.ClipRight(1);
return find_word_in_dictionary(singular.GetCStr());
}
}
return -1;
}
int is_valid_word_char(char theChar) {
if ((Common::isAlnum((unsigned char)theChar)) || (theChar == '\'') || (theChar == '-')) {
return 1;
}
return 0;
}
int FindMatchingMultiWordWord(char *thisword, const char **text) {
// see if there are any multi-word words
// that match -- if so, use them
const char *tempptr = *text;
char tempword[150] = "";
if (thisword != nullptr)
Common::strcpy_s(tempword, thisword);
int bestMatchFound = -1, word;
const char *tempptrAtBestMatch = tempptr;
do {
// extract and concat the next word
Common::strcat_s(tempword, " ");
while (tempptr[0] == ' ') tempptr++;
char chbuffer[2];
while (is_valid_word_char(tempptr[0])) {
snprintf(chbuffer, sizeof(chbuffer), "%c", tempptr[0]);
Common::strcat_s(tempword, chbuffer);
tempptr++;
}
// is this it?
word = find_word_in_dictionary(tempword);
// take the longest match we find
if (word >= 0) {
bestMatchFound = word;
tempptrAtBestMatch = tempptr;
}
} while (tempptr[0] == ' ');
word = bestMatchFound;
if (word >= 0) {
// yes, a word like "pick up" was found
*text = tempptrAtBestMatch;
if (thisword != nullptr)
Common::strcpy_s(thisword, 150, tempword);
}
return word;
}
// parse_sentence: pass compareto as NULL to parse the sentence, or
// compareto as non-null to check if it matches the passed sentence
int parse_sentence(const char *src_text, int *numwords, short *wordarray, short *compareto, int comparetonum) {
char thisword[150] = "\0";
int i = 0, comparing = 0;
int8 in_optional = 0, do_word_now = 0;
int optional_start = 0;
numwords[0] = 0;
if (compareto == nullptr)
_GP(play).bad_parsed_word[0] = 0;
String uniform_text = src_text;
uniform_text.MakeLower();
const char *text = uniform_text.GetCStr();
while (1) {
if ((compareto != nullptr) && (compareto[comparing] == RESTOFLINE))
return 1;
if ((text[0] == ']') && (compareto != nullptr)) {
if (!in_optional)
quitprintf("!Said: unexpected ']'\nText: %s", src_text);
do_word_now = 1;
}
if (is_valid_word_char(text[0])) {
// Part of a word, add it on
thisword[i] = text[0];
i++;
} else if ((text[0] == '[') && (compareto != nullptr)) {
if (in_optional)
quitprintf("!Said: nested optional words\nText: %s", src_text);
in_optional = 1;
optional_start = comparing;
} else if ((thisword[0] != 0) || ((text[0] == 0) && (i > 0)) || (do_word_now == 1)) {
// End of word, so process it
thisword[i] = 0;
i = 0;
int word = -1;
if (text[0] == ' ') {
word = FindMatchingMultiWordWord(thisword, &text);
}
if (word < 0) {
// just a normal word
word = find_word_in_dictionary(thisword);
}
// "look rol"
if (word == RESTOFLINE)
return 1;
if (compareto) {
// check string is longer than user input
if (comparing >= comparetonum) {
if (in_optional) {
// eg. "exit [door]" - there's no more user input
// but the optional word is still there
if (do_word_now) {
in_optional = 0;
do_word_now = 0;
}
thisword[0] = 0;
text++;
continue;
}
return 0;
}
if (word <= 0)
quitprintf("!Said: supplied word '%s' is not in dictionary or is an ignored word\nText: %s", thisword, src_text);
if (word == ANYWORD) {
} else if (word != compareto[comparing]) {
// words don't match - if a comma then a list of possibles,
// so allow retry
if (text[0] == ',')
comparing--;
else {
// words don't match
if (in_optional) {
// inside an optional clause, so skip it
while (text[0] != ']') {
if (text[0] == 0)
quitprintf("!Said: unterminated [optional]\nText: %s", src_text);
text++;
}
// -1 because it's about to be ++'d again
comparing = optional_start - 1;
}
// words don't match outside an optional clause, abort
else
return 0;
}
} else if (text[0] == ',') {
// this alternative matched, but there are more
// so skip the other alternatives
int continueSearching = 1;
while (continueSearching) {
const char *textStart = ++text; // begin with next char
// find where the next word ends
while ((text[0] == ',') || is_valid_word_char(text[0])) {
// shift beginning of potential multi-word each time we see a comma
if (text[0] == ',')
textStart = ++text;
else
text++;
}
continueSearching = 0;
if (text[0] == 0 || text[0] == ' ') {
Common::strcpy_s(thisword, textStart);
thisword[text - textStart] = 0;
// forward past any multi-word alternatives
if (FindMatchingMultiWordWord(thisword, &text) >= 0) {
if (text[0] == 0)
break;
continueSearching = 1;
}
}
}
if ((text[0] == ']') && (in_optional)) {
// [go,move] we just matched "go", so skip over "move"
in_optional = 0;
text++;
}
// go back cos it'll be ++'d in a minute
text--;
}
comparing++;
} else if (word != 0) {
// it's not an ignore word (it's a known word, or an unknown
// word, so save its index)
wordarray[numwords[0]] = word;
numwords[0]++;
if (numwords[0] >= MAX_PARSED_WORDS)
return 0;
// if it's an unknown word, store it for use in messages like
// "you can't use the word 'xxx' in this game"
if ((word < 0) && (_GP(play).bad_parsed_word[0] == 0))
Common::strcpy_s(_GP(play).bad_parsed_word, 100, thisword);
}
if (do_word_now) {
in_optional = 0;
do_word_now = 0;
}
thisword[0] = 0;
}
if (text[0] == 0)
break;
text++;
}
// If the user input is longer than the Said string, it's wrong
// eg Said("look door") and they type "look door jibble"
// rol should be used instead to enable this
if (comparing < comparetonum)
return 0;
return 1;
}
//=============================================================================
//
// Script API Functions
//
//=============================================================================
// int (const char *wordToFind)
RuntimeScriptValue Sc_Parser_FindWordID(const RuntimeScriptValue *params, int32_t param_count) {
API_SCALL_INT_POBJ(Parser_FindWordID, const char);
}
// void (char*text)
RuntimeScriptValue Sc_ParseText(const RuntimeScriptValue *params, int32_t param_count) {
API_SCALL_VOID_POBJ(ParseText, /*const*/ char);
}
// const char* ()
RuntimeScriptValue Sc_Parser_SaidUnknownWord(const RuntimeScriptValue *params, int32_t param_count) {
API_SCALL_OBJ(const char, _GP(myScriptStringImpl), Parser_SaidUnknownWord);
}
// int (char*checkwords)
RuntimeScriptValue Sc_Said(const RuntimeScriptValue *params, int32_t param_count) {
API_SCALL_INT_POBJ(Said, /*const*/ char);
}
void RegisterParserAPI() {
ScFnRegister parser_api[] = {
{"Parser::FindWordID^1", API_FN_PAIR(Parser_FindWordID)},
{"Parser::ParseText^1", API_FN_PAIR(ParseText)},
{"Parser::SaidUnknownWord^0", API_FN_PAIR(Parser_SaidUnknownWord)},
{"Parser::Said^1", API_FN_PAIR(Said)},
};
ccAddExternalFunctions361(parser_api);
}
} // namespace AGS3