[Scummvm-git-logs] scummvm master -> 6fd36716c955a45108f4edaa709f4edf92497373
bluegr
noreply at scummvm.org
Sat May 23 01:39:43 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:
49c2dfbc51 NANCY: More work on the cell phone interface for Nancy10+
36257c744e NANCY: Implement CellPhonePopCellSceneFromStack for Nancy10+
d0aea692cc NANCY: Add information about cell phone contacts fields
4c107f1d83 NANCY: Don't try to load viewport videos in scenes that don't have them
02cd306964 NANCY: Return an empty string for captions that aren't found
834fe18348 NANCY: Refresh the cursor when a piece is selected in multibuildpuzzle
6fd36716c9 NANCY: More work on inventory item handling for Nancy10+
Commit: 49c2dfbc515d0f932954c764fe397f98833ddc45
https://github.com/scummvm/scummvm/commit/49c2dfbc515d0f932954c764fe397f98833ddc45
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-23T04:39:25+03:00
Commit Message:
NANCY: More work on the cell phone interface for Nancy10+
It's now possible to place phone calls
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 726a14815e1..e8798a4f7a5 100644
--- a/engines/nancy/ui/cellphonepopup.cpp
+++ b/engines/nancy/ui/cellphonepopup.cpp
@@ -74,6 +74,7 @@ void CellPhonePopup::init() {
_dialedNumber.clear();
_resolvedContact = -1;
_directoryScroll = 0;
+ _directorySelection = 0;
drawChrome();
drawScreenContent();
@@ -107,6 +108,7 @@ void CellPhonePopup::open() {
_dialedNumber.clear();
_resolvedContact = -1;
_directoryScroll = 0;
+ _directorySelection = 0;
_closeButtonHovered = false;
drawChrome();
@@ -141,9 +143,7 @@ void CellPhonePopup::updateGraphics() {
return;
}
- // TODO: Process states 0xa..0x10 (web/email/search/help/browser)
- // are not implemented â the corresponding screen states are
- // missing from the enum too.
+ // TODO: web/email/search/help/browser modes not implemented.
switch (_screenState) {
case kWelcome:
case kDialing:
@@ -166,7 +166,11 @@ void CellPhonePopup::updateGraphics() {
break;
case kLookupContact: {
- _resolvedContact = findContactByDialBuffer();
+ // Directory-mode calls pre-resolve the contact, so only fall back
+ // to dial-buffer lookup when the contact isn't already known.
+ if (_resolvedContact == -1) {
+ _resolvedContact = findContactByDialBuffer();
+ }
if (_resolvedContact == -1) {
enterScreenState(kInvalidNumber);
} else if (playSoundIfPresent(_uiclData->pickupSound)) {
@@ -185,13 +189,15 @@ void CellPhonePopup::updateGraphics() {
break;
case kConnected:
+ // Trigger the scene change once, then sit in kConnected so the
+ // connecting sprite stays on screen for the duration of the
+ // conversation. AR 128 closes the popup when the call ends.
if (_resolvedContact >= 0 &&
_resolvedContact < (int)_uiclData->contacts.size()) {
triggerContactCallSceneChange((uint)_resolvedContact);
+ _resolvedContact = -1;
+ resetDialPad();
}
- _resolvedContact = -1;
- resetDialPad();
- enterScreenState(kWelcome);
break;
case kInvalidNumber:
@@ -218,18 +224,14 @@ void CellPhonePopup::updateGraphics() {
// --------------------------------------------------------------------
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) {
@@ -238,10 +240,11 @@ void CellPhonePopup::drawScreenContent() {
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.
+ drawWebDirLabels();
if (_noSignal) {
drawStatusLabels();
+ } else {
+ drawWelcomeScreen();
}
break;
@@ -249,6 +252,7 @@ void CellPhonePopup::drawScreenContent() {
case kPlaceCall:
case kWaitOutgoingRing:
case kLookupContact:
+ drawWebDirLabels();
drawDialLabel();
drawTypeMessage();
drawDialedNumber();
@@ -266,7 +270,6 @@ void CellPhonePopup::drawScreenContent() {
break;
case kDirectory:
- drawDirHeading();
drawDirectoryList();
drawDirectoryArrows();
break;
@@ -422,8 +425,6 @@ void CellPhonePopup::drawCloseButton(uint state) {
}
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;
@@ -457,11 +458,7 @@ void CellPhonePopup::drawDirHeading() {
}
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.
+ // Contacts have one record per dial-pattern variant; collapse by name.
const Font *font = g_nancy->_graphics->getFont(_uiclData->fontId2);
if (!font) {
return;
@@ -476,7 +473,7 @@ void CellPhonePopup::drawDirectoryList() {
contactIdx < _uiclData->contacts.size() && visibleRow < maxRows;
++contactIdx) {
const UICL::Contact &c = _uiclData->contacts[contactIdx];
- if (c.name.empty()) {
+ if (c.name.empty() || !isContactVisible(c)) {
continue;
}
@@ -492,7 +489,6 @@ void CellPhonePopup::drawDirectoryList() {
}
seenNames.push_back(c.name);
- // _directoryScroll counts deduplicated entries.
if (visited < _directoryScroll) {
++visited;
continue;
@@ -507,8 +503,31 @@ void CellPhonePopup::drawDirectoryList() {
}
}
+void CellPhonePopup::drawWelcomeScreen() {
+ const UICL::SrcDestRectPair &ws = _uiclData->welcomeScreen;
+ if (ws.srcRect.isEmpty() || ws.destRect.isEmpty()) {
+ return;
+ }
+ const Common::Point chunkOrigin(_screenPosition.left, _screenPosition.top);
+ _drawSurface.blitFrom(_spritesImage, ws.srcRect,
+ Common::Point(ws.destRect.left - chunkOrigin.x,
+ ws.destRect.top - chunkOrigin.y));
+}
+
+void CellPhonePopup::drawBackLabel() {
+ const UICL::ThreeRectWidget &back = _uiclData->subButtons[2];
+ if (back.srcRectIdle.isEmpty() || back.destRect.isEmpty()) {
+ return;
+ }
+
+ const Common::Point chunkOrigin(_screenPosition.left, _screenPosition.top);
+ _drawSurface.blitFrom(_spritesImage, back.srcRectIdle,
+ Common::Point(back.destRect.left - chunkOrigin.x,
+ back.destRect.top - chunkOrigin.y));
+}
+
void CellPhonePopup::drawDirectoryArrows() {
- // Sub-buttons 1 and 2 carry the up/down scroll arrows for list modes.
+ // Up/down scroll arrows are not in the chrome image; blit on every redraw.
const UICL::ThreeRectWidget &up = _uiclData->subButtons[1];
const UICL::ThreeRectWidget &down = _uiclData->subButtons[2];
@@ -524,6 +543,21 @@ void CellPhonePopup::drawDirectoryArrows() {
Common::Point(down.destRect.left - chunkOrigin.x,
down.destRect.top - chunkOrigin.y));
}
+
+ // Selection indicator next to the active row.
+ const Common::Rect &arrowSrc = _uiclData->dirArrowSrc;
+ if (arrowSrc.isEmpty()) {
+ return;
+ }
+ const uint maxRows = maxDirectoryRows();
+ if (_directorySelection >= maxRows) {
+ return;
+ }
+ const Common::Rect rowRect = directoryRowRect(_directorySelection);
+ const int arrowX = MAX(0, rowRect.left - arrowSrc.width() - 2);
+ const int arrowY = rowRect.top;
+ _drawSurface.blitFrom(_spritesImage, arrowSrc,
+ Common::Point(arrowX, arrowY));
}
// --------------------------------------------------------------------
@@ -535,14 +569,12 @@ void CellPhonePopup::resetDialPad() {
}
void CellPhonePopup::enterScreenState(ScreenState newState) {
- // Always redraw, even when newState == _screenState, so successive
- // digit entries in kDialing each refresh the readout.
+ // Always redraw, so successive digit entries 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;
}
@@ -561,8 +593,7 @@ bool CellPhonePopup::playSoundIfPresent(const Common::Path &soundName) {
_callSound.name = nameStr;
if (_callSound.channelID == 0) {
- // TODO: read the channel from the chunk's per-call channel slot
- // instead of hardcoding it.
+ // TODO: should come from the per-call channel slot in the chunk.
_callSound.channelID = 28;
}
_callSound.volume = 100;
@@ -586,23 +617,25 @@ int CellPhonePopup::findContactByDialBuffer() const {
return -1;
}
- // Contact prefix bytes are slot indices (0..9); _dialedNumber holds
- // '0'..'9' chars, so convert each char back to its slot index.
+ // Dial pattern lives in prefix[2..], terminated by '\n'.
const uint dialLen = _dialedNumber.size();
+ const uint kDialOffset = 2;
for (uint i = 0; i < _uiclData->contacts.size(); ++i) {
const UICL::Contact &c = _uiclData->contacts[i];
+ if (!isContactVisible(c)) {
+ continue;
+ }
bool match = true;
for (uint b = 0; b < dialLen; ++b) {
const byte slotIdx = (byte)(_dialedNumber[b] - '0');
- if (slotIdx != c.unknownPrefix[b]) {
+ if (kDialOffset + b >= sizeof(c.unknownPrefix) ||
+ slotIdx != c.unknownPrefix[kDialOffset + 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') {
+ if (match && kDialOffset + dialLen < sizeof(c.unknownPrefix) &&
+ c.unknownPrefix[kDialOffset + dialLen] == '\n') {
return (int)i;
}
}
@@ -614,31 +647,35 @@ void CellPhonePopup::triggerContactCallSceneChange(uint contactIndex) {
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
+ if (sceneID == kNoScene) {
+ return;
}
+ const uint16 frameID = (uint16)c.unknownSuffix[2] | ((uint16)c.unknownSuffix[3] << 8);
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.frameID = frameID;
scene.verticalOffset = 0;
- scene.continueSceneSound = kContinueSceneSound;
+ // The destination scene's sound carries the conversation audio.
+ scene.continueSceneSound = kLoadSceneSound;
if (eventFlagLabel != -1) {
NancySceneState.setEventFlag(eventFlagLabel,
eventFlagValue ? g_nancy->_true : g_nancy->_false);
}
+ // Pushed so AR 128 in the conversation scene can return the player here.
+ NancySceneState.pushScene();
+
NancySceneState.changeScene(scene);
- setVisible(false);
+ // Phone stays on screen through the conversation; AR 128 closes it.
}
// --------------------------------------------------------------------
@@ -646,7 +683,6 @@ void CellPhonePopup::triggerContactCallSceneChange(uint contactIndex) {
// --------------------------------------------------------------------
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;
@@ -657,15 +693,10 @@ uint CellPhonePopup::maxDirectoryRows() const {
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();
+ const int rowH = MAX(arrow.height() + 4, 14);
+ const Common::Rect &ws = _uiclData->welcomeScreen.destRect;
+ const int firstRowOffset = 22;
+ const int lcdH = MAX(0, ws.height() - firstRowOffset);
return MAX<int>(1, lcdH / rowH);
}
@@ -675,27 +706,66 @@ Common::Rect CellPhonePopup::directoryRowRect(uint visibleIndex) const {
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);
+ const int rowH = MAX(arrow.height() + 4, 14);
+ const Common::Rect &ws = _uiclData->welcomeScreen.destRect;
+ const int lcdLeft = ws.left - _screenPosition.left;
+ const int lcdTop = ws.top - _screenPosition.top;
+ const int lcdWidth = ws.width();
+ const int firstRowOffset = 22;
+
+ const int x = lcdLeft + arrow.width() + 4;
+ const int y = lcdTop + firstRowOffset + (int)visibleIndex * rowH;
+ const int width = MAX(0, lcdWidth - (arrow.width() + 4) - 2);
+ // Clamp to the LCD so a row rect can't leak onto the keypad below.
+ const int lcdBottom = lcdTop + ws.height();
+ const int rowBottom = MIN(y + rowH, lcdBottom);
+ if (rowBottom <= y) {
+ return Common::Rect();
+ }
+ return Common::Rect(x, y, x + width, rowBottom);
+}
+
+bool CellPhonePopup::isContactVisible(const UICL::Contact &c) const {
+ const uint16 flag = (uint16)c.unknownPrefix[0] | ((uint16)c.unknownPrefix[1] << 8);
+ if (flag == 10) {
+ return true;
+ }
+ if (flag == 11) {
+ return false;
+ }
+ return NancySceneState.getEventFlag((int16)flag, g_nancy->_true);
+}
+
+Common::Rect CellPhonePopup::backLabelHitRect() const {
+ // Back overlays the Web/Dir label area. Returns popup-local coordinates.
+ Common::Rect hit;
+ const Common::Rect &web = _uiclData->webLabel.destRect;
+ const Common::Rect &dir = _uiclData->dirLabel.destRect;
+ if (!web.isEmpty()) {
+ hit = web;
+ }
+ if (!dir.isEmpty()) {
+ if (hit.isEmpty()) {
+ hit = dir;
+ } else {
+ hit.extend(dir);
+ }
+ }
+ if (hit.isEmpty()) {
+ return hit;
+ }
+ hit.translate(-_screenPosition.left, -_screenPosition.top);
+ return hit;
}
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()) {
+ if (c.name.empty() || !isContactVisible(c)) {
continue;
}
bool duplicate = false;
@@ -722,8 +792,47 @@ int CellPhonePopup::contactIndexForVisibleRow(uint visibleRow) const {
return -1;
}
+void CellPhonePopup::moveDirectorySelection(int delta) {
+ if (_screenState != kDirectory || delta == 0) {
+ return;
+ }
+
+ const uint total = deduplicatedContactCount();
+ const uint maxRows = maxDirectoryRows();
+ if (total == 0 || maxRows == 0) {
+ return;
+ }
+
+ uint absolute = _directoryScroll + _directorySelection;
+
+ if (delta < 0) {
+ const uint dec = (uint)(-delta);
+ if (dec >= absolute) {
+ absolute = 0;
+ } else {
+ absolute -= dec;
+ }
+ } else {
+ absolute += (uint)delta;
+ if (absolute >= total) {
+ absolute = total - 1;
+ }
+ }
+
+ if (absolute < _directoryScroll) {
+ _directoryScroll = absolute;
+ _directorySelection = 0;
+ } else if (absolute >= _directoryScroll + maxRows) {
+ _directorySelection = maxRows - 1;
+ _directoryScroll = absolute - _directorySelection;
+ } else {
+ _directorySelection = absolute - _directoryScroll;
+ }
+
+ drawScreenContent();
+}
+
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();
@@ -741,16 +850,14 @@ void CellPhonePopup::startCallToContact(uint contactIndex) {
}
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.
+ // Rebuild _dialedNumber so the call flow's lookup matches.
_dialedNumber.clear();
- for (uint b = 0; b < sizeof(c.unknownPrefix); ++b) {
+ for (uint b = 2; 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;
}
@@ -766,7 +873,7 @@ 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()) {
+ if (c.name.empty() || !isContactVisible(c)) {
continue;
}
bool dup = false;
@@ -788,7 +895,6 @@ uint CellPhonePopup::deduplicatedContactCount() const {
// --------------------------------------------------------------------
Common::Point CellPhonePopup::mouseToChunkCoords(const Common::Point &mouse) const {
- // Chunk destRects are screen coords, so chunk-mouse == screen-mouse.
return mouse;
}
@@ -797,14 +903,14 @@ void CellPhonePopup::handleInput(NancyInput &input) {
return;
}
- // Transient call-flow states ignore everything except the close X.
+ // Mid-call states accept only 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.
+ // Close (X) wins on overlap.
if (_uiclData->header.secondaryButtonEnabled) {
const UIButtonRecord &closeBtn = _uiclData->header.secondaryButton;
Common::Rect closeScreen = closeBtn.destRect;
@@ -831,51 +937,61 @@ void CellPhonePopup::handleInput(NancyInput &input) {
}
if (transientCallState) {
- if (_screenPosition.contains(input.mousePos)) {
- input.eatMouseInput();
- }
+ // Block the viewport from seeing the cursor (edge-pan, etc.).
+ 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;
+ // Up/down move the selection; scrolling kicks in at page edges.
if (upDst.contains(chunkMouse)) {
g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
if (input.input & NancyInput::kLeftMouseButtonUp) {
- if (_directoryScroll > 0) {
- --_directoryScroll;
- drawScreenContent();
- }
+ moveDirectorySelection(-1);
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();
- }
+ moveDirectorySelection(+1);
+ input.eatMouseInput();
+ return;
+ }
+ }
+
+ // Invisible Back hotspot. Gated so it can't intercept up/down clicks.
+ const Common::Rect backHit = backLabelHitRect();
+ const Common::Point popupMouse(chunkMouse.x - _screenPosition.left,
+ chunkMouse.y - _screenPosition.top);
+ const bool overUpDown =
+ upDst.contains(chunkMouse) || downDst.contains(chunkMouse);
+ if (!overUpDown && !backHit.isEmpty() && backHit.contains(popupMouse)) {
+ g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ _directoryScroll = 0;
+ _directorySelection = 0;
+ _dialedNumber.clear();
+ enterScreenState(kWelcome);
input.eatMouseInput();
return;
}
}
+ // Row click selects (the call button does the actual call).
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);
+ _directorySelection = row;
+ drawScreenContent();
input.eatMouseInput();
return;
}
@@ -885,11 +1001,38 @@ void CellPhonePopup::handleInput(NancyInput &input) {
// can override the directory by starting a fresh dial.
}
+ // Call/talk button. Checked before the dial-pad loop so an overlapping
+ // slot can't eat it. The keypad Talk key is slot 12; the callButton
+ // widget covers a different region.
+ if (_uiclData->callButton.destRect.contains(chunkMouse) ||
+ _uiclData->dialPadSlots[12].destRect.contains(chunkMouse)) {
+ g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ if (!_noSignal) {
+ if (_screenState == kDirectory) {
+ const int contactIdx =
+ contactIndexForVisibleRow(_directorySelection);
+ if (contactIdx >= 0) {
+ // Pre-resolve so kLookupContact skips the dial-buffer
+ // match and the ring/pickup animation still plays.
+ _resolvedContact = contactIdx;
+ enterScreenState(kPlaceCall);
+ }
+ } else if (!_dialedNumber.empty()) {
+ enterScreenState(kPlaceCall);
+ }
+ }
+ input.eatMouseInput();
+ return;
+ }
+ }
+
// 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)
+ // 10, 11 - *, # (no-op)
+ // 12 - call/talk key (handled above)
+ // 13 - web mode (TODO)
// 14 - directory toggle
int newHovered = -1;
for (uint i = 0; i < UICL::kNumDialPadSlots; ++i) {
@@ -919,42 +1062,24 @@ void CellPhonePopup::handleInput(NancyInput &input) {
} else if (newHovered == 14) {
if (_screenState == kDirectory) {
_directoryScroll = 0;
+ _directorySelection = 0;
enterScreenState(kWelcome);
} else {
_dialedNumber.clear();
_directoryScroll = 0;
+ _directorySelection = 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.
+ // TODO: sub-buttons (email/search/help/browser/in-call menu) not hooked up.
- // Swallow any remaining click so it doesn't fall through to the scene.
- if (_screenPosition.contains(input.mousePos)) {
- input.eatMouseInput();
- }
+ // Block the viewport from acting on the cursor while the phone is up.
+ input.eatMouseInput();
}
} // End of namespace UI
diff --git a/engines/nancy/ui/cellphonepopup.h b/engines/nancy/ui/cellphonepopup.h
index 2d3ca4ffe1c..80a09e83066 100644
--- a/engines/nancy/ui/cellphonepopup.h
+++ b/engines/nancy/ui/cellphonepopup.h
@@ -23,25 +23,17 @@
#define NANCY_UI_CELLPHONEPOPUP_H
#include "engines/nancy/commontypes.h"
+#include "engines/nancy/enginedata.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.
+// Nancy 10+ cell phone popup, driven by the UICL chunk.
+// TODO: email, search, help, browser modes; in-call menu; redial.
class CellPhonePopup : public RenderObject {
public:
CellPhonePopup();
@@ -56,29 +48,26 @@ public:
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.
+ // Swaps the welcome graphic for the No Signal / No Access / Old Email
+ // Only labels and blocks outgoing calls. TODO: hook to scene flag.
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
+ kWelcome = 0,
+ kDialing = 1,
+ kPlaceCall = 2,
kWaitOutgoingRing = 3,
- kLookupContact = 4, // match dial buffer, start pickup
+ kLookupContact = 4,
kWaitPickup = 5,
- kConnected = 6, // trigger contact scene change
- kInvalidNumber = 7, // start invalid-number sound
+ kConnected = 6,
+ kInvalidNumber = 7,
kWaitInvalid = 8,
- kDirectory = 9 // contact list, scrollable
+ kDirectory = 9
};
- // Drawing helpers
- void drawChrome(); // popup overlay + persistent labels + buttons
- void drawScreenContent(); // LCD area: dispatches on _screenState
+ void drawChrome();
+ void drawScreenContent();
void drawStatusIcons();
void drawWebDirLabels();
void drawDialLabel();
@@ -92,8 +81,9 @@ private:
void drawDirectoryList();
void drawDirHeading();
void drawDirectoryArrows();
+ void drawWelcomeScreen();
+ void drawBackLabel();
- // State machine helpers
void resetDialPad();
void enterScreenState(ScreenState newState);
void appendDigit(byte slotIndex);
@@ -102,52 +92,47 @@ private:
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;
+ // True when the contact's visibility flag is currently unlocked.
+ bool isContactVisible(const UICL::Contact &c) const;
+ // Popup-local rect of the Back hotspot in directory mode.
+ Common::Rect backLabelHitRect() const;
+ // Move the directory selection by delta, scrolling as needed.
+ void moveDirectorySelection(int delta);
- // Mouse hit-test helpers
Common::Point mouseToChunkCoords(const Common::Point &mouse) const;
const UICL *_uiclData;
- // Chrome image (header.imageName).
+ // Chrome (header.imageName) and sprite atlas (overlayImageName).
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.
+ // Dialed digits as '0'..'9' chars; convert with `c - '0'` to get
+ // the slot index that matches a contact's dial prefix.
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.
+ // Resolved during kLookupContact, valid through kWaitPickup; -1 = miss.
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.
+ // First visible deduplicated contact, and the active row within the page.
uint _directoryScroll = 0;
+ uint _directorySelection = 0;
- // When true, the LCD shows the status labels and outgoing calls
- // are blocked. See setNoSignal().
bool _noSignal = false;
};
Commit: 36257c744ea4583a0a202819d2e56a7cb4c58417
https://github.com/scummvm/scummvm/commit/36257c744ea4583a0a202819d2e56a7cb4c58417
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-23T04:39:27+03:00
Commit Message:
NANCY: Implement CellPhonePopCellSceneFromStack for Nancy10+
Used to return to the current scene after placing a phone call
Changed paths:
engines/nancy/action/arfactory.cpp
engines/nancy/action/miscrecords.cpp
engines/nancy/action/miscrecords.h
diff --git a/engines/nancy/action/arfactory.cpp b/engines/nancy/action/arfactory.cpp
index 0c741ba7905..0fb762b9254 100644
--- a/engines/nancy/action/arfactory.cpp
+++ b/engines/nancy/action/arfactory.cpp
@@ -332,6 +332,9 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
return new PopInvViewPriorScene();
case 126:
return new GoInvViewScene();
+ case 128:
+ // Nancy 10+
+ return new CellPhonePopCellSceneFromStack();
case 130:
// Nancy 10+
warning("ChangeCellPhoneInfo - not implemented yet");
diff --git a/engines/nancy/action/miscrecords.cpp b/engines/nancy/action/miscrecords.cpp
index d3fad02f548..b0663a851cf 100644
--- a/engines/nancy/action/miscrecords.cpp
+++ b/engines/nancy/action/miscrecords.cpp
@@ -238,6 +238,23 @@ void AddSearchLink::execute() {
finishExecution();
}
+void CellPhonePopCellSceneFromStack::readData(Common::SeekableReadStream &stream) {
+ _sceneChange.readData(stream);
+}
+
+void CellPhonePopCellSceneFromStack::execute() {
+ if (_sceneChange.sceneID == kNoScene) {
+ NancySceneState.popScene(false);
+ } else {
+ NancySceneState.changeScene(_sceneChange);
+ }
+
+ // Conversation is over; take the phone down.
+ NancySceneState.getCellPhonePopup().close();
+
+ finishExecution();
+}
+
void BumpPlayerClock::readData(Common::SeekableReadStream &stream) {
_relative = stream.readByte();
_hours = stream.readUint16LE();
diff --git a/engines/nancy/action/miscrecords.h b/engines/nancy/action/miscrecords.h
index 38f6ce69234..1bf6885b2b1 100644
--- a/engines/nancy/action/miscrecords.h
+++ b/engines/nancy/action/miscrecords.h
@@ -187,6 +187,19 @@ protected:
Common::String getRecordTypeName() const override { return "AddSearchLink"; }
};
+// Returns from a cellphone-driven conversation scene to the pre-call scene.
+// sceneID == kNoScene pops the saved scene; any other sceneID overrides it.
+class CellPhonePopCellSceneFromStack : public ActionRecord {
+public:
+ void readData(Common::SeekableReadStream &stream) override;
+ void execute() override;
+
+ SceneChangeDescription _sceneChange;
+
+protected:
+ Common::String getRecordTypeName() const override { return "CellPhonePopCellSceneFromStack"; }
+};
+
// Changes the in-game time. Used prior to the introduction of SetPlayerClock.
class BumpPlayerClock : public ActionRecord {
public:
Commit: d0aea692cccb553232e9fef8d83fc889bad36e08
https://github.com/scummvm/scummvm/commit/d0aea692cccb553232e9fef8d83fc889bad36e08
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-23T04:39:28+03:00
Commit Message:
NANCY: Add information about cell phone contacts fields
Changed paths:
engines/nancy/enginedata.h
diff --git a/engines/nancy/enginedata.h b/engines/nancy/enginedata.h
index 5c4dc7146e1..cbac93d172a 100644
--- a/engines/nancy/enginedata.h
+++ b/engines/nancy/enginedata.h
@@ -583,9 +583,17 @@ struct UICL : public EngineData {
};
struct Contact {
- byte unknownPrefix[13]; // 13 bytes preceding the name (purpose not yet determined)
- Common::String name; // 20-byte null-terminated contact name
- byte unknownSuffix[8]; // 8 bytes following the name (purpose not yet determined)
+ // Prefix layout:
+ // [0..1] visibility flag (10 = always, 11 = never, else =
+ // scene event-flag index; contact hidden until set).
+ // [2..8] 7-digit dial pattern (slot indices 0..9).
+ // [9] '\n' terminator.
+ // [10..12] unused.
+ byte unknownPrefix[13];
+ Common::String name; // 20-byte null-terminated
+ // Suffix layout: [0..1] sceneID, [2..3] frameID,
+ // [4..5] event-flag label, [6] event-flag value, [7] unused.
+ byte unknownSuffix[8];
};
struct SrcDestRectPair {
Commit: 4c107f1d83027ca53125ac4df75d28fb96b4afcd
https://github.com/scummvm/scummvm/commit/4c107f1d83027ca53125ac4df75d28fb96b4afcd
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-23T04:39:28+03:00
Commit Message:
NANCY: Don't try to load viewport videos in scenes that don't have them
Happens with cell phone scenes in Nancy10+
Changed paths:
engines/nancy/state/scene.cpp
diff --git a/engines/nancy/state/scene.cpp b/engines/nancy/state/scene.cpp
index a1506be3ee4..8af99d22288 100644
--- a/engines/nancy/state/scene.cpp
+++ b/engines/nancy/state/scene.cpp
@@ -1011,12 +1011,18 @@ void Scene::load(bool fromSaveFile) {
_sceneState.currentScene.paletteID = 0;
}
- _viewport.loadVideo(_sceneState.summary.videoFile,
- _sceneState.currentScene.frameID,
- _sceneState.currentScene.verticalOffset,
- _sceneState.summary.panningType,
- _sceneState.summary.videoFormat,
- _sceneState.summary.palettes.size() ? _sceneState.summary.palettes[(byte)_sceneState.currentScene.paletteID] : Common::Path());
+ if (_sceneState.summary.videoFile != "NO_ART_SCENE") {
+ const Common::Path palettePath = !_sceneState.summary.palettes.empty() ?
+ _sceneState.summary.palettes[(byte)_sceneState.currentScene.paletteID] :
+ Common::Path();
+
+ _viewport.loadVideo(_sceneState.summary.videoFile,
+ _sceneState.currentScene.frameID,
+ _sceneState.currentScene.verticalOffset,
+ _sceneState.summary.panningType,
+ _sceneState.summary.videoFormat,
+ palettePath);
+ }
if (_viewport.getFrameCount() <= 1) {
_viewport.disableEdges(kLeft | kRight);
Commit: 02cd306964d52076e70849a6003aeefa664ded55
https://github.com/scummvm/scummvm/commit/02cd306964d52076e70849a6003aeefa664ded55
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-23T04:39:30+03:00
Commit Message:
NANCY: Return an empty string for captions that aren't found
This is a workaround, for now, and happens with some conversations in
Nancy10 (e.g. when calling the Rawleys)
Changed paths:
engines/nancy/action/conversation.cpp
diff --git a/engines/nancy/action/conversation.cpp b/engines/nancy/action/conversation.cpp
index 7f9c4e4efd5..d2622ddc3e0 100644
--- a/engines/nancy/action/conversation.cpp
+++ b/engines/nancy/action/conversation.cpp
@@ -185,7 +185,9 @@ void ConversationSound::readTerseCaptionText(Common::SeekableReadStream &stream)
const CVTX *convo = (const CVTX *)g_nancy->getEngineData("CONVO");
assert(convo);
- _text = convo->texts[key];
+ // WORKAROUND: Return an empty string for captions that aren't found.
+ // Happens with some conversations in Nancy10 (e.g. when calling the Rawleys).
+ _text = convo->texts.getValOrDefault(key, "");
}
void ConversationSound::readTerseResponseText(Common::SeekableReadStream &stream, ResponseStruct &response) {
Commit: 834fe18348c273b7d4f702e4f21092183a97eb1c
https://github.com/scummvm/scummvm/commit/834fe18348c273b7d4f702e4f21092183a97eb1c
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-23T04:39:31+03:00
Commit Message:
NANCY: Refresh the cursor when a piece is selected in multibuildpuzzle
Addresses the remaining issue of not showing the correct mouse cursor
during closeups
Fix #16743
Changed paths:
engines/nancy/action/puzzle/multibuildpuzzle.cpp
diff --git a/engines/nancy/action/puzzle/multibuildpuzzle.cpp b/engines/nancy/action/puzzle/multibuildpuzzle.cpp
index ab3b651f2cb..88ec7581676 100644
--- a/engines/nancy/action/puzzle/multibuildpuzzle.cpp
+++ b/engines/nancy/action/puzzle/multibuildpuzzle.cpp
@@ -436,6 +436,8 @@ void MultiBuildPuzzle::handleInput(NancyInput &input) {
}
if (_selectedPiece != -1) {
+ g_nancy->_cursor->setCursorType(dragCursor);
+
if (input.input & NancyInput::kLeftMouseButtonUp) {
int sel = _selectedPiece;
_selectedPiece = -1;
Commit: 6fd36716c955a45108f4edaa709f4edf92497373
https://github.com/scummvm/scummvm/commit/6fd36716c955a45108f4edaa709f4edf92497373
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-05-23T04:39:32+03:00
Commit Message:
NANCY: More work on inventory item handling for Nancy10+
Changed paths:
engines/nancy/ui/inventorypopup.cpp
diff --git a/engines/nancy/ui/inventorypopup.cpp b/engines/nancy/ui/inventorypopup.cpp
index a352af534b0..eb24e8693b9 100644
--- a/engines/nancy/ui/inventorypopup.cpp
+++ b/engines/nancy/ui/inventorypopup.cpp
@@ -127,10 +127,17 @@ void InventoryPopup::rebuildVisibleList() {
const uint16 numItems = MIN<uint16>(g_nancy->getStaticData().numItems,
_invData->itemDescriptions.size());
+ const int16 heldItem = NancySceneState.getHeldItem();
+
for (uint16 id = 0; id < numItems; ++id) {
if (NancySceneState.hasItem(id) != g_nancy->_true)
continue;
+ // `hasItem` reports the held item as owned; the player is already
+ // carrying it, so don't list it in the grid too.
+ if ((int16)id == heldItem)
+ continue;
+
const INV::ItemDescription &desc = _invData->itemDescriptions[id];
switch (_activeFilterIndex) {
@@ -304,6 +311,14 @@ void InventoryPopup::handleInput(NancyInput &input) {
input.mousePos.x - _screenPosition.left + _uiivData->header.normalDestRect.left,
input.mousePos.y - _screenPosition.top + _uiivData->header.normalDestRect.top);
+ // Nancy 10+ only blends the held-item sprite onto kNormal / kHotspot
+ // cursors. Switch to kHotspot when holding so the item stays visible
+ // while hovering popup widgets.
+ const CursorManager::CursorType hoverCursor =
+ NancySceneState.getHeldItem() == -1
+ ? CursorManager::kHotspotArrow
+ : CursorManager::kHotspot;
+
// Scrollbar interaction takes priority while dragging.
const UISliderRecord &slider = _uiivData->header.slider;
if (_uiivData->header.sliderEnabled) {
@@ -322,7 +337,7 @@ void InventoryPopup::handleInput(NancyInput &input) {
const bool overScrollbar = thumbInChunk.contains(chunkMouse);
if (_scrollbarDragging) {
- g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+ g_nancy->_cursor->setCursorType(hoverCursor);
const int newThumbTop = chunkMouse.y - _scrollbarGrabOffset;
const int clamped = CLIP<int>(newThumbTop, track.top, track.top + travel);
@@ -345,7 +360,7 @@ void InventoryPopup::handleInput(NancyInput &input) {
_needsRedraw = true;
}
if (overScrollbar) {
- g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+ g_nancy->_cursor->setCursorType(hoverCursor);
if (slider.isDraggable && (input.input & NancyInput::kLeftMouseButtonDown)) {
_scrollbarDragging = true;
_scrollbarGrabOffset = chunkMouse.y - thumbY;
@@ -373,7 +388,7 @@ void InventoryPopup::handleInput(NancyInput &input) {
_needsRedraw = true;
}
if (overClose) {
- g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+ g_nancy->_cursor->setCursorType(hoverCursor);
if (input.input & NancyInput::kLeftMouseButtonUp) {
input.eatMouseInput();
close();
@@ -382,28 +397,68 @@ void InventoryPopup::handleInput(NancyInput &input) {
}
}
- int newHovered = -1;
+ // If the player is already holding an item, any click on the slot grid
+ // puts it back into the inventory. Otherwise a click on an occupied
+ // slot picks that item up (or navigates to its close-up scene).
+ const int16 heldItem = NancySceneState.getHeldItem();
+
+ int hoveredSlot = -1;
for (uint i = 0; i < kSlotsPerPage; ++i) {
if (i >= _uiivData->slotDestRects.size())
break;
- if (_slotItemIDs[i] < 0)
+ if (!_uiivData->slotDestRects[i].contains(chunkMouse))
continue;
- if (_uiivData->slotDestRects[i].contains(chunkMouse)) {
- newHovered = (int)i;
- break;
- }
+ if (heldItem == -1 && _slotItemIDs[i] < 0)
+ continue;
+ hoveredSlot = (int)i;
+ break;
}
- if (newHovered != -1) {
- g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+ if (hoveredSlot != -1) {
+ g_nancy->_cursor->setCursorType(hoverCursor);
if (input.input & NancyInput::kLeftMouseButtonUp) {
- const int16 itemID = _slotItemIDs[newHovered];
- if (itemID >= 0) {
- // Pick the item up: it leaves the inventory and becomes
- // the held item (cursor sprite)
- NancySceneState.removeItemFromInventory(itemID, true);
+ if (heldItem != -1) {
+ const int16 slotItem = _slotItemIDs[hoveredSlot];
+
+ // Empty slot: drop the held item back into the inventory
+ // and keep the popup open. Occupied slot: swap â held
+ // item goes into the inventory, the clicked item becomes
+ // the new held item.
+ NancySceneState.addItemToInventory(heldItem);
+ if (slotItem >= 0 && slotItem != heldItem) {
+ NancySceneState.removeItemFromInventory(slotItem, true);
+ }
refreshGrid();
+ input.eatMouseInput();
+ return;
+ }
+
+ const int16 itemID = _slotItemIDs[hoveredSlot];
+ if (itemID >= 0) {
+ const INV::ItemDescription &item = _invData->itemDescriptions[itemID];
+ const byte disabled = NancySceneState.getItemDisabledState(itemID);
+
+ if (disabled) {
+ if (disabled == 2) {
+ NancySceneState.playItemCantSound(itemID);
+ }
+ input.eatMouseInput();
+ return;
+ }
+
+ const bool pickUp = item.keepItem != kInvItemNewSceneView;
+ NancySceneState.removeItemFromInventory(itemID, pickUp);
+
+ if (item.keepItem == kInvItemNewSceneView) {
+ // Close-up view: stash the item and warp to its scene.
+ NancySceneState.pushScene(itemID);
+ SceneChangeDescription sceneChange;
+ sceneChange.sceneID = item.sceneID;
+ sceneChange.continueSceneSound = item.sceneSoundFlag;
+ NancySceneState.changeScene(sceneChange);
+ }
+
close();
input.eatMouseInput();
return;
@@ -433,7 +488,7 @@ void InventoryPopup::handleInput(NancyInput &input) {
continue;
}
- g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+ g_nancy->_cursor->setCursorType(hoverCursor);
drawFilterTab(i, true);
More information about the Scummvm-git-logs
mailing list