[Replay] Seek to the exact time of an event (#7610)

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.

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2026-11-04 05:56:32 +01:00 • committed by GitHub
parent 624163916f
commit 81a0b97240
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 56 additions and 15 deletions

View file

@ -57,7 +57,7 @@ ReplayManager::ReplayManager(QObject *parent, GameReplay *replay)
rewindBufferingTimer = new QTimer(this); rewindBufferingTimer = new QTimer(this);
rewindBufferingTimer->setSingleShot(true); rewindBufferingTimer->setSingleShot(true);
connect(rewindBufferingTimer, &QTimer::timeout, this, &ReplayManager::processRewind); connect(rewindBufferingTimer, &QTimer::timeout, this, [this] { processRewind(); });
} }
ReplayManager::~ReplayManager() ReplayManager::~ReplayManager()
@ -65,7 +65,7 @@ ReplayManager::~ReplayManager()
delete replay; delete replay;
} }
void ReplayManager::skipToTime(int newTime, bool doRewindBuffering) void ReplayManager::skipToTime(int newTime, bool doRewindBuffering, bool includeEventsAtTime)
{ {
// check boundary conditions // check boundary conditions
if (newTime < 0) { if (newTime < 0) {
@ -75,15 +75,17 @@ void ReplayManager::skipToTime(int newTime, bool doRewindBuffering)
newTime = maxTime; 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; const bool isBackwardsSkip = newTime < currentProcessedTime;
currentVisualTime = newTime; currentVisualTime = newTime;
if (isBackwardsSkip) { if (isBackwardsSkip) {
handleBackwardsSkip(doRewindBuffering); handleBackwardsSkip(doRewindBuffering, includeEventsAtTime);
} else { } else {
processNewEvents(FORWARD_SKIP); processNewEvents(FORWARD_SKIP, includeEventsAtTime);
} }
timeChanged(currentVisualTime); 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 * @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. * 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) { if (doRewindBuffering) {
// We use a one-shot timer to implement the rewind buffering. // We use a one-shot timer to implement the rewind buffering.
@ -105,11 +107,11 @@ void ReplayManager::handleBackwardsSkip(bool doRewindBuffering)
rewindBufferingTimer->start(SettingsCache::instance().userInterface().getRewindBufferingMs()); rewindBufferingTimer->start(SettingsCache::instance().userInterface().getRewindBufferingMs());
} else { } else {
// otherwise, process the rewind immediately // otherwise, process the rewind immediately
processRewind(); processRewind(includeEventsAtTime);
} }
} }
void ReplayManager::processRewind() void ReplayManager::processRewind(bool includeEventsAtTime)
{ {
// stop any queued-up rewinds // stop any queued-up rewinds
rewindBufferingTimer->stop(); rewindBufferingTimer->stop();
@ -117,7 +119,7 @@ void ReplayManager::processRewind()
// process the rewind // process the rewind
currentEvent = 0; currentEvent = 0;
emit rewound(); emit rewound();
processNewEvents(BACKWARD_SKIP); processNewEvents(BACKWARD_SKIP, includeEventsAtTime);
} }
void ReplayManager::replayTimerTimeout() void ReplayManager::replayTimerTimeout()
@ -134,11 +136,13 @@ void ReplayManager::replayTimerTimeout()
} }
/** @brief Processes all unprocessed events up to the current time. */ /** @brief Processes all unprocessed events up to the current time. */
void ReplayManager::processNewEvents(PlaybackMode playbackMode) void ReplayManager::processNewEvents(PlaybackMode playbackMode, bool includeEventsAtTime)
{ {
currentProcessedTime = currentVisualTime; currentProcessedTime = currentVisualTime;
while (currentEvent < replayTimeline.size() && replayTimeline[currentEvent] < currentProcessedTime) { while (currentEvent < replayTimeline.size() &&
(replayTimeline[currentEvent] < currentProcessedTime ||
(includeEventsAtTime && replayTimeline[currentEvent] == currentProcessedTime))) {
EventProcessingOptions options; EventProcessingOptions options;
// backwards skip => always skip reveal windows // backwards skip => always skip reveal windows
@ -236,6 +240,24 @@ void ReplayManager::setTime(int time)
skipToTime(time, false); skipToTime(time, false);
} }
void ReplayManager::setTimeToEvent(int eventIndex)
{
if (eventIndex < 0 || eventIndex >= replayTimeline.size()) {
return;
}
// Flush a pending buffered rewind before seeking, otherwise its timer fires after this seek and rebuilds the
// replay state from an outdated currentVisualTime, dropping the event we seek to. Stopping the timer instead
// of processing it is not enough: playback ticks advance currentProcessedTime without rewinding currentEvent,
// so the board could still be ahead of the target.
if (rewindBufferingTimer->isActive()) {
processRewind();
}
// Unlike a timeline seek, jumping to an event should apply that event immediately.
skipToTime(replayTimeline.at(eventIndex), false, true);
}
void ReplayManager::skipByAmount(int amount) void ReplayManager::skipByAmount(int amount)
{ {
skipToTime(currentVisualTime + amount, amount < 0); skipToTime(currentVisualTime + amount, amount < 0);

View file

@ -40,10 +40,10 @@ class ReplayManager : public QObject
///< to rewind buffering ///< to rewind buffering
int currentEvent = 0; ///< current event's index int currentEvent = 0; ///< current event's index
void skipToTime(int newTime, bool doRewindBuffering); void skipToTime(int newTime, bool doRewindBuffering, bool includeEventsAtTime = false);
void handleBackwardsSkip(bool doRewindBuffering); void handleBackwardsSkip(bool doRewindBuffering, bool includeEventsAtTime = false);
void processRewind(); void processRewind(bool includeEventsAtTime = false);
void processNewEvents(PlaybackMode playbackMode); void processNewEvents(PlaybackMode playbackMode, bool includeEventsAtTime = false);
void handleSkipEmptySection(); void handleSkipEmptySection();
private slots: private slots:
@ -73,6 +73,7 @@ public slots:
void startReplay(); void startReplay();
void stopReplay(); void stopReplay();
void setTime(int time); void setTime(int time);
void setTimeToEvent(int eventIndex);
void skipByAmount(int amount); // use a negative amount to skip backwards void skipByAmount(int amount); // use a negative amount to skip backwards
signals: signals:

View file

@ -28,6 +28,7 @@ ReplayWidget::ReplayWidget(QWidget *parent, GameReplay *replay)
connect(replayManager, &ReplayManager::eventReplayed, this, &ReplayWidget::eventReplayed); connect(replayManager, &ReplayManager::eventReplayed, this, &ReplayWidget::eventReplayed);
connect(replayManager, &ReplayManager::replayFinished, this, &ReplayWidget::replayFinished); connect(replayManager, &ReplayManager::replayFinished, this, &ReplayWidget::replayFinished);
connect(replayManager, &ReplayManager::rewound, this, &ReplayWidget::rewound); connect(replayManager, &ReplayManager::rewound, this, &ReplayWidget::rewound);
connect(replayManager, &ReplayManager::timeChanged, this, &ReplayWidget::timeChanged);
// timeline widget // timeline widget
timelineWidget = new ReplayTimelineWidget; timelineWidget = new ReplayTimelineWidget;
@ -96,6 +97,16 @@ ReplayWidget::ReplayWidget(QWidget *parent, GameReplay *replay)
refreshShortcuts(); refreshShortcuts();
} }
const QList<int> &ReplayWidget::getReplayTimeline() const
{
return replayManager->getReplayTimeline();
}
void ReplayWidget::seekToEvent(int eventIndex)
{
replayManager->setTimeToEvent(eventIndex);
}
void ReplayWidget::replayFinished() void ReplayWidget::replayFinished()
{ {
replayPlayButton->setChecked(false); replayPlayButton->setChecked(false);

View file

@ -8,6 +8,7 @@
#ifndef REPLAY_WIDGET_H #ifndef REPLAY_WIDGET_H
#define REPLAY_WIDGET_H #define REPLAY_WIDGET_H
#include <QList>
#include <QWidget> #include <QWidget>
#include <libcockatrice/game/player/event_processing_options.h> #include <libcockatrice/game/player/event_processing_options.h>
#include <qtmetamacros.h> #include <qtmetamacros.h>
@ -35,8 +36,14 @@ public:
*/ */
ReplayWidget(QWidget *parent, GameReplay *replay); ReplayWidget(QWidget *parent, GameReplay *replay);
[[nodiscard]] const QList<int> &getReplayTimeline() const;
public slots:
void seekToEvent(int eventIndex);
signals: signals:
void rewound(); void rewound();
void timeChanged(int time);
void eventReplayed(const GameEventContainer &cont, EventProcessingOptions options); void eventReplayed(const GameEventContainer &cont, EventProcessingOptions options);
private: private: