mirror of
https://github.com/libretro/scummvm.git
synced 2025-01-27 05:32:45 +00:00
0ff7274993
To accomodate minor version.
601 lines
14 KiB
C++
601 lines
14 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 2
|
|
* 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, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
*
|
|
*/
|
|
|
|
#include "director/director.h"
|
|
#include "director/movie.h"
|
|
#include "director/lingo/lingo.h"
|
|
|
|
namespace Director {
|
|
|
|
Common::String preprocessWhen(Common::String in, bool *changed);
|
|
Common::String preprocessPlay(Common::String in);
|
|
Common::String preprocessSound(Common::String in);
|
|
|
|
bool isspec(char c) {
|
|
return strchr("-+*/%%^:,()><&[]=", c) != NULL;
|
|
}
|
|
|
|
static Common::String nexttok(const char *s, const char **newP = nullptr) {
|
|
Common::String res;
|
|
|
|
// Scan first non-whitespace
|
|
while (*s && (*s == ' ' || *s == '\t' || *s == '\xC2')) // If we see a whitespace
|
|
s++;
|
|
|
|
if (*s == '"') { // If it is a string then scan till end quote
|
|
res += *s++;
|
|
|
|
while (*s && *s != '"')
|
|
res += *s++;
|
|
|
|
if (*s == '"')
|
|
res += *s++;
|
|
} else if (Common::isAlnum(*s) || *s == '#' || *s == '.') {
|
|
// Now copy everything till whitespace
|
|
while (*s && (Common::isAlnum(*s) || *s == '.' || *s == '#' || *s == '_'))
|
|
res += *s++;
|
|
} else {
|
|
while (*s && isspec(*s))
|
|
res += *s++;
|
|
}
|
|
|
|
if (newP)
|
|
*newP = s;
|
|
|
|
return res;
|
|
}
|
|
|
|
static Common::String prevtok(const char *s, const char *lineStart, const char **newP = nullptr) {
|
|
Common::String res;
|
|
|
|
// Scan first non-whitespace
|
|
while (s >= lineStart && (*s == ' ' || *s == '\t' || *s == '\xC2')) // If we see a whitespace
|
|
if (s > lineStart) {
|
|
s--;
|
|
} else {
|
|
break;
|
|
}
|
|
|
|
if (*s == '"') { // If it is a string then scan till end quote
|
|
res += *s--;
|
|
|
|
while (s >= lineStart && *s != '"')
|
|
res = *s-- + res;
|
|
|
|
if (*s == '"')
|
|
res = *s-- + res;
|
|
} else if (Common::isAlnum(*s)) { // Now copy everything till whitespace
|
|
// Now copy everything till whitespace
|
|
while (s >= lineStart && (Common::isAlnum(*s) || *s == '.' || *s == '#' || *s == '_'))
|
|
res = *s-- + res;
|
|
} else {
|
|
while (s >= lineStart && isspec(*s))
|
|
res = *s-- + res;
|
|
}
|
|
|
|
if (newP)
|
|
*newP = s;
|
|
|
|
return res;
|
|
}
|
|
|
|
static const char *findtokstart(const char *start, const char *token) {
|
|
// First, determine, if we sit inside of a string
|
|
//
|
|
// Since we do not have escaping characters, simple count is enough
|
|
int numquotes = 0;
|
|
const char *ptr = start;
|
|
|
|
while (*ptr && ptr <= token) {
|
|
if (*ptr == '"')
|
|
numquotes++;
|
|
ptr++;
|
|
}
|
|
|
|
// We're inside of quote. Scan backwards
|
|
if (numquotes % 2) {
|
|
while (*ptr != '"')
|
|
ptr--;
|
|
|
|
return ptr;
|
|
}
|
|
|
|
// If we're in the middle of a word
|
|
while (ptr > start && Common::isAlnum(*(ptr - 1)))
|
|
ptr--;
|
|
|
|
return ptr;
|
|
}
|
|
|
|
Common::String Lingo::codePreprocessor(const char *s, LingoArchive *archive, ScriptType type, uint16 id, bool simple) {
|
|
Common::String res;
|
|
|
|
// We start from processing the continuation synbols
|
|
// \xC2\n -> \xC2
|
|
// This will greatly simplify newline processing, still leaving
|
|
// the line number tracking intact
|
|
while (*s) {
|
|
if (*s == '\xC2') {
|
|
res += *s++;
|
|
if (!*s) // Who knows, maybe it is the last symbol in the script
|
|
break;
|
|
s++;
|
|
continue;
|
|
}
|
|
res += *s++;
|
|
}
|
|
|
|
Common::String tmp(res);
|
|
res.clear();
|
|
s = tmp.c_str();
|
|
|
|
// Strip comments
|
|
bool inString = false;
|
|
while (*s) {
|
|
if (*s == '"')
|
|
inString = !inString;
|
|
|
|
if (!inString && *s == '-' && *(s + 1) == '-') { // At the end of the line we will have \0
|
|
while (*s && *s != '\n')
|
|
s++;
|
|
}
|
|
|
|
if (*s == '\r')
|
|
res += '\n';
|
|
else if (*s)
|
|
res += *s;
|
|
|
|
if (*s)
|
|
s++;
|
|
}
|
|
|
|
tmp = res;
|
|
res.clear();
|
|
|
|
// Strip trailing whitespaces
|
|
s = tmp.c_str();
|
|
while (*s) {
|
|
if (*s == ' ' || *s == '\t' || *s == '\xC2') { // If we see a whitespace
|
|
const char *ps = s; // Remember where we saw it
|
|
|
|
while (*ps == ' ' || *ps == '\t' || *ps == '\xC2') // Scan until end of whitespaces
|
|
ps++;
|
|
|
|
if (*ps) { // Not end of the string
|
|
if (*ps == '\n') { // If it is newline, then we continue from it
|
|
s = ps;
|
|
} else { // It is not a newline
|
|
while (s != ps) { // Add all whitespaces
|
|
res += *s;
|
|
s++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (*s)
|
|
res += *s;
|
|
|
|
s++;
|
|
}
|
|
|
|
if (simple)
|
|
return res;
|
|
|
|
tmp = res;
|
|
s = tmp.c_str();
|
|
res.clear();
|
|
|
|
// Preprocess if statements
|
|
// Here we add ' end if' at end of each statement, which lets us
|
|
// make the grammar very straightforward
|
|
Common::String line, tok, res1;
|
|
const char *lineStart, *prevEnd;
|
|
int iflevel = 0;
|
|
int linenumber = 1;
|
|
bool defFound = false;
|
|
|
|
while (*s) {
|
|
line.clear();
|
|
res1.clear();
|
|
|
|
// Get next line
|
|
while (*s && *s != '\n') { // If we see a whitespace
|
|
res1 += *s;
|
|
line += tolower(*s++);
|
|
|
|
if (*s == '\xc2')
|
|
linenumber++;
|
|
}
|
|
debugC(2, kDebugParse | kDebugPreprocess, "line: %d '%s'", iflevel, line.c_str());
|
|
|
|
if (!defFound && (type == kMovieScript || type == kCastScript) && (_vm->getVersion() < 400 || _vm->getCurrentMovie()->_allowOutdatedLingo)) {
|
|
tok = nexttok(line.c_str());
|
|
if (tok.equals("macro") || tok.equals("factory") || tok.equals("on")) {
|
|
defFound = true;
|
|
} else {
|
|
debugC(2, kDebugParse | kDebugPreprocess, "skipping line before first definition");
|
|
linenumber++;
|
|
if (*s) // copy newline symbol
|
|
res += *s++;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
res1 = patchLingoCode(res1, archive, type, id, linenumber);
|
|
|
|
bool changed = false;
|
|
res1 = preprocessWhen(res1, &changed);
|
|
|
|
if (!changed) {
|
|
res1 = preprocessPlay(res1);
|
|
res1 = preprocessSound(res1);
|
|
}
|
|
|
|
res += res1;
|
|
|
|
linenumber++; // We do it here because of 'continue' statements
|
|
|
|
if (line.size() < 4 || changed) { // If line is too small, then skip it
|
|
if (*s) // copy newline symbol
|
|
res += *s++;
|
|
|
|
debugC(2, kDebugParse | kDebugPreprocess, "too small");
|
|
|
|
continue;
|
|
}
|
|
|
|
tok = nexttok(line.c_str(), &lineStart);
|
|
if (tok.equals("if")) {
|
|
tok = prevtok(&line.c_str()[line.size() - 1], lineStart, &prevEnd);
|
|
debugC(2, kDebugParse | kDebugPreprocess, "start-if <%s>", tok.c_str());
|
|
|
|
if (tok.equals("if")) {
|
|
debugC(2, kDebugParse | kDebugPreprocess, "end-if");
|
|
tok = prevtok(prevEnd, lineStart);
|
|
|
|
if (tok.equals("end")) {
|
|
// do nothing, we open and close same line
|
|
debugC(2, kDebugParse | kDebugPreprocess, "end-end");
|
|
} else {
|
|
iflevel++;
|
|
}
|
|
} else if (tok.equals("then")) {
|
|
debugC(2, kDebugParse | kDebugPreprocess, "last-then");
|
|
iflevel++;
|
|
} else if (tok.equals("else")) {
|
|
debugC(2, kDebugParse | kDebugPreprocess, "last-else");
|
|
iflevel++;
|
|
} else { // other token
|
|
// Now check if we have tNLELSE
|
|
if (!*s) {
|
|
iflevel++; // end, we have to add 'end if'
|
|
break;
|
|
}
|
|
const char *s1 = s + 1;
|
|
|
|
while (*s1 && *s1 == '\n')
|
|
s1++;
|
|
tok = nexttok(s1);
|
|
|
|
if (tok.equalsIgnoreCase("else")) { // ignore case because it is look-ahead
|
|
debugC(2, kDebugParse | kDebugPreprocess, "tNLELSE");
|
|
iflevel++;
|
|
} else {
|
|
debugC(2, kDebugParse | kDebugPreprocess, "++++ end if (no nlelse after single liner)");
|
|
res += " end if";
|
|
}
|
|
}
|
|
} else if (tok.equals("else")) {
|
|
debugC(2, kDebugParse | kDebugPreprocess, "start-else");
|
|
bool elseif = false;
|
|
|
|
tok = nexttok(lineStart);
|
|
if (tok.equals("if")) {
|
|
debugC(2, kDebugParse | kDebugPreprocess, "second-if");
|
|
elseif = true;
|
|
} else if (tok.empty()) {
|
|
debugC(2, kDebugParse | kDebugPreprocess, "lonely-else");
|
|
|
|
if (*s) // copy newline symbol
|
|
res += *s++;
|
|
|
|
continue;
|
|
}
|
|
|
|
tok = prevtok(&line.c_str()[line.size() - 1], lineStart, &prevEnd);
|
|
debugC(2, kDebugParse | kDebugPreprocess, "last: '%s'", tok.c_str());
|
|
|
|
if (tok.equals("if")) {
|
|
debugC(2, kDebugParse | kDebugPreprocess, "end-if");
|
|
tok = prevtok(prevEnd, lineStart);
|
|
|
|
if (tok.equals("end")) {
|
|
debugC(2, kDebugParse | kDebugPreprocess, "end-end");
|
|
iflevel--;
|
|
}
|
|
} else if (tok.equals("then")) {
|
|
debugC(2, kDebugParse | kDebugPreprocess, "last-then");
|
|
|
|
if (elseif == false) {
|
|
warning("Badly nested then");
|
|
}
|
|
} else if (tok.equals("else")) {
|
|
debugC(2, kDebugParse | kDebugPreprocess, "last-else");
|
|
if (elseif == false) {
|
|
warning("Badly nested else");
|
|
}
|
|
} else { // check if we have tNLELSE or \nEND
|
|
if (!*s) {
|
|
break;
|
|
}
|
|
const char *s1 = s + 1;
|
|
|
|
while (*s1 && *s1 == '\n')
|
|
s1++;
|
|
tok = nexttok(s1, &s1);
|
|
|
|
if (tok.equalsIgnoreCase("else") && elseif) {
|
|
// Nothing to do here, same level
|
|
debugC(2, kDebugParse | kDebugPreprocess, "tNLELSE");
|
|
} else if (tok.equalsIgnoreCase("end") && elseif) {
|
|
tok = nexttok(s1);
|
|
|
|
if (tok.equalsIgnoreCase("if")) {
|
|
// Nothing to do here
|
|
debugC(2, kDebugParse | kDebugPreprocess, "see-end-if");
|
|
} else {
|
|
debugC(2, kDebugParse | kDebugPreprocess, "++++ end if (no tNLELSE 2)");
|
|
res += " end if";
|
|
iflevel--;
|
|
}
|
|
} else {
|
|
debugC(2, kDebugParse | kDebugPreprocess, "++++ end if (no tNLELSE)");
|
|
res += " end if";
|
|
iflevel--;
|
|
}
|
|
}
|
|
} else if (tok.equals("end")) {
|
|
debugC(2, kDebugParse | kDebugPreprocess, "start-end");
|
|
|
|
tok = nexttok(lineStart);
|
|
if (tok.equals("if")) {
|
|
debugC(2, kDebugParse | kDebugPreprocess, "second-if");
|
|
iflevel--;
|
|
}
|
|
} else if (tok.equals("when")) {
|
|
debugC(2, kDebugParse | kDebugPreprocess, "start-when");
|
|
|
|
if (strstr(lineStart, "if") && strstr(lineStart, "then")) {
|
|
tok = prevtok(&line.c_str()[line.size() - 1], lineStart, &prevEnd);
|
|
debugC(2, kDebugParse | kDebugPreprocess, "when-start-if <%s>", tok.c_str());
|
|
|
|
if (tok.equals("if")) {
|
|
debugC(2, kDebugParse | kDebugPreprocess, "when-end-if");
|
|
tok = prevtok(prevEnd, lineStart);
|
|
|
|
if (tok.equals("end")) {
|
|
// do nothing, we open and close same line
|
|
debugC(2, kDebugParse | kDebugPreprocess, "when-end-end");
|
|
} else {
|
|
res += " end if";
|
|
}
|
|
} else {
|
|
res += " end if";
|
|
}
|
|
}
|
|
} else {
|
|
debugC(2, kDebugParse | kDebugPreprocess, "nothing");
|
|
}
|
|
|
|
if (*s) // copy newline symbol
|
|
res += *s++;
|
|
}
|
|
|
|
for (int i = 0; i < iflevel; i++) {
|
|
debugC(2, kDebugParse | kDebugPreprocess, "++++ end if (unclosed)");
|
|
res += "\nend if";
|
|
}
|
|
|
|
// Make the parser happier when there is no newline at the end
|
|
res += '\n';
|
|
|
|
debugC(2, kDebugParse | kDebugPreprocess, "#############\n%s\n#############", res.c_str());
|
|
|
|
return res;
|
|
}
|
|
|
|
// when ID then statement -> when ID then "statement"
|
|
Common::String preprocessWhen(Common::String in, bool *changed) {
|
|
Common::String res, next;
|
|
const char *ptr = in.c_str();
|
|
const char *beg = ptr;
|
|
const char *nextPtr;
|
|
|
|
while ((ptr = scumm_strcasestr(beg, "when")) != NULL) {
|
|
if (ptr != findtokstart(in.c_str(), ptr)) { // If we're in the middle of a word
|
|
res += *beg++;
|
|
continue;
|
|
}
|
|
|
|
ptr += 4; // end of 'play'
|
|
res += Common::String(beg, ptr);
|
|
|
|
if (!*ptr) // If it is end of the line
|
|
break;
|
|
|
|
if (Common::isAlnum(*ptr)) { // If it is in the middle of the word
|
|
beg = ptr;
|
|
continue;
|
|
}
|
|
|
|
*changed = true;
|
|
|
|
res += ' ';
|
|
next = nexttok(ptr, &nextPtr); // ID
|
|
res += next;
|
|
|
|
res += ' ';
|
|
next = nexttok(nextPtr, &nextPtr); // then
|
|
res += next;
|
|
|
|
res += ' ';
|
|
res += '"';
|
|
|
|
// now we need to preprocess quotes
|
|
bool skipQuote = false;
|
|
while (*nextPtr) {
|
|
if (*nextPtr == '"') {
|
|
res += "\" & QUOTE ";
|
|
|
|
if (*(nextPtr + 1))
|
|
res += "& \"";
|
|
else
|
|
skipQuote = true; // we do not want the last quote
|
|
} else {
|
|
res += *nextPtr;
|
|
}
|
|
|
|
nextPtr++;
|
|
}
|
|
|
|
if (!skipQuote)
|
|
res += '"';
|
|
|
|
beg = nextPtr;
|
|
|
|
break;
|
|
}
|
|
|
|
res += Common::String(beg);
|
|
|
|
if (in.size() != res.size())
|
|
debugC(2, kDebugParse | kDebugPreprocess, "WHEN: in: %s\nout: %s", in.c_str(), res.c_str());
|
|
|
|
return res;
|
|
}
|
|
|
|
// play done -> play #done
|
|
Common::String preprocessPlay(Common::String in) {
|
|
Common::String res, next;
|
|
const char *ptr = in.c_str();
|
|
const char *beg = ptr;
|
|
const char *nextPtr;
|
|
|
|
while ((ptr = scumm_strcasestr(beg, "play")) != NULL) {
|
|
if (ptr != findtokstart(in.c_str(), ptr)) { // If we're in the middle of a word
|
|
res += *beg++;
|
|
continue;
|
|
}
|
|
|
|
ptr += 4; // end of 'play'
|
|
res += Common::String(beg, ptr);
|
|
|
|
if (!*ptr) // If it is end of the line
|
|
break;
|
|
|
|
if (Common::isAlnum(*ptr)) { // If it is in the middle of the word
|
|
beg = ptr;
|
|
continue;
|
|
}
|
|
|
|
next = nexttok(ptr, &nextPtr);
|
|
|
|
debugC(2, kDebugParse | kDebugPreprocess, "PLAY: nexttok: %s", next.c_str());
|
|
|
|
if (next.equalsIgnoreCase("done")) {
|
|
res += " #"; // Turn it into SYMBOL
|
|
} else {
|
|
res += ' ';
|
|
}
|
|
|
|
res += next;
|
|
ptr = nextPtr;
|
|
beg = ptr;
|
|
}
|
|
|
|
res += Common::String(beg);
|
|
|
|
if (in.size() != res.size())
|
|
debugC(2, kDebugParse | kDebugPreprocess, "PLAY: in: %s\nout: %s", in.c_str(), res.c_str());
|
|
|
|
return res;
|
|
}
|
|
|
|
// sound fadeIn 5, 10 -> sound #fadeIn, 5, 10
|
|
Common::String preprocessSound(Common::String in) {
|
|
Common::String res, next;
|
|
const char *ptr = in.c_str();
|
|
const char *beg = ptr;
|
|
const char *nextPtr;
|
|
|
|
while ((ptr = scumm_strcasestr(beg, "sound")) != NULL) {
|
|
if (ptr != findtokstart(in.c_str(), ptr)) { // If we're in the middle of a word
|
|
res += *beg++;
|
|
continue;
|
|
}
|
|
|
|
ptr += 5; // end of 'sound'
|
|
res += Common::String(beg, ptr);
|
|
|
|
if (!*ptr) // If it is end of the line
|
|
break;
|
|
|
|
if (Common::isAlnum(*ptr)) { // If it is in the middle of the word
|
|
beg = ptr;
|
|
continue;
|
|
}
|
|
|
|
next = nexttok(ptr, &nextPtr);
|
|
|
|
debugC(2, kDebugParse | kDebugPreprocess, "SOUND: nexttok: %s", next.c_str());
|
|
|
|
bool modified = false;
|
|
|
|
if (next.equalsIgnoreCase("close") ||
|
|
next.equalsIgnoreCase("fadeIn") ||
|
|
next.equalsIgnoreCase("fadeOut") ||
|
|
next.equalsIgnoreCase("playFile") ||
|
|
next.equalsIgnoreCase("stop")) {
|
|
res += '#'; // Turn it into SYMBOL
|
|
modified = true;
|
|
} else {
|
|
res += ' ';
|
|
}
|
|
|
|
res += next;
|
|
if (modified)
|
|
res += ',';
|
|
ptr = nextPtr;
|
|
beg = ptr;
|
|
}
|
|
|
|
res += Common::String(beg);
|
|
|
|
if (in.size() != res.size())
|
|
debugC(2, kDebugParse | kDebugPreprocess, "SOUND: in: %s\nout: %s", in.c_str(), res.c_str());
|
|
|
|
return res;
|
|
}
|
|
|
|
} // End of namespace Director
|