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

fracturehill noreply at scummvm.org
Sat Sep 2 14:26:34 UTC 2023


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

Summary:
a62f7cd7d7 NANCY: Implement TangramPuzzle
e11ec7c8cd DEVTOOLS: Fix broken strings in nancy3 data
13f95f1781 NANCY: Implement SafeLockPuzzle
d520b0c8db NANCY: Fix arrow cursor hotspot


Commit: a62f7cd7d7e840eca041831750c00f0d7e2cb6f5
    https://github.com/scummvm/scummvm/commit/a62f7cd7d7e840eca041831750c00f0d7e2cb6f5
Author: Kaloyan Chehlarski (strahy at outlook.com)
Date: 2023-09-02T17:24:39+03:00

Commit Message:
NANCY: Implement TangramPuzzle

Implemented the puzzle type responsible for nancy3's tile
fitting puzzle.

Changed paths:
  A engines/nancy/action/tangrampuzzle.cpp
  A engines/nancy/action/tangrampuzzle.h
    engines/nancy/action/arfactory.cpp
    engines/nancy/graphics.cpp
    engines/nancy/module.mk


diff --git a/engines/nancy/action/arfactory.cpp b/engines/nancy/action/arfactory.cpp
index e1625dd3183..2ff96a7a24c 100644
--- a/engines/nancy/action/arfactory.cpp
+++ b/engines/nancy/action/arfactory.cpp
@@ -39,6 +39,7 @@
 #include "engines/nancy/action/setplayerclock.h"
 #include "engines/nancy/action/raycastpuzzle.h"
 #include "engines/nancy/action/turningpuzzle.h"
+#include "engines/nancy/action/tangrampuzzle.h"
 
 #include "engines/nancy/state/scene.h"
 
@@ -200,6 +201,8 @@ ActionRecord *ActionManager::createActionRecord(uint16 type) {
 		return new RiddlePuzzle();
 	case 206:
 		return new RaycastPuzzle();
+	case 207:
+		return new TangramPuzzle();
 	case 208:
 		return new OrderingPuzzle(OrderingPuzzle::PuzzleType::kPiano);
 	case 209:
diff --git a/engines/nancy/action/tangrampuzzle.cpp b/engines/nancy/action/tangrampuzzle.cpp
new file mode 100644
index 00000000000..b1395ba7791
--- /dev/null
+++ b/engines/nancy/action/tangrampuzzle.cpp
@@ -0,0 +1,481 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "engines/nancy/util.h"
+#include "engines/nancy/nancy.h"
+#include "engines/nancy/graphics.h"
+#include "engines/nancy/resource.h"
+#include "engines/nancy/sound.h"
+#include "engines/nancy/input.h"
+#include "engines/nancy/puzzledata.h"
+#include "engines/nancy/state/scene.h"
+
+#include "engines/nancy/action/tangrampuzzle.h"
+
+namespace Nancy {
+namespace Action {
+
+TangramPuzzle::~TangramPuzzle() {
+	delete[] _zBuffer;
+}
+
+void TangramPuzzle::init() {
+	Common::Rect screenBounds = NancySceneState.getViewport().getBounds();
+	_drawSurface.create(screenBounds.width(), screenBounds.height(), g_nancy->_graphicsManager->getInputPixelFormat());
+	_drawSurface.clear(g_nancy->_graphicsManager->getTransColor());
+	setTransparent(true);
+	setVisible(true);
+	moveTo(screenBounds);
+
+	g_nancy->_resource->loadImage(_tileImageName, _tileImage);
+	g_nancy->_resource->loadImage(_maskImageName, _maskImage);
+
+	_zBuffer = new byte[_drawSurface.w * _drawSurface.h];
+	memset(_zBuffer, -1, _drawSurface.w * _drawSurface.h);
+
+	_tiles.resize(_tileSrcs.size() + 1);
+
+	// First, add the mask as its own tile for easier handling
+	Tile *curTile = &_tiles[0];
+	curTile->_srcImage.create(_maskImage, _maskImage.getBounds());
+	curTile->_drawSurface.copyFrom(_tiles[0]._srcImage);
+	curTile->_id = 0;
+	curTile->drawMask();
+	curTile->moveTo(_maskImage.getBounds());
+	curTile->setTransparent(true);
+	curTile->setVisible(true);
+	drawToBuffer(*curTile);
+	curTile->setZ(_z + 1);
+
+	// Then, add the actual tiles
+	for (uint i = 0; i < _tileSrcs.size(); ++i) {
+		curTile = &_tiles[i + 1];
+		curTile->_srcImage.create(_tileImage, _tileSrcs[i]);
+		curTile->_drawSurface.copyFrom(curTile->_srcImage);
+		curTile->_id = i + 1;
+		curTile->moveTo(_tileDests[i]);
+		curTile->setTransparent(true);
+		curTile->setVisible(true);
+		curTile->setZ(_z + curTile->_id + 1);
+		curTile->drawMask();
+		drawToBuffer(*curTile);
+
+		// Draw the highlighted tile
+		curTile->_highlightedSrcImage.copyFrom(curTile->_srcImage);
+
+		Graphics::PixelFormat format = curTile->_highlightedSrcImage.format;
+		for (int y = 0; y < curTile->_highlightedSrcImage.h; ++y) {
+			uint16 *p = (uint16 *)curTile->_highlightedSrcImage.getBasePtr(0, y);
+			for (int x = 0; x < curTile->_highlightedSrcImage.w; ++x) {
+				if (*p != g_nancy->_graphicsManager->getTransColor()) {
+					// I'm not sure *3/2 is the exact formula but it's close enough
+					byte r, g, b;
+					format.colorToRGB(*p, r, g, b);
+					r = (byte)((((uint16)r) * 3) >> 1);
+					g = (byte)((((uint16)g) * 3) >> 1);
+					b = (byte)((((uint16)b) * 3) >> 1);
+					*p = (uint16)format.RGBToColor((byte)r, (byte)g, (byte)b);
+				}
+
+				++p;
+			}
+		}
+	}
+
+	registerGraphics();
+}
+
+void TangramPuzzle::registerGraphics() {
+	for (Tile &tile : _tiles) {
+		tile.registerGraphics();
+	}
+
+	RenderActionRecord::registerGraphics();
+}
+
+void TangramPuzzle::readData(Common::SeekableReadStream &stream) {
+	readFilename(stream, _tileImageName);
+	readFilename(stream, _maskImageName);
+
+	stream.skip(2); // Supposedly number of tiles, actually useless
+
+	for (uint i = 0; i < 15; ++i) {
+		Common::Rect src, dest;
+		readRect(stream, src);
+		readRect(stream, dest);
+
+		if ((src.width() == 1 && src.height() == 1) || (dest.width() == 1 && dest.height() == 1)) {
+			continue;
+		}
+
+		_tileSrcs.push_back(src);
+		_tileDests.push_back(dest);
+	}
+
+	readRect(stream, _maskSolveBounds);
+
+	_pickUpSound.readNormal(stream);
+	_putDownSound.readNormal(stream);
+	_rotateSound.readNormal(stream);
+
+	_solveScene.readData(stream);
+	_solveSound.readNormal(stream);
+
+	_exitScene.readData(stream);
+	readRect(stream, _exitHotspot);
+}
+
+void TangramPuzzle::execute() {
+	switch (_state) {
+	case kBegin :
+		init();
+		g_nancy->_sound->loadSound(_pickUpSound);
+		g_nancy->_sound->loadSound(_putDownSound);
+		g_nancy->_sound->loadSound(_rotateSound);
+		_state = kRun;
+		// fall through
+	case kRun :
+		if (_pickedUpTile == -1 && _shouldCheck) {
+			for (int y = 0; y < _maskSolveBounds.height(); ++y) {
+				byte *p = &_zBuffer[(y + _maskSolveBounds.top) * _drawSurface.w + _maskSolveBounds.left];
+				for (int x = 0; x < _maskSolveBounds.width(); ++x) {
+					if (*p == 0) {
+						_shouldCheck = false;
+						return;
+					}
+					++p;
+				}
+			}
+
+			g_nancy->_sound->loadSound(_solveSound);
+			g_nancy->_sound->playSound(_solveSound);
+			_solved = true;
+			_state = kActionTrigger;
+		}
+
+		break;
+	case kActionTrigger :
+		if (_solved) {
+			if (g_nancy->_sound->isSoundPlaying(_solveSound)) {
+				break;
+			}
+
+			_solveScene.execute();
+		} else {
+			_exitScene.execute();
+		}
+
+		g_nancy->_sound->stopSound(_solveSound);
+		g_nancy->_sound->stopSound(_pickUpSound);
+		g_nancy->_sound->stopSound(_putDownSound);
+		g_nancy->_sound->stopSound(_rotateSound);
+
+		finishExecution();
+		
+		break;
+	}
+}
+
+void TangramPuzzle::handleInput(NancyInput &input) {
+	if (_state != kRun) {
+		return;
+	}
+
+	Common::Rect viewport = NancySceneState.getViewport().getScreenPosition();
+
+	if (!viewport.contains(input.mousePos)) {
+		return;
+	}
+
+	Common::Point mousePos = input.mousePos;
+	mousePos.x -= viewport.left;
+	mousePos.y -= viewport.top;
+
+	viewport.moveTo(Common::Point(0, 0));
+
+	if (_pickedUpTile == -1) {
+		// Not holding a tile, check what's under the cursor
+		byte idUnderMouse = _zBuffer[mousePos.y * _drawSurface.w + mousePos.x];
+
+		if (idUnderMouse != 0 && idUnderMouse != (byte)-1) {
+			// A tile is under the cursor
+			g_nancy->_cursorManager->setCursorType(CursorManager::kHotspot);
+
+			if (input.input & NancyInput::kLeftMouseButtonUp) {
+				pickUpTile(idUnderMouse);
+				g_nancy->_sound->playSound(_pickUpSound);
+			} else if (input.input & NancyInput::kRightMouseButtonUp) {
+				rotateTile(idUnderMouse);
+				g_nancy->_sound->playSound(_rotateSound);
+			}
+
+			return;
+		}
+
+		// No tile under cursor, check exit hotspot
+		if (_exitHotspot.contains(mousePos)) {
+			g_nancy->_cursorManager->setCursorType(CursorManager::kExit);
+
+			if (input.input & NancyInput::kLeftMouseButtonUp) {
+				_state = kActionTrigger;
+			}
+
+			return;
+		}
+	} else {
+		// Currently holding a tile
+		Tile &tileHolding = _tiles[_pickedUpTile];
+
+		// Check if we need to place it back down
+		if (input.input & NancyInput::kLeftMouseButtonUp) {
+			putDownTile(_pickedUpTile);
+			g_nancy->_sound->playSound(_putDownSound);
+			return;
+		}
+
+		// Move the tile under the cursor
+		Common::Rect newScreenPos = tileHolding._screenPosition;
+		newScreenPos.moveTo(mousePos);
+		newScreenPos.translate(-newScreenPos.width() / 2, -newScreenPos.height() / 2);
+
+		// Clip movement so the ring stays entirely inside the viewport
+		if (newScreenPos.left < viewport.left) {
+			newScreenPos.translate(viewport.left - newScreenPos.left, 0);
+		} else if (newScreenPos.right > viewport.right) {
+			newScreenPos.translate(viewport.right - newScreenPos.right, 0);
+		}
+
+		if (newScreenPos.top < viewport.top) {
+			newScreenPos.translate(0, viewport.top - newScreenPos.top);
+		} else if (newScreenPos.bottom > viewport.bottom) {
+			newScreenPos.translate(0, viewport.bottom - newScreenPos.bottom);
+		}
+
+		if (newScreenPos != tileHolding._screenPosition) {
+			tileHolding.moveTo(newScreenPos);
+		}
+
+		bool rotated = false;
+
+		// Check if we need to rotate it
+		if (input.input & NancyInput::kRightMouseButtonUp) {
+			rotateTile(_pickedUpTile);
+			g_nancy->_sound->playSound(_rotateSound);
+			rotated = true;
+		}
+
+		if (!rotated) {
+			// Check if we need to highlight, but only if we haven't rotated,
+			// since rotateTile() already checks as well
+			if (checkBuffer(tileHolding) != tileHolding._isHighlighted) {
+				tileHolding.setHighlighted(!tileHolding._isHighlighted);
+			}
+		}
+	}
+}
+
+void TangramPuzzle::drawToBuffer(const Tile &tile, Common::Rect subRect) {
+	if (subRect.isEmpty()) {
+		subRect = tile._screenPosition;
+	}
+
+	uint16 xDiff = subRect.left - tile._screenPosition.left;
+	uint16 yDiff = subRect.top - tile._screenPosition.top;
+
+	for (int y = 0; y < subRect.height(); ++y) {
+		byte *src = &tile._mask[(y + yDiff) * tile._drawSurface.w + xDiff];
+		byte *dest = &_zBuffer[(subRect.top + y) * _drawSurface.w + subRect.left];
+		for (int x = 0; x < subRect.width(); ++x) {
+			if (*src != (byte)-1) {
+				*dest = *src;
+			}
+
+			++src;
+			++dest;
+		}
+	}
+}
+
+void TangramPuzzle::pickUpTile(uint id) {
+	assert(id < _tiles.size() && id != 0);
+
+	Tile &tileToPickUp = _tiles[id];
+
+	moveToTop(id);
+
+	_pickedUpTile = id;
+
+	redrawBuffer(tileToPickUp._screenPosition);
+
+	// Make sure we don't have a frame with the correct zOrder, but wrong position
+	// This is not done when we're calling from rotate()
+	NancyInput input = g_nancy->_input->getInput();
+	input.input = 0;
+	handleInput(input);
+}
+
+void TangramPuzzle::putDownTile(uint id) {
+	Tile &tile = _tiles[id];
+	_pickedUpTile = -1;
+	
+	drawToBuffer(tile);
+
+	if (tile._isHighlighted) {
+		tile.setHighlighted(false);
+	}
+
+	_shouldCheck = true;
+}
+
+void TangramPuzzle::rotateTile(uint id) {
+	assert(id < _tiles.size() && id != 0);
+
+	Tile &tileToRotate = _tiles[id];
+	
+	if (tileToRotate._rotation == 3) {
+		tileToRotate._rotation = 0;
+	} else {
+		++tileToRotate._rotation;
+	}
+
+	moveToTop(id);
+
+	Common::Rect oldPos = tileToRotate._screenPosition;
+
+	if (_pickedUpTile != -1 && checkBuffer(tileToRotate)) {
+		tileToRotate.setHighlighted(true);
+	} else {
+		tileToRotate.setHighlighted(false);
+	}
+	
+	Common::Rect newPos = tileToRotate._drawSurface.getBounds();
+	newPos.moveTo(oldPos.left + oldPos.width() / 2 - newPos.width() / 2, oldPos.top + oldPos.height() / 2 - newPos.height() / 2);
+
+	// Do NOT use moveTo()!
+	// If moved and rotated in the same frame, we need to make sure the last position isn't overwritten
+	tileToRotate._screenPosition = newPos;
+	_needsRedraw = true;
+
+
+	tileToRotate.drawMask();
+	tileToRotate._needsRedraw = true;
+
+	if (_pickedUpTile == -1) {
+		redrawBuffer(oldPos);
+		drawToBuffer(tileToRotate);
+		_shouldCheck = true;
+	}
+}
+
+void TangramPuzzle::moveToTop(uint id) {
+	for (uint i = 1; i < _tiles.size(); ++i) {
+		Tile &tile = _tiles[i];
+		if (tile._z > _tiles[id]._z) {
+			tile.setZ(tile._z - 1);
+			tile.registerGraphics();
+		}
+	}
+	
+	_tiles[id].setZ(_z + _tiles.size());
+	_tiles[id].registerGraphics();
+}
+
+void TangramPuzzle::redrawBuffer(const Common::Rect &rect) {
+	// Redraw the zBuffer for all intersecting pixels, except for the topmost tile
+	for (int y = 0; y < rect.height(); ++y) {
+		byte *dest = &_zBuffer[(y + rect.top) * _drawSurface.w + rect.left];
+		memset(dest, -1, rect.width());
+	}
+
+	for (uint z = _z + 1; z < _z  + _tiles.size(); ++z) {
+		for (uint i = 0; i < _tiles.size() - 1; ++i) {
+			Tile &tile = _tiles[i];
+			if (tile._z == z) {
+				if (tile._screenPosition.intersects(rect)) {
+					drawToBuffer(tile, tile._screenPosition.findIntersectingRect(rect));
+				}
+
+				break;
+			}
+		}
+	}
+}
+
+bool TangramPuzzle::checkBuffer(const Tile &tile) const {
+	// Check if the provided tile has any pixel overlapping with a non-zero in the zBuffer
+	// In other words, this checks if we're placing on a valid empty spot
+	for (int y = 0; y < tile._drawSurface.h; ++y) {
+		const byte *tilePtr = &tile._mask[y * tile._drawSurface.w];
+		const byte *bufPtr = &_zBuffer[(y + tile._screenPosition.top) * _drawSurface.w + tile._screenPosition.left];
+		for (int x = 0; x < tile._drawSurface.w; ++x) {
+			if (*tilePtr != (byte)-1 && *bufPtr != 0) {
+				return false;
+			}
+
+			++tilePtr;
+			++bufPtr;
+		}
+	}
+
+	return true;
+}
+
+TangramPuzzle::Tile::Tile() : RenderObject(1), _mask(nullptr), _id(0), _rotation(0), _isHighlighted(false) {}
+
+TangramPuzzle::Tile::~Tile() {
+	delete _mask;
+}
+
+void TangramPuzzle::Tile::drawMask() {
+	if (!_mask) {
+		_mask = new byte[_drawSurface.w * _drawSurface.h];
+	}
+
+	uint16 transColor = g_nancy->_graphicsManager->getTransColor();
+	for (int y = 0; y < _drawSurface.h; ++y) {
+		uint16 *src = (uint16 *)_drawSurface.getBasePtr(0, y);
+		for (int x = 0; x < _drawSurface.w; ++x) {
+			if (*src == transColor) {
+				_mask[y * _drawSurface.w + x] = -1;
+			} else {
+				_mask[y * _drawSurface.w + x] = _id;
+			}
+			++src;
+		}
+	}
+}
+
+void TangramPuzzle::Tile::setZ(uint z) {
+	_z = z;
+	_needsRedraw = true;
+}
+
+void TangramPuzzle::Tile::setHighlighted(bool highlighted) {
+	_isHighlighted = highlighted;
+	GraphicsManager::rotateBlit(_isHighlighted ? _highlightedSrcImage : _srcImage,
+								_drawSurface,
+								_rotation);
+	setTransparent(true);
+	_needsRedraw = true;
+}
+
+} // End of namespace Action
+} // End of namespace Nancy
diff --git a/engines/nancy/action/tangrampuzzle.h b/engines/nancy/action/tangrampuzzle.h
new file mode 100644
index 00000000000..218533601b2
--- /dev/null
+++ b/engines/nancy/action/tangrampuzzle.h
@@ -0,0 +1,115 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef NANCY_ACTION_TANGRAMPUZZLE_H
+#define NANCY_ACTION_TANGRAMPUZZLE_H
+
+#include "engines/nancy/action/actionrecord.h"
+
+namespace Nancy {
+namespace Action {
+
+// Handles a specific type of puzzle where clicking an object rotates it,
+// as well as several other objects linked to it. Examples are the sun/moon
+// and staircase spindle puzzles in nancy3
+class TangramPuzzle : public RenderActionRecord {
+public:
+	TangramPuzzle() : RenderActionRecord(7) {}
+	virtual ~TangramPuzzle();
+
+	void init() override;
+	void registerGraphics() override;
+
+	void readData(Common::SeekableReadStream &stream) override;
+	void execute() override;
+	void handleInput(NancyInput &input) override;
+
+protected:
+	Common::String getRecordTypeName() const override { return "TangramPuzzle"; }
+	bool isViewportRelative() const override { return true; }
+
+	class Tile : public RenderObject {
+		friend class TangramPuzzle;
+	public:
+		Tile();
+		virtual ~Tile();
+
+		void drawMask();
+		void setZ(uint z);
+		void setHighlighted(bool highlighted);
+
+		Graphics::ManagedSurface _srcImage;
+		Graphics::ManagedSurface _highlightedSrcImage;
+		byte *_mask;
+		byte _id;
+		byte _rotation;
+		bool _isHighlighted;
+	
+	protected:
+		bool isViewportRelative() const override { return true; }
+	};
+
+	void drawToBuffer(const Tile &tile, Common::Rect subRect = Common::Rect());
+
+	void pickUpTile(uint id);
+	void putDownTile(uint id);
+	void rotateTile(uint id);
+
+	void moveToTop(uint id);
+	void redrawBuffer(const Common::Rect &rect);
+
+	bool checkBuffer(const Tile &tile) const;
+
+	Common::String _tileImageName;
+	Common::String _maskImageName;
+
+	Common::Array<Common::Rect> _tileSrcs;
+	Common::Array<Common::Rect> _tileDests;
+
+	Common::Rect _maskSolveBounds;
+
+	SoundDescription _pickUpSound;
+	SoundDescription _putDownSound;
+	SoundDescription _rotateSound;
+
+	SceneChangeWithFlag _solveScene;
+	SoundDescription _solveSound;
+
+	SceneChangeWithFlag _exitScene;
+	Common::Rect _exitHotspot;
+
+	Graphics::ManagedSurface _tileImage;
+	Graphics::ManagedSurface _maskImage;
+	byte *_zBuffer = nullptr;
+
+	Common::Array<Tile> _tiles;
+
+	int16 _pickedUpTile = -1;
+	bool _shouldCheck = false;
+	bool _solved = false;
+
+	uint _pixelAdjustment = 5;
+};
+
+} // End of namespace Action
+} // End of namespace Nancy
+
+#endif // NANCY_ACTION_TANGRAMPUZZLE_H
diff --git a/engines/nancy/graphics.cpp b/engines/nancy/graphics.cpp
index 583c6731e78..bd75e629e72 100644
--- a/engines/nancy/graphics.cpp
+++ b/engines/nancy/graphics.cpp
@@ -130,13 +130,11 @@ void GraphicsManager::loadFonts(Common::SeekableReadStream *chunkStream) {
 }
 
 void GraphicsManager::addObject(RenderObject *object) {
-	for (const auto &r : _objects) {
+	for (auto &r : _objects) {
 		if (r == object) {
-			return;
-		}
-
-		if (r->getZOrder() > object->getZOrder()) {
-			break;
+			// Erase and re-add objects already in the array to make sure
+			// any changes in the z depth are reflected correctly
+			_objects.erase(&r);
 		}
 	}
 
@@ -269,17 +267,29 @@ void GraphicsManager::copyToManaged(void *src, Graphics::ManagedSurface &dst, ui
 }
 
 // Custom rotation code since Surface::rotoscale() produces incorrect results
-// Only works on 16 bit square surfaces with the same size, and ignores transparency
+// Only works on 16 bit surfaces and ignores transparency
 // Rotation is a value between 0 and 3, corresponding to 0, 90, 180, or 270 degrees clockwise
 void GraphicsManager::rotateBlit(const Graphics::ManagedSurface &src, Graphics::ManagedSurface &dest, byte rotation) {
 	assert(!src.empty() && !dest.empty());
-	assert(src.w == src.h && src.h == dest.w && dest.w == dest.h);
 	assert(rotation <= 3);
 	assert(src.format.bytesPerPixel == 2 && dest.format.bytesPerPixel == 2);
 
-	uint size = src.w;
+	uint srcW = src.w;
+	uint srcH = src.h;
 	const uint16 *s, *e;
 
+	if (rotation % 2) {
+		if (src.h != dest.w || src.w != dest.h) {
+			// Dest surface is wrong size, destroy it and create an appropriate one
+			dest.create(src.h, src.w, src.format);
+		}
+	} else {
+		if (src.w != dest.w || src.h != dest.h) {
+			// Dest surface is wrong size, destroy it and create an appropriate one
+			dest.create(src.w, src.h, src.format);
+		}
+	}
+
 	switch (rotation) {
 	case 0 :
 		// No rotation, just blit
@@ -288,10 +298,10 @@ void GraphicsManager::rotateBlit(const Graphics::ManagedSurface &src, Graphics::
 	case 2 : {
 		// 180 degrees
 		uint16 *d;
-		for (uint y = 0; y < size; ++y) {
+		for (uint y = 0; y < srcH; ++y) {
 			s = (const uint16 *)src.getBasePtr(0, y);
-			e = (const uint16 *)src.getBasePtr(size - 1, y);
-			d = (uint16 *)dest.getBasePtr(size - 1, size - y - 1);
+			e = (const uint16 *)src.getBasePtr(srcW, y);
+			d = (uint16 *)dest.getBasePtr(srcW - 1, srcH - y - 1);
 			for (; s < e; ++s, --d) {
 				*d = *s;
 			}
@@ -301,22 +311,20 @@ void GraphicsManager::rotateBlit(const Graphics::ManagedSurface &src, Graphics::
 	}
 	case 1 :
 		// 90 degrees
-		for (uint y = 0; y < size; ++y) {
+		for (uint y = 0; y < srcH; ++y) {
 			s = (const uint16 *)src.getBasePtr(0, y);
-			e = (const uint16 *)src.getBasePtr(size - 1, y);
-			for (uint x = 0; x < size; ++x, ++s) {
-				*((uint16 *)dest.getBasePtr(size - y - 1, x)) = *s;
+			for (uint x = 0; x < srcW; ++x, ++s) {
+				*((uint16 *)dest.getBasePtr(srcH - y - 1, x)) = *s;
 			}
 		}
 
 		break;
 	case 3 :
 		// 270 degrees
-		for (uint y = 0; y < size; ++y) {
+		for (uint y = 0; y < srcH; ++y) {
 			s = (const uint16 *)src.getBasePtr(0, y);
-			e = (const uint16 *)src.getBasePtr(size - 1, y);
-			for (uint x = 0; x < size; ++x, ++s) {
-				*((uint16 *)dest.getBasePtr(y, size - x - 1)) = *s;
+			for (uint x = 0; x < srcW; ++x, ++s) {
+				*((uint16 *)dest.getBasePtr(y, srcW - x - 1)) = *s;
 			}
 		}
 
diff --git a/engines/nancy/module.mk b/engines/nancy/module.mk
index 9ce5418defd..adb4c4b572a 100644
--- a/engines/nancy/module.mk
+++ b/engines/nancy/module.mk
@@ -21,6 +21,7 @@ MODULE_OBJS = \
   action/setplayerclock.o \
   action/sliderpuzzle.o \
   action/soundequalizerpuzzle.o \
+  action/tangrampuzzle.o \
   action/towerpuzzle.o \
   action/turningpuzzle.o \
   action/telephone.o \


Commit: e11ec7c8cd378235ab9d3e66b631e11ebdb9014f
    https://github.com/scummvm/scummvm/commit/e11ec7c8cd378235ab9d3e66b631e11ebdb9014f
Author: Kaloyan Chehlarski (strahy at outlook.com)
Date: 2023-09-02T17:24:39+03:00

Commit Message:
DEVTOOLS: Fix broken strings in nancy3 data

Added a bunch of missing commas that were causing
strings to get merged.

Changed paths:
    devtools/create_nancy/nancy3_data.h


diff --git a/devtools/create_nancy/nancy3_data.h b/devtools/create_nancy/nancy3_data.h
index 8c81cfc418d..8a6666fbbae 100644
--- a/devtools/create_nancy/nancy3_data.h
+++ b/devtools/create_nancy/nancy3_data.h
@@ -307,7 +307,7 @@ const Common::Array<Common::Array<const char *>> _nancy3ConditionalDialogueTexts
 	"Have you seen the poem in the Chinese room?<h><n>", // NCM73
 	"What is that small closet in the hallway for?<h><n>", // NCM74
 	"Can you tell me about the Chinese writing system? I seem to come across a lot of Chinese symbols.<h><n>", // NEF30
-	"I found some old papers in the house, plus a page from a phone directory dated 1894.<h><n>" // NEF31
+	"I found some old papers in the house, plus a page from a phone directory dated 1894.<h><n>", // NEF31
 	// 40
 	"Have you ever heard of the Ladies Protection Society?<h><n>", // NEF32
 	"What do you know about 'The Bandit's Treasure'?<h><n>", // NEF33
@@ -317,41 +317,41 @@ const Common::Array<Common::Array<const char *>> _nancy3ConditionalDialogueTexts
 	// 45
 	"Do you know what the words, 'gum bo fu' mean?<h><n>", // NEF40
 	"Do you know where Yerba Buena town is?<h><n>", // NEF41
-	"Do you know anything about 'Valdez'?<h><n>" // NEF44
+	"Do you know anything about 'Valdez'?<h><n>", // NEF44
 	"Abby is very strange. She really is convinced there's a ghost somewhere in the house.<h><n>", // NHG20
 	"I met Rose's handyman, Charlie. He seems nice.<h><n>", // NHG23
 	// 50
 	"Did you know Rose has a resident expert on Victorians?<h><n>", // NHG26
-	"Have you heard about the seance Abby hosted for Rose and me?<h><n>" // NHG29
+	"Have you heard about the seance Abby hosted for Rose and me?<h><n>", // NHG29
 	"Abby faked the seance. She rigged a table with a projector.<h><n>", // NHG33
-	"I just found a hidden attic. I wonder if it has anything to do with all these accidents.<h><n>" // NHG34
-	"There was a fire in the parlor but I put it out in time.<h><n>" // NHG37
+	"I just found a hidden attic. I wonder if it has anything to do with all these accidents.<h><n>", // NHG34
+	"There was a fire in the parlor but I put it out in time.<h><n>", // NHG37
 	// 55
-	"Hannah, do you think Rose could have started the fire to collect the insurance on the house?<h><n>" // NHG38
+	"Hannah, do you think Rose could have started the fire to collect the insurance on the house?<h><n>", // NHG38
 	"This house is full of surprises. I found a secret room in the basement where someone's been living.<h><n>", // NHG39
 	"Louis is up to something. I saw him take a book from the library.<h><n>", // NHG40
 	"Louis's book mentioned that this house was once called 'gum bo fu' in the 1800's.<h><n>", // NHG41
 	"It turns out Charlie is the one living in the basement.<h><n>", // NHG42
 	// 60
 	"I was wondering whether you knew anything about someone named 'E. Valdez'?<h><n>", // NLC50
-	"Have you ever heard of the Great Christmas gold robbery?<h><n>" // NLC51
+	"Have you ever heard of the Great Christmas gold robbery?<h><n>", // NLC51
 	"What kind of antique store do you own?<h><n>", // NLC52
-	"Was this house once a hotel?<h><n>" // NLC53
+	"Was this house once a hotel?<h><n>", // NLC53
 	"Have you seen a paint scraper anywhere?<h><n>", // NLC54
 	// 65
 	"Do you know what a phoenix is?<h><n>", // NLC55
 	"Do you know why the fireplace in the parlor didn't have a screen?<h><n>", // NLC57
 	"Have you found any secret passage ways in this house?<h><n>", // NLC58
-	"Do you think Rose should sell the house?<h><n>" // NLC59
+	"Do you think Rose should sell the house?<h><n>", // NLC59
 	"Do you know what 'gum bo fu' means?<h><n>", // NLC60
 	// 70
 	"Do you know who Lizzie Applegate was?<h><n>", // NLC62
 	"What was the Ladies Protection Society?<h><n>", // NLC65
 	"Have you seen the poem in my room?<h><n>", // NAS54
 	"Do you think Charlie is doing a good job?<h><n>", // NLC67
-	"Did Lizzie ever wear men's clothing?<h><n>" // NLC68
+	"Did Lizzie ever wear men's clothing?<h><n>", // NLC68
 	// 75
-	"Where did Abby find those papers that are in the parlor?<h><n>" // NRM17
+	"Where did Abby find those papers that are in the parlor?<h><n>", // NRM17
 	"How did you find Charlie?<h><n>", // NRM18
 	"How do you know Louis?<h><n>", // NRM19
 	"How did you meet Abby?<h><n>", // NRM20
@@ -360,12 +360,12 @@ const Common::Array<Common::Array<const char *>> _nancy3ConditionalDialogueTexts
 	"Are you missing any papers?<h><n>", // NRM55
 	"Have you heard of someone named Lizzie Applegate?<h><n>", // NRM22
 	"What is Abby planning for tonight?<h><n>", // NRM23
-	"Have you found any rainbow designs in the house?<h><n>" // NRM24
-	"Do you think Charlie is responsible for these accidents?<h><n>" // NRM25
+	"Have you found any rainbow designs in the house?<h><n>", // NRM24
+	"Do you think Charlie is responsible for these accidents?<h><n>", // NRM25
 	// 85
 	"Does Charlie live around here?<h><n>", // NAS67
 	"Do you know why there's a speaker in the air vent?<h><n>", // NRM27
-	"Whose laptop is that in the library?<h><n>" // NRM28
+	"Whose laptop is that in the library?<h><n>", // NRM28
 	"Why are there dead roses in the parlor?<h><n>", // NRM29
 	"Do you know what 'gum bo fu' means?<h><n>", // NLC60
 },
@@ -486,8 +486,8 @@ const Common::Array<Common::Array<const char *>> _nancy3GoodbyeTexts = {
 	"I'll let you get back to what you were doing.<h>", // NAS90
 	"I should get going. Talk to you later.<h>", // NBG90
 	"I'll let you get back to your renovation.<h>", // NCM90
-	"I should get going. Goodbye, Emily.<h>" // NEF90
-	"I should get back to work. Goodbye.<h>" // NHG90
+	"I should get going. Goodbye, Emily.<h>", // NEF90
+	"I should get back to work. Goodbye.<h>", // NHG90
 	"I won't keep you any longer.<h>", // NLC90
 	"Goodbye, Ned.<h>", // NNN90
 	"I can see you're busy - I'll let you go.<h>" // NRG90


Commit: 13f95f178138e54cda26ff42ff7ae71186d4add2
    https://github.com/scummvm/scummvm/commit/13f95f178138e54cda26ff42ff7ae71186d4add2
Author: Kaloyan Chehlarski (strahy at outlook.com)
Date: 2023-09-02T17:24:39+03:00

Commit Message:
NANCY: Implement SafeLockPuzzle

Implemented the puzzle type responsible for safe locks
with a single dial. First used in nancy3.

Changed paths:
  A engines/nancy/action/safelockpuzzle.cpp
  A engines/nancy/action/safelockpuzzle.h
    engines/nancy/action/arfactory.cpp
    engines/nancy/module.mk


diff --git a/engines/nancy/action/arfactory.cpp b/engines/nancy/action/arfactory.cpp
index 2ff96a7a24c..f0cd65855e2 100644
--- a/engines/nancy/action/arfactory.cpp
+++ b/engines/nancy/action/arfactory.cpp
@@ -40,6 +40,7 @@
 #include "engines/nancy/action/raycastpuzzle.h"
 #include "engines/nancy/action/turningpuzzle.h"
 #include "engines/nancy/action/tangrampuzzle.h"
+#include "engines/nancy/action/safelockpuzzle.h"
 
 #include "engines/nancy/state/scene.h"
 
@@ -207,6 +208,8 @@ ActionRecord *ActionManager::createActionRecord(uint16 type) {
 		return new OrderingPuzzle(OrderingPuzzle::PuzzleType::kPiano);
 	case 209:
 		return new TurningPuzzle();
+	case 210:
+		return new SafeLockPuzzle();
 	default:
 		error("Action Record type %i is invalid!", type);
 		return nullptr;
diff --git a/engines/nancy/action/safelockpuzzle.cpp b/engines/nancy/action/safelockpuzzle.cpp
new file mode 100644
index 00000000000..43740a5a6ec
--- /dev/null
+++ b/engines/nancy/action/safelockpuzzle.cpp
@@ -0,0 +1,283 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/random.h"
+
+#include "engines/nancy/nancy.h"
+#include "engines/nancy/resource.h"
+#include "engines/nancy/graphics.h"
+#include "engines/nancy/sound.h"
+#include "engines/nancy/input.h"
+#include "engines/nancy/util.h"
+
+#include "engines/nancy/action/safelockpuzzle.h"
+
+#include "engines/nancy/state/scene.h"
+
+namespace Nancy {
+namespace Action {
+
+void SafeLockPuzzle::init() {
+	g_nancy->_resource->loadImage(_imageName1, _image1);
+	g_nancy->_resource->loadImage(_imageName2, _image2);
+	g_nancy->_resource->loadImage(_resetImageName, _resetImage);
+
+	Common::Rect screenBounds = NancySceneState.getViewport().getBounds();
+	_drawSurface.create(screenBounds.width(), screenBounds.height(), g_nancy->_graphicsManager->getInputPixelFormat());
+	_drawSurface.clear(g_nancy->_graphicsManager->getTransColor());
+	setTransparent(true);
+	setVisible(true);
+	moveTo(screenBounds);
+
+	registerGraphics();
+}
+
+void SafeLockPuzzle::updateGraphics() {
+	if (_animState == kSelect && (_state == kActionTrigger ? _nextAnim - 500 : _nextAnim) < g_nancy->getTotalPlayTime()) {
+		_drawSurface.fillRect(_arrowDest, _drawSurface.getTransparentColor());
+		_animState = kNone;
+		_needsRedraw = true;
+	}
+
+	if (_animState == kSpin && _nextAnim < g_nancy->getTotalPlayTime()) {
+		drawDialFrame(_current * 2);
+		_animState = kNone;
+	}
+
+	if (_animState == kReset && _nextAnim < g_nancy->getTotalPlayTime()) {
+		_animState = kResetAnim;
+		g_nancy->_sound->playSound(_resetSound);
+	}
+
+	if (_animState == kResetAnim) {
+		// Framerate-dependent animation. We're restricting the engine to ~60fps so it shouldn't be too fast
+		_drawSurface.blitFrom(_resetImage, _resetDialSrcs[_current % _resetDialSrcs.size()], _dialDest);
+		++_current;
+		if (_current >= _resetDialSrcs.size() * _resetTurns) {
+			_animState = kNone;
+			_current = 0;
+			drawDialFrame(_current * 2);
+		}
+		_needsRedraw = true;
+	}
+}
+
+void SafeLockPuzzle::readData(Common::SeekableReadStream &stream) {
+	readFilename(stream, _imageName1);
+	readFilename(stream, _imageName2);
+	readFilename(stream, _resetImageName);
+
+	readRect(stream, _dialDest);
+	readRectArray(stream, _dialSrcs, 20);
+
+	readRect(stream, _resetDest);
+	readRect(stream, _resetSrc);
+	readRect(stream, _arrowDest);
+	readRect(stream, _arrowSrc);
+
+	readRectArray(stream, _resetDialSrcs, 10);
+
+	_resetTurns = stream.readUint16LE();
+
+	uint16 solveSize = stream.readUint16LE();
+	_correctSequence.resize(solveSize);
+	for (uint i = 0; i < solveSize; ++i) {
+		_correctSequence[i] = stream.readUint16LE();
+	}
+	stream.skip((10 - solveSize) * 2);
+
+	readRect(stream, _ccwHotspot);
+	readRect(stream, _cwHotspot);
+
+	_spinSound.readNormal(stream);
+	_selectSound.readNormal(stream);
+	_resetSound.readNormal(stream);
+
+	_solveScene.readData(stream);
+	_solveSoundDelay = stream.readUint16LE();
+	_solveSound.readNormal(stream);
+
+	_exitScene.readData(stream);
+	readRect(stream, _exitHotspot);
+}
+
+void SafeLockPuzzle::execute() {
+	switch (_state) {
+	case kBegin :
+		init();
+		g_nancy->_sound->loadSound(_spinSound);
+		g_nancy->_sound->loadSound(_selectSound);
+		g_nancy->_sound->loadSound(_resetSound);
+		_state = kRun;
+		// fall through
+	case kRun :
+		if (!g_nancy->_sound->isSoundPlaying(_selectSound) && g_nancy->getTotalPlayTime() > _nextAnim) {
+			if (_playerSequence == _correctSequence) {
+				_solved = true;
+				_state = kActionTrigger;
+				_nextAnim = g_nancy->getTotalPlayTime() + 1000 * _solveSoundDelay;
+			}
+		}
+
+		break;
+	case kActionTrigger :
+		if (_solved) {
+			if (_nextAnim == 0) {
+				if (g_nancy->_sound->isSoundPlaying(_solveSound)) {
+					break;
+				}
+			} else {
+				if (_nextAnim < g_nancy->getTotalPlayTime()) {
+					g_nancy->_sound->loadSound(_solveSound);
+					g_nancy->_sound->playSound(_solveSound);
+					_nextAnim = 0;
+				}
+				break;
+			}
+
+			_solveScene.execute();
+		} else {
+			_exitScene.execute();
+		}
+
+		g_nancy->_sound->stopSound(_solveSound);
+		g_nancy->_sound->stopSound(_spinSound);
+		g_nancy->_sound->stopSound(_selectSound);
+		g_nancy->_sound->stopSound(_resetSound);
+
+		finishExecution();
+		
+		break;
+	}
+}
+
+void SafeLockPuzzle::handleInput(NancyInput &input) {
+	if (_state != kRun || _playerSequence == _correctSequence) {
+		return;
+	}
+
+	if (NancySceneState.getViewport().convertViewportToScreen(_exitHotspot).contains(input.mousePos)) {
+		g_nancy->_cursorManager->setCursorType(CursorManager::kExit);
+
+		if (input.input & NancyInput::kLeftMouseButtonUp) {
+			_state = kActionTrigger;
+		}
+
+		return;
+	} else if (NancySceneState.getViewport().convertViewportToScreen(_ccwHotspot).contains(input.mousePos)) {
+		g_nancy->_cursorManager->setCursorType(CursorManager::kRotateCCW);
+
+		if (input.input & NancyInput::kLeftMouseButtonUp && _nextAnim < g_nancy->getTotalPlayTime() &&
+				_animState != kReset && _animState != kResetAnim) {
+			if (_current == 0) {
+				_current = _dialSrcs.size() / 2 - 1;
+			} else {
+				--_current;
+			}
+
+			drawDialFrame(_current * 2 + 1);
+			_nextAnim = g_nancy->getTotalPlayTime() + 250; // hardcoded
+
+			g_nancy->_sound->playSound(_spinSound);
+			_animState = kSpin;
+		}
+
+		return;
+	} else if (NancySceneState.getViewport().convertViewportToScreen(_cwHotspot).contains(input.mousePos)) {
+		g_nancy->_cursorManager->setCursorType(CursorManager::kRotateCW);
+
+		if (input.input & NancyInput::kLeftMouseButtonUp && _nextAnim < g_nancy->getTotalPlayTime() &&
+				_animState != kReset && _animState != kResetAnim) {
+			drawDialFrame(_current * 2 + 1);
+			_nextAnim = g_nancy->getTotalPlayTime() + 250; // hardcoded
+
+			if (_current == (_dialSrcs.size() / 2) - 1) {
+				_current = 0;
+			} else {
+				++_current;
+			}
+
+			g_nancy->_sound->playSound(_spinSound);
+			_animState = kSpin;
+		}
+
+		return;
+	}
+
+	if (g_nancy->_sound->isSoundPlaying(_selectSound) || _animState == kReset || _animState == kResetAnim || _nextAnim > g_nancy->getTotalPlayTime()) {
+		return;
+	}
+	
+	if (NancySceneState.getViewport().convertViewportToScreen(_arrowDest).contains(input.mousePos)) {
+		g_nancy->_cursorManager->setCursorType(CursorManager::kHotspot);
+
+		if (input.input & NancyInput::kLeftMouseButtonUp) {
+			g_nancy->_sound->playSound(_selectSound);
+			pushSequence(_current);
+			_drawSurface.blitFrom(_image1, _arrowSrc, _arrowDest);
+			_animState = kSelect;
+			_nextAnim = g_nancy->getTotalPlayTime() + 500; // hardcoded
+			_needsRedraw = true;
+		}
+
+		return;
+	} else if (NancySceneState.getViewport().convertViewportToScreen(_resetDest).contains(input.mousePos)) {
+		g_nancy->_cursorManager->setCursorType(CursorManager::kHotspot);
+
+		if (input.input & NancyInput::kLeftMouseButtonUp) {
+			_drawSurface.blitFrom(_image1, _resetSrc, _resetDest);
+			g_nancy->_sound->playSound(_selectSound);
+			_animState = kReset;
+			_nextAnim = g_nancy->getTotalPlayTime() + 500; // hardcoded
+			_current = 0;
+			_playerSequence.clear();
+			_needsRedraw = true;
+		}
+
+		return;
+	}	
+}
+
+void SafeLockPuzzle::drawDialFrame(uint frame) {
+	debug("%u", frame);
+	if (frame >= _dialSrcs.size() / 2) {
+		_drawSurface.blitFrom(_image2, _dialSrcs[frame], _dialDest);
+	} else {
+		_drawSurface.blitFrom(_image1, _dialSrcs[frame], _dialDest);
+	}
+
+	_needsRedraw = true;
+}
+
+void SafeLockPuzzle::pushSequence(uint id) {
+	if (id != 0) {
+		// The ids in the correct sequence are in reverse order
+		id = (_dialSrcs.size() / 2) - id;
+	}
+
+	_playerSequence.push_back(id);
+	if (_playerSequence.size() > _correctSequence.size()) {
+		_playerSequence.erase(_playerSequence.begin());
+	}
+}
+
+} // End of namespace Action
+} // End of namespace Nancy
diff --git a/engines/nancy/action/safelockpuzzle.h b/engines/nancy/action/safelockpuzzle.h
new file mode 100644
index 00000000000..9696e3bd969
--- /dev/null
+++ b/engines/nancy/action/safelockpuzzle.h
@@ -0,0 +1,98 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef NANCY_ACTION_SAFELOCKPUZZLE_H
+#define NANCY_ACTION_SAFELOCKPUZZLE_H
+
+#include "engines/nancy/action/actionrecord.h"
+
+namespace Nancy {
+namespace Action {
+
+// Handles a specific type of puzzle where clicking an object rotates it,
+// as well as several other objects linked to it. Examples are the sun/moon
+// and staircase spindle puzzles in nancy3
+class SafeLockPuzzle : public RenderActionRecord {
+public:
+	SafeLockPuzzle() : RenderActionRecord(7) {}
+	virtual ~SafeLockPuzzle() {}
+
+	void init() override;
+	void updateGraphics() override;
+
+	void readData(Common::SeekableReadStream &stream) override;
+	void execute() override;
+	void handleInput(NancyInput &input) override;
+
+protected:
+	enum AnimState { kNone, kSpin, kSelect, kReset, kResetAnim };
+	Common::String getRecordTypeName() const override { return "SafeLockPuzzle"; }
+	bool isViewportRelative() const override { return true; }
+
+	void drawDialFrame(uint frame);
+	void pushSequence(uint id);
+
+	Common::String _imageName1;
+	Common::String _imageName2;
+	Common::String _resetImageName;
+
+	Common::Rect _dialDest;
+
+	Common::Array<Common::Rect> _dialSrcs;
+
+	Common::Rect _resetDest;
+	Common::Rect _resetSrc;
+	Common::Rect _arrowDest;
+	Common::Rect _arrowSrc;
+
+	Common::Array<Common::Rect> _resetDialSrcs;
+
+	uint16 _resetTurns;
+
+	Common::Array<uint16> _correctSequence;
+
+	Common::Rect _ccwHotspot;
+	Common::Rect _cwHotspot;
+
+	SoundDescription _spinSound;
+	SoundDescription _selectSound;
+	SoundDescription _resetSound;
+
+	SceneChangeWithFlag _solveScene;
+	uint _solveSoundDelay;
+	SoundDescription _solveSound;
+
+	SceneChangeWithFlag _exitScene;
+	Common::Rect _exitHotspot;
+
+	Graphics::ManagedSurface _image1, _image2, _resetImage;
+
+	Common::Array<uint16> _playerSequence;
+	bool _solved = false;
+	AnimState _animState = kNone;
+	uint32 _nextAnim = 0;
+	uint16 _current = 0;
+};
+
+} // End of namespace Action
+} // End of namespace Nancy
+
+#endif // NANCY_ACTION_SAFELOCKPUZZLE_H
diff --git a/engines/nancy/module.mk b/engines/nancy/module.mk
index adb4c4b572a..c89d91a64e2 100644
--- a/engines/nancy/module.mk
+++ b/engines/nancy/module.mk
@@ -16,6 +16,7 @@ MODULE_OBJS = \
   action/riddlepuzzle.o \
   action/rippedletterpuzzle.o \
   action/rotatinglockpuzzle.o \
+  action/safelockpuzzle.o \
   action/secondarymovie.o \
   action/secondaryvideo.o \
   action/setplayerclock.o \


Commit: d520b0c8dbc50b4f69d42117502db77488c08fc5
    https://github.com/scummvm/scummvm/commit/d520b0c8dbc50b4f69d42117502db77488c08fc5
Author: Kaloyan Chehlarski (strahy at outlook.com)
Date: 2023-09-02T17:24:39+03:00

Commit Message:
NANCY: Fix arrow cursor hotspot

Added a manual fix for the Nancy games' awful hotspot
position for the arrow cursor.

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


diff --git a/engines/nancy/cursor.cpp b/engines/nancy/cursor.cpp
index adc3acf3b1a..884be28ab30 100644
--- a/engines/nancy/cursor.cpp
+++ b/engines/nancy/cursor.cpp
@@ -76,6 +76,8 @@ void CursorManager::init(Common::SeekableReadStream *chunkStream) {
 
 	_isInitialized = true;
 
+	adjustCursorHotspot();
+
 	delete chunkStream;
 }
 
@@ -239,4 +241,27 @@ void CursorManager::showCursor(bool shouldShow) {
 	CursorMan.showMouse(shouldShow);
 }
 
+void CursorManager::adjustCursorHotspot() {
+	if (g_nancy->getGameType() == kGameTypeVampire) {
+		return;
+	}
+
+	// Improvement: the arrow cursor in the Nancy games has an atrocious hotspot that's
+	// right in the middle of the graphic, instead of in the top left where
+	// it would make sense to be. This function fixes that.
+	// The hotspot is still a few pixels lower than it should be to account
+	// for the different graphic when hovering UI elements
+
+	// TODO: Make this optional?
+
+	uint startID = _curCursorID;
+
+	setCursorType(kNormalArrow);
+	_cursors[_curCursorID].hotspot = {3, 4};
+	setCursorType(kHotspotArrow);
+	_cursors[_curCursorID].hotspot = {3, 4};
+
+	_curCursorID = startID;
+}
+
 } // End of namespace Nancy
diff --git a/engines/nancy/cursor.h b/engines/nancy/cursor.h
index 1a22475af3e..f73ed10c225 100644
--- a/engines/nancy/cursor.h
+++ b/engines/nancy/cursor.h
@@ -76,6 +76,8 @@ public:
 private:
 	void showCursor(bool shouldShow);
 
+	void adjustCursorHotspot();
+
 	struct Cursor {
 		Common::Rect bounds;
 		Common::Point hotspot;




More information about the Scummvm-git-logs mailing list