Initial commit of mtgonline project
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user