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

sev- noreply at scummvm.org
Sun Aug 10 20:38: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:
ebe27c7fa3 MM: Add text-to-speech (TTS)


Commit: ebe27c7fa312699ef2790038bc360fbbd7d73d4c
    https://github.com/scummvm/scummvm/commit/ebe27c7fa312699ef2790038bc360fbbd7d73d4c
Author: ellm135 (ellm13531 at gmail.com)
Date: 2025-08-10T22:38:19+02:00

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

Changed paths:
    engines/mm/detection.h
    engines/mm/detection_tables.h
    engines/mm/metaengine.cpp
    engines/mm/mm1/mm1.cpp
    engines/mm/mm1/views/text_view.cpp
    engines/mm/xeen/dialogs/credits_screen.cpp
    engines/mm/xeen/dialogs/credits_screen.h
    engines/mm/xeen/dialogs/dialogs.cpp
    engines/mm/xeen/dialogs/dialogs.h
    engines/mm/xeen/dialogs/dialogs_awards.cpp
    engines/mm/xeen/dialogs/dialogs_char_info.cpp
    engines/mm/xeen/dialogs/dialogs_char_info.h
    engines/mm/xeen/dialogs/dialogs_control_panel.cpp
    engines/mm/xeen/dialogs/dialogs_control_panel.h
    engines/mm/xeen/dialogs/dialogs_copy_protection.cpp
    engines/mm/xeen/dialogs/dialogs_copy_protection.h
    engines/mm/xeen/dialogs/dialogs_create_char.cpp
    engines/mm/xeen/dialogs/dialogs_create_char.h
    engines/mm/xeen/dialogs/dialogs_difficulty.cpp
    engines/mm/xeen/dialogs/dialogs_dismiss.cpp
    engines/mm/xeen/dialogs/dialogs_info.cpp
    engines/mm/xeen/dialogs/dialogs_info.h
    engines/mm/xeen/dialogs/dialogs_input.cpp
    engines/mm/xeen/dialogs/dialogs_items.cpp
    engines/mm/xeen/dialogs/dialogs_items.h
    engines/mm/xeen/dialogs/dialogs_map.cpp
    engines/mm/xeen/dialogs/dialogs_message.cpp
    engines/mm/xeen/dialogs/dialogs_party.cpp
    engines/mm/xeen/dialogs/dialogs_party.h
    engines/mm/xeen/dialogs/dialogs_query.cpp
    engines/mm/xeen/dialogs/dialogs_quests.cpp
    engines/mm/xeen/dialogs/dialogs_quick_fight.cpp
    engines/mm/xeen/dialogs/dialogs_quick_fight.h
    engines/mm/xeen/dialogs/dialogs_quick_ref.cpp
    engines/mm/xeen/dialogs/dialogs_quick_ref.h
    engines/mm/xeen/dialogs/dialogs_spells.cpp
    engines/mm/xeen/dialogs/dialogs_spells.h
    engines/mm/xeen/dialogs/dialogs_whowill.cpp
    engines/mm/xeen/dialogs/dialogs_whowill.h
    engines/mm/xeen/events.cpp
    engines/mm/xeen/font.cpp
    engines/mm/xeen/font.h
    engines/mm/xeen/interface.cpp
    engines/mm/xeen/interface.h
    engines/mm/xeen/locations.cpp
    engines/mm/xeen/locations.h
    engines/mm/xeen/party.cpp
    engines/mm/xeen/scripts.cpp
    engines/mm/xeen/subtitles.cpp
    engines/mm/xeen/swordsofxeen/swordsofxeen_menu.cpp
    engines/mm/xeen/window.h
    engines/mm/xeen/worldofxeen/clouds_cutscenes.cpp
    engines/mm/xeen/worldofxeen/worldofxeen_menu.cpp
    engines/mm/xeen/worldofxeen/worldofxeen_menu.h
    engines/mm/xeen/xeen.cpp
    engines/mm/xeen/xeen.h


diff --git a/engines/mm/detection.h b/engines/mm/detection.h
index 6b8d9605a8c..850fb39d874 100644
--- a/engines/mm/detection.h
+++ b/engines/mm/detection.h
@@ -53,6 +53,7 @@ struct MightAndMagicGameDescription {
 #define GAMEOPTION_DURABLE_ARMOR	GUIO_GAMEOPTIONS2
 #define GAMEOPTION_SHOW_HP_SP_BARS	GUIO_GAMEOPTIONS3
 #define GAMEOPTION_COPY_PROTECTION      GUIO_GAMEOPTIONS4
+#define GAMEOPTION_TTS				GUIO_GAMEOPTIONS5
 
 } // namespace MM
 
diff --git a/engines/mm/detection_tables.h b/engines/mm/detection_tables.h
index 0c200477213..e332145354c 100644
--- a/engines/mm/detection_tables.h
+++ b/engines/mm/detection_tables.h
@@ -24,7 +24,7 @@
 
 namespace MM {
 
-#define GUIO_XEEN GUIO4(GAMEOPTION_SHOW_ITEM_COSTS, GAMEOPTION_DURABLE_ARMOR, GAMEOPTION_SHOW_HP_SP_BARS, GAMEOPTION_COPY_PROTECTION)
+#define GUIO_XEEN GUIO5(GAMEOPTION_SHOW_ITEM_COSTS, GAMEOPTION_DURABLE_ARMOR, GAMEOPTION_SHOW_HP_SP_BARS, GAMEOPTION_COPY_PROTECTION, GAMEOPTION_TTS)
 
 static const MightAndMagicGameDescription GAME_DESCRIPTIONS[] = {
 	{
@@ -36,7 +36,7 @@ static const MightAndMagicGameDescription GAME_DESCRIPTIONS[] = {
 			Common::EN_ANY,
 			Common::kPlatformDOS,
 			ADGF_NO_FLAGS,
-			GUIO0()
+			GUIO1(GAMEOPTION_TTS)
 		},
 		GType_MightAndMagic1,
 		0
@@ -51,7 +51,7 @@ static const MightAndMagicGameDescription GAME_DESCRIPTIONS[] = {
 			Common::EN_ANY,
 			Common::kPlatformDOS,
 			ADGF_NO_FLAGS,
-			GUIO0()
+			GUIO1(GAMEOPTION_TTS)
 		},
 		GType_MightAndMagic1,
 		GF_ENHANCED
@@ -68,7 +68,7 @@ static const MightAndMagicGameDescription GAME_DESCRIPTIONS[] = {
 			Common::EN_ANY,
 			Common::kPlatformDOS,
 			ADGF_NO_FLAGS,
-			GUIO0()
+			GUIO1(GAMEOPTION_TTS)
 		},
 		GType_MightAndMagic1,
 		GF_GFX_PACK
diff --git a/engines/mm/metaengine.cpp b/engines/mm/metaengine.cpp
index 1a4c1690fd8..6a96fcaeeac 100644
--- a/engines/mm/metaengine.cpp
+++ b/engines/mm/metaengine.cpp
@@ -84,6 +84,19 @@ 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/mm/mm1/mm1.cpp b/engines/mm/mm1/mm1.cpp
index b9894a2b7a4..f942a7e26fe 100644
--- a/engines/mm/mm1/mm1.cpp
+++ b/engines/mm/mm1/mm1.cpp
@@ -24,6 +24,7 @@
 #include "common/debug-channels.h"
 #include "common/file.h"
 #include "common/system.h"
+#include "common/text-to-speech.h"
 #include "common/translation.h"
 #include "engines/util.h"
 #include "graphics/paletteman.h"
@@ -73,6 +74,14 @@ Common::Error MM1Engine::run() {
 	_sound = new Sound(_mixer);
 	syncSoundSettings();
 
+#ifdef USE_TTS
+	Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+	if (ttsMan != nullptr) {
+		ttsMan->enable(ConfMan.getBool("tts_enabled"));
+		ttsMan->setLanguage(ConfMan.get("language"));
+	}
+#endif
+
 	// Setup console
 	setDebugger(new Console());
 	if (gDebugLevel > 0)
diff --git a/engines/mm/mm1/views/text_view.cpp b/engines/mm/mm1/views/text_view.cpp
index 6884348916c..cfdaea97992 100644
--- a/engines/mm/mm1/views/text_view.cpp
+++ b/engines/mm/mm1/views/text_view.cpp
@@ -19,6 +19,8 @@
  *
  */
 
+#include "common/text-to-speech.h"
+
 #include "mm/mm1/globals.h"
 #include "mm/mm1/gfx/gfx.h"
 #include "mm/mm1/views/text_view.h"
@@ -59,6 +61,13 @@ void TextView::writeChar(int x, int y, unsigned char c) {
 }
 
 void TextView::writeString(const Common::String &str) {
+#ifdef USE_TTS
+	Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+	if (ttsMan && ConfMan.getBool("tts_enabled")) {
+		ttsMan->say(str, Common::CodePage::kDos850);
+	}
+#endif
+
 	for (const unsigned char *s = (const unsigned char *)str.c_str(); *s; ++s) {
 		writeChar(*s);
 
diff --git a/engines/mm/xeen/dialogs/credits_screen.cpp b/engines/mm/xeen/dialogs/credits_screen.cpp
index 6acb382da5b..0c0b85d7637 100644
--- a/engines/mm/xeen/dialogs/credits_screen.cpp
+++ b/engines/mm/xeen/dialogs/credits_screen.cpp
@@ -55,9 +55,14 @@ void CreditsScreen::execute(const char *content) {
 	windows[GAME_WINDOW].close();
 
 	screen.loadBackground("marb.raw");
-	windows[0].writeString(content);
+	Common::String ttsMessage;
+	windows[0].writeString(content, false, &ttsMessage);
 	doScroll(false, false);
 
+#ifdef USE_TTS
+	speakText(ttsMessage, (_vm->getGameID() != GType_Swords || content == Res.SWORDS_CREDITS1));
+#endif
+
 	events.setCursor(0);
 	windows[0].update();
 	clearButtons();
@@ -66,8 +71,60 @@ void CreditsScreen::execute(const char *content) {
 	while (!_vm->shouldExit() && !events.isKeyMousePressed())
 		events.pollEventsAndWait();
 
+#ifdef USE_TTS
+	_vm->stopTextToSpeech();
+#endif
 	doScroll(true, false);
 }
 
+#ifdef USE_TTS
+
+void CreditsScreen::speakText(const Common::String &text, bool firstCreditsScreen) const {
+	if (_vm->getGameID() == GType_Swords && firstCreditsScreen) {
+		uint index = 0;
+		// Developed/published by
+		_vm->sayText(getNextTextSection(text, index, 2));
+
+		// Next four headers are separate from their corresponding credits. First get the headers, then voice the person
+		// for the first header, and then voice the second header and second person
+		for (uint8 i = 0; i < 2; ++i) {
+			_vm->sayText(getNextTextSection(text, index));
+			Common::String nextHeader = getNextTextSection(text, index);
+			_vm->sayText(getNextTextSection(text, index));
+			_vm->sayText(nextHeader);
+			_vm->sayText(getNextTextSection(text, index));
+		}
+
+		// Same as first four headers, but with two people at a time instead of one
+		for (uint8 i = 0; i < 2; ++i) {
+			// First two headers
+			_vm->sayText(getNextTextSection(text, index));
+			Common::String nextHeader = getNextTextSection(text, index);
+
+			// First people listed
+			_vm->sayText(getNextTextSection(text, index));
+			Common::String nextHeaderPerson = getNextTextSection(text, index);
+
+			// Second person listed under first header
+			_vm->sayText(getNextTextSection(text, index));
+
+			// Next header
+			_vm->sayText(nextHeader);
+			_vm->sayText(nextHeaderPerson);
+
+			// Second person for second header
+			if (i == 0) {
+				_vm->sayText(getNextTextSection(text, index));
+			} else {	// Last header has more than two people
+				_vm->sayText(text.substr(index));
+			}
+		}
+	} else {
+		_vm->sayText(text);
+	}
+}
+
+#endif
+
 } // End of namespace Xeen
 } // End of namespace MM
diff --git a/engines/mm/xeen/dialogs/credits_screen.h b/engines/mm/xeen/dialogs/credits_screen.h
index d323a41720b..f3155838ab6 100644
--- a/engines/mm/xeen/dialogs/credits_screen.h
+++ b/engines/mm/xeen/dialogs/credits_screen.h
@@ -32,6 +32,10 @@ private:
 	CreditsScreen(XeenEngine *vm) : ButtonContainer(vm) {}
 
 	void execute(const char *content);
+
+#ifdef USE_TTS
+	void speakText(const Common::String &text, bool firstCreditsScreen) const;
+#endif
 public:
 	static void show(XeenEngine *vm);
 };
diff --git a/engines/mm/xeen/dialogs/dialogs.cpp b/engines/mm/xeen/dialogs/dialogs.cpp
index b246ba8d256..f7e21bf0038 100644
--- a/engines/mm/xeen/dialogs/dialogs.cpp
+++ b/engines/mm/xeen/dialogs/dialogs.cpp
@@ -46,13 +46,13 @@ void ButtonContainer::restoreButtons() {
 }
 
 void ButtonContainer::addButton(const Common::Rect &bounds, int val,
-		SpriteResource *sprites) {
-	_buttons.push_back(UIButton(bounds, val, _buttons.size() * 2, sprites, sprites != nullptr));
+		SpriteResource *sprites, uint8 ttsIndex) {
+	_buttons.push_back(UIButton(bounds, val, _buttons.size() * 2, sprites, sprites != nullptr, ttsIndex, ttsIndex < UINT8_MAX));
 }
 
 void ButtonContainer::addButton(const Common::Rect &bounds, int val,
-		int frameNum, SpriteResource *sprites) {
-	_buttons.push_back(UIButton(bounds, val, frameNum, sprites, sprites != nullptr));
+		int frameNum, SpriteResource *sprites, uint8 ttsIndex) {
+	_buttons.push_back(UIButton(bounds, val, frameNum, sprites, sprites != nullptr, ttsIndex, ttsIndex < UINT8_MAX));
 }
 
 void ButtonContainer::addPartyButtons(XeenEngine *vm) {
@@ -69,6 +69,11 @@ bool ButtonContainer::checkEvents(XeenEngine *vm) {
 	PendingEvent event;
 	_buttonValue = 0;
 
+#ifdef USE_TTS
+	bool buttonKeyboardPressed = false;
+	checkHoverOverButton();
+#endif
+
 	if (events.getEvent(event)) {
 		if (event._leftButton) {
 			Common::Point pt = events._mousePos;
@@ -111,8 +116,12 @@ bool ButtonContainer::checkEvents(XeenEngine *vm) {
 					&& keycode != Common::KEYCODE_LALT && keycode != Common::KEYCODE_RALT)
 				_buttonValue = keycode;
 
-			if (_buttonValue)
+			if (_buttonValue) {
 				_buttonValue |= (event._keyState.flags & ~Common::KBD_STICKY) << 16;
+#ifdef USE_TTS
+				buttonKeyboardPressed = true;
+#endif
+			}
 		}
 	}
 
@@ -121,22 +130,32 @@ bool ButtonContainer::checkEvents(XeenEngine *vm) {
 		Window &win = windows[39];
 		for (uint btnIndex = 0; btnIndex < _buttons.size(); ++btnIndex) {
 			UIButton &btn = _buttons[btnIndex];
-			if (btn._draw && btn._value == _buttonValue) {
-				// Found the correct button
-				// Draw button depressed
-				btn._sprites->draw(0, btn._selectedFrame, Common::Point(btn._bounds.left, btn._bounds.top));
-				win.setBounds(btn._bounds);
-				win.update();
-
-				// Slight delay
-				events.updateGameCounter();
-				events.wait(2);
-
-				// Redraw button in it's original non-depressed form
-				btn._sprites->draw(0, btn._frameNum, Common::Point(btn._bounds.left, btn._bounds.top));
-				win.setBounds(btn._bounds);
-				win.update();
-				break;
+			if (btn._value == _buttonValue) {
+#ifdef USE_TTS
+				// Only voice the button's text if it was activated by keyboard press, to avoid voicing it
+				// every time the player clicks it
+				if (buttonKeyboardPressed && btn._canVoice && btn._ttsIndex < _buttonTexts.size()) {
+					_vm->sayText(_buttonTexts[btn._ttsIndex], Common::TextToSpeechManager::INTERRUPT);
+				}
+#endif
+
+				if (btn._draw) {
+					// Found the correct button
+					// Draw button depressed
+					btn._sprites->draw(0, btn._selectedFrame, Common::Point(btn._bounds.left, btn._bounds.top));
+					win.setBounds(btn._bounds);
+					win.update();
+
+					// Slight delay
+					events.updateGameCounter();
+					events.wait(2);
+
+					// Redraw button in it's original non-depressed form
+					btn._sprites->draw(0, btn._frameNum, Common::Point(btn._bounds.left, btn._bounds.top));
+					win.setBounds(btn._bounds);
+					win.update();
+					break;
+				}
 			}
 		}
 
@@ -146,6 +165,37 @@ bool ButtonContainer::checkEvents(XeenEngine *vm) {
 	return false;
 }
 
+#ifdef USE_TTS
+
+void ButtonContainer::checkHoverOverButton() {
+	if (g_vm->_mouseMoved) {
+		bool hoveringOverButton = false;
+
+		Common::Point pt = g_vm->_events->_mousePos;
+
+		for (uint i = 0; i < _buttons.size(); ++i) {
+			if (_buttons[i]._canVoice && _buttons[i]._ttsIndex < _buttonTexts.size() && _buttons[i]._bounds.contains(pt) && _buttons[i]._value) {
+				hoveringOverButton = true;
+
+				if (_previousButton != (int)i) {
+					_vm->sayText(_buttonTexts[_buttons[i]._ttsIndex], Common::TextToSpeechManager::INTERRUPT);
+					_previousButton = (int)i;
+				}
+
+				break;
+			}
+		}
+
+		if (!hoveringOverButton) {
+			_previousButton = -1;
+		}
+
+		g_vm->_mouseMoved = false;
+	}
+}
+
+#endif
+
 void ButtonContainer::drawButtons(XSurface *surface) {
 	for (uint btnIndex = 0; btnIndex < _buttons.size(); ++btnIndex) {
 		UIButton &btn = _buttons[btnIndex];
@@ -189,6 +239,99 @@ void ButtonContainer::setWaitBounds() {
 	_waitBounds = Common::Rect(8, 8, 224, 140);
 }
 
+#ifdef USE_TTS
+
+Common::String ButtonContainer::getNextTextSection(const Common::String &text, uint &index, uint count, const char *separator) const {
+	Common::String result;
+	for (uint i = 0; i < count; ++i) {
+		result += getNextTextSection(text, index) + separator;
+
+		if (index == Common::String::npos) {
+			break;
+		}
+	}
+	
+	return result;
+}
+
+Common::String ButtonContainer::getNextTextSection(const Common::String &text, uint &index, uint count) const {
+	return getNextTextSection(text, index, count, "\n");
+}
+
+Common::String ButtonContainer::getNextTextSection(const Common::String &text, uint &index) const {
+	Common::String result;
+
+	index = text.findFirstNotOf('\n', index);
+	if (index == Common::String::npos) {
+		return result;
+	}
+
+	uint endIndex = text.find('\n', index + 1);
+
+	if (endIndex == Common::String::npos) {
+		result = text.substr(index);
+		index = endIndex;
+		return result;
+	}
+
+	result = text.substr(index, endIndex - index);
+	index = endIndex + 1;
+	return result;
+}
+
+Common::String ButtonContainer::addNextTextToButtons(const Common::String &text, uint &index) {
+	Common::String buttonText = getNextTextSection(text, index) + '\n';
+	_buttonTexts.push_back(buttonText);
+	return buttonText;
+}
+
+Common::String ButtonContainer::addNextTextToButtons(const Common::String &text, uint &index, uint count) {
+	Common::String result;
+	for (uint i = 0; i < count; ++i) {
+		result += addNextTextToButtons(text, index);
+
+		if (index == Common::String::npos) {
+			break;
+		}
+	}
+	return result;
+}
+
+void ButtonContainer::setButtonTexts(const Common::String &text) {
+	_buttonTexts.clear();
+
+	uint index = 0;
+	for (uint i = 0; i < _buttons.size(); ++i) {
+		if (_buttons[i]._value) {
+			_buttonTexts.push_back(getNextTextSection(text, index));
+		} else {
+			_buttonTexts.push_back("");
+		}
+
+		if (index == Common::String::npos) {
+			break;
+		}
+	}
+}
+
+void ButtonContainer::disableButtonVoicing(uint startIndex, uint endIndex) {
+	for (uint i = startIndex; i < endIndex; ++i) {
+		if (i < _buttons.size()) {
+			_buttons[i]._canVoice = false;
+		}
+	}
+}
+
+void ButtonContainer::enableButtonVoicing(uint startIndex, uint endIndex) {
+	for (uint i = startIndex; i < endIndex; ++i) {
+		if (i < _buttons.size()) {
+			_buttons[i]._canVoice = true;
+		}
+	}
+}
+
+#endif
+
 /*------------------------------------------------------------------------*/
 
 void SettingsBaseDialog::showContents(SpriteResource &title1, bool waitFlag) {
diff --git a/engines/mm/xeen/dialogs/dialogs.h b/engines/mm/xeen/dialogs/dialogs.h
index d9ab5aa2fe2..141581d4bfe 100644
--- a/engines/mm/xeen/dialogs/dialogs.h
+++ b/engines/mm/xeen/dialogs/dialogs.h
@@ -41,18 +41,32 @@ public:
 	int _value;
 	uint _frameNum, _selectedFrame;
 	bool _draw;
+#ifdef USE_TTS
+	uint8 _ttsIndex;
+	bool _canVoice;
+#endif
 
 	/**
 	 * Constructor
 	 */
-	UIButton(const Common::Rect &bounds, int value, uint frameNum, SpriteResource *sprites, bool draw) :
+	UIButton(const Common::Rect &bounds, int value, uint frameNum, SpriteResource *sprites, bool draw, uint8 ttsIndex, bool ttsCanVoice) :
 		_bounds(bounds), _value(value), _frameNum(frameNum), _selectedFrame(frameNum | 1),
-		_sprites(sprites), _draw(draw) {}
+		_sprites(sprites), _draw(draw) {
+#ifdef USE_TTS
+		_ttsIndex = ttsIndex;
+		_canVoice = ttsCanVoice;	
+#endif
+	}
 
 	/**
 	 * Constructor
 	 */
-	UIButton() : _value(0), _frameNum(0), _selectedFrame(0), _sprites(nullptr), _draw(false) {}
+	UIButton() : _value(0), _frameNum(0), _selectedFrame(0), _sprites(nullptr), _draw(false) {
+#ifdef USE_TTS
+		_ttsIndex = 0;
+		_canVoice = false;
+#endif
+	}
 
 	/**
 	 * Set the frame
@@ -79,6 +93,10 @@ protected:
 	Common::StringArray _textStrings;
 	Common::Rect _waitBounds;
 	int _buttonValue;
+#ifdef USE_TTS
+	Common::StringArray _buttonTexts;
+	int _previousButton;
+#endif
 
 	bool checkEvents(XeenEngine *vm);
 
@@ -108,8 +126,78 @@ protected:
 	 * the equivalent of a space bar press, to the main interface area
 	 */
 	void setWaitBounds();
+
+#ifdef USE_TTS
+	/**
+	 * Reads one or more sections of text, then combines them
+	 * @param text			Text to take the sections from. Each section should be separated by one or more newlines
+	 * @param index			Starting index, which is moved to the start of the following section, or npos if no sections remain
+	 * @param count			How many sections to read
+	 * @param separator		What to separate sections with
+	 * @returns				The text sections combined, each separated by the separator
+	 */
+	Common::String getNextTextSection(const Common::String &text, uint &index, uint count, const char *separator) const;
+
+	/**
+	 * Reads one or more sections of text, then combines them. Newlines are kept intact
+	 * @param text			Text to take the sections from. Each section should be separated by one or more newlines
+	 * @param index			Starting index, which is moved to the start of the following section, or npos if no sections remain
+	 * @param count			How many sections to read
+	 * @returns				The text sections combined 
+	 */
+	Common::String getNextTextSection(const Common::String &text, uint &index, uint count) const;
+
+	/**
+	 * Reads one section of text
+	 * @param text			Text to take the section from. Each section should be separated by one or more newlines
+	 * @param index			Starting index, which is moved to the start of the following section, or npos if no sections remain
+	 * @returns				The text section
+	 */
+	Common::String getNextTextSection(const Common::String &text, uint &index) const;
+
+	/**
+	 * Reads one section of text and adds it to the button texts
+	 * @param text		Text to take the section from. Each section should be separated by one or more newlines
+	 * @param index		Starting index, which is moved to the start of the following section, or npos if no sections remain
+	 * @returns			The text section
+	 */
+	Common::String addNextTextToButtons(const Common::String &text, uint &index);
+
+	/**
+	 * Reads one or more sections of text and adds them to the button texts
+	 * @param text		Text to take the sections from. Each section should be separated by one or more newlines
+	 * @param index		Starting index, which is moved to the start of the following section, or npos if no sections remain
+	 * @param count		How many sections to read
+	 * @returns			The text sections combined
+	 */
+	Common::String addNextTextToButtons(const Common::String &text, uint &index, uint count);
+
+	/**
+	 * Sets the text of each button for use by TTS
+	 * @param text	Text for buttons. Each button's text should be separated by newlines
+	 */
+	void setButtonTexts(const Common::String &text);
+
+	/**
+	 * Disables the voicing of buttons from the start index to the end index
+	 * @param startIndex	Starting index
+	 * @param endIndex		Ending index
+	 */
+	void disableButtonVoicing(uint startIndex, uint endIndex);
+
+	/**
+	 * Enables the voicing of buttons from the start index to the end index
+	 * @param startIndex	Starting index
+	 * @param endIndex		Ending index
+	 */
+	void enableButtonVoicing(uint startIndex, uint endIndex);
+#endif
 public:
-	ButtonContainer(XeenEngine *vm) : Cutscenes(vm), _buttonValue(0) {}
+	ButtonContainer(XeenEngine *vm) : Cutscenes(vm), _buttonValue(0) {
+#ifdef USE_TTS
+		_previousButton = -1;
+#endif
+	}
 
 	/**
 	 * Saves the current list of buttons
@@ -121,9 +209,9 @@ public:
 	void restoreButtons();
 
 	void addButton(const Common::Rect &bounds, int val,
-		SpriteResource *sprites = nullptr);
+		SpriteResource *sprites = nullptr, uint8 ttsIndex = UINT8_MAX);
 	void addButton(const Common::Rect &bounds, int val,
-		int frameNum, SpriteResource *sprites = nullptr);
+		int frameNum, SpriteResource *sprites = nullptr, uint8 ttsIndex = UINT8_MAX);
 
 	void addPartyButtons(XeenEngine *vm);
 
@@ -136,6 +224,13 @@ public:
 	 * Clears any currently set button value
 	 */
 	void clearEvents() { _buttonValue = 0; }
+
+#ifdef USE_TTS
+	/**
+	 * Checks if a button is being hovered over and voices its text with TTS, if there is text
+	 */
+	void checkHoverOverButton();
+#endif
 };
 
 class SettingsBaseDialog : public ButtonContainer {
diff --git a/engines/mm/xeen/dialogs/dialogs_awards.cpp b/engines/mm/xeen/dialogs/dialogs_awards.cpp
index 375fa005c12..aac65da7b16 100644
--- a/engines/mm/xeen/dialogs/dialogs_awards.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_awards.cpp
@@ -27,6 +27,12 @@
 namespace MM {
 namespace Xeen {
 
+enum AwardsButtonTTSTextIndex {
+	kAwardsUp = 0,
+	kAwardsDown = 1,
+	kAwardsExit = 2
+};
+
 void Awards::show(XeenEngine *vm, const Character *ch) {
 	Awards *dlg = new Awards(vm);
 	dlg->execute(ch);
@@ -51,8 +57,12 @@ void Awards::execute(const Character *ch) {
 		windows[30].open();
 	}
 
-	windows[29].writeString(Res.AWARDS_TEXT);
+	Common::String buttonText;
+	windows[29].writeString(Res.AWARDS_TEXT, false, &buttonText);
 	drawButtons(&windows[0]);
+#ifdef USE_TTS
+	setButtonTexts(buttonText);
+#endif
 
 	while (!_vm->shouldExit()) {
 		// Build up a list of awards the character has
@@ -94,6 +104,9 @@ void Awards::execute(const Character *ch) {
 			awards[topIndex + 8].c_str()
 		);
 		windows[30].writeString(msg);
+#ifdef USE_TTS
+		_vm->sayText(buttonText);
+#endif
 		windows[24].update();
 
 		// Wait for keypress
@@ -123,9 +136,9 @@ void Awards::execute(const Character *ch) {
 
 void Awards::addButtons() {
 	_iconSprites.load("award.icn");
-	addButton(Common::Rect(216, 109, 240, 129), Common::KEYCODE_u, &_iconSprites);
-	addButton(Common::Rect(250, 109, 274, 129), Common::KEYCODE_d, &_iconSprites);
-	addButton(Common::Rect(284, 109, 308, 129), Common::KEYCODE_ESCAPE, &_iconSprites);
+	addButton(Common::Rect(216, 109, 240, 129), Common::KEYCODE_u, &_iconSprites, kAwardsUp);
+	addButton(Common::Rect(250, 109, 274, 129), Common::KEYCODE_d, &_iconSprites, kAwardsDown);
+	addButton(Common::Rect(284, 109, 308, 129), Common::KEYCODE_ESCAPE, &_iconSprites, kAwardsExit);
 }
 
 } // End of namespace Xeen
diff --git a/engines/mm/xeen/dialogs/dialogs_char_info.cpp b/engines/mm/xeen/dialogs/dialogs_char_info.cpp
index c30343794ef..44eeda4f18f 100644
--- a/engines/mm/xeen/dialogs/dialogs_char_info.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_char_info.cpp
@@ -30,6 +30,43 @@
 namespace MM {
 namespace Xeen {
 
+#ifdef USE_TTS
+
+static const uint8 kCharacterInfoInformationCount = 20;
+static const uint8 kCharacterInfoSideButtonCount = 4;
+static const uint8 kCharacterInfoPartyGoldIndex = 7;
+static const uint8 kCharacterInfoRowCount = 5;
+static const uint8 kCharacterInfoColumnCount = 4;
+
+#endif
+
+enum CharacterInfoButtonTTSTextIndex {
+	kCharacterInfoItem = 0,
+	kCharacterInfoQuickRef = 1,
+	kCharacterInfoExchange = 2,
+	kCharacterInfoExit = 3,
+	kCharacterInfoMight = 4,
+	kCharacterInfoAccuracy = 5,
+	kCharacterInfoHP = 6,
+	kCharacterInfoExperience = 7,
+	kCharacterInfoIntellect = 8,
+	kCharacterInfoLuck = 9,
+	kCharacterInfoSP = 10,
+	kCharacterInfoPartyGold = 11,
+	kCharacterInfoPersonality = 12,
+	kCharacterInfoAge = 13,
+	kCharacterInfoResistances = 14,
+	kCharacterInfoPartyGems = 15,
+	kCharacterInfoEndurance = 16,
+	kCharacterInfoLevel = 17,
+	kCharacterInfoSkills = 18,
+	kCharacterInfoPartyFood = 19,
+	kCharacterInfoSpeed = 20,
+	kCharacterInfoAC = 21,
+	kCharacterInfoAwards = 22,
+	kCharacterInfoCondition = 23
+};
+
 void CharacterInfo::show(XeenEngine *vm, int charIndex) {
 	CharacterInfo *dlg = new CharacterInfo(vm);
 	dlg->execute(charIndex);
@@ -57,7 +94,11 @@ void CharacterInfo::execute(int charIndex) {
 	do {
 		if (redrawFlag) {
 			Common::String charDetails = loadCharacterDetails(*c);
-			w.writeString(Common::String::format(Res.CHARACTER_TEMPLATE, charDetails.c_str()));
+			Common::String ttsMessage;
+			w.writeString(Common::String::format(Res.CHARACTER_TEMPLATE, charDetails.c_str()), false, &ttsMessage);
+#ifdef USE_TTS
+			speakText(ttsMessage);
+#endif
 			w.drawList(_drawList, 24);
 			w.update();
 			redrawFlag = false;
@@ -103,6 +144,9 @@ void CharacterInfo::execute(int charIndex) {
 				showCursor(false);
 				--_cursorCell;
 				showCursor(true);
+#ifdef USE_TTS
+				_vm->sayText(_buttonTexts[_buttons[_cursorCell]._ttsIndex], Common::TextToSpeechManager::INTERRUPT);
+#endif
 			}
 			w.update();
 
@@ -112,6 +156,9 @@ void CharacterInfo::execute(int charIndex) {
 				showCursor(false);
 				++_cursorCell;
 				showCursor(true);
+#ifdef USE_TTS
+				_vm->sayText(_buttonTexts[_buttons[_cursorCell]._ttsIndex], Common::TextToSpeechManager::INTERRUPT);
+#endif
 			}
 			w.update();
 
@@ -121,6 +168,9 @@ void CharacterInfo::execute(int charIndex) {
 				showCursor(false);
 				_cursorCell -= 5;
 				showCursor(true);
+#ifdef USE_TTS
+				_vm->sayText(_buttonTexts[_buttons[_cursorCell]._ttsIndex], Common::TextToSpeechManager::INTERRUPT);
+#endif
 			}
 			w.update();
 
@@ -130,6 +180,9 @@ void CharacterInfo::execute(int charIndex) {
 				showCursor(false);
 				_cursorCell += 5;
 				showCursor(true);
+#ifdef USE_TTS
+				_vm->sayText(_buttonTexts[_buttons[_cursorCell]._ttsIndex], Common::TextToSpeechManager::INTERRUPT);
+#endif
 			}
 			w.update();
 
@@ -240,32 +293,32 @@ void CharacterInfo::loadDrawStructs() {
 }
 
 void CharacterInfo::addButtons() {
-	addButton(Common::Rect(10, 24, 34, 44), 1001, &_iconSprites);
-	addButton(Common::Rect(10, 47, 34, 67), 1002, &_iconSprites);
-	addButton(Common::Rect(10, 70, 34, 90), 1003, &_iconSprites);
-	addButton(Common::Rect(10, 93, 34, 113), 1004, &_iconSprites);
-	addButton(Common::Rect(10, 116, 34, 136), 1005, &_iconSprites);
-	addButton(Common::Rect(61, 24, 85, 44), 1006, &_iconSprites);
-	addButton(Common::Rect(61, 47, 85, 67), 1007, &_iconSprites);
-	addButton(Common::Rect(61, 70, 85, 90), 1008, &_iconSprites);
-	addButton(Common::Rect(61, 93, 85, 113), 1009, &_iconSprites);
-	addButton(Common::Rect(61, 116, 85, 136), 1010, &_iconSprites);
-	addButton(Common::Rect(112, 24, 136, 44), 1011, &_iconSprites);
-	addButton(Common::Rect(112, 47, 136, 67), 1012, &_iconSprites);
-	addButton(Common::Rect(112, 70, 136, 90), 1013, &_iconSprites);
-	addButton(Common::Rect(112, 93, 136, 113), 1014, &_iconSprites);
-	addButton(Common::Rect(112, 116, 136, 136), 1015, &_iconSprites);
-	addButton(Common::Rect(177, 24, 201, 44), 1016, &_iconSprites);
-	addButton(Common::Rect(177, 47, 201, 67), 1017, &_iconSprites);
-	addButton(Common::Rect(177, 70, 201, 90), 1018, &_iconSprites);
-	addButton(Common::Rect(177, 93, 201, 113), 1019, &_iconSprites);
-	addButton(Common::Rect(177, 116, 201, 136), 1020, &_iconSprites);
-
-	addButton(Common::Rect(285, 11, 309, 31), Res.KeyConstants.DialogsCharInfo.KEY_ITEM, &_iconSprites);
-	addButton(Common::Rect(285, 43, 309, 63), Res.KeyConstants.DialogsCharInfo.KEY_QUICK, &_iconSprites);
-	addButton(Common::Rect(285, 75, 309, 95), Res.KeyConstants.DialogsCharInfo.KEY_EXCHANGE, &_iconSprites);
-
-	addButton(Common::Rect(285, 107, 309, 127), Common::KEYCODE_ESCAPE, &_iconSprites);
+	addButton(Common::Rect(10, 24, 34, 44), 1001, &_iconSprites, kCharacterInfoMight);
+	addButton(Common::Rect(10, 47, 34, 67), 1002, &_iconSprites, kCharacterInfoIntellect);
+	addButton(Common::Rect(10, 70, 34, 90), 1003, &_iconSprites, kCharacterInfoPersonality);
+	addButton(Common::Rect(10, 93, 34, 113), 1004, &_iconSprites, kCharacterInfoEndurance);
+	addButton(Common::Rect(10, 116, 34, 136), 1005, &_iconSprites, kCharacterInfoSpeed);
+	addButton(Common::Rect(61, 24, 85, 44), 1006, &_iconSprites, kCharacterInfoAccuracy);
+	addButton(Common::Rect(61, 47, 85, 67), 1007, &_iconSprites, kCharacterInfoLuck);
+	addButton(Common::Rect(61, 70, 85, 90), 1008, &_iconSprites, kCharacterInfoAge);
+	addButton(Common::Rect(61, 93, 85, 113), 1009, &_iconSprites, kCharacterInfoLevel);
+	addButton(Common::Rect(61, 116, 85, 136), 1010, &_iconSprites, kCharacterInfoAC);
+	addButton(Common::Rect(112, 24, 136, 44), 1011, &_iconSprites, kCharacterInfoHP);
+	addButton(Common::Rect(112, 47, 136, 67), 1012, &_iconSprites, kCharacterInfoSP);
+	addButton(Common::Rect(112, 70, 136, 90), 1013, &_iconSprites, kCharacterInfoResistances);
+	addButton(Common::Rect(112, 93, 136, 113), 1014, &_iconSprites, kCharacterInfoSkills);
+	addButton(Common::Rect(112, 116, 136, 136), 1015, &_iconSprites, kCharacterInfoAwards);
+	addButton(Common::Rect(177, 24, 201, 44), 1016, &_iconSprites, kCharacterInfoExperience);
+	addButton(Common::Rect(177, 47, 201, 67), 1017, &_iconSprites, kCharacterInfoPartyGold);
+	addButton(Common::Rect(177, 70, 201, 90), 1018, &_iconSprites, kCharacterInfoPartyGems);
+	addButton(Common::Rect(177, 93, 201, 113), 1019, &_iconSprites, kCharacterInfoPartyFood);
+	addButton(Common::Rect(177, 116, 201, 136), 1020, &_iconSprites, kCharacterInfoCondition);
+
+	addButton(Common::Rect(285, 11, 309, 31), Res.KeyConstants.DialogsCharInfo.KEY_ITEM, &_iconSprites, kCharacterInfoItem);
+	addButton(Common::Rect(285, 43, 309, 63), Res.KeyConstants.DialogsCharInfo.KEY_QUICK, &_iconSprites, kCharacterInfoQuickRef);
+	addButton(Common::Rect(285, 75, 309, 95), Res.KeyConstants.DialogsCharInfo.KEY_EXCHANGE, &_iconSprites, kCharacterInfoExchange);
+
+	addButton(Common::Rect(285, 107, 309, 127), Common::KEYCODE_ESCAPE, &_iconSprites, kCharacterInfoExit);
 	addPartyButtons(_vm);
 }
 
@@ -372,6 +425,56 @@ const char *CharacterInfo::getFoodOnHandPlurals(int food) {
 	return Res.FOOD_ON_HAND[0];
 }
 
+#ifdef USE_TTS
+
+void CharacterInfo::speakText(const Common::String &text) {
+	uint index = 0;
+	uint statNameIndex = 0;
+
+	// Get the header for each piece of information
+	Common::String informationHeaders[kCharacterInfoInformationCount];
+	for (uint i = 0; i < kCharacterInfoInformationCount; ++i) {
+		if (i != kCharacterInfoPartyGoldIndex) {
+			// Items in the rightmost column already have their full names, not abbreviations
+			if ((i + 1) % kCharacterInfoColumnCount != 0) {
+				// Replace abbreviations with their full versions
+				informationHeaders[i] = Res.STAT_NAMES[statNameIndex];
+				getNextTextSection(text, index);
+			} else {
+				informationHeaders[i] = getNextTextSection(text, index);
+			}
+		}
+
+		// The text is displayed in order from left to right, while the stat names in the STAT_NAMES array are ordered
+		// from top to bottom. Therefore, we need to set the stat name index to correspond to the STAT_NAMES array
+		statNameIndex += kCharacterInfoRowCount;
+		if (statNameIndex >= kCharacterInfoInformationCount) {
+			statNameIndex -= kCharacterInfoInformationCount - 1;
+		}
+	}
+
+	// Get the text for the side buttons
+	Common::String sideButtonsText = addNextTextToButtons(text, index, kCharacterInfoSideButtonCount);
+
+	// Party gold label (but not the value) sorts out of order and not with the rest of the information,
+	// so we need to move it to its correct place
+	informationHeaders[kCharacterInfoPartyGoldIndex] = getNextTextSection(text, index);
+
+	// Character name
+	_vm->sayText(getNextTextSection(text, index), Common::TextToSpeechManager::INTERRUPT);
+
+	// Each attribute
+	for (uint i = 0; i < kCharacterInfoInformationCount; ++i) {
+		Common::String buttonText = informationHeaders[i] + ": " + getNextTextSection(text, index);
+		_vm->sayText(buttonText);
+		_buttonTexts.push_back(buttonText);
+	}
+
+	_vm->sayText(sideButtonsText);
+}
+
+#endif
+
 bool CharacterInfo::expandStat(int attrib, const Character &c) {
 	const int STAT_POS[2][20] = {
 		{
@@ -620,6 +723,9 @@ bool CharacterInfo::expandStat(int attrib, const Character &c) {
 	while (!_vm->shouldExit() && !events.isKeyMousePressed())
 		events.pollEventsAndWait();
 	events.clearEvents();
+#ifdef USE_TTS
+	_vm->stopTextToSpeech();
+#endif
 
 	w.close();
 	return false;
diff --git a/engines/mm/xeen/dialogs/dialogs_char_info.h b/engines/mm/xeen/dialogs/dialogs_char_info.h
index 53129ec4199..6298f26611c 100644
--- a/engines/mm/xeen/dialogs/dialogs_char_info.h
+++ b/engines/mm/xeen/dialogs/dialogs_char_info.h
@@ -74,6 +74,10 @@ private:
 	*/
 	const char *getFoodOnHandPlurals(int food);
 
+#ifdef USE_TTS
+	void speakText(const Common::String &text);
+#endif
+
 	bool expandStat(int attrib, const Character &c);
 public:
 	static void show(XeenEngine *vm, int charIndex);
diff --git a/engines/mm/xeen/dialogs/dialogs_control_panel.cpp b/engines/mm/xeen/dialogs/dialogs_control_panel.cpp
index a6474b008e0..8db974fbe2c 100644
--- a/engines/mm/xeen/dialogs/dialogs_control_panel.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_control_panel.cpp
@@ -28,6 +28,23 @@
 namespace MM {
 namespace Xeen {
 
+#ifdef USE_TTS
+
+static const uint8 kControlPanelTextCount = 7;
+static const uint8 kControlPanelEfxIndex = 1;
+static const uint8 kControlPanelMusicIndex = 3;
+
+#endif
+
+enum ControlPanelButtonTTSTextIndex {
+	kControlPanelLoad = 0,
+	kControlPanelEfx = 1,
+	kControlPanelSave = 2,
+	kControlPanelMusic = 3,
+	kControlPanelQuit = 4,
+	kControlPanelWizard = 5
+};
+
 int ControlPanel::show(XeenEngine *vm) {
 	ControlPanel *dlg = new ControlPanel(vm);
 	int result = dlg->execute();
@@ -51,24 +68,37 @@ int ControlPanel::execute() {
 
 	int result = 0, debugCtr = 0;
 	w.open();
+#ifdef USE_TTS
+	bool voiceText = true;
+	_vm->stopTextToSpeech();
+#endif
 
 	do {
 		Common::String btnText = getButtonText();
 		Common::String text = Common::String::format(Res.CONTROL_PANEL_TEXT, btnText.c_str());
 
 		drawButtons(&w);
-		w.writeString(text);
+
+		Common::String ttsMessage;
+		w.writeString(text, false, &ttsMessage);
 		w.writeString("\xB""000\t000\x1");
 		w.update();
 
+#ifdef USE_TTS
+		if (voiceText) {
+			speakText(ttsMessage);
+			voiceText = false;
+		}
+#endif
+
 		events.updateGameCounter();
 		intf.draw3d(false, false);
 
 		do {
-			w.writeString("\r");
+			w.writeString("\r", false);
 			drawButtons(&w);
-			w.writeString(text);
-			w.writeString("\v000\t000");
+			w.writeString(text, false);
+			w.writeString("\v000\t000", false);
 			w.frame();
 
 			if (_debugFlag)
@@ -131,10 +161,14 @@ int ControlPanel::execute() {
 			sound.setFxOn(!sound._fxOn);
 			if (sound._fxOn)
 				sound.playFX(20);
-
+#ifdef USE_TTS
+			voiceOnOffText(sound._fxOn, kControlPanelEfxIndex);
+#endif
 		} else if (Res.KeyConstants.DialogsControlPanel.KEY_MUSICON == _buttonValue) {
 			sound.setMusicOn(!sound._musicOn);
-
+#ifdef USE_TTS
+			voiceOnOffText(sound._musicOn, kControlPanelMusicIndex);
+#endif
 		} else if (Common::KEYCODE_ESCAPE == _buttonValue) {
 			result = 1;
 
@@ -168,17 +202,17 @@ int ControlPanel::execute() {
 
 void ControlPanel::loadButtons() {
 	_iconSprites.load("cpanel.icn");
-	addButton(Common::Rect(214, 56, 244, 69), Res.KeyConstants.DialogsControlPanel.KEY_FXON, 0, &_iconSprites);
-	addButton(Common::Rect(214, 75, 244, 88), Res.KeyConstants.DialogsControlPanel.KEY_MUSICON, 0, &_iconSprites);
-	addButton(Common::Rect(135, 56, 165, 69), Res.KeyConstants.DialogsControlPanel.KEY_LOAD, 0, &_iconSprites);
-	addButton(Common::Rect(135, 75, 165, 88), Res.KeyConstants.DialogsControlPanel.KEY_SAVE, 0, &_iconSprites);
+	addButton(Common::Rect(214, 56, 244, 69), Res.KeyConstants.DialogsControlPanel.KEY_FXON, 0, &_iconSprites, kControlPanelEfx);
+	addButton(Common::Rect(214, 75, 244, 88), Res.KeyConstants.DialogsControlPanel.KEY_MUSICON, 0, &_iconSprites, kControlPanelMusic);
+	addButton(Common::Rect(135, 56, 165, 69), Res.KeyConstants.DialogsControlPanel.KEY_LOAD, 0, &_iconSprites, kControlPanelLoad);
+	addButton(Common::Rect(135, 75, 165, 88), Res.KeyConstants.DialogsControlPanel.KEY_SAVE, 0, &_iconSprites, kControlPanelSave);
 
 	// For ScummVM we've merged both Save and Save As into a single
 	// save item, so we don't need this one
 	addButton(Common::Rect(), 0);
 
-	addButton(Common::Rect(135, 94, 165, 107), Res.KeyConstants.DialogsControlPanel.KEY_QUIT, 0, &_iconSprites);
-	addButton(Common::Rect(175, 113, 205, 126), Res.KeyConstants.DialogsControlPanel.KEY_MRWIZARD, 0, &_iconSprites);
+	addButton(Common::Rect(135, 94, 165, 107), Res.KeyConstants.DialogsControlPanel.KEY_QUIT, 0, &_iconSprites, kControlPanelQuit);
+	addButton(Common::Rect(175, 113, 205, 126), Res.KeyConstants.DialogsControlPanel.KEY_MRWIZARD, 0, &_iconSprites, kControlPanelWizard);
 }
 
 Common::String ControlPanel::getButtonText() {
@@ -205,5 +239,27 @@ Common::String ControlPanel::getTimeText() const {
 		timeStr.c_str(), playtimeStr.c_str());
 }
 
+#ifdef USE_TTS
+
+void ControlPanel::speakText(const Common::String &text) {
+	uint index = 0;
+	_vm->sayText(getNextTextSection(text, index, kControlPanelTextCount));
+	addNextTextToButtons(text, index, kControlPanelTextCount - 1);
+}
+
+void ControlPanel::voiceOnOffText(bool on, uint buttonTextIndex) {
+	const char *baseMessage = on ? Res.ON : Res.OFF;
+	Common::String cleanedMessage;
+	for (uint i = 0; i < strlen(baseMessage); ++i) {
+		if (Common::isAlpha(baseMessage[i])) {
+			cleanedMessage += baseMessage[i];
+		}
+	}
+	_vm->sayText(cleanedMessage, Common::TextToSpeechManager::INTERRUPT);
+	_buttonTexts[buttonTextIndex] = cleanedMessage;
+}
+
+#endif
+
 } // End of namespace Xeen
 } // End of namespace MM
diff --git a/engines/mm/xeen/dialogs/dialogs_control_panel.h b/engines/mm/xeen/dialogs/dialogs_control_panel.h
index fc5ca126f1a..ecc84f44f65 100644
--- a/engines/mm/xeen/dialogs/dialogs_control_panel.h
+++ b/engines/mm/xeen/dialogs/dialogs_control_panel.h
@@ -54,6 +54,21 @@ private:
 	 * Gets the current time
 	 */
 	Common::String getTimeText() const;
+
+#ifdef USE_TTS
+	/**
+	 * Voices text as text-to-speech and sets the buttons
+	 * @param text	Text of the control panel, each piece separated by a newline
+	 */
+	void speakText(const Common::String &text);
+
+	/**
+	 * Voices "On" or "Off" text and changes the TTS text of an on/off button to this text
+	 * @param on				Whether to voice "on" or "off"
+	 * @param buttonTextIndex	The index of the button text to change
+	 */
+	void voiceOnOffText(bool on, uint buttonTextIndex);
+#endif
 public:
 	/**
 	 * Show the control panel
diff --git a/engines/mm/xeen/dialogs/dialogs_copy_protection.cpp b/engines/mm/xeen/dialogs/dialogs_copy_protection.cpp
index ba850f645a3..796f6efa7ae 100644
--- a/engines/mm/xeen/dialogs/dialogs_copy_protection.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_copy_protection.cpp
@@ -27,6 +27,12 @@
 namespace MM {
 namespace Xeen {
 
+#ifdef USE_TTS
+
+static const uint8 kCopyProtectionDirectionCount = 2;
+
+#endif
+
 bool CopyProtection::show(XeenEngine *vm) {
 	CopyProtection *dlg = new CopyProtection(vm);
 	int result = dlg->execute();
@@ -52,9 +58,14 @@ bool CopyProtection::execute() {
 		protEntry._pageNum, protEntry._lineNum, protEntry._wordNum);
 
 	w.open();
-	w.writeString(msg);
+	Common::String ttsMessage;
+	w.writeString(msg, false, &ttsMessage);
 	w.update();
 
+#ifdef USE_TTS
+	speakText(ttsMessage);
+#endif
+
 	for (int tryNum = 0; tryNum < 3 && !_vm->shouldExit(); ++tryNum) {
 		line.clear();
 		if (getString(line, 20, 200, false) && !line.compareToIgnoreCase(protEntry._text)) {
@@ -97,5 +108,17 @@ void CopyProtection::loadEntries() {
 	}
 }
 
+#ifdef USE_TTS
+
+void CopyProtection::speakText(const Common::String &text) const {
+	uint index = 0;
+	_vm->sayText(getNextTextSection(text, index), Common::TextToSpeechManager::INTERRUPT);
+	// Combine the directions so they're spoken cleanly as one sentence
+	_vm->sayText(getNextTextSection(text, index, kCopyProtectionDirectionCount, " "));
+	_vm->sayText(text.substr(index));
+}
+
+#endif
+
 } // End of namespace Xeen
 } // End of namespace MM
diff --git a/engines/mm/xeen/dialogs/dialogs_copy_protection.h b/engines/mm/xeen/dialogs/dialogs_copy_protection.h
index 7c3d97ac6f7..92901ce3ca8 100644
--- a/engines/mm/xeen/dialogs/dialogs_copy_protection.h
+++ b/engines/mm/xeen/dialogs/dialogs_copy_protection.h
@@ -52,6 +52,14 @@ private:
 	 * Load the copy protection entries
 	 */
 	void loadEntries();
+
+#ifdef USE_TTS
+	/**
+	 * Voices text with TTS
+	 * @param text	Text to voice
+	 */
+	void speakText(const Common::String &text) const;
+#endif
 public:
 	/**
 	 * Show the dialog
diff --git a/engines/mm/xeen/dialogs/dialogs_create_char.cpp b/engines/mm/xeen/dialogs/dialogs_create_char.cpp
index f4eaf8c9e7f..6f1408733a9 100644
--- a/engines/mm/xeen/dialogs/dialogs_create_char.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_create_char.cpp
@@ -26,6 +26,43 @@
 namespace MM {
 namespace Xeen {
 
+#ifdef USE_TTS
+
+static const uint8 kCreateCharacterBasicInfoCount = 6;
+static const uint8 kCreateCharacterSideButtonCount = 3;
+
+#endif
+
+enum CreateCharacterButtonTTSTextIndex {
+	kCreateCharacterRoll = 0,
+	kCreateCharacterCreate = 1,
+	kCreateCharacterExit = 2,
+	kCreateCharacterMight = 3,
+	kCreateCharacterIntellect = 4,
+	kCreateCharacterPersonality = 5,
+	kCreateCharacterEndurance = 6,
+	kCreateCharacterSpeed = 7,
+	kCreateCharacterAccuracy = 8,
+	kCreateCharacterLuck = 9,
+	kCreateCharacterKnight = 10,
+	kCreateCharacterPaladin = 11,
+	kCreateCharacterArcher = 12,
+	kCreateCharacterCleric = 13,
+	kCreateCharacterSorcerer = 14,
+	kCreateCharacterRobber = 15,
+	kCreateCharacterNinja = 16,
+	kCreateCharacterBarbarian = 17,
+	kCreateCharacterDruid = 18,
+	kCreateCharacterRanger = 19,
+	kCreateCharacterSwapMight = 20,
+	kCreateCharacterSwapIntellect = 21,
+	kCreateCharacterSwapPersonality = 22,
+	kCreateCharacterSwapEndurance = 23,
+	kCreateCharacterSwapSpeed = 24,
+	kCreateCharacterSwapAccuracy = 25,
+	kCreateCharacterSwapLuck = 26
+};
+
 void CreateCharacterDialog::show(XeenEngine *vm) {
 	CreateCharacterDialog *dlg = new CreateCharacterDialog(vm);
 	dlg->execute();
@@ -104,9 +141,13 @@ void CreateCharacterDialog::execute() {
 			party._roster[freeCharList[charIndex]]._faceSprites->draw(
 				w, 0, Common::Point(27, 102));
 
+			Common::String ttsMessage;
 			// Render all on-screen text
-			w.writeString(msg);
+			w.writeString(msg, false, &ttsMessage);
 			w.update();
+#ifdef USE_TTS
+			speakText(ttsMessage, false, classId != -1, selectedClass);
+#endif
 
 			// Draw the arrow for the selected class, if applicable
 			if (selectedClass != -1)
@@ -221,8 +262,12 @@ void CreateCharacterDialog::execute() {
 			party._roster[freeCharList[charIndex]]._faceSprites->draw(w, 0,
 				Common::Point(27, 102));
 
-			w.writeString(msg);
+			Common::String ttsMessage;
+			w.writeString(msg, false, &ttsMessage);
 			w.update();
+#ifdef USE_TTS
+			speakText(ttsMessage, true, classId != -1, selectedClass);
+#endif
 
 			if (selectedClass != -1) {
 				printSelectionArrow(selectedClass);
@@ -249,31 +294,31 @@ void CreateCharacterDialog::loadButtons() {
 	_icons.load("create.icn");
 
 	// Add buttons
-	addButton(Common::Rect(132, 98, 156, 118), Res.KeyConstants.DialogsCreateChar.KEY_ROLL, &_icons);
-	addButton(Common::Rect(132, 128, 156, 148), Res.KeyConstants.DialogsCreateChar.KEY_CREATE, &_icons);
+	addButton(Common::Rect(132, 98, 156, 118), Res.KeyConstants.DialogsCreateChar.KEY_ROLL, &_icons, kCreateCharacterRoll);
+	addButton(Common::Rect(132, 128, 156, 148), Res.KeyConstants.DialogsCreateChar.KEY_CREATE, &_icons, kCreateCharacterCreate);
 
-	addButton(Common::Rect(132, 158, 156, 178), Common::KEYCODE_ESCAPE, &_icons);
+	addButton(Common::Rect(132, 158, 156, 178), Common::KEYCODE_ESCAPE, &_icons, kCreateCharacterExit);
 	addButton(Common::Rect(86, 98, 110, 118), Common::KEYCODE_UP, &_icons);
 	addButton(Common::Rect(86, 120, 110, 140), Common::KEYCODE_DOWN, &_icons);
 
-	addButton(Common::Rect(168, 19, 192, 39), Res.KeyConstants.DialogsCreateChar.KEY_MGT, nullptr);
-	addButton(Common::Rect(168, 43, 192, 63),   Res.KeyConstants.DialogsCreateChar.KEY_INT, nullptr);
-	addButton(Common::Rect(168, 67, 192, 87),   Res.KeyConstants.DialogsCreateChar.KEY_PER, nullptr);
-	addButton(Common::Rect(168, 91, 192, 111),  Res.KeyConstants.DialogsCreateChar.KEY_END, nullptr);
-	addButton(Common::Rect(168, 115, 192, 135), Res.KeyConstants.DialogsCreateChar.KEY_SPD, nullptr);
-	addButton(Common::Rect(168, 139, 192, 159), Res.KeyConstants.DialogsCreateChar.KEY_ACY, nullptr);
-	addButton(Common::Rect(168, 163, 192, 183), Res.KeyConstants.DialogsCreateChar.KEY_LCK, nullptr);
-
-	addButton(Common::Rect(227, 19, 239, 29), 1000, nullptr);
-	addButton(Common::Rect(227, 30, 239, 40), 1001, nullptr);
-	addButton(Common::Rect(227, 41, 239, 51), 1002, nullptr);
-	addButton(Common::Rect(227, 52, 239, 62), 1003, nullptr);
-	addButton(Common::Rect(227, 63, 239, 73), 1004, nullptr);
-	addButton(Common::Rect(227, 74, 239, 84), 1005, nullptr);
-	addButton(Common::Rect(227, 85, 239, 95), 1006, nullptr);
-	addButton(Common::Rect(227, 96, 239, 106), 1007, nullptr);
-	addButton(Common::Rect(227, 107, 239, 117), 1008, nullptr);
-	addButton(Common::Rect(227, 118, 239, 128), 1009, nullptr);
+	addButton(Common::Rect(168, 19, 192, 39), Res.KeyConstants.DialogsCreateChar.KEY_MGT, nullptr, kCreateCharacterMight);
+	addButton(Common::Rect(168, 43, 192, 63),   Res.KeyConstants.DialogsCreateChar.KEY_INT, nullptr, kCreateCharacterIntellect);
+	addButton(Common::Rect(168, 67, 192, 87),   Res.KeyConstants.DialogsCreateChar.KEY_PER, nullptr, kCreateCharacterPersonality);
+	addButton(Common::Rect(168, 91, 192, 111),  Res.KeyConstants.DialogsCreateChar.KEY_END, nullptr, kCreateCharacterEndurance);
+	addButton(Common::Rect(168, 115, 192, 135), Res.KeyConstants.DialogsCreateChar.KEY_SPD, nullptr, kCreateCharacterSpeed);
+	addButton(Common::Rect(168, 139, 192, 159), Res.KeyConstants.DialogsCreateChar.KEY_ACY, nullptr, kCreateCharacterAccuracy);
+	addButton(Common::Rect(168, 163, 192, 183), Res.KeyConstants.DialogsCreateChar.KEY_LCK, nullptr, kCreateCharacterLuck);
+
+	addButton(Common::Rect(227, 19, 239, 29), 1000, nullptr, kCreateCharacterKnight);
+	addButton(Common::Rect(227, 30, 239, 40), 1001, nullptr, kCreateCharacterPaladin);
+	addButton(Common::Rect(227, 41, 239, 51), 1002, nullptr, kCreateCharacterArcher);
+	addButton(Common::Rect(227, 52, 239, 62), 1003, nullptr, kCreateCharacterCleric);
+	addButton(Common::Rect(227, 63, 239, 73), 1004, nullptr, kCreateCharacterSorcerer);
+	addButton(Common::Rect(227, 74, 239, 84), 1005, nullptr, kCreateCharacterRobber);
+	addButton(Common::Rect(227, 85, 239, 95), 1006, nullptr, kCreateCharacterNinja);
+	addButton(Common::Rect(227, 96, 239, 106), 1007, nullptr, kCreateCharacterBarbarian);
+	addButton(Common::Rect(227, 107, 239, 117), 1008, nullptr, kCreateCharacterDruid);
+	addButton(Common::Rect(227, 118, 239, 128), 1009, nullptr, kCreateCharacterRanger);
 }
 
 void CreateCharacterDialog::drawIcons() {
@@ -523,13 +568,21 @@ int CreateCharacterDialog::exchangeAttribute(int srcAttr) {
 	saveButtons();
 
 	addButton(Common::Rect(118, 58, 142, 78), Common::KEYCODE_ESCAPE, &_icons);
-	addButton(Common::Rect(168, 19, 192, 39),   Res.KeyConstants.DialogsCreateChar.KEY_MGT);
-	addButton(Common::Rect(168, 43, 192, 63),   Res.KeyConstants.DialogsCreateChar.KEY_INT);
-	addButton(Common::Rect(168, 67, 192, 87),   Res.KeyConstants.DialogsCreateChar.KEY_PER);
-	addButton(Common::Rect(168, 91, 192, 111),  Res.KeyConstants.DialogsCreateChar.KEY_END);
-	addButton(Common::Rect(168, 115, 192, 135), Res.KeyConstants.DialogsCreateChar.KEY_SPD);
-	addButton(Common::Rect(168, 139, 192, 159), Res.KeyConstants.DialogsCreateChar.KEY_ACY);
-	addButton(Common::Rect(168, 163, 192, 183), Res.KeyConstants.DialogsCreateChar.KEY_LCK);
+	addButton(Common::Rect(168, 19, 192, 39),   Res.KeyConstants.DialogsCreateChar.KEY_MGT, nullptr, kCreateCharacterSwapMight);
+	addButton(Common::Rect(168, 43, 192, 63),   Res.KeyConstants.DialogsCreateChar.KEY_INT, nullptr, kCreateCharacterSwapIntellect);
+	addButton(Common::Rect(168, 67, 192, 87),   Res.KeyConstants.DialogsCreateChar.KEY_PER, nullptr, kCreateCharacterSwapPersonality);
+	addButton(Common::Rect(168, 91, 192, 111),  Res.KeyConstants.DialogsCreateChar.KEY_END, nullptr, kCreateCharacterSwapEndurance);
+	addButton(Common::Rect(168, 115, 192, 135), Res.KeyConstants.DialogsCreateChar.KEY_SPD, nullptr, kCreateCharacterSwapSpeed);
+	addButton(Common::Rect(168, 139, 192, 159), Res.KeyConstants.DialogsCreateChar.KEY_ACY, nullptr, kCreateCharacterSwapAccuracy);
+	addButton(Common::Rect(168, 163, 192, 183), Res.KeyConstants.DialogsCreateChar.KEY_LCK, nullptr, kCreateCharacterSwapLuck);
+
+#ifdef USE_TTS
+	for (uint i = 0; i < TOTAL_ATTRIBUTES; ++i) {
+		_buttonTexts.push_back(Res.STAT_NAMES[i]);
+	}
+
+	_vm->stopTextToSpeech();
+#endif
 
 	Window &w = windows[26];
 	w.open();
@@ -558,6 +611,11 @@ int CreateCharacterDialog::exchangeAttribute(int srcAttr) {
 
 	w.close();
 	restoreButtons();
+#ifdef USE_TTS
+	for (uint i = 0; i < TOTAL_ATTRIBUTES; ++i) {
+		_buttonTexts.pop_back();
+	}
+#endif
 	_buttonValue = 0;
 	return result;
 }
@@ -588,6 +646,9 @@ bool CreateCharacterDialog::saveCharacter(Character &c, int classId, Race race,
 		// Name aborted, so exit
 		return false;
 
+#ifdef USE_TTS
+	_vm->sayText(name, Common::TextToSpeechManager::INTERRUPT);
+#endif
 	// Save new character details
 	c.clear();
 	c._name = name;
@@ -640,5 +701,91 @@ bool CreateCharacterDialog::saveCharacter(Character &c, int classId, Race race,
 	return true;
 }
 
+#ifdef USE_TTS
+
+void CreateCharacterDialog::speakText(const Common::String &text, bool hasAttributeLabels, bool classSelected, int selectedClass) {
+	_vm->stopTextToSpeech();
+	uint index = 0;
+	bool addNewButtons = _buttonTexts.empty();
+
+	Common::String buttonTexts;
+	if (!hasAttributeLabels) {
+		// Roll/create/ESC buttons
+		if (addNewButtons) {
+			buttonTexts = addNextTextToButtons(text, index, kCreateCharacterSideButtonCount);
+		} else {
+			buttonTexts = getNextTextSection(text, index, kCreateCharacterSideButtonCount);
+		}
+
+		for (uint i = 0; i < TOTAL_ATTRIBUTES; ++i) {
+			getNextTextSection(text, index);
+		}
+	}
+
+	// Race/sex/class info
+	_vm->sayText(getNextTextSection(text, index, kCreateCharacterBasicInfoCount));
+
+	uint classIndex = 0;
+
+	// The selected class is at the very end of the string, but it should be voiced with the rest of the race/sex/class
+	// info, so find it here early and voice it
+	if (classSelected) {
+		uint endClassIndex = text.findLastNotOf('\n');
+
+		if (endClassIndex != Common::String::npos) {
+			for (uint i = endClassIndex; i >= index; --i) {
+				if (text[i] == '\n') {
+					classIndex = i;
+					_vm->sayText(text.substr(classIndex));
+					break;
+				}
+			}
+		}
+	}
+
+	if (!hasAttributeLabels) {
+		_vm->sayText(buttonTexts);
+	}
+
+	// Get attribute values
+	for (uint i = 0; i < TOTAL_ATTRIBUTES; ++i) {
+		Common::String attribute = Common::String(Res.STAT_NAMES[i]) + ": " + getNextTextSection(text, index);
+
+		if (addNewButtons) {
+			_buttonTexts.push_back(attribute);
+		} else {
+			_buttonTexts[i + kCreateCharacterSideButtonCount] = attribute;
+		}
+
+		_vm->sayText(attribute);
+	}
+
+	// Classes
+	for (int i = 0; i < TOTAL_CLASSES; ++i) {
+		Common::String buttonText;
+		if (_allowedClasses[i]) {
+			buttonText = getNextTextSection(text, index);
+		} else {
+			buttonText = "";
+			getNextTextSection(text, index);
+		}
+
+		if (addNewButtons) {
+			_buttonTexts.push_back(buttonText);
+		} else {
+			_buttonTexts[i + TOTAL_CLASSES] = buttonText;
+		}
+
+		if (i == selectedClass) {
+			_vm->sayText(buttonText);
+		}
+	}
+
+	// Skills
+	_vm->sayText(text.substr(index, classIndex - index));
+}
+
+#endif
+
 } // End of namespace Xeen
 } // End of namespace MM
diff --git a/engines/mm/xeen/dialogs/dialogs_create_char.h b/engines/mm/xeen/dialogs/dialogs_create_char.h
index 4b9d04d6b0a..c2590caa8a4 100644
--- a/engines/mm/xeen/dialogs/dialogs_create_char.h
+++ b/engines/mm/xeen/dialogs/dialogs_create_char.h
@@ -112,6 +112,17 @@ private:
 	 * well as a list of classes that the attributes meet the requirements for
 	 */
 	void rollAttributes();
+
+#ifdef USE_TTS
+	/**
+	 * Voices text with TTS and sets up buttons
+	 * @param text					Text for voicing and buttons. Each section should be separated by a newline
+	 * @param hasAttributeLabels	Whether this text has attribute labels in it
+	 * @param classSelected			Whether a class is selected
+	 * @param selectedClass			Class selected by the player
+	 */
+	void speakText(const Common::String &text, bool hasAttributeLabels, bool classSelected, int selectedClass);
+#endif
 public:
 	/**
 	 * Shows the Create Character dialog
diff --git a/engines/mm/xeen/dialogs/dialogs_difficulty.cpp b/engines/mm/xeen/dialogs/dialogs_difficulty.cpp
index b3f8a2758d5..9ebf7f3733b 100644
--- a/engines/mm/xeen/dialogs/dialogs_difficulty.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_difficulty.cpp
@@ -26,6 +26,24 @@
 namespace MM {
 namespace Xeen {
 
+#ifdef USE_TTS
+
+static const char *difficultyButtonsText[] = {
+	"Adventurer\nWarrior",													// English
+	"Abenteurer\nK\204mpfer",												// German
+	"Aventurier\nGuerrier",													// French
+	"Aventurero\nGuerrero",													// Spanish
+	"\x80\xa2\xa0\xad\xe2\xee\xe0\xa8\xe1\n\x82\xae\xa8\xad",				// Russian (Авантюрис, Воин)
+	"\xab\x5f\xc0\x49\xbc\xd2\xa6\xa1\n\xbe\xd4\xa4\x68\xbc\xd2\xa6\xa1"	// Chinese (冒險模式, 戰士模式)
+};
+
+#endif
+
+enum DifficultyButtonTTSTextIndex {
+	kDifficultyAdventurer = 0,
+	kDifficultyWarrior = 1
+};
+
 int DifficultyDialog::show(XeenEngine *vm) {
 	DifficultyDialog *dlg = new DifficultyDialog(vm);
 	int result = dlg->execute();
@@ -36,6 +54,9 @@ int DifficultyDialog::show(XeenEngine *vm) {
 
 DifficultyDialog::DifficultyDialog(XeenEngine *vm) : ButtonContainer(vm) {
 	loadButtons();
+#ifdef USE_TTS
+	setButtonTexts(difficultyButtonsText[_vm->_ttsLanguage]);
+#endif
 }
 
 int DifficultyDialog::execute() {
@@ -44,6 +65,9 @@ int DifficultyDialog::execute() {
 
 	Window &w = windows[6];
 	w.open();
+#ifdef USE_TTS
+	_vm->stopTextToSpeech();
+#endif
 	w.writeString(Res.DIFFICULTY_TEXT);
 	drawButtons(&w);
 
@@ -69,8 +93,8 @@ int DifficultyDialog::execute() {
 void DifficultyDialog::loadButtons() {
 	_sprites.load("choice.icn");
 
-	addButton(Common::Rect(68, 167, 158, 187),  Res.KeyConstants.DialogsDifficulty.KEY_ADVENTURER, &_sprites);
-	addButton(Common::Rect(166, 167, 256, 187), Res.KeyConstants.DialogsDifficulty.KEY_WARRIOR, &_sprites);
+	addButton(Common::Rect(68, 167, 158, 187),  Res.KeyConstants.DialogsDifficulty.KEY_ADVENTURER, &_sprites, kDifficultyAdventurer);
+	addButton(Common::Rect(166, 167, 256, 187), Res.KeyConstants.DialogsDifficulty.KEY_WARRIOR, &_sprites, kDifficultyWarrior);
 }
 
 } // End of namespace Xeen
diff --git a/engines/mm/xeen/dialogs/dialogs_dismiss.cpp b/engines/mm/xeen/dialogs/dialogs_dismiss.cpp
index c83cd533a93..9c6c07f0db7 100644
--- a/engines/mm/xeen/dialogs/dialogs_dismiss.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_dismiss.cpp
@@ -43,6 +43,7 @@ void Dismiss::execute() {
 	Window &w = windows[31];
 	w.open();
 
+	bool ttsVoiceText = true;
 	bool breakFlag = false;
 	while (!_vm->shouldExit() && !breakFlag) {
 		do {
@@ -51,9 +52,10 @@ void Dismiss::execute() {
 
 			w.frame();
 			w.fill();
-			w.writeString(Res.DISMISS_WHOM);
+			w.writeString(Res.DISMISS_WHOM, ttsVoiceText);
 			_iconSprites.draw(w, 0, Common::Point(225, 120));
 			w.update();
+			ttsVoiceText = false;
 
 			do {
 				events.pollEventsAndWait();
diff --git a/engines/mm/xeen/dialogs/dialogs_info.cpp b/engines/mm/xeen/dialogs/dialogs_info.cpp
index 1408bd99f58..95979ba2165 100644
--- a/engines/mm/xeen/dialogs/dialogs_info.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_info.cpp
@@ -26,6 +26,12 @@
 namespace MM {
 namespace Xeen {
 
+#ifdef USE_TTS
+
+static const uint8 kInfoTimeCount = 3;
+
+#endif
+
 void InfoDialog::show(XeenEngine *vm) {
 	InfoDialog *dlg = new InfoDialog(vm);
 	dlg->execute();
@@ -64,22 +70,55 @@ void InfoDialog::execute() {
 	Window &w = windows[28];
 	w.setBounds(Common::Rect(88, 20, 248, 112 + (_lines.empty() ? 0 : _lines.size() * 9 + 13)));
 	w.open();
-	w.writeString(details);
+	Common::String ttsMessage;
+	w.writeString(details, false, &ttsMessage);
+#ifdef USE_TTS
+	_vm->stopTextToSpeech();
+	speakText(ttsMessage);
+#endif
 
 	do {
 		events.updateGameCounter();
 		intf.draw3d(false, false);
 		w.frame();
-		w.writeString(details);
+		w.writeString(details, false);
 		w.update();
 
 		events.wait(1);
 	} while (!_vm->shouldExit() && !events.isKeyMousePressed());
 
+#ifdef USE_TTS
+	_vm->stopTextToSpeech();
+#endif
+
 	events.clearEvents();
 	w.close();
 }
 
+#ifdef USE_TTS
+
+void InfoDialog::speakText(const Common::String &text) const {
+	uint index = 0;
+
+	_vm->sayText(getNextTextSection(text, index, kInfoTimeCount));
+
+	Common::String timeInfo[kInfoTimeCount];
+	// Time, day, and year headers
+	for (uint i = 0; i < kInfoTimeCount; ++i) {
+		timeInfo[i] = getNextTextSection(text, index);
+	}
+
+	// Time, day, and year numbers
+	for (uint i = 0; i < kInfoTimeCount; ++i) {
+		_vm->sayText(timeInfo[i] + ": " + getNextTextSection(text, index));
+	}
+
+	// Any remaining lines
+	_vm->sayText(text.substr(index));
+}
+
+#endif
+
 void InfoDialog::protectionText() {
 	Party &party = *_vm->_party;
 //	Common::StringArray _lines;
diff --git a/engines/mm/xeen/dialogs/dialogs_info.h b/engines/mm/xeen/dialogs/dialogs_info.h
index f1e38ea68a3..f770ea7eeab 100644
--- a/engines/mm/xeen/dialogs/dialogs_info.h
+++ b/engines/mm/xeen/dialogs/dialogs_info.h
@@ -37,6 +37,10 @@ private:
 	void execute();
 
 	void protectionText();
+
+#ifdef USE_TTS
+	void speakText(const Common::String &text) const;
+#endif
 public:
 	static void show(XeenEngine *vm);
 };
diff --git a/engines/mm/xeen/dialogs/dialogs_input.cpp b/engines/mm/xeen/dialogs/dialogs_input.cpp
index 13e3c9956d9..750eea2d7d0 100644
--- a/engines/mm/xeen/dialogs/dialogs_input.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_input.cpp
@@ -83,7 +83,7 @@ int Input::getString(Common::String &line, uint maxLen, int maxWidth, bool isNum
 
 	_vm->_noDirectionSense = true;
 	Common::String msg = Common::String::format("\x3""l\t000\x4%03d\x3""c", maxWidth);
-	_window->writeString(msg);
+	_window->writeString(msg, false);
 	_window->update();
 
 	while (!_vm->shouldExit()) {
@@ -135,11 +135,14 @@ int Input::getString(Common::String &line, uint maxLen, int maxWidth, bool isNum
 
 		if (refresh) {
 			msg = Common::String::format("\x3""l\t000\x4%03d\x3""c%s", maxWidth, line.c_str());
-			_window->writeString(msg);
+			_window->writeString(msg, false);
 			_window->update();
 		}
 	}
 
+#ifdef USE_TTS
+	_vm->sayText(line, Common::TextToSpeechManager::INTERRUPT);
+#endif
 	_vm->_noDirectionSense = false;
 	return line.size();
 }
@@ -163,7 +166,7 @@ Common::KeyState Input::waitForKey(const Common::String &msg) {
 
 		if (flag)
 			intf.draw3d(false);
-		_window->writeString(msg);
+		_window->writeString(msg, false);
 		animateCursor();
 		_window->update();
 
@@ -175,7 +178,7 @@ Common::KeyState Input::waitForKey(const Common::String &msg) {
 			break;
 	}
 
-	_window->writeString("");
+	_window->writeString("", false);
 	_window->update();
 
 	intf._tillMove = oldTillMove;
diff --git a/engines/mm/xeen/dialogs/dialogs_items.cpp b/engines/mm/xeen/dialogs/dialogs_items.cpp
index 615903b4e4d..10faa78d2fe 100644
--- a/engines/mm/xeen/dialogs/dialogs_items.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_items.cpp
@@ -28,6 +28,58 @@
 namespace MM {
 namespace Xeen {
 
+enum ItemsButtonEnchantTTSTextIndex {
+	kItemsEnchantWeapons = 0,
+	kItemsEnchantArmor = 1,
+	kItemsEnchantAccessories = 2,
+	kItemsEnchantMisc = 3,
+	kItemsEnchantEnchant = 4,
+	kItemsEnchantExit = 5,
+	kItemsEnchantUse = 6,
+	kItemsEnchantItem1 = 7,
+	kItemsEnchantItem2 = 8,
+	kItemsEnchantItem3 = 9,
+	kItemsEnchantItem4 = 10,
+	kItemsEnchantItem5 = 11,
+	kItemsEnchantItem6 = 12,
+	kItemsEnchantItem7 = 13,
+	kItemsEnchantItem8 = 14,
+	kItemsEnchantItem9 = 15
+};
+
+enum kItemsButtonTTSTextIndex {
+	kItemsWeapons = 0,
+	kItemsArmor = 1,
+	kItemsAccessories = 2,
+	kItemsMisc = 3,
+	kItemsBuyOrEquip = 4,
+	kItemsSellOrRemove = 5,
+	kItemsIdentifyOrDiscard = 6,
+	kItemsFixOrQuest = 7,
+	kItemsExit = 8,
+	kItemsItem1 = 9,
+	kItemsItem2 = 10,
+	kItemsItem3 = 11,
+	kItemsItem4 = 12,
+	kItemsItem5 = 13,
+	kItemsItem6 = 14,
+	kItemsItem7 = 15,
+	kItemsItem8 = 16,
+	kItemsItem9 = 17
+};
+
+enum ItemSelectionButtonTTSTextIndex {
+	kItemSelectionItem1 = 0,
+	kItemSelectionItem2 = 1,
+	kItemSelectionItem3 = 2,
+	kItemSelectionItem4 = 3,
+	kItemSelectionItem5 = 4,
+	kItemSelectionItem6 = 5,
+	kItemSelectionItem7 = 6,
+	kItemSelectionItem8 = 7,
+	kItemSelectionItem9 = 8
+};
+
 Character *ItemsDialog::show(XeenEngine *vm, Character *c, ItemsMode mode) {
 	ItemsDialog *dlg = new ItemsDialog(vm);
 	Character *result = dlg->execute(c, mode);
@@ -70,6 +122,11 @@ Character *ItemsDialog::execute(Character *c, ItemsMode mode) {
 	windows[29].open();
 	windows[30].open();
 
+	Common::String buttonsText;
+#ifdef USE_TTS
+	uint buttonTextCount = 0;
+#endif
+
 	enum { REDRAW_NONE, REDRAW_TEXT, REDRAW_FULL } redrawFlag = REDRAW_FULL;
 	for (;;) {
 		if (redrawFlag == REDRAW_FULL) {
@@ -91,7 +148,8 @@ Character *ItemsDialog::execute(Character *c, ItemsMode mode) {
 				msg = Common::String::format(Res.ITEMS_DIALOG_TEXT2, Res.BTN_GOLD);
 			}
 
-			windows[29].writeString(msg);
+			buttonsText.clear();
+			windows[29].writeString(msg, false, &buttonsText);
 
 			Common::fill(&arr[0], &arr[40], 0);
 			itemIndex = -1;
@@ -101,6 +159,10 @@ Character *ItemsDialog::execute(Character *c, ItemsMode mode) {
 		if (mode != priorMode) {
 			// Set up the buttons for the dialog
 			loadButtons(mode, c, category);
+#ifdef USE_TTS
+			setButtonTexts(buttonsText);
+			buttonTextCount = _buttonTexts.size();
+#endif
 			priorMode = mode;
 			drawButtons(&windows[0]);
 		}
@@ -176,9 +238,19 @@ Character *ItemsDialog::execute(Character *c, ItemsMode mode) {
 					break;
 				}
 			}
+#ifdef USE_TTS
+			uint8 lineCount = lines.size();
+			// Make space for spells
+			_buttonTexts.resize(lines.size() + buttonTextCount - 1);
+#endif
+
 			while (lines.size() < INV_ITEMS_TOTAL)
 				lines.push_back("");
 
+			Common::String ttsMessage;
+#ifdef USE_TTS
+			uint8 headerCount = 0;
+#endif
 			// Draw out overall text and the list of items
 			switch (mode) {
 			case ITEMMODE_CHAR_INFO:
@@ -190,7 +262,11 @@ Character *ItemsDialog::execute(Character *c, ItemsMode mode) {
 					lines[0].c_str(), lines[1].c_str(), lines[2].c_str(), lines[3].c_str(),
 					lines[4].c_str(), lines[5].c_str(), lines[6].c_str(), lines[7].c_str(),
 					lines[8].c_str()
-				));
+				), false, &ttsMessage);
+#ifdef USE_TTS
+				// Misc category lists charges as well
+				headerCount = category == CATEGORY_MISC ? 2 : 1;
+#endif
 				break;
 
 			case ITEMMODE_BUY:
@@ -199,7 +275,10 @@ Character *ItemsDialog::execute(Character *c, ItemsMode mode) {
 					lines[0].c_str(), lines[1].c_str(), lines[2].c_str(), lines[3].c_str(),
 					lines[4].c_str(), lines[5].c_str(), lines[6].c_str(), lines[7].c_str(),
 					lines[8].c_str()
-				));
+				), false, &ttsMessage);
+#ifdef USE_TTS
+				headerCount = 3;
+#endif
 				break;
 
 			case ITEMMODE_SELL:
@@ -214,7 +293,10 @@ Character *ItemsDialog::execute(Character *c, ItemsMode mode) {
 					lines[0].c_str(), lines[1].c_str(), lines[2].c_str(), lines[3].c_str(),
 					lines[4].c_str(), lines[5].c_str(), lines[6].c_str(), lines[7].c_str(),
 					lines[8].c_str()
-				));
+				), false, &ttsMessage);
+#ifdef USE_TTS
+				headerCount = 2;
+#endif
 				break;
 
 			case ITEMMODE_3:
@@ -224,13 +306,26 @@ Character *ItemsDialog::execute(Character *c, ItemsMode mode) {
 					lines[0].c_str(), lines[1].c_str(), lines[2].c_str(), lines[3].c_str(),
 					lines[4].c_str(), lines[5].c_str(), lines[6].c_str(), lines[7].c_str(),
 					lines[8].c_str()
-					));
+					), false, &ttsMessage);
+#ifdef USE_TTS
+				headerCount = 2;
+#endif
 				break;
 
 			default:
 				break;
 			}
 
+#ifdef USE_TTS
+			if (lines[0] == Res.NO_ITEMS_AVAILABLE) {
+				// Speak headers and the "no items" message
+				uint index = 0;
+				_vm->sayText(getNextTextSection(ttsMessage, index, headerCount + 1));
+			} else {
+				speakText(ttsMessage, headerCount, lineCount);
+			}
+#endif
+
 			// Draw the glyphs for the items
 			windows[0].drawList(_itemsDrawList, INV_ITEMS_TOTAL);
 			windows[0].update();
@@ -278,6 +373,10 @@ Character *ItemsDialog::execute(Character *c, ItemsMode mode) {
 			continue;
 		}
 
+#ifdef USE_TTS
+		_vm->sayText(buttonsText);
+#endif
+
 		// Wait for a selection
 		_buttonValue = 0;
 		while (!_vm->shouldExit() && !_buttonValue) {
@@ -307,6 +406,9 @@ Character *ItemsDialog::execute(Character *c, ItemsMode mode) {
 
 				if (_buttonValue < (int)(_vm->_mode == MODE_COMBAT ? combat._combatParty.size() : party._activeParty.size())) {
 					// Character number is valid
+#ifdef USE_TTS
+					_vm->stopTextToSpeech();
+#endif
 					redrawFlag = REDRAW_FULL;
 					Character *newChar = _vm->_mode == MODE_COMBAT ? combat._combatParty[_buttonValue] : &party._activeParty[_buttonValue];
 
@@ -477,43 +579,43 @@ void ItemsDialog::loadButtons(ItemsMode mode, Character *&c, ItemCategory catego
 	clearButtons();
 	if (mode == ITEMMODE_ENCHANT || mode == ITEMMODE_RECHARGE || mode == ITEMMODE_TO_GOLD) {
 		// Enchant button list
-		addButton(Common::Rect(12, 109, 36, 129),   Res.KeyConstants.DialogsItems.KEY_WEAPONS, &_iconSprites);
-		addButton(Common::Rect(46, 109, 70, 129), Res.KeyConstants.DialogsItems.KEY_ARMOR, &_iconSprites);
-		addButton(Common::Rect(80, 109, 104, 129), Res.KeyConstants.DialogsItems.KEY_ACCESSORY, &_iconSprites);
-		addButton(Common::Rect(114, 109, 138, 129), Res.KeyConstants.DialogsItems.KEY_MISC, &_iconSprites);
-		addButton(Common::Rect(148, 109, 172, 129), Res.KeyConstants.DialogsItems.KEY_ENCHANT, &_iconSprites);
-		addButton(Common::Rect(284, 109, 308, 129), Common::KEYCODE_ESCAPE, &_iconSprites);
-		addButton(Common::Rect(148, 109, 172, 129), Res.KeyConstants.DialogsItems.KEY_USE, &_iconSprites);
-		addButton(Common::Rect(8, 20, 263, 28), Common::KEYCODE_1);
-		addButton(Common::Rect(8, 29, 263, 37), Common::KEYCODE_2);
-		addButton(Common::Rect(8, 38, 263, 46), Common::KEYCODE_3);
-		addButton(Common::Rect(8, 47, 263, 55), Common::KEYCODE_4);
-		addButton(Common::Rect(8, 56, 263, 64), Common::KEYCODE_5);
-		addButton(Common::Rect(8, 65, 263, 73), Common::KEYCODE_6);
-		addButton(Common::Rect(8, 74, 263, 82), Common::KEYCODE_7);
-		addButton(Common::Rect(8, 83, 263, 91), Common::KEYCODE_8);
-		addButton(Common::Rect(8, 92, 263, 100), Common::KEYCODE_9);
+		addButton(Common::Rect(12, 109, 36, 129),   Res.KeyConstants.DialogsItems.KEY_WEAPONS, &_iconSprites, kItemsEnchantWeapons);
+		addButton(Common::Rect(46, 109, 70, 129), Res.KeyConstants.DialogsItems.KEY_ARMOR, &_iconSprites, kItemsEnchantArmor);
+		addButton(Common::Rect(80, 109, 104, 129), Res.KeyConstants.DialogsItems.KEY_ACCESSORY, &_iconSprites, kItemsEnchantAccessories);
+		addButton(Common::Rect(114, 109, 138, 129), Res.KeyConstants.DialogsItems.KEY_MISC, &_iconSprites, kItemsEnchantMisc);
+		addButton(Common::Rect(148, 109, 172, 129), Res.KeyConstants.DialogsItems.KEY_ENCHANT, &_iconSprites, kItemsEnchantEnchant);
+		addButton(Common::Rect(284, 109, 308, 129), Common::KEYCODE_ESCAPE, &_iconSprites, kItemsEnchantExit);
+		addButton(Common::Rect(148, 109, 172, 129), Res.KeyConstants.DialogsItems.KEY_USE, &_iconSprites, kItemsEnchantUse);
+		addButton(Common::Rect(8, 20, 263, 28), Common::KEYCODE_1, nullptr, kItemsEnchantItem1);
+		addButton(Common::Rect(8, 29, 263, 37), Common::KEYCODE_2, nullptr, kItemsEnchantItem2);
+		addButton(Common::Rect(8, 38, 263, 46), Common::KEYCODE_3, nullptr, kItemsEnchantItem3);
+		addButton(Common::Rect(8, 47, 263, 55), Common::KEYCODE_4, nullptr, kItemsEnchantItem4);
+		addButton(Common::Rect(8, 56, 263, 64), Common::KEYCODE_5, nullptr, kItemsEnchantItem5);
+		addButton(Common::Rect(8, 65, 263, 73), Common::KEYCODE_6, nullptr, kItemsEnchantItem6);
+		addButton(Common::Rect(8, 74, 263, 82), Common::KEYCODE_7, nullptr, kItemsEnchantItem7);
+		addButton(Common::Rect(8, 83, 263, 91), Common::KEYCODE_8, nullptr, kItemsEnchantItem8);
+		addButton(Common::Rect(8, 92, 263, 100), Common::KEYCODE_9, nullptr, kItemsEnchantItem9);
 	} else {
 		bool flag = mode == ITEMMODE_BUY || mode == ITEMMODE_SELL || mode == ITEMMODE_IDENTIFY
 			|| mode == ITEMMODE_REPAIR;
-		addButton(Common::Rect(12, 109, 36, 129), Res.KeyConstants.DialogsItems.KEY_WEAPONS, &_iconSprites);
-		addButton(Common::Rect(46, 109, 70, 129), Res.KeyConstants.DialogsItems.KEY_ARMOR, &_iconSprites);
-		addButton(Common::Rect(80, 109, 104, 129), Res.KeyConstants.DialogsItems.KEY_ACCESSORY, &_iconSprites);
-		addButton(Common::Rect(114, 109, 138, 129), Res.KeyConstants.DialogsItems.KEY_MISC, &_iconSprites);
-		addButton(Common::Rect(148, 109, 172, 129), flag ? Res.KeyConstants.DialogsItems.KEY_BUY : Res.KeyConstants.DialogsItems.KEY_EQUIP, &_iconSprites);
-		addButton(Common::Rect(182, 109, 206, 129), flag ? Res.KeyConstants.DialogsItems.KEY_SELL : Res.KeyConstants.DialogsItems.KEY_REM, &_iconSprites);
-		addButton(Common::Rect(216, 109, 240, 129), flag ? Res.KeyConstants.DialogsItems.KEY_IDENTIFY : Res.KeyConstants.DialogsItems.KEY_DISC, &_iconSprites);
-		addButton(Common::Rect(250, 109, 274, 129), flag ? Res.KeyConstants.DialogsItems.KEY_FIX : Res.KeyConstants.DialogsItems.KEY_QUEST, &_iconSprites);
-		addButton(Common::Rect(284, 109, 308, 129), Common::KEYCODE_ESCAPE, &_iconSprites);
-		addButton(Common::Rect(8, 20, 263, 28), Common::KEYCODE_1);
-		addButton(Common::Rect(8, 29, 263, 37), Common::KEYCODE_2);
-		addButton(Common::Rect(8, 38, 263, 46), Common::KEYCODE_3);
-		addButton(Common::Rect(8, 47, 263, 55), Common::KEYCODE_4);
-		addButton(Common::Rect(8, 56, 263, 64), Common::KEYCODE_5);
-		addButton(Common::Rect(8, 65, 263, 73), Common::KEYCODE_6);
-		addButton(Common::Rect(8, 74, 263, 82), Common::KEYCODE_7);
-		addButton(Common::Rect(8, 83, 263, 91), Common::KEYCODE_8);
-		addButton(Common::Rect(8, 92, 263, 100), Common::KEYCODE_9);
+		addButton(Common::Rect(12, 109, 36, 129), Res.KeyConstants.DialogsItems.KEY_WEAPONS, &_iconSprites, kItemsWeapons);
+		addButton(Common::Rect(46, 109, 70, 129), Res.KeyConstants.DialogsItems.KEY_ARMOR, &_iconSprites, kItemsArmor);
+		addButton(Common::Rect(80, 109, 104, 129), Res.KeyConstants.DialogsItems.KEY_ACCESSORY, &_iconSprites, kItemsAccessories);
+		addButton(Common::Rect(114, 109, 138, 129), Res.KeyConstants.DialogsItems.KEY_MISC, &_iconSprites, kItemsMisc);
+		addButton(Common::Rect(148, 109, 172, 129), flag ? Res.KeyConstants.DialogsItems.KEY_BUY : Res.KeyConstants.DialogsItems.KEY_EQUIP, &_iconSprites, kItemsBuyOrEquip);
+		addButton(Common::Rect(182, 109, 206, 129), flag ? Res.KeyConstants.DialogsItems.KEY_SELL : Res.KeyConstants.DialogsItems.KEY_REM, &_iconSprites, kItemsSellOrRemove);
+		addButton(Common::Rect(216, 109, 240, 129), flag ? Res.KeyConstants.DialogsItems.KEY_IDENTIFY : Res.KeyConstants.DialogsItems.KEY_DISC, &_iconSprites, kItemsIdentifyOrDiscard);
+		addButton(Common::Rect(250, 109, 274, 129), flag ? Res.KeyConstants.DialogsItems.KEY_FIX : Res.KeyConstants.DialogsItems.KEY_QUEST, &_iconSprites, kItemsFixOrQuest);
+		addButton(Common::Rect(284, 109, 308, 129), Common::KEYCODE_ESCAPE, &_iconSprites, kItemsExit);
+		addButton(Common::Rect(8, 20, 263, 28), Common::KEYCODE_1, nullptr, kItemsItem1);
+		addButton(Common::Rect(8, 29, 263, 37), Common::KEYCODE_2, nullptr, kItemsItem2);
+		addButton(Common::Rect(8, 38, 263, 46), Common::KEYCODE_3, nullptr, kItemsItem3);
+		addButton(Common::Rect(8, 47, 263, 55), Common::KEYCODE_4, nullptr, kItemsItem4);
+		addButton(Common::Rect(8, 56, 263, 64), Common::KEYCODE_5, nullptr, kItemsItem5);
+		addButton(Common::Rect(8, 65, 263, 73), Common::KEYCODE_6, nullptr, kItemsItem6);
+		addButton(Common::Rect(8, 74, 263, 82), Common::KEYCODE_7, nullptr, kItemsItem7);
+		addButton(Common::Rect(8, 83, 263, 91), Common::KEYCODE_8, nullptr, kItemsItem8);
+		addButton(Common::Rect(8, 92, 263, 100), Common::KEYCODE_9, nullptr, kItemsItem9);
 		addPartyButtons(_vm);
 	}
 
@@ -743,8 +845,23 @@ int ItemsDialog::doItemOptions(Character &c, int actionIndex, int itemIndex, Ite
 		// Inventory is empty
 		return category == CATEGORY_MISC ? 0 : 2;
 
-	if (itemIndex < 0 || itemIndex > 8)
-		itemIndex = ItemSelectionDialog::show(actionIndex, items);
+	if (itemIndex < 0 || itemIndex > 8) {
+		// Populate item button texts. Text for items starts at keycode 1 and ends at keycode 9
+		Common::StringArray ttsItemButtonTexts;
+#ifdef USE_TTS
+		for (uint i = 0; i < _buttons.size(); ++i) {
+			if (_buttons[i]._value == Common::KeyCode::KEYCODE_1) {
+				if (_buttons[i]._ttsIndex >= _buttonTexts.size()) {
+					break;
+				}
+
+				ttsItemButtonTexts.assign(_buttonTexts.begin() + _buttons[i]._ttsIndex, _buttonTexts.end());
+				break;
+			}
+		}
+#endif
+		itemIndex = ItemSelectionDialog::show(actionIndex, items, ttsItemButtonTexts);
+	}
 
 	if (itemIndex != -1) {
 		XeenItem &item = items[itemIndex];
@@ -979,10 +1096,41 @@ void ItemsDialog::itemToGold(Character &c, int itemIndex, ItemCategory category,
 	}
 }
 
+#ifdef USE_TTS
+
+void ItemsDialog::speakText(const Common::String &text, uint8 headerCount, uint8 lineCount) {
+	uint index = 0;
+	_vm->sayText(getNextTextSection(text, index, headerCount));
+
+	uint startingIndex = 0;
+
+	for (uint i = 0; i < _buttonTexts.size(); ++i) {
+		if (_buttonTexts[i].empty()) {
+			startingIndex = i;
+			break;
+		}
+	}
+
+	// In some cases, each item has 3 fields: number, name, and cost. In others, it only has the number and name
+	// This generally corresponds to the number of fields in the header (i.e. "Weapons for Character" and "Cost" is 2 headers
+	// and 2 fields, versus just "Weapons for Character" that's 1 header with 1 field)
+	uint fieldsPerSection = (headerCount >= 2 || g_vm->_extOptions._showItemCosts) ? 3 : 2;
+	for (uint i = 0; i < lineCount; ++i) {
+		Common::String itemInfo = getNextTextSection(text, index, fieldsPerSection, ", ");
+		_vm->sayText(itemInfo);
+		
+		if (startingIndex != 0 && i + startingIndex < _buttonTexts.size()) {
+			_buttonTexts[i + startingIndex] = itemInfo;
+		}
+	}
+}
+
+#endif
+
 /*------------------------------------------------------------------------*/
 
-int ItemSelectionDialog::show(int actionIndex, InventoryItems &items) {
-	ItemSelectionDialog *dlg = new ItemSelectionDialog(g_vm, actionIndex, items);
+int ItemSelectionDialog::show(int actionIndex, InventoryItems &items, const Common::StringArray &ttsItemButtonTexts) {
+	ItemSelectionDialog *dlg = new ItemSelectionDialog(g_vm, actionIndex, items, ttsItemButtonTexts);
 	int result = dlg->execute();
 	delete dlg;
 
@@ -992,15 +1140,15 @@ int ItemSelectionDialog::show(int actionIndex, InventoryItems &items) {
 void ItemSelectionDialog::loadButtons() {
 	_icons.load("esc.icn");
 	addButton(Common::Rect(235, 111, 259, 131), Common::KEYCODE_ESCAPE, &_icons);
-	addButton(Common::Rect(8, 20, 263, 28), Common::KEYCODE_1);
-	addButton(Common::Rect(8, 29, 263, 37), Common::KEYCODE_2);
-	addButton(Common::Rect(8, 38, 263, 46), Common::KEYCODE_3);
-	addButton(Common::Rect(8, 47, 263, 55), Common::KEYCODE_4);
-	addButton(Common::Rect(8, 56, 263, 64), Common::KEYCODE_5);
-	addButton(Common::Rect(8, 65, 263, 73), Common::KEYCODE_6);
-	addButton(Common::Rect(8, 74, 263, 82), Common::KEYCODE_7);
-	addButton(Common::Rect(8, 83, 263, 91), Common::KEYCODE_8);
-	addButton(Common::Rect(8, 92, 263, 100), Common::KEYCODE_9);
+	addButton(Common::Rect(8, 20, 263, 28), Common::KEYCODE_1, nullptr, kItemSelectionItem1);
+	addButton(Common::Rect(8, 29, 263, 37), Common::KEYCODE_2, nullptr, kItemSelectionItem2);
+	addButton(Common::Rect(8, 38, 263, 46), Common::KEYCODE_3, nullptr, kItemSelectionItem3);
+	addButton(Common::Rect(8, 47, 263, 55), Common::KEYCODE_4, nullptr, kItemSelectionItem4);
+	addButton(Common::Rect(8, 56, 263, 64), Common::KEYCODE_5, nullptr, kItemSelectionItem5);
+	addButton(Common::Rect(8, 65, 263, 73), Common::KEYCODE_6, nullptr, kItemSelectionItem6);
+	addButton(Common::Rect(8, 74, 263, 82), Common::KEYCODE_7, nullptr, kItemSelectionItem7);
+	addButton(Common::Rect(8, 83, 263, 91), Common::KEYCODE_8, nullptr, kItemSelectionItem8);
+	addButton(Common::Rect(8, 92, 263, 100), Common::KEYCODE_9, nullptr, kItemSelectionItem9);
 }
 
 int ItemSelectionDialog::execute() {
@@ -1013,6 +1161,12 @@ int ItemSelectionDialog::execute() {
 	_icons.draw(0, 0, Common::Point(235, 111));
 	w.update();
 
+#ifdef USE_TTS
+	for (uint i = 0; i < _buttonTexts.size(); ++i) {
+		_vm->sayText(_buttonTexts[i]);
+	}
+#endif
+
 	int itemIndex = -1;
 	while (!_vm->shouldExit()) {
 		_buttonValue = 0;
diff --git a/engines/mm/xeen/dialogs/dialogs_items.h b/engines/mm/xeen/dialogs/dialogs_items.h
index 7c3f0361b63..12c01eac24f 100644
--- a/engines/mm/xeen/dialogs/dialogs_items.h
+++ b/engines/mm/xeen/dialogs/dialogs_items.h
@@ -73,6 +73,10 @@ private:
 		ItemCategory category, ItemsMode mode);
 
 	void itemToGold(Character &c, int itemIndex, ItemCategory category, ItemsMode mode);
+
+#ifdef USE_TTS
+	void speakText(const Common::String &text, uint8 headerCount, uint8 lineCount);
+#endif
 public:
 	static Character *show(XeenEngine *vm, Character *c, ItemsMode mode);
 };
@@ -83,9 +87,12 @@ private:
 	int _actionIndex;
 	InventoryItems &_items;
 
-	ItemSelectionDialog(XeenEngine *vm, int actionIndex, InventoryItems &items) : ButtonContainer(vm),
+	ItemSelectionDialog(XeenEngine *vm, int actionIndex, InventoryItems &items, const Common::StringArray &ttsItemButtonTexts) : ButtonContainer(vm),
 			_actionIndex(actionIndex), _items(items) {
 		loadButtons();
+#ifdef USE_TTS
+		_buttonTexts = ttsItemButtonTexts;
+#endif
 	}
 
 	/**
@@ -101,11 +108,12 @@ private:
 public:
 	/**
 	 * Shows the dialog
-	 * @param actionIndex		Current action type
-	 * @param items				Currently active items category
-	 * @returns					Selected item index
+	 * @param actionIndex			Current action type
+	 * @param items					Currently active items category
+	 * @param ttsItemButtonTexts	Text for each item button
+	 * @returns						Selected item index
 	 */
-	static int show(int actionIndex, InventoryItems &items);
+	static int show(int actionIndex, InventoryItems &items, const Common::StringArray &ttsItemButtonTexts);
 };
 
 
diff --git a/engines/mm/xeen/dialogs/dialogs_map.cpp b/engines/mm/xeen/dialogs/dialogs_map.cpp
index 7c65c239d09..4ecb788a859 100644
--- a/engines/mm/xeen/dialogs/dialogs_map.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_map.cpp
@@ -78,6 +78,11 @@ void MapDialog::execute() {
 	windows[5].open();
 	bool drawFlag = true;
 
+#ifdef USE_TTS
+	_vm->stopTextToSpeech();
+#endif
+	bool ttsVoiceText = true;
+
 	events.updateGameCounter();
 	do {
 		if (drawFlag)
@@ -105,7 +110,8 @@ void MapDialog::execute() {
 
 		windows[5].writeString(Common::String::format(Res.MAP_TEXT,
 			map._mazeName.c_str(), party._mazePosition.x,
-			party._mazePosition.y, Res.DIRECTION_TEXT[party._mazeDirection]));
+			party._mazePosition.y, Res.DIRECTION_TEXT[party._mazeDirection]), ttsVoiceText);
+		ttsVoiceText = false;
 		windows[5].update();
 		windows[3].update();
 
@@ -113,6 +119,9 @@ void MapDialog::execute() {
 		drawFlag = false;
 	} while (!_vm->shouldExit() && !events.isKeyMousePressed());
 
+#ifdef USE_TTS
+	_vm->stopTextToSpeech();
+#endif
 	events.clearEvents();
 	windows[5].close();
 }
diff --git a/engines/mm/xeen/dialogs/dialogs_message.cpp b/engines/mm/xeen/dialogs/dialogs_message.cpp
index 1b17142bdd4..7f8d65d0c37 100644
--- a/engines/mm/xeen/dialogs/dialogs_message.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_message.cpp
@@ -38,6 +38,9 @@ void MessageDialog::execute(const Common::String &msg, MessageWaitType waitType)
 	Windows &windows = *_vm->_windows;
 	Window &w = windows[6];
 
+#ifdef USE_TTS
+	_vm->stopTextToSpeech();
+#endif
 	w.open();
 	w.writeString(msg);
 	w.update();
@@ -48,6 +51,9 @@ void MessageDialog::execute(const Common::String &msg, MessageWaitType waitType)
 			events.pollEventsAndWait();
 
 		events.clearEvents();
+#ifdef USE_TTS
+		_vm->stopTextToSpeech();
+#endif
 		break;
 
 	case WT_ANIMATED_WAIT:
@@ -66,6 +72,9 @@ void MessageDialog::execute(const Common::String &msg, MessageWaitType waitType)
 			if (checkEvents(_vm))
 				break;
 		} while (!_vm->shouldExit() && !_buttonValue);
+#ifdef USE_TTS
+		_vm->stopTextToSpeech();
+#endif
 		break;
 
 	case WT_LOC_WAIT:
@@ -117,6 +126,9 @@ void CantCast::execute(int spellId, int componentNum) {
 	events.clearEvents();
 
 	w.close();
+#ifdef USE_TTS
+	_vm->stopTextToSpeech();
+#endif
 	_vm->_mode = oldMode;
 }
 
diff --git a/engines/mm/xeen/dialogs/dialogs_party.cpp b/engines/mm/xeen/dialogs/dialogs_party.cpp
index f1717cf75fd..7ec1d817c55 100644
--- a/engines/mm/xeen/dialogs/dialogs_party.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_party.cpp
@@ -32,8 +32,32 @@
 namespace MM {
 namespace Xeen {
 
+#ifdef USE_TTS
+
+static const uint8 kPartyDialogFaceCount = 4;
+static const uint8 kPartyDialogButtonCount = 6;
+static const uint8 kPartyDialogInfoPerFaceCount = 4;
+
+#endif
+
+enum PartyDialogButtonTTSTextIndex {
+	kPartyDialogUp = 0,
+	kPartyDialogDown = 1,
+	kPartyDialogDelete = 2,
+	kPartyDialogRemove = 3,
+	kPartyDialogCreate = 4,
+	kPartyDialogExit = 5,
+	kPartyDialogFace1 = 6,
+	kPartyDialogFace2 = 7,
+	kPartyDialogFace3 = 8,
+	kPartyDialogFace4 = 9
+};
+
 PartyDialog::PartyDialog(XeenEngine *vm) : ButtonContainer(vm),
 		PartyDrawer(vm), _vm(vm) {
+#ifdef USE_TTS
+	_faceCount = 0;
+#endif
 	initDrawStructs();
 }
 
@@ -58,6 +82,11 @@ void PartyDialog::execute() {
 	loadButtons();
 	setupBackground();
 
+	Common::String ttsMessage;
+#ifdef USE_TTS
+	bool returnFromCreate = false;
+#endif
+
 	while (!_vm->shouldExit()) {
 		_vm->_mode = MODE_INTERACTIVE;
 
@@ -68,7 +97,13 @@ void PartyDialog::execute() {
 		Window &w = windows[11];
 		w.open();
 		setupFaces(startingChar, false);
-		w.writeString(Common::String::format(Res.PARTY_DIALOG_TEXT, _partyDetails.c_str()));
+		w.writeString(Common::String::format(Res.PARTY_DIALOG_TEXT, _partyDetails.c_str()), false, &ttsMessage);
+		
+#ifdef USE_TTS
+		speakText(ttsMessage, returnFromCreate);
+		returnFromCreate = false;
+#endif
+
 		w.drawList(&_faceDrawStructs[0], 4);
 
 		_uiSprites.draw(w, 0, Common::Point(16, 100));
@@ -206,6 +241,9 @@ void PartyDialog::execute() {
 					screen.fadeOut();
 					screen.blitFrom(savedBg);
 					windows[0].update();
+#ifdef USE_TTS
+					returnFromCreate = true;
+#endif
 
 					modeFlag = true;
 					breakFlag = true;
@@ -274,13 +312,13 @@ void PartyDialog::loadCharacters() {
 
 void PartyDialog::loadButtons() {
 	_uiSprites.load("inn.icn");
-	addButton(Common::Rect(16, 100, 40, 120), Common::KEYCODE_UP, &_uiSprites);
-	addButton(Common::Rect(52, 100, 76, 120), Common::KEYCODE_DOWN, &_uiSprites);
+	addButton(Common::Rect(16, 100, 40, 120), Common::KEYCODE_UP, &_uiSprites, kPartyDialogUp);
+	addButton(Common::Rect(52, 100, 76, 120), Common::KEYCODE_DOWN, &_uiSprites, kPartyDialogDown);
 
-	addButton(Common::Rect(87, 100, 111, 120), Res.KeyConstants.DialogsParty.KEY_DELETE, &_uiSprites);
-	addButton(Common::Rect(122, 100, 146, 120), Res.KeyConstants.DialogsParty.KEY_REMOVE, &_uiSprites);
-	addButton(Common::Rect(157, 100, 181, 120), Res.KeyConstants.DialogsParty.KEY_CREATE, &_uiSprites);
-	addButton(Common::Rect(192, 100, 216, 120), Res.KeyConstants.DialogsParty.KEY_EXIT, &_uiSprites);
+	addButton(Common::Rect(87, 100, 111, 120), Res.KeyConstants.DialogsParty.KEY_DELETE, &_uiSprites, kPartyDialogDelete);
+	addButton(Common::Rect(122, 100, 146, 120), Res.KeyConstants.DialogsParty.KEY_REMOVE, &_uiSprites, kPartyDialogRemove);
+	addButton(Common::Rect(157, 100, 181, 120), Res.KeyConstants.DialogsParty.KEY_CREATE, &_uiSprites, kPartyDialogCreate);
+	addButton(Common::Rect(192, 100, 216, 120), Res.KeyConstants.DialogsParty.KEY_EXIT, &_uiSprites, kPartyDialogExit);
 
 	addButton(Common::Rect(0, 0, 0, 0), Common::KEYCODE_ESCAPE);
 }
@@ -309,11 +347,14 @@ void PartyDialog::setupFaces(int firstDisplayChar, bool updateFlag) {
 	// Reset the button areas for the display character images
 	while (_buttons.size() > 7)
 		_buttons.remove_at(7);
-	addButton(Common::Rect(16, 16, 48, 48), Common::KEYCODE_1);
-	addButton(Common::Rect(117, 16, 149, 48), Common::KEYCODE_2);
-	addButton(Common::Rect(59, 59, 91, 91), Common::KEYCODE_3);
-	addButton(Common::Rect(117, 59, 151, 91), Common::KEYCODE_4);
+	addButton(Common::Rect(16, 16, 48, 48), Common::KEYCODE_1, nullptr, kPartyDialogFace1);
+	addButton(Common::Rect(117, 16, 149, 48), Common::KEYCODE_2, nullptr, kPartyDialogFace2);
+	addButton(Common::Rect(59, 59, 91, 91), Common::KEYCODE_3, nullptr, kPartyDialogFace3);
+	addButton(Common::Rect(117, 59, 151, 91), Common::KEYCODE_4, nullptr, kPartyDialogFace4);
 
+#ifdef USE_TTS
+	_faceCount = 0;
+#endif
 
 	for (posIndex = 0; posIndex < 4; ++posIndex) {
 		charId = (firstDisplayChar + posIndex) >= (int)_charList.size() ? -1 :
@@ -326,6 +367,10 @@ void PartyDialog::setupFaces(int firstDisplayChar, bool updateFlag) {
 			break;
 		}
 
+#ifdef USE_TTS
+		_faceCount++;
+#endif
+
 		Common::Rect &b = _buttons[7 + posIndex]._bounds;
 		b.moveTo((posIndex & 1) ? 117 : 16, b.top);
 		Character &ps = party._roster[_charList[firstDisplayChar + posIndex]];
@@ -359,7 +404,11 @@ void PartyDialog::startingCharChanged(int firstDisplayChar) {
 	Window &w = windows[11];
 
 	setupFaces(firstDisplayChar, true);
-	w.writeString(Common::String::format(Res.PARTY_DIALOG_TEXT, _partyDetails.c_str()));
+	Common::String ttsMessage;
+	w.writeString(Common::String::format(Res.PARTY_DIALOG_TEXT, _partyDetails.c_str()), false, &ttsMessage);
+#ifdef USE_TTS
+	speakText(ttsMessage, false);
+#endif
 	w.drawList(_faceDrawStructs, 4);
 
 	_uiSprites.draw(w, 0, Common::Point(16, 100));
@@ -391,10 +440,10 @@ int PartyDialog::selectCharacter(bool isDelete, int firstDisplayChar) {
 	saveButtons();
 	addButton(Common::Rect(225, isDelete ? 120 : 84, 249, isDelete ? 140 : 104),
 		Common::KEYCODE_ESCAPE, &iconSprites);
-	addButton(Common::Rect(16, 16, 48, 48), Common::KEYCODE_1);
-	addButton(Common::Rect(117, 16, 149, 48), Common::KEYCODE_2);
-	addButton(Common::Rect(16, 59, 48, 91), Common::KEYCODE_3);
-	addButton(Common::Rect(117, 59, 149, 91), Common::KEYCODE_4);
+	addButton(Common::Rect(16, 16, 48, 48), Common::KEYCODE_1, nullptr, kPartyDialogFace1);
+	addButton(Common::Rect(117, 16, 149, 48), Common::KEYCODE_2, nullptr, kPartyDialogFace2);
+	addButton(Common::Rect(16, 59, 48, 91), Common::KEYCODE_3, nullptr, kPartyDialogFace3);
+	addButton(Common::Rect(117, 59, 149, 91), Common::KEYCODE_4, nullptr, kPartyDialogFace4);
 	addPartyButtons(_vm);
 
 	int result = -1, v;
@@ -444,5 +493,39 @@ int PartyDialog::selectCharacter(bool isDelete, int firstDisplayChar) {
 	return result == -1 ? 0 : result;
 }
 
+#ifdef USE_TTS
+
+void PartyDialog::speakText(const Common::String &text, bool isCharacterCreation) {
+	uint index = 0;
+
+	// After returning from the character creation screen, the text is duplicated, with the first iteration being
+	// outdated
+	if (isCharacterCreation) {
+		for (uint i = 0; i < _faceCount; ++i) {
+			getNextTextSection(text, index, kPartyDialogInfoPerFaceCount);
+		}
+
+		getNextTextSection(text, index, kPartyDialogButtonCount);
+	}
+
+	_buttonTexts.clear();
+
+	Common::String faceTexts[kPartyDialogFaceCount];
+	for (uint8 i = 0; i < _faceCount; ++i) {
+		faceTexts[i] = getNextTextSection(text, index, kPartyDialogInfoPerFaceCount);
+		_vm->sayText(faceTexts[i]);
+	}
+
+	Common::String buttonTexts = addNextTextToButtons(text, index, kPartyDialogButtonCount);
+	_vm->sayText(buttonTexts);
+
+	// Since the number of faces can vary, sort them after the bottom buttons
+	for (uint8 i = 0; i < _faceCount; ++i) {
+		_buttonTexts.push_back(faceTexts[i]);
+	}
+}
+
+#endif
+
 } // End of namespace Xeen
 } // End of namespace MM
diff --git a/engines/mm/xeen/dialogs/dialogs_party.h b/engines/mm/xeen/dialogs/dialogs_party.h
index 5eed32c03ac..d89f1fd6bc2 100644
--- a/engines/mm/xeen/dialogs/dialogs_party.h
+++ b/engines/mm/xeen/dialogs/dialogs_party.h
@@ -41,6 +41,9 @@ private:
 	DrawStruct _faceDrawStructs[4];
 	Common::String _partyDetails;
 	Common::Array<int> _charList;
+#ifdef USE_TTS
+	uint8 _faceCount;
+#endif
 
 	/**
 	 * Constructor
@@ -80,6 +83,10 @@ private:
 	void startingCharChanged(int firstDisplayChar);
 
 	int selectCharacter(bool isDelete, int firstDisplayChar);
+
+#ifdef USE_TTS
+	void speakText(const Common::String &text, bool isCharacterCreation);
+#endif
 public:
 	/**
 	 * Show the Party dialog
diff --git a/engines/mm/xeen/dialogs/dialogs_query.cpp b/engines/mm/xeen/dialogs/dialogs_query.cpp
index 5b5a8cd34fe..ca2fa77591b 100644
--- a/engines/mm/xeen/dialogs/dialogs_query.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_query.cpp
@@ -61,6 +61,9 @@ bool Confirm::execute(const Common::String &msg, int mode) {
 		}
 	}
 
+#ifdef USE_TTS
+	_vm->stopTextToSpeech();
+#endif
 	w.writeString(msg);
 	w.update();
 
@@ -84,6 +87,9 @@ bool Confirm::execute(const Common::String &msg, int mode) {
 		}
 	}
 
+#ifdef USE_TTS
+	_vm->stopTextToSpeech();
+#endif
 	events.clearEvents();
 	w.close();
 	return result;
diff --git a/engines/mm/xeen/dialogs/dialogs_quests.cpp b/engines/mm/xeen/dialogs/dialogs_quests.cpp
index f127c7dae96..0c33317124f 100644
--- a/engines/mm/xeen/dialogs/dialogs_quests.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_quests.cpp
@@ -28,6 +28,15 @@
 namespace MM {
 namespace Xeen {
 
+enum QuestButtonTTSTextIndex {
+	kQuestItems = 0,
+	kQuestCurrentQuests = 1,
+	kQuestAutoNotes = 2,
+	kQuestUp = 3,
+	kQuestDown = 4,
+	kQuestExit = 5
+};
+
 #define MAX_DIALOG_LINES 128
 
 void Quests::show(XeenEngine *vm) {
@@ -61,8 +70,14 @@ void Quests::execute() {
 		windowFlag = true;
 	}
 
-	windows[29].writeString(Res.QUESTS_DIALOG_TEXT);
+	Common::String ttsButtons;
+	windows[29].writeString(Res.QUESTS_DIALOG_TEXT, false, &ttsButtons);
+#ifdef USE_TTS
+	_vm->stopTextToSpeech();
+	setButtonTexts(ttsButtons);
+#endif
 	drawButtons(&windows[0]);
+	Common::String ttsMessage;
 
 	while (!_vm->shouldExit()) {
 		Common::String lines[MAX_DIALOG_LINES];
@@ -116,6 +131,7 @@ void Quests::execute() {
 				}
 			}
 
+			ttsMessage.clear();
 			if (count == 0) {
 				windows[30].writeString(Res.NO_QUEST_ITEMS);
 			} else {
@@ -125,7 +141,11 @@ void Quests::execute() {
 					lines[topRow + 4].c_str(), lines[topRow + 5].c_str(),
 					lines[topRow + 6].c_str(), lines[topRow + 7].c_str(),
 					lines[topRow + 8].c_str()
-				));
+				), false, &ttsMessage);
+				ttsMessage.replace('*', ' ');
+#ifdef USE_TTS
+				_vm->sayText(ttsMessage);
+#endif
 			}
 			break;
 
@@ -152,8 +172,13 @@ void Quests::execute() {
 			if (count == 0)
 				lines[1] = Res.NO_CURRENT_QUESTS;
 
+			ttsMessage.clear();
 			windows[30].writeString(Common::String::format(Res.CURRENT_QUESTS_DATA,
-				lines[topRow].c_str(), lines[topRow + 1].c_str(), lines[topRow + 2].c_str()));
+				lines[topRow].c_str(), lines[topRow + 1].c_str(), lines[topRow + 2].c_str()), false, &ttsMessage);
+			ttsMessage.replace('*', ' ');
+#ifdef USE_TTS
+			_vm->sayText(ttsMessage);
+#endif
 			break;
 
 		case AUTO_NOTES: {
@@ -211,6 +236,9 @@ void Quests::execute() {
 
 		windows[30].writeString("\v000\t000");
 		windows[24].update();
+#ifdef USE_TTS
+		g_vm->sayText(ttsButtons);
+#endif
 
 		// Key handling
 		_buttonValue = 0;
@@ -260,13 +288,13 @@ void Quests::execute() {
 void Quests::addButtons() {
 	_iconSprites.load("quest.icn");
 
-	addButton(Common::Rect(12, 109, 36, 129),   Res.KeyConstants.DialogsQuests.KEY_QUEST_ITEMS, &_iconSprites);
-	addButton(Common::Rect(80, 109, 104, 129),  Res.KeyConstants.DialogsQuests.KEY_CURRENT_QUESTS, &_iconSprites);
-	addButton(Common::Rect(148, 109, 172, 129), Res.KeyConstants.DialogsQuests.KEY_AUTO_NOTES, &_iconSprites);
+	addButton(Common::Rect(12, 109, 36, 129),   Res.KeyConstants.DialogsQuests.KEY_QUEST_ITEMS, &_iconSprites, kQuestItems);
+	addButton(Common::Rect(80, 109, 104, 129),  Res.KeyConstants.DialogsQuests.KEY_CURRENT_QUESTS, &_iconSprites, kQuestCurrentQuests);
+	addButton(Common::Rect(148, 109, 172, 129), Res.KeyConstants.DialogsQuests.KEY_AUTO_NOTES, &_iconSprites, kQuestAutoNotes);
 
-	addButton(Common::Rect(216, 109, 240, 129), Common::KEYCODE_UP, &_iconSprites);
-	addButton(Common::Rect(250, 109, 274, 129), Common::KEYCODE_DOWN, &_iconSprites);
-	addButton(Common::Rect(284, 109, 308, 129), Common::KEYCODE_ESCAPE, &_iconSprites);
+	addButton(Common::Rect(216, 109, 240, 129), Common::KEYCODE_UP, &_iconSprites, kQuestUp);
+	addButton(Common::Rect(250, 109, 274, 129), Common::KEYCODE_DOWN, &_iconSprites, kQuestDown);
+	addButton(Common::Rect(284, 109, 308, 129), Common::KEYCODE_ESCAPE, &_iconSprites, kQuestExit);
 }
 
 void Quests::loadQuestNotes() {
diff --git a/engines/mm/xeen/dialogs/dialogs_quick_fight.cpp b/engines/mm/xeen/dialogs/dialogs_quick_fight.cpp
index 12c299ed7d4..45fc813a35c 100644
--- a/engines/mm/xeen/dialogs/dialogs_quick_fight.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_quick_fight.cpp
@@ -26,6 +26,18 @@
 namespace MM {
 namespace Xeen {
 
+#ifdef USE_TTS
+
+static const uint8 kQuickFightInfoCount = 4;
+static const uint8 kQuickFightButtonCount = 2;
+
+#endif
+
+enum QuickFightButtonTTSTextIndex {
+	kQuickFightNext = 0,
+	kQuickFightExit = 1
+};
+
 void QuickFight::show(XeenEngine *vm, Character *currentChar) {
 	QuickFight *dlg = new QuickFight(vm, currentChar);
 	dlg->execute();
@@ -45,15 +57,24 @@ void QuickFight::execute() {
 	Windows &windows = *_vm->_windows;
 	Window &w = windows[10];
 	w.open();
+	bool ttsVoiceText = true;
 
 	do {
 		// Draw the dialog text and buttons
 		Common::String msg = Common::String::format(Res.QUICK_FIGHT_TEXT,
 			_currentChar->_name.c_str(),
 			Res.QUICK_FIGHT_OPTIONS[_currentChar->_quickOption]);
-		w.writeString(msg);
+		Common::String ttsMessage;
+		w.writeString(msg, ttsVoiceText, &ttsMessage);
 		drawButtons(&w);
 
+#ifdef USE_TTS
+		if (ttsVoiceText) {
+			setUpButtons(ttsMessage);
+			ttsVoiceText = false;
+		}
+#endif
+
 		// Wait for selection
 		_buttonValue = 0;
 		events.updateGameCounter();
@@ -76,11 +97,17 @@ void QuickFight::execute() {
 			if (charIdx < (int)combat._combatParty.size()) {
 				// Highlight new character
 				_currentChar = &party._activeParty[charIdx];
+#ifdef USE_TTS
+				_vm->sayText(_currentChar->_name + ": " + Res.QUICK_FIGHT_OPTIONS[_currentChar->_quickOption], Common::TextToSpeechManager::INTERRUPT);
+#endif	
 				intf.highlightChar(charIdx);
 			}
 		} else if (Common::KEYCODE_n == _buttonValue || 
 				   Res.KeyConstants.DialogsQuickFight.KEY_NEXT == _buttonValue) {
 			_currentChar->_quickOption = (QuickAction)(((int)_currentChar->_quickOption + 1) % 4);
+#ifdef USE_TTS
+			_vm->sayText(Res.QUICK_FIGHT_OPTIONS[_currentChar->_quickOption], Common::TextToSpeechManager::INTERRUPT);
+#endif
 		}
 	} while (_buttonValue != Common::KEYCODE_RETURN && _buttonValue != Common::KEYCODE_ESCAPE);
 
@@ -90,10 +117,22 @@ void QuickFight::execute() {
 
 void QuickFight::loadButtons() {
 	_icons.load("train.icn");
-	addButton(Common::Rect(281, 108, 305, 128), Common::KEYCODE_ESCAPE, &_icons);
+	addButton(Common::Rect(281, 108, 305, 128), Common::KEYCODE_ESCAPE, &_icons, kQuickFightExit);
 
-	addButton(Common::Rect(242, 108, 266, 128), Res.KeyConstants.DialogsQuickFight.KEY_NEXT, &_icons);
+	addButton(Common::Rect(242, 108, 266, 128), Res.KeyConstants.DialogsQuickFight.KEY_NEXT, &_icons, kQuickFightNext);
 }
 
+#ifdef USE_TTS
+
+void QuickFight::setUpButtons(const Common::String &text) {
+	if (_buttonTexts.empty()) {
+		uint index = 0;
+		getNextTextSection(text, index, kQuickFightInfoCount);
+		addNextTextToButtons(text, index, kQuickFightButtonCount);
+	}
+}
+
+#endif
+
 } // End of namespace Xeen
 } // End of namespace MM
diff --git a/engines/mm/xeen/dialogs/dialogs_quick_fight.h b/engines/mm/xeen/dialogs/dialogs_quick_fight.h
index 6cc2ae855ae..0efacdb947c 100644
--- a/engines/mm/xeen/dialogs/dialogs_quick_fight.h
+++ b/engines/mm/xeen/dialogs/dialogs_quick_fight.h
@@ -48,6 +48,14 @@ private:
 	 * Load butons for the dialog
 	 */
 	void loadButtons();
+
+#ifdef USE_TTS
+	/**
+	 * Sets up button texts
+	 * @param text	Text for buttons, with each button text separated by newlines
+	 */
+	void setUpButtons(const Common::String &text);
+#endif
 public:
 	/**
 	 * Show the dialog
diff --git a/engines/mm/xeen/dialogs/dialogs_quick_ref.cpp b/engines/mm/xeen/dialogs/dialogs_quick_ref.cpp
index 70ce7e2dc41..f00e96af271 100644
--- a/engines/mm/xeen/dialogs/dialogs_quick_ref.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_quick_ref.cpp
@@ -27,6 +27,13 @@
 namespace MM {
 namespace Xeen {
 
+#ifdef USE_TTS
+
+static const uint8 kQuickRefUpperHeaderCount = 8;
+static const uint8 kQuickRefLowerHeaderCount = 3;
+
+#endif
+
 void QuickReferenceDialog::show(XeenEngine *vm) {
 	QuickReferenceDialog *dlg = new QuickReferenceDialog(vm);
 	dlg->execute();
@@ -59,8 +66,9 @@ void QuickReferenceDialog::execute() {
 	Common::String lines[8];
 
 	events.setCursor(0);
-	for (uint idx = 0; idx < (combat._globalCombat == 2 ? combat._combatParty.size() :
-			party._activeParty.size()); ++idx) {
+
+	uint partySize = combat._globalCombat == 2 ? combat._combatParty.size() : party._activeParty.size();
+	for (uint idx = 0; idx < partySize; ++idx) {
 		Character &c = combat._globalCombat == 2 ? *combat._combatParty[idx] :
 			party._activeParty[idx];
 		const char **tmpConditions = c._sex == FEMALE ? (const char **)Res.CONDITION_NAMES_F : (const char **)Res.CONDITION_NAMES_M;
@@ -92,20 +100,66 @@ void QuickReferenceDialog::execute() {
 	if (!windowOpen)
 		w.open();
 
+	Common::String ttsMessage;
+
 	// Turn off reduced font mode and then print everything
 	w.writeString("\1");
-	w.writeString(msg);
+	w.writeString(msg, false, &ttsMessage);
 	w.update();
 
+#ifdef USE_TTS
+	speakText(ttsMessage, partySize);
+#endif
+
 	// Wait for a key/mouse press
 	events.clearEvents();
 	while (!_vm->shouldExit() && !events.isKeyMousePressed())
 		events.pollEventsAndWait();
+#ifdef USE_TTS
+	_vm->stopTextToSpeech();
+#endif
 	events.clearEvents();
 
 	if (!windowOpen)
 		w.close();
 }
 
+#ifdef USE_TTS
+
+void QuickReferenceDialog::speakText(const Common::String &text, uint partySize) const {
+	uint index = 0;
+
+	// "Quick reference chart"
+	_vm->sayText(getNextTextSection(text, index), Common::TextToSpeechManager::INTERRUPT);
+
+	// Split the header descriptors ("Name", "Cond", etc.) into an array
+	// If we voice everything as originally ordered, the context isn't clear (i.e. "Name, Class, Level, ..."
+	// will be voiced as one piece, which may make it difficult to match each field with the corresponding info of
+	// each party member)
+	Common::String headers[kQuickRefUpperHeaderCount];
+	for (uint8 i = 0; i < kQuickRefUpperHeaderCount; ++i) {
+		headers[i] = getNextTextSection(text, index);
+	}
+
+	// Voice each party member's description
+	for (uint i = 0; i < partySize; ++i) {
+		for (uint8 j = 0; j < kQuickRefUpperHeaderCount; ++j) {
+			_vm->sayText(headers[j] + ": " + getNextTextSection(text, index));
+		}
+	}
+
+	// Split the gold/gems/food descriptors into an array
+	for (uint8 i = 0; i < kQuickRefLowerHeaderCount; ++i) {
+		headers[i] = getNextTextSection(text, index);
+	}
+
+	// Voice the gold/gems/food amounts
+	for (uint8 i = 0; i < kQuickRefLowerHeaderCount; ++i) {
+		_vm->sayText(headers[i] + ": " + getNextTextSection(text, index));
+	}
+}
+
+#endif
+
 } // End of namespace Xeen
 } // End of namespace MM
diff --git a/engines/mm/xeen/dialogs/dialogs_quick_ref.h b/engines/mm/xeen/dialogs/dialogs_quick_ref.h
index cb3e3adbeb4..5115f97df93 100644
--- a/engines/mm/xeen/dialogs/dialogs_quick_ref.h
+++ b/engines/mm/xeen/dialogs/dialogs_quick_ref.h
@@ -37,6 +37,10 @@ private:
 	const char *getDaysPlurals(int val);
 
 	void execute();
+
+#ifdef USE_TTS
+	void speakText(const Common::String &text, uint partySize) const;
+#endif
 public:
 	static void show(XeenEngine *vm);
 };
diff --git a/engines/mm/xeen/dialogs/dialogs_spells.cpp b/engines/mm/xeen/dialogs/dialogs_spells.cpp
index d224b449805..f4bdf62d51e 100644
--- a/engines/mm/xeen/dialogs/dialogs_spells.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_spells.cpp
@@ -30,6 +30,72 @@
 namespace MM {
 namespace Xeen {
 
+#ifdef USE_TTS
+
+static const uint8 kSpellsButtonCount = 3;
+static const uint8 kSpellsSpellCount = 10;
+
+static const uint8 kSelectElementButtonCount = 4;
+
+static const uint8 kLloydsBeaconInfoCount = 5;
+static const uint8 kLloydsBeaconButtonCount = 2;
+
+static const uint8 kIdentifyMonsterHeaderCount = 5;
+static const uint8 kIdentifyMonsterMaxMonsterCount = 3;
+
+static const char *selectButton[] = {
+	"Select",				// English
+	"O.K.",					// German
+	"S\202lectionner",		// French (not translated in-game)
+	"Seleccionar",			// Spanish (shown as "Selecc" in-game)
+	"\x82\xeb\xa1\xae\xe0",	// Russian (Выбор)
+	"\xbf\xef\xa9\x77"		// Chinese (選定)
+};
+
+static const char *exitButton[] = {
+	"Exit",					// English
+	"Ende",					// German
+	"Sortir",				// French (not translated in-game)
+	"ESC",					// Spanish
+	"\x82\xeb\xe5\xae\xa4",	// Russian (Выход)
+	"\xc2\xf7"				// Chinese (離)
+};
+
+#endif
+
+enum SpellsButtonTTSTextIndex {
+	kSpellsSpell1 = 0,
+	kSpellsSpell2 = 1,
+	kSpellsSpell3 = 2,
+	kSpellsSpell4 = 3,
+	kSpellsSpell5 = 4,
+	kSpellsSpell6 = 5,
+	kSpellsSpell7 = 6,
+	kSpellsSpell8 = 7,
+	kSpellsSpell9 = 8,
+	kSpellsSpell10 = 9,
+	kSpellsSelect = 10,
+	kSpellsExit = 11
+};
+
+enum CastSpellButtonTTSTextIndex {
+	kCastSpellCast = 0,
+	kCastSpellNew = 1,
+	kCastSpellExit = 2
+};
+
+enum SelectElementButtonTTSTextIndex {
+	kSelectElementFire = 0,
+	kSelectElementElectricity = 1,
+	kSelectElementCold = 2,
+	kSelectElementAcid = 3
+};
+
+enum LloydsBeaconButtonTTSTextIndex {
+	kLloydsBeaconSet = 0,
+	kLloydsBeaconReturn = 1
+};
+
 Character *SpellsDialog::show(XeenEngine *vm, ButtonContainer *priorDialog,
 		Character *c, SpellDialogMode mode) {
 	SpellsDialog *dlg = new SpellsDialog(vm);
@@ -59,6 +125,11 @@ Character *SpellsDialog::execute(ButtonContainer *priorDialog, Character *c, int
 	int newSelection;
 	w.open();
 
+#ifdef USE_TTS
+	bool voiceText = true;
+	bool resetButtons = false;
+#endif
+
 	do {
 		if (!mode) {
 			if (!c->guildMember()) {
@@ -71,15 +142,16 @@ Character *SpellsDialog::execute(ButtonContainer *priorDialog, Character *c, int
 			Common::String title = Common::String::format(Res.BUY_SPELLS, c->_name.c_str());
 			Common::String msg = Common::String::format(Res.GUILD_OPTIONS,
 				title.c_str(), XeenEngine::printMil(party._gold).c_str());
-			windows[10].writeString(msg);
+			windows[10].writeString(msg, false);
 			priorDialog->drawButtons(&windows[10]);
 		}
 
 		_spells.clear();
 		const char *errorMsg = setSpellText(c, modeCopy);
+		Common::String ttsMessage;
 		w.writeString(Common::String::format(Res.SPELLS_FOR,
 			errorMsg == nullptr ? Res.SPELL_LINES_0_TO_9 : "",
-			c->_name.c_str()));
+			c->_name.c_str()), false, &ttsMessage);
 
 		// Setup and write out spell list
 		const char *names[10];
@@ -107,7 +179,27 @@ Character *SpellsDialog::execute(ButtonContainer *priorDialog, Character *c, int
 			colors[9], names[9],
 			mode ? Res.SPELL_PTS : Res.GOLD,
 			mode ? c->_currentSp : party._gold
-		));
+		), false, &ttsMessage);
+
+#ifdef USE_TTS
+		if (voiceText) {
+			if (errorMsg == nullptr) {
+				speakText(ttsMessage, mode);
+			} else {
+				_vm->sayText(ttsMessage, Common::TextToSpeechManager::INTERRUPT);
+				_buttonTexts.resize(kSpellsSpellCount);
+				_buttonTexts.push_back(selectButton[_vm->_ttsLanguage]);
+				_buttonTexts.push_back(exitButton[_vm->_ttsLanguage]);
+			}
+
+			voiceText = false;
+		}
+
+		if (resetButtons) {
+			resetButtonTexts(ttsMessage, mode);
+			resetButtons = false;
+		}
+#endif
 
 		_scrollSprites.draw(0, 4, Common::Point(39, 26));
 		_scrollSprites.draw(0, 0, Common::Point(187, 26));
@@ -136,10 +228,10 @@ Character *SpellsDialog::execute(ButtonContainer *priorDialog, Character *c, int
 					c = &party._activeParty[_buttonValue];
 					spells._lastCaster = _buttonValue;
 					intf.highlightChar(_buttonValue);
-
+					ttsMessage.clear();
 					if (_vm->_mode == MODE_INTERACTIVE7) {
 						windows[10].writeString(Common::String::format(Res.GUILD_OPTIONS,
-							XeenEngine::printMil(party._gold).c_str(), Res.GUILD_TEXT, c->_name.c_str()));
+							XeenEngine::printMil(party._gold).c_str(), Res.GUILD_TEXT, c->_name.c_str()), false, &ttsMessage);
 					} else {
 						SpellsCategory category = c->getSpellsCategory();
 						int spellIndex = (c->_currentSpell == -1) ? SPELLS_PER_CLASS : c->_currentSpell;
@@ -148,9 +240,13 @@ Character *SpellsDialog::execute(ButtonContainer *priorDialog, Character *c, int
 						windows[10].writeString(Common::String::format(Res.CAST_SPELL_DETAILS,
 							c->_name.c_str(), spells._spellNames[spellId].c_str(),
 							spells.calcSpellPoints(spellId, c->getCurrentLevel()),
-							Res.SPELL_GEM_COST[spellId], c->_currentSp));
+							Res.SPELL_GEM_COST[spellId], c->_currentSp), false, &ttsMessage);
 					}
 
+#ifdef USE_TTS
+					voiceText = true;
+					_buttonTexts.clear();
+#endif
 					if (priorDialog != nullptr)
 						priorDialog->drawButtons(&windows[0]);
 					windows[10].update();
@@ -194,6 +290,9 @@ Character *SpellsDialog::execute(ButtonContainer *priorDialog, Character *c, int
 				if (mode) {
 					// Casting
 					selection = newSelection;
+#ifdef USE_TTS
+					_vm->sayText(_buttonTexts[(selection - topIndex) % kSpellsSpellCount], Common::TextToSpeechManager::INTERRUPT);
+#endif
 				} else {
 					// Guild spells dialog: Spells Info or Buy
 					const Common::String &spellName = spells._spellNames[spellId];
@@ -209,6 +308,9 @@ Character *SpellsDialog::execute(ButtonContainer *priorDialog, Character *c, int
 							sound.stopSound();
 							intf._overallFrame = 0;
 							sound.playSound(ccNum ? "parrot2.voc" : "guild12.voc", 1);
+#ifdef USE_TTS
+							resetButtons = true;
+#endif
 						} else {
 							sound.playFX(21);
 						}
@@ -220,23 +322,37 @@ Character *SpellsDialog::execute(ButtonContainer *priorDialog, Character *c, int
 		case Common::KEYCODE_PAGEUP:
 		case Common::KEYCODE_KP9:
 			topIndex = MAX((int)topIndex - 10, 0);
+#ifdef USE_TTS
+			resetButtons = true;
+#endif
 			break;
 
 		case Common::KEYCODE_PAGEDOWN:
 		case Common::KEYCODE_KP3:
 			topIndex = MIN(topIndex + 10, (((int)_spells.size() - 1) / 10) * 10);
+#ifdef USE_TTS
+			resetButtons = true;
+#endif
 			break;
 
 		case Common::KEYCODE_UP:
 		case Common::KEYCODE_KP8:
-			if (topIndex > 0)
+			if (topIndex > 0) {
 				--topIndex;
+#ifdef USE_TTS
+				resetButtons = true;
+#endif
+			}
 			break;
 
 		case Common::KEYCODE_DOWN:
 		case Common::KEYCODE_KP2:
-			if (topIndex < ((int)_spells.size() - 10))
+			if (topIndex < ((int)_spells.size() - 10)) {
 				++topIndex;
+#ifdef USE_TTS
+				resetButtons = true;
+#endif
+			}
 			break;
 
 		default:
@@ -259,20 +375,20 @@ void SpellsDialog::loadButtons() {
 	_scrollSprites.load("scroll.icn");
 	addButton(Common::Rect(187, 26, 198, 36), Common::KEYCODE_UP, &_scrollSprites);
 	addButton(Common::Rect(187, 111, 198, 121), Common::KEYCODE_DOWN, &_scrollSprites);
-	addButton(Common::Rect(40, 28, 187, 36), Common::KEYCODE_1);
-	addButton(Common::Rect(40, 37, 187, 45), Common::KEYCODE_2);
-	addButton(Common::Rect(40, 46, 187, 54), Common::KEYCODE_3);
-	addButton(Common::Rect(40, 55, 187, 63), Common::KEYCODE_4);
-	addButton(Common::Rect(40, 64, 187, 72), Common::KEYCODE_5);
-	addButton(Common::Rect(40, 73, 187, 81), Common::KEYCODE_6);
-	addButton(Common::Rect(40, 82, 187, 90), Common::KEYCODE_7);
-	addButton(Common::Rect(40, 91, 187, 99), Common::KEYCODE_8);
-	addButton(Common::Rect(40, 100, 187, 108), Common::KEYCODE_9);
-	addButton(Common::Rect(40, 109, 187, 117), Common::KEYCODE_0);
-	addButton(Common::Rect(174, 123, 198, 133), Common::KEYCODE_ESCAPE);
+	addButton(Common::Rect(40, 28, 187, 36), Common::KEYCODE_1, nullptr, kSpellsSpell1);
+	addButton(Common::Rect(40, 37, 187, 45), Common::KEYCODE_2, nullptr, kSpellsSpell2);
+	addButton(Common::Rect(40, 46, 187, 54), Common::KEYCODE_3, nullptr, kSpellsSpell3);
+	addButton(Common::Rect(40, 55, 187, 63), Common::KEYCODE_4, nullptr, kSpellsSpell4);
+	addButton(Common::Rect(40, 64, 187, 72), Common::KEYCODE_5, nullptr, kSpellsSpell5);
+	addButton(Common::Rect(40, 73, 187, 81), Common::KEYCODE_6, nullptr, kSpellsSpell6);
+	addButton(Common::Rect(40, 82, 187, 90), Common::KEYCODE_7, nullptr, kSpellsSpell7);
+	addButton(Common::Rect(40, 91, 187, 99), Common::KEYCODE_8, nullptr, kSpellsSpell8);
+	addButton(Common::Rect(40, 100, 187, 108), Common::KEYCODE_9, nullptr, kSpellsSpell9);
+	addButton(Common::Rect(40, 109, 187, 117), Common::KEYCODE_0, nullptr, kSpellsSpell10);
+	addButton(Common::Rect(174, 123, 198, 133), Common::KEYCODE_ESCAPE, nullptr, kSpellsExit);
 	addButton(Common::Rect(187, 35, 198, 73), Common::KEYCODE_PAGEUP);
 	addButton(Common::Rect(187, 74, 198, 112), Common::KEYCODE_PAGEDOWN);
-	addButton(Common::Rect(132, 123, 168, 133), Common::KEYCODE_s);
+	addButton(Common::Rect(132, 123, 168, 133), Common::KEYCODE_s, nullptr, kSpellsSelect);
 	addPartyButtons(_vm);
 }
 
@@ -404,6 +520,53 @@ const char *SpellsDialog::setSpellText(Character *c, int mode) {
 	return _spells.empty() ? Res.SPELLS_LEARNED_ALL : nullptr;
 }
 
+#ifdef USE_TTS
+
+void SpellsDialog::speakText(const Common::String &text, int mode) {
+	_vm->sayText(resetButtonTexts(text, mode));
+}
+
+Common::String SpellsDialog::resetButtonTexts(const Common::String &text, int mode) {
+	uint index = 0;
+
+	// Get the numbers 0 - 9
+	Common::String spellList[kSpellsSpellCount];
+	for (uint8 i = 0; i < kSpellsSpellCount; ++i) {
+		spellList[i] = getNextTextSection(text, index);
+	}
+
+	// "Spells for" message
+	Common::String combinedText = getNextTextSection(text, index);
+
+	_buttonTexts.clear();
+	// Spell names and their costs
+	uint spellCount = MIN((uint)kSpellsSpellCount, _spells.size());
+	for (uint i = 0; i < spellCount; ++i) {
+		spellList[i] += ": " + getNextTextSection(text, index, 2, ": ");
+		_buttonTexts.push_back(spellList[i]);
+		combinedText += spellList[i];
+	}
+
+	// Numbers for empty spells
+	for (uint i = spellCount; i < kSpellsSpellCount; ++i) {
+		_buttonTexts.push_back(spellList[i]);
+	}
+
+	if (mode) {
+		_buttonTexts.push_back(selectButton[_vm->_ttsLanguage]);
+	} else {
+		_buttonTexts.push_back("");
+	}
+	_buttonTexts.push_back(exitButton[_vm->_ttsLanguage]);
+
+	// Spell points/gold
+	combinedText += getNextTextSection(text, index);
+
+	return combinedText;
+}
+
+#endif
+
 /*------------------------------------------------------------------------*/
 
 CastSpell::CastSpell(XeenEngine *vm) : ButtonContainer(vm) {
@@ -478,6 +641,11 @@ int CastSpell::execute(Character *&c) {
 
 	int spellId = -1;
 	bool redrawFlag = true;
+
+#ifdef USE_TTS
+	_vm->stopTextToSpeech();
+#endif
+
 	do {
 		if (redrawFlag) {
 			SpellsCategory category = c->getSpellsCategory();
@@ -487,9 +655,13 @@ int CastSpell::execute(Character *&c) {
 			int gemCost = Res.SPELL_GEM_COST[spellId];
 			int spCost = spells.calcSpellPoints(spellId, c->getCurrentLevel());
 
+			Common::String ttsMessage;
 			w.writeString(Common::String::format(Res.CAST_SPELL_DETAILS,
 				c->_name.c_str(), spells._spellNames[spellId].c_str(),
-				spCost, gemCost, c->_currentSp));
+				spCost, gemCost, c->_currentSp), false, &ttsMessage);
+#ifdef USE_TTS
+			speakText(ttsMessage);
+#endif
 			drawButtons(&windows[0]);
 			w.update();
 
@@ -547,13 +719,24 @@ int CastSpell::execute(Character *&c) {
 void CastSpell::loadButtons() {
 	_iconSprites.load("cast.icn");
 
-	addButton(Common::Rect(234, 108, 259, 128), Res.KeyConstants.DialogsSpells.KEY_CAST, &_iconSprites);
-	addButton(Common::Rect(261, 108, 285, 128), Res.KeyConstants.DialogsSpells.KEY_NEW, &_iconSprites);
+	addButton(Common::Rect(234, 108, 259, 128), Res.KeyConstants.DialogsSpells.KEY_CAST, &_iconSprites, kCastSpellCast);
+	addButton(Common::Rect(261, 108, 285, 128), Res.KeyConstants.DialogsSpells.KEY_NEW, &_iconSprites, kCastSpellNew);
 
-	addButton(Common::Rect(288, 108, 312, 128), Common::KEYCODE_ESCAPE, &_iconSprites);
+	addButton(Common::Rect(288, 108, 312, 128), Common::KEYCODE_ESCAPE, &_iconSprites, kCastSpellExit);
 	addPartyButtons(_vm);
 }
 
+#ifdef USE_TTS
+
+void CastSpell::speakText(const Common::String &text) {
+	uint index = 0;
+	Common::String buttonText = addNextTextToButtons(text, index, kSpellsButtonCount);
+	_vm->sayText(text.substr(index + 1), Common::TextToSpeechManager::INTERRUPT);
+	_vm->sayText(buttonText);
+}
+
+#endif
+
 /*------------------------------------------------------------------------*/
 
 Character *SpellOnWho::show(XeenEngine *vm, int spellId) {
@@ -654,16 +837,21 @@ int SelectElement::execute(int spellId) {
 	loadButtons();
 
 	w.open();
-	w.writeString(Res.WHICH_ELEMENT1);
+	Common::String ttsMessage;
+	w.writeString(Res.WHICH_ELEMENT1, false, &ttsMessage);
 	drawButtons(&windows[0]);
 	w.update();
 
+#ifdef USE_TTS
+	speakText(ttsMessage);
+#endif
+
 	while (result == 999) {
 		do {
 			events.updateGameCounter();
 			intf.draw3d(true, false);
 			w.frame();
-			w.writeString(Res.WHICH_ELEMENT2);
+			w.writeString(Res.WHICH_ELEMENT2, false);
 			drawButtons(&windows[0]);
 			w.update();
 
@@ -698,12 +886,22 @@ int SelectElement::execute(int spellId) {
 void SelectElement::loadButtons() {
 	_iconSprites.load("element.icn");
 
-	addButton(Common::Rect(60, 92, 84, 112),   Res.KeyConstants.DialogsSpells.KEY_FIRE, &_iconSprites);
-	addButton(Common::Rect(90, 92, 114, 112),  Res.KeyConstants.DialogsSpells.KEY_ELEC, &_iconSprites);
-	addButton(Common::Rect(120, 92, 144, 112), Res.KeyConstants.DialogsSpells.KEY_COLD, &_iconSprites);
-	addButton(Common::Rect(150, 92, 174, 112), Res.KeyConstants.DialogsSpells.KEY_ACID, &_iconSprites);
+	addButton(Common::Rect(60, 92, 84, 112),   Res.KeyConstants.DialogsSpells.KEY_FIRE, &_iconSprites, kSelectElementFire);
+	addButton(Common::Rect(90, 92, 114, 112),  Res.KeyConstants.DialogsSpells.KEY_ELEC, &_iconSprites, kSelectElementElectricity);
+	addButton(Common::Rect(120, 92, 144, 112), Res.KeyConstants.DialogsSpells.KEY_COLD, &_iconSprites, kSelectElementCold);
+	addButton(Common::Rect(150, 92, 174, 112), Res.KeyConstants.DialogsSpells.KEY_ACID, &_iconSprites, kSelectElementAcid);
+}
+
+#ifdef USE_TTS
+
+void SelectElement::speakText(const Common::String &text) {
+	uint index = 0;
+	_vm->sayText(getNextTextSection(text, index), Common::TextToSpeechManager::INTERRUPT);
+	_vm->sayText(addNextTextToButtons(text, index, kSelectElementButtonCount));
 }
 
+#endif
+
 /*------------------------------------------------------------------------*/
 
 void NotWhileEngaged::show(XeenEngine *vm, int spellId) {
@@ -773,13 +971,18 @@ bool LloydsBeacon::execute() {
 	// Get the destination map name
 	Common::String mapName = Map::getMazeName(c._lloydMap, c._lloydSide);
 
+	Common::String ttsMessage;
 	// Display the dialog
 	w.open();
 	w.writeString(Common::String::format(Res.LLOYDS_BEACON, mapName.c_str(),
-		c._lloydPosition.x, c._lloydPosition.y));
+		c._lloydPosition.x, c._lloydPosition.y), true, &ttsMessage);
 	drawButtons(&w);
 	w.update();
 
+#ifdef USE_TTS
+	speakText(ttsMessage);
+#endif
+
 	bool result = true;
 	do {
 		do {
@@ -829,10 +1032,20 @@ bool LloydsBeacon::execute() {
 void LloydsBeacon::loadButtons() {
 	_iconSprites.load("lloyds.icn");
 
-	addButton(Common::Rect(281, 108, 305, 128), Res.KeyConstants.DialogsSpells.KEY_RETURN, &_iconSprites);
-	addButton(Common::Rect(242, 108, 266, 128), Res.KeyConstants.DialogsSpells.KEY_SET, &_iconSprites);
+	addButton(Common::Rect(281, 108, 305, 128), Res.KeyConstants.DialogsSpells.KEY_RETURN, &_iconSprites, kLloydsBeaconReturn);
+	addButton(Common::Rect(242, 108, 266, 128), Res.KeyConstants.DialogsSpells.KEY_SET, &_iconSprites, kLloydsBeaconSet);
 }
 
+#ifdef USE_TTS
+
+void LloydsBeacon::speakText(const Common::String &text) {
+	uint index = 0;
+	getNextTextSection(text, index, kLloydsBeaconInfoCount);
+	addNextTextToButtons(text, index, kLloydsBeaconButtonCount);
+}
+
+#endif
+
 /*------------------------------------------------------------------------*/
 
 int Teleport::show(XeenEngine *vm) {
@@ -988,10 +1201,15 @@ void IdentifyMonster::execute() {
 
 	sound.playFX(20);
 	w.open();
+	Common::String ttsMessage;
 	w.writeString(Common::String::format(Res.IDENTIFY_MONSTERS,
-		monsterDesc[0].c_str(), monsterDesc[1].c_str(), monsterDesc[2].c_str()));
+		monsterDesc[0].c_str(), monsterDesc[1].c_str(), monsterDesc[2].c_str()), false, &ttsMessage);
 	w.update();
 
+#ifdef USE_TTS
+	speakText(ttsMessage);
+#endif
+
 	do {
 		events.updateGameCounter();
 		intf.draw3d(false, false);
@@ -1004,6 +1222,27 @@ void IdentifyMonster::execute() {
 	w.close();
 }
 
+#ifdef USE_TTS
+
+void IdentifyMonster::speakText(const Common::String &text) const {
+	Common::String headers[kIdentifyMonsterHeaderCount];
+	uint index = 0;
+	for (uint8 i = 0; i < kIdentifyMonsterHeaderCount; ++i) {
+		headers[i] = getNextTextSection(text, index);
+	}
+
+	Combat &combat = *_vm->_combat;
+	for (uint8 i = 0; i < kIdentifyMonsterMaxMonsterCount; ++i) {
+		if (combat._attackMonsters[i] == -1)
+			continue;
+
+		for (int j = 0; j < kIdentifyMonsterHeaderCount; ++j) {
+			_vm->sayText(headers[j] + ": " + getNextTextSection(text, index));
+		}
+	}
+}
+
+#endif
 
 /*------------------------------------------------------------------------*/
 
diff --git a/engines/mm/xeen/dialogs/dialogs_spells.h b/engines/mm/xeen/dialogs/dialogs_spells.h
index fdb497afc7d..b2f68ff8f20 100644
--- a/engines/mm/xeen/dialogs/dialogs_spells.h
+++ b/engines/mm/xeen/dialogs/dialogs_spells.h
@@ -72,6 +72,23 @@ private:
 	 * Sets the spell text
 	 */
 	const char *setSpellText(Character *c, int isCasting);
+
+#ifdef USE_TTS
+	/**
+	 * Voices text with TTS and sets up buttons
+	 * @param text	Text for voicing and buttons. Each section should be separated by a newline
+	 * @param mode	Mode of the spells menu
+	 */
+	void speakText(const Common::String &text, int mode);
+
+	/**
+	 * Resets all button texts to what's currently on screen
+	 * @param text	Text, both for buttons and for voicing. Each section should be separated by a newline
+	 * @param mode	Mode of the spells menu
+	 * @returns		All text combined
+	 */
+	Common::String resetButtonTexts(const Common::String &text, int mode);
+#endif
 public:
 	/**
 	 * Show the spells list dialog
@@ -91,6 +108,10 @@ private:
 	int execute(Character *&c);
 
 	void loadButtons();
+
+#ifdef USE_TTS
+	void speakText(const Common::String &text);
+#endif
 public:
 	static int show(XeenEngine *vm);
 };
@@ -113,6 +134,10 @@ private:
 	int execute(int spellId);
 
 	void loadButtons();
+
+#ifdef USE_TTS
+	void speakText(const Common::String &text);
+#endif
 public:
 	static int show(XeenEngine *vm, int spellId);
 };
@@ -135,6 +160,10 @@ private:
 	bool execute();
 
 	void loadButtons();
+
+#ifdef USE_TTS
+	void speakText(const Common::String &text);
+#endif
 public:
 	static bool show(XeenEngine *vm);
 };
@@ -164,6 +193,10 @@ private:
 	IdentifyMonster(XeenEngine *vm) : ButtonContainer(vm) {}
 
 	void execute();
+
+#ifdef USE_TTS
+	void speakText(const Common::String &text) const;
+#endif
 public:
 	static void show(XeenEngine *vm);
 };
diff --git a/engines/mm/xeen/dialogs/dialogs_whowill.cpp b/engines/mm/xeen/dialogs/dialogs_whowill.cpp
index 5549c6297d0..26a34fa770a 100644
--- a/engines/mm/xeen/dialogs/dialogs_whowill.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_whowill.cpp
@@ -56,9 +56,14 @@ int WhoWill::execute(int message, int action, bool type) {
 		Res.WHO_ACTIONS[message], party._activeParty.size());
 
 	windows[36].open();
-	windows[36].writeString(msg);
+	Common::String ttsMessage;
+	windows[36].writeString(msg, false, &ttsMessage);
 	windows[36].update();
 
+#ifdef USE_TTS
+	speakText(ttsMessage);
+#endif
+
 	intf._face1State = map._headData[party._mazePosition.y][party._mazePosition.x]._left;
 	intf._face2State = map._headData[party._mazePosition.y][party._mazePosition.x]._right;
 
@@ -97,10 +102,29 @@ int WhoWill::execute(int message, int action, bool type) {
 		}
 	}
 
+#ifdef USE_TTS
+	_vm->stopTextToSpeech();
+#endif
 	intf._face1State = intf._face2State = 2;
 	windows[36].close();
 	return _buttonValue;
 }
 
+#ifdef USE_TTS
+
+void WhoWill::speakText(const Common::String &text) const {
+	uint index = 0;
+	// Title
+	_vm->sayText(getNextTextSection(text, index));
+
+	// "Who will" and the action verb (i.e. try)
+	_vm->sayText(getNextTextSection(text, index, 2, " "));
+
+	// F1 - F6
+	_vm->sayText(getNextTextSection(text, index));
+}
+
+#endif
+
 } // End of namespace Xeen
 } // End of namespace MM
diff --git a/engines/mm/xeen/dialogs/dialogs_whowill.h b/engines/mm/xeen/dialogs/dialogs_whowill.h
index 39d742b228b..7ad6470aeca 100644
--- a/engines/mm/xeen/dialogs/dialogs_whowill.h
+++ b/engines/mm/xeen/dialogs/dialogs_whowill.h
@@ -32,6 +32,10 @@ private:
 	WhoWill(XeenEngine *vm) : ButtonContainer(vm) {}
 
 	int execute(int message, int action, bool type);
+
+#ifdef USE_TTS
+	void speakText(const Common::String &text) const;
+#endif
 public:
 	static int show(XeenEngine *vm, int message, int action, bool type);
 };
diff --git a/engines/mm/xeen/events.cpp b/engines/mm/xeen/events.cpp
index 160edc45cb0..e7b00ff2739 100644
--- a/engines/mm/xeen/events.cpp
+++ b/engines/mm/xeen/events.cpp
@@ -89,6 +89,9 @@ void EventsManager::pollEvents() {
 			break;
 		case Common::EVENT_MOUSEMOVE:
 			_mousePos = event.mouse;
+#ifdef USE_TTS
+			_vm->_mouseMoved = true;
+#endif
 			break;
 		case Common::EVENT_LBUTTONDOWN:
 			_mousePressed = true;
diff --git a/engines/mm/xeen/font.cpp b/engines/mm/xeen/font.cpp
index 91fac408226..d4f0fd91fa2 100644
--- a/engines/mm/xeen/font.cpp
+++ b/engines/mm/xeen/font.cpp
@@ -109,10 +109,18 @@ bool FontSurface::isSpace(char c) {
 	return (c & 0x7f) == ' ';
 }
 
-const char *FontSurface::writeString(const Common::String &s, const Common::Rect &bounds) {
+const char *FontSurface::writeString(const Common::String &s, const Common::Rect &bounds, bool ttsVoiceText, Common::String *ttsMessage) {
 	_displayString = s.c_str();
 	assert(_fontData);
 
+#ifdef USE_TTS
+	bool deleteTTSMessage = false;
+	if (!ttsMessage) {
+		ttsMessage = new Common::String();
+		deleteTTSMessage = true;
+	}
+#endif
+
 	for (;;) {
 		const char *msgStartP = _displayString;
 		_msgWraps = false;
@@ -198,10 +206,16 @@ const char *FontSurface::writeString(const Common::String &s, const Common::Rect
 
 			if (c == ' ') {
 				_writePos.x += _fontReduced ? 3 : 4;
+#ifdef USE_TTS
+				*ttsMessage += ' ';
+#endif
 			} else if (c == '\r') {
 				fillRect(bounds, _bgColor);
 				addDirtyRect(bounds);
 				_writePos = Common::Point(bounds.left, bounds.top);
+#ifdef USE_TTS
+				*ttsMessage += '\n';
+#endif
 			} else if (c == 1) {
 				// Turn off reduced font mode
 				_fontReduced = false;
@@ -231,6 +245,9 @@ const char *FontSurface::writeString(const Common::String &s, const Common::Rect
 			} else if (c == 6) {
 				// Non-breakable space
 				writeChar(' ', bounds);
+#ifdef USE_TTS
+				*ttsMessage += ' ';
+#endif
 			} else if (c == 7) {
 				// Set text background color
 				int bgColor = fontAtoi();
@@ -270,7 +287,14 @@ const char *FontSurface::writeString(const Common::String &s, const Common::Rect
 				// Skip x position
 				int xAmount = fontAtoi();
 				_writePos.x = MIN(bounds.left + xAmount, (int)bounds.right);
+#ifdef USE_TTS
+				*ttsMessage += '\n';
+#endif
 			} else if (c == 10) {
+#ifdef USE_TTS
+				*ttsMessage += '\n';
+#endif
+
 				// Newline
 				if (newLine(bounds))
 					return _displayString;
@@ -278,6 +302,9 @@ const char *FontSurface::writeString(const Common::String &s, const Common::Rect
 				// Set y position
 				int yp = fontAtoi();
 				_writePos.y = MIN(bounds.top + yp, (int)bounds.bottom);
+#ifdef USE_TTS
+				*ttsMessage += '\n';
+#endif
 			} else if (c == 12) {
 				// Set text colors
 				int idx = fontAtoi(2);
@@ -286,13 +313,22 @@ const char *FontSurface::writeString(const Common::String &s, const Common::Rect
 				setTextColor(idx);
 			} else if (Common::RU_RUS == lang && (c & 0x80)) {
 				writeChar(c, bounds);
+#ifdef USE_TTS
+				*ttsMessage += c;
+#endif
 			} else if (c < ' ') {
 				// End of string or invalid command
 				_displayString = nullptr;
+#ifdef USE_TTS
+				*ttsMessage += '\n';
+#endif
 				break;
 			} else {
 				// Standard character - write it out
 				writeChar(c, bounds);
+#ifdef USE_TTS
+				*ttsMessage += c;
+#endif
 			}
 		}
 
@@ -303,6 +339,17 @@ const char *FontSurface::writeString(const Common::String &s, const Common::Rect
 			break;
 	}
 
+#ifdef USE_TTS
+	if (ttsVoiceText) {
+		g_vm->sayText(*ttsMessage);
+	}
+
+	if (deleteTTSMessage) {
+		delete ttsMessage;
+		ttsMessage = nullptr;
+	}
+#endif
+
 	return _displayString;
 }
 
diff --git a/engines/mm/xeen/font.h b/engines/mm/xeen/font.h
index b8b33605c2e..850185148fa 100644
--- a/engines/mm/xeen/font.h
+++ b/engines/mm/xeen/font.h
@@ -108,13 +108,15 @@ public:
 
 	/**
 	 * Write a string to the surface
-	 * @param s			String to display
-	 * @param clipRect	Window bounds to display string within
-	 * @returns			Any string remainder that couldn't be displayed
+	 * @param s				String to display
+	 * @param clipRect		Window bounds to display string within
+	 * @param ttsVoiceText	Whether to voice the string with TTS
+	 * @param ttsMessage	Message for TTS, which will be built
+	 * @returns				Any string remainder that couldn't be displayed
 	 * @remarks		Note that bounds is just used for wrapping purposes. Unless
 	 *		justification is set, the message will be written at _writePos
 	 */
-	const char *writeString(const Common::String &s, const Common::Rect &clipRect);
+	const char *writeString(const Common::String &s, const Common::Rect &clipRect, bool ttsVoiceText = true, Common::String *ttsMessage = nullptr);
 	bool isSpace(char c);
 	/**
 	 * Write a charcter to the window
diff --git a/engines/mm/xeen/interface.cpp b/engines/mm/xeen/interface.cpp
index e6685cfc8eb..c90c9122745 100644
--- a/engines/mm/xeen/interface.cpp
+++ b/engines/mm/xeen/interface.cpp
@@ -40,10 +40,22 @@
 namespace MM {
 namespace Xeen {
 
+#ifdef USE_TTS
+
+static const uint8 kCombatButtonCount = 3;
+
+#endif
+
 enum {
 	SCENE_WINDOW = 11, SCENE_WIDTH = 216, SCENE_HEIGHT = 132
 };
 
+enum CombatButtonTTSTextIndex {
+	kCombatMonster1 = 0,
+	kCombatMonster2 = 1,
+	kCombatMonster3 = 2
+};
+
 PartyDrawer::PartyDrawer(XeenEngine *vm): _vm(vm) {
 	_restoreSprites.load("restorex.icn");
 	_hpSprites.load("hpbars.icn");
@@ -337,9 +349,9 @@ void Interface::setMainButtons(IconsMode mode) {
 	addButton(Common::Rect(260, 169, 284, 189), Common::KEYCODE_DOWN, spr);
 	addButton(Common::Rect(286, 169, 310, 189), (Common::KBD_CTRL << 16) | Common::KEYCODE_RIGHT, spr);
 	addButton(Common::Rect(236,  11, 308,  69),  Common::KEYCODE_EQUALS);
-	addButton(Common::Rect(239,  27, 312,  37),  Common::KEYCODE_1);
-	addButton(Common::Rect(239, 37, 312, 47), Common::KEYCODE_2);
-	addButton(Common::Rect(239, 47, 312, 57), Common::KEYCODE_3);
+	addButton(Common::Rect(239,  27, 312,  37),  Common::KEYCODE_1, nullptr, kCombatMonster1);
+	addButton(Common::Rect(239, 37, 312, 47), Common::KEYCODE_2, nullptr, kCombatMonster2);
+	addButton(Common::Rect(239, 47, 312, 57), Common::KEYCODE_3, nullptr, kCombatMonster3);
 	addPartyButtons(_vm);
 
 	if (mode == ICONS_COMBAT) {
@@ -1357,7 +1369,10 @@ void Interface::draw3d(bool updateFlag, bool pauseFlag) {
 
 	// Draw any on-screen text if flagged to do so
 	if (_upDoorText && combat._attackMonsters[0] == -1) {
-		windows[3].writeString(_screenText);
+		windows[3].writeString(_screenText, _screenText != _ttsPreviousScreenText);
+		_ttsPreviousScreenText = _screenText;
+	} else {
+		_ttsPreviousScreenText.clear();
 	}
 
 	if (updateFlag) {
@@ -1627,6 +1642,11 @@ void Interface::doCombat() {
 		Window &w = windows[2];
 		w.open();
 		bool breakFlag = false;
+		Common::String ttsMessage;
+		bool ttsVoiceText = true;
+#ifdef USE_TTS
+		uint ttsIndex = 0;
+#endif
 
 		while (!_vm->shouldExit() && !breakFlag && !party._dead && _vm->_mode == MODE_COMBAT) {
 			// FIXME: I've had a rare issue where the loop starts with a non-party _whosTurn. Unfortunately,
@@ -1638,12 +1658,22 @@ void Interface::doCombat() {
 			highlightChar(combat._whosTurn);
 			combat.setSpeedTable();
 
+			ttsMessage.clear();
 			// Write out the description of the monsters being battled
-			w.writeString(combat.getMonsterDescriptions());
+			w.writeString(combat.getMonsterDescriptions(), ttsVoiceText, &ttsMessage);
+			ttsVoiceText = false;
+
 			_combatIcons.draw(0, 32, Common::Point(233, combat._attackDurationCtr * 10 + 27),
 				SPRFLAG_800, 0);
 			w.update();
 
+#ifdef USE_TTS
+			ttsIndex = 0;
+			getNextTextSection(ttsMessage, ttsIndex);	// "Combat"
+			_buttonTexts.clear();
+			addNextTextToButtons(ttsMessage, ttsIndex, kCombatButtonCount);
+#endif
+
 			// Wait for keypress
 			index = 0;
 			do {
@@ -1682,6 +1712,9 @@ void Interface::doCombat() {
 				if (combat._attackMonsters[_buttonValue] != -1) {
 					combat._monster2Attack = combat._attackMonsters[_buttonValue];
 					combat._attackDurationCtr = _buttonValue;
+#ifdef USE_TTS
+					_vm->sayText(_buttonTexts[_buttonValue], Common::TextToSpeechManager::INTERRUPT);
+#endif
 				}
 				break;
 
diff --git a/engines/mm/xeen/interface.h b/engines/mm/xeen/interface.h
index eda151c6a08..c65e17f13f1 100644
--- a/engines/mm/xeen/interface.h
+++ b/engines/mm/xeen/interface.h
@@ -175,6 +175,7 @@ public:
 	int _levitateUIFrame;
 	bool _upDoorText;
 	Common::String _screenText;
+	Common::String _ttsPreviousScreenText;
 	byte _tillMove;
 	int _charFX[6];
 	IconsMode _iconsMode;
diff --git a/engines/mm/xeen/locations.cpp b/engines/mm/xeen/locations.cpp
index 79b5041f06c..1e5052362b0 100644
--- a/engines/mm/xeen/locations.cpp
+++ b/engines/mm/xeen/locations.cpp
@@ -34,6 +34,57 @@ namespace MM {
 namespace Xeen {
 namespace Locations {
 
+#ifdef USE_TTS
+	static const uint8 kBankButtonCount = 3;
+
+	static const uint8 kTavernOptionsCount = 4;
+	static const uint8 kTavernButtonCount = 2;
+
+	static const uint8 kTempleOptionsCount = 3;
+
+	static const uint8 kTrainingButtonCount = 2;
+	static const uint8 kTrainingIneligibleInfoCount = 2;
+	static const uint8 kTrainingEligibleInfoCount = 4;
+#endif
+
+enum BankButtonTTSTextIndex {
+	kBankDeposit = 0,
+	kBankWithdraw = 1,
+	kBankEscape = 2
+};
+
+enum BlacksmithButtonTTSTextIndex {
+	kBlacksmithBrowse = 0,
+	kBlacksmithExit = 1
+};
+
+enum GuildButtonTTSTextIndex {
+	kGuildBuySpells = 0,
+	kGuildSpellInfo = 1,
+	kGuildExit = 2
+};
+
+enum TavernButtonTTSTextIndex {
+	kTavernDrink = 0,
+	kTavernFood = 1,
+	kTavernTip = 2,
+	kTavernRumors = 3,
+	kTavernSignIn = 4,
+	kTavernExit = 5
+};
+
+enum TempleButtonTTSTextIndex {
+	kTempleHeal = 0,
+	kTempleDonation = 1,
+	kTempleUncurse = 2,
+	kTempleExit = 3
+};
+
+enum TrainingButtonTTSTextIndex {
+	kTrainingTrain = 0,
+	kTrainingExit = 1
+};
+
 BaseLocation::BaseLocation(LocationAction action) : ButtonContainer(g_vm),
 		_locationActionId(action), _ccNum(g_vm->_files->_ccNum),
 		_vocName("hello1.voc"), _exitToUi(false) {
@@ -48,6 +99,10 @@ BaseLocation::BaseLocation(LocationAction action) : ButtonContainer(g_vm),
 	_farewellTime = 0;
 	_drawCtr1 = _drawCtr2 = 0;
 	_animPos = Common::Point(8, 8);
+	_ttsVoiceText = false;
+#ifdef USE_TTS
+	_resetText = false;
+#endif
 }
 
 BaseLocation::~BaseLocation() {
@@ -92,8 +147,19 @@ int BaseLocation::show() {
 		if (_vm->shouldExit() || _exitToUi)
 			return 0;
 
+		Common::String ttsMessage;
 		Common::String msg = createLocationText(*charP);
-		windows[10].writeString(msg);
+		windows[10].writeString(msg, _ttsVoiceText, &ttsMessage);
+		_ttsVoiceText = false;
+
+#ifdef USE_TTS
+		if (_resetText) {
+			_buttonTexts.clear();
+			speakTextAndButtons(ttsMessage);
+			_resetText = false;
+		}
+#endif
+
 		drawButtons(&windows[0]);
 	} while (_buttonValue != Common::KEYCODE_ESCAPE);
 
@@ -135,8 +201,12 @@ void BaseLocation::drawWindow() {
 	// Open up the window and write the string
 	intf.assembleBorder();
 	windows[10].open();
-	windows[10].writeString(title);
+	Common::String ttsMessage;
+	windows[10].writeString(title, false, &ttsMessage);
 	drawButtons(&windows[0]);
+#ifdef USE_TTS
+	speakTextAndButtons(ttsMessage);
+#endif
 
 	windows[0].update();
 	intf.highlightChar(0);
@@ -303,9 +373,9 @@ int BaseLocation::wait() {
 BankLocation::BankLocation() : BaseLocation(BANK) {
 	_icons1.load("bank.icn");
 	_icons2.load("bank2.icn");
-	addButton(Common::Rect(234, 108, 259, 128), Res.KeyConstants.Locations.KEY_DEP, &_icons1);
-	addButton(Common::Rect(261, 108, 285, 128), Res.KeyConstants.Locations.KEY_WITH, &_icons1);
-	addButton(Common::Rect(288, 108, 312, 128), Common::KEYCODE_ESCAPE, &_icons1);
+	addButton(Common::Rect(234, 108, 259, 128), Res.KeyConstants.Locations.KEY_DEP, &_icons1, kBankDeposit);
+	addButton(Common::Rect(261, 108, 285, 128), Res.KeyConstants.Locations.KEY_WITH, &_icons1, kBankWithdraw);
+	addButton(Common::Rect(288, 108, 312, 128), Common::KEYCODE_ESCAPE, &_icons1, kBankEscape);
 	_animFrame = 1;
 
 	_vocName = _ccNum ? "bank1.voc" : "banker.voc";
@@ -372,10 +442,16 @@ void BankLocation::depositWithdrawl(PartyBank whereId) {
 		XeenEngine::printMil(gems).c_str());
 
 	w.open();
-	w.writeString(msg);
+	Common::String ttsMessage;
+	w.writeString(msg, false, &ttsMessage);
 	drawButtons(&w);
 	w.update();
 
+#ifdef USE_TTS
+	Common::String oldButtonTexts[kBankButtonCount];
+	speakDepositWithdrawalText(ttsMessage, oldButtonTexts);
+#endif
+
 	sound.stopSound();
 	File voc("coina.voc");
 	ConsumableType consType = CONS_GOLD;
@@ -437,8 +513,12 @@ void BankLocation::depositWithdrawl(PartyBank whereId) {
 		}
 	} while (!g_vm->shouldExit());
 
-	for (uint idx = 0; idx < _buttons.size(); ++idx)
+	for (uint idx = 0; idx < _buttons.size(); ++idx) {
 		_buttons[idx]._sprites = &_icons1;
+#ifdef USE_TTS
+		_buttonTexts[idx] = oldButtonTexts[idx];
+#endif
+	}
 	_buttons[0]._value = Res.KeyConstants.Locations.KEY_DEP;
 	_buttons[1]._value = Res.KeyConstants.Locations.KEY_WITH;
 	_buttons[2]._value = Common::KEYCODE_ESCAPE;
@@ -447,13 +527,60 @@ void BankLocation::depositWithdrawl(PartyBank whereId) {
 	clearEvents();
 }
 
+#ifdef USE_TTS
+
+void BankLocation::speakTextAndButtons(const Common::String &text) {
+	uint index = 0;
+
+	// Dep/With/ESC buttons
+	Common::String buttonTexts = addNextTextToButtons(text, index, kBankButtonCount);
+
+	// Header ("Bank of ..." and "Bank")
+	_vm->sayText(getNextTextSection(text, index, 2));
+
+	// Bank gold and gems
+	for (uint8 i = 0; i < 2; ++i) {
+		_vm->sayText(getNextTextSection(text, index, 2, ": "));
+	}
+
+	// Header ("Party")
+	_vm->sayText(getNextTextSection(text, index));
+
+	// Party gold and gems
+	for (uint8 i = 0; i < 2; ++i) {
+		_vm->sayText(getNextTextSection(text, index, 2, ": "));
+	}
+
+	_vm->sayText(buttonTexts);
+}
+
+void BankLocation::speakDepositWithdrawalText(const Common::String &text, Common::String oldButtonTexts[]) {
+	uint index = 0;
+	// Title
+	_vm->sayText(getNextTextSection(text, index), Common::TextToSpeechManager::INTERRUPT);
+
+	// Gold and gems
+	for (uint8 i = 0; i < 2; ++i) {
+		_vm->sayText(getNextTextSection(text, index, 2, ": "));
+	}
+
+	// New buttons
+	for (uint i = 0; i < _buttonTexts.size(); ++i) {
+		oldButtonTexts[i] = _buttonTexts[i];
+		_buttonTexts[i] = getNextTextSection(text, index);
+		_vm->sayText(_buttonTexts[i]);
+	}
+}
+
+#endif
+
 /*------------------------------------------------------------------------*/
 
 BlacksmithLocation::BlacksmithLocation() : BaseLocation(BLACKSMITH) {
 	_icons1.load("esc.icn");
-	addButton(Common::Rect(261, 108, 285, 128), Common::KEYCODE_ESCAPE, &_icons1);
+	addButton(Common::Rect(261, 108, 285, 128), Common::KEYCODE_ESCAPE, &_icons1, kBlacksmithExit);
 	addButton(Common::Rect(234, 54, 308, 62), 0);
-	addButton(Common::Rect(234, 64, 308, 72), Res.KeyConstants.Locations.KEY_BROWSE);
+	addButton(Common::Rect(234, 64, 308, 72), Res.KeyConstants.Locations.KEY_BROWSE, nullptr, kBlacksmithBrowse);
 	addButton(Common::Rect(234, 74, 308, 82), 0);
 	addButton(Common::Rect(234, 84, 308, 92), 0);
 
@@ -475,6 +602,9 @@ Character *BlacksmithLocation::doOptions(Character *c) {
 		_buttonValue -= Common::KEYCODE_F1;
 		if (_buttonValue < (int)party._activeParty.size()) {
 			c = &party._activeParty[_buttonValue];
+#ifdef USE_TTS
+			_vm->sayText(c->_name, Common::TextToSpeechManager::INTERRUPT);
+#endif
 			intf.highlightChar(_buttonValue);
 		}
 	} else if (_buttonValue == Res.KeyConstants.Locations.KEY_BROWSE) {
@@ -497,15 +627,34 @@ void BlacksmithLocation::farewell() {
 	}
 }
 
+#ifdef USE_TTS
+
+void BlacksmithLocation::speakTextAndButtons(const Common::String &text) {
+	uint index = 0;
+	// Header ("Store options for" and character name)
+	_vm->sayText(getNextTextSection(text, index, 2, " "));
+
+	// Browse option
+	_vm->sayText(addNextTextToButtons(text, index));
+
+	// Gold
+	_vm->sayText(getNextTextSection(text, index, 2, ": "));
+
+	// ESC button
+	_vm->sayText(addNextTextToButtons(text, index));
+}
+
+#endif
+
 /*------------------------------------------------------------------------*/
 
 GuildLocation::GuildLocation() : BaseLocation(GUILD) {
 	loadStrings("spldesc.bin");
 	_icons1.load("esc.icn");
-	addButton(Common::Rect(261, 108, 285, 128), Common::KEYCODE_ESCAPE, &_icons1);
+	addButton(Common::Rect(261, 108, 285, 128), Common::KEYCODE_ESCAPE, &_icons1, kGuildExit);
 	addButton(Common::Rect(234, 54, 308, 62), 0);
-	addButton(Common::Rect(234, 64, 308, 72), Res.KeyConstants.Locations.KEY_BUY_SPELLS);
-	addButton(Common::Rect(234, 74, 308, 82), Res.KeyConstants.Locations.KEY_SPELL_INFO);
+	addButton(Common::Rect(234, 64, 308, 72), Res.KeyConstants.Locations.KEY_BUY_SPELLS, nullptr, kGuildBuySpells);
+	addButton(Common::Rect(234, 74, 308, 82), Res.KeyConstants.Locations.KEY_SPELL_INFO, nullptr, kGuildSpellInfo);
 	addButton(Common::Rect(234, 84, 308, 92), 0);
 	g_vm->_mode = MODE_INTERACTIVE7;
 
@@ -532,7 +681,10 @@ Character *GuildLocation::doOptions(Character *c) {
 		if (_buttonValue < (int)party._activeParty.size()) {
 			c = &party._activeParty[_buttonValue];
 			intf.highlightChar(_buttonValue);
-
+#ifdef USE_TTS
+			_vm->stopTextToSpeech();
+			_resetText = true;
+#endif
 			if (!c->guildMember()) {
 				sound.stopSound();
 				_animFrame = 5;
@@ -554,6 +706,38 @@ Character *GuildLocation::doOptions(Character *c) {
 	return c;
 }
 
+#ifdef USE_TTS
+
+void GuildLocation::speakTextAndButtons(const Common::String &text) {
+	uint index = 0;
+	bool isInGuild = g_vm->_party->_activeParty[_buttonValue].guildMember();
+
+	// Header
+	if (isInGuild) {	// "Guild options for" and character name
+		_vm->sayText(getNextTextSection(text, index, 2, " "));
+	} else {	// "Guild options for" and "need to be a member"
+		_vm->sayText(getNextTextSection(text, index, 2));
+	}
+
+	_buttonTexts.clear();
+
+	if (isInGuild) {
+		// Options
+		_vm->sayText(addNextTextToButtons(text, index, 2));
+	} else {
+		// Add empty buffers for the usual options strings, so the escape button aligns properly with the text
+		_buttonTexts.resize(2);
+	}
+
+	// Gold
+	_vm->sayText(getNextTextSection(text, index, 2, ": "));
+
+	// ESC
+	_vm->sayText(addNextTextToButtons(text, index));
+}
+
+#endif
+
 /*------------------------------------------------------------------------*/
 
 TavernLocation::TavernLocation() : BaseLocation(TAVERN) {
@@ -564,12 +748,12 @@ TavernLocation::TavernLocation() : BaseLocation(TAVERN) {
 
 	loadStrings("tavern.bin");
 	_icons1.load("tavern.icn");
-	addButton(Common::Rect(281, 108, 305, 128), Common::KEYCODE_ESCAPE, &_icons1);
-	addButton(Common::Rect(242, 108, 266, 128), Res.KeyConstants.Locations.KEY_SIGN_IN, &_icons1);
-	addButton(Common::Rect(234, 54, 308, 62), Res.KeyConstants.Locations.KEY_DRINK);
-	addButton(Common::Rect(234, 64, 308, 72), Res.KeyConstants.Locations.KEY_FOOD);
-	addButton(Common::Rect(234, 74, 308, 82), Res.KeyConstants.Locations.KEY_TIP);
-	addButton(Common::Rect(234, 84, 308, 92), Res.KeyConstants.Locations.KEY_RUMORS);
+	addButton(Common::Rect(281, 108, 305, 128), Common::KEYCODE_ESCAPE, &_icons1, kTavernExit);
+	addButton(Common::Rect(242, 108, 266, 128), Res.KeyConstants.Locations.KEY_SIGN_IN, &_icons1, kTavernSignIn);
+	addButton(Common::Rect(234, 54, 308, 62), Res.KeyConstants.Locations.KEY_DRINK, nullptr, kTavernDrink);
+	addButton(Common::Rect(234, 64, 308, 72), Res.KeyConstants.Locations.KEY_FOOD, nullptr, kTavernFood);
+	addButton(Common::Rect(234, 74, 308, 82), Res.KeyConstants.Locations.KEY_TIP, nullptr, kTavernTip);
+	addButton(Common::Rect(234, 84, 308, 92), Res.KeyConstants.Locations.KEY_RUMORS, nullptr, kTavernRumors);
 	g_vm->_mode = MODE_INTERACTIVE7;
 
 	_vocName = _ccNum ? "hello1.voc" : "hello.voc";
@@ -601,6 +785,9 @@ Character *TavernLocation::doOptions(Character *c) {
 		_buttonValue -= Common::KEYCODE_F1;
 		if (_buttonValue < (int)party._activeParty.size()) {
 			c = &party._activeParty[_buttonValue];
+#ifdef USE_TTS
+			_vm->sayText(c->_name, Common::TextToSpeechManager::INTERRUPT);
+#endif
 			intf.highlightChar(_buttonValue);
 			_v21 = 0;
 		}
@@ -612,9 +799,18 @@ Character *TavernLocation::doOptions(Character *c) {
 				sound.playSound("gulp.voc");
 				_v21 = 1;
 
+#ifdef USE_TTS
+				// Disable voicing for option buttons
+				disableButtonVoicing(2, _buttons.size());
+#endif
+
+				Common::String ttsMessage;
 				windows[10].writeString(Common::String::format(Res.TAVERN_TEXT,
 					c->_name.c_str(), Res.GOOD_STUFF,
-					XeenEngine::printMil(party._gold).c_str()));
+					XeenEngine::printMil(party._gold).c_str()), false, &ttsMessage);
+#ifdef USE_TTS
+				speakNotificationText(ttsMessage);
+#endif
 				drawButtons(&windows[0]);
 				windows[10].update();
 
@@ -625,6 +821,10 @@ Character *TavernLocation::doOptions(Character *c) {
 				}
 
 				wait();
+#ifdef USE_TTS
+				_vm->stopTextToSpeech();
+				enableButtonVoicing(2, _buttons.size());
+#endif
 			}
 		}
 	} else if (Res.KeyConstants.Locations.KEY_FOOD == _buttonValue) {
@@ -663,7 +863,13 @@ Character *TavernLocation::doOptions(Character *c) {
 
 		if (YesNo::show(_vm, false, true)) {
 			if (party._food >= _v22) {
+#ifdef USE_TTS
+				disableButtonVoicing(0, _buttons.size());
+#endif
 				ErrorScroll::show(_vm, Res.FOOD_PACKS_FULL, WT_LOC_WAIT);
+#ifdef USE_TTS
+				enableButtonVoicing(0, _buttons.size());
+#endif
 			} else if (party.subtract(CONS_GOLD, _v23, WHERE_PARTY, WT_LOC_WAIT)) {
 				party._food = _v22;
 				sound.stopSound();
@@ -673,6 +879,9 @@ Character *TavernLocation::doOptions(Character *c) {
 
 		windows[12].close();
 		windows[10].open();
+#ifdef USE_TTS
+		_vm->stopTextToSpeech();
+#endif
 		_buttonValue = 0;
 	} else if (Res.KeyConstants.Locations.KEY_RUMORS == _buttonValue) {
 		// Rumors
@@ -684,6 +893,9 @@ Character *TavernLocation::doOptions(Character *c) {
 			idx = 20;
 		}
 
+#ifdef USE_TTS
+		disableButtonVoicing(0, _buttons.size());
+#endif
 		Common::String msg = Common::String::format("\x03""c\x0B""012%s",
 			_textStrings[(party._day % 10) + idx].c_str());
 		Window &w = windows[12];
@@ -692,6 +904,10 @@ Character *TavernLocation::doOptions(Character *c) {
 		w.update();
 
 		wait();
+#ifdef USE_TTS
+		enableButtonVoicing(0, _buttons.size());
+		_vm->stopTextToSpeech();
+#endif
 		w.close();
 	} else if (Res.KeyConstants.Locations.KEY_SIGN_IN == _buttonValue) {
 		// Sign In
@@ -760,21 +976,46 @@ Character *TavernLocation::doOptions(Character *c) {
 	} else if (Res.KeyConstants.Locations.KEY_TIP == _buttonValue) {
 		if (!c->noActions()) {
 			if (!_v21) {
+#ifdef USE_TTS
+				disableButtonVoicing(0, _buttons.size());
+#endif
+				Common::String ttsMessage;
 				windows[10].writeString(Common::String::format(Res.TAVERN_TEXT,
 					c->_name.c_str(), Res.HAVE_A_DRINK,
-					XeenEngine::printMil(party._gold).c_str()));
+					XeenEngine::printMil(party._gold).c_str()), false, &ttsMessage);
 				drawButtons(&windows[0]);
 				windows[10].update();
+
+#ifdef USE_TTS
+				speakNotificationText(ttsMessage);
+#endif
+
 				wait();
+#ifdef USE_TTS
+				enableButtonVoicing(0, _buttons.size());
+#endif
 			} else {
 				_v21 = 0;
 				if (c->_conditions[DRUNK]) {
+#ifdef USE_TTS
+					disableButtonVoicing(2, _buttons.size());
+#endif
+					Common::String ttsMessage;
 					windows[10].writeString(Common::String::format(Res.TAVERN_TEXT,
 						c->_name.c_str(), Res.YOURE_DRUNK,
-						XeenEngine::printMil(party._gold).c_str()));
+						XeenEngine::printMil(party._gold).c_str()), false, &ttsMessage);
 					drawButtons(&windows[0]);
 					windows[10].update();
+
+#ifdef USE_TTS
+					speakNotificationText(ttsMessage);
+#endif
+
 					wait();
+#ifdef USE_TTS
+					_vm->stopTextToSpeech();
+					enableButtonVoicing(2, _buttons.size());
+#endif
 				} else if (party.subtract(CONS_GOLD, 1, WHERE_PARTY, WT_LOC_WAIT)) {
 					sound.stopSound();
 					sound.playSound(_ccNum ? "thanks2.voc" : "thankyou.voc", 1);
@@ -791,6 +1032,9 @@ Character *TavernLocation::doOptions(Character *c) {
 						_v24 = 50;
 					}
 
+#ifdef USE_TTS
+					disableButtonVoicing(0, _buttons.size());
+#endif
 					Common::String msg = _textStrings[map.mazeData()._tavernTips + _v24];
 					map.mazeData()._tavernTips = (map.mazeData()._tavernTips + 1) /
 						(_ccNum ? 10 : 15);
@@ -800,6 +1044,10 @@ Character *TavernLocation::doOptions(Character *c) {
 					w.writeString(Common::String::format("\x03""c\x0B""012%s", msg.c_str()));
 					w.update();
 					wait();
+#ifdef USE_TTS
+					enableButtonVoicing(0, _buttons.size());
+					_vm->stopTextToSpeech();
+#endif
 					w.close();
 				}
 			}
@@ -822,6 +1070,33 @@ void TavernLocation::farewell() {
 	map.mazeData()._mazeNumber = party._mazeId;
 }
 
+#ifdef USE_TTS
+
+void TavernLocation::speakTextAndButtons(const Common::String &text) {
+	uint index = 0;
+	// Header
+	_vm->sayText(getNextTextSection(text, index, 2, " "));
+
+	// Options
+	_vm->sayText(addNextTextToButtons(text, index, kTavernOptionsCount));
+
+	// Gold
+	_vm->sayText(getNextTextSection(text, index, 2, ": "));
+
+	// Sign in/ESC buttons
+	_vm->sayText(addNextTextToButtons(text, index, kTavernButtonCount));
+}
+
+void TavernLocation::speakNotificationText(const Common::String &text) const {
+	uint index = 0;
+	// Header
+	getNextTextSection(text, index, 2);
+	// Info
+	_vm->sayText(getNextTextSection(text, index, 2));
+}
+
+#endif
+
 /*------------------------------------------------------------------------*/
 
 TempleLocation::TempleLocation() : BaseLocation(TEMPLE) {
@@ -837,10 +1112,10 @@ TempleLocation::TempleLocation() : BaseLocation(TEMPLE) {
 	_v5 = _v6 = 0;
 
 	_icons1.load("esc.icn");
-	addButton(Common::Rect(261, 108, 285, 128), Common::KEYCODE_ESCAPE, &_icons1);
-	addButton(Common::Rect(234, 54, 308, 62), Res.KeyConstants.Locations.KEY_HEAL);
-	addButton(Common::Rect(234, 64, 308, 72), Res.KeyConstants.Locations.KEY_DONATION);
-	addButton(Common::Rect(234, 74, 308, 82), Res.KeyConstants.Locations.KEY_UNCURSE);
+	addButton(Common::Rect(261, 108, 285, 128), Common::KEYCODE_ESCAPE, &_icons1, kTempleExit);
+	addButton(Common::Rect(234, 54, 308, 62), Res.KeyConstants.Locations.KEY_HEAL, nullptr, kTempleHeal);
+	addButton(Common::Rect(234, 64, 308, 72), Res.KeyConstants.Locations.KEY_DONATION, nullptr, kTempleDonation);
+	addButton(Common::Rect(234, 74, 308, 82), Res.KeyConstants.Locations.KEY_UNCURSE, nullptr, kTempleUncurse);
 	addButton(Common::Rect(234, 84, 308, 92), 0);
 
 	_vocName = _ccNum ? "help2.voc" : "maywe2.voc";
@@ -924,6 +1199,9 @@ Character *TempleLocation::doOptions(Character *c) {
 		_buttonValue -= Common::KEYCODE_F1;
 		if (_buttonValue < (int)party._activeParty.size()) {
 			c = &party._activeParty[_buttonValue];
+#ifdef USE_TTS
+			_resetText = true;
+#endif
 			intf.highlightChar(_buttonValue);
 			_dayOfWeek = 0;
 		}
@@ -989,6 +1267,31 @@ Character *TempleLocation::doOptions(Character *c) {
 	return c;
 }
 
+#ifdef USE_TTS
+
+void TempleLocation::speakTextAndButtons(const Common::String &text) {
+	uint index = 0;
+	
+	// Header ("Options for" and character name)
+	_vm->sayText(getNextTextSection(text, index, 2, " "));
+
+	// Options
+	for (uint8 i = 0; i < kTempleOptionsCount; ++i) {
+		// Text and price
+		Common::String optionText = getNextTextSection(text, index, 2, ": ");
+		_buttonTexts.push_back(optionText);
+		_vm->sayText(optionText);
+	}
+
+	// Gold
+	_vm->sayText(getNextTextSection(text, index, 2, ": "));
+
+	// ESC button
+	_vm->sayText(addNextTextToButtons(text, index));
+}
+
+#endif
+
 /*------------------------------------------------------------------------*/
 
 TrainingLocation::TrainingLocation() : BaseLocation(TRAINING) {
@@ -997,8 +1300,8 @@ TrainingLocation::TrainingLocation() : BaseLocation(TRAINING) {
 	_charIndex = 0;
 
 	_icons1.load("train.icn");
-	addButton(Common::Rect(281, 108, 305, 128), Common::KEYCODE_ESCAPE, &_icons1);
-	addButton(Common::Rect(242, 108, 266, 128), Res.KeyConstants.Locations.KEY_TRAIN, &_icons1);
+	addButton(Common::Rect(281, 108, 305, 128), Common::KEYCODE_ESCAPE, &_icons1, kTrainingExit);
+	addButton(Common::Rect(242, 108, 266, 128), Res.KeyConstants.Locations.KEY_TRAIN, &_icons1, kTrainingTrain);
 
 	_vocName = _ccNum ? "youtrn1.voc" : "training.voc";
 }
@@ -1077,6 +1380,10 @@ Character *TrainingLocation::doOptions(Character *c) {
 		if (_buttonValue < (int)party._activeParty.size()) {
 			_charIndex = _buttonValue;
 			c = &party._activeParty[_buttonValue];
+#ifdef USE_TTS
+			_vm->sayText(c->_name, Common::TextToSpeechManager::INTERRUPT);
+#endif
+			_ttsVoiceText = true;
 			intf.highlightChar(_buttonValue);
 		}
 	} else if (Res.KeyConstants.Locations.KEY_TRAIN == _buttonValue) {
@@ -1112,6 +1419,7 @@ Character *TrainingLocation::doOptions(Character *c) {
 				c->_currentHp = c->getMaxHP();
 				c->_currentSp = c->getMaxSP();
 				intf.drawParty(true);
+				_ttsVoiceText = true;
 			}
 		}
 	}
@@ -1119,6 +1427,34 @@ Character *TrainingLocation::doOptions(Character *c) {
 	return c;
 }
 
+#ifdef USE_TTS
+
+void TrainingLocation::speakTextAndButtons(const Common::String &text) {
+	uint index = 0;
+	uint infoCount;
+
+	if (_experienceToNextLevel || g_vm->_party->_activeParty[_buttonValue]._level._permanent >= maxLevel()) {
+		infoCount = kTrainingIneligibleInfoCount;
+	} else {
+		infoCount = kTrainingEligibleInfoCount;
+	}
+
+	// Header and info
+	_vm->sayText(getNextTextSection(text, index, infoCount));
+
+	// Gold
+	_vm->sayText(getNextTextSection(text, index, 2, ": "));
+
+	// Train/ESC buttons
+	if (_buttonTexts.empty()) {
+		_vm->sayText(addNextTextToButtons(text, index, 2));
+	} else {
+		_vm->sayText(getNextTextSection(text, index, 2));
+	}
+}
+
+#endif
+
 /*------------------------------------------------------------------------*/
 
 ArenaLocation::ArenaLocation() : BaseLocation(ARENA) {
@@ -2381,12 +2717,21 @@ bool LocationMessage::execute(int portrait, const Common::String &name, const Co
 
 	int result = -1;
 	Common::String msgText = text;
+#ifdef USE_TTS
+	bool voiceName = true;
+#endif
 	do {
 		Common::String msg = Common::String::format(g_vm->getLanguage() == Common::ZH_TWN ? "\r\v014\x0c""07\x03""c\t125%s\x0c""04\x03""l\t000\v044%s" : "\r\v014\x03""c\t125%s\t000\v054%s",
 			name.c_str(), msgText.c_str());
 
+		Common::String ttsMessage;
 		// Count the number of words
-		const char *msgEnd = w.writeString(msg);
+		const char *msgEnd = w.writeString(msg, false, &ttsMessage);
+#ifdef USE_TTS
+		speakText(ttsMessage, name, voiceName);
+		voiceName = false;
+#endif
+
 		int wordCount = 0;
 
 		for (const char *msgP = msg.c_str(); msgP != msgEnd && *msgP; ++msgP) {
@@ -2452,6 +2797,9 @@ bool LocationMessage::execute(int portrait, const Common::String &name, const Co
 	if (!confirm)
 		intf.mainIconsPrint();
 
+#ifdef USE_TTS
+	_vm->stopTextToSpeech();
+#endif
 	_townSprites[0].clear();
 	_townSprites[1].clear();
 	events.clearEvents();
@@ -2466,5 +2814,28 @@ void LocationMessage::loadButtons() {
 	addButton(Common::Rect(), Common::KEYCODE_ESCAPE);
 }
 
+#ifdef USE_TTS
+
+void LocationMessage::speakText(const Common::String &text, const Common::String &name, bool voiceName) const {
+	uint index = 0;
+	_vm->stopTextToSpeech();
+
+	Common::String cleanedName;
+	// If the name has a newline character, it has two parts
+	if (name.find('\n') != Common::String::npos) {
+		cleanedName = getNextTextSection(text, index, 2, " ");
+	} else {
+		cleanedName = getNextTextSection(text, index);
+	}
+
+	if (voiceName) {
+		_vm->sayText(cleanedName);
+	}
+
+	_vm->sayText(text.substr(index));
+}
+
+#endif
+
 } // End of namespace Xeen
 } // End of namespace MM
diff --git a/engines/mm/xeen/locations.h b/engines/mm/xeen/locations.h
index 5ab0cd290af..c5970dc8e59 100644
--- a/engines/mm/xeen/locations.h
+++ b/engines/mm/xeen/locations.h
@@ -55,6 +55,10 @@ protected:
 	uint _farewellTime;
 	int _drawCtr1, _drawCtr2;
 	bool _exitToUi;
+	bool _ttsVoiceText;
+#ifdef USE_TTS
+	bool _resetText;
+#endif
 protected:
 	/**
 	 * Draw the window
@@ -80,6 +84,10 @@ protected:
 		return c;
 	}
 
+#ifdef USE_TTS
+	virtual void speakTextAndButtons(const Common::String &text) { }
+#endif
+
 	/**
 	 * Handle any farewell
 	 */
@@ -111,6 +119,21 @@ private:
 	 * Handles deposits or withdrawls fro the bank
 	 */
 	void depositWithdrawl(PartyBank whereId);
+
+#ifdef USE_TTS
+	/**
+	 * Voices text with TTS and sets up button texts
+	 * @param text	All text, with each section separated by newlines
+	 */
+	void speakTextAndButtons(const Common::String &text) override;
+
+	/**
+	 * Voices text with TTS and sets up button texts for the deposit/withdrawal menu
+	 * @param text				All text, with each section separated by newlines
+	 * @param oldButtonTexts	Array to put old button texts in
+	 */
+	void speakDepositWithdrawalText(const Common::String &text, Common::String oldButtonTexts[]);
+#endif
 protected:
 	/**
 	 * Generates the display text for the location, for a given character
@@ -133,6 +156,14 @@ public:
 };
 
 class BlacksmithLocation : public BaseLocation {
+private:
+#ifdef USE_TTS
+	/**
+	 * Voices text with TTS and sets up button texts
+	 * @param text	Text for voicing and button texts. Each section should be separated by a newline
+	 */
+	void speakTextAndButtons(const Common::String &text) override;
+#endif
 protected:
 	/**
 	* Generates the display text for the location, for a given character
@@ -155,6 +186,14 @@ public:
 };
 
 class GuildLocation : public BaseLocation {
+private:
+#ifdef USE_TTS
+	/**
+	 * Voices text with TTS and sets up button texts
+	 * @param text	Text for voicing and button texts. Each section should be separated by a newline
+	 */
+	void speakTextAndButtons(const Common::String &text) override;
+#endif
 protected:
 	/**
 	 * Generates the display text for the location, for a given character
@@ -172,6 +211,20 @@ public:
 };
 
 class TavernLocation : public BaseLocation {
+private:
+#ifdef USE_TTS
+	/**
+	 * Voices text with TTS and sets up button texts
+	 * @param text	Text for voicing and button texts. Each section should be separated by a newline
+	 */
+	void speakTextAndButtons(const Common::String &text) override;
+
+	/**
+	 * Voices "drunk" and similar notification texts with TTS
+	 * @param text	Text for voicing. Each section should be separated by a newline
+	 */
+	void speakNotificationText(const Common::String &text) const;
+#endif
 private:
 	int _v21;
 	uint _v22;
@@ -199,6 +252,14 @@ public:
 };
 
 class TempleLocation : public BaseLocation {
+private:
+#ifdef USE_TTS
+	/**
+	 * Voices text with TTS and sets up button texts
+	 * @param text	Text for voicing and button texts. Each section should be separated by a newline
+	 */
+	void speakTextAndButtons(const Common::String &text) override;
+#endif
 private:
 	int _currentCharLevel;
 	int _donation;
@@ -226,6 +287,14 @@ public:
 };
 
 class TrainingLocation : public BaseLocation {
+private:
+#ifdef USE_TTS
+	/**
+	 * Voices text with TTS and sets up button texts
+	 * @param text	Text for voicing and button texts. Each section should be separated by a newline
+	 */
+	void speakTextAndButtons(const Common::String &text) override;
+#endif
 private:
 	int _charIndex;
 	bool _charsTrained[MAX_ACTIVE_PARTY];
@@ -373,6 +442,10 @@ private:
 		const Common::String &text, int confirm);
 
 	void loadButtons();
+
+#ifdef USE_TTS
+	void speakText(const Common::String &text, const Common::String &name, bool voiceName) const;
+#endif
 public:
 	static bool showMessage(int portrait, const Common::String &name,
 		const Common::String &text, int confirm);
diff --git a/engines/mm/xeen/party.cpp b/engines/mm/xeen/party.cpp
index bae9d802003..3edef1d2fbb 100644
--- a/engines/mm/xeen/party.cpp
+++ b/engines/mm/xeen/party.cpp
@@ -722,6 +722,9 @@ void Party::giveTreasure() {
 	events.clearEvents();
 	w.close();
 	w.open();
+#ifdef USE_TTS
+	_vm->stopTextToSpeech();
+#endif
 	w.writeString(Common::String::format(Res.PARTY_FOUND, _treasure._gold, _treasure._gems));
 	w.update();
 
@@ -801,6 +804,9 @@ void Party::giveTreasure() {
 	_gems += _treasure._gems;
 	_treasure._gold = 0;
 	_treasure._gems = 0;
+#ifdef USE_TTS
+	_vm->stopTextToSpeech();
+#endif
 
 	_treasure._hasItems = false;
 	_treasure.clear();
@@ -850,7 +856,12 @@ void Party::giveTreasureToCharacter(Character &c, ItemCategory category, int ite
 	if (index >= (_vm->getGameID() == GType_Swords ? 88 : 82)) {
 		// Quest item, give an extra '*' prefix
 		Common::String format = Common::String::format("\f04 * \fd%s", itemName);
-		w.writeString(Common::String::format(Res.X_FOUND_Y, c._name.c_str(), getFoundForm(c), format.c_str()));
+		Common::String ttsMessage;
+		w.writeString(Common::String::format(Res.X_FOUND_Y, c._name.c_str(), getFoundForm(c), format.c_str()), false, &ttsMessage);
+#ifdef USE_TTS
+		ttsMessage.replace('*', ' ');
+		_vm->sayText(ttsMessage);
+#endif
 	} else {
 		w.writeString(Common::String::format(Res.X_FOUND_Y, c._name.c_str(), getFoundForm(c), itemName));
 	}
diff --git a/engines/mm/xeen/scripts.cpp b/engines/mm/xeen/scripts.cpp
index 81b154ea5b8..c940d9a737f 100644
--- a/engines/mm/xeen/scripts.cpp
+++ b/engines/mm/xeen/scripts.cpp
@@ -257,6 +257,9 @@ int Scripts::checkEvents() {
 	} else {
 		Window &w = windows[38];
 		w.open();
+#ifdef USE_TTS
+		_vm->stopTextToSpeech();
+#endif
 		w.writeString(Res.NOTHING_HERE);
 		w.update();
 
@@ -266,7 +269,9 @@ int Scripts::checkEvents() {
 			events.wait(1);
 		} while (!events.isKeyMousePressed() && !_vm->shouldExit());
 		events.clearEvents();
-
+#ifdef USE_TTS
+		_vm->stopTextToSpeech();
+#endif
 		w.close();
 	}
 
diff --git a/engines/mm/xeen/subtitles.cpp b/engines/mm/xeen/subtitles.cpp
index 91e80ab7fb1..c704a436e51 100644
--- a/engines/mm/xeen/subtitles.cpp
+++ b/engines/mm/xeen/subtitles.cpp
@@ -19,6 +19,7 @@
  *
  */
 
+#include "common/config-manager.h"
 #include "common/scummsys.h"
 #include "mm/xeen/subtitles.h"
 #include "mm/xeen/events.h"
@@ -90,6 +91,13 @@ void Subtitles::setLine(int line) {
 	_lineSize = _lines[_lineNum].size();
 	_lineEnd = 1;
 	_displayLine.clear();
+
+#ifdef USE_TTS
+	// Only voice subtitles if there's no voice
+	if ((ConfMan.hasKey("subtitles") && ConfMan.getBool("subtitles")) || ConfMan.getInt("speech_volume") == 0) {
+		g_vm->sayText(_lines[_lineNum], Common::TextToSpeechManager::QUEUE_NO_REPEAT);
+	}
+#endif
 }
 
 bool Subtitles::active() const {
@@ -134,7 +142,8 @@ void Subtitles::show() {
 		// Subtitles aren't needed
 		reset();
 	} else {
-		if (timeElapsed()) {
+		Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+		if (timeElapsed() && (_lineEnd != _lineSize - 1 || !ttsMan || !ttsMan->isSpeaking())) {
 			_lineEnd = (_lineEnd + 1) % _lineSize;
 			int count;
 			if (Common::RU_RUS == g_vm->getLanguage())
@@ -159,9 +168,9 @@ void Subtitles::show() {
 		_boxSprites->draw(0, 0, Common::Point(36, 189));
 
 		// Write the subtitle line
-		windows[0].writeString(_displayLine);
+		windows[0].writeString(_displayLine, false);
 
-		if (_lineEnd == 0)
+		if (_lineEnd == 0 && (!ttsMan || !ttsMan->isSpeaking()))
 			reset();
 	}
 }
diff --git a/engines/mm/xeen/swordsofxeen/swordsofxeen_menu.cpp b/engines/mm/xeen/swordsofxeen/swordsofxeen_menu.cpp
index b00442f960a..e8a2a0a87f2 100644
--- a/engines/mm/xeen/swordsofxeen/swordsofxeen_menu.cpp
+++ b/engines/mm/xeen/swordsofxeen/swordsofxeen_menu.cpp
@@ -28,6 +28,18 @@ namespace MM {
 namespace Xeen {
 namespace SwordsOfXeen {
 
+#ifdef USE_TTS
+
+static const char *swordsMainMenuButtons = "Start\nLoad\nView Credits";
+
+#endif
+
+enum SwordsMainMenuButtonTTSTextIndex {
+	kSwordsStart = 0,
+	kSwordsLoad = 1,
+	kSwordsCredits = 2
+};
+
 void MainMenu::show(XeenEngine *vm) {
 	MainMenu *dlg = new MainMenu(vm);
 	dlg->execute();
@@ -48,6 +60,9 @@ void MainMenu::execute() {
 	events.setCursor(0);
 	events.showCursor();
 	sound.playSong("newbrigh.m");
+#ifdef USE_TTS
+	setButtonTexts(swordsMainMenuButtons);
+#endif
 
 	do {
 		// Draw the screen
@@ -99,9 +114,9 @@ void MainMenu::execute() {
 }
 
 void MainMenu::loadButtons() {
-	addButton(Common::Rect(93, 87, 227, 97), Common::KEYCODE_s);
-	addButton(Common::Rect(93, 98, 227, 108), Common::KEYCODE_l);
-	addButton(Common::Rect(93, 110, 227, 120), Common::KEYCODE_v);
+	addButton(Common::Rect(93, 87, 227, 97), Common::KEYCODE_s, nullptr, kSwordsStart);
+	addButton(Common::Rect(93, 98, 227, 108), Common::KEYCODE_l, nullptr, kSwordsLoad);
+	addButton(Common::Rect(93, 110, 227, 120), Common::KEYCODE_v, nullptr, kSwordsCredits);
 }
 
 } // End of namespace SwordsOfXeen
diff --git a/engines/mm/xeen/window.h b/engines/mm/xeen/window.h
index 3e5a28ec408..dbb004e7d1f 100644
--- a/engines/mm/xeen/window.h
+++ b/engines/mm/xeen/window.h
@@ -147,13 +147,15 @@ public:
 
 	/**
 	 * Write a string to the window
-	 * @param s			String to display
-	 * @returns			Any string remainder that couldn't be displayed
+	 * @param s				String to display
+	 * @param ttsVoiceText	Whether to voice the text with the TTS system
+	 * @param ttsMessage	Message for TTS, which will be built
+	 * @returns				Any string remainder that couldn't be displayed
 	 * @remarks		Note that bounds is just used for wrapping purposes. Unless
 	 *		justification is set, the message will be written at _writePos
 	 */
-	const char *writeString(const Common::String &s) {
-		return FontSurface::writeString(s, _innerBounds);
+	const char *writeString(const Common::String &s, bool ttsVoiceText = true, Common::String *ttsMessage = nullptr) {
+		return FontSurface::writeString(s, _innerBounds, ttsVoiceText, ttsMessage);
 	}
 
 	/**
diff --git a/engines/mm/xeen/worldofxeen/clouds_cutscenes.cpp b/engines/mm/xeen/worldofxeen/clouds_cutscenes.cpp
index e93ad8e4e2b..97918013b1e 100644
--- a/engines/mm/xeen/worldofxeen/clouds_cutscenes.cpp
+++ b/engines/mm/xeen/worldofxeen/clouds_cutscenes.cpp
@@ -259,6 +259,8 @@ bool CloudsCutscenes::showCloudsIntroInner() {
 	screen.update();
 	_subtitles.setLine(0);
 
+	Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+
 	// Loop through each spoken line
 	int ctr1 = 0, ctr2 = 0, ctr3 = 0, ctr4 = 0, ctr5 = 0, totalCtr = 0;
 	for (int lineCtr = 0; lineCtr < 14; ++lineCtr) {
@@ -337,7 +339,7 @@ bool CloudsCutscenes::showCloudsIntroInner() {
 			case 12:
 			case 13: {
 				crodo.draw(0, 0, Common::Point(0, -5));
-				windows[0].writeString(Res.CLOUDS_INTRO1);
+				windows[0].writeString(Res.CLOUDS_INTRO1, false);
 
 				ctr5 = (ctr5 + 1) % 19;
 
@@ -350,7 +352,7 @@ bool CloudsCutscenes::showCloudsIntroInner() {
 				if (lookup > 30)
 					lookup = 30;
 				frameCtr = _INTRO_FRAMES_VALS[_INTRO_FRAMES_LOOKUP[lineCtr]][lookup];
-				windows[0].writeString(Res.CLOUDS_INTRO1);
+				windows[0].writeString(Res.CLOUDS_INTRO1, false);
 
 				ctr5 = (ctr5 + 1) % 19;
 				break;
@@ -359,7 +361,7 @@ bool CloudsCutscenes::showCloudsIntroInner() {
 			uint expiry = _INTRO_FRAMES_WAIT[_INTRO_FRAMES_LOOKUP[lineCtr]][lookup];
 			do {
 				WAIT(1);
-			} while (events.timeElapsed1() < expiry);
+			} while (events.timeElapsed1() < expiry || (!sound.isSoundPlaying() && ttsMan && ttsMan->isSpeaking()));
 
 			++lookup;
 			if (!sound._fxOn && lookup > 30)
diff --git a/engines/mm/xeen/worldofxeen/worldofxeen_menu.cpp b/engines/mm/xeen/worldofxeen/worldofxeen_menu.cpp
index 9bc30ba10c3..9f3b3c1b28a 100644
--- a/engines/mm/xeen/worldofxeen/worldofxeen_menu.cpp
+++ b/engines/mm/xeen/worldofxeen/worldofxeen_menu.cpp
@@ -30,6 +30,86 @@ namespace MM {
 namespace Xeen {
 namespace WorldOfXeen {
 
+#ifdef USE_TTS
+
+static const uint8 kOtherOptionsMaxButtonCount = 5;
+
+static const char *worldMainMenuButtons[] = {
+	"Start a New Game\nLoad a Saved Game\nCredits\nOther Options\n",	// English
+	"Neues Spiel\nSpielstand laden\nCredits\nWeitere Optionen\n",		// German
+	"Nouvelle Partie\nCharger une Partie\nCredits\nOptions\n",			// French
+	"Partida nueva\nCargar una partida\nCr\44ditos\nOtras opciones\n"	// Spanish
+	// No Russian or Chinese versions exist
+};
+
+static const char *worldOtherOptionsButtons[] = {
+	"View Darkside Intro\nView Clouds Intro\nView Darkside End\nView Clouds End\nView World End\n",			// English
+	"Darkside-Intro\nClouds-Intro\nDarkside-Ende\nClouds-Ende\nWorld-Ende\n",								// German
+	"Intro de Darkside\nIntro des Nuages\nFin de Darkside\nFin des Nuages\nFin de World\n",					// French	
+	"Intro de Darkside\nIntro de Clouds\nVer Final de Darkside\nVer Final de Clouds\nVer Final de World\n",	// Spanish
+	"",																										// Russian (no version exists)
+	// Chinese
+	"\xb6\xc2\xb7\x74\xbb\xe2\xa5\x44\xa4\xb6\xd2\xd0\n"	// 黑暗領主介紹
+	"\xb6\xb3\xa4\xb6\xd2\xd0\n"							// 雲介紹
+	"\xb6\xc2\xb7\x74\xbb\xe2\xa5\x44\xa5\xbd\xa7\xc0\n"	// 黑暗領主末尾
+	"\xb6\xb3\xa5\xbd\xa7\xc0\n"							// 雲末尾
+	"\xa5\x40\xac\xc9\n"									// 世界末尾
+};
+
+static const char *cloudsMainMenuButtons[] = {
+	"Start a New Game\nLoad a Saved Game\nCredits\nView Endgame\n",	// English
+	"Neues Spiel\nSpielstand laden\nCredits\nEndspiel ansehen\n",	// German
+	"Nouvelle Partie\nCharger une Partie\nCr\202dits\nVoir la fin",	// French
+	"",																// Spanish (no version exists)
+	// Russian
+	"\x8d\xa0\xe7\xa0\xe2\xec \xad\xae\xa2\xe3\xee \xa8\xa3\xe0\xe3\n"	// Начать новую игру
+	"\x87\xa0\xa3\xe0\xe3\xa7\xa8\xe2\xec \xa8\xa3\xe0\xe3\n"			// Загрузить игру
+	"\x91\xae\xa7\xa4\xa0\xe2\xa5\xab\xa8\n"							// Создатели
+	"\x94\xa8\xad\xa0\xab\xec\xad\xeb\xa9 \xe0\xae\xab\xa8\xaa\n",		// Финальный ролик
+	// Chinese
+	"\xb6\x7d\xa9\x6c\xb7\x73\xb9\x43\xc0\xb8\n"						// 開始新遊戲
+	"\xb8\xfc\xa4\x4a\xb6\x69\xab\xd7\xc0\xc9\n"						// 載入進度檔
+	"\xb9\x43\xc0\xb8\xbb\x73\xa7\x40\xb8\x73\n"						// 遊戲製作群
+	"\xac\x64\xac\xdd\xb4\xdd\xa7\xbd\n"								// 查看殘局
+};
+
+static const char *darksideMainMenuButtons[] = {
+	"Start\nLoad\nView Credits\nOther Options\n",	// English
+	"Start\nLaden\nCredits\nWeitere Optionen\n",	// German
+	"Nouveau\nPartie\nCredits\nOptions\n",			// French
+	"",												// Spanish (no version exists)
+	"",												// Russian (no version exists)
+	// Chinese
+	"\xb6\x7d\xa9\x6c\xb7\x73\xb9\x43\xc0\xb8\n"	// 開始新遊戲
+	"\xb8\xfc\xa4\x4a\xc2\xc2\xc0\xc9\n"			// 載入舊檔
+	"\xf7\x54\xac\xdd\xb3\x5d\xad\x70\xb8\x73\n"	// 覾看設計群
+	"\xa8\xe4\xa5\x4c\xbf\xef\xb6\xb5\n"			// 其他選項
+};
+
+#endif
+
+enum CloudsMainMenuButtonTTSTextIndex {
+	kCloudsNew = 0,
+	kCloudsLoad = 1,
+	kCloudsCredits = 2,
+	kCloudsEndgame = 3
+};
+
+enum DarksideWorldMainMenuButtonTTSTextIndex {
+	kDarksideWorldStart = 0,
+	kDarksideWorldLoad = 1,
+	kDarksideWorldCredits = 2,
+	kDarksideWorldOther = 3
+};
+
+enum OtherOptionsButtonTTSTextIndex {
+	kDarksideIntro = 0,
+	kCloudsIntro = 1,
+	kDarksideEnd = 2,
+	kCloudsEnd = 3,
+	kWorldEnd = 4
+};
+
 void MainMenuContainer::show() {
 	MainMenuContainer *menu;
 
@@ -105,11 +185,20 @@ void MainMenuContainer::execute() {
 
 	screen.doScroll(true, false);
 
+	if (_dialog) {
+		_dialog->_ttsVoiceText = true;
+	}
+
 	while (!g_vm->shouldExit() && g_vm->_gameMode == GMODE_NONE) {
 		// Draw the menu
 		draw();
-		if (_dialog)
+		if (_dialog) {
 			_dialog->draw();
+#ifdef USE_TTS
+			_dialog->checkHoverOverButton();
+#endif
+			_dialog->_ttsVoiceText = false;
+		}
 
 		// Fade/scroll in screen if first frame showing screen
 		if (!showFlag) {
@@ -306,6 +395,9 @@ CloudsMenuDialog::CloudsMenuDialog(MainMenuContainer *owner) : MainMenuDialog(ow
 	w.open();
 
 	loadButtons();
+#ifdef USE_TTS
+	setButtonTexts(cloudsMainMenuButtons[_vm->_ttsLanguage]);
+#endif
 }
 
 CloudsMenuDialog::~CloudsMenuDialog() {
@@ -316,11 +408,11 @@ CloudsMenuDialog::~CloudsMenuDialog() {
 
 void CloudsMenuDialog::loadButtons() {
 	_buttonSprites.load("start.icn");
-	addButton(Common::Rect(93, 53, 227, 73), Res.KeyConstants.CloudsOfXeenMenu.KEY_START_NEW_GAME, &_buttonSprites);
-	addButton(Common::Rect(93, 78, 227, 98), Res.KeyConstants.CloudsOfXeenMenu.KEY_LOAD_GAME, &_buttonSprites);
-	addButton(Common::Rect(93, 103, 227, 123), Res.KeyConstants.CloudsOfXeenMenu.KEY_SHOW_CREDITS, &_buttonSprites);
+	addButton(Common::Rect(93, 53, 227, 73), Res.KeyConstants.CloudsOfXeenMenu.KEY_START_NEW_GAME, &_buttonSprites, kCloudsNew);
+	addButton(Common::Rect(93, 78, 227, 98), Res.KeyConstants.CloudsOfXeenMenu.KEY_LOAD_GAME, &_buttonSprites, kCloudsLoad);
+	addButton(Common::Rect(93, 103, 227, 123), Res.KeyConstants.CloudsOfXeenMenu.KEY_SHOW_CREDITS, &_buttonSprites, kCloudsCredits);
 	if (g_vm->_gameWon[0])
-		addButton(Common::Rect(93, 128, 227, 148), Res.KeyConstants.CloudsOfXeenMenu.KEY_VIEW_ENDGAME, &_buttonSprites);
+		addButton(Common::Rect(93, 128, 227, 148), Res.KeyConstants.CloudsOfXeenMenu.KEY_VIEW_ENDGAME, &_buttonSprites, kCloudsEndgame);
 }
 
 void CloudsMenuDialog::draw() {
@@ -328,7 +420,7 @@ void CloudsMenuDialog::draw() {
 	Window &w = windows[GAME_WINDOW];
 
 	w.frame();
-	w.writeString(Common::String::format(Res.OPTIONS_MENU, Res.GAME_NAMES[0], g_vm->_gameWon[0] ? 117 : 92, 1992));
+	w.writeString(Common::String::format(Res.OPTIONS_MENU, Res.GAME_NAMES[0], g_vm->_gameWon[0] ? 117 : 92, 1992), _ttsVoiceText);
 	drawButtons(&w);
 }
 
@@ -360,6 +452,9 @@ DarkSideMenuDialog::DarkSideMenuDialog(MainMenuContainer *owner) : MainMenuDialo
 	w.open();
 
 	loadButtons();
+#ifdef USE_TTS
+	setButtonTexts(darksideMainMenuButtons[_vm->_ttsLanguage]);
+#endif
 }
 
 DarkSideMenuDialog::~DarkSideMenuDialog() {
@@ -369,10 +464,10 @@ DarkSideMenuDialog::~DarkSideMenuDialog() {
 }
 
 void DarkSideMenuDialog::loadButtons() {
-	addButton(Common::Rect(124, 87, 177, 97), Common::KEYCODE_s);
-	addButton(Common::Rect(126, 98, 173, 108), Common::KEYCODE_l);
-	addButton(Common::Rect(91, 110, 209, 120), Common::KEYCODE_c);
-	addButton(Common::Rect(85, 121, 216, 131), Common::KEYCODE_o);
+	addButton(Common::Rect(124, 87, 177, 97), Common::KEYCODE_s, nullptr, kDarksideWorldStart);
+	addButton(Common::Rect(126, 98, 173, 108), Common::KEYCODE_l, nullptr, kDarksideWorldLoad);
+	addButton(Common::Rect(91, 110, 209, 120), Common::KEYCODE_c, nullptr, kDarksideWorldCredits);
+	addButton(Common::Rect(85, 121, 216, 131), Common::KEYCODE_o, nullptr, kDarksideWorldOther);
 }
 
 void DarkSideMenuDialog::draw() {
@@ -450,6 +545,9 @@ WorldMenuDialog::WorldMenuDialog(MainMenuContainer *owner) : MainMenuDialog(owne
 	w.open();
 
 	loadButtons();
+#ifdef USE_TTS
+	setButtonTexts(worldMainMenuButtons[_vm->_ttsLanguage]);
+#endif
 }
 
 WorldMenuDialog::~WorldMenuDialog() {
@@ -460,10 +558,10 @@ WorldMenuDialog::~WorldMenuDialog() {
 
 void WorldMenuDialog::loadButtons() {
 	_buttonSprites.load("start.icn");
-	addButton(Common::Rect(93, 53, 227, 73), Common::KEYCODE_s, &_buttonSprites);
-	addButton(Common::Rect(93, 78, 227, 98), Common::KEYCODE_l, &_buttonSprites);
-	addButton(Common::Rect(93, 103, 227, 123), Common::KEYCODE_c, &_buttonSprites);
-	addButton(Common::Rect(93, 128, 227, 148), Common::KEYCODE_o, &_buttonSprites);
+	addButton(Common::Rect(93, 53, 227, 73), Common::KEYCODE_s, &_buttonSprites, kDarksideWorldStart);
+	addButton(Common::Rect(93, 78, 227, 98), Common::KEYCODE_l, &_buttonSprites, kDarksideWorldLoad);
+	addButton(Common::Rect(93, 103, 227, 123), Common::KEYCODE_c, &_buttonSprites, kDarksideWorldCredits);
+	addButton(Common::Rect(93, 128, 227, 148), Common::KEYCODE_o, &_buttonSprites, kDarksideWorldOther);
 }
 
 void WorldMenuDialog::draw() {
@@ -471,7 +569,7 @@ void WorldMenuDialog::draw() {
 	Window &w = windows[GAME_WINDOW];
 
 	w.frame();
-	w.writeString(Common::String::format(Res.OPTIONS_MENU, Res.GAME_NAMES[2], 117, 1993));
+	w.writeString(Common::String::format(Res.OPTIONS_MENU, Res.GAME_NAMES[2], 117, 1993), _ttsVoiceText);
 	drawButtons(&w);
 }
 
@@ -527,39 +625,44 @@ void OtherOptionsDialog::loadButtons() {
 	Common::Rect r(93, 53, 227, 73);
 
 	// View Darkside Intro
-	addButton(r, Common::KEYCODE_d, &_buttonSprites);
+	addButton(r, Common::KEYCODE_d, &_buttonSprites, kDarksideIntro);
 	r.translate(0, 25);
 
 	// View Clouds Intro
 	if (g_vm->getGameID() == GType_WorldOfXeen) {
-		addButton(r, Common::KEYCODE_c, &_buttonSprites);
+		addButton(r, Common::KEYCODE_c, &_buttonSprites, kCloudsIntro);
 		r.translate(0, 25);
 	} else {
-		addButton(Common::Rect(), Common::KEYCODE_INVALID);
+		addButton(Common::Rect(), Common::KEYCODE_INVALID, nullptr, kCloudsIntro);
 	}
 
 	// View Darkside End
 	if (g_vm->_gameWon[1]) {
-		addButton(r, Common::KEYCODE_e, &_buttonSprites);
+		addButton(r, Common::KEYCODE_e, &_buttonSprites, kDarksideEnd);
 		r.translate(0, 25);
 	} else {
-		addButton(Common::Rect(), Common::KEYCODE_INVALID);
+		addButton(Common::Rect(), Common::KEYCODE_INVALID, nullptr, kDarksideEnd);
 	}
 
 	// View Clouds End
 	if (g_vm->_gameWon[0]) {
-		addButton(r, Common::KEYCODE_v, &_buttonSprites);
+		addButton(r, Common::KEYCODE_v, &_buttonSprites, kCloudsEnd);
 		r.translate(0, 25);
 	} else {
-		addButton(Common::Rect(), Common::KEYCODE_INVALID);
+		addButton(Common::Rect(), Common::KEYCODE_INVALID, nullptr, kCloudsEnd);
 	}
 
 	// View World End
 	if (g_vm->_gameWon[2]) {
-		addButton(r, Common::KEYCODE_w, &_buttonSprites);
+		addButton(r, Common::KEYCODE_w, &_buttonSprites, kWorldEnd);
 	} else {
-		addButton(Common::Rect(), Common::KEYCODE_INVALID);
+		addButton(Common::Rect(), Common::KEYCODE_INVALID, nullptr, kWorldEnd);
 	}
+
+#ifdef USE_TTS
+	uint index = 0;
+	addNextTextToButtons(worldOtherOptionsButtons[_vm->_ttsLanguage], index, kOtherOptionsMaxButtonCount);
+#endif
 }
 
 void OtherOptionsDialog::draw() {
@@ -569,7 +672,7 @@ void OtherOptionsDialog::draw() {
 	w.frame();
 	w.writeString(Common::String::format(Res.OPTIONS_MENU,
 		Res.GAME_NAMES[g_vm->getGameID() == GType_WorldOfXeen ? 2 : 1],
-		w.getBounds().height() - 33, 1993));
+		w.getBounds().height() - 33, 1993), _ttsVoiceText);
 	drawButtons(&w);
 }
 
diff --git a/engines/mm/xeen/worldofxeen/worldofxeen_menu.h b/engines/mm/xeen/worldofxeen/worldofxeen_menu.h
index 2764a008bc8..b14bbf8a9b3 100644
--- a/engines/mm/xeen/worldofxeen/worldofxeen_menu.h
+++ b/engines/mm/xeen/worldofxeen/worldofxeen_menu.h
@@ -147,11 +147,13 @@ public:
 class MenuContainerDialog : public ButtonContainer {
 protected:
 	MainMenuContainer *_owner;
+public:
+	bool _ttsVoiceText;
 public:
 	/**
 	 * Constructor
 	 */
-	MenuContainerDialog(MainMenuContainer *owner) : ButtonContainer(g_vm), _owner(owner) {}
+	MenuContainerDialog(MainMenuContainer *owner) : ButtonContainer(g_vm), _owner(owner), _ttsVoiceText(true) {}
 
 	/**
 	 * Destructor
diff --git a/engines/mm/xeen/xeen.cpp b/engines/mm/xeen/xeen.cpp
index 17aefa70438..fceb3e18053 100644
--- a/engines/mm/xeen/xeen.cpp
+++ b/engines/mm/xeen/xeen.cpp
@@ -31,6 +31,16 @@
 namespace MM {
 namespace Xeen {
 
+#ifdef USE_TTS
+
+static const uint16 spanishEncodingTable[] = {
+	0x23, 0xc3a1, 0x24, 0xc3a9, 0x25, 0xc3bc, 0x26, 0xc3b3, 0x3d, 0xc3b1, 0x5b, 0xc2bf,	// á, é, ü, ó, ñ, ¿
+	0x5c, 0xc39a, 0x5d, 0xc3ba, 0x5e, 0xc389, 0x5f, 0xc381, 0x7b, 0xc2a1, 0x7d, 0xc3ad,	// Ú, ú, É, Á, ¡, í
+	0
+};
+
+#endif
+
 XeenEngine *g_vm = nullptr;
 
 XeenEngine::XeenEngine(OSystem *syst, const MightAndMagicGameDescription *gameDesc)
@@ -59,6 +69,9 @@ XeenEngine::XeenEngine(OSystem *syst, const MightAndMagicGameDescription *gameDe
 	_loadSaveSlot = -1;
 	_gameWon[0] = _gameWon[1] = _gameWon[2] = false;
 	_finalScore = 0;
+#ifdef USE_TTS
+	_mouseMoved = false;
+#endif
 	g_vm = this;
 }
 
@@ -111,6 +124,46 @@ bool XeenEngine::initialize() {
 	// Setup mixer
 	syncSoundSettings();
 
+#ifdef USE_TTS
+	Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+	if (ttsMan != nullptr) {
+		ttsMan->enable(ConfMan.getBool("tts_enabled"));
+		ttsMan->setLanguage(ConfMan.get("language"));
+	}
+
+	switch (getLanguage()) {
+	case Common::EN_ANY:
+		_ttsLanguage = kEnglish;
+		break;
+	case Common::DE_DEU:
+		_ttsLanguage = kGerman;
+		break;
+	case Common::FR_FRA:
+		_ttsLanguage = kFrench;
+		break;
+	case Common::ES_ESP:
+		_ttsLanguage = kSpanish;
+		break;
+	case Common::RU_RUS:
+		_ttsLanguage = kRussian;
+		break;
+	case Common::ZH_TWN:
+		_ttsLanguage = kChinese;
+		break;
+	default:
+		_ttsLanguage = kEnglish;
+		break;
+	}
+
+	if (_ttsLanguage == kRussian) {
+		_ttsTextEncoding = Common::CodePage::kDos866;
+	} else if (_ttsLanguage == kChinese) {
+		_ttsTextEncoding = Common::CodePage::kBig5;
+	} else {
+		_ttsTextEncoding = Common::CodePage::kDos850;
+	}
+#endif
+
 	// Load settings
 	loadSettings();
 
@@ -327,6 +380,69 @@ void XeenEngine::GUIError(const Common::U32String &msg) {
 	GUIErrorMessage(msg);
 }
 
+#ifdef USE_TTS
+
+void XeenEngine::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")) {
+		if (getLanguage() == Common::ES_ESP) {
+			ttsMan->say(convertSpanishText(text), action);
+		} else {
+			ttsMan->say(text, action, _ttsTextEncoding);
+		}
+	}
+}
+
+void XeenEngine::stopTextToSpeech() const {
+	Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+	if (ttsMan && ConfMan.getBool("tts_enabled") && ttsMan->isSpeaking()) {
+		ttsMan->stop();
+	}
+}
+
+Common::U32String XeenEngine::convertSpanishText(const Common::String &text) const {
+	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) {
+		if (*b == 0x60) {	// ` to =
+			convertedBytes[i] = 0x3d;
+			i++;
+			continue;
+		}
+
+		bool inTable = false;
+		for (uint j = 0; spanishEncodingTable[j]; j += 2) {
+			if (*b == spanishEncodingTable[j]) {
+				convertedBytes[i] = (spanishEncodingTable[j + 1] >> 8) & 0xff;
+				convertedBytes[i + 1] = spanishEncodingTable[j + 1] & 0xff;
+				i += 2;
+				inTable = true;
+				break;
+			}
+		}
+
+		if (!inTable) {
+			convertedBytes[i] = *b;
+			i++;
+		}
+	}
+
+	convertedBytes[i] = 0;
+
+	Common::U32String result((char *)convertedBytes);
+	delete[] convertedBytes;
+
+	return result;
+}
+
+#endif
+
 
 uint32 XeenEngine::getSpecificGameId() const {
 	uint gameId = g_vm->getGameID();
diff --git a/engines/mm/xeen/xeen.h b/engines/mm/xeen/xeen.h
index aede957108c..fcb0fa04dbc 100644
--- a/engines/mm/xeen/xeen.h
+++ b/engines/mm/xeen/xeen.h
@@ -24,6 +24,7 @@
 
 #include "common/scummsys.h"
 #include "common/system.h"
+#include "common/text-to-speech.h"
 #include "common/error.h"
 #include "common/random.h"
 #include "common/serializer.h"
@@ -88,6 +89,19 @@ enum GameMode {
 	GMODE_QUIT = 4
 };
 
+#ifdef USE_TTS
+
+enum TTSLanguage {
+	kEnglish = 0,
+	kGerman = 1,
+	kFrench = 2,
+	kSpanish = 3,
+	kRussian = 4,
+	kChinese = 5
+};
+
+#endif
+
 #define XEEN_SAVEGAME_VERSION 2
 
 class XeenEngine : public MMEngine {
@@ -181,6 +195,11 @@ public:
 	bool _gameWon[3];
 	uint _finalScore;
 	ExtendedOptions _extOptions;
+#ifdef USE_TTS
+	bool _mouseMoved;
+	TTSLanguage _ttsLanguage;
+	Common::CodePage _ttsTextEncoding;
+#endif
 
 	CCArchive *_xeenCc = nullptr, *_darkCc = nullptr,
 		*_introCc = nullptr;
@@ -285,6 +304,25 @@ public:
 	 * Show an error message in a GUI dialog
 	 */
 	void GUIError(const Common::U32String &msg);
+
+#ifdef USE_TTS
+	/**
+	 * Voice text with the text-to-speech system
+	 */
+	void sayText(const Common::String &text, Common::TextToSpeechManager::Action action = Common::TextToSpeechManager::QUEUE_NO_REPEAT) const;
+
+	/**
+	 * Stop the text-to-speech system's speech
+	 */
+	void stopTextToSpeech() const;
+
+	/**
+	 * Convert text to a custom encoding for Spanish text
+	 * @param text	Text to convert
+	 * @returns		Converted string
+	 */
+	Common::U32String convertSpanishText(const Common::String &text) const;
+#endif
 };
 
 extern XeenEngine *g_vm;




More information about the Scummvm-git-logs mailing list