[Scummvm-git-logs] scummvm master -> 1da82828ab5982c733df5ed0f27c76284f14d692
bluegr
noreply at scummvm.org
Tue May 19 22:42:33 UTC 2026
This automated email contains information about 4 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
bcace1a372 NANCY: Initial work on the cellphone popup for Nancy10+
72241e074d NANCY: Add a sanity check for checking for held items in Nancy10+
e02ee0f665 NANCY: Fix the no response conversation check for Nancy10+
1da82828ab NANCY: Add workarounds for videos which are longer than their audio
Commit: bcace1a372db0715235476fc0a6e5a86c5419c5a
https://github.com/scummvm/scummvm/commit/bcace1a372db0715235476fc0a6e5a86c5419c5a
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-20T01:41:44+03:00
Commit Message:
NANCY: Initial work on the cellphone popup for Nancy10+
Changed paths:
A engines/nancy/ui/cellphonepopup.cpp
A engines/nancy/ui/cellphonepopup.h
engines/nancy/module.mk
engines/nancy/state/scene.cpp
engines/nancy/state/scene.h
diff --git a/engines/nancy/module.mk b/engines/nancy/module.mk
index b6f5a1fe60d..35d20548ffe 100644
--- a/engines/nancy/module.mk
+++ b/engines/nancy/module.mk
@@ -57,6 +57,7 @@ MODULE_OBJS = \
ui/animatedbutton.o \
ui/button.o \
ui/clock.o \
+ ui/cellphonepopup.o \
ui/inventorybox.o \
ui/inventorypopup.o \
ui/notebookpopup.o \
diff --git a/engines/nancy/state/scene.cpp b/engines/nancy/state/scene.cpp
index ce17c5fa2a7..e000096d8be 100644
--- a/engines/nancy/state/scene.cpp
+++ b/engines/nancy/state/scene.cpp
@@ -615,6 +615,7 @@ void Scene::registerGraphics() {
} else {
_inventoryPopup.registerGraphics();
_notebookPopup.registerGraphics();
+ _cellPhonePopup.registerGraphics();
}
_hotspotDebug.registerGraphics();
@@ -1142,13 +1143,20 @@ void Scene::handleInput() {
}
// We handle the textbox and inventory box first because of their scrollbars, which
- // need to take highest priority
+ // need to take highest priority. On Nancy 10+ the taskbar-driven popups
+ // (inventory/notebook/cellphone) sit visually on top of the textbox
+ // strip, so they get first crack at input â otherwise a click inside
+ // the popup that overlapped the textbox area could accidentally pick
+ // a conversation response.
+ if (g_nancy->getGameType() >= kGameTypeNancy10) {
+ _inventoryPopup.handleInput(input);
+ _notebookPopup.handleInput(input);
+ _cellPhonePopup.handleInput(input);
+ }
+
_textbox.handleInput(input);
if (g_nancy->getGameType() <= kGameTypeNancy9) {
_inventoryBox.handleInput(input);
- } else {
- _inventoryPopup.handleInput(input);
- _notebookPopup.handleInput(input);
}
// Handle invisible map button
@@ -1206,7 +1214,7 @@ void Scene::handleInput() {
_notebookPopup.toggle();
break;
case kTaskButtonCellphone:
- // TODO: open cell-phone popup (UICL)
+ _cellPhonePopup.toggle();
break;
case kTaskButtonHelp:
requestStateChange(NancyState::kHelp);
@@ -1277,6 +1285,7 @@ void Scene::initStaticData() {
} else {
_inventoryPopup.init();
_notebookPopup.init();
+ _cellPhonePopup.init();
}
// Init buttons
diff --git a/engines/nancy/state/scene.h b/engines/nancy/state/scene.h
index d7095756b93..1474f3ca118 100644
--- a/engines/nancy/state/scene.h
+++ b/engines/nancy/state/scene.h
@@ -38,6 +38,7 @@
#include "engines/nancy/ui/inventorybox.h"
#include "engines/nancy/ui/inventorypopup.h"
#include "engines/nancy/ui/notebookpopup.h"
+#include "engines/nancy/ui/cellphonepopup.h"
namespace Common {
class SeekableReadStream;
@@ -175,6 +176,7 @@ public:
UI::InventoryBox &getInventoryBox() { return _inventoryBox; }
UI::InventoryPopup &getInventoryPopup() { return _inventoryPopup; }
UI::NotebookPopup &getNotebookPopup() { return _notebookPopup; }
+ UI::CellPhonePopup &getCellPhonePopup() { return _cellPhonePopup; }
UI::Clock *getClock();
UI::Taskbar *getTaskbar() { return _taskbar; }
@@ -276,6 +278,7 @@ private:
UI::InventoryBox _inventoryBox;
UI::InventoryPopup _inventoryPopup;
UI::NotebookPopup _notebookPopup;
+ UI::CellPhonePopup _cellPhonePopup;
UI::Button *_menuButton;
UI::Button *_helpButton;
diff --git a/engines/nancy/ui/cellphonepopup.cpp b/engines/nancy/ui/cellphonepopup.cpp
new file mode 100644
index 00000000000..726a14815e1
--- /dev/null
+++ b/engines/nancy/ui/cellphonepopup.cpp
@@ -0,0 +1,961 @@
+/* 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 "engines/nancy/cursor.h"
+#include "engines/nancy/font.h"
+#include "engines/nancy/graphics.h"
+#include "engines/nancy/input.h"
+#include "engines/nancy/nancy.h"
+#include "engines/nancy/resource.h"
+#include "engines/nancy/sound.h"
+
+#include "engines/nancy/state/scene.h"
+
+#include "engines/nancy/ui/cellphonepopup.h"
+
+namespace Nancy {
+namespace UI {
+
+// Chunk destRects are raw screen coords; subtract _screenPosition.topLeft
+// to get popup-local. srcRects are atlas-image coords for _spritesImage
+// and pass through unchanged.
+
+CellPhonePopup::CellPhonePopup() :
+ RenderObject(12),
+ _uiclData(nullptr) {}
+
+void CellPhonePopup::init() {
+ _uiclData = GetEngineData(UICL);
+ assert(_uiclData);
+
+ // Chrome image; the sprite atlas is loaded separately below.
+ g_nancy->_resource->loadImage(_uiclData->header.imageName, _overlayImage);
+
+ if (!_uiclData->overlayImageName.empty()) {
+ g_nancy->_resource->loadImage(_uiclData->overlayImageName, _spritesImage);
+ } else if (_uiclData->header.secondaryButtonEnabled &&
+ !_uiclData->header.secondaryButton.primaryImageName.empty()) {
+ g_nancy->_resource->loadImage(_uiclData->header.secondaryButton.primaryImageName,
+ _spritesImage);
+ }
+
+ Common::Rect popupRect = _uiclData->header.normalDestRect;
+ if (_uiclData->header.overlayInGameFrame) {
+ const VIEW *view = GetEngineData(VIEW);
+ if (view) {
+ popupRect.translate(view->screenPosition.left, view->screenPosition.top);
+ }
+ }
+ moveTo(popupRect);
+
+ Common::Rect bounds = _screenPosition;
+ bounds.moveTo(0, 0);
+ _drawSurface.create(bounds.width(), bounds.height(), g_nancy->_graphics->getInputPixelFormat());
+
+ _screenState = kWelcome;
+ _dialedNumber.clear();
+ _resolvedContact = -1;
+ _directoryScroll = 0;
+
+ drawChrome();
+ drawScreenContent();
+
+ setTransparent(false);
+ setVisible(false);
+
+ RenderObject::init();
+}
+
+void CellPhonePopup::registerGraphics() {
+ RenderObject::registerGraphics();
+}
+
+void CellPhonePopup::setNoSignal(bool noSignal) {
+ if (_noSignal == noSignal) {
+ return;
+ }
+ _noSignal = noSignal;
+ if (_isVisible) {
+ drawScreenContent();
+ }
+}
+
+void CellPhonePopup::open() {
+ if (_isVisible) {
+ return;
+ }
+
+ _screenState = kWelcome;
+ _dialedNumber.clear();
+ _resolvedContact = -1;
+ _directoryScroll = 0;
+ _closeButtonHovered = false;
+
+ drawChrome();
+ drawScreenContent();
+ setVisible(true);
+
+ if (!_uiclData->header.sounds[0].name.empty()) {
+ g_nancy->_sound->loadSound(_uiclData->header.sounds[0]);
+ g_nancy->_sound->playSound(_uiclData->header.sounds[0]);
+ }
+}
+
+void CellPhonePopup::close() {
+ if (!_isVisible) {
+ return;
+ }
+
+ if (!_callSound.name.empty()) {
+ g_nancy->_sound->stopSound(_callSound);
+ }
+
+ setVisible(false);
+
+ if (!_uiclData->header.sounds[1].name.empty()) {
+ g_nancy->_sound->loadSound(_uiclData->header.sounds[1]);
+ g_nancy->_sound->playSound(_uiclData->header.sounds[1]);
+ }
+}
+
+void CellPhonePopup::updateGraphics() {
+ if (!_isVisible) {
+ return;
+ }
+
+ // TODO: Process states 0xa..0x10 (web/email/search/help/browser)
+ // are not implemented â the corresponding screen states are
+ // missing from the enum too.
+ switch (_screenState) {
+ case kWelcome:
+ case kDialing:
+ case kDirectory:
+ break;
+
+ case kPlaceCall:
+ if (playSoundIfPresent(_uiclData->outgoingRingSound)) {
+ enterScreenState(kWaitOutgoingRing);
+ } else {
+ enterScreenState(kLookupContact);
+ }
+ break;
+
+ case kWaitOutgoingRing:
+ if (!callSoundIsStillPlaying()) {
+ g_nancy->_sound->stopSound(_callSound);
+ enterScreenState(kLookupContact);
+ }
+ break;
+
+ case kLookupContact: {
+ _resolvedContact = findContactByDialBuffer();
+ if (_resolvedContact == -1) {
+ enterScreenState(kInvalidNumber);
+ } else if (playSoundIfPresent(_uiclData->pickupSound)) {
+ enterScreenState(kWaitPickup);
+ } else {
+ enterScreenState(kConnected);
+ }
+ break;
+ }
+
+ case kWaitPickup:
+ if (!callSoundIsStillPlaying()) {
+ g_nancy->_sound->stopSound(_callSound);
+ enterScreenState(kConnected);
+ }
+ break;
+
+ case kConnected:
+ if (_resolvedContact >= 0 &&
+ _resolvedContact < (int)_uiclData->contacts.size()) {
+ triggerContactCallSceneChange((uint)_resolvedContact);
+ }
+ _resolvedContact = -1;
+ resetDialPad();
+ enterScreenState(kWelcome);
+ break;
+
+ case kInvalidNumber:
+ if (playSoundIfPresent(_uiclData->invalidNumberSound)) {
+ enterScreenState(kWaitInvalid);
+ } else {
+ resetDialPad();
+ enterScreenState(kWelcome);
+ }
+ break;
+
+ case kWaitInvalid:
+ if (!callSoundIsStillPlaying()) {
+ g_nancy->_sound->stopSound(_callSound);
+ resetDialPad();
+ enterScreenState(kWelcome);
+ }
+ break;
+ }
+}
+
+// --------------------------------------------------------------------
+// Drawing
+// --------------------------------------------------------------------
+
+void CellPhonePopup::drawChrome() {
+ // Chrome reads from the primary overlay; all other blits use _spritesImage.
+ _drawSurface.blitFrom(_overlayImage, _uiclData->header.normalSrcRect,
+ Common::Point(0, 0));
+ drawCloseButton(_closeButtonHovered ? 1 : 0);
+ drawWebDirLabels();
+ drawCallButtonState(0);
+ _needsRedraw = true;
+}
+
+void CellPhonePopup::drawScreenContent() {
+ // Repaint chrome (clears the LCD strip), then layer state-specific
+ // content on top.
+ drawChrome();
+
+ if (_screenState != kConnected) {
+ drawStatusIcons();
+ }
+
+ switch (_screenState) {
+ case kWelcome:
+ // Welcome graphic is baked into the chrome; only the status-label
+ // overlay is layered on top when the carrier is unreachable.
+ if (_noSignal) {
+ drawStatusLabels();
+ }
+ break;
+
+ case kDialing:
+ case kPlaceCall:
+ case kWaitOutgoingRing:
+ case kLookupContact:
+ drawDialLabel();
+ drawTypeMessage();
+ drawDialedNumber();
+ break;
+
+ case kWaitPickup:
+ case kConnected:
+ drawConnectedLabel();
+ drawConnectingSprite();
+ break;
+
+ case kInvalidNumber:
+ case kWaitInvalid:
+ drawConnectingSprite();
+ break;
+
+ case kDirectory:
+ drawDirHeading();
+ drawDirectoryList();
+ drawDirectoryArrows();
+ break;
+ }
+
+ _needsRedraw = true;
+}
+
+void CellPhonePopup::drawStatusIcons() {
+ const Common::Point chunkOrigin(_screenPosition.left, _screenPosition.top);
+
+ // Signal indicator uses the alt source when no-signal is set.
+ const Common::Rect &signalSrc = _noSignal && !_uiclData->signalSpriteSrcAlt.isEmpty()
+ ? _uiclData->signalSpriteSrcAlt
+ : _uiclData->signalSpriteSrc;
+ if (!signalSrc.isEmpty() && !_uiclData->signalSpriteDest.isEmpty()) {
+ _drawSurface.blitFrom(_spritesImage, signalSrc,
+ Common::Point(_uiclData->signalSpriteDest.left - chunkOrigin.x,
+ _uiclData->signalSpriteDest.top - chunkOrigin.y));
+ }
+
+ if (!_uiclData->batterySpriteSrc.isEmpty() && !_uiclData->batterySpriteDest.isEmpty()) {
+ _drawSurface.blitFrom(_spritesImage, _uiclData->batterySpriteSrc,
+ Common::Point(_uiclData->batterySpriteDest.left - chunkOrigin.x,
+ _uiclData->batterySpriteDest.top - chunkOrigin.y));
+ }
+}
+
+void CellPhonePopup::drawWebDirLabels() {
+ const Common::Point chunkOrigin(_screenPosition.left, _screenPosition.top);
+
+ if (!_uiclData->webLabel.srcRect.isEmpty() && !_uiclData->webLabel.destRect.isEmpty()) {
+ _drawSurface.blitFrom(_spritesImage, _uiclData->webLabel.srcRect,
+ Common::Point(_uiclData->webLabel.destRect.left - chunkOrigin.x,
+ _uiclData->webLabel.destRect.top - chunkOrigin.y));
+ }
+
+ if (!_uiclData->dirLabel.srcRect.isEmpty() && !_uiclData->dirLabel.destRect.isEmpty()) {
+ _drawSurface.blitFrom(_spritesImage, _uiclData->dirLabel.srcRect,
+ Common::Point(_uiclData->dirLabel.destRect.left - chunkOrigin.x,
+ _uiclData->dirLabel.destRect.top - chunkOrigin.y));
+ }
+}
+
+void CellPhonePopup::drawDialLabel() {
+ const UICL::SrcDestRectPair &dl = _uiclData->dialLabel;
+ if (dl.srcRect.isEmpty() || dl.destRect.isEmpty()) {
+ return;
+ }
+
+ const Common::Point chunkOrigin(_screenPosition.left, _screenPosition.top);
+ _drawSurface.blitFrom(_spritesImage, dl.srcRect,
+ Common::Point(dl.destRect.left - chunkOrigin.x,
+ dl.destRect.top - chunkOrigin.y));
+}
+
+void CellPhonePopup::drawTypeMessage() {
+ const UICL::SrcDestRectPair &tm = _uiclData->typeMessage;
+ if (tm.srcRect.isEmpty() || tm.destRect.isEmpty()) {
+ return;
+ }
+
+ const Common::Point chunkOrigin(_screenPosition.left, _screenPosition.top);
+ _drawSurface.blitFrom(_spritesImage, tm.srcRect,
+ Common::Point(tm.destRect.left - chunkOrigin.x,
+ tm.destRect.top - chunkOrigin.y));
+}
+
+void CellPhonePopup::drawConnectedLabel() {
+ const UICL::SrcDestRectPair &cl = _uiclData->connectedLabel;
+ if (cl.srcRect.isEmpty() || cl.destRect.isEmpty()) {
+ return;
+ }
+
+ const Common::Point chunkOrigin(_screenPosition.left, _screenPosition.top);
+ _drawSurface.blitFrom(_spritesImage, cl.srcRect,
+ Common::Point(cl.destRect.left - chunkOrigin.x,
+ cl.destRect.top - chunkOrigin.y));
+}
+
+void CellPhonePopup::drawConnectingSprite() {
+ if (_uiclData->connectingSpriteSrc.isEmpty() || _uiclData->connectingSpriteDest.isEmpty()) {
+ return;
+ }
+
+ const Common::Point chunkOrigin(_screenPosition.left, _screenPosition.top);
+ _drawSurface.blitFrom(_spritesImage, _uiclData->connectingSpriteSrc,
+ Common::Point(_uiclData->connectingSpriteDest.left - chunkOrigin.x,
+ _uiclData->connectingSpriteDest.top - chunkOrigin.y));
+}
+
+void CellPhonePopup::drawDialedNumber() {
+ if (_dialedNumber.empty()) {
+ return;
+ }
+
+ const Font *font = g_nancy->_graphics->getFont(_uiclData->fontId1);
+ if (!font) {
+ return;
+ }
+
+ const int x = _uiclData->statusTextX - _screenPosition.left;
+ const int y = _uiclData->statusTextY - _screenPosition.top;
+
+ font->drawString(&_drawSurface, _dialedNumber, x, y,
+ _screenPosition.width() - x, 0);
+}
+
+void CellPhonePopup::drawCallButtonState(uint state) {
+ const UICL::ThreeRectWidget &cb = _uiclData->callButton;
+ if (cb.destRect.isEmpty()) {
+ return;
+ }
+
+ const Common::Rect &src = (state == 1 && !cb.srcRectPressed.isEmpty())
+ ? cb.srcRectPressed
+ : cb.srcRectIdle;
+ if (src.isEmpty()) {
+ return;
+ }
+
+ const Common::Point chunkOrigin(_screenPosition.left, _screenPosition.top);
+ _drawSurface.blitFrom(_spritesImage, src,
+ Common::Point(cb.destRect.left - chunkOrigin.x,
+ cb.destRect.top - chunkOrigin.y));
+}
+
+void CellPhonePopup::drawCloseButton(uint state) {
+ const UIButtonRecord &btn = _uiclData->header.secondaryButton;
+ if (!_uiclData->header.secondaryButtonEnabled || btn.destRect.isEmpty()) {
+ return;
+ }
+
+ const uint stateIdx = MIN<uint>(state, 3);
+ Common::Rect src = btn.sourceRects[stateIdx];
+ if (src.isEmpty()) {
+ src = btn.sourceRects[0];
+ }
+ if (src.isEmpty()) {
+ return;
+ }
+
+ Common::Rect dstRect = btn.destRect;
+ if (btn.destUsesGameFrameOffset) {
+ const VIEW *view = GetEngineData(VIEW);
+ if (view) {
+ dstRect.translate(view->screenPosition.left, view->screenPosition.top);
+ }
+ }
+ const Common::Point dst(dstRect.left - _screenPosition.left,
+ dstRect.top - _screenPosition.top);
+ _drawSurface.blitFrom(_spritesImage, src, dst);
+}
+
+void CellPhonePopup::drawStatusLabels() {
+ // Three stacked lines (No Signal / No Access / Old Email Only)
+ // at statusTextX with Y offsets -10 / +20 / +50.
+ const Font *font = g_nancy->_graphics->getFont(_uiclData->fontId1);
+ if (!font) {
+ return;
+ }
+
+ const int x = _uiclData->statusTextX - _screenPosition.left;
+ const int yBase = _uiclData->statusTextY - _screenPosition.top;
+ const int kLineYOffsets[UICL::kNumStatusLabels] = { -10, 20, 50 };
+
+ for (uint i = 0; i < UICL::kNumStatusLabels; ++i) {
+ const Common::String &label = _uiclData->statusLabels[i];
+ if (label.empty()) {
+ continue;
+ }
+ const int y = yBase + kLineYOffsets[i];
+ font->drawString(&_drawSurface, label, x, y,
+ _screenPosition.width() - x, 0);
+ }
+}
+
+void CellPhonePopup::drawDirHeading() {
+ const UICL::SrcDestRectPair &dh = _uiclData->dirHeading;
+ if (dh.srcRect.isEmpty() || dh.destRect.isEmpty()) {
+ return;
+ }
+
+ const Common::Point chunkOrigin(_screenPosition.left, _screenPosition.top);
+ _drawSurface.blitFrom(_spritesImage, dh.srcRect,
+ Common::Point(dh.destRect.left - chunkOrigin.x,
+ dh.destRect.top - chunkOrigin.y));
+}
+
+void CellPhonePopup::drawDirectoryList() {
+ // The chunk stores one entry per dial-pattern variant, so several
+ // entries can share a name. Collapse by name to avoid showing the
+ // same contact multiple times.
+ // TODO: also sort by name and respect per-entry visibility flags,
+ // the way the original engine's contact-filter pass does.
+ const Font *font = g_nancy->_graphics->getFont(_uiclData->fontId2);
+ if (!font) {
+ return;
+ }
+
+ const uint maxRows = maxDirectoryRows();
+ Common::Array<Common::String> seenNames;
+ uint visibleRow = 0;
+ uint visited = 0;
+
+ for (uint contactIdx = 0;
+ contactIdx < _uiclData->contacts.size() && visibleRow < maxRows;
+ ++contactIdx) {
+ const UICL::Contact &c = _uiclData->contacts[contactIdx];
+ if (c.name.empty()) {
+ continue;
+ }
+
+ bool duplicate = false;
+ for (uint s = 0; s < seenNames.size(); ++s) {
+ if (seenNames[s].equalsIgnoreCase(c.name)) {
+ duplicate = true;
+ break;
+ }
+ }
+ if (duplicate) {
+ continue;
+ }
+ seenNames.push_back(c.name);
+
+ // _directoryScroll counts deduplicated entries.
+ if (visited < _directoryScroll) {
+ ++visited;
+ continue;
+ }
+ ++visited;
+
+ const Common::Rect rowRect = directoryRowRect(visibleRow);
+ font->drawString(&_drawSurface, c.name,
+ rowRect.left, rowRect.top,
+ rowRect.width(), 0);
+ ++visibleRow;
+ }
+}
+
+void CellPhonePopup::drawDirectoryArrows() {
+ // Sub-buttons 1 and 2 carry the up/down scroll arrows for list modes.
+ const UICL::ThreeRectWidget &up = _uiclData->subButtons[1];
+ const UICL::ThreeRectWidget &down = _uiclData->subButtons[2];
+
+ const Common::Point chunkOrigin(_screenPosition.left, _screenPosition.top);
+
+ if (!up.srcRectIdle.isEmpty() && !up.destRect.isEmpty()) {
+ _drawSurface.blitFrom(_spritesImage, up.srcRectIdle,
+ Common::Point(up.destRect.left - chunkOrigin.x,
+ up.destRect.top - chunkOrigin.y));
+ }
+ if (!down.srcRectIdle.isEmpty() && !down.destRect.isEmpty()) {
+ _drawSurface.blitFrom(_spritesImage, down.srcRectIdle,
+ Common::Point(down.destRect.left - chunkOrigin.x,
+ down.destRect.top - chunkOrigin.y));
+ }
+}
+
+// --------------------------------------------------------------------
+// State-machine helpers
+// --------------------------------------------------------------------
+
+void CellPhonePopup::resetDialPad() {
+ _dialedNumber.clear();
+}
+
+void CellPhonePopup::enterScreenState(ScreenState newState) {
+ // Always redraw, even when newState == _screenState, so successive
+ // digit entries in kDialing each refresh the readout.
+ _screenState = newState;
+ drawScreenContent();
+}
+
+void CellPhonePopup::appendDigit(byte slotIndex) {
+ // 11-char cap matches the LCD readout buffer width.
+ if (_dialedNumber.size() >= 11) {
+ return;
+ }
+ _dialedNumber += (char)('0' + slotIndex);
+ enterScreenState(kDialing);
+}
+
+bool CellPhonePopup::playSoundIfPresent(const Common::Path &soundName) {
+ if (soundName.empty()) {
+ return false;
+ }
+ Common::String nameStr = soundName.toString();
+ if (nameStr.equalsIgnoreCase("NO SOUND")) {
+ return false;
+ }
+
+ _callSound.name = nameStr;
+ if (_callSound.channelID == 0) {
+ // TODO: read the channel from the chunk's per-call channel slot
+ // instead of hardcoding it.
+ _callSound.channelID = 28;
+ }
+ _callSound.volume = 100;
+ _callSound.numLoops = 1;
+ _callSound.playCommands = 1;
+
+ g_nancy->_sound->loadSound(_callSound);
+ g_nancy->_sound->playSound(_callSound);
+ return true;
+}
+
+bool CellPhonePopup::callSoundIsStillPlaying() const {
+ if (_callSound.name.empty()) {
+ return false;
+ }
+ return g_nancy->_sound->isSoundPlaying(_callSound);
+}
+
+int CellPhonePopup::findContactByDialBuffer() const {
+ if (_dialedNumber.empty()) {
+ return -1;
+ }
+
+ // Contact prefix bytes are slot indices (0..9); _dialedNumber holds
+ // '0'..'9' chars, so convert each char back to its slot index.
+ const uint dialLen = _dialedNumber.size();
+ for (uint i = 0; i < _uiclData->contacts.size(); ++i) {
+ const UICL::Contact &c = _uiclData->contacts[i];
+ bool match = true;
+ for (uint b = 0; b < dialLen; ++b) {
+ const byte slotIdx = (byte)(_dialedNumber[b] - '0');
+ if (slotIdx != c.unknownPrefix[b]) {
+ match = false;
+ break;
+ }
+ }
+ // Require the prefix to terminate at the dialed length so a
+ // partial dial doesn't match a longer number.
+ if (match && dialLen < sizeof(c.unknownPrefix) &&
+ c.unknownPrefix[dialLen] == '\n') {
+ return (int)i;
+ }
+ }
+ return -1;
+}
+
+void CellPhonePopup::triggerContactCallSceneChange(uint contactIndex) {
+ if (contactIndex >= _uiclData->contacts.size()) {
+ return;
+ }
+
+ // Suffix layout: [0..1]=sceneID, [4..5]=eventFlag label, [6]=eventFlag value.
+ const UICL::Contact &c = _uiclData->contacts[contactIndex];
+
+ const uint16 sceneID = (uint16)c.unknownSuffix[0] | ((uint16)c.unknownSuffix[1] << 8);
+ if (sceneID == 9999) {
+ return; // "no scene" sentinel
+ }
+ const int16 eventFlagLabel = (int16)((uint16)c.unknownSuffix[4] |
+ ((uint16)c.unknownSuffix[5] << 8));
+ const byte eventFlagValue = c.unknownSuffix[6];
+
+ SceneChangeDescription scene;
+ scene.sceneID = sceneID;
+ scene.frameID = 0;
+ scene.verticalOffset = 0;
+ scene.continueSceneSound = kContinueSceneSound;
+
+ if (eventFlagLabel != -1) {
+ NancySceneState.setEventFlag(eventFlagLabel,
+ eventFlagValue ? g_nancy->_true : g_nancy->_false);
+ }
+
+ NancySceneState.changeScene(scene);
+
+ setVisible(false);
+}
+
+// --------------------------------------------------------------------
+// Directory helpers
+// --------------------------------------------------------------------
+
+uint CellPhonePopup::maxDirectoryRows() const {
+ // fontId2 is the list font; fontId1 is the (larger) dial readout.
+ const Font *font = g_nancy->_graphics->getFont(_uiclData->fontId2);
+ if (!font) {
+ return 0;
+ }
+
+ const Common::Rect &arrow = _uiclData->dirArrowSrc;
+ if (arrow.isEmpty()) {
+ return 0;
+ }
+
+ // TODO: derive the real visible-row count from the layout
+ // FUN_004d8476 sets up at directory entry (case 0xe). For now,
+ // cap at the LCD height divided by row pitch.
+ const int rowH = arrow.height() + 8;
+ if (rowH <= 0) {
+ return 0;
+ }
+ // Approximate LCD strip height as welcomeScreen height.
+ const int lcdH = _uiclData->welcomeScreen.destRect.height();
+ return MAX<int>(1, lcdH / rowH);
+}
+
+Common::Rect CellPhonePopup::directoryRowRect(uint visibleIndex) const {
+ const Font *font = g_nancy->_graphics->getFont(_uiclData->fontId2);
+ if (!font) {
+ return Common::Rect();
+ }
+
+ // Row layout: X start = dirArrowSrc.right + 5,
+ // Y start = dirArrowSrc.top - 5,
+ // pitch = dirArrowSrc.height() + 8.
+ const Common::Rect &arrow = _uiclData->dirArrowSrc;
+ const int rowH = arrow.height() + 8;
+ const int xScreen = arrow.right + 5;
+ const int yScreen = arrow.top - 5 + (int)visibleIndex * rowH;
+ const int x = xScreen - _screenPosition.left;
+ const int y = yScreen - _screenPosition.top;
+ const int width = _screenPosition.width() - x - 4;
+ return Common::Rect(x, y, x + width, y + rowH);
+}
+
+int CellPhonePopup::contactIndexForVisibleRow(uint visibleRow) const {
+ // Mirrors the dedup walk in drawDirectoryList.
+ Common::Array<Common::String> seenNames;
+ uint visited = 0;
+ uint visibleSoFar = 0;
+ for (uint i = 0; i < _uiclData->contacts.size(); ++i) {
+ const UICL::Contact &c = _uiclData->contacts[i];
+ if (c.name.empty()) {
+ continue;
+ }
+ bool duplicate = false;
+ for (uint s = 0; s < seenNames.size(); ++s) {
+ if (seenNames[s].equalsIgnoreCase(c.name)) {
+ duplicate = true;
+ break;
+ }
+ }
+ if (duplicate) {
+ continue;
+ }
+ seenNames.push_back(c.name);
+ if (visited < _directoryScroll) {
+ ++visited;
+ continue;
+ }
+ if (visibleSoFar == visibleRow) {
+ return (int)i;
+ }
+ ++visibleSoFar;
+ ++visited;
+ }
+ return -1;
+}
+
+uint CellPhonePopup::directoryRowAt(const Common::Point &chunkMouse) const {
+ // directoryRowRect returns popup-local rects; convert the mouse too.
+ const Common::Point popupMouse(chunkMouse.x - _screenPosition.left,
+ chunkMouse.y - _screenPosition.top);
+ const uint maxRows = maxDirectoryRows();
+ for (uint i = 0; i < maxRows; ++i) {
+ if (directoryRowRect(i).contains(popupMouse)) {
+ return i;
+ }
+ }
+ return (uint)-1;
+}
+
+void CellPhonePopup::startCallToContact(uint contactIndex) {
+ if (contactIndex >= _uiclData->contacts.size()) {
+ return;
+ }
+ const UICL::Contact &c = _uiclData->contacts[contactIndex];
+
+ // Rebuild _dialedNumber from the contact prefix (slot indices,
+ // terminated by '\n') so the call flow's lookup matches.
+ _dialedNumber.clear();
+ for (uint b = 0; b < sizeof(c.unknownPrefix); ++b) {
+ const byte v = c.unknownPrefix[b];
+ if (v == '\n') {
+ break;
+ }
+ if (v > 9) {
+ // Non-digit slot index: abort so the call hits kInvalidNumber.
+ _dialedNumber.clear();
+ break;
+ }
+ _dialedNumber += (char)('0' + v);
+ }
+ if (_dialedNumber.empty()) {
+ return;
+ }
+ enterScreenState(kPlaceCall);
+}
+
+uint CellPhonePopup::deduplicatedContactCount() const {
+ Common::Array<Common::String> seen;
+ for (uint i = 0; i < _uiclData->contacts.size(); ++i) {
+ const UICL::Contact &c = _uiclData->contacts[i];
+ if (c.name.empty()) {
+ continue;
+ }
+ bool dup = false;
+ for (uint s = 0; s < seen.size(); ++s) {
+ if (seen[s].equalsIgnoreCase(c.name)) {
+ dup = true;
+ break;
+ }
+ }
+ if (!dup) {
+ seen.push_back(c.name);
+ }
+ }
+ return seen.size();
+}
+
+// --------------------------------------------------------------------
+// Input
+// --------------------------------------------------------------------
+
+Common::Point CellPhonePopup::mouseToChunkCoords(const Common::Point &mouse) const {
+ // Chunk destRects are screen coords, so chunk-mouse == screen-mouse.
+ return mouse;
+}
+
+void CellPhonePopup::handleInput(NancyInput &input) {
+ if (!_isVisible) {
+ return;
+ }
+
+ // Transient call-flow states ignore everything except the close X.
+ const bool transientCallState =
+ _screenState == kPlaceCall || _screenState == kWaitOutgoingRing ||
+ _screenState == kLookupContact || _screenState == kWaitPickup ||
+ _screenState == kConnected || _screenState == kInvalidNumber ||
+ _screenState == kWaitInvalid;
+
+ // Close (X) â checked first so it wins overlap.
+ if (_uiclData->header.secondaryButtonEnabled) {
+ const UIButtonRecord &closeBtn = _uiclData->header.secondaryButton;
+ Common::Rect closeScreen = closeBtn.destRect;
+ if (closeBtn.destUsesGameFrameOffset) {
+ const VIEW *view = GetEngineData(VIEW);
+ if (view) {
+ closeScreen.translate(view->screenPosition.left, view->screenPosition.top);
+ }
+ }
+ const bool overClose = closeScreen.contains(input.mousePos);
+ if (overClose != _closeButtonHovered) {
+ _closeButtonHovered = overClose;
+ drawCloseButton(overClose ? 1 : 0);
+ _needsRedraw = true;
+ }
+ if (overClose) {
+ g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ input.eatMouseInput();
+ close();
+ return;
+ }
+ }
+ }
+
+ if (transientCallState) {
+ if (_screenPosition.contains(input.mousePos)) {
+ input.eatMouseInput();
+ }
+ return;
+ }
+
+ const Common::Point chunkMouse = mouseToChunkCoords(input.mousePos);
+
+ // Directory mode: scroll arrows + click-to-call on a contact row.
+ if (_screenState == kDirectory) {
+ const Common::Rect &upDst = _uiclData->subButtons[1].destRect;
+ const Common::Rect &downDst = _uiclData->subButtons[2].destRect;
+
+ if (upDst.contains(chunkMouse)) {
+ g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ if (_directoryScroll > 0) {
+ --_directoryScroll;
+ drawScreenContent();
+ }
+ input.eatMouseInput();
+ return;
+ }
+ } else if (downDst.contains(chunkMouse)) {
+ g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ const uint total = deduplicatedContactCount();
+ const uint rows = maxDirectoryRows();
+ const uint maxScroll = total > rows ? total - rows : 0;
+ if (_directoryScroll < maxScroll) {
+ ++_directoryScroll;
+ drawScreenContent();
+ }
+ input.eatMouseInput();
+ return;
+ }
+ }
+
+ const uint row = directoryRowAt(chunkMouse);
+ if (row != (uint)-1) {
+ const int contactIdx = contactIndexForVisibleRow(row);
+ if (contactIdx >= 0) {
+ g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ startCallToContact((uint)contactIdx);
+ input.eatMouseInput();
+ return;
+ }
+ }
+ }
+ // Fall through so slot 14 can toggle the mode off and slots 0..9
+ // can override the directory by starting a fresh dial.
+ }
+
+ // Dial-pad slot behaviour:
+ // 0..9 - digit input
+ // 10, 11 - *, # (no-op for now)
+ // 12 - welcome / dial toggle (TODO: hook up)
+ // 13 - web mode (TODO: not implemented)
+ // 14 - directory toggle
+ int newHovered = -1;
+ for (uint i = 0; i < UICL::kNumDialPadSlots; ++i) {
+ const UICL::DialPadSlot &slot = _uiclData->dialPadSlots[i];
+ if (slot.destRect.contains(chunkMouse)) {
+ newHovered = (int)i;
+ break;
+ }
+ }
+ _hoveredSlot = newHovered;
+
+ if (newHovered != -1) {
+ g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ const UICL::DialPadSlot &slot = _uiclData->dialPadSlots[newHovered];
+
+ if (!slot.soundName.empty()) {
+ g_nancy->_sound->playSound(slot.soundName);
+ }
+
+ if (newHovered < 10) {
+ if (_screenState == kDirectory) {
+ _dialedNumber.clear();
+ }
+ appendDigit((byte)newHovered);
+ } else if (newHovered == 14) {
+ if (_screenState == kDirectory) {
+ _directoryScroll = 0;
+ enterScreenState(kWelcome);
+ } else {
+ _dialedNumber.clear();
+ _directoryScroll = 0;
+ enterScreenState(kDirectory);
+ }
+ }
+ // TODO: slots 10/11 (*, #), slot 12 (mode toggle), slot 13
+ // (web mode) are unimplemented; they fall through as no-ops.
+
+ input.eatMouseInput();
+ return;
+ }
+ }
+
+ // Call button â disabled while in no-signal mode.
+ if (_uiclData->callButton.destRect.contains(chunkMouse)) {
+ g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ if (!_dialedNumber.empty() && !_noSignal) {
+ enterScreenState(kPlaceCall);
+ }
+ input.eatMouseInput();
+ return;
+ }
+ }
+
+ // TODO: sub-buttons (10 entries in _uiclData->subButtons) are not
+ // hooked up â they drive email/search/help/browser modes and the
+ // in-call menu in the original.
+
+ // Swallow any remaining click so it doesn't fall through to the scene.
+ if (_screenPosition.contains(input.mousePos)) {
+ input.eatMouseInput();
+ }
+}
+
+} // End of namespace UI
+} // End of namespace Nancy
diff --git a/engines/nancy/ui/cellphonepopup.h b/engines/nancy/ui/cellphonepopup.h
new file mode 100644
index 00000000000..2d3ca4ffe1c
--- /dev/null
+++ b/engines/nancy/ui/cellphonepopup.h
@@ -0,0 +1,157 @@
+/* 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_UI_CELLPHONEPOPUP_H
+#define NANCY_UI_CELLPHONEPOPUP_H
+
+#include "engines/nancy/commontypes.h"
+#include "engines/nancy/renderobject.h"
+#include "engines/nancy/sound.h"
+
+namespace Nancy {
+
+struct NancyInput;
+struct UICL;
+
+namespace UI {
+
+// Nancy 10+ cell phone popup driven by the UICL chunk.
+//
+// Implemented modes: welcome screen, call-placement (dial â ring â
+// lookup â pickup â connect â invalid), directory list, no-signal
+// overlay.
+//
+// TODO: email, contact search, help, and browser modes are not
+// implemented; web/email/redial sub-buttons and the in-call menu
+// are unhooked.
+class CellPhonePopup : public RenderObject {
+public:
+ CellPhonePopup();
+ ~CellPhonePopup() override = default;
+
+ void init() override;
+ void registerGraphics() override;
+ void updateGraphics() override;
+ void handleInput(NancyInput &input);
+
+ void open();
+ void close();
+ void toggle() { if (_isVisible) close(); else open(); }
+
+ // Replaces the welcome graphic with the No Signal / No Access /
+ // Old Email Only labels and disables outgoing calls.
+ // TODO: hook this up to the scene event flag that drives it in
+ // the original engine.
+ void setNoSignal(bool noSignal);
+
+private:
+ enum ScreenState : int {
+ kWelcome = 0, // idle / dial-pad accepting input
+ kDialing = 1, // digit accumulating
+ kPlaceCall = 2, // start outgoing-ring sound
+ kWaitOutgoingRing = 3,
+ kLookupContact = 4, // match dial buffer, start pickup
+ kWaitPickup = 5,
+ kConnected = 6, // trigger contact scene change
+ kInvalidNumber = 7, // start invalid-number sound
+ kWaitInvalid = 8,
+ kDirectory = 9 // contact list, scrollable
+ };
+
+ // Drawing helpers
+ void drawChrome(); // popup overlay + persistent labels + buttons
+ void drawScreenContent(); // LCD area: dispatches on _screenState
+ void drawStatusIcons();
+ void drawWebDirLabels();
+ void drawDialLabel();
+ void drawTypeMessage();
+ void drawConnectedLabel();
+ void drawConnectingSprite();
+ void drawDialedNumber();
+ void drawCallButtonState(uint state);
+ void drawCloseButton(uint state);
+ void drawStatusLabels();
+ void drawDirectoryList();
+ void drawDirHeading();
+ void drawDirectoryArrows();
+
+ // State machine helpers
+ void resetDialPad();
+ void enterScreenState(ScreenState newState);
+ void appendDigit(byte slotIndex);
+ bool playSoundIfPresent(const Common::Path &soundName);
+ bool callSoundIsStillPlaying() const;
+ void triggerContactCallSceneChange(uint contactIndex);
+ int findContactByDialBuffer() const;
+
+ // Directory helpers
+ uint maxDirectoryRows() const;
+ uint directoryRowAt(const Common::Point &chunkMouse) const;
+ Common::Rect directoryRowRect(uint visibleIndex) const;
+ void startCallToContact(uint contactIndex);
+ // Visible (deduplicated) row -> raw contact index, or -1.
+ int contactIndexForVisibleRow(uint visibleRow) const;
+ // Total deduplicated contacts in the directory list.
+ uint deduplicatedContactCount() const;
+
+ // Mouse hit-test helpers
+ Common::Point mouseToChunkCoords(const Common::Point &mouse) const;
+
+ const UICL *_uiclData;
+
+ // Chrome image (header.imageName).
+ Graphics::ManagedSurface _overlayImage;
+ // Sprite atlas (overlayImageName). Source for every non-chrome
+ // blit: status icons, labels, headings, call/close buttons.
+ Graphics::ManagedSurface _spritesImage;
+
+ bool _closeButtonHovered = false;
+
+ ScreenState _screenState = kWelcome;
+
+ // '0'..'9' chars of the digits the player has dialed. Convert with
+ // `c - '0'` to recover the slot index that the contact prefix stores.
+ Common::String _dialedNumber;
+
+ // Active dial-tone / ring / pickup / invalid-number cue. The
+ // original engine reuses one channel for all of them.
+ SoundDescription _callSound;
+
+ // Contact index resolved during kLookupContact, valid through
+ // kWaitPickup; -1 means the number didn't match.
+ int _resolvedContact = -1;
+
+ // Dial-pad slot under the cursor, or -1.
+ int _hoveredSlot = -1;
+
+ // Index of the first deduplicated contact rendered in directory mode.
+ // Updated by the up/down arrow sub-buttons.
+ uint _directoryScroll = 0;
+
+ // When true, the LCD shows the status labels and outgoing calls
+ // are blocked. See setNoSignal().
+ bool _noSignal = false;
+};
+
+} // End of namespace UI
+} // End of namespace Nancy
+
+#endif // NANCY_UI_CELLPHONEPOPUP_H
Commit: 72241e074d27213907708b49a6221ee74f7f7ec7
https://github.com/scummvm/scummvm/commit/72241e074d27213907708b49a6221ee74f7f7ec7
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-20T01:41:45+03:00
Commit Message:
NANCY: Add a sanity check for checking for held items in Nancy10+
There's a special case used in Nancy10+, where held items with a
large item ID are checked, when adding tasks to the notebook. Add
a sanity check for now, to avoid crashing
Changed paths:
engines/nancy/state/scene.cpp
diff --git a/engines/nancy/state/scene.cpp b/engines/nancy/state/scene.cpp
index e000096d8be..042c8a764b4 100644
--- a/engines/nancy/state/scene.cpp
+++ b/engines/nancy/state/scene.cpp
@@ -392,8 +392,17 @@ void Scene::setNoHeldItem() {
byte Scene::hasItem(int16 id) const {
if (getHeldItem() == id) {
return g_nancy->_true;
- } else {
+ } else if (id > 0 && (uint)id < _flags.items.size()) {
return _flags.items[id];
+ } else {
+ // TODO: Happens in Nancy10+. Gets called for item IDs
+ // 1824, 1825, 1826, 1827, when adding tasks to the
+ // notebook, for an array of 70 items in total. Looks
+ // like a case where a flag is contained for the held
+ // item ID to be checked.
+ debug(2, "Scene::hasItem: out-of-range id %d (items.size=%u)", id,
+ (uint)_flags.items.size());
+ return g_nancy->_false;
}
}
Commit: e02ee0f6659b3071bedbf6e7dd1179999bb11eb1
https://github.com/scummvm/scummvm/commit/e02ee0f6659b3071bedbf6e7dd1179999bb11eb1
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-20T01:41:46+03:00
Commit Message:
NANCY: Fix the no response conversation check for Nancy10+
Changed paths:
engines/nancy/action/conversation.cpp
diff --git a/engines/nancy/action/conversation.cpp b/engines/nancy/action/conversation.cpp
index 8bfa5b9a9f6..040d3d26efc 100644
--- a/engines/nancy/action/conversation.cpp
+++ b/engines/nancy/action/conversation.cpp
@@ -40,7 +40,8 @@ namespace Action {
ConversationSound::ConversationSound() :
RenderActionRecord(8),
- _noResponse(g_nancy->getGameType() <= kGameTypeNancy2 ? 10 : 20),
+ _noResponse(g_nancy->getGameType() <= kGameTypeNancy2 ||
+ g_nancy->getGameType() >= kGameTypeNancy10 ? 10 : 20),
_hasDrawnTextbox(false),
_pickedResponse(-1) {
_conditionalResponseCharacterID = _noResponse;
@@ -133,11 +134,6 @@ void ConversationSound::readTerseData(Common::SeekableReadStream &stream) {
_conditionalResponseCharacterID = stream.readByte();
_goodbyeResponseCharacterID = stream.readByte();
- if (g_nancy->getGameType() >= kGameTypeNancy10) {
- _conditionalResponseCharacterID -= 7;
- _goodbyeResponseCharacterID -= 7;
- }
-
_defaultNextScene = stream.readByte();
_sceneChange.sceneID = stream.readUint16LE();
Commit: 1da82828ab5982c733df5ed0f27c76284f14d692
https://github.com/scummvm/scummvm/commit/1da82828ab5982c733df5ed0f27c76284f14d692
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-20T01:41:48+03:00
Commit Message:
NANCY: Add workarounds for videos which are longer than their audio
Fix #16753, #16786
Changed paths:
engines/nancy/action/conversation.cpp
diff --git a/engines/nancy/action/conversation.cpp b/engines/nancy/action/conversation.cpp
index 040d3d26efc..d9f5cf1b2ae 100644
--- a/engines/nancy/action/conversation.cpp
+++ b/engines/nancy/action/conversation.cpp
@@ -894,6 +894,17 @@ void ConversationCelTerse::readData(Common::SeekableReadStream &stream) {
_overrideTreeRects.resize(4, kCelOverrideTreeRectsOff);
readTerseData(stream);
+
+ // WORKAROUND: Fix the last frame for some videos, to prevent them from
+ // running for too long, if the associated sound file is shorter than
+ // the video
+ if (g_nancy->getGameType() == kGameTypeNancy9 && xsheetName == "KFB28" && _lastFrame == 102 && _sound.name == "KFF28") {
+ // Offerring to call the Sheriff for Katie - bug #16753
+ _lastFrame = 70;
+ } else if (g_nancy->getGameType() == kGameTypeNancy9 && xsheetName == "StubAndy" && _lastFrame == 344 && _sound.name == "ACC03") {
+ // Asking Andy for a whale watching keychain design - bug #16786
+ _lastFrame = 30;
+ }
}
} // End of namespace Action
More information about the Scummvm-git-logs
mailing list