[Scummvm-git-logs] scummvm master -> 2d5d17e6dc4365840f85099f6ce422793981c623

fracturehill noreply at scummvm.org
Tue Sep 19 14:42:28 UTC 2023


This automated email contains information about 22 new commits which have been
pushed to the 'scummvm' repo located at https://github.com/scummvm/scummvm .

Summary:
896f90929a NANCY:  Fix cursor hotspots in nancy6
9711675f50 NANCY: Implement auto-move
e35722f382 NANCY: Improve BSUM reading
73c6d2087d NANCY: Avoid immediate crash when an error is thrown
ee42466559 NANCY: Print additional info alongside errors
fa522ff939 NANCY: Move hypertext handling outside Textbox
3bbd8e6bb5 NANCY: Improve TBOX and FONT chunk reading
bfc576fb8d NANCY: Fix for some 3D sounds not playing
c3f86160d8 NANCY: Fix crash in ConversationCel
b0c84deb2b NANCY: Do not exit when reaching unknown AR type
839486c452 NANCY: Improve scan_ar_type console command
070e6ff2c9 NANCY: Fix issues in PianoPuzzle
393f686b34 NANCY: Respect movie frame src rects
5f800b8d7e NANCY: Overlay fixes
cdb0d4aaff NANCY: Do not restart already playing sounds
78f82c9ab9 NANCY: Do not repeat "can't" sound caption
087d9c4f39 NANCY: Correctly initialize 3D sound
f0d189498d NANCY: Fix 3D sound pop when changing scenes
ed49ad1102 NANCY: Improve textbox rendering
547cc34efe NANCY: Fix Coverity issues
f9ccd48a8e NANCY: Handle Overlay input as special case
2d5d17e6dc NANCY: Show last frame of videos


Commit: 896f90929adeb2a30b0d5d603c779d4daf36b0c5
    https://github.com/scummvm/scummvm/commit/896f90929adeb2a30b0d5d603c779d4daf36b0c5
Author: Kaloyan Chehlarski (strahy at outlook.com)
Date: 2023-09-19T17:38:23+03:00

Commit Message:
NANCY:  Fix cursor hotspots in nancy6

Added a heuristic for figuring out the number of cursor
types in every game.  This removes the need for a massive
switch statement for every game title, and fixes an issue
in nancy6 where hotspots would be wildly incorrect.

Changed paths:
    engines/nancy/cursor.cpp


diff --git a/engines/nancy/cursor.cpp b/engines/nancy/cursor.cpp
index d977d3689a0..9a66a20a6b3 100644
--- a/engines/nancy/cursor.cpp
+++ b/engines/nancy/cursor.cpp
@@ -43,20 +43,10 @@ void CursorManager::init(Common::SeekableReadStream *chunkStream) {
 
 	chunkStream->seek(0);
 
-	switch(g_nancy->getGameType()) {
-	case kGameTypeVampire:
-		// fall thorugh
-	case kGameTypeNancy1:
-		_numCursorTypes = 4;
-		break;
-	case kGameTypeNancy2:
-		_numCursorTypes = 5;
-		break;
-	case kGameTypeNancy3:
-		_numCursorTypes = 8;
-		break;
-	default:
-		_numCursorTypes = 12;
+	if (g_nancy->getGameType() == kGameTypeVampire) {
+		_numCursorTypes = g_nancy->getStaticData().numNonItemCursors / 2;
+	} else {
+		_numCursorTypes = g_nancy->getStaticData().numNonItemCursors / 3;
 	}
 
 	uint numCursors = g_nancy->getStaticData().numNonItemCursors + g_nancy->getStaticData().numItems * _numCursorTypes;
@@ -106,8 +96,7 @@ void CursorManager::setCursor(CursorType type, int16 itemID) {
 
 	_hasItem = false;
 
-	// kNormalArrow, kHotspotArrow, kExit, kTurnLeft and kTurnRight are
-	// cases where the selected cursor is _always_ shown, regardless
+	// For all cases below, the selected cursor is _always_ shown, regardless
 	// of whether or not an item is held. All other types of cursor
 	// are overridable when holding an item. Every item cursor has
 	// _numItemCursor variants, one corresponding to every numbered


Commit: 9711675f50b635e50f27e63c38b075f64ad76be5
    https://github.com/scummvm/scummvm/commit/9711675f50b635e50f27e63c38b075f64ad76be5
Author: Kaloyan Chehlarski (strahy at outlook.com)
Date: 2023-09-19T17:38:24+03:00

Commit Message:
NANCY: Implement auto-move

Implemented the (optional) automatic viewport movement
when hovering over an edge introduced in nancy6. Also
made the option for enabling/disabling it in the Setup
screen functional, and added a GUI option to MetaEngine's
extra options map.

Changed paths:
    engines/nancy/detection.cpp
    engines/nancy/detection.h
    engines/nancy/metaengine.cpp
    engines/nancy/state/setupmenu.cpp
    engines/nancy/state/setupmenu.h
    engines/nancy/ui/viewport.cpp
    engines/nancy/ui/viewport.h


diff --git a/engines/nancy/detection.cpp b/engines/nancy/detection.cpp
index 69b40de00e3..8d41a88e5ee 100644
--- a/engines/nancy/detection.cpp
+++ b/engines/nancy/detection.cpp
@@ -63,7 +63,7 @@ static const Nancy::NancyGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformWindows,
 			ADGF_TESTING | ADGF_DROPLANGUAGE | ADGF_DROPPLATFORM,
-			GUIO1(GUIO_NOLANG)
+			GUIO3(GUIO_NOLANG, GAMEOPTION_PLAYER_SPEECH, GAMEOPTION_CHARACTER_SPEECH)
 		},
 		Nancy::kGameTypeVampire
 	},
@@ -74,7 +74,7 @@ static const Nancy::NancyGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformWindows,
 			ADGF_TESTING | ADGF_DROPPLATFORM,
-			GUIO0()
+			GUIO2(GAMEOPTION_PLAYER_SPEECH, GAMEOPTION_CHARACTER_SPEECH)
 		},
 		Nancy::kGameTypeNancy1
 	},
@@ -85,7 +85,7 @@ static const Nancy::NancyGameDescription gameDescriptions[] = {
 			Common::RU_RUS,
 			Common::kPlatformWindows,
 			ADGF_TESTING | ADGF_DROPPLATFORM,
-			GUIO0()
+			GUIO2(GAMEOPTION_PLAYER_SPEECH, GAMEOPTION_CHARACTER_SPEECH)
 		},
 		Nancy::kGameTypeNancy1
 	},
@@ -101,7 +101,7 @@ static const Nancy::NancyGameDescription gameDescriptions[] = {
 			Common::RU_RUS,
 			Common::kPlatformWindows,
 			ADGF_TESTING | ADGF_DROPPLATFORM | Nancy::GF_COMPRESSED,
-			GUIO0()
+			GUIO2(GAMEOPTION_PLAYER_SPEECH, GAMEOPTION_CHARACTER_SPEECH)
 		},
 		Nancy::kGameTypeNancy1
 	},
@@ -112,7 +112,7 @@ static const Nancy::NancyGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformWindows,
 			ADGF_UNSTABLE | ADGF_DROPPLATFORM,
-			GUIO0()
+			GUIO2(GAMEOPTION_PLAYER_SPEECH, GAMEOPTION_CHARACTER_SPEECH)
 		},
 		Nancy::kGameTypeNancy2
 	},
@@ -123,7 +123,7 @@ static const Nancy::NancyGameDescription gameDescriptions[] = {
 			Common::RU_RUS,
 			Common::kPlatformWindows,
 			ADGF_UNSTABLE | ADGF_DROPPLATFORM,
-			GUIO0()
+			GUIO2(GAMEOPTION_PLAYER_SPEECH, GAMEOPTION_CHARACTER_SPEECH)
 		},
 		Nancy::kGameTypeNancy2
 	},
@@ -139,7 +139,7 @@ static const Nancy::NancyGameDescription gameDescriptions[] = {
 			Common::RU_RUS,
 			Common::kPlatformWindows,
 			ADGF_UNSTABLE | ADGF_DROPPLATFORM | Nancy::GF_COMPRESSED,
-			GUIO0()
+			GUIO2(GAMEOPTION_PLAYER_SPEECH, GAMEOPTION_CHARACTER_SPEECH)
 		},
 		Nancy::kGameTypeNancy2
 	},
@@ -150,7 +150,7 @@ static const Nancy::NancyGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformWindows,
 			ADGF_UNSTABLE | ADGF_DROPPLATFORM,
-			GUIO0()
+			GUIO2(GAMEOPTION_PLAYER_SPEECH, GAMEOPTION_CHARACTER_SPEECH)
 		},
 		Nancy::kGameTypeNancy3
 	},
@@ -161,7 +161,7 @@ static const Nancy::NancyGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformWindows,
 			ADGF_UNSTABLE | ADGF_DROPPLATFORM,
-			GUIO0()
+			GUIO2(GAMEOPTION_PLAYER_SPEECH, GAMEOPTION_CHARACTER_SPEECH)
 		},
 		Nancy::kGameTypeNancy3
 	},
@@ -177,7 +177,7 @@ static const Nancy::NancyGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformWindows,
 			ADGF_UNSTABLE | ADGF_DROPPLATFORM | Nancy::GF_COMPRESSED,
-			GUIO0()
+			GUIO2(GAMEOPTION_PLAYER_SPEECH, GAMEOPTION_CHARACTER_SPEECH)
 		},
 		Nancy::kGameTypeNancy3
 	},
@@ -188,7 +188,7 @@ static const Nancy::NancyGameDescription gameDescriptions[] = {
 			Common::RU_RUS,
 			Common::kPlatformWindows,
 			ADGF_UNSTABLE | ADGF_DROPPLATFORM,
-			GUIO0()
+			GUIO2(GAMEOPTION_PLAYER_SPEECH, GAMEOPTION_CHARACTER_SPEECH)
 		},
 		Nancy::kGameTypeNancy3
 	},
@@ -204,7 +204,7 @@ static const Nancy::NancyGameDescription gameDescriptions[] = {
 			Common::RU_RUS,
 			Common::kPlatformWindows,
 			ADGF_UNSTABLE | ADGF_DROPPLATFORM | Nancy::GF_COMPRESSED,
-			GUIO0()
+			GUIO2(GAMEOPTION_PLAYER_SPEECH, GAMEOPTION_CHARACTER_SPEECH)
 		},
 		Nancy::kGameTypeNancy3
 	},
@@ -215,7 +215,7 @@ static const Nancy::NancyGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformWindows,
 			ADGF_UNSTABLE | ADGF_DROPPLATFORM,
-			GUIO0()
+			GUIO2(GAMEOPTION_PLAYER_SPEECH, GAMEOPTION_CHARACTER_SPEECH)
 		},
 		Nancy::kGameTypeNancy4
 	},
@@ -226,7 +226,7 @@ static const Nancy::NancyGameDescription gameDescriptions[] = {
 			Common::RU_RUS,
 			Common::kPlatformWindows,
 			ADGF_UNSTABLE | ADGF_DROPPLATFORM,
-			GUIO0()
+			GUIO2(GAMEOPTION_PLAYER_SPEECH, GAMEOPTION_CHARACTER_SPEECH)
 		},
 		Nancy::kGameTypeNancy4
 	},
@@ -237,7 +237,7 @@ static const Nancy::NancyGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformWindows,
 			ADGF_UNSTABLE | ADGF_DROPPLATFORM,
-			GUIO0()
+			GUIO2(GAMEOPTION_PLAYER_SPEECH, GAMEOPTION_CHARACTER_SPEECH)
 		},
 		Nancy::kGameTypeNancy4
 	},
@@ -253,7 +253,7 @@ static const Nancy::NancyGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformWindows,
 			ADGF_UNSTABLE | ADGF_DROPPLATFORM | Nancy::GF_COMPRESSED,
-			GUIO0()
+			GUIO2(GAMEOPTION_PLAYER_SPEECH, GAMEOPTION_CHARACTER_SPEECH)
 		},
 		Nancy::kGameTypeNancy4
 	},
@@ -269,7 +269,7 @@ static const Nancy::NancyGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformWindows,
 			ADGF_UNSTABLE | ADGF_DROPPLATFORM | Nancy::GF_COMPRESSED,
-			GUIO0()
+			GUIO2(GAMEOPTION_PLAYER_SPEECH, GAMEOPTION_CHARACTER_SPEECH)
 		},
 		Nancy::kGameTypeNancy4
 	},
@@ -286,7 +286,7 @@ static const Nancy::NancyGameDescription gameDescriptions[] = {
 			Common::RU_RUS,
 			Common::kPlatformWindows,
 			ADGF_UNSTABLE | ADGF_DROPPLATFORM | Nancy::GF_COMPRESSED,
-			GUIO0()
+			GUIO2(GAMEOPTION_PLAYER_SPEECH, GAMEOPTION_CHARACTER_SPEECH)
 		},
 		Nancy::kGameTypeNancy4
 	},
@@ -297,7 +297,7 @@ static const Nancy::NancyGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformWindows,
 			ADGF_UNSTABLE | ADGF_DROPPLATFORM,
-			GUIO0()
+			GUIO2(GAMEOPTION_PLAYER_SPEECH, GAMEOPTION_CHARACTER_SPEECH)
 		},
 		Nancy::kGameTypeNancy5
 	},
@@ -313,7 +313,7 @@ static const Nancy::NancyGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformWindows,
 			ADGF_UNSTABLE | ADGF_DROPPLATFORM | Nancy::GF_COMPRESSED,
-			GUIO0()
+			GUIO2(GAMEOPTION_PLAYER_SPEECH, GAMEOPTION_CHARACTER_SPEECH)
 		},
 		Nancy::kGameTypeNancy5
 	},
@@ -329,7 +329,7 @@ static const Nancy::NancyGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformWindows,
 			ADGF_UNSTABLE | ADGF_DROPPLATFORM | Nancy::GF_COMPRESSED,
-			GUIO0()
+			GUIO2(GAMEOPTION_PLAYER_SPEECH, GAMEOPTION_CHARACTER_SPEECH)
 		},
 		Nancy::kGameTypeNancy5
 	},
@@ -345,7 +345,7 @@ static const Nancy::NancyGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformWindows,
 			ADGF_UNSTABLE | ADGF_DROPPLATFORM | Nancy::GF_COMPRESSED,
-			GUIO0()
+			GUIO2(GAMEOPTION_PLAYER_SPEECH, GAMEOPTION_CHARACTER_SPEECH)
 		},
 		Nancy::kGameTypeNancy5
 	},
@@ -356,7 +356,7 @@ static const Nancy::NancyGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformWindows,
 			ADGF_UNSTABLE | ADGF_DROPPLATFORM,
-			GUIO0()
+			GUIO1(GAMEOPTION_AUTO_MOVE)
 		},
 		Nancy::kGameTypeNancy6
 	},
@@ -372,7 +372,7 @@ static const Nancy::NancyGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformWindows,
 			ADGF_UNSTABLE | ADGF_DROPPLATFORM | Nancy::GF_COMPRESSED,
-			GUIO0()
+			GUIO1(GAMEOPTION_AUTO_MOVE)
 		},
 		Nancy::kGameTypeNancy6
 	},
@@ -383,7 +383,7 @@ static const Nancy::NancyGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformWindows,
 			ADGF_UNSTABLE | ADGF_DROPPLATFORM,
-			GUIO0()
+			GUIO1(GAMEOPTION_AUTO_MOVE)
 		},
 		Nancy::kGameTypeNancy7
 	},
@@ -410,7 +410,7 @@ static const Nancy::NancyGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformWindows,
 			ADGF_UNSTABLE | ADGF_DROPPLATFORM,
-			GUIO0()
+			GUIO1(GAMEOPTION_AUTO_MOVE)
 		},
 		Nancy::kGameTypeNancy8
 	},
@@ -421,7 +421,7 @@ static const Nancy::NancyGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformWindows,
 			ADGF_UNSTABLE | ADGF_DROPPLATFORM,
-			GUIO0()
+			GUIO1(GAMEOPTION_AUTO_MOVE)
 		},
 		Nancy::kGameTypeNancy9
 	},
@@ -435,7 +435,7 @@ public:
 	NancyMetaEngineDetection() : AdvancedMetaEngineDetection(gameDescriptions, sizeof(Nancy::NancyGameDescription), nancyGames) {
 		_maxScanDepth = 2;
 		_directoryGlobs = directoryGlobs;
-		_guiOptions = GUIO4(GUIO_NOMIDI, GUIO_NOASPECT, GUIO_GAMEOPTIONS1, GUIO_GAMEOPTIONS2);
+		_guiOptions = GUIO2(GUIO_NOMIDI, GUIO_NOASPECT);
 	}
 
 	const char *getName() const override {
diff --git a/engines/nancy/detection.h b/engines/nancy/detection.h
index 8e7e05e8027..ce721356cca 100644
--- a/engines/nancy/detection.h
+++ b/engines/nancy/detection.h
@@ -56,6 +56,10 @@ enum NancyDebugChannels {
 	kDebugSound			= 1 << 3
 };
 
+#define GAMEOPTION_PLAYER_SPEECH GUIO_GAMEOPTIONS1
+#define GAMEOPTION_CHARACTER_SPEECH GUIO_GAMEOPTIONS2
+#define GAMEOPTION_AUTO_MOVE GUIO_GAMEOPTIONS3
+
 } // End of namespace Nancy
 
 #endif // NANCY_DETECTION_H
diff --git a/engines/nancy/metaengine.cpp b/engines/nancy/metaengine.cpp
index 60b2f7dad39..e932904488f 100644
--- a/engines/nancy/metaengine.cpp
+++ b/engines/nancy/metaengine.cpp
@@ -33,7 +33,7 @@
 
 static const ADExtraGuiOptionsMap optionsList[] = {
 	{
-		GUIO_GAMEOPTIONS1,
+		GAMEOPTION_PLAYER_SPEECH,
 		{
 			_s("Player Speech"),
 			_s("Enable player speech. Only works if speech is enabled in the Audio settings."),
@@ -44,7 +44,7 @@ static const ADExtraGuiOptionsMap optionsList[] = {
 		}
 	},
 	{
-		GUIO_GAMEOPTIONS2,
+		GAMEOPTION_CHARACTER_SPEECH,
 		{
 			_s("Character Speech"),
 			_s("Enable NPC speech. Only works if speech is enabled in the Audio settings."),
@@ -54,6 +54,17 @@ static const ADExtraGuiOptionsMap optionsList[] = {
 			0
 		}
 	},
+	{
+		GAMEOPTION_AUTO_MOVE,
+		{
+			_s("Auto Move"),
+			_s("Automatically rotate the viewport when the mouse reaches an edge."),
+			"auto_move",
+			true,
+			0,
+			0
+		}
+	},
 	AD_EXTRA_GUI_OPTIONS_TERMINATOR
 };
 
diff --git a/engines/nancy/state/setupmenu.cpp b/engines/nancy/state/setupmenu.cpp
index 86c30fb2d85..2064bcfc227 100644
--- a/engines/nancy/state/setupmenu.cpp
+++ b/engines/nancy/state/setupmenu.cpp
@@ -89,6 +89,44 @@ void SetupMenu::registerGraphics() {
 	}
 }
 
+const Common::String SetupMenu::getToggleConfManKey(uint id) {
+	GameType gameType = g_nancy->getGameType();
+			
+	if (gameType == kGameTypeVampire) {
+		// Note that toggle id 1 (interlaced video) is ignored since we don't support that option
+		switch (id) {
+		case 0 :
+			return "subtitles";
+		case 2 :
+			return "player_speech";
+		case 3 :
+			return "character_speech";
+		default:
+			return "";
+		}
+	} else if (gameType <= kGameTypeNancy5) {
+		switch (id) {
+		case 0 :
+			return "subtitles";
+		case 1 :
+			return "player_speech";
+		case 2 :
+			return "character_speech";
+		default:
+			return "";
+		}
+	} else {
+		switch (id) {
+		case 0 :
+			return "subtitles";
+		case 1 :
+			return "auto_move";
+		default:
+			return "";
+		}
+	}
+}
+
 void SetupMenu::init() {
 	_setupData = (const SET*)g_nancy->getEngineData("SET");
 	assert(_setupData);
@@ -123,14 +161,9 @@ void SetupMenu::init() {
 	}
 
 	// Set toggle visibility
-	bool isVampire = g_nancy->getGameType() == kGameTypeVampire;
-	if (isVampire) {
-		// Interlaced video, currently useless
-		_toggles[1]->setState(false);
+	for (uint i = 0; i < _toggles.size(); ++i) {
+		_toggles[i]->setState(ConfMan.getBool(getToggleConfManKey(i), ConfMan.getActiveDomainName()));
 	}
-	_toggles[0]->setState(ConfMan.getBool("subtitles"));
-	_toggles[isVampire ? 2 : 1]->setState(ConfMan.getBool("player_speech"));
-	_toggles[isVampire ? 3 : 2]->setState(ConfMan.getBool("character_speech"));
 
 	for (uint i = 0; i < _setupData->_scrollbarSrcs.size(); ++i) {
 		_scrollbars.push_back(new UI::Scrollbar(7, _setupData->_scrollbarSrcs[i],
@@ -192,28 +225,10 @@ void SetupMenu::run() {
 		auto *tog = _toggles[i];
 		tog->handleInput(input);
 		if (tog->_stateChanged) {
-			bool isVampire = g_nancy->getGameType() == kGameTypeVampire;
-			uint toggleID = i;
-			// Make sure we ignore the interlaced video toggle
-			if (isVampire) {
-				if (i == 1) {
-					toggleID = 99;
-				} else if (i > 1) {
-					--toggleID;
-				}
-			}
-			switch (toggleID) {
-			case 0 :
-				ConfMan.setBool("subtitles", tog->_toggleState);
-				break;
-			case 1 :
-				ConfMan.setBool("player_speech", tog->_toggleState);
-				break;
-			case 2 :
-				ConfMan.setBool("character_speech", tog->_toggleState);
-				break;
-			default:
-				break;
+			Common::String key = getToggleConfManKey(i);
+			if (key.size()) {
+				// Make sure we don't write an empty string as a key in ConfMan
+				ConfMan.setBool(key, tog->_toggleState, ConfMan.getActiveDomainName());
 			}
 		}
 	}
diff --git a/engines/nancy/state/setupmenu.h b/engines/nancy/state/setupmenu.h
index 7c5bc61eae0..cedc0f147e7 100644
--- a/engines/nancy/state/setupmenu.h
+++ b/engines/nancy/state/setupmenu.h
@@ -57,6 +57,8 @@ private:
 
 	void registerGraphics();
 
+	const Common::String getToggleConfManKey(uint id);
+
 	enum State { kInit, kRun, kStop };
 
 	UI::FullScreenImage _background;
diff --git a/engines/nancy/ui/viewport.cpp b/engines/nancy/ui/viewport.cpp
index c25570328fd..ab9b4501878 100644
--- a/engines/nancy/ui/viewport.cpp
+++ b/engines/nancy/ui/viewport.cpp
@@ -31,6 +31,8 @@
 
 #include "engines/nancy/ui/viewport.h"
 
+#include "common/config-manager.h"
+
 namespace Nancy {
 namespace UI {
 
@@ -56,6 +58,8 @@ void Viewport::handleInput(NancyInput &input) {
 	Time systemTime = g_system->getMillis();
 	byte direction = 0;
 
+	_autoMove = ConfMan.getBool("auto_move", ConfMan.getActiveDomainName());
+
 	// Make cursor sticky when scrolling the viewport
 	if (	g_nancy->getGameType() != kGameTypeVampire &&
 			input.input & (NancyInput::kLeftMouseButton | NancyInput::kRightMouseButton)
@@ -132,7 +136,7 @@ void Viewport::handleInput(NancyInput &input) {
 
 		if (input.input & NancyInput::kRightMouseButton) {
 			direction |= kMoveFast;
-		} else if ((input.input & NancyInput::kLeftMouseButton) == 0) {
+		} else if ((input.input & NancyInput::kLeftMouseButton) == 0 && _autoMove == false) {
 			direction = 0;
 		}
 
diff --git a/engines/nancy/ui/viewport.h b/engines/nancy/ui/viewport.h
index 55683bee58a..9dec16f17ca 100644
--- a/engines/nancy/ui/viewport.h
+++ b/engines/nancy/ui/viewport.h
@@ -52,7 +52,8 @@ public:
 		_videoFormat(kLargeVideoFormat),
 		_stickyCursorPos(-1, -1),
 		_panningType(kPanNone),
-		_decoder(AVFDecoder::kLoadBidirectional) {}
+		_decoder(AVFDecoder::kLoadBidirectional),
+		_autoMove(false) {}
 
 	virtual ~Viewport() { _decoder.close(); _fullFrame.free(); }
 
@@ -98,6 +99,8 @@ protected:
 	Common::Rect _format1Bounds;
 	Common::Rect _format2Bounds;
 	Common::Point _stickyCursorPos;
+
+	bool _autoMove;
 };
 
 } // End of namespace UI


Commit: e35722f382f93ffa9b6ffbb12fdc410db0b6a18e
    https://github.com/scummvm/scummvm/commit/e35722f382f93ffa9b6ffbb12fdc410db0b6a18e
Author: Kaloyan Chehlarski (strahy at outlook.com)
Date: 2023-09-19T17:38:24+03:00

Commit Message:
NANCY: Improve BSUM reading

Added several missing fields in the BSUM chunk, and
used their values in relevant code. Also fixed an issue
in nancy6 which would make viewport movement slow.

Changed paths:
    engines/nancy/enginedata.cpp
    engines/nancy/enginedata.h
    engines/nancy/graphics.cpp
    engines/nancy/graphics.h


diff --git a/engines/nancy/enginedata.cpp b/engines/nancy/enginedata.cpp
index e66d52fd718..e0f21d0110f 100644
--- a/engines/nancy/enginedata.cpp
+++ b/engines/nancy/enginedata.cpp
@@ -72,15 +72,26 @@ BSUM::BSUM(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
 	readRect(s, helpButtonHighlightSrc, kGameTypeNancy2);
 	readRect(s, clockHighlightSrc, kGameTypeNancy2);
 
-	s.skip(0xE, kGameTypeVampire, kGameTypeVampire);
-	s.skip(9, kGameTypeNancy1);
+	s.skip(0x2, kGameTypeVampire, kGameTypeVampire);
+	s.syncAsByte(paletteTrans, kGameTypeVampire, kGameTypeVampire);
+	s.skip(0x2, kGameTypeVampire, kGameTypeVampire);
+	s.syncAsByte(rTrans);
+	s.syncAsByte(gTrans);
+	s.syncAsByte(bTrans);
+	s.skip(6); // Black and white
+
 	s.syncAsUint16LE(horizontalEdgesSize);
 	s.syncAsUint16LE(verticalEdgesSize);
 
-	s.skip(0x1A, kGameTypeVampire, kGameTypeVampire);
-	s.skip(0x1C, kGameTypeNancy1);
+	s.syncAsUint16LE(numFonts);
+
+	// Skip data for debug features (diagnostics, version...)
+	s.skip(0x18, kGameTypeVampire, kGameTypeVampire);
+	s.skip(0x1A, kGameTypeNancy1);
+
 	s.syncAsSint16LE(playerTimeMinuteLength);
 	s.syncAsUint16LE(buttonPressTimeDelay);
+	s.skip(4, kGameTypeNancy6);
 	s.syncAsByte(overrideMovementTimeDeltas);
 	s.syncAsSint16LE(slowMovementTimeDelta);
 	s.syncAsSint16LE(fastMovementTimeDelta);
diff --git a/engines/nancy/enginedata.h b/engines/nancy/enginedata.h
index b58727bd502..2cd85aeef11 100644
--- a/engines/nancy/enginedata.h
+++ b/engines/nancy/enginedata.h
@@ -60,9 +60,14 @@ struct BSUM : public EngineData {
 	Common::Rect helpButtonHighlightSrc;
 	Common::Rect clockHighlightSrc;
 
+	// Transparent color
+	byte paletteTrans, rTrans, gTrans, bTrans;
+
 	uint16 horizontalEdgesSize;
 	uint16 verticalEdgesSize;
 
+	uint16 numFonts;
+
 	uint16 playerTimeMinuteLength;
 	uint16 buttonPressTimeDelay;
 	byte overrideMovementTimeDeltas;
diff --git a/engines/nancy/graphics.cpp b/engines/nancy/graphics.cpp
index 73d13d41a03..2bd57f2c5a8 100644
--- a/engines/nancy/graphics.cpp
+++ b/engines/nancy/graphics.cpp
@@ -40,6 +40,18 @@ GraphicsManager::GraphicsManager() :
 	_isSuppressed(false) {}
 
 void GraphicsManager::init() {
+	const BSUM *bsum = (const BSUM *)g_nancy->getEngineData("BSUM");
+	assert(bsum);
+
+	// Extract transparent color from the boot summary
+	if (g_nancy->getGameType() == kGameTypeVampire) {
+		_transColor = bsum->paletteTrans;
+	} else {
+		_transColor = 	(bsum->rTrans << _inputPixelFormat.rShift) |
+						(bsum->gTrans << _inputPixelFormat.gShift) |
+						(bsum->bTrans << _inputPixelFormat.bShift);
+	}
+
 	initGraphics(640, 480, &_screenPixelFormat);
 	_screen.create(640, 480, _screenPixelFormat);
 	_screen.setTransparentColor(getTransColor());
@@ -120,12 +132,14 @@ void GraphicsManager::draw(bool updateScreen) {
 }
 
 void GraphicsManager::loadFonts(Common::SeekableReadStream *chunkStream) {
+	const BSUM *bsum = (const BSUM *)g_nancy->getEngineData("BSUM");
+	assert(bsum);
 	assert(chunkStream);
 
 	chunkStream->seek(0);
-	while (chunkStream->pos() < chunkStream->size() - 1) {
-		_fonts.push_back(Font());
-		_fonts.back().read(*chunkStream);
+	_fonts.resize(bsum->numFonts);
+	for (uint i = 0; i < _fonts.size(); ++i) {
+		_fonts[i].read(*chunkStream);
 	}
 
 	delete chunkStream;
@@ -359,14 +373,6 @@ const Graphics::PixelFormat &GraphicsManager::getScreenPixelFormat() {
 	return _screenPixelFormat;
 }
 
-uint GraphicsManager::getTransColor() {
-	if (g_nancy->getGameType() == kGameTypeVampire) {
-		return 1; // If this isn't correct, try picking the pixel at [0, 0] inside the palette bitmap
-	} else {
-		return _inputPixelFormat.ARGBToColor(0, 0, 255, 0);
-	}
-}
-
 void GraphicsManager::grabViewportObjects(Common::Array<RenderObject *> &inArray) {
 	// Add the viewport
 	inArray.push_back(&(RenderObject &)NancySceneState.getViewport());
diff --git a/engines/nancy/graphics.h b/engines/nancy/graphics.h
index 26b33419751..2c1d3e4630f 100644
--- a/engines/nancy/graphics.h
+++ b/engines/nancy/graphics.h
@@ -54,7 +54,7 @@ public:
 
 	const Graphics::PixelFormat &getInputPixelFormat();
 	const Graphics::PixelFormat &getScreenPixelFormat();
-	uint getTransColor();
+	uint32 getTransColor() { return _transColor; }
 
 	void grabViewportObjects(Common::Array<RenderObject *> &inArray);
 	void screenshotViewport(Graphics::ManagedSurface &inSurf);
@@ -89,6 +89,8 @@ private:
 
 	Common::List<Common::Rect> _dirtyRects;
 
+	uint32 _transColor = 0;
+
 	bool _isSuppressed;
 };
 


Commit: 73c6d2087d20537dedba54414e0978cd1a4ba205
    https://github.com/scummvm/scummvm/commit/73c6d2087d20537dedba54414e0978cd1a4ba205
Author: Kaloyan Chehlarski (strahy at outlook.com)
Date: 2023-09-19T17:38:24+03:00

Commit Message:
NANCY: Avoid immediate crash when an error is thrown

Fixed an edge case where an error being thrown during
the very first time a Scene is loaded would result in a
chain reaction that would crash ScummVM while trying to
draw invalid data on screen. Instead, the error is now
properly shown on screen before quitting.
Also made changes to the savegame screenshot code to make
sure this change does not impact second chance saves.

Changed paths:
    engines/nancy/graphics.cpp
    engines/nancy/metaengine.cpp
    engines/nancy/state/scene.cpp


diff --git a/engines/nancy/graphics.cpp b/engines/nancy/graphics.cpp
index 2bd57f2c5a8..49d17b1fde2 100644
--- a/engines/nancy/graphics.cpp
+++ b/engines/nancy/graphics.cpp
@@ -64,7 +64,7 @@ void GraphicsManager::init() {
 }
 
 void GraphicsManager::draw(bool updateScreen) {
-	if (_isSuppressed) {
+	if (_isSuppressed && updateScreen) {
 		_isSuppressed = false;
 		return;
 	}
diff --git a/engines/nancy/metaengine.cpp b/engines/nancy/metaengine.cpp
index e932904488f..890068c1909 100644
--- a/engines/nancy/metaengine.cpp
+++ b/engines/nancy/metaengine.cpp
@@ -127,6 +127,8 @@ SaveStateDescriptor NancyMetaEngine::querySaveMetaInfos(const char *target, int
 
 void NancyMetaEngine::getSavegameThumbnail(Graphics::Surface &thumb) {
 	if (Nancy::g_nancy->getState() == Nancy::NancyState::kLoadSave) {
+		// Do not screenshot the screen if we're in the engine's original menus,
+		// since that would just screenshot the menu itself
 		if (Nancy::State::Scene::hasInstance()) {
 			Graphics::ManagedSurface &screenshot = Nancy::State::Scene::instance().getLastScreenshot();
 			if (!screenshot.empty() && createThumbnail(&thumb, &screenshot)) {
@@ -135,10 +137,12 @@ void NancyMetaEngine::getSavegameThumbnail(Graphics::Surface &thumb) {
 		}
 	}
 
-	// Second Chance autosaves trigger when a scene changes, but before
-	// it is drawn, so we need to refresh the screen before we take a screenshot
-	Nancy::g_nancy->_graphicsManager->draw();
-	AdvancedMetaEngine::getSavegameThumbnail(thumb);
+	// Make sure we always trigger a screen redraw to support second chance saves
+	Graphics::ManagedSurface screenshotSurf;
+	Nancy::g_nancy->_graphicsManager->screenshotScreen(screenshotSurf);
+	if (!screenshotSurf.empty() && createThumbnail(&thumb, &screenshotSurf)) {
+		return;
+	}
 }
 
 void NancyMetaEngine::registerDefaultSettings(const Common::String &target) const {
diff --git a/engines/nancy/state/scene.cpp b/engines/nancy/state/scene.cpp
index ad9cce49e3b..57ae6efbbfa 100644
--- a/engines/nancy/state/scene.cpp
+++ b/engines/nancy/state/scene.cpp
@@ -190,7 +190,10 @@ void Scene::onStateEnter(const NancyState::NancyState prevState) {
 }
 
 bool Scene::onStateExit(const NancyState::NancyState nextState) {
-	g_nancy->_graphicsManager->screenshotScreen(_lastScreenshot);
+	if (_state == kRun) {
+		// Exiting the state outside the kRun state means we've encountered an error
+		g_nancy->_graphicsManager->screenshotScreen(_lastScreenshot);
+	}
 
 	if (nextState != NancyState::kPause) {
 		_timers.pushedPlayTime = g_nancy->getTotalPlayTime();
@@ -748,6 +751,7 @@ void Scene::load() {
 	}
 
 	clearSceneData();
+	g_nancy->_graphicsManager->suppressNextDraw();
 
 	// Scene IDs are prefixed with S inside the cif tree; e.g 100 -> S100
 	Common::String sceneName = Common::String::format("S%u", _sceneState.nextScene.sceneID);
@@ -832,7 +836,6 @@ void Scene::load() {
 	_flags.sceneCounts.getOrCreateVal(_sceneState.currentScene.sceneID)++;
 
 	g_nancy->_sound->recalculateSoundEffects();
-	g_nancy->_graphicsManager->suppressNextDraw();
 
 	_state = kStartSound;
 }


Commit: ee424665592d0ea207108dbc9536bd7697d4f1ae
    https://github.com/scummvm/scummvm/commit/ee424665592d0ea207108dbc9536bd7697d4f1ae
Author: Kaloyan Chehlarski (strahy at outlook.com)
Date: 2023-09-19T17:38:24+03:00

Commit Message:
NANCY: Print additional info alongside errors

Added an override to the errorString() method, allowing
the engine to print additional data when an error is thrown.

Changed paths:
    engines/nancy/action/actionmanager.cpp
    engines/nancy/nancy.cpp
    engines/nancy/nancy.h


diff --git a/engines/nancy/action/actionmanager.cpp b/engines/nancy/action/actionmanager.cpp
index 68e7f89a506..da5eb83510d 100644
--- a/engines/nancy/action/actionmanager.cpp
+++ b/engines/nancy/action/actionmanager.cpp
@@ -134,10 +134,9 @@ bool ActionManager::addNewActionRecord(Common::SeekableReadStream &inputData) {
 		uint singleDepSize = g_nancy->getGameType() <= kGameTypeNancy2 ? 12 : 16;
 		uint numDependencies = depsDataSize / singleDepSize;
 		if (depsDataSize % singleDepSize) {
-			error("Action record type %s has incorrect read size!\nScene S%u, AR %u, description:\n%s",
+			error("Action record type %u, %s has incorrect read size!\ndescription:\n%s",
+				newRecord->_type,
 				newRecord->getRecordTypeName().c_str(),
-				NancySceneState.getSceneInfo().sceneID,
-				_records.size(),
 				newRecord->_description.c_str());
 		}
 
diff --git a/engines/nancy/nancy.cpp b/engines/nancy/nancy.cpp
index ec007f95d1c..7902bd65f38 100644
--- a/engines/nancy/nancy.cpp
+++ b/engines/nancy/nancy.cpp
@@ -134,6 +134,29 @@ void NancyEngine::secondChance() {
 	saveGameState(secondChanceSlot, "SECOND CHANCE", true);
 }
 
+void NancyEngine::errorString(const char *buf_input, char *buf_output, int buf_output_size) {
+	if (State::Scene::hasInstance()) {
+		if (NancySceneState._state == State::Scene::kLoad) {
+			// Error while loading scene
+			snprintf(buf_output, buf_output_size, "While loading scene S%u, frame %u, action record %u:\n%s",
+				NancySceneState._sceneState.currentScene.sceneID,
+				NancySceneState._sceneState.currentScene.frameID,
+				NancySceneState._actionManager.getActionRecords().size(),
+				buf_input);
+		} else {
+			// Error while running
+			snprintf(buf_output, buf_output_size, "In current scene S%u, frame %u:\n%s",
+				NancySceneState._sceneState.currentScene.sceneID,
+				NancySceneState._sceneState.currentScene.frameID,
+				buf_input);
+		}
+	} else {
+		strncpy(buf_output, buf_input, buf_output_size);
+		if (buf_output_size > 0)
+			buf_output[buf_output_size - 1] = '\0';
+	}
+}
+
 bool NancyEngine::hasFeature(EngineFeature f) const {
 	return  (f == kSupportsReturnToLauncher) ||
 			(f == kSupportsLoadingDuringRuntime) ||
diff --git a/engines/nancy/nancy.h b/engines/nancy/nancy.h
index 9a698c09788..89dec377691 100644
--- a/engines/nancy/nancy.h
+++ b/engines/nancy/nancy.h
@@ -79,6 +79,7 @@ public:
 
 	static NancyEngine *create(GameType type, OSystem *syst, const NancyGameDescription *gd);
 
+	void errorString(const char *buf_input, char *buf_output, int buf_output_size) override;
 	bool hasFeature(EngineFeature f) const override;
 
 	Common::Error loadGameStream(Common::SeekableReadStream *stream) override;


Commit: fa522ff939563f77774523527989b99ad4485a0c
    https://github.com/scummvm/scummvm/commit/fa522ff939563f77774523527989b99ad4485a0c
Author: Kaloyan Chehlarski (strahy at outlook.com)
Date: 2023-09-19T17:38:24+03:00

Commit Message:
NANCY: Move hypertext handling outside Textbox

Added a separate class for handling hypertext (tagged
text used in conversations, etc) in preparation for the
addition of Autotext, which will also use it.

Changed paths:
  A engines/nancy/misc/hypertext.cpp
  A engines/nancy/misc/hypertext.h
    engines/nancy/action/conversation.cpp
    engines/nancy/action/miscrecords.cpp
    engines/nancy/action/overlay.h
    engines/nancy/action/soundrecords.cpp
    engines/nancy/commontypes.h
    engines/nancy/module.mk
    engines/nancy/ui/textbox.cpp
    engines/nancy/ui/textbox.h
    engines/nancy/util.cpp
    engines/nancy/util.h


diff --git a/engines/nancy/action/conversation.cpp b/engines/nancy/action/conversation.cpp
index 993230bc09f..4cc01765538 100644
--- a/engines/nancy/action/conversation.cpp
+++ b/engines/nancy/action/conversation.cpp
@@ -287,14 +287,14 @@ void ConversationSound::execute() {
 void ConversationSound::readCaptionText(Common::SeekableReadStream &stream) {
 	char *rawText = new char[1500];
 	stream.read(rawText, 1500);
-	UI::Textbox::assembleTextLine(rawText, _text, 1500);
+	assembleTextLine(rawText, _text, 1500);
 	delete[] rawText;
 }
 
 void ConversationSound::readResponseText(Common::SeekableReadStream &stream, ResponseStruct &response) {
 	char *rawText = new char[400];
 	stream.read(rawText, 400);
-	UI::Textbox::assembleTextLine(rawText, response.text, 400);
+	assembleTextLine(rawText, response.text, 400);
 	delete[] rawText;
 }
 
diff --git a/engines/nancy/action/miscrecords.cpp b/engines/nancy/action/miscrecords.cpp
index b1572eab303..483f3688d17 100644
--- a/engines/nancy/action/miscrecords.cpp
+++ b/engines/nancy/action/miscrecords.cpp
@@ -105,7 +105,7 @@ void TextBoxWrite::readData(Common::SeekableReadStream &stream) {
 	stream.read(buf, size);
 	buf[size - 1] = '\0';
 
-	UI::Textbox::assembleTextLine(buf, _text, size);
+	assembleTextLine(buf, _text, size);
 
 	delete[] buf;
 }
diff --git a/engines/nancy/action/overlay.h b/engines/nancy/action/overlay.h
index d5fe78396b9..ba20d264a2a 100644
--- a/engines/nancy/action/overlay.h
+++ b/engines/nancy/action/overlay.h
@@ -36,24 +36,6 @@ namespace Action {
 // - Overlay: nancy2 and above, supports static mode
 class Overlay : public RenderActionRecord {
 public:
-	static const byte kPlayOverlayPlain				= 1;
-	static const byte kPlayOverlayTransparent		= 2;
-
-	static const byte kPlayOverlaySceneChange		= 1;
-	static const byte kPlayOverlayNoSceneChange 	= 2;
-
-	static const byte kPlayOverlayStatic			= 1;
-	static const byte kPlayOverlayAnimated			= 2;
-
-	static const byte kPlayOverlayOnce				= 1;
-	static const byte kPlayOverlayLoop				= 2;
-
-	static const byte kPlayOverlayForward			= 1;
-	static const byte kPlayOverlayReverse			= 2;
-
-	static const byte kPlayOverlayWithHotspot		= 1;
-	static const byte kPlayOverlayNoHotspot			= 2;
-
 	Overlay(bool interruptible) : RenderActionRecord(7), _isInterruptible(interruptible) {}
 	virtual ~Overlay() { _fullSurface.free(); }
 
diff --git a/engines/nancy/action/soundrecords.cpp b/engines/nancy/action/soundrecords.cpp
index 36717fa9ffc..5043e3e6872 100644
--- a/engines/nancy/action/soundrecords.cpp
+++ b/engines/nancy/action/soundrecords.cpp
@@ -78,7 +78,7 @@ void PlayDigiSoundCC::readData(Common::SeekableReadStream &stream) {
 	if (textSize) {
 		char *strBuf = new char[textSize];
 		stream.read(strBuf, textSize);
-		UI::Textbox::assembleTextLine(strBuf, _ccText, textSize);
+		assembleTextLine(strBuf, _ccText, textSize);
 		delete[] strBuf;
 	}
 }
diff --git a/engines/nancy/commontypes.h b/engines/nancy/commontypes.h
index 660a60a22a9..ef8827b0b2a 100644
--- a/engines/nancy/commontypes.h
+++ b/engines/nancy/commontypes.h
@@ -80,6 +80,25 @@ static const byte kPlayerDuskDawn					= 2;
 static const byte kSmallVideoFormat					= 1;
 static const byte kLargeVideoFormat					= 2;
 
+// Overlay
+static const byte kPlayOverlayPlain					= 1;
+static const byte kPlayOverlayTransparent			= 2;
+
+static const byte kPlayOverlaySceneChange			= 1;
+static const byte kPlayOverlayNoSceneChange			= 2;
+
+static const byte kPlayOverlayStatic				= 1;
+static const byte kPlayOverlayAnimated				= 2;
+
+static const byte kPlayOverlayOnce					= 1;
+static const byte kPlayOverlayLoop					= 2;
+
+static const byte kPlayOverlayForward				= 1;
+static const byte kPlayOverlayReverse				= 2;
+
+static const byte kPlayOverlayWithHotspot			= 1;
+static const byte kPlayOverlayNoHotspot				= 2;
+
 enum MovementDirection : byte { kUp = 1, kDown = 2, kLeft = 4, kRight = 8, kMoveFast = 16 };
 
 // Separate namespace to remove possible clashes
diff --git a/engines/nancy/misc/hypertext.cpp b/engines/nancy/misc/hypertext.cpp
new file mode 100644
index 00000000000..498b7bd1d86
--- /dev/null
+++ b/engines/nancy/misc/hypertext.cpp
@@ -0,0 +1,241 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+#include "common/tokenizer.h"
+
+#include "engines/nancy/nancy.h"
+#include "engines/nancy/graphics.h"
+
+#include "engines/nancy/misc/hypertext.h"
+
+namespace Nancy {
+namespace Misc {
+
+struct ColorChange {
+	uint numChars;
+	byte colorID;
+};
+
+void HypertextParser::initSurfaces(uint width, uint height, const Graphics::PixelFormat &format) {
+	_fullSurface.create(width, height, format);
+	_fullSurface.setTransparentColor(g_nancy->_graphicsManager->getTransColor());
+	_textHighlightSurface.copyFrom(_fullSurface);
+}
+
+void HypertextParser::addTextLine(const Common::String &text) {
+	_textLines.push_back(text);
+	_needsTextRedraw = true;
+}
+
+void HypertextParser::drawAllText(Common::Point startOffset, uint maxWidth, uint lineHeight, uint leading, uint fontID, uint highlightFontID) {
+	using namespace Common;
+
+	_numDrawnLines = 0;
+
+	for (uint lineID = 0; lineID < _textLines.size(); ++lineID) {
+		Common::String currentLine;
+		bool hasHotspot = false;
+		Rect hotspot;
+		Common::Queue<ColorChange> colorChanges;
+		int curFontID = fontID;
+
+		// Token braces plus invalid characters that are known to appear in strings
+		Common::StringTokenizer tokenizer(_textLines[lineID], "<>\"");
+
+		Common::String curToken;
+		while(!tokenizer.empty()) {
+			curToken = tokenizer.nextToken();
+
+			if (curToken.size() <= 2) {
+				switch (curToken.firstChar()) {
+				case 'i' :
+					// CC begin
+					// fall through
+				case 'o' :
+					// CC end
+					// fall through
+				case 'e' :
+					// Telephone end
+					// Do nothing and just skip
+					continue;
+				case 'h' :
+					// Hotspot
+					if (hasHotspot) {
+						// Replace duplicate hotspot token with a newline to copy the original behavior
+						currentLine += '\n';
+					}
+					hasHotspot = true;
+					continue;
+				case 'n' :
+					// Newline
+					currentLine += '\n';
+					continue;
+				case 't' :
+					// Tab
+					currentLine += "    ";
+					continue;
+				case 'c' :
+					// Color tokens
+					// We keep the positions and colors of the color tokens in a queue
+					if (curToken.size() != 2) {
+						break;
+					}
+					
+					colorChanges.push({currentLine.size(), (byte)(curToken[1] - 48)});
+					continue;
+				case 'f' :
+					// Font token
+					// This selects a specific font ID for the current line
+					if (curToken.size() != 2) {
+						break;
+					}
+
+					curFontID = (int)Common::String(curToken[1]).asUint64();
+
+					continue;
+				}
+			}
+
+			currentLine += curToken;
+		}
+		
+		const Font *font = g_nancy->_graphicsManager->getFont(curFontID);
+		const Font *highlightFont = g_nancy->_graphicsManager->getFont(highlightFontID);
+
+		// Do word wrapping on the text, sans tokens
+		Array<Common::String> wrappedLines;
+		font->wordWrap(currentLine, maxWidth, wrappedLines, 0);
+
+		// Setup most of the hotspot
+		if (hasHotspot) {
+			hotspot.left = startOffset.x;
+			hotspot.top = startOffset.y - lineHeight + (_numDrawnLines * leading) - 1;
+			hotspot.setHeight((wrappedLines.size() * leading) - (leading - lineHeight));
+			hotspot.setWidth(0);
+		}
+
+		// Go through the wrapped lines and draw them, making sure to
+		// respect color tokens
+		uint totalCharsDrawn = 0;
+		byte colorID = 0;
+		for (Common::String &line : wrappedLines) {
+			uint horizontalOffset = 0;
+
+			// Trim whitespaces at end of wrapped lines to make counting
+			// of characters consistent. We do this manually since we _want_
+			// some whitespaces at the beginning of a line (e.g. tabs)
+			if (Common::isSpace(line.lastChar())) {
+				line.deleteLastChar();
+			}
+
+			// Set the width of the hotspot
+			if (hasHotspot) {
+				hotspot.setWidth(MAX<int16>(hotspot.width(), font->getStringWidth(line)));
+			}
+
+			while (!line.empty()) {
+				Common::String subLine;
+
+				if (colorChanges.size()) {
+					// Text contains color part
+
+					if (totalCharsDrawn >= colorChanges.front().numChars) {
+						// Token is at begginning of (what's left of) the current line
+						colorID = colorChanges.pop().colorID;
+					}
+
+					if (totalCharsDrawn < colorChanges.front().numChars && colorChanges.front().numChars < (totalCharsDrawn + line.size())) {
+						// There's a token inside the current line, so split off the part before it
+						subLine = line.substr(0, colorChanges.front().numChars - totalCharsDrawn);
+						line = line.substr(subLine.size());
+					}
+				}
+
+				// Choose whether to draw the subLine, or the full line
+				Common::String &stringToDraw = subLine.size() ? subLine : line;
+
+				// Draw the normal text
+				font->drawString(				&_fullSurface,
+												stringToDraw,
+												startOffset.x + horizontalOffset,
+												startOffset.y - font->getFontHeight() + _numDrawnLines * leading,
+												maxWidth,
+												colorID);
+
+				// Then, draw the highlight
+				if (hasHotspot) {
+					highlightFont->drawString(	&_textHighlightSurface,
+												stringToDraw,
+												startOffset.x + horizontalOffset,
+												startOffset.y - highlightFont->getFontHeight() + _numDrawnLines * leading,
+												maxWidth,
+												colorID);
+				}
+
+				if (subLine.size()) {
+					horizontalOffset += font->getStringWidth(subLine);
+					totalCharsDrawn += subLine.size();
+				} else {
+					totalCharsDrawn += line.size();
+					break;
+				}
+			}
+
+			++totalCharsDrawn; // Account for newlines, which are removed from the string when doing word wrap
+			++_numDrawnLines;
+		}
+
+		// Add the hotspot to the list
+		if (hasHotspot) {
+			_hotspots.push_back(hotspot);
+		}
+
+		// Note: disabled since it was most likely a bug, and is behavior exclusive to the textbox
+		/*
+		// Simulate a bug in the original engine where player text longer than
+		// a single line gets a double newline afterwards
+		if (wrappedLines.size() > 1 && hasHotspot) {
+			++_numLines;
+
+			if (lineID == _textLines.size() - 1) {
+				_lastResponseisMultiline = true;
+			}
+		}
+		*/
+
+		// Add a newline after every full piece of text
+		++_numDrawnLines;
+	}
+
+	_needsTextRedraw = false;
+}
+
+void HypertextParser::clear() {
+	if (_textLines.size()) {
+		_fullSurface.clear();
+		_textHighlightSurface.clear(_textHighlightSurface.getTransparentColor());
+		_textLines.clear();
+		_hotspots.clear();
+		_numDrawnLines = 0;
+	}
+}
+
+} // End of namespace Misc
+} // End of namespace Nancy
diff --git a/engines/nancy/misc/hypertext.h b/engines/nancy/misc/hypertext.h
new file mode 100644
index 00000000000..f05a5428fac
--- /dev/null
+++ b/engines/nancy/misc/hypertext.h
@@ -0,0 +1,60 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef NANCY_MISC_HYPERTEXT_H
+#define NANCY_MISC_HYPERTEXT_H
+
+#include "engines/nancy/renderobject.h"
+
+namespace Nancy {
+namespace Misc {
+
+// Base class for handling the engine's custom hypertext format
+// Used by the Textbox and by Autotext action records
+class HypertextParser {
+public:
+	HypertextParser() :
+		_numDrawnLines(0),
+		_needsTextRedraw(false) {}
+	virtual ~HypertextParser() {};
+
+protected:
+	void initSurfaces(uint width, uint height, const struct Graphics::PixelFormat &format);
+
+	void addTextLine(const Common::String &text);
+
+	void drawAllText(Common::Point startOffset, uint maxWidth, uint lineHeight, uint leading, uint fontID, uint highlightFontID);
+	virtual void clear();
+
+	Graphics::ManagedSurface _fullSurface; 			// Contains all rendered text (may be cropped)
+	Graphics::ManagedSurface _textHighlightSurface; // Same as above, but drawn with the highlight font
+
+	Common::Array<Common::String> _textLines;
+	Common::Array<Common::Rect> _hotspots;
+
+	uint16 _numDrawnLines;
+	bool _needsTextRedraw;
+};
+
+} // End of namespace Misc
+} // End of namespace Nancy
+
+#endif // NANCY_MISC_HYPERTEXT_H
diff --git a/engines/nancy/module.mk b/engines/nancy/module.mk
index 92490c618c6..32090cba661 100644
--- a/engines/nancy/module.mk
+++ b/engines/nancy/module.mk
@@ -48,6 +48,7 @@ MODULE_OBJS = \
   state/savedialog.o \
   state/scene.o \
   state/setupmenu.o \
+  misc/hypertext.o \
   misc/lightning.o \
   misc/specialeffect.o \
   commontypes.o \
diff --git a/engines/nancy/ui/textbox.cpp b/engines/nancy/ui/textbox.cpp
index b7aac9c70f1..32e82e476c0 100644
--- a/engines/nancy/ui/textbox.cpp
+++ b/engines/nancy/ui/textbox.cpp
@@ -37,13 +37,11 @@ namespace UI {
 
 Textbox::Textbox() :
 		RenderObject(6),
-		_needsTextRedraw(false),
 		_scrollbar(nullptr),
 		_scrollbarPos(0),
-		_numLines(0),
-		_lastResponseisMultiline(false),
 		_highlightRObj(7),
-		_fontIDOverride(-1) {}
+		_fontIDOverride(-1),
+		_autoClearTime(0) {}
 
 Textbox::~Textbox() {
 	delete _scrollbar;
@@ -58,9 +56,7 @@ void Textbox::init() {
 
 	moveTo(bootSummary->textboxScreenPosition);
 	_highlightRObj.moveTo(bootSummary->textboxScreenPosition);
-	_fullSurface.create(textboxData->innerBoundingBox.width(), textboxData->innerBoundingBox.height(), g_nancy->_graphicsManager->getScreenPixelFormat());
-	_textHighlightSurface.create(textboxData->innerBoundingBox.width(), textboxData->innerBoundingBox.height(), g_nancy->_graphicsManager->getScreenPixelFormat());
-	_textHighlightSurface.setTransparentColor(g_nancy->_graphicsManager->getTransColor());
+	initSurfaces(textboxData->innerBoundingBox.width(), textboxData->innerBoundingBox.height(), g_nancy->_graphicsManager->getScreenPixelFormat());
 
 	Common::Rect outerBoundingBox = _screenPosition;
 	outerBoundingBox.moveTo(0, 0);
@@ -138,231 +134,32 @@ void Textbox::handleInput(NancyInput &input) {
 }
 
 void Textbox::drawTextbox() {
-	using namespace Common;
-
 	const TBOX *textboxData = (const TBOX *)g_nancy->getEngineData("TBOX");
 	assert(textboxData);
 
-	_numLines = 0;
-
-	uint maxWidth = _fullSurface.w - textboxData->maxWidthDifference - textboxData->borderWidth - 2;
-	uint lineDist = textboxData->lineHeight + textboxData->lineHeight / 4;
-
-	for (uint lineID = 0; lineID < _textLines.size(); ++lineID) {
-		Common::String currentLine;
-		bool hasHotspot = false;
-		Rect hotspot;
-		Common::Queue<uint> colorTokens;
-		int fontID = _fontIDOverride;
-
-		// Token braces plus invalid characters that are known to appear in strings
-		Common::StringTokenizer tokenizer(_textLines[lineID], "<>\"");
-
-		Common::String curToken;
-		while(!tokenizer.empty()) {
-			curToken = tokenizer.nextToken();
-
-			if (curToken.size() <= 2) {
-				switch (curToken.firstChar()) {
-				case 'i' :
-					// CC begin
-					// fall through
-				case 'o' :
-					// CC end
-					// fall through
-				case 'e' :
-					// Telephone end
-					// Do nothing and just skip
-					continue;
-				case 'h' :
-					// Hotspot
-					if (hasHotspot) {
-						// Replace duplicate hotspot token with a newline to copy the original behavior
-						currentLine += '\n';
-					}
-					hasHotspot = true;
-					continue;
-				case 'n' :
-					// Newline
-					currentLine += '\n';
-					continue;
-				case 't' :
-					// Tab
-					currentLine += "    ";
-					continue;
-				case 'c' :
-					// Color tokens
-					// We keep the positions of the color tokens in a queue
-					if (curToken.size() != 2) {
-						break;
-					}
-
-					if (curToken[1] == '0' && colorTokens.size() % 2 == 0) {
-						// Found a color end token ("c0") without a corresponding begin ("c1"),
-						// or following another color end token. This is invalid, so we just skip it
-						// This happens in nancy4's intro, and nancy5's beginning cutscene
-						continue;
-					}
-
-					if (curToken[1] == '1' && colorTokens.size() % 2 == 1) {
-						// Found a color begin token ("c1") following another color begin token.
-						// This is invalid, so we just skip it
-						// This probably also happens somewhere
-						continue;
-					}
-					
-					colorTokens.push(currentLine.size());
-					continue;
-				case 'f' :
-					// Font token
-					// This selects a specific font ID for the current line
-					if (curToken.size() != 2) {
-						break;
-					}
-
-					fontID = (int)Common::String(curToken[1]).asUint64();
-
-					continue;
-				}
-			}
-
-			currentLine += curToken;
-		}
-		
-		const Font *font = g_nancy->_graphicsManager->getFont(fontID == -1 ? textboxData->conversationFontID : fontID);
-		const Font *highlightFont = g_nancy->_graphicsManager->getFont(textboxData->highlightConversationFontID);
-
-		// Do word wrapping on the text, sans tokens
-		Array<Common::String> wrappedLines;
-		font->wordWrap(currentLine, maxWidth, wrappedLines, 0);
-
-		// Setup most of the hotspot
-		if (hasHotspot) {
-			hotspot.left = textboxData->borderWidth;
-			hotspot.top = textboxData->firstLineOffset - textboxData->lineHeight + (_numLines * lineDist) - 1;
-			hotspot.setHeight((wrappedLines.size() * lineDist) - (lineDist - textboxData->lineHeight));
-			hotspot.setWidth(0);
-		}
-
-		// Go through the wrapped lines and draw them, making sure to
-		// respect color tokens
-		uint totalCharsDrawn = 0;
-		bool isColor = false;
-		for (Common::String &line : wrappedLines) {
-			uint horizontalOffset = 0;
-
-			// Trim whitespaces at end of wrapped lines to make counting
-			// of characters consistent. We do this manually since we _want_
-			// some whitespaces at the beginning of a line (e.g. tabs)
-			if (Common::isSpace(line.lastChar())) {
-				line.deleteLastChar();
-			}
-
-			// Set the width of the hotspot
-			if (hasHotspot) {
-				hotspot.setWidth(MAX<int16>(hotspot.width(), font->getStringWidth(line)));
-			}
-
-			while (!line.empty()) {
-				Common::String subLine;
-
-				if (colorTokens.size()) {
-					// Text contains color part
-
-					if (totalCharsDrawn >= colorTokens.front()) {
-						// Token is at begginning of (what's left of) the current line
-						uint val = colorTokens.pop();
-
-						if (!colorTokens.empty() && colorTokens.front() == val) {
-							// Two tokens with the same position just get ignored
-							colorTokens.pop();
-						} else {
-							isColor = !isColor;
-						}
-					}
-
-					if (totalCharsDrawn < colorTokens.front() && colorTokens.front() < (totalCharsDrawn + line.size())) {
-						// There's a token inside the current line, so split off the part before it
-						subLine = line.substr(0, colorTokens.front() - totalCharsDrawn);
-						line = line.substr(subLine.size());
-					}
-				}
-
-				// Choose whether to draw the subLine, or the full line
-				Common::String &stringToDraw = subLine.size() ? subLine : line;
-
-				// Draw the normal text
-				font->drawString(				&_fullSurface,
-												stringToDraw,
-												textboxData->borderWidth + horizontalOffset,
-												textboxData->firstLineOffset - font->getFontHeight() + _numLines * lineDist,
-												maxWidth,
-												isColor);
-
-				// Then, draw the highlight
-				if (hasHotspot) {
-					highlightFont->drawString(	&_textHighlightSurface,
-												stringToDraw,
-												textboxData->borderWidth + horizontalOffset,
-												textboxData->firstLineOffset - font->getFontHeight() + _numLines * lineDist,
-												maxWidth,
-												isColor);
-				}
-
-				if (subLine.size()) {
-					horizontalOffset += font->getStringWidth(subLine);
-					totalCharsDrawn += subLine.size();
-				} else {
-					totalCharsDrawn += line.size();
-					break;
-				}
-			}
-
-			++totalCharsDrawn; // Account for newlines, which are removed from the string when doing word wrap
-			++_numLines;
-		}
-
-		// Add the hotspot to the list
-		if (hasHotspot) {
-			_hotspots.push_back(hotspot);
-		}
-
-		// Simulate a bug in the original engine where player text longer than
-		// a single line gets a double newline afterwards
-		if (wrappedLines.size() > 1 && hasHotspot) {
-			++_numLines;
-
-			if (lineID == _textLines.size() - 1) {
-				_lastResponseisMultiline = true;
-			}
-		}
-
-		// Add a newline after every full piece of text
-		++_numLines;
-	}
+	HypertextParser::drawAllText(	Common::Point(textboxData->borderWidth, textboxData->firstLineOffset),				// x, y offset from surface edge
+									_fullSurface.w - textboxData->maxWidthDifference - textboxData->borderWidth - 1,	// maximum width of text
+									textboxData->lineHeight,															// expected height of text line
+									textboxData->lineHeight + textboxData->lineHeight / 4,								// leading (vertical distance between two lines' bottoms)
+									_fontIDOverride != -1 ? _fontIDOverride : textboxData->conversationFontID,			// font for basic text
+									textboxData->highlightConversationFontID);											// font for highlight text
 
 	setVisible(true);
-	_needsTextRedraw = false;
 }
 
 void Textbox::clear() {
 	if (_textLines.size()) {
-		_fullSurface.clear();
-		_textHighlightSurface.clear(_textHighlightSurface.getTransparentColor());
-		_textLines.clear();
-		_hotspots.clear();
+		HypertextParser::clear();
 		_scrollbar->resetPosition();
-		_numLines = 0;
-		_fontIDOverride = -1;
 		onScrollbarMove();
+		_fontIDOverride = -1;
 		_needsRedraw = true;
 		_autoClearTime = 0;
 	}
 }
 
 void Textbox::addTextLine(const Common::String &text, uint32 autoClearTime) {
-	_textLines.push_back(text);
-	_needsTextRedraw = true;
+	HypertextParser::addTextLine(text);
 
 	if (autoClearTime != 0) {
 		// Start a timer, after which the textbox will automatically be cleared.
@@ -371,27 +168,15 @@ void Textbox::addTextLine(const Common::String &text, uint32 autoClearTime) {
 	}
 }
 
-// A text line will often be broken up into chunks separated by nulls, use
-// this function to put it back together as a Common::String
-void Textbox::assembleTextLine(char *rawCaption, Common::String &output, uint size) {
-	for (uint i = 0; i < size; ++i) {
-		// A single line can be broken up into bits, look for them and
-		// concatenate them when we're done
-		if (rawCaption[i] != 0) {
-			Common::String newBit(rawCaption + i);
-			output += newBit;
-			i += newBit.size();
-		}
-	}
-
-	// Fix spaces at the end of the string in nancy1
-	output.trim();
+void Textbox::overrideFontID(const uint fontID) {
+	const BSUM *bootSummary = (const BSUM *)g_nancy->getEngineData("BSUM");
+	assert(bootSummary);
 
-	// Scan the text line for doubly-closed tokens; happens in some strings in The Vampire Diaries
-	uint pos = Common::String::npos;
-	while (pos = output.find(">>"), pos != Common::String::npos) {
-		output.replace(pos, 2, ">");
+	if (fontID >= bootSummary->numFonts) {
+		error("Requested invalid override font ID %u in Textbox", fontID);
 	}
+
+	_fontIDOverride = fontID;
 }
 
 void Textbox::onScrollbarMove() {
@@ -420,9 +205,9 @@ uint16 Textbox::getInnerHeight() const {
 	// These calculations are _almost_ correct, but off by a pixel sometimes
 	uint lineDist = textboxData->lineHeight + textboxData->lineHeight / 4;
 	if (g_nancy->getGameType() == kGameTypeVampire) {
-		return _numLines * lineDist + textboxData->firstLineOffset + (_lastResponseisMultiline ? - textboxData->lineHeight / 2 : 1);
+		return _numDrawnLines * lineDist + textboxData->firstLineOffset + 1;
 	} else {
-		return _numLines * lineDist + textboxData->firstLineOffset + lineDist / 2 - 1;
+		return _numDrawnLines * lineDist + textboxData->firstLineOffset + lineDist / 2 - 1;
 	}
 }
 
diff --git a/engines/nancy/ui/textbox.h b/engines/nancy/ui/textbox.h
index 5f4fcc6c3f2..fbcb0ab8f05 100644
--- a/engines/nancy/ui/textbox.h
+++ b/engines/nancy/ui/textbox.h
@@ -22,6 +22,7 @@
 #ifndef NANCY_UI_TEXTBOX_H
 #define NANCY_UI_TEXTBOX_H
 
+#include "engines/nancy/misc/hypertext.h"
 #include "engines/nancy/renderobject.h"
 
 namespace Nancy {
@@ -34,7 +35,7 @@ namespace UI {
 
 class Scrollbar;
 
-class Textbox : public Nancy::RenderObject {
+class Textbox : public Nancy::RenderObject, public Misc::HypertextParser {
 public:
 	Textbox();
 	virtual ~Textbox();
@@ -45,40 +46,21 @@ public:
 	void handleInput(NancyInput &input);
 
 	void drawTextbox();
-	void clear();
+	void clear() override;
 
 	void addTextLine(const Common::String &text, uint32 autoClearTime = 0);
-	void overrideFontID(const uint fontID) { _fontIDOverride = fontID; };
-
-	static void assembleTextLine(char *rawCaption, Common::String &output, uint size);
+	void overrideFontID(const uint fontID);
 
 private:
 	uint16 getInnerHeight() const;
 	void onScrollbarMove();
 
-	struct Response {
-		Common::String text;
-		Common::Rect hotspot;
-	};
-
-	Graphics::ManagedSurface _fullSurface;
-	Graphics::ManagedSurface _textHighlightSurface;
-
 	RenderObject _highlightRObj;
-
 	Scrollbar *_scrollbar;
 
-	Common::Array<Common::String> _textLines;
-	Common::Array<Common::Rect> _hotspots;
-
-	uint16 _numLines;
-	bool _lastResponseisMultiline;
-
-	bool _needsTextRedraw;
 	float _scrollbarPos;
 
-	uint32 _autoClearTime = 0;
-
+	uint32 _autoClearTime;
 	int _fontIDOverride;
 };
 
diff --git a/engines/nancy/util.cpp b/engines/nancy/util.cpp
index a2bd970e7eb..87db1fa9711 100644
--- a/engines/nancy/util.cpp
+++ b/engines/nancy/util.cpp
@@ -230,6 +230,29 @@ void readFilenameArray(Common::Serializer &stream, Common::Array<Common::String>
 	}
 }
 
+// A text line will often be broken up into chunks separated by nulls, use
+// this function to put it back together as a Common::String
+void assembleTextLine(char *rawCaption, Common::String &output, uint size) {
+	for (uint i = 0; i < size; ++i) {
+		// A single line can be broken up into bits, look for them and
+		// concatenate them when we're done
+		if (rawCaption[i] != 0) {
+			Common::String newBit(rawCaption + i);
+			output += newBit;
+			i += newBit.size();
+		}
+	}
+
+	// Fix spaces at the end of the string in nancy1
+	output.trim();
+
+	// Scan the text line for doubly-closed tokens; happens in some strings in The Vampire Diaries
+	uint pos = Common::String::npos;
+	while (pos = output.find(">>"), pos != Common::String::npos) {
+		output.replace(pos, 2, ">");
+	}
+}
+
 bool DeferredLoader::load(uint32 endTime) {
 	uint32 loopStartTime = g_system->getMillis();
 	uint32 loopTime = 0; // Stores the loop that took the longest time to complete
diff --git a/engines/nancy/util.h b/engines/nancy/util.h
index 5aa79ccb250..47974a16f3b 100644
--- a/engines/nancy/util.h
+++ b/engines/nancy/util.h
@@ -41,6 +41,8 @@ void readFilename(Common::Serializer &stream, Common::String &inString, Common::
 void readFilenameArray(Common::SeekableReadStream &stream, Common::Array<Common::String> &inArray, uint num);
 void readFilenameArray(Common::Serializer &stream, Common::Array<Common::String> &inArray, uint num, Common::Serializer::Version minVersion = 0, Common::Serializer::Version maxVersion = Common::Serializer::kLastVersion);
 
+void assembleTextLine(char *rawCaption, Common::String &output, uint size);
+
 // Abstract base class used for loading data that would take too much time in a single frame
 class DeferredLoader {
 public:


Commit: 3bbd8e6bb5344d8fdb7ac2b1879a6aca41a40579
    https://github.com/scummvm/scummvm/commit/3bbd8e6bb5344d8fdb7ac2b1879a6aca41a40579
Author: Kaloyan Chehlarski (strahy at outlook.com)
Date: 2023-09-19T17:38:24+03:00

Commit Message:
NANCY: Improve TBOX and FONT chunk reading

Fixed some incorrect assumptions about the data found
inside TBOX and FONT boot chunks. Made relevant changes
to HypertextParser to reflect the new data.

Changed paths:
    engines/nancy/enginedata.cpp
    engines/nancy/enginedata.h
    engines/nancy/font.cpp
    engines/nancy/font.h
    engines/nancy/misc/hypertext.cpp
    engines/nancy/misc/hypertext.h
    engines/nancy/ui/textbox.cpp


diff --git a/engines/nancy/enginedata.cpp b/engines/nancy/enginedata.cpp
index e0f21d0110f..f9f42248446 100644
--- a/engines/nancy/enginedata.cpp
+++ b/engines/nancy/enginedata.cpp
@@ -22,6 +22,7 @@
 #include "engines/nancy/enginedata.h"
 #include "engines/nancy/nancy.h"
 #include "engines/nancy/util.h"
+#include "engines/nancy/graphics.h"
 
 #include "common/serializer.h"
 
@@ -206,36 +207,49 @@ TBOX::TBOX(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
 	scrollbarDefaultPos.y = chunkStream->readUint16LE();
 	scrollbarMaxScroll = chunkStream->readUint16LE();
 
-	firstLineOffset = chunkStream->readUint16LE() + 1;
-	lineHeight = chunkStream->readUint16LE() + (isVampire ? 1 : 0);
-	borderWidth = chunkStream->readUint16LE() - 1;
-	maxWidthDifference = chunkStream->readUint16LE();
+	upOffset = chunkStream->readUint16LE() + 1;
+	downOffset = chunkStream->readUint16LE();
+	leftOffset = chunkStream->readUint16LE() - 1;
+	rightOffset = chunkStream->readUint16LE();
 
-	if (isVampire) {
-		ornamentSrcs.resize(14);
-		ornamentDests.resize(14);
+	readRectArray(*chunkStream, ornamentSrcs, 14);
+	readRectArray(*chunkStream, ornamentDests, 14);
 
-		chunkStream->seek(0x3E);
-		for (uint i = 0; i < 14; ++i) {
-			readRect(*chunkStream, ornamentSrcs[i]);
-		}
-
-		for (uint i = 0; i < 14; ++i) {
-			readRect(*chunkStream, ornamentDests[i]);
-		}
-	}
-
-	chunkStream->seek(0x1FE);
 	defaultFontID = chunkStream->readUint16LE();
+	defaultTextColor = chunkStream->readUint16LE();
 
 	if (g_nancy->getGameType() >= kGameTypeNancy2) {
-		chunkStream->skip(2);
 		conversationFontID = chunkStream->readUint16LE();
 		highlightConversationFontID = chunkStream->readUint16LE();
 	} else {
 		conversationFontID = defaultFontID;
 		highlightConversationFontID = defaultFontID;
 	}
+
+	tabWidth = chunkStream->readUint16LE();
+	pageScrollPercent = chunkStream->readUint16LE(); // Not implemented yet
+
+	Graphics::PixelFormat format = g_nancy->_graphicsManager->getInputPixelFormat();
+	if (g_nancy->getGameType() >= kGameTypeNancy2) {
+		byte r, g, b;
+		r = chunkStream->readByte();
+		g = chunkStream->readByte();
+		b = chunkStream->readByte();
+
+		textBackground =			(r << format.rShift) |
+									(g << format.gShift) |
+									(b << format.bShift);
+
+		r = chunkStream->readByte();
+		g = chunkStream->readByte();
+		b = chunkStream->readByte();
+
+		highlightTextBackground =	(r << format.rShift) |
+									(g << format.gShift) |
+									(b << format.bShift);
+	} else {
+		textBackground = highlightTextBackground = 0;
+	}
 }
 
 MAP::MAP(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
diff --git a/engines/nancy/enginedata.h b/engines/nancy/enginedata.h
index 2cd85aeef11..6b4e2b43449 100644
--- a/engines/nancy/enginedata.h
+++ b/engines/nancy/enginedata.h
@@ -140,17 +140,23 @@ struct TBOX : public EngineData {
 	Common::Point scrollbarDefaultPos;
 	uint16 scrollbarMaxScroll;
 
-	uint16 firstLineOffset;
-	uint16 lineHeight;
-	uint16 borderWidth;
-	uint16 maxWidthDifference;
+	uint16 upOffset;
+	uint16 downOffset;
+	uint16 leftOffset;
+	uint16 rightOffset;
 
 	Common::Array<Common::Rect> ornamentSrcs;
 	Common::Array<Common::Rect> ornamentDests;
 
 	uint16 defaultFontID;
+	uint16 defaultTextColor;
 	uint16 conversationFontID;
 	uint16 highlightConversationFontID;
+	uint16 tabWidth;
+	uint16 pageScrollPercent;
+
+	uint32 textBackground;
+	uint32 highlightTextBackground;
 };
 
 // Contains data about the map state. Only used in TVD and nancy1
diff --git a/engines/nancy/font.cpp b/engines/nancy/font.cpp
index 1b110e1741c..41f72b39ab4 100644
--- a/engines/nancy/font.cpp
+++ b/engines/nancy/font.cpp
@@ -37,17 +37,19 @@ void Font::read(Common::SeekableReadStream &stream) {
 
 	g_nancy->_resource->loadImage(imageName, _image);
 
-	char desc[0x20];
-	stream.read(desc, 0x20);
-	desc[0x1F] = '\0';
+	char desc[31];
+	stream.read(desc, 30);
+	desc[30] = '\0';
 	_description = desc;
-	stream.skip(8);
-	_colorCoordsOffset.x = stream.readUint16LE();
-	_colorCoordsOffset.y = stream.readUint16LE();
 
-	stream.skip(2);
+	_color0CoordsOffset.x = stream.readUint32LE();
+	_color0CoordsOffset.y = stream.readUint32LE();
+	_color1CoordsOffset.x = stream.readUint32LE();
+	_color1CoordsOffset.y = stream.readUint32LE();
+
 	_spaceWidth = stream.readUint16LE();
-	stream.skip(2);
+	_charSpace = stream.readSint16LE() - 1; // Account for the added pixel in readRect
+
 	_uppercaseOffset					= stream.readUint16LE();
 	_lowercaseOffset					= stream.readUint16LE();
 	_digitOffset						= stream.readUint16LE();
@@ -110,18 +112,20 @@ void Font::read(Common::SeekableReadStream &stream) {
 		}
 
 		_maxCharWidth = MAX<int>(cur.width(), _maxCharWidth);
-		_fontHeight = MAX<int>(cur.height(), _maxCharWidth);
+		_fontHeight = MAX<int>(cur.height(), _fontHeight);
 	}
 }
 
 int Font::getCharWidth(uint32 chr) const {
-	return getCharacterSourceRect(chr).width();
+	return getCharacterSourceRect(chr).width() + _charSpace;
 }
 
 void Font::drawChar(Graphics::Surface *dst, uint32 chr, int x, int y, uint32 color) const {
 	Common::Rect srcRect = getCharacterSourceRect(chr);
-	if (color != 0) {
-		srcRect.translate(_colorCoordsOffset.x, _colorCoordsOffset.y);
+	if (color == 0) {
+		srcRect.translate(_color0CoordsOffset.x, _color0CoordsOffset.y);
+	} else if (color == 1) {
+		srcRect.translate(_color1CoordsOffset.x, _color1CoordsOffset.y);
 	}
 
 	uint vampireAdjust = g_nancy->getGameType() == kGameTypeVampire ? 1 : 0;
diff --git a/engines/nancy/font.h b/engines/nancy/font.h
index af7dc3ef9d2..ae6aedbb6b1 100644
--- a/engines/nancy/font.h
+++ b/engines/nancy/font.h
@@ -41,7 +41,7 @@ public:
 
 	void read(Common::SeekableReadStream &stream);
 
-	int getFontHeight() const override { return _fontHeight; }
+	int getFontHeight() const override { return _fontHeight - 1; }
 	int getMaxCharWidth() const override { return _maxCharWidth; }
 	int getCharWidth(uint32 chr) const override;
 	int getKerningOffset(uint32 left, uint32 right) const override { return 1; }
@@ -57,7 +57,9 @@ private:
 	Common::Rect getCharacterSourceRect(char chr) const;
 
 	Common::String _description;
-	Common::Point _colorCoordsOffset; // Added to source rects when colored text is requested
+	Common::Point _color0CoordsOffset;
+	Common::Point _color1CoordsOffset; // Added to source rects when colored text is requested
+	int16 _charSpace;
 	uint16 _spaceWidth;
 
 	// Specific offsets into the _characterRects array
diff --git a/engines/nancy/misc/hypertext.cpp b/engines/nancy/misc/hypertext.cpp
index 498b7bd1d86..54251647a2d 100644
--- a/engines/nancy/misc/hypertext.cpp
+++ b/engines/nancy/misc/hypertext.cpp
@@ -33,10 +33,13 @@ struct ColorChange {
 	byte colorID;
 };
 
-void HypertextParser::initSurfaces(uint width, uint height, const Graphics::PixelFormat &format) {
+void HypertextParser::initSurfaces(uint width, uint height, const Graphics::PixelFormat &format, uint32 backgroundColor, uint32 highlightBackgroundColor) {
+	_backgroundColor = backgroundColor;
+	_highlightBackgroundColor = highlightBackgroundColor;
 	_fullSurface.create(width, height, format);
-	_fullSurface.setTransparentColor(g_nancy->_graphicsManager->getTransColor());
-	_textHighlightSurface.copyFrom(_fullSurface);
+	_fullSurface.clear(backgroundColor);
+	_textHighlightSurface.create(width, height, format);
+	_textHighlightSurface.clear(highlightBackgroundColor);
 }
 
 void HypertextParser::addTextLine(const Common::String &text) {
@@ -44,9 +47,13 @@ void HypertextParser::addTextLine(const Common::String &text) {
 	_needsTextRedraw = true;
 }
 
-void HypertextParser::drawAllText(Common::Point startOffset, uint maxWidth, uint lineHeight, uint leading, uint fontID, uint highlightFontID) {
+void HypertextParser::drawAllText(const Common::Rect &textBounds, uint fontID, uint highlightFontID) {
 	using namespace Common;
 
+	// Used to get tab width
+	const TBOX *tbox = (const TBOX *)g_nancy->getEngineData("TBOX");
+	assert(tbox);
+
 	_numDrawnLines = 0;
 
 	for (uint lineID = 0; lineID < _textLines.size(); ++lineID) {
@@ -89,7 +96,9 @@ void HypertextParser::drawAllText(Common::Point startOffset, uint maxWidth, uint
 					continue;
 				case 't' :
 					// Tab
-					currentLine += "    ";
+					for (uint i = 0; i < tbox->tabWidth; ++i) {
+						currentLine += " ";
+					}
 					continue;
 				case 'c' :
 					// Color tokens
@@ -121,13 +130,13 @@ void HypertextParser::drawAllText(Common::Point startOffset, uint maxWidth, uint
 
 		// Do word wrapping on the text, sans tokens
 		Array<Common::String> wrappedLines;
-		font->wordWrap(currentLine, maxWidth, wrappedLines, 0);
+		font->wordWrap(currentLine, textBounds.width(), wrappedLines, 0);
 
 		// Setup most of the hotspot
 		if (hasHotspot) {
-			hotspot.left = startOffset.x;
-			hotspot.top = startOffset.y - lineHeight + (_numDrawnLines * leading) - 1;
-			hotspot.setHeight((wrappedLines.size() * leading) - (leading - lineHeight));
+			hotspot.left = textBounds.left;
+			hotspot.top = textBounds.top + ((_numDrawnLines - 1) * font->getFontHeight()) - 1;
+			hotspot.setHeight(wrappedLines.size() * font->getFontHeight());
 			hotspot.setWidth(0);
 		}
 
@@ -174,18 +183,18 @@ void HypertextParser::drawAllText(Common::Point startOffset, uint maxWidth, uint
 				// Draw the normal text
 				font->drawString(				&_fullSurface,
 												stringToDraw,
-												startOffset.x + horizontalOffset,
-												startOffset.y - font->getFontHeight() + _numDrawnLines * leading,
-												maxWidth,
+												textBounds.left + horizontalOffset,
+												textBounds.top + (_numDrawnLines - 1) * font->getFontHeight(),
+												textBounds.width(),
 												colorID);
 
 				// Then, draw the highlight
 				if (hasHotspot) {
 					highlightFont->drawString(	&_textHighlightSurface,
 												stringToDraw,
-												startOffset.x + horizontalOffset,
-												startOffset.y - highlightFont->getFontHeight() + _numDrawnLines * leading,
-												maxWidth,
+												textBounds.left + horizontalOffset,
+												textBounds.top + (_numDrawnLines - 1) * highlightFont->getFontHeight(),
+												textBounds.width(),
 												colorID);
 				}
 
@@ -200,6 +209,9 @@ void HypertextParser::drawAllText(Common::Point startOffset, uint maxWidth, uint
 
 			++totalCharsDrawn; // Account for newlines, which are removed from the string when doing word wrap
 			++_numDrawnLines;
+
+			// Record the height of the text currently drawn. Used for textbox scrolling
+			_drawnTextHeight = (_numDrawnLines - 1) * font->getFontHeight();
 		}
 
 		// Add the hotspot to the list
@@ -229,11 +241,12 @@ void HypertextParser::drawAllText(Common::Point startOffset, uint maxWidth, uint
 
 void HypertextParser::clear() {
 	if (_textLines.size()) {
-		_fullSurface.clear();
-		_textHighlightSurface.clear(_textHighlightSurface.getTransparentColor());
+		_fullSurface.clear(_backgroundColor);
+		_textHighlightSurface.clear(_highlightBackgroundColor);
 		_textLines.clear();
 		_hotspots.clear();
 		_numDrawnLines = 0;
+		_drawnTextHeight = 0;
 	}
 }
 
diff --git a/engines/nancy/misc/hypertext.h b/engines/nancy/misc/hypertext.h
index f05a5428fac..d54ed04509d 100644
--- a/engines/nancy/misc/hypertext.h
+++ b/engines/nancy/misc/hypertext.h
@@ -33,24 +33,29 @@ class HypertextParser {
 public:
 	HypertextParser() :
 		_numDrawnLines(0),
+		_drawnTextHeight(0),
 		_needsTextRedraw(false) {}
 	virtual ~HypertextParser() {};
 
 protected:
-	void initSurfaces(uint width, uint height, const struct Graphics::PixelFormat &format);
+	void initSurfaces(uint width, uint height, const struct Graphics::PixelFormat &format, uint32 backgroundColor, uint32 highlightBackgroundColor);
 
 	void addTextLine(const Common::String &text);
 
-	void drawAllText(Common::Point startOffset, uint maxWidth, uint lineHeight, uint leading, uint fontID, uint highlightFontID);
+	void drawAllText(const Common::Rect &textBounds, uint fontID, uint highlightFontID);
 	virtual void clear();
 
 	Graphics::ManagedSurface _fullSurface; 			// Contains all rendered text (may be cropped)
 	Graphics::ManagedSurface _textHighlightSurface; // Same as above, but drawn with the highlight font
 
+	uint32 _backgroundColor;
+	uint32 _highlightBackgroundColor;
+
 	Common::Array<Common::String> _textLines;
 	Common::Array<Common::Rect> _hotspots;
 
 	uint16 _numDrawnLines;
+	uint16 _drawnTextHeight;
 	bool _needsTextRedraw;
 };
 
diff --git a/engines/nancy/ui/textbox.cpp b/engines/nancy/ui/textbox.cpp
index 32e82e476c0..5d0b69396c5 100644
--- a/engines/nancy/ui/textbox.cpp
+++ b/engines/nancy/ui/textbox.cpp
@@ -48,15 +48,16 @@ Textbox::~Textbox() {
 }
 
 void Textbox::init() {
-	const BSUM *bootSummary = (const BSUM *)g_nancy->getEngineData("BSUM");
-	assert(bootSummary);
+	const BSUM *bsum = (const BSUM *)g_nancy->getEngineData("BSUM");
+	assert(bsum);
 
-	const TBOX *textboxData = (const TBOX *)g_nancy->getEngineData("TBOX");
-	assert(textboxData);
+	const TBOX *tbox = (const TBOX *)g_nancy->getEngineData("TBOX");
+	assert(tbox);
 
-	moveTo(bootSummary->textboxScreenPosition);
-	_highlightRObj.moveTo(bootSummary->textboxScreenPosition);
-	initSurfaces(textboxData->innerBoundingBox.width(), textboxData->innerBoundingBox.height(), g_nancy->_graphicsManager->getScreenPixelFormat());
+	moveTo(bsum->textboxScreenPosition);
+	_highlightRObj.moveTo(bsum->textboxScreenPosition);
+	initSurfaces(tbox->innerBoundingBox.width(), tbox->innerBoundingBox.height(), g_nancy->_graphicsManager->getScreenPixelFormat(),
+		tbox->textBackground, tbox->highlightTextBackground);
 
 	Common::Rect outerBoundingBox = _screenPosition;
 	outerBoundingBox.moveTo(0, 0);
@@ -66,9 +67,9 @@ void Textbox::init() {
 
 	// zOrder bumped by 2 to avoid overlap with the inventory box curtains in The Vampire Diaries
 	_scrollbar = new Scrollbar(	11,
-								textboxData->scrollbarSrcBounds,
-								textboxData->scrollbarDefaultPos,
-								textboxData->scrollbarMaxScroll - textboxData->scrollbarDefaultPos.y);
+								tbox->scrollbarSrcBounds,
+								tbox->scrollbarDefaultPos,
+								tbox->scrollbarMaxScroll - tbox->scrollbarDefaultPos.y);
 	_scrollbar->init();
 }
 
@@ -134,15 +135,18 @@ void Textbox::handleInput(NancyInput &input) {
 }
 
 void Textbox::drawTextbox() {
-	const TBOX *textboxData = (const TBOX *)g_nancy->getEngineData("TBOX");
-	assert(textboxData);
+	const TBOX *tbox = (const TBOX *)g_nancy->getEngineData("TBOX");
+	assert(tbox);
 
-	HypertextParser::drawAllText(	Common::Point(textboxData->borderWidth, textboxData->firstLineOffset),				// x, y offset from surface edge
-									_fullSurface.w - textboxData->maxWidthDifference - textboxData->borderWidth - 1,	// maximum width of text
-									textboxData->lineHeight,															// expected height of text line
-									textboxData->lineHeight + textboxData->lineHeight / 4,								// leading (vertical distance between two lines' bottoms)
-									_fontIDOverride != -1 ? _fontIDOverride : textboxData->conversationFontID,			// font for basic text
-									textboxData->highlightConversationFontID);											// font for highlight text
+	Common::Rect textBounds = _fullSurface.getBounds();
+	textBounds.top += tbox->upOffset;
+	textBounds.bottom -= tbox->downOffset;
+	textBounds.left += tbox->leftOffset;
+	textBounds.right -= tbox->rightOffset;
+
+	HypertextParser::drawAllText(	textBounds,															// bounds of text within full surface
+									_fontIDOverride != -1 ? _fontIDOverride : tbox->conversationFontID,	// font for basic text
+									tbox->highlightConversationFontID);									// font for highlight text
 
 	setVisible(true);
 }
@@ -169,10 +173,10 @@ void Textbox::addTextLine(const Common::String &text, uint32 autoClearTime) {
 }
 
 void Textbox::overrideFontID(const uint fontID) {
-	const BSUM *bootSummary = (const BSUM *)g_nancy->getEngineData("BSUM");
-	assert(bootSummary);
+	const BSUM *bsum = (const BSUM *)g_nancy->getEngineData("BSUM");
+	assert(bsum);
 
-	if (fontID >= bootSummary->numFonts) {
+	if (fontID >= bsum->numFonts) {
 		error("Requested invalid override font ID %u in Textbox", fontID);
 	}
 
@@ -199,16 +203,10 @@ void Textbox::onScrollbarMove() {
 }
 
 uint16 Textbox::getInnerHeight() const {
-	const TBOX *textboxData = (const TBOX *)g_nancy->getEngineData("TBOX");
-	assert(textboxData);
+	const TBOX *tbox = (const TBOX *)g_nancy->getEngineData("TBOX");
+	assert(tbox);
 
-	// These calculations are _almost_ correct, but off by a pixel sometimes
-	uint lineDist = textboxData->lineHeight + textboxData->lineHeight / 4;
-	if (g_nancy->getGameType() == kGameTypeVampire) {
-		return _numDrawnLines * lineDist + textboxData->firstLineOffset + 1;
-	} else {
-		return _numDrawnLines * lineDist + textboxData->firstLineOffset + lineDist / 2 - 1;
-	}
+	return _drawnTextHeight + tbox->upOffset + tbox->downOffset;
 }
 
 } // End of namespace UI


Commit: bfc576fb8d9ac65e8450cc16ecf9e4c63e0e01ab
    https://github.com/scummvm/scummvm/commit/bfc576fb8d9ac65e8450cc16ecf9e4c63e0e01ab
Author: Kaloyan Chehlarski (strahy at outlook.com)
Date: 2023-09-19T17:38:25+03:00

Commit Message:
NANCY: Fix for some 3D sounds not playing

Added a fix for an edge case where if a sound is exactly
the minimum distance away from the listener, its volume
would be set to zero.

Changed paths:
    engines/nancy/sound.cpp


diff --git a/engines/nancy/sound.cpp b/engines/nancy/sound.cpp
index 6888ab8ffbb..441d6fcdd62 100644
--- a/engines/nancy/sound.cpp
+++ b/engines/nancy/sound.cpp
@@ -292,10 +292,6 @@ void SoundManager::playSound(uint16 channelID) {
 	Channel &chan = _channels[channelID];
 	chan.stream->seek(0);
 
-	if (chan.playCommands != 1) {
-		debugC(kDebugSound, "Unhandled playCommand type 0x%08x! Sound name: %s", chan.playCommands, chan.name.c_str());
-	}
-
 	// Init 3D sound
 	if (chan.playCommands & ~kPlaySequential && chan.effectData) {
 		uint16 playCommands = chan.playCommands;
@@ -791,9 +787,9 @@ void SoundManager::soundEffectMaintenance(uint16 channelID) {
 		}
 
 		// Attenuate sound based on distance
-		if (dist < chan.effectData->minDistance) {
+		if (dist <= chan.effectData->minDistance) {
 			volume = 255;
-		} else if (dist > chan.effectData->maxDistance) {
+		} else if (dist >= chan.effectData->maxDistance) {
 			volume = 255.0 / (2 * log2f(chan.effectData->maxDistance - chan.effectData->minDistance + 1));
 		} else {
 			float dlog = (2 * log2f(dist - chan.effectData->minDistance + 1));


Commit: c3f86160d80bce955d47c38ad5dfcd00a7da5240
    https://github.com/scummvm/scummvm/commit/c3f86160d80bce955d47c38ad5dfcd00a7da5240
Author: Kaloyan Chehlarski (strahy at outlook.com)
Date: 2023-09-19T17:38:25+03:00

Commit Message:
NANCY: Fix crash in ConversationCel

Fixed a crash due to a ConversationCel with an invalid
_lastFrame field (nancy3 scene 1000)

Changed paths:
    engines/nancy/action/conversation.cpp


diff --git a/engines/nancy/action/conversation.cpp b/engines/nancy/action/conversation.cpp
index 4cc01765538..f442be21a2f 100644
--- a/engines/nancy/action/conversation.cpp
+++ b/engines/nancy/action/conversation.cpp
@@ -639,7 +639,7 @@ void ConversationCel::registerGraphics() {
 void ConversationCel::updateGraphics() {
 	uint32 currentTime = g_nancy->getTotalPlayTime();
 
-	if (_state == kRun && currentTime > _nextFrameTime && _curFrame <= _lastFrame) {
+	if (_state == kRun && currentTime > _nextFrameTime && _curFrame < MIN<uint>(_lastFrame + 1, _celNames[0].size())) {
 		for (uint i = 0; i < _celRObjects.size(); ++i) {
 			Cel &cel = loadCel(_celNames[i][_curFrame], _treeNames[i]);
 			if (_overrideTreeRects[i] == kCelOverrideTreeRectsOn) {
@@ -722,7 +722,7 @@ void ConversationCel::readData(Common::SeekableReadStream &stream) {
 }
 
 bool ConversationCel::isVideoDonePlaying() {
-	return _curFrame >= _lastFrame && _nextFrameTime <= g_nancy->getTotalPlayTime();
+	return _curFrame >= MIN<uint>(_lastFrame, _celNames[0].size()) && _nextFrameTime <= g_nancy->getTotalPlayTime();
 }
 
 ConversationCel::Cel &ConversationCel::loadCel(const Common::String &name, const Common::String &treeName) {


Commit: b0c84deb2b7b583201ad546546dffcf23cad4dd2
    https://github.com/scummvm/scummvm/commit/b0c84deb2b7b583201ad546546dffcf23cad4dd2
Author: Kaloyan Chehlarski (strahy at outlook.com)
Date: 2023-09-19T17:38:25+03:00

Commit Message:
NANCY: Do not exit when reaching unknown AR type

Changed the ActionRecord loading code so that reaching
an unknown/changed record type just shows a warning
and continues, instead of throwing an error and quitting.

Changed paths:
    engines/nancy/action/actionmanager.cpp
    engines/nancy/action/actionmanager.h
    engines/nancy/action/arfactory.cpp
    engines/nancy/state/scene.cpp


diff --git a/engines/nancy/action/actionmanager.cpp b/engines/nancy/action/actionmanager.cpp
index da5eb83510d..b367cd5c3f1 100644
--- a/engines/nancy/action/actionmanager.cpp
+++ b/engines/nancy/action/actionmanager.cpp
@@ -108,11 +108,27 @@ void ActionManager::handleInput(NancyInput &input) {
 	}
 }
 
-bool ActionManager::addNewActionRecord(Common::SeekableReadStream &inputData) {
+void ActionManager::addNewActionRecord(Common::SeekableReadStream &inputData) {
+	ActionRecord *newRecord = createAndLoadNewRecord(inputData);
+	if (!newRecord) {
+		inputData.seek(0x30);
+		byte ARType = inputData.readByte();
+
+		warning("Action Record type %i is unimplemented or invalid!", ARType);
+		return;
+	}
+	_records.push_back(newRecord);
+}
+
+ActionRecord *ActionManager::createAndLoadNewRecord(Common::SeekableReadStream &inputData) {
 	inputData.seek(0x30);
 	byte ARType = inputData.readByte();
 	ActionRecord *newRecord = createActionRecord(ARType);
 
+	if (!newRecord) {
+		return nullptr;
+	}
+
 	inputData.seek(0);
 	char descBuf[0x30];
 	inputData.read(descBuf, 0x30);
@@ -134,10 +150,13 @@ bool ActionManager::addNewActionRecord(Common::SeekableReadStream &inputData) {
 		uint singleDepSize = g_nancy->getGameType() <= kGameTypeNancy2 ? 12 : 16;
 		uint numDependencies = depsDataSize / singleDepSize;
 		if (depsDataSize % singleDepSize) {
-			error("Action record type %u, %s has incorrect read size!\ndescription:\n%s",
+			warning("Action record type %u, %s has incorrect read size!\ndescription:\n%s",
 				newRecord->_type,
 				newRecord->getRecordTypeName().c_str(),
 				newRecord->_description.c_str());
+
+				delete newRecord;
+				return nullptr;
 		}
 
 		if (numDependencies == 0) {
@@ -202,9 +221,7 @@ bool ActionManager::addNewActionRecord(Common::SeekableReadStream &inputData) {
 		newRecord->_isActive = true;
 	}
 
-	_records.push_back(newRecord);
-
-	return true;
+	return newRecord;
 }
 
 void ActionManager::processActionRecords() {
diff --git a/engines/nancy/action/actionmanager.h b/engines/nancy/action/actionmanager.h
index 14657d90da1..dd293560958 100644
--- a/engines/nancy/action/actionmanager.h
+++ b/engines/nancy/action/actionmanager.h
@@ -62,7 +62,7 @@ public:
 	void processActionRecords();
 	void processDependency(DependencyRecord &dep, ActionRecord &record, bool doNotCheckCursor);
 
-	bool addNewActionRecord(Common::SeekableReadStream &inputData);
+	void addNewActionRecord(Common::SeekableReadStream &inputData);
 	Common::Array<ActionRecord *> &getActionRecords() { return _records; }
 	ActionRecord *getActionRecord(uint id) { if (id < _records.size()) return _records[id]; else return nullptr;}
 	void clearActionRecords();
@@ -72,7 +72,8 @@ public:
 	void synchronize(Common::Serializer &serializer);
 
 protected:
-	virtual ActionRecord *createActionRecord(uint16 type);
+	static ActionRecord *createActionRecord(uint16 type);
+	static ActionRecord *createAndLoadNewRecord(Common::SeekableReadStream &inputData);
 
 	Common::Array<ActionRecord *> _records;
 };
diff --git a/engines/nancy/action/arfactory.cpp b/engines/nancy/action/arfactory.cpp
index 95d30433ba7..da569c6daf0 100644
--- a/engines/nancy/action/arfactory.cpp
+++ b/engines/nancy/action/arfactory.cpp
@@ -236,7 +236,6 @@ ActionRecord *ActionManager::createActionRecord(uint16 type) {
 	case 215:
 		return new MazeChasePuzzle();
 	default:
-		error("Action Record type %i is invalid!", type);
 		return nullptr;
 	}
 }
diff --git a/engines/nancy/state/scene.cpp b/engines/nancy/state/scene.cpp
index 57ae6efbbfa..ff1bfcf2b46 100644
--- a/engines/nancy/state/scene.cpp
+++ b/engines/nancy/state/scene.cpp
@@ -789,9 +789,11 @@ void Scene::load() {
 	// Search for Action Records, maximum for a scene is 30
 	Common::SeekableReadStream *actionRecordChunk = nullptr;
 
-	while (actionRecordChunk = sceneIFF.getChunkStream("ACT", _actionManager._records.size()), actionRecordChunk != nullptr) {
+	uint numRecords = 0;
+	while (actionRecordChunk = sceneIFF.getChunkStream("ACT", numRecords), actionRecordChunk != nullptr) {
 		_actionManager.addNewActionRecord(*actionRecordChunk);
 		delete actionRecordChunk;
+		++numRecords;
 	}
 
 	if (_sceneState.currentScene.paletteID == -1) {


Commit: 839486c4523b69eaad3b7d83ad38606b5b949bf9
    https://github.com/scummvm/scummvm/commit/839486c4523b69eaad3b7d83ad38606b5b949bf9
Author: Kaloyan Chehlarski (strahy at outlook.com)
Date: 2023-09-19T17:38:25+03:00

Commit Message:
NANCY: Improve scan_ar_type console command

Added an optional parameter to scan_ar_type to allow
for scanning in scenes other than the current one.

Changed paths:
    engines/nancy/console.cpp
    engines/nancy/console.h


diff --git a/engines/nancy/console.cpp b/engines/nancy/console.cpp
index 04cc94315e0..3d16ceb7e89 100644
--- a/engines/nancy/console.cpp
+++ b/engines/nancy/console.cpp
@@ -459,7 +459,21 @@ bool NancyConsole::Cmd_sceneID(int argc, const char **argv) {
 	return true;
 }
 
-void NancyConsole::recurseDependencies(const Nancy::Action::DependencyRecord &record) {
+void NancyConsole::printActionRecord(const Nancy::Action::ActionRecord *record, bool noDependencies) {
+	debugPrintf("\n%s\n\ttype: %i, %s\n\texecType: %s",
+		record->_description.c_str(),
+		record->_type,
+		record->getRecordTypeName().c_str(),
+		record->_execType == Nancy::Action::ActionRecord::kRepeating ? "kRepeating" : "kOneShot");
+
+	if (!noDependencies && record->_dependencies.children.size()) {
+		debugPrintf("\n\tDependencies:");
+
+		recursePrintDependencies(record->_dependencies);
+	}
+}
+
+void NancyConsole::recursePrintDependencies(const Nancy::Action::DependencyRecord &record) {
 	using namespace Nancy::Action;
 
 	const INV *inventoryData = (const INV *)g_nancy->getEngineData("INV");
@@ -503,7 +517,8 @@ void NancyConsole::recurseDependencies(const Nancy::Action::DependencyRecord &re
 				dep.milliseconds);
 			break;
 		case DependencyType::kElapsedPlayerTime :
-			debugPrintf("kPlayerTime, %i hours, %i minutes, %i seconds, %i milliseconds",
+			debugPrintf("kPlayerTime, player time %s %i hours, %i minutes, %i seconds, %i milliseconds",
+				dep.condition == 0 ? "greater than" : (dep.condition == 1 ? "less than" : "equals"),
 				dep.hours,
 				dep.minutes,
 				dep.seconds,
@@ -545,7 +560,7 @@ void NancyConsole::recurseDependencies(const Nancy::Action::DependencyRecord &re
 			break;
 		case DependencyType::kOpenParenthesis :
 			debugPrintf("((((((((\n");
-			recurseDependencies(dep);
+			recursePrintDependencies(dep);
 			debugPrintf("\n))))))))");
 			break;
 		case DependencyType::kRandom :
@@ -563,29 +578,68 @@ void NancyConsole::recurseDependencies(const Nancy::Action::DependencyRecord &re
 bool NancyConsole::Cmd_listActionRecords(int argc, const char **argv) {
 	using namespace Nancy::Action;
 
-	if (g_nancy->_gameFlow.curState != NancyState::kScene) {
-		debugPrintf("Not in the kScene state\n");
-		return true;
-	}
+	if (argc == 1) {
+		// Print the current scene
+		if (g_nancy->_gameFlow.curState != NancyState::kScene) {
+			debugPrintf("Not in the kScene state\n");
+			return true;
+		}
+
+		Common::Array<ActionRecord *> &records = NancySceneState.getActionManager()._records;
 
-	Common::Array<ActionRecord *> &records = NancySceneState.getActionManager()._records;
+		debugPrintf("Scene %u has %u action records:\n\n", NancySceneState.getSceneInfo().sceneID, records.size());
 
-	debugPrintf("Scene %u has %u action records:\n\n", NancySceneState.getSceneInfo().sceneID, records.size());
+		for (uint i = 0; i < records.size(); ++i) {
+			ActionRecord *rec = records[i];
+			debugPrintf("Record %u:\n", i);
+			printActionRecord(rec);
+			debugPrintf("\n\n");
+		}
+	} else if (argc == 2) {
+		// Print a different scene. We need to load all records into a temporary array and read from it
+		Common::String s = argv[1];
+
+		Common::Array<ActionRecord *> records;
+		Common::Queue<uint> unknownTypes;
+		Common::Queue<Common::String> unknownDescs;
+		Common::SeekableReadStream *chunk;
+		IFF sceneIFF("S" + s);
+		sceneIFF.load();
+
+		while (chunk = sceneIFF.getChunkStream("ACT", records.size()), chunk != nullptr) {
+			ActionRecord *rec = ActionManager::createAndLoadNewRecord(*chunk);
+			if (rec == nullptr) {
+				chunk->seek(0);
+				char descBuf[0x30];
+				chunk->read(descBuf, 0x30);
+				descBuf[0x2F] = '\0';
+				byte ARType = chunk->readByte();
+				unknownDescs.push(descBuf);
+				unknownTypes.push(ARType);
+			}
+			records.push_back(rec);
+			delete chunk;
+		}
 
-	for (ActionRecord *rec : records) {
-		debugPrintf("\n%s\n\ttype: %i, %s\n\texecType: %s",
-			rec->_description.c_str(),
-			rec->_type,
-			rec->getRecordTypeName().c_str(),
-			rec->_execType == ActionRecord::kRepeating ? "kRepeating" : "kOneShot");
+		for (uint i = 0; i < records.size(); ++i) {
+			ActionRecord *rec = records[i];
+			debugPrintf("Record %u:\n", i);
 
-		if (rec->_dependencies.children.size()) {
-			debugPrintf("\n\tDependencies:");
+			if (rec == nullptr) {
+				// For unknown record types, we want to print the typeID and description
+				debugPrintf("\nUnknown or changed type %u, description:\n%s", unknownTypes.pop(), unknownDescs.pop().c_str());
+			} else {
+				printActionRecord(rec);
+			}
 
-			recurseDependencies(rec->_dependencies);
+			debugPrintf("\n\n");
 		}
 
-		debugPrintf("\n\n");
+		for (uint i = 0; i < records.size(); ++i) {
+			delete records[i];
+		}
+	} else {
+		debugPrintf("Invalid input\n");
 	}
 
 	return true;
@@ -733,12 +787,12 @@ bool NancyConsole::Cmd_setEventFlags(int argc, const char **argv) {
 
 		if (Common::String(argv[i + 1]).compareTo("true") == 0) {
 			NancySceneState.setEventFlag(flagID, g_nancy->_true);
-			debugPrintf("Set flag %i, %s, to g_nancy->_true\n",
+			debugPrintf("Set flag %i, %s, to true\n",
 				flagID,
 				g_nancy->getStaticData().eventFlagNames[flagID].c_str());
 		} else if (Common::String(argv[i + 1]).compareTo("false") == 0) {
 			NancySceneState.setEventFlag(flagID, g_nancy->_false);
-			debugPrintf("Set flag %i, %s, to g_nancy->_false\n",
+			debugPrintf("Set flag %i, %s, to false\n",
 				flagID,
 				g_nancy->getStaticData().eventFlagNames[flagID].c_str());
 		} else {
diff --git a/engines/nancy/console.h b/engines/nancy/console.h
index f52488fb40c..d4dca79d872 100644
--- a/engines/nancy/console.h
+++ b/engines/nancy/console.h
@@ -27,6 +27,7 @@
 namespace Nancy {
 
 namespace Action {
+class ActionRecord;
 struct DependencyRecord;
 }
 
@@ -66,7 +67,8 @@ private:
 	bool Cmd_setDifficulty(int argc, const char **argv);
 	bool Cmd_soundInfo(int argc, const char **argv);
 
-	void recurseDependencies(const Nancy::Action::DependencyRecord &record);
+	void printActionRecord(const Nancy::Action::ActionRecord *record, bool noDependencies = false);
+	void recursePrintDependencies(const Nancy::Action::DependencyRecord &record);
 
 	Common::String _videoFile;
 	Common::String _imageFile;


Commit: 070e6ff2c95ad4fb73f0c21431b31eeae919c660
    https://github.com/scummvm/scummvm/commit/070e6ff2c95ad4fb73f0c21431b31eeae919c660
Author: Kaloyan Chehlarski (strahy at outlook.com)
Date: 2023-09-19T17:38:25+03:00

Commit Message:
NANCY: Fix issues in PianoPuzzle

Fixed an issue caused by the recent changes to
readRectArray(), which would cause the number of hotspots
to be invalid, and produce a crash. Also fixed the double
playing of sounds when pressing a key.

Changed paths:
    engines/nancy/action/puzzle/orderingpuzzle.cpp


diff --git a/engines/nancy/action/puzzle/orderingpuzzle.cpp b/engines/nancy/action/puzzle/orderingpuzzle.cpp
index 3bb6f021bd3..6ea404b7cb5 100644
--- a/engines/nancy/action/puzzle/orderingpuzzle.cpp
+++ b/engines/nancy/action/puzzle/orderingpuzzle.cpp
@@ -112,7 +112,6 @@ void OrderingPuzzle::readData(Common::SeekableReadStream &stream) {
 
 	readRectArray(ser, _destRects, numElements, maxNumElements);
 
-	_hotspots.resize(numElements);
 	if (isPiano) {
 		readRectArray(stream, _hotspots, numElements, maxNumElements);
 	} else {
@@ -464,17 +463,19 @@ void OrderingPuzzle::setToSecondState(uint id) {
 }
 
 void OrderingPuzzle::popUp(uint id) {
-	if (g_nancy->getGameType() == kGameTypeVampire) {
-		g_nancy->_sound->playSound("BUOK");
-	} else {
-		if (_popUpSound.name.size()) {
-			g_nancy->_sound->playSound(_popUpSound);
+	if (_itemsStayDown) {
+		// Make sure we only play the sound when the buttons don't auto-depress
+		if (g_nancy->getGameType() == kGameTypeVampire) {
+			g_nancy->_sound->playSound("BUOK");
 		} else {
-			g_nancy->_sound->playSound(_pushDownSound);
+			if (_popUpSound.name.size()) {
+				g_nancy->_sound->playSound(_popUpSound);
+			} else {
+				g_nancy->_sound->playSound(_pushDownSound);
+			}
 		}
 	}
 
-
 	_downItems[id] = false;
 	Common::Rect destRect = _destRects[id];
 	destRect.translate(-_screenPosition.left, -_screenPosition.top);


Commit: 393f686b3432c58941624bb5573cae4768ae5139
    https://github.com/scummvm/scummvm/commit/393f686b3432c58941624bb5573cae4768ae5139
Author: Kaloyan Chehlarski (strahy at outlook.com)
Date: 2023-09-19T17:38:25+03:00

Commit Message:
NANCY: Respect movie frame src rects

Added code to make use of secondary movies' source
rects, instead of just copying over the entire movie. This
fixes the movies in nancy3's seance sequence.

Changed paths:
    engines/nancy/action/secondarymovie.cpp


diff --git a/engines/nancy/action/secondarymovie.cpp b/engines/nancy/action/secondarymovie.cpp
index 1b2b97e38ad..61f0049dbe1 100644
--- a/engines/nancy/action/secondarymovie.cpp
+++ b/engines/nancy/action/secondarymovie.cpp
@@ -88,8 +88,6 @@ void PlaySecondaryMovie::init() {
 			error("Couldn't load video file %s", _videoName.c_str());
 		}
 
-		_drawSurface.create(_decoder.getWidth(), _decoder.getHeight(), g_nancy->_graphicsManager->getInputPixelFormat());
-
 		if (_paletteName.size()) {
 			GraphicsManager::loadSurfacePalette(_fullFrame, _paletteName);
 			GraphicsManager::loadSurfacePalette(_drawSurface, _paletteName);
@@ -176,7 +174,7 @@ void PlaySecondaryMovie::execute() {
 			}
 
 			GraphicsManager::copyToManaged(*_decoder.decodeNextFrame(), _fullFrame, _paletteName.size() > 0);
-			_drawSurface.create(_fullFrame, _fullFrame.getBounds());
+			_drawSurface.create(_fullFrame, _videoDescs[_curViewportFrame].srcRect);
 			moveTo(_videoDescs[descID].destRect);
 
 			_needsRedraw = true;


Commit: 5f800b8d7e784c86d685f0dff82e7ccdcc6a3105
    https://github.com/scummvm/scummvm/commit/5f800b8d7e784c86d685f0dff82e7ccdcc6a3105
Author: Kaloyan Chehlarski (strahy at outlook.com)
Date: 2023-09-19T17:38:25+03:00

Commit Message:
NANCY: Overlay fixes

Fixed behavior for the two arcade machine animations in
nancy1 scene 833. Also improved timings for all Overlays.

Changed paths:
    engines/nancy/action/overlay.cpp


diff --git a/engines/nancy/action/overlay.cpp b/engines/nancy/action/overlay.cpp
index ba24e53706a..729b966f86a 100644
--- a/engines/nancy/action/overlay.cpp
+++ b/engines/nancy/action/overlay.cpp
@@ -91,6 +91,7 @@ void Overlay::readData(Common::SeekableReadStream &stream) {
 	_sceneChange.readData(stream);
 	_flagsOnTrigger.readData(stream);
 	_sound.readNormal(stream);
+
 	uint numViewportFrames = stream.readUint16LE();
 
 	if (_overlayType == kPlayOverlayAnimated) {
@@ -118,12 +119,24 @@ void Overlay::execute() {
 	case kRun: {
 		// Check the timer to see if we need to draw the next animation frame
 		if (_overlayType == kPlayOverlayAnimated && _nextFrameTime <= _currentFrameTime) {
-			// World's worst if statement
-			if (NancySceneState.getEventFlag(_interruptCondition) ||
-				(	(((_currentFrame == _loopLastFrame) && (_playDirection == kPlayOverlayForward) && (_loop == kPlayOverlayOnce)) ||
-					((_currentFrame == _loopFirstFrame) && (_playDirection == kPlayOverlayReverse) && (_loop == kPlayOverlayOnce))) &&
-						!g_nancy->_sound->isSoundPlaying(_sound))	) {
+			bool shouldTrigger = false;
+
+			// Check for interrupt flag
+			if (NancySceneState.getEventFlag(_interruptCondition)) {
+				shouldTrigger = true;
+			}
+
+			// Wait until sound stops (if present)
+			if (!g_nancy->_sound->isSoundPlaying(_sound)) {
+				// Check if we're at the last frame
+				if ((_currentFrame == _loopLastFrame) && (_playDirection == kPlayOverlayForward) && (_loop == kPlayOverlayOnce)) {
+					shouldTrigger = true;
+				} else if ((_currentFrame == _loopFirstFrame) && (_playDirection == kPlayOverlayReverse) && (_loop == kPlayOverlayOnce)) {
+					shouldTrigger = true;
+				}
+			}
 
+			if (shouldTrigger) {
 				_state = kActionTrigger;
 			} else {
 				// Check if we've moved the viewport
@@ -150,13 +163,18 @@ void Overlay::execute() {
 					}
 				}
 
-				_nextFrameTime = _currentFrameTime + _frameTime;
+				if (_nextFrameTime == 0) {
+					_nextFrameTime = _currentFrameTime + _frameTime;
+				} else {
+					_nextFrameTime += _frameTime;
+				}
 
 				uint16 nextFrame = _currentFrame;
 
 				if (_playDirection == kPlayOverlayReverse) {
 					if (nextFrame - 1 < _loopFirstFrame) {
-						if (_loop == kPlayOverlayLoop) {
+						// We keep looping if sound is present (nancy1 only)
+						if (_loop == kPlayOverlayLoop || (_sound.name != "NO SOUND" && g_nancy->getGameType() == kGameTypeNancy1)) {
 							nextFrame = _loopLastFrame;
 						}
 					} else {
@@ -164,7 +182,7 @@ void Overlay::execute() {
 					}
 				} else {
 					if (nextFrame + 1 > _loopLastFrame) {
-						if (_loop == kPlayOverlayLoop) {
+						if (_loop == kPlayOverlayLoop || (_sound.name != "NO SOUND" && g_nancy->getGameType() == kGameTypeNancy1)) {
 							nextFrame = _loopFirstFrame;
 						}
 					} else {
@@ -237,31 +255,32 @@ Common::String Overlay::getRecordTypeName() const {
 void Overlay::setFrame(uint frame) {
 	_currentFrame = frame;
 
-	Common::Rect srcRect;
+	Common::Rect srcRect = _srcRects[frame];
 
-	// Workaround for:
-	// - the fireplace in nancy2 scene 2491, where one of the rects is invalid.
-	// - the ball thing in nancy2 scene 1562, where one of the rects is twice as tall as it should be
-	// Assumes all rects in a single animation have the same dimensions
-	srcRect = _srcRects[frame];
-	if (!srcRect.isValidRect() || srcRect.height() > _srcRects[0].height()) {
-		srcRect.setWidth(_srcRects[0].width());
-		srcRect.setHeight(_srcRects[0].height());
-	}
-
-	if (_overlayType == kPlayOverlayStatic && srcRect.isEmpty()) {
-		// In static mode the srcRect above may be empty (see "out of service" sign in nancy5 scenes 2056, 2075),
-		// in which case we need to take the rect from the bitmap struct instead. Note that this is a backup,
-		// since if we use the bitmap src rect in all cases rendering can be incorrect (see same sign in nancy5 scene 2000)
-		for (uint i = 0; i < _bitmaps.size(); ++i) {
-			if (_currentViewportFrame == _bitmaps[i].frameID) {
-				srcRect = _bitmaps[i].src;
+	if (_overlayType == kPlayOverlayAnimated) {
+		// Workaround for:
+		// - the arcade machine in nancy1 scene 833
+		// - the fireplace in nancy2 scene 2491, where one of the rects is invalid.
+		// - the ball thing in nancy2 scene 1562, where one of the rects is twice as tall as it should be
+		// Assumes all rects in a single animation have the same dimensions
+		if (!srcRect.isValidRect() || srcRect.width() != _srcRects[0].width() || srcRect.height() != _srcRects[0].height()) {
+			srcRect.setWidth(_srcRects[0].width());
+			srcRect.setHeight(_srcRects[0].height());
+		}
+	} else {
+		if (srcRect.isEmpty()) {
+			// In static mode the srcRect above may be empty (see "out of service" sign in nancy5 scenes 2056, 2075),
+			// in which case we need to take the rect from the bitmap struct instead. Note that this is a backup,
+			// since if we use the bitmap src rect in all cases rendering can be incorrect (see same sign in nancy5 scene 2000)
+			for (uint i = 0; i < _bitmaps.size(); ++i) {
+				if (_currentViewportFrame == _bitmaps[i].frameID) {
+					srcRect = _bitmaps[i].src;
+				}
 			}
 		}
 	}
 
 	_drawSurface.create(_fullSurface, srcRect);
-
 	setTransparent(_transparency == kPlayOverlayTransparent);
 
 	_needsRedraw = true;


Commit: cdb0d4aaffc400789a8c7bd675c6c252ab2b48e1
    https://github.com/scummvm/scummvm/commit/cdb0d4aaffc400789a8c7bd675c6c252ab2b48e1
Author: Kaloyan Chehlarski (strahy at outlook.com)
Date: 2023-09-19T17:38:25+03:00

Commit Message:
NANCY: Do not restart already playing sounds

If a sound is already loaded in a channel and we try to re-load it,
or play it for the second time, the sound manager just ignores
the calls and continues playing the sound as normal, rather than
restarting it.

Changed paths:
    engines/nancy/sound.cpp


diff --git a/engines/nancy/sound.cpp b/engines/nancy/sound.cpp
index 441d6fcdd62..ff9dfce5379 100644
--- a/engines/nancy/sound.cpp
+++ b/engines/nancy/sound.cpp
@@ -256,6 +256,16 @@ void SoundManager::loadSound(const SoundDescription &description, SoundEffectDes
 		return;
 	}
 
+	Channel &existing = _channels[description.channelID];
+	if (existing.stream != nullptr) {
+		// There's a channel already loaded. Check if we're trying to reload the exact same sound
+		if (	description.name == existing.name &&
+				description.numLoops == existing.numLoops &&
+				description.playCommands == existing.playCommands) {
+			return;
+		}
+	}
+
 	if (_mixer->isSoundHandleActive(_channels[description.channelID].handle)) {
 		_mixer->stopHandle(_channels[description.channelID].handle);
 	}
@@ -286,7 +296,7 @@ void SoundManager::loadSound(const SoundDescription &description, SoundEffectDes
 }
 
 void SoundManager::playSound(uint16 channelID) {
-	if (channelID >= _channels.size() || _channels[channelID].stream == nullptr)
+	if (channelID >= _channels.size() || _channels[channelID].stream == nullptr || isSoundPlaying(channelID))
 		return;
 
 	Channel &chan = _channels[channelID];


Commit: 78f82c9ab9271bf69e2b6e9fb0944605584ce8d0
    https://github.com/scummvm/scummvm/commit/78f82c9ab9271bf69e2b6e9fb0944605584ce8d0
Author: Kaloyan Chehlarski (strahy at outlook.com)
Date: 2023-09-19T17:38:25+03:00

Commit Message:
NANCY: Do not repeat "can't" sound caption

Clicking on a hotspot that triggers a "can't" sound will now
clear the textbox before pushing the caption. This makes
sure that the caption won't be displayed twice.

Changed paths:
    engines/nancy/state/scene.cpp


diff --git a/engines/nancy/state/scene.cpp b/engines/nancy/state/scene.cpp
index ff1bfcf2b46..c7991644304 100644
--- a/engines/nancy/state/scene.cpp
+++ b/engines/nancy/state/scene.cpp
@@ -329,6 +329,10 @@ void Scene::installInventorySoundOverride(byte command, const SoundDescription &
 }
 
 void Scene::playItemCantSound(int16 itemID) {
+	if (ConfMan.getBool("subtitles") && g_nancy->getGameType() >= kGameTypeNancy2) {
+		_textbox.clear();
+	}
+
 	// Improvement: nancy2 never shows the caption text, even though it exists in the data; we show it
 	const INV *inventoryData = (const INV *)g_nancy->getEngineData("INV");
 	assert(inventoryData);


Commit: 087d9c4f395bf0b6227ebac14afc0a1db6c65b84
    https://github.com/scummvm/scummvm/commit/087d9c4f395bf0b6227ebac14afc0a1db6c65b84
Author: Kaloyan Chehlarski (strahy at outlook.com)
Date: 2023-09-19T17:38:25+03:00

Commit Message:
NANCY: Correctly initialize 3D sound

Fixed an issue where 3D sounds would only get their
correct positions in space after the viewport has been moved.

Changed paths:
    engines/nancy/sound.cpp
    engines/nancy/sound.h


diff --git a/engines/nancy/sound.cpp b/engines/nancy/sound.cpp
index ff9dfce5379..9f1bfbddd1f 100644
--- a/engines/nancy/sound.cpp
+++ b/engines/nancy/sound.cpp
@@ -366,7 +366,7 @@ void SoundManager::playSound(uint16 channelID) {
 						chan.volume * 255 / 100,
 						0, DisposeAfterUse::NO);
 
-	soundEffectMaintenance(channelID);
+	soundEffectMaintenance(channelID, true);
 }
 
 void SoundManager::playSound(const SoundDescription &description) {
@@ -655,7 +655,7 @@ void SoundManager::soundEffectMaintenance() {
 	_shouldRecalculate = false;
 }
 
-void SoundManager::soundEffectMaintenance(uint16 channelID) {
+void SoundManager::soundEffectMaintenance(uint16 channelID, bool force) {
 	if (channelID >= _channels.size() || !isSoundPlaying(channelID))
 		return;
 
@@ -664,8 +664,8 @@ void SoundManager::soundEffectMaintenance(uint16 channelID) {
 
 	// Handle sound effects and 3D sound, which started being used from nancy3.
 	// The original engine used DirectSound 3D, whose effects are only approximated.
-	// In particular, there are some slight but noticeable differences in panning
-	bool hasStepped = false;
+// In particular, there are some slight but noticeable differences in panning
+	bool hasStepped = force;
 	if (g_nancy->getGameType() >= 3 && chan.effectData) {
 		uint16 playCommands = chan.playCommands;
 		SoundEffectDescription *effectData = chan.effectData;
diff --git a/engines/nancy/sound.h b/engines/nancy/sound.h
index 490a85a022b..6df8c9da3c9 100644
--- a/engines/nancy/sound.h
+++ b/engines/nancy/sound.h
@@ -145,7 +145,7 @@ protected:
 		uint32 nextRepeatTime = 0;
 	};
 
-	void soundEffectMaintenance(uint16 channelID);
+	void soundEffectMaintenance(uint16 channelID, bool force = false);
 
 	Audio::Mixer *_mixer;
 


Commit: f0d189498d1d009db5f5c5477f774773be3787b1
    https://github.com/scummvm/scummvm/commit/f0d189498d1d009db5f5c5477f774773be3787b1
Author: Kaloyan Chehlarski (strahy at outlook.com)
Date: 2023-09-19T17:38:26+03:00

Commit Message:
NANCY: Fix 3D sound pop when changing scenes

Added a short interpolation to listener position and sound
panning when changing a scene. This (somewhat) fixes the
audible pop when a loud 3D sound is playing while the
character is moving (e.g. the clock chime in nancy3).

Changed paths:
    engines/nancy/sound.cpp
    engines/nancy/sound.h


diff --git a/engines/nancy/sound.cpp b/engines/nancy/sound.cpp
index 9f1bfbddd1f..0f9ecc35e48 100644
--- a/engines/nancy/sound.cpp
+++ b/engines/nancy/sound.cpp
@@ -566,6 +566,8 @@ void SoundManager::setRate(const Common::String &chunkName, uint32 rate) {
 void SoundManager::recalculateSoundEffects() {
 	_shouldRecalculate = true;
 
+	_positionLerp = 0;
+
 	if (g_nancy->getGameType() >= kGameTypeNancy3) {
 		const Nancy::State::Scene::SceneSummary &sceneSummary = NancySceneState.getSceneSummary();
 		SceneChangeDescription &sceneInfo = NancySceneState.getSceneInfo();
@@ -648,6 +650,19 @@ SoundManager::Channel::~Channel() {
 }
 
 void SoundManager::soundEffectMaintenance() {
+	// Interpolate position and rotation when scene has changed to avoid audible chop in sound
+	if (_position != NancySceneState.getSceneSummary().listenerPosition) {
+		++_positionLerp;
+	}
+
+	if (_positionLerp != 0) {
+		++_positionLerp;
+		if (_positionLerp == 10) {
+			_position = NancySceneState.getSceneSummary().listenerPosition;
+			_positionLerp = 0;
+		}
+	}
+
 	for (uint i = 0; i < _channels.size(); ++i) {
 		soundEffectMaintenance(i);
 	}
@@ -664,7 +679,7 @@ void SoundManager::soundEffectMaintenance(uint16 channelID, bool force) {
 
 	// Handle sound effects and 3D sound, which started being used from nancy3.
 	// The original engine used DirectSound 3D, whose effects are only approximated.
-// In particular, there are some slight but noticeable differences in panning
+	// In particular, there are some slight but noticeable differences in panning
 	bool hasStepped = force;
 	if (g_nancy->getGameType() >= 3 && chan.effectData) {
 		uint16 playCommands = chan.playCommands;
@@ -779,7 +794,8 @@ void SoundManager::soundEffectMaintenance(uint16 channelID, bool force) {
 	if (g_nancy->getGameType() >= 3 && chan.effectData &&
 			(chan.playCommands & ~kPlaySequential) & (kPlaySequentialFrameAnchor | kPlayRandomPosition | kPlayMoveLinear)) {
 
-		const Math::Vector3d &listenerPos = NancySceneState.getSceneSummary().listenerPosition;
+		// Interpolate position when we've changed scenes				
+		Math::Vector3d listenerPos = Math::Vector3d::interpolate(_position, NancySceneState.getSceneSummary().listenerPosition, (float)_positionLerp / 10.0);
 		float dist = listenerPos.getDistanceTo(chan.position);
 		float volume;
 
@@ -810,6 +826,12 @@ void SoundManager::soundEffectMaintenance(uint16 channelID, bool force) {
 			pan -= pan / dlog;
 		}
 
+		// (Non-linearly) interpolate pan as well
+		if (_positionLerp) {
+			float lastPan = _mixer->getChannelBalance(chan.handle) / 127.0;
+			pan = lastPan + (pan - lastPan) * ((float)_positionLerp / 10.0);
+		}
+
 		// Doppler effect is affected by the velocities of the source and listener,
 		// as projected onto the vector between source and listener
 		Math::Vector3d listenerToSource = chan.position - listenerPos;
diff --git a/engines/nancy/sound.h b/engines/nancy/sound.h
index 6df8c9da3c9..2062d50ca8f 100644
--- a/engines/nancy/sound.h
+++ b/engines/nancy/sound.h
@@ -153,7 +153,10 @@ protected:
 	Common::HashMap<Common::String, SoundDescription> _commonSounds;
 
 	bool _shouldRecalculate;
+
 	Math::Vector3d _orientation;
+	Math::Vector3d _position;
+	uint _positionLerp = 0;
 };
 
 } // End of namespace Nancy


Commit: ed49ad110200506442641ed29bc2039e922ce4fd
    https://github.com/scummvm/scummvm/commit/ed49ad110200506442641ed29bc2039e922ce4fd
Author: Kaloyan Chehlarski (strahy at outlook.com)
Date: 2023-09-19T17:38:26+03:00

Commit Message:
NANCY: Improve textbox rendering

Single whitespaces at the beginning of a line now get
cleared (hopefully) without impacting color changes. Also,
the height at which scrolling stops is now closer to the
original engine's.

Changed paths:
    engines/nancy/misc/hypertext.cpp


diff --git a/engines/nancy/misc/hypertext.cpp b/engines/nancy/misc/hypertext.cpp
index 54251647a2d..aea8faea3ec 100644
--- a/engines/nancy/misc/hypertext.cpp
+++ b/engines/nancy/misc/hypertext.cpp
@@ -50,6 +50,9 @@ void HypertextParser::addTextLine(const Common::String &text) {
 void HypertextParser::drawAllText(const Common::Rect &textBounds, uint fontID, uint highlightFontID) {
 	using namespace Common;
 
+	const Font *font = nullptr;
+	const Font *highlightFont = nullptr;
+
 	// Used to get tab width
 	const TBOX *tbox = (const TBOX *)g_nancy->getEngineData("TBOX");
 	assert(tbox);
@@ -125,8 +128,8 @@ void HypertextParser::drawAllText(const Common::Rect &textBounds, uint fontID, u
 			currentLine += curToken;
 		}
 		
-		const Font *font = g_nancy->_graphicsManager->getFont(curFontID);
-		const Font *highlightFont = g_nancy->_graphicsManager->getFont(highlightFontID);
+		font = g_nancy->_graphicsManager->getFont(curFontID);
+		highlightFont = g_nancy->_graphicsManager->getFont(highlightFontID);
 
 		// Do word wrapping on the text, sans tokens
 		Array<Common::String> wrappedLines;
@@ -159,6 +162,7 @@ void HypertextParser::drawAllText(const Common::Rect &textBounds, uint fontID, u
 				hotspot.setWidth(MAX<int16>(hotspot.width(), font->getStringWidth(line)));
 			}
 
+			bool hasSplit = false;
 			while (!line.empty()) {
 				Common::String subLine;
 
@@ -174,12 +178,20 @@ void HypertextParser::drawAllText(const Common::Rect &textBounds, uint fontID, u
 						// There's a token inside the current line, so split off the part before it
 						subLine = line.substr(0, colorChanges.front().numChars - totalCharsDrawn);
 						line = line.substr(subLine.size());
+						hasSplit = true;
 					}
 				}
 
 				// Choose whether to draw the subLine, or the full line
 				Common::String &stringToDraw = subLine.size() ? subLine : line;
 
+				// Clear (single!) whitespace from beginning of line. We do this after
+				bool clearedSpaceAtStart = false;
+				if (!hasSplit && line.firstChar() == ' ' && line.size() > 1 && line[1] != ' ') {
+					line.deleteChar(0);
+					clearedSpaceAtStart = true;
+				}
+
 				// Draw the normal text
 				font->drawString(				&_fullSurface,
 												stringToDraw,
@@ -198,6 +210,11 @@ void HypertextParser::drawAllText(const Common::Rect &textBounds, uint fontID, u
 												colorID);
 				}
 
+				// Account for space cleared at start to make sure color calculations are correct
+				if (clearedSpaceAtStart) {
+					++totalCharsDrawn;
+				}
+
 				if (subLine.size()) {
 					horizontalOffset += font->getStringWidth(subLine);
 					totalCharsDrawn += subLine.size();
@@ -234,6 +251,12 @@ void HypertextParser::drawAllText(const Common::Rect &textBounds, uint fontID, u
 
 		// Add a newline after every full piece of text
 		++_numDrawnLines;
+		_drawnTextHeight += font->getFontHeight();
+	}
+
+	// Add a line's height at end of text to replicate original behavior 
+	if (font) {
+		_drawnTextHeight += font->getFontHeight();
 	}
 
 	_needsTextRedraw = false;


Commit: 547cc34efecf08b1e80678edb136c74369497455
    https://github.com/scummvm/scummvm/commit/547cc34efecf08b1e80678edb136c74369497455
Author: Kaloyan Chehlarski (strahy at outlook.com)
Date: 2023-09-19T17:40:49+03:00

Commit Message:
NANCY: Fix Coverity issues

Changed paths:
    engines/nancy/action/miscrecords.cpp
    engines/nancy/action/puzzle/collisionpuzzle.h
    engines/nancy/action/puzzle/mazechasepuzzle.h
    engines/nancy/action/puzzle/safedialpuzzle.h
    engines/nancy/action/puzzle/soundequalizerpuzzle.cpp
    engines/nancy/enginedata.cpp
    engines/nancy/enginedata.h
    engines/nancy/font.h
    engines/nancy/state/savedialog.h


diff --git a/engines/nancy/action/miscrecords.cpp b/engines/nancy/action/miscrecords.cpp
index 483f3688d17..be22f79df9e 100644
--- a/engines/nancy/action/miscrecords.cpp
+++ b/engines/nancy/action/miscrecords.cpp
@@ -114,6 +114,7 @@ void TextBoxWrite::execute() {
 	auto &tb = NancySceneState.getTextbox();
 	tb.clear();
 	const TBOX *textboxData = (const TBOX *)g_nancy->getEngineData("TBOX");
+	assert(textboxData);
 	tb.overrideFontID(textboxData->defaultFontID);
 	tb.addTextLine(_text);
 	tb.setVisible(true);
diff --git a/engines/nancy/action/puzzle/collisionpuzzle.h b/engines/nancy/action/puzzle/collisionpuzzle.h
index 73977d1a959..f6b24265405 100644
--- a/engines/nancy/action/puzzle/collisionpuzzle.h
+++ b/engines/nancy/action/puzzle/collisionpuzzle.h
@@ -90,15 +90,15 @@ protected:
 
 	Common::Point _gridPos;
 
-	uint16 _lineWidth;
-	uint16 _framesPerMove;
+	uint16 _lineWidth = 0;
+	uint16 _framesPerMove = 0;
 
 	SoundDescription _moveSound;
 	SoundDescription _homeSound;
 	SoundDescription _wallHitSound;
 
 	SceneChangeWithFlag _solveScene;
-	uint16 _solveSoundDelay;
+	uint16 _solveSoundDelay = 0;
 	SoundDescription _solveSound;
 
 	SceneChangeWithFlag _exitScene;
diff --git a/engines/nancy/action/puzzle/mazechasepuzzle.h b/engines/nancy/action/puzzle/mazechasepuzzle.h
index 5d632e246f8..c2af6577b12 100644
--- a/engines/nancy/action/puzzle/mazechasepuzzle.h
+++ b/engines/nancy/action/puzzle/mazechasepuzzle.h
@@ -98,14 +98,14 @@ protected:
 	Common::Rect _leftButtonDest;
 	Common::Rect _resetButtonDest;
 
-	uint16 _lineWidth;
-	uint16 _framesPerMove;
+	uint16 _lineWidth = 0;
+	uint16 _framesPerMove = 0;
 
 	SoundDescription _failSound;
 	SoundDescription _moveSound;
 
 	SceneChangeWithFlag _solveScene;
-	uint16 _solveSoundDelay;
+	uint16 _solveSoundDelay = 0;
 	SoundDescription _solveSound;
 
 	SceneChangeWithFlag _exitScene;
diff --git a/engines/nancy/action/puzzle/safedialpuzzle.h b/engines/nancy/action/puzzle/safedialpuzzle.h
index 7296191360e..825c274097c 100644
--- a/engines/nancy/action/puzzle/safedialpuzzle.h
+++ b/engines/nancy/action/puzzle/safedialpuzzle.h
@@ -66,7 +66,7 @@ protected:
 
 	Common::Array<Common::Rect> _resetDialSrcs;
 
-	uint16 _resetTurns;
+	uint16 _resetTurns = 0;
 
 	Common::Array<uint16> _correctSequence;
 
@@ -80,7 +80,7 @@ protected:
 	SoundDescription _resetSound;
 
 	SceneChangeWithFlag _solveScene;
-	uint _solveSoundDelay;
+	uint _solveSoundDelay = 0;
 	SoundDescription _solveSound;
 
 	SceneChangeWithFlag _exitScene;
diff --git a/engines/nancy/action/puzzle/soundequalizerpuzzle.cpp b/engines/nancy/action/puzzle/soundequalizerpuzzle.cpp
index 63211dc7800..54a05127ce0 100644
--- a/engines/nancy/action/puzzle/soundequalizerpuzzle.cpp
+++ b/engines/nancy/action/puzzle/soundequalizerpuzzle.cpp
@@ -72,6 +72,7 @@ void SoundEqualizerPuzzle::init() {
 	_image.setTransparentColor(_drawSurface.getTransparentColor());
 
 	const VIEW *viewportData = (const VIEW *)g_nancy->getEngineData("VIEW");
+	assert(viewportData);
 	Common::Rect vpPos = viewportData->screenPosition;
 
 	if (_puzzleState->sliderValues[0] == 255) {
diff --git a/engines/nancy/enginedata.cpp b/engines/nancy/enginedata.cpp
index f9f42248446..f9a70fbffb6 100644
--- a/engines/nancy/enginedata.cpp
+++ b/engines/nancy/enginedata.cpp
@@ -712,17 +712,21 @@ CVTX::CVTX(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
 		readFilename(*chunkStream, keyName);
 		uint16 stringSize = chunkStream->readUint16LE();
 		if (stringSize > bufSize) {
-			delete buf;
+			delete[] buf;
 			buf = new char[stringSize * 2];
 			bufSize = stringSize * 2;
 		}
 
-		chunkStream->read(buf, stringSize);
-		buf[stringSize] = '\0';
-		texts.setVal(keyName, buf);
+		if (buf) {
+			chunkStream->read(buf, stringSize);
+			buf[stringSize] = '\0';
+			texts.setVal(keyName, buf);
+		} else {
+			texts.setVal(keyName, Common::String());
+		}
 	}
 
-	delete buf;
+	delete[] buf;
 }
 
 } // End of namespace Nancy
diff --git a/engines/nancy/enginedata.h b/engines/nancy/enginedata.h
index 6b4e2b43449..76f0d67bfe9 100644
--- a/engines/nancy/enginedata.h
+++ b/engines/nancy/enginedata.h
@@ -336,10 +336,10 @@ struct CLOK : public EngineData {
 	Common::Rect staticImageSrc;
 	Common::Rect staticImageDest;
 
-	uint32 timeToKeepOpen;
-	uint16 frameTime;
+	uint32 timeToKeepOpen = 0;
+	uint16 frameTime = 0;
 
-	uint32 nancy5CountdownTime;
+	uint32 nancy5CountdownTime = 0;
 	Common::Array<Common::Rect> nancy5DaySrcs;
 	Common::Array<Common::Rect> nancy5CountdownSrcs;
 };
diff --git a/engines/nancy/font.h b/engines/nancy/font.h
index ae6aedbb6b1..7e7ac37077d 100644
--- a/engines/nancy/font.h
+++ b/engines/nancy/font.h
@@ -59,8 +59,8 @@ private:
 	Common::String _description;
 	Common::Point _color0CoordsOffset;
 	Common::Point _color1CoordsOffset; // Added to source rects when colored text is requested
-	int16 _charSpace;
-	uint16 _spaceWidth;
+	int16 _charSpace = 0;
+	uint16 _spaceWidth = 0;
 
 	// Specific offsets into the _characterRects array
 	uint16 _uppercaseOffset			= 0;
diff --git a/engines/nancy/state/savedialog.h b/engines/nancy/state/savedialog.h
index e04c8ed7da0..270e577a4cd 100644
--- a/engines/nancy/state/savedialog.h
+++ b/engines/nancy/state/savedialog.h
@@ -40,7 +40,7 @@ namespace State {
 
 class SaveDialog : public State, public Common::Singleton<SaveDialog> {
 public:
-	SaveDialog() : _state(kInit), _yesButton(nullptr), _noButton(nullptr), _cancelButton(nullptr), _selected(-1) {}
+	SaveDialog() : _state(kInit), _yesButton(nullptr), _noButton(nullptr), _cancelButton(nullptr), _selected(-1), _dialogData(nullptr) {}
 	virtual ~SaveDialog();
 
 	// State API


Commit: f9ccd48a8e5919c046901fc9b553821d9a319a68
    https://github.com/scummvm/scummvm/commit/f9ccd48a8e5919c046901fc9b553821d9a319a68
Author: Kaloyan Chehlarski (strahy at outlook.com)
Date: 2023-09-19T17:40:49+03:00

Commit Message:
NANCY: Handle Overlay input as special case

Added a workaround for a bug in nancy3 where the engine
would move to an incorrect scene, because Overlay input
wasn't being handled with priority like it was in the original
engine.

Changed paths:
    engines/nancy/action/actionmanager.cpp
    engines/nancy/action/overlay.cpp
    engines/nancy/action/overlay.h


diff --git a/engines/nancy/action/actionmanager.cpp b/engines/nancy/action/actionmanager.cpp
index b367cd5c3f1..1534cff080a 100644
--- a/engines/nancy/action/actionmanager.cpp
+++ b/engines/nancy/action/actionmanager.cpp
@@ -39,10 +39,14 @@ void ActionManager::handleInput(NancyInput &input) {
 	bool setHoverCursor = false;
 	for (auto &rec : _records) {
 		if (rec->_isActive) {
-			// Send input to all active records
+			// First, loop through all records and handle special cases.
+			// This needs to be a separate loop to handle Overlays as a special case
+			// (see note in Overlay::handleInput())
 			rec->handleInput(input);
 		}
+	}
 
+	for (auto &rec : _records) {
 		if (	rec->_isActive &&
 				rec->_hasHotspot &&
 				rec->_hotspot.isValidRect() && // Needed for nancy2 scene 1600
diff --git a/engines/nancy/action/overlay.cpp b/engines/nancy/action/overlay.cpp
index 729b966f86a..f28caddcd12 100644
--- a/engines/nancy/action/overlay.cpp
+++ b/engines/nancy/action/overlay.cpp
@@ -23,6 +23,8 @@
 #include "engines/nancy/sound.h"
 #include "engines/nancy/resource.h"
 #include "engines/nancy/util.h"
+#include "engines/nancy/input.h"
+#include "engines/nancy/cursor.h"
 
 #include "engines/nancy/action/overlay.h"
 
@@ -41,6 +43,25 @@ void Overlay::init() {
 	RenderObject::init();
 }
 
+void Overlay::handleInput(NancyInput &input) {
+	// For no apparent reason, the original engine handles Overlay input as a special case,
+	// rather than simply set the general hotspot inside the ActionRecord struct. Special cases
+	// (a.k.a puzzle types) get handled before regular ActionRecords, which means an Overlay
+	// must take precedence when handling the mouse. Thus, out ActionManager class first iterates
+	// through all records and calls their handleInput() function just to make sure this special
+	// case is handled. This fixes nancy3 scene 7081.
+	if (_enableHotspot == kPlayOverlayWithHotspot) {
+		if (NancySceneState.getViewport().convertViewportToScreen(_hotspot).contains(input.mousePos)) {
+			g_nancy->_cursorManager->setCursorType(CursorManager::kHotspot);
+
+			if (input.input & NancyInput::kLeftMouseButtonUp) {
+				_state = kActionTrigger;
+				input.eatMouseInput(); // Make sure nothing else gets triggered
+			}
+		}
+	}
+}
+
 void Overlay::readData(Common::SeekableReadStream &stream) {
 	Common::Serializer ser(&stream, nullptr);
 	ser.setVersion(g_nancy->getGameType());
@@ -155,7 +176,6 @@ void Overlay::execute() {
 
 							if (_enableHotspot == kPlayOverlayWithHotspot) {
 								_hotspot = _screenPosition;
-								_hasHotspot = true;
 							}
 
 							break;
diff --git a/engines/nancy/action/overlay.h b/engines/nancy/action/overlay.h
index ba20d264a2a..2339c64709b 100644
--- a/engines/nancy/action/overlay.h
+++ b/engines/nancy/action/overlay.h
@@ -40,6 +40,7 @@ public:
 	virtual ~Overlay() { _fullSurface.free(); }
 
 	void init() override;
+	void handleInput(NancyInput &input) override;
 
 	void readData(Common::SeekableReadStream &stream) override;
 	void execute() override;


Commit: 2d5d17e6dc4365840f85099f6ce422793981c623
    https://github.com/scummvm/scummvm/commit/2d5d17e6dc4365840f85099f6ce422793981c623
Author: Kaloyan Chehlarski (strahy at outlook.com)
Date: 2023-09-19T17:40:49+03:00

Commit Message:
NANCY: Show last frame of videos

The Nancy video decoder was a victim of an off-by-one
error that would cause videos to not play their last frame;
this has now been fixed. This change also seems to fix the
long-standing issue of looping videos showing the same
frame twice.

Changed paths:
    engines/nancy/video.cpp
    engines/nancy/video.h


diff --git a/engines/nancy/video.cpp b/engines/nancy/video.cpp
index d39714f4079..4a9113bd612 100644
--- a/engines/nancy/video.cpp
+++ b/engines/nancy/video.cpp
@@ -214,7 +214,7 @@ bool AVFDecoder::AVFVideoTrack::endOfTrack() const {
 	if (_reversed)
 		return _curFrame < 0;
 
-	return _curFrame >= (getFrameCount() - 1);
+	return _curFrame >= getFrameCount();
 }
 
 bool AVFDecoder::AVFVideoTrack::decode(byte *outBuf, uint32 frameSize, Common::ReadStream &inBuf) const {
@@ -351,7 +351,7 @@ const Graphics::Surface *AVFDecoder::AVFVideoTrack::decodeFrame(uint frameNr)  {
 }
 
 const Graphics::Surface *AVFDecoder::AVFVideoTrack::decodeNextFrame() {
-	return decodeFrame(_reversed ? _curFrame-- : _curFrame++);
+	return decodeFrame(_reversed ? --_curFrame : _curFrame++);
 }
 
 } // End of namespace Nancy
diff --git a/engines/nancy/video.h b/engines/nancy/video.h
index 93db53aa6c4..bc260e7673c 100644
--- a/engines/nancy/video.h
+++ b/engines/nancy/video.h
@@ -63,7 +63,7 @@ private:
 		uint16 getWidth() const override { return _width; }
 		uint16 getHeight() const override { return _height; }
 		Graphics::PixelFormat getPixelFormat() const override { return _pixelFormat; }
-		int getCurFrame() const override { return _curFrame; }
+		int getCurFrame() const override { return _curFrame - 1; }
 		int getFrameCount() const override { return _frameCount; }
 		bool isSeekable() const override { return true; }
 		bool seek(const Audio::Timestamp &time) override;




More information about the Scummvm-git-logs mailing list