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