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

bluegr noreply at scummvm.org
Sun Jul 26 02:36:03 UTC 2026


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

Summary:
934494d370 NANCY: NANCY10: Fix grid gaps in GridMapPuzzle, and update comments
8b924e8dbf NANCY: NANCY10: Clip overflowed text in ScrollTextBox
b39d3b3b27 NANCY: NANCY10: More work on the full/mini textbox
28bfc71ac2 NANCY: NANCY10: Fix issues with GridMapPuzzle (Petroglyph puzzle)
957fd44b78 NANCY: NANCY10: Fix cursor when placing pieces in OneBuildPuzzle
85ea8f5e81 NANCY: Handle common sounds from the console play_sound command
6808a7a71a NANCY: NANCY10: Cleanup comments
f4cfed09bc NANCY: Activate all AR records when loading a game


Commit: 934494d370f045374a5ac425e5097765ae05fd9f
    https://github.com/scummvm/scummvm/commit/934494d370f045374a5ac425e5097765ae05fd9f
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-26T05:35:42+03:00

Commit Message:
NANCY: NANCY10: Fix grid gaps in GridMapPuzzle, and update comments

Fix #16980

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


diff --git a/engines/nancy/action/puzzle/gridmappuzzle.cpp b/engines/nancy/action/puzzle/gridmappuzzle.cpp
index b229acd2e21..6aea72e5bef 100644
--- a/engines/nancy/action/puzzle/gridmappuzzle.cpp
+++ b/engines/nancy/action/puzzle/gridmappuzzle.cpp
@@ -255,16 +255,22 @@ void GridMapPuzzle::execute() {
 
 Common::Rect GridMapPuzzle::mapCellRect(int row, int col) const {
 	// Stride uses the raw src-rect dimensions (right - left, before readRect's
-	// inclusive→exclusive +1), to match the original's cell layout.
-	int x = (int)_mapOriginX + col * ((int)_mapSpacingX + _mapCellW - 1);
-	int y = (int)_mapOriginY + row * ((int)_mapSpacingY + _mapCellH - 1);
-	return Common::Rect(x, y, x + _mapCellW, y + _mapCellH);
+	// inclusive→exclusive +1). The rect spans the full stride so cells tile the
+	// grid with no gaps, keeping the grab-hand cursor steady across the whole
+	// grid. Only blits read the rect's top-left, so sprites still land correctly.
+	int strideX = (int)_mapSpacingX + _mapCellW - 1;
+	int strideY = (int)_mapSpacingY + _mapCellH - 1;
+	int x = (int)_mapOriginX + col * strideX;
+	int y = (int)_mapOriginY + row * strideY;
+	return Common::Rect(x, y, x + strideX, y + strideY);
 }
 
 Common::Rect GridMapPuzzle::itemsCellRect(int row, int col) const {
-	int x = (int)_itemsOriginX + col * ((int)_itemsSpacingX + _itemsCellW - 1);
-	int y = (int)_itemsOriginY + row * ((int)_itemsSpacingY + _itemsCellH - 1);
-	return Common::Rect(x, y, x + _itemsCellW, y + _itemsCellH);
+	int strideX = (int)_itemsSpacingX + _itemsCellW - 1;
+	int strideY = (int)_itemsSpacingY + _itemsCellH - 1;
+	int x = (int)_itemsOriginX + col * strideX;
+	int y = (int)_itemsOriginY + row * strideY;
+	return Common::Rect(x, y, x + strideX, y + strideY);
 }
 
 bool GridMapPuzzle::hitTestMap(const Common::Point &p, int &outRow, int &outCol) const {
@@ -347,9 +353,10 @@ void GridMapPuzzle::handleInput(NancyInput &input) {
 	if (!hitMap)
 		hitItems = hitTestItems(mouseVP, iRow, iCol);
 
-	// Grid cells (map and items) use the grab-hand cursor; picking something up
-	// doesn't change it. The exit hotspot uses the puzzle-exit cursor, and every
-	// other area (letter headers, gaps) keeps the idle eyeglass.
+	// Both grids (map and items) show the grab-hand cursor across their whole
+	// area; picking something up doesn't change it. The exit hotspot uses the
+	// puzzle-exit cursor, and everything outside the grids (the letter strips,
+	// the gap between the grids) keeps the idle eyeglass.
 	if (!hitMap && !hitItems) {
 		if (!_exitHotspot.isEmpty() && _exitHotspot.contains(mouseVP)) {
 			g_nancy->_cursor->setCursorType(g_nancy->_cursor->_puzzleExitCursor);


Commit: 8b924e8dbf92e0bc36d1aa1e1a0479b992b10d0c
    https://github.com/scummvm/scummvm/commit/8b924e8dbf92e0bc36d1aa1e1a0479b992b10d0c
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-26T05:35:43+03:00

Commit Message:
NANCY: NANCY10: Clip overflowed text in ScrollTextBox

Fix #17006

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


diff --git a/engines/nancy/ui/scrolltextbox.cpp b/engines/nancy/ui/scrolltextbox.cpp
index 9a858580149..ddcf5b4b72b 100644
--- a/engines/nancy/ui/scrolltextbox.cpp
+++ b/engines/nancy/ui/scrolltextbox.cpp
@@ -160,15 +160,16 @@ void ScrollTextBox::drawContent() {
 			scrollY = (int)(_scrollPos * (contentHeight - visibleTextHeight));
 		}
 	} else {
-		// A strip at the top of the overlay, sized to the text with a two-line
-		// minimum. Drop the chrome's bottom margin so it hugs the content.
+		// Fixed two-line strip at the top of the overlay. Overflow is clipped
+		// (hidden beneath the taskbar), matching the original rather than growing
+		// the box down over the UI.
 		const Font *font = g_nancy->_graphics->getFont(fontID);
 		const int lineStep = font->getLineHeight() + font->getLineHeight() / 9;
-		const int twoLineContent = _tboxData->scrollbarDefaultPos.y + 2 * lineStep;
-		const int miniHeight = MIN(viewport.top + MAX(contentHeight, twoLineContent), fullHeight);
+		const int stripContent = _tboxData->scrollbarDefaultPos.y + 2 * lineStep;
+		const int miniHeight = MIN(viewport.top + stripContent, fullHeight);
 		boxRect = Common::Rect(_fullPopupRect.left, _fullPopupRect.top,
 							   _fullPopupRect.right, _fullPopupRect.top + miniHeight);
-		visibleTextHeight = contentHeight;
+		visibleTextHeight = MIN(contentHeight, stripContent);
 	}
 
 	moveTo(boxRect);


Commit: b39d3b3b27a8938ae2dfa2ff3c5c92a29d404b26
    https://github.com/scummvm/scummvm/commit/b39d3b3b27a8938ae2dfa2ff3c5c92a29d404b26
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-26T05:35:45+03:00

Commit Message:
NANCY: NANCY10: More work on the full/mini textbox

- Use the correct screen rect for the mini text strip
- Use the correct mini strip width

Fix #17006, #16890

Changed paths:
    engines/nancy/action/arfactory.cpp
    engines/nancy/enginedata.cpp
    engines/nancy/enginedata.h
    engines/nancy/state/scene.cpp
    engines/nancy/ui/scrolltextbox.cpp
    engines/nancy/ui/scrolltextbox.h
    engines/nancy/ui/textbox.cpp
    engines/nancy/ui/textbox.h


diff --git a/engines/nancy/action/arfactory.cpp b/engines/nancy/action/arfactory.cpp
index 437943d42aa..9255f99ab1e 100644
--- a/engines/nancy/action/arfactory.cpp
+++ b/engines/nancy/action/arfactory.cpp
@@ -288,7 +288,7 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
 		return new ModifyListEntry(ModifyListEntry::kDelete);
 	case 73:
 		return new ModifyListEntry(ModifyListEntry::kMark);
-	case 74:	// Nancy10
+	case 74:	// Nancy10 only: writes the full, taskbar-covering box
 		return new FrameTextBox(true);
 	case 75:
 		if (g_nancy->getGameType() <= kGameTypeNancy9)
diff --git a/engines/nancy/enginedata.cpp b/engines/nancy/enginedata.cpp
index b41ff97a5f8..4254c70a819 100644
--- a/engines/nancy/enginedata.cpp
+++ b/engines/nancy/enginedata.cpp
@@ -282,7 +282,7 @@ TBOX::TBOX(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
 
 	if (g_nancy->getGameType() >= kGameTypeNancy10) {
 		lineStartXCursor = chunkStream->readUint16LE();	// text left inset
-		chunkStream->skip(2);	// bottom margin
+		stripRightMargin = chunkStream->readUint16LE();	// strip text right margin (see FrameTextBox)
 		chunkStream->skip(2);	// unused
 		chunkStream->skip(2);	// initial color (we use the AR for this)
 	}
@@ -894,7 +894,7 @@ TASK::TASK(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
 	readRect(*chunkStream, srcRect);
 	readRect(*chunkStream, dstRect);
 	readRect(*chunkStream, unkRect1);
-	readRect(*chunkStream, unkRect2);
+	readRect(*chunkStream, ccTextboxScreenRect);
 
 	// The button count varies by game (Nancy12 adds a 6th slot for the coin
 	// purse), so derive it from the chunk size. A slot marked "NO_UI_ITEM" (e.g.
diff --git a/engines/nancy/enginedata.h b/engines/nancy/enginedata.h
index c7e9fd94237..94a60407fe5 100644
--- a/engines/nancy/enginedata.h
+++ b/engines/nancy/enginedata.h
@@ -182,6 +182,7 @@ struct TBOX : public EngineData {
 	int32 maxScrollWidth = 0;
 	int32 firstLineY = 0; // added to the y-cursor when starting a new line
 	uint16 lineStartXCursor = 0; // left inset of the text within the text area
+	uint16 stripRightMargin = 0; // subtracted from contentWidth for the strip's text width
 	int32 unknown1 = 0;
 	int32 unknown2 = 0;
 	int32 contentWidth = 0;
@@ -553,7 +554,9 @@ struct TASK : public EngineData {
 	Common::Rect srcRect;
 	Common::Rect dstRect;
 	Common::Rect unkRect1;
-	Common::Rect unkRect2;
+	// Screen rect of the closed-caption text strip the taskbar draws (the
+	// ScrollTextBox's mini strip is positioned to it).
+	Common::Rect ccTextboxScreenRect;
 
 	ButtonRecord buttons[kNumButtons];
 };
diff --git a/engines/nancy/state/scene.cpp b/engines/nancy/state/scene.cpp
index 3ed6fcee6d3..f43fdd7d813 100644
--- a/engines/nancy/state/scene.cpp
+++ b/engines/nancy/state/scene.cpp
@@ -1545,7 +1545,7 @@ void Scene::handleInput() {
 		// original); restored automatically once the popup closes.
 		_taskbar->setPopupLockout(popupOpen);
 	}
-	if (_taskbar && !_textbox.isFullMode() && !popupOpen) {
+	if (_taskbar && !_textbox.coversTaskbar() && !popupOpen) {
 		// MENU and HELP leave gameplay entirely, which would cut off the
 		// taskbar click sound. The original defers the transition until that
 		// sound finishes, so we hold the click here and only switch state
diff --git a/engines/nancy/ui/scrolltextbox.cpp b/engines/nancy/ui/scrolltextbox.cpp
index ddcf5b4b72b..8a1749ca56a 100644
--- a/engines/nancy/ui/scrolltextbox.cpp
+++ b/engines/nancy/ui/scrolltextbox.cpp
@@ -42,6 +42,7 @@ ScrollTextBox::ScrollTextBox() :
 		_scrollbarDragging(false),
 		_scrollbarHovered(false),
 		_scrollbarGrabOffset(0),
+		_fullMode(false),
 		_expanded(false),
 		_fontIDOverride(-1),
 		_autoClearTime(0) {}
@@ -75,14 +76,22 @@ void ScrollTextBox::init() {
 	}
 	moveTo(_fullPopupRect);
 
+	// The taskbar owns the caption strip, so take the strip's text origin from the
+	// TASK chunk rather than the conversation overlay.
+	auto *task = GetEngineData(TASK);
+	if (task) {
+		_stripScreenRect = task->ccTextboxScreenRect;
+	}
+
 	Common::Rect bounds = _screenPosition;
 	bounds.moveTo(0, 0);
 	_drawSurface.create(bounds.width(), bounds.height(), g_nancy->_graphics->getInputPixelFormat());
 
-	// Key the text surface on the transparent color so only glyphs composite,
-	// over the chrome when expanded or onto the taskbar strip when mini.
+	// Key on the transparent color so only glyphs composite. Size for the wider
+	// (strip) layout plus the line-start inset so either fits.
 	const uint32 transColor = g_nancy->_graphics->getTransColor();
-	initSurfaces(textViewportLocal().width(), kHypertextSurfaceHeight,
+	const int surfaceWidth = _tboxData->lineStartXCursor + MAX<int>(stripTextWidth(), textViewportLocal().width());
+	initSurfaces(surfaceWidth, kHypertextSurfaceHeight,
 				 g_nancy->_graphics->getInputPixelFormat(), transColor, transColor);
 	_fullSurface.setTransparentColor(transColor);
 
@@ -115,14 +124,19 @@ Common::Rect ScrollTextBox::textViewportLocal() const {
 	return r;
 }
 
+int ScrollTextBox::stripTextWidth() const {
+	// Strip text spans lineStartXCursor..(contentWidth - stripRightMargin), narrower
+	// than the conversation viewport (matches FrameTextBox's normal path).
+	return (_tboxData->contentWidth - _tboxData->stripRightMargin - _tboxData->lineStartXCursor) + 1;
+}
+
 void ScrollTextBox::drawBackground() {
 	_drawSurface.blitFrom(_overlayImage, _header->normalSrcRect,
 						  Common::Point(0, 0));
 	_needsRedraw = true;
 }
 
-void ScrollTextBox::drawContent() {
-	// Render all text into the scratch surface to measure its height.
+void ScrollTextBox::layoutText(int wrapWidth, uint16 fontID) {
 	const uint32 transColor = g_nancy->_graphics->getTransColor();
 	_drawnTextHeight = 0;
 	_numDrawnLines = 0;
@@ -130,24 +144,34 @@ void ScrollTextBox::drawContent() {
 	_fullSurface.fillRect(Common::Rect(0, 0, _fullSurface.w, _fullSurface.h), transColor);
 	_textHighlightSurface.fillRect(Common::Rect(0, 0, _textHighlightSurface.w, _textHighlightSurface.h), transColor);
 
-	Common::Rect textBounds(0, 0, _fullSurface.w, _fullSurface.h);
-	// The text's inset within the text area is the line-start X cursor. There is
-	// no vertical inset — the first line starts at the top of the text area, so
-	// don't add scrollbarDefaultPos (which is the scrollbar's gutter position).
-	textBounds.left += _tboxData->lineStartXCursor;
+	// The line-start X cursor is the text's horizontal inset; there is no vertical
+	// inset, so the first line starts at the top of the text area.
+	Common::Rect textBounds(_tboxData->lineStartXCursor, 0,
+							_tboxData->lineStartXCursor + wrapWidth, _fullSurface.h);
+	drawAllText(textBounds, 0, fontID, _tboxData->highlightConversationFontID);
+}
+
+void ScrollTextBox::drawContent() {
+	const uint32 transColor = g_nancy->_graphics->getTransColor();
+	const Common::Rect viewport = textViewportLocal();
+	const int fullHeight = _fullPopupRect.height();
 
 	// Narration/caption text uses the (white) highlight font, not the (cyan)
 	// conversation body font that the ConversationPopup uses.
 	const uint16 fontID = _fontIDOverride != -1 ? (uint16)_fontIDOverride : _tboxData->highlightConversationFontID;
-	drawAllText(textBounds, 0, fontID, _tboxData->highlightConversationFontID);
 
-	// Mini strip vs full overlay with scrollbar, decided by whether the text
-	// overflows the viewport (not by line count): a caption that fits stays mini.
-	const Common::Rect viewport = textViewportLocal();
-	const int fullHeight = _fullPopupRect.height();
-	const int contentHeight = getInnerHeight();
+	// Lay out at the strip's text width first, then pick the box: Nancy 11+ by line
+	// count, Nancy 10 by which FrameTextBox AR wrote the text (74 full / 75 strip).
+	layoutText(stripTextWidth(), fontID);
+	_expanded = g_nancy->getGameType() >= kGameTypeNancy11 ? (_numDrawnLines > 2) : _fullMode;
 
-	_expanded = contentHeight > viewport.height();
+	if (_expanded) {
+		// The expanded box reserves the gutter for its slider, so re-lay-out
+		// narrower to clear it.
+		layoutText(viewport.width(), fontID);
+	}
+
+	const int contentHeight = getInnerHeight();
 
 	Common::Rect boxRect;
 	int visibleTextHeight;
@@ -160,13 +184,12 @@ void ScrollTextBox::drawContent() {
 			scrollY = (int)(_scrollPos * (contentHeight - visibleTextHeight));
 		}
 	} else {
-		// Fixed two-line strip at the top of the overlay. Overflow is clipped
-		// (hidden beneath the taskbar), matching the original rather than growing
-		// the box down over the UI.
+		// Fixed two-line strip; overflow is clipped (hidden beneath the taskbar).
+		// Line 3 starts at exactly 2 * lineStep, so the clip must add no padding.
 		const Font *font = g_nancy->_graphics->getFont(fontID);
 		const int lineStep = font->getLineHeight() + font->getLineHeight() / 9;
-		const int stripContent = _tboxData->scrollbarDefaultPos.y + 2 * lineStep;
-		const int miniHeight = MIN(viewport.top + stripContent, fullHeight);
+		const int stripContent = 2 * lineStep;
+		const int miniHeight = MIN(viewport.top + 1 + stripContent, fullHeight);
 		boxRect = Common::Rect(_fullPopupRect.left, _fullPopupRect.top,
 							   _fullPopupRect.right, _fullPopupRect.top + miniHeight);
 		visibleTextHeight = MIN(contentHeight, stripContent);
@@ -175,25 +198,26 @@ void ScrollTextBox::drawContent() {
 	moveTo(boxRect);
 	_drawSurface.create(boxRect.width(), boxRect.height(), g_nancy->_graphics->getInputPixelFormat());
 
-	int textLeft = viewport.left;
+	// Expanded text clears the slider gutter; the strip aligns to the taskbar's
+	// caption rect (on top of the baked-in lineStartXCursor), or falls back to 0.
+	const bool haveStripRect = !_expanded && !_stripScreenRect.isEmpty();
+	int textLeft = _expanded ? viewport.left
+							 : (haveStripRect ? _stripScreenRect.left - _fullPopupRect.left : 0);
 	int textTop = viewport.top;
 	if (_expanded) {
+		// The textured overlay panel, with its slider.
 		setTransparent(false);
 		drawBackground();
 	} else {
-		// The taskbar draws the strip, so keep mini transparent with no chrome,
-		// and pull the text left into the unused scrollbar gap.
+		// The taskbar already draws the strip, so keep it transparent with no chrome.
 		setTransparent(true);
 		_drawSurface.clear(transColor);
-		// Nudge the first line down one pixel so its top padding matches the
-		// original's mini strip.
-		textTop += 1;
-		const UISliderRecord &sl = _header->slider;
-		if (_header->sliderEnabled && !sl.destRect.isEmpty()) {
-			// Align the glyphs to the scrollbar's left edge, dropping the
-			// line-start inset that is baked into the text surface (the mini
-			// strip reclaims the whole gutter).
-			textLeft = toPopupLocal(sl.destRect, sl.destUsesGameFrameOffset != 0).left - _tboxData->lineStartXCursor;
+		if (haveStripRect) {
+			textTop = _stripScreenRect.top - _fullPopupRect.top;
+		} else {
+			// Nudge the first line down one pixel so its top padding matches the
+			// original's caption strip.
+			textTop += 1;
 		}
 	}
 
@@ -209,10 +233,8 @@ void ScrollTextBox::drawContent() {
 }
 
 void ScrollTextBox::redrawScroll() {
-	// Only reachable while dragging the scrollbar, which only exists in the
-	// expanded state, so the layout (box rect, surface size, text in _fullSurface)
-	// is already settled. Just re-composite the visible slice at the new scroll
-	// offset instead of re-running drawContent()'s full text re-layout.
+	// Only runs while dragging the scrollbar, so the layout is already settled:
+	// re-composite the visible slice at the new scroll offset, no re-layout.
 	const Common::Rect viewport = textViewportLocal();
 	const int visibleTextHeight = viewport.height();
 	const int contentHeight = getInnerHeight();
@@ -306,12 +328,27 @@ void ScrollTextBox::setOverrideFont(uint fontID) {
 	_fontIDOverride = (int)fontID;
 }
 
+void ScrollTextBox::setFullMode(bool fullMode) {
+	// Nancy 11+ has no second box; the mode only matters for Nancy 10.
+	if (g_nancy->getGameType() >= kGameTypeNancy11 || _fullMode == fullMode) {
+		return;
+	}
+
+	_fullMode = fullMode;
+
+	// The AR writes its text before selecting the mode, so re-render.
+	if (_isVisible) {
+		drawContent();
+	}
+}
+
 void ScrollTextBox::clear() {
 	HypertextParser::clear();
 
 	_scrollPos = 0.0f;
 	_scrollbarDragging = false;
 	_scrollbarHovered = false;
+	_fullMode = false;
 	_expanded = false;
 	_fontIDOverride = -1;
 	_autoClearTime = 0;
@@ -332,7 +369,7 @@ void ScrollTextBox::handleInput(NancyInput &input) {
 	const Common::Point localMouse = popupLocalMouse(input.mousePos);
 	const UISliderRecord &slider = _header->slider;
 
-	// The scrollbar only exists in the expanded state.
+	// The scrollbar only exists on the expanded box.
 	if (_header->sliderEnabled && _expanded) {
 		const Common::Rect trackLocal = toPopupLocal(slider.destRect, slider.destUsesGameFrameOffset != 0);
 		const int trackHeight = trackLocal.height();
@@ -357,11 +394,8 @@ void ScrollTextBox::handleInput(NancyInput &input) {
 				released = true;
 			}
 
-			// Re-composite only when the thumb moves (or the drag just ended, to
-			// repaint the thumb out of its pressed state), and via redrawScroll() --
-			// a cheap slice re-blit -- rather than drawContent()'s full text
-			// re-layout. This keeps dragging smooth instead of re-rendering the
-			// whole text surface each frame.
+			// Re-composite (via the cheap redrawScroll()) only when the thumb moves
+			// or the drag ends, to keep dragging smooth.
 			if (newScrollPos != _scrollPos || released) {
 				_scrollPos = newScrollPos;
 				redrawScroll();
diff --git a/engines/nancy/ui/scrolltextbox.h b/engines/nancy/ui/scrolltextbox.h
index 9b798014a1b..2cd0b9758bc 100644
--- a/engines/nancy/ui/scrolltextbox.h
+++ b/engines/nancy/ui/scrolltextbox.h
@@ -55,14 +55,20 @@ public:
 	// that many milliseconds (used by inventory closed captions).
 	void addTextLine(const Common::String &text, uint32 autoClearTime = 0);
 	void setOverrideFont(uint fontID);
+	// Nancy 10 only: pick the full box (AR 74) or the strip (AR 75). Ignored from
+	// Nancy 11 on, which picks by line count.
+	void setFullMode(bool fullMode);
+	// True while the full box is showing and covering the taskbar buttons.
+	bool coversTaskbar() const { return _isVisible && _expanded; }
 	void clear() override;
 
 private:
 	void drawBackground();
 	void drawContent();
+	// Render the text into the scratch surface, wrapping at wrapWidth.
+	void layoutText(int wrapWidth, uint16 fontID);
 	// Re-composite the expanded overlay at the current scroll position without
-	// re-laying out the text (which drawContent() does). Used while dragging the
-	// scrollbar, so a drag is a cheap slice re-blit rather than a full re-render.
+	// re-laying out the text; used for cheap scrollbar-drag updates.
 	void redrawScroll();
 	void drawScrollbar(UIButtonState state);
 	uint16 getInnerHeight() const;
@@ -71,6 +77,9 @@ private:
 	// viewport brought into the popup surface's space).
 	Common::Rect textViewportLocal() const;
 
+	// Width the strip wraps at: its own text area from TBOX, not the viewport.
+	int stripTextWidth() const;
+
 	// Popup-local bounding rect of the scrollbar thumb at the current scroll
 	// position, or an empty rect when the slider is disabled.
 	Common::Rect computeThumbRect() const;
@@ -86,6 +95,9 @@ private:
 	// The fully-expanded popup rect on screen, with the game-frame offset
 	// applied. The mini strip shares its top edge.
 	Common::Rect _fullPopupRect;
+	// Screen rect the taskbar reserves for the caption strip (TASK chunk). The
+	// strip's text is aligned to it; empty when there is no taskbar data.
+	Common::Rect _stripScreenRect;
 	const TBOX *_tboxData;
 
 	Graphics::ManagedSurface _overlayImage;
@@ -95,8 +107,10 @@ private:
 	bool _scrollbarHovered;
 	int _scrollbarGrabOffset;
 
-	// True when text overflows the viewport: full overlay with scrollbar.
-	// False when it fits: mini strip above the taskbar, sized to content.
+	// Nancy 10 only: true selects the AR 74 full box, false the AR 75 strip.
+	// Defaults false since every writer but AR 74 uses the strip.
+	bool _fullMode;
+	// True for the full, taskbar-covering box (with scrollbar); false for the strip.
 	bool _expanded;
 
 	int _fontIDOverride;
diff --git a/engines/nancy/ui/textbox.cpp b/engines/nancy/ui/textbox.cpp
index e6a2adcc206..d86f359fa13 100644
--- a/engines/nancy/ui/textbox.cpp
+++ b/engines/nancy/ui/textbox.cpp
@@ -119,9 +119,13 @@ void Textbox::updateGraphics() {
 	RenderObject::updateGraphics();
 }
 
-void Textbox::setFullMode(bool open, uint32 timeoutMs) {
-	// No-op: the Nancy 10+ overlay shows itself when it holds text, and the
-	// Nancy 1-9 box has no full mode. Kept for the FrameTextBox AR's API.
+bool Textbox::coversTaskbar() const {
+	return _scrollTextBox ? _scrollTextBox->coversTaskbar() : false;
+}
+
+void Textbox::setFullMode(bool open) {
+	if (_scrollTextBox)
+		_scrollTextBox->setFullMode(open);
 }
 
 void Textbox::handleInput(NancyInput &input) {
diff --git a/engines/nancy/ui/textbox.h b/engines/nancy/ui/textbox.h
index 9fbc7266139..23080039b12 100644
--- a/engines/nancy/ui/textbox.h
+++ b/engines/nancy/ui/textbox.h
@@ -52,9 +52,13 @@ public:
 	void addTextLine(const Common::String &text, uint32 autoClearTime = 0);
 	void setOverrideFont(const uint fontID);
 
-	// Vestigial "full mode" toggle, now a no-op. Kept for the FrameTextBox AR.
-	void setFullMode(bool open, uint32 timeoutMs = 15000);
-	bool isFullMode() const { return false; }
+	// Nancy 10 only: picks the full, taskbar-covering box (AR 74) over the strip
+	// (AR 75). No effect on Nancy 1-9 (no such mode) or Nancy 11+ (a single box).
+	// The AR owns the full box's auto-close timer.
+	void setFullMode(bool open);
+	// True while the text box visually covers the taskbar buttons, so Scene can
+	// skip taskbar input. Always false before Nancy 10, whose box sits clear of them.
+	bool coversTaskbar() const;
 
 private:
 	uint16 getInnerHeight() const;


Commit: 28bfc71ac27feeed72f4b0cc53148ba900fef567
    https://github.com/scummvm/scummvm/commit/28bfc71ac27feeed72f4b0cc53148ba900fef567
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-26T05:35:47+03:00

Commit Message:
NANCY: NANCY10: Fix issues with GridMapPuzzle (Petroglyph puzzle)

- Allow placing glyphs on any square on the grid, not just right ones
- Use correct graphic when dragging a glyph
- Remove item from inventory when accessing it via GoInvViewScene
- Play the BUDE sound when dropping a held item back to the inventory

Fix #17014

Changed paths:
    engines/nancy/action/inventoryrecords.cpp
    engines/nancy/action/puzzle/gridmappuzzle.cpp
    engines/nancy/action/puzzle/gridmappuzzle.h
    engines/nancy/ui/inventorypopup.cpp


diff --git a/engines/nancy/action/inventoryrecords.cpp b/engines/nancy/action/inventoryrecords.cpp
index 64ed1f693c5..7f6948b1dd9 100644
--- a/engines/nancy/action/inventoryrecords.cpp
+++ b/engines/nancy/action/inventoryrecords.cpp
@@ -214,6 +214,10 @@ void GoInvViewScene::execute() {
 	if (!disabled && item.keepItem == kInvItemNewSceneView) {
 		if (_addToInventory || NancySceneState.hasItem(_itemID)) {
 			NancySceneState.pushScene(_itemID);
+			// Viewing an item's close-up takes it out of the inventory; the
+			// re-add on return (popScene, or a cancel scene's AddInventory
+			// record) is what plays the "returned to inventory" click.
+			NancySceneState.removeItemFromInventory(_itemID, false);
 		} else {
 			// Do not add the item to the inventory, only go to its scene
 			NancySceneState.pushScene();
diff --git a/engines/nancy/action/puzzle/gridmappuzzle.cpp b/engines/nancy/action/puzzle/gridmappuzzle.cpp
index 6aea72e5bef..de5eb93673e 100644
--- a/engines/nancy/action/puzzle/gridmappuzzle.cpp
+++ b/engines/nancy/action/puzzle/gridmappuzzle.cpp
@@ -254,15 +254,13 @@ void GridMapPuzzle::execute() {
 }
 
 Common::Rect GridMapPuzzle::mapCellRect(int row, int col) const {
-	// Stride uses the raw src-rect dimensions (right - left, before readRect's
-	// inclusive→exclusive +1). The rect spans the full stride so cells tile the
-	// grid with no gaps, keeping the grab-hand cursor steady across the whole
-	// grid. Only blits read the rect's top-left, so sprites still land correctly.
+	// Sprite destination. Stride uses the raw src-rect dimensions (right - left,
+	// before readRect's inclusive→exclusive +1). Blits read the rect's top-left.
 	int strideX = (int)_mapSpacingX + _mapCellW - 1;
 	int strideY = (int)_mapSpacingY + _mapCellH - 1;
 	int x = (int)_mapOriginX + col * strideX;
 	int y = (int)_mapOriginY + row * strideY;
-	return Common::Rect(x, y, x + strideX, y + strideY);
+	return Common::Rect(x, y, x + _mapCellW, y + _mapCellH);
 }
 
 Common::Rect GridMapPuzzle::itemsCellRect(int row, int col) const {
@@ -270,13 +268,34 @@ Common::Rect GridMapPuzzle::itemsCellRect(int row, int col) const {
 	int strideY = (int)_itemsSpacingY + _itemsCellH - 1;
 	int x = (int)_itemsOriginX + col * strideX;
 	int y = (int)_itemsOriginY + row * strideY;
-	return Common::Rect(x, y, x + strideX, y + strideY);
+	return Common::Rect(x, y, x + _itemsCellW, y + _itemsCellH);
+}
+
+Common::Rect GridMapPuzzle::mapCellHitRect(int row, int col) const {
+	// Hit area = sprite rect grown by half the spacing on each side. Neighboring
+	// cells then meet with no gaps, so the whole grid reads as one hand-cursor
+	// region and every point maps to its nearest cell.
+	Common::Rect r = mapCellRect(row, col);
+	r.left   -= (int)_mapSpacingX / 2;
+	r.right  += (int)_mapSpacingX / 2;
+	r.top    -= (int)_mapSpacingY / 2;
+	r.bottom += (int)_mapSpacingY / 2;
+	return r;
+}
+
+Common::Rect GridMapPuzzle::itemsCellHitRect(int row, int col) const {
+	Common::Rect r = itemsCellRect(row, col);
+	r.left   -= (int)_itemsSpacingX / 2;
+	r.right  += (int)_itemsSpacingX / 2;
+	r.top    -= (int)_itemsSpacingY / 2;
+	r.bottom += (int)_itemsSpacingY / 2;
+	return r;
 }
 
 bool GridMapPuzzle::hitTestMap(const Common::Point &p, int &outRow, int &outCol) const {
 	for (int r = 0; r < (int)_mapRows; ++r) {
 		for (int c = 0; c < (int)_mapCols; ++c) {
-			if (mapCellRect(r, c).contains(p)) {
+			if (mapCellHitRect(r, c).contains(p)) {
 				outRow = r;
 				outCol = c;
 				return true;
@@ -289,7 +308,7 @@ bool GridMapPuzzle::hitTestMap(const Common::Point &p, int &outRow, int &outCol)
 bool GridMapPuzzle::hitTestItems(const Common::Point &p, int &outRow, int &outCol) const {
 	for (int r = 0; r < (int)_itemsRows; ++r) {
 		for (int c = 0; c < (int)_itemsCols; ++c) {
-			if (itemsCellRect(r, c).contains(p)) {
+			if (itemsCellHitRect(r, c).contains(p)) {
 				outRow = r;
 				outCol = c;
 				return true;
@@ -315,18 +334,6 @@ int GridMapPuzzle::findItemInItems(int row, int col) const {
 	return -1;
 }
 
-bool GridMapPuzzle::isValidMapSlot(int row, int col) const {
-	// A map cell is a real placement slot iff at least one solution places
-	// some item there. Empty map cells outside any solution reject drops.
-	for (int s = 0; s < (int)_numSolutions; ++s) {
-		for (int i = 0; i < (int)_numItems; ++i) {
-			if (_solutionRows[s][i] == (int16)row && _solutionCols[s][i] == (int16)col)
-				return true;
-		}
-	}
-	return false;
-}
-
 Common::Rect GridMapPuzzle::resultsCellRect(int row, int col) const {
 	int x = (int)_resultsOriginX + col * ((int)_resultsSpacingX + _resultsCellW - 1);
 	int y = (int)_resultsOriginY + row * ((int)_resultsSpacingY + _resultsCellH - 1);
@@ -397,13 +404,10 @@ void GridMapPuzzle::handleInput(NancyInput &input) {
 			g_nancy->_sound->playSound(_pickupSound);
 		}
 	} else {
-		// Drop. Map cells only accept the held item if they are real
-		// placement slots (i.e. used by some solution); items cells always
-		// accept. An occupied cell sends its current occupant back to the
-		// cursor so the player can swap.
-		if (hitMap && !isValidMapSlot(row, col))
-			return;
-
+		// Drop. Any map or items cell accepts the held item; the wrong
+		// placement just decodes to the wrong letters in the results strip.
+		// An occupied cell sends its current occupant back to the cursor so
+		// the player can swap.
 		if (existingItem != -1) {
 			if (hitMap) {
 				_items[existingItem].inMap = false;
@@ -520,14 +524,14 @@ void GridMapPuzzle::redraw() {
 		}
 	}
 
+	// The glyph following the cursor uses the small map sprite (on the board
+	// image), not the large items-grid sprite.
 	if (_heldItem >= 0 && _heldItem < (int)_numItems && !_skipHeldDraw) {
-		const Common::Rect &src = _itemsItemSrcRects[_heldItem].isEmpty()
-		                          ? _mapItemSrcRects[_heldItem]
-		                          : _itemsItemSrcRects[_heldItem];
+		const Common::Rect &src = _mapItemSrcRects[_heldItem];
 		if (!src.isEmpty()) {
 			int x = _heldDrawPos.x - src.width()  / 2;
 			int y = _heldDrawPos.y - src.height() / 2;
-			_drawSurface.blitFrom(_cursorImage, src, Common::Point(x, y));
+			_drawSurface.blitFrom(_boardImage, src, Common::Point(x, y));
 		}
 	}
 
diff --git a/engines/nancy/action/puzzle/gridmappuzzle.h b/engines/nancy/action/puzzle/gridmappuzzle.h
index 0a0b6c53ba1..c0c964cab45 100644
--- a/engines/nancy/action/puzzle/gridmappuzzle.h
+++ b/engines/nancy/action/puzzle/gridmappuzzle.h
@@ -146,12 +146,13 @@ protected:
 	void redraw();
 	Common::Rect mapCellRect(int row, int col) const;
 	Common::Rect itemsCellRect(int row, int col) const;
+	Common::Rect mapCellHitRect(int row, int col) const;
+	Common::Rect itemsCellHitRect(int row, int col) const;
 	Common::Rect resultsCellRect(int row, int col) const;
 	bool hitTestMap(const Common::Point &p, int &outRow, int &outCol) const;
 	bool hitTestItems(const Common::Point &p, int &outRow, int &outCol) const;
 	int findItemInMap(int row, int col) const;
 	int findItemInItems(int row, int col) const;
-	bool isValidMapSlot(int row, int col) const;
 	void checkSolved();
 };
 
diff --git a/engines/nancy/ui/inventorypopup.cpp b/engines/nancy/ui/inventorypopup.cpp
index 7009e5108c9..3b9b8bf482c 100644
--- a/engines/nancy/ui/inventorypopup.cpp
+++ b/engines/nancy/ui/inventorypopup.cpp
@@ -485,6 +485,9 @@ void InventoryPopup::handleInput(NancyInput &input) {
 
 			if (input.input & NancyInput::kLeftMouseButtonUp) {
 				NancySceneState.addItemToInventory(heldItem);
+				// Dropping a held item back plays the button-down sound
+				// layered over the item-added sound.
+				g_nancy->_sound->playSound("BUDE");
 			}
 			input.eatMouseInput();
 		}


Commit: 957fd44b78b6b86d6b810e20a19f5d703738c19b
    https://github.com/scummvm/scummvm/commit/957fd44b78b6b86d6b810e20a19f5d703738c19b
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-26T05:35:49+03:00

Commit Message:
NANCY: NANCY10: Fix cursor when placing pieces in OneBuildPuzzle

The cursor would incorrectly flash as a magnifying glass when putting
down pieces in places that are not the solution positions.

Fix #16991

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


diff --git a/engines/nancy/action/puzzle/onebuildpuzzle.cpp b/engines/nancy/action/puzzle/onebuildpuzzle.cpp
index 114f7646558..23dc6d00f28 100644
--- a/engines/nancy/action/puzzle/onebuildpuzzle.cpp
+++ b/engines/nancy/action/puzzle/onebuildpuzzle.cpp
@@ -529,11 +529,9 @@ void OneBuildPuzzle::handleInput(NancyInput &input) {
 		return;
 	}
 
-	// Not dragging: only process when idle
-	if (_solveState != kIdle)
-		return;
-
-	// Find topmost piece under cursor (separately tracking unplaced vs any)
+	// Not dragging: find the topmost piece under the cursor. The hover cursor is
+	// refreshed even while a drop/placement sound plays (non-idle) so a piece put
+	// down off-target keeps the piece cursor; only clicks are gated on kIdle.
 	int16 topmostUnplaced = -1;
 	int16 topmostAny = -1;
 
@@ -553,6 +551,10 @@ void OneBuildPuzzle::handleInput(NancyInput &input) {
 		if (topmostUnplaced != -1)
 			setPieceCursor();
 
+		// Clicks are only processed when idle
+		if (_solveState != kIdle)
+			return;
+
 		// Left click on an unplaced piece: pick it up
 		// Right click: pick it up and rotate it
 		bool leftClick = (input.input & NancyInput::kLeftMouseButtonUp);
@@ -576,6 +578,10 @@ void OneBuildPuzzle::handleInput(NancyInput &input) {
 		return;
 	}
 
+	// Nothing else is interactive while a drop/placement sound plays
+	if (_solveState != kIdle)
+		return;
+
 	// Check exit hotspot
 	Common::Rect exitScreen = NancySceneState.getViewport().convertViewportToScreen(_exitHotspot);
 	if (exitScreen.contains(input.mousePos)) {


Commit: 85ea8f5e81bf526c53314ccd397af1231713c21a
    https://github.com/scummvm/scummvm/commit/85ea8f5e81bf526c53314ccd397af1231713c21a
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-26T05:35:51+03:00

Commit Message:
NANCY: Handle common sounds from the console play_sound command

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


diff --git a/engines/nancy/console.cpp b/engines/nancy/console.cpp
index 9f076c414e2..a082a464be2 100644
--- a/engines/nancy/console.cpp
+++ b/engines/nancy/console.cpp
@@ -414,6 +414,11 @@ bool NancyConsole::Cmd_playSound(int argc, const char **argv) {
 		return true;
 	}
 
+	if (g_nancy->_sound->isCommonSound(argv[1])) {
+		g_nancy->_sound->playSound(argv[1]);
+		return true;
+	}
+
 	Common::File *f = new Common::File;
 	if (!f->open(Common::Path(argv[1]).appendInPlace(".his"))) {
 		debugPrintf("Failed to open '%s.his'\n", argv[1]);
diff --git a/engines/nancy/sound.h b/engines/nancy/sound.h
index 01594f92e11..fc047e7c46b 100644
--- a/engines/nancy/sound.h
+++ b/engines/nancy/sound.h
@@ -122,6 +122,8 @@ public:
 	Audio::Timestamp getLength(const SoundDescription &description);
 	Audio::Timestamp getLength(const Common::String &chunkName);
 
+	bool isCommonSound(const Common::String &soundName) const { return _commonSounds.contains(soundName); }
+
 	void soundEffectMaintenance();
 	void recalculateSoundEffects();
 


Commit: 6808a7a71a144c5086007493d88988d851c198b3
    https://github.com/scummvm/scummvm/commit/6808a7a71a144c5086007493d88988d851c198b3
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-26T05:35:52+03:00

Commit Message:
NANCY: NANCY10: Cleanup comments

Changed paths:
    engines/nancy/ui/textbox.h


diff --git a/engines/nancy/ui/textbox.h b/engines/nancy/ui/textbox.h
index 23080039b12..83894ba5b02 100644
--- a/engines/nancy/ui/textbox.h
+++ b/engines/nancy/ui/textbox.h
@@ -54,7 +54,6 @@ public:
 
 	// Nancy 10 only: picks the full, taskbar-covering box (AR 74) over the strip
 	// (AR 75). No effect on Nancy 1-9 (no such mode) or Nancy 11+ (a single box).
-	// The AR owns the full box's auto-close timer.
 	void setFullMode(bool open);
 	// True while the text box visually covers the taskbar buttons, so Scene can
 	// skip taskbar input. Always false before Nancy 10, whose box sits clear of them.


Commit: f4cfed09bcfb1dcde3e16126e95c19be24cb7cb8
    https://github.com/scummvm/scummvm/commit/f4cfed09bcfb1dcde3e16126e95c19be24cb7cb8
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-26T05:35:53+03:00

Commit Message:
NANCY: Activate all AR records when loading a game

The original re-runs every scene action record on load, without
persisting per-record state, which is exactly what happens when
entering a scene.

This probably won't introduce regressions, but it should be tested
just in case. Some cursory tests showed no issues.

Fix #16892

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


diff --git a/engines/nancy/action/actionmanager.cpp b/engines/nancy/action/actionmanager.cpp
index f062244c793..1c56956ae17 100644
--- a/engines/nancy/action/actionmanager.cpp
+++ b/engines/nancy/action/actionmanager.cpp
@@ -631,8 +631,15 @@ void ActionManager::synchronize(Common::Serializer &ser) {
 		ser.syncAsByte(rec->_isActive);
 		ser.syncAsByte(rec->_isDone);
 
-		// Forcefully re-activate Autotext records, since we need to regenerate the surface
-		if (ser.isLoading() && g_nancy->getGameType() >= kGameTypeNancy6 && rec->_type == 61) {
+		if (ser.isLoading()) {
+			// Records restart fresh on load, just like a normal scene entry: clearing
+			// _isDone lets one-shot records run again -- chained tutorial narration
+			// sounds, conversations, overlays, etc. Anything that must stay "done" is
+			// gated by its own dependencies (event flags, scene counts, inventory),
+			// which are restored separately. Without this, a sound or conversation
+			// that had finished before the save stays silent on load (e.g. the
+			// Nancy 10 movement tutorial). _isActive is recomputed from the record's
+			// dependencies on the next frame.
 			rec->_isDone = false;
 		}
 	}




More information about the Scummvm-git-logs mailing list