[Scummvm-git-logs] scummvm master -> 7dec9a56ab6f657ccb014399a7a08b62ce86521b
bluegr
noreply at scummvm.org
Sun Aug 30 10:25:37 UTC 2026
This automated email contains information about 6 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
d3f59d446c NANCY: NANCY12: Fix the position of Nancy's cash indicator
34fe503989 NANCY: NANCY13: Fix setting the solve scene flag
0cd1152025 NANCY: NANCY13: Map more mouse cursors
a3eaf5c558 NANCY: NANCY13: Implement more cellphone functionality
f2b884e47f NANCY: NANCY9: Implement screen saving
7dec9a56ab NANCY: NANCY14: Implement new sound related ARs
Commit: d3f59d446cd267832030ddc9eb7d0fa97c635ae2
https://github.com/scummvm/scummvm/commit/d3f59d446cd267832030ddc9eb7d0fa97c635ae2
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-08-30T13:25:20+03:00
Commit Message:
NANCY: NANCY12: Fix the position of Nancy's cash indicator
Changed paths:
engines/nancy/ui/taskbar.cpp
diff --git a/engines/nancy/ui/taskbar.cpp b/engines/nancy/ui/taskbar.cpp
index c3129ecea9d..3fcd0a7a585 100644
--- a/engines/nancy/ui/taskbar.cpp
+++ b/engines/nancy/ui/taskbar.cpp
@@ -189,10 +189,12 @@ void Taskbar::drawMoney() {
Common::Rect dst = taskData->buttons[kTaskButtonCoinPurse].button.destRect;
dst.translate(-_screenPosition.left, -_screenPosition.top);
- // Position matches the original: a small inset from the left, and a little
- // below the button's vertical centre.
+ // A small inset from the left, and a little below the button's vertical
+ // center. That vertical coordinate is the bottom row the glyphs are aligned
+ // on, while drawString() takes the top of the line, so shift it up by the
+ // height of a line.
const int x = dst.left + 12;
- const int y = dst.top + dst.height() / 2 + 10;
+ const int y = dst.top + dst.height() / 2 + 10 - font->getFontHeight() + 1;
font->drawString(&_drawSurface, text, x, y, dst.right - x, 0, Graphics::kTextAlignLeft);
_needsRedraw = true;
}
Commit: 34fe5039891d14c9648d3bef0dbccbc78be1254f
https://github.com/scummvm/scummvm/commit/34fe5039891d14c9648d3bef0dbccbc78be1254f
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-08-30T13:25:21+03:00
Commit Message:
NANCY: NANCY13: Fix setting the solve scene flag
Now, the game progresses correctly to the win scene in the Pagoda
Pillar puzzle, inside the Caboose carriage
Changed paths:
engines/nancy/action/puzzle/rotatinglockpuzzle.cpp
diff --git a/engines/nancy/action/puzzle/rotatinglockpuzzle.cpp b/engines/nancy/action/puzzle/rotatinglockpuzzle.cpp
index 850287b67a5..863d1e9fcd7 100644
--- a/engines/nancy/action/puzzle/rotatinglockpuzzle.cpp
+++ b/engines/nancy/action/puzzle/rotatinglockpuzzle.cpp
@@ -130,7 +130,6 @@ void RotatingLockPuzzle::readData(Common::SeekableReadStream &stream) {
_exitScene._flag.flag = stream.readByte();
readRect(stream, _exitHotspot);
- // 16 trailing bytes (cursor type + unused) at offset 0x317 are ignored.
} else {
_solveExitScene.readData(stream);
_solveSoundDelay = stream.readUint16LE();
@@ -174,7 +173,6 @@ void RotatingLockPuzzle::execute() {
}
}
- NancySceneState.setEventFlag(_solveExitScene._flag);
_solveSoundPlayTime = g_nancy->getTotalPlayTime() + _solveSoundDelay * 1000;
_solveState = kPlaySound;
// fall through
@@ -198,10 +196,13 @@ void RotatingLockPuzzle::execute() {
g_nancy->_sound->stopSound(_clickSound);
g_nancy->_sound->stopSound(_solveSound);
+ // The solve event flag is set here, together with the scene change, and
+ // not when the solution is first detected: a record whose own dependency
+ // tests that flag would stop being evaluated before it could trigger.
if (_solveState == kNotSolved)
_exitScene.execute();
else
- NancySceneState.changeScene(_solveExitScene._sceneChange);
+ _solveExitScene.execute();
finishExecution();
}
Commit: 0cd11520252a5d9b620b06cdfaafff31549b4a92
https://github.com/scummvm/scummvm/commit/0cd11520252a5d9b620b06cdfaafff31549b4a92
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-08-30T13:25:21+03:00
Commit Message:
NANCY: NANCY13: Map more mouse cursors
Changed paths:
engines/nancy/cursor.cpp
engines/nancy/cursor.h
diff --git a/engines/nancy/cursor.cpp b/engines/nancy/cursor.cpp
index d15bf7cfd31..b60aebca272 100644
--- a/engines/nancy/cursor.cpp
+++ b/engines/nancy/cursor.cpp
@@ -238,18 +238,19 @@ uint CursorManager::resolveNancy13CursorID(CursorType type, int16 itemID, bool s
case kMoveDown: sysType = kNancy13MoveDown; break;
case kMoveLeft: sysType = kNancy13MoveLeft; break;
case kMoveRight: sysType = kNancy13MoveRight; break;
+ case kRotateRight: sysType = kNancy13RotateRight; break;
+ case kRotateLeft: sysType = kNancy13RotateLeft; break;
+ case kInvertedRotateRight: sysType = kNancy13InvertedRotateRight; break;
+ case kInvertedRotateLeft: sysType = kNancy13InvertedRotateLeft; break;
+ case kHotspotTalk: sysType = kNancy13Talk; hotspot = true; break;
+ // Only puzzles use the types below, so they take the blue variants.
case kRotateCW:
- case kRotateRight:
- case kDialCW:
- case kInvertedRotateRight: sysType = kNancy13RotateCW; break;
+ case kDialCW: sysType = kNancy13PuzzleRotateCW; break;
case kRotateCCW:
- case kRotateLeft:
- case kDialCCW:
- case kInvertedRotateLeft: sysType = kNancy13RotateCCW; break;
- case kDragHand:
- case kDropHand: sysType = kNancy13DropHand; break;
+ case kDialCCW: sysType = kNancy13PuzzleRotateCCW; break;
+ case kDragHand: sysType = kNancy13PuzzleDragHand; break;
+ case kDropHand: sysType = kNancy13PuzzleDropHand; break;
case kPuzzleArrow: sysType = kNancy13PuzzleArrow; hotspot = true; break;
- case kHotspotTalk: sysType = kNancy13Normal; hotspot = true; break; // TODO: talk sprite not yet identified in the sheet
default: sysType = kNancy13Normal; break;
}
diff --git a/engines/nancy/cursor.h b/engines/nancy/cursor.h
index 1cb44f99d30..e1aa240f8fd 100644
--- a/engines/nancy/cursor.h
+++ b/engines/nancy/cursor.h
@@ -110,19 +110,29 @@ public:
// pair at slots (type * 2, type * 2 + 1). These constants are Nancy13
// system TYPE indices (not slot indices); resolveNancy13CursorID turns a
// type into the idle or hotspot slot.
- kNancy13Normal = 0, // Eyeglass (kHotspot is the same type's hotspot slot)
- kNancy13MoveUp = 6, // Scene-change: look/move up
- kNancy13MoveDown = 7, // Scene-change: look/move down
- kNancy13MoveLeft = 8, // Scene-change: move left
- kNancy13MoveRight = 9, // Scene-change: move right
- kNancy13MoveBackward = 10, // Scene-change: move back / exit puzzle
- kNancy13MoveForward = 12, // Scene-change: move forward
- kNancy13Arrow = 14, // Frame / taskbar arrow
- kNancy13Exit = 19, // Exit
- kNancy13DropHand = 24, // Hand shown while carrying a puzzle piece
- kNancy13RotateCCW = 25, // Dial / turn cursor
- kNancy13RotateCW = 26, // Dial / turn cursor
- kNancy13PuzzleArrow = 29 // Generic clickable puzzle hotspot
+ // Types 25-34 are the blue in-puzzle variants of the plain ones.
+ kNancy13Normal = 0, // Eyeglass
+ kNancy13Talk = 5,
+ kNancy13MoveUp = 6,
+ kNancy13MoveDown = 7,
+ kNancy13MoveLeft = 8,
+ kNancy13MoveRight = 9,
+ kNancy13MoveBackward = 10,
+ kNancy13MoveForward = 12,
+ kNancy13Arrow = 14,
+ kNancy13RotateRight = 17,
+ kNancy13RotateLeft = 18,
+ kNancy13Exit = 19,
+ kNancy13InvertedRotateRight = 20,
+ kNancy13InvertedRotateLeft = 21,
+ // Empty. Selected while the cell phone's camera viewfinder is up, so
+ // only the viewfinder is seen.
+ kNancy13Blank = 24,
+ kNancy13PuzzleRotateCW = 25,
+ kNancy13PuzzleRotateCCW = 26,
+ kNancy13PuzzleDragHand = 28,
+ kNancy13PuzzleDropHand = 29,
+ kNancy13PuzzleArrow = 34
};
CursorManager();
Commit: a3eaf5c558c896841b9ae6fbb66e4fe85eee9fe7
https://github.com/scummvm/scummvm/commit/a3eaf5c558c896841b9ae6fbb66e4fe85eee9fe7
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-08-30T13:25:22+03:00
Commit Message:
NANCY: NANCY13: Implement more cellphone functionality
- Split the cellphone contracts fields
- Added enums for the dial pad keys
- Set flags when capturing or sending photos from the camera
- Use the correct camera viewfinder image
- Fix image gallery up/down button placements and functionality
- Save phone photos correctly - this invalidates Nancy13 saves that
contained photos, but this feature didn't work correctly up to now,
so that's not an issue
Changed paths:
engines/nancy/action/miscrecords.cpp
engines/nancy/enginedata.cpp
engines/nancy/enginedata.h
engines/nancy/puzzledata.cpp
engines/nancy/puzzledata.h
engines/nancy/ui/cellphonepopup.cpp
engines/nancy/ui/cellphonepopup.h
diff --git a/engines/nancy/action/miscrecords.cpp b/engines/nancy/action/miscrecords.cpp
index 036e130bb69..43ab948197a 100644
--- a/engines/nancy/action/miscrecords.cpp
+++ b/engines/nancy/action/miscrecords.cpp
@@ -359,14 +359,7 @@ void SetCellPhoneBatteryAndSignal::execute() {
}
void ChangeCellPhoneInfo::readData(Common::SeekableReadStream &stream) {
- stream.read(_contact.unknownPrefix, sizeof(_contact.unknownPrefix));
-
- char nameBuf[21];
- stream.read(nameBuf, 20);
- nameBuf[20] = '\0';
- _contact.name = nameBuf;
-
- stream.read(_contact.unknownSuffix, sizeof(_contact.unknownSuffix));
+ readContact(stream, _contact);
}
void ChangeCellPhoneInfo::execute() {
diff --git a/engines/nancy/enginedata.cpp b/engines/nancy/enginedata.cpp
index 6fd618fd24b..d8175c40c1c 100644
--- a/engines/nancy/enginedata.cpp
+++ b/engines/nancy/enginedata.cpp
@@ -964,6 +964,22 @@ UIBW::UIBW(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
}
}
+// Shared by the UICL chunk and ChangeCellPhoneInfo (AR 130).
+void readContact(Common::SeekableReadStream &stream, UICL::Contact &c) {
+ c.visibility = stream.readUint16LE();
+ stream.read(c.dialPattern, sizeof(c.dialPattern));
+
+ char nameBuf[21];
+ stream.read(nameBuf, 20);
+ nameBuf[20] = 0;
+ c.name = nameBuf;
+
+ c.sceneID = stream.readUint16LE();
+ c.frameID = stream.readUint16LE();
+ c.flag.label = stream.readSint16LE();
+ c.flag.flag = (byte)stream.readUint16LE();
+}
+
UICL::UICL(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
readUIPopupHeader(*chunkStream, header);
@@ -1179,30 +1195,20 @@ UICL::UICL(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
const uint16 entries = MIN<uint16>(contactCount, (uint16)maxEntries);
contacts.resize(entries);
for (uint i = 0; i < entries; ++i) {
- Contact &c = contacts[i];
-
- chunkStream->read(c.unknownPrefix, sizeof(c.unknownPrefix));
-
- char nameBuf[21];
- chunkStream->read(nameBuf, 20);
- nameBuf[20] = '\0';
- c.name = nameBuf;
-
- chunkStream->read(c.unknownSuffix, sizeof(c.unknownSuffix));
+ readContact(*chunkStream, contacts[i]);
}
if (isNancy13) {
- // Trailing captured-picture slot table, added in Nancy 13. These are the
- // game's built-in photo slots; runtime captures are persisted separately
- // (CellPhonePictureData), so this table is parsed but currently unused.
- // TODO: identify PictureRecord::unknown (6 bytes, identical across slots).
- const uint16 pictureCount = chunkStream->readUint16LE();
- pictures.resize(pictureCount);
- for (uint i = 0; i < pictureCount; ++i) {
- PictureRecord &p = pictures[i];
- p.id = chunkStream->readUint16LE();
- readRect(*chunkStream, p.rect);
- chunkStream->read(p.unknown, sizeof(p.unknown));
+ // Trailing table of photographable subjects, added in Nancy 13.
+ const uint16 subjectCount = chunkStream->readUint16LE();
+ cameraSubjects.resize(subjectCount);
+ for (uint i = 0; i < subjectCount; ++i) {
+ CameraSubject &s = cameraSubjects[i];
+ s.sceneID = chunkStream->readSint16LE();
+ readRect(*chunkStream, s.coords);
+ s.captureFlag = chunkStream->readSint16LE();
+ s.sendFlag = chunkStream->readSint16LE();
+ s.recipientIndex = chunkStream->readSint16LE();
}
}
}
diff --git a/engines/nancy/enginedata.h b/engines/nancy/enginedata.h
index 5dfd55342bd..1eec70b1adf 100644
--- a/engines/nancy/enginedata.h
+++ b/engines/nancy/enginedata.h
@@ -598,18 +598,25 @@ struct UICL : public EngineData {
Common::Rect destRect;
};
+ // One phonebook entry, 41 bytes. A person can own several, one per number
+ // they are reachable on; `visibility` picks which is listed.
struct Contact {
- // 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];
+ static const uint kDialPatternLength = 11;
+
+ enum {
+ kAlwaysListed = 10,
+ kNeverListed = 11
+ };
+
+ // kAlwaysListed, kNeverListed, or the event flag that reveals the entry.
+ uint16 visibility = kNeverListed;
+ // Dial-pad slot indices (0..9), newline-terminated unless all 11 are used.
+ byte dialPattern[kDialPatternLength] = {};
+ Common::String name; // 20-byte field
+ // Calling the contact changes to this scene and fires the flag.
+ uint16 sceneID = kNoScene;
+ uint16 frameID = 0;
+ FlagDescription flag;
};
struct SrcDestRectPair {
@@ -618,6 +625,17 @@ struct UICL : public EngineData {
};
static const uint kNumDialPadSlots = 15;
+
+ // The last three are soft keys. Nancy 13 relabels them per screen (Cam or
+ // Dial, Del or Yes, Send or No); the ribbon label says which.
+ enum DialPadKey {
+ kDialKeyStar = 10,
+ kDialKeyHash = 11,
+ kDialKeyTalk = 12,
+ kDialKeyMenu = 13,
+ kDialKeyDirectory = 14
+ };
+
// Nancy 10-12 have 10 online sub-buttons; Nancy 13 added an 11th (the Back
// button, Ghidra widget 0x10) at the front of the array.
static const uint kNumSubButtons = 10;
@@ -708,13 +726,20 @@ struct UICL : public EngineData {
// Nancy 13 added a camera / pictures sub-UI to the cell phone, which
// reorganized the chunk body. The fields below are only populated for
// Nancy 13 and later.
- struct PictureRecord {
- uint16 id = 0;
- Common::Rect rect;
- byte unknown[6] = {};
+
+ // A photographable subject. Framing coords in sceneID sets captureFlag;
+ // sending that snapshot to contact recipientIndex sets sendFlag.
+ struct CameraSubject {
+ int16 sceneID = -1;
+ Common::Rect coords;
+ int16 captureFlag = -1;
+ int16 sendFlag = -1;
+ int16 recipientIndex = -1;
};
- Common::Rect cameraViewSrcRect; // camera viewfinder SRC on the overlay
+ // The LCD screen area: cleared through, and bounds the directory list.
+ // Earlier games keep it in screenOutSrcRect.
+ Common::Rect cameraViewSrcRect;
int32 cameraTextX = 0;
int32 cameraTextY = 0;
Common::Path cameraViewImageName; // "UI_CellCamView_OVL"
@@ -723,9 +748,12 @@ struct UICL : public EngineData {
Common::Rect noPictureScreenRect; // "no pictures" placeholder
Common::Path helpTextKey2; // second CVTX key (phone-use help)
byte screenColors[9] = {}; // 3 RGB colors for the phone screen
- Common::Array<PictureRecord> pictures; // captured-picture slots (up to 50)
+ Common::Array<CameraSubject> cameraSubjects; // up to 50
};
+// Shared by the UICL chunk and ChangeCellPhoneInfo (AR 130).
+void readContact(Common::SeekableReadStream &stream, UICL::Contact &c);
+
// Camera UI, added in Nancy 14. This is a standalone camera. While it is active,
// the cursor becomes a large viewfinder rectangle that the player aims at the
// scene; clicking photographs every subject whose region falls inside the framed
diff --git a/engines/nancy/puzzledata.cpp b/engines/nancy/puzzledata.cpp
index 747dbf39f5d..43f9006b479 100644
--- a/engines/nancy/puzzledata.cpp
+++ b/engines/nancy/puzzledata.cpp
@@ -339,7 +339,8 @@ void CellPhoneData::synchronize(Common::Serializer &ser) {
char nameBuf[21];
for (uint16 i = 0; i < numContacts; ++i) {
UICL::Contact &c = contacts[i];
- ser.syncBytes(c.unknownPrefix, sizeof(c.unknownPrefix));
+ ser.syncAsUint16LE(c.visibility);
+ ser.syncBytes(c.dialPattern, sizeof(c.dialPattern));
if (ser.isSaving()) {
memset(nameBuf, 0, sizeof(nameBuf));
@@ -351,7 +352,15 @@ void CellPhoneData::synchronize(Common::Serializer &ser) {
c.name = nameBuf;
}
- ser.syncBytes(c.unknownSuffix, sizeof(c.unknownSuffix));
+ ser.syncAsUint16LE(c.sceneID);
+ ser.syncAsUint16LE(c.frameID);
+ ser.syncAsSint16LE(c.flag.label);
+
+ uint16 flagValue = c.flag.flag;
+ ser.syncAsUint16LE(flagValue);
+ if (ser.isLoading()) {
+ c.flag.flag = (byte)flagValue;
+ }
}
syncLinkArray(ser, emailMessages);
@@ -394,6 +403,15 @@ void CellPhonePictureData::synchronize(Common::Serializer &ser) {
if (numBytes) {
ser.syncBytes(p.pixels.data(), numBytes);
}
+
+ uint16 numSubjects = (uint16)p.subjects.size();
+ ser.syncAsUint16LE(numSubjects);
+ if (ser.isLoading()) {
+ p.subjects.resize(numSubjects);
+ }
+ for (uint16 j = 0; j < numSubjects; ++j) {
+ ser.syncAsSint16LE(p.subjects[j]);
+ }
}
}
diff --git a/engines/nancy/puzzledata.h b/engines/nancy/puzzledata.h
index 22de11e9043..b3cb0a899e7 100644
--- a/engines/nancy/puzzledata.h
+++ b/engines/nancy/puzzledata.h
@@ -263,6 +263,9 @@ struct CapturedPicture {
uint16 height = 0;
Common::Array<byte> pixels; // width * height * 4, BGRA32
bool sent = false; // true once the player has "sent" it
+
+ // Indices into UICL::cameraSubjects that were inside the viewfinder.
+ Common::Array<int16> subjects;
};
// Nancy 13 camera snapshots. Kept in its own lazily-created PuzzleData chunk so
diff --git a/engines/nancy/ui/cellphonepopup.cpp b/engines/nancy/ui/cellphonepopup.cpp
index 57bfe8101f6..eed69c43cf1 100644
--- a/engines/nancy/ui/cellphonepopup.cpp
+++ b/engines/nancy/ui/cellphonepopup.cpp
@@ -74,7 +74,7 @@ enum {
kN13SubViewPics = 3, // Menu "View Pictures" option (widget 0x13 -> state 0xb)
kN13SubEmail = 4, // Menu "E-mail / Messaging" option (widget 0x14)
kN13SubBrowser = 5, // Menu "Internet Browser" option (widget 0x15, removed)
- kN13SubListUp = 6, // zoomed list / picture-view paging
+ kN13SubListUp = 6, // zoomed list scroll up
kN13SubListDown = 7,
kN13SubBackFull = 8 // Back at the bottom of a zoomed (full-screen) list
};
@@ -155,6 +155,12 @@ void CellPhonePopup::init() {
_spritesImage);
}
+ // The viewfinder is blitted over the scene while aiming; the pointer is
+ // blanked meanwhile.
+ if (!_uiclData->cameraViewImageName.empty()) {
+ g_nancy->_resource->loadImage(_uiclData->cameraViewImageName, _cameraViewImage);
+ }
+
Common::Rect popupRect = _uiclData->header.normalDestRect;
if (_uiclData->header.overlayInGameFrame) {
const VIEW *view = GetEngineData(VIEW);
@@ -269,12 +275,11 @@ void CellPhonePopup::addSearchLink(int16 mode, const SearchLink &link) {
}
void CellPhonePopup::upsertContact(const UICL::Contact &c) {
- // Match against the 11-byte dial pattern (prefix[2..12]). If an entry
- // already carries that pattern, overwrite it; otherwise append.
+ // An entry with this dial pattern is overwritten; otherwise append.
bool replaced = false;
for (uint i = 0; i < _contacts.size(); ++i) {
- if (memcmp(_contacts[i].unknownPrefix + 2,
- c.unknownPrefix + 2, 11) == 0) {
+ if (memcmp(_contacts[i].dialPattern, c.dialPattern,
+ sizeof(c.dialPattern)) == 0) {
_contacts[i] = c;
replaced = true;
break;
@@ -665,11 +670,12 @@ void CellPhonePopup::drawScreenContent() {
drawHeading(_uiclData->dialHilite);
drawBackButton(kSubBack);
if (n13Keyboard) {
- // Relabel the first bottom button (Cam): "Send" while choosing a photo
- // recipient, "Dial" while browsing contacts to place a call.
- drawRibbonLabelAt(_sendingPicture ? _uiclData->sendLabel.srcRect
- : _uiclData->dialingLabel.srcRect,
- _uiclData->dialLabel.destRect);
+ // Cam becomes Dial here; while picking a photo recipient the third
+ // button also becomes Send, and dialling stays available.
+ drawRibbonLabelAt(_uiclData->dialingLabel.srcRect, _uiclData->dialLabel.destRect);
+ if (_sendingPicture) {
+ drawRibbonLabel(_uiclData->sendLabel);
+ }
}
break;
@@ -754,11 +760,11 @@ void CellPhonePopup::drawScreenContent() {
drawRibbonLabel(_uiclData->delLabel);
drawRibbonLabel(_uiclData->sendLabel);
drawBackButton(kSubBack);
- // Paging arrows appear only when there is more than one photo to leaf
- // through (they page _pictureIndex; see handleInput).
- const CellPhonePictureData *pd = pictureData();
- if (pd && pd->pictures.size() > 1) {
+ // Each paging arrow only appears while there is a photo that way.
+ if (canPageToPreviousPicture()) {
drawScrollArrow(scrollUpButton(), _scrollUpHovered);
+ }
+ if (canPageToNextPicture()) {
drawScrollArrow(scrollDownButton(), _scrollDownHovered);
}
break;
@@ -1265,14 +1271,14 @@ void CellPhonePopup::drawContentView() {
}
void CellPhonePopup::drawDirectoryList() {
- // Contacts have one record per dial-pattern variant; collapse by name.
+ // Filtered by the visibility flag alone, in the list's own (alphabetical)
+ // order. Camera subjects address contacts by row, so nothing may collapse.
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;
@@ -1280,21 +1286,9 @@ void CellPhonePopup::drawDirectoryList() {
contactIdx < _contacts.size() && visibleRow < maxRows;
++contactIdx) {
const UICL::Contact &c = _contacts[contactIdx];
- if (c.name.empty() || !isContactVisible(c)) {
- continue;
- }
-
- bool duplicate = false;
- for (uint s = 0; s < seenNames.size(); ++s) {
- if (seenNames[s].equalsIgnoreCase(c.name)) {
- duplicate = true;
- break;
- }
- }
- if (duplicate) {
+ if (!isContactVisible(c)) {
continue;
}
- seenNames.push_back(c.name);
if (visited < _directoryScroll) {
++visited;
@@ -1424,14 +1418,65 @@ void CellPhonePopup::captureViewport(const Common::Rect &screenRegion) {
conv->free();
delete conv;
+ // Every subject wholly inside the framed area is captured.
+ const uint16 sceneID = NancySceneState.getSceneInfo().sceneID;
+ for (uint i = 0; i < _uiclData->cameraSubjects.size(); ++i) {
+ const UICL::CameraSubject &subject = _uiclData->cameraSubjects[i];
+ if (subject.sceneID != (int16)sceneID || !grab.contains(subject.coords)) {
+ continue;
+ }
+
+ pic.subjects.push_back((int16)i);
+ if (subject.captureFlag != kEvNoEvent) {
+ NancySceneState.setEventFlag(subject.captureFlag, g_nancy->_true);
+ }
+ }
+
pd->pictures.push_back(pic);
_pictureIndex = (int)pd->pictures.size() - 1;
}
+void CellPhonePopup::sendCurrentPicture(uint listRow) {
+ _sendingPicture = false;
+
+ CellPhonePictureData *pd = pictureData();
+ if (pd && _pictureIndex >= 0 && _pictureIndex < (int)pd->pictures.size()) {
+ CapturedPicture &pic = pd->pictures[_pictureIndex];
+ pic.sent = true;
+
+ // A subject's recipient is a row in the directory as displayed, not an
+ // index into the full contact table. Wrong recipient, nothing happens.
+ for (uint i = 0; i < pic.subjects.size(); ++i) {
+ const int16 subjectID = pic.subjects[i];
+ if (subjectID < 0 || subjectID >= (int16)_uiclData->cameraSubjects.size()) {
+ continue;
+ }
+
+ const UICL::CameraSubject &subject = _uiclData->cameraSubjects[subjectID];
+ if (subject.recipientIndex == (int16)listRow && subject.sendFlag != kEvNoEvent) {
+ NancySceneState.setEventFlag(subject.sendFlag, g_nancy->_true);
+ }
+ }
+ }
+
+ showMessageScreen(kN13MsgPictureSent, kPictureView);
+}
+
+bool CellPhonePopup::canPageToPreviousPicture() const {
+ return _screenState == kPictureView && _pictureIndex > 0;
+}
+
+bool CellPhonePopup::canPageToNextPicture() const {
+ const CellPhonePictureData *pd = pictureData();
+ return _screenState == kPictureView && pd &&
+ _pictureIndex + 1 < (int)pd->pictures.size();
+}
+
Common::Rect CellPhonePopup::framingScreenRect() const {
+ // Sized like a stored picture, centred on the cursor, clamped to the viewport.
const Common::Rect vp = NancySceneState.getViewport().getScreenPosition();
- const int w = MIN<int>(kFramingWidth, vp.width());
- const int h = MIN<int>(kFramingHeight, vp.height());
+ const int w = MIN<int>(_uiclData->pictureDisplayRect.width(), vp.width());
+ const int h = MIN<int>(_uiclData->pictureDisplayRect.height(), vp.height());
const int cx = CLIP<int>(_framingMouse.x, vp.left + w / 2, vp.right - w / 2);
const int cy = CLIP<int>(_framingMouse.y, vp.top + h / 2, vp.bottom - h / 2);
return Common::Rect(cx - w / 2, cy - h / 2, cx - w / 2 + w, cy - h / 2 + h);
@@ -1444,8 +1489,7 @@ void CellPhonePopup::enterCameraFraming() {
_inCameraFraming = true;
_savedPhoneRect = _screenPosition;
- // Grow the popup to cover the viewport so the framing box can be drawn
- // anywhere over the live scene.
+ // Cover the viewport so the popup keeps input while the player aims.
const Common::Rect vp = NancySceneState.getViewport().getScreenPosition();
moveTo(vp);
_drawSurface.create(vp.width(), vp.height(), g_nancy->_graphics->getScreenPixelFormat());
@@ -1466,22 +1510,15 @@ void CellPhonePopup::exitCameraFraming() {
}
void CellPhonePopup::drawCameraFraming() {
- const uint32 trans = g_nancy->_graphics->getTransColor();
- _drawSurface.clear(trans);
+ _drawSurface.clear(g_nancy->_graphics->getTransColor());
- Common::Rect box = framingScreenRect();
- box.translate(-_screenPosition.left, -_screenPosition.top);
-
- // TODO: The original swaps the mouse cursor for a framing-rectangle sprite
- // loaded from the game resources (via the scene-prep capture path, not the
- // UICL chunk); locate that cursor and blit it here instead of the drawn
- // outline. The 220x176 framing size is likewise a placeholder to confirm.
- const uint32 col = _drawSurface.format.RGBToColor(255, 255, 255);
- _drawSurface.frameRect(box, col);
- if (box.width() > 4 && box.height() > 4) {
- box.grow(-1);
- _drawSurface.frameRect(box, col);
+ if (_cameraViewImage.w && _cameraViewImage.h) {
+ Common::Rect box = framingScreenRect();
+ box.translate(-_screenPosition.left, -_screenPosition.top);
+ _drawSurface.blitFrom(_cameraViewImage,
+ Common::Rect(_cameraViewImage.w, _cameraViewImage.h), box);
}
+
_needsRedraw = true;
}
@@ -1594,11 +1631,10 @@ bool CellPhonePopup::isBrowserArticle() const {
}
const UICL::ThreeRectWidget &CellPhonePopup::scrollUpButton() const {
- // Directory and help scroll with the small-LCD arrow pair (subButtons[1]/[2]);
- // the zoomed email / browser lists use a different pair. Nancy 13 keeps the
- // directory on [1]/[2] and uses the list arrows [6]/[7] elsewhere; earlier
- // games use [5]/[6] for the zoomed lists.
- const bool smallLcd = _screenState == kDirectory || isHelpContentView();
+ // Directory, help and the picture view use the small-LCD pair
+ // (subButtons[1]/[2]); zoomed lists use [6]/[7], or [5]/[6] before Nancy 13.
+ const bool smallLcd = _screenState == kDirectory || _screenState == kPictureView ||
+ isHelpContentView();
if (g_nancy->getGameType() >= kGameTypeNancy13) {
return smallLcd ? _uiclData->subButtons[kN13SubDirUp]
: _uiclData->subButtons[kN13SubListUp];
@@ -1607,7 +1643,8 @@ const UICL::ThreeRectWidget &CellPhonePopup::scrollUpButton() const {
}
const UICL::ThreeRectWidget &CellPhonePopup::scrollDownButton() const {
- const bool smallLcd = _screenState == kDirectory || isHelpContentView();
+ const bool smallLcd = _screenState == kDirectory || _screenState == kPictureView ||
+ isHelpContentView();
if (g_nancy->getGameType() >= kGameTypeNancy13) {
return smallLcd ? _uiclData->subButtons[kN13SubDirDown]
: _uiclData->subButtons[kN13SubListDown];
@@ -1652,10 +1689,13 @@ void CellPhonePopup::drawDirectoryArrows() {
if (selRow >= maxDirectoryRows()) {
return;
}
- const int arrowX = cursor.left - _screenPosition.left;
- const int arrowY = cursor.top - _screenPosition.top + (int)selRow * rowPitch();
- _drawSurface.blitFrom(_spritesImage, arrowSrc,
- Common::Point(arrowX, arrowY));
+
+ // dirArrowSrc is the sprite, dirCursorSrc the box it scales into, stepped
+ // down one row pitch per selected row.
+ Common::Rect dest = cursor;
+ dest.translate(-_screenPosition.left,
+ -_screenPosition.top + (int)selRow * rowPitch());
+ _drawSurface.blitFrom(_spritesImage, arrowSrc, dest);
}
// --------------------------------------------------------------------
@@ -1699,7 +1739,7 @@ void CellPhonePopup::enterScreenState(ScreenState newState) {
bool CellPhonePopup::isDialKeyActive(uint slot) const {
if (_noSignal && (_screenState == kWelcome || _screenState == kDialing ||
_screenState == kOnlineHub)) {
- return slot == 13;
+ return slot == UICL::kDialKeyMenu;
}
return true;
}
@@ -1793,7 +1833,6 @@ int CellPhonePopup::findContactByDialBuffer() const {
// Dial pattern lives in prefix[2..], terminated by '\n'.
const uint dialLen = _dialedNumber.size();
- const uint kDialOffset = 2;
for (uint i = 0; i < _contacts.size(); ++i) {
const UICL::Contact &c = _contacts[i];
if (!isContactVisible(c)) {
@@ -1802,14 +1841,13 @@ int CellPhonePopup::findContactByDialBuffer() const {
bool match = true;
for (uint b = 0; b < dialLen; ++b) {
const byte slotIdx = (byte)(_dialedNumber[b] - '0');
- if (kDialOffset + b >= sizeof(c.unknownPrefix) ||
- slotIdx != c.unknownPrefix[kDialOffset + b]) {
+ if (b >= sizeof(c.dialPattern) || slotIdx != c.dialPattern[b]) {
match = false;
break;
}
}
- if (match && kDialOffset + dialLen < sizeof(c.unknownPrefix) &&
- c.unknownPrefix[kDialOffset + dialLen] == '\n') {
+ if (match && dialLen < sizeof(c.dialPattern) &&
+ c.dialPattern[dialLen] == '\n') {
return (int)i;
}
}
@@ -1823,25 +1861,20 @@ void CellPhonePopup::triggerContactCallSceneChange(uint contactIndex) {
const UICL::Contact &c = _contacts[contactIndex];
- const uint16 sceneID = (uint16)c.unknownSuffix[0] | ((uint16)c.unknownSuffix[1] << 8);
- if (sceneID == kNoScene) {
+ if (c.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 = frameID;
+ scene.sceneID = c.sceneID;
+ scene.frameID = c.frameID;
scene.verticalOffset = 0;
// 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);
+ if (c.flag.label != kEvNoEvent) {
+ NancySceneState.setEventFlag(c.flag.label,
+ c.flag.flag ? g_nancy->_true : g_nancy->_false);
}
// Save the pre-call scene on the popup so AR 128 can return there
@@ -1897,6 +1930,13 @@ int CellPhonePopup::rowTopScreen() const {
return _uiclData->welcomeScreen.destRect.top + 22;
}
+const Common::Rect &CellPhonePopup::lcdListBounds() const {
+ // The LCD screen area; Nancy 13 moved it into the camera block.
+ return _uiclData->cameraViewSrcRect.isEmpty()
+ ? _uiclData->screenOutSrcRect
+ : _uiclData->cameraViewSrcRect;
+}
+
uint CellPhonePopup::maxDirectoryRows() const {
const int pitch = rowPitch();
if (pitch <= 0) {
@@ -1904,7 +1944,7 @@ uint CellPhonePopup::maxDirectoryRows() const {
}
const int yLimit = (isLinkListMode() && !_uiclData->emailListContainer.isEmpty())
? _uiclData->emailListContainer.bottom
- : _uiclData->welcomeScreen.destRect.bottom;
+ : lcdListBounds().bottom;
int y = rowTopScreen();
uint count = 0;
while (y + pitch < yLimit) {
@@ -1916,32 +1956,24 @@ uint CellPhonePopup::maxDirectoryRows() const {
Common::Rect CellPhonePopup::directoryRowRect(uint visibleIndex) const {
const Common::Rect &cursor = _uiclData->dirCursorSrc;
- const Common::Rect &ws = _uiclData->welcomeScreen.destRect;
const int pitch = rowPitch();
- // The web / email lists render under the zoomed (keypad-hidden) chrome,
- // where the LCD extends into the wider emailListContainer. Use that as
- // the right bound so long entries aren't clipped to the narrow
- // keypad-mode screen; the directory list keeps the small LCD.
- const Common::Rect &lcd =
- (isZoomedChromeState() && !_uiclData->emailListContainer.isEmpty())
- ? _uiclData->emailListContainer
- : ws;
-
- // Row text spans from just right of the arrow cursor to a margin
- // inside the LCD's right edge.
int xLeftScreen, xRightScreen;
- if (_screenState == kWebList) {
- // Search list: a plain left-aligned list â no arrow/icon column.
- xLeftScreen = lcd.left + 8;
- xRightScreen = lcd.right - 8;
+ if (isLinkListMode() && !_uiclData->emailListContainer.isEmpty()) {
+ // Under the zoomed chrome the LCD widens into emailListContainer; both
+ // lists span it, indented past the envelope-icon column.
+ const Common::Rect &container = _uiclData->emailListContainer;
+ xLeftScreen = container.left + 5;
+ xRightScreen = container.right;
} else if (!cursor.isEmpty()) {
+ // Indented past the selection arrow, short of the LCD's right edge.
xLeftScreen = cursor.right + 5;
- xRightScreen = lcd.right - 30;
+ xRightScreen = lcdListBounds().right - 30;
} else {
+ const Common::Rect &ws = _uiclData->welcomeScreen.destRect;
const Common::Rect &arrow = _uiclData->dirArrowSrc;
xLeftScreen = ws.left + arrow.width() + 4;
- xRightScreen = lcd.right - 2;
+ xRightScreen = ws.right - 2;
}
const int yTopScreen = rowTopScreen() + (int)visibleIndex * pitch;
@@ -1953,14 +1985,13 @@ Common::Rect CellPhonePopup::directoryRowRect(uint visibleIndex) const {
}
bool CellPhonePopup::isContactVisible(const UICL::Contact &c) const {
- const uint16 flag = (uint16)c.unknownPrefix[0] | ((uint16)c.unknownPrefix[1] << 8);
- if (flag == 10) {
+ if (c.visibility == UICL::Contact::kAlwaysListed) {
return true;
}
- if (flag == 11) {
+ if (c.visibility == UICL::Contact::kNeverListed) {
return false;
}
- return NancySceneState.getEventFlag((int16)flag, g_nancy->_true);
+ return NancySceneState.getEventFlag((int16)c.visibility, g_nancy->_true);
}
Common::Rect CellPhonePopup::hubEmailRect() const {
@@ -2003,25 +2034,12 @@ Common::Rect CellPhonePopup::backLabelHitRect() const {
}
int CellPhonePopup::contactIndexForVisibleRow(uint visibleRow) const {
- Common::Array<Common::String> seenNames;
uint visited = 0;
uint visibleSoFar = 0;
for (uint i = 0; i < _contacts.size(); ++i) {
- const UICL::Contact &c = _contacts[i];
- if (c.name.empty() || !isContactVisible(c)) {
- continue;
- }
- bool duplicate = false;
- for (uint s = 0; s < seenNames.size(); ++s) {
- if (seenNames[s].equalsIgnoreCase(c.name)) {
- duplicate = true;
- break;
- }
- }
- if (duplicate) {
+ if (!isContactVisible(_contacts[i])) {
continue;
}
- seenNames.push_back(c.name);
if (visited < _directoryScroll) {
++visited;
continue;
@@ -2038,7 +2056,7 @@ int CellPhonePopup::contactIndexForVisibleRow(uint visibleRow) const {
uint CellPhonePopup::currentListEntryCount() const {
switch (_screenState) {
case kDirectory:
- return deduplicatedContactCount();
+ return visibleContactCount();
case kWebList:
case kEmailList:
return listVisibleIndices().size();
@@ -2105,8 +2123,8 @@ void CellPhonePopup::startCallToContact(uint contactIndex) {
// Rebuild _dialedNumber so the call flow's lookup matches.
_dialedNumber.clear();
- for (uint b = 2; b < sizeof(c.unknownPrefix); ++b) {
- const byte v = c.unknownPrefix[b];
+ for (uint b = 0; b < sizeof(c.dialPattern); ++b) {
+ const byte v = c.dialPattern[b];
if (v == '\n') {
break;
}
@@ -2122,25 +2140,14 @@ void CellPhonePopup::startCallToContact(uint contactIndex) {
enterScreenState(kPlaceCall);
}
-uint CellPhonePopup::deduplicatedContactCount() const {
- Common::Array<Common::String> seen;
+uint CellPhonePopup::visibleContactCount() const {
+ uint count = 0;
for (uint i = 0; i < _contacts.size(); ++i) {
- const UICL::Contact &c = _contacts[i];
- if (c.name.empty() || !isContactVisible(c)) {
- 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);
+ if (isContactVisible(_contacts[i])) {
+ ++count;
}
}
- return seen.size();
+ return count;
}
// --------------------------------------------------------------------
@@ -2159,6 +2166,8 @@ void CellPhonePopup::handleInput(NancyInput &input) {
// Nancy 13 camera framing: the movable box tracks the mouse; a left click
// takes the shot, a right click cancels back to the welcome screen.
if (_inCameraFraming) {
+ // The pointer itself is blanked while the viewfinder is up.
+ g_nancy->_cursor->setCursorType(CursorManager::kNancy13Blank, true, false);
if (input.mousePos != _framingMouse) {
_framingMouse = input.mousePos;
drawScreenContent();
@@ -2251,11 +2260,10 @@ void CellPhonePopup::handleInput(NancyInput &input) {
// link lists, the content view â help included, which scrolls via the
// small-LCD arrow pair â and the Nancy 13 picture-view paging arrows).
const bool arrowsActive = _screenState == kDirectory || isLinkListMode() ||
- _screenState == kContentView ||
- _screenState == kPictureView;
- const bool overUp = arrowsActive &&
+ _screenState == kContentView;
+ const bool overUp = (arrowsActive || canPageToPreviousPicture()) &&
scrollUpButton().destRect.contains(chunkMouse);
- const bool overDown = arrowsActive && !overUp &&
+ const bool overDown = (arrowsActive || canPageToNextPicture()) && !overUp &&
scrollDownButton().destRect.contains(chunkMouse);
if (overUp != _scrollUpHovered || overDown != _scrollDownHovered) {
_scrollUpHovered = overUp;
@@ -2308,11 +2316,10 @@ void CellPhonePopup::handleInput(NancyInput &input) {
drawScreenContent();
}
- // Help "?" button: opens the help page in the content view. Hidden
- // (and unclickable) on sub-screens that already show their own heading.
- if (!isSubScreenState() && !_noSignal &&
+ // Only on the welcome / dialing screen, which also keeps it off the picture
+ // view's paging arrows.
+ if (helpVisible &&
!_uiclData->helpButton.destRect.isEmpty() && !_uiclData->helpTextKey.empty() &&
- !(_screenState == kContentView && _contentKey == _uiclData->helpTextKey) &&
_uiclData->helpButton.destRect.contains(chunkMouse)) {
g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
if (input.input & NancyInput::kLeftMouseButtonUp) {
@@ -2368,6 +2375,7 @@ void CellPhonePopup::handleInput(NancyInput &input) {
_directoryScroll = 0;
_directorySelection = 0;
_dialedNumber.clear();
+ _sendingPicture = false;
enterScreenState(kWelcome);
input.eatMouseInput();
return;
@@ -2389,8 +2397,8 @@ void CellPhonePopup::handleInput(NancyInput &input) {
}
}
}
- // Fall through so slot 14 can toggle the mode off and slots 0..9
- // can override the directory by starting a fresh dial.
+ // Fall through: the Directory key toggles the mode off, digits start a
+ // fresh dial.
}
// Online hub: two labels â Email and Web â plus the Back hotspot.
@@ -2458,11 +2466,11 @@ void CellPhonePopup::handleInput(NancyInput &input) {
CellPhonePictureData *pd = pictureData();
const int numPics = pd ? (int)pd->pictures.size() : 0;
- Common::Rect camR = _uiclData->dialPadSlots[12].destRect;
- Common::Rect delR = _uiclData->dialPadSlots[13].destRect;
- Common::Rect sendR = _uiclData->dialPadSlots[14].destRect;
- Common::Rect upR = _uiclData->subButtons[kN13SubListUp].destRect;
- Common::Rect downR = _uiclData->subButtons[kN13SubListDown].destRect;
+ Common::Rect camR = _uiclData->dialPadSlots[UICL::kDialKeyTalk].destRect;
+ Common::Rect delR = _uiclData->dialPadSlots[UICL::kDialKeyMenu].destRect;
+ Common::Rect sendR = _uiclData->dialPadSlots[UICL::kDialKeyDirectory].destRect;
+ Common::Rect upR = _uiclData->subButtons[kN13SubDirUp].destRect;
+ Common::Rect downR = _uiclData->subButtons[kN13SubDirDown].destRect;
const Common::Point origin(_screenPosition.left, _screenPosition.top);
camR.translate(-origin.x, -origin.y);
delR.translate(-origin.x, -origin.y);
@@ -2471,17 +2479,14 @@ void CellPhonePopup::handleInput(NancyInput &input) {
downR.translate(-origin.x, -origin.y);
const Common::Rect backHit = backButtonHitRect(kSubBack);
- const bool overButton = camR.contains(popupMouse) ||
- (numPics > 0 && (delR.contains(popupMouse) || sendR.contains(popupMouse))) ||
- (numPics > 1 && (upR.contains(popupMouse) || downR.contains(popupMouse))) ||
- (!backHit.isEmpty() && backHit.contains(popupMouse));
- if (overButton) {
+ // Back's pressed sprite and the arrows' hover state are handled above.
+ const bool overBackButton = !backHit.isEmpty() && backHit.contains(popupMouse);
+ if (overBackButton || camR.contains(popupMouse) ||
+ (numPics > 0 && (delR.contains(popupMouse) || sendR.contains(popupMouse))) ||
+ (canPageToPreviousPicture() && upR.contains(popupMouse)) ||
+ (canPageToNextPicture() && downR.contains(popupMouse))) {
g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
}
- if (overButton != _backButtonHovered) {
- _backButtonHovered = overButton;
- drawScreenContent();
- }
if (input.input & NancyInput::kLeftMouseButtonUp) {
if (camR.contains(popupMouse)) {
@@ -2497,13 +2502,13 @@ void CellPhonePopup::handleInput(NancyInput &input) {
_directoryScroll = 0;
_directorySelection = 0;
enterScreenState(kDirectory);
- } else if (numPics > 1 && upR.contains(popupMouse)) {
- _pictureIndex = (_pictureIndex + numPics - 1) % numPics;
+ } else if (canPageToPreviousPicture() && upR.contains(popupMouse)) {
+ --_pictureIndex;
drawScreenContent();
- } else if (numPics > 1 && downR.contains(popupMouse)) {
- _pictureIndex = (_pictureIndex + 1) % numPics;
+ } else if (canPageToNextPicture() && downR.contains(popupMouse)) {
+ ++_pictureIndex;
drawScreenContent();
- } else if (!backHit.isEmpty() && backHit.contains(popupMouse)) {
+ } else if (overBackButton) {
enterScreenState(kOnlineHub);
}
input.eatMouseInput();
@@ -2513,15 +2518,13 @@ void CellPhonePopup::handleInput(NancyInput &input) {
return;
}
- // Nancy 13 delete-confirm ("DELETE? YES OR NO"): Yes and No are the Menu
- // (slot 13) and Dir (slot 14) dial-pad keys relabelled (like slot 12's
- // Cam/Dial/Send), so those slots are the hitboxes. Yes deletes the current
- // photo; No returns to the picture view.
+ // Delete-confirm: Yes and No are the Menu and Directory soft keys
+ // relabelled, so those keys are the hitboxes.
if (_screenState == kDeleteConfirm) {
const Common::Point popupMouse(chunkMouse.x - _screenPosition.left,
chunkMouse.y - _screenPosition.top);
- Common::Rect yesR = _uiclData->dialPadSlots[13].destRect;
- Common::Rect noR = _uiclData->dialPadSlots[14].destRect;
+ Common::Rect yesR = _uiclData->dialPadSlots[UICL::kDialKeyMenu].destRect;
+ Common::Rect noR = _uiclData->dialPadSlots[UICL::kDialKeyDirectory].destRect;
yesR.translate(-_screenPosition.left, -_screenPosition.top);
noR.translate(-_screenPosition.left, -_screenPosition.top);
if (yesR.contains(popupMouse) || noR.contains(popupMouse)) {
@@ -2797,40 +2800,39 @@ void CellPhonePopup::handleInput(NancyInput &input) {
}
}
+ // The Directory soft key becomes Send while picking a recipient. Checked
+ // before the dial-pad loop so its normal action can't eat the click.
+ if (_sendingPicture && _screenState == kDirectory &&
+ _uiclData->dialPadSlots[UICL::kDialKeyDirectory].destRect.contains(chunkMouse)) {
+ g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
+
+ if (input.input & NancyInput::kLeftMouseButtonUp) {
+ playDialPadSound(_uiclData->dialPadSlots[UICL::kDialKeyDirectory].soundName);
+ sendCurrentPicture(_directoryScroll + _directorySelection);
+ input.eatMouseInput();
+ return;
+ }
+ }
+
// Call/talk button. Checked before the dial-pad loop so an overlapping
- // slot can't eat it. The Talk key is dial-pad slot 12. Only live while the
- // keypad is on screen (skipped in the zoomed web / email / browser views).
- if (keypadVisible && isDialKeyActive(12) &&
- _uiclData->dialPadSlots[12].destRect.contains(chunkMouse)) {
+ // slot can't eat it. Only live while the keypad is on screen (skipped in
+ // the zoomed web / email / browser views).
+ if (keypadVisible && isDialKeyActive(UICL::kDialKeyTalk) &&
+ _uiclData->dialPadSlots[UICL::kDialKeyTalk].destRect.contains(chunkMouse)) {
g_nancy->_cursor->setCursorType(CursorManager::kHotspotArrow);
if (input.input & NancyInput::kLeftMouseButtonUp) {
- playDialPadSound(_uiclData->dialPadSlots[12].soundName);
+ playDialPadSound(_uiclData->dialPadSlots[UICL::kDialKeyTalk].soundName);
if (g_nancy->getGameType() >= kGameTypeNancy13 &&
(_screenState == kWelcome || _screenState == kDialing)) {
- // Nancy 13's slot 12 is dual-purpose (Ghidra widget 0xc): on the
- // welcome / dialing screen it is the camera button â enter the
- // framing overlay (the phone yields to the viewport and a movable
- // box marks the shot). In the directory it dials instead, so fall
- // through to the call logic below.
+ // The Talk key doubles as the camera button on the welcome /
+ // dialing screen; in the directory it dials, so fall through.
_screenState = kCamera;
enterCameraFraming();
drawScreenContent();
input.eatMouseInput();
return;
}
- if (_sendingPicture && _screenState == kDirectory) {
- // Choosing a photo recipient: the first button is "Send" â mark
- // the picture sent and show the confirmation.
- _sendingPicture = false;
- CellPhonePictureData *pd = pictureData();
- if (pd && _pictureIndex >= 0 && _pictureIndex < (int)pd->pictures.size()) {
- pd->pictures[_pictureIndex].sent = true;
- }
- showMessageScreen(kN13MsgPictureSent, kPictureView);
- input.eatMouseInput();
- return;
- }
if (!_noSignal) {
if (_screenState == kDirectory) {
const int contactIdx =
@@ -2852,12 +2854,8 @@ void CellPhonePopup::handleInput(NancyInput &input) {
}
}
- // Dial-pad slot behaviour:
- // 0..9 - digit input
- // 10, 11 - *, # (no-op)
- // 12 - call/talk key, or Nancy 13 camera/dial/send (handled above)
- // 13 - Menu: opens the online hub (e-mail + web / view pictures)
- // 14 - directory toggle
+ // Keys below kDialKeyStar enter digits; Star / Hash do nothing; Talk is
+ // handled above; Menu opens the online hub and Directory toggles the list.
int newHovered = -1;
if (keypadVisible) {
for (uint i = 0; i < UICL::kNumDialPadSlots; ++i) {
@@ -2878,19 +2876,19 @@ void CellPhonePopup::handleInput(NancyInput &input) {
playDialPadSound(slot.soundName);
- if (newHovered < 10) {
+ if (newHovered < UICL::kDialKeyStar) {
if (_screenState == kDirectory || isLinkListMode()) {
_dialedNumber.clear();
}
appendDigit((byte)newHovered);
- } else if (newHovered == 13) {
+ } else if (newHovered == UICL::kDialKeyMenu) {
// Opens the Email/Web hub. Re-pressing does not toggle back to
// the welcome screen â the on-screen Back button does that.
_dialedNumber.clear();
_directoryScroll = 0;
_directorySelection = 0;
enterScreenState(kOnlineHub);
- } else if (newHovered == 14) {
+ } else if (newHovered == UICL::kDialKeyDirectory) {
_dialedNumber.clear();
_directoryScroll = 0;
_directorySelection = 0;
diff --git a/engines/nancy/ui/cellphonepopup.h b/engines/nancy/ui/cellphonepopup.h
index d71a955c265..6f818634779 100644
--- a/engines/nancy/ui/cellphonepopup.h
+++ b/engines/nancy/ui/cellphonepopup.h
@@ -158,7 +158,13 @@ private:
void drawCameraFraming();
// Screen-space rect of the framing box, centred on the mouse and clamped to
// the viewport.
+ // Each paging arrow is only drawn and live while its side has a photo.
+ bool canPageToPreviousPicture() const;
+ bool canPageToNextPicture() const;
Common::Rect framingScreenRect() const;
+ // Marks the shown snapshot sent and fires the flags of every subject in it
+ // addressed to the contact on the given directory row.
+ void sendCurrentPicture(uint listRow);
// Blit a sub-button's idle sprite at its chunk dest (used for the visible
// Back buttons: subButtons[0] on the help / directory / online screens,
// subButtons[7] in the zoomed email / browser content view).
@@ -225,7 +231,7 @@ private:
void enterScreenState(ScreenState newState);
// With no signal the phone locks to "Old Email Only": on the welcome,
// dialing and online-hub screens every keypad key is dead except Menu
- // (slot 13), which still reaches the e-mail list. Digits, *, #, Talk, Dir
+ // (the Menu key), which still reaches the e-mail list. Digits, *, #, Talk, Dir
// and the Help "?" go inert. The directory keeps its keys so the reachable
// e-mail path still works.
bool isDialKeyActive(uint slot) const;
@@ -253,6 +259,8 @@ private:
void triggerContactCallSceneChange(uint contactIndex);
int findContactByDialBuffer() const;
+ // LCD content bounds the directory list is laid out against.
+ const Common::Rect &lcdListBounds() const;
uint maxDirectoryRows() const;
uint directoryRowAt(const Common::Point &chunkMouse) const;
Common::Rect directoryRowRect(uint visibleIndex) const;
@@ -271,9 +279,9 @@ private:
Common::Rect hubEmailRect() const;
Common::Rect hubWebRect() const;
void startCallToContact(uint contactIndex);
- // Visible (deduplicated) row -> raw contact index, or -1.
+ // Directory row -> raw contact index, or -1.
int contactIndexForVisibleRow(uint visibleRow) const;
- uint deduplicatedContactCount() const;
+ uint visibleContactCount() const;
// Entry count for whichever list the popup is currently showing.
uint currentListEntryCount() const;
// Absolute indices into the current list's backing array that pass
@@ -307,6 +315,8 @@ private:
// Chrome (header.imageName) and sprite atlas (overlayImageName).
Graphics::ManagedSurface _overlayImage;
Graphics::ManagedSurface _spritesImage;
+ // Nancy 13 camera viewfinder, drawn over the scene while aiming.
+ Graphics::ManagedSurface _cameraViewImage;
bool _closeButtonHovered = false;
bool _scrollUpHovered = false;
@@ -351,8 +361,6 @@ private:
bool _inCameraFraming = false;
Common::Rect _savedPhoneRect; // phone rect to restore when framing ends
Common::Point _framingMouse; // last mouse pos (screen coords)
- static const int kFramingWidth = 220;
- static const int kFramingHeight = 176;
// The original caps the persisted camera roll at 50 pictures.
static const uint kMaxPictures = 50;
Commit: f2b884e47f3a6dc09f7214812b9d5483faf1cde6
https://github.com/scummvm/scummvm/commit/f2b884e47f3a6dc09f7214812b9d5483faf1cde6
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-08-30T13:25:23+03:00
Commit Message:
NANCY: NANCY9: Implement screen saving
This is used as an easter egg in the Nancy9 sandcastle puzzle, as well
as saving a snapshot of the final award screen, shown after completing
Nancy12
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 ab2a338fa10..dc850f5307e 100644
--- a/engines/nancy/action/arfactory.cpp
+++ b/engines/nancy/action/arfactory.cpp
@@ -403,11 +403,8 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
return new SetPlayerClock(); // Moved from 170 in Nancy12
else
return new SetVolume(); // Legacy SetVolume slot (used up to Nancy8)
- case 141:
- // MakeScreenFile, moved here from 148 in Nancy12.
- // Saves a cropped image of the screen to a bitmap/TGA file.
- // TODO: debug-only feature, not implemented
- return nullptr;
+ case 141: // Nancy12
+ return new MakeScreenFile(); // Moved from 148 in Nancy12
case 143: // Nancy14 - ConcatSound
case 144: // Nancy14 - MultiSound (dropped from the Nancy15 dispatch)
// Sibling sound ARs. ConcatSound plays a list of named sounds back-to-back;
@@ -426,8 +423,8 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
return new StopSound(); // Nancy13: StopSound moved here (was 154)
if (g_nancy->getGameType() >= kGameTypeNancy12)
return new SetVolume(); // Moved from 149 in Nancy12
- // MakeScreenFile - seems to save a cropped image of the screen in a bitmap file?
- // TODO: Used in Nancy 9, sand castle puzzle. Moved to 141 in Nancy12.
+ if (g_nancy->getGameType() >= kGameTypeNancy9)
+ return new MakeScreenFile(); // Moved to 141 in Nancy12
return nullptr;
case 149:
if (g_nancy->getGameType() >= kGameTypeNancy13)
@@ -452,9 +449,8 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
case 153:
return new PlaySoundMultiHS();
case 154:
+ case 155: // StopAndUnloadSound, but we always unload
return new StopSound();
- case 155:
- return new StopSound(); // StopAndUnloadSound, but we always unload
case 156: // Nancy11
return new Update3DSound();
case 157:
diff --git a/engines/nancy/action/miscrecords.cpp b/engines/nancy/action/miscrecords.cpp
index 43ab948197a..4ecd1a880dc 100644
--- a/engines/nancy/action/miscrecords.cpp
+++ b/engines/nancy/action/miscrecords.cpp
@@ -35,6 +35,10 @@
#include "common/events.h"
#include "common/config-manager.h"
#include "common/random.h"
+#include "common/file.h"
+
+#include "image/png.h"
+
#include "nancy/ui/taskbar.h"
namespace Nancy {
@@ -419,6 +423,44 @@ void SaveContinueGame::execute() {
_isDone = true;
}
+void MakeScreenFile::readData(Common::SeekableReadStream &stream) {
+ readFilename(stream, _filename);
+
+ readRect(stream, _cropRect);
+
+ // Image format selector, unused
+ stream.skip(1);
+}
+
+void MakeScreenFile::execute() {
+ Graphics::ManagedSurface screenshot;
+ g_nancy->_graphics->screenshotScreen(screenshot);
+
+ Common::Rect cropRect = _cropRect;
+
+ if (cropRect.isValidRect()) {
+ cropRect.clip(Common::Rect(screenshot.w, screenshot.h));
+
+ if (!cropRect.isEmpty()) {
+ Common::Path outName(_filename + ".png");
+ Common::DumpFile outFile;
+
+ if (outFile.open(outName)) {
+ if (!Image::writePNG(outFile, screenshot.getSubArea(cropRect))) {
+ warning("Could not write screen file %s", outName.toString().c_str());
+ }
+
+ outFile.finalize();
+ outFile.close();
+ } else {
+ warning("Could not create screen file %s", outName.toString().c_str());
+ }
+ }
+ }
+
+ finishExecution();
+}
+
void TurnOffMainRendering::readData(Common::SeekableReadStream &stream) {
stream.skip(1);
}
diff --git a/engines/nancy/action/miscrecords.h b/engines/nancy/action/miscrecords.h
index e383914f898..6f387b8e88a 100644
--- a/engines/nancy/action/miscrecords.h
+++ b/engines/nancy/action/miscrecords.h
@@ -264,6 +264,26 @@ protected:
Common::String getRecordTypeName() const override { return "SaveContinueGame"; }
};
+// Nancy9 AR 148, moved to AR 141 in Nancy12. Saves a cropped picture of the
+// current frame to a PNG file. Used as an easter egg, e.g. on the sandcastle
+// screen in Danger on Deception Island.
+class MakeScreenFile : public ActionRecord {
+public:
+ void readData(Common::SeekableReadStream &stream) override;
+ void execute() override;
+
+ Common::String _filename;
+ Common::Rect _cropRect;
+
+ Common::String getRecordExtraInfo() const override {
+ return Common::String::format("Filename: %s.png, crop rect: (%d, %d, %d, %d)",
+ _filename.c_str(), _cropRect.left, _cropRect.top, _cropRect.right, _cropRect.bottom);
+ }
+
+protected:
+ Common::String getRecordTypeName() const override { return "MakeScreenFile"; }
+};
+
// Stops the screen from rendering. Our rendering system is different from the original engine's,
// so we have no use for this.
class TurnOffMainRendering : public Unimplemented {
Commit: 7dec9a56ab6f657ccb014399a7a08b62ce86521b
https://github.com/scummvm/scummvm/commit/7dec9a56ab6f657ccb014399a7a08b62ce86521b
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-08-30T13:25:25+03:00
Commit Message:
NANCY: NANCY14: Implement new sound related ARs
New ARs:
- ConcatSound and MultiSound - play a list of named sounds consecutively
- SetMovieVolume - changes movie volume by its filename
Changed paths:
engines/nancy/action/arfactory.cpp
engines/nancy/action/secondarymovie.cpp
engines/nancy/action/secondarymovie.h
engines/nancy/action/soundrecords.cpp
engines/nancy/action/soundrecords.h
engines/nancy/movieplayer.cpp
engines/nancy/movieplayer.h
diff --git a/engines/nancy/action/arfactory.cpp b/engines/nancy/action/arfactory.cpp
index dc850f5307e..5e43bffb63d 100644
--- a/engines/nancy/action/arfactory.cpp
+++ b/engines/nancy/action/arfactory.cpp
@@ -405,11 +405,10 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
return new SetVolume(); // Legacy SetVolume slot (used up to Nancy8)
case 141: // Nancy12
return new MakeScreenFile(); // Moved from 148 in Nancy12
- case 143: // Nancy14 - ConcatSound
- case 144: // Nancy14 - MultiSound (dropped from the Nancy15 dispatch)
- // Sibling sound ARs. ConcatSound plays a list of named sounds back-to-back;
- // TODO: not yet implemented
- return nullptr;
+ case 143: // Nancy14
+ return new ConcatSound();
+ case 144: // Nancy14
+ return new MultiSound();
case 145: // Nancy13
return new PlaySound(); // Moved from 150 in Nancy13
case 146: // Nancy13
@@ -437,7 +436,7 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
return nullptr;
case 150:
if (g_nancy->getGameType() >= kGameTypeNancy14)
- return nullptr; // Nancy14: SetMovieVolume, TODO. PlaySound moved to 145 in Nancy13.
+ return new SetMovieVolume(); // PlaySound moved to 145 in Nancy13
return new PlaySound();
case 151:
if (g_nancy->getGameType() <= kGameTypeNancy6)
diff --git a/engines/nancy/action/secondarymovie.cpp b/engines/nancy/action/secondarymovie.cpp
index 7016df86ed0..c93386d14e2 100644
--- a/engines/nancy/action/secondarymovie.cpp
+++ b/engines/nancy/action/secondarymovie.cpp
@@ -196,7 +196,9 @@ void PlaySecondaryMovie::readRandomMovieDataNancy14(Common::Serializer &ser, Com
ser.skip(4); // Two u16s (object offsets 0x8c / 0xe7); purpose not yet mapped
ser.syncAsSint16LE(_sceneChange.sceneID);
ser.syncAsUint16LE(_sceneChange.frameID);
- ser.skip(1); // Per-movie volume byte (movie sound off since Nancy6)
+
+ ser.syncAsByte(_movieVolume);
+ _movieVolume = MIN<byte>(_movieVolume, 100);
uint16 sequenceCount = 0;
ser.syncAsUint16LE(sequenceCount);
@@ -278,6 +280,8 @@ bool PlaySecondaryMovie::activateRandomSequence(int index) {
return false;
}
+ _decoder.setVolume(_movieVolume);
+
resolveSentinelFrames();
_isFinished = false;
@@ -295,6 +299,8 @@ bool PlaySecondaryMovie::activateSecondaryMovie() {
return false;
}
+ _decoder.setVolume(_movieVolume);
+
resolveSentinelFrames();
_isFinished = false;
@@ -488,12 +494,12 @@ void PlaySecondaryMovie::readDataNancy14(Common::Serializer &ser, Common::Seekab
_videoSceneChange = _sceneChange.sceneID != kNoScene ? kMovieSceneChange : kMovieNoSceneChange;
- // Per-movie volume; consumed but unused (movie sound is off since Nancy6).
- // AR 44 always carries it. AR 47 does too, but only from Nancy15 - the
- // retail flipped the "mode" convention, and Nancy14's AR 47 omits the byte.
+ // Per-movie volume. AR 44 always carries it. AR 47 does too, but only from
+ // Nancy15 - the retail flipped the "mode" convention, and Nancy14's AR 47
+ // omits the byte.
if (_type == 44 || (_type == 47 && isNancy15)) {
- byte movieVolume = 0;
- ser.syncAsByte(movieVolume);
+ ser.syncAsByte(_movieVolume);
+ _movieVolume = MIN<byte>(_movieVolume, 100);
}
uint16 numFrameFlags = 0;
@@ -632,6 +638,8 @@ void PlaySecondaryMovie::init() {
error("Couldn't load video file %s", _videoName.toString().c_str());
}
+ _decoder.setVolume(_movieVolume);
+
if (!_paletteName.empty()) {
GraphicsManager::loadSurfacePalette(_fullFrame, _paletteName);
GraphicsManager::loadSurfacePalette(_drawSurface, _paletteName);
diff --git a/engines/nancy/action/secondarymovie.h b/engines/nancy/action/secondarymovie.h
index 49fd839635a..a711b717f27 100644
--- a/engines/nancy/action/secondarymovie.h
+++ b/engines/nancy/action/secondarymovie.h
@@ -115,6 +115,11 @@ public:
// unused by playback.
uint16 _playStyle = 1;
+ // Volume of the movie's audio track, as a percentage. Carried by Nancy14+
+ // AR 44/47 only; every other record plays at full volume. SetMovieVolume
+ // (AR 150) can change it later.
+ byte _movieVolume = 100;
+
// AR 47 "InteractiveVideo" (a PlaySecondaryMovie subclass): after the
// normal AR-44-style movie data it carries a name, a flag byte, and a
// list of named {value, flag} entries. Read but not yet acted on.
diff --git a/engines/nancy/action/soundrecords.cpp b/engines/nancy/action/soundrecords.cpp
index b4143548b55..290f06b22e7 100644
--- a/engines/nancy/action/soundrecords.cpp
+++ b/engines/nancy/action/soundrecords.cpp
@@ -24,6 +24,7 @@
#include "common/system.h"
#include "engines/nancy/nancy.h"
+#include "engines/nancy/movieplayer.h"
#include "engines/nancy/sound.h"
#include "engines/nancy/util.h"
@@ -83,6 +84,20 @@ void SetVolume::execute() {
_isDone = true;
}
+void SetMovieVolume::readData(Common::SeekableReadStream &stream) {
+ readFilename(stream, movieName);
+ volume = MIN<byte>(stream.readByte(), 100);
+}
+
+void SetMovieVolume::execute() {
+ MoviePlayer *movie = MoviePlayer::findLoadedMovie(movieName);
+ if (movie) {
+ movie->setVolume(volume);
+ }
+
+ _isDone = true;
+}
+
void FadeSoundToSilence::readData(Common::SeekableReadStream &stream) {
channel = stream.readUint16LE();
stream.skip(2); // pad / flag
@@ -488,5 +503,132 @@ void TableIndexPlaySound::execute() {
PlaySoundCC::execute();
}
+void ConcatMultiSound::readData(Common::SeekableReadStream &stream) {
+ // Sound records split into groups; each group declares its size.
+ int16 remaining = stream.readSint16LE();
+ while (remaining > 0) {
+ int16 groupSize = stream.readSint16LE();
+ if (groupSize <= 0) {
+ break;
+ }
+
+ _groups.push_back(SoundGroup());
+ SoundGroup &group = _groups.back();
+ for (int16 i = 0; i < groupSize; ++i) {
+ group.sounds.push_back(SequencedSound());
+ SequencedSound &sound = group.sounds.back();
+ readFilename(stream, sound.name); // 33-byte field
+ sound.flag = stream.readByte();
+ sound.delay = stream.readSint16LE();
+ }
+
+ // ConcatSound: flag pairs per group.
+ if (perGroupFlags()) {
+ int16 numFlags = stream.readSint16LE();
+ for (int16 i = 0; i < numFlags; ++i) {
+ group.flags.push_back(FlagDescription());
+ group.flags.back().label = stream.readSint16LE();
+ group.flags.back().flag = (byte)stream.readSint16LE();
+ }
+ }
+
+ remaining -= groupSize;
+ }
+
+ // Shared sound descriptor.
+ _sound.channelID = stream.readUint16LE();
+ _sound.numLoops = (uint16)stream.readSint32LE(); // stored as an int32 on disk
+ _sound.volume = stream.readUint16LE();
+ _exitSceneID = stream.readSint16LE();
+ _field35 = stream.readByte();
+
+ // MultiSound: one shared set of flag pairs.
+ if (!perGroupFlags()) {
+ int16 numFlags = stream.readSint16LE();
+ for (int16 i = 0; i < numFlags; ++i) {
+ _sharedFlags.push_back(FlagDescription());
+ _sharedFlags.back().label = stream.readSint16LE();
+ _sharedFlags.back().flag = (byte)stream.readSint16LE();
+ }
+ }
+
+ _sound.name = "NO SOUND";
+}
+
+void ConcatMultiSound::startCurrentSound() {
+ SoundGroup &group = _groups[_currentGroup];
+ SequencedSound &sound = group.sounds[_currentSound];
+
+ // ConcatSound: apply the group's flags on its first sound.
+ if (perGroupFlags() && _currentSound == 0) {
+ for (const FlagDescription &flag : group.flags) {
+ NancySceneState.setEventFlag(flag);
+ }
+ }
+
+ _sound.name = sound.name;
+ if (!_sound.name.empty() && _sound.name != "NO SOUND") {
+ g_nancy->_sound->loadSound(_sound);
+ g_nancy->_sound->playSound(_sound);
+ }
+
+ _delayEnd = g_nancy->getTotalPlayTime() + (sound.delay > 0 ? (uint32)sound.delay * 1000 : 0);
+}
+
+void ConcatMultiSound::execute() {
+ switch (_state) {
+ case kBegin:
+ _currentGroup = 0;
+ _currentSound = 0;
+ _soundStarted = false;
+
+ // MultiSound: apply the shared flags up front.
+ if (!perGroupFlags()) {
+ for (const FlagDescription &flag : _sharedFlags) {
+ NancySceneState.setEventFlag(flag);
+ }
+ }
+
+ _state = kRun;
+ break;
+ case kRun:
+ if (_currentGroup >= _groups.size()) {
+ _state = kActionTrigger;
+ break;
+ }
+
+ if (_currentSound >= _groups[_currentGroup].sounds.size()) {
+ ++_currentGroup;
+ _currentSound = 0;
+ _soundStarted = false;
+ break;
+ }
+
+ if (!_soundStarted) {
+ startCurrentSound();
+ _soundStarted = true;
+ } else {
+ // Advance once the sound finishes and its delay elapses.
+ bool soundDone = !g_nancy->_sound->isSoundPlaying(_sound);
+ bool delayDone = g_nancy->getTotalPlayTime() >= _delayEnd;
+ if (soundDone && delayDone) {
+ ++_currentSound;
+ _soundStarted = false;
+ }
+ }
+
+ break;
+ case kActionTrigger:
+ if (_exitSceneID != kNoScene) {
+ SceneChangeDescription desc;
+ desc.sceneID = _exitSceneID;
+ NancySceneState.changeScene(desc);
+ }
+
+ finishExecution();
+ break;
+ }
+}
+
} // End of namespace Action
} // End of namespace Nancy
diff --git a/engines/nancy/action/soundrecords.h b/engines/nancy/action/soundrecords.h
index bf69d95bd39..7b571714efe 100644
--- a/engines/nancy/action/soundrecords.h
+++ b/engines/nancy/action/soundrecords.h
@@ -40,6 +40,20 @@ protected:
Common::String getRecordTypeName() const override { return "SetVolume"; }
};
+// Nancy14 AR 150. Changes the volume of a movie that is already loaded,
+// addressed by its filename.
+class SetMovieVolume : public ActionRecord {
+public:
+ void readData(Common::SeekableReadStream &stream) override;
+ void execute() override;
+
+ Common::Path movieName;
+ byte volume = 0;
+
+protected:
+ Common::String getRecordTypeName() const override { return "SetMovieVolume"; }
+};
+
// Nancy 11+ AR 147. Linearly ramps a channel's volume down to 0 over
// the given time, then stops execution.
class FadeSoundToSilence : public ActionRecord {
@@ -251,6 +265,57 @@ protected:
int16 _lastIndexVal = -1;
};
+// Nancy14 sequenced sound player (AR 143 ConcatSound / 144 MultiSound): plays a
+// list of grouped sounds one after another on a shared channel, then optionally
+// changes scene. ConcatSound keeps a set of event flags per group, MultiSound one
+// shared set.
+class ConcatMultiSound : public ActionRecord {
+public:
+ void readData(Common::SeekableReadStream &stream) override;
+ void execute() override;
+
+protected:
+ struct SequencedSound {
+ Common::String name;
+ byte flag = 0;
+ int16 delay = 0; // seconds to hold after the sound starts
+ };
+
+ struct SoundGroup {
+ Common::Array<SequencedSound> sounds;
+ Common::Array<FlagDescription> flags; // ConcatSound only
+ };
+
+ // Flags stored per group (ConcatSound) or as one shared set (MultiSound).
+ virtual bool perGroupFlags() const = 0;
+
+ void startCurrentSound();
+
+ Common::Array<SoundGroup> _groups;
+ Common::Array<FlagDescription> _sharedFlags; // MultiSound only
+ SoundDescription _sound;
+ int16 _exitSceneID = kNoScene;
+ byte _field35 = 0;
+
+ // Runtime state
+ uint _currentGroup = 0;
+ uint _currentSound = 0;
+ bool _soundStarted = false;
+ uint32 _delayEnd = 0;
+};
+
+class ConcatSound : public ConcatMultiSound {
+protected:
+ bool perGroupFlags() const override { return true; }
+ Common::String getRecordTypeName() const override { return "ConcatSound"; }
+};
+
+class MultiSound : public ConcatMultiSound {
+protected:
+ bool perGroupFlags() const override { return false; }
+ Common::String getRecordTypeName() const override { return "MultiSound"; }
+};
+
} // End of namespace Action
} // End of namespace Nancy
diff --git a/engines/nancy/movieplayer.cpp b/engines/nancy/movieplayer.cpp
index 77ac2d48f80..5fbb850ea0e 100644
--- a/engines/nancy/movieplayer.cpp
+++ b/engines/nancy/movieplayer.cpp
@@ -30,6 +30,8 @@
#include "engines/nancy/util.h"
#include "engines/nancy/commontypes.h"
+#include "audio/mixer.h"
+
#include "engines/nancy/movieplayer.h"
namespace Nancy {
@@ -45,9 +47,34 @@ private:
MoviePlayer &_owner;
};
+Common::Array<MoviePlayer *> MoviePlayer::_loadedMovies;
+
MoviePlayer::MoviePlayer() {}
-MoviePlayer::~MoviePlayer() {}
+MoviePlayer::~MoviePlayer() {
+ unregisterMovie();
+}
+
+void MoviePlayer::unregisterMovie() {
+ for (uint i = 0; i < _loadedMovies.size(); ++i) {
+ if (_loadedMovies[i] == this) {
+ _loadedMovies.remove_at(i);
+ break;
+ }
+ }
+
+ _loadedName.clear();
+}
+
+MoviePlayer *MoviePlayer::findLoadedMovie(const Common::Path &name) {
+ for (MoviePlayer *movie : _loadedMovies) {
+ if (movie->_loadedName.equalsIgnoreCase(name)) {
+ return movie;
+ }
+ }
+
+ return nullptr;
+}
byte MoviePlayer::resolvePlaytype(byte videoPlaytype) {
if (videoPlaytype != kVideoPlaytypeAuto) {
@@ -60,6 +87,7 @@ byte MoviePlayer::resolvePlaytype(byte videoPlaytype) {
bool MoviePlayer::loadFile(const Common::Path &name, byte videoPlaytype, bool bidirectionalCache) {
freeFrameCache();
+ unregisterMovie();
const Common::Path avfPath = name.append(".avf");
const Common::Path bikPath = name.append(".bik");
@@ -114,6 +142,9 @@ bool MoviePlayer::loadFile(const Common::Path &name, byte videoPlaytype, bool bi
g_nancy->addDeferredLoader(_cacheLoader);
}
+ _loadedName = name;
+ _loadedMovies.push_back(this);
+
return true;
}
@@ -124,6 +155,7 @@ bool MoviePlayer::isVideoLoaded() const {
void MoviePlayer::close() {
_currentSurface = nullptr;
freeFrameCache();
+ unregisterMovie();
if (_decoder) {
_decoder->close();
}
@@ -193,6 +225,12 @@ Audio::Timestamp MoviePlayer::getDuration() const { return _decoder->getDuration
uint16 MoviePlayer::getWidth() const { return _decoder->getWidth(); }
uint16 MoviePlayer::getHeight() const { return _decoder->getHeight(); }
+void MoviePlayer::setVolume(byte percent) {
+ if (_decoder) {
+ _decoder->setVolume(MIN<byte>(percent, 100) * Audio::Mixer::kMaxChannelVolume / 100);
+ }
+}
+
void MoviePlayer::addFrameTime(uint16 timeToAdd) {
if (_decoder && _videoType == kVideoPlaytypeAVF) {
((AVFDecoder *)_decoder.get())->addFrameTime(timeToAdd);
diff --git a/engines/nancy/movieplayer.h b/engines/nancy/movieplayer.h
index ed2a5c7a578..36e4a734d38 100644
--- a/engines/nancy/movieplayer.h
+++ b/engines/nancy/movieplayer.h
@@ -89,6 +89,13 @@ public:
uint16 getHeight() const;
void addFrameTime(uint16 timeToAdd); // AVF only, no-op otherwise
+ // Sets the volume of the movie's audio track, as a percentage (0 - 100).
+ void setVolume(byte percent);
+
+ // Loaded movies are registered under their filename, so SetMovieVolume
+ // (Nancy14 AR 150) can address one that is already playing by name.
+ static MoviePlayer *findLoadedMovie(const Common::Path &name);
+
// Decode a frame: frameNr < 0 returns the next frame; otherwise that
// specific frame via the format-appropriate cached path (AVF decodeFrame;
// Bink decodes forward when possible and caches frames, see _frameCache).
@@ -112,11 +119,17 @@ private:
void storeCurrentFrame();
void freeFrameCache();
+ void unregisterMovie();
bool fillNextCacheFrame(); // decode one uncached frame; true when the cache is full
Common::ScopedPtr<Video::VideoDecoder> _decoder;
byte _videoType = kVideoPlaytypeAVF;
+ // Name this movie was loaded with, and the list of all currently loaded
+ // movies; both only serve findLoadedMovie().
+ Common::Path _loadedName;
+ static Common::Array<MoviePlayer *> _loadedMovies;
+
// 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; the
More information about the Scummvm-git-logs
mailing list