[Scummvm-git-logs] scummvm master -> 95e82d6bbb7c60dcc12f85822e4643a260f96d9d

bluegr noreply at scummvm.org
Sun May 24 13:31:39 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:
769ce400e9 NANCY: Throw a warning when a terse caption text can't be found
328f5dcdec NANCY: Add information about some of the fields in SecondaryVideo
41b6c1d9ba NANCY: Add some more cursor enums for Nancy10+
709cee8136 NANCY: Set the hover cursor to "talk" instead of "eyeglass" in Nancy10+
12086db3a6 NANCY: Correct handling of default action records
5dad193549 NANCY: More work on the cell phone popup in Nancy10+
fed5429270 NANCY: Implement ARs related to the cellphone battery and directory
df3a4c95a1 NANCY: Persist cell phone data for Nancy10+
a42b87c336 NANCY: Map notification state for TASK buttons
95e82d6bbb NANCY: Fix regression in debug command scan_ar_type


Commit: 769ce400e97db8217551719cd12279da4dfcaa09
    https://github.com/scummvm/scummvm/commit/769ce400e97db8217551719cd12279da4dfcaa09
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-24T16:31:23+03:00

Commit Message:
NANCY: Throw a warning when a terse caption text can't be found

Will help us identify the cases where this happens more easily

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


diff --git a/engines/nancy/action/conversation.cpp b/engines/nancy/action/conversation.cpp
index d2622ddc3e0..6ece9bbf0ff 100644
--- a/engines/nancy/action/conversation.cpp
+++ b/engines/nancy/action/conversation.cpp
@@ -187,7 +187,12 @@ void ConversationSound::readTerseCaptionText(Common::SeekableReadStream &stream)
 
 	// WORKAROUND: Return an empty string for captions that aren't found.
 	// Happens with some conversations in Nancy10 (e.g. when calling the Rawleys).
-	_text = convo->texts.getValOrDefault(key, "");
+	if (convo->texts.contains(key)) {
+		_text = convo->texts[key];
+	} else {
+		warning("Convo key not found: %s", key.c_str());
+		_text = "";
+	}
 }
 
 void ConversationSound::readTerseResponseText(Common::SeekableReadStream &stream, ResponseStruct &response) {


Commit: 328f5dcdec3bd45767a685c9697803e17d820c85
    https://github.com/scummvm/scummvm/commit/328f5dcdec3bd45767a685c9697803e17d820c85
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-24T16:31:23+03:00

Commit Message:
NANCY: Add information about some of the fields in SecondaryVideo

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


diff --git a/engines/nancy/action/secondaryvideo.cpp b/engines/nancy/action/secondaryvideo.cpp
index c6f7da9d583..808362db49e 100644
--- a/engines/nancy/action/secondaryvideo.cpp
+++ b/engines/nancy/action/secondaryvideo.cpp
@@ -178,6 +178,8 @@ void PlaySecondaryVideo::readData(Common::SeekableReadStream &stream) {
 	_sceneChange.readData(stream, ser.getVersion() == kGameTypeVampire);
 	ser.skip(1, kGameTypeNancy1);
 
+	// Count of extra 6-byte entries that sit between the header and the
+	// video descs in the original layout. Always 0 in practice.
 	ser.skip(2, kGameTypeNancy10);
 
 	uint16 numVideoDescs = 0;
@@ -186,6 +188,12 @@ void PlaySecondaryVideo::readData(Common::SeekableReadStream &stream) {
 	for (uint i = 0; i < numVideoDescs; ++i) {
 		_videoDescs[i].readData(stream);
 	}
+
+	// Nancy 10+ tail: 16 trailing bytes after the video descs. Purpose
+	// not yet identified.
+	if (g_nancy->getGameType() >= kGameTypeNancy10) {
+		stream.skip(16);
+	}
 }
 
 void PlaySecondaryVideo::execute() {


Commit: 41b6c1d9bab22d11fc86fe907f17f2a5643f723b
    https://github.com/scummvm/scummvm/commit/41b6c1d9bab22d11fc86fe907f17f2a5643f723b
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-24T16:31:24+03:00

Commit Message:
NANCY: Add some more cursor enums for Nancy10+

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


diff --git a/engines/nancy/cursor.cpp b/engines/nancy/cursor.cpp
index 4e81f2d600a..772589a4c13 100644
--- a/engines/nancy/cursor.cpp
+++ b/engines/nancy/cursor.cpp
@@ -154,6 +154,7 @@ uint CursorManager::resolveNancy10CursorID(CursorType type, int16 itemID) {
 	switch (type) {
 	case kNormal:               return kNewNormal;
 	case kHotspot:              return kNewHotspot;
+	case kHotspotTalk:          return kNewHotspotTalk;
 	case kNormalArrow:          return kNewNormalArrow;
 	case kHotspotArrow:         return kNewHotspotArrow;
 	case kExit:                 return kNewExit;
diff --git a/engines/nancy/cursor.h b/engines/nancy/cursor.h
index 690d362aa3f..fccbcf47f68 100644
--- a/engines/nancy/cursor.h
+++ b/engines/nancy/cursor.h
@@ -55,12 +55,19 @@ public:
 		kCustom2Hotspot			= 19,
 		kNormalArrow			= 20,
 		kHotspotArrow			= 21,
+		kHotspotTalk			= 22,	// Speech-bubble hover cursor (Nancy 10+)
 
 		// Cursors in Nancy10 and newer games. Each cursor type stores
 		// two consecutive entries in the chunk: an idle slot at
 		// (type * 2) and a hotspot/highlighted slot at (type * 2 + 1).
 		kNewNormal 				= 0,	// Type 0 idle  — Eyeglass
 		kNewHotspot 			= 1,	// Type 0 hotspot — Eyeglass highlighted (only "hotspot" variant we expose)
+		kNewUse					= 2,	// Type 1 idle  — Used for interaction with characters and objects
+		kNewHotspotUse			= 3,	// Type 1 hotspot
+		kNewLockedUse			= 4,	// Type 2 idle
+		kNewHotspotLockedUse	= 5,	// Type 2 hotspot
+		kNewTalk				= 6,	// Type 3 idle  — Speech-bubble (talking to characters)
+		kNewHotspotTalk			= 7,	// Type 3 hotspot
 		kNewNormalArrow			= 8,	// Type 4 idle  — when the cursor is over the taskbar
 		kNewHotspotArrow		= 9,	// Type 4 hotspot
 		kNewExit 				= 10,	// Type 5 idle  — Used for movement and exiting puzzles


Commit: 709cee8136314d570f5d2180a4dcc9ee621a5f9f
    https://github.com/scummvm/scummvm/commit/709cee8136314d570f5d2180a4dcc9ee621a5f9f
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-24T16:31:25+03:00

Commit Message:
NANCY: Set the hover cursor to "talk" instead of "eyeglass" in Nancy10+

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


diff --git a/engines/nancy/action/secondaryvideo.h b/engines/nancy/action/secondaryvideo.h
index aab64bf0f3b..9117dedb011 100644
--- a/engines/nancy/action/secondaryvideo.h
+++ b/engines/nancy/action/secondaryvideo.h
@@ -22,6 +22,8 @@
 #ifndef NANCY_ACTION_SECONDARYVIDEO_H
 #define NANCY_ACTION_SECONDARYVIDEO_H
 
+#include "engines/nancy/cursor.h"
+#include "engines/nancy/nancy.h"
 #include "engines/nancy/video.h"
 #include "engines/nancy/action/actionrecord.h"
 
@@ -71,6 +73,10 @@ public:
 	bool canHaveHotspot() const override { return true; }
 	bool isViewportRelative() const override { return true; }
 
+	CursorManager::CursorType getHoverCursor() const override {
+		return g_nancy->getGameType() >= kGameTypeNancy10 ? CursorManager::kHotspotTalk : CursorManager::kHotspot;
+	}
+
 protected:
 	Common::String getRecordTypeName() const override { return "PlaySecondaryVideo"; }
 


Commit: 12086db3a69ada9865cd8b1fbe3cf3425fed7505
    https://github.com/scummvm/scummvm/commit/12086db3a69ada9865cd8b1fbe3cf3425fed7505
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-24T16:31:26+03:00

Commit Message:
NANCY: Correct handling of default action records

Default action records are like "else" cases for the previously
executed action record. We were erroneously treating them as a
default switch case, so we were checking them against ALL the scene
action records, which was not right.

This fixes cases scenes that rely on the default AR

Fix #16783, #16795, #16796

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


diff --git a/engines/nancy/action/actionmanager.cpp b/engines/nancy/action/actionmanager.cpp
index cb161d77ba7..453179b3479 100644
--- a/engines/nancy/action/actionmanager.cpp
+++ b/engines/nancy/action/actionmanager.cpp
@@ -240,7 +240,6 @@ ActionRecord *ActionManager::createAndLoadNewRecord(Common::SeekableReadStream &
 }
 
 void ActionManager::processActionRecords() {
-	_recordsWereExecuted = false;
 	_activatedRecordsThisFrame.clear();
 
 	for (auto record : _records) {
@@ -251,7 +250,7 @@ void ActionManager::processActionRecords() {
 		// Process dependencies every call. We make sure to ignore cursor dependencies,
 		// as they are only handled when calling from handleInput()
 		processDependency(record->_dependencies, *record, record->canHaveHotspot());
-		record->_isActive = record->_dependencies.satisfied;
+		record->_isActive = _previousRecordWasExecuted = record->_dependencies.satisfied;
 
 		if (record->_isActive) {
 			if(record->_state == ActionRecord::kBegin) {
@@ -259,7 +258,6 @@ void ActionManager::processActionRecords() {
 			}
 
 			record->execute();
-			_recordsWereExecuted = true;
 		}
 
 		if (g_nancy->getGameType() >= kGameTypeNancy4 && NancySceneState.getState() == State::Scene::kLoad) {
@@ -560,7 +558,7 @@ void ActionManager::processDependency(DependencyRecord &dep, ActionRecord &recor
 
 			break;
 		case DependencyType::kDefaultAR:
-			dep.satisfied = !_recordsWereExecuted;
+			dep.satisfied = !_previousRecordWasExecuted;
 			break;
 		default:
 			warning("Unimplemented Dependency type %i", (int)dep.type);
@@ -575,7 +573,7 @@ void ActionManager::clearActionRecords() {
 	}
 	_records.clear();
 	_activatedRecordsThisFrame.clear();
-	_recordsWereExecuted = false;
+	_previousRecordWasExecuted = false;
 }
 
 void ActionManager::onPause(bool pause) {
diff --git a/engines/nancy/action/actionmanager.h b/engines/nancy/action/actionmanager.h
index 4aa946963d7..23bafa7e4c7 100644
--- a/engines/nancy/action/actionmanager.h
+++ b/engines/nancy/action/actionmanager.h
@@ -79,7 +79,7 @@ protected:
 
 	void debugDrawHotspots();
 
-	bool _recordsWereExecuted = false; // Used for kDefaultAR dependency
+	bool _previousRecordWasExecuted = false;
 	Common::Array<ActionRecord *> _activatedRecordsThisFrame;
 };
 


Commit: 5dad193549de49b2207f2a959b8b0c3dfef4f8db
    https://github.com/scummvm/scummvm/commit/5dad193549de49b2207f2a959b8b0c3dfef4f8db
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-24T16:31:26+03:00

Commit Message:
NANCY: More work on the cell phone popup in Nancy10+

- Implemented battery level and signal strength
- Changed contact list to be mutable

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 e8798a4f7a5..c40500bad0f 100644
--- a/engines/nancy/ui/cellphonepopup.cpp
+++ b/engines/nancy/ui/cellphonepopup.cpp
@@ -70,6 +70,8 @@ void CellPhonePopup::init() {
 	bounds.moveTo(0, 0);
 	_drawSurface.create(bounds.width(), bounds.height(), g_nancy->_graphics->getInputPixelFormat());
 
+	_contacts = _uiclData->contacts;
+
 	_screenState = kWelcome;
 	_dialedNumber.clear();
 	_resolvedContact = -1;
@@ -99,6 +101,36 @@ void CellPhonePopup::setNoSignal(bool noSignal) {
 	}
 }
 
+void CellPhonePopup::setBatteryLow(bool low) {
+	if (_batteryLow == low) {
+		return;
+	}
+	_batteryLow = low;
+	if (_isVisible) {
+		drawScreenContent();
+	}
+}
+
+void CellPhonePopup::upsertContact(const UICL::Contact &c) {
+	// Match against the 11-byte dial pattern (prefix[2..12]). If an entry
+	// already carries that pattern, overwrite it; otherwise append.
+	for (uint i = 0; i < _contacts.size(); ++i) {
+		if (memcmp(_contacts[i].unknownPrefix + 2,
+					c.unknownPrefix + 2, 11) == 0) {
+			_contacts[i] = c;
+			if (_isVisible && _screenState == kDirectory) {
+				drawScreenContent();
+			}
+			return;
+		}
+	}
+
+	_contacts.push_back(c);
+	if (_isVisible && _screenState == kDirectory) {
+		drawScreenContent();
+	}
+}
+
 void CellPhonePopup::open() {
 	if (_isVisible) {
 		return;
@@ -193,7 +225,7 @@ void CellPhonePopup::updateGraphics() {
 		// connecting sprite stays on screen for the duration of the
 		// conversation. AR 128 closes the popup when the call ends.
 		if (_resolvedContact >= 0 &&
-				_resolvedContact < (int)_uiclData->contacts.size()) {
+				_resolvedContact < (int)_contacts.size()) {
 			triggerContactCallSceneChange((uint)_resolvedContact);
 			_resolvedContact = -1;
 			resetDialPad();
@@ -291,8 +323,11 @@ void CellPhonePopup::drawStatusIcons() {
 												_uiclData->signalSpriteDest.top - chunkOrigin.y));
 	}
 
-	if (!_uiclData->batterySpriteSrc.isEmpty() && !_uiclData->batterySpriteDest.isEmpty()) {
-		_drawSurface.blitFrom(_spritesImage, _uiclData->batterySpriteSrc,
+	const Common::Rect &batterySrc = _batteryLow && !_uiclData->batterySpriteSrcAlt.isEmpty()
+		? _uiclData->batterySpriteSrcAlt
+		: _uiclData->batterySpriteSrc;
+	if (!batterySrc.isEmpty() && !_uiclData->batterySpriteDest.isEmpty()) {
+		_drawSurface.blitFrom(_spritesImage, batterySrc,
 								Common::Point(_uiclData->batterySpriteDest.left - chunkOrigin.x,
 												_uiclData->batterySpriteDest.top - chunkOrigin.y));
 	}
@@ -470,9 +505,9 @@ void CellPhonePopup::drawDirectoryList() {
 	uint visited = 0;
 
 	for (uint contactIdx = 0;
-			contactIdx < _uiclData->contacts.size() && visibleRow < maxRows;
+			contactIdx < _contacts.size() && visibleRow < maxRows;
 			++contactIdx) {
-		const UICL::Contact &c = _uiclData->contacts[contactIdx];
+		const UICL::Contact &c = _contacts[contactIdx];
 		if (c.name.empty() || !isContactVisible(c)) {
 			continue;
 		}
@@ -620,8 +655,8 @@ int CellPhonePopup::findContactByDialBuffer() const {
 	// Dial pattern lives in prefix[2..], terminated by '\n'.
 	const uint dialLen = _dialedNumber.size();
 	const uint kDialOffset = 2;
-	for (uint i = 0; i < _uiclData->contacts.size(); ++i) {
-		const UICL::Contact &c = _uiclData->contacts[i];
+	for (uint i = 0; i < _contacts.size(); ++i) {
+		const UICL::Contact &c = _contacts[i];
 		if (!isContactVisible(c)) {
 			continue;
 		}
@@ -643,11 +678,11 @@ int CellPhonePopup::findContactByDialBuffer() const {
 }
 
 void CellPhonePopup::triggerContactCallSceneChange(uint contactIndex) {
-	if (contactIndex >= _uiclData->contacts.size()) {
+	if (contactIndex >= _contacts.size()) {
 		return;
 	}
 
-	const UICL::Contact &c = _uiclData->contacts[contactIndex];
+	const UICL::Contact &c = _contacts[contactIndex];
 
 	const uint16 sceneID = (uint16)c.unknownSuffix[0] | ((uint16)c.unknownSuffix[1] << 8);
 	if (sceneID == kNoScene) {
@@ -763,8 +798,8 @@ int CellPhonePopup::contactIndexForVisibleRow(uint visibleRow) const {
 	Common::Array<Common::String> seenNames;
 	uint visited = 0;
 	uint visibleSoFar = 0;
-	for (uint i = 0; i < _uiclData->contacts.size(); ++i) {
-		const UICL::Contact &c = _uiclData->contacts[i];
+	for (uint i = 0; i < _contacts.size(); ++i) {
+		const UICL::Contact &c = _contacts[i];
 		if (c.name.empty() || !isContactVisible(c)) {
 			continue;
 		}
@@ -845,10 +880,10 @@ uint CellPhonePopup::directoryRowAt(const Common::Point &chunkMouse) const {
 }
 
 void CellPhonePopup::startCallToContact(uint contactIndex) {
-	if (contactIndex >= _uiclData->contacts.size()) {
+	if (contactIndex >= _contacts.size()) {
 		return;
 	}
-	const UICL::Contact &c = _uiclData->contacts[contactIndex];
+	const UICL::Contact &c = _contacts[contactIndex];
 
 	// Rebuild _dialedNumber so the call flow's lookup matches.
 	_dialedNumber.clear();
@@ -871,8 +906,8 @@ void CellPhonePopup::startCallToContact(uint contactIndex) {
 
 uint CellPhonePopup::deduplicatedContactCount() const {
 	Common::Array<Common::String> seen;
-	for (uint i = 0; i < _uiclData->contacts.size(); ++i) {
-		const UICL::Contact &c = _uiclData->contacts[i];
+	for (uint i = 0; i < _contacts.size(); ++i) {
+		const UICL::Contact &c = _contacts[i];
 		if (c.name.empty() || !isContactVisible(c)) {
 			continue;
 		}
diff --git a/engines/nancy/ui/cellphonepopup.h b/engines/nancy/ui/cellphonepopup.h
index 80a09e83066..884795c0e9a 100644
--- a/engines/nancy/ui/cellphonepopup.h
+++ b/engines/nancy/ui/cellphonepopup.h
@@ -49,9 +49,16 @@ public:
 	void toggle() { if (_isVisible) close(); else open(); }
 
 	// Swaps the welcome graphic for the No Signal / No Access / Old Email
-	// Only labels and blocks outgoing calls. TODO: hook to scene flag.
+	// Only labels and blocks outgoing calls.
 	void setNoSignal(bool noSignal);
 
+	// Swaps the battery sprite for the low/dead variant.
+	void setBatteryLow(bool low);
+
+	// Insert or replace a contact (matched by its 11-byte dial pattern).
+	// Used by AR 130 to add/modify entries at runtime.
+	void upsertContact(const UICL::Contact &c);
+
 private:
 	enum ScreenState : int {
 		kWelcome          = 0,
@@ -110,6 +117,10 @@ private:
 
 	const UICL *_uiclData;
 
+	// Runtime contact list, seeded from _uiclData->contacts and then
+	// mutable (AR 130 inserts/replaces entries).
+	Common::Array<UICL::Contact> _contacts;
+
 	// Chrome (header.imageName) and sprite atlas (overlayImageName).
 	Graphics::ManagedSurface _overlayImage;
 	Graphics::ManagedSurface _spritesImage;
@@ -134,6 +145,7 @@ private:
 	uint _directorySelection = 0;
 
 	bool _noSignal = false;
+	bool _batteryLow = false;
 };
 
 } // End of namespace UI


Commit: fed54292709060541061a5564c3a15d69d43b049
    https://github.com/scummvm/scummvm/commit/fed54292709060541061a5564c3a15d69d43b049
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-24T16:31:27+03:00

Commit Message:
NANCY: Implement ARs related to the cellphone battery and directory

Implement the following ARs:
- SetCellPhoneBatteryAndSignal
- ChangeCellPhoneInfo

Will check if these need to persist in saved games during playthrough
and testing

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


diff --git a/engines/nancy/action/arfactory.cpp b/engines/nancy/action/arfactory.cpp
index 0fb762b9254..9f3fe55fbf2 100644
--- a/engines/nancy/action/arfactory.cpp
+++ b/engines/nancy/action/arfactory.cpp
@@ -335,10 +335,12 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
 	case 128:
 		// Nancy 10+
 		return new CellPhonePopCellSceneFromStack();
+	case 129:
+		// Nancy 10+
+		return new SetCellPhoneBatteryAndSignal();
 	case 130:
 		// Nancy 10+
-		warning("ChangeCellPhoneInfo - not implemented yet");
-		return nullptr;
+		return new ChangeCellPhoneInfo();
 	case 131:
 		// Nancy 10+
 		return new AddSearchLink();
diff --git a/engines/nancy/action/miscrecords.cpp b/engines/nancy/action/miscrecords.cpp
index b0663a851cf..5c373c50fb5 100644
--- a/engines/nancy/action/miscrecords.cpp
+++ b/engines/nancy/action/miscrecords.cpp
@@ -238,6 +238,40 @@ void AddSearchLink::execute() {
 	finishExecution();
 }
 
+void SetCellPhoneBatteryAndSignal::readData(Common::SeekableReadStream &stream) {
+	_mode = stream.readUint16LE();
+}
+
+void SetCellPhoneBatteryAndSignal::execute() {
+	UI::CellPhonePopup &popup = NancySceneState.getCellPhonePopup();
+	switch (_mode) {
+	case 0: popup.setBatteryLow(false); break;
+	case 1: popup.setBatteryLow(true);  break;
+	case 2: popup.setNoSignal(false);   break;
+	case 3: popup.setNoSignal(true);    break;
+	default:
+		warning("SetCellPhoneBatteryAndSignal: unknown mode %u", _mode);
+		break;
+	}
+	finishExecution();
+}
+
+void ChangeCellPhoneInfo::readData(Common::SeekableReadStream &stream) {
+	stream.read(_contact.unknownPrefix, sizeof(_contact.unknownPrefix));
+
+	char nameBuf[21];
+	stream.read(nameBuf, 20);
+	nameBuf[20] = '\0';
+	_contact.name = nameBuf;
+
+	stream.read(_contact.unknownSuffix, sizeof(_contact.unknownSuffix));
+}
+
+void ChangeCellPhoneInfo::execute() {
+	NancySceneState.getCellPhonePopup().upsertContact(_contact);
+	finishExecution();
+}
+
 void CellPhonePopCellSceneFromStack::readData(Common::SeekableReadStream &stream) {
 	_sceneChange.readData(stream);
 }
diff --git a/engines/nancy/action/miscrecords.h b/engines/nancy/action/miscrecords.h
index 1bf6885b2b1..ed6cd179032 100644
--- a/engines/nancy/action/miscrecords.h
+++ b/engines/nancy/action/miscrecords.h
@@ -23,6 +23,7 @@
 #define NANCY_ACTION_RECORDTYPES_H
 
 #include "engines/nancy/action/actionrecord.h"
+#include "engines/nancy/enginedata.h"
 
 namespace Nancy {
 
@@ -187,6 +188,33 @@ protected:
 	Common::String getRecordTypeName() const override { return "AddSearchLink"; }
 };
 
+// Sets the cellphone's battery/signal indicators. Modes 0/1 toggle the
+// battery (normal / low) and 2/3 toggle the signal (normal / no signal).
+class SetCellPhoneBatteryAndSignal : public ActionRecord {
+public:
+	void readData(Common::SeekableReadStream &stream) override;
+	void execute() override;
+
+	uint16 _mode = 0;
+
+protected:
+	Common::String getRecordTypeName() const override { return "SetCellPhoneBatteryAndSignal"; }
+};
+
+// Adds a new entry to the cellphone directory, or overwrites an existing
+// one matched by dial pattern. Used to unlock contacts as the player
+// progresses (Nancy 10+).
+class ChangeCellPhoneInfo : public ActionRecord {
+public:
+	void readData(Common::SeekableReadStream &stream) override;
+	void execute() override;
+
+	UICL::Contact _contact;
+
+protected:
+	Common::String getRecordTypeName() const override { return "ChangeCellPhoneInfo"; }
+};
+
 // Returns from a cellphone-driven conversation scene to the pre-call scene.
 // sceneID == kNoScene pops the saved scene; any other sceneID overrides it.
 class CellPhonePopCellSceneFromStack : public ActionRecord {


Commit: df3a4c95a1ad0151ae7be1667be8ac1e72463f8d
    https://github.com/scummvm/scummvm/commit/df3a4c95a1ad0151ae7be1667be8ac1e72463f8d
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-24T16:31:27+03:00

Commit Message:
NANCY: Persist cell phone data for Nancy10+

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


diff --git a/engines/nancy/puzzledata.cpp b/engines/nancy/puzzledata.cpp
index cd6076c71d8..5d1781dee78 100644
--- a/engines/nancy/puzzledata.cpp
+++ b/engines/nancy/puzzledata.cpp
@@ -277,6 +277,37 @@ float TableData::getComboValue(uint16 index) const {
 	return index < comboValues.size() ? comboValues[index] : kNoTableValue;
 }
 
+void CellPhoneData::synchronize(Common::Serializer &ser) {
+	ser.syncAsByte(noSignal);
+	ser.syncAsByte(batteryLow);
+	ser.syncAsByte(seeded);
+
+	uint16 numContacts = (uint16)contacts.size();
+	ser.syncAsUint16LE(numContacts);
+
+	if (ser.isLoading()) {
+		contacts.resize(numContacts);
+	}
+
+	char nameBuf[21];
+	for (uint16 i = 0; i < numContacts; ++i) {
+		UICL::Contact &c = contacts[i];
+		ser.syncBytes(c.unknownPrefix, sizeof(c.unknownPrefix));
+
+		if (ser.isSaving()) {
+			memset(nameBuf, 0, sizeof(nameBuf));
+			Common::strlcpy(nameBuf, c.name.c_str(), sizeof(nameBuf));
+		}
+		ser.syncBytes((byte *)nameBuf, 20);
+		if (ser.isLoading()) {
+			nameBuf[20] = '\0';
+			c.name = nameBuf;
+		}
+
+		ser.syncBytes(c.unknownSuffix, sizeof(c.unknownSuffix));
+	}
+}
+
 PuzzleData *makePuzzleData(const uint32 tag) {
 	switch(tag) {
 	case SliderPuzzleData::getTag():
@@ -297,6 +328,8 @@ PuzzleData *makePuzzleData(const uint32 tag) {
 		return new JournalData();
 	case TableData::getTag():
 		return new TableData();
+	case CellPhoneData::getTag():
+		return new CellPhoneData();
 	default:
 		return nullptr;
 	}
diff --git a/engines/nancy/puzzledata.h b/engines/nancy/puzzledata.h
index bf11859d3ea..d5df60db10c 100644
--- a/engines/nancy/puzzledata.h
+++ b/engines/nancy/puzzledata.h
@@ -24,6 +24,7 @@
 #include "common/hashmap.h"
 
 #include "engines/nancy/commontypes.h"
+#include "engines/nancy/enginedata.h"
 
 #ifndef NANCY_PUZZLEDATA_H
 #define NANCY_PUZZLEDATA_H
@@ -167,6 +168,23 @@ struct TableData : public PuzzleData {
 	Common::Array<float> comboValues;
 };
 
+// Nancy 10+ cellphone state mutated by the ChangeCellPhoneInfo and
+// SetCellPhoneBatteryAndSignal action records, persisted between saves.
+struct CellPhoneData : public PuzzleData {
+	CellPhoneData() {}
+	virtual ~CellPhoneData() {}
+
+	static constexpr uint32 getTag() { return MKTAG('C', 'E', 'L', 'L'); }
+	virtual void synchronize(Common::Serializer &ser);
+
+	bool noSignal = false;
+	bool batteryLow = false;
+	// Loaded set to true once the popup has seeded the contact list from
+	// the UICL chunk; we then own it as runtime data.
+	bool seeded = false;
+	Common::Array<UICL::Contact> contacts;
+};
+
 PuzzleData *makePuzzleData(const uint32 tag);
 
 } // End of namespace Nancy
diff --git a/engines/nancy/ui/cellphonepopup.cpp b/engines/nancy/ui/cellphonepopup.cpp
index c40500bad0f..0de5e463f4e 100644
--- a/engines/nancy/ui/cellphonepopup.cpp
+++ b/engines/nancy/ui/cellphonepopup.cpp
@@ -24,6 +24,7 @@
 #include "engines/nancy/graphics.h"
 #include "engines/nancy/input.h"
 #include "engines/nancy/nancy.h"
+#include "engines/nancy/puzzledata.h"
 #include "engines/nancy/resource.h"
 #include "engines/nancy/sound.h"
 
@@ -70,7 +71,21 @@ void CellPhonePopup::init() {
 	bounds.moveTo(0, 0);
 	_drawSurface.create(bounds.width(), bounds.height(), g_nancy->_graphics->getInputPixelFormat());
 
-	_contacts = _uiclData->contacts;
+	// Persistent state lives in CellPhoneData (saved across the game).
+	// First-time init seeds the runtime contact list from the chunk;
+	// subsequent inits (e.g. after a load) restore the saved state.
+	CellPhoneData *cellData = (CellPhoneData *)NancySceneState.getPuzzleData(CellPhoneData::getTag());
+	if (cellData) {
+		if (!cellData->seeded) {
+			cellData->contacts = _uiclData->contacts;
+			cellData->seeded = true;
+		}
+		_contacts = cellData->contacts;
+		_noSignal = cellData->noSignal;
+		_batteryLow = cellData->batteryLow;
+	} else {
+		_contacts = _uiclData->contacts;
+	}
 
 	_screenState = kWelcome;
 	_dialedNumber.clear();
@@ -96,6 +111,10 @@ void CellPhonePopup::setNoSignal(bool noSignal) {
 		return;
 	}
 	_noSignal = noSignal;
+	CellPhoneData *cellData = (CellPhoneData *)NancySceneState.getPuzzleData(CellPhoneData::getTag());
+	if (cellData) {
+		cellData->noSignal = noSignal;
+	}
 	if (_isVisible) {
 		drawScreenContent();
 	}
@@ -106,6 +125,10 @@ void CellPhonePopup::setBatteryLow(bool low) {
 		return;
 	}
 	_batteryLow = low;
+	CellPhoneData *cellData = (CellPhoneData *)NancySceneState.getPuzzleData(CellPhoneData::getTag());
+	if (cellData) {
+		cellData->batteryLow = low;
+	}
 	if (_isVisible) {
 		drawScreenContent();
 	}
@@ -114,18 +137,24 @@ void CellPhonePopup::setBatteryLow(bool low) {
 void CellPhonePopup::upsertContact(const UICL::Contact &c) {
 	// Match against the 11-byte dial pattern (prefix[2..12]). If an entry
 	// already carries that pattern, overwrite it; otherwise append.
+	bool replaced = false;
 	for (uint i = 0; i < _contacts.size(); ++i) {
 		if (memcmp(_contacts[i].unknownPrefix + 2,
 					c.unknownPrefix + 2, 11) == 0) {
 			_contacts[i] = c;
-			if (_isVisible && _screenState == kDirectory) {
-				drawScreenContent();
-			}
-			return;
+			replaced = true;
+			break;
 		}
 	}
+	if (!replaced) {
+		_contacts.push_back(c);
+	}
+
+	CellPhoneData *cellData = (CellPhoneData *)NancySceneState.getPuzzleData(CellPhoneData::getTag());
+	if (cellData) {
+		cellData->contacts = _contacts;
+	}
 
-	_contacts.push_back(c);
 	if (_isVisible && _screenState == kDirectory) {
 		drawScreenContent();
 	}
@@ -136,6 +165,14 @@ void CellPhonePopup::open() {
 		return;
 	}
 
+	// Re-pull persistent state in case a save was loaded after init().
+	CellPhoneData *cellData = (CellPhoneData *)NancySceneState.getPuzzleData(CellPhoneData::getTag());
+	if (cellData && cellData->seeded) {
+		_contacts = cellData->contacts;
+		_noSignal = cellData->noSignal;
+		_batteryLow = cellData->batteryLow;
+	}
+
 	_screenState = kWelcome;
 	_dialedNumber.clear();
 	_resolvedContact = -1;


Commit: a42b87c33626e0e0bee0019c883b9d19fb547a04
    https://github.com/scummvm/scummvm/commit/a42b87c33626e0e0bee0019c883b9d19fb547a04
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-24T16:31:28+03:00

Commit Message:
NANCY: Map notification state for TASK buttons

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


diff --git a/engines/nancy/enginedata.cpp b/engines/nancy/enginedata.cpp
index 749e49db028..2c029526ba6 100644
--- a/engines/nancy/enginedata.cpp
+++ b/engines/nancy/enginedata.cpp
@@ -895,7 +895,7 @@ TASK::TASK(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
 	char nameBuf[34];
 	for (uint i = 0; i < kNumButtons; ++i) {
 		readUIButton(*chunkStream, buttons[i].button);
-		chunkStream->read(buttons[i].unknownPad, sizeof(buttons[i].unknownPad));
+		readRect(*chunkStream, buttons[i].notificationSrcRect);
 		for (uint s = 0; s < kNumAltSounds; ++s) {
 			chunkStream->read(nameBuf, 33);
 			nameBuf[33] = '\0';
diff --git a/engines/nancy/enginedata.h b/engines/nancy/enginedata.h
index cbac93d172a..c4310365ae0 100644
--- a/engines/nancy/enginedata.h
+++ b/engines/nancy/enginedata.h
@@ -526,7 +526,10 @@ enum TaskButton {
 struct TASK : public EngineData {
 	struct ButtonRecord {
 		UIButtonRecord button;
-		byte unknownPad[16];
+		// Source rect for the notification sprite (the badge shown on
+		// the middle three buttons when the popup has new content).
+		// Empty for buttons that don't carry a notification.
+		Common::Rect notificationSrcRect;
 		Common::String clickSoundName[3];
 	};
 


Commit: 95e82d6bbb7c60dcc12f85822e4643a260f96d9d
    https://github.com/scummvm/scummvm/commit/95e82d6bbb7c60dcc12f85822e4643a260f96d9d
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-24T16:31:29+03:00

Commit Message:
NANCY: Fix regression in debug command scan_ar_type

Fix loading consecutive lists from ResourceManager::list().
A regression from 7eeff19.

Changed paths:
    engines/nancy/resource.cpp


diff --git a/engines/nancy/resource.cpp b/engines/nancy/resource.cpp
index 986f5127404..7c2bdedf21d 100644
--- a/engines/nancy/resource.cpp
+++ b/engines/nancy/resource.cpp
@@ -301,14 +301,16 @@ void ResourceManager::list(const Common::String &treeName, Common::Array<Common:
 		if (!tree) {
 			return;
 		}
-		outList = tree->getPathsForType(type);
+		Common::Array<Common::Path> result = tree->getPathsForType(type);
+		outList.insert_at(outList.size(), result);
 	} else {
 		for (uint i = 0; i < _cifTreeNames.size(); ++i) {
 			// No provided tree name, check inside every loaded tree
 			Common::String upper = _cifTreeNames[i];
 			upper.toUppercase();
 			const CifTree *tree = (const CifTree *)SearchMan.getArchive(treePrefix + upper);
-			outList = tree->getPathsForType(type);
+			Common::Array<Common::Path> result = tree->getPathsForType(type);
+			outList.insert_at(outList.size(), result);
 		}
 	}
 }




More information about the Scummvm-git-logs mailing list