mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-21 09:05:10 -07:00
[Replay] Refactor: extract replay playback logic into single class (#7060)
* [Replay] Refactor: consolidate replay logic into single class * fixes
This commit is contained in:
parent
ca1c063687
commit
b44dcf5951
12 changed files with 320 additions and 237 deletions
|
|
@ -230,6 +230,7 @@ set(cockatrice_SOURCES
|
||||||
src/interface/widgets/printing_selector/set_name_and_collectors_number_display_widget.cpp
|
src/interface/widgets/printing_selector/set_name_and_collectors_number_display_widget.cpp
|
||||||
src/interface/widgets/quick_settings/settings_button_widget.cpp
|
src/interface/widgets/quick_settings/settings_button_widget.cpp
|
||||||
src/interface/widgets/quick_settings/settings_popup_widget.cpp
|
src/interface/widgets/quick_settings/settings_popup_widget.cpp
|
||||||
|
src/interface/widgets/replay/replay_manager.cpp
|
||||||
src/interface/widgets/replay/replay_quick_settings_widget.cpp
|
src/interface/widgets/replay/replay_quick_settings_widget.cpp
|
||||||
src/interface/widgets/replay/replay_timeline_widget.cpp
|
src/interface/widgets/replay/replay_timeline_widget.cpp
|
||||||
src/interface/widgets/replay/replay_widget.cpp
|
src/interface/widgets/replay/replay_widget.cpp
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ AbstractClient *AbstractGame::getClientForPlayer(int playerId) const
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AbstractGame::loadReplay(GameReplay *replay)
|
void AbstractGame::loadReplay(const GameReplay *replay)
|
||||||
{
|
{
|
||||||
gameMetaInfo->setFromProto(replay->game_info());
|
gameMetaInfo->setFromProto(replay->game_info());
|
||||||
gameMetaInfo->setSpectatorsOmniscient(true);
|
gameMetaInfo->setSpectatorsOmniscient(true);
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ public:
|
||||||
|
|
||||||
AbstractClient *getClientForPlayer(int playerId) const;
|
AbstractClient *getClientForPlayer(int playerId) const;
|
||||||
|
|
||||||
void loadReplay(GameReplay *replay);
|
void loadReplay(const GameReplay *replay);
|
||||||
|
|
||||||
CardItem *getCard(int playerId, const QString &zoneName, int cardId) const;
|
CardItem *getCard(int playerId, const QString &zoneName, int cardId) const;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
#include "../interface/widgets/tabs/tab_game.h"
|
#include "../interface/widgets/tabs/tab_game.h"
|
||||||
|
|
||||||
Replay::Replay(QObject *_parent, GameReplay *_replay, bool isLocalGame) : AbstractGame(_parent)
|
Replay::Replay(QObject *_parent, const GameReplay *_replay, bool isLocalGame) : AbstractGame(_parent)
|
||||||
{
|
{
|
||||||
gameState = new GameState(this, 0, -1, isLocalGame, {}, false, false, -1, false);
|
gameState = new GameState(this, 0, -1, isLocalGame, {}, false, false, -1, false);
|
||||||
connect(gameMetaInfo, &GameMetaInfo::startedChanged, gameState, &GameState::onStartedChanged);
|
connect(gameMetaInfo, &GameMetaInfo::startedChanged, gameState, &GameState::onStartedChanged);
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ class Replay : public AbstractGame
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit Replay(QObject *_parent, GameReplay *_replay, bool isLocalGame);
|
explicit Replay(QObject *_parent, const GameReplay *_replay, bool isLocalGame);
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // COCKATRICE_REPLAY_H
|
#endif // COCKATRICE_REPLAY_H
|
||||||
|
|
|
||||||
178
cockatrice/src/interface/widgets/replay/replay_manager.cpp
Normal file
178
cockatrice/src/interface/widgets/replay/replay_manager.cpp
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
#include "replay_manager.h"
|
||||||
|
|
||||||
|
#include "../../../client/settings/cache_settings.h"
|
||||||
|
|
||||||
|
#include <QTimer>
|
||||||
|
#include <libcockatrice/settings/interface_settings.h>
|
||||||
|
|
||||||
|
static constexpr int TIMER_INTERVAL_MS = 200;
|
||||||
|
|
||||||
|
static QList<int> createReplayTimeline(const GameReplay *replay)
|
||||||
|
{
|
||||||
|
// Create list: event number -> time [ms]
|
||||||
|
unsigned int lastEventTimestamp = 0;
|
||||||
|
const int eventCount = replay->event_list_size();
|
||||||
|
|
||||||
|
QList<int> replayTimeline;
|
||||||
|
for (int i = 0; i < eventCount; ++i) {
|
||||||
|
int nextSecondIndex = i + 1;
|
||||||
|
while (nextSecondIndex < eventCount &&
|
||||||
|
replay->event_list(nextSecondIndex).seconds_elapsed() == lastEventTimestamp) {
|
||||||
|
++nextSecondIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distribute simultaneous events evenly across 1 second.
|
||||||
|
const int numberEventsThisSecond = nextSecondIndex - i;
|
||||||
|
for (int k = 0; k < numberEventsThisSecond; ++k) {
|
||||||
|
int eventMs = replay->event_list(i + k).seconds_elapsed() * 1000;
|
||||||
|
int distributionMs = static_cast<int>(static_cast<qreal>(k) / numberEventsThisSecond * 1000);
|
||||||
|
replayTimeline.append(eventMs + distributionMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextSecondIndex < eventCount) {
|
||||||
|
lastEventTimestamp = replay->event_list(nextSecondIndex).seconds_elapsed();
|
||||||
|
}
|
||||||
|
i += numberEventsThisSecond - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return replayTimeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReplayManager::ReplayManager(QObject *parent, GameReplay *replay)
|
||||||
|
: QObject(parent), replay(replay), replayTimeline(createReplayTimeline(replay))
|
||||||
|
{
|
||||||
|
maxTime = replayTimeline.isEmpty() ? 0 : replayTimeline.last();
|
||||||
|
|
||||||
|
replayTimer = new QTimer(this);
|
||||||
|
replayTimer->setInterval(TIMER_INTERVAL_MS);
|
||||||
|
connect(replayTimer, &QTimer::timeout, this, &ReplayManager::replayTimerTimeout);
|
||||||
|
|
||||||
|
rewindBufferingTimer = new QTimer(this);
|
||||||
|
rewindBufferingTimer->setSingleShot(true);
|
||||||
|
connect(rewindBufferingTimer, &QTimer::timeout, this, &ReplayManager::processRewind);
|
||||||
|
}
|
||||||
|
|
||||||
|
ReplayManager::~ReplayManager()
|
||||||
|
{
|
||||||
|
delete replay;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReplayManager::skipToTime(int newTime, bool doRewindBuffering)
|
||||||
|
{
|
||||||
|
// check boundary conditions
|
||||||
|
if (newTime < 0) {
|
||||||
|
newTime = 0;
|
||||||
|
}
|
||||||
|
if (newTime > maxTime) {
|
||||||
|
newTime = maxTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
} else {
|
||||||
|
processNewEvents(FORWARD_SKIP);
|
||||||
|
}
|
||||||
|
|
||||||
|
timeChanged(currentVisualTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Handles a backwards skip in the replay timeline.
|
||||||
|
*
|
||||||
|
* @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)
|
||||||
|
{
|
||||||
|
if (doRewindBuffering) {
|
||||||
|
// We use a one-shot timer to implement the rewind buffering.
|
||||||
|
// The rewind only happens once the timer runs out.
|
||||||
|
// If another backwards skip happens, the timer will just get reset instead of rewinding.
|
||||||
|
rewindBufferingTimer->stop();
|
||||||
|
rewindBufferingTimer->start(SettingsCache::instance().interface().getRewindBufferingMs());
|
||||||
|
} else {
|
||||||
|
// otherwise, process the rewind immediately
|
||||||
|
processRewind();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReplayManager::processRewind()
|
||||||
|
{
|
||||||
|
// stop any queued-up rewinds
|
||||||
|
rewindBufferingTimer->stop();
|
||||||
|
|
||||||
|
// process the rewind
|
||||||
|
currentEvent = 0;
|
||||||
|
emit rewound();
|
||||||
|
processNewEvents(BACKWARD_SKIP);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReplayManager::replayTimerTimeout()
|
||||||
|
{
|
||||||
|
currentVisualTime += TIMER_INTERVAL_MS;
|
||||||
|
|
||||||
|
processNewEvents(NORMAL_PLAYBACK);
|
||||||
|
|
||||||
|
timeChanged(currentVisualTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Processes all unprocessed events up to the current time. */
|
||||||
|
void ReplayManager::processNewEvents(PlaybackMode playbackMode)
|
||||||
|
{
|
||||||
|
currentProcessedTime = currentVisualTime;
|
||||||
|
|
||||||
|
while (currentEvent < replayTimeline.size() && replayTimeline[currentEvent] < currentProcessedTime) {
|
||||||
|
EventProcessingOptions options;
|
||||||
|
|
||||||
|
// backwards skip => always skip reveal windows
|
||||||
|
// forwards skip => skip reveal windows that don't happen within a big skip of the target
|
||||||
|
if (playbackMode == BACKWARD_SKIP || currentProcessedTime - replayTimeline[currentEvent] > BIG_SKIP_MS) {
|
||||||
|
options |= SKIP_REVEAL_WINDOW;
|
||||||
|
}
|
||||||
|
|
||||||
|
// backwards skip => always skip tap animation
|
||||||
|
if (playbackMode == BACKWARD_SKIP) {
|
||||||
|
options |= SKIP_TAP_ANIMATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit eventReplayed(replay->event_list(currentEvent), options);
|
||||||
|
++currentEvent;
|
||||||
|
}
|
||||||
|
if (currentEvent == replayTimeline.size()) {
|
||||||
|
emit replayFinished();
|
||||||
|
replayTimer->stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReplayManager::setTimeScaleFactor(qreal _timeScaleFactor)
|
||||||
|
{
|
||||||
|
timeScaleFactor = _timeScaleFactor;
|
||||||
|
int interval = std::max(1, qRound(TIMER_INTERVAL_MS / timeScaleFactor));
|
||||||
|
replayTimer->setInterval(interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReplayManager::startReplay()
|
||||||
|
{
|
||||||
|
replayTimer->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReplayManager::stopReplay()
|
||||||
|
{
|
||||||
|
replayTimer->stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReplayManager::setTime(int time)
|
||||||
|
{
|
||||||
|
// don't buffer rewinds from clicks, since clicks usually don't happen fast enough to require buffering
|
||||||
|
skipToTime(time, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReplayManager::skipByAmount(int amount)
|
||||||
|
{
|
||||||
|
skipToTime(currentVisualTime + amount, amount < 0);
|
||||||
|
}
|
||||||
80
cockatrice/src/interface/widgets/replay/replay_manager.h
Normal file
80
cockatrice/src/interface/widgets/replay/replay_manager.h
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
#ifndef COCKATRICE_REPLAY_MANAGER_H
|
||||||
|
#define COCKATRICE_REPLAY_MANAGER_H
|
||||||
|
|
||||||
|
#include "../../../game/player/event_processing_options.h"
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <libcockatrice/protocol/pb/game_replay.pb.h>
|
||||||
|
|
||||||
|
class GameReplay;
|
||||||
|
class QTimer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief This class handles all logic to do with playing back replays
|
||||||
|
*/
|
||||||
|
class ReplayManager : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
enum PlaybackMode
|
||||||
|
{
|
||||||
|
NORMAL_PLAYBACK,
|
||||||
|
FORWARD_SKIP,
|
||||||
|
BACKWARD_SKIP
|
||||||
|
};
|
||||||
|
|
||||||
|
GameReplay *replay;
|
||||||
|
QList<int> replayTimeline; ///< timestamp of each event, with the indexes corresponding
|
||||||
|
int maxTime;
|
||||||
|
|
||||||
|
QTimer *replayTimer;
|
||||||
|
QTimer *rewindBufferingTimer;
|
||||||
|
|
||||||
|
qreal timeScaleFactor = 1.0;
|
||||||
|
|
||||||
|
int currentVisualTime = 0; ///< time currently displayed by the timeline
|
||||||
|
int currentProcessedTime = 0; ///< time that events are currently processed up to. Could differ from visual time due
|
||||||
|
///< 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);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void replayTimerTimeout();
|
||||||
|
|
||||||
|
public:
|
||||||
|
static constexpr int SMALL_SKIP_MS = 1000;
|
||||||
|
static constexpr int BIG_SKIP_MS = 10000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param parent The parent QObject
|
||||||
|
* @param replay Cannot be null. Takes ownership of the object.
|
||||||
|
*/
|
||||||
|
explicit ReplayManager(QObject *parent, GameReplay *replay);
|
||||||
|
|
||||||
|
~ReplayManager() override;
|
||||||
|
|
||||||
|
const QList<int> &getReplayTimeline() const
|
||||||
|
{
|
||||||
|
return replayTimeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setTimeScaleFactor(qreal _timeScaleFactor);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void startReplay();
|
||||||
|
void stopReplay();
|
||||||
|
void setTime(int time);
|
||||||
|
void skipByAmount(int amount); // use a negative amount to skip backwards
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void timeChanged(int time);
|
||||||
|
void eventReplayed(const GameEventContainer &cont, EventProcessingOptions options);
|
||||||
|
void replayFinished();
|
||||||
|
void rewound();
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // COCKATRICE_REPLAY_MANAGER_H
|
||||||
|
|
@ -4,26 +4,19 @@
|
||||||
|
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPainterPath>
|
#include <QPainterPath>
|
||||||
#include <QTimer>
|
|
||||||
#include <libcockatrice/settings/interface_settings.h>
|
|
||||||
|
|
||||||
ReplayTimelineWidget::ReplayTimelineWidget(QWidget *parent)
|
static constexpr int BIN_LENGTH = 5000;
|
||||||
: QWidget(parent), maxBinValue(1), maxTime(1), timeScaleFactor(1.0), currentVisualTime(0), currentProcessedTime(0),
|
static constexpr int MIN_RESOLUTION_MS = 1000;
|
||||||
currentEvent(0)
|
|
||||||
|
ReplayTimelineWidget::ReplayTimelineWidget(QWidget *parent) : QWidget(parent)
|
||||||
{
|
{
|
||||||
replayTimer = new QTimer(this);
|
|
||||||
replayTimer->setInterval(TIMER_INTERVAL_MS);
|
|
||||||
connect(replayTimer, &QTimer::timeout, this, &ReplayTimelineWidget::replayTimerTimeout);
|
|
||||||
|
|
||||||
rewindBufferingTimer = new QTimer(this);
|
|
||||||
rewindBufferingTimer->setSingleShot(true);
|
|
||||||
connect(rewindBufferingTimer, &QTimer::timeout, this, &ReplayTimelineWidget::processRewind);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReplayTimelineWidget::setTimeline(const QList<int> &_replayTimeline)
|
void ReplayTimelineWidget::setTimeline(const QList<int> &replayTimeline)
|
||||||
{
|
{
|
||||||
replayTimeline = _replayTimeline;
|
|
||||||
histogram.clear();
|
histogram.clear();
|
||||||
|
currentTime = 0;
|
||||||
|
|
||||||
int binEndTime = BIN_LENGTH - 1;
|
int binEndTime = BIN_LENGTH - 1;
|
||||||
int binValue = 0;
|
int binValue = 0;
|
||||||
for (int i : replayTimeline) {
|
for (int i : replayTimeline) {
|
||||||
|
|
@ -66,7 +59,7 @@ void ReplayTimelineWidget::paintEvent(QPaintEvent * /* event */)
|
||||||
painter.fillPath(path, Qt::black);
|
painter.fillPath(path, Qt::black);
|
||||||
|
|
||||||
const QColor barColor = QColor::fromHsv(120, 255, 255, 100);
|
const QColor barColor = QColor::fromHsv(120, 255, 255, 100);
|
||||||
quint64 w = (quint64)(width() - 1) * (quint64)currentVisualTime / maxTime;
|
quint64 w = (quint64)(width() - 1) * (quint64)currentTime / maxTime;
|
||||||
painter.fillRect(0, 0, static_cast<int>(w), height() - 1, barColor);
|
painter.fillRect(0, 0, static_cast<int>(w), height() - 1, barColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,63 +70,24 @@ void ReplayTimelineWidget::mousePressEvent(QMouseEvent *event)
|
||||||
#else
|
#else
|
||||||
int newTime = static_cast<int>((qint64)maxTime * (qint64)event->x() / width());
|
int newTime = static_cast<int>((qint64)maxTime * (qint64)event->x() / width());
|
||||||
#endif
|
#endif
|
||||||
// don't buffer rewinds from clicks, since clicks usually don't happen fast enough to require buffering
|
emit timeClicked(newTime);
|
||||||
skipToTime(newTime, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReplayTimelineWidget::skipToTime(int newTime, bool doRewindBuffering)
|
void ReplayTimelineWidget::setCurrentTime(int time)
|
||||||
{
|
{
|
||||||
// check boundary conditions
|
int newTime = qBound(0, time, maxTime);
|
||||||
if (newTime < 0) {
|
|
||||||
newTime = 0;
|
if (currentTime == newTime) {
|
||||||
}
|
return;
|
||||||
if (newTime > maxTime) {
|
|
||||||
newTime = maxTime;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
newTime -= newTime % TIMER_INTERVAL_MS; // Time should always be a multiple of the interval
|
bool doUpdate = currentTime / MIN_RESOLUTION_MS != newTime / MIN_RESOLUTION_MS;
|
||||||
|
|
||||||
const bool isBackwardsSkip = newTime < currentProcessedTime;
|
currentTime = newTime;
|
||||||
currentVisualTime = newTime;
|
|
||||||
|
|
||||||
if (isBackwardsSkip) {
|
if (doUpdate) {
|
||||||
handleBackwardsSkip(doRewindBuffering);
|
update();
|
||||||
} else {
|
|
||||||
processNewEvents(FORWARD_SKIP);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Handles a backwards skip in the replay timeline.
|
|
||||||
*
|
|
||||||
* @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 ReplayTimelineWidget::handleBackwardsSkip(bool doRewindBuffering)
|
|
||||||
{
|
|
||||||
if (doRewindBuffering) {
|
|
||||||
// We use a one-shot timer to implement the rewind buffering.
|
|
||||||
// The rewind only happens once the timer runs out.
|
|
||||||
// If another backwards skip happens, the timer will just get reset instead of rewinding.
|
|
||||||
rewindBufferingTimer->stop();
|
|
||||||
rewindBufferingTimer->start(SettingsCache::instance().interface().getRewindBufferingMs());
|
|
||||||
} else {
|
|
||||||
// otherwise, process the rewind immediately
|
|
||||||
processRewind();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ReplayTimelineWidget::processRewind()
|
|
||||||
{
|
|
||||||
// stop any queued-up rewinds
|
|
||||||
rewindBufferingTimer->stop();
|
|
||||||
|
|
||||||
// process the rewind
|
|
||||||
currentEvent = 0;
|
|
||||||
emit rewound();
|
|
||||||
processNewEvents(BACKWARD_SKIP);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QSize ReplayTimelineWidget::sizeHint() const
|
QSize ReplayTimelineWidget::sizeHint() const
|
||||||
|
|
@ -145,64 +99,3 @@ QSize ReplayTimelineWidget::minimumSizeHint() const
|
||||||
{
|
{
|
||||||
return {400, 50};
|
return {400, 50};
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReplayTimelineWidget::replayTimerTimeout()
|
|
||||||
{
|
|
||||||
currentVisualTime += TIMER_INTERVAL_MS;
|
|
||||||
|
|
||||||
processNewEvents(NORMAL_PLAYBACK);
|
|
||||||
|
|
||||||
if (!(currentVisualTime % 1000)) {
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @brief Processes all unprocessed events up to the current time. */
|
|
||||||
void ReplayTimelineWidget::processNewEvents(PlaybackMode playbackMode)
|
|
||||||
{
|
|
||||||
currentProcessedTime = currentVisualTime;
|
|
||||||
|
|
||||||
while ((currentEvent < replayTimeline.size()) && (replayTimeline[currentEvent] < currentProcessedTime)) {
|
|
||||||
EventProcessingOptions options;
|
|
||||||
|
|
||||||
// backwards skip => always skip reveal windows
|
|
||||||
// forwards skip => skip reveal windows that don't happen within a big skip of the target
|
|
||||||
if (playbackMode == BACKWARD_SKIP || currentProcessedTime - replayTimeline[currentEvent] > BIG_SKIP_MS) {
|
|
||||||
options |= SKIP_REVEAL_WINDOW;
|
|
||||||
}
|
|
||||||
|
|
||||||
// backwards skip => always skip tap animation
|
|
||||||
if (playbackMode == BACKWARD_SKIP) {
|
|
||||||
options |= SKIP_TAP_ANIMATION;
|
|
||||||
}
|
|
||||||
|
|
||||||
emit processNextEvent(options);
|
|
||||||
++currentEvent;
|
|
||||||
}
|
|
||||||
if (currentEvent == replayTimeline.size()) {
|
|
||||||
emit replayFinished();
|
|
||||||
replayTimer->stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ReplayTimelineWidget::setTimeScaleFactor(qreal _timeScaleFactor)
|
|
||||||
{
|
|
||||||
timeScaleFactor = _timeScaleFactor;
|
|
||||||
int interval = std::max(1, qRound(TIMER_INTERVAL_MS / timeScaleFactor));
|
|
||||||
replayTimer->setInterval(interval);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ReplayTimelineWidget::startReplay()
|
|
||||||
{
|
|
||||||
replayTimer->start();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ReplayTimelineWidget::stopReplay()
|
|
||||||
{
|
|
||||||
replayTimer->stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ReplayTimelineWidget::skipByAmount(int amount)
|
|
||||||
{
|
|
||||||
skipToTime(currentVisualTime + amount, amount < 0);
|
|
||||||
}
|
|
||||||
|
|
@ -18,57 +18,25 @@ class QTimer;
|
||||||
class ReplayTimelineWidget : public QWidget
|
class ReplayTimelineWidget : public QWidget
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void processNextEvent(EventProcessingOptions options);
|
void timeClicked(int newTime);
|
||||||
void replayFinished();
|
|
||||||
void rewound();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum PlaybackMode
|
|
||||||
{
|
|
||||||
NORMAL_PLAYBACK,
|
|
||||||
FORWARD_SKIP,
|
|
||||||
BACKWARD_SKIP
|
|
||||||
};
|
|
||||||
|
|
||||||
static constexpr int TIMER_INTERVAL_MS = 200;
|
|
||||||
static constexpr int BIN_LENGTH = 5000;
|
|
||||||
|
|
||||||
QTimer *replayTimer;
|
|
||||||
QTimer *rewindBufferingTimer;
|
|
||||||
QList<int> replayTimeline;
|
|
||||||
QList<int> histogram;
|
QList<int> histogram;
|
||||||
int maxBinValue, maxTime;
|
int maxBinValue = 1;
|
||||||
qreal timeScaleFactor;
|
int maxTime = 1;
|
||||||
int currentVisualTime; // time currently displayed by the timeline
|
|
||||||
int currentProcessedTime; // time that events are currently processed up to. Could differ from visual time due to
|
|
||||||
// rewind buffering
|
|
||||||
int currentEvent;
|
|
||||||
|
|
||||||
void skipToTime(int newTime, bool doRewindBuffering);
|
int currentTime = 0;
|
||||||
void handleBackwardsSkip(bool doRewindBuffering);
|
|
||||||
void processRewind();
|
|
||||||
void processNewEvents(PlaybackMode playbackMode);
|
|
||||||
private slots:
|
|
||||||
void replayTimerTimeout();
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
static constexpr int SMALL_SKIP_MS = 1000;
|
|
||||||
static constexpr int BIG_SKIP_MS = 10000;
|
|
||||||
|
|
||||||
explicit ReplayTimelineWidget(QWidget *parent = nullptr);
|
explicit ReplayTimelineWidget(QWidget *parent = nullptr);
|
||||||
void setTimeline(const QList<int> &_replayTimeline);
|
void setTimeline(const QList<int> &replayTimeline);
|
||||||
[[nodiscard]] QSize sizeHint() const override;
|
[[nodiscard]] QSize sizeHint() const override;
|
||||||
[[nodiscard]] QSize minimumSizeHint() const override;
|
[[nodiscard]] QSize minimumSizeHint() const override;
|
||||||
void setTimeScaleFactor(qreal _timeScaleFactor);
|
|
||||||
[[nodiscard]] int getCurrentEvent() const
|
|
||||||
{
|
|
||||||
return currentEvent;
|
|
||||||
}
|
|
||||||
public slots:
|
public slots:
|
||||||
void startReplay();
|
void setCurrentTime(int time);
|
||||||
void stopReplay();
|
|
||||||
void skipByAmount(int amount); // use a negative amount to skip backwards
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void paintEvent(QPaintEvent *event) override;
|
void paintEvent(QPaintEvent *event) override;
|
||||||
|
|
|
||||||
|
|
@ -1,70 +1,50 @@
|
||||||
#include "replay_widget.h"
|
#include "replay_widget.h"
|
||||||
|
|
||||||
|
#include "../../../client/settings/cache_settings.h"
|
||||||
#include "../../../client/settings/shortcuts_settings.h"
|
#include "../../../client/settings/shortcuts_settings.h"
|
||||||
#include "../interface/widgets/tabs/tab_game.h"
|
#include "../interface/widgets/tabs/tab_game.h"
|
||||||
|
#include "replay_manager.h"
|
||||||
#include "replay_quick_settings_widget.h"
|
#include "replay_quick_settings_widget.h"
|
||||||
|
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QToolButton>
|
#include <QToolButton>
|
||||||
|
|
||||||
ReplayWidget::ReplayWidget(TabGame *parent, GameReplay *_replay)
|
ReplayWidget::ReplayWidget(QWidget *parent, GameReplay *replay)
|
||||||
: QWidget(parent), game(parent), replay(_replay), replayPlayButton(nullptr), replayFastForwardButton(nullptr),
|
: QWidget(parent), replayPlayButton(nullptr), replayFastForwardButton(nullptr), aReplaySkipForward(nullptr),
|
||||||
aReplaySkipForward(nullptr), aReplaySkipBackward(nullptr), aReplaySkipForwardBig(nullptr),
|
aReplaySkipBackward(nullptr), aReplaySkipForwardBig(nullptr), aReplaySkipBackwardBig(nullptr)
|
||||||
aReplaySkipBackwardBig(nullptr)
|
|
||||||
{
|
{
|
||||||
if (replay) {
|
// replay manager
|
||||||
game->getGame()->loadReplay(replay);
|
replayManager = new ReplayManager(this, replay);
|
||||||
|
connect(replayManager, &ReplayManager::eventReplayed, this, &ReplayWidget::eventReplayed);
|
||||||
// Create list: event number -> time [ms]
|
connect(replayManager, &ReplayManager::replayFinished, this, &ReplayWidget::replayFinished);
|
||||||
// Distribute simultaneous events evenly across 1 second.
|
connect(replayManager, &ReplayManager::rewound, this, &ReplayWidget::rewound);
|
||||||
unsigned int lastEventTimestamp = 0;
|
|
||||||
const int eventCount = replay->event_list_size();
|
|
||||||
for (int i = 0; i < eventCount; ++i) {
|
|
||||||
int j = i + 1;
|
|
||||||
while ((j < eventCount) && (replay->event_list(j).seconds_elapsed() == lastEventTimestamp)) {
|
|
||||||
++j;
|
|
||||||
}
|
|
||||||
|
|
||||||
const int numberEventsThisSecond = j - i;
|
|
||||||
for (int k = 0; k < numberEventsThisSecond; ++k) {
|
|
||||||
replayTimeline.append(replay->event_list(i + k).seconds_elapsed() * 1000 +
|
|
||||||
(int)((qreal)k / (qreal)numberEventsThisSecond * 1000));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (j < eventCount) {
|
|
||||||
lastEventTimestamp = replay->event_list(j).seconds_elapsed();
|
|
||||||
}
|
|
||||||
i += numberEventsThisSecond - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// timeline widget
|
// timeline widget
|
||||||
timelineWidget = new ReplayTimelineWidget;
|
timelineWidget = new ReplayTimelineWidget;
|
||||||
timelineWidget->setTimeline(replayTimeline);
|
timelineWidget->setTimeline(replayManager->getReplayTimeline());
|
||||||
connect(timelineWidget, &ReplayTimelineWidget::processNextEvent, this, &ReplayWidget::replayNextEvent);
|
connect(replayManager, &ReplayManager::timeChanged, timelineWidget, &ReplayTimelineWidget::setCurrentTime);
|
||||||
connect(timelineWidget, &ReplayTimelineWidget::replayFinished, this, &ReplayWidget::replayFinished);
|
connect(timelineWidget, &ReplayTimelineWidget::timeClicked, replayManager, &ReplayManager::setTime);
|
||||||
connect(timelineWidget, &ReplayTimelineWidget::rewound, this, &ReplayWidget::replayRewind);
|
|
||||||
|
|
||||||
// timeline skip shortcuts
|
// timeline skip shortcuts
|
||||||
aReplaySkipForward = new QAction(timelineWidget);
|
aReplaySkipForward = new QAction(timelineWidget);
|
||||||
timelineWidget->addAction(aReplaySkipForward);
|
timelineWidget->addAction(aReplaySkipForward);
|
||||||
connect(aReplaySkipForward, &QAction::triggered, this,
|
connect(aReplaySkipForward, &QAction::triggered, this,
|
||||||
[this] { timelineWidget->skipByAmount(ReplayTimelineWidget::SMALL_SKIP_MS); });
|
[this] { replayManager->skipByAmount(ReplayManager::SMALL_SKIP_MS); });
|
||||||
|
|
||||||
aReplaySkipBackward = new QAction(timelineWidget);
|
aReplaySkipBackward = new QAction(timelineWidget);
|
||||||
timelineWidget->addAction(aReplaySkipBackward);
|
timelineWidget->addAction(aReplaySkipBackward);
|
||||||
connect(aReplaySkipBackward, &QAction::triggered, this,
|
connect(aReplaySkipBackward, &QAction::triggered, this,
|
||||||
[this] { timelineWidget->skipByAmount(-ReplayTimelineWidget::SMALL_SKIP_MS); });
|
[this] { replayManager->skipByAmount(-ReplayManager::SMALL_SKIP_MS); });
|
||||||
|
|
||||||
aReplaySkipForwardBig = new QAction(timelineWidget);
|
aReplaySkipForwardBig = new QAction(timelineWidget);
|
||||||
timelineWidget->addAction(aReplaySkipForwardBig);
|
timelineWidget->addAction(aReplaySkipForwardBig);
|
||||||
connect(aReplaySkipForwardBig, &QAction::triggered, this,
|
connect(aReplaySkipForwardBig, &QAction::triggered, this,
|
||||||
[this] { timelineWidget->skipByAmount(ReplayTimelineWidget::BIG_SKIP_MS); });
|
[this] { replayManager->skipByAmount(ReplayManager::BIG_SKIP_MS); });
|
||||||
|
|
||||||
aReplaySkipBackwardBig = new QAction(timelineWidget);
|
aReplaySkipBackwardBig = new QAction(timelineWidget);
|
||||||
timelineWidget->addAction(aReplaySkipBackwardBig);
|
timelineWidget->addAction(aReplaySkipBackwardBig);
|
||||||
connect(aReplaySkipBackwardBig, &QAction::triggered, this,
|
connect(aReplaySkipBackwardBig, &QAction::triggered, this,
|
||||||
[this] { timelineWidget->skipByAmount(-ReplayTimelineWidget::BIG_SKIP_MS); });
|
[this] { replayManager->skipByAmount(-ReplayManager::BIG_SKIP_MS); });
|
||||||
|
|
||||||
// buttons
|
// buttons
|
||||||
replayPlayButton = new QToolButton;
|
replayPlayButton = new QToolButton;
|
||||||
|
|
@ -97,18 +77,11 @@ ReplayWidget::ReplayWidget(TabGame *parent, GameReplay *_replay)
|
||||||
setObjectName("replayControlWidget");
|
setObjectName("replayControlWidget");
|
||||||
setLayout(replayControlLayout);
|
setLayout(replayControlLayout);
|
||||||
|
|
||||||
connect(this, &ReplayWidget::requestChatAndPhaseReset, game, &TabGame::resetChatAndPhase);
|
|
||||||
|
|
||||||
connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this,
|
connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this,
|
||||||
&ReplayWidget::refreshShortcuts);
|
&ReplayWidget::refreshShortcuts);
|
||||||
refreshShortcuts();
|
refreshShortcuts();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReplayWidget::replayNextEvent(EventProcessingOptions options)
|
|
||||||
{
|
|
||||||
emit eventReplayed(replay->event_list(timelineWidget->getCurrentEvent()), options);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ReplayWidget::replayFinished()
|
void ReplayWidget::replayFinished()
|
||||||
{
|
{
|
||||||
replayPlayButton->setChecked(false);
|
replayPlayButton->setChecked(false);
|
||||||
|
|
@ -117,24 +90,16 @@ void ReplayWidget::replayFinished()
|
||||||
void ReplayWidget::replayPlayButtonToggled(bool checked)
|
void ReplayWidget::replayPlayButtonToggled(bool checked)
|
||||||
{
|
{
|
||||||
if (checked) { // start replay
|
if (checked) { // start replay
|
||||||
timelineWidget->startReplay();
|
replayManager->startReplay();
|
||||||
} else { // pause replay
|
} else { // pause replay
|
||||||
timelineWidget->stopReplay();
|
replayManager->stopReplay();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReplayWidget::updateTimeScaleFactor(bool isFastForward)
|
void ReplayWidget::updateTimeScaleFactor(bool isFastForward)
|
||||||
{
|
{
|
||||||
qreal factor = isFastForward ? SettingsCache::instance().interface().getFastForwardSpeed() : 1.0;
|
qreal factor = isFastForward ? SettingsCache::instance().interface().getFastForwardSpeed() : 1.0;
|
||||||
timelineWidget->setTimeScaleFactor(factor);
|
replayManager->setTimeScaleFactor(factor);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Handles everything that needs to be reset when doing a replay rewind.
|
|
||||||
*/
|
|
||||||
void ReplayWidget::replayRewind()
|
|
||||||
{
|
|
||||||
emit requestChatAndPhaseReset();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReplayWidget::refreshShortcuts()
|
void ReplayWidget::refreshShortcuts()
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,12 @@
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include <libcockatrice/protocol/pb/game_replay.pb.h>
|
#include <libcockatrice/protocol/pb/game_replay.pb.h>
|
||||||
|
|
||||||
|
class ReplayManager;
|
||||||
class ReplayQuickSettingsWidget;
|
class ReplayQuickSettingsWidget;
|
||||||
class TabGame;
|
class TabGame;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The top-level that is put in the replay dock widget.
|
* @brief The top-level widget that is put in the replay dock widget.
|
||||||
* Contains the replay timeline as well as the buttons.
|
* Contains the replay timeline as well as the buttons.
|
||||||
*/
|
*/
|
||||||
class ReplayWidget : public QWidget
|
class ReplayWidget : public QWidget
|
||||||
|
|
@ -26,29 +27,28 @@ class ReplayWidget : public QWidget
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
ReplayWidget(TabGame *parent, GameReplay *replay);
|
/**
|
||||||
TabGame *game;
|
* @param parent The parent widget
|
||||||
GameReplay *replay;
|
* @param replay Cannot be null. Takes ownership of the replay.
|
||||||
|
*/
|
||||||
|
ReplayWidget(QWidget *parent, GameReplay *replay);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void requestChatAndPhaseReset();
|
void rewound();
|
||||||
void eventReplayed(const GameEventContainer &cont, EventProcessingOptions options);
|
void eventReplayed(const GameEventContainer &cont, EventProcessingOptions options);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Replay related members
|
ReplayManager *replayManager;
|
||||||
int currentReplayStep = 0;
|
|
||||||
QList<int> replayTimeline;
|
|
||||||
ReplayTimelineWidget *timelineWidget;
|
ReplayTimelineWidget *timelineWidget;
|
||||||
QToolButton *replayPlayButton, *replayFastForwardButton;
|
QToolButton *replayPlayButton, *replayFastForwardButton;
|
||||||
ReplayQuickSettingsWidget *settingsWidget;
|
ReplayQuickSettingsWidget *settingsWidget;
|
||||||
QAction *aReplaySkipForward, *aReplaySkipBackward, *aReplaySkipForwardBig, *aReplaySkipBackwardBig;
|
QAction *aReplaySkipForward, *aReplaySkipBackward, *aReplaySkipForwardBig, *aReplaySkipBackwardBig;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void replayNextEvent(EventProcessingOptions options);
|
|
||||||
void replayFinished();
|
void replayFinished();
|
||||||
void replayPlayButtonToggled(bool checked);
|
void replayPlayButtonToggled(bool checked);
|
||||||
void updateTimeScaleFactor(bool checked);
|
void updateTimeScaleFactor(bool checked);
|
||||||
void replayRewind();
|
|
||||||
void refreshShortcuts();
|
void refreshShortcuts();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -265,9 +265,6 @@ void TabGame::emitUserEvent()
|
||||||
|
|
||||||
TabGame::~TabGame()
|
TabGame::~TabGame()
|
||||||
{
|
{
|
||||||
if (replayWidget) {
|
|
||||||
delete replayWidget->replay;
|
|
||||||
}
|
|
||||||
for (auto &player : game->getPlayerManager()->getPlayers()) {
|
for (auto &player : game->getPlayerManager()->getPlayers()) {
|
||||||
player->clear();
|
player->clear();
|
||||||
}
|
}
|
||||||
|
|
@ -1183,6 +1180,7 @@ void TabGame::createReplayDock(GameReplay *replay)
|
||||||
replayDock->setWidget(replayWidget);
|
replayDock->setWidget(replayWidget);
|
||||||
replayDock->setFloating(false);
|
replayDock->setFloating(false);
|
||||||
|
|
||||||
|
connect(replayWidget, &ReplayWidget::rewound, this, &TabGame::resetChatAndPhase);
|
||||||
connect(replayWidget, &ReplayWidget::eventReplayed, game->getGameEventHandler(),
|
connect(replayWidget, &ReplayWidget::eventReplayed, game->getGameEventHandler(),
|
||||||
[this](const auto &event, auto options) {
|
[this](const auto &event, auto options) {
|
||||||
game->getGameEventHandler()->processGameEventContainer(event, nullptr, options);
|
game->getGameEventHandler()->processGameEventContainer(event, nullptr, options);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue