Initial commit of mtgonline project

This commit is contained in:
2026-07-18 04:57:40 +00:00
commit 86c12376f8
1870 changed files with 547994 additions and 0 deletions
@@ -0,0 +1,222 @@
#include "card_database.h"
#include "../relation/card_relation.h"
#include "card_database_manager.h"
#include "parser/cockatrice_xml_4.h"
#include <QCryptographicHash>
#include <QDebug>
#include <QDirIterator>
#include <QFile>
#include <QRegularExpression>
#include <algorithm>
#include <utility>
CardDatabase::CardDatabase(QObject *parent,
ICardPreferenceProvider *prefs,
ICardDatabasePathProvider *pathProvider,
ICardSetPriorityController *_setPriorityController)
: QObject(parent), setPriorityController(_setPriorityController), loadStatus(NotLoaded)
{
qRegisterMetaType<CardInfoPtr>("CardInfoPtr");
qRegisterMetaType<CardInfoPtr>("CardSetPtr");
// create loader and wire it up
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);
connect(loader, &CardDatabaseLoader::newSetsFound, this, &CardDatabase::cardDatabaseNewSetsFound);
connect(loader, &CardDatabaseLoader::allNewSetsEnabled, this, &CardDatabase::cardDatabaseAllNewSetsEnabled);
querier = new CardDatabaseQuerier(this, this, prefs);
}
CardDatabase::~CardDatabase()
{
clear();
}
void CardDatabase::clear()
{
QMutexLocker locker(clearDatabaseMutex);
for (const auto &card : cards.values()) {
if (card) {
removeCard(card);
}
}
cards.clear();
simpleNameCards.clear();
sets.clear();
ICardDatabaseParser::clearSetlist();
loadStatus = NotLoaded;
}
void CardDatabase::loadCardDatabases()
{
loadStatus = loader->loadCardDatabases();
}
void CardDatabase::reloadCardDatabasesAndNotify()
{
loadCardDatabases();
if (loadStatus == Ok) {
notifyEnabledSetsChanged();
}
}
bool CardDatabase::saveCustomTokensToFile()
{
return loader->saveCustomTokensToFile();
}
void CardDatabase::refreshCachedReverseRelatedCards()
{
for (const auto &card : cards) {
card->resetReverseRelatedCards2Me();
}
for (const auto &card : cards) {
for (auto *rel : card->getReverseRelatedCards()) {
if (auto target = cards.value(rel->getName())) {
auto *newRel = new CardRelation(card->getName(), rel->getAttachType(), rel->getIsCreateAllExclusion(),
rel->getIsVariable(), rel->getDefaultCount(), rel->getIsPersistent(),
rel->getIsFaceDown());
target->addReverseRelatedCards2Me(newRel);
}
}
}
}
void CardDatabase::addCard(const CardInfoPtr &card)
{
if (card == nullptr) {
qCWarning(CardDatabaseLog) << "CardDatabase::addCard(nullptr)";
return;
}
auto name = card->getName();
// If a card already exists, just add the new set property.
if (auto existing = cards.value(name)) {
for (const auto &printings : card->getSets()) {
for (const auto &printing : printings) {
existing->addToSet(printing.getSet(), printing);
}
}
return;
}
QMutexLocker locker(addCardMutex);
cards.insert(name, card);
simpleNameCards.insert(card->getSimpleName(), card);
emit cardAdded(card);
}
void CardDatabase::removeCard(const CardInfoPtr &card)
{
if (card.isNull()) {
qCWarning(CardDatabaseLog) << "CardDatabase::removeCard(nullptr)";
return;
}
for (auto *cardRelation : card->getRelatedCards()) {
cardRelation->deleteLater();
}
for (auto *cardRelation : card->getReverseRelatedCards()) {
cardRelation->deleteLater();
}
for (auto *cardRelation : card->getReverseRelatedCards2Me()) {
cardRelation->deleteLater();
}
QMutexLocker locker(removeCardMutex);
cards.remove(card->getName());
simpleNameCards.remove(card->getSimpleName());
emit cardRemoved(card);
}
void CardDatabase::addSet(const CardSetPtr &set)
{
sets.insert(set->getShortName(), set);
}
CardSetPtr CardDatabase::getSet(const QString &setName)
{
if (sets.contains(setName)) {
return sets.value(setName);
} else {
CardSetPtr newSet = CardSet::newInstance(setPriorityController, setName);
sets.insert(setName, newSet);
return newSet;
}
}
CardSetList CardDatabase::getSetList() const
{
CardSetList result;
for (auto set : sets.values()) {
result << set;
}
return result;
}
void CardDatabase::checkUnknownSets()
{
auto _sets = getSetList();
if (_sets.getEnabledSetsNum()) {
// if some sets are first found on this run, ask the user
int numUnknownSets = _sets.getUnknownSetsNum();
QStringList unknownSetNames = _sets.getUnknownSetsNames();
if (numUnknownSets > 0) {
emit cardDatabaseNewSetsFound(numUnknownSets, unknownSetNames);
} else {
_sets.markAllAsKnown();
}
} else {
// No set enabled. Probably this is the first time running trice
_sets.guessSortKeys();
_sets.sortByKey();
_sets.enableAll();
notifyEnabledSetsChanged();
emit cardDatabaseAllNewSetsEnabled();
}
}
void CardDatabase::enableAllUnknownSets()
{
auto _sets = getSetList();
_sets.enableAllUnknown();
}
void CardDatabase::markAllSetsAsKnown()
{
auto _sets = getSetList();
_sets.markAllAsKnown();
}
void CardDatabase::notifyEnabledSetsChanged()
{
// refresh the list of cached set names
for (const CardInfoPtr &card : cards) {
card->refreshCachedSets();
}
// inform the carddatabasemodels that they need to re-check their list of cards
emit cardDatabaseEnabledSetsChanged();
}
void CardDatabase::addFormat(const FormatRulesPtr &format)
{
formats.insert(format->formatName.toLower(), format);
}
@@ -0,0 +1,192 @@
#ifndef CARDDATABASE_H
#define CARDDATABASE_H
#include "../set/card_set_list.h"
#include "card_database_loader.h"
#include "card_database_querier.h"
#include <QBasicMutex>
#include <QDate>
#include <QHash>
#include <QList>
#include <QLoggingCategory>
#include <QVector>
#include <libcockatrice/interfaces/interface_card_database_path_provider.h>
#include <utility>
inline Q_LOGGING_CATEGORY(CardDatabaseLog, "card_database");
/**
* @class CardDatabase
* @ingroup CardDatabase
* @brief Core in-memory container for card and set data.
*
* Responsible for maintaining CardInfo objects, CardSet objects, and
* providing access to CardDatabaseQuerier for query operations.
* Handles addition, removal, and clearing of cards and sets.
*/
class CardDatabase : public QObject
{
Q_OBJECT
protected:
/** @brief Controller to determine set priority when choosing preferred printings. */
ICardSetPriorityController *setPriorityController;
/** @brief Cards indexed by exact name. */
CardNameMap cards;
/** @brief Cards indexed by simplified name (normalized). */
CardNameMap simpleNameCards;
/** @brief Sets indexed by short name. */
SetNameMap sets;
FormatRulesNameMap formats;
/** @brief Loader responsible for file discovery and parsing. */
CardDatabaseLoader *loader;
/** @brief Current load status of the database. */
LoadStatus loadStatus;
/** @brief Querier for higher-level card lookups. */
CardDatabaseQuerier *querier;
private:
/**
* @brief Check for sets that are unknown and emit signals if needed.
*/
void checkUnknownSets();
/**
* @brief Refreshes the cached reverse-related cards for all cards.
*/
void refreshCachedReverseRelatedCards();
/** @brief Mutexes for thread safety. */
QBasicMutex *clearDatabaseMutex = new QBasicMutex(), *addCardMutex = new QBasicMutex(),
*removeCardMutex = new QBasicMutex();
public:
/**
* @brief Constructs a new CardDatabase instance.
* @param parent QObject parent.
* @param prefs Optional card preference provider.
* @param pathProvider Optional database path provider.
* @param setPriorityController Optional controller for set priority.
*/
explicit CardDatabase(QObject *parent = nullptr,
ICardPreferenceProvider *prefs = nullptr,
ICardDatabasePathProvider *pathProvider = nullptr,
ICardSetPriorityController *setPriorityController = nullptr);
/** @brief Destructor clears all internal data. */
~CardDatabase() override;
/**
* @brief Removes a card from the database.
* @param card Pointer to the card to remove.
*/
void removeCard(const CardInfoPtr &card);
/** @brief Clears all cards, sets, and internal state. */
void clear();
/** @brief Returns the map of cards by name. */
[[nodiscard]] const CardNameMap &getCardList() const
{
return cards;
}
/**
* @brief Retrieves a set by short name, creating a new one if missing.
* @param setName Short name of the set.
* @return Pointer to the CardSet.
*/
CardSetPtr getSet(const QString &setName);
/** @brief Returns a list of all sets in the database. */
[[nodiscard]] CardSetList getSetList() const;
/** @brief Returns the current load status. */
[[nodiscard]] LoadStatus getLoadStatus() const
{
return loadStatus;
}
/** @brief Returns the querier for performing card lookups. */
[[nodiscard]] CardDatabaseQuerier *query() const
{
return querier;
}
/** @brief Enables all unknown sets in the database. */
void enableAllUnknownSets();
/** @brief Marks all sets as known. */
void markAllSetsAsKnown();
/** @brief Notifies listeners that enabled sets changed. */
void notifyEnabledSetsChanged();
ICardSetPriorityController *getPriorityController()
{
return setPriorityController;
}
public slots:
/**
* @brief Adds a card to the database.
* @param card CardInfoPtr to add.
*/
void addCard(const CardInfoPtr &card);
/**
* @brief Adds a set to the database.
* @param set Pointer to CardSet to add.
*/
void addSet(const CardSetPtr &set);
void addFormat(const FormatRulesPtr &format);
/** @brief Loads card databases from configured paths. */
void loadCardDatabases();
void reloadCardDatabasesAndNotify();
/** @brief Saves custom tokens to file.
* @return True if successful.
*/
bool saveCustomTokensToFile();
signals:
/** @brief Emitted when the card database has finished loading successfully. */
void cardDatabaseLoadingFinished();
/** @brief Emitted when the card database fails to load. */
void cardDatabaseLoadingFailed();
/**
* @brief Emitted when new sets are found.
* @param numUnknownSets Number of unknown sets.
* @param unknownSetsNames Names of unknown sets.
*/
void cardDatabaseNewSetsFound(int numUnknownSets, QStringList unknownSetsNames);
/** @brief Emitted when all new sets have been enabled. */
void cardDatabaseAllNewSetsEnabled();
/** @brief Emitted when enabled sets have changed. */
void cardDatabaseEnabledSetsChanged();
/** @brief Emitted when a new card is added. */
void cardAdded(CardInfoPtr card);
/** @brief Emitted when a card is removed. */
void cardRemoved(CardInfoPtr card);
friend class CardDatabaseLoader;
friend class CardDatabaseQuerier;
};
#endif // CARDDATABASE_H
@@ -0,0 +1,157 @@
#include "card_database_loader.h"
#include "card_database.h"
#include "parser/cockatrice_xml_3.h"
#include "parser/cockatrice_xml_4.h"
#include <QDebug>
#include <QDirIterator>
#include <QFile>
#include <QTime>
CardDatabaseLoader::CardDatabaseLoader(QObject *parent,
CardDatabase *db,
ICardDatabasePathProvider *_pathProvider,
ICardPreferenceProvider *_preferenceProvider,
ICardSetPriorityController *_priorityController)
: QObject(parent), database(db), pathProvider(_pathProvider)
{
// instantiate available parsers here and connect them to the database
availableParsers << new CockatriceXml4Parser(_preferenceProvider, _priorityController);
availableParsers << new CockatriceXml3Parser(_priorityController);
for (auto *p : availableParsers) {
// connect parser outputs to the database adders
connect(p, &ICardDatabaseParser::addCard, database, &CardDatabase::addCard, Qt::DirectConnection);
connect(p, &ICardDatabaseParser::addSet, database, &CardDatabase::addSet, Qt::DirectConnection);
connect(p, &ICardDatabaseParser::addFormat, database, &CardDatabase::addFormat, Qt::DirectConnection);
}
// when SettingsCache's path changes, trigger reloads
connect(pathProvider, &ICardDatabasePathProvider::cardDatabasePathChanged, this,
&CardDatabaseLoader::loadCardDatabases);
}
CardDatabaseLoader::~CardDatabaseLoader()
{
qDeleteAll(availableParsers);
availableParsers.clear();
}
LoadStatus CardDatabaseLoader::loadFromFile(const QString &fileName)
{
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly)) {
return FileError;
}
for (auto parser : availableParsers) {
file.reset();
if (parser->getCanParseFile(fileName, file)) {
file.reset();
parser->parseFile(file);
return Ok;
}
}
return Invalid;
}
LoadStatus CardDatabaseLoader::loadCardDatabase(const QString &path)
{
auto startTime = QTime::currentTime();
LoadStatus tempLoadStatus = NotLoaded;
if (!path.isEmpty()) {
QMutexLocker locker(loadFromFileMutex);
tempLoadStatus = loadFromFile(path);
}
int msecs = startTime.msecsTo(QTime::currentTime());
qCInfo(CardDatabaseLoadingLog) << "Loaded card database: Path =" << path << "Status =" << tempLoadStatus
<< "Cards =" << (database ? database->cards.size() : 0)
<< "Sets =" << (database ? database->sets.size() : 0) << QString("%1ms").arg(msecs);
return tempLoadStatus;
}
LoadStatus CardDatabaseLoader::loadCardDatabases()
{
QMutexLocker locker(reloadDatabaseMutex);
if (!database) {
qCWarning(CardDatabaseLoadingLog) << "Loader has no database pointer";
emit loadingFailed();
return FileError;
}
emit loadingStarted();
qCInfo(CardDatabaseLoadingLog) << "Card Database Loading Started";
database->clear(); // remove old db
LoadStatus loadStatus = loadCardDatabase(pathProvider->getCardDatabasePath()); // load main card database
loadCardDatabase(pathProvider->getTokenDatabasePath()); // load tokens database
loadCardDatabase(pathProvider->getSpoilerCardDatabasePath()); // load spoilers database
// find all custom card databases, recursively & following symlinks
// then load them alphabetically
const QStringList customPaths = collectCustomDatabasePaths();
for (int i = 0; i < customPaths.size(); ++i) {
const auto &p = customPaths.at(i);
qCInfo(CardDatabaseLoadingLog) << "Loading Custom Set" << i << "(" << p << ")";
loadCardDatabase(p);
}
// AFTER all the cards have been loaded
// resolve the reverse-related tags
database->refreshCachedReverseRelatedCards();
if (loadStatus == Ok) {
database->checkUnknownSets(); // update deck editors, etc
qCInfo(CardDatabaseLoadingSuccessOrFailureLog) << "Card Database Loading Success";
emit loadingFinished();
} else {
qCInfo(CardDatabaseLoadingSuccessOrFailureLog) << "Card Database Loading Failed";
emit loadingFailed(); // bring up the settings dialog
}
return loadStatus;
}
QStringList CardDatabaseLoader::collectCustomDatabasePaths() const
{
QDirIterator it(pathProvider->getCustomCardDatabasePath(), {"*.xml"}, QDir::Files,
QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
QStringList paths;
while (it.hasNext()) {
paths << it.next();
}
paths.sort();
return paths;
}
bool CardDatabaseLoader::saveCustomTokensToFile()
{
if (!database) {
qCWarning(CardDatabaseLog) << "saveCustomTokensToFile: database pointer missing";
return false;
}
QString fileName = pathProvider->getCustomCardDatabasePath() + "/" + CardSet::TOKENS_SETNAME + ".xml";
SetNameMap tmpSets;
CardSetPtr customTokensSet = database->getSet(CardSet::TOKENS_SETNAME);
tmpSets.insert(CardSet::TOKENS_SETNAME, customTokensSet);
CardNameMap tmpCards;
for (const CardInfoPtr &card : database->cards) {
if (card->getSets().contains(CardSet::TOKENS_SETNAME)) {
tmpCards.insert(card->getName(), card);
}
}
availableParsers.first()->saveToFile(FormatRulesNameMap(), tmpSets, tmpCards, fileName);
return true;
}
@@ -0,0 +1,125 @@
#ifndef COCKATRICE_CARD_DATABASE_LOADER_H
#define COCKATRICE_CARD_DATABASE_LOADER_H
#include <QBasicMutex>
#include <QList>
#include <QLoggingCategory>
#include <libcockatrice/interfaces/interface_card_database_path_provider.h>
#include <libcockatrice/interfaces/interface_card_preference_provider.h>
#include <libcockatrice/interfaces/interface_card_set_priority_controller.h>
inline Q_LOGGING_CATEGORY(CardDatabaseLoadingLog, "card_database.loading");
inline Q_LOGGING_CATEGORY(CardDatabaseLoadingSuccessOrFailureLog, "card_database.loading.success_or_failure");
class CardDatabase;
class ICardDatabaseParser;
/**
* @enum LoadStatus
* @brief Represents the result of attempting to load a card database.
*/
enum LoadStatus
{
Ok, /**< Database loaded successfully. */
VersionTooOld, /**< Database version is too old to load. */
Invalid, /**< Database is invalid or unparsable. */
NotLoaded, /**< Database has not been loaded. */
FileError, /**< Error opening or reading the file. */
NoCards /**< Database contains no cards. */
};
/**
* @class CardDatabaseLoader
* @ingroup CardDatabase
* @brief Handles loading card databases from disk and saving custom tokens.
*
* This class is responsible for:
* - Discovering configured card database paths.
* - Loading main, token, spoiler, and custom databases.
* - Populating a CardDatabase instance using connected parsers.
* - Emitting signals about loading progress and new sets.
*/
class CardDatabaseLoader : public QObject
{
Q_OBJECT
public:
/**
* @brief Constructs a CardDatabaseLoader.
* @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. */
~CardDatabaseLoader() override;
public slots:
/**
* @brief Loads all configured card databases.
* @return Status of the main database load.
*/
LoadStatus loadCardDatabases();
/**
* @brief Loads a single card database file.
* @param path Path to the database file.
* @return LoadStatus indicating success or failure.
*/
LoadStatus loadCardDatabase(const QString &path);
/**
* @brief Saves custom tokens to the user-defined custom database path.
* @return True if the save was successful.
*/
bool saveCustomTokensToFile();
signals:
/** @brief Emitted when loading starts. */
void loadingStarted();
/** @brief Emitted when loading finishes successfully. */
void loadingFinished();
/** @brief Emitted when loading fails. */
void loadingFailed();
/**
* @brief Emitted when new sets are discovered during loading.
* @param numSets Number of new sets.
* @param setNames Names of the discovered sets.
*/
void newSetsFound(int numSets, const QStringList &setNames);
/** @brief Emitted when all newly discovered sets have been enabled. */
void allNewSetsEnabled();
private:
/**
* @brief Loads a database from a single file using the available parsers.
* @param fileName Path to the database file.
* @return LoadStatus indicating success or failure.
*/
LoadStatus loadFromFile(const QString &fileName);
/**
* @brief Collects custom card database paths recursively.
* @return Sorted list of file paths to custom databases.
*/
[[nodiscard]] QStringList collectCustomDatabasePaths() const;
private:
CardDatabase *database; /**< Non-owning pointer to the target CardDatabase. */
ICardDatabasePathProvider *pathProvider; /**< Pointer to the path provider. */
QList<ICardDatabaseParser *> availableParsers; /**< List of available parsers for different formats. */
QBasicMutex *loadFromFileMutex = new QBasicMutex(); /**< Mutex for single-file loading. */
QBasicMutex *reloadDatabaseMutex = new QBasicMutex(); /**< Mutex for reloading entire database. */
};
#endif // COCKATRICE_CARD_DATABASE_LOADER_H
@@ -0,0 +1,35 @@
#include "card_database_manager.h"
#include <libcockatrice/interfaces/noop_card_database_path_provider.h>
#include <libcockatrice/interfaces/noop_card_preference_provider.h>
#include <libcockatrice/interfaces/noop_card_set_priority_controller.h>
ICardPreferenceProvider *CardDatabaseManager::cardPreferenceProvider = new NoopCardPreferenceProvider();
ICardDatabasePathProvider *CardDatabaseManager::pathProvider = new NoopCardDatabasePathProvider();
ICardSetPriorityController *CardDatabaseManager::setPriorityController = new NoopCardSetPriorityController();
void CardDatabaseManager::setCardPreferenceProvider(ICardPreferenceProvider *provider)
{
cardPreferenceProvider = provider;
}
void CardDatabaseManager::setCardDatabasePathProvider(ICardDatabasePathProvider *provider)
{
pathProvider = provider;
}
void CardDatabaseManager::setCardSetPriorityController(ICardSetPriorityController *controller)
{
setPriorityController = controller;
}
CardDatabase *CardDatabaseManager::getInstance()
{
static CardDatabase instance(nullptr, cardPreferenceProvider, pathProvider, setPriorityController);
return &instance;
}
CardDatabaseQuerier *CardDatabaseManager::query()
{
return getInstance()->query();
}
@@ -0,0 +1,80 @@
#ifndef CARD_DATABASE_ACCESSOR_H
#define CARD_DATABASE_ACCESSOR_H
#pragma once
#include "card_database.h"
/**
* @class CardDatabaseManager
* @ingroup CardDatabase
* @brief The CardDatabaseManager is responsible for managing the global CardDatabase singleton.
*
* This class provides a static interface for accessing the global CardDatabase instance
* and its CardDatabaseQuerier. It also allows the configuration of optional providers:
* - ICardPreferenceProvider
* - ICardDatabasePathProvider
* - ICardSetPriorityController
*
* Only a single instance of CardDatabase exists, enforced via a private constructor and
* deleted copy/move operations.
*/
class CardDatabaseManager
{
public:
/** @brief Deleted copy constructor to enforce singleton. */
CardDatabaseManager(const CardDatabaseManager &) = delete;
/** @brief Deleted assignment operator to enforce singleton. */
CardDatabaseManager &operator=(const CardDatabaseManager &) = delete;
/**
* @brief Sets the card preference provider.
* @param provider Pointer to an ICardPreferenceProvider.
* @note Must be called before the first call to getInstance().
*/
static void setCardPreferenceProvider(ICardPreferenceProvider *provider);
/**
* @brief Sets the card database path provider.
* @param provider Pointer to an ICardDatabasePathProvider.
* @note Must be called before the first call to getInstance().
*/
static void setCardDatabasePathProvider(ICardDatabasePathProvider *provider);
/**
* @brief Sets the card set priority controller.
* @param controller Pointer to an ICardSetPriorityController.
* @note Must be called before the first call to getInstance().
*/
static void setCardSetPriorityController(ICardSetPriorityController *controller);
/**
* @brief Returns the singleton CardDatabase instance.
* @return Pointer to the global CardDatabase.
*/
static CardDatabase *getInstance();
/**
* @brief Returns the CardDatabaseQuerier of the singleton database.
* @return Pointer to CardDatabaseQuerier.
*/
static CardDatabaseQuerier *query();
private:
/** @brief Private default constructor to enforce singleton. */
CardDatabaseManager() = default;
/** @brief Private destructor. */
~CardDatabaseManager() = default;
/** @brief Static card preference provider pointer (default: Noop). */
static ICardPreferenceProvider *cardPreferenceProvider;
/** @brief Static path provider pointer (default: Noop). */
static ICardDatabasePathProvider *pathProvider;
/** @brief Static set priority controller pointer (default: Noop). */
static ICardSetPriorityController *setPriorityController;
};
#endif // CARD_DATABASE_ACCESSOR_H
@@ -0,0 +1,368 @@
#include "card_database_querier.h"
#include "../card_info.h"
#include "../printing/exact_card.h"
#include "../set/card_set_comparator.h"
#include "card_database.h"
#include <qrandom.h>
CardDatabaseQuerier::CardDatabaseQuerier(QObject *_parent,
const CardDatabase *_db,
const ICardPreferenceProvider *prefs)
: QObject(_parent), db(_db), prefs(prefs)
{
}
/**
* Looks up the cardInfo corresponding to the cardName.
*
* @param cardName The card name to look up
* @return A CardInfoPtr, or null if not corresponding CardInfo is found.
*/
CardInfoPtr CardDatabaseQuerier::getCardInfo(const QString &cardName) const
{
return db->cards.value(cardName);
}
/**
* Looks up the cardInfos for a list of card names.
*
* @param cardNames The card names to look up
* @return A List of CardInfoPtr. Any failed lookups will be ignored and dropped from the resulting list
*/
QList<CardInfoPtr> CardDatabaseQuerier::getCardInfos(const QStringList &cardNames) const
{
QList<CardInfoPtr> cardInfos;
for (const QString &cardName : cardNames) {
CardInfoPtr ptr = db->cards.value(cardName);
if (ptr) {
cardInfos.append(ptr);
}
}
return cardInfos;
}
CardInfoPtr CardDatabaseQuerier::getCardBySimpleName(const QString &cardName) const
{
return db->simpleNameCards.value(CardInfo::simplifyName(cardName));
}
CardInfoPtr CardDatabaseQuerier::lookupCardByName(const QString &name) const
{
if (auto info = getCardInfo(name)) {
return info;
}
if (auto info = getCardBySimpleName(name)) {
return info;
}
return getCardBySimpleName(CardInfo::simplifyName(name));
}
/**
* Looks up the cards corresponding to the CardRefs.
* If the providerId is empty, will default to the preferred printing.
* If providerId is given but not found, the PrintingInfo will be empty.
*
* @param cardRefs The cards to look up. If providerId is empty for an entry, will default to the preferred printing for
* that entry. If providerId is given but not found, the PrintingInfo will be empty for that entry.
* @return A list of cards. Any failed lookups will be ignored and dropped from the resulting list.
*/
QList<ExactCard> CardDatabaseQuerier::getCards(const QList<CardRef> &cardRefs) const
{
QList<ExactCard> cards;
for (const auto &cardRef : cardRefs) {
ExactCard card = getCard(cardRef);
if (card) {
cards.append(card);
}
}
return cards;
}
/**
* Looks up the card corresponding to the CardRef.
* If the providerId is empty, will default to the preferred printing.
* If providerId is given but not found, the PrintingInfo will be empty.
*
* @param cardRef The card to look up.
* @return A specific printing of a card, or empty if not found.
*/
ExactCard CardDatabaseQuerier::getCard(const CardRef &cardRef) const
{
auto info = getCardInfo(cardRef.name);
if (info.isNull()) {
return {};
}
if (cardRef.providerId.isEmpty() || cardRef.providerId.isNull()) {
return ExactCard(info, getPreferredPrinting(info));
}
return ExactCard(info, findPrintingWithId(info, cardRef.providerId));
}
/**
* Looks up the card by CardRef, simplifying the name if required.
* If the providerId is empty, will default to the preferred printing.
* If providerId is given but not found, the PrintingInfo will be empty.
*
* @param cardRef The card to look up.
* @return A specific printing of a card, or empty if not found.
*/
ExactCard CardDatabaseQuerier::guessCard(const CardRef &cardRef) const
{
auto card = lookupCardByName(cardRef.name);
auto printing =
cardRef.providerId.isEmpty() ? getPreferredPrinting(card) : findPrintingWithId(card, cardRef.providerId);
return ExactCard(card, printing);
}
ExactCard CardDatabaseQuerier::getRandomCard() const
{
if (db->cards.isEmpty()) {
return {};
}
const auto keys = db->cards.keys();
int randomIndex = QRandomGenerator::global()->bounded(keys.size());
const QString &randomKey = keys.at(randomIndex);
CardInfoPtr randomCard = getCardInfo(randomKey);
return ExactCard{randomCard, getPreferredPrinting(randomCard)};
}
ExactCard CardDatabaseQuerier::getCardFromSameSet(const QString &cardName, const PrintingInfo &otherPrinting) const
{
// The source card does not have a printing defined, which means we can't get a card from the same set.
if (otherPrinting.isEmpty()) {
return getCard({cardName});
}
// The source card does have a printing defined, which means we can attempt to get a card from the same set.
PrintingInfo relatedPrinting = getSpecificPrinting(cardName, otherPrinting.getSet()->getCorrectedShortName(), "");
ExactCard relatedCard(guessCard({cardName}).getCardPtr(), relatedPrinting);
// If we didn't find a card from the same set, just try to find any card with the same name.
return relatedCard ? relatedCard : getCard({cardName});
}
/**
* Finds the PrintingInfo in the cardInfo that has the given uuid field.
*
* @param cardInfo The CardInfo to search
* @param providerId The uuid to look for
* @return The PrintingInfo, or a default-constructed PrintingInfo if not found.
*/
PrintingInfo CardDatabaseQuerier::findPrintingWithId(const CardInfoPtr &cardInfo, const QString &providerId) const
{
for (const auto &printings : cardInfo->getSets()) {
for (const auto &printing : printings) {
if (printing.getUuid() == providerId) {
return printing;
}
}
}
return PrintingInfo();
}
PrintingInfo CardDatabaseQuerier::getSpecificPrinting(const CardRef &cardRef) const
{
CardInfoPtr cardInfo = getCardInfo(cardRef.name);
if (!cardInfo) {
return PrintingInfo(nullptr);
}
return findPrintingWithId(cardInfo, cardRef.providerId);
}
PrintingInfo CardDatabaseQuerier::getSpecificPrinting(const QString &cardName,
const QString &setShortName,
const QString &collectorNumber) const
{
CardInfoPtr cardInfo = getCardInfo(cardName);
if (!cardInfo) {
return PrintingInfo(nullptr);
}
SetToPrintingsMap setMap = cardInfo->getSets();
if (setMap.empty()) {
return PrintingInfo(nullptr);
}
for (const auto &printings : setMap) {
for (auto &cardInfoForSet : printings) {
if (!collectorNumber.isEmpty()) {
if (cardInfoForSet.getSet()->getShortName() == setShortName &&
cardInfoForSet.getProperty("num") == collectorNumber) {
return cardInfoForSet;
}
} else {
if (cardInfoForSet.getSet()->getShortName() == setShortName) {
return cardInfoForSet;
}
}
}
}
return PrintingInfo(nullptr);
}
/**
* Gets the card representing the preferred printing of the cardInfo
*
* @param cardName The cardName to find the preferred card and printing for
* @return A specific printing of a card
*/
ExactCard CardDatabaseQuerier::getPreferredCard(const QString &cardName) const
{
return getPreferredCard(getCardInfo(cardName));
}
/**
* Gets the card representing the preferred printing of the cardInfo
*
* @param cardInfo The cardInfo to find the preferred printing for
* @return A specific printing of a card
*/
ExactCard CardDatabaseQuerier::getPreferredCard(const CardInfoPtr &cardInfo) const
{
return ExactCard(cardInfo, getPreferredPrinting(cardInfo));
}
bool CardDatabaseQuerier::isPreferredPrinting(const CardRef &cardRef) const
{
if (cardRef.providerId.startsWith("card_")) {
return cardRef.providerId ==
QLatin1String("card_") + cardRef.name + QString("_") + getPreferredPrintingProviderId(cardRef.name);
}
return cardRef.providerId == getPreferredPrintingProviderId(cardRef.name);
}
PrintingInfo CardDatabaseQuerier::getPreferredPrinting(const QString &cardName) const
{
CardInfoPtr cardInfo = getCardInfo(cardName);
return getPreferredPrinting(cardInfo);
}
PrintingInfo CardDatabaseQuerier::getPreferredPrinting(const CardInfoPtr &cardInfo) const
{
if (!cardInfo) {
return PrintingInfo(nullptr);
}
const auto &pinnedPrintingProviderId = prefs->getCardPreferenceOverride(cardInfo->getName());
if (!pinnedPrintingProviderId.isEmpty()) {
return getSpecificPrinting({cardInfo->getName(), pinnedPrintingProviderId});
}
SetToPrintingsMap setMap = cardInfo->getSets();
if (setMap.empty()) {
return PrintingInfo(nullptr);
}
CardSetPtr preferredSet = nullptr;
PrintingInfo preferredPrinting;
SetPriorityComparator comparator;
for (const auto &printings : setMap) {
for (auto &printing : printings) {
CardSetPtr currentSet = printing.getSet();
if (!preferredSet || comparator(currentSet, preferredSet)) {
preferredSet = currentSet;
preferredPrinting = printing;
}
}
}
if (preferredSet) {
return preferredPrinting;
}
return PrintingInfo(nullptr);
}
QString CardDatabaseQuerier::getPreferredPrintingProviderId(const QString &cardName) const
{
PrintingInfo preferredPrinting = getPreferredPrinting(cardName);
QString uuid = preferredPrinting.getUuid();
if (!uuid.isEmpty()) {
return uuid;
}
CardInfoPtr defaultCardInfo = getCardInfo(cardName);
if (defaultCardInfo.isNull()) {
return cardName;
}
return defaultCardInfo->getName();
}
QStringList CardDatabaseQuerier::getAllMainCardTypes() const
{
QSet<QString> types;
for (const auto &card : db->cards.values()) {
types.insert(card->getMainCardType());
}
return types.values();
}
QMap<QString, int> CardDatabaseQuerier::getAllMainCardTypesWithCount() const
{
QMap<QString, int> typeCounts;
for (const auto &card : db->cards.values()) {
QString type = card->getMainCardType();
typeCounts[type]++;
}
return typeCounts;
}
QMap<QString, int> CardDatabaseQuerier::getAllSubCardTypesWithCount() const
{
QMap<QString, int> typeCounts;
for (const auto &card : db->cards.values()) {
QString type = card->getCardType();
QStringList parts = type.split("");
if (parts.size() > 1) { // Ensure there are subtypes
QStringList subtypes = parts[1].split(" ", Qt::SkipEmptyParts);
for (const QString &subtype : subtypes) {
typeCounts[subtype]++;
}
}
}
return typeCounts;
}
FormatRulesPtr CardDatabaseQuerier::getFormat(const QString &formatName) const
{
return db->formats.value(formatName.toLower());
}
QMap<QString, int> CardDatabaseQuerier::getAllFormatsWithCount() const
{
QMap<QString, int> formatCounts;
for (const auto &card : db->cards.values()) {
QStringList allProps = card->getProperties();
for (const QString &prop : allProps) {
if (prop.startsWith("format-")) {
QString formatName = prop.mid(QStringLiteral("format-").size());
formatCounts[formatName]++;
}
}
}
return formatCounts;
}
@@ -0,0 +1,225 @@
#ifndef COCKATRICE_CARD_DATABASE_QUERIER_H
#define COCKATRICE_CARD_DATABASE_QUERIER_H
#include "../card_info.h"
#include "../printing/exact_card.h"
#include <QObject>
#include <libcockatrice/interfaces/interface_card_preference_provider.h>
#include <libcockatrice/utility/card_ref.h>
class CardDatabase;
/**
* @class CardDatabaseQuerier
* @ingroup CardDatabase
* @brief Provides lookup and convenience functions for querying cards and their printings.
*
* The CardDatabaseQuerier class offers various lookup helpers for retrieving card information
* (e.g., CardInfoPtr, ExactCard, and PrintingInfo) from a CardDatabase. It also applies user
* printing preferences via ICardPreferenceProvider when determining preferred printings.
*/
class CardDatabaseQuerier : public QObject
{
Q_OBJECT
public:
/**
* @brief Constructs a CardDatabaseQuerier.
*
* @param parent Parent QObject.
* @param db Pointer to the CardDatabase used for lookups.
* @param prefs Pointer to card preference provider which supplies user-preference for printings.
*/
explicit CardDatabaseQuerier(QObject *parent, const CardDatabase *db, const ICardPreferenceProvider *prefs);
/**
* @brief Retrieves a card by its exact name.
*
* @param cardName Exact card name.
* @return A CardInfoPtr, or null if no matching card exists.
*/
[[nodiscard]] CardInfoPtr getCardInfo(const QString &cardName) const;
/**
* @brief Retrieves multiple cards by their exact names.
*
* Failed lookups are skipped and not included in the result.
*
* @param cardNames List of exact card names.
* @return List of CardInfoPtr objects for which a match was found.
*/
[[nodiscard]] QList<CardInfoPtr> getCardInfos(const QStringList &cardNames) const;
/**
* @brief Retrieves a card using simplified name matching.
*
* The name is automatically normalized, so callers do not need to simplify it.
*
* @param cardName A (possibly simplified or misspelled) card name.
* @return A CardInfoPtr, or null if not found.
*/
[[nodiscard]] CardInfoPtr getCardBySimpleName(const QString &cardName) const;
/**
* @brief Looks up a card using exact name first, then simplified matching as fallback.
*
* @param name Raw card name input.
* @return The best-match CardInfoPtr, or null if no match is found.
*/
[[nodiscard]] CardInfoPtr lookupCardByName(const QString &name) const;
/**
* @brief Converts a CardRef into an ExactCard.
*
* If the providerId is empty, the preferred printing is used.
* If providerId exists but cannot be found, an ExactCard with an empty PrintingInfo is returned.
*
* @param cardRef Card reference with name and optional providerId.
* @return The resolved ExactCard, or empty if no card was found.
*/
[[nodiscard]] ExactCard getCard(const CardRef &cardRef) const;
/**
* @brief Resolves multiple CardRefs into ExactCards.
*
* Failed entries are not included in the result.
*
* @param cardRefs List of card references.
* @return List of successfully resolved ExactCards.
*/
[[nodiscard]] QList<ExactCard> getCards(const QList<CardRef> &cardRefs) const;
/**
* @brief Attempts a more flexible card lookup using both simple name matching and CardRef rules.
*
* If providerId is missing, uses preferred printing. If lookup fails, attempts simplified name.
*
* @param cardRef Card reference to resolve.
* @return The best-guess ExactCard, or empty if unresolved.
*/
[[nodiscard]] ExactCard guessCard(const CardRef &cardRef) const;
/**
* @brief Returns a random card from the database using the preferred printing.
*
* @return A random ExactCard, or empty if the database is empty.
*/
[[nodiscard]] ExactCard getRandomCard() const;
/**
* @brief Returns a printing of a card from the same set as another given printing when possible.
*
* If no matching printing exists, falls back to a standard lookup.
*
* @param cardName Card to retrieve.
* @param otherPrinting Printing to match the set against.
* @return Matching ExactCard if found, otherwise fallback ExactCard.
*/
[[nodiscard]] ExactCard getCardFromSameSet(const QString &cardName, const PrintingInfo &otherPrinting) const;
/**
* @brief Returns the preferred printing of a card based on user preferences and set priority.
*
* @param cardName Name of the card.
* @return The preferred ExactCard.
*/
[[nodiscard]] ExactCard getPreferredCard(const QString &cardName) const;
/**
* @brief Returns the preferred printing of a card based on user preferences and set priority.
*
* @param cardInfo Card information object.
* @return The preferred ExactCard.
*/
[[nodiscard]] ExactCard getPreferredCard(const CardInfoPtr &cardInfo) const;
/**
* @brief Checks whether the CardRef refers to the preferred printing.
*
* @param cardRef Card reference to test.
* @return True if providerId matches the preferred printing.
*/
[[nodiscard]] bool isPreferredPrinting(const CardRef &cardRef) const;
/**
* @brief Returns the preferred printing for the given card name.
*
* @param cardName Card name.
* @return Preferred PrintingInfo, or empty if not found.
*/
[[nodiscard]] PrintingInfo getPreferredPrinting(const QString &cardName) const;
/**
* @brief Returns the preferred printing for the given card.
*
* @param cardInfo Card information object.
* @return Preferred PrintingInfo, or empty if not applicable.
*/
[[nodiscard]] PrintingInfo getPreferredPrinting(const CardInfoPtr &cardInfo) const;
/**
* @brief Returns the providerId of the preferred printing.
*
* @param cardName Card name.
* @return ProviderId string for preferred printing.
*/
[[nodiscard]] QString getPreferredPrintingProviderId(const QString &cardName) const;
/**
* @brief Retrieves a specific printing referenced by CardRef.
*
* @param cardRef Card reference including providerId.
* @return Matching PrintingInfo, or empty if not found.
*/
[[nodiscard]] PrintingInfo getSpecificPrinting(const CardRef &cardRef) const;
/**
* @brief Searches for a specific printing by set code and collector number.
*
* @param cardName Card name to search.
* @param setCode Set (short) code to match.
* @param collectorNumber Collector number. If empty, any printing from the set is returned.
* @return Matching PrintingInfo, or empty if not found.
*/
[[nodiscard]] PrintingInfo
getSpecificPrinting(const QString &cardName, const QString &setCode, const QString &collectorNumber) const;
/**
* @brief Searches for a printing that matches a given providerId.
*
* @param card Card to search.
* @param providerId Provider identifier to match.
* @return Matching PrintingInfo, or empty if not found.
*/
[[nodiscard]] PrintingInfo findPrintingWithId(const CardInfoPtr &card, const QString &providerId) const;
/**
* @brief Returns a list of all main card types present in the database.
*
* @return List of main card type strings.
*/
[[nodiscard]] QStringList getAllMainCardTypes() const;
/**
* @brief Returns a mapping of main card types to their occurrence counts.
*
* @return Map of main card type to count.
*/
[[nodiscard]] QMap<QString, int> getAllMainCardTypesWithCount() const;
/**
* @brief Returns a mapping of card subtypes to their occurrence counts.
*
* @return Map of subtype string to count.
*/
[[nodiscard]] QMap<QString, int> getAllSubCardTypesWithCount() const;
FormatRulesPtr getFormat(const QString &formatName) const;
QMap<QString, int> getAllFormatsWithCount() const;
private:
const CardDatabase *db; //!< Card database used for all lookups.
const ICardPreferenceProvider *prefs; //!< Preference provider for preferred printings.
};
#endif // COCKATRICE_CARD_DATABASE_QUERIER_H
@@ -0,0 +1,35 @@
#include "card_database_parser.h"
#include <libcockatrice/interfaces/noop_card_set_priority_controller.h>
SetNameMap ICardDatabaseParser::sets;
ICardDatabaseParser::ICardDatabaseParser(ICardSetPriorityController *_cardSetPriorityController)
: cardSetPriorityController(_cardSetPriorityController)
{
}
void ICardDatabaseParser::clearSetlist()
{
sets.clear();
}
CardSetPtr ICardDatabaseParser::internalAddSet(const QString &setName,
const QString &longName,
const QString &setType,
const QDate &releaseDate,
const CardSet::Priority priority)
{
if (sets.contains(setName)) {
return sets.value(setName);
}
CardSetPtr newSet = CardSet::newInstance(cardSetPriorityController, setName);
newSet->setLongName(longName);
newSet->setSetType(setType);
newSet->setReleaseDate(releaseDate);
newSet->setPriority(priority);
sets.insert(setName, newSet);
emit addSet(newSet);
return newSet;
}
@@ -0,0 +1,92 @@
#ifndef CARDDATABASE_PARSER_H
#define CARDDATABASE_PARSER_H
#include "../../card_info.h"
#include <QIODevice>
#include <QString>
#define COCKATRICE_XML_XSI_NAMESPACE "http://www.w3.org/2001/XMLSchema-instance"
/**
* @class ICardDatabaseParser
* @ingroup CardDatabase
* @brief Defines the base parser interface (ICardDatabaseParser) for all card database parsers.
*
* Provides methods for checking file compatibility, parsing, and saving card databases.
* Also provides shared access to the global set list for cross-referencing.
*/
class ICardDatabaseParser : public QObject
{
Q_OBJECT
public:
ICardDatabaseParser(ICardSetPriorityController *cardSetPriorityController);
~ICardDatabaseParser() override = default;
/**
* @brief Checks whether this parser can parse the given file.
* @param name File name (used for extension checks).
* @param device QIODevice representing the file content.
* @return true if the parser can handle this file.
*/
virtual bool getCanParseFile(const QString &name, QIODevice &device) = 0;
/**
* @brief Parses a database file and emits addCard/addSet signals.
* @param device QIODevice representing the file content.
*/
virtual void parseFile(QIODevice &device) = 0;
/**
* @brief Saves card and set data to a file.
* @param _formats
* @param sets Map of sets to save.
* @param cards Map of cards to save.
* @param fileName Target file path.
* @param sourceUrl Optional source URL of the database.
* @param sourceVersion Optional version string of the source.
* @return true if save succeeded.
*/
virtual bool saveToFile(FormatRulesNameMap _formats,
SetNameMap sets,
CardNameMap cards,
const QString &fileName,
const QString &sourceUrl = "unknown",
const QString &sourceVersion = "unknown") = 0;
/** @brief Clears the cached global set list. */
static void clearSetlist();
protected:
/** @brief Cached global list of sets shared between all parsers. */
static SetNameMap sets;
ICardSetPriorityController *cardSetPriorityController;
/**
* @brief Internal helper to add a set to the global set cache.
* @param setName Short set name.
* @param longName Optional full name.
* @param setType Optional set type string.
* @param releaseDate Optional release date.
* @param priority Optional priority (fallback if not specified).
* @return Pointer to the added or existing CardSet instance.
*/
CardSetPtr internalAddSet(const QString &setName,
const QString &longName = "",
const QString &setType = "",
const QDate &releaseDate = QDate(),
const CardSet::Priority priority = CardSet::PriorityFallback);
signals:
/** Emitted when a card is loaded from the database. */
void addCard(CardInfoPtr card);
/** Emitted when a set is loaded from the database. */
void addSet(CardSetPtr set);
void addFormat(FormatRulesPtr format);
};
Q_DECLARE_INTERFACE(ICardDatabaseParser, "ICardDatabaseParser")
#endif
@@ -0,0 +1,501 @@
#include "cockatrice_xml_3.h"
#include "../../relation/card_relation.h"
#include "../../relation/card_relation_type.h"
#include <QCoreApplication>
#include <QDebug>
#include <QFile>
#include <QXmlStreamReader>
#include <version_string.h>
#define COCKATRICE_XML3_TAGNAME "cockatrice_carddatabase"
#define COCKATRICE_XML3_TAGVER 3
#define COCKATRICE_XML3_SCHEMALOCATION \
"https://raw.githubusercontent.com/Cockatrice/Cockatrice/master/doc/carddatabase_v3/cards.xsd"
CockatriceXml3Parser::CockatriceXml3Parser(ICardSetPriorityController *_cardSetPriorityController)
: ICardDatabaseParser(_cardSetPriorityController)
{
}
bool CockatriceXml3Parser::getCanParseFile(const QString &fileName, QIODevice &device)
{
qCInfo(CockatriceXml3Log) << "Trying to parse: " << fileName;
if (!fileName.endsWith(".xml", Qt::CaseInsensitive)) {
qCInfo(CockatriceXml3Log) << "Parsing failed: wrong extension";
return false;
}
QXmlStreamReader xml(&device);
while (!xml.atEnd()) {
if (xml.readNext() == QXmlStreamReader::StartElement) {
if (xml.name().toString() == COCKATRICE_XML3_TAGNAME) {
int version = xml.attributes().value("version").toString().toInt();
if (version == COCKATRICE_XML3_TAGVER) {
return true;
} else {
qCInfo(CockatriceXml3Log) << "Parsing failed: wrong version" << version;
return false;
}
} else {
qCInfo(CockatriceXml3Log) << "Parsing failed: wrong element tag" << xml.name();
return false;
}
}
}
return true;
}
void CockatriceXml3Parser::parseFile(QIODevice &device)
{
QXmlStreamReader xml(&device);
while (!xml.atEnd()) {
if (xml.readNext() == QXmlStreamReader::StartElement) {
while (!xml.atEnd()) {
if (xml.readNext() == QXmlStreamReader::EndElement) {
break;
}
auto name = xml.name().toString();
if (name == "sets") {
loadSetsFromXml(xml);
} else if (name == "cards") {
loadCardsFromXml(xml);
} else if (!name.isEmpty()) {
qCInfo(CockatriceXml3Log) << "Unknown item" << name << ", trying to continue anyway";
xml.skipCurrentElement();
}
}
}
}
if (xml.hasError()) {
QString preamble = tr("Parse error at line %1 col %2:").arg(xml.lineNumber()).arg(xml.columnNumber());
qCWarning(CockatriceXml3Log).noquote() << preamble << xml.errorString();
}
}
void CockatriceXml3Parser::loadSetsFromXml(QXmlStreamReader &xml)
{
while (!xml.atEnd()) {
if (xml.readNext() == QXmlStreamReader::EndElement) {
break;
}
auto name = xml.name().toString();
if (name == "set") {
QString shortName, longName, setType;
QDate releaseDate;
while (!xml.atEnd()) {
if (xml.readNext() == QXmlStreamReader::EndElement) {
break;
}
name = xml.name().toString();
if (name == "name") {
shortName = xml.readElementText(QXmlStreamReader::IncludeChildElements);
} else if (name == "longname") {
longName = xml.readElementText(QXmlStreamReader::IncludeChildElements);
} else if (name == "settype") {
setType = xml.readElementText(QXmlStreamReader::IncludeChildElements);
} else if (name == "releasedate") {
releaseDate =
QDate::fromString(xml.readElementText(QXmlStreamReader::IncludeChildElements), Qt::ISODate);
} else if (!name.isEmpty()) {
qCInfo(CockatriceXml3Log) << "Unknown set property" << name << ", trying to continue anyway";
xml.skipCurrentElement();
}
}
internalAddSet(shortName, longName, setType, releaseDate);
}
}
}
QString CockatriceXml3Parser::getMainCardType(QString &type)
{
QString result = type;
/*
Legendary Artifact Creature - Golem
Instant // Instant
*/
int pos;
if ((pos = result.indexOf('-')) != -1) {
result.remove(pos, result.length());
}
if ((pos = result.indexOf("")) != -1) {
result.remove(pos, result.length());
}
if ((pos = result.indexOf("//")) != -1) {
result.remove(pos, result.length());
}
result = result.simplified();
/*
Legendary Artifact Creature
Instant
*/
if ((pos = result.lastIndexOf(' ')) != -1) {
result = result.mid(pos + 1);
}
/*
Creature
Instant
*/
return result;
}
void CockatriceXml3Parser::loadCardsFromXml(QXmlStreamReader &xml)
{
while (!xml.atEnd()) {
if (xml.readNext() == QXmlStreamReader::EndElement) {
break;
}
auto xmlName = xml.name().toString();
if (xmlName == "card") {
QString name = QString("");
QString text = QString("");
QVariantHash properties = QVariantHash();
QString colors = QString("");
QList<CardRelation *> relatedCards, reverseRelatedCards;
auto _sets = SetToPrintingsMap();
int tableRow = 0;
bool cipt = false;
bool landscapeOrientation = false;
bool isToken = false;
bool upsideDown = false;
while (!xml.atEnd()) {
if (xml.readNext() == QXmlStreamReader::EndElement) {
break;
}
xmlName = xml.name().toString();
// variable - assigned properties
if (xmlName == "name") {
name = xml.readElementText(QXmlStreamReader::IncludeChildElements);
} else if (xmlName == "text") {
text = xml.readElementText(QXmlStreamReader::IncludeChildElements);
} else if (xmlName == "color" || xmlName == "colors") {
colors.append(xml.readElementText(QXmlStreamReader::IncludeChildElements));
} else if (xmlName == "token") {
isToken = static_cast<bool>(xml.readElementText(QXmlStreamReader::IncludeChildElements).toInt());
// generic properties
} else if (xmlName == "manacost") {
properties.insert("manacost", xml.readElementText(QXmlStreamReader::IncludeChildElements));
} else if (xmlName == "cmc") {
properties.insert("cmc", xml.readElementText(QXmlStreamReader::IncludeChildElements));
} else if (xmlName == "type") {
QString type = xml.readElementText(QXmlStreamReader::IncludeChildElements);
properties.insert("type", type);
properties.insert("maintype", getMainCardType(type));
} else if (xmlName == "pt") {
properties.insert("pt", xml.readElementText(QXmlStreamReader::IncludeChildElements));
} else if (xmlName == "loyalty") {
properties.insert("loyalty", xml.readElementText(QXmlStreamReader::IncludeChildElements));
// positioning info
} else if (xmlName == "tablerow") {
tableRow = xml.readElementText(QXmlStreamReader::IncludeChildElements).toInt();
} else if (xmlName == "cipt") {
cipt = (xml.readElementText(QXmlStreamReader::IncludeChildElements) == "1");
} else if (xmlName == "landscapeOrientation") {
landscapeOrientation = (xml.readElementText(QXmlStreamReader::IncludeChildElements) == "1");
} else if (xmlName == "upsidedown") {
upsideDown = (xml.readElementText(QXmlStreamReader::IncludeChildElements) == "1");
// sets
} else if (xmlName == "set") {
// NOTE: attributes must be read before readElementText()
QXmlStreamAttributes attrs = xml.attributes();
QString setName = xml.readElementText(QXmlStreamReader::IncludeChildElements);
auto set = internalAddSet(setName);
// Only load printings from sets the user has enabled, matching the v4 loader's
// behaviour. Without this check, disabling a set has no effect on v3 databases.
if (set->getEnabled()) {
PrintingInfo setInfo(set);
if (attrs.hasAttribute("muId")) {
setInfo.setProperty("muid", attrs.value("muId").toString());
}
if (attrs.hasAttribute("uuId")) {
setInfo.setProperty("uuid", attrs.value("uuId").toString());
}
if (attrs.hasAttribute("picURL")) {
setInfo.setProperty("picurl", attrs.value("picURL").toString());
}
if (attrs.hasAttribute("num")) {
setInfo.setProperty("num", attrs.value("num").toString());
}
if (attrs.hasAttribute("rarity")) {
setInfo.setProperty("rarity", attrs.value("rarity").toString());
}
_sets[setName].append(setInfo);
}
// related cards
} else if (xmlName == "related" || xmlName == "reverse-related") {
CardRelationType attach = CardRelationType::DoesNotAttach;
bool exclude = false;
bool variable = false;
int count = 1;
QXmlStreamAttributes attrs = xml.attributes();
QString cardName = xml.readElementText(QXmlStreamReader::IncludeChildElements);
if (attrs.hasAttribute("count")) {
if (attrs.value("count").toString().indexOf("x=") == 0) {
variable = true;
count = attrs.value("count").toString().remove(0, 2).toInt();
} else if (attrs.value("count").toString().indexOf("x") == 0) {
variable = true;
} else {
count = attrs.value("count").toString().toInt();
}
if (count < 1) {
count = 1;
}
}
if (attrs.hasAttribute("attach")) {
attach = CardRelationType::AttachTo;
}
if (attrs.hasAttribute("exclude")) {
exclude = true;
}
auto *relation = new CardRelation(cardName, attach, exclude, variable, count);
if (xmlName == "reverse-related") {
reverseRelatedCards << relation;
} else {
relatedCards << relation;
}
} else if (!xmlName.isEmpty()) {
qCInfo(CockatriceXml3Log) << "Unknown card property" << xmlName << ", trying to continue anyway";
xml.skipCurrentElement();
}
}
if (name.isEmpty()) {
qCWarning(CockatriceXml3Log) << "Encountered card with empty name; skipping";
continue;
}
properties.insert("colors", colors);
CardInfo::UiAttributes attributes = {.cipt = cipt,
.landscapeOrientation = landscapeOrientation,
.tableRow = tableRow,
.upsideDownArt = upsideDown};
CardInfoPtr newCard = CardInfo::newInstance(name, text, isToken, properties, relatedCards,
reverseRelatedCards, _sets, attributes);
emit addCard(newCard);
}
}
}
static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardSetPtr &set)
{
if (set.isNull()) {
qCWarning(CockatriceXml3Log) << "&operator<< set is nullptr";
return xml;
}
xml.writeStartElement("set");
xml.writeTextElement("name", set->getShortName());
xml.writeTextElement("longname", set->getLongName());
xml.writeTextElement("settype", set->getSetType());
xml.writeTextElement("releasedate", set->getReleaseDate().toString(Qt::ISODate));
xml.writeEndElement();
return xml;
}
static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfoPtr &info)
{
if (info.isNull()) {
qCWarning(CockatriceXml3Log) << "operator<< info is nullptr";
return xml;
}
QString tmpString;
xml.writeStartElement("card");
// variable - assigned properties
xml.writeTextElement("name", info->getName());
xml.writeTextElement("text", info->getText());
if (info->getIsToken()) {
xml.writeTextElement("token", "1");
}
// generic properties
xml.writeTextElement("manacost", info->getProperty("manacost"));
xml.writeTextElement("cmc", info->getProperty("cmc"));
xml.writeTextElement("type", info->getProperty("type"));
int colorSize = info->getColors().size();
for (int i = 0; i < colorSize; ++i) {
xml.writeTextElement("color", info->getColors().at(i));
}
tmpString = info->getProperty("pt");
if (!tmpString.isEmpty()) {
xml.writeTextElement("pt", tmpString);
}
tmpString = info->getProperty("loyalty");
if (!tmpString.isEmpty()) {
xml.writeTextElement("loyalty", tmpString);
}
// sets
const SetToPrintingsMap setMap = info->getSets();
for (const auto &printings : setMap) {
for (const PrintingInfo &set : printings) {
xml.writeStartElement("set");
xml.writeAttribute("rarity", set.getProperty("rarity"));
xml.writeAttribute("muId", set.getProperty("muid"));
xml.writeAttribute("uuId", set.getProperty("uuid"));
tmpString = set.getProperty("num");
if (!tmpString.isEmpty()) {
xml.writeAttribute("num", tmpString);
}
tmpString = set.getProperty("picurl");
if (!tmpString.isEmpty()) {
xml.writeAttribute("picURL", tmpString);
}
xml.writeCharacters(set.getSet()->getShortName());
xml.writeEndElement();
}
}
// related cards
const QList<CardRelation *> related = info->getRelatedCards();
for (auto i : related) {
xml.writeStartElement("related");
if (i->getDoesAttach()) {
xml.writeAttribute("attach", "attach");
}
if (i->getIsCreateAllExclusion()) {
xml.writeAttribute("exclude", "exclude");
}
if (i->getIsVariable()) {
if (1 == i->getDefaultCount()) {
xml.writeAttribute("count", "x");
} else {
xml.writeAttribute("count", "x=" + QString::number(i->getDefaultCount()));
}
} else if (1 != i->getDefaultCount()) {
xml.writeAttribute("count", QString::number(i->getDefaultCount()));
}
xml.writeCharacters(i->getName());
xml.writeEndElement();
}
const QList<CardRelation *> reverseRelated = info->getReverseRelatedCards();
for (auto i : reverseRelated) {
xml.writeStartElement("reverse-related");
if (i->getDoesAttach()) {
xml.writeAttribute("attach", "attach");
}
if (i->getIsCreateAllExclusion()) {
xml.writeAttribute("exclude", "exclude");
}
if (i->getIsVariable()) {
if (1 == i->getDefaultCount()) {
xml.writeAttribute("count", "x");
} else {
xml.writeAttribute("count", "x=" + QString::number(i->getDefaultCount()));
}
} else if (1 != i->getDefaultCount()) {
xml.writeAttribute("count", QString::number(i->getDefaultCount()));
}
xml.writeCharacters(i->getName());
xml.writeEndElement();
}
// positioning
const CardInfo::UiAttributes &attributes = info->getUiAttributes();
xml.writeTextElement("tablerow", QString::number(attributes.tableRow));
if (attributes.cipt) {
xml.writeTextElement("cipt", "1");
}
if (attributes.landscapeOrientation) {
xml.writeTextElement("landscapeOrientation", "1");
}
if (attributes.upsideDownArt) {
xml.writeTextElement("upsidedown", "1");
}
xml.writeEndElement(); // card
return xml;
}
bool CockatriceXml3Parser::saveToFile(FormatRulesNameMap _formats,
SetNameMap _sets,
CardNameMap cards,
const QString &fileName,
const QString &sourceUrl,
const QString &sourceVersion)
{
Q_UNUSED(_formats);
QFile file(fileName);
if (!file.open(QIODevice::WriteOnly)) {
return false;
}
QXmlStreamWriter xml(&file);
xml.setAutoFormatting(true);
xml.writeStartDocument();
xml.writeStartElement(COCKATRICE_XML3_TAGNAME);
xml.writeAttribute("version", QString::number(COCKATRICE_XML3_TAGVER));
xml.writeAttribute("xmlns:xsi", COCKATRICE_XML_XSI_NAMESPACE);
xml.writeAttribute("xsi:schemaLocation", COCKATRICE_XML3_SCHEMALOCATION);
xml.writeStartElement("info");
xml.writeTextElement("author", QCoreApplication::applicationName() + QString(" %1").arg(VERSION_STRING));
xml.writeTextElement("createdAt", QDateTime::currentDateTimeUtc().toString(Qt::ISODate));
xml.writeTextElement("sourceUrl", sourceUrl);
xml.writeTextElement("sourceVersion", sourceVersion);
xml.writeEndElement();
if (_sets.count() > 0) {
xml.writeStartElement("sets");
for (CardSetPtr set : _sets) {
xml << set;
}
xml.writeEndElement();
}
if (cards.count() > 0) {
xml.writeStartElement("cards");
for (CardInfoPtr card : cards) {
xml << card;
}
xml.writeEndElement();
}
xml.writeEndElement(); // cockatrice_carddatabase
xml.writeEndDocument();
return true;
}
@@ -0,0 +1,79 @@
#ifndef COCKATRICE_XML3_H
#define COCKATRICE_XML3_H
#include "card_database_parser.h"
#include <QLoggingCategory>
#include <QXmlStreamReader>
inline Q_LOGGING_CATEGORY(CockatriceXml3Log, "cockatrice_xml.xml_3_parser");
/**
* @class CockatriceXml3Parser
* @ingroup CardDatabase
* @brief Parses version 3 of the Cockatrice XML Schema.
*
* This parser reads a Cockatrice XML3 database and emits CardInfoPtr
* and CardSetPtr objects. All card properties are read individually.
*
* @note Differences from v4:
* - No <prop> block; properties are hardcoded (manacost, cmc, type, pt, loyalty, etc.).
* - No set priority field.
* - No support for rebalanced cards or preferences.
* - Related cards support only attach, exclude, variable, and count attributes.
*/
class CockatriceXml3Parser : public ICardDatabaseParser
{
Q_OBJECT
public:
CockatriceXml3Parser(ICardSetPriorityController *cardSetPriorityController);
~CockatriceXml3Parser() override = default;
/**
* @brief Determines if the parser can handle this file.
* @param name File name.
* @param device Open QIODevice containing the XML.
* @return True if the file is a Cockatrice XML3 database.
*/
bool getCanParseFile(const QString &name, QIODevice &device) override;
/**
* @brief Parse the XML database.
* @param device Open QIODevice positioned at start of file.
*/
void parseFile(QIODevice &device) override;
/**
* @brief Save sets and cards back to an XML3 file.
*/
bool saveToFile(FormatRulesNameMap _formats,
SetNameMap _sets,
CardNameMap cards,
const QString &fileName,
const QString &sourceUrl = "unknown",
const QString &sourceVersion = "unknown") override;
private:
/**
* @brief Load all <card> elements from the XML stream.
* @param xml The open QXmlStreamReader positioned at the <cards> element.
* Parses each <card> node and emits addCard signals for each CardInfoPtr created.
*/
void loadCardsFromXml(QXmlStreamReader &xml);
/**
* @brief Load all <set> elements from the XML stream.
* @param xml The open QXmlStreamReader positioned at the <sets> element.
* Parses each <set> node and adds them to the shared set cache.
*/
void loadSetsFromXml(QXmlStreamReader &xml);
/**
* @brief Extracts the main card type from a full type string.
* @param type The full type string (e.g., "Legendary Artifact Creature - Golem")
* @return The primary type (e.g., "Creature").
*/
QString getMainCardType(QString &type);
};
#endif
@@ -0,0 +1,635 @@
#include "cockatrice_xml_4.h"
#include "../../relation/card_relation.h"
#include <QCoreApplication>
#include <QDebug>
#include <QFile>
#include <QXmlStreamReader>
#include <libcockatrice/card/format/format_legality_rules.h>
#include <version_string.h>
#define COCKATRICE_XML4_TAGNAME "cockatrice_carddatabase"
#define COCKATRICE_XML4_TAGVER 4
#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)
{
}
bool CockatriceXml4Parser::getCanParseFile(const QString &fileName, QIODevice &device)
{
qCInfo(CockatriceXml4Log) << "Trying to parse: " << fileName;
if (!fileName.endsWith(".xml", Qt::CaseInsensitive)) {
qCInfo(CockatriceXml4Log) << "Parsing failed: wrong extension";
return false;
}
QXmlStreamReader xml(&device);
while (!xml.atEnd()) {
if (xml.readNext() == QXmlStreamReader::StartElement) {
if (xml.name().toString() == COCKATRICE_XML4_TAGNAME) {
int version = xml.attributes().value("version").toString().toInt();
if (version == COCKATRICE_XML4_TAGVER) {
return true;
} else {
qCInfo(CockatriceXml4Log) << "Parsing failed: wrong version" << version;
return false;
}
} else {
qCInfo(CockatriceXml4Log) << "Parsing failed: wrong element tag" << xml.name();
return false;
}
}
}
return true;
}
void CockatriceXml4Parser::parseFile(QIODevice &device)
{
QXmlStreamReader xml(&device);
while (!xml.atEnd()) {
if (xml.readNext() == QXmlStreamReader::StartElement) {
while (!xml.atEnd()) {
if (xml.readNext() == QXmlStreamReader::EndElement) {
break;
}
auto xmlName = xml.name().toString();
if (xmlName == "formats") {
loadFormats(xml);
} else if (xmlName == "sets") {
loadSetsFromXml(xml);
} else if (xmlName == "cards") {
loadCardsFromXml(xml);
} else if (!xmlName.isEmpty()) {
qCInfo(CockatriceXml4Log) << "Unknown item" << xmlName << ", trying to continue anyway";
xml.skipCurrentElement();
}
}
}
}
if (xml.hasError()) {
QString preamble = tr("Parse error at line %1 col %2:").arg(xml.lineNumber()).arg(xml.columnNumber());
qCWarning(CockatriceXml4Log).noquote() << preamble << xml.errorString();
}
}
static QSharedPointer<FormatRules> parseFormat(QXmlStreamReader &xml)
{
auto rulesPtr = FormatRulesPtr(new FormatRules());
if (xml.attributes().hasAttribute("formatName")) {
rulesPtr->formatName = xml.attributes().value("formatName").toString();
}
while (!xml.atEnd()) {
auto token = xml.readNext();
if (token == QXmlStreamReader::EndElement && xml.name().toString() == "format") {
break;
}
if (token != QXmlStreamReader::StartElement) {
continue;
}
QString xmlName = xml.name().toString();
if (xmlName == "minDeckSize") {
rulesPtr->minDeckSize = xml.readElementText().toInt();
} else if (xmlName == "maxDeckSize") {
QString text = xml.readElementText();
rulesPtr->maxDeckSize = text.toInt();
} else if (xmlName == "maxSideboardSize") {
rulesPtr->maxSideboardSize = xml.readElementText().toInt();
} else if (xmlName == "allowedCounts") {
while (!xml.atEnd()) {
token = xml.readNext();
if (token == QXmlStreamReader::EndElement && xml.name().toString() == "allowedCounts") {
break;
}
if (token == QXmlStreamReader::StartElement && xml.name().toString() == "count") {
AllowedCount c;
QString maxAttr = xml.attributes().value("max").toString();
c.max = (maxAttr == "unlimited") ? -1 : maxAttr.toInt();
c.label = xml.readElementText().trimmed();
rulesPtr->allowedCounts.append(c);
}
}
} else if (xmlName == "exceptions") {
while (!xml.atEnd()) {
token = xml.readNext();
if (token == QXmlStreamReader::EndElement && xml.name().toString() == "exceptions") {
break;
}
if (token == QXmlStreamReader::StartElement && xml.name().toString() == "exception") {
ExceptionRule ex;
while (!xml.atEnd()) {
token = xml.readNext();
if (token == QXmlStreamReader::EndElement && xml.name().toString() == "exception") {
break;
}
if (token == QXmlStreamReader::StartElement) {
QString ename = xml.name().toString();
if (ename == "maxCopies") {
QString text = xml.readElementText();
ex.maxCopies = (text == "unlimited") ? -1 : text.toInt();
} else if (ename == "cardCondition") {
CardCondition cond;
cond.field = xml.attributes().value("field").toString();
cond.matchType = xml.attributes().value("match").toString();
cond.value = xml.attributes().value("value").toString();
ex.conditions.append(cond);
xml.skipCurrentElement();
} else {
xml.skipCurrentElement();
}
}
}
rulesPtr->exceptions.append(ex);
}
}
} else {
xml.skipCurrentElement();
}
}
return rulesPtr;
}
void CockatriceXml4Parser::loadFormats(QXmlStreamReader &xml)
{
while (!xml.atEnd()) {
if (xml.readNext() == QXmlStreamReader::EndElement) {
break;
}
if (xml.name().toString() == "format") {
auto rulesPtr = parseFormat(xml);
emit addFormat(rulesPtr);
}
}
}
void CockatriceXml4Parser::loadSetsFromXml(QXmlStreamReader &xml)
{
while (!xml.atEnd()) {
if (xml.readNext() == QXmlStreamReader::EndElement) {
break;
}
auto xmlName = xml.name().toString();
if (xmlName == "set") {
QString shortName, longName, setType;
QDate releaseDate;
short priority;
while (!xml.atEnd()) {
if (xml.readNext() == QXmlStreamReader::EndElement) {
break;
}
xmlName = xml.name().toString();
if (xmlName == "name") {
shortName = xml.readElementText(QXmlStreamReader::IncludeChildElements);
} else if (xmlName == "longname") {
longName = xml.readElementText(QXmlStreamReader::IncludeChildElements);
} else if (xmlName == "settype") {
setType = xml.readElementText(QXmlStreamReader::IncludeChildElements);
} else if (xmlName == "releasedate") {
releaseDate =
QDate::fromString(xml.readElementText(QXmlStreamReader::IncludeChildElements), Qt::ISODate);
} else if (xmlName == "priority") {
priority = xml.readElementText(QXmlStreamReader::IncludeChildElements).toShort();
} else if (!xmlName.isEmpty()) {
qCInfo(CockatriceXml4Log) << "Unknown set property" << xmlName << ", trying to continue anyway";
xml.skipCurrentElement();
}
}
internalAddSet(shortName, longName, setType, releaseDate, static_cast<CardSet::Priority>(priority));
}
}
}
QVariantHash CockatriceXml4Parser::loadCardPropertiesFromXml(QXmlStreamReader &xml)
{
QVariantHash properties = QVariantHash();
while (!xml.atEnd()) {
if (xml.readNext() == QXmlStreamReader::EndElement) {
break;
}
auto xmlName = xml.name().toString();
if (!xmlName.isEmpty()) {
properties.insert(xmlName, xml.readElementText(QXmlStreamReader::IncludeChildElements));
}
}
return properties;
}
void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml)
{
bool includeRebalancedCards = cardPreferenceProvider->getIncludeRebalancedCards();
while (!xml.atEnd()) {
if (xml.readNext() == QXmlStreamReader::EndElement) {
break;
}
auto xmlName = xml.name().toString();
if (xmlName == "card") {
QString name = QString("");
QString text = QString("");
QVariantHash properties = QVariantHash();
QList<CardRelation *> relatedCards, reverseRelatedCards;
auto _sets = SetToPrintingsMap();
int tableRow = 0;
bool cipt = false;
bool landscapeOrientation = false;
bool isToken = false;
bool upsideDown = false;
while (!xml.atEnd()) {
if (xml.readNext() == QXmlStreamReader::EndElement) {
break;
}
xmlName = xml.name().toString();
// variable - assigned properties
if (xmlName == "name") {
name = xml.readElementText(QXmlStreamReader::IncludeChildElements);
} else if (xmlName == "text") {
text = xml.readElementText(QXmlStreamReader::IncludeChildElements);
} else if (xmlName == "token") {
isToken = static_cast<bool>(xml.readElementText(QXmlStreamReader::IncludeChildElements).toInt());
// generic properties
} else if (xmlName == "prop") {
properties = loadCardPropertiesFromXml(xml);
// positioning info
} else if (xmlName == "tablerow") {
tableRow = xml.readElementText(QXmlStreamReader::IncludeChildElements).toInt();
} else if (xmlName == "cipt") {
cipt = (xml.readElementText(QXmlStreamReader::IncludeChildElements) == "1");
} else if (xmlName == "landscapeOrientation") {
landscapeOrientation = (xml.readElementText(QXmlStreamReader::IncludeChildElements) == "1");
} else if (xmlName == "upsidedown") {
upsideDown = (xml.readElementText(QXmlStreamReader::IncludeChildElements) == "1");
// sets
} else if (xmlName == "set") {
// NOTE: attributes but be read before readElementText()
QXmlStreamAttributes attrs = xml.attributes();
QString setName = xml.readElementText(QXmlStreamReader::IncludeChildElements);
auto set = internalAddSet(setName);
if (set->getEnabled()) {
PrintingInfo printingInfo(set);
for (QXmlStreamAttribute attr : attrs) {
QString attrName = attr.name().toString();
if (attrName == "picURL") {
attrName = "picurl";
}
printingInfo.setProperty(attrName, attr.value().toString());
}
// 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") {
CardRelationType attachType = CardRelationType::DoesNotAttach;
bool exclude = false;
bool variable = false;
bool persistent = false;
bool facedown = false;
int count = 1;
QXmlStreamAttributes attrs = xml.attributes();
QString cardName = xml.readElementText(QXmlStreamReader::IncludeChildElements);
if (attrs.hasAttribute("count")) {
if (attrs.value("count").toString().indexOf("x=") == 0) {
variable = true;
count = attrs.value("count").toString().remove(0, 2).toInt();
} else if (attrs.value("count").toString().indexOf("x") == 0) {
variable = true;
} else {
count = attrs.value("count").toString().toInt();
}
if (count < 1) {
count = 1;
}
}
if (attrs.hasAttribute("attach")) {
attachType = attrs.value("attach").toString() == "transform" ? CardRelationType::TransformInto
: CardRelationType::AttachTo;
}
if (attrs.hasAttribute("exclude")) {
exclude = true;
}
if (attrs.hasAttribute("persistent")) {
persistent = true;
}
if (attrs.hasAttribute("facedown")) {
facedown = true;
}
auto *relation =
new CardRelation(cardName, attachType, exclude, variable, count, persistent, facedown);
if (xmlName == "reverse-related") {
reverseRelatedCards << relation;
} else {
relatedCards << relation;
}
} else if (!xmlName.isEmpty()) {
qCInfo(CockatriceXml4Log) << "Unknown card property" << xmlName << ", trying to continue anyway";
xml.skipCurrentElement();
}
}
if (name.isEmpty()) {
qCWarning(CockatriceXml4Log) << "Encountered card with empty name; skipping";
continue;
}
CardInfo::UiAttributes attributes = {.cipt = cipt,
.landscapeOrientation = landscapeOrientation,
.tableRow = tableRow,
.upsideDownArt = upsideDown};
CardInfoPtr newCard = CardInfo::newInstance(name, text, isToken, properties, relatedCards,
reverseRelatedCards, _sets, attributes);
emit addCard(newCard);
}
}
}
static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const QSharedPointer<FormatRules> &rulesPtr)
{
if (rulesPtr.isNull()) {
qCWarning(CockatriceXml4Log) << "&operator<< FormatRules is nullptr";
return xml;
}
const FormatRules &rules = *rulesPtr;
xml.writeStartElement("format");
if (!rules.formatName.isEmpty()) {
xml.writeAttribute("formatName", rules.formatName);
}
xml.writeTextElement("minDeckSize", QString::number(rules.minDeckSize));
xml.writeTextElement("maxDeckSize", rules.maxDeckSize >= 0 ? QString::number(rules.maxDeckSize) : "0");
xml.writeTextElement("maxSideboardSize", QString::number(rules.maxSideboardSize));
if (!rules.allowedCounts.isEmpty()) {
xml.writeStartElement("allowedCounts");
for (const AllowedCount &c : rules.allowedCounts) {
xml.writeStartElement("count");
xml.writeAttribute("max", c.max == -1 ? "unlimited" : QString::number(c.max));
xml.writeCharacters(c.label);
xml.writeEndElement(); // count
}
xml.writeEndElement(); // allowedCounts
}
if (!rules.exceptions.isEmpty()) {
xml.writeStartElement("exceptions");
for (const ExceptionRule &ex : rules.exceptions) {
xml.writeStartElement("exception");
xml.writeTextElement("maxCopies", ex.maxCopies == -1 ? "unlimited" : QString::number(ex.maxCopies));
for (const CardCondition &cond : ex.conditions) {
xml.writeStartElement("cardCondition");
xml.writeAttribute("field", cond.field);
xml.writeAttribute("match", cond.matchType);
xml.writeAttribute("value", cond.value);
xml.writeEndElement(); // cardCondition
}
xml.writeEndElement(); // exception
}
xml.writeEndElement(); // exceptions
}
xml.writeEndElement(); // format
return xml;
}
static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardSetPtr &set)
{
if (set.isNull()) {
qCWarning(CockatriceXml4Log) << "&operator<< set is nullptr";
return xml;
}
xml.writeStartElement("set");
xml.writeTextElement("name", set->getShortName());
xml.writeTextElement("longname", set->getLongName());
xml.writeTextElement("settype", set->getSetType());
xml.writeTextElement("releasedate", set->getReleaseDate().toString(Qt::ISODate));
xml.writeTextElement("priority", QString::number(set->getPriority()));
xml.writeEndElement();
return xml;
}
static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfoPtr &info)
{
if (info.isNull()) {
qCWarning(CockatriceXml4Log) << "operator<< info is nullptr";
return xml;
}
QString tmpString;
xml.writeStartElement("card");
// variable - assigned properties
xml.writeTextElement("name", info->getName());
xml.writeTextElement("text", info->getText());
if (info->getIsToken()) {
xml.writeTextElement("token", "1");
}
// generic properties
xml.writeStartElement("prop");
for (QString propName : info->getProperties()) {
xml.writeTextElement(propName, info->getProperty(propName));
}
xml.writeEndElement();
// sets
for (const auto &printings : info->getSets()) {
for (const PrintingInfo &set : printings) {
xml.writeStartElement("set");
for (const QString &propName : set.getProperties()) {
xml.writeAttribute(propName, set.getProperty(propName));
}
xml.writeCharacters(set.getSet()->getShortName());
xml.writeEndElement();
}
}
// related cards
const QList<CardRelation *> related = info->getRelatedCards();
for (auto i : related) {
xml.writeStartElement("related");
if (i->getDoesAttach()) {
xml.writeAttribute("attach", i->getAttachTypeAsString());
}
if (i->getIsCreateAllExclusion()) {
xml.writeAttribute("exclude", "exclude");
}
if (i->getIsPersistent()) {
xml.writeAttribute("persistent", "persistent");
}
if (i->getIsFaceDown()) {
xml.writeAttribute("facedown", "facedown");
}
if (i->getIsVariable()) {
if (1 == i->getDefaultCount()) {
xml.writeAttribute("count", "x");
} else {
xml.writeAttribute("count", "x=" + QString::number(i->getDefaultCount()));
}
} else if (1 != i->getDefaultCount()) {
xml.writeAttribute("count", QString::number(i->getDefaultCount()));
}
xml.writeCharacters(i->getName());
xml.writeEndElement();
}
const QList<CardRelation *> reverseRelated = info->getReverseRelatedCards();
for (auto i : reverseRelated) {
xml.writeStartElement("reverse-related");
if (i->getDoesAttach()) {
xml.writeAttribute("attach", i->getAttachTypeAsString());
}
if (i->getIsCreateAllExclusion()) {
xml.writeAttribute("exclude", "exclude");
}
if (i->getIsPersistent()) {
xml.writeAttribute("persistent", "persistent");
}
if (i->getIsVariable()) {
if (1 == i->getDefaultCount()) {
xml.writeAttribute("count", "x");
} else {
xml.writeAttribute("count", "x=" + QString::number(i->getDefaultCount()));
}
} else if (1 != i->getDefaultCount()) {
xml.writeAttribute("count", QString::number(i->getDefaultCount()));
}
xml.writeCharacters(i->getName());
xml.writeEndElement();
}
// positioning
const CardInfo::UiAttributes &attributes = info->getUiAttributes();
xml.writeTextElement("tablerow", QString::number(attributes.tableRow));
if (attributes.cipt) {
xml.writeTextElement("cipt", "1");
}
if (attributes.landscapeOrientation) {
xml.writeTextElement("landscapeOrientation", "1");
}
if (attributes.upsideDownArt) {
xml.writeTextElement("upsidedown", "1");
}
xml.writeEndElement(); // card
return xml;
}
bool CockatriceXml4Parser::saveToFile(FormatRulesNameMap _formats,
SetNameMap _sets,
CardNameMap cards,
const QString &fileName,
const QString &sourceUrl,
const QString &sourceVersion)
{
QFile file(fileName);
if (!file.open(QIODevice::WriteOnly)) {
return false;
}
QXmlStreamWriter xml(&file);
xml.setAutoFormatting(true);
xml.writeStartDocument();
xml.writeStartElement(COCKATRICE_XML4_TAGNAME);
xml.writeAttribute("version", QString::number(COCKATRICE_XML4_TAGVER));
xml.writeAttribute("xmlns:xsi", COCKATRICE_XML_XSI_NAMESPACE);
xml.writeAttribute("xsi:schemaLocation", COCKATRICE_XML4_SCHEMALOCATION);
xml.writeStartElement("info");
xml.writeTextElement("author", QCoreApplication::applicationName() + QString(" %1").arg(VERSION_STRING));
xml.writeTextElement("createdAt", QDateTime::currentDateTimeUtc().toString(Qt::ISODate));
xml.writeTextElement("sourceUrl", sourceUrl);
xml.writeTextElement("sourceVersion", sourceVersion);
xml.writeEndElement();
if (_formats.count() > 0) {
xml.writeStartElement("formats");
for (FormatRulesPtr format : _formats) {
xml << format;
}
xml.writeEndElement();
}
if (_sets.count() > 0) {
xml.writeStartElement("sets");
for (CardSetPtr set : _sets) {
xml << set;
}
xml.writeEndElement();
}
if (cards.count() > 0) {
xml.writeStartElement("cards");
for (CardInfoPtr card : cards) {
xml << card;
}
xml.writeEndElement();
}
xml.writeEndElement(); // cockatrice_carddatabase
xml.writeEndDocument();
return true;
}
@@ -0,0 +1,86 @@
#ifndef COCKATRICE_XML4_H
#define COCKATRICE_XML4_H
#include "card_database_parser.h"
#include <QLoggingCategory>
#include <QXmlStreamReader>
#include <libcockatrice/interfaces/interface_card_preference_provider.h>
inline Q_LOGGING_CATEGORY(CockatriceXml4Log, "cockatrice_xml.xml_4_parser");
/**
* @class CockatriceXml4Parser
* @ingroup CardDatabase
* @brief Parses version 4 of the Cockatrice XML Schema.
*
* This parser reads a Cockatrice XML4 database and emits CardInfoPtr
* and CardSetPtr objects. Card properties are read inside <prop> blocks,
* making the parser more extensible and schema-compliant.
*
* @note Differences from v3:
* - Card properties are stored in <prop> blocks as a QVariantHash.
* - 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.
*/
class CockatriceXml4Parser : public ICardDatabaseParser
{
Q_OBJECT
public:
explicit CockatriceXml4Parser(ICardPreferenceProvider *cardPreferenceProvider,
ICardSetPriorityController *cardSetPriorityController);
~CockatriceXml4Parser() override = default;
/**
* @brief Determines if the parser can handle this file.
* @param name File name.
* @param device Open QIODevice containing the XML.
* @return True if the file is a Cockatrice XML4 database.
*/
bool getCanParseFile(const QString &name, QIODevice &device) override;
/**
* @brief Parse the XML database.
* @param device Open QIODevice positioned at start of file.
*/
void parseFile(QIODevice &device) override;
/**
* @brief Save sets and cards back to an XML4 file.
*/
bool saveToFile(FormatRulesNameMap _formats,
SetNameMap _sets,
CardNameMap cards,
const QString &fileName,
const QString &sourceUrl = "unknown",
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.
* @return A QVariantHash mapping property names to values.
*/
QVariantHash loadCardPropertiesFromXml(QXmlStreamReader &xml);
/**
* @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);
void loadFormats(QXmlStreamReader &xml);
/**
* @brief Load all <set> elements from the XML stream.
* @param xml The open QXmlStreamReader positioned at the <sets> element.
* Parses <set> nodes including priority information.
*/
void loadSetsFromXml(QXmlStreamReader &xml);
};
#endif