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

sev- noreply at scummvm.org
Mon Jul 21 11:08:22 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:
bef5a559d3 PRINCE: Add text-to-speech (TTS)


Commit: bef5a559d3f782dada282b04a51c13b5605ee3bc
    https://github.com/scummvm/scummvm/commit/bef5a559d3f782dada282b04a51c13b5605ee3bc
Author: ellm135 (ellm13531 at gmail.com)
Date: 2025-07-21T13:08:19+02:00

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

Changed paths:
    engines/prince/POTFILES
    engines/prince/cursor.cpp
    engines/prince/detection.cpp
    engines/prince/detection.h
    engines/prince/draw.cpp
    engines/prince/inventory.cpp
    engines/prince/metaengine.cpp
    engines/prince/mob.cpp
    engines/prince/prince.cpp
    engines/prince/prince.h
    engines/prince/script.cpp
    engines/prince/sound.cpp


diff --git a/engines/prince/POTFILES b/engines/prince/POTFILES
index 70397fc7959..386505d068d 100644
--- a/engines/prince/POTFILES
+++ b/engines/prince/POTFILES
@@ -1 +1,2 @@
+engines/prince/metaengine.cpp
 engines/prince/saveload.cpp
diff --git a/engines/prince/cursor.cpp b/engines/prince/cursor.cpp
index 4d08beee207..65c79bd02b5 100644
--- a/engines/prince/cursor.cpp
+++ b/engines/prince/cursor.cpp
@@ -69,6 +69,7 @@ void PrinceEngine::changeCursor(uint16 curId) {
 		CursorMan.showMouse(false);
 		_optionsFlag = 0;
 		_selectedMob = -1;
+		_previousMob = -1;
 		return;
 	case 1:
 		curSurface = _cursor1->getSurface();
diff --git a/engines/prince/detection.cpp b/engines/prince/detection.cpp
index 70ed5e46aa6..6748c594b4b 100644
--- a/engines/prince/detection.cpp
+++ b/engines/prince/detection.cpp
@@ -45,7 +45,7 @@ static const PrinceGameDescription gameDescriptions[] = {
 			Common::DE_DEU,
 			Common::kPlatformWindows,
 			ADGF_USEEXTRAASTITLE | ADGF_DROPPLATFORM,
-			GUIO1(GUIO_NONE)
+			GUIO2(GAMEOPTION_TTS_OBJECTS, GAMEOPTION_TTS_MISSING_VOICE)
 		},
 		kPrinceDataDE
 	},
@@ -57,7 +57,7 @@ static const PrinceGameDescription gameDescriptions[] = {
 			Common::PL_POL,
 			Common::kPlatformWindows,
 			ADGF_USEEXTRAASTITLE | ADGF_DROPPLATFORM,
-			GUIO1(GUIO_NONE)
+			GUIO2(GAMEOPTION_TTS_OBJECTS, GAMEOPTION_TTS_MISSING_VOICE)
 		},
 		kPrinceDataPL
 	},
@@ -69,7 +69,7 @@ static const PrinceGameDescription gameDescriptions[] = {
 			Common::RU_RUS,
 			Common::kPlatformWindows,
 			GF_EXTRACTED | ADGF_DROPPLATFORM,
-			GUIO1(GUIO_NONE)
+			GUIO2(GAMEOPTION_TTS_OBJECTS, GAMEOPTION_TTS_MISSING_VOICE)
 		},
 		kPrinceDataDE
 	},
@@ -81,7 +81,7 @@ static const PrinceGameDescription gameDescriptions[] = {
 			Common::RU_RUS,
 			Common::kPlatformWindows,
 			GF_NOVOICES | ADGF_DROPPLATFORM,
-			GUIO1(GUIO_NONE)
+			GUIO2(GAMEOPTION_TTS_OBJECTS, GAMEOPTION_TTS_SPEECH)
 		},
 		kPrinceDataDE
 	},
@@ -93,7 +93,7 @@ static const PrinceGameDescription gameDescriptions[] = {
 			Common::RU_RUS,
 			Common::kPlatformWindows,
 			GF_RUSPROJEDITION | ADGF_USEEXTRAASTITLE | ADGF_DROPPLATFORM,
-			GUIO1(GUIO_NONE)
+			GUIO2(GAMEOPTION_TTS_OBJECTS, GAMEOPTION_TTS_MISSING_VOICE)
 		},
 		kPrinceDataDE
 	},
@@ -106,7 +106,7 @@ static const PrinceGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformWindows,
 			GF_TRANSLATED | ADGF_DROPPLATFORM,
-			GUIO1(GUIO_NONE)
+			GUIO2(GAMEOPTION_TTS_OBJECTS, GAMEOPTION_TTS_SPEECH)
 		},
 		kPrinceDataDE
 	},
@@ -119,7 +119,7 @@ static const PrinceGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformWindows,
 			GF_TRANSLATED | ADGF_DROPPLATFORM,
-			GUIO1(GUIO_NONE)
+			GUIO2(GAMEOPTION_TTS_OBJECTS, GAMEOPTION_TTS_SPEECH)
 		},
 		kPrinceDataPL
 	},
@@ -133,7 +133,7 @@ static const PrinceGameDescription gameDescriptions[] = {
 			Common::ES_ESP,
 			Common::kPlatformWindows,
 			GF_TRANSLATED | ADGF_DROPPLATFORM,
-			GUIO1(GUIO_NONE)
+			GUIO2(GAMEOPTION_TTS_OBJECTS, GAMEOPTION_TTS_SPEECH)
 		},
 		kPrinceDataDE
 	},
@@ -147,7 +147,7 @@ static const PrinceGameDescription gameDescriptions[] = {
 			Common::ES_ESP,
 			Common::kPlatformWindows,
 			GF_TRANSLATED | ADGF_DROPPLATFORM,
-			GUIO1(GUIO_NONE)
+			GUIO2(GAMEOPTION_TTS_OBJECTS, GAMEOPTION_TTS_SPEECH)
 		},
 		kPrinceDataPL
 	},
diff --git a/engines/prince/detection.h b/engines/prince/detection.h
index 49f365355e4..a9e35e498b4 100644
--- a/engines/prince/detection.h
+++ b/engines/prince/detection.h
@@ -46,6 +46,10 @@ struct PrinceGameDescription {
 	PrinceGameType gameType;
 };
 
+#define GAMEOPTION_TTS_OBJECTS				GUIO_GAMEOPTIONS1
+#define GAMEOPTION_TTS_SPEECH				GUIO_GAMEOPTIONS2
+#define GAMEOPTION_TTS_MISSING_VOICE        GUIO_GAMEOPTIONS3
+
 } // End of namespace Prince
 
 #endif // PRINCE_DETECTION_H
diff --git a/engines/prince/draw.cpp b/engines/prince/draw.cpp
index ca3aff1411a..4c37c3f55ad 100644
--- a/engines/prince/draw.cpp
+++ b/engines/prince/draw.cpp
@@ -738,6 +738,19 @@ void PrinceEngine::doTalkAnim(int animNumber, int slot, AnimType animType) {
 		correctStringDEU((char *)_interpreter->getString());
 	}
 	text._str = (const char *)_interpreter->getString();
+
+	if (slot == 9) {
+		// Location 4 has the gambling merchants, who speak frequently and can interrupt the player's
+		// dialog with other characters, so don't voice their text unless the player isn't in dialog
+		if ((!_dialogFlag && !_isConversing) || _locationNr != 4) {
+			setTTSVoice(text._color);
+			sayText(text._str, true, Common::TextToSpeechManager::QUEUE);
+		}
+	} else {
+		setTTSVoice(text._color);
+		sayText(text._str, true);
+	}
+
 	_interpreter->increaseString();
 }
 
diff --git a/engines/prince/inventory.cpp b/engines/prince/inventory.cpp
index d0412d34492..05f73d8bb2e 100644
--- a/engines/prince/inventory.cpp
+++ b/engines/prince/inventory.cpp
@@ -332,6 +332,7 @@ void PrinceEngine::inventoryLeftMouseButton() {
 	if (!_mouseFlag) {
 		_textSlots[0]._time = 0;
 		_textSlots[0]._str = nullptr;
+		stopTextToSpeech();
 		stopSample(28);
 	}
 
@@ -502,6 +503,7 @@ void PrinceEngine::checkOptions() {
 		}
 		_graph->drawAsShadowSurface(_graph->_frontScreen, _optionsX, _optionsY, _optionsPic, _graph->_shadowTable50);
 
+		int previousOption = _optionEnabled;
 		_optionEnabled = -1;
 		int optionsYCord = mousePos.y - (_optionsY + 16);
 		if (optionsYCord >= 0) {
@@ -513,11 +515,6 @@ void PrinceEngine::checkOptions() {
 		int optionsColor;
 		int textY = _optionsY + 16;
 		for (int i = 0; i < _optionsNumber; i++) {
-			if (i != _optionEnabled) {
-				optionsColor = _optionsColor1;
-			} else {
-				optionsColor = _optionsColor2;
-			}
 			Common::String optText;
 			switch(getLanguage()) {
 			case Common::PL_POL:
@@ -542,6 +539,18 @@ void PrinceEngine::checkOptions() {
 			default:
 				break;
 			};
+
+			if (i != _optionEnabled) {
+				optionsColor = _optionsColor1;
+			} else {
+				optionsColor = _optionsColor2;
+
+				if (_optionEnabled != previousOption) {
+					setTTSVoice(kHeroTextColor);
+					sayText(optText, false);
+				}
+			}
+			
 			uint16 textW = getTextWidth(optText.c_str());
 			uint16 textX = _optionsX + _optionsWidth / 2 - textW / 2;
 			_font->drawString(_graph->_frontScreen, optText, textX, textY, textW, optionsColor);
@@ -561,6 +570,7 @@ void PrinceEngine::checkInvOptions() {
 		}
 		_graph->drawAsShadowSurface(_graph->_screenForInventory, _optionsX, _optionsY, _optionsPicInInventory, _graph->_shadowTable50);
 
+		int previousOption = _optionEnabled;
 		_optionEnabled = -1;
 		int optionsYCord = mousePos.y - (_optionsY + 16);
 		if (optionsYCord >= 0) {
@@ -572,11 +582,6 @@ void PrinceEngine::checkInvOptions() {
 		int optionsColor;
 		int textY = _optionsY + 16;
 		for (int i = 0; i < _invOptionsNumber; i++) {
-			if (i != _optionEnabled) {
-				optionsColor = _optionsColor1;
-			} else {
-				optionsColor = _optionsColor2;
-			}
 			Common::String invText;
 			switch(getLanguage()) {
 			case Common::PL_POL:
@@ -602,6 +607,18 @@ void PrinceEngine::checkInvOptions() {
 				error("Unknown game language %d", getLanguage());
 				break;
 			};
+
+			if (i != _optionEnabled) {
+				optionsColor = _optionsColor1;
+			} else {
+				optionsColor = _optionsColor2;
+
+				if (_optionEnabled != previousOption) {
+					setTTSVoice(kHeroTextColor);
+					sayText(invText, false);
+				}
+			}
+
 			uint16 textW = getTextWidth(invText.c_str());
 			uint16 textX = _optionsX + _invOptionsWidth / 2 - textW / 2;
 			_font->drawString(_graph->_screenForInventory, invText, textX, textY, _graph->_screenForInventory->w, optionsColor);
diff --git a/engines/prince/metaengine.cpp b/engines/prince/metaengine.cpp
index c0ba7dd2787..b96e2c9bf02 100644
--- a/engines/prince/metaengine.cpp
+++ b/engines/prince/metaengine.cpp
@@ -19,12 +19,58 @@
  *
  */
 
+#include "common/translation.h"
+
 #include "engines/advancedDetector.h"
 #include "prince/prince.h"
 #include "prince/detection.h"
 
 namespace Prince {
 
+#ifdef USE_TTS
+
+static const ADExtraGuiOptionsMap optionsList[] = {
+	{
+		GAMEOPTION_TTS_OBJECTS,
+		{
+			_s("Enable Text to Speech for Objects and Options"),
+			_s("Use TTS to read the descriptions (if TTS is available)"),
+			"tts_enabled_objects",
+			false,
+			0,
+			0
+		}
+	},
+
+	{
+		GAMEOPTION_TTS_SPEECH,
+		{
+			_s("Enable Text to Speech for Subtitles"),
+			_s("Use TTS to read the subtitles (if TTS is available)"),
+			"tts_enabled_speech",
+			false,
+			0,
+			0
+		}
+	},
+
+	{
+		GAMEOPTION_TTS_MISSING_VOICE,
+		{
+			_s("Enable Text to Speech for Missing Voiceovers"),
+			_s("Use TTS to read the subtitles of missing voiceovers (if TTS is available)"),
+			"tts_enabled_missing_voice",
+			false,
+			0,
+			0
+		}
+	},
+
+	AD_EXTRA_GUI_OPTIONS_TERMINATOR
+};
+
+#endif
+
 int PrinceEngine::getGameType() const {
 	return _gameDescription->gameType;
 }
@@ -49,6 +95,12 @@ public:
 		return "prince";
 	}
 
+#ifdef USE_TTS
+	const ADExtraGuiOptionsMap *getAdvancedExtraGuiOptions() const override {
+		return Prince::optionsList;
+	}
+#endif
+
 	Common::Error createInstance(OSystem *syst, Engine **engine, const Prince::PrinceGameDescription *desc) const override;
 	bool hasFeature(MetaEngineFeature f) const override;
 
diff --git a/engines/prince/mob.cpp b/engines/prince/mob.cpp
index 99454506b7e..d7d3c586d39 100644
--- a/engines/prince/mob.cpp
+++ b/engines/prince/mob.cpp
@@ -242,6 +242,13 @@ int PrinceEngine::checkMob(Graphics::Surface *screen, Common::Array<Mob> &mobLis
 			}
 		}
 
+		// _selectedMob can't be used here, as it gets reset when the player clicks, causing the mob
+		// name to be voiced again
+		if (mobNumber != _previousMob) {
+			setTTSVoice(kHeroTextColor);
+			sayText(mobName, false);
+		}
+
 		uint16 textW = getTextWidth(mobName.c_str());
 
 		uint16 x = mousePos.x - textW / 2;
@@ -261,6 +268,8 @@ int PrinceEngine::checkMob(Graphics::Surface *screen, Common::Array<Mob> &mobLis
 		_font->drawString(screen, mobName, x, y, screen->w, 216);
 	}
 
+	_previousMob = mobNumber;
+
 	return mobNumber;
 }
 
diff --git a/engines/prince/prince.cpp b/engines/prince/prince.cpp
index 2b07b4faf32..b50357afb6a 100644
--- a/engines/prince/prince.cpp
+++ b/engines/prince/prince.cpp
@@ -54,6 +54,111 @@
 
 namespace Prince {
 
+#ifdef USE_TTS
+
+// Mazovia encoding
+static const uint16 polishEncodingTable[] = {
+	0x86, 0xc485, 0x8d, 0xc487, 0x8f, 0xc484, 0x90, 0xc498,	// ą, ć, Ą, Ę
+	0x91, 0xc499, 0x92, 0xc582, 0x95, 0xc486, 0x98, 0xc59a,	// ę, ł, Ć, Ś
+	0x9c, 0xc581, 0x9e, 0xc59b, 0xa0, 0xc5b9, 0xa1, 0xc5bb,	// Ł, ś, Ź, Ż
+	0xa2, 0xc3b3, 0xa3, 0xc393, 0xa4, 0xc584, 0xa5, 0xc583,	// ó, Ó, ń, Ń
+	0xa6, 0xc5ba, 0xa7, 0xc5bc,								// ź, ż
+	0
+};
+
+// Custom encoding
+static const uint16 russianEncodingTable[] = {
+	0x46, 0xd090, 0x66, 0xd0b0, 0x83, 0xd091, 0x92, 0xd0b1,	// А, а, Б, б
+	0x44, 0xd092, 0x64, 0xd0b2, 0x55, 0xd093, 0x75, 0xd0b3,	// В, в, Г, г
+	0x4c, 0xd094, 0x6c, 0xd0b4, 0x54, 0xd095, 0x74, 0xd0b5,	// Д, д, Е, е
+	0x81, 0xd096, 0x8f, 0xd0b6, 0x50, 0xd097, 0x70, 0xd0b7,	// Ж, ж, З, з
+	0x42, 0xd098, 0x62, 0xd0b8, 0x51, 0xd099, 0x71, 0xd0b9,	// И, и, Й, й
+	0x52, 0xd09a, 0x72, 0xd0ba, 0x4b, 0xd09b, 0x6b, 0xd0bb,	// К, к, Л, л
+	0x56, 0xd09c, 0x76, 0xd0bc, 0x59, 0xd09d, 0x79, 0xd0bd,	// М, м, Н, н
+	0x4a, 0xd09e, 0x6a, 0xd0be, 0x47, 0xd09f, 0x67, 0xd0bf,	// О, о, П, п
+	0x48, 0xd0a0, 0x68, 0xd180, 0x43, 0xd0a1, 0x63, 0xd181,	// Р, р, С, с
+	0x4e, 0xd0a2, 0x6e, 0xd182, 0x45, 0xd0a3, 0x65, 0xd183,	// Т, т, У, у
+	0x41, 0xd0a4, 0x61, 0xd184, 0x7f, 0xd0a5, 0x85, 0xd185,	// Ф, ф, Х, х
+	0x57, 0xd0a6, 0x77, 0xd186, 0x58, 0xd0a7, 0x78, 0xd187,	// Ц, ц, Ч, ч
+	0x49, 0xd0a8, 0x69, 0xd188, 0x4f, 0xd0a9, 0x6f, 0xd189,	// Ш, ш, Щ, щ
+	0x8d, 0xd18a, 0x53, 0xd0ab, 0x73, 0xd18b, 0x4d, 0xd0ac,	// ъ, Ы, ы, Ь
+	0x6d, 0xd18c, 0x82, 0xd0ad, 0x90, 0xd18d, 0x92, 0xd18e,	// ь, Э, э, ю
+	0x5a, 0xd0af, 0x7a, 0xd18f,								// Я, я
+	0
+};
+
+// Custom encoding
+static const uint16 spanishEncodingTable[] = {
+	0x25, 0xc3ad, 0x26, 0xc3ba, 0x35, 0xc3a1, 0x36, 0xc3a9, // í, ú, á, é
+	0x37, 0xc2bf, 0x38, 0xc3b1, 0x3b, 0xc2a1, 0x5f, 0xc3b3, // ¿, ñ, ¡, ó
+	0
+};
+
+// Custom encoding
+static const uint16 germanEncodingTable[] = {
+	0x83, 0xc384, 0x84, 0xc396, 0x85, 0xc39c, 0x7f, 0xc39f,  // Ä, Ö, Ü, ß
+	0x80, 0xc3a4, 0x81, 0xc3b6, 0x82, 0xc3bc,				 // ä, ö, ü
+	0
+};
+
+struct CharacterVoiceData {
+	uint8 textColor;
+	uint8 voiceID;
+	uint8 locationNumber;
+	int8 mobIndex;
+	bool male;
+};
+
+static const CharacterVoiceData characterVoiceData[] = {
+	{ 220, 0, 0, -1, true },	// Hero
+	{ 216, 0, 0, -1, true },	// Hover text
+	{ 211, 1, 7, -1, true },	// Bard
+	{ 211, 1, 5, 11, true },	// Bard (tavern)
+	{ 211, 2, 6, -1, true },	// Witch
+	{ 211, 3, 0, -1, true },	// Arivald (all other cases of text color 211)
+	{ 202, 4, 1, -1, true },	// Grave digger
+	{ 202, 0, 13, -1, false },	// Sheila
+	{ 253, 5, 4, -1, true },	// Tall merchant
+	{ 253, 6, 54, -1, true },	// Butcher
+	{ 225, 7, 4, -1, true },	// Thief
+	{ 225, 8, 6, -1, true },	// Alchemist
+	{ 225, 1, 7, -1, false },	// Bard's wife
+	{ 236, 9, 4, 19, true },	// Fat merchant
+	{ 236, 9, 4, 2, true },		// Fat merchant (initial town cutscene)
+	{ 236, 10, 25, 4, true },	// Dragon
+	{ 236, 2, 0, -1, false },	// Shandria (all other cases of text color 236)
+	{ 246, 11, 5, -1, true },	// Monk
+	{ 246, 12, 31, -1, true },	// Priest
+	{ 195, 13, 0, -1, true },	// Zandahan
+	{ 195, 14, 3, -1, true },	// Hermit
+	{ 252, 15, 10, -1, true },	// Gate guard
+	{ 252, 16, 12, -1, true },	// Courtyard guard
+	{ 252, 17, 30, -1, true },	// Passerby
+	{ 196, 18, 30, -1, true },	// Modern merchant
+	{ 196, 3, 5, -1, false },	// Stranger
+	{ 244, 19, 43, -1, true },	// Devil
+	{ 244, 20, 0, -1, true },	// Devil
+	{ 203, 4, 0, -1, false },	// Witch
+	{ 197, 21, 0, -1, true },	// Short merchant
+	{ 212, 22, 0, -1, true },	// Merchant cooking soup
+	{ 200, 23, 0, -1, true },	// Homunculus
+	{ 205, 24, 0, -1, true },	// Beggar
+	{ 232, 25, 0, -1, true },	// Dwarf
+	{ 208, 26, 0, -1, true },	// Barkeeper
+	{ 235, 27, 42, -1, true },	// Devil
+	{ 235, 28, 0, -1, true },	// Lord Sun
+	{ 201, 5, 0, -1, false },	// Elegant lady
+	{ 245, 29, 0, -1, true },	// Devil
+	{ 219, 30, 0, -1, true },	// Lucifer
+	{ 217, 31, 0, -1, true }	// Narrator
+};
+
+static const int kCharacterVoiceDataCount = ARRAYSIZE(characterVoiceData);
+
+#endif
+
+static const uint8 kNarratorTextColor = 217;
+
 void PrinceEngine::debugEngine(const char *s, ...) {
 	char buf[STRINGBUFLEN];
 	va_list va;
@@ -71,14 +176,14 @@ PrinceEngine::PrinceEngine(OSystem *syst, const PrinceGameDescription *gameDesc)
 	_cursor1(nullptr), _cursor2(nullptr), _cursor3(nullptr), _font(nullptr),
 	_suitcaseBmp(nullptr), _roomBmp(nullptr), _cursorNr(0), _picWindowX(0), _picWindowY(0), _randomSource("prince"),
 	_invLineX(134), _invLineY(176), _invLine(5), _invLines(3), _invLineW(70), _invLineH(76), _maxInvW(72), _maxInvH(76),
-	_invLineSkipX(2), _invLineSkipY(3), _showInventoryFlag(false), _inventoryBackgroundRemember(false),
+	_printMapNotification(false), _invLineSkipX(2), _invLineSkipY(3), _showInventoryFlag(false), _inventoryBackgroundRemember(false),
 	_mst_shadow(0), _mst_shadow2(0), _candleCounter(0), _invX1(53), _invY1(18), _invWidth(536), _invHeight(438),
 	_invCurInside(false), _optionsFlag(false), _optionEnabled(0), _invExamY(120), _invMaxCount(2), _invCounter(0),
-	_optionsMob(-1), _currentPointerNumber(1), _selectedMob(-1), _selectedItem(0), _selectedMode(0),
+	_optionsMob(-1), _currentPointerNumber(1), _selectedMob(-1), _previousMob(-1), _dialogMob(-1), _selectedItem(0), _selectedMode(0),
 	_optionsWidth(210), _optionsHeight(170), _invOptionsWidth(210), _invOptionsHeight(130), _optionsStep(20),
 	_invOptionsStep(20), _optionsNumber(7), _invOptionsNumber(5), _optionsColor1(236), _optionsColor2(252),
 	_dialogWidth(600), _dialogHeight(0), _dialogLineSpace(10), _dialogColor1(220), _dialogColor2(223),
-	_dialogFlag(false), _dialogLines(0), _dialogText(nullptr), _mouseFlag(1),
+	_dialogFlag(false), _dialogLines(0), _dialogText(nullptr), _previousSelectedDialog(-1), _isConversing(false), _mouseFlag(1),
 	_roomPathBitmap(nullptr), _roomPathBitmapTemp(nullptr), _coordsBufEnd(nullptr), _coordsBuf(nullptr), _coords(nullptr),
 	_traceLineLen(0), _rembBitmapTemp(nullptr), _rembBitmap(nullptr), _rembMask(0), _rembX(0), _rembY(0), _fpX(0), _fpY(0),
 	_checkBitmapTemp(nullptr), _checkBitmap(nullptr), _checkMask(0), _checkX(0), _checkY(0), _traceLineFirstPointFlag(false),
@@ -86,7 +191,7 @@ PrinceEngine::PrinceEngine(OSystem *syst, const PrinceGameDescription *gameDesc)
 	_shanLen(0), _directionTable(nullptr), _currentMidi(0), _lightX(0), _lightY(0), _curveData(nullptr), _curvPos(0),
 	_creditsData(nullptr), _creditsDataSize(0), _currentTime(0), _zoomBitmap(nullptr), _shadowBitmap(nullptr), _transTable(nullptr),
 	_flcFrameSurface(nullptr), _shadScaleValue(0), _shadLineLen(0), _scaleValue(0), _dialogImage(nullptr), _mobTranslationData(nullptr),
-	_mobTranslationSize(0), _missingVoice(false) {
+	_mobTranslationSize(0), _missingVoice(false), _intro(false), _credits(false) {
 
 	DebugMan.enableDebugChannel("script");
 
@@ -393,6 +498,12 @@ void PrinceEngine::init() {
 	if (getFeatures() & GF_TRANSLATED) {
 		loadMobTranslationTexts();
 	}
+
+	Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+	if (ttsMan != nullptr) {
+		ttsMan->enable(ConfMan.getBool("tts_enabled_objects") || ConfMan.getBool("tts_enabled_speech") || ConfMan.getBool("tts_enabled_missing_voice"));
+		ttsMan->setLanguage(ConfMan.get("language"));
+	}
 }
 
 void PrinceEngine::showLogo() {
@@ -437,6 +548,7 @@ Common::Error PrinceEngine::run() {
 	int startGameSlot = ConfMan.hasKey("save_slot") ? ConfMan.getInt("save_slot") : -1;
 	init();
 	if (startGameSlot == -1) {
+		_intro = true;
 		playVideo("topware.avi");
 		showLogo();
 	} else {
@@ -549,6 +661,11 @@ void PrinceEngine::keyHandler(Common::Event event) {
 		}
 		break;
 	case Common::KEYCODE_ESCAPE:
+		if (_intro) {
+			stopTextToSpeech();
+			_intro = false;
+		}
+
 		_flags->setFlagValue(Flags::ESCAPED2, 1);
 		break;
 	default:
@@ -562,6 +679,34 @@ void PrinceEngine::printAt(uint32 slot, uint8 color, char *s, uint16 x, uint16 y
 	if (getLanguage() == Common::DE_DEU)
 		correctStringDEU(s);
 
+	// Cutscene
+	if (slot == 9) {
+		setTTSVoice(color);
+		sayText(s, true, Common::TextToSpeechManager::QUEUE);
+	} else {
+		bool printText = true;
+		bool isSpeech = false;
+
+		if (slot == 10) {
+			if (_locationNr == 50) {	// Map
+				if (_printMapNotification) {
+					_printMapNotification = false;
+				} else {
+					printText = false;
+				}
+			} else {
+				isSpeech = true;
+			}
+		} else if (slot == 0) {
+			isSpeech = true;
+		}
+
+		if (printText) {
+			setTTSVoice(color);
+			sayText(s, isSpeech);
+		}
+	}
+
 	Text &text = _textSlots[slot];
 	text._str = s;
 	text._x = x;
@@ -627,6 +772,8 @@ uint32 PrinceEngine::getTextWidth(const char *s) {
 }
 
 void PrinceEngine::showTexts(Graphics::Surface *screen) {
+	Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+
 	for (uint32 slot = 0; slot < kMaxTexts; slot++) {
 
 		if (_showInventoryFlag && slot) {
@@ -682,11 +829,178 @@ void PrinceEngine::showTexts(Graphics::Surface *screen) {
 
 		text._time--;
 		if (!text._time) {
+			if (ttsMan != nullptr && (ConfMan.getBool("tts_enabled_speech") || 
+				ConfMan.getBool("tts_enabled_objects") || ConfMan.getBool("tts_enabled_missing_voice")) && ttsMan->isSpeaking()) {
+				text._time = 1;
+				continue;
+			}
 			text._str = nullptr;
 		}
 	}
 }
 
+void PrinceEngine::sayText(const Common::String &text, bool isSpeech, Common::TextToSpeechManager::Action action) {
+	Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+	// Only voice subtitles if either this is a version with no voices or the speech volume is muted (the English/Spanish
+	// translations still have dubs in different languages, so don't voice the subtitles unless the dub is muted)
+	bool speak = (!isSpeech && ConfMan.getBool("tts_enabled_objects")) || 
+				 (isSpeech && ConfMan.getBool("tts_enabled_speech") && 
+				 (getFeatures() & GF_NOVOICES || ConfMan.getInt("speech_volume") == 0 || ConfMan.getBool("subtitles"))); 
+	if (ttsMan != nullptr && speak) {
+		Common::String ttsText(text);
+		// Some emotive text has a < at the front, which causes the entire text to not be voiced by the TTS system
+		// Text with quotation marks also contains \ as an escape character, which is awkwardly voiced by TTS if not
+		// removed
+		ttsText.replace('\n', ' ');
+		ttsText.replace('<', ' ');
+		ttsText.replace('\\', ' ');
+#ifdef USE_TTS
+		ttsMan->say(convertText(ttsText), action);
+#endif
+	}
+}
+
+#ifdef USE_TTS
+
+Common::U32String PrinceEngine::convertText(const Common::String &text) const {
+	const uint16 *conversionTable;
+
+	switch (getLanguage()) {
+	case Common::EN_ANY:	// Some of the English text has a few Polish characters
+	case Common::PL_POL:
+		conversionTable = polishEncodingTable;
+		break;
+	case Common::RU_RUS:
+		if (getFeatures() & GF_RUSPROJEDITION) {
+			return Common::U32String(text, Common::CodePage::kDos866);
+		}
+
+		conversionTable = russianEncodingTable;
+		break;
+	case Common::DE_DEU:
+		conversionTable = germanEncodingTable;
+		break;
+	case Common::ES_ESP:
+		conversionTable = spanishEncodingTable;
+		break;
+	default:
+		conversionTable = polishEncodingTable;
+	}
+
+	const byte *bytes = (const byte *)text.c_str();
+	byte *convertedBytes = new byte[text.size() * 2 + 1];
+
+	int i = 0;
+	for (const byte *b = bytes; *b; ++b) {
+		bool inTable = checkConversionTable(b, i, convertedBytes, conversionTable);
+		
+		if (_credits && !inTable) {
+			if (*b == 0x2a) {	// * in credits
+				convertedBytes[i] = 0x20;
+				i++;
+				continue;
+			}
+
+			if (*b == 0x23) {
+				i++;
+				break;
+			}
+
+			// Credits in other languages may have some Polish characters
+			inTable = checkConversionTable(b, i, convertedBytes, polishEncodingTable);
+		}
+
+		if (!inTable) {
+			convertedBytes[i] = *b;
+			i++;
+		}
+	}
+
+	convertedBytes[i] = 0;
+
+	Common::U32String result((char *)convertedBytes);
+	delete[] convertedBytes;
+
+	return result;
+}
+
+bool PrinceEngine::checkConversionTable(const byte *character, int &index, byte *convertedBytes, const uint16 *table) const {
+	for (int i = 0; table[i]; i += 2) {
+		if (*character == table[i]) {
+			convertedBytes[index] = (table[i + 1] >> 8) & 0xff;
+			convertedBytes[index + 1] = table[i + 1] & 0xff;
+			index += 2;
+			return true;
+		}
+	}
+
+	return false;
+}
+
+#endif
+
+void PrinceEngine::setTTSVoice(uint8 textColor) const {
+#ifdef USE_TTS
+	Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+	if (ttsMan != nullptr && (ConfMan.getBool("tts_enabled_speech") || ConfMan.getBool("tts_enabled_objects") || ConfMan.getBool("tts_enabled_missing_voice"))) {
+		int id = 0;
+
+		for (int i = 0; i < kCharacterVoiceDataCount; ++i) {
+			// In many cases, characters can be differentiated by just the text color, but sometimes
+			// there may be different characters with the same text colors in different locations, and rarely
+			// different characters with the same text colors in the same location. Using the location number and/or
+			// mob index differentiates characters in these cases
+			if (characterVoiceData[i].textColor == textColor && 
+				(characterVoiceData[i].locationNumber == 0 || characterVoiceData[i].locationNumber == _locationNr) &&
+				(characterVoiceData[i].mobIndex == -1 || characterVoiceData[i].mobIndex == _dialogMob)) {
+				id = i;
+				break;
+			}
+		}
+
+		Common::Array<int> voices;
+		int pitch = 0;
+		Common::TTSVoice::Gender gender;
+
+		if (characterVoiceData[id].male) {
+			voices = ttsMan->getVoiceIndicesByGender(Common::TTSVoice::MALE);
+			gender = Common::TTSVoice::MALE;
+		} else {
+			voices = ttsMan->getVoiceIndicesByGender(Common::TTSVoice::FEMALE);
+			gender = Common::TTSVoice::FEMALE;
+		}
+
+		// If no voice is available for the necessary gender, set the voice to default
+		if (voices.empty()) {
+			ttsMan->setVoice(0);
+		} else {
+			int voiceIndex = characterVoiceData[id].voiceID % voices.size();
+			ttsMan->setVoice(voices[voiceIndex]);
+		}
+
+		// If no voices are available for this gender, alter the pitch to mimic a voice
+		// of the other gender
+		if (ttsMan->getVoice().getGender() != gender) {
+			if (gender == Common::TTSVoice::MALE) {
+				pitch -= 50;
+			} else {
+				pitch += 50;
+			}
+		}
+
+		ttsMan->setPitch(pitch);
+	}
+#endif
+}
+
+void PrinceEngine::stopTextToSpeech() const {
+	Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+	if (ttsMan != nullptr && (ConfMan.getBool("tts_enabled_objects") || ConfMan.getBool("tts_enabled_speech") || ConfMan.getBool("tts_enabled_missing_voice")) &&
+		ttsMan->isSpeaking()) {
+		ttsMan->stop();
+	}
+}
+
 void PrinceEngine::pausePrinceEngine(int fps) {
 	int delay = 1000 / fps - int32(_system->getMillis() - _currentTime);
 	delay = delay < 0 ? 0 : delay;
@@ -755,9 +1069,15 @@ void PrinceEngine::leftMouseButton() {
 		}
 		_interpreter->storeNewPC(optionEvent);
 		_flags->setFlagValue(Flags::CURRMOB, _selectedMob);
+		_dialogMob = _selectedMob;
 		_selectedMob = -1;
 		_optionsMob = -1;
 	} else {
+		if (_intro) {
+			stopTextToSpeech();
+			_intro = false;
+		}
+
 		if (!_flags->getFlagValue(Flags::POWERENABLED)) {
 			if (!_flags->getFlagValue(Flags::NOCLSTEXT)) {
 				for (int slot = 0; slot < kMaxTexts; slot++) {
@@ -766,6 +1086,7 @@ void PrinceEngine::leftMouseButton() {
 						if (!text._str) {
 							continue;
 						}
+						stopTextToSpeech();
 						text._str = nullptr;
 						text._time = 0;
 					}
@@ -826,6 +1147,7 @@ void PrinceEngine::createDialogBox(int dialogBoxNr) {
 void PrinceEngine::dialogRun() {
 
 	_dialogFlag = true;
+	setTTSVoice(kHeroTextColor);
 
 	while (!shouldQuit()) {
 
@@ -867,6 +1189,11 @@ void PrinceEngine::dialogRun() {
 					actualColor = _dialogColor2;
 					dialogSelected = sentenceNumber;
 					dialogCurrentText = dialogText;
+
+					if (_previousSelectedDialog != dialogSelected) {
+						sayText((const char *)dialogCurrentText, false);
+						_previousSelectedDialog = dialogSelected;
+					}
 				}
 
 				for (uint j = 0; j < lines.size(); j++) {
@@ -881,6 +1208,10 @@ void PrinceEngine::dialogRun() {
 			} while (c);
 		}
 
+		if (dialogSelected == -1) {
+			_previousSelectedDialog = -1;
+		}
+
 		Common::Event event;
 		Common::EventManager *eventMan = _system->getEventManager();
 		while (eventMan->pollEvent(event)) {
@@ -959,6 +1290,10 @@ void PrinceEngine::talkHero(int slot) {
 		correctStringDEU((char *)_interpreter->getString());
 	}
 	text._str = (const char *)_interpreter->getString();
+
+	setTTSVoice(text._color);
+	sayText(text._str, true);
+
 	_interpreter->increaseString();
 }
 
@@ -1053,6 +1388,14 @@ void PrinceEngine::showPower() {
 
 void PrinceEngine::scrollCredits() {
 	byte *scrollAdress = _creditsData;
+
+	_credits = true;
+	setTTSVoice(kNarratorTextColor);
+	if (getLanguage() == Common::DE_DEU) {
+		correctStringDEU((char *)scrollAdress);
+	}
+	sayText((char *)scrollAdress, false, Common::TextToSpeechManager::INTERRUPT);
+
 	while (!shouldQuit()) {
 		for (int scrollPos = 0; scrollPos > -23; scrollPos--) {
 			const Graphics::Surface *roomSurface = _roomBmp->getSurface();
diff --git a/engines/prince/prince.h b/engines/prince/prince.h
index 2f29d4e1fd2..573ff2f9650 100644
--- a/engines/prince/prince.h
+++ b/engines/prince/prince.h
@@ -27,6 +27,7 @@
 #include "common/debug.h"
 #include "common/debug-channels.h"
 #include "common/textconsole.h"
+#include "common/text-to-speech.h"
 #include "common/rect.h"
 #include "common/events.h"
 #include "common/endian.h"
@@ -267,6 +268,8 @@ enum Type {
 
 };
 
+static const uint8 kHeroTextColor = 220;
+
 class PrinceEngine : public Engine {
 protected:
 	Common::Error run() override;
@@ -343,6 +346,14 @@ public:
 	int calcTextTime(int numberOfLines);
 	void correctStringDEU(char *s);
 
+	void sayText(const Common::String &text, bool isSpeech, Common::TextToSpeechManager::Action action = Common::TextToSpeechManager::INTERRUPT);
+#ifdef USE_TTS
+	Common::U32String convertText(const Common::String &text) const;
+	bool checkConversionTable(const byte *character, int &index, byte *convertedBytes, const uint16 *table) const;
+#endif
+	void setTTSVoice(uint8 textColor) const;
+	void stopTextToSpeech() const;
+
 	static const uint8 kMaxTexts = 32;
 	Text _textSlots[kMaxTexts];
 
@@ -361,6 +372,10 @@ public:
 	int32 _picWindowX;
 	int32 _picWindowY;
 
+	bool _printMapNotification;
+	bool _intro;
+	bool _credits;
+
 	Image::BitmapDecoder *_roomBmp;
 	MhwanhDecoder *_suitcaseBmp;
 	Room *_room;
@@ -425,6 +440,8 @@ public:
 	void grabMap();
 
 	int _selectedMob; // number of selected Mob / inventory item
+	int _previousMob;
+	int _dialogMob;
 	int _selectedItem; // number of item on mouse cursor
 	int _selectedMode;
 	int _currentPointerNumber;
@@ -517,6 +534,8 @@ public:
 	int _dialogLineSpace;
 	int _dialogColor1; // color for non-selected options
 	int _dialogColor2; // color for selected option
+	int _previousSelectedDialog;
+	bool _isConversing;
 	Graphics::Surface *_dialogImage;
 
 	void createDialogBox(int dialogBoxNr);
diff --git a/engines/prince/script.cpp b/engines/prince/script.cpp
index ffef1724ce2..fb4d0bedca9 100644
--- a/engines/prince/script.cpp
+++ b/engines/prince/script.cpp
@@ -608,6 +608,7 @@ void Interpreter::O_SETUPPALETTE() {
 void Interpreter::O_INITROOM() {
 	int32 roomId = readScriptFlagValue();
 	debugInterpreter("O_INITROOM %d", roomId);
+	_vm->_printMapNotification = true;
 	_vm->loadLocation(roomId);
 	_opcodeNF = 1;
 }
@@ -872,6 +873,8 @@ void Interpreter::O_CHANGECURSOR() {
 	int32 cursorId = readScriptFlagValue();
 	debugInterpreter("O_CHANGECURSOR %x", cursorId);
 	_vm->changeCursor(cursorId);
+
+	_vm->_isConversing = (cursorId == 0);
 }
 
 // Not used in script
@@ -1108,6 +1111,9 @@ void Interpreter::O_HEROON() {
 void Interpreter::O_CLSTEXT() {
 	int32 slot = readScriptFlagValue();
 	debugInterpreter("O_CLSTEXT slot %d", slot);
+	if (slot == 0) {
+		_vm->stopTextToSpeech();
+	}
 	_vm->_textSlots[slot]._str = nullptr;
 	_vm->_textSlots[slot]._time = 0;
 }
diff --git a/engines/prince/sound.cpp b/engines/prince/sound.cpp
index 19fe48ab7d0..94bb0d6a8c5 100644
--- a/engines/prince/sound.cpp
+++ b/engines/prince/sound.cpp
@@ -20,6 +20,7 @@
  */
 
 #include "common/archive.h"
+#include "common/config-manager.h"
 
 #include "audio/audiostream.h"
 #include "audio/decoders/wave.h"
@@ -108,6 +109,12 @@ bool PrinceEngine::loadVoice(uint32 slot, uint32 sampleSlot, const Common::Strin
 		_missingVoice = true;	// Insert END tag if needed
 		_textSlots[slot]._time = 1; // Set phrase time to none
 		_mainHero->_talkTime = 1;
+
+		// Speak missing voice clips like objects
+		if (_textSlots[slot]._str && (ConfMan.getBool("tts_enabled_missing_voice") || ConfMan.getBool("tts_enabled_speech"))) {
+			sayText(_textSlots[slot]._str, false);
+		}
+
 		return false;
 	}
 




More information about the Scummvm-git-logs mailing list