mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-24 02:13:02 -07:00
Compare commits
5 commits
6766b0af89
...
1ed9823b56
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ed9823b56 | ||
|
|
b44dcf5951 | ||
|
|
ca1c063687 | ||
|
|
a4557454a7 | ||
|
|
486b8e8407 |
31 changed files with 462 additions and 316 deletions
|
|
@ -99,6 +99,7 @@ set(cockatrice_SOURCES
|
|||
src/game_graphics/player/menu/sideboard_menu.cpp
|
||||
src/game_graphics/player/menu/tally_menu.cpp
|
||||
src/game_graphics/player/menu/utility_menu.cpp
|
||||
src/game_graphics/tally/stats_tally.cpp
|
||||
src/game_graphics/tally/subtype_tally.cpp
|
||||
src/game_graphics/tally/tally.cpp
|
||||
src/game/player/player_actions.cpp
|
||||
|
|
@ -229,6 +230,7 @@ set(cockatrice_SOURCES
|
|||
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_popup_widget.cpp
|
||||
src/interface/widgets/replay/replay_manager.cpp
|
||||
src/interface/widgets/replay/replay_quick_settings_widget.cpp
|
||||
src/interface/widgets/replay/replay_timeline_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->setSpectatorsOmniscient(true);
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ public:
|
|||
|
||||
AbstractClient *getClientForPlayer(int playerId) const;
|
||||
|
||||
void loadReplay(GameReplay *replay);
|
||||
void loadReplay(const GameReplay *replay);
|
||||
|
||||
CardItem *getCard(int playerId, const QString &zoneName, int cardId) const;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
#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);
|
||||
connect(gameMetaInfo, &GameMetaInfo::startedChanged, gameState, &GameState::onStartedChanged);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class Replay : public AbstractGame
|
|||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit Replay(QObject *_parent, GameReplay *_replay, bool isLocalGame);
|
||||
explicit Replay(QObject *_parent, const GameReplay *_replay, bool isLocalGame);
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_REPLAY_H
|
||||
|
|
|
|||
|
|
@ -256,7 +256,7 @@ void GameView::updateTotalSelectionCount(const QSize &viewSize)
|
|||
GameScene *gameScene = static_cast<GameScene *>(scene());
|
||||
QList<TallyRow> entries = Tally::compute(gameScene->selectedCards(), tallyType);
|
||||
|
||||
if (entries.isEmpty() || count <= 1) {
|
||||
if (entries.isEmpty()) {
|
||||
tallyContainer->hide();
|
||||
cachedTallyRows.clear();
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -11,10 +11,12 @@ TallyMenu::TallyMenu()
|
|||
|
||||
aTallyNone = createTallyAction(TallyType::None);
|
||||
aTallySubtypes = createTallyAction(TallyType::Subtypes);
|
||||
aTallyTotalPower = createTallyAction(TallyType::TotalPower);
|
||||
|
||||
addAction(aTallyNone);
|
||||
addSeparator();
|
||||
addAction(aTallySubtypes);
|
||||
addAction(aTallyTotalPower);
|
||||
|
||||
retranslateUi();
|
||||
}
|
||||
|
|
@ -51,4 +53,5 @@ void TallyMenu::retranslateUi()
|
|||
|
||||
aTallyNone->setText(tr("None"));
|
||||
aTallySubtypes->setText(tr("Subtypes"));
|
||||
aTallyTotalPower->setText(tr("Total Power"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ private:
|
|||
|
||||
QAction *aTallyNone = nullptr;
|
||||
QAction *aTallySubtypes = nullptr;
|
||||
QAction *aTallyTotalPower = nullptr;
|
||||
|
||||
QAction *createTallyAction(TallyType tallyType);
|
||||
};
|
||||
|
|
|
|||
36
cockatrice/src/game_graphics/tally/stats_tally.cpp
Normal file
36
cockatrice/src/game_graphics/tally/stats_tally.cpp
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
#include "stats_tally.h"
|
||||
|
||||
#include "../board/card_item.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QList>
|
||||
#include <algorithm>
|
||||
|
||||
static int sumPowers(const QList<CardItem *> &cards)
|
||||
{
|
||||
// calculate total power;
|
||||
int total = 0;
|
||||
for (auto card : cards) {
|
||||
QVariantList parsed = CardItem::parsePT(card->getPT());
|
||||
if (!parsed.isEmpty()) {
|
||||
int power = parsed.first().toInt(); // toInt will default to 0 if it's not an int
|
||||
total += qMax(power, 0);
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
QList<TallyRow> StatsTally::computeTotalPower(const QList<CardItem *> &cards)
|
||||
{
|
||||
// don't bother if none of the cards have pt
|
||||
bool hasPT =
|
||||
std::any_of(cards.cbegin(), cards.cend(), [](const CardItem *card) { return !card->getPT().isEmpty(); });
|
||||
if (!hasPT) {
|
||||
return {};
|
||||
}
|
||||
|
||||
int total = sumPowers(cards);
|
||||
|
||||
QString name = QCoreApplication::translate("StatsTally", "Total Power");
|
||||
return {TallyRow{name, QString::number(total)}};
|
||||
}
|
||||
21
cockatrice/src/game_graphics/tally/stats_tally.h
Normal file
21
cockatrice/src/game_graphics/tally/stats_tally.h
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#ifndef COCKATRICE_STATS_TALLY_H
|
||||
#define COCKATRICE_STATS_TALLY_H
|
||||
#include "tally.h"
|
||||
|
||||
/**
|
||||
* @brief Extracts and tallies stats from selected cards.
|
||||
*/
|
||||
namespace StatsTally
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Sums the power of all selected cards
|
||||
*
|
||||
* @param cards The list of selected card items to analyze.
|
||||
* @return A single row containing the total, or an empty list if none of the cards have pt
|
||||
*/
|
||||
QList<TallyRow> computeTotalPower(const QList<CardItem *> &cards);
|
||||
|
||||
} // namespace StatsTally
|
||||
|
||||
#endif // COCKATRICE_STATS_TALLY_H
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
#include "tally.h"
|
||||
|
||||
#include "stats_tally.h"
|
||||
#include "subtype_tally.h"
|
||||
|
||||
TallyType Tally::intToType(int value)
|
||||
|
|
@ -18,6 +19,8 @@ QList<TallyRow> Tally::compute(const QList<CardItem *> &cards, const TallyType t
|
|||
return {};
|
||||
case TallyType::Subtypes:
|
||||
return SubtypeTally::countSubtypes(cards);
|
||||
case TallyType::TotalPower:
|
||||
return StatsTally::computeTotalPower(cards);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ enum class TallyType
|
|||
{
|
||||
None,
|
||||
Subtypes,
|
||||
MaxValue = Subtypes // sentinel value
|
||||
TotalPower,
|
||||
MaxValue = TotalPower // sentinel value
|
||||
};
|
||||
|
||||
namespace Tally
|
||||
|
|
|
|||
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 <QPainterPath>
|
||||
#include <QTimer>
|
||||
#include <libcockatrice/settings/interface_settings.h>
|
||||
|
||||
ReplayTimelineWidget::ReplayTimelineWidget(QWidget *parent)
|
||||
: QWidget(parent), maxBinValue(1), maxTime(1), timeScaleFactor(1.0), currentVisualTime(0), currentProcessedTime(0),
|
||||
currentEvent(0)
|
||||
static constexpr int BIN_LENGTH = 5000;
|
||||
static constexpr int MIN_RESOLUTION_MS = 1000;
|
||||
|
||||
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();
|
||||
currentTime = 0;
|
||||
|
||||
int binEndTime = BIN_LENGTH - 1;
|
||||
int binValue = 0;
|
||||
for (int i : replayTimeline) {
|
||||
|
|
@ -66,7 +59,7 @@ void ReplayTimelineWidget::paintEvent(QPaintEvent * /* event */)
|
|||
painter.fillPath(path, Qt::black);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -77,63 +70,24 @@ void ReplayTimelineWidget::mousePressEvent(QMouseEvent *event)
|
|||
#else
|
||||
int newTime = static_cast<int>((qint64)maxTime * (qint64)event->x() / width());
|
||||
#endif
|
||||
// don't buffer rewinds from clicks, since clicks usually don't happen fast enough to require buffering
|
||||
skipToTime(newTime, false);
|
||||
emit timeClicked(newTime);
|
||||
}
|
||||
|
||||
void ReplayTimelineWidget::skipToTime(int newTime, bool doRewindBuffering)
|
||||
void ReplayTimelineWidget::setCurrentTime(int time)
|
||||
{
|
||||
// check boundary conditions
|
||||
if (newTime < 0) {
|
||||
newTime = 0;
|
||||
}
|
||||
if (newTime > maxTime) {
|
||||
newTime = maxTime;
|
||||
int newTime = qBound(0, time, maxTime);
|
||||
|
||||
if (currentTime == newTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
currentVisualTime = newTime;
|
||||
currentTime = newTime;
|
||||
|
||||
if (isBackwardsSkip) {
|
||||
handleBackwardsSkip(doRewindBuffering);
|
||||
} else {
|
||||
processNewEvents(FORWARD_SKIP);
|
||||
if (doUpdate) {
|
||||
update();
|
||||
}
|
||||
|
||||
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
|
||||
|
|
@ -145,64 +99,3 @@ QSize ReplayTimelineWidget::minimumSizeHint() const
|
|||
{
|
||||
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
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
signals:
|
||||
void processNextEvent(EventProcessingOptions options);
|
||||
void replayFinished();
|
||||
void rewound();
|
||||
void timeClicked(int newTime);
|
||||
|
||||
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;
|
||||
int maxBinValue, maxTime;
|
||||
qreal timeScaleFactor;
|
||||
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;
|
||||
int maxBinValue = 1;
|
||||
int maxTime = 1;
|
||||
|
||||
void skipToTime(int newTime, bool doRewindBuffering);
|
||||
void handleBackwardsSkip(bool doRewindBuffering);
|
||||
void processRewind();
|
||||
void processNewEvents(PlaybackMode playbackMode);
|
||||
private slots:
|
||||
void replayTimerTimeout();
|
||||
int currentTime = 0;
|
||||
|
||||
public:
|
||||
static constexpr int SMALL_SKIP_MS = 1000;
|
||||
static constexpr int BIG_SKIP_MS = 10000;
|
||||
|
||||
explicit ReplayTimelineWidget(QWidget *parent = nullptr);
|
||||
void setTimeline(const QList<int> &_replayTimeline);
|
||||
void setTimeline(const QList<int> &replayTimeline);
|
||||
[[nodiscard]] QSize sizeHint() const override;
|
||||
[[nodiscard]] QSize minimumSizeHint() const override;
|
||||
void setTimeScaleFactor(qreal _timeScaleFactor);
|
||||
[[nodiscard]] int getCurrentEvent() const
|
||||
{
|
||||
return currentEvent;
|
||||
}
|
||||
|
||||
public slots:
|
||||
void startReplay();
|
||||
void stopReplay();
|
||||
void skipByAmount(int amount); // use a negative amount to skip backwards
|
||||
void setCurrentTime(int time);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
|
|
|
|||
|
|
@ -1,70 +1,50 @@
|
|||
#include "replay_widget.h"
|
||||
|
||||
#include "../../../client/settings/cache_settings.h"
|
||||
#include "../../../client/settings/shortcuts_settings.h"
|
||||
#include "../interface/widgets/tabs/tab_game.h"
|
||||
#include "replay_manager.h"
|
||||
#include "replay_quick_settings_widget.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QToolButton>
|
||||
|
||||
ReplayWidget::ReplayWidget(TabGame *parent, GameReplay *_replay)
|
||||
: QWidget(parent), game(parent), replay(_replay), replayPlayButton(nullptr), replayFastForwardButton(nullptr),
|
||||
aReplaySkipForward(nullptr), aReplaySkipBackward(nullptr), aReplaySkipForwardBig(nullptr),
|
||||
aReplaySkipBackwardBig(nullptr)
|
||||
ReplayWidget::ReplayWidget(QWidget *parent, GameReplay *replay)
|
||||
: QWidget(parent), replayPlayButton(nullptr), replayFastForwardButton(nullptr), aReplaySkipForward(nullptr),
|
||||
aReplaySkipBackward(nullptr), aReplaySkipForwardBig(nullptr), aReplaySkipBackwardBig(nullptr)
|
||||
{
|
||||
if (replay) {
|
||||
game->getGame()->loadReplay(replay);
|
||||
|
||||
// Create list: event number -> time [ms]
|
||||
// Distribute simultaneous events evenly across 1 second.
|
||||
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;
|
||||
}
|
||||
}
|
||||
// replay manager
|
||||
replayManager = new ReplayManager(this, replay);
|
||||
connect(replayManager, &ReplayManager::eventReplayed, this, &ReplayWidget::eventReplayed);
|
||||
connect(replayManager, &ReplayManager::replayFinished, this, &ReplayWidget::replayFinished);
|
||||
connect(replayManager, &ReplayManager::rewound, this, &ReplayWidget::rewound);
|
||||
|
||||
// timeline widget
|
||||
timelineWidget = new ReplayTimelineWidget;
|
||||
timelineWidget->setTimeline(replayTimeline);
|
||||
connect(timelineWidget, &ReplayTimelineWidget::processNextEvent, this, &ReplayWidget::replayNextEvent);
|
||||
connect(timelineWidget, &ReplayTimelineWidget::replayFinished, this, &ReplayWidget::replayFinished);
|
||||
connect(timelineWidget, &ReplayTimelineWidget::rewound, this, &ReplayWidget::replayRewind);
|
||||
timelineWidget->setTimeline(replayManager->getReplayTimeline());
|
||||
connect(replayManager, &ReplayManager::timeChanged, timelineWidget, &ReplayTimelineWidget::setCurrentTime);
|
||||
connect(timelineWidget, &ReplayTimelineWidget::timeClicked, replayManager, &ReplayManager::setTime);
|
||||
|
||||
// timeline skip shortcuts
|
||||
aReplaySkipForward = new QAction(timelineWidget);
|
||||
timelineWidget->addAction(aReplaySkipForward);
|
||||
connect(aReplaySkipForward, &QAction::triggered, this,
|
||||
[this] { timelineWidget->skipByAmount(ReplayTimelineWidget::SMALL_SKIP_MS); });
|
||||
[this] { replayManager->skipByAmount(ReplayManager::SMALL_SKIP_MS); });
|
||||
|
||||
aReplaySkipBackward = new QAction(timelineWidget);
|
||||
timelineWidget->addAction(aReplaySkipBackward);
|
||||
connect(aReplaySkipBackward, &QAction::triggered, this,
|
||||
[this] { timelineWidget->skipByAmount(-ReplayTimelineWidget::SMALL_SKIP_MS); });
|
||||
[this] { replayManager->skipByAmount(-ReplayManager::SMALL_SKIP_MS); });
|
||||
|
||||
aReplaySkipForwardBig = new QAction(timelineWidget);
|
||||
timelineWidget->addAction(aReplaySkipForwardBig);
|
||||
connect(aReplaySkipForwardBig, &QAction::triggered, this,
|
||||
[this] { timelineWidget->skipByAmount(ReplayTimelineWidget::BIG_SKIP_MS); });
|
||||
[this] { replayManager->skipByAmount(ReplayManager::BIG_SKIP_MS); });
|
||||
|
||||
aReplaySkipBackwardBig = new QAction(timelineWidget);
|
||||
timelineWidget->addAction(aReplaySkipBackwardBig);
|
||||
connect(aReplaySkipBackwardBig, &QAction::triggered, this,
|
||||
[this] { timelineWidget->skipByAmount(-ReplayTimelineWidget::BIG_SKIP_MS); });
|
||||
[this] { replayManager->skipByAmount(-ReplayManager::BIG_SKIP_MS); });
|
||||
|
||||
// buttons
|
||||
replayPlayButton = new QToolButton;
|
||||
|
|
@ -97,18 +77,11 @@ ReplayWidget::ReplayWidget(TabGame *parent, GameReplay *_replay)
|
|||
setObjectName("replayControlWidget");
|
||||
setLayout(replayControlLayout);
|
||||
|
||||
connect(this, &ReplayWidget::requestChatAndPhaseReset, game, &TabGame::resetChatAndPhase);
|
||||
|
||||
connect(&SettingsCache::instance().shortcuts(), &ShortcutsSettings::shortCutChanged, this,
|
||||
&ReplayWidget::refreshShortcuts);
|
||||
refreshShortcuts();
|
||||
}
|
||||
|
||||
void ReplayWidget::replayNextEvent(EventProcessingOptions options)
|
||||
{
|
||||
emit eventReplayed(replay->event_list(timelineWidget->getCurrentEvent()), options);
|
||||
}
|
||||
|
||||
void ReplayWidget::replayFinished()
|
||||
{
|
||||
replayPlayButton->setChecked(false);
|
||||
|
|
@ -117,24 +90,16 @@ void ReplayWidget::replayFinished()
|
|||
void ReplayWidget::replayPlayButtonToggled(bool checked)
|
||||
{
|
||||
if (checked) { // start replay
|
||||
timelineWidget->startReplay();
|
||||
replayManager->startReplay();
|
||||
} else { // pause replay
|
||||
timelineWidget->stopReplay();
|
||||
replayManager->stopReplay();
|
||||
}
|
||||
}
|
||||
|
||||
void ReplayWidget::updateTimeScaleFactor(bool isFastForward)
|
||||
{
|
||||
qreal factor = isFastForward ? SettingsCache::instance().interface().getFastForwardSpeed() : 1.0;
|
||||
timelineWidget->setTimeScaleFactor(factor);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handles everything that needs to be reset when doing a replay rewind.
|
||||
*/
|
||||
void ReplayWidget::replayRewind()
|
||||
{
|
||||
emit requestChatAndPhaseReset();
|
||||
replayManager->setTimeScaleFactor(factor);
|
||||
}
|
||||
|
||||
void ReplayWidget::refreshShortcuts()
|
||||
|
|
|
|||
|
|
@ -14,11 +14,12 @@
|
|||
#include <QWidget>
|
||||
#include <libcockatrice/protocol/pb/game_replay.pb.h>
|
||||
|
||||
class ReplayManager;
|
||||
class ReplayQuickSettingsWidget;
|
||||
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.
|
||||
*/
|
||||
class ReplayWidget : public QWidget
|
||||
|
|
@ -26,29 +27,28 @@ class ReplayWidget : public QWidget
|
|||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ReplayWidget(TabGame *parent, GameReplay *replay);
|
||||
TabGame *game;
|
||||
GameReplay *replay;
|
||||
/**
|
||||
* @param parent The parent widget
|
||||
* @param replay Cannot be null. Takes ownership of the replay.
|
||||
*/
|
||||
ReplayWidget(QWidget *parent, GameReplay *replay);
|
||||
|
||||
signals:
|
||||
void requestChatAndPhaseReset();
|
||||
void rewound();
|
||||
void eventReplayed(const GameEventContainer &cont, EventProcessingOptions options);
|
||||
|
||||
private:
|
||||
// Replay related members
|
||||
int currentReplayStep = 0;
|
||||
QList<int> replayTimeline;
|
||||
ReplayManager *replayManager;
|
||||
|
||||
ReplayTimelineWidget *timelineWidget;
|
||||
QToolButton *replayPlayButton, *replayFastForwardButton;
|
||||
ReplayQuickSettingsWidget *settingsWidget;
|
||||
QAction *aReplaySkipForward, *aReplaySkipBackward, *aReplaySkipForwardBig, *aReplaySkipBackwardBig;
|
||||
|
||||
private slots:
|
||||
void replayNextEvent(EventProcessingOptions options);
|
||||
void replayFinished();
|
||||
void replayPlayButtonToggled(bool checked);
|
||||
void updateTimeScaleFactor(bool checked);
|
||||
void replayRewind();
|
||||
void refreshShortcuts();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -241,15 +241,15 @@ void UserInterfaceSettingsPage::retranslateUi()
|
|||
visualDeckStoragePromptForConversionLabel.setText(
|
||||
tr("When adding a tag in the visual deck storage to a .txt deck:"));
|
||||
visualDeckStoragePromptForConversionSelector.setItemText(visualDeckStoragePromptForConversionIndexNone,
|
||||
tr("do nothing"));
|
||||
tr("Do nothing"));
|
||||
visualDeckStoragePromptForConversionSelector.setItemText(visualDeckStoragePromptForConversionIndexPrompt,
|
||||
tr("ask to convert to .cod"));
|
||||
tr("Ask to convert to .cod"));
|
||||
visualDeckStoragePromptForConversionSelector.setItemText(visualDeckStoragePromptForConversionIndexAlways,
|
||||
tr("always convert to .cod"));
|
||||
tr("Always convert to .cod"));
|
||||
defaultDeckEditorTypeLabel.setText(tr("Default deck editor type"));
|
||||
defaultDeckEditorTypeSelector.setItemText(TabSupervisor::ClassicDeckEditor, tr("Classic Deck Editor"));
|
||||
defaultDeckEditorTypeSelector.setItemText(TabSupervisor::VisualDeckEditor, tr("Visual Deck Editor"));
|
||||
replayGroupBox->setTitle(tr("Replay settings"));
|
||||
rewindBufferingMsLabel.setText(tr("Buffer time for backwards skip via shortcut:"));
|
||||
rewindBufferingMsBox.setSuffix(" ms");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -265,9 +265,6 @@ void TabGame::emitUserEvent()
|
|||
|
||||
TabGame::~TabGame()
|
||||
{
|
||||
if (replayWidget) {
|
||||
delete replayWidget->replay;
|
||||
}
|
||||
for (auto &player : game->getPlayerManager()->getPlayers()) {
|
||||
player->clear();
|
||||
}
|
||||
|
|
@ -1183,6 +1180,7 @@ void TabGame::createReplayDock(GameReplay *replay)
|
|||
replayDock->setWidget(replayWidget);
|
||||
replayDock->setFloating(false);
|
||||
|
||||
connect(replayWidget, &ReplayWidget::rewound, this, &TabGame::resetChatAndPhase);
|
||||
connect(replayWidget, &ReplayWidget::eventReplayed, game->getGameEventHandler(),
|
||||
[this](const auto &event, auto options) {
|
||||
game->getGameEventHandler()->processGameEventContainer(event, nullptr, options);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@
|
|||
#include <QRegularExpression>
|
||||
#include <QSharedPointer>
|
||||
#include <QString>
|
||||
#include <QVariant>
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
|
|
@ -24,7 +23,7 @@ using CardInfoPtr = QSharedPointer<CardInfo>;
|
|||
|
||||
namespace
|
||||
{
|
||||
QByteArray serializeProperties(const QVariantHash &props)
|
||||
QByteArray serializeProperties(const QHash<QString, QString> &props)
|
||||
{
|
||||
QByteArray blob;
|
||||
QDataStream out(&blob, QIODevice::WriteOnly);
|
||||
|
|
@ -48,7 +47,7 @@ void CardInfo::ensurePropertiesLoaded() const
|
|||
propertiesLoaded = true;
|
||||
}
|
||||
|
||||
const QVariantHash &CardInfo::getPropertiesHash() const
|
||||
const QHash<QString, QString> &CardInfo::getPropertiesHash() const
|
||||
{
|
||||
ensurePropertiesLoaded();
|
||||
return propertiesCache;
|
||||
|
|
@ -57,7 +56,7 @@ const QVariantHash &CardInfo::getPropertiesHash() const
|
|||
void CardInfo::setProperty(const QString &_name, const QString &_value)
|
||||
{
|
||||
ensurePropertiesLoaded();
|
||||
if (propertiesCache.value(_name).toString() == _value) {
|
||||
if (propertiesCache.value(_name) == _value) {
|
||||
return;
|
||||
}
|
||||
propertiesCache.insert(_name, _value);
|
||||
|
|
@ -65,7 +64,7 @@ void CardInfo::setProperty(const QString &_name, const QString &_value)
|
|||
emit cardInfoChanged(smartThis);
|
||||
}
|
||||
|
||||
void CardInfo::setProperties(const QVariantHash &_props)
|
||||
void CardInfo::setProperties(const QHash<QString, QString> &_props)
|
||||
{
|
||||
ensurePropertiesLoaded();
|
||||
propertiesCache = _props;
|
||||
|
|
@ -76,7 +75,7 @@ void CardInfo::setProperties(const QVariantHash &_props)
|
|||
CardInfo::CardInfo(const QString &_name,
|
||||
const QString &_text,
|
||||
bool _isToken,
|
||||
QVariantHash _properties,
|
||||
QHash<QString, QString> _properties,
|
||||
const QList<CardRelation *> &_relatedCards,
|
||||
const QList<CardRelation *> &_reverseRelatedCards,
|
||||
SetToPrintingsMap _sets,
|
||||
|
|
@ -122,7 +121,7 @@ CardInfoPtr CardInfo::newInstance(const QString &_name)
|
|||
CardInfoPtr CardInfo::newInstance(const QString &_name,
|
||||
const QString &_text,
|
||||
bool _isToken,
|
||||
QVariantHash _properties,
|
||||
QHash<QString, QString> _properties,
|
||||
const QList<CardRelation *> &_relatedCards,
|
||||
const QList<CardRelation *> &_reverseRelatedCards,
|
||||
SetToPrintingsMap _sets,
|
||||
|
|
@ -210,10 +209,10 @@ void CardInfo::addToSet(const CardSetPtr &_set, const PrintingInfo &_info)
|
|||
refreshCachedSets();
|
||||
}
|
||||
|
||||
void CardInfo::combineLegalities(const QVariantHash &props)
|
||||
void CardInfo::combineLegalities(const QHash<QString, QString> &props)
|
||||
{
|
||||
ensurePropertiesLoaded();
|
||||
QHashIterator<QString, QVariant> it(props);
|
||||
QHashIterator it(props);
|
||||
while (it.hasNext()) {
|
||||
it.next();
|
||||
if (it.key().startsWith("format-")) {
|
||||
|
|
|
|||
|
|
@ -76,12 +76,12 @@ private:
|
|||
QString text; ///< Text description or rules text of the card.
|
||||
bool isToken; ///< Whether this card is a token or not.
|
||||
// Properties are stored as a pre-serialized blob (cheap to load) and the
|
||||
// QVariantHash is materialized on first query, so database load avoids
|
||||
// constructing thousands of QVariants per card.
|
||||
mutable QByteArray propertiesBlob; ///< Serialized properties (load form).
|
||||
mutable QVariantHash propertiesCache; ///< Materialized properties (query form).
|
||||
mutable bool propertiesLoaded = false; ///< Whether propertiesCache is valid.
|
||||
mutable QMutex propertiesMutex; ///< Guards lazy materialization.
|
||||
// QHash<QString, QString> is materialized on first query, so database load avoids
|
||||
// constructing thousands of QStrings per card.
|
||||
mutable QByteArray propertiesBlob; ///< Serialized properties (load form).
|
||||
mutable QHash<QString, QString> propertiesCache; ///< Materialized properties (query form).
|
||||
mutable bool propertiesLoaded = false; ///< Whether propertiesCache is valid.
|
||||
mutable QMutex propertiesMutex; ///< Guards lazy materialization.
|
||||
|
||||
/**
|
||||
* @brief Materializes propertiesCache from propertiesBlob if not already done.
|
||||
|
|
@ -114,7 +114,7 @@ public:
|
|||
explicit CardInfo(const QString &_name,
|
||||
const QString &_text,
|
||||
bool _isToken,
|
||||
QVariantHash _properties,
|
||||
QHash<QString, QString> _properties,
|
||||
const QList<CardRelation *> &_relatedCards,
|
||||
const QList<CardRelation *> &_reverseRelatedCards,
|
||||
SetToPrintingsMap _sets,
|
||||
|
|
@ -127,7 +127,7 @@ public:
|
|||
* Used by the binary cache reader to skip recomputing @p _simpleName and
|
||||
* @p _altNames (which otherwise require a Unicode normalization and a full
|
||||
* printing scan). Properties are supplied as a pre-serialized @p _propertiesBlob
|
||||
* so the QVariantHash is not built at load time (it is materialized on first
|
||||
* so the QHash<QString, QString> is not built at load time (it is materialized on first
|
||||
* query).
|
||||
*
|
||||
* @param _name The card name.
|
||||
|
|
@ -194,7 +194,7 @@ public:
|
|||
static CardInfoPtr newInstance(const QString &_name,
|
||||
const QString &_text,
|
||||
bool _isToken,
|
||||
QVariantHash _properties,
|
||||
QHash<QString, QString> _properties,
|
||||
const QList<CardRelation *> &_relatedCards,
|
||||
const QList<CardRelation *> &_reverseRelatedCards,
|
||||
SetToPrintingsMap _sets,
|
||||
|
|
@ -288,12 +288,12 @@ public:
|
|||
{
|
||||
return getPropertiesHash().keys();
|
||||
}
|
||||
[[nodiscard]] const QVariantHash &getPropertiesHash() const;
|
||||
[[nodiscard]] const QHash<QString, QString> &getPropertiesHash() const;
|
||||
|
||||
/**
|
||||
* @brief Stores the pre-serialized properties blob and invalidates the
|
||||
* materialized cache. Used by the binary cache reader so the
|
||||
* QVariantHash is not built at load time.
|
||||
* QHash<QString, QString> is not built at load time.
|
||||
* @param _blob The serialized properties (as written by the cache writer).
|
||||
*/
|
||||
void setPropertiesBlob(QByteArray _blob) const
|
||||
|
|
@ -305,10 +305,10 @@ public:
|
|||
}
|
||||
[[nodiscard]] QString getProperty(const QString &propertyName) const
|
||||
{
|
||||
return getPropertiesHash().value(propertyName).toString();
|
||||
return getPropertiesHash().value(propertyName);
|
||||
}
|
||||
void setProperty(const QString &_name, const QString &_value);
|
||||
void setProperties(const QVariantHash &_props);
|
||||
void setProperties(const QHash<QString, QString> &_props);
|
||||
[[nodiscard]] bool hasProperty(const QString &propertyName) const
|
||||
{
|
||||
return getPropertiesHash().contains(propertyName);
|
||||
|
|
@ -415,7 +415,7 @@ public:
|
|||
*
|
||||
* @param props Key-value mapping of format legalities.
|
||||
*/
|
||||
void combineLegalities(const QVariantHash &props);
|
||||
void combineLegalities(const QHash<QString, QString> &props);
|
||||
|
||||
/**
|
||||
* @brief Refreshes all cached fields that are calculated from the contained sets and printings.
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ QVariant CardInfoComparator::getProperty(const CardInfoPtr &card, const QString
|
|||
return card->getIsToken();
|
||||
}
|
||||
|
||||
// Otherwise, check if it's a custom property in the QVariantHash
|
||||
// Otherwise, check if it's a custom property in the properties hash
|
||||
if (card->hasProperty(property)) {
|
||||
return card->getProperty(property);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,12 +13,11 @@
|
|||
#include <QElapsedTimer>
|
||||
#include <QFile>
|
||||
#include <QSaveFile>
|
||||
#include <QVariantHash>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr quint32 CACHE_MAGIC = 0x43445243; // "CDRC"
|
||||
constexpr quint32 CACHE_VERSION = 1;
|
||||
constexpr quint32 CACHE_VERSION = 2;
|
||||
|
||||
// ---- Primitives -----------------------------------------------------------
|
||||
|
||||
|
|
@ -34,11 +33,11 @@ QString readString(QDataStream &in)
|
|||
return s;
|
||||
}
|
||||
|
||||
// Stores a QVariantHash as a single pre-serialized blob. The reader keeps the
|
||||
// blob as-is and materializes the QVariantHash lazily on first query, which is
|
||||
// Stores a QHash<QString, QString> as a single pre-serialized blob. The reader keeps the
|
||||
// blob as-is and materializes the QHash<QString, QString> lazily on first query, which is
|
||||
// what removes the allocation storm from database load (see card_info.cpp /
|
||||
// printing_info.cpp).
|
||||
void writeHashBlob(QDataStream &out, const QVariantHash &h)
|
||||
void writeHashBlob(QDataStream &out, const QHash<QString, QString> &h)
|
||||
{
|
||||
QByteArray blob;
|
||||
QDataStream blobOut(&blob, QIODevice::WriteOnly);
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml)
|
|||
if (xmlName == "card") {
|
||||
QString name = QString("");
|
||||
QString text = QString("");
|
||||
QVariantHash properties = QVariantHash();
|
||||
QHash<QString, QString> properties;
|
||||
QString colors = QString("");
|
||||
QList<CardRelation *> relatedCards, reverseRelatedCards;
|
||||
auto _sets = SetToPrintingsMap();
|
||||
|
|
@ -229,7 +229,7 @@ void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml)
|
|||
// behaviour. Without this check, disabling a set has no effect on v3 databases.
|
||||
if (set->getEnabled()) {
|
||||
PrintingInfo setInfo(set);
|
||||
QVariantHash printingProps;
|
||||
QHash<QString, QString> printingProps;
|
||||
if (attrs.hasAttribute("muId")) {
|
||||
printingProps.insert("muid", attrs.value("muId").toString());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -243,9 +243,9 @@ void CockatriceXml4Parser::loadSetsFromXml(QXmlStreamReader &xml)
|
|||
}
|
||||
}
|
||||
|
||||
QVariantHash CockatriceXml4Parser::loadCardPropertiesFromXml(QXmlStreamReader &xml)
|
||||
QHash<QString, QString> CockatriceXml4Parser::loadCardPropertiesFromXml(QXmlStreamReader &xml)
|
||||
{
|
||||
QVariantHash properties = QVariantHash();
|
||||
QHash<QString, QString> properties;
|
||||
while (!xml.atEnd()) {
|
||||
if (xml.readNext() == QXmlStreamReader::EndElement) {
|
||||
break;
|
||||
|
|
@ -272,7 +272,7 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml)
|
|||
if (xmlName == "card") {
|
||||
QString name = QString("");
|
||||
QString text = QString("");
|
||||
QVariantHash properties = QVariantHash();
|
||||
QHash<QString, QString> properties;
|
||||
QList<CardRelation *> relatedCards, reverseRelatedCards;
|
||||
auto _sets = SetToPrintingsMap();
|
||||
int tableRow = 0;
|
||||
|
|
@ -315,7 +315,7 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml)
|
|||
auto set = internalAddSet(setName);
|
||||
if (set->getEnabled()) {
|
||||
PrintingInfo printingInfo(set);
|
||||
QVariantHash printingProps;
|
||||
QHash<QString, QString> printingProps;
|
||||
for (QXmlStreamAttribute attr : attrs) {
|
||||
QString attrName = attr.name().toString();
|
||||
if (attrName == "picURL") {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ inline Q_LOGGING_CATEGORY(CockatriceXml4Log, "cockatrice_xml.xml_4_parser");
|
|||
* making the parser more extensible and schema-compliant.
|
||||
*
|
||||
* @note Differences from v3:
|
||||
* - Card properties are stored in <prop> blocks as a QVariantHash.
|
||||
* - Card properties are stored in <prop> blocks as a QHash<QString, QString>.
|
||||
* - Sets can include a <priority> element.
|
||||
* - Supports user preferences via ICardPreferenceProvider (e.g., skipping rebalanced cards).
|
||||
* - Related cards support persistent relations and multiple attach types (e.g., transform).
|
||||
|
|
@ -70,9 +70,9 @@ private:
|
|||
/**
|
||||
* @brief Loads a generic <prop> block from a <card> element.
|
||||
* @param xml The open QXmlStreamReader positioned at a <prop> element.
|
||||
* @return A QVariantHash mapping property names to values.
|
||||
* @return A QHash<QString, QString> mapping property names to values.
|
||||
*/
|
||||
QVariantHash loadCardPropertiesFromXml(QXmlStreamReader &xml);
|
||||
QHash<QString, QString> loadCardPropertiesFromXml(QXmlStreamReader &xml);
|
||||
|
||||
/**
|
||||
* @brief Load all <card> elements from the XML stream.
|
||||
|
|
|
|||
|
|
@ -27,14 +27,14 @@ void PrintingInfo::ensurePropertiesLoaded() const
|
|||
void PrintingInfo::setProperty(const QString &_name, const QString &_value)
|
||||
{
|
||||
ensurePropertiesLoaded();
|
||||
if (propertiesCache.value(_name).toString() == _value) {
|
||||
if (propertiesCache.value(_name) == _value) {
|
||||
return;
|
||||
}
|
||||
propertiesCache.insert(_name, _value);
|
||||
setProperties(propertiesCache);
|
||||
}
|
||||
|
||||
void PrintingInfo::setProperties(const QVariantHash &_props)
|
||||
void PrintingInfo::setProperties(const QHash<QString, QString> &_props)
|
||||
{
|
||||
ensurePropertiesLoaded();
|
||||
propertiesCache = _props;
|
||||
|
|
@ -48,10 +48,10 @@ void PrintingInfo::setProperties(const QVariantHash &_props)
|
|||
*/
|
||||
QString PrintingInfo::getUuid() const
|
||||
{
|
||||
return getPropertiesHash().value("uuid").toString();
|
||||
return getPropertiesHash().value("uuid");
|
||||
}
|
||||
|
||||
QString PrintingInfo::getFlavorName() const
|
||||
{
|
||||
return getPropertiesHash().value("flavorName").toString();
|
||||
}
|
||||
return getPropertiesHash().value("flavorName");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,11 +70,11 @@ private:
|
|||
CardSetPtr set; ///< The set this variation belongs to.
|
||||
|
||||
// Properties are stored as a pre-serialized blob (cheap to load) and the
|
||||
// QVariantHash is materialized on first query. This avoids constructing
|
||||
// thousands of QVariants per card at database-load time.
|
||||
mutable QByteArray propertiesBlob; ///< Serialized properties (load form).
|
||||
mutable QVariantHash propertiesCache; ///< Materialized properties (query form).
|
||||
mutable bool propertiesLoaded = false; ///< Whether propertiesCache is valid.
|
||||
// QHash<QString, QString> is materialized on first query. This avoids constructing
|
||||
// thousands of QStrings per card at database-load time.
|
||||
mutable QByteArray propertiesBlob; ///< Serialized properties (load form).
|
||||
mutable QHash<QString, QString> propertiesCache; ///< Materialized properties (query form).
|
||||
mutable bool propertiesLoaded = false; ///< Whether propertiesCache is valid.
|
||||
mutable QSharedPointer<QBasicMutex> propertiesMutex =
|
||||
QSharedPointer<QBasicMutex>::create(); ///< Guards lazy materialization.
|
||||
|
||||
|
|
@ -101,7 +101,7 @@ public:
|
|||
return getPropertiesHash().keys();
|
||||
}
|
||||
|
||||
[[nodiscard]] const QVariantHash &getPropertiesHash() const
|
||||
[[nodiscard]] const QHash<QString, QString> &getPropertiesHash() const
|
||||
{
|
||||
ensurePropertiesLoaded();
|
||||
return propertiesCache;
|
||||
|
|
@ -115,7 +115,7 @@ public:
|
|||
*/
|
||||
[[nodiscard]] QString getProperty(const QString &propertyName) const
|
||||
{
|
||||
return getPropertiesHash().value(propertyName).toString();
|
||||
return getPropertiesHash().value(propertyName);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -127,12 +127,12 @@ public:
|
|||
* @param _value The string value to assign.
|
||||
*/
|
||||
void setProperty(const QString &_name, const QString &_value);
|
||||
void setProperties(const QVariantHash &_props);
|
||||
void setProperties(const QHash<QString, QString> &_props);
|
||||
|
||||
/**
|
||||
* @brief Stores the pre-serialized properties blob and marks the materialized
|
||||
* cache as invalid. Used by the binary cache reader to avoid building a
|
||||
* QVariantHash at load time.
|
||||
* QHash<QString, QString> at load time.
|
||||
* @param _blob The serialized properties (as written by the cache writer).
|
||||
*/
|
||||
void setPropertiesBlob(QByteArray _blob) const
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ static const QList<AllowedCount> kSingletonCounts = {{1, "legal"}, {0, "banned"}
|
|||
|
||||
SplitCardPart::SplitCardPart(const QString &_name,
|
||||
const QString &_text,
|
||||
const QVariantHash &_properties,
|
||||
const QHash<QString, QString> &_properties,
|
||||
const PrintingInfo &_printingInfo)
|
||||
: name(_name), text(_text), properties(_properties), printingInfo(_printingInfo)
|
||||
{
|
||||
|
|
@ -135,7 +135,7 @@ static void sortAndReduceColors(QString &colors)
|
|||
CardInfoPtr OracleImporter::addCard(QString name,
|
||||
const QString &text,
|
||||
bool isToken,
|
||||
QVariantHash properties,
|
||||
QHash<QString, QString> properties,
|
||||
const QList<CardRelation *> &relatedCards,
|
||||
const PrintingInfo &printingInfo)
|
||||
{
|
||||
|
|
@ -152,7 +152,7 @@ CardInfoPtr OracleImporter::addCard(QString name,
|
|||
}
|
||||
|
||||
// Remove {} around mana costs, except if it's split cost
|
||||
QString manacost = properties.value("manacost").toString();
|
||||
QString manacost = properties.value("manacost");
|
||||
if (!manacost.isEmpty()) {
|
||||
QStringList symbols = manacost.split("}");
|
||||
QString formattedCardCost;
|
||||
|
|
@ -169,12 +169,12 @@ CardInfoPtr OracleImporter::addCard(QString name,
|
|||
}
|
||||
|
||||
// fix colors
|
||||
QString allColors = properties.value("colors").toString();
|
||||
QString allColors = properties.value("colors");
|
||||
if (allColors.size() > 1) {
|
||||
sortAndReduceColors(allColors);
|
||||
properties.insert("colors", allColors);
|
||||
}
|
||||
QString allColorIdent = properties.value("coloridentity").toString();
|
||||
QString allColorIdent = properties.value("coloridentity");
|
||||
if (allColorIdent.size() > 1) {
|
||||
sortAndReduceColors(allColorIdent);
|
||||
properties.insert("coloridentity", allColorIdent);
|
||||
|
|
@ -182,16 +182,15 @@ CardInfoPtr OracleImporter::addCard(QString name,
|
|||
|
||||
// DETECT CARD POSITIONING INFO
|
||||
|
||||
bool landscapeOrientation = properties.value("maintype").toString() == "Battle" ||
|
||||
properties.value("layout").toString() == "split" ||
|
||||
properties.value("layout").toString() == "planar";
|
||||
bool landscapeOrientation = properties.value("maintype") == "Battle" || properties.value("layout") == "split" ||
|
||||
properties.value("layout") == "planar";
|
||||
|
||||
// cards that enter the field tapped
|
||||
bool cipt = parseCipt(name, text) || landscapeOrientation;
|
||||
|
||||
// table row
|
||||
int tableRow = 1;
|
||||
QString mainCardType = properties.value("maintype").toString();
|
||||
QString mainCardType = properties.value("maintype");
|
||||
if (mainCardType == "Land") {
|
||||
tableRow = 0;
|
||||
} else if (mainCardType == "Sorcery" || mainCardType == "Instant") {
|
||||
|
|
@ -201,11 +200,11 @@ CardInfoPtr OracleImporter::addCard(QString name,
|
|||
}
|
||||
|
||||
// card side
|
||||
QString side = properties.value("side").toString() == "b" ? "back" : "front";
|
||||
QString side = properties.value("side") == "b" ? "back" : "front";
|
||||
properties.insert("side", side);
|
||||
|
||||
// upsideDown (flip cards)
|
||||
QString layout = properties.value("layout").toString();
|
||||
QString layout = properties.value("layout");
|
||||
bool upsideDown = layout == "flip" && side == "back";
|
||||
|
||||
// insert the card and its properties
|
||||
|
|
@ -279,7 +278,7 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList
|
|||
}
|
||||
|
||||
// card properties
|
||||
QVariantHash properties;
|
||||
QHash<QString, QString> properties;
|
||||
for (auto i = cardProperties.cbegin(), end = cardProperties.cend(); i != end; ++i) {
|
||||
QString mtgjsonProperty = i.key();
|
||||
QString xmlPropertyName = i.value();
|
||||
|
|
@ -291,7 +290,7 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList
|
|||
|
||||
// per-set properties
|
||||
PrintingInfo printingInfo = PrintingInfo(currentSet);
|
||||
QVariantHash printingProps;
|
||||
QHash<QString, QString> printingProps;
|
||||
for (auto i = setInfoProperties.cbegin(), end = setInfoProperties.cend(); i != end; ++i) {
|
||||
QString mtgjsonProperty = i.key();
|
||||
QString xmlPropertyName = i.value();
|
||||
|
|
@ -431,7 +430,7 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList
|
|||
QList<QPair<QList<SplitCardPart>, QString>> partsAndNames = splitCards.values();
|
||||
for (auto [splitCardParts, name] : partsAndNames) {
|
||||
QString text;
|
||||
QVariantHash properties;
|
||||
QHash<QString, QString> properties;
|
||||
PrintingInfo printingInfo;
|
||||
|
||||
for (const SplitCardPart &tmp : splitCardParts) {
|
||||
|
|
@ -444,11 +443,11 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList
|
|||
properties = tmp.getProperties();
|
||||
printingInfo = tmp.getPrintingInfo();
|
||||
} else {
|
||||
const QVariantHash &tmpProps = tmp.getProperties();
|
||||
const QHash<QString, QString> &tmpProps = tmp.getProperties();
|
||||
for (auto i = tmpProps.cbegin(), end = tmpProps.cend(); i != end; ++i) {
|
||||
QString prop = i.key();
|
||||
QString originalPropertyValue = properties.value(prop).toString();
|
||||
QString thisCardPropertyValue = i.value().toString();
|
||||
QString originalPropertyValue = properties.value(prop);
|
||||
QString thisCardPropertyValue = i.value();
|
||||
if (!thisCardPropertyValue.isEmpty() && originalPropertyValue != thisCardPropertyValue) {
|
||||
if (originalPropertyValue.isEmpty()) { // don't create //es if one field is empty
|
||||
properties.insert(prop, thisCardPropertyValue);
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ class SplitCardPart
|
|||
public:
|
||||
SplitCardPart(const QString &_name,
|
||||
const QString &_text,
|
||||
const QVariantHash &_properties,
|
||||
const QHash<QString, QString> &_properties,
|
||||
const PrintingInfo &_printingInfo);
|
||||
inline const QString &getName() const
|
||||
{
|
||||
|
|
@ -105,7 +105,7 @@ public:
|
|||
{
|
||||
return text;
|
||||
}
|
||||
inline const QVariantHash &getProperties() const
|
||||
inline const QHash<QString, QString> &getProperties() const
|
||||
{
|
||||
return properties;
|
||||
}
|
||||
|
|
@ -117,7 +117,7 @@ public:
|
|||
private:
|
||||
QString name;
|
||||
QString text;
|
||||
QVariantHash properties;
|
||||
QHash<QString, QString> properties;
|
||||
PrintingInfo printingInfo;
|
||||
};
|
||||
|
||||
|
|
@ -142,7 +142,7 @@ private:
|
|||
CardInfoPtr addCard(QString name,
|
||||
const QString &text,
|
||||
bool isToken,
|
||||
QVariantHash properties,
|
||||
QHash<QString, QString> properties,
|
||||
const QList<CardRelation *> &relatedCards,
|
||||
const PrintingInfo &printingInfo);
|
||||
signals:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue