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

sev- noreply at scummvm.org
Sat Aug 9 16:06:51 UTC 2025


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

Summary:
cdc2b1152b MADE: Add text-to-speech (TTS)


Commit: cdc2b1152b35532344db6a045d81fc036389988e
    https://github.com/scummvm/scummvm/commit/cdc2b1152b35532344db6a045d81fc036389988e
Author: ellm135 (ellm13531 at gmail.com)
Date: 2025-08-09T18:06:47+02:00

Commit Message:
MADE: Add text-to-speech (TTS)

Changed paths:
    engines/made/detection.cpp
    engines/made/detection.h
    engines/made/made.cpp
    engines/made/made.h
    engines/made/metaengine.cpp
    engines/made/pmvplayer.cpp
    engines/made/screen.cpp
    engines/made/screen.h
    engines/made/scriptfuncs.cpp
    engines/made/scriptfuncs.h


diff --git a/engines/made/detection.cpp b/engines/made/detection.cpp
index feff2a7a29c..8652a86f294 100644
--- a/engines/made/detection.cpp
+++ b/engines/made/detection.cpp
@@ -39,6 +39,7 @@ static const PlainGameDescriptor madeGames[] = {
 class MadeMetaEngineDetection : public AdvancedMetaEngineDetection<Made::MadeGameDescription> {
 public:
 	MadeMetaEngineDetection() : AdvancedMetaEngineDetection(Made::gameDescriptions, madeGames) {
+		_guiOptions = GUIO1(GAMEOPTION_TTS);
 	}
 
 	const char *getName() const override {
diff --git a/engines/made/detection.h b/engines/made/detection.h
index 9ccedfff1d0..22b6380dc5e 100644
--- a/engines/made/detection.h
+++ b/engines/made/detection.h
@@ -52,6 +52,7 @@ struct MadeGameDescription {
 };
 
 #define GAMEOPTION_INTRO_MUSIC_DIGITAL GUIO_GAMEOPTIONS1
+#define GAMEOPTION_TTS                 GUIO_GAMEOPTIONS2
 
 } // End of namespace Made
 
diff --git a/engines/made/made.cpp b/engines/made/made.cpp
index fcb0a6a4a25..9702ec42760 100644
--- a/engines/made/made.cpp
+++ b/engines/made/made.cpp
@@ -77,6 +77,22 @@ MadeEngine::MadeEngine(OSystem *syst, const MadeGameDescription *gameDesc) : Eng
 
 	_soundRate = 0;
 
+	_saveLoadScreenOpen = false;
+	_openingCreditsOpen = true;
+	_tapeRecorderOpen = false;
+	_previousRect = -1;
+	_previousTextBox = -1;
+	_voiceText = true;
+	_forceVoiceText = false;
+	_forceQueueText = false;
+
+#ifdef USE_TTS
+	_rtzSaveLoadIndex = ARRAYSIZE(_rtzSaveLoadButtonText);
+	_rtzFirstSaveSlot = 0;
+	_tapeRecorderIndex = 0;
+	_playOMaticButtonIndex = ARRAYSIZE(_playOMaticButtonText);
+#endif
+
 	// Set default sound frequency
 	switch (getGameID()) {
 	case GID_RODNEY:
@@ -120,9 +136,14 @@ int16 MadeEngine::getTicks() {
 }
 
 int16 MadeEngine::getTimer(int16 timerNum) {
-	if (timerNum > 0 && timerNum <= ARRAYSIZE(_timers) && _timers[timerNum - 1] != -1)
+	if (timerNum > 0 && timerNum <= ARRAYSIZE(_timers) && _timers[timerNum - 1] != -1) {
+		Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+		if (getGameID() == GID_LGOP2 && ttsMan && ttsMan->isSpeaking()) {
+			return 1;
+		}
+
 		return (getTicks() - _timers[timerNum - 1]);
-	else
+	} else
 		return 32000;
 }
 
@@ -156,6 +177,104 @@ void MadeEngine::resetAllTimers() {
 		_timers[i] = -1;
 }
 
+void MadeEngine::sayText(const Common::String &text, Common::TextToSpeechManager::Action action) const {
+	if (text.empty()) {
+		return;
+	}
+
+	Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+	if (ttsMan != nullptr && ConfMan.getBool("tts_enabled")) {
+		ttsMan->say(text, action, _ttsTextEncoding);
+	}
+}
+
+void MadeEngine::stopTextToSpeech() const {
+	Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+	if (ttsMan != nullptr && ConfMan.getBool("tts_enabled") && ttsMan->isSpeaking()) {
+		ttsMan->stop();
+	}
+}
+
+#ifdef USE_TTS
+
+void MadeEngine::checkHoveringSaveLoadScreen() {
+	static const Common::Rect rtzSaveLoadScreenButtons[] = {
+		Common::Rect(184, 174, 241, 189),	// Cancel button
+		Common::Rect(109, 174, 166, 189),	// Save/load button
+		Common::Rect(25, 20, 297, 158)		// Text entry box
+	};
+
+	static const uint8 kRtzSaveLoadButtonCount = ARRAYSIZE(rtzSaveLoadScreenButtons);
+	static const uint8 kRtzSaveBoxHeight = 14;
+
+	enum RtzSaveLoadScreenIndex {
+		kCancel = 0,
+		kSaveOrLoad = 1,
+		kTextBox = 2
+	};
+
+	if (_saveLoadScreenOpen && getGameID() == GID_RTZ) {
+		bool hoveringOverButton = false;
+		for (uint8 i = 0; i < kRtzSaveLoadButtonCount; ++i) {
+			if (rtzSaveLoadScreenButtons[i].contains(_eventMouseX, _eventMouseY)) {
+				if (_previousRect != i) {
+					if (i == kTextBox) {
+						int index = MIN((_eventMouseY - 20) / kRtzSaveBoxHeight, 9);
+
+						if (index != _previousTextBox) {
+							sayText(Common::String::format("%d", _rtzFirstSaveSlot + index));
+							_previousTextBox = index;
+						}
+					} else {
+						sayText(_rtzSaveLoadButtonText[i]);
+						_previousRect = i;
+					}
+				}
+
+				hoveringOverButton = true;
+				break;
+			}
+		}
+
+		if (!hoveringOverButton) {
+			_previousRect = -1;
+			_previousTextBox = -1;
+		}
+	}
+}
+
+void MadeEngine::checkHoveringPlayOMatic(int16 spriteY) {
+	static const Common::Rect lgop2PlayOMaticButtons[] = {
+		Common::Rect(105, 102, 225, 122),
+		Common::Rect(105, 127, 225, 147),
+		Common::Rect(105, 152, 225, 172),
+		Common::Rect(105, 177, 225, 197)
+	};
+
+	static const uint8 kLgop2PlayOMaticButtonCount = ARRAYSIZE(lgop2PlayOMaticButtons);
+
+	if (_saveLoadScreenOpen && getGameID() == GID_LGOP2) {
+		bool hoveringOverButton = false;
+		for (uint8 i = 0; i < kLgop2PlayOMaticButtonCount; ++i) {
+			if (lgop2PlayOMaticButtons[i].contains(_eventMouseX, _eventMouseY) || spriteY == lgop2PlayOMaticButtons[i].top) {
+				if (_previousRect != i || spriteY != -1) {
+					sayText(_playOMaticButtonText[i], Common::TextToSpeechManager::INTERRUPT);
+					_previousRect = i;
+				}
+
+				hoveringOverButton = true;
+				break;
+			}
+		}
+
+		if (!hoveringOverButton) {
+			_previousRect = -1;
+		}
+	}
+}
+
+#endif
+
 Common::String MadeEngine::getSavegameFilename(int16 saveNum) {
 	return Common::String::format("%s.%03d", getTargetName().c_str(), saveNum);
 }
@@ -173,10 +292,22 @@ void MadeEngine::handleEvents() {
 		case Common::EVENT_MOUSEMOVE:
 			_eventMouseX = event.mouse.x;
 			_eventMouseY = event.mouse.y;
+
+#ifdef USE_TTS
+			checkHoveringSaveLoadScreen();
+			checkHoveringPlayOMatic();
+#endif
+
 			break;
 
 		case Common::EVENT_LBUTTONDOWN:
 			_eventNum = 2;
+
+			if (_openingCreditsOpen) {
+				_openingCreditsOpen = false;
+				stopTextToSpeech();
+			}
+
 			break;
 
 		case Common::EVENT_LBUTTONUP:
@@ -261,6 +392,23 @@ Common::Error MadeEngine::run() {
 
 	resetAllTimers();
 
+	Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+	if (ttsMan != nullptr) {
+		ttsMan->enable(ConfMan.getBool("tts_enabled"));
+
+		if (getLanguage() == Common::KO_KOR) {	// Korean version doesn't translate any text
+			ttsMan->setLanguage("en");
+		} else {
+			ttsMan->setLanguage(ConfMan.get("language"));
+		}
+
+		if (getLanguage() == Common::JA_JPN) {
+			_ttsTextEncoding = Common::CodePage::kWindows932;
+		} else {
+			_ttsTextEncoding = Common::CodePage::kDos850;
+		}
+	}
+
 	if (getGameID() == GID_RTZ) {
 		if (getFeatures() & GF_DEMO) {
 			_dat->open("demo.dat");
diff --git a/engines/made/made.h b/engines/made/made.h
index 9f7d05a7110..19e9dfd31e1 100644
--- a/engines/made/made.h
+++ b/engines/made/made.h
@@ -28,6 +28,7 @@
 #include "engines/engine.h"
 
 #include "common/random.h"
+#include "common/text-to-speech.h"
 
 /**
  * This is the namespace of the Made engine.
@@ -84,6 +85,7 @@ public:
 	uint32 getFeatures() const;
 	uint16 getVersion() const;
 	Common::Platform getPlatform() const;
+	Common::Language getLanguage() const;
 
 public:
 	PmvPlayer *_pmvPlayer;
@@ -106,6 +108,29 @@ public:
 	uint32 _cdTimeStart;
 	bool _introMusicDigital;
 
+	Common::CodePage _ttsTextEncoding;
+	int _previousRect;
+	int _previousTextBox;
+	bool _saveLoadScreenOpen;
+	bool _openingCreditsOpen;
+	bool _tapeRecorderOpen;
+
+	bool _voiceText;
+	bool _forceVoiceText;
+	bool _forceQueueText;
+
+#ifdef USE_TTS
+	Common::String _rtzSaveLoadButtonText[2];
+	uint8 _rtzFirstSaveSlot;
+	uint8 _rtzSaveLoadIndex;
+
+	Common::String _tapeRecorderText[4];
+	uint8 _tapeRecorderIndex;
+
+	Common::String _playOMaticButtonText[4];
+	uint8 _playOMaticButtonIndex;
+#endif
+
 	int32 _timers[50];
 	int16 getTicks();
 	int16 getTimer(int16 timerNum);
@@ -115,6 +140,13 @@ public:
 	void freeTimer(int16 timerNum);
 	void resetAllTimers();
 
+	void sayText(const Common::String &text, Common::TextToSpeechManager::Action action = Common::TextToSpeechManager::INTERRUPT) const;
+	void stopTextToSpeech() const;
+#ifdef USE_TTS
+	void checkHoveringSaveLoadScreen();
+	void checkHoveringPlayOMatic(int16 spriteY = -1);
+#endif
+
 	const Common::String getTargetName() { return _targetName; }
 	Common::String getSavegameFilename(int16 saveNum);
 
diff --git a/engines/made/metaengine.cpp b/engines/made/metaengine.cpp
index 91b3649fe94..2cc36cffe7c 100644
--- a/engines/made/metaengine.cpp
+++ b/engines/made/metaengine.cpp
@@ -43,6 +43,21 @@ static const ADExtraGuiOptionsMap optionsList[] = {
 			0
 		}
 	},
+
+#ifdef USE_TTS
+	{
+		GAMEOPTION_TTS,
+		{
+			_s("Enable Text to Speech"),
+			_s("Use TTS to read text in the game (if TTS is available)"),
+			"tts_enabled",
+			false,
+			0,
+			0
+		}
+	},
+#endif
+
 	AD_EXTRA_GUI_OPTIONS_TERMINATOR
 };
 
@@ -62,6 +77,10 @@ uint16 MadeEngine::getVersion() const {
 	return _gameDescription->version;
 }
 
+Common::Language MadeEngine::getLanguage() const {
+	return _gameDescription->desc.language;
+}
+
 } // End of namespace Made
 
 class MadeMetaEngine : public AdvancedMetaEngine<Made::MadeGameDescription> {
diff --git a/engines/made/pmvplayer.cpp b/engines/made/pmvplayer.cpp
index 8c2df986434..bd3efba5bd6 100644
--- a/engines/made/pmvplayer.cpp
+++ b/engines/made/pmvplayer.cpp
@@ -36,6 +36,82 @@
 
 namespace Made {
 
+#ifdef USE_TTS
+
+// English seems to be the only language that doesn't have voice clips for these lines
+static const char *introOpeningLines[] = {
+	"You are standing by a white house",
+	"Behind House\nYou are standing behind the white house. In one corner is a small window which is slightly ajar.",
+	"Go southwest then go northwest",
+	"West of House\nYou are standing in a field west of a white house with a boarded front door. There is a small mailbox here.",
+	"Open mailbox"
+};
+
+static const char *openingCreditsEnglish[] = {
+	"Design: Doug Barnett",
+	"Art Direction: Joe Asperin",
+	"Technical Direction: William Volk",
+	"Screenplay: Michele Em",
+	"Music: Nathan Wang and Teri Mason",
+	"Producer: Eddie Dombrower"
+};
+
+static const char *openingCreditsGerman[] = {
+	"Entwurf: Doug Barnett",
+	"K\201nstlerischer Leitung: Joe Asperin",
+	"Technische Leitung: William Volk",
+	"Drehbuch: Michele Em",
+	"Musik: Nathan Wang und Teri Mason",
+	"Produzent: Eddie Dombrower"
+};
+
+static const char *openingCreditsItalian[] = {
+	"Disegno: Doug Barnett",
+	"Direzione Artistica: Joe Asperin",
+	"Direzione Tecnica: William Volk",
+	"Sceneggiatura: Michele Em",
+	"Musica: Nathan Wang e Teri Mason",
+	"Produttore: Eddie Dombrower"
+};
+
+static const char *openingCreditsFrench[] = {
+	"Conception: Doug Barnett",
+	"Direction Artistique: Joe Asperin",
+	"Direction Technique: William Volk",
+	"Sc\202nario: Michele Em",
+	"Musique: Nathan Wang et Teri Mason",
+	"Producteur: Eddie Dombrower"
+};
+
+static const char *openingCreditsJapanese[] = {
+	"\x83\x66\x83\x55\x83\x43\x83\x93\x81\x45\x83\x5f\x83\x4f\x81\x45\x83\x6f\x81\x5b\x83\x6c\x83\x62\x83\x67",	// デザイン・ダグ・バーネット
+	"\x83\x41\x81\x5b\x83\x67\x83\x66\x83\x42\x83\x8c\x83\x4e\x83\x56\x83\x87\x83\x93:"
+	"\x83\x57\x83\x87\x81\x5b\x81\x45\x83\x41\x83\x58\x83\x79\x83\x8a\x83\x93",	// アートディレクション:ジョー・アスペリン
+	"\x83\x65\x83\x4e\x83\x6a\x83\x4a\x83\x8b\x83\x66\x83\x42\x83\x8c\x83\x4e\x83\x56\x83\x87\x83\x93:"
+	"\x83\x45\x83\x42\x83\x8a\x83\x41\x83\x80\x81\x45\x83\x94\x83\x48\x83\x8b\x83\x4e",	// テクニカルディレクション:ウィリアム・ヴォルク
+	"\x8b\x72\x96\x7b:\x83\x7e\x83\x56\x83\x46\x83\x8b\x81\x45\x83\x47\x83\x80",	// 脚本:ミシェル・エム
+	"\x89\xb9\x8a\x79:\x83\x6c\x83\x43\x83\x54\x83\x93\x81\x45\x83\x8f\x83\x93\x83\x67\x83\x65\x83\x8a\x81\x5b"
+	"\x81\x45\x83\x81\x83\x43\x83\x5c\x83\x93",	// 音楽:ネイサン・ワンとテリー・メイソン
+	"\x83\x76\x83\x8d\x83\x66\x83\x85\x81\x5b\x83\x54\x81\x5b:\x83\x47\x83\x66\x83\x42\x81\x45\x83\x68\x83\x93"
+	"\x83\x75\x83\x8d\x83\x8f\x81\x5b"	// プロデューサー: エディ・ドンブロワー
+};
+
+enum IntroTextFrame {
+	kStandingByHouse = 20,
+	kBehindHouse = 53,
+	kGoSouthwest = 170,
+	kWestOfHouse = 312,
+	kOpenMailbox = 430,
+	kDesign = 716,
+	kArtDirection = 773,
+	kTechnicalDirection = 833,
+	kScreenplay = 892,
+	kMusic = 948,
+	kProducer = 1004
+};
+
+#endif
+
 PmvPlayer::PmvPlayer(MadeEngine *vm, Audio::Mixer *mixer) : _fd(nullptr), _vm(vm), _mixer(mixer) {
 	_audioStream = nullptr;
 	_surface = nullptr;
@@ -119,6 +195,11 @@ bool PmvPlayer::play(const char *filename) {
 
 	SoundDecoderData *soundDecoderData = new SoundDecoderData();
 
+	// First cutscene after the opening credits finish
+	if (strcmp(filename, "FWIZ01X1.PMV") == 0) {
+		_vm->_openingCreditsOpen = false;
+	}
+
 	while (!_vm->shouldQuit() && !_aborted && !_fd->eos() && frameNumber < frameCount) {
 
 		int32 frameTime = _vm->getTotalPlayTime();
@@ -210,6 +291,70 @@ bool PmvPlayer::play(const char *filename) {
 		} else
 			skipFrames--;
 
+#ifdef USE_TTS
+		if (strcmp(filename, "fintro00.pmv") == 0 || strcmp(filename, "fintro01.pmv") == 0) {
+			const char **texts;
+
+			switch (_vm->getLanguage()) {
+			case Common::EN_ANY:
+				if (frameNumber < kDesign) {
+					texts = introOpeningLines;
+				} else {
+					texts = openingCreditsEnglish;
+				}
+				break;
+			case Common::DE_DEU:
+				texts = openingCreditsGerman;
+				break;
+			case Common::IT_ITA:
+				texts = openingCreditsItalian;
+				break;
+			case Common::FR_FRA:
+				texts = openingCreditsFrench;
+				break;
+			case Common::JA_JPN:
+				texts = openingCreditsJapanese;
+				break;
+			case Common::KO_KOR:
+				texts = openingCreditsEnglish;
+				break;
+			default:
+				texts = openingCreditsEnglish;
+			}
+
+			int index = -1;
+
+			switch (frameNumber) {
+			case kStandingByHouse:
+			case kDesign:
+				index = 0;
+				break;
+			case kBehindHouse:
+			case kArtDirection:
+				index = 1;
+				break;
+			case kGoSouthwest:
+			case kTechnicalDirection:
+				index = 2;
+				break;
+			case kWestOfHouse:
+			case kScreenplay:
+				index = 3;
+				break;
+			case kOpenMailbox:
+			case kMusic:
+				index = 4;
+				break;
+			case kProducer:
+				index = 5;
+			}
+
+			if (index != -1 && (_vm->getLanguage() == Common::EN_ANY || frameNumber >= kDesign)) {
+				_vm->sayText(texts[index], Common::TextToSpeechManager::QUEUE);
+			}
+		}
+#endif
+
 		frameNumber++;
 
 	}
@@ -248,8 +393,10 @@ void PmvPlayer::handleEvents() {
 	while (_vm->_system->getEventManager()->pollEvent(event)) {
 		switch (event.type) {
 		case Common::EVENT_KEYDOWN:
-			if (event.kbd.keycode == Common::KEYCODE_ESCAPE)
+			if (event.kbd.keycode == Common::KEYCODE_ESCAPE) {
 				_aborted = true;
+				_vm->stopTextToSpeech();
+			}
 			break;
 		default:
 			break;
diff --git a/engines/made/screen.cpp b/engines/made/screen.cpp
index 331542877a9..bf18959a09d 100644
--- a/engines/made/screen.cpp
+++ b/engines/made/screen.cpp
@@ -32,6 +32,23 @@
 
 namespace Made {
 
+enum TextChannelIndex {
+	kTapeRecorderName = 84,
+	kTapeRecorderTrack = 85,
+	kTapeRecorderMaxTrack = 86,
+	kTapeRecorderTime = 87,
+	kTapeRecorderScan = 88,
+	kHoverOver = 89,
+	kClickMessage = 97
+};
+
+enum TapeRecorderIndex {
+	kTime = 0,
+	kMaxTrack = 1,
+	kTrack = 2,
+	kName = 3
+};
+
 Screen::Screen(MadeEngine *vm) : _vm(vm) {
 
 	_palette = new byte[768];
@@ -93,6 +110,9 @@ Screen::Screen(MadeEngine *vm) : _vm(vm) {
 	_outlineColor = 0;
 	_dropShadowColor = 0;
 
+	_queueNextText = false;
+	_voiceTimeText = false;
+
 	clearChannels();
 }
 
@@ -232,6 +252,11 @@ uint16 Screen::updateChannel(uint16 channelIndex) {
 void Screen::deleteChannel(uint16 channelIndex) {
 	if (channelIndex < 1 || channelIndex >= 100)
 		return;
+
+	if (_channels[channelIndex - 1].type == 2) {
+		_channels[channelIndex - 1].previousText.clear();
+	}
+
 	_channels[channelIndex - 1].type = 0;
 	_channels[channelIndex - 1].state = 0;
 	_channels[channelIndex - 1].index = 0;
@@ -252,6 +277,12 @@ int16 Screen::getChannelState(uint16 channelIndex) {
 void Screen::setChannelState(uint16 channelIndex, int16 state) {
 	if (channelIndex < 1 || channelIndex >= 100 || _channels[channelIndex - 1].type == 0)
 		return;
+
+	if (state != _channels[channelIndex - 1].state && _channels[channelIndex - 1].type == 2) {
+		_channels[channelIndex - 1].previousText.clear();
+		_queueNextText = true;
+	} 
+
 	_channels[channelIndex - 1].state = state;
 }
 
@@ -406,6 +437,39 @@ void Screen::drawAnimFrame(uint16 animIndex, int16 x, int16 y, int16 frameNum, i
 
 uint16 Screen::drawPic(uint16 index, int16 x, int16 y, int16 flipX, int16 flipY) {
 	drawFlex(index, x, y, flipX, flipY, 0, _backgroundScreenDrawCtx);
+
+#ifdef USE_TTS
+	if (_vm->getGameID() == GID_RTZ && index > 0) {
+		if (index == 843) {	// Save/load screen
+			_vm->_saveLoadScreenOpen = true;
+			_vm->_rtzSaveLoadIndex = 0;
+			_vm->_rtzFirstSaveSlot = 0;
+		} else {
+			_vm->_saveLoadScreenOpen = false;
+		}
+
+		if (index == 1501) {	// Tape recorder
+			_vm->_tapeRecorderOpen = true;
+		} else {
+			_vm->_tapeRecorderOpen = false;
+		}
+	} else if (_vm->getGameID() == GID_LGOP2) {
+		if (index == 465) {	// Save/load screen (Play-O-Matic)
+			_vm->_playOMaticButtonIndex = 0;
+			_vm->_saveLoadScreenOpen = true;
+		} else if (index == 196) {	// Play-O-Matic button highlights
+			_vm->checkHoveringPlayOMatic(y);
+		} else if (index != 463 && index != 0) {
+			// 1216 is drawn before "best interactive fiction" line, 757 before "choose your character", and 761 before the
+			// second copyright message, all of which need voicing
+			if (index == 1216 || index == 757 || index == 761) {
+				_vm->_forceVoiceText = true;
+			}
+			_vm->_saveLoadScreenOpen = false;
+		}
+	}
+#endif
+
 	return 0;
 }
 
@@ -576,6 +640,14 @@ uint16 Screen::placeText(uint16 channelIndex, uint16 textObjectIndex, int16 x, i
 	if (_ground == 0)
 		state |= 2;
 
+	// The channel for this message isn't deleted until the text on screen disappears, but it gets refreshed
+	// if the player clicks again, so the previous text needs to be manually reset here to allow the message to be voiced
+	// whenever the player clicks
+	if (channelIndex == kClickMessage && (_channels[channelIndex].x != x || _channels[channelIndex].y != y)) {
+		_channels[channelIndex].previousText.clear();
+		_queueNextText = true;
+	}
+
 	_channels[channelIndex].state = state;
 	_channels[channelIndex].type = 2;
 	_channels[channelIndex].index = textObjectIndex;
@@ -585,12 +657,96 @@ uint16 Screen::placeText(uint16 channelIndex, uint16 textObjectIndex, int16 x, i
 	_channels[channelIndex].fontNum = fontNum;
 	_channels[channelIndex].outlineColor = outlineColor;
 
+#ifdef USE_TTS
+	voiceChannelText(text, channelIndex);
+#endif
+
 	if (_channelsUsedCount <= channelIndex)
 		_channelsUsedCount = channelIndex + 1;
 
 	return channelIndex + 1;
 }
 
+#ifdef USE_TTS
+
+void Screen::voiceChannelText(const char *text, uint16 channelIndex) {
+	if ((channelIndex != kTapeRecorderTime && strcmp(_channels[channelIndex].previousText.c_str(), text)) || 
+			(channelIndex == kTapeRecorderTime && _voiceTimeText)) {
+		size_t len = strlen(text);
+		_channels[channelIndex].previousText = text;
+
+		if (len == 0) {
+			return;
+		}
+		
+		if (channelIndex == kHoverOver && _queueNextText) {
+			_vm->sayText(text, Common::TextToSpeechManager::QUEUE);
+			_queueNextText = false;
+		} else {
+			bool voiceText = true;
+
+			Object *object = nullptr;
+			const char *message = nullptr;
+			switch (channelIndex) {
+			case kTapeRecorderName:
+				// Voice name, track, and max track all at once, so that they're all properly voiced
+				// when the player switches between entries on the tape recorder (the track and max track numbers
+				// aren't necessarily unique, and may not be voiced otherwise)
+				_vm->sayText(Common::String::format("%s: %s", _vm->_tapeRecorderText[kName].c_str(), text));
+
+				// Track
+				object = _vm->_dat->getObject(_channels[kTapeRecorderTrack].index);
+				if (object) {
+					message = object->getString();
+				}
+				_channels[kTapeRecorderTrack].previousText = message;
+				_vm->sayText(Common::String::format("%s: %s", _vm->_tapeRecorderText[kTrack].c_str(),
+								_channels[kTapeRecorderTrack].previousText.c_str()), 
+								Common::TextToSpeechManager::QUEUE);
+
+				// Max track
+				object = _vm->_dat->getObject(_channels[kTapeRecorderMaxTrack].index);
+				if (object) {
+					message = object->getString();
+				}
+				_vm->sayText(Common::String::format("%s: %s", _vm->_tapeRecorderText[kMaxTrack].c_str(), message), 
+								Common::TextToSpeechManager::QUEUE);
+
+				voiceText = false;
+				break;
+			case kTapeRecorderTrack:
+				if (!_channels[kTapeRecorderName].previousText.empty()) {
+					// Voice here in case the track is changed while the tape recorder is open
+					_vm->sayText(Common::String::format("%s: %s", _vm->_tapeRecorderText[kTrack].c_str(), text), 
+								Common::TextToSpeechManager::QUEUE);
+				}
+				// fall through
+			case kTapeRecorderMaxTrack:
+				// Max track shouldn't change unless the player changes entries, in which case it'll be
+				// voiced under the kTapeRecorderName condition, so no need to voice it here
+				voiceText = false;
+				break;
+			case kTapeRecorderTime:
+				_voiceTimeText = false;
+				_vm->sayText(Common::String::format("%s: %s", _vm->_tapeRecorderText[kTime].c_str(), text), Common::TextToSpeechManager::QUEUE);
+				voiceText = false;
+			}
+
+			if (voiceText) {
+				if (_vm->_saveLoadScreenOpen) {
+					Common::String ttsText(text);
+					ttsText.replace('_', ' ');
+					_vm->sayText(ttsText, Common::TextToSpeechManager::QUEUE);
+				} else {
+					_vm->sayText(text);
+				}
+			}
+		}
+	}
+}
+
+#endif
+
 void Screen::show() {
 
 	if (_screenLock)
@@ -751,6 +907,9 @@ void Screen::printTextEx(const char *text, int16 x, int16 y, int16 fontNum, int1
 	setFont(oldFontNum);
 	_fontDrawCtx = oldFontDrawCtx;
 
+	if (_vm->getGameID() != GID_RTZ && _vm->getGameID() != GID_LGOP2) {
+		_vm->sayText(text);
+	}
 }
 
 void Screen::printObjectText(int16 objectIndex, int16 x, int16 y, int16 fontNum, int16 textColor, int16 outlineColor, const ClipInfo &clipInfo) {
diff --git a/engines/made/screen.h b/engines/made/screen.h
index b9d0a8d911d..62c9c58dfe1 100644
--- a/engines/made/screen.h
+++ b/engines/made/screen.h
@@ -37,6 +37,7 @@ struct SpriteChannel {
 	int16 textColor, outlineColor;
 	int16 frameNum;
 	int16 mask;
+	Common::String previousText;
 };
 
 struct ClipInfo {
@@ -129,6 +130,9 @@ public:
 		_textY = _textRect.top;
 	}
 
+	void setQueueNextText(bool value) { _queueNextText = value; }
+	void setVoiceTimeText(bool value) { _voiceTimeText = value; }
+
 	uint16 updateChannel(uint16 channelIndex);
 	void deleteChannel(uint16 channelIndex);
 	int16 getChannelType(uint16 channelIndex);
@@ -160,6 +164,9 @@ public:
 	int16 getAnimFrame(uint16 channelIndex);
 
 	uint16 placeText(uint16 channelIndex, uint16 textObjectIndex, int16 x, int16 y, uint16 fontNum, int16 textColor, int16 outlineColor);
+#ifdef USE_TTS
+	void voiceChannelText(const char *text, uint16 channelIndex);
+#endif
 
 	void show();
 	void flash(int count);
@@ -216,6 +223,9 @@ protected:
 	uint16 _channelsUsedCount;
 	SpriteChannel _channels[100];
 
+	bool _queueNextText;
+	bool _voiceTimeText;
+
 	Common::Array<SpriteListItem> _spriteList;
 
 };
diff --git a/engines/made/scriptfuncs.cpp b/engines/made/scriptfuncs.cpp
index 981e6bdf9d2..572e21772bb 100644
--- a/engines/made/scriptfuncs.cpp
+++ b/engines/made/scriptfuncs.cpp
@@ -30,6 +30,8 @@
 
 #include "backends/audiocd/audiocd.h"
 
+#include "common/config-manager.h"
+
 #include "graphics/cursorman.h"
 #include "graphics/surface.h"
 
@@ -42,6 +44,7 @@ ScriptFunctions::ScriptFunctions(MadeEngine *vm) : _vm(vm), _soundStarted(false)
 	_pcSpeaker1->init();
 	_pcSpeaker2->init();
 	_soundResource = nullptr;
+	_soundWasPlaying = false;
 }
 
 ScriptFunctions::~ScriptFunctions() {
@@ -195,6 +198,10 @@ int16 ScriptFunctions::sfDrawPicture(int16 argc, int16 *argv) {
 }
 
 int16 ScriptFunctions::sfClearScreen(int16 argc, int16 *argv) {
+	if (_vm->getGameID() == GID_LGOP2) {
+		_vm->stopTextToSpeech();
+	}
+
 	if (_vm->_screen->isScreenLocked())
 		return 0;
 	if (_vm->_autoStopSound) {
@@ -248,6 +255,7 @@ int16 ScriptFunctions::sfPlaySound(int16 argc, int16 *argv) {
 		soundNum = argv[1];
 		_vm->_autoStopSound = (argv[0] == 1);
 	}
+	_soundWasPlaying = true;
 	if (soundNum > 0) {
 		SoundResource *soundRes = _vm->_res->getSound(soundNum);
 		_vm->_mixer->playStream(Audio::Mixer::kSFXSoundType, &_audioStreamHandle,
@@ -390,6 +398,15 @@ int16 ScriptFunctions::sfShowMouseCursor(int16 argc, int16 *argv) {
 }
 
 int16 ScriptFunctions::sfGetMusicBeat(int16 argc, int16 *argv) {
+	// Delay the opening credits when TTS is enabled until TTS is done speaking,
+	// as they move too fast otherwise
+	if (_vm->getGameID() == GID_RTZ && _vm->_openingCreditsOpen) {
+		Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+		if (ttsMan != nullptr && ConfMan.getBool("tts_enabled") && ttsMan->isSpeaking()) {
+			return 0;
+		}
+	}
+
 	// This is used as timer in some games
 	return (_vm->_system->getMillis() - _vm->_musicBeatStart) / 360;
 }
@@ -425,6 +442,17 @@ int16 ScriptFunctions::sfDrawSprite(int16 argc, int16 *argv) {
 		SpriteListItem item = _vm->_screen->getFromSpriteList(argv[2]);
 		int16 channelIndex = _vm->_screen->drawSprite(item.index, argv[1] - item.xofs, argv[0] - item.yofs);
 		_vm->_screen->setChannelUseMask(channelIndex);
+
+		if (_vm->getGameID() == GID_LGOP2) {
+			// Postcard examine, which displays several pieces of text immediately, resulting in only the last being
+			// voiced unless the text is queued
+			if (item.index == 687) {
+				_vm->_forceQueueText = true;
+			} else {
+				_vm->_forceQueueText = false;
+			}
+		}
+
 		return 0;
 	} else {
 		return 0;
@@ -507,6 +535,45 @@ int16 ScriptFunctions::sfDrawText(int16 argc, int16 *argv) {
 			break;
 		}
 		_vm->_screen->printText(finalText.c_str());
+
+#ifdef USE_TTS
+		if (_vm->getGameID() == GID_LGOP2) {
+			if (_vm->_playOMaticButtonIndex < ARRAYSIZE(_vm->_playOMaticButtonText)) {
+				_vm->_playOMaticButtonText[_vm->_playOMaticButtonIndex] = finalText;
+				_vm->_playOMaticButtonIndex++;
+			} else {
+				finalText.replace('\x09', '\n');	// Replace tabs with newlines
+				if (_vm->_voiceText) {
+					if (_vm->_forceQueueText) {
+						_vm->sayText(finalText, Common::TextToSpeechManager::QUEUE);
+					} else {
+						_vm->sayText(finalText);
+					}
+				} else if (_vm->_forceVoiceText) {
+					_vm->sayText(finalText, Common::TextToSpeechManager::QUEUE);
+					_vm->_forceVoiceText = false;
+				} else {
+					_vm->stopTextToSpeech();
+				}
+			}
+		} else if (_vm->getGameID() == GID_RTZ) {
+			if (_vm->_saveLoadScreenOpen) {
+				if (_vm->_rtzFirstSaveSlot == 0) {
+					_vm->_rtzFirstSaveSlot = atoi(finalText.c_str());
+				}
+			} else if (_vm->_tapeRecorderOpen) {
+				if (_vm->_tapeRecorderIndex < ARRAYSIZE(_vm->_tapeRecorderText)) {
+					_vm->_tapeRecorderText[_vm->_tapeRecorderIndex] = finalText;
+					_vm->_tapeRecorderIndex++;
+				}
+			} else {
+				_vm->sayText(finalText, Common::TextToSpeechManager::QUEUE);
+				_vm->_screen->setQueueNextText(true);
+			}
+		} else {
+			_vm->sayText(finalText);
+		}
+#endif
 	}
 
 	return 0;
@@ -544,6 +611,15 @@ int16 ScriptFunctions::sfSetFontDropShadow(int16 argc, int16 *argv) {
 }
 
 int16 ScriptFunctions::sfSetFontColor(int16 argc, int16 *argv) {
+	if (_vm->getGameID() == GID_LGOP2) {
+		// White text usually has a voiceover, so it shouldn't be voiced by TTS, while text of other colors should be
+		if (argv[0] == 255) {
+			_vm->_voiceText = false;
+		} else {
+			_vm->_voiceText = true;
+		}
+	}
+
 	_vm->_screen->setTextColor(argv[0]);
 	return 0;
 }
@@ -598,8 +674,16 @@ int16 ScriptFunctions::sfSetSpriteMask(int16 argc, int16 *argv) {
 
 int16 ScriptFunctions::sfSoundPlaying(int16 argc, int16 *argv) {
 	if (_vm->getGameID() == GID_RTZ) {
-		if (!_vm->_mixer->isSoundHandleActive(_audioStreamHandle))
+		if (!_vm->_mixer->isSoundHandleActive(_audioStreamHandle)) {
+			if (_soundWasPlaying) {
+				_vm->_screen->setVoiceTimeText(true);
+				_soundWasPlaying = false;
+			}
+			
 			return 0;
+		}
+
+		_vm->_screen->setVoiceTimeText(false);
 
 		// For looping sounds the game script regularly checks if the sound has
 		// finished playing, then plays it again. This works in the original
@@ -930,9 +1014,25 @@ int16 ScriptFunctions::sfDrawMenu(int16 argc, int16 *argv) {
 	MenuResource *menu = _vm->_res->getMenu(menuIndex);
 	if (menu) {
 		const char *text = menu->getString(textIndex);
-		if (text)
+		if (text) {
 			_vm->_screen->printText(text);
 
+#ifdef USE_TTS
+			if (_vm->_saveLoadScreenOpen) {
+				if (_vm->_rtzSaveLoadIndex < ARRAYSIZE(_vm->_rtzSaveLoadButtonText)) {
+					_vm->_rtzSaveLoadButtonText[_vm->_rtzSaveLoadIndex] = text;
+					_vm->_rtzSaveLoadIndex++;
+				}
+			} else {
+				if (_vm->_openingCreditsOpen) {
+					_vm->sayText(text, Common::TextToSpeechManager::QUEUE);
+				} else {
+					_vm->sayText(text);
+				}
+			}
+#endif
+		}
+
 		_vm->_res->freeResource(menu);
 	}
 	return 0;
diff --git a/engines/made/scriptfuncs.h b/engines/made/scriptfuncs.h
index 47c61a804c6..21032f4a13a 100644
--- a/engines/made/scriptfuncs.h
+++ b/engines/made/scriptfuncs.h
@@ -62,6 +62,7 @@ protected:
 	Audio::SoundHandle _voiceStreamHandle;
 	SoundResource* _soundResource;
 	bool _soundStarted;
+	bool _soundWasPlaying;
 	// The sound length in milliseconds for purpose of checking if the sound is
 	// still playing.
 	int _soundCheckLength;




More information about the Scummvm-git-logs mailing list