[Scummvm-git-logs] scummvm master -> d628bd41df34d169c76ef82968e40d5e4cc498d7
bluegr
noreply at scummvm.org
Sat Jul 18 13:02:25 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:
18190b1c02 NANCY: NANCY8: Slight cleanup in AngleTossPuzzle
cfcfaf640a NANCY: NANCY13: Implement new PlayRandomMovie functionality
d628bd41df NANCY: Better file naming for exported AR dumps
Commit: 18190b1c02811e85e035306d59671905dbec187a
https://github.com/scummvm/scummvm/commit/18190b1c02811e85e035306d59671905dbec187a
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-18T16:02:14+03:00
Commit Message:
NANCY: NANCY8: Slight cleanup in AngleTossPuzzle
Changed paths:
engines/nancy/action/puzzle/angletosspuzzle.cpp
diff --git a/engines/nancy/action/puzzle/angletosspuzzle.cpp b/engines/nancy/action/puzzle/angletosspuzzle.cpp
index 6f08628850f..4877d2d0a9a 100644
--- a/engines/nancy/action/puzzle/angletosspuzzle.cpp
+++ b/engines/nancy/action/puzzle/angletosspuzzle.cpp
@@ -83,7 +83,7 @@ void AngleTossPuzzle::readData(Common::SeekableReadStream &stream) {
_throwSquidScene.readData(stream); // data+0x220, ends at data+0x238
- // HACK: We've skipped some of the flag data at this point,
+ // WORKAROUND: We've skipped some of the flag data at this point,
// so go back to read them correctly
_throwSquidScene._flag.label = kEvNoEvent;
_throwSquidScene._flag.flag = 0;
@@ -132,19 +132,14 @@ void AngleTossPuzzle::execute() {
// Set exactly one result flag based on how accurate the throw was.
// The animation scene reads these flags to decide what to show.
if (_curPower == _targetPower && _curAngle == _targetAngle) {
- // Exact match of power and angle â player wins round!
NancySceneState.setEventFlag(_winFlag, g_nancy->_true);
} else if (_curPower > _targetPower) {
- // Power too strong
NancySceneState.setEventFlag(_powerTooStrongFlag, g_nancy->_true);
} else if (_curPower < _targetPower) {
- // Power too weak
NancySceneState.setEventFlag(_powerTooWeakFlag, g_nancy->_true);
} else if (_curAngle > _targetAngle) {
- // Angle too far right
NancySceneState.setEventFlag(_angleTooRightFlag, g_nancy->_true);
} else if (_curAngle < _targetAngle) {
- // Angle too far left
NancySceneState.setEventFlag(_angleTooLeftFlag, g_nancy->_true);
}
@@ -245,7 +240,6 @@ void AngleTossPuzzle::handleInput(NancyInput &input) {
_drawSurface.blitFrom(_image, _throwSprite, _throwDisplay);
_needsRedraw = true;
}
- return;
}
}
Commit: cfcfaf640a4916f36b74551d94515aedf4ae559c
https://github.com/scummvm/scummvm/commit/cfcfaf640a4916f36b74551d94515aedf4ae559c
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-18T16:02:14+03:00
Commit Message:
NANCY: NANCY13: Implement new PlayRandomMovie functionality
Fixes character idle/fidgeting animations
Changed paths:
engines/nancy/action/secondarymovie.cpp
engines/nancy/action/secondarymovie.h
diff --git a/engines/nancy/action/secondarymovie.cpp b/engines/nancy/action/secondarymovie.cpp
index 70854e4e200..99279aa5947 100644
--- a/engines/nancy/action/secondarymovie.cpp
+++ b/engines/nancy/action/secondarymovie.cpp
@@ -55,13 +55,30 @@ PlaySecondaryMovie::~PlaySecondaryMovie() {
}
}
+bool PlaySecondaryMovie::isPersistentAcrossScenes() const {
+ // Nancy11's random movies can be ambient loops that intentionally keep
+ // playing across scene changes. Nancy13's per-character reaction movies
+ // (AR 42) are scene-local: they must stop when their scene is left, and are
+ // reloaded if it's re-entered.
+ return _isRandom && g_nancy->getGameType() < kGameTypeNancy13 && !_isDone && !_randomStopRequested;
+}
+
void PlaySecondaryMovie::readRandomSequence(Common::Serializer &ser, RandomSequence &seq) {
readFilename(ser, seq.name);
ser.syncAsUint16LE(seq.startFrame);
ser.syncAsUint16LE(seq.lastFrame);
ser.syncAsSint32LE(seq.minPauseMs);
ser.syncAsSint32LE(seq.maxPauseMs);
- ser.syncAsUint16LE(seq.stayWeight);
+
+ if (g_nancy->getGameType() >= kGameTypeNancy13) {
+ // Percent (0-100) chance to stay on this sequence and pause, rather
+ // than transition to one of the next sequences.
+ byte pauseChance = 0;
+ ser.syncAsByte(pauseChance);
+ seq.stayWeight = pauseChance;
+ } else {
+ ser.syncAsUint16LE(seq.stayWeight);
+ }
uint16 nextCount = 0;
ser.syncAsUint16LE(nextCount);
@@ -73,19 +90,54 @@ void PlaySecondaryMovie::readRandomSequence(Common::Serializer &ser, RandomSeque
}
}
+void PlaySecondaryMovie::readSecondaryRandomMovie(Common::Serializer &ser, RandomSequence &seq) {
+ // Nancy13's random-movie chunk carries one extra "secondary" movie record
+ // (e.g. a character's recognition animation) between the sequence list and
+ // the hotspot list. It is a name + 5 uint16 + a next-list; only the first
+ // two uint16s (start/last frame) have known roles so far, but the whole
+ // record must be consumed here or the hotspot list below misaligns.
+ readFilename(ser, seq.name);
+ ser.syncAsUint16LE(seq.startFrame);
+ ser.syncAsUint16LE(seq.lastFrame);
+ ser.skip(6); // three uint16 fields, roles not yet characterized
+
+ uint16 nextCount = 0;
+ ser.syncAsUint16LE(nextCount);
+ seq.nextSequences.resize(nextCount);
+ for (uint i = 0; i < nextCount; ++i) {
+ readFilename(ser, seq.nextSequences[i].name);
+ ser.syncAsUint16LE(seq.nextSequences[i].weight);
+ }
+}
+
void PlaySecondaryMovie::readRandomMovieData(Common::Serializer &ser, Common::SeekableReadStream &stream) {
readFilename(ser, _startingSequenceName);
ser.syncAsUint16LE(_randomPlayerCursorAllowed);
uint16 sequenceCount = 0, hotspotCount = 0;
- ser.syncAsUint16LE(sequenceCount);
- ser.syncAsUint16LE(hotspotCount);
+
+ if (g_nancy->getGameType() >= kGameTypeNancy13) {
+ // Nancy13 replaced the inline hotspot count with two header fields (a
+ // flag tested against 1, and a u16); the hotspot list is now length-
+ // prefixed after the sequences instead.
+ ser.skip(2);
+ ser.skip(2);
+ ser.syncAsUint16LE(sequenceCount);
+ } else {
+ ser.syncAsUint16LE(sequenceCount);
+ ser.syncAsUint16LE(hotspotCount);
+ }
_sequences.resize(sequenceCount);
for (uint i = 0; i < sequenceCount; ++i) {
readRandomSequence(ser, _sequences[i]);
}
+ if (g_nancy->getGameType() >= kGameTypeNancy13) {
+ readSecondaryRandomMovie(ser, _secondaryMovie);
+ ser.syncAsUint16LE(hotspotCount);
+ }
+
_videoDescs.resize(hotspotCount);
for (uint i = 0; i < hotspotCount; ++i) {
_videoDescs[i].readData(stream);
@@ -142,11 +194,26 @@ bool PlaySecondaryMovie::activateRandomSequence(int index) {
return false;
}
+ resolveSentinelFrames();
+
_isFinished = false;
_curViewportFrame = -1; // force visibility re-evaluation next tick
return true;
}
+void PlaySecondaryMovie::resolveSentinelFrames() {
+ // Random sequences use -1/-2 for the start/last frame to mean "play the
+ // movie's own first/last frame". Resolve them now that the decoder (and
+ // thus the real frame count) is available.
+ if (_firstFrame == 0xFFFF) {
+ _firstFrame = 0;
+ }
+ if (_lastFrame == 0xFFFE || _lastFrame == 0xFFFF) {
+ int frameCount = _decoder.getFrameCount();
+ _lastFrame = frameCount > 0 ? (uint16)(frameCount - 1) : 0;
+ }
+}
+
void PlaySecondaryMovie::playRandomSequence() {
if (!_isRandom || _sequences.empty()) {
return;
@@ -157,6 +224,28 @@ void PlaySecondaryMovie::playRandomSequence() {
activateRandomSequence(picked);
}
+int PlaySecondaryMovie::beginRandomPause(const RandomSequence &seq) {
+ int32 pauseMs = seq.minPauseMs;
+ if (seq.maxPauseMs > seq.minPauseMs) {
+ pauseMs += g_nancy->_randomSource->getRandomNumber(seq.maxPauseMs - seq.minPauseMs - 1);
+ }
+ _randomPauseEndTime = g_system->getMillis() + (uint32)MAX<int32>(0, pauseMs);
+ _randomChainState = kRandomPaused;
+ setVisible(false);
+ _decoder.pauseVideo(true);
+ return -1;
+}
+
+int PlaySecondaryMovie::lookupSequence(const Common::Path &name) const {
+ for (uint j = 0; j < _sequences.size(); ++j) {
+ if (_sequences[j].name == name) {
+ return (int)j;
+ }
+ }
+ warning("PlayRandomMovie: next-sequence \"%s\" not part of this AR", name.toString().c_str());
+ return -1;
+}
+
int PlaySecondaryMovie::rollNextSequence() {
if (_activeSequenceIndex < 0 || _activeSequenceIndex >= (int)_sequences.size()) {
return -1;
@@ -164,6 +253,38 @@ int PlaySecondaryMovie::rollNextSequence() {
const RandomSequence &seq = _sequences[_activeSequenceIndex];
+ if (g_nancy->getGameType() >= kGameTypeNancy13) {
+ // Two independent rolls: first a percent chance to stay on this
+ // sequence and pause, then a percent-weighted pick among the next
+ // sequences (weights sum to 100, or all EQUAL_CHANCE for a uniform pick).
+ if (seq.stayWeight != 0 && (uint)g_nancy->_randomSource->getRandomNumber(99) < seq.stayWeight) {
+ return beginRandomPause(seq);
+ }
+
+ if (seq.nextSequences.empty()) {
+ _randomChainState = kRandomPaused;
+ _randomPauseEndTime = g_system->getMillis() + 1000; // re-check in 1s
+ return -1;
+ }
+
+ const bool equalChance = seq.nextSequences[0].weight == 0xFFFF;
+ const uint step = 100 / seq.nextSequences.size();
+ uint roll = g_nancy->_randomSource->getRandomNumber(99);
+ uint cumulative = 0;
+ for (uint i = 0; i < seq.nextSequences.size(); ++i) {
+ if (i == seq.nextSequences.size() - 1) {
+ cumulative = 100;
+ } else {
+ cumulative += equalChance ? step : seq.nextSequences[i].weight;
+ }
+ if (roll < cumulative) {
+ return lookupSequence(seq.nextSequences[i].name);
+ }
+ }
+
+ return -1;
+ }
+
uint32 totalWeight = seq.stayWeight;
for (const NextSequenceRef &ns : seq.nextSequences) {
totalWeight += ns.weight;
@@ -179,30 +300,14 @@ int PlaySecondaryMovie::rollNextSequence() {
uint32 roll = g_nancy->_randomSource->getRandomNumber(totalWeight - 1);
if (roll < seq.stayWeight) {
- int32 pauseMs = seq.minPauseMs;
- if (seq.maxPauseMs > seq.minPauseMs) {
- pauseMs += g_nancy->_randomSource->getRandomNumber(seq.maxPauseMs - seq.minPauseMs - 1);
- }
- _randomPauseEndTime = g_system->getMillis() + (uint32)MAX<int32>(0, pauseMs);
- _randomChainState = kRandomPaused;
- setVisible(false);
- _decoder.pauseVideo(true);
- return -1;
+ return beginRandomPause(seq);
}
uint32 cumulative = seq.stayWeight;
for (uint i = 0; i < seq.nextSequences.size(); ++i) {
cumulative += seq.nextSequences[i].weight;
if (roll < cumulative) {
- // Look up the named sequence in _sequences[].
- for (uint j = 0; j < _sequences.size(); ++j) {
- if (_sequences[j].name == seq.nextSequences[i].name) {
- return (int)j;
- }
- }
- warning("PlayRandomMovie: next-sequence \"%s\" not part of this AR",
- seq.nextSequences[i].name.toString().c_str());
- return -1;
+ return lookupSequence(seq.nextSequences[i].name);
}
}
@@ -354,6 +459,10 @@ void PlaySecondaryMovie::init() {
}
}
+ if (_isRandom) {
+ resolveSentinelFrames();
+ }
+
_screenPosition = _drawSurface.getBounds();
RenderObject::init();
diff --git a/engines/nancy/action/secondarymovie.h b/engines/nancy/action/secondarymovie.h
index 7d754ebc69e..4a1d659f0ca 100644
--- a/engines/nancy/action/secondarymovie.h
+++ b/engines/nancy/action/secondarymovie.h
@@ -121,6 +121,11 @@ public:
uint16 _randomPlayerCursorAllowed = kPlayerCursorAllowed;
Common::Array<RandomSequence> _sequences;
+ // Nancy13+ carries one extra "secondary" movie (e.g. a recognition
+ // animation) after the sequence list. Stored for future playback; reading
+ // it is required so the trailing hotspot list stays aligned.
+ RandomSequence _secondaryMovie;
+
// Chain state. After a sequence's movie finishes the engine rolls a
// weighted pick: "stay" -> enter pause for a random duration and
// re-roll; valid next-sequence -> swap to that sequence's movie.
@@ -138,9 +143,7 @@ public:
bool isViewportRelative() const override { return true; }
- bool isPersistentAcrossScenes() const override {
- return _isRandom && !_isDone && !_randomStopRequested;
- }
+ bool isPersistentAcrossScenes() const override;
Common::String getRecordExtraInfo() const override { return Common::String::format("Scene %d", _sceneChange.sceneID); }
@@ -153,6 +156,7 @@ protected:
// needed for SecondaryVideoDescription::readData.
void readRandomMovieData(Common::Serializer &ser, Common::SeekableReadStream &stream);
void readRandomSequence(Common::Serializer &ser, RandomSequence &seq);
+ void readSecondaryRandomMovie(Common::Serializer &ser, RandomSequence &seq);
void readDataNancy14(Common::Serializer &ser, Common::SeekableReadStream &stream);
@@ -165,6 +169,17 @@ protected:
// or the chosen sequence index otherwise.
int rollNextSequence();
+ // Enter the paused chain state for a random duration in the sequence's
+ // [minPauseMs, maxPauseMs] range. Always returns -1.
+ int beginRandomPause(const RandomSequence &seq);
+
+ // Find a sequence by name, warning and returning -1 if it isn't present.
+ int lookupSequence(const Common::Path &name) const;
+
+ // Resolve the -1/-2 "whole movie" sentinels in _firstFrame/_lastFrame
+ // against the loaded decoder's frame count. Random sequences only.
+ void resolveSentinelFrames();
+
Graphics::ManagedSurface _fullFrame;
int _curViewportFrame = -1;
bool _isFinished = false;
Commit: d628bd41df34d169c76ef82968e40d5e4cc498d7
https://github.com/scummvm/scummvm/commit/d628bd41df34d169c76ef82968e40d5e4cc498d7
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-07-18T16:02:15+03:00
Commit Message:
NANCY: Better file naming for exported AR dumps
Changed paths:
engines/nancy/console.cpp
diff --git a/engines/nancy/console.cpp b/engines/nancy/console.cpp
index b7b2e9ae01d..ce75446f40d 100644
--- a/engines/nancy/console.cpp
+++ b/engines/nancy/console.cpp
@@ -704,10 +704,11 @@ bool NancyConsole::Cmd_actionRecordExport(int argc, const char **argv) {
char descBuf[48];
chunk->read(descBuf, 48);
descBuf[47] = '\0';
- chunk->skip(2); // ARType, execType
+ byte ARType = chunk->readByte();
+ chunk->skip(1); // execType
Common::DumpFile f;
- Common::String filename = Common::String::format("%s_scene_%d_record_%d_%s.dat", g_nancy->getGameId(), sceneId, recordId, descBuf);
+ Common::String filename = Common::String::format("%s_ar_%d_scene_%d_%d_%s.dat", g_nancy->getGameId(), ARType, sceneId, recordId, descBuf);
f.open(Common::Path(filename));
f.writeStream(chunk, chunk->size() - 50);
f.close();
More information about the Scummvm-git-logs
mailing list