[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