[Scummvm-git-logs] scummvm master -> c515f70e8381b1768693195ed499a4e4f90f82c4

elasota noreply at scummvm.org
Sun Apr 23 05:16:03 UTC 2023


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

Summary:
3c4b00cb40 VCRUISE: Fix say2 opcode.  Add subtitles for speech.
230b0589e6 VCRUISE: Try to use NotoSans-Regular if it's available.
89781b6932 VCRUISE: Add volume ramp opcodes.
5e3acc3514 VCRUISE: Add animation subtitles.
c515f70e83 VCRUISE: Add menus


Commit: 3c4b00cb40cacbe3896527cc8b2d6fa100be5d76
    https://github.com/scummvm/scummvm/commit/3c4b00cb40cacbe3896527cc8b2d6fa100be5d76
Author: elasota (ejlasota at gmail.com)
Date: 2023-04-23T01:15:31-04:00

Commit Message:
VCRUISE: Fix say2 opcode.  Add subtitles for speech.

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


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 656fac63d70..4c56fa0ba04 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -213,7 +213,7 @@ void Runtime::GyroState::reset() {
 	isWaitingForAnimation = false;
 }
 
-Runtime::SubtitleDef::SubtitleDef() : subIndex(0), color{0, 0, 0}, unknownValue1(0), unknownValue2(0) {
+Runtime::SubtitleDef::SubtitleDef() : color{0, 0, 0}, unknownValue1(0), durationInDeciseconds(0) {
 }
 
 SfxPlaylistEntry::SfxPlaylistEntry() : frame(0), balance(0), volume(0), isUpdate(false) {
@@ -410,7 +410,7 @@ SoundCache::~SoundCache() {
 
 SoundInstance::SoundInstance()
 	: id(0), rampStartVolume(0), rampEndVolume(0), rampRatePerMSec(0), rampStartTime(0), rampTerminateOnCompletion(false),
-	  volume(0), balance(0), effectiveBalance(0), effectiveVolume(0), is3D(false), isLooping(false), isSpeech(false), isSilencedLoop(false), x(0), y(0), endTime(0) {
+	  volume(0), balance(0), effectiveBalance(0), effectiveVolume(0), is3D(false), isLooping(false), isSpeech(false), isSilencedLoop(false), x(0), y(0), endTime(0), duration(0) {
 }
 
 SoundInstance::~SoundInstance() {
@@ -737,7 +737,7 @@ Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &roo
 	  _delayCompletionTime(0),
 	  _panoramaState(kPanoramaStateInactive),
 	  _listenerX(0), _listenerY(0), _listenerAngle(0), _soundCacheIndex(0),
-	  _subtitleFont(nullptr), _subtitleExpireTime(0), _displayingSubtitles(false), _languageIndex(0) {
+	  _subtitleFont(nullptr), _isDisplayingSubtitles(false), _languageIndex(0) {
 
 	for (uint i = 0; i < kNumDirections; i++) {
 		_haveIdleAnimations[i] = false;
@@ -750,7 +750,7 @@ Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &roo
 
 	_rng.reset(new Common::RandomSource("vcruise"));
 
-	_subtitleFont = FontMan.getFontByUsage(Graphics::FontManager::kGUIFont);
+	_subtitleFont = FontMan.getFontByUsage(Graphics::FontManager::kLocalizedFont);
 	if (!_subtitleFont)
 		warning("Couldn't load subtitle font, subtitles will be disabled");
 }
@@ -890,6 +890,7 @@ bool Runtime::runFrame() {
 	uint32 timestamp = g_system->getMillis();
 
 	updateSounds(timestamp);
+	updateSubtitles();
 
 	return true;
 }
@@ -1563,6 +1564,7 @@ bool Runtime::runScript() {
 			DISPATCH_OP(Dup);
 			DISPATCH_OP(Swap);
 			DISPATCH_OP(Say1);
+			DISPATCH_OP(Say2);
 			DISPATCH_OP(Say3);
 			DISPATCH_OP(Say3Get);
 			DISPATCH_OP(SetTimer);
@@ -2462,6 +2464,7 @@ void Runtime::triggerSound(bool looping, SoundInstance &snd, uint volume, int32
 
 		snd.isSilencedLoop = true;
 		snd.endTime = 0;
+		snd.duration = 0;
 		return;
 	}
 
@@ -2469,6 +2472,8 @@ void Runtime::triggerSound(bool looping, SoundInstance &snd, uint volume, int32
 
 	SoundCache *cache = loadCache(snd);
 
+	snd.duration = cache->stream->getLength().msecs();
+
 	// Reset if looping state changes
 	if (cache->loopingStream && !looping) {
 		cache->player.reset();
@@ -2519,6 +2524,65 @@ void Runtime::triggerSoundRamp(SoundInstance &snd, uint durationMSec, uint newVo
 		snd.rampRatePerMSec = 65536 / durationMSec;
 }
 
+void Runtime::triggerWaveSubtitles(const SoundInstance &snd, const Common::String &id) {
+	char appendedCode[4] = {'_', '0', '0', '\0'};
+
+	char digit1 = '0';
+	char digit2 = '0';
+
+	stopSubtitles();
+
+	uint32 currentTime = g_system->getMillis(true);
+
+	uint32 soundEndTime = currentTime + snd.duration;
+
+	for (;;) {
+		if (digit2 == '9') {
+			digit2 = '0';
+
+			if (digit1 == '9')
+				return;	// This should never happen
+
+			digit1++;
+		} else
+			digit2++;
+
+		appendedCode[1] = digit1;
+		appendedCode[2] = digit2;
+
+		Common::String subtitleID = id + appendedCode;
+
+		WaveSubtitleMap_t::const_iterator subIt = _waveSubtitles.find(subtitleID);
+
+		if (subIt != _waveSubtitles.end()) {
+			const SubtitleDef &subDef = subIt->_value;
+
+			SubtitleQueueItem queueItem;
+			queueItem.startTime = currentTime;
+			queueItem.endTime = soundEndTime + 1000u;
+
+			if (_subtitleQueue.size() > 0)
+				queueItem.startTime = _subtitleQueue.back().endTime;
+
+			for (int ch = 0; ch < 3; ch++)
+				queueItem.color[ch] = subDef.color[ch];
+
+			if (subDef.durationInDeciseconds != 1)
+				queueItem.endTime = queueItem.startTime + subDef.durationInDeciseconds * 100u;
+
+			queueItem.str = subDef.str.decode(Common::kUtf8);
+
+			_subtitleQueue.push_back(queueItem);
+		}
+	}
+}
+
+void Runtime::stopSubtitles() {
+	_subtitleQueue.clear();
+	_isDisplayingSubtitles = false;
+	redrawTray();
+}
+
 void Runtime::stopSound(SoundInstance &sound) {
 	if (!sound.cache)
 		return;
@@ -2584,6 +2648,63 @@ void Runtime::updateSounds(uint32 timestamp) {
 	}
 }
 
+void Runtime::updateSubtitles() {
+	uint32 timestamp = g_system->getMillis(true);
+
+	while (_subtitleQueue.size() > 0) {
+		const SubtitleQueueItem &queueItem = _subtitleQueue[0];
+
+		if (_isDisplayingSubtitles) {
+			assert(_subtitleQueue.size() > 0);
+
+			if (queueItem.endTime <= timestamp) {
+				_subtitleQueue.remove_at(0);
+				_isDisplayingSubtitles = false;
+
+				if (_subtitleQueue.size() == 0)
+					redrawTray();
+			} else
+				break;
+		} else {
+			Graphics::ManagedSurface *surf = _traySection.surf.get();
+
+			Common::Array<Common::U32String> lines;
+
+			uint lineStart = 0;
+			for (;;) {
+				uint lineEnd = queueItem.str.find(static_cast<Common::u32char_type_t>('\\'), lineStart);
+				if (lineEnd == Common::U32String::npos) {
+					lines.push_back(queueItem.str.substr(lineStart));
+					break;
+				}
+
+				lines.push_back(queueItem.str.substr(lineStart, lineEnd - lineStart));
+				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);
+				}
+			}
+
+			commitSectionToScreen(_traySection, Common::Rect(0, 0, _traySection.rect.width(), _traySection.rect.height()));
+
+			_isDisplayingSubtitles = true;
+		}
+	}
+}
+
 void Runtime::update3DSounds() {
 	for (const Common::SharedPtr<SoundInstance> &sndPtr : _activeSounds) {
 		SoundInstance &snd = *sndPtr;
@@ -3020,7 +3141,27 @@ void Runtime::inventoryRemoveItem(uint itemID) {
 	}
 }
 
+void Runtime::redrawTray() {
+	if (_subtitleQueue.size() != 0)
+		return;
+
+	clearTray();
+
+	drawCompass();
+
+	for (uint slot = 0; slot < kNumInventorySlots; slot++)
+		drawInventory(slot);
+}
+
+void Runtime::clearTray() {
+	uint32 blackColor = _traySection.surf->format.RGBToColor(0, 0, 0);
+	_traySection.surf->fillRect(Common::Rect(0, 0, _traySection.surf->w, _traySection.surf->h), blackColor);
+}
+
 void Runtime::drawInventory(uint slot) {
+	if (_subtitleQueue.size() > 0)
+		return;
+
 	Common::Rect trayRect = _traySection.rect;
 	trayRect.translate(-trayRect.left, -trayRect.top);
 
@@ -3058,6 +3199,9 @@ void Runtime::drawInventory(uint slot) {
 }
 
 void Runtime::drawCompass() {
+	if (_subtitleQueue.size() > 0)
+		return;
+
 	bool haveHorizontalRotate = false;
 	bool haveUp = false;
 	bool haveDown = false;
@@ -3222,7 +3366,7 @@ void Runtime::loadSubtitles(Common::CodePage codePage) {
 						subDef->color[1] = ((colorCode >> 8) & 0xff);
 						subDef->color[2] = (colorCode & 0xff);
 						subDef->unknownValue1 = param1;
-						subDef->unknownValue2 = param2;
+						subDef->durationInDeciseconds = param2;
 						subDef->str = kv.value.substr(22, kv.value.size() - 23).decode(codePage).encode(Common::kUtf8);
 					}
 				}
@@ -3439,8 +3583,8 @@ void Runtime::restoreSaveGameSnapshot() {
 	_havePendingScreenChange = true;
 	_forceScreenChange = true;
 
-	for (uint slot = 0; slot < kNumInventorySlots; slot++)
-		drawInventory(slot);
+	stopSubtitles();
+	redrawTray();
 }
 
 void Runtime::saveGame(Common::WriteStream *stream) const {
@@ -4356,8 +4500,28 @@ void Runtime::scriptOpSay1(ScriptArg_t arg) {
 	SoundInstance *cachedSound = nullptr;
 	resolveSoundByName(soundIDStr, true, soundID, cachedSound);
 
-	if (cachedSound)
+	if (cachedSound) {
 		triggerSound(false, *cachedSound, 100, 0, false, true);
+		triggerWaveSubtitles(*cachedSound, soundIDStr);
+	}
+}
+
+void Runtime::scriptOpSay2(ScriptArg_t arg) {
+	TAKE_STACK_INT_NAMED(2, sndParamArgs);
+	TAKE_STACK_STR_NAMED(1, sndNameArgs);
+
+	StackInt_t soundID = 0;
+	SoundInstance *cachedSound = nullptr;
+	resolveSoundByName(sndNameArgs[0], true, soundID, cachedSound);
+
+	if (cachedSound) {
+		// The third param seems to control sound interruption, but say3 is a Reah-only op and it's only ever 1.
+		if (sndParamArgs[1] != 1)
+			error("Invalid interrupt arg for say2, only 1 is supported.");
+
+		triggerSound(false, *cachedSound, 100, 0, false, true);
+		triggerWaveSubtitles(*cachedSound, sndNameArgs[0]);
+	}
 }
 
 void Runtime::scriptOpSay3(ScriptArg_t arg) {
@@ -4380,6 +4544,8 @@ void Runtime::scriptOpSay3(ScriptArg_t arg) {
 		if (Common::find(_triggeredOneShots.begin(), _triggeredOneShots.end(), oneShot) == _triggeredOneShots.end()) {
 			triggerSound(false, *cachedSound, 100, 0, false, true);
 			_triggeredOneShots.push_back(oneShot);
+
+			triggerWaveSubtitles(*cachedSound, sndNameArgs[0]);
 		}
 	}
 }
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index a7c0e9fd542..14754dbbf37 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -230,6 +230,7 @@ struct SoundInstance {
 	SoundParams3D params3D;
 
 	uint32 endTime;
+	uint32 duration;
 };
 
 struct RandomAmbientSound {
@@ -596,13 +597,19 @@ private:
 	struct SubtitleDef {
 		SubtitleDef();
 
-		uint subIndex;
 		uint8 color[3];
 		uint unknownValue1;
-		uint unknownValue2;
+		uint durationInDeciseconds;
 		Common::String str;
 	};
 
+	struct SubtitleQueueItem {
+		Common::U32String str;
+		uint8 color[3];
+		uint32 startTime;
+		uint32 endTime;
+	};
+
 	bool runIdle();
 	bool runDelay();
 	bool runHorizontalPan(bool isRight);
@@ -651,10 +658,14 @@ private:
 	void triggerSoundRamp(SoundInstance &sound, uint durationMSec, uint newVolume, bool terminateOnCompletion);
 	void stopSound(SoundInstance &sound);
 	void updateSounds(uint32 timestamp);
+	void updateSubtitles();
 	void update3DSounds();
 	bool computeEffectiveVolumeAndBalance(SoundInstance &snd);
 	void triggerAmbientSounds();
 
+	void triggerWaveSubtitles(const SoundInstance &sound, const Common::String &id);
+	void stopSubtitles();
+
 	AnimationDef stackArgsToAnimDef(const StackInt_t *args) const;
 	void pushAnimDef(const AnimationDef &animDef);
 
@@ -675,6 +686,8 @@ private:
 
 	void inventoryAddItem(uint item);
 	void inventoryRemoveItem(uint item);
+	void redrawTray();
+	void clearTray();
 	void drawInventory(uint slot);
 	void drawCompass();
 	void resetInventoryHighlights();
@@ -754,6 +767,7 @@ private:
 	void scriptOpDup(ScriptArg_t arg);
 	void scriptOpSwap(ScriptArg_t arg);
 	void scriptOpSay1(ScriptArg_t arg);
+	void scriptOpSay2(ScriptArg_t arg);
 	void scriptOpSay3(ScriptArg_t arg);
 	void scriptOpSay3Get(ScriptArg_t arg);
 	void scriptOpSetTimer(ScriptArg_t arg);
@@ -963,9 +977,7 @@ private:
 	Common::SharedPtr<SaveGameSnapshot> _saveGame;
 
 	const Graphics::Font *_subtitleFont;
-	uint32 _subtitleExpireTime;
 	uint _languageIndex;
-	bool _displayingSubtitles;
 
 	typedef Common::HashMap<uint, SubtitleDef> FrameToSubtitleMap_t;
 	typedef Common::HashMap<uint, FrameToSubtitleMap_t> AnimSubtitleMap_t;
@@ -973,6 +985,8 @@ private:
 
 	AnimSubtitleMap_t _animSubtitles;
 	Common::HashMap<Common::String, SubtitleDef> _waveSubtitles;
+	Common::Array<SubtitleQueueItem> _subtitleQueue;
+	bool _isDisplayingSubtitles;
 };
 
 } // End of namespace VCruise
diff --git a/engines/vcruise/script.cpp b/engines/vcruise/script.cpp
index 346eb56ed6d..b11fee6ee5e 100644
--- a/engines/vcruise/script.cpp
+++ b/engines/vcruise/script.cpp
@@ -387,7 +387,7 @@ static ScriptNamedInstruction g_namedInstructions[] = {
 	{"dup", ProtoOp::kProtoOpScript, ScriptOps::kDup},
 	{"swap", ProtoOp::kProtoOpScript, ScriptOps::kSwap},
 	{"say1", ProtoOp::kProtoOpScript, ScriptOps::kSay1},
-	{"say2", ProtoOp::kProtoOpScript, ScriptOps::kSay3}, // FIXME: Figure out what the difference is between say2 and say3.  I think say2 is repeatable?  Maybe works as say1 instead?
+	{"say2", ProtoOp::kProtoOpScript, ScriptOps::kSay2},
 	{"say3", ProtoOp::kProtoOpScript, ScriptOps::kSay3},
 	{"say3@", ProtoOp::kProtoOpScript, ScriptOps::kSay3Get},
 	{"setTimer", ProtoOp::kProtoOpScript, ScriptOps::kSetTimer},
diff --git a/engines/vcruise/script.h b/engines/vcruise/script.h
index 5f69d5aaae4..10f60c93b0c 100644
--- a/engines/vcruise/script.h
+++ b/engines/vcruise/script.h
@@ -108,6 +108,7 @@ enum ScriptOp {
 	kDup,
 	kSwap,
 	kSay1,
+	kSay2,
 	kSay3,
 	kSay3Get,
 	kSetTimer,


Commit: 230b0589e689f4edc17b3123a1fe3bbf933a39cb
    https://github.com/scummvm/scummvm/commit/230b0589e689f4edc17b3123a1fe3bbf933a39cb
Author: elasota (ejlasota at gmail.com)
Date: 2023-04-23T01:15:31-04:00

Commit Message:
VCRUISE: Try to use NotoSans-Regular if it's available.

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


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 4c56fa0ba04..73b24bfc6ce 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -31,6 +31,7 @@
 
 #include "graphics/cursorman.h"
 #include "graphics/font.h"
+#include "graphics/fonts/ttf.h"
 #include "graphics/fontman.h"
 #include "graphics/wincursor.h"
 #include "graphics/managed_surface.h"
@@ -750,7 +751,14 @@ Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &roo
 
 	_rng.reset(new Common::RandomSource("vcruise"));
 
-	_subtitleFont = FontMan.getFontByUsage(Graphics::FontManager::kLocalizedFont);
+#ifdef USE_FREETYPE2
+	_subtitleFontKeepalive.reset(Graphics::loadTTFFontFromArchive("NotoSans-Regular.ttf", 16, Graphics::kTTFSizeModeCharacter, 0, Graphics::kTTFRenderModeLight));
+	_subtitleFont = _subtitleFontKeepalive.get();
+#endif
+
+	if (!_subtitleFont)
+		_subtitleFont = FontMan.getFontByUsage(Graphics::FontManager::kLocalizedFont);
+
 	if (!_subtitleFont)
 		warning("Couldn't load subtitle font, subtitles will be disabled");
 }
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index 14754dbbf37..7942166331b 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -977,6 +977,7 @@ private:
 	Common::SharedPtr<SaveGameSnapshot> _saveGame;
 
 	const Graphics::Font *_subtitleFont;
+	Common::SharedPtr<Graphics::Font> _subtitleFontKeepalive;
 	uint _languageIndex;
 
 	typedef Common::HashMap<uint, SubtitleDef> FrameToSubtitleMap_t;


Commit: 89781b69321a3c6a69fff8628bf6284a11631e22
    https://github.com/scummvm/scummvm/commit/89781b69321a3c6a69fff8628bf6284a11631e22
Author: elasota (ejlasota at gmail.com)
Date: 2023-04-23T01:15:32-04:00

Commit Message:
VCRUISE: Add volume ramp opcodes.

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


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 73b24bfc6ce..d067a6a5a8a 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -570,7 +570,7 @@ void SaveGameSnapshot::Sound::read(Common::ReadStream *stream) {
 	params3D.read(stream);
 }
 
-SaveGameSnapshot::SaveGameSnapshot() : roomNumber(0), screenNumber(0), direction(0), escOn(false), musicTrack(0), loadedAnimation(0),
+SaveGameSnapshot::SaveGameSnapshot() : roomNumber(0), screenNumber(0), direction(0), escOn(false), musicTrack(0), musicVolume(100), loadedAnimation(0),
 									   animDisplayingFrame(0), listenerX(0), listenerY(0), listenerAngle(0) {
 }
 
@@ -584,6 +584,7 @@ void SaveGameSnapshot::write(Common::WriteStream *stream) const {
 
 	stream->writeByte(escOn ? 1 : 0);
 	stream->writeSint32BE(musicTrack);
+	stream->writeUint32BE(musicVolume);
 
 	stream->writeUint32BE(loadedAnimation);
 	stream->writeUint32BE(animDisplayingFrame);
@@ -655,6 +656,11 @@ LoadGameOutcome SaveGameSnapshot::read(Common::ReadStream *stream) {
 	escOn = (stream->readByte() != 0);
 	musicTrack = stream->readSint32BE();
 
+	if (saveVersion >= 5)
+		musicVolume = stream->readUint32BE();
+	else
+		musicVolume = 100;
+
 	loadedAnimation = stream->readUint32BE();
 	animDisplayingFrame = stream->readUint32BE();
 
@@ -730,7 +736,9 @@ LoadGameOutcome SaveGameSnapshot::read(Common::ReadStream *stream) {
 Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &rootFSNode, VCruiseGameID gameID)
 	: _system(system), _mixer(mixer), _roomNumber(1), _screenNumber(0), _direction(0), _haveHorizPanAnimations(false), _loadedRoomNumber(0), _activeScreenNumber(0),
 	  _gameState(kGameStateBoot), _gameID(gameID), _havePendingScreenChange(false), _forceScreenChange(false), _havePendingReturnToIdleState(false), _havePendingCompletionCheck(false),
-	  _havePendingPlayAmbientSounds(false), _ambientSoundFinishTime(0), _scriptNextInstruction(0), _escOn(false), _debugMode(false), _fastAnimationMode(false), _musicTrack(0), _panoramaDirectionFlags(0),
+	  _havePendingPlayAmbientSounds(false), _ambientSoundFinishTime(0), _scriptNextInstruction(0), _escOn(false), _debugMode(false), _fastAnimationMode(false),
+	  _musicTrack(0), _musicVolume(100), _musicVolumeRampStartTime(0), _musicVolumeRampStartVolume(0), _musicVolumeRampRatePerMSec(0), _musicVolumeRampEnd(0),
+	  _panoramaDirectionFlags(0),
 	  _loadedAnimation(0), _animPendingDecodeFrame(0), _animDisplayingFrame(0), _animFirstFrame(0), _animLastFrame(0), _animStopFrame(0),
 	  _animStartTime(0), _animFramesDecoded(0), _animDecoderState(kAnimDecoderStateStopped),
 	  _animPlayWhileIdle(false), _idleIsOnInteraction(false), _idleHaveClickInteraction(false), _idleHaveDragInteraction(false), _idleInteractionID(0), _haveIdleStaticAnimation(false),
@@ -1553,8 +1561,7 @@ bool Runtime::runScript() {
 			DISPATCH_OP(StopSndLO);
 
 			DISPATCH_OP(Music);
-			DISPATCH_OP(MusicUp);
-			DISPATCH_OP(MusicDn);
+			DISPATCH_OP(MusicVolRamp);
 			DISPATCH_OP(Parm0);
 			DISPATCH_OP(Parm1);
 			DISPATCH_OP(Parm2);
@@ -2342,7 +2349,7 @@ void Runtime::changeMusicTrack(int track) {
 			Common::SharedPtr<Audio::AudioStream> loopingStream(Audio::makeLoopingAudioStream(audioStream, 0));
 
 			_musicPlayer.reset(new AudioPlayer(_mixer, loopingStream, Audio::Mixer::kMusicSoundType));
-			_musicPlayer->play(100, 0);
+			_musicPlayer->play(_musicVolume, 0);
 		}
 	} else {
 		warning("Music file '%s' is missing", wavFileName.c_str());
@@ -2654,6 +2661,40 @@ void Runtime::updateSounds(uint32 timestamp) {
 			}
 		}
 	}
+
+	if (_musicVolumeRampRatePerMSec != 0) {
+		bool negative = (_musicVolumeRampRatePerMSec < 0);
+
+		uint32 rampMax = 0;
+		uint32 absRampRate = 0;
+		if (negative) {
+			rampMax = _musicVolumeRampStartVolume - _musicVolumeRampEnd;
+			absRampRate = -_musicVolumeRampRatePerMSec;
+		} else {
+			rampMax = _musicVolumeRampEnd - _musicVolumeRampStartVolume;
+			absRampRate = _musicVolumeRampRatePerMSec;
+		}
+
+		uint32 rampTime = timestamp - _musicVolumeRampStartTime;
+
+		uint32 ramp = (rampTime * absRampRate) >> 16;
+		if (ramp > rampMax)
+			ramp = rampMax;
+
+		uint32 newVolume = _musicVolumeRampStartVolume;
+		if (negative)
+			newVolume -= ramp;
+		else
+			newVolume += ramp;
+
+		if (newVolume != _musicVolume) {
+			_musicPlayer->setVolume(static_cast<byte>(newVolume));
+			_musicVolume = newVolume;
+		}
+
+		if (newVolume == _musicVolumeRampEnd)
+			_musicVolumeRampRatePerMSec = 0;
+	}
 }
 
 void Runtime::updateSubtitles() {
@@ -3459,6 +3500,12 @@ void Runtime::recordSaveGameSnapshot() {
 
 	snapshot->musicTrack = _musicTrack;
 
+	snapshot->musicVolume = _musicVolume;
+
+	// If music volume is ramping, use the end volume and skip the ramp
+	if (_musicVolumeRampRatePerMSec != 0)
+		snapshot->musicVolume = _musicVolumeRampEnd;
+
 	snapshot->loadedAnimation = _loadedAnimation;
 	snapshot->animDisplayingFrame = _animDisplayingFrame;
 
@@ -3537,6 +3584,12 @@ void Runtime::restoreSaveGameSnapshot() {
 
 	_escOn = _saveGame->escOn;
 
+	_musicVolume = _saveGame->musicVolume;
+	_musicVolumeRampStartTime = 0;
+	_musicVolumeRampStartVolume = 0;
+	_musicVolumeRampRatePerMSec = 0;
+	_musicVolumeRampEnd = _musicVolume;
+
 	changeMusicTrack(_saveGame->musicTrack);
 
 	// Stop all sounds since the player instances are stored in the sound cache.
@@ -4277,20 +4330,31 @@ void Runtime::scriptOpMusic(ScriptArg_t arg) {
 	changeMusicTrack(stackArgs[0]);
 }
 
-void Runtime::scriptOpMusicUp(ScriptArg_t arg) {
+void Runtime::scriptOpMusicVolRamp(ScriptArg_t arg) {
 	TAKE_STACK_INT(2);
 
-	warning("Music volume ramp up is not implemented");
-	(void)stackArgs;
-}
+	uint32 duration = static_cast<uint32>(stackArgs[0]) * 100u;
+	uint32 newVolume = stackArgs[1];
 
-void Runtime::scriptOpMusicDn(ScriptArg_t arg) {
-	TAKE_STACK_INT(2);
+	_musicVolumeRampRatePerMSec = 0;
 
-	warning("Music volume ramp down is not implemented");
-	(void)stackArgs;
+	if (duration == 0) {
+		_musicVolume = newVolume;
+		if (_musicPlayer)
+			_musicPlayer->setVolume(newVolume);
+	} else {
+		if (newVolume != _musicVolume) {
+			uint32 timestamp = g_system->getMillis();
+
+			_musicVolumeRampRatePerMSec = (static_cast<int32>(newVolume) - static_cast<int32>(_musicVolume)) * 65536 / static_cast<int32>(duration);
+			_musicVolumeRampStartTime = timestamp;
+			_musicVolumeRampStartVolume = _musicVolume;
+			_musicVolumeRampEnd = newVolume;
+		}
+	}
 }
 
+
 void Runtime::scriptOpParm0(ScriptArg_t arg) {
 	TAKE_STACK_INT(4);
 
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index 7942166331b..1b9a6e2882c 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -336,7 +336,7 @@ struct SaveGameSnapshot {
 	LoadGameOutcome read(Common::ReadStream *stream);
 
 	static const uint kSaveGameIdentifier = 0x53566372;
-	static const uint kSaveGameCurrentVersion = 4;
+	static const uint kSaveGameCurrentVersion = 5;
 	static const uint kSaveGameEarliestSupportedVersion = 2;
 
 	struct InventoryItem {
@@ -377,6 +377,8 @@ struct SaveGameSnapshot {
 	bool escOn;
 	int musicTrack;
 
+	uint musicVolume;
+
 	uint loadedAnimation;
 	uint animDisplayingFrame;
 
@@ -748,8 +750,7 @@ private:
 	void scriptOpStopSndLO(ScriptArg_t arg);
 
 	void scriptOpMusic(ScriptArg_t arg);
-	void scriptOpMusicUp(ScriptArg_t arg);
-	void scriptOpMusicDn(ScriptArg_t arg);
+	void scriptOpMusicVolRamp(ScriptArg_t arg);
 	void scriptOpParm0(ScriptArg_t arg);
 	void scriptOpParm1(ScriptArg_t arg);
 	void scriptOpParm2(ScriptArg_t arg);
@@ -892,6 +893,13 @@ private:
 
 	Common::SharedPtr<AudioPlayer> _musicPlayer;
 	int _musicTrack;
+	uint _musicVolume;
+
+	uint32 _musicVolumeRampStartTime;
+	uint _musicVolumeRampStartVolume;
+	int32 _musicVolumeRampRatePerMSec;
+	uint _musicVolumeRampEnd;
+
 	SfxData _sfxData;
 
 	Common::SharedPtr<Video::AVIDecoder> _animDecoder;
diff --git a/engines/vcruise/script.cpp b/engines/vcruise/script.cpp
index b11fee6ee5e..93e728abefc 100644
--- a/engines/vcruise/script.cpp
+++ b/engines/vcruise/script.cpp
@@ -429,8 +429,8 @@ static ScriptNamedInstruction g_namedInstructions[] = {
 	{"stopSndLO", ProtoOp::kProtoOpScript, ScriptOps::kStopSndLO},
 
 	{"music", ProtoOp::kProtoOpScript, ScriptOps::kMusic},
-	{"musicUp", ProtoOp::kProtoOpScript, ScriptOps::kMusicUp},
-	{"musicDn", ProtoOp::kProtoOpScript, ScriptOps::kMusicDn},
+	{"musicUp", ProtoOp::kProtoOpScript, ScriptOps::kMusicVolRamp},
+	{"musicDn", ProtoOp::kProtoOpScript, ScriptOps::kMusicVolRamp},
 
 	{"parm0", ProtoOp::kProtoOpScript, ScriptOps::kParm0},
 	{"parm1", ProtoOp::kProtoOpScript, ScriptOps::kParm1},
diff --git a/engines/vcruise/script.h b/engines/vcruise/script.h
index 10f60c93b0c..f0086c07a12 100644
--- a/engines/vcruise/script.h
+++ b/engines/vcruise/script.h
@@ -90,8 +90,7 @@ enum ScriptOp {
 	kStopSndLA,
 	kStopSndLO,
 	kMusic,
-	kMusicUp,
-	kMusicDn,
+	kMusicVolRamp,
 	kParm0,
 	kParm1,
 	kParm2,


Commit: 5e3acc35142b7ff2f366fedfb793366af89449ee
    https://github.com/scummvm/scummvm/commit/5e3acc35142b7ff2f366fedfb793366af89449ee
Author: elasota (ejlasota at gmail.com)
Date: 2023-04-23T01:15:32-04:00

Commit Message:
VCRUISE: Add animation subtitles.

Changed paths:
    engines/vcruise/runtime.cpp


diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index d067a6a5a8a..4d3bb4b5061 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -1430,6 +1430,36 @@ void Runtime::continuePlayingAnimation(bool loop, bool useStopFrame, bool &outAn
 
 		update3DSounds();
 
+		AnimSubtitleMap_t::const_iterator animSubtitlesIt = _animSubtitles.find(_loadedAnimation);
+		if (animSubtitlesIt != _animSubtitles.end()) {
+			const FrameToSubtitleMap_t &frameMap = animSubtitlesIt->_value;
+
+			FrameToSubtitleMap_t::const_iterator frameIt = frameMap.find(_animDisplayingFrame);
+			if (frameIt != frameMap.end()) {
+				if (!millis)
+					millis = g_system->getMillis();
+
+				const SubtitleDef &subDef = frameIt->_value;
+
+				_subtitleQueue.clear();
+				_isDisplayingSubtitles = false;
+
+				SubtitleQueueItem queueItem;
+				queueItem.startTime = millis;
+				queueItem.endTime = millis + 1000u;
+
+				for (int ch = 0; ch < 3; ch++)
+					queueItem.color[ch] = subDef.color[ch];
+
+				if (subDef.durationInDeciseconds != 1)
+					queueItem.endTime = queueItem.startTime + subDef.durationInDeciseconds * 100u;
+
+				queueItem.str = subDef.str.decode(Common::kUtf8);
+
+				_subtitleQueue.push_back(queueItem);
+			}
+		}
+
 		if (_animPlaylist) {
 			uint decodeFrameInPlaylist = _animDisplayingFrame - _animFirstFrame;
 			for (const SfxPlaylistEntry &playlistEntry : _animPlaylist->entries) {
@@ -2417,6 +2447,8 @@ void Runtime::changeAnimation(const AnimationDef &animDef, uint initialFrame, bo
 		if (twoDtFile.open(twoDtFileName))
 			loadFrameData2(&twoDtFile);
 		twoDtFile.close();
+
+		stopSubtitles();
 	}
 
 	if (_animDecoderState == kAnimDecoderStatePlaying) {
@@ -2710,8 +2742,13 @@ void Runtime::updateSubtitles() {
 				_subtitleQueue.remove_at(0);
 				_isDisplayingSubtitles = false;
 
-				if (_subtitleQueue.size() == 0)
-					redrawTray();
+				if (_subtitleQueue.size() == 0) {
+					// Is this really what we want to be doing?
+					if (_escOn)
+						clearTray();
+					else
+						redrawTray();
+				}
 			} else
 				break;
 		} else {


Commit: c515f70e8381b1768693195ed499a4e4f90f82c4
    https://github.com/scummvm/scummvm/commit/c515f70e8381b1768693195ed499a4e4f90f82c4
Author: elasota (ejlasota at gmail.com)
Date: 2023-04-23T01:15:32-04:00

Commit Message:
VCRUISE: Add menus

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


diff --git a/engines/vcruise/menu.cpp b/engines/vcruise/menu.cpp
new file mode 100644
index 00000000000..f4da9ec2374
--- /dev/null
+++ b/engines/vcruise/menu.cpp
@@ -0,0 +1,886 @@
+/* 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/config-manager.h"
+
+#include "graphics/managed_surface.h"
+
+#include "audio/mixer.h"
+
+#include "vcruise/menu.h"
+#include "vcruise/runtime.h"
+
+namespace VCruise {
+
+class ReahMenuPage : public MenuPage {
+public:
+	ReahMenuPage();
+
+	bool run() override;
+	void start() override;
+
+protected:
+	virtual void onButtonClicked(uint button, bool &outChangedState);
+	virtual void onCheckboxClicked(uint button, bool &outChangedState);
+	virtual void onSliderMoved(uint slider);
+	virtual void eraseSlider(uint sliderIndex) const;
+
+protected:
+	enum ButtonState {
+		kButtonStateDisabled,
+		kButtonStateIdle,
+		kButtonStateHighlighted,
+		kButtonStatePressed,
+	};
+
+	enum CheckboxState {
+		kCheckboxStateOff,
+		kCheckboxStateOffHighlighted,
+		kCheckboxStateOn,
+		kCheckboxStateOnHighlighted,
+	};
+
+	enum InteractionState {
+		kInteractionStateNotInteracting,
+
+		kInteractionStateOverButton,
+		kInteractionStateClickingOnButton,
+		kInteractionStateClickingOffButton,
+
+		kInteractionStateOverSlider,
+		kInteractionStateDraggingSlider,
+
+		kInteractionStateOverCheckbox,
+		kInteractionStateClickingOnCheckbox,
+		kInteractionStateClickingOffCheckbox,
+	};
+
+	struct Button {
+		Button();
+		Button(Graphics::Surface *graphic, const Common::Rect &graphicRect, const Common::Rect &screenRect, const Common::Point &stateOffset, bool enabled);
+
+		Graphics::Surface *_graphic;
+		Common::Rect _graphicRect;
+		Common::Rect _screenRect;
+		Common::Point _stateOffset;
+		bool _enabled;
+	};
+
+	struct Slider {
+		Slider();
+		Slider(Graphics::Surface *graphic, const Common::Rect &baseRect, int value, int maxValue);
+
+		Graphics::Surface *_graphic;
+		Common::Rect _baseRect;
+		int _value;
+		int _maxValue;
+	};
+
+private:
+	void drawButtonInState(uint buttonIndex, ButtonState state) const;
+	void drawCheckboxInState(uint buttonIndex, CheckboxState state) const;
+	void drawSlider(uint sliderIndex) const;
+	void drawButtonFromListInState(const Common::Array<Button> &buttonList, uint buttonIndex, int state) const;
+
+	void handleMouseMove(const Common::Point &pt);
+	void handleMouseDown(const Common::Point &pt, bool &outChangedState);
+	void handleMouseUp(const Common::Point &pt, bool &outChangedState);
+
+protected:
+	Common::Array<Button> _buttons;
+	Common::Array<Button> _checkboxes;
+	Common::Array<Slider> _sliders;
+
+	InteractionState _interactionState;
+	uint _interactionIndex;
+
+	Common::Point _sliderDragStart;
+	int _sliderDragValue;
+};
+
+class ReahMenuBarPage : public ReahMenuPage {
+public:
+	explicit ReahMenuBarPage(uint page);
+
+	void start() override final;
+
+protected:
+	enum MenuBarButtonID {
+		kMenuBarButtonHelp,
+		kMenuBarButtonSave,
+		kMenuBarButtonLoad,
+		kMenuBarButtonSound,
+		kMenuBarButtonQuit,
+
+		kMenuBarButtonReturn,
+	};
+
+	virtual void addPageContents() = 0;
+	void onButtonClicked(uint button, bool &outChangedState) override;
+
+	uint _page;
+};
+
+class ReahHelpMenuPage : public ReahMenuBarPage {
+public:
+	ReahHelpMenuPage();
+
+	void addPageContents() override;
+};
+
+class ReahSoundMenuPage : public ReahMenuBarPage {
+public:
+	ReahSoundMenuPage();
+
+	void addPageContents() override;
+
+protected:
+	void eraseSlider(uint sliderIndex) const override;
+	void onCheckboxClicked(uint button, bool &outChangedState) override;
+	void onSliderMoved(uint slider) override;
+
+private:
+	enum SoundMenuCheckbox {
+		kCheckboxSound = 0,
+		kCheckboxMusic,
+	};
+
+	enum SoundMenuSlider {
+		kSliderSound = 0,
+		kSliderMusic,
+	};
+
+	void applySoundVolume() const;
+	void applyMusicVolume() const;
+
+	Common::SharedPtr<Graphics::ManagedSurface> _sliderKeyGraphic;
+
+	static const int kSoundSliderWidth = 300;
+	static const int kSoundSliderY = 127;
+	static const int kMusicSliderY = 275;
+
+	bool _soundChecked;
+	bool _musicChecked;
+};
+
+class ReahQuitMenuPage : public ReahMenuBarPage {
+public:
+	ReahQuitMenuPage();
+
+	void addPageContents() override;
+	void onButtonClicked(uint button, bool &outChangedState) override;
+
+private:
+	enum QuitMenuButton {
+		kButtonYes = 6,
+		kButtonNo,
+	};
+};
+
+class ReahMainMenuPage : public ReahMenuPage {
+public:
+	void start() override;
+
+protected:
+	void onButtonClicked(uint button, bool &outChangedState) override;
+
+private:
+	enum ButtonID {
+		kButtonContinue,
+		kButtonNew,
+		kButtonLoad,
+		kButtonSound,
+		kButtonCredits,
+		kButtonQuit,
+	};
+};
+
+ReahMenuPage::ReahMenuPage() : _interactionIndex(0), _interactionState(kInteractionStateNotInteracting), _sliderDragValue(0) {
+}
+
+bool ReahMenuPage::run() {
+	bool changedState = false;
+
+	OSEvent evt;
+	while (_menuInterface->popOSEvent(evt)) {
+		switch (evt.type) {
+		case kOSEventTypeLButtonUp:
+			handleMouseMove(evt.pos);
+			handleMouseUp(evt.pos, changedState);
+			if (changedState)
+				return changedState;
+			break;
+		case kOSEventTypeLButtonDown:
+			handleMouseMove(evt.pos);
+			handleMouseDown(evt.pos, changedState);
+			if (changedState)
+				return changedState;
+			break;
+		case kOSEventTypeMouseMove:
+			handleMouseMove(evt.pos);
+			break;
+		default:
+			break;
+		}
+	}
+
+	return false;
+}
+
+void ReahMenuPage::start() {
+	for (uint buttonIndex = 0; buttonIndex < _buttons.size(); buttonIndex++)
+		drawButtonInState(buttonIndex, _buttons[buttonIndex]._enabled ? kButtonStateIdle : kButtonStateDisabled);
+
+	for (uint checkboxIndex = 0; checkboxIndex < _checkboxes.size(); checkboxIndex++)
+		drawCheckboxInState(checkboxIndex, _checkboxes[checkboxIndex]._enabled ? kCheckboxStateOn : kCheckboxStateOff);
+
+	for (uint sliderIndex = 0; sliderIndex < _sliders.size(); sliderIndex++)
+		drawSlider(sliderIndex);
+
+	Common::Point mousePoint = _menuInterface->getMouseCoordinate();
+	handleMouseMove(mousePoint);
+}
+
+void ReahMenuPage::onButtonClicked(uint button, bool &outChangedState) {
+	outChangedState = false;
+}
+
+void ReahMenuPage::onCheckboxClicked(uint button, bool &outChangedState) {
+}
+
+void ReahMenuPage::onSliderMoved(uint slider) {
+}
+
+void ReahMenuPage::eraseSlider(uint sliderIndex) const {
+}
+
+void ReahMenuPage::handleMouseMove(const Common::Point &pt) {
+	switch (_interactionState) {
+	case kInteractionStateNotInteracting:
+		for (uint buttonIndex = 0; buttonIndex < _buttons.size(); buttonIndex++) {
+			const Button &button = _buttons[buttonIndex];
+
+			if (button._enabled && button._screenRect.contains(pt)) {
+				drawButtonInState(buttonIndex, kButtonStateHighlighted);
+
+				_interactionIndex = buttonIndex;
+				_interactionState = kInteractionStateOverButton;
+				break;
+			}
+		}
+
+		for (uint checkboxIndex = 0; checkboxIndex < _checkboxes.size(); checkboxIndex++) {
+			const Button &checkbox = _checkboxes[checkboxIndex];
+
+			if (checkbox._screenRect.contains(pt)) {
+				drawCheckboxInState(checkboxIndex, checkbox._enabled ? kCheckboxStateOnHighlighted : kCheckboxStateOffHighlighted);
+
+				_interactionIndex = checkboxIndex;
+				_interactionState = kInteractionStateOverCheckbox;
+				break;
+			}
+		}
+
+		for (uint sliderIndex = 0; sliderIndex < _sliders.size(); sliderIndex++) {
+			const Slider &slider = _sliders[sliderIndex];
+
+			Common::Rect sliderRect = slider._baseRect;
+			sliderRect.translate(slider._value, 0);
+
+			if (sliderRect.contains(pt)) {
+				_interactionIndex = sliderIndex;
+				_interactionState = kInteractionStateOverSlider;
+			}
+		}
+		break;
+
+	case kInteractionStateOverButton: {
+			const Button &button = _buttons[_interactionIndex];
+			if (!button._screenRect.contains(pt)) {
+				drawButtonInState(_interactionIndex, kButtonStateIdle);
+
+				_interactionState = kInteractionStateNotInteracting;
+				handleMouseMove(pt);
+			}
+		} break;
+
+	case kInteractionStateClickingOnButton: {
+			const Button &button = _buttons[_interactionIndex];
+			if (!button._screenRect.contains(pt)) {
+				drawButtonInState(_interactionIndex, kButtonStateHighlighted);
+
+				_interactionState = kInteractionStateClickingOffButton;
+			}
+		} break;
+
+	case kInteractionStateClickingOffButton: {
+			const Button &button = _buttons[_interactionIndex];
+			if (button._screenRect.contains(pt)) {
+				drawButtonInState(_interactionIndex, kButtonStatePressed);
+
+				_interactionState = kInteractionStateClickingOnButton;
+			}
+		} break;
+
+	case kInteractionStateOverSlider: {
+			const Slider &slider = _sliders[_interactionIndex];
+
+			Common::Rect sliderRect = slider._baseRect;
+			sliderRect.translate(slider._value, 0);
+
+			if (!sliderRect.contains(pt)) {
+				_interactionState = kInteractionStateNotInteracting;
+				handleMouseMove(pt);
+			}
+		} break;
+
+	case kInteractionStateDraggingSlider: {
+			Slider &slider = _sliders[_interactionIndex];
+
+			int newValue = _sliderDragValue + pt.x - _sliderDragStart.x;
+			if (newValue < 0)
+				newValue = 0;
+			else if (newValue >= slider._maxValue)
+				newValue = slider._maxValue;
+
+			if (newValue != slider._value) {
+				eraseSlider(_interactionIndex);
+				slider._value = newValue;
+				drawSlider(_interactionIndex);
+
+				onSliderMoved(_interactionIndex);
+			}
+		} break;
+
+	case kInteractionStateOverCheckbox: {
+			const Button &checkbox = _checkboxes[_interactionIndex];
+			if (!checkbox._screenRect.contains(pt)) {
+				drawCheckboxInState(_interactionIndex, checkbox._enabled ? kCheckboxStateOn : kCheckboxStateOff);
+
+				_interactionState = kInteractionStateNotInteracting;
+				handleMouseMove(pt);
+			}
+		} break;
+
+	case kInteractionStateClickingOnCheckbox: {
+			const Button &checkbox = _checkboxes[_interactionIndex];
+			if (!checkbox._screenRect.contains(pt)) {
+				drawCheckboxInState(_interactionIndex, checkbox._enabled ? kCheckboxStateOnHighlighted : kCheckboxStateOffHighlighted);
+
+				_interactionState = kInteractionStateClickingOffCheckbox;
+			}
+		} break;
+
+	case kInteractionStateClickingOffCheckbox: {
+			const Button &checkbox = _checkboxes[_interactionIndex];
+			if (checkbox._screenRect.contains(pt)) {
+				drawCheckboxInState(_interactionIndex, checkbox._enabled ? kCheckboxStateOffHighlighted : kCheckboxStateOnHighlighted);
+
+				_interactionState = kInteractionStateClickingOnCheckbox;
+			}
+		} break;
+
+	default:
+		error("Unhandled UI state");
+		break;
+	}
+}
+
+void ReahMenuPage::handleMouseDown(const Common::Point &pt, bool &outChangedState) {
+	switch (_interactionState) {
+	case kInteractionStateNotInteracting:
+	case kInteractionStateClickingOnButton:
+	case kInteractionStateClickingOffButton:
+	case kInteractionStateDraggingSlider:
+	case kInteractionStateClickingOnCheckbox:
+	case kInteractionStateClickingOffCheckbox:
+		break;
+
+	case kInteractionStateOverButton:
+		drawButtonInState(_interactionIndex, kButtonStatePressed);
+		_interactionState = kInteractionStateClickingOnButton;
+		break;
+
+	case kInteractionStateOverSlider:
+		_interactionState = kInteractionStateDraggingSlider;
+		_sliderDragStart = pt;
+		_sliderDragValue = _sliders[_interactionIndex]._value;
+		break;
+
+	case kInteractionStateOverCheckbox:
+		drawCheckboxInState(_interactionIndex, _checkboxes[_interactionIndex]._enabled ? kCheckboxStateOffHighlighted : kCheckboxStateOnHighlighted);
+		_interactionState = kInteractionStateClickingOnCheckbox;
+		break;
+
+	default:
+		break;
+	}
+}
+
+void ReahMenuPage::handleMouseUp(const Common::Point &pt, bool &outChangedState) {
+	switch (_interactionState) {
+	case kInteractionStateNotInteracting:
+	case kInteractionStateOverButton:
+	case kInteractionStateOverCheckbox:
+	case kInteractionStateOverSlider:
+		break;
+
+	case kInteractionStateClickingOnButton:
+		drawButtonInState(_interactionIndex, kButtonStateHighlighted);
+		_interactionState = kInteractionStateOverButton;
+
+		onButtonClicked(_interactionIndex, outChangedState);
+		break;
+
+	case kInteractionStateClickingOffButton:
+		drawButtonInState(_interactionIndex, kButtonStateIdle);
+		_interactionState = kInteractionStateNotInteracting;
+		handleMouseMove(pt);
+		break;
+
+	case kInteractionStateDraggingSlider:
+		_interactionState = kInteractionStateNotInteracting;
+		handleMouseMove(pt);
+		break;
+
+	case kInteractionStateClickingOnCheckbox:
+		_checkboxes[_interactionIndex]._enabled = !_checkboxes[_interactionIndex]._enabled;
+		drawCheckboxInState(_interactionIndex, _checkboxes[_interactionIndex]._enabled ? kCheckboxStateOnHighlighted : kCheckboxStateOffHighlighted);
+		_interactionState = kInteractionStateOverCheckbox;
+
+		onCheckboxClicked(_interactionIndex, outChangedState);
+		break;
+
+	case kInteractionStateClickingOffCheckbox:
+		drawCheckboxInState(_interactionIndex, _checkboxes[_interactionIndex]._enabled ? kCheckboxStateOn : kCheckboxStateOff);
+		_interactionState = kInteractionStateNotInteracting;
+		handleMouseMove(pt);
+		break;
+
+	default:
+		break;
+	}
+}
+
+ReahMenuBarPage::ReahMenuBarPage(uint page) : _page(page) {
+}
+
+void ReahMenuBarPage::start() {
+	Graphics::Surface *graphic = _menuInterface->getUIGraphic(4);
+
+	bool menuButtonsEnabled[5] = {true, true, true, true, true};
+
+	menuButtonsEnabled[1] = _menuInterface->canSave();
+	menuButtonsEnabled[_page] = false;
+
+	if (graphic) {
+		for (int buttonIndex = 0; buttonIndex < 5; buttonIndex++) {
+			Common::Rect buttonRect(128 * buttonIndex, 0, 128 * buttonIndex + 128, 44);
+			_buttons.push_back(Button(graphic, buttonRect, buttonRect, Common::Point(0, 44), menuButtonsEnabled[buttonIndex]));
+		}
+	}
+
+	Graphics::Surface *returnButtonGraphic = _menuInterface->getUIGraphic(9);
+	if (returnButtonGraphic)
+		_buttons.push_back(Button(returnButtonGraphic, Common::Rect(0, 0, 112, 44), Common::Rect(519, 423, 631, 467), Common::Point(0, 44), true));
+
+	Graphics::Surface *lowerBarGraphic = _menuInterface->getUIGraphic(8);
+
+	if (lowerBarGraphic) {
+		_menuInterface->getMenuSurface()->blitFrom(*lowerBarGraphic, Common::Point(0, 392));
+		_menuInterface->commitRect(Common::Rect(0, 392, 640, 480));
+	}
+
+	addPageContents();
+
+	ReahMenuPage::start();
+}
+
+void ReahMenuBarPage::onButtonClicked(uint button, bool &outChangedState) {
+	switch (button) {
+	case kMenuBarButtonHelp:
+		_menuInterface->changeMenu(new ReahHelpMenuPage());
+		outChangedState = true;
+		break;
+	case kMenuBarButtonLoad:
+		outChangedState = g_engine->loadGameDialog();
+		break;
+	case kMenuBarButtonSave:
+		g_engine->saveGameDialog();
+		break;
+	case kMenuBarButtonSound:
+		_menuInterface->changeMenu(new ReahSoundMenuPage());
+		outChangedState = true;
+		break;
+	case kMenuBarButtonQuit:
+		_menuInterface->changeMenu(new ReahQuitMenuPage());
+		outChangedState = true;
+		break;
+
+	case kMenuBarButtonReturn:
+		if (_menuInterface->canSave())
+			outChangedState = _menuInterface->reloadFromCheckpoint();
+		else {
+			_menuInterface->changeMenu(new ReahMainMenuPage());
+			outChangedState = true;
+		}
+		break;
+	default:
+		break;
+	}
+}
+
+void ReahMenuPage::drawButtonInState(uint buttonIndex, ButtonState state) const {
+	drawButtonFromListInState(_buttons, buttonIndex, state);
+}
+
+void ReahMenuPage::drawCheckboxInState(uint buttonIndex, CheckboxState state) const {
+	drawButtonFromListInState(_checkboxes, buttonIndex, state);
+}
+
+void ReahMenuPage::drawSlider(uint sliderIndex) const {
+	const Slider &slider = _sliders[sliderIndex];
+
+	Common::Point screenPoint(slider._baseRect.left + slider._value, slider._baseRect.top);
+
+	_menuInterface->getMenuSurface()->blitFrom(*slider._graphic, screenPoint);
+	_menuInterface->commitRect(Common::Rect(screenPoint.x, screenPoint.y, screenPoint.x + slider._baseRect.width(), screenPoint.y + slider._baseRect.height()));
+}
+
+void ReahMenuPage::drawButtonFromListInState(const Common::Array<Button> &buttonList, uint buttonIndex, int state) const {
+	const Button &button = buttonList[buttonIndex];
+
+	Common::Rect graphicRect = button._graphicRect;
+	graphicRect.translate(button._stateOffset.x * state, button._stateOffset.y * state);
+
+	_menuInterface->getMenuSurface()->blitFrom(*button._graphic, graphicRect, button._screenRect);
+	_menuInterface->commitRect(Common::Rect(button._screenRect.left, button._screenRect.top, button._screenRect.left + graphicRect.width(), button._screenRect.top + graphicRect.height()));
+}
+
+ReahMenuPage::Button::Button() : _graphic(nullptr), _enabled(true) {
+}
+
+ReahMenuPage::Button::Button(Graphics::Surface *graphic, const Common::Rect &graphicRect, const Common::Rect &screenRect, const Common::Point &stateOffset, bool enabled)
+	: _graphic(graphic), _graphicRect(graphicRect), _screenRect(screenRect), _stateOffset(stateOffset), _enabled(enabled) {
+}
+
+ReahMenuPage::Slider::Slider() : _graphic(nullptr), _value(0), _maxValue(1) {
+}
+
+ReahMenuPage::Slider::Slider(Graphics::Surface *graphic, const Common::Rect &baseRect, int value, int maxValue)
+	: _graphic(graphic), _baseRect(baseRect), _value(value), _maxValue(maxValue) {
+	assert(_value >= 0 && _value <= maxValue);
+}
+
+ReahHelpMenuPage::ReahHelpMenuPage() : ReahMenuBarPage(kMenuBarButtonHelp) {
+}
+
+void ReahHelpMenuPage::addPageContents() {
+	Graphics::Surface *helpBG = _menuInterface->getUIGraphic(12);
+	if (helpBG) {
+		_menuInterface->getMenuSurface()->blitFrom(*helpBG, Common::Point(0, 44));
+		_menuInterface->commitRect(Common::Rect(0, 44, helpBG->w, 44 + helpBG->h));
+	}
+}
+
+ReahSoundMenuPage::ReahSoundMenuPage() : ReahMenuBarPage(kMenuBarButtonSound), _soundChecked(false), _musicChecked(false) {
+}
+
+void ReahSoundMenuPage::addPageContents() {
+	Graphics::Surface *soundBG = _menuInterface->getUIGraphic(16);
+	if (soundBG) {
+		_menuInterface->getMenuSurface()->blitFrom(*soundBG, Common::Point(0, 44));
+		_menuInterface->commitRect(Common::Rect(0, 44, soundBG->w, 44 + soundBG->h));
+	}
+
+	int sndVol = 0;
+	if (ConfMan.hasKey("sfx_volume"))
+		sndVol = ConfMan.getInt("sfx_volume");
+
+	int musVol = 0;
+	if (ConfMan.hasKey("music_volume"))
+		musVol = ConfMan.getInt("music_volume");
+
+	_soundChecked = (sndVol != 0);
+	_musicChecked = (musVol != 0);
+
+	// Set defaults so clicking the checkbox does something
+	if (sndVol == 0)
+		sndVol = 3 * Audio::Mixer::kMaxMixerVolume / 4;
+
+	if (musVol == 0)
+		musVol = 3 * Audio::Mixer::kMaxMixerVolume / 4;
+
+	Graphics::Surface *soundGraphics = _menuInterface->getUIGraphic(17);
+	if (soundGraphics) {
+		_checkboxes.push_back(Button(soundGraphics, Common::Rect(0, 0, 112, 44), Common::Rect(77, 90, 77 + 112, 90 + 44), Common::Point(0, 44), _soundChecked));
+		_checkboxes.push_back(Button(soundGraphics, Common::Rect(112, 0, 224, 44), Common::Rect(77, 231, 77 + 112, 231 + 44), Common::Point(0, 44), _musicChecked));
+
+		Common::Point sliderSize(40, 60);
+
+		_sliderKeyGraphic.reset(new Graphics::ManagedSurface(sliderSize.x, sliderSize.y, Graphics::createPixelFormat<8888>()));
+
+		Graphics::PixelFormat srcFormat = soundGraphics->format;
+		Graphics::PixelFormat dstFormat = _sliderKeyGraphic->format;
+
+		for (int y = 0; y < sliderSize.y; y++) {
+			for (int x = 0; x < sliderSize.x; x++) {
+				uint32 maskColor = soundGraphics->getPixel(224 + x, y + 60);
+
+				byte r = 0;
+				byte g = 0;
+				byte b = 0;
+				srcFormat.colorToRGB(maskColor, r, g, b);
+
+				uint32 dstColor = 0;
+				if (r > 128) {
+					dstColor = dstFormat.ARGBToColor(0, 0, 0, 0);
+				} else {
+					uint32 srcColor = soundGraphics->getPixel(224 + x, y);
+					srcFormat.colorToRGB(srcColor, r, g, b);
+					dstColor = dstFormat.ARGBToColor(255, r, g, b);
+				}
+
+				_sliderKeyGraphic->setPixel(x, y, dstColor);
+			}
+		}
+
+		_sliders.push_back(Slider(_sliderKeyGraphic->surfacePtr(), Common::Rect(236, kSoundSliderY, 236 + 40, kSoundSliderY + 60), sndVol * kSoundSliderWidth / Audio::Mixer::kMaxMixerVolume, kSoundSliderWidth));
+		_sliders.push_back(Slider(_sliderKeyGraphic->surfacePtr(), Common::Rect(236, kMusicSliderY, 236 + 40, kMusicSliderY + 60), musVol * kSoundSliderWidth / Audio::Mixer::kMaxMixerVolume, kSoundSliderWidth));
+	}
+}
+
+void ReahSoundMenuPage::eraseSlider(uint sliderIndex) const {
+	Graphics::Surface *soundBG = _menuInterface->getUIGraphic(16);
+
+	if (soundBG) {
+		Common::Rect sliderRect = _sliders[sliderIndex]._baseRect;
+		sliderRect.translate(_sliders[sliderIndex]._value, 0);
+
+		Common::Rect backgroundSourceRect = sliderRect;
+		backgroundSourceRect.translate(0, -44);
+
+		_menuInterface->getMenuSurface()->blitFrom(*soundBG, backgroundSourceRect, Common::Point(sliderRect.left, sliderRect.top));
+		_menuInterface->commitRect(sliderRect);
+	}
+}
+
+void ReahSoundMenuPage::onCheckboxClicked(uint button, bool &outChangedState) {
+	if (button == kCheckboxSound) {
+		_soundChecked = _checkboxes[button]._enabled;
+		applySoundVolume();
+	}
+	if (button == kCheckboxMusic) {
+		_musicChecked = _checkboxes[button]._enabled;
+		applyMusicVolume();
+	}
+
+	outChangedState = false;
+}
+
+void ReahSoundMenuPage::onSliderMoved(uint slider) {
+	if (slider == kSliderSound && _soundChecked)
+		applySoundVolume();
+
+	if (slider == kSliderMusic && _musicChecked)
+		applyMusicVolume();
+}
+
+void ReahSoundMenuPage::applySoundVolume() const {
+	int vol = 0;
+
+	if (_soundChecked)
+		vol = _sliders[kSliderSound]._value * Audio::Mixer::kMaxMixerVolume / _sliders[kSliderSound]._maxValue;
+
+	ConfMan.setInt("sfx_volume", vol, ConfMan.getActiveDomainName());
+
+	if (g_engine->_mixer)
+		g_engine->_mixer->setVolumeForSoundType(Audio::Mixer::kSFXSoundType, vol);
+}
+
+void ReahSoundMenuPage::applyMusicVolume() const {
+	int vol = 0;
+
+	if (_musicChecked)
+		vol = _sliders[kSliderMusic]._value * Audio::Mixer::kMaxMixerVolume / _sliders[kSliderMusic]._maxValue;
+
+	ConfMan.setInt("music_volume", vol, ConfMan.getActiveDomainName());
+
+	if (g_engine->_mixer)
+		g_engine->_mixer->setVolumeForSoundType(Audio::Mixer::kMusicSoundType, vol);
+}
+
+ReahQuitMenuPage::ReahQuitMenuPage() : ReahMenuBarPage(kMenuBarButtonQuit) {
+}
+
+void ReahQuitMenuPage::addPageContents() {
+	Graphics::ManagedSurface *menuSurf = _menuInterface->getMenuSurface();
+	menuSurf->fillRect(Common::Rect(0, 44, 640, 392), menuSurf->format.RGBToColor(0, 0, 0));
+
+	Graphics::Surface *borderGraphic = _menuInterface->getUIGraphic(10);
+
+	if (borderGraphic) {
+		Graphics::PixelFormat borderGraphicFmt = borderGraphic->format;
+		Graphics::PixelFormat menuSurfFmt = menuSurf->format;
+		byte r = 0;
+		byte g = 0;
+		byte b = 0;
+
+		const int xOffsets[2] = {0, 640 - 16};
+
+		for (int y = 0; y < borderGraphic->h; y++) {
+			for (int x = 0; x < 16; x++) {
+				uint32 pixels[2] = { borderGraphic->getPixel(x, y), borderGraphic->getPixel(x + 16, y) };
+				int intensities[2] = {(16 - x) * 32, (x + 1) * 32};
+				for (int i = 0; i < 2; i++) {
+					borderGraphicFmt.colorToRGB(pixels[i], r, g, b);
+
+					int intensity = intensities[i];
+					if (intensity < 256) {
+						r = (r * intensity) >> 8;
+						g = (g * intensity) >> 8;
+						b = (b * intensity) >> 8;
+					}
+
+					menuSurf->setPixel(x + xOffsets[i], y + 44, menuSurfFmt.RGBToColor(r, g, b));
+				}
+			}
+		}
+	}
+
+	Graphics::Surface *windowGraphic = _menuInterface->getUIGraphic(13);
+
+	if (windowGraphic)
+		menuSurf->blitFrom(*windowGraphic, Common::Point(82, 114));
+
+	Graphics::Surface *textGraphic = _menuInterface->getUIGraphic(14);
+
+	if (textGraphic)
+		menuSurf->blitFrom(*textGraphic, Common::Rect(0, 72, textGraphic->w, textGraphic->h), Common::Point(82, 174));
+
+	Graphics::Surface *buttonsGraphic = _menuInterface->getUIGraphic(15);
+
+	if (buttonsGraphic) {
+		_buttons.push_back(Button(buttonsGraphic, Common::Rect(224, 0, 336, 44), Common::Rect(174, 246, 286, 290), Common::Point(0, 44), true));
+		_buttons.push_back(Button(buttonsGraphic, Common::Rect(336, 0, 448, 44), Common::Rect(351, 248, 463, 292), Common::Point(0, 44), true));
+	}
+
+	_menuInterface->commitRect(Common::Rect(0, 44, 640, 392));
+
+	// Disable the "Return" button since the "No" button is functionally the same (and Reah does this)
+	_buttons[kMenuBarButtonReturn]._enabled = false;
+}
+
+void ReahQuitMenuPage::onButtonClicked(uint button, bool &outChangedState) {
+	ReahMenuBarPage::onButtonClicked(button, outChangedState);
+
+	if (button == kButtonYes)
+		_menuInterface->quitGame();
+	else if (button == kButtonNo)
+		onButtonClicked(kMenuBarButtonReturn, outChangedState);
+}
+
+void ReahMainMenuPage::start() {
+	Graphics::Surface *bgGraphic = _menuInterface->getUIGraphic(0);
+
+	Graphics::ManagedSurface *menuSurf = _menuInterface->getMenuSurface();
+
+	if (bgGraphic) {
+		menuSurf->blitFrom(*bgGraphic, Common::Point(0, 0));
+	}
+
+	_menuInterface->commitRect(Common::Rect(0, 0, 640, 480));
+
+	Graphics::Surface *buttonGraphic = _menuInterface->getUIGraphic(1);
+
+	Common::Point buttonStateOffset = Common::Point(112, 0);
+	Common::Point buttonTopLeft = Common::Point(492, 66);
+	const int buttonTopYs[6] = {66, 119, 171, 224, 277, 330};
+
+	for (int i = 0; i < 6; i++) {
+		bool isEnabled = true;
+		if (i == kButtonContinue)
+			isEnabled = _menuInterface->hasDefaultSave();
+
+		_buttons.push_back(Button(buttonGraphic, Common::Rect(0, i * 44, 112, i * 44 + 44), Common::Rect(492, buttonTopYs[i], 492 + 112, buttonTopYs[i] + 44), Common::Point(112, 0), isEnabled));
+	}
+
+	ReahMenuPage::start();
+}
+
+void ReahMainMenuPage::onButtonClicked(uint button, bool &outChangedState) {
+	switch (button) {
+	case kButtonContinue: {
+			Common::Error loadError = g_engine->loadGameState(g_engine->getAutosaveSlot());
+			outChangedState = (loadError.getCode() == Common::kNoError);
+		} break;
+
+	case kButtonNew:
+		_menuInterface->restartGame();
+		outChangedState = true;
+		break;
+
+	case kButtonLoad:
+		outChangedState = g_engine->loadGameDialog();
+		break;
+
+	case kButtonSound:
+		_menuInterface->changeMenu(new ReahSoundMenuPage());
+		outChangedState = true;
+		break;
+
+	case kButtonCredits:
+		_menuInterface->goToCredits();
+		outChangedState = true;
+		break;
+
+	case kButtonQuit:
+		_menuInterface->changeMenu(new ReahQuitMenuPage());
+		outChangedState = true;
+		break;
+	}
+}
+
+MenuInterface::~MenuInterface() {
+}
+
+MenuPage::MenuPage() : _menuInterface(nullptr) {
+}
+
+MenuPage::~MenuPage() {
+}
+
+void MenuPage::init(const MenuInterface *menuInterface) {
+	_menuInterface = menuInterface;
+}
+
+void MenuPage::start() {
+}
+
+bool MenuPage::run() {
+	return false;
+}
+
+MenuPage *createMenuReahMain() {
+	return new ReahMainMenuPage();
+}
+
+} // End of namespace VCruise
diff --git a/engines/vcruise/menu.h b/engines/vcruise/menu.h
new file mode 100644
index 00000000000..5fd1beab317
--- /dev/null
+++ b/engines/vcruise/menu.h
@@ -0,0 +1,83 @@
+/* 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_MENU_H
+#define VCRUISE_MENU_H
+
+#include "common/array.h"
+#include "common/ptr.h"
+
+namespace Graphics {
+
+struct Surface;
+class ManagedSurface;
+
+} // End of namespace Graphics
+
+namespace Common {
+
+struct Rect;
+
+} // End of namespace Common
+
+namespace VCruise {
+
+struct OSEvent;
+class MenuPage;
+class Runtime;
+
+class MenuInterface {
+public:
+	virtual ~MenuInterface();
+
+	virtual void commitRect(const Common::Rect &rect) const = 0;
+	virtual bool popOSEvent(OSEvent &evt) const = 0;
+	virtual Graphics::Surface *getUIGraphic(uint index) const = 0;
+	virtual Graphics::ManagedSurface *getMenuSurface() const = 0;
+	virtual bool hasDefaultSave() const = 0;
+	virtual Common::Point getMouseCoordinate() const = 0;
+	virtual void restartGame() const = 0;
+	virtual void goToCredits() const = 0;
+	virtual void changeMenu(MenuPage *newPage) const = 0;
+	virtual void quitGame() const = 0;
+	virtual bool canSave() const = 0;
+	virtual bool reloadFromCheckpoint() const = 0;
+};
+
+class MenuPage {
+public:
+	MenuPage();
+	virtual ~MenuPage();
+
+	void init(const MenuInterface *menuInterface);
+
+	virtual void start();
+	virtual bool run();
+
+protected:
+	const MenuInterface *_menuInterface;
+};
+
+MenuPage *createMenuReahMain();
+
+} // End of namespace VCruise
+
+#endif
diff --git a/engines/vcruise/module.mk b/engines/vcruise/module.mk
index 5360a78e6d6..95134a32ce3 100644
--- a/engines/vcruise/module.mk
+++ b/engines/vcruise/module.mk
@@ -3,6 +3,7 @@ MODULE := engines/vcruise
 MODULE_OBJS = \
 	audio_player.o \
 	metaengine.o \
+	menu.o \
 	runtime.o \
 	script.o \
 	textparser.o \
diff --git a/engines/vcruise/runtime.cpp b/engines/vcruise/runtime.cpp
index 4d3bb4b5061..40791fd07fa 100644
--- a/engines/vcruise/runtime.cpp
+++ b/engines/vcruise/runtime.cpp
@@ -22,10 +22,12 @@
 #include "common/formats/winexe.h"
 #include "common/config-manager.h"
 #include "common/endian.h"
+#include "common/events.h"
 #include "common/file.h"
 #include "common/math.h"
 #include "common/ptr.h"
 #include "common/random.h"
+#include "common/savefile.h"
 #include "common/system.h"
 #include "common/stream.h"
 
@@ -46,13 +48,114 @@
 #include "gui/message.h"
 
 #include "vcruise/audio_player.h"
+#include "vcruise/menu.h"
 #include "vcruise/runtime.h"
 #include "vcruise/script.h"
 #include "vcruise/textparser.h"
+#include "vcruise/vcruise.h"
 
 
 namespace VCruise {
 
+class RuntimeMenuInterface : public MenuInterface {
+public:
+	explicit RuntimeMenuInterface(Runtime *runtime);
+
+	void commitRect(const Common::Rect &rect) const override;
+	bool popOSEvent(OSEvent &evt) const override;
+	Graphics::Surface *getUIGraphic(uint index) const override;
+	Graphics::ManagedSurface *getMenuSurface() const override;
+	bool hasDefaultSave() const override;
+	Common::Point getMouseCoordinate() const override;
+	void restartGame() const override;
+	void goToCredits() const override;
+	void changeMenu(MenuPage *newPage) const override;
+	void quitGame() const override;
+	bool canSave() const override;
+	bool reloadFromCheckpoint() const override;
+
+private:
+	Runtime *_runtime;
+};
+
+
+RuntimeMenuInterface::RuntimeMenuInterface(Runtime *runtime) : _runtime(runtime) {
+}
+
+void RuntimeMenuInterface::commitRect(const Common::Rect &rect) const {
+	_runtime->commitSectionToScreen(_runtime->_fullscreenMenuSection, rect);
+}
+
+bool RuntimeMenuInterface::popOSEvent(OSEvent &evt) const {
+	return _runtime->popOSEvent(evt);
+}
+
+Graphics::Surface *RuntimeMenuInterface::getUIGraphic(uint index) const {
+	if (index >= _runtime->_uiGraphics.size())
+		return nullptr;
+	return _runtime->_uiGraphics[index].get();
+}
+
+Graphics::ManagedSurface *RuntimeMenuInterface::getMenuSurface() const {
+	return _runtime->_fullscreenMenuSection.surf.get();
+}
+
+bool RuntimeMenuInterface::hasDefaultSave() const {
+	return static_cast<VCruiseEngine *>(g_engine)->hasDefaultSave();
+}
+
+Common::Point RuntimeMenuInterface::getMouseCoordinate() const {
+	return _runtime->_mousePos;
+}
+
+void RuntimeMenuInterface::restartGame() const {
+	Common::SharedPtr<SaveGameSnapshot> snapshot(new SaveGameSnapshot());
+
+	if (_runtime->_gameID == GID_REAH) {
+		snapshot->roomNumber = 1;
+		snapshot->screenNumber = 0xb0;
+		snapshot->loadedAnimation = 1;
+	} else {
+		error("Don't know what screen to start on for this game");
+	}
+
+	_runtime->_saveGame = snapshot;
+	_runtime->restoreSaveGameSnapshot();
+}
+
+void RuntimeMenuInterface::goToCredits() const {
+	_runtime->clearScreen();
+
+	if (_runtime->_gameID == GID_REAH) {
+		_runtime->changeToScreen(40, 0xa1);
+	} else {
+		error("Don't know what screen to go to for credits for this game");
+	}
+}
+
+void RuntimeMenuInterface::changeMenu(MenuPage *newPage) const {
+	_runtime->changeToMenuPage(newPage);
+}
+
+void RuntimeMenuInterface::quitGame() const {
+	Common::Event evt;
+	evt.type = Common::EVENT_QUIT;
+
+	g_engine->getEventManager()->pushEvent(evt);
+}
+
+bool RuntimeMenuInterface::canSave() const {
+	return _runtime->canSave();
+}
+
+bool RuntimeMenuInterface::reloadFromCheckpoint() const {
+	if (!_runtime->canSave())
+		return false;
+
+	_runtime->restoreSaveGameSnapshot();
+	return true;
+}
+
 AnimationDef::AnimationDef() : animNum(0), firstFrame(0), lastFrame(0) {
 }
 
@@ -77,7 +180,10 @@ const MapScreenDirectionDef *MapDef::getScreenDirection(uint screen, uint direct
 	return screenDirections[screen][direction].get();
 }
 
-ScriptEnvironmentVars::ScriptEnvironmentVars() : lmb(false), lmbDrag(false), panInteractionID(0), fpsOverride(0), lastHighlightedItem(0) {
+ScriptEnvironmentVars::ScriptEnvironmentVars() : lmb(false), lmbDrag(false), esc(false), exitToMenu(false), panInteractionID(0), fpsOverride(0), lastHighlightedItem(0) {
+}
+
+OSEvent::OSEvent() : type(kOSEventTypeInvalid), keyCode(static_cast<Common::KeyCode>(0)) {
 }
 
 void Runtime::RenderSection::init(const Common::Rect &paramRect, const Graphics::PixelFormat &fmt) {
@@ -86,9 +192,6 @@ void Runtime::RenderSection::init(const Common::Rect &paramRect, const Graphics:
 	surf->fillRect(Common::Rect(0, 0, surf->w, surf->h), 0xffffffff);
 }
 
-Runtime::OSEvent::OSEvent() : type(kOSEventTypeInvalid), keyCode(static_cast<Common::KeyCode>(0)) {
-}
-
 Runtime::StackValue::ValueUnion::ValueUnion() {
 }
 
@@ -739,13 +842,14 @@ Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &roo
 	  _havePendingPlayAmbientSounds(false), _ambientSoundFinishTime(0), _scriptNextInstruction(0), _escOn(false), _debugMode(false), _fastAnimationMode(false),
 	  _musicTrack(0), _musicVolume(100), _musicVolumeRampStartTime(0), _musicVolumeRampStartVolume(0), _musicVolumeRampRatePerMSec(0), _musicVolumeRampEnd(0),
 	  _panoramaDirectionFlags(0),
-	  _loadedAnimation(0), _animPendingDecodeFrame(0), _animDisplayingFrame(0), _animFirstFrame(0), _animLastFrame(0), _animStopFrame(0),
+	  _loadedAnimation(0), _loadedAnimationHasSound(false), _animPendingDecodeFrame(0), _animDisplayingFrame(0), _animFirstFrame(0), _animLastFrame(0), _animStopFrame(0),
 	  _animStartTime(0), _animFramesDecoded(0), _animDecoderState(kAnimDecoderStateStopped),
 	  _animPlayWhileIdle(false), _idleIsOnInteraction(false), _idleHaveClickInteraction(false), _idleHaveDragInteraction(false), _idleInteractionID(0), _haveIdleStaticAnimation(false),
 	  /*_loadedArea(0), */_lmbDown(false), _lmbDragging(false), _lmbReleaseWasClick(false), _lmbDownTime(0),
 	  _delayCompletionTime(0),
 	  _panoramaState(kPanoramaStateInactive),
 	  _listenerX(0), _listenerY(0), _listenerAngle(0), _soundCacheIndex(0),
+	  _isInGame(false),
 	  _subtitleFont(nullptr), _isDisplayingSubtitles(false), _languageIndex(0) {
 
 	for (uint i = 0; i < kNumDirections; i++) {
@@ -769,15 +873,18 @@ Runtime::Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &roo
 
 	if (!_subtitleFont)
 		warning("Couldn't load subtitle font, subtitles will be disabled");
+
+	_menuInterface.reset(new RuntimeMenuInterface(this));
 }
 
 Runtime::~Runtime() {
 }
 
-void Runtime::initSections(Common::Rect gameRect, Common::Rect menuRect, Common::Rect trayRect, const Graphics::PixelFormat &pixFmt) {
+void Runtime::initSections(const Common::Rect &gameRect, const Common::Rect &menuRect, const Common::Rect &trayRect, const Common::Rect &fullscreenMenuRect, const Graphics::PixelFormat &pixFmt) {
 	_gameSection.init(gameRect, pixFmt);
 	_menuSection.init(menuRect, pixFmt);
 	_traySection.init(trayRect, pixFmt);
+	_fullscreenMenuSection.init(fullscreenMenuRect, pixFmt);
 }
 
 void Runtime::loadCursors(const char *exeName) {
@@ -891,6 +998,9 @@ bool Runtime::runFrame() {
 		case kGameStateGyroAnimation:
 			moreActions = runGyroAnimation();
 			break;
+		case kGameStateMenu:
+			moreActions = _menuPage->run();
+			break;
 		default:
 			error("Unknown game state");
 			return false;
@@ -929,8 +1039,7 @@ bool Runtime::bootGame(bool newGame) {
 
 	if (newGame) {
 		if (_gameID == GID_REAH) {
-			// TODO: Change to the logo instead (0xb1) instead when menus are implemented
-			changeToScreen(1, 0xb0);
+			changeToScreen(1, 0xb1);
 		} else
 			error("Couldn't figure out what screen to start on");
 	}
@@ -1010,6 +1119,15 @@ bool Runtime::bootGame(bool newGame) {
 	loadSubtitles(codePage);
 	debug(1, "Subtitles loaded OK");
 
+	_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);
+		}
+	}
+
 	return true;
 }
 
@@ -1686,6 +1804,14 @@ void Runtime::terminateScript() {
 
 	if (_havePendingScreenChange)
 		changeToScreen(_roomNumber, _screenNumber);
+
+	if (_scriptEnv.exitToMenu && _gameState == kGameStateIdle) {
+		changeToCursor(_cursors[kCursorArrow]);
+		if (_gameID == GID_REAH)
+			changeToMenuPage(createMenuReahMain());
+		else
+			error("Missing main menu behavior for this game");
+	}
 }
 
 bool Runtime::checkCompletionConditions() {
@@ -2366,7 +2492,7 @@ void Runtime::loadFrameData2(Common::SeekableReadStream *stream) {
 }
 
 void Runtime::changeMusicTrack(int track) {
-	if (track == _musicTrack)
+	if (track == _musicTrack && _musicPlayer.get() != nullptr)
 		return;
 
 	_musicPlayer.reset();
@@ -2448,6 +2574,8 @@ void Runtime::changeAnimation(const AnimationDef &animDef, uint initialFrame, bo
 			loadFrameData2(&twoDtFile);
 		twoDtFile.close();
 
+		_loadedAnimationHasSound = (_animDecoder->getAudioTrackCount() > 0);
+
 		stopSubtitles();
 	}
 
@@ -2474,7 +2602,7 @@ void Runtime::changeAnimation(const AnimationDef &animDef, uint initialFrame, bo
 		_animFrameRateLock = Fraction(_scriptEnv.fpsOverride, 1);
 		_scriptEnv.fpsOverride = 0;
 	} else {
-		if (!_fastAnimationMode && _animDecoder && _animDecoder->getAudioTrackCount() == 0)
+		if (!_fastAnimationMode && _animDecoder && !_loadedAnimationHasSound)
 			_animFrameRateLock = defaultFrameRate;
 	}
 
@@ -3227,6 +3355,10 @@ void Runtime::inventoryRemoveItem(uint itemID) {
 	}
 }
 
+void Runtime::clearScreen() {
+	_system->fillScreen(_system->getScreenFormat().RGBToColor(0, 0, 0));
+}
+
 void Runtime::redrawTray() {
 	if (_subtitleQueue.size() != 0)
 		return;
@@ -3245,7 +3377,7 @@ void Runtime::clearTray() {
 }
 
 void Runtime::drawInventory(uint slot) {
-	if (_subtitleQueue.size() > 0)
+	if (_subtitleQueue.size() > 0 || _loadedAnimationHasSound || !_isInGame)
 		return;
 
 	Common::Rect trayRect = _traySection.rect;
@@ -3285,7 +3417,7 @@ void Runtime::drawInventory(uint slot) {
 }
 
 void Runtime::drawCompass() {
-	if (_subtitleQueue.size() > 0)
+	if (_subtitleQueue.size() > 0 || _loadedAnimationHasSound || !_isInGame)
 		return;
 
 	bool haveHorizontalRotate = false;
@@ -3384,6 +3516,10 @@ Common::SharedPtr<Graphics::Surface> Runtime::loadGraphic(const Common::String &
 		return nullptr;
 	}
 
+	// 1-byte BMPs are placeholders for no file
+	if (f.size() == 1)
+		return nullptr;
+
 	Image::BitmapDecoder bmpDecoder;
 	if (!bmpDecoder.loadStream(f)) {
 		warning("Failed to load BMP file '%s'", filePath.c_str());
@@ -3461,6 +3597,15 @@ void Runtime::loadSubtitles(Common::CodePage codePage) {
 	}
 }
 
+void Runtime::changeToMenuPage(MenuPage *menuPage) {
+	_menuPage.reset(menuPage);
+
+	_gameState = kGameStateMenu;
+
+	menuPage->init(_menuInterface.get());
+	menuPage->start();
+}
+
 void Runtime::onLButtonDown(int16 x, int16 y) {
 	onMouseMove(x, y);
 
@@ -3502,10 +3647,13 @@ bool Runtime::canSave() const {
 }
 
 bool Runtime::canLoad() const {
-	return _gameState == kGameStateIdle;
+	return _gameState == kGameStateIdle || _gameState == kGameStateMenu;
 }
 
 void Runtime::recordSaveGameSnapshot() {
+	if (!_isInGame)
+		return;
+
 	_saveGame.reset();
 
 	uint32 timeBase = g_system->getMillis();
@@ -3677,11 +3825,13 @@ void Runtime::restoreSaveGameSnapshot() {
 	changeAnimation(animDef, false);
 
 	_gameState = kGameStateWaitingForAnimation;
+	_isInGame = true;
 
 	_havePendingScreenChange = true;
 	_forceScreenChange = true;
 
 	stopSubtitles();
+	clearScreen();
 	redrawTray();
 }
 
@@ -4919,8 +5069,14 @@ void Runtime::scriptOpEscOff(ScriptArg_t arg) {
 	_escOn = false;
 }
 
-OPCODE_STUB(EscGet)
-OPCODE_STUB(BackStart)
+void Runtime::scriptOpEscGet(ScriptArg_t arg) {
+	_scriptStack.push_back(StackValue(_scriptEnv.esc ? 1 : 0));
+	_scriptEnv.esc = false;
+}
+
+void Runtime::scriptOpBackStart(ScriptArg_t arg) {
+	_scriptEnv.exitToMenu = true;
+}
 
 void Runtime::scriptOpAnimName(ScriptArg_t arg) {
 	if (_roomNumber >= _roomDefs.size())
diff --git a/engines/vcruise/runtime.h b/engines/vcruise/runtime.h
index 1b9a6e2882c..8aed91311ee 100644
--- a/engines/vcruise/runtime.h
+++ b/engines/vcruise/runtime.h
@@ -24,6 +24,7 @@
 
 #include "common/hashmap.h"
 #include "common/keyboard.h"
+#include "common/rect.h"
 
 #include "vcruise/detection.h"
 
@@ -67,6 +68,9 @@ static const uint kNumHighPrecisionDirections = 256;
 static const uint kHighPrecisionDirectionMultiplier = kNumHighPrecisionDirections / kNumDirections;
 
 class AudioPlayer;
+class MenuInterface;
+class MenuPage;
+class RuntimeMenuInterface;
 class TextParser;
 struct ScriptSet;
 struct Script;
@@ -86,6 +90,8 @@ enum GameState {
 
 	kGameStatePanLeft,
 	kGameStatePanRight,
+
+	kGameStateMenu,
 };
 
 struct AnimationDef {
@@ -135,11 +141,13 @@ struct MapDef {
 struct ScriptEnvironmentVars {
 	ScriptEnvironmentVars();
 
-	bool lmb;
-	bool lmbDrag;
 	uint panInteractionID;
 	uint fpsOverride;
 	uint lastHighlightedItem;
+	bool lmb;
+	bool lmbDrag;
+	bool esc;
+	bool exitToMenu;
 };
 
 struct SfxSound {
@@ -399,12 +407,33 @@ struct SaveGameSnapshot {
 	Common::HashMap<uint, uint32> timers;
 };
 
+enum OSEventType {
+	kOSEventTypeInvalid,
+
+	kOSEventTypeMouseMove,
+	kOSEventTypeLButtonDown,
+	kOSEventTypeLButtonUp,
+
+	kOSEventTypeKeyDown,
+};
+
+struct OSEvent {
+	OSEvent();
+
+	OSEventType type;
+	Common::Point pos;
+	Common::KeyCode keyCode;
+	uint32 timestamp;
+};
+
 class Runtime {
 public:
+	friend class RuntimeMenuInterface;
+
 	Runtime(OSystem *system, Audio::Mixer *mixer, const Common::FSNode &rootFSNode, VCruiseGameID gameID);
 	virtual ~Runtime();
 
-	void initSections(Common::Rect gameRect, Common::Rect menuRect, Common::Rect trayRect, const Graphics::PixelFormat &pixFmt);
+	void initSections(const Common::Rect &gameRect, const Common::Rect &menuRect, const Common::Rect &trayRect, const Common::Rect &fullscreenMenuRect, const Graphics::PixelFormat &pixFmt);
 
 	void loadCursors(const char *exeName);
 	void setDebugMode(bool debugMode);
@@ -505,16 +534,6 @@ private:
 		bool isWaitingForAnimation;
 	};
 
-	enum OSEventType {
-		kOSEventTypeInvalid,
-
-		kOSEventTypeMouseMove,
-		kOSEventTypeLButtonDown,
-		kOSEventTypeLButtonUp,
-
-		kOSEventTypeKeyDown,
-	};
-
 	enum PanoramaCursorFlags {
 		kPanCursorDraggableHoriz	= (1 << 0),
 		kPanCursorDraggableUp		= (1 << 1),
@@ -552,15 +571,6 @@ private:
 
 	static const uint kNumInventorySlots = 6;
 
-	struct OSEvent {
-		OSEvent();
-
-		OSEventType type;
-		Common::Point pos;
-		Common::KeyCode keyCode;
-		uint32 timestamp;
-	};
-
 	typedef int32 ScriptArg_t;
 	typedef int32 StackInt_t;
 
@@ -688,6 +698,7 @@ private:
 
 	void inventoryAddItem(uint item);
 	void inventoryRemoveItem(uint item);
+	void clearScreen();
 	void redrawTray();
 	void clearTray();
 	void drawInventory(uint slot);
@@ -699,6 +710,8 @@ private:
 
 	void loadSubtitles(Common::CodePage codePage);
 
+	void changeToMenuPage(MenuPage *menuPage);
+
 	// Script things
 	void scriptOpNumber(ScriptArg_t arg);
 	void scriptOpRotate(ScriptArg_t arg);
@@ -830,6 +843,8 @@ private:
 	Common::SharedPtr<Graphics::Surface> _trayHighlightGraphic;
 	Common::SharedPtr<Graphics::Surface> _trayCornerGraphic;
 
+	Common::Array<Common::SharedPtr<Graphics::Surface> > _uiGraphics;
+
 	uint _panCursors[kPanCursorMaxCount];
 
 	Common::HashMap<Common::String, StackInt_t> _namedCursors;
@@ -872,10 +887,14 @@ private:
 	bool _havePendingCompletionCheck;
 	GameState _gameState;
 
+	Common::SharedPtr<MenuPage> _menuPage;
+	Common::SharedPtr<MenuInterface> _menuInterface;
+
 	bool _havePendingPlayAmbientSounds;
 	uint32 _ambientSoundFinishTime;
 
 	bool _escOn;
+	bool _escUsed;
 	bool _debugMode;
 	bool _fastAnimationMode;
 
@@ -915,6 +934,7 @@ private:
 	uint32 _animStartTime;
 	uint32 _animFramesDecoded;
 	uint _loadedAnimation;
+	bool _loadedAnimationHasSound;
 	bool _animPlayWhileIdle;
 
 	Common::Array<FrameData> _frameData;
@@ -937,6 +957,7 @@ private:
 	RenderSection _gameDebugBackBuffer;
 	RenderSection _menuSection;
 	RenderSection _traySection;
+	RenderSection _fullscreenMenuSection;
 
 	Common::Point _mousePos;
 	Common::Point _lmbDownPos;
@@ -983,6 +1004,7 @@ private:
 	uint _soundCacheIndex;
 
 	Common::SharedPtr<SaveGameSnapshot> _saveGame;
+	bool _isInGame;
 
 	const Graphics::Font *_subtitleFont;
 	Common::SharedPtr<Graphics::Font> _subtitleFontKeepalive;
diff --git a/engines/vcruise/vcruise.cpp b/engines/vcruise/vcruise.cpp
index 4964e1ad39d..cb51405fa3b 100644
--- a/engines/vcruise/vcruise.cpp
+++ b/engines/vcruise/vcruise.cpp
@@ -24,6 +24,7 @@
 #include "common/config-manager.h"
 #include "common/events.h"
 #include "common/stream.h"
+#include "common/savefile.h"
 #include "common/system.h"
 #include "common/algorithm.h"
 #include "common/translation.h"
@@ -159,7 +160,7 @@ Common::Error VCruiseEngine::run() {
 	_system->fillScreen(0);
 
 	_runtime.reset(new Runtime(_system, _mixer, _rootFSNode, _gameDescription->gameID));
-	_runtime->initSections(_videoRect, _menuBarRect, _trayRect, _system->getScreenFormat());
+	_runtime->initSections(_videoRect, _menuBarRect, _trayRect, Common::Rect(640, 480), _system->getScreenFormat());
 
 	const char *exeName = _gameDescription->desc.filesDescriptions[0].fileName;
 
@@ -283,4 +284,12 @@ void VCruiseEngine::initializePath(const Common::FSNode &gamePath) {
 	_rootFSNode = gamePath;
 }
 
+bool VCruiseEngine::hasDefaultSave() {
+	const Common::String &autoSaveName = getSaveStateName(getMetaEngine()->getAutosaveSlot());
+	bool autoSaveExists = getSaveFileManager()->exists(autoSaveName);
+
+	return autoSaveExists;
+}
+
+
 } // End of namespace VCruise
diff --git a/engines/vcruise/vcruise.h b/engines/vcruise/vcruise.h
index 44ccb05b9ad..53b60919c01 100644
--- a/engines/vcruise/vcruise.h
+++ b/engines/vcruise/vcruise.h
@@ -62,6 +62,8 @@ public:
 
 	void initializePath(const Common::FSNode &gamePath) override;
 
+	bool hasDefaultSave();
+
 protected:
 	void pauseEngineIntern(bool pause) override;
 




More information about the Scummvm-git-logs mailing list