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

bluegr noreply at scummvm.org
Tue Aug 4 01:00:38 UTC 2026


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

Summary:
0e966b9b1b NANCY: NANCY12: Fix TurningPuzzle
61ddf21f64 NANCY: NANCY12: Implement new functionality for SafeDialPuzzle
abf147b0d1 NANCY: NANCY9: Fixes for OneBuildPuzzle


Commit: 0e966b9b1bed8e8d98fc70d04944df9794e83701
    https://github.com/scummvm/scummvm/commit/0e966b9b1bed8e8d98fc70d04944df9794e83701
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-08-04T04:00:21+03:00

Commit Message:
NANCY: NANCY12: Fix TurningPuzzle

The 3 new bytes in Nancy12 are placed before the spindle data, not
afterwards. Fixes the Coach House door puzzle.

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


diff --git a/engines/nancy/action/puzzle/turningpuzzle.cpp b/engines/nancy/action/puzzle/turningpuzzle.cpp
index a0013c1acdd..53c3c675516 100644
--- a/engines/nancy/action/puzzle/turningpuzzle.cpp
+++ b/engines/nancy/action/puzzle/turningpuzzle.cpp
@@ -366,17 +366,17 @@ void TurningPuzzle::readData(Common::SeekableReadStream &stream) {
 
 	_turnSound.readNormal(stream);
 
+	if (g_nancy->getGameType() >= kGameTypeNancy12) {
+		// Nancy 12 inserts 3 bytes before the correct order; purpose unknown.
+		stream.skip(3);
+	}
+
 	_correctOrder.resize(numSpindles);
 	for (uint i = 0; i < numSpindles; ++i) {
 		_correctOrder[i] = stream.readUint16LE();
 	}
 	stream.skip((16 - numSpindles) * 2);
 
-	if (g_nancy->getGameType() >= kGameTypeNancy12) {
-		// Nancy 12 inserts 3 bytes here (zero in the samples seen); purpose unknown.
-		stream.skip(3);
-	}
-
 	_solveScene.readData(stream);
 	_solveSoundDelay = stream.readUint16LE();
 	_solveSound.readNormal(stream);


Commit: 61ddf21f649ae7faa2c8f4b4db7e4a98cade56f4
    https://github.com/scummvm/scummvm/commit/61ddf21f649ae7faa2c8f4b4db7e4a98cade56f4
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-08-04T04:00:22+03:00

Commit Message:
NANCY: NANCY12: Implement new functionality for SafeDialPuzzle

Fixes the alarm clock puzzle

Changed paths:
    engines/nancy/action/puzzle/safedialpuzzle.cpp
    engines/nancy/action/puzzle/safedialpuzzle.h
    engines/nancy/cursor.cpp
    engines/nancy/cursor.h


diff --git a/engines/nancy/action/puzzle/safedialpuzzle.cpp b/engines/nancy/action/puzzle/safedialpuzzle.cpp
index 20de6afa520..435c56a11af 100644
--- a/engines/nancy/action/puzzle/safedialpuzzle.cpp
+++ b/engines/nancy/action/puzzle/safedialpuzzle.cpp
@@ -134,6 +134,10 @@ void SafeDialPuzzle::readData(Common::SeekableReadStream &stream) {
 		Common::Rect temp = _cwHotspot;
 		_cwHotspot = _ccwHotspot;
 		_ccwHotspot = temp;
+	} else if (g_nancy->getGameType() >= kGameTypeNancy12) {
+		// Nancy 12's dial frames are laid out counter-clockwise, so the counter-clockwise
+		// hotspot is the one that steps forwards through them
+		_ccwTurnsForwards = true;
 	}
 
 	_spinSound.readNormal(stream);
@@ -217,48 +221,28 @@ void SafeDialPuzzle::handleInput(NancyInput &input) {
 
 		return;
 	} else if (NancySceneState.getViewport().convertViewportToScreen(_ccwHotspot).contains(input.mousePos)) {
-		if (!_enableWraparound && _current == 0) {
+		if (!canTurnDial(_ccwTurnsForwards)) {
 			return;
 		}
 
-		g_nancy->_cursor->setCursorType(_useMoveArrows ? CursorManager::kMoveLeft : CursorManager::kRotateCCW);
+		g_nancy->_cursor->setCursorType(_useMoveArrows ? CursorManager::kMoveLeft : CursorManager::kDialCCW);
 
 		if (!g_nancy->_sound->isSoundPlaying(_spinSound) && input.input & NancyInput::kLeftMouseButtonUp && _nextAnim < g_nancy->getTotalPlayTime() &&
 				_animState != kReset && _animState != kResetAnim) {
-			if (_current == 0) {
-				_current = _dialSrcs.size() / (1 + _numInbetweens) - 1;
-			} else {
-				--_current;
-			}
-
-			drawDialFrame(_current * (1 + _numInbetweens) + (_numInbetweens ? 1 : 0));
-			_nextAnim = g_nancy->getTotalPlayTime() + (g_nancy->getGameType() == kGameTypeNancy3 ? 250 : 500); // hardcoded
-
-			g_nancy->_sound->playSound(_spinSound);
-			_animState = kSpin;
+			turnDial(_ccwTurnsForwards);
 		}
 
 		return;
 	} else if (NancySceneState.getViewport().convertViewportToScreen(_cwHotspot).contains(input.mousePos)) {
-		if (!_enableWraparound && _current == (_dialSrcs.size() / (1 + _numInbetweens) - 1)) {
+		if (!canTurnDial(!_ccwTurnsForwards)) {
 			return;
 		}
 
-		g_nancy->_cursor->setCursorType(_useMoveArrows ? CursorManager::kMoveRight : CursorManager::kRotateCW);
+		g_nancy->_cursor->setCursorType(_useMoveArrows ? CursorManager::kMoveRight : CursorManager::kDialCW);
 
 		if (!g_nancy->_sound->isSoundPlaying(_spinSound) && input.input & NancyInput::kLeftMouseButtonUp && _nextAnim < g_nancy->getTotalPlayTime() &&
 				_animState != kReset && _animState != kResetAnim) {
-			drawDialFrame(_current * (1 + _numInbetweens) + 1);
-			_nextAnim = g_nancy->getTotalPlayTime() + (g_nancy->getGameType() == kGameTypeNancy3 ? 250 : 500); // hardcoded
-
-			if (_current == (_dialSrcs.size() / (1 + _numInbetweens)) - 1) {
-				_current = 0;
-			} else {
-				++_current;
-			}
-
-			g_nancy->_sound->playSound(_spinSound);
-			_animState = kSpin;
+			turnDial(!_ccwTurnsForwards);
 		}
 
 		return;
@@ -268,8 +252,12 @@ void SafeDialPuzzle::handleInput(NancyInput &input) {
 		return;
 	}
 
+	// The puzzle arrow cursor only exists in Nancy 10 and up
+	CursorManager::CursorType buttonCursor = g_nancy->getGameType() >= kGameTypeNancy10 ?
+		CursorManager::kPuzzleArrow : CursorManager::kHotspot;
+
 	if (NancySceneState.getViewport().convertViewportToScreen(_arrowDest).contains(input.mousePos)) {
-		g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+		g_nancy->_cursor->setCursorType(buttonCursor);
 
 		if (!g_nancy->_sound->isSoundPlaying(_selectSound) && input.input & NancyInput::kLeftMouseButtonUp) {
 			g_nancy->_sound->playSound(_selectSound);
@@ -282,7 +270,7 @@ void SafeDialPuzzle::handleInput(NancyInput &input) {
 
 		return;
 	} else if (NancySceneState.getViewport().convertViewportToScreen(_resetDest).contains(input.mousePos)) {
-		g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+		g_nancy->_cursor->setCursorType(buttonCursor);
 
 		if (!g_nancy->_sound->isSoundPlaying(_resetSound) && input.input & NancyInput::kLeftMouseButtonUp) {
 			_drawSurface.blitFrom(_image1, _resetSrc, _resetDest);
@@ -298,6 +286,30 @@ void SafeDialPuzzle::handleInput(NancyInput &input) {
 	}
 }
 
+bool SafeDialPuzzle::canTurnDial(bool forwards) const {
+	if (_enableWraparound) {
+		return true;
+	}
+
+	return forwards ? _current != numDialPositions() - 1 : _current != 0;
+}
+
+void SafeDialPuzzle::turnDial(bool forwards) {
+	// The in-between frame of a step always belongs to the lower of the two positions
+	if (forwards) {
+		drawDialFrame(_current * (1 + _numInbetweens) + 1);
+		_current = (_current == numDialPositions() - 1) ? 0 : _current + 1;
+	} else {
+		_current = (_current == 0) ? numDialPositions() - 1 : _current - 1;
+		drawDialFrame(_current * (1 + _numInbetweens) + (_numInbetweens ? 1 : 0));
+	}
+
+	_nextAnim = g_nancy->getTotalPlayTime() + (g_nancy->getGameType() == kGameTypeNancy3 ? 250 : 500); // hardcoded
+
+	g_nancy->_sound->playSound(_spinSound);
+	_animState = kSpin;
+}
+
 void SafeDialPuzzle::drawDialFrame(uint frame) {
 	if (frame >= _dialSrcs.size() / 2 && !_imageName2.empty()) {
 		_drawSurface.blitFrom(_image2, _dialSrcs[frame], _dialDest);
@@ -309,9 +321,10 @@ void SafeDialPuzzle::drawDialFrame(uint frame) {
 }
 
 void SafeDialPuzzle::pushSequence(uint id) {
-	if (!_useMoveArrows && id != 0) {
-		// When the puzzle is set to use rotation cursors, the ids in the correct sequence are in reverse order
-		id = (_dialSrcs.size() / (1 + _numInbetweens)) - id;
+	if (!_useMoveArrows && id != 0 && g_nancy->getGameType() < kGameTypeNancy12) {
+		// When the puzzle is set to use rotation cursors, the ids in the correct sequence are in reverse order.
+		// Nancy 12 stores the dial positions themselves.
+		id = numDialPositions() - id;
 	}
 
 	_playerSequence.push_back(id);
diff --git a/engines/nancy/action/puzzle/safedialpuzzle.h b/engines/nancy/action/puzzle/safedialpuzzle.h
index 910bebf8905..7c775fd37b4 100644
--- a/engines/nancy/action/puzzle/safedialpuzzle.h
+++ b/engines/nancy/action/puzzle/safedialpuzzle.h
@@ -49,6 +49,9 @@ protected:
 
 	void drawDialFrame(uint frame);
 	void pushSequence(uint id);
+	uint numDialPositions() const { return _dialSrcs.size() / (1 + _numInbetweens); }
+	bool canTurnDial(bool forwards) const;
+	void turnDial(bool forwards);
 
 	Common::Path _imageName1;
 	Common::Path _imageName2;
@@ -75,6 +78,7 @@ protected:
 	Common::Rect _cwHotspot;
 
 	bool _useMoveArrows = false;
+	bool _ccwTurnsForwards = false;
 
 	SoundDescription _spinSound;
 	SoundDescription _selectSound;
diff --git a/engines/nancy/cursor.cpp b/engines/nancy/cursor.cpp
index e628451c762..24ec422d61d 100644
--- a/engines/nancy/cursor.cpp
+++ b/engines/nancy/cursor.cpp
@@ -177,6 +177,8 @@ uint CursorManager::resolveNancy10CursorID(CursorType type, int16 itemID, bool s
 	case kMoveDown:             return kNewMoveDown;
 	case kRotateCW:             return kNewRotateCW;
 	case kRotateCCW:            return kNewRotateCCW;
+	case kDialCW:               return kNewDialCW;
+	case kDialCCW:              return kNewDialCCW;
 	case kRotateRight:          return kNewRotateRight;
 	case kRotateLeft:           return kNewRotateLeft;
 	case kInvertedRotateRight:  return kNewInvertedRotateRight;
@@ -233,9 +235,11 @@ uint CursorManager::resolveNancy13CursorID(CursorType type, int16 itemID, bool s
 	case kMoveRight:            sysType = kNancy13MoveRight; break;
 	case kRotateCW:
 	case kRotateRight:
+	case kDialCW:
 	case kInvertedRotateRight:  sysType = kNancy13RotateCW; break;
 	case kRotateCCW:
 	case kRotateLeft:
+	case kDialCCW:
 	case kInvertedRotateLeft:   sysType = kNancy13RotateCCW; break;
 	case kDragHand:
 	case kDropHand:             sysType = kNancy13DropHand; break;
@@ -356,9 +360,11 @@ void CursorManager::setCursor(CursorType type, int16 itemID, bool setFromScript,
 		}
 		break;
 	case kRotateCW:
+	case kDialCW:
 		_curCursorID = kRotateCW;
 		return;
 	case kRotateCCW:
+	case kDialCCW:
 		_curCursorID = kRotateCCW;
 		return;
 	default:
diff --git a/engines/nancy/cursor.h b/engines/nancy/cursor.h
index 5f00f938b29..f8824feb022 100644
--- a/engines/nancy/cursor.h
+++ b/engines/nancy/cursor.h
@@ -59,6 +59,8 @@ public:
 		kDragHand				= 23,	// Hand cursor used when dragging an item (Nancy 10+)
 		kDropHand				= 24,	// Drop-hand cursor used while a piece is held over a target (Nancy 10+)
 		kPuzzleArrow			= 25,	// Puzzle arrow cursor shown when hovering a clickable puzzle hotspot (Nancy 10+)
+		kDialCW					= 26,	// Dial turn cursors used by SafeDialPuzzle (Nancy 10+); the older
+		kDialCCW				= 27,	// games use kRotateCW / kRotateCCW instead
 
 		// Cursors in Nancy10 and newer games. The CURS chunk holds 37 system
 		// cursor types in pairs; type T's idle slot is (T*2) and its hotspot
@@ -90,6 +92,8 @@ public:
 		kNewInvertedRotateLeft	= 34,	// Type 17 — Inverted 360 rotation
 		kNewUseHand				= 36,	// Type 18 — Hand used while using items
 		kNewDragHand			= 38,	// Type 19 — Hand used while dragging puzzle pieces (e.g. SortPuzzle pickup action sets this)
+		kNewDialCW				= 41,	// Type 20 hotspot — Dial turn cursors, used by SafeDialPuzzle
+		kNewDialCCW				= 43,	// Type 21 hotspot
 		kNewPuzzleArrow			= 45,	// Type 22 hotspot — Arrow cursor shown when hovering a clickable puzzle hotspot
 		kNewPuzzleSlideUp		= 47,	// Type 23 hotspot — Slide-arrow shown over a movable tile in CollisionPuzzle/TileMovePuzzle
 		kNewPuzzleSlideDown		= 49,	// Type 24 hotspot


Commit: abf147b0d18fe4a61618c9ce215c0ea2c62eca6c
    https://github.com/scummvm/scummvm/commit/abf147b0d18fe4a61618c9ce215c0ea2c62eca6c
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-08-04T04:00:26+03:00

Commit Message:
NANCY: NANCY9: Fixes for OneBuildPuzzle

- Fixes rotated piece rendering in Nancy9 pipe puzzle
- Fixes rotation check in Nancy12 domino puzzle

Changed paths:
    engines/nancy/action/puzzle/onebuildpuzzle.cpp
    engines/nancy/action/puzzle/onebuildpuzzle.h


diff --git a/engines/nancy/action/puzzle/onebuildpuzzle.cpp b/engines/nancy/action/puzzle/onebuildpuzzle.cpp
index 1a5d0bc6603..13a56f960d9 100644
--- a/engines/nancy/action/puzzle/onebuildpuzzle.cpp
+++ b/engines/nancy/action/puzzle/onebuildpuzzle.cpp
@@ -73,8 +73,9 @@ void OneBuildPuzzle::init() {
 		p.rotateSurfaces[0].blitFrom(_image, p.srcRect, Common::Point(0, 0));
 		p.hasSurface[0] = true;
 
-		// Rotations 1-3: only needed if pieces can rotate
-		if (_canRotateAll || p.isPreRotated) {
+		// Rotations 1-3: only needed if pieces can rotate, or if this one doesn't
+		// start upright. Pre-placed pieces never rotate and stay at rotation 0.
+		if ((_canRotateAll || p.defaultRotation != 0) && !p.isPreRotated) {
 			for (int r = 1; r < 4; ++r) {
 				rotateSurface90CW(p.rotateSurfaces[r - 1], p.rotateSurfaces[r]);
 				p.rotateSurfaces[r].setTransparentColor(_drawSurface.getTransparentColor());
@@ -222,8 +223,8 @@ void OneBuildPuzzle::readDataNancy12(Common::SeekableReadStream &stream) {
 		readRect(stream, p.slotRect);
 		readRect(stream, p.homeRect);
 		p.defaultRotation = stream.readByte();
-		// A piece is pre-placed only when this marker is exactly 10.
-		p.isPreRotated = stream.readByte() == 10;
+		p.requiredRotation = stream.readByte();
+		p.isPreRotated = p.requiredRotation == kPrePlacedRotation;
 	}
 
 	// Optional placement-order arrays, each present only when its flag is set.
@@ -235,9 +236,9 @@ void OneBuildPuzzle::readDataNancy12(Common::SeekableReadStream &stream) {
 	}
 
 	if (stream.readByte() != 0) {
-		_legacyPlacementOrder.resize(_numPieces);
+		_preplacedZOrder.resize(_numPieces);
 		for (uint i = 0; i < _numPieces; ++i)
-			_legacyPlacementOrder[i] = stream.readSint16LE();
+			_preplacedZOrder[i] = stream.readSint16LE();
 	}
 }
 
@@ -257,20 +258,19 @@ void OneBuildPuzzle::readData(Common::SeekableReadStream &stream) {
 	stream.skip(6); // rotationMode, zoneHeight, zoneWidth, mouse-clamping flag
 	_slotTolerance = stream.readSint16LE();
 
-	if (isNancy10) {
-		// TODO: purpose of this duplicate placement-order block is unknown.
-		_legacyOrderedFlag = stream.readByte() != 0;
-		_legacyPlacementOrder.resize(20);
-		for (uint i = 0; i < 20; ++i)
-			_legacyPlacementOrder[i] = stream.readSint16LE();
-	}
-
-	_orderedPlacement = stream.readByte();
+	_orderedPlacement = stream.readByte() != 0;
 
 	_placementOrder.resize(20);
 	for (uint i = 0; i < 20; ++i)
 		_placementOrder[i] = stream.readSint16LE();
 
+	if (isNancy10) {
+		stream.readByte(); // Set when the puzzle stacks its pre-placed pieces
+		_preplacedZOrder.resize(20);
+		for (uint i = 0; i < 20; ++i)
+			_preplacedZOrder[i] = stream.readSint16LE();
+	}
+
 	// Nancy 10 piece records add an alternative source rect at the front.
 	const uint pieceSize = isNancy10 ? 66 : 50;
 
@@ -298,7 +298,9 @@ void OneBuildPuzzle::readData(Common::SeekableReadStream &stream) {
 		readRect(stream, p.slotRect);
 		readRect(stream, p.homeRect);
 		p.defaultRotation = stream.readByte();
-		p.isPreRotated = stream.readByte();
+		// Up to Nancy 11 this byte is a plain pre-placed flag, and a piece only
+		// ever fits its slot upright; requiredRotation stays at 0.
+		p.isPreRotated = stream.readByte() != 0;
 	}
 
 	if (isNancy10) {
@@ -486,10 +488,10 @@ void OneBuildPuzzle::handleInput(NancyInput &input) {
 							 piece.gameRect.right  <= slot.right  + _slotTolerance &&
 							 piece.gameRect.bottom <= slot.bottom + _slotTolerance);
 
-			// A piece only fits at its correct (unrotated) orientation; a
+			// A piece only fits at the orientation its slot calls for; a
 			// 180-degree flip keeps the same bounding box, so proximity alone
 			// would accept an upside-down piece.
-			bool rotationOk = (piece.curRotation == 0);
+			bool rotationOk = (piece.curRotation == piece.requiredRotation);
 
 			bool orderOk = !_orderedPlacement ||
 				(_piecesPlaced < (uint16)_placementOrder.size() &&
@@ -648,7 +650,7 @@ void OneBuildPuzzle::updatePieceRender(int pieceIdx) {
 void OneBuildPuzzle::rotatePiece(int pieceIdx) {
 	Piece &p = _pieces[pieceIdx];
 
-	if (!_canRotateAll && !p.isPreRotated)
+	if (p.isPreRotated || (!_canRotateAll && p.defaultRotation == 0))
 		return;
 
 	int oldRot = p.curRotation;
diff --git a/engines/nancy/action/puzzle/onebuildpuzzle.h b/engines/nancy/action/puzzle/onebuildpuzzle.h
index f467ce45e92..8e8e6b9e31a 100644
--- a/engines/nancy/action/puzzle/onebuildpuzzle.h
+++ b/engines/nancy/action/puzzle/onebuildpuzzle.h
@@ -51,6 +51,11 @@ public:
 protected:
 	Common::String getRecordTypeName() const override { return "OneBuildPuzzle"; }
 
+	// Nancy 12 repurposed the piece's trailing flag byte into the rotation the
+	// piece must be in to be accepted, with this value standing in for the
+	// pre-placed flag it used to be.
+	static const uint8 kPrePlacedRotation = 10;
+
 	struct Piece : RenderObject {
 		Piece() : RenderObject(0) {}
 
@@ -60,6 +65,9 @@ protected:
 		Common::Rect slotRect;
 		Common::Rect homeRect;
 		uint8 defaultRotation = 0;
+		// Rotation the piece must be in to be accepted into its slot. Always 0
+		// before Nancy 12, which is why older games only accept upright pieces.
+		uint8 requiredRotation = 0;
 		bool isPreRotated = false;
 
 		// Runtime
@@ -67,7 +75,7 @@ protected:
 		int curRotation = 0;
 		bool placed = false;
 
-		// Rotations 1-3 only built when canRotateAll or isPreRotated
+		// Rotations 1-3 only built when canRotateAll or the piece starts rotated
 		Graphics::ManagedSurface rotateSurfaces[4];
 		bool hasSurface[4] = {};
 
@@ -89,11 +97,11 @@ protected:
 	bool _orderedPlacement = false; // Pieces must be placed in a specific order
 	Common::Array<int16> _placementOrder; // 1-indexed piece IDs in required placement order
 
-	// --- Nancy 10 additions ---
+	// Stacking order of the pieces that start out already placed, 1-indexed.
+	// TODO: not applied yet; pre-placed pieces keep their array order.
+	Common::Array<int16> _preplacedZOrder;
 
-	// TODO: runtime role unknown; parsed for round-trip but not consumed.
-	bool _legacyOrderedFlag = false;
-	Common::Array<int16> _legacyPlacementOrder;
+	// --- Nancy 10 additions ---
 
 	// Filename only (no SoundDescription metadata).
 	Common::String _extraSoundName;




More information about the Scummvm-git-logs mailing list