Compare commits

...

4 commits

Author SHA1 Message Date
BruebachL
205e1ecebf
[CardDB] Keep parse-time set filtering and bust the cache on enablement changes (#7448)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
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 13 Intel (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 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
The parsers keep dropping printings for disabled sets so that disabling a
set hides them everywhere, not just in deck editor search. The cached parse
output therefore depends on enablement, which the source hash did not cover:
the first parse froze whatever enablement state existed at that moment and
later toggles hit the stale cache.

- Fold a parser schema version and the sorted enabled set names into the
  binary cache source hash so toggling a set or enabling newly discovered
  sets (including custom ones) forces a re-parse on the next load.
- Harden the legacy set migration to only write keys that exist so missing
  legacy values can no longer clobber cardDatabase.ini and silently disable
  sets on upgrade.
- Make CardDatabase::reloadCardDatabasesAndNotify() bypass the binary cache
  so an explicit reload always re-reads the files on disk, even when their
  size/mtime hash still matches.
- Cover cache invalidation end to end: sandboxed loads assert that disabling
  a set, adding a custom database and enabling a fresh set each rewrite the
  cache, that a no-op load keeps it, and that an explicit reload picks up
  edits the source hash cannot see.

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-09 06:09:18 +02:00
BruebachL
aaafa72c0e
[Game] Use native title bar height for ZoneViewWidget close button (#7450)
* [Game] Use native title bar height for ZoneViewWidget close button

Compute title bar height from style (PM_TitleBarHeight) instead of hardcoded 24.0 to align hit box with native close button.

* [Game] Fix ZoneViewWidget title bar height compile errors

Drop the redundant static on the anonymous namespace helper, stop
redeclaring titleBarHeight's parameter and reuse tbh in the fallback
path, and restore the position clamp that still referenced the removed
kTitleBarHeight constant.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-09 06:08:15 +02:00
BruebachL
56c456414b
[Game] Use non-blocking popup for zone context menus (#7452)
Replace QMenu::exec with popup() for zone context menus to avoid blocking the scene.

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-09 06:07:39 +02:00
BruebachL
95f02ac0a1
[CardDB] Drop the rebalanced-cards setting entirely (#7457)
* [CardDB] Drop rebalanced-card filtering from the v4 parser

Rebalanced cards no longer exist, so the includeRebalancedCards
preference has no data to filter. Always keep printings marked
isRebalanced and remove the now-unused ICardPreferenceProvider
plumbing from CockatriceXml4Parser and CardDatabaseLoader.

The setting itself (CardsDisplaySettings, Manage Sets checkbox,
Oracle attribute mapping) is removed in a follow-up.

* [CardDB] Drop the rebalanced-cards setting entirely

Rebalanced Alchemy cards no longer exist, so remove the
includeRebalancedCards preference surface: the CardsDisplaySettings
getter/setter/signal, the ICardPreferenceProvider and
ICardsDisplaySettingsProvider methods, the Manage Sets checkbox, the
legacy settings migration mapping, the isRebalanced attribute mapping
in the Oracle importer, and the isRebalanced attribute in the v4
schema.

Builds on #7448, which already stopped the v4 parser from filtering
on the preference.

* [CardDB] Drop the unused isOnlineOnly printing property

The Arena-card detection switched from isOnlineOnly to isRebalanced in
#5778, so nothing has read this printing property since; it only
round-trips from the Oracle importer into cards.xml. Remove the
importer mapping, the benchmark fixture field and the v4 schema
attribute so regenerated databases stop carrying it.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-09 06:02:47 +02:00
26 changed files with 397 additions and 115 deletions

View file

@ -13,11 +13,6 @@ public:
{
return SettingsCache::instance().cardOverrides().getCardPreferenceOverride(cardName);
}
[[nodiscard]] bool getIncludeRebalancedCards() const override
{
return SettingsCache::instance().cardsDisplay().getIncludeRebalancedCards();
}
};
#endif // COCKATRICE_SETTINGS_CARD_PREFERENCE_PROVIDER_H

View file

@ -48,7 +48,7 @@ void CardZone::mouseDoubleClickEvent(QGraphicsSceneMouseEvent * /*event*/)
bool CardZone::showContextMenu(const QPoint &screenPos)
{
if (menu) {
menu->exec(screenPos);
menu->popup(screenPos);
return true;
}
return false;

View file

@ -51,7 +51,13 @@
namespace
{
constexpr qreal kTitleBarHeight = 24.0;
qreal titleBarHeight(QWidget *styleWidget)
{
if (styleWidget) {
return static_cast<qreal>(styleWidget->style()->pixelMetric(QStyle::PM_TitleBarHeight, nullptr, styleWidget));
}
return 24.0;
}
constexpr qreal kMinVisibleWidth = 100.0;
} // namespace
@ -315,7 +321,8 @@ QRectF ZoneViewWidget::closeButtonRect(QWidget *styleWidget) const
// query the style for the close button position (handles macOS top-left placement)
// Title bar rect MUST be local (0,0-based) for QStyle
const QRect titleBarRect(0, 0, static_cast<int>(frameRectF.width()), static_cast<int>(kTitleBarHeight));
const qreal tbh = titleBarHeight(styleWidget);
const QRect titleBarRect(0, 0, static_cast<int>(frameRectF.width()), static_cast<int>(tbh));
if (styleWidget) {
QStyleOptionTitleBar opt;
@ -344,7 +351,7 @@ QRectF ZoneViewWidget::closeButtonRect(QWidget *styleWidget) const
}
// Fallback: frame-relative top-right
return QRectF(frameRectF.right() - kTitleBarHeight, frameRectF.top(), kTitleBarHeight, kTitleBarHeight);
return QRectF(frameRectF.right() - tbh, frameRectF.top(), tbh, tbh);
}
QGraphicsView *ZoneViewWidget::findDragView(QWidget *eventWidget) const
@ -468,8 +475,9 @@ QVariant ZoneViewWidget::itemChange(GraphicsItemChange change, const QVariant &v
const qreal minX = sceneRect.left();
const qreal maxX = qMax(minX, sceneRect.right() - kMinVisibleWidth);
const qreal minY = sceneRect.top() + kTitleBarHeight;
const qreal maxY = qMax(minY, sceneRect.bottom() - kTitleBarHeight);
const qreal tbh = titleBarHeight(nullptr);
const qreal minY = sceneRect.top() + tbh;
const qreal maxY = qMax(minY, sceneRect.bottom() - tbh);
desiredPos.setX(qBound(minX, desiredPos.x(), maxX));
desiredPos.setY(qBound(minY, desiredPos.y(), maxY));

View file

@ -178,12 +178,6 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent)
sortWarning->setLayout(sortWarningLayout);
sortWarning->setVisible(false);
includeRebalancedCards = SettingsCache::instance().cardsDisplay().getIncludeRebalancedCards();
QCheckBox *includeRebalancedCardsCheckBox =
new QCheckBox(tr("Include cards rebalanced for Alchemy [requires restart]"));
includeRebalancedCardsCheckBox->setChecked(includeRebalancedCards);
connect(includeRebalancedCardsCheckBox, &QAbstractButton::toggled, this, &WndSets::includeRebalancedCardsChanged);
buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(buttonBox, &QDialogButtonBox::accepted, this, &WndSets::actSave);
connect(buttonBox, &QDialogButtonBox::rejected, this, &WndSets::actRestore);
@ -197,9 +191,8 @@ WndSets::WndSets(QWidget *parent) : QMainWindow(parent)
mainLayout->addWidget(enableSomeButton, 2, 1);
mainLayout->addWidget(disableSomeButton, 2, 2);
mainLayout->addWidget(sortWarning, 3, 1, 1, 2);
mainLayout->addWidget(includeRebalancedCardsCheckBox, 4, 1, 1, 2);
mainLayout->addWidget(hintsGroupBox, 5, 1, 1, 2);
mainLayout->addWidget(buttonBox, 6, 1, 1, 2);
mainLayout->addWidget(hintsGroupBox, 4, 1, 1, 2);
mainLayout->addWidget(buttonBox, 5, 1, 1, 2);
mainLayout->setColumnStretch(1, 1);
mainLayout->setColumnStretch(2, 1);
@ -270,15 +263,9 @@ void WndSets::resetSort()
sortWarning->setVisible(false);
}
void WndSets::includeRebalancedCardsChanged(bool _includeRebalancedCards)
{
includeRebalancedCards = _includeRebalancedCards;
}
void WndSets::actSave()
{
model->save(CardDatabaseManager::getInstance());
SettingsCache::instance().cardsDisplay().setIncludeRebalancedCards(includeRebalancedCards);
CardPictureLoader::clearPixmapCache();
const auto reloadOk1 = QtConcurrent::run([] {
CardDatabaseManager::getInstance()->reloadCardDatabasesAndNotify();

View file

@ -52,7 +52,6 @@ private:
int sortIndex;
Qt::SortOrder sortOrder;
bool setOrderIsSorted;
bool includeRebalancedCards;
enum
{
NO_SETS_SELECTED,
@ -87,7 +86,6 @@ private slots:
void actDisableResetButton(const QString &filterText);
void actSort(int index);
void actIgnoreWarning();
void includeRebalancedCardsChanged(bool _includeRebalancedCardsChanged);
};
#endif

View file

@ -29,8 +29,6 @@
<xs:attribute type="xs:anyURI" name="picURL" use="optional" />
<xs:attribute type="xs:string" name="num" use="optional" />
<xs:attribute type="xs:string" name="rarity" use="optional" />
<xs:attribute type="xs:boolean" name="isOnlineOnly" use="optional" />
<xs:attribute type="xs:boolean" name="isRebalanced" use="optional" />
<xs:attribute type="xs:string" name="flavorName" use="optional"/>
</xs:extension>
</xs:simpleContent>

View file

@ -33,7 +33,7 @@ CardDatabase::CardDatabase(QObject *parent,
qRegisterMetaType<CardDatabaseData>("CardDatabaseData");
// create loader and wire it up
loader = new CardDatabaseLoader(this, this, pathProvider, prefs, setPriorityController);
loader = new CardDatabaseLoader(this, this, pathProvider, setPriorityController);
// re-emit loader signals (so other code doesn't need to know about internals)
connect(loader, &CardDatabaseLoader::loadingFinished, this, &CardDatabase::cardDatabaseLoadingFinished);
connect(loader, &CardDatabaseLoader::loadingFailed, this, &CardDatabase::cardDatabaseLoadingFailed);
@ -78,7 +78,9 @@ void CardDatabase::loadCardDatabases()
void CardDatabase::reloadCardDatabasesAndNotify()
{
loadCardDatabases();
// An explicit reload bypasses the binary cache so the files on disk are
// always re-read, even when their size/mtime hash still matches the cache.
loadStatus = loader->loadCardDatabases(/* ignoreCache */ true);
if (loadStatus == Ok) {
checkUnknownSets();

View file

@ -177,8 +177,12 @@ public slots:
void addFormat(const FormatRulesPtr &format);
/** @brief Loads card databases from configured paths. */
/** @brief Loads card databases from configured paths, using the binary cache when valid. */
void loadCardDatabases();
/**
* @brief Reloads card databases from disk, bypassing the binary cache,
* and notifies observers of the fresh state.
*/
void reloadCardDatabasesAndNotify();
/** @brief Saves custom tokens to file.

View file

@ -6,6 +6,7 @@
#include "libcockatrice/card/database/card_database_data.h"
#include "libcockatrice/card/set/card_set.h"
#include "libcockatrice/interfaces/interface_card_database_path_provider.h"
#include "libcockatrice/interfaces/interface_card_set_priority_controller.h"
#include "parser/card_database_parser.h"
#include "parser/cockatrice_xml_3.h"
#include "parser/cockatrice_xml_4.h"
@ -28,25 +29,22 @@
#include <utility>
class ICardPreferenceProvider;
class ICardSetPriorityController;
CardDatabaseLoader::CardDatabaseLoader(QObject *parent,
CardDatabase *db,
ICardDatabasePathProvider *_pathProvider,
ICardPreferenceProvider *_preferenceProvider,
ICardSetPriorityController *_priorityController)
: QObject(parent), database(db), pathProvider(_pathProvider), priorityController(_priorityController)
{
// instantiate available parsers here
availableParsers << new CockatriceXml4Parser(_preferenceProvider, _priorityController);
availableParsers << new CockatriceXml4Parser(_priorityController);
availableParsers << new CockatriceXml3Parser(_priorityController);
// The load path parses into a snapshot and never emits per-card signals;
// the finished snapshot is swapped into the live database on the GUI thread.
// when SettingsCache's path changes, trigger reloads
connect(pathProvider, &ICardDatabasePathProvider::cardDatabasePathChanged, this,
&CardDatabaseLoader::loadCardDatabases);
connect(pathProvider, &ICardDatabasePathProvider::cardDatabasePathChanged, this, [this] { loadCardDatabases(); });
}
CardDatabaseLoader::~CardDatabaseLoader()
@ -91,12 +89,12 @@ LoadStatus CardDatabaseLoader::loadCardDatabase(const QString &path, CardDatabas
return tempLoadStatus;
}
LoadStatus CardDatabaseLoader::loadCardDatabases()
LoadStatus CardDatabaseLoader::loadCardDatabases(bool ignoreCache)
{
return doLoadCardDatabases();
return doLoadCardDatabases(ignoreCache);
}
LoadStatus CardDatabaseLoader::doLoadCardDatabases()
LoadStatus CardDatabaseLoader::doLoadCardDatabases(bool ignoreCache)
{
QMutexLocker locker(reloadDatabaseMutex);
@ -114,9 +112,14 @@ LoadStatus CardDatabaseLoader::doLoadCardDatabases()
LoadStatus loadStatus = NotLoaded;
// Try the binary cache first: a cache hit avoids re-parsing the (large) XML.
// An explicit reload skips the lookup so the files on disk are always
// re-read, even when their size/mtime hash still matches the cache.
const QStringList customPaths = collectCustomDatabasePaths();
const QByteArray sourceHash = computeSourceHash(customPaths);
if (loadFromCache(data, sourceHash)) {
if (ignoreCache) {
qCInfo(CardDatabaseLoadingLog) << "Ignoring binary cache, re-parsing source files";
}
if (!ignoreCache && loadFromCache(data, sourceHash)) {
qCInfo(CardDatabaseLoadingLog) << "Loaded card database from binary cache";
loadStatus = Ok;
} else {
@ -181,6 +184,12 @@ QByteArray CardDatabaseLoader::computeSourceHash(const QStringList &customPaths)
hash.addData(QCoreApplication::applicationVersion().toUtf8());
hash.addData(QByteArray(1, '\0'));
// Include parser schema version to invalidate caches when parser behavior changes.
// Increment this if the parser logic changes in a way that affects cached data.
static const char SCHEMA_VERSION[] = "carddb-parse-schema-v1";
hash.addData(QByteArray(SCHEMA_VERSION, sizeof(SCHEMA_VERSION) - 1));
hash.addData(QByteArray(1, '\0'));
const QStringList inputs = QStringList()
<< pathProvider->getCardDatabasePath() << pathProvider->getTokenDatabasePath()
<< pathProvider->getSpoilerCardDatabasePath() << customPaths;
@ -195,6 +204,17 @@ QByteArray CardDatabaseLoader::computeSourceHash(const QStringList &customPaths)
hash.addData(QByteArray(1, '\0'));
}
}
// The parsers drop printings of disabled sets at parse time, so the cached
// snapshot depends on enablement: fold the enabled set names in so that
// toggling a set invalidates the cache and forces a re-parse.
if (priorityController) {
const QStringList enabledSets = priorityController->getEnabledSetNames();
for (const QString &setName : enabledSets) {
hash.addData(setName.toUtf8());
hash.addData(QByteArray(1, '\0'));
}
}
return hash.result();
}

View file

@ -11,7 +11,6 @@
#include <qtmetamacros.h>
class ICardDatabasePathProvider;
class ICardPreferenceProvider;
class ICardSetPriorityController;
// IWYU pragma: keep
@ -59,12 +58,10 @@ public:
* @param parent QObject parent.
* @param db Pointer to the CardDatabase to populate (non-owning).
* @param pathProvider Provider for card database file paths.
* @param preferenceProvider Optional card preference provider for pinned printings.
*/
explicit CardDatabaseLoader(QObject *parent,
CardDatabase *db,
ICardDatabasePathProvider *pathProvider,
ICardPreferenceProvider *preferenceProvider,
ICardSetPriorityController *_priorityController);
/** @brief Destructor cleans up allocated parsers. */
@ -76,9 +73,13 @@ public slots:
*
* Runs synchronously on the calling thread. The caller should ensure
* that any signal receivers are already connected before invoking this.
* @param ignoreCache Skip the binary cache lookup and re-parse the source
* files from disk, refreshing the cache afterwards. Used for
* explicit reloads so that edits the source hash cannot see are
* still picked up.
* @return Status of the main database load.
*/
LoadStatus loadCardDatabases();
LoadStatus loadCardDatabases(bool ignoreCache = false);
/**
* @brief Loads a single card database file into the given snapshot.
@ -131,9 +132,10 @@ private:
/**
* @brief Performs the actual load work synchronously on the calling thread.
* @param ignoreCache Skip the binary cache lookup and re-parse from disk.
* @return Status of the main database load.
*/
LoadStatus doLoadCardDatabases();
LoadStatus doLoadCardDatabases(bool ignoreCache);
/**
* @brief Collects custom card database paths recursively.

View file

@ -37,9 +37,8 @@ class ICardSetPriorityController;
#define COCKATRICE_XML4_SCHEMALOCATION \
"https://raw.githubusercontent.com/Cockatrice/Cockatrice/master/doc/carddatabase_v4/cards.xsd"
CockatriceXml4Parser::CockatriceXml4Parser(ICardPreferenceProvider *_cardPreferenceProvider,
ICardSetPriorityController *_cardSetPriorityController)
: ICardDatabaseParser(_cardSetPriorityController), cardPreferenceProvider(_cardPreferenceProvider)
CockatriceXml4Parser::CockatriceXml4Parser(ICardSetPriorityController *_cardSetPriorityController)
: ICardDatabaseParser(_cardSetPriorityController)
{
}
@ -284,7 +283,6 @@ QHash<QString, QString> CockatriceXml4Parser::loadCardPropertiesFromXml(QXmlStre
void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml)
{
bool includeRebalancedCards = cardPreferenceProvider->getIncludeRebalancedCards();
while (!xml.atEnd()) {
if (xml.readNext() == QXmlStreamReader::EndElement) {
break;
@ -386,17 +384,7 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml)
printingProps.insert(attrName, attr.value().toString());
}
PrintingInfo printingInfo(set, LazyPropertiesHash(printingProps));
// This is very much a hack and not the right place to
// put this check, as it requires a reload of Cockatrice
// to be apply.
//
// However, this is also true of the `set->getEnabled()`
// check above (which is currently bugged as well), so
// we'll fix both at the same time.
if (includeRebalancedCards || printingInfo.getProperty("isRebalanced") != "true") {
_sets[setName].append(printingInfo);
}
_sets[setName].append(printingInfo);
}
// related cards
} else if (xmlName == "related" || xmlName == "reverse-related") {

View file

@ -9,7 +9,6 @@
#include <QString>
#include <qtmetamacros.h>
class ICardPreferenceProvider;
class ICardSetPriorityController;
class QXmlStreamReader;
@ -27,7 +26,6 @@ inline Q_LOGGING_CATEGORY(CockatriceXml4Log, "cockatrice_xml.xml_4_parser");
* @note Differences from v3:
* - 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).
* - More robust serialization; easier to extend schema in the future.
*/
@ -35,8 +33,7 @@ class CockatriceXml4Parser : public ICardDatabaseParser
{
Q_OBJECT
public:
explicit CockatriceXml4Parser(ICardPreferenceProvider *cardPreferenceProvider,
ICardSetPriorityController *cardSetPriorityController);
explicit CockatriceXml4Parser(ICardSetPriorityController *cardSetPriorityController);
~CockatriceXml4Parser() override = default;
/**
@ -71,8 +68,6 @@ public:
const QString &sourceVersion = "unknown") override;
private:
ICardPreferenceProvider *cardPreferenceProvider; ///< Interface to handle user preferences
/**
* @brief Loads a generic <prop> block from a <card> element.
* @param xml The open QXmlStreamReader positioned at a <prop> element.
@ -83,7 +78,6 @@ private:
/**
* @brief Load all <card> elements from the XML stream.
* @param xml The open QXmlStreamReader positioned at the <cards> element.
* Honors the user's preference regarding rebalanced cards.
*/
void loadCardsFromXml(QXmlStreamReader &xml);

View file

@ -8,7 +8,6 @@ class ICardPreferenceProvider
public:
virtual ~ICardPreferenceProvider() = default;
[[nodiscard]] virtual QString getCardPreferenceOverride(const QString &cardName) const = 0;
[[nodiscard]] virtual bool getIncludeRebalancedCards() const = 0;
};
#endif // COCKATRICE_INTERFACE_CARD_PREFERENCE_PROVIDER_H

View file

@ -2,6 +2,7 @@
#define COCKATRICE_INTERFACE_CARD_SET_PRIORITY_CONTROLLER_H
#include <QString>
#include <QStringList>
class ICardSetPriorityController
{
@ -46,6 +47,16 @@ public:
*/
virtual SetOptions getSetOptions(QString shortName) const = 0;
/**
* @brief Returns the short names of every set currently marked as enabled.
*
* Callers fold this into the card database cache source hash so that
* toggling set enablement invalidates the cache.
*
* @return The enabled set short names, sorted alphabetically.
*/
virtual QStringList getEnabledSetNames() const = 0;
virtual void saveSets(const QVector<SetSaveData> &data) = 0;
};

View file

@ -14,7 +14,6 @@ public:
[[nodiscard]] virtual bool getBumpSetsWithCardsInDeckToTop() const = 0;
[[nodiscard]] virtual int getPrintingSelectorSortOrder() const = 0;
[[nodiscard]] virtual int getPrintingSelectorCardSize() const = 0;
[[nodiscard]] virtual bool getIncludeRebalancedCards() const = 0;
[[nodiscard]] virtual bool getPrintingSelectorNavigationButtonsVisible() const = 0;
[[nodiscard]] virtual bool getTapAnimation() const = 0;
[[nodiscard]] virtual bool getArrowDrawAnimation() const = 0;

View file

@ -9,11 +9,6 @@ public:
{
return {};
}
[[nodiscard]] bool getIncludeRebalancedCards() const override
{
return true;
}
};
#endif // COCKATRICE_NOOP_CARD_PREFERENCE_PROVIDER_H

View file

@ -34,6 +34,11 @@ public:
return {0, true, true};
}
QStringList getEnabledSetNames() const override
{
return {};
}
void saveSets(const QVector<SetSaveData> & /* data */) override
{
}

View file

@ -88,6 +88,20 @@ ICardSetPriorityController::SetOptions CardDatabaseSettings::getSetOptions(QStri
return setOptionsCache.value(shortName);
}
QStringList CardDatabaseSettings::getEnabledSetNames() const
{
QMutexLocker lock(&setOptionsMutex);
ensureSetOptionsLoaded();
QStringList names;
for (auto it = setOptionsCache.constBegin(); it != setOptionsCache.constEnd(); ++it) {
if (it.value().enabled) {
names << it.key();
}
}
names.sort();
return names;
}
void CardDatabaseSettings::saveSets(const QVector<ICardSetPriorityController::SetSaveData> &data)
{
batchWrite([&](QSettings &s) {

View file

@ -35,6 +35,8 @@ public:
SetOptions getSetOptions(QString shortName) const override;
QStringList getEnabledSetNames() const override;
void saveSets(const QVector<SetSaveData> &data) override;
private:

View file

@ -43,11 +43,6 @@ int CardsDisplaySettings::getPrintingSelectorCardSize() const
return getValue("printingSelector", "cards", "cardSize", 100).toInt();
}
bool CardsDisplaySettings::getIncludeRebalancedCards() const
{
return getValue("includerebalancedcards", QString(), QString(), true).toBool();
}
bool CardsDisplaySettings::getPrintingSelectorNavigationButtonsVisible() const
{
return getValue("navigationButtonsVisible", "cards", "printingSelector", true).toBool();
@ -167,15 +162,6 @@ void CardsDisplaySettings::setPrintingSelectorCardSize(int _printingSelectorCard
emit printingSelectorCardSizeChanged();
}
void CardsDisplaySettings::setIncludeRebalancedCards(bool _includeRebalancedCards)
{
if (_includeRebalancedCards == getIncludeRebalancedCards()) {
return;
}
setValue(_includeRebalancedCards, "includerebalancedcards");
emit includeRebalancedCardsChanged(_includeRebalancedCards);
}
void CardsDisplaySettings::setPrintingSelectorNavigationButtonsVisible(bool _navigationButtonsVisible)
{
setValue(_navigationButtonsVisible, "navigationButtonsVisible", "cards", "printingSelector");

View file

@ -21,7 +21,6 @@ public:
[[nodiscard]] bool getBumpSetsWithCardsInDeckToTop() const override;
[[nodiscard]] int getPrintingSelectorSortOrder() const override;
[[nodiscard]] int getPrintingSelectorCardSize() const override;
[[nodiscard]] bool getIncludeRebalancedCards() const override;
[[nodiscard]] bool getPrintingSelectorNavigationButtonsVisible() const override;
[[nodiscard]] bool getTapAnimation() const override;
[[nodiscard]] bool getArrowDrawAnimation() const override;
@ -45,7 +44,6 @@ public:
void setBumpSetsWithCardsInDeckToTop(bool _bumpSetsWithCardsInDeckToTop);
void setPrintingSelectorSortOrder(int _printingSelectorSortOrder);
void setPrintingSelectorCardSize(int _printingSelectorCardSize);
void setIncludeRebalancedCards(bool _includeRebalancedCards);
void setPrintingSelectorNavigationButtonsVisible(bool _navigationButtonsVisible);
void setTapAnimation(bool _tapAnimation);
void setArrowDrawAnimation(bool _arrowDrawAnimation);
@ -70,7 +68,6 @@ signals:
void bumpSetsWithCardsInDeckToTopChanged();
void printingSelectorSortOrderChanged();
void printingSelectorCardSizeChanged();
void includeRebalancedCardsChanged(bool _includeRebalancedCards);
void printingSelectorNavigationButtonsVisibleChanged();
void visualDeckStorageCardSizeChanged();
void visualDatabaseDisplayCardSizeChanged();

View file

@ -259,7 +259,6 @@ static void migrateCardsDisplaySettings(const QString &settingsPath, QSettings &
{"cards/roundcardcorners", "cards/roundCardCorners"},
{"cards/overrideallcardartwithpersonalpreference", "cards/overrideAllCardArtWithPersonalPreference"},
{"cards/bumpsetswithcardsindecktotop", "cards/bumpSetsWithCardsInDeckToTop"},
{"cards/includerebalancedcards", "cards/includerebalancedcards"},
{"cards/tapanimation", "cards/tapAnimation"},
{"cards/autorotatesidewayslayoutcards", "cards/autoRotateSidewaysLayoutCards"},
{"cards/scaleCards", "cards/scaleCards"},
@ -571,9 +570,15 @@ static void migrateLegacySets(const QString &settingsPath)
QSettings cardDbIni(settingsPath + "cardDatabase.ini", QSettings::IniFormat);
for (const auto &shortName : groups) {
legacySetting.beginGroup(shortName);
cardDbIni.setValue("sets/" + shortName + "/sortKey", legacySetting.value("sortkey"));
cardDbIni.setValue("sets/" + shortName + "/enabled", legacySetting.value("enabled"));
cardDbIni.setValue("sets/" + shortName + "/isKnown", legacySetting.value("isknown"));
if (legacySetting.contains("sortkey")) {
cardDbIni.setValue("sets/" + shortName + "/sortKey", legacySetting.value("sortkey"));
}
if (legacySetting.contains("enabled")) {
cardDbIni.setValue("sets/" + shortName + "/enabled", legacySetting.value("enabled"));
}
if (legacySetting.contains("isknown")) {
cardDbIni.setValue("sets/" + shortName + "/isKnown", legacySetting.value("isknown"));
}
legacySetting.endGroup();
}
legacySetting.endGroup();

View file

@ -4,7 +4,6 @@
#include "libcockatrice/card/format/format_legality_rules.h"
#include "libcockatrice/card/lazy_properties_hash.h"
#include "libcockatrice/card/relation/card_relation_type.h"
#include "libcockatrice/interfaces/noop_card_preference_provider.h"
#include "libcockatrice/interfaces/noop_card_set_priority_controller.h"
#include "parsehelpers.h"
@ -302,9 +301,9 @@ CardInfoPtr OracleImporter::addCard(QString name,
static QString getJsonString(const QJsonObject &obj, const QString &key)
{
// QVariant coerces numbers and booleans to text, while QJsonValue::toString()
// returns a null string for them — some MTGJSON fields (manaValue,
// convertedManaCost, isOnlineOnly, isRebalanced) carry those types.
// QVariant coerces numbers to text, while QJsonValue::toString() returns a
// null string for them — MTGJSON fields like manaValue and convertedManaCost
// carry those types.
return obj.value(key).toVariant().toString();
}
@ -413,11 +412,8 @@ int OracleImporter::importCardsFromSet(const CardSetPtr &currentSet, const QJson
};
// mtgjson name => xml name
static const QMap<QString, QString> setInfoProperties{{"number", "num"},
{"rarity", "rarity"},
{"isOnlineOnly", "isOnlineOnly"},
{"isRebalanced", "isRebalanced"},
{"artist", "artist"}};
static const QMap<QString, QString> setInfoProperties{
{"number", "num"}, {"rarity", "rarity"}, {"artist", "artist"}};
// mtgjson name => xml name
static const QMap<QString, QString> identifierProperties{{"multiverseId", "muid"}, {"scryfallId", "uuid"}};
@ -852,7 +848,7 @@ int OracleImporter::startImport()
bool OracleImporter::saveToFile(const QString &fileName, const QString &sourceUrl, const QString &sourceVersion)
{
CockatriceXml4Parser parser(new NoopCardPreferenceProvider(), new NoopCardSetPriorityController());
CockatriceXml4Parser parser(new NoopCardSetPriorityController());
return parser.saveToFile(createDefaultMagicFormats(), sets, cards, fileName, sourceUrl, sourceVersion);
}

View file

@ -7,18 +7,26 @@
#include "gtest/gtest.h"
#include <QDate>
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QFileDevice>
#include <QFileInfo>
#include <QHash>
#include <QIODevice>
#include <QList>
#include <QSharedPointer>
#include <QString>
#include <QStringList>
#include <QTemporaryDir>
#include <QVector>
#include <libcockatrice/card/card_info.h>
#include <libcockatrice/card/database/card_database_data.h>
#include <libcockatrice/card/database/parser/cockatrice_xml_4.h>
#include <libcockatrice/card/lazy_properties_hash.h>
#include <libcockatrice/card/printing/printing_info.h>
#include <libcockatrice/interfaces/interface_card_database_path_provider.h>
#include <libcockatrice/interfaces/interface_card_set_priority_controller.h>
#include <libcockatrice/interfaces/noop_card_preference_provider.h>
#include <libcockatrice/interfaces/noop_card_set_priority_controller.h>
#include <string>
@ -77,12 +85,11 @@ TEST(CardDatabaseTest, Xml4LocalizedDataRoundTrip)
QTemporaryDir tempDir;
const QString fileName = tempDir.filePath("cards.xml");
NoopCardPreferenceProvider prefProvider;
CockatriceXml4Parser writer(&prefProvider, &controller);
CockatriceXml4Parser writer(&controller);
ASSERT_TRUE(writer.saveToFile({}, sets, cards, fileName));
CardDatabaseData data;
CockatriceXml4Parser parser(&prefProvider, &controller);
CockatriceXml4Parser parser(&controller);
QFile file(fileName);
ASSERT_TRUE(file.open(QIODevice::ReadOnly));
parser.parseFileInto(file, data);
@ -94,6 +101,280 @@ TEST(CardDatabaseTest, Xml4LocalizedDataRoundTrip)
ASSERT_EQ(loaded->getLocalizedText("de"), "Blitzschlag fügt 3 Schadenspunkte zu.");
ASSERT_EQ(loaded->getLocalizedText("fr"), "Deal 3 damage.");
}
/**
* @brief Path provider serving a sandboxed copy of the test card database.
*/
class SandboxPathProvider : public ICardDatabasePathProvider
{
public:
explicit SandboxPathProvider(const QString &_root) : root(_root)
{
}
QString getCardDatabasePath() const override
{
return root + "/cards.xml";
}
QString getCustomCardDatabasePath() const override
{
return root + "/customsets/";
}
QString getTokenDatabasePath() const override
{
return root + "/tokens.xml";
}
QString getSpoilerCardDatabasePath() const override
{
return root + "/spoiler.xml";
}
private:
QString root;
};
/**
* @brief Set priority controller whose enablement state the test drives directly.
*
* Seeded short names behave like entries present in cardDatabase.ini (enabled,
* known). Anything the parser discovers but the test did not seed falls back to
* the default SetOptions, i.e. disabled and unknown, mirroring
* CardDatabaseSettings for sets without an ini group.
*/
class SeededSetPriorityController : public ICardSetPriorityController
{
public:
void seed(const QStringList &shortNames)
{
for (const QString &shortName : shortNames) {
SetOptions options;
options.enabled = true;
options.isKnown = true;
setOptions.insert(shortName, options);
}
}
void setSortKey(QString shortName, unsigned int sortKey) override
{
setOptions[shortName].sortKey = sortKey;
}
void setEnabled(QString shortName, bool enabled) override
{
setOptions[shortName].enabled = enabled;
}
void setIsKnown(QString shortName, bool isknown) override
{
setOptions[shortName].isKnown = isknown;
}
unsigned int getSortKey(QString shortName) const override
{
return setOptions.value(shortName).sortKey;
}
bool isEnabled(QString shortName) const override
{
return setOptions.value(shortName).enabled;
}
bool isKnown(QString shortName) const override
{
return setOptions.value(shortName).isKnown;
}
SetOptions getSetOptions(QString shortName) const override
{
return setOptions.value(shortName);
}
QStringList getEnabledSetNames() const override
{
QStringList names;
for (auto it = setOptions.constBegin(); it != setOptions.constEnd(); ++it) {
if (it.value().enabled) {
names << it.key();
}
}
names.sort();
return names;
}
void saveSets(const QVector<SetSaveData> &data) override
{
for (const SetSaveData &entry : data) {
setOptions[entry.shortName].sortKey = entry.sortKey;
setOptions[entry.shortName].enabled = entry.enabled;
}
}
private:
QHash<QString, SetOptions> setOptions;
};
/**
* @brief Sandboxed card database plus enablement controller for cache tests.
*
* Copies the checked-in fixture XML into a temporary directory so each test
* owns the files (and the binary cache written next to them).
*/
class CardDatabaseCacheInvalidationTest : public ::testing::Test
{
protected:
void SetUp() override
{
ASSERT_TRUE(tempDir.isValid());
root = tempDir.path();
ASSERT_TRUE(QFile::copy(QString(CARDDB_DATADIR) + "cards.xml", root + "/cards.xml"));
ASSERT_TRUE(QFile::copy(QString(CARDDB_DATADIR) + "tokens.xml", root + "/tokens.xml"));
ASSERT_TRUE(QDir(root).mkpath("customsets"));
ASSERT_TRUE(
QFile::copy(QString(CARDDB_DATADIR) + "customsets/customset1.xml", root + "/customsets/customset1.xml"));
controller.seed({"CAT", "DOG", "WHO", " Not a Card", "BRD"});
}
/**
* @brief Contents of the binary cache written next to cards.xml.
* @return The cache file bytes, or an empty array if no cache exists.
*/
QByteArray cacheContents() const
{
QFile file(root + "/cards.xml.cache");
if (!file.open(QIODevice::ReadOnly)) {
return {};
}
return file.readAll();
}
QTemporaryDir tempDir;
QString root;
SeededSetPriorityController controller;
};
TEST_F(CardDatabaseCacheInvalidationTest, DisablingSetBustsCacheAndDropsPrintings)
{
NoopCardPreferenceProvider prefs;
SandboxPathProvider pathProvider(root);
CardDatabase db(nullptr, &prefs, &pathProvider, &controller);
db.loadCardDatabases();
const QByteArray initialCache = cacheContents();
ASSERT_FALSE(initialCache.isEmpty()) << "first load must write the binary cache";
ASSERT_FALSE(db.getCardList().value("Cat").isNull());
ASSERT_TRUE(db.getCardList().value("Cat")->getSets().contains("CAT"));
// A cache-aware load with unchanged inputs keeps the cache: the parse
// result would be identical, so there is nothing to invalidate.
db.loadCardDatabases();
EXPECT_EQ(initialCache, cacheContents()) << "load with unchanged inputs must keep the cache";
// Disabling a set changes the source hash, so the next load re-parses and
// the set's printings disappear everywhere.
controller.setEnabled("CAT", false);
db.reloadCardDatabasesAndNotify();
EXPECT_NE(initialCache, cacheContents()) << "toggling set enablement must bust the cache";
ASSERT_FALSE(db.getCardList().value("Cat").isNull());
EXPECT_FALSE(db.getCardList().value("Cat")->getSets().contains("CAT")) << "disabled set printings must be dropped";
ASSERT_FALSE(db.getCardList().value("Kitten").isNull());
EXPECT_FALSE(db.getCardList().value("Kitten")->getSets().contains("CAT"));
}
TEST_F(CardDatabaseCacheInvalidationTest, AddingAndEnablingCustomSetBustsCache)
{
NoopCardPreferenceProvider prefs;
SandboxPathProvider pathProvider(root);
CardDatabase db(nullptr, &prefs, &pathProvider, &controller);
db.loadCardDatabases();
const QByteArray initialCache = cacheContents();
ASSERT_FALSE(initialCache.isEmpty()) << "first load must write the binary cache";
// Drop a new custom set in: the input file list changes, so the next load
// must re-parse. BRD2 is unknown, so its set starts disabled.
QFile customSetFile(root + "/customsets/customset2.xml");
ASSERT_TRUE(customSetFile.open(QIODevice::WriteOnly | QIODevice::Truncate));
customSetFile.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
"<cockatrice_carddatabase version=\"4\">\n"
" <cards>\n"
" <card>\n"
" <name>Finch</name>\n"
" <set>BRD2</set>\n"
" <tablerow>0</tablerow>\n"
" <text>Chirp!</text>\n"
" </card>\n"
" </cards>\n"
"</cockatrice_carddatabase>\n");
customSetFile.close();
db.reloadCardDatabasesAndNotify();
const QByteArray afterCustomSetAdded = cacheContents();
EXPECT_NE(initialCache, afterCustomSetAdded) << "adding a custom card database must bust the cache";
ASSERT_FALSE(db.getCardList().value("Finch").isNull()) << "the custom card must be loaded";
EXPECT_TRUE(db.getCardList().value("Finch")->getSets().isEmpty())
<< "BRD2 starts disabled, so Finch has no printings yet";
// Enabling the freshly discovered set busts the cache again, so the next
// load finally parses its printings (#7455).
controller.setEnabled("BRD2", true);
db.reloadCardDatabasesAndNotify();
EXPECT_NE(afterCustomSetAdded, cacheContents()) << "enabling a set must bust the cache";
ASSERT_FALSE(db.getCardList().value("Finch").isNull());
EXPECT_TRUE(db.getCardList().value("Finch")->getSets().contains("BRD2"));
}
TEST_F(CardDatabaseCacheInvalidationTest, ExplicitReloadBypassesCache)
{
NoopCardPreferenceProvider prefs;
SandboxPathProvider pathProvider(root);
CardDatabase db(nullptr, &prefs, &pathProvider, &controller);
db.loadCardDatabases();
ASSERT_FALSE(db.getCardList().value("Cat").isNull());
ASSERT_TRUE(db.getCardList().value("Cat")->getSets().contains("CAT"));
// Move Cat to the DOG set in place, keeping the file size identical and
// restoring the modification time afterwards: the source hash (path, size,
// mtime) still matches, so the cache stays valid by construction and only
// an explicit reload can pick the edit up.
const QString cardsPath = root + "/cards.xml";
QFile file(cardsPath);
ASSERT_TRUE(file.open(QIODevice::ReadOnly));
const qint64 stampedSecs = QFileInfo(cardsPath).lastModified().toSecsSinceEpoch();
QByteArray contents = file.readAll();
file.close();
const QByteArray oldSetCode("<set>CAT</set>");
const QByteArray newSetCode("<set>DOG</set>");
ASSERT_EQ(contents.count(oldSetCode), 1) << "fixture must contain exactly one CAT printing";
contents.replace(contents.indexOf(oldSetCode), oldSetCode.size(), newSetCode);
ASSERT_TRUE(file.open(QIODevice::ReadWrite));
ASSERT_EQ(file.write(contents), contents.size());
// Flush the pending write before stamping the time: close() would otherwise
// flush it afterwards and bump the modification time again, invalidating the
// cache on platforms that buffer writes (e.g. macOS).
ASSERT_TRUE(file.flush());
ASSERT_TRUE(file.setFileTime(QDateTime::fromSecsSinceEpoch(stampedSecs), QFileDevice::FileModificationTime));
file.close();
ASSERT_EQ(QFileInfo(cardsPath).lastModified().toSecsSinceEpoch(), stampedSecs)
<< "the sandbox file's modification time must be restored for the cache to stay valid";
// The cache is still valid for a regular load, which serves the stale data.
db.loadCardDatabases();
ASSERT_FALSE(db.getCardList().value("Cat").isNull());
EXPECT_TRUE(db.getCardList().value("Cat")->getSets().contains("CAT")) << "regular load must hit the cache";
EXPECT_FALSE(db.getCardList().value("Cat")->getSets().contains("DOG"));
// The explicit reload ignores the cache and re-reads the file.
const QByteArray staleCache = cacheContents();
db.reloadCardDatabasesAndNotify();
EXPECT_NE(staleCache, cacheContents()) << "explicit reload must rewrite the cache from disk";
ASSERT_FALSE(db.getCardList().value("Cat").isNull());
EXPECT_FALSE(db.getCardList().value("Cat")->getSets().contains("CAT"));
EXPECT_TRUE(db.getCardList().value("Cat")->getSets().contains("DOG"));
}
} // namespace
int main(int argc, char **argv)

View file

@ -62,12 +62,10 @@ static QByteArray buildSyntheticData(int numSets, int cardsPerSet)
card["colors"] = QJsonArray{"W"};
card["colorIdentity"] = QJsonArray{"W"};
card["types"] = QJsonArray{"Creature"};
// Real MTGJSON types: floats and booleans, not strings. This
// Real MTGJSON types: floats, not strings. This
// exercises the QVariant coercion in the property reader.
card["convertedManaCost"] = 1.0;
card["manaValue"] = 1.0;
card["isOnlineOnly"] = false;
card["isRebalanced"] = false;
QJsonObject legalities;
legalities["standard"] = "legal";

View file

@ -236,7 +236,6 @@ TEST_F(SettingsMigrationTest, MigratesAllSettingsGroups)
g.setValue("cards/roundcardcorners", false);
g.setValue("cards/overrideallcardartwithpersonalpreference", true);
g.setValue("cards/bumpsetswithcardsindecktotop", false);
g.setValue("cards/includerebalancedcards", false);
g.setValue("cards/autorotatesidewayslayoutcards", false);
g.setValue("cards/tapanimation", true);
g.setValue("cards/scaleCards", false);
@ -395,7 +394,6 @@ TEST_F(SettingsMigrationTest, MigratesAllSettingsGroups)
ASSERT_EQ(readFromIni("cards_display.ini", "cards/roundCardCorners"), QVariant(false));
ASSERT_EQ(readFromIni("cards_display.ini", "cards/overrideAllCardArtWithPersonalPreference"), QVariant(true));
ASSERT_EQ(readFromIni("cards_display.ini", "cards/bumpSetsWithCardsInDeckToTop"), QVariant(false));
ASSERT_EQ(readFromIni("cards_display.ini", "cards/includerebalancedcards"), QVariant(false));
ASSERT_EQ(readFromIni("cards_display.ini", "cards/autoRotateSidewaysLayoutCards"), QVariant(false));
ASSERT_EQ(readFromIni("cards_display.ini", "cards/tapAnimation"), QVariant(true));
ASSERT_EQ(readFromIni("cards_display.ini", "cards/scaleCards"), QVariant(false));