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

bluegr noreply at scummvm.org
Sun Mar 22 10:42:56 UTC 2026


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

Summary:
6eeae24d16 NANCY: Implement soundmatchpuzzle (whale sound matching) for Nancy 9
b5ad0b7643 DEVTOOLS: Add Nancy 9 patch for missing sound to create_nancy


Commit: 6eeae24d1654de984ffcc3b909565967b45040af
    https://github.com/scummvm/scummvm/commit/6eeae24d1654de984ffcc3b909565967b45040af
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-03-22T12:42:06+02:00

Commit Message:
NANCY: Implement soundmatchpuzzle (whale sound matching) for Nancy 9

This is the whale sound-matching puzzle used in Nancy 9 (Danger on Deception
Island). The player hears a whale call by clicking one of 5 numbered buttons,
then clicks the matching whale image. Correct pairs stay lit. Player wins when
all the required pairs have been matched.

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


diff --git a/engines/nancy/action/puzzle/soundmatchpuzzle.cpp b/engines/nancy/action/puzzle/soundmatchpuzzle.cpp
index b7250489386..46cdac167fb 100644
--- a/engines/nancy/action/puzzle/soundmatchpuzzle.cpp
+++ b/engines/nancy/action/puzzle/soundmatchpuzzle.cpp
@@ -32,34 +32,259 @@
 namespace Nancy {
 namespace Action {
 
+void SoundMatchPuzzle::readData(Common::SeekableReadStream &stream) {
+	readFilename(stream, _imageNameLitButtons);	// 0x000: overlay image (lit buttons)
+
+	// 0x021: feedback sounds (loaded at init, played on whale button clicks)
+	_feedbackSoundWrong.readNormal(stream);	// 0x021: played on incorrect whale click
+	_feedbackSoundRight.readNormal(stream); // 0x052: played on correct whale click
+
+	stream.skip(1);	// 0x083
+
+	_winScene.readData(stream);	// 0x084
+	_winSound.readNormal(stream);	// 0x09d
+	_exitScene.readData(stream);	// 0x0ce
+
+	readRect(stream, _exitHotspot);	// 0x0e7
+
+	_requiredPairs = stream.readUint16LE();	// 0x0f7
+
+	// 0x119: per-button entries (kNumButtons x 0x15c bytes each)
+	// NOTE: The original tangled the sound button and whale button
+	// data together, but we read them into separate structures for clarity.
+	for (int i = 0; i < kNumButtons; ++i) {
+		SoundButtonEntry &soundButton = _soundButtons[i];
+		WhaleButtonEntry &whaleButton = _whaleButtons[i];
+
+		readRect(stream, whaleButton.whaleSrcRect);  // 0x13c
+		readRect(stream, whaleButton.whaleDestRect); // 0x14c
+
+		soundButton.sound.readNormal(stream);    // 0x000
+
+		stream.skip(33);               // 0x031: empty name field
+
+		soundButton.text = stream.readString('\0', 200); // 0x052: whale sound subtitle
+
+		whaleButton.correctSound = stream.readUint16LE(); // 0x11a
+
+		readRect(stream, soundButton.numSrcRect);    // 0x11c
+		readRect(stream, soundButton.numDestRect);   // 0x12c
+	}
+}
+
 void SoundMatchPuzzle::init() {
-	// TODO
+	Common::Rect vpBounds = NancySceneState.getViewport().getBounds();
+	_drawSurface.create(vpBounds.width(), vpBounds.height(),
+	                    g_nancy->_graphics->getInputPixelFormat());
+	_drawSurface.clear(g_nancy->_graphics->getTransColor());
+	setTransparent(true);
+	setVisible(true);
+	moveTo(vpBounds);
+
+	g_nancy->_resource->loadImage(_imageNameLitButtons, _imageLitButtons);
+	_imageLitButtons.setTransparentColor(_drawSurface.getTransparentColor());
+
+	_selectedSoundButton = -1;
+
+	for (int i = 0; i < kNumButtons; ++i) {
+		_soundButtons[i].matched = false;
+		_whaleButtons[i].matched = false;
+	}
+
+	_matchedPairs  = 0;
+	_isExiting     = false;
+	_solveSubState = kIdle;
+
+	redraw();
 }
 
 void SoundMatchPuzzle::execute() {
-	if (_state == kBegin) {
+	switch (_state) {
+	case kBegin:
 		init();
 		registerGraphics();
+
+		// Load feedback sounds so they are ready for playback in handleInput
+		if (_feedbackSoundWrong.name != "NO SOUND")
+			g_nancy->_sound->loadSound(_feedbackSoundWrong);
+		if (_feedbackSoundRight.name != "NO SOUND")
+			g_nancy->_sound->loadSound(_feedbackSoundRight);
+
 		_state = kRun;
-	}
+		// fall through
 
-	// TODO
-	// Stub - move to the winning screen
-	warning("STUB - Nancy 9 Whale sounds puzzle");
-	NancySceneState.setEventFlag(436, g_nancy->_true); // EV_Solved_Whale_Call
-	SceneChangeDescription scene;
-	scene.sceneID = 2936;
-	NancySceneState.resetStateToInit();
-	NancySceneState.changeScene(scene);
-}
+	case kRun:
+		switch (_solveSubState) {
+		case kIdle:
+			break;
 
-void SoundMatchPuzzle::readData(Common::SeekableReadStream &stream) {
-	// TODO
-	stream.skip(stream.size() - stream.pos());
+		case kSoundPlaying:
+			// Per-button (whale call) sound is playing. If it stops naturally
+			// before the player picks a whale, deselect and return to idle.
+			if (!g_nancy->_sound->isSoundPlaying(_soundButtons[_selectedSoundButton].sound)) {
+				g_nancy->_sound->stopSound(_soundButtons[_selectedSoundButton].sound);
+				NancySceneState.getTextbox().clear();
+				_selectedSoundButton = -1;
+				redraw();
+				_solveSubState = kIdle;
+			}
+			break;
+
+		case kCheckMatch:
+			// A whale button was clicked.
+			// Correct: wait for _feedbackSoundRight to stop, then check win.
+			// Wrong:   wait for _feedbackSoundWrong to stop, then back to idle.
+			if (_soundButtons[_selectedSoundButton].matched) {
+				if (_feedbackSoundRight.name == "NO SOUND" ||
+				    !g_nancy->_sound->isSoundPlaying(_feedbackSoundRight)) {
+					if (_matchedPairs >= _requiredPairs) {
+						if (_winSound.name != "NO SOUND") {
+							g_nancy->_sound->loadSound(_winSound);
+							g_nancy->_sound->playSound(_winSound);
+						}
+						_solveSubState = kWinSound;
+					} else {
+						_selectedSoundButton = -1;
+						_solveSubState  = kIdle;
+					}
+				}
+			} else {
+				if (_feedbackSoundWrong.name == "NO SOUND" ||
+				    !g_nancy->_sound->isSoundPlaying(_feedbackSoundWrong)) {
+					_selectedSoundButton = -1;
+					_solveSubState  = kIdle;
+				}
+			}
+			redraw();
+			break;
+
+		case kWinSound:
+			if (_winSound.name == "NO SOUND" ||
+			    !g_nancy->_sound->isSoundPlaying(_winSound)) {
+				g_nancy->_sound->stopSound(_winSound);
+				_state = kActionTrigger;
+			}
+			break;
+		}
+		break;
+
+	case kActionTrigger:
+		g_nancy->_sound->stopSound(_feedbackSoundWrong);
+		g_nancy->_sound->stopSound(_feedbackSoundRight);
+		if (_selectedSoundButton >= 0)
+			g_nancy->_sound->stopSound(_soundButtons[_selectedSoundButton].sound);
+		g_nancy->_sound->stopSound(_winSound);
+		if (_isExiting)
+			_exitScene.execute();
+		else
+			_winScene.execute();
+		finishExecution();
+		break;
+	}
 }
 
 void SoundMatchPuzzle::handleInput(NancyInput &input) {
-	// TODO
+	if (_state != kRun || _matchedPairs >= _requiredPairs)
+		return;
+
+	Common::Rect vpScreen = NancySceneState.getViewport().getScreenPosition();
+	Common::Point mouseVP = input.mousePos - Common::Point(vpScreen.left, vpScreen.top);
+
+	// Numbered button clicks — accepted in idle or while a sound is playing
+	if (_solveSubState == kIdle || _solveSubState == kSoundPlaying) {
+		for (int i = 0; i < kNumButtons; ++i) {
+			if (_soundButtons[i].matched)
+				continue; // already matched; numbered button is inactive
+			if (!_soundButtons[i].numDestRect.contains(mouseVP))
+				continue;
+
+			g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+
+			if (input.input & NancyInput::kLeftMouseButtonUp) {
+				// Stop any currently playing per-button sound
+				if (_selectedSoundButton >= 0)
+					g_nancy->_sound->stopSound(_soundButtons[_selectedSoundButton].sound);
+
+				_selectedSoundButton = i;
+
+				if (_soundButtons[i].sound.name != "NO SOUND") {
+					g_nancy->_sound->loadSound(_soundButtons[i].sound);
+					g_nancy->_sound->playSound(_soundButtons[i].sound);
+				}
+
+				NancySceneState.getTextbox().clear();
+				if (!_soundButtons[i].text.empty())
+					NancySceneState.getTextbox().addTextLine(_soundButtons[i].text);
+
+				redraw();
+				_solveSubState = kSoundPlaying;
+			}
+			return;
+		}
+	}
+
+	// Whale button clicks — only while a numbered button is selected and its sound plays
+	if (_solveSubState == kSoundPlaying) {
+		for (uint16 whaleButton = 0; whaleButton < kNumButtons; ++whaleButton) {
+			if (!_whaleButtons[whaleButton].whaleDestRect.contains(mouseVP))
+				continue;
+
+			g_nancy->_cursor->setCursorType(CursorManager::kHotspot);
+
+			if (input.input & NancyInput::kLeftMouseButtonUp) {
+				// Stop the per-button sound now that the player has made a choice
+				g_nancy->_sound->stopSound(_soundButtons[_selectedSoundButton].sound);
+				NancySceneState.getTextbox().clear();
+
+				uint16 soundButton = _soundButtonIndex[_selectedSoundButton] + 1;
+
+				if (soundButton == _whaleButtons[whaleButton].correctSound && !_whaleButtons[whaleButton].matched) {
+					// Correct whale - match!
+					_soundButtons[_selectedSoundButton].matched = true;
+					_whaleButtons[whaleButton].matched = true;
+					++_matchedPairs;
+					if (_feedbackSoundRight.name != "NO SOUND")
+						g_nancy->_sound->playSound(_feedbackSoundRight);
+				} else {
+					// Wrong whale
+					if (_feedbackSoundWrong.name != "NO SOUND")
+						g_nancy->_sound->playSound(_feedbackSoundWrong);
+				}
+
+				redraw();
+				_solveSubState = kCheckMatch;
+			}
+			return;
+		}
+	}
+
+	if (_exitHotspot.contains(mouseVP)) {
+		g_nancy->_cursor->setCursorType(g_nancy->_cursor->_puzzleExitCursor);
+		if (input.input & NancyInput::kLeftMouseButtonUp) {
+			_isExiting = true;
+			_state = kActionTrigger;
+		}
+	}
+}
+
+void SoundMatchPuzzle::redraw() {
+	_drawSurface.clear(_drawSurface.getTransparentColor());
+
+	for (int i = 0; i < kNumButtons; ++i) {
+		if (_soundButtons[i].matched || i == _selectedSoundButton) {
+			const Common::Rect &dest = _soundButtons[i].numDestRect;
+			_drawSurface.blitFrom(_imageLitButtons, _soundButtons[i].numSrcRect,
+			                      Common::Point(dest.left, dest.top));
+		}
+
+		if (_whaleButtons[i].matched) {
+			const Common::Rect &dest = _whaleButtons[i].whaleDestRect;
+			_drawSurface.blitFrom(_imageLitButtons, _whaleButtons[i].whaleSrcRect,
+			                      Common::Point(dest.left, dest.top));
+		}
+	}
+
+	_needsRedraw = true;
 }
 
 } // End of namespace Action
diff --git a/engines/nancy/action/puzzle/soundmatchpuzzle.h b/engines/nancy/action/puzzle/soundmatchpuzzle.h
index 435d38c6806..8a0cca6aa9b 100644
--- a/engines/nancy/action/puzzle/soundmatchpuzzle.h
+++ b/engines/nancy/action/puzzle/soundmatchpuzzle.h
@@ -23,12 +23,15 @@
 #define NANCY_ACTION_SOUNDMATCHPUZZLE_H
 
 #include "engines/nancy/action/actionrecord.h"
+#include "engines/nancy/commontypes.h"
 
 namespace Nancy {
 namespace Action {
 
-// Whale sound matching puzzle in Nancy 9
-
+// Whale sound-matching puzzle used in Nancy 9 (Danger on Deception Island).
+// The player hears a whale call by clicking one of 5 numbered buttons, then
+// clicks the matching whale image. Correct pairs stay lit. Player wins when
+// all the required pairs have been matched.
 class SoundMatchPuzzle : public RenderActionRecord {
 public:
 	SoundMatchPuzzle() : RenderActionRecord(7) {}
@@ -43,6 +46,72 @@ public:
 protected:
 	Common::String getRecordTypeName() const override { return "SoundMatchPuzzle"; }
 	bool isViewportRelative() const override { return true; }
+
+	// File data
+
+	Common::Path _imageNameLitButtons;
+
+	static const int kNumButtons = 5;
+
+	SoundDescription _feedbackSoundWrong;  // played on incorrect whale click
+	SoundDescription _feedbackSoundRight;  // played on correct whale click
+	SceneChangeWithFlag _winScene;
+	SoundDescription _winSound;            // played when all pairs are matched
+	SceneChangeWithFlag _exitScene;
+
+	Common::Rect _exitHotspot;
+
+	uint16 _requiredPairs = kNumButtons;   // how many matches needed to win
+
+	struct SoundButtonEntry {
+		SoundDescription sound;   // whale call sound
+		Common::String text;      // onomatopoeia shown in the text box
+		Common::Rect numSrcRect;  // overlay src for numbered button lit state
+		Common::Rect numDestRect; // screen dest for numbered button (hotspot)
+		bool matched = false;     // whether the sound button has been correctly paired
+	};
+
+	SoundButtonEntry _soundButtons[kNumButtons];
+
+	struct WhaleButtonEntry {
+		Common::Rect whaleSrcRect;  // overlay src for whale button lit state
+		Common::Rect whaleDestRect; // screen dest for whale button (hotspot)
+		uint16 correctSound = 0;    // index (0..4) of the correct sound button
+		bool matched = false;       // whether the whale button has been correctly paired
+	};
+
+	WhaleButtonEntry _whaleButtons[kNumButtons];
+
+	// Runtime state
+
+	Graphics::ManagedSurface _imageLitButtons;
+
+	int  _selectedSoundButton  = -1;    // currently selected numbered button, -1 = none
+	int  _matchedPairs    = 0;
+	bool _isExiting       = false;
+
+	enum SolveSubState {
+		kIdle         = 0,
+		kCheckMatch   = 1,
+		kSoundPlaying = 2,
+		kWinSound     = 4
+	};
+	SolveSubState _solveSubState = kIdle;
+
+	// NOTE: In the original, the sound button and whale button
+	// data were intertwined in the puzzle data, and the sound
+	// button entries were not stored in order of their actual
+	// button index. This array maps the sound button entries to
+	// their actual button index (0..4) for easier handling at
+	// runtime.
+	// This means that this implementation is only valid for
+	// Nancy 9's specific puzzle data, but that's not a problem
+	// since this puzzle is only used in that game.
+	uint16 _soundButtonIndex[kNumButtons] = { 3, 2, 4, 0, 1 };
+
+	// Internal methods
+
+	void redraw();
 };
 
 } // End of namespace Action


Commit: b5ad0b764324a679aefdeca1c32b5816f3db34df
    https://github.com/scummvm/scummvm/commit/b5ad0b764324a679aefdeca1c32b5816f3db34df
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-03-22T12:42:09+02:00

Commit Message:
DEVTOOLS: Add Nancy 9 patch for missing sound to create_nancy

Taken from https://www.herinteractive.com/2014/04/patch-danger-on-deception-island-clams-missing-and-csound-error/

Changed paths:
  A devtools/create_nancy/files/nancy9/NCP06na.HIS
    devtools/create_nancy/create_nancy.cpp
    devtools/create_nancy/nancy9_data.h
    dists/engine-data/nancy.dat


diff --git a/devtools/create_nancy/create_nancy.cpp b/devtools/create_nancy/create_nancy.cpp
index 2eedccd90e6..b6fad3e20b1 100644
--- a/devtools/create_nancy/create_nancy.cpp
+++ b/devtools/create_nancy/create_nancy.cpp
@@ -323,6 +323,8 @@ int main(int argc, char *argv[]) {
 	WRAPWITHOFFSET(writeGoodbyes(output, _nancy9Goodbyes))
 	WRAPWITHOFFSET(writeRingingTexts(output, _nancy8TelephoneRinging))	// same as 8
 	WRAPWITHOFFSET(writeEventFlagNames(output, _nancy9EventFlagNames))
+	WRAPWITHOFFSET(writePatchFile(output, 10, nancy9PatchSrcFiles, "files/nancy9"))
+	WRAPWITHOFFSET(writePatchAssociations(output, nancy9PatchAssociations))
 
 	// Nancy Drew: The Secret of Shadow Ranch
 	gameOffsets.push_back(output.pos());
diff --git a/devtools/create_nancy/files/nancy9/NCP06na.HIS b/devtools/create_nancy/files/nancy9/NCP06na.HIS
new file mode 100644
index 00000000000..8857481baaa
Binary files /dev/null and b/devtools/create_nancy/files/nancy9/NCP06na.HIS differ
diff --git a/devtools/create_nancy/nancy9_data.h b/devtools/create_nancy/nancy9_data.h
index 05123d8b7a4..37ca18d4071 100644
--- a/devtools/create_nancy/nancy9_data.h
+++ b/devtools/create_nancy/nancy9_data.h
@@ -975,4 +975,14 @@ const Common::Array<const char *> _nancy9EventFlagNames = {
 	"EV_Empty114",
 };
 
+const Common::Array<const char *> nancy9PatchSrcFiles {
+	"NCP06na.HIS"
+};
+
+// Patch notes:
+// - The missing sound file is a patch from the original devs. Should only be enabled in the English version
+const Common::Array<PatchAssociation> nancy9PatchAssociations {
+	{ { "language", "en" }, { "NCP06na.HIS" } }
+};
+
 #endif // NANCY9DATA_H
diff --git a/dists/engine-data/nancy.dat b/dists/engine-data/nancy.dat
index 46c610fb54b..18fea5790e7 100644
Binary files a/dists/engine-data/nancy.dat and b/dists/engine-data/nancy.dat differ




More information about the Scummvm-git-logs mailing list