[Scummvm-git-logs] scummvm master -> ce70c890a6d8016a97d49a9795614ddbfc3336ac

bluegr noreply at scummvm.org
Sun Jul 19 02:38:15 UTC 2026


This automated email contains information about 4 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .

Summary:
1a38c9a894 NANCY: NANCY10: Always use the grab-hand cursor for GridMapPuzzle
f674c058a4 NANCY: NANCY13: Handle talkable characters with random animations
9ccfefbf06 NANCY: NANCY13: Initial work on the changes to cellphone (e.g. camera)
ce70c890a6 NANCY: NANCY13: Initial ActionZone changes needed for PachinkoPuzzle


Commit: 1a38c9a8948a8f81ee6aa914ef9bb30698b41799
    https://github.com/scummvm/scummvm/commit/1a38c9a8948a8f81ee6aa914ef9bb30698b41799
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-19T05:27:55+03:00

Commit Message:
NANCY: NANCY10: Always use the grab-hand cursor for GridMapPuzzle

Fix #16980

Changed paths:
    engines/nancy/action/puzzle/gridmappuzzle.cpp


diff --git a/engines/nancy/action/puzzle/gridmappuzzle.cpp b/engines/nancy/action/puzzle/gridmappuzzle.cpp
index bb1659be529..ec59d18b315 100644
--- a/engines/nancy/action/puzzle/gridmappuzzle.cpp
+++ b/engines/nancy/action/puzzle/gridmappuzzle.cpp
@@ -347,20 +347,20 @@ void GridMapPuzzle::handleInput(NancyInput &input) {
 	if (!hitMap)
 		hitItems = hitTestItems(mouseVP, iRow, iCol);
 
+	// The whole puzzle uses the grab-hand cursor; only the exit hotspot differs.
+	// Picking something up doesn't change it.
 	if (!hitMap && !hitItems) {
 		if (!_exitHotspot.isEmpty() && _exitHotspot.contains(mouseVP)) {
 			g_nancy->_cursor->setCursorType(g_nancy->_cursor->_puzzleExitCursor);
 			if (input.input & NancyInput::kLeftMouseButtonUp)
 				_subState = kExitToCancel;
 		} else {
-			g_nancy->_cursor->setCursorType(_heldItem != -1 ? CursorManager::kDragHand
-			                                                : CursorManager::kNormal);
+			g_nancy->_cursor->setCursorType(CursorManager::kDropHand);
 		}
 		return;
 	}
 
-	g_nancy->_cursor->setCursorType(_heldItem != -1 ? CursorManager::kDragHand
-	                                                : CursorManager::kNormal);
+	g_nancy->_cursor->setCursorType(CursorManager::kDropHand);
 	if (!(input.input & NancyInput::kLeftMouseButtonUp))
 		return;
 


Commit: f674c058a42d6d6e2df6730b499d7508ba25e731
    https://github.com/scummvm/scummvm/commit/f674c058a42d6d6e2df6730b499d7508ba25e731
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-19T05:31:43+03:00

Commit Message:
NANCY: NANCY13: Handle talkable characters with random animations

Changed paths:
    engines/nancy/action/secondarymovie.cpp
    engines/nancy/action/secondarymovie.h


diff --git a/engines/nancy/action/secondarymovie.cpp b/engines/nancy/action/secondarymovie.cpp
index 99279aa5947..bbf06540bab 100644
--- a/engines/nancy/action/secondarymovie.cpp
+++ b/engines/nancy/action/secondarymovie.cpp
@@ -19,7 +19,9 @@
  *
  */
 
+#include "engines/nancy/cursor.h"
 #include "engines/nancy/graphics.h"
+#include "engines/nancy/input.h"
 #include "engines/nancy/nancy.h"
 #include "engines/nancy/sound.h"
 #include "engines/nancy/util.h"
@@ -63,6 +65,32 @@ bool PlaySecondaryMovie::isPersistentAcrossScenes() const {
 	return _isRandom && g_nancy->getGameType() < kGameTypeNancy13 && !_isDone && !_randomStopRequested;
 }
 
+void PlaySecondaryMovie::handleInput(NancyInput &input) {
+	// The character's box (set as the hotspot while it is on screen) is
+	// clickable; clicking opens its conversation scene, and hovering drives the
+	// recognition movie. The talk hover cursor is applied by ActionManager via
+	// getHoverCursor().
+	if (!_hasHotspot || _talkSceneID == kNoScene) {
+		_isHovered = false;
+		return;
+	}
+
+	_isHovered = NancySceneState.getViewport().convertViewportToScreen(_hotspot).contains(input.mousePos);
+
+	if (_isHovered && (input.input & NancyInput::kLeftMouseButtonUp)) {
+		input.eatMouseInput();
+		SceneChangeDescription desc;
+		desc.sceneID = _talkSceneID;
+		NancySceneState.changeScene(desc);
+	}
+}
+
+CursorManager::CursorType PlaySecondaryMovie::getHoverCursor() const {
+	// The character's own cursor type (a raw Nancy13 cursor id) comes from the
+	// secondary record; cursorSetFromScript() routes it through the raw-slot path.
+	return (CursorManager::CursorType)_talkCursorType;
+}
+
 void PlaySecondaryMovie::readRandomSequence(Common::Serializer &ser, RandomSequence &seq) {
 	readFilename(ser, seq.name);
 	ser.syncAsUint16LE(seq.startFrame);
@@ -92,14 +120,17 @@ void PlaySecondaryMovie::readRandomSequence(Common::Serializer &ser, RandomSeque
 
 void PlaySecondaryMovie::readSecondaryRandomMovie(Common::Serializer &ser, RandomSequence &seq) {
 	// Nancy13's random-movie chunk carries one extra "secondary" movie record
-	// (e.g. a character's recognition animation) between the sequence list and
-	// the hotspot list. It is a name + 5 uint16 + a next-list; only the first
-	// two uint16s (start/last frame) have known roles so far, but the whole
-	// record must be consumed here or the hotspot list below misaligns.
+	// (a character's recognition animation) between the sequence list and the
+	// hotspot list: a name + 5 uint16 + a next-list. The 4th uint16 is the scene
+	// to open when the character is clicked (its conversation); 9999 means the
+	// character isn't clickable. The whole record must be consumed here or the
+	// hotspot list below misaligns.
 	readFilename(ser, seq.name);
 	ser.syncAsUint16LE(seq.startFrame);
 	ser.syncAsUint16LE(seq.lastFrame);
-	ser.skip(6);	// three uint16 fields, roles not yet characterized
+	ser.syncAsUint16LE(_talkCursorType);	// hover cursor for the character
+	ser.syncAsUint16LE(_talkSceneID);
+	ser.skip(2);	// conversation frameID (0 in known data)
 
 	uint16 nextCount = 0;
 	ser.syncAsUint16LE(nextCount);
@@ -201,6 +232,23 @@ bool PlaySecondaryMovie::activateRandomSequence(int index) {
 	return true;
 }
 
+bool PlaySecondaryMovie::activateSecondaryMovie() {
+	_videoName = _secondaryMovie.name;
+	_firstFrame = _secondaryMovie.startFrame;
+	_lastFrame = _secondaryMovie.lastFrame;
+
+	if (!_decoder.loadFile(_videoName)) {
+		warning("PlayRandomMovie: couldn't load recognition movie %s", _videoName.toString().c_str());
+		return false;
+	}
+
+	resolveSentinelFrames();
+
+	_isFinished = false;
+	_curViewportFrame = -1;
+	return true;
+}
+
 void PlaySecondaryMovie::resolveSentinelFrames() {
 	// Random sequences use -1/-2 for the start/last frame to mean "play the
 	// movie's own first/last frame". Resolve them now that the decoder (and
@@ -526,6 +574,19 @@ void PlaySecondaryMovie::execute() {
 			break;
 		}
 
+		// Talkable character: swap immediately between the idle loop and the
+		// recognition ("turn around") movie as the mouse enters/leaves the
+		// character, without waiting for the current cycle to finish.
+		if (isTalkable()) {
+			if (_isHovered && !_playingSecondary) {
+				_playingSecondary = true;
+				activateSecondaryMovie();
+			} else if (!_isHovered && _playingSecondary) {
+				_playingSecondary = false;
+				activateRandomSequence(_activeSequenceIndex);
+			}
+		}
+
 		int newFrame = NancySceneState.getSceneInfo().frameID;
 
 		if (newFrame != _curViewportFrame) {
@@ -541,13 +602,22 @@ void PlaySecondaryMovie::execute() {
 			if (activeFrame != -1) {
 				_screenPosition = _videoDescs[activeFrame].destRect;
 				setVisible(true);
+
+				// Nancy13 talkable characters: the character's on-screen box
+				// doubles as a clickable hotspot that opens its conversation.
+				if (_talkSceneID != kNoScene) {
+					_hotspot = _screenPosition;
+					_hasHotspot = true;
+				}
 			} else if (_isRandom) {
 				// Random movies aren't gated on hotspot/viewport-frame
 				// matches the way regular PSMs are: play full viewport.
 				_screenPosition = NancySceneState.getViewport().getBounds();
 				setVisible(true);
+				_hasHotspot = false;
 			} else {
 				setVisible(false);
+				_hasHotspot = false;
 			}
 		}
 
@@ -608,6 +678,16 @@ void PlaySecondaryMovie::execute() {
 				// by a PlayRandomMovieControl, wind the AR down normally.
 				if (_randomStopRequested) {
 					_state = kActionTrigger;
+				} else if (isTalkable()) {
+					// Hover swaps are handled at the top of kRun. Here we only
+					// keep the idle loop going; the recognition movie, once
+					// finished, holds on its last frame while the mouse stays.
+					if (!_playingSecondary) {
+						// Replay the idle movie in place without reopening it.
+						_isFinished = false;
+						_decoder.seekToFrame(_playDirection == kPlayMovieReverse ? _lastFrame : _firstFrame);
+						_decoder.pauseVideo(false);
+					}
 				} else {
 					int picked = rollNextSequence();
 					if (picked >= 0) {
diff --git a/engines/nancy/action/secondarymovie.h b/engines/nancy/action/secondarymovie.h
index 4a1d659f0ca..3073c61f35a 100644
--- a/engines/nancy/action/secondarymovie.h
+++ b/engines/nancy/action/secondarymovie.h
@@ -121,11 +121,17 @@ public:
 	uint16 _randomPlayerCursorAllowed = kPlayerCursorAllowed;
 	Common::Array<RandomSequence> _sequences;
 
-	// Nancy13+ carries one extra "secondary" movie (e.g. a recognition
-	// animation) after the sequence list. Stored for future playback; reading
-	// it is required so the trailing hotspot list stays aligned.
+	// Nancy13+ carries one extra "secondary" movie (a recognition animation)
+	// after the sequence list. Stored for future playback; reading it is
+	// required so the trailing hotspot list stays aligned.
 	RandomSequence _secondaryMovie;
 
+	// Nancy13 talkable characters: the scene to open when the character is
+	// clicked (its conversation). kNoScene means the character isn't clickable.
+	uint16 _talkSceneID = kNoScene;
+	// Hover cursor for the character (a raw Nancy13 cursor id from the chunk).
+	uint16 _talkCursorType = 0;
+
 	// Chain state. After a sequence's movie finishes the engine rolls a
 	// weighted pick: "stay" -> enter pause for a random duration and
 	// re-roll; valid next-sequence -> swap to that sequence's movie.
@@ -135,6 +141,11 @@ public:
 	uint32 _randomPauseEndTime = 0;
 	bool _randomStopRequested = false;
 
+	// Talkable-character hover state: whether the mouse is over the character,
+	// and whether the recognition (secondary) movie is currently playing.
+	bool _isHovered = false;
+	bool _playingSecondary = false;
+
 	// Called by PlayRandomMovieControl::execute() to wind down the AR.
 	void stopRandom() { _randomStopRequested = true; }
 
@@ -145,6 +156,13 @@ public:
 
 	bool isPersistentAcrossScenes() const override;
 
+	// Nancy13 talkable characters expose the character's on-screen box as a
+	// clickable hotspot with a talk cursor; clicking opens _talkSceneID, and
+	// hovering plays the recognition ("turn around") movie.
+	void handleInput(NancyInput &input) override;
+	CursorManager::CursorType getHoverCursor() const override;
+	bool cursorSetFromScript() const override { return _isRandom && _talkSceneID != kNoScene; }
+
 	Common::String getRecordExtraInfo() const override { return Common::String::format("Scene %d", _sceneChange.sceneID); }
 
 protected:
@@ -164,6 +182,13 @@ protected:
 	// and reload the decoder. Returns true on success.
 	bool activateRandomSequence(int index);
 
+	// Load & start the recognition (secondary) movie in place of the idle loop.
+	bool activateSecondaryMovie();
+
+	// A Nancy13 talkable character: has a conversation scene and a recognition
+	// movie to swap to on hover.
+	bool isTalkable() const { return _isRandom && _talkSceneID != kNoScene && !_secondaryMovie.name.empty(); }
+
 	// Pick the next sequence (or "stay") per the weighted random rules.
 	// Returns -1 if "stay" was picked (and sets up the pause state),
 	// or the chosen sequence index otherwise.


Commit: 9ccfefbf06532bdf3c3e6552e615895868e88afd
    https://github.com/scummvm/scummvm/commit/9ccfefbf06532bdf3c3e6552e615895868e88afd
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-19T05:34:27+03:00

Commit Message:
NANCY: NANCY13: Initial work on the changes to cellphone (e.g. camera)

The cellphone has been revamped in Nancy13 (e.g. it has a camera now).
Add handling for these fields, to keep the rest of the phone functional

Changed paths:
    engines/nancy/enginedata.cpp
    engines/nancy/enginedata.h


diff --git a/engines/nancy/enginedata.cpp b/engines/nancy/enginedata.cpp
index da61b27a392..bbcbfb30482 100644
--- a/engines/nancy/enginedata.cpp
+++ b/engines/nancy/enginedata.cpp
@@ -964,64 +964,131 @@ UICL::UICL(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
 		dialPadSlots[i].soundName = nameBuf;
 	}
 
-	// Screen-frame and label rects
-	readRect(*chunkStream, dialHilite.srcRect);
-	readRect(*chunkStream, dialHilite.destRect);
-	readRect(*chunkStream, screenOutSrcRect);
-	statusTextX = chunkStream->readSint32LE();
-	statusTextY = chunkStream->readSint32LE();
-	readRect(*chunkStream, welcomeScreen.srcRect);
-	readRect(*chunkStream, welcomeScreen.destRect);
-
 	char labelBuf[21];
+	const bool isNancy13 = g_nancy->getGameType() >= kGameTypeNancy13;
+
+	// Version-specific preamble: Nancy 13 replaced the dial-highlight /
+	// screen-out / welcome block with a camera sub-UI, and moved the
+	// dial/web/dir labels ahead of the status labels.
+	if (isNancy13) {
+		readRect(*chunkStream, cameraViewSrcRect);
+		cameraTextX = chunkStream->readSint32LE();
+		cameraTextY = chunkStream->readSint32LE();
+		readFilename(*chunkStream, cameraViewImageName);
+		readFilename(*chunkStream, cameraClickSound);
+		readRect(*chunkStream, pictureDisplayRect);
+
+		// Camera picture-slot / thumbnail data (int16 rects). Not yet mapped.
+		chunkStream->skip(114);
+
+		readRect(*chunkStream, noPictureScreenRect);
+
+		// The dialed-number baseline is the camera text position (Nancy 13
+		// dropped the separate statusText field). welcomeScreen is read from the
+		// screen-graphic block further down.
+		statusTextX = cameraTextX;
+		statusTextY = cameraTextY;
+
+		// Eight dial/web/dir/camera label SrcDestRectPairs across three on-screen
+		// columns (dest x = 437 / 492 / 551), each with alternate glyph variants;
+		// take one pair per column for the dial / web / dir labels.
+		readRect(*chunkStream, dialLabel.srcRect);	// column 1 (x=437)
+		readRect(*chunkStream, dialLabel.destRect);
+		chunkStream->skip(32);						// second x=437 variant
+		readRect(*chunkStream, webLabel.srcRect);	// column 2 (x=492)
+		readRect(*chunkStream, webLabel.destRect);
+		readRect(*chunkStream, dirLabel.srcRect);	// column 3 (x=551)
+		readRect(*chunkStream, dirLabel.destRect);
+		chunkStream->skip(4 * 32);					// remaining variants
+	} else {
+		readRect(*chunkStream, dialHilite.srcRect);
+		readRect(*chunkStream, dialHilite.destRect);
+		readRect(*chunkStream, screenOutSrcRect);
+		statusTextX = chunkStream->readSint32LE();
+		statusTextY = chunkStream->readSint32LE();
+		readRect(*chunkStream, welcomeScreen.srcRect);
+		readRect(*chunkStream, welcomeScreen.destRect);
+	}
+
 	for (uint i = 0; i < kNumStatusLabels; ++i) {
 		chunkStream->read(labelBuf, 20);
 		labelBuf[20] = '\0';
 		statusLabels[i] = labelBuf;
 	}
 
-	readRect(*chunkStream, dialLabel.srcRect);
-	readRect(*chunkStream, dialLabel.destRect);
-	readRect(*chunkStream, webLabel.srcRect);
-	readRect(*chunkStream, webLabel.destRect);
-	readRect(*chunkStream, dirLabel.srcRect);
-	readRect(*chunkStream, dirLabel.destRect);
+	if (!isNancy13) {
+		// Nancy 13 reads the dial/web/dir labels in the camera block above.
+		readRect(*chunkStream, dialLabel.srcRect);
+		readRect(*chunkStream, dialLabel.destRect);
+		readRect(*chunkStream, webLabel.srcRect);
+		readRect(*chunkStream, webLabel.destRect);
+		readRect(*chunkStream, dirLabel.srcRect);
+		readRect(*chunkStream, dirLabel.destRect);
+	}
 
-	// Help "?" button widget (3 rects).
-	readRect(*chunkStream, helpButton.srcRectIdle);
-	readRect(*chunkStream, helpButton.srcRectPressed);
-	readRect(*chunkStream, helpButton.destRect);
+	// Help "?" button (3 rects) + its CVTX text key. Nancy 13 orders the key
+	// before the button and adds a second key.
+	if (isNancy13) {
+		readFilename(*chunkStream, helpTextKey);
+		readRect(*chunkStream, helpButton.srcRectIdle);
+		readRect(*chunkStream, helpButton.srcRectPressed);
+		readRect(*chunkStream, helpButton.destRect);
+		readFilename(*chunkStream, helpTextKey2);
+	} else {
+		readRect(*chunkStream, helpButton.srcRectIdle);
+		readRect(*chunkStream, helpButton.srcRectPressed);
+		readRect(*chunkStream, helpButton.destRect);
+		readFilename(*chunkStream, helpTextKey);
+	}
 
-	// Screen-content sprite block
-	readFilename(*chunkStream, helpTextKey);
 	readRect(*chunkStream, signalSpriteSrc);
 	readRect(*chunkStream, signalSpriteSrcAlt);
 	readRect(*chunkStream, signalSpriteDest);
 	readRect(*chunkStream, batterySpriteSrc);
 	readRect(*chunkStream, batterySpriteSrcAlt);
 	readRect(*chunkStream, batterySpriteDest);
-	readRect(*chunkStream, typeMessage.srcRect);
-	readRect(*chunkStream, typeMessage.destRect);
-	readRect(*chunkStream, connectedLabel.srcRect);
-	readRect(*chunkStream, connectedLabel.destRect);
-	readRect(*chunkStream, connectingSpriteSrc);
-	readRect(*chunkStream, connectingSpriteSrcAlt);
-	readRect(*chunkStream, connectingSpriteDest);
-
-	if (g_nancy->getGameType() >= kGameTypeNancy11) {
-		// TODO: Looks to be a new coordinate - values (548, 50)
-		chunkStream->skip(4);
-		chunkStream->skip(4);
+
+	if (isNancy13) {
+		// Welcome / idle screen graphic: a normal source variant, a no-signal
+		// source variant, then the on-screen dest (all 171x164). This is what
+		// drawWelcomeScreen blits, and it carries the top-row button
+		// backgrounds; its dest rect also bounds the small-LCD directory list.
+		// Nancy 13 dropped the separate typeMessage / connectedLabel /
+		// connectingSprite fields.
+		readRect(*chunkStream, welcomeScreen.srcRect);
+		chunkStream->skip(16);	// no-signal source variant
+		readRect(*chunkStream, welcomeScreen.destRect);
+		chunkStream->skip(8);	// trailing pad
+	} else {
+		readRect(*chunkStream, typeMessage.srcRect);
+		readRect(*chunkStream, typeMessage.destRect);
+		readRect(*chunkStream, connectedLabel.srcRect);
+		readRect(*chunkStream, connectedLabel.destRect);
+		readRect(*chunkStream, connectingSpriteSrc);
+		readRect(*chunkStream, connectingSpriteSrcAlt);
+		readRect(*chunkStream, connectingSpriteDest);
+
+		if (g_nancy->getGameType() >= kGameTypeNancy11) {
+			// TODO: Looks to be a new coordinate - values (548, 50)
+			chunkStream->skip(8);
+		}
+
+		readRect(*chunkStream, onlineHeading.srcRect);
+		readRect(*chunkStream, onlineHeading.destRect);
 	}
 
-	readRect(*chunkStream, onlineHeading.srcRect);
-	readRect(*chunkStream, onlineHeading.destRect);
 	readRect(*chunkStream, fullEmptyScreenSrc);
 	readRect(*chunkStream, emailListContainer);
 	readRect(*chunkStream, dirArrowSrc);
 	readRect(*chunkStream, dirCursorSrc);
-	readRect(*chunkStream, dirHeading.srcRect);
-	readRect(*chunkStream, dirHeading.destRect);
+
+	if (isNancy13) {
+		// Online / directory headings — N13 changed these values, not mapped yet.
+		chunkStream->skip(48);
+	} else {
+		readRect(*chunkStream, dirHeading.srcRect);
+		readRect(*chunkStream, dirHeading.destRect);
+	}
 
 	for (uint i = 0; i < kNumSubButtons; ++i) {
 		readRect(*chunkStream, subButtons[i].srcRectIdle);
@@ -1029,15 +1096,19 @@ UICL::UICL(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
 		readRect(*chunkStream, subButtons[i].destRect);
 	}
 
-	// Heading/icon rect pairs
 	readRect(*chunkStream, searchHeading.srcRect);
 	readRect(*chunkStream, searchHeading.destRect);
 	readRect(*chunkStream, emailIconUnread);
 	readRect(*chunkStream, emailIconSelected);
 	readRect(*chunkStream, emailHeading.srcRect);
 	readRect(*chunkStream, emailHeading.destRect);
-	readRect(*chunkStream, helpHeading.srcRect);
-	readRect(*chunkStream, helpHeading.destRect);
+
+	if (!isNancy13) {
+		// Nancy 13 has no separate help heading in this block.
+		readRect(*chunkStream, helpHeading.srcRect);
+		readRect(*chunkStream, helpHeading.destRect);
+	}
+
 	readRect(*chunkStream, browserHeading.srcRect);
 	readRect(*chunkStream, browserHeading.destRect);
 
@@ -1053,6 +1124,11 @@ UICL::UICL(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
 	initialSearch.flag = chunkStream->readSint16LE();
 	initialSearch.eventFlag = chunkStream->readSint16LE();
 
+	if (isNancy13) {
+		// Three RGB colors for the phone screen, added in Nancy 13.
+		chunkStream->read(screenColors, sizeof(screenColors));
+	}
+
 	fontId1 = chunkStream->readUint16LE();
 	fontId2 = chunkStream->readUint16LE();
 
@@ -1077,6 +1153,18 @@ UICL::UICL(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
 
 		chunkStream->read(c.unknownSuffix, sizeof(c.unknownSuffix));
 	}
+
+	if (isNancy13) {
+		// Trailing captured-picture slot table, added in Nancy 13.
+		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));
+		}
+	}
 }
 
 UICO::UICO(Common::SeekableReadStream *chunkStream) : EngineData(chunkStream) {
diff --git a/engines/nancy/enginedata.h b/engines/nancy/enginedata.h
index 9355488d99c..f3edb1fca3f 100644
--- a/engines/nancy/enginedata.h
+++ b/engines/nancy/enginedata.h
@@ -689,6 +689,26 @@ struct UICL : public EngineData {
 
 	uint16 contactCount = 0;
 	Common::Array<Contact> contacts;
+
+	// 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] = {};
+	};
+
+	Common::Rect cameraViewSrcRect;           // camera viewfinder SRC on the overlay
+	int32 cameraTextX = 0;
+	int32 cameraTextY = 0;
+	Common::Path cameraViewImageName;         // "UI_CellCamView_OVL"
+	Common::Path cameraClickSound;
+	Common::Rect pictureDisplayRect;          // where a captured picture is shown
+	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)
 };
 
 // New conversation popup UI (the text strip that appears above the taskbar


Commit: ce70c890a6d8016a97d49a9795614ddbfc3336ac
    https://github.com/scummvm/scummvm/commit/ce70c890a6d8016a97d49a9795614ddbfc3336ac
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-19T05:36:23+03:00

Commit Message:
NANCY: NANCY13: Initial ActionZone changes needed for PachinkoPuzzle

Changed paths:
    engines/nancy/action/actionzone.cpp
    engines/nancy/action/actionzone.h


diff --git a/engines/nancy/action/actionzone.cpp b/engines/nancy/action/actionzone.cpp
index 3764d711d38..c7eb6066ccd 100644
--- a/engines/nancy/action/actionzone.cpp
+++ b/engines/nancy/action/actionzone.cpp
@@ -27,7 +27,7 @@
 namespace Nancy {
 namespace Action {
 
-void ActionZone::readData(Common::SeekableReadStream &stream) {
+void ActionZone::readData(Common::SeekableReadStream &stream, bool isNancy13) {
 	// Base ActionZone (matches the original "Action Zone Boundary OVL" reader).
 	typeField = stream.readSint32LE();
 	type = typeField & 0xFF;
@@ -42,11 +42,21 @@ void ActionZone::readData(Common::SeekableReadStream &stream) {
 	val49 = stream.readSint16LE();
 	val4b = stream.readByte();
 
+	// The Nancy13 base carries an extra int32 here (before the sound block).
+	if (isNancy13) {
+		stream.skip(4);
+	}
+
 	// Random-sound block: count, then that many 33-byte names + 8 bytes of params.
 	_sound.readData(stream);
 
-	// Subtype-specific trailing data. Fields not yet needed are skipped to keep
-	// the stream aligned.
+	readSubtype(stream, isNancy13);
+}
+
+// Subtype-specific trailing data. Fields not yet needed are skipped to keep the stream
+// aligned. The Nancy12 and Nancy13 layouts are identical apart from three subtypes
+// (0x0d / 0x15 / 0x16), which branch on isNancy13.
+void ActionZone::readSubtype(Common::SeekableReadStream &stream, bool isNancy13) {
 	switch (type) {
 	case 1:		// base only
 	case 5:
@@ -72,7 +82,7 @@ void ActionZone::readData(Common::SeekableReadStream &stream) {
 	case 3:
 		stream.skip(8);		// double
 		break;
-	case 0x17:				// Flat Tire (min/max)
+	case 0x17:				// Flat Tire (min/max) - Nancy12 only
 		stream.skip(8);		// int32 + int32
 		break;
 	case 4:
@@ -86,19 +96,30 @@ void ActionZone::readData(Common::SeekableReadStream &stream) {
 		tailId = stream.readSint16LE();
 		tailFlag = stream.readByte();
 		break;
-	case 0x15:				// special effect + a trailing int32 (purpose unconfirmed)
-		readSpecialEffect(stream);
-		stream.skip(4);
+	case 0x15:
+		// Nancy12: special effect + a trailing int32. Nancy13: the flat-tire zone
+		// (min/max int32), with no special effect.
+		if (isNancy13) {
+			stream.skip(8);
+		} else {
+			readSpecialEffect(stream);
+			stream.skip(4);
+		}
 		break;
-	case 0x0d:				// OverlayZone
-		readOverlayZone(stream);
+	case 0x0d:				// OverlayZone (Nancy13 carries one extra int32)
+		readOverlayZone(stream, isNancy13);
 		break;
-	case 0x16:				// OverlayZone + int32
-		readOverlayZone(stream);
-		stream.skip(4);
+	case 0x16:
+		// Nancy12: OverlayZone + int32. Nancy13: a short bumper record (two bytes + int16).
+		if (isNancy13) {
+			stream.skip(4);
+		} else {
+			readOverlayZone(stream, false);
+			stream.skip(4);
+		}
 		break;
 	default:
-		warning("Nancy12 ActionZone: unknown type %d - chunk may desync", type);
+		warning("ActionZone: unknown type %d - chunk may desync", type);
 		break;
 	}
 }
@@ -121,7 +142,7 @@ void ActionZone::readSpecialEffect(Common::SeekableReadStream &stream) {
 	readRect(stream, seRect);
 }
 
-void ActionZone::readOverlayZone(Common::SeekableReadStream &stream) {
+void ActionZone::readOverlayZone(Common::SeekableReadStream &stream, bool isNancy13) {
 	readFilename(stream, overlayName);
 
 	int16 numSrcRects = stream.readSint16LE();
@@ -133,12 +154,15 @@ void ActionZone::readOverlayZone(Common::SeekableReadStream &stream) {
 	}
 
 	readRect(stream, overlayDestRect);
+	if (isNancy13) {
+		stream.skip(4);	// extra int32 vs Nancy12
+	}
 	stream.skip(4);		// int32
 	stream.skip(1);		// byte (loop/play mode)
 	stream.skip(4);		// int32
 }
 
-void readActionZoneArray(Common::SeekableReadStream &stream, Common::Array<ActionZone> &out) {
+void readActionZoneArray(Common::SeekableReadStream &stream, Common::Array<ActionZone> &out, bool isNancy13) {
 	int16 count = stream.readSint16LE();
 	if (count <= 0) {
 		return;
@@ -146,7 +170,7 @@ void readActionZoneArray(Common::SeekableReadStream &stream, Common::Array<Actio
 
 	out.resize(count);
 	for (int i = 0; i < count; ++i) {
-		out[i].readData(stream);
+		out[i].readData(stream, isNancy13);
 	}
 }
 
diff --git a/engines/nancy/action/actionzone.h b/engines/nancy/action/actionzone.h
index 75e32642548..2397ca3b12a 100644
--- a/engines/nancy/action/actionzone.h
+++ b/engines/nancy/action/actionzone.h
@@ -76,15 +76,19 @@ struct ActionZone {
 	Common::Array<Common::Rect> overlaySrcRects;
 	Common::Rect overlayDestRect;
 
-	void readData(Common::SeekableReadStream &stream);
+	// The Nancy13 pinball layout (AR 175) differs from the Nancy12 one: the base carries an
+	// extra int32 before the sound block, and subtypes 0x0d/0x15/0x16 have different trailers.
+	// Pass isNancy13 = true to parse it; the default keeps the Nancy12 behaviour.
+	void readData(Common::SeekableReadStream &stream, bool isNancy13 = false);
 
 private:
 	void readSpecialEffect(Common::SeekableReadStream &stream);
-	void readOverlayZone(Common::SeekableReadStream &stream);
+	void readOverlayZone(Common::SeekableReadStream &stream, bool isNancy13);
+	void readSubtype(Common::SeekableReadStream &stream, bool isNancy13);
 };
 
 // Reads an int16 count, then that many ActionZones.
-void readActionZoneArray(Common::SeekableReadStream &stream, Common::Array<ActionZone> &out);
+void readActionZoneArray(Common::SeekableReadStream &stream, Common::Array<ActionZone> &out, bool isNancy13 = false);
 
 } // End of namespace Action
 } // End of namespace Nancy




More information about the Scummvm-git-logs mailing list