[Scummvm-git-logs] scummvm master -> 05da459107a838cec49e4b2d6e446b2f11eed309

elasota noreply at scummvm.org
Sun Mar 31 19:40:07 UTC 2024


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

Summary:
26c054e0c5 IMAGE: Add support for loading CUR and ANI files
bdd801ce00 VCRUISE: Detect and boot AD2044
9db748a3f9 VCRUISE: More AD2044 loading stuff
b7f0a753ca VCRUISE: Add #M and #EM opcodes
e26ec842f9 VCRUISE: Add CUR_LUPA binding for AD2044
cd2e9aa37f VCRUISE: Load AD2044 font. Fix cursor animations.
3ccaa683cd VCRUISE: Fix AD2044 fullscreen UI
09a7b1fb5f VCRUISE: Add AD2044 subtitle rendering
3729728a83 VCRUISE: Add missing opcode dispatches
073a3d35d1 VCRUISE: Fix wrong forward cursor ID
e907bb1153 VCRUISE: Add say1rnd opcode
e33a407f56 VCRUISE: Stub sound ops
e7c47fb500 VCRUISE: Fix sound IDs, open cursor, and some screen overrides
b313b4068f VCRUISE: AD2044 inventory support
63bcca8cc9 VCRUISE: Add some boilerplate for loading graphics and strings
712481918e VCRUISE: Fix a bunch of things to get bathroom working
3b1bebf0a2 VCRUISE: Fix up bathroom mirror behavior
a90317c90d VCRUISE: Stub out inventory interactions
6892545213 VCRUISE: Add inventory pickup/stash
00c7323818 VCRUISE: Stub Say3K op
965e7afab6 VCRUISE: Fix static looping animations not persisting through reload
d1b08c9960 VCRUISE: Add MIDI playback
1d7c33e893 VCRUISE: Fix some missing mutex locks
c8d2d34644 VCRUISE: Add item examination
ffa36e1898 VCRUISE: Fix MIDI crash when restarting the game
774a7dfac3 VCRUISE: Avoid restarting music if the track didn't change
e9000d2a8e VCRUISE: Add return from item examination
0867fd9126 VCRUISE: Add say cycle ops and some item infos
fa2dfbcd7b VCRUISE: Disallow examining while already examining
b66044f2e7 VCRUISE: Add some more handling of unusual animations
05da459107 VCRUISE: Fix C++11 narrowing conversion warning


Commit: 26c054e0c50cdd8da87d98ec7bbe9e54a2809d13
    https://github.com/scummvm/scummvm/commit/26c054e0c50cdd8da87d98ec7bbe9e54a2809d13
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2024-03-31T14:39:28-04:00

Commit Message:
IMAGE: Add support for loading CUR and ANI files

Changed paths:
  A image/ani.cpp
  A image/ani.h
  A image/icocur.cpp
  A image/icocur.h
    graphics/wincursor.cpp
    graphics/wincursor.h
    image/module.mk


diff --git a/graphics/wincursor.cpp b/graphics/wincursor.cpp
index e5634a0fdca..15e7aba7718 100644
--- a/graphics/wincursor.cpp
+++ b/graphics/wincursor.cpp
@@ -31,7 +31,7 @@ namespace Graphics {
 /** A Windows cursor. */
 class WinCursor : public Cursor {
 public:
-	WinCursor();
+	WinCursor(uint16 hotspotX, uint16 hotspotY);
 	~WinCursor();
 
 	/** Return the cursor's width. */
@@ -56,6 +56,8 @@ public:
 	bool readFromStream(Common::SeekableReadStream &stream);
 
 private:
+	WinCursor() = delete;
+
 	byte *_surface;
 	byte *_mask;
 	byte _palette[256 * 3];
@@ -70,11 +72,11 @@ private:
 	void clear();
 };
 
-WinCursor::WinCursor() {
+WinCursor::WinCursor(uint16 hotspotX, uint16 hotspotY) {
 	_width    = 0;
 	_height   = 0;
-	_hotspotX = 0;
-	_hotspotY = 0;
+	_hotspotX = hotspotX;
+	_hotspotY = hotspotY;
 	_surface  = nullptr;
 	_mask     = nullptr;
 	_keyColor = 0;
@@ -111,9 +113,6 @@ bool WinCursor::readFromStream(Common::SeekableReadStream &stream) {
 	const bool supportOpacity = g_system->hasFeature(OSystem::kFeatureCursorMask);
 	const bool supportInvert = g_system->hasFeature(OSystem::kFeatureCursorMaskInvert);
 
-	_hotspotX = stream.readUint16LE();
-	_hotspotY = stream.readUint16LE();
-
 	// Check header size
 	if (stream.readUint32LE() != 40)
 		return false;
@@ -151,8 +150,10 @@ bool WinCursor::readFromStream(Common::SeekableReadStream &stream) {
 	if (numColors == 0)
 		numColors = 1 << bitsPerPixel;
 
+	// Skip number of important colors
+	stream.skip(4);
+
 	// Reading the palette
-	stream.seek(40 + 4);
 	for (uint32 i = 0 ; i < numColors; i++) {
 		_palette[i * 3 + 2] = stream.readByte();
 		_palette[i * 3 + 1] = stream.readByte();
@@ -311,11 +312,14 @@ WinCursorGroup *WinCursorGroup::createCursorGroup(Common::WinResources *exe, con
 			return 0;
 		}
 
-		WinCursor *cursor = new WinCursor();
-		if (!cursor->readFromStream(*cursorStream)) {
-			delete cursor;
+		uint16 hotspotX = cursorStream->readUint16LE();
+		uint16 hotspotY = cursorStream->readUint16LE();
+
+		Cursor *cursor = loadWindowsCursorFromDIB(*cursorStream, hotspotX, hotspotY);
+
+		if (!cursor) {
 			delete group;
-			return 0;
+			return nullptr;
 		}
 
 		CursorItem item;
@@ -448,4 +452,14 @@ Cursor *makeBusyWinCursor() {
 	return new BusyWinCursor();
 }
 
+Cursor *loadWindowsCursorFromDIB(Common::SeekableReadStream &stream, uint16 hotspotX, uint16 hotspotY) {
+	WinCursor *cursor = new WinCursor(hotspotX, hotspotY);
+	if (!cursor->readFromStream(stream)) {
+		delete cursor;
+		return nullptr;
+	}
+
+	return cursor;
+}
+
 } // End of namespace Graphics
diff --git a/graphics/wincursor.h b/graphics/wincursor.h
index 92e6479575e..210128fdd8f 100644
--- a/graphics/wincursor.h
+++ b/graphics/wincursor.h
@@ -80,6 +80,13 @@ Cursor *makeDefaultWinCursor();
  */
 Cursor *makeBusyWinCursor();
 
+/**
+ * Create a Cursor from DIB-format data, i.e. starting with a BITMAPINFOHEADER
+ *
+ * @note The calling code is responsible for deleting the returned pointer.
+ */
+Cursor *loadWindowsCursorFromDIB(Common::SeekableReadStream &stream, uint16 hotspotX, uint16 hotspotY);
+
 /** @} */
 
 } // End of namespace Graphics
diff --git a/image/ani.cpp b/image/ani.cpp
new file mode 100644
index 00000000000..c556e32815f
--- /dev/null
+++ b/image/ani.cpp
@@ -0,0 +1,304 @@
+/* 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/memstream.h"
+#include "common/stream.h"
+#include "common/substream.h"
+
+#include "image/ani.h"
+
+namespace Image {
+
+AniDecoder::Metadata::Metadata()
+	: numFrames(0), numSteps(0), width(0), height(0), bitCount(0),
+	  numPlanes(0), perFrameDelay(0), haveSeqData(false), isCURFormat(false) {
+}
+
+AniDecoder::FrameDef::FrameDef() : delay(0), imageIndex(0) {
+}
+
+AniDecoder::AniDecoder() : _stream(nullptr), _disposeAfterUse(DisposeAfterUse::NO) {
+}
+
+AniDecoder::~AniDecoder() {
+	close();
+}
+
+void AniDecoder::close() {
+	if (_disposeAfterUse == DisposeAfterUse::YES && _stream != nullptr)
+		delete _stream;
+
+	_stream = nullptr;
+}
+
+const AniDecoder::Metadata &AniDecoder::getMetadata() const {
+	return _metadata;
+}
+
+AniDecoder::FrameDef AniDecoder::getSequenceFrame(uint sequenceIndex) const {
+	FrameDef frameDef;
+
+	if (sequenceIndex >= _rateData.size())
+		frameDef.delay = _metadata.perFrameDelay;
+	else
+		frameDef.delay = _rateData[sequenceIndex];
+
+	if (sequenceIndex >= _seqData.size())
+		frameDef.imageIndex = sequenceIndex;
+	else
+		frameDef.imageIndex = _seqData[sequenceIndex];
+
+	return frameDef;
+}
+
+Common::SeekableReadStream *AniDecoder::openImageStream(uint imageIndex) const {
+	if (imageIndex >= _frameDataLocations.size())
+		error("Invalid ANI image index");
+
+	const FrameDataRange &frameDataRange = _frameDataLocations[imageIndex];
+
+	return new Common::SafeSeekableSubReadStream(_stream, frameDataRange.pos, frameDataRange.pos + frameDataRange.size);
+}
+
+bool AniDecoder::open(Common::SeekableReadStream &stream, DisposeAfterUse::Flag disposeAfterUse) {
+	close();
+
+	_stream = &stream;
+	_disposeAfterUse = disposeAfterUse;
+
+	bool loadedOK = load();
+	if (!loadedOK)
+		close();
+
+	return loadedOK;
+}
+
+bool AniDecoder::load() {
+	if (!parseRIFFChunks(*_stream, Common::Functor2Mem<const RIFFChunkDef &, Common::SeekableReadStream &, bool, AniDecoder>(this, &AniDecoder::parseTopLevelChunk))) {
+		warning("AniDecoder::load: Failed to load ANI container");
+		return false;
+	}
+
+	return true;
+}
+
+bool AniDecoder::parseRIFFChunks(Common::SeekableReadStream &stream, const RIFFChunkParseFunc_t &callback) {
+	int64 nextChunkStartPos = 0;
+	int64 endPos = stream.size();
+
+	while (nextChunkStartPos < endPos) {
+		if (!stream.seek(nextChunkStartPos)) {
+			warning("AniDecoder::parseRIFFChunks: Failed to reset to start of RIFF chunk");
+			return false;
+		}
+
+		byte riffChunkHeader[8];
+
+		if (stream.read(riffChunkHeader, 8) != 8) {
+			warning("AniDecoder::parseRIFFChunks: Failed to read RIFF chunk header");
+			return false;
+		}
+
+		uint32 chunkSize = READ_LE_UINT32(riffChunkHeader + 4);
+
+		int64 actualChunkSize = chunkSize;
+		if (chunkSize & 1)
+			actualChunkSize++;
+
+		int64 chunkAvailable = stream.size() - stream.pos();
+		if (chunkAvailable < actualChunkSize) {
+			warning("AniDecoder::parseRIFFChunk: RIFF chunk is too large");
+			return false;
+		}
+
+		RIFFChunkDef chunkDef;
+		chunkDef.id = READ_BE_UINT32(riffChunkHeader);
+		chunkDef.size = chunkSize;
+
+		Common::SeekableSubReadStream substream(&stream, static_cast<uint32>(stream.pos()), static_cast<uint32>(stream.pos()) + chunkSize);
+		if (!callback(chunkDef, substream))
+			return false;
+
+		nextChunkStartPos += actualChunkSize + 8;
+	}
+
+	return true;
+}
+
+bool AniDecoder::parseRIFFContainer(Common::SeekableReadStream &chunkStream, const RIFFChunkDef &chunkDef, const RIFFContainerParseFunc_t &callback) {
+	if (chunkDef.size < 4) {
+		warning("AniDecoder::parseRIFFContainer: RIFF container is too small");
+		return false;
+	}
+
+	byte containerTypeID[4];
+	if (chunkStream.read(containerTypeID, 4) != 4) {
+		warning("AniDecoder::parseRIFFContainer: Failed to read RIFF container type");
+		return false;
+	}
+
+	RIFFContainerDef containerDef;
+	containerDef.id = READ_BE_UINT32(containerTypeID);
+	containerDef.size = chunkDef.size - 4;
+
+	Common::SeekableSubReadStream substream(&chunkStream, 4, chunkDef.size);
+	return callback(containerDef, substream);
+}
+
+bool AniDecoder::parseTopLevelChunk(const RIFFChunkDef &chunk, Common::SeekableReadStream &stream) {
+	if (chunk.id != MKTAG('R', 'I', 'F', 'F')) {
+		warning("AniDecoder::parseTopLevelChunk: Top-level chunk isn't RIFF");
+		return false;
+	}
+
+	return parseRIFFContainer(stream, chunk, Common::Functor2Mem<const RIFFContainerDef &, Common::SeekableReadStream &, bool, AniDecoder>(this, &AniDecoder::parseTopLevelContainer));
+}
+
+bool AniDecoder::parseTopLevelContainer(const RIFFContainerDef &container, Common::SeekableReadStream &stream) {
+	if (container.id == MKTAG('A', 'C', 'O', 'N'))
+		return parseRIFFChunks(stream, Common::Functor2Mem<const RIFFChunkDef &, Common::SeekableReadStream &, bool, AniDecoder>(this, &AniDecoder::parseSecondLevelChunk));
+
+	warning("AniDecoder::parseTopLevelContainer: Top-level container isn't ACON");
+	return false;
+}
+
+bool AniDecoder::parseSecondLevelChunk(const RIFFChunkDef &chunk, Common::SeekableReadStream &stream) {
+	if (chunk.id == MKTAG('L', 'I', 'S', 'T'))
+		return parseRIFFContainer(stream, chunk, Common::Functor2Mem<const RIFFContainerDef &, Common::SeekableReadStream &, bool, AniDecoder>(this, &AniDecoder::parseListContainer));
+
+	if (chunk.id == MKTAG('a', 'n', 'i', 'h'))
+		return parseAnimHeaderChunk(chunk, stream);
+
+	if (chunk.id == MKTAG('s', 'e', 'q', ' '))
+		return parseSeqChunk(chunk, stream);
+
+	if (chunk.id == MKTAG('r', 'a', 't', 'e'))
+		return parseRateChunk(chunk, stream);
+
+	return true;
+}
+
+bool AniDecoder::parseListContainer(const RIFFContainerDef &container, Common::SeekableReadStream &stream) {
+	if (container.id == MKTAG('f', 'r', 'a', 'm'))
+		return parseRIFFChunks(stream, Common::Functor2Mem<const RIFFChunkDef &, Common::SeekableReadStream &, bool, AniDecoder>(this, &AniDecoder::parseIconChunk));
+
+	return true;
+}
+
+bool AniDecoder::parseAnimHeaderChunk(const RIFFChunkDef &chunk, Common::SeekableReadStream &stream) {
+	byte animHeader[36];
+	for (byte &b : animHeader)
+		b = 0;
+
+	uint32 amountToRead = 36;
+	if (chunk.size < amountToRead)
+		amountToRead = chunk.size;
+
+	if (amountToRead > 0 && stream.read(animHeader, amountToRead) != amountToRead) {
+		warning("AniDecoder::parseAnimHeaderChunk: Read failed");
+		return false;
+	}
+
+	uint32 structSize = READ_LE_UINT32(animHeader);
+	if (structSize < 36) {
+		for (uint i = structSize; i < 36; i++)
+			animHeader[i] = 0;
+	}
+
+	_metadata.numFrames = READ_LE_UINT32(animHeader + 4);
+	_metadata.numSteps = READ_LE_UINT32(animHeader + 8);
+	_metadata.width = READ_LE_UINT32(animHeader + 12);
+	_metadata.height = READ_LE_UINT32(animHeader + 16);
+	_metadata.bitCount = READ_LE_UINT32(animHeader + 20);
+	_metadata.numPlanes = READ_LE_UINT32(animHeader + 24);
+	_metadata.perFrameDelay = READ_LE_UINT32(animHeader + 28);
+
+	uint32 flags = READ_LE_UINT32(animHeader + 32);
+	_metadata.isCURFormat = ((flags & 1) != 0);
+	_metadata.haveSeqData = ((flags & 2) != 0);
+
+	return true;
+}
+
+bool AniDecoder::parseSeqChunk(const RIFFChunkDef &chunk, Common::SeekableReadStream &stream) {
+	uint32 numFrames = chunk.size / 4u;
+
+	if (numFrames > 1000u) {
+		warning("AniDecoder::parseRateChunk: Too many frames");
+		return false;
+	}
+
+	if (numFrames > _seqData.size())
+		_seqData.resize(numFrames);
+
+	for (uint i = 0; i < numFrames; i++) {
+		byte seqData[4];
+
+		if (stream.read(seqData, 4) != 4) {
+			warning("AniDecoder::parseRateChunk: Failed to read sequence information");
+			return false;
+		}
+
+		_seqData[i] = READ_LE_UINT32(seqData);
+	}
+
+	return true;
+}
+
+bool AniDecoder::parseRateChunk(const RIFFChunkDef &chunk, Common::SeekableReadStream &stream) {
+	uint32 numFrames = chunk.size / 4u;
+
+	if (numFrames > 1000u) {
+		warning("AniDecoder::parseRateChunk: Too many frames");
+		return false;
+	}
+
+	if (numFrames > _rateData.size())
+		_rateData.resize(numFrames);
+
+	for (uint i = 0; i < numFrames; i++) {
+		byte rateData[4];
+
+		if (stream.read(rateData, 4) != 4) {
+			warning("AniDecoder::parseRateChunk: Failed to read rate information");
+			return false;
+		}
+
+		_rateData[i] = READ_LE_UINT32(rateData);
+	}
+
+	return true;
+}
+
+bool AniDecoder::parseIconChunk(const RIFFChunkDef &chunk, Common::SeekableReadStream &stream) {
+	FrameDataRange frameDataRange;
+
+	// Get the global stream position
+	frameDataRange.pos = static_cast<uint32>(_stream->pos());
+	frameDataRange.size = chunk.size;
+
+	_frameDataLocations.push_back(frameDataRange);
+
+	return true;
+}
+
+
+} // End of namespace Image
diff --git a/image/ani.h b/image/ani.h
new file mode 100644
index 00000000000..dc47b62bb08
--- /dev/null
+++ b/image/ani.h
@@ -0,0 +1,136 @@
+/* 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 GFX_ANI_H
+#define GFX_ANI_H
+
+#include "common/array.h"
+#include "common/types.h"
+#include "common/func.h"
+
+namespace Common {
+
+class SeekableReadStream;
+struct IFFChunk;
+
+} // End of namespace Common
+
+namespace Graphics {
+
+class Cursor;
+struct Surface;
+
+} // End of namespace Graphics
+
+namespace Image {
+
+class AniDecoder {
+public:
+	struct Metadata {
+		Metadata();
+
+		uint32 numFrames;	// Number of images
+		uint32 numSteps;	// Number of frames (use the FrameDef to determine which frame)
+		uint32 width;
+		uint32 height;
+		uint32 bitCount;
+		uint32 numPlanes;
+		uint32 perFrameDelay;
+		bool haveSeqData;
+		bool isCURFormat;
+	};
+
+	struct FrameDef {
+		FrameDef();
+
+		uint32 imageIndex;
+		uint32 delay;	// In 1/60 sec
+	};
+
+	AniDecoder();
+	~AniDecoder();
+
+	bool open(Common::SeekableReadStream &stream, DisposeAfterUse::Flag = DisposeAfterUse::NO);
+	void close();
+
+	const Metadata &getMetadata() const;
+	FrameDef getSequenceFrame(uint sequenceIndex) const;
+
+	/**
+	 * Opens a substream for an image.  If the metadata field
+	 * "isCURFormat" is set, you can pass the stream to IcoCurDecoder to
+	 * read it.  Otherwise, you must determine the format.  The stream
+	 * is valid for as long as the stream used to construct the AniDecoder
+	 * is valid.
+	 * 
+	 * @param imageIndex The index of the image in the ANI file.
+	 * @return A substream for the image.
+	 */
+	Common::SeekableReadStream *openImageStream(uint imageIndex) const;
+
+private:
+	struct RIFFContainerDef {
+		uint32 id;
+		uint32 size;
+	};
+
+	struct RIFFChunkDef {
+		uint32 id;
+		uint32 size;
+	};
+
+	struct FrameDataRange {
+		uint32 pos;
+		uint32 size;
+	};
+
+	typedef Common::Functor2<const RIFFContainerDef &, Common::SeekableReadStream &, bool> RIFFContainerParseFunc_t;
+	typedef Common::Functor2<const RIFFChunkDef &, Common::SeekableReadStream &, bool> RIFFChunkParseFunc_t;
+
+	bool load();
+
+	static bool parseRIFFChunks(Common::SeekableReadStream &stream, const RIFFChunkParseFunc_t &callback);
+	static bool parseRIFFContainer(Common::SeekableReadStream &stream, const RIFFChunkDef &chunkDef, const RIFFContainerParseFunc_t &callback);
+
+	bool parseTopLevelChunk(const RIFFChunkDef &chunk, Common::SeekableReadStream &stream);
+	bool parseTopLevelContainer(const RIFFContainerDef &container, Common::SeekableReadStream &stream);
+
+	bool parseSecondLevelChunk(const RIFFChunkDef &chunk, Common::SeekableReadStream &stream);
+
+	bool parseListContainer(const RIFFContainerDef &container, Common::SeekableReadStream &stream);
+
+	bool parseAnimHeaderChunk(const RIFFChunkDef &chunk, Common::SeekableReadStream &stream);
+	bool parseSeqChunk(const RIFFChunkDef &chunk, Common::SeekableReadStream &stream);
+	bool parseRateChunk(const RIFFChunkDef &chunk, Common::SeekableReadStream &stream);
+	bool parseIconChunk(const RIFFChunkDef &chunk, Common::SeekableReadStream &stream);
+
+	Metadata _metadata;
+	Common::Array<uint32> _rateData;
+	Common::Array<uint32> _seqData;
+	Common::Array<FrameDataRange> _frameDataLocations;
+
+	Common::SeekableReadStream *_stream;
+	DisposeAfterUse::Flag _disposeAfterUse;
+};
+
+} // End of namespace Image
+
+#endif
diff --git a/image/icocur.cpp b/image/icocur.cpp
new file mode 100644
index 00000000000..60876facf3a
--- /dev/null
+++ b/image/icocur.cpp
@@ -0,0 +1,142 @@
+/* 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/substream.h"
+#include "common/memstream.h"
+
+#include "image/icocur.h"
+
+#include "graphics/wincursor.h"
+
+namespace Image {
+
+IcoCurDecoder::IcoCurDecoder() : _type(kTypeInvalid), _stream(nullptr), _disposeAfterUse(DisposeAfterUse::NO) {
+}
+
+IcoCurDecoder::~IcoCurDecoder() {
+	close();
+}
+
+void IcoCurDecoder::close() {
+	if (_disposeAfterUse == DisposeAfterUse::YES && _stream != nullptr)
+		delete _stream;
+
+	_stream = nullptr;
+	_type = kTypeInvalid;
+	_items.clear();
+}
+
+bool IcoCurDecoder::open(Common::SeekableReadStream &stream, DisposeAfterUse::Flag disposeAfterUse) {
+	close();
+
+	_stream = &stream;
+	_disposeAfterUse = disposeAfterUse;
+
+	bool loadedOK = load();
+	if (!loadedOK)
+		close();
+
+	return loadedOK;
+}
+
+bool IcoCurDecoder::load() {
+	uint8 iconDirData[6];
+
+	if (_stream->read(iconDirData, 6) != 6)
+		return false;
+
+	if (iconDirData[0] != 0 || iconDirData[1] != 0 || (iconDirData[2] != 1 && iconDirData[2] != 2) || iconDirData[3] != 0) {
+		warning("Malformed ICO/CUR header");
+		return false;
+	}
+
+	uint16 numImages = READ_LE_UINT16(iconDirData + 4);
+	_type = static_cast<Type>(iconDirData[2]);
+
+	if (numImages == 0)
+		return true;
+
+	uint32 dirSize = static_cast<uint32>(numImages) * 16;
+
+	Common::Array<uint8> iconDir;
+	iconDir.resize(dirSize);
+
+	if (_stream->read(&iconDir[0], dirSize) != dirSize)
+		return false;
+
+	_items.resize(numImages);
+	for (uint i = 0; i < numImages; i++) {
+		const uint8 *entryData = &iconDir[i * 16u];
+		Item &item = _items[i];
+
+		item.width = entryData[0];
+		if (item.width == 0)
+			item.width = 256;
+
+		item.height = entryData[1];
+		if (item.height == 0)
+			item.height = 256;
+
+		item.numColors = entryData[2];
+
+		item.data.ico.numPlanes = READ_LE_UINT16(entryData + 4);
+		item.data.ico.bitsPerPixel = READ_LE_UINT16(entryData + 6);
+		item.dataSize = READ_LE_UINT32(entryData + 8);
+		item.dataOffset = READ_LE_UINT32(entryData + 12);
+	}
+
+	return true;
+}
+
+IcoCurDecoder::Type IcoCurDecoder::getType() const {
+	return _type;
+}
+
+uint IcoCurDecoder::numItems() const {
+	return _items.size();
+}
+
+const IcoCurDecoder::Item &IcoCurDecoder::getItem(uint itemIndex) const {
+	return _items[itemIndex];
+}
+
+Graphics::Cursor *IcoCurDecoder::loadItemAsCursor(uint itemIndex) const {
+	const IcoCurDecoder::Item &dirItem = _items[itemIndex];
+
+	if (_type != kTypeCUR)
+		warning("ICO/CUR file type wasn't a cursor, but is being requested as a cursor anyway");
+
+	if (static_cast<int64>(dirItem.dataOffset) > _stream->size()) {
+		warning("ICO/CUR data offset was outside of the file");
+		return nullptr;
+	}
+
+	if (_stream->size() - static_cast<int64>(dirItem.dataOffset) < static_cast<int64>(dirItem.dataSize)) {
+		warning("ICO/CUR data bounds were outside of the file");
+		return nullptr;
+	}
+
+	Common::SeekableSubReadStream substream(_stream, dirItem.dataOffset, dirItem.dataOffset + dirItem.dataSize);
+	return Graphics::loadWindowsCursorFromDIB(substream, dirItem.data.cur.hotspotX, dirItem.data.cur.hotspotY);
+}
+
+} // End of namespace Image
diff --git a/image/icocur.h b/image/icocur.h
new file mode 100644
index 00000000000..b0858a98aa3
--- /dev/null
+++ b/image/icocur.h
@@ -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/>.
+ *
+ */
+
+#ifndef GFX_ICOCUR_H
+#define GFX_ICOCUR_H
+
+#include "common/array.h"
+#include "common/types.h"
+
+namespace Common {
+
+class SeekableReadStream;
+
+} // End of namespace Common
+
+namespace Graphics {
+
+class Cursor;
+struct Surface;
+
+} // End of namespace Graphics
+
+namespace Image {
+
+class IcoCurDecoder {
+public:
+	enum Type {
+		kTypeInvalid,
+
+		kTypeICO,
+		kTypeCUR,
+	};
+
+	struct Item {
+		struct IconData {
+			uint16 numPlanes;
+			uint16 bitsPerPixel;
+		};
+
+		struct CursorData {
+			uint16 hotspotX;
+			uint16 hotspotY;
+		};
+
+		union DataUnion {
+			IconData ico;
+			CursorData cur;
+		};
+
+		uint16 width;
+		uint16 height;
+		uint8 numColors;	// May be 0
+		DataUnion data;
+		uint32 dataSize;
+		uint32 dataOffset;
+	};
+
+	IcoCurDecoder();
+	~IcoCurDecoder();
+
+	bool open(Common::SeekableReadStream &stream, DisposeAfterUse::Flag = DisposeAfterUse::NO);
+	void close();
+
+	Type getType() const;
+	uint numItems() const;
+	const Item &getItem(uint itemIndex) const;
+
+	/**
+	 * Loads an item from the directory as a cursor.
+	 *
+	 * @param itemIndex The index of the item in the directory.
+	 * @return Loaded cursor.
+	 */
+	Graphics::Cursor *loadItemAsCursor(uint itemIndex) const;
+
+private:
+	bool load();
+
+	Type _type;
+	Common::Array<Item> _items;
+
+	Common::SeekableReadStream *_stream;
+	DisposeAfterUse::Flag _disposeAfterUse;
+};
+
+} // End of namespace Image
+
+#endif
diff --git a/image/module.mk b/image/module.mk
index df24ea3f61f..5bcc928f43a 100644
--- a/image/module.mk
+++ b/image/module.mk
@@ -1,9 +1,11 @@
 MODULE := image
 
 MODULE_OBJS := \
+	ani.o \
 	bmp.o \
 	cel_3do.o \
 	gif.o \
+	icocur.o \
 	iff.o \
 	jpeg.o \
 	neo.o \


Commit: bdd801ce0069a88e14dc01cf1c5c9a10ce6b6fd5
    https://github.com/scummvm/scummvm/commit/bdd801ce0069a88e14dc01cf1c5c9a10ce6b6fd5
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2024-03-31T14:39:28-04:00

Commit Message:
VCRUISE: Detect and boot AD2044

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


diff --git a/engines/vcruise/detection.cpp b/engines/vcruise/detection.cpp
index afb968ec6fc..db1eee153b4 100644
--- a/engines/vcruise/detection.cpp
+++ b/engines/vcruise/detection.cpp
@@ -29,6 +29,7 @@
 #include "vcruise/detection.h"
 
 static const PlainGameDescriptor g_vcruiseGames[] = {
+	{"ad2044", "A.D. 2044"},
 	{"reah", "Reah: Face the Unknown"},
 	{"schizm", "Schizm: Mysterious Journey"},
 	{nullptr, nullptr}
@@ -39,6 +40,7 @@ static const char *const g_vcruiseDirectoryGlobs[] = {
 	"Log",
 	"Waves-12",
 	"Waves-22",
+	"WAVE-01",
 	nullptr
 };
 
@@ -71,7 +73,10 @@ public:
 		VCruise::VCruiseGameID gameID = reinterpret_cast<const VCruise::VCruiseGameDescription *>(adGame.desc)->gameID;
 
 		if ((adGame.desc->flags & VCruise::VCRUISE_GF_FORCE_LANGUAGE) == 0) {
-			if (gameID == VCruise::GID_REAH) {
+			if (gameID == VCruise::GID_AD2044) {
+				game.appendGUIOptions(Common::getGameGUIOptionsDescriptionLanguage(Common::EN_ANY));
+				game.appendGUIOptions(Common::getGameGUIOptionsDescriptionLanguage(Common::PL_POL));
+			} else if (gameID == VCruise::GID_REAH) {
 				game.appendGUIOptions(Common::getGameGUIOptionsDescriptionLanguage(Common::EN_ANY));
 				game.appendGUIOptions(Common::getGameGUIOptionsDescriptionLanguage(Common::NL_NLD));
 				game.appendGUIOptions(Common::getGameGUIOptionsDescriptionLanguage(Common::FR_FRA));
diff --git a/engines/vcruise/detection.h b/engines/vcruise/detection.h
index 00df2271d39..cdb143a05e6 100644
--- a/engines/vcruise/detection.h
+++ b/engines/vcruise/detection.h
@@ -31,6 +31,7 @@ enum VCruiseGameID {
 
 	GID_REAH	= 1,
 	GID_SCHIZM	= 2,
+	GID_AD2044	= 3,
 };
 
 enum VCruiseGameFlag {
@@ -41,6 +42,8 @@ enum VCruiseGameFlag {
 	
 	VCRUISE_GF_STEAM_LANGUAGES	= (1 << 4),
 	VCRUISE_GF_FORCE_LANGUAGE	= (1 << 5),
+	
+	VCRUISE_GF_WANT_MIDI		= (1 << 6),
 };
 
 struct VCruiseGameDescription {
@@ -58,6 +61,7 @@ struct VCruiseGameDescription {
 #define GAMEOPTION_FAST_ANIMATIONS				GUIO_GAMEOPTIONS2
 #define GAMEOPTION_SKIP_MENU					GUIO_GAMEOPTIONS3
 #define GAMEOPTION_INCREASE_DRAG_DISTANCE		GUIO_GAMEOPTIONS4
+#define GAMEOPTION_USE_4BIT_GRAPHICS			GUIO_GAMEOPTIONS5
 
 
 } // End of namespace VCruise
diff --git a/engines/vcruise/detection_tables.h b/engines/vcruise/detection_tables.h
index db1f9b77108..43f398a1f32 100644
--- a/engines/vcruise/detection_tables.h
+++ b/engines/vcruise/detection_tables.h
@@ -29,6 +29,20 @@
 namespace VCruise {
 
 static const VCruiseGameDescription gameDescriptions[] = {
+	{ // A.D. 2044, GOG English digital version
+		{
+			"ad2044",
+			"English Digital",
+			AD_ENTRY2s("ad2044.exe", "0ab1e3f8b3a17a5b18bb5ee356face25", 327168,
+					   "00010001.wav", "d385bb2f1b10ea8c13bbb2948794c9f6", 74950),
+			Common::UNK_LANG,
+			Common::kPlatformWindows,
+			VCRUISE_GF_WANT_MP3 | ADGF_UNSTABLE,
+			GUIO0()
+		},
+		GID_AD2044,
+		Common::EN_ANY,
+	},
 	{ // Reah: Face the Unknown, English DVD version
 		{
 			"reah",
diff --git a/engines/vcruise/metaengine.cpp b/engines/vcruise/metaengine.cpp
index 49d7e3938d3..73a4e1a794d 100644
--- a/engines/vcruise/metaengine.cpp
+++ b/engines/vcruise/metaengine.cpp
@@ -80,6 +80,17 @@ static const ADExtraGuiOptionsMap optionsList[] = {
 			0
 		}
 	},
+	{
+		GAMEOPTION_USE_4BIT_GRAPHICS,
+		{
+			_s("Use 16-color graphics"),
+			_s("Uses 16-color graphics."),
+			"vcruise_use_4bit",
+			false,
+			0,
+			0
+		}
+	},
 	AD_EXTRA_GUI_OPTIONS_TERMINATOR
 };
 
diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 03d5c8fdbb4..a31a331d2c8 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -39,7 +39,9 @@
 #include "graphics/managed_surface.h"
 #include "graphics/palette.h"
 
+#include "image/ani.h"
 #include "image/bmp.h"
+#include "image/icocur.h"
 
 #include "audio/decoders/wave.h"
 #include "audio/decoders/vorbis.h"
@@ -198,13 +200,70 @@ 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();
+MapLoader::~MapLoader() {
+}
+
+Common::SharedPtr<MapScreenDirectionDef> MapLoader::loadScreenDirectionDef(Common::ReadStream &stream) {
+	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);
+		}
+
+		return screenDirectionDef;
+	}
+
+	return nullptr;
 }
 
-const MapScreenDirectionDef *MapDef::getScreenDirection(uint screen, uint direction) {
+class ReahSchizmMapLoader : public MapLoader {
+public:
+	ReahSchizmMapLoader();
+
+	void setRoomNumber(uint roomNumber) override;
+	const MapScreenDirectionDef *getScreenDirection(uint screen, uint direction) override;
+	void unload() override;
+
+private:
+	void load();
+
+	static const uint kNumScreens = 96;
+	static const uint kFirstScreen = 0xa0;
+
+	uint _roomNumber;
+	bool _isLoaded;
+
+	Common::SharedPtr<MapScreenDirectionDef> _screenDirections[kNumScreens][kNumDirections];
+};
+
+ReahSchizmMapLoader::ReahSchizmMapLoader() : _roomNumber(0), _isLoaded(false) {
+}
+
+void ReahSchizmMapLoader::setRoomNumber(uint roomNumber) {
+	if (_roomNumber != roomNumber)
+		unload();
+
+	_roomNumber = roomNumber;
+}
+
+const MapScreenDirectionDef *ReahSchizmMapLoader::getScreenDirection(uint screen, uint direction) {
 	if (screen < kFirstScreen)
 		return nullptr;
 
@@ -213,9 +272,115 @@ const MapScreenDirectionDef *MapDef::getScreenDirection(uint screen, uint direct
 	if (screen >= kNumScreens)
 		return nullptr;
 
-	return screenDirections[screen][direction].get();
+	if (!_isLoaded)
+		load();
+
+	return _screenDirections[screen][direction].get();
+}
+
+void ReahSchizmMapLoader::load() {
+	// This is loaded even if the open fails
+	_isLoaded = true;
+
+	Common::Path mapFileName(Common::String::format("Map/Room%02i.map", static_cast<int>(_roomNumber)));
+	Common::File mapFile;
+
+	if (!mapFile.open(mapFileName))
+		return;
+
+	byte screenDefOffsets[kNumScreens * kNumDirections * 4];
+
+	if (!mapFile.seek(16))
+		error("Error skipping map file header");
+
+	if (mapFile.read(screenDefOffsets, sizeof(screenDefOffsets)) != sizeof(screenDefOffsets))
+		error("Error reading map offset table");
+
+	for (uint screen = 0; screen < kNumScreens; screen++) {
+		for (uint direction = 0; direction < kNumDirections; direction++) {
+			uint32 offset = READ_LE_UINT32(screenDefOffsets + (kNumDirections * screen + direction) * 4);
+			if (!offset)
+				continue;
+
+			// QUIRK: The stone game in the tower in Reah (Room 06) has two 0cb screens and the second one is damaged,
+			// so it must be ignored.
+			if (!_screenDirections[screen][direction]) {
+				if (!mapFile.seek(offset))
+					error("Error seeking to screen data");
+
+				_screenDirections[screen][direction] = loadScreenDirectionDef(mapFile);
+			}
+		}
+	}
+}
+
+void ReahSchizmMapLoader::unload() {
+	for (uint screen = 0; screen < kNumScreens; screen++)
+		for (uint direction = 0; direction < kNumDirections; direction++)
+			_screenDirections[screen][direction].reset();
 }
 
+class AD2044MapLoader : public MapLoader {
+public:
+	AD2044MapLoader();
+
+	void setRoomNumber(uint roomNumber) override;
+	const MapScreenDirectionDef *getScreenDirection(uint screen, uint direction) override;
+	void unload() override;
+
+private:
+	void load();
+
+	static const uint kNumScreens = 96;
+	static const uint kFirstScreen = 0xa0;
+
+	uint _roomNumber;
+	uint _screenNumber;
+	bool _isLoaded;
+
+	Common::SharedPtr<MapScreenDirectionDef> _currentMap;
+};
+
+AD2044MapLoader::AD2044MapLoader() : _roomNumber(0), _screenNumber(0), _isLoaded(false) {
+}
+
+void AD2044MapLoader::setRoomNumber(uint roomNumber) {
+	if (_roomNumber != roomNumber)
+		unload();
+
+	_roomNumber = roomNumber;
+}
+
+const MapScreenDirectionDef *AD2044MapLoader::getScreenDirection(uint screen, uint direction) {
+	if (screen != _screenNumber)
+		unload();
+
+	_screenNumber = screen;
+
+	if (!_isLoaded)
+		load();
+
+	return _currentMap.get();
+}
+
+void AD2044MapLoader::load() {
+	// This is loaded even if the open fails
+	_isLoaded = true;
+
+	Common::Path mapFileName(Common::String::format("map/SCR%i.MAP", static_cast<int>(_roomNumber * 100u + _screenNumber)));
+	Common::File mapFile;
+
+	if (!mapFile.open(mapFileName))
+		return;
+
+	_currentMap = loadScreenDirectionDef(mapFile);
+}
+
+void AD2044MapLoader::unload() {
+	_currentMap.reset();
+}
+
+
 ScriptEnvironmentVars::ScriptEnvironmentVars() : lmb(false), lmbDrag(false), esc(false), exitToMenu(false), animChangeSet(false), isEntryScript(false), puzzleWasSet(false),
 	panInteractionID(0), fpsOverride(0), lastHighlightedItem(0), animChangeFrameOffset(0), animChangeNumFrames(0) {
 }
@@ -1043,7 +1208,7 @@ Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &roo
 	: _system(system), _mixer(mixer), _roomNumber(1), _screenNumber(0), _direction(0), _hero(0), _swapOutRoom(0), _swapOutScreen(0), _swapOutDirection(0),
 	  _haveHorizPanAnimations(false), _loadedRoomNumber(0), _activeScreenNumber(0),
 	  _gameState(kGameStateBoot), _gameID(gameID), _havePendingScreenChange(false), _forceScreenChange(false), _havePendingPreIdleActions(false), _havePendingReturnToIdleState(false), _havePendingPostSwapScreenReset(false),
-	  _havePendingCompletionCheck(false), _havePendingPlayAmbientSounds(false), _ambientSoundFinishTime(0), _escOn(false), _debugMode(false), _fastAnimationMode(false),
+	  _havePendingCompletionCheck(false), _havePendingPlayAmbientSounds(false), _ambientSoundFinishTime(0), _escOn(false), _debugMode(false), _fastAnimationMode(false), _lowQualityGraphicsMode(false),
 	  _musicTrack(0), _musicActive(true), _musicMute(false), _musicMuteDisabled(false),
 	  _scoreSectionEndTime(0), _musicVolume(getDefaultSoundVolume()), _musicVolumeRampStartTime(0), _musicVolumeRampStartVolume(0), _musicVolumeRampRatePerMSec(0), _musicVolumeRampEnd(0),
 	  _panoramaDirectionFlags(0),
@@ -1061,7 +1226,7 @@ Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &roo
 	  _isInGame(false),
 	  _subtitleFont(nullptr), _isDisplayingSubtitles(false), _isSubtitleSourceAnimation(false),
 	  _languageIndex(0), _defaultLanguageIndex(0), _defaultLanguage(defaultLanguage), _charSet(kCharSetLatin),
-	  _isCDVariant(false) {
+	  _isCDVariant(false), _currentAnimatedCursor(nullptr), _currentCursor(nullptr), _cursorTimeBase(0), _cursorCycleLength(0) {
 
 	for (uint i = 0; i < kNumDirections; i++) {
 		_haveIdleAnimations[i] = false;
@@ -1102,34 +1267,72 @@ void Runtime::initSections(const Common::Rect &gameRect, const Common::Rect &men
 }
 
 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 (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;
+	if (_gameID == GID_AD2044) {
+		const int staticCursorIDs[] = {0, 29, 30, 31, 32, 33, 34, 35, 36, 39, 40, 41, 50, 96, 97, 99};
+		const int animatedCursorIDs[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
+
+		_cursors.resize(100);
+
+		for (int cid : staticCursorIDs) {
+			Common::String cursorPath = Common::String::format("gfx/CURSOR%02i.CUR", static_cast<int>(cid));
+
+			Common::File f;
+			Image::IcoCurDecoder decoder;
+			if (!f.open(Common::Path(cursorPath)) || !decoder.open(f))
+				error("Couldn't load cursor %s", cursorPath.c_str());
+
+			uint numItems = decoder.numItems();
+
+			if (numItems < 1)
+				error("Cursor %s had no items", cursorPath.c_str());
+
+			Graphics::Cursor *cursor = decoder.loadItemAsCursor(0);
+			if (!cursor)
+				error("Couldn't load cursor %s", cursorPath.c_str());
+
+			_cursors[cid] = staticCursorToAnimatedCursor(Common::SharedPtr<Graphics::Cursor>(cursor));
 		}
 
-		Common::String nameStr = id.getString();
-		if (nameStr.matchString("CURSOR_#")) {
-			char c = nameStr[7];
-
-			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];
-
-			uint longID = (c1 - '0') * 10 + (c2 - '0');
-			if (longID >= _cursors.size())
-				_cursors.resize(longID + 1);
-			_cursors[longID] = cursorGroup;
+		for (int cid : animatedCursorIDs) {
+			Common::String cursorPath = Common::String::format("gfx/CURSOR%i.ani", static_cast<int>(cid));
+
+			Common::File f;
+			Image::AniDecoder decoder;
+			if (!f.open(Common::Path(cursorPath)) || !decoder.open(f))
+				error("Couldn't load cursor %s", cursorPath.c_str());
+
+			_cursors[cid] = aniFileToAnimatedCursor(decoder);
+		}
+	} else {
+		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 (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.matchString("CURSOR_#")) {
+				char c = nameStr[7];
+
+				uint shortID = c - '0';
+				if (shortID >= _cursorsShort.size())
+					_cursorsShort.resize(shortID + 1);
+				_cursorsShort[shortID] = winCursorGroupToAnimatedCursor(cursorGroup);
+			} else if (nameStr.matchString("CURSOR_CUR_##")) {
+				char c1 = nameStr[11];
+				char c2 = nameStr[12];
+
+				uint longID = (c1 - '0') * 10 + (c2 - '0');
+				if (longID >= _cursors.size())
+					_cursors.resize(longID + 1);
+				_cursors[longID] = winCursorGroupToAnimatedCursor(cursorGroup);
+			}
 		}
 	}
 
@@ -1182,6 +1385,10 @@ void Runtime::setFastAnimationMode(bool fastAnimationMode) {
 	_fastAnimationMode = fastAnimationMode;
 }
 
+void Runtime::setLowQualityGraphicsMode(bool lowQualityGraphicsMode) {
+	_lowQualityGraphicsMode = lowQualityGraphicsMode;
+}
+
 bool Runtime::runFrame() {
 	bool moreActions = true;
 	while (moreActions) {
@@ -1253,6 +1460,8 @@ bool Runtime::runFrame() {
 	updateSounds(timestamp);
 	updateSubtitles();
 
+	refreshCursor(timestamp);
+
 	return true;
 }
 
@@ -1291,18 +1500,27 @@ bool Runtime::bootGame(bool newGame) {
 
 		loadAllSchizmScreenNames();
 		debug(1, "Screen names resolved OK");
-	} else {
+	} else if (_gameID == GID_REAH) {
 		StartConfigDef &startConfig = _startConfigs[kStartConfigInitial];
 		startConfig.disc = 1;
 		startConfig.room = 1;
 		startConfig.screen = 0xb0;
 		startConfig.direction = 0;
-	}
+	} else if (_gameID == GID_AD2044) {
+		StartConfigDef &startConfig = _startConfigs[kStartConfigInitial];
+		startConfig.disc = 1;
+		startConfig.room = 1;
+		startConfig.screen = 5;
+		startConfig.direction = 0;
+	} else
+		error("Don't have a start config for this game");
 
-	_trayBackgroundGraphic = loadGraphic("Pocket", true);
-	_trayHighlightGraphic = loadGraphic("Select", true);
-	_trayCompassGraphic = loadGraphic("Select_1", true);
-	_trayCornerGraphic = loadGraphic("Select_2", true);
+	if (_gameID != GID_AD2044) {
+		_trayBackgroundGraphic = loadGraphic("Pocket", true);
+		_trayHighlightGraphic = loadGraphic("Select", true);
+		_trayCompassGraphic = loadGraphic("Select_1", true);
+		_trayCornerGraphic = loadGraphic("Select_2", true);
+	}
 
 	Common::Language lang = _defaultLanguage;
 
@@ -1394,26 +1612,33 @@ bool Runtime::bootGame(bool newGame) {
 	Common::CodePage codePage = Common::CodePage::kASCII;
 	resolveCodePageForLanguage(lang, codePage, _charSet);
 
-	bool subtitlesLoadedOK = loadSubtitles(codePage, false);
+	bool subtitlesLoadedOK = false;
 
-	if (!subtitlesLoadedOK) {
-		lang = _defaultLanguage;
-		_languageIndex = _defaultLanguageIndex;
-
-		warning("Localization data failed to load, retrying with default language");
-
-		resolveCodePageForLanguage(lang, codePage, _charSet);
+	if (_gameID == GID_AD2044) {
+		subtitlesLoadedOK = true;
+	} else {
 		subtitlesLoadedOK = loadSubtitles(codePage, false);
 
 		if (!subtitlesLoadedOK) {
-			if (_languageIndex != 0) {
-				codePage = Common::CodePage::kWindows1250;
-				_languageIndex = 0;
-				_defaultLanguageIndex = 0;
+			lang = _defaultLanguage;
+			_languageIndex = _defaultLanguageIndex;
+
+			warning("Localization data failed to load, retrying with default language");
 
-				warning("Localization data failed to load again, trying one more time and guessing the encoding");
+			resolveCodePageForLanguage(lang, codePage, _charSet);
 
-				subtitlesLoadedOK = loadSubtitles(codePage, true);
+			subtitlesLoadedOK = loadSubtitles(codePage, false);
+
+			if (!subtitlesLoadedOK) {
+				if (_languageIndex != 0) {
+					codePage = Common::CodePage::kWindows1250;
+					_languageIndex = 0;
+					_defaultLanguageIndex = 0;
+
+					warning("Localization data failed to load again, trying one more time and guessing the encoding");
+
+					subtitlesLoadedOK = loadSubtitles(codePage, true);
+				}
 			}
 		}
 	}
@@ -1425,17 +1650,24 @@ bool Runtime::bootGame(bool newGame) {
 	else
 		warning("Localization data failed to load!  Text and subtitles will be disabled.");
 
-	_uiGraphics.resize(24);
-	for (uint i = 0; i < _uiGraphics.size(); i++) {
-		if (_gameID == GID_REAH) {
-			_uiGraphics[i] = loadGraphic(Common::String::format("Image%03u", static_cast<uint>(_languageIndex * 100u + i)), false);
-			if (_languageIndex != 0 && !_uiGraphics[i])
-				_uiGraphics[i] = loadGraphic(Common::String::format("Image%03u", static_cast<uint>(i)), false);
-		} else if (_gameID == GID_SCHIZM) {
-			_uiGraphics[i] = loadGraphic(Common::String::format("Data%03u", i), false);
+	if (_gameID != GID_AD2044) {
+		_uiGraphics.resize(24);
+		for (uint i = 0; i < _uiGraphics.size(); i++) {
+			if (_gameID == GID_REAH) {
+				_uiGraphics[i] = loadGraphic(Common::String::format("Image%03u", static_cast<uint>(_languageIndex * 100u + i)), false);
+				if (_languageIndex != 0 && !_uiGraphics[i])
+					_uiGraphics[i] = loadGraphic(Common::String::format("Image%03u", static_cast<uint>(i)), false);
+			} else if (_gameID == GID_SCHIZM) {
+				_uiGraphics[i] = loadGraphic(Common::String::format("Data%03u", i), false);
+			}
 		}
 	}
 
+	if (_gameID == GID_AD2044)
+		_mapLoader.reset(new AD2044MapLoader());
+	else
+		_mapLoader.reset(new ReahSchizmMapLoader());
+
 	_gameState = kGameStateIdle;
 
 	if (newGame) {
@@ -1443,7 +1675,9 @@ bool Runtime::bootGame(bool newGame) {
 			_mostRecentValidSaveState = generateNewGameSnapshot();
 			restoreSaveGameSnapshot();
 		} else {
-			changeToScreen(1, 0xb1);
+			uint initialScreen = (_gameID == GID_AD2044) ? 5 : 0xb1;
+
+			changeToScreen(1, initialScreen);
 		}
 	}
 
@@ -2489,6 +2723,11 @@ void Runtime::queueOSEvent(const OSEvent &evt) {
 }
 
 void Runtime::loadIndex() {
+	if (_gameID == GID_AD2044) {
+		// No index
+		return;
+	}
+
 	const char *indexPath = "Log/Index.txt";
 
 	Common::INIFile iniFile;
@@ -2543,19 +2782,47 @@ void Runtime::loadIndex() {
 }
 
 void Runtime::findWaves() {
-	Common::ArchiveMemberList waves;
-	SearchMan.listMatchingMembers(waves, "Sfx/Waves-##/####*.wav", true);
+	if (_gameID == GID_AD2044) {
+		for (int disc = 0; disc < 2; disc++) {
+			for (int cat = 0; cat < 3; cat++) {
+				if (disc == 1 && cat == 0)
+					continue;
+
+				char subdir[3] = {static_cast<char>('0' + disc), static_cast<char>('0' + cat), 0};
+
+				Common::String searchPattern = Common::String("sfx/WAVE-") + subdir + "/########.WAV";
+
+				Common::ArchiveMemberList waves;
+				SearchMan.listMatchingMembers(waves, Common::Path(searchPattern, '/'), true);
+
+				for (const Common::ArchiveMemberPtr &wave : waves) {
+					Common::String name = wave->getFileName();
+
+					// Strip .wav extension
+					name = name.substr(0, name.size() - 4);
+
+					// Make case-insensitive
+					name.toLowercase();
 
-	for (const Common::ArchiveMemberPtr &wave : waves) {
-		Common::String name = wave->getName();
+					_waves[Common::String(subdir) + "-" + name] = wave;
+				}
+			}
+		}
+	} else {
+		Common::ArchiveMemberList waves;
+		SearchMan.listMatchingMembers(waves, "Sfx/Waves-##/####*.wav", true);
 
-		// Strip .wav extension
-		name = name.substr(0, name.size() - 4);
+		for (const Common::ArchiveMemberPtr &wave : waves) {
+			Common::String name = wave->getFileName();
 
-		// Make case-insensitive
-		name.toLowercase();
+			// Strip .wav extension
+			name = name.substr(0, name.size() - 4);
 
-		_waves[name] = wave;
+			// Make case-insensitive
+			name.toLowercase();
+
+			_waves[name] = wave;
+		}
 	}
 }
 
@@ -2882,20 +3149,24 @@ void Runtime::changeToScreen(uint roomNumber, uint screenNumber) {
 			if (logicFile.open(logicFileName)) {
 				_scriptSet = compileReahLogicFile(logicFile, static_cast<uint>(logicFile.size()), logicFileName);
 
+				logicFile.close();
+			}
+		} else if (_gameID == GID_AD2044) {
+			_scriptSet.reset();
+
+			Common::Path logicFileName(Common::String::format("log/kom%i.log", static_cast<int>(roomNumber)));
+			Common::File logicFile;
+			if (logicFile.open(logicFileName)) {
+				_scriptSet = compileAD2044LogicFile(logicFile, static_cast<uint>(logicFile.size()), logicFileName);
+
 				logicFile.close();
 			}
 		} else
 			error("Don't know how to compile scripts for this game");
 
-		_map.clear();
-
-		Common::Path mapFileName(Common::String::format("Map/Room%02i.map", static_cast<int>(roomNumber)));
-		Common::File mapFile;
+		_mapLoader->unload();
 
-		if (mapFile.open(mapFileName)) {
-			loadMap(&mapFile);
-			mapFile.close();
-		}
+		_mapLoader->setRoomNumber(roomNumber);
 	}
 
 	if (changedScreen) {
@@ -3050,17 +3321,65 @@ void Runtime::returnToIdleState() {
 	(void) dischargeIdleMouseMove();
 }
 
-void Runtime::changeToCursor(const Common::SharedPtr<Graphics::WinCursorGroup> &cursor) {
+void Runtime::changeToCursor(const Common::SharedPtr<AnimatedCursor> &cursor) {
 	if (!cursor)
 		CursorMan.showMouse(false);
 	else {
-		CursorMan.replaceCursor(cursor->cursors[0].cursor);
+		_currentAnimatedCursor = cursor.get();
+
+		_cursorCycleLength = 0;
+		for (const AnimatedCursor::FrameDef &frame : cursor->frames)
+			_cursorCycleLength += frame.delay;
+
+		_cursorTimeBase = g_system->getMillis(true);
+
+		refreshCursor(_cursorTimeBase);
 		CursorMan.showMouse(true);
 	}
 }
 
+void Runtime::refreshCursor(uint32 currentTime) {
+	if (!_currentAnimatedCursor)
+		return;
+
+	uint32 timeSinceTimeBase = currentTime - _cursorTimeBase;
+
+	uint stepTime = 0;
+
+	if (_cursorCycleLength > 0) {
+		// 3 ticks at 60Hz is 50ms, so this will reduce the precision of the math that we have to do
+		timeSinceTimeBase %= _cursorCycleLength * 50u;
+		_cursorTimeBase = currentTime - timeSinceTimeBase;
+
+		stepTime = timeSinceTimeBase * 60u / 1000u;
+	}
+
+	uint imageIndex = 0;
+
+	if (_currentAnimatedCursor) {
+		uint frameStartTime = 0;
+		for (const AnimatedCursor::FrameDef &frame : _currentAnimatedCursor->frames) {
+			if (frameStartTime > stepTime)
+				break;
+
+			imageIndex = frame.imageIndex;
+			frameStartTime += frame.delay;
+		}
+	}
+
+	if (imageIndex >= _currentAnimatedCursor->images.size())
+		error("Out-of-bounds animated cursor image index");
+
+	Graphics::Cursor *cursor = _currentAnimatedCursor->images[imageIndex];
+
+	if (!cursor)
+		error("Missing cursor");
+
+	CursorMan.replaceCursor(cursor);
+}
+
 bool Runtime::dischargeIdleMouseMove() {
-	const MapScreenDirectionDef *sdDef = _map.getScreenDirection(_screenNumber, _direction);
+	const MapScreenDirectionDef *sdDef = _mapLoader->getScreenDirection(_screenNumber, _direction);
 
 	if (_inGameMenuState != kInGameMenuStateInvisible) {
 		checkInGameMenuHover();
@@ -3254,55 +3573,6 @@ bool Runtime::dischargeIdleClick() {
 	return false;
 }
 
-void Runtime::loadMap(Common::SeekableReadStream *stream) {
-	byte screenDefOffsets[MapDef::kNumScreens * 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 < kNumDirections; direction++) {
-			uint32 offset = READ_LE_UINT32(screenDefOffsets + (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);
-				}
-
-				// QUIRK: The stone game in the tower in Reah (Room 06) has two 0cb screens and the second one is damaged,
-				// so it must be ignored.
-				if (!_map.screenDirections[screen][direction])
-					_map.screenDirections[screen][direction] = screenDirectionDef;
-			}
-		}
-	}
-}
-
 void Runtime::loadFrameData(Common::SeekableReadStream *stream) {
 	int64 size = stream->size();
 	if (size < 2048 || size > 0xffffffu)
@@ -4373,7 +4643,7 @@ void Runtime::drawDebugOverlay() {
 	uint32 whiteColor = pixFmt.ARGBToColor(255, 255, 255, 255);
 	uint32 blackColor = pixFmt.ARGBToColor(255, 0, 0, 0);
 
-	const MapScreenDirectionDef *sdDef = _map.getScreenDirection(_screenNumber, _direction);
+	const MapScreenDirectionDef *sdDef = _mapLoader->getScreenDirection(_screenNumber, _direction);
 	if (sdDef) {
 		for (const InteractionDef &idef : sdDef->interactions) {
 			Common::Rect rect = idef.rect;
@@ -5305,6 +5575,75 @@ Common::Rect Runtime::padCircuitInteractionRect(const Common::Rect &rect) {
 	return result;
 }
 
+Common::SharedPtr<AnimatedCursor> Runtime::winCursorGroupToAnimatedCursor(const Common::SharedPtr<Graphics::WinCursorGroup> &cursorGroup) {
+	Common::SharedPtr<AnimatedCursor> result(new AnimatedCursor());
+
+	result->cursorGroupKeepAlive = cursorGroup;
+	result->images.push_back(cursorGroup->cursors[0].cursor);
+
+	AnimatedCursor::FrameDef frameDef;
+	frameDef.delay = 1;
+	frameDef.imageIndex = 0;
+
+	result->frames.push_back(frameDef);
+
+	return result;
+}
+
+Common::SharedPtr<AnimatedCursor> Runtime::aniFileToAnimatedCursor(Image::AniDecoder &aniDecoder) {
+	Common::SharedPtr<AnimatedCursor> result(new AnimatedCursor());
+
+	const Image::AniDecoder::Metadata &metadata = aniDecoder.getMetadata();
+
+	if (!metadata.isCURFormat)
+		error("ANI file isn't CUR format");
+
+	for (uint step = 0; step < metadata.numSteps; step++) {
+		const Image::AniDecoder::FrameDef frame = aniDecoder.getSequenceFrame(step);
+
+		AnimatedCursor::FrameDef outFrameDef;
+		outFrameDef.delay = frame.delay;
+		outFrameDef.imageIndex = frame.imageIndex;
+
+		result->frames.push_back(outFrameDef);
+	}
+
+	for (uint frame = 0; frame < metadata.numFrames; frame++) {
+		Common::ScopedPtr<Common::SeekableReadStream> stream(aniDecoder.openImageStream(frame));
+
+		if (!stream)
+			error("Couldn't open animated cursor frame");
+
+		Image::IcoCurDecoder icoCurDecoder;
+		icoCurDecoder.open(*stream);
+
+		Graphics::Cursor *cursor = icoCurDecoder.loadItemAsCursor(0);
+
+		if (!cursor)
+			error("Couldn't load cursor frame");
+
+		result->cursorKeepAlive.push_back(Common::SharedPtr<Graphics::Cursor>(cursor));
+		result->images.push_back(cursor);
+	}
+
+	return result;
+}
+
+Common::SharedPtr<AnimatedCursor> Runtime::staticCursorToAnimatedCursor(const Common::SharedPtr<Graphics::Cursor> &cursor) {
+	Common::SharedPtr<AnimatedCursor> result(new AnimatedCursor());
+
+	result->cursorKeepAlive.push_back(cursor);
+	result->images.push_back(cursor.get());
+
+	AnimatedCursor::FrameDef frameDef;
+	frameDef.delay = 1;
+	frameDef.imageIndex = 0;
+
+	result->frames.push_back(frameDef);
+
+	return result;
+}
+
 void Runtime::onLButtonDown(int16 x, int16 y) {
 	onMouseMove(x, y);
 
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index b94b4538fa0..d554363efae 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -53,6 +53,7 @@ struct PixelFormat;
 struct WinCursorGroup;
 class ManagedSurface;
 class Font;
+class Cursor;
 
 } // End of namespace Graphics
 
@@ -62,6 +63,12 @@ class AVIDecoder;
 
 } // End of namespace Video
 
+namespace Image {
+
+class AniDecoder;
+
+} // End of namespace Image
+
 namespace VCruise {
 
 static const uint kNumDirections = 8;
@@ -143,14 +150,16 @@ struct MapScreenDirectionDef {
 	Common::Array<InteractionDef> interactions;
 };
 
-struct MapDef {
-	static const uint kNumScreens = 96;
-	static const uint kFirstScreen = 0xa0;
+class MapLoader {
+public:
+	virtual ~MapLoader();
 
-	Common::SharedPtr<MapScreenDirectionDef> screenDirections[kNumScreens][kNumDirections];
+	virtual void setRoomNumber(uint roomNumber) = 0;
+	virtual const MapScreenDirectionDef *getScreenDirection(uint screen, uint direction) = 0;
+	virtual void unload() = 0;
 
-	void clear();
-	const MapScreenDirectionDef *getScreenDirection(uint screen, uint direction);
+protected:
+	static Common::SharedPtr<MapScreenDirectionDef> loadScreenDirectionDef(Common::ReadStream &stream);
 };
 
 struct ScriptEnvironmentVars {
@@ -560,6 +569,19 @@ struct FontCacheItem {
 typedef Common::HashMap<Common::String, uint> ScreenNameToRoomMap_t;
 typedef Common::HashMap<uint, ScreenNameToRoomMap_t> RoomToScreenNameToRoomMap_t;
 
+struct AnimatedCursor {
+	struct FrameDef {
+		uint imageIndex;
+		uint delay;
+	};
+
+	Common::Array<FrameDef> frames;
+	Common::Array<Graphics::Cursor *> images;
+
+	Common::Array<Common::SharedPtr<Graphics::Cursor> > cursorKeepAlive;
+	Common::SharedPtr<Graphics::WinCursorGroup> cursorGroupKeepAlive;
+};
+
 class Runtime {
 public:
 	friend class RuntimeMenuInterface;
@@ -581,6 +603,7 @@ public:
 	void loadCursors(const char *exeName);
 	void setDebugMode(bool debugMode);
 	void setFastAnimationMode(bool fastAnimationMode);
+	void setLowQualityGraphicsMode(bool lowQualityGraphicsMode);
 
 	bool runFrame();
 	void drawFrame();
@@ -839,11 +862,11 @@ private:
 	void changeHero();
 	bool triggerPreIdleActions();
 	void returnToIdleState();
-	void changeToCursor(const Common::SharedPtr<Graphics::WinCursorGroup> &cursor);
+	void changeToCursor(const Common::SharedPtr<AnimatedCursor> &cursor);
+	void refreshCursor(uint32 currentTime);
 	bool dischargeIdleMouseMove();
 	bool dischargeIdleMouseDown();
 	bool dischargeIdleClick();
-	void loadMap(Common::SeekableReadStream *stream);
 	void loadFrameData(Common::SeekableReadStream *stream);
 	void loadFrameData2(Common::SeekableReadStream *stream);
 
@@ -925,6 +948,10 @@ private:
 	void drawCircuitHighlightRect(const Common::Rect &rect);
 	static Common::Rect padCircuitInteractionRect(const Common::Rect &rect);
 
+	static Common::SharedPtr<AnimatedCursor> winCursorGroupToAnimatedCursor(const Common::SharedPtr<Graphics::WinCursorGroup> &cursorGroup);
+	static Common::SharedPtr<AnimatedCursor> aniFileToAnimatedCursor(Image::AniDecoder &aniDecoder);
+	static Common::SharedPtr<AnimatedCursor> staticCursorToAnimatedCursor(const Common::SharedPtr<Graphics::Cursor> &cursor);
+
 	// Script things
 	void scriptOpNumber(ScriptArg_t arg);
 	void scriptOpRotate(ScriptArg_t arg);
@@ -1109,8 +1136,31 @@ private:
 	void scriptOpFn(ScriptArg_t arg);
 	void scriptOpItemHighlightSetTrue(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_#
+	// AD2044 ops
+	void scriptOpAnimT(ScriptArg_t arg);
+	void scriptOpAnimForward(ScriptArg_t arg);
+	void scriptOpAnimReverse(ScriptArg_t arg);
+	void scriptOpAnimKForward(ScriptArg_t arg);
+	void scriptOpNoUpdate(ScriptArg_t arg);
+	void scriptOpNoClear(ScriptArg_t arg);
+	void scriptOpSay1_AD2044(ScriptArg_t arg);
+	void scriptOpSay2_AD2044(ScriptArg_t arg);
+	void scriptOpSay1Rnd(ScriptArg_t arg);
+	void scriptOpM(ScriptArg_t arg);
+	void scriptOpEM(ScriptArg_t arg);
+	void scriptOpSE(ScriptArg_t arg);
+	void scriptOpSDot(ScriptArg_t arg);
+	void scriptOpE(ScriptArg_t arg);
+	void scriptOpDot(ScriptArg_t arg);
+	void scriptOpSound(ScriptArg_t arg);
+	void scriptOpISound(ScriptArg_t arg);
+	void scriptOpUSound(ScriptArg_t arg);
+	void scriptOpSay2K(ScriptArg_t arg);
+	void scriptOpSay3K(ScriptArg_t arg);
+	void scriptOpRGet(ScriptArg_t arg);
+
+	Common::Array<Common::SharedPtr<AnimatedCursor> > _cursors;      // Cursors indexed as CURSOR_CUR_##
+	Common::Array<Common::SharedPtr<AnimatedCursor> > _cursorsShort;      // Cursors indexed as CURSOR_#
 
 	InventoryItem _inventory[kNumInventorySlots];
 
@@ -1192,6 +1242,7 @@ private:
 	bool _escOn;
 	bool _debugMode;
 	bool _fastAnimationMode;
+	bool _lowQualityGraphicsMode;
 
 	VCruiseGameID _gameID;
 
@@ -1271,7 +1322,7 @@ private:
 
 	Audio::Mixer *_mixer;
 
-	MapDef _map;
+	Common::SharedPtr<MapLoader> _mapLoader;
 
 	RenderSection _gameSection;
 	RenderSection _gameDebugBackBuffer;
@@ -1357,6 +1408,11 @@ private:
 
 	Common::Array<Common::SharedPtr<FontCacheItem> > _fontCache;
 
+	AnimatedCursor *_currentAnimatedCursor;
+	Graphics::Cursor *_currentCursor;
+	uint32 _cursorTimeBase;
+	uint32 _cursorCycleLength;
+
 	int32 _dbToVolume[49];
 };
 
diff --git a/engines/vcruise/runtime_scriptexec.cpp b/engines/vcruise/runtime_scriptexec.cpp
index a3d2ff062aa..69dbca7462f 100644
--- a/engines/vcruise/runtime_scriptexec.cpp
+++ b/engines/vcruise/runtime_scriptexec.cpp
@@ -2041,6 +2041,30 @@ void Runtime::scriptOpPuzzleDone(ScriptArg_t arg) {
 	_circuitPuzzle.reset();
 }
 
+// AD2044 ops
+OPCODE_STUB(AnimT)
+OPCODE_STUB(AnimForward)
+OPCODE_STUB(AnimReverse)
+OPCODE_STUB(AnimKForward)
+OPCODE_STUB(Say2K)
+OPCODE_STUB(Say3K)
+OPCODE_STUB(NoUpdate)
+OPCODE_STUB(NoClear)
+
+OPCODE_STUB(Say1_AD2044)
+OPCODE_STUB(Say2_AD2044)
+OPCODE_STUB(Say1Rnd)
+OPCODE_STUB(M)
+OPCODE_STUB(EM)
+OPCODE_STUB(SE)
+OPCODE_STUB(SDot)
+OPCODE_STUB(E)
+OPCODE_STUB(Dot)
+OPCODE_STUB(Sound)
+OPCODE_STUB(ISound)
+OPCODE_STUB(USound)
+OPCODE_STUB(RGet)
+
 // Only used in fnRandomBirds and fnRandomMachines in Room 60, both of which are unused
 OPCODE_STUB(SndAddRandom)
 OPCODE_STUB(SndClearRandom)
@@ -2109,6 +2133,7 @@ bool Runtime::runScript() {
 			DISPATCH_OP(AnimN);
 			DISPATCH_OP(AnimG);
 			DISPATCH_OP(AnimS);
+			DISPATCH_OP(AnimT);
 			DISPATCH_OP(Anim);
 
 			DISPATCH_OP(Static);
@@ -2274,6 +2299,12 @@ bool Runtime::runScript() {
 			DISPATCH_OP(Fn);
 			DISPATCH_OP(ItemHighlightSetTrue);
 
+			DISPATCH_OP(AnimForward);
+			DISPATCH_OP(AnimReverse);
+			DISPATCH_OP(AnimKForward);
+			DISPATCH_OP(NoUpdate);
+			DISPATCH_OP(NoClear);
+
 		default:
 			error("Unimplemented opcode %i", static_cast<int>(instr.op));
 		}
diff --git a/engines/vcruise/script.cpp b/engines/vcruise/script.cpp
index f86fe71f0b9..92d6abdbd71 100644
--- a/engines/vcruise/script.cpp
+++ b/engines/vcruise/script.cpp
@@ -32,6 +32,7 @@ namespace VCruise {
 enum ScriptDialect {
 	kScriptDialectReah,
 	kScriptDialectSchizm,
+	kScriptDialectAD2044,
 };
 
 class LogicUnscrambleStream : public Common::ReadStream {
@@ -162,7 +163,7 @@ private:
 	void expectNumber(uint32 &outNumber);
 
 	void compileRoomScriptSet(RoomScriptSet *rss);
-	void compileReahScreenScriptSet(ScreenScriptSet *sss);
+	void compileReahOrAD2044ScreenScriptSet(ScreenScriptSet *sss);
 	void compileSchizmScreenScriptSet(ScreenScriptSet *sss);
 	void compileFunction(Script *script);
 	bool compileInstructionToken(ProtoScript &script, const Common::String &token);
@@ -235,10 +236,10 @@ bool ScriptCompiler::parseNumber(const Common::String &token, uint32 &outNumber)
 	if (token.size() == 0)
 		return false;
 
-	if (_dialect == kScriptDialectReah) {
-		if (token[0] == 'd')
-			return parseDecNumber(token, 1, outNumber);
+	if (_dialect == kScriptDialectReah && token[0] == 'd')
+		return parseDecNumber(token, 1, outNumber);
 
+	if (_dialect == kScriptDialectReah || _dialect == kScriptDialectAD2044) {
 		if (token[0] == '0') {
 			switch (_numberParsingMode) {
 			case kNumberParsingDec:
@@ -349,7 +350,7 @@ void ScriptCompiler::compileScriptSet(ScriptSet *ss) {
 
 	const char *roomToken = nullptr;
 
-	if (_dialect == kScriptDialectReah) {
+	if (_dialect == kScriptDialectReah || _dialect == kScriptDialectAD2044) {
 		roomToken = "~ROOM";
 		_eroomToken = "~EROOM";
 		_scrToken = "~SCR";
@@ -408,10 +409,9 @@ void ScriptCompiler::compileRoomScriptSet(RoomScriptSet *rss) {
 			expectNumber(screenNumber);
 
 			Common::SharedPtr<ScreenScriptSet> sss(new ScreenScriptSet());
-			if (_dialect == kScriptDialectReah)
-				compileReahScreenScriptSet(sss.get());
+			if (_dialect == kScriptDialectReah || _dialect == kScriptDialectAD2044)
+				compileReahOrAD2044ScreenScriptSet(sss.get());
 			else if (_dialect == kScriptDialectSchizm) {
-
 				if (!_parser.parseToken(token, state))
 					error("Error compiling script at line %i col %i: Expected screen name", static_cast<int>(state._lineNum), static_cast<int>(state._col));
 
@@ -475,7 +475,7 @@ void ScriptCompiler::compileRoomScriptSet(RoomScriptSet *rss) {
 	error("Error compiling script: Room wasn't terminated");
 }
 
-void ScriptCompiler::compileReahScreenScriptSet(ScreenScriptSet *sss) {
+void ScriptCompiler::compileReahOrAD2044ScreenScriptSet(ScreenScriptSet *sss) {
 	TextParserState state;
 	Common::String token;
 
@@ -506,7 +506,7 @@ void ScriptCompiler::compileReahScreenScriptSet(ScreenScriptSet *sss) {
 			_numberParsingMode = kNumberParsingHex;
 		} else if (token == "BIN") {
 			_numberParsingMode = kNumberParsingBin;
-		} else if (token == "dubbing") {
+		} else if (_dialect == kScriptDialectReah && token == "dubbing") {
 			Common::String dubbingName;
 			_parser.expectToken(dubbingName, _blamePath);
 			protoScript.instrs.push_back(ProtoInstruction(ScriptOps::kDubbing, indexString(dubbingName)));
@@ -571,6 +571,145 @@ void ScriptCompiler::compileFunction(Script *script) {
 	}
 }
 
+static ScriptNamedInstruction g_ad2044NamedInstructions[] = {
+	//{"rotate", ProtoOp::kProtoOpScript, ScriptOps::kRotate},
+	//{"angle", ProtoOp::kProtoOpScript, ScriptOps::kAngle},
+	//{"angleG@", ProtoOp::kProtoOpScript, ScriptOps::kAngleGGet},
+	//{"speed", ProtoOp::kProtoOpScript, ScriptOps::kSpeed},
+	//{"sanimL", ProtoOp::kProtoOpScript, ScriptOps::kSAnimL},
+	//{"changeL", ProtoOp::kProtoOpScript, ScriptOps::kChangeL},
+	//{"changeL1", ProtoOp::kProtoOpScript, ScriptOps::kChangeL}, // This seems wrong, but not sure what changeL1 does differently from changeL yet
+	//{"animF", ProtoOp::kProtoOpScript, ScriptOps::kAnimF},
+	//{"animG", ProtoOp::kProtoOpScript, ScriptOps::kAnimG},
+	//{"animN", ProtoOp::kProtoOpScript, ScriptOps::kAnimN},
+	//{"animR", ProtoOp::kProtoOpScript, ScriptOps::kAnimR},
+	//{"animS", ProtoOp::kProtoOpScript, ScriptOps::kAnimS},
+	//{"anim", ProtoOp::kProtoOpScript, ScriptOps::kAnim},
+	{"animT", ProtoOp::kProtoOpScript, ScriptOps::kAnimT},
+	{"ani+", ProtoOp::kProtoOpScript, ScriptOps::kAnimForward},
+	{"ani-", ProtoOp::kProtoOpScript, ScriptOps::kAnimReverse},
+	{"kani+", ProtoOp::kProtoOpScript, ScriptOps::kAnimForward},
+	//{"static", ProtoOp::kProtoOpScript, ScriptOps::kStatic},
+	{"yes@", ProtoOp::kProtoOpScript, ScriptOps::kVarLoad},
+	{"yes!", ProtoOp::kProtoOpScript, ScriptOps::kVarStore},
+	{"yesg@", ProtoOp::kProtoOpScript, ScriptOps::kVarGlobalLoad},
+	{"yesg!", ProtoOp::kProtoOpScript, ScriptOps::kVarGlobalStore},
+	//{"setaX+!", ProtoOp::kProtoOpScript, ScriptOps::kVarAddAndStore},
+	//{"cr?", ProtoOp::kProtoOpScript, ScriptOps::kItemCheck},
+	//{"cr!", ProtoOp::kProtoOpScript, ScriptOps::kItemRemove},
+	//{"sr!", ProtoOp::kProtoOpScript, ScriptOps::kItemHighlightSet},
+	//{"r?", ProtoOp::kProtoOpScript, ScriptOps::kItemHaveSpace},
+	//{"r!", ProtoOp::kProtoOpScript, ScriptOps::kItemAdd},
+	{"r@", ProtoOp::kProtoOpScript, ScriptOps::kRGet},
+	//{"clearPocket", ProtoOp::kProtoOpScript, ScriptOps::kItemClear},
+	{"cursor!", ProtoOp::kProtoOpScript, ScriptOps::kSetCursor},
+	{"room!", ProtoOp::kProtoOpScript, ScriptOps::kSetRoom},
+	{"lmb", ProtoOp::kProtoOpScript, ScriptOps::kLMB},
+	//{"lmb1", ProtoOp::kProtoOpScript, ScriptOps::kLMB1},
+	//{"volumeDn2", ProtoOp::kProtoOpScript, ScriptOps::kVolumeDn2},
+	//{"volumeDn3", ProtoOp::kProtoOpScript, ScriptOps::kVolumeDn3},
+	//{"volumeDn4", ProtoOp::kProtoOpScript, ScriptOps::kVolumeDn4},
+	//{"volumeUp3", ProtoOp::kProtoOpScript, ScriptOps::kVolumeUp3},
+	{"rnd", ProtoOp::kProtoOpScript, ScriptOps::kRandom},
+	//{"drop", ProtoOp::kProtoOpScript, ScriptOps::kDrop},
+	//{"dup", ProtoOp::kProtoOpScript, ScriptOps::kDup},
+	//{"swap", ProtoOp::kProtoOpScript, ScriptOps::kSwap},
+	//{"say1", ProtoOp::kProtoOpScript, ScriptOps::kSay1},
+	//{"say2", ProtoOp::kProtoOpScript, ScriptOps::kSay2},
+	//{"say3", ProtoOp::kProtoOpScript, ScriptOps::kSay3},
+	//{"say3@", ProtoOp::kProtoOpScript, ScriptOps::kSay3Get},
+	{"say2k", ProtoOp::kProtoOpScript, ScriptOps::kSay2K},
+	{"say3k", ProtoOp::kProtoOpScript, ScriptOps::kSay3K},
+	//{"setTimer", ProtoOp::kProtoOpScript, ScriptOps::kSetTimer},
+	//{"getTimer", ProtoOp::kProtoOpScript, ScriptOps::kGetTimer},
+	//{"delay", ProtoOp::kProtoOpScript, ScriptOps::kDelay},
+	//{"lo!", ProtoOp::kProtoOpScript, ScriptOps::kLoSet},
+	//{"lo@", ProtoOp::kProtoOpScript, ScriptOps::kLoGet},
+	//{"hi!", ProtoOp::kProtoOpScript, ScriptOps::kHiSet},
+	//{"hi@", ProtoOp::kProtoOpScript, ScriptOps::kHiGet},
+
+	{"and", ProtoOp::kProtoOpScript, ScriptOps::kAnd},
+	{"or", ProtoOp::kProtoOpScript, ScriptOps::kOr},
+	{"+", ProtoOp::kProtoOpScript, ScriptOps::kAdd},
+	{"-", ProtoOp::kProtoOpScript, ScriptOps::kSub},
+	{"not", ProtoOp::kProtoOpScript, ScriptOps::kNot},
+	{"=", ProtoOp::kProtoOpScript, ScriptOps::kCmpEq},
+	{">", ProtoOp::kProtoOpScript, ScriptOps::kCmpGt},
+	{"<", ProtoOp::kProtoOpScript, ScriptOps::kCmpLt},
+
+	//{"bit@", ProtoOp::kProtoOpScript, ScriptOps::kBitLoad},
+	//{"bit0!", ProtoOp::kProtoOpScript, ScriptOps::kBitSet0},
+	//{"bit1!", ProtoOp::kProtoOpScript, ScriptOps::kBitSet1},
+
+	//{"soundS1", ProtoOp::kProtoOpScript, ScriptOps::kSoundS1},
+	//{"soundS2", ProtoOp::kProtoOpScript, ScriptOps::kSoundS2},
+	//{"soundS3", ProtoOp::kProtoOpScript, ScriptOps::kSoundS3},
+	//{"soundL1", ProtoOp::kProtoOpScript, ScriptOps::kSoundL1},
+	//{"soundL2", ProtoOp::kProtoOpScript, ScriptOps::kSoundL2},
+	//{"soundL3", ProtoOp::kProtoOpScript, ScriptOps::kSoundL3},
+	//{"3DsoundS2", ProtoOp::kProtoOpScript, ScriptOps::k3DSoundS2},
+	//{"3DsoundL2", ProtoOp::kProtoOpScript, ScriptOps::k3DSoundL2},
+	//{"3DsoundL3", ProtoOp::kProtoOpScript, ScriptOps::k3DSoundL3},
+	//{"stopaL", ProtoOp::kProtoOpScript, ScriptOps::kStopAL},
+	//{"range", ProtoOp::kProtoOpScript, ScriptOps::kRange},
+	//{"addXsound", ProtoOp::kProtoOpScript, ScriptOps::kAddXSound},
+	//{"clrXsound", ProtoOp::kProtoOpScript, ScriptOps::kClrXSound},
+	//{"stopSndLA", ProtoOp::kProtoOpScript, ScriptOps::kStopSndLA},
+	//{"stopSndLO", ProtoOp::kProtoOpScript, ScriptOps::kStopSndLO},
+
+	//{"music", ProtoOp::kProtoOpScript, ScriptOps::kMusic},
+	//{"musicUp", ProtoOp::kProtoOpScript, ScriptOps::kMusicVolRamp},
+	//{"musicDn", ProtoOp::kProtoOpScript, ScriptOps::kMusicVolRamp},
+
+	//{"parm0", ProtoOp::kProtoOpScript, ScriptOps::kParm0},
+	//{"parm1", ProtoOp::kProtoOpScript, ScriptOps::kParm1},
+	//{"parm2", ProtoOp::kProtoOpScript, ScriptOps::kParm2},
+	//{"parm3", ProtoOp::kProtoOpScript, ScriptOps::kParm3},
+	//{"parmG", ProtoOp::kProtoOpScript, ScriptOps::kParmG},
+	//{"sparmX", ProtoOp::kProtoOpScript, ScriptOps::kSParmX},
+	//{"sanimX", ProtoOp::kProtoOpScript, ScriptOps::kSAnimX},
+
+	//{"disc1", ProtoOp::kProtoOpScript, ScriptOps::kDisc1},
+	//{"disc2", ProtoOp::kProtoOpScript, ScriptOps::kDisc2},
+	//{"disc3", ProtoOp::kProtoOpScript, ScriptOps::kDisc3},
+
+	//{"goto", ProtoOp::kProtoOpScript, ScriptOps::kGoto},
+
+	{"#if", ProtoOp::kProtoOpIf, ScriptOps::kInvalid},
+	{"#eif", ProtoOp::kProtoOpEndIf, ScriptOps::kInvalid},
+	{"#else", ProtoOp::kProtoOpElse, ScriptOps::kInvalid},
+
+	{"#switch:", ProtoOp::kProtoOpSwitch, ScriptOps::kInvalid},
+	{"#eswitch", ProtoOp::kProtoOpEndSwitch, ScriptOps::kInvalid},
+	{"break", ProtoOp::kProtoOpBreak, ScriptOps::kInvalid},
+	{"#default", ProtoOp::kProtoOpDefault, ScriptOps::kInvalid},
+
+	//{"esc_on", ProtoOp::kProtoOpScript, ScriptOps::kEscOn},
+	//{"esc_off", ProtoOp::kProtoOpScript, ScriptOps::kEscOff},
+	//{"esc_get@", ProtoOp::kProtoOpScript, ScriptOps::kEscGet},
+	//{"backStart", ProtoOp::kProtoOpScript, ScriptOps::kBackStart},
+	//{"saveAs", ProtoOp::kProtoOpScript, ScriptOps::kSaveAs},
+	//{"save0", ProtoOp::kProtoOpNoop, ScriptOps::kSave0},
+	//{"exit", ProtoOp::kProtoOpScript, ScriptOps::kExit},
+	//{"allowedSave", ProtoOp::kProtoOpScript, ScriptOps::kAllowSaves},
+
+	{"NO_UPDATE", ProtoOp::kProtoOpScript, ScriptOps::kNoUpdate},
+	{"no_clear", ProtoOp::kProtoOpScript, ScriptOps::kNoClear},
+	{"#M\"", ProtoOp::kProtoOpScript, ScriptOps::kM},
+	{"#EM", ProtoOp::kProtoOpScript, ScriptOps::kEM},
+	{"e\"", ProtoOp::kProtoOpScript, ScriptOps::kE},
+	{"se\"", ProtoOp::kProtoOpScript, ScriptOps::kSE},
+	{".\"", ProtoOp::kProtoOpScript, ScriptOps::kDot},
+	{"s.\"", ProtoOp::kProtoOpScript, ScriptOps::kSDot},
+	{"say1", ProtoOp::kProtoOpScript, ScriptOps::kSay1_AD2044},
+	{"say2", ProtoOp::kProtoOpScript, ScriptOps::kSay2_AD2044},
+	{"say1rnd", ProtoOp::kProtoOpScript, ScriptOps::kSay1Rnd},
+	{"sound", ProtoOp::kProtoOpScript, ScriptOps::kSound},
+	{"isound", ProtoOp::kProtoOpScript, ScriptOps::kISound},
+	{"usound", ProtoOp::kProtoOpScript, ScriptOps::kUSound},
+	{"r@", ProtoOp::kProtoOpScript, ScriptOps::kRGet},
+};
+
 static ScriptNamedInstruction g_reahNamedInstructions[] = {
 	{"rotate", ProtoOp::kProtoOpScript, ScriptOps::kRotate},
 	{"angle", ProtoOp::kProtoOpScript, ScriptOps::kAngle},
@@ -688,8 +827,6 @@ static ScriptNamedInstruction g_reahNamedInstructions[] = {
 	{"allowedSave", ProtoOp::kProtoOpScript, ScriptOps::kAllowSaves},
 };
 
-
-
 static ScriptNamedInstruction g_schizmNamedInstructions[] = {
 	{"StopScore", ProtoOp::kProtoOpScript, ScriptOps::kMusicStop},
 	{"PlayScore", ProtoOp::kProtoOpScript, ScriptOps::kMusicPlayScore},
@@ -863,7 +1000,7 @@ bool ScriptCompiler::compileInstructionToken(ProtoScript &script, const Common::
 		return true;
 	}
 
-	if (_dialect == kScriptDialectReah) {
+	if (_dialect == kScriptDialectReah || _dialect == kScriptDialectAD2044) {
 		if (token.hasPrefix("CUR_")) {
 			script.instrs.push_back(ProtoInstruction(ScriptOps::kCursorName, indexString(token)));
 			return true;
@@ -893,7 +1030,38 @@ bool ScriptCompiler::compileInstructionToken(ProtoScript &script, const Common::
 		return true;
 	}
 
-	if (_dialect == kScriptDialectReah) {
+	if (_dialect == kScriptDialectAD2044) {
+		for (const ScriptNamedInstruction &namedInstr : g_ad2044NamedInstructions) {
+			if (token == namedInstr.str) {
+				int32 paramArg = 0;
+
+				if (token.hasSuffix("\"")) {
+					TextParserState state;
+
+					char c = 0;
+					if (!_parser.skipWhitespaceAndComments(c, state))
+						error("Error compiling script at line %i col %i: Unterminated string", static_cast<int>(state._lineNum), static_cast<int>(state._col));
+
+					Common::String str;
+					for (;;) {
+						if (c == '\"')
+							break;
+
+						str += c;
+
+						if (!_parser.readOneChar(c, state)) {
+							error("Error compiling script at line %i col %i: Unterminated string", static_cast<int>(state._lineNum), static_cast<int>(state._col));
+						}
+					}
+
+					paramArg = indexString(str);
+				}
+
+				script.instrs.push_back(ProtoInstruction(namedInstr.protoOp, namedInstr.op, paramArg));
+				return true;
+			}
+		}
+	} else if (_dialect == kScriptDialectReah) {
 		for (const ScriptNamedInstruction &namedInstr : g_reahNamedInstructions) {
 			if (token == namedInstr.str) {
 				script.instrs.push_back(ProtoInstruction(namedInstr.protoOp, namedInstr.op, 0));
@@ -1375,6 +1543,13 @@ Common::SharedPtr<ScriptSet> compileReahLogicFile(Common::ReadStream &stream, ui
 	return scriptSet;
 }
 
+Common::SharedPtr<ScriptSet> compileAD2044LogicFile(Common::ReadStream &stream, uint streamSize, const Common::Path &blamePath) {
+	Common::SharedPtr<ScriptSet> scriptSet(new ScriptSet());
+
+	compileLogicFile(*scriptSet, stream, streamSize, blamePath, kScriptDialectAD2044, 0, 0, nullptr);
+	return scriptSet;
+}
+
 void compileSchizmLogicFile(ScriptSet &scriptSet, uint loadAsRoom, uint fileRoom, Common::ReadStream &stream, uint streamSize, const Common::Path &blamePath, IScriptCompilerGlobalState *gs) {
 	compileLogicFile(scriptSet, stream, streamSize, blamePath, kScriptDialectSchizm, loadAsRoom, fileRoom, gs);
 }
diff --git a/engines/vcruise/script.h b/engines/vcruise/script.h
index 39ff3dadcf1..a758183d59c 100644
--- a/engines/vcruise/script.h
+++ b/engines/vcruise/script.h
@@ -219,6 +219,29 @@ enum ScriptOp {
 	kFn,
 	kItemHighlightSetTrue,
 
+	// AD2044 ops
+	kAnimT,
+	kAnimForward,
+	kAnimReverse,
+	kAnimKForward,
+	kNoUpdate,
+	kNoClear,
+	kSay1_AD2044,
+	kSay2_AD2044,
+	kSay1Rnd,
+	kM,
+	kEM,
+	kSE,
+	kSDot,
+	kE,
+	kDot,
+	kSound,
+	kISound,
+	kUSound,
+	kSay2K,
+	kSay3K,
+	kRGet,
+
 	kNumOps,
 };
 
@@ -285,6 +308,8 @@ struct IScriptCompilerGlobalState {
 
 Common::SharedPtr<IScriptCompilerGlobalState> createScriptCompilerGlobalState();
 Common::SharedPtr<ScriptSet> compileReahLogicFile(Common::ReadStream &stream, uint streamSize, const Common::Path &blamePath);
+Common::SharedPtr<ScriptSet> compileAD2044LogicFile(Common::ReadStream &stream, uint streamSize, const Common::Path &blamePath);
+
 void compileSchizmLogicFile(ScriptSet &scriptSet, uint loadAsRoom, uint fileRoom, Common::ReadStream &stream, uint streamSize, const Common::Path &blamePath, IScriptCompilerGlobalState *gs);
 bool checkSchizmLogicForDuplicatedRoom(Common::ReadStream &stream, uint streamSize);
 void optimizeScriptSet(ScriptSet &scriptSet);
diff --git a/engines/vcruise/vcruise.cpp b/engines/vcruise/vcruise.cpp
index e9546bd73f5..809eceeb546 100644
--- a/engines/vcruise/vcruise.cpp
+++ b/engines/vcruise/vcruise.cpp
@@ -135,37 +135,51 @@ Common::Error VCruiseEngine::run() {
 	// Figure out screen layout
 	Common::Point size;
 
-	Common::Point videoSize;
-	Common::Point traySize;
-	Common::Point menuBarSize;
-
-	if (_gameDescription->gameID == GID_REAH) {
-		videoSize = Common::Point(608, 348);
-		menuBarSize = Common::Point(640, 44);
-		traySize = Common::Point(640, 88);
-	} else if (_gameDescription->gameID == GID_SCHIZM) {
-		videoSize = Common::Point(640, 360);
-		menuBarSize = Common::Point(640, 32);
-		traySize = Common::Point(640, 88);
+	if (_gameDescription->gameID == GID_AD2044) {
+		size = Common::Point(640, 480);
+
+		Common::Point traySize = Common::Point(640, 97);
+		Common::Point menuBarSize = Common::Point(188, 102);
+		Common::Point videoTL = Common::Point(20, 21);
+
+		Common::Point videoSize = Common::Point(432, 307);
+
+		_menuBarRect = Common::Rect(size.x - menuBarSize.x, 0, size.x, menuBarSize.y);
+		_videoRect = Common::Rect(videoTL, videoTL + videoSize);
+		_trayRect = Common::Rect(0, size.y - traySize.y, size.x, size.y);
 	} else {
-		error("Unknown game");
-	}
+		Common::Point videoSize;
+		Common::Point traySize;
+		Common::Point menuBarSize;
+
+		if (_gameDescription->gameID == GID_REAH) {
+			videoSize = Common::Point(608, 348);
+			menuBarSize = Common::Point(640, 44);
+			traySize = Common::Point(640, 88);
+		} else if (_gameDescription->gameID == GID_SCHIZM) {
+			videoSize = Common::Point(640, 360);
+			menuBarSize = Common::Point(640, 32);
+			traySize = Common::Point(640, 88);
+		} 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.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;
+		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 + menuBarSize.y);
-	Common::Point trayTL = Common::Point((size.x - traySize.x) / 2, videoTL.y + videoSize.y);
+		Common::Point menuTL = Common::Point((size.x - menuBarSize.x) / 2, 0);
+		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);
-	_trayRect = Common::Rect(trayTL.x, trayTL.y, trayTL.x + traySize.x, trayTL.y + traySize.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);
@@ -196,6 +210,10 @@ Common::Error VCruiseEngine::run() {
 		_runtime->setFastAnimationMode(true);
 	}
 
+	if (ConfMan.getBool("vcruise_use_4bit")) {
+		_runtime->setLowQualityGraphicsMode(true);
+	}
+
 	if (ConfMan.hasKey("save_slot")) {
 		int saveSlot = ConfMan.getInt("save_slot");
 		if (saveSlot >= 0) {
@@ -215,7 +233,7 @@ Common::Error VCruiseEngine::run() {
 			break;
 
 		_runtime->drawFrame();
-		_system->delayMillis(10);
+		_system->delayMillis(5);
 	}
 
 	_runtime.reset();


Commit: 9db748a3f9459a841df08e2d17301d25f45e1b3c
    https://github.com/scummvm/scummvm/commit/9db748a3f9459a841df08e2d17301d25f45e1b3c
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2024-03-31T14:39:28-04:00

Commit Message:
VCRUISE: More AD2044 loading stuff

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


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index a31a331d2c8..82881769f07 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -331,7 +331,6 @@ public:
 private:
 	void load();
 
-	static const uint kNumScreens = 96;
 	static const uint kFirstScreen = 0xa0;
 
 	uint _roomNumber;
@@ -367,7 +366,15 @@ void AD2044MapLoader::load() {
 	// This is loaded even if the open fails
 	_isLoaded = true;
 
-	Common::Path mapFileName(Common::String::format("map/SCR%i.MAP", static_cast<int>(_roomNumber * 100u + _screenNumber)));
+	if (_screenNumber < kFirstScreen)
+		return;
+
+	uint adjustedScreenNumber = _screenNumber - kFirstScreen;
+
+	if (adjustedScreenNumber > 99)
+		return;
+
+	Common::Path mapFileName(Common::String::format("map/SCR%i.MAP", static_cast<int>(_roomNumber * 100u + adjustedScreenNumber)));
 	Common::File mapFile;
 
 	if (!mapFile.open(mapFileName))
@@ -840,6 +847,9 @@ FrameData::FrameData() : areaID{0, 0, 0, 0}, areaFrameIndex(0), frameIndex(0), f
 FrameData2::FrameData2() : x(0), y(0), angle(0), frameNumberInArea(0), unknown(0) {
 }
 
+AnimFrameRange::AnimFrameRange() : animationNum(0), firstFrame(0), lastFrame(0) {
+}
+
 SoundParams3D::SoundParams3D() : minRange(0), maxRange(0), unknownRange(0) {
 }
 
@@ -1361,6 +1371,11 @@ void Runtime::loadCursors(const char *exeName) {
 		_namedCursors["curDrop"] = 91;
 	}
 
+	if (_gameID == GID_AD2044) {
+		_namedCursors["CUR_PRZOD"] = 2; // Przod = forward
+		_namedCursors["CUR_PRAWO"] = 3;	// Prawo = right
+	}
+
 	_panCursors[kPanCursorDraggableHoriz | kPanCursorDraggableUp] = 2;
 	_panCursors[kPanCursorDraggableHoriz | kPanCursorDraggableDown] = 3;
 	_panCursors[kPanCursorDraggableHoriz] = 4;
@@ -1477,7 +1492,12 @@ bool Runtime::bootGame(bool newGame) {
 		_musicMute = false;
 
 	debug(1, "Booting V-Cruise game...");
-	loadIndex();
+
+	if (_gameID == GID_AD2044)
+		loadAD2044Index();
+	else
+		loadReahSchizmIndex();
+
 	debug(1, "Index loaded OK");
 	findWaves();
 	debug(1, "Waves indexed OK");
@@ -1510,7 +1530,7 @@ bool Runtime::bootGame(bool newGame) {
 		StartConfigDef &startConfig = _startConfigs[kStartConfigInitial];
 		startConfig.disc = 1;
 		startConfig.room = 1;
-		startConfig.screen = 5;
+		startConfig.screen = 0xa5;
 		startConfig.direction = 0;
 	} else
 		error("Don't have a start config for this game");
@@ -1671,13 +1691,11 @@ bool Runtime::bootGame(bool newGame) {
 	_gameState = kGameStateIdle;
 
 	if (newGame) {
-		if (ConfMan.hasKey("vcruise_skip_menu") && ConfMan.getBool("vcruise_skip_menu")) {
+		if (_gameID == GID_AD2044 || (ConfMan.hasKey("vcruise_skip_menu") && ConfMan.getBool("vcruise_skip_menu"))) {
 			_mostRecentValidSaveState = generateNewGameSnapshot();
 			restoreSaveGameSnapshot();
 		} else {
-			uint initialScreen = (_gameID == GID_AD2044) ? 5 : 0xb1;
-
-			changeToScreen(1, initialScreen);
+			changeToScreen(1, 0xb1);
 		}
 	}
 
@@ -2722,12 +2740,7 @@ void Runtime::queueOSEvent(const OSEvent &evt) {
 	_pendingEvents.push_back(timedEvt);
 }
 
-void Runtime::loadIndex() {
-	if (_gameID == GID_AD2044) {
-		// No index
-		return;
-	}
-
+void Runtime::loadReahSchizmIndex() {
 	const char *indexPath = "Log/Index.txt";
 
 	Common::INIFile iniFile;
@@ -2781,6 +2794,65 @@ void Runtime::loadIndex() {
 	}
 }
 
+void Runtime::loadAD2044Index() {
+	const byte searchPattern[] = {0x01, 0x01, 0xa1, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc7, 0x00, 0xc7, 0x00, 0x00, 0x00};
+
+	Common::File f;
+	if (!f.open("ad2044.exe") || f.size() > 2u * 1024u * 1024u)
+		error("Couldn't open ad2044.exe to read animation index");
+
+	uint32 exeSize = static_cast<uint32>(f.size());
+
+	Common::Array<byte> exeContents;
+	exeContents.resize(exeSize);
+
+	if (exeSize < sizeof(searchPattern) || f.read(&exeContents[0], exeSize) != exeSize)
+		error("Couldn't load executable to scan for animation table");
+
+	uint endPoint = exeSize - sizeof(searchPattern);
+
+	bool foundAnimTable = false;
+	uint32 animTablePos = 0;
+
+	for (uint i = 0; i <= endPoint; i++) {
+		bool match = true;
+		for (uint j = 0; j < sizeof(searchPattern); j++) {
+			if (exeContents[i + j] != searchPattern[j]) {
+				match = false;
+				break;
+			}
+		}
+
+		if (match) {
+			if (foundAnimTable)
+				error("Found multiple byte patterns matching animation table in ad2044.exe");
+
+			foundAnimTable = true;
+			animTablePos = i;
+		}
+	}
+
+	if (!foundAnimTable)
+		error("Couldn't find animation table in ad2044.exe");
+
+	// Parse the animation table.  The unparsed values are the same for all entries.
+	for (uint entryPos = animTablePos; entryPos < exeSize - 15u; entryPos += 16) {
+		const byte *entry = &exeContents[entryPos];
+
+		AD2044AnimationDef animDef;
+		animDef.roomID = entry[1];
+		animDef.lookupID = entry[2];
+
+		animDef.fwdAnimationID = READ_LE_INT16(entry + 10);
+		animDef.revAnimationID = READ_LE_INT16(entry + 12);
+
+		if (animDef.lookupID == 0)	// Terminator
+			break;
+
+		_ad2044AnimationDefs.push_back(animDef);
+	}
+}
+
 void Runtime::findWaves() {
 	if (_gameID == GID_AD2044) {
 		for (int disc = 0; disc < 2; disc++) {
@@ -3600,13 +3672,28 @@ void Runtime::loadFrameData(Common::SeekableReadStream *stream) {
 		char decAreaFrameIndex[4];
 		memcpy(decAreaFrameIndex, frameData + 12, 4);
 
+		bool isPadFrame = false;
+
+		if (_gameID == GID_AD2044) {
+			isPadFrame = true;
+
+			for (uint j = 0; j < 8; j++) {
+				if (frameData[j + 8] != 0) {
+					isPadFrame = false;
+					break;
+				}
+			}
+		}
+
 		uint areaFrameIndex = 0;
-		for (int digit = 0; digit < 4; digit++) {
-			char c = decAreaFrameIndex[digit];
-			if (c < '0' || c > '9')
-				error("Invalid area frame index in DTA data");
+		if (!isPadFrame) {
+			for (int digit = 0; digit < 4; digit++) {
+				char c = decAreaFrameIndex[digit];
+				if (c < '0' || c > '9')
+					error("Invalid area frame index in DTA data");
 
-			areaFrameIndex = areaFrameIndex * 10u + static_cast<uint>(c - '0');
+				areaFrameIndex = areaFrameIndex * 10u + static_cast<uint>(c - '0');
+			}
 		}
 
 		fd.areaFrameIndex = areaFrameIndex;
@@ -3642,6 +3729,99 @@ void Runtime::loadFrameData2(Common::SeekableReadStream *stream) {
 	}
 }
 
+void Runtime::loadTabData(uint animNumber, Common::SeekableReadStream *stream) {
+	int64 size64 = stream->size() - stream->pos();
+
+	if (size64 > UINT_MAX || size64 < 0)
+		error("Internal error: Oversized TAB file");
+
+	uint32 fileSize = static_cast<uint32>(size64);
+
+	Common::Array<Common::String> lines;
+
+	Common::Array<char> chars;
+	chars.resize(fileSize);
+
+	if (fileSize > 0 && stream->read(&chars[0], fileSize) != fileSize)
+		error("Failed to read TAB file data");
+
+	uint searchStart = 0;
+	while (searchStart < chars.size()) {
+		uint endPos = searchStart;
+		while (endPos != chars.size()) {
+			if (chars[endPos] == '\n' || chars[endPos] == '\r')
+				break;
+
+			endPos++;
+		}
+
+		if (endPos == searchStart)
+			lines.push_back(Common::String(""));
+		else
+			lines.push_back(Common::String(&chars[searchStart], endPos - searchStart));
+
+
+		if (endPos != chars.size() && chars[endPos] == '\r')
+			endPos++;
+		if (endPos != chars.size() && chars[endPos] == '\n')
+			endPos++;
+
+		searchStart = endPos;
+	}
+
+	for (const Common::String &line : lines) {
+		if (line.hasPrefix("//"))
+			continue;
+
+		uint32 openBracePos = line.find('{');
+		if (openBracePos == Common::String::npos)
+			continue;
+
+		uint32 closeBracePos = line.find("},", openBracePos + 1);
+		if (closeBracePos == Common::String::npos)
+			error("Strangely-formatted animation table line: %s", line.c_str());
+
+		Common::String enclosedContents = line.substr(1, closeBracePos - 1);
+
+		uint32 firstCommaPos = enclosedContents.find(',');
+		if (firstCommaPos == Common::String::npos)
+			error("Strangely-formatted animation table line: %s", line.c_str());
+
+		uint32 secondCommaPos = enclosedContents.find(',', firstCommaPos + 1);
+		if (secondCommaPos == Common::String::npos)
+			error("Strangely-formatted animation table line: %s", line.c_str());
+
+		Common::String numbers[3] = {
+			enclosedContents.substr(0, firstCommaPos),
+			enclosedContents.substr(firstCommaPos + 1, secondCommaPos - (firstCommaPos + 1)),
+			enclosedContents.substr(secondCommaPos + 1)
+		};
+
+		int parsedNumbers[3] = {0, 0, 0};
+
+		uint i = 0;
+		for (Common::String &str : numbers) {
+			str.trim();
+
+			if (sscanf(str.c_str(), "%i", &parsedNumbers[i]) != 1)
+				error("Strangely-formatted animation table line: %s", line.c_str());
+
+			i++;
+		}
+
+		AnimFrameRange frameRange;
+		frameRange.animationNum = animNumber;
+		frameRange.firstFrame = static_cast<uint>(parsedNumbers[1]);
+		frameRange.lastFrame = static_cast<uint>(parsedNumbers[2]);
+
+		// Animation ID 9099 is duplicated but it doesn't really matter since the duplicate is identical
+		if (_animIDToFrameRange.find(parsedNumbers[0]) == _animIDToFrameRange.end())
+			_animIDToFrameRange[parsedNumbers[0]] = frameRange;
+		else
+			warning("Animation ID %i was duplicated", parsedNumbers[0]);
+	}
+}
+
 void Runtime::changeMusicTrack(int track) {
 	if (track == _musicTrack && _musicPlayer.get() != nullptr)
 		return;
@@ -3757,6 +3937,8 @@ void Runtime::changeAnimation(const AnimationDef &animDef, uint initialFrame, bo
 	if (animFile < 0)
 		animFile = -animFile;
 
+	bool isAD2044 = (_gameID == GID_AD2044);
+
 	if (_loadedAnimation != static_cast<uint>(animFile)) {
 		_loadedAnimation = animFile;
 		_frameData.clear();
@@ -3764,7 +3946,7 @@ void Runtime::changeAnimation(const AnimationDef &animDef, uint initialFrame, bo
 		_animDecoder.reset();
 		_animDecoderState = kAnimDecoderStateStopped;
 
-		Common::Path aviFileName(Common::String::format("Anims/Anim%04i.avi", animFile));
+		Common::Path aviFileName(Common::String::format(isAD2044 ? "anims/ANIM%04i.AVI" : "Anims/Anim%04i.avi", animFile));
 		Common::File *aviFile = new Common::File();
 
 		if (aviFile->open(aviFileName)) {
@@ -3780,28 +3962,43 @@ void Runtime::changeAnimation(const AnimationDef &animDef, uint initialFrame, bo
 
 		applyAnimationVolume();
 
-		Common::Path sfxFileName(Common::String::format("Sfx/Anim%04i.sfx", animFile));
-		Common::File sfxFile;
-
 		_sfxData.reset();
 
-		if (sfxFile.open(sfxFileName))
-			_sfxData.load(sfxFile, _mixer);
-		sfxFile.close();
+		if (!isAD2044) {
+			Common::Path sfxFileName(Common::String::format("Sfx/Anim%04i.sfx", animFile));
+			Common::File sfxFile;
+
+			if (sfxFile.open(sfxFileName))
+				_sfxData.load(sfxFile, _mixer);
+			sfxFile.close();
+		}
 
-		Common::Path dtaFileName(Common::String::format("Anims/Anim%04i.dta", animFile));
+		Common::Path dtaFileName(Common::String::format(isAD2044 ? "anims/ANIM0001.DTA" : "Anims/Anim%04i.dta", animFile));
 		Common::File dtaFile;
 
 		if (dtaFile.open(dtaFileName))
 			loadFrameData(&dtaFile);
 		dtaFile.close();
 
-		Common::Path twoDtFileName(Common::String::format("Dta/Anim%04i.2dt", animFile));
-		Common::File twoDtFile;
+		if (!isAD2044) {
+			Common::Path twoDtFileName(Common::String::format("Dta/Anim%04i.2dt", animFile));
+			Common::File twoDtFile;
 
-		if (twoDtFile.open(twoDtFileName))
-			loadFrameData2(&twoDtFile);
-		twoDtFile.close();
+			if (twoDtFile.open(twoDtFileName))
+				loadFrameData2(&twoDtFile);
+			twoDtFile.close();
+		}
+
+		if (isAD2044) {
+			_animIDToFrameRange.clear();
+
+			Common::Path tabFileName(Common::String::format("anims/ANIM%04i.TAB", animFile));
+			Common::File tabFile;
+
+			if (tabFile.open(tabFileName))
+				loadTabData(animFile, &tabFile);
+			tabFile.close();
+		}
 
 		_loadedAnimationHasSound = (_animDecoder->getAudioTrackCount() > 0);
 
@@ -4862,6 +5059,9 @@ void Runtime::drawCompass() {
 	if (!isTrayVisible())
 		return;
 
+	if (_gameID == GID_AD2044)
+		return;
+
 	bool haveHorizontalRotate = false;
 	bool haveUp = false;
 	bool haveDown = false;
@@ -5224,6 +5424,10 @@ void Runtime::changeToMenuPage(MenuPage *menuPage) {
 }
 
 void Runtime::checkInGameMenuHover() {
+	// TODO
+	if (_gameID == GID_AD2044)
+		return;
+
 	if (_inGameMenuState == kInGameMenuStateInvisible) {
 		if (_menuSection.rect.contains(_mousePos) && _isInGame) {
 			// Figure out what elements should be visible
@@ -5981,6 +6185,12 @@ Common::SharedPtr<SaveGameSnapshot> Runtime::generateNewGameSnapshot() const {
 	} else
 		mainState->loadedAnimation = 1;
 
+	// AD2044 new game normally loads a pre-packaged save.  Unlike Reah and Schizm,
+	// it doesn't appear to have a startup script, so we need to set up everything
+	// that it needs here.
+	if (_gameID == GID_AD2044)
+		mainState->animDisplayingFrame = 345;
+
 	return snapshot;
 }
 
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index d554363efae..2909ee5063b 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -116,6 +116,13 @@ enum GameState {
 	kGameStateMenu,
 };
 
+struct AD2044AnimationDef {
+	byte roomID;
+	byte lookupID;
+	short fwdAnimationID;
+	short revAnimationID;
+};
+
 struct AnimationDef {
 	AnimationDef();
 
@@ -370,6 +377,14 @@ struct FrameData2 {
 	uint16 unknown;	// Subarea or something?
 };
 
+struct AnimFrameRange {
+	AnimFrameRange();
+
+	uint animationNum;
+	uint firstFrame;
+	uint lastFrame;	// Inclusive
+};
+
 struct InventoryItem {
 	InventoryItem();
 
@@ -845,7 +860,8 @@ private:
 
 	void processUniversalKeymappedEvents(KeymappedEvent evt);
 
-	void loadIndex();
+	void loadReahSchizmIndex();
+	void loadAD2044Index();
 	void findWaves();
 	void loadConfig(const char *cfgPath);
 	void loadScore();
@@ -869,6 +885,7 @@ private:
 	bool dischargeIdleClick();
 	void loadFrameData(Common::SeekableReadStream *stream);
 	void loadFrameData2(Common::SeekableReadStream *stream);
+	void loadTabData(uint animNumber, Common::SeekableReadStream *stream);
 
 	void changeMusicTrack(int musicID);
 	void startScoreSection();
@@ -1250,6 +1267,7 @@ private:
 	Common::Array<uint> _roomDuplicationOffsets;
 	RoomToScreenNameToRoomMap_t _globalRoomScreenNameToScreenIDs;
 	Common::SharedPtr<ScriptSet> _scriptSet;
+	Common::Array<AD2044AnimationDef> _ad2044AnimationDefs;
 
 	Common::Array<CallStackFrame> _scriptCallStack;
 
@@ -1299,9 +1317,13 @@ private:
 	Common::Array<FrameData2> _frameData2;
 	//uint32 _loadedArea;
 
+	// Reah/Schizm animation map
 	Common::Array<Common::String> _animDefNames;
 	Common::HashMap<Common::String, uint> _animDefNameToIndex;
 
+	// AD2044 animation map
+	Common::HashMap<int, AnimFrameRange> _animIDToFrameRange;
+
 	bool _idleLockInteractions;
 	bool _idleIsOnInteraction;
 	bool _idleHaveClickInteraction;
diff --git a/engines/vcruise/runtime_scriptexec.cpp b/engines/vcruise/runtime_scriptexec.cpp
index 69dbca7462f..0e094b9554a 100644
--- a/engines/vcruise/runtime_scriptexec.cpp
+++ b/engines/vcruise/runtime_scriptexec.cpp
@@ -2043,7 +2043,13 @@ void Runtime::scriptOpPuzzleDone(ScriptArg_t arg) {
 
 // AD2044 ops
 OPCODE_STUB(AnimT)
-OPCODE_STUB(AnimForward)
+
+void Runtime::scriptOpAnimForward(ScriptArg_t arg) {
+	TAKE_STACK_INT(2);
+
+	error("AnimForward NYI");
+}
+
 OPCODE_STUB(AnimReverse)
 OPCODE_STUB(AnimKForward)
 OPCODE_STUB(Say2K)
@@ -2305,6 +2311,12 @@ bool Runtime::runScript() {
 			DISPATCH_OP(NoUpdate);
 			DISPATCH_OP(NoClear);
 
+			DISPATCH_OP(M);
+			DISPATCH_OP(EM);
+			DISPATCH_OP(SE);
+			DISPATCH_OP(SDot);
+			DISPATCH_OP(E);
+
 		default:
 			error("Unimplemented opcode %i", static_cast<int>(instr.op));
 		}


Commit: b7f0a753ca3aa9b1cf5025c24ad62f77f7b48866
    https://github.com/scummvm/scummvm/commit/b7f0a753ca3aa9b1cf5025c24ad62f77f7b48866
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2024-03-31T14:39:28-04:00

Commit Message:
VCRUISE: Add #M and #EM opcodes

Changed paths:
    engines/vcruise/runtime_scriptexec.cpp


diff --git a/engines/vcruise/runtime_scriptexec.cpp b/engines/vcruise/runtime_scriptexec.cpp
index 0e094b9554a..2a1957a49cb 100644
--- a/engines/vcruise/runtime_scriptexec.cpp
+++ b/engines/vcruise/runtime_scriptexec.cpp
@@ -2060,8 +2060,17 @@ OPCODE_STUB(NoClear)
 OPCODE_STUB(Say1_AD2044)
 OPCODE_STUB(Say2_AD2044)
 OPCODE_STUB(Say1Rnd)
-OPCODE_STUB(M)
-OPCODE_STUB(EM)
+
+void Runtime::scriptOpM(ScriptArg_t arg) {
+	// Looks like this is possibly support to present a mouse click prompt and end
+	// with the #EM instruction, but so far as best I can tell, it just stops
+	// execution.
+	scriptOpLMB(arg);
+}
+
+void Runtime::scriptOpEM(ScriptArg_t arg) {
+}
+
 OPCODE_STUB(SE)
 OPCODE_STUB(SDot)
 OPCODE_STUB(E)
@@ -2071,6 +2080,8 @@ OPCODE_STUB(ISound)
 OPCODE_STUB(USound)
 OPCODE_STUB(RGet)
 
+
+// Unused Schizm ops
 // Only used in fnRandomBirds and fnRandomMachines in Room 60, both of which are unused
 OPCODE_STUB(SndAddRandom)
 OPCODE_STUB(SndClearRandom)


Commit: e26ec842f931b5f413628409dae525fd9b1e1126
    https://github.com/scummvm/scummvm/commit/e26ec842f931b5f413628409dae525fd9b1e1126
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2024-03-31T14:39:28-04:00

Commit Message:
VCRUISE: Add CUR_LUPA binding for AD2044

Changed paths:
    engines/vcruise/runtime.cpp


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 82881769f07..c152f38138b 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -1374,6 +1374,7 @@ void Runtime::loadCursors(const char *exeName) {
 	if (_gameID == GID_AD2044) {
 		_namedCursors["CUR_PRZOD"] = 2; // Przod = forward
 		_namedCursors["CUR_PRAWO"] = 3;	// Prawo = right
+		_namedCursors["CUR_LUPA"] = 6; // Lupa = magnifier
 	}
 
 	_panCursors[kPanCursorDraggableHoriz | kPanCursorDraggableUp] = 2;


Commit: cd2e9aa37f5f1c8a70ac6ec01d29e62a2428a4e6
    https://github.com/scummvm/scummvm/commit/cd2e9aa37f5f1c8a70ac6ec01d29e62a2428a4e6
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2024-03-31T14:39:28-04:00

Commit Message:
VCRUISE: Load AD2044 font. Fix cursor animations.

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


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index c152f38138b..d432da4b081 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -318,6 +318,8 @@ void ReahSchizmMapLoader::unload() {
 	for (uint screen = 0; screen < kNumScreens; screen++)
 		for (uint direction = 0; direction < kNumDirections; direction++)
 			_screenDirections[screen][direction].reset();
+
+	_isLoaded = false;
 }
 
 class AD2044MapLoader : public MapLoader {
@@ -385,6 +387,7 @@ void AD2044MapLoader::load() {
 
 void AD2044MapLoader::unload() {
 	_currentMap.reset();
+	_isLoaded = false;
 }
 
 
@@ -1250,7 +1253,13 @@ Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &roo
 	_rng.reset(new Common::RandomSource("vcruise"));
 
 #ifdef USE_FREETYPE2
-	_subtitleFontKeepalive.reset(Graphics::loadTTFFontFromArchive("NotoSans-Regular.ttf", 16, Graphics::kTTFSizeModeCharacter, 0, 0, Graphics::kTTFRenderModeLight));
+	if (_gameID == GID_AD2044) {
+		Common::File f;
+		if (f.open("gfx/AD2044.TTF"))
+			_subtitleFontKeepalive.reset(Graphics::loadTTFFont(f, 16, Graphics::kTTFSizeModeCharacter, 0, 0, Graphics::kTTFRenderModeLight));
+	} else
+		_subtitleFontKeepalive.reset(Graphics::loadTTFFontFromArchive("NotoSans-Regular.ttf", 16, Graphics::kTTFSizeModeCharacter, 0, 0, Graphics::kTTFRenderModeLight));
+
 	_subtitleFont = _subtitleFontKeepalive.get();
 #endif
 
@@ -1374,6 +1383,7 @@ void Runtime::loadCursors(const char *exeName) {
 	if (_gameID == GID_AD2044) {
 		_namedCursors["CUR_PRZOD"] = 2; // Przod = forward
 		_namedCursors["CUR_PRAWO"] = 3;	// Prawo = right
+		_namedCursors["CUR_LEWO"] = 1; // Lewo = left
 		_namedCursors["CUR_LUPA"] = 6; // Lupa = magnifier
 	}
 
@@ -3424,7 +3434,7 @@ void Runtime::refreshCursor(uint32 currentTime) {
 		timeSinceTimeBase %= _cursorCycleLength * 50u;
 		_cursorTimeBase = currentTime - timeSinceTimeBase;
 
-		stepTime = timeSinceTimeBase * 60u / 1000u;
+		stepTime = (timeSinceTimeBase * 60u / 1000u) % _cursorCycleLength;
 	}
 
 	uint imageIndex = 0;
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index 2909ee5063b..89d72a1ccad 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -1154,6 +1154,7 @@ private:
 	void scriptOpItemHighlightSetTrue(ScriptArg_t arg);
 
 	// AD2044 ops
+	void scriptOpAnimAD2044(bool isForward);
 	void scriptOpAnimT(ScriptArg_t arg);
 	void scriptOpAnimForward(ScriptArg_t arg);
 	void scriptOpAnimReverse(ScriptArg_t arg);
@@ -1388,6 +1389,7 @@ private:
 	static const uint kAnimDefStackArgs = 8;
 
 	static const uint kCursorArrow = 0;
+	static const uint kCursorWait = 29;
 
 	static const int kPanoramaPanningMarginX = 11;
 	static const int kPanoramaPanningMarginY = 11;
diff --git a/engines/vcruise/runtime_scriptexec.cpp b/engines/vcruise/runtime_scriptexec.cpp
index 2a1957a49cb..3cbd969b53e 100644
--- a/engines/vcruise/runtime_scriptexec.cpp
+++ b/engines/vcruise/runtime_scriptexec.cpp
@@ -2044,17 +2044,63 @@ void Runtime::scriptOpPuzzleDone(ScriptArg_t arg) {
 // AD2044 ops
 OPCODE_STUB(AnimT)
 
-void Runtime::scriptOpAnimForward(ScriptArg_t arg) {
+void Runtime::scriptOpAnimAD2044(bool isForward) {
 	TAKE_STACK_INT(2);
 
-	error("AnimForward NYI");
+	int16 animationID = 0;
+
+	bool found = false;
+
+	for (const AD2044AnimationDef &def : _ad2044AnimationDefs) {
+		if (static_cast<StackInt_t>(def.lookupID) == stackArgs[0]) {
+			animationID = isForward ? def.fwdAnimationID : def.revAnimationID;
+			found = true;
+			break;
+		}
+	}
+
+	if (!found)
+		error("Couldn't resolve animation lookup ID %i", static_cast<int>(stackArgs[0]));
+
+	Common::HashMap<int, AnimFrameRange>::const_iterator animRangeIt = _animIDToFrameRange.find(animationID);
+	if (animRangeIt == _animIDToFrameRange.end())
+		error("Couldn't resolve animation ID %i", static_cast<int>(animationID));
+
+	AnimationDef animDef;
+	animDef.animNum = animRangeIt->_value.animationNum;
+	animDef.firstFrame = animRangeIt->_value.firstFrame;
+	animDef.lastFrame = animRangeIt->_value.lastFrame;
+
+	changeAnimation(animDef, animDef.firstFrame, true, _animSpeedDefault);
+
+	_gameState = kGameStateWaitingForAnimation;
+	_screenNumber = stackArgs[1];
+	_havePendingScreenChange = true;
+
+	clearIdleAnimations();
+
+	if (_loadedAnimationHasSound)
+		changeToCursor(nullptr);
+	else {
+		changeToCursor(_cursors[kCursorWait]);
+	}
+}
+
+void Runtime::scriptOpAnimForward(ScriptArg_t arg) {
+	scriptOpAnimAD2044(true);
+}
+
+void Runtime::scriptOpAnimReverse(ScriptArg_t arg) {
+	scriptOpAnimAD2044(false);
 }
 
-OPCODE_STUB(AnimReverse)
 OPCODE_STUB(AnimKForward)
 OPCODE_STUB(Say2K)
 OPCODE_STUB(Say3K)
-OPCODE_STUB(NoUpdate)
+
+void Runtime::scriptOpNoUpdate(ScriptArg_t arg) {
+}
+
 OPCODE_STUB(NoClear)
 
 OPCODE_STUB(Say1_AD2044)


Commit: 3ccaa683cdaae11db02e22087fdb7390281378d3
    https://github.com/scummvm/scummvm/commit/3ccaa683cdaae11db02e22087fdb7390281378d3
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2024-03-31T14:39:28-04:00

Commit Message:
VCRUISE: Fix AD2044 fullscreen UI

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 d432da4b081..17f584d6471 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -1553,6 +1553,9 @@ bool Runtime::bootGame(bool newGame) {
 		_trayCornerGraphic = loadGraphic("Select_2", true);
 	}
 
+	if (_gameID == GID_AD2044)
+		_backgroundGraphic = loadGraphicFromPath("SCR0.BMP", true);
+
 	Common::Language lang = _defaultLanguage;
 
 	if (ConfMan.hasKey("language")) {
@@ -5002,7 +5005,11 @@ void Runtime::inventoryRemoveItem(uint itemID) {
 }
 
 void Runtime::clearScreen() {
-	_system->fillScreen(_system->getScreenFormat().RGBToColor(0, 0, 0));
+	if (_gameID == GID_AD2044) {
+		_fullscreenMenuSection.surf->blitFrom(*_backgroundGraphic);
+		commitSectionToScreen(_fullscreenMenuSection, _fullscreenMenuSection.rect);
+	} else
+		_system->fillScreen(_system->getScreenFormat().RGBToColor(0, 0, 0));
 }
 
 void Runtime::redrawTray() {
@@ -5018,10 +5025,17 @@ void Runtime::redrawTray() {
 }
 
 void Runtime::clearTray() {
-	uint32 blackColor = _traySection.surf->format.RGBToColor(0, 0, 0);
-	Common::Rect trayRect(0, 0, _traySection.surf->w, _traySection.surf->h);
+	Common::Rect trayRect;
+	if (_gameID == GID_AD2044) {
+		trayRect = _traySection.rect;
+		trayRect.translate(-trayRect.left, -trayRect.top);
+		_traySection.surf->blitFrom(*_backgroundGraphic, _traySection.rect, trayRect);
+	} else {
+		uint32 blackColor = _traySection.surf->format.RGBToColor(0, 0, 0);
+		trayRect = Common::Rect(0, 0, _traySection.surf->w, _traySection.surf->h);
 
-	_traySection.surf->fillRect(trayRect, blackColor);
+		_traySection.surf->fillRect(trayRect, blackColor);
+	}
 
 	this->commitSectionToScreen(_traySection, trayRect);
 }
@@ -5030,6 +5044,9 @@ void Runtime::drawInventory(uint slot) {
 	if (!isTrayVisible())
 		return;
 
+	if (_gameID == GID_AD2044)
+		return;
+
 	Common::Rect trayRect = _traySection.rect;
 	trayRect.translate(-trayRect.left, -trayRect.top);
 
@@ -5196,6 +5213,10 @@ Common::SharedPtr<Graphics::Surface> Runtime::loadGraphic(const Common::String &
 	filePath.appendInPlace(graphicName);
 	filePath.appendInPlace(".bmp");
 
+	return loadGraphicFromPath(filePath, required);
+}
+
+Common::SharedPtr<Graphics::Surface> Runtime::loadGraphicFromPath(const Common::Path &filePath, bool required) {
 	Common::File f;
 	if (!f.open(filePath)) {
 		warning("Couldn't open BMP file '%s'", filePath.toString(Common::Path::kNativeSeparator).c_str());
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index 89d72a1ccad..a1d53e9899a 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -947,6 +947,7 @@ private:
 
 	Common::String getFileNameForItemGraphic(uint itemID) const;
 	Common::SharedPtr<Graphics::Surface> loadGraphic(const Common::String &graphicName, bool required);
+	Common::SharedPtr<Graphics::Surface> loadGraphicFromPath(const Common::Path &path, bool required);
 
 	bool loadSubtitles(Common::CodePage codePage, bool guessCodePage);
 
@@ -1186,6 +1187,7 @@ private:
 	Common::SharedPtr<Graphics::Surface> _trayBackgroundGraphic;
 	Common::SharedPtr<Graphics::Surface> _trayHighlightGraphic;
 	Common::SharedPtr<Graphics::Surface> _trayCornerGraphic;
+	Common::SharedPtr<Graphics::Surface> _backgroundGraphic;
 
 	Common::Array<Common::SharedPtr<Graphics::Surface> > _uiGraphics;
 
diff --git a/engines/vcruise/vcruise.cpp b/engines/vcruise/vcruise.cpp
index 809eceeb546..3967182491e 100644
--- a/engines/vcruise/vcruise.cpp
+++ b/engines/vcruise/vcruise.cpp
@@ -80,6 +80,7 @@ Common::Error VCruiseEngine::run() {
 
 #if !defined(USE_JPEG)
 	if (_gameDescription->desc.flags & VCRUISE_GF_NEED_JPEG) {
+		.
 		return Common::Error(Common::kUnknownError, _s("This game requires JPEG support, which was not compiled in."));
 	}
 #endif


Commit: 09a7b1fb5f23c0f8373b0cfadb31b2ff8df00215
    https://github.com/scummvm/scummvm/commit/09a7b1fb5f23c0f8373b0cfadb31b2ff8df00215
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2024-03-31T14:39:28-04:00

Commit Message:
VCRUISE: Add AD2044 subtitle rendering

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


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 17f584d6471..ba545b739db 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -1256,7 +1256,7 @@ Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &roo
 	if (_gameID == GID_AD2044) {
 		Common::File f;
 		if (f.open("gfx/AD2044.TTF"))
-			_subtitleFontKeepalive.reset(Graphics::loadTTFFont(f, 16, Graphics::kTTFSizeModeCharacter, 0, 0, Graphics::kTTFRenderModeLight));
+			_subtitleFontKeepalive.reset(Graphics::loadTTFFont(f, 16, Graphics::kTTFSizeModeCharacter, 108, 72, Graphics::kTTFRenderModeLight));
 	} else
 		_subtitleFontKeepalive.reset(Graphics::loadTTFFontFromArchive("NotoSans-Regular.ttf", 16, Graphics::kTTFSizeModeCharacter, 0, 0, Graphics::kTTFRenderModeLight));
 
@@ -1278,11 +1278,14 @@ Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &roo
 Runtime::~Runtime() {
 }
 
-void Runtime::initSections(const Common::Rect &gameRect, const Common::Rect &menuRect, const Common::Rect &trayRect, const Common::Rect &fullscreenMenuRect, const Graphics::PixelFormat &pixFmt) {
+void Runtime::initSections(const Common::Rect &gameRect, const Common::Rect &menuRect, const Common::Rect &trayRect, const Common::Rect &subtitleRect, const Common::Rect &fullscreenMenuRect, const Graphics::PixelFormat &pixFmt) {
 	_gameSection.init(gameRect, pixFmt);
 	_menuSection.init(menuRect, pixFmt);
 	_traySection.init(trayRect, pixFmt);
 	_fullscreenMenuSection.init(fullscreenMenuRect, pixFmt);
+
+	if (!subtitleRect.isEmpty())
+		_subtitleSection.init(subtitleRect, pixFmt);
 }
 
 void Runtime::loadCursors(const char *exeName) {
@@ -3537,6 +3540,11 @@ bool Runtime::dischargeIdleMouseMove() {
 		_idleHaveDragInteraction = false;
 		changeToCursor(_cursors[kCursorArrow]);
 		resetInventoryHighlights();
+
+		if (_gameID == GID_AD2044 && _tooltipText.size() > 0) {
+			_tooltipText.clear();
+			redrawSubtitleSection();
+		}
 	}
 
 	bool changedCircuitState = false;
@@ -4234,7 +4242,11 @@ void Runtime::stopSubtitles() {
 	_subtitleQueue.clear();
 	_isDisplayingSubtitles = false;
 	_isSubtitleSourceAnimation = false;
-	redrawTray();
+
+	if (_gameID == GID_AD2044)
+		redrawSubtitleSection();
+	else
+		redrawTray();
 }
 
 void Runtime::stopSound(SoundInstance &sound) {
@@ -4397,17 +4409,24 @@ void Runtime::updateSubtitles() {
 				_isDisplayingSubtitles = false;
 
 				if (_subtitleQueue.size() == 0) {
+					// Queue was exhausted
+
 					// Is this really what we want to be doing?
-					if (_escOn)
-						clearTray();
-					else
-						redrawTray();
+					if (_escOn) {
+						if (_gameID == GID_AD2044)
+							clearSubtitleSection();
+						else
+							clearTray();
+					} else {
+						if (_gameID == GID_AD2044)
+							redrawSubtitleSection();
+						else
+							redrawTray();
+					}
 				}
 			} else
 				break;
 		} else {
-			Graphics::ManagedSurface *surf = _traySection.surf.get();
-
 			Common::Array<Common::U32String> lines;
 
 			uint lineStart = 0;
@@ -4422,23 +4441,12 @@ void Runtime::updateSubtitles() {
 				lineStart = lineEnd + 1;
 			}
 
-			clearTray();
-
-			if (_subtitleFont) {
-				int lineHeight = _subtitleFont->getFontHeight();
-
-				int topY = (surf->h - lineHeight * static_cast<int>(lines.size())) / 2;
-
-				uint32 textColor = surf->format.RGBToColor(queueItem.color[0], queueItem.color[1], queueItem.color[2]);
-
-				for (uint lineIndex = 0; lineIndex < lines.size(); lineIndex++) {
-					const Common::U32String &line = lines[lineIndex];
-					int lineWidth = _subtitleFont->getStringWidth(line);
-					_subtitleFont->drawString(surf, line, (surf->w - lineWidth) / 2, topY + static_cast<int>(lineIndex) * lineHeight, lineWidth, textColor);
-				}
-			}
+			if (_gameID == GID_AD2044)
+				clearSubtitleSection();
+			else
+				clearTray();
 
-			commitSectionToScreen(_traySection, Common::Rect(0, 0, _traySection.rect.width(), _traySection.rect.height()));
+			drawSubtitleText(lines, queueItem.color);
 
 			_isDisplayingSubtitles = true;
 		}
@@ -5040,6 +5048,79 @@ void Runtime::clearTray() {
 	this->commitSectionToScreen(_traySection, trayRect);
 }
 
+void Runtime::redrawSubtitleSection() {
+	if (_subtitleQueue.size() != 0)
+		return;
+
+	clearSubtitleSection();
+
+	if (!_tooltipText.empty()) {
+		Common::CodePage codePage = Common::kWindows1250;
+
+		Common::Array<Common::U32String> lines;
+
+		uint32 lastStringStart = 0;
+		for (;;) {
+			uint32 backslashPos = _tooltipText.find('\\', lastStringStart);
+			if (backslashPos == Common::String::npos)
+				break;
+
+			Common::String slice = _tooltipText.substr(lastStringStart, backslashPos - lastStringStart);
+			lines.push_back(slice.decode(codePage));
+			lastStringStart = backslashPos + 1;
+		}
+
+		Common::String lastSlice = _tooltipText.substr(lastStringStart, _tooltipText.size() - lastStringStart);
+		lines.push_back(lastSlice.decode(codePage));
+
+		uint8 color[3] = {255, 255, 0};
+		drawSubtitleText(lines, color);
+	}
+}
+
+void Runtime::clearSubtitleSection() {
+	Common::Rect stRect;
+	if (_gameID == GID_AD2044) {
+		stRect = _subtitleSection.rect;
+		stRect.translate(-stRect.left, -stRect.top);
+		_subtitleSection.surf->blitFrom(*_backgroundGraphic, _subtitleSection.rect, stRect);
+	}
+
+	this->commitSectionToScreen(_subtitleSection, stRect);
+}
+
+void Runtime::drawSubtitleText(const Common::Array<Common::U32String> &lines, const uint8 (&color)[3]) {
+	RenderSection &stSection = (_gameID == GID_AD2044) ? _subtitleSection : _traySection;
+	Graphics::ManagedSurface *surf = stSection.surf.get();
+
+	if (_subtitleFont) {
+		int lineHeight = _subtitleFont->getFontHeight();
+
+		int xOffset = 0;
+		int topY = 0;
+		if (_gameID == GID_AD2044) {
+			topY = 13;
+			xOffset = 5;
+		} else
+			topY = (surf->h - lineHeight * static_cast<int>(lines.size())) / 2;
+
+		uint32 textColor = surf->format.RGBToColor(color[0], color[1], color[2]);
+
+		for (uint lineIndex = 0; lineIndex < lines.size(); lineIndex++) {
+			const Common::U32String &line = lines[lineIndex];
+			int lineWidth = _subtitleFont->getStringWidth(line);
+
+			int xPos = (surf->w - lineWidth) / 2 + xOffset;
+			int yPos = topY + static_cast<int>(lineIndex) * lineHeight;
+
+			_subtitleFont->drawString(surf, line, xPos + 2, yPos + 2, lineWidth, 0);
+			_subtitleFont->drawString(surf, line, xPos, yPos, lineWidth, textColor);
+		}
+	}
+
+	commitSectionToScreen(stSection, Common::Rect(0, 0, stSection.rect.width(), stSection.rect.height()));
+}
+
 void Runtime::drawInventory(uint slot) {
 	if (!isTrayVisible())
 		return;
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index a1d53e9899a..916be46cea1 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -613,7 +613,7 @@ public:
 	Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &rootFSNode, VCruiseGameID gameID, Common::Language defaultLanguage);
 	virtual ~Runtime();
 
-	void initSections(const Common::Rect &gameRect, const Common::Rect &menuRect, const Common::Rect &trayRect, const Common::Rect &fullscreenMenuRect, const Graphics::PixelFormat &pixFmt);
+	void initSections(const Common::Rect &gameRect, const Common::Rect &menuRect, const Common::Rect &trayRect, const Common::Rect &subtitleRect, const Common::Rect &fullscreenMenuRect, const Graphics::PixelFormat &pixFmt);
 
 	void loadCursors(const char *exeName);
 	void setDebugMode(bool debugMode);
@@ -940,6 +940,9 @@ private:
 	void clearScreen();
 	void redrawTray();
 	void clearTray();
+	void redrawSubtitleSection();
+	void clearSubtitleSection();
+	void drawSubtitleText(const Common::Array<Common::U32String> &lines, const uint8 (&color)[3]);
 	void drawInventory(uint slot);
 	void drawCompass();
 	bool isTrayVisible() const;
@@ -1354,6 +1357,7 @@ private:
 	RenderSection _menuSection;
 	RenderSection _traySection;
 	RenderSection _fullscreenMenuSection;
+	RenderSection _subtitleSection;
 
 	Common::Point _mousePos;
 	Common::Point _lmbDownPos;
@@ -1440,6 +1444,9 @@ private:
 	uint32 _cursorCycleLength;
 
 	int32 _dbToVolume[49];
+
+	// AD2044 tooltips
+	Common::String _tooltipText;
 };
 
 } // End of namespace VCruise
diff --git a/engines/vcruise/runtime_scriptexec.cpp b/engines/vcruise/runtime_scriptexec.cpp
index 3cbd969b53e..3b44b69db8f 100644
--- a/engines/vcruise/runtime_scriptexec.cpp
+++ b/engines/vcruise/runtime_scriptexec.cpp
@@ -2119,7 +2119,12 @@ void Runtime::scriptOpEM(ScriptArg_t arg) {
 
 OPCODE_STUB(SE)
 OPCODE_STUB(SDot)
-OPCODE_STUB(E)
+
+void Runtime::scriptOpE(ScriptArg_t arg) {
+	_tooltipText = _scriptSet->strings[arg];
+	redrawSubtitleSection();
+}
+
 OPCODE_STUB(Dot)
 OPCODE_STUB(Sound)
 OPCODE_STUB(ISound)
diff --git a/engines/vcruise/vcruise.cpp b/engines/vcruise/vcruise.cpp
index 3967182491e..7565d3219f5 100644
--- a/engines/vcruise/vcruise.cpp
+++ b/engines/vcruise/vcruise.cpp
@@ -148,6 +148,7 @@ Common::Error VCruiseEngine::run() {
 		_menuBarRect = Common::Rect(size.x - menuBarSize.x, 0, size.x, menuBarSize.y);
 		_videoRect = Common::Rect(videoTL, videoTL + videoSize);
 		_trayRect = Common::Rect(0, size.y - traySize.y, size.x, size.y);
+		_subtitleRect = Common::Rect(_videoRect.left, _videoRect.bottom, _videoRect.right, _trayRect.top);
 	} else {
 		Common::Point videoSize;
 		Common::Point traySize;
@@ -194,7 +195,7 @@ Common::Error VCruiseEngine::run() {
 	_system->fillScreen(0);
 
 	_runtime.reset(new Runtime(_system, _mixer, _rootFSNode, _gameDescription->gameID, _gameDescription->defaultLanguage));
-	_runtime->initSections(_videoRect, _menuBarRect, _trayRect, Common::Rect(640, 480), _system->getScreenFormat());
+	_runtime->initSections(_videoRect, _menuBarRect, _trayRect, _subtitleRect, Common::Rect(640, 480), _system->getScreenFormat());
 
 	const char *exeName = _gameDescription->desc.filesDescriptions[0].fileName;
 
diff --git a/engines/vcruise/vcruise.h b/engines/vcruise/vcruise.h
index 920037171e2..4dee4b73aba 100644
--- a/engines/vcruise/vcruise.h
+++ b/engines/vcruise/vcruise.h
@@ -76,6 +76,7 @@ private:
 	Common::Rect _videoRect;
 	Common::Rect _menuBarRect;
 	Common::Rect _trayRect;
+	Common::Rect _subtitleRect;
 
 	Common::FSNode _rootFSNode;
 


Commit: 3729728a83ac5d83bf68251642f4f0b49ffead80
    https://github.com/scummvm/scummvm/commit/3729728a83ac5d83bf68251642f4f0b49ffead80
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2024-03-31T14:39:28-04:00

Commit Message:
VCRUISE: Add missing opcode dispatches

Changed paths:
    engines/vcruise/runtime_scriptexec.cpp


diff --git a/engines/vcruise/runtime_scriptexec.cpp b/engines/vcruise/runtime_scriptexec.cpp
index 3b44b69db8f..34a6f81cb63 100644
--- a/engines/vcruise/runtime_scriptexec.cpp
+++ b/engines/vcruise/runtime_scriptexec.cpp
@@ -2373,6 +2373,10 @@ bool Runtime::runScript() {
 			DISPATCH_OP(NoUpdate);
 			DISPATCH_OP(NoClear);
 
+			DISPATCH_OP(Say1_AD2044);
+			DISPATCH_OP(Say2_AD2044);
+			DISPATCH_OP(Say1Rnd);
+
 			DISPATCH_OP(M);
 			DISPATCH_OP(EM);
 			DISPATCH_OP(SE);


Commit: 073a3d35d1c068b8476197ba916e77259df0aae3
    https://github.com/scummvm/scummvm/commit/073a3d35d1c068b8476197ba916e77259df0aae3
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2024-03-31T14:39:28-04:00

Commit Message:
VCRUISE: Fix wrong forward cursor ID

Changed paths:
    engines/vcruise/runtime.cpp


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index ba545b739db..7b7aaf9b860 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -1384,7 +1384,7 @@ void Runtime::loadCursors(const char *exeName) {
 	}
 
 	if (_gameID == GID_AD2044) {
-		_namedCursors["CUR_PRZOD"] = 2; // Przod = forward
+		_namedCursors["CUR_PRZOD"] = 4; // Przod = forward
 		_namedCursors["CUR_PRAWO"] = 3;	// Prawo = right
 		_namedCursors["CUR_LEWO"] = 1; // Lewo = left
 		_namedCursors["CUR_LUPA"] = 6; // Lupa = magnifier


Commit: e907bb11536f2925d00a00a3f534c97490ff1696
    https://github.com/scummvm/scummvm/commit/e907bb11536f2925d00a00a3f534c97490ff1696
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2024-03-31T14:39:28-04:00

Commit Message:
VCRUISE: Add say1rnd opcode

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


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 7b7aaf9b860..2d6bd5fe7ed 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -940,7 +940,7 @@ void SaveGameSwappableState::Sound::read(Common::ReadStream *stream, uint saveGa
 	params3D.read(stream);
 }
 
-SaveGameSwappableState::SaveGameSwappableState() : roomNumber(0), screenNumber(0), direction(0), havePendingPostSwapScreenReset(false),
+SaveGameSwappableState::SaveGameSwappableState() : roomNumber(0), screenNumber(0), direction(0), disc(0), havePendingPostSwapScreenReset(false),
 												   musicTrack(0), musicVolume(100), musicActive(true), musicMuteDisabled(false), animVolume(100),
 												   loadedAnimation(0), animDisplayingFrame(0) {
 }
@@ -959,6 +959,7 @@ void SaveGameSnapshot::write(Common::WriteStream *stream) const {
 		stream->writeUint32BE(states[sti]->roomNumber);
 		stream->writeUint32BE(states[sti]->screenNumber);
 		stream->writeUint32BE(states[sti]->direction);
+		stream->writeUint32BE(states[sti]->disc);
 		stream->writeByte(states[sti]->havePendingPostSwapScreenReset ? 1 : 0);
 	}
 
@@ -1067,6 +1068,9 @@ LoadGameOutcome SaveGameSnapshot::read(Common::ReadStream *stream) {
 		states[sti]->screenNumber = stream->readUint32BE();
 		states[sti]->direction = stream->readUint32BE();
 
+		if (saveVersion >= 10)
+			states[sti]->disc = stream->readUint32BE();
+
 		if (saveVersion >= 7)
 			states[sti]->havePendingPostSwapScreenReset = (stream->readByte() != 0);
 	}
@@ -1218,7 +1222,7 @@ FontCacheItem::FontCacheItem() : font(nullptr), size(0) {
 }
 
 Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &rootFSNode, VCruiseGameID gameID, Common::Language defaultLanguage)
-	: _system(system), _mixer(mixer), _roomNumber(1), _screenNumber(0), _direction(0), _hero(0), _swapOutRoom(0), _swapOutScreen(0), _swapOutDirection(0),
+	: _system(system), _mixer(mixer), _roomNumber(1), _screenNumber(0), _direction(0), _hero(0), _disc(0), _swapOutRoom(0), _swapOutScreen(0), _swapOutDirection(0),
 	  _haveHorizPanAnimations(false), _loadedRoomNumber(0), _activeScreenNumber(0),
 	  _gameState(kGameStateBoot), _gameID(gameID), _havePendingScreenChange(false), _forceScreenChange(false), _havePendingPreIdleActions(false), _havePendingReturnToIdleState(false), _havePendingPostSwapScreenReset(false),
 	  _havePendingCompletionCheck(false), _havePendingPlayAmbientSounds(false), _ambientSoundFinishTime(0), _escOn(false), _debugMode(false), _fastAnimationMode(false), _lowQualityGraphicsMode(false),
@@ -1388,6 +1392,8 @@ void Runtime::loadCursors(const char *exeName) {
 		_namedCursors["CUR_PRAWO"] = 3;	// Prawo = right
 		_namedCursors["CUR_LEWO"] = 1; // Lewo = left
 		_namedCursors["CUR_LUPA"] = 6; // Lupa = magnifier
+		_namedCursors["CUR_NAC"] = 5; // Nac = top?  Not sure.  But this is the finger pointer.
+		_namedCursors["CUR_TYL"] = 2; // Tyl = back
 	}
 
 	_panCursors[kPanCursorDraggableHoriz | kPanCursorDraggableUp] = 2;
@@ -6155,6 +6161,7 @@ void Runtime::restoreSaveGameSnapshot() {
 	_roomNumber = mainState->roomNumber;
 	_screenNumber = mainState->screenNumber;
 	_direction = mainState->direction;
+	_disc = mainState->disc;
 	_havePendingPostSwapScreenReset = mainState->havePendingPostSwapScreenReset;
 	_hero = snapshot->hero;
 	_swapOutRoom = snapshot->swapOutRoom;
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index 916be46cea1..60a4ff2cc23 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -451,6 +451,7 @@ struct SaveGameSwappableState {
 	uint roomNumber;
 	uint screenNumber;
 	uint direction;
+	uint disc;
 	bool havePendingPostSwapScreenReset;
 
 	uint loadedAnimation;
@@ -478,7 +479,7 @@ struct SaveGameSnapshot {
 	LoadGameOutcome read(Common::ReadStream *stream);
 
 	static const uint kSaveGameIdentifier = 0x53566372;
-	static const uint kSaveGameCurrentVersion = 9;
+	static const uint kSaveGameCurrentVersion = 10;
 	static const uint kSaveGameEarliestSupportedVersion = 2;
 	static const uint kMaxStates = 2;
 
@@ -1204,6 +1205,7 @@ private:
 	uint _screenNumber;
 	uint _direction;
 	uint _hero;
+	uint _disc;
 
 	uint _swapOutRoom;
 	uint _swapOutScreen;
diff --git a/engines/vcruise/runtime_scriptexec.cpp b/engines/vcruise/runtime_scriptexec.cpp
index 34a6f81cb63..1a9a01551f6 100644
--- a/engines/vcruise/runtime_scriptexec.cpp
+++ b/engines/vcruise/runtime_scriptexec.cpp
@@ -2103,9 +2103,32 @@ void Runtime::scriptOpNoUpdate(ScriptArg_t arg) {
 
 OPCODE_STUB(NoClear)
 
-OPCODE_STUB(Say1_AD2044)
+void Runtime::scriptOpSay1_AD2044(ScriptArg_t arg) {
+	TAKE_STACK_INT(1);
+
+	Common::String soundName = Common::String::format("%02i-%08i", static_cast<int>(_disc * 10u + 1u), static_cast<int>(stackArgs[0]));
+
+	StackInt_t soundID = 0;
+	SoundInstance *cachedSound = nullptr;
+	resolveSoundByName(soundName, true, soundID, cachedSound);
+
+	if (cachedSound) {
+		TriggeredOneShot oneShot;
+		oneShot.soundID = soundID;
+		oneShot.uniqueSlot = _disc;
+
+		if (Common::find(_triggeredOneShots.begin(), _triggeredOneShots.end(), oneShot) == _triggeredOneShots.end()) {
+			triggerSound(kSoundLoopBehaviorNo, *cachedSound, 100, 0, false, true);
+			_triggeredOneShots.push_back(oneShot);
+		}
+	}
+}
+
+void Runtime::scriptOpSay1Rnd(ScriptArg_t arg) {
+	scriptOpSay1_AD2044(arg);
+}
+
 OPCODE_STUB(Say2_AD2044)
-OPCODE_STUB(Say1Rnd)
 
 void Runtime::scriptOpM(ScriptArg_t arg) {
 	// Looks like this is possibly support to present a mouse click prompt and end
@@ -2383,6 +2406,9 @@ bool Runtime::runScript() {
 			DISPATCH_OP(SDot);
 			DISPATCH_OP(E);
 
+			DISPATCH_OP(Sound);
+			DISPATCH_OP(ISound);
+
 		default:
 			error("Unimplemented opcode %i", static_cast<int>(instr.op));
 		}


Commit: e33a407f56843e883ec6a0fbd8eef9a570f56438
    https://github.com/scummvm/scummvm/commit/e33a407f56843e883ec6a0fbd8eef9a570f56438
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2024-03-31T14:39:28-04:00

Commit Message:
VCRUISE: Stub sound ops

Changed paths:
    engines/vcruise/runtime_scriptexec.cpp


diff --git a/engines/vcruise/runtime_scriptexec.cpp b/engines/vcruise/runtime_scriptexec.cpp
index 1a9a01551f6..937d0be55a8 100644
--- a/engines/vcruise/runtime_scriptexec.cpp
+++ b/engines/vcruise/runtime_scriptexec.cpp
@@ -2128,7 +2128,9 @@ void Runtime::scriptOpSay1Rnd(ScriptArg_t arg) {
 	scriptOpSay1_AD2044(arg);
 }
 
-OPCODE_STUB(Say2_AD2044)
+void Runtime::scriptOpSay2_AD2044(ScriptArg_t arg) {
+	scriptOpSay1_AD2044(arg);
+}
 
 void Runtime::scriptOpM(ScriptArg_t arg) {
 	// Looks like this is possibly support to present a mouse click prompt and end
@@ -2149,8 +2151,15 @@ void Runtime::scriptOpE(ScriptArg_t arg) {
 }
 
 OPCODE_STUB(Dot)
-OPCODE_STUB(Sound)
-OPCODE_STUB(ISound)
+
+void Runtime::scriptOpSound(ScriptArg_t arg) {
+	TAKE_STACK_INT(2);
+}
+
+void Runtime::scriptOpISound(ScriptArg_t arg) {
+	TAKE_STACK_INT(2);
+}
+
 OPCODE_STUB(USound)
 OPCODE_STUB(RGet)
 


Commit: e7c47fb50082523b402b914b36645a4c8c490c99
    https://github.com/scummvm/scummvm/commit/e7c47fb50082523b402b914b36645a4c8c490c99
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2024-03-31T14:39:29-04:00

Commit Message:
VCRUISE: Fix sound IDs, open cursor, and some screen overrides

Changed paths:
    engines/vcruise/runtime.cpp


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 2d6bd5fe7ed..721beaf3728 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -331,6 +331,12 @@ public:
 	void unload() override;
 
 private:
+	struct ScreenOverride {
+		uint roomNumber;
+		uint screenNumber;
+		int actualMapFileID;
+	};
+
 	void load();
 
 	static const uint kFirstScreen = 0xa0;
@@ -340,6 +346,15 @@ private:
 	bool _isLoaded;
 
 	Common::SharedPtr<MapScreenDirectionDef> _currentMap;
+
+	static const ScreenOverride sk_screenOverrides[];
+};
+
+const AD2044MapLoader::ScreenOverride AD2044MapLoader::sk_screenOverrides[] = {
+	// Room 1
+	{1, 0xb6, 145},	// After pushing the button to open the capsule
+	{1, 0x6a, 142},	// Opening an apple on the table
+	{1, 0x6b, 143}, // Clicking the tablet in the apple
 };
 
 AD2044MapLoader::AD2044MapLoader() : _roomNumber(0), _screenNumber(0), _isLoaded(false) {
@@ -368,19 +383,35 @@ void AD2044MapLoader::load() {
 	// This is loaded even if the open fails
 	_isLoaded = true;
 
-	if (_screenNumber < kFirstScreen)
-		return;
+	int scrFileID = -1;
 
-	uint adjustedScreenNumber = _screenNumber - kFirstScreen;
+	for (const ScreenOverride &screenOverride : sk_screenOverrides) {
+		if (screenOverride.roomNumber == _roomNumber && screenOverride.screenNumber == _screenNumber) {
+			scrFileID = screenOverride.actualMapFileID;
+			break;
+		}
+	}
 
-	if (adjustedScreenNumber > 99)
-		return;
+	if (scrFileID < 0) {
+		if (_screenNumber < kFirstScreen)
+			return;
+
+		uint adjustedScreenNumber = _screenNumber - kFirstScreen;
 
-	Common::Path mapFileName(Common::String::format("map/SCR%i.MAP", static_cast<int>(_roomNumber * 100u + adjustedScreenNumber)));
+		if (adjustedScreenNumber > 99)
+			return;
+
+		scrFileID = static_cast<int>(_roomNumber * 100u + adjustedScreenNumber);
+	}
+
+	Common::Path mapFileName(Common::String::format("map/SCR%i.MAP", scrFileID));
 	Common::File mapFile;
 
-	if (!mapFile.open(mapFileName))
-		return;
+	debug(1, "Loading screen map %s", mapFileName.toString(Common::Path::kNativeSeparator).c_str());
+
+	if (!mapFile.open(mapFileName)) {
+		error("Couldn't resolve map file for room %u screen %u", _roomNumber, _screenNumber);
+	}
 
 	_currentMap = loadScreenDirectionDef(mapFile);
 }
@@ -1394,6 +1425,7 @@ void Runtime::loadCursors(const char *exeName) {
 		_namedCursors["CUR_LUPA"] = 6; // Lupa = magnifier
 		_namedCursors["CUR_NAC"] = 5; // Nac = top?  Not sure.  But this is the finger pointer.
 		_namedCursors["CUR_TYL"] = 2; // Tyl = back
+		_namedCursors["CUR_OTWORZ"] = 11; // Otworz = open
 	}
 
 	_panCursors[kPanCursorDraggableHoriz | kPanCursorDraggableUp] = 2;
@@ -3157,8 +3189,16 @@ void Runtime::resolveSoundByName(const Common::String &soundName, bool load, Sta
 	Common::String sndName = soundName;
 
 	uint soundID = 0;
-	for (uint i = 0; i < 4; i++)
-		soundID = soundID * 10u + (sndName[i] - '0');
+
+	if (_gameID == GID_AD2044) {
+		for (uint i = 0; i < 2; i++)
+			soundID = soundID * 10u + (sndName[i] - '0');
+		for (uint i = 0; i < 5; i++)
+			soundID = soundID * 10u + (sndName[6 + i] - '0');
+	} else {
+		for (uint i = 0; i < 4; i++)
+			soundID = soundID * 10u + (sndName[i] - '0');
+	}
 
 	sndName.toLowercase();
 


Commit: b313b4068f9c369e04c55632b92e31bf9e0a15d5
    https://github.com/scummvm/scummvm/commit/b313b4068f9c369e04c55632b92e31bf9e0a15d5
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2024-03-31T14:39:29-04:00

Commit Message:
VCRUISE: AD2044 inventory support

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


diff --git a/engines/vcruise/metaengine.cpp b/engines/vcruise/metaengine.cpp
index 73a4e1a794d..575f0f1b405 100644
--- a/engines/vcruise/metaengine.cpp
+++ b/engines/vcruise/metaengine.cpp
@@ -197,6 +197,10 @@ Common::Array<Common::Keymap *> VCruiseMetaEngine::initKeymaps(const char *targe
 	act->setCustomEngineActionEvent(VCruise::kKeymappedEventSkipAnimation);
 	keymap->addAction(act);
 
+	act = new Common::Action("VCRUISE_PUT_ITEM", _("Cycle item in scene (debug cheat)"));
+	act->setCustomEngineActionEvent(VCruise::kKeymappedEventPutItem);
+	keymap->addAction(act);
+
 	return Common::Keymap::arrayOf(keymap);
 }
 
diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 721beaf3728..829095fdd05 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -64,6 +64,17 @@
 
 namespace VCruise {
 
+struct InitialItemPlacement {
+	uint roomNumber;
+	uint screenNumber;
+	uint itemID;
+};
+
+const InitialItemPlacement g_ad2044InitialItemPlacements[] = {
+	{1, 0xb8, 24},	// Cigarette pack
+	{1, 0xac, 27},	// Matches
+};
+
 struct CodePageGuess {
 	Common::CodePage codePage;
 	Runtime::CharSet charSet;
@@ -431,8 +442,13 @@ OSEvent::OSEvent() : type(kOSEventTypeInvalid), keyCode(static_cast<Common::KeyC
 
 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);
+	pixFmt = fmt;
+	if (paramRect.isEmpty())
+		surf.reset();
+	else {
+		surf.reset(new Graphics::ManagedSurface(paramRect.width(), paramRect.height(), fmt));
+		surf->fillRect(Common::Rect(0, 0, surf->w, surf->h), 0xffffffff);
+	}
 }
 
 Runtime::StackValue::ValueUnion::ValueUnion() {
@@ -976,8 +992,36 @@ SaveGameSwappableState::SaveGameSwappableState() : roomNumber(0), screenNumber(0
 												   loadedAnimation(0), animDisplayingFrame(0) {
 }
 
+SaveGameSnapshot::PagedInventoryItem::PagedInventoryItem() : page(0), slot(0), itemID(0) {
+}
+
+void SaveGameSnapshot::PagedInventoryItem::write(Common::WriteStream *stream) const {
+	stream->writeByte(page);
+	stream->writeByte(slot);
+	stream->writeByte(itemID);
+}
+
+void SaveGameSnapshot::PagedInventoryItem::read(Common::ReadStream *stream, uint saveGameVersion) {
+	page = stream->readByte();
+	slot = stream->readByte();
+	itemID = stream->readByte();
+}
+
+SaveGameSnapshot::PlacedInventoryItem::PlacedInventoryItem() : locationID(0), itemID(0) {
+}
+
+void SaveGameSnapshot::PlacedInventoryItem::write(Common::WriteStream *stream) const {
+	stream->writeUint32BE(locationID);
+	stream->writeByte(itemID);
+}
+
+void SaveGameSnapshot::PlacedInventoryItem::read(Common::ReadStream *stream, uint saveGameVersion) {
+	locationID = stream->readUint32BE();
+	itemID = stream->readByte();
+}
+
 SaveGameSnapshot::SaveGameSnapshot() : hero(0), swapOutRoom(0), swapOutScreen(0), swapOutDirection(0),
-	escOn(false), numStates(1), listenerX(0), listenerY(0), listenerAngle(0) {
+	escOn(false), numStates(1), listenerX(0), listenerY(0), listenerAngle(0), inventoryPage(0), inventoryActiveItem(0) {
 }
 
 void SaveGameSnapshot::write(Common::WriteStream *stream) const {
@@ -1035,6 +1079,10 @@ void SaveGameSnapshot::write(Common::WriteStream *stream) const {
 
 	stream->writeUint32BE(variables.size());
 	stream->writeUint32BE(timers.size());
+	stream->writeUint32BE(placedItems.size());
+	stream->writeUint32BE(pagedItems.size());
+	stream->writeByte(inventoryPage);
+	stream->writeByte(inventoryActiveItem);
 
 	for (uint sti = 0; sti < numStates; sti++) {
 		for (const SaveGameSwappableState::InventoryItem &invItem : states[sti]->inventory)
@@ -1066,6 +1114,12 @@ void SaveGameSnapshot::write(Common::WriteStream *stream) const {
 		stream->writeUint32BE(timer._key);
 		stream->writeUint32BE(timer._value);
 	}
+
+	for (const PlacedInventoryItem &item : placedItems)
+		item.write(stream);
+
+	for (const PagedInventoryItem &item : pagedItems)
+		item.write(stream);
 }
 
 LoadGameOutcome SaveGameSnapshot::read(Common::ReadStream *stream) {
@@ -1180,6 +1234,19 @@ LoadGameOutcome SaveGameSnapshot::read(Common::ReadStream *stream) {
 	uint numVars = stream->readUint32BE();
 	uint numTimers = stream->readUint32BE();
 
+	uint numPlacedItems = 0;
+	uint numPagedItems = 0;
+
+	if (saveVersion >= 10) {
+		numPlacedItems = stream->readUint32BE();
+		numPagedItems = stream->readUint32BE();
+		this->inventoryPage = stream->readByte();
+		this->inventoryActiveItem = stream->readByte();
+	} else {
+		this->inventoryPage = 0;
+		this->inventoryActiveItem = 0;
+	}
+
 	if (stream->eos() || stream->err())
 		return kLoadGameOutcomeSaveDataCorrupted;
 
@@ -1191,7 +1258,6 @@ LoadGameOutcome SaveGameSnapshot::read(Common::ReadStream *stream) {
 
 	triggeredOneShots.resize(numOneShots);
 
-	
 	for (uint sti = 0; sti < numStates; sti++) {
 		for (uint i = 0; i < numInventory[sti]; i++)
 			states[sti]->inventory[i].read(stream);
@@ -1229,6 +1295,18 @@ LoadGameOutcome SaveGameSnapshot::read(Common::ReadStream *stream) {
 		timers[key] = value;
 	}
 
+	for (uint i = 0; i < numPlacedItems; i++) {
+		PlacedInventoryItem item;
+		item.read(stream, saveVersion);
+		placedItems.push_back(item);
+	}
+
+	for (uint i = 0; i < numPagedItems; i++) {
+		PagedInventoryItem item;
+		item.read(stream, saveVersion);
+		pagedItems.push_back(item);
+	}
+
 	if (stream->eos() || stream->err())
 		return kLoadGameOutcomeSaveDataCorrupted;
 
@@ -1274,7 +1352,8 @@ Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &roo
 	  _isInGame(false),
 	  _subtitleFont(nullptr), _isDisplayingSubtitles(false), _isSubtitleSourceAnimation(false),
 	  _languageIndex(0), _defaultLanguageIndex(0), _defaultLanguage(defaultLanguage), _charSet(kCharSetLatin),
-	  _isCDVariant(false), _currentAnimatedCursor(nullptr), _currentCursor(nullptr), _cursorTimeBase(0), _cursorCycleLength(0) {
+	  _isCDVariant(false), _currentAnimatedCursor(nullptr), _currentCursor(nullptr), _cursorTimeBase(0), _cursorCycleLength(0),
+	  _inventoryActivePage(0) {
 
 	for (uint i = 0; i < kNumDirections; i++) {
 		_haveIdleAnimations[i] = false;
@@ -1321,6 +1400,8 @@ void Runtime::initSections(const Common::Rect &gameRect, const Common::Rect &men
 
 	if (!subtitleRect.isEmpty())
 		_subtitleSection.init(subtitleRect, pixFmt);
+
+	_placedItemBackBufferSection.init(Common::Rect(), pixFmt);
 }
 
 void Runtime::loadCursors(const char *exeName) {
@@ -1588,10 +1669,10 @@ bool Runtime::bootGame(bool newGame) {
 		error("Don't have a start config for this game");
 
 	if (_gameID != GID_AD2044) {
-		_trayBackgroundGraphic = loadGraphic("Pocket", true);
-		_trayHighlightGraphic = loadGraphic("Select", true);
-		_trayCompassGraphic = loadGraphic("Select_1", true);
-		_trayCornerGraphic = loadGraphic("Select_2", true);
+		_trayBackgroundGraphic = loadGraphic("Pocket", "", true);
+		_trayHighlightGraphic = loadGraphic("Select", "", true);
+		_trayCompassGraphic = loadGraphic("Select_1", "", true);
+		_trayCornerGraphic = loadGraphic("Select_2", "", true);
 	}
 
 	if (_gameID == GID_AD2044)
@@ -1729,11 +1810,11 @@ bool Runtime::bootGame(bool newGame) {
 		_uiGraphics.resize(24);
 		for (uint i = 0; i < _uiGraphics.size(); i++) {
 			if (_gameID == GID_REAH) {
-				_uiGraphics[i] = loadGraphic(Common::String::format("Image%03u", static_cast<uint>(_languageIndex * 100u + i)), false);
+				_uiGraphics[i] = loadGraphic(Common::String::format("Image%03u", static_cast<uint>(_languageIndex * 100u + i)), "", false);
 				if (_languageIndex != 0 && !_uiGraphics[i])
-					_uiGraphics[i] = loadGraphic(Common::String::format("Image%03u", static_cast<uint>(i)), false);
+					_uiGraphics[i] = loadGraphic(Common::String::format("Image%03u", static_cast<uint>(i)), "", false);
 			} else if (_gameID == GID_SCHIZM) {
-				_uiGraphics[i] = loadGraphic(Common::String::format("Data%03u", i), false);
+				_uiGraphics[i] = loadGraphic(Common::String::format("Data%03u", i), "", false);
 			}
 		}
 	}
@@ -2003,6 +2084,9 @@ bool Runtime::runIdle() {
 				case kKeymappedEventQuit:
 					changeToMenuPage(createMenuQuit(_gameID == GID_SCHIZM));
 					return true;
+				case kKeymappedEventPutItem:
+					cheatPutItem();
+					return true;
 				default:
 					break;
 				}
@@ -3343,6 +3427,33 @@ void Runtime::changeToScreen(uint roomNumber, uint screenNumber) {
 		_forceAllowSaves = false;
 
 		recordSaveGameSnapshot();
+
+		_placedItemRect = Common::Rect();
+		if (_gameID == GID_AD2044) {
+			const MapScreenDirectionDef *screenDef = _mapLoader->getScreenDirection(_screenNumber, _direction);
+			if (screenDef) {
+				for (const InteractionDef &interaction : screenDef->interactions) {
+					if (interaction.objectType == 2) {
+						_placedItemRect = interaction.rect;
+						break;
+					}
+				}
+			}
+
+			_placedItemRect = _placedItemRect.findIntersectingRect(Common::Rect(640, 480));
+
+			Common::Rect renderRect = _placedItemRect;
+			renderRect.translate(_gameSection.rect.left, _gameSection.rect.top);
+
+			_placedItemBackBufferSection.init(renderRect, _placedItemBackBufferSection.pixFmt);
+
+			if (!_placedItemRect.isEmpty())
+				_placedItemBackBufferSection.surf->blitFrom(*_gameSection.surf, _placedItemRect, Common::Point(0, 0));
+
+			updatePlacedItemCache();
+
+			drawPlacedItemGraphic();
+		}
 	}
 }
 
@@ -5036,10 +5147,12 @@ void Runtime::inventoryAddItem(uint item) {
 	if (firstOpenSlot == kNumInventorySlots)
 		error("Tried to add an inventory item but ran out of slots");
 
-	Common::String itemFileName = getFileNameForItemGraphic(item);
+	Common::String itemFileName;
+	Common::String alphaFileName;
+	getFileNamesForItemGraphic(item, itemFileName, alphaFileName);
 
 	_inventory[firstOpenSlot].itemID = item;
-	_inventory[firstOpenSlot].graphic = loadGraphic(itemFileName, false);
+	_inventory[firstOpenSlot].graphic = loadGraphic(itemFileName, alphaFileName, false);
 
 	drawInventory(firstOpenSlot);
 }
@@ -5324,23 +5437,120 @@ void Runtime::resetInventoryHighlights() {
 	}
 }
 
-Common::String Runtime::getFileNameForItemGraphic(uint itemID) const {
+void Runtime::loadInventoryFromPage() {
+	for (uint slot = 0; slot < kNumInventorySlots; slot++)
+		_inventory[slot] = _inventoryPages[_inventoryActivePage][slot];
+}
+
+void Runtime::copyInventoryToPage() {
+	for (uint slot = 0; slot < kNumInventorySlots; slot++)
+		_inventoryPages[_inventoryActivePage][slot] = _inventory[slot];
+}
+
+void Runtime::cheatPutItem() {
+	uint32 location = getLocationForScreen(_roomNumber, _screenNumber);
+
+	uint8 &pid = _placedItems[location];
+	pid++;
+
+	if (pid == 30 || pid == 45 || pid == 49 || pid == 59)
+		pid++;
+	else if (pid == 62)
+		pid += 2;
+	else if (pid == 74)
+		pid = 1;
+
+	updatePlacedItemCache();
+
+	clearPlacedItemGraphic();
+	drawPlacedItemGraphic();
+}
+
+uint32 Runtime::getLocationForScreen(uint roomNumber, uint screenNumber) {
+	return roomNumber * 10000u + screenNumber;
+}
+
+void Runtime::updatePlacedItemCache() {
+	uint32 placedItemLocationID = getLocationForScreen(_roomNumber, _screenNumber);
+	Common::HashMap<uint32, uint8>::const_iterator placedItemIt = _placedItems.find(placedItemLocationID);
+	if (placedItemIt != _placedItems.end()) {
+		uint8 itemID = placedItemIt->_value;
+
+		if (_inventoryPlacedItemCache.itemID != itemID) {
+			Common::String itemFileName;
+			Common::String alphaFileName;
+
+			_inventoryPlacedItemCache.itemID = itemID;
+			getFileNamesForItemGraphic(itemID, itemFileName, alphaFileName);
+			_inventoryPlacedItemCache.graphic = loadGraphic(itemFileName, alphaFileName, false);
+		}
+	} else {
+		_inventoryPlacedItemCache = InventoryItem();
+	}
+}
+
+void Runtime::drawPlacedItemGraphic() {
+	const Graphics::Surface *surf = _inventoryPlacedItemCache.graphic.get();
+	if (surf) {
+		Common::Point drawPos((_placedItemRect.left + _placedItemRect.right - surf->w) / 2, (_placedItemRect.top + _placedItemRect.bottom - surf->h) / 2);
+
+		_gameSection.surf->blitFrom(*surf, drawPos);
+		drawSectionToScreen(_gameSection, _placedItemRect);
+	}
+}
+
+void Runtime::clearPlacedItemGraphic() {
+	if (!_placedItemRect.isEmpty()) {
+		_gameSection.surf->blitFrom(*_placedItemBackBufferSection.surf, Common::Point(_placedItemRect.left, _placedItemRect.top));
+		drawSectionToScreen(_gameSection, _placedItemRect);
+	}
+}
+
+void Runtime::getFileNamesForItemGraphic(uint itemID, Common::String &outFileName, Common::String &outAlphaFileName) const {
 	if (_gameID == GID_REAH)
-		return Common::String::format("Thing%u", itemID);
+		outFileName = Common::String::format("Thing%u", itemID);
 	else if (_gameID == GID_SCHIZM)
-		return Common::String::format("Item%u", itemID);
-	else {
+		outFileName = Common::String::format("Item%u", itemID);
+	else if (_gameID == GID_AD2044) {
+		outFileName = Common::String::format(_lowQualityGraphicsMode ? "RZB%u" : "RZE%u", itemID);
+		outAlphaFileName = Common::String::format("MAS%u", itemID);
+	} else
 		error("Unknown game, can't format inventory item");
-		return "";
-	}
 }
 
-Common::SharedPtr<Graphics::Surface> Runtime::loadGraphic(const Common::String &graphicName, bool required) {
-	Common::Path filePath("Gfx/");
+Common::SharedPtr<Graphics::Surface> Runtime::loadGraphic(const Common::String &graphicName, const Common::String &alphaName, bool required) {
+	Common::Path filePath((_gameID == GID_AD2044) ? "rze/" : "Gfx/");
+
 	filePath.appendInPlace(graphicName);
-	filePath.appendInPlace(".bmp");
+	filePath.appendInPlace((_gameID == GID_AD2044) ? ".BMP" : ".bmp");
+
+	Common::SharedPtr<Graphics::Surface> surf = loadGraphicFromPath(filePath, required);
+
+	if (surf && !alphaName.empty()) {
+		Common::SharedPtr<Graphics::Surface> alphaSurf = loadGraphic(alphaName, "", required);
+		if (alphaSurf) {
+			if (surf->w != alphaSurf->w || surf->h != alphaSurf->h)
+				error("Mismatched graphic sizes");
+
+			int h = surf->h;
+			int w = surf->w;
+			for (int y = 0; y < h; y++) {
+				for (int x = 0; x < w; x++) {
+					uint32 alphaSurfPixel = alphaSurf->getPixel(x, y);
+
+					uint8 r = 0;
+					uint8 g = 0;
+					uint8 b = 0;
+					uint8 a = 0;
+					alphaSurf->format.colorToARGB(alphaSurfPixel, a, r, g, b);
+					if (r < 128)
+						surf->setPixel(x, y, 0);
+				}
+			}
+		}
+	}
 
-	return loadGraphicFromPath(filePath, required);
+	return surf;
 }
 
 Common::SharedPtr<Graphics::Surface> Runtime::loadGraphicFromPath(const Common::Path &filePath, bool required) {
@@ -5362,7 +5572,8 @@ Common::SharedPtr<Graphics::Surface> Runtime::loadGraphicFromPath(const Common::
 
 	Common::SharedPtr<Graphics::Surface> surf(new Graphics::Surface(), Graphics::SurfaceDeleter());
 	surf->copyFrom(*bmpDecoder.getSurface());
-	surf = Common::SharedPtr<Graphics::Surface>(surf->convertTo(Graphics::createPixelFormat<8888>()), Graphics::SurfaceDeleter());
+	surf = Common::SharedPtr<Graphics::Surface>(surf->convertTo(Graphics::createPixelFormat<8888>(), bmpDecoder.getPalette(), bmpDecoder.getPaletteColorCount()), Graphics::SurfaceDeleter());
+
 	return surf;
 }
 
@@ -6078,19 +6289,23 @@ void Runtime::recordSaveGameSnapshot() {
 	snapshot->states[0].reset(new SaveGameSwappableState());
 	if (_gameID == GID_REAH)
 		snapshot->numStates = 1;
-	else if (_gameID == GID_SCHIZM) {
+	else if (_gameID == GID_SCHIZM || _gameID == GID_AD2044) {
 		snapshot->numStates = 2;
 		snapshot->states[1] = _altState;
 	}
 
 	SaveGameSwappableState *mainState = snapshot->states[0].get();
 
-	for (const InventoryItem &inventoryItem : _inventory) {
-		SaveGameSwappableState::InventoryItem saveItem;
-		saveItem.itemID = inventoryItem.itemID;
-		saveItem.highlighted = inventoryItem.highlighted;
+	if (_gameID == GID_AD2044) {
+		copyInventoryToPage();
+	} else {
+		for (const InventoryItem &inventoryItem : _inventory) {
+			SaveGameSwappableState::InventoryItem saveItem;
+			saveItem.itemID = inventoryItem.itemID;
+			saveItem.highlighted = inventoryItem.highlighted;
 
-		mainState->inventory.push_back(saveItem);
+			mainState->inventory.push_back(saveItem);
+		}
 	}
 
 	mainState->roomNumber = _roomNumber;
@@ -6098,6 +6313,8 @@ void Runtime::recordSaveGameSnapshot() {
 	mainState->direction = _direction;
 	mainState->havePendingPostSwapScreenReset = false;
 	snapshot->hero = _hero;
+	snapshot->inventoryPage = _inventoryActivePage;
+	snapshot->inventoryActiveItem = _inventoryActiveItem.itemID;
 
 	snapshot->pendingStaticAnimParams = _pendingStaticAnimParams;
 
@@ -6134,6 +6351,28 @@ void Runtime::recordSaveGameSnapshot() {
 	snapshot->listenerX = _listenerX;
 	snapshot->listenerY = _listenerY;
 	snapshot->listenerAngle = _listenerAngle;
+
+	for (const Common::HashMap<uint32, uint8>::Node &placedItem : _placedItems) {
+		SaveGameSnapshot::PlacedInventoryItem saveItem;
+		saveItem.locationID = placedItem._key;
+		saveItem.itemID = placedItem._value;
+
+		snapshot->placedItems.push_back(saveItem);
+	}
+
+	for (uint page = 0; page < kNumInventoryPages; page++) {
+		for (uint slot = 0; slot < kNumInventorySlots; slot++) {
+			uint itemID = _inventoryPages[page][slot].itemID;
+			if (itemID != 0) {
+				SaveGameSnapshot::PagedInventoryItem pagedItem;
+				pagedItem.page = page;
+				pagedItem.slot = slot;
+				pagedItem.itemID = itemID;
+
+				snapshot->pagedItems.push_back(pagedItem);
+			}
+		}
+	}
 }
 
 void Runtime::recordSounds(SaveGameSwappableState &state) {
@@ -6184,17 +6423,72 @@ void Runtime::restoreSaveGameSnapshot() {
 
 	SaveGameSwappableState *mainState = snapshot->states[0].get();
 
-	for (uint i = 0; i < kNumInventorySlots && i < mainState->inventory.size(); i++) {
-		const SaveGameSwappableState::InventoryItem &saveItem = mainState->inventory[i];
+	if (_gameID == GID_AD2044) {
+		for (uint page = 0; page < kNumInventoryPages; page++)
+			for (uint slot = 0; slot < kNumInventorySlots; slot++)
+				_inventoryPages[page][slot].itemID = 0;
+
+		for (const SaveGameSnapshot::PagedInventoryItem &pagedItem : snapshot->pagedItems) {
+			if (pagedItem.page >= kNumInventoryPages || pagedItem.slot >= kNumInventorySlots)
+				error("Invalid item slot in save game snapshot");
+
+			InventoryItem &invItem = _inventoryPages[pagedItem.page][pagedItem.slot];
+			invItem.itemID = pagedItem.itemID;			
+
+			if (invItem.itemID) {
+				Common::String itemFileName;
+				Common::String alphaFileName;
+				getFileNamesForItemGraphic(invItem.itemID, itemFileName, alphaFileName);
+				invItem.graphic = loadGraphic(itemFileName, alphaFileName, false);
+			}
+		}
 
-		_inventory[i].itemID = saveItem.itemID;
-		_inventory[i].highlighted = saveItem.highlighted;
+		for (uint page = 0; page < kNumInventoryPages; page++) {
+			for (uint slot = 0; slot < kNumInventorySlots; slot++) {
+				InventoryItem &invItem = _inventoryPages[page][slot];
+				if (!invItem.itemID)
+					invItem.graphic.reset();
+			}
+		}
 
-		if (saveItem.itemID) {
-			Common::String itemFileName = getFileNameForItemGraphic(saveItem.itemID);
-			_inventory[i].graphic = loadGraphic(itemFileName, false);
+		_inventoryActiveItem.itemID = snapshot->inventoryActiveItem;
+		if (_inventoryActiveItem.itemID) {
+			Common::String itemFileName;
+			Common::String alphaFileName;
+			getFileNamesForItemGraphic(_inventoryActiveItem.itemID, itemFileName, alphaFileName);
+			_inventoryActiveItem.graphic = loadGraphic(itemFileName, alphaFileName, false);
 		} else {
-			_inventory[i].graphic.reset();
+			_inventoryActiveItem.graphic.reset();
+		}
+
+		_inventoryPlacedItemCache = InventoryItem();
+
+		if (snapshot->inventoryPage >= kNumInventoryPages)
+			error("Invalid inventory page");
+
+		_inventoryActivePage = snapshot->inventoryPage;
+
+		loadInventoryFromPage();
+
+		_placedItems.clear();
+		for (const SaveGameSnapshot::PlacedInventoryItem &placedItem : snapshot->placedItems) {
+			_placedItems[placedItem.locationID] = placedItem.itemID;
+		}
+	} else {
+		for (uint i = 0; i < kNumInventorySlots && i < mainState->inventory.size(); i++) {
+			const SaveGameSwappableState::InventoryItem &saveItem = mainState->inventory[i];
+
+			_inventory[i].itemID = saveItem.itemID;
+			_inventory[i].highlighted = saveItem.highlighted;
+
+			if (saveItem.itemID) {
+				Common::String itemFileName;
+				Common::String alphaFileName;
+				getFileNamesForItemGraphic(saveItem.itemID, itemFileName, alphaFileName);
+				_inventory[i].graphic = loadGraphic(itemFileName, alphaFileName, false);
+			} else {
+				_inventory[i].graphic.reset();
+			}
 		}
 	}
 
@@ -6348,9 +6642,25 @@ Common::SharedPtr<SaveGameSnapshot> Runtime::generateNewGameSnapshot() const {
 	// AD2044 new game normally loads a pre-packaged save.  Unlike Reah and Schizm,
 	// it doesn't appear to have a startup script, so we need to set up everything
 	// that it needs here.
-	if (_gameID == GID_AD2044)
+	if (_gameID == GID_AD2044) {
 		mainState->animDisplayingFrame = 345;
 
+		SaveGameSnapshot::PagedInventoryItem item;
+		item.page = 0;
+		item.slot = 1;
+		item.itemID = 54;	// Electronic goaler (sic)
+
+		for (const InitialItemPlacement &itemPlacement : g_ad2044InitialItemPlacements) {
+			SaveGameSnapshot::PlacedInventoryItem placedItem;
+			placedItem.locationID = getLocationForScreen(itemPlacement.roomNumber, itemPlacement.screenNumber);
+			placedItem.itemID = itemPlacement.itemID;
+
+			snapshot->placedItems.push_back(placedItem);
+		}
+
+		snapshot->pagedItems.push_back(item);
+	}
+
 	return snapshot;
 }
 
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index 60a4ff2cc23..f08a8560d98 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -22,6 +22,8 @@
 #ifndef VCRUISE_RUNTIME_H
 #define VCRUISE_RUNTIME_H
 
+#include "graphics/pixelformat.h"
+
 #include "common/hashmap.h"
 #include "common/keyboard.h"
 #include "common/rect.h"
@@ -473,6 +475,27 @@ struct SaveGameSwappableState {
 };
 
 struct SaveGameSnapshot {
+	struct PagedInventoryItem {
+		PagedInventoryItem();
+
+		uint8 page;
+		uint8 slot;
+		uint8 itemID;
+
+		void write(Common::WriteStream *stream) const;
+		void read(Common::ReadStream *stream, uint saveGameVersion);
+	};
+
+	struct PlacedInventoryItem {
+		PlacedInventoryItem();
+
+		uint32 locationID;
+		uint8 itemID;
+
+		void write(Common::WriteStream *stream) const;
+		void read(Common::ReadStream *stream, uint saveGameVersion);
+	};
+
 	SaveGameSnapshot();
 
 	void write(Common::WriteStream *stream) const;
@@ -490,6 +513,8 @@ struct SaveGameSnapshot {
 	uint swapOutRoom;
 	uint swapOutScreen;
 	uint swapOutDirection;
+	uint8 inventoryPage;
+	uint8 inventoryActiveItem;
 
 	uint numStates;
 	Common::SharedPtr<SaveGameSwappableState> states[kMaxStates];
@@ -508,6 +533,8 @@ struct SaveGameSnapshot {
 
 	Common::HashMap<uint32, int32> variables;
 	Common::HashMap<uint, uint32> timers;
+	Common::Array<PagedInventoryItem> pagedItems;
+	Common::Array<PlacedInventoryItem> placedItems;
 };
 
 enum OSEventType {
@@ -539,6 +566,8 @@ enum KeymappedEvent {
 	kKeymappedEventSoundVolumeUp,
 
 	kKeymappedEventSkipAnimation,
+
+	kKeymappedEventPutItem,
 };
 
 struct OSEvent {
@@ -674,6 +703,7 @@ private:
 	struct RenderSection {
 		Common::SharedPtr<Graphics::ManagedSurface> surf;
 		Common::Rect rect;
+		Graphics::PixelFormat pixFmt;
 
 		void init(const Common::Rect &paramRect, const Graphics::PixelFormat &fmt);
 	};
@@ -775,6 +805,7 @@ private:
 	static const uint kPanoramaHorizFlags = (kPanoramaLeftFlag | kPanoramaRightFlag);
 
 	static const uint kNumInventorySlots = 6;
+	static const uint kNumInventoryPages = 8;
 
 	typedef int32 ScriptArg_t;
 	typedef int32 StackInt_t;
@@ -948,9 +979,16 @@ private:
 	void drawCompass();
 	bool isTrayVisible() const;
 	void resetInventoryHighlights();
-
-	Common::String getFileNameForItemGraphic(uint itemID) const;
-	Common::SharedPtr<Graphics::Surface> loadGraphic(const Common::String &graphicName, bool required);
+	void loadInventoryFromPage();
+	void copyInventoryToPage();
+	void cheatPutItem();
+	static uint32 getLocationForScreen(uint roomNumber, uint screenNumber);
+	void updatePlacedItemCache();
+	void drawPlacedItemGraphic();
+	void clearPlacedItemGraphic();
+
+	void getFileNamesForItemGraphic(uint itemID, Common::String &outGraphicFileName, Common::String &outAlphaFileName) const;
+	Common::SharedPtr<Graphics::Surface> loadGraphic(const Common::String &graphicName, const Common::String &alphaName, bool required);
 	Common::SharedPtr<Graphics::Surface> loadGraphicFromPath(const Common::Path &path, bool required);
 
 	bool loadSubtitles(Common::CodePage codePage, bool guessCodePage);
@@ -1186,6 +1224,12 @@ private:
 	Common::Array<Common::SharedPtr<AnimatedCursor> > _cursorsShort;      // Cursors indexed as CURSOR_#
 
 	InventoryItem _inventory[kNumInventorySlots];
+	InventoryItem _inventoryPages[kNumInventoryPages][kNumInventorySlots];
+	Common::HashMap<uint32, uint8> _placedItems;
+	uint8 _inventoryActivePage;
+	InventoryItem _inventoryActiveItem;
+	InventoryItem _inventoryPlacedItemCache;
+	Common::Rect _placedItemRect;
 
 	Common::SharedPtr<Graphics::Surface> _trayCompassGraphic;
 	Common::SharedPtr<Graphics::Surface> _trayBackgroundGraphic;
@@ -1360,6 +1404,7 @@ private:
 	RenderSection _traySection;
 	RenderSection _fullscreenMenuSection;
 	RenderSection _subtitleSection;
+	RenderSection _placedItemBackBufferSection;
 
 	Common::Point _mousePos;
 	Common::Point _lmbDownPos;


Commit: 63bcca8cc9257afa11c78bc3127080389c5f133a
    https://github.com/scummvm/scummvm/commit/63bcca8cc9257afa11c78bc3127080389c5f133a
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2024-03-31T14:39:29-04:00

Commit Message:
VCRUISE: Add some boilerplate for loading graphics and strings

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


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 829095fdd05..8db84757b51 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -21,6 +21,7 @@
 
 #include "common/formats/winexe.h"
 #include "common/config-manager.h"
+#include "common/crc.h"
 #include "common/endian.h"
 #include "common/events.h"
 #include "common/file.h"
@@ -64,6 +65,89 @@
 
 namespace VCruise {
 
+struct AD2044ItemInfo {
+	uint32 enNameCRC;
+	uint32 plNameCRC;
+	bool inspectable;
+};
+
+const AD2044ItemInfo g_ad2044ItemInfos[] = {
+	{0, 0, false},	// 0
+	{0, 0, false},	// 1
+	{0, 0, false},	// 2
+	{0, 0, false},	// 3
+	{0, 0, false},	// 4
+	{0, 0, false},	// 5
+	{0, 0, false},	// 6
+	{0, 0, false},	// 7
+	{0, 0, false},	// 8
+	{0, 0, false},	// 9
+	{0, 0, false},	// 10
+	{0, 0, false},	// 11
+	{0, 0, false},	// 12
+	{0, 0, false},	// 13
+	{0, 0, false},	// 14
+	{0, 0, false},	// 15
+	{0, 0, false},	// 16
+	{0, 0, false},	// 17
+	{0, 0, false},	// 18
+	{0, 0, false},	// 19
+	{0, 0, false},	// 20
+	{0, 0, false},	// 21
+	{0, 0, false},	// 22
+	{0, 0, false},	// 23
+	{0, 0, false},	// 24
+	{0, 0, false},	// 25
+	{0, 0, false},	// 26
+	{0, 0, false},	// 27
+	{0, 0, false},	// 28
+	{0, 0, false},	// 29
+	{0, 0, false},	// 30
+	{0, 0, false},	// 31
+	{0, 0, false},	// 32
+	{0, 0, false},	// 33
+	{0, 0, false},	// 34
+	{0, 0, false},	// 35
+	{0, 0, false},	// 36
+	{0, 0, false},	// 37
+	{0, 0, false},	// 38
+	{0, 0, false},	// 39
+	{0, 0, false},	// 40
+	{0, 0, false},	// 41
+	{0, 0, false},	// 42
+	{0, 0, false},	// 43
+	{0, 0, false},	// 44
+	{0, 0, false},	// 45
+	{0, 0, false},	// 46
+	{0, 0, false},	// 47
+	{0, 0, false},	// 48
+	{0, 0, false},	// 49
+	{0, 0, false},	// 50
+	{0, 0, false},	// 51
+	{0, 0, false},	// 52
+	{0, 0, false},	// 53
+	{0x83d54448, 0x839911EF, false}, // 54
+	{0, 0, false},	// 55
+	{0, 0, false},	// 56
+	{0, 0, false},	// 57
+	{0, 0, false},	// 58
+	{0, 0, false},	// 59
+	{0, 0, false},	// 60
+	{0, 0, false},	// 61
+	{0, 0, false},	// 62
+	{0, 0, false},	// 63
+	{0, 0, false},	// 64
+	{0, 0, false},	// 65
+	{0, 0, false},	// 66
+	{0, 0, false},	// 67
+	{0, 0, false},	// 68
+	{0, 0, false},	// 69
+	{0, 0, false},	// 70
+	{0, 0, false},	// 71
+	{0, 0, false},	// 72
+	{0, 0, false},	// 73
+};
+
 struct InitialItemPlacement {
 	uint roomNumber;
 	uint screenNumber;
@@ -75,6 +159,90 @@ const InitialItemPlacement g_ad2044InitialItemPlacements[] = {
 	{1, 0xac, 27},	// Matches
 };
 
+struct AD2044Graphics {
+	explicit AD2044Graphics(const Common::SharedPtr<Common::WinResources> &resources, bool lowQuality, const Graphics::PixelFormat &pixFmt);
+
+	Common::SharedPtr<Graphics::Surface> invDownClicked;
+	Common::SharedPtr<Graphics::Surface> invUpClicked;
+	Common::SharedPtr<Graphics::Surface> musicClicked;
+	Common::SharedPtr<Graphics::Surface> musicClickedDeep;
+	Common::SharedPtr<Graphics::Surface> soundClicked;
+	Common::SharedPtr<Graphics::Surface> soundClickedDeep;
+	Common::SharedPtr<Graphics::Surface> exitClicked;
+	Common::SharedPtr<Graphics::Surface> loadClicked;
+	Common::SharedPtr<Graphics::Surface> saveClicked;
+	Common::SharedPtr<Graphics::Surface> resizeClicked;
+	Common::SharedPtr<Graphics::Surface> musicVolUpClicked;
+	Common::SharedPtr<Graphics::Surface> musicVolDownClicked;
+	Common::SharedPtr<Graphics::Surface> music;
+	Common::SharedPtr<Graphics::Surface> musicVol;
+	Common::SharedPtr<Graphics::Surface> sound;
+	Common::SharedPtr<Graphics::Surface> soundVol;
+	Common::SharedPtr<Graphics::Surface> musicDisabled;
+	Common::SharedPtr<Graphics::Surface> musicVolDisabled;
+	Common::SharedPtr<Graphics::Surface> soundDisabled;
+	Common::SharedPtr<Graphics::Surface> soundVolDisabled;
+	Common::SharedPtr<Graphics::Surface> examine;
+	Common::SharedPtr<Graphics::Surface> examineDisabled;
+	Common::SharedPtr<Graphics::Surface> invPage[8];
+
+
+	void loadGraphic(Common::SharedPtr<Graphics::Surface> AD2044Graphics::*field, const Common::String &resName);
+	Common::SharedPtr<Graphics::Surface> loadGraphic(const Common::String &resName) const;
+	void finishLoading();
+
+private:
+	AD2044Graphics() = delete;
+
+	bool _lowQuality;
+	Common::SharedPtr<Common::WinResources> _resources;
+	Common::Array<Common::WinResourceID> _resourceIDs;
+	const Graphics::PixelFormat _pixFmt;
+};
+
+AD2044Graphics::AD2044Graphics(const Common::SharedPtr<Common::WinResources> &resources, bool lowQuality, const Graphics::PixelFormat &pixFmt)
+	: _resources(resources), _lowQuality(lowQuality), _pixFmt(pixFmt) {
+
+	_resourceIDs = resources->getIDList(Common::kWinBitmap);
+}
+
+void AD2044Graphics::loadGraphic(Common::SharedPtr<Graphics::Surface> AD2044Graphics::*field, const Common::String &resNameBase) {
+	this->*field = loadGraphic(resNameBase);
+}
+
+Common::SharedPtr<Graphics::Surface> AD2044Graphics::loadGraphic(const Common::String &resNameBase) const {
+	Common::String resName = _lowQuality ? (Common::String("D") + resNameBase) : resNameBase;
+
+	const Common::WinResourceID *resID = nullptr;
+	for (const Common::WinResourceID &resIDCandidate : _resourceIDs) {
+		if (resIDCandidate.getString() == resName) {
+			resID = &resIDCandidate;
+			break;
+		}
+	}
+
+	if (!resID)
+		error("Couldn't find bitmap graphic %s", resName.c_str());
+
+	Common::ScopedPtr<Common::SeekableReadStream> bmpResource(_resources->getResource(Common::kWinBitmap, *resID));
+
+	if (!bmpResource)
+		error("Couldn't open bitmap graphic %s", resName.c_str());
+
+	Image::BitmapDecoder decoder;
+	if (!decoder.loadStream(*bmpResource))
+		error("Couldn't load bitmap graphic %s", resName.c_str());
+
+	const Graphics::Surface *bmpSurf = decoder.getSurface();
+
+	Common::SharedPtr<Graphics::Surface> surf(bmpSurf->convertTo(_pixFmt, decoder.getPalette(), decoder.getPaletteColorCount()), Graphics::SurfaceDeleter());
+	return surf;
+}
+
+void AD2044Graphics::finishLoading() {
+	_resources.reset();
+}
+
 struct CodePageGuess {
 	Common::CodePage codePage;
 	Runtime::CharSet charSet;
@@ -1627,7 +1795,7 @@ bool Runtime::bootGame(bool newGame) {
 	debug(1, "Booting V-Cruise game...");
 
 	if (_gameID == GID_AD2044)
-		loadAD2044Index();
+		loadAD2044ExecutableResources();
 	else
 		loadReahSchizmIndex();
 
@@ -2933,7 +3101,7 @@ void Runtime::loadReahSchizmIndex() {
 	}
 }
 
-void Runtime::loadAD2044Index() {
+void Runtime::loadAD2044ExecutableResources() {
 	const byte searchPattern[] = {0x01, 0x01, 0xa1, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc7, 0x00, 0xc7, 0x00, 0x00, 0x00};
 
 	Common::File f;
@@ -2990,6 +3158,81 @@ void Runtime::loadAD2044Index() {
 
 		_ad2044AnimationDefs.push_back(animDef);
 	}
+
+	f.seek(0);
+
+	Common::SharedPtr<Common::WinResources> winRes(Common::WinResources::createFromEXE(&f));
+
+	if (!winRes)
+		error("Couldn't open executable resources");
+
+	_ad2044Graphics.reset(new AD2044Graphics(winRes, _lowQualityGraphicsMode, _gameSection.pixFmt));
+
+	_ad2044Graphics->loadGraphic(&AD2044Graphics::invDownClicked, "GDOL");
+	_ad2044Graphics->loadGraphic(&AD2044Graphics::invUpClicked, "GGORA");
+	_ad2044Graphics->loadGraphic(&AD2044Graphics::musicClickedDeep, "GUZ03");
+	_ad2044Graphics->loadGraphic(&AD2044Graphics::soundClickedDeep, "GUZ06");
+	_ad2044Graphics->loadGraphic(&AD2044Graphics::exitClicked, "GUZ1");
+	_ad2044Graphics->loadGraphic(&AD2044Graphics::saveClicked, "GUZ10");
+	_ad2044Graphics->loadGraphic(&AD2044Graphics::resizeClicked, "GUZ2");
+	_ad2044Graphics->loadGraphic(&AD2044Graphics::musicClicked, "GUZ3");
+	_ad2044Graphics->loadGraphic(&AD2044Graphics::soundClicked, "GUZ6");
+	_ad2044Graphics->loadGraphic(&AD2044Graphics::musicVolUpClicked, "GUZ7");
+	_ad2044Graphics->loadGraphic(&AD2044Graphics::musicVolDownClicked, "GUZ8");
+	_ad2044Graphics->loadGraphic(&AD2044Graphics::loadClicked, "GUZ9");
+	_ad2044Graphics->loadGraphic(&AD2044Graphics::music, "GUZN3");
+	_ad2044Graphics->loadGraphic(&AD2044Graphics::musicVol, "GUZN4");
+	_ad2044Graphics->loadGraphic(&AD2044Graphics::sound, "GUZN6");
+	_ad2044Graphics->loadGraphic(&AD2044Graphics::soundVol, "GUZN7");
+	_ad2044Graphics->loadGraphic(&AD2044Graphics::musicDisabled, "NIC3");
+	_ad2044Graphics->loadGraphic(&AD2044Graphics::musicVolDisabled, "NIC4");
+	_ad2044Graphics->loadGraphic(&AD2044Graphics::soundDisabled, "NIC6");
+	_ad2044Graphics->loadGraphic(&AD2044Graphics::soundVolDisabled, "NIC7");
+	_ad2044Graphics->loadGraphic(&AD2044Graphics::examine, "OKO");
+	_ad2044Graphics->loadGraphic(&AD2044Graphics::examineDisabled, "OKOZ");
+
+	for (int i = 0; i < 8; i++)
+		_ad2044Graphics->invPage[i] = _ad2044Graphics->loadGraphic(Common::String::format("POJ%i", static_cast<int>(i + 1)));
+
+	_ad2044Graphics->finishLoading();
+
+	Common::HashMap<uint32, uint32> stringHashToFilePos;
+
+	for (const AD2044ItemInfo &itemInfo : g_ad2044ItemInfos) {
+		stringHashToFilePos[itemInfo.enNameCRC] = 0;
+		stringHashToFilePos[itemInfo.plNameCRC] = 0;
+	}
+
+	stringHashToFilePos.erase(0);
+
+	// Scan for strings
+	Common::CRC32 crc;
+
+	uint32 strStartPos = 0;
+	uint32 rollingCRC = crc.getInitRemainder();
+
+	for (uint i = 0; i < exeContents.size(); i++) {
+		byte b = exeContents[i];
+		if (b == 0) {
+			uint32 strLength = i - strStartPos;
+			rollingCRC = crc.finalize(rollingCRC);
+			if (strLength != 0) {
+				Common::HashMap<uint32, uint32>::iterator it = stringHashToFilePos.find(rollingCRC);
+				if (it != stringHashToFilePos.end())
+					it->_value = strStartPos;
+			}
+
+#if 1
+			if (strStartPos == 100460) {
+				debug(1, "Check CRC was %u", rollingCRC);
+			}
+#endif
+
+			rollingCRC = crc.getInitRemainder();
+			strStartPos = i + 1;
+		} else
+			rollingCRC = crc.processByte(b, rollingCRC);
+	}
 }
 
 void Runtime::findWaves() {
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index f08a8560d98..5fef682b119 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -97,6 +97,7 @@ struct Instruction;
 struct RoomScriptSet;
 struct SoundLoopInfo;
 class SampleLoopAudioStream;
+struct AD2044Graphics;
 
 enum GameState {
 	kGameStateBoot,							// Booting the game
@@ -893,7 +894,7 @@ private:
 	void processUniversalKeymappedEvents(KeymappedEvent evt);
 
 	void loadReahSchizmIndex();
-	void loadAD2044Index();
+	void loadAD2044ExecutableResources();
 	void findWaves();
 	void loadConfig(const char *cfgPath);
 	void loadScore();
@@ -1450,6 +1451,7 @@ private:
 	static const uint kSoundCacheSize = 16;
 
 	static const uint kHeroChangeInteractionID = 0xffffffffu;
+	static const uint kObjectInteractionID = 0xfffffffeu;
 
 	Common::Pair<Common::String, Common::SharedPtr<SoundCache> > _soundCache[kSoundCacheSize];
 	uint _soundCacheIndex;
@@ -1494,6 +1496,8 @@ private:
 
 	// AD2044 tooltips
 	Common::String _tooltipText;
+
+	Common::SharedPtr<AD2044Graphics> _ad2044Graphics;
 };
 
 } // End of namespace VCruise


Commit: 712481918ebf40a04c9b33a272252db88f9753ed
    https://github.com/scummvm/scummvm/commit/712481918ebf40a04c9b33a272252db88f9753ed
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2024-03-31T14:39:29-04:00

Commit Message:
VCRUISE: Fix a bunch of things to get bathroom working

Changed paths:
  A engines/vcruise/ad2044_items.cpp
  A engines/vcruise/ad2044_items.h
  A engines/vcruise/ad2044_ui.cpp
  A engines/vcruise/ad2044_ui.h
    engines/vcruise/module.mk
    engines/vcruise/runtime.cpp
    engines/vcruise/runtime.h
    engines/vcruise/runtime_scriptexec.cpp
    engines/vcruise/script.cpp
    engines/vcruise/script.h


diff --git a/engines/vcruise/ad2044_items.cpp b/engines/vcruise/ad2044_items.cpp
new file mode 100644
index 00000000000..3fc142a1de6
--- /dev/null
+++ b/engines/vcruise/ad2044_items.cpp
@@ -0,0 +1,103 @@
+/* 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/ad2044_items.h"
+
+namespace VCruise {
+
+const AD2044ItemInfo g_ad2044ItemInfos[kNumAD2044Items] = {
+	{0, 0, 0, 0},                // 0
+	{0, 0, 0, 0},                // 1
+	{0, 0, 0, 0},                // 2
+	{0, 0, 0, 0},                // 3
+	{0, 0, 0, 0},                // 4
+	{0, 0, 0, 0},                // 5
+	{0, 0, 0, 0},                // 6
+	{0, 0, 0, 0},                // 7
+	{0, 0, 0, 0},                // 8
+	{0, 0, 0, 0},                // 9
+	{0, 0, 0, 0},                // 10
+	{0, 0, 0, 0},                // 11
+	{0, 0, 0, 0},                // 12
+	{0, 0, 0, 0},                // 13
+	{0, 0, 0, 0},                // 14
+	{0, 0, 0, 0},                // 15
+	{0, 0, 0, 0},                // 16
+	{0, 0, 0, 0},                // 17
+	{0, 0, 0x18, 0x128},         // 18
+	{0, 0, 0, 0},                // 19
+	{0, 0, 0, 0},                // 20
+	{0, 0, 0, 0},                // 21
+	{0, 0, 0, 0},                // 22
+	{0, 0, 0, 0},                // 23
+	{0, 0, 0, 0},                // 24
+	{0, 0, 0, 0},                // 25
+	{0, 0, 0, 0},                // 26
+	{0, 0, 0, 0},                // 27
+	{0, 0, 0, 0},                // 28
+	{0, 0, 0, 0},                // 29
+	{0, 0, 0, 0},                // 30
+	{0, 0, 0, 0},                // 31
+	{0, 0, 0, 0},                // 32
+	{0, 0, 0, 0},                // 33
+	{0, 0, 0, 0},                // 34
+	{0, 0, 0, 0},                // 35
+	{0, 0, 0, 0},                // 36
+	{0, 0, 0, 0},                // 37
+	{0, 0, 0, 0},                // 38
+	{0, 0, 0, 0},                // 39
+	{0, 0, 0, 0},                // 40
+	{0, 0, 0, 0},                // 41
+	{0, 0, 0, 0},                // 42
+	{0, 0, 0, 0},                // 43
+	{0, 0, 0, 0},                // 44
+	{0, 0, 0, 0},                // 45
+	{0, 0, 0, 0},                // 46
+	{0, 0, 0, 0},                // 47
+	{0, 0, 0, 0},                // 48
+	{0, 0, 0, 0},                // 49
+	{0, 0, 0, 0},                // 50
+	{0, 0, 0, 0},                // 51
+	{0, 0, 0, 0},                // 52
+	{0, 0, 0, 0},                // 53
+	{0x83d54448, 0x839911EF, 0, 0}, // 54
+	{0, 0, 0, 0},                // 55
+	{0, 0, 0, 0},                // 56
+	{0, 0, 0, 0},                // 57
+	{0, 0, 0, 0},                // 58
+	{0, 0, 0, 0},                // 59
+	{0, 0, 0, 0},                // 60
+	{0, 0, 0, 0},                // 61
+	{0, 0, 0, 0},                // 62
+	{0, 0, 0, 0},                // 63
+	{0, 0, 0, 0},                // 64
+	{0, 0, 0, 0},                // 65
+	{0, 0, 0, 0},                // 66
+	{0, 0, 0, 0},                // 67
+	{0, 0, 0, 0},                // 68
+	{0, 0, 0, 0},                // 69
+	{0, 0, 0, 0},                // 70
+	{0, 0, 0, 0},                // 71
+	{0, 0, 0, 0},                // 72
+	{0, 0, 0, 0},                // 73
+};
+
+} // End of namespace VCruise
diff --git a/engines/vcruise/ad2044_items.h b/engines/vcruise/ad2044_items.h
new file mode 100644
index 00000000000..fbb6dcb86eb
--- /dev/null
+++ b/engines/vcruise/ad2044_items.h
@@ -0,0 +1,43 @@
+/* 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_AD2044_ITEMS_H
+#define VCRUISE_AD2044_ITEMS_H
+
+#include "common/scummsys.h"
+
+namespace VCruise {
+
+struct AD2044ItemInfo {
+	uint32 enNameCRC;
+	uint32 plNameCRC;
+	uint16 inspectionScreenID;
+	uint16 scriptItemID;
+};
+
+static const uint kNumAD2044Items = 74;
+
+extern const AD2044ItemInfo g_ad2044ItemInfos[kNumAD2044Items];
+
+} // End of namespace VCruise
+
+
+#endif
diff --git a/engines/vcruise/ad2044_ui.cpp b/engines/vcruise/ad2044_ui.cpp
new file mode 100644
index 00000000000..d6572441f51
--- /dev/null
+++ b/engines/vcruise/ad2044_ui.cpp
@@ -0,0 +1,41 @@
+/* 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/ad2044_ui.h"
+
+namespace VCruise {
+
+namespace AD2044Interface {
+
+Common::Rect getRectForUI(AD2044InterfaceRectID rectID) {
+	switch (rectID) {
+	case AD2044InterfaceRectID::ActiveItemRender:
+		return Common::Rect(512, 150, 588, 217);
+	case AD2044InterfaceRectID::ExamineButton:
+		return Common::Rect(495, 248, 595, 318);
+	default:
+		return Common::Rect();
+	}
+}
+
+} // End of namespace AD2044Interface
+
+} // End of namespace VCruise
diff --git a/engines/vcruise/ad2044_ui.h b/engines/vcruise/ad2044_ui.h
new file mode 100644
index 00000000000..53f21fdb796
--- /dev/null
+++ b/engines/vcruise/ad2044_ui.h
@@ -0,0 +1,42 @@
+/* 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_AD2044_UI_H
+#define VCRUISE_AD2044_UI_H
+
+#include "common/rect.h"
+
+namespace VCruise {
+
+enum class AD2044InterfaceRectID {
+	ActiveItemRender,
+	ExamineButton,
+};
+
+namespace AD2044Interface {
+
+Common::Rect getRectForUI(AD2044InterfaceRectID rectID);
+
+} // End of namespace AD2044Interface
+
+} // End of namespace VCruise
+
+#endif
diff --git a/engines/vcruise/module.mk b/engines/vcruise/module.mk
index 32086488026..11fa090293d 100644
--- a/engines/vcruise/module.mk
+++ b/engines/vcruise/module.mk
@@ -1,6 +1,8 @@
 MODULE := engines/vcruise
 
 MODULE_OBJS = \
+	ad2044_items.o \
+	ad2044_ui.o \
 	audio_player.o \
 	circuitpuzzle.o \
 	metaengine.o \
diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 8db84757b51..7bca07f29de 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -53,6 +53,8 @@
 
 #include "gui/message.h"
 
+#include "vcruise/ad2044_items.h"
+#include "vcruise/ad2044_ui.h"
 #include "vcruise/audio_player.h"
 #include "vcruise/circuitpuzzle.h"
 #include "vcruise/sampleloop.h"
@@ -65,89 +67,6 @@
 
 namespace VCruise {
 
-struct AD2044ItemInfo {
-	uint32 enNameCRC;
-	uint32 plNameCRC;
-	bool inspectable;
-};
-
-const AD2044ItemInfo g_ad2044ItemInfos[] = {
-	{0, 0, false},	// 0
-	{0, 0, false},	// 1
-	{0, 0, false},	// 2
-	{0, 0, false},	// 3
-	{0, 0, false},	// 4
-	{0, 0, false},	// 5
-	{0, 0, false},	// 6
-	{0, 0, false},	// 7
-	{0, 0, false},	// 8
-	{0, 0, false},	// 9
-	{0, 0, false},	// 10
-	{0, 0, false},	// 11
-	{0, 0, false},	// 12
-	{0, 0, false},	// 13
-	{0, 0, false},	// 14
-	{0, 0, false},	// 15
-	{0, 0, false},	// 16
-	{0, 0, false},	// 17
-	{0, 0, false},	// 18
-	{0, 0, false},	// 19
-	{0, 0, false},	// 20
-	{0, 0, false},	// 21
-	{0, 0, false},	// 22
-	{0, 0, false},	// 23
-	{0, 0, false},	// 24
-	{0, 0, false},	// 25
-	{0, 0, false},	// 26
-	{0, 0, false},	// 27
-	{0, 0, false},	// 28
-	{0, 0, false},	// 29
-	{0, 0, false},	// 30
-	{0, 0, false},	// 31
-	{0, 0, false},	// 32
-	{0, 0, false},	// 33
-	{0, 0, false},	// 34
-	{0, 0, false},	// 35
-	{0, 0, false},	// 36
-	{0, 0, false},	// 37
-	{0, 0, false},	// 38
-	{0, 0, false},	// 39
-	{0, 0, false},	// 40
-	{0, 0, false},	// 41
-	{0, 0, false},	// 42
-	{0, 0, false},	// 43
-	{0, 0, false},	// 44
-	{0, 0, false},	// 45
-	{0, 0, false},	// 46
-	{0, 0, false},	// 47
-	{0, 0, false},	// 48
-	{0, 0, false},	// 49
-	{0, 0, false},	// 50
-	{0, 0, false},	// 51
-	{0, 0, false},	// 52
-	{0, 0, false},	// 53
-	{0x83d54448, 0x839911EF, false}, // 54
-	{0, 0, false},	// 55
-	{0, 0, false},	// 56
-	{0, 0, false},	// 57
-	{0, 0, false},	// 58
-	{0, 0, false},	// 59
-	{0, 0, false},	// 60
-	{0, 0, false},	// 61
-	{0, 0, false},	// 62
-	{0, 0, false},	// 63
-	{0, 0, false},	// 64
-	{0, 0, false},	// 65
-	{0, 0, false},	// 66
-	{0, 0, false},	// 67
-	{0, 0, false},	// 68
-	{0, 0, false},	// 69
-	{0, 0, false},	// 70
-	{0, 0, false},	// 71
-	{0, 0, false},	// 72
-	{0, 0, false},	// 73
-};
-
 struct InitialItemPlacement {
 	uint roomNumber;
 	uint screenNumber;
@@ -155,6 +74,7 @@ struct InitialItemPlacement {
 };
 
 const InitialItemPlacement g_ad2044InitialItemPlacements[] = {
+	{1, 0xb0, 18},	// Spoon
 	{1, 0xb8, 24},	// Cigarette pack
 	{1, 0xac, 27},	// Matches
 };
@@ -534,6 +454,17 @@ const AD2044MapLoader::ScreenOverride AD2044MapLoader::sk_screenOverrides[] = {
 	{1, 0xb6, 145},	// After pushing the button to open the capsule
 	{1, 0x6a, 142},	// Opening an apple on the table
 	{1, 0x6b, 143}, // Clicking the tablet in the apple
+	{1, 0x6c, 144}, // Table facing the center of the room with soup bowl empty
+
+	// Room 23
+	{23, 0xbb, 127}, // Bathroom entry point
+	{23, 0xbc, 128}, // Sink
+	{23, 0xbd, 129}, // Looking at toilet, seat down
+	{23, 0xbe, 130}, // Looking at toilet, seat up
+	{23, 0x61, 133}, // Bathroom looking at boots
+	{23, 0x62, 134}, // Looking behind boots
+	{23, 0x63, 135}, // Standing behind toilet looking at sink
+	{23, 0x64, 136}, // Looking under toilet
 };
 
 AD2044MapLoader::AD2044MapLoader() : _roomNumber(0), _screenNumber(0), _isLoaded(false) {
@@ -589,7 +520,7 @@ void AD2044MapLoader::load() {
 	debug(1, "Loading screen map %s", mapFileName.toString(Common::Path::kNativeSeparator).c_str());
 
 	if (!mapFile.open(mapFileName)) {
-		error("Couldn't resolve map file for room %u screen %u", _roomNumber, _screenNumber);
+		error("Couldn't resolve map file for room %u screen %x", _roomNumber, _screenNumber);
 	}
 
 	_currentMap = loadScreenDirectionDef(mapFile);
@@ -1519,7 +1450,7 @@ Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &roo
 	  _listenerX(0), _listenerY(0), _listenerAngle(0), _soundCacheIndex(0),
 	  _isInGame(false),
 	  _subtitleFont(nullptr), _isDisplayingSubtitles(false), _isSubtitleSourceAnimation(false),
-	  _languageIndex(0), _defaultLanguageIndex(0), _defaultLanguage(defaultLanguage), _charSet(kCharSetLatin),
+	  _languageIndex(0), _defaultLanguageIndex(0), _defaultLanguage(defaultLanguage), _language(defaultLanguage), _charSet(kCharSetLatin),
 	  _isCDVariant(false), _currentAnimatedCursor(nullptr), _currentCursor(nullptr), _cursorTimeBase(0), _cursorCycleLength(0),
 	  _inventoryActivePage(0) {
 
@@ -1931,6 +1862,8 @@ bool Runtime::bootGame(bool newGame) {
 		}
 	}
 
+	_language = lang;
+
 	debug(2, "Language index: %u   Default language index: %u", _languageIndex, _defaultLanguageIndex);
 
 	Common::CodePage codePage = Common::CodePage::kASCII;
@@ -3918,9 +3851,24 @@ bool Runtime::dischargeIdleMouseMove() {
 	uint interactionID = 0;
 	if (sdDef && !_idleLockInteractions) {
 		for (const InteractionDef &idef : sdDef->interactions) {
-			if (idef.objectType == 1 && idef.rect.contains(relMouse)) {
+			if (idef.objectType == 1 && interactionID == 0 && idef.rect.contains(relMouse)) {
 				isOnInteraction = true;
 				interactionID = idef.interactionID;
+			}
+
+			if (_gameID == GID_AD2044 && idef.objectType == 3 && idef.rect.contains(relMouse)) {
+				uint32 locationID = getLocationForScreen(_roomNumber, _screenNumber);
+				if (_placedItems.find(locationID) == _placedItems.end()) {
+					if (_inventoryActiveItem.itemID != 0) {
+						isOnInteraction = true;
+						interactionID = kObjectDropInteractionID;
+					}
+				} else {
+					if (_inventoryActiveItem.itemID == 0) {
+						isOnInteraction = true;
+						interactionID = kObjectPickupInteractionID;
+					}
+				}
 				break;
 			}
 		}
@@ -3995,6 +3943,12 @@ bool Runtime::dischargeIdleMouseMove() {
 		if (interactionID == kHeroChangeInteractionID) {
 			changeToCursor(_cursors[16]);
 			_idleHaveClickInteraction = true;
+		} else if (interactionID == kObjectDropInteractionID) {
+			changeToCursor(_cursors[7]);
+			_idleHaveClickInteraction = true;
+		} else if (interactionID == kObjectPickupInteractionID) {
+			changeToCursor(_cursors[8]);
+			_idleHaveClickInteraction = true;
 		} else {
 			// New interaction, is there a script?
 			Common::SharedPtr<Script> script = findScriptForInteraction(interactionID);
@@ -4047,11 +4001,21 @@ bool Runtime::dischargeIdleClick() {
 		if (_gameID == GID_SCHIZM && _idleInteractionID == kHeroChangeInteractionID) {
 			changeHero();
 			return true;
+		} else if (_gameID == GID_AD2044 && _idleInteractionID == kObjectDropInteractionID) {
+			dropActiveItem();
+			recordSaveGameSnapshot();
+			_havePendingReturnToIdleState = true;
+			return true;
+		} else if (_gameID == GID_AD2044 && _idleInteractionID == kObjectPickupInteractionID) {
+			pickupPlacedItem();
+			recordSaveGameSnapshot();
+			_havePendingReturnToIdleState = true;
+			return true;
 		} else {
 			// Interaction, is there a script?
 			Common::SharedPtr<Script> script = findScriptForInteraction(_idleInteractionID);
 
-			_idleIsOnInteraction = false; // ?
+			_idleIsOnInteraction = false; // Clear so new interactions at the same mouse coord are detected
 
 			if (script) {
 				ScriptEnvironmentVars vars;
@@ -5749,6 +5713,71 @@ void Runtime::clearPlacedItemGraphic() {
 	}
 }
 
+void Runtime::drawActiveItemGraphic() {
+	if (_inventoryActiveItem.graphic) {
+		Common::Rect itemRect = AD2044Interface::getRectForUI(AD2044InterfaceRectID::ActiveItemRender);
+
+		_fullscreenMenuSection.surf->blitFrom(*_inventoryActiveItem.graphic, Common::Point(itemRect.left, itemRect.top));
+		drawSectionToScreen(_fullscreenMenuSection, itemRect);
+	}
+
+	if (g_ad2044ItemInfos[_inventoryActiveItem.itemID].inspectionScreenID != 0) {
+		Common::Rect examineRect = AD2044Interface::getRectForUI(AD2044InterfaceRectID::ExamineButton);
+
+		_fullscreenMenuSection.surf->blitFrom(*_ad2044Graphics->examine, Common::Point(examineRect.left, examineRect.top));
+		drawSectionToScreen(_fullscreenMenuSection, examineRect);
+	}
+}
+
+void Runtime::clearActiveItemGraphic() {
+	Common::Rect rectsToClear[] = {
+		AD2044Interface::getRectForUI(AD2044InterfaceRectID::ActiveItemRender),
+		AD2044Interface::getRectForUI(AD2044InterfaceRectID::ExamineButton),
+	};
+
+	for (const Common::Rect &rectToClear : rectsToClear) {
+		_fullscreenMenuSection.surf->blitFrom(*_backgroundGraphic, rectToClear, rectToClear);
+		drawSectionToScreen(_fullscreenMenuSection, rectToClear);
+	}
+}
+
+void Runtime::dropActiveItem() {
+	if (_inventoryActiveItem.itemID == 0)
+		return;
+
+	uint32 locationID = getLocationForScreen(_roomNumber, _screenNumber);
+	uint8 &placedItemIDRef = _placedItems[locationID];
+
+	if (placedItemIDRef == 0) {
+		placedItemIDRef = static_cast<uint8>(_inventoryActiveItem.itemID);
+		_inventoryPlacedItemCache = _inventoryActiveItem;
+		_inventoryActiveItem = InventoryItem();
+	}
+
+	drawPlacedItemGraphic();
+	clearActiveItemGraphic();
+}
+
+void Runtime::pickupPlacedItem() {
+	if (_inventoryActiveItem.itemID != 0)
+		return;
+
+	uint32 locationID = getLocationForScreen(_roomNumber, _screenNumber);
+	Common::HashMap<uint32, uint8>::iterator placedItemIt = _placedItems.find(locationID);
+	if (placedItemIt == _placedItems.end())
+		return;
+
+	if (placedItemIt->_value != _inventoryPlacedItemCache.itemID)
+		error("Placed item cache desynced somehow, please report this as a bug");
+
+	_placedItems.erase(placedItemIt);
+	_inventoryActiveItem = _inventoryPlacedItemCache;
+	_inventoryPlacedItemCache = InventoryItem();
+
+	clearPlacedItemGraphic();
+	drawActiveItemGraphic();
+}
+
 void Runtime::getFileNamesForItemGraphic(uint itemID, Common::String &outFileName, Common::String &outAlphaFileName) const {
 	if (_gameID == GID_REAH)
 		outFileName = Common::String::format("Thing%u", itemID);
@@ -6507,7 +6536,7 @@ void Runtime::onKeymappedEvent(KeymappedEvent kme) {
 
 bool Runtime::canSave(bool onCurrentScreen) const {
 	if (onCurrentScreen) {
-		return (_mostRecentlyRecordedSaveState.get() != nullptr && (_haveHorizPanAnimations || _forceAllowSaves));
+		return (_mostRecentlyRecordedSaveState.get() != nullptr && (_haveHorizPanAnimations || _forceAllowSaves || _gameID == GID_AD2044));
 	} else {
 		return _mostRecentValidSaveState.get() != nullptr && _isInGame;
 	}
@@ -6902,6 +6931,10 @@ Common::SharedPtr<SaveGameSnapshot> Runtime::generateNewGameSnapshot() const {
 		}
 
 		snapshot->pagedItems.push_back(item);
+
+		// Alt state is for item inspection
+		snapshot->numStates = 2;
+		snapshot->states[1].reset(new SaveGameSwappableState());
 	}
 
 	return snapshot;
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index 5fef682b119..6787604567a 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -987,6 +987,10 @@ private:
 	void updatePlacedItemCache();
 	void drawPlacedItemGraphic();
 	void clearPlacedItemGraphic();
+	void drawActiveItemGraphic();
+	void clearActiveItemGraphic();
+	void dropActiveItem();
+	void pickupPlacedItem();
 
 	void getFileNamesForItemGraphic(uint itemID, Common::String &outGraphicFileName, Common::String &outAlphaFileName) const;
 	Common::SharedPtr<Graphics::Surface> loadGraphic(const Common::String &graphicName, const Common::String &alphaName, bool required);
@@ -1220,6 +1224,7 @@ private:
 	void scriptOpSay2K(ScriptArg_t arg);
 	void scriptOpSay3K(ScriptArg_t arg);
 	void scriptOpRGet(ScriptArg_t arg);
+	void scriptOpRSet(ScriptArg_t arg);
 
 	Common::Array<Common::SharedPtr<AnimatedCursor> > _cursors;      // Cursors indexed as CURSOR_CUR_##
 	Common::Array<Common::SharedPtr<AnimatedCursor> > _cursorsShort;      // Cursors indexed as CURSOR_#
@@ -1451,7 +1456,8 @@ private:
 	static const uint kSoundCacheSize = 16;
 
 	static const uint kHeroChangeInteractionID = 0xffffffffu;
-	static const uint kObjectInteractionID = 0xfffffffeu;
+	static const uint kObjectDropInteractionID = 0xfffffffeu;
+	static const uint kObjectPickupInteractionID = 0xfffffffdu;
 
 	Common::Pair<Common::String, Common::SharedPtr<SoundCache> > _soundCache[kSoundCacheSize];
 	uint _soundCacheIndex;
@@ -1465,6 +1471,7 @@ private:
 	Common::SharedPtr<Graphics::Font> _subtitleFontKeepalive;
 	uint _defaultLanguageIndex;
 	uint _languageIndex;
+	Common::Language _language;
 	CharSet _charSet;
 	bool _isCDVariant;
 	StartConfigDef _startConfigs[kNumStartConfigs];
@@ -1496,6 +1503,7 @@ private:
 
 	// AD2044 tooltips
 	Common::String _tooltipText;
+	Common::String _subtitleText;
 
 	Common::SharedPtr<AD2044Graphics> _ad2044Graphics;
 };
diff --git a/engines/vcruise/runtime_scriptexec.cpp b/engines/vcruise/runtime_scriptexec.cpp
index 937d0be55a8..6fa373be2e1 100644
--- a/engines/vcruise/runtime_scriptexec.cpp
+++ b/engines/vcruise/runtime_scriptexec.cpp
@@ -21,6 +21,7 @@
 
 #include "common/random.h"
 
+#include "vcruise/ad2044_items.h"
 #include "vcruise/audio_player.h"
 #include "vcruise/circuitpuzzle.h"
 #include "vcruise/runtime.h"
@@ -2042,7 +2043,28 @@ void Runtime::scriptOpPuzzleDone(ScriptArg_t arg) {
 }
 
 // AD2044 ops
-OPCODE_STUB(AnimT)
+void Runtime::scriptOpAnimT(ScriptArg_t arg) {
+	TAKE_STACK_INT(1);
+
+	StackInt_t animationID = stackArgs[0];
+
+	Common::HashMap<int, AnimFrameRange>::const_iterator animRangeIt = _animIDToFrameRange.find(animationID);
+	if (animRangeIt == _animIDToFrameRange.end())
+		error("Couldn't resolve animation ID %i", static_cast<int>(animationID));
+
+	AnimationDef animDef;
+	animDef.animNum = animRangeIt->_value.animationNum;
+	animDef.firstFrame = animRangeIt->_value.firstFrame;
+	animDef.lastFrame = animRangeIt->_value.lastFrame;
+
+	_haveIdleAnimations[0] = true;
+
+	StaticAnimation &outAnim = _idleAnimations[0];
+
+	outAnim = StaticAnimation();
+	outAnim.animDefs[0] = animDef;
+	outAnim.animDefs[1] = animDef;
+}
 
 void Runtime::scriptOpAnimAD2044(bool isForward) {
 	TAKE_STACK_INT(2);
@@ -2142,15 +2164,37 @@ void Runtime::scriptOpM(ScriptArg_t arg) {
 void Runtime::scriptOpEM(ScriptArg_t arg) {
 }
 
-OPCODE_STUB(SE)
-OPCODE_STUB(SDot)
+void Runtime::scriptOpSE(ScriptArg_t arg) {
+	// English subtitle
+	if (_language == Common::PL_POL)
+		return;
+
+	_subtitleText = _scriptSet->strings[arg];
+}
+
+void Runtime::scriptOpSDot(ScriptArg_t arg) {
+	// Polish subtitle
+	if (_language == Common::PL_POL)
+		return;
+
+	_subtitleText = _scriptSet->strings[arg];
+}
 
 void Runtime::scriptOpE(ScriptArg_t arg) {
+	if (_language == Common::PL_POL)
+		return;
+
 	_tooltipText = _scriptSet->strings[arg];
 	redrawSubtitleSection();
 }
 
-OPCODE_STUB(Dot)
+void Runtime::scriptOpDot(ScriptArg_t arg) {
+	if (_language != Common::PL_POL)
+		return;
+
+	_tooltipText = _scriptSet->strings[arg];
+	redrawSubtitleSection();
+}
 
 void Runtime::scriptOpSound(ScriptArg_t arg) {
 	TAKE_STACK_INT(2);
@@ -2161,7 +2205,23 @@ void Runtime::scriptOpISound(ScriptArg_t arg) {
 }
 
 OPCODE_STUB(USound)
-OPCODE_STUB(RGet)
+
+void Runtime::scriptOpRGet(ScriptArg_t arg) {
+	StackInt_t itemID = 0x2000;
+
+	if (_inventoryActiveItem.itemID < kNumAD2044Items) {
+		itemID = g_ad2044ItemInfos[_inventoryActiveItem.itemID].scriptItemID;
+		if (itemID == 0) {
+			warning("No script item ID for item type %i", static_cast<int>(_inventoryActiveItem.itemID));
+			itemID = 0x2000;
+		}
+	} else
+		error("Invalid item ID");
+
+	_scriptStack.push_back(StackValue(itemID));
+}
+
+OPCODE_STUB(RSet)
 
 
 // Unused Schizm ops
@@ -2414,10 +2474,14 @@ bool Runtime::runScript() {
 			DISPATCH_OP(SE);
 			DISPATCH_OP(SDot);
 			DISPATCH_OP(E);
+			DISPATCH_OP(Dot);
 
 			DISPATCH_OP(Sound);
 			DISPATCH_OP(ISound);
 
+			DISPATCH_OP(RGet);
+			DISPATCH_OP(RSet);
+
 		default:
 			error("Unimplemented opcode %i", static_cast<int>(instr.op));
 		}
diff --git a/engines/vcruise/script.cpp b/engines/vcruise/script.cpp
index 92d6abdbd71..f7ceec3c806 100644
--- a/engines/vcruise/script.cpp
+++ b/engines/vcruise/script.cpp
@@ -156,11 +156,11 @@ public:
 	void compileScriptSet(ScriptSet *ss);
 
 private:
-	bool parseNumber(const Common::String &token, uint32 &outNumber) const;
+	bool parseNumber(const Common::String &token, uint32 &outNumber, bool mayStartWithNonZeroDigit) const;
 	static bool parseDecNumber(const Common::String &token, uint start, uint32 &outNumber);
 	static bool parseHexNumber(const Common::String &token, uint start, uint32 &outNumber);
 	static bool parseBinNumber(const Common::String &token, uint start, uint32 &outNumber);
-	void expectNumber(uint32 &outNumber);
+	void expectNumber(uint32 &outNumber, bool mayStartWithNonZeroDigit);
 
 	void compileRoomScriptSet(RoomScriptSet *rss);
 	void compileReahOrAD2044ScreenScriptSet(ScreenScriptSet *sss);
@@ -232,7 +232,7 @@ ScriptCompiler::ScriptCompiler(TextParser &parser, const Common::Path &blamePath
 	  _scrToken(nullptr), _eroomToken(nullptr) {
 }
 
-bool ScriptCompiler::parseNumber(const Common::String &token, uint32 &outNumber) const {
+bool ScriptCompiler::parseNumber(const Common::String &token, uint32 &outNumber, bool mayStartWithNonZeroDigit) const {
 	if (token.size() == 0)
 		return false;
 
@@ -240,7 +240,16 @@ bool ScriptCompiler::parseNumber(const Common::String &token, uint32 &outNumber)
 		return parseDecNumber(token, 1, outNumber);
 
 	if (_dialect == kScriptDialectReah || _dialect == kScriptDialectAD2044) {
-		if (token[0] == '0') {
+		bool isValidNumber = false;
+
+		char firstChar = token[0];
+
+		if (_dialect == kScriptDialectReah || !mayStartWithNonZeroDigit)
+			isValidNumber = (firstChar == '0');
+		else
+			isValidNumber = (firstChar >= '0' && firstChar <= '9');	// Some room numbers lack a 0 prefix
+
+		if (isValidNumber) {
 			switch (_numberParsingMode) {
 			case kNumberParsingDec:
 				return parseDecNumber(token, 0, outNumber);
@@ -329,11 +338,11 @@ bool ScriptCompiler::parseBinNumber(const Common::String &token, uint start, uin
 	return true;
 }
 
-void ScriptCompiler::expectNumber(uint32 &outNumber) {
+void ScriptCompiler::expectNumber(uint32 &outNumber, bool mayStartWithNonZeroDigit) {
 	TextParserState state;
 	Common::String token;
 	if (_parser.parseToken(token, state)) {
-		if (!parseNumber(token, outNumber))
+		if (!parseNumber(token, outNumber, mayStartWithNonZeroDigit))
 			error("Error compiling script at line %i col %i: Expected number but found '%s'", static_cast<int>(state._lineNum), static_cast<int>(state._col), token.c_str());
 	} else {
 		error("Error compiling script at line %i col %i: Expected number", static_cast<int>(state._lineNum), static_cast<int>(state._col));
@@ -373,7 +382,7 @@ void ScriptCompiler::compileScriptSet(ScriptSet *ss) {
 				uint32 roomNumber = 0;
 
 				if (_parser.parseToken(token, state)) {
-					if (!parseNumber(token, roomNumber))
+					if (!parseNumber(token, roomNumber, true))
 						error("Error compiling script at line %i col %i: Expected number but found '%s'", static_cast<int>(state._lineNum), static_cast<int>(state._col), token.c_str());
 
 					ss->roomScripts[roomNumber] = roomScript;
@@ -406,7 +415,7 @@ void ScriptCompiler::compileRoomScriptSet(RoomScriptSet *rss) {
 			return;
 		} else if (token == _scrToken) {
 			uint32 screenNumber = 0;
-			expectNumber(screenNumber);
+			expectNumber(screenNumber, false);
 
 			Common::SharedPtr<ScreenScriptSet> sss(new ScreenScriptSet());
 			if (_dialect == kScriptDialectReah || _dialect == kScriptDialectAD2044)
@@ -438,7 +447,7 @@ void ScriptCompiler::compileRoomScriptSet(RoomScriptSet *rss) {
 			}
 
 			uint32 number = 0;
-			if (!parseNumber(value, number))
+			if (!parseNumber(value, number, false))
 				error("Error compiling script at line %i col %i: Expected number", static_cast<int>(state._lineNum), static_cast<int>(state._col));
 
 			int32 signedNumber = static_cast<int32>(number);
@@ -492,7 +501,7 @@ void ScriptCompiler::compileReahOrAD2044ScreenScriptSet(ScreenScriptSet *sss) {
 			return;
 		} else if (token == "~*") {
 			uint32 interactionNumber = 0;
-			expectNumber(interactionNumber);
+			expectNumber(interactionNumber, false);
 
 			codeGenScript(protoScript, *currentScript);
 
@@ -535,7 +544,7 @@ void ScriptCompiler::compileSchizmScreenScriptSet(ScreenScriptSet *sss) {
 			return;
 		} else if (token == "~*") {
 			uint32 interactionNumber = 0;
-			expectNumber(interactionNumber);
+			expectNumber(interactionNumber, false);
 
 			codeGenScript(protoScript, *currentScript);
 
@@ -601,6 +610,7 @@ static ScriptNamedInstruction g_ad2044NamedInstructions[] = {
 	//{"r?", ProtoOp::kProtoOpScript, ScriptOps::kItemHaveSpace},
 	//{"r!", ProtoOp::kProtoOpScript, ScriptOps::kItemAdd},
 	{"r@", ProtoOp::kProtoOpScript, ScriptOps::kRGet},
+	{"r!", ProtoOp::kProtoOpScript, ScriptOps::kRSet},
 	//{"clearPocket", ProtoOp::kProtoOpScript, ScriptOps::kItemClear},
 	{"cursor!", ProtoOp::kProtoOpScript, ScriptOps::kSetCursor},
 	{"room!", ProtoOp::kProtoOpScript, ScriptOps::kSetRoom},
@@ -964,15 +974,15 @@ bool ScriptCompiler::compileInstructionToken(ProtoScript &script, const Common::
 	}
 
 	if (_dialect == kScriptDialectSchizm && token.hasPrefix("-")) {
-		uint32 unumber = 0;
-		if (parseNumber(token.substr(1), unumber)) {
-			script.instrs.push_back(ProtoInstruction(ScriptOps::kNumber, -static_cast<int32>(unumber)));
+		uint32 number = 0;
+		if (parseNumber(token.substr(1), number, false)) {
+			script.instrs.push_back(ProtoInstruction(ScriptOps::kNumber, -static_cast<int32>(number)));
 			return true;
 		}
 	}
 
 	uint32 number = 0;
-	if (parseNumber(token, number)) {
+	if (parseNumber(token, number, false)) {
 		script.instrs.push_back(ProtoInstruction(ScriptOps::kNumber, number));
 		return true;
 	}
@@ -1016,7 +1026,7 @@ bool ScriptCompiler::compileInstructionToken(ProtoScript &script, const Common::
 	if (token == "#case") {
 		uint32 caseNumber = 0;
 		_parser.expect(":", _blamePath);
-		expectNumber(caseNumber);
+		expectNumber(caseNumber, false);
 
 		script.instrs.push_back(ProtoInstruction(kProtoOpCase, ScriptOps::kInvalid, caseNumber));
 		return true;
@@ -1024,7 +1034,7 @@ bool ScriptCompiler::compileInstructionToken(ProtoScript &script, const Common::
 
 	if (token == "#case:") {
 		uint32 caseNumber = 0;
-		expectNumber(caseNumber);
+		expectNumber(caseNumber, false);
 
 		script.instrs.push_back(ProtoInstruction(kProtoOpCase, ScriptOps::kInvalid, caseNumber));
 		return true;
diff --git a/engines/vcruise/script.h b/engines/vcruise/script.h
index a758183d59c..0900991c8fa 100644
--- a/engines/vcruise/script.h
+++ b/engines/vcruise/script.h
@@ -241,6 +241,7 @@ enum ScriptOp {
 	kSay2K,
 	kSay3K,
 	kRGet,
+	kRSet,
 
 	kNumOps,
 };


Commit: 3b1bebf0a2b4ba98a45dd51f4fca921146eca058
    https://github.com/scummvm/scummvm/commit/3b1bebf0a2b4ba98a45dd51f4fca921146eca058
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2024-03-31T14:39:29-04:00

Commit Message:
VCRUISE: Fix up bathroom mirror behavior

Changed paths:
    engines/vcruise/ad2044_items.cpp
    engines/vcruise/runtime.cpp
    engines/vcruise/runtime_scriptexec.cpp


diff --git a/engines/vcruise/ad2044_items.cpp b/engines/vcruise/ad2044_items.cpp
index 3fc142a1de6..216ef7f86e9 100644
--- a/engines/vcruise/ad2044_items.cpp
+++ b/engines/vcruise/ad2044_items.cpp
@@ -84,7 +84,7 @@ const AD2044ItemInfo g_ad2044ItemInfos[kNumAD2044Items] = {
 	{0, 0, 0, 0},                // 57
 	{0, 0, 0, 0},                // 58
 	{0, 0, 0, 0},                // 59
-	{0, 0, 0, 0},                // 60
+	{0, 0, 0, 0x170},            // 60
 	{0, 0, 0, 0},                // 61
 	{0, 0, 0, 0},                // 62
 	{0, 0, 0, 0},                // 63
diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 7bca07f29de..cb3b1aa3e18 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -457,6 +457,11 @@ const AD2044MapLoader::ScreenOverride AD2044MapLoader::sk_screenOverrides[] = {
 	{1, 0x6c, 144}, // Table facing the center of the room with soup bowl empty
 
 	// Room 23
+	{23, 0xa3, 103}, // Looking at high mirror
+	{23, 0xa4, 104}, // After taking mirror
+
+	{23, 0xb9, 125}, // Bathroom looking down the stairs
+	//{23, 0xb9, 126}, // ???
 	{23, 0xbb, 127}, // Bathroom entry point
 	{23, 0xbc, 128}, // Sink
 	{23, 0xbd, 129}, // Looking at toilet, seat down
@@ -1606,6 +1611,8 @@ void Runtime::loadCursors(const char *exeName) {
 		_namedCursors["CUR_NAC"] = 5; // Nac = top?  Not sure.  But this is the finger pointer.
 		_namedCursors["CUR_TYL"] = 2; // Tyl = back
 		_namedCursors["CUR_OTWORZ"] = 11; // Otworz = open
+		_namedCursors["CUR_WEZ"] = 8; // Wez = Pick up
+		_namedCursors["CUR_ZOSTAW"] = 7; // Put down
 	}
 
 	_panCursors[kPanCursorDraggableHoriz | kPanCursorDraggableUp] = 2;
diff --git a/engines/vcruise/runtime_scriptexec.cpp b/engines/vcruise/runtime_scriptexec.cpp
index 6fa373be2e1..98108b184a6 100644
--- a/engines/vcruise/runtime_scriptexec.cpp
+++ b/engines/vcruise/runtime_scriptexec.cpp
@@ -2211,7 +2211,7 @@ void Runtime::scriptOpRGet(ScriptArg_t arg) {
 
 	if (_inventoryActiveItem.itemID < kNumAD2044Items) {
 		itemID = g_ad2044ItemInfos[_inventoryActiveItem.itemID].scriptItemID;
-		if (itemID == 0) {
+		if (itemID == 0 && _inventoryActiveItem.itemID != 0) {
 			warning("No script item ID for item type %i", static_cast<int>(_inventoryActiveItem.itemID));
 			itemID = 0x2000;
 		}
@@ -2221,7 +2221,30 @@ void Runtime::scriptOpRGet(ScriptArg_t arg) {
 	_scriptStack.push_back(StackValue(itemID));
 }
 
-OPCODE_STUB(RSet)
+void Runtime::scriptOpRSet(ScriptArg_t arg) {
+	TAKE_STACK_INT(1);
+
+	for (uint itemID = 0; itemID < kNumAD2044Items; itemID++) {
+		if (static_cast<StackInt_t>(g_ad2044ItemInfos[itemID].scriptItemID) == stackArgs[0]) {
+
+			if (_inventoryActiveItem.itemID != itemID) {
+				Common::String itemFileName;
+				Common::String alphaFileName;
+
+				_inventoryActiveItem.itemID = itemID;
+				getFileNamesForItemGraphic(itemID, itemFileName, alphaFileName);
+				_inventoryActiveItem.graphic = loadGraphic(itemFileName, alphaFileName, false);
+
+				clearActiveItemGraphic();
+				drawActiveItemGraphic();
+			}
+			return;
+		}
+	}
+
+	// NYI
+	error("Couldn't resolve item ID for script item %i", static_cast<int>(stackArgs[0]));
+}
 
 
 // Unused Schizm ops


Commit: a90317c90db0b1b49c0f6a1f1af040ab1908e1d1
    https://github.com/scummvm/scummvm/commit/a90317c90db0b1b49c0f6a1f1af040ab1908e1d1
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2024-03-31T14:39:29-04:00

Commit Message:
VCRUISE: Stub out inventory interactions

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


diff --git a/engines/vcruise/ad2044_ui.cpp b/engines/vcruise/ad2044_ui.cpp
index d6572441f51..e46da440698 100644
--- a/engines/vcruise/ad2044_ui.cpp
+++ b/engines/vcruise/ad2044_ui.cpp
@@ -31,11 +31,31 @@ Common::Rect getRectForUI(AD2044InterfaceRectID rectID) {
 		return Common::Rect(512, 150, 588, 217);
 	case AD2044InterfaceRectID::ExamineButton:
 		return Common::Rect(495, 248, 595, 318);
+	case AD2044InterfaceRectID::InventoryRender0:
+		return Common::Rect(24, 394, 100, 461);
+	case AD2044InterfaceRectID::InventoryRender1:
+		return Common::Rect(119, 395, 195, 462);
+	case AD2044InterfaceRectID::InventoryRender2:
+		return Common::Rect(209, 393, 285, 460);
+	case AD2044InterfaceRectID::InventoryRender3:
+		return Common::Rect(302, 393, 378, 460);
+	case AD2044InterfaceRectID::InventoryRender4:
+		return Common::Rect(393, 394, 469, 461);
+	case AD2044InterfaceRectID::InventoryRender5:
+		return Common::Rect(481, 393, 557, 460);
 	default:
 		return Common::Rect();
 	}
 }
 
+Common::Rect getFirstInvSlotRect() {
+	return Common::Rect(21, 392, 96, 460);
+}
+
+uint getInvSlotSpacing() {
+	return 92;
+}
+
 } // End of namespace AD2044Interface
 
 } // End of namespace VCruise
diff --git a/engines/vcruise/ad2044_ui.h b/engines/vcruise/ad2044_ui.h
index 53f21fdb796..aaf7cd911b0 100644
--- a/engines/vcruise/ad2044_ui.h
+++ b/engines/vcruise/ad2044_ui.h
@@ -29,11 +29,22 @@ namespace VCruise {
 enum class AD2044InterfaceRectID {
 	ActiveItemRender,
 	ExamineButton,
+
+	InventoryRender0,
+	InventoryRender1,
+	InventoryRender2,
+	InventoryRender3,
+	InventoryRender4,
+	InventoryRender5,
+
 };
 
 namespace AD2044Interface {
 
 Common::Rect getRectForUI(AD2044InterfaceRectID rectID);
+Common::Rect getFirstInvSlotRect();
+uint getInvSlotSpacing();
+
 
 } // End of namespace AD2044Interface
 
diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index cb3b1aa3e18..c12db4875c7 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -3888,6 +3888,29 @@ bool Runtime::dischargeIdleMouseMove() {
 		}
 	}
 
+	if (_gameID == GID_AD2044 && !isOnInteraction) {
+		Common::Rect invSlotRect = AD2044Interface::getFirstInvSlotRect();
+
+		for (uint i = 0; i < kNumInventorySlots; i++) {
+			bool isItemInInventory = (_inventory[i].itemID != 0);
+			bool isItemActive = (_inventoryActiveItem.itemID != 0);
+
+			if (invSlotRect.contains(_mousePos)) {
+				if (isItemInInventory && !isItemActive) {
+					isOnInteraction = true;
+					interactionID = kPickupInventorySlot0InteractionID + i;
+				} else if (!isItemInInventory && isItemActive) {
+					isOnInteraction = true;
+					interactionID = kReturnInventorySlot0InteractionID + i;
+				}
+
+				break;
+			}
+
+			invSlotRect.translate(static_cast<int16>(AD2044Interface::getInvSlotSpacing()), 0);
+		}
+	}
+
 	if (_idleIsOnInteraction && (!isOnInteraction || interactionID != _idleInteractionID)) {
 		// Mouse left the previous interaction
 		_idleIsOnInteraction = false;
@@ -3950,10 +3973,10 @@ bool Runtime::dischargeIdleMouseMove() {
 		if (interactionID == kHeroChangeInteractionID) {
 			changeToCursor(_cursors[16]);
 			_idleHaveClickInteraction = true;
-		} else if (interactionID == kObjectDropInteractionID) {
+		} else if (interactionID == kObjectDropInteractionID || (interactionID >= kReturnInventorySlot0InteractionID && interactionID <= kReturnInventorySlot5InteractionID)) {
 			changeToCursor(_cursors[7]);
 			_idleHaveClickInteraction = true;
-		} else if (interactionID == kObjectPickupInteractionID) {
+		} else if (interactionID == kObjectPickupInteractionID || (interactionID >= kPickupInventorySlot0InteractionID && interactionID <= kPickupInventorySlot5InteractionID)) {
 			changeToCursor(_cursors[8]);
 			_idleHaveClickInteraction = true;
 		} else {
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index 6787604567a..547ea72d40a 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -1459,6 +1459,20 @@ private:
 	static const uint kObjectDropInteractionID = 0xfffffffeu;
 	static const uint kObjectPickupInteractionID = 0xfffffffdu;
 
+	static const uint kReturnInventorySlot0InteractionID = 0xfffffff1u;
+	static const uint kReturnInventorySlot1InteractionID = 0xfffffff2u;
+	static const uint kReturnInventorySlot2InteractionID = 0xfffffff3u;
+	static const uint kReturnInventorySlot3InteractionID = 0xfffffff4u;
+	static const uint kReturnInventorySlot4InteractionID = 0xfffffff5u;
+	static const uint kReturnInventorySlot5InteractionID = 0xfffffff6u;
+
+	static const uint kPickupInventorySlot0InteractionID = 0xfffffff7u;
+	static const uint kPickupInventorySlot1InteractionID = 0xfffffff8u;
+	static const uint kPickupInventorySlot2InteractionID = 0xfffffff9u;
+	static const uint kPickupInventorySlot3InteractionID = 0xfffffffau;
+	static const uint kPickupInventorySlot4InteractionID = 0xfffffffbu;
+	static const uint kPickupInventorySlot5InteractionID = 0xfffffffcu;
+
 	Common::Pair<Common::String, Common::SharedPtr<SoundCache> > _soundCache[kSoundCacheSize];
 	uint _soundCacheIndex;
 


Commit: 68925452137538a051b0bcd749fda6704a484834
    https://github.com/scummvm/scummvm/commit/68925452137538a051b0bcd749fda6704a484834
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2024-03-31T14:39:29-04:00

Commit Message:
VCRUISE: Add inventory pickup/stash

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


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index c12db4875c7..19bb2e508a7 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -4041,6 +4041,16 @@ bool Runtime::dischargeIdleClick() {
 			recordSaveGameSnapshot();
 			_havePendingReturnToIdleState = true;
 			return true;
+		} else if (_gameID == GID_AD2044 && _idleInteractionID >= kPickupInventorySlot0InteractionID && _idleInteractionID <= kPickupInventorySlot5InteractionID) {
+			pickupInventoryItem(_idleInteractionID - kPickupInventorySlot0InteractionID);
+			recordSaveGameSnapshot();
+			_havePendingReturnToIdleState = true;
+			return true;
+		} else if (_gameID == GID_AD2044 && _idleInteractionID >= kReturnInventorySlot0InteractionID && _idleInteractionID <= kReturnInventorySlot5InteractionID) {
+			stashActiveItemToInventory(_idleInteractionID - kReturnInventorySlot0InteractionID);
+			recordSaveGameSnapshot();
+			_havePendingReturnToIdleState = true;
+			return true;
 		} else {
 			// Interaction, is there a script?
 			Common::SharedPtr<Script> script = findScriptForInteraction(_idleInteractionID);
@@ -5771,6 +5781,22 @@ void Runtime::clearActiveItemGraphic() {
 	}
 }
 
+void Runtime::drawInventoryItemGraphic(uint slot) {
+	if (_inventory[slot].graphic) {
+		Common::Rect rect = AD2044Interface::getRectForUI(static_cast<AD2044InterfaceRectID>(static_cast<uint>(AD2044InterfaceRectID::InventoryRender0) + slot));
+
+		_fullscreenMenuSection.surf->blitFrom(*_inventory[slot].graphic, Common::Point(rect.left, rect.top));
+		drawSectionToScreen(_fullscreenMenuSection, rect);
+	}
+}
+
+void Runtime::clearInventoryItemGraphic(uint slot) {
+	Common::Rect rect = AD2044Interface::getRectForUI(static_cast<AD2044InterfaceRectID>(static_cast<uint>(AD2044InterfaceRectID::InventoryRender0) + slot));
+
+	_fullscreenMenuSection.surf->blitFrom(*_backgroundGraphic, rect, rect);
+	drawSectionToScreen(_fullscreenMenuSection, rect);
+}
+
 void Runtime::dropActiveItem() {
 	if (_inventoryActiveItem.itemID == 0)
 		return;
@@ -5808,6 +5834,35 @@ void Runtime::pickupPlacedItem() {
 	drawActiveItemGraphic();
 }
 
+void Runtime::stashActiveItemToInventory(uint slot) {
+	if (_inventoryActiveItem.itemID == 0)
+		return;
+
+	if (_inventory[slot].itemID != 0)
+		return;
+
+	_inventory[slot] = _inventoryActiveItem;
+	_inventoryActiveItem = InventoryItem();
+
+
+	clearActiveItemGraphic();
+	drawInventoryItemGraphic(slot);
+}
+
+void Runtime::pickupInventoryItem(uint slot) {
+	if (_inventoryActiveItem.itemID != 0)
+		return;
+
+	if (_inventory[slot].itemID == 0)
+		return;
+
+	_inventoryActiveItem = _inventory[slot];
+	_inventory[slot] = InventoryItem();
+
+	clearInventoryItemGraphic(slot);
+	drawActiveItemGraphic();
+}
+
 void Runtime::getFileNamesForItemGraphic(uint itemID, Common::String &outFileName, Common::String &outAlphaFileName) const {
 	if (_gameID == GID_REAH)
 		outFileName = Common::String::format("Thing%u", itemID);
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index 547ea72d40a..85a280cb60f 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -989,8 +989,12 @@ private:
 	void clearPlacedItemGraphic();
 	void drawActiveItemGraphic();
 	void clearActiveItemGraphic();
+	void drawInventoryItemGraphic(uint slot);
+	void clearInventoryItemGraphic(uint slot);
 	void dropActiveItem();
 	void pickupPlacedItem();
+	void stashActiveItemToInventory(uint slot);
+	void pickupInventoryItem(uint slot);
 
 	void getFileNamesForItemGraphic(uint itemID, Common::String &outGraphicFileName, Common::String &outAlphaFileName) const;
 	Common::SharedPtr<Graphics::Surface> loadGraphic(const Common::String &graphicName, const Common::String &alphaName, bool required);


Commit: 00c7323818446f6afc890fec7700842c8a76fd71
    https://github.com/scummvm/scummvm/commit/00c7323818446f6afc890fec7700842c8a76fd71
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2024-03-31T14:39:29-04:00

Commit Message:
VCRUISE: Stub Say3K op

Changed paths:
    engines/vcruise/runtime_scriptexec.cpp


diff --git a/engines/vcruise/runtime_scriptexec.cpp b/engines/vcruise/runtime_scriptexec.cpp
index 98108b184a6..6bc31bb9ed9 100644
--- a/engines/vcruise/runtime_scriptexec.cpp
+++ b/engines/vcruise/runtime_scriptexec.cpp
@@ -2505,6 +2505,8 @@ bool Runtime::runScript() {
 			DISPATCH_OP(RGet);
 			DISPATCH_OP(RSet);
 
+			DISPATCH_OP(Say3K);
+
 		default:
 			error("Unimplemented opcode %i", static_cast<int>(instr.op));
 		}


Commit: 965e7afab6a17c6634ac170afb7a2567c8028596
    https://github.com/scummvm/scummvm/commit/965e7afab6a17c6634ac170afb7a2567c8028596
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2024-03-31T14:39:29-04:00

Commit Message:
VCRUISE: Fix static looping animations not persisting through reload

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


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 19bb2e508a7..6936eccd0a9 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -77,6 +77,8 @@ const InitialItemPlacement g_ad2044InitialItemPlacements[] = {
 	{1, 0xb0, 18},	// Spoon
 	{1, 0xb8, 24},	// Cigarette pack
 	{1, 0xac, 27},	// Matches
+	{1, 0x62, 2},	// "A" tag
+	{1, 0x64, 58},  // Newspaper
 };
 
 struct AD2044Graphics {
@@ -452,6 +454,7 @@ private:
 const AD2044MapLoader::ScreenOverride AD2044MapLoader::sk_screenOverrides[] = {
 	// Room 1
 	{1, 0xb6, 145},	// After pushing the button to open the capsule
+	{1, 0x66, 138}, // Looking at banner
 	{1, 0x6a, 142},	// Opening an apple on the table
 	{1, 0x6b, 143}, // Clicking the tablet in the apple
 	{1, 0x6c, 144}, // Table facing the center of the room with soup bowl empty
@@ -1093,7 +1096,8 @@ void SaveGameSwappableState::Sound::read(Common::ReadStream *stream, uint saveGa
 
 SaveGameSwappableState::SaveGameSwappableState() : roomNumber(0), screenNumber(0), direction(0), disc(0), havePendingPostSwapScreenReset(false),
 												   musicTrack(0), musicVolume(100), musicActive(true), musicMuteDisabled(false), animVolume(100),
-												   loadedAnimation(0), animDisplayingFrame(0) {
+												   loadedAnimation(0), animDisplayingFrame(0), haveIdleAnimationLoop(false), idleAnimNum(0), idleFirstFrame(0), idleLastFrame(0)
+{
 }
 
 SaveGameSnapshot::PagedInventoryItem::PagedInventoryItem() : page(0), slot(0), itemID(0) {
@@ -1140,6 +1144,12 @@ void SaveGameSnapshot::write(Common::WriteStream *stream) const {
 		stream->writeUint32BE(states[sti]->direction);
 		stream->writeUint32BE(states[sti]->disc);
 		stream->writeByte(states[sti]->havePendingPostSwapScreenReset ? 1 : 0);
+		stream->writeByte(states[sti]->haveIdleAnimationLoop ? 1 : 0);
+		if (states[sti]->haveIdleAnimationLoop) {
+			stream->writeUint32BE(states[sti]->idleAnimNum);
+			stream->writeUint32BE(states[sti]->idleFirstFrame);
+			stream->writeUint32BE(states[sti]->idleLastFrame);
+		}
 	}
 
 	stream->writeUint32BE(hero);
@@ -1262,6 +1272,17 @@ LoadGameOutcome SaveGameSnapshot::read(Common::ReadStream *stream) {
 
 		if (saveVersion >= 7)
 			states[sti]->havePendingPostSwapScreenReset = (stream->readByte() != 0);
+
+		if (saveVersion >= 10)
+			states[sti]->haveIdleAnimationLoop = (stream->readByte() != 0);
+		else
+			states[sti]->haveIdleAnimationLoop = false;
+
+		if (states[sti]->haveIdleAnimationLoop) {
+			states[sti]->idleAnimNum = stream->readUint32BE();
+			states[sti]->idleFirstFrame = stream->readUint32BE();
+			states[sti]->idleLastFrame = stream->readUint32BE();
+		}
 	}
 
 	if (saveVersion >= 6) {
@@ -1457,7 +1478,7 @@ Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &roo
 	  _subtitleFont(nullptr), _isDisplayingSubtitles(false), _isSubtitleSourceAnimation(false),
 	  _languageIndex(0), _defaultLanguageIndex(0), _defaultLanguage(defaultLanguage), _language(defaultLanguage), _charSet(kCharSetLatin),
 	  _isCDVariant(false), _currentAnimatedCursor(nullptr), _currentCursor(nullptr), _cursorTimeBase(0), _cursorCycleLength(0),
-	  _inventoryActivePage(0) {
+	  _inventoryActivePage(0), _keepStaticAnimationInIdle(false) {
 
 	for (uint i = 0; i < kNumDirections; i++) {
 		_haveIdleAnimations[i] = false;
@@ -3598,19 +3619,16 @@ void Runtime::changeToScreen(uint roomNumber, uint screenNumber) {
 			_havePanDownFromDirection[i] = false;
 		}
 
-		clearIdleAnimations();
-
-		for (uint i = 0; i < kNumDirections; i++)
-			_haveIdleAnimations[i] = false;
+		if (!_keepStaticAnimationInIdle)
+			clearIdleAnimations();
 
-		_havePendingPreIdleActions = true;
-		_haveIdleStaticAnimation = false;
-		_idleCurrentStaticAnimation.clear();
-		_havePendingPlayAmbientSounds = true;
 		_forceAllowSaves = false;
 
 		recordSaveGameSnapshot();
 
+		if (_keepStaticAnimationInIdle)
+			_keepStaticAnimationInIdle = false;
+
 		_placedItemRect = Common::Rect();
 		if (_gameID == GID_AD2044) {
 			const MapScreenDirectionDef *screenDef = _mapLoader->getScreenDirection(_screenNumber, _direction);
@@ -3644,9 +3662,9 @@ void Runtime::clearIdleAnimations() {
 	for (uint i = 0; i < kNumDirections; i++)
 		_haveIdleAnimations[i] = false;
 
-	_havePendingPreIdleActions = true;
 	_haveIdleStaticAnimation = false;
 	_idleCurrentStaticAnimation.clear();
+	_havePendingPreIdleActions = true;
 	_havePendingPlayAmbientSounds = true;
 }
 
@@ -5451,7 +5469,7 @@ void Runtime::clearTray() {
 		_traySection.surf->fillRect(trayRect, blackColor);
 	}
 
-	this->commitSectionToScreen(_traySection, trayRect);
+	drawSectionToScreen(_traySection, trayRect);
 }
 
 void Runtime::redrawSubtitleSection() {
@@ -5500,7 +5518,7 @@ void Runtime::drawSubtitleText(const Common::Array<Common::U32String> &lines, co
 	Graphics::ManagedSurface *surf = stSection.surf.get();
 
 	if (_subtitleFont) {
-		int lineHeight = _subtitleFont->getFontHeight();
+		int lineHeight = (_gameID == GID_AD2044) ? 24 : _subtitleFont->getFontHeight();
 
 		int xOffset = 0;
 		int topY = 0;
@@ -5531,8 +5549,10 @@ void Runtime::drawInventory(uint slot) {
 	if (!isTrayVisible())
 		return;
 
-	if (_gameID == GID_AD2044)
+	if (_gameID == GID_AD2044) {
+		drawInventoryItemGraphic(slot);
 		return;
+	}
 
 	Common::Rect trayRect = _traySection.rect;
 	trayRect.translate(-trayRect.left, -trayRect.top);
@@ -6698,6 +6718,17 @@ void Runtime::recordSaveGameSnapshot() {
 	mainState->loadedAnimation = _loadedAnimation;
 	mainState->animDisplayingFrame = _animDisplayingFrame;
 
+	if (_gameID == GID_AD2044) {
+		mainState->haveIdleAnimationLoop = _haveIdleAnimations[_direction];
+
+		if (mainState->haveIdleAnimationLoop) {
+			mainState->idleAnimNum = _idleAnimations[0].animDefs[0].animNum;
+			mainState->idleFirstFrame = _idleAnimations[0].animDefs[0].firstFrame;
+			mainState->idleLastFrame = _idleAnimations[0].animDefs[0].lastFrame;
+		}
+	} else
+		mainState->haveIdleAnimationLoop = false;
+
 	recordSounds(*mainState);
 
 	snapshot->pendingSoundParams3D = _pendingSoundParams3D;
@@ -6849,6 +6880,7 @@ void Runtime::restoreSaveGameSnapshot() {
 		}
 	}
 
+	_keepStaticAnimationInIdle = false;
 	_roomNumber = mainState->roomNumber;
 	_screenNumber = mainState->screenNumber;
 	_direction = mainState->direction;
@@ -6966,6 +6998,32 @@ void Runtime::restoreSaveGameSnapshot() {
 	stopSubtitles();
 	clearScreen();
 	redrawTray();
+
+	if (_gameID == GID_AD2044) {
+		drawActiveItemGraphic();
+
+		clearIdleAnimations();
+
+		if (mainState->haveIdleAnimationLoop) {
+			_keepStaticAnimationInIdle = true;
+
+			
+			AnimationDef idleAnimDef;
+			idleAnimDef.animNum = mainState->idleAnimNum;
+			idleAnimDef.firstFrame = mainState->idleFirstFrame;
+			idleAnimDef.lastFrame = mainState->idleLastFrame;
+
+			_keepStaticAnimationInIdle = true;
+
+			_haveIdleAnimations[0] = true;
+
+			StaticAnimation staticAnim;
+			staticAnim.animDefs[0] = idleAnimDef;
+			staticAnim.animDefs[1] = idleAnimDef;
+
+			_idleAnimations[0] = staticAnim;
+		}
+	}
 }
 
 Common::SharedPtr<SaveGameSnapshot> Runtime::generateNewGameSnapshot() const {
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index 85a280cb60f..bad38036954 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -459,6 +459,10 @@ struct SaveGameSwappableState {
 
 	uint loadedAnimation;
 	uint animDisplayingFrame;
+	bool haveIdleAnimationLoop;
+	uint idleAnimNum;
+	uint idleFirstFrame;
+	uint idleLastFrame;
 
 	int musicTrack;
 
@@ -1276,6 +1280,7 @@ private:
 	StaticAnimation _idleAnimations[kNumDirections];
 	bool _haveIdleAnimations[kNumDirections];
 	bool _haveIdleStaticAnimation;
+	bool _keepStaticAnimationInIdle;
 	Common::String _idleCurrentStaticAnimation;
 	StaticAnimParams _pendingStaticAnimParams;
 
diff --git a/engines/vcruise/runtime_scriptexec.cpp b/engines/vcruise/runtime_scriptexec.cpp
index 6bc31bb9ed9..0fb60010c22 100644
--- a/engines/vcruise/runtime_scriptexec.cpp
+++ b/engines/vcruise/runtime_scriptexec.cpp
@@ -2057,6 +2057,8 @@ void Runtime::scriptOpAnimT(ScriptArg_t arg) {
 	animDef.firstFrame = animRangeIt->_value.firstFrame;
 	animDef.lastFrame = animRangeIt->_value.lastFrame;
 
+	_keepStaticAnimationInIdle = true;
+
 	_haveIdleAnimations[0] = true;
 
 	StaticAnimation &outAnim = _idleAnimations[0];


Commit: d1b08c9960810f81bcba9e67218acbe623a66778
    https://github.com/scummvm/scummvm/commit/d1b08c9960810f81bcba9e67218acbe623a66778
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2024-03-31T14:39:29-04:00

Commit Message:
VCRUISE: Add MIDI playback

Changed paths:
  A engines/vcruise/midi_player.cpp
  A engines/vcruise/midi_player.h
    engines/vcruise/module.mk
    engines/vcruise/runtime.cpp
    engines/vcruise/runtime.h
    engines/vcruise/runtime_scriptexec.cpp
    engines/vcruise/vcruise.cpp
    engines/vcruise/vcruise.h


diff --git a/engines/vcruise/midi_player.cpp b/engines/vcruise/midi_player.cpp
new file mode 100644
index 00000000000..cf464122cbd
--- /dev/null
+++ b/engines/vcruise/midi_player.cpp
@@ -0,0 +1,79 @@
+/* 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 "audio/mididrv.h"
+#include "audio/midiparser_smf.h"
+
+#include "vcruise/midi_player.h"
+
+namespace VCruise {
+
+MidiPlayer::MidiPlayer(MidiDriver *midiDrv, Common::Array<byte> &&musicData, int volume)
+	: _midiDrv(midiDrv), _data(Common::move(musicData)), _volume(-1) {
+	_parser.reset(MidiParser::createParser_SMF());
+
+	if (_data.size() > 0u && _parser->loadMusic(&_data[0], _data.size())) {
+		_parser->setTrack(0);
+		_parser->setMidiDriver(_midiDrv);
+		_parser->startPlaying();
+		_parser->property(MidiParser::mpAutoLoop, 1);
+		_parser->setTimerRate(_midiDrv->getBaseTempo());
+
+		setVolume(volume);
+	} else
+		_parser.reset();
+}
+
+MidiPlayer::~MidiPlayer() {
+	if (_parser)
+		_parser->stopPlaying();
+}
+
+void MidiPlayer::setVolume(int volume) {
+	if (!_parser)
+		return;
+
+	if (volume > 100)
+		volume = 100;
+	else if (volume < 0)
+		volume = 0;
+
+	volume = 51;
+
+	//uint32 effectiveValue = static_cast<uint32>(floor(sqrt(sqrt(volume)) * 5180.76));
+	uint32 effectiveValue = static_cast<uint32>(volume * 0x3fff / 100);
+
+	if (effectiveValue > 0x3fffu)
+		effectiveValue = 0x3fffu;
+
+	byte masterVolMessage[6] = {
+		0x7f, 0x00, 0x04, 0x01, (effectiveValue & 0x7f), ((effectiveValue >> 7) & 0x7f)
+	};
+
+	_midiDrv->sysEx(masterVolMessage, 6);
+}
+
+void MidiPlayer::onMidiTimer() {
+	if (_parser)
+		_parser->onTimer();
+}
+
+} // End of namespace VCruise
diff --git a/engines/vcruise/midi_player.h b/engines/vcruise/midi_player.h
new file mode 100644
index 00000000000..67db4ce251f
--- /dev/null
+++ b/engines/vcruise/midi_player.h
@@ -0,0 +1,57 @@
+/* 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_MIDI_PLAYER_H
+#define VCRUISE_MIDI_PLAYER_H
+
+#include "common/array.h"
+#include "common/mutex.h"
+#include "common/ptr.h"
+
+class MidiDriver;
+class MidiParser;
+
+namespace Common {
+
+class SeekableReadStream;
+
+} // End of namespace Common
+
+namespace VCruise {
+
+class MidiPlayer {
+public:
+	MidiPlayer(MidiDriver *midiDrv, Common::Array<byte> &&musicData, int volume);
+	~MidiPlayer();
+
+	void setVolume(int volume);
+	void onMidiTimer();
+
+private:
+	MidiDriver *_midiDrv;
+	Common::SharedPtr<MidiParser> _parser;
+	Common::Array<byte> _data;
+	int _volume;
+};
+
+} // End of namespace VCruise
+
+#endif
diff --git a/engines/vcruise/module.mk b/engines/vcruise/module.mk
index 11fa090293d..e29593a95ce 100644
--- a/engines/vcruise/module.mk
+++ b/engines/vcruise/module.mk
@@ -5,6 +5,7 @@ MODULE_OBJS = \
 	ad2044_ui.o \
 	audio_player.o \
 	circuitpuzzle.o \
+	midi_player.o \
 	metaengine.o \
 	menu.o \
 	runtime.o \
diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 6936eccd0a9..bd6bd3b5069 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -58,6 +58,7 @@
 #include "vcruise/audio_player.h"
 #include "vcruise/circuitpuzzle.h"
 #include "vcruise/sampleloop.h"
+#include "vcruise/midi_player.h"
 #include "vcruise/menu.h"
 #include "vcruise/runtime.h"
 #include "vcruise/script.h"
@@ -1455,8 +1456,8 @@ void SaveGameSnapshot::writeString(Common::WriteStream *stream, const Common::St
 FontCacheItem::FontCacheItem() : font(nullptr), size(0) {
 }
 
-Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &rootFSNode, VCruiseGameID gameID, Common::Language defaultLanguage)
-	: _system(system), _mixer(mixer), _roomNumber(1), _screenNumber(0), _direction(0), _hero(0), _disc(0), _swapOutRoom(0), _swapOutScreen(0), _swapOutDirection(0),
+Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, MidiDriver *midiDrv, const Common::FSNode &rootFSNode, VCruiseGameID gameID, Common::Language defaultLanguage)
+	: _system(system), _mixer(mixer), _midiDrv(midiDrv), _roomNumber(1), _screenNumber(0), _direction(0), _hero(0), _disc(0), _swapOutRoom(0), _swapOutScreen(0), _swapOutDirection(0),
 	  _haveHorizPanAnimations(false), _loadedRoomNumber(0), _activeScreenNumber(0),
 	  _gameState(kGameStateBoot), _gameID(gameID), _havePendingScreenChange(false), _forceScreenChange(false), _havePendingPreIdleActions(false), _havePendingReturnToIdleState(false), _havePendingPostSwapScreenReset(false),
 	  _havePendingCompletionCheck(false), _havePendingPlayAmbientSounds(false), _ambientSoundFinishTime(0), _escOn(false), _debugMode(false), _fastAnimationMode(false), _lowQualityGraphicsMode(false),
@@ -2065,6 +2066,14 @@ void Runtime::drawLabel(Graphics::ManagedSurface *surface, const Common::String
 	font->drawString(surface, text, textPos.x, textPos.y, strWidth, realTextColor);
 }
 
+void Runtime::onMidiTimer() {
+	if (_musicMidiPlayer) {
+		Common::StackLock lock(_midiPlayerMutex);
+		_musicMidiPlayer->onMidiTimer();
+	}
+}
+
+
 bool Runtime::runIdle() {
 	if (_havePendingScreenChange) {
 		_havePendingScreenChange = false;
@@ -4267,10 +4276,15 @@ void Runtime::loadTabData(uint animNumber, Common::SeekableReadStream *stream) {
 }
 
 void Runtime::changeMusicTrack(int track) {
-	if (track == _musicTrack && _musicPlayer.get() != nullptr)
+	if (track == _musicTrack && _musicWavePlayer.get() != nullptr && _musicMidiPlayer.get() != nullptr)
 		return;
 
-	_musicPlayer.reset();
+	_musicWavePlayer.reset();
+	if (_musicMidiPlayer)
+	{
+		Common::StackLock lock(_midiPlayerMutex);
+		_musicMidiPlayer.reset();
+	}
 	_musicTrack = track;
 
 	if (!_musicActive)
@@ -4279,23 +4293,46 @@ void Runtime::changeMusicTrack(int track) {
 	if (_musicMute && !_musicMuteDisabled)
 		return;
 
-	Common::Path 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));
+	Common::String musicPathStr;
+
+	if (_gameID == GID_AD2044)
+		musicPathStr = Common::String::format("sfx/music%02i.mid", static_cast<int>(track));
+	else
+		musicPathStr = Common::String::format("Sfx/Music-%02i.wav", static_cast<int>(track));
+
+	Common::Path musicFileName(musicPathStr);
+	Common::File *musicFile = new Common::File();
+	if (musicFile->open(musicFileName)) {
+		if (_gameID == GID_AD2044) {
+			Common::ScopedPtr<Common::File> fileHolder(musicFile);
+
+			uint musicFileSize = static_cast<uint>(musicFile->size());
+
+			if (musicFileSize > 0) {
+				Common::Array<byte> musicData;
+				musicData.resize(musicFileSize);
 
-			_musicPlayer.reset(new AudioPlayer(_mixer, loopingStream, Audio::Mixer::kMusicSoundType));
-			_musicPlayer->play(applyVolumeScale(_musicVolume), 0);
+				musicFile->read(&musicData[0], musicFileSize);
+
+				Common::StackLock lock(_midiPlayerMutex);
+				_musicMidiPlayer.reset(new MidiPlayer(_midiDrv, Common::move(musicData), _musicVolume));
+			}
+		} else {
+			if (Audio::SeekableAudioStream *audioStream = Audio::makeWAVStream(musicFile, DisposeAfterUse::YES)) {
+				Common::SharedPtr<Audio::AudioStream> loopingStream(Audio::makeLoopingAudioStream(audioStream, 0));
+
+				_musicWavePlayer.reset(new AudioPlayer(_mixer, loopingStream, Audio::Mixer::kMusicSoundType));
+				_musicWavePlayer->play(applyVolumeScale(_musicVolume), 0);
+			}
 		}
 	} else {
-		warning("Music file '%s' is missing", wavFileName.toString(Common::Path::kNativeSeparator).c_str());
-		delete wavFile;
+		warning("Music file '%s' is missing", musicFileName.toString(Common::Path::kNativeSeparator).c_str());
+		delete musicFile;
 	}
 }
 
 void Runtime::startScoreSection() {
-	_musicPlayer.reset();
+	_musicWavePlayer.reset();
 	_scoreSectionEndTime = 0;
 
 	if (!_musicActive)
@@ -4322,8 +4359,8 @@ void Runtime::startScoreSection() {
 				Common::File *trackFile = new Common::File();
 				if (trackFile->open(trackFileName)) {
 					if (Audio::SeekableAudioStream *audioStream = Audio::makeVorbisStream(trackFile, DisposeAfterUse::YES)) {
-						_musicPlayer.reset(new AudioPlayer(_mixer, Common::SharedPtr<Audio::AudioStream>(audioStream), Audio::Mixer::kMusicSoundType));
-						_musicPlayer->play(applyVolumeScale(sectionDef.volumeOrDurationInSeconds), 0);
+						_musicWavePlayer.reset(new AudioPlayer(_mixer, Common::SharedPtr<Audio::AudioStream>(audioStream), Audio::Mixer::kMusicSoundType));
+						_musicWavePlayer->play(applyVolumeScale(sectionDef.volumeOrDurationInSeconds), 0);
 
 						_scoreSectionEndTime = static_cast<uint32>(audioStream->getLength().msecs()) + g_system->getMillis();
 					} else {
@@ -4352,11 +4389,16 @@ void Runtime::setMusicMute(bool muted) {
 	if (prevIsActuallyMuted != isActuallyMuted) {
 		if (isActuallyMuted) {
 			// Became muted
-			_musicPlayer.reset();
+			_musicWavePlayer.reset();
+			if (_musicMidiPlayer)
+			{
+				Common::StackLock lock(_midiPlayerMutex);
+				_musicMidiPlayer.reset();
+			}
 			_scoreSectionEndTime = 0;
 		} else {
 			// Became unmuted
-			if (_gameID == GID_REAH)
+			if (_gameID == GID_REAH || _gameID == GID_AD2044)
 				changeMusicTrack(_musicTrack);
 			else if (_gameID == GID_SCHIZM)
 				startScoreSection();
@@ -4791,8 +4833,13 @@ void Runtime::updateSounds(uint32 timestamp) {
 			newVolume += static_cast<int32>(ramp);
 
 		if (newVolume != _musicVolume) {
-			if (_musicPlayer)
-				_musicPlayer->setVolume(applyVolumeScale(newVolume));
+			if (_musicWavePlayer)
+				_musicWavePlayer->setVolume(applyVolumeScale(newVolume));
+
+			if (_musicMidiPlayer) {
+				Common::StackLock lock(_midiPlayerMutex);
+				_musicMidiPlayer->setVolume(newVolume);
+			}
 			_musicVolume = newVolume;
 		}
 
@@ -6921,7 +6968,7 @@ void Runtime::restoreSaveGameSnapshot() {
 		_musicMuteDisabled = mainState->musicMuteDisabled;
 		_scoreTrack = mainState->scoreTrack;
 
-		if (_gameID == GID_REAH)
+		if (_gameID == GID_REAH || _gameID == GID_AD2044)
 			changeMusicTrack(mainState->musicTrack);
 		if (_gameID == GID_SCHIZM) {
 			// Only restart music if a new track is playing
@@ -6929,13 +6976,21 @@ void Runtime::restoreSaveGameSnapshot() {
 				_scoreSection = mainState->scoreSection;
 
 			if (!musicMutedBeforeRestore && musicMutedAfterRestore) {
-				_musicPlayer.reset();
+				_musicWavePlayer.reset();
+				{
+					Common::StackLock lock(_midiPlayerMutex);
+					_musicMidiPlayer.reset();
+				}
 				_scoreSectionEndTime = 0;
 			} else if (!musicMutedAfterRestore && (isNewTrack || musicMutedBeforeRestore))
 				startScoreSection();
 		}
 	} else {
-		_musicPlayer.reset();
+		_musicWavePlayer.reset();
+		if (_musicMidiPlayer) {
+			Common::StackLock lock(_midiPlayerMutex);
+			_musicMidiPlayer.reset();
+		}
 		_scoreSectionEndTime = 0;
 	}
 
@@ -7059,6 +7114,8 @@ Common::SharedPtr<SaveGameSnapshot> Runtime::generateNewGameSnapshot() const {
 	// that it needs here.
 	if (_gameID == GID_AD2044) {
 		mainState->animDisplayingFrame = 345;
+		mainState->musicActive = true;
+		mainState->musicTrack = 1;
 
 		SaveGameSnapshot::PagedInventoryItem item;
 		item.page = 0;
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index bad38036954..a681e327d61 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -26,11 +26,13 @@
 
 #include "common/hashmap.h"
 #include "common/keyboard.h"
+#include "common/mutex.h"
 #include "common/rect.h"
 
 #include "vcruise/detection.h"
 
 class OSystem;
+class MidiDriver;
 
 namespace Common {
 
@@ -86,6 +88,7 @@ enum StartConfig {
 
 class AudioPlayer;
 class CircuitPuzzle;
+class MidiPlayer;
 class MenuInterface;
 class MenuPage;
 class RuntimeMenuInterface;
@@ -645,7 +648,7 @@ public:
 		kCharSetChineseSimplified,
 	};
 
-	Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &rootFSNode, VCruiseGameID gameID, Common::Language defaultLanguage);
+	Runtime(OSystem *system, Audio::Mixer *mixer, MidiDriver *midiDrv, const Common::FSNode &rootFSNode, VCruiseGameID gameID, Common::Language defaultLanguage);
 	virtual ~Runtime();
 
 	void initSections(const Common::Rect &gameRect, const Common::Rect &menuRect, const Common::Rect &trayRect, const Common::Rect &subtitleRect, const Common::Rect &fullscreenMenuRect, const Graphics::PixelFormat &pixFmt);
@@ -681,6 +684,8 @@ public:
 	void drawLabel(Graphics::ManagedSurface *surface, const Common::String &labelID, const Common::Rect &contentRect);
 	void getLabelDef(const Common::String &labelID, const Graphics::Font *&outFont, const Common::String *&outTextUTF8, uint32 &outColor, uint32 &outShadowColor, uint32 &outShadowOffset);
 
+	void onMidiTimer();
+
 private:
 	enum IndexParseType {
 		kIndexParseTypeNone,
@@ -1343,7 +1348,9 @@ private:
 
 	Common::SharedPtr<Common::RandomSource> _rng;
 
-	Common::SharedPtr<AudioPlayer> _musicPlayer;
+	Common::SharedPtr<AudioPlayer> _musicWavePlayer;
+	Common::Mutex _midiPlayerMutex;
+	Common::SharedPtr<MidiPlayer> _musicMidiPlayer;
 	int _musicTrack;
 	int32 _musicVolume;
 	bool _musicActive;
@@ -1410,6 +1417,7 @@ private:
 	bool _inGameMenuButtonActive[5];
 
 	Audio::Mixer *_mixer;
+	MidiDriver *_midiDrv;
 
 	Common::SharedPtr<MapLoader> _mapLoader;
 
diff --git a/engines/vcruise/runtime_scriptexec.cpp b/engines/vcruise/runtime_scriptexec.cpp
index 0fb60010c22..f1765337d8b 100644
--- a/engines/vcruise/runtime_scriptexec.cpp
+++ b/engines/vcruise/runtime_scriptexec.cpp
@@ -744,8 +744,8 @@ void Runtime::scriptOpMusicVolRamp(ScriptArg_t arg) {
 
 	if (duration == 0) {
 		_musicVolume = newVolume;
-		if (_musicPlayer)
-			_musicPlayer->setVolume(newVolume);
+		if (_musicWavePlayer)
+			_musicWavePlayer->setVolume(newVolume);
 	} else {
 		if (newVolume != _musicVolume) {
 			uint32 timestamp = g_system->getMillis();
@@ -1184,8 +1184,8 @@ void Runtime::scriptOpExit(ScriptArg_t arg) {
 		terminateScript();
 
 		changeMusicTrack(0);
-		if (_musicPlayer)
-			_musicPlayer->setVolumeAndBalance(applyVolumeScale(getDefaultSoundVolume()), 0);
+		if (_musicWavePlayer)
+			_musicWavePlayer->setVolumeAndBalance(applyVolumeScale(getDefaultSoundVolume()), 0);
 	} else {
 		error("Don't know what screen to go to on exit");
 	}
@@ -1486,7 +1486,8 @@ void Runtime::scriptOpJump(ScriptArg_t arg) {
 }
 
 void Runtime::scriptOpMusicStop(ScriptArg_t arg) {
-	_musicPlayer.reset();
+	_musicWavePlayer.reset();
+	_musicMidiPlayer.reset();
 	_musicActive = false;
 }
 
@@ -1514,7 +1515,8 @@ void Runtime::scriptOpScoreNormal(ScriptArg_t arg) {
 	_musicMuteDisabled = false;
 
 	if (_musicMute) {
-		_musicPlayer.reset();
+		_musicWavePlayer.reset();
+		_musicMidiPlayer.reset();
 		_scoreSectionEndTime = 0;
 	}
 }
diff --git a/engines/vcruise/vcruise.cpp b/engines/vcruise/vcruise.cpp
index 7565d3219f5..e88bce1c440 100644
--- a/engines/vcruise/vcruise.cpp
+++ b/engines/vcruise/vcruise.cpp
@@ -32,6 +32,7 @@
 #include "common/algorithm.h"
 #include "common/translation.h"
 
+#include "audio/mididrv.h"
 #include "audio/mixer.h"
 
 #include "gui/message.h"
@@ -75,6 +76,14 @@ void VCruiseEngine::handleEvents() {
 	}
 }
 
+void VCruiseEngine::staticHandleMidiTimer(void *refCon) {
+	static_cast<VCruiseEngine *>(refCon)->handleMidiTimer();
+}
+
+void VCruiseEngine::handleMidiTimer() {
+	_runtime->onMidiTimer();
+}
+
 Common::Error VCruiseEngine::run() {
 	Common::List<Graphics::PixelFormat> pixelFormats = _system->getSupportedFormats();
 
@@ -105,6 +114,17 @@ Common::Error VCruiseEngine::run() {
 	}
 #endif
 
+	Common::ScopedPtr<MidiDriver> midiDrv;
+	if (_gameDescription->gameID == GID_AD2044) {
+		MidiDriver::DeviceHandle midiDevHdl = MidiDriver::detectDevice(MDT_MIDI | MDT_ADLIB | MDT_PREFER_GM);
+		if (midiDevHdl) {
+			midiDrv.reset(MidiDriver::createMidi(midiDevHdl));
+
+			if (midiDrv->open() != 0)
+				midiDrv.reset();
+		}
+	}
+
 	if (_gameDescription->desc.flags & VCRUISE_GF_GENTEE_PACKAGE) {
 		Common::File *f = new Common::File();
 
@@ -194,9 +214,12 @@ Common::Error VCruiseEngine::run() {
 
 	_system->fillScreen(0);
 
-	_runtime.reset(new Runtime(_system, _mixer, _rootFSNode, _gameDescription->gameID, _gameDescription->defaultLanguage));
+	_runtime.reset(new Runtime(_system, _mixer, midiDrv.get(), _rootFSNode, _gameDescription->gameID, _gameDescription->defaultLanguage));
 	_runtime->initSections(_videoRect, _menuBarRect, _trayRect, _subtitleRect, Common::Rect(640, 480), _system->getScreenFormat());
 
+	if (midiDrv)
+		midiDrv->setTimerCallback(this, VCruiseEngine::staticHandleMidiTimer);
+
 	const char *exeName = _gameDescription->desc.filesDescriptions[0].fileName;
 
 	if (_gameDescription->desc.flags & VCRUISE_GF_GENTEE_PACKAGE)
@@ -238,6 +261,10 @@ Common::Error VCruiseEngine::run() {
 		_system->delayMillis(5);
 	}
 
+
+	if (midiDrv)
+		midiDrv->setTimerCallback(nullptr, nullptr);
+
 	_runtime.reset();
 
 	if (_gameDescription->desc.flags & VCRUISE_GF_GENTEE_PACKAGE)
diff --git a/engines/vcruise/vcruise.h b/engines/vcruise/vcruise.h
index 4dee4b73aba..3d1fff0e6f1 100644
--- a/engines/vcruise/vcruise.h
+++ b/engines/vcruise/vcruise.h
@@ -72,6 +72,8 @@ protected:
 
 private:
 	void handleEvents();
+	static void staticHandleMidiTimer(void *refCon);
+	void handleMidiTimer();
 
 	Common::Rect _videoRect;
 	Common::Rect _menuBarRect;


Commit: 1d7c33e89384c993cf10db92cb654ad9dcc31ca2
    https://github.com/scummvm/scummvm/commit/1d7c33e89384c993cf10db92cb654ad9dcc31ca2
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2024-03-31T14:39:29-04:00

Commit Message:
VCRUISE: Fix some missing mutex locks

Changed paths:
    engines/vcruise/runtime_scriptexec.cpp


diff --git a/engines/vcruise/runtime_scriptexec.cpp b/engines/vcruise/runtime_scriptexec.cpp
index f1765337d8b..8778d61ea26 100644
--- a/engines/vcruise/runtime_scriptexec.cpp
+++ b/engines/vcruise/runtime_scriptexec.cpp
@@ -1487,7 +1487,10 @@ void Runtime::scriptOpJump(ScriptArg_t arg) {
 
 void Runtime::scriptOpMusicStop(ScriptArg_t arg) {
 	_musicWavePlayer.reset();
-	_musicMidiPlayer.reset();
+	if (_musicMidiPlayer) {
+		Common::StackLock lock(_midiPlayerMutex);
+		_musicMidiPlayer.reset();
+	}
 	_musicActive = false;
 }
 
@@ -1516,7 +1519,10 @@ void Runtime::scriptOpScoreNormal(ScriptArg_t arg) {
 
 	if (_musicMute) {
 		_musicWavePlayer.reset();
-		_musicMidiPlayer.reset();
+		if (_musicMidiPlayer) {
+			Common::StackLock lock(_midiPlayerMutex);
+			_musicMidiPlayer.reset();
+		}
 		_scoreSectionEndTime = 0;
 	}
 }


Commit: c8d2d34644ec74908513921b67ff540ccf41ff18
    https://github.com/scummvm/scummvm/commit/c8d2d34644ec74908513921b67ff540ccf41ff18
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2024-03-31T14:39:29-04:00

Commit Message:
VCRUISE: Add item examination

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


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index bd6bd3b5069..90baa6261ba 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -511,6 +511,13 @@ void AD2044MapLoader::load() {
 		}
 	}
 
+	if (_roomNumber == 87) {
+		uint highDigit = (_screenNumber & 0xf0) >> 4;
+		uint lowDigit = _screenNumber & 0x0f;
+
+		scrFileID = 8700 + static_cast<int>(highDigit * 10u + lowDigit);
+	}
+
 	if (scrFileID < 0) {
 		if (_screenNumber < kFirstScreen)
 			return;
@@ -1754,9 +1761,16 @@ bool Runtime::bootGame(bool newGame) {
 
 	debug(1, "Booting V-Cruise game...");
 
-	if (_gameID == GID_AD2044)
+	if (_gameID == GID_AD2044) {
 		loadAD2044ExecutableResources();
-	else
+
+		Common::File tabFile;
+
+		if (tabFile.open(Common::Path("anims/ANIM0087.TAB")))
+			loadTabData(_examineAnimIDToFrameRange, 87, &tabFile);
+		else
+			error("Failed to load inspection animations");
+	} else
 		loadReahSchizmIndex();
 
 	debug(1, "Index loaded OK");
@@ -2067,10 +2081,9 @@ void Runtime::drawLabel(Graphics::ManagedSurface *surface, const Common::String
 }
 
 void Runtime::onMidiTimer() {
-	if (_musicMidiPlayer) {
-		Common::StackLock lock(_midiPlayerMutex);
+	Common::StackLock lock(_midiPlayerMutex);
+	if (_musicMidiPlayer)
 		_musicMidiPlayer->onMidiTimer();
-	}
 }
 
 
@@ -3709,6 +3722,58 @@ void Runtime::changeHero() {
 	restoreSaveGameSnapshot();
 }
 
+void Runtime::changeToExamineItem() {
+	assert(canSave(true));
+
+	InventoryItem itemToExamine = _inventoryActiveItem;
+
+	_inventoryActiveItem = InventoryItem();
+
+	recordSaveGameSnapshot();
+
+	SaveGameSnapshot *snapshot = _mostRecentlyRecordedSaveState.get();
+
+	Common::SharedPtr<SaveGameSwappableState> currentState = snapshot->states[0];
+	Common::SharedPtr<SaveGameSwappableState> alternateState = snapshot->states[1];
+
+	// Move inventory into the new state
+	alternateState->inventory.clear();
+	alternateState->inventory = Common::move(currentState->inventory);
+
+	// For some reason the screen number translation converts decimal to hex
+	uint highDigit = itemToExamine.itemID / 10u;
+	uint lowDigit = itemToExamine.itemID % 10u;
+
+	Common::HashMap<int, AnimFrameRange>::const_iterator frameRangeIt = _examineAnimIDToFrameRange.find(8700 + static_cast<int>(itemToExamine.itemID));
+
+	if (frameRangeIt == _examineAnimIDToFrameRange.end())
+		error("Couldn't resolve animation frame range to examine item %u", static_cast<uint>(itemToExamine.itemID));
+
+	alternateState->loadedAnimation = frameRangeIt->_value.animationNum;
+	alternateState->animDisplayingFrame = frameRangeIt->_value.firstFrame;
+
+	alternateState->havePendingPostSwapScreenReset = true;
+	alternateState->roomNumber = 87;
+	alternateState->screenNumber = (highDigit * 0x10u) + lowDigit;
+	alternateState->direction = 0;
+
+	alternateState->musicActive = currentState->musicActive;
+	alternateState->musicMuteDisabled = currentState->musicMuteDisabled;
+	alternateState->musicTrack = currentState->musicTrack;
+	alternateState->musicVolume = currentState->musicVolume;
+
+	snapshot->states[0] = alternateState;
+	snapshot->states[1] = currentState;
+
+	snapshot->hero ^= 1u;
+
+	changeToCursor(_cursors[kCursorArrow]);
+
+	_mostRecentValidSaveState = _mostRecentlyRecordedSaveState;
+
+	restoreSaveGameSnapshot();
+}
+
 bool Runtime::triggerPreIdleActions() {
 	debug(1, "Triggering pre-idle actions in room %u screen 0%x facing direction %u", _roomNumber, _screenNumber, _direction);
 
@@ -3936,6 +4001,17 @@ bool Runtime::dischargeIdleMouseMove() {
 
 			invSlotRect.translate(static_cast<int16>(AD2044Interface::getInvSlotSpacing()), 0);
 		}
+
+		if (_inventoryActiveItem.itemID != 0) {
+			if (g_ad2044ItemInfos[_inventoryActiveItem.itemID].inspectionScreenID != 0) {
+				Common::Rect examineRect = AD2044Interface::getRectForUI(AD2044InterfaceRectID::ExamineButton);
+
+				if (examineRect.contains(_mousePos)) {
+					isOnInteraction = true;
+					interactionID = kExamineItemInteractionID;
+				}
+			}
+		}
 	}
 
 	if (_idleIsOnInteraction && (!isOnInteraction || interactionID != _idleInteractionID)) {
@@ -4000,6 +4076,9 @@ bool Runtime::dischargeIdleMouseMove() {
 		if (interactionID == kHeroChangeInteractionID) {
 			changeToCursor(_cursors[16]);
 			_idleHaveClickInteraction = true;
+		} else if (interactionID == kExamineItemInteractionID) {
+			changeToCursor(_cursors[5]);
+			_idleHaveClickInteraction = true;
 		} else if (interactionID == kObjectDropInteractionID || (interactionID >= kReturnInventorySlot0InteractionID && interactionID <= kReturnInventorySlot5InteractionID)) {
 			changeToCursor(_cursors[7]);
 			_idleHaveClickInteraction = true;
@@ -4068,6 +4147,9 @@ bool Runtime::dischargeIdleClick() {
 			recordSaveGameSnapshot();
 			_havePendingReturnToIdleState = true;
 			return true;
+		} else if (_gameID == GID_AD2044 && _idleInteractionID == kExamineItemInteractionID) {
+			changeToExamineItem();
+			return true;
 		} else if (_gameID == GID_AD2044 && _idleInteractionID >= kPickupInventorySlot0InteractionID && _idleInteractionID <= kPickupInventorySlot5InteractionID) {
 			pickupInventoryItem(_idleInteractionID - kPickupInventorySlot0InteractionID);
 			recordSaveGameSnapshot();
@@ -4182,7 +4264,9 @@ void Runtime::loadFrameData2(Common::SeekableReadStream *stream) {
 	}
 }
 
-void Runtime::loadTabData(uint animNumber, Common::SeekableReadStream *stream) {
+void Runtime::loadTabData(Common::HashMap<int, AnimFrameRange> &animIDToFrameRangeMap, uint animNumber, Common::SeekableReadStream *stream) {
+	animIDToFrameRangeMap.clear();
+
 	int64 size64 = stream->size() - stream->pos();
 
 	if (size64 > UINT_MAX || size64 < 0)
@@ -4268,8 +4352,8 @@ void Runtime::loadTabData(uint animNumber, Common::SeekableReadStream *stream) {
 		frameRange.lastFrame = static_cast<uint>(parsedNumbers[2]);
 
 		// Animation ID 9099 is duplicated but it doesn't really matter since the duplicate is identical
-		if (_animIDToFrameRange.find(parsedNumbers[0]) == _animIDToFrameRange.end())
-			_animIDToFrameRange[parsedNumbers[0]] = frameRange;
+		if (animIDToFrameRangeMap.find(parsedNumbers[0]) == animIDToFrameRangeMap.end())
+			animIDToFrameRangeMap[parsedNumbers[0]] = frameRange;
 		else
 			warning("Animation ID %i was duplicated", parsedNumbers[0]);
 	}
@@ -4476,14 +4560,11 @@ void Runtime::changeAnimation(const AnimationDef &animDef, uint initialFrame, bo
 		}
 
 		if (isAD2044) {
-			_animIDToFrameRange.clear();
-
 			Common::Path tabFileName(Common::String::format("anims/ANIM%04i.TAB", animFile));
 			Common::File tabFile;
 
 			if (tabFile.open(tabFileName))
-				loadTabData(animFile, &tabFile);
-			tabFile.close();
+				loadTabData(_currentRoomAnimIDToFrameRange, animFile, &tabFile);
 		}
 
 		_loadedAnimationHasSound = (_animDecoder->getAudioTrackCount() > 0);
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index a681e327d61..8ace933bc32 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -918,6 +918,7 @@ private:
 	void changeToScreen(uint roomNumber, uint screenNumber);
 	void clearIdleAnimations();
 	void changeHero();
+	void changeToExamineItem();
 	bool triggerPreIdleActions();
 	void returnToIdleState();
 	void changeToCursor(const Common::SharedPtr<AnimatedCursor> &cursor);
@@ -927,7 +928,7 @@ private:
 	bool dischargeIdleClick();
 	void loadFrameData(Common::SeekableReadStream *stream);
 	void loadFrameData2(Common::SeekableReadStream *stream);
-	void loadTabData(uint animNumber, Common::SeekableReadStream *stream);
+	void loadTabData(Common::HashMap<int, AnimFrameRange> &animIDToFrameRangeMap, uint animNumber, Common::SeekableReadStream *stream);
 
 	void changeMusicTrack(int musicID);
 	void startScoreSection();
@@ -1238,6 +1239,7 @@ private:
 	void scriptOpSay3K(ScriptArg_t arg);
 	void scriptOpRGet(ScriptArg_t arg);
 	void scriptOpRSet(ScriptArg_t arg);
+	void scriptOpEndRSet(ScriptArg_t arg);
 
 	Common::Array<Common::SharedPtr<AnimatedCursor> > _cursors;      // Cursors indexed as CURSOR_CUR_##
 	Common::Array<Common::SharedPtr<AnimatedCursor> > _cursorsShort;      // Cursors indexed as CURSOR_#
@@ -1396,7 +1398,8 @@ private:
 	Common::HashMap<Common::String, uint> _animDefNameToIndex;
 
 	// AD2044 animation map
-	Common::HashMap<int, AnimFrameRange> _animIDToFrameRange;
+	Common::HashMap<int, AnimFrameRange> _currentRoomAnimIDToFrameRange;
+	Common::HashMap<int, AnimFrameRange> _examineAnimIDToFrameRange;
 
 	bool _idleLockInteractions;
 	bool _idleIsOnInteraction;
@@ -1472,9 +1475,7 @@ private:
 
 	static const uint kSoundCacheSize = 16;
 
-	static const uint kHeroChangeInteractionID = 0xffffffffu;
-	static const uint kObjectDropInteractionID = 0xfffffffeu;
-	static const uint kObjectPickupInteractionID = 0xfffffffdu;
+	static const uint kExamineItemInteractionID = 0xfffffff0u;
 
 	static const uint kReturnInventorySlot0InteractionID = 0xfffffff1u;
 	static const uint kReturnInventorySlot1InteractionID = 0xfffffff2u;
@@ -1490,6 +1491,11 @@ private:
 	static const uint kPickupInventorySlot4InteractionID = 0xfffffffbu;
 	static const uint kPickupInventorySlot5InteractionID = 0xfffffffcu;
 
+	static const uint kObjectPickupInteractionID = 0xfffffffdu;
+	static const uint kObjectDropInteractionID = 0xfffffffeu;
+
+	static const uint kHeroChangeInteractionID = 0xffffffffu;
+
 	Common::Pair<Common::String, Common::SharedPtr<SoundCache> > _soundCache[kSoundCacheSize];
 	uint _soundCacheIndex;
 
diff --git a/engines/vcruise/runtime_scriptexec.cpp b/engines/vcruise/runtime_scriptexec.cpp
index 8778d61ea26..9a1e3535557 100644
--- a/engines/vcruise/runtime_scriptexec.cpp
+++ b/engines/vcruise/runtime_scriptexec.cpp
@@ -2056,8 +2056,8 @@ void Runtime::scriptOpAnimT(ScriptArg_t arg) {
 
 	StackInt_t animationID = stackArgs[0];
 
-	Common::HashMap<int, AnimFrameRange>::const_iterator animRangeIt = _animIDToFrameRange.find(animationID);
-	if (animRangeIt == _animIDToFrameRange.end())
+	Common::HashMap<int, AnimFrameRange>::const_iterator animRangeIt = _currentRoomAnimIDToFrameRange.find(animationID);
+	if (animRangeIt == _currentRoomAnimIDToFrameRange.end())
 		error("Couldn't resolve animation ID %i", static_cast<int>(animationID));
 
 	AnimationDef animDef;
@@ -2094,8 +2094,8 @@ void Runtime::scriptOpAnimAD2044(bool isForward) {
 	if (!found)
 		error("Couldn't resolve animation lookup ID %i", static_cast<int>(stackArgs[0]));
 
-	Common::HashMap<int, AnimFrameRange>::const_iterator animRangeIt = _animIDToFrameRange.find(animationID);
-	if (animRangeIt == _animIDToFrameRange.end())
+	Common::HashMap<int, AnimFrameRange>::const_iterator animRangeIt = _currentRoomAnimIDToFrameRange.find(animationID);
+	if (animRangeIt == _currentRoomAnimIDToFrameRange.end())
 		error("Couldn't resolve animation ID %i", static_cast<int>(animationID));
 
 	AnimationDef animDef;
@@ -2252,10 +2252,11 @@ void Runtime::scriptOpRSet(ScriptArg_t arg) {
 		}
 	}
 
-	// NYI
 	error("Couldn't resolve item ID for script item %i", static_cast<int>(stackArgs[0]));
 }
 
+OPCODE_STUB(EndRSet)
+
 
 // Unused Schizm ops
 // Only used in fnRandomBirds and fnRandomMachines in Room 60, both of which are unused
@@ -2514,6 +2515,7 @@ bool Runtime::runScript() {
 
 			DISPATCH_OP(RGet);
 			DISPATCH_OP(RSet);
+			DISPATCH_OP(EndRSet);
 
 			DISPATCH_OP(Say3K);
 
diff --git a/engines/vcruise/script.cpp b/engines/vcruise/script.cpp
index f7ceec3c806..af21395dbe3 100644
--- a/engines/vcruise/script.cpp
+++ b/engines/vcruise/script.cpp
@@ -597,7 +597,9 @@ static ScriptNamedInstruction g_ad2044NamedInstructions[] = {
 	{"animT", ProtoOp::kProtoOpScript, ScriptOps::kAnimT},
 	{"ani+", ProtoOp::kProtoOpScript, ScriptOps::kAnimForward},
 	{"ani-", ProtoOp::kProtoOpScript, ScriptOps::kAnimReverse},
-	{"kani+", ProtoOp::kProtoOpScript, ScriptOps::kAnimForward},
+	{"kani+", ProtoOp::kProtoOpScript, ScriptOps::kAnimForward},	// FIXME: Do we really want to use this op?
+	{"anis+", ProtoOp::kProtoOpScript, ScriptOps::kAnimForward}, // FIXME: Do we really want to use this op?
+	{"anis-", ProtoOp::kProtoOpScript, ScriptOps::kAnimReverse}, // FIXME: Do we really want to use this op?
 	//{"static", ProtoOp::kProtoOpScript, ScriptOps::kStatic},
 	{"yes@", ProtoOp::kProtoOpScript, ScriptOps::kVarLoad},
 	{"yes!", ProtoOp::kProtoOpScript, ScriptOps::kVarStore},
@@ -611,6 +613,7 @@ static ScriptNamedInstruction g_ad2044NamedInstructions[] = {
 	//{"r!", ProtoOp::kProtoOpScript, ScriptOps::kItemAdd},
 	{"r@", ProtoOp::kProtoOpScript, ScriptOps::kRGet},
 	{"r!", ProtoOp::kProtoOpScript, ScriptOps::kRSet},
+	{"endr!", ProtoOp::kProtoOpScript, ScriptOps::kEndRSet},
 	//{"clearPocket", ProtoOp::kProtoOpScript, ScriptOps::kItemClear},
 	{"cursor!", ProtoOp::kProtoOpScript, ScriptOps::kSetCursor},
 	{"room!", ProtoOp::kProtoOpScript, ScriptOps::kSetRoom},
@@ -685,6 +688,8 @@ static ScriptNamedInstruction g_ad2044NamedInstructions[] = {
 
 	//{"goto", ProtoOp::kProtoOpScript, ScriptOps::kGoto},
 
+	{"stop", ProtoOp::kProtoOpScript, ScriptOps::kStop},
+
 	{"#if", ProtoOp::kProtoOpIf, ScriptOps::kInvalid},
 	{"#eif", ProtoOp::kProtoOpEndIf, ScriptOps::kInvalid},
 	{"#else", ProtoOp::kProtoOpElse, ScriptOps::kInvalid},
diff --git a/engines/vcruise/script.h b/engines/vcruise/script.h
index 0900991c8fa..0d2070e6aa0 100644
--- a/engines/vcruise/script.h
+++ b/engines/vcruise/script.h
@@ -242,6 +242,8 @@ enum ScriptOp {
 	kSay3K,
 	kRGet,
 	kRSet,
+	kEndRSet,
+	kStop,
 
 	kNumOps,
 };


Commit: ffa36e18982df8930b01d63685a166d684e10f8d
    https://github.com/scummvm/scummvm/commit/ffa36e18982df8930b01d63685a166d684e10f8d
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2024-03-31T14:39:29-04:00

Commit Message:
VCRUISE: Fix MIDI crash when restarting the game

Changed paths:
    engines/vcruise/vcruise.cpp


diff --git a/engines/vcruise/vcruise.cpp b/engines/vcruise/vcruise.cpp
index e88bce1c440..5267899759f 100644
--- a/engines/vcruise/vcruise.cpp
+++ b/engines/vcruise/vcruise.cpp
@@ -267,6 +267,9 @@ Common::Error VCruiseEngine::run() {
 
 	_runtime.reset();
 
+	if (midiDrv)
+		midiDrv->close();
+
 	if (_gameDescription->desc.flags & VCRUISE_GF_GENTEE_PACKAGE)
 		SearchMan.remove("VCruiseInstallerPackage");
 


Commit: 774a7dfac325aa8a373aaad78b7abbf65fcbedf6
    https://github.com/scummvm/scummvm/commit/774a7dfac325aa8a373aaad78b7abbf65fcbedf6
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2024-03-31T14:39:30-04:00

Commit Message:
VCRUISE: Avoid restarting music if the track didn't change

Changed paths:
    engines/vcruise/runtime.cpp


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 90baa6261ba..466669657ad 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -4360,7 +4360,7 @@ void Runtime::loadTabData(Common::HashMap<int, AnimFrameRange> &animIDToFrameRan
 }
 
 void Runtime::changeMusicTrack(int track) {
-	if (track == _musicTrack && _musicWavePlayer.get() != nullptr && _musicMidiPlayer.get() != nullptr)
+	if (track == _musicTrack && (_musicWavePlayer.get() != nullptr || _musicMidiPlayer.get() != nullptr))
 		return;
 
 	_musicWavePlayer.reset();
@@ -4379,9 +4379,12 @@ void Runtime::changeMusicTrack(int track) {
 
 	Common::String musicPathStr;
 
-	if (_gameID == GID_AD2044)
+	if (_gameID == GID_AD2044) {
+		if (!_midiDrv)
+			return;
+
 		musicPathStr = Common::String::format("sfx/music%02i.mid", static_cast<int>(track));
-	else
+	} else
 		musicPathStr = Common::String::format("Sfx/Music-%02i.wav", static_cast<int>(track));
 
 	Common::Path musicFileName(musicPathStr);


Commit: e9000d2a8ea06121fd0b1f90c855b4a58fb3151f
    https://github.com/scummvm/scummvm/commit/e9000d2a8ea06121fd0b1f90c855b4a58fb3151f
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2024-03-31T14:39:30-04:00

Commit Message:
VCRUISE: Add return from item examination

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


diff --git a/engines/vcruise/ad2044_items.cpp b/engines/vcruise/ad2044_items.cpp
index 216ef7f86e9..d69fc298610 100644
--- a/engines/vcruise/ad2044_items.cpp
+++ b/engines/vcruise/ad2044_items.cpp
@@ -24,80 +24,80 @@
 namespace VCruise {
 
 const AD2044ItemInfo g_ad2044ItemInfos[kNumAD2044Items] = {
-	{0, 0, 0, 0},                // 0
-	{0, 0, 0, 0},                // 1
-	{0, 0, 0, 0},                // 2
-	{0, 0, 0, 0},                // 3
-	{0, 0, 0, 0},                // 4
-	{0, 0, 0, 0},                // 5
-	{0, 0, 0, 0},                // 6
-	{0, 0, 0, 0},                // 7
-	{0, 0, 0, 0},                // 8
-	{0, 0, 0, 0},                // 9
-	{0, 0, 0, 0},                // 10
-	{0, 0, 0, 0},                // 11
-	{0, 0, 0, 0},                // 12
-	{0, 0, 0, 0},                // 13
-	{0, 0, 0, 0},                // 14
-	{0, 0, 0, 0},                // 15
-	{0, 0, 0, 0},                // 16
-	{0, 0, 0, 0},                // 17
-	{0, 0, 0x18, 0x128},         // 18
-	{0, 0, 0, 0},                // 19
-	{0, 0, 0, 0},                // 20
-	{0, 0, 0, 0},                // 21
-	{0, 0, 0, 0},                // 22
-	{0, 0, 0, 0},                // 23
-	{0, 0, 0, 0},                // 24
-	{0, 0, 0, 0},                // 25
-	{0, 0, 0, 0},                // 26
-	{0, 0, 0, 0},                // 27
-	{0, 0, 0, 0},                // 28
-	{0, 0, 0, 0},                // 29
-	{0, 0, 0, 0},                // 30
-	{0, 0, 0, 0},                // 31
-	{0, 0, 0, 0},                // 32
-	{0, 0, 0, 0},                // 33
-	{0, 0, 0, 0},                // 34
-	{0, 0, 0, 0},                // 35
-	{0, 0, 0, 0},                // 36
-	{0, 0, 0, 0},                // 37
-	{0, 0, 0, 0},                // 38
-	{0, 0, 0, 0},                // 39
-	{0, 0, 0, 0},                // 40
-	{0, 0, 0, 0},                // 41
-	{0, 0, 0, 0},                // 42
-	{0, 0, 0, 0},                // 43
-	{0, 0, 0, 0},                // 44
-	{0, 0, 0, 0},                // 45
-	{0, 0, 0, 0},                // 46
-	{0, 0, 0, 0},                // 47
-	{0, 0, 0, 0},                // 48
-	{0, 0, 0, 0},                // 49
-	{0, 0, 0, 0},                // 50
-	{0, 0, 0, 0},                // 51
-	{0, 0, 0, 0},                // 52
-	{0, 0, 0, 0},                // 53
+	{0, 0, false, 0},                // 0
+	{0, 0, false, 0},               // 1
+	{0, 0, false, 0},               // 2
+	{0, 0, false, 0},               // 3
+	{0, 0, false, 0},               // 4
+	{0, 0, false, 0},               // 5
+	{0, 0, false, 0},               // 6
+	{0, 0, false, 0},               // 7
+	{0, 0, false, 0},               // 8
+	{0, 0, false, 0},               // 9
+	{0, 0, false, 0},               // 10
+	{0, 0, false, 0},               // 11
+	{0, 0, false, 0},               // 12
+	{0, 0, false, 0},               // 13
+	{0, 0, false, 0},               // 14
+	{0, 0, false, 0},               // 15
+	{0, 0, false, 0},               // 16
+	{0, 0, false, 0},               // 17
+	{0, 0, true, 0x128},            // 18
+	{0, 0, false, 0},               // 19
+	{0, 0, false, 0},               // 20
+	{0, 0, false, 0},               // 21
+	{0, 0, false, 0},               // 22
+	{0, 0, false, 0},               // 23
+	{0, 0, false, 0},               // 24
+	{0, 0, false, 0},               // 25
+	{0, 0, false, 0},               // 26
+	{0, 0, false, 0},               // 27
+	{0, 0, false, 0},               // 28
+	{0, 0, false, 0},               // 29
+	{0, 0, false, 0},               // 30
+	{0, 0, false, 0},               // 31
+	{0, 0, false, 0},               // 32
+	{0, 0, false, 0},               // 33
+	{0, 0, false, 0},               // 34
+	{0, 0, false, 0},               // 35
+	{0, 0, false, 0},               // 36
+	{0, 0, false, 0},               // 37
+	{0, 0, false, 0},               // 38
+	{0, 0, false, 0},               // 39
+	{0, 0, false, 0},               // 40
+	{0, 0, false, 0},               // 41
+	{0, 0, false, 0},               // 42
+	{0, 0, false, 0},               // 43
+	{0, 0, false, 0},               // 44
+	{0, 0, false, 0},               // 45
+	{0, 0, false, 0},               // 46
+	{0, 0, false, 0},               // 47
+	{0, 0, false, 0},               // 48
+	{0, 0, false, 0},               // 49
+	{0, 0, false, 0},               // 50
+	{0, 0, false, 0},               // 51
+	{0, 0, false, 0},               // 52
+	{0, 0, false, 0},               // 53
 	{0x83d54448, 0x839911EF, 0, 0}, // 54
-	{0, 0, 0, 0},                // 55
-	{0, 0, 0, 0},                // 56
-	{0, 0, 0, 0},                // 57
-	{0, 0, 0, 0},                // 58
-	{0, 0, 0, 0},                // 59
-	{0, 0, 0, 0x170},            // 60
-	{0, 0, 0, 0},                // 61
-	{0, 0, 0, 0},                // 62
-	{0, 0, 0, 0},                // 63
-	{0, 0, 0, 0},                // 64
-	{0, 0, 0, 0},                // 65
-	{0, 0, 0, 0},                // 66
-	{0, 0, 0, 0},                // 67
-	{0, 0, 0, 0},                // 68
-	{0, 0, 0, 0},                // 69
-	{0, 0, 0, 0},                // 70
-	{0, 0, 0, 0},                // 71
-	{0, 0, 0, 0},                // 72
-	{0, 0, 0, 0},                // 73
+	{0, 0, false, 0},               // 55
+	{0, 0, false, 0},               // 56
+	{0, 0, false, 0},               // 57
+	{0, 0, false, 0},               // 58
+	{0, 0, false, 0},               // 59
+	{0, 0, false, 0x170},           // 60
+	{0, 0, false, 0},               // 61
+	{0, 0, false, 0},               // 62
+	{0, 0, false, 0},               // 63
+	{0, 0, false, 0},               // 64
+	{0, 0, false, 0},               // 65
+	{0, 0, false, 0},               // 66
+	{0, 0, false, 0},               // 67
+	{0, 0, false, 0},               // 68
+	{0, 0, false, 0},               // 69
+	{0, 0, false, 0},               // 70
+	{0, 0, false, 0},               // 71
+	{0, 0, false, 0},               // 72
+	{0, 0, false, 0},               // 73
 };
 
 } // End of namespace VCruise
diff --git a/engines/vcruise/ad2044_items.h b/engines/vcruise/ad2044_items.h
index fbb6dcb86eb..a7a506f959e 100644
--- a/engines/vcruise/ad2044_items.h
+++ b/engines/vcruise/ad2044_items.h
@@ -29,7 +29,7 @@ namespace VCruise {
 struct AD2044ItemInfo {
 	uint32 enNameCRC;
 	uint32 plNameCRC;
-	uint16 inspectionScreenID;
+	bool canBeExamined;
 	uint16 scriptItemID;
 };
 
diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 466669657ad..fa56a98c317 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -3724,6 +3724,7 @@ void Runtime::changeHero() {
 
 void Runtime::changeToExamineItem() {
 	assert(canSave(true));
+	assert(_hero == 0);
 
 	InventoryItem itemToExamine = _inventoryActiveItem;
 
@@ -3774,6 +3775,39 @@ void Runtime::changeToExamineItem() {
 	restoreSaveGameSnapshot();
 }
 
+void Runtime::returnFromExaminingItem() {
+	assert(canSave(true));
+	assert(_hero == 1);
+
+	InventoryItem itemToReturnWith = _inventoryActiveItem;
+
+	_inventoryActiveItem = InventoryItem();
+
+	recordSaveGameSnapshot();
+
+	SaveGameSnapshot *snapshot = _mostRecentlyRecordedSaveState.get();
+
+	Common::SharedPtr<SaveGameSwappableState> currentState = snapshot->states[0];
+	Common::SharedPtr<SaveGameSwappableState> alternateState = snapshot->states[1];
+
+	// Move inventory into the new state
+	alternateState->inventory.clear();
+	alternateState->inventory = Common::move(currentState->inventory);
+
+	snapshot->inventoryActiveItem = itemToReturnWith.itemID;
+
+	snapshot->states[0] = alternateState;
+	snapshot->states[1] = currentState;
+
+	snapshot->hero ^= 1u;
+
+	changeToCursor(_cursors[kCursorArrow]);
+
+	_mostRecentValidSaveState = _mostRecentlyRecordedSaveState;
+
+	restoreSaveGameSnapshot();
+}
+
 bool Runtime::triggerPreIdleActions() {
 	debug(1, "Triggering pre-idle actions in room %u screen 0%x facing direction %u", _roomNumber, _screenNumber, _direction);
 
@@ -4003,7 +4037,7 @@ bool Runtime::dischargeIdleMouseMove() {
 		}
 
 		if (_inventoryActiveItem.itemID != 0) {
-			if (g_ad2044ItemInfos[_inventoryActiveItem.itemID].inspectionScreenID != 0) {
+			if (g_ad2044ItemInfos[_inventoryActiveItem.itemID].canBeExamined) {
 				Common::Rect examineRect = AD2044Interface::getRectForUI(AD2044InterfaceRectID::ExamineButton);
 
 				if (examineRect.contains(_mousePos)) {
@@ -5912,7 +5946,7 @@ void Runtime::drawActiveItemGraphic() {
 		drawSectionToScreen(_fullscreenMenuSection, itemRect);
 	}
 
-	if (g_ad2044ItemInfos[_inventoryActiveItem.itemID].inspectionScreenID != 0) {
+	if (g_ad2044ItemInfos[_inventoryActiveItem.itemID].canBeExamined) {
 		Common::Rect examineRect = AD2044Interface::getRectForUI(AD2044InterfaceRectID::ExamineButton);
 
 		_fullscreenMenuSection.surf->blitFrom(*_ad2044Graphics->examine, Common::Point(examineRect.left, examineRect.top));
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index 8ace933bc32..c7befb00feb 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -919,6 +919,7 @@ private:
 	void clearIdleAnimations();
 	void changeHero();
 	void changeToExamineItem();
+	void returnFromExaminingItem();
 	bool triggerPreIdleActions();
 	void returnToIdleState();
 	void changeToCursor(const Common::SharedPtr<AnimatedCursor> &cursor);
diff --git a/engines/vcruise/runtime_scriptexec.cpp b/engines/vcruise/runtime_scriptexec.cpp
index 9a1e3535557..a3836df6667 100644
--- a/engines/vcruise/runtime_scriptexec.cpp
+++ b/engines/vcruise/runtime_scriptexec.cpp
@@ -2255,7 +2255,11 @@ void Runtime::scriptOpRSet(ScriptArg_t arg) {
 	error("Couldn't resolve item ID for script item %i", static_cast<int>(stackArgs[0]));
 }
 
-OPCODE_STUB(EndRSet)
+void Runtime::scriptOpEndRSet(ScriptArg_t arg) {
+	scriptOpRSet(arg);
+
+	returnFromExaminingItem();
+}
 
 
 // Unused Schizm ops


Commit: 0867fd912639e56ee9761e630d7ee47ef2e53940
    https://github.com/scummvm/scummvm/commit/0867fd912639e56ee9761e630d7ee47ef2e53940
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2024-03-31T14:39:30-04:00

Commit Message:
VCRUISE: Add say cycle ops and some item infos

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


diff --git a/engines/vcruise/ad2044_items.cpp b/engines/vcruise/ad2044_items.cpp
index d69fc298610..dc043cb18a6 100644
--- a/engines/vcruise/ad2044_items.cpp
+++ b/engines/vcruise/ad2044_items.cpp
@@ -41,17 +41,17 @@ const AD2044ItemInfo g_ad2044ItemInfos[kNumAD2044Items] = {
 	{0, 0, false, 0},               // 14
 	{0, 0, false, 0},               // 15
 	{0, 0, false, 0},               // 16
-	{0, 0, false, 0},               // 17
-	{0, 0, true, 0x128},            // 18
+	{0, 0, false, 0},                      // 17
+	{0x3D70D2EC, 0x98382A38, true, 0x128}, // 18 spoon
 	{0, 0, false, 0},               // 19
 	{0, 0, false, 0},               // 20
 	{0, 0, false, 0},               // 21
 	{0, 0, false, 0},               // 22
 	{0, 0, false, 0},               // 23
-	{0, 0, false, 0},               // 24
+	{0, 0, true, 0x134},            // 24 cigarettes (filled)
 	{0, 0, false, 0},               // 25
 	{0, 0, false, 0},               // 26
-	{0, 0, false, 0},               // 27
+	{0, 0, true, 0x137},            // 27 matches
 	{0, 0, false, 0},               // 28
 	{0, 0, false, 0},               // 29
 	{0, 0, false, 0},               // 30
@@ -78,13 +78,13 @@ const AD2044ItemInfo g_ad2044ItemInfos[kNumAD2044Items] = {
 	{0, 0, false, 0},               // 51
 	{0, 0, false, 0},               // 52
 	{0, 0, false, 0},               // 53
-	{0x83d54448, 0x839911EF, 0, 0}, // 54
+	{0x83d54448, 0x839911EF, 0, 0}, // 54 goaler (sic)
 	{0, 0, false, 0},               // 55
 	{0, 0, false, 0},               // 56
 	{0, 0, false, 0},               // 57
 	{0, 0, false, 0},               // 58
 	{0, 0, false, 0},               // 59
-	{0, 0, false, 0x170},           // 60
+	{0, 0, false, 0x170},           // 60 mirror
 	{0, 0, false, 0},               // 61
 	{0, 0, false, 0},               // 62
 	{0, 0, false, 0},               // 63
@@ -92,12 +92,12 @@ const AD2044ItemInfo g_ad2044ItemInfos[kNumAD2044Items] = {
 	{0, 0, false, 0},               // 65
 	{0, 0, false, 0},               // 66
 	{0, 0, false, 0},               // 67
-	{0, 0, false, 0},               // 68
-	{0, 0, false, 0},               // 69
+	{0, 0, true, 0x178},            // 68 cigarette
+	{0, 0, true, 0x179},            // 69 cigarette (lit)
 	{0, 0, false, 0},               // 70
 	{0, 0, false, 0},               // 71
 	{0, 0, false, 0},               // 72
-	{0, 0, false, 0},               // 73
+	{0, 0, true, 0x183},            // 73 cigarettes (1 missing)
 };
 
 } // End of namespace VCruise
diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index fa56a98c317..b1bdfcbfd2f 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -3179,14 +3179,16 @@ void Runtime::loadAD2044ExecutableResources() {
 
 	_ad2044Graphics->finishLoading();
 
-	Common::HashMap<uint32, uint32> stringHashToFilePos;
+	Common::HashMap<uint32, Common::Pair<uint32, uint> > stringHashToFilePosAndLength;
 
 	for (const AD2044ItemInfo &itemInfo : g_ad2044ItemInfos) {
-		stringHashToFilePos[itemInfo.enNameCRC] = 0;
-		stringHashToFilePos[itemInfo.plNameCRC] = 0;
+		if (_language == Common::PL_POL)
+			stringHashToFilePosAndLength[itemInfo.plNameCRC] = Common::Pair<uint32, uint>(0, 0);
+		else
+			stringHashToFilePosAndLength[itemInfo.enNameCRC] = Common::Pair<uint32, uint>(0, 0);
 	}
 
-	stringHashToFilePos.erase(0);
+	stringHashToFilePosAndLength.erase(0);
 
 	// Scan for strings
 	Common::CRC32 crc;
@@ -3200,9 +3202,9 @@ void Runtime::loadAD2044ExecutableResources() {
 			uint32 strLength = i - strStartPos;
 			rollingCRC = crc.finalize(rollingCRC);
 			if (strLength != 0) {
-				Common::HashMap<uint32, uint32>::iterator it = stringHashToFilePos.find(rollingCRC);
-				if (it != stringHashToFilePos.end())
-					it->_value = strStartPos;
+				Common::HashMap<uint32, Common::Pair<uint32, uint> >::iterator it = stringHashToFilePosAndLength.find(rollingCRC);
+				if (it != stringHashToFilePosAndLength.end())
+					it->_value = Common::Pair<uint32, uint> (strStartPos, strLength);
 			}
 
 #if 1
@@ -3216,6 +3218,34 @@ void Runtime::loadAD2044ExecutableResources() {
 		} else
 			rollingCRC = crc.processByte(b, rollingCRC);
 	}
+
+	_ad2044ItemNames.clear();
+	_ad2044ItemNames.reserve(ARRAYSIZE(g_ad2044ItemInfos));
+
+	for (const AD2044ItemInfo &itemInfo : g_ad2044ItemInfos) {
+		Common::String itemInfoUTF8;
+		uint32 hash = itemInfo.enNameCRC;
+
+		if (_language == Common::PL_POL)
+			hash = itemInfo.plNameCRC;
+
+		if (hash != 0) {
+			Common::HashMap<uint32, Common::Pair<uint32, uint> >::const_iterator strIt = stringHashToFilePosAndLength.find(hash);
+
+			if (strIt != stringHashToFilePosAndLength.end()) {
+				uint32 filePos = strIt->_value.first;
+				uint length = strIt->_value.second;
+
+				if (length > 0) {
+					Common::String str(reinterpret_cast<const char *>(&exeContents[filePos]), length);
+
+					itemInfoUTF8 = str.decode(Common::CodePage::kWindows1250).encode(Common::kUtf8);
+				}
+			}
+		}
+
+		_ad2044ItemNames.push_back(itemInfoUTF8);
+	}
 }
 
 void Runtime::findWaves() {
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index c7befb00feb..1dad02e42d8 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -1236,11 +1236,13 @@ private:
 	void scriptOpSound(ScriptArg_t arg);
 	void scriptOpISound(ScriptArg_t arg);
 	void scriptOpUSound(ScriptArg_t arg);
+	void scriptOpSayCycle_AD2044(const StackInt_t *values, uint numValues);
 	void scriptOpSay2K(ScriptArg_t arg);
 	void scriptOpSay3K(ScriptArg_t arg);
 	void scriptOpRGet(ScriptArg_t arg);
 	void scriptOpRSet(ScriptArg_t arg);
 	void scriptOpEndRSet(ScriptArg_t arg);
+	void scriptOpStop(ScriptArg_t arg);
 
 	Common::Array<Common::SharedPtr<AnimatedCursor> > _cursors;      // Cursors indexed as CURSOR_CUR_##
 	Common::Array<Common::SharedPtr<AnimatedCursor> > _cursorsShort;      // Cursors indexed as CURSOR_#
@@ -1544,6 +1546,7 @@ private:
 	Common::String _subtitleText;
 
 	Common::SharedPtr<AD2044Graphics> _ad2044Graphics;
+	Common::Array<Common::String> _ad2044ItemNames;
 };
 
 } // End of namespace VCruise
diff --git a/engines/vcruise/runtime_scriptexec.cpp b/engines/vcruise/runtime_scriptexec.cpp
index a3836df6667..5eb06e961d8 100644
--- a/engines/vcruise/runtime_scriptexec.cpp
+++ b/engines/vcruise/runtime_scriptexec.cpp
@@ -30,6 +30,23 @@
 
 namespace VCruise {
 
+struct AD2044UnusualAnimationRules {
+	enum Type {
+		kTypeLoop,	// Loop the animation
+		kTypePlayFirstFrameOnly,
+	};
+
+	uint roomNumber;
+	uint screenNumber;
+	uint interactionID;
+	uint8 animLookupID;
+	Type ruleType;
+};
+
+AD2044UnusualAnimationRules g_unusualAnimationRules[] = {
+	{87, 0x69, 0xa3, 0xa5, AD2044UnusualAnimationRules::kTypePlayFirstFrameOnly},
+};
+
 #ifdef PEEK_STACK
 #error "PEEK_STACK is already defined"
 #endif
@@ -2084,7 +2101,7 @@ void Runtime::scriptOpAnimAD2044(bool isForward) {
 	bool found = false;
 
 	for (const AD2044AnimationDef &def : _ad2044AnimationDefs) {
-		if (static_cast<StackInt_t>(def.lookupID) == stackArgs[0]) {
+		if (def.roomID == _roomNumber && static_cast<StackInt_t>(def.lookupID) == stackArgs[0]) {
 			animationID = isForward ? def.fwdAnimationID : def.revAnimationID;
 			found = true;
 			break;
@@ -2127,13 +2144,46 @@ void Runtime::scriptOpAnimReverse(ScriptArg_t arg) {
 }
 
 OPCODE_STUB(AnimKForward)
-OPCODE_STUB(Say2K)
-OPCODE_STUB(Say3K)
 
 void Runtime::scriptOpNoUpdate(ScriptArg_t arg) {
 }
 
-OPCODE_STUB(NoClear)
+void Runtime::scriptOpNoClear(ScriptArg_t arg) {
+}
+
+void Runtime::scriptOpSayCycle_AD2044(const StackInt_t *values, uint numValues) {
+	// Checking the scripts, there don't appear to be any cycles that can't be tracked from
+	// the first value, so just use that.
+	uint &cyclePosRef = _sayCycles[static_cast<uint32>(values[0])];
+
+	Common::String soundName = Common::String::format("%02i-%08i", static_cast<int>(_disc * 10u + 1u), static_cast<int>(values[cyclePosRef]));
+
+	cyclePosRef = (cyclePosRef + 1u) % numValues;
+
+	StackInt_t soundID = 0;
+	SoundInstance *cachedSound = nullptr;
+	resolveSoundByName(soundName, true, soundID, cachedSound);
+
+	if (cachedSound) {
+		TriggeredOneShot oneShot;
+		oneShot.soundID = soundID;
+		oneShot.uniqueSlot = _disc;
+
+		triggerSound(kSoundLoopBehaviorNo, *cachedSound, 100, 0, false, true);
+	}
+}
+
+void Runtime::scriptOpSay2K(ScriptArg_t arg) {
+	TAKE_STACK_INT(2);
+
+	scriptOpSayCycle_AD2044(stackArgs, 2);
+}
+
+void Runtime::scriptOpSay3K(ScriptArg_t arg) {
+	TAKE_STACK_INT(3);
+
+	scriptOpSayCycle_AD2044(stackArgs, 3);
+}
 
 void Runtime::scriptOpSay1_AD2044(ScriptArg_t arg) {
 	TAKE_STACK_INT(1);
@@ -2252,7 +2302,7 @@ void Runtime::scriptOpRSet(ScriptArg_t arg) {
 		}
 	}
 
-	error("Couldn't resolve item ID for script item %i", static_cast<int>(stackArgs[0]));
+	error("Couldn't resolve item ID for script item 0x%x", static_cast<int>(stackArgs[0]));
 }
 
 void Runtime::scriptOpEndRSet(ScriptArg_t arg) {
@@ -2261,6 +2311,9 @@ void Runtime::scriptOpEndRSet(ScriptArg_t arg) {
 	returnFromExaminingItem();
 }
 
+void Runtime::scriptOpStop(ScriptArg_t arg) {
+	terminateScript();
+}
 
 // Unused Schizm ops
 // Only used in fnRandomBirds and fnRandomMachines in Room 60, both of which are unused
@@ -2521,7 +2574,9 @@ bool Runtime::runScript() {
 			DISPATCH_OP(RSet);
 			DISPATCH_OP(EndRSet);
 
+			DISPATCH_OP(Say2K);
 			DISPATCH_OP(Say3K);
+			DISPATCH_OP(Stop);
 
 		default:
 			error("Unimplemented opcode %i", static_cast<int>(instr.op));


Commit: fa2dfbcd7be897498759ea800a4628cd5a4fd123
    https://github.com/scummvm/scummvm/commit/fa2dfbcd7be897498759ea800a4628cd5a4fd123
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2024-03-31T14:39:30-04:00

Commit Message:
VCRUISE: Disallow examining while already examining

Changed paths:
    engines/vcruise/runtime.cpp


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index b1bdfcbfd2f..7e49602c0b0 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -4066,7 +4066,7 @@ bool Runtime::dischargeIdleMouseMove() {
 			invSlotRect.translate(static_cast<int16>(AD2044Interface::getInvSlotSpacing()), 0);
 		}
 
-		if (_inventoryActiveItem.itemID != 0) {
+		if (_inventoryActiveItem.itemID != 0 && _hero == 0) {
 			if (g_ad2044ItemInfos[_inventoryActiveItem.itemID].canBeExamined) {
 				Common::Rect examineRect = AD2044Interface::getRectForUI(AD2044InterfaceRectID::ExamineButton);
 
@@ -5976,7 +5976,7 @@ void Runtime::drawActiveItemGraphic() {
 		drawSectionToScreen(_fullscreenMenuSection, itemRect);
 	}
 
-	if (g_ad2044ItemInfos[_inventoryActiveItem.itemID].canBeExamined) {
+	if (g_ad2044ItemInfos[_inventoryActiveItem.itemID].canBeExamined && _hero == 0) {
 		Common::Rect examineRect = AD2044Interface::getRectForUI(AD2044InterfaceRectID::ExamineButton);
 
 		_fullscreenMenuSection.surf->blitFrom(*_ad2044Graphics->examine, Common::Point(examineRect.left, examineRect.top));


Commit: b66044f2e7bea2f18c2305eb53f2013f97edbe55
    https://github.com/scummvm/scummvm/commit/b66044f2e7bea2f18c2305eb53f2013f97edbe55
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2024-03-31T14:39:30-04:00

Commit Message:
VCRUISE: Add some more handling of unusual animations

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


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 7e49602c0b0..c25e9f94169 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -549,7 +549,7 @@ void AD2044MapLoader::unload() {
 
 
 ScriptEnvironmentVars::ScriptEnvironmentVars() : lmb(false), lmbDrag(false), esc(false), exitToMenu(false), animChangeSet(false), isEntryScript(false), puzzleWasSet(false),
-	panInteractionID(0), fpsOverride(0), lastHighlightedItem(0), animChangeFrameOffset(0), animChangeNumFrames(0) {
+	panInteractionID(0), clickInteractionID(0), fpsOverride(0), lastHighlightedItem(0), animChangeFrameOffset(0), animChangeNumFrames(0) {
 }
 
 OSEvent::OSEvent() : type(kOSEventTypeInvalid), keyCode(static_cast<Common::KeyCode>(0)), keymappedEvent(kKeymappedEventNone), timestamp(0) {
@@ -4154,7 +4154,9 @@ bool Runtime::dischargeIdleMouseMove() {
 			Common::SharedPtr<Script> script = findScriptForInteraction(interactionID);
 
 			if (script) {
-				activateScript(script, false, ScriptEnvironmentVars());
+				ScriptEnvironmentVars envVars;
+				envVars.clickInteractionID = interactionID;
+				activateScript(script, false, envVars);
 				return true;
 			}
 		}
@@ -4186,6 +4188,7 @@ bool Runtime::dischargeIdleMouseDown() {
 		if (script) {
 			ScriptEnvironmentVars vars;
 			vars.lmbDrag = true;
+			vars.clickInteractionID = _idleInteractionID;
 
 			activateScript(script, false, vars);
 			return true;
@@ -4233,6 +4236,7 @@ bool Runtime::dischargeIdleClick() {
 			if (script) {
 				ScriptEnvironmentVars vars;
 				vars.lmb = true;
+				vars.clickInteractionID = _idleInteractionID;
 
 				activateScript(script, false, vars);
 				return true;
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index 1dad02e42d8..d720952dc7e 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -179,6 +179,7 @@ struct ScriptEnvironmentVars {
 	ScriptEnvironmentVars();
 
 	uint panInteractionID;
+	uint clickInteractionID;
 	uint fpsOverride;
 	uint lastHighlightedItem;
 	uint animChangeFrameOffset;
diff --git a/engines/vcruise/runtime_scriptexec.cpp b/engines/vcruise/runtime_scriptexec.cpp
index 5eb06e961d8..71873636418 100644
--- a/engines/vcruise/runtime_scriptexec.cpp
+++ b/engines/vcruise/runtime_scriptexec.cpp
@@ -34,6 +34,7 @@ struct AD2044UnusualAnimationRules {
 	enum Type {
 		kTypeLoop,	// Loop the animation
 		kTypePlayFirstFrameOnly,
+		kTypeSkip,
 	};
 
 	uint roomNumber;
@@ -44,7 +45,10 @@ struct AD2044UnusualAnimationRules {
 };
 
 AD2044UnusualAnimationRules g_unusualAnimationRules[] = {
-	{87, 0x69, 0xa3, 0xa5, AD2044UnusualAnimationRules::kTypePlayFirstFrameOnly},
+	// Room, screen, interaction, animation lookup ID, rule
+	{87, 0x24, 0xa4, 0xa7, AD2044UnusualAnimationRules::kTypePlayFirstFrameOnly},	// Taking cigarette, don't play box spin
+	{87, 0x68, 0xa3, 0xa5, AD2044UnusualAnimationRules::kTypeLoop},					// Loop lit cigarette animation
+	{87, 0x69, 0xa3, 0xa5, AD2044UnusualAnimationRules::kTypeSkip},					// Taking lit cigarette, don't play cycling animation
 };
 
 #ifdef PEEK_STACK
@@ -2120,6 +2124,19 @@ void Runtime::scriptOpAnimAD2044(bool isForward) {
 	animDef.firstFrame = animRangeIt->_value.firstFrame;
 	animDef.lastFrame = animRangeIt->_value.lastFrame;
 
+	for (const AD2044UnusualAnimationRules &unusualAnimRule : g_unusualAnimationRules) {
+		if (static_cast<StackInt_t>(unusualAnimRule.animLookupID) == stackArgs[0] && unusualAnimRule.interactionID == _scriptEnv.clickInteractionID && unusualAnimRule.roomNumber == _roomNumber && unusualAnimRule.screenNumber == _screenNumber) {
+			switch (unusualAnimRule.ruleType) {
+			case AD2044UnusualAnimationRules::kTypePlayFirstFrameOnly:
+				animDef.lastFrame = animDef.firstFrame;
+				break;
+			default:
+				error("Unknown unusual animation rule");
+			}
+			break;
+		}
+	}
+
 	changeAnimation(animDef, animDef.firstFrame, true, _animSpeedDefault);
 
 	_gameState = kGameStateWaitingForAnimation;


Commit: 05da459107a838cec49e4b2d6e446b2f11eed309
    https://github.com/scummvm/scummvm/commit/05da459107a838cec49e4b2d6e446b2f11eed309
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2024-03-31T15:12:05-04:00

Commit Message:
VCRUISE: Fix C++11 narrowing conversion warning

Changed paths:
    engines/vcruise/midi_player.cpp


diff --git a/engines/vcruise/midi_player.cpp b/engines/vcruise/midi_player.cpp
index cf464122cbd..4d104719770 100644
--- a/engines/vcruise/midi_player.cpp
+++ b/engines/vcruise/midi_player.cpp
@@ -65,7 +65,7 @@ void MidiPlayer::setVolume(int volume) {
 		effectiveValue = 0x3fffu;
 
 	byte masterVolMessage[6] = {
-		0x7f, 0x00, 0x04, 0x01, (effectiveValue & 0x7f), ((effectiveValue >> 7) & 0x7f)
+		0x7f, 0x00, 0x04, 0x01, static_cast<byte>(effectiveValue & 0x7f), static_cast<byte>((effectiveValue >> 7) & 0x7f)
 	};
 
 	_midiDrv->sysEx(masterVolMessage, 6);




More information about the Scummvm-git-logs mailing list