[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.
This commit is contained in:
Lukas Brübach 2026-10-20 22:14:07 +02:00
parent e08402ac94
commit 77f60e0526
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;
}
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,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: