Compare commits

..

No commits in common. "205e1ecebfae5062f35a599a7db3e6685bb05d47" and "d0fc27e3a8f555eef94203ada4b41962f35a1cc1" have entirely different histories.

26 changed files with 115 additions and 397 deletions

View file

@ -13,6 +13,11 @@ 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->popup(screenPos);
menu->exec(screenPos);
return true;
}
return false;

View file

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

View file

@ -178,6 +178,12 @@ 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);
@ -191,8 +197,9 @@ 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(hintsGroupBox, 4, 1, 1, 2);
mainLayout->addWidget(buttonBox, 5, 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->setColumnStretch(1, 1);
mainLayout->setColumnStretch(2, 1);
@ -263,9 +270,15 @@ 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,6 +52,7 @@ private:
int sortIndex;
Qt::SortOrder sortOrder;
bool setOrderIsSorted;
bool includeRebalancedCards;
enum
{
NO_SETS_SELECTED,
@ -86,6 +87,7 @@ private slots:
void actDisableResetButton(const QString &filterText);
void actSort(int index);
void actIgnoreWarning();
void includeRebalancedCardsChanged(bool _includeRebalancedCardsChanged);
};
#endif

View file

@ -29,6 +29,8 @@
<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, setPriorityController);
loader = new CardDatabaseLoader(this, this, pathProvider, prefs, 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,9 +78,7 @@ void CardDatabase::loadCardDatabases()
void CardDatabase::reloadCardDatabasesAndNotify()
{
// 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);
loadCardDatabases();
if (loadStatus == Ok) {
checkUnknownSets();

View file

@ -177,12 +177,8 @@ public slots:
void addFormat(const FormatRulesPtr &format);
/** @brief Loads card databases from configured paths, using the binary cache when valid. */
/** @brief Loads card databases from configured paths. */
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,7 +6,6 @@
#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"
@ -29,22 +28,25 @@
#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(_priorityController);
availableParsers << new CockatriceXml4Parser(_preferenceProvider, _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, [this] { loadCardDatabases(); });
connect(pathProvider, &ICardDatabasePathProvider::cardDatabasePathChanged, this,
&CardDatabaseLoader::loadCardDatabases);
}
CardDatabaseLoader::~CardDatabaseLoader()
@ -89,12 +91,12 @@ LoadStatus CardDatabaseLoader::loadCardDatabase(const QString &path, CardDatabas
return tempLoadStatus;
}
LoadStatus CardDatabaseLoader::loadCardDatabases(bool ignoreCache)
LoadStatus CardDatabaseLoader::loadCardDatabases()
{
return doLoadCardDatabases(ignoreCache);
return doLoadCardDatabases();
}
LoadStatus CardDatabaseLoader::doLoadCardDatabases(bool ignoreCache)
LoadStatus CardDatabaseLoader::doLoadCardDatabases()
{
QMutexLocker locker(reloadDatabaseMutex);
@ -112,14 +114,9 @@ LoadStatus CardDatabaseLoader::doLoadCardDatabases(bool ignoreCache)
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 (ignoreCache) {
qCInfo(CardDatabaseLoadingLog) << "Ignoring binary cache, re-parsing source files";
}
if (!ignoreCache && loadFromCache(data, sourceHash)) {
if (loadFromCache(data, sourceHash)) {
qCInfo(CardDatabaseLoadingLog) << "Loaded card database from binary cache";
loadStatus = Ok;
} else {
@ -184,12 +181,6 @@ 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;
@ -204,17 +195,6 @@ 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,6 +11,7 @@
#include <qtmetamacros.h>
class ICardDatabasePathProvider;
class ICardPreferenceProvider;
class ICardSetPriorityController;
// IWYU pragma: keep
@ -58,10 +59,12 @@ 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. */
@ -73,13 +76,9 @@ 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(bool ignoreCache = false);
LoadStatus loadCardDatabases();
/**
* @brief Loads a single card database file into the given snapshot.
@ -132,10 +131,9 @@ 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(bool ignoreCache);
LoadStatus doLoadCardDatabases();
/**
* @brief Collects custom card database paths recursively.

View file

@ -37,8 +37,9 @@ class ICardSetPriorityController;
#define COCKATRICE_XML4_SCHEMALOCATION \
"https://raw.githubusercontent.com/Cockatrice/Cockatrice/master/doc/carddatabase_v4/cards.xsd"
CockatriceXml4Parser::CockatriceXml4Parser(ICardSetPriorityController *_cardSetPriorityController)
: ICardDatabaseParser(_cardSetPriorityController)
CockatriceXml4Parser::CockatriceXml4Parser(ICardPreferenceProvider *_cardPreferenceProvider,
ICardSetPriorityController *_cardSetPriorityController)
: ICardDatabaseParser(_cardSetPriorityController), cardPreferenceProvider(_cardPreferenceProvider)
{
}
@ -283,6 +284,7 @@ QHash<QString, QString> CockatriceXml4Parser::loadCardPropertiesFromXml(QXmlStre
void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml)
{
bool includeRebalancedCards = cardPreferenceProvider->getIncludeRebalancedCards();
while (!xml.atEnd()) {
if (xml.readNext() == QXmlStreamReader::EndElement) {
break;
@ -384,7 +386,17 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml)
printingProps.insert(attrName, attr.value().toString());
}
PrintingInfo printingInfo(set, LazyPropertiesHash(printingProps));
_sets[setName].append(printingInfo);
// 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);
}
}
// related cards
} else if (xmlName == "related" || xmlName == "reverse-related") {

View file

@ -9,6 +9,7 @@
#include <QString>
#include <qtmetamacros.h>
class ICardPreferenceProvider;
class ICardSetPriorityController;
class QXmlStreamReader;
@ -26,6 +27,7 @@ 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.
*/
@ -33,7 +35,8 @@ class CockatriceXml4Parser : public ICardDatabaseParser
{
Q_OBJECT
public:
explicit CockatriceXml4Parser(ICardSetPriorityController *cardSetPriorityController);
explicit CockatriceXml4Parser(ICardPreferenceProvider *cardPreferenceProvider,
ICardSetPriorityController *cardSetPriorityController);
~CockatriceXml4Parser() override = default;
/**
@ -68,6 +71,8 @@ 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.
@ -78,6 +83,7 @@ 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,6 +8,7 @@ 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,7 +2,6 @@
#define COCKATRICE_INTERFACE_CARD_SET_PRIORITY_CONTROLLER_H
#include <QString>
#include <QStringList>
class ICardSetPriorityController
{
@ -47,16 +46,6 @@ 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,6 +14,7 @@ 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,6 +9,11 @@ public:
{
return {};
}
[[nodiscard]] bool getIncludeRebalancedCards() const override
{
return true;
}
};
#endif // COCKATRICE_NOOP_CARD_PREFERENCE_PROVIDER_H

View file

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

View file

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

View file

@ -43,6 +43,11 @@ 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();
@ -162,6 +167,15 @@ 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,6 +21,7 @@ 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;
@ -44,6 +45,7 @@ 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);
@ -68,6 +70,7 @@ signals:
void bumpSetsWithCardsInDeckToTopChanged();
void printingSelectorSortOrderChanged();
void printingSelectorCardSizeChanged();
void includeRebalancedCardsChanged(bool _includeRebalancedCards);
void printingSelectorNavigationButtonsVisibleChanged();
void visualDeckStorageCardSizeChanged();
void visualDatabaseDisplayCardSizeChanged();

View file

@ -259,6 +259,7 @@ 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"},
@ -570,15 +571,9 @@ static void migrateLegacySets(const QString &settingsPath)
QSettings cardDbIni(settingsPath + "cardDatabase.ini", QSettings::IniFormat);
for (const auto &shortName : groups) {
legacySetting.beginGroup(shortName);
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"));
}
cardDbIni.setValue("sets/" + shortName + "/sortKey", legacySetting.value("sortkey"));
cardDbIni.setValue("sets/" + shortName + "/enabled", legacySetting.value("enabled"));
cardDbIni.setValue("sets/" + shortName + "/isKnown", legacySetting.value("isknown"));
legacySetting.endGroup();
}
legacySetting.endGroup();

View file

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

View file

@ -7,26 +7,18 @@
#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>
@ -85,11 +77,12 @@ TEST(CardDatabaseTest, Xml4LocalizedDataRoundTrip)
QTemporaryDir tempDir;
const QString fileName = tempDir.filePath("cards.xml");
CockatriceXml4Parser writer(&controller);
NoopCardPreferenceProvider prefProvider;
CockatriceXml4Parser writer(&prefProvider, &controller);
ASSERT_TRUE(writer.saveToFile({}, sets, cards, fileName));
CardDatabaseData data;
CockatriceXml4Parser parser(&controller);
CockatriceXml4Parser parser(&prefProvider, &controller);
QFile file(fileName);
ASSERT_TRUE(file.open(QIODevice::ReadOnly));
parser.parseFileInto(file, data);
@ -101,280 +94,6 @@ 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,10 +62,12 @@ static QByteArray buildSyntheticData(int numSets, int cardsPerSet)
card["colors"] = QJsonArray{"W"};
card["colorIdentity"] = QJsonArray{"W"};
card["types"] = QJsonArray{"Creature"};
// Real MTGJSON types: floats, not strings. This
// Real MTGJSON types: floats and booleans, 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,6 +236,7 @@ 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);
@ -394,6 +395,7 @@ 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));