[Scummvm-git-logs] scummvm master -> 9842c1e48da40ffa88f0023a58dac467c7b25253

bluegr noreply at scummvm.org
Thu Jul 9 19:04:57 UTC 2026


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

Summary:
b8c688ca63 NANCY: NANCY10: Add debug info to some ARs
08f0cb5d4a NANCY: NANCY10: Inventory UI popup fixes
2cfdf36d97 NANCY: Clear held item when starting new game
da1a1172d2 NANCY: NANCY10: Fix cellphone close sound after another sound starts
c429a02d2c NANCY: NANCY10: Fix target for cellphone browser homepage back button
9842c1e48d NANCY: NANCY10: Implement text underline


Commit: b8c688ca63884f693962fa69674bc59c72a124b8
    https://github.com/scummvm/scummvm/commit/b8c688ca63884f693962fa69674bc59c72a124b8
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-09T22:04:42+03:00

Commit Message:
NANCY: NANCY10: Add debug info to some ARs

Changed paths:
    engines/nancy/action/miscrecords.h


diff --git a/engines/nancy/action/miscrecords.h b/engines/nancy/action/miscrecords.h
index 3ef861c8462..8a0fd3030c2 100644
--- a/engines/nancy/action/miscrecords.h
+++ b/engines/nancy/action/miscrecords.h
@@ -155,6 +155,11 @@ public:
 	int16 _startScene = 0; // start scene id (9999 = none); also the auto-open cell phone's call target
 	int16 _endScene = 0;   // end scene id (9999 = none)
 
+	Common::String getRecordExtraInfo() const override {
+		return Common::String::format("uiButton: %d, autoOpenOrBadgeSound: %d, flagB: %d, startScene: %d, endScene: %d",
+									  _uiButton, _autoOpenOrBadgeSound, _flagB, _startScene, _endScene);
+	}
+
 protected:
 	Common::String getRecordTypeName() const override { return "ControlUIItems"; }
 };
@@ -186,6 +191,11 @@ public:
 	int16 _flag = 0;              // stored but unused by the original; reserved
 	int16 _eventFlag = 0;         // event-flag index set when the entry is opened
 
+	Common::String getRecordExtraInfo() const override {
+		return Common::String::format("Key: %s, Value: %s, Mode: %d, Extra: %d, Flag: %d, EventFlag: %d",
+			_key.c_str(), _value.c_str(), _mode, _extra, _flag, _eventFlag);
+	}
+
 protected:
 	Common::String getRecordTypeName() const override { return "AddSearchLink"; }
 };
@@ -213,6 +223,10 @@ public:
 
 	UICL::Contact _contact;
 
+	Common::String getRecordExtraInfo() const override {
+		return Common::String::format("Contact: %s", _contact.name.c_str());
+	}
+
 protected:
 	Common::String getRecordTypeName() const override { return "ChangeCellPhoneInfo"; }
 };


Commit: 08f0cb5d4ae919bc5fbd8c8a6aa26fe064594147
    https://github.com/scummvm/scummvm/commit/08f0cb5d4ae919bc5fbd8c8a6aa26fe064594147
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-09T22:04:44+03:00

Commit Message:
NANCY: NANCY10: Inventory UI popup fixes

Fix #16932

- Don't close the inventory popup when picking an item
- Drop held item when clicking, instead of picking another one
- Make it easier to drop items in the inventory popup, by increasing
  its hitbox
- Most recently dropped item should be last in the list of items

Changed paths:
    engines/nancy/enginedata.cpp
    engines/nancy/enginedata.h
    engines/nancy/state/scene.cpp
    engines/nancy/ui/inventorypopup.cpp
    engines/nancy/ui/inventorypopup.h


diff --git a/engines/nancy/enginedata.cpp b/engines/nancy/enginedata.cpp
index 4d98522ef2d..61137052c73 100644
--- a/engines/nancy/enginedata.cpp
+++ b/engines/nancy/enginedata.cpp
@@ -1090,7 +1090,11 @@ UIIV::UIIV(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
 	if (g_nancy->getGameType() >= kGameTypeNancy13)
 		readRect(*chunkStream, slotsHotspot);
 
-	chunkStream->skip(2);
+	// Two byte flags. The first controls where items added while the popup is
+	// open land in the inventory order (see appendItemsWhileOpen); the second
+	// is unused here.
+	appendItemsWhileOpen = chunkStream->readByte();
+	chunkStream->skip(1);
 
 	for (uint i = 0; i < kNumFilters; ++i) {
 		readUIButtonSlot(*chunkStream, filters[i]);
diff --git a/engines/nancy/enginedata.h b/engines/nancy/enginedata.h
index 680964e8d7b..2388d5967ae 100644
--- a/engines/nancy/enginedata.h
+++ b/engines/nancy/enginedata.h
@@ -717,6 +717,10 @@ struct UIIV : public EngineData {
 	Common::Array<Common::Rect> slotSrcRects;       // 16 entries (image coords)
 	Common::Array<Common::Rect> slotDestRects;      // 16 entries (screen coords)
 	Common::Rect slotsHotspot;                      // Nancy13+: clickable region of the item slots
+	// When nonzero, items added while the popup is open are appended to the end
+	// of the inventory order instead of being inserted at the front (so the most
+	// recently dropped item ends up last). See Scene::addItemToInventory.
+	byte appendItemsWhileOpen = 0;
 	UIButtonSlot filters[kNumFilters];              // 6 entries
 	Common::Array<Common::Rect> tabCaptionSrcRects; // 6 entries
 	Common::Rect tabCaptionDestRect;                // on-screen target
diff --git a/engines/nancy/state/scene.cpp b/engines/nancy/state/scene.cpp
index eb3b4c59d6a..ca313397375 100644
--- a/engines/nancy/state/scene.cpp
+++ b/engines/nancy/state/scene.cpp
@@ -398,6 +398,30 @@ void Scene::addItemToInventory(int16 id) {
 		if (g_nancy->getGameType() <= kGameTypeNancy9) {
 			_inventoryBox.addItem(id);
 		} else {
+			// Nancy 10+ has no always-visible inventory box; the popup renders
+			// from the shared, save-persisted order list instead. Items are
+			// inserted at the front, except that when the UIIV chunk opts in
+			// (appendItemsWhileOpen) an item added while the popup is open goes
+			// to the end. That's how the most recently dropped item ends up last.
+			bool addToBack = false;
+			if (_inventoryPopup.isOpen()) {
+				const UIIV *uiivData = GetEngineData(UIIV);
+				addToBack = uiivData && uiivData->appendItemsWhileOpen;
+			}
+
+			Common::Array<int16> &order = _inventoryBox.getOrder();
+			for (uint i = 0; i < order.size(); ++i) {
+				if (order[i] == id) {
+					order.remove_at(i);
+					break;
+				}
+			}
+			if (addToBack) {
+				order.push_back(id);
+			} else {
+				order.insert_at(0, id);
+			}
+
 			if (_inventoryPopup.isOpen()) {
 				_inventoryPopup.refreshGrid();
 			} else if (_taskbar) {
@@ -425,8 +449,18 @@ void Scene::removeItemFromInventory(int16 id, bool pickUp) {
 
 		if (g_nancy->getGameType() <= kGameTypeNancy9) {
 			_inventoryBox.removeItem(id);
-		} else if (_inventoryPopup.isOpen()) {
-			_inventoryPopup.refreshGrid();
+		} else {
+			Common::Array<int16> &order = _inventoryBox.getOrder();
+			for (uint i = 0; i < order.size(); ++i) {
+				if (order[i] == id) {
+					order.remove_at(i);
+					break;
+				}
+			}
+
+			if (_inventoryPopup.isOpen()) {
+				_inventoryPopup.refreshGrid();
+			}
 		}
 	}
 }
diff --git a/engines/nancy/ui/inventorypopup.cpp b/engines/nancy/ui/inventorypopup.cpp
index 6a6a066260a..b5568d8834f 100644
--- a/engines/nancy/ui/inventorypopup.cpp
+++ b/engines/nancy/ui/inventorypopup.cpp
@@ -87,6 +87,15 @@ void InventoryPopup::open() {
 
 	setVisible(true);
 
+	// If the player opens the popup while carrying an item, move the cursor
+	// into the toolbox so the whole popup is immediately usable as a drop
+	// zone (e.g. double-clicking the taskbar icon opens the popup and lands
+	// the cursor over it, ready to drop on the next click).
+	if (NancySceneState.getHeldItem() != -1) {
+		g_nancy->_cursor->warpCursor(Common::Point(_screenPosition.left + _screenPosition.width() / 2,
+													_screenPosition.top + _screenPosition.height() / 2));
+	}
+
 	NancySceneState.getTaskbar()->clearAllNotifications(kTaskButtonInventory);
 
 	if (!_uiivData->header.sounds[0].name.empty()) {
@@ -124,39 +133,93 @@ void InventoryPopup::refreshGrid() {
 	_needsRedraw = true;
 }
 
+bool InventoryPopup::itemMatchesFilter(int16 itemID) const {
+	const INV::ItemDescription &desc = _invData->itemDescriptions[itemID];
+
+	switch (_activeFilterIndex) {
+	case kFilterViewable:
+		return desc.keepItem == 3;
+	case kFilterPortable:
+		return desc.keepItem <= 2;
+	case kFilterAll:
+	default:
+		return true;
+	}
+}
+
+bool InventoryPopup::syncOrderWithInventory() {
+	const uint16 numItems = MIN<uint16>(g_nancy->getStaticData().numItems,
+										_invData->itemDescriptions.size());
+	const int16 heldItem = NancySceneState.getHeldItem();
+	Common::Array<int16> &order = NancySceneState.getInventoryBox().getOrder();
+
+	bool changed = false;
+
+	// Drop entries that are out of range, not owned, or being carried
+	// (`hasItem` reports the held item as owned, so exclude it explicitly).
+	for (uint i = 0; i < order.size();) {
+		const int16 id = order[i];
+		if (id < 0 || id >= (int16)numItems || id == heldItem ||
+				NancySceneState.hasItem(id) != g_nancy->_true) {
+			order.remove_at(i);
+			changed = true;
+		} else {
+			++i;
+		}
+	}
+
+	// Append any owned items missing from the list, in item-ID order.
+	for (uint16 id = 0; id < numItems; ++id) {
+		if ((int16)id == heldItem || NancySceneState.hasItem(id) != g_nancy->_true)
+			continue;
+
+		bool inOrder = false;
+		for (uint i = 0; i < order.size(); ++i) {
+			if (order[i] == (int16)id) {
+				inOrder = true;
+				break;
+			}
+		}
+		if (!inOrder) {
+			order.push_back(id);
+			changed = true;
+		}
+	}
+
+	return changed;
+}
+
 void InventoryPopup::rebuildVisibleList() {
 	_visibleItems.clear();
 
+	// Make sure the persisted order list holds exactly the items the player
+	// owns before reading from it. This heals older saves (or any desync) in
+	// one pass, instead of waiting for the player to pick up and drop each
+	// item individually.
+	syncOrderWithInventory();
+
 	const uint16 numItems = MIN<uint16>(g_nancy->getStaticData().numItems,
 										_invData->itemDescriptions.size());
 
 	const int16 heldItem = NancySceneState.getHeldItem();
 
-	for (uint16 id = 0; id < numItems; ++id) {
-		if (NancySceneState.hasItem(id) != g_nancy->_true)
+	// The grid follows the shared, save-persisted inventory order (see
+	// Scene::addItemToInventory), so the sequence in which items are picked
+	// up and dropped is preserved rather than always sorting by item ID.
+	const Common::Array<int16> &order = NancySceneState.getInventoryBox().getOrder();
+
+	for (uint i = 0; i < order.size(); ++i) {
+		const int16 id = order[i];
+		if (id < 0 || id >= (int16)numItems)
 			continue;
 
 		// `hasItem` reports the held item as owned; the player is already
 		// carrying it, so don't list it in the grid too.
-		if ((int16)id == heldItem)
+		if (id == heldItem || NancySceneState.hasItem(id) != g_nancy->_true)
 			continue;
 
-		const INV::ItemDescription &desc = _invData->itemDescriptions[id];
-
-		switch (_activeFilterIndex) {
-		case kFilterViewable:
-			if (desc.keepItem == 3)
-				_visibleItems.push_back(id);
-			break;
-		case kFilterPortable:
-			if (desc.keepItem <= 2)
-				_visibleItems.push_back(id);
-			break;
-		case kFilterAll:
-		default:
+		if (itemMatchesFilter(id))
 			_visibleItems.push_back(id);
-			break;
-		}
 	}
 }
 
@@ -396,18 +459,36 @@ void InventoryPopup::handleInput(NancyInput &input) {
 		}
 	}
 
-	// If the player is already holding an item, any click on the slot grid
-	// puts it back into the inventory. Otherwise a click on an occupied
-	// slot picks that item up (or navigates to its close-up scene).
 	const int16 heldItem = NancySceneState.getHeldItem();
 
+	// While the player is carrying an item, the whole popup acts as a single
+	// drop zone: a click anywhere inside it drops the held item back into the
+	// inventory (appended to the end of the list) and keeps the popup open, so
+	// the player never has to carefully target one of the slot squares. The
+	// close button, handled above, still closes without dropping.
+	if (heldItem != -1) {
+		if (_screenPosition.contains(input.mousePos)) {
+			// kHotspot keeps the carried-item sprite showing on Nancy 10+.
+			g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+
+			if (input.input & NancyInput::kLeftMouseButtonUp) {
+				NancySceneState.addItemToInventory(heldItem);
+			}
+			input.eatMouseInput();
+		}
+		return;
+	}
+
+	// Empty-handed: a click on an occupied slot picks that item up (or
+	// navigates to its close-up scene). Hovering an item uses the yellow
+	// hotspot arrow rather than the magnifier.
 	int hoveredSlot = -1;
 	for (uint i = 0; i < kSlotsPerPage; ++i) {
 		if (i >= _uiivData->slotDestRects.size())
 			break;
 		if (!_uiivData->slotDestRects[i].contains(chunkMouse))
 			continue;
-		if (heldItem == -1 && _slotItemIDs[i] < 0)
+		if (_slotItemIDs[i] < 0)
 			continue;
 		hoveredSlot = (int)i;
 		break;
@@ -417,51 +498,34 @@ void InventoryPopup::handleInput(NancyInput &input) {
 		g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
 
 		if (input.input & NancyInput::kLeftMouseButtonUp) {
-			if (heldItem != -1) {
-				const int16 slotItem = _slotItemIDs[hoveredSlot];
+			const int16 itemID = _slotItemIDs[hoveredSlot];
+			const INV::ItemDescription &item = _invData->itemDescriptions[itemID];
+			const byte disabled = NancySceneState.getItemDisabledState(itemID);
 
-				// Empty slot: drop the held item back into the inventory
-				// and keep the popup open. Occupied slot: swap - held
-				// item goes into the inventory, the clicked item becomes
-				// the new held item.
-				NancySceneState.addItemToInventory(heldItem);
-				if (slotItem >= 0 && slotItem != heldItem) {
-					NancySceneState.removeItemFromInventory(slotItem, true);
+			if (disabled) {
+				if (disabled == 2) {
+					NancySceneState.playItemCantSound(itemID);
 				}
-				refreshGrid();
 				input.eatMouseInput();
 				return;
 			}
 
-			const int16 itemID = _slotItemIDs[hoveredSlot];
-			if (itemID >= 0) {
-				const INV::ItemDescription &item = _invData->itemDescriptions[itemID];
-				const byte disabled = NancySceneState.getItemDisabledState(itemID);
-
-				if (disabled) {
-					if (disabled == 2) {
-						NancySceneState.playItemCantSound(itemID);
-					}
-					input.eatMouseInput();
-					return;
-				}
-
-				const bool pickUp = item.keepItem != kInvItemNewSceneView;
-				NancySceneState.removeItemFromInventory(itemID, pickUp);
-
-				if (item.keepItem == kInvItemNewSceneView) {
-					// Close-up view: stash the item and warp to its scene.
-					NancySceneState.pushScene(itemID);
-					SceneChangeDescription sceneChange;
-					sceneChange.sceneID = item.sceneID;
-					sceneChange.continueSceneSound = item.sceneSoundFlag;
-					NancySceneState.changeScene(sceneChange);
-				}
-
+			const bool pickUp = item.keepItem != kInvItemNewSceneView;
+			NancySceneState.removeItemFromInventory(itemID, pickUp);
+
+			if (item.keepItem == kInvItemNewSceneView) {
+				// Close-up view: stash the item and warp to its scene, which
+				// dismisses the popup. A normal pickup keeps the popup open.
+				NancySceneState.pushScene(itemID);
+				SceneChangeDescription sceneChange;
+				sceneChange.sceneID = item.sceneID;
+				sceneChange.continueSceneSound = item.sceneSoundFlag;
+				NancySceneState.changeScene(sceneChange);
 				close();
-				input.eatMouseInput();
-				return;
 			}
+
+			input.eatMouseInput();
+			return;
 		}
 	}
 
diff --git a/engines/nancy/ui/inventorypopup.h b/engines/nancy/ui/inventorypopup.h
index 7162e96e628..383354bd4d5 100644
--- a/engines/nancy/ui/inventorypopup.h
+++ b/engines/nancy/ui/inventorypopup.h
@@ -84,6 +84,15 @@ private:
 	void drawCloseButton(bool hovered);
 	void drawScrollbar(UIButtonState state);
 	void rebuildVisibleList();
+
+	// Reconcile the shared, save-persisted inventory order list with the items
+	// the player actually owns: drop stale entries and append any owned items
+	// missing from it (in item-ID order). Fixes up saves made before ordering
+	// was tracked in one pass, instead of one item at a time. Returns true if
+	// the order list was changed.
+	bool syncOrderWithInventory();
+
+	bool itemMatchesFilter(int16 itemID) const;
 	void setActiveFilterIndex(uint index);
 
 	// Play a popup button's click sound (the filter tabs and the close X),


Commit: 2cfdf36d97024615d0182d21d148c2309cf3a509
    https://github.com/scummvm/scummvm/commit/2cfdf36d97024615d0182d21d148c2309cf3a509
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-09T22:04:44+03:00

Commit Message:
NANCY: Clear held item when starting new game

Fix #16937

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


diff --git a/engines/nancy/state/scene.cpp b/engines/nancy/state/scene.cpp
index ca313397375..a15f95cffe3 100644
--- a/engines/nancy/state/scene.cpp
+++ b/engines/nancy/state/scene.cpp
@@ -1006,6 +1006,11 @@ void Scene::init() {
 	_flags.items.resize(g_nancy->getStaticData().numItems, g_nancy->_false);
 	_flags.disabledItems.resize(_flags.items.size(), 0);
 
+	// The CursorManager is owned by the engine and survives a New Game (which
+	// destroys and recreates the Scene). Clear any held-item cursor left over
+	// from a previous playthrough so a fresh game starts with the normal cursor.
+	g_nancy->_cursor->setCursorItemID(-1);
+
 	_timers.lastTotalTime = 0;
 	_timers.playerTime = bootSummary->startTimeHours * 3600000;
 	_timers.sceneTime = 0;


Commit: da1a1172d23f03ada1dd6c236c85789534798750
    https://github.com/scummvm/scummvm/commit/da1a1172d23f03ada1dd6c236c85789534798750
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-09T22:04:45+03:00

Commit Message:
NANCY: NANCY10: Fix cellphone close sound after another sound starts

Fix #16916

Changed paths:
    engines/nancy/ui/cellphonepopup.cpp


diff --git a/engines/nancy/ui/cellphonepopup.cpp b/engines/nancy/ui/cellphonepopup.cpp
index ec2e7c319fd..82b1ff3ab15 100644
--- a/engines/nancy/ui/cellphonepopup.cpp
+++ b/engines/nancy/ui/cellphonepopup.cpp
@@ -1679,9 +1679,12 @@ void CellPhonePopup::handleInput(NancyInput &input) {
 		if (overClose) {
 			g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
 			if (input.input & NancyInput::kLeftMouseButtonUp) {
-				playButtonClickSound(closeBtn);
 				input.eatMouseInput();
+				// close() stops the call-sound channel, which the X's click
+				// sound may share; close first so the click sound isn't cut off
+				// once a call / dial / web tone has occupied that channel.
 				close();
+				playButtonClickSound(closeBtn);
 				return;
 			}
 		}


Commit: c429a02d2c4c08c99da9aaccb0a613077a9daeae
    https://github.com/scummvm/scummvm/commit/c429a02d2c4c08c99da9aaccb0a613077a9daeae
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-09T22:04:46+03:00

Commit Message:
NANCY: NANCY10: Fix target for cellphone browser homepage back button

Fix #16940

Changed paths:
    engines/nancy/ui/cellphonepopup.cpp


diff --git a/engines/nancy/ui/cellphonepopup.cpp b/engines/nancy/ui/cellphonepopup.cpp
index 82b1ff3ab15..36f19132b99 100644
--- a/engines/nancy/ui/cellphonepopup.cpp
+++ b/engines/nancy/ui/cellphonepopup.cpp
@@ -868,6 +868,12 @@ void CellPhonePopup::openBrowserHome() {
 	const UIBW *browserData = GetEngineData(UIBW);
 	if (browserData && !browserData->pages.empty()) {
 		openContentView(browserData->pages[0].imageName.toString(), _uiclData->browserHeading);
+		// The homepage's Back button always returns to the main phone (welcome)
+		// screen, regardless of whether the browser was opened from the online
+		// hub or reached via the search list's HOME button. openContentView
+		// otherwise records whichever screen we came from, which could send Back
+		// to the search list.
+		_contentReturnState = kWelcome;
 	} else {
 		enterScreenState(kWebList);
 	}


Commit: 9842c1e48da40ffa88f0023a58dac467c7b25253
    https://github.com/scummvm/scummvm/commit/9842c1e48da40ffa88f0023a58dac467c7b25253
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-09T22:04:47+03:00

Commit Message:
NANCY: NANCY10: Implement text underline

Fix #16943

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


diff --git a/engines/nancy/font.cpp b/engines/nancy/font.cpp
index abfa2179292..2f4fcd63bed 100644
--- a/engines/nancy/font.cpp
+++ b/engines/nancy/font.cpp
@@ -183,6 +183,35 @@ void Font::drawChar(Graphics::Surface *dst, uint32 chr, int x, int y, uint32 col
 	dest.blitFrom(_image, srcRect, Common::Point(x, y + yOffset));
 }
 
+uint32 Font::getColorPixel(uint color) const {
+	// The glyphs are pre-colored in the atlas (two color variants selected by
+	// the color-coordinate offsets, mirroring drawChar). Scan a solid glyph for
+	// the first non-transparent pixel to recover the color an underline should
+	// use to match the text.
+	Common::Rect src = getCharacterSourceRect('l');
+	if (color == 0) {
+		src.translate(_color0CoordsOffset.x, _color0CoordsOffset.y);
+	} else if (color == 1) {
+		src.translate(_color1CoordsOffset.x, _color1CoordsOffset.y);
+	}
+
+	const Graphics::Surface &surf = _image.rawSurface();
+	for (int yy = src.top; yy < src.bottom; ++yy) {
+		for (int xx = src.left; xx < src.right; ++xx) {
+			if (xx < 0 || yy < 0 || xx >= surf.w || yy >= surf.h) {
+				continue;
+			}
+			const void *p = surf.getBasePtr(xx, yy);
+			const uint32 px = surf.format.bytesPerPixel == 2 ? *(const uint16 *)p : *(const uint32 *)p;
+			if (px != _transColor) {
+				return px;
+			}
+		}
+	}
+
+	return _transColor;
+}
+
 void Font::wordWrap(const Common::String &str, int maxWidth, Common::Array<Common::String> &lines, int initWidth) const {
 	Common::String temp;
 	for (const char *c = str.begin(); c != str.end(); ++c) {
diff --git a/engines/nancy/font.h b/engines/nancy/font.h
index c3d73477efe..60c4b6301cb 100644
--- a/engines/nancy/font.h
+++ b/engines/nancy/font.h
@@ -52,6 +52,11 @@ public:
 
 	const Graphics::ManagedSurface &getImageSurface() const { return _image; }
 
+	// Samples an opaque pixel from a solid glyph rendered in the given color
+	// variant, so callers can match the exact text color. Used to draw
+	// underlines (the <u> markup toggle), for which the atlas has no glyph.
+	uint32 getColorPixel(uint color) const;
+
 	// Custom word wrapping function to fix an edge case with overflowing whitespaces
 	void wordWrap(const Common::String &str, int maxWidth, Common::Array<Common::String> &lines, int initWidth = 0) const;
 
diff --git a/engines/nancy/misc/hypertext.cpp b/engines/nancy/misc/hypertext.cpp
index c43eb941456..a01671b29e0 100644
--- a/engines/nancy/misc/hypertext.cpp
+++ b/engines/nancy/misc/hypertext.cpp
@@ -30,7 +30,7 @@ namespace Nancy {
 namespace Misc {
 
 struct MetaInfo {
-	enum Type { kColor, kFont, kMark, kHotspot };
+	enum Type { kColor, kFont, kMark, kHotspot, kUnderline };
 
 	Type type;
 	uint numChars;
@@ -171,6 +171,15 @@ void HypertextParser::drawAllText(const Common::Rect &textBounds, uint leftOffse
 
 					currentLine += '\t';
 					continue;
+				case 'u' :
+					// Underline toggle. Paired <u> tags bracket underlined text
+					// (e.g. journal cross-references).
+					if (curToken.size() != 1) {
+						break;
+					}
+
+					metaInfo.push({MetaInfo::kUnderline, numNonSpaceChars, 0});
+					continue;
 				case 'c' :
 					// Color tokens
 					// We keep the positions (excluding spaces) and colors of the color tokens in a queue
@@ -259,6 +268,7 @@ void HypertextParser::drawAllText(const Common::Rect &textBounds, uint leftOffse
 		// respect color tokens
 		uint totalCharsDrawn = 0;
 		byte colorID = _defaultTextColor;
+		bool underline = false;
 		uint numNewlineTokens = 0;
 		uint horizontalOffset = 0;
 		bool newLineStart = false;
@@ -327,6 +337,9 @@ void HypertextParser::drawAllText(const Common::Rect &textBounds, uint leftOffse
 					case MetaInfo::kColor:
 						colorID = change.index;
 						break;
+					case MetaInfo::kUnderline:
+						underline = !underline;
+						break;
 					case MetaInfo::kMark: {
 						auto *mark = GetEngineData(MARK);
 						assert(mark);
@@ -407,13 +420,23 @@ void HypertextParser::drawAllText(const Common::Rect &textBounds, uint leftOffse
 				Common::String &stringToDraw = subLine.size() ? subLine : line;
 
 				// Draw the normal text
+				const int drawX = textBounds.left + horizontalOffset + (newLineStart ? 0 : leftOffsetNonNewline);
+				const int drawY = textBounds.top + _numDrawnLines * lineStep(font) + _imageVerticalOffset;
 				font->drawString(				&_fullSurface,
 												stringToDraw,
-												textBounds.left + horizontalOffset + (newLineStart ? 0 : leftOffsetNonNewline),
-												textBounds.top + _numDrawnLines * lineStep(font) + _imageVerticalOffset,
+												drawX,
+												drawY,
 												textBounds.width(),
 												colorID);
 
+				// Underline the segment (the <u> markup toggle) in the text color.
+				if (underline && !stringToDraw.empty()) {
+					const int underlineWidth = font->getStringWidth(stringToDraw);
+					const int underlineY = drawY + font->getFontHeight() - 1;
+					_fullSurface.fillRect(Common::Rect(drawX, underlineY, drawX + underlineWidth, underlineY + 1),
+											font->getColorPixel(colorID));
+				}
+
 				// Then, draw the highlight
 				if (hasHotspot && !_textHighlightSurface.empty()) {
 					highlightFont->drawString(	&_textHighlightSurface,




More information about the Scummvm-git-logs mailing list