[Scummvm-git-logs] scummvm master -> 99be9e6211e4211cadd7b30c99d526acdf75c7f7
bluegr
noreply at scummvm.org
Wed Jun 24 01:26:25 UTC 2026
This automated email contains information about 1 new commit which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
99be9e6211 NANCY: Implement the Nancy11 timer functionality
Commit: 99be9e6211e4211cadd7b30c99d526acdf75c7f7
https://github.com/scummvm/scummvm/commit/99be9e6211e4211cadd7b30c99d526acdf75c7f7
Author: Filippos Karapetis (bluegr at gmail.com)
Date: 2026-06-24T04:24:14+03:00
Commit Message:
NANCY: Implement the Nancy11 timer functionality
Changed paths:
engines/nancy/action/arfactory.cpp
engines/nancy/action/miscrecords.cpp
engines/nancy/action/miscrecords.h
engines/nancy/puzzledata.cpp
engines/nancy/puzzledata.h
engines/nancy/state/scene.cpp
engines/nancy/state/scene.h
diff --git a/engines/nancy/action/arfactory.cpp b/engines/nancy/action/arfactory.cpp
index 13f009f0b88..efa0523850b 100644
--- a/engines/nancy/action/arfactory.cpp
+++ b/engines/nancy/action/arfactory.cpp
@@ -259,8 +259,7 @@ ActionRecord *ActionManager::createActionRecord(uint16 type, Common::SeekableRea
case 68:
return new TextScroll(false);
case 69: // Nancy11
- warning("TimerControl"); // TODO
- return nullptr;
+ return new TimerControl();
case 70:
return new TextScroll(true); // AutotextEntryList
case 71:
diff --git a/engines/nancy/action/miscrecords.cpp b/engines/nancy/action/miscrecords.cpp
index e05029c5429..3f0d9f77eb4 100644
--- a/engines/nancy/action/miscrecords.cpp
+++ b/engines/nancy/action/miscrecords.cpp
@@ -435,21 +435,150 @@ void TurnOnMainRendering::readData(Common::SeekableReadStream &stream) {
stream.skip(1);
}
+// Returns the Nancy 11+ software-timer slot at the given index, lazily creating
+// the TimerData puzzle-data chunk. nullptr if the index is out of range.
+static TimerData::Timer *getSoftwareTimer(int16 index) {
+ if (index < 0 || (uint)index >= TimerData::kNumTimers) {
+ return nullptr;
+ }
+
+ TimerData *timerData = (TimerData *)NancySceneState.getPuzzleData(TimerData::getTag());
+ return timerData ? &timerData->timers[index] : nullptr;
+}
+
void ResetAndStartTimer::readData(Common::SeekableReadStream &stream) {
- stream.skip(1);
+ _timerIndex = stream.readByte();
}
void ResetAndStartTimer::execute() {
NancySceneState.resetAndStartTimer();
+
+ // Nancy 11 also resets and starts one of the software-timer slots
+ if (g_nancy->getGameType() >= kGameTypeNancy11) {
+ TimerData::Timer *timer = getSoftwareTimer(_timerIndex);
+ if (timer) {
+ timer->reset();
+ timer->state = TimerData::Timer::kRunning;
+ }
+ }
+
_isDone = true;
}
void StopTimer::readData(Common::SeekableReadStream &stream) {
- stream.skip(1);
+ _timerIndex = stream.readByte();
}
void StopTimer::execute() {
NancySceneState.stopTimer();
+
+ if (g_nancy->getGameType() >= kGameTypeNancy11) {
+ TimerData::Timer *timer = getSoftwareTimer(_timerIndex);
+ if (timer) {
+ timer->reset();
+ }
+ }
+
+ _isDone = true;
+}
+
+// Reads exactly size bytes from the stream, returning the text up to the first
+// null byte. Used for the fixed-size string fields inside a TimerControl chunk.
+static Common::String readFixedSizeString(Common::SeekableReadStream &stream, uint size) {
+ Common::String result;
+ bool ended = false;
+ for (uint i = 0; i < size; ++i) {
+ byte b = stream.readByte();
+ if (b == 0) {
+ ended = true;
+ }
+ if (!ended) {
+ result += (char)b;
+ }
+ }
+
+ return result;
+}
+
+void TimerControl::readData(Common::SeekableReadStream &stream) {
+ const int64 startPos = stream.pos();
+
+ _timerIndex = stream.readSint16LE(); // 0x00
+ _command = stream.readSint16LE(); // 0x02
+ _hours = stream.readSint16LE(); // 0x04
+ _minutes = stream.readSint16LE(); // 0x06
+ _seconds = stream.readSint16LE(); // 0x08
+ stream.skip(2); // 0x0a, unused
+
+ // Sound to play on expiry (0x0c)
+ _sound.readDIGI(stream);
+
+ // Autotext key for the caption (0x3d, 33 bytes)
+ stream.skip((startPos + 0x3d) - stream.pos());
+ _autotextKey = readFixedSizeString(stream, 0x21);
+
+ // Inline caption text (0x5e, 100 bytes)
+ _caption = readFixedSizeString(stream, 0x64);
+
+ // Event flags to fire on expiry (count at 0xc2, then count*4 bytes)
+ uint16 numFlags = stream.readUint16LE();
+ _flags.resize(numFlags);
+ for (uint i = 0; i < numFlags; ++i) {
+ _flags[i].label = stream.readSint16LE();
+ _flags[i].flag = (byte)stream.readSint16LE();
+ }
+}
+
+void TimerControl::execute() {
+ TimerData::Timer *timer = getSoftwareTimer(_timerIndex);
+ if (!timer) {
+ _isDone = true;
+ return;
+ }
+
+ const uint32 durationMs = ((uint32)_hours * 3600 + (uint32)_minutes * 60 + (uint32)_seconds) * 1000;
+
+ switch (_command) {
+ case kReset:
+ timer->reset();
+ break;
+ case kStart:
+ timer->state = TimerData::Timer::kRunning;
+ break;
+ case kPause:
+ timer->state = TimerData::Timer::kPaused;
+ break;
+ case kAddTime:
+ if (timer->state != TimerData::Timer::kIdle) {
+ timer->currentTimeMs += durationMs;
+ }
+ break;
+ case kSubtractTime:
+ if (timer->state != TimerData::Timer::kIdle) {
+ timer->currentTimeMs = timer->currentTimeMs > durationMs ? timer->currentTimeMs - durationMs : 0;
+ }
+ break;
+ case kConfigOneShot:
+ case kConfigRepeating:
+ // A timer can only be configured once it has been started
+ if (timer->state == TimerData::Timer::kRunning) {
+ timer->durationMs = durationMs;
+ timer->hasFired = false;
+ timer->sound = _sound;
+ timer->autotextKey = _autotextKey;
+ timer->caption = _caption;
+
+ for (uint i = 0; i < ARRAYSIZE(timer->flags); ++i) {
+ timer->flags[i] = i < _flags.size() ? _flags[i] : FlagDescription();
+ }
+
+ timer->state = _command;
+ }
+ break;
+ default:
+ break;
+ }
+
_isDone = true;
}
diff --git a/engines/nancy/action/miscrecords.h b/engines/nancy/action/miscrecords.h
index d133518b151..54716cb49f1 100644
--- a/engines/nancy/action/miscrecords.h
+++ b/engines/nancy/action/miscrecords.h
@@ -288,12 +288,15 @@ protected:
};
// Starts the timer. Used in combination with Dependency types that check for
-// how much time has passed since the timer was started.
+// how much time has passed since the timer was started. From Nancy 11 onwards
+// the record also carries a software-timer slot index (see TimerControl).
class ResetAndStartTimer : public ActionRecord {
public:
void readData(Common::SeekableReadStream &stream) override;
void execute() override;
+ byte _timerIndex = 0; // Nancy 11+ software-timer slot
+
protected:
Common::String getRecordTypeName() const override { return "ResetAndStartTimer"; }
};
@@ -304,10 +307,47 @@ public:
void readData(Common::SeekableReadStream &stream) override;
void execute() override;
+ byte _timerIndex = 0; // Nancy 11+ software-timer slot
+
protected:
Common::String getRecordTypeName() const override { return "StopTimer"; }
};
+// Nancy 11+ AR 69 (AT_TIMER_CONTROL). Issues a command to one of the 10
+// software-timer slots (see TimerData::Timer). The fixed-size chunk
+// (0xc4 header + count*4 flag entries) carries a slot index, a command, a
+// target duration, an optional sound + caption, and the event flags to fire
+// when the timer expires.
+class TimerControl : public ActionRecord {
+public:
+ enum Command {
+ kReset = 0, // Clear the slot back to idle
+ kStart = 1, // Begin counting, with no target
+ kPause = 2, // Suspend counting
+ kAddTime = 3, // Add the duration to the elapsed time
+ kSubtractTime = 4, // Subtract the duration from the elapsed time
+ kConfigOneShot = 5, // Set target/payload; fire once, then reset
+ kConfigRepeating = 6 // Set target/payload; fire once, then keep running
+ };
+
+ void readData(Common::SeekableReadStream &stream) override;
+ void execute() override;
+
+ int16 _timerIndex = 0;
+ int16 _command = 0;
+ int16 _hours = 0;
+ int16 _minutes = 0;
+ int16 _seconds = 0;
+
+ SoundDescription _sound;
+ Common::String _autotextKey;
+ Common::String _caption;
+ Common::Array<FlagDescription> _flags;
+
+protected:
+ Common::String getRecordTypeName() const override { return "TimerControl"; }
+};
+
// Nancy 11+ AR 30. Disables the player's ability to scroll/pan the viewport
// (both mouse-edge and keyboard movement). State persists across scene changes.
class StopPlayerScrolling : public ActionRecord {
diff --git a/engines/nancy/puzzledata.cpp b/engines/nancy/puzzledata.cpp
index a11dbc379cd..938d31bc253 100644
--- a/engines/nancy/puzzledata.cpp
+++ b/engines/nancy/puzzledata.cpp
@@ -352,6 +352,30 @@ void CellPhoneData::syncLinkArray(Common::Serializer &ser, Common::Array<LinkEnt
}
}
+void TimerData::synchronize(Common::Serializer &ser) {
+ for (uint i = 0; i < kNumTimers; ++i) {
+ Timer &t = timers[i];
+ ser.syncAsSint32LE(t.state);
+ ser.syncAsUint32LE(t.currentTimeMs);
+ ser.syncAsUint32LE(t.durationMs);
+ ser.syncAsByte(t.hasFired);
+
+ ser.syncString(t.sound.name);
+ ser.syncAsUint16LE(t.sound.channelID);
+ ser.syncAsUint16LE(t.sound.playCommands);
+ ser.syncAsUint16LE(t.sound.numLoops);
+ ser.syncAsUint16LE(t.sound.volume);
+
+ ser.syncString(t.autotextKey);
+ ser.syncString(t.caption);
+
+ for (uint j = 0; j < ARRAYSIZE(t.flags); ++j) {
+ ser.syncAsSint16LE(t.flags[j].label);
+ ser.syncAsByte(t.flags[j].flag);
+ }
+ }
+}
+
PuzzleData *makePuzzleData(const uint32 tag) {
switch(tag) {
case SliderPuzzleData::getTag():
@@ -382,6 +406,8 @@ PuzzleData *makePuzzleData(const uint32 tag) {
return new TableData();
case CellPhoneData::getTag():
return new CellPhoneData();
+ case TimerData::getTag():
+ return new TimerData();
default:
return nullptr;
}
diff --git a/engines/nancy/puzzledata.h b/engines/nancy/puzzledata.h
index 5932ec92c0e..fafedab7de4 100644
--- a/engines/nancy/puzzledata.h
+++ b/engines/nancy/puzzledata.h
@@ -254,6 +254,38 @@ private:
void syncLinkArray(Common::Serializer &ser, Common::Array<LinkEntry> &arr);
};
+// Nancy 11+ AR 69 (TimerControl). 10 software timers, each counting up from
+// zero. A "configured" timer (state 5/6) fires a set of event flags, plays an
+// optional sound and shows an optional caption once its target duration
+// elapses. Started/stopped via ResetAndStartTimer (104) and StopTimer (105),
+// which in Nancy 11 carry a timer-slot index.
+struct TimerData : public PuzzleData {
+ struct Timer {
+ enum State { kIdle = 0, kRunning = 1, kPaused = 2, kOneShot = 5, kRepeating = 6 };
+
+ int32 state = kIdle;
+ uint32 currentTimeMs = 0;
+ uint32 durationMs = 0;
+ bool hasFired = false;
+ SoundDescription sound;
+ Common::String autotextKey;
+ Common::String caption;
+ FlagDescription flags[10];
+
+ void reset() { *this = Timer(); }
+ };
+
+ static const uint kNumTimers = 10;
+
+ TimerData() {}
+ virtual ~TimerData() {}
+
+ static constexpr uint32 getTag() { return MKTAG('T', 'M', 'R', 'S'); }
+ virtual void synchronize(Common::Serializer &ser);
+
+ Timer timers[kNumTimers];
+};
+
PuzzleData *makePuzzleData(const uint32 tag);
} // End of namespace Nancy
diff --git a/engines/nancy/state/scene.cpp b/engines/nancy/state/scene.cpp
index 18571f13cdd..66c2405d07f 100644
--- a/engines/nancy/state/scene.cpp
+++ b/engines/nancy/state/scene.cpp
@@ -1119,6 +1119,10 @@ void Scene::run() {
_timers.sceneTime += deltaTime;
+ // Advance the Nancy 11+ software timers before processing action records,
+ // so any flags they fire this frame are visible to record dependencies
+ tickSoftwareTimers((uint32)deltaTime);
+
// Calculate the in-game time (playerTime)
if (currentPlayTime > _timers.playerTimeNextMinute) {
auto *bootSummary = GetEngineData(BSUM);
@@ -1160,6 +1164,76 @@ void Scene::run() {
}
}
+void Scene::tickSoftwareTimers(uint32 deltaMs) {
+ if (g_nancy->getGameType() < kGameTypeNancy11 || deltaMs == 0) {
+ return;
+ }
+
+ // getPuzzleData() below lazily creates (and thereafter persists) the TimerData
+ // chunk. This runs every frame, so without this guard every Nancy 11 save
+ // would carry an empty TimerData chunk even if no timer is ever used. The
+ // chunk only exists once a timer AR has configured a slot.
+ if (!_puzzleData.contains(TimerData::getTag())) {
+ return;
+ }
+
+ TimerData *timerData = (TimerData *)getPuzzleData(TimerData::getTag());
+
+ for (uint i = 0; i < TimerData::kNumTimers; ++i) {
+ TimerData::Timer &timer = timerData->timers[i];
+
+ if (timer.state != TimerData::Timer::kRunning &&
+ timer.state != TimerData::Timer::kOneShot &&
+ timer.state != TimerData::Timer::kRepeating) {
+ continue;
+ }
+
+ timer.currentTimeMs += deltaMs;
+
+ if ((timer.state == TimerData::Timer::kOneShot || timer.state == TimerData::Timer::kRepeating) &&
+ timer.durationMs > 0 && !timer.hasFired && timer.currentTimeMs >= timer.durationMs) {
+ fireSoftwareTimer(timer);
+
+ if (timer.state == TimerData::Timer::kOneShot) {
+ // One-shot timers clear themselves once they fire
+ timer.reset();
+ } else {
+ // Repeating timers keep counting up but will not fire again
+ timer.state = TimerData::Timer::kRunning;
+ }
+ }
+ }
+}
+
+void Scene::fireSoftwareTimer(TimerData::Timer &timer) {
+ timer.hasFired = true;
+
+ // Set the configured event flags
+ for (uint i = 0; i < ARRAYSIZE(timer.flags); ++i) {
+ if (timer.flags[i].label != kFlagNoLabel) {
+ setEventFlag(timer.flags[i]);
+ }
+ }
+
+ // Play the optional expiry sound
+ if (timer.sound.name != "NO SOUND") {
+ g_nancy->_sound->loadSound(timer.sound);
+ g_nancy->_sound->playSound(timer.sound);
+ }
+
+ // Show the optional caption, if captions are enabled
+ if (ConfMan.getBool("subtitles", ConfMan.getActiveDomainName())) {
+ if (!timer.autotextKey.empty()) {
+ const CVTX *autotext = (const CVTX *)g_nancy->getEngineData("AUTOTEXT");
+ if (autotext && autotext->texts.contains(timer.autotextKey)) {
+ _textbox.addTextLine(autotext->texts[timer.autotextKey]);
+ }
+ } else if (!timer.caption.empty()) {
+ _textbox.addTextLine(timer.caption);
+ }
+ }
+}
+
Common::Rect Scene::activePopupConfinement() const {
// Pick the first visible Nancy 10+ popup; if more than one is open
// (shouldn't normally happen) the priority order matches the input
diff --git a/engines/nancy/state/scene.h b/engines/nancy/state/scene.h
index 407971b6b9c..68c16600899 100644
--- a/engines/nancy/state/scene.h
+++ b/engines/nancy/state/scene.h
@@ -247,6 +247,11 @@ private:
void run();
void handleInput();
+ // Nancy 11+ AR 69. Advances all running software timers (stored as TimerData
+ // puzzle data) and fires any whose configured duration has just elapsed.
+ void tickSoftwareTimers(uint32 deltaMs);
+ void fireSoftwareTimer(TimerData::Timer &timer);
+
// Rect of the open Nancy 10+ taskbar popup, or empty if none.
Common::Rect activePopupConfinement() const;
More information about the Scummvm-git-logs
mailing list