scummvm/engines/vcruise/script.cpp
2024-08-17 09:24:54 +03:00

1715 lines
63 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 "common/debug.h"
#include "common/stream.h"
#include "common/hash-str.h"
#include "vcruise/script.h"
#include "vcruise/textparser.h"
namespace VCruise {
enum ScriptDialect {
kScriptDialectReah,
kScriptDialectSchizm,
kScriptDialectAD2044,
};
class LogicUnscrambleStream : public Common::ReadStream {
public:
LogicUnscrambleStream(Common::ReadStream *stream, uint streamSize);
bool eos() const override;
uint32 read(void *dataPtr, uint32 dataSize) override;
private:
byte _cipher[255];
uint _cipherOffset;
Common::ReadStream *_stream;
};
LogicUnscrambleStream::LogicUnscrambleStream(Common::ReadStream *stream, uint streamSize) : _stream(stream) {
int key = 255;
for (int i = 0; i < 255; i++) {
int parityBit = ((key ^ (key >> 1) ^ (key >> 6) ^ (key >> 7))) & 1;
key = (key >> 1) | (parityBit << 7);
_cipher[254 - i] = key;
}
_cipherOffset = 255u - (streamSize % 255u);
}
bool LogicUnscrambleStream::eos() const {
return _stream->eos();
}
uint32 LogicUnscrambleStream::read(void *dataPtr, uint32 dataSize) {
uint32 numRead = _stream->read(dataPtr, dataSize);
byte *decipher = static_cast<byte *>(dataPtr);
uint cipherOffset = _cipherOffset;
uint32 remaining = numRead;
while (remaining) {
if (cipherOffset == 255)
cipherOffset = 0;
(*decipher++) ^= _cipher[cipherOffset++];
remaining--;
}
_cipherOffset = cipherOffset;
return numRead;
}
Instruction::Instruction() : op(ScriptOps::kInvalid), arg(0) {
}
Instruction::Instruction(ScriptOps::ScriptOp paramOp) : op(paramOp), arg(0) {
}
Instruction::Instruction(ScriptOps::ScriptOp paramOp, int32 paramArg) : op(paramOp), arg(paramArg) {
}
enum ProtoOp {
kProtoOpScript, // Use script opcode
kProtoOpNoop,
kProtoOpJumpToLabel,
kProtoOpLabel,
kProtoOpIf,
kProtoOpElse,
kProtoOpEndIf,
kProtoOpSwitch,
kProtoOpCase,
kProtoOpEndSwitch,
kProtoOpDefault,
kProtoOpBreak,
};
struct ProtoInstruction {
ProtoInstruction();
explicit ProtoInstruction(ScriptOps::ScriptOp op);
ProtoInstruction(ScriptOps::ScriptOp paramOp, int32 paramArg);
ProtoInstruction(ProtoOp paramProtoOp, ScriptOps::ScriptOp paramOp, int32 paramArg);
ProtoOp protoOp;
ScriptOps::ScriptOp op;
int32 arg;
};
ProtoInstruction::ProtoInstruction() : protoOp(kProtoOpScript), op(ScriptOps::kInvalid), arg(0) {
}
ProtoInstruction::ProtoInstruction(ScriptOps::ScriptOp paramOp) : protoOp(kProtoOpScript), op(paramOp), arg(0) {
}
ProtoInstruction::ProtoInstruction(ScriptOps::ScriptOp paramOp, int32 paramArg) : protoOp(kProtoOpScript), op(paramOp), arg(paramArg) {
}
ProtoInstruction::ProtoInstruction(ProtoOp paramProtoOp, ScriptOps::ScriptOp paramOp, int32 paramArg) : protoOp(paramProtoOp), op(paramOp), arg(paramArg) {
}
struct ProtoScript {
void reset();
Common::Array<ProtoInstruction> instrs;
};
void ProtoScript::reset() {
instrs.clear();
}
struct ScriptNamedInstruction {
const char *str;
ProtoOp protoOp;
ScriptOps::ScriptOp op;
};
class ScriptCompiler {
public:
ScriptCompiler(TextParser &parser, const Common::Path &blamePath, ScriptDialect dialect, uint loadAsRoom, uint fileRoom, IScriptCompilerGlobalState *gs);
void compileScriptSet(ScriptSet *ss);
private:
bool parseNumber(const Common::String &token, uint32 &outNumber, bool mayStartWithNonZeroDigit) const;
static bool parseDecNumber(const Common::String &token, uint start, uint32 &outNumber);
static bool parseHexNumber(const Common::String &token, uint start, uint32 &outNumber);
static bool parseBinNumber(const Common::String &token, uint start, uint32 &outNumber);
void expectNumber(uint32 &outNumber, bool mayStartWithNonZeroDigit);
void compileRoomScriptSet(RoomScriptSet *rss);
void compileReahOrAD2044ScreenScriptSet(ScreenScriptSet *sss);
void compileSchizmScreenScriptSet(ScreenScriptSet *sss);
void compileFunction(Script *script);
bool compileInstructionToken(ProtoScript &script, const Common::String &token);
void codeGenScript(ProtoScript &protoScript, Script &script);
uint indexString(const Common::String &str);
enum NumberParsingMode {
kNumberParsingDec,
kNumberParsingHex,
kNumberParsingBin,
};
TextParser &_parser;
NumberParsingMode _numberParsingMode;
const Common::Path _blamePath;
ScriptDialect _dialect;
uint _loadAsRoom;
const char *_scrToken;
const char *_eroomToken;
Common::HashMap<Common::String, uint> _stringToIndex;
IScriptCompilerGlobalState *_gs;
};
class ScriptCompilerGlobalState : public IScriptCompilerGlobalState {
public:
void define(const Common::String &key, uint roomNumber, int32 value) override;
bool getDefine(const Common::String &str, uint &outRoomNumber, int32 &outValue) const override;
uint getFunctionIndex(const Common::String &fnName) override;
void setFunction(uint fnIndex, const Common::SharedPtr<Script> &fn) override;
uint getNumFunctions() const override;
void dumpFunctionNames(Common::Array<Common::String> &fnNames) const override;
Common::SharedPtr<Script> getFunction(uint fnIndex) const override;
private:
// Defs are linked to room numbers to deal with some weird variable sharing behavior. In Reah,
// variables are bound to room, but in Schizm they are clearly not. fnInitNewGame in Room01.log
// initializes a lot of things in various rooms, but some of those IDs also collide with other
// things. For example, dwNawigat2SZ is 13, and is read in Room20, but variable ID 13 is also
// used for dwStartSound47 in Room47 to determine if sounds have started, and it is expected
// to be initially 0 there.
struct Def {
Def();
int32 _value;
uint _roomNumber;
};
Common::HashMap<Common::String, Def> _defs;
Common::HashMap<Common::String, uint> _functionNameToIndex;
Common::Array<Common::SharedPtr<Script> > _functions;
};
ScriptCompiler::ScriptCompiler(TextParser &parser, const Common::Path &blamePath, ScriptDialect dialect, uint loadAsRoom, uint fileRoom, IScriptCompilerGlobalState *gs)
: _numberParsingMode(kNumberParsingHex), _parser(parser), _blamePath(blamePath), _dialect(dialect), _loadAsRoom(loadAsRoom), _gs(gs),
_scrToken(nullptr), _eroomToken(nullptr) {
}
bool ScriptCompiler::parseNumber(const Common::String &token, uint32 &outNumber, bool mayStartWithNonZeroDigit) const {
if (token.size() == 0)
return false;
if (_dialect == kScriptDialectReah && token[0] == 'd')
return parseDecNumber(token, 1, outNumber);
if (_dialect == kScriptDialectReah || _dialect == kScriptDialectAD2044) {
bool isValidNumber = false;
char firstChar = token[0];
if (_dialect == kScriptDialectReah || !mayStartWithNonZeroDigit)
isValidNumber = (firstChar == '0');
else
isValidNumber = (firstChar >= '0' && firstChar <= '9'); // Some room numbers lack a 0 prefix
if (isValidNumber) {
switch (_numberParsingMode) {
case kNumberParsingDec:
return parseDecNumber(token, 0, outNumber);
case kNumberParsingHex:
return parseHexNumber(token, 0, outNumber);
case kNumberParsingBin:
return parseBinNumber(token, 0, outNumber);
default:
error("Unknown number parsing mode");
return false;
}
}
} else if (_dialect == kScriptDialectSchizm) {
if (token.size() >= 2 && token[0] == '0' && token[1] == 'x')
return parseHexNumber(token, 2, outNumber);
if (token[token.size() - 1] == 'b')
return parseBinNumber(token.substr(0, token.size() - 1), 0, outNumber);
if (token[token.size() - 1] == 'h')
return parseHexNumber(token.substr(0, token.size() - 1), 0, outNumber);
return parseDecNumber(token, 0, outNumber);
}
return false;
}
bool ScriptCompiler::parseDecNumber(const Common::String &token, uint start, uint32 &outNumber) {
if (start == token.size())
return false;
// We don't use sscanf because sscanf accepts partial results and we want to reject this if any character is mismatched
uint32 num = 0;
for (uint i = start; i < token.size(); i++) {
num *= 10u;
char c = token[i];
if (c >= '0' && c <= '9')
num += static_cast<uint32>(c - '0');
else
return false;
}
outNumber = num;
return true;
}
bool ScriptCompiler::parseHexNumber(const Common::String &token, uint start, uint32 &outNumber) {
if (start == token.size())
return false;
// We don't use sscanf because sscanf accepts partial results and we want to reject this if any character is mismatched
uint32 num = 0;
for (uint i = start; i < token.size(); i++) {
num *= 16u;
char c = token[i];
if (c >= '0' && c <= '9')
num += static_cast<uint32>(c - '0');
else if (c >= 'a' && c <= 'f')
num += static_cast<uint32>(c - 'a' + 0xa);
else if (c >= 'A' && c <= 'F')
num += static_cast<uint32>(c - 'A' + 0xa);
else
return false;
}
outNumber = num;
return true;
}
bool ScriptCompiler::parseBinNumber(const Common::String &token, uint start, uint32 &outNumber) {
if (start == token.size())
return false;
uint num = 0;
for (uint i = start; i < token.size(); i++) {
char c = token[i];
num <<= 1;
if (c == '1')
num |= 1;
else if (c != '0')
return false;
}
outNumber = num;
return true;
}
void ScriptCompiler::expectNumber(uint32 &outNumber, bool mayStartWithNonZeroDigit) {
TextParserState state;
Common::String token;
if (_parser.parseToken(token, state)) {
if (!parseNumber(token, outNumber, mayStartWithNonZeroDigit))
error("Error compiling script at line %i col %i: Expected number but found '%s'", static_cast<int>(state._lineNum), static_cast<int>(state._col), token.c_str());
} else {
error("Error compiling script at line %i col %i: Expected number", static_cast<int>(state._lineNum), static_cast<int>(state._col));
}
}
void ScriptCompiler::compileScriptSet(ScriptSet *ss) {
Common::SharedPtr<RoomScriptSet> roomScript;
uint numExistingStrings = ss->strings.size();
for (uint i = 0; i < numExistingStrings; i++)
_stringToIndex[ss->strings[i]] = i;
const char *roomToken = nullptr;
if (_dialect == kScriptDialectReah || _dialect == kScriptDialectAD2044) {
roomToken = "~ROOM";
_eroomToken = "~EROOM";
_scrToken = "~SCR";
} else if (_dialect == kScriptDialectSchizm) {
roomToken = "~Room";
_eroomToken = "~ERoom";
_scrToken = "~Scr";
} else
error("Unknown script dialect");
TextParserState state;
Common::String token;
while (_parser.parseToken(token, state)) {
if (token == roomToken) {
if (roomScript)
error("Error compiling script at line %i col %i: Encountered %s without %s", static_cast<int>(state._lineNum), static_cast<int>(state._col), roomToken, _eroomToken);
roomScript.reset(new RoomScriptSet());
{
uint32 roomNumber = 0;
if (_parser.parseToken(token, state)) {
if (!parseNumber(token, roomNumber, true))
error("Error compiling script at line %i col %i: Expected number but found '%s'", static_cast<int>(state._lineNum), static_cast<int>(state._col), token.c_str());
ss->roomScripts[roomNumber] = roomScript;
} else {
error("Error compiling script at line %i col %i: Expected number", static_cast<int>(state._lineNum), static_cast<int>(state._col));
}
}
compileRoomScriptSet(roomScript.get());
} else {
error("Error compiling script at line %i col %i: Expected %s and found '%s'", static_cast<int>(state._lineNum), static_cast<int>(state._col), roomToken, token.c_str());
}
}
for (const Common::HashMap<Common::String, uint>::Node &stiNode : _stringToIndex) {
if (stiNode._value >= numExistingStrings) {
if (stiNode._value >= ss->strings.size())
ss->strings.resize(stiNode._value + 1);
ss->strings[stiNode._value] = stiNode._key;
}
}
}
void ScriptCompiler::compileRoomScriptSet(RoomScriptSet *rss) {
TextParserState state;
Common::String token;
while (_parser.parseToken(token, state)) {
if (token == _eroomToken) {
return;
} else if (token == _scrToken) {
uint32 screenNumber = 0;
expectNumber(screenNumber, false);
Common::SharedPtr<ScreenScriptSet> sss(new ScreenScriptSet());
if (_dialect == kScriptDialectReah || _dialect == kScriptDialectAD2044)
compileReahOrAD2044ScreenScriptSet(sss.get());
else if (_dialect == kScriptDialectSchizm) {
if (!_parser.parseToken(token, state))
error("Error compiling script at line %i col %i: Expected screen name", static_cast<int>(state._lineNum), static_cast<int>(state._col));
rss->screenNames[token] = screenNumber;
compileSchizmScreenScriptSet(sss.get());
}
// QUIRK: The tower in Reah (Room 06) has two 0cb screens, the second one is bad and must be ignored
if (rss->screenScripts.find(screenNumber) == rss->screenScripts.end())
rss->screenScripts[screenNumber] = sss;
} else if (_dialect == kScriptDialectSchizm && token == "#def") {
Common::String key;
Common::String value;
if (!_parser.parseToken(key, state))
error("Error compiling script at line %i col %i: Expected key", static_cast<int>(state._lineNum), static_cast<int>(state._col));
if (!_parser.parseToken(value, state))
error("Error compiling script at line %i col %i: Expected value", static_cast<int>(state._lineNum), static_cast<int>(state._col));
bool isNegative = false;
if (value[0] == '-') {
isNegative = true;
value = value.substr(1);
}
uint32 number = 0;
if (!parseNumber(value, number, false))
error("Error compiling script at line %i col %i: Expected number", static_cast<int>(state._lineNum), static_cast<int>(state._col));
int32 signedNumber = static_cast<int32>(number);
if (isNegative)
signedNumber = -signedNumber;
// Based on testing, this should be _loadAsRoom
_gs->define(key, _loadAsRoom, signedNumber);
} else if (_dialect == kScriptDialectSchizm && token == "~Fun") {
Common::String fnName;
if (!_parser.parseToken(fnName, state))
error("Error compiling script at line %i col %i: Expected function name", static_cast<int>(state._lineNum), static_cast<int>(state._col));
Common::SharedPtr<Script> func(new Script());
compileFunction(func.get());
uint fnIndex = _gs->getFunctionIndex(fnName);
if (_gs->getFunction(fnIndex)) {
// This triggers on fnSoundFountain_Start and fnSoundFountain_Stop in Room30.
// fnSoundFountain_Start is called in Room31, so that doesn't collide, but
// fnSoundFountain_Stop is called in Room30. Based on testing, the correct
// behavior is to call the Room30 version, so we do want to override here.
warning("Function '%s' was defined multiple times", fnName.c_str());
}
_gs->setFunction(_gs->getFunctionIndex(fnName), func);
} else {
error("Error compiling script at line %i col %i: Expected %s or %s and found '%s'", static_cast<int>(state._lineNum), static_cast<int>(state._col), _eroomToken, _scrToken, token.c_str());
}
}
error("Error compiling script: Room wasn't terminated");
}
void ScriptCompiler::compileReahOrAD2044ScreenScriptSet(ScreenScriptSet *sss) {
TextParserState state;
Common::String token;
ProtoScript protoScript;
Common::SharedPtr<Script> currentScript(new Script());
sss->entryScript.reset(currentScript);
while (_parser.parseToken(token, state)) {
if (token == "~EROOM" || token == "~SCR") {
_parser.requeue(token, state);
codeGenScript(protoScript, *currentScript);
return;
} else if (token == "~*") {
uint32 interactionNumber = 0;
expectNumber(interactionNumber, false);
codeGenScript(protoScript, *currentScript);
currentScript.reset(new Script());
protoScript.reset();
sss->interactionScripts[interactionNumber] = currentScript;
} else if (token == "DEC") {
_numberParsingMode = kNumberParsingDec;
} else if (token == "HEX") {
_numberParsingMode = kNumberParsingHex;
} else if (token == "BIN") {
_numberParsingMode = kNumberParsingBin;
} else if (_dialect == kScriptDialectReah && token == "dubbing") {
Common::String dubbingName;
_parser.expectToken(dubbingName, _blamePath);
protoScript.instrs.push_back(ProtoInstruction(ScriptOps::kDubbing, indexString(dubbingName)));
} else if (compileInstructionToken(protoScript, token)) {
// Nothing
} else {
error("Error compiling script at line %i col %i: Expected %s or %s or ~* or instruction but found '%s'", static_cast<int>(state._lineNum), static_cast<int>(state._col), _eroomToken, _scrToken, token.c_str());
}
}
}
void ScriptCompiler::compileSchizmScreenScriptSet(ScreenScriptSet *sss) {
TextParserState state;
Common::String token;
ProtoScript protoScript;
Common::SharedPtr<Script> currentScript(new Script());
sss->entryScript.reset(currentScript);
while (_parser.parseToken(token, state)) {
if (token == "~ERoom" || token == "~Scr" || token == "~Fun") {
_parser.requeue(token, state);
codeGenScript(protoScript, *currentScript);
return;
} else if (token == "~*") {
uint32 interactionNumber = 0;
expectNumber(interactionNumber, false);
codeGenScript(protoScript, *currentScript);
currentScript.reset(new Script());
protoScript.reset();
sss->interactionScripts[interactionNumber] = currentScript;
} else if (compileInstructionToken(protoScript, token)) {
// Nothing
} else {
error("Error compiling script at line %i col %i: Expected ~EROOM or ~SCR or ~* or instruction but found '%s'", static_cast<int>(state._lineNum), static_cast<int>(state._col), token.c_str());
}
}
}
void ScriptCompiler::compileFunction(Script *script) {
TextParserState state;
Common::String token;
ProtoScript protoScript;
while (_parser.parseToken(token, state)) {
if (token == "~ERoom" || token == "~Scr" || token == "~Fun") {
_parser.requeue(token, state);
codeGenScript(protoScript, *script);
return;
} else if (compileInstructionToken(protoScript, token)) {
// Nothing
} else {
error("Error compiling script at line %i col %i: Expected ~ERoom or ~Scr or ~Fun but found '%s'", static_cast<int>(state._lineNum), static_cast<int>(state._col), token.c_str());
}
}
}
static ScriptNamedInstruction g_ad2044NamedInstructions[] = {
//{"rotate", ProtoOp::kProtoOpScript, ScriptOps::kRotate},
//{"angle", ProtoOp::kProtoOpScript, ScriptOps::kAngle},
//{"angleG@", ProtoOp::kProtoOpScript, ScriptOps::kAngleGGet},
//{"speed", ProtoOp::kProtoOpScript, ScriptOps::kSpeed},
//{"sanimL", ProtoOp::kProtoOpScript, ScriptOps::kSAnimL},
//{"changeL", ProtoOp::kProtoOpScript, ScriptOps::kChangeL},
//{"changeL1", ProtoOp::kProtoOpScript, ScriptOps::kChangeL}, // This seems wrong, but not sure what changeL1 does differently from changeL yet
//{"animF", ProtoOp::kProtoOpScript, ScriptOps::kAnimF},
//{"animG", ProtoOp::kProtoOpScript, ScriptOps::kAnimG},
//{"animN", ProtoOp::kProtoOpScript, ScriptOps::kAnimN},
//{"animR", ProtoOp::kProtoOpScript, ScriptOps::kAnimR},
//{"animS", ProtoOp::kProtoOpScript, ScriptOps::kAnimS},
//{"anim", ProtoOp::kProtoOpScript, ScriptOps::kAnim},
{"animT", ProtoOp::kProtoOpScript, ScriptOps::kAnimT},
{"ani+", ProtoOp::kProtoOpScript, ScriptOps::kAnimForward},
{"ani-", ProtoOp::kProtoOpScript, ScriptOps::kAnimReverse},
{"kani+", ProtoOp::kProtoOpScript, ScriptOps::kAnimForward}, // FIXME: Do we really want to use this op?
{"anis+", ProtoOp::kProtoOpScript, ScriptOps::kAnimForward}, // FIXME: Do we really want to use this op?
{"anis-", ProtoOp::kProtoOpScript, ScriptOps::kAnimReverse}, // FIXME: Do we really want to use this op?
//{"static", ProtoOp::kProtoOpScript, ScriptOps::kStatic},
{"yes@", ProtoOp::kProtoOpScript, ScriptOps::kVarLoad},
{"yes!", ProtoOp::kProtoOpScript, ScriptOps::kVarStore},
{"yesg@", ProtoOp::kProtoOpScript, ScriptOps::kVarGlobalLoad},
{"yesg!", ProtoOp::kProtoOpScript, ScriptOps::kVarGlobalStore},
//{"setaX+!", ProtoOp::kProtoOpScript, ScriptOps::kVarAddAndStore},
//{"cr?", ProtoOp::kProtoOpScript, ScriptOps::kItemCheck},
//{"cr!", ProtoOp::kProtoOpScript, ScriptOps::kItemRemove},
//{"sr!", ProtoOp::kProtoOpScript, ScriptOps::kItemHighlightSet},
//{"r?", ProtoOp::kProtoOpScript, ScriptOps::kItemHaveSpace},
//{"r!", ProtoOp::kProtoOpScript, ScriptOps::kItemAdd},
{"r@", ProtoOp::kProtoOpScript, ScriptOps::kRGet},
{"r!", ProtoOp::kProtoOpScript, ScriptOps::kRSet},
{"endr!", ProtoOp::kProtoOpScript, ScriptOps::kEndRSet},
//{"clearPocket", ProtoOp::kProtoOpScript, ScriptOps::kItemClear},
{"cursor!", ProtoOp::kProtoOpScript, ScriptOps::kSetCursor},
{"room!", ProtoOp::kProtoOpScript, ScriptOps::kSetRoom},
{"lmb", ProtoOp::kProtoOpScript, ScriptOps::kLMB},
//{"lmb1", ProtoOp::kProtoOpScript, ScriptOps::kLMB1},
//{"volumeDn2", ProtoOp::kProtoOpScript, ScriptOps::kVolumeDn2},
//{"volumeDn3", ProtoOp::kProtoOpScript, ScriptOps::kVolumeDn3},
//{"volumeDn4", ProtoOp::kProtoOpScript, ScriptOps::kVolumeDn4},
//{"volumeUp3", ProtoOp::kProtoOpScript, ScriptOps::kVolumeUp3},
{"rnd", ProtoOp::kProtoOpScript, ScriptOps::kRandom},
//{"drop", ProtoOp::kProtoOpScript, ScriptOps::kDrop},
//{"dup", ProtoOp::kProtoOpScript, ScriptOps::kDup},
//{"swap", ProtoOp::kProtoOpScript, ScriptOps::kSwap},
//{"say1", ProtoOp::kProtoOpScript, ScriptOps::kSay1},
//{"say2", ProtoOp::kProtoOpScript, ScriptOps::kSay2},
//{"say3", ProtoOp::kProtoOpScript, ScriptOps::kSay3},
//{"say3@", ProtoOp::kProtoOpScript, ScriptOps::kSay3Get},
{"say2k", ProtoOp::kProtoOpScript, ScriptOps::kSay2K},
{"say3k", ProtoOp::kProtoOpScript, ScriptOps::kSay3K},
//{"setTimer", ProtoOp::kProtoOpScript, ScriptOps::kSetTimer},
//{"getTimer", ProtoOp::kProtoOpScript, ScriptOps::kGetTimer},
//{"delay", ProtoOp::kProtoOpScript, ScriptOps::kDelay},
//{"lo!", ProtoOp::kProtoOpScript, ScriptOps::kLoSet},
//{"lo@", ProtoOp::kProtoOpScript, ScriptOps::kLoGet},
//{"hi!", ProtoOp::kProtoOpScript, ScriptOps::kHiSet},
//{"hi@", ProtoOp::kProtoOpScript, ScriptOps::kHiGet},
{"and", ProtoOp::kProtoOpScript, ScriptOps::kAnd},
{"or", ProtoOp::kProtoOpScript, ScriptOps::kOr},
{"+", ProtoOp::kProtoOpScript, ScriptOps::kAdd},
{"-", ProtoOp::kProtoOpScript, ScriptOps::kSub},
{"not", ProtoOp::kProtoOpScript, ScriptOps::kNot},
{"=", ProtoOp::kProtoOpScript, ScriptOps::kCmpEq},
{">", ProtoOp::kProtoOpScript, ScriptOps::kCmpGt},
{"<", ProtoOp::kProtoOpScript, ScriptOps::kCmpLt},
//{"bit@", ProtoOp::kProtoOpScript, ScriptOps::kBitLoad},
//{"bit0!", ProtoOp::kProtoOpScript, ScriptOps::kBitSet0},
//{"bit1!", ProtoOp::kProtoOpScript, ScriptOps::kBitSet1},
//{"soundS1", ProtoOp::kProtoOpScript, ScriptOps::kSoundS1},
//{"soundS2", ProtoOp::kProtoOpScript, ScriptOps::kSoundS2},
//{"soundS3", ProtoOp::kProtoOpScript, ScriptOps::kSoundS3},
//{"soundL1", ProtoOp::kProtoOpScript, ScriptOps::kSoundL1},
//{"soundL2", ProtoOp::kProtoOpScript, ScriptOps::kSoundL2},
//{"soundL3", ProtoOp::kProtoOpScript, ScriptOps::kSoundL3},
//{"3DsoundS2", ProtoOp::kProtoOpScript, ScriptOps::k3DSoundS2},
//{"3DsoundL2", ProtoOp::kProtoOpScript, ScriptOps::k3DSoundL2},
//{"3DsoundL3", ProtoOp::kProtoOpScript, ScriptOps::k3DSoundL3},
//{"stopaL", ProtoOp::kProtoOpScript, ScriptOps::kStopAL},
//{"range", ProtoOp::kProtoOpScript, ScriptOps::kRange},
//{"addXsound", ProtoOp::kProtoOpScript, ScriptOps::kAddXSound},
//{"clrXsound", ProtoOp::kProtoOpScript, ScriptOps::kClrXSound},
//{"stopSndLA", ProtoOp::kProtoOpScript, ScriptOps::kStopSndLA},
//{"stopSndLO", ProtoOp::kProtoOpScript, ScriptOps::kStopSndLO},
//{"music", ProtoOp::kProtoOpScript, ScriptOps::kMusic},
//{"musicUp", ProtoOp::kProtoOpScript, ScriptOps::kMusicVolRamp},
//{"musicDn", ProtoOp::kProtoOpScript, ScriptOps::kMusicVolRamp},
//{"parm0", ProtoOp::kProtoOpScript, ScriptOps::kParm0},
//{"parm1", ProtoOp::kProtoOpScript, ScriptOps::kParm1},
//{"parm2", ProtoOp::kProtoOpScript, ScriptOps::kParm2},
//{"parm3", ProtoOp::kProtoOpScript, ScriptOps::kParm3},
//{"parmG", ProtoOp::kProtoOpScript, ScriptOps::kParmG},
//{"sparmX", ProtoOp::kProtoOpScript, ScriptOps::kSParmX},
//{"sanimX", ProtoOp::kProtoOpScript, ScriptOps::kSAnimX},
//{"disc1", ProtoOp::kProtoOpScript, ScriptOps::kDisc1},
//{"disc2", ProtoOp::kProtoOpScript, ScriptOps::kDisc2},
//{"disc3", ProtoOp::kProtoOpScript, ScriptOps::kDisc3},
//{"goto", ProtoOp::kProtoOpScript, ScriptOps::kGoto},
{"stop", ProtoOp::kProtoOpScript, ScriptOps::kStop},
{"#if", ProtoOp::kProtoOpIf, ScriptOps::kInvalid},
{"#eif", ProtoOp::kProtoOpEndIf, ScriptOps::kInvalid},
{"#else", ProtoOp::kProtoOpElse, ScriptOps::kInvalid},
{"#switch:", ProtoOp::kProtoOpSwitch, ScriptOps::kInvalid},
{"#eswitch", ProtoOp::kProtoOpEndSwitch, ScriptOps::kInvalid},
{"break", ProtoOp::kProtoOpBreak, ScriptOps::kInvalid},
{"#default", ProtoOp::kProtoOpDefault, ScriptOps::kInvalid},
//{"esc_on", ProtoOp::kProtoOpScript, ScriptOps::kEscOn},
//{"esc_off", ProtoOp::kProtoOpScript, ScriptOps::kEscOff},
//{"esc_get@", ProtoOp::kProtoOpScript, ScriptOps::kEscGet},
//{"backStart", ProtoOp::kProtoOpScript, ScriptOps::kBackStart},
//{"saveAs", ProtoOp::kProtoOpScript, ScriptOps::kSaveAs},
//{"save0", ProtoOp::kProtoOpNoop, ScriptOps::kSave0},
//{"exit", ProtoOp::kProtoOpScript, ScriptOps::kExit},
//{"allowedSave", ProtoOp::kProtoOpScript, ScriptOps::kAllowSaves},
{"NO_UPDATE", ProtoOp::kProtoOpScript, ScriptOps::kNoUpdate},
{"no_clear", ProtoOp::kProtoOpScript, ScriptOps::kNoClear},
{"#M\"", ProtoOp::kProtoOpScript, ScriptOps::kM},
{"#EM", ProtoOp::kProtoOpScript, ScriptOps::kEM},
{"e\"", ProtoOp::kProtoOpScript, ScriptOps::kE},
{"se\"", ProtoOp::kProtoOpScript, ScriptOps::kSE},
{".\"", ProtoOp::kProtoOpScript, ScriptOps::kDot},
{"s.\"", ProtoOp::kProtoOpScript, ScriptOps::kSDot},
{"say1", ProtoOp::kProtoOpScript, ScriptOps::kSay1_AD2044},
{"say2", ProtoOp::kProtoOpScript, ScriptOps::kSay2_AD2044},
{"say1rnd", ProtoOp::kProtoOpScript, ScriptOps::kSay1Rnd},
{"sound", ProtoOp::kProtoOpScript, ScriptOps::kSound},
{"isound", ProtoOp::kProtoOpScript, ScriptOps::kISound},
{"usound", ProtoOp::kProtoOpScript, ScriptOps::kUSound},
{"r@", ProtoOp::kProtoOpScript, ScriptOps::kRGet},
};
static ScriptNamedInstruction g_reahNamedInstructions[] = {
{"rotate", ProtoOp::kProtoOpScript, ScriptOps::kRotate},
{"angle", ProtoOp::kProtoOpScript, ScriptOps::kAngle},
{"angleG@", ProtoOp::kProtoOpScript, ScriptOps::kAngleGGet},
{"speed", ProtoOp::kProtoOpScript, ScriptOps::kSpeed},
{"sanimL", ProtoOp::kProtoOpScript, ScriptOps::kSAnimL},
{"changeL", ProtoOp::kProtoOpScript, ScriptOps::kChangeL},
{"changeL1", ProtoOp::kProtoOpScript, ScriptOps::kChangeL}, // This seems wrong, but not sure what changeL1 does differently from changeL yet
{"animF", ProtoOp::kProtoOpScript, ScriptOps::kAnimF},
{"animG", ProtoOp::kProtoOpScript, ScriptOps::kAnimG},
{"animN", ProtoOp::kProtoOpScript, ScriptOps::kAnimN},
{"animR", ProtoOp::kProtoOpScript, ScriptOps::kAnimR},
{"animS", ProtoOp::kProtoOpScript, ScriptOps::kAnimS},
{"anim", ProtoOp::kProtoOpScript, ScriptOps::kAnim},
{"static", ProtoOp::kProtoOpScript, ScriptOps::kStatic},
{"yes@", ProtoOp::kProtoOpScript, ScriptOps::kVarLoad},
{"yes!", ProtoOp::kProtoOpScript, ScriptOps::kVarStore},
{"yesg@", ProtoOp::kProtoOpScript, ScriptOps::kVarGlobalLoad},
{"yesg!", ProtoOp::kProtoOpScript, ScriptOps::kVarGlobalStore},
{"setaX+!", ProtoOp::kProtoOpScript, ScriptOps::kVarAddAndStore},
{"cr?", ProtoOp::kProtoOpScript, ScriptOps::kItemCheck},
{"cr!", ProtoOp::kProtoOpScript, ScriptOps::kItemRemove},
{"sr!", ProtoOp::kProtoOpScript, ScriptOps::kItemHighlightSet},
{"r?", ProtoOp::kProtoOpScript, ScriptOps::kItemHaveSpace},
{"r!", ProtoOp::kProtoOpScript, ScriptOps::kItemAdd},
{"clearPocket", ProtoOp::kProtoOpScript, ScriptOps::kItemClear},
{"cursor!", ProtoOp::kProtoOpScript, ScriptOps::kSetCursor},
{"room!", ProtoOp::kProtoOpScript, ScriptOps::kSetRoom},
{"lmb", ProtoOp::kProtoOpScript, ScriptOps::kLMB},
{"lmb1", ProtoOp::kProtoOpScript, ScriptOps::kLMB1},
{"volumeDn2", ProtoOp::kProtoOpScript, ScriptOps::kVolumeDn2},
{"volumeDn3", ProtoOp::kProtoOpScript, ScriptOps::kVolumeDn3},
{"volumeDn4", ProtoOp::kProtoOpScript, ScriptOps::kVolumeDn4},
{"volumeUp3", ProtoOp::kProtoOpScript, ScriptOps::kVolumeUp3},
{"rnd", ProtoOp::kProtoOpScript, ScriptOps::kRandom},
{"drop", ProtoOp::kProtoOpScript, ScriptOps::kDrop},
{"dup", ProtoOp::kProtoOpScript, ScriptOps::kDup},
{"swap", ProtoOp::kProtoOpScript, ScriptOps::kSwap},
{"say1", ProtoOp::kProtoOpScript, ScriptOps::kSay1},
{"say2", ProtoOp::kProtoOpScript, ScriptOps::kSay2},
{"say3", ProtoOp::kProtoOpScript, ScriptOps::kSay3},
{"say3@", ProtoOp::kProtoOpScript, ScriptOps::kSay3Get},
{"setTimer", ProtoOp::kProtoOpScript, ScriptOps::kSetTimer},
{"getTimer", ProtoOp::kProtoOpScript, ScriptOps::kGetTimer},
{"delay", ProtoOp::kProtoOpScript, ScriptOps::kDelay},
{"lo!", ProtoOp::kProtoOpScript, ScriptOps::kLoSet},
{"lo@", ProtoOp::kProtoOpScript, ScriptOps::kLoGet},
{"hi!", ProtoOp::kProtoOpScript, ScriptOps::kHiSet},
{"hi@", ProtoOp::kProtoOpScript, ScriptOps::kHiGet},
{"and", ProtoOp::kProtoOpScript, ScriptOps::kAnd},
{"or", ProtoOp::kProtoOpScript, ScriptOps::kOr},
{"+", ProtoOp::kProtoOpScript, ScriptOps::kAdd},
{"-", ProtoOp::kProtoOpScript, ScriptOps::kSub},
{"not", ProtoOp::kProtoOpScript, ScriptOps::kNot},
{"minus", ProtoOp::kProtoOpScript, ScriptOps::kNegate},
{"=", ProtoOp::kProtoOpScript, ScriptOps::kCmpEq},
{">", ProtoOp::kProtoOpScript, ScriptOps::kCmpGt},
{"<", ProtoOp::kProtoOpScript, ScriptOps::kCmpLt},
{"bit@", ProtoOp::kProtoOpScript, ScriptOps::kBitLoad},
{"bit0!", ProtoOp::kProtoOpScript, ScriptOps::kBitSet0},
{"bit1!", ProtoOp::kProtoOpScript, ScriptOps::kBitSet1},
{"soundS1", ProtoOp::kProtoOpScript, ScriptOps::kSoundS1},
{"soundS2", ProtoOp::kProtoOpScript, ScriptOps::kSoundS2},
{"soundS3", ProtoOp::kProtoOpScript, ScriptOps::kSoundS3},
{"soundL1", ProtoOp::kProtoOpScript, ScriptOps::kSoundL1},
{"soundL2", ProtoOp::kProtoOpScript, ScriptOps::kSoundL2},
{"soundL3", ProtoOp::kProtoOpScript, ScriptOps::kSoundL3},
{"3DsoundS2", ProtoOp::kProtoOpScript, ScriptOps::k3DSoundS2},
{"3DsoundL2", ProtoOp::kProtoOpScript, ScriptOps::k3DSoundL2},
{"3DsoundL3", ProtoOp::kProtoOpScript, ScriptOps::k3DSoundL3},
{"stopaL", ProtoOp::kProtoOpScript, ScriptOps::kStopAL},
{"range", ProtoOp::kProtoOpScript, ScriptOps::kRange},
{"addXsound", ProtoOp::kProtoOpScript, ScriptOps::kAddXSound},
{"clrXsound", ProtoOp::kProtoOpScript, ScriptOps::kClrXSound},
{"stopSndLA", ProtoOp::kProtoOpScript, ScriptOps::kStopSndLA},
{"stopSndLO", ProtoOp::kProtoOpScript, ScriptOps::kStopSndLO},
{"music", ProtoOp::kProtoOpScript, ScriptOps::kMusic},
{"musicUp", ProtoOp::kProtoOpScript, ScriptOps::kMusicVolRamp},
{"musicDn", ProtoOp::kProtoOpScript, ScriptOps::kMusicVolRamp},
{"parm0", ProtoOp::kProtoOpScript, ScriptOps::kParm0},
{"parm1", ProtoOp::kProtoOpScript, ScriptOps::kParm1},
{"parm2", ProtoOp::kProtoOpScript, ScriptOps::kParm2},
{"parm3", ProtoOp::kProtoOpScript, ScriptOps::kParm3},
{"parmG", ProtoOp::kProtoOpScript, ScriptOps::kParmG},
{"sparmX", ProtoOp::kProtoOpScript, ScriptOps::kSParmX},
{"sanimX", ProtoOp::kProtoOpScript, ScriptOps::kSAnimX},
{"disc1", ProtoOp::kProtoOpScript, ScriptOps::kDisc1},
{"disc2", ProtoOp::kProtoOpScript, ScriptOps::kDisc2},
{"disc3", ProtoOp::kProtoOpScript, ScriptOps::kDisc3},
{"goto", ProtoOp::kProtoOpScript, ScriptOps::kGoto},
{"#if", ProtoOp::kProtoOpIf, ScriptOps::kInvalid},
{"#eif", ProtoOp::kProtoOpEndIf, ScriptOps::kInvalid},
{"#else", ProtoOp::kProtoOpElse, ScriptOps::kInvalid},
{"#switch:", ProtoOp::kProtoOpSwitch, ScriptOps::kInvalid},
{"#eswitch", ProtoOp::kProtoOpEndSwitch, ScriptOps::kInvalid},
{"break", ProtoOp::kProtoOpBreak, ScriptOps::kInvalid},
{"#default", ProtoOp::kProtoOpDefault, ScriptOps::kInvalid},
{"esc_on", ProtoOp::kProtoOpScript, ScriptOps::kEscOn},
{"esc_off", ProtoOp::kProtoOpScript, ScriptOps::kEscOff},
{"esc_get@", ProtoOp::kProtoOpScript, ScriptOps::kEscGet},
{"backStart", ProtoOp::kProtoOpScript, ScriptOps::kBackStart},
{"saveAs", ProtoOp::kProtoOpScript, ScriptOps::kSaveAs},
{"save0", ProtoOp::kProtoOpNoop, ScriptOps::kSave0},
{"exit", ProtoOp::kProtoOpScript, ScriptOps::kExit},
{"allowedSave", ProtoOp::kProtoOpScript, ScriptOps::kAllowSaves},
};
static ScriptNamedInstruction g_schizmNamedInstructions[] = {
{"StopScore", ProtoOp::kProtoOpScript, ScriptOps::kMusicStop},
{"PlayScore", ProtoOp::kProtoOpScript, ScriptOps::kMusicPlayScore},
{"ScoreAlways", ProtoOp::kProtoOpScript, ScriptOps::kScoreAlways},
{"ScoreNormal", ProtoOp::kProtoOpScript, ScriptOps::kScoreNormal},
{"SndAddRandom", ProtoOp::kProtoOpScript, ScriptOps::kSndAddRandom},
{"SndClearRandom", ProtoOp::kProtoOpScript, ScriptOps::kSndClearRandom},
{"SndPlay", ProtoOp::kProtoOpScript, ScriptOps::kSndPlay},
{"SndPlayEx", ProtoOp::kProtoOpScript, ScriptOps::kSndPlayEx},
{"SndPlay3D", ProtoOp::kProtoOpScript, ScriptOps::kSndPlay3D},
{"SndPlaying", ProtoOp::kProtoOpScript, ScriptOps::kSndPlaying},
{"SndHalt", ProtoOp::kProtoOpScript, ScriptOps::kSndHalt},
{"SndWait", ProtoOp::kProtoOpScript, ScriptOps::kSndWait},
{"SndToBack", ProtoOp::kProtoOpScript, ScriptOps::kSndToBack},
{"SndStop", ProtoOp::kProtoOpScript, ScriptOps::kSndStop},
{"SndStopAll", ProtoOp::kProtoOpScript, ScriptOps::kSndStopAll},
{"VolumeAdd", ProtoOp::kProtoOpScript, ScriptOps::kVolumeAdd},
{"VolumeChange", ProtoOp::kProtoOpScript, ScriptOps::kVolumeChange},
{"VolumeDown", ProtoOp::kProtoOpScript, ScriptOps::kVolumeDn2},
{"esc_on", ProtoOp::kProtoOpScript, ScriptOps::kEscOn},
{"esc_off", ProtoOp::kProtoOpScript, ScriptOps::kEscOff},
{"esc_get@", ProtoOp::kProtoOpScript, ScriptOps::kEscGet},
{"room!", ProtoOp::kProtoOpScript, ScriptOps::kSetRoom},
{"room@", ProtoOp::kProtoOpScript, ScriptOps::kGetRoom},
{"lmb", ProtoOp::kProtoOpScript, ScriptOps::kLMB},
{"lmb1", ProtoOp::kProtoOpScript, ScriptOps::kLMB1},
{"animVolume", ProtoOp::kProtoOpScript, ScriptOps::kAnimVolume},
{"animChange", ProtoOp::kProtoOpScript, ScriptOps::kAnimChange},
{"anim", ProtoOp::kProtoOpScript, ScriptOps::kAnim}, // Dialect difference: Accepts room name
{"static", ProtoOp::kProtoOpScript, ScriptOps::kStatic},
{"animF", ProtoOp::kProtoOpScript, ScriptOps::kAnimF},
{"animG", ProtoOp::kProtoOpScript, ScriptOps::kAnimG},
{"animN", ProtoOp::kProtoOpScript, ScriptOps::kAnimN},
{"animR", ProtoOp::kProtoOpScript, ScriptOps::kAnimR},
{"animS", ProtoOp::kProtoOpScript, ScriptOps::kAnimS},
{"sanimL", ProtoOp::kProtoOpScript, ScriptOps::kSAnimL},
{"sparmX", ProtoOp::kProtoOpScript, ScriptOps::kSParmX},
{"sanimX", ProtoOp::kProtoOpScript, ScriptOps::kSAnimX},
{"byte@", ProtoOp::kProtoOpScript, ScriptOps::kExtractByte},
{"byte!", ProtoOp::kProtoOpScript, ScriptOps::kInsertByte},
{"rotate", ProtoOp::kProtoOpScript, ScriptOps::kRotate},
{"rotateUpdate", ProtoOp::kProtoOpScript, ScriptOps::kRotateUpdate},
{"bit@", ProtoOp::kProtoOpScript, ScriptOps::kBitLoad},
{"bit0!", ProtoOp::kProtoOpScript, ScriptOps::kBitSet0},
{"bit1!", ProtoOp::kProtoOpScript, ScriptOps::kBitSet1},
{"speech", ProtoOp::kProtoOpScript, ScriptOps::kSpeech},
{"speechEx", ProtoOp::kProtoOpScript, ScriptOps::kSpeechEx},
{"speechTest", ProtoOp::kProtoOpScript, ScriptOps::kSpeechTest},
{"say", ProtoOp::kProtoOpScript, ScriptOps::kSay},
{"changeL", ProtoOp::kProtoOpScript, ScriptOps::kChangeL}, // Dialect difference: Accepts room name
{"range", ProtoOp::kProtoOpScript, ScriptOps::kRange},
{"sound3DL2", ProtoOp::kProtoOpScript, ScriptOps::k3DSoundL2}, // Dialect difference: Different name
{"random", ProtoOp::kProtoOpScript, ScriptOps::kRandomInclusive},
{"heroSetPos", ProtoOp::kProtoOpScript, ScriptOps::kHeroSetPos},
{"heroGetPos", ProtoOp::kProtoOpScript, ScriptOps::kHeroGetPos},
{"heroOut", ProtoOp::kProtoOpScript, ScriptOps::kHeroOut},
{"hero@", ProtoOp::kProtoOpScript, ScriptOps::kHeroGet},
{"ret", ProtoOp::kProtoOpScript, ScriptOps::kReturn},
{"setTimer", ProtoOp::kProtoOpScript, ScriptOps::kSetTimer},
{"getTimer", ProtoOp::kProtoOpScript, ScriptOps::kGetTimer},
{"delay", ProtoOp::kProtoOpScript, ScriptOps::kDelay},
{"lo!", ProtoOp::kProtoOpScript, ScriptOps::kLoSet},
{"lo@", ProtoOp::kProtoOpScript, ScriptOps::kLoGet},
{"hi!", ProtoOp::kProtoOpScript, ScriptOps::kHiSet},
{"hi@", ProtoOp::kProtoOpScript, ScriptOps::kHiGet},
{"angle@", ProtoOp::kProtoOpScript, ScriptOps::kAngleGet},
{"angleG@", ProtoOp::kProtoOpScript, ScriptOps::kAngleGGet},
{"cd@", ProtoOp::kProtoOpScript, ScriptOps::kIsCDVersion},
{"dvd@", ProtoOp::kProtoOpScript, ScriptOps::kIsDVDVersion},
{"disc", ProtoOp::kProtoOpScript, ScriptOps::kDisc},
{"save0", ProtoOp::kProtoOpNoop, ScriptOps::kSave0},
{"hidePanel", ProtoOp::kProtoOpScript, ScriptOps::kHidePanel},
{"ItemExist@", ProtoOp::kProtoOpScript, ScriptOps::kItemCheck},
{"ItemSelect!", ProtoOp::kProtoOpScript, ScriptOps::kItemHighlightSetTrue},
{"ItemPlace@", ProtoOp::kProtoOpScript, ScriptOps::kItemHaveSpace},
{"ItemPutInto!", ProtoOp::kProtoOpScript, ScriptOps::kItemAdd},
{"ItemRemove!", ProtoOp::kProtoOpScript, ScriptOps::kItemRemove},
{"cyfra@", ProtoOp::kProtoOpScript, ScriptOps::kGetDigit}, // Cyfra = digit
{"puzzleInit", ProtoOp::kProtoOpScript, ScriptOps::kPuzzleInit},
{"puzzleCanPress", ProtoOp::kProtoOpScript, ScriptOps::kPuzzleCanPress},
{"puzzleDoMove1", ProtoOp::kProtoOpScript, ScriptOps::kPuzzleDoMove1},
{"puzzleDoMove2", ProtoOp::kProtoOpScript, ScriptOps::kPuzzleDoMove2},
{"puzzleDone", ProtoOp::kProtoOpScript, ScriptOps::kPuzzleDone},
{"puzzleWhoWon", ProtoOp::kProtoOpScript, ScriptOps::kPuzzleWhoWon},
{"fn", ProtoOp::kProtoOpScript, ScriptOps::kFn},
{"parm1", ProtoOp::kProtoOpScript, ScriptOps::kParm1},
{"parm2", ProtoOp::kProtoOpScript, ScriptOps::kParm2},
{"parm3", ProtoOp::kProtoOpScript, ScriptOps::kParm3},
{"parmG", ProtoOp::kProtoOpScript, ScriptOps::kParmG},
{"+", ProtoOp::kProtoOpScript, ScriptOps::kAdd},
{"-", ProtoOp::kProtoOpScript, ScriptOps::kSub},
{"*", ProtoOp::kProtoOpScript, ScriptOps::kMul},
{"/", ProtoOp::kProtoOpScript, ScriptOps::kDiv},
{"%", ProtoOp::kProtoOpScript, ScriptOps::kMod},
{"&&", ProtoOp::kProtoOpScript, ScriptOps::kAnd},
{"or", ProtoOp::kProtoOpScript, ScriptOps::kOr},
{"||", ProtoOp::kProtoOpScript, ScriptOps::kOr},
{"+", ProtoOp::kProtoOpScript, ScriptOps::kAdd},
{"-", ProtoOp::kProtoOpScript, ScriptOps::kSub},
{">", ProtoOp::kProtoOpScript, ScriptOps::kCmpGt},
{"<", ProtoOp::kProtoOpScript, ScriptOps::kCmpLt},
{"=", ProtoOp::kProtoOpScript, ScriptOps::kCmpEq},
{"==", ProtoOp::kProtoOpScript, ScriptOps::kCmpEq},
{"!=", ProtoOp::kProtoOpScript, ScriptOps::kCmpNE},
{">=", ProtoOp::kProtoOpScript, ScriptOps::kCmpGE},
{"<=", ProtoOp::kProtoOpScript, ScriptOps::kCmpLE},
{"&", ProtoOp::kProtoOpScript, ScriptOps::kBitAnd},
{"|", ProtoOp::kProtoOpScript, ScriptOps::kBitOr},
{"#if", ProtoOp::kProtoOpIf, ScriptOps::kInvalid},
{"#eif", ProtoOp::kProtoOpEndIf, ScriptOps::kInvalid},
{"#else", ProtoOp::kProtoOpElse, ScriptOps::kInvalid},
{"#switch:", ProtoOp::kProtoOpSwitch, ScriptOps::kInvalid},
{"#eswitch", ProtoOp::kProtoOpEndSwitch, ScriptOps::kInvalid},
{"break", ProtoOp::kProtoOpBreak, ScriptOps::kInvalid},
{"ret", ProtoOp::kProtoOpScript, ScriptOps::kReturn},
{"backStart", ProtoOp::kProtoOpScript, ScriptOps::kBackStart},
{"allowedSave", ProtoOp::kProtoOpScript, ScriptOps::kAllowSaves},
};
bool ScriptCompiler::compileInstructionToken(ProtoScript &script, const Common::String &token) {
if (_dialect == kScriptDialectSchizm) {
uint roomNumber = 0;
int32 value = 0;
if (_gs->getDefine(token, roomNumber, value)) {
script.instrs.push_back(ProtoInstruction(ScriptOps::kNumber, value));
return true;
}
}
if (_dialect == kScriptDialectSchizm && token.hasPrefix("-")) {
uint32 number = 0;
if (parseNumber(token.substr(1), number, false)) {
script.instrs.push_back(ProtoInstruction(ScriptOps::kNumber, -static_cast<int32>(number)));
return true;
}
}
uint32 number = 0;
if (parseNumber(token, number, false)) {
script.instrs.push_back(ProtoInstruction(ScriptOps::kNumber, number));
return true;
}
if (token.size() >= 1 && token[0] == ':') {
if (_dialect == kScriptDialectReah) {
if (token.size() >= 3 && token[2] == ':') {
if (token[1] == 'Y') {
script.instrs.push_back(ProtoInstruction(ScriptOps::kVarName, indexString(token.substr(3))));
return true;
} else if (token[1] == 'V') {
script.instrs.push_back(ProtoInstruction(ScriptOps::kValueName, indexString(token.substr(3))));
return true;
} else
return false;
}
}
script.instrs.push_back(ProtoInstruction(ScriptOps::kAnimName, indexString(token.substr(1))));
return true;
}
if (token.size() >= 2 && token[0] == '_') {
script.instrs.push_back(ProtoInstruction(ScriptOps::kSoundName, indexString(token.substr(1))));
return true;
}
if (_dialect == kScriptDialectReah || _dialect == kScriptDialectAD2044) {
if (token.hasPrefix("CUR_")) {
script.instrs.push_back(ProtoInstruction(ScriptOps::kCursorName, indexString(token)));
return true;
}
}
if (token == "#switch") {
_parser.expect(":", _blamePath);
script.instrs.push_back(ProtoInstruction(kProtoOpSwitch, ScriptOps::kInvalid, 0));
return true;
}
if (token == "#case") {
uint32 caseNumber = 0;
_parser.expect(":", _blamePath);
expectNumber(caseNumber, false);
script.instrs.push_back(ProtoInstruction(kProtoOpCase, ScriptOps::kInvalid, caseNumber));
return true;
}
if (token == "#case:") {
uint32 caseNumber = 0;
expectNumber(caseNumber, false);
script.instrs.push_back(ProtoInstruction(kProtoOpCase, ScriptOps::kInvalid, caseNumber));
return true;
}
if (_dialect == kScriptDialectAD2044) {
for (const ScriptNamedInstruction &namedInstr : g_ad2044NamedInstructions) {
if (token == namedInstr.str) {
int32 paramArg = 0;
if (token.hasSuffix("\"")) {
TextParserState state;
char c = 0;
if (!_parser.skipWhitespaceAndComments(c, state))
error("Error compiling script at line %i col %i: Unterminated string", static_cast<int>(state._lineNum), static_cast<int>(state._col));
Common::String str;
for (;;) {
if (c == '\"')
break;
str += c;
if (!_parser.readOneChar(c, state)) {
error("Error compiling script at line %i col %i: Unterminated string", static_cast<int>(state._lineNum), static_cast<int>(state._col));
}
}
paramArg = indexString(str);
}
script.instrs.push_back(ProtoInstruction(namedInstr.protoOp, namedInstr.op, paramArg));
return true;
}
}
} else if (_dialect == kScriptDialectReah) {
for (const ScriptNamedInstruction &namedInstr : g_reahNamedInstructions) {
if (token == namedInstr.str) {
script.instrs.push_back(ProtoInstruction(namedInstr.protoOp, namedInstr.op, 0));
return true;
}
}
} else if (_dialect == kScriptDialectSchizm) {
if (token.size() >= 2 && token[0] == '\"' && token[token.size() - 1] == '\"') {
// Seems like these are only used for sounds and music?
uint fnIndex = indexString(token.substr(1, token.size() - 2));
script.instrs.push_back(ProtoInstruction(kProtoOpScript, ScriptOps::kString, fnIndex));
return true;
}
for (const ScriptNamedInstruction &namedInstr : g_schizmNamedInstructions) {
if (token == namedInstr.str) {
script.instrs.push_back(ProtoInstruction(namedInstr.protoOp, namedInstr.op, 0));
return true;
}
}
if (token.hasPrefix("fn")) {
uint fnIndex = _gs->getFunctionIndex(token);
script.instrs.push_back(ProtoInstruction(kProtoOpScript, ScriptOps::kCallFunction, fnIndex));
return true;
}
if (token.size() >= 2 && token.hasSuffix("!")) {
uint roomNumber = 0;
int32 varNumber = 0;
if (!_gs->getDefine(token.substr(0, token.size() - 1), roomNumber, varNumber) || varNumber < 0)
return false;
script.instrs.push_back(ProtoInstruction(kProtoOpScript, ScriptOps::kNumber, (roomNumber << 16) + static_cast<uint>(varNumber)));
script.instrs.push_back(ProtoInstruction(kProtoOpScript, ScriptOps::kVarGlobalStore, 0));
return true;
}
// HACK: Work around bugged variable name in Room02.log
if (token == "dwFirst\x8c@") {
script.instrs.push_back(ProtoInstruction(kProtoOpScript, ScriptOps::kNumber, 0));
return true;
}
if (token.size() >= 2 && token.hasSuffix("@")) {
uint roomNumber = 0;
int32 varNumber = 0;
if (!_gs->getDefine(token.substr(0, token.size() - 1), roomNumber, varNumber) || varNumber < 0)
return false;
script.instrs.push_back(ProtoInstruction(kProtoOpScript, ScriptOps::kNumber, (roomNumber << 16) + static_cast<uint>(varNumber)));
script.instrs.push_back(ProtoInstruction(kProtoOpScript, ScriptOps::kVarGlobalLoad, 0));
return true;
}
// Does this look like a screen name?
bool couldBeScreenName = true;
for (uint i = 0; i < token.size(); i++) {
char c = token[i];
bool isAlphaNumeric = ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'));
if (!isAlphaNumeric) {
couldBeScreenName = false;
break;
}
}
if (couldBeScreenName) {
script.instrs.push_back(ProtoInstruction(kProtoOpScript, ScriptOps::kScreenName, indexString(token)));
return true;
}
if (token.hasPrefix("cur")) {
script.instrs.push_back(ProtoInstruction(ScriptOps::kCursorName, indexString(token)));
script.instrs.push_back(ProtoInstruction(ScriptOps::kSetCursor, 0));
return true;
}
// Disabled since Room02 is a cheat room and so not needed.
#if 0
// HACK: Work around broken volume variable names in Room02. Some of these appear to have "par"
// where it should be "vol" but some are garbage. Figure this out later.
if (token.hasPrefix("par")) {
script.instrs.push_back(ProtoInstruction(kProtoOpScript, ScriptOps::kGarbage, indexString(token)));
return true;
}
#endif
}
return false;
}
enum CodeGenFlowControlBlockType {
kFlowControlInvalid,
kFlowControlIf,
kFlowControlSwitch,
};
struct CodeGenControlFlowBlock {
CodeGenControlFlowBlock();
CodeGenFlowControlBlockType type;
uint index;
};
CodeGenControlFlowBlock::CodeGenControlFlowBlock() : type(kFlowControlInvalid), index(0) {
}
struct CodeGenSwitchCase {
CodeGenSwitchCase();
int32 value;
uint label;
};
CodeGenSwitchCase::CodeGenSwitchCase() : value(0), label(0) {
}
struct CodeGenSwitch {
CodeGenSwitch();
Common::Array<CodeGenSwitchCase> cases;
uint endLabel;
uint defaultLabel;
bool hasDefault;
};
CodeGenSwitch::CodeGenSwitch() : defaultLabel(0), endLabel(0), hasDefault(false) {
}
struct CodeGenIf {
CodeGenIf();
uint endLabel;
uint elseLabel;
bool hasElse;
};
CodeGenIf::CodeGenIf() : endLabel(0), elseLabel(0), hasElse(false) {
}
void ScriptCompiler::codeGenScript(ProtoScript &protoScript, Script &script) {
Common::Array<ProtoInstruction> instrs;
Common::Array<CodeGenSwitch> switches;
Common::Array<CodeGenIf> ifs;
Common::Array<CodeGenControlFlowBlock> controlFlowStack;
int32 nextLabel = 0;
// Pass 1: Collect flow control constructs, make all flow control constructs point to the index of the construct,
// replace Else, Case, EndIf, EndSwitch, and Default instructions with Label and JumpToLabel. Clear noops.
for (const ProtoInstruction &instr : protoScript.instrs) {
switch (instr.protoOp) {
case kProtoOpScript:
instrs.push_back(instr);
break;
case kProtoOpBreak: {
bool found = false;
for (uint ri = 0; ri < controlFlowStack.size(); ri++) {
const CodeGenControlFlowBlock &cf = controlFlowStack[controlFlowStack.size() - 1 - ri];
if (cf.type == kFlowControlSwitch) {
found = true;
instrs.push_back(ProtoInstruction(kProtoOpBreak, ScriptOps::kInvalid, cf.index));
break;
}
}
if (!found)
error("Error in codegen: break statement outside of a switch case");
} break;
case kProtoOpIf: {
CodeGenControlFlowBlock cf;
cf.type = kFlowControlIf;
cf.index = ifs.size();
controlFlowStack.push_back(cf);
CodeGenIf ifBlock;
ifBlock.endLabel = nextLabel++;
ifs.push_back(ifBlock);
instrs.push_back(ProtoInstruction(kProtoOpIf, ScriptOps::kInvalid, cf.index));
} break;
case kProtoOpElse: {
if (controlFlowStack.size() == 0)
error("Error in codegen: #else outside of control flow");
CodeGenControlFlowBlock &cf = controlFlowStack[controlFlowStack.size() - 1];
if (cf.type != kFlowControlIf)
error("Error in codegen: #else inside wrong control block type");
CodeGenIf &ifBlock = ifs[cf.index];
if (ifBlock.hasElse)
error("Error in codegen: #else already set for #if block");
ifBlock.hasElse = true;
ifBlock.elseLabel = nextLabel++;
instrs.push_back(ProtoInstruction(kProtoOpJumpToLabel, ScriptOps::kInvalid, ifBlock.endLabel));
instrs.push_back(ProtoInstruction(kProtoOpLabel, ScriptOps::kInvalid, ifBlock.elseLabel));
} break;
case kProtoOpEndIf: {
if (controlFlowStack.size() == 0)
error("Error in codegen: #eif outside of control flow");
CodeGenControlFlowBlock &cf = controlFlowStack[controlFlowStack.size() - 1];
if (cf.type != kFlowControlIf)
error("Error in codegen: #eif inside wrong control block type");
const CodeGenIf &ifBlock = ifs[cf.index];
instrs.push_back(ProtoInstruction(kProtoOpLabel, ScriptOps::kInvalid, ifBlock.endLabel));
controlFlowStack.pop_back();
} break;
case kProtoOpSwitch: {
CodeGenControlFlowBlock cf;
cf.type = kFlowControlSwitch;
cf.index = switches.size();
controlFlowStack.push_back(cf);
CodeGenSwitch switchBlock;
switchBlock.endLabel = nextLabel++;
switches.push_back(switchBlock);
instrs.push_back(ProtoInstruction(kProtoOpSwitch, ScriptOps::kInvalid, cf.index));
} break;
case kProtoOpCase: {
if (controlFlowStack.size() == 0)
error("Error in codegen: #case outside of control flow");
CodeGenControlFlowBlock &cf = controlFlowStack[controlFlowStack.size() - 1];
if (cf.type != kFlowControlSwitch)
error("Error in codegen: #case inside wrong control block type");
CodeGenSwitch &switchBlock = switches[cf.index];
CodeGenSwitchCase caseDef;
caseDef.label = nextLabel++;
caseDef.value = instr.arg;
switchBlock.cases.push_back(caseDef);
instrs.push_back(ProtoInstruction(kProtoOpLabel, ScriptOps::kInvalid, caseDef.label));
} break;
case kProtoOpDefault: {
if (controlFlowStack.size() == 0)
error("Error in codegen: #case outside of control flow");
CodeGenControlFlowBlock &cf = controlFlowStack[controlFlowStack.size() - 1];
if (cf.type != kFlowControlSwitch)
error("Error in codegen: #case inside wrong control block type");
CodeGenSwitch &switchBlock = switches[cf.index];
if (switchBlock.hasDefault)
error("Error in codegen: #switch already has a default");
switchBlock.hasDefault = true;
switchBlock.defaultLabel = nextLabel++;
instrs.push_back(ProtoInstruction(kProtoOpLabel, ScriptOps::kInvalid, switchBlock.defaultLabel));
} break;
case kProtoOpEndSwitch: {
if (controlFlowStack.size() == 0)
error("Error in codegen: #eswitch outside of control flow");
CodeGenControlFlowBlock &cf = controlFlowStack[controlFlowStack.size() - 1];
if (cf.type != kFlowControlSwitch)
error("Error in codegen: #eswitch inside wrong control block type");
const CodeGenSwitch &switchBlock = switches[cf.index];
instrs.push_back(ProtoInstruction(kProtoOpLabel, ScriptOps::kInvalid, switchBlock.endLabel));
controlFlowStack.pop_back();
} break;
case kProtoOpNoop:
break;
default:
error("Internal error: Unhandled proto-op");
break;
}
}
if (controlFlowStack.size() > 0) {
if (_dialect == kScriptDialectSchizm) {
// For some reason line 105 in Room36 and line 342 in Room61 have unterminated conditional blocks,
// and the comments say that's intentional.
while (controlFlowStack.size() > 0) {
const CodeGenControlFlowBlock &cf = controlFlowStack.back();
switch (controlFlowStack.back().type) {
case kFlowControlIf: {
warning("CodeGen encountered unterminated #if statement");
instrs.push_back(ProtoInstruction(kProtoOpLabel, ScriptOps::kInvalid, ifs[cf.index].endLabel));
controlFlowStack.pop_back();
} break;
case kFlowControlSwitch: {
warning("CodeGen encountered unterminated #switch statement");
instrs.push_back(ProtoInstruction(kProtoOpLabel, ScriptOps::kInvalid, switches[cf.index].endLabel));
controlFlowStack.pop_back();
} break;
default:
error("Unknown control flow type");
}
}
} else {
error("Error in codegen: Unterminated flow control construct");
}
}
Common::Array<ProtoInstruction> instrs2;
Common::HashMap<uint, uint> labelToInstr;
// Pass 2: Unroll If and Switch instructions into CheckValue and JumpToLabel ops, resolve label locations
for (const ProtoInstruction &instr : instrs) {
switch (instr.protoOp) {
case kProtoOpScript:
case kProtoOpJumpToLabel:
instrs2.push_back(instr);
break;
case kProtoOpIf: {
const CodeGenIf &ifBlock = ifs[instr.arg];
instrs2.push_back(ProtoInstruction(ScriptOps::kCheckValue, 0));
instrs2.push_back(ProtoInstruction(kProtoOpJumpToLabel, ScriptOps::kInvalid, ifBlock.hasElse ? ifBlock.elseLabel : ifBlock.endLabel));
instrs2.push_back(ProtoInstruction(ScriptOps::kDrop));
} break;
case kProtoOpSwitch: {
const CodeGenSwitch &switchBlock = switches[instr.arg];
for (const CodeGenSwitchCase &caseDef : switchBlock.cases) {
instrs2.push_back(ProtoInstruction(ScriptOps::kCheckValue, caseDef.value));
instrs2.push_back(ProtoInstruction(kProtoOpJumpToLabel, ScriptOps::kInvalid, caseDef.label));
}
instrs2.push_back(ProtoInstruction(ScriptOps::kDrop));
instrs2.push_back(ProtoInstruction(kProtoOpJumpToLabel, ScriptOps::kInvalid, switchBlock.hasDefault ? switchBlock.defaultLabel : switchBlock.endLabel));
} break;
case kProtoOpLabel:
labelToInstr[static_cast<uint>(instr.arg)] = instrs2.size();
break;
case kProtoOpBreak: {
const CodeGenSwitch &switchBlock = switches[instr.arg];
instrs2.push_back(ProtoInstruction(kProtoOpJumpToLabel, ScriptOps::kInvalid, switchBlock.endLabel));
} break;
default:
error("Internal error: Unhandled proto-op");
break;
}
}
instrs.clear();
// Pass 3: Change all JumpToLabel ops to Jump ops and write out final instructions
script.instrs.reserve(instrs2.size());
for (const ProtoInstruction &instr : instrs2) {
switch (instr.protoOp) {
case kProtoOpScript:
script.instrs.push_back(Instruction(instr.op, instr.arg));
break;
case kProtoOpJumpToLabel: {
Common::HashMap<uint, uint>::const_iterator it = labelToInstr.find(static_cast<uint>(instr.arg));
if (it == labelToInstr.end())
error("Internal error: Unmatched label");
script.instrs.push_back(Instruction(ScriptOps::kJump, it->_value));
} break;
default:
error("Internal error: Unhandled proto-op");
break;
}
}
}
uint ScriptCompiler::indexString(const Common::String &str) {
Common::HashMap<Common::String, uint>::const_iterator it = _stringToIndex.find(str);
if (it == _stringToIndex.end()) {
uint index = _stringToIndex.size();
_stringToIndex[str] = index;
return index;
}
return it->_value;
}
ScriptCompilerGlobalState::Def::Def() : _value(0), _roomNumber(0) {
}
void ScriptCompilerGlobalState::define(const Common::String &key, uint roomNumber, int32 value) {
Def &def = _defs[key];
def._roomNumber = roomNumber;
def._value = value;
}
bool ScriptCompilerGlobalState::getDefine(const Common::String &str, uint &outRoomNumber, int32 &outValue) const {
Common::HashMap<Common::String, Def>::const_iterator it = _defs.find(str);
if (it == _defs.end())
return false;
outRoomNumber = it->_value._roomNumber;
outValue = it->_value._value;
return true;
}
uint ScriptCompilerGlobalState::getFunctionIndex(const Common::String &fnName) {
Common::HashMap<Common::String, uint>::const_iterator it = _functionNameToIndex.find(fnName);
assert(fnName != "fn");
if (it == _functionNameToIndex.end()) {
uint newIndex = _functionNameToIndex.size();
_functionNameToIndex.setVal(fnName, newIndex);
_functions.push_back(nullptr);
return newIndex;
} else
return it->_value;
}
void ScriptCompilerGlobalState::setFunction(uint fnIndex, const Common::SharedPtr<Script> &fn) {
_functions[fnIndex] = fn;
}
uint ScriptCompilerGlobalState::getNumFunctions() const {
return _functionNameToIndex.size();
}
void ScriptCompilerGlobalState::dumpFunctionNames(Common::Array<Common::String> &fnNames) const {
fnNames.clear();
fnNames.resize(_functionNameToIndex.size());
for (const Common::HashMap<Common::String, uint>::Node &node : _functionNameToIndex)
fnNames[node._value] = node._key;
}
Common::SharedPtr<Script> ScriptCompilerGlobalState::getFunction(uint fnIndex) const {
return _functions[fnIndex];
}
ScriptSet::ScriptSet() {
}
IScriptCompilerGlobalState::~IScriptCompilerGlobalState() {
}
static void compileLogicFile(ScriptSet &scriptSet, Common::ReadStream &stream, uint streamSize, const Common::Path &blamePath, ScriptDialect dialect, uint loadAsRoom, uint fileRoom, IScriptCompilerGlobalState *gs) {
LogicUnscrambleStream unscrambleStream(&stream, streamSize);
TextParser parser(&unscrambleStream);
ScriptCompiler compiler(parser, blamePath, dialect, loadAsRoom, fileRoom, gs);
compiler.compileScriptSet(&scriptSet);
}
Common::SharedPtr<IScriptCompilerGlobalState> createScriptCompilerGlobalState() {
return Common::SharedPtr<IScriptCompilerGlobalState>(new ScriptCompilerGlobalState());
}
Common::SharedPtr<ScriptSet> compileReahLogicFile(Common::ReadStream &stream, uint streamSize, const Common::Path &blamePath) {
Common::SharedPtr<ScriptSet> scriptSet(new ScriptSet());
compileLogicFile(*scriptSet, stream, streamSize, blamePath, kScriptDialectReah, 0, 0, nullptr);
return scriptSet;
}
Common::SharedPtr<ScriptSet> compileAD2044LogicFile(Common::ReadStream &stream, uint streamSize, const Common::Path &blamePath) {
Common::SharedPtr<ScriptSet> scriptSet(new ScriptSet());
compileLogicFile(*scriptSet, stream, streamSize, blamePath, kScriptDialectAD2044, 0, 0, nullptr);
return scriptSet;
}
void compileSchizmLogicFile(ScriptSet &scriptSet, uint loadAsRoom, uint fileRoom, Common::ReadStream &stream, uint streamSize, const Common::Path &blamePath, IScriptCompilerGlobalState *gs) {
compileLogicFile(scriptSet, stream, streamSize, blamePath, kScriptDialectSchizm, loadAsRoom, fileRoom, gs);
}
bool checkSchizmLogicForDuplicatedRoom(Common::ReadStream &stream, uint streamSize) {
LogicUnscrambleStream unscrambleStream(&stream, streamSize);
TextParser parser(&unscrambleStream);
TextParserState state;
Common::String token;
const char *expectedTokenSequence[] = {"~Room", "0xxh", "~ERoom"};
for (const char *tokenExpected : expectedTokenSequence) {
if (!parser.parseToken(token, state))
return false;
if (token != tokenExpected)
return false;
}
if (parser.parseToken(token, state))
return false;
return true;
}
static bool opArgIsStringIndex(ScriptOps::ScriptOp op) {
switch (op) {
case ScriptOps::kDubbing:
case ScriptOps::kVarName:
case ScriptOps::kValueName:
case ScriptOps::kAnimName:
case ScriptOps::kSoundName:
case ScriptOps::kCursorName:
case ScriptOps::kString:
case ScriptOps::kScreenName:
case ScriptOps::kGarbage:
return true;
default:
return false;
}
}
void optimizeScriptSet(ScriptSet &scriptSet) {
Common::HashMap<uint, uint> functionIndexToUsedFunction;
Common::HashMap<uint, uint> stringIndexToUsedString;
Common::Array<Script *> scriptCheckQueue;
ScreenNameMap_t screenNames;
// Find all screen names.
// There is one duplicate: START exists in room 1 and room 63. In that case, we want to use the latter ID.
for (const RoomScriptSetMap_t::Node &rsNode : scriptSet.roomScripts) {
for (const ScreenNameMap_t::Node &snNode : rsNode._value->screenNames)
screenNames[snNode._key] = snNode._value;
}
for (const RoomScriptSetMap_t::Node &rsNode : scriptSet.roomScripts) {
for (const ScreenScriptSetMap_t::Node &ssNode : rsNode._value->screenScripts) {
if (ssNode._value->entryScript)
scriptCheckQueue.push_back(ssNode._value->entryScript.get());
for (const ScriptMap_t::Node &isNode : ssNode._value->interactionScripts)
scriptCheckQueue.push_back(isNode._value.get());
}
}
// scriptCheckQueue.size() may grow during the loop
for (uint i = 0; i < scriptCheckQueue.size(); i++) {
Script *script = scriptCheckQueue[i];
for (Instruction &instr : script->instrs) {
if (instr.op == ScriptOps::kCallFunction) {
uint funcID = instr.arg;
Common::HashMap<uint, uint>::const_iterator funcIDIt = functionIndexToUsedFunction.find(funcID);
if (funcIDIt == functionIndexToUsedFunction.end()) {
uint newIndex = functionIndexToUsedFunction.size();
functionIndexToUsedFunction[funcID] = newIndex;
scriptCheckQueue.push_back(scriptSet.functions[funcID].get());
instr.arg = newIndex;
} else
instr.arg = funcIDIt->_value;
} else if (instr.op == ScriptOps::kScreenName) {
// In a few places, the screen number being transitioned to isn't valid in the target room.
//
// fnKOLEJKA_PRACOWNIA in room 61 and fnKOLEJKA_CIEMNO in room 62 both execute animF ops after changing room number,
// but reference a screen ID in the current room. They then execute another animF op in the new room, which changes
// to a valid screen number.
//
// The way this appears to work is that screen IDs are applied based on the loaded room script
// instead of the active room. We compensate for this by just resolving the screen name here.
const Common::String &screenNameStr = scriptSet.strings[instr.arg];
ScreenNameMap_t::const_iterator snIt = screenNames.find(screenNameStr);
if (snIt != screenNames.end()) {
instr.op = ScriptOps::kNumber;
instr.arg = snIt->_value;
} else
error("Couldn't resolve screen name %s to number", screenNameStr.c_str());
} else if (opArgIsStringIndex(instr.op)) {
uint strID = instr.arg;
Common::HashMap<uint, uint>::const_iterator strIndexIt = stringIndexToUsedString.find(strID);
if (strIndexIt == stringIndexToUsedString.end()) {
uint newIndex = stringIndexToUsedString.size();
stringIndexToUsedString[strID] = newIndex;
instr.arg = newIndex;
} else
instr.arg = strIndexIt->_value;
}
}
}
debug(1, "Optimize result: Fns: %u -> %u Strs: %u -> %u", static_cast<uint>(scriptSet.functions.size()), static_cast<uint>(functionIndexToUsedFunction.size()), static_cast<uint>(scriptSet.strings.size()), static_cast<uint>(stringIndexToUsedString.size()));
Common::Array<Common::SharedPtr<Script> > functions;
Common::Array<Common::String> functionNames;
functions.resize(functionIndexToUsedFunction.size());
if (scriptSet.functionNames.size())
functionNames.resize(functionIndexToUsedFunction.size());
for (const Common::HashMap<uint, uint>::Node &fnRemapNode : functionIndexToUsedFunction) {
functions[fnRemapNode._value] = scriptSet.functions[fnRemapNode._key];
if (functionNames.size())
functionNames[fnRemapNode._value] = scriptSet.functionNames[fnRemapNode._key];
}
Common::Array<Common::String> strings;
strings.resize(stringIndexToUsedString.size());
for (const Common::HashMap<uint, uint>::Node &strRemapNode : stringIndexToUsedString)
strings[strRemapNode._value] = scriptSet.strings[strRemapNode._key];
scriptSet.functions = Common::move(functions);
scriptSet.functionNames = Common::move(functionNames);
scriptSet.strings = Common::move(strings);
}
} // namespace VCruise