VCRUISE: Add support for Schizm script dialect.

This commit is contained in:
elasota 2023-05-01 14:01:08 -04:00
parent dc324cdd14
commit 1c5898d3fd
6 changed files with 702 additions and 99 deletions

View File

@ -116,13 +116,9 @@ Common::Point RuntimeMenuInterface::getMouseCoordinate() const {
void RuntimeMenuInterface::restartGame() const {
Common::SharedPtr<SaveGameSnapshot> snapshot(new SaveGameSnapshot());
if (_runtime->_gameID == GID_REAH) {
snapshot->roomNumber = 1;
snapshot->screenNumber = 0xb0;
snapshot->loadedAnimation = 1;
} else {
error("Don't know what screen to start on for this game");
}
snapshot->roomNumber = 1;
snapshot->screenNumber = 0xb0;
snapshot->loadedAnimation = 1;
_runtime->_saveGame = snapshot;
_runtime->restoreSaveGameSnapshot();
@ -1043,13 +1039,49 @@ bool Runtime::bootGame(bool newGame) {
_gameState = kGameStateIdle;
if (newGame) {
if (_gameID == GID_REAH) {
changeToScreen(1, 0xb1);
} else
error("Couldn't figure out what screen to start on");
if (_gameID == GID_SCHIZM) {
Common::SharedPtr<IScriptCompilerGlobalState> gs = createScriptCompilerGlobalState();
// Precompile all scripts. We must do this because global functions are stored in room 3, which is never used.
Common::SharedPtr<ScriptSet> scriptSet(new ScriptSet());
Common::ArchiveMemberList scriptFiles;
SearchMan.listMatchingMembers(scriptFiles, "Log/Room##.log", true);
Common::Array<bool> scriptExists;
uint highestScriptIndex = 0;
for (const Common::ArchiveMemberPtr &scriptFile : scriptFiles) {
Common::String scriptName = scriptFile->getName();
uint scriptIndex = ((scriptName[4] - '0') * 10) + (scriptName[5] - '0');
if (scriptIndex > highestScriptIndex) {
highestScriptIndex = scriptIndex;
scriptExists.resize(highestScriptIndex + 1);
}
scriptExists[scriptIndex] = true;
}
for (uint i = 0; i <= highestScriptIndex; i++) {
if (!scriptExists[i])
continue;
Common::String logicFileName = Common::String::format("Log/Room%02u.log", i);
Common::File logicFile;
if (logicFile.open(logicFileName)) {
debug(1, "Compiling script %s...", logicFileName.c_str());
compileSchizmLogicFile(*scriptSet, logicFile, static_cast<uint>(logicFile.size()), logicFileName, gs.get());
logicFile.close();
}
}
_scriptSet = scriptSet;
}
if (newGame)
changeToScreen(1, 0xb1);
Common::Language lang = Common::parseLanguage(ConfMan.get("language"));
bool foundLang = false;
@ -1738,6 +1770,8 @@ bool Runtime::runScript() {
DISPATCH_OP(AnimG);
DISPATCH_OP(AnimS);
DISPATCH_OP(Anim);
DISPATCH_OP(AnimChange);
DISPATCH_OP(AnimVolume);
DISPATCH_OP(Static);
DISPATCH_OP(VarLoad);
@ -1773,6 +1807,7 @@ bool Runtime::runScript() {
DISPATCH_OP(Music);
DISPATCH_OP(MusicVolRamp);
DISPATCH_OP(MusicStop);
DISPATCH_OP(Parm0);
DISPATCH_OP(Parm1);
DISPATCH_OP(Parm2);
@ -2280,14 +2315,21 @@ void Runtime::changeToScreen(uint roomNumber, uint screenNumber) {
// This shouldn't happen when running a script
assert(!_activeScript);
_scriptSet.reset();
if (_gameID == GID_SCHIZM) {
// Keep script set
} else if (_gameID == GID_REAH) {
_scriptSet.reset();
Common::String logicFileName = Common::String::format("Log/Room%02i.log", static_cast<int>(roomNumber));
Common::File logicFile;
if (logicFile.open(logicFileName)) {
_scriptSet = compileReahLogicFile(logicFile, static_cast<uint>(logicFile.size()), logicFileName);
logicFile.close();
}
} else
error("Don't know how to compile scripts for this game");
Common::String logicFileName = Common::String::format("Log/Room%02i.log", static_cast<int>(roomNumber));
Common::File logicFile;
if (logicFile.open(logicFileName)) {
_scriptSet = compileLogicFile(logicFile, static_cast<uint>(logicFile.size()), logicFileName);
logicFile.close();
}
_map.clear();
@ -4473,6 +4515,9 @@ void Runtime::scriptOpAnim(ScriptArg_t arg) {
}
}
OPCODE_STUB(AnimChange)
OPCODE_STUB(AnimVolume)
void Runtime::scriptOpStatic(ScriptArg_t arg) {
TAKE_STACK_INT(kAnimDefStackArgs);
@ -4863,6 +4908,7 @@ void Runtime::scriptOpMusicVolRamp(ScriptArg_t arg) {
}
}
OPCODE_STUB(MusicStop)
void Runtime::scriptOpParm0(ScriptArg_t arg) {
TAKE_STACK_INT(4);
@ -5433,9 +5479,9 @@ void Runtime::scriptOpAnimName(ScriptArg_t arg) {
error("Can't resolve animation for room, room number was invalid");
Common::HashMap<Common::String, AnimationDef>::const_iterator it = roomDef->animations.find(_scriptSet->strings[arg]);
Common::HashMap<Common::String, AnimationDef>::const_iterator it = roomDef->animations.find(_activeScript->strings[arg]);
if (it == roomDef->animations.end())
error("Can't resolve animation for room, couldn't find animation '%s'", _scriptSet->strings[arg].c_str());
error("Can't resolve animation for room, couldn't find animation '%s'", _activeScript->strings[arg].c_str());
pushAnimDef(it->_value);
}
@ -5448,7 +5494,7 @@ void Runtime::scriptOpValueName(ScriptArg_t arg) {
if (!roomDef)
error("Room def doesn't exist");
const Common::String &varName = _scriptSet->strings[arg];
const Common::String &varName = _activeScript->strings[arg];
Common::HashMap<Common::String, int>::const_iterator it = roomDef->values.find(varName);
if (it == roomDef->values.end())
@ -5465,7 +5511,7 @@ void Runtime::scriptOpVarName(ScriptArg_t arg) {
if (!roomDef)
error("Room def doesn't exist");
const Common::String &varName = _scriptSet->strings[arg];
const Common::String &varName = _activeScript->strings[arg];
Common::HashMap<Common::String, uint>::const_iterator it = roomDef->vars.find(varName);
if (it == roomDef->vars.end())
@ -5475,11 +5521,11 @@ void Runtime::scriptOpVarName(ScriptArg_t arg) {
}
void Runtime::scriptOpSoundName(ScriptArg_t arg) {
_scriptStack.push_back(StackValue(_scriptSet->strings[arg]));
_scriptStack.push_back(StackValue(_activeScript->strings[arg]));
}
void Runtime::scriptOpCursorName(ScriptArg_t arg) {
const Common::String &cursorName = _scriptSet->strings[arg];
const Common::String &cursorName = _activeScript->strings[arg];
Common::HashMap<Common::String, StackInt_t>::const_iterator namedCursorIt = _namedCursors.find(cursorName);
if (namedCursorIt == _namedCursors.end()) {

View File

@ -74,6 +74,7 @@ class RuntimeMenuInterface;
class TextParser;
struct ScriptSet;
struct Script;
struct IScriptCompilerGlobalState;
struct Instruction;
enum GameState {
@ -768,6 +769,8 @@ private:
void scriptOpAnimG(ScriptArg_t arg);
void scriptOpAnimS(ScriptArg_t arg);
void scriptOpAnim(ScriptArg_t arg);
void scriptOpAnimChange(ScriptArg_t arg);
void scriptOpAnimVolume(ScriptArg_t arg);
void scriptOpStatic(ScriptArg_t arg);
void scriptOpVarLoad(ScriptArg_t arg);
@ -805,6 +808,7 @@ private:
void scriptOpMusic(ScriptArg_t arg);
void scriptOpMusicVolRamp(ScriptArg_t arg);
void scriptOpMusicStop(ScriptArg_t arg);
void scriptOpParm0(ScriptArg_t arg);
void scriptOpParm1(ScriptArg_t arg);
void scriptOpParm2(ScriptArg_t arg);

View File

@ -28,6 +28,11 @@
namespace VCruise {
enum ScriptDialect {
kScriptDialectReah,
kScriptDialectSchizm,
};
class LogicUnscrambleStream : public Common::ReadStream {
public:
LogicUnscrambleStream(Common::ReadStream *stream, uint streamSize);
@ -129,11 +134,17 @@ ProtoInstruction::ProtoInstruction(ProtoOp paramProtoOp, ScriptOps::ScriptOp par
struct ProtoScript {
Common::Array<ProtoInstruction> instrs;
Common::Array<Common::String> strings;
Common::HashMap<Common::String, uint> stringToIndex;
void reset();
};
void ProtoScript::reset() {
instrs.clear();
strings.clear();
stringToIndex.clear(true);
}
struct ScriptNamedInstruction {
@ -144,9 +155,9 @@ struct ScriptNamedInstruction {
class ScriptCompiler {
public:
ScriptCompiler(TextParser &parser, const Common::String &blamePath);
ScriptCompiler(TextParser &parser, const Common::String &blamePath, ScriptDialect dialect, IScriptCompilerGlobalState *gs);
void compileRoomScriptSet(ScriptSet *ss);
void compileScriptSet(ScriptSet *ss);
private:
bool parseNumber(const Common::String &token, uint32 &outNumber) const;
@ -156,12 +167,14 @@ private:
void expectNumber(uint32 &outNumber);
void compileRoomScriptSet(RoomScriptSet *rss);
void compileScreenScriptSet(ScreenScriptSet *sss);
void compileReahScreenScriptSet(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);
static uint indexString(ProtoScript &script, const Common::String &str);
enum NumberParsingMode {
kNumberParsingDec,
@ -173,32 +186,62 @@ private:
NumberParsingMode _numberParsingMode;
const Common::String _blamePath;
Common::HashMap<Common::String, uint> _stringToIndex;
Common::Array<Common::String> _strings;
ScriptDialect _dialect;
const char *_scrToken;
const char *_eroomToken;
IScriptCompilerGlobalState *_gs;
};
ScriptCompiler::ScriptCompiler(TextParser &parser, const Common::String &blamePath) : _numberParsingMode(kNumberParsingHex), _parser(parser), _blamePath(blamePath) {
class ScriptCompilerGlobalState : public IScriptCompilerGlobalState {
public:
void define(const Common::String &key, const Common::String &value) override;
const Common::String *getTokenReplacement(const Common::String &str) const override;
void addFunction(const Common::String &fnName, const Common::SharedPtr<Script> &fn) override;
Common::SharedPtr<Script> getFunction(const Common::String &fnName) const override;
private:
Common::HashMap<Common::String, Common::String> _defs;
Common::HashMap<Common::String, Common::SharedPtr<Script> > _functions;
};
ScriptCompiler::ScriptCompiler(TextParser &parser, const Common::String &blamePath, ScriptDialect dialect, IScriptCompilerGlobalState *gs)
: _numberParsingMode(kNumberParsingHex), _parser(parser), _blamePath(blamePath), _dialect(dialect), _gs(gs),
_scrToken(nullptr), _eroomToken(nullptr) {
}
bool ScriptCompiler::parseNumber(const Common::String &token, uint32 &outNumber) const {
if (token.size() == 0)
return false;
if (token[0] == 'd')
return parseDecNumber(token, 1, outNumber);
if (_dialect == kScriptDialectReah) {
if (token[0] == 'd')
return parseDecNumber(token, 1, outNumber);
if (token[0] == '0') {
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;
if (token[0] == '0') {
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;
@ -256,56 +299,100 @@ void ScriptCompiler::expectNumber(uint32 &outNumber) {
}
}
void ScriptCompiler::compileRoomScriptSet(ScriptSet *ss) {
void ScriptCompiler::compileScriptSet(ScriptSet *ss) {
Common::SharedPtr<RoomScriptSet> roomScript;
const char *roomToken = nullptr;
if (_dialect == kScriptDialectReah) {
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 == "~ROOM") {
if (token == roomToken) {
if (roomScript)
error("Error compiling script at line %i col %i: Encountered ~ROOM without ~EROOM", static_cast<int>(state._lineNum), static_cast<int>(state._col));
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;
expectNumber(roomNumber);
{
uint32 roomNumber = 0;
ss->roomScripts[roomNumber] = roomScript;
if (_parser.parseToken(token, state)) {
// Many Schizm rooms use 0xxh as the room number and are empty. In this case the room is discarded.
if (_dialect != kScriptDialectSchizm || token != "0xxh") {
if (!parseNumber(token, roomNumber))
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 ~ROOM and found '%s'", static_cast<int>(state._lineNum), static_cast<int>(state._col), token.c_str());
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());
}
}
ss->strings = Common::move(_strings);
}
void ScriptCompiler::compileRoomScriptSet(RoomScriptSet *rss) {
TextParserState state;
Common::String token;
while (_parser.parseToken(token, state)) {
if (token == "~EROOM") {
if (token == _eroomToken) {
return;
} else if (token == "~SCR") {
} else if (token == _scrToken) {
uint32 screenNumber = 0;
expectNumber(screenNumber);
Common::SharedPtr<ScreenScriptSet> sss(new ScreenScriptSet());
compileScreenScriptSet(sss.get());
if (_dialect == kScriptDialectReah)
compileReahScreenScriptSet(sss.get());
else if (_dialect == kScriptDialectSchizm)
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));
_gs->define(key, value);
} 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());
_gs->addFunction(fnName, func);
} else {
error("Error compiling script at line %i col %i: Expected ~EROOM or ~SCR and found '%s'", static_cast<int>(state._lineNum), static_cast<int>(state._col), token.c_str());
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::compileScreenScriptSet(ScreenScriptSet *sss) {
void ScriptCompiler::compileReahScreenScriptSet(ScreenScriptSet *sss) {
TextParserState state;
Common::String token;
@ -339,7 +426,45 @@ void ScriptCompiler::compileScreenScriptSet(ScreenScriptSet *sss) {
} else if (token == "dubbing") {
Common::String dubbingName;
_parser.expectToken(dubbingName, _blamePath);
protoScript.instrs.push_back(ProtoInstruction(ScriptOps::kDubbing, indexString(dubbingName)));
protoScript.instrs.push_back(ProtoInstruction(ScriptOps::kDubbing, indexString(protoScript, 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);
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));
sss->screenName = token;
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);
codeGenScript(protoScript, *currentScript);
currentScript.reset(new Script());
protoScript.reset();
sss->interactionScripts[interactionNumber] = currentScript;
} else if (compileInstructionToken(protoScript, token)) {
// Nothing
} else {
@ -348,7 +473,27 @@ void ScriptCompiler::compileScreenScriptSet(ScreenScriptSet *sss) {
}
}
static ScriptNamedInstruction g_namedInstructions[] = {
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_reahNamedInstructions[] = {
{"rotate", ProtoOp::kProtoOpScript, ScriptOps::kRotate},
{"angle", ProtoOp::kProtoOpScript, ScriptOps::kAngle},
{"angleG@", ProtoOp::kProtoOpScript, ScriptOps::kAngleGGet},
@ -356,10 +501,10 @@ static ScriptNamedInstruction g_namedInstructions[] = {
{"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
{"animR", ProtoOp::kProtoOpScript, ScriptOps::kAnimR},
{"animF", ProtoOp::kProtoOpScript, ScriptOps::kAnimF},
{"animN", ProtoOp::kProtoOpScript, ScriptOps::kAnimN},
{"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},
@ -465,7 +610,147 @@ static ScriptNamedInstruction g_namedInstructions[] = {
{"allowedSave", ProtoOp::kProtoOpNoop, ScriptOps::kInvalid},
};
bool ScriptCompiler::compileInstructionToken(ProtoScript &script, const Common::String &token) {
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::kVolumeDown},
{"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::kCDGet},
{"disc", ProtoOp::kProtoOpScript, ScriptOps::kDisc},
{"save0", ProtoOp::kProtoOpNoop, ScriptOps::kSave0},
{"hidePanel", ProtoOp::kProtoOpNoop, ScriptOps::kHidePanel},
{"ItemExist@", ProtoOp::kProtoOpScript, ScriptOps::kItemCheck},
{"ItemSelect!", ProtoOp::kProtoOpScript, ScriptOps::kItemHighlightSet},
{"ItemPlace@", ProtoOp::kProtoOpScript, ScriptOps::kItemHaveSpace},
{"ItemPutInto!", ProtoOp::kProtoOpScript, ScriptOps::kItemAdd},
{"ItemRemove!", ProtoOp::kProtoOpScript, ScriptOps::kItemRemove},
{"cyfra@", ProtoOp::kProtoOpScript, ScriptOps::kCyfraGet},
{"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},
{"+", 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::kProtoOpNoop, ScriptOps::kInvalid},
};
bool ScriptCompiler::compileInstructionToken(ProtoScript &script, const Common::String &tokenBase) {
const Common::String *tokenPtr = &tokenBase;
if (_dialect == kScriptDialectSchizm) {
const Common::String *ppToken = _gs->getTokenReplacement(tokenBase);
if (ppToken)
tokenPtr = ppToken;
}
const Common::String &token = *tokenPtr;
if (_dialect == kScriptDialectSchizm && token.hasPrefix("-")) {
uint32 unumber = 0;
if (parseNumber(token.substr(1), unumber)) {
script.instrs.push_back(ProtoInstruction(ScriptOps::kNumber, -static_cast<int32>(unumber)));
return true;
}
}
uint32 number = 0;
if (parseNumber(token, number)) {
script.instrs.push_back(ProtoInstruction(ScriptOps::kNumber, number));
@ -473,29 +758,33 @@ bool ScriptCompiler::compileInstructionToken(ProtoScript &script, const Common::
}
if (token.size() >= 1 && token[0] == ':') {
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;
if (_dialect == kScriptDialectReah) {
if (token.size() >= 3 && token[2] == ':') {
if (token[1] == 'Y') {
script.instrs.push_back(ProtoInstruction(ScriptOps::kVarName, indexString(script, token.substr(3))));
return true;
} else if (token[1] == 'V') {
script.instrs.push_back(ProtoInstruction(ScriptOps::kValueName, indexString(script, token.substr(3))));
return true;
} else
return false;
}
}
script.instrs.push_back(ProtoInstruction(ScriptOps::kAnimName, indexString(token.substr(1))));
script.instrs.push_back(ProtoInstruction(ScriptOps::kAnimName, indexString(script, token.substr(1))));
return true;
}
if (token.size() >= 2 && token[0] == '_') {
script.instrs.push_back(ProtoInstruction(ScriptOps::kSoundName, indexString(token.substr(1))));
script.instrs.push_back(ProtoInstruction(ScriptOps::kSoundName, indexString(script, token.substr(1))));
return true;
}
if (token.hasPrefix("CUR_")) {
script.instrs.push_back(ProtoInstruction(ScriptOps::kCursorName, indexString(token)));
return true;
if (_dialect == kScriptDialectReah) {
if (token.hasPrefix("CUR_")) {
script.instrs.push_back(ProtoInstruction(ScriptOps::kCursorName, indexString(script, token)));
return true;
}
}
if (token == "#switch") {
@ -521,9 +810,87 @@ bool ScriptCompiler::compileInstructionToken(ProtoScript &script, const Common::
return true;
}
for (const ScriptNamedInstruction &namedInstr : g_namedInstructions) {
if (token == namedInstr.str) {
script.instrs.push_back(ProtoInstruction(namedInstr.protoOp, namedInstr.op, 0));
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.hasPrefix("fn")) {
uint fnIndex = indexString(script, token);
script.instrs.push_back(ProtoInstruction(kProtoOpScript, ScriptOps::kCallFunction, fnIndex));
return true;
}
if (token.size() >= 2 && token[0] == '\"' && token[token.size() - 1] == '\"') {
// Seems like these are only used for sounds and music?
uint fnIndex = indexString(script, token.substr(1, token.size() - 2));
script.instrs.push_back(ProtoInstruction(kProtoOpScript, ScriptOps::kString, fnIndex));
return true;
}
if (token == "dvd@") {
// Always pass disc checks
script.instrs.push_back(ProtoInstruction(kProtoOpScript, ScriptOps::kNumber, 1));
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.size() >= 2 && token.hasSuffix("!")) {
if (compileInstructionToken(script, token.substr(0, token.size() - 1))) {
script.instrs.push_back(ProtoInstruction(kProtoOpScript, ScriptOps::kVarGlobalStore, 0));
return true;
} else
return false;
}
// 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("@")) {
if (compileInstructionToken(script, token.substr(0, token.size() - 1))) {
script.instrs.push_back(ProtoInstruction(kProtoOpScript, ScriptOps::kVarGlobalLoad, 0));
return true;
} else
return false;
}
// 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(script, token)));
return true;
}
if (token.hasPrefix("cur")) {
script.instrs.push_back(ProtoInstruction(ScriptOps::kCursorName, indexString(script, token)));
return true;
}
// 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(script, token)));
return true;
}
}
@ -734,8 +1101,33 @@ void ScriptCompiler::codeGenScript(ProtoScript &protoScript, Script &script) {
}
}
if (controlFlowStack.size() > 0)
error("Error in codegen: Unterminated flow control construct");
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;
@ -802,31 +1194,72 @@ void ScriptCompiler::codeGenScript(ProtoScript &protoScript, Script &script) {
break;
}
}
// Commit strings
script.strings = Common::move(protoScript.strings);
}
uint ScriptCompiler::indexString(const Common::String &str) {
Common::HashMap<Common::String, uint>::const_iterator it = _stringToIndex.find(str);
if (it == _stringToIndex.end()) {
uint index = _strings.size();
_stringToIndex[str] = index;
_strings.push_back(str);
uint ScriptCompiler::indexString(ProtoScript &script, const Common::String &str) {
Common::HashMap<Common::String, uint>::const_iterator it = script.stringToIndex.find(str);
if (it == script.stringToIndex.end()) {
uint index = script.strings.size();
script.stringToIndex[str] = index;
script.strings.push_back(str);
return index;
}
return it->_value;
}
Common::SharedPtr<ScriptSet> compileLogicFile(Common::ReadStream &stream, uint streamSize, const Common::String &blamePath) {
void ScriptCompilerGlobalState::define(const Common::String &key, const Common::String &value) {
_defs.setVal(key, value);
}
const Common::String *ScriptCompilerGlobalState::getTokenReplacement(const Common::String &str) const {
Common::HashMap<Common::String, Common::String>::const_iterator it = _defs.find(str);
if (it == _defs.end())
return nullptr;
return &it->_value;
}
void ScriptCompilerGlobalState::addFunction(const Common::String &fnName, const Common::SharedPtr<Script> &fn) {
_functions.setVal(fnName, fn);
}
Common::SharedPtr<Script> ScriptCompilerGlobalState::getFunction(const Common::String &fnName) const {
Common::HashMap<Common::String, Common::SharedPtr<Script> >::const_iterator it = _functions.find(fnName);
if (it == _functions.end())
return Common::SharedPtr<Script>();
return it->_value;
}
IScriptCompilerGlobalState::~IScriptCompilerGlobalState() {
}
static void compileLogicFile(ScriptSet &scriptSet, Common::ReadStream &stream, uint streamSize, const Common::String &blamePath, ScriptDialect dialect, IScriptCompilerGlobalState *gs) {
LogicUnscrambleStream unscrambleStream(&stream, streamSize);
TextParser parser(&unscrambleStream);
ScriptCompiler compiler(parser, blamePath, dialect, 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::String &blamePath) {
Common::SharedPtr<ScriptSet> scriptSet(new ScriptSet());
ScriptCompiler compiler(parser, blamePath);
compiler.compileRoomScriptSet(scriptSet.get());
compileLogicFile(*scriptSet, stream, streamSize, blamePath, kScriptDialectReah, nullptr);
return scriptSet;
}
void compileSchizmLogicFile(ScriptSet &scriptSet, Common::ReadStream &stream, uint streamSize, const Common::String &blamePath, IScriptCompilerGlobalState *gs) {
compileLogicFile(scriptSet, stream, streamSize, blamePath, kScriptDialectSchizm, gs);
}
} // namespace VCruise

View File

@ -38,6 +38,7 @@ namespace VCruise {
struct ScreenScriptSet;
struct RoomScriptSet;
struct ScriptSet;
struct ITextPreprocessor;
namespace ScriptOps {
@ -156,6 +157,65 @@ enum ScriptOp {
kCheckValue, // Check if stack top is equal to arg. If it is, pop the argument, otherwise leave it on the stack and skip the next instruction.
kJump, // Offset instruction index by arg.
// Schizm ops
kCallFunction,
kMusicStop,
kMusicPlayScore,
kScoreAlways,
kScoreNormal,
kSndPlay,
kSndPlayEx,
kSndPlay3D,
kSndPlaying,
kSndWait,
kSndHalt,
kSndToBack,
kSndStop,
kSndStopAll,
kSndAddRandom,
kSndClearRandom,
kVolumeAdd,
kVolumeChange,
kVolumeDown,
kAnimVolume,
kAnimChange,
kScreenName,
kExtractByte,
kInsertByte,
kString,
kCmpNE,
kCmpLE,
kCmpGE,
kReturn,
kSpeech,
kSpeechEx,
kSpeechTest,
kSay,
kRandomInclusive,
kHeroOut,
kHeroGetPos,
kHeroSetPos,
kHeroGet,
kGarbage,
kGetRoom,
kBitAnd,
kBitOr,
kAngleGet,
kCDGet,
kDisc,
kHidePanel,
kRotateUpdate,
kMul,
kDiv,
kMod,
kCyfraGet, // Cyfra = digit?
kPuzzleInit,
kPuzzleCanPress,
kPuzzleDoMove1,
kPuzzleDoMove2,
kPuzzleDone,
kPuzzleWhoWon,
kNumOps,
};
@ -172,6 +232,7 @@ struct Instruction {
struct Script {
Common::Array<Instruction> instrs;
Common::Array<Common::String> strings;
};
typedef Common::HashMap<uint, Common::SharedPtr<Script> > ScriptMap_t;
@ -181,6 +242,8 @@ typedef Common::HashMap<uint, Common::SharedPtr<RoomScriptSet> > RoomScriptSetMa
struct ScreenScriptSet {
Common::SharedPtr<Script> entryScript;
ScriptMap_t interactionScripts;
Common::String screenName; // Only in Schizm
};
struct RoomScriptSet {
@ -189,10 +252,28 @@ struct RoomScriptSet {
struct ScriptSet {
RoomScriptSetMap_t roomScripts;
Common::Array<Common::String> strings;
};
Common::SharedPtr<ScriptSet> compileLogicFile(Common::ReadStream &stream, uint streamSize, const Common::String &blamePath);
struct FunctionDef {
Common::String fnName;
Common::SharedPtr<Script> func;
};
// Global state is required for Schizm because its preprocessor defines exist across files.
// For example, volPortWaves is set in Room01 but used in Room03 and Room20
struct IScriptCompilerGlobalState {
virtual ~IScriptCompilerGlobalState();
virtual void define(const Common::String &key, const Common::String &value) = 0;
virtual const Common::String *getTokenReplacement(const Common::String &str) const = 0;
virtual void addFunction(const Common::String &fnName, const Common::SharedPtr<Script> &fn) = 0;
virtual Common::SharedPtr<Script> getFunction(const Common::String &fnName) const = 0;
};
Common::SharedPtr<IScriptCompilerGlobalState> createScriptCompilerGlobalState();
Common::SharedPtr<ScriptSet> compileReahLogicFile(Common::ReadStream &stream, uint streamSize, const Common::String &blamePath);
void compileSchizmLogicFile(ScriptSet &scriptSet, Common::ReadStream &stream, uint streamSize, const Common::String &blamePath, IScriptCompilerGlobalState *gs);
}

View File

@ -29,6 +29,7 @@ namespace VCruise {
TextParserState::TextParserState() : _lineNum(1), _col(1), _prevWasCR(false), _isParsingComment(false) {
}
TextParser::TextParser(Common::ReadStream *stream) : _stream(stream), _returnedBufferPos(kReturnedBufferSize) {
memset(_returnedBuffer, 0, kReturnedBufferSize);
}
@ -102,6 +103,13 @@ bool TextParser::isDelimiter(char c) {
return false;
}
bool TextParser::isCompoundDelimiter(char c1, char c2) {
if (c2 == '=' && (c1 == '=' || c1 == '<' || c1 == '>' || c1 == '!'))
return true;
return false;
}
bool TextParser::isWhitespace(char c) {
return (c == ' ') || ((c & 0xe0) == 0);
}
@ -246,18 +254,46 @@ bool TextParser::parseToken(Common::String &outString, TextParserState &outState
outString += c;
if (isDelimiter(c))
if (c == '\"') {
while (readOneChar(c, state)) {
if (c == '\n' || c == '\r') {
requeue(&c, 1, state);
return true;
}
outString += c;
if (c == '\"')
return true;
}
return true;
}
if (isDelimiter(c)) {
char firstC = c;
if (readOneChar(c, state)) {
if (isCompoundDelimiter(firstC, c))
outString += c;
else
requeue(&c, 1, state);
}
return true;
}
while (readOneChar(c, state)) {
if (isWhitespace(c) || _state._isParsingComment) {
requeue(&c, 1, state);
return true;
break;
}
if (outString.size() == 1 && isCompoundDelimiter(outString[0], c)) {
outString += c;
break;
}
if (isDelimiter(c)) {
requeue(&c, 1, state);
return true;
break;
}
outString += c;

View File

@ -22,6 +22,8 @@
#ifndef VCRUISE_TEXTPARSER_H
#define VCRUISE_TEXTPARSER_H
#include "common/hashmap.h"
#include "common/hash-str.h"
#include "common/str.h"
#include "common/memstream.h"
@ -69,6 +71,7 @@ private:
void expectTokenInternal(Common::String &outToken, const Common::String &blamePath, TextParserState &outState);
static bool isDelimiter(char c);
static bool isCompoundDelimiter(char c1, char c2);
static bool isWhitespace(char c);
TextParserState _state;