1 /* ScummVM - Graphic Adventure Engine
2  *
3  * ScummVM is the legal property of its developers, whose names
4  * are too numerous to list here. Please refer to the COPYRIGHT
5  * file distributed with this source distribution.
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License
9  * as published by the Free Software Foundation; either version 2
10  * of the License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20  *
21  */
22 
23 //include <cctype> //isalnum()
24 //include <cstdio>
25 #include "ags/shared/ac/common.h"
26 #include "ags/shared/ac/game_setup_struct.h"
27 #include "ags/engine/ac/game_state.h"
28 #include "ags/engine/ac/parser.h"
29 #include "ags/engine/ac/string.h"
30 #include "ags/shared/ac/words_dictionary.h"
31 #include "ags/engine/debugging/debug_log.h"
32 #include "ags/shared/util/string.h"
33 #include "ags/shared/util/string_compat.h"
34 #include "ags/shared/debugging/out.h"
35 #include "ags/engine/script/script_api.h"
36 #include "ags/engine/script/script_runtime.h"
37 #include "ags/engine/ac/dynobj/script_string.h"
38 #include "ags/globals.h"
39 
40 namespace AGS3 {
41 
42 using namespace AGS::Shared;
43 
44 
45 
46 
Parser_FindWordID(const char * wordToFind)47 int Parser_FindWordID(const char *wordToFind) {
48 	return find_word_in_dictionary(wordToFind);
49 }
50 
Parser_SaidUnknownWord()51 const char *Parser_SaidUnknownWord() {
52 	if (_GP(play).bad_parsed_word[0] == 0)
53 		return nullptr;
54 	return CreateNewScriptString(_GP(play).bad_parsed_word);
55 }
56 
ParseText(const char * text)57 void ParseText(const char *text) {
58 	parse_sentence(text, &_GP(play).num_parsed_words, _GP(play).parsed_words, nullptr, 0);
59 }
60 
61 // Said: call with argument for example "get apple"; we then check
62 // word by word if it matches (using dictonary ID equivalence to match
63 // synonyms). Returns 1 if it does, 0 if not.
Said(const char * checkwords)64 int Said(const char *checkwords) {
65 	int numword = 0;
66 	short words[MAX_PARSED_WORDS];
67 	return parse_sentence(checkwords, &numword, &words[0], _GP(play).parsed_words, _GP(play).num_parsed_words);
68 }
69 
70 //=============================================================================
71 
find_word_in_dictionary(const char * lookfor)72 int find_word_in_dictionary(const char *lookfor) {
73 	int j;
74 	if (_GP(game).dict == nullptr)
75 		return -1;
76 
77 	for (j = 0; j < _GP(game).dict->num_words; j++) {
78 		if (ags_stricmp(lookfor, _GP(game).dict->word[j]) == 0) {
79 			return _GP(game).dict->wordnum[j];
80 		}
81 	}
82 	if (lookfor[0] != 0) {
83 		// If the word wasn't found, but it ends in 'S', see if there's
84 		// a non-plural version
85 		const char *ptat = &lookfor[strlen(lookfor) - 1];
86 		char lastletter = *ptat;
87 		if ((lastletter == 's') || (lastletter == 'S') || (lastletter == '\'')) {
88 			String singular = lookfor;
89 			singular.ClipRight(1);
90 			return find_word_in_dictionary(singular.GetCStr());
91 		}
92 	}
93 	return -1;
94 }
95 
is_valid_word_char(char theChar)96 int is_valid_word_char(char theChar) {
97 	if ((Common::isAlnum((unsigned char)theChar)) || (theChar == '\'') || (theChar == '-')) {
98 		return 1;
99 	}
100 	return 0;
101 }
102 
FindMatchingMultiWordWord(char * thisword,const char ** text)103 int FindMatchingMultiWordWord(char *thisword, const char **text) {
104 	// see if there are any multi-word words
105 	// that match -- if so, use them
106 	const char *tempptr = *text;
107 	char tempword[150] = "";
108 	if (thisword != nullptr)
109 		strcpy(tempword, thisword);
110 
111 	int bestMatchFound = -1, word;
112 	const char *tempptrAtBestMatch = tempptr;
113 
114 	do {
115 		// extract and concat the next word
116 		strcat(tempword, " ");
117 		while (tempptr[0] == ' ') tempptr++;
118 		char chbuffer[2];
119 		while (is_valid_word_char(tempptr[0])) {
120 			sprintf(chbuffer, "%c", tempptr[0]);
121 			strcat(tempword, chbuffer);
122 			tempptr++;
123 		}
124 		// is this it?
125 		word = find_word_in_dictionary(tempword);
126 		// take the longest match we find
127 		if (word >= 0) {
128 			bestMatchFound = word;
129 			tempptrAtBestMatch = tempptr;
130 		}
131 
132 	} while (tempptr[0] == ' ');
133 
134 	word = bestMatchFound;
135 
136 	if (word >= 0) {
137 		// yes, a word like "pick up" was found
138 		*text = tempptrAtBestMatch;
139 		if (thisword != nullptr)
140 			strcpy(thisword, tempword);
141 	}
142 
143 	return word;
144 }
145 
146 // parse_sentence: pass compareto as NULL to parse the sentence, or
147 // compareto as non-null to check if it matches the passed sentence
parse_sentence(const char * src_text,int * numwords,short * wordarray,short * compareto,int comparetonum)148 int parse_sentence(const char *src_text, int *numwords, short *wordarray, short *compareto, int comparetonum) {
149 	char thisword[150] = "\0";
150 	int  i = 0, comparing = 0;
151 	int8 in_optional = 0, do_word_now = 0;
152 	int  optional_start = 0;
153 
154 	numwords[0] = 0;
155 	if (compareto == nullptr)
156 		_GP(play).bad_parsed_word[0] = 0;
157 
158 	String uniform_text = src_text;
159 	uniform_text.MakeLower();
160 	const char *text = uniform_text.GetCStr();
161 	while (1) {
162 		if ((compareto != nullptr) && (compareto[comparing] == RESTOFLINE))
163 			return 1;
164 
165 		if ((text[0] == ']') && (compareto != nullptr)) {
166 			if (!in_optional)
167 				quitprintf("!Said: unexpected ']'\nText: %s", src_text);
168 			do_word_now = 1;
169 		}
170 
171 		if (is_valid_word_char(text[0])) {
172 			// Part of a word, add it on
173 			thisword[i] = text[0];
174 			i++;
175 		} else if ((text[0] == '[') && (compareto != nullptr)) {
176 			if (in_optional)
177 				quitprintf("!Said: nested optional words\nText: %s", src_text);
178 
179 			in_optional = 1;
180 			optional_start = comparing;
181 		} else if ((thisword[0] != 0) || ((text[0] == 0) && (i > 0)) || (do_word_now == 1)) {
182 			// End of word, so process it
183 			thisword[i] = 0;
184 			i = 0;
185 			int word = -1;
186 
187 			if (text[0] == ' ') {
188 				word = FindMatchingMultiWordWord(thisword, &text);
189 			}
190 
191 			if (word < 0) {
192 				// just a normal word
193 				word = find_word_in_dictionary(thisword);
194 			}
195 
196 			// "look rol"
197 			if (word == RESTOFLINE)
198 				return 1;
199 			if (compareto) {
200 				// check string is longer than user input
201 				if (comparing >= comparetonum) {
202 					if (in_optional) {
203 						// eg. "exit [door]" - there's no more user input
204 						// but the optional word is still there
205 						if (do_word_now) {
206 							in_optional = 0;
207 							do_word_now = 0;
208 						}
209 						thisword[0] = 0;
210 						text++;
211 						continue;
212 					}
213 					return 0;
214 				}
215 				if (word <= 0)
216 					quitprintf("!Said: supplied word '%s' is not in dictionary or is an ignored word\nText: %s", thisword, src_text);
217 				if (word == ANYWORD) {
218 				} else if (word != compareto[comparing]) {
219 					// words don't match - if a comma then a list of possibles,
220 					// so allow retry
221 					if (text[0] == ',')
222 						comparing--;
223 					else {
224 						// words don't match
225 						if (in_optional) {
226 							// inside an optional clause, so skip it
227 							while (text[0] != ']') {
228 								if (text[0] == 0)
229 									quitprintf("!Said: unterminated [optional]\nText: %s", src_text);
230 								text++;
231 							}
232 							// -1 because it's about to be ++'d again
233 							comparing = optional_start - 1;
234 						}
235 						// words don't match outside an optional clause, abort
236 						else
237 							return 0;
238 					}
239 				} else if (text[0] == ',') {
240 					// this alternative matched, but there are more
241 					// so skip the other alternatives
242 					int continueSearching = 1;
243 					while (continueSearching) {
244 
245 						const char *textStart = &text[1];
246 
247 						while ((text[0] == ',') || (Common::isAlnum((unsigned char)text[0]) != 0))
248 							text++;
249 
250 						continueSearching = 0;
251 
252 						if (text[0] == ' ') {
253 							strcpy(thisword, textStart);
254 							thisword[text - textStart] = 0;
255 							// forward past any multi-word alternatives
256 							if (FindMatchingMultiWordWord(thisword, &text) >= 0)
257 								continueSearching = 1;
258 						}
259 					}
260 
261 					if ((text[0] == ']') && (in_optional)) {
262 						// [go,move]  we just matched "go", so skip over "move"
263 						in_optional = 0;
264 						text++;
265 					}
266 
267 					// go back cos it'll be ++'d in a minute
268 					text--;
269 				}
270 				comparing++;
271 			} else if (word != 0) {
272 				// it's not an ignore word (it's a known word, or an unknown
273 				// word, so save its index)
274 				wordarray[numwords[0]] = word;
275 				numwords[0]++;
276 				if (numwords[0] >= MAX_PARSED_WORDS)
277 					return 0;
278 				// if it's an unknown word, store it for use in messages like
279 				// "you can't use the word 'xxx' in this game"
280 				if ((word < 0) && (_GP(play).bad_parsed_word[0] == 0))
281 					strcpy(_GP(play).bad_parsed_word, thisword);
282 			}
283 
284 			if (do_word_now) {
285 				in_optional = 0;
286 				do_word_now = 0;
287 			}
288 
289 			thisword[0] = 0;
290 		}
291 		if (text[0] == 0)
292 			break;
293 		text++;
294 	}
295 	// If the user input is longer than the Said string, it's wrong
296 	// eg Said("look door") and they type "look door jibble"
297 	// rol should be used instead to enable this
298 	if (comparing < comparetonum)
299 		return 0;
300 	return 1;
301 }
302 
303 //=============================================================================
304 //
305 // Script API Functions
306 //
307 //=============================================================================
308 
309 // int (const char *wordToFind)
Sc_Parser_FindWordID(const RuntimeScriptValue * params,int32_t param_count)310 RuntimeScriptValue Sc_Parser_FindWordID(const RuntimeScriptValue *params, int32_t param_count) {
311 	API_SCALL_INT_POBJ(Parser_FindWordID, const char);
312 }
313 
314 // void  (char*text)
Sc_ParseText(const RuntimeScriptValue * params,int32_t param_count)315 RuntimeScriptValue Sc_ParseText(const RuntimeScriptValue *params, int32_t param_count) {
316 	API_SCALL_VOID_POBJ(ParseText, /*const*/ char);
317 }
318 
319 // const char* ()
Sc_Parser_SaidUnknownWord(const RuntimeScriptValue * params,int32_t param_count)320 RuntimeScriptValue Sc_Parser_SaidUnknownWord(const RuntimeScriptValue *params, int32_t param_count) {
321 	API_CONST_SCALL_OBJ(const char, _GP(myScriptStringImpl), Parser_SaidUnknownWord);
322 }
323 
324 // int  (char*checkwords)
Sc_Said(const RuntimeScriptValue * params,int32_t param_count)325 RuntimeScriptValue Sc_Said(const RuntimeScriptValue *params, int32_t param_count) {
326 	API_SCALL_INT_POBJ(Said, /*const*/ char);
327 }
328 
329 
RegisterParserAPI()330 void RegisterParserAPI() {
331 	ccAddExternalStaticFunction("Parser::FindWordID^1", Sc_Parser_FindWordID);
332 	ccAddExternalStaticFunction("Parser::ParseText^1", Sc_ParseText);
333 	ccAddExternalStaticFunction("Parser::SaidUnknownWord^0", Sc_Parser_SaidUnknownWord);
334 	ccAddExternalStaticFunction("Parser::Said^1", Sc_Said);
335 }
336 
337 } // namespace AGS3
338