[Scummvm-git-logs] scummvm master -> 62712ca02b34478a6b14a34cfe451a95cf23b74c

elasota noreply at scummvm.org
Thu May 4 03:41:32 UTC 2023


This automated email contains information about 11 new commits which have been
pushed to the 'scummvm' repo located at https://github.com/scummvm/scummvm .

Summary:
1c5898d3fd VCRUISE: Add support for Schizm script dialect.
8f38fbcdc7 VCRUISE: Add function calls and Schizm main menu things
50a505b6a8 VCRUISE: Add animation volume handling and score loader.
cc0dcb1f71 VCRUISE: Fix up some initial Schizm things.
0188660e82 VCRUISE: Fix up volume calculations for Schizm.
3cb2a31280 VCRUISE: Fix some function names being parsed as hex numbers.  Add disc type variations.
298011eb43 VCRUISE: Support Schizm's presets section in playlist file
e7f10fe8f8 VCRUISE: Fix Schizm DVD identification
d0aee6d817 VCRUISE: Add menu text labels
9923c44a78 VCRUISE: Fix options
62712ca02b VCRUISE: Add skip main menu option functionality.


Commit: 1c5898d3fdee849e6051c181e7ee97b1ea800928
    https://github.com/scummvm/scummvm/commit/1c5898d3fdee849e6051c181e7ee97b1ea800928
Author: elasota (ejlasota at gmail.com)
Date: 2023-05-03T23:40:43-04:00

Commit Message:
VCRUISE: Add support for Schizm script dialect.

Changed paths:
    engines/vcruise/runtime.cpp
    engines/vcruise/runtime.h
    engines/vcruise/script.cpp
    engines/vcruise/script.h
    engines/vcruise/textparser.cpp
    engines/vcruise/textparser.h


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index a41a008e982..580cea7d983 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -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()) {
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index b95a50f9288..08dc37247da 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -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);
diff --git a/engines/vcruise/script.cpp b/engines/vcruise/script.cpp
index 4661241a07b..43d57b16621 100644
--- a/engines/vcruise/script.cpp
+++ b/engines/vcruise/script.cpp
@@ -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;
+};
+
+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) : _numberParsingMode(kNumberParsingHex), _parser(parser), _blamePath(blamePath) {
+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 (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 (_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;
+			}
 		}
+	} 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);
 
-	Common::SharedPtr<ScriptSet> scriptSet(new ScriptSet());
+	ScriptCompiler compiler(parser, blamePath, dialect, gs);
 
-	ScriptCompiler compiler(parser, blamePath);
+	compiler.compileScriptSet(&scriptSet);
+}
+
+Common::SharedPtr<IScriptCompilerGlobalState> createScriptCompilerGlobalState() {
+	return Common::SharedPtr<IScriptCompilerGlobalState>(new ScriptCompilerGlobalState());
+}
 
-	compiler.compileRoomScriptSet(scriptSet.get());
+Common::SharedPtr<ScriptSet> compileReahLogicFile(Common::ReadStream &stream, uint streamSize, const Common::String &blamePath) {
+	Common::SharedPtr<ScriptSet> scriptSet(new ScriptSet());
 
+	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
diff --git a/engines/vcruise/script.h b/engines/vcruise/script.h
index f0086c07a12..6cf002f4144 100644
--- a/engines/vcruise/script.h
+++ b/engines/vcruise/script.h
@@ -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);
 
 }
 
diff --git a/engines/vcruise/textparser.cpp b/engines/vcruise/textparser.cpp
index b8ecf16189e..03f3d355b77 100644
--- a/engines/vcruise/textparser.cpp
+++ b/engines/vcruise/textparser.cpp
@@ -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;
diff --git a/engines/vcruise/textparser.h b/engines/vcruise/textparser.h
index 7bbe73eb115..f62dbaa690d 100644
--- a/engines/vcruise/textparser.h
+++ b/engines/vcruise/textparser.h
@@ -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;


Commit: 8f38fbcdc7ce979fb9b59eb7ab7b6e090b0b384f
    https://github.com/scummvm/scummvm/commit/8f38fbcdc7ce979fb9b59eb7ab7b6e090b0b384f
Author: elasota (ejlasota at gmail.com)
Date: 2023-05-03T23:40:43-04:00

Commit Message:
VCRUISE: Add function calls and Schizm main menu things

Changed paths:
    engines/vcruise/runtime.cpp
    engines/vcruise/runtime.h
    engines/vcruise/script.cpp
    engines/vcruise/script.h


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 580cea7d983..3ba53a525f2 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -261,6 +261,9 @@ Runtime::StackValue &Runtime::StackValue::operator=(StackValue &&other) {
 	return *this;
 }
 
+Runtime::CallStackFrame::CallStackFrame() : _nextInstruction(0) {
+}
+
 Runtime::Gyro::Gyro() {
 	reset();
 }
@@ -840,7 +843,7 @@ LoadGameOutcome SaveGameSnapshot::read(Common::ReadStream *stream) {
 Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &rootFSNode, VCruiseGameID gameID)
 	: _system(system), _mixer(mixer), _roomNumber(1), _screenNumber(0), _direction(0), _haveHorizPanAnimations(false), _loadedRoomNumber(0), _activeScreenNumber(0),
 	  _gameState(kGameStateBoot), _gameID(gameID), _havePendingScreenChange(false), _forceScreenChange(false), _havePendingReturnToIdleState(false), _havePendingCompletionCheck(false),
-	  _havePendingPlayAmbientSounds(false), _ambientSoundFinishTime(0), _scriptNextInstruction(0), _escOn(false), _debugMode(false), _fastAnimationMode(false),
+	  _havePendingPlayAmbientSounds(false), _ambientSoundFinishTime(0), _escOn(false), _debugMode(false), _fastAnimationMode(false),
 	  _musicTrack(0), _musicVolume(100), _musicVolumeRampStartTime(0), _musicVolumeRampStartVolume(0), _musicVolumeRampRatePerMSec(0), _musicVolumeRampEnd(0),
 	  _panoramaDirectionFlags(0),
 	  _loadedAnimation(0), _loadedAnimationHasSound(false), _animPendingDecodeFrame(0), _animDisplayingFrame(0), _animFirstFrame(0), _animLastFrame(0), _animStopFrame(0),
@@ -982,6 +985,10 @@ bool Runtime::runFrame() {
 		case kGameStatePanRight:
 			moreActions = runHorizontalPan(true);
 			break;
+		case kGameStateScriptReset:
+			_gameState = kGameStateScript;
+			moreActions = runScript();
+			break;
 		case kGameStateScript:
 			moreActions = runScript();
 			break;
@@ -1039,46 +1046,6 @@ bool Runtime::bootGame(bool newGame) {
 
 	_gameState = kGameStateIdle;
 
-	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);
 
@@ -1743,16 +1710,25 @@ void Runtime::commitSectionToScreen(const RenderSection &section, const Common::
 	case ScriptOps::k##op: this->scriptOp##op(arg); break
 
 bool Runtime::runScript() {
+	if (_scriptCallStack.empty()) {
+		terminateScript();
+		return true;
+	}
+
+	CallStackFrame &frame = _scriptCallStack.back();
+	const Common::Array<Instruction> &instrs = frame._script->instrs;
+
 	while (_gameState == kGameStateScript) {
-		uint instrNum = _scriptNextInstruction;
-		if (!_activeScript || instrNum >= _activeScript->instrs.size()) {
-			terminateScript();
+		uint instrNum = frame._nextInstruction;
+
+		if (instrNum >= instrs.size()) {
+			_scriptCallStack.pop_back();
 			return true;
 		}
 
-		_scriptNextInstruction++;
+		frame._nextInstruction = instrNum + 1u;
 
-		const Instruction &instr = _activeScript->instrs[instrNum];
+		const Instruction &instr = instrs[instrNum];
 		int32 arg = instr.arg;
 
 		switch (instr.op) {
@@ -1770,8 +1746,6 @@ bool Runtime::runScript() {
 			DISPATCH_OP(AnimG);
 			DISPATCH_OP(AnimS);
 			DISPATCH_OP(Anim);
-			DISPATCH_OP(AnimChange);
-			DISPATCH_OP(AnimVolume);
 
 			DISPATCH_OP(Static);
 			DISPATCH_OP(VarLoad);
@@ -1807,7 +1781,6 @@ bool Runtime::runScript() {
 
 			DISPATCH_OP(Music);
 			DISPATCH_OP(MusicVolRamp);
-			DISPATCH_OP(MusicStop);
 			DISPATCH_OP(Parm0);
 			DISPATCH_OP(Parm1);
 			DISPATCH_OP(Parm2);
@@ -1874,6 +1847,67 @@ bool Runtime::runScript() {
 			DISPATCH_OP(CheckValue);
 			DISPATCH_OP(Jump);
 
+			// Schizm ops
+			DISPATCH_OP(CallFunction);
+			DISPATCH_OP(Return);
+
+			DISPATCH_OP(MusicStop);
+			DISPATCH_OP(MusicPlayScore);
+			DISPATCH_OP(ScoreAlways);
+			DISPATCH_OP(ScoreNormal);
+			DISPATCH_OP(SndPlay);
+			DISPATCH_OP(SndPlayEx);
+			DISPATCH_OP(SndPlay3D);
+			DISPATCH_OP(SndPlaying);
+			DISPATCH_OP(SndWait);
+			DISPATCH_OP(SndHalt);
+			DISPATCH_OP(SndToBack);
+			DISPATCH_OP(SndStop);
+			DISPATCH_OP(SndStopAll);
+			DISPATCH_OP(SndAddRandom);
+			DISPATCH_OP(SndClearRandom);
+			DISPATCH_OP(VolumeAdd);
+			DISPATCH_OP(VolumeChange);
+			DISPATCH_OP(VolumeDown);
+			DISPATCH_OP(AnimVolume);
+			DISPATCH_OP(AnimChange);
+			DISPATCH_OP(ScreenName);
+			DISPATCH_OP(ExtractByte);
+			DISPATCH_OP(InsertByte);
+			DISPATCH_OP(String);
+			DISPATCH_OP(CmpNE);
+			DISPATCH_OP(CmpLE);
+			DISPATCH_OP(CmpGE);
+			DISPATCH_OP(Speech);
+			DISPATCH_OP(SpeechEx);
+			DISPATCH_OP(SpeechTest);
+			DISPATCH_OP(Say);
+			DISPATCH_OP(RandomInclusive);
+			DISPATCH_OP(HeroOut);
+			DISPATCH_OP(HeroGetPos);
+			DISPATCH_OP(HeroSetPos);
+			DISPATCH_OP(HeroGet);
+			DISPATCH_OP(Garbage);
+			DISPATCH_OP(GetRoom);
+			DISPATCH_OP(BitAnd);
+			DISPATCH_OP(BitOr);
+			DISPATCH_OP(AngleGet);
+			DISPATCH_OP(CDGet);
+			DISPATCH_OP(Disc);
+			DISPATCH_OP(HidePanel);
+			DISPATCH_OP(RotateUpdate);
+			DISPATCH_OP(Mul);
+			DISPATCH_OP(Div);
+			DISPATCH_OP(Mod);
+			DISPATCH_OP(CyfraGet);
+			DISPATCH_OP(PuzzleInit);
+			DISPATCH_OP(PuzzleCanPress);
+			DISPATCH_OP(PuzzleDoMove1);
+			DISPATCH_OP(PuzzleDoMove2);
+			DISPATCH_OP(PuzzleDone);
+			DISPATCH_OP(PuzzleWhoWon);
+			DISPATCH_OP(Fn);
+
 		default:
 			error("Unimplemented opcode %i", static_cast<int>(instr.op));
 		}
@@ -1885,8 +1919,7 @@ bool Runtime::runScript() {
 #undef DISPATCH_OP
 
 void Runtime::terminateScript() {
-	_activeScript.reset();
-	_scriptNextInstruction = 0;
+	_scriptCallStack.clear();
 
 	if (_gameState == kGameStateScript)
 		_gameState = kGameStateIdle;
@@ -2313,10 +2346,16 @@ void Runtime::changeToScreen(uint roomNumber, uint screenNumber) {
 
 	if (changedRoom) {
 		// This shouldn't happen when running a script
-		assert(!_activeScript);
+		assert(_scriptCallStack.empty());
 
 		if (_gameID == GID_SCHIZM) {
-			// Keep script set
+			uint roomsToCompile[3] = {1, 3, 0};
+			uint numRoomsToCompile = 2;
+
+			if (roomNumber != 1 && roomNumber != 3)
+				roomsToCompile[numRoomsToCompile++] = roomNumber;
+
+			compileSchizmLogicSet(roomsToCompile, numRoomsToCompile);
 		} else if (_gameID == GID_REAH) {
 			_scriptSet.reset();
 
@@ -3276,12 +3315,56 @@ void Runtime::activateScript(const Common::SharedPtr<Script> &script, const Scri
 	if (script->instrs.size() == 0)
 		return;
 
+	assert(_gameState != kGameStateScript);
+
 	_scriptEnv = envVars;
-	_activeScript = script;
-	_scriptNextInstruction = 0;
+
+	CallStackFrame frame;
+	frame._script = script;
+	frame._nextInstruction = 0;
+
+	_scriptCallStack.resize(1);
+	_scriptCallStack[0] = frame;
+
 	_gameState = kGameStateScript;
 }
 
+void Runtime::compileSchizmLogicSet(const uint *roomNumbers, uint numRooms) {
+	_scriptSet.reset();
+
+	Common::SharedPtr<IScriptCompilerGlobalState> gs = createScriptCompilerGlobalState();
+
+	Common::SharedPtr<ScriptSet> scriptSet(new ScriptSet());
+
+	for (uint i = 0; i < numRooms; i++) {
+
+		Common::String logicFileName = Common::String::format("Log/Room%02u.log", roomNumbers[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();
+		}
+	}
+
+	gs->dumpFunctionNames(scriptSet->functionNames);
+
+	uint numFunctions = gs->getNumFunctions();
+
+	scriptSet->functions.resize(numFunctions);
+
+	for (uint i = 0; i < numFunctions; i++) {
+		Common::SharedPtr<Script> function = gs->getFunction(i);
+		scriptSet->functions[i] = function;
+
+		if (!function)
+			warning("Function '%s' was referenced but not defined", scriptSet->functionNames[i].c_str());
+	}
+
+	_scriptSet = scriptSet;
+}
+
 bool Runtime::parseIndexDef(IndexParseType parseType, uint roomNumber, const Common::String &key, const Common::String &value) {
 	switch (parseType) {
 	case kIndexParseTypeNameRoom: {
@@ -3317,6 +3400,10 @@ bool Runtime::parseIndexDef(IndexParseType parseType, uint roomNumber, const Com
 
 		int numValuesRead = sscanf(value.c_str(), "%i, %i, %i, %i", &left, &top, &width, &height);
 
+		// Work around bad def in Schizm at line 5899
+		if (numValuesRead != 4)
+			numValuesRead = sscanf(value.c_str(), "%i ,%i, %i, %i", &left, &top, &width, &height);
+
 		if (numValuesRead == 4) {
 			AnimationDef &animDef = _roomDefs[roomNumber]->animations[key];
 
@@ -4515,9 +4602,6 @@ void Runtime::scriptOpAnim(ScriptArg_t arg) {
 	}
 }
 
-OPCODE_STUB(AnimChange)
-OPCODE_STUB(AnimVolume)
-
 void Runtime::scriptOpStatic(ScriptArg_t arg) {
 	TAKE_STACK_INT(kAnimDefStackArgs);
 
@@ -4908,8 +4992,6 @@ void Runtime::scriptOpMusicVolRamp(ScriptArg_t arg) {
 	}
 }
 
-OPCODE_STUB(MusicStop)
-
 void Runtime::scriptOpParm0(ScriptArg_t arg) {
 	TAKE_STACK_INT(4);
 
@@ -5292,6 +5374,26 @@ void Runtime::scriptOpVerticalPanGet() {
 	_scriptStack.push_back(StackValue(isInRadius ? 1 : 0));
 }
 
+void Runtime::scriptOpCallFunction(ScriptArg_t arg) {
+	Common::SharedPtr<Script> function = _scriptSet->functions[arg];
+	if (function) {
+		CallStackFrame newFrame;
+		newFrame._script = function;
+		newFrame._nextInstruction = 0;
+
+		_scriptCallStack.push_back(newFrame);
+
+		_gameState = kGameStateScriptReset;
+	} else {
+		error("Unknown function '%s'", _scriptSet->functionNames[arg].c_str());
+	}
+}
+
+void Runtime::scriptOpReturn(ScriptArg_t arg) {
+	_scriptCallStack.pop_back();
+	_gameState = kGameStateScriptReset;
+}
+
 void Runtime::scriptOpSaveAs(ScriptArg_t arg) {
 	TAKE_STACK_INT(4);
 
@@ -5444,8 +5546,15 @@ void Runtime::scriptOpGoto(ScriptArg_t arg) {
 	}
 
 	if (newScript) {
-		_scriptNextInstruction = 0;
-		_activeScript = newScript;
+		// This only happens in Reah so we don't have to worry about what to do about frames on the callstack in Schizm
+		_gameState = kGameStateScriptReset;
+
+		CallStackFrame frame;
+		frame._script = newScript;
+		frame._nextInstruction = 0;
+
+		_scriptCallStack.resize(1);
+		_scriptCallStack[0] = frame;
 	} else {
 		error("Goto target %u couldn't be resolved", newInteraction);
 	}
@@ -5479,9 +5588,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(_activeScript->strings[arg]);
+	Common::HashMap<Common::String, AnimationDef>::const_iterator it = roomDef->animations.find(_scriptSet->strings[arg]);
 	if (it == roomDef->animations.end())
-		error("Can't resolve animation for room, couldn't find animation '%s'", _activeScript->strings[arg].c_str());
+		error("Can't resolve animation for room, couldn't find animation '%s'", _scriptSet->strings[arg].c_str());
 
 	pushAnimDef(it->_value);
 }
@@ -5494,7 +5603,7 @@ void Runtime::scriptOpValueName(ScriptArg_t arg) {
 	if (!roomDef)
 		error("Room def doesn't exist");
 
-	const Common::String &varName = _activeScript->strings[arg];
+	const Common::String &varName = _scriptSet->strings[arg];
 
 	Common::HashMap<Common::String, int>::const_iterator it = roomDef->values.find(varName);
 	if (it == roomDef->values.end())
@@ -5511,7 +5620,7 @@ void Runtime::scriptOpVarName(ScriptArg_t arg) {
 	if (!roomDef)
 		error("Room def doesn't exist");
 
-	const Common::String &varName = _activeScript->strings[arg];
+	const Common::String &varName = _scriptSet->strings[arg];
 
 	Common::HashMap<Common::String, uint>::const_iterator it = roomDef->vars.find(varName);
 	if (it == roomDef->vars.end())
@@ -5521,11 +5630,11 @@ void Runtime::scriptOpVarName(ScriptArg_t arg) {
 }
 
 void Runtime::scriptOpSoundName(ScriptArg_t arg) {
-	_scriptStack.push_back(StackValue(_activeScript->strings[arg]));
+	_scriptStack.push_back(StackValue(_scriptSet->strings[arg]));
 }
 
 void Runtime::scriptOpCursorName(ScriptArg_t arg) {
-	const Common::String &cursorName = _activeScript->strings[arg];
+	const Common::String &cursorName = _scriptSet->strings[arg];
 
 	Common::HashMap<Common::String, StackInt_t>::const_iterator namedCursorIt = _namedCursors.find(cursorName);
 	if (namedCursorIt == _namedCursors.end()) {
@@ -5546,13 +5655,76 @@ void Runtime::scriptOpCheckValue(ScriptArg_t arg) {
 	if (stackArgs[0].type == StackValue::kNumber && stackArgs[0].value.i == arg)
 		_scriptStack.pop_back();
 	else
-		_scriptNextInstruction++;
+		_scriptCallStack.back()._nextInstruction++;
 }
 
 void Runtime::scriptOpJump(ScriptArg_t arg) {
-	_scriptNextInstruction = arg;
+	_scriptCallStack.back()._nextInstruction = arg;
 }
 
+OPCODE_STUB(MusicStop)
+OPCODE_STUB(MusicPlayScore)
+OPCODE_STUB(ScoreAlways)
+OPCODE_STUB(ScoreNormal)
+OPCODE_STUB(SndPlay)
+OPCODE_STUB(SndPlayEx)
+OPCODE_STUB(SndPlay3D)
+OPCODE_STUB(SndPlaying)
+OPCODE_STUB(SndWait)
+OPCODE_STUB(SndHalt)
+OPCODE_STUB(SndToBack)
+OPCODE_STUB(SndStop)
+OPCODE_STUB(SndStopAll)
+OPCODE_STUB(SndAddRandom)
+OPCODE_STUB(SndClearRandom)
+OPCODE_STUB(VolumeAdd)
+OPCODE_STUB(VolumeChange)
+OPCODE_STUB(VolumeDown)
+OPCODE_STUB(AnimVolume)
+OPCODE_STUB(AnimChange)
+OPCODE_STUB(ScreenName)
+
+void Runtime::scriptOpExtractByte(ScriptArg_t arg) {
+	TAKE_STACK_INT(2);
+
+	_scriptStack.push_back(StackValue(static_cast<StackInt_t>((stackArgs[0] >> (stackArgs[1] * 8) & 0xff))));
+}
+
+OPCODE_STUB(InsertByte)
+OPCODE_STUB(String)
+OPCODE_STUB(CmpNE)
+OPCODE_STUB(CmpLE)
+OPCODE_STUB(CmpGE)
+OPCODE_STUB(Speech)
+OPCODE_STUB(SpeechEx)
+OPCODE_STUB(SpeechTest)
+OPCODE_STUB(Say)
+OPCODE_STUB(RandomInclusive)
+OPCODE_STUB(HeroOut)
+OPCODE_STUB(HeroGetPos)
+OPCODE_STUB(HeroSetPos)
+OPCODE_STUB(HeroGet)
+OPCODE_STUB(Garbage)
+OPCODE_STUB(GetRoom)
+OPCODE_STUB(BitAnd)
+OPCODE_STUB(BitOr)
+OPCODE_STUB(AngleGet)
+OPCODE_STUB(CDGet)
+OPCODE_STUB(Disc)
+OPCODE_STUB(HidePanel)
+OPCODE_STUB(RotateUpdate)
+OPCODE_STUB(Mul)
+OPCODE_STUB(Div)
+OPCODE_STUB(Mod)
+OPCODE_STUB(CyfraGet)
+OPCODE_STUB(PuzzleInit)
+OPCODE_STUB(PuzzleCanPress)
+OPCODE_STUB(PuzzleDoMove1)
+OPCODE_STUB(PuzzleDoMove2)
+OPCODE_STUB(PuzzleDone)
+OPCODE_STUB(PuzzleWhoWon)
+OPCODE_STUB(Fn)
+
 #undef TAKE_STACK_STR
 #undef TAKE_STACK_STR_NAMED
 #undef TAKE_STACK_INT
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index 08dc37247da..9680f4aa33f 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -86,6 +86,7 @@ enum GameState {
 	kGameStateIdle,							// Waiting for input events
 	kGameStateDelay,						// Waiting for delay completion time
 	kGameStateScript,						// Running a script
+	kGameStateScriptReset,					// Resetting script interpreter into a new script
 	kGameStateGyroIdle,						// Waiting for mouse movement to run a gyro
 	kGameStateGyroAnimation,				// Animating a gyro
 
@@ -640,6 +641,13 @@ private:
 		ValueUnion value;
 	};
 
+	struct CallStackFrame {
+		CallStackFrame();
+
+		Common::SharedPtr<Script> _script;
+		uint _nextInstruction;
+	};
+
 	struct SubtitleDef {
 		SubtitleDef();
 
@@ -718,6 +726,7 @@ private:
 	void pushAnimDef(const AnimationDef &animDef);
 
 	void activateScript(const Common::SharedPtr<Script> &script, const ScriptEnvironmentVars &envVars);
+	void compileSchizmLogicSet(const uint *roomNumbers, uint numRooms);
 
 	bool parseIndexDef(IndexParseType parseType, uint roomNumber, const Common::String &key, const Common::String &value);
 	void allocateRoomsUpTo(uint roomNumber);
@@ -769,8 +778,6 @@ 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);
@@ -808,7 +815,6 @@ 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);
@@ -878,6 +884,67 @@ private:
 	void scriptOpVerticalPanSet(bool *flags);
 	void scriptOpVerticalPanGet();
 
+	// Schizm ops
+	void scriptOpCallFunction(ScriptArg_t arg);
+
+	void scriptOpMusicStop(ScriptArg_t arg);
+	void scriptOpMusicPlayScore(ScriptArg_t arg);
+	void scriptOpScoreAlways(ScriptArg_t arg);
+	void scriptOpScoreNormal(ScriptArg_t arg);
+	void scriptOpSndPlay(ScriptArg_t arg);
+	void scriptOpSndPlayEx(ScriptArg_t arg);
+	void scriptOpSndPlay3D(ScriptArg_t arg);
+	void scriptOpSndPlaying(ScriptArg_t arg);
+	void scriptOpSndWait(ScriptArg_t arg);
+	void scriptOpSndHalt(ScriptArg_t arg);
+	void scriptOpSndToBack(ScriptArg_t arg);
+	void scriptOpSndStop(ScriptArg_t arg);
+	void scriptOpSndStopAll(ScriptArg_t arg);
+	void scriptOpSndAddRandom(ScriptArg_t arg);
+	void scriptOpSndClearRandom(ScriptArg_t arg);
+	void scriptOpVolumeAdd(ScriptArg_t arg);
+	void scriptOpVolumeChange(ScriptArg_t arg);
+	void scriptOpVolumeDown(ScriptArg_t arg);
+	void scriptOpAnimVolume(ScriptArg_t arg);
+	void scriptOpAnimChange(ScriptArg_t arg);
+	void scriptOpScreenName(ScriptArg_t arg);
+	void scriptOpExtractByte(ScriptArg_t arg);
+	void scriptOpInsertByte(ScriptArg_t arg);
+	void scriptOpString(ScriptArg_t arg);
+	void scriptOpCmpNE(ScriptArg_t arg);
+	void scriptOpCmpLE(ScriptArg_t arg);
+	void scriptOpCmpGE(ScriptArg_t arg);
+	void scriptOpReturn(ScriptArg_t arg);
+	void scriptOpSpeech(ScriptArg_t arg);
+	void scriptOpSpeechEx(ScriptArg_t arg);
+	void scriptOpSpeechTest(ScriptArg_t arg);
+	void scriptOpSay(ScriptArg_t arg);
+	void scriptOpRandomInclusive(ScriptArg_t arg);
+	void scriptOpHeroOut(ScriptArg_t arg);
+	void scriptOpHeroGetPos(ScriptArg_t arg);
+	void scriptOpHeroSetPos(ScriptArg_t arg);
+	void scriptOpHeroGet(ScriptArg_t arg);
+	void scriptOpGarbage(ScriptArg_t arg);
+	void scriptOpGetRoom(ScriptArg_t arg);
+	void scriptOpBitAnd(ScriptArg_t arg);
+	void scriptOpBitOr(ScriptArg_t arg);
+	void scriptOpAngleGet(ScriptArg_t arg);
+	void scriptOpCDGet(ScriptArg_t arg);
+	void scriptOpDisc(ScriptArg_t arg);
+	void scriptOpHidePanel(ScriptArg_t arg);
+	void scriptOpRotateUpdate(ScriptArg_t arg);
+	void scriptOpMul(ScriptArg_t arg);
+	void scriptOpDiv(ScriptArg_t arg);
+	void scriptOpMod(ScriptArg_t arg);
+	void scriptOpCyfraGet(ScriptArg_t arg);
+	void scriptOpPuzzleInit(ScriptArg_t arg);
+	void scriptOpPuzzleCanPress(ScriptArg_t arg);
+	void scriptOpPuzzleDoMove1(ScriptArg_t arg);
+	void scriptOpPuzzleDoMove2(ScriptArg_t arg);
+	void scriptOpPuzzleDone(ScriptArg_t arg);
+	void scriptOpPuzzleWhoWon(ScriptArg_t arg);
+	void scriptOpFn(ScriptArg_t arg);
+
 	Common::Array<Common::SharedPtr<Graphics::WinCursorGroup> > _cursors;		// Cursors indexed as CURSOR_CUR_##
 	Common::Array<Common::SharedPtr<Graphics::WinCursorGroup> > _cursorsShort;	// Cursors indexed as CURSOR_#
 
@@ -947,8 +1014,8 @@ private:
 	Common::Array<Common::SharedPtr<RoomDef> > _roomDefs;
 	Common::SharedPtr<ScriptSet> _scriptSet;
 
-	Common::SharedPtr<Script> _activeScript;
-	uint _scriptNextInstruction;
+	Common::Array<CallStackFrame> _scriptCallStack;
+
 	Common::Array<StackValue> _scriptStack;
 	ScriptEnvironmentVars _scriptEnv;
 
diff --git a/engines/vcruise/script.cpp b/engines/vcruise/script.cpp
index 43d57b16621..5ef4c330312 100644
--- a/engines/vcruise/script.cpp
+++ b/engines/vcruise/script.cpp
@@ -132,19 +132,13 @@ 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();
+
+	Common::Array<ProtoInstruction> instrs;
 };
 
 void ProtoScript::reset() {
 	instrs.clear();
-
-	strings.clear();
-	stringToIndex.clear(true);
 }
 
 struct ScriptNamedInstruction {
@@ -174,7 +168,7 @@ private:
 
 	void codeGenScript(ProtoScript &protoScript, Script &script);
 
-	static uint indexString(ProtoScript &script, const Common::String &str);
+	uint indexString(const Common::String &str);
 
 	enum NumberParsingMode {
 		kNumberParsingDec,
@@ -191,6 +185,8 @@ private:
 	const char *_scrToken;
 	const char *_eroomToken;
 
+	Common::HashMap<Common::String, uint> _stringToIndex;
+
 	IScriptCompilerGlobalState *_gs;
 };
 
@@ -200,12 +196,18 @@ public:
 
 	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;
+	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:
 	Common::HashMap<Common::String, Common::String> _defs;
-	Common::HashMap<Common::String, Common::SharedPtr<Script> > _functions;
+
+	Common::HashMap<Common::String, uint> _functionNameToIndex;
+	Common::Array<Common::SharedPtr<Script> > _functions;
 };
 
 ScriptCompiler::ScriptCompiler(TextParser &parser, const Common::String &blamePath, ScriptDialect dialect, IScriptCompilerGlobalState *gs)
@@ -302,6 +304,11 @@ void ScriptCompiler::expectNumber(uint32 &outNumber) {
 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) {
@@ -344,6 +351,15 @@ void ScriptCompiler::compileScriptSet(ScriptSet *ss) {
 			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) {
@@ -383,7 +399,15 @@ void ScriptCompiler::compileRoomScriptSet(RoomScriptSet *rss) {
 
 			compileFunction(func.get());
 
-			_gs->addFunction(fnName, func);
+			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 might not matter there?  But fnSoundFountain_Stop is called in Room30.
+				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());
 		}
@@ -426,7 +450,7 @@ void ScriptCompiler::compileReahScreenScriptSet(ScreenScriptSet *sss) {
 		} else if (token == "dubbing") {
 			Common::String dubbingName;
 			_parser.expectToken(dubbingName, _blamePath);
-			protoScript.instrs.push_back(ProtoInstruction(ScriptOps::kDubbing, indexString(protoScript, dubbingName)));
+			protoScript.instrs.push_back(ProtoInstruction(ScriptOps::kDubbing, indexString(dubbingName)));
 		} else if (compileInstructionToken(protoScript, token)) {
 			// Nothing
 		} else {
@@ -695,6 +719,7 @@ static ScriptNamedInstruction g_schizmNamedInstructions[] = {
 	{"puzzleDoMove2", ProtoOp::kProtoOpScript, ScriptOps::kPuzzleDoMove2},
 	{"puzzleDone", ProtoOp::kProtoOpScript, ScriptOps::kPuzzleDone},
 	{"puzzleWhoWon", ProtoOp::kProtoOpScript, ScriptOps::kPuzzleWhoWon},
+	{"fn", ProtoOp::kProtoOpScript, ScriptOps::kFn},
 
 	{"+", ProtoOp::kProtoOpScript, ScriptOps::kAdd},
 	{"-", ProtoOp::kProtoOpScript, ScriptOps::kSub},
@@ -761,28 +786,28 @@ bool ScriptCompiler::compileInstructionToken(ProtoScript &script, const Common::
 		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))));
+					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(script, token.substr(3))));
+					script.instrs.push_back(ProtoInstruction(ScriptOps::kValueName, indexString(token.substr(3))));
 					return true;
 				} else
 					return false;
 			}
 		}
 
-		script.instrs.push_back(ProtoInstruction(ScriptOps::kAnimName, indexString(script, token.substr(1))));
+		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(script, token.substr(1))));
+		script.instrs.push_back(ProtoInstruction(ScriptOps::kSoundName, indexString(token.substr(1))));
 		return true;
 	}
 
 	if (_dialect == kScriptDialectReah) {
 		if (token.hasPrefix("CUR_")) {
-			script.instrs.push_back(ProtoInstruction(ScriptOps::kCursorName, indexString(script, token)));
+			script.instrs.push_back(ProtoInstruction(ScriptOps::kCursorName, indexString(token)));
 			return true;
 		}
 	}
@@ -818,15 +843,9 @@ bool ScriptCompiler::compileInstructionToken(ProtoScript &script, const Common::
 			}
 		}
 	} 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));
+			uint fnIndex = indexString(token.substr(1, token.size() - 2));
 			script.instrs.push_back(ProtoInstruction(kProtoOpScript, ScriptOps::kString, fnIndex));
 			return true;
 		}
@@ -844,6 +863,12 @@ bool ScriptCompiler::compileInstructionToken(ProtoScript &script, const Common::
 			}
 		}
 
+		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("!")) {
 			if (compileInstructionToken(script, token.substr(0, token.size() - 1))) {
 				script.instrs.push_back(ProtoInstruction(kProtoOpScript, ScriptOps::kVarGlobalStore, 0));
@@ -878,19 +903,19 @@ bool ScriptCompiler::compileInstructionToken(ProtoScript &script, const Common::
 		}
 
 		if (couldBeScreenName) {
-			script.instrs.push_back(ProtoInstruction(kProtoOpScript, ScriptOps::kScreenName, indexString(script, token)));
+			script.instrs.push_back(ProtoInstruction(kProtoOpScript, ScriptOps::kScreenName, indexString(token)));
 			return true;
 		}
 
 		if (token.hasPrefix("cur")) {
-			script.instrs.push_back(ProtoInstruction(ScriptOps::kCursorName, indexString(script, token)));
+			script.instrs.push_back(ProtoInstruction(ScriptOps::kCursorName, indexString(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)));
+			script.instrs.push_back(ProtoInstruction(kProtoOpScript, ScriptOps::kGarbage, indexString(token)));
 			return true;
 		}
 	}
@@ -1194,17 +1219,13 @@ void ScriptCompiler::codeGenScript(ProtoScript &protoScript, Script &script) {
 			break;
 		}
 	}
-
-	// Commit strings
-	script.strings = Common::move(protoScript.strings);
 }
 
-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);
+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;
 	}
 
@@ -1223,16 +1244,39 @@ const Common::String *ScriptCompilerGlobalState::getTokenReplacement(const Commo
 	return &it->_value;
 }
 
-void ScriptCompilerGlobalState::addFunction(const Common::String &fnName, const Common::SharedPtr<Script> &fn) {
-	_functions.setVal(fnName, fn);
+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;
 }
 
-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>();
+uint ScriptCompilerGlobalState::getNumFunctions() const {
+	return _functionNameToIndex.size();
+}
 
-	return it->_value;
+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];
 }
 
 IScriptCompilerGlobalState::~IScriptCompilerGlobalState() {
diff --git a/engines/vcruise/script.h b/engines/vcruise/script.h
index 6cf002f4144..b7b6d5ee859 100644
--- a/engines/vcruise/script.h
+++ b/engines/vcruise/script.h
@@ -215,6 +215,7 @@ enum ScriptOp {
 	kPuzzleDoMove2,
 	kPuzzleDone,
 	kPuzzleWhoWon,
+	kFn,
 
 	kNumOps,
 };
@@ -232,7 +233,6 @@ struct Instruction {
 
 struct Script {
 	Common::Array<Instruction> instrs;
-	Common::Array<Common::String> strings;
 };
 
 typedef Common::HashMap<uint, Common::SharedPtr<Script> > ScriptMap_t;
@@ -252,6 +252,10 @@ struct RoomScriptSet {
 
 struct ScriptSet {
 	RoomScriptSetMap_t roomScripts;
+
+	Common::Array<Common::SharedPtr<Script> > functions;
+	Common::Array<Common::String> functionNames;
+	Common::Array<Common::String> strings;
 };
 
 struct FunctionDef {
@@ -267,8 +271,12 @@ struct 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;
+	virtual uint getFunctionIndex(const Common::String &fnName) = 0;
+	virtual void setFunction(uint fnIndex, const Common::SharedPtr<Script> &fn) = 0;
+
+	virtual uint getNumFunctions() const = 0;
+	virtual void dumpFunctionNames(Common::Array<Common::String> &fnNames) const = 0;
+	virtual Common::SharedPtr<Script> getFunction(uint fnIndex) const = 0;
 };
 
 Common::SharedPtr<IScriptCompilerGlobalState> createScriptCompilerGlobalState();


Commit: 50a505b6a8c66f786175a2fc62ec6a4cd14c8253
    https://github.com/scummvm/scummvm/commit/50a505b6a8c66f786175a2fc62ec6a4cd14c8253
Author: elasota (ejlasota at gmail.com)
Date: 2023-05-03T23:40:43-04:00

Commit Message:
VCRUISE: Add animation volume handling and score loader.

Changed paths:
    engines/vcruise/runtime.cpp
    engines/vcruise/runtime.h
    engines/vcruise/script.cpp
    engines/vcruise/script.h


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 3ba53a525f2..fae7d37a028 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -573,6 +573,9 @@ void TriggeredOneShot::read(Common::ReadStream *stream) {
 	uniqueSlot = stream->readUint32BE();
 }
 
+ScoreSectionDef::ScoreSectionDef() : volumeOrDurationInSeconds(0) {
+}
+
 StaticAnimParams::StaticAnimParams() : initialDelay(0), repeatDelay(0), lockInteractions(false) {
 }
 
@@ -677,8 +680,8 @@ void SaveGameSnapshot::Sound::read(Common::ReadStream *stream) {
 	params3D.read(stream);
 }
 
-SaveGameSnapshot::SaveGameSnapshot() : roomNumber(0), screenNumber(0), direction(0), escOn(false), musicTrack(0), musicVolume(100), loadedAnimation(0),
-									   animDisplayingFrame(0), listenerX(0), listenerY(0), listenerAngle(0) {
+SaveGameSnapshot::SaveGameSnapshot() : roomNumber(0), screenNumber(0), direction(0), escOn(false), musicTrack(0), musicVolume(100), musicActive(true), loadedAnimation(0),
+									   animDisplayingFrame(0), animVolume(100), listenerX(0), listenerY(0), listenerAngle(0) {
 }
 
 void SaveGameSnapshot::write(Common::WriteStream *stream) const {
@@ -693,8 +696,13 @@ void SaveGameSnapshot::write(Common::WriteStream *stream) const {
 	stream->writeSint32BE(musicTrack);
 	stream->writeUint32BE(musicVolume);
 
+	writeString(stream, scoreTrack);
+	writeString(stream, scoreSection);
+	stream->writeByte(musicActive ? 1 : 0);
+
 	stream->writeUint32BE(loadedAnimation);
 	stream->writeUint32BE(animDisplayingFrame);
+	stream->writeUint32BE(animVolume);
 
 	pendingStaticAnimParams.write(stream);
 	pendingSoundParams3D.write(stream);
@@ -768,9 +776,22 @@ LoadGameOutcome SaveGameSnapshot::read(Common::ReadStream *stream) {
 	else
 		musicVolume = 100;
 
+	if (saveVersion >= 6) {
+		scoreTrack = safeReadString(stream);
+		scoreSection = safeReadString(stream);
+		musicActive = (stream->readByte() != 0);
+	} else {
+		musicActive = true;
+	}
+
 	loadedAnimation = stream->readUint32BE();
 	animDisplayingFrame = stream->readUint32BE();
 
+	if (saveVersion >= 6)
+		animVolume = stream->readUint32BE();
+	else
+		animVolume = 100;
+
 	pendingStaticAnimParams.read(stream);
 	pendingSoundParams3D.read(stream);
 
@@ -840,13 +861,26 @@ LoadGameOutcome SaveGameSnapshot::read(Common::ReadStream *stream) {
 	return kLoadGameOutcomeSucceeded;
 }
 
+Common::String SaveGameSnapshot::safeReadString(Common::ReadStream *stream) {
+	uint len = stream->readUint32BE();
+	if (stream->eos() || stream->err())
+		len = 0;
+
+	return stream->readString(0, len);
+}
+
+void SaveGameSnapshot::writeString(Common::WriteStream *stream, const Common::String &str) {
+	stream->writeUint32BE(str.size());
+	stream->writeString(str);
+}
+
 Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &rootFSNode, VCruiseGameID gameID)
 	: _system(system), _mixer(mixer), _roomNumber(1), _screenNumber(0), _direction(0), _haveHorizPanAnimations(false), _loadedRoomNumber(0), _activeScreenNumber(0),
 	  _gameState(kGameStateBoot), _gameID(gameID), _havePendingScreenChange(false), _forceScreenChange(false), _havePendingReturnToIdleState(false), _havePendingCompletionCheck(false),
 	  _havePendingPlayAmbientSounds(false), _ambientSoundFinishTime(0), _escOn(false), _debugMode(false), _fastAnimationMode(false),
-	  _musicTrack(0), _musicVolume(100), _musicVolumeRampStartTime(0), _musicVolumeRampStartVolume(0), _musicVolumeRampRatePerMSec(0), _musicVolumeRampEnd(0),
+	  _musicTrack(0), _musicActive(true), _scoreSectionEndTime(0), _musicVolume(100), _musicVolumeRampStartTime(0), _musicVolumeRampStartVolume(0), _musicVolumeRampRatePerMSec(0), _musicVolumeRampEnd(0),
 	  _panoramaDirectionFlags(0),
-	  _loadedAnimation(0), _loadedAnimationHasSound(false), _animPendingDecodeFrame(0), _animDisplayingFrame(0), _animFirstFrame(0), _animLastFrame(0), _animStopFrame(0),
+	  _loadedAnimation(0), _loadedAnimationHasSound(false), _animPendingDecodeFrame(0), _animDisplayingFrame(0), _animFirstFrame(0), _animLastFrame(0), _animStopFrame(0), _animVolume(100),
 	  _animStartTime(0), _animFramesDecoded(0), _animDecoderState(kAnimDecoderStateStopped),
 	  _animPlayWhileIdle(false), _idleLockInteractions(false), _idleIsOnInteraction(false), _idleHaveClickInteraction(false), _idleHaveDragInteraction(false), _idleInteractionID(0), _haveIdleStaticAnimation(false),
 	  _inGameMenuState(kInGameMenuStateInvisible), _inGameMenuActiveElement(0), _inGameMenuButtonActive {false, false, false, false, false},
@@ -1039,6 +1073,11 @@ bool Runtime::bootGame(bool newGame) {
 	findWaves();
 	debug(1, "Waves indexed OK");
 
+	if (_gameID == GID_SCHIZM) {
+		loadScore();
+		debug(1, "Score loaded OK");
+	}
+
 	_trayBackgroundGraphic = loadGraphic("Pocket", true);
 	_trayHighlightGraphic = loadGraphic("Select", true);
 	_trayCompassGraphic = loadGraphic("Select_1", true);
@@ -1046,8 +1085,13 @@ bool Runtime::bootGame(bool newGame) {
 
 	_gameState = kGameStateIdle;
 
-	if (newGame)
-		changeToScreen(1, 0xb1);
+	if (newGame) {
+		// TODO: Implement menus and go to b1 in Schizm instead
+		if (_gameID == GID_SCHIZM)
+			changeToScreen(1, 0xb0);
+		else
+			changeToScreen(1, 0xb1);
+	}
 
 	Common::Language lang = Common::parseLanguage(ConfMan.get("language"));
 
@@ -2200,6 +2244,52 @@ void Runtime::findWaves() {
 	}
 }
 
+void Runtime::loadScore() {
+	Common::INIFile scoreINI;
+	if (scoreINI.loadFromFile("Sfx/score.ini")) {
+
+		for (const Common::INIFile::Section &section : scoreINI.getSections()) {
+			ScoreTrackDef &trackDef = _scoreDefs[section.name];
+
+			for (const Common::INIFile::KeyValue &kv : section.keys) {
+
+				uint32 firstSpacePos = kv.value.find(' ', 0);
+				if (firstSpacePos != Common::String::npos) {
+					uint32 secondSpacePos = kv.value.find(' ', firstSpacePos + 1);
+
+					if (secondSpacePos == Common::String::npos) {
+						// Silent section
+						Common::String durationSlice = kv.value.substr(0, firstSpacePos);
+						Common::String nextSectionSlice = kv.value.substr(firstSpacePos + 1);
+
+						uint duration = 0;
+						if (sscanf(durationSlice.c_str(), "%u", &duration) == 1) {
+							ScoreSectionDef &sectionDef = trackDef.sections[kv.key];
+							sectionDef.nextSection = nextSectionSlice;
+							sectionDef.volumeOrDurationInSeconds = duration;
+						} else
+							warning("Couldn't parse score silent section duration");
+					} else {
+						Common::String fileNameSlice = kv.value.substr(0, firstSpacePos);
+						Common::String volumeSlice = kv.value.substr(firstSpacePos + 1, secondSpacePos - firstSpacePos - 1);
+						Common::String nextSectionSlice = kv.value.substr(secondSpacePos + 1);
+
+						int volume = 0;
+						if (sscanf(volumeSlice.c_str(), "%i", &volume) == 1) {
+							ScoreSectionDef &sectionDef = trackDef.sections[kv.key];
+							sectionDef.nextSection = nextSectionSlice;
+							sectionDef.volumeOrDurationInSeconds = normalizeSoundVolume(volume);
+							sectionDef.musicFileName = fileNameSlice;
+						} else
+							warning("Couldn't parse score section volume");
+					}
+				}
+			}
+		}
+	} else
+		warning("Couldn't load music score");
+}
+
 Common::SharedPtr<SoundInstance> Runtime::loadWave(const Common::String &soundName, uint soundID, const Common::ArchiveMemberPtr &archiveMemberPtr) {
 	for (const Common::SharedPtr<SoundInstance> &activeSound : _activeSounds) {
 		if (activeSound->name == soundName)
@@ -2312,6 +2402,15 @@ void Runtime::resolveSoundByName(const Common::String &soundName, bool load, Sta
 	}
 }
 
+SoundInstance *Runtime::resolveSoundByID(uint soundID) {
+	for (const Common::SharedPtr<SoundInstance> &snd : _activeSounds) {
+		if (snd->id == soundID)
+			return snd.get();
+	}
+
+	return nullptr;
+}
+
 void Runtime::resolveSoundByNameOrID(const StackValue &stackValue, bool load, StackInt_t &outSoundID, SoundInstance *&outWave) {
 	outSoundID = 0;
 	outWave = nullptr;
@@ -2721,6 +2820,9 @@ void Runtime::changeMusicTrack(int track) {
 	_musicPlayer.reset();
 	_musicTrack = track;
 
+	if (!_musicActive)
+		return;
+
 	Common::String wavFileName = Common::String::format("Sfx/Music-%02i.wav", static_cast<int>(track));
 	Common::File *wavFile = new Common::File();
 	if (wavFile->open(wavFileName)) {
@@ -2774,6 +2876,8 @@ void Runtime::changeAnimation(const AnimationDef &animDef, uint initialFrame, bo
 			delete aviFile;
 		}
 
+		applyAnimationVolume();
+
 		Common::String sfxFileName = Common::String::format("Sfx/Anim%04i.sfx", animFile);
 		Common::File sfxFile;
 
@@ -2837,6 +2941,15 @@ void Runtime::changeAnimation(const AnimationDef &animDef, uint initialFrame, bo
 	debug(1, "Animation last frame set to %u", animDef.lastFrame);
 }
 
+void Runtime::applyAnimationVolume() {
+	if (_animDecoder) {
+		uint volume = _animVolume * static_cast<uint>(Audio::Mixer::kMaxChannelVolume) / 100u;
+		if (volume > Audio::Mixer::kMaxChannelVolume)
+			volume = Audio::Mixer::kMaxChannelVolume;
+		_animDecoder->setVolume(volume);
+	}
+}
+
 void Runtime::setSound3DParameters(SoundInstance &snd, int32 x, int32 y, const SoundParams3D &soundParams3D) {
 	snd.x = x;
 	snd.y = y;
@@ -3273,6 +3386,16 @@ void Runtime::triggerAmbientSounds() {
 		snd.sceneChangesRemaining--;
 }
 
+uint Runtime::normalizeSoundVolume(StackInt_t arg) const {
+	int32 adjustedVol = static_cast<int32>(arg) + 50;
+	if (adjustedVol < 0)
+		return 0;
+	if (adjustedVol > 100)
+		return 100;
+
+	return static_cast<uint>(adjustedVol);
+}
+
 AnimationDef Runtime::stackArgsToAnimDef(const StackInt_t *args) const {
 	AnimationDef def;
 	def.animNum = args[0];
@@ -4131,6 +4254,7 @@ void Runtime::recordSaveGameSnapshot() {
 	snapshot->escOn = _escOn;
 
 	snapshot->musicTrack = _musicTrack;
+	snapshot->musicActive = _musicActive;
 
 	snapshot->musicVolume = _musicVolume;
 
@@ -4222,6 +4346,8 @@ void Runtime::restoreSaveGameSnapshot() {
 	_musicVolumeRampRatePerMSec = 0;
 	_musicVolumeRampEnd = _musicVolume;
 
+	_musicActive = _saveGame->musicActive;
+
 	changeMusicTrack(_saveGame->musicTrack);
 
 	// Stop all sounds since the player instances are stored in the sound cache.
@@ -5662,11 +5788,22 @@ void Runtime::scriptOpJump(ScriptArg_t arg) {
 	_scriptCallStack.back()._nextInstruction = arg;
 }
 
-OPCODE_STUB(MusicStop)
-OPCODE_STUB(MusicPlayScore)
+void Runtime::scriptOpMusicStop(ScriptArg_t arg) {
+	_musicPlayer.reset();
+	_musicActive = false;
+}
+
+void Runtime::scriptOpMusicPlayScore(ScriptArg_t arg) {
+	error("MusicPlayScore opcode not implemented");
+}
+
 OPCODE_STUB(ScoreAlways)
 OPCODE_STUB(ScoreNormal)
-OPCODE_STUB(SndPlay)
+
+void Runtime::scriptOpSndPlay(ScriptArg_t arg) {
+	scriptOpSoundL1(arg);
+}
+
 OPCODE_STUB(SndPlayEx)
 OPCODE_STUB(SndPlay3D)
 OPCODE_STUB(SndPlaying)
@@ -5678,11 +5815,48 @@ OPCODE_STUB(SndStopAll)
 OPCODE_STUB(SndAddRandom)
 OPCODE_STUB(SndClearRandom)
 OPCODE_STUB(VolumeAdd)
-OPCODE_STUB(VolumeChange)
+
+void Runtime::scriptOpVolumeChange(ScriptArg_t arg) {
+	TAKE_STACK_INT(3);
+
+	SoundInstance *cachedSound = resolveSoundByID(static_cast<uint>(stackArgs[0]));
+
+	if (cachedSound)
+		triggerSoundRamp(*cachedSound, stackArgs[1] * 100, stackArgs[2], false);
+}
+
 OPCODE_STUB(VolumeDown)
-OPCODE_STUB(AnimVolume)
-OPCODE_STUB(AnimChange)
-OPCODE_STUB(ScreenName)
+
+void Runtime::scriptOpAnimVolume(ScriptArg_t arg) {
+	TAKE_STACK_INT(1);
+
+	_animVolume = normalizeSoundVolume(stackArgs[0]);
+
+	applyAnimationVolume();
+}
+
+void Runtime::scriptOpAnimChange(ScriptArg_t arg) {
+	TAKE_STACK_INT(2);
+
+	(void)stackArgs;
+
+	warning("animChange opcode isn't implemented yet");
+}
+
+void Runtime::scriptOpScreenName(ScriptArg_t arg) {
+	const Common::String &scrName = _scriptSet->strings[arg];
+	RoomScriptSetMap_t::const_iterator scriptSetIt = _scriptSet->roomScripts.find(_roomNumber);
+	if (scriptSetIt == _scriptSet->roomScripts.end())
+		error("Couldn't resolve room number to find screen name: '%s'", scrName.c_str());
+
+	const RoomScriptSet *rss = scriptSetIt->_value.get();
+
+	ScreenNameMap_t::const_iterator screenNameIt = rss->screenNames.find(scrName);
+	if (screenNameIt == rss->screenNames.end())
+		error("Couldn't resolve screen name '%s'", scrName.c_str());
+
+	_scriptStack.push_back(StackValue(static_cast<StackInt_t>(screenNameIt->_value)));
+}
 
 void Runtime::scriptOpExtractByte(ScriptArg_t arg) {
 	TAKE_STACK_INT(2);
@@ -5691,7 +5865,11 @@ void Runtime::scriptOpExtractByte(ScriptArg_t arg) {
 }
 
 OPCODE_STUB(InsertByte)
-OPCODE_STUB(String)
+
+void Runtime::scriptOpString(ScriptArg_t arg) {
+	_scriptStack.push_back(StackValue(_scriptSet->strings[arg]));
+}
+
 OPCODE_STUB(CmpNE)
 OPCODE_STUB(CmpLE)
 OPCODE_STUB(CmpGE)
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index 9680f4aa33f..552f8b80282 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -271,6 +271,20 @@ struct TriggeredOneShot {
 	void read(Common::ReadStream *stream);
 };
 
+struct ScoreSectionDef {
+	ScoreSectionDef();
+
+	Common::String musicFileName;	// If empty, this is silent
+	Common::String nextSection;
+	uint32 volumeOrDurationInSeconds;
+};
+
+struct ScoreTrackDef {
+	typedef Common::HashMap<Common::String, ScoreSectionDef> ScoreSectionMap_t;
+
+	ScoreSectionMap_t sections;
+};
+
 struct StaticAnimParams {
 	StaticAnimParams();
 
@@ -346,7 +360,7 @@ struct SaveGameSnapshot {
 	LoadGameOutcome read(Common::ReadStream *stream);
 
 	static const uint kSaveGameIdentifier = 0x53566372;
-	static const uint kSaveGameCurrentVersion = 5;
+	static const uint kSaveGameCurrentVersion = 6;
 	static const uint kSaveGameEarliestSupportedVersion = 2;
 
 	struct InventoryItem {
@@ -380,6 +394,9 @@ struct SaveGameSnapshot {
 		void read(Common::ReadStream *stream);
 	};
 
+	static Common::String safeReadString(Common::ReadStream *stream);
+	static void writeString(Common::WriteStream *stream, const Common::String &str);
+
 	uint roomNumber;
 	uint screenNumber;
 	uint direction;
@@ -387,10 +404,15 @@ struct SaveGameSnapshot {
 	bool escOn;
 	int musicTrack;
 
+	Common::String scoreTrack;
+	Common::String scoreSection;
+	bool musicActive;
+
 	uint musicVolume;
 
 	uint loadedAnimation;
 	uint animDisplayingFrame;
+	uint animVolume;
 
 	StaticAnimParams pendingStaticAnimParams;
 	SoundParams3D pendingSoundParams3D;
@@ -689,10 +711,12 @@ private:
 
 	void loadIndex();
 	void findWaves();
+	void loadScore();
 	Common::SharedPtr<SoundInstance> loadWave(const Common::String &soundName, uint soundID, const Common::ArchiveMemberPtr &archiveMemberPtr);
 	SoundCache *loadCache(SoundInstance &sound);
-	void resolveSoundByName(const Common::String &soundName, bool resolveSoundByName, StackInt_t &outSoundID, SoundInstance *&outWave);
-	void resolveSoundByNameOrID(const StackValue &stackValue, bool resolveSoundByName, StackInt_t &outSoundID, SoundInstance *&outWave);
+	void resolveSoundByName(const Common::String &soundName, bool load, StackInt_t &outSoundID, SoundInstance *&outWave);
+	SoundInstance *resolveSoundByID(uint soundID);
+	void resolveSoundByNameOrID(const StackValue &stackValue, bool load, StackInt_t &outSoundID, SoundInstance *&outWave);
 
 	void changeToScreen(uint roomNumber, uint screenNumber);
 	void returnToIdleState();
@@ -708,6 +732,7 @@ private:
 	void changeAnimation(const AnimationDef &animDef, bool consumeFPSOverride);
 	void changeAnimation(const AnimationDef &animDef, uint initialFrame, bool consumeFPSOverride);
 	void changeAnimation(const AnimationDef &animDef, uint initialFrame, bool consumeFPSOverride, const Fraction &defaultFrameRate);
+	void applyAnimationVolume();
 
 	void setSound3DParameters(SoundInstance &sound, int32 x, int32 y, const SoundParams3D &soundParams3D);
 	void triggerSound(bool looping, SoundInstance &sound, uint volume, int32 balance, bool is3D, bool isSpeech);
@@ -718,6 +743,7 @@ private:
 	void update3DSounds();
 	bool computeEffectiveVolumeAndBalance(SoundInstance &snd);
 	void triggerAmbientSounds();
+	uint normalizeSoundVolume(StackInt_t arg) const;
 
 	void triggerWaveSubtitles(const SoundInstance &sound, const Common::String &id);
 	void stopSubtitles();
@@ -1024,6 +1050,12 @@ private:
 	Common::SharedPtr<AudioPlayer> _musicPlayer;
 	int _musicTrack;
 	uint _musicVolume;
+	bool _musicActive;
+
+	Common::String _scoreTrack;
+	Common::String _scoreSection;
+	uint32 _scoreSectionEndTime;
+	Common::HashMap<Common::String, ScoreTrackDef> _scoreDefs;
 
 	uint32 _musicVolumeRampStartTime;
 	uint _musicVolumeRampStartVolume;
@@ -1040,6 +1072,7 @@ private:
 	uint _animFirstFrame;
 	uint _animLastFrame;
 	uint _animStopFrame;
+	uint _animVolume;
 	Fraction _animFrameRateLock;
 	Common::Rect _animConstraintRect;
 	uint32 _animStartTime;
diff --git a/engines/vcruise/script.cpp b/engines/vcruise/script.cpp
index 5ef4c330312..158c66e46db 100644
--- a/engines/vcruise/script.cpp
+++ b/engines/vcruise/script.cpp
@@ -375,8 +375,15 @@ void ScriptCompiler::compileRoomScriptSet(RoomScriptSet *rss) {
 			Common::SharedPtr<ScreenScriptSet> sss(new ScreenScriptSet());
 			if (_dialect == kScriptDialectReah)
 				compileReahScreenScriptSet(sss.get());
-			else if (_dialect == kScriptDialectSchizm)
+			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())
@@ -468,11 +475,6 @@ void ScriptCompiler::compileSchizmScreenScriptSet(ScreenScriptSet *sss) {
 
 	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);
diff --git a/engines/vcruise/script.h b/engines/vcruise/script.h
index b7b6d5ee859..94b996ebdcd 100644
--- a/engines/vcruise/script.h
+++ b/engines/vcruise/script.h
@@ -238,16 +238,16 @@ struct Script {
 typedef Common::HashMap<uint, Common::SharedPtr<Script> > ScriptMap_t;
 typedef Common::HashMap<uint, Common::SharedPtr<ScreenScriptSet> > ScreenScriptSetMap_t;
 typedef Common::HashMap<uint, Common::SharedPtr<RoomScriptSet> > RoomScriptSetMap_t;
+typedef Common::HashMap<Common::String, uint> ScreenNameMap_t;
 
 struct ScreenScriptSet {
 	Common::SharedPtr<Script> entryScript;
 	ScriptMap_t interactionScripts;
-
-	Common::String screenName;	// Only in Schizm
 };
 
 struct RoomScriptSet {
 	ScreenScriptSetMap_t screenScripts;
+	ScreenNameMap_t screenNames;
 };
 
 struct ScriptSet {


Commit: cc0dcb1f717dd837cd506e131d573c1d0f9a50b2
    https://github.com/scummvm/scummvm/commit/cc0dcb1f717dd837cd506e131d573c1d0f9a50b2
Author: elasota (ejlasota at gmail.com)
Date: 2023-05-03T23:40:43-04:00

Commit Message:
VCRUISE: Fix up some initial Schizm things.

Changed paths:
    engines/vcruise/runtime.cpp
    engines/vcruise/runtime.h
    engines/vcruise/script.cpp


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index fae7d37a028..53909b516ce 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -41,6 +41,8 @@
 #include "image/bmp.h"
 
 #include "audio/decoders/wave.h"
+#include "audio/decoders/vorbis.h"
+
 #include "audio/audiostream.h"
 
 #include "video/avi_decoder.h"
@@ -971,19 +973,29 @@ void Runtime::loadCursors(const char *exeName) {
 		_namedCursors["CUR_PRZOD"] = 1;		// Przod = forward
 
 		// CUR_ZOSTAW is in the executable memory but appears to be unused
-
-		_panCursors[kPanCursorDraggableHoriz | kPanCursorDraggableUp] = 2;
-		_panCursors[kPanCursorDraggableHoriz | kPanCursorDraggableDown] = 3;
-		_panCursors[kPanCursorDraggableHoriz] = 4;
-		_panCursors[kPanCursorDraggableHoriz | kPanCursorDirectionRight] = 5;
-		_panCursors[kPanCursorDraggableHoriz | kPanCursorDirectionLeft] = 6;
-		_panCursors[kPanCursorDraggableUp] = 7;
-		_panCursors[kPanCursorDraggableDown] = 8;
-		_panCursors[kPanCursorDraggableUp | kPanCursorDirectionUp] = 9;
-		_panCursors[kPanCursorDraggableDown | kPanCursorDirectionDown] = 10;
-		_panCursors[kPanCursorDraggableUp | kPanCursorDraggableDown] = 11;
-		_panCursors[kPanCursorDraggableHoriz | kPanCursorDraggableUp | kPanCursorDraggableDown] = 12;
 	}
+
+	if (_gameID == GID_SCHIZM) {
+		_namedCursors["curPress"] = 16;
+		_namedCursors["curLookFor"] = 21;
+		_namedCursors["curForward"] = 1;
+		_namedCursors["curBack"] = 13;
+		_namedCursors["curNothing"] = 0;
+		_namedCursors["curPickUp"] = 90;
+		_namedCursors["curDrop"] = 91;
+	}
+
+	_panCursors[kPanCursorDraggableHoriz | kPanCursorDraggableUp] = 2;
+	_panCursors[kPanCursorDraggableHoriz | kPanCursorDraggableDown] = 3;
+	_panCursors[kPanCursorDraggableHoriz] = 4;
+	_panCursors[kPanCursorDraggableHoriz | kPanCursorDirectionRight] = 5;
+	_panCursors[kPanCursorDraggableHoriz | kPanCursorDirectionLeft] = 6;
+	_panCursors[kPanCursorDraggableUp] = 7;
+	_panCursors[kPanCursorDraggableDown] = 8;
+	_panCursors[kPanCursorDraggableUp | kPanCursorDirectionUp] = 9;
+	_panCursors[kPanCursorDraggableDown | kPanCursorDirectionDown] = 10;
+	_panCursors[kPanCursorDraggableUp | kPanCursorDraggableDown] = 11;
+	_panCursors[kPanCursorDraggableHoriz | kPanCursorDraggableUp | kPanCursorDraggableDown] = 12;
 }
 
 void Runtime::setDebugMode(bool debugMode) {
@@ -2838,6 +2850,44 @@ void Runtime::changeMusicTrack(int track) {
 	}
 }
 
+void Runtime::startScoreSection() {
+	_musicPlayer.reset();
+
+	if (!_musicActive)
+		return;
+
+	Common::HashMap<Common::String, ScoreTrackDef>::const_iterator trackIt = _scoreDefs.find(_scoreTrack);
+	if (trackIt != _scoreDefs.end()) {
+		const ScoreTrackDef::ScoreSectionMap_t &sectionMap = trackIt->_value.sections;
+
+		ScoreTrackDef::ScoreSectionMap_t::const_iterator sectionIt = sectionMap.find(_scoreSection);
+		if (sectionIt != sectionMap.end()) {
+			const ScoreSectionDef &sectionDef = sectionIt->_value;
+
+			if (sectionDef.musicFileName.empty()) {
+				_scoreSectionEndTime = sectionDef.volumeOrDurationInSeconds * 1000u + g_system->getMillis();
+			} else {
+				Common::String trackFileName = Common::String("Sfx/") + sectionDef.musicFileName;
+
+				Common::File *trackFile = new Common::File();
+				if (trackFile->open(trackFileName)) {
+					if (Audio::SeekableAudioStream *audioStream = Audio::makeVorbisStream(trackFile, DisposeAfterUse::YES)) {
+						_musicPlayer.reset(new AudioPlayer(_mixer, Common::SharedPtr<Audio::AudioStream>(audioStream), Audio::Mixer::kMusicSoundType));
+						_musicPlayer->play(sectionDef.volumeOrDurationInSeconds, 0);
+
+						_scoreSectionEndTime = static_cast<uint32>(audioStream->getLength().msecs()) + g_system->getMillis();
+					} else {
+						warning("Couldn't create Vorbis stream for music file '%s'", trackFileName.c_str());
+						delete trackFile;
+					}
+				} else {
+					warning("Music file '%s' is missing", trackFileName.c_str());
+				}
+			}
+		}
+	}
+}
+
 void Runtime::changeAnimation(const AnimationDef &animDef, bool consumeFPSOverride) {
 	changeAnimation(animDef, animDef.firstFrame, consumeFPSOverride);
 }
@@ -5794,7 +5844,13 @@ void Runtime::scriptOpMusicStop(ScriptArg_t arg) {
 }
 
 void Runtime::scriptOpMusicPlayScore(ScriptArg_t arg) {
-	error("MusicPlayScore opcode not implemented");
+	TAKE_STACK_STR(2);
+
+	_scoreTrack = stackArgs[0];
+	_scoreSection = stackArgs[1];
+	_musicActive = true;
+
+	startScoreSection();
 }
 
 OPCODE_STUB(ScoreAlways)
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index 552f8b80282..c249a5af6eb 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -729,6 +729,8 @@ private:
 	void loadFrameData2(Common::SeekableReadStream *stream);
 
 	void changeMusicTrack(int musicID);
+	void startScoreSection();
+
 	void changeAnimation(const AnimationDef &animDef, bool consumeFPSOverride);
 	void changeAnimation(const AnimationDef &animDef, uint initialFrame, bool consumeFPSOverride);
 	void changeAnimation(const AnimationDef &animDef, uint initialFrame, bool consumeFPSOverride, const Fraction &defaultFrameRate);
diff --git a/engines/vcruise/script.cpp b/engines/vcruise/script.cpp
index 158c66e46db..4975a50e854 100644
--- a/engines/vcruise/script.cpp
+++ b/engines/vcruise/script.cpp
@@ -911,6 +911,7 @@ bool ScriptCompiler::compileInstructionToken(ProtoScript &script, const Common::
 
 		if (token.hasPrefix("cur")) {
 			script.instrs.push_back(ProtoInstruction(ScriptOps::kCursorName, indexString(token)));
+			script.instrs.push_back(ProtoInstruction(ScriptOps::kSetCursor, 0));
 			return true;
 		}
 


Commit: 0188660e82e87e133ed73ff8804ed381dd14855e
    https://github.com/scummvm/scummvm/commit/0188660e82e87e133ed73ff8804ed381dd14855e
Author: elasota (ejlasota at gmail.com)
Date: 2023-05-03T23:40:43-04:00

Commit Message:
VCRUISE: Fix up volume calculations for Schizm.

Changed paths:
    engines/vcruise/runtime.cpp
    engines/vcruise/runtime.h
    engines/vcruise/vcruise.cpp


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 53909b516ce..cefe58121b1 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -533,7 +533,7 @@ void RandomAmbientSound::write(Common::WriteStream *stream) const {
 	stream->writeUint32BE(name.size());
 	stream->writeString(name);
 
-	stream->writeUint32BE(volume);
+	stream->writeSint32BE(volume);
 	stream->writeSint32BE(balance);
 
 	stream->writeUint32BE(frequency);
@@ -547,7 +547,7 @@ void RandomAmbientSound::read(Common::ReadStream *stream) {
 
 	name = stream->readString(0, nameLen);
 
-	volume = stream->readUint32BE();
+	volume = stream->readSint32BE();
 	balance = stream->readSint32BE();
 
 	frequency = stream->readUint32BE();
@@ -647,7 +647,7 @@ void SaveGameSnapshot::Sound::write(Common::WriteStream *stream) const {
 	stream->writeString(name);
 
 	stream->writeUint32BE(id);
-	stream->writeUint32BE(volume);
+	stream->writeSint32BE(volume);
 	stream->writeSint32BE(balance);
 
 	stream->writeByte(is3D ? 1 : 0);
@@ -669,7 +669,7 @@ void SaveGameSnapshot::Sound::read(Common::ReadStream *stream) {
 	name = stream->readString(0, nameLen);
 
 	id = stream->readUint32BE();
-	volume = stream->readUint32BE();
+	volume = stream->readSint32BE();
 	balance = stream->readSint32BE();
 
 	is3D = (stream->readByte() != 0);
@@ -696,7 +696,7 @@ void SaveGameSnapshot::write(Common::WriteStream *stream) const {
 
 	stream->writeByte(escOn ? 1 : 0);
 	stream->writeSint32BE(musicTrack);
-	stream->writeUint32BE(musicVolume);
+	stream->writeSint32BE(musicVolume);
 
 	writeString(stream, scoreTrack);
 	writeString(stream, scoreSection);
@@ -704,7 +704,7 @@ void SaveGameSnapshot::write(Common::WriteStream *stream) const {
 
 	stream->writeUint32BE(loadedAnimation);
 	stream->writeUint32BE(animDisplayingFrame);
-	stream->writeUint32BE(animVolume);
+	stream->writeSint32BE(animVolume);
 
 	pendingStaticAnimParams.write(stream);
 	pendingSoundParams3D.write(stream);
@@ -774,7 +774,7 @@ LoadGameOutcome SaveGameSnapshot::read(Common::ReadStream *stream) {
 	musicTrack = stream->readSint32BE();
 
 	if (saveVersion >= 5)
-		musicVolume = stream->readUint32BE();
+		musicVolume = stream->readSint32BE();
 	else
 		musicVolume = 100;
 
@@ -790,7 +790,7 @@ LoadGameOutcome SaveGameSnapshot::read(Common::ReadStream *stream) {
 	animDisplayingFrame = stream->readUint32BE();
 
 	if (saveVersion >= 6)
-		animVolume = stream->readUint32BE();
+		animVolume = stream->readSint32BE();
 	else
 		animVolume = 100;
 
@@ -880,9 +880,9 @@ Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &roo
 	: _system(system), _mixer(mixer), _roomNumber(1), _screenNumber(0), _direction(0), _haveHorizPanAnimations(false), _loadedRoomNumber(0), _activeScreenNumber(0),
 	  _gameState(kGameStateBoot), _gameID(gameID), _havePendingScreenChange(false), _forceScreenChange(false), _havePendingReturnToIdleState(false), _havePendingCompletionCheck(false),
 	  _havePendingPlayAmbientSounds(false), _ambientSoundFinishTime(0), _escOn(false), _debugMode(false), _fastAnimationMode(false),
-	  _musicTrack(0), _musicActive(true), _scoreSectionEndTime(0), _musicVolume(100), _musicVolumeRampStartTime(0), _musicVolumeRampStartVolume(0), _musicVolumeRampRatePerMSec(0), _musicVolumeRampEnd(0),
+	  _musicTrack(0), _musicActive(true), _scoreSectionEndTime(0), _musicVolume(getDefaultSoundVolume()), _musicVolumeRampStartTime(0), _musicVolumeRampStartVolume(0), _musicVolumeRampRatePerMSec(0), _musicVolumeRampEnd(0),
 	  _panoramaDirectionFlags(0),
-	  _loadedAnimation(0), _loadedAnimationHasSound(false), _animPendingDecodeFrame(0), _animDisplayingFrame(0), _animFirstFrame(0), _animLastFrame(0), _animStopFrame(0), _animVolume(100),
+	  _loadedAnimation(0), _loadedAnimationHasSound(false), _animPendingDecodeFrame(0), _animDisplayingFrame(0), _animFirstFrame(0), _animLastFrame(0), _animStopFrame(0), _animVolume(getDefaultSoundVolume()),
 	  _animStartTime(0), _animFramesDecoded(0), _animDecoderState(kAnimDecoderStateStopped),
 	  _animPlayWhileIdle(false), _idleLockInteractions(false), _idleIsOnInteraction(false), _idleHaveClickInteraction(false), _idleHaveDragInteraction(false), _idleInteractionID(0), _haveIdleStaticAnimation(false),
 	  _inGameMenuState(kInGameMenuStateInvisible), _inGameMenuActiveElement(0), _inGameMenuButtonActive {false, false, false, false, false},
@@ -916,6 +916,9 @@ Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &roo
 		warning("Couldn't load subtitle font, subtitles will be disabled");
 
 	_menuInterface.reset(new RuntimeMenuInterface(this));
+
+	for (int32 i = 0; i < 49; i++)
+		_dbToVolume[i] = decibelsToLinear(i - 49, Audio::Mixer::kMaxChannelVolume, Audio::Mixer::kMaxChannelVolume);
 }
 
 Runtime::~Runtime() {
@@ -1099,9 +1102,10 @@ bool Runtime::bootGame(bool newGame) {
 
 	if (newGame) {
 		// TODO: Implement menus and go to b1 in Schizm instead
-		if (_gameID == GID_SCHIZM)
+		if (_gameID == GID_SCHIZM) {
 			changeToScreen(1, 0xb0);
-		else
+			_isInGame = true;
+		} else
 			changeToScreen(1, 0xb1);
 	}
 
@@ -1186,6 +1190,8 @@ bool Runtime::bootGame(bool newGame) {
 			_uiGraphics[i] = loadGraphic(Common::String::format("Image%03u", static_cast<uint>(_languageIndex * 100u + i)), false);
 			if (_languageIndex != 0 && !_uiGraphics[i])
 				_uiGraphics[i] = loadGraphic(Common::String::format("Image%03u", static_cast<uint>(i)), false);
+		} else if (_gameID == GID_SCHIZM) {
+			_uiGraphics[i] = loadGraphic(Common::String::format("Data%03u", i), false);
 		}
 	}
 
@@ -2290,7 +2296,7 @@ void Runtime::loadScore() {
 						if (sscanf(volumeSlice.c_str(), "%i", &volume) == 1) {
 							ScoreSectionDef &sectionDef = trackDef.sections[kv.key];
 							sectionDef.nextSection = nextSectionSlice;
-							sectionDef.volumeOrDurationInSeconds = normalizeSoundVolume(volume);
+							sectionDef.volumeOrDurationInSeconds = volume;
 							sectionDef.musicFileName = fileNameSlice;
 						} else
 							warning("Couldn't parse score section volume");
@@ -2842,7 +2848,7 @@ void Runtime::changeMusicTrack(int track) {
 			Common::SharedPtr<Audio::AudioStream> loopingStream(Audio::makeLoopingAudioStream(audioStream, 0));
 
 			_musicPlayer.reset(new AudioPlayer(_mixer, loopingStream, Audio::Mixer::kMusicSoundType));
-			_musicPlayer->play(_musicVolume, 0);
+			_musicPlayer->play(applyVolumeScale(_musicVolume), 0);
 		}
 	} else {
 		warning("Music file '%s' is missing", wavFileName.c_str());
@@ -2873,7 +2879,7 @@ void Runtime::startScoreSection() {
 				if (trackFile->open(trackFileName)) {
 					if (Audio::SeekableAudioStream *audioStream = Audio::makeVorbisStream(trackFile, DisposeAfterUse::YES)) {
 						_musicPlayer.reset(new AudioPlayer(_mixer, Common::SharedPtr<Audio::AudioStream>(audioStream), Audio::Mixer::kMusicSoundType));
-						_musicPlayer->play(sectionDef.volumeOrDurationInSeconds, 0);
+						_musicPlayer->play(applyVolumeScale(sectionDef.volumeOrDurationInSeconds), 0);
 
 						_scoreSectionEndTime = static_cast<uint32>(audioStream->getLength().msecs()) + g_system->getMillis();
 					} else {
@@ -2993,10 +2999,7 @@ void Runtime::changeAnimation(const AnimationDef &animDef, uint initialFrame, bo
 
 void Runtime::applyAnimationVolume() {
 	if (_animDecoder) {
-		uint volume = _animVolume * static_cast<uint>(Audio::Mixer::kMaxChannelVolume) / 100u;
-		if (volume > Audio::Mixer::kMaxChannelVolume)
-			volume = Audio::Mixer::kMaxChannelVolume;
-		_animDecoder->setVolume(volume);
+		_animDecoder->setVolume(applyVolumeScale(_animVolume));
 	}
 }
 
@@ -3006,7 +3009,7 @@ void Runtime::setSound3DParameters(SoundInstance &snd, int32 x, int32 y, const S
 	snd.params3D = soundParams3D;
 }
 
-void Runtime::triggerSound(bool looping, SoundInstance &snd, uint volume, int32 balance, bool is3D, bool isSpeech) {
+void Runtime::triggerSound(bool looping, SoundInstance &snd, int32 volume, int32 balance, bool is3D, bool isSpeech) {
 	snd.volume = volume;
 	snd.balance = balance;
 	snd.is3D = is3D;
@@ -3015,7 +3018,7 @@ void Runtime::triggerSound(bool looping, SoundInstance &snd, uint volume, int32
 
 	computeEffectiveVolumeAndBalance(snd);
 
-	if (volume == 0 && looping) {
+	if (volume == getSilentSoundVolume() && looping) {
 		if (snd.cache) {
 			if (snd.cache->player)
 				snd.cache->player.reset();
@@ -3071,14 +3074,14 @@ void Runtime::triggerSound(bool looping, SoundInstance &snd, uint volume, int32
 		snd.endTime = g_system->getMillis(true) + static_cast<uint32>(cache->stream->getLength().msecs()) + 1000u;
 }
 
-void Runtime::triggerSoundRamp(SoundInstance &snd, uint durationMSec, uint newVolume, bool terminateOnCompletion) {
+void Runtime::triggerSoundRamp(SoundInstance &snd, uint durationMSec, int32 newVolume, bool terminateOnCompletion) {
 	snd.rampStartVolume = snd.volume;
 	snd.rampEndVolume = newVolume;
 	snd.rampTerminateOnCompletion = terminateOnCompletion;
 	snd.rampStartTime = g_system->getMillis();
 	snd.rampRatePerMSec = 65536;
 
-	if (!snd.isLooping && newVolume == 0)
+	if (!snd.isLooping && newVolume == getSilentSoundVolume())
 		snd.rampTerminateOnCompletion = true;
 
 	if (durationMSec)
@@ -3159,15 +3162,15 @@ void Runtime::updateSounds(uint32 timestamp) {
 		SoundInstance &snd = *_activeSounds[sndIndex];
 
 		if (snd.rampRatePerMSec) {
-			uint ramp = snd.rampRatePerMSec * (timestamp - snd.rampStartTime);
-			uint newVolume = snd.volume;
+			int32 ramp = snd.rampRatePerMSec * static_cast<int32>(timestamp - snd.rampStartTime);
+			int32 newVolume = snd.volume;
 			if (ramp >= 65536) {
 				snd.rampRatePerMSec = 0;
 				newVolume = snd.rampEndVolume;
 				if (snd.rampTerminateOnCompletion)
 					stopSound(snd);
 			} else {
-				uint rampedVolume = (snd.rampStartVolume * (65536u - ramp)) + (snd.rampEndVolume * ramp);
+				int32 rampedVolume = (snd.rampStartVolume * (65536 - ramp)) + (snd.rampEndVolume * ramp);
 				newVolume = rampedVolume >> 16;
 			}
 
@@ -3191,7 +3194,7 @@ void Runtime::updateSounds(uint32 timestamp) {
 		}
 
 		if (snd.isLooping) {
-			if (snd.volume == 0) {
+			if (snd.volume == getSilentSoundVolume()) {
 				if (!snd.isSilencedLoop) {
 					if (snd.cache) {
 						snd.cache->player.reset();
@@ -3227,14 +3230,14 @@ void Runtime::updateSounds(uint32 timestamp) {
 		if (ramp > rampMax)
 			ramp = rampMax;
 
-		uint32 newVolume = _musicVolumeRampStartVolume;
+		int32 newVolume = _musicVolumeRampStartVolume;
 		if (negative)
-			newVolume -= ramp;
+			newVolume -= static_cast<int32>(ramp);
 		else
-			newVolume += ramp;
+			newVolume += static_cast<int32>(ramp);
 
 		if (newVolume != _musicVolume) {
-			_musicPlayer->setVolume(static_cast<byte>(newVolume));
+			_musicPlayer->setVolume(applyVolumeScale(newVolume));
 			_musicVolume = newVolume;
 		}
 
@@ -3328,7 +3331,7 @@ void Runtime::update3DSounds() {
 }
 
 bool Runtime::computeEffectiveVolumeAndBalance(SoundInstance &snd) {
-	uint effectiveVolume = snd.volume;
+	uint effectiveVolume = applyVolumeScale(snd.volume);
 	int32 effectiveBalance = snd.balance;
 
 	double radians = Common::deg2rad<double>(_listenerAngle);
@@ -3436,14 +3439,45 @@ void Runtime::triggerAmbientSounds() {
 		snd.sceneChangesRemaining--;
 }
 
-uint Runtime::normalizeSoundVolume(StackInt_t arg) const {
-	int32 adjustedVol = static_cast<int32>(arg) + 50;
-	if (adjustedVol < 0)
+uint Runtime::decibelsToLinear(int db, uint baseVolume, uint maxVol) const {
+	double linearized = floor(pow(1.1220184543019634355910389464779, db) * static_cast<double>(baseVolume) + 0.5);
+
+	if (linearized > static_cast<double>(maxVol))
+		return maxVol;
+
+	return static_cast<uint>(linearized);
+}
+
+int32 Runtime::getSilentSoundVolume() const {
+	if (_gameID == GID_SCHIZM)
+		return -50;
+	else
 		return 0;
-	if (adjustedVol > 100)
+}
+
+int32 Runtime::getDefaultSoundVolume() const {
+	if (_gameID == GID_SCHIZM)
+		return 0;
+	else
 		return 100;
+}
 
-	return static_cast<uint>(adjustedVol);
+uint Runtime::applyVolumeScale(int32 volume) const {
+	if (_gameID == GID_SCHIZM) {
+		if (volume >= 0)
+			return Audio::Mixer::kMaxChannelVolume;
+		else if (volume < -49)
+			return 0;
+
+		return _dbToVolume[volume + 49];
+	} else {
+		if (volume > 100)
+			return Audio::Mixer::kMaxChannelVolume;
+		else if (volume < 0)
+			return 0;
+
+		return volume * Audio::Mixer::kMaxChannelVolume / 100;
+	}
 }
 
 AnimationDef Runtime::stackArgsToAnimDef(const StackInt_t *args) const {
@@ -5025,7 +5059,7 @@ void Runtime::scriptOpSoundL1(ScriptArg_t arg) {
 	resolveSoundByName(sndNameArgs[0], true, soundID, cachedSound);
 
 	if (cachedSound)
-		triggerSound(true, *cachedSound, 100, 0, false, false);
+		triggerSound(true, *cachedSound, getDefaultSoundVolume(), 0, false, false);
 }
 
 void Runtime::scriptOpSoundL2(ScriptArg_t arg) {
@@ -5148,7 +5182,7 @@ void Runtime::scriptOpMusicVolRamp(ScriptArg_t arg) {
 	TAKE_STACK_INT(2);
 
 	uint32 duration = static_cast<uint32>(stackArgs[0]) * 100u;
-	uint32 newVolume = stackArgs[1];
+	int32 newVolume = stackArgs[1];
 
 	_musicVolumeRampRatePerMSec = 0;
 
@@ -5160,7 +5194,7 @@ void Runtime::scriptOpMusicVolRamp(ScriptArg_t arg) {
 		if (newVolume != _musicVolume) {
 			uint32 timestamp = g_system->getMillis();
 
-			_musicVolumeRampRatePerMSec = (static_cast<int32>(newVolume) - static_cast<int32>(_musicVolume)) * 65536 / static_cast<int32>(duration);
+			_musicVolumeRampRatePerMSec = (newVolume - _musicVolume) * 65536 / static_cast<int32>(duration);
 			_musicVolumeRampStartTime = timestamp;
 			_musicVolumeRampStartVolume = _musicVolume;
 			_musicVolumeRampEnd = newVolume;
@@ -5597,7 +5631,7 @@ void Runtime::scriptOpExit(ScriptArg_t arg) {
 
 		changeMusicTrack(0);
 		if (_musicPlayer)
-			_musicPlayer->setVolumeAndBalance(100, 0);
+			_musicPlayer->setVolumeAndBalance(applyVolumeScale(getDefaultSoundVolume()), 0);
 	} else {
 		error("Don't know what screen to go to on exit");
 	}
@@ -5877,8 +5911,9 @@ void Runtime::scriptOpVolumeChange(ScriptArg_t arg) {
 
 	SoundInstance *cachedSound = resolveSoundByID(static_cast<uint>(stackArgs[0]));
 
+	// FIXME: Figure out what the duration scale really is
 	if (cachedSound)
-		triggerSoundRamp(*cachedSound, stackArgs[1] * 100, stackArgs[2], false);
+		triggerSoundRamp(*cachedSound, stackArgs[1], stackArgs[2], false);
 }
 
 OPCODE_STUB(VolumeDown)
@@ -5886,7 +5921,7 @@ OPCODE_STUB(VolumeDown)
 void Runtime::scriptOpAnimVolume(ScriptArg_t arg) {
 	TAKE_STACK_INT(1);
 
-	_animVolume = normalizeSoundVolume(stackArgs[0]);
+	_animVolume = stackArgs[0];
 
 	applyAnimationVolume();
 }
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index c249a5af6eb..c22a04ebaa8 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -218,13 +218,13 @@ struct SoundInstance {
 
 	uint id;
 
-	uint rampStartVolume;
-	uint rampEndVolume;
-	uint32 rampRatePerMSec;
+	int32 rampStartVolume;
+	int32 rampEndVolume;
+	int32 rampRatePerMSec;
 	uint32 rampStartTime;
 	bool rampTerminateOnCompletion;
 
-	uint volume;
+	int32 volume;
 	int32 balance;
 
 	uint effectiveVolume;
@@ -248,7 +248,7 @@ struct RandomAmbientSound {
 
 	Common::String name;
 
-	uint volume;
+	int32 volume;
 	int32 balance;
 
 	uint frequency;
@@ -276,7 +276,7 @@ struct ScoreSectionDef {
 
 	Common::String musicFileName;	// If empty, this is silent
 	Common::String nextSection;
-	uint32 volumeOrDurationInSeconds;
+	int32 volumeOrDurationInSeconds;
 };
 
 struct ScoreTrackDef {
@@ -378,7 +378,7 @@ struct SaveGameSnapshot {
 
 		Common::String name;
 		uint id;
-		uint volume;
+		int32 volume;
 		int32 balance;
 
 		bool is3D;
@@ -408,11 +408,11 @@ struct SaveGameSnapshot {
 	Common::String scoreSection;
 	bool musicActive;
 
-	uint musicVolume;
+	int32 musicVolume;
 
 	uint loadedAnimation;
 	uint animDisplayingFrame;
-	uint animVolume;
+	int32 animVolume;
 
 	StaticAnimParams pendingStaticAnimParams;
 	SoundParams3D pendingSoundParams3D;
@@ -737,15 +737,18 @@ private:
 	void applyAnimationVolume();
 
 	void setSound3DParameters(SoundInstance &sound, int32 x, int32 y, const SoundParams3D &soundParams3D);
-	void triggerSound(bool looping, SoundInstance &sound, uint volume, int32 balance, bool is3D, bool isSpeech);
-	void triggerSoundRamp(SoundInstance &sound, uint durationMSec, uint newVolume, bool terminateOnCompletion);
+	void triggerSound(bool looping, SoundInstance &sound, int32 volume, int32 balance, bool is3D, bool isSpeech);
+	void triggerSoundRamp(SoundInstance &sound, uint durationMSec, int32 newVolume, bool terminateOnCompletion);
 	void stopSound(SoundInstance &sound);
 	void updateSounds(uint32 timestamp);
 	void updateSubtitles();
 	void update3DSounds();
 	bool computeEffectiveVolumeAndBalance(SoundInstance &snd);
 	void triggerAmbientSounds();
-	uint normalizeSoundVolume(StackInt_t arg) const;
+	uint decibelsToLinear(int db, uint baseVolume, uint maxVolume) const;
+	int32 getSilentSoundVolume() const;
+	int32 getDefaultSoundVolume() const;
+	uint applyVolumeScale(int32 volume) const;
 
 	void triggerWaveSubtitles(const SoundInstance &sound, const Common::String &id);
 	void stopSubtitles();
@@ -1051,7 +1054,7 @@ private:
 
 	Common::SharedPtr<AudioPlayer> _musicPlayer;
 	int _musicTrack;
-	uint _musicVolume;
+	int32 _musicVolume;
 	bool _musicActive;
 
 	Common::String _scoreTrack;
@@ -1060,9 +1063,9 @@ private:
 	Common::HashMap<Common::String, ScoreTrackDef> _scoreDefs;
 
 	uint32 _musicVolumeRampStartTime;
-	uint _musicVolumeRampStartVolume;
+	int32 _musicVolumeRampStartVolume;
 	int32 _musicVolumeRampRatePerMSec;
-	uint _musicVolumeRampEnd;
+	int32 _musicVolumeRampEnd;
 
 	SfxData _sfxData;
 
@@ -1169,6 +1172,8 @@ private:
 	Common::HashMap<Common::String, SubtitleDef> _waveSubtitles;
 	Common::Array<SubtitleQueueItem> _subtitleQueue;
 	bool _isDisplayingSubtitles;
+
+	int32 _dbToVolume[49];
 };
 
 } // End of namespace VCruise
diff --git a/engines/vcruise/vcruise.cpp b/engines/vcruise/vcruise.cpp
index 509780b3653..41f96302975 100644
--- a/engines/vcruise/vcruise.cpp
+++ b/engines/vcruise/vcruise.cpp
@@ -103,7 +103,7 @@ Common::Error VCruiseEngine::run() {
 	}
 #endif
 
-	
+	syncSoundSettings();
 
 	const Graphics::PixelFormat *fmt16_565 = nullptr;
 	const Graphics::PixelFormat *fmt16_555 = nullptr;


Commit: 3cb2a31280da053872e969f68bd1dc398e63a92b
    https://github.com/scummvm/scummvm/commit/3cb2a31280da053872e969f68bd1dc398e63a92b
Author: elasota (ejlasota at gmail.com)
Date: 2023-05-03T23:40:43-04:00

Commit Message:
VCRUISE: Fix some function names being parsed as hex numbers.  Add disc type variations.

Changed paths:
    engines/vcruise/detection.h
    engines/vcruise/detection_tables.h
    engines/vcruise/runtime.cpp
    engines/vcruise/runtime.h
    engines/vcruise/script.cpp
    engines/vcruise/script.h
    engines/vcruise/vcruise.cpp


diff --git a/engines/vcruise/detection.h b/engines/vcruise/detection.h
index 073c0c37d27..6ae8bb0761f 100644
--- a/engines/vcruise/detection.h
+++ b/engines/vcruise/detection.h
@@ -37,6 +37,8 @@ enum VCruiseGameFlag {
 	VCRUISE_GF_WANT_MP3			= (1 << 0),
 	VCRUISE_GF_WANT_OGG_VORBIS	= (1 << 1),
 	VCRUISE_GF_NEED_JPEG		= (1 << 2),
+	VCRUISE_GF_CD_VARIANT		= (1 << 3),
+	VCRUISE_GF_DVD_VARIANT		= (1 << 4),
 };
 
 struct VCruiseGameDescription {
diff --git a/engines/vcruise/detection_tables.h b/engines/vcruise/detection_tables.h
index ef4756d51c5..a91d38f0ff4 100644
--- a/engines/vcruise/detection_tables.h
+++ b/engines/vcruise/detection_tables.h
@@ -61,7 +61,7 @@ static const VCruiseGameDescription gameDescriptions[] = {
 			AD_ENTRY1s("Schizm.exe", "296edd26d951c3bdc4d303c4c88b27cd", 364544),
 			Common::UNK_LANG,
 			Common::kPlatformWindows,
-			ADGF_UNSTABLE | VCRUISE_GF_WANT_OGG_VORBIS | VCRUISE_GF_NEED_JPEG,
+			ADGF_UNSTABLE | VCRUISE_GF_WANT_OGG_VORBIS | VCRUISE_GF_NEED_JPEG | VCRUISE_GF_DVD_VARIANT,
 			GUIO0()
 		},
 		GID_SCHIZM,
diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index cefe58121b1..8c825c378f7 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -682,7 +682,7 @@ void SaveGameSnapshot::Sound::read(Common::ReadStream *stream) {
 	params3D.read(stream);
 }
 
-SaveGameSnapshot::SaveGameSnapshot() : roomNumber(0), screenNumber(0), direction(0), escOn(false), musicTrack(0), musicVolume(100), musicActive(true), loadedAnimation(0),
+SaveGameSnapshot::SaveGameSnapshot() : roomNumber(0), screenNumber(0), direction(0), hero(0), escOn(false), musicTrack(0), musicVolume(100), musicActive(true), loadedAnimation(0),
 									   animDisplayingFrame(0), animVolume(100), listenerX(0), listenerY(0), listenerAngle(0) {
 }
 
@@ -693,6 +693,7 @@ void SaveGameSnapshot::write(Common::WriteStream *stream) const {
 	stream->writeUint32BE(roomNumber);
 	stream->writeUint32BE(screenNumber);
 	stream->writeUint32BE(direction);
+	stream->writeUint32BE(hero);
 
 	stream->writeByte(escOn ? 1 : 0);
 	stream->writeSint32BE(musicTrack);
@@ -770,6 +771,11 @@ LoadGameOutcome SaveGameSnapshot::read(Common::ReadStream *stream) {
 	screenNumber = stream->readUint32BE();
 	direction = stream->readUint32BE();
 
+	if (saveVersion >= 6)
+		hero = stream->readUint32BE();
+	else
+		hero = 0;
+
 	escOn = (stream->readByte() != 0);
 	musicTrack = stream->readSint32BE();
 
@@ -876,8 +882,8 @@ void SaveGameSnapshot::writeString(Common::WriteStream *stream, const Common::St
 	stream->writeString(str);
 }
 
-Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &rootFSNode, VCruiseGameID gameID)
-	: _system(system), _mixer(mixer), _roomNumber(1), _screenNumber(0), _direction(0), _haveHorizPanAnimations(false), _loadedRoomNumber(0), _activeScreenNumber(0),
+Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &rootFSNode, VCruiseGameID gameID, bool isCDVariant, bool isDVDVariant)
+	: _system(system), _mixer(mixer), _roomNumber(1), _screenNumber(0), _direction(0), _hero(0), _haveHorizPanAnimations(false), _loadedRoomNumber(0), _activeScreenNumber(0),
 	  _gameState(kGameStateBoot), _gameID(gameID), _havePendingScreenChange(false), _forceScreenChange(false), _havePendingReturnToIdleState(false), _havePendingCompletionCheck(false),
 	  _havePendingPlayAmbientSounds(false), _ambientSoundFinishTime(0), _escOn(false), _debugMode(false), _fastAnimationMode(false),
 	  _musicTrack(0), _musicActive(true), _scoreSectionEndTime(0), _musicVolume(getDefaultSoundVolume()), _musicVolumeRampStartTime(0), _musicVolumeRampStartVolume(0), _musicVolumeRampRatePerMSec(0), _musicVolumeRampEnd(0),
@@ -891,7 +897,8 @@ Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &roo
 	  _panoramaState(kPanoramaStateInactive),
 	  _listenerX(0), _listenerY(0), _listenerAngle(0), _soundCacheIndex(0),
 	  _isInGame(false),
-	  _subtitleFont(nullptr), _isDisplayingSubtitles(false), _languageIndex(0) {
+	  _subtitleFont(nullptr), _isDisplayingSubtitles(false), _languageIndex(0),
+	  _isCDVariant(isCDVariant), _isDVDVariant(isDVDVariant) {
 
 	for (uint i = 0; i < kNumDirections; i++) {
 		_haveIdleAnimations[i] = false;
@@ -1091,6 +1098,9 @@ bool Runtime::bootGame(bool newGame) {
 	if (_gameID == GID_SCHIZM) {
 		loadScore();
 		debug(1, "Score loaded OK");
+
+		if (_isCDVariant == _isDVDVariant)
+			error("Detection entry is malformed, Schizm requires either VCRUISE_GF_CD_VARIANT or VCRUISE_GF_DVD_VARIANT");
 	}
 
 	_trayBackgroundGraphic = loadGraphic("Pocket", true);
@@ -1954,7 +1964,8 @@ bool Runtime::runScript() {
 			DISPATCH_OP(BitAnd);
 			DISPATCH_OP(BitOr);
 			DISPATCH_OP(AngleGet);
-			DISPATCH_OP(CDGet);
+			DISPATCH_OP(IsCDVersion);
+			DISPATCH_OP(IsDVDVersion);
 			DISPATCH_OP(Disc);
 			DISPATCH_OP(HidePanel);
 			DISPATCH_OP(RotateUpdate);
@@ -4327,6 +4338,7 @@ void Runtime::recordSaveGameSnapshot() {
 	snapshot->roomNumber = _roomNumber;
 	snapshot->screenNumber = _screenNumber;
 	snapshot->direction = _direction;
+	snapshot->hero = _hero;
 
 	snapshot->pendingStaticAnimParams = _pendingStaticAnimParams;
 
@@ -4411,6 +4423,7 @@ void Runtime::restoreSaveGameSnapshot() {
 	_roomNumber = _saveGame->roomNumber;
 	_screenNumber = _saveGame->screenNumber;
 	_direction = _saveGame->direction;
+	_hero = _saveGame->hero;
 
 	_pendingStaticAnimParams = _saveGame->pendingStaticAnimParams;
 
@@ -5679,18 +5692,36 @@ void Runtime::scriptOpCmpEq(ScriptArg_t arg) {
 	_scriptStack.push_back(StackValue((stackArgs[0] == stackArgs[1]) ? 1 : 0));
 }
 
+void Runtime::scriptOpCmpNE(ScriptArg_t arg) {
+	TAKE_STACK_INT(2);
+
+	_scriptStack.push_back(StackValue((stackArgs[0] != stackArgs[1]) ? 1 : 0));
+}
+
 void Runtime::scriptOpCmpLt(ScriptArg_t arg) {
 	TAKE_STACK_INT(2);
 
 	_scriptStack.push_back(StackValue((stackArgs[0] < stackArgs[1]) ? 1 : 0));
 }
 
+void Runtime::scriptOpCmpLE(ScriptArg_t arg) {
+	TAKE_STACK_INT(2);
+
+	_scriptStack.push_back(StackValue((stackArgs[0] <= stackArgs[1]) ? 1 : 0));
+}
+
 void Runtime::scriptOpCmpGt(ScriptArg_t arg) {
 	TAKE_STACK_INT(2);
 
 	_scriptStack.push_back(StackValue((stackArgs[0] > stackArgs[1]) ? 1 : 0));
 }
 
+void Runtime::scriptOpCmpGE(ScriptArg_t arg) {
+	TAKE_STACK_INT(2);
+
+	_scriptStack.push_back(StackValue((stackArgs[0] >= stackArgs[1]) ? 1 : 0));
+}
+
 void Runtime::scriptOpBitLoad(ScriptArg_t arg) {
 	TAKE_STACK_INT(2);
 
@@ -5961,24 +5992,42 @@ void Runtime::scriptOpString(ScriptArg_t arg) {
 	_scriptStack.push_back(StackValue(_scriptSet->strings[arg]));
 }
 
-OPCODE_STUB(CmpNE)
-OPCODE_STUB(CmpLE)
-OPCODE_STUB(CmpGE)
 OPCODE_STUB(Speech)
 OPCODE_STUB(SpeechEx)
 OPCODE_STUB(SpeechTest)
 OPCODE_STUB(Say)
-OPCODE_STUB(RandomInclusive)
+
+void Runtime::scriptOpRandomInclusive(ScriptArg_t arg) {
+	TAKE_STACK_INT(1);
+
+	if (stackArgs[0] == 0)
+		_scriptStack.push_back(StackValue(0));
+	else
+		_scriptStack.push_back(StackValue(_rng->getRandomNumber(stackArgs[0])));
+}
+
 OPCODE_STUB(HeroOut)
 OPCODE_STUB(HeroGetPos)
 OPCODE_STUB(HeroSetPos)
-OPCODE_STUB(HeroGet)
+
+void Runtime::scriptOpHeroGet(ScriptArg_t arg) {
+	_scriptStack.push_back(StackValue(_hero));
+}
+
 OPCODE_STUB(Garbage)
 OPCODE_STUB(GetRoom)
 OPCODE_STUB(BitAnd)
 OPCODE_STUB(BitOr)
 OPCODE_STUB(AngleGet)
-OPCODE_STUB(CDGet)
+
+void Runtime::scriptOpIsDVDVersion(ScriptArg_t arg) {
+	_scriptStack.push_back(StackValue(_isDVDVariant ? 1 : 0));
+}
+
+void Runtime::scriptOpIsCDVersion(ScriptArg_t arg) {
+	_scriptStack.push_back(StackValue(_isCDVariant ? 1 : 0));
+}
+
 OPCODE_STUB(Disc)
 OPCODE_STUB(HidePanel)
 OPCODE_STUB(RotateUpdate)
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index c22a04ebaa8..a8c70cdc8b5 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -400,6 +400,7 @@ struct SaveGameSnapshot {
 	uint roomNumber;
 	uint screenNumber;
 	uint direction;
+	uint hero;
 
 	bool escOn;
 	int musicTrack;
@@ -476,7 +477,7 @@ class Runtime {
 public:
 	friend class RuntimeMenuInterface;
 
-	Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &rootFSNode, VCruiseGameID gameID);
+	Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &rootFSNode, VCruiseGameID gameID, bool isCDVariant, bool isDVDVariant);
 	virtual ~Runtime();
 
 	void initSections(const Common::Rect &gameRect, const Common::Rect &menuRect, const Common::Rect &trayRect, const Common::Rect &fullscreenMenuRect, const Graphics::PixelFormat &pixFmt);
@@ -960,7 +961,8 @@ private:
 	void scriptOpBitAnd(ScriptArg_t arg);
 	void scriptOpBitOr(ScriptArg_t arg);
 	void scriptOpAngleGet(ScriptArg_t arg);
-	void scriptOpCDGet(ScriptArg_t arg);
+	void scriptOpIsDVDVersion(ScriptArg_t arg);
+	void scriptOpIsCDVersion(ScriptArg_t arg);
 	void scriptOpDisc(ScriptArg_t arg);
 	void scriptOpHidePanel(ScriptArg_t arg);
 	void scriptOpRotateUpdate(ScriptArg_t arg);
@@ -997,7 +999,7 @@ private:
 	uint _roomNumber;	// Room number can be changed independently of the loaded room, the screen doesn't change until a command changes it
 	uint _screenNumber;
 	uint _direction;
-	//uint _highPrecisionDirection;
+	uint _hero;
 
 	GyroState _gyros;
 
@@ -1163,6 +1165,8 @@ private:
 	const Graphics::Font *_subtitleFont;
 	Common::SharedPtr<Graphics::Font> _subtitleFontKeepalive;
 	uint _languageIndex;
+	bool _isDVDVariant;
+	bool _isCDVariant;
 
 	typedef Common::HashMap<uint, SubtitleDef> FrameToSubtitleMap_t;
 	typedef Common::HashMap<uint, FrameToSubtitleMap_t> AnimSubtitleMap_t;
diff --git a/engines/vcruise/script.cpp b/engines/vcruise/script.cpp
index 4975a50e854..1340754b53a 100644
--- a/engines/vcruise/script.cpp
+++ b/engines/vcruise/script.cpp
@@ -253,9 +253,17 @@ bool ScriptCompiler::parseDecNumber(const Common::String &token, uint start, uin
 	if (start == token.size())
 		return false;
 
-	uint num = 0;
-	if (!sscanf(token.c_str() + start, "%u", &num))
-		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;
@@ -265,9 +273,21 @@ bool ScriptCompiler::parseHexNumber(const Common::String &token, uint start, uin
 	if (start == token.size())
 		return false;
 
-	uint num = 0;
-	if (!sscanf(token.c_str() + start, "%x", &num))
-		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;
@@ -278,7 +298,9 @@ bool ScriptCompiler::parseBinNumber(const Common::String &token, uint start, uin
 		return false;
 
 	uint num = 0;
-	for (char c : token) {
+	for (uint i = start; i < token.size(); i++) {
+		char c = token[i];
+
 		num <<= 1;
 		if (c == '1')
 			num |= 1;
@@ -705,7 +727,8 @@ static ScriptNamedInstruction g_schizmNamedInstructions[] = {
 	{"hi@", ProtoOp::kProtoOpScript, ScriptOps::kHiGet},
 	{"angle@", ProtoOp::kProtoOpScript, ScriptOps::kAngleGet},
 	{"angleG@", ProtoOp::kProtoOpScript, ScriptOps::kAngleGGet},
-	{"cd@", ProtoOp::kProtoOpScript, ScriptOps::kCDGet},
+	{"cd@", ProtoOp::kProtoOpScript, ScriptOps::kIsCDVersion},
+	{"dvd@", ProtoOp::kProtoOpScript, ScriptOps::kIsDVDVersion},
 	{"disc", ProtoOp::kProtoOpScript, ScriptOps::kDisc},
 	{"save0", ProtoOp::kProtoOpNoop, ScriptOps::kSave0},
 	{"hidePanel", ProtoOp::kProtoOpNoop, ScriptOps::kHidePanel},
@@ -852,12 +875,6 @@ bool ScriptCompiler::compileInstructionToken(ProtoScript &script, const Common::
 			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));
diff --git a/engines/vcruise/script.h b/engines/vcruise/script.h
index 94b996ebdcd..00562195dff 100644
--- a/engines/vcruise/script.h
+++ b/engines/vcruise/script.h
@@ -201,7 +201,8 @@ enum ScriptOp {
 	kBitAnd,
 	kBitOr,
 	kAngleGet,
-	kCDGet,
+	kIsCDVersion,
+	kIsDVDVersion,
 	kDisc,
 	kHidePanel,
 	kRotateUpdate,
diff --git a/engines/vcruise/vcruise.cpp b/engines/vcruise/vcruise.cpp
index 41f96302975..cbf78a2ee40 100644
--- a/engines/vcruise/vcruise.cpp
+++ b/engines/vcruise/vcruise.cpp
@@ -105,6 +105,9 @@ Common::Error VCruiseEngine::run() {
 
 	syncSoundSettings();
 
+	bool isCDVariant = ((_gameDescription->desc.flags & VCRUISE_GF_CD_VARIANT) != 0);
+	bool isDVDVariant = ((_gameDescription->desc.flags & VCRUISE_GF_DVD_VARIANT) != 0);
+
 	const Graphics::PixelFormat *fmt16_565 = nullptr;
 	const Graphics::PixelFormat *fmt16_555 = nullptr;
 	const Graphics::PixelFormat *fmt32 = nullptr;
@@ -164,7 +167,7 @@ Common::Error VCruiseEngine::run() {
 
 	_system->fillScreen(0);
 
-	_runtime.reset(new Runtime(_system, _mixer, _rootFSNode, _gameDescription->gameID));
+	_runtime.reset(new Runtime(_system, _mixer, _rootFSNode, _gameDescription->gameID, isCDVariant, isDVDVariant));
 	_runtime->initSections(_videoRect, _menuBarRect, _trayRect, Common::Rect(640, 480), _system->getScreenFormat());
 
 	const char *exeName = _gameDescription->desc.filesDescriptions[0].fileName;


Commit: 298011eb430953e9990f0b3eaed48153c731e036
    https://github.com/scummvm/scummvm/commit/298011eb430953e9990f0b3eaed48153c731e036
Author: elasota (ejlasota at gmail.com)
Date: 2023-05-03T23:40:43-04:00

Commit Message:
VCRUISE: Support Schizm's presets section in playlist file

Changed paths:
    engines/vcruise/runtime.cpp
    engines/vcruise/runtime.h


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 8c825c378f7..9d0f267f213 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -351,14 +351,24 @@ void SfxData::load(Common::SeekableReadStream &stream, Audio::Mixer *mixer) {
 
 	const Common::INIFile::Section *samplesSection = nullptr;
 	const Common::INIFile::Section *playlistsSection = nullptr;
+	const Common::INIFile::Section *presetsSection = nullptr;
 
-	Common::INIFile::SectionList sections = iniFile.getSections();	// Why does this require a copy??
+	Common::INIFile::SectionList sections = iniFile.getSections();	// Why does this require a copy?  Sigh.
 
 	for (const Common::INIFile::Section &section : sections) {
 		if (section.name == "samples")
 			samplesSection = §ion;
 		else if (section.name == "playlists")
 			playlistsSection = §ion;
+		else if (section.name == "presets")
+			presetsSection = §ion;
+	}
+
+	Common::HashMap<Common::String, Common::String> presets;
+
+	if (presetsSection) {
+		for (const Common::INIFile::KeyValue &keyValue : presetsSection->keys)
+			presets.setVal(keyValue.key, keyValue.value);
 	}
 
 	if (samplesSection) {
@@ -470,11 +480,19 @@ void SfxData::load(Common::SeekableReadStream &stream, Audio::Mixer *mixer) {
 					continue;
 				}
 
+				if (!presets.empty()) {
+					for (Common::String &tokenRef : tokens) {
+						Common::HashMap<Common::String, Common::String>::const_iterator presetIt = presets.find(tokenRef);
+						if (presetIt != presets.end())
+							tokenRef = presetIt->_value;
+					}
+				}
+
 				unsigned int frameNum = 0;
 				int balance = 0;
-				unsigned int volume = 0;
+				int volume = 0;
 
-				if (!sscanf(tokens[0].c_str(), "%u", &frameNum) || !sscanf(tokens[2].c_str(), "%i", &balance) || !sscanf(tokens[3].c_str(), "%u", &volume)) {
+				if (!sscanf(tokens[0].c_str(), "%u", &frameNum) || !sscanf(tokens[2].c_str(), "%i", &balance) || !sscanf(tokens[3].c_str(), "%i", &volume)) {
 					warning("Malformed playlist entry: %s", key.c_str());
 					continue;
 				}
@@ -1719,11 +1737,11 @@ void Runtime::continuePlayingAnimation(bool loop, bool useStopFrame, bool &outAn
 					VCruise::AudioPlayer &audioPlayer = *playlistEntry.sample->audioPlayer;
 
 					if (playlistEntry.isUpdate) {
-						audioPlayer.setVolumeAndBalance(playlistEntry.volume, playlistEntry.balance);
+						audioPlayer.setVolumeAndBalance(applyVolumeScale(playlistEntry.volume), playlistEntry.balance);
 					} else {
 						audioPlayer.stop();
 						playlistEntry.sample->audioStream->seek(0);
-						audioPlayer.play(playlistEntry.volume, playlistEntry.balance);
+						audioPlayer.play(applyVolumeScale(playlistEntry.volume), playlistEntry.balance);
 					}
 
 					// No break, it's possible for there to be multiple sounds in the same frame
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index a8c70cdc8b5..54a7765229f 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -165,7 +165,7 @@ struct SfxPlaylistEntry {
 	uint frame;
 	Common::SharedPtr<SfxSound> sample;
 	int8 balance;
-	uint8 volume;
+	int32 volume;
 	bool isUpdate;
 };
 


Commit: e7f10fe8f83faae4108ae1b4f4ac173f8a8111ea
    https://github.com/scummvm/scummvm/commit/e7f10fe8f83faae4108ae1b4f4ac173f8a8111ea
Author: elasota (ejlasota at gmail.com)
Date: 2023-05-03T23:40:44-04:00

Commit Message:
VCRUISE: Fix Schizm DVD identification

Changed paths:
    engines/vcruise/detection_tables.h


diff --git a/engines/vcruise/detection_tables.h b/engines/vcruise/detection_tables.h
index a91d38f0ff4..b949c0fd94d 100644
--- a/engines/vcruise/detection_tables.h
+++ b/engines/vcruise/detection_tables.h
@@ -54,10 +54,10 @@ static const VCruiseGameDescription gameDescriptions[] = {
 		},
 		GID_REAH,
 	},
-	{ // Schizm, 5 CD Version
+	{ // Schizm, DVD/digital Version
 		{
 			"schizm",
-			"CD",
+			"DVD",
 			AD_ENTRY1s("Schizm.exe", "296edd26d951c3bdc4d303c4c88b27cd", 364544),
 			Common::UNK_LANG,
 			Common::kPlatformWindows,


Commit: d0aee6d817a05eea165214794e361c96b087d33e
    https://github.com/scummvm/scummvm/commit/d0aee6d817a05eea165214794e361c96b087d33e
Author: elasota (ejlasota at gmail.com)
Date: 2023-05-03T23:40:44-04:00

Commit Message:
VCRUISE: Add menu text labels

Changed paths:
    engines/vcruise/menu.cpp
    engines/vcruise/menu.h
    engines/vcruise/runtime.cpp
    engines/vcruise/runtime.h


diff --git a/engines/vcruise/menu.cpp b/engines/vcruise/menu.cpp
index accaf524e2e..a029e692df8 100644
--- a/engines/vcruise/menu.cpp
+++ b/engines/vcruise/menu.cpp
@@ -21,6 +21,7 @@
 
 #include "common/config-manager.h"
 
+#include "graphics/font.h"
 #include "graphics/managed_surface.h"
 
 #include "audio/mixer.h"
@@ -30,9 +31,9 @@
 
 namespace VCruise {
 
-class ReahMenuPage : public MenuPage {
+class ReahSchizmMenuPage : public MenuPage {
 public:
-	ReahMenuPage();
+	explicit ReahSchizmMenuPage(bool isSchizm);
 
 	bool run() override;
 	void start() override;
@@ -76,12 +77,15 @@ protected:
 	struct Button {
 		Button();
 		Button(Graphics::Surface *graphic, const Common::Rect &graphicRect, const Common::Rect &screenRect, const Common::Point &stateOffset, bool enabled);
+		Button(Graphics::Surface *graphic, const Common::Rect &graphicRect, const Common::Rect &screenRect, const Common::Point &stateOffset, bool enabled, const Common::String (&states)[4]);
 
 		Graphics::Surface *_graphic;
 		Common::Rect _graphicRect;
 		Common::Rect _screenRect;
 		Common::Point _stateOffset;
 		bool _enabled;
+
+		Common::String _buttonStates[4];
 	};
 
 	struct Slider {
@@ -113,11 +117,13 @@ protected:
 
 	Common::Point _sliderDragStart;
 	int _sliderDragValue;
+
+	bool _isSchizm;
 };
 
-class ReahMenuBarPage : public ReahMenuPage {
+class ReahMenuBarPage : public ReahSchizmMenuPage {
 public:
-	explicit ReahMenuBarPage(uint page);
+	ReahMenuBarPage(uint page, bool isSchizm);
 
 	void start() override final;
 
@@ -140,14 +146,14 @@ protected:
 
 class ReahHelpMenuPage : public ReahMenuBarPage {
 public:
-	ReahHelpMenuPage();
+	explicit ReahHelpMenuPage(bool isSchizm);
 
 	void addPageContents() override;
 };
 
 class ReahSoundMenuPage : public ReahMenuBarPage {
 public:
-	ReahSoundMenuPage();
+	explicit ReahSoundMenuPage(bool isSchizm);
 
 	void addPageContents() override;
 	void onSettingsChanged() override;
@@ -183,7 +189,7 @@ private:
 
 class ReahQuitMenuPage : public ReahMenuBarPage {
 public:
-	ReahQuitMenuPage();
+	explicit ReahQuitMenuPage(bool isSchizm);
 
 	void addPageContents() override;
 	void onButtonClicked(uint button, bool &outChangedState) override;
@@ -197,13 +203,15 @@ private:
 
 class ReahPauseMenuPage : public ReahMenuBarPage {
 public:
-	ReahPauseMenuPage();
+	explicit ReahPauseMenuPage(bool isSchizm);
 
 	void addPageContents() override;
 };
 
-class ReahMainMenuPage : public ReahMenuPage {
+class ReahSchizmMainMenuPage : public ReahSchizmMenuPage {
 public:
+	explicit ReahSchizmMainMenuPage(bool isSchizm);
+
 	void start() override;
 
 protected:
@@ -220,10 +228,10 @@ private:
 	};
 };
 
-ReahMenuPage::ReahMenuPage() : _interactionIndex(0), _interactionState(kInteractionStateNotInteracting), _sliderDragValue(0) {
+ReahSchizmMenuPage::ReahSchizmMenuPage(bool isSchizm) : _interactionIndex(0), _interactionState(kInteractionStateNotInteracting), _sliderDragValue(0), _isSchizm(isSchizm) {
 }
 
-bool ReahMenuPage::run() {
+bool ReahSchizmMenuPage::run() {
 	bool changedState = false;
 
 	OSEvent evt;
@@ -252,7 +260,7 @@ bool ReahMenuPage::run() {
 	return false;
 }
 
-void ReahMenuPage::start() {
+void ReahSchizmMenuPage::start() {
 	for (uint buttonIndex = 0; buttonIndex < _buttons.size(); buttonIndex++)
 		drawButtonInState(buttonIndex, _buttons[buttonIndex]._enabled ? kButtonStateIdle : kButtonStateDisabled);
 
@@ -266,20 +274,20 @@ void ReahMenuPage::start() {
 	handleMouseMove(mousePoint);
 }
 
-void ReahMenuPage::onButtonClicked(uint button, bool &outChangedState) {
+void ReahSchizmMenuPage::onButtonClicked(uint button, bool &outChangedState) {
 	outChangedState = false;
 }
 
-void ReahMenuPage::onCheckboxClicked(uint button, bool &outChangedState) {
+void ReahSchizmMenuPage::onCheckboxClicked(uint button, bool &outChangedState) {
 }
 
-void ReahMenuPage::onSliderMoved(uint slider) {
+void ReahSchizmMenuPage::onSliderMoved(uint slider) {
 }
 
-void ReahMenuPage::eraseSlider(uint sliderIndex) const {
+void ReahSchizmMenuPage::eraseSlider(uint sliderIndex) const {
 }
 
-void ReahMenuPage::handleMouseMove(const Common::Point &pt) {
+void ReahSchizmMenuPage::handleMouseMove(const Common::Point &pt) {
 	switch (_interactionState) {
 	case kInteractionStateNotInteracting:
 		for (uint buttonIndex = 0; buttonIndex < _buttons.size(); buttonIndex++) {
@@ -411,7 +419,7 @@ void ReahMenuPage::handleMouseMove(const Common::Point &pt) {
 	}
 }
 
-void ReahMenuPage::handleMouseDown(const Common::Point &pt, bool &outChangedState) {
+void ReahSchizmMenuPage::handleMouseDown(const Common::Point &pt, bool &outChangedState) {
 	switch (_interactionState) {
 	case kInteractionStateNotInteracting:
 	case kInteractionStateClickingOnButton:
@@ -442,7 +450,7 @@ void ReahMenuPage::handleMouseDown(const Common::Point &pt, bool &outChangedStat
 	}
 }
 
-void ReahMenuPage::handleMouseUp(const Common::Point &pt, bool &outChangedState) {
+void ReahSchizmMenuPage::handleMouseUp(const Common::Point &pt, bool &outChangedState) {
 	switch (_interactionState) {
 	case kInteractionStateNotInteracting:
 	case kInteractionStateOverButton:
@@ -487,7 +495,7 @@ void ReahMenuPage::handleMouseUp(const Common::Point &pt, bool &outChangedState)
 	}
 }
 
-ReahMenuBarPage::ReahMenuBarPage(uint page) : _page(page) {
+ReahMenuBarPage::ReahMenuBarPage(uint page, bool isSchizm) : ReahSchizmMenuPage(isSchizm), _page(page) {
 }
 
 void ReahMenuBarPage::start() {
@@ -519,13 +527,13 @@ void ReahMenuBarPage::start() {
 
 	addPageContents();
 
-	ReahMenuPage::start();
+	ReahSchizmMenuPage::start();
 }
 
 void ReahMenuBarPage::onButtonClicked(uint button, bool &outChangedState) {
 	switch (button) {
 	case kMenuBarButtonHelp:
-		_menuInterface->changeMenu(new ReahHelpMenuPage());
+		_menuInterface->changeMenu(new ReahHelpMenuPage(_isSchizm));
 		outChangedState = true;
 		break;
 	case kMenuBarButtonLoad:
@@ -535,11 +543,11 @@ void ReahMenuBarPage::onButtonClicked(uint button, bool &outChangedState) {
 		g_engine->saveGameDialog();
 		break;
 	case kMenuBarButtonSound:
-		_menuInterface->changeMenu(new ReahSoundMenuPage());
+		_menuInterface->changeMenu(new ReahSoundMenuPage(_isSchizm));
 		outChangedState = true;
 		break;
 	case kMenuBarButtonQuit:
-		_menuInterface->changeMenu(new ReahQuitMenuPage());
+		_menuInterface->changeMenu(new ReahQuitMenuPage(_isSchizm));
 		outChangedState = true;
 		break;
 
@@ -547,7 +555,7 @@ void ReahMenuBarPage::onButtonClicked(uint button, bool &outChangedState) {
 		if (_menuInterface->canSave())
 			outChangedState = _menuInterface->reloadFromCheckpoint();
 		else {
-			_menuInterface->changeMenu(new ReahMainMenuPage());
+			_menuInterface->changeMenu(new ReahSchizmMainMenuPage(_isSchizm));
 			outChangedState = true;
 		}
 		break;
@@ -556,15 +564,15 @@ void ReahMenuBarPage::onButtonClicked(uint button, bool &outChangedState) {
 	}
 }
 
-void ReahMenuPage::drawButtonInState(uint buttonIndex, ButtonState state) const {
+void ReahSchizmMenuPage::drawButtonInState(uint buttonIndex, ButtonState state) const {
 	drawButtonFromListInState(_buttons, buttonIndex, state);
 }
 
-void ReahMenuPage::drawCheckboxInState(uint buttonIndex, CheckboxState state) const {
+void ReahSchizmMenuPage::drawCheckboxInState(uint buttonIndex, CheckboxState state) const {
 	drawButtonFromListInState(_checkboxes, buttonIndex, state);
 }
 
-void ReahMenuPage::drawSlider(uint sliderIndex) const {
+void ReahSchizmMenuPage::drawSlider(uint sliderIndex) const {
 	const Slider &slider = _sliders[sliderIndex];
 
 	Common::Point screenPoint(slider._baseRect.left + slider._value, slider._baseRect.top);
@@ -573,32 +581,65 @@ void ReahMenuPage::drawSlider(uint sliderIndex) const {
 	_menuInterface->commitRect(Common::Rect(screenPoint.x, screenPoint.y, screenPoint.x + slider._baseRect.width(), screenPoint.y + slider._baseRect.height()));
 }
 
-void ReahMenuPage::drawButtonFromListInState(const Common::Array<Button> &buttonList, uint buttonIndex, int state) const {
+void ReahSchizmMenuPage::drawButtonFromListInState(const Common::Array<Button> &buttonList, uint buttonIndex, int state) const {
 	const Button &button = buttonList[buttonIndex];
 
 	Common::Rect graphicRect = button._graphicRect;
 	graphicRect.translate(button._stateOffset.x * state, button._stateOffset.y * state);
 
-	_menuInterface->getMenuSurface()->blitFrom(*button._graphic, graphicRect, button._screenRect);
+	Graphics::ManagedSurface *menuSurf = _menuInterface->getMenuSurface();
+	menuSurf->blitFrom(*button._graphic, graphicRect, button._screenRect);
+
+	const Graphics::Font *font = nullptr;
+	const Common::String *labelTextUTF8 = nullptr;
+	uint32 textColor;
+	uint32 shadowColor;
+	_menuInterface->getLabelDef(button._buttonStates[state], font, labelTextUTF8, textColor, shadowColor);
+
+	if (font && labelTextUTF8) {
+		Common::U32String text = labelTextUTF8->decode(Common::kUtf8);
+
+		int strWidth = font->getStringWidth(text);
+		int strHeight = font->getFontHeight();
+
+		Common::Point textPos(button._screenRect.left + (button._screenRect.width() - strWidth) / 2, button._screenRect.top + (button._screenRect.height() - strHeight) / 2);
+
+		if (shadowColor != 0) {
+			Common::Point shadowPos = textPos + Common::Point(1, 1);
+
+			uint32 realShadowColor = menuSurf->format.RGBToColor((shadowColor >> 16) & 0xff, (shadowColor >> 8) & 0xff, shadowColor & 0xff);
+			font->drawString(menuSurf, text, shadowPos.x, shadowPos.y, strWidth, realShadowColor);
+		}
+
+		uint32 realTextColor = menuSurf->format.RGBToColor((textColor >> 16) & 0xff, (textColor >> 8) & 0xff, textColor & 0xff);
+		font->drawString(menuSurf, text, textPos.x, textPos.y, strWidth, realTextColor);
+	}
+
 	_menuInterface->commitRect(Common::Rect(button._screenRect.left, button._screenRect.top, button._screenRect.left + graphicRect.width(), button._screenRect.top + graphicRect.height()));
 }
 
-ReahMenuPage::Button::Button() : _graphic(nullptr), _enabled(true) {
+ReahSchizmMenuPage::Button::Button() : _graphic(nullptr), _enabled(true) {
+}
+
+ReahSchizmMenuPage::Button::Button(Graphics::Surface *graphic, const Common::Rect &graphicRect, const Common::Rect &screenRect, const Common::Point &stateOffset, bool enabled)
+	: _graphic(graphic), _graphicRect(graphicRect), _screenRect(screenRect), _stateOffset(stateOffset), _enabled(enabled) {
 }
 
-ReahMenuPage::Button::Button(Graphics::Surface *graphic, const Common::Rect &graphicRect, const Common::Rect &screenRect, const Common::Point &stateOffset, bool enabled)
+ReahSchizmMenuPage::Button::Button(Graphics::Surface *graphic, const Common::Rect &graphicRect, const Common::Rect &screenRect, const Common::Point &stateOffset, bool enabled, const Common::String (&states)[4])
 	: _graphic(graphic), _graphicRect(graphicRect), _screenRect(screenRect), _stateOffset(stateOffset), _enabled(enabled) {
+	for (int i = 0; i < 4; i++)
+		this->_buttonStates[i] = states[i];
 }
 
-ReahMenuPage::Slider::Slider() : _graphic(nullptr), _value(0), _maxValue(1) {
+ReahSchizmMenuPage::Slider::Slider() : _graphic(nullptr), _value(0), _maxValue(1) {
 }
 
-ReahMenuPage::Slider::Slider(Graphics::Surface *graphic, const Common::Rect &baseRect, int value, int maxValue)
+ReahSchizmMenuPage::Slider::Slider(Graphics::Surface *graphic, const Common::Rect &baseRect, int value, int maxValue)
 	: _graphic(graphic), _baseRect(baseRect), _value(value), _maxValue(maxValue) {
 	assert(_value >= 0 && _value <= maxValue);
 }
 
-ReahHelpMenuPage::ReahHelpMenuPage() : ReahMenuBarPage(kMenuBarButtonHelp) {
+ReahHelpMenuPage::ReahHelpMenuPage(bool isSchizm) : ReahMenuBarPage(kMenuBarButtonHelp, isSchizm) {
 }
 
 void ReahHelpMenuPage::addPageContents() {
@@ -609,7 +650,7 @@ void ReahHelpMenuPage::addPageContents() {
 	}
 }
 
-ReahSoundMenuPage::ReahSoundMenuPage() : ReahMenuBarPage(kMenuBarButtonSound), _soundChecked(false), _musicChecked(false) {
+ReahSoundMenuPage::ReahSoundMenuPage(bool isSchizm) : ReahMenuBarPage(kMenuBarButtonSound, isSchizm), _soundChecked(false), _musicChecked(false) {
 }
 
 void ReahSoundMenuPage::addPageContents() {
@@ -770,7 +811,7 @@ void ReahSoundMenuPage::applyMusicVolume() const {
 	g_engine->syncSoundSettings();
 }
 
-ReahQuitMenuPage::ReahQuitMenuPage() : ReahMenuBarPage(kMenuBarButtonQuit) {
+ReahQuitMenuPage::ReahQuitMenuPage(bool isSchizm) : ReahMenuBarPage(kMenuBarButtonQuit, isSchizm) {
 }
 
 void ReahQuitMenuPage::addPageContents() {
@@ -840,7 +881,7 @@ void ReahQuitMenuPage::onButtonClicked(uint button, bool &outChangedState) {
 		onButtonClicked(kMenuBarButtonReturn, outChangedState);
 }
 
-ReahPauseMenuPage::ReahPauseMenuPage() : ReahMenuBarPage(static_cast<uint>(-1)) {
+ReahPauseMenuPage::ReahPauseMenuPage(bool isSchizm) : ReahMenuBarPage(static_cast<uint>(-1), isSchizm) {
 }
 
 void ReahPauseMenuPage::addPageContents() {
@@ -858,8 +899,10 @@ void ReahPauseMenuPage::addPageContents() {
 	_menuInterface->commitRect(Common::Rect(0, 44, 640, 392));
 }
 
+ReahSchizmMainMenuPage::ReahSchizmMainMenuPage(bool isSchizm) : ReahSchizmMenuPage(isSchizm) {
+}
 
-void ReahMainMenuPage::start() {
+void ReahSchizmMainMenuPage::start() {
 	Graphics::Surface *bgGraphic = _menuInterface->getUIGraphic(0);
 
 	Graphics::ManagedSurface *menuSurf = _menuInterface->getMenuSurface();
@@ -872,7 +915,39 @@ void ReahMainMenuPage::start() {
 
 	Graphics::Surface *buttonGraphic = _menuInterface->getUIGraphic(1);
 
-	const int buttonTopYs[6] = {66, 119, 171, 224, 277, 330};
+	Common::Point buttonSize;
+
+	Common::Point buttonCoords[6];
+	Common::String buttonStates[6][4];
+
+	if (_isSchizm) {
+		buttonCoords[0] = Common::Point(240, 52);
+		buttonCoords[1] = Common::Point(181, 123);
+		buttonCoords[2] = Common::Point(307, 157);
+		buttonCoords[3] = Common::Point(179, 232);
+		buttonCoords[4] = Common::Point(298, 296);
+		buttonCoords[5] = Common::Point(373, 395);
+
+		buttonSize = Common::Point(150, 40);
+
+		for (int i = 0; i < 6; i++) {
+			int index = i;
+			if (i == 5)
+				index = 6;
+
+			buttonStates[i][0] = Common::String::format("szData001_%02i", static_cast<int>(index + 1));
+			buttonStates[i][1] = Common::String::format("szData001_%02i", static_cast<int>(index + 8));
+			buttonStates[i][2] = Common::String::format("szData001_%02i", static_cast<int>(index + 15));
+			buttonStates[i][3] = Common::String::format("szData001_%02i", static_cast<int>(index + 22));
+		}
+
+	} else {
+		const int buttonTopYs[6] = {66, 119, 171, 224, 277, 330};
+		for (int i = 0; i < 6; i++)
+			buttonCoords[i] = Common::Point(492, buttonTopYs[i]);
+
+		buttonSize = Common::Point(112, 44);
+	}
 
 	for (int i = 0; i < 6; i++) {
 		bool isEnabled = true;
@@ -881,13 +956,22 @@ void ReahMainMenuPage::start() {
 		else if (i == kButtonLoad)
 			isEnabled = _menuInterface->hasAnySave();
 
-		_buttons.push_back(Button(buttonGraphic, Common::Rect(0, i * 44, 112, i * 44 + 44), Common::Rect(492, buttonTopYs[i], 492 + 112, buttonTopYs[i] + 44), Common::Point(112, 0), isEnabled));
+		int coordScale = i;
+
+		// Skip uninstall button
+		if (_isSchizm && i == 5)
+			coordScale = 6;
+
+		Common::Rect graphicRect(0, coordScale * buttonSize.y, buttonSize.x, (coordScale + 1) * buttonSize.y);
+		Common::Rect screenRect(buttonCoords[i].x, buttonCoords[i].y, buttonCoords[i].x + buttonSize.x, buttonCoords[i].y + buttonSize.y);
+
+		_buttons.push_back(Button(buttonGraphic, graphicRect, screenRect, Common::Point(buttonSize.x, 0), isEnabled, buttonStates[i]));
 	}
 
-	ReahMenuPage::start();
+	ReahSchizmMenuPage::start();
 }
 
-void ReahMainMenuPage::onButtonClicked(uint button, bool &outChangedState) {
+void ReahSchizmMainMenuPage::onButtonClicked(uint button, bool &outChangedState) {
 	switch (button) {
 	case kButtonContinue: {
 			Common::Error loadError = g_engine->loadGameState(g_engine->getAutosaveSlot());
@@ -904,7 +988,7 @@ void ReahMainMenuPage::onButtonClicked(uint button, bool &outChangedState) {
 		break;
 
 	case kButtonSound:
-		_menuInterface->changeMenu(new ReahSoundMenuPage());
+		_menuInterface->changeMenu(new ReahSoundMenuPage(_isSchizm));
 		outChangedState = true;
 		break;
 
@@ -914,7 +998,7 @@ void ReahMainMenuPage::onButtonClicked(uint button, bool &outChangedState) {
 		break;
 
 	case kButtonQuit:
-		_menuInterface->changeMenu(new ReahQuitMenuPage());
+		_menuInterface->changeMenu(new ReahQuitMenuPage(_isSchizm));
 		outChangedState = true;
 		break;
 	}
@@ -943,24 +1027,24 @@ bool MenuPage::run() {
 	return false;
 }
 
-MenuPage *createMenuReahMain() {
-	return new ReahMainMenuPage();
+MenuPage *createMenuMain(bool isSchizm) {
+	return new ReahSchizmMainMenuPage(isSchizm);
 }
 
-MenuPage *createMenuReahQuit() {
-	return new ReahQuitMenuPage();
+MenuPage *createMenuQuit(bool isSchizm) {
+	return new ReahQuitMenuPage(isSchizm);
 }
 
-MenuPage *createMenuReahHelp() {
-	return new ReahHelpMenuPage();
+MenuPage *createMenuHelp(bool isSchizm) {
+	return new ReahHelpMenuPage(isSchizm);
 }
 
-MenuPage *createMenuReahSound() {
-	return new ReahSoundMenuPage();
+MenuPage *createMenuSound(bool isSchizm) {
+	return new ReahSoundMenuPage(isSchizm);
 }
 
-MenuPage *createMenuReahPause() {
-	return new ReahPauseMenuPage();
+MenuPage *createMenuPause(bool isSchizm) {
+	return new ReahPauseMenuPage(isSchizm);
 }
 
 } // End of namespace VCruise
diff --git a/engines/vcruise/menu.h b/engines/vcruise/menu.h
index 7e88f08d12d..44132582dc6 100644
--- a/engines/vcruise/menu.h
+++ b/engines/vcruise/menu.h
@@ -27,6 +27,7 @@
 
 namespace Graphics {
 
+class Font;
 struct Surface;
 class ManagedSurface;
 
@@ -61,6 +62,8 @@ public:
 	virtual void quitGame() const = 0;
 	virtual bool canSave() const = 0;
 	virtual bool reloadFromCheckpoint() const = 0;
+
+	virtual void getLabelDef(const Common::String &labelID, const Graphics::Font *&outFont, const Common::String *&outTextUTF8, uint32 &outColor, uint32 &outShadowColor) const = 0;
 };
 
 class MenuPage {
@@ -78,11 +81,11 @@ protected:
 	const MenuInterface *_menuInterface;
 };
 
-MenuPage *createMenuReahMain();
-MenuPage *createMenuReahHelp();
-MenuPage *createMenuReahSound();
-MenuPage *createMenuReahQuit();
-MenuPage *createMenuReahPause();
+MenuPage *createMenuMain(bool isSchizm);
+MenuPage *createMenuHelp(bool isSchizm);
+MenuPage *createMenuSound(bool isSchizm);
+MenuPage *createMenuQuit(bool isSchizm);
+MenuPage *createMenuPause(bool isSchizm);
 
 } // End of namespace VCruise
 
diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 9d0f267f213..ba1404d182c 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -77,6 +77,8 @@ public:
 	bool canSave() const override;
 	bool reloadFromCheckpoint() const override;
 
+	void getLabelDef(const Common::String &labelID, const Graphics::Font *&outFont, const Common::String *&outTextUTF8, uint32 &outColor, uint32 &outShadowColor) const override;
+
 private:
 	Runtime *_runtime;
 };
@@ -117,10 +119,14 @@ Common::Point RuntimeMenuInterface::getMouseCoordinate() const {
 
 void RuntimeMenuInterface::restartGame() const {
 	Common::SharedPtr<SaveGameSnapshot> snapshot(new SaveGameSnapshot());
-
+	
 	snapshot->roomNumber = 1;
 	snapshot->screenNumber = 0xb0;
-	snapshot->loadedAnimation = 1;
+
+	if (_runtime->_gameID == GID_SCHIZM)
+		snapshot->loadedAnimation = 200;
+	else
+		snapshot->loadedAnimation = 1;
 
 	_runtime->_saveGame = snapshot;
 	_runtime->restoreSaveGameSnapshot();
@@ -159,6 +165,11 @@ bool RuntimeMenuInterface::reloadFromCheckpoint() const {
 	return true;
 }
 
+void RuntimeMenuInterface::getLabelDef(const Common::String &labelID, const Graphics::Font *&outFont, const Common::String *&outTextUTF8, uint32 &outColor, uint32 &outShadowColor) const {
+	return _runtime->getLabelDef(labelID, outFont, outTextUTF8, outColor, outShadowColor);
+}
+
+
 AnimationDef::AnimationDef() : animNum(0), firstFrame(0), lastFrame(0) {
 }
 
@@ -900,6 +911,10 @@ void SaveGameSnapshot::writeString(Common::WriteStream *stream, const Common::St
 	stream->writeString(str);
 }
 
+
+FontCacheItem::FontCacheItem() : font(nullptr), size(0) {
+}
+
 Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &rootFSNode, VCruiseGameID gameID, bool isCDVariant, bool isDVDVariant)
 	: _system(system), _mixer(mixer), _roomNumber(1), _screenNumber(0), _direction(0), _hero(0), _haveHorizPanAnimations(false), _loadedRoomNumber(0), _activeScreenNumber(0),
 	  _gameState(kGameStateBoot), _gameID(gameID), _havePendingScreenChange(false), _forceScreenChange(false), _havePendingReturnToIdleState(false), _havePendingCompletionCheck(false),
@@ -1129,11 +1144,9 @@ bool Runtime::bootGame(bool newGame) {
 	_gameState = kGameStateIdle;
 
 	if (newGame) {
-		// TODO: Implement menus and go to b1 in Schizm instead
-		if (_gameID == GID_SCHIZM) {
-			changeToScreen(1, 0xb0);
-			_isInGame = true;
-		} else
+		if (_gameID == GID_SCHIZM)
+			changeToScreen(1, 0xb1);
+		else
 			changeToScreen(1, 0xb1);
 	}
 
@@ -1226,6 +1239,31 @@ bool Runtime::bootGame(bool newGame) {
 	return true;
 }
 
+void Runtime::getLabelDef(const Common::String &labelID, const Graphics::Font *&outFont, const Common::String *&outTextUTF8, uint32 &outColor, uint32 &outShadowColor) {
+	outFont = nullptr;
+	outTextUTF8 = nullptr;
+	outColor = 0;
+	outShadowColor = 0;
+
+	Common::HashMap<Common::String, UILabelDef>::const_iterator labelDefIt = _locUILabels.find(labelID);
+	if (labelDefIt != _locUILabels.end()) {
+		const UILabelDef &labelDef = labelDefIt->_value;
+
+		Common::HashMap<Common::String, Common::String>::const_iterator lineIt = _locStrings.find(labelDef.lineID);
+
+		if (lineIt != _locStrings.end()) {
+			Common::HashMap<Common::String, TextStyleDef>::const_iterator styleIt = _locTextStyles.find(labelDef.styleDefID);
+
+			if (styleIt != _locTextStyles.end()) {
+				outFont = resolveFont(styleIt->_value.fontName, styleIt->_value.size);
+				outColor = styleIt->_value.colorRGB;
+				outShadowColor = styleIt->_value.shadowColorRGB;
+				outTextUTF8 = &lineIt->_value;
+			}
+		}
+	}
+}
+
 bool Runtime::runIdle() {
 	if (_havePendingScreenChange) {
 		_havePendingScreenChange = false;
@@ -1351,7 +1389,7 @@ bool Runtime::runIdle() {
 				switch (osEvent.keymappedEvent) {
 				case kKeymappedEventHelp:
 					if (_gameID == GID_REAH)
-						changeToMenuPage(createMenuReahHelp());
+						changeToMenuPage(createMenuHelp(_gameID == GID_SCHIZM));
 					else
 						error("Don't have a help menu for this game");
 					return true;
@@ -1365,13 +1403,13 @@ bool Runtime::runIdle() {
 					break;
 				case kKeymappedEventPause:
 					if (_gameID == GID_REAH)
-						changeToMenuPage(createMenuReahPause());
+						changeToMenuPage(createMenuPause(_gameID == GID_SCHIZM));
 					else
 						error("Don't have a pause menu for this game");
 					return true;
 				case kKeymappedEventQuit:
 					if (_gameID == GID_REAH)
-						changeToMenuPage(createMenuReahQuit());
+						changeToMenuPage(createMenuQuit(_gameID == GID_SCHIZM));
 					else
 						error("Don't have a quit menu for this game");
 					return true;
@@ -1462,6 +1500,7 @@ bool Runtime::runWaitForAnimation() {
 					_animDecoder->pauseVideo(true);
 					_animDecoderState = kAnimDecoderStatePaused;
 				}
+				_scriptEnv.esc = true;
 				_gameState = kGameStateScript;
 				return true;
 			}
@@ -2029,8 +2068,8 @@ void Runtime::terminateScript() {
 
 	if (_scriptEnv.exitToMenu && _gameState == kGameStateIdle) {
 		changeToCursor(_cursors[kCursorArrow]);
-		if (_gameID == GID_REAH)
-			changeToMenuPage(createMenuReahMain());
+		if (_gameID == GID_REAH || _gameID == GID_SCHIZM)
+			changeToMenuPage(createMenuMain(_gameID == GID_SCHIZM));
 		else
 			error("Missing main menu behavior for this game");
 	}
@@ -3223,7 +3262,7 @@ void Runtime::updateSounds(uint32 timestamp) {
 		}
 
 		if (snd.isLooping) {
-			if (snd.volume == getSilentSoundVolume()) {
+			if (snd.volume <= getSilentSoundVolume()) {
 				if (!snd.isSilencedLoop) {
 					if (snd.cache) {
 						snd.cache->player.reset();
@@ -4069,20 +4108,74 @@ void Runtime::loadSubtitles(Common::CodePage codePage) {
 				frameMap = &_animSubtitles[animID];
 		}
 
-		if (frameMap != nullptr || isWave) {
-			for (const Common::INIFile::KeyValue &kv : section.getKeys()) {
-				if (kv.value.size() < 23)
-					continue;
+		bool isTextData = (section.name == "szTextData");
+		bool isFontData = (section.name == "szFontData");
+		bool isStringData = (section.name.hasPrefix("szData"));
+
+		for (const Common::INIFile::KeyValue &kv : section.getKeys()) {
+			// Tokenize the line
+			Common::Array<Common::String> tokens;
+
+			{
+				const Common::String &valueStr = kv.value;
+
+				uint currentTokenStart = 0;
+				uint nextCharPos = 0;
+				bool isQuotedString = false;
+
+				while (nextCharPos < valueStr.size()) {
+					char c = valueStr[nextCharPos];
+					nextCharPos++;
+
+					if (isQuotedString) {
+						if (c == '\"')
+							isQuotedString = false;
+						continue;
+					}
+
+					if (c == '\"') {
+						isQuotedString = true;
+						continue;
+					}
+
+					if (c == ',') {
+						while (valueStr[currentTokenStart] == ' ')
+							currentTokenStart++;
+
+						tokens.push_back(valueStr.substr(currentTokenStart, (nextCharPos - currentTokenStart) - 1u));
+
+						currentTokenStart = nextCharPos;
+					}
+
+					if (c == ';') {
+						nextCharPos--;
+						break;
+					}
+				}
+
+				while (currentTokenStart < nextCharPos && valueStr[currentTokenStart] == ' ')
+					currentTokenStart++;
+
+				while (nextCharPos > currentTokenStart && valueStr[nextCharPos - 1] == ' ')
+					nextCharPos--;
+
+				if (currentTokenStart < nextCharPos)
+					tokens.push_back(valueStr.substr(currentTokenStart, (nextCharPos - currentTokenStart)));
+			}
 
-				if (kv.value[21] != '\"' || kv.value[kv.value.size() - 1] != '\"')
+			if (frameMap != nullptr || isWave) {
+				if (tokens.size() != 4)
 					continue;
 
-				Common::String locLineParamSlice = kv.value.substr(0, 21);
+				const Common::String &textToken = tokens[3];
+
+				if (textToken[0] != '\"' || textToken[textToken.size() - 1] != '\"')
+					continue;
 
 				uint colorCode = 0;
 				uint param1 = 0;
 				uint param2 = 0;
-				if (sscanf(locLineParamSlice.c_str(), "0x%x, 0x%x, %u, ", &colorCode, &param1, &param2) == 3) {
+				if (sscanf(tokens[0].c_str(), "0x%x", &colorCode) && sscanf(tokens[1].c_str(), "0x%x", &param1) && sscanf(tokens[2].c_str(), "%u", &param2)) {
 					SubtitleDef *subDef = nullptr;
 
 					if (isWave)
@@ -4099,9 +4192,55 @@ void Runtime::loadSubtitles(Common::CodePage codePage) {
 						subDef->color[2] = (colorCode & 0xff);
 						subDef->unknownValue1 = param1;
 						subDef->durationInDeciseconds = param2;
-						subDef->str = kv.value.substr(22, kv.value.size() - 23).decode(codePage).encode(Common::kUtf8);
+						subDef->str = textToken.substr(1, textToken.size() - 2).decode(codePage).encode(Common::kUtf8);
 					}
 				}
+			} else if (isTextData) {
+				if (tokens.size() != 1)
+					continue;
+
+				const Common::String &textToken = tokens[0];
+
+				if (textToken[0] != '\"' || textToken[textToken.size() - 1] != '\"')
+					continue;
+
+				_locStrings[kv.key] = textToken.substr(1, textToken.size() - 2);
+			} else if (isFontData) {
+				if (tokens.size() != 9)
+					continue;
+
+				const Common::String &fontToken = tokens[0];
+
+				if (fontToken[0] != '\"' || fontToken[fontToken.size() - 1] != '\"')
+					continue;
+
+				TextStyleDef tsDef;
+				tsDef.fontName = fontToken.substr(1, fontToken.size() - 2);
+
+				if (sscanf(tokens[1].c_str(), "%u", &tsDef.size) &&
+					sscanf(tokens[2].c_str(), "%u", &tsDef.unknown1) &&
+					sscanf(tokens[3].c_str(), "%u", &tsDef.unknown2) &&
+					sscanf(tokens[4].c_str(), "%u", &tsDef.unknown3) &&
+					sscanf(tokens[5].c_str(), "0x%x", &tsDef.colorRGB) &&
+					sscanf(tokens[6].c_str(), "0x%x", &tsDef.shadowColorRGB) &&
+					sscanf(tokens[7].c_str(), "%u", &tsDef.unknown4) &&
+					sscanf(tokens[8].c_str(), "%u", &tsDef.unknown5)) {
+					_locTextStyles[kv.key] = tsDef;
+				}
+			} else if (isStringData) {
+				if (tokens.size() != 6)
+					continue;
+
+				UILabelDef labelDef;
+				labelDef.lineID = tokens[0];
+				labelDef.styleDefID = tokens[1];
+
+				if (sscanf(tokens[2].c_str(), "%u", &labelDef.unknown1) &&
+					sscanf(tokens[3].c_str(), "%u", &labelDef.unknown2) &&
+					sscanf(tokens[4].c_str(), "%u", &labelDef.unknown3) &&
+					sscanf(tokens[5].c_str(), "%u", &labelDef.unknown4)) {
+					_locUILabels[kv.key] = labelDef;
+				}
 			}
 		}
 	}
@@ -4218,7 +4357,7 @@ void Runtime::dischargeInGameMenuMouseUp() {
 		// Handle click event
 		switch (_inGameMenuActiveElement) {
 		case 0:
-			changeToMenuPage(createMenuReahHelp());
+			changeToMenuPage(createMenuHelp(_gameID == GID_SCHIZM));
 			break;
 		case 1:
 			g_engine->saveGameDialog();
@@ -4227,10 +4366,10 @@ void Runtime::dischargeInGameMenuMouseUp() {
 			g_engine->loadGameDialog();
 			break;
 		case 3:
-			changeToMenuPage(createMenuReahSound());
+			changeToMenuPage(createMenuSound(_gameID == GID_SCHIZM));
 			break;
 		case 4:
-			changeToMenuPage(createMenuReahQuit());
+			changeToMenuPage(createMenuQuit(_gameID == GID_SCHIZM));
 			break;
 		default:
 			break;
@@ -4281,6 +4420,32 @@ void Runtime::drawInGameMenuButton(uint element) {
 	commitSectionToScreen(_menuSection, buttonDestRect);
 }
 
+const Graphics::Font *Runtime::resolveFont(const Common::String &textStyle, uint size) {
+	for (const Common::SharedPtr<FontCacheItem> &item : _fontCache) {
+		if (item->fname == textStyle && item->size == size)
+			return item->font;
+	}
+
+	Common::SharedPtr<FontCacheItem> fcItem(new FontCacheItem());
+	fcItem->fname = textStyle;
+	fcItem->size = size;
+
+
+#ifdef USE_FREETYPE2
+	const char *fontFile = "NotoSans-Regular.ttf";
+
+	fcItem->keepAlive.reset(Graphics::loadTTFFontFromArchive(fontFile, size, Graphics::kTTFSizeModeCharacter, 0, Graphics::kTTFRenderModeLight));
+	fcItem->font = fcItem->keepAlive.get();
+#endif
+
+	if (!fcItem->font)
+		fcItem->font = FontMan.getFontByUsage(Graphics::FontManager::kLocalizedFont);
+
+	_fontCache.push_back(fcItem);
+
+	return fcItem->font;
+}
+
 void Runtime::onLButtonDown(int16 x, int16 y) {
 	onMouseMove(x, y);
 
@@ -5940,7 +6105,14 @@ OPCODE_STUB(ScoreAlways)
 OPCODE_STUB(ScoreNormal)
 
 void Runtime::scriptOpSndPlay(ScriptArg_t arg) {
-	scriptOpSoundL1(arg);
+	TAKE_STACK_STR_NAMED(1, sndNameArgs);
+
+	StackInt_t soundID = 0;
+	SoundInstance *cachedSound = nullptr;
+	resolveSoundByName(sndNameArgs[0], true, soundID, cachedSound);
+
+	if (cachedSound)
+		triggerSound(true, *cachedSound, getSilentSoundVolume(), 0, false, false);
 }
 
 OPCODE_STUB(SndPlayEx)
@@ -5960,9 +6132,8 @@ void Runtime::scriptOpVolumeChange(ScriptArg_t arg) {
 
 	SoundInstance *cachedSound = resolveSoundByID(static_cast<uint>(stackArgs[0]));
 
-	// FIXME: Figure out what the duration scale really is
 	if (cachedSound)
-		triggerSoundRamp(*cachedSound, stackArgs[1], stackArgs[2], false);
+		triggerSoundRamp(*cachedSound, stackArgs[1] * 100, stackArgs[2], false);
 }
 
 OPCODE_STUB(VolumeDown)
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index 54a7765229f..ce9a7787244 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -473,6 +473,37 @@ struct OSEvent {
 	uint32 timestamp;
 };
 
+struct TextStyleDef {
+	Common::String fontName;
+	uint size;
+	uint unknown1;
+	uint unknown2;
+	uint unknown3;	// Seems to always be 0 for English, other values for other languages
+	uint colorRGB;
+	uint shadowColorRGB;
+	uint unknown4;
+	uint unknown5;	// Possibly drop shadow offset
+};
+
+struct UILabelDef {
+	Common::String lineID;
+	Common::String styleDefID;
+	uint unknown1;
+	uint unknown2;
+	uint unknown3;
+	uint unknown4;
+};
+
+struct FontCacheItem {
+	FontCacheItem();
+
+	Common::String fname;
+	uint size;
+
+	const Graphics::Font *font;
+	Common::SharedPtr<Graphics::Font> keepAlive;
+};
+
 class Runtime {
 public:
 	friend class RuntimeMenuInterface;
@@ -506,6 +537,8 @@ public:
 
 	bool bootGame(bool newGame);
 
+	void getLabelDef(const Common::String &labelID, const Graphics::Font *&outFont, const Common::String *&outTextUTF8, uint32 &outColor, uint32 &outShadowColor);
+
 private:
 	enum IndexParseType {
 		kIndexParseTypeNone,
@@ -795,6 +828,8 @@ private:
 	void dischargeInGameMenuMouseUp();
 	void drawInGameMenuButton(uint element);
 
+	const Graphics::Font *resolveFont(const Common::String &textStyle, uint size);
+
 	// Script things
 	void scriptOpNumber(ScriptArg_t arg);
 	void scriptOpRotate(ScriptArg_t arg);
@@ -1177,6 +1212,12 @@ private:
 	Common::Array<SubtitleQueueItem> _subtitleQueue;
 	bool _isDisplayingSubtitles;
 
+	Common::HashMap<Common::String, Common::String> _locStrings;
+	Common::HashMap<Common::String, TextStyleDef> _locTextStyles;
+	Common::HashMap<Common::String, UILabelDef> _locUILabels;
+
+	Common::Array<Common::SharedPtr<FontCacheItem> > _fontCache;
+
 	int32 _dbToVolume[49];
 };
 


Commit: 9923c44a78f4d2b7ce0b6536734379fa7072a794
    https://github.com/scummvm/scummvm/commit/9923c44a78f4d2b7ce0b6536734379fa7072a794
Author: elasota (ejlasota at gmail.com)
Date: 2023-05-03T23:40:44-04:00

Commit Message:
VCRUISE: Fix options

Changed paths:
    engines/vcruise/detection.cpp
    engines/vcruise/detection.h
    engines/vcruise/metaengine.cpp
    engines/vcruise/runtime.cpp


diff --git a/engines/vcruise/detection.cpp b/engines/vcruise/detection.cpp
index 096e9010f2e..454560ba017 100644
--- a/engines/vcruise/detection.cpp
+++ b/engines/vcruise/detection.cpp
@@ -39,7 +39,7 @@ static const PlainGameDescriptor vCruiseGames[] = {
 class VCruiseMetaEngineDetection : public AdvancedMetaEngineDetection {
 public:
 	VCruiseMetaEngineDetection() : AdvancedMetaEngineDetection(VCruise::gameDescriptions, sizeof(VCruise::VCruiseGameDescription), vCruiseGames) {
-		_guiOptions = GUIO1(GAMEOPTION_LAUNCH_DEBUG);
+		_guiOptions = GUIO3(GAMEOPTION_LAUNCH_DEBUG, GAMEOPTION_FAST_ANIMATIONS, GAMEOPTION_SKIP_MENU);
 		_maxScanDepth = 1;
 		_directoryGlobs = nullptr;
 		_flags = kADFlagCanPlayUnknownVariants;
diff --git a/engines/vcruise/detection.h b/engines/vcruise/detection.h
index 6ae8bb0761f..510455e9cfa 100644
--- a/engines/vcruise/detection.h
+++ b/engines/vcruise/detection.h
@@ -50,6 +50,7 @@ struct VCruiseGameDescription {
 
 #define GAMEOPTION_LAUNCH_DEBUG					GUIO_GAMEOPTIONS1
 #define GAMEOPTION_FAST_ANIMATIONS				GUIO_GAMEOPTIONS2
+#define GAMEOPTION_SKIP_MENU					GUIO_GAMEOPTIONS3
 
 
 } // End of namespace VCruise
diff --git a/engines/vcruise/metaengine.cpp b/engines/vcruise/metaengine.cpp
index 57b5d41a1d7..031638c690c 100644
--- a/engines/vcruise/metaengine.cpp
+++ b/engines/vcruise/metaengine.cpp
@@ -48,7 +48,7 @@ static const ADExtraGuiOptionsMap optionsList[] = {
 		}
 	},
 	{
-		GAMEOPTION_LAUNCH_DEBUG,
+		GAMEOPTION_FAST_ANIMATIONS,
 		{
 			_s("Faster animations"),
 			_s("Speeds up animations."),
@@ -58,6 +58,17 @@ static const ADExtraGuiOptionsMap optionsList[] = {
 			0
 		}
 	},
+	{
+		GAMEOPTION_SKIP_MENU,
+		{
+			_s("Skip main menu"),
+			_s("Starts a new game upon launching instead of going to the main menu."),
+			"vcruise_skip_menu",
+			false,
+			0,
+			0
+		}
+	},
 	AD_EXTRA_GUI_OPTIONS_TERMINATOR
 };
 
diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index ba1404d182c..694403962b4 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -4432,7 +4432,7 @@ const Graphics::Font *Runtime::resolveFont(const Common::String &textStyle, uint
 
 
 #ifdef USE_FREETYPE2
-	const char *fontFile = "NotoSans-Regular.ttf";
+	const char *fontFile = "NotoSans-Bold.ttf";
 
 	fcItem->keepAlive.reset(Graphics::loadTTFFontFromArchive(fontFile, size, Graphics::kTTFSizeModeCharacter, 0, Graphics::kTTFRenderModeLight));
 	fcItem->font = fcItem->keepAlive.get();


Commit: 62712ca02b34478a6b14a34cfe451a95cf23b74c
    https://github.com/scummvm/scummvm/commit/62712ca02b34478a6b14a34cfe451a95cf23b74c
Author: elasota (ejlasota at gmail.com)
Date: 2023-05-03T23:40:44-04:00

Commit Message:
VCRUISE: Add skip main menu option functionality.

Changed paths:
    engines/vcruise/runtime.cpp


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 694403962b4..fb660888cd1 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -1144,10 +1144,12 @@ bool Runtime::bootGame(bool newGame) {
 	_gameState = kGameStateIdle;
 
 	if (newGame) {
-		if (_gameID == GID_SCHIZM)
-			changeToScreen(1, 0xb1);
-		else
+		if (ConfMan.hasKey("vcruise_skip_menu") && ConfMan.getBool("vcruise_skip_menu")) {
+			_isInGame = true;
+			changeToScreen(1, 0xb0);
+		} else {
 			changeToScreen(1, 0xb1);
+		}
 	}
 
 	Common::Language lang = Common::parseLanguage(ConfMan.get("language"));




More information about the Scummvm-git-logs mailing list