[Scummvm-git-logs] scummvm master -> 1f4f556e194351202487c48b5d396d62ba32c4b5

sev- noreply at scummvm.org
Tue Feb 21 16:17:16 UTC 2023


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

Summary:
c6a6a0d6e7 VCRUISE: Initial commit, boilerplate
9d179f101f VCRUISE: Add script compiler and initial loading things
e3360047a1 VCRUISE: Add map loader
974fe14b2c VIDEO: Tag V-Cruise as using AVIDecoder
e58c6ba1b2 VCRUISE: Add enough stuff to render opening cinematic
8f8836617b VIDEO: Increase stream name capacity to deal with long (67 character) stream names in Reah
5216131a5d VCRUISE: Add more things to get Reah to the first screen
db5c7050f2 VCRUISE: Rename detection entry from GOG to DVD
9df627d497 VCRUISE: Add game ID initializer for end marker.
3cbb072dd1 VCRUISE: Remove unused variable
10273c1ad0 VCRUISE: Quiet some warnings
4807ac156a VCRUISE: Remove unused variable
8dccd9d2b0 VCRUISE: Quiet more unused variable warnings
0d77df259c VCRUISE: Only allow real 32-bit formats, permit 555 RGB.
c5956be096 VCRUISE: Add Schizm and Reah CD
0ea1beacc8 VCRUISE: Fix video frame overrun
83dee81242 VCRUISE: Fix formatting
bfa24934b7 VCRUISE: Fix script miscompilation
2f69cda8f0 VCRUISE: Implement debug mode and some idle state functionality
0b7c19a070 VCRUISE: Combine env vars, make some tweaks for panorama input detection
02df16b6af VCRUISE: Clean up some code quality issues
bdcf695c13 VCRUISE: Clean up file access
42188df4cc VCRUISE: Stub unsupported opcodes
f64666d782 VCRUISE: Clean up hash map types
31a1fcdfdd VCRUISE: Remove input buffering from TextParser
94de968496 VCRUISE: Use INI loader for index
90354659ab VCRUISE: Use sscanf for number parsing
c7f6aa409a VCRUISE: Cleanup
cbe78b083f VCRUISE: Use sscanf for TextParser number parsing
1f4f556e19 VCRUISE: Initial panorama controls


Commit: c6a6a0d6e70862609caab1ff3420ba0df56d230a
    https://github.com/scummvm/scummvm/commit/c6a6a0d6e70862609caab1ff3420ba0df56d230a
Author: elasota (ejlasota at gmail.com)
Date: 2023-02-21T17:16:53+01:00

Commit Message:
VCRUISE: Initial commit, boilerplate

Changed paths:
  A engines/vcruise/configure.engine
  A engines/vcruise/credits.pl
  A engines/vcruise/detection.cpp
  A engines/vcruise/detection.h
  A engines/vcruise/detection_tables.h
  A engines/vcruise/metaengine.cpp
  A engines/vcruise/module.mk
  A engines/vcruise/runtime.cpp
  A engines/vcruise/runtime.h
  A engines/vcruise/vcruise.cpp
  A engines/vcruise/vcruise.h


diff --git a/engines/vcruise/configure.engine b/engines/vcruise/configure.engine
new file mode 100644
index 00000000000..1f953f440f0
--- /dev/null
+++ b/engines/vcruise/configure.engine
@@ -0,0 +1,4 @@
+# This file is included from the main "configure" script
+# add_engine [name] [desc] [build-by-default] [subengines] [base games] [deps]
+add_engine vcruise "V-Cruise" no "schizm" "" "16bit highres"
+add_engine schizm "Schizm" no "" "" "16bit highres jpeg"
diff --git a/engines/vcruise/credits.pl b/engines/vcruise/credits.pl
new file mode 100644
index 00000000000..00258c50b8d
--- /dev/null
+++ b/engines/vcruise/credits.pl
@@ -0,0 +1,3 @@
+begin_section("V-Cruise");
+	add_person("Eric Lasota", "OneEightHundred", "");
+end_section();
diff --git a/engines/vcruise/detection.cpp b/engines/vcruise/detection.cpp
new file mode 100644
index 00000000000..4780a534c39
--- /dev/null
+++ b/engines/vcruise/detection.cpp
@@ -0,0 +1,105 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "base/plugins.h"
+
+#include "common/config-manager.h"
+#include "common/language.h"
+
+#include "engines/advancedDetector.h"
+
+#include "vcruise/detection.h"
+
+static const PlainGameDescriptor vCruiseGames[] = {
+	{"reah", "Reah: Face the Unknown"},
+	{"schizm", "Schizm: Mysterious Journey"},
+	{nullptr, nullptr}
+};
+
+#include "vcruise/detection_tables.h"
+
+static const char *directoryGlobs[] = {
+	nullptr
+};
+
+class VCruiseMetaEngineDetection : public AdvancedMetaEngineDetection {
+public:
+	VCruiseMetaEngineDetection() : AdvancedMetaEngineDetection(VCruise::gameDescriptions, sizeof(VCruise::VCruiseGameDescription), vCruiseGames) {
+		_guiOptions = GUIO1(GAMEOPTION_LAUNCH_DEBUG);
+		_maxScanDepth = 1;
+		_directoryGlobs = directoryGlobs;
+		_flags = kADFlagCanPlayUnknownVariants;
+	}
+
+	const char *getName() const override {
+		return "vcruise";
+	}
+
+	const char *getEngineName() const override {
+		return "V-Cruise";
+	}
+
+	const char *getOriginalCopyright() const override {
+		return "V-Cruise (C) LK Avalon";
+	}
+
+	DetectedGame toDetectedGame(const ADDetectedGame &adGame, ADDetectedGameExtraInfo *extraInfo) const override {
+		DetectedGame game = AdvancedMetaEngineDetection::toDetectedGame(adGame, extraInfo);
+
+		VCruise::VCruiseGameID gameID = reinterpret_cast<const VCruise::VCruiseGameDescription *>(adGame.desc)->gameID;
+
+		if (gameID == VCruise::GID_REAH) {
+			game.appendGUIOptions(Common::getGameGUIOptionsDescriptionLanguage(Common::NL_NLD));
+			game.appendGUIOptions(Common::getGameGUIOptionsDescriptionLanguage(Common::FR_FRA));
+			game.appendGUIOptions(Common::getGameGUIOptionsDescriptionLanguage(Common::IT_ITA));
+			game.appendGUIOptions(Common::getGameGUIOptionsDescriptionLanguage(Common::DE_DEU));
+			game.appendGUIOptions(Common::getGameGUIOptionsDescriptionLanguage(Common::PL_POL));
+			game.appendGUIOptions(Common::getGameGUIOptionsDescriptionLanguage(Common::ES_ESP));
+		} else if (gameID == VCruise::GID_SCHIZM) {
+			game.appendGUIOptions(Common::getGameGUIOptionsDescriptionLanguage(Common::NL_NLD));
+			game.appendGUIOptions(Common::getGameGUIOptionsDescriptionLanguage(Common::FR_FRA));
+			game.appendGUIOptions(Common::getGameGUIOptionsDescriptionLanguage(Common::IT_ITA));
+			game.appendGUIOptions(Common::getGameGUIOptionsDescriptionLanguage(Common::DE_DEU));
+			game.appendGUIOptions(Common::getGameGUIOptionsDescriptionLanguage(Common::PL_POL));
+			game.appendGUIOptions(Common::getGameGUIOptionsDescriptionLanguage(Common::ES_ESP));
+			game.appendGUIOptions(Common::getGameGUIOptionsDescriptionLanguage(Common::EL_GRC));
+			game.appendGUIOptions(Common::getGameGUIOptionsDescriptionLanguage(Common::RU_RUS));
+		}
+
+		return game;
+	}
+
+	/*
+	Common::String parseAndCustomizeGuiOptions(const Common::String &optionsString, const Common::String &domain) const {
+		Common::String guiOptions = AdvancedMetaEngineDetection::parseAndCustomizeGuiOptions(optionsString, domain);
+
+		if (domain.hasPrefix("reah")) {
+			guiOptions += " lang_Dutch lang_French lang_Italian lang_German lang_Polish lang_Spanish";
+		} else if (domain.hasPrefix("schizm")) {
+			guiOptions += " lang_Dutch lang_French lang_Italian lang_German lang_Greek lang_Polish lang_Russian lang_Spanish";
+		}
+
+		return guiOptions;
+	}
+	*/
+};
+
+REGISTER_PLUGIN_STATIC(VCRUISE_DETECTION, PLUGIN_TYPE_ENGINE_DETECTION, VCruiseMetaEngineDetection);
diff --git a/engines/vcruise/detection.h b/engines/vcruise/detection.h
new file mode 100644
index 00000000000..8e7c8c64efc
--- /dev/null
+++ b/engines/vcruise/detection.h
@@ -0,0 +1,46 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef VCRUISE_DETECTION_H
+#define VCRUISE_DETECTION_H
+
+#include "engines/advancedDetector.h"
+
+namespace VCruise {
+
+enum VCruiseGameID {
+	GID_REAH	= 0,
+	GID_SCHIZM	= 1,
+};
+
+struct VCruiseGameDescription {
+	ADGameDescription desc;
+
+	VCruiseGameID gameID;
+};
+
+
+#define GAMEOPTION_LAUNCH_DEBUG					GUIO_GAMEOPTIONS1
+
+
+} // End of namespace VCruise
+
+#endif // VCRUISE_DETECTION_H
diff --git a/engines/vcruise/detection_tables.h b/engines/vcruise/detection_tables.h
new file mode 100644
index 00000000000..44b555f485f
--- /dev/null
+++ b/engines/vcruise/detection_tables.h
@@ -0,0 +1,53 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef VCRUISE_DETECTION_TABLES_H
+#define VCRUISE_DETECTION_TABLES_H
+
+#include "engines/advancedDetector.h"
+
+#include "vcruise/detection.h"
+
+namespace VCruise {
+
+static const VCruiseGameDescription gameDescriptions[] = {
+
+	{ // Reah: Face the Unknown, GOG downloadable version
+		{
+			"reah",
+			"GOG",
+			{
+				{"Reah.exe", 0, "60ec19c53f1323cc7f0314f98d396283", 304128},
+				AD_LISTEND
+			},
+			Common::EN_ANY,
+			Common::kPlatformWindows,
+			ADGF_UNSTABLE,
+			GUIO0()
+		},
+		GID_REAH,
+	},
+	{ AD_TABLE_END_MARKER }
+};
+
+} // End of namespace MTropolis
+
+#endif
diff --git a/engines/vcruise/metaengine.cpp b/engines/vcruise/metaengine.cpp
new file mode 100644
index 00000000000..0994980ed8b
--- /dev/null
+++ b/engines/vcruise/metaengine.cpp
@@ -0,0 +1,74 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/translation.h"
+
+#include "engines/advancedDetector.h"
+#include "vcruise/vcruise.h"
+
+namespace Graphics {
+
+struct Surface;
+
+} // End of namespace Graphics
+
+namespace VCruise {
+
+static const ADExtraGuiOptionsMap optionsList[] = {
+	{GAMEOPTION_LAUNCH_DEBUG,
+	 {_s("Start with debugger"),
+	  _s("Starts with the debugger dashboard active."),
+	  "vcruise_debug",
+	  false,
+	  0,
+	  0}},
+	AD_EXTRA_GUI_OPTIONS_TERMINATOR};
+
+} // End of namespace VCruise
+
+class VCruiseMetaEngine : public AdvancedMetaEngine {
+public:
+	const char *getName() const override {
+		return "vcruise";
+	}
+
+	const ADExtraGuiOptionsMap *getAdvancedExtraGuiOptions() const override {
+		return VCruise::optionsList;
+	}
+
+	bool hasFeature(MetaEngineFeature f) const override;
+	Common::Error createInstance(OSystem *syst, Engine **engine, const ADGameDescription *desc) const override;
+};
+
+bool VCruiseMetaEngine::hasFeature(MetaEngineFeature f) const {
+	return checkExtendedSaves(f);
+}
+
+Common::Error VCruiseMetaEngine::createInstance(OSystem *syst, Engine **engine, const ADGameDescription *desc) const {
+	*engine = new VCruise::VCruiseEngine(syst, reinterpret_cast<const VCruise::VCruiseGameDescription *>(desc));
+	return Common::kNoError;
+}
+
+#if PLUGIN_ENABLED_DYNAMIC(VCRUISE)
+REGISTER_PLUGIN_DYNAMIC(VCRUISE, PLUGIN_TYPE_ENGINE, VCruiseMetaEngine);
+#else
+REGISTER_PLUGIN_STATIC(VCRUISE, PLUGIN_TYPE_ENGINE, VCruiseMetaEngine);
+#endif
diff --git a/engines/vcruise/module.mk b/engines/vcruise/module.mk
new file mode 100644
index 00000000000..63eeca9f809
--- /dev/null
+++ b/engines/vcruise/module.mk
@@ -0,0 +1,18 @@
+MODULE := engines/vcruise
+
+MODULE_OBJS = \
+	metaengine.o \
+	runtime.o \
+	vcruise.o
+
+
+# This module can be built as a plugin
+ifeq ($(ENABLE_VCRUISE), DYNAMIC_PLUGIN)
+PLUGIN := 1
+endif
+
+# Include common rules
+include $(srcdir)/rules.mk
+
+# Detection objects
+DETECT_OBJS += $(MODULE)/detection.o
diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
new file mode 100644
index 00000000000..d77b0771c5f
--- /dev/null
+++ b/engines/vcruise/runtime.cpp
@@ -0,0 +1,106 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/formats/winexe.h"
+#include "common/ptr.h"
+#include "common/system.h"
+
+#include "graphics/cursorman.h"
+#include "graphics/wincursor.h"
+#include "graphics/managed_surface.h"
+
+#include "vcruise/runtime.h"
+
+
+
+namespace VCruise {
+
+Runtime::Runtime(OSystem *system) : _system(system), _roomNumber(1), _gameState(kGameStateBoot) {
+}
+
+Runtime::~Runtime() {
+}
+
+void Runtime::loadCursors(const char *exeName) {
+	Common::SharedPtr<Common::WinResources> winRes(Common::WinResources::createFromEXE(exeName));
+	if (!winRes)
+		error("Couldn't open executable file %s", exeName);
+
+	Common::Array<Common::WinResourceID> cursorGroupIDs = winRes->getIDList(Common::kWinGroupCursor);
+	for (Common::Array<Common::WinResourceID>::const_iterator it = cursorGroupIDs.begin(), itEnd = cursorGroupIDs.end(); it != itEnd; ++it) {
+		const Common::WinResourceID &id = *it;
+
+		Common::SharedPtr<Graphics::WinCursorGroup> cursorGroup(Graphics::WinCursorGroup::createCursorGroup(winRes.get(), *it));
+		if (!winRes) {
+			warning("Couldn't load cursor group");
+			continue;
+		}
+
+		Common::String nameStr = id.getString();
+		if (nameStr.size() == 8 && nameStr.substr(0, 7) == "CURSOR_") {
+			char c = nameStr[7];
+			if (c >= '0' && c <= '9') {
+				uint shortID = c - '0';
+				if (shortID >= _cursorsShort.size())
+					_cursorsShort.resize(shortID + 1);
+				_cursorsShort[shortID] = cursorGroup;
+			}
+		} else if (nameStr.size() == 13 && nameStr.substr(0, 11) == "CURSOR_CUR_") {
+			char c1 = nameStr[11];
+			char c2 = nameStr[12];
+			if (c1 >= '0' && c1 <= '9' && c2 >= '0' && c2 <= '9') {
+				uint longID = (c1 - '0') * 10 + (c2 - '0');
+				if (longID >= _cursors.size())
+					_cursors.resize(longID + 1);
+				_cursors[longID] = cursorGroup;
+			}
+		}
+	}
+}
+
+bool Runtime::runFrame() {
+	bool moreActions = true;
+	while (moreActions) {
+		moreActions = false;
+		switch (_gameState) {
+		case kGameStateBoot:
+			bootGame();
+			break;
+		case kGameStateQuit:
+			return false;
+		default:
+			error("Unknown game state");
+			return false;
+		}
+	}
+
+	return true;
+}
+
+void Runtime::bootGame() {
+}
+
+void Runtime::drawFrame() {
+
+	_system->updateScreen();
+}
+
+} // End of namespace VCruise
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
new file mode 100644
index 00000000000..b6c653cb068
--- /dev/null
+++ b/engines/vcruise/runtime.h
@@ -0,0 +1,64 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef VCRUISE_RUNTIME_H
+#define VCRUISE_RUNTIME_H
+
+class OSystem;
+
+namespace Graphics {
+
+struct WinCursorGroup;
+
+} // End of namespace Graphics
+
+namespace VCruise {
+
+enum GameState {
+	kGameStateBoot,
+	kGameStateCinematic,
+	kGameStateQuit,
+};
+
+class Runtime {
+public:
+	explicit Runtime(OSystem *system);
+	virtual ~Runtime();
+
+	void loadCursors(const char *exeName);
+
+	bool runFrame();
+	void drawFrame();
+
+private:
+	void bootGame();
+
+	Common::Array<Common::SharedPtr<Graphics::WinCursorGroup> > _cursors;		// Cursors indexed as CURSOR_CUR_##
+	Common::Array<Common::SharedPtr<Graphics::WinCursorGroup> > _cursorsShort;	// Cursors indexed as CURSOR_#
+
+	OSystem *_system;
+	uint _roomNumber;
+	GameState _gameState;
+};
+
+} // End of namespace VCruise
+
+#endif
diff --git a/engines/vcruise/vcruise.cpp b/engines/vcruise/vcruise.cpp
new file mode 100644
index 00000000000..c084988b570
--- /dev/null
+++ b/engines/vcruise/vcruise.cpp
@@ -0,0 +1,157 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "engines/util.h"
+
+#include "common/config-manager.h"
+#include "common/events.h"
+#include "common/system.h"
+#include "common/algorithm.h"
+
+#include "vcruise/runtime.h"
+#include "vcruise/vcruise.h"
+
+namespace VCruise {
+
+VCruiseEngine::VCruiseEngine(OSystem *syst, const VCruiseGameDescription *gameDesc) : Engine(syst), _gameDescription(gameDesc) {
+	const Common::FSNode gameDataDir(ConfMan.get("path"));
+}
+
+VCruiseEngine::~VCruiseEngine() {
+}
+
+void VCruiseEngine::handleEvents() {
+	Common::Event evt;
+	Common::EventManager *eventMan = _system->getEventManager();
+
+	while (eventMan->pollEvent(evt)) {
+		switch (evt.type) {
+		default:
+			break;
+		}
+	}
+}
+
+Common::Error VCruiseEngine::run() {
+	Common::List<Graphics::PixelFormat> pixelFormats = _system->getSupportedFormats();
+
+	const Graphics::PixelFormat *fmt16 = nullptr;
+	const Graphics::PixelFormat *fmt32 = nullptr;
+
+	for (const Graphics::PixelFormat &fmt : pixelFormats) {
+		if (fmt.rBits() == 8 && fmt.gBits() == 8 && fmt.bBits() == 8)
+			fmt32 = &fmt;
+		if ((fmt.rBits() + fmt.gBits() + fmt.bBits()) == 16)
+			fmt16 = &fmt;
+	}
+
+	// Figure out screen layout
+	Common::Point size;
+
+	Common::Point videoSize;
+	Common::Point traySize;
+	Common::Point menuBarSize;
+	const char *exeName = nullptr;
+
+	if (_gameDescription->gameID == GID_REAH) {
+		videoSize = Common::Point(608, 348);
+		menuBarSize = Common::Point(640, 44);
+		traySize = Common::Point(640, 88);
+		exeName = "Reah.exe";
+	} else if (_gameDescription->gameID == GID_SCHIZM) {
+		videoSize = Common::Point(640, 360);
+		menuBarSize = Common::Point(640, 32);
+		traySize = Common::Point(640, 88);
+		exeName = "Schizm.exe";
+	} else {
+		error("Unknown game");
+	}
+
+	size.x = videoSize.x;
+	if (menuBarSize.x > size.x)
+		size.x = menuBarSize.x;
+	if (traySize.x > size.x)
+		size.x = traySize.x;
+
+	size.y = videoSize.y + menuBarSize.y + traySize.y;
+
+	Common::Point menuTL = Common::Point((size.x - menuBarSize.x) / 2, 0);
+	Common::Point videoTL = Common::Point((size.x - videoSize.x) / 2, menuTL.y);
+	Common::Point trayTL = Common::Point((size.x - traySize.x) / 2, videoTL.y);
+
+	_menuBarRect = Common::Rect(menuTL.x, menuTL.y, menuTL.x + menuBarSize.x, menuTL.y + menuBarSize.y);
+	_videoRect = Common::Rect(videoTL.x, videoTL.y, videoTL.x + videoSize.x, videoTL.y + videoSize.y);
+	_trayRect = Common::Rect(trayTL.x, trayTL.y, trayTL.x + traySize.x, trayTL.y + traySize.y);
+
+	if (fmt32)
+		initGraphics(size.x, size.y, fmt32);
+	else if (fmt16)
+		initGraphics(size.x, size.y, fmt16);
+	else
+		error("Unable to find a suitable graphics format");
+
+	_runtime.reset(new Runtime(_system));
+
+	_runtime->loadCursors(exeName);
+
+	// Run the game
+	while (!shouldQuit()) {
+		handleEvents();
+
+		if (!_runtime->runFrame())
+			break;
+
+		_runtime->drawFrame();
+		_system->delayMillis(10);
+	}
+
+	_runtime.reset();
+
+	return Common::kNoError;
+}
+
+void VCruiseEngine::pauseEngineIntern(bool pause) {
+	Engine::pauseEngineIntern(pause);
+}
+
+bool VCruiseEngine::hasFeature(EngineFeature f) const {
+	switch (f) {
+	case kSupportsReturnToLauncher:
+	case kSupportsSavingDuringRuntime:
+		return true;
+	default:
+		return false;
+	};
+}
+
+Common::Error VCruiseEngine::saveGameStream(Common::WriteStream *stream, bool isAutosave) {
+	return Common::Error(Common::kUnknownError);
+}
+
+bool VCruiseEngine::canSaveAutosaveCurrently() {
+	return false;
+}
+
+bool VCruiseEngine::canSaveGameStateCurrently() {
+	return false;
+}
+
+} // End of namespace VCruise
diff --git a/engines/vcruise/vcruise.h b/engines/vcruise/vcruise.h
new file mode 100644
index 00000000000..0aa35004c8a
--- /dev/null
+++ b/engines/vcruise/vcruise.h
@@ -0,0 +1,75 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef VCRUISE_VCRUISE_H
+#define VCRUISE_VCRUISE_H
+
+#include "engines/engine.h"
+#include "common/rect.h"
+
+#include "vcruise/runtime.h"
+#include "vcruise/detection.h"
+
+/**
+ * This is the namespace of the V-Cruise engine.
+ *
+ * Status of this engine: ???
+ *
+ * Games using this engine:
+ * - Reah: Face the Unknown
+ * - Schizm: Mysterious Journey
+ */
+namespace VCruise {
+
+class VCruiseEngine : public ::Engine {
+protected:
+	// Engine APIs
+	Common::Error run() override;
+
+public:
+	VCruiseEngine(OSystem *syst, const VCruiseGameDescription *gameDesc);
+	~VCruiseEngine() override;
+
+	bool hasFeature(EngineFeature f) const override;
+	//void syncSoundSettings() override;
+
+	const VCruiseGameDescription *_gameDescription;
+
+	Common::Error saveGameStream(Common::WriteStream *stream, bool isAutosave) override;
+	bool canSaveAutosaveCurrently() override;
+	bool canSaveGameStateCurrently() override;
+
+protected:
+	void pauseEngineIntern(bool pause) override;
+
+private:
+	void handleEvents();
+
+	Common::Rect _videoRect;
+	Common::Rect _menuBarRect;
+	Common::Rect _trayRect;
+
+	Common::SharedPtr<Runtime> _runtime;
+};
+
+} // End of namespace VCruise
+
+#endif /* VCRUISE_VCRUISE_H */


Commit: 9d179f101f32b771ed2680bf4cdaf14b4bb04603
    https://github.com/scummvm/scummvm/commit/9d179f101f32b771ed2680bf4cdaf14b4bb04603
Author: elasota (ejlasota at gmail.com)
Date: 2023-02-21T17:16:53+01:00

Commit Message:
VCRUISE: Add script compiler and initial loading things

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


diff --git a/engines/vcruise/module.mk b/engines/vcruise/module.mk
index 63eeca9f809..026a3a98277 100644
--- a/engines/vcruise/module.mk
+++ b/engines/vcruise/module.mk
@@ -3,6 +3,8 @@ MODULE := engines/vcruise
 MODULE_OBJS = \
 	metaengine.o \
 	runtime.o \
+	script.o \
+	textparser.o \
 	vcruise.o
 
 
diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index d77b0771c5f..df09638a907 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -22,18 +22,29 @@
 #include "common/formats/winexe.h"
 #include "common/ptr.h"
 #include "common/system.h"
+#include "common/stream.h"
 
 #include "graphics/cursorman.h"
 #include "graphics/wincursor.h"
 #include "graphics/managed_surface.h"
 
 #include "vcruise/runtime.h"
-
+#include "vcruise/script.h"
+#include "vcruise/textparser.h"
 
 
 namespace VCruise {
 
-Runtime::Runtime(OSystem *system) : _system(system), _roomNumber(1), _gameState(kGameStateBoot) {
+AnimationDef::AnimationDef() : animNum(0), firstFrame(0), lastFrame(0) {
+}
+
+
+Runtime::Runtime(OSystem *system, const Common::FSNode &rootFSNode, VCruiseGameID gameID)
+	: _system(system), _roomNumber(1), _screenNumber(0), _loadedRoomNumber(0), _activeScreenNumber(0), _gameState(kGameStateBoot), _gameID(gameID), _rootFSNode(rootFSNode), _havePendingScreenChange(false) {
+
+	_logDir = _rootFSNode.getChild("Log");
+	if (!_logDir.exists() || !_logDir.isDirectory())
+		error("Couldn't resolve Log directory");
 }
 
 Runtime::~Runtime() {
@@ -82,10 +93,13 @@ bool Runtime::runFrame() {
 		moreActions = false;
 		switch (_gameState) {
 		case kGameStateBoot:
-			bootGame();
+			moreActions = bootGame();
 			break;
 		case kGameStateQuit:
 			return false;
+		case kGameStateRunning:
+			moreActions = runGame();
+			break;
 		default:
 			error("Unknown game state");
 			return false;
@@ -95,7 +109,244 @@ bool Runtime::runFrame() {
 	return true;
 }
 
-void Runtime::bootGame() {
+bool Runtime::bootGame() {
+	debug(1, "Booting V-Cruise game...");
+	loadIndex();
+	debug(1, "Index loaded OK");
+
+	_gameState = kGameStateRunning;
+
+	if (_gameID == GID_REAH) {
+		// TODO: Change to the logo instead (0xb1) instead when menus are implemented
+		_roomNumber = 1;
+		_screenNumber = 0xb0;
+		_havePendingScreenChange = true;
+	} else
+		error("Couldn't figure out what screen to start on");
+
+	return true;
+}
+
+bool Runtime::runGame() {
+
+	if (_havePendingScreenChange) {
+		_havePendingScreenChange = false;
+
+		changeToScreen(_roomNumber, _screenNumber);
+		return true;
+	}
+
+	return false;
+}
+
+void Runtime::loadIndex() {
+	Common::FSNode indexFSNode = _logDir.getChild("Index.txt");
+
+	Common::ReadStream *stream = indexFSNode.createReadStream();
+	if (!stream)
+		error("Failed to open main index");
+
+	Common::String blamePath = indexFSNode.getPath();
+
+	TextParser parser(stream);
+
+	Common::String token;
+	TextParserState state;
+
+	static const IndexPrefixTypePair parsePrefixes[] = {
+		{"Room", kIndexParseTypeRoom},
+		{"RRoom", kIndexParseTypeRRoom},
+		{"YRoom", kIndexParseTypeYRoom},
+		{"VRoom", kIndexParseTypeVRoom},
+		{"TRoom", kIndexParseTypeTRoom},
+		{"CRoom", kIndexParseTypeCRoom},
+		{"SRoom", kIndexParseTypeSRoom},
+	};
+
+	IndexParseType indexParseType = kIndexParseTypeNone;
+	uint currentRoomNumber = 0;
+
+	for (;;) {
+		char firstCh = 0;
+		if (!parser.skipWhitespaceAndComments(firstCh, state))
+			break;
+
+		if (firstCh == '[') {
+			if (!parser.parseToken(token, state))
+				error("Index open bracket wasn't terminated");
+
+			if (token == "NameRoom") {
+				indexParseType = kIndexParseTypeNameRoom;
+			} else {
+				bool foundType = false;
+				uint prefixLen = 0;
+				for (const IndexPrefixTypePair &prefixTypePair : parsePrefixes) {
+					uint len = strlen(prefixTypePair.prefix);
+					if (token.size() > len && !memcmp(token.c_str(), prefixTypePair.prefix, len)) {
+						indexParseType = prefixTypePair.parseType;
+						foundType = true;
+						prefixLen = len;
+						break;
+					}
+				}
+
+				if (!foundType)
+					error("Unknown index heading type %s", token.c_str());
+
+				currentRoomNumber = 0;
+				for (uint i = prefixLen; i < token.size(); i++) {
+					char digit = token[i];
+					if (digit < '0' || digit > '9')
+						error("Malformed room def");
+					currentRoomNumber = currentRoomNumber * 10 + (token[i] - '0');
+				}
+			}
+
+			parser.expect("]", blamePath);
+
+			allocateRoomsUpTo(currentRoomNumber);
+		} else {
+			parser.requeue(&firstCh, 1, state);
+
+			if (!parseIndexDef(parser, indexParseType, currentRoomNumber, blamePath))
+				break;
+		}
+	}
+}
+
+void Runtime::changeToScreen(uint roomNumber, uint screenNumber) {
+	bool changedRoom = (roomNumber != _loadedRoomNumber);
+	bool changedScreen = (screenNumber != _activeScreenNumber) || changedRoom;
+
+	if (changedRoom) {
+		Common::String logFileName = Common::String::format("Room%02i.log", static_cast<int>(roomNumber));
+
+		Common::FSNode logFileNode = _logDir.getChild(logFileName);
+		if (Common::SeekableReadStream *logicFile = logFileNode.createReadStream())
+			_scriptSet = compileLogicFile(*logicFile, static_cast<uint>(logicFile->size()), logFileNode.getPath());
+		else
+			_scriptSet.reset();
+	}
+}
+
+bool Runtime::parseIndexDef(TextParser &parser, IndexParseType parseType, uint roomNumber, const Common::String &blamePath) {
+	Common::String lineText;
+	parser.expectLine(lineText, blamePath, true);
+
+	Common::MemoryReadStream lineStream(reinterpret_cast<const byte *>(lineText.c_str()), lineText.size(), DisposeAfterUse::NO);
+	TextParser strParser(&lineStream);
+
+	switch (parseType) {
+	case kIndexParseTypeNameRoom: {
+			uint nameRoomNumber = 0;
+			Common::String name;
+			strParser.expectToken(name, blamePath);
+			strParser.expect("=", blamePath);
+			strParser.expectUInt(nameRoomNumber, blamePath);
+
+			allocateRoomsUpTo(nameRoomNumber);
+			_roomDefs[nameRoomNumber]->name = name;
+		} break;
+	case kIndexParseTypeRoom: {
+			Common::String name;
+
+			AnimationDef animDef;
+
+			strParser.expectToken(name, blamePath);
+			strParser.expect("=", blamePath);
+			strParser.expectInt(animDef.animNum, blamePath);
+			strParser.expect(",", blamePath);
+			strParser.expectUInt(animDef.firstFrame, blamePath);
+			strParser.expect(",", blamePath);
+			strParser.expectUInt(animDef.lastFrame, blamePath);
+			_roomDefs[roomNumber]->animations[name] = animDef;
+		} break;
+	case kIndexParseTypeRRoom: {
+			Common::String name;
+
+			Common::Rect rect;
+
+			strParser.expectToken(name, blamePath);
+			strParser.expect("=", blamePath);
+			strParser.expectShort(rect.left, blamePath);
+			strParser.expect(",", blamePath);
+			strParser.expectShort(rect.top, blamePath);
+			strParser.expect(",", blamePath);
+			strParser.expectShort(rect.right, blamePath);
+
+			// Line 4210 in Reah contains an animation def instead of a rect def, detect this and discard
+			if (!strParser.checkEOL()) {
+				strParser.expect(",", blamePath);
+				strParser.expectShort(rect.bottom, blamePath);
+
+				_roomDefs[roomNumber]->rects[name] = rect;
+			}
+
+		} break;
+	case kIndexParseTypeYRoom: {
+			Common::String name;
+
+			uint varSlot = 0;
+
+			strParser.expectToken(name, blamePath);
+			strParser.expect("=", blamePath);
+			strParser.expectUInt(varSlot, blamePath);
+
+			_roomDefs[roomNumber]->vars[name] = varSlot;
+		} break;
+	case kIndexParseTypeVRoom: {
+			Common::String name;
+
+			int value = 0;
+
+			strParser.expectToken(name, blamePath);
+			strParser.expect("=", blamePath);
+			strParser.expectInt(value, blamePath);
+
+			_roomDefs[roomNumber]->values[name] = value;
+		} break;
+	case kIndexParseTypeTRoom: {
+			Common::String name;
+			Common::String value;
+
+			strParser.expectToken(name, blamePath);
+			strParser.expect("=", blamePath);
+			strParser.expectLine(value, blamePath, false);
+
+			_roomDefs[roomNumber]->texts[name] = value;
+		} break;
+	case kIndexParseTypeCRoom: {
+			Common::String name;
+			int value;
+
+			strParser.expectToken(name, blamePath);
+			strParser.expect("=", blamePath);
+			strParser.expectInt(value, blamePath);
+
+			_roomDefs[roomNumber]->consts[name] = value;
+		} break;
+	case kIndexParseTypeSRoom: {
+			Common::String name;
+			int value;
+
+			strParser.expectToken(name, blamePath);
+			strParser.expect("=", blamePath);
+			strParser.expectInt(value, blamePath);
+
+			_roomDefs[roomNumber]->sounds[name] = value;
+		} break;
+	default:
+		assert(false);
+		return false;
+	}
+
+	return true;
+}
+
+void Runtime::allocateRoomsUpTo(uint roomNumber) {
+	while (_roomDefs.size() <= roomNumber) {
+		_roomDefs.push_back(Common::SharedPtr<RoomDef>(new RoomDef()));
+	}
 }
 
 void Runtime::drawFrame() {
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index b6c653cb068..7928dfcaa6a 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -22,6 +22,10 @@
 #ifndef VCRUISE_RUNTIME_H
 #define VCRUISE_RUNTIME_H
 
+#include "common/hashmap.h"
+
+#include "vcruise/detection.h"
+
 class OSystem;
 
 namespace Graphics {
@@ -32,15 +36,39 @@ struct WinCursorGroup;
 
 namespace VCruise {
 
+class TextParser;
+struct ScriptSet;
+
 enum GameState {
 	kGameStateBoot,
 	kGameStateCinematic,
 	kGameStateQuit,
+	kGameStateRunning,
+};
+
+struct AnimationDef {
+	AnimationDef();
+
+	int animNum;	// May be negative if reversed
+	uint firstFrame;
+	uint lastFrame;	// Inclusive
+};
+
+struct RoomDef {
+	Common::HashMap<Common::String, AnimationDef> animations;
+	Common::HashMap<Common::String, Common::Rect> rects;
+	Common::HashMap<Common::String, uint> vars;
+	Common::HashMap<Common::String, int> values;
+	Common::HashMap<Common::String, Common::String> texts;
+	Common::HashMap<Common::String, int> consts;
+	Common::HashMap<Common::String, int> sounds;
+
+	Common::String name;
 };
 
 class Runtime {
 public:
-	explicit Runtime(OSystem *system);
+	Runtime(OSystem *system, const Common::FSNode &rootFSNode, VCruiseGameID gameID);
 	virtual ~Runtime();
 
 	void loadCursors(const char *exeName);
@@ -49,14 +77,50 @@ public:
 	void drawFrame();
 
 private:
-	void bootGame();
+	bool bootGame();
+	bool runGame();
+
+	void loadIndex();
+	void changeToScreen(uint roomNumber, uint screenNumber);
 
 	Common::Array<Common::SharedPtr<Graphics::WinCursorGroup> > _cursors;		// Cursors indexed as CURSOR_CUR_##
 	Common::Array<Common::SharedPtr<Graphics::WinCursorGroup> > _cursorsShort;	// Cursors indexed as CURSOR_#
 
 	OSystem *_system;
-	uint _roomNumber;
+	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 _loadedRoomNumber;
+	uint _activeScreenNumber;
+	bool _havePendingScreenChange;
 	GameState _gameState;
+	VCruiseGameID _gameID;
+
+	Common::FSNode _rootFSNode;
+	Common::FSNode _logDir;
+
+	Common::Array<Common::SharedPtr<RoomDef> > _roomDefs;
+	Common::SharedPtr<ScriptSet> _scriptSet;
+
+	enum IndexParseType {
+		kIndexParseTypeNone,
+		kIndexParseTypeRoom,
+		kIndexParseTypeRRoom,	// Rectangle room (constrains animation areas)
+		kIndexParseTypeYRoom,	// Yes room (variable/ID mappings)
+		kIndexParseTypeVRoom,	// Value room (value/ID mappings?)
+		kIndexParseTypeTRoom,	// Text
+		kIndexParseTypeCRoom,	// Const
+		kIndexParseTypeSRoom,	// Sound
+		kIndexParseTypeNameRoom,
+	};
+
+	struct IndexPrefixTypePair {
+		const char *prefix;
+		IndexParseType parseType;
+	};
+
+	bool parseIndexDef(TextParser &parser, IndexParseType parseType, uint roomNumber, const Common::String &blamePath);
+	void allocateRoomsUpTo(uint roomNumber);
 };
 
 } // End of namespace VCruise
diff --git a/engines/vcruise/script.cpp b/engines/vcruise/script.cpp
new file mode 100644
index 00000000000..569e3c79f56
--- /dev/null
+++ b/engines/vcruise/script.cpp
@@ -0,0 +1,770 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/stream.h"
+#include "common/hash-str.h"
+
+#include "engines/vcruise/script.h"
+#include "engines/vcruise/textparser.h"
+
+
+namespace VCruise {
+
+class LogicUnscrambleStream : public Common::ReadStream {
+public:
+	LogicUnscrambleStream(Common::ReadStream *stream, uint streamSize);
+
+	bool eos() const override;
+	uint32 read(void *dataPtr, uint32 dataSize) override;
+
+private:
+	byte _cipher[255];
+	uint _cipherOffset;
+	Common::ReadStream *_stream;
+};
+
+LogicUnscrambleStream::LogicUnscrambleStream(Common::ReadStream *stream, uint streamSize) : _stream(stream) {
+	int key = 255;
+	for (int i = 0; i < 255; i++) {
+		int parityBit = ((key ^ (key >> 1) ^ (key >> 6) ^ (key >> 7))) & 1;
+		key = (key >> 1) | (parityBit << 7);
+		_cipher[254 - i] = key;
+	}
+
+	_cipherOffset = 255u - (streamSize % 255u);
+}
+
+bool LogicUnscrambleStream::eos() const {
+	return _stream->eos();
+}
+
+uint32 LogicUnscrambleStream::read(void *dataPtr, uint32 dataSize) {
+	uint32 numRead = _stream->read(dataPtr, dataSize);
+
+	byte *decipher = static_cast<byte *>(dataPtr);
+
+	uint cipherOffset = _cipherOffset;
+
+	uint32 remaining = numRead;
+	while (remaining) {
+		if (cipherOffset == 255)
+			cipherOffset = 0;
+
+		(*decipher++) ^= _cipher[cipherOffset++];
+		remaining--;
+	}
+
+	_cipherOffset = cipherOffset;
+	return numRead;
+}
+
+Instruction::Instruction() : op(ScriptOps::kInvalid), arg(0) {
+}
+
+Instruction::Instruction(ScriptOps::ScriptOp paramOp) : op(paramOp), arg(0) {
+}
+
+Instruction::Instruction(ScriptOps::ScriptOp paramOp, int32 paramArg) : op(paramOp), arg(paramArg) {
+}
+
+enum ProtoOp {
+	kProtoOpScript, // Use script opcode
+
+	kProtoOpJumpToLabel,
+	kProtoOpLabel,
+
+	kProtoOpIf,
+	kProtoOpElse,
+	kProtoOpEndIf,
+	kProtoOpSwitch,
+	kProtoOpCase,
+	kProtoOpEndSwitch,
+	kProtoOpDefault,
+	kProtoOpBreak,
+};
+
+struct ProtoInstruction {
+	ProtoInstruction();
+	explicit ProtoInstruction(ScriptOps::ScriptOp op);
+	ProtoInstruction(ScriptOps::ScriptOp paramOp, int32 paramArg);
+	ProtoInstruction(ProtoOp paramProtoOp, ScriptOps::ScriptOp paramOp, int32 paramArg);
+
+	ProtoOp protoOp;
+	ScriptOps::ScriptOp op;
+	int32 arg;
+};
+
+ProtoInstruction::ProtoInstruction() : protoOp(kProtoOpScript), op(ScriptOps::kInvalid), arg(0) {
+}
+
+ProtoInstruction::ProtoInstruction(ScriptOps::ScriptOp paramOp) : protoOp(kProtoOpScript), op(paramOp), arg(0) {
+}
+
+ProtoInstruction::ProtoInstruction(ScriptOps::ScriptOp paramOp, int32 paramArg) : protoOp(kProtoOpScript), op(paramOp), arg(paramArg) {
+}
+
+ProtoInstruction::ProtoInstruction(ProtoOp paramProtoOp, ScriptOps::ScriptOp paramOp, int32 paramArg) : protoOp(paramProtoOp), op(paramOp), arg(paramArg) {
+}
+
+struct ProtoScript {
+	Common::Array<ProtoInstruction> instrs;
+
+	void reset();
+};
+
+void ProtoScript::reset() {
+	instrs.clear();
+}
+
+struct ScriptNamedInstruction {
+	const char *str;
+	ProtoOp protoOp;
+	ScriptOps::ScriptOp op;
+};
+
+class ScriptCompiler {
+public:
+	ScriptCompiler(TextParser &parser, const Common::String &blamePath);
+
+	void compileRoomScriptSet(ScriptSet *ss);
+
+private:
+	bool parseNumber(const Common::String &token, uint32 &outNumber) const;
+	static bool parseDecNumber(const Common::String &token, uint start, uint32 &outNumber);
+	static bool parseHexNumber(const Common::String &token, uint start, uint32 &outNumber);
+	void expectNumber(uint32 &outNumber);
+
+	void compileRoomScriptSet(RoomScriptSet *rss);
+	void compileScreenScriptSet(ScreenScriptSet *sss);
+	bool compileInstructionToken(ProtoScript &script, const Common::String &token);
+
+	void codeGenScript(ProtoScript &protoScript, Script &script);
+
+	uint indexString(const Common::String &str);
+
+	enum NumberParsingMode {
+		kNumberParsingDec,
+		kNumberParsingHex,
+	};
+
+	TextParser &_parser;
+	NumberParsingMode _numberParsingMode;
+	const Common::String _blamePath;
+
+	Common::HashMap<Common::String, uint> _stringToIndex;
+	Common::Array<Common::String> _strings;
+};
+
+ScriptCompiler::ScriptCompiler(TextParser &parser, const Common::String &blamePath) : _numberParsingMode(kNumberParsingHex), _parser(parser), _blamePath(blamePath) {
+}
+
+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);
+		default:
+			error("Unknown number parsing mode");
+			return false;
+		}
+	}
+
+	return false;
+}
+
+bool ScriptCompiler::parseDecNumber(const Common::String &token, uint start, uint32 &outNumber) {
+	if (start == token.size())
+		return false;
+
+	uint32 num = 0;
+	for (uint i = start; i < token.size(); i++) {
+		char c = token[i];
+		uint32 digit = 0;
+		if (c >= '0' && c <= '9')
+			digit = c - '0';
+		else
+			return false;
+
+		num = num * 10u + digit;
+	}
+
+	outNumber = num;
+	return true;
+}
+
+bool ScriptCompiler::parseHexNumber(const Common::String &token, uint start, uint32 &outNumber) {
+	if (start == token.size())
+		return false;
+
+	uint32 num = 0;
+	for (uint i = start; i < token.size(); i++) {
+		char c = token[i];
+		uint32 digit = 0;
+		if (c >= '0' && c <= '9')
+			digit = c - '0';
+		else if (c >= 'a' && c <= 'f')
+			digit = (c - 'a') + 0xa;
+		else
+			return false;
+
+		num = num * 16u + digit;
+	}
+
+	outNumber = num;
+	return true;
+}
+
+void ScriptCompiler::expectNumber(uint32 &outNumber) {
+	TextParserState state;
+	Common::String token;
+	if (_parser.parseToken(token, state)) {
+		if (!parseNumber(token, outNumber))
+			error("Error compiling script at line %i col %i: Expected number but found '%s'", static_cast<int>(state._lineNum), static_cast<int>(state._col), token.c_str());
+	} else {
+		error("Error compiling script at line %i col %i: Expected number", static_cast<int>(state._lineNum), static_cast<int>(state._col));
+	}
+}
+
+void ScriptCompiler::compileRoomScriptSet(ScriptSet *ss) {
+	Common::SharedPtr<RoomScriptSet> roomScript;
+	uint roomID = 0;
+
+	TextParserState state;
+	Common::String token;
+	while (_parser.parseToken(token, state)) {
+		if (token == "~ROOM") {
+			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));
+			roomScript.reset(new RoomScriptSet());
+
+			uint32 roomNumber = 0;
+			expectNumber(roomNumber);
+
+			ss->roomScripts[roomNumber] = roomScript;
+
+			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());
+		}
+	}
+
+	ss->strings = Common::move(_strings);
+}
+
+void ScriptCompiler::compileRoomScriptSet(RoomScriptSet *rss) {
+	TextParserState state;
+	Common::String token;
+	while (_parser.parseToken(token, state)) {
+		if (token == "~EROOM") {
+			return;
+		} else if (token == "~SCR") {
+			uint32 screenNumber = 0;
+			expectNumber(screenNumber);
+
+			Common::SharedPtr<ScreenScriptSet> sss(new ScreenScriptSet());
+			compileScreenScriptSet(sss.get());
+
+			rss->screenScripts[screenNumber] = sss;
+		} 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: Room wasn't terminated");
+}
+
+void ScriptCompiler::compileScreenScriptSet(ScreenScriptSet *sss) {
+	TextParserState state;
+	Common::String token;
+
+	ProtoScript protoScript;
+	Common::SharedPtr<Script> currentScript(new Script());
+
+	sss->entryScript.reset(currentScript);
+
+	while (_parser.parseToken(token, state)) {
+		if (token == "~EROOM" || token == "~SCR") {
+			_parser.requeue(token, state);
+
+			codeGenScript(protoScript, *currentScript);
+			return;
+		} else if (token == "~*") {
+			uint32 interactionNumber = 0;
+			expectNumber(interactionNumber);
+
+			codeGenScript(protoScript, *currentScript);
+
+			currentScript.reset(new Script());
+
+			sss->interactionScripts[interactionNumber] = currentScript;
+		} else if (compileInstructionToken(protoScript, token)) {
+			// Nothing
+		} else {
+			error("Error compiling script at line %i col %i: Expected ~EROOM or ~SCR or ~* or instruction but found '%s'", static_cast<int>(state._lineNum), static_cast<int>(state._col), token.c_str());
+		}
+	}
+}
+
+static ScriptNamedInstruction g_namedInstructions[] = {
+	{"rotate", ProtoOp::kProtoOpScript, ScriptOps::kRotate},
+	{"angle", ProtoOp::kProtoOpScript, ScriptOps::kAngle},
+	{"angleG@", ProtoOp::kProtoOpScript, ScriptOps::kAngleGGet},
+	{"speed", ProtoOp::kProtoOpScript, ScriptOps::kSpeed},
+	{"sanimL", ProtoOp::kProtoOpScript, ScriptOps::kSAnimL},
+	{"changeL", ProtoOp::kProtoOpScript, ScriptOps::kChangeL},
+	{"animR", ProtoOp::kProtoOpScript, ScriptOps::kAnimR},
+	{"animF", ProtoOp::kProtoOpScript, ScriptOps::kAnimF},
+	{"animN", ProtoOp::kProtoOpScript, ScriptOps::kAnimN},
+	{"animG", ProtoOp::kProtoOpScript, ScriptOps::kAnimG},
+	{"animS", ProtoOp::kProtoOpScript, ScriptOps::kAnimS},
+	{"anim", ProtoOp::kProtoOpScript, ScriptOps::kAnim},
+	{"static", ProtoOp::kProtoOpScript, ScriptOps::kStatic},
+	{"yes@", ProtoOp::kProtoOpScript, ScriptOps::kVarLoad},
+	{"yes!", ProtoOp::kProtoOpScript, ScriptOps::kVarStore},
+	{"cursor!", ProtoOp::kProtoOpScript, ScriptOps::kSetCursor},
+	{"room!", ProtoOp::kProtoOpScript, ScriptOps::kSetRoom},
+	{"lmb", ProtoOp::kProtoOpScript, ScriptOps::kLMB},
+	{"lmb1", ProtoOp::kProtoOpScript, ScriptOps::kLMB1},
+	{"volumeDn4", ProtoOp::kProtoOpScript, ScriptOps::kVolumeDn4},
+	{"volumeUp3", ProtoOp::kProtoOpScript, ScriptOps::kVolumeUp3},
+	{"rnd", ProtoOp::kProtoOpScript, ScriptOps::kRandom},
+	{"drop", ProtoOp::kProtoOpScript, ScriptOps::kDrop},
+	{"dup", ProtoOp::kProtoOpScript, ScriptOps::kDup},
+	{"say3", ProtoOp::kProtoOpScript, ScriptOps::kSay3},
+	{"setTimer", ProtoOp::kProtoOpScript, ScriptOps::kSetTimer},
+	{"lo!", ProtoOp::kProtoOpScript, ScriptOps::kLoSet},
+	{"lo@", ProtoOp::kProtoOpScript, ScriptOps::kLoGet},
+	{"hi!", ProtoOp::kProtoOpScript, ScriptOps::kHiSet},
+	{"hi@", ProtoOp::kProtoOpScript, ScriptOps::kHiGet},
+
+	{"and", ProtoOp::kProtoOpScript, ScriptOps::kAnd},
+	{"or", ProtoOp::kProtoOpScript, ScriptOps::kOr},
+	{"not", ProtoOp::kProtoOpScript, ScriptOps::kNot},
+	{"=", ProtoOp::kProtoOpScript, ScriptOps::kCmpEq},
+
+	{"bit@", ProtoOp::kProtoOpScript, ScriptOps::kBitLoad},
+	{"bit0!", ProtoOp::kProtoOpScript, ScriptOps::kBitSet0},
+	{"bit1!", ProtoOp::kProtoOpScript, ScriptOps::kBitSet1},
+
+	{"soundS1", ProtoOp::kProtoOpScript, ScriptOps::kSoundS1},
+	{"soundL2", ProtoOp::kProtoOpScript, ScriptOps::kSoundL2},
+	{"music", ProtoOp::kProtoOpScript, ScriptOps::kMusic},
+	{"musicUp", ProtoOp::kProtoOpScript, ScriptOps::kMusicUp},
+
+	{"parm1", ProtoOp::kProtoOpScript, ScriptOps::kParm1},
+	{"parm2", ProtoOp::kProtoOpScript, ScriptOps::kParm2},
+	{"parm3", ProtoOp::kProtoOpScript, ScriptOps::kParm3},
+	{"parmG", ProtoOp::kProtoOpScript, ScriptOps::kParmG},
+
+	{"disc1", ProtoOp::kProtoOpScript, ScriptOps::kDisc1},
+	{"disc2", ProtoOp::kProtoOpScript, ScriptOps::kDisc2},
+	{"disc3", ProtoOp::kProtoOpScript, ScriptOps::kDisc3},
+
+	{"#if", ProtoOp::kProtoOpIf, ScriptOps::kInvalid},
+	{"#eif", ProtoOp::kProtoOpEndIf, ScriptOps::kInvalid},
+	{"#else", ProtoOp::kProtoOpElse, ScriptOps::kInvalid},
+
+	{"#switch:", ProtoOp::kProtoOpSwitch, ScriptOps::kInvalid},
+	{"#eswitch", ProtoOp::kProtoOpEndSwitch, ScriptOps::kInvalid},
+	{"break", ProtoOp::kProtoOpBreak, ScriptOps::kInvalid},
+	{"#default", ProtoOp::kProtoOpDefault, ScriptOps::kInvalid},
+
+	{"esc_on", ProtoOp::kProtoOpScript, ScriptOps::kEscOn},
+	{"esc_off", ProtoOp::kProtoOpScript, ScriptOps::kEscOff},
+	{"esc_get@", ProtoOp::kProtoOpScript, ScriptOps::kEscGet},
+	{"backStart", ProtoOp::kProtoOpScript, ScriptOps::kBackStart},
+};
+
+bool ScriptCompiler::compileInstructionToken(ProtoScript &script, const Common::String &token) {
+	uint32 number = 0;
+	if (parseNumber(token, number)) {
+		script.instrs.push_back(ProtoInstruction(ScriptOps::kNumber, number));
+		return true;
+	}
+
+	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;
+		}
+
+		script.instrs.push_back(ProtoInstruction(ScriptOps::kAnimName, indexString(token.substr(1))));
+		return true;
+	}
+
+	if (token.size() >= 2 && token[0] == '_') {
+		script.instrs.push_back(ProtoInstruction(ScriptOps::kSoundName, indexString(token.substr(1))));
+		return true;
+	}
+
+	if (token.size() >= 5 && token[0] == 'C' && token[1] == 'U' && token[2] == 'R' && token[3] == '_') {
+		script.instrs.push_back(ProtoInstruction(ScriptOps::kCursorName, indexString(token)));
+		return true;
+	}
+
+	if (token == "#switch") {
+		_parser.expect(":", _blamePath);
+		script.instrs.push_back(ProtoInstruction(kProtoOpSwitch, ScriptOps::kInvalid, 0));
+		return true;
+	}
+
+	if (token == "#case") {
+		uint32 caseNumber = 0;
+		_parser.expect(":", _blamePath);
+		expectNumber(caseNumber);
+
+		script.instrs.push_back(ProtoInstruction(kProtoOpCase, ScriptOps::kInvalid, caseNumber));
+		return true;
+	}
+
+	if (token == "#case:") {
+		uint32 caseNumber = 0;
+		expectNumber(caseNumber);
+
+		script.instrs.push_back(ProtoInstruction(kProtoOpCase, ScriptOps::kInvalid, caseNumber));
+		return true;
+	}
+
+	for (const ScriptNamedInstruction &namedInstr : g_namedInstructions) {
+		if (token == namedInstr.str) {
+			script.instrs.push_back(ProtoInstruction(namedInstr.protoOp, namedInstr.op, 0));
+			return true;
+		}
+	}
+
+	return false;
+}
+
+enum CodeGenFlowControlBlockType {
+	kFlowControlInvalid,
+
+	kFlowControlIf,
+	kFlowControlSwitch,
+};
+
+struct CodeGenControlFlowBlock {
+	CodeGenControlFlowBlock();
+
+	CodeGenFlowControlBlockType type;
+	uint index;
+};
+
+CodeGenControlFlowBlock::CodeGenControlFlowBlock() : type(kFlowControlInvalid), index(0) {
+}
+
+struct CodeGenSwitchCase {
+	CodeGenSwitchCase();
+
+	int32 value;
+	uint label;
+};
+
+CodeGenSwitchCase::CodeGenSwitchCase() : value(0), label(0) {
+}
+
+struct CodeGenSwitch {
+	CodeGenSwitch();
+
+	Common::Array<CodeGenSwitchCase> cases;
+
+	uint endLabel;
+
+	uint defaultLabel;
+	bool hasDefault;
+};
+
+CodeGenSwitch::CodeGenSwitch() : defaultLabel(0), endLabel(0), hasDefault(false) {
+}
+
+struct CodeGenIf {
+	CodeGenIf();
+
+	uint endLabel;
+
+	uint elseLabel;
+	bool hasElse;
+};
+
+CodeGenIf::CodeGenIf() : endLabel(0), elseLabel(0), hasElse(false) {
+}
+
+
+void ScriptCompiler::codeGenScript(ProtoScript &protoScript, Script &script) {
+	Common::Array<ProtoInstruction> instrs;
+	Common::Array<CodeGenSwitch> switches;
+	Common::Array<CodeGenIf> ifs;
+	Common::Array<CodeGenControlFlowBlock> controlFlowStack;
+
+	int32 nextLabel = 0;
+
+	// Pass 1: Convert all flow control constructs into references to the flow control construct.
+	// This changes Switch, If, and Break proto-ops to point to the corresponding if or switch index
+	for (const ProtoInstruction &instr : protoScript.instrs) {
+		switch (instr.protoOp) {
+		case kProtoOpScript:
+			instrs.push_back(instr);
+			break;
+		case kProtoOpBreak: {
+			bool found = false;
+			for (uint ri = 0; ri < controlFlowStack.size(); ri++) {
+				const CodeGenControlFlowBlock &cf = controlFlowStack[controlFlowStack.size() - 1 - ri];
+				if (cf.type == kFlowControlSwitch) {
+					found = true;
+					instrs.push_back(ProtoInstruction(kProtoOpBreak, ScriptOps::kInvalid, cf.index));
+					break;
+				}
+			}
+
+			if (!found)
+				error("Error in codegen: break statement outside of a switch case");
+		} break;
+		case kProtoOpIf: {
+			CodeGenControlFlowBlock cf;
+			cf.type = kFlowControlIf;
+			cf.index = ifs.size();
+			controlFlowStack.push_back(cf);
+
+			CodeGenIf ifBlock;
+			ifBlock.endLabel = nextLabel++;
+			ifs.push_back(ifBlock);
+
+			instrs.push_back(ProtoInstruction(kProtoOpIf, ScriptOps::kInvalid, cf.index));
+		} break;
+		case kProtoOpElse: {
+			if (controlFlowStack.size() == 0)
+				error("Error in codegen: #else outside of control flow");
+
+			CodeGenControlFlowBlock &cf = controlFlowStack[controlFlowStack.size() - 1];
+
+			if (cf.type != kFlowControlIf)
+				error("Error in codegen: #else inside wrong control block type");
+
+			CodeGenIf &ifBlock = ifs[cf.index];
+
+			if (ifBlock.hasElse)
+				error("Error in codegen: #else already set for #if block");
+
+			ifBlock.hasElse = true;
+			ifBlock.elseLabel = nextLabel++;
+
+			instrs.push_back(ProtoInstruction(kProtoOpJumpToLabel, ScriptOps::kInvalid, ifBlock.endLabel));
+			instrs.push_back(ProtoInstruction(kProtoOpLabel, ScriptOps::kInvalid, ifBlock.elseLabel));
+		} break;
+		case kProtoOpEndIf: {
+			if (controlFlowStack.size() == 0)
+				error("Error in codegen: #eif outside of control flow");
+
+			CodeGenControlFlowBlock &cf = controlFlowStack[controlFlowStack.size() - 1];
+
+			if (cf.type != kFlowControlIf)
+				error("Error in codegen: #eif inside wrong control block type");
+
+			const CodeGenIf &ifBlock = ifs[cf.index];
+
+			instrs.push_back(ProtoInstruction(kProtoOpLabel, ScriptOps::kInvalid, ifBlock.endLabel));
+
+			controlFlowStack.pop_back();
+		} break;
+		case kProtoOpSwitch: {
+			CodeGenControlFlowBlock cf;
+			cf.type = kFlowControlSwitch;
+			cf.index = switches.size();
+			controlFlowStack.push_back(cf);
+
+			CodeGenSwitch switchBlock;
+			switchBlock.endLabel = nextLabel++;
+			switches.push_back(switchBlock);
+
+			instrs.push_back(ProtoInstruction(kProtoOpSwitch, ScriptOps::kInvalid, cf.index));
+		} break;
+		case kProtoOpCase: {
+			if (controlFlowStack.size() == 0)
+				error("Error in codegen: #case outside of control flow");
+
+			CodeGenControlFlowBlock &cf = controlFlowStack[controlFlowStack.size() - 1];
+
+			if (cf.type != kFlowControlSwitch)
+				error("Error in codegen: #case inside wrong control block type");
+
+			CodeGenSwitch &switchBlock = switches[cf.index];
+
+			CodeGenSwitchCase caseDef;
+			caseDef.label = nextLabel++;
+			caseDef.value = instr.arg;
+
+			switchBlock.cases.push_back(caseDef);
+
+			instrs.push_back(ProtoInstruction(kProtoOpLabel, ScriptOps::kInvalid, caseDef.label));
+		} break;
+		case kProtoOpDefault: {
+			if (controlFlowStack.size() == 0)
+				error("Error in codegen: #case outside of control flow");
+
+			CodeGenControlFlowBlock &cf = controlFlowStack[controlFlowStack.size() - 1];
+
+			if (cf.type != kFlowControlSwitch)
+				error("Error in codegen: #case inside wrong control block type");
+
+			CodeGenSwitch &switchBlock = switches[cf.index];
+
+			if (switchBlock.hasDefault)
+				error("Error in codegen: #switch already has a default");
+
+			switchBlock.hasDefault = true;
+			switchBlock.defaultLabel = nextLabel++;
+
+			instrs.push_back(ProtoInstruction(kProtoOpLabel, ScriptOps::kInvalid, switchBlock.defaultLabel));
+		} break;
+		case kProtoOpEndSwitch: {
+			if (controlFlowStack.size() == 0)
+				error("Error in codegen: #eswitch outside of control flow");
+
+			CodeGenControlFlowBlock &cf = controlFlowStack[controlFlowStack.size() - 1];
+
+			if (cf.type != kFlowControlSwitch)
+				error("Error in codegen: #eswitch inside wrong control block type");
+
+			const CodeGenSwitch &switchBlock = switches[cf.index];
+
+			instrs.push_back(ProtoInstruction(kProtoOpLabel, ScriptOps::kInvalid, switchBlock.endLabel));
+
+			controlFlowStack.pop_back();
+		} break;
+		default:
+			error("Internal error: Unhandled proto-op");
+			break;
+		}
+	}
+
+	if (controlFlowStack.size() > 0)
+		error("Error in codegen: Unterminated flow control construct");
+
+	Common::Array<ProtoInstruction> instrs2;
+
+	Common::HashMap<uint, uint> labelToInstr;
+
+	// Pass 2: Convert CF block references into label-targeting ops
+	// This eliminates If, Switch, and Break ops
+	for (const ProtoInstruction &instr : instrs) {
+		switch (instr.protoOp) {
+		case kProtoOpScript:
+		case kProtoOpJumpToLabel:
+			instrs2.push_back(instr);
+			break;
+		case kProtoOpIf: {
+			const CodeGenIf &ifBlock = ifs[instr.arg];
+
+			instrs2.push_back(ProtoInstruction(ScriptOps::kCheckValue, 0));
+			instrs2.push_back(ProtoInstruction(kProtoOpJumpToLabel, ScriptOps::kInvalid, ifBlock.hasElse ? ifBlock.elseLabel : ifBlock.endLabel));
+			instrs2.push_back(ProtoInstruction(ScriptOps::kDrop));
+		} break;
+		case kProtoOpSwitch: {
+			const CodeGenSwitch &switchBlock = switches[instr.arg];
+
+			for (const CodeGenSwitchCase &caseDef : switchBlock.cases) {
+				instrs2.push_back(ProtoInstruction(ScriptOps::kCheckValue, caseDef.value));
+				instrs2.push_back(ProtoInstruction(kProtoOpJumpToLabel, ScriptOps::kInvalid, caseDef.value));
+			}
+
+			instrs2.push_back(ProtoInstruction(ScriptOps::kDrop));
+			instrs2.push_back(ProtoInstruction(kProtoOpJumpToLabel, ScriptOps::kInvalid, switchBlock.hasDefault ? switchBlock.defaultLabel : switchBlock.endLabel));
+		} break;
+		case kProtoOpLabel:
+			labelToInstr[static_cast<uint>(instr.arg)] = instrs2.size();
+			break;
+		case kProtoOpBreak: {
+			const CodeGenSwitch &switchBlock = switches[instr.arg];
+
+			instrs2.push_back(ProtoInstruction(kProtoOpJumpToLabel, ScriptOps::kInvalid, switchBlock.endLabel));
+		} break;
+		default:
+			error("Internal error: Unhandled proto-op");
+			break;
+		}
+	}
+
+	instrs.clear();
+
+	// Pass 3: Resolve labels and write out finished instructions
+	script.instrs.reserve(instrs2.size());
+
+	for (const ProtoInstruction &instr : instrs2) {
+		switch (instr.protoOp) {
+		case kProtoOpScript:
+			script.instrs.push_back(Instruction(instr.op, instr.arg));
+			break;
+		case kProtoOpJumpToLabel: {
+			Common::HashMap<uint, uint>::const_iterator it = labelToInstr.find(static_cast<uint>(instr.arg));
+			if (it == labelToInstr.end())
+				error("Internal error: Unmatched label");
+
+			script.instrs.push_back(Instruction(ScriptOps::kJump, it->_value));
+		} break;
+		default:
+			error("Internal error: Unhandled proto-op");
+			break;
+		}
+	}
+}
+
+uint ScriptCompiler::indexString(const Common::String &str) {
+	Common::HashMap<Common::String, uint>::const_iterator it = _stringToIndex.find(str);
+	if (it == _stringToIndex.end()) {
+		uint index = _strings.size();
+		_stringToIndex[str] = index;
+		_strings.push_back(str);
+		return index;
+	}
+
+	return it->_value;
+}
+
+Common::SharedPtr<ScriptSet> compileLogicFile(Common::ReadStream &stream, uint streamSize, const Common::String &blamePath) {
+
+	uint cipherOffset = 255u - (streamSize % 255u);
+
+	LogicUnscrambleStream unscrambleStream(&stream, streamSize);
+	TextParser parser(&unscrambleStream);
+
+	Common::SharedPtr<ScriptSet> scriptSet(new ScriptSet());
+
+	ScriptCompiler compiler(parser, blamePath);
+
+	compiler.compileRoomScriptSet(scriptSet.get());
+
+	return scriptSet;
+}
+
+} // namespace VCruise
diff --git a/engines/vcruise/script.h b/engines/vcruise/script.h
new file mode 100644
index 00000000000..afec75d6c55
--- /dev/null
+++ b/engines/vcruise/script.h
@@ -0,0 +1,145 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef VCRUISE_SCRIPT_H
+#define VCRUISE_SCRIPT_H
+
+#include "common/array.h"
+#include "common/hashmap.h"
+#include "common/ptr.h"
+#include "common/types.h"
+
+namespace Common {
+
+class ReadStream;
+
+} // End of namespace Common
+
+namespace VCruise {
+
+namespace ScriptOps {
+
+enum ScriptOp {
+	kInvalid,
+
+	kNumber,
+
+	kRotate,
+	kAngle,
+	kAngleGGet,
+	kSpeed,
+	kSAnimL,
+	kChangeL,
+	kAnimR,
+	kAnimF,
+	kAnimN,
+	kAnimG,
+	kAnimS,
+	kAnim,
+	kStatic,
+	kVarLoad,
+	kVarStore,
+	kSetCursor,
+	kSetRoom,
+	kLMB,
+	kLMB1,
+	kSoundS1,
+	kSoundL2,
+	kMusic,
+	kMusicUp,
+	kParm1,
+	kParm2,
+	kParm3,
+	kParmG,
+	kVolumeDn4,
+	kVolumeUp3,
+	kRandom,
+	kDrop,
+	kDup,
+	kSay3,
+	kSetTimer,
+	kLoSet,
+	kLoGet,
+	kHiSet,
+	kHiGet,
+
+	kNot,
+	kAnd,
+	kOr,
+	kCmpEq,
+
+	kBitLoad,
+	kBitSet0,
+	kBitSet1,
+
+	kDisc1,
+	kDisc2,
+	kDisc3,
+
+	kEscOn,
+	kEscOff,
+	kEscGet,
+	kBackStart,
+
+	kAnimName,
+	kValueName,
+	kVarName,
+	kSoundName,
+	kCursorName,
+
+	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.
+};
+
+} // End of namespace ScriptOps
+
+struct Instruction {
+	Instruction();
+	explicit Instruction(ScriptOps::ScriptOp paramOp);
+	Instruction(ScriptOps::ScriptOp paramOp, int32 paramArg);
+
+	ScriptOps::ScriptOp op;
+	int32 arg;
+};
+
+struct Script {
+	Common::Array<Instruction> instrs;
+};
+
+struct ScreenScriptSet {
+	Common::SharedPtr<Script> entryScript;
+	Common::HashMap<uint, Common::SharedPtr<Script> > interactionScripts;
+};
+
+struct RoomScriptSet {
+	Common::HashMap<uint, Common::SharedPtr<ScreenScriptSet> > screenScripts;
+};
+
+struct ScriptSet {
+	Common::HashMap<uint, Common::SharedPtr<RoomScriptSet> > roomScripts;
+	Common::Array<Common::String> strings;
+};
+
+Common::SharedPtr<ScriptSet> compileLogicFile(Common::ReadStream &stream, uint streamSize, const Common::String &blamePath);
+
+}
+
+#endif
diff --git a/engines/vcruise/textparser.cpp b/engines/vcruise/textparser.cpp
new file mode 100644
index 00000000000..116b06dbee2
--- /dev/null
+++ b/engines/vcruise/textparser.cpp
@@ -0,0 +1,322 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/stream.h"
+#include "common/textconsole.h"
+
+#include "vcruise/textparser.h"
+
+namespace VCruise {
+
+TextParserState::TextParserState() : _lineNum(1), _col(1), _prevWasCR(false), _isParsingComment(false) {
+}
+
+TextParser::TextParser(Common::ReadStream *stream) : _stream(stream), _readBufferPos(0), _readBufferEnd(0), _returnBufferPos(kReturnBufferSize) {
+	memset(_readBuffer, 0, kReadBufferSize);
+	memset(_returnedBuffer, 0, kReturnBufferSize);
+}
+
+bool TextParser::readOneChar(char &outC, TextParserState &outState) {
+	if (_returnBufferPos == kReturnBufferSize) {
+		if (_readBufferPos == _readBufferEnd) {
+			if (_stream->eos())
+				return false;
+
+			_readBufferPos = 0;
+			_readBufferEnd = _stream->read(_readBuffer, kReadBufferSize);
+			if (_readBufferEnd == 0)
+				return false;
+		}
+	}
+
+	char c = 0;
+
+	if (_returnBufferPos != kReturnBufferSize) {
+		c = _returnedBuffer[_returnBufferPos++];
+	} else {
+		c = _readBuffer[_readBufferPos++];
+	}
+
+	TextParserState prevState = _state;
+
+	if (c == '\r') {
+		_state._lineNum++;
+		_state._col = 1;
+		_state._isParsingComment = false;
+		_state._prevWasCR = true;
+	} else if (c == '\n') {
+		if (!_state._prevWasCR) {
+			_state._lineNum++;
+			_state._col = 1;
+		}
+		_state._prevWasCR = false;
+	} else {
+		_state._col++;
+		_state._prevWasCR = false;
+
+		if (c == ';')
+			_state._isParsingComment = true;
+	}
+
+
+	outC = c;
+	outState = prevState;
+
+	return true;
+}
+
+bool TextParser::skipWhitespaceAndComments(char &outC, TextParserState &outState) {
+	char c = 0;
+	TextParserState firstCharState;
+
+	while (readOneChar(c, firstCharState)) {
+		if (isWhitespace(c))
+			continue;
+
+		if (_state._isParsingComment)
+			continue;
+
+		outC = c;
+		outState = firstCharState;
+		return true;
+	}
+
+	return false;
+}
+
+bool TextParser::isDelimiter(char c) {
+	if (c == ',' || c == '=' || c == '[' || c == ']')
+		return true;
+
+	return false;
+}
+
+bool TextParser::isWhitespace(char c) {
+	return (c == ' ') || ((c & 0xe0) == 0);
+}
+
+void TextParser::requeue(const char *chars, uint numChars, const TextParserState &state) {
+	_state = state;
+	assert(_returnBufferPos >= numChars);
+	_returnBufferPos -= numChars;
+	memcpy(_returnedBuffer + _returnBufferPos, chars, numChars);
+}
+
+void TextParser::requeue(const Common::String &str, const TextParserState &state) {
+	requeue(str.c_str(), str.size(), state);
+}
+
+void TextParser::expectToken(Common::String &outToken, const Common::String &blamePath) {
+	TextParserState state;
+	expectTokenInternal(outToken, blamePath, state);
+}
+
+void TextParser::expectShort(int16 &outInt, const Common::String &blamePath) {
+	int i;
+	expectInt(i, blamePath);
+	outInt = static_cast<int16>(i);
+}
+
+void TextParser::expectInt(int &outInt, const Common::String &blamePath) {
+	Common::String token;
+	TextParserState state;
+	expectTokenInternal(token, blamePath, state);
+
+	int result = 0;
+	bool isNegative = false;
+	uint startIndex = 0;
+	if (token[0] == '-') {
+		if (token.size() == 1)
+			error("Parsing error in '%s' at line %i col %i: Signed integer was malformed", blamePath.c_str(), static_cast<int>(state._lineNum), static_cast<int>(state._col));
+
+		isNegative = true;
+		startIndex = 1;
+	}
+
+	int base = 10;
+	bool isHex = (token.size() >= (startIndex) + 3 && token[startIndex] == '0' && token[startIndex + 1] == 'x');
+	if (isHex) {
+		startIndex += 2;
+		base = 16;
+	}
+
+	for (uint i = startIndex; i < token.size(); i++) {
+		char c = token[i];
+		int digit = 0;
+		if (c >= '0' && c <= '9')
+			digit = (c - '0');
+		else if (isHex && (c >= 'a' && c <= 'f'))
+			digit = (c - 'a') + 0xa;
+		else if (isHex && (c >= 'A' && c <= 'F'))
+			digit = (c - 'A') + 0xa;
+		else
+			error("Parsing error in '%s' at line %i col %i: Integer contained non-digits", blamePath.c_str(), static_cast<int>(state._lineNum), static_cast<int>(state._col));
+
+		if (isNegative)
+			digit = -digit;
+
+		result = result * base + digit;
+	}
+
+	outInt = result;
+}
+
+void TextParser::expectUInt(uint &outUInt, const Common::String &blamePath) {
+	Common::String token;
+	TextParserState state;
+	expectTokenInternal(token, blamePath, state);
+
+	uint result = 0;
+
+	for (uint i = 0; i < token.size(); i++) {
+		char c = token[i];
+		if (c < '0' || c > '9')
+			error("Parsing error in '%s' at line %i col %i: Integer contained non-digits", blamePath.c_str(), static_cast<int>(state._lineNum), static_cast<int>(state._col));
+
+		uint additional = c - '0';
+
+		result = result * 10 + additional;
+	}
+
+	outUInt = result;
+}
+
+void TextParser::expectLine(Common::String &outToken, const Common::String &blamePath, bool continueToNextLine) {
+	outToken.clear();
+
+	char c = 0;
+	TextParserState state;
+
+	bool isSkippingWhitespace = true;
+	uint nonWhitespaceLength = 0;
+
+	while (readOneChar(c, state)) {
+		if (c == '\r' || c == '\n' || _state._isParsingComment) {
+			requeue(&c, 1, state);
+			if (continueToNextLine)
+				skipToEOL();
+			break;
+		}
+
+		bool cIsWhitespace = isWhitespace(c);
+		if (isSkippingWhitespace) {
+			if (cIsWhitespace)
+				continue;
+			isSkippingWhitespace = false;
+		}
+
+		outToken += c;
+		if (!cIsWhitespace)
+			nonWhitespaceLength = outToken.size();
+	}
+
+	if (nonWhitespaceLength != outToken.size())
+		outToken = outToken.substr(0, nonWhitespaceLength);
+}
+
+void TextParser::expect(const char *str, const Common::String &blamePath) {
+	Common::String token;
+	TextParserState state;
+	expectTokenInternal(token, blamePath, state);
+
+	if (token != str)
+		error("Parsing error in '%s' at line %i col %i: Expected token '%s' but found '%s'", blamePath.c_str(), static_cast<int>(state._lineNum), static_cast<int>(state._col), str, token.c_str());
+}
+
+void TextParser::skipToEOL() {
+	char c = 0;
+	TextParserState state;
+
+	while (readOneChar(c, state)) {
+		if (c == '\n')
+			return;
+
+		if (c == '\r') {
+			if (readOneChar(c, state)) {
+				if (c != '\n')
+					requeue(&c, 1, state);
+				return;
+			}
+		}
+	}
+}
+
+bool TextParser::checkEOL() {
+	char c = 0;
+	TextParserState state;
+
+	for (;;) {
+		if (!readOneChar(c, state))
+			return true;
+
+		if (_state._isParsingComment || c == '\n' || c == '\r') {
+			// EOL or comment
+			requeue(&c, 1, state);
+			return true;
+		}
+
+		if (!isWhitespace(c)) {
+			// Non-whitespace
+			requeue(&c, 1, state);
+			return false;
+		}
+	}
+}
+
+void TextParser::expectTokenInternal(Common::String &outToken, const Common::String &blamePath, TextParserState &outState) {
+	if (!parseToken(outToken, outState))
+		error("Parsing error in '%s' unexpected end of file", blamePath.c_str());
+}
+
+bool TextParser::parseToken(Common::String &outString, TextParserState &outState) {
+	outString.clear();
+
+	char c = 0;
+	TextParserState state;
+
+	if (!skipWhitespaceAndComments(c, state))
+		return false;
+
+	outState = state;
+
+	outString += c;
+
+	if (isDelimiter(c))
+		return true;
+
+	while (readOneChar(c, state)) {
+		if (isWhitespace(c) || _state._isParsingComment) {
+			requeue(&c, 1, state);
+			return true;
+		}
+
+		if (isDelimiter(c)) {
+			requeue(&c, 1, state);
+			return true;
+		}
+
+		outString += c;
+	}
+
+	return true;
+}
+
+} // End of namespace VCruise
diff --git a/engines/vcruise/textparser.h b/engines/vcruise/textparser.h
new file mode 100644
index 00000000000..0c3d92f6269
--- /dev/null
+++ b/engines/vcruise/textparser.h
@@ -0,0 +1,91 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef VCRUISE_TEXTPARSER_H
+#define VCRUISE_TEXTPARSER_H
+
+#include "common/str.h"
+#include "common/memstream.h"
+
+namespace Common {
+
+class ReadStream;
+
+} // End of namespace Common
+
+namespace VCruise {
+
+struct TextParserState {
+	TextParserState();
+
+	uint _lineNum;
+	uint _col;
+	bool _prevWasCR;
+	bool _isParsingComment;
+};
+
+class TextParser {
+public:
+	explicit TextParser(Common::ReadStream *stream);
+
+	bool parseToken(Common::String &outString, TextParserState &outState);
+
+	bool readOneChar(char &outC, TextParserState &outState);
+	bool skipWhitespaceAndComments(char &outC, TextParserState &outState);
+
+	void requeue(const char *chars, uint numChars, const TextParserState &state);
+	void requeue(const Common::String &str, const TextParserState &state);
+
+	void expectToken(Common::String &outToken, const Common::String &blamePath);
+	void expectShort(int16 &outInt, const Common::String &blamePath);
+	void expectInt(int &outInt, const Common::String &blamePath);
+	void expectUInt(uint &outUInt, const Common::String &blamePath);
+	void expectLine(Common::String &outToken, const Common::String &blamePath, bool continueToNextLine);
+
+	void expect(const char *str, const Common::String &blamePath);
+
+	void skipToEOL();
+	bool checkEOL();
+
+private:
+	void expectTokenInternal(Common::String &outToken, const Common::String &blamePath, TextParserState &outState);
+
+	static bool isDelimiter(char c);
+	static bool isWhitespace(char c);
+
+	TextParserState _state;
+
+	Common::ReadStream *_stream;
+
+	static const uint kReadBufferSize = 4096;
+	static const uint kReturnBufferSize = 8;
+
+	char _returnedBuffer[kReturnBufferSize];
+	uint _returnBufferPos;
+
+	char _readBuffer[kReadBufferSize];
+	uint _readBufferPos;
+	uint _readBufferEnd;
+};
+
+} // End of namespace VCruise
+
+#endif
diff --git a/engines/vcruise/vcruise.cpp b/engines/vcruise/vcruise.cpp
index c084988b570..a3193de72d0 100644
--- a/engines/vcruise/vcruise.cpp
+++ b/engines/vcruise/vcruise.cpp
@@ -108,7 +108,7 @@ Common::Error VCruiseEngine::run() {
 	else
 		error("Unable to find a suitable graphics format");
 
-	_runtime.reset(new Runtime(_system));
+	_runtime.reset(new Runtime(_system, _rootFSNode, _gameDescription->gameID));
 
 	_runtime->loadCursors(exeName);
 
@@ -154,4 +154,10 @@ bool VCruiseEngine::canSaveGameStateCurrently() {
 	return false;
 }
 
+void VCruiseEngine::initializePath(const Common::FSNode &gamePath) {
+	Engine::initializePath(gamePath);
+
+	_rootFSNode = gamePath;
+}
+
 } // End of namespace VCruise
diff --git a/engines/vcruise/vcruise.h b/engines/vcruise/vcruise.h
index 0aa35004c8a..b5551c5e169 100644
--- a/engines/vcruise/vcruise.h
+++ b/engines/vcruise/vcruise.h
@@ -57,6 +57,8 @@ public:
 	bool canSaveAutosaveCurrently() override;
 	bool canSaveGameStateCurrently() override;
 
+	void initializePath(const Common::FSNode &gamePath) override;
+
 protected:
 	void pauseEngineIntern(bool pause) override;
 
@@ -67,6 +69,8 @@ private:
 	Common::Rect _menuBarRect;
 	Common::Rect _trayRect;
 
+	Common::FSNode _rootFSNode;
+
 	Common::SharedPtr<Runtime> _runtime;
 };
 


Commit: e3360047a133bc4946d2ad52851915d3cac42616
    https://github.com/scummvm/scummvm/commit/e3360047a133bc4946d2ad52851915d3cac42616
Author: elasota (ejlasota at gmail.com)
Date: 2023-02-21T17:16:53+01:00

Commit Message:
VCRUISE: Add map loader

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


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index df09638a907..26540f8271e 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -38,6 +38,26 @@ namespace VCruise {
 AnimationDef::AnimationDef() : animNum(0), firstFrame(0), lastFrame(0) {
 }
 
+InteractionDef::InteractionDef() : objectType(0), interactionID(0) {
+}
+
+void MapDef::clear() {
+	for (uint screen = 0; screen < kNumScreens; screen++)
+		for (uint direction = 0; direction < kNumDirections; direction++)
+			screenDirections[screen][direction].reset();
+}
+
+const MapScreenDirectionDef *MapDef::getScreenDirection(uint screen, uint direction) {
+	if (screen < kFirstScreen)
+		return nullptr;
+
+	screen -= kFirstScreen;
+
+	if (screen >= kNumScreens)
+		return nullptr;
+
+	return screenDirections[screen][direction].get();
+}
 
 Runtime::Runtime(OSystem *system, const Common::FSNode &rootFSNode, VCruiseGameID gameID)
 	: _system(system), _roomNumber(1), _screenNumber(0), _loadedRoomNumber(0), _activeScreenNumber(0), _gameState(kGameStateBoot), _gameID(gameID), _rootFSNode(rootFSNode), _havePendingScreenChange(false) {
@@ -45,6 +65,10 @@ Runtime::Runtime(OSystem *system, const Common::FSNode &rootFSNode, VCruiseGameI
 	_logDir = _rootFSNode.getChild("Log");
 	if (!_logDir.exists() || !_logDir.isDirectory())
 		error("Couldn't resolve Log directory");
+
+	_mapDir = _rootFSNode.getChild("Map");
+	if (!_mapDir.exists() || !_mapDir.isDirectory())
+		error("Couldn't resolve Map directory");
 }
 
 Runtime::~Runtime() {
@@ -219,13 +243,73 @@ void Runtime::changeToScreen(uint roomNumber, uint screenNumber) {
 	bool changedScreen = (screenNumber != _activeScreenNumber) || changedRoom;
 
 	if (changedRoom) {
-		Common::String logFileName = Common::String::format("Room%02i.log", static_cast<int>(roomNumber));
+		_scriptSet.reset();
 
+		Common::String logFileName = Common::String::format("Room%02i.log", static_cast<int>(roomNumber));
 		Common::FSNode logFileNode = _logDir.getChild(logFileName);
-		if (Common::SeekableReadStream *logicFile = logFileNode.createReadStream())
-			_scriptSet = compileLogicFile(*logicFile, static_cast<uint>(logicFile->size()), logFileNode.getPath());
-		else
-			_scriptSet.reset();
+		if (logFileNode.exists()) {
+			if (Common::SeekableReadStream *logicFile = logFileNode.createReadStream()) {
+				_scriptSet = compileLogicFile(*logicFile, static_cast<uint>(logicFile->size()), logFileNode.getPath());
+				delete logicFile;
+			}
+		}
+
+		_map.clear();
+
+		Common::String mapFileName = Common::String::format("Room%02i.map", static_cast<int>(roomNumber));
+		Common::FSNode mapFileNode = _mapDir.getChild(mapFileName);
+		if (mapFileNode.exists()) {
+			if (Common::SeekableReadStream *mapFile = mapFileNode.createReadStream()) {
+				loadMap(mapFile);
+				delete mapFile;
+			}
+		}
+	}
+}
+
+void Runtime::loadMap(Common::SeekableReadStream *stream) {
+	byte screenDefOffsets[MapDef::kNumScreens * MapDef::kNumDirections * 4];
+
+	if (!stream->seek(16))
+		error("Error skipping map file header");
+
+	if (stream->read(screenDefOffsets, sizeof(screenDefOffsets)) != sizeof(screenDefOffsets))
+		error("Error reading map offset table");
+
+	for (uint screen = 0; screen < MapDef::kNumScreens; screen++) {
+		for (uint direction = 0; direction < MapDef::kNumDirections; direction++) {
+			uint32 offset = READ_LE_UINT32(screenDefOffsets + (MapDef::kNumDirections * screen + direction) * 4);
+			if (!offset)
+				continue;
+
+			if (!stream->seek(offset))
+				error("Error seeking to screen data");
+
+			byte screenDefHeader[16];
+			if (stream->read(screenDefHeader, 16) != 16)
+				error("Error reading screen def header");
+
+			uint16 numInteractions = READ_LE_UINT16(screenDefHeader + 0);
+
+			if (numInteractions > 0) {
+				Common::SharedPtr<MapScreenDirectionDef> screenDirectionDef(new MapScreenDirectionDef());
+				screenDirectionDef->interactions.resize(numInteractions);
+
+				for (uint i = 0; i < numInteractions; i++) {
+					InteractionDef &idef = screenDirectionDef->interactions[i];
+
+					byte interactionData[12];
+					if (stream->read(interactionData, 12) != 12)
+						error("Error reading interaction data");
+
+					idef.rect = Common::Rect(READ_LE_INT16(interactionData + 0), READ_LE_INT16(interactionData + 2), READ_LE_INT16(interactionData + 4), READ_LE_INT16(interactionData + 6));
+					idef.interactionID = READ_LE_UINT16(interactionData + 8);
+					idef.objectType = READ_LE_UINT16(interactionData + 10);
+				}
+
+				_map.screenDirections[screen][direction] = screenDirectionDef;
+			}
+		}
 	}
 }
 
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index 7928dfcaa6a..8cbc66eb25c 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -66,6 +66,29 @@ struct RoomDef {
 	Common::String name;
 };
 
+struct InteractionDef {
+	InteractionDef();
+
+	Common::Rect rect;
+	uint16 interactionID;
+	uint16 objectType;
+};
+
+struct MapScreenDirectionDef {
+	Common::Array<InteractionDef> interactions;
+};
+
+struct MapDef {
+	static const uint kNumScreens = 96;
+	static const uint kNumDirections = 8;
+	static const uint kFirstScreen = 0xa0;
+
+	Common::SharedPtr<MapScreenDirectionDef> screenDirections[kNumScreens][kNumDirections];
+
+	void clear();
+	const MapScreenDirectionDef *getScreenDirection(uint screen, uint direction);
+};
+
 class Runtime {
 public:
 	Runtime(OSystem *system, const Common::FSNode &rootFSNode, VCruiseGameID gameID);
@@ -82,6 +105,7 @@ private:
 
 	void loadIndex();
 	void changeToScreen(uint roomNumber, uint screenNumber);
+	void loadMap(Common::SeekableReadStream *stream);
 
 	Common::Array<Common::SharedPtr<Graphics::WinCursorGroup> > _cursors;		// Cursors indexed as CURSOR_CUR_##
 	Common::Array<Common::SharedPtr<Graphics::WinCursorGroup> > _cursorsShort;	// Cursors indexed as CURSOR_#
@@ -98,10 +122,13 @@ private:
 
 	Common::FSNode _rootFSNode;
 	Common::FSNode _logDir;
+	Common::FSNode _mapDir;
 
 	Common::Array<Common::SharedPtr<RoomDef> > _roomDefs;
 	Common::SharedPtr<ScriptSet> _scriptSet;
 
+	MapDef _map;
+
 	enum IndexParseType {
 		kIndexParseTypeNone,
 		kIndexParseTypeRoom,


Commit: 974fe14b2c0996f6837c4e39a4c1aa265a7cfe2f
    https://github.com/scummvm/scummvm/commit/974fe14b2c0996f6837c4e39a4c1aa265a7cfe2f
Author: elasota (ejlasota at gmail.com)
Date: 2023-02-21T17:16:53+01:00

Commit Message:
VIDEO: Tag V-Cruise as using AVIDecoder

Changed paths:
    video/avi_decoder.h


diff --git a/video/avi_decoder.h b/video/avi_decoder.h
index a52cf42b044..0b6bdda7652 100644
--- a/video/avi_decoder.h
+++ b/video/avi_decoder.h
@@ -58,6 +58,7 @@ namespace Video {
  *  - sword1
  *  - sword2
  *  - titanic
+ *  - vcruise
  *  - zvision
  */
 class AVIDecoder : public VideoDecoder {


Commit: e58c6ba1b2ff62144e65256056f4da43b3bf0e28
    https://github.com/scummvm/scummvm/commit/e58c6ba1b2ff62144e65256056f4da43b3bf0e28
Author: elasota (ejlasota at gmail.com)
Date: 2023-02-21T17:16:53+01:00

Commit Message:
VCRUISE: Add enough stuff to render opening cinematic

Changed paths:
  A engines/vcruise/audio_player.cpp
  A engines/vcruise/audio_player.h
    engines/vcruise/configure.engine
    engines/vcruise/module.mk
    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/audio_player.cpp b/engines/vcruise/audio_player.cpp
new file mode 100644
index 00000000000..7b648902a3f
--- /dev/null
+++ b/engines/vcruise/audio_player.cpp
@@ -0,0 +1,75 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "vcruise/audio_player.h"
+
+namespace VCruise {
+
+AudioPlayer::AudioPlayer(Audio::Mixer *mixer, const Common::SharedPtr<Audio::AudioStream> &baseStream, byte volume, int8 balance)
+	: _exhausted(false), _mixer(nullptr), _baseStream(baseStream) {
+	_mixer = mixer;
+	mixer->playStream(Audio::Mixer::kPlainSoundType, &_handle, this, -1, volume, balance, DisposeAfterUse::NO);
+}
+
+AudioPlayer::~AudioPlayer() {
+	stop();
+}
+
+int AudioPlayer::readBuffer(int16 *buffer, const int numSamplesTimesChannelCount) {
+	Common::StackLock lock(_mutex);
+
+	int samplesRead = 0;
+	if (_exhausted)
+		return 0;
+
+	samplesRead = _baseStream->readBuffer(buffer, numSamplesTimesChannelCount);
+
+	if (samplesRead != numSamplesTimesChannelCount)
+		_exhausted = true;
+
+	return samplesRead;
+}
+
+bool AudioPlayer::isStereo() const {
+	return _baseStream->isStereo();
+}
+
+int AudioPlayer::getRate() const {
+	return _baseStream->getRate();
+}
+
+bool AudioPlayer::endOfData() const {
+	return _exhausted;
+}
+
+void AudioPlayer::sendToMixer(Audio::Mixer *mixer, byte volume, int8 balance) {
+	mixer->playStream(Audio::Mixer::kPlainSoundType, &_handle, this, -1, volume, balance, DisposeAfterUse::NO);
+}
+
+void AudioPlayer::stop() {
+	if (_mixer)
+		_mixer->stopHandle(_handle);
+
+	_exhausted = true;
+	_mixer = nullptr;
+}
+
+} // End of namespace VCruise
diff --git a/engines/vcruise/audio_player.h b/engines/vcruise/audio_player.h
new file mode 100644
index 00000000000..f6597e2d79c
--- /dev/null
+++ b/engines/vcruise/audio_player.h
@@ -0,0 +1,60 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef VCRUISE_AUDIO_PLAYER_H
+#define VCRUISE_AUDIO_PLAYER_H
+
+#include "common/mutex.h"
+
+#include "audio/audiostream.h"
+#include "audio/mixer.h"
+
+namespace VCruise {
+
+struct AudioMetadata;
+class CachedAudio;
+
+class AudioPlayer : public Audio::AudioStream {
+public:
+	AudioPlayer(Audio::Mixer *mixer, const Common::SharedPtr<Audio::AudioStream> &baseStream, byte volume, int8 balance);
+	~AudioPlayer();
+
+	int readBuffer(int16 *buffer, const int numSamples) override;
+	bool isStereo() const override;
+	int getRate() const override;
+	bool endOfData() const override;
+
+	void sendToMixer(Audio::Mixer *mixer, byte volume, int8 balance);
+	void stop();
+
+private:
+	Common::Mutex _mutex;
+
+	Audio::SoundHandle _handle;
+	bool _isLooping;
+	bool _exhausted;
+	Audio::Mixer *_mixer;
+	Common::SharedPtr<Audio::AudioStream> _baseStream;
+};
+
+} // End of namespace VCruise
+
+#endif
diff --git a/engines/vcruise/configure.engine b/engines/vcruise/configure.engine
index 1f953f440f0..2a9c2384f8e 100644
--- a/engines/vcruise/configure.engine
+++ b/engines/vcruise/configure.engine
@@ -1,4 +1,4 @@
 # This file is included from the main "configure" script
 # add_engine [name] [desc] [build-by-default] [subengines] [base games] [deps]
-add_engine vcruise "V-Cruise" no "schizm" "" "16bit highres"
-add_engine schizm "Schizm" no "" "" "16bit highres jpeg"
+add_engine vcruise "V-Cruise" no "schizm" "" "16bit highres mad"
+add_engine schizm "Schizm" no "" "" "16bit highres mad jpeg"
diff --git a/engines/vcruise/module.mk b/engines/vcruise/module.mk
index 026a3a98277..5360a78e6d6 100644
--- a/engines/vcruise/module.mk
+++ b/engines/vcruise/module.mk
@@ -1,6 +1,7 @@
 MODULE := engines/vcruise
 
 MODULE_OBJS = \
+	audio_player.o \
 	metaengine.o \
 	runtime.o \
 	script.o \
diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 26540f8271e..b23d081b117 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -28,6 +28,12 @@
 #include "graphics/wincursor.h"
 #include "graphics/managed_surface.h"
 
+#include "audio/decoders/wave.h"
+#include "audio/audiostream.h"
+
+#include "video/avi_decoder.h"
+
+#include "vcruise/audio_player.h"
 #include "vcruise/runtime.h"
 #include "vcruise/script.h"
 #include "vcruise/textparser.h"
@@ -59,8 +65,16 @@ const MapScreenDirectionDef *MapDef::getScreenDirection(uint screen, uint direct
 	return screenDirections[screen][direction].get();
 }
 
-Runtime::Runtime(OSystem *system, const Common::FSNode &rootFSNode, VCruiseGameID gameID)
-	: _system(system), _roomNumber(1), _screenNumber(0), _loadedRoomNumber(0), _activeScreenNumber(0), _gameState(kGameStateBoot), _gameID(gameID), _rootFSNode(rootFSNode), _havePendingScreenChange(false) {
+void Runtime::RenderSection::init(const Common::Rect &paramRect, const Graphics::PixelFormat &fmt) {
+	rect = paramRect;
+	surf.reset(new Graphics::ManagedSurface(paramRect.width(), paramRect.height(), fmt));
+	surf->fillRect(Common::Rect(0, 0, surf->w, surf->h), 0xffffffff);
+}
+
+Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &rootFSNode, VCruiseGameID gameID)
+	: _system(system), _mixer(mixer), _roomNumber(1), _screenNumber(0), _direction(0), _loadedRoomNumber(0), _activeScreenNumber(0),
+	  _gameState(kGameStateBoot), _gameID(gameID), _rootFSNode(rootFSNode), _havePendingScreenChange(false), _scriptNextInstruction(0),
+	  _escOn(false), _loadedAnimation(0), _animFrameNumber(0), _animLastFrame(0), _animDecoderState(kAnimDecoderStateStopped) {
 
 	_logDir = _rootFSNode.getChild("Log");
 	if (!_logDir.exists() || !_logDir.isDirectory())
@@ -69,11 +83,25 @@ Runtime::Runtime(OSystem *system, const Common::FSNode &rootFSNode, VCruiseGameI
 	_mapDir = _rootFSNode.getChild("Map");
 	if (!_mapDir.exists() || !_mapDir.isDirectory())
 		error("Couldn't resolve Map directory");
+
+	_sfxDir = _rootFSNode.getChild("Sfx");
+	if (!_sfxDir.exists() || !_sfxDir.isDirectory())
+		error("Couldn't resolve Sfx directory");
+
+	_animsDir = _rootFSNode.getChild("Anims");
+	if (!_animsDir.exists() || !_animsDir.isDirectory())
+		error("Couldn't resolve Anims directory");
 }
 
 Runtime::~Runtime() {
 }
 
+void Runtime::initSections(Common::Rect gameRect, Common::Rect menuRect, Common::Rect trayRect, const Graphics::PixelFormat &pixFmt) {
+	_gameSection.init(gameRect, pixFmt);
+	_menuSection.init(menuRect, pixFmt);
+	_traySection.init(trayRect, pixFmt);
+}
+
 void Runtime::loadCursors(const char *exeName) {
 	Common::SharedPtr<Common::WinResources> winRes(Common::WinResources::createFromEXE(exeName));
 	if (!winRes)
@@ -121,8 +149,14 @@ bool Runtime::runFrame() {
 			break;
 		case kGameStateQuit:
 			return false;
-		case kGameStateRunning:
-			moreActions = runGame();
+		case kGameStateIdle:
+			moreActions = runIdle();
+			break;
+		case kGameStateScript:
+			moreActions = runScript();
+			break;
+		case kGameStateWaitingForAnimation:
+			moreActions = runWaitForAnimation();
 			break;
 		default:
 			error("Unknown game state");
@@ -138,20 +172,18 @@ bool Runtime::bootGame() {
 	loadIndex();
 	debug(1, "Index loaded OK");
 
-	_gameState = kGameStateRunning;
+	_gameState = kGameStateIdle;
 
 	if (_gameID == GID_REAH) {
 		// TODO: Change to the logo instead (0xb1) instead when menus are implemented
-		_roomNumber = 1;
-		_screenNumber = 0xb0;
-		_havePendingScreenChange = true;
+		changeToScreen(1, 0xb0);
 	} else
 		error("Couldn't figure out what screen to start on");
 
 	return true;
 }
 
-bool Runtime::runGame() {
+bool Runtime::runIdle() {
 
 	if (_havePendingScreenChange) {
 		_havePendingScreenChange = false;
@@ -163,6 +195,175 @@ bool Runtime::runGame() {
 	return false;
 }
 
+bool Runtime::runWaitForAnimation() {
+	if (!_animDecoder) {
+		_gameState = kGameStateScript;
+		return true;
+	}
+
+	bool needsFirstFrame = false;
+	if (_animDecoderState == kAnimDecoderStatePaused) {
+		_animDecoder->pauseVideo(false);
+		_animDecoderState = kAnimDecoderStatePlaying;
+		needsFirstFrame = true;
+	} else if (_animDecoderState == kAnimDecoderStateStopped) {
+		_animDecoder->start();
+		_animDecoderState = kAnimDecoderStatePlaying;
+		needsFirstFrame = true;
+	}
+
+	for (;;) {
+		bool needNewFrame = needsFirstFrame || (_animDecoder->getTimeToNextFrame() == 0);
+		needsFirstFrame = false;
+
+		if (!needNewFrame)
+			break;
+
+		const Graphics::Surface *surface = _animDecoder->decodeNextFrame();
+		if (!surface) {
+			_gameState = kGameStateScript;
+			return true;
+		}
+
+		Common::Rect copyRect = Common::Rect(0, 0, surface->w, surface->h);
+
+		Common::Rect constraintRect = Common::Rect(0, 0, _gameSection.rect.width(), _gameSection.rect.height());
+
+		copyRect = copyRect.findIntersectingRect(constraintRect);
+
+		if (copyRect.isValidRect() || !copyRect.isEmpty()) {
+			_gameSection.surf->blitFrom(*surface, copyRect, copyRect);
+			_system->copyRectToScreen(_gameSection.surf->getBasePtr(copyRect.left, copyRect.top), _gameSection.surf->pitch, copyRect.left + _gameSection.rect.left, copyRect.top + _gameSection.rect.top, copyRect.width(), copyRect.height());
+		}
+
+		// The current frame will be the frame to be decoded on the next decode call,
+		// which means if it exceeds the last frame then it's time to stop
+		if (_animDecoder->getCurFrame() > static_cast<int>(_animLastFrame)) {
+			_animDecoder->pauseVideo(true);
+			_animDecoderState = kAnimDecoderStatePaused;
+
+			_gameState = kGameStateScript;
+			return true;
+		}
+	}
+
+	// Pump and handle events
+	return false;
+}
+
+#ifdef DISPATCH_OP
+#error "DISPATCH_OP already defined"
+#endif
+
+#define DISPATCH_OP(op) \
+	case ScriptOps::k##op: this->scriptOp##op(arg); break
+
+bool Runtime::runScript() {
+	while (_gameState == kGameStateScript) {
+		uint instrNum = _scriptNextInstruction;
+		if (!_activeScript || instrNum >= _activeScript->instrs.size()) {
+			terminateScript();
+			return true;
+		}
+
+		_scriptNextInstruction++;
+
+		const Instruction &instr = _activeScript->instrs[instrNum];
+		int32 arg = instr.arg;
+
+		switch (instr.op) {
+			DISPATCH_OP(Number);
+			DISPATCH_OP(Rotate);
+			DISPATCH_OP(Angle);
+			DISPATCH_OP(AngleGGet);
+			DISPATCH_OP(Speed);
+			DISPATCH_OP(SAnimL);
+			DISPATCH_OP(ChangeL);
+
+			DISPATCH_OP(AnimR);
+			DISPATCH_OP(AnimF);
+			DISPATCH_OP(AnimN);
+			DISPATCH_OP(AnimG);
+			DISPATCH_OP(AnimS);
+			DISPATCH_OP(Anim);
+
+			DISPATCH_OP(Static);
+			DISPATCH_OP(VarLoad);
+			DISPATCH_OP(VarStore);
+			DISPATCH_OP(SetCursor);
+			DISPATCH_OP(SetRoom);
+			DISPATCH_OP(LMB);
+			DISPATCH_OP(LMB1);
+			DISPATCH_OP(SoundS1);
+			DISPATCH_OP(SoundL2);
+
+			DISPATCH_OP(Music);
+			DISPATCH_OP(MusicUp);
+			DISPATCH_OP(Parm1);
+			DISPATCH_OP(Parm2);
+			DISPATCH_OP(Parm3);
+			DISPATCH_OP(ParmG);
+
+			DISPATCH_OP(VolumeDn4);
+			DISPATCH_OP(VolumeUp3);
+			DISPATCH_OP(Random);
+			DISPATCH_OP(Drop);
+			DISPATCH_OP(Dup);
+			DISPATCH_OP(Say3);
+			DISPATCH_OP(SetTimer);
+			DISPATCH_OP(LoSet);
+			DISPATCH_OP(LoGet);
+			DISPATCH_OP(HiSet);
+			DISPATCH_OP(HiGet);
+
+			DISPATCH_OP(Not);
+			DISPATCH_OP(And);
+			DISPATCH_OP(Or);
+			DISPATCH_OP(CmpEq);
+
+			DISPATCH_OP(BitLoad);
+			DISPATCH_OP(BitSet0);
+			DISPATCH_OP(BitSet1);
+
+			DISPATCH_OP(Disc1);
+			DISPATCH_OP(Disc2);
+			DISPATCH_OP(Disc3);
+
+			DISPATCH_OP(EscOn);
+			DISPATCH_OP(EscOff);
+			DISPATCH_OP(EscGet);
+			DISPATCH_OP(BackStart);
+
+			DISPATCH_OP(AnimName);
+			DISPATCH_OP(ValueName);
+			DISPATCH_OP(VarName);
+			DISPATCH_OP(SoundName);
+			DISPATCH_OP(CursorName);
+
+			DISPATCH_OP(CheckValue);
+			DISPATCH_OP(Jump);
+
+		default:
+			error("Unimplemented opcode %i", static_cast<int>(instr.op));
+		}
+	}
+
+	return true;
+}
+
+#undef DISPATCH_OP
+
+void Runtime::terminateScript() {
+	_activeScript.reset();
+	_scriptNextInstruction = 0;
+
+	if (_gameState == kGameStateScript)
+		_gameState = kGameStateIdle;
+
+	if (_havePendingScreenChange)
+		changeToScreen(_roomNumber, _screenNumber);
+}
+
 void Runtime::loadIndex() {
 	Common::FSNode indexFSNode = _logDir.getChild("Index.txt");
 
@@ -242,7 +443,16 @@ void Runtime::changeToScreen(uint roomNumber, uint screenNumber) {
 	bool changedRoom = (roomNumber != _loadedRoomNumber);
 	bool changedScreen = (screenNumber != _activeScreenNumber) || changedRoom;
 
+	_roomNumber = roomNumber;
+	_screenNumber = screenNumber;
+
+	_loadedRoomNumber = roomNumber;
+	_activeScreenNumber = screenNumber;
+
 	if (changedRoom) {
+		// Scripts are not allowed
+		assert(!_activeScript);
+
 		_scriptSet.reset();
 
 		Common::String logFileName = Common::String::format("Room%02i.log", static_cast<int>(roomNumber));
@@ -265,6 +475,21 @@ void Runtime::changeToScreen(uint roomNumber, uint screenNumber) {
 			}
 		}
 	}
+
+	if (changedScreen) {
+		if (_scriptSet) {
+			Common::HashMap<uint, Common::SharedPtr<RoomScriptSet> >::const_iterator roomScriptIt = _scriptSet->roomScripts.find(_roomNumber);
+			if (roomScriptIt != _scriptSet->roomScripts.end()) {
+				Common::HashMap<uint, Common::SharedPtr<ScreenScriptSet> > &screenScriptsMap = roomScriptIt->_value->screenScripts;
+				Common::HashMap<uint, Common::SharedPtr<ScreenScriptSet> >::const_iterator screenScriptIt = screenScriptsMap.find(_screenNumber);
+				if (screenScriptIt != screenScriptsMap.end()) {
+					const Common::SharedPtr<Script> &script = screenScriptIt->_value->entryScript;
+					if (script)
+						activateScript(script);
+				}
+			}
+		}
+	}
 }
 
 void Runtime::loadMap(Common::SeekableReadStream *stream) {
@@ -313,6 +538,79 @@ void Runtime::loadMap(Common::SeekableReadStream *stream) {
 	}
 }
 
+void Runtime::changeMusicTrack(int track) {
+	_musicPlayer.reset();
+
+	Common::String wavFileName = Common::String::format("Music-%02i.wav", static_cast<int>(track));
+	Common::FSNode wavFileNode = _sfxDir.getChild(wavFileName);
+	if (wavFileNode.exists()) {
+		if (Common::SeekableReadStream *wavFile = wavFileNode.createReadStream()) {
+			if (Audio::SeekableAudioStream *audioStream = Audio::makeWAVStream(wavFile, DisposeAfterUse::YES)) {
+				Common::SharedPtr<Audio::AudioStream> loopingStream(Audio::makeLoopingAudioStream(audioStream, 0));
+
+				_musicPlayer.reset(new AudioPlayer(_mixer, loopingStream, 255, 0));
+			}
+		}
+	}
+}
+
+void Runtime::changeAnimation(const AnimationDef &animDef) {
+	int animFile = animDef.animNum;
+	if (animFile < 0)
+		animFile = -animFile;
+
+	if (_loadedAnimation != static_cast<uint>(animFile)) {
+		_loadedAnimation = animFile;
+		_animDecoder.reset();
+		_animDecoderState = kAnimDecoderStateStopped;
+
+		Common::String aviFileName = Common::String::format("Anim%04i.avi", animFile);
+		Common::FSNode aviFileNode = _animsDir.getChild(aviFileName);
+		if (!aviFileNode.exists()) {
+			warning("Animation file %i is missing", animFile);
+			return;
+		}
+
+		if (Common::SeekableReadStream *aviFile = aviFileNode.createReadStream()) {
+			_animDecoder.reset(new Video::AVIDecoder());
+			if (!_animDecoder->loadStream(aviFile)) {
+				warning("Animation file %i could not be loaded", animFile);
+				return;
+			}
+		} else {
+			warning("Animation file %i is missing", animFile);
+			return;
+		}
+	}
+
+	if (_animDecoderState == kAnimDecoderStatePlaying) {
+		_animDecoder->pauseVideo(true);
+		_animDecoderState = kAnimDecoderStatePaused;
+	}
+
+	_animDecoder->seekToFrame(animDef.firstFrame);
+	_animFrameNumber = animDef.firstFrame;
+	_animLastFrame = animDef.lastFrame;
+}
+
+AnimationDef Runtime::stackArgsToAnimDef(const StackValue_t *args) const {
+	AnimationDef def;
+	def.animNum = args[0];
+	def.firstFrame = args[1];
+	def.lastFrame = args[2];
+
+	return def;
+}
+
+void Runtime::activateScript(const Common::SharedPtr<Script> &script) {
+	if (script->instrs.size() == 0)
+		return;
+
+	_activeScript = script;
+	_scriptNextInstruction = 0;
+	_gameState = kGameStateScript;
+}
+
 bool Runtime::parseIndexDef(TextParser &parser, IndexParseType parseType, uint roomNumber, const Common::String &blamePath) {
 	Common::String lineText;
 	parser.expectLine(lineText, blamePath, true);
@@ -433,8 +731,183 @@ void Runtime::allocateRoomsUpTo(uint roomNumber) {
 	}
 }
 
-void Runtime::drawFrame() {
+#ifdef CHECK_STACK
+#error "CHECK_STACK is already defined"
+#endif
+
+#define PEEK_STACK(n)                                                                         \
+	if (this->_scriptStack.size() < (n)) {                                                      \
+		error("Script stack underflow");                                                      \
+		return;                                                                               \
+	}                                                                                         \
+	const ScriptArg_t *stackArgs = &this->_scriptStack[this->_scriptStack.size() - (n)]
+
+
+#define TAKE_STACK(n)                                                                         \
+	StackValue_t stackArgs[n];                                                                \
+	do {                                                                                      \
+		const uint stackSize = _scriptStack.size();                                           \
+		if (stackSize < (n)) {                                                                \
+			error("Script stack underflow");                                                  \
+			return;                                                                           \
+		}                                                                                     \
+		const StackValue_t *stackArgsPtr = &this->_scriptStack[stackSize - (n)];              \
+		for (uint i = 0; i < (n); i++)                                                        \
+			stackArgs[i] = stackArgsPtr[i];                                                   \
+		this->_scriptStack.resize(stackSize - (n));                                           \
+	} while (false)
+
+void Runtime::scriptOpNumber(ScriptArg_t arg) {
+	_scriptStack.push_back(arg);
+}
+
+void Runtime::scriptOpRotate(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpAngle(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpAngleGGet(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpSpeed(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpSAnimL(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpChangeL(ScriptArg_t arg) { error("Unimplemented opcode"); }
+
+void Runtime::scriptOpAnimR(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpAnimF(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpAnimN(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpAnimG(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpAnimS(ScriptArg_t arg) { error("Unimplemented opcode"); }
+
+void Runtime::scriptOpAnim(ScriptArg_t arg) {
+	TAKE_STACK(kAnimDefStackArgs + 2);
+
+	AnimationDef animDef = stackArgsToAnimDef(stackArgs + 0);
+	changeAnimation(animDef);
+
+	_gameState = kGameStateWaitingForAnimation;
+	_screenNumber = stackArgs[kAnimDefStackArgs + 0];
+	_direction = stackArgs[kAnimDefStackArgs + 1];
+}
+
+void Runtime::scriptOpStatic(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpVarLoad(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpVarStore(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpSetCursor(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpSetRoom(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpLMB(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpLMB1(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpSoundS1(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpSoundL2(ScriptArg_t arg) { error("Unimplemented opcode"); }
+
+void Runtime::scriptOpMusic(ScriptArg_t arg) {
+	TAKE_STACK(1);
+
+	changeMusicTrack(stackArgs[0]);
+}
+
+void Runtime::scriptOpMusicUp(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpParm1(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpParm2(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpParm3(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpParmG(ScriptArg_t arg) { error("Unimplemented opcode"); }
 
+void Runtime::scriptOpVolumeDn4(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpVolumeUp3(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpRandom(ScriptArg_t arg) { error("Unimplemented opcode"); }
+
+void Runtime::scriptOpDrop(ScriptArg_t arg) {
+	TAKE_STACK(1);
+}
+
+void Runtime::scriptOpDup(ScriptArg_t arg) {
+	TAKE_STACK(1);
+
+	_scriptStack.push_back(stackArgs[0]);
+	_scriptStack.push_back(stackArgs[0]);
+}
+
+void Runtime::scriptOpSay3(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpSetTimer(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpLoSet(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpLoGet(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpHiSet(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpHiGet(ScriptArg_t arg) { error("Unimplemented opcode"); }
+
+void Runtime::scriptOpNot(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpAnd(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpOr(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpCmpEq(ScriptArg_t arg) { error("Unimplemented opcode"); }
+
+void Runtime::scriptOpBitLoad(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpBitSet0(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpBitSet1(ScriptArg_t arg) { error("Unimplemented opcode"); }
+
+void Runtime::scriptOpDisc1(ScriptArg_t arg) {
+	// Always pass disc check
+	TAKE_STACK(1);
+	_scriptStack.push_back(1);
+}
+
+void Runtime::scriptOpDisc2(ScriptArg_t arg) {
+	// Always pass disc check
+	TAKE_STACK(2);
+	_scriptStack.push_back(1);
+}
+
+void Runtime::scriptOpDisc3(ScriptArg_t arg) {
+	// Always pass disc check
+	TAKE_STACK(3);
+	_scriptStack.push_back(1);
+}
+
+void Runtime::scriptOpEscOn(ScriptArg_t arg) {
+	TAKE_STACK(1);
+
+	_escOn = (stackArgs[0] != 0);
+}
+
+void Runtime::scriptOpEscOff(ScriptArg_t arg) {
+	_escOn = false;
+}
+
+void Runtime::scriptOpEscGet(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpBackStart(ScriptArg_t arg) { error("Unimplemented opcode"); }
+
+void Runtime::scriptOpAnimName(ScriptArg_t arg) {
+	// I doubt this is actually how it works internally but whatever
+	if (_roomNumber >= _roomDefs.size())
+		error("Can't resolve animation for room, room number was invalid");
+
+	Common::SharedPtr<RoomDef> roomDef = _roomDefs[_roomNumber];
+	if (!roomDef)
+		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]);
+	if (it == roomDef->animations.end())
+		error("Can't resolve animation for room, couldn't find animation '%s'", _scriptSet->strings[arg].c_str());
+
+	_scriptStack.push_back(it->_value.animNum);
+	_scriptStack.push_back(it->_value.firstFrame);
+	_scriptStack.push_back(it->_value.lastFrame);
+}
+
+
+void Runtime::scriptOpValueName(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpVarName(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpSoundName(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpCursorName(ScriptArg_t arg) { error("Unimplemented opcode"); }
+
+void Runtime::scriptOpCheckValue(ScriptArg_t arg) {
+	PEEK_STACK(1);
+
+	if (arg == stackArgs[0])
+		_scriptStack.pop_back();
+	else
+		_scriptNextInstruction++;
+}
+
+void Runtime::scriptOpJump(ScriptArg_t arg) {
+	_scriptNextInstruction = arg;
+}
+
+void Runtime::drawFrame() {
 	_system->updateScreen();
 }
 
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index 8cbc66eb25c..77dd957813c 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -30,20 +30,32 @@ class OSystem;
 
 namespace Graphics {
 
+struct PixelFormat;
 struct WinCursorGroup;
+class ManagedSurface;
 
 } // End of namespace Graphics
 
+namespace Video {
+
+class AVIDecoder;
+
+} // End of namespace Video
+
 namespace VCruise {
 
+class AudioPlayer;
 class TextParser;
 struct ScriptSet;
+struct Script;
+struct Instruction;
 
 enum GameState {
-	kGameStateBoot,
-	kGameStateCinematic,
-	kGameStateQuit,
-	kGameStateRunning,
+	kGameStateBoot,					// Booting the game
+	kGameStateWaitingForAnimation,	// Waiting for a blocking animation to complete, then resuming script
+	kGameStateQuit,					// Quitting
+	kGameStateIdle,					// Waiting for input events
+	kGameStateScript,				// Running a script
 };
 
 struct AnimationDef {
@@ -91,63 +103,188 @@ struct MapDef {
 
 class Runtime {
 public:
-	Runtime(OSystem *system, const Common::FSNode &rootFSNode, VCruiseGameID gameID);
+	Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &rootFSNode, VCruiseGameID gameID);
 	virtual ~Runtime();
 
+	void initSections(Common::Rect gameRect, Common::Rect menuRect, Common::Rect trayRect, const Graphics::PixelFormat &pixFmt);
+
 	void loadCursors(const char *exeName);
 
 	bool runFrame();
 	void drawFrame();
 
 private:
+	enum IndexParseType {
+		kIndexParseTypeNone,
+		kIndexParseTypeRoom,
+		kIndexParseTypeRRoom,	// Rectangle room (constrains animation areas)
+		kIndexParseTypeYRoom,	// Yes room (variable/ID mappings)
+		kIndexParseTypeVRoom,	// Value room (value/ID mappings?)
+		kIndexParseTypeTRoom,	// Text
+		kIndexParseTypeCRoom,	// Const
+		kIndexParseTypeSRoom,	// Sound
+		kIndexParseTypeNameRoom,
+	};
+
+	enum AnimDecoderState {
+		kAnimDecoderStateStopped,
+		kAnimDecoderStatePlaying,
+		kAnimDecoderStatePaused,
+	};
+
+	struct IndexPrefixTypePair {
+		const char *prefix;
+		IndexParseType parseType;
+	};
+
+	struct RenderSection {
+		Common::SharedPtr<Graphics::ManagedSurface> surf;
+		Common::Rect rect;
+
+		void init(const Common::Rect &paramRect, const Graphics::PixelFormat &fmt);
+	};
+
+	typedef int32 ScriptArg_t;
+	typedef int32 StackValue_t;
+
 	bool bootGame();
-	bool runGame();
+	bool runIdle();
+	bool runScript();
+	bool runWaitForAnimation();
+	void terminateScript();
 
 	void loadIndex();
 	void changeToScreen(uint roomNumber, uint screenNumber);
 	void loadMap(Common::SeekableReadStream *stream);
 
+	void changeMusicTrack(int musicID);
+	void changeAnimation(const AnimationDef &animDef);
+
+	AnimationDef stackArgsToAnimDef(const StackValue_t *args) const;
+
+	void activateScript(const Common::SharedPtr<Script> &script);
+
+	bool parseIndexDef(TextParser &parser, IndexParseType parseType, uint roomNumber, const Common::String &blamePath);
+	void allocateRoomsUpTo(uint roomNumber);
+
+	void scriptOpNumber(ScriptArg_t arg);
+	void scriptOpRotate(ScriptArg_t arg);
+	void scriptOpAngle(ScriptArg_t arg);
+	void scriptOpAngleGGet(ScriptArg_t arg);
+	void scriptOpSpeed(ScriptArg_t arg);
+	void scriptOpSAnimL(ScriptArg_t arg);
+	void scriptOpChangeL(ScriptArg_t arg);
+
+	void scriptOpAnimR(ScriptArg_t arg);
+	void scriptOpAnimF(ScriptArg_t arg);
+	void scriptOpAnimN(ScriptArg_t arg);
+	void scriptOpAnimG(ScriptArg_t arg);
+	void scriptOpAnimS(ScriptArg_t arg);
+	void scriptOpAnim(ScriptArg_t arg);
+
+	void scriptOpStatic(ScriptArg_t arg);
+	void scriptOpVarLoad(ScriptArg_t arg);
+	void scriptOpVarStore(ScriptArg_t arg);
+	void scriptOpSetCursor(ScriptArg_t arg);
+	void scriptOpSetRoom(ScriptArg_t arg);
+	void scriptOpLMB(ScriptArg_t arg);
+	void scriptOpLMB1(ScriptArg_t arg);
+	void scriptOpSoundS1(ScriptArg_t arg);
+	void scriptOpSoundL2(ScriptArg_t arg);
+
+	void scriptOpMusic(ScriptArg_t arg);
+	void scriptOpMusicUp(ScriptArg_t arg);
+	void scriptOpParm1(ScriptArg_t arg);
+	void scriptOpParm2(ScriptArg_t arg);
+	void scriptOpParm3(ScriptArg_t arg);
+	void scriptOpParmG(ScriptArg_t arg);
+
+	void scriptOpVolumeDn4(ScriptArg_t arg);
+	void scriptOpVolumeUp3(ScriptArg_t arg);
+	void scriptOpRandom(ScriptArg_t arg);
+	void scriptOpDrop(ScriptArg_t arg);
+	void scriptOpDup(ScriptArg_t arg);
+	void scriptOpSay3(ScriptArg_t arg);
+	void scriptOpSetTimer(ScriptArg_t arg);
+	void scriptOpLoSet(ScriptArg_t arg);
+	void scriptOpLoGet(ScriptArg_t arg);
+	void scriptOpHiSet(ScriptArg_t arg);
+	void scriptOpHiGet(ScriptArg_t arg);
+
+	void scriptOpNot(ScriptArg_t arg);
+	void scriptOpAnd(ScriptArg_t arg);
+	void scriptOpOr(ScriptArg_t arg);
+	void scriptOpCmpEq(ScriptArg_t arg);
+
+	void scriptOpBitLoad(ScriptArg_t arg);
+	void scriptOpBitSet0(ScriptArg_t arg);
+	void scriptOpBitSet1(ScriptArg_t arg);
+
+	void scriptOpDisc1(ScriptArg_t arg);
+	void scriptOpDisc2(ScriptArg_t arg);
+	void scriptOpDisc3(ScriptArg_t arg);
+
+	void scriptOpEscOn(ScriptArg_t arg);
+	void scriptOpEscOff(ScriptArg_t arg);
+	void scriptOpEscGet(ScriptArg_t arg);
+	void scriptOpBackStart(ScriptArg_t arg);
+
+	void scriptOpAnimName(ScriptArg_t arg);
+	void scriptOpValueName(ScriptArg_t arg);
+	void scriptOpVarName(ScriptArg_t arg);
+	void scriptOpSoundName(ScriptArg_t arg);
+	void scriptOpCursorName(ScriptArg_t arg);
+
+	void scriptOpCheckValue(ScriptArg_t arg);
+	void scriptOpJump(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_#
 
 	OSystem *_system;
 	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 _loadedRoomNumber;
 	uint _activeScreenNumber;
 	bool _havePendingScreenChange;
 	GameState _gameState;
+
+	bool _escOn;
+
 	VCruiseGameID _gameID;
 
 	Common::FSNode _rootFSNode;
 	Common::FSNode _logDir;
 	Common::FSNode _mapDir;
+	Common::FSNode _sfxDir;
+	Common::FSNode _animsDir;
 
 	Common::Array<Common::SharedPtr<RoomDef> > _roomDefs;
 	Common::SharedPtr<ScriptSet> _scriptSet;
 
-	MapDef _map;
+	Common::SharedPtr<Script> _activeScript;
+	uint _scriptNextInstruction;
+	Common::Array<StackValue_t> _scriptStack;
 
-	enum IndexParseType {
-		kIndexParseTypeNone,
-		kIndexParseTypeRoom,
-		kIndexParseTypeRRoom,	// Rectangle room (constrains animation areas)
-		kIndexParseTypeYRoom,	// Yes room (variable/ID mappings)
-		kIndexParseTypeVRoom,	// Value room (value/ID mappings?)
-		kIndexParseTypeTRoom,	// Text
-		kIndexParseTypeCRoom,	// Const
-		kIndexParseTypeSRoom,	// Sound
-		kIndexParseTypeNameRoom,
-	};
+	Common::SharedPtr<AudioPlayer> _musicPlayer;
 
-	struct IndexPrefixTypePair {
-		const char *prefix;
-		IndexParseType parseType;
-	};
+	Common::SharedPtr<Video::AVIDecoder> _animDecoder;
+	AnimDecoderState _animDecoderState;
+	uint _animFrameNumber;
+	uint _animLastFrame;
+	uint _loadedAnimation;
 
-	bool parseIndexDef(TextParser &parser, IndexParseType parseType, uint roomNumber, const Common::String &blamePath);
-	void allocateRoomsUpTo(uint roomNumber);
+	Audio::Mixer *_mixer;
+
+	MapDef _map;
+
+	RenderSection _gameSection;
+	RenderSection _menuSection;
+	RenderSection _traySection;
+
+	static const uint kAnimDefStackArgs = 3;
 };
 
 } // End of namespace VCruise
diff --git a/engines/vcruise/script.cpp b/engines/vcruise/script.cpp
index 569e3c79f56..99573faabff 100644
--- a/engines/vcruise/script.cpp
+++ b/engines/vcruise/script.cpp
@@ -22,8 +22,8 @@
 #include "common/stream.h"
 #include "common/hash-str.h"
 
-#include "engines/vcruise/script.h"
-#include "engines/vcruise/textparser.h"
+#include "vcruise/script.h"
+#include "vcruise/textparser.h"
 
 
 namespace VCruise {
@@ -529,8 +529,8 @@ void ScriptCompiler::codeGenScript(ProtoScript &protoScript, Script &script) {
 
 	int32 nextLabel = 0;
 
-	// Pass 1: Convert all flow control constructs into references to the flow control construct.
-	// This changes Switch, If, and Break proto-ops to point to the corresponding if or switch index
+	// Pass 1: Collect flow control constructs, make all flow control constructs point to the index of the construct,
+	// replace Else, Case, EndIf, EndSwitch, and Default instructions with Label and JumpToLabel.
 	for (const ProtoInstruction &instr : protoScript.instrs) {
 		switch (instr.protoOp) {
 		case kProtoOpScript:
@@ -675,8 +675,7 @@ void ScriptCompiler::codeGenScript(ProtoScript &protoScript, Script &script) {
 
 	Common::HashMap<uint, uint> labelToInstr;
 
-	// Pass 2: Convert CF block references into label-targeting ops
-	// This eliminates If, Switch, and Break ops
+	// Pass 2: Unroll If and Switch instructions into CheckValue and JumpToLabel ops, resolve label locations
 	for (const ProtoInstruction &instr : instrs) {
 		switch (instr.protoOp) {
 		case kProtoOpScript:
@@ -717,7 +716,7 @@ void ScriptCompiler::codeGenScript(ProtoScript &protoScript, Script &script) {
 
 	instrs.clear();
 
-	// Pass 3: Resolve labels and write out finished instructions
+	// Pass 3: Change all JumpToLabel ops to Jump ops and write out final instructions
 	script.instrs.reserve(instrs2.size());
 
 	for (const ProtoInstruction &instr : instrs2) {
diff --git a/engines/vcruise/script.h b/engines/vcruise/script.h
index afec75d6c55..8d2e7402eab 100644
--- a/engines/vcruise/script.h
+++ b/engines/vcruise/script.h
@@ -107,6 +107,8 @@ 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.
+
+	kNumOps,
 };
 
 } // End of namespace ScriptOps
diff --git a/engines/vcruise/vcruise.cpp b/engines/vcruise/vcruise.cpp
index a3193de72d0..39d59ac4ac1 100644
--- a/engines/vcruise/vcruise.cpp
+++ b/engines/vcruise/vcruise.cpp
@@ -94,8 +94,8 @@ Common::Error VCruiseEngine::run() {
 	size.y = videoSize.y + menuBarSize.y + traySize.y;
 
 	Common::Point menuTL = Common::Point((size.x - menuBarSize.x) / 2, 0);
-	Common::Point videoTL = Common::Point((size.x - videoSize.x) / 2, menuTL.y);
-	Common::Point trayTL = Common::Point((size.x - traySize.x) / 2, videoTL.y);
+	Common::Point videoTL = Common::Point((size.x - videoSize.x) / 2, menuTL.y + menuBarSize.y);
+	Common::Point trayTL = Common::Point((size.x - traySize.x) / 2, videoTL.y + videoSize.y);
 
 	_menuBarRect = Common::Rect(menuTL.x, menuTL.y, menuTL.x + menuBarSize.x, menuTL.y + menuBarSize.y);
 	_videoRect = Common::Rect(videoTL.x, videoTL.y, videoTL.x + videoSize.x, videoTL.y + videoSize.y);
@@ -108,7 +108,10 @@ Common::Error VCruiseEngine::run() {
 	else
 		error("Unable to find a suitable graphics format");
 
-	_runtime.reset(new Runtime(_system, _rootFSNode, _gameDescription->gameID));
+	_system->fillScreen(0);
+
+	_runtime.reset(new Runtime(_system, _mixer, _rootFSNode, _gameDescription->gameID));
+	_runtime->initSections(_videoRect, _menuBarRect, _trayRect, _system->getScreenFormat());
 
 	_runtime->loadCursors(exeName);
 


Commit: 8f8836617bf4b8f487f95bd62fd2957251051c36
    https://github.com/scummvm/scummvm/commit/8f8836617bf4b8f487f95bd62fd2957251051c36
Author: elasota (ejlasota at gmail.com)
Date: 2023-02-21T17:16:53+01:00

Commit Message:
VIDEO: Increase stream name capacity to deal with long (67 character) stream names in Reah

Changed paths:
    video/avi_decoder.cpp


diff --git a/video/avi_decoder.cpp b/video/avi_decoder.cpp
index e868d80f31a..3a589c5fd55 100644
--- a/video/avi_decoder.cpp
+++ b/video/avi_decoder.cpp
@@ -383,8 +383,8 @@ void AVIDecoder::readStreamName(uint32 size) {
 		skipChunk(size);
 	} else {
 		// Get in the name
-		assert(size > 0 && size < 64);
-		char buffer[64];
+		assert(size > 0 && size < 128);
+		char buffer[128];
 		_fileStream->read(buffer, size);
 		if (size & 1)
 			_fileStream->skip(1);


Commit: 5216131a5d3f6a7fb9dc5963476b46f864fc0eca
    https://github.com/scummvm/scummvm/commit/5216131a5d3f6a7fb9dc5963476b46f864fc0eca
Author: elasota (ejlasota at gmail.com)
Date: 2023-02-21T17:16:53+01:00

Commit Message:
VCRUISE: Add more things to get Reah to the first screen

Changed paths:
    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/runtime.cpp b/engines/vcruise/runtime.cpp
index b23d081b117..32ea00f9a8e 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -72,10 +72,13 @@ void Runtime::RenderSection::init(const Common::Rect &paramRect, const Graphics:
 }
 
 Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &rootFSNode, VCruiseGameID gameID)
-	: _system(system), _mixer(mixer), _roomNumber(1), _screenNumber(0), _direction(0), _loadedRoomNumber(0), _activeScreenNumber(0),
+	: _system(system), _mixer(mixer), _roomNumber(1), _screenNumber(0), _direction(0), _havePanAnimations(0), _loadedRoomNumber(0), _activeScreenNumber(0),
 	  _gameState(kGameStateBoot), _gameID(gameID), _rootFSNode(rootFSNode), _havePendingScreenChange(false), _scriptNextInstruction(0),
 	  _escOn(false), _loadedAnimation(0), _animFrameNumber(0), _animLastFrame(0), _animDecoderState(kAnimDecoderStateStopped) {
 
+	for (uint i = 0; i < kNumDirections; i++)
+		_haveIdleAnimations[i] = false;
+
 	_logDir = _rootFSNode.getChild("Log");
 	if (!_logDir.exists() || !_logDir.isDirectory())
 		error("Couldn't resolve Log directory");
@@ -299,6 +302,7 @@ bool Runtime::runScript() {
 
 			DISPATCH_OP(Music);
 			DISPATCH_OP(MusicUp);
+			DISPATCH_OP(MusicDn);
 			DISPATCH_OP(Parm1);
 			DISPATCH_OP(Parm2);
 			DISPATCH_OP(Parm3);
@@ -489,11 +493,16 @@ void Runtime::changeToScreen(uint roomNumber, uint screenNumber) {
 				}
 			}
 		}
+
+		_havePanAnimations = false;
+
+		for (uint i = 0; i < kNumDirections; i++)
+			_haveIdleAnimations[i] = false;
 	}
 }
 
 void Runtime::loadMap(Common::SeekableReadStream *stream) {
-	byte screenDefOffsets[MapDef::kNumScreens * MapDef::kNumDirections * 4];
+	byte screenDefOffsets[MapDef::kNumScreens * kNumDirections * 4];
 
 	if (!stream->seek(16))
 		error("Error skipping map file header");
@@ -502,8 +511,8 @@ void Runtime::loadMap(Common::SeekableReadStream *stream) {
 		error("Error reading map offset table");
 
 	for (uint screen = 0; screen < MapDef::kNumScreens; screen++) {
-		for (uint direction = 0; direction < MapDef::kNumDirections; direction++) {
-			uint32 offset = READ_LE_UINT32(screenDefOffsets + (MapDef::kNumDirections * screen + direction) * 4);
+		for (uint direction = 0; direction < kNumDirections; direction++) {
+			uint32 offset = READ_LE_UINT32(screenDefOffsets + (kNumDirections * screen + direction) * 4);
 			if (!offset)
 				continue;
 
@@ -731,6 +740,31 @@ void Runtime::allocateRoomsUpTo(uint roomNumber) {
 	}
 }
 
+void Runtime::onLButtonDown(int16 x, int16 y) {
+}
+
+void Runtime::onLButtonUp(int16 x, int16 y) {
+}
+
+void Runtime::onMouseMove(int16 x, int16 y) {
+}
+
+void Runtime::onKeyDown(Common::KeyCode keyCode) {
+	if (keyCode == Common::KEYCODE_ESCAPE) {
+		if (_gameState == kGameStateWaitingForAnimation) {
+			if (_escOn) {
+				// Terminate the animation
+				if (_animDecoderState == kAnimDecoderStatePlaying) {
+					_animDecoder->pauseVideo(true);
+					_animDecoderState = kAnimDecoderStatePaused;
+				}
+				_gameState = kGameStateScript;
+				return;
+			}
+		}
+	}
+}
+
 #ifdef CHECK_STACK
 #error "CHECK_STACK is already defined"
 #endif
@@ -761,18 +795,54 @@ void Runtime::scriptOpNumber(ScriptArg_t arg) {
 	_scriptStack.push_back(arg);
 }
 
-void Runtime::scriptOpRotate(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpRotate(ScriptArg_t arg) {
+	TAKE_STACK(kAnimDefStackArgs + kAnimDefStackArgs);
+
+	_panLeftAnimationDef = stackArgsToAnimDef(stackArgs + 0);
+	_panRightAnimationDef = stackArgsToAnimDef(stackArgs + kAnimDefStackArgs);
+	_havePanAnimations = true;
+}
+
 void Runtime::scriptOpAngle(ScriptArg_t arg) { error("Unimplemented opcode"); }
 void Runtime::scriptOpAngleGGet(ScriptArg_t arg) { error("Unimplemented opcode"); }
 void Runtime::scriptOpSpeed(ScriptArg_t arg) { error("Unimplemented opcode"); }
-void Runtime::scriptOpSAnimL(ScriptArg_t arg) { error("Unimplemented opcode"); }
+
+void Runtime::scriptOpSAnimL(ScriptArg_t arg) {
+	TAKE_STACK(kAnimDefStackArgs + 2);
+
+	if (stackArgs[kAnimDefStackArgs] != 0)
+		warning("sanimL second operand wasn't zero (what does that do??)");
+
+	AnimationDef animDef = stackArgsToAnimDef(stackArgs + 0);
+	uint direction = stackArgs[kAnimDefStackArgs + 1];
+
+	if (direction >= kNumDirections)
+		error("sanimL invalid direction");
+
+	_haveIdleAnimations[direction] = true;
+	_idleAnimations[direction] = animDef;
+}
+
 void Runtime::scriptOpChangeL(ScriptArg_t arg) { error("Unimplemented opcode"); }
 
 void Runtime::scriptOpAnimR(ScriptArg_t arg) { error("Unimplemented opcode"); }
 void Runtime::scriptOpAnimF(ScriptArg_t arg) { error("Unimplemented opcode"); }
 void Runtime::scriptOpAnimN(ScriptArg_t arg) { error("Unimplemented opcode"); }
 void Runtime::scriptOpAnimG(ScriptArg_t arg) { error("Unimplemented opcode"); }
-void Runtime::scriptOpAnimS(ScriptArg_t arg) { error("Unimplemented opcode"); }
+
+void Runtime::scriptOpAnimS(ScriptArg_t arg) {
+	TAKE_STACK(kAnimDefStackArgs + 2);
+
+	AnimationDef animDef = stackArgsToAnimDef(stackArgs + 0);
+	animDef.lastFrame = animDef.firstFrame;	// Static animation
+
+	changeAnimation(animDef);
+
+	_gameState = kGameStateWaitingForAnimation;	// FIXME
+	_screenNumber = stackArgs[kAnimDefStackArgs + 0];
+	_direction = stackArgs[kAnimDefStackArgs + 1];
+	_havePendingScreenChange = true;
+}
 
 void Runtime::scriptOpAnim(ScriptArg_t arg) {
 	TAKE_STACK(kAnimDefStackArgs + 2);
@@ -783,17 +853,42 @@ void Runtime::scriptOpAnim(ScriptArg_t arg) {
 	_gameState = kGameStateWaitingForAnimation;
 	_screenNumber = stackArgs[kAnimDefStackArgs + 0];
 	_direction = stackArgs[kAnimDefStackArgs + 1];
+	_havePendingScreenChange = true;
 }
 
 void Runtime::scriptOpStatic(ScriptArg_t arg) { error("Unimplemented opcode"); }
-void Runtime::scriptOpVarLoad(ScriptArg_t arg) { error("Unimplemented opcode"); }
-void Runtime::scriptOpVarStore(ScriptArg_t arg) { error("Unimplemented opcode"); }
+
+void Runtime::scriptOpVarLoad(ScriptArg_t arg) {
+	TAKE_STACK(1);
+
+	uint32 varID = (static_cast<uint32>(_roomNumber) << 16) | static_cast<uint32>(stackArgs[0]);
+
+	Common::HashMap<uint32, int32>::const_iterator it = _variables.find(varID);
+	if (it == _variables.end())
+		_scriptStack.push_back(0);
+	else
+		_scriptStack.push_back(it->_value);
+}
+
+void Runtime::scriptOpVarStore(ScriptArg_t arg) {
+	TAKE_STACK(2);
+
+	uint32 varID = (static_cast<uint32>(_roomNumber) << 16) | static_cast<uint32>(stackArgs[1]);
+
+	_variables[varID] = stackArgs[0];
+}
+
 void Runtime::scriptOpSetCursor(ScriptArg_t arg) { error("Unimplemented opcode"); }
 void Runtime::scriptOpSetRoom(ScriptArg_t arg) { error("Unimplemented opcode"); }
 void Runtime::scriptOpLMB(ScriptArg_t arg) { error("Unimplemented opcode"); }
 void Runtime::scriptOpLMB1(ScriptArg_t arg) { error("Unimplemented opcode"); }
 void Runtime::scriptOpSoundS1(ScriptArg_t arg) { error("Unimplemented opcode"); }
-void Runtime::scriptOpSoundL2(ScriptArg_t arg) { error("Unimplemented opcode"); }
+
+void Runtime::scriptOpSoundL2(ScriptArg_t arg) {
+	TAKE_STACK(2);
+
+	warning("Sound loop not implemented yet");
+}
 
 void Runtime::scriptOpMusic(ScriptArg_t arg) {
 	TAKE_STACK(1);
@@ -801,7 +896,18 @@ void Runtime::scriptOpMusic(ScriptArg_t arg) {
 	changeMusicTrack(stackArgs[0]);
 }
 
-void Runtime::scriptOpMusicUp(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpMusicUp(ScriptArg_t arg) {
+	TAKE_STACK(2);
+
+	warning("Music volume changes are not implemented");
+}
+
+void Runtime::scriptOpMusicDn(ScriptArg_t arg) {
+	TAKE_STACK(2);
+
+	warning("Music volume changes are not implemented");
+}
+
 void Runtime::scriptOpParm1(ScriptArg_t arg) { error("Unimplemented opcode"); }
 void Runtime::scriptOpParm2(ScriptArg_t arg) { error("Unimplemented opcode"); }
 void Runtime::scriptOpParm3(ScriptArg_t arg) { error("Unimplemented opcode"); }
@@ -829,29 +935,48 @@ void Runtime::scriptOpLoGet(ScriptArg_t arg) { error("Unimplemented opcode"); }
 void Runtime::scriptOpHiSet(ScriptArg_t arg) { error("Unimplemented opcode"); }
 void Runtime::scriptOpHiGet(ScriptArg_t arg) { error("Unimplemented opcode"); }
 
-void Runtime::scriptOpNot(ScriptArg_t arg) { error("Unimplemented opcode"); }
-void Runtime::scriptOpAnd(ScriptArg_t arg) { error("Unimplemented opcode"); }
-void Runtime::scriptOpOr(ScriptArg_t arg) { error("Unimplemented opcode"); }
-void Runtime::scriptOpCmpEq(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpNot(ScriptArg_t arg) {
+	TAKE_STACK(1);
+
+	_scriptStack.push_back((stackArgs[0] == 0) ? 1 : 0);
+}
+
+void Runtime::scriptOpAnd(ScriptArg_t arg) {
+	TAKE_STACK(2);
+
+	_scriptStack.push_back((stackArgs[0] != 0 && stackArgs[1] != 0) ? 1 : 0);
+}
+
+void Runtime::scriptOpOr(ScriptArg_t arg) {
+	TAKE_STACK(2);
+
+	_scriptStack.push_back((stackArgs[0] != 0 || stackArgs[1] != 0) ? 1 : 0);
+}
+
+void Runtime::scriptOpCmpEq(ScriptArg_t arg) {
+	TAKE_STACK(2);
+
+	_scriptStack.push_back((stackArgs[0] == stackArgs[1]) ? 1 : 0);
+}
 
 void Runtime::scriptOpBitLoad(ScriptArg_t arg) { error("Unimplemented opcode"); }
 void Runtime::scriptOpBitSet0(ScriptArg_t arg) { error("Unimplemented opcode"); }
 void Runtime::scriptOpBitSet1(ScriptArg_t arg) { error("Unimplemented opcode"); }
 
 void Runtime::scriptOpDisc1(ScriptArg_t arg) {
-	// Always pass disc check
+	// Disc check, always pass
 	TAKE_STACK(1);
 	_scriptStack.push_back(1);
 }
 
 void Runtime::scriptOpDisc2(ScriptArg_t arg) {
-	// Always pass disc check
+	// Disc check, always pass
 	TAKE_STACK(2);
 	_scriptStack.push_back(1);
 }
 
 void Runtime::scriptOpDisc3(ScriptArg_t arg) {
-	// Always pass disc check
+	// Disc check, always pass
 	TAKE_STACK(3);
 	_scriptStack.push_back(1);
 }
@@ -890,8 +1015,36 @@ void Runtime::scriptOpAnimName(ScriptArg_t arg) {
 
 
 void Runtime::scriptOpValueName(ScriptArg_t arg) { error("Unimplemented opcode"); }
-void Runtime::scriptOpVarName(ScriptArg_t arg) { error("Unimplemented opcode"); }
-void Runtime::scriptOpSoundName(ScriptArg_t arg) { error("Unimplemented opcode"); }
+
+void Runtime::scriptOpVarName(ScriptArg_t arg) {
+	if (_roomNumber >= _roomDefs.size())
+		error("Invalid room number for var name op");
+
+	const RoomDef *roomDef = _roomDefs[_roomNumber].get();
+	if (!roomDef)
+		error("Room def doesn't exist");
+
+	const Common::String &varName = _scriptSet->strings[arg];
+
+	Common::HashMap<Common::String, uint>::const_iterator it = roomDef->vars.find(varName);
+	if (it == roomDef->vars.end())
+		error("Var '%s' doesn't exist in room %i", varName.c_str(), static_cast<int>(_roomNumber));
+
+	_scriptStack.push_back(it->_value);
+}
+
+void Runtime::scriptOpSoundName(ScriptArg_t arg) {
+	const Common::String sndName = _scriptSet->strings[arg];
+
+	warning("Sound IDs are not implemented yet");
+
+	int32 soundID = 0;
+	for (uint i = 0; i < 4; i++)
+		soundID = soundID * 10 + (sndName[i] - '0');
+
+	_scriptStack.push_back(soundID);
+}
+
 void Runtime::scriptOpCursorName(ScriptArg_t arg) { error("Unimplemented opcode"); }
 
 void Runtime::scriptOpCheckValue(ScriptArg_t arg) {
@@ -911,4 +1064,5 @@ void Runtime::drawFrame() {
 	_system->updateScreen();
 }
 
+
 } // End of namespace VCruise
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index 77dd957813c..ab514e0c05a 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -23,6 +23,7 @@
 #define VCRUISE_RUNTIME_H
 
 #include "common/hashmap.h"
+#include "common/keyboard.h"
 
 #include "vcruise/detection.h"
 
@@ -44,6 +45,8 @@ class AVIDecoder;
 
 namespace VCruise {
 
+static const uint kNumDirections = 8;
+
 class AudioPlayer;
 class TextParser;
 struct ScriptSet;
@@ -92,7 +95,6 @@ struct MapScreenDirectionDef {
 
 struct MapDef {
 	static const uint kNumScreens = 96;
-	static const uint kNumDirections = 8;
 	static const uint kFirstScreen = 0xa0;
 
 	Common::SharedPtr<MapScreenDirectionDef> screenDirections[kNumScreens][kNumDirections];
@@ -113,6 +115,11 @@ public:
 	bool runFrame();
 	void drawFrame();
 
+	void onLButtonDown(int16 x, int16 y);
+	void onLButtonUp(int16 x, int16 y);
+	void onMouseMove(int16 x, int16 y);
+	void onKeyDown(Common::KeyCode keyCode);
+
 private:
 	enum IndexParseType {
 		kIndexParseTypeNone,
@@ -194,6 +201,7 @@ private:
 
 	void scriptOpMusic(ScriptArg_t arg);
 	void scriptOpMusicUp(ScriptArg_t arg);
+	void scriptOpMusicDn(ScriptArg_t arg);
 	void scriptOpParm1(ScriptArg_t arg);
 	void scriptOpParm2(ScriptArg_t arg);
 	void scriptOpParm3(ScriptArg_t arg);
@@ -246,6 +254,18 @@ private:
 	uint _screenNumber;
 	uint _direction;
 
+	AnimationDef _panLeftAnimationDef;
+	AnimationDef _panRightAnimationDef;
+	bool _havePanAnimations;
+
+	AnimationDef _idleAnimations[kNumDirections];
+	bool _haveIdleAnimations[kNumDirections];
+
+	Common::HashMap<uint32, int32> _variables;
+
+	static const uint kPanLeftInteraction = 1;
+	static const uint kPanRightInteraction = 3;
+
 	uint _loadedRoomNumber;
 	uint _activeScreenNumber;
 	bool _havePendingScreenChange;
diff --git a/engines/vcruise/script.cpp b/engines/vcruise/script.cpp
index 99573faabff..8701a433064 100644
--- a/engines/vcruise/script.cpp
+++ b/engines/vcruise/script.cpp
@@ -376,6 +376,7 @@ static ScriptNamedInstruction g_namedInstructions[] = {
 	{"soundL2", ProtoOp::kProtoOpScript, ScriptOps::kSoundL2},
 	{"music", ProtoOp::kProtoOpScript, ScriptOps::kMusic},
 	{"musicUp", ProtoOp::kProtoOpScript, ScriptOps::kMusicUp},
+	{"musicDn", ProtoOp::kProtoOpScript, ScriptOps::kMusicDn},
 
 	{"parm1", ProtoOp::kProtoOpScript, ScriptOps::kParm1},
 	{"parm2", ProtoOp::kProtoOpScript, ScriptOps::kParm2},
diff --git a/engines/vcruise/script.h b/engines/vcruise/script.h
index 8d2e7402eab..b8e86f780f8 100644
--- a/engines/vcruise/script.h
+++ b/engines/vcruise/script.h
@@ -65,6 +65,7 @@ enum ScriptOp {
 	kSoundL2,
 	kMusic,
 	kMusicUp,
+	kMusicDn,
 	kParm1,
 	kParm2,
 	kParm3,
diff --git a/engines/vcruise/vcruise.cpp b/engines/vcruise/vcruise.cpp
index 39d59ac4ac1..b87d2c6d18a 100644
--- a/engines/vcruise/vcruise.cpp
+++ b/engines/vcruise/vcruise.cpp
@@ -44,6 +44,18 @@ void VCruiseEngine::handleEvents() {
 
 	while (eventMan->pollEvent(evt)) {
 		switch (evt.type) {
+		case Common::EVENT_LBUTTONDOWN:
+			_runtime->onLButtonDown(evt.mouse.x, evt.mouse.y);
+			break;
+		case Common::EVENT_LBUTTONUP:
+			_runtime->onLButtonUp(evt.mouse.x, evt.mouse.y);
+			break;
+		case Common::EVENT_MOUSEMOVE:
+			_runtime->onMouseMove(evt.mouse.x, evt.mouse.y);
+			break;
+		case Common::EVENT_KEYDOWN:
+			_runtime->onKeyDown(evt.kbd.keycode);
+			break;
 		default:
 			break;
 		}


Commit: db5c7050f291464d676febbed9eed88ceed6c534
    https://github.com/scummvm/scummvm/commit/db5c7050f291464d676febbed9eed88ceed6c534
Author: elasota (ejlasota at gmail.com)
Date: 2023-02-21T17:16:53+01:00

Commit Message:
VCRUISE: Rename detection entry from GOG to DVD

Changed paths:
    engines/vcruise/detection_tables.h


diff --git a/engines/vcruise/detection_tables.h b/engines/vcruise/detection_tables.h
index 44b555f485f..c6ac0508c0c 100644
--- a/engines/vcruise/detection_tables.h
+++ b/engines/vcruise/detection_tables.h
@@ -30,10 +30,10 @@ namespace VCruise {
 
 static const VCruiseGameDescription gameDescriptions[] = {
 
-	{ // Reah: Face the Unknown, GOG downloadable version
+	{ // Reah: Face the Unknown, DVD/digital version
 		{
 			"reah",
-			"GOG",
+			"DVD",
 			{
 				{"Reah.exe", 0, "60ec19c53f1323cc7f0314f98d396283", 304128},
 				AD_LISTEND


Commit: 9df627d4972abac3fbeff269d589f72560459aaf
    https://github.com/scummvm/scummvm/commit/9df627d4972abac3fbeff269d589f72560459aaf
Author: elasota (ejlasota at gmail.com)
Date: 2023-02-21T17:16:53+01:00

Commit Message:
VCRUISE: Add game ID initializer for end marker.

Changed paths:
    engines/vcruise/detection.h
    engines/vcruise/detection_tables.h


diff --git a/engines/vcruise/detection.h b/engines/vcruise/detection.h
index 8e7c8c64efc..373ba7a482b 100644
--- a/engines/vcruise/detection.h
+++ b/engines/vcruise/detection.h
@@ -27,8 +27,10 @@
 namespace VCruise {
 
 enum VCruiseGameID {
-	GID_REAH	= 0,
-	GID_SCHIZM	= 1,
+	GID_UNKNOWN	= 0,
+
+	GID_REAH	= 1,
+	GID_SCHIZM	= 2,
 };
 
 struct VCruiseGameDescription {
diff --git a/engines/vcruise/detection_tables.h b/engines/vcruise/detection_tables.h
index c6ac0508c0c..15883611af0 100644
--- a/engines/vcruise/detection_tables.h
+++ b/engines/vcruise/detection_tables.h
@@ -45,7 +45,7 @@ static const VCruiseGameDescription gameDescriptions[] = {
 		},
 		GID_REAH,
 	},
-	{ AD_TABLE_END_MARKER }
+	{ AD_TABLE_END_MARKER, GID_UNKNOWN }
 };
 
 } // End of namespace MTropolis


Commit: 3cbb072dd1d353f28dfc5f7606027a308d4f3758
    https://github.com/scummvm/scummvm/commit/3cbb072dd1d353f28dfc5f7606027a308d4f3758
Author: elasota (ejlasota at gmail.com)
Date: 2023-02-21T17:16:53+01:00

Commit Message:
VCRUISE: Remove unused variable

Changed paths:
    engines/vcruise/script.cpp


diff --git a/engines/vcruise/script.cpp b/engines/vcruise/script.cpp
index 8701a433064..42b8bb7a549 100644
--- a/engines/vcruise/script.cpp
+++ b/engines/vcruise/script.cpp
@@ -752,9 +752,6 @@ uint ScriptCompiler::indexString(const Common::String &str) {
 }
 
 Common::SharedPtr<ScriptSet> compileLogicFile(Common::ReadStream &stream, uint streamSize, const Common::String &blamePath) {
-
-	uint cipherOffset = 255u - (streamSize % 255u);
-
 	LogicUnscrambleStream unscrambleStream(&stream, streamSize);
 	TextParser parser(&unscrambleStream);
 


Commit: 10273c1ad03d1574488e901aed912145a1187773
    https://github.com/scummvm/scummvm/commit/10273c1ad03d1574488e901aed912145a1187773
Author: elasota (ejlasota at gmail.com)
Date: 2023-02-21T17:16:53+01:00

Commit Message:
VCRUISE: Quiet some warnings

Changed paths:
    engines/vcruise/runtime.cpp


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 32ea00f9a8e..d734454ef18 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -811,7 +811,7 @@ void Runtime::scriptOpSAnimL(ScriptArg_t arg) {
 	TAKE_STACK(kAnimDefStackArgs + 2);
 
 	if (stackArgs[kAnimDefStackArgs] != 0)
-		warning("sanimL second operand wasn't zero (what does that do??)");
+		warning("sanimL second operand wasn't zero (what does that do?)");
 
 	AnimationDef animDef = stackArgsToAnimDef(stackArgs + 0);
 	uint direction = stackArgs[kAnimDefStackArgs + 1];
@@ -888,6 +888,7 @@ void Runtime::scriptOpSoundL2(ScriptArg_t arg) {
 	TAKE_STACK(2);
 
 	warning("Sound loop not implemented yet");
+	(void)stackArgs;
 }
 
 void Runtime::scriptOpMusic(ScriptArg_t arg) {
@@ -900,12 +901,14 @@ void Runtime::scriptOpMusicUp(ScriptArg_t arg) {
 	TAKE_STACK(2);
 
 	warning("Music volume changes are not implemented");
+	(void)stackArgs;
 }
 
 void Runtime::scriptOpMusicDn(ScriptArg_t arg) {
 	TAKE_STACK(2);
 
 	warning("Music volume changes are not implemented");
+	(void)stackArgs;
 }
 
 void Runtime::scriptOpParm1(ScriptArg_t arg) { error("Unimplemented opcode"); }
@@ -919,6 +922,7 @@ void Runtime::scriptOpRandom(ScriptArg_t arg) { error("Unimplemented opcode"); }
 
 void Runtime::scriptOpDrop(ScriptArg_t arg) {
 	TAKE_STACK(1);
+	(void)stackArgs;
 }
 
 void Runtime::scriptOpDup(ScriptArg_t arg) {


Commit: 4807ac156a4248b701c426f24e7ea0dbb14026c0
    https://github.com/scummvm/scummvm/commit/4807ac156a4248b701c426f24e7ea0dbb14026c0
Author: elasota (ejlasota at gmail.com)
Date: 2023-02-21T17:16:53+01:00

Commit Message:
VCRUISE: Remove unused variable

Changed paths:
    engines/vcruise/script.cpp


diff --git a/engines/vcruise/script.cpp b/engines/vcruise/script.cpp
index 42b8bb7a549..96a56c14103 100644
--- a/engines/vcruise/script.cpp
+++ b/engines/vcruise/script.cpp
@@ -253,7 +253,6 @@ void ScriptCompiler::expectNumber(uint32 &outNumber) {
 
 void ScriptCompiler::compileRoomScriptSet(ScriptSet *ss) {
 	Common::SharedPtr<RoomScriptSet> roomScript;
-	uint roomID = 0;
 
 	TextParserState state;
 	Common::String token;


Commit: 8dccd9d2b0945c841ac8b3534a772ab63bbcc27b
    https://github.com/scummvm/scummvm/commit/8dccd9d2b0945c841ac8b3534a772ab63bbcc27b
Author: elasota (ejlasota at gmail.com)
Date: 2023-02-21T17:16:53+01:00

Commit Message:
VCRUISE: Quiet more unused variable warnings

Changed paths:
    engines/vcruise/runtime.cpp


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index d734454ef18..658f54c9a38 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -970,18 +970,21 @@ void Runtime::scriptOpBitSet1(ScriptArg_t arg) { error("Unimplemented opcode");
 void Runtime::scriptOpDisc1(ScriptArg_t arg) {
 	// Disc check, always pass
 	TAKE_STACK(1);
+	(void)stackArgs;
 	_scriptStack.push_back(1);
 }
 
 void Runtime::scriptOpDisc2(ScriptArg_t arg) {
 	// Disc check, always pass
 	TAKE_STACK(2);
+	(void)stackArgs;
 	_scriptStack.push_back(1);
 }
 
 void Runtime::scriptOpDisc3(ScriptArg_t arg) {
 	// Disc check, always pass
 	TAKE_STACK(3);
+	(void)stackArgs;
 	_scriptStack.push_back(1);
 }
 


Commit: 0d77df259c4f528d0fabf1cb67d71dd7d21eeaa1
    https://github.com/scummvm/scummvm/commit/0d77df259c4f528d0fabf1cb67d71dd7d21eeaa1
Author: elasota (ejlasota at gmail.com)
Date: 2023-02-21T17:16:53+01:00

Commit Message:
VCRUISE: Only allow real 32-bit formats, permit 555 RGB.

Changed paths:
    engines/vcruise/vcruise.cpp


diff --git a/engines/vcruise/vcruise.cpp b/engines/vcruise/vcruise.cpp
index b87d2c6d18a..dda9e4c0ef5 100644
--- a/engines/vcruise/vcruise.cpp
+++ b/engines/vcruise/vcruise.cpp
@@ -65,14 +65,17 @@ void VCruiseEngine::handleEvents() {
 Common::Error VCruiseEngine::run() {
 	Common::List<Graphics::PixelFormat> pixelFormats = _system->getSupportedFormats();
 
-	const Graphics::PixelFormat *fmt16 = nullptr;
+	const Graphics::PixelFormat *fmt16_565 = nullptr;
+	const Graphics::PixelFormat *fmt16_555 = nullptr;
 	const Graphics::PixelFormat *fmt32 = nullptr;
 
 	for (const Graphics::PixelFormat &fmt : pixelFormats) {
-		if (fmt.rBits() == 8 && fmt.gBits() == 8 && fmt.bBits() == 8)
+		if (fmt32 == nullptr && fmt.bytesPerPixel == 4 && fmt.rBits() == 8 && fmt.gBits() == 8 && fmt.bBits() == 8)
 			fmt32 = &fmt;
-		if ((fmt.rBits() + fmt.gBits() + fmt.bBits()) == 16)
-			fmt16 = &fmt;
+		if (fmt16_555 == nullptr && fmt.rBits() == 5 && fmt.gBits() == 5 && fmt.bBits() == 5)
+			fmt16_555 = &fmt;
+		if (fmt16_565 == nullptr && fmt.rBits() == 5 && fmt.gBits() == 6 && fmt.bBits() == 5)
+			fmt16_565 = &fmt;
 	}
 
 	// Figure out screen layout
@@ -115,8 +118,10 @@ Common::Error VCruiseEngine::run() {
 
 	if (fmt32)
 		initGraphics(size.x, size.y, fmt32);
-	else if (fmt16)
-		initGraphics(size.x, size.y, fmt16);
+	else if (fmt16_565)
+		initGraphics(size.x, size.y, fmt16_565);
+	else if (fmt16_555)
+		initGraphics(size.x, size.y, fmt16_555);
 	else
 		error("Unable to find a suitable graphics format");
 


Commit: c5956be096f4ddfef996713ebfe222a4253c68aa
    https://github.com/scummvm/scummvm/commit/c5956be096f4ddfef996713ebfe222a4253c68aa
Author: elasota (ejlasota at gmail.com)
Date: 2023-02-21T17:16:53+01:00

Commit Message:
VCRUISE: Add Schizm and Reah CD

Changed paths:
    engines/vcruise/detection_tables.h


diff --git a/engines/vcruise/detection_tables.h b/engines/vcruise/detection_tables.h
index 15883611af0..cdcea576603 100644
--- a/engines/vcruise/detection_tables.h
+++ b/engines/vcruise/detection_tables.h
@@ -45,6 +45,36 @@ static const VCruiseGameDescription gameDescriptions[] = {
 		},
 		GID_REAH,
 	},
+	{ // Reah: Face the Unknown, 6 CD Version
+		{
+			"reah",
+			"CD",
+			{
+				{"Reah.exe", 0, "77bc7f7819cdd443f52b193529138c87", 305664},
+				AD_LISTEND
+			},
+			Common::EN_ANY,
+			Common::kPlatformWindows,
+			ADGF_UNSTABLE,
+			GUIO0()
+		},
+		GID_REAH,
+	},
+	{ // Schizm, 5 CD Version
+		{
+			"schizm",
+			"CD",
+			{
+				{"Schizm.exe", 0, "296edd26d951c3bdc4d303c4c88b27cd", 364544},
+				AD_LISTEND
+			},
+			Common::EN_ANY,
+			Common::kPlatformWindows,
+			ADGF_UNSTABLE,
+			GUIO0()
+		},
+		GID_SCHIZM,
+	},
 	{ AD_TABLE_END_MARKER, GID_UNKNOWN }
 };
 


Commit: 0ea1beacc8cb38889304ff5f9f1618873f1cf752
    https://github.com/scummvm/scummvm/commit/0ea1beacc8cb38889304ff5f9f1618873f1cf752
Author: elasota (ejlasota at gmail.com)
Date: 2023-02-21T17:16:53+01:00

Commit Message:
VCRUISE: Fix video frame overrun

Changed paths:
    engines/vcruise/runtime.cpp


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 658f54c9a38..872afb6a581 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -239,9 +239,7 @@ bool Runtime::runWaitForAnimation() {
 			_system->copyRectToScreen(_gameSection.surf->getBasePtr(copyRect.left, copyRect.top), _gameSection.surf->pitch, copyRect.left + _gameSection.rect.left, copyRect.top + _gameSection.rect.top, copyRect.width(), copyRect.height());
 		}
 
-		// The current frame will be the frame to be decoded on the next decode call,
-		// which means if it exceeds the last frame then it's time to stop
-		if (_animDecoder->getCurFrame() > static_cast<int>(_animLastFrame)) {
+		if (_animDecoder->getCurFrame() >= static_cast<int>(_animLastFrame)) {
 			_animDecoder->pauseVideo(true);
 			_animDecoderState = kAnimDecoderStatePaused;
 


Commit: 83dee812429b682a744a198de1c69df4845eabe1
    https://github.com/scummvm/scummvm/commit/83dee812429b682a744a198de1c69df4845eabe1
Author: elasota (ejlasota at gmail.com)
Date: 2023-02-21T17:16:53+01:00

Commit Message:
VCRUISE: Fix formatting

Changed paths:
    engines/vcruise/metaengine.cpp


diff --git a/engines/vcruise/metaengine.cpp b/engines/vcruise/metaengine.cpp
index 0994980ed8b..56c5535e8fb 100644
--- a/engines/vcruise/metaengine.cpp
+++ b/engines/vcruise/metaengine.cpp
@@ -33,14 +33,19 @@ struct Surface;
 namespace VCruise {
 
 static const ADExtraGuiOptionsMap optionsList[] = {
-	{GAMEOPTION_LAUNCH_DEBUG,
-	 {_s("Start with debugger"),
-	  _s("Starts with the debugger dashboard active."),
-	  "vcruise_debug",
-	  false,
-	  0,
-	  0}},
-	AD_EXTRA_GUI_OPTIONS_TERMINATOR};
+	{
+		GAMEOPTION_LAUNCH_DEBUG,
+		{
+			_s("Start with debugger"),
+			_s("Starts with the debugger dashboard active."),
+			"vcruise_debug",
+			false,
+			0,
+			0
+		}
+	},
+	AD_EXTRA_GUI_OPTIONS_TERMINATOR
+};
 
 } // End of namespace VCruise
 


Commit: bfa24934b7b765dc8a7cdd7d8dfedbe58621dd15
    https://github.com/scummvm/scummvm/commit/bfa24934b7b765dc8a7cdd7d8dfedbe58621dd15
Author: elasota (ejlasota at gmail.com)
Date: 2023-02-21T17:16:53+01:00

Commit Message:
VCRUISE: Fix script miscompilation

Changed paths:
    engines/vcruise/script.cpp


diff --git a/engines/vcruise/script.cpp b/engines/vcruise/script.cpp
index 96a56c14103..6d104aead69 100644
--- a/engines/vcruise/script.cpp
+++ b/engines/vcruise/script.cpp
@@ -320,6 +320,7 @@ void ScriptCompiler::compileScreenScriptSet(ScreenScriptSet *sss) {
 			codeGenScript(protoScript, *currentScript);
 
 			currentScript.reset(new Script());
+			protoScript.reset();
 
 			sss->interactionScripts[interactionNumber] = currentScript;
 		} else if (compileInstructionToken(protoScript, token)) {


Commit: 2f69cda8f0ed7cbc184924cbf27ed0645393dfd7
    https://github.com/scummvm/scummvm/commit/2f69cda8f0ed7cbc184924cbf27ed0645393dfd7
Author: elasota (ejlasota at gmail.com)
Date: 2023-02-21T17:16:53+01:00

Commit Message:
VCRUISE: Implement debug mode and some idle state functionality

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 872afb6a581..580ba39a54a 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -25,6 +25,8 @@
 #include "common/stream.h"
 
 #include "graphics/cursorman.h"
+#include "graphics/font.h"
+#include "graphics/fontman.h"
 #include "graphics/wincursor.h"
 #include "graphics/managed_surface.h"
 
@@ -71,10 +73,16 @@ void Runtime::RenderSection::init(const Common::Rect &paramRect, const Graphics:
 	surf->fillRect(Common::Rect(0, 0, surf->w, surf->h), 0xffffffff);
 }
 
+Runtime::OSEvent::OSEvent() : type(kOSEventTypeInvalid), keyCode(static_cast<Common::KeyCode>(0)) {
+}
+
+
 Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &rootFSNode, VCruiseGameID gameID)
 	: _system(system), _mixer(mixer), _roomNumber(1), _screenNumber(0), _direction(0), _havePanAnimations(0), _loadedRoomNumber(0), _activeScreenNumber(0),
-	  _gameState(kGameStateBoot), _gameID(gameID), _rootFSNode(rootFSNode), _havePendingScreenChange(false), _scriptNextInstruction(0),
-	  _escOn(false), _loadedAnimation(0), _animFrameNumber(0), _animLastFrame(0), _animDecoderState(kAnimDecoderStateStopped) {
+	  _gameState(kGameStateBoot), _gameID(gameID), _rootFSNode(rootFSNode), _havePendingScreenChange(false), _havePendingReturnToIdleState(false), _scriptNextInstruction(0),
+	  _escOn(false), _lmbInteractionState(false), _debugMode(false),
+	  _loadedAnimation(0), _animPendingDecodeFrame(0), _animDisplayingFrame(0), _animFirstFrame(0), _animLastFrame(0), _animDecoderState(kAnimDecoderStateStopped),
+	  _animPlayWhileIdle(false), _idleIsOnInteraction(false), _idleInteractionID(0) {
 
 	for (uint i = 0; i < kNumDirections; i++)
 		_haveIdleAnimations[i] = false;
@@ -140,6 +148,24 @@ void Runtime::loadCursors(const char *exeName) {
 			}
 		}
 	}
+
+	if (_gameID == GID_REAH) {
+		_namedCursors["CUR_TYL"] = 13;		// Tyl = back
+		//_namedCursors["CUR_NIC"] = ?		// Nic = nothing
+		//_namedCursors["CUR_WEZ"] = 50		// Wez = call? FIXME
+		_namedCursors["CUR_LUPA"] = 21;		// Lupa = magnifier, could be 36 too?
+		_namedCursors["CUR_NAC"] = 35;		// Nac = top?  Not sure.  But this is the finger pointer.
+		_namedCursors["CUR_PRZOD"] = 1;		// Przod = forward
+
+		// CUR_ZOSTAW is in the executable memory but appears to be unused
+	}
+}
+
+void Runtime::setDebugMode(bool debugMode) {
+	if (debugMode) {
+		_debugMode = true;
+		_gameDebugBackBuffer.init(_gameSection.rect, _gameSection.surf->format);
+	}
 }
 
 bool Runtime::runFrame() {
@@ -167,6 +193,9 @@ bool Runtime::runFrame() {
 		}
 	}
 
+	// Discard any unconsumed OS events
+	_pendingEvents.clear();
+
 	return true;
 }
 
@@ -191,17 +220,71 @@ bool Runtime::runIdle() {
 	if (_havePendingScreenChange) {
 		_havePendingScreenChange = false;
 
+		_havePendingReturnToIdleState = true;
+
 		changeToScreen(_roomNumber, _screenNumber);
 		return true;
 	}
 
+	if (_havePendingReturnToIdleState) {
+		_havePendingReturnToIdleState = false;
+
+		returnToIdleState();
+		return true;
+	}
+
+	if (_animPlayWhileIdle) {
+		bool animEnded = false;
+		continuePlayingAnimation(true, animEnded);
+	}
+
+	if (_debugMode)
+		drawDebugOverlay();
+
+	OSEvent osEvent;
+	while (popOSEvent(osEvent)) {
+		if (osEvent.type == kOSEventTypeMouseMove)
+			dischargeIdleMouseMove();
+	}
+
 	return false;
 }
 
 bool Runtime::runWaitForAnimation() {
-	if (!_animDecoder) {
+	bool animEnded = false;
+	continuePlayingAnimation(false, animEnded);
+
+	if (animEnded) {
 		_gameState = kGameStateScript;
 		return true;
+	} else {
+		// Still waiting, check events
+		OSEvent evt;
+		while (popOSEvent(evt)) {
+			if (evt.type == kOSEventTypeKeyDown && evt.keyCode == Common::KEYCODE_ESCAPE) {
+				if (_escOn) {
+					// Terminate the animation
+					if (_animDecoderState == kAnimDecoderStatePlaying) {
+						_animDecoder->pauseVideo(true);
+						_animDecoderState = kAnimDecoderStatePaused;
+					}
+					_gameState = kGameStateScript;
+					return true;
+				}
+			}
+		}
+
+		// Yield
+		return false;
+	}
+}
+
+void Runtime::continuePlayingAnimation(bool loop, bool &outAnimationEnded) {
+	outAnimationEnded = false;
+
+	if (!_animDecoder) {
+		outAnimationEnded = true;
+		return;
 	}
 
 	bool needsFirstFrame = false;
@@ -222,12 +305,27 @@ bool Runtime::runWaitForAnimation() {
 		if (!needNewFrame)
 			break;
 
+		// We check this here for timing reasons: The no-loop case after the draw terminates the animation as soon as the last frame
+		// starts delaying without waiting for the time until the next frame to expire.
+		// The loop check here DOES wait for the time until next frame to expire.
+		if (loop && _animPendingDecodeFrame > _animLastFrame) {
+			if (!_animDecoder->seekToFrame(_animFirstFrame)) {
+				outAnimationEnded = true;
+				return;
+			}
+
+			_animPendingDecodeFrame = _animFirstFrame;
+		}
+
 		const Graphics::Surface *surface = _animDecoder->decodeNextFrame();
 		if (!surface) {
-			_gameState = kGameStateScript;
-			return true;
+			outAnimationEnded = true;
+			return;
 		}
 
+		_animDisplayingFrame = _animPendingDecodeFrame;
+		_animPendingDecodeFrame++;
+
 		Common::Rect copyRect = Common::Rect(0, 0, surface->w, surface->h);
 
 		Common::Rect constraintRect = Common::Rect(0, 0, _gameSection.rect.width(), _gameSection.rect.height());
@@ -236,20 +334,31 @@ bool Runtime::runWaitForAnimation() {
 
 		if (copyRect.isValidRect() || !copyRect.isEmpty()) {
 			_gameSection.surf->blitFrom(*surface, copyRect, copyRect);
-			_system->copyRectToScreen(_gameSection.surf->getBasePtr(copyRect.left, copyRect.top), _gameSection.surf->pitch, copyRect.left + _gameSection.rect.left, copyRect.top + _gameSection.rect.top, copyRect.width(), copyRect.height());
+			drawSectionToScreen(_gameSection, copyRect);
 		}
 
-		if (_animDecoder->getCurFrame() >= static_cast<int>(_animLastFrame)) {
-			_animDecoder->pauseVideo(true);
-			_animDecoderState = kAnimDecoderStatePaused;
+		if (!loop) {
+			if (_animDisplayingFrame >= static_cast<int>(_animLastFrame)) {
+				_animDecoder->pauseVideo(true);
+				_animDecoderState = kAnimDecoderStatePaused;
 
-			_gameState = kGameStateScript;
-			return true;
+				outAnimationEnded = true;
+				return;
+			}
 		}
 	}
+}
 
-	// Pump and handle events
-	return false;
+void Runtime::drawSectionToScreen(const RenderSection &section, const Common::Rect &rect) {
+	if (_debugMode && (&_gameSection == &section)) {
+		_gameDebugBackBuffer.surf->blitFrom(*section.surf, rect, rect);
+		commitSectionToScreen(_gameDebugBackBuffer, rect);
+	} else
+		commitSectionToScreen(section, rect);
+}
+
+void Runtime::commitSectionToScreen(const RenderSection &section, const Common::Rect &rect) {
+	_system->copyRectToScreen(section.surf->getBasePtr(rect.left, rect.top), section.surf->pitch, rect.left + section.rect.left, rect.top + section.rect.top, rect.width(), rect.height());
 }
 
 #ifdef DISPATCH_OP
@@ -366,6 +475,30 @@ void Runtime::terminateScript() {
 		changeToScreen(_roomNumber, _screenNumber);
 }
 
+bool Runtime::popOSEvent(OSEvent &evt) {
+	OSEvent tempEvent;
+
+	while (_pendingEvents.size() > 0) {
+		tempEvent = _pendingEvents[0];
+		_pendingEvents.remove_at(0);
+
+		// Button events automatically inject a mouse move position
+		if (tempEvent.type == kOSEventTypeMouseMove) {
+			// If this didn't actually change the mouse position, which is common with synthetic mouse events,
+			// discard the event.
+			if (_mousePos == tempEvent.pos)
+				continue;
+
+			_mousePos = tempEvent.pos;
+		}
+
+		evt = tempEvent;
+		return true;
+	}
+
+	return false;
+}
+
 void Runtime::loadIndex() {
 	Common::FSNode indexFSNode = _logDir.getChild("Index.txt");
 
@@ -487,7 +620,7 @@ void Runtime::changeToScreen(uint roomNumber, uint screenNumber) {
 				if (screenScriptIt != screenScriptsMap.end()) {
 					const Common::SharedPtr<Script> &script = screenScriptIt->_value->entryScript;
 					if (script)
-						activateScript(script);
+						activateScript(script, false);
 				}
 			}
 		}
@@ -496,6 +629,63 @@ void Runtime::changeToScreen(uint roomNumber, uint screenNumber) {
 
 		for (uint i = 0; i < kNumDirections; i++)
 			_haveIdleAnimations[i] = false;
+
+		_havePendingReturnToIdleState = true;
+	}
+}
+
+void Runtime::returnToIdleState() {
+	_animPlayWhileIdle = false;
+
+	if (_haveIdleAnimations[_direction]) {
+		changeAnimation(_idleAnimations[_direction]);
+		_animPlayWhileIdle = true;
+	}
+
+	_idleIsOnInteraction = false;
+
+	changeToCursor(_cursors[kCursorArrow]);
+	dischargeIdleMouseMove();
+}
+
+void Runtime::changeToCursor(const Common::SharedPtr<Graphics::WinCursorGroup> &cursor) {
+	if (!cursor)
+		CursorMan.showMouse(false);
+	else {
+		CursorMan.replaceCursor(cursor->cursors[0].cursor);
+		CursorMan.showMouse(true);
+	}
+}
+
+void Runtime::dischargeIdleMouseMove() {
+	const MapScreenDirectionDef *sdDef = _map.getScreenDirection(_screenNumber, _direction);
+
+	Common::Point relMouse(_mousePos.x - _gameSection.rect.left, _mousePos.y - _gameSection.rect.top);
+
+	bool isOnInteraction = false;
+	uint interactionID = 0;
+	if (sdDef) {
+		for (const InteractionDef &idef : sdDef->interactions) {
+			if (idef.rect.contains(relMouse)) {
+				isOnInteraction = true;
+				interactionID = idef.interactionID;
+				break;
+			}
+		}
+	}
+
+	if (isOnInteraction && (_idleIsOnInteraction == false || interactionID != _idleInteractionID)) {
+		_idleIsOnInteraction = true;
+		_idleInteractionID = interactionID;
+
+		// New interaction, is there a script?
+		Common::SharedPtr<Script> script = findScriptForInteraction(interactionID);
+
+		if (script)
+			activateScript(script, false);
+	} else if (!isOnInteraction && _idleIsOnInteraction) {
+		_idleIsOnInteraction = false;
+		changeToCursor(_cursors[kCursorArrow]);
 	}
 }
 
@@ -596,7 +786,8 @@ void Runtime::changeAnimation(const AnimationDef &animDef) {
 	}
 
 	_animDecoder->seekToFrame(animDef.firstFrame);
-	_animFrameNumber = animDef.firstFrame;
+	_animPendingDecodeFrame = animDef.firstFrame;
+	_animFirstFrame = animDef.firstFrame;
 	_animLastFrame = animDef.lastFrame;
 }
 
@@ -609,10 +800,11 @@ AnimationDef Runtime::stackArgsToAnimDef(const StackValue_t *args) const {
 	return def;
 }
 
-void Runtime::activateScript(const Common::SharedPtr<Script> &script) {
+void Runtime::activateScript(const Common::SharedPtr<Script> &script, bool lmbInteractionState) {
 	if (script->instrs.size() == 0)
 		return;
 
+	_lmbInteractionState = false;
 	_activeScript = script;
 	_scriptNextInstruction = 0;
 	_gameState = kGameStateScript;
@@ -738,29 +930,92 @@ void Runtime::allocateRoomsUpTo(uint roomNumber) {
 	}
 }
 
+void Runtime::drawDebugOverlay() {
+	if (!_debugMode)
+		return;
+
+	const Graphics::PixelFormat pixFmt = _gameDebugBackBuffer.surf->format;
+
+	const Graphics::Font *font = FontMan.getFontByUsage(Graphics::FontManager::kConsoleFont);
+
+	uint32 whiteColor = pixFmt.ARGBToColor(255, 255, 255, 255);
+	uint32 blackColor = pixFmt.ARGBToColor(255, 0, 0, 0);
+
+	const MapScreenDirectionDef *sdDef = _map.getScreenDirection(_screenNumber, _direction);
+	if (sdDef) {
+		for (const InteractionDef &idef : sdDef->interactions) {
+			Common::Rect rect = idef.rect;
+
+			Common::String label = Common::String::format("0%x", static_cast<int>(idef.interactionID));
+
+			Graphics::ManagedSurface *surf = _gameDebugBackBuffer.surf.get();
+
+			if (font) {
+				Common::Point pt = Common::Point(rect.left + 2, rect.top + 2);
+
+				font->drawString(surf, label, pt.x + 1, pt.y + 1, rect.width(), blackColor);
+				font->drawString(surf, label, pt.x, pt.y, rect.width(), whiteColor);
+			}
+
+			surf->frameRect(Common::Rect(rect.left + 1, rect.top + 1, rect.right + 1, rect.bottom + 1), blackColor);
+			surf->frameRect(rect, whiteColor);
+		}
+	}
+
+	commitSectionToScreen(_gameDebugBackBuffer, Common::Rect(0, 0, _gameDebugBackBuffer.rect.width(), _gameDebugBackBuffer.rect.height()));
+}
+
+Common::SharedPtr<Script> Runtime::findScriptForInteraction(uint interactionID) const {
+	if (_scriptSet) {
+		Common::HashMap<uint, Common::SharedPtr<RoomScriptSet> >::const_iterator roomScriptIt = _scriptSet->roomScripts.find(_roomNumber);
+
+		if (roomScriptIt != _scriptSet->roomScripts.end()) {
+			const RoomScriptSet &roomScriptSet = *roomScriptIt->_value;
+
+			Common::HashMap<uint, Common::SharedPtr<ScreenScriptSet> >::const_iterator screenScriptIt = roomScriptSet.screenScripts.find(_screenNumber);
+			if (screenScriptIt != roomScriptSet.screenScripts.end()) {
+				const ScreenScriptSet &screenScriptSet = *screenScriptIt->_value;
+
+				Common::HashMap<uint, Common::SharedPtr<Script> >::const_iterator interactionScriptIt = screenScriptSet.interactionScripts.find(interactionID);
+				if (interactionScriptIt != screenScriptSet.interactionScripts.end())
+					return interactionScriptIt->_value;
+			}
+		}
+	}
+
+	return nullptr;
+}
+
 void Runtime::onLButtonDown(int16 x, int16 y) {
+	onMouseMove(x, y);
+
+	OSEvent evt;
+	evt.type = kOSEventTypeLButtonDown;
+	evt.pos = Common::Point(x, y);
+	_pendingEvents.push_back(evt);
 }
 
 void Runtime::onLButtonUp(int16 x, int16 y) {
+	onMouseMove(x, y);
+
+	OSEvent evt;
+	evt.type = kOSEventTypeLButtonUp;
+	evt.pos = Common::Point(x, y);
+	_pendingEvents.push_back(evt);
 }
 
 void Runtime::onMouseMove(int16 x, int16 y) {
+	OSEvent evt;
+	evt.type = kOSEventTypeMouseMove;
+	evt.pos = Common::Point(x, y);
+	_pendingEvents.push_back(evt);
 }
 
 void Runtime::onKeyDown(Common::KeyCode keyCode) {
-	if (keyCode == Common::KEYCODE_ESCAPE) {
-		if (_gameState == kGameStateWaitingForAnimation) {
-			if (_escOn) {
-				// Terminate the animation
-				if (_animDecoderState == kAnimDecoderStatePlaying) {
-					_animDecoder->pauseVideo(true);
-					_animDecoderState = kAnimDecoderStatePaused;
-				}
-				_gameState = kGameStateScript;
-				return;
-			}
-		}
-	}
+	OSEvent evt;
+	evt.type = kOSEventTypeKeyDown;
+	evt.keyCode = keyCode;
+	_pendingEvents.push_back(evt);
 }
 
 #ifdef CHECK_STACK
@@ -836,7 +1091,7 @@ void Runtime::scriptOpAnimS(ScriptArg_t arg) {
 
 	changeAnimation(animDef);
 
-	_gameState = kGameStateWaitingForAnimation;	// FIXME
+	_gameState = kGameStateWaitingForAnimation;
 	_screenNumber = stackArgs[kAnimDefStackArgs + 0];
 	_direction = stackArgs[kAnimDefStackArgs + 1];
 	_havePendingScreenChange = true;
@@ -876,9 +1131,22 @@ void Runtime::scriptOpVarStore(ScriptArg_t arg) {
 	_variables[varID] = stackArgs[0];
 }
 
-void Runtime::scriptOpSetCursor(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpSetCursor(ScriptArg_t arg) {
+	TAKE_STACK(1);
+
+	if (stackArgs[0] < 0 || static_cast<uint>(stackArgs[0]) >= _cursors.size())
+		error("Invalid cursor ID");
+
+	changeToCursor(_cursors[stackArgs[0]]);
+}
+
 void Runtime::scriptOpSetRoom(ScriptArg_t arg) { error("Unimplemented opcode"); }
-void Runtime::scriptOpLMB(ScriptArg_t arg) { error("Unimplemented opcode"); }
+
+void Runtime::scriptOpLMB(ScriptArg_t arg) {
+	if (!_lmbInteractionState)
+		terminateScript();
+}
+
 void Runtime::scriptOpLMB1(ScriptArg_t arg) { error("Unimplemented opcode"); }
 void Runtime::scriptOpSoundS1(ScriptArg_t arg) { error("Unimplemented opcode"); }
 
@@ -1050,7 +1318,17 @@ void Runtime::scriptOpSoundName(ScriptArg_t arg) {
 	_scriptStack.push_back(soundID);
 }
 
-void Runtime::scriptOpCursorName(ScriptArg_t arg) { error("Unimplemented opcode"); }
+void Runtime::scriptOpCursorName(ScriptArg_t arg) {
+	const Common::String &cursorName = _scriptSet->strings[arg];
+
+	Common::HashMap<Common::String, StackValue_t>::const_iterator namedCursorIt = _namedCursors.find(cursorName);
+	if (namedCursorIt == _namedCursors.end()) {
+		error("Unimplemented cursor name '%s'", cursorName.c_str());
+		return;
+	}
+
+	_scriptStack.push_back(namedCursorIt->_value);
+}
 
 void Runtime::scriptOpCheckValue(ScriptArg_t arg) {
 	PEEK_STACK(1);
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index ab514e0c05a..042e7cc9fb5 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -111,6 +111,7 @@ public:
 	void initSections(Common::Rect gameRect, Common::Rect menuRect, Common::Rect trayRect, const Graphics::PixelFormat &pixFmt);
 
 	void loadCursors(const char *exeName);
+	void setDebugMode(bool debugMode);
 
 	bool runFrame();
 	void drawFrame();
@@ -151,6 +152,24 @@ private:
 		void init(const Common::Rect &paramRect, const Graphics::PixelFormat &fmt);
 	};
 
+	enum OSEventType {
+		kOSEventTypeInvalid,
+
+		kOSEventTypeMouseMove,
+		kOSEventTypeLButtonDown,
+		kOSEventTypeLButtonUp,
+
+		kOSEventTypeKeyDown,
+	};
+
+	struct OSEvent {
+		OSEvent();
+
+		OSEventType type;
+		Common::Point pos;
+		Common::KeyCode keyCode;
+	};
+
 	typedef int32 ScriptArg_t;
 	typedef int32 StackValue_t;
 
@@ -158,10 +177,18 @@ private:
 	bool runIdle();
 	bool runScript();
 	bool runWaitForAnimation();
+	void continuePlayingAnimation(bool loop, bool &outEndedAnimation);
+	void drawSectionToScreen(const RenderSection &section, const Common::Rect &rect);
+	void commitSectionToScreen(const RenderSection &section, const Common::Rect &rect);
 	void terminateScript();
 
+	bool popOSEvent(OSEvent &evt);
+
 	void loadIndex();
 	void changeToScreen(uint roomNumber, uint screenNumber);
+	void returnToIdleState();
+	void changeToCursor(const Common::SharedPtr<Graphics::WinCursorGroup> &cursor);
+	void dischargeIdleMouseMove();
 	void loadMap(Common::SeekableReadStream *stream);
 
 	void changeMusicTrack(int musicID);
@@ -169,11 +196,16 @@ private:
 
 	AnimationDef stackArgsToAnimDef(const StackValue_t *args) const;
 
-	void activateScript(const Common::SharedPtr<Script> &script);
+	void activateScript(const Common::SharedPtr<Script> &script, bool lmbInteractionState);
 
 	bool parseIndexDef(TextParser &parser, IndexParseType parseType, uint roomNumber, const Common::String &blamePath);
 	void allocateRoomsUpTo(uint roomNumber);
 
+	void drawDebugOverlay();
+
+	Common::SharedPtr<Script> findScriptForInteraction(uint interactionID) const;
+
+	// Script things
 	void scriptOpNumber(ScriptArg_t arg);
 	void scriptOpRotate(ScriptArg_t arg);
 	void scriptOpAngle(ScriptArg_t arg);
@@ -249,6 +281,8 @@ private:
 	Common::Array<Common::SharedPtr<Graphics::WinCursorGroup> > _cursors;		// Cursors indexed as CURSOR_CUR_##
 	Common::Array<Common::SharedPtr<Graphics::WinCursorGroup> > _cursorsShort;	// Cursors indexed as CURSOR_#
 
+	Common::HashMap<Common::String, StackValue_t> _namedCursors;
+
 	OSystem *_system;
 	uint _roomNumber;	// Room number can be changed independently of the loaded room, the screen doesn't change until a command changes it
 	uint _screenNumber;
@@ -269,9 +303,12 @@ private:
 	uint _loadedRoomNumber;
 	uint _activeScreenNumber;
 	bool _havePendingScreenChange;
+	bool _havePendingReturnToIdleState;
 	GameState _gameState;
 
 	bool _escOn;
+	bool _lmbInteractionState;
+	bool _debugMode;
 
 	VCruiseGameID _gameID;
 
@@ -292,19 +329,31 @@ private:
 
 	Common::SharedPtr<Video::AVIDecoder> _animDecoder;
 	AnimDecoderState _animDecoderState;
-	uint _animFrameNumber;
+	uint _animPendingDecodeFrame;
+	uint _animDisplayingFrame;
+	uint _animFirstFrame;
 	uint _animLastFrame;
 	uint _loadedAnimation;
+	bool _animPlayWhileIdle;
+
+	bool _idleIsOnInteraction;
+	uint _idleInteractionID;
 
 	Audio::Mixer *_mixer;
 
 	MapDef _map;
 
 	RenderSection _gameSection;
+	RenderSection _gameDebugBackBuffer;
 	RenderSection _menuSection;
 	RenderSection _traySection;
 
+	Common::Point _mousePos;
+	Common::Array<OSEvent> _pendingEvents;
+
 	static const uint kAnimDefStackArgs = 3;
+
+	static const uint kCursorArrow = 0;
 };
 
 } // End of namespace VCruise
diff --git a/engines/vcruise/vcruise.cpp b/engines/vcruise/vcruise.cpp
index dda9e4c0ef5..d6857288fe3 100644
--- a/engines/vcruise/vcruise.cpp
+++ b/engines/vcruise/vcruise.cpp
@@ -132,6 +132,10 @@ Common::Error VCruiseEngine::run() {
 
 	_runtime->loadCursors(exeName);
 
+	if (ConfMan.getBool("vcruise_debug")) {
+		_runtime->setDebugMode(true);
+	}
+
 	// Run the game
 	while (!shouldQuit()) {
 		handleEvents();


Commit: 0b7c19a0702d1c6ff873552b12600640d64c4e83
    https://github.com/scummvm/scummvm/commit/0b7c19a0702d1c6ff873552b12600640d64c4e83
Author: elasota (ejlasota at gmail.com)
Date: 2023-02-21T17:16:53+01:00

Commit Message:
VCRUISE: Combine env vars, make some tweaks for panorama input detection

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


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 580ba39a54a..54c2caf3abc 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -67,6 +67,9 @@ const MapScreenDirectionDef *MapDef::getScreenDirection(uint screen, uint direct
 	return screenDirections[screen][direction].get();
 }
 
+ScriptEnvironmentVars::ScriptEnvironmentVars() : lmb(false) {
+}
+
 void Runtime::RenderSection::init(const Common::Rect &paramRect, const Graphics::PixelFormat &fmt) {
 	rect = paramRect;
 	surf.reset(new Graphics::ManagedSurface(paramRect.width(), paramRect.height(), fmt));
@@ -76,13 +79,13 @@ void Runtime::RenderSection::init(const Common::Rect &paramRect, const Graphics:
 Runtime::OSEvent::OSEvent() : type(kOSEventTypeInvalid), keyCode(static_cast<Common::KeyCode>(0)) {
 }
 
-
 Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &rootFSNode, VCruiseGameID gameID)
 	: _system(system), _mixer(mixer), _roomNumber(1), _screenNumber(0), _direction(0), _havePanAnimations(0), _loadedRoomNumber(0), _activeScreenNumber(0),
 	  _gameState(kGameStateBoot), _gameID(gameID), _rootFSNode(rootFSNode), _havePendingScreenChange(false), _havePendingReturnToIdleState(false), _scriptNextInstruction(0),
-	  _escOn(false), _lmbInteractionState(false), _debugMode(false),
+	  _escOn(false), _debugMode(false), _panoramaDirectionFlags(0),
 	  _loadedAnimation(0), _animPendingDecodeFrame(0), _animDisplayingFrame(0), _animFirstFrame(0), _animLastFrame(0), _animDecoderState(kAnimDecoderStateStopped),
-	  _animPlayWhileIdle(false), _idleIsOnInteraction(false), _idleInteractionID(0) {
+	  _animPlayWhileIdle(false), _idleIsOnInteraction(false), _idleInteractionID(0),
+	  _lmbDown(false), _lmbDragging(false), _lmbDownTime(0) {
 
 	for (uint i = 0; i < kNumDirections; i++)
 		_haveIdleAnimations[i] = false;
@@ -194,7 +197,10 @@ bool Runtime::runFrame() {
 	}
 
 	// Discard any unconsumed OS events
-	_pendingEvents.clear();
+	OSEvent evt;
+	while (popOSEvent(evt)) {
+		// Do nothing
+	}
 
 	return true;
 }
@@ -216,7 +222,6 @@ bool Runtime::bootGame() {
 }
 
 bool Runtime::runIdle() {
-
 	if (_havePendingScreenChange) {
 		_havePendingScreenChange = false;
 
@@ -489,7 +494,27 @@ bool Runtime::popOSEvent(OSEvent &evt) {
 			if (_mousePos == tempEvent.pos)
 				continue;
 
+			if (_lmbDown && tempEvent.pos != _lmbDownPos)
+				_lmbDragging = true;
+
 			_mousePos = tempEvent.pos;
+		} else if (tempEvent.type == kOSEventTypeLButtonDown) {
+			// Discard LButtonDown events missing a matching release (can happen if e.g. user holds button down
+			// and then alt-tabs out of the process on Windows)
+			if (_lmbDown)
+				continue;
+
+			_lmbDown = true;
+			_lmbDragging = false;
+			_lmbDownTime = tempEvent.timestamp;
+			_lmbDownPos = tempEvent.pos;
+		} else if (tempEvent.type == kOSEventTypeLButtonUp) {
+			// Discard LButtonUp events missing a matching down
+			if (!_lmbDown)
+				continue;
+
+			_lmbDown = false;
+			// Don't reset _lmbDragging - Want that available to determine if this was a drag release or click
 		}
 
 		evt = tempEvent;
@@ -499,6 +524,13 @@ bool Runtime::popOSEvent(OSEvent &evt) {
 	return false;
 }
 
+void Runtime::queueOSEvent(const OSEvent &evt) {
+	OSEvent timedEvt = evt;
+	timedEvt.timestamp = g_system->getMillis();
+
+	_pendingEvents.push_back(timedEvt);
+}
+
 void Runtime::loadIndex() {
 	Common::FSNode indexFSNode = _logDir.getChild("Index.txt");
 
@@ -620,7 +652,7 @@ void Runtime::changeToScreen(uint roomNumber, uint screenNumber) {
 				if (screenScriptIt != screenScriptsMap.end()) {
 					const Common::SharedPtr<Script> &script = screenScriptIt->_value->entryScript;
 					if (script)
-						activateScript(script, false);
+						activateScript(script, ScriptEnvironmentVars());
 				}
 			}
 		}
@@ -644,6 +676,8 @@ void Runtime::returnToIdleState() {
 
 	_idleIsOnInteraction = false;
 
+	detectPanoramaDirections();
+
 	changeToCursor(_cursors[kCursorArrow]);
 	dischargeIdleMouseMove();
 }
@@ -682,7 +716,7 @@ void Runtime::dischargeIdleMouseMove() {
 		Common::SharedPtr<Script> script = findScriptForInteraction(interactionID);
 
 		if (script)
-			activateScript(script, false);
+			activateScript(script, ScriptEnvironmentVars());
 	} else if (!isOnInteraction && _idleIsOnInteraction) {
 		_idleIsOnInteraction = false;
 		changeToCursor(_cursors[kCursorArrow]);
@@ -800,11 +834,11 @@ AnimationDef Runtime::stackArgsToAnimDef(const StackValue_t *args) const {
 	return def;
 }
 
-void Runtime::activateScript(const Common::SharedPtr<Script> &script, bool lmbInteractionState) {
+void Runtime::activateScript(const Common::SharedPtr<Script> &script, const ScriptEnvironmentVars &envVars) {
 	if (script->instrs.size() == 0)
 		return;
 
-	_lmbInteractionState = false;
+	_scriptEnv = envVars;
 	_activeScript = script;
 	_scriptNextInstruction = 0;
 	_gameState = kGameStateScript;
@@ -986,13 +1020,21 @@ Common::SharedPtr<Script> Runtime::findScriptForInteraction(uint interactionID)
 	return nullptr;
 }
 
+void Runtime::detectPanoramaDirections() {
+	_panoramaDirectionFlags = 0;
+
+	if (_havePanAnimations)
+		_panoramaDirectionFlags |= kPanoramaHorizFlags;
+}
+
 void Runtime::onLButtonDown(int16 x, int16 y) {
 	onMouseMove(x, y);
 
 	OSEvent evt;
 	evt.type = kOSEventTypeLButtonDown;
 	evt.pos = Common::Point(x, y);
-	_pendingEvents.push_back(evt);
+
+	queueOSEvent(evt);
 }
 
 void Runtime::onLButtonUp(int16 x, int16 y) {
@@ -1001,21 +1043,24 @@ void Runtime::onLButtonUp(int16 x, int16 y) {
 	OSEvent evt;
 	evt.type = kOSEventTypeLButtonUp;
 	evt.pos = Common::Point(x, y);
-	_pendingEvents.push_back(evt);
+
+	queueOSEvent(evt);
 }
 
 void Runtime::onMouseMove(int16 x, int16 y) {
 	OSEvent evt;
 	evt.type = kOSEventTypeMouseMove;
 	evt.pos = Common::Point(x, y);
-	_pendingEvents.push_back(evt);
+
+	queueOSEvent(evt);
 }
 
 void Runtime::onKeyDown(Common::KeyCode keyCode) {
 	OSEvent evt;
 	evt.type = kOSEventTypeKeyDown;
 	evt.keyCode = keyCode;
-	_pendingEvents.push_back(evt);
+
+	queueOSEvent(evt);
 }
 
 #ifdef CHECK_STACK
@@ -1143,7 +1188,7 @@ void Runtime::scriptOpSetCursor(ScriptArg_t arg) {
 void Runtime::scriptOpSetRoom(ScriptArg_t arg) { error("Unimplemented opcode"); }
 
 void Runtime::scriptOpLMB(ScriptArg_t arg) {
-	if (!_lmbInteractionState)
+	if (!_scriptEnv.lmb)
 		terminateScript();
 }
 
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index 042e7cc9fb5..cd0d0dc1cb6 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -103,6 +103,12 @@ struct MapDef {
 	const MapScreenDirectionDef *getScreenDirection(uint screen, uint direction);
 };
 
+struct ScriptEnvironmentVars {
+	ScriptEnvironmentVars();
+
+	bool lmb;
+};
+
 class Runtime {
 public:
 	Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &rootFSNode, VCruiseGameID gameID);
@@ -162,12 +168,21 @@ private:
 		kOSEventTypeKeyDown,
 	};
 
+	enum PanoramaState {
+		kPanoramaStateInactive,
+		kPanoramaStatePanningLeft,
+		kPanoramaStatePanningRight,
+		kPanoramaStatePanningUp,
+		kPanoramaStatePanningDown,
+	};
+
 	struct OSEvent {
 		OSEvent();
 
 		OSEventType type;
 		Common::Point pos;
 		Common::KeyCode keyCode;
+		uint32 timestamp;
 	};
 
 	typedef int32 ScriptArg_t;
@@ -183,6 +198,7 @@ private:
 	void terminateScript();
 
 	bool popOSEvent(OSEvent &evt);
+	void queueOSEvent(const OSEvent &evt);
 
 	void loadIndex();
 	void changeToScreen(uint roomNumber, uint screenNumber);
@@ -196,7 +212,7 @@ private:
 
 	AnimationDef stackArgsToAnimDef(const StackValue_t *args) const;
 
-	void activateScript(const Common::SharedPtr<Script> &script, bool lmbInteractionState);
+	void activateScript(const Common::SharedPtr<Script> &script, const ScriptEnvironmentVars &envVars);
 
 	bool parseIndexDef(TextParser &parser, IndexParseType parseType, uint roomNumber, const Common::String &blamePath);
 	void allocateRoomsUpTo(uint roomNumber);
@@ -204,6 +220,7 @@ private:
 	void drawDebugOverlay();
 
 	Common::SharedPtr<Script> findScriptForInteraction(uint interactionID) const;
+	void detectPanoramaDirections();
 
 	// Script things
 	void scriptOpNumber(ScriptArg_t arg);
@@ -298,7 +315,17 @@ private:
 	Common::HashMap<uint32, int32> _variables;
 
 	static const uint kPanLeftInteraction = 1;
+	static const uint kPanDownInteraction = 2;
 	static const uint kPanRightInteraction = 3;
+	static const uint kPanUpInteraction = 4;
+
+	static const uint kPanoramaLeftFlag = 1;
+	static const uint kPanoramaRightFlag = 2;
+	static const uint kPanoramaUpFlag = 4;
+	static const uint kPanoramaDownFlag = 8;
+	static const uint kPanoramaHorizFlags = (kPanoramaLeftFlag | kPanoramaRightFlag);
+
+	uint _panoramaDirectionFlags;
 
 	uint _loadedRoomNumber;
 	uint _activeScreenNumber;
@@ -307,7 +334,6 @@ private:
 	GameState _gameState;
 
 	bool _escOn;
-	bool _lmbInteractionState;
 	bool _debugMode;
 
 	VCruiseGameID _gameID;
@@ -324,6 +350,7 @@ private:
 	Common::SharedPtr<Script> _activeScript;
 	uint _scriptNextInstruction;
 	Common::Array<StackValue_t> _scriptStack;
+	ScriptEnvironmentVars _scriptEnv;
 
 	Common::SharedPtr<AudioPlayer> _musicPlayer;
 
@@ -349,6 +376,11 @@ private:
 	RenderSection _traySection;
 
 	Common::Point _mousePos;
+	Common::Point _lmbDownPos;
+	uint32 _lmbDownTime;
+	bool _lmbDown;
+	bool _lmbDragging;
+
 	Common::Array<OSEvent> _pendingEvents;
 
 	static const uint kAnimDefStackArgs = 3;


Commit: 02df16b6afd9ccd77353f69021c22efdc925a18f
    https://github.com/scummvm/scummvm/commit/02df16b6afd9ccd77353f69021c22efdc925a18f
Author: elasota (ejlasota at gmail.com)
Date: 2023-02-21T17:16:53+01:00

Commit Message:
VCRUISE: Clean up some code quality issues

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


diff --git a/engines/vcruise/POTFILES b/engines/vcruise/POTFILES
new file mode 100644
index 00000000000..bb49b390af7
--- /dev/null
+++ b/engines/vcruise/POTFILES
@@ -0,0 +1 @@
+engines/vcruise/metaengine.cpp
diff --git a/engines/vcruise/detection.cpp b/engines/vcruise/detection.cpp
index 4780a534c39..0c0a7fae4ba 100644
--- a/engines/vcruise/detection.cpp
+++ b/engines/vcruise/detection.cpp
@@ -36,16 +36,12 @@ static const PlainGameDescriptor vCruiseGames[] = {
 
 #include "vcruise/detection_tables.h"
 
-static const char *directoryGlobs[] = {
-	nullptr
-};
-
 class VCruiseMetaEngineDetection : public AdvancedMetaEngineDetection {
 public:
 	VCruiseMetaEngineDetection() : AdvancedMetaEngineDetection(VCruise::gameDescriptions, sizeof(VCruise::VCruiseGameDescription), vCruiseGames) {
 		_guiOptions = GUIO1(GAMEOPTION_LAUNCH_DEBUG);
 		_maxScanDepth = 1;
-		_directoryGlobs = directoryGlobs;
+		_directoryGlobs = nullptr;
 		_flags = kADFlagCanPlayUnknownVariants;
 	}
 
diff --git a/engines/vcruise/detection_tables.h b/engines/vcruise/detection_tables.h
index cdcea576603..2a3929c6ba8 100644
--- a/engines/vcruise/detection_tables.h
+++ b/engines/vcruise/detection_tables.h
@@ -34,10 +34,7 @@ static const VCruiseGameDescription gameDescriptions[] = {
 		{
 			"reah",
 			"DVD",
-			{
-				{"Reah.exe", 0, "60ec19c53f1323cc7f0314f98d396283", 304128},
-				AD_LISTEND
-			},
+			AD_ENTRY1s("Reah.exe", "60ec19c53f1323cc7f0314f98d396283", 304128),
 			Common::EN_ANY,
 			Common::kPlatformWindows,
 			ADGF_UNSTABLE,
@@ -49,10 +46,7 @@ static const VCruiseGameDescription gameDescriptions[] = {
 		{
 			"reah",
 			"CD",
-			{
-				{"Reah.exe", 0, "77bc7f7819cdd443f52b193529138c87", 305664},
-				AD_LISTEND
-			},
+			AD_ENTRY1s("Reah.exe", "77bc7f7819cdd443f52b193529138c87", 305664),
 			Common::EN_ANY,
 			Common::kPlatformWindows,
 			ADGF_UNSTABLE,
@@ -64,10 +58,7 @@ static const VCruiseGameDescription gameDescriptions[] = {
 		{
 			"schizm",
 			"CD",
-			{
-				{"Schizm.exe", 0, "296edd26d951c3bdc4d303c4c88b27cd", 364544},
-				AD_LISTEND
-			},
+			AD_ENTRY1s("Schizm.exe", "296edd26d951c3bdc4d303c4c88b27cd", 364544),
 			Common::EN_ANY,
 			Common::kPlatformWindows,
 			ADGF_UNSTABLE,
@@ -78,6 +69,6 @@ static const VCruiseGameDescription gameDescriptions[] = {
 	{ AD_TABLE_END_MARKER, GID_UNKNOWN }
 };
 
-} // End of namespace MTropolis
+} // End of namespace VCruise
 
 #endif
diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 54c2caf3abc..759fcffa9df 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -262,26 +262,26 @@ bool Runtime::runWaitForAnimation() {
 	if (animEnded) {
 		_gameState = kGameStateScript;
 		return true;
-	} else {
-		// Still waiting, check events
-		OSEvent evt;
-		while (popOSEvent(evt)) {
-			if (evt.type == kOSEventTypeKeyDown && evt.keyCode == Common::KEYCODE_ESCAPE) {
-				if (_escOn) {
-					// Terminate the animation
-					if (_animDecoderState == kAnimDecoderStatePlaying) {
-						_animDecoder->pauseVideo(true);
-						_animDecoderState = kAnimDecoderStatePaused;
-					}
-					_gameState = kGameStateScript;
-					return true;
+	}
+
+	// Still waiting, check events
+	OSEvent evt;
+	while (popOSEvent(evt)) {
+		if (evt.type == kOSEventTypeKeyDown && evt.keyCode == Common::KEYCODE_ESCAPE) {
+			if (_escOn) {
+				// Terminate the animation
+				if (_animDecoderState == kAnimDecoderStatePlaying) {
+					_animDecoder->pauseVideo(true);
+					_animDecoderState = kAnimDecoderStatePaused;
 				}
+				_gameState = kGameStateScript;
+				return true;
 			}
 		}
-
-		// Yield
-		return false;
 	}
+
+	// Yield
+	return false;
 }
 
 void Runtime::continuePlayingAnimation(bool loop, bool &outAnimationEnded) {
@@ -343,7 +343,7 @@ void Runtime::continuePlayingAnimation(bool loop, bool &outAnimationEnded) {
 		}
 
 		if (!loop) {
-			if (_animDisplayingFrame >= static_cast<int>(_animLastFrame)) {
+			if (_animDisplayingFrame >= _animLastFrame) {
 				_animDecoder->pauseVideo(true);
 				_animDecoderState = kAnimDecoderStatePaused;
 
diff --git a/engines/vcruise/vcruise.cpp b/engines/vcruise/vcruise.cpp
index d6857288fe3..9f6cc537485 100644
--- a/engines/vcruise/vcruise.cpp
+++ b/engines/vcruise/vcruise.cpp
@@ -33,6 +33,8 @@ namespace VCruise {
 
 VCruiseEngine::VCruiseEngine(OSystem *syst, const VCruiseGameDescription *gameDesc) : Engine(syst), _gameDescription(gameDesc) {
 	const Common::FSNode gameDataDir(ConfMan.get("path"));
+
+	SearchMan.addDirectory(gameDataDir.getPath(), gameDataDir);
 }
 
 VCruiseEngine::~VCruiseEngine() {
@@ -84,18 +86,15 @@ Common::Error VCruiseEngine::run() {
 	Common::Point videoSize;
 	Common::Point traySize;
 	Common::Point menuBarSize;
-	const char *exeName = nullptr;
 
 	if (_gameDescription->gameID == GID_REAH) {
 		videoSize = Common::Point(608, 348);
 		menuBarSize = Common::Point(640, 44);
 		traySize = Common::Point(640, 88);
-		exeName = "Reah.exe";
 	} else if (_gameDescription->gameID == GID_SCHIZM) {
 		videoSize = Common::Point(640, 360);
 		menuBarSize = Common::Point(640, 32);
 		traySize = Common::Point(640, 88);
-		exeName = "Schizm.exe";
 	} else {
 		error("Unknown game");
 	}
@@ -130,6 +129,8 @@ Common::Error VCruiseEngine::run() {
 	_runtime.reset(new Runtime(_system, _mixer, _rootFSNode, _gameDescription->gameID));
 	_runtime->initSections(_videoRect, _menuBarRect, _trayRect, _system->getScreenFormat());
 
+	const char *exeName = _gameDescription->desc.filesDescriptions[0].fileName;
+
 	_runtime->loadCursors(exeName);
 
 	if (ConfMan.getBool("vcruise_debug")) {
@@ -159,7 +160,7 @@ void VCruiseEngine::pauseEngineIntern(bool pause) {
 bool VCruiseEngine::hasFeature(EngineFeature f) const {
 	switch (f) {
 	case kSupportsReturnToLauncher:
-	case kSupportsSavingDuringRuntime:
+	//case kSupportsSavingDuringRuntime:
 		return true;
 	default:
 		return false;
diff --git a/engines/vcruise/vcruise.h b/engines/vcruise/vcruise.h
index b5551c5e169..eb16bc7adcf 100644
--- a/engines/vcruise/vcruise.h
+++ b/engines/vcruise/vcruise.h
@@ -19,8 +19,8 @@
  *
  */
 
-#ifndef VCRUISE_VCRUISE_H
-#define VCRUISE_VCRUISE_H
+#ifndef VCRUISE_H
+#define VCRUISE_H
 
 #include "engines/engine.h"
 #include "common/rect.h"
@@ -76,4 +76,4 @@ private:
 
 } // End of namespace VCruise
 
-#endif /* VCRUISE_VCRUISE_H */
+#endif /* VCRUISE_H */


Commit: bdcf695c131302afb8727ce78925fc832569ac94
    https://github.com/scummvm/scummvm/commit/bdcf695c131302afb8727ce78925fc832569ac94
Author: elasota (ejlasota at gmail.com)
Date: 2023-02-21T17:16:53+01:00

Commit Message:
VCRUISE: Clean up file access

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 759fcffa9df..507e8e6f520 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -23,6 +23,7 @@
 #include "common/ptr.h"
 #include "common/system.h"
 #include "common/stream.h"
+#include "common/file.h"
 
 #include "graphics/cursorman.h"
 #include "graphics/font.h"
@@ -81,7 +82,7 @@ Runtime::OSEvent::OSEvent() : type(kOSEventTypeInvalid), keyCode(static_cast<Com
 
 Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &rootFSNode, VCruiseGameID gameID)
 	: _system(system), _mixer(mixer), _roomNumber(1), _screenNumber(0), _direction(0), _havePanAnimations(0), _loadedRoomNumber(0), _activeScreenNumber(0),
-	  _gameState(kGameStateBoot), _gameID(gameID), _rootFSNode(rootFSNode), _havePendingScreenChange(false), _havePendingReturnToIdleState(false), _scriptNextInstruction(0),
+	  _gameState(kGameStateBoot), _gameID(gameID), _havePendingScreenChange(false), _havePendingReturnToIdleState(false), _scriptNextInstruction(0),
 	  _escOn(false), _debugMode(false), _panoramaDirectionFlags(0),
 	  _loadedAnimation(0), _animPendingDecodeFrame(0), _animDisplayingFrame(0), _animFirstFrame(0), _animLastFrame(0), _animDecoderState(kAnimDecoderStateStopped),
 	  _animPlayWhileIdle(false), _idleIsOnInteraction(false), _idleInteractionID(0),
@@ -89,22 +90,6 @@ Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &roo
 
 	for (uint i = 0; i < kNumDirections; i++)
 		_haveIdleAnimations[i] = false;
-
-	_logDir = _rootFSNode.getChild("Log");
-	if (!_logDir.exists() || !_logDir.isDirectory())
-		error("Couldn't resolve Log directory");
-
-	_mapDir = _rootFSNode.getChild("Map");
-	if (!_mapDir.exists() || !_mapDir.isDirectory())
-		error("Couldn't resolve Map directory");
-
-	_sfxDir = _rootFSNode.getChild("Sfx");
-	if (!_sfxDir.exists() || !_sfxDir.isDirectory())
-		error("Couldn't resolve Sfx directory");
-
-	_animsDir = _rootFSNode.getChild("Anims");
-	if (!_animsDir.exists() || !_animsDir.isDirectory())
-		error("Couldn't resolve Anims directory");
 }
 
 Runtime::~Runtime() {
@@ -122,33 +107,29 @@ void Runtime::loadCursors(const char *exeName) {
 		error("Couldn't open executable file %s", exeName);
 
 	Common::Array<Common::WinResourceID> cursorGroupIDs = winRes->getIDList(Common::kWinGroupCursor);
-	for (Common::Array<Common::WinResourceID>::const_iterator it = cursorGroupIDs.begin(), itEnd = cursorGroupIDs.end(); it != itEnd; ++it) {
-		const Common::WinResourceID &id = *it;
-
-		Common::SharedPtr<Graphics::WinCursorGroup> cursorGroup(Graphics::WinCursorGroup::createCursorGroup(winRes.get(), *it));
+	for (const Common::WinResourceID &id : cursorGroupIDs) {
+		Common::SharedPtr<Graphics::WinCursorGroup> cursorGroup(Graphics::WinCursorGroup::createCursorGroup(winRes.get(), id));
 		if (!winRes) {
 			warning("Couldn't load cursor group");
 			continue;
 		}
 
 		Common::String nameStr = id.getString();
-		if (nameStr.size() == 8 && nameStr.substr(0, 7) == "CURSOR_") {
+		if (nameStr.matchString("CURSOR_#")) {
 			char c = nameStr[7];
-			if (c >= '0' && c <= '9') {
-				uint shortID = c - '0';
-				if (shortID >= _cursorsShort.size())
-					_cursorsShort.resize(shortID + 1);
-				_cursorsShort[shortID] = cursorGroup;
-			}
-		} else if (nameStr.size() == 13 && nameStr.substr(0, 11) == "CURSOR_CUR_") {
+
+			uint shortID = c - '0';
+			if (shortID >= _cursorsShort.size())
+				_cursorsShort.resize(shortID + 1);
+			_cursorsShort[shortID] = cursorGroup;
+		} else if (nameStr.matchString("CURSOR_CUR_##")) {
 			char c1 = nameStr[11];
 			char c2 = nameStr[12];
-			if (c1 >= '0' && c1 <= '9' && c2 >= '0' && c2 <= '9') {
-				uint longID = (c1 - '0') * 10 + (c2 - '0');
-				if (longID >= _cursors.size())
-					_cursors.resize(longID + 1);
-				_cursors[longID] = cursorGroup;
-			}
+
+			uint longID = (c1 - '0') * 10 + (c2 - '0');
+			if (longID >= _cursors.size())
+				_cursors.resize(longID + 1);
+			_cursors[longID] = cursorGroup;
 		}
 	}
 
@@ -532,15 +513,15 @@ void Runtime::queueOSEvent(const OSEvent &evt) {
 }
 
 void Runtime::loadIndex() {
-	Common::FSNode indexFSNode = _logDir.getChild("Index.txt");
+	const char *indexPath = "Log/Index.txt";
 
-	Common::ReadStream *stream = indexFSNode.createReadStream();
-	if (!stream)
+	Common::File stream;
+	if (!stream.open(indexPath))
 		error("Failed to open main index");
 
-	Common::String blamePath = indexFSNode.getPath();
+	Common::String blamePath = indexPath;
 
-	TextParser parser(stream);
+	TextParser parser(&stream);
 
 	Common::String token;
 	TextParserState state;
@@ -622,24 +603,21 @@ void Runtime::changeToScreen(uint roomNumber, uint screenNumber) {
 
 		_scriptSet.reset();
 
-		Common::String logFileName = Common::String::format("Room%02i.log", static_cast<int>(roomNumber));
-		Common::FSNode logFileNode = _logDir.getChild(logFileName);
-		if (logFileNode.exists()) {
-			if (Common::SeekableReadStream *logicFile = logFileNode.createReadStream()) {
-				_scriptSet = compileLogicFile(*logicFile, static_cast<uint>(logicFile->size()), logFileNode.getPath());
-				delete logicFile;
-			}
+		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();
 
-		Common::String mapFileName = Common::String::format("Room%02i.map", static_cast<int>(roomNumber));
-		Common::FSNode mapFileNode = _mapDir.getChild(mapFileName);
-		if (mapFileNode.exists()) {
-			if (Common::SeekableReadStream *mapFile = mapFileNode.createReadStream()) {
-				loadMap(mapFile);
-				delete mapFile;
-			}
+		Common::String mapFileName = Common::String::format("Map/Room%02i.map", static_cast<int>(roomNumber));
+		Common::File mapFile;
+
+		if (mapFile.open(mapFileName)) {
+			loadMap(&mapFile);
+			mapFile.close();
 		}
 	}
 
@@ -772,16 +750,17 @@ void Runtime::loadMap(Common::SeekableReadStream *stream) {
 void Runtime::changeMusicTrack(int track) {
 	_musicPlayer.reset();
 
-	Common::String wavFileName = Common::String::format("Music-%02i.wav", static_cast<int>(track));
-	Common::FSNode wavFileNode = _sfxDir.getChild(wavFileName);
-	if (wavFileNode.exists()) {
-		if (Common::SeekableReadStream *wavFile = wavFileNode.createReadStream()) {
-			if (Audio::SeekableAudioStream *audioStream = Audio::makeWAVStream(wavFile, DisposeAfterUse::YES)) {
-				Common::SharedPtr<Audio::AudioStream> loopingStream(Audio::makeLoopingAudioStream(audioStream, 0));
+	Common::String wavFileName = Common::String::format("Sfx/Music-%02i.wav", static_cast<int>(track));
+	Common::File *wavFile = new Common::File();
+	if (wavFile->open(wavFileName)) {
+		if (Audio::SeekableAudioStream *audioStream = Audio::makeWAVStream(wavFile, DisposeAfterUse::YES)) {
+			Common::SharedPtr<Audio::AudioStream> loopingStream(Audio::makeLoopingAudioStream(audioStream, 0));
 
-				_musicPlayer.reset(new AudioPlayer(_mixer, loopingStream, 255, 0));
-			}
+			_musicPlayer.reset(new AudioPlayer(_mixer, loopingStream, 255, 0));
 		}
+	} else {
+		warning("Music file '%s' is missing", wavFileName.c_str());
+		delete wavFile;
 	}
 }
 
@@ -795,14 +774,10 @@ void Runtime::changeAnimation(const AnimationDef &animDef) {
 		_animDecoder.reset();
 		_animDecoderState = kAnimDecoderStateStopped;
 
-		Common::String aviFileName = Common::String::format("Anim%04i.avi", animFile);
-		Common::FSNode aviFileNode = _animsDir.getChild(aviFileName);
-		if (!aviFileNode.exists()) {
-			warning("Animation file %i is missing", animFile);
-			return;
-		}
+		Common::String aviFileName = Common::String::format("Anims/Anim%04i.avi", animFile);
+		Common::File *aviFile = new Common::File();
 
-		if (Common::SeekableReadStream *aviFile = aviFileNode.createReadStream()) {
+		if (aviFile->open(aviFileName)) {
 			_animDecoder.reset(new Video::AVIDecoder());
 			if (!_animDecoder->loadStream(aviFile)) {
 				warning("Animation file %i could not be loaded", animFile);
@@ -810,7 +785,7 @@ void Runtime::changeAnimation(const AnimationDef &animDef) {
 			}
 		} else {
 			warning("Animation file %i is missing", animFile);
-			return;
+			delete aviFile;
 		}
 	}
 
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index cd0d0dc1cb6..70673868f3b 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -338,12 +338,6 @@ private:
 
 	VCruiseGameID _gameID;
 
-	Common::FSNode _rootFSNode;
-	Common::FSNode _logDir;
-	Common::FSNode _mapDir;
-	Common::FSNode _sfxDir;
-	Common::FSNode _animsDir;
-
 	Common::Array<Common::SharedPtr<RoomDef> > _roomDefs;
 	Common::SharedPtr<ScriptSet> _scriptSet;
 
diff --git a/engines/vcruise/vcruise.cpp b/engines/vcruise/vcruise.cpp
index 9f6cc537485..117885e9d88 100644
--- a/engines/vcruise/vcruise.cpp
+++ b/engines/vcruise/vcruise.cpp
@@ -34,7 +34,7 @@ namespace VCruise {
 VCruiseEngine::VCruiseEngine(OSystem *syst, const VCruiseGameDescription *gameDesc) : Engine(syst), _gameDescription(gameDesc) {
 	const Common::FSNode gameDataDir(ConfMan.get("path"));
 
-	SearchMan.addDirectory(gameDataDir.getPath(), gameDataDir);
+	SearchMan.addDirectory(gameDataDir.getPath(), gameDataDir, 0, 2);
 }
 
 VCruiseEngine::~VCruiseEngine() {


Commit: 42188df4cc1e6782b9852a1c0a5fbc9ce6972f37
    https://github.com/scummvm/scummvm/commit/42188df4cc1e6782b9852a1c0a5fbc9ce6972f37
Author: elasota (ejlasota at gmail.com)
Date: 2023-02-21T17:16:53+01:00

Commit Message:
VCRUISE: Stub unsupported opcodes

Changed paths:
    engines/vcruise/runtime.cpp


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 507e8e6f520..0674c9230bd 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -1038,8 +1038,16 @@ void Runtime::onKeyDown(Common::KeyCode keyCode) {
 	queueOSEvent(evt);
 }
 
-#ifdef CHECK_STACK
-#error "CHECK_STACK is already defined"
+#ifdef PEEK_STACK
+#error "PEEK_STACK is already defined"
+#endif
+
+#ifdef TAKE_STACK
+#error "TAKE_STACK is already defined"
+#endif
+
+#ifdef OPCODE_STUB
+#error "OPCODE_STUB is already defined"
 #endif
 
 #define PEEK_STACK(n)                                                                         \
@@ -1064,6 +1072,11 @@ void Runtime::onKeyDown(Common::KeyCode keyCode) {
 		this->_scriptStack.resize(stackSize - (n));                                           \
 	} while (false)
 
+#define OPCODE_STUB(op)                           \
+	void Runtime::scriptOp##op(ScriptArg_t arg) { \
+		error("Unimplemented opcode '" #op "'");  \
+	}
+
 void Runtime::scriptOpNumber(ScriptArg_t arg) {
 	_scriptStack.push_back(arg);
 }
@@ -1076,9 +1089,9 @@ void Runtime::scriptOpRotate(ScriptArg_t arg) {
 	_havePanAnimations = true;
 }
 
-void Runtime::scriptOpAngle(ScriptArg_t arg) { error("Unimplemented opcode"); }
-void Runtime::scriptOpAngleGGet(ScriptArg_t arg) { error("Unimplemented opcode"); }
-void Runtime::scriptOpSpeed(ScriptArg_t arg) { error("Unimplemented opcode"); }
+OPCODE_STUB(Angle)
+OPCODE_STUB(AngleGGet)
+OPCODE_STUB(Speed)
 
 void Runtime::scriptOpSAnimL(ScriptArg_t arg) {
 	TAKE_STACK(kAnimDefStackArgs + 2);
@@ -1096,12 +1109,12 @@ void Runtime::scriptOpSAnimL(ScriptArg_t arg) {
 	_idleAnimations[direction] = animDef;
 }
 
-void Runtime::scriptOpChangeL(ScriptArg_t arg) { error("Unimplemented opcode"); }
+OPCODE_STUB(ChangeL)
 
-void Runtime::scriptOpAnimR(ScriptArg_t arg) { error("Unimplemented opcode"); }
-void Runtime::scriptOpAnimF(ScriptArg_t arg) { error("Unimplemented opcode"); }
-void Runtime::scriptOpAnimN(ScriptArg_t arg) { error("Unimplemented opcode"); }
-void Runtime::scriptOpAnimG(ScriptArg_t arg) { error("Unimplemented opcode"); }
+OPCODE_STUB(AnimR)
+OPCODE_STUB(AnimF)
+OPCODE_STUB(AnimN)
+OPCODE_STUB(AnimG)
 
 void Runtime::scriptOpAnimS(ScriptArg_t arg) {
 	TAKE_STACK(kAnimDefStackArgs + 2);
@@ -1129,7 +1142,7 @@ void Runtime::scriptOpAnim(ScriptArg_t arg) {
 	_havePendingScreenChange = true;
 }
 
-void Runtime::scriptOpStatic(ScriptArg_t arg) { error("Unimplemented opcode"); }
+OPCODE_STUB(Static)
 
 void Runtime::scriptOpVarLoad(ScriptArg_t arg) {
 	TAKE_STACK(1);
@@ -1160,15 +1173,15 @@ void Runtime::scriptOpSetCursor(ScriptArg_t arg) {
 	changeToCursor(_cursors[stackArgs[0]]);
 }
 
-void Runtime::scriptOpSetRoom(ScriptArg_t arg) { error("Unimplemented opcode"); }
+OPCODE_STUB(SetRoom)
 
 void Runtime::scriptOpLMB(ScriptArg_t arg) {
 	if (!_scriptEnv.lmb)
 		terminateScript();
 }
 
-void Runtime::scriptOpLMB1(ScriptArg_t arg) { error("Unimplemented opcode"); }
-void Runtime::scriptOpSoundS1(ScriptArg_t arg) { error("Unimplemented opcode"); }
+OPCODE_STUB(LMB1)
+OPCODE_STUB(SoundS1)
 
 void Runtime::scriptOpSoundL2(ScriptArg_t arg) {
 	TAKE_STACK(2);
@@ -1186,25 +1199,26 @@ void Runtime::scriptOpMusic(ScriptArg_t arg) {
 void Runtime::scriptOpMusicUp(ScriptArg_t arg) {
 	TAKE_STACK(2);
 
-	warning("Music volume changes are not implemented");
+	warning("Music volume ramp up is not implemented");
 	(void)stackArgs;
 }
 
 void Runtime::scriptOpMusicDn(ScriptArg_t arg) {
 	TAKE_STACK(2);
 
-	warning("Music volume changes are not implemented");
+	warning("Music volume ramp down is not implemented");
 	(void)stackArgs;
 }
 
-void Runtime::scriptOpParm1(ScriptArg_t arg) { error("Unimplemented opcode"); }
-void Runtime::scriptOpParm2(ScriptArg_t arg) { error("Unimplemented opcode"); }
-void Runtime::scriptOpParm3(ScriptArg_t arg) { error("Unimplemented opcode"); }
-void Runtime::scriptOpParmG(ScriptArg_t arg) { error("Unimplemented opcode"); }
 
-void Runtime::scriptOpVolumeDn4(ScriptArg_t arg) { error("Unimplemented opcode"); }
-void Runtime::scriptOpVolumeUp3(ScriptArg_t arg) { error("Unimplemented opcode"); }
-void Runtime::scriptOpRandom(ScriptArg_t arg) { error("Unimplemented opcode"); }
+OPCODE_STUB(Parm1)
+OPCODE_STUB(Parm2)
+OPCODE_STUB(Parm3)
+OPCODE_STUB(ParmG)
+
+OPCODE_STUB(VolumeDn4)
+OPCODE_STUB(VolumeUp3)
+OPCODE_STUB(Random)
 
 void Runtime::scriptOpDrop(ScriptArg_t arg) {
 	TAKE_STACK(1);
@@ -1218,12 +1232,12 @@ void Runtime::scriptOpDup(ScriptArg_t arg) {
 	_scriptStack.push_back(stackArgs[0]);
 }
 
-void Runtime::scriptOpSay3(ScriptArg_t arg) { error("Unimplemented opcode"); }
-void Runtime::scriptOpSetTimer(ScriptArg_t arg) { error("Unimplemented opcode"); }
-void Runtime::scriptOpLoSet(ScriptArg_t arg) { error("Unimplemented opcode"); }
-void Runtime::scriptOpLoGet(ScriptArg_t arg) { error("Unimplemented opcode"); }
-void Runtime::scriptOpHiSet(ScriptArg_t arg) { error("Unimplemented opcode"); }
-void Runtime::scriptOpHiGet(ScriptArg_t arg) { error("Unimplemented opcode"); }
+OPCODE_STUB(Say3)
+OPCODE_STUB(SetTimer)
+OPCODE_STUB(LoSet)
+OPCODE_STUB(LoGet)
+OPCODE_STUB(HiSet)
+OPCODE_STUB(HiGet)
 
 void Runtime::scriptOpNot(ScriptArg_t arg) {
 	TAKE_STACK(1);
@@ -1249,9 +1263,9 @@ void Runtime::scriptOpCmpEq(ScriptArg_t arg) {
 	_scriptStack.push_back((stackArgs[0] == stackArgs[1]) ? 1 : 0);
 }
 
-void Runtime::scriptOpBitLoad(ScriptArg_t arg) { error("Unimplemented opcode"); }
-void Runtime::scriptOpBitSet0(ScriptArg_t arg) { error("Unimplemented opcode"); }
-void Runtime::scriptOpBitSet1(ScriptArg_t arg) { error("Unimplemented opcode"); }
+OPCODE_STUB(BitLoad)
+OPCODE_STUB(BitSet0)
+OPCODE_STUB(BitSet1)
 
 void Runtime::scriptOpDisc1(ScriptArg_t arg) {
 	// Disc check, always pass
@@ -1284,8 +1298,8 @@ void Runtime::scriptOpEscOff(ScriptArg_t arg) {
 	_escOn = false;
 }
 
-void Runtime::scriptOpEscGet(ScriptArg_t arg) { error("Unimplemented opcode"); }
-void Runtime::scriptOpBackStart(ScriptArg_t arg) { error("Unimplemented opcode"); }
+OPCODE_STUB(EscGet)
+OPCODE_STUB(BackStart)
 
 void Runtime::scriptOpAnimName(ScriptArg_t arg) {
 	// I doubt this is actually how it works internally but whatever
@@ -1307,7 +1321,7 @@ void Runtime::scriptOpAnimName(ScriptArg_t arg) {
 }
 
 
-void Runtime::scriptOpValueName(ScriptArg_t arg) { error("Unimplemented opcode"); }
+OPCODE_STUB(ValueName)
 
 void Runtime::scriptOpVarName(ScriptArg_t arg) {
 	if (_roomNumber >= _roomDefs.size())
@@ -1363,6 +1377,11 @@ void Runtime::scriptOpJump(ScriptArg_t arg) {
 	_scriptNextInstruction = arg;
 }
 
+#undef TAKE_STACK
+#undef PEEK_STACK
+#undef OPCODE_STUB
+
+
 void Runtime::drawFrame() {
 	_system->updateScreen();
 }


Commit: f64666d7827448b43ecdd79d7134a0479c465c1e
    https://github.com/scummvm/scummvm/commit/f64666d7827448b43ecdd79d7134a0479c465c1e
Author: elasota (ejlasota at gmail.com)
Date: 2023-02-21T17:16:53+01:00

Commit Message:
VCRUISE: Clean up hash map types

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


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 0674c9230bd..8b9ac6fbf2b 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -623,10 +623,10 @@ void Runtime::changeToScreen(uint roomNumber, uint screenNumber) {
 
 	if (changedScreen) {
 		if (_scriptSet) {
-			Common::HashMap<uint, Common::SharedPtr<RoomScriptSet> >::const_iterator roomScriptIt = _scriptSet->roomScripts.find(_roomNumber);
+			RoomScriptSetMap_t::const_iterator roomScriptIt = _scriptSet->roomScripts.find(_roomNumber);
 			if (roomScriptIt != _scriptSet->roomScripts.end()) {
-				Common::HashMap<uint, Common::SharedPtr<ScreenScriptSet> > &screenScriptsMap = roomScriptIt->_value->screenScripts;
-				Common::HashMap<uint, Common::SharedPtr<ScreenScriptSet> >::const_iterator screenScriptIt = screenScriptsMap.find(_screenNumber);
+				const ScreenScriptSetMap_t &screenScriptsMap = roomScriptIt->_value->screenScripts;
+				ScreenScriptSetMap_t::const_iterator screenScriptIt = screenScriptsMap.find(_screenNumber);
 				if (screenScriptIt != screenScriptsMap.end()) {
 					const Common::SharedPtr<Script> &script = screenScriptIt->_value->entryScript;
 					if (script)
@@ -976,16 +976,16 @@ void Runtime::drawDebugOverlay() {
 
 Common::SharedPtr<Script> Runtime::findScriptForInteraction(uint interactionID) const {
 	if (_scriptSet) {
-		Common::HashMap<uint, Common::SharedPtr<RoomScriptSet> >::const_iterator roomScriptIt = _scriptSet->roomScripts.find(_roomNumber);
+		RoomScriptSetMap_t::const_iterator roomScriptIt = _scriptSet->roomScripts.find(_roomNumber);
 
 		if (roomScriptIt != _scriptSet->roomScripts.end()) {
 			const RoomScriptSet &roomScriptSet = *roomScriptIt->_value;
 
-			Common::HashMap<uint, Common::SharedPtr<ScreenScriptSet> >::const_iterator screenScriptIt = roomScriptSet.screenScripts.find(_screenNumber);
+			ScreenScriptSetMap_t::const_iterator screenScriptIt = roomScriptSet.screenScripts.find(_screenNumber);
 			if (screenScriptIt != roomScriptSet.screenScripts.end()) {
 				const ScreenScriptSet &screenScriptSet = *screenScriptIt->_value;
 
-				Common::HashMap<uint, Common::SharedPtr<Script> >::const_iterator interactionScriptIt = screenScriptSet.interactionScripts.find(interactionID);
+				ScriptMap_t::const_iterator interactionScriptIt = screenScriptSet.interactionScripts.find(interactionID);
 				if (interactionScriptIt != screenScriptSet.interactionScripts.end())
 					return interactionScriptIt->_value;
 			}
diff --git a/engines/vcruise/script.h b/engines/vcruise/script.h
index b8e86f780f8..ddf6cc84f46 100644
--- a/engines/vcruise/script.h
+++ b/engines/vcruise/script.h
@@ -35,6 +35,10 @@ class ReadStream;
 
 namespace VCruise {
 
+struct ScreenScriptSet;
+struct RoomScriptSet;
+struct ScriptSet;
+
 namespace ScriptOps {
 
 enum ScriptOp {
@@ -127,17 +131,21 @@ struct Script {
 	Common::Array<Instruction> instrs;
 };
 
+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;
+
 struct ScreenScriptSet {
 	Common::SharedPtr<Script> entryScript;
-	Common::HashMap<uint, Common::SharedPtr<Script> > interactionScripts;
+	ScriptMap_t interactionScripts;
 };
 
 struct RoomScriptSet {
-	Common::HashMap<uint, Common::SharedPtr<ScreenScriptSet> > screenScripts;
+	ScreenScriptSetMap_t screenScripts;
 };
 
 struct ScriptSet {
-	Common::HashMap<uint, Common::SharedPtr<RoomScriptSet> > roomScripts;
+	RoomScriptSetMap_t roomScripts;
 	Common::Array<Common::String> strings;
 };
 


Commit: 31a1fcdfddbba6c9601bde2a8909a06111201922
    https://github.com/scummvm/scummvm/commit/31a1fcdfddbba6c9601bde2a8909a06111201922
Author: elasota (ejlasota at gmail.com)
Date: 2023-02-21T17:16:53+01:00

Commit Message:
VCRUISE: Remove input buffering from TextParser

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


diff --git a/engines/vcruise/textparser.cpp b/engines/vcruise/textparser.cpp
index 116b06dbee2..9cbdec32a1f 100644
--- a/engines/vcruise/textparser.cpp
+++ b/engines/vcruise/textparser.cpp
@@ -29,30 +29,23 @@ namespace VCruise {
 TextParserState::TextParserState() : _lineNum(1), _col(1), _prevWasCR(false), _isParsingComment(false) {
 }
 
-TextParser::TextParser(Common::ReadStream *stream) : _stream(stream), _readBufferPos(0), _readBufferEnd(0), _returnBufferPos(kReturnBufferSize) {
-	memset(_readBuffer, 0, kReadBufferSize);
-	memset(_returnedBuffer, 0, kReturnBufferSize);
+TextParser::TextParser(Common::ReadStream *stream) : _stream(stream), _returnedBufferPos(kReturnedBufferSize) {
+	memset(_returnedBuffer, 0, kReturnedBufferSize);
 }
 
 bool TextParser::readOneChar(char &outC, TextParserState &outState) {
-	if (_returnBufferPos == kReturnBufferSize) {
-		if (_readBufferPos == _readBufferEnd) {
-			if (_stream->eos())
-				return false;
-
-			_readBufferPos = 0;
-			_readBufferEnd = _stream->read(_readBuffer, kReadBufferSize);
-			if (_readBufferEnd == 0)
-				return false;
-		}
+	if (_returnedBufferPos == kReturnedBufferSize) {
+		if (_stream->eos())
+			return false;
 	}
 
 	char c = 0;
 
-	if (_returnBufferPos != kReturnBufferSize) {
-		c = _returnedBuffer[_returnBufferPos++];
+	if (_returnedBufferPos != kReturnedBufferSize) {
+		c = _returnedBuffer[_returnedBufferPos++];
 	} else {
-		c = _readBuffer[_readBufferPos++];
+		if (!_stream->read(&c, 1))
+			return false;
 	}
 
 	TextParserState prevState = _state;
@@ -115,9 +108,9 @@ bool TextParser::isWhitespace(char c) {
 
 void TextParser::requeue(const char *chars, uint numChars, const TextParserState &state) {
 	_state = state;
-	assert(_returnBufferPos >= numChars);
-	_returnBufferPos -= numChars;
-	memcpy(_returnedBuffer + _returnBufferPos, chars, numChars);
+	assert(_returnedBufferPos >= numChars);
+	_returnedBufferPos -= numChars;
+	memcpy(_returnedBuffer + _returnedBufferPos, chars, numChars);
 }
 
 void TextParser::requeue(const Common::String &str, const TextParserState &state) {
diff --git a/engines/vcruise/textparser.h b/engines/vcruise/textparser.h
index 0c3d92f6269..7bbe73eb115 100644
--- a/engines/vcruise/textparser.h
+++ b/engines/vcruise/textparser.h
@@ -75,15 +75,10 @@ private:
 
 	Common::ReadStream *_stream;
 
-	static const uint kReadBufferSize = 4096;
-	static const uint kReturnBufferSize = 8;
+	static const uint kReturnedBufferSize = 8;
 
-	char _returnedBuffer[kReturnBufferSize];
-	uint _returnBufferPos;
-
-	char _readBuffer[kReadBufferSize];
-	uint _readBufferPos;
-	uint _readBufferEnd;
+	char _returnedBuffer[kReturnedBufferSize];
+	uint _returnedBufferPos;
 };
 
 } // End of namespace VCruise


Commit: 94de968496ed5ebcf7032d6461f1d963f4d4e93a
    https://github.com/scummvm/scummvm/commit/94de968496ed5ebcf7032d6461f1d963f4d4e93a
Author: elasota (ejlasota at gmail.com)
Date: 2023-02-21T17:16:53+01:00

Commit Message:
VCRUISE: Use INI loader for index

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


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 8b9ac6fbf2b..e64e8e3bdbe 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -515,16 +515,12 @@ void Runtime::queueOSEvent(const OSEvent &evt) {
 void Runtime::loadIndex() {
 	const char *indexPath = "Log/Index.txt";
 
-	Common::File stream;
-	if (!stream.open(indexPath))
-		error("Failed to open main index");
+	Common::INIFile iniFile;
+	iniFile.allowNonEnglishCharacters();
+	if (!iniFile.loadFromFile(indexPath))
+		error("Failed to load main logic index");
 
-	Common::String blamePath = indexPath;
-
-	TextParser parser(&stream);
-
-	Common::String token;
-	TextParserState state;
+	IndexParseType indexParseType = kIndexParseTypeNone;
 
 	static const IndexPrefixTypePair parsePrefixes[] = {
 		{"Room", kIndexParseTypeRoom},
@@ -536,52 +532,35 @@ void Runtime::loadIndex() {
 		{"SRoom", kIndexParseTypeSRoom},
 	};
 
-	IndexParseType indexParseType = kIndexParseTypeNone;
-	uint currentRoomNumber = 0;
-
-	for (;;) {
-		char firstCh = 0;
-		if (!parser.skipWhitespaceAndComments(firstCh, state))
-			break;
+	for (const Common::INIFile::Section &section : iniFile.getSections()) {
+		uint roomNumber = 0;
 
-		if (firstCh == '[') {
-			if (!parser.parseToken(token, state))
-				error("Index open bracket wasn't terminated");
-
-			if (token == "NameRoom") {
-				indexParseType = kIndexParseTypeNameRoom;
-			} else {
-				bool foundType = false;
-				uint prefixLen = 0;
-				for (const IndexPrefixTypePair &prefixTypePair : parsePrefixes) {
-					uint len = strlen(prefixTypePair.prefix);
-					if (token.size() > len && !memcmp(token.c_str(), prefixTypePair.prefix, len)) {
-						indexParseType = prefixTypePair.parseType;
-						foundType = true;
-						prefixLen = len;
-						break;
-					}
+		if (section.name == "NameRoom") {
+			indexParseType = kIndexParseTypeNameRoom;
+		} else {
+			bool foundType = false;
+			uint prefixLen = 0;
+			for (const IndexPrefixTypePair &prefixTypePair : parsePrefixes) {
+				if (section.name.hasPrefix(prefixTypePair.prefix)) {
+					indexParseType = prefixTypePair.parseType;
+					foundType = true;
+					prefixLen = strlen(prefixTypePair.prefix);
+					break;
 				}
+			}
 
-				if (!foundType)
-					error("Unknown index heading type %s", token.c_str());
+			if (!foundType)
+				error("Unknown index heading type %s", section.name.c_str());
 
-				currentRoomNumber = 0;
-				for (uint i = prefixLen; i < token.size(); i++) {
-					char digit = token[i];
-					if (digit < '0' || digit > '9')
-						error("Malformed room def");
-					currentRoomNumber = currentRoomNumber * 10 + (token[i] - '0');
-				}
-			}
 
-			parser.expect("]", blamePath);
+			if (!sscanf(section.name.c_str() + prefixLen, "%u", &roomNumber))
+				error("Malformed room def '%s'", section.name.c_str());
 
-			allocateRoomsUpTo(currentRoomNumber);
-		} else {
-			parser.requeue(&firstCh, 1, state);
+			allocateRoomsUpTo(roomNumber);
+		}
 
-			if (!parseIndexDef(parser, indexParseType, currentRoomNumber, blamePath))
+		for (const Common::INIFile::KeyValue &keyValue : section.getKeys()) {
+			if (!parseIndexDef(indexParseType, roomNumber, keyValue.key, keyValue.value))
 				break;
 		}
 	}
@@ -819,112 +798,85 @@ void Runtime::activateScript(const Common::SharedPtr<Script> &script, const Scri
 	_gameState = kGameStateScript;
 }
 
-bool Runtime::parseIndexDef(TextParser &parser, IndexParseType parseType, uint roomNumber, const Common::String &blamePath) {
-	Common::String lineText;
-	parser.expectLine(lineText, blamePath, true);
-
-	Common::MemoryReadStream lineStream(reinterpret_cast<const byte *>(lineText.c_str()), lineText.size(), DisposeAfterUse::NO);
-	TextParser strParser(&lineStream);
-
+bool Runtime::parseIndexDef(IndexParseType parseType, uint roomNumber, const Common::String &key, const Common::String &value) {
 	switch (parseType) {
 	case kIndexParseTypeNameRoom: {
-			uint nameRoomNumber = 0;
-			Common::String name;
-			strParser.expectToken(name, blamePath);
-			strParser.expect("=", blamePath);
-			strParser.expectUInt(nameRoomNumber, blamePath);
-
-			allocateRoomsUpTo(nameRoomNumber);
-			_roomDefs[nameRoomNumber]->name = name;
-		} break;
+		uint nameRoomNumber = 0;
+
+		if (!sscanf(value.c_str(), "%u", &nameRoomNumber))
+			error("Malformed NameRoom def '%s'", value.c_str());
+
+		allocateRoomsUpTo(nameRoomNumber);
+		_roomDefs[nameRoomNumber]->name = key;
+	} break;
 	case kIndexParseTypeRoom: {
-			Common::String name;
-
-			AnimationDef animDef;
-
-			strParser.expectToken(name, blamePath);
-			strParser.expect("=", blamePath);
-			strParser.expectInt(animDef.animNum, blamePath);
-			strParser.expect(",", blamePath);
-			strParser.expectUInt(animDef.firstFrame, blamePath);
-			strParser.expect(",", blamePath);
-			strParser.expectUInt(animDef.lastFrame, blamePath);
-			_roomDefs[roomNumber]->animations[name] = animDef;
-		} break;
+		int animNum = 0;
+		uint firstFrame = 0;
+		uint lastFrame = 0;
+		if (sscanf(value.c_str(), "%i, %u, %u", &animNum, &firstFrame, &lastFrame) != 3)
+			error("Malformed room animation def '%s'", value.c_str());
+
+		
+		AnimationDef animDef;
+		animDef.animNum = animNum;
+		animDef.firstFrame = firstFrame;
+		animDef.lastFrame = lastFrame;
+
+		_roomDefs[roomNumber]->animations[key] = animDef;
+	} break;
 	case kIndexParseTypeRRoom: {
-			Common::String name;
+		Common::String name;
 
-			Common::Rect rect;
 
-			strParser.expectToken(name, blamePath);
-			strParser.expect("=", blamePath);
-			strParser.expectShort(rect.left, blamePath);
-			strParser.expect(",", blamePath);
-			strParser.expectShort(rect.top, blamePath);
-			strParser.expect(",", blamePath);
-			strParser.expectShort(rect.right, blamePath);
+		int left = 0;
+		int top = 0;
+		int width = 0;
+		int height = 0;
 
-			// Line 4210 in Reah contains an animation def instead of a rect def, detect this and discard
-			if (!strParser.checkEOL()) {
-				strParser.expect(",", blamePath);
-				strParser.expectShort(rect.bottom, blamePath);
+		int numValuesRead = sscanf(value.c_str(), "%i, %i, %i, %i", &left, &top, &width, &height);
 
-				_roomDefs[roomNumber]->rects[name] = rect;
-			}
-
-		} break;
+		if (numValuesRead == 4) {
+			_roomDefs[roomNumber]->rects[key] = Common::Rect(left, top, left + width, top + height);
+		} else {
+			// Line 4210 in Reah contains an animation def instead of a rect def, so we need to tolerate invalid values here
+			warning("Invalid rect def in logic index '%s'", value.c_str());
+		}
+	} break;
 	case kIndexParseTypeYRoom: {
-			Common::String name;
+		uint varSlot = 0;
 
-			uint varSlot = 0;
-
-			strParser.expectToken(name, blamePath);
-			strParser.expect("=", blamePath);
-			strParser.expectUInt(varSlot, blamePath);
-
-			_roomDefs[roomNumber]->vars[name] = varSlot;
-		} break;
+		if (!sscanf(value.c_str(), "%u", &varSlot))
+			error("Malformed var def '%s'", value.c_str());
+			
+		_roomDefs[roomNumber]->vars[key] = varSlot;
+	} break;
 	case kIndexParseTypeVRoom: {
-			Common::String name;
+		Common::String name;
 
-			int value = 0;
+		int val = 0;
 
-			strParser.expectToken(name, blamePath);
-			strParser.expect("=", blamePath);
-			strParser.expectInt(value, blamePath);
+		if (!sscanf(value.c_str(), "%i", &val))
+			error("Malformed value def '%s'", value.c_str());
 
-			_roomDefs[roomNumber]->values[name] = value;
-		} break;
+		_roomDefs[roomNumber]->values[key] = val;
+	} break;
 	case kIndexParseTypeTRoom: {
-			Common::String name;
-			Common::String value;
-
-			strParser.expectToken(name, blamePath);
-			strParser.expect("=", blamePath);
-			strParser.expectLine(value, blamePath, false);
-
-			_roomDefs[roomNumber]->texts[name] = value;
-		} break;
+		_roomDefs[roomNumber]->texts[key] = value;
+	} break;
 	case kIndexParseTypeCRoom: {
-			Common::String name;
-			int value;
-
-			strParser.expectToken(name, blamePath);
-			strParser.expect("=", blamePath);
-			strParser.expectInt(value, blamePath);
-
-			_roomDefs[roomNumber]->consts[name] = value;
-		} break;
+		// This is only used for one entry ("PrzedDrzwiamiDoZsypu" = "In front of the door to the chute") in Reah
+		// and doesn't seem to be referenced in any scripts or anything else.  Discard it.
+	} break;
 	case kIndexParseTypeSRoom: {
-			Common::String name;
-			int value;
+		Common::String name;
+
+		int soundID = 0;
 
-			strParser.expectToken(name, blamePath);
-			strParser.expect("=", blamePath);
-			strParser.expectInt(value, blamePath);
+		if (!sscanf(value.c_str(), "%i", &soundID))
+			error("Malformed sound def '%s'", value.c_str());
 
-			_roomDefs[roomNumber]->sounds[name] = value;
-		} break;
+		_roomDefs[roomNumber]->values[key] = soundID;
+	} break;
 	default:
 		assert(false);
 		return false;
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index 70673868f3b..fb48aea1cc8 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -75,7 +75,6 @@ struct RoomDef {
 	Common::HashMap<Common::String, uint> vars;
 	Common::HashMap<Common::String, int> values;
 	Common::HashMap<Common::String, Common::String> texts;
-	Common::HashMap<Common::String, int> consts;
 	Common::HashMap<Common::String, int> sounds;
 
 	Common::String name;
@@ -214,7 +213,7 @@ private:
 
 	void activateScript(const Common::SharedPtr<Script> &script, const ScriptEnvironmentVars &envVars);
 
-	bool parseIndexDef(TextParser &parser, IndexParseType parseType, uint roomNumber, const Common::String &blamePath);
+	bool parseIndexDef(IndexParseType parseType, uint roomNumber, const Common::String &key, const Common::String &value);
 	void allocateRoomsUpTo(uint roomNumber);
 
 	void drawDebugOverlay();


Commit: 90354659ab39923279b311dc3cbe87897d018669
    https://github.com/scummvm/scummvm/commit/90354659ab39923279b311dc3cbe87897d018669
Author: elasota (ejlasota at gmail.com)
Date: 2023-02-21T17:16:53+01:00

Commit Message:
VCRUISE: Use sscanf for number parsing

Changed paths:
    engines/vcruise/script.cpp


diff --git a/engines/vcruise/script.cpp b/engines/vcruise/script.cpp
index 6d104aead69..d31eab21756 100644
--- a/engines/vcruise/script.cpp
+++ b/engines/vcruise/script.cpp
@@ -202,17 +202,9 @@ bool ScriptCompiler::parseDecNumber(const Common::String &token, uint start, uin
 	if (start == token.size())
 		return false;
 
-	uint32 num = 0;
-	for (uint i = start; i < token.size(); i++) {
-		char c = token[i];
-		uint32 digit = 0;
-		if (c >= '0' && c <= '9')
-			digit = c - '0';
-		else
-			return false;
-
-		num = num * 10u + digit;
-	}
+	uint num = 0;
+	if (!sscanf(token.c_str() + start, "%u", &num))
+		return false;
 
 	outNumber = num;
 	return true;
@@ -222,19 +214,9 @@ bool ScriptCompiler::parseHexNumber(const Common::String &token, uint start, uin
 	if (start == token.size())
 		return false;
 
-	uint32 num = 0;
-	for (uint i = start; i < token.size(); i++) {
-		char c = token[i];
-		uint32 digit = 0;
-		if (c >= '0' && c <= '9')
-			digit = c - '0';
-		else if (c >= 'a' && c <= 'f')
-			digit = (c - 'a') + 0xa;
-		else
-			return false;
-
-		num = num * 16u + digit;
-	}
+	uint num = 0;
+	if (!sscanf(token.c_str() + start, "%x", &num))
+		return false;
 
 	outNumber = num;
 	return true;


Commit: c7f6aa409a515b8ee86aac5bd45f3448b78daab0
    https://github.com/scummvm/scummvm/commit/c7f6aa409a515b8ee86aac5bd45f3448b78daab0
Author: elasota (ejlasota at gmail.com)
Date: 2023-02-21T17:16:53+01:00

Commit Message:
VCRUISE: Cleanup

Changed paths:
    engines/vcruise/script.cpp


diff --git a/engines/vcruise/script.cpp b/engines/vcruise/script.cpp
index d31eab21756..5f8474f7527 100644
--- a/engines/vcruise/script.cpp
+++ b/engines/vcruise/script.cpp
@@ -412,7 +412,7 @@ bool ScriptCompiler::compileInstructionToken(ProtoScript &script, const Common::
 		return true;
 	}
 
-	if (token.size() >= 5 && token[0] == 'C' && token[1] == 'U' && token[2] == 'R' && token[3] == '_') {
+	if (token.hasPrefix("CUR_")) {
 		script.instrs.push_back(ProtoInstruction(ScriptOps::kCursorName, indexString(token)));
 		return true;
 	}


Commit: cbe78b083f04e53ccc23b0f4ea436b799882a49d
    https://github.com/scummvm/scummvm/commit/cbe78b083f04e53ccc23b0f4ea436b799882a49d
Author: elasota (ejlasota at gmail.com)
Date: 2023-02-21T17:16:53+01:00

Commit Message:
VCRUISE: Use sscanf for TextParser number parsing

Changed paths:
    engines/vcruise/textparser.cpp


diff --git a/engines/vcruise/textparser.cpp b/engines/vcruise/textparser.cpp
index 9cbdec32a1f..b8ecf16189e 100644
--- a/engines/vcruise/textparser.cpp
+++ b/engines/vcruise/textparser.cpp
@@ -133,43 +133,8 @@ void TextParser::expectInt(int &outInt, const Common::String &blamePath) {
 	TextParserState state;
 	expectTokenInternal(token, blamePath, state);
 
-	int result = 0;
-	bool isNegative = false;
-	uint startIndex = 0;
-	if (token[0] == '-') {
-		if (token.size() == 1)
-			error("Parsing error in '%s' at line %i col %i: Signed integer was malformed", blamePath.c_str(), static_cast<int>(state._lineNum), static_cast<int>(state._col));
-
-		isNegative = true;
-		startIndex = 1;
-	}
-
-	int base = 10;
-	bool isHex = (token.size() >= (startIndex) + 3 && token[startIndex] == '0' && token[startIndex + 1] == 'x');
-	if (isHex) {
-		startIndex += 2;
-		base = 16;
-	}
-
-	for (uint i = startIndex; i < token.size(); i++) {
-		char c = token[i];
-		int digit = 0;
-		if (c >= '0' && c <= '9')
-			digit = (c - '0');
-		else if (isHex && (c >= 'a' && c <= 'f'))
-			digit = (c - 'a') + 0xa;
-		else if (isHex && (c >= 'A' && c <= 'F'))
-			digit = (c - 'A') + 0xa;
-		else
-			error("Parsing error in '%s' at line %i col %i: Integer contained non-digits", blamePath.c_str(), static_cast<int>(state._lineNum), static_cast<int>(state._col));
-
-		if (isNegative)
-			digit = -digit;
-
-		result = result * base + digit;
-	}
-
-	outInt = result;
+	if (!sscanf(token.c_str(), "%i", &outInt))
+		error("Parsing error in '%s' at line %i col %i: Integer was malformed", blamePath.c_str(), static_cast<int>(state._lineNum), static_cast<int>(state._col));
 }
 
 void TextParser::expectUInt(uint &outUInt, const Common::String &blamePath) {
@@ -177,19 +142,8 @@ void TextParser::expectUInt(uint &outUInt, const Common::String &blamePath) {
 	TextParserState state;
 	expectTokenInternal(token, blamePath, state);
 
-	uint result = 0;
-
-	for (uint i = 0; i < token.size(); i++) {
-		char c = token[i];
-		if (c < '0' || c > '9')
-			error("Parsing error in '%s' at line %i col %i: Integer contained non-digits", blamePath.c_str(), static_cast<int>(state._lineNum), static_cast<int>(state._col));
-
-		uint additional = c - '0';
-
-		result = result * 10 + additional;
-	}
-
-	outUInt = result;
+	if (!sscanf(token.c_str(), "%u", &outUInt))
+		error("Parsing error in '%s' at line %i col %i: Unsigned integer was malformed", blamePath.c_str(), static_cast<int>(state._lineNum), static_cast<int>(state._col));
 }
 
 void TextParser::expectLine(Common::String &outToken, const Common::String &blamePath, bool continueToNextLine) {


Commit: 1f4f556e194351202487c48b5d396d62ba32c4b5
    https://github.com/scummvm/scummvm/commit/1f4f556e194351202487c48b5d396d62ba32c4b5
Author: elasota (ejlasota at gmail.com)
Date: 2023-02-21T17:16:53+01:00

Commit Message:
VCRUISE: Initial panorama controls

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


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index e64e8e3bdbe..fb5a02b1918 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -68,7 +68,7 @@ const MapScreenDirectionDef *MapDef::getScreenDirection(uint screen, uint direct
 	return screenDirections[screen][direction].get();
 }
 
-ScriptEnvironmentVars::ScriptEnvironmentVars() : lmb(false) {
+ScriptEnvironmentVars::ScriptEnvironmentVars() : lmb(false), panInteractionID(0) {
 }
 
 void Runtime::RenderSection::init(const Common::Rect &paramRect, const Graphics::PixelFormat &fmt) {
@@ -86,7 +86,8 @@ Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &roo
 	  _escOn(false), _debugMode(false), _panoramaDirectionFlags(0),
 	  _loadedAnimation(0), _animPendingDecodeFrame(0), _animDisplayingFrame(0), _animFirstFrame(0), _animLastFrame(0), _animDecoderState(kAnimDecoderStateStopped),
 	  _animPlayWhileIdle(false), _idleIsOnInteraction(false), _idleInteractionID(0),
-	  _lmbDown(false), _lmbDragging(false), _lmbDownTime(0) {
+	  _lmbDown(false), _lmbDragging(false), _lmbReleaseWasClick(false), _lmbDownTime(0),
+	  _panoramaState(kPanoramaStateInactive) {
 
 	for (uint i = 0; i < kNumDirections; i++)
 		_haveIdleAnimations[i] = false;
@@ -165,6 +166,12 @@ bool Runtime::runFrame() {
 		case kGameStateIdle:
 			moreActions = runIdle();
 			break;
+		case kGameStatePanLeft:
+			moreActions = runHorizontalPan(false);
+			break;
+		case kGameStatePanRight:
+			moreActions = runHorizontalPan(true);
+			break;
 		case kGameStateScript:
 			moreActions = runScript();
 			break;
@@ -227,12 +234,65 @@ bool Runtime::runIdle() {
 	if (_debugMode)
 		drawDebugOverlay();
 
+	detectPanoramaMouseMovement();
+
 	OSEvent osEvent;
 	while (popOSEvent(osEvent)) {
-		if (osEvent.type == kOSEventTypeMouseMove)
-			dischargeIdleMouseMove();
+		if (osEvent.type == kOSEventTypeMouseMove) {
+			bool changedState = dischargeIdleMouseMove();
+			if (changedState)
+				return true;
+		}
 	}
 
+	// Yield
+	return false;
+}
+
+bool Runtime::runHorizontalPan(bool isRight) {
+	bool animEnded = false;
+	continuePlayingAnimation(true, animEnded);
+
+	Common::Point panRelMouse = _mousePos - _panoramaAnchor;
+
+	if (!_lmbDown) {
+		debug(1, "Terminating pan: LMB is not down");
+		startTerminatingHorizontalPan(isRight);
+		return true;
+	}
+
+	if (!isRight && panRelMouse.x > -kPanoramaPanningMarginX) {
+		debug(1, "Terminating pan: Over threshold for left movement");
+		startTerminatingHorizontalPan(isRight);
+		return true;
+	} else if (isRight && panRelMouse.x < kPanoramaPanningMarginX) {
+		debug(1, "Terminating pan: Over threshold for right movement");
+		startTerminatingHorizontalPan(isRight);
+		return true;
+	}
+
+	OSEvent osEvent;
+	while (popOSEvent(osEvent)) {
+		if (osEvent.type == kOSEventTypeLButtonUp) {
+			debug(1, "Terminating pan: LMB up");
+			startTerminatingHorizontalPan(isRight);
+			return true;
+		} else if (osEvent.type == kOSEventTypeMouseMove) {
+			panRelMouse = osEvent.pos - _panoramaAnchor;
+
+			if (!isRight && panRelMouse.x > -kPanoramaPanningMarginX) {
+				debug(1, "Terminating pan: Over threshold for left movement (from queue)");
+				startTerminatingHorizontalPan(isRight);
+				return true;
+			} else if (isRight && panRelMouse.x < kPanoramaPanningMarginX) {
+				debug(1, "Terminating pan: Over threshold for right movement (from queue)");
+				startTerminatingHorizontalPan(isRight);
+				return true;
+			}
+		}
+	}
+
+	// Didn't terminate, yield
 	return false;
 }
 
@@ -295,6 +355,7 @@ void Runtime::continuePlayingAnimation(bool loop, bool &outAnimationEnded) {
 		// starts delaying without waiting for the time until the next frame to expire.
 		// The loop check here DOES wait for the time until next frame to expire.
 		if (loop && _animPendingDecodeFrame > _animLastFrame) {
+			debug(4, "Looped animation at frame %u", _animLastFrame);
 			if (!_animDecoder->seekToFrame(_animFirstFrame)) {
 				outAnimationEnded = true;
 				return;
@@ -303,6 +364,8 @@ void Runtime::continuePlayingAnimation(bool loop, bool &outAnimationEnded) {
 			_animPendingDecodeFrame = _animFirstFrame;
 		}
 
+		debug(4, "Decoding animation frame %u", _animPendingDecodeFrame);
+
 		const Graphics::Surface *surface = _animDecoder->decodeNextFrame();
 		if (!surface) {
 			outAnimationEnded = true;
@@ -461,6 +524,28 @@ void Runtime::terminateScript() {
 		changeToScreen(_roomNumber, _screenNumber);
 }
 
+void Runtime::startTerminatingHorizontalPan(bool isRight) {
+
+	// Figure out what slice this is.  The last frame is 1 less than usual.
+	uint slice = (_animDisplayingFrame - _animFirstFrame) * kNumDirections / (_animLastFrame - _animFirstFrame + 1);
+
+	// Compute an end frame at the end of the slice.
+	_animLastFrame = (slice + 1) * (_animLastFrame - _animFirstFrame + 1) / kNumDirections + _animFirstFrame;
+
+	debug(1, "Terminating pan at frame slice %u -> frame %u", slice, _animLastFrame);
+
+	if (isRight)
+		_direction = (slice + 1) % kNumDirections;
+	else
+		_direction = kNumDirections - 1 - slice;
+
+	_gameState = kGameStateWaitingForAnimation;
+	_panoramaState = kPanoramaStateInactive;
+
+	// Need to return to idle after direction change
+	_havePendingReturnToIdleState = true;
+}
+
 bool Runtime::popOSEvent(OSEvent &evt) {
 	OSEvent tempEvent;
 
@@ -494,8 +579,9 @@ bool Runtime::popOSEvent(OSEvent &evt) {
 			if (!_lmbDown)
 				continue;
 
+			_lmbReleaseWasClick = !_lmbDragging;
 			_lmbDown = false;
-			// Don't reset _lmbDragging - Want that available to determine if this was a drag release or click
+			_lmbDragging = false;
 		}
 
 		evt = tempEvent;
@@ -635,8 +721,11 @@ void Runtime::returnToIdleState() {
 
 	detectPanoramaDirections();
 
+	_panoramaState = kPanoramaStateInactive;
+	detectPanoramaMouseMovement();
+
 	changeToCursor(_cursors[kCursorArrow]);
-	dischargeIdleMouseMove();
+	(void) dischargeIdleMouseMove();
 }
 
 void Runtime::changeToCursor(const Common::SharedPtr<Graphics::WinCursorGroup> &cursor) {
@@ -648,36 +737,65 @@ void Runtime::changeToCursor(const Common::SharedPtr<Graphics::WinCursorGroup> &
 	}
 }
 
-void Runtime::dischargeIdleMouseMove() {
+bool Runtime::dischargeIdleMouseMove() {
 	const MapScreenDirectionDef *sdDef = _map.getScreenDirection(_screenNumber, _direction);
 
-	Common::Point relMouse(_mousePos.x - _gameSection.rect.left, _mousePos.y - _gameSection.rect.top);
+	if (_panoramaState == kPanoramaStateInactive) {
+		Common::Point relMouse(_mousePos.x - _gameSection.rect.left, _mousePos.y - _gameSection.rect.top);
 
-	bool isOnInteraction = false;
-	uint interactionID = 0;
-	if (sdDef) {
-		for (const InteractionDef &idef : sdDef->interactions) {
-			if (idef.rect.contains(relMouse)) {
-				isOnInteraction = true;
-				interactionID = idef.interactionID;
-				break;
+		bool isOnInteraction = false;
+		uint interactionID = 0;
+		if (sdDef) {
+			for (const InteractionDef &idef : sdDef->interactions) {
+				if (idef.rect.contains(relMouse)) {
+					isOnInteraction = true;
+					interactionID = idef.interactionID;
+					break;
+				}
 			}
 		}
-	}
 
-	if (isOnInteraction && (_idleIsOnInteraction == false || interactionID != _idleInteractionID)) {
-		_idleIsOnInteraction = true;
-		_idleInteractionID = interactionID;
+		if (isOnInteraction && (_idleIsOnInteraction == false || interactionID != _idleInteractionID)) {
+			_idleIsOnInteraction = true;
+			_idleInteractionID = interactionID;
 
-		// New interaction, is there a script?
-		Common::SharedPtr<Script> script = findScriptForInteraction(interactionID);
+			// New interaction, is there a script?
+			Common::SharedPtr<Script> script = findScriptForInteraction(interactionID);
 
-		if (script)
-			activateScript(script, ScriptEnvironmentVars());
-	} else if (!isOnInteraction && _idleIsOnInteraction) {
-		_idleIsOnInteraction = false;
-		changeToCursor(_cursors[kCursorArrow]);
+			if (script) {
+				activateScript(script, ScriptEnvironmentVars());
+				return true;
+			}
+		} else if (!isOnInteraction && _idleIsOnInteraction) {
+			_idleIsOnInteraction = false;
+			changeToCursor(_cursors[kCursorArrow]);
+		}
+	} else {
+		uint interactionID = 0;
+
+		Common::Point panRelMouse = _mousePos - _panoramaAnchor;
+		if (_havePanAnimations) {
+			if (panRelMouse.x <= -kPanoramaPanningMarginX)
+				interactionID = kPanLeftInteraction;
+			else if (panRelMouse.x >= kPanoramaPanningMarginX)
+				interactionID = kPanRightInteraction;
+
+			if (interactionID) {
+				// If there's an interaction script for this direction, execute it
+				Common::SharedPtr<Script> script = findScriptForInteraction(interactionID);
+
+				if (script) {
+					ScriptEnvironmentVars vars;
+					vars.panInteractionID = interactionID;
+					activateScript(script, vars);
+					return true;
+				}
+			}
+		}
 	}
+
+	// Didn't do anything
+	return false;
 }
 
 void Runtime::loadMap(Common::SeekableReadStream *stream) {
@@ -744,6 +862,10 @@ void Runtime::changeMusicTrack(int track) {
 }
 
 void Runtime::changeAnimation(const AnimationDef &animDef) {
+	changeAnimation(animDef, animDef.firstFrame);
+}
+
+void Runtime::changeAnimation(const AnimationDef &animDef, uint initialFrame) {
 	int animFile = animDef.animNum;
 	if (animFile < 0)
 		animFile = -animFile;
@@ -773,10 +895,14 @@ void Runtime::changeAnimation(const AnimationDef &animDef) {
 		_animDecoderState = kAnimDecoderStatePaused;
 	}
 
-	_animDecoder->seekToFrame(animDef.firstFrame);
-	_animPendingDecodeFrame = animDef.firstFrame;
+	assert(initialFrame >= animDef.firstFrame && initialFrame <= animDef.lastFrame);
+
+	_animDecoder->seekToFrame(initialFrame);
+	_animPendingDecodeFrame = initialFrame;
 	_animFirstFrame = animDef.firstFrame;
 	_animLastFrame = animDef.lastFrame;
+
+	debug(1, "Animation last frame set to %u", animDef.lastFrame);
 }
 
 AnimationDef Runtime::stackArgsToAnimDef(const StackValue_t *args) const {
@@ -954,6 +1080,19 @@ void Runtime::detectPanoramaDirections() {
 		_panoramaDirectionFlags |= kPanoramaHorizFlags;
 }
 
+void Runtime::detectPanoramaMouseMovement() {
+	if (_panoramaState == kPanoramaStateInactive && (_lmbDragging || (_lmbDown && (g_system->getMillis() - _lmbDownTime) >= 500)))
+		panoramaActivate();
+}
+
+void Runtime::panoramaActivate() {
+	assert(_panoramaState == kPanoramaStateInactive);
+	_panoramaState = kPanoramaStatePanningUncertainDirection;
+	_panoramaAnchor = _mousePos;
+
+	// TODO: Change mouse cursor
+}
+
 void Runtime::onLButtonDown(int16 x, int16 y) {
 	onMouseMove(x, y);
 
@@ -1063,7 +1202,38 @@ void Runtime::scriptOpSAnimL(ScriptArg_t arg) {
 
 OPCODE_STUB(ChangeL)
 
-OPCODE_STUB(AnimR)
+void Runtime::scriptOpAnimR(ScriptArg_t arg) {
+	if (_scriptEnv.panInteractionID == kPanLeftInteraction) {
+		debug(1, "Pan-left interaction from direction %u", _direction);
+
+		uint reverseDirectionSlice = (kNumDirections - _direction);
+		if (reverseDirectionSlice == kNumDirections)
+			reverseDirectionSlice = 0;
+
+		uint initialFrame = reverseDirectionSlice * (_panLeftAnimationDef.lastFrame - _panLeftAnimationDef.firstFrame) / kNumDirections + _panLeftAnimationDef.firstFrame;
+
+		AnimationDef trimmedAnimation = _panLeftAnimationDef;
+		trimmedAnimation.lastFrame--;
+
+		debug(1, "Running frame loop of %u - %u from frame %u", trimmedAnimation.firstFrame, trimmedAnimation.lastFrame, initialFrame);
+
+		changeAnimation(trimmedAnimation, initialFrame);
+		_gameState = kGameStatePanLeft;
+	} else if (_scriptEnv.panInteractionID == kPanRightInteraction) {
+		debug(1, "Pan-right interaction from direction %u", _direction);
+
+		uint initialFrame = _direction * (_panRightAnimationDef.lastFrame - _panRightAnimationDef.firstFrame) / kNumDirections + _panRightAnimationDef.firstFrame;
+
+		AnimationDef trimmedAnimation = _panRightAnimationDef;
+		trimmedAnimation.lastFrame--;
+
+		debug(1, "Running frame loop of %u - %u from frame %u", trimmedAnimation.firstFrame, trimmedAnimation.lastFrame, initialFrame);
+
+		changeAnimation(_panRightAnimationDef, initialFrame);
+		_gameState = kGameStatePanRight;
+	}
+}
+
 OPCODE_STUB(AnimF)
 OPCODE_STUB(AnimN)
 OPCODE_STUB(AnimG)
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index fb48aea1cc8..1fc3e09a692 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -59,6 +59,9 @@ enum GameState {
 	kGameStateQuit,					// Quitting
 	kGameStateIdle,					// Waiting for input events
 	kGameStateScript,				// Running a script
+
+	kGameStatePanLeft,
+	kGameStatePanRight,
 };
 
 struct AnimationDef {
@@ -106,6 +109,7 @@ struct ScriptEnvironmentVars {
 	ScriptEnvironmentVars();
 
 	bool lmb;
+	uint panInteractionID;
 };
 
 class Runtime {
@@ -169,6 +173,9 @@ private:
 
 	enum PanoramaState {
 		kPanoramaStateInactive,
+
+		kPanoramaStatePanningUncertainDirection,
+
 		kPanoramaStatePanningLeft,
 		kPanoramaStatePanningRight,
 		kPanoramaStatePanningUp,
@@ -189,6 +196,7 @@ private:
 
 	bool bootGame();
 	bool runIdle();
+	bool runHorizontalPan(bool isRight);
 	bool runScript();
 	bool runWaitForAnimation();
 	void continuePlayingAnimation(bool loop, bool &outEndedAnimation);
@@ -196,6 +204,8 @@ private:
 	void commitSectionToScreen(const RenderSection &section, const Common::Rect &rect);
 	void terminateScript();
 
+	void startTerminatingHorizontalPan(bool isRight);
+
 	bool popOSEvent(OSEvent &evt);
 	void queueOSEvent(const OSEvent &evt);
 
@@ -203,11 +213,12 @@ private:
 	void changeToScreen(uint roomNumber, uint screenNumber);
 	void returnToIdleState();
 	void changeToCursor(const Common::SharedPtr<Graphics::WinCursorGroup> &cursor);
-	void dischargeIdleMouseMove();
+	bool dischargeIdleMouseMove();
 	void loadMap(Common::SeekableReadStream *stream);
 
 	void changeMusicTrack(int musicID);
 	void changeAnimation(const AnimationDef &animDef);
+	void changeAnimation(const AnimationDef &animDef, uint initialFrame);
 
 	AnimationDef stackArgsToAnimDef(const StackValue_t *args) const;
 
@@ -219,7 +230,10 @@ private:
 	void drawDebugOverlay();
 
 	Common::SharedPtr<Script> findScriptForInteraction(uint interactionID) const;
+
 	void detectPanoramaDirections();
+	void detectPanoramaMouseMovement();
+	void panoramaActivate();
 
 	// Script things
 	void scriptOpNumber(ScriptArg_t arg);
@@ -373,12 +387,23 @@ private:
 	uint32 _lmbDownTime;
 	bool _lmbDown;
 	bool _lmbDragging;
+	bool _lmbReleaseWasClick;	// If true, then the mouse didn't move at all since the LMB down
+
+	PanoramaState _panoramaState;
+	// The anchor point behavior is kind of weird.  The way it works in Reah is that if the camera is panning left or right and the mouse is moved
+	// back across the anchor threshold at all, then the anchor point is reset to the mouse position.  This means it can drift, because the center
+	// is moved by roughly the margin amount.
+	// Vertical panning on the other hand resets the anchor vertical coordinate every time.
+	Common::Point _panoramaAnchor;
 
 	Common::Array<OSEvent> _pendingEvents;
 
 	static const uint kAnimDefStackArgs = 3;
 
 	static const uint kCursorArrow = 0;
+
+	static const int kPanoramaPanningMarginX = 10;
+	static const int kPanoramaPanningMarginY = 10;
 };
 
 } // End of namespace VCruise




More information about the Scummvm-git-logs mailing list