[Scummvm-git-logs] scummvm master -> 2868a2a1a5f7a9eab5938147771c56f01c2b9cbb
bluegr
noreply at scummvm.org
Thu Jul 30 00:05:21 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:
937e0e6935 NANCY: NANCY10: Add more OneBuildPuzzle functionality for tuning forks
8874b0faee NANCY: NANCY10: Add more functionality for MultiBuildPuzzle
2868a2a1a5 NANCY: Add helper functions for subtitle handling in puzzles
Commit: 937e0e6935f92d90236e678f5295597183155dcd
https://github.com/scummvm/scummvm/commit/937e0e6935f92d90236e678f5295597183155dcd
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-30T03:05:10+03:00
Commit Message:
NANCY: NANCY10: Add more OneBuildPuzzle functionality for tuning forks
- Add placement region
- Use the correct cursor when hovering over pieces
- Allow turning the crank before placing all forks correctly
- Fix caption appearing when it shouldn't
Fix #17030
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 23dc6d00f28..9c35a9c611e 100644
--- a/engines/nancy/action/puzzle/onebuildpuzzle.cpp
+++ b/engines/nancy/action/puzzle/onebuildpuzzle.cpp
@@ -21,6 +21,7 @@
#include "common/random.h"
#include "common/system.h"
+#include "common/config-manager.h"
#include "engines/nancy/nancy.h"
#include "engines/nancy/graphics.h"
@@ -164,6 +165,7 @@ void OneBuildPuzzle::readDataNancy12(Common::SeekableReadStream &stream) {
_animSound1.readNormal(stream); // 0x16d
_animSound2.readNormal(stream); // 0x19e
_hasFinalAnim = !_animRectA.isEmpty();
+ _hasCrank = !_animRectB.isEmpty();
_solveScene.readData(stream); // 0x1cf
_cancelScene.readData(stream); // 0x1e8 (ends the 513-byte blob)
@@ -301,7 +303,11 @@ void OneBuildPuzzle::readData(Common::SeekableReadStream &stream) {
}
if (isNancy10) {
- stream.skip(32); // TODO: 32 post-piece bytes, layout undecoded.
+ // The 32-byte post-piece block holds two rects. The first is a
+ // bad-placement check region (unused here); the second is the region
+ // forks may be dragged onto and released in.
+ stream.skip(16);
+ readRect(stream, _placementZone);
readFilename(stream, _extraSoundName);
readRect(stream, _animRectA);
readRect(stream, _animRectB);
@@ -310,6 +316,7 @@ void OneBuildPuzzle::readData(Common::SeekableReadStream &stream) {
_animSound1.readNormal(stream);
_animSound2.readNormal(stream);
_hasFinalAnim = !_animRectA.isEmpty();
+ _hasCrank = !_animRectB.isEmpty();
}
_pickupSound.readNormal(stream);
@@ -337,7 +344,10 @@ void OneBuildPuzzle::readData(Common::SeekableReadStream &stream) {
_solveScene.readData(stream);
_completionSound.readNormal(stream);
- // Completion caption: AUTOTEXT key if known, else inline text.
+ // Completion caption. Only an AUTOTEXT key produces a textbox caption; the
+ // trailing inline string is a sound subtitle (e.g. "High pitched sound" for
+ // the tuning-fork puzzle), which the original never writes to the textbox.
+ // It is still read to keep the stream aligned.
Common::String completionKey;
char textBuf[200];
readFilename(stream, completionKey);
@@ -345,8 +355,6 @@ void OneBuildPuzzle::readData(Common::SeekableReadStream &stream) {
_completionText.clear();
if (!completionKey.empty() && autotext->texts.contains(completionKey))
_completionText = autotext->texts[completionKey];
- else
- assembleTextLine(textBuf, _completionText, 200);
_cancelScene.readData(stream);
readRect(stream, _exitHotspot);
@@ -378,7 +386,10 @@ void OneBuildPuzzle::execute() {
// Pickup/rotate sound finished; return to idle (piece still dragging)
_solveState = kIdle;
} else if (_correctlyPlaced) {
- checkAllPlaced();
+ // Crank puzzles never solve by placement alone; the player
+ // must turn the crank to finish (see finishCrankTurn()).
+ if (!_hasCrank)
+ checkAllPlaced();
if (!_isSolved)
playGoodPlacementSound();
} else {
@@ -405,7 +416,7 @@ void OneBuildPuzzle::execute() {
// Play completion sound/text, then wait for it to finish
g_nancy->_sound->loadSound(_completionSound);
g_nancy->_sound->playSound(_completionSound);
- if (!_completionText.empty()) {
+ if (!_completionText.empty() && ConfMan.getBool("subtitles")) {
NancySceneState.getTextbox().clear();
NancySceneState.getTextbox().addTextLine(_completionText);
}
@@ -443,23 +454,16 @@ void OneBuildPuzzle::handleInput(NancyInput &input) {
Common::Point mouseVP(input.mousePos.x - vpScreen.left,
input.mousePos.y - vpScreen.top);
- // Post-placement final-animation stage: once all pieces are placed on a
- // puzzle that defines _animRectA, the puzzle waits here until the user
- // clicks the hotspot (e.g. winding a music-box crank, throwing a lever).
- if (_waitingForFinalAnim && _solveState == kIdle) {
- if (_animRectA.contains(mouseVP)) {
- g_nancy->_cursor->setCursorType(CursorManager::kPuzzleArrow);
- if (input.input & NancyInput::kLeftMouseButtonUp)
- startFinalAnimation();
- return;
- }
- // Fall through so the exit hotspot still works while waiting.
- }
-
if (_isDragging) {
// Always update drag position while carrying a piece
updateDragPosition(mouseVP);
- setPieceCursor();
+
+ // The held fork shows the hotspot hand cursor while over the placement
+ // region, and the plain magnifying glass everywhere else.
+ if (_placementZone.isEmpty() || _placementZone.contains(mouseVP))
+ setPieceCursor();
+ else
+ g_nancy->_cursor->setCursorType(CursorManager::kNormal);
if (_solveState != kIdle)
return;
@@ -476,6 +480,11 @@ void OneBuildPuzzle::handleInput(NancyInput &input) {
// Left click while dragging: attempt to place
if (input.input & NancyInput::kLeftMouseButtonUp) {
+ // A fork can only be released inside the contraption region; a
+ // click outside it is ignored and the piece stays on the cursor.
+ if (!_placementZone.isEmpty() && !_placementZone.contains(mouseVP))
+ return;
+
Piece &piece = _pieces[_pickedUpPiece];
Common::Rect slot = piece.slotRect;
@@ -529,6 +538,17 @@ void OneBuildPuzzle::handleInput(NancyInput &input) {
return;
}
+ // Crank hotspot: on puzzles solved by a crank it can be turned at any time
+ // before the puzzle is solved. Turning it plays the winding animation, then
+ // either solves the puzzle or (if the forks aren't all correctly placed)
+ // makes a bad noise so the player can try again. See finishCrankTurn().
+ if (_hasCrank && _solveState == kIdle && _animRectB.contains(mouseVP)) {
+ g_nancy->_cursor->setCursorType(CursorManager::kPuzzleArrow);
+ if (input.input & NancyInput::kLeftMouseButtonUp)
+ startFinalAnimation();
+ return;
+ }
+
// Not dragging: find the topmost piece under the cursor. The hover cursor is
// refreshed even while a drop/placement sound plays (non-idle) so a piece put
// down off-target keeps the piece cursor; only clicks are gated on kIdle.
@@ -616,7 +636,8 @@ void OneBuildPuzzle::readPlacementTexts(Common::SeekableReadStream &stream, Comm
void OneBuildPuzzle::setPieceCursor() {
if (g_nancy->getGameType() >= kGameTypeNancy10)
- g_nancy->_cursor->setCursorType((CursorManager::CursorType)_pieceCursorType, true, false);
+ // The piece hand uses the hotspot variant (blue hand with an outline).
+ g_nancy->_cursor->setCursorType((CursorManager::CursorType)_pieceCursorType, true, true);
else
g_nancy->_cursor->setCursorType(CursorManager::kCustom1);
}
@@ -751,13 +772,6 @@ void OneBuildPuzzle::checkAllPlaced() {
return;
}
- // Puzzles with a post-placement animation (e.g. scene 3637's music-box
- // crank) require the player to click _animRectA before the puzzle solves.
- if (_hasFinalAnim && !_finalAnimDone) {
- _waitingForFinalAnim = true;
- return;
- }
-
_isSolved = true;
_solveState = kTriggerCompletion;
}
@@ -804,7 +818,7 @@ void OneBuildPuzzle::playGoodPlacementSound() {
idx = 0;
g_nancy->_sound->loadSound(_currentSound);
g_nancy->_sound->playSound(_currentSound);
- if (!_goodTexts[idx].empty()) {
+ if (!_goodTexts[idx].empty() && ConfMan.getBool("subtitles")) {
NancySceneState.getTextbox().clear();
NancySceneState.getTextbox().addTextLine(_goodTexts[idx]);
}
@@ -823,7 +837,7 @@ void OneBuildPuzzle::playBadPlacementSound() {
idx = 0;
g_nancy->_sound->loadSound(_currentSound);
g_nancy->_sound->playSound(_currentSound);
- if (!_badTexts[idx].empty()) {
+ if (!_badTexts[idx].empty() && ConfMan.getBool("subtitles")) {
NancySceneState.getTextbox().clear();
NancySceneState.getTextbox().addTextLine(_badTexts[idx]);
}
@@ -832,19 +846,17 @@ void OneBuildPuzzle::playBadPlacementSound() {
}
void OneBuildPuzzle::startFinalAnimation() {
- _finalAnimDone = true; // one-shot guard
- _waitingForFinalAnim = false;
+ _finalAnimDone = true;
_animFrameCounter = 0;
_animRowCounter = 0;
- // Without an animation image to step through, fall straight into completion.
+ // Without an animation image to step through, resolve the crank turn now.
if (_animImage.w == 0) {
if (_animSound1.name != "NO SOUND" && !_animSound1.name.empty()) {
g_nancy->_sound->loadSound(_animSound1);
g_nancy->_sound->playSound(_animSound1);
}
- _isSolved = true;
- _solveState = kTriggerCompletion;
+ finishCrankTurn();
return;
}
@@ -901,10 +913,31 @@ void OneBuildPuzzle::stepFinalAnimation() {
return;
}
- // Animation finished: hide overlay and run the standard completion flow.
+ // Animation finished: solve the puzzle or make the bad noise.
+ finishCrankTurn();
+}
+
+void OneBuildPuzzle::finishCrankTurn() {
_finalAnimOverlay.setVisible(false);
- _isSolved = true;
- _solveState = kTriggerCompletion;
+
+ // checkAllPlaced() sets _isSolved and moves to kTriggerCompletion once every
+ // required fork is in place.
+ checkAllPlaced();
+ if (_isSolved)
+ return;
+
+ // The forks aren't all correctly placed yet: the contraption makes a bad
+ // noise and the player can turn the crank again.
+ if (_animSound2.name != "NO SOUND" && !_animSound2.name.empty()) {
+ g_nancy->_sound->loadSound(_animSound2);
+ g_nancy->_sound->playSound(_animSound2);
+ _currentSound = _animSound2;
+ _timerEnd = g_system->getMillis() + 800;
+ _solveState = kWaitPlaceSound;
+ } else {
+ _solveState = kIdle;
+ }
+ _finalAnimDone = false;
}
// static
diff --git a/engines/nancy/action/puzzle/onebuildpuzzle.h b/engines/nancy/action/puzzle/onebuildpuzzle.h
index d9bde97d369..f467ce45e92 100644
--- a/engines/nancy/action/puzzle/onebuildpuzzle.h
+++ b/engines/nancy/action/puzzle/onebuildpuzzle.h
@@ -109,8 +109,14 @@ protected:
Common::Rect _animRectB;
int16 _animLayout[6] = {}; // cols, framesPerStep, baseX, baseY, spacing, totalRows
SoundDescription _animSound1;
- SoundDescription _animSound2;
- bool _hasFinalAnim = false; // true when _animRectA is non-empty
+ SoundDescription _animSound2; // "bad" noise played when the crank is turned before the puzzle is solved
+ bool _hasFinalAnim = false; // true when _animRectA is non-empty (the animation atlas region)
+ bool _hasCrank = false; // true when _animRectB is non-empty; the puzzle is solved by turning the crank
+
+ // Forks can only be dragged onto / released inside this region (the
+ // contraption area), not the whole viewport. Empty when the puzzle has no
+ // such constraint.
+ Common::Rect _placementZone;
// Nancy12: pieces with an empty home rect start scattered inside this zone.
Common::Rect _scatterZone;
@@ -172,10 +178,6 @@ protected:
uint16 _piecesPlaced = 0; // Number of pieces correctly placed so far
uint32 _timerEnd = 0; // Millisecond timestamp when the current timer expires
- // Final-animation gating: after all pieces are placed on a puzzle that
- // has _animRectA defined, _waitingForFinalAnim is set and the puzzle
- // stalls in kIdle until the user clicks _animRectA.
- bool _waitingForFinalAnim = false;
bool _finalAnimDone = false;
// Final-animation runtime state (matches original `+0xc35`/`+0xc33` per-tick counters).
@@ -225,6 +227,9 @@ protected:
// Final-animation helpers.
void startFinalAnimation();
void stepFinalAnimation();
+ // After a crank turn finishes: solve if every fork is placed, otherwise
+ // play the "bad" noise and let the player try again.
+ void finishCrankTurn();
};
} // End of namespace Action
Commit: 8874b0faeebb88aa41c66717b64ebb4a9fc39269
https://github.com/scummvm/scummvm/commit/8874b0faeebb88aa41c66717b64ebb4a9fc39269
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-30T03:05:11+03:00
Commit Message:
NANCY: NANCY10: Add more functionality for MultiBuildPuzzle
- Fix ingredient closeup position
- Handle exit hotspot when a closeup is shown
- Handle throwing ingredients away
- Check ingredients on scene cancel
Fix items 1-4 of bug #17024
Changed paths:
engines/nancy/action/puzzle/multibuildpuzzle.cpp
engines/nancy/action/puzzle/multibuildpuzzle.h
diff --git a/engines/nancy/action/puzzle/multibuildpuzzle.cpp b/engines/nancy/action/puzzle/multibuildpuzzle.cpp
index ba1743d9832..e1b6aab1dae 100644
--- a/engines/nancy/action/puzzle/multibuildpuzzle.cpp
+++ b/engines/nancy/action/puzzle/multibuildpuzzle.cpp
@@ -33,10 +33,6 @@
namespace Nancy {
namespace Action {
-// Nancy 10 TODOs:
-// - completion-animation playback (data read, never rendered)
-// - _retainState save/restore via a PuzzleData entry
-// - 4th cursor id, 7 Ã 33-byte string slots
void MultiBuildPuzzle::init() {
g_nancy->_resource->loadImage(_primaryImageName, _primaryImage);
_primaryImage.setTransparentColor(_drawSurface.getTransparentColor());
@@ -133,7 +129,8 @@ void MultiBuildPuzzle::readData(Common::SeekableReadStream &stream) {
readRect(stream, _targetZone);
- _allowAltZoneSnap = stream.readByte() != 0;
+ _altZoneSnapMode = stream.readByte();
+ _allowAltZoneSnap = _altZoneSnapMode != 0;
_checkOverlapOnDrop = stream.readByte() != 0;
if (isNancy10) {
@@ -188,9 +185,14 @@ void MultiBuildPuzzle::readData(Common::SeekableReadStream &stream) {
_dropSound.readNormal(stream);
if (isNancy10) {
- _animSound.readNormal(stream);
- stream.skip(7 * 33); // TODO: 7 unknown strings
- stream.skip(2); // TODO: 4th cursor id at the front of the block; role TBD.
+ // "Missed" feedback when an ingredient is thrown away: a sound plus a
+ // caption laid out exactly like the solve caption (33-byte CONVO key +
+ // 200-byte raw fallback).
+ _missedSound.readNormal(stream);
+ readFilename(stream, _missedTextKey);
+ char missedBuf[200];
+ stream.read(missedBuf, 200);
+ assembleTextLine(missedBuf, _missedText, 200);
}
_dragCursorID = stream.readSint16LE();
@@ -220,6 +222,7 @@ void MultiBuildPuzzle::execute() {
g_nancy->_sound->loadSound(_pickupSound);
g_nancy->_sound->loadSound(_dropSound);
g_nancy->_sound->loadSound(_solveSound);
+ g_nancy->_sound->loadSound(_missedSound);
_state = kRun;
// fall through
case kRun:
@@ -303,6 +306,7 @@ void MultiBuildPuzzle::execute() {
g_nancy->_sound->stopSound(_pickupSound);
g_nancy->_sound->stopSound(_dropSound);
g_nancy->_sound->stopSound(_solveSound);
+ g_nancy->_sound->stopSound(_missedSound);
if (_isCancelled) {
NancySceneState.changeScene(_cancelScene._sceneChange);
// Cancel flag is only set if at least one piece was placed (or
@@ -481,8 +485,10 @@ void MultiBuildPuzzle::handleInput(NancyInput &input) {
return;
Common::Rect vpScreen = viewData->screenPosition;
- // Exit hotspots can sit below the viewport (cake mixing).
- if (!_isDragging && _selectedPiece == -1 && !vpScreen.contains(input.mousePos)) {
+ // Exit hotspots can sit below the viewport (cake mixing). They remain
+ // reachable while an ingredient closeup is showing, so the player can back
+ // away at the bottom of the screen to cancel adding it.
+ if (!_isDragging && !vpScreen.contains(input.mousePos)) {
if (!checkExitHotspot(_exitHotspot, _exitCursorID1, input))
checkExitHotspot(_exitHotspot2, _exitCursorID2, input);
return;
@@ -528,25 +534,44 @@ void MultiBuildPuzzle::handleInput(NancyInput &input) {
int placedIdx = _pickedUpPiece;
_pickedUpPiece = -1;
+ const bool isNancy10 = g_nancy->getGameType() >= kGameTypeNancy10;
+ // "Add ingredient" mode (cake mixing): a valid drop just bumps the
+ // ingredient's placement count and returns the piece to the shelf;
+ // there is a single piece per ingredient (no counter-spawn), so the
+ // count is tracked purely by placeCount.
+ const bool addMode = isNancy10 && _altZoneSnapMode == 2;
+
if (validDrop) {
- pp.isPlaced = true;
- const bool isNancy10 = g_nancy->getGameType() >= kGameTypeNancy10;
- if (isNancy10) {
- // Clone placements bump the source piece's counter.
- int srcIdx = (pp.typeIdx >= 0) ? pp.typeIdx : placedIdx;
- if (_pieces[srcIdx].placeCount < 255)
- _pieces[srcIdx].placeCount++;
+ int srcIdx = (pp.typeIdx >= 0) ? pp.typeIdx : placedIdx;
+ if (isNancy10 && _pieces[srcIdx].placeCount < 255)
+ _pieces[srcIdx].placeCount++;
+
+ if (addMode) {
+ // Ingredient goes back to its shelf slot, still pickable.
+ pp.isPlaced = false;
+ pp.gameRect = pp.homeRect;
+ } else {
+ pp.isPlaced = true;
+ g_nancy->_sound->playSound(_dropSound);
+ // Counter pieces respawn at home for unlimited supply.
+ if (pp.counterByte != 0)
+ spawnCounterPiece(placedIdx);
}
- g_nancy->_sound->playSound(_dropSound);
-
- // Counter pieces respawn at home for unlimited supply.
- if (pp.counterByte != 0)
- spawnCounterPiece(placedIdx);
if (isNancy10)
updateSolveFlags();
} else {
+ // Missed: the ingredient was thrown away. Return it to the shelf
+ // and play the "Missed." feedback (sound + caption).
+ pp.isPlaced = false;
pp.gameRect = pp.homeRect;
+ if (isNancy10 && _missedSound.name != "NO SOUND") {
+ g_nancy->_sound->playSound(_missedSound);
+ if (_solveState == kIdle) {
+ _solveState = kWaitTimer;
+ _timerEnd = g_system->getMillis() + 200;
+ }
+ }
}
updatePieceRender(placedIdx);
@@ -570,12 +595,22 @@ void MultiBuildPuzzle::handleInput(NancyInput &input) {
}
if (_selectedPiece != -1) {
+ Piece &pp = _pieces[_selectedPiece];
+
+ // Only the closeup itself is interactive: clicking it picks it up to
+ // drag. Anywhere else the exit hotspots stay live, so the player can back
+ // away at the bottom of the screen to cancel adding the ingredient.
+ if (!pp.gameRect.contains(mouseVP)) {
+ if (!checkExitHotspot(_exitHotspot, _exitCursorID1, input))
+ checkExitHotspot(_exitHotspot2, _exitCursorID2, input);
+ return;
+ }
+
g_nancy->_cursor->setCursorType(dragCursor, true);
if (input.input & NancyInput::kLeftMouseButtonUp) {
int sel = _selectedPiece;
_selectedPiece = -1;
- Piece &pp = _pieces[sel];
_pickedUpPiece = sel;
_isDragging = true;
pp.curRotation = 0;
@@ -650,18 +685,26 @@ void MultiBuildPuzzle::handleInput(NancyInput &input) {
pp.registerGraphics();
if (_hasCloseupImage && !pp.cuSrcRect.isEmpty()) {
- // First click shows the closeup view centred on the piece.
+ // First click shows the closeup view. When the piece carries a
+ // fixed closeup destination (cake mixing), the closeup appears at
+ // that absolute, screen-centred position; otherwise it is centred
+ // on the piece (plant potting).
_selectedPiece = topmost;
const int cuW = pp.cuSrcRect.width();
const int cuH = pp.cuSrcRect.height();
- const int pieceW = pp.rotateSurfaces[pp.curRotation].w;
- const int pieceH = pp.rotateSurfaces[pp.curRotation].h;
- const int centerX = pp.gameRect.left + pieceW / 2;
- const int centerY = pp.gameRect.top + pieceH / 2;
- int cuLeft = centerX - cuW / 2;
- int cuTop = centerY - cuH / 2;
- cuLeft = CLIP<int>(cuLeft, 0, MAX(0, vpScreen.width() - cuW));
- cuTop = CLIP<int>(cuTop, 0, MAX(0, vpScreen.height() - cuH));
+ int cuLeft;
+ int cuTop;
+ if (!pp.placedDstRect.isEmpty()) {
+ cuLeft = pp.placedDstRect.left;
+ cuTop = pp.placedDstRect.top;
+ } else {
+ const int pieceW = pp.rotateSurfaces[pp.curRotation].w;
+ const int pieceH = pp.rotateSurfaces[pp.curRotation].h;
+ cuLeft = pp.gameRect.left + pieceW / 2 - cuW / 2;
+ cuTop = pp.gameRect.top + pieceH / 2 - cuH / 2;
+ cuLeft = CLIP<int>(cuLeft, 0, MAX(0, vpScreen.width() - cuW));
+ cuTop = CLIP<int>(cuTop, 0, MAX(0, vpScreen.height() - cuH));
+ }
pp.gameRect = Common::Rect(cuLeft, cuTop, cuLeft + cuW, cuTop + cuH);
} else {
// Direct drag on first click.
@@ -710,24 +753,54 @@ bool MultiBuildPuzzle::updateSolveFlags() {
}
total += (uint16)(_pieces.size() - _numPieces);
+ // The cancel scene's flag is the "enough ingredients" flag that enables the
+ // BAKE option in the neighbouring scene. It is raised at/above the threshold.
+ // Below the threshold, Nancy 10 leaves it latched (so backing away from the
+ // counter and stepping back up keeps BAKE available), while Nancy 11+ clears
+ // it. Written only on a value change (see member comment).
+ if (_cancelScene._flag.label != kFlagNoLabel) {
+ const bool enough = total >= _requiredPieces;
+ if (enough || g_nancy->getGameType() >= kGameTypeNancy11) {
+ byte want = enough ? _cancelScene._flag.flag
+ : (_cancelScene._flag.flag == g_nancy->_false ? g_nancy->_true : g_nancy->_false);
+ if ((int)want != _minCountFlagLastValue) {
+ NancySceneState.setEventFlag(_cancelScene._flag.label, want);
+ _minCountFlagLastValue = want;
+ }
+ }
+ }
+
if (total < _requiredPieces)
return false;
+ // The solve scene's flag marks an exact recipe match. It is cleared until
+ // every ingredient count matches; only then is it raised to its true value.
+ bool exact = true;
for (uint i = 0; i < _numPieces; ++i) {
- if (_pieces[i].placeCount > 0 && _pieces[i].mustNotPlace > 0)
- return false;
+ if (_pieces[i].placeCount > 0 && _pieces[i].mustNotPlace > 0) {
+ exact = false;
+ break;
+ }
// mustPlace is an exact required count only when non-zero. A zero
// mustPlace means the piece has no count requirement (e.g. cake
// cooking, where the win rule is just "place enough good ingredients
// and no bad ones"); placing it must not fail the check.
- if (_pieces[i].mustPlace > 0 && _pieces[i].placeCount != _pieces[i].mustPlace)
- return false;
+ if (_pieces[i].mustPlace > 0 && _pieces[i].placeCount != _pieces[i].mustPlace) {
+ exact = false;
+ break;
+ }
}
- // Cancel flag is owned by kActionTrigger so it doesn't retrigger per drop
- NancySceneState.setEventFlag(_solveScene._flag);
+ if (_solveScene._flag.label != kFlagNoLabel) {
+ byte want = exact ? _solveScene._flag.flag
+ : (_solveScene._flag.flag == g_nancy->_false ? g_nancy->_true : g_nancy->_false);
+ if ((int)want != _solveFlagLastValue) {
+ NancySceneState.setEventFlag(_solveScene._flag.label, want);
+ _solveFlagLastValue = want;
+ }
+ }
- return true;
+ return exact;
}
void MultiBuildPuzzle::checkIfSolved() {
diff --git a/engines/nancy/action/puzzle/multibuildpuzzle.h b/engines/nancy/action/puzzle/multibuildpuzzle.h
index 1078efadc09..0b368b7a0ec 100644
--- a/engines/nancy/action/puzzle/multibuildpuzzle.h
+++ b/engines/nancy/action/puzzle/multibuildpuzzle.h
@@ -88,12 +88,21 @@ protected:
bool _hasCloseupImage = false;
// Nancy 10 additions
- bool _retainState = false; // TODO: state-persistence not yet wired up
+ // Parsed for correct chunk alignment but not acted upon: no shipping N10/N11
+ // MultiBuild puzzle sets it (cake mixing and cake cooking both have it clear),
+ // so the save/restore of placement counts is deliberately not implemented.
+ bool _retainState = false;
Common::Path _animImageName; // Completion animation sprite sheet
bool _hasAnimImage = false;
Common::Rect _animRect;
int16 _animLayout[4] = {}; // cols / framesPerStep / spacing / totalRows
- SoundDescription _animSound; // Sound played during the animation
+
+ // Played when an ingredient is dropped outside a valid area (thrown away).
+ // The caption uses the CONVO lookup of _missedTextKey, falling back to the
+ // raw _missedText.
+ SoundDescription _missedSound;
+ Common::String _missedTextKey;
+ Common::String _missedText;
uint16 _numPieces = 0;
uint16 _requiredPieces = 0; // Minimum placed pieces (counterByte==0) for solve check
@@ -103,6 +112,7 @@ protected:
int16 _rotHotspotHeight = 0;
int16 _rotHotspotWidth = 0;
bool _allowAltZoneSnap = false; // Allow drop outside target zone if stacking on a moved piece
+ uint8 _altZoneSnapMode = 0; // Raw value: 2 = "add ingredient" mode (cake), count via placeCount, no counter-spawn
bool _checkOverlapOnDrop = false; // Reject drop if it overlaps an already-placed piece
Common::Array<Piece> _pieces;
@@ -134,6 +144,12 @@ protected:
bool _isSolved = false;
bool _isCancelled = false;
+ // Event-flag write tracking: the original re-writes the solve flags on every
+ // drop, but re-writing an unchanged flag can re-trigger scene voice lines, so
+ // we only write on an actual value change.
+ int _minCountFlagLastValue = -1; // last value written to the cancel-scene "enough ingredients" flag
+ int _solveFlagLastValue = -1; // last value written to the solve-scene flag (-1 = never)
+
enum SolveState {
kIdle = 0,
kWaitTimer = 1,
Commit: 2868a2a1a5f7a9eab5938147771c56f01c2b9cbb
https://github.com/scummvm/scummvm/commit/2868a2a1a5f7a9eab5938147771c56f01c2b9cbb
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-30T03:05:12+03:00
Commit Message:
NANCY: Add helper functions for subtitle handling in puzzles
Changed paths:
engines/nancy/action/puzzle/assemblypuzzle.cpp
engines/nancy/action/puzzle/cardgamepuzzle.cpp
engines/nancy/action/puzzle/cardgamepuzzle.h
engines/nancy/action/puzzle/hamradiopuzzle.cpp
engines/nancy/action/puzzle/multibuildpuzzle.cpp
engines/nancy/action/puzzle/onebuildpuzzle.cpp
engines/nancy/action/puzzle/quizpuzzle.cpp
engines/nancy/action/puzzle/quizpuzzle.h
engines/nancy/action/puzzle/riddlepuzzle.cpp
engines/nancy/action/puzzle/sewingmachinepuzzle.cpp
engines/nancy/action/puzzle/sewingmachinepuzzle.h
engines/nancy/action/puzzle/soundmatchpuzzle.cpp
engines/nancy/util.cpp
engines/nancy/util.h
diff --git a/engines/nancy/action/puzzle/assemblypuzzle.cpp b/engines/nancy/action/puzzle/assemblypuzzle.cpp
index c4fd6353294..bc143d3f089 100644
--- a/engines/nancy/action/puzzle/assemblypuzzle.cpp
+++ b/engines/nancy/action/puzzle/assemblypuzzle.cpp
@@ -137,8 +137,7 @@ void AssemblyPuzzle::execute() {
g_nancy->_sound->loadSound(_solveSound);
g_nancy->_sound->playSound(_solveSound);
- NancySceneState.getTextbox().clear();
- NancySceneState.getTextbox().addTextLine(_solveText);
+ showSubtitle(_solveText);
NancySceneState.setEventFlag(_solveScene._flag);
_completed = true;
diff --git a/engines/nancy/action/puzzle/cardgamepuzzle.cpp b/engines/nancy/action/puzzle/cardgamepuzzle.cpp
index f634c710cab..3dfb7cbe745 100644
--- a/engines/nancy/action/puzzle/cardgamepuzzle.cpp
+++ b/engines/nancy/action/puzzle/cardgamepuzzle.cpp
@@ -498,21 +498,8 @@ void CardGamePuzzle::playVoice(const Common::String &name) {
g_nancy->_sound->loadSound(_voiceSound);
g_nancy->_sound->playSound(_voiceSound);
- showSubtitle(name);
-}
-
-// The card-game lines carry no inline caption; look the subtitle up by sound name in the Autotext table.
-void CardGamePuzzle::showSubtitle(const Common::String &soundName) {
- const CVTX *autotext = (const CVTX *)g_nancy->getEngineData("AUTOTEXT");
- if (!autotext) {
- return;
- }
-
- Common::String text = autotext->texts.getValOrDefault(soundName, "");
- if (!text.empty()) {
- NancySceneState.getTextbox().clear();
- NancySceneState.getTextbox().addTextLine(text);
- }
+ // The card-game lines carry no inline caption; look the subtitle up by sound name.
+ showSubtitle(resolveSubtitleText(name));
}
void CardGamePuzzle::init() {
diff --git a/engines/nancy/action/puzzle/cardgamepuzzle.h b/engines/nancy/action/puzzle/cardgamepuzzle.h
index be2ce219dcb..1bda848e24d 100644
--- a/engines/nancy/action/puzzle/cardgamepuzzle.h
+++ b/engines/nancy/action/puzzle/cardgamepuzzle.h
@@ -81,7 +81,6 @@ protected:
// Compare side 1's grid against a pre-move snapshot and start sliding the changed cards.
void startMoveAnimation(const bool beforeGrid[kMaxRows][kMaxCols]);
void playVoice(const Common::String &name); // play a voiced line / SFX on the card-game channel
- void showSubtitle(const Common::String &soundName); // push the line's AUTOTEXT caption to the textbox
Common::Path _imageName;
diff --git a/engines/nancy/action/puzzle/hamradiopuzzle.cpp b/engines/nancy/action/puzzle/hamradiopuzzle.cpp
index 41db1edcc2c..2d2eb10c8fc 100644
--- a/engines/nancy/action/puzzle/hamradiopuzzle.cpp
+++ b/engines/nancy/action/puzzle/hamradiopuzzle.cpp
@@ -174,11 +174,7 @@ void HamRadioPuzzle::CCSound::loadAndPlay() {
g_nancy->_sound->loadSound(sound);
g_nancy->_sound->playSound(sound);
- if (text.size() && ConfMan.getBool("subtitles")) {
- NancySceneState.getTextbox().clear();
- NancySceneState.getTextbox().addTextLine(text);
- NancySceneState.getTextbox().drawTextbox();
- }
+ showSubtitle(text, true);
}
void HamRadioPuzzle::Frequency::readData(Common::SeekableReadStream &stream, uint16 numDigits) {
@@ -285,12 +281,7 @@ void HamRadioPuzzle::execute() {
_curMorseString.clear();
_badLetterSound.loadAndPlay();
} else {
- if (ConfMan.getBool("subtitles")) {
- NancySceneState.getTextbox().clear();
- NancySceneState.getTextbox().addTextLine(_curMorseString);
- NancySceneState.getTextbox().setOverrideFont(3); // Original engine pushes <f3> tag instead
- NancySceneState.getTextbox().drawTextbox();
- }
+ showSubtitle(_curMorseString, true, 3); // Original engine pushes <f3> tag instead
}
break;
diff --git a/engines/nancy/action/puzzle/multibuildpuzzle.cpp b/engines/nancy/action/puzzle/multibuildpuzzle.cpp
index e1b6aab1dae..17031a3378c 100644
--- a/engines/nancy/action/puzzle/multibuildpuzzle.cpp
+++ b/engines/nancy/action/puzzle/multibuildpuzzle.cpp
@@ -567,6 +567,7 @@ void MultiBuildPuzzle::handleInput(NancyInput &input) {
pp.gameRect = pp.homeRect;
if (isNancy10 && _missedSound.name != "NO SOUND") {
g_nancy->_sound->playSound(_missedSound);
+ showSubtitle(resolveSubtitleText(_missedTextKey, _missedText, "CONVO"));
if (_solveState == kIdle) {
_solveState = kWaitTimer;
_timerEnd = g_system->getMillis() + 200;
@@ -834,26 +835,9 @@ void MultiBuildPuzzle::checkIfSolved() {
_isSolved = true;
g_nancy->_sound->playSound(_solveSound);
-
- // Caption: prefer the CONVO lookup of the text key. An empty lookup
- // result means audio-only â keep the textbox empty. Only fall back to
- // the raw _solveText when the key isn't in CONVO.
- Common::String textToShow;
- bool useLookup = false;
- if (!_solveTextKey.empty()) {
- const CVTX *convo = (const CVTX *)g_nancy->getEngineData("CONVO");
- if (convo && convo->texts.contains(_solveTextKey)) {
- textToShow = convo->texts[_solveTextKey];
- useLookup = true;
- }
- }
- if (!useLookup)
- textToShow = _solveText;
- if (!textToShow.empty()) {
- NancySceneState.getTextbox().clear();
- NancySceneState.getTextbox().addTextLine(textToShow);
- }
-
+ // A CONVO key that resolves to an empty string means audio-only; otherwise fall
+ // back to the raw caption when the key isn't in CONVO.
+ showSubtitle(resolveSubtitleText(_solveTextKey, _solveText, "CONVO"));
_solveState = kWaitSolveSound;
}
diff --git a/engines/nancy/action/puzzle/onebuildpuzzle.cpp b/engines/nancy/action/puzzle/onebuildpuzzle.cpp
index 9c35a9c611e..1a5d0bc6603 100644
--- a/engines/nancy/action/puzzle/onebuildpuzzle.cpp
+++ b/engines/nancy/action/puzzle/onebuildpuzzle.cpp
@@ -21,7 +21,6 @@
#include "common/random.h"
#include "common/system.h"
-#include "common/config-manager.h"
#include "engines/nancy/nancy.h"
#include "engines/nancy/graphics.h"
@@ -325,9 +324,6 @@ void OneBuildPuzzle::readData(Common::SeekableReadStream &stream) {
readFilename(stream, _dropAlt1Filename);
readFilename(stream, _dropAlt2Filename);
- const CVTX *autotext = (const CVTX *)g_nancy->getEngineData("AUTOTEXT");
- assert(autotext);
-
_goodPlacementSound.readNormal(stream);
readFilename(stream, _goodAlt1Filename);
readFilename(stream, _goodAlt2Filename);
@@ -352,9 +348,7 @@ void OneBuildPuzzle::readData(Common::SeekableReadStream &stream) {
char textBuf[200];
readFilename(stream, completionKey);
stream.read(textBuf, 200);
- _completionText.clear();
- if (!completionKey.empty() && autotext->texts.contains(completionKey))
- _completionText = autotext->texts[completionKey];
+ _completionText = resolveSubtitleText(completionKey);
_cancelScene.readData(stream);
readRect(stream, _exitHotspot);
@@ -416,10 +410,7 @@ void OneBuildPuzzle::execute() {
// Play completion sound/text, then wait for it to finish
g_nancy->_sound->loadSound(_completionSound);
g_nancy->_sound->playSound(_completionSound);
- if (!_completionText.empty() && ConfMan.getBool("subtitles")) {
- NancySceneState.getTextbox().clear();
- NancySceneState.getTextbox().addTextLine(_completionText);
- }
+ showSubtitle(_completionText);
_solveState = kWaitCompletion;
break;
case kAnimateFinal:
@@ -616,9 +607,6 @@ void OneBuildPuzzle::handleInput(NancyInput &input) {
// --- Internal helpers ---
void OneBuildPuzzle::readPlacementTexts(Common::SeekableReadStream &stream, Common::Array<Common::String> &out) {
- const CVTX *autotext = (const CVTX *)g_nancy->getEngineData("AUTOTEXT");
- assert(autotext);
-
Common::String keys[3];
for (uint i = 0; i < 3; ++i)
readFilename(stream, keys[i]);
@@ -627,10 +615,9 @@ void OneBuildPuzzle::readPlacementTexts(Common::SeekableReadStream &stream, Comm
out.resize(3);
for (uint i = 0; i < 3; ++i) {
stream.read(textBuf, 200);
- if (!keys[i].empty() && autotext->texts.contains(keys[i]))
- out[i] = autotext->texts[keys[i]];
- else
- assembleTextLine(textBuf, out[i], 200);
+ Common::String literal;
+ assembleTextLine(textBuf, literal, 200);
+ out[i] = resolveSubtitleText(keys[i], literal);
}
}
@@ -818,10 +805,7 @@ void OneBuildPuzzle::playGoodPlacementSound() {
idx = 0;
g_nancy->_sound->loadSound(_currentSound);
g_nancy->_sound->playSound(_currentSound);
- if (!_goodTexts[idx].empty() && ConfMan.getBool("subtitles")) {
- NancySceneState.getTextbox().clear();
- NancySceneState.getTextbox().addTextLine(_goodTexts[idx]);
- }
+ showSubtitle(_goodTexts[idx]);
_solveState = kWaitPlaceSound;
_timerEnd = g_system->getMillis() + 1000;
}
@@ -837,10 +821,7 @@ void OneBuildPuzzle::playBadPlacementSound() {
idx = 0;
g_nancy->_sound->loadSound(_currentSound);
g_nancy->_sound->playSound(_currentSound);
- if (!_badTexts[idx].empty() && ConfMan.getBool("subtitles")) {
- NancySceneState.getTextbox().clear();
- NancySceneState.getTextbox().addTextLine(_badTexts[idx]);
- }
+ showSubtitle(_badTexts[idx]);
_solveState = kWaitPlaceSound;
_timerEnd = g_system->getMillis() + 1000;
}
diff --git a/engines/nancy/action/puzzle/quizpuzzle.cpp b/engines/nancy/action/puzzle/quizpuzzle.cpp
index 6ab64370cc9..e86f4dc367d 100644
--- a/engines/nancy/action/puzzle/quizpuzzle.cpp
+++ b/engines/nancy/action/puzzle/quizpuzzle.cpp
@@ -55,30 +55,6 @@ void QuizPuzzle::init() {
RenderActionRecord::init();
}
-Common::String QuizPuzzle::readSubtitle(Common::SeekableReadStream &stream) {
- const CVTX *autotext = (const CVTX *)g_nancy->getEngineData("AUTOTEXT");
- assert(autotext);
-
- Common::String result;
- char textBuf[30];
-
- stream.read(textBuf, 30);
- textBuf[29] = '\0';
- result = textBuf;
-
- if (!result.empty() && autotext->texts.contains(result))
- result = autotext->texts[result];
-
- return result;
-}
-
-void QuizPuzzle::showSubtitle(const Common::String &text) {
- if (!text.empty()) {
- NancySceneState.getTextbox().clear();
- NancySceneState.getTextbox().addTextLine(text);
- }
-}
-
// ---- Nancy 8 data format ----
// Offset Size Field
// 0x000 2 fontID
@@ -114,14 +90,14 @@ void QuizPuzzle::readDataOld(Common::SeekableReadStream &stream) {
}
_correctSound.readNormal(stream);
- _correctText = readSubtitle(stream);
+ _correctText = readSubtitleText(stream);
_wrongSound.readNormal(stream);
- _wrongText = readSubtitle(stream);
+ _wrongText = readSubtitleText(stream);
_solveScene.readData(stream);
_doneSound.readNormal(stream);
- _doneText = readSubtitle(stream);
+ _doneText = readSubtitleText(stream);
_cancelScene.readData(stream);
}
@@ -153,9 +129,6 @@ void QuizPuzzle::readDataOld(Common::SeekableReadStream &stream) {
// +0xB0 2 wrong sound volume
// +0xB2 30 wrong subtitle (skip)
void QuizPuzzle::readDataNew(Common::SeekableReadStream &stream) {
- const CVTX *autotext = (const CVTX *)g_nancy->getEngineData("AUTOTEXT");
- assert(autotext);
-
_fontID = stream.readUint16LE();
_cursorBlinkInterval = stream.readUint16LE();
_cursorChar = stream.readByte();
@@ -167,7 +140,7 @@ void QuizPuzzle::readDataNew(Common::SeekableReadStream &stream) {
_solveScene.readData(stream);
_doneSound.readNormal(stream);
- _doneText = readSubtitle(stream);
+ _doneText = readSubtitleText(stream);
_cancelScene.readData(stream);
readRect(stream, _exitHotspot);
@@ -200,13 +173,13 @@ void QuizPuzzle::readDataNew(Common::SeekableReadStream &stream) {
soundNameBuf[32] = '\0';
_boxCorrectSoundName[i] = soundNameBuf;
_boxCorrectSoundVolume[i] = stream.readUint16LE();
- _boxCorrectText[i] = readSubtitle(stream);
+ _boxCorrectText[i] = readSubtitleText(stream);
stream.read(soundNameBuf, 33);
soundNameBuf[32] = '\0';
_boxWrongSoundName[i] = soundNameBuf;
_boxWrongSoundVolume[i] = stream.readUint16LE();
- _boxWrongText[i] = readSubtitle(stream);
+ _boxWrongText[i] = readSubtitleText(stream);
// Precompute max answer length for auto-check mode
_boxMaxLen[i] = 0;
diff --git a/engines/nancy/action/puzzle/quizpuzzle.h b/engines/nancy/action/puzzle/quizpuzzle.h
index 9d5371ebddb..d95a3d5bf30 100644
--- a/engines/nancy/action/puzzle/quizpuzzle.h
+++ b/engines/nancy/action/puzzle/quizpuzzle.h
@@ -61,9 +61,6 @@ private:
bool checkAllSolved() const;
bool checkAnswerForCurrentBox(); // checks, marks correct, sets event flag
- Common::String readSubtitle(Common::SeekableReadStream &stream);
- void showSubtitle(const Common::String &text);
-
// ---- Data (Nancy 8) ----
uint16 _fontID = 0;
uint16 _cursorBlinkInterval = 500;
diff --git a/engines/nancy/action/puzzle/riddlepuzzle.cpp b/engines/nancy/action/puzzle/riddlepuzzle.cpp
index 76b5a586603..5ad797bae86 100644
--- a/engines/nancy/action/puzzle/riddlepuzzle.cpp
+++ b/engines/nancy/action/puzzle/riddlepuzzle.cpp
@@ -138,9 +138,7 @@ void RiddlePuzzle::execute() {
g_nancy->_sound->loadSound(_riddles[_riddleID].sound);
g_nancy->_sound->playSound(_riddles[_riddleID].sound);
- NancySceneState.getTextbox().clear();
- NancySceneState.getTextbox().setOverrideFont(_textboxTextFontID);
- NancySceneState.getTextbox().addTextLine(_riddles[_riddleID].text);
+ showSubtitle(_riddles[_riddleID].text, false, _textboxTextFontID);
NancySceneState.setNoHeldItem();
_state = kRun;
diff --git a/engines/nancy/action/puzzle/sewingmachinepuzzle.cpp b/engines/nancy/action/puzzle/sewingmachinepuzzle.cpp
index 5cde462aca9..03a0d54e298 100644
--- a/engines/nancy/action/puzzle/sewingmachinepuzzle.cpp
+++ b/engines/nancy/action/puzzle/sewingmachinepuzzle.cpp
@@ -101,30 +101,12 @@ void SewingMachinePuzzle::playSoundBlock(const RandomSoundBlock &block) {
g_nancy->_sound->loadSound(desc);
g_nancy->_sound->playSound(desc);
- showSubtitle(name);
-}
-
-void SewingMachinePuzzle::showSubtitle(const Common::String &soundName) {
// The mistake lines carry no inline caption; look the subtitle up by sound name,
// first in the Autotext table, then in the conversation table.
- Common::String text;
-
- const CVTX *autotext = (const CVTX *)g_nancy->getEngineData("AUTOTEXT");
- if (autotext) {
- text = autotext->texts.getValOrDefault(soundName, "");
- }
-
- if (text.empty()) {
- const CVTX *convo = (const CVTX *)g_nancy->getEngineData("CONVO");
- if (convo) {
- text = convo->texts.getValOrDefault(soundName, "");
- }
- }
-
- if (!text.empty()) {
- NancySceneState.getTextbox().clear();
- NancySceneState.getTextbox().addTextLine(text);
- }
+ Common::String text = resolveSubtitleText(name);
+ if (text.empty())
+ text = resolveSubtitleText(name, Common::String(), "CONVO");
+ showSubtitle(text);
}
Common::Point SewingMachinePuzzle::needleInStrip() const {
diff --git a/engines/nancy/action/puzzle/sewingmachinepuzzle.h b/engines/nancy/action/puzzle/sewingmachinepuzzle.h
index 0eb508b12a4..7fb43e7807a 100644
--- a/engines/nancy/action/puzzle/sewingmachinepuzzle.h
+++ b/engines/nancy/action/puzzle/sewingmachinepuzzle.h
@@ -56,9 +56,6 @@ protected:
void classifyZones();
// Plays one entry of a random-sound block (needle/stitch cues).
void playSoundBlock(const RandomSoundBlock &block);
- // Shows a played voice line's subtitle, looked up by sound name in the CVTX text
- // chunks (Autotext, then Convo).
- void showSubtitle(const Common::String &soundName);
// Draws the visible part of the cloth strip plus the stitches sewn so far.
void drawCloth();
diff --git a/engines/nancy/action/puzzle/soundmatchpuzzle.cpp b/engines/nancy/action/puzzle/soundmatchpuzzle.cpp
index cfd6a343b3a..787337d73bb 100644
--- a/engines/nancy/action/puzzle/soundmatchpuzzle.cpp
+++ b/engines/nancy/action/puzzle/soundmatchpuzzle.cpp
@@ -217,9 +217,7 @@ void SoundMatchPuzzle::handleInput(NancyInput &input) {
g_nancy->_sound->playSound(_soundButtons[i].sound);
}
- NancySceneState.getTextbox().clear();
- if (!_soundButtons[i].text.empty())
- NancySceneState.getTextbox().addTextLine(_soundButtons[i].text);
+ showSubtitle(_soundButtons[i].text);
_solveSubState = kSoundPlaying;
redraw();
diff --git a/engines/nancy/util.cpp b/engines/nancy/util.cpp
index 971d90a6df4..d5af5a7341c 100644
--- a/engines/nancy/util.cpp
+++ b/engines/nancy/util.cpp
@@ -21,6 +21,11 @@
#include "engines/nancy/enginedata.h"
#include "engines/nancy/nancy.h"
#include "engines/nancy/util.h"
+
+#include "engines/nancy/state/scene.h"
+#include "engines/nancy/ui/textbox.h"
+
+#include "common/config-manager.h"
#include "common/system.h"
namespace Nancy {
@@ -344,6 +349,42 @@ void assembleTextLine(char *rawCaption, Common::String &output, uint size) {
}
}
+Common::String resolveSubtitleText(const Common::String &keyOrText, const Common::String &fallback, const char *tableID) {
+ if (!keyOrText.empty()) {
+ const CVTX *table = (const CVTX *)g_nancy->getEngineData(tableID);
+ if (table && table->texts.contains(keyOrText)) {
+ return table->texts[keyOrText];
+ }
+ }
+
+ return fallback;
+}
+
+Common::String readSubtitleText(Common::SeekableReadStream &stream) {
+ char buf[30];
+ stream.read(buf, sizeof(buf));
+ buf[sizeof(buf) - 1] = '\0';
+ Common::String text(buf);
+
+ return resolveSubtitleText(text, text);
+}
+
+void showSubtitle(const Common::String &text, bool forceRedraw, int overrideFontID) {
+ if (text.empty() || !ConfMan.getBool("subtitles")) {
+ return;
+ }
+
+ UI::Textbox &textbox = NancySceneState.getTextbox();
+ textbox.clear();
+ if (overrideFontID >= 0) {
+ textbox.setOverrideFont(overrideFontID);
+ }
+ textbox.addTextLine(text);
+ if (forceRedraw) {
+ textbox.drawTextbox();
+ }
+}
+
bool DeferredLoader::load(uint32 endTime) {
uint32 loopStartTime = g_system->getMillis();
uint32 loopTime = 0; // Stores the loop that took the longest time to complete
diff --git a/engines/nancy/util.h b/engines/nancy/util.h
index 2d62fde1269..0fe835a4c2b 100644
--- a/engines/nancy/util.h
+++ b/engines/nancy/util.h
@@ -60,6 +60,22 @@ void readFilenameArray(Common::Serializer &stream, Common::Array<Common::Path> &
void assembleTextLine(char *rawCaption, Common::String &output, uint size);
+// Resolves a subtitle/caption string that may be a key into an engine-data CVTX text
+// table (AUTOTEXT by default, CONVO for some puzzles). Returns the table's entry for
+// `keyOrText` when the table exists and contains that key, otherwise returns `fallback`.
+Common::String resolveSubtitleText(const Common::String &keyOrText, const Common::String &fallback = Common::String(), const char *tableID = "AUTOTEXT");
+
+// Reads a 30-byte, NUL-terminated subtitle string from `stream` and resolves it as an
+// AUTOTEXT key, falling back to the literal text when the key is not present in the table.
+Common::String readSubtitleText(Common::SeekableReadStream &stream);
+
+// Shows `text` as a single line in the game textbox, replacing its current contents.
+// Does nothing when `text` is empty or when the player has subtitles disabled. A
+// non-negative `overrideFontID` selects a font other than the textbox default. When
+// `forceRedraw` is true, the textbox is redrawn immediately instead of on the next
+// render pass.
+void showSubtitle(const Common::String &text, bool forceRedraw = false, int overrideFontID = -1);
+
void readUIButton(Common::SeekableReadStream &stream, UIButtonRecord &dst);
void readUISlider(Common::SeekableReadStream &stream, UISliderRecord &dst);
void readUIPopupHeader(Common::SeekableReadStream &stream, UIPopupHeader &dst);
More information about the Scummvm-git-logs
mailing list