[Scummvm-git-logs] scummvm master -> dafa34cf1351b4abb8fa7f4233c847068de4c92d
bluegr
noreply at scummvm.org
Sat Jul 18 22:03:56 UTC 2026
This automated email contains information about 11 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
ec55373eee NANCY: NANCY13: Implement DropSortPuzzle
1466951785 NANCY: Disallow loading of saves created with a newer ScummVM version
99f43263c0 NANCY: NANCY10: Add arrow hover highlight on cellphone links
84ae5658d0 NANCY: NANCY10: Highlight items in the inventory on hover
7349ababbf NANCY: NANCY10: Fix sticky highlight on inventory/notebook tabs
e660521ff2 NANCY: NANCY10: Remove margin in bottom of journal
d66e833739 NANCY: NANCY10: Fix CPU spike when scrollbar is held
a14113a28e NANCY: NANCY10: Fix cursor of OverrideLockPuzzle
e8ccbb06f6 NANCY: NANCY10: Use cursors from puzzle data for RotatingLockPuzzle
b3c33f8a69 NANCY: NANCY10: Show the "Back" button in the phone ringing out screen
dafa34cf13 NANCY: NANCY10: Only clear taskbar badges when opening popups
Commit: ec55373eeeeaa1f4e08ca1eca5f0e60518cdc95d
https://github.com/scummvm/scummvm/commit/ec55373eeeeaa1f4e08ca1eca5f0e60518cdc95d
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-19T01:03:34+03:00
Commit Message:
NANCY: NANCY13: Implement DropSortPuzzle
Conveyor-belt candy-sorting puzzle, new in Nancy13 (AR 176). A hose on
the left drops random candies onto a belt that carries them left to
right; the player picks a candy up and drops it in a bin. Each sorting
bin takes one candy type; the "Rejects" bin at the belt's end takes the
types with no bin. A candy dropped in the wrong bin, or left to fall
off into the Rejects bin, is a mistake unless the Rejects bin accepts
it; three mistakes lose. Win once all candies are dispensed and the
belt is empty. The scene ships several copies as a difficulty ramp
(faster belt, shorter dispense interval).
Changed paths:
A engines/nancy/action/puzzle/dropsortpuzzle.cpp
A engines/nancy/action/puzzle/dropsortpuzzle.h
engines/nancy/action/arfactory.cpp
engines/nancy/module.mk
diff --git a/engines/nancy/action/arfactory.cpp b/engines/nancy/action/arfactory.cpp
index 386be86607a..b4d518e4925 100644
--- a/engines/nancy/action/arfactory.cpp
+++ b/engines/nancy/action/arfactory.cpp
@@ -47,6 +47,7 @@
#include "engines/nancy/action/puzzle/cuttingpuzzle.h"
#include "engines/nancy/action/puzzle/dotconnectpuzzle.h"
#include "engines/nancy/action/puzzle/drivingpuzzle.h"
+#include "engines/nancy/action/puzzle/dropsortpuzzle.h"
#include "engines/nancy/action/puzzle/gridmappuzzle.h"
#include "engines/nancy/action/puzzle/matchpuzzle.h"
#include "engines/nancy/action/puzzle/hamradiopuzzle.h"
@@ -205,8 +206,7 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
return new PlaySecondaryMovie();
case 42: // Nancy14
case 43: // Nancy14
- return new PlaySecondaryMovie(true);
- case 45: // Nancy11 - random-movie variant of PlaySecondaryMovie
+ case 45: // Nancy11
return new PlaySecondaryMovie(true);
case 46: // Nancy11
return new PlayRandomMovieControl();
@@ -490,9 +490,7 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
// TODO: not yet implemented
return nullptr;
case 176:
- // DropSortPuzzle (drag-drop sort activity), new in Nancy13
- // TODO: not yet implemented
- return nullptr;
+ return new DropSortPuzzle(); // conveyor-belt candy sorting
// -- Nancy14 new puzzles (types 177-182) --
case 177: // HangmanPuzzle
// TODO: not yet implemented
diff --git a/engines/nancy/action/puzzle/dropsortpuzzle.cpp b/engines/nancy/action/puzzle/dropsortpuzzle.cpp
new file mode 100644
index 00000000000..259039f320b
--- /dev/null
+++ b/engines/nancy/action/puzzle/dropsortpuzzle.cpp
@@ -0,0 +1,489 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/random.h"
+#include "common/util.h"
+
+#include "graphics/font.h"
+
+#include "engines/nancy/nancy.h"
+#include "engines/nancy/graphics.h"
+#include "engines/nancy/resource.h"
+#include "engines/nancy/sound.h"
+#include "engines/nancy/input.h"
+#include "engines/nancy/cursor.h"
+#include "engines/nancy/util.h"
+
+#include "engines/nancy/state/scene.h"
+#include "engines/nancy/action/puzzle/dropsortpuzzle.h"
+
+namespace Nancy {
+namespace Action {
+
+// Reads a count-prefixed array of int32 rects.
+static void readRects(Common::SeekableReadStream &stream, Common::Array<Common::Rect> &rects) {
+ uint16 count = stream.readUint16LE();
+ rects.resize(count);
+ for (uint i = 0; i < count; ++i) {
+ readRect(stream, rects[i]);
+ }
+}
+
+void DropSortPuzzle::readData(Common::SeekableReadStream &stream) {
+ readFilename(stream, _imageName); // 0x00
+ readFilename(stream, _hoseMovieName); // 0x21
+ readRect(stream, _hoseRect); // 0x42
+ _dispenseFrame = stream.readUint32LE(); // 0x52
+ readFilename(stream, _conveyorMovieName); // 0x56
+ readRect(stream, _conveyorRect); // 0x77
+ _totalItems = stream.readUint32LE(); // 0x87
+
+ // Belt path (left/right ends) and speed.
+ readRect(stream, _beltLeft);
+ readRect(stream, _beltRight);
+ _beltSpeed = stream.readSint32LE();
+
+ _dispenseInterval = stream.readSint32LE();
+ _hoverCursorType = stream.readUint16LE(); // raw Nancy13 cursor type ids
+ _dragCursorType = stream.readUint16LE();
+
+ readRects(stream, _itemSrcRects); // candy sprites in the overlay image
+ _unk1ad = stream.readSint32LE();
+
+ uint16 numBins = stream.readUint16LE();
+ _bins.resize(numBins);
+ for (uint i = 0; i < numBins; ++i) {
+ Bin &b = _bins[i];
+ readRect(stream, b.region);
+ b.enabled = stream.readByte();
+ uint16 numAccepted = stream.readUint16LE();
+ b.accepted.resize(numAccepted);
+ for (uint j = 0; j < numAccepted; ++j) {
+ b.accepted[j] = stream.readSint16LE();
+ }
+ }
+
+ _showCounter = stream.readByte();
+ _counterX = stream.readSint32LE(); // viewport position of the "Taffy to sort" number
+ _counterY = stream.readSint32LE();
+ _atlasId = stream.readUint16LE();
+
+ readRects(stream, _strikeSrcRects); // strike marker sprites in the overlay image
+ readRects(stream, _strikeDestRects); // where the strike markers are drawn
+
+ _pickupSound.readData(stream);
+ _dropSound.readData(stream);
+ _hornSound.readData(stream);
+
+ // Win scene + flag. frameID 0xffff means "no specific frame" (target may be a video) - keep 0.
+ _winScene.sceneID = stream.readUint16LE();
+ uint16 winFrame = stream.readUint16LE();
+ _winScene.frameID = (winFrame == 0xffff) ? 0 : winFrame;
+ _winScene.continueSceneSound = kContinueSceneSound;
+ _winFlag.label = stream.readSint16LE();
+ _winFlag.flag = stream.readByte();
+
+ _winSound.readData(stream);
+
+ _loseScene.sceneID = stream.readUint16LE();
+ uint16 loseFrame = stream.readUint16LE();
+ _loseScene.frameID = (loseFrame == 0xffff) ? 0 : loseFrame;
+ _loseScene.continueSceneSound = kContinueSceneSound;
+ _loseFlag.label = stream.readSint16LE();
+ _loseFlag.flag = stream.readByte();
+
+ _loseSound.readData(stream);
+
+ // Count-prefixed 23-byte hotspot records; the first is the "give up / exit" hotspot.
+ int16 numZones = stream.readSint16LE();
+ for (int16 i = 0; i < numZones; ++i) {
+ Common::Rect r;
+ readRect(stream, r);
+ uint16 cursorType = stream.readUint16LE();
+ uint16 sceneID = stream.readUint16LE();
+ uint16 frameID = stream.readUint16LE();
+ stream.skip(1); // trailing flag
+
+ if (i == 0) {
+ _exitHotspot = r;
+ _exitCursorType = cursorType;
+ _exitScene.sceneID = sceneID;
+ _exitScene.frameID = (frameID == 0xffff) ? 0 : frameID;
+ }
+ }
+}
+
+void DropSortPuzzle::init() {
+ Common::Rect vpBounds = NancySceneState.getViewport().getBounds();
+ _drawSurface.create(vpBounds.width(), vpBounds.height(), g_nancy->_graphics->getInputPixelFormat());
+ _drawSurface.clear(g_nancy->_graphics->getTransColor());
+ setTransparent(true);
+ setVisible(true);
+ moveTo(vpBounds);
+
+ g_nancy->_resource->loadImage(_imageName, _image);
+ _image.setTransparentColor(_drawSurface.getTransparentColor());
+
+ // Both animations loop for the whole puzzle.
+ if (_conveyorMovie.loadFile(_conveyorMovieName)) {
+ _conveyorMovie.playRange(0, _conveyorMovie.getFrameCount() - 1);
+ }
+ if (_hoseMovie.loadFile(_hoseMovieName)) {
+ _hoseMovie.playRange(0, _hoseMovie.getFrameCount() - 1);
+ }
+
+ // Time to cross the belt, from its length and speed (guarded against bad data).
+ int dist = ABS(_beltRight.left - _beltLeft.left);
+ int speed = _beltSpeed > 0 ? _beltSpeed : 60;
+ _travelDuration = dist > 0 ? (uint32)((int64)dist * 1000 / speed) : 8000;
+ if (_travelDuration < 1000) {
+ _travelDuration = 8000;
+ }
+
+ _lastDispenseTime = g_nancy->getTotalPlayTime();
+
+ // The reject bin sits at the belt's end, so candies that fall off land in it.
+ Common::Point fallPoint = beltPosition(1.0f);
+ for (uint i = 0; i < _bins.size(); ++i) {
+ if (_bins[i].region.contains(fallPoint)) {
+ _rejectBin = (int)i;
+ break;
+ }
+ }
+
+ NancySceneState.setNoHeldItem();
+
+ redraw();
+ registerGraphics();
+}
+
+Common::Point DropSortPuzzle::beltPosition(float progress) const {
+ int16 x = (int16)(_beltLeft.left + (_beltRight.left - _beltLeft.left) * progress);
+ int16 y = (int16)((_beltLeft.top + _beltLeft.bottom) / 2);
+ return Common::Point(x, y);
+}
+
+Common::Rect DropSortPuzzle::itemDestAt(const Common::Point &pos, int16 type) const {
+ int w = _itemSrcRects[type].width();
+ int h = _itemSrcRects[type].height();
+ return Common::Rect((int16)(pos.x - w / 2), (int16)(pos.y - h / 2),
+ (int16)(pos.x - w / 2 + w), (int16)(pos.y - h / 2 + h));
+}
+
+int DropSortPuzzle::itemAtCursor(const Common::Point &mousePos) const {
+ uint32 now = g_nancy->getTotalPlayTime();
+ // Oldest (front-most) candy first, so overlapping sprites pick the one nearest the bins.
+ for (uint i = 0; i < _items.size(); ++i) {
+ float progress = (float)(now - _items[i].spawnTime) / (float)_travelDuration;
+ Common::Rect dest = itemDestAt(beltPosition(progress), _items[i].type);
+ if (NancySceneState.getViewport().convertViewportToScreen(dest).contains(mousePos)) {
+ return (int)i;
+ }
+ }
+ return -1;
+}
+
+int DropSortPuzzle::binAtCursor(const Common::Point &mousePos) const {
+ for (uint i = 0; i < _bins.size(); ++i) {
+ if (NancySceneState.getViewport().convertViewportToScreen(_bins[i].region).contains(mousePos)) {
+ return (int)i;
+ }
+ }
+ return -1;
+}
+
+Common::Point DropSortPuzzle::cursorToViewport(const Common::Point &mousePos) const {
+ Common::Rect screenPt(mousePos.x, mousePos.y, mousePos.x + 1, mousePos.y + 1);
+ Common::Rect vpPt = NancySceneState.getViewport().convertScreenToViewport(screenPt);
+ return Common::Point(vpPt.left, vpPt.top);
+}
+
+void DropSortPuzzle::applyDrop(int binIndex, int16 type) {
+ if (binIndex < 0 || binIndex >= (int)_bins.size()) {
+ return;
+ }
+
+ const Bin &b = _bins[binIndex];
+ bool accepted = false;
+ for (uint i = 0; i < b.accepted.size(); ++i) {
+ if (b.accepted[i] == type) {
+ accepted = true;
+ break;
+ }
+ }
+
+ if (accepted) {
+ playSoundBlock(_dropSound);
+ } else if (b.enabled) {
+ // Wrong bin: a mistake.
+ ++_strikes;
+ playSoundBlock(_hornSound);
+ }
+}
+
+void DropSortPuzzle::redraw() {
+ _drawSurface.clear(g_nancy->_graphics->getTransColor());
+
+ uint32 now = g_nancy->getTotalPlayTime();
+
+ // The conveyor belt, behind everything.
+ if (_conveyorMovie.isVideoLoaded()) {
+ _conveyorMovie.drawFrame(_drawSurface, Common::Point(_conveyorRect.left, _conveyorRect.top));
+ }
+
+ // The candies riding the belt, drawn on top of the belt.
+ for (uint i = 0; i < _items.size(); ++i) {
+ float progress = (float)(now - _items[i].spawnTime) / (float)_travelDuration;
+ Common::Rect dest = itemDestAt(beltPosition(progress), _items[i].type);
+ _drawSurface.blitFrom(_image, _itemSrcRects[_items[i].type], Common::Point(dest.left, dest.top));
+ }
+
+ // The hose, over the candies: a fresh candy stays hidden under it until the belt carries it out.
+ if (_hoseMovie.isVideoLoaded()) {
+ _hoseMovie.drawFrame(_drawSurface, Common::Point(_hoseRect.left, _hoseRect.top));
+ }
+
+ // The strike markers earned so far.
+ for (int i = 0; i < _strikes && i < (int)_strikeDestRects.size() && i < (int)_strikeSrcRects.size(); ++i) {
+ _drawSurface.blitFrom(_image, _strikeSrcRects[i],
+ Common::Point(_strikeDestRects[i].left, _strikeDestRects[i].top));
+ }
+
+ // The candy currently being carried, following the cursor.
+ if (_carriedType != kNoItem) {
+ const Common::Rect &src = _itemSrcRects[_carriedType];
+ int w = src.width();
+ int h = src.height();
+ _drawSurface.blitFrom(_image, src, Common::Point(_dragPos.x - w / 2, _dragPos.y - h / 2));
+ }
+
+ drawCounter();
+
+ _needsRedraw = true;
+}
+
+// The "Taffy to sort" counter: candies still to be dispensed, drawn at the AR-data position
+// with the AR-data font id.
+void DropSortPuzzle::drawCounter() {
+ if (!_showCounter) {
+ return;
+ }
+
+ const Graphics::Font *font = g_nancy->_graphics->getFont(_atlasId);
+ if (!font) {
+ font = g_nancy->_graphics->getFont(0);
+ }
+ if (!font) {
+ return;
+ }
+
+ int remaining = (int)_totalItems - (int)_numDispensed;
+ if (remaining < 0) {
+ remaining = 0;
+ }
+
+ Common::String str = Common::String::format("%d", remaining);
+ int w = font->getStringWidth(str);
+ if (w <= 0) {
+ w = 24;
+ }
+
+ font->drawString(&_drawSurface, str, _counterX, _counterY, w + 4, 0);
+}
+
+SoundDescription DropSortPuzzle::playSoundBlock(const RandomSoundBlock &block) {
+ SoundDescription desc;
+ if (block.names.empty()) {
+ return desc;
+ }
+
+ uint idx = block.names.size() == 1 ? 0 : g_nancy->_randomSource->getRandomNumber(block.names.size() - 1);
+ const Common::String &name = block.names[idx];
+ if (name.empty() || name == "NO SOUND") {
+ return desc;
+ }
+
+ desc.name = name;
+ desc.channelID = block.channel;
+ desc.numLoops = block.numLoops > 0 ? block.numLoops : 1;
+ desc.volume = block.volume;
+
+ g_nancy->_sound->loadSound(desc);
+ g_nancy->_sound->playSound(desc);
+ return desc;
+}
+
+void DropSortPuzzle::execute() {
+ switch (_state) {
+ case kBegin:
+ init();
+ _state = kRun;
+ // fall through
+ case kRun: {
+ uint32 now = g_nancy->getTotalPlayTime();
+
+ if (_exitRequested) {
+ _state = kActionTrigger;
+ break;
+ }
+
+ if (!_ended) {
+ // Keep both animations looping.
+ if (_conveyorMovie.isVideoLoaded() && !_conveyorMovie.isRangePlaying()) {
+ _conveyorMovie.playRange(0, _conveyorMovie.getFrameCount() - 1);
+ }
+ if (_hoseMovie.isVideoLoaded() && !_hoseMovie.isRangePlaying()) {
+ _hoseMovie.playRange(0, _hoseMovie.getFrameCount() - 1);
+ }
+ bool moviesUpdated = false;
+ if (_conveyorMovie.isVideoLoaded() && _conveyorMovie.update()) {
+ moviesUpdated = true;
+ }
+ if (_hoseMovie.isVideoLoaded() && _hoseMovie.update()) {
+ moviesUpdated = true;
+ }
+
+ // Dispense on the data's interval (ramps with the difficulty variant).
+ int interval = _dispenseInterval > 0 ? _dispenseInterval : 2000;
+ if (now - _lastDispenseTime >= (uint32)interval &&
+ _numDispensed < _totalItems && !_itemSrcRects.empty()) {
+ BeltItem item;
+ item.type = (int16)g_nancy->_randomSource->getRandomNumber(_itemSrcRects.size() - 1);
+ item.spawnTime = now;
+ _items.push_back(item);
+ ++_numDispensed;
+ _lastDispenseTime = now;
+ }
+
+ // Candies reaching the belt's end fall into the reject bin (a mistake unless it
+ // accepts them).
+ for (uint i = 0; i < _items.size();) {
+ if (now - _items[i].spawnTime >= _travelDuration) {
+ applyDrop(_rejectBin, _items[i].type);
+ _items.remove_at(i);
+ } else {
+ ++i;
+ }
+ }
+
+ // The belt and candies move every frame, so keep the overlay in sync.
+ if (moviesUpdated || !_items.empty() || _carriedType != kNoItem) {
+ redraw();
+ }
+
+ // Lose on three strikes; win once all candies are dispensed and the belt is empty.
+ if (_strikes >= (int)_strikeDestRects.size() && !_strikeDestRects.empty()) {
+ _ended = true;
+ _lost = true;
+ _endSound = playSoundBlock(_loseSound);
+ _endTime = now;
+ } else if (_numDispensed >= _totalItems && _items.empty() && _carriedType == kNoItem) {
+ _ended = true;
+ _solved = true;
+ _endSound = playSoundBlock(_winSound);
+ _endTime = now;
+ }
+ }
+
+ if (_ended) {
+ bool soundDone = _endSound.name.empty() || !g_nancy->_sound->isSoundPlaying(_endSound);
+ if (soundDone && now - _endTime > 500) {
+ _state = kActionTrigger;
+ }
+ }
+
+ break;
+ }
+ case kActionTrigger:
+ if (_exitRequested) {
+ if (_exitScene.sceneID != kNoScene) {
+ NancySceneState.changeScene(_exitScene);
+ }
+ } else if (_solved) {
+ NancySceneState.setEventFlag(_winFlag);
+ if (_winScene.sceneID != kNoScene) {
+ NancySceneState.changeScene(_winScene);
+ }
+ } else {
+ NancySceneState.setEventFlag(_loseFlag);
+ if (_loseScene.sceneID != kNoScene) {
+ NancySceneState.changeScene(_loseScene);
+ }
+ }
+
+ finishExecution();
+ break;
+ }
+}
+
+void DropSortPuzzle::handleInput(NancyInput &input) {
+ if (_state != kRun || _ended) {
+ return;
+ }
+
+ const bool click = (input.input & NancyInput::kLeftMouseButtonUp) != 0;
+
+ // -- Carrying a candy: it follows the cursor; drop it into a bin. --
+ if (_carriedType != kNoItem) {
+ // Raw Nancy13 cursor type ids from the AR data, applied via the "set from script" path.
+ g_nancy->_cursor->setCursorType((CursorManager::CursorType)_dragCursorType, true);
+ _dragPos = cursorToViewport(input.mousePos);
+ redraw();
+
+ if (click) {
+ int bin = binAtCursor(input.mousePos);
+ if (bin >= 0) {
+ applyDrop(bin, _carriedType);
+ _carriedType = kNoItem;
+ redraw();
+ }
+ }
+
+ input.eatMouseInput();
+ return;
+ }
+
+ // -- Not carrying: pick a candy off the belt, or leave via the exit hotspot. --
+ int item = itemAtCursor(input.mousePos);
+ if (item >= 0) {
+ g_nancy->_cursor->setCursorType((CursorManager::CursorType)_hoverCursorType, true);
+ if (click) {
+ _carriedType = _items[item].type;
+ _items.remove_at(item);
+ _dragPos = cursorToViewport(input.mousePos);
+ playSoundBlock(_pickupSound);
+ redraw();
+ }
+ input.eatMouseInput();
+ return;
+ }
+
+ if (!_exitHotspot.isEmpty() &&
+ NancySceneState.getViewport().convertViewportToScreen(_exitHotspot).contains(input.mousePos)) {
+ g_nancy->_cursor->setCursorType((CursorManager::CursorType)_exitCursorType, true);
+ if (click) {
+ _exitRequested = true;
+ }
+ }
+}
+
+} // End of namespace Action
+} // End of namespace Nancy
diff --git a/engines/nancy/action/puzzle/dropsortpuzzle.h b/engines/nancy/action/puzzle/dropsortpuzzle.h
new file mode 100644
index 00000000000..64603d2c668
--- /dev/null
+++ b/engines/nancy/action/puzzle/dropsortpuzzle.h
@@ -0,0 +1,159 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef NANCY_ACTION_DROPSORTPUZZLE_H
+#define NANCY_ACTION_DROPSORTPUZZLE_H
+
+#include "engines/nancy/commontypes.h"
+#include "engines/nancy/movieplayer.h"
+#include "engines/nancy/action/actionrecord.h"
+
+namespace Nancy {
+namespace Action {
+
+// Conveyor-belt candy-sorting puzzle, new in Nancy13 (AR 176). A hose on
+// the left drops random candies onto a belt that carries them left to
+// right; the player picks a candy up and drops it in a bin. Each sorting
+// bin takes one candy type; the "Rejects" bin at the belt's end takes the
+// types with no bin. A candy dropped in the wrong bin, or left to fall
+// off into the Rejects bin, is a mistake unless the Rejects bin accepts
+// it; three mistakes lose. Win once all candies are dispensed and the
+// belt is empty. The scene ships several copies as a difficulty ramp
+// (faster belt, shorter dispense interval).
+class DropSortPuzzle : public RenderActionRecord {
+public:
+ DropSortPuzzle() : RenderActionRecord(7) {}
+ virtual ~DropSortPuzzle() {}
+
+ void init() override;
+
+ void readData(Common::SeekableReadStream &stream) override;
+ void execute() override;
+ void handleInput(NancyInput &input) override;
+
+ bool isViewportRelative() const override { return true; }
+
+protected:
+ Common::String getRecordTypeName() const override { return "DropSortPuzzle"; }
+
+ static const int16 kNoItem = -1;
+
+ struct Bin {
+ Common::Rect region; // the clickable drop zone
+ byte enabled = 0; // whether a wrong drop here counts as a strike
+ Common::Array<int16> accepted; // candy types this bin accepts
+ };
+
+ struct BeltItem {
+ int16 type = 0; // index into _itemSrcRects
+ uint32 spawnTime = 0; // dispense time, used to interpolate belt position
+ };
+
+ // Belt position (viewport space) at normalized progress: 0 = dispensed at the left,
+ // 1 = about to fall off at the right.
+ Common::Point beltPosition(float progress) const;
+ Common::Rect itemDestAt(const Common::Point &pos, int16 type) const;
+ int itemAtCursor(const Common::Point &mousePos) const; // belt item under the cursor, or -1
+ int binAtCursor(const Common::Point &mousePos) const; // bin under the cursor, or -1
+ Common::Point cursorToViewport(const Common::Point &mousePos) const;
+
+ // Drops a candy into a bin: correct-bin sound if accepted, else a mistake (for an
+ // enabled bin). Also handles candies falling off the belt into the reject bin.
+ void applyDrop(int binIndex, int16 type);
+
+ void redraw();
+ void drawCounter();
+ SoundDescription playSoundBlock(const RandomSoundBlock &block);
+
+ // -- File data --
+ Common::Path _imageName; // 0x00 - overlay sprite sheet (candies, strikes)
+ Common::Path _hoseMovieName; // 0x21 - hose (dispenser) animation
+ Common::Rect _hoseRect; // 0x42
+ uint32 _dispenseFrame = 0; // 0x52 - unused in this port
+ Common::Path _conveyorMovieName; // 0x56 - belt (conveyor) animation
+ Common::Rect _conveyorRect; // 0x77
+ uint32 _totalItems = 0; // 0x87 - candies dispensed over the puzzle
+
+ Common::Rect _beltLeft; // belt left end (candies dispensed here)
+ Common::Rect _beltRight; // belt right end (candies fall off here)
+ int32 _beltSpeed = 0; // pixels per second
+ int32 _dispenseInterval = 0; // ms between candy dispenses
+ uint16 _hoverCursorType = 0; // raw Nancy13 cursor over a candy
+ uint16 _dragCursorType = 0; // raw Nancy13 cursor while carrying
+
+ Common::Array<Common::Rect> _itemSrcRects; // candy sprites in the overlay image
+ int32 _unk1ad = 0;
+
+ Common::Array<Bin> _bins;
+
+ byte _showCounter = 0;
+ int32 _counterX = 0; // viewport position of the "Taffy to sort" number
+ int32 _counterY = 0;
+ uint16 _atlasId = 0; // font id for the counter digits
+
+ Common::Array<Common::Rect> _strikeSrcRects; // strike marker sprites in the overlay image
+ Common::Array<Common::Rect> _strikeDestRects; // where the strike markers are drawn
+
+ RandomSoundBlock _pickupSound; // candy picked up
+ RandomSoundBlock _dropSound; // candy dropped in the correct bin
+ RandomSoundBlock _hornSound; // candy dropped in the wrong bin (a strike)
+
+ SceneChangeDescription _winScene;
+ FlagDescription _winFlag;
+ RandomSoundBlock _winSound;
+
+ SceneChangeDescription _loseScene;
+ FlagDescription _loseFlag;
+ RandomSoundBlock _loseSound;
+
+ Common::Rect _exitHotspot;
+ uint16 _exitCursorType = 0;
+ SceneChangeDescription _exitScene;
+
+ // -- Runtime state --
+ Common::Array<BeltItem> _items; // candies currently on the belt, oldest first
+ uint _numDispensed = 0;
+ uint32 _lastDispenseTime = 0;
+ uint32 _travelDuration = 0; // ms for a candy to cross the belt
+ int _rejectBin = -1; // bin at the end of the belt (unsorted candies land here)
+
+ int16 _carriedType = kNoItem;
+ Common::Point _dragPos;
+
+ int _strikes = 0;
+ bool _solved = false;
+ bool _lost = false;
+ bool _ended = false;
+ bool _exitRequested = false;
+ uint32 _endTime = 0;
+ SoundDescription _endSound;
+
+ // Decorative; both loop for the whole puzzle.
+ MoviePlayer _conveyorMovie;
+ MoviePlayer _hoseMovie;
+
+ Graphics::ManagedSurface _image;
+};
+
+} // End of namespace Action
+} // End of namespace Nancy
+
+#endif // NANCY_ACTION_DROPSORTPUZZLE_H
diff --git a/engines/nancy/module.mk b/engines/nancy/module.mk
index 97625d02f65..19aeacaeb11 100644
--- a/engines/nancy/module.mk
+++ b/engines/nancy/module.mk
@@ -31,6 +31,7 @@ MODULE_OBJS = \
action/puzzle/cuttingpuzzle.o \
action/puzzle/dotconnectpuzzle.o \
action/puzzle/drivingpuzzle.o \
+ action/puzzle/dropsortpuzzle.o \
action/puzzle/gridmappuzzle.o \
action/puzzle/hamradiopuzzle.o \
action/puzzle/leverpuzzle.o \
Commit: 14669517857467a88f2acedd5404703e845979d9
https://github.com/scummvm/scummvm/commit/14669517857467a88f2acedd5404703e845979d9
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-19T01:03:35+03:00
Commit Message:
NANCY: Disallow loading of saves created with a newer ScummVM version
Changed paths:
engines/nancy/POTFILES
engines/nancy/nancy.cpp
diff --git a/engines/nancy/POTFILES b/engines/nancy/POTFILES
index f1e708064c5..97e82493e5e 100644
--- a/engines/nancy/POTFILES
+++ b/engines/nancy/POTFILES
@@ -1,3 +1,4 @@
engines/nancy/input.cpp
engines/nancy/metaengine.cpp
+engines/nancy/nancy.cpp
engines/nancy/state/loadsave.cpp
diff --git a/engines/nancy/nancy.cpp b/engines/nancy/nancy.cpp
index b4ee347a02c..01630698704 100644
--- a/engines/nancy/nancy.cpp
+++ b/engines/nancy/nancy.cpp
@@ -26,6 +26,9 @@
#include "common/memstream.h"
#include "common/compression/installshield_cab.h"
#include "common/serializer.h"
+#include "common/translation.h"
+
+#include "gui/message.h"
#include "engines/nancy/nancy.h"
#include "engines/nancy/resource.h"
@@ -803,11 +806,18 @@ void NancyEngine::populateStaticData() {
}
Common::Error NancyEngine::synchronize(Common::Serializer &ser) {
- auto *bootSummary = GetEngineData(BSUM);
+ auto *bootSummary = GetEngineData(BSUM)
assert(bootSummary);
// Sync boot summary header, which includes full game title
ser.syncVersion(kSavegameVersion);
+
+ if (ser.getVersion() > kSavegameVersion) {
+ GUI::MessageDialog dialog(_s("Saved game was created with a newer version of ScummVM. Unable to load."));
+ dialog.runModal();
+ return Common::kUnknownError;
+ }
+
ser.matchBytes((const char *)bootSummary->header, 90);
// Sync scene and action records
Commit: 99f43263c0985569c49053919ca4a0381d38f16f
https://github.com/scummvm/scummvm/commit/99f43263c0985569c49053919ca4a0381d38f16f
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-19T01:03:36+03:00
Commit Message:
NANCY: NANCY10: Add arrow hover highlight on cellphone links
Fix #16963
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 e99f40c8897..874b3086ae4 100644
--- a/engines/nancy/ui/cellphonepopup.cpp
+++ b/engines/nancy/ui/cellphonepopup.cpp
@@ -252,6 +252,8 @@ void CellPhonePopup::open() {
_closeButtonHovered = false;
_scrollUpHovered = false;
_scrollDownHovered = false;
+ _helpButtonHovered = false;
+ _backButtonHovered = false;
_autoDialPending = false;
_pressedSlot = -1;
@@ -466,7 +468,7 @@ void CellPhonePopup::drawChrome() {
// it once a call is being placed (the connecting / "We're sorry" screens)
// and on every sub-screen that shows its own heading.
if (_screenState == kWelcome || _screenState == kDialing) {
- drawHelpButton(0);
+ drawHelpButton(_helpButtonHovered ? 1 : 0);
}
_needsRedraw = true;
}
@@ -1087,12 +1089,22 @@ void CellPhonePopup::drawBackButton(uint subButtonIndex) {
// subButtons[0] is the Back button in the lower ribbon (help / sub-screens);
// subButtons[7] is the Back button at the bottom of the zoomed content view.
const UICL::ThreeRectWidget &back = _uiclData->subButtons[subButtonIndex];
- if (back.srcRectIdle.isEmpty() || back.destRect.isEmpty()) {
+ if (back.destRect.isEmpty()) {
+ return;
+ }
+
+ // Highlight (pressed sprite, green arrow) when the cursor is over this
+ // button and it's the one currently accepting input.
+ const bool hovered = _backButtonHovered && (int)subButtonIndex == currentBackButtonIndex();
+ const Common::Rect &src = (hovered && !back.srcRectPressed.isEmpty())
+ ? back.srcRectPressed
+ : back.srcRectIdle;
+ if (src.isEmpty()) {
return;
}
const Common::Point chunkOrigin(_screenPosition.left, _screenPosition.top);
- _drawSurface.blitFrom(_spritesImage, back.srcRectIdle,
+ _drawSurface.blitFrom(_spritesImage, src,
Common::Point(back.destRect.left - chunkOrigin.x,
back.destRect.top - chunkOrigin.y));
}
@@ -1142,6 +1154,24 @@ Common::Rect CellPhonePopup::backButtonHitRect(uint subButtonIndex) const {
return r;
}
+int CellPhonePopup::currentBackButtonIndex() const {
+ // Mirrors the drawBackButton() calls in drawScreenContent: which
+ // subButtons slot holds the visible Back / HOME button per screen.
+ switch (_screenState) {
+ case kDirectory:
+ case kOnlineHub:
+ return 0;
+ case kWebList:
+ return 9;
+ case kEmailList:
+ return 7;
+ case kContentView:
+ return isHelpContentView() ? 0 : 7;
+ default:
+ return -1;
+ }
+}
+
const UICL::ThreeRectWidget &CellPhonePopup::scrollUpButton() const {
// Directory and help both scroll with the small-LCD arrow pair
// (subButtons[1]/[2]); the zoomed email / browser articles use [5]/[6].
@@ -1219,6 +1249,8 @@ void CellPhonePopup::enterScreenState(ScreenState newState) {
// Always redraw, so successive digit entries refresh the readout.
_screenState = newState;
_hoveredHubButton = -1;
+ _helpButtonHovered = false;
+ _backButtonHovered = false;
if (newState != kContentView) {
// Cancel a pending email-open flash unless we're completing it.
_openingEmailRow = -1;
@@ -1724,6 +1756,22 @@ void CellPhonePopup::handleInput(NancyInput &input) {
drawScreenContent();
}
+ // Green-arrow highlight for the captioned "> HELP" and "< BACK" / HOME
+ // buttons: swap to the pressed sprite while the cursor is over them.
+ const bool helpVisible = (_screenState == kWelcome || _screenState == kDialing);
+ const bool overHelp = helpVisible &&
+ !_uiclData->helpButton.destRect.isEmpty() &&
+ _uiclData->helpButton.destRect.contains(chunkMouse);
+ const int backIndex = currentBackButtonIndex();
+ const bool overBack = backIndex >= 0 &&
+ !_uiclData->subButtons[backIndex].destRect.isEmpty() &&
+ _uiclData->subButtons[backIndex].destRect.contains(chunkMouse);
+ if (overHelp != _helpButtonHovered || overBack != _backButtonHovered) {
+ _helpButtonHovered = overHelp;
+ _backButtonHovered = overBack;
+ drawScreenContent();
+ }
+
// The keypad is only on screen in the non-zoomed chrome (welcome / dialing /
// directory / help), so its slots are only interactive there. The zoomed
// web / email / browser views hide the keypad but keep the slots' dest rects
diff --git a/engines/nancy/ui/cellphonepopup.h b/engines/nancy/ui/cellphonepopup.h
index 25a6156f8cf..92342936589 100644
--- a/engines/nancy/ui/cellphonepopup.h
+++ b/engines/nancy/ui/cellphonepopup.h
@@ -219,6 +219,9 @@ private:
Common::Rect backLabelHitRect() const;
// Popup-local rect of a visible Back sub-button (subButtons[index]).
Common::Rect backButtonHitRect(uint subButtonIndex) const;
+ // subButtons index of the Back / HOME button visible in the current state,
+ // or -1 when none is shown. Used to drive its hover highlight.
+ int currentBackButtonIndex() const;
// Move the directory selection by delta, scrolling as needed.
void moveDirectorySelection(int delta);
@@ -237,6 +240,10 @@ private:
bool _closeButtonHovered = false;
bool _scrollUpHovered = false;
bool _scrollDownHovered = false;
+ // Green-arrow highlight state for the captioned "> HELP" (welcome / dialing)
+ // and "< BACK" / HOME sub-buttons: each swaps to its pressed sprite on hover.
+ bool _helpButtonHovered = false;
+ bool _backButtonHovered = false;
ScreenState _screenState = kWelcome;
Commit: 84ae5658d00eb1d80566da7a31b58e462ead2078
https://github.com/scummvm/scummvm/commit/84ae5658d00eb1d80566da7a31b58e462ead2078
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-19T01:03:37+03:00
Commit Message:
NANCY: NANCY10: Highlight items in the inventory on hover
Fix #16974
Changed paths:
engines/nancy/ui/inventorypopup.cpp
engines/nancy/ui/inventorypopup.h
diff --git a/engines/nancy/ui/inventorypopup.cpp b/engines/nancy/ui/inventorypopup.cpp
index c54c840c592..2c9c6a7ac2d 100644
--- a/engines/nancy/ui/inventorypopup.cpp
+++ b/engines/nancy/ui/inventorypopup.cpp
@@ -113,6 +113,10 @@ void InventoryPopup::close() {
void InventoryPopup::refreshGrid() {
rebuildVisibleList();
+ // The whole grid is redrawn below with every slot in its plain state, so
+ // any previous hover highlight is gone.
+ _hoveredSlot = -1;
+
drawBackground();
drawFilterTabs();
drawFilterCaption();
@@ -347,7 +351,7 @@ void InventoryPopup::playButtonClickSound(const UIButtonRecord &button) {
g_nancy->_sound->playSound(sound);
}
-void InventoryPopup::drawSlot(uint slotIndex, int16 itemId) {
+void InventoryPopup::drawSlot(uint slotIndex, int16 itemId, bool highlighted) {
if (slotIndex >= _uiivData->slotDestRects.size())
return;
@@ -355,13 +359,20 @@ void InventoryPopup::drawSlot(uint slotIndex, int16 itemId) {
return;
const INV::ItemDescription &desc = _invData->itemDescriptions[itemId];
- if (desc.sourceRect.isEmpty())
+
+ // Hovering an item shows its highlighted sprite (the icon with a blue glow
+ // baked in) instead of the plain one. Both are opaque and cell-sized, so
+ // one fully overwrites the other. Fall back to the plain sprite if the game
+ // has no highlighted variant.
+ const Common::Rect &src = (highlighted && !desc.highlightedSourceRect.isEmpty())
+ ? desc.highlightedSourceRect : desc.sourceRect;
+ if (src.isEmpty())
return;
const Common::Point chunkOrigin(_uiivData->header.normalDestRect.left,
_uiivData->header.normalDestRect.top);
const Common::Rect &slotDst = _uiivData->slotDestRects[slotIndex];
- _drawSurface.blitFrom(_itemIcons, desc.sourceRect,
+ _drawSurface.blitFrom(_itemIcons, src,
Common::Point(slotDst.left - chunkOrigin.x,
slotDst.top - chunkOrigin.y));
}
@@ -488,6 +499,18 @@ void InventoryPopup::handleInput(NancyInput &input) {
break;
}
+ // Swap the glow between slots when the hovered slot changes: restore the
+ // slot we left to its plain sprite and draw the newly hovered one with its
+ // highlighted sprite.
+ if (hoveredSlot != _hoveredSlot) {
+ if (_hoveredSlot != -1)
+ drawSlot((uint)_hoveredSlot, _slotItemIDs[_hoveredSlot], false);
+ if (hoveredSlot != -1)
+ drawSlot((uint)hoveredSlot, _slotItemIDs[hoveredSlot], true);
+ _hoveredSlot = hoveredSlot;
+ _needsRedraw = true;
+ }
+
if (hoveredSlot != -1) {
g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
diff --git a/engines/nancy/ui/inventorypopup.h b/engines/nancy/ui/inventorypopup.h
index 383354bd4d5..5838b0018ee 100644
--- a/engines/nancy/ui/inventorypopup.h
+++ b/engines/nancy/ui/inventorypopup.h
@@ -77,7 +77,7 @@ private:
static const uint kNumFilters = 3;
void drawBackground();
- void drawSlot(uint slotIndex, int16 itemId);
+ void drawSlot(uint slotIndex, int16 itemId, bool highlighted = false);
void drawFilterTabs();
void drawFilterTab(uint index, bool drawHover = false);
void drawFilterCaption();
@@ -128,6 +128,10 @@ private:
// Item ID currently shown in each of the 16 slots (-1 if empty).
int16 _slotItemIDs[kSlotsPerPage];
+ // Slot the cursor is currently hovering over (-1 if none). The hovered
+ // occupied slot is drawn with the item's highlighted (blue-glow) sprite.
+ int _hoveredSlot = -1;
+
// Slider state. Driven by header.slider.
float _scrollPos = 0.0f; // [0, 1]: 0 = top page, 1 = bottom page
bool _scrollbarDragging = false;
Commit: 7349ababbf4052a243b40cadddaaa219b23aa5f5
https://github.com/scummvm/scummvm/commit/7349ababbf4052a243b40cadddaaa219b23aa5f5
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-19T01:03:38+03:00
Commit Message:
NANCY: NANCY10: Fix sticky highlight on inventory/notebook tabs
Fix #16969
Changed paths:
engines/nancy/ui/inventorypopup.cpp
engines/nancy/ui/notebookpopup.cpp
diff --git a/engines/nancy/ui/inventorypopup.cpp b/engines/nancy/ui/inventorypopup.cpp
index 2c9c6a7ac2d..5c57e45d451 100644
--- a/engines/nancy/ui/inventorypopup.cpp
+++ b/engines/nancy/ui/inventorypopup.cpp
@@ -582,7 +582,6 @@ void InventoryPopup::handleInput(NancyInput &input) {
input.eatMouseInput();
return;
}
- break;
}
if (_filterHovered || wasHovered)
diff --git a/engines/nancy/ui/notebookpopup.cpp b/engines/nancy/ui/notebookpopup.cpp
index 32e2b78bcfe..ccad888f2bd 100644
--- a/engines/nancy/ui/notebookpopup.cpp
+++ b/engines/nancy/ui/notebookpopup.cpp
@@ -416,7 +416,6 @@ void NotebookPopup::handleInput(NancyInput &input) {
input.eatMouseInput();
return;
}
- break;
}
// Swallow clicks on the popup itself so they don't fall through.
Commit: e660521ff2b52306e75dc630aa8d9ead1cb0e879
https://github.com/scummvm/scummvm/commit/e660521ff2b52306e75dc630aa8d9ead1cb0e879
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-19T01:03:39+03:00
Commit Message:
NANCY: NANCY10: Remove margin in bottom of journal
Fix #16964
Changed paths:
engines/nancy/ui/notebookpopup.cpp
diff --git a/engines/nancy/ui/notebookpopup.cpp b/engines/nancy/ui/notebookpopup.cpp
index ccad888f2bd..ef632b1bda7 100644
--- a/engines/nancy/ui/notebookpopup.cpp
+++ b/engines/nancy/ui/notebookpopup.cpp
@@ -530,18 +530,17 @@ void NotebookPopup::drawContent() {
buildTextLines();
// Chunk's textRect already provides top padding from the chrome.
- // A small left inset gives breathing room; the bottom strip is
- // reserved so the last line clears the inner bevel.
+ // A small left inset gives breathing room. The original draws text
+ // across the full text-rect height (source blit height == dest rect
+ // height), so no bottom strip is reserved.
const uint16 fontID = _uinbData->primaryFontID;
const Font *font = g_nancy->_graphics->getFont(fontID);
- const int oW = font ? font->getCharWidth('o') : 0;
- const int leftInset = oW;
- const int bottomInset = oW;
+ const int leftInset = font ? font->getCharWidth('o') : 0;
Common::Rect hypertextBounds(leftInset, 0, _fullSurface.w, _fullSurface.h);
drawAllText(hypertextBounds, 0, fontID, fontID);
- const int visibleH = MAX<int>(0, localTextRect.height() - bottomInset);
+ const int visibleH = localTextRect.height();
const int maxScroll = MAX<int>(0, (int)_drawnTextHeight - visibleH);
const int safeMax = MAX<int>(0, (int)_fullSurface.h - visibleH);
int scrollY = (int)(_scrollPos * maxScroll);
Commit: d66e8337393b4875b67affd79cf7ddc123a4e851
https://github.com/scummvm/scummvm/commit/d66e8337393b4875b67affd79cf7ddc123a4e851
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-19T01:03:40+03:00
Commit Message:
NANCY: NANCY10: Fix CPU spike when scrollbar is held
Fix #16973
Changed paths:
engines/nancy/ui/scrolltextbox.cpp
diff --git a/engines/nancy/ui/scrolltextbox.cpp b/engines/nancy/ui/scrolltextbox.cpp
index 311deb4a184..c3d415ea3c9 100644
--- a/engines/nancy/ui/scrolltextbox.cpp
+++ b/engines/nancy/ui/scrolltextbox.cpp
@@ -326,13 +326,23 @@ void ScrollTextBox::handleInput(NancyInput &input) {
const int newThumbTop = localMouse.y - _scrollbarGrabOffset;
const int clamped = CLIP<int>(newThumbTop, trackLocal.top, trackLocal.top + travel);
- _scrollPos = travel > 0 ? (float)(clamped - trackLocal.top) / (float)travel : 0.0f;
+ const float newScrollPos = travel > 0 ? (float)(clamped - trackLocal.top) / (float)travel : 0.0f;
+ bool released = false;
if (input.input & NancyInput::kLeftMouseButtonUp) {
_scrollbarDragging = false;
+ released = true;
+ }
+
+ // Only re-render when the thumb actually moves (or the drag just ended, to
+ // repaint the thumb out of its pressed state). drawContent() re-lays out
+ // the whole text surface, so calling it every frame while the button is
+ // merely held down pins the CPU and makes dragging choppy.
+ if (newScrollPos != _scrollPos || released) {
+ _scrollPos = newScrollPos;
+ drawContent();
}
- drawContent();
input.eatMouseInput();
return;
}
Commit: a14113a28ec9234ca5960fbc71dc6e4a72839931
https://github.com/scummvm/scummvm/commit/a14113a28ec9234ca5960fbc71dc6e4a72839931
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-19T01:03:40+03:00
Commit Message:
NANCY: NANCY10: Fix cursor of OverrideLockPuzzle
Fix #16976
Changed paths:
engines/nancy/action/puzzle/overridelockpuzzle.cpp
diff --git a/engines/nancy/action/puzzle/overridelockpuzzle.cpp b/engines/nancy/action/puzzle/overridelockpuzzle.cpp
index 3c364f93910..94590f376a9 100644
--- a/engines/nancy/action/puzzle/overridelockpuzzle.cpp
+++ b/engines/nancy/action/puzzle/overridelockpuzzle.cpp
@@ -194,7 +194,8 @@ void OverrideLockPuzzle::handleInput(NancyInput &input) {
}
if (NancySceneState.getViewport().convertViewportToScreen(_hotspots[i]).contains(input.mousePos)) {
- g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+ g_nancy->_cursor->setCursorType(g_nancy->getGameType() >= kGameTypeNancy10 ?
+ CursorManager::kPuzzleArrow : CursorManager::kHotspot);
if (!g_nancy->_sound->isSoundPlaying(_buttonSound) && input.input & NancyInput::kLeftMouseButtonUp) {
drawButton(i, false);
Commit: e8ccbb06f61d55f5620ba05d5766cca7041ef690
https://github.com/scummvm/scummvm/commit/e8ccbb06f61d55f5620ba05d5766cca7041ef690
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-19T01:03:41+03:00
Commit Message:
NANCY: NANCY10: Use cursors from puzzle data for RotatingLockPuzzle
Fix #16968
Changed paths:
engines/nancy/action/puzzle/rotatinglockpuzzle.cpp
engines/nancy/action/puzzle/rotatinglockpuzzle.h
diff --git a/engines/nancy/action/puzzle/rotatinglockpuzzle.cpp b/engines/nancy/action/puzzle/rotatinglockpuzzle.cpp
index d8f9b4608c7..f31fd18169c 100644
--- a/engines/nancy/action/puzzle/rotatinglockpuzzle.cpp
+++ b/engines/nancy/action/puzzle/rotatinglockpuzzle.cpp
@@ -95,8 +95,20 @@ void RotatingLockPuzzle::readData(Common::SeekableReadStream &stream) {
_correctSequence.push_back(stream.readByte());
}
- const uint padding = isNancy10 ? 12 : 8;
- stream.skip(padding - numDials);
+ // The correct sequence occupies a fixed 8-byte slot regardless of dial count.
+ stream.skip(8 - numDials);
+
+ if (isNancy10) {
+ // Nancy 10 added per-puzzle cursor types for the up/down hotspots,
+ // stored right after the sequence slot. A value of 0 means "use the
+ // default movement cursor".
+ int16 upType = stream.readSint16LE();
+ int16 downType = stream.readSint16LE();
+ if (upType != 0)
+ _upCursorType = (CursorManager::CursorType)upType;
+ if (downType != 0)
+ _downCursorType = (CursorManager::CursorType)downType;
+ }
_clickSound.readNormal(stream);
@@ -212,7 +224,7 @@ void RotatingLockPuzzle::handleInput(NancyInput &input) {
for (uint i = 0; i < _upHotspots.size(); ++i) {
if (NancySceneState.getViewport().convertViewportToScreen(_upHotspots[i]).contains(input.mousePos)) {
- g_nancy->_cursor->setCursorType(CursorManager::kMoveUp);
+ g_nancy->_cursor->setCursorType(_upCursorType, true);
if (!g_nancy->_sound->isSoundPlaying(_clickSound) && input.input & NancyInput::kLeftMouseButtonUp) {
g_nancy->_sound->playSound(_clickSound);
@@ -230,7 +242,7 @@ void RotatingLockPuzzle::handleInput(NancyInput &input) {
for (uint i = 0; i < _downHotspots.size(); ++i) {
if (NancySceneState.getViewport().convertViewportToScreen(_downHotspots[i]).contains(input.mousePos)) {
- g_nancy->_cursor->setCursorType(CursorManager::kMoveDown);
+ g_nancy->_cursor->setCursorType(_downCursorType, true);
if (!g_nancy->_sound->isSoundPlaying(_clickSound) && input.input & NancyInput::kLeftMouseButtonUp) {
g_nancy->_sound->playSound(_clickSound);
diff --git a/engines/nancy/action/puzzle/rotatinglockpuzzle.h b/engines/nancy/action/puzzle/rotatinglockpuzzle.h
index 1e5f4402584..a0246bd50fc 100644
--- a/engines/nancy/action/puzzle/rotatinglockpuzzle.h
+++ b/engines/nancy/action/puzzle/rotatinglockpuzzle.h
@@ -23,6 +23,7 @@
#define NANCY_ACTION_ROTATINGLOCKPUZZLE_H
#include "engines/nancy/action/actionrecord.h"
+#include "engines/nancy/cursor.h"
namespace Nancy {
namespace Action {
@@ -46,6 +47,11 @@ public:
Common::Array<Common::Rect> _downHotspots;
Common::Array<byte> _correctSequence;
uint16 _iconsPerDial = 10;
+ // Cursor types shown while hovering a dial's up/down hotspot. Nancy 10+
+ // stores these per-puzzle (e.g. a crank uses the rotate-clockwise cursor);
+ // older games and unset fields default to the up/down movement cursors.
+ CursorManager::CursorType _upCursorType = CursorManager::kMoveUp;
+ CursorManager::CursorType _downCursorType = CursorManager::kMoveDown;
SoundDescription _clickSound;
SceneChangeWithFlag _solveExitScene;
uint16 _solveSoundDelay = 0;
Commit: b3c33f8a696b8c0bcadf7dda0f28043b1fbc698f
https://github.com/scummvm/scummvm/commit/b3c33f8a696b8c0bcadf7dda0f28043b1fbc698f
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-19T01:03:41+03:00
Commit Message:
NANCY: NANCY10: Show the "Back" button in the phone ringing out screen
Fix #16975
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 874b3086ae4..e80477a277f 100644
--- a/engines/nancy/ui/cellphonepopup.cpp
+++ b/engines/nancy/ui/cellphonepopup.cpp
@@ -511,12 +511,19 @@ void CellPhonePopup::drawScreenContent() {
// no digits to display, just the connecting animation.
drawConnectingSprite();
}
+ // Back button on the connecting strip cancels the ringing call.
+ if (isCallBackButtonActive()) {
+ drawBackButton(0);
+ }
break;
case kWaitPickup:
case kConnected:
drawConnectedLabel();
drawConnectingSprite();
+ if (isCallBackButtonActive()) {
+ drawBackButton(0);
+ }
break;
case kInvalidNumber:
@@ -1157,6 +1164,9 @@ Common::Rect CellPhonePopup::backButtonHitRect(uint subButtonIndex) const {
int CellPhonePopup::currentBackButtonIndex() const {
// Mirrors the drawBackButton() calls in drawScreenContent: which
// subButtons slot holds the visible Back / HOME button per screen.
+ if (isCallBackButtonActive()) {
+ return 0;
+ }
switch (_screenState) {
case kDirectory:
case kOnlineHub:
@@ -1258,6 +1268,17 @@ void CellPhonePopup::enterScreenState(ScreenState newState) {
drawScreenContent();
}
+void CellPhonePopup::cancelCall() {
+ if (!_callSound.name.empty()) {
+ g_nancy->_sound->stopSound(_callSound);
+ }
+ _autoDialPending = false;
+ _resolvedContact = -1;
+ _pressedSlot = -1;
+ resetDialPad();
+ enterScreenState(kWelcome);
+}
+
void CellPhonePopup::appendDigit(byte slotIndex) {
if (_dialedNumber.size() >= 11) {
return;
@@ -1734,6 +1755,25 @@ void CellPhonePopup::handleInput(NancyInput &input) {
}
if (transientCallState) {
+ // While ringing, only the Back button is live (cancels the call).
+ if (isCallBackButtonActive()) {
+ const Common::Rect backHit = backButtonHitRect(0);
+ const Common::Point popupMouse(input.mousePos.x - _screenPosition.left,
+ input.mousePos.y - _screenPosition.top);
+ const bool overBack = !backHit.isEmpty() && backHit.contains(popupMouse);
+ if (overBack != _backButtonHovered) {
+ _backButtonHovered = overBack;
+ drawScreenContent();
+ }
+ if (overBack) {
+ g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ input.eatMouseInput();
+ cancelCall();
+ return;
+ }
+ }
+ }
// Block the viewport from seeing the cursor (edge-pan, etc.).
input.eatMouseInput();
return;
diff --git a/engines/nancy/ui/cellphonepopup.h b/engines/nancy/ui/cellphonepopup.h
index 92342936589..3cc25bda5da 100644
--- a/engines/nancy/ui/cellphonepopup.h
+++ b/engines/nancy/ui/cellphonepopup.h
@@ -173,6 +173,17 @@ private:
void resetDialPad();
void enterScreenState(ScreenState newState);
+ // True while a player-placed call is ringing / waiting for pickup, so the
+ // connecting strip shows a Back button (subButtons[0]) that cancels it.
+ // Incoming calls have no Back button.
+ bool isCallBackButtonActive() const {
+ return (_screenState == kPlaceCall || _screenState == kWaitOutgoingRing ||
+ _screenState == kLookupContact || _screenState == kWaitPickup) &&
+ !_hasPendingCallScene && _uiclData &&
+ !_uiclData->subButtons[0].destRect.isEmpty();
+ }
+ // Cancel a ringing / waiting call and return to the welcome screen.
+ void cancelCall();
void appendDigit(byte slotIndex);
// Play a dial-pad key's DTMF tone. The name is a raw sound filename, so it
// is played through the phone's call-sound channel rather than the common
Commit: dafa34cf1351b4abb8fa7f4233c847068de4c92d
https://github.com/scummvm/scummvm/commit/dafa34cf1351b4abb8fa7f4233c847068de4c92d
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-19T01:03:42+03:00
Commit Message:
NANCY: NANCY10: Only clear taskbar badges when opening popups
Fix #16970
Changed paths:
engines/nancy/ui/cellphonepopup.cpp
engines/nancy/ui/notebookpopup.cpp
engines/nancy/ui/notebookpopup.h
engines/nancy/ui/taskbar.cpp
diff --git a/engines/nancy/ui/cellphonepopup.cpp b/engines/nancy/ui/cellphonepopup.cpp
index e80477a277f..7f93ea15f7a 100644
--- a/engines/nancy/ui/cellphonepopup.cpp
+++ b/engines/nancy/ui/cellphonepopup.cpp
@@ -264,7 +264,8 @@ void CellPhonePopup::open() {
g_nancy->_cursor->warpCursor(Common::Point(_screenPosition.left + _screenPosition.width() / 2,
_screenPosition.top + _screenPosition.height() / 2));
- NancySceneState.getTaskbar()->clearAllNotifications(kTaskButtonCellphone);
+ // Badge sub-categories clear when their list is viewed (enterScreenState),
+ // not on open.
if (!_uiclData->header.sounds[0].name.empty()) {
g_nancy->_sound->loadSound(_uiclData->header.sounds[0]);
@@ -1256,6 +1257,23 @@ void CellPhonePopup::resetDialPad() {
}
void CellPhonePopup::enterScreenState(ScreenState newState) {
+ // Viewing a list clears its badge: directory = sub 0, email = sub 1, web = sub 2.
+ if (UI::Taskbar *taskbar = NancySceneState.getTaskbar()) {
+ switch (newState) {
+ case kDirectory:
+ taskbar->clearNotification(kTaskButtonCellphone, 0);
+ break;
+ case kEmailList:
+ taskbar->clearNotification(kTaskButtonCellphone, 1);
+ break;
+ case kWebList:
+ taskbar->clearNotification(kTaskButtonCellphone, 2);
+ break;
+ default:
+ break;
+ }
+ }
+
// Always redraw, so successive digit entries refresh the readout.
_screenState = newState;
_hoveredHubButton = -1;
diff --git a/engines/nancy/ui/notebookpopup.cpp b/engines/nancy/ui/notebookpopup.cpp
index ef632b1bda7..82f8d215d02 100644
--- a/engines/nancy/ui/notebookpopup.cpp
+++ b/engines/nancy/ui/notebookpopup.cpp
@@ -131,7 +131,8 @@ void NotebookPopup::open() {
g_nancy->_cursor->warpCursor(Common::Point(_screenPosition.left + _screenPosition.width() / 2,
_screenPosition.top + _screenPosition.height() / 2));
- NancySceneState.getTaskbar()->clearAllNotifications(kTaskButtonNotebook);
+ // Only the tab being shown is acknowledged; the other keeps its badge.
+ clearActiveTabNotification();
// JournalData entries may have changed since the last open (added by
// ModifyListEntry, marked complete, etc.) â re-render content.
@@ -410,7 +411,7 @@ void NotebookPopup::handleInput(NancyInput &input) {
_scrollbarDragging = false;
playButtonClickSound(tab.button);
-
+ clearActiveTabNotification();
refreshContent();
}
input.eatMouseInput();
@@ -440,6 +441,19 @@ uint16 NotebookPopup::notebookJournalTabId() const {
return g_nancy->getGameType() >= kGameTypeNancy13 ? 0 : 1;
}
+void NotebookPopup::clearActiveTabNotification() {
+ if (!_uinbData || (uint)_activeTab >= kNumTabs) {
+ return;
+ }
+ UI::Taskbar *taskbar = NancySceneState.getTaskbar();
+ if (!taskbar) {
+ return;
+ }
+ // Journal = sub 0, Tasks = sub 1 (see ModifyListEntry).
+ const bool journalActive = (_uinbData->tabs[_activeTab].id == notebookJournalTabId());
+ taskbar->clearNotification(kTaskButtonNotebook, journalActive ? 0 : 1);
+}
+
void NotebookPopup::buildTextLines() {
if (!_uinbData)
return;
diff --git a/engines/nancy/ui/notebookpopup.h b/engines/nancy/ui/notebookpopup.h
index d592858e60a..16d383ad03c 100644
--- a/engines/nancy/ui/notebookpopup.h
+++ b/engines/nancy/ui/notebookpopup.h
@@ -98,6 +98,9 @@ private:
// ids from {1,2} to {0,1}, so the Journal id dropped from 1 to 0.
uint16 notebookJournalTabId() const;
+ // Clear the taskbar badge for the active tab (Journal = sub 0, Tasks = sub 1).
+ void clearActiveTabNotification();
+
// Populate HypertextParser's text-line list with the active tab's
// entries.
void buildTextLines();
diff --git a/engines/nancy/ui/taskbar.cpp b/engines/nancy/ui/taskbar.cpp
index 13e89c82143..4690fa7272e 100644
--- a/engines/nancy/ui/taskbar.cpp
+++ b/engines/nancy/ui/taskbar.cpp
@@ -483,11 +483,7 @@ void Taskbar::handleInput(NancyInput &input) {
} else if (input.input & NancyInput::kLeftMouseButtonUp) {
// Mouse released over the button: trigger the click action and
// snap the sprite back to hover (the cursor is still over it).
- // Acknowledging the click also clears any pending notifications
- // for this button â the popup will read them on entry.
- for (uint s = 0; s < kNumNotificationSubCategories; ++s) {
- _notifications[newHovered][s] = false;
- }
+ // Badges are cleared per-view inside the popup, not on click.
drawButton(newHovered, kButtonHover);
_clickedButton = newHovered;
More information about the Scummvm-git-logs
mailing list