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

bluegr noreply at scummvm.org
Mon Jun 29 01:56:52 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:
0163ebdeb4 NANCY: NANCY11: Use new timer dependencies
d2b9d288d9 NANCY: NANCY12: Check for the correct cursor for swallowing clicks
6a0bf99335 NANCY: NANCY10+: Implement click sound handling for taskbar icons
28e2941056 NANCY: NANCY12: Implement the new coin purse interface icon
b297542156 NANCY: NANCY10: Fix display of punctuation glyphs
b033598c7b NaNCY: NANCY12: Use a larger grid size for CollisionPuzzle
c4b0137f10 NANCY: NANCY12: Add an initial stub for AR Set3DSoundListenerPosition
d025c7ca4a NANCY: Mark looping sound records as done


Commit: 0163ebdeb47362647da61da14f41cf9d625ff2c3
    https://github.com/scummvm/scummvm/commit/0163ebdeb47362647da61da14f41cf9d625ff2c3
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-06-29T04:56:30+03:00

Commit Message:
NANCY: NANCY11: Use new timer dependencies

Now, the timer dependencies use the new timer semantics in Nancy11+.
Fixes timer related issues in Nancy11

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


diff --git a/engines/nancy/action/actionmanager.cpp b/engines/nancy/action/actionmanager.cpp
index 8802874824f..625aff0beaf 100644
--- a/engines/nancy/action/actionmanager.cpp
+++ b/engines/nancy/action/actionmanager.cpp
@@ -496,20 +496,28 @@ void ActionManager::processDependency(DependencyRecord &dep, ActionRecord &recor
 
 			break;
 		case DependencyType::kTimerLessThanDependencyTime:
-			if (NancySceneState._timers.timerTime <= dep.timeData) {
-				dep.satisfied = true;
+			if (g_nancy->getGameType() >= kGameTypeNancy11) {
+				// Nancy11+ checks a software-timer slot (label = slot index)
+				dep.satisfied = NancySceneState.isSoftwareTimerActive(dep.label) &&
+					NancySceneState.getSoftwareTimerElapsed(dep.label) <= (uint32)dep.timeData;
 			} else {
-				dep.satisfied = false;
+				dep.satisfied = NancySceneState._timers.timerTime <= dep.timeData;
 			}
 
 			break;
 		case DependencyType::kTimerGreaterThanDependencyTime:
-			if (NancySceneState._timers.timerTime > dep.timeData) {
-				dep.satisfied = true;
+			if (g_nancy->getGameType() >= kGameTypeNancy11) {
+				dep.satisfied = NancySceneState.isSoftwareTimerActive(dep.label) &&
+					(uint32)dep.timeData < NancySceneState.getSoftwareTimerElapsed(dep.label);
 			} else {
-				dep.satisfied = false;
+				dep.satisfied = NancySceneState._timers.timerTime > dep.timeData;
 			}
 
+			break;
+		case DependencyType::kTimerIsActive:
+			// Nancy11+ only: satisfied while the software-timer slot is running/counting
+			dep.satisfied = NancySceneState.isSoftwareTimerActive(dep.label);
+
 			break;
 		case DependencyType::kDifficultyLevel:
 			if (dep.condition == NancySceneState.getDifficulty()) {
diff --git a/engines/nancy/action/actionrecord.h b/engines/nancy/action/actionrecord.h
index 5b18d164f0e..a4d6fcf503a 100644
--- a/engines/nancy/action/actionrecord.h
+++ b/engines/nancy/action/actionrecord.h
@@ -61,7 +61,8 @@ enum struct DependencyType : int16 {
 	kOpenParenthesis				= 18,
 	kCloseParenthesis				= 19,
 	kRandom							= 20,
-	kDefaultAR						= 21
+	kDefaultAR						= 21,
+	kTimerIsActive					= 22	// Nancy11+ software-timer slot is running/counting
 };
 
 // Describes a condition that needs to be fulfilled before the
diff --git a/engines/nancy/state/scene.cpp b/engines/nancy/state/scene.cpp
index 43d383c404d..194514dbf7a 100644
--- a/engines/nancy/state/scene.cpp
+++ b/engines/nancy/state/scene.cpp
@@ -871,8 +871,8 @@ UI::Clock *Scene::getClock() {
 }
 
 void Scene::init() {
-	auto *bootSummary = GetEngineData(BSUM);
-	auto *hintData = GetEngineData(HINT);
+	auto *bootSummary = GetEngineData(BSUM)
+	auto *hintData = GetEngineData(HINT)
 	assert(bootSummary);
 
 	_flags.eventFlags.resize(g_nancy->getStaticData().numEventFlags, g_nancy->_false);
@@ -1215,6 +1215,25 @@ void Scene::tickSoftwareTimers(uint32 deltaMs) {
 	}
 }
 
+bool Scene::isSoftwareTimerActive(uint16 index) const {
+	if (index >= TimerData::kNumTimers || !_puzzleData.contains(TimerData::getTag())) {
+		return false;
+	}
+
+	const TimerData::Timer &timer = ((const TimerData *)_puzzleData.getVal(TimerData::getTag()))->timers[index];
+	return timer.state == TimerData::Timer::kRunning ||
+		timer.state == TimerData::Timer::kOneShot ||
+		timer.state == TimerData::Timer::kRepeating;
+}
+
+uint32 Scene::getSoftwareTimerElapsed(uint16 index) const {
+	if (index >= TimerData::kNumTimers || !_puzzleData.contains(TimerData::getTag())) {
+		return 0;
+	}
+
+	return ((const TimerData *)_puzzleData.getVal(TimerData::getTag()))->timers[index].currentTimeMs;
+}
+
 void Scene::fireSoftwareTimer(TimerData::Timer &timer) {
 	timer.hasFired = true;
 
diff --git a/engines/nancy/state/scene.h b/engines/nancy/state/scene.h
index 3429f670727..ce9cf8b9b8d 100644
--- a/engines/nancy/state/scene.h
+++ b/engines/nancy/state/scene.h
@@ -149,6 +149,10 @@ public:
 	bool getEventFlag(int16 label, byte flag) const;
 	bool getEventFlag(FlagDescription eventFlag) const;
 
+	// Nancy 11+ software-timer queries used by timer dependencies.
+	bool isSoftwareTimerActive(uint16 index) const;
+	uint32 getSoftwareTimerElapsed(uint16 index) const;
+
 	void setLogicCondition(int16 label, byte flag);
 	bool getLogicCondition(int16 label, byte flag) const;
 	void clearLogicConditions();


Commit: d2b9d288d920b7dffe23dba2d8c3addb5373c1cf
    https://github.com/scummvm/scummvm/commit/d2b9d288d920b7dffe23dba2d8c3addb5373c1cf
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-06-29T04:56:31+03:00

Commit Message:
NANCY: NANCY12: Check for the correct cursor for swallowing clicks

this has been changed in Nancy10, and then Nancy12

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


diff --git a/engines/nancy/action/datarecords.cpp b/engines/nancy/action/datarecords.cpp
index a4824a7598e..46bf2f41328 100644
--- a/engines/nancy/action/datarecords.cpp
+++ b/engines/nancy/action/datarecords.cpp
@@ -386,7 +386,10 @@ void EventFlagsMultiHS::execute() {
 		// Swallow clicks if the cursor is in the puzzle-drag range
 		if (g_nancy->getGameType() <= kGameTypeNancy9 && (_hoverCursor == CursorManager::kCustom1 || _hoverCursor == CursorManager::kCustom2)) {
 			_state = kRun;
-		} else if (g_nancy->getGameType() >= kGameTypeNancy10 && (int)_hoverCursor >= CursorManager::kNewUseHand) {
+		} else if (g_nancy->getGameType() >= kGameTypeNancy10 &&
+				(int)_hoverCursor >= (g_nancy->getGameType() >= kGameTypeNancy12 ?
+					CursorManager::kNewUseHand : CursorManager::kNewUseHand - 1)) {
+			// The puzzle-drag cursor range starts one slot earlier before Nancy12
 			_state = kRun;
 		} else {
 			_hasHotspot = false;


Commit: 6a0bf9933566f45eaf653637f5cc8f02dc379f1f
    https://github.com/scummvm/scummvm/commit/6a0bf9933566f45eaf653637f5cc8f02dc379f1f
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-06-29T04:56:33+03:00

Commit Message:
NANCY: NANCY10+: Implement click sound handling for taskbar icons

Now, click sounds are played when clicking on taskbar icons, and
rejection sounds are played when taskbar icons are inactive.

Fix #16891

Changed paths:
    engines/nancy/action/miscrecords.cpp
    engines/nancy/ui/taskbar.cpp
    engines/nancy/ui/taskbar.h


diff --git a/engines/nancy/action/miscrecords.cpp b/engines/nancy/action/miscrecords.cpp
index df51c755226..4131cac0c48 100644
--- a/engines/nancy/action/miscrecords.cpp
+++ b/engines/nancy/action/miscrecords.cpp
@@ -282,8 +282,9 @@ void ControlUIItems::execute() {
 		// player is in scene range [_startScene, _endScene]. _flagB != 0 sets
 		// the toggle (a _startScene of 9997 means "from scene 0", with the
 		// range capped at 9997); _flagB == 0 clears it once a bound is 9999
-		// (kNoScene). The _autoOpenOrBadgeSound value selects the button's
-		// click sound in the original (not yet implemented).
+		// (kNoScene). _flagB != 0 also sets the disabled button's rejection-sound
+		// mode from _autoOpenOrBadgeSound (which clickSoundName line plays when
+		// the button is clicked while its popup is unavailable).
 		UI::Taskbar *taskbar = NancySceneState.getTaskbar();
 		if (taskbar) {
 			if (_flagB != 0) {
@@ -294,6 +295,7 @@ void ControlUIItems::execute() {
 					end = 9997;
 				}
 				taskbar->setDisabledRange(_uiButton, start, end);
+				taskbar->setClickSoundMode(_uiButton, _autoOpenOrBadgeSound);
 			} else if (_startScene == (int16)kNoScene || _endScene == (int16)kNoScene) {
 				taskbar->clearButtonOverride(_uiButton);
 			}
diff --git a/engines/nancy/ui/taskbar.cpp b/engines/nancy/ui/taskbar.cpp
index a7f70f473a2..6de8eeea5c3 100644
--- a/engines/nancy/ui/taskbar.cpp
+++ b/engines/nancy/ui/taskbar.cpp
@@ -19,13 +19,18 @@
  *
  */
 
+#include "common/random.h"
+
 #include "engines/nancy/nancy.h"
 #include "engines/nancy/cursor.h"
+#include "engines/nancy/font.h"
 #include "engines/nancy/graphics.h"
 #include "engines/nancy/input.h"
 #include "engines/nancy/resource.h"
 #include "engines/nancy/sound.h"
 
+#include "engines/nancy/state/scene.h"
+
 #include "engines/nancy/ui/taskbar.h"
 
 namespace Nancy {
@@ -73,11 +78,23 @@ void Taskbar::registerGraphics() {
 	setVisible(true);
 }
 
+// "NO_UI_ITEM" marks an absent button slot — e.g. Nancy12's removed cell phone,
+// whose on-screen position the coin purse occupies instead. Such slots are neither
+// drawn nor hit-tested.
+static bool isButtonSlotUsed(const TASK::ButtonRecord &rec) {
+	return !rec.button.primaryImageName.empty() &&
+		!rec.button.primaryImageName.toString().equalsIgnoreCase("NO_UI_ITEM");
+}
+
 void Taskbar::drawButton(uint index, ButtonState state) {
 	auto *taskData = GetEngineData(TASK);
 	assert(taskData);
 
 	const TASK::ButtonRecord &rec = taskData->buttons[index];
+	if (!isButtonSlotUsed(rec)) {
+		return;
+	}
+
 	const UIButtonRecord &btn = rec.button;
 
 	// The notification sprite lives outside the standard sourceRects
@@ -128,6 +145,10 @@ Taskbar::ButtonState Taskbar::restingState(uint index) const {
 }
 
 bool Taskbar::isButtonActive(uint index) const {
+	auto *taskData = GetEngineData(TASK);
+	if (!taskData || !isButtonSlotUsed(taskData->buttons[index])) {
+		return false;
+	}
 	return _enabled[index] && restingState(index) != kButtonDisabled;
 }
 
@@ -194,12 +215,20 @@ void Taskbar::clearButtonOverride(uint buttonIndex) {
 		return;
 	}
 	_overrides[buttonIndex].active = false;
+	_overrides[buttonIndex].clickSoundMode = kClickSoundDefault;
 
 	if ((int)buttonIndex != _hoveredButton) {
 		drawButton(buttonIndex, restingState(buttonIndex));
 	}
 }
 
+void Taskbar::setClickSoundMode(uint buttonIndex, uint mode) {
+	if (buttonIndex >= TASK::kNumButtons) {
+		return;
+	}
+	_overrides[buttonIndex].clickSoundMode = mode;
+}
+
 void Taskbar::updateNotificationStates(int16 currentSceneID) {
 	_currentScene = currentSceneID;
 	for (uint i = 0; i < TASK::kNumButtons; ++i) {
@@ -213,28 +242,91 @@ void Taskbar::updateNotificationStates(int16 currentSceneID) {
 	}
 }
 
+void Taskbar::playClickSound(uint index) {
+	auto *taskData = GetEngineData(TASK);
+	if (!taskData) {
+		return;
+	}
+	// A normal click plays the button's own click sound ("Click01").
+	const SoundDescription &clickSound = taskData->buttons[index].button.clickSound;
+	g_nancy->_sound->loadSound(clickSound);
+	g_nancy->_sound->playSound(clickSound);
+}
+
+void Taskbar::playRejectionSound(uint index) {
+	auto *taskData = GetEngineData(TASK);
+	if (!taskData) {
+		return;
+	}
+
+	const uint mode = _overrides[index].clickSoundMode;
+	if (mode == kClickSoundSilent) {
+		return;
+	}
+
+	// Clicking a disabled button plays a "popup unavailable" line from
+	// clickSoundName, over the clickSound's channel/volume. The mode forces a
+	// specific line (kClickSound0..2) or picks a random valid one; an empty /
+	// "NO SOUND" slot leaves the default click sound.
+	const TASK::ButtonRecord &rec = taskData->buttons[index];
+	SoundDescription sound = rec.button.clickSound;
+	Common::String chosen;
+
+	if (mode >= kClickSound0 && mode <= kClickSound2) {
+		const Common::String &n = rec.clickSoundName[mode - kClickSound0];
+		if (!n.empty() && !n.equalsIgnoreCase("NO SOUND")) {
+			chosen = n;
+		}
+	} else {
+		Common::String validAlts[TASK::kNumAltSounds];
+		uint numValid = 0;
+		for (uint s = 0; s < TASK::kNumAltSounds; ++s) {
+			const Common::String &n = rec.clickSoundName[s];
+			if (!n.empty() && !n.equalsIgnoreCase("NO SOUND")) {
+				validAlts[numValid++] = n;
+			}
+		}
+		if (numValid > 0) {
+			chosen = validAlts[g_nancy->_randomSource->getRandomNumber(numValid - 1)];
+		}
+	}
+
+	if (!chosen.empty()) {
+		sound.name = chosen;
+	}
+	g_nancy->_sound->loadSound(sound);
+	g_nancy->_sound->playSound(sound);
+}
+
 void Taskbar::handleInput(NancyInput &input) {
 	auto *taskData = GetEngineData(TASK);
 	assert(taskData);
 
 	_clickedButton = -1;
 
+	// Hit-test present slots. Enabled buttons get full hover/press/click;
+	// disabled buttons are still clickable, but only to play their "popup
+	// unavailable" rejection sound.
 	int newHovered = -1;
+	bool hoveredActive = false;
 	for (uint i = 0; i < TASK::kNumButtons; ++i) {
-		if (isButtonActive(i) && taskData->buttons[i].button.destRect.contains(input.mousePos)) {
+		if (!isButtonSlotUsed(taskData->buttons[i])) {
+			continue;
+		}
+		if (taskData->buttons[i].button.destRect.contains(input.mousePos)) {
 			newHovered = i;
+			hoveredActive = isButtonActive(i);
 			break;
 		}
 	}
 
-	// Update hover graphic on enter/exit. The previously-hovered button
-	// returns to its resting sprite (idle or notification) so it doesn't
-	// get stuck in hover/pressed after the cursor leaves.
+	// Update hover graphic on enter/exit. The previously-hovered button returns
+	// to its resting sprite; only enabled buttons swap to the hover sprite.
 	if (newHovered != _hoveredButton) {
 		if (_hoveredButton != -1) {
 			drawButton(_hoveredButton, restingState(_hoveredButton));
 		}
-		if (newHovered != -1) {
+		if (newHovered != -1 && hoveredActive) {
 			drawButton(newHovered, kButtonHover);
 		}
 		_hoveredButton = newHovered;
@@ -246,6 +338,15 @@ void Taskbar::handleInput(NancyInput &input) {
 
 	g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
 
+	// Disabled button: a click just plays the rejection sound, no popup.
+	if (!hoveredActive) {
+		if (input.input & NancyInput::kLeftMouseButtonUp) {
+			playRejectionSound(newHovered);
+			input.eatMouseInput();
+		}
+		return;
+	}
+
 	if (input.input & NancyInput::kLeftMouseButtonDown) {
 		// Mouse pressed: show the pressed sprite for the duration of the hold.
 		if (_buttonStates[newHovered] != kButtonPressed) {
@@ -262,13 +363,7 @@ void Taskbar::handleInput(NancyInput &input) {
 		drawButton(newHovered, kButtonHover);
 		_clickedButton = newHovered;
 
-		// Play the first available click sound for this button. The TASK
-		// chunk stores up to three alternates per button; if the slot is
-		// empty we silently skip.
-		const Common::String &snd = taskData->buttons[newHovered].clickSoundName[0];
-		if (!snd.empty()) {
-			g_nancy->_sound->playSound(snd);
-		}
+		playClickSound(newHovered);
 
 		input.eatMouseInput();
 	}
diff --git a/engines/nancy/ui/taskbar.h b/engines/nancy/ui/taskbar.h
index 3838ed4f3a6..9f1bc047332 100644
--- a/engines/nancy/ui/taskbar.h
+++ b/engines/nancy/ui/taskbar.h
@@ -64,6 +64,9 @@ public:
 	// sprite. Call after a scene change so the range check kicks in.
 	void updateNotificationStates(int16 currentSceneID);
 
+	// Set a disabled button's rejection-sound mode, from ControlUIItems (AR 29).
+	void setClickSoundMode(uint buttonIndex, uint mode);
+
 	// Returns the index of the button that was clicked this frame, or -1
 	// if none. Cleared on the next call to handleInput().
 	int getClickedButton() const { return _clickedButton; }
@@ -73,17 +76,30 @@ private:
 		kButtonIdle         = 0,
 		kButtonHover        = 1,
 		kButtonPressed      = 2,
-		kButtonDisabled     = 3,   // not clickable
+		kButtonDisabled     = 3,   // popup unavailable; click plays a rejection sound
 		kButtonNotification = 4    // popup has new content (badge sprite)
 	};
 
+	// Rejection-sound mode for a disabled button (ControlUIItems'
+	// autoOpenOrBadgeSound value). When a disabled button is clicked it plays a
+	// "popup unavailable" line from clickSoundName: the default picks a random
+	// valid one; kClickSound0..2 force a specific one; kClickSoundSilent is mute.
+	enum ClickSoundMode {
+		kClickSoundDefault = 0,
+		kClickSound0       = 10,
+		kClickSound1       = 11,
+		kClickSound2       = 12,
+		kClickSoundSilent  = 13
+	};
+
 	// A scene-ranged disable override for one button. While active and the
-	// current scene is within [startScene, endScene] the button renders in
-	// the disabled state.
+	// current scene is within [startScene, endScene] the button renders in the
+	// disabled state; clicking it then plays the rejection sound (clickSoundMode).
 	struct ButtonOverride {
 		bool active = false;
 		int16 startScene = -1;
 		int16 endScene = -1;
+		uint clickSoundMode = kClickSoundDefault;
 	};
 
 	static const uint kNumNotificationSubCategories = 3;
@@ -93,6 +109,11 @@ private:
 	// True when the button currently accepts hover/click (not disabled).
 	bool isButtonActive(uint index) const;
 
+	// Play a normal click sound (the button's clickSound), or the "popup
+	// unavailable" rejection sound for a disabled button.
+	void playClickSound(uint index);
+	void playRejectionSound(uint index);
+
 	Graphics::ManagedSurface _backgroundImage; // TASK::imageName (e.g. "Frame")
 	Graphics::ManagedSurface _buttonImage;     // buttons' primaryImageName (e.g. "UIShared_OVL")
 	int _hoveredButton;


Commit: 28e29410569f2350a143fb97c868b4c9333089da
    https://github.com/scummvm/scummvm/commit/28e29410569f2350a143fb97c868b4c9333089da
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-06-29T04:56:34+03:00

Commit Message:
NANCY: NANCY12: Implement the new coin purse interface icon

Nancy12 replaces the (era-inappropriate) cell phone with a coin purse that
is not clickable but shows Nancy's money when hovered.

This uses the new AR 132, ResourceUse (UIRC boot data chunk)

Changed paths:
    engines/nancy/action/arfactory.cpp
    engines/nancy/action/miscrecords.cpp
    engines/nancy/action/miscrecords.h
    engines/nancy/enginedata.cpp
    engines/nancy/enginedata.h
    engines/nancy/puzzledata.cpp
    engines/nancy/puzzledata.h
    engines/nancy/state/scene.cpp
    engines/nancy/state/scene.h
    engines/nancy/ui/taskbar.cpp
    engines/nancy/ui/taskbar.h


diff --git a/engines/nancy/action/arfactory.cpp b/engines/nancy/action/arfactory.cpp
index 25600ac762e..dbc3e2eff6b 100644
--- a/engines/nancy/action/arfactory.cpp
+++ b/engines/nancy/action/arfactory.cpp
@@ -379,8 +379,7 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
 	case 132:
 		// ResourceUse ("UIResource Adjust") - adjusts a UI overlay resource (see the UIRC
 		// boot chunk) at runtime. Added in Nancy12.
-		// TODO: not implemented
-		return nullptr;
+		return new ResourceUse();
 	case 140:
 		if (g_nancy->getGameType() >= kGameTypeNancy12)
 			return new SetPlayerClock();	// Moved from 170 in Nancy12
diff --git a/engines/nancy/action/miscrecords.cpp b/engines/nancy/action/miscrecords.cpp
index 4131cac0c48..544e4a56517 100644
--- a/engines/nancy/action/miscrecords.cpp
+++ b/engines/nancy/action/miscrecords.cpp
@@ -751,5 +751,35 @@ void HintSystem::selectHint() {
 	}
 }
 
+void ResourceUse::readData(Common::SeekableReadStream &stream) {
+	_resourceIndex = stream.readSint16LE();   // which UIRC resource to change
+	_amount = stream.readSint16LE();           // value / delta
+	_mode = stream.readByte();                 // 0 = set, non-zero = add
+	_flag.label = stream.readSint16LE();       // event flag set on success
+	_flag.flag = stream.readByte();
+	// The rest of the 113-byte block (success/fail sounds, an optional scene
+	// change at +0x5b, and a transient "UIResource_Overlay" sprite) isn't
+	// handled yet.
+	stream.skip(0x71 - 8);
+}
+
+void ResourceUse::execute() {
+	if (_mode == 0) {
+		// Set the resource outright.
+		NancySceneState.setUIResource(_resourceIndex, _amount);
+		NancySceneState.setEventFlag(_flag);
+	} else {
+		// Add the (signed) amount, but never let the resource go negative —
+		// the original skips the change (e.g. when Nancy can't afford it).
+		const int32 result = NancySceneState.getUIResource(_resourceIndex) + _amount;
+		if (result >= 0) {
+			NancySceneState.setUIResource(_resourceIndex, result);
+			NancySceneState.setEventFlag(_flag);
+		}
+	}
+
+	_isDone = true;
+}
+
 } // End of namespace Action
 } // End of namespace Nancy
diff --git a/engines/nancy/action/miscrecords.h b/engines/nancy/action/miscrecords.h
index 54716cb49f1..13ba5ce3443 100644
--- a/engines/nancy/action/miscrecords.h
+++ b/engines/nancy/action/miscrecords.h
@@ -445,6 +445,22 @@ protected:
 	Common::String getRecordTypeName() const override { return "HintSystem"; }
 };
 
+// Added in Nancy12 (AR 132). Adjusts a UI overlay resource (from the UIRC boot
+// chunk) at runtime.
+class ResourceUse : public ActionRecord {
+public:
+	void readData(Common::SeekableReadStream &stream) override;
+	void execute() override;
+
+protected:
+	Common::String getRecordTypeName() const override { return "ResourceUse"; }
+
+	int16 _resourceIndex = 0;
+	int16 _amount = 0;
+	byte _mode = 0;        // 0 = set the resource, non-zero = add (clamped to >= 0)
+	FlagDescription _flag; // event flag set when the change is applied
+};
+
 } // End of namespace Action
 } // End of namespace Nancy
 
diff --git a/engines/nancy/enginedata.cpp b/engines/nancy/enginedata.cpp
index ba4c5db2d83..88e9128045f 100644
--- a/engines/nancy/enginedata.cpp
+++ b/engines/nancy/enginedata.cpp
@@ -890,8 +890,14 @@ TASK::TASK(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
 	readRect(*chunkStream, unkRect1);
 	readRect(*chunkStream, unkRect2);
 
+	// 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.
+	// the cell phone Nancy12 removed) is read but skipped by the taskbar.
+	const uint numButtons = MIN<uint>(kNumButtons,
+		(uint)(chunkStream->size() - chunkStream->pos()) / kButtonRecordSize);
+
 	char nameBuf[34];
-	for (uint i = 0; i < kNumButtons; ++i) {
+	for (uint i = 0; i < numButtons; ++i) {
 		readUIButton(*chunkStream, buttons[i].button);
 		readRect(*chunkStream, buttons[i].notificationSrcRect);
 		for (uint s = 0; s < kNumAltSounds; ++s) {
diff --git a/engines/nancy/enginedata.h b/engines/nancy/enginedata.h
index 05d7d363ba9..80311273362 100644
--- a/engines/nancy/enginedata.h
+++ b/engines/nancy/enginedata.h
@@ -520,7 +520,11 @@ enum TaskButton {
 	kTaskButtonInventory = 1,
 	kTaskButtonNotebook = 2,
 	kTaskButtonCellphone = 3,
-	kTaskButtonHelp = 4
+	// Nancy12 only: a non-clickable coin purse that shows Nancy's money on
+	// hover, inserted before HELP. HELP is therefore always the last taskbar
+	// button (index 4 in games without the coin purse, index 5 in Nancy12) and
+	// has no fixed constant.
+	kTaskButtonCoinPurse = 4
 };
 
 // Taskbar (the always-on strip at the bottom of the screen with MENU /
@@ -537,7 +541,9 @@ struct TASK : public EngineData {
 
 	TASK(Common::SeekableReadStream *chunkStream);
 
-	static const uint kNumButtons = 5;
+	// Maximum taskbar button slots: menu / inventory / notebook / cell phone /
+	// help, plus Nancy12's extra coin purse. Earlier games fill only the first 5.
+	static const uint kNumButtons = 6;
 	static const uint kButtonRecordSize = 354;
 	static const uint kNumAltSounds = 3;
 
diff --git a/engines/nancy/puzzledata.cpp b/engines/nancy/puzzledata.cpp
index 938d31bc253..1234c412be4 100644
--- a/engines/nancy/puzzledata.cpp
+++ b/engines/nancy/puzzledata.cpp
@@ -376,6 +376,20 @@ void TimerData::synchronize(Common::Serializer &ser) {
 	}
 }
 
+void UIResourceData::synchronize(Common::Serializer &ser) {
+	ser.syncAsByte(seeded);
+
+	uint16 numValues = (uint16)values.size();
+	ser.syncAsUint16LE(numValues);
+	if (ser.isLoading()) {
+		values.resize(numValues);
+	}
+
+	for (uint16 i = 0; i < numValues; ++i) {
+		ser.syncAsSint32LE(values[i]);
+	}
+}
+
 PuzzleData *makePuzzleData(const uint32 tag) {
 	switch(tag) {
 	case SliderPuzzleData::getTag():
@@ -408,6 +422,8 @@ PuzzleData *makePuzzleData(const uint32 tag) {
 		return new CellPhoneData();
 	case TimerData::getTag():
 		return new TimerData();
+	case UIResourceData::getTag():
+		return new UIResourceData();
 	default:
 		return nullptr;
 	}
diff --git a/engines/nancy/puzzledata.h b/engines/nancy/puzzledata.h
index fafedab7de4..392b394262b 100644
--- a/engines/nancy/puzzledata.h
+++ b/engines/nancy/puzzledata.h
@@ -286,6 +286,21 @@ struct TimerData : public PuzzleData {
 	Timer timers[kNumTimers];
 };
 
+// Nancy 12+ UI resource values (from the UIRC boot chunk), e.g. resource 0 is
+// the coin purse amount in cents. Seeded from UIRC on first use, mutated by AR
+// 132 (ResourceUse), and persisted between saves.
+struct UIResourceData : public PuzzleData {
+	UIResourceData() {}
+	virtual ~UIResourceData() {}
+
+	static constexpr uint32 getTag() { return MKTAG('U', 'R', 'E', 'S'); }
+	virtual void synchronize(Common::Serializer &ser);
+
+	// Set true once seeded from UIRC, so a loaded save isn't re-seeded.
+	bool seeded = false;
+	Common::Array<int32> values;
+};
+
 PuzzleData *makePuzzleData(const uint32 tag);
 
 } // End of namespace Nancy
diff --git a/engines/nancy/state/scene.cpp b/engines/nancy/state/scene.cpp
index 194514dbf7a..d4c4b0221d9 100644
--- a/engines/nancy/state/scene.cpp
+++ b/engines/nancy/state/scene.cpp
@@ -581,6 +581,42 @@ bool Scene::getEventFlag(FlagDescription eventFlag) const {
 	return getEventFlag(eventFlag.label, eventFlag.flag);
 }
 
+// On first use, seed each resource value from the UIRC boot chunk (record id =
+// initial value). After a save is loaded `seeded` is already true, so the
+// restored values are kept.
+static void seedUIResourceData(UIResourceData *data) {
+	if (!data || data->seeded) {
+		return;
+	}
+
+	const UIRC *uirc = GetEngineData(UIRC)
+
+	data->seeded = true;
+	if (uirc) {
+		data->values.resize(uirc->items.size());
+		for (uint i = 0; i < uirc->items.size(); ++i) {
+			data->values[i] = uirc->items[i].id;
+		}
+	}
+}
+
+int32 Scene::getUIResource(uint index) {
+	UIResourceData *data = (UIResourceData *)getPuzzleData(UIResourceData::getTag());
+	seedUIResourceData(data);
+	if (!data || index >= data->values.size()) {
+		return 0;
+	}
+	return data->values[index];
+}
+
+void Scene::setUIResource(uint index, int32 value) {
+	UIResourceData *data = (UIResourceData *)getPuzzleData(UIResourceData::getTag());
+	seedUIResourceData(data);
+	if (data && index < data->values.size()) {
+		data->values[index] = value;
+	}
+}
+
 // Nancy 11+ AR 30/31 store the "player scrolling disabled" state in an event
 // flag (eventData[0x21] in the original). It persists across scenes and is
 // saved/restored together with the rest of the event flags. Nancy12 shifted the
@@ -1391,10 +1427,13 @@ void Scene::handleInput() {
 			case kTaskButtonCellphone:
 				_cellPhonePopup.toggle();
 				break;
-			case kTaskButtonHelp:
-				requestStateChange(NancyState::kHelp);
+			case -1:
 				break;
 			default:
+				// HELP is always the last taskbar button. Its index shifts from
+				// 4 to 5 in Nancy12, where a non-clickable coin purse occupies slot
+				// 4 (and never reports a click), so match it as the fall-through.
+				requestStateChange(NancyState::kHelp);
 				break;
 			}
 		}
diff --git a/engines/nancy/state/scene.h b/engines/nancy/state/scene.h
index ce9cf8b9b8d..54d5b4b291b 100644
--- a/engines/nancy/state/scene.h
+++ b/engines/nancy/state/scene.h
@@ -153,6 +153,13 @@ public:
 	bool isSoftwareTimerActive(uint16 index) const;
 	uint32 getSoftwareTimerElapsed(uint16 index) const;
 
+	// Nancy 12+ UI resource values (the UIRC boot chunk). Resource 0 is the
+	// coin purse amount in cents. Backed by the lazily-created, saved
+	// UIResourceData puzzle chunk, seeded from UIRC on first use and mutated by
+	// AR 132 (ResourceUse). Non-const because the first access creates/seeds it.
+	int32 getUIResource(uint index);
+	void setUIResource(uint index, int32 value);
+
 	void setLogicCondition(int16 label, byte flag);
 	bool getLogicCondition(int16 label, byte flag) const;
 	void clearLogicConditions();
diff --git a/engines/nancy/ui/taskbar.cpp b/engines/nancy/ui/taskbar.cpp
index 6de8eeea5c3..a7951025af8 100644
--- a/engines/nancy/ui/taskbar.cpp
+++ b/engines/nancy/ui/taskbar.cpp
@@ -152,6 +152,46 @@ bool Taskbar::isButtonActive(uint index) const {
 	return _enabled[index] && restingState(index) != kButtonDisabled;
 }
 
+bool Taskbar::isMoneyDisplay(uint index) const {
+	return g_nancy->getGameType() == kGameTypeNancy12 && index == kTaskButtonCoinPurse;
+}
+
+void Taskbar::drawMoney() {
+	auto *taskData = GetEngineData(TASK);
+	const UIRC *uirc = GetEngineData(UIRC);
+	if (!taskData || !uirc || uirc->items.empty()) {
+		return;
+	}
+
+	// The coin purse displays UI resource 0: its current value rendered with a
+	// '$' prefix and `unknown2` decimal places. Old Clock tracks cents
+	// (decimals 2), so a value of 350 shows as "$3.50". `unknown1` selects the
+	// font. The live value lives in the scene state (seeded from UIRC, changed
+	// by AR 132); UIRC only supplies the formatting config.
+	const UIRC::ItemRecord &res = uirc->items[0];
+	if (res.unknown2 < 1) {
+		return;
+	}
+	const int32 value = NancySceneState.getUIResource(0);
+	const Common::String text =
+		Common::String::format("$%d.%02d", value / 100, value % 100);
+
+	const Font *font = g_nancy->_graphics->getFont(res.unknown1);
+	if (!font) {
+		return;
+	}
+
+	Common::Rect dst = taskData->buttons[kTaskButtonCoinPurse].button.destRect;
+	dst.translate(-_screenPosition.left, -_screenPosition.top);
+
+	// Position matches the original: a small inset from the left, and a little
+	// below the button's vertical centre.
+	const int x = dst.left + 12;
+	const int y = dst.top + dst.height() / 2 + 10;
+	font->drawString(&_drawSurface, text, x, y, dst.right - x, 0, Graphics::kTextAlignLeft);
+	_needsRedraw = true;
+}
+
 void Taskbar::toggleButton(uint index, bool enabled) {
 	if (index >= TASK::kNumButtons) {
 		return;
@@ -328,6 +368,9 @@ void Taskbar::handleInput(NancyInput &input) {
 		}
 		if (newHovered != -1 && hoveredActive) {
 			drawButton(newHovered, kButtonHover);
+			if (isMoneyDisplay(newHovered)) {
+				drawMoney();
+			}
 		}
 		_hoveredButton = newHovered;
 	}
@@ -338,6 +381,12 @@ void Taskbar::handleInput(NancyInput &input) {
 
 	g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
 
+	// The Nancy12 coin purse shows Nancy's money on hover but isn't clickable, so
+	// it skips the press/click handling below.
+	if (isMoneyDisplay(newHovered)) {
+		return;
+	}
+
 	// Disabled button: a click just plays the rejection sound, no popup.
 	if (!hoveredActive) {
 		if (input.input & NancyInput::kLeftMouseButtonUp) {
diff --git a/engines/nancy/ui/taskbar.h b/engines/nancy/ui/taskbar.h
index 9f1bc047332..781995c5363 100644
--- a/engines/nancy/ui/taskbar.h
+++ b/engines/nancy/ui/taskbar.h
@@ -109,6 +109,13 @@ private:
 	// True when the button currently accepts hover/click (not disabled).
 	bool isButtonActive(uint index) const;
 
+	// Nancy12 replaces the (era-inappropriate) cell phone with a coin purse that
+	// is not clickable but shows Nancy's money when hovered. True only for that
+	// game and button slot.
+	bool isMoneyDisplay(uint index) const;
+	// Draw Nancy's current money over the coin purse button (Nancy12 only).
+	void drawMoney();
+
 	// Play a normal click sound (the button's clickSound), or the "popup
 	// unavailable" rejection sound for a disabled button.
 	void playClickSound(uint index);
@@ -119,10 +126,11 @@ private:
 	int _hoveredButton;
 	int _clickedButton;
 	int16 _currentScene;
-	bool _enabled[5];
-	ButtonState _buttonStates[5];
-	ButtonOverride _overrides[5];
-	bool _notifications[5][kNumNotificationSubCategories];
+	// Sized to TASK::kNumButtons (the maximum, 6 — Nancy12's coin purse slot).
+	bool _enabled[6];
+	ButtonState _buttonStates[6];
+	ButtonOverride _overrides[6];
+	bool _notifications[6][kNumNotificationSubCategories];
 };
 
 } // End of namespace UI


Commit: b297542156803b355f42d9bc6c1063a7399f751e
    https://github.com/scummvm/scummvm/commit/b297542156803b355f42d9bc6c1063a7399f751e
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-06-29T04:56:36+03:00

Commit Message:
NANCY: NANCY10: Fix display of punctuation glyphs

Fixes the display of the dollar sign in the Nancy12 coin purse icon

Changed paths:
    engines/nancy/font.cpp


diff --git a/engines/nancy/font.cpp b/engines/nancy/font.cpp
index e247e5ede9b..abfa2179292 100644
--- a/engines/nancy/font.cpp
+++ b/engines/nancy/font.cpp
@@ -328,27 +328,6 @@ Common::Rect Font::getCharacterSourceRect(char chr) const {
 				break;
 			// TODO: _uppercaseAWithDotOffset
 			// TODO: _aWithDotOffset
-			case '\x5f':
-				offset = _underscoreOffset;
-				break;
-			case '\x23':
-				offset = _hashOffset;
-				break;
-			case '\x24':
-				offset = _dollarOffset;
-				break;
-			case '\x3c':
-				offset = _lessThanOffset;
-				break;
-			case '\x3e':
-				offset = _greaterThanOffset;
-				break;
-			case '\x7b':
-				offset = _leftCurlyBracketOffset;
-				break;
-			case '\x7d':
-				offset = _rightCurlyBracketOffset;
-				break;
 			// TODO: _euroOffset
 			default:
 				offset = -1;
@@ -417,6 +396,29 @@ Common::Rect Font::getCharacterSourceRect(char chr) const {
 		case '/':
 			offset = _slashOffset;
 			break;
+		// ASCII punctuation whose glyphs were added in nancy10. These are < 128,
+		// so they belong here rather than in the extended-ASCII switch above.
+		case '_':
+			offset = _underscoreOffset;
+			break;
+		case '#':
+			offset = _hashOffset;
+			break;
+		case '$':
+			offset = _dollarOffset;
+			break;
+		case '<':
+			offset = _lessThanOffset;
+			break;
+		case '>':
+			offset = _greaterThanOffset;
+			break;
+		case '{':
+			offset = _leftCurlyBracketOffset;
+			break;
+		case '}':
+			offset = _rightCurlyBracketOffset;
+			break;
 		default:
 			offset = -1;
 			break;


Commit: b033598c7bdd1e2806375c6bf5d96bdf0db96d43
    https://github.com/scummvm/scummvm/commit/b033598c7bdd1e2806375c6bf5d96bdf0db96d43
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-06-29T04:56:38+03:00

Commit Message:
NaNCY: NANCY12: Use a larger grid size for CollisionPuzzle

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


diff --git a/engines/nancy/action/puzzle/collisionpuzzle.cpp b/engines/nancy/action/puzzle/collisionpuzzle.cpp
index 27f78648859..41b918aa207 100644
--- a/engines/nancy/action/puzzle/collisionpuzzle.cpp
+++ b/engines/nancy/action/puzzle/collisionpuzzle.cpp
@@ -212,7 +212,13 @@ void CollisionPuzzle::readData(Common::SeekableReadStream &stream) {
 		numPieces = 6;
 	}
 
-	const uint maxGridSize = (_puzzleType == kTileMove && g_nancy->getGameType() >= kGameTypeNancy10) ? 11 : 8;
+	// Nancy 12 enlarged the Collision grid from 8x8 to 12x12; TileMove went 8 to 11 in Nancy 10
+	uint maxGridSize = 8;
+	if (_puzzleType == kTileMove && g_nancy->getGameType() >= kGameTypeNancy10) {
+		maxGridSize = 11;
+	} else if (_puzzleType == kCollision && g_nancy->getGameType() >= kGameTypeNancy12) {
+		maxGridSize = 12;
+	}
 
 	_grid.resize(height, Common::Array<uint16>(width));
 	for (uint y = 0; y < height; ++y) {


Commit: c4b0137f1046aa49504d320f10f22f32227c0173
    https://github.com/scummvm/scummvm/commit/c4b0137f1046aa49504d320f10f22f32227c0173
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-06-29T04:56:39+03:00

Commit Message:
NANCY: NANCY12: Add an initial stub for AR Set3DSoundListenerPosition

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


diff --git a/engines/nancy/action/arfactory.cpp b/engines/nancy/action/arfactory.cpp
index dbc3e2eff6b..75f3c6b709a 100644
--- a/engines/nancy/action/arfactory.cpp
+++ b/engines/nancy/action/arfactory.cpp
@@ -463,8 +463,7 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
 		// TODO: Nancy12 ChasePuzzle (new), not implemented
 		return nullptr;
 	case 168:
-		// TODO: Nancy12 - new 3D-sound positioning action record, not implemented
-		return nullptr;
+		return new Set3DSoundListenerPosition();
 	case 170:
 		// Moved to 140 in Nancy12
 		return new SetPlayerClock();
diff --git a/engines/nancy/action/soundrecords.cpp b/engines/nancy/action/soundrecords.cpp
index 76055a44abe..91171344f60 100644
--- a/engines/nancy/action/soundrecords.cpp
+++ b/engines/nancy/action/soundrecords.cpp
@@ -99,6 +99,17 @@ void Update3DSound::execute() {
 	_isDone = true;
 }
 
+void Set3DSoundListenerPosition::readData(Common::SeekableReadStream &stream) {
+	_posX = stream.readSint32LE();
+	_posY = stream.readSint32LE();
+	_posZ = stream.readSint16LE();
+}
+
+void Set3DSoundListenerPosition::execute() {
+	// TODO: forward the listener position to the sound manager.
+	_isDone = true;
+}
+
 void PlaySound::readData(Common::SeekableReadStream &stream) {
 	_sound.readDIGI(stream);
 
diff --git a/engines/nancy/action/soundrecords.h b/engines/nancy/action/soundrecords.h
index 3700c8454bb..b429e84ab85 100644
--- a/engines/nancy/action/soundrecords.h
+++ b/engines/nancy/action/soundrecords.h
@@ -78,6 +78,21 @@ protected:
 	Common::String getRecordTypeName() const override { return "Update3DSound"; }
 };
 
+// Added in Nancy12 (AR 168). Sets a 3D-sound position from a set of coordinates,
+// most likely the global listener position.
+class Set3DSoundListenerPosition : public ActionRecord {
+public:
+	void readData(Common::SeekableReadStream &stream) override;
+	void execute() override;
+
+	int32 _posX = 0;
+	int32 _posY = 0;
+	int32 _posZ = 0;
+
+protected:
+	Common::String getRecordTypeName() const override { return "Set3DSoundListenerPosition"; }
+};
+
 // Used for sound effects. From nancy3 up it includes 3D sound data, which lets
 // the sound move in 3D space as the player rotates/changes scenes. Also supports
 // changing the scene and/or setting a flag


Commit: d025c7ca4a0a7db475f3815535a456ee15f03ceb
    https://github.com/scummvm/scummvm/commit/d025c7ca4a0a7db475f3815535a456ee15f03ceb
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-06-29T04:56:40+03:00

Commit Message:
NANCY: Mark looping sound records as done

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


diff --git a/engines/nancy/action/soundrecords.cpp b/engines/nancy/action/soundrecords.cpp
index 91171344f60..fefda95c842 100644
--- a/engines/nancy/action/soundrecords.cpp
+++ b/engines/nancy/action/soundrecords.cpp
@@ -139,6 +139,14 @@ void PlaySound::execute() {
 			NancySceneState.setEventFlag(_flag);
 		}
 
+		// A looping sound with no scene change and no event flag is started and then
+		// left to play; the record is marked done at once instead of waiting on a sound
+		// that never ends.
+		if (_sceneChange.sceneID == kNoScene && _flag.label == kEvNoEvent && _sound.numLoops == 0) {
+			_isDone = true;
+			break;
+		}
+
 		if (_changeSceneImmediately) {
 			NancySceneState.changeScene(_sceneChange);
 			finishExecution();




More information about the Scummvm-git-logs mailing list