[Scummvm-git-logs] scummvm master -> 3ba943006aef1c77eab0eb5dde7619cbccaef279

bluegr noreply at scummvm.org
Sun Jul 19 02:02:08 UTC 2026


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

Summary:
4f18a6819d NANCY: NANCY10: Add a frame cache for Bink panorama videos
7d0c3a21a4 NANCY: NANCY12: Implement new conditional dialog system
2523e0c1e9 NANCY: NANCY10: Don't depress taskbar icons with notification badges
6467156ddd NANCY: Use correct cursor for main menu, setup menu and new game prompt
2b38fde154 NANCY: NANCY10: Add click sound when viewing an item in the inventory
556004256d NANCY: NANCY10: More performance fixes for scrolling
b962792797 NANCY: Copy save name to input box on click
df65b11973 NANCY: Show a confirmation when starting new game while playing a game
ca2a2388e4 NANCY: NANCY10: Fixes for the cellphone browser
3ba943006a NANCY: NANCY10: Fix badge notification when returning from a closeup


Commit: 4f18a6819d5eaca8d8bfe2885cd06c12df6b1203
    https://github.com/scummvm/scummvm/commit/4f18a6819d5eaca8d8bfe2885cd06c12df6b1203
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-19T05:01:38+03:00

Commit Message:
NANCY: NANCY10: Add a frame cache for Bink panorama videos

Improves performance when scrubbing left and right in scene panoramas.
Fix #16966

Changed paths:
    engines/nancy/movieplayer.cpp
    engines/nancy/movieplayer.h
    engines/nancy/ui/viewport.cpp


diff --git a/engines/nancy/movieplayer.cpp b/engines/nancy/movieplayer.cpp
index bb9727d5e65..10fe1e814bf 100644
--- a/engines/nancy/movieplayer.cpp
+++ b/engines/nancy/movieplayer.cpp
@@ -36,6 +36,8 @@ MoviePlayer::MoviePlayer() {}
 MoviePlayer::~MoviePlayer() {}
 
 bool MoviePlayer::loadFile(const Common::Path &name, bool bidirectionalCache) {
+	freeFrameCache();
+
 	const Common::Path avfPath = name.append(".avf");
 	const Common::Path bikPath = name.append(".bik");
 
@@ -57,6 +59,13 @@ bool MoviePlayer::loadFile(const Common::Path &name, bool bidirectionalCache) {
 		_decoder.reset();
 		return false;
 	}
+
+	// The AVF decoder caches frames itself, so only the Bink path needs ours.
+	_useFrameCache = bidirectionalCache && _videoType == kVideoPlaytypeBink;
+	if (_useFrameCache) {
+		_frameCache.resize(_decoder->getFrameCount());
+	}
+
 	return true;
 }
 
@@ -66,11 +75,20 @@ bool MoviePlayer::isVideoLoaded() const {
 
 void MoviePlayer::close() {
 	_currentSurface = nullptr;
+	freeFrameCache();
 	if (_decoder) {
 		_decoder->close();
 	}
 }
 
+void MoviePlayer::freeFrameCache() {
+	for (Graphics::Surface &surf : _frameCache) {
+		surf.free();
+	}
+	_frameCache.clear();
+	_useFrameCache = false;
+}
+
 void MoviePlayer::start()					{ if (_decoder) _decoder->start(); }
 void MoviePlayer::stop()					{ if (_decoder) _decoder->stop(); }
 void MoviePlayer::pauseVideo(bool pause)	{ if (_decoder) _decoder->pauseVideo(pause); }
@@ -119,8 +137,26 @@ const Graphics::Surface *MoviePlayer::decodeNextFrame(int frameNr) {
 		return frame;
 	}
 
-	_decoder->seekToFrame(frameNr);
-	return _decoder->decodeNextFrame();
+	// Bink path.
+	if (_useFrameCache && (uint)frameNr < _frameCache.size() && _frameCache[frameNr].getPixels()) {
+		return &_frameCache[frameNr];
+	}
+
+	// seekToFrame() re-decodes from the previous keyframe, so only seek when the
+	// frame can't be reached by decoding forward one step.
+	if (_decoder->getCurFrame() + 1 != frameNr) {
+		_decoder->seekToFrame(frameNr);
+	}
+
+	const Graphics::Surface *frame = _decoder->decodeNextFrame();
+
+	// The decoder reuses one surface per frame, so cache a copy.
+	if (_useFrameCache && frame && (uint)frameNr < _frameCache.size()) {
+		_frameCache[frameNr].copyFrom(*frame);
+		return &_frameCache[frameNr];
+	}
+
+	return frame;
 }
 
 // --- Simple frame-range player ------------------------------------------
diff --git a/engines/nancy/movieplayer.h b/engines/nancy/movieplayer.h
index 6b32b5b923d..b8413882f92 100644
--- a/engines/nancy/movieplayer.h
+++ b/engines/nancy/movieplayer.h
@@ -22,6 +22,7 @@
 #ifndef NANCY_MOVIEPLAYER_H
 #define NANCY_MOVIEPLAYER_H
 
+#include "common/array.h"
 #include "common/ptr.h"
 #include "common/path.h"
 #include "common/rational.h"
@@ -57,7 +58,8 @@ public:
 
 	// Load <name> + ".avf"/".bik", auto-detecting the format from which file
 	// exists (AVF preferred if both do) and creating the matching decoder.
-	// bidirectionalCache selects the AVF frame-cache hint (for scrubbing).
+	// bidirectionalCache enables fast bidirectional scrubbing; pass it only for
+	// scrubbed panorama scenes.
 	bool loadFile(const Common::Path &name, bool bidirectionalCache = false);
 	bool isVideoLoaded() const;
 	void close();
@@ -86,8 +88,8 @@ public:
 	void addFrameTime(uint16 timeToAdd);	// AVF only, no-op otherwise
 
 	// Decode a frame: frameNr < 0 returns the next frame; otherwise that
-	// specific frame via the format-appropriate cached path (AVF decodeFrame +
-	// seek; Bink seekToFrame + decodeNextFrame).
+	// specific frame via the format-appropriate cached path (AVF decodeFrame;
+	// Bink decodes forward when possible and caches frames, see _frameCache).
 	const Graphics::Surface *decodeNextFrame(int frameNr = -1);
 
 	// --- Optional simple frame-range player, built on the above. Handy for
@@ -103,10 +105,17 @@ public:
 
 private:
 	void storeCurrentFrame();
+	void freeFrameCache();
 
 	Common::ScopedPtr<Video::VideoDecoder> _decoder;
 	byte _videoType = kVideoPlaytypeAVF;
 
+	// Decoded-frame cache for the Bink path (AVF caches internally). Bink seeking
+	// re-decodes from the previous keyframe, so caching keeps panorama scrubbing
+	// fast. Enabled only when loadFile() is asked for a bidirectional cache.
+	bool _useFrameCache = false;
+	Common::Array<Graphics::Surface> _frameCache;
+
 	// Simple frame-range player state. _currentSurface points at the decoder's
 	// last-decoded frame (owned by it, valid until the next decode).
 	const Graphics::Surface *_currentSurface = nullptr;
diff --git a/engines/nancy/ui/viewport.cpp b/engines/nancy/ui/viewport.cpp
index 1a4be6fc8c0..16a76a1819e 100644
--- a/engines/nancy/ui/viewport.cpp
+++ b/engines/nancy/ui/viewport.cpp
@@ -222,9 +222,10 @@ void Viewport::loadVideo(const Common::Path &filename, uint frameNr, uint vertic
 		_decoder.close();
 	}
 
-	// The scene video uses the bidirectional AVF frame cache so scrubbing in
-	// both directions is fast; the player selects AVF/Bink by which file exists.
-	if (!_decoder.loadFile(filename, true)) {
+	// Only panorama scenes step through frames, so only they need the frame cache
+	// for fast bidirectional scrubbing; other scenes would just waste memory.
+	const bool isPanorama = panningType == kPan360 || panningType == kPanLeftRight;
+	if (!_decoder.loadFile(filename, isPanorama)) {
 		error("Couldn't load video file %s.avf or %s.bik", filename.toString().c_str(), filename.toString().c_str());
 	}
 


Commit: 7d0c3a21a47a840f28c5a74ce45b66144bc30279
    https://github.com/scummvm/scummvm/commit/7d0c3a21a47a840f28c5a74ce45b66144bc30279
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-19T05:01:39+03:00

Commit Message:
NANCY: NANCY12: Implement new conditional dialog system

Conditional dialog data is now stored in game scenes, instead of being
hardcoded in the executable: 8xx for dialogs and 9xx for goodbyes

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


diff --git a/engines/nancy/action/arfactory.cpp b/engines/nancy/action/arfactory.cpp
index b4d518e4925..7bdbc855876 100644
--- a/engines/nancy/action/arfactory.cpp
+++ b/engines/nancy/action/arfactory.cpp
@@ -196,6 +196,10 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
 		return new StartPlayerScrolling();
 	case 32:	// Nancy10
 		return new UIPopupPrepScene();
+	case 35:	// Nancy12
+		return new ConversationInfoCheck();
+	case 36:	// Nancy12
+		return new ConversationGoodbye();
 	case 40:
 		if (g_nancy->getGameType() <= kGameTypeNancy1)
 			return new LightningOn(); // Only used in TVD
diff --git a/engines/nancy/action/conversation.cpp b/engines/nancy/action/conversation.cpp
index 03b183e471b..8c1f4889866 100644
--- a/engines/nancy/action/conversation.cpp
+++ b/engines/nancy/action/conversation.cpp
@@ -30,8 +30,10 @@
 #include "engines/nancy/util.h"
 #include "engines/nancy/graphics.h"
 #include "engines/nancy/resource.h"
+#include "engines/nancy/iff.h"
 
 #include "engines/nancy/action/conversation.h"
+#include "engines/nancy/action/actionmanager.h"
 #include "engines/nancy/action/secondarymovie.h"
 
 #include "engines/nancy/state/scene.h"
@@ -493,9 +495,7 @@ void ConversationSound::execute() {
 
 void ConversationSound::addConditionalDialogue() {
 	if (g_nancy->getGameType() >= kGameTypeNancy12) {
-		// TODO: Nancy12+ moved the per-character conditional dialogue out
-		// of the executable and into separate data files.
-		warning("Conditional dialogue for character %d is not implemented in Nancy12+", _conditionalResponseCharacterID);
+		addConditionalDialogueNancy12();
 		return;
 	}
 
@@ -570,9 +570,7 @@ void ConversationSound::addConditionalDialogue() {
 
 void ConversationSound::addGoodbye() {
 	if (g_nancy->getGameType() >= kGameTypeNancy12) {
-		// TODO: Nancy12+ moved the per-character goodbye dialogue out
-		// of the executable and into separate data files.
-		warning("Goodbye dialogue for character %d is not implemented in Nancy12+", _goodbyeResponseCharacterID);
+		addGoodbyeNancy12();
 		return;
 	}
 
@@ -654,6 +652,133 @@ void ConversationSound::addGoodbye() {
 	NancySceneState.setEventFlag(sceneChange.flagToSet.label, sceneChange.flagToSet.flag);
 }
 
+void ConversationSound::addConditionalDialogueNancy12() {
+	// Every action record in scene S<800 + charID> is one conditional response
+	Common::Path sceneName(Common::String::format("S%u", 800 + _conditionalResponseCharacterID));
+	IFF *iff = g_nancy->_resource->loadIFF(sceneName);
+	if (!iff) {
+		warning("Could not load conditional dialogue scene %s", sceneName.toString().c_str());
+		return;
+	}
+
+	// Response text is in the CONVO file's CVTX chunk, keyed by sound ID
+	const CVTX *convo = (const CVTX *)g_nancy->getEngineData("CONVO");
+	assert(convo);
+
+	Common::SeekableReadStream *chunk = nullptr;
+	for (uint i = 0; (chunk = iff->getChunkStream("ACT", i)) != nullptr; ++i) {
+		ActionRecord *record = ActionManager::createAndLoadNewRecord(*chunk);
+		delete chunk;
+
+		ConversationInfoCheck *infoCheck = dynamic_cast<ConversationInfoCheck *>(record);
+		if (infoCheck) {
+			NancySceneState.getActionManager().processDependency(infoCheck->_dependencies, *infoCheck, true);
+
+			if (infoCheck->_dependencies.satisfied) {
+				_responses.push_back(ResponseStruct());
+				ResponseStruct &newResponse = _responses.back();
+				newResponse.soundName = infoCheck->_soundID;
+				newResponse.text = convo->texts[infoCheck->_soundID];
+				newResponse.sceneChange.sceneID = infoCheck->_sceneID;
+				newResponse.sceneChange.continueSceneSound = kContinueSceneSound;
+				newResponse.sceneChange.listenerFrontVector.set(0, 0, 1);
+
+				// Drop the response if it's a repeat
+				for (uint j = 0; j < _responses.size() - 1; ++j) {
+					if (	_responses[j].soundName == newResponse.soundName &&
+							_responses[j].text == newResponse.text &&
+							_responses[j].sceneChange.sceneID == newResponse.sceneChange.sceneID) {
+						_responses.pop_back();
+						break;
+					}
+				}
+			}
+		}
+
+		delete record;
+	}
+
+	delete iff;
+}
+
+void ConversationSound::addGoodbyeNancy12() {
+	// Use the first satisfied goodbye record in scene S<900 + charID>
+	Common::Path sceneName(Common::String::format("S%u", 900 + _goodbyeResponseCharacterID));
+	IFF *iff = g_nancy->_resource->loadIFF(sceneName);
+	if (!iff) {
+		warning("Could not load goodbye scene %s", sceneName.toString().c_str());
+		return;
+	}
+
+	// Response text is in the CONVO file's CVTX chunk, keyed by sound ID
+	const CVTX *convo = (const CVTX *)g_nancy->getEngineData("CONVO");
+	assert(convo);
+
+	bool added = false;
+	Common::SeekableReadStream *chunk = nullptr;
+	for (uint i = 0; !added && (chunk = iff->getChunkStream("ACT", i)) != nullptr; ++i) {
+		ActionRecord *record = ActionManager::createAndLoadNewRecord(*chunk);
+		delete chunk;
+
+		ConversationGoodbye *goodbye = dynamic_cast<ConversationGoodbye *>(record);
+		if (goodbye && !goodbye->_soundIDs.empty() && !goodbye->_sceneIDs.empty()) {
+			NancySceneState.getActionManager().processDependency(goodbye->_dependencies, *goodbye, true);
+
+			if (goodbye->_dependencies.satisfied) {
+				_responses.push_back(ResponseStruct());
+				ResponseStruct &newResponse = _responses.back();
+
+				const Common::String &soundID = goodbye->_soundIDs[g_nancy->_randomSource->getRandomNumber(goodbye->_soundIDs.size() - 1)];
+				newResponse.soundName = soundID;
+				newResponse.text = convo->texts[soundID];
+				newResponse.sceneChange.sceneID = goodbye->_sceneIDs[g_nancy->_randomSource->getRandomNumber(goodbye->_sceneIDs.size() - 1)];
+				newResponse.sceneChange.continueSceneSound = kContinueSceneSound;
+				newResponse.sceneChange.listenerFrontVector.set(0, 0, 1);
+
+				added = true;
+			}
+		}
+
+		delete record;
+	}
+
+	delete iff;
+}
+
+void ConversationInfoCheck::readData(Common::SeekableReadStream &stream) {
+	readFilename(stream, _soundID);
+	_sceneID = stream.readUint16LE();
+}
+
+void ConversationGoodbye::readData(Common::SeekableReadStream &stream) {
+	if (g_nancy->getGameType() == kGameTypeNancy12) {
+		// A single sound, then a count and a fixed set of 5 candidate scenes
+		_soundIDs.resize(1);
+		readFilename(stream, _soundIDs[0]);
+
+		uint16 numScenes = stream.readUint16LE();
+		_sceneIDs.resize(numScenes);
+		for (uint i = 0; i < 5; ++i) {
+			uint16 sceneID = stream.readUint16LE();
+			if (i < numScenes) {
+				_sceneIDs[i] = sceneID;
+			}
+		}
+
+		return;
+	}
+
+	// Nancy13+: a count-prefixed list of sounds, then one of scenes
+	uint16 numSounds = stream.readUint16LE();
+	readFilenameArray(stream, _soundIDs, numSounds);
+
+	uint16 numScenes = stream.readUint16LE();
+	_sceneIDs.resize(numScenes);
+	for (uint i = 0; i < numScenes; ++i) {
+		_sceneIDs[i] = stream.readUint16LE();
+	}
+}
+
 void ConversationSound::ConversationFlag::read(Common::SeekableReadStream &stream) {
 	type = stream.readByte();
 	flag.label = stream.readSint16LE();
diff --git a/engines/nancy/action/conversation.h b/engines/nancy/action/conversation.h
index ea33c8f72a7..8d15bda741c 100644
--- a/engines/nancy/action/conversation.h
+++ b/engines/nancy/action/conversation.h
@@ -36,9 +36,8 @@ namespace Action {
 // A conversation will auto-advance to a next scene when no responses are available; the next scene
 // can either be described within the Conversation data, or can be whatever's pushed onto the scene "stack".
 // Also supports branching scenes depending on a condition, though that is only used in older games.
-// Player responses can also be conditional; the original engine had special-purpose "infocheck"
-// functions, two per character ID, which were used to evaluate those conditions. We replace that with
-// the data bundled inside nancy.dat (see devtools/create_nancy).
+// Player responses can also be conditional. Up to Nancy11 the condition data comes from nancy.dat
+// (see devtools/create_nancy); from Nancy12 on it lives in per-character data scenes (S<800 + charID> / S<900 + charID>).
 class ConversationSound : public RenderActionRecord {
 public:
 	ConversationSound();
@@ -115,9 +114,11 @@ protected:
 	void readDataNancy13(Common::SeekableReadStream &stream);
 	virtual void readCelDataNancy13(Common::SeekableReadStream &stream);
 
-	// Functions for handling the built-in dialogue responses found in the executable
+	// Add conditional/goodbye responses: from nancy.dat up to Nancy11, from data scenes for Nancy12+
 	void addConditionalDialogue();
 	void addGoodbye();
+	void addConditionalDialogueNancy12();
+	void addGoodbyeNancy12();
 
 	Common::String _text;
 
@@ -263,6 +264,26 @@ protected:
 	Common::String getRecordTypeName() const override { return "ConversationCelTerse"; }
 };
 
+// Nancy12+ conditional dialogue record (type 35), one per response, stored in scene S<800 + charID>
+class ConversationInfoCheck : public ActionRecord {
+public:
+	void readData(Common::SeekableReadStream &stream) override;
+	Common::String getRecordTypeName() const override { return "ConversationInfoCheck"; }
+
+	Common::String _soundID; // Doubles as the CONVO text key
+	uint16 _sceneID = 0;
+};
+
+// Nancy12+ goodbye record (type 36), stored in scene S<900 + charID>; one sound and scene are picked at random
+class ConversationGoodbye : public ActionRecord {
+public:
+	void readData(Common::SeekableReadStream &stream) override;
+	Common::String getRecordTypeName() const override { return "ConversationGoodbye"; }
+
+	Common::Array<Common::String> _soundIDs;
+	Common::Array<uint16> _sceneIDs;
+};
+
 } // End of namespace Action
 } // End of namespace Nancy
 


Commit: 2523e0c1e9d9c9a793d6197cd3f8f28d95ebb48b
    https://github.com/scummvm/scummvm/commit/2523e0c1e9d9c9a793d6197cd3f8f28d95ebb48b
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-19T05:01:40+03:00

Commit Message:
NANCY: NANCY10: Don't depress taskbar icons with notification badges

Fix #16971

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


diff --git a/engines/nancy/ui/taskbar.cpp b/engines/nancy/ui/taskbar.cpp
index 4690fa7272e..c3129ecea9d 100644
--- a/engines/nancy/ui/taskbar.cpp
+++ b/engines/nancy/ui/taskbar.cpp
@@ -475,16 +475,22 @@ void Taskbar::handleInput(NancyInput &input) {
 		return;
 	}
 
+	// A badged (highlighted) button never depresses: it keeps showing its
+	// notification sprite through the press and release. Only non-highlighted
+	// buttons swap to the pressed sprite while held.
+	const bool isBadged = restingState(newHovered) == kButtonNotification;
+
 	if (input.input & NancyInput::kLeftMouseButtonDown) {
 		// Mouse pressed: show the pressed sprite for the duration of the hold.
-		if (_buttonStates[newHovered] != kButtonPressed) {
+		if (!isBadged && _buttonStates[newHovered] != kButtonPressed) {
 			drawButton(newHovered, kButtonPressed);
 		}
 	} else if (input.input & NancyInput::kLeftMouseButtonUp) {
 		// Mouse released over the button: trigger the click action and
-		// snap the sprite back to hover (the cursor is still over it).
-		// Badges are cleared per-view inside the popup, not on click.
-		drawButton(newHovered, kButtonHover);
+		// snap the sprite back to its resting hover state (the cursor is still
+		// over it). Badges are cleared per-view inside the popup, not on click,
+		// so a badged button keeps its badge here.
+		drawButton(newHovered, isBadged ? kButtonNotification : kButtonHover);
 		_clickedButton = newHovered;
 
 		playClickSound(newHovered);


Commit: 6467156dddf609f9e0b4c297c2a86dd7f1d024ad
    https://github.com/scummvm/scummvm/commit/6467156dddf609f9e0b4c297c2a86dd7f1d024ad
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-19T05:01:40+03:00

Commit Message:
NANCY: Use correct cursor for main menu, setup menu and new game prompt

Fix #16978

Changed paths:
    engines/nancy/state/mainmenu.cpp
    engines/nancy/state/savedialog.cpp
    engines/nancy/state/setupmenu.cpp


diff --git a/engines/nancy/state/mainmenu.cpp b/engines/nancy/state/mainmenu.cpp
index 79c6778b4ed..3ee58e4f5f9 100644
--- a/engines/nancy/state/mainmenu.cpp
+++ b/engines/nancy/state/mainmenu.cpp
@@ -84,7 +84,7 @@ void MainMenu::init() {
 	_background.init(_menuData->_imageName);
 	_background.registerGraphics();
 
-	g_nancy->_cursor->setCursorType(CursorManager::kNormalArrow);
+	g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
 	g_nancy->setMouseEnabled(true);
 
 	if (!g_nancy->_sound->isSoundPlaying("MSND")) {
@@ -161,7 +161,7 @@ void MainMenu::run() {
 		}
 	}
 
-	g_nancy->_cursor->setCursorType(CursorManager::kNormalArrow);
+	g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
 }
 
 void MainMenu::stop() {
diff --git a/engines/nancy/state/savedialog.cpp b/engines/nancy/state/savedialog.cpp
index f58eea0049b..1ecab823ca7 100644
--- a/engines/nancy/state/savedialog.cpp
+++ b/engines/nancy/state/savedialog.cpp
@@ -55,7 +55,7 @@ void SaveDialog::process() {
 		break;
 	}
 
-	g_nancy->_cursor->setCursorType(CursorManager::kNormalArrow);
+	g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
 }
 
 void SaveDialog::onStateEnter(const NancyState::NancyState prevState) {
diff --git a/engines/nancy/state/setupmenu.cpp b/engines/nancy/state/setupmenu.cpp
index a08788e2f6b..f65455951a7 100644
--- a/engines/nancy/state/setupmenu.cpp
+++ b/engines/nancy/state/setupmenu.cpp
@@ -129,7 +129,7 @@ void SetupMenu::init() {
 
 	_background.registerGraphics();
 
-	g_nancy->_cursor->setCursorType(CursorManager::kNormalArrow);
+	g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
 	g_nancy->setMouseEnabled(true);
 
 	g_nancy->_sound->stopSound("MSND");
@@ -234,7 +234,7 @@ void SetupMenu::run() {
 		}
 	}
 
-	g_nancy->_cursor->setCursorType(CursorManager::kNormalArrow);
+	g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
 }
 
 void SetupMenu::stop() {


Commit: 2b38fde154ff8eca435c05fa8c5062a9139d1d44
    https://github.com/scummvm/scummvm/commit/2b38fde154ff8eca435c05fa8c5062a9139d1d44
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-19T05:01:41+03:00

Commit Message:
NANCY: NANCY10: Add click sound when viewing an item in the inventory

Fix #16982

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


diff --git a/engines/nancy/ui/inventorypopup.cpp b/engines/nancy/ui/inventorypopup.cpp
index 5c57e45d451..11d19426fe0 100644
--- a/engines/nancy/ui/inventorypopup.cpp
+++ b/engines/nancy/ui/inventorypopup.cpp
@@ -533,6 +533,7 @@ void InventoryPopup::handleInput(NancyInput &input) {
 			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.
+				g_nancy->_sound->playSound("BUOK");
 				NancySceneState.pushScene(itemID);
 				SceneChangeDescription sceneChange;
 				sceneChange.sceneID = item.sceneID;


Commit: 556004256d62331c6af6139d089f9fba3dba612d
    https://github.com/scummvm/scummvm/commit/556004256d62331c6af6139d089f9fba3dba612d
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-19T05:01:42+03:00

Commit Message:
NANCY: NANCY10: More performance fixes for scrolling

Fix #16973

Changed paths:
    engines/nancy/ui/conversationpopup.cpp
    engines/nancy/ui/inventorypopup.cpp
    engines/nancy/ui/notebookpopup.cpp


diff --git a/engines/nancy/ui/conversationpopup.cpp b/engines/nancy/ui/conversationpopup.cpp
index 4660c8071a0..069dbef981b 100644
--- a/engines/nancy/ui/conversationpopup.cpp
+++ b/engines/nancy/ui/conversationpopup.cpp
@@ -236,11 +236,17 @@ void ConversationPopup::handleInput(NancyInput &input) {
 
 			const int newThumbTop = localMouse.y - _scrollbarGrabOffset;
 			const int clamped = CLIP<int>(newThumbTop, trackLocal.top, trackLocal.top + travel);
-			_scrollPos = travel > 0 ? (float)(clamped - trackLocal.top) / (float)travel : 0.0f;
-
-			drawBackground();
-			drawContent();
-			drawScrollbar(kUIButtonPressed);
+			const float newScrollPos = travel > 0 ? (float)(clamped - trackLocal.top) / (float)travel : 0.0f;
+
+			// Only re-render when the thumb actually moves. drawContent() re-lays out
+			// the whole text surface, so calling it every frame while the button is
+			// merely held down pins the CPU and makes dragging choppy.
+			if (newScrollPos != _scrollPos) {
+				_scrollPos = newScrollPos;
+				drawBackground();
+				drawContent();
+				drawScrollbar(kUIButtonPressed);
+			}
 
 			if (input.input & NancyInput::kLeftMouseButtonUp) {
 				_scrollbarDragging = false;
diff --git a/engines/nancy/ui/inventorypopup.cpp b/engines/nancy/ui/inventorypopup.cpp
index 11d19426fe0..16fc372ab15 100644
--- a/engines/nancy/ui/inventorypopup.cpp
+++ b/engines/nancy/ui/inventorypopup.cpp
@@ -407,9 +407,16 @@ void InventoryPopup::handleInput(NancyInput &input) {
 
 			const int newThumbTop = chunkMouse.y - _scrollbarGrabOffset;
 			const int clamped = CLIP<int>(newThumbTop, track.top, track.top + travel);
-			_scrollPos = travel > 0 ? (float)(clamped - track.top) / (float)travel : 0.0f;
-			updatePageFromScroll();
-			refreshGrid();
+			const float newScrollPos = travel > 0 ? (float)(clamped - track.top) / (float)travel : 0.0f;
+
+			// Only re-render when the thumb actually moves. refreshGrid() redraws the
+			// whole popup, so calling it every frame while the button is merely held
+			// down pins the CPU and makes dragging choppy.
+			if (newScrollPos != _scrollPos) {
+				_scrollPos = newScrollPos;
+				updatePageFromScroll();
+				refreshGrid();
+			}
 
 			if (input.input & NancyInput::kLeftMouseButtonUp) {
 				_scrollbarDragging = false;
diff --git a/engines/nancy/ui/notebookpopup.cpp b/engines/nancy/ui/notebookpopup.cpp
index 82f8d215d02..ea5d64dea52 100644
--- a/engines/nancy/ui/notebookpopup.cpp
+++ b/engines/nancy/ui/notebookpopup.cpp
@@ -296,8 +296,16 @@ void NotebookPopup::handleInput(NancyInput &input) {
 
 			const int newThumbTop = localMouse.y - _scrollbarGrabOffset;
 			const int clamped = CLIP<int>(newThumbTop, trackLocal.top, trackLocal.top + travel);
-			_scrollPos = travel > 0 ? (float)(clamped - trackLocal.top) / (float)travel : 0.0f;
-			refreshContent();
+			const float newScrollPos = travel > 0 ? (float)(clamped - trackLocal.top) / (float)travel : 0.0f;
+
+			// Only re-render when the thumb actually moves. refreshContent() re-lays
+			// out the whole popup (background, tabs, caption and full text surface),
+			// so calling it every frame while the button is merely held down pins the
+			// CPU and makes dragging choppy.
+			if (newScrollPos != _scrollPos) {
+				_scrollPos = newScrollPos;
+				refreshContent();
+			}
 
 			if (input.input & NancyInput::kLeftMouseButtonUp) {
 				_scrollbarDragging = false;


Commit: b962792797c90f779f3a01f934506f6a82805ab5
    https://github.com/scummvm/scummvm/commit/b962792797c90f779f3a01f934506f6a82805ab5
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-19T05:01:43+03:00

Commit Message:
NANCY: Copy save name to input box on click

Fix #16983

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


diff --git a/engines/nancy/state/loadsave.cpp b/engines/nancy/state/loadsave.cpp
index 8cc51808b76..9bc99bf336f 100644
--- a/engines/nancy/state/loadsave.cpp
+++ b/engines/nancy/state/loadsave.cpp
@@ -1006,6 +1006,13 @@ void LoadSaveMenu_V2::run() {
 
 				_loadButton->setDisabled(false);
 
+				// Copy the clicked save's name into the input textbox at the top,
+				// so clicking Save overwrites that save without retyping its name
+				if (_currentPage == 0 && _inputTextbox && Scene::hasInstance()) {
+					_enteredString = _filenameStrings[i];
+					writeToInputTextbox(_baseFont);
+				}
+
 				_hoveredSave = -1;
 				_selectedSave = i;
 


Commit: df65b119738aaf5ef20b65d27897608c9e5ba228
    https://github.com/scummvm/scummvm/commit/df65b119738aaf5ef20b65d27897608c9e5ba228
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-19T05:01:44+03:00

Commit Message:
NANCY: Show a confirmation when starting new game while playing a game

Fix #16979

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


diff --git a/engines/nancy/state/mainmenu.cpp b/engines/nancy/state/mainmenu.cpp
index 3ee58e4f5f9..315c800cb5f 100644
--- a/engines/nancy/state/mainmenu.cpp
+++ b/engines/nancy/state/mainmenu.cpp
@@ -170,14 +170,45 @@ void MainMenu::stop() {
 		// Credits
 		g_nancy->setState(NancyState::kCredits);
 		break;
-	case 1:
+	case 1: {
 		// New Game
+		auto *sdlg = GetEngineData(SDLG);
+
+		if (sdlg && sdlg->dialogs.size() > 2 && Scene::hasInstance() && !g_nancy->_hasJustSaved) {
+			// nancy6+ warns before abandoning an in-progress game. Reuse the
+			// "load a new game without saving the current one" dialog.
+			if (!ConfMan.hasKey("sdlg_return", Common::ConfigManager::kTransientDomain)) {
+				// Request the dialog
+				ConfMan.setInt("sdlg_id", 2, Common::ConfigManager::kTransientDomain);
+				_destroyOnExit = false;
+				g_nancy->setState(NancyState::kSaveDialog);
+				break;
+			} else {
+				// Dialog has returned
+				_destroyOnExit = true;
+				g_nancy->_graphics->suppressNextDraw();
+				uint ret = ConfMan.getInt("sdlg_return", Common::ConfigManager::kTransientDomain);
+				ConfMan.removeKey("sdlg_return", Common::ConfigManager::kTransientDomain);
+
+				if (ret != 0) {
+					// "No" or "Cancel" keeps us in the main menu
+					_selected = -1;
+					clearButtonState();
+					_state = kRun;
+					break;
+				}
+
+				// "Yes" falls through to start a new game
+			}
+		}
+
 		if (Scene::hasInstance()) {
 			NancySceneState.destroy(); // Destroy the existing Scene and create a new one
 		}
 
 		g_nancy->setState(NancyState::kScene);
 		break;
+	}
 	case 2:
 		// Load and Save Game, TODO
 		g_nancy->setState(NancyState::kLoadSave);


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

Commit Message:
NANCY: NANCY10: Fixes for the cellphone browser

Fix the header, back button and rendering of long pages

Fix #16985, #16986

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


diff --git a/engines/nancy/ui/cellphonepopup.cpp b/engines/nancy/ui/cellphonepopup.cpp
index 7f93ea15f7a..bc95aa4a8d9 100644
--- a/engines/nancy/ui/cellphonepopup.cpp
+++ b/engines/nancy/ui/cellphonepopup.cpp
@@ -519,14 +519,18 @@ void CellPhonePopup::drawScreenContent() {
 		break;
 
 	case kWaitPickup:
-	case kConnected:
-		drawConnectedLabel();
+		// Still ringing — only the connecting sprite, plus the Back button.
 		drawConnectingSprite();
 		if (isCallBackButtonActive()) {
 			drawBackButton(0);
 		}
 		break;
 
+	case kConnected:
+		drawConnectedLabel();
+		drawConnectingSprite();
+		break;
+
 	case kInvalidNumber:
 	case kWaitInvalid:
 		drawConnectingSprite();
@@ -567,21 +571,16 @@ void CellPhonePopup::drawScreenContent() {
 		break;
 
 	case kContentView:
-		// Browser pages use the interactive top-row "SEARCH" button
-		// (subButtons[8], drawn below) in place of a static heading; help /
-		// email keep their static heading.
-		if (_contentHeading && _contentHeading != &_uiclData->browserHeading) {
+		// Articles show the BROWSER heading; the main browser page shows the
+		// interactive SEARCH button (drawn below) instead.
+		if (_contentHeading &&
+			(_contentHeading != &_uiclData->browserHeading || isBrowserArticle())) {
 			drawHeading(*_contentHeading);
 		}
 		drawContentView();
-		// Help's Back sits in the lower ribbon (subButtons[0]); the zoomed
-		// articles' Back sits at the bottom of the screen (subButtons[7]).
-		// drawDirectoryArrows() blits whichever scroll pair applies.
 		drawDirectoryArrows();
-		drawBackButton(isHelpContentView() ? 0 : 7);
-		// Browser pages carry the top-row "SEARCH" button (subButtons[8]),
-		// which highlights green on hover and opens the search-topics list.
-		if (_contentHeading == &_uiclData->browserHeading) {
+		drawBackButton(contentViewBottomButton());
+		if (_contentHeading == &_uiclData->browserHeading && !isBrowserArticle()) {
 			drawHubButton(8);
 		}
 		break;
@@ -882,6 +881,9 @@ void CellPhonePopup::openBrowserHome() {
 	// further pages / the search list.
 	const UIBW *browserData = GetEngineData(UIBW);
 	if (browserData && !browserData->pages.empty()) {
+		// Remember the main-page key so isBrowserArticle() can tell it apart.
+		_browserHomeKey = browserData->pages[0].imageName.toString();
+		_browserHomeKey.toUppercase();
 		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
@@ -949,11 +951,18 @@ void CellPhonePopup::renderContentPage(int surfaceWidth) {
 		}
 	}
 	const uint32 trans = g_nancy->_graphics->getTransColor();
-	ht.render(surfaceWidth, 2000, trans, renderText, _uiclData->fontId2);
 
-	_contentCacheSurface.copyFrom(ht.surface());
+	// Render into a tall scratch surface, then cache only the used height
+	static const uint16 kMaxContentHeight = 6000;
+	ht.render(surfaceWidth, kMaxContentHeight, trans, renderText, _uiclData->fontId2);
+
+	const uint16 textHeight = MIN<uint16>(MAX<uint16>(ht.textHeight(), 1), kMaxContentHeight);
+	_contentCacheSurface.create(surfaceWidth, textHeight, ht.surface().format);
 	_contentCacheSurface.setTransparentColor(trans);
-	_contentCacheTextHeight = ht.textHeight();
+	_contentCacheSurface.clear(trans);
+	_contentCacheSurface.blitFrom(ht.surface(), Common::Rect(0, 0, (int16)surfaceWidth, (int16)textHeight),
+									Common::Point(0, 0));
+	_contentCacheTextHeight = textHeight;
 	_contentCacheHotspots = ht.hotspots();
 }
 
@@ -1177,12 +1186,28 @@ int CellPhonePopup::currentBackButtonIndex() const {
 	case kEmailList:
 		return 7;
 	case kContentView:
-		return isHelpContentView() ? 0 : 7;
+		return (int)contentViewBottomButton();
 	default:
 		return -1;
 	}
 }
 
+uint CellPhonePopup::contentViewBottomButton() const {
+	// Help Back (0); browser-article HOME (9); main page / email article Back (7).
+	if (isHelpContentView()) {
+		return 0;
+	}
+	return isBrowserArticle() ? 9 : 7;
+}
+
+bool CellPhonePopup::isBrowserArticle() const {
+	// A browser content view other than the main page opened by openBrowserHome().
+	if (_contentHeading != &_uiclData->browserHeading) {
+		return false;
+	}
+	return !_browserHomeKey.empty() && !_contentKey.equalsIgnoreCase(_browserHomeKey);
+}
+
 const UICL::ThreeRectWidget &CellPhonePopup::scrollUpButton() const {
 	// Directory and help both scroll with the small-LCD arrow pair
 	// (subButtons[1]/[2]); the zoomed email / browser articles use [5]/[6].
@@ -2091,9 +2116,9 @@ void CellPhonePopup::handleInput(NancyInput &input) {
 		const Common::Point popupMouseLink(chunkMouse.x - _screenPosition.left,
 											chunkMouse.y - _screenPosition.top);
 
-		// On browser pages the top-row "SEARCH" button (subButtons[8]) opens the
-		// search-topics list and highlights green while hovered.
-		if (_contentHeading == &_uiclData->browserHeading &&
+		// The main browser page carries the top-row SEARCH button (subButtons[8])
+		// which opens the search list; it highlights green while hovered.
+		if (_contentHeading == &_uiclData->browserHeading && !isBrowserArticle() &&
 				!_uiclData->subButtons[8].destRect.isEmpty()) {
 			Common::Rect searchBtn = _uiclData->subButtons[8].destRect;
 			searchBtn.translate(-_screenPosition.left, -_screenPosition.top);
@@ -2163,11 +2188,9 @@ void CellPhonePopup::handleInput(NancyInput &input) {
 		const bool overUpDown =
 			upDst.contains(chunkMouse) || downDst.contains(chunkMouse);
 
-		// Help draws its Back button in the lower ribbon (subButtons[0]); the
-		// zoomed email / browser view draws it at the bottom of the screen
-		// (subButtons[7]). Hit-test the matching button so it lines up with the
-		// visible sprite.
-		const Common::Rect backHit = backButtonHitRect(isHelpContentView() ? 0 : 7);
+		// Hit-test the bottom button that this view actually draws.
+		const bool onBrowserArticle = isBrowserArticle();
+		const Common::Rect backHit = backButtonHitRect(contentViewBottomButton());
 		const Common::Point popupMouse(chunkMouse.x - _screenPosition.left,
 										chunkMouse.y - _screenPosition.top);
 		if (!overUpDown && !backHit.isEmpty() && backHit.contains(popupMouse)) {
@@ -2175,7 +2198,12 @@ void CellPhonePopup::handleInput(NancyInput &input) {
 			if (input.input & NancyInput::kLeftMouseButtonUp) {
 				_contentKey.clear();
 				_contentHeading = nullptr;
-				enterScreenState(_contentReturnState);
+				if (onBrowserArticle) {
+					// HOME → the browser homepage (River Heights Wireless).
+					openBrowserHome();
+				} else {
+					enterScreenState(_contentReturnState);
+				}
 				input.eatMouseInput();
 				return;
 			}
diff --git a/engines/nancy/ui/cellphonepopup.h b/engines/nancy/ui/cellphonepopup.h
index 3cc25bda5da..4a7ba6d5130 100644
--- a/engines/nancy/ui/cellphonepopup.h
+++ b/engines/nancy/ui/cellphonepopup.h
@@ -233,6 +233,10 @@ private:
 	// subButtons index of the Back / HOME button visible in the current state,
 	// or -1 when none is shown. Used to drive its hover highlight.
 	int currentBackButtonIndex() const;
+	// subButtons index of the bottom button on a content view.
+	uint contentViewBottomButton() const;
+	// A browser content view other than the main page (i.e. an actual web page).
+	bool isBrowserArticle() const;
 	// Move the directory selection by delta, scrolling as needed.
 	void moveDirectorySelection(int delta);
 
@@ -288,6 +292,8 @@ private:
 	ScreenState _contentReturnState = kOnlineHub;
 	const UICL::SrcDestRectPair *_contentHeading = nullptr;
 	Common::String _contentKey;
+	// Uppercased key of the main browser page (empty until the browser is opened).
+	Common::String _browserHomeKey;
 	uint _contentScroll = 0;
 
 	// Email "opening" transition: the visible row whose closed envelope briefly


Commit: 3ba943006aef1c77eab0eb5dde7619cbccaef279
    https://github.com/scummvm/scummvm/commit/3ba943006aef1c77eab0eb5dde7619cbccaef279
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-19T05:01:45+03:00

Commit Message:
NANCY: NANCY10: Fix badge notification when returning from a closeup

Fix #16981

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


diff --git a/engines/nancy/state/scene.cpp b/engines/nancy/state/scene.cpp
index a15f95cffe3..6ea15e351c6 100644
--- a/engines/nancy/state/scene.cpp
+++ b/engines/nancy/state/scene.cpp
@@ -286,6 +286,12 @@ void Scene::popScene(bool inventory) {
 		changeScene(_sceneState.pushedInvScene);
 		_sceneState.isInvScenePushed = false;
 		addItemToInventory(_sceneState.pushedInvItemID);
+		// Returning from a close-up view restores an item the player already
+		// owned, so it must not raise the "new item" taskbar badge that
+		// addItemToInventory sets for a genuine pickup.
+		if (_taskbar) {
+			_taskbar->clearNotification(kTaskButtonInventory, 0);
+		}
 		_sceneState.pushedInvItemID = kEvNoEvent;
 		_sceneState.pushedInvScene.sceneID = kNoScene;
 	}




More information about the Scummvm-git-logs mailing list