Compare commits

..

7 commits

Author SHA1 Message Date
BruebachL
daa896866f
[VDD] Defer heavy construction until after the tab paints (#7115)
Some checks are pending
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 15 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker Image / amd64 & arm64 (push) Waiting to run
* [VDD] Defer heavy construction until after the tab paints

* Use singleshot QTimer instead of member variable.

Took 27 minutes

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-08-14 11:39:10 +02:00
BruebachL
404b0cdf28
[Game] Animate Arrows (#7099)
* Add an arrow draw animation setting

- New arrowDrawAnimation cards-display setting, default on
- The arrow draw animation checkbox joins the animation settings group
- Visual Deck Storage selection animation checkbox moves next to the
  other animation checkboxes, and the enable/disable-all buttons now
  cover it and the arrow animation

Took 2 minutes

Took 21 minutes

Took 7 minutes


Took 11 minutes

Took 20 seconds

* Animate arrows drawing from start to target

- The arrow stroke reveals itself along the arc with an eased timing,
  followed by a short light sheen that sweeps down the shaft
- The arrow head pops in once the reveal reaches it, then the whole
  arrow fades from its initial glow
- Decay is driven by GameScene's shared animation timer through the
  IAnimatedItem interface (QElapsedTimer based), respecting the
  arrowDrawAnimation setting
- GameScene adds the arrow item to the scene before starting its
  animation so the item is registered against a valid scene

Took 6 minutes


Took 1 minute

* Defer animation start so arrows don't start halfway materialized

Took 13 minutes

* Don't draw tip/shaft outline

Took 12 minutes

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-08-14 10:44:17 +02:00
BruebachL
1eca89d75a
[Refactor] Batch CardDatabaseModel enabled-sets rebuild into one model reset (#7113)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-08-14 09:03:24 +02:00
BruebachL
d93f63050c
[Server] Fix unauthenticated crash via replay submit code (#7072)
cmdReplaySubmitCode dereferenced userInfo without an authentication
guard, allowing an unauthenticated connection with a valid replay code
to segfault the server. Add the same authState != PasswordRight guard
used by all other replay handlers, and gate session command dispatch on
a pre-auth whitelist so future handlers cannot be reached before login.

Took 2 minutes

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-08-14 08:56:54 +02:00
BruebachL
7971ebfe94
[VDD] Parent filter toolbar layouts to their group boxes (#7116)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-08-14 07:30:11 +02:00
BruebachL
a40969003c
[CardDB] Cache CardDatabaseQuerier count maps (#7114)
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-08-14 07:19:34 +02:00
BruebachL
2eca362e2b
[DeckEditor] Reuse the inherited CardDatabaseModel in the visual editor (#7112)
Took 6 minutes

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-08-14 07:11:26 +02:00
21 changed files with 423 additions and 105 deletions

View file

@ -4,12 +4,14 @@
#include "../../client/settings/cache_settings.h" #include "../../client/settings/cache_settings.h"
#include "../../game/player/player_actions.h" #include "../../game/player/player_actions.h"
#include "../../game/player/player_logic.h" #include "../../game/player/player_logic.h"
#include "../game_scene.h"
#include "../player/player_target.h" #include "../player/player_target.h"
#include "../z_values.h" #include "../z_values.h"
#include "../zones/card_zone.h" #include "../zones/card_zone.h"
#include "card_item.h" #include "card_item.h"
#include <QDebug> #include <QDebug>
#include <QElapsedTimer>
#include <QGraphicsScene> #include <QGraphicsScene>
#include <QGraphicsSceneMouseEvent> #include <QGraphicsSceneMouseEvent>
#include <QPainter> #include <QPainter>
@ -18,10 +20,27 @@
#include <libcockatrice/protocol/pb/command_attach_card.pb.h> #include <libcockatrice/protocol/pb/command_attach_card.pb.h>
#include <libcockatrice/protocol/pb/command_create_arrow.pb.h> #include <libcockatrice/protocol/pb/command_create_arrow.pb.h>
#include <libcockatrice/protocol/pb/command_delete_arrow.pb.h> #include <libcockatrice/protocol/pb/command_delete_arrow.pb.h>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/settings/interface_settings.h> #include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/utility/color.h> #include <libcockatrice/utility/color.h>
#include <libcockatrice/utility/zone_names.h> #include <libcockatrice/utility/zone_names.h>
namespace
{
constexpr qreal kMinStrokeDurationMs = 200.0;
constexpr qreal kMaxStrokeDurationMs = 450.0;
constexpr qreal kMsPerPixel = 0.8;
constexpr qreal kGlowFadeDurationMs = 120.0;
constexpr qreal kSheenHalfWidth = 14.0;
/// @brief Ease-out cubic, for a natural "slow in / slow out" reveal.
qreal easeOutCubic(qreal t)
{
const qreal inverse = 1.0 - t;
return 1.0 - inverse * inverse * inverse;
}
} // namespace
ArrowItem::ArrowItem(QSharedPointer<const ArrowData> _data, ArrowTarget *_startItem, ArrowTarget *_targetItem) ArrowItem::ArrowItem(QSharedPointer<const ArrowData> _data, ArrowTarget *_startItem, ArrowTarget *_targetItem)
: data(std::move(_data)), startItem(_startItem), targetItem(_targetItem) : data(std::move(_data)), startItem(_startItem), targetItem(_targetItem)
{ {
@ -47,6 +66,13 @@ ArrowItem::ArrowItem(QSharedPointer<const ArrowData> _data, ArrowTarget *_startI
} }
} }
ArrowItem::~ArrowItem()
{
if (auto *scene = qobject_cast<GameScene *>(this->scene())) {
scene->unregisterAnimationItem(this);
}
}
void ArrowItem::onTargetDestroyed() void ArrowItem::onTargetDestroyed()
{ {
emit requestDeletion(data->creatorId, data->id); emit requestDeletion(data->creatorId, data->id);
@ -91,16 +117,21 @@ void ArrowItem::updatePath(const QPointF &endPoint)
prepareGeometryChange(); prepareGeometryChange();
if (lineLength < 30) { if (lineLength < 30) {
path = QPainterPath(); path = QPainterPath();
bodyPath = QPainterPath();
headPath = QPainterPath();
shaftOutlinePath = QPainterPath();
centerLine = QPainterPath();
headBaseFraction = 1.0;
} else { } else {
QPointF c(lineLength / 2, qTan(phi * M_PI / 180) * lineLength); QPointF c(lineLength / 2, qTan(phi * M_PI / 180) * lineLength);
QPainterPath centerLine; centerLine = QPainterPath();
centerLine.moveTo(0, 0); centerLine.moveTo(0, 0);
centerLine.quadTo(c, QPointF(lineLength, 0)); centerLine.quadTo(c, QPointF(lineLength, 0));
double percentage = 1 - headLength / lineLength; headBaseFraction = 1 - headLength / lineLength;
QPointF arrowBodyEndPoint = centerLine.pointAtPercent(percentage); QPointF arrowBodyEndPoint = centerLine.pointAtPercent(headBaseFraction);
QLineF testLine(arrowBodyEndPoint, centerLine.pointAtPercent(percentage + 0.001)); QLineF testLine(arrowBodyEndPoint, centerLine.pointAtPercent(headBaseFraction + 0.001));
qreal alpha = testLine.angle() - 90; qreal alpha = testLine.angle() - 90;
QPointF endPoint1 = QPointF endPoint1 =
arrowBodyEndPoint + arrowWidth / 2 * QPointF(qCos(alpha * M_PI / 180), -qSin(alpha * M_PI / 180)); arrowBodyEndPoint + arrowWidth / 2 * QPointF(qCos(alpha * M_PI / 180), -qSin(alpha * M_PI / 180));
@ -111,20 +142,89 @@ void ArrowItem::updatePath(const QPointF &endPoint)
QPointF point2 = QPointF point2 =
endPoint2 + (headWidth - arrowWidth) / 2 * QPointF(-qCos(alpha * M_PI / 180), qSin(alpha * M_PI / 180)); endPoint2 + (headWidth - arrowWidth) / 2 * QPointF(-qCos(alpha * M_PI / 180), qSin(alpha * M_PI / 180));
path = QPainterPath(-arrowWidth / 2 * QPointF(qCos((phi - 90) * M_PI / 180), qSin((phi - 90) * M_PI / 180))); QPointF start1 = -arrowWidth / 2 * QPointF(qCos((phi - 90) * M_PI / 180), qSin((phi - 90) * M_PI / 180));
QPointF start2 = arrowWidth / 2 * QPointF(qCos((phi - 90) * M_PI / 180), qSin((phi - 90) * M_PI / 180));
path = QPainterPath(start1);
path.quadTo(c, endPoint1); path.quadTo(c, endPoint1);
path.lineTo(point1); path.lineTo(point1);
path.lineTo(QPointF(lineLength, 0)); path.lineTo(QPointF(lineLength, 0));
path.lineTo(point2); path.lineTo(point2);
path.lineTo(endPoint2); path.lineTo(endPoint2);
path.quadTo(c, arrowWidth / 2 * QPointF(qCos((phi - 90) * M_PI / 180), qSin((phi - 90) * M_PI / 180))); path.quadTo(c, start2);
path.lineTo(-arrowWidth / 2 * QPointF(qCos((phi - 90) * M_PI / 180), qSin((phi - 90) * M_PI / 180))); path.lineTo(start1);
bodyPath = QPainterPath(start1);
bodyPath.quadTo(c, endPoint1);
bodyPath.lineTo(endPoint2);
bodyPath.quadTo(c, start2);
bodyPath.lineTo(start1);
headPath = QPainterPath(endPoint1);
headPath.lineTo(point1);
headPath.lineTo(QPointF(lineLength, 0));
headPath.lineTo(point2);
headPath.lineTo(endPoint2);
shaftOutlinePath = QPainterPath(start1);
shaftOutlinePath.quadTo(c, endPoint1);
shaftOutlinePath.moveTo(endPoint2);
shaftOutlinePath.quadTo(c, start2);
shaftOutlinePath.lineTo(start1);
} }
setPos(startPoint); setPos(startPoint);
setTransform(QTransform().rotate(-line.angle())); setTransform(QTransform().rotate(-line.angle()));
} }
void ArrowItem::startDrawAnimation()
{
if (!SettingsCache::instance().cardsDisplay().getArrowDrawAnimation() || centerLine.isEmpty()) {
return;
}
strokeDurationMs = qBound(kMinStrokeDurationMs, centerLine.length() * kMsPerPixel, kMaxStrokeDurationMs);
glowFadeDurationMs = kGlowFadeDurationMs;
// The clock is started on the first animationEvent() tick so that t=0
// corresponds to the first rendered frame. Starting it here would count
// the time spent before the item's first paint (event-loop delays, bursts
// of arrows created together), making the arrow appear already partway
// drawn when it first shows up.
animationStarted = false;
drawProgress = 0.0;
glowAlpha = 1.0;
update();
if (auto *scene = qobject_cast<GameScene *>(this->scene())) {
scene->registerAnimationItem(this);
}
}
bool ArrowItem::animationEvent()
{
if (!animationStarted) {
animationClock.start();
animationStarted = true;
}
const qint64 elapsed = animationClock.elapsed();
if (elapsed >= strokeDurationMs + glowFadeDurationMs) {
drawProgress = 1.0;
glowAlpha = 0.0;
update();
return false;
}
if (elapsed < strokeDurationMs) {
drawProgress = easeOutCubic(qBound<qreal>(0.0, elapsed / strokeDurationMs, 1.0));
glowAlpha = 1.0;
} else {
drawProgress = 1.0;
glowAlpha = 1.0 - (elapsed - strokeDurationMs) / glowFadeDurationMs;
}
update();
return true;
}
void ArrowItem::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) void ArrowItem::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
{ {
QColor paintColor(data->color); QColor paintColor(data->color);
@ -133,8 +233,66 @@ void ArrowItem::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*opti
} else { } else {
paintColor.setAlpha(150); paintColor.setAlpha(150);
} }
painter->save();
const QPen outlinePen = painter->pen();
painter->setBrush(paintColor); painter->setBrush(paintColor);
painter->drawPath(path);
const auto drawShaft = [this, painter, &outlinePen, paintColor]() {
painter->setPen(Qt::NoPen);
painter->drawPath(bodyPath);
painter->setPen(outlinePen);
painter->setBrush(Qt::NoBrush);
painter->drawPath(shaftOutlinePath);
painter->setBrush(paintColor);
};
if (drawProgress >= 1.0 || path.isEmpty()) {
painter->drawPath(path);
} else if (drawProgress < headBaseFraction) {
// The reveal edge and the sheen share the same arc-length parameterization,
// so the stroke stays exactly in sync with the trailing sheen.
const qreal revealX = centerLine.pointAtPercent(drawProgress).x();
QPainterPath clip;
clip.addRect(QRectF(-glowExtent, path.boundingRect().top() - glowExtent, revealX + glowExtent,
path.boundingRect().height() + 2 * glowExtent));
painter->setClipPath(clip);
drawShaft();
} else {
// Once the reveal reaches the head base, pop the whole head in with a fade
// instead of slicing the triangle into a growing stub.
drawShaft();
const qreal headFadeIn = (drawProgress - headBaseFraction) / (1.0 - headBaseFraction);
painter->setOpacity(headFadeIn);
painter->setPen(Qt::NoPen);
painter->drawPath(headPath);
painter->setPen(outlinePen);
painter->setBrush(Qt::NoBrush);
painter->drawPath(headPath);
painter->setOpacity(1.0);
painter->setBrush(paintColor);
}
if (glowAlpha > 0.0 && !centerLine.isEmpty()) {
// Sweep a bright band across the arrow. Clipping to the
// silhouette keeps it flat against the shaft so it reads as a light reflection.
const qreal anticipation = qMin<qreal>(1.0, drawProgress / 0.08);
const QPointF sweep = centerLine.pointAtPercent(qMin<qreal>(drawProgress, 1.0));
QLinearGradient sheen(sweep.x() - kSheenHalfWidth, 0.0, sweep.x() + kSheenHalfWidth, 0.0);
sheen.setColorAt(0.0, QColor(paintColor.red(), paintColor.green(), paintColor.blue(), 0));
sheen.setColorAt(0.5, QColor(255, 255, 255, 200));
sheen.setColorAt(1.0, QColor(paintColor.red(), paintColor.green(), paintColor.blue(), 0));
painter->save();
painter->setPen(Qt::NoPen);
painter->setClipPath(path);
painter->setBrush(sheen);
painter->setOpacity(glowAlpha * anticipation);
painter->drawRect(QRectF(sweep.x() - kSheenHalfWidth - glowExtent, path.boundingRect().top() - glowExtent,
(kSheenHalfWidth + glowExtent) * 2.0,
path.boundingRect().height() + glowExtent * 2.0));
painter->restore();
}
painter->restore();
} }
void ArrowItem::mousePressEvent(QGraphicsSceneMouseEvent *event) void ArrowItem::mousePressEvent(QGraphicsSceneMouseEvent *event)

View file

@ -2,9 +2,12 @@
#define ARROWITEM_H #define ARROWITEM_H
#include "../../game/board/arrow_data.h" #include "../../game/board/arrow_data.h"
#include "../animated_item.h"
#include "arrow_target.h" #include "arrow_target.h"
#include <QElapsedTimer>
#include <QGraphicsItem> #include <QGraphicsItem>
#include <QPainterPath>
#include <QPointer> #include <QPointer>
#include <QSharedPointer> #include <QSharedPointer>
@ -12,7 +15,7 @@ class CardItem;
class QGraphicsSceneMouseEvent; class QGraphicsSceneMouseEvent;
class PlayerLogic; class PlayerLogic;
class ArrowItem : public QObject, public QGraphicsItem class ArrowItem : public QObject, public QGraphicsItem, public IAnimatedItem
{ {
Q_OBJECT Q_OBJECT
Q_INTERFACES(QGraphicsItem) Q_INTERFACES(QGraphicsItem)
@ -21,6 +24,19 @@ signals:
private: private:
QPainterPath path; QPainterPath path;
QPainterPath bodyPath;
QPainterPath headPath;
QPainterPath shaftOutlinePath;
QPainterPath centerLine;
qreal headBaseFraction = 1.0;
QElapsedTimer animationClock;
qreal strokeDurationMs = 0;
qreal glowFadeDurationMs = 0;
qreal drawProgress = 1.0;
qreal glowAlpha = 0.0;
bool animationStarted = false;
static constexpr qreal glowExtent = 12.0;
protected: protected:
QSharedPointer<const ArrowData> data; QSharedPointer<const ArrowData> data;
@ -33,16 +49,19 @@ protected:
public: public:
ArrowItem(QSharedPointer<const ArrowData> _data, ArrowTarget *_startItem, ArrowTarget *_targetItem); ArrowItem(QSharedPointer<const ArrowData> _data, ArrowTarget *_startItem, ArrowTarget *_targetItem);
~ArrowItem() override;
void onTargetDestroyed(); void onTargetDestroyed();
void delArrow(); void delArrow();
void updatePath(); void updatePath();
void updatePath(const QPointF &endPoint); void updatePath(const QPointF &endPoint);
void startDrawAnimation();
bool animationEvent() override;
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
[[nodiscard]] QRectF boundingRect() const override [[nodiscard]] QRectF boundingRect() const override
{ {
return path.boundingRect(); return path.boundingRect().adjusted(-glowExtent, -glowExtent, glowExtent, glowExtent);
} }
[[nodiscard]] QPainterPath shape() const override [[nodiscard]] QPainterPath shape() const override
{ {

View file

@ -502,6 +502,7 @@ void GameScene::addArrow(QSharedPointer<ArrowData> data)
auto *arrow = new ArrowItem(data, startCard, targetItem); auto *arrow = new ArrowItem(data, startCard, targetItem);
addItem(arrow); addItem(arrow);
arrow->startDrawAnimation();
arrowRegistry.insert(data, arrow); arrowRegistry.insert(data, arrow);
connect(arrow, &ArrowItem::requestDeletion, this, &GameScene::requestArrowDeletion); connect(arrow, &ArrowItem::requestDeletion, this, &GameScene::requestArrowDeletion);
} }

View file

@ -116,6 +116,10 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
connect(&tapAnimationCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().cardsDisplay(), connect(&tapAnimationCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().cardsDisplay(),
&CardsDisplaySettings::setTapAnimation); &CardsDisplaySettings::setTapAnimation);
arrowDrawAnimationCheckBox.setChecked(SettingsCache::instance().cardsDisplay().getArrowDrawAnimation());
connect(&arrowDrawAnimationCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().cardsDisplay(),
&CardsDisplaySettings::setArrowDrawAnimation);
lifeCounterAnimationsCheckBox.setChecked( lifeCounterAnimationsCheckBox.setChecked(
SettingsCache::instance().userInterface().getLifeCounterAnimationsEnabled()); SettingsCache::instance().userInterface().getLifeCounterAnimationsEnabled());
connect(&lifeCounterAnimationsCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(), connect(&lifeCounterAnimationsCheckBox, &QCheckBox::QT_STATE_CHANGED, &SettingsCache::instance().userInterface(),
@ -132,8 +136,9 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
animationGrid->addWidget(&enableAllAnimationsButton, 0, 0); animationGrid->addWidget(&enableAllAnimationsButton, 0, 0);
animationGrid->addWidget(&disableAllAnimationsButton, 0, 1); animationGrid->addWidget(&disableAllAnimationsButton, 0, 1);
animationGrid->addWidget(&tapAnimationCheckBox, 1, 0); animationGrid->addWidget(&tapAnimationCheckBox, 1, 0);
animationGrid->addWidget(&lifeCounterAnimationsCheckBox, 2, 0); animationGrid->addWidget(&arrowDrawAnimationCheckBox, 2, 0);
animationGrid->addWidget(&battlefieldFlashCheckBox, 3, 0); animationGrid->addWidget(&lifeCounterAnimationsCheckBox, 3, 0);
animationGrid->addWidget(&battlefieldFlashCheckBox, 4, 0);
animationGroupBox = new QGroupBox; animationGroupBox = new QGroupBox;
animationGroupBox->setLayout(animationGrid); animationGroupBox->setLayout(animationGrid);
@ -287,6 +292,7 @@ void UserInterfaceSettingsPage::setNotificationEnabled(QT_STATE_CHANGED_T i)
void UserInterfaceSettingsPage::enableAllAnimations() void UserInterfaceSettingsPage::enableAllAnimations()
{ {
tapAnimationCheckBox.setChecked(true); tapAnimationCheckBox.setChecked(true);
arrowDrawAnimationCheckBox.setChecked(true);
lifeCounterAnimationsCheckBox.setChecked(true); lifeCounterAnimationsCheckBox.setChecked(true);
battlefieldFlashCheckBox.setChecked(true); battlefieldFlashCheckBox.setChecked(true);
} }
@ -294,6 +300,7 @@ void UserInterfaceSettingsPage::enableAllAnimations()
void UserInterfaceSettingsPage::disableAllAnimations() void UserInterfaceSettingsPage::disableAllAnimations()
{ {
tapAnimationCheckBox.setChecked(false); tapAnimationCheckBox.setChecked(false);
arrowDrawAnimationCheckBox.setChecked(false);
lifeCounterAnimationsCheckBox.setChecked(false); lifeCounterAnimationsCheckBox.setChecked(false);
battlefieldFlashCheckBox.setChecked(false); battlefieldFlashCheckBox.setChecked(false);
} }
@ -343,6 +350,7 @@ void UserInterfaceSettingsPage::retranslateUi()
enableAllAnimationsButton.setText(tr("&Enable all animations")); enableAllAnimationsButton.setText(tr("&Enable all animations"));
disableAllAnimationsButton.setText(tr("&Disable all animations")); disableAllAnimationsButton.setText(tr("&Disable all animations"));
tapAnimationCheckBox.setText(tr("&Tap/untap animation")); tapAnimationCheckBox.setText(tr("&Tap/untap animation"));
arrowDrawAnimationCheckBox.setText(tr("&Arrow draw animation"));
lifeCounterAnimationsCheckBox.setText(tr("Life counter flash")); lifeCounterAnimationsCheckBox.setText(tr("Life counter flash"));
battlefieldFlashCheckBox.setText(tr("Battlefield flash on damage")); battlefieldFlashCheckBox.setText(tr("Battlefield flash on damage"));
deckEditorGroupBox->setTitle(tr("Deck editor/storage settings")); deckEditorGroupBox->setTitle(tr("Deck editor/storage settings"));

View file

@ -40,6 +40,7 @@ private:
QPushButton enableAllAnimationsButton; QPushButton enableAllAnimationsButton;
QPushButton disableAllAnimationsButton; QPushButton disableAllAnimationsButton;
QCheckBox tapAnimationCheckBox; QCheckBox tapAnimationCheckBox;
QCheckBox arrowDrawAnimationCheckBox;
QCheckBox lifeCounterAnimationsCheckBox; QCheckBox lifeCounterAnimationsCheckBox;
QCheckBox battlefieldFlashCheckBox; QCheckBox battlefieldFlashCheckBox;
QCheckBox openDeckInNewTabCheckBox; QCheckBox openDeckInNewTabCheckBox;

View file

@ -27,7 +27,6 @@
#include <QTimer> #include <QTimer>
#include <QTreeView> #include <QTreeView>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/models/deck_list/deck_list_model.h> #include <libcockatrice/models/deck_list/deck_list_model.h>
#include <libcockatrice/protocol/pb/command_deck_upload.pb.h> #include <libcockatrice/protocol/pb/command_deck_upload.pb.h>
#include <libcockatrice/protocol/pending_command.h> #include <libcockatrice/protocol/pending_command.h>
@ -67,9 +66,6 @@ void TabDeckEditorVisual::createCentralFrame()
centralFrame = new QVBoxLayout; centralFrame = new QVBoxLayout;
centralWidget->setLayout(centralFrame); centralWidget->setLayout(centralFrame);
auto databaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), true, this);
databaseModel->setObjectName("databaseModel");
tabContainer = new TabDeckEditorVisualTabWidget(centralWidget, this, deckStateManager->getModel(), databaseModel); tabContainer = new TabDeckEditorVisualTabWidget(centralWidget, this, deckStateManager->getModel(), databaseModel);
connect(tabContainer, &TabDeckEditorVisualTabWidget::cardChanged, this, connect(tabContainer, &TabDeckEditorVisualTabWidget::cardChanged, this,

View file

@ -25,7 +25,8 @@ TabDeckEditorVisualTabWidget::TabDeckEditorVisualTabWidget(QWidget *parent,
layout = new QVBoxLayout(this); layout = new QVBoxLayout(this);
setLayout(layout); setLayout(layout);
visualDeckView = new VisualDeckEditorWidget(this, deckModel, _deckEditor->deckDockWidget->getSelectionModel()); visualDeckView = new VisualDeckEditorWidget(this, deckModel, _deckEditor->deckDockWidget->getSelectionModel(),
_cardDatabaseModel);
visualDeckView->setObjectName("visualDeckView"); visualDeckView->setObjectName("visualDeckView");
connect(visualDeckView, &VisualDeckEditorWidget::activeCardChanged, this, connect(visualDeckView, &VisualDeckEditorWidget::activeCardChanged, this,
&TabDeckEditorVisualTabWidget::onCardChanged); &TabDeckEditorVisualTabWidget::onCardChanged);

View file

@ -82,17 +82,14 @@ VisualDatabaseDisplayFilterToolbarWidget::VisualDatabaseDisplayFilterToolbarWidg
void VisualDatabaseDisplayFilterToolbarWidget::initialize() void VisualDatabaseDisplayFilterToolbarWidget::initialize()
{ {
// create groupbox layouts auto sortLayout = new QHBoxLayout(sortGroupBox);
auto sortLayout = new QHBoxLayout(this);
sortLayout->setContentsMargins(0, 0, 0, 0); sortLayout->setContentsMargins(0, 0, 0, 0);
sortLayout->setSpacing(0); sortLayout->setSpacing(0);
sortGroupBox->setLayout(sortLayout);
sortLayout->setAlignment(Qt::AlignLeft); sortLayout->setAlignment(Qt::AlignLeft);
auto filterLayout = new QHBoxLayout(this); auto filterLayout = new QHBoxLayout(filterGroupBox);
filterLayout->setContentsMargins(0, 0, 0, 0); filterLayout->setContentsMargins(0, 0, 0, 0);
filterLayout->setSpacing(2); filterLayout->setSpacing(2);
filterGroupBox->setLayout(filterLayout);
filterLayout->setAlignment(Qt::AlignLeft); filterLayout->setAlignment(Qt::AlignLeft);
// create settings widgets // create settings widgets

View file

@ -16,6 +16,7 @@
#include <QHeaderView> #include <QHeaderView>
#include <QScrollBar> #include <QScrollBar>
#include <QShowEvent>
#include <QStyledItemDelegate> #include <QStyledItemDelegate>
#include <libcockatrice/card/card_info_comparator.h> #include <libcockatrice/card/card_info_comparator.h>
#include <libcockatrice/card/database/card_database.h> #include <libcockatrice/card/database/card_database.h>
@ -140,9 +141,6 @@ void VisualDatabaseDisplayWidget::initialize()
{ {
databaseLoadIndicator->setVisible(false); databaseLoadIndicator->setVisible(false);
filterContainer->initialize();
filterContainer->setVisible(true);
searchContainer->addWidget(colorFilterWidget); searchContainer->addWidget(colorFilterWidget);
searchContainer->addWidget(clearFilterWidget); searchContainer->addWidget(clearFilterWidget);
searchContainer->addWidget(searchEdit); searchContainer->addWidget(searchEdit);
@ -158,17 +156,43 @@ void VisualDatabaseDisplayWidget::initialize()
mainLayout->addWidget(cardSizeWidget); mainLayout->addWidget(cardSizeWidget);
databaseDisplayModel->setFilterTree(filterModel->filterTree());
connect(filterModel, &FilterTreeModel::layoutChanged, this, &VisualDatabaseDisplayWidget::onSearchModelChanged); connect(filterModel, &FilterTreeModel::layoutChanged, this, &VisualDatabaseDisplayWidget::onSearchModelChanged);
loadCardsTimer = new QTimer(this); initializeFilters();
loadCardsTimer->setSingleShot(true); // Ensure it only fires once after the timeout }
connect(loadCardsTimer, &QTimer::timeout, this, [this]() { loadCurrentPage(); }); void VisualDatabaseDisplayWidget::initializeFilters()
loadCardsTimer->start(5000); {
if (filtersInitialized || !isVisible() || CardDatabaseManager::getInstance()->getLoadStatus() != LoadStatus::Ok) {
return;
}
retranslateUi(); filtersInitialized = true;
// The filter toolbar builds its widgets by iterating the entire card database
// (per-set, per-main-type, per-sub-type and per-format buttons). Building it
// inside showEvent would block the tab switch, so keep it hidden and defer the
// build to the next event loop turn, letting the tab paint first. The toolbar
// then appears one event loop turn later, shifting the grid down by the toolbar
// height -- the intended tradeoff of an responsive tab switch.
filterContainer->setVisible(false);
QTimer::singleShot(0, this, [this] {
filterContainer->initialize();
filterContainer->setVisible(true);
databaseDisplayModel->setFilterTree(filterModel->filterTree());
QTimer::singleShot(5000, this, [this] { loadCurrentPage(); });
retranslateUi();
});
}
void VisualDatabaseDisplayWidget::showEvent(QShowEvent *event)
{
QWidget::showEvent(event);
initializeFilters();
} }
void VisualDatabaseDisplayWidget::retranslateUi() void VisualDatabaseDisplayWidget::retranslateUi()
@ -292,9 +316,17 @@ void VisualDatabaseDisplayWidget::loadCurrentPage()
{ {
// Ensure only the initial page is loaded // Ensure only the initial page is loaded
if (currentPage == 0) { if (currentPage == 0) {
// Only load the first page initially if (!initialLoadScheduled) {
qCDebug(VisualDatabaseDisplayLog) << "Loading the first page"; initialLoadScheduled = true;
populateCards(); qCDebug(VisualDatabaseDisplayLog) << "Loading the first page";
// Defer the first page so the tab switch stays responsive. The card
// grid builds one event loop turn later. This also applies to
// search-driven reloads, which reset currentPage back to 0.
QTimer::singleShot(0, this, [this] {
initialLoadScheduled = false;
populateCards();
});
}
} else if (nearEndOfPage()) { } else if (nearEndOfPage()) {
// If not the first page, just load the next page and append to the flow widget // If not the first page, just load the next page and append to the flow widget
loadNextPage(); loadNextPage();

View file

@ -115,17 +115,20 @@ private:
OverlapControlWidget *overlapControlWidget; OverlapControlWidget *overlapControlWidget;
CardSizeWidget *cardSizeWidget; CardSizeWidget *cardSizeWidget;
QTimer *debounceTimer; QTimer *debounceTimer;
QTimer *loadCardsTimer;
int debounceTime = 300; // in Ms int debounceTime = 300; // in Ms
int currentPage = 0; // Current page index int currentPage = 0; // Current page index
int cardsPerPage = 100; // Number of cards per page int cardsPerPage = 100; // Number of cards per page
bool filtersInitialized = false;
bool initialLoadScheduled = false;
void initializeFilters();
void highlightAllSearchEdit(); void highlightAllSearchEdit();
bool nearEndOfPage() const; bool nearEndOfPage() const;
protected: protected:
void resizeEvent(QResizeEvent *event) override; void resizeEvent(QResizeEvent *event) override;
void showEvent(QShowEvent *event) override;
}; };
#endif // VISUAL_DATABASE_DISPLAY_WIDGET_H #endif // VISUAL_DATABASE_DISPLAY_WIDGET_H

View file

@ -29,8 +29,10 @@
VisualDeckEditorWidget::VisualDeckEditorWidget(QWidget *parent, VisualDeckEditorWidget::VisualDeckEditorWidget(QWidget *parent,
DeckListModel *_deckListModel, DeckListModel *_deckListModel,
QItemSelectionModel *_selectionModel) QItemSelectionModel *_selectionModel,
: QWidget(parent), deckListModel(_deckListModel), selectionModel(_selectionModel) CardDatabaseModel *_cardDatabaseModel)
: QWidget(parent), deckListModel(_deckListModel), selectionModel(_selectionModel),
cardDatabaseModel(_cardDatabaseModel)
{ {
// The Main Widget and Main Layout, which contain a single Widget: The Scroll Area // The Main Widget and Main Layout, which contain a single Widget: The Scroll Area
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
@ -94,7 +96,9 @@ void VisualDeckEditorWidget::initializeSearchBarAndCompleter()
setFocusProxy(searchBar); setFocusProxy(searchBar);
setFocusPolicy(Qt::ClickFocus); setFocusPolicy(Qt::ClickFocus);
cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this); if (!cardDatabaseModel) {
cardDatabaseModel = new CardDatabaseModel(CardDatabaseManager::getInstance(), false, this);
}
cardDatabaseDisplayModel = new CardDatabaseDisplayModel(this); cardDatabaseDisplayModel = new CardDatabaseDisplayModel(this);
cardDatabaseDisplayModel->setSourceModel(cardDatabaseModel); cardDatabaseDisplayModel->setSourceModel(cardDatabaseModel);

View file

@ -38,7 +38,10 @@ class VisualDeckEditorWidget : public QWidget
Q_OBJECT Q_OBJECT
public: public:
explicit VisualDeckEditorWidget(QWidget *parent, DeckListModel *deckListModel, QItemSelectionModel *selectionModel); explicit VisualDeckEditorWidget(QWidget *parent,
DeckListModel *deckListModel,
QItemSelectionModel *selectionModel,
CardDatabaseModel *_cardDatabaseModel = nullptr);
void retranslateUi(); void retranslateUi();
void updateCompactMode(); void updateCompactMode();
void clearAllDisplayWidgets(); void clearAllDisplayWidgets();

View file

@ -12,6 +12,17 @@ CardDatabaseQuerier::CardDatabaseQuerier(QObject *_parent,
const ICardPreferenceProvider *prefs) const ICardPreferenceProvider *prefs)
: QObject(_parent), db(_db), prefs(prefs) : QObject(_parent), db(_db), prefs(prefs)
{ {
// Invalidate the cached count maps whenever the database contents change.
connect(db, &CardDatabase::cardAdded, this, &CardDatabaseQuerier::invalidateCaches);
connect(db, &CardDatabase::cardRemoved, this, &CardDatabaseQuerier::invalidateCaches);
connect(db, &CardDatabase::cardDatabaseReset, this, &CardDatabaseQuerier::invalidateCaches);
}
void CardDatabaseQuerier::invalidateCaches()
{
mainCardTypeCountsCache.clear();
subCardTypeCountsCache.clear();
formatsCountCache.clear();
} }
/** /**
@ -304,44 +315,43 @@ QString CardDatabaseQuerier::getPreferredPrintingProviderId(const QString &cardN
QStringList CardDatabaseQuerier::getAllMainCardTypes() const QStringList CardDatabaseQuerier::getAllMainCardTypes() const
{ {
QSet<QString> types; return getAllMainCardTypesWithCount().keys();
for (const auto &card : db->cards.values()) {
types.insert(card->getMainCardType());
}
return types.values();
} }
QMap<QString, int> CardDatabaseQuerier::getAllMainCardTypesWithCount() const QMap<QString, int> CardDatabaseQuerier::getAllMainCardTypesWithCount() const
{ {
QMap<QString, int> typeCounts; // An empty cache is always recomputed correctly: a database with no cards
// produces an empty map, so the cache is only ever empty when it needs a
for (const auto &card : db->cards.values()) { // (trivially cheap) rebuild.
QString type = card->getMainCardType(); if (mainCardTypeCountsCache.isEmpty()) {
typeCounts[type]++; for (const auto &card : db->cards.values()) {
QString type = card->getMainCardType();
mainCardTypeCountsCache[type]++;
}
} }
return typeCounts; return mainCardTypeCountsCache;
} }
QMap<QString, int> CardDatabaseQuerier::getAllSubCardTypesWithCount() const QMap<QString, int> CardDatabaseQuerier::getAllSubCardTypesWithCount() const
{ {
QMap<QString, int> typeCounts; if (subCardTypeCountsCache.isEmpty()) {
for (const auto &card : db->cards.values()) {
QString type = card->getCardType();
for (const auto &card : db->cards.values()) { QStringList parts = type.split(" — ");
QString type = card->getCardType();
QStringList parts = type.split(" — "); if (parts.size() > 1) { // Ensure there are subtypes
QStringList subtypes = parts[1].split(" ", Qt::SkipEmptyParts);
if (parts.size() > 1) { // Ensure there are subtypes for (const QString &subtype : subtypes) {
QStringList subtypes = parts[1].split(" ", Qt::SkipEmptyParts); subCardTypeCountsCache[subtype]++;
}
for (const QString &subtype : subtypes) {
typeCounts[subtype]++;
} }
} }
} }
return typeCounts; return subCardTypeCountsCache;
} }
FormatRulesPtr CardDatabaseQuerier::getFormat(const QString &formatName) const FormatRulesPtr CardDatabaseQuerier::getFormat(const QString &formatName) const
@ -351,18 +361,18 @@ FormatRulesPtr CardDatabaseQuerier::getFormat(const QString &formatName) const
QMap<QString, int> CardDatabaseQuerier::getAllFormatsWithCount() const QMap<QString, int> CardDatabaseQuerier::getAllFormatsWithCount() const
{ {
QMap<QString, int> formatCounts; if (formatsCountCache.isEmpty()) {
for (const auto &card : db->cards.values()) {
QStringList allProps = card->getProperties();
for (const auto &card : db->cards.values()) { for (const QString &prop : allProps) {
QStringList allProps = card->getProperties(); if (prop.startsWith("format-")) {
QString formatName = prop.mid(QStringLiteral("format-").size());
for (const QString &prop : allProps) { formatsCountCache[formatName]++;
if (prop.startsWith("format-")) { }
QString formatName = prop.mid(QStringLiteral("format-").size());
formatCounts[formatName]++;
} }
} }
} }
return formatCounts; return formatsCountCache;
} }

View file

@ -220,6 +220,16 @@ public:
private: private:
const CardDatabase *db; //!< Card database used for all lookups. const CardDatabase *db; //!< Card database used for all lookups.
const ICardPreferenceProvider *prefs; //!< Preference provider for preferred printings. const ICardPreferenceProvider *prefs; //!< Preference provider for preferred printings.
// Count maps are expensive to compute (they iterate the whole database) and are
// queried every time a filter widget is built, so cache them and invalidate on
// any database mutation. Only the main thread reads or writes these.
mutable QMap<QString, int> mainCardTypeCountsCache;
mutable QMap<QString, int> subCardTypeCountsCache;
mutable QMap<QString, int> formatsCountCache;
private slots:
void invalidateCaches();
}; };
#endif // COCKATRICE_CARD_DATABASE_QUERIER_H #endif // COCKATRICE_CARD_DATABASE_QUERIER_H

View file

@ -15,6 +15,7 @@ public:
[[nodiscard]] virtual bool getIncludeRebalancedCards() const = 0; [[nodiscard]] virtual bool getIncludeRebalancedCards() const = 0;
[[nodiscard]] virtual bool getPrintingSelectorNavigationButtonsVisible() const = 0; [[nodiscard]] virtual bool getPrintingSelectorNavigationButtonsVisible() const = 0;
[[nodiscard]] virtual bool getTapAnimation() const = 0; [[nodiscard]] virtual bool getTapAnimation() const = 0;
[[nodiscard]] virtual bool getArrowDrawAnimation() const = 0;
[[nodiscard]] virtual bool getAutoRotateSidewaysLayoutCards() const = 0; [[nodiscard]] virtual bool getAutoRotateSidewaysLayoutCards() const = 0;
[[nodiscard]] virtual bool getScaleCards() const = 0; [[nodiscard]] virtual bool getScaleCards() const = 0;
[[nodiscard]] virtual int getStackCardOverlapPercent() const = 0; [[nodiscard]] virtual int getStackCardOverlapPercent() const = 0;

View file

@ -111,19 +111,50 @@ bool CardDatabaseModel::checkCardHasAtLeastOneEnabledSet(const CardInfoPtr &card
void CardDatabaseModel::cardDatabaseEnabledSetsChanged() void CardDatabaseModel::cardDatabaseEnabledSetsChanged()
{ {
// remove all the cards no more present in at least one enabled set // Build the new card list in a single pass.
QList<CardInfoPtr> newCardList;
newCardList.reserve(cardList.size());
for (const CardInfoPtr &card : cardList) { for (const CardInfoPtr &card : cardList) {
if (!checkCardHasAtLeastOneEnabledSet(card)) { if (checkCardHasAtLeastOneEnabledSet(card)) {
cardRemoved(card); newCardList.append(card);
}
}
for (const CardInfoPtr &card : db->getCardList()) {
if (!cardListSet.contains(card) && checkCardHasAtLeastOneEnabledSet(card)) {
newCardList.append(card);
} }
} }
// re-check all the card currently not shown, maybe their part of a newly-enabled set if (newCardList == cardList) {
for (const CardInfoPtr &card : db->getCardList()) { return;
if (!cardListSet.contains(card)) { }
cardAdded(card);
// Rebuild the whole list inside a single model reset instead of emitting
// per-card insert/remove notifications. With tens of thousands of cards the
// per-card path is the dominant cost of constructing a CardDatabaseModel.
QSet<CardInfoPtr> oldCardListSet = cardListSet;
QSet<CardInfoPtr> newCardListSet(newCardList.begin(), newCardList.end());
beginResetModel();
// Disconnect cards that are no longer shown.
for (const CardInfoPtr &card : cardList) {
if (!newCardListSet.contains(card)) {
disconnect(card.data(), nullptr, this, nullptr);
} }
} }
cardList = newCardList;
cardListSet = newCardListSet;
// Connect cards that are now shown for the first time.
for (const CardInfoPtr &card : cardList) {
if (!oldCardListSet.contains(card)) {
connect(card.data(), &CardInfo::cardInfoChanged, this, &CardDatabaseModel::cardInfoChanged);
}
}
endResetModel();
} }
void CardDatabaseModel::cardAdded(const CardInfoPtr &card) void CardDatabaseModel::cardAdded(const CardInfoPtr &card)

View file

@ -133,6 +133,22 @@ void Server_ProtocolHandler::sendProtocolItem(const RoomEvent &item)
Response::ResponseCode Server_ProtocolHandler::processSessionCommandContainer(const CommandContainer &cont, Response::ResponseCode Server_ProtocolHandler::processSessionCommandContainer(const CommandContainer &cont,
ResponseContainer &rc) ResponseContainer &rc)
{ {
const auto isPreAuthSessionCommand = [](SessionCommand::SessionCommandType type) {
switch (type) {
case SessionCommand::PING:
case SessionCommand::LOGIN:
case SessionCommand::REGISTER:
case SessionCommand::ACTIVATE:
case SessionCommand::FORGOT_PASSWORD_REQUEST:
case SessionCommand::FORGOT_PASSWORD_RESET:
case SessionCommand::FORGOT_PASSWORD_CHALLENGE:
case SessionCommand::REQUEST_PASSWORD_SALT:
return true;
default:
return false;
}
};
Response::ResponseCode finalResponseCode = Response::RespOk; Response::ResponseCode finalResponseCode = Response::RespOk;
for (int i = cont.session_command_size() - 1; i >= 0; --i) { for (int i = cont.session_command_size() - 1; i >= 0; --i) {
Response::ResponseCode resp = Response::RespInvalidCommand; Response::ResponseCode resp = Response::RespInvalidCommand;
@ -141,33 +157,38 @@ Response::ResponseCode Server_ProtocolHandler::processSessionCommandContainer(co
if (num != SessionCommand::PING) { // don't log ping commands if (num != SessionCommand::PING) { // don't log ping commands
logDebugMessage(getSafeDebugString(sc)); logDebugMessage(getSafeDebugString(sc));
} }
switch ((SessionCommand::SessionCommandType)num) { const auto commandType = static_cast<SessionCommand::SessionCommandType>(num);
case SessionCommand::PING: if (authState == NotLoggedIn && !isPreAuthSessionCommand(commandType)) {
resp = cmdPing(sc.GetExtension(Command_Ping::ext), rc); resp = Response::RespLoginNeeded;
break; } else {
case SessionCommand::LOGIN: switch (commandType) {
resp = cmdLogin(sc.GetExtension(Command_Login::ext), rc); case SessionCommand::PING:
break; resp = cmdPing(sc.GetExtension(Command_Ping::ext), rc);
case SessionCommand::MESSAGE: break;
resp = cmdMessage(sc.GetExtension(Command_Message::ext), rc); case SessionCommand::LOGIN:
break; resp = cmdLogin(sc.GetExtension(Command_Login::ext), rc);
case SessionCommand::GET_GAMES_OF_USER: break;
resp = cmdGetGamesOfUser(sc.GetExtension(Command_GetGamesOfUser::ext), rc); case SessionCommand::MESSAGE:
break; resp = cmdMessage(sc.GetExtension(Command_Message::ext), rc);
case SessionCommand::GET_USER_INFO: break;
resp = cmdGetUserInfo(sc.GetExtension(Command_GetUserInfo::ext), rc); case SessionCommand::GET_GAMES_OF_USER:
break; resp = cmdGetGamesOfUser(sc.GetExtension(Command_GetGamesOfUser::ext), rc);
case SessionCommand::LIST_ROOMS: break;
resp = cmdListRooms(sc.GetExtension(Command_ListRooms::ext), rc); case SessionCommand::GET_USER_INFO:
break; resp = cmdGetUserInfo(sc.GetExtension(Command_GetUserInfo::ext), rc);
case SessionCommand::JOIN_ROOM: break;
resp = cmdJoinRoom(sc.GetExtension(Command_JoinRoom::ext), rc); case SessionCommand::LIST_ROOMS:
break; resp = cmdListRooms(sc.GetExtension(Command_ListRooms::ext), rc);
case SessionCommand::LIST_USERS: break;
resp = cmdListUsers(sc.GetExtension(Command_ListUsers::ext), rc); case SessionCommand::JOIN_ROOM:
break; resp = cmdJoinRoom(sc.GetExtension(Command_JoinRoom::ext), rc);
default: break;
resp = processExtendedSessionCommand(num, sc, rc); case SessionCommand::LIST_USERS:
resp = cmdListUsers(sc.GetExtension(Command_ListUsers::ext), rc);
break;
default:
resp = processExtendedSessionCommand(num, sc, rc);
}
} }
if (resp != Response::RespOk) { if (resp != Response::RespOk) {
finalResponseCode = resp; finalResponseCode = resp;

View file

@ -50,6 +50,11 @@ bool CardsDisplaySettings::getTapAnimation() const
return getValue("tapAnimation", QString(), QString(), true).toBool(); return getValue("tapAnimation", QString(), QString(), true).toBool();
} }
bool CardsDisplaySettings::getArrowDrawAnimation() const
{
return getValue("arrowDrawAnimation", QString(), QString(), true).toBool();
}
bool CardsDisplaySettings::getAutoRotateSidewaysLayoutCards() const bool CardsDisplaySettings::getAutoRotateSidewaysLayoutCards() const
{ {
return getValue("autoRotateSidewaysLayoutCards", QString(), QString(), true).toBool(); return getValue("autoRotateSidewaysLayoutCards", QString(), QString(), true).toBool();
@ -159,6 +164,11 @@ void CardsDisplaySettings::setTapAnimation(bool _tapAnimation)
setValue(_tapAnimation, "tapAnimation"); setValue(_tapAnimation, "tapAnimation");
} }
void CardsDisplaySettings::setArrowDrawAnimation(bool _arrowDrawAnimation)
{
setValue(_arrowDrawAnimation, "arrowDrawAnimation");
}
void CardsDisplaySettings::setAutoRotateSidewaysLayoutCards(bool _autoRotateSidewaysLayoutCards) void CardsDisplaySettings::setAutoRotateSidewaysLayoutCards(bool _autoRotateSidewaysLayoutCards)
{ {
setValue(_autoRotateSidewaysLayoutCards, "autoRotateSidewaysLayoutCards"); setValue(_autoRotateSidewaysLayoutCards, "autoRotateSidewaysLayoutCards");

View file

@ -20,6 +20,7 @@ public:
[[nodiscard]] bool getIncludeRebalancedCards() const override; [[nodiscard]] bool getIncludeRebalancedCards() const override;
[[nodiscard]] bool getPrintingSelectorNavigationButtonsVisible() const override; [[nodiscard]] bool getPrintingSelectorNavigationButtonsVisible() const override;
[[nodiscard]] bool getTapAnimation() const override; [[nodiscard]] bool getTapAnimation() const override;
[[nodiscard]] bool getArrowDrawAnimation() const override;
[[nodiscard]] bool getAutoRotateSidewaysLayoutCards() const override; [[nodiscard]] bool getAutoRotateSidewaysLayoutCards() const override;
[[nodiscard]] bool getScaleCards() const override; [[nodiscard]] bool getScaleCards() const override;
[[nodiscard]] int getStackCardOverlapPercent() const override; [[nodiscard]] int getStackCardOverlapPercent() const override;
@ -40,6 +41,7 @@ public:
void setIncludeRebalancedCards(bool _includeRebalancedCards); void setIncludeRebalancedCards(bool _includeRebalancedCards);
void setPrintingSelectorNavigationButtonsVisible(bool _navigationButtonsVisible); void setPrintingSelectorNavigationButtonsVisible(bool _navigationButtonsVisible);
void setTapAnimation(bool _tapAnimation); void setTapAnimation(bool _tapAnimation);
void setArrowDrawAnimation(bool _arrowDrawAnimation);
void setAutoRotateSidewaysLayoutCards(bool _autoRotateSidewaysLayoutCards); void setAutoRotateSidewaysLayoutCards(bool _autoRotateSidewaysLayoutCards);
void setCardScaling(bool _scaleCards); void setCardScaling(bool _scaleCards);
void setStackCardOverlapPercent(int _verticalCardOverlapPercent); void setStackCardOverlapPercent(int _verticalCardOverlapPercent);

View file

@ -896,6 +896,10 @@ Response::ResponseCode AbstractServerSocketInterface::cmdReplayGetCode(const Com
Response::ResponseCode AbstractServerSocketInterface::cmdReplaySubmitCode(const Command_ReplaySubmitCode &cmd, Response::ResponseCode AbstractServerSocketInterface::cmdReplaySubmitCode(const Command_ReplaySubmitCode &cmd,
ResponseContainer & /*rc*/) ResponseContainer & /*rc*/)
{ {
if (authState != PasswordRight) {
return Response::RespFunctionNotAllowed;
}
// code is of the form <game-id>-<hash> // code is of the form <game-id>-<hash>
QString code = QString::fromStdString(cmd.replay_code()); QString code = QString::fromStdString(cmd.replay_code());
QStringList split = code.split("-"); QStringList split = code.split("-");

View file

@ -476,6 +476,12 @@ TEST_F(SettingsDefaultsTest, CardsDisplay_SampleHandSize_Default)
ASSERT_EQ(s.getSampleHandSize(), 7); ASSERT_EQ(s.getSampleHandSize(), 7);
} }
TEST_F(SettingsDefaultsTest, CardsDisplay_ArrowDrawAnimation_Default)
{
CardsDisplaySettings s(settingsPath, nullptr);
ASSERT_EQ(s.getArrowDrawAnimation(), true);
}
// --- VisualDeckStorageSettings --- // --- VisualDeckStorageSettings ---
TEST_F(SettingsDefaultsTest, VisualDeckStorage_SortingOrder_Default) TEST_F(SettingsDefaultsTest, VisualDeckStorage_SortingOrder_Default)