From b863a53b358cb585925e78cf0b93d5d941230745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Tue, 20 Oct 2026 22:14:07 +0200 Subject: [PATCH] [Replay] Seek to the exact time of an event Timeline seeks only expose whole timer intervals, so jumping to an event could leave that event unprocessed until the next tick. Add an event-time seek that also applies events at the target timestamp. --- .../widgets/replay/replay_manager.cpp | 39 ++++++++++++++----- .../interface/widgets/replay/replay_manager.h | 8 ++-- .../widgets/replay/replay_widget.cpp | 11 ++++++ .../interface/widgets/replay/replay_widget.h | 7 ++++ 4 files changed, 52 insertions(+), 13 deletions(-) diff --git a/cockatrice/src/interface/widgets/replay/replay_manager.cpp b/cockatrice/src/interface/widgets/replay/replay_manager.cpp index f3f1da2c4..713cbbdf7 100644 --- a/cockatrice/src/interface/widgets/replay/replay_manager.cpp +++ b/cockatrice/src/interface/widgets/replay/replay_manager.cpp @@ -57,7 +57,7 @@ ReplayManager::ReplayManager(QObject *parent, GameReplay *replay) rewindBufferingTimer = new QTimer(this); rewindBufferingTimer->setSingleShot(true); - connect(rewindBufferingTimer, &QTimer::timeout, this, &ReplayManager::processRewind); + connect(rewindBufferingTimer, &QTimer::timeout, this, qOverload<>(&ReplayManager::processRewind)); } ReplayManager::~ReplayManager() @@ -65,7 +65,7 @@ ReplayManager::~ReplayManager() delete replay; } -void ReplayManager::skipToTime(int newTime, bool doRewindBuffering) +void ReplayManager::skipToTime(int newTime, bool doRewindBuffering, bool includeEventsAtTime) { // check boundary conditions if (newTime < 0) { @@ -75,15 +75,17 @@ void ReplayManager::skipToTime(int newTime, bool doRewindBuffering) newTime = maxTime; } - newTime -= newTime % TIMER_INTERVAL_MS; // Time should always be a multiple of the interval + if (!includeEventsAtTime) { + newTime -= newTime % TIMER_INTERVAL_MS; // Time should always be a multiple of the interval + } const bool isBackwardsSkip = newTime < currentProcessedTime; currentVisualTime = newTime; if (isBackwardsSkip) { - handleBackwardsSkip(doRewindBuffering); + handleBackwardsSkip(doRewindBuffering, includeEventsAtTime); } else { - processNewEvents(FORWARD_SKIP); + processNewEvents(FORWARD_SKIP, includeEventsAtTime); } timeChanged(currentVisualTime); @@ -95,7 +97,7 @@ void ReplayManager::skipToTime(int newTime, bool doRewindBuffering) * @param doRewindBuffering When true, if multiple backward skips are made in quick succession, only a single rewind * is processed at the end. When false, the backwards skip will always cause an immediate rewind. */ -void ReplayManager::handleBackwardsSkip(bool doRewindBuffering) +void ReplayManager::handleBackwardsSkip(bool doRewindBuffering, bool includeEventsAtTime) { if (doRewindBuffering) { // We use a one-shot timer to implement the rewind buffering. @@ -105,11 +107,16 @@ void ReplayManager::handleBackwardsSkip(bool doRewindBuffering) rewindBufferingTimer->start(SettingsCache::instance().userInterface().getRewindBufferingMs()); } else { // otherwise, process the rewind immediately - processRewind(); + processRewind(includeEventsAtTime); } } void ReplayManager::processRewind() +{ + processRewind(false); +} + +void ReplayManager::processRewind(bool includeEventsAtTime) { // stop any queued-up rewinds rewindBufferingTimer->stop(); @@ -117,7 +124,7 @@ void ReplayManager::processRewind() // process the rewind currentEvent = 0; emit rewound(); - processNewEvents(BACKWARD_SKIP); + processNewEvents(BACKWARD_SKIP, includeEventsAtTime); } void ReplayManager::replayTimerTimeout() @@ -134,11 +141,13 @@ void ReplayManager::replayTimerTimeout() } /** @brief Processes all unprocessed events up to the current time. */ -void ReplayManager::processNewEvents(PlaybackMode playbackMode) +void ReplayManager::processNewEvents(PlaybackMode playbackMode, bool includeEventsAtTime) { currentProcessedTime = currentVisualTime; - while (currentEvent < replayTimeline.size() && replayTimeline[currentEvent] < currentProcessedTime) { + while (currentEvent < replayTimeline.size() && + (replayTimeline[currentEvent] < currentProcessedTime || + (includeEventsAtTime && replayTimeline[currentEvent] == currentProcessedTime))) { EventProcessingOptions options; // backwards skip => always skip reveal windows @@ -236,6 +245,16 @@ void ReplayManager::setTime(int time) skipToTime(time, false); } +void ReplayManager::setTimeForEvent(int eventIndex) +{ + if (eventIndex < 0 || eventIndex >= replayTimeline.size()) { + return; + } + + // Unlike a timeline seek, jumping to an event should apply that event immediately. + skipToTime(replayTimeline.at(eventIndex), false, true); +} + void ReplayManager::skipByAmount(int amount) { skipToTime(currentVisualTime + amount, amount < 0); diff --git a/cockatrice/src/interface/widgets/replay/replay_manager.h b/cockatrice/src/interface/widgets/replay/replay_manager.h index 3b33deada..c4dd96d66 100644 --- a/cockatrice/src/interface/widgets/replay/replay_manager.h +++ b/cockatrice/src/interface/widgets/replay/replay_manager.h @@ -41,10 +41,11 @@ class ReplayManager : public QObject ///< to rewind buffering int currentEvent = 0; ///< current event's index - void skipToTime(int newTime, bool doRewindBuffering); - void handleBackwardsSkip(bool doRewindBuffering); + void skipToTime(int newTime, bool doRewindBuffering, bool includeEventsAtTime = false); + void handleBackwardsSkip(bool doRewindBuffering, bool includeEventsAtTime = false); void processRewind(); - void processNewEvents(PlaybackMode playbackMode); + void processRewind(bool includeEventsAtTime); + void processNewEvents(PlaybackMode playbackMode, bool includeEventsAtTime = false); void handleSkipEmptySection(); private slots: @@ -74,6 +75,7 @@ public slots: void startReplay(); void stopReplay(); void setTime(int time); + void setTimeForEvent(int eventIndex); void skipByAmount(int amount); // use a negative amount to skip backwards signals: diff --git a/cockatrice/src/interface/widgets/replay/replay_widget.cpp b/cockatrice/src/interface/widgets/replay/replay_widget.cpp index 07b4e72ce..577a128ee 100644 --- a/cockatrice/src/interface/widgets/replay/replay_widget.cpp +++ b/cockatrice/src/interface/widgets/replay/replay_widget.cpp @@ -28,6 +28,7 @@ ReplayWidget::ReplayWidget(QWidget *parent, GameReplay *replay) connect(replayManager, &ReplayManager::eventReplayed, this, &ReplayWidget::eventReplayed); connect(replayManager, &ReplayManager::replayFinished, this, &ReplayWidget::replayFinished); connect(replayManager, &ReplayManager::rewound, this, &ReplayWidget::rewound); + connect(replayManager, &ReplayManager::timeChanged, this, &ReplayWidget::timeChanged); // timeline widget timelineWidget = new ReplayTimelineWidget; @@ -96,6 +97,16 @@ ReplayWidget::ReplayWidget(QWidget *parent, GameReplay *replay) refreshShortcuts(); } +const QList &ReplayWidget::getReplayTimeline() const +{ + return replayManager->getReplayTimeline(); +} + +void ReplayWidget::seekToEvent(int eventIndex) +{ + replayManager->setTimeForEvent(eventIndex); +} + void ReplayWidget::replayFinished() { replayPlayButton->setChecked(false); diff --git a/cockatrice/src/interface/widgets/replay/replay_widget.h b/cockatrice/src/interface/widgets/replay/replay_widget.h index 74aace570..ec503b6f7 100644 --- a/cockatrice/src/interface/widgets/replay/replay_widget.h +++ b/cockatrice/src/interface/widgets/replay/replay_widget.h @@ -10,6 +10,7 @@ #include "../../../game/player/event_processing_options.h" +#include #include #include @@ -36,8 +37,14 @@ public: */ ReplayWidget(QWidget *parent, GameReplay *replay); + [[nodiscard]] const QList &getReplayTimeline() const; + +public slots: + void seekToEvent(int eventIndex); + signals: void rewound(); + void timeChanged(int time); void eventReplayed(const GameEventContainer &cont, EventProcessingOptions options); private: