[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->setSingleShot(true);
connect(rewindBufferingTimer, &QTimer::timeout, this, &ReplayManager::processRewind);
connect(rewindBufferingTimer, &QTimer::timeout, this, [this] { 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;
}
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,11 @@ void ReplayManager::handleBackwardsSkip(bool doRewindBuffering)
rewindBufferingTimer->start(SettingsCache::instance().userInterface().getRewindBufferingMs());
} else {
// otherwise, process the rewind immediately
processRewind();
processRewind(includeEventsAtTime);
}
}
void ReplayManager::processRewind()
void ReplayManager::processRewind(bool includeEventsAtTime)
{
// stop any queued-up rewinds
rewindBufferingTimer->stop();
@ -117,7 +119,7 @@ void ReplayManager::processRewind()
// process the rewind
currentEvent = 0;
emit rewound();
processNewEvents(BACKWARD_SKIP);
processNewEvents(BACKWARD_SKIP, includeEventsAtTime);
}
void ReplayManager::replayTimerTimeout()
@ -134,11 +136,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 +240,24 @@ void ReplayManager::setTime(int time)
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)
{
skipToTime(currentVisualTime + amount, amount < 0);

View file

@ -40,10 +40,10 @@ 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 processRewind();
void processNewEvents(PlaybackMode playbackMode);
void skipToTime(int newTime, bool doRewindBuffering, bool includeEventsAtTime = false);
void handleBackwardsSkip(bool doRewindBuffering, bool includeEventsAtTime = false);
void processRewind(bool includeEventsAtTime = false);
void processNewEvents(PlaybackMode playbackMode, bool includeEventsAtTime = false);
void handleSkipEmptySection();
private slots:
@ -73,6 +73,7 @@ public slots:
void startReplay();
void stopReplay();
void setTime(int time);
void setTimeToEvent(int eventIndex);
void skipByAmount(int amount); // use a negative amount to skip backwards
signals:

View file

@ -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<int> &ReplayWidget::getReplayTimeline() const
{
return replayManager->getReplayTimeline();
}
void ReplayWidget::seekToEvent(int eventIndex)
{
replayManager->setTimeToEvent(eventIndex);
}
void ReplayWidget::replayFinished()
{
replayPlayButton->setChecked(false);

View file

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