[Scummvm-git-logs] scummvm master -> 15573cf8a0a7ecbf17c19373e72932e5aa8bcf0b

sev- noreply at scummvm.org
Thu Aug 21 08:22:49 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:
15573cf8a0 GOB: Add text-to-speech (TTS)


Commit: 15573cf8a0a7ecbf17c19373e72932e5aa8bcf0b
    https://github.com/scummvm/scummvm/commit/15573cf8a0a7ecbf17c19373e72932e5aa8bcf0b
Author: ellm135 (ellm13531 at gmail.com)
Date: 2025-08-21T10:22:46+02:00

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

Changed paths:
    engines/gob/detection/detection.cpp
    engines/gob/detection/detection.h
    engines/gob/draw.h
    engines/gob/draw_fascin.cpp
    engines/gob/draw_playtoons.cpp
    engines/gob/draw_v1.cpp
    engines/gob/draw_v2.cpp
    engines/gob/game.cpp
    engines/gob/gob.cpp
    engines/gob/gob.h
    engines/gob/hotspots.cpp
    engines/gob/hotspots.h
    engines/gob/inter_v1.cpp
    engines/gob/metaengine.cpp
    engines/gob/mult.cpp
    engines/gob/util.cpp
    engines/gob/videoplayer.cpp


diff --git a/engines/gob/detection/detection.cpp b/engines/gob/detection/detection.cpp
index c220d69b858..9c983c47a87 100644
--- a/engines/gob/detection/detection.cpp
+++ b/engines/gob/detection/detection.cpp
@@ -87,7 +87,7 @@ private:
 GobMetaEngineDetection::GobMetaEngineDetection() :
 	AdvancedMetaEngineDetection(Gob::gameDescriptions, gobGames) {
 
-	_guiOptions = GUIO1(GUIO_NOLAUNCHLOAD);
+	_guiOptions = GUIO2(GUIO_NOLAUNCHLOAD, GAMEOPTION_TTS);
 }
 
 ADDetectedGame GobMetaEngineDetection::fallbackDetect(const FileMap &allFiles, const Common::FSList &fslist, ADDetectedGameExtraInfo **extra) const {
diff --git a/engines/gob/detection/detection.h b/engines/gob/detection/detection.h
index 452677baa58..3727cf090ee 100644
--- a/engines/gob/detection/detection.h
+++ b/engines/gob/detection/detection.h
@@ -107,6 +107,7 @@ struct GOBGameDescription {
 };
 
 #define GAMEOPTION_COPY_PROTECTION	GUIO_GAMEOPTIONS1
+#define GAMEOPTION_TTS				GUIO_GAMEOPTIONS2
 
 } // End of namespace Gob
 
diff --git a/engines/gob/draw.h b/engines/gob/draw.h
index c2e8e50e368..00a8a370dec 100644
--- a/engines/gob/draw.h
+++ b/engines/gob/draw.h
@@ -225,7 +225,7 @@ public:
 
 	virtual void animateCursor(int16 cursor) = 0;
 	virtual void printTotText(int16 id) = 0;
-	virtual void spriteOperation(int16 operation) = 0;
+	virtual void spriteOperation(int16 operation, bool ttsAddHotspotText = true) = 0;
 
 	virtual int16 openWin(int16 id) { return 0; }
 	virtual void closeWin(int16 id) {}
@@ -241,6 +241,9 @@ public:
 
 protected:
 	GobEngine *_vm;
+#ifdef USE_TTS
+	Common::String _previousTot;
+#endif
 };
 
 class Draw_v1 : public Draw {
@@ -250,7 +253,7 @@ public:
 	void blitCursor() override;
 	void animateCursor(int16 cursor) override;
 	void printTotText(int16 id) override;
-	void spriteOperation(int16 operation) override;
+	void spriteOperation(int16 operation, bool ttsAddHotspotText = true) override;
 
 	Draw_v1(GobEngine *vm);
 	~Draw_v1() override {}
@@ -263,7 +266,7 @@ public:
 	void blitCursor() override;
 	void animateCursor(int16 cursor) override;
 	void printTotText(int16 id) override;
-	void spriteOperation(int16 operation) override;
+	void spriteOperation(int16 operation, bool ttsAddHotspotText = true) override;
 
 	Draw_v2(GobEngine *vm);
 	~Draw_v2() override {}
@@ -286,7 +289,7 @@ class Draw_Fascination: public Draw_v2 {
 public:
 	Draw_Fascination(GobEngine *vm);
 	~Draw_Fascination() override {}
-	void spriteOperation(int16 operation) override;
+	void spriteOperation(int16 operation, bool ttsAddHotspotText = true) override;
 
 	void decompWin(int16 x, int16 y, SurfacePtr destPtr);
 	void drawWin(int16 fct);
@@ -310,7 +313,7 @@ class Draw_Playtoons: public Draw_v2 {
 public:
 	Draw_Playtoons(GobEngine *vm);
 	~Draw_Playtoons() override {}
-	void spriteOperation(int16 operation) override;
+	void spriteOperation(int16 operation, bool ttsAddHotspotText = true) override;
 };
 
 
diff --git a/engines/gob/draw_fascin.cpp b/engines/gob/draw_fascin.cpp
index 2f1203ebc9a..ac09b9079e0 100644
--- a/engines/gob/draw_fascin.cpp
+++ b/engines/gob/draw_fascin.cpp
@@ -28,6 +28,7 @@
 #include "gob/draw.h"
 #include "gob/game.h"
 #include "gob/global.h"
+#include "gob/hotspots.h"
 #include "gob/inter.h"
 #include "gob/resources.h"
 
@@ -36,7 +37,7 @@ namespace Gob {
 Draw_Fascination::Draw_Fascination(GobEngine *vm) : Draw_v2(vm) {
 }
 
-void Draw_Fascination::spriteOperation(int16 operation) {
+void Draw_Fascination::spriteOperation(int16 operation, bool ttsAddHotspotText) {
 	int16 len;
 	int16 x, y;
 	SurfacePtr sourceSurf, destSurf;
@@ -257,6 +258,13 @@ void Draw_Fascination::spriteOperation(int16 operation) {
 			}
 		}
 
+#ifdef USE_TTS
+		if (ttsAddHotspotText) {
+			_vm->_game->_hotspots->addHotspotText(_textToPrint, left, _destSpriteY,
+											_destSpriteX - 1, _destSpriteY + _fonts[_fontIndex]->getCharHeight() - 1, _destSurface);
+		}
+#endif
+
 		dirtiedRect(_destSurface, left, _destSpriteY,
 				_destSpriteX - 1, _destSpriteY + _fonts[_fontIndex]->getCharHeight() - 1);
 		break;
@@ -542,6 +550,12 @@ void Draw_Fascination::drawWin(int16 fct) {
 				_fonts[_fontIndex]->drawLetter(*tempSrf, _textToPrint[j],
 						j * _fonts[_fontIndex]->getCharWidth(), 0, _frontColor, _backColor, _transparency);
 			_destSpriteX += len * _fonts[_fontIndex]->getCharWidth();
+
+#ifdef USE_TTS
+			_vm->_game->_hotspots->addHotspotText(_textToPrint, left, _destSpriteY,
+											_destSpriteX - 1, _destSpriteY + _fonts[_fontIndex]->getCharHeight() - 1, _destSurface);
+#endif
+
 			break;
 
 		case DRAW_DRAWBAR:     // 7 - draw border
@@ -660,6 +674,12 @@ void Draw_Fascination::drawWin(int16 fct) {
 						_destSpriteX + j * _fonts[_fontIndex]->getCharWidth(), _destSpriteY,
 						_frontColor, _backColor, _transparency);
 			_destSpriteX += len * _fonts[_fontIndex]->getCharWidth();
+
+#ifdef USE_TTS
+			_vm->_game->_hotspots->addHotspotText(_textToPrint, left, _destSpriteY,
+											_destSpriteX - 1, _destSpriteY + _fonts[_fontIndex]->getCharHeight() - 1, _destSurface);
+#endif
+
 			break;
 
 		case DRAW_DRAWBAR:     // 7 - draw border
diff --git a/engines/gob/draw_playtoons.cpp b/engines/gob/draw_playtoons.cpp
index ac439173a72..2bf25312b40 100644
--- a/engines/gob/draw_playtoons.cpp
+++ b/engines/gob/draw_playtoons.cpp
@@ -31,13 +31,14 @@
 #include "gob/inter.h"
 #include "gob/game.h"
 #include "gob/resources.h"
+#include "gob/hotspots.h"
 
 namespace Gob {
 
 Draw_Playtoons::Draw_Playtoons(GobEngine *vm) : Draw_v2(vm) {
 }
 
-void Draw_Playtoons::spriteOperation(int16 operation) {
+void Draw_Playtoons::spriteOperation(int16 operation, bool ttsAddHotspotText) {
 	int16 len;
 	int16 x, y;
 	bool deltaVeto;
@@ -350,6 +351,13 @@ void Draw_Playtoons::spriteOperation(int16 operation) {
 			}
 		}
 
+#ifdef USE_TTS
+		if (ttsAddHotspotText) {
+			_vm->_game->_hotspots->addHotspotText(_textToPrint, left, _destSpriteY, 
+											_destSpriteX - 1, _destSpriteY + _fonts[_fontIndex]->getCharHeight() - 1, _destSurface);
+		}
+#endif
+
 		dirtiedRect(_destSurface, left, _destSpriteY,
 				_destSpriteX - 1, _destSpriteY + _fonts[_fontIndex]->getCharHeight() - 1);
 		break;
diff --git a/engines/gob/draw_v1.cpp b/engines/gob/draw_v1.cpp
index 1cecff2583e..ebda9c4b480 100644
--- a/engines/gob/draw_v1.cpp
+++ b/engines/gob/draw_v1.cpp
@@ -233,6 +233,9 @@ void Draw_v1::printTotText(int16 id) {
 	}
 	ptrEnd++;
 
+#ifdef USE_TTS
+	Common::String ttsMessage;
+#endif
 	while (*ptr != 1) {
 		cmd = *ptr;
 		if (cmd == 3) {
@@ -281,8 +284,13 @@ void Draw_v1::printTotText(int16 id) {
 			}
 
 			_textToPrint = buf;
+#ifdef USE_TTS
+			ttsMessage += _textToPrint;
+			ttsMessage += " ";
+#endif
+
 			destSpriteX = _destSpriteX;
-			spriteOperation(DRAW_PRINTTEXT);
+			spriteOperation(DRAW_PRINTTEXT, false);
 			if (ptrEnd[17] & 0x80) {
 				if (ptr[1] == ' ') {
 					_destSpriteX += _fonts[_fontIndex]->getCharWidth();
@@ -304,6 +312,18 @@ void Draw_v1::printTotText(int16 id) {
 		}
 	}
 
+#ifdef USE_TTS
+	if (_previousTot != ttsMessage) {
+		if (_vm->_game->_hotspots->hoveringOverHotspot()) {
+			_vm->sayText(ttsMessage);
+		} else {
+			_vm->sayText(ttsMessage, Common::TextToSpeechManager::QUEUE);
+		}
+
+		_previousTot = ttsMessage;
+	}
+#endif
+
 	delete textItem;
 	_renderFlags = savedFlags;
 
@@ -316,7 +336,7 @@ void Draw_v1::printTotText(int16 id) {
 	}
 }
 
-void Draw_v1::spriteOperation(int16 operation) {
+void Draw_v1::spriteOperation(int16 operation, bool ttsAddHotspotText) {
 	int16 len;
 	int16 x, y;
 	int16 perLine;
@@ -416,6 +436,14 @@ void Draw_v1::spriteOperation(int16 operation) {
 				_destSpriteX + len * font->getCharWidth() - 1,
 				_destSpriteY + font->getCharHeight() - 1);
 
+#ifdef USE_TTS
+		if (ttsAddHotspotText) {
+			_vm->_game->_hotspots->addHotspotText(_textToPrint, _destSpriteX, _destSpriteY,
+											_destSpriteX + len * font->getCharWidth() - 1,
+											_destSpriteY + font->getCharHeight() - 1, _destSurface);
+		}
+#endif
+
 		for (int i = 0; i < len; i++) {
 			font->drawLetter(*_spritesArray[_destSurface], _textToPrint[i],
 					_destSpriteX, _destSpriteY, _frontColor, _backColor, _transparency);
diff --git a/engines/gob/draw_v2.cpp b/engines/gob/draw_v2.cpp
index 9cf8b2d85ea..91a5636ea5e 100644
--- a/engines/gob/draw_v2.cpp
+++ b/engines/gob/draw_v2.cpp
@@ -395,6 +395,9 @@ void Draw_v2::printTotText(int16 id) {
 	_backColor = 0;
 	_transparency = 1;
 
+#ifdef USE_TTS
+	Common::String ttsMessage;
+#endif
 	while (true) {
 		if ((((*ptr >= 1) && (*ptr <= 7)) || (*ptr == 10)) && (strPos != 0)) {
 			str[MAX(strPos, strPos2)] = 0;
@@ -429,6 +432,10 @@ void Draw_v2::printTotText(int16 id) {
 				_fontIndex   = fontIndex;
 				_frontColor  = frontColor;
 				_textToPrint = str;
+#ifdef USE_TTS
+				ttsMessage += _textToPrint;
+				ttsMessage += " ";
+#endif
 
 				if (isSubtitle) {
 					_fontIndex  = _subtitleFont;
@@ -442,10 +449,10 @@ void Draw_v2::printTotText(int16 id) {
 							width -= _fonts[_fontIndex]->getCharWidth() / 2;
 							str[strlen(str) - 1] = '\0';
 						}
-						spriteOperation(DRAW_PRINTTEXT);
+						spriteOperation(DRAW_PRINTTEXT, false);
 					}
 				} else
-					spriteOperation(DRAW_PRINTTEXT);
+					spriteOperation(DRAW_PRINTTEXT, false);
 
 				width = strlen(str);
 				for (strPos = 0; strPos < width; strPos++) {
@@ -624,6 +631,18 @@ void Draw_v2::printTotText(int16 id) {
 		}
 	}
 
+#ifdef USE_TTS
+	if (_previousTot != ttsMessage && !isSubtitle) {
+		if (_vm->_game->_hotspots->hoveringOverHotspot()) {
+			_vm->sayText(ttsMessage);
+		} else {
+			_vm->sayText(ttsMessage, Common::TextToSpeechManager::QUEUE);
+		}
+
+		_previousTot = ttsMessage;
+	}
+#endif
+
 	delete textItem;
 	_renderFlags = savedFlags;
 
@@ -638,7 +657,7 @@ void Draw_v2::printTotText(int16 id) {
 	}
 }
 
-void Draw_v2::spriteOperation(int16 operation) {
+void Draw_v2::spriteOperation(int16 operation, bool ttsAddHotspotText) {
 	int16 len;
 	int16 x, y;
 	SurfacePtr sourceSurf, destSurf;
@@ -868,6 +887,23 @@ void Draw_v2::spriteOperation(int16 operation) {
 			}
 		}
 
+#ifdef USE_TTS
+		// Ween's notepad displays 1 character at a time. Stopping speech as the characters are displayed prevents TTS from
+		// slowly voicing each character
+		if (_vm->getGameType() == kGameTypeWeen && _vm->isCurrentTot("edit.tot")) {
+			_vm->stopTextToSpeech();
+			
+			if (!_vm->_weenVoiceNotepad) {
+				ttsAddHotspotText = false;
+			}
+		}
+
+		if (ttsAddHotspotText) {
+			_vm->_game->_hotspots->addHotspotText(_textToPrint, left, _destSpriteY,
+											_destSpriteX - 1, _destSpriteY + _fonts[_fontIndex]->getCharHeight() - 1, _destSurface);
+		}
+#endif
+
 		dirtiedRect(_destSurface, left, _destSpriteY,
 				_destSpriteX - 1, _destSpriteY + _fonts[_fontIndex]->getCharHeight() - 1);
 		break;
diff --git a/engines/gob/game.cpp b/engines/gob/game.cpp
index d6eb63ce2e2..ef92f2c54bd 100644
--- a/engines/gob/game.cpp
+++ b/engines/gob/game.cpp
@@ -685,6 +685,13 @@ void Game::playTot(int32 function) {
 			_vm->_inter->_terminate = 2;
 	}
 
+#ifdef USE_TTS
+	if (_vm->getGameType() == kGameTypeWeen && _vm->isCurrentTot("edit.tot")) {
+		_vm->_weenVoiceNotepad = true;
+		_vm->_game->_hotspots->clearHotspotText();
+	}
+#endif
+
 	_curTotFile = oldTotFile;
 
 	_vm->_inter->_nestLevel         = oldNestLevel;
@@ -755,6 +762,10 @@ void Game::capturePop(char doDraw) {
 		_vm->_draw->_needAdjust = 10;
 		_vm->_draw->spriteOperation(DRAW_BLITSURF);
 		_vm->_draw->_needAdjust = savedNeedAdjust;
+
+#ifdef USE_TTS
+		_hotspots->clearHotspotText();
+#endif
 	}
 	_vm->_draw->freeSprite(Draw::kCaptureSurface + _captureCount);
 }
diff --git a/engines/gob/gob.cpp b/engines/gob/gob.cpp
index 6d4b0013ba1..42e3938002a 100644
--- a/engines/gob/gob.cpp
+++ b/engines/gob/gob.cpp
@@ -40,6 +40,7 @@
 
 #include "gob/gob.h"
 #include "gob/global.h"
+#include "gob/hotspots.h"
 #include "gob/util.h"
 #include "gob/dataio.h"
 #include "gob/game.h"
@@ -141,6 +142,10 @@ GobEngine::GobEngine(OSystem *syst) : Engine(syst), _rnd("gob") {
 
 	_copyProtection = ConfMan.getBool("copy_protection");
 
+#ifdef USE_TTS
+	_weenVoiceNotepad = true;
+#endif
+
 	_console = new GobConsole(this);
 	setDebugger(_console);
 }
@@ -384,6 +389,35 @@ Common::Error GobEngine::run() {
 	}
 	_global->_languageWanted = _global->_language;
 
+#ifdef USE_TTS
+	Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+	if (ttsMan) {
+		ttsMan->setLanguage(ConfMan.get("language"));
+		ttsMan->enable(ConfMan.getBool("tts_enabled"));
+	}
+
+	switch (_language) {
+	case Common::HU_HUN:
+		_ttsEncoding = Common::CodePage::kWindows1250;
+		break;
+	case Common::KO_KOR:
+		_ttsEncoding = Common::CodePage::kWindows949;
+		break;
+	case Common::HE_ISR:
+		_ttsEncoding = Common::CodePage::kDos862;
+		break;
+	case Common::JA_JPN:
+		_ttsEncoding = Common::CodePage::kWindows932;
+		break;
+	case Common::RU_RUS:
+		_ttsEncoding = Common::CodePage::kWindows1251;
+		break;
+	default:
+		_ttsEncoding = Common::CodePage::kDos850;
+		break;
+	}
+#endif
+
 	_init->initGame();
 
 	return Common::kNoError;
@@ -427,6 +461,29 @@ void GobEngine::pauseGame() {
 	pauseEngineIntern(false);
 }
 
+#ifdef USE_TTS
+
+void GobEngine::sayText(const Common::String &text, Common::TextToSpeechManager::Action action) const {
+	if (text.empty()) {
+		return;
+	}
+
+	Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+	if (ttsMan && ConfMan.getBool("tts_enabled")) {
+		ttsMan->say(text, action, _ttsEncoding);
+	}
+}
+
+void GobEngine::stopTextToSpeech() const {
+	Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+	if (ttsMan && ConfMan.getBool("tts_enabled") && ttsMan->isSpeaking()) {
+		ttsMan->stop();
+		_game->_hotspots->clearPreviousSaid();
+	}
+}
+
+#endif
+
 Common::Error GobEngine::initGameParts() {
 	_resourceSizeWorkaround = false;
 
diff --git a/engines/gob/gob.h b/engines/gob/gob.h
index a6e0b2e80e7..eabde573e39 100644
--- a/engines/gob/gob.h
+++ b/engines/gob/gob.h
@@ -30,6 +30,7 @@
 
 #include "common/random.h"
 #include "common/system.h"
+#include "common/text-to-speech.h"
 
 #include "graphics/pixelformat.h"
 
@@ -227,12 +228,22 @@ public:
 	VideoPlayer *_vidPlayer;
 	PreGob *_preGob;
 
+#ifdef USE_TTS
+	bool _weenVoiceNotepad;
+	Common::CodePage _ttsEncoding;
+#endif
+
 	const char *getLangDesc(int16 language) const;
 	void validateLanguage();
 	void validateVideoMode(int16 videoMode);
 
 	void pauseGame();
 
+#ifdef USE_TTS
+	void sayText(const Common::String &text, Common::TextToSpeechManager::Action action = Common::TextToSpeechManager::INTERRUPT) const;
+	void stopTextToSpeech() const;
+#endif
+
 	EndiannessMethod getEndiannessMethod() const;
 	Endianness getEndianness() const;
 	Common::Platform getPlatform() const;
diff --git a/engines/gob/hotspots.cpp b/engines/gob/hotspots.cpp
index d71b57ea158..0412462c7a3 100644
--- a/engines/gob/hotspots.cpp
+++ b/engines/gob/hotspots.cpp
@@ -211,6 +211,10 @@ Hotspots::Hotspots(GobEngine *vm) : _vm(vm) {
 	_currentId    = 0;
 	_currentX     = 0;
 	_currentY     = 0;
+#ifdef USE_TTS
+	_currentHotspotTextIndex = -1;
+	_hotspotSpokenLast = false;
+#endif
 }
 
 Hotspots::~Hotspots() {
@@ -265,6 +269,11 @@ uint16 Hotspots::add(const Hotspot &hotspot) {
 		spot.scriptFuncPos = _vm->_game->_script;
 		spot.scriptFuncLeave = _vm->_game->_script;
 
+#ifdef USE_TTS
+		removeHotspotText(i);
+		expandHotspotText(i);
+#endif
+
 		debugC(1, kDebugHotspots, "Adding hotspot %03d: Coord:%3d+%3d+%3d+%3d - id:%04X, key:%04X, flag:%04X - fcts:%5d, %5d, %5d",
 				i, spot.left, spot.top, spot.right, spot.bottom,
 				spot.id, spot.key, spot.flags, spot.funcEnter, spot.funcLeave, spot.funcPos);
@@ -281,6 +290,9 @@ void Hotspots::remove(uint16 id) {
 		if (_hotspots[i].id == id) {
 			debugC(1, kDebugHotspots, "Removing hotspot %d: %X", i, id);
 			_hotspots[i].clear();
+#ifdef USE_TTS
+			removeHotspotText(i);
+#endif
 		}
 	}
 }
@@ -292,6 +304,9 @@ void Hotspots::removeState(uint8 state) {
 		if (spot.getState() == state) {
 			debugC(1, kDebugHotspots, "Removing hotspot %d: %X (by state %X)", i, spot.id, state);
 			spot.clear();
+#ifdef USE_TTS
+			removeHotspotText(i);
+#endif
 		}
 	}
 }
@@ -371,6 +386,11 @@ void Hotspots::recalculate(bool force) {
 
 		_vm->_game->_script = curScript;
 	}
+
+#ifdef USE_TTS
+	voiceUnassignedHotspots();
+	clearUnassignedHotspotText();
+#endif
 }
 
 void Hotspots::push(uint8 all, bool force) {
@@ -380,6 +400,10 @@ void Hotspots::push(uint8 all, bool force) {
 	if (!_shouldPush && !force)
 		return;
 
+#ifdef USE_TTS
+	voiceUnassignedHotspots();
+#endif
+
 	// Count the hotspots
 	uint32 size = 0;
 	for (int i = 0; (i < kHotspotCount) && !_hotspots[i].isEnd(); i++) {
@@ -448,6 +472,10 @@ void Hotspots::pop() {
 
 	assert(!_stack.empty());
 
+#ifdef USE_TTS
+	voiceUnassignedHotspots();
+#endif
+
 	StackEntry backup = _stack.pop();
 
 	// Find the end of the filled hotspot space
@@ -539,6 +567,10 @@ void Hotspots::leave(uint16 index) {
 		return;
 	}
 
+#ifdef USE_TTS
+	clearPreviousSaid();
+#endif
+
 	Hotspot &spot = _hotspots[index];
 
 	// If requested, write the ID into a variable
@@ -1238,8 +1270,12 @@ uint16 Hotspots::handleInputs(int16 time, uint16 inputCount, uint16 &curInput,
 
 		case kKeyReturn:
 			// Just one input => return
-			if (inputCount == 1)
+			if (inputCount == 1) {
+#ifdef USE_TTS
+				_vm->sayText(GET_VARO_STR(inputSpot.key));
+#endif
 				return kKeyReturn;
+			}
 
 			// End of input chain reached => wrap
 			if (curInput == (inputCount - 1)) {
@@ -1655,6 +1691,11 @@ void Hotspots::evaluate() {
 	// Recalculate all hotspots if requested
 	if (needRecalculation)
 		recalculate(true);
+#ifdef USE_TTS
+	else {
+		voiceUnassignedHotspots();
+	}
+#endif
 
 	_vm->_game->_forceHandleMouse = 0;
 	_vm->_util->clearKeyBuf();
@@ -1735,6 +1776,9 @@ void Hotspots::evaluate() {
 				spot.disable();
 	}
 
+#ifdef USE_TTS
+	clearUnassignedHotspotText();
+#endif
 }
 
 int16 Hotspots::findCursor(uint16 x, uint16 y) const {
@@ -1863,6 +1907,119 @@ void Hotspots::oPlaytoons_F_1B() {
 	return;
 }
 
+#ifdef USE_TTS
+
+bool Hotspots::hoveringOverHotspot() const {
+	return _currentIndex != 0;
+}
+
+void Hotspots::addHotspotText(const Common::String &text, uint16 x1, uint16 y1, uint16 x2, uint16 y2, int16 surf) {
+	if (x1 <= x2 && y1 <= y2) {
+		Common::Rect rect(x1, y1, x2, y2);
+		for (uint i = 0; i < _hotspotText.size(); ++i) {
+			// If there's already hotspot text at the same position, simply change the text
+			if (rect.contains(_hotspotText[i].rect) || _hotspotText[i].rect.contains(rect)) {
+				_hotspotText[i].str = text;
+				_hotspotText[i].voiced = false;
+				return;
+			}
+		}
+
+		_hotspotText.push_back(HotspotTTSText{text, rect, -1, false, surf});
+	} else {
+		_vm->sayText(text, Common::TextToSpeechManager::QUEUE);
+	}
+}
+
+void Hotspots::voiceHotspotText(int16 x, int16 y) {
+	// Don't allow any text on Ween's notepad to be voiced by moving the mouse, as the text is typically broken into pieces,
+	// which results in awkward voicing
+	if (_vm->getGameType() == kGameTypeWeen && _vm->isCurrentTot("edit.tot")) {
+		return;
+	}
+
+	for (uint i = 0; i < _hotspotText.size(); ++i) {
+		if (_hotspotText[i].rect.contains(x, y)) {
+			if ((int16)i != _currentHotspotTextIndex) {
+				_vm->sayText(_hotspotText[i].str, _hotspotSpokenLast ? Common::TextToSpeechManager::INTERRUPT : Common::TextToSpeechManager::QUEUE);
+				_hotspotSpokenLast = true;
+				_currentHotspotTextIndex = i;
+			}
+			
+			return;
+		}
+	}
+
+	_currentHotspotTextIndex = -1;
+	if (!hoveringOverHotspot()) {
+		clearPreviousSaid();
+	}
+}
+
+
+void Hotspots::voiceUnassignedHotspots() {
+	Common::String ttsMessage;
+	for (uint i = 0; i < _hotspotText.size(); ++i) {
+		// For Ween's notepad, manually add spaces back in
+		if (_vm->getGameType() == kGameTypeWeen && _vm->isCurrentTot("edit.tot")) {
+			if (_vm->_weenVoiceNotepad) {
+				if (_vm->_draw->_fontIndex < _vm->_draw->kFontCount && _vm->_draw->_fonts[_vm->_draw->_fontIndex]) {
+					if (i > 0 && 
+						_hotspotText[i].rect.left != 
+						_hotspotText[i - 1].rect.left + _vm->_draw->_fonts[_vm->_draw->_fontIndex]->getCharWidth()) {
+						ttsMessage += " ";
+					}
+				}
+
+				ttsMessage += _hotspotText[i].str;
+			}
+		} else if (_hotspotText[i].hotspot == -1 && !_hotspotText[i].voiced) {
+			ttsMessage += _hotspotText[i].str + "\n";
+			_hotspotText[i].voiced = true;
+		}
+	}
+
+	if (_previousSaid != ttsMessage && !ttsMessage.empty()) {
+		_hotspotSpokenLast = false;
+		_vm->sayText(ttsMessage, Common::TextToSpeechManager::QUEUE);
+		_previousSaid = ttsMessage;
+	}
+}
+
+void Hotspots::clearHotspotText() {
+	_hotspotText.clear();
+}
+
+void Hotspots::clearUnassignedHotspotText() {
+	for (Common::Array<HotspotTTSText>::iterator it = _hotspotText.begin(); it != _hotspotText.end();) {
+		if (it->hotspot == -1) {
+			it = _hotspotText.erase(it);
+		} else {
+			it++;
+		}
+	}
+}
+
+void Hotspots::clearPreviousSaid() {
+	_previousSaid.clear();
+}
+
+void Hotspots::adjustHotspotTextRect(uint16 oldLeft, uint16 oldTop, uint16 oldRight, uint16 oldBottom, uint16 newX, uint16 newY, int16 surf) {
+	if (oldLeft > oldRight || oldTop > oldBottom) {
+		return;
+	}
+
+	Common::Rect oldRect(oldLeft, oldTop, oldRight, oldBottom);
+	for (uint i = 0; i < _hotspotText.size(); ++i) {
+		if (_hotspotText[i].surf == surf && oldRect.contains(_hotspotText[i].rect)) {
+			_hotspotText[i].rect.moveTo(newX, newY);
+			return;
+		}
+	}
+}
+
+#endif
+
 uint16 Hotspots::inputToHotspot(uint16 input) const {
 	uint16 inputIndex = 0;
 	for (int i = 0; i < kHotspotCount; i++) {
@@ -2272,4 +2429,69 @@ void Hotspots::updateAllTexts(const InputDesc *inputs) const {
 		input++;
 	}
 }
+
+#ifdef USE_TTS
+
+void Hotspots::expandHotspotText(uint16 spotID) {
+	if (spotID > kHotspotCount || _hotspots[spotID].getType() == kTypeClick || _hotspots[spotID].isDisabled()) {
+		return;
+	}
+
+	if (_vm->getGameType() == kGameTypeFascination) {
+		// Don't include hotspots that aren't on the current window
+		uint16 windowNum = _hotspots[spotID].getWindow();
+		if (windowNum != 0 && (_vm->_draw->_fascinWin[windowNum].id == -1 || 
+							_vm->_draw->_fascinWin[windowNum].id != _vm->_draw->_winCount - 1)) {
+			return;
+		}
+	}
+
+	Common::Rect spotRect;
+	spotRect.left = _hotspots[spotID].left;
+	spotRect.top = _hotspots[spotID].top;
+	spotRect.right = _hotspots[spotID].right;
+	spotRect.bottom = _hotspots[spotID].bottom;
+	
+	if (!spotRect.isValidRect()) {
+		return;
+	}
+	
+	for (int i = _hotspotText.size() - 1; i >= 0; --i) {
+		if (_hotspotText[i].hotspot != -1) {
+			continue;
+		}
+
+		if (spotRect.intersects(_hotspotText[i].rect)) {
+			_hotspotText[i].rect = spotRect;
+			_hotspotText[i].hotspot = spotID;
+
+			// Try to voice what the mouse is hovering over, as the text underneath the mouse may have changed
+			int16 dx = 0;
+			int16 dy = 0;
+			if (_vm->_draw->getWinFromCoord(dx, dy) < 0) {
+				dx = 0;
+				dy = 0;
+			}
+
+			voiceHotspotText(_vm->_global->_inter_mouseX - dx, _vm->_global->_inter_mouseY - dy);
+			return;
+		}
+	}
+}
+
+void Hotspots::removeHotspotText(uint16 spotID) {
+	for (uint i = 0; i < _hotspotText.size(); ++i) {
+		if (_hotspotText[i].hotspot == spotID) {
+			_hotspotText.remove_at(i);
+
+			if ((int16)i == _currentHotspotTextIndex) {
+				_currentHotspotTextIndex = -1;
+			}
+			return;
+		}
+	}
+}
+
+#endif
+
 } // End of namespace Gob
diff --git a/engines/gob/hotspots.h b/engines/gob/hotspots.h
index 6e8375c8802..d49cc1ff88c 100644
--- a/engines/gob/hotspots.h
+++ b/engines/gob/hotspots.h
@@ -106,6 +106,17 @@ public:
 	/** implementation of oPlaytoons_F_1B code*/
 	void oPlaytoons_F_1B();
 
+#ifdef USE_TTS
+	bool hoveringOverHotspot() const;
+	void addHotspotText(const Common::String &text, uint16 x1, uint16 y1, uint16 x2, uint16 y2, int16 surf);
+	void voiceUnassignedHotspots();
+	void voiceHotspotText(int16 x, int16 y);
+	void clearHotspotText();
+	void clearUnassignedHotspotText();
+	void clearPreviousSaid();
+	void adjustHotspotTextRect(uint16 oldLeft, uint16 oldTop, uint16 oldRight, uint16 oldBottom, uint16 newX, uint16 newY, int16 surf);
+#endif
+
 private:
 	struct Hotspot {
 		uint16  id;
@@ -157,6 +168,16 @@ private:
 		void enable ();
 	};
 
+#ifdef USE_TTS
+	struct HotspotTTSText {
+		Common::String str;
+		Common::Rect rect;
+		int16 hotspot;
+		bool voiced;
+		int16 surf;
+	};
+#endif
+
 	struct StackEntry {
 		bool     shouldPush;
 		Hotspot *hotspots;
@@ -189,6 +210,13 @@ private:
 	uint16 _currentX;
 	uint16 _currentY;
 
+#ifdef USE_TTS
+	Common::String _previousSaid;
+	int16 _currentHotspotTextIndex;
+	bool _hotspotSpokenLast;
+	Common::Array<HotspotTTSText> _hotspotText;
+#endif
+
 	/** Add a hotspot, returning the new index. */
 	uint16 add(const Hotspot &hotspot);
 
@@ -278,6 +306,11 @@ private:
 
 	/** Go through all inputs we manage and redraw their texts. */
 	void updateAllTexts(const InputDesc *inputs) const;
+
+#ifdef USE_TTS
+	void expandHotspotText(uint16 spotID);
+	void removeHotspotText(uint16 spotID);
+#endif
 };
 
 } // End of namespace Gob
diff --git a/engines/gob/inter_v1.cpp b/engines/gob/inter_v1.cpp
index 5f0578fa4d3..59d35460c11 100644
--- a/engines/gob/inter_v1.cpp
+++ b/engines/gob/inter_v1.cpp
@@ -1361,12 +1361,19 @@ void Inter_v1::o1_keyFunc(OpFuncParams &params) {
 	int16 cmd = _vm->_game->_script->readInt16();
 	int16 key;
 
+#ifdef USE_TTS
+	_vm->_game->_hotspots->voiceUnassignedHotspots();
+#endif
 	switch (cmd) {
 	case 0:
 		_vm->_draw->_showCursor &= ~2;
 		_vm->_util->longDelay(1);
 		key = _vm->_game->_hotspots->check(0, 0);
 		storeKey(key);
+#ifdef USE_TTS
+		_vm->stopTextToSpeech();
+		_vm->_game->_hotspots->clearHotspotText();
+#endif
 
 		_vm->_util->clearKeyBuf();
 		break;
@@ -1395,6 +1402,17 @@ void Inter_v1::o1_keyFunc(OpFuncParams &params) {
 		key = _vm->_game->checkKeys(&_vm->_global->_inter_mouseX,
 				&_vm->_global->_inter_mouseY, &_vm->_game->_mouseButtons, 0);
 		storeKey(key);
+#ifdef USE_TTS
+		if (key) {
+			// After a key is pressed with the notepad open, no longer voice the notepad. This prevents very awkward voicing
+			// as the user types
+			if (_vm->getGameType() == kGameTypeWeen && _vm->isCurrentTot("edit.tot")) {
+				_vm->_weenVoiceNotepad = false;
+			}
+
+			_vm->stopTextToSpeech();
+		}
+#endif
 		break;
 
 	case 2:
@@ -1657,6 +1675,13 @@ void Inter_v1::o1_copySprite(OpFuncParams &params) {
 		return;
 	}
 
+#ifdef USE_TTS
+	_vm->_game->_hotspots->adjustHotspotTextRect(_vm->_draw->_spriteLeft, _vm->_draw->_spriteTop, 
+											_vm->_draw->_spriteLeft + _vm->_draw->_spriteRight - 1, 
+											_vm->_draw->_spriteTop + _vm->_draw->_spriteBottom - 1,
+											_vm->_draw->_destSpriteX, _vm->_draw->_destSpriteY, _vm->_draw->_sourceSurface);
+#endif
+
 	_vm->_draw->spriteOperation(DRAW_BLITSURF);
 }
 
diff --git a/engines/gob/metaengine.cpp b/engines/gob/metaengine.cpp
index 7fe879a6abc..2d6c8b5844c 100644
--- a/engines/gob/metaengine.cpp
+++ b/engines/gob/metaengine.cpp
@@ -50,6 +50,20 @@ 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
 };
 
diff --git a/engines/gob/mult.cpp b/engines/gob/mult.cpp
index f254413eb93..4a341025d7c 100644
--- a/engines/gob/mult.cpp
+++ b/engines/gob/mult.cpp
@@ -180,6 +180,7 @@ void Mult::playMult(int16 startFrame, int16 endFrame, char checkEscape,
 	if (_frame == -1)
 		playMultInit();
 
+	Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
 	do {
 		stop = true;
 
@@ -209,13 +210,19 @@ void Mult::playMult(int16 startFrame, int16 endFrame, char checkEscape,
 		if (_vm->_sound->blasterPlayingSound())
 			stop = false;
 
-		_vm->_util->processInput();
-		if (checkEscape && (_vm->_util->checkKey() == kKeyEscape))
-			stop = true;
+		do {
+			_vm->_util->processInput();
+			if (checkEscape && (_vm->_util->checkKey() == kKeyEscape))
+				stop = true;
+
+			_vm->_util->waitEndFrame();
+		} while (!stop && stopNoClear && ttsMan && ttsMan->isSpeaking());
 
 		_frame++;
-		_vm->_util->waitEndFrame();
 	} while (!stop && !stopNoClear && !_vm->shouldQuit());
+#ifdef USE_TTS
+	_vm->stopTextToSpeech();
+#endif
 
 	if (!stopNoClear) {
 		if (_animDataAllocated) {
diff --git a/engines/gob/util.cpp b/engines/gob/util.cpp
index edd8fc005cb..4e9a158a186 100644
--- a/engines/gob/util.cpp
+++ b/engines/gob/util.cpp
@@ -30,6 +30,7 @@
 #include "graphics/paletteman.h"
 
 #include "gob/gob.h"
+#include "gob/hotspots.h"
 #include "gob/util.h"
 #include "gob/global.h"
 #include "gob/dataio.h"
@@ -371,6 +372,10 @@ void Util::setMousePos(int16 x, int16 y) {
 	x = CLIP<int>(x + _vm->_video->_screenDeltaX, 0, _vm->_width - 1);
 	y = CLIP<int>(y + _vm->_video->_screenDeltaY, 0, _vm->_height - 1);
 	g_system->warpMouse(x, y);
+
+#ifdef USE_TTS
+	_vm->_game->_hotspots->voiceHotspotText(x, y);
+#endif
 }
 
 void Util::waitMouseUp() {
diff --git a/engines/gob/videoplayer.cpp b/engines/gob/videoplayer.cpp
index 5b7a57042dd..b71f3faebd4 100644
--- a/engines/gob/videoplayer.cpp
+++ b/engines/gob/videoplayer.cpp
@@ -808,12 +808,18 @@ void VideoPlayer::checkAbort(Video &video, Properties &properties) {
 			if (pressedBreak ||
 				_vm->_game->_mouseButtons == properties.breakKey) {
 				properties.canceled = true;
+#ifdef USE_TTS
+				_vm->stopTextToSpeech();
+#endif
 				return;
 			}
 
 			if (properties.breakKey == 4) {
 				if (_vm->_game->_mouseButtons == kMouseButtonsRight || key == kKeyEscape) {
 					properties.canceled = true;
+#ifdef USE_TTS
+					_vm->stopTextToSpeech();
+#endif
 					return;
 				}
 
@@ -824,6 +830,9 @@ void VideoPlayer::checkAbort(Video &video, Properties &properties) {
 					_vm->_game->_forwardedKeyFromVideo = key;
 					_vm->_game->_forwardedMouseButtonsFromVideo = _vm->_game->_mouseButtons;
 					properties.canceled = true;
+#ifdef USE_TTS
+					_vm->stopTextToSpeech();
+#endif
 					return;
 				}
 			}
@@ -844,6 +853,9 @@ void VideoPlayer::checkAbort(Video &video, Properties &properties) {
 				// Seek to the last frame. Some scripts depend on that.
 				video.decoder->seek(properties.endFrame + 1, SEEK_SET, true);
 				properties.canceled = true;
+#ifdef USE_TTS
+				_vm->stopTextToSpeech();
+#endif
 			}
 		}
 	}




More information about the Scummvm-git-logs mailing list