[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