[Scummvm-git-logs] scummvm master -> 0de802eeef5f3237ff5ae3a8dcccac57e5764397
bluegr
noreply at scummvm.org
Sun Jul 19 23:47:17 UTC 2026
This automated email contains information about 7 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
5ff21b13d4 NANCY: NANCY10: Enhancements and cleanup for OneBuildPuzzle
5abc300871 NANCY: NANCY10: Use a deferred loader for Bink panoramas
2f66e6ae52 NANCY: NANCY10: Use correct cursors in GridMapPuzzle (Petroglyphs)
3961a828a8 NANCY: NANCY10: Don't disable the taskbar while SecondaryMove is active
35158dfb8b NANCY: NANCY10: Fix cursor repositioning during cellphone conversation
d73e479964 NANCY: NANCY10: Cellphone UI fixes
0de802eeef NANCY: Set the correct menu cursors for Nancy1-9 and Nancy10+ games
Commit: 5ff21b13d400c4c089bc508984b6a8d8d79b7552
https://github.com/scummvm/scummvm/commit/5ff21b13d400c4c089bc508984b6a8d8d79b7552
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-20T02:47:02+03:00
Commit Message:
NANCY: NANCY10: Enhancements and cleanup for OneBuildPuzzle
Fixes issues with the basked repair puzzle.
Fix #16991, #16992, #16993
Changed paths:
engines/nancy/action/puzzle/onebuildpuzzle.cpp
engines/nancy/action/puzzle/onebuildpuzzle.h
diff --git a/engines/nancy/action/puzzle/onebuildpuzzle.cpp b/engines/nancy/action/puzzle/onebuildpuzzle.cpp
index 6606a8e27a8..114f7646558 100644
--- a/engines/nancy/action/puzzle/onebuildpuzzle.cpp
+++ b/engines/nancy/action/puzzle/onebuildpuzzle.cpp
@@ -318,49 +318,35 @@ void OneBuildPuzzle::readData(Common::SeekableReadStream &stream) {
readFilename(stream, _dropAlt1Filename);
readFilename(stream, _dropAlt2Filename);
- _goodPlacementSound.readNormal(stream);
- readFilename(stream, _goodAlt1Filename);
- readFilename(stream, _goodAlt2Filename);
-
- _goodTexts.resize(3);
-
const CVTX *autotext = (const CVTX *)g_nancy->getEngineData("AUTOTEXT");
assert(autotext);
- Common::String unusedKey;
- char textBuf[200];
-
- for (uint i = 0; i < 3; ++i) {
- stream.read(textBuf, 200);
- assembleTextLine(textBuf, _goodTexts[i], 200);
- if (!_goodTexts[i].empty() && autotext->texts.contains(_goodTexts[i]))
- _goodTexts[i] = autotext->texts[_goodTexts[i]];
- }
- for (uint i = 0; i < 3; ++i)
- readFilename(stream, unusedKey);
+ _goodPlacementSound.readNormal(stream);
+ readFilename(stream, _goodAlt1Filename);
+ readFilename(stream, _goodAlt2Filename);
+ readPlacementTexts(stream, _goodTexts);
_badPlacementSound.readNormal(stream);
readFilename(stream, _badAlt1Filename);
readFilename(stream, _badAlt2Filename);
+ readPlacementTexts(stream, _badTexts);
- _badTexts.resize(3);
- for (uint i = 0; i < 3; ++i) {
- stream.read(textBuf, 200);
- assembleTextLine(textBuf, _badTexts[i], 200);
- if (!_badTexts[i].empty() && autotext->texts.contains(_badTexts[i]))
- _badTexts[i] = autotext->texts[_badTexts[i]];
- }
- for (uint i = 0; i < 3; ++i)
- readFilename(stream, unusedKey);
-
- stream.skip(4); // TODO: 4 bytes before solveScene, unknown.
+ // Piece hover/drag cursor, then exit cursor (handled via _puzzleExitCursor).
+ _pieceCursorType = stream.readSint16LE();
+ stream.skip(2);
_solveScene.readData(stream);
_completionSound.readNormal(stream);
- readFilename(stream, unusedKey);
+
+ // Completion caption: AUTOTEXT key if known, else inline text.
+ Common::String completionKey;
+ char textBuf[200];
+ readFilename(stream, completionKey);
stream.read(textBuf, 200);
- assembleTextLine(textBuf, _completionText, 200);
- if (!_completionText.empty() && autotext->texts.contains(_completionText))
- _completionText = autotext->texts[_completionText];
+ _completionText.clear();
+ if (!completionKey.empty() && autotext->texts.contains(completionKey))
+ _completionText = autotext->texts[completionKey];
+ else
+ assembleTextLine(textBuf, _completionText, 200);
_cancelScene.readData(stream);
readRect(stream, _exitHotspot);
@@ -473,7 +459,7 @@ void OneBuildPuzzle::handleInput(NancyInput &input) {
if (_isDragging) {
// Always update drag position while carrying a piece
updateDragPosition(mouseVP);
- g_nancy->_cursor->setCursorType(CursorManager::kCustom1);
+ setPieceCursor();
if (_solveState != kIdle)
return;
@@ -494,20 +480,22 @@ void OneBuildPuzzle::handleInput(NancyInput &input) {
Common::Rect slot = piece.slotRect;
- // Bounding-box must fit within slot +- tolerance. The original
- // engine doesn't check rotation separately; a rotated piece's
- // dimensions are reflected in gameRect, so a non-fitting rotation
- // is rejected by the rect inequalities below.
+ // Bounding-box must fit within slot +- tolerance.
bool nearSlot = (piece.gameRect.left >= slot.left - _slotTolerance &&
piece.gameRect.top >= slot.top - _slotTolerance &&
piece.gameRect.right <= slot.right + _slotTolerance &&
piece.gameRect.bottom <= slot.bottom + _slotTolerance);
+ // A piece only fits at its correct (unrotated) orientation; a
+ // 180-degree flip keeps the same bounding box, so proximity alone
+ // would accept an upside-down piece.
+ bool rotationOk = (piece.curRotation == 0);
+
bool orderOk = !_orderedPlacement ||
(_piecesPlaced < (uint16)_placementOrder.size() &&
_placementOrder[_piecesPlaced] == (int16)(_pickedUpPiece + 1));
- if (nearSlot && orderOk) {
+ if (nearSlot && rotationOk && orderOk) {
piece.gameRect = piece.slotRect;
piece.placed = true;
_correctlyPlaced = true;
@@ -563,7 +551,7 @@ void OneBuildPuzzle::handleInput(NancyInput &input) {
if (topmostAny != -1) {
if (topmostUnplaced != -1)
- g_nancy->_cursor->setCursorType(CursorManager::kCustom1);
+ setPieceCursor();
// Left click on an unplaced piece: pick it up
// Right click: pick it up and rotate it
@@ -601,6 +589,32 @@ void OneBuildPuzzle::handleInput(NancyInput &input) {
// --- Internal helpers ---
+void OneBuildPuzzle::readPlacementTexts(Common::SeekableReadStream &stream, Common::Array<Common::String> &out) {
+ const CVTX *autotext = (const CVTX *)g_nancy->getEngineData("AUTOTEXT");
+ assert(autotext);
+
+ Common::String keys[3];
+ for (uint i = 0; i < 3; ++i)
+ readFilename(stream, keys[i]);
+
+ char textBuf[200];
+ out.resize(3);
+ for (uint i = 0; i < 3; ++i) {
+ stream.read(textBuf, 200);
+ if (!keys[i].empty() && autotext->texts.contains(keys[i]))
+ out[i] = autotext->texts[keys[i]];
+ else
+ assembleTextLine(textBuf, out[i], 200);
+ }
+}
+
+void OneBuildPuzzle::setPieceCursor() {
+ if (g_nancy->getGameType() >= kGameTypeNancy10)
+ g_nancy->_cursor->setCursorType((CursorManager::CursorType)_pieceCursorType, true, false);
+ else
+ g_nancy->_cursor->setCursorType(CursorManager::kCustom1);
+}
+
void OneBuildPuzzle::updatePieceRender(int pieceIdx) {
Piece &p = _pieces[pieceIdx];
if (p.useAltSurface && !p.altSurface.empty()) {
diff --git a/engines/nancy/action/puzzle/onebuildpuzzle.h b/engines/nancy/action/puzzle/onebuildpuzzle.h
index 29d8cbe2903..d9bde97d369 100644
--- a/engines/nancy/action/puzzle/onebuildpuzzle.h
+++ b/engines/nancy/action/puzzle/onebuildpuzzle.h
@@ -98,6 +98,9 @@ protected:
// Filename only (no SoundDescription metadata).
Common::String _extraSoundName;
+ // Cursor type shown while hovering or carrying a piece (Nancy 10+).
+ int16 _pieceCursorType = 0;
+
// Post-placement sprite-sheet animation. _animRectA is the on-screen
// rect where the animation plays AND the click hotspot the user must
// activate after placing all pieces (e.g. the music-box crank in scene
@@ -197,6 +200,11 @@ protected:
// --- Internal methods ---
+ // Read a good/bad caption block: three AUTOTEXT keys then three inline
+ // texts; each caption uses its key if known, else the inline text.
+ void readPlacementTexts(Common::SeekableReadStream &stream, Common::Array<Common::String> &out);
+ void setPieceCursor();
+
void playPickupSound();
void playRotateSoundAndStartTimer();
void playDropSound();
Commit: 5abc300871dc49a5944712e0e4c04120c6a6e58d
https://github.com/scummvm/scummvm/commit/5abc300871dc49a5944712e0e4c04120c6a6e58d
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-20T02:47:03+03:00
Commit Message:
NANCY: NANCY10: Use a deferred loader for Bink panoramas
This follows the same pattern as the AVF deferred loader, and improves
the performance of the Bink panoramas in Nancy10 and newer games, in a
similar way as the AVF panoramas used in Nancy9 and older games.
Follow-up improvement for #16966
Changed paths:
engines/nancy/movieplayer.cpp
engines/nancy/movieplayer.h
diff --git a/engines/nancy/movieplayer.cpp b/engines/nancy/movieplayer.cpp
index 10fe1e814bf..ce685c3650f 100644
--- a/engines/nancy/movieplayer.cpp
+++ b/engines/nancy/movieplayer.cpp
@@ -24,13 +24,26 @@
#include "video/bink_decoder.h"
+#include "engines/nancy/nancy.h"
#include "engines/nancy/video.h"
+#include "engines/nancy/util.h"
#include "engines/nancy/commontypes.h"
#include "engines/nancy/movieplayer.h"
namespace Nancy {
+// Fills the Bink frame cache forward in spare time (see MoviePlayer::_frameCache).
+class BinkCacheLoader : public DeferredLoader {
+public:
+ BinkCacheLoader(MoviePlayer &owner) : _owner(owner) {}
+
+private:
+ bool loadInner() override { return _owner.fillNextCacheFrame(); }
+
+ MoviePlayer &_owner;
+};
+
MoviePlayer::MoviePlayer() {}
MoviePlayer::~MoviePlayer() {}
@@ -64,6 +77,9 @@ bool MoviePlayer::loadFile(const Common::Path &name, bool bidirectionalCache) {
_useFrameCache = bidirectionalCache && _videoType == kVideoPlaytypeBink;
if (_useFrameCache) {
_frameCache.resize(_decoder->getFrameCount());
+ _cacheFillNext = 0;
+ _cacheLoader.reset(new BinkCacheLoader(*this));
+ g_nancy->addDeferredLoader(_cacheLoader);
}
return true;
@@ -82,6 +98,8 @@ void MoviePlayer::close() {
}
void MoviePlayer::freeFrameCache() {
+ _cacheLoader.reset();
+ _cacheFillNext = 0;
for (Graphics::Surface &surf : _frameCache) {
surf.free();
}
@@ -89,6 +107,30 @@ void MoviePlayer::freeFrameCache() {
_useFrameCache = false;
}
+bool MoviePlayer::fillNextCacheFrame() {
+ // Skip frames already decoded by interactive scrubbing.
+ while (_cacheFillNext < _frameCache.size() && _frameCache[_cacheFillNext].getPixels()) {
+ ++_cacheFillNext;
+ }
+
+ if (_cacheFillNext >= _frameCache.size()) {
+ return true;
+ }
+
+ // Decode forward; only seek to resync after interactive scrubbing moved us.
+ if (_decoder->getCurFrame() + 1 != (int)_cacheFillNext) {
+ _decoder->seekToFrame(_cacheFillNext);
+ }
+
+ const Graphics::Surface *frame = _decoder->decodeNextFrame();
+ if (frame) {
+ _frameCache[_cacheFillNext].copyFrom(*frame);
+ }
+ ++_cacheFillNext;
+
+ return _cacheFillNext >= _frameCache.size();
+}
+
void MoviePlayer::start() { if (_decoder) _decoder->start(); }
void MoviePlayer::stop() { if (_decoder) _decoder->stop(); }
void MoviePlayer::pauseVideo(bool pause) { if (_decoder) _decoder->pauseVideo(pause); }
diff --git a/engines/nancy/movieplayer.h b/engines/nancy/movieplayer.h
index b8413882f92..d8f5aa8df05 100644
--- a/engines/nancy/movieplayer.h
+++ b/engines/nancy/movieplayer.h
@@ -44,6 +44,8 @@ class VideoDecoder;
namespace Nancy {
+class DeferredLoader;
+
// The single low-level interface to the AVF/Bink video decoders. Every consumer
// (PlaySecondaryMovie, PlaySecondaryVideo, Conversation, Viewport, Map,
// BoardGamePuzzle, ...) owns one of these instead of a raw Video::VideoDecoder,
@@ -104,17 +106,24 @@ public:
void drawFrame(Graphics::ManagedSurface &dst, const Common::Point &pos) const;
private:
+ friend class BinkCacheLoader;
+
void storeCurrentFrame();
void freeFrameCache();
+ bool fillNextCacheFrame(); // decode one uncached frame; true when the cache is full
Common::ScopedPtr<Video::VideoDecoder> _decoder;
byte _videoType = kVideoPlaytypeAVF;
// Decoded-frame cache for the Bink path (AVF caches internally). Bink seeking
// re-decodes from the previous keyframe, so caching keeps panorama scrubbing
- // fast. Enabled only when loadFile() is asked for a bidirectional cache.
+ // fast. Enabled only when loadFile() is asked for a bidirectional cache; the
+ // whole cache is then filled forward (the fast direction) in spare time by a
+ // deferred loader, so even the first turn is smooth.
bool _useFrameCache = false;
Common::Array<Graphics::Surface> _frameCache;
+ Common::SharedPtr<DeferredLoader> _cacheLoader;
+ uint _cacheFillNext = 0;
// Simple frame-range player state. _currentSurface points at the decoder's
// last-decoded frame (owned by it, valid until the next decode).
Commit: 2f66e6ae52d8e6b4def45dfc6edd1458859aab21
https://github.com/scummvm/scummvm/commit/2f66e6ae52d8e6b4def45dfc6edd1458859aab21
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-20T02:47:03+03:00
Commit Message:
NANCY: NANCY10: Use correct cursors in GridMapPuzzle (Petroglyphs)
Properly fix #16980
Changed paths:
engines/nancy/action/puzzle/gridmappuzzle.cpp
diff --git a/engines/nancy/action/puzzle/gridmappuzzle.cpp b/engines/nancy/action/puzzle/gridmappuzzle.cpp
index ec59d18b315..b229acd2e21 100644
--- a/engines/nancy/action/puzzle/gridmappuzzle.cpp
+++ b/engines/nancy/action/puzzle/gridmappuzzle.cpp
@@ -347,15 +347,16 @@ void GridMapPuzzle::handleInput(NancyInput &input) {
if (!hitMap)
hitItems = hitTestItems(mouseVP, iRow, iCol);
- // The whole puzzle uses the grab-hand cursor; only the exit hotspot differs.
- // Picking something up doesn't change it.
+ // Grid cells (map and items) use the grab-hand cursor; picking something up
+ // doesn't change it. The exit hotspot uses the puzzle-exit cursor, and every
+ // other area (letter headers, gaps) keeps the idle eyeglass.
if (!hitMap && !hitItems) {
if (!_exitHotspot.isEmpty() && _exitHotspot.contains(mouseVP)) {
g_nancy->_cursor->setCursorType(g_nancy->_cursor->_puzzleExitCursor);
if (input.input & NancyInput::kLeftMouseButtonUp)
_subState = kExitToCancel;
} else {
- g_nancy->_cursor->setCursorType(CursorManager::kDropHand);
+ g_nancy->_cursor->setCursorType(CursorManager::kNormal);
}
return;
}
Commit: 3961a828a88d2d9859d724ec2bad7cc997ce6d5d
https://github.com/scummvm/scummvm/commit/3961a828a88d2d9859d724ec2bad7cc997ce6d5d
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-20T02:47:04+03:00
Commit Message:
NANCY: NANCY10: Don't disable the taskbar while SecondaryMove is active
The Nancy10 taskbar handling code was added in the same block as the
previous games. Commit 4541d085 specifically disabled buttons that exit
the scene while a SecondaryMovie is active in Nancy9 and earlier games,
but the Nancy10 taskbar buttons are responsive in such cases.
Fix #16994
Changed paths:
engines/nancy/state/scene.cpp
diff --git a/engines/nancy/state/scene.cpp b/engines/nancy/state/scene.cpp
index 6ea15e351c6..c5feff686b8 100644
--- a/engines/nancy/state/scene.cpp
+++ b/engines/nancy/state/scene.cpp
@@ -1526,72 +1526,76 @@ void Scene::handleInput() {
_actionManager.handleInput(input);
- // Menu/help are disabled when a movie is active. The taskbar is also
- // skipped while the textbox is in open mode (it visually covers the
- // buttons, so they should not receive hover/clicks).
- if (!_activeMovie) {
- // While a Nancy 10+ popup (inventory / notebook / cellphone /
- // conversation) is open, the original disables the entire taskbar â
- // every button, including MENU and HELP. Skip the taskbar input so it
- // neither hovers nor reacts to clicks until the popup is closed.
- const bool popupOpen = g_nancy->getGameType() >= kGameTypeNancy10 &&
- !activePopupConfinement().isEmpty();
- if (_taskbar) {
- // Grey out the whole taskbar while a popup is open (matches the
- // original); restored automatically once the popup closes.
- _taskbar->setPopupLockout(popupOpen);
- }
- if (_taskbar && !_textbox.isFullMode() && !popupOpen) {
- // MENU and HELP leave gameplay entirely, which would cut off the
- // taskbar click sound. The original defers the transition until that
- // sound finishes, so we hold the click here and only switch state
- // once the button's click sound has stopped playing.
- if (_pendingTaskbarButton != -1) {
- auto *taskData = GetEngineData(TASK);
- if (!taskData || !g_nancy->_sound->isSoundPlaying(taskData->buttons[_pendingTaskbarButton].button.clickSound)) {
- NancyState::NancyState target = _pendingTaskbarButton == kTaskButtonMenu ? NancyState::kMainMenu : NancyState::kHelp;
- _pendingTaskbarButton = -1;
- requestStateChange(target);
- }
- } else {
- _taskbar->handleInput(input);
-
- int clicked = _taskbar->getClickedButton();
- switch (clicked) {
- case kTaskButtonMenu:
- _pendingTaskbarButton = kTaskButtonMenu;
- break;
- case kTaskButtonInventory:
- _inventoryPopup.toggle();
- break;
- case kTaskButtonNotebook: {
- // Nancy 11+ populates the notebook lazily: opening it first
- // runs a hidden prep scene (header.linkbackScene) whose ARs
- // add the journal / task entries. Games without a prep scene
- // (linkbackScene == kNoScene, e.g. Nancy 10) just toggle.
- const int16 prepScene = _notebookPopup.getPrepSceneID();
- if (!_notebookPopup.isVisible() && (uint16)prepScene != kNoScene) {
- startUIPrepScene(kUITypeNotebook, prepScene);
- } else {
- _notebookPopup.toggle();
- }
- break;
- }
- case kTaskButtonCellphone:
- _cellPhonePopup.toggle();
- break;
- 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.
- _pendingTaskbarButton = clicked;
- break;
+ // The whole Nancy 10+ taskbar (inventory / notebook / cell phone / MENU /
+ // HELP) stays usable even while a SecondaryMovie is playing; only the
+ // standalone Nancy <=9 menu/help buttons further down are disabled during a
+ // movie. While a Nancy 10+ popup (inventory / notebook / cellphone /
+ // conversation) is open, the original disables the entire taskbar â every
+ // button, including MENU and HELP. Skip the taskbar input so it neither
+ // hovers nor reacts to clicks until the popup is closed. The taskbar is also
+ // skipped while the textbox is in open mode, since it visually covers the
+ // buttons.
+ const bool popupOpen = g_nancy->getGameType() >= kGameTypeNancy10 &&
+ !activePopupConfinement().isEmpty();
+ if (_taskbar) {
+ // Grey out the whole taskbar while a popup is open (matches the
+ // original); restored automatically once the popup closes.
+ _taskbar->setPopupLockout(popupOpen);
+ }
+ if (_taskbar && !_textbox.isFullMode() && !popupOpen) {
+ // MENU and HELP leave gameplay entirely, which would cut off the
+ // taskbar click sound. The original defers the transition until that
+ // sound finishes, so we hold the click here and only switch state
+ // once the button's click sound has stopped playing.
+ if (_pendingTaskbarButton != -1) {
+ auto *taskData = GetEngineData(TASK);
+ if (!taskData || !g_nancy->_sound->isSoundPlaying(taskData->buttons[_pendingTaskbarButton].button.clickSound)) {
+ NancyState::NancyState target = _pendingTaskbarButton == kTaskButtonMenu ? NancyState::kMainMenu : NancyState::kHelp;
+ _pendingTaskbarButton = -1;
+ requestStateChange(target);
+ }
+ } else {
+ _taskbar->handleInput(input);
+
+ int clicked = _taskbar->getClickedButton();
+ switch (clicked) {
+ case kTaskButtonMenu:
+ _pendingTaskbarButton = kTaskButtonMenu;
+ break;
+ case kTaskButtonInventory:
+ _inventoryPopup.toggle();
+ break;
+ case kTaskButtonNotebook: {
+ // Nancy 11+ populates the notebook lazily: opening it first
+ // runs a hidden prep scene (header.linkbackScene) whose ARs
+ // add the journal / task entries. Games without a prep scene
+ // (linkbackScene == kNoScene, e.g. Nancy 10) just toggle.
+ const int16 prepScene = _notebookPopup.getPrepSceneID();
+ if (!_notebookPopup.isVisible() && (uint16)prepScene != kNoScene) {
+ startUIPrepScene(kUITypeNotebook, prepScene);
+ } else {
+ _notebookPopup.toggle();
}
+ break;
+ }
+ case kTaskButtonCellphone:
+ _cellPhonePopup.toggle();
+ break;
+ 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.
+ _pendingTaskbarButton = clicked;
+ break;
}
}
+ }
+ // The standalone Nancy <=9 menu/help buttons leave the scene, so they're
+ // disabled while a movie is active.
+ if (!_activeMovie) {
if (_menuButton) {
_menuButton->handleInput(input);
Commit: 35158dfb8b489176252818d45f6fcd7f70809899
https://github.com/scummvm/scummvm/commit/35158dfb8b489176252818d45f6fcd7f70809899
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-20T02:47:05+03:00
Commit Message:
NANCY: NANCY10: Fix cursor repositioning during cellphone conversation
Fix #16927
Changed paths:
engines/nancy/state/scene.cpp
engines/nancy/ui/cellphonepopup.h
diff --git a/engines/nancy/state/scene.cpp b/engines/nancy/state/scene.cpp
index c5feff686b8..9e66d491184 100644
--- a/engines/nancy/state/scene.cpp
+++ b/engines/nancy/state/scene.cpp
@@ -1411,8 +1411,11 @@ Common::Rect Scene::activePopupConfinement() const {
if (_notebookPopup.isVisible()) return _notebookPopup.getScreenPosition();
// The cellphone stays up during a call it placed, but the conversation
// (textbox) is the active UI then â don't confine the cursor to the phone,
- // or it fights the textbox as each new line starts.
- if (_cellPhonePopup.isVisible() && _activeConversation == nullptr)
+ // or it fights the textbox as each new line starts. _activeConversation
+ // can't gate this: it toggles per dialogue line (null between lines), so the
+ // confinement would flicker on and snap the cursor up into the phone. Gate on
+ // the phone's own call state instead, which stays set for the whole call.
+ if (_cellPhonePopup.isVisible() && !_cellPhonePopup.isInCall())
return _cellPhonePopup.getScreenPosition();
return Common::Rect();
}
diff --git a/engines/nancy/ui/cellphonepopup.h b/engines/nancy/ui/cellphonepopup.h
index 4a7ba6d5130..96f4fc79290 100644
--- a/engines/nancy/ui/cellphonepopup.h
+++ b/engines/nancy/ui/cellphonepopup.h
@@ -48,6 +48,11 @@ public:
void close();
void toggle() { if (_isVisible) close(); else open(); }
+ // True once a call has connected and the popup has scene-changed into the
+ // conversation. The phone is just decoration then and the conversation
+ // textbox is the active UI, so the cursor must not be confined to the phone.
+ bool isInCall() const { return _screenState == kConnected; }
+
// Swaps the welcome graphic for the No Signal / No Access / Old Email
// Only labels and blocks outgoing calls.
void setNoSignal(bool noSignal);
Commit: d73e479964e618439d94b68db4a1a00b94da54b5
https://github.com/scummvm/scummvm/commit/d73e479964e618439d94b68db4a1a00b94da54b5
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-20T02:47:06+03:00
Commit Message:
NANCY: NANCY10: Cellphone UI fixes
- Don't play a dialing-out tone when receiving a call - fix #16995
- Sort contacts alphabetically (including older saves) - fix #16997
- Show correct icons in the in-call UI - fix #16998
- Show correct icons in the 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 bc95aa4a8d9..9094a7c0dc9 100644
--- a/engines/nancy/ui/cellphonepopup.cpp
+++ b/engines/nancy/ui/cellphonepopup.cpp
@@ -19,6 +19,7 @@
*
*/
+#include "common/algorithm.h"
#include "common/system.h"
#include "engines/nancy/cursor.h"
@@ -41,6 +42,12 @@
namespace Nancy {
namespace UI {
+// Contacts are shown alphabetically by name (case-insensitive), matching
+// the original's CCellPhoneSortContacts.
+static bool contactNameLess(const UICL::Contact &a, const UICL::Contact &b) {
+ return a.name.compareToIgnoreCase(b.name) < 0;
+}
+
// Renders the engine's hypertext markup (colour / formatting tags) into a
// scratch surface, which the content view then blits into the LCD. Thin
// wrapper that exposes HypertextParser's protected rendering entry points.
@@ -135,6 +142,10 @@ void CellPhonePopup::init() {
_contacts = _uiclData->contacts;
}
+ // Keep the list sorted, including contacts restored from older saves that
+ // predate the alphabetical ordering.
+ Common::sort(_contacts.begin(), _contacts.end(), contactNameLess);
+
_screenState = kWelcome;
_dialedNumber.clear();
_resolvedContact = -1;
@@ -221,6 +232,8 @@ void CellPhonePopup::upsertContact(const UICL::Contact &c) {
_contacts.push_back(c);
}
+ Common::sort(_contacts.begin(), _contacts.end(), contactNameLess);
+
CellPhoneData *cellData = (CellPhoneData *)NancySceneState.getPuzzleData(CellPhoneData::getTag());
if (cellData) {
cellData->contacts = _contacts;
@@ -242,6 +255,7 @@ void CellPhonePopup::open() {
_contacts = cellData->contacts;
_noSignal = cellData->noSignal;
_batteryLow = cellData->batteryLow;
+ Common::sort(_contacts.begin(), _contacts.end(), contactNameLess);
}
_screenState = kWelcome;
@@ -364,7 +378,9 @@ void CellPhonePopup::updateGraphics() {
break;
case kPlaceCall:
- if (playSoundIfPresent(_uiclData->outgoingRingSound)) {
+ // Incoming calls skip the dial-out ring entirely (it's a manual-dial
+ // cue); they go straight to the pickup/connect step.
+ if (!_hasPendingCallScene && playSoundIfPresent(_uiclData->outgoingRingSound)) {
enterScreenState(kWaitOutgoingRing);
} else {
enterScreenState(kLookupContact);
@@ -477,11 +493,13 @@ void CellPhonePopup::drawChrome() {
void CellPhonePopup::drawScreenContent() {
drawChrome();
- // Original only draws the signal/battery indicators on the welcome
- // screen â every other state (dialing, ringing, connected, lists,
- // browser, etc.) hides them.
+ // Signal + battery indicators show on the welcome screen; the connected
+ // in-call screen keeps the battery but not the signal. Every other state
+ // (dialing, ringing, lists, browser, ...) hides both.
if (_screenState == kWelcome) {
- drawStatusIcons();
+ drawStatusIcons(true);
+ } else if (_screenState == kConnected) {
+ drawStatusIcons(false);
}
switch (_screenState) {
@@ -498,7 +516,7 @@ void CellPhonePopup::drawScreenContent() {
case kPlaceCall:
case kWaitOutgoingRing:
case kLookupContact:
- drawWebDirLabels();
+ // Web / Dir labels show only on the welcome screen, not while dialing.
if (!_dialedNumber.empty()) {
// User is manually dialing â show the dial header,
// "please dial a number" hint, the typed digits and
@@ -527,6 +545,8 @@ void CellPhonePopup::drawScreenContent() {
break;
case kConnected:
+ // In-call screen: keeps the Web / Dir labels (like the welcome screen).
+ drawWebDirLabels();
drawConnectedLabel();
drawConnectingSprite();
break;
@@ -592,14 +612,15 @@ void CellPhonePopup::drawScreenContent() {
_needsRedraw = true;
}
-void CellPhonePopup::drawStatusIcons() {
+void CellPhonePopup::drawStatusIcons(bool includeSignal) {
const Common::Point chunkOrigin(_screenPosition.left, _screenPosition.top);
- // Signal indicator uses the alt source when no-signal is set.
+ // Signal indicator uses the alt source when no-signal is set. It is hidden
+ // during a call (the connected screen keeps only the battery).
const Common::Rect &signalSrc = _noSignal && !_uiclData->signalSpriteSrcAlt.isEmpty()
? _uiclData->signalSpriteSrcAlt
: _uiclData->signalSpriteSrc;
- if (!signalSrc.isEmpty() && !_uiclData->signalSpriteDest.isEmpty()) {
+ if (includeSignal && !signalSrc.isEmpty() && !_uiclData->signalSpriteDest.isEmpty()) {
_drawSurface.blitFrom(_spritesImage, signalSrc,
Common::Point(_uiclData->signalSpriteDest.left - chunkOrigin.x,
_uiclData->signalSpriteDest.top - chunkOrigin.y));
@@ -966,6 +987,23 @@ void CellPhonePopup::renderContentPage(int surfaceWidth) {
_contentCacheHotspots = ht.hotspots();
}
+uint CellPhonePopup::contentScrollStep() const {
+ const Font *font = g_nancy->_graphics->getFont(_uiclData->fontId2);
+ if (!font) {
+ return 12;
+ }
+
+ // Original: one click scrolls ~1/10th of the article (capped near a full
+ // page), plus 1.25 line heights.
+ const Common::Rect &ws =
+ (isHelpContentView() || _uiclData->emailListContainer.isEmpty())
+ ? _uiclData->welcomeScreen.destRect
+ : _uiclData->emailListContainer;
+ const int viewH = MAX(0, ws.height() - 2);
+ int page = MIN((int)_contentCacheTextHeight / 10, MAX(0, viewH - 30));
+ return (font->getFontHeight() * 5) / 4 + page;
+}
+
void CellPhonePopup::drawContentView() {
if (_contentKey.empty()) {
return;
@@ -992,7 +1030,6 @@ void CellPhonePopup::drawContentView() {
// LCD, so the body text starts flush with the LCD top â a small inset only.
const int textTop = 2;
const int viewH = MAX(0, lcdH - textTop);
- const int rowH = MAX(font->getFontHeight() + 1, 12);
// (Re)render the page only when its key changes; scrolling and hover
// redraws reuse the cached surface (just re-blit a different window).
@@ -1002,14 +1039,13 @@ void CellPhonePopup::drawContentView() {
}
_contentHotspotTargets = _contentCacheTargets;
- // Clamp scroll to the rendered text height.
+ // Clamp scroll (a pixel offset) to the rendered text height.
const int maxScrollPx = MAX(0, (int)_contentCacheTextHeight - viewH);
- const int maxScroll = maxScrollPx / rowH;
- if ((int)_contentScroll > maxScroll) {
- _contentScroll = maxScroll;
+ if ((int)_contentScroll > maxScrollPx) {
+ _contentScroll = maxScrollPx;
}
- const int srcTop = (int)_contentScroll * rowH;
+ const int srcTop = (int)_contentScroll;
Common::Rect srcRect(0, srcTop, lcdW, srcTop + viewH);
srcRect.clip(Common::Rect(_contentCacheSurface.w, _contentCacheSurface.h));
if (srcRect.isEmpty()) {
@@ -2163,14 +2199,12 @@ void CellPhonePopup::handleInput(NancyInput &input) {
const Common::Rect &upDst = scrollUpButton().destRect;
const Common::Rect &downDst = scrollDownButton().destRect;
- // One click scrolls several lines, matching the original (a single line
- // per click makes long web pages tedious to read).
- const uint kContentScrollStep = 3;
+ const uint step = contentScrollStep();
if (upDst.contains(chunkMouse)) {
g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
if (input.input & NancyInput::kLeftMouseButtonUp) {
if (_contentScroll > 0) {
- _contentScroll = _contentScroll > kContentScrollStep ? _contentScroll - kContentScrollStep : 0;
+ _contentScroll = _contentScroll > step ? _contentScroll - step : 0;
drawScreenContent();
}
input.eatMouseInput();
@@ -2179,7 +2213,7 @@ void CellPhonePopup::handleInput(NancyInput &input) {
} else if (downDst.contains(chunkMouse)) {
g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
if (input.input & NancyInput::kLeftMouseButtonUp) {
- _contentScroll += kContentScrollStep;
+ _contentScroll += step;
drawScreenContent();
input.eatMouseInput();
return;
diff --git a/engines/nancy/ui/cellphonepopup.h b/engines/nancy/ui/cellphonepopup.h
index 96f4fc79290..f7f9f482cd5 100644
--- a/engines/nancy/ui/cellphonepopup.h
+++ b/engines/nancy/ui/cellphonepopup.h
@@ -106,7 +106,7 @@ private:
void drawChrome();
void drawScreenContent();
- void drawStatusIcons();
+ void drawStatusIcons(bool includeSignal = true);
void drawWebDirLabels();
void drawDialLabel();
void drawTypeMessage();
@@ -141,6 +141,8 @@ private:
// surface (+ text height, image/link hotspots). Called by drawContentView
// only when the page key changes.
void renderContentPage(int surfaceWidth);
+ // Per-click scroll amount (pixels) for the article/help content view.
+ uint contentScrollStep() const;
// Enter the content view for a list entry whose AUTOTEXT key is `key`.
void openContentView(const Common::String &key, const UICL::SrcDestRectPair &heading);
// Web button: open the first url entry as the browser home page (page 0).
Commit: 0de802eeef5f3237ff5ae3a8dcccac57e5764397
https://github.com/scummvm/scummvm/commit/0de802eeef5f3237ff5ae3a8dcccac57e5764397
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-20T02:47:07+03:00
Commit Message:
NANCY: Set the correct menu cursors for Nancy1-9 and Nancy10+ games
Properly fixes #16978
Changed paths:
engines/nancy/state/mainmenu.cpp
engines/nancy/state/savedialog.cpp
engines/nancy/state/setupmenu.cpp
diff --git a/engines/nancy/state/mainmenu.cpp b/engines/nancy/state/mainmenu.cpp
index 315c800cb5f..d81ecff36c5 100644
--- a/engines/nancy/state/mainmenu.cpp
+++ b/engines/nancy/state/mainmenu.cpp
@@ -84,7 +84,7 @@ void MainMenu::init() {
_background.init(_menuData->_imageName);
_background.registerGraphics();
- g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+ g_nancy->_cursor->setCursorType(g_nancy->getGameType() >= kGameTypeNancy10 ? CursorManager::kHotspotArrow : CursorManager::kNormalArrow);
g_nancy->setMouseEnabled(true);
if (!g_nancy->_sound->isSoundPlaying("MSND")) {
@@ -161,7 +161,7 @@ void MainMenu::run() {
}
}
- g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+ g_nancy->_cursor->setCursorType(g_nancy->getGameType() >= kGameTypeNancy10 ? CursorManager::kHotspotArrow : CursorManager::kNormalArrow);
}
void MainMenu::stop() {
diff --git a/engines/nancy/state/savedialog.cpp b/engines/nancy/state/savedialog.cpp
index 1ecab823ca7..64a3dad9f53 100644
--- a/engines/nancy/state/savedialog.cpp
+++ b/engines/nancy/state/savedialog.cpp
@@ -55,7 +55,7 @@ void SaveDialog::process() {
break;
}
- g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+ g_nancy->_cursor->setCursorType(g_nancy->getGameType() >= kGameTypeNancy10 ? CursorManager::kHotspotArrow : CursorManager::kNormalArrow);
}
void SaveDialog::onStateEnter(const NancyState::NancyState prevState) {
diff --git a/engines/nancy/state/setupmenu.cpp b/engines/nancy/state/setupmenu.cpp
index f65455951a7..21842ed751f 100644
--- a/engines/nancy/state/setupmenu.cpp
+++ b/engines/nancy/state/setupmenu.cpp
@@ -129,7 +129,7 @@ void SetupMenu::init() {
_background.registerGraphics();
- g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+ g_nancy->_cursor->setCursorType(g_nancy->getGameType() >= kGameTypeNancy10 ? CursorManager::kHotspotArrow : CursorManager::kNormalArrow);
g_nancy->setMouseEnabled(true);
g_nancy->_sound->stopSound("MSND");
@@ -234,7 +234,7 @@ void SetupMenu::run() {
}
}
- g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+ g_nancy->_cursor->setCursorType(g_nancy->getGameType() >= kGameTypeNancy10 ? CursorManager::kHotspotArrow : CursorManager::kNormalArrow);
}
void SetupMenu::stop() {
More information about the Scummvm-git-logs
mailing list