[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 0fe4fd0d8c
commit b863a53b35
4 changed files with 52 additions and 13 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, 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);

View file

@ -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:

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->setTimeForEvent(eventIndex);
}
void ReplayWidget::replayFinished()
{
replayPlayButton->setChecked(false);

View file

@ -10,6 +10,7 @@
#include "../../../game/player/event_processing_options.h"
#include <QList>
#include <QWidget>
#include <qtmetamacros.h>
@ -36,8 +37,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: