Initial commit of mtgonline project
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
#include "card_info.h"
|
||||
|
||||
#include "game_specific_terms.h"
|
||||
#include "printing/printing_info.h"
|
||||
#include "relation/card_relation.h"
|
||||
#include "set/card_set.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QRegularExpression>
|
||||
#include <QSharedPointer>
|
||||
#include <QString>
|
||||
#include <QVariant>
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
class CardRelation;
|
||||
class CardSet;
|
||||
class CardInfo;
|
||||
|
||||
using CardInfoPtr = QSharedPointer<CardInfo>;
|
||||
|
||||
CardInfo::CardInfo(const QString &_name,
|
||||
const QString &_text,
|
||||
bool _isToken,
|
||||
QVariantHash _properties,
|
||||
const QList<CardRelation *> &_relatedCards,
|
||||
const QList<CardRelation *> &_reverseRelatedCards,
|
||||
SetToPrintingsMap _sets,
|
||||
const UiAttributes _uiAttributes)
|
||||
: name(_name), text(_text), isToken(_isToken), properties(std::move(_properties)), relatedCards(_relatedCards),
|
||||
reverseRelatedCards(_reverseRelatedCards), setsToPrintings(std::move(_sets)), uiAttributes(_uiAttributes)
|
||||
{
|
||||
simpleName = CardInfo::simplifyName(name);
|
||||
|
||||
refreshCachedSets();
|
||||
}
|
||||
|
||||
CardInfoPtr CardInfo::newInstance(const QString &_name)
|
||||
{
|
||||
return newInstance(_name, "", false, {}, {}, {}, {}, {});
|
||||
}
|
||||
|
||||
CardInfoPtr CardInfo::newInstance(const QString &_name,
|
||||
const QString &_text,
|
||||
bool _isToken,
|
||||
QVariantHash _properties,
|
||||
const QList<CardRelation *> &_relatedCards,
|
||||
const QList<CardRelation *> &_reverseRelatedCards,
|
||||
SetToPrintingsMap _sets,
|
||||
const UiAttributes _uiAttributes)
|
||||
{
|
||||
CardInfoPtr ptr(new CardInfo(_name, _text, _isToken, std::move(_properties), _relatedCards, _reverseRelatedCards,
|
||||
_sets, _uiAttributes));
|
||||
ptr->setSmartPointer(ptr);
|
||||
|
||||
for (const auto &printings : _sets) {
|
||||
for (const PrintingInfo &printing : printings) {
|
||||
printing.getSet()->append(ptr);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ptr;
|
||||
}
|
||||
|
||||
QString CardInfo::getCorrectedName() const
|
||||
{
|
||||
// remove all the characters reserved in windows file paths,
|
||||
// other oses only disallow a subset of these so it covers all
|
||||
static const QRegularExpression rmrx(R"(( // |[*<>:"\\?\x00-\x08\x10-\x1f]))");
|
||||
static const QRegularExpression spacerx(R"([/\x09-\x0f])");
|
||||
static const QString space(' ');
|
||||
QString result = name;
|
||||
// Fire // Ice, Circle of Protection: Red, "Ach! Hans, Run!", Who/What/When/Where/Why, Question Elemental?
|
||||
return result.remove(rmrx).replace(spacerx, space);
|
||||
}
|
||||
|
||||
QString CardInfo::getLegalityProp(const QString &format) const
|
||||
{
|
||||
return getProperty("format-" + format);
|
||||
}
|
||||
|
||||
bool CardInfo::isLegalInFormat(const QString &format) const
|
||||
{
|
||||
if (format.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
QString formatLegality = getLegalityProp(format);
|
||||
return formatLegality == "legal" || formatLegality == "restricted";
|
||||
}
|
||||
|
||||
void CardInfo::addToSet(const CardSetPtr &_set, const PrintingInfo &_info)
|
||||
{
|
||||
if (!_set->contains(smartThis)) {
|
||||
_set->append(smartThis);
|
||||
}
|
||||
if (!setsToPrintings[_set->getShortName()].contains(_info)) {
|
||||
setsToPrintings[_set->getShortName()].append(_info);
|
||||
}
|
||||
|
||||
refreshCachedSets();
|
||||
}
|
||||
|
||||
void CardInfo::combineLegalities(const QVariantHash &props)
|
||||
{
|
||||
QHashIterator<QString, QVariant> it(props);
|
||||
while (it.hasNext()) {
|
||||
it.next();
|
||||
if (it.key().startsWith("format-")) {
|
||||
smartThis->setProperty(it.key(), it.value().toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CardInfo::refreshCachedSets()
|
||||
{
|
||||
refreshCachedSetNames();
|
||||
refreshCachedAltNames();
|
||||
}
|
||||
|
||||
void CardInfo::refreshCachedSetNames()
|
||||
{
|
||||
QStringList setList;
|
||||
// update the cached list of set names
|
||||
for (const auto &printings : setsToPrintings) {
|
||||
for (const auto &printing : printings) {
|
||||
if (printing.getSet()->getEnabled()) {
|
||||
setList << printing.getSet()->getShortName();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
setsNames = setList.join(", ");
|
||||
}
|
||||
|
||||
void CardInfo::refreshCachedAltNames()
|
||||
{
|
||||
altNames.clear();
|
||||
|
||||
// update the altNames with the flavorNames
|
||||
for (const auto &printings : setsToPrintings) {
|
||||
for (const auto &printing : printings) {
|
||||
QString flavorName = printing.getFlavorName();
|
||||
if (!flavorName.isEmpty()) {
|
||||
altNames.insert(flavorName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString CardInfo::simplifyName(const QString &name)
|
||||
{
|
||||
static const QRegularExpression spaceOrSplit("(\\s+|\\/\\/.*)");
|
||||
static const QRegularExpression nonAlnum("[^a-z0-9]");
|
||||
|
||||
QString simpleName = name.toLower();
|
||||
|
||||
// remove spaces and right halves of split cards
|
||||
simpleName.remove(spaceOrSplit);
|
||||
|
||||
// So Aetherling would work, but not Ætherling since 'Æ' would get replaced
|
||||
// with nothing.
|
||||
simpleName.replace("æ", "ae");
|
||||
|
||||
// Replace Jötun Grunt with Jotun Grunt.
|
||||
simpleName = simpleName.normalized(QString::NormalizationForm_KD);
|
||||
|
||||
// remove all non alphanumeric characters from the name
|
||||
simpleName.remove(nonAlnum);
|
||||
return simpleName;
|
||||
}
|
||||
|
||||
QChar CardInfo::getColorChar() const
|
||||
{
|
||||
QString colors = getColors();
|
||||
switch (colors.size()) {
|
||||
case 0:
|
||||
return QChar();
|
||||
case 1:
|
||||
return colors.at(0);
|
||||
default:
|
||||
return QChar('m');
|
||||
}
|
||||
}
|
||||
|
||||
void CardInfo::resetReverseRelatedCards2Me()
|
||||
{
|
||||
for (CardRelation *cardRelation : this->getReverseRelatedCards2Me()) {
|
||||
cardRelation->deleteLater();
|
||||
}
|
||||
reverseRelatedCardsToMe = QList<CardRelation *>();
|
||||
}
|
||||
|
||||
// Back-compatibility methods. Remove ASAP
|
||||
QString CardInfo::getCardType() const
|
||||
{
|
||||
return getProperty(Mtg::CardType);
|
||||
}
|
||||
void CardInfo::setCardType(const QString &value)
|
||||
{
|
||||
setProperty(Mtg::CardType, value);
|
||||
}
|
||||
QString CardInfo::getCmc() const
|
||||
{
|
||||
return getProperty(Mtg::ConvertedManaCost);
|
||||
}
|
||||
QString CardInfo::getColors() const
|
||||
{
|
||||
return getProperty(Mtg::Colors);
|
||||
}
|
||||
void CardInfo::setColors(const QString &value)
|
||||
{
|
||||
setProperty(Mtg::Colors, value);
|
||||
}
|
||||
QString CardInfo::getLoyalty() const
|
||||
{
|
||||
return getProperty(Mtg::Loyalty);
|
||||
}
|
||||
QString CardInfo::getMainCardType() const
|
||||
{
|
||||
return getProperty(Mtg::MainCardType);
|
||||
}
|
||||
QString CardInfo::getManaCost() const
|
||||
{
|
||||
return getProperty(Mtg::ManaCost);
|
||||
}
|
||||
QString CardInfo::getPowTough() const
|
||||
{
|
||||
return getProperty(Mtg::PowTough);
|
||||
}
|
||||
void CardInfo::setPowTough(const QString &value)
|
||||
{
|
||||
setProperty(Mtg::PowTough, value);
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
#ifndef CARD_INFO_H
|
||||
#define CARD_INFO_H
|
||||
|
||||
#include "format/format_legality_rules.h"
|
||||
#include "printing/printing_info.h"
|
||||
|
||||
#include <QDate>
|
||||
#include <QHash>
|
||||
#include <QList>
|
||||
#include <QLoggingCategory>
|
||||
#include <QMap>
|
||||
#include <QMetaType>
|
||||
#include <QSharedPointer>
|
||||
#include <QVariant>
|
||||
#include <utility>
|
||||
|
||||
inline Q_LOGGING_CATEGORY(CardInfoLog, "card_info");
|
||||
|
||||
class CardInfo;
|
||||
class CardSet;
|
||||
class CardRelation;
|
||||
class ICardDatabaseParser;
|
||||
|
||||
typedef QSharedPointer<CardInfo> CardInfoPtr;
|
||||
typedef QSharedPointer<CardSet> CardSetPtr;
|
||||
typedef QSharedPointer<FormatRules> FormatRulesPtr;
|
||||
typedef QMap<QString, QList<PrintingInfo>> SetToPrintingsMap;
|
||||
|
||||
typedef QHash<QString, CardInfoPtr> CardNameMap;
|
||||
typedef QHash<QString, CardSetPtr> SetNameMap;
|
||||
typedef QHash<QString, FormatRulesPtr> FormatRulesNameMap;
|
||||
|
||||
Q_DECLARE_METATYPE(CardInfoPtr)
|
||||
|
||||
/**
|
||||
* @class CardInfo
|
||||
* @ingroup Cards
|
||||
*
|
||||
* @brief Represents a card and its associated metadata, properties, and relationships.
|
||||
*
|
||||
* CardInfo holds both static information (name, text, flags) and dynamic data
|
||||
* (properties, set memberships, relationships). It also integrates with
|
||||
* signals/slots, allowing observers to react to property or visual updates.
|
||||
*
|
||||
* Each CardInfo may belong to multiple sets through its printings, and can
|
||||
* be related to other cards through defined relationships.
|
||||
*/
|
||||
class CardInfo : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/**
|
||||
* @class CardInfo::UiAttributes
|
||||
* @ingroup Cards
|
||||
*
|
||||
* @brief Attributes of the card that affect display and game logic.
|
||||
*/
|
||||
struct UiAttributes
|
||||
{
|
||||
bool cipt = false; ///< Positioning flag used by UI.
|
||||
bool landscapeOrientation = false; ///< Orientation flag for rendering.
|
||||
int tableRow = 0; ///< Row index in a table or visual representation.
|
||||
bool upsideDownArt = false; ///< Whether artwork is flipped for visual purposes.
|
||||
};
|
||||
|
||||
private:
|
||||
/** @name Private Card Properties
|
||||
* @anchor PrivateCardProperties
|
||||
*/
|
||||
///@{
|
||||
CardInfoPtr smartThis; ///< Smart pointer to self for safe cross-references.
|
||||
QString name; ///< Full name of the card.
|
||||
QString simpleName; ///< Simplified name for fuzzy matching.
|
||||
QString text; ///< Text description or rules text of the card.
|
||||
bool isToken; ///< Whether this card is a token or not.
|
||||
QVariantHash properties; ///< Key-value store of dynamic card properties.
|
||||
QList<CardRelation *> relatedCards; ///< Forward references to related cards.
|
||||
QList<CardRelation *> reverseRelatedCards; ///< Cards that refer back to this card.
|
||||
QList<CardRelation *> reverseRelatedCardsToMe; ///< Cards that consider this card as related.
|
||||
SetToPrintingsMap setsToPrintings; ///< Mapping from set names to printing variations.
|
||||
UiAttributes uiAttributes; ///< Attributes that affect display and game logic
|
||||
QString setsNames; ///< Cached, human-readable list of set names.
|
||||
QSet<QString> altNames; ///< Cached set of alternate names, used when searching
|
||||
///@}
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Constructs a CardInfo with full initialization.
|
||||
*
|
||||
* @param _name The name of the card.
|
||||
* @param _text Rules text or description of the card.
|
||||
* @param _isToken Flag indicating whether the card is a token.
|
||||
* @param _properties Arbitrary key-value properties.
|
||||
* @param _relatedCards Forward references to related cards.
|
||||
* @param _reverseRelatedCards Backward references to related cards.
|
||||
* @param _sets Map of set names to printing information.
|
||||
* @param _uiAttributes Attributes that affect display and game logic
|
||||
*/
|
||||
explicit CardInfo(const QString &_name,
|
||||
const QString &_text,
|
||||
bool _isToken,
|
||||
QVariantHash _properties,
|
||||
const QList<CardRelation *> &_relatedCards,
|
||||
const QList<CardRelation *> &_reverseRelatedCards,
|
||||
SetToPrintingsMap _sets,
|
||||
UiAttributes _uiAttributes);
|
||||
|
||||
/**
|
||||
* @brief Copy constructor for CardInfo.
|
||||
*
|
||||
* Performs a deep copy of properties, sets, and related card lists.
|
||||
*
|
||||
* @param other Another CardInfo to copy.
|
||||
*/
|
||||
CardInfo(const CardInfo &other)
|
||||
: QObject(other.parent()), name(other.name), simpleName(other.simpleName), text(other.text),
|
||||
isToken(other.isToken), properties(other.properties), relatedCards(other.relatedCards),
|
||||
reverseRelatedCards(other.reverseRelatedCards), reverseRelatedCardsToMe(other.reverseRelatedCardsToMe),
|
||||
setsToPrintings(other.setsToPrintings), uiAttributes(other.uiAttributes), setsNames(other.setsNames),
|
||||
altNames(other.altNames)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Creates a new instance with only the card name.
|
||||
*
|
||||
* All other fields are set to defaults.
|
||||
*
|
||||
* @param _name The card name.
|
||||
* @return Shared pointer to the new CardInfo instance.
|
||||
*/
|
||||
static CardInfoPtr newInstance(const QString &_name);
|
||||
|
||||
/**
|
||||
* @brief Creates a new instance with full initialization.
|
||||
*
|
||||
* @param _name Name of the card.
|
||||
* @param _text Rules text or description.
|
||||
* @param _isToken Token flag.
|
||||
* @param _properties Arbitrary properties.
|
||||
* @param _relatedCards Forward relationships.
|
||||
* @param _reverseRelatedCards Reverse relationships.
|
||||
* @param _sets Printing information per set.
|
||||
* @param _uiAttributes Attributes that affect display and game logic
|
||||
* @return Shared pointer to the new CardInfo instance.
|
||||
*/
|
||||
static CardInfoPtr newInstance(const QString &_name,
|
||||
const QString &_text,
|
||||
bool _isToken,
|
||||
QVariantHash _properties,
|
||||
const QList<CardRelation *> &_relatedCards,
|
||||
const QList<CardRelation *> &_reverseRelatedCards,
|
||||
SetToPrintingsMap _sets,
|
||||
UiAttributes _uiAttributes);
|
||||
|
||||
/**
|
||||
* @brief Clones the current CardInfo instance.
|
||||
*
|
||||
* Uses the copy constructor and ensures the smart pointer is properly set.
|
||||
*
|
||||
* @return Shared pointer to the cloned CardInfo.
|
||||
*/
|
||||
[[nodiscard]] CardInfoPtr clone() const
|
||||
{
|
||||
auto newCardInfo = CardInfoPtr(new CardInfo(*this));
|
||||
newCardInfo->setSmartPointer(newCardInfo); // Set the smart pointer for the new instance
|
||||
return newCardInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sets the internal smart pointer to self.
|
||||
*
|
||||
* Used internally to allow safe cross-references among CardInfo and CardSet.
|
||||
*
|
||||
* @param _ptr Shared pointer pointing to this instance.
|
||||
*/
|
||||
void setSmartPointer(CardInfoPtr _ptr)
|
||||
{
|
||||
smartThis = std::move(_ptr);
|
||||
}
|
||||
|
||||
/** @name Basic Properties Accessors */ //@{
|
||||
[[nodiscard]] inline const QString &getName() const
|
||||
{
|
||||
return name;
|
||||
}
|
||||
[[nodiscard]] const QString &getSimpleName() const
|
||||
{
|
||||
return simpleName;
|
||||
}
|
||||
const QSet<QString> &getAltNames()
|
||||
{
|
||||
return altNames;
|
||||
}
|
||||
[[nodiscard]] const QString &getText() const
|
||||
{
|
||||
return text;
|
||||
}
|
||||
void setText(const QString &_text)
|
||||
{
|
||||
text = _text;
|
||||
emit cardInfoChanged(smartThis);
|
||||
}
|
||||
[[nodiscard]] bool getIsToken() const
|
||||
{
|
||||
return isToken;
|
||||
}
|
||||
[[nodiscard]] QStringList getProperties() const
|
||||
{
|
||||
return properties.keys();
|
||||
}
|
||||
[[nodiscard]] QString getProperty(const QString &propertyName) const
|
||||
{
|
||||
return properties.value(propertyName).toString();
|
||||
}
|
||||
void setProperty(const QString &_name, const QString &_value)
|
||||
{
|
||||
properties.insert(_name, _value);
|
||||
emit cardInfoChanged(smartThis);
|
||||
}
|
||||
[[nodiscard]] bool hasProperty(const QString &propertyName) const
|
||||
{
|
||||
return properties.contains(propertyName);
|
||||
}
|
||||
[[nodiscard]] const SetToPrintingsMap &getSets() const
|
||||
{
|
||||
return setsToPrintings;
|
||||
}
|
||||
[[nodiscard]] const QString &getSetsNames() const
|
||||
{
|
||||
return setsNames;
|
||||
}
|
||||
//@}
|
||||
|
||||
/** @name Related Cards Accessors */ //@{
|
||||
[[nodiscard]] const QList<CardRelation *> &getRelatedCards() const
|
||||
{
|
||||
return relatedCards;
|
||||
}
|
||||
[[nodiscard]] const QList<CardRelation *> &getReverseRelatedCards() const
|
||||
{
|
||||
return reverseRelatedCards;
|
||||
}
|
||||
[[nodiscard]] const QList<CardRelation *> &getReverseRelatedCards2Me() const
|
||||
{
|
||||
return reverseRelatedCardsToMe;
|
||||
}
|
||||
[[nodiscard]] QList<CardRelation *> getAllRelatedCards() const
|
||||
{
|
||||
QList<CardRelation *> result;
|
||||
result.append(getRelatedCards());
|
||||
result.append(getReverseRelatedCards2Me());
|
||||
return result;
|
||||
}
|
||||
void resetReverseRelatedCards2Me();
|
||||
void addReverseRelatedCards2Me(CardRelation *cardRelation)
|
||||
{
|
||||
reverseRelatedCardsToMe.append(cardRelation);
|
||||
}
|
||||
//@}
|
||||
|
||||
/** @name UI Positioning */ //@{
|
||||
[[nodiscard]] const UiAttributes &getUiAttributes() const
|
||||
{
|
||||
return uiAttributes;
|
||||
}
|
||||
//@}
|
||||
|
||||
[[nodiscard]] QChar getColorChar() const;
|
||||
|
||||
/** @name Legacy/Convenience Property Accessors */ //@{
|
||||
[[nodiscard]] QString getCardType() const;
|
||||
void setCardType(const QString &value);
|
||||
[[nodiscard]] QString getCmc() const;
|
||||
[[nodiscard]] QString getColors() const;
|
||||
void setColors(const QString &value);
|
||||
[[nodiscard]] QString getLoyalty() const;
|
||||
[[nodiscard]] QString getMainCardType() const;
|
||||
[[nodiscard]] QString getManaCost() const;
|
||||
[[nodiscard]] QString getPowTough() const;
|
||||
void setPowTough(const QString &value);
|
||||
//@}
|
||||
|
||||
/**
|
||||
* @brief Returns a version of the card name safe for file storage or fuzzy matching.
|
||||
*
|
||||
* Removes invalid characters, replaces spacing markers, and normalizes diacritics.
|
||||
*
|
||||
* @return Corrected card name as a QString.
|
||||
*/
|
||||
[[nodiscard]] QString getCorrectedName() const;
|
||||
|
||||
/**
|
||||
* @brief Gets the card's legality value for the given format.
|
||||
* The legality prop for a format is stored in the property map under the key "format-<name>"
|
||||
* @param format The format's name.
|
||||
* @return The card's legality value for the format. Empty if not found.
|
||||
*/
|
||||
[[nodiscard]] QString getLegalityProp(const QString &format) const;
|
||||
|
||||
/**
|
||||
* @brief Checks if the card is legal in the given format.
|
||||
* A card is considered legal in a format if its properties map contains an entry for "format-<name>", with value
|
||||
* "legal" or "restricted".
|
||||
* @param format The format's name. If empty, will always return true.
|
||||
* @return Whether the card is legal in the given format.
|
||||
*/
|
||||
[[nodiscard]] bool isLegalInFormat(const QString &format) const;
|
||||
|
||||
/**
|
||||
* @brief Adds a printing to a specific set.
|
||||
*
|
||||
* Updates the mapping and refreshes the cached list of set names.
|
||||
*
|
||||
* @param _set The set to which the card should be added.
|
||||
* @param _info Optional printing information.
|
||||
*/
|
||||
void addToSet(const CardSetPtr &_set, const PrintingInfo &_info = PrintingInfo());
|
||||
|
||||
/**
|
||||
* @brief Combines legality properties from a provided map.
|
||||
*
|
||||
* Useful for merging format legality flags from multiple sources.
|
||||
*
|
||||
* @param props Key-value mapping of format legalities.
|
||||
*/
|
||||
void combineLegalities(const QVariantHash &props);
|
||||
|
||||
/**
|
||||
* @brief Refreshes all cached fields that are calculated from the contained sets and printings.
|
||||
*
|
||||
* Typically called after adding or modifying set memberships or printings.
|
||||
*/
|
||||
void refreshCachedSets();
|
||||
|
||||
/**
|
||||
* @brief Simplifies a name for fuzzy matching.
|
||||
*
|
||||
* Converts to lowercase, removes punctuation/spacing.
|
||||
*
|
||||
* @param name Original name string.
|
||||
* @return Simplified name string.
|
||||
*/
|
||||
static QString simplifyName(const QString &name);
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Refreshes the cached, human-readable list of set names.
|
||||
*
|
||||
* Typically called after adding or modifying set memberships.
|
||||
*/
|
||||
void refreshCachedSetNames();
|
||||
|
||||
/**
|
||||
* @brief Refreshes the cached list of alt names for the card.
|
||||
*
|
||||
* Typically called after adding or modifying the contained printings.
|
||||
*/
|
||||
void refreshCachedAltNames();
|
||||
|
||||
signals:
|
||||
/**
|
||||
* @brief Emitted when a pixmap for this card has been updated or finished loading.
|
||||
*
|
||||
* @param printing Specific printing for which the pixmap has updated.
|
||||
*/
|
||||
void pixmapUpdated(const PrintingInfo &printing);
|
||||
|
||||
/**
|
||||
* @brief Emitted when card properties or state have changed.
|
||||
*
|
||||
* @param card Shared pointer to the CardInfo instance that changed.
|
||||
*/
|
||||
void cardInfoChanged(CardInfoPtr card);
|
||||
};
|
||||
#endif
|
||||
@@ -0,0 +1,75 @@
|
||||
#include "card_info_comparator.h"
|
||||
|
||||
CardInfoComparator::CardInfoComparator(const QStringList &properties, Qt::SortOrder order)
|
||||
: m_properties(properties), m_order(order)
|
||||
{
|
||||
}
|
||||
|
||||
bool CardInfoComparator::operator()(const CardInfoPtr &a, const CardInfoPtr &b) const
|
||||
{
|
||||
// Iterate over each property in the list
|
||||
for (const QString &property : m_properties) {
|
||||
QVariant valueA = getProperty(a, property);
|
||||
QVariant valueB = getProperty(b, property);
|
||||
|
||||
// Compare the current property
|
||||
if (valueA != valueB) {
|
||||
// If values differ, perform comparison
|
||||
return compareVariants(valueA, valueB) ? (m_order == Qt::AscendingOrder) : (m_order == Qt::DescendingOrder);
|
||||
}
|
||||
}
|
||||
|
||||
// If all properties are equal, return false (indicating they are considered equal for sorting purposes)
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CardInfoComparator::compareVariants(const QVariant &a, const QVariant &b) const
|
||||
{
|
||||
// Determine the type of QVariant based on Qt version
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
if (a.typeId() != b.typeId()) {
|
||||
#else
|
||||
if (a.type() != b.type()) {
|
||||
#endif
|
||||
// If they are not the same type, compare as strings
|
||||
return a.toString() < b.toString();
|
||||
}
|
||||
|
||||
// Perform type-specific comparison
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
switch (static_cast<int>(a.typeId())) {
|
||||
#else
|
||||
switch (static_cast<int>(a.type())) {
|
||||
#endif
|
||||
case static_cast<int>(QMetaType::Int):
|
||||
return a.toInt() < b.toInt();
|
||||
case static_cast<int>(QMetaType::Double):
|
||||
return a.toDouble() < b.toDouble();
|
||||
case static_cast<int>(QMetaType::QString):
|
||||
return a.toString() < b.toString();
|
||||
case static_cast<int>(QMetaType::Bool):
|
||||
return a.toBool() < b.toBool();
|
||||
default:
|
||||
// Default to comparing as strings
|
||||
return a.toString() < b.toString();
|
||||
}
|
||||
}
|
||||
|
||||
QVariant CardInfoComparator::getProperty(const CardInfoPtr &card, const QString &property) const
|
||||
{
|
||||
// Check if the property exists in the main fields of the class
|
||||
if (property == "name") {
|
||||
return card->getName();
|
||||
} else if (property == "text") {
|
||||
return card->getText();
|
||||
} else if (property == "isToken") {
|
||||
return card->getIsToken();
|
||||
}
|
||||
|
||||
// Otherwise, check if it's a custom property in the QVariantHash
|
||||
if (card->hasProperty(property)) {
|
||||
return card->getProperty(property);
|
||||
}
|
||||
|
||||
return QVariant(); // Return an invalid variant if the property does not exist
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* @file card_info_comparator.h
|
||||
* @ingroup Cards
|
||||
*/
|
||||
//! \todo Document this file.
|
||||
|
||||
#ifndef CARD_INFO_COMPARATOR_H
|
||||
#define CARD_INFO_COMPARATOR_H
|
||||
|
||||
#include "card_info.h"
|
||||
|
||||
#include <QVariant>
|
||||
#include <Qt>
|
||||
|
||||
class CardInfoComparator
|
||||
{
|
||||
public:
|
||||
explicit CardInfoComparator(const QStringList &properties, Qt::SortOrder order = Qt::AscendingOrder);
|
||||
bool operator()(const CardInfoPtr &a, const CardInfoPtr &b) const;
|
||||
|
||||
private:
|
||||
QStringList m_properties; // List of properties to sort by
|
||||
Qt::SortOrder m_order;
|
||||
|
||||
[[nodiscard]] QVariant getProperty(const CardInfoPtr &card, const QString &property) const;
|
||||
[[nodiscard]] bool compareVariants(const QVariant &a, const QVariant &b) const;
|
||||
};
|
||||
|
||||
#endif // CARD_INFO_COMPARATOR_H
|
||||
@@ -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
|
||||
@@ -0,0 +1,53 @@
|
||||
#include "format_legality_rules.h"
|
||||
|
||||
#include <libcockatrice/card/card_info.h>
|
||||
|
||||
bool cardMatchesCondition(const CardInfo &card, const CardCondition &cond)
|
||||
{
|
||||
CardMatchType type = matchTypeFromString(cond.matchType);
|
||||
QString fieldValue;
|
||||
if (cond.field == "name") {
|
||||
fieldValue = card.getName();
|
||||
} else if (cond.field == "text") {
|
||||
fieldValue = card.getText();
|
||||
} else {
|
||||
fieldValue = card.getProperty(cond.field);
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case CardMatchType::Equals:
|
||||
return fieldValue == cond.value;
|
||||
case CardMatchType::NotEquals:
|
||||
return fieldValue != cond.value;
|
||||
case CardMatchType::Contains:
|
||||
return fieldValue.contains(cond.value, Qt::CaseInsensitive);
|
||||
case CardMatchType::NotContains:
|
||||
return !fieldValue.contains(cond.value, Qt::CaseInsensitive);
|
||||
case CardMatchType::Regex: {
|
||||
QRegularExpression re(cond.value, QRegularExpression::CaseInsensitiveOption);
|
||||
return re.match(fieldValue).hasMatch();
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool exceptionAppliesToCard(const CardInfo &card, const ExceptionRule &rule)
|
||||
{
|
||||
for (const CardCondition &cond : rule.conditions) {
|
||||
if (!cardMatchesCondition(card, cond)) {
|
||||
return false; // all conditions must match
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool cardHasAnyException(const CardInfo &card, const FormatRules &format)
|
||||
{
|
||||
for (const ExceptionRule &rule : format.exceptions) {
|
||||
if (exceptionAppliesToCard(card, rule)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
#ifndef COCKATRICE_FORMAT_LEGALITY_RULES_H
|
||||
#define COCKATRICE_FORMAT_LEGALITY_RULES_H
|
||||
|
||||
#include <QRegularExpression>
|
||||
#include <QSharedPointer>
|
||||
#include <QString>
|
||||
|
||||
class CardInfo;
|
||||
using CardInfoPtr = QSharedPointer<CardInfo>;
|
||||
|
||||
struct CardCondition
|
||||
{
|
||||
QString field; // e.g. "type", "maintype", "text"
|
||||
QString matchType; // "contains", "equals", "regex", "notContains", etc.
|
||||
QString value; // e.g. "Basic Land"
|
||||
};
|
||||
|
||||
struct AllowedCount
|
||||
{
|
||||
int max = 0; // 4, 1, 0, or -1 for unlimited
|
||||
QString label; // "legal", "restricted", "banned"
|
||||
};
|
||||
|
||||
struct ExceptionRule
|
||||
{
|
||||
QList<CardCondition> conditions; // All must match
|
||||
int maxCopies = -1; // -1 = unlimited
|
||||
};
|
||||
|
||||
struct FormatRules
|
||||
{
|
||||
QString formatName;
|
||||
int minDeckSize = 60;
|
||||
int maxDeckSize = -1; // -1 = unlimited
|
||||
int maxSideboardSize = 15;
|
||||
|
||||
QList<AllowedCount> allowedCounts;
|
||||
|
||||
QList<ExceptionRule> exceptions; // Cards allowed to break maxCopies
|
||||
};
|
||||
|
||||
enum class CardMatchType
|
||||
{
|
||||
Equals,
|
||||
NotEquals,
|
||||
Contains,
|
||||
NotContains,
|
||||
Regex
|
||||
};
|
||||
|
||||
// convert string to enum
|
||||
inline CardMatchType matchTypeFromString(const QString &str)
|
||||
{
|
||||
if (str == "equals") {
|
||||
return CardMatchType::Equals;
|
||||
}
|
||||
if (str == "notEquals") {
|
||||
return CardMatchType::NotEquals;
|
||||
}
|
||||
if (str == "contains") {
|
||||
return CardMatchType::Contains;
|
||||
}
|
||||
if (str == "notContains") {
|
||||
return CardMatchType::NotContains;
|
||||
}
|
||||
if (str == "regex") {
|
||||
return CardMatchType::Regex;
|
||||
}
|
||||
return CardMatchType::Equals; // fallback default
|
||||
}
|
||||
|
||||
bool cardMatchesCondition(const CardInfo &card, const CardCondition &cond);
|
||||
|
||||
bool exceptionAppliesToCard(const CardInfo &card, const ExceptionRule &rule);
|
||||
|
||||
bool cardHasAnyException(const CardInfo &card, const FormatRules &format);
|
||||
|
||||
#endif // COCKATRICE_FORMAT_LEGALITY_RULES_H
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* @file game_specific_terms.h
|
||||
* @ingroup Cards
|
||||
*/
|
||||
//! \todo Document this file.
|
||||
|
||||
#ifndef GAME_SPECIFIC_TERMS_H
|
||||
#define GAME_SPECIFIC_TERMS_H
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QString>
|
||||
|
||||
/*
|
||||
* Collection of traslatable property names used in games,
|
||||
* so we can use Game::Property instead of hardcoding strings.
|
||||
* Note: Mtg = "Maybe that game"
|
||||
*/
|
||||
|
||||
namespace Mtg
|
||||
{
|
||||
QString const CardType("type");
|
||||
QString const ConvertedManaCost("cmc");
|
||||
QString const Colors("colors");
|
||||
QString const Loyalty("loyalty");
|
||||
QString const MainCardType("maintype");
|
||||
QString const ManaCost("manacost");
|
||||
QString const PowTough("pt");
|
||||
QString const Side("side");
|
||||
QString const Layout("layout");
|
||||
QString const ColorIdentity("coloridentity");
|
||||
|
||||
inline static const QString getNicePropertyName(QString key)
|
||||
{
|
||||
if (key == CardType) {
|
||||
return QCoreApplication::translate("Mtg", "Card Type");
|
||||
}
|
||||
if (key == ConvertedManaCost) {
|
||||
return QCoreApplication::translate("Mtg", "Mana Value");
|
||||
}
|
||||
if (key == Colors) {
|
||||
return QCoreApplication::translate("Mtg", "Color(s)");
|
||||
}
|
||||
if (key == Loyalty) {
|
||||
return QCoreApplication::translate("Mtg", "Loyalty");
|
||||
}
|
||||
if (key == MainCardType) {
|
||||
return QCoreApplication::translate("Mtg", "Main Card Type");
|
||||
}
|
||||
if (key == ManaCost) {
|
||||
return QCoreApplication::translate("Mtg", "Mana Cost");
|
||||
}
|
||||
if (key == PowTough) {
|
||||
return QCoreApplication::translate("Mtg", "P/T");
|
||||
}
|
||||
if (key == Side) {
|
||||
return QCoreApplication::translate("Mtg", "Side");
|
||||
}
|
||||
if (key == Layout) {
|
||||
return QCoreApplication::translate("Mtg", "Layout");
|
||||
}
|
||||
if (key == ColorIdentity) {
|
||||
return QCoreApplication::translate("Mtg", "Color Identity");
|
||||
}
|
||||
return key;
|
||||
}
|
||||
} // namespace Mtg
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,66 @@
|
||||
#include "card_name_normalizer.h"
|
||||
|
||||
#include "../database/card_database_manager.h"
|
||||
#include "../printing/exact_card.h"
|
||||
|
||||
#include <QRegularExpression>
|
||||
|
||||
/**
|
||||
* @brief Resolves the complete display name of a card.
|
||||
* @param cardName Base name.
|
||||
* @return Full display name, or the cardName unchanged if a display name is not found.
|
||||
*/
|
||||
static QString getCompleteCardName(const QString &cardName)
|
||||
{
|
||||
ExactCard temp = CardDatabaseManager::query()->guessCard({cardName});
|
||||
if (temp) {
|
||||
return temp.getName();
|
||||
}
|
||||
|
||||
return cardName;
|
||||
}
|
||||
|
||||
QString CardNameNormalizer::operator()(const QString &cardNameString) const
|
||||
{
|
||||
QString cardName = cardNameString;
|
||||
|
||||
// Regex for advanced card parsing
|
||||
static const QRegularExpression reSplitCard(R"( ?\/\/ ?)");
|
||||
static const QRegularExpression reBrace(R"( ?[\[\{][^\]\}]*[\]\}] ?)"); // not nested
|
||||
static const QRegularExpression reRoundBrace(R"(^\([^\)]*\) ?)"); // () are only matched at start of string
|
||||
static const QRegularExpression reDigitBrace(R"( ?\(\d*\) ?)"); // () are matched if containing digits
|
||||
static const QRegularExpression reBraceDigit(
|
||||
R"( ?\([\dA-Z]+\) *\d+$)"); // () are matched if containing setcode then a number
|
||||
static const QRegularExpression reDoubleFacedMarker(R"( ?\(Transform\) ?)");
|
||||
|
||||
static const QHash<QRegularExpression, QString> differences{{QRegularExpression("’"), "'"},
|
||||
{QRegularExpression("Æ"), "Ae"},
|
||||
{QRegularExpression("æ"), "ae"},
|
||||
{QRegularExpression(" ?[|/]+ ?"), " // "}};
|
||||
|
||||
// Handle advanced card types
|
||||
if (cardName.contains(reSplitCard)) {
|
||||
cardName = cardName.split(reSplitCard).join(" // ");
|
||||
}
|
||||
|
||||
if (cardName.contains(reDoubleFacedMarker)) {
|
||||
QStringList faces = cardName.split(reDoubleFacedMarker);
|
||||
cardName = faces.first().trimmed();
|
||||
}
|
||||
|
||||
// Remove unnecessary characters
|
||||
cardName.remove(reBrace);
|
||||
cardName.remove(reRoundBrace); // I'll be entirely honest here, these are split to accommodate just three cards
|
||||
cardName.remove(reDigitBrace); // from un-sets that have a word in between round braces at the end
|
||||
cardName.remove(reBraceDigit); // very specific format with the set code in () and collectors number after
|
||||
|
||||
// Normalize characters
|
||||
for (auto diff = differences.constBegin(); diff != differences.constEnd(); ++diff) {
|
||||
cardName.replace(diff.key(), diff.value());
|
||||
}
|
||||
|
||||
// Resolve complete card name
|
||||
cardName = getCompleteCardName(cardName);
|
||||
|
||||
return cardName;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#ifndef COCKATRICE_CARD_NAME_NORMALIZER_H
|
||||
#define COCKATRICE_CARD_NAME_NORMALIZER_H
|
||||
|
||||
#include <QString>
|
||||
|
||||
/**
|
||||
* Functor that normalizes the raw card name parsed during a plaintext deck import into the card name that Cockatrice
|
||||
* uses.
|
||||
*/
|
||||
struct CardNameNormalizer
|
||||
{
|
||||
QString operator()(const QString &cardNameString) const;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_CARD_NAME_NORMALIZER_H
|
||||
@@ -0,0 +1,82 @@
|
||||
#include "exact_card.h"
|
||||
|
||||
#include "../card_info.h"
|
||||
#include "printing_info.h"
|
||||
|
||||
/**
|
||||
* Default constructor.
|
||||
* This will set the CardInfoPtr to null.
|
||||
* The printing will be the default-constructed PrintingInfo.
|
||||
*/
|
||||
ExactCard::ExactCard()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param _card The card. Can be null.
|
||||
* @param _printing The printing. Can be empty.
|
||||
*/
|
||||
ExactCard::ExactCard(const CardInfoPtr &_card, const PrintingInfo &_printing) : card(_card), printing(_printing)
|
||||
{
|
||||
}
|
||||
|
||||
bool ExactCard::operator==(const ExactCard &other) const
|
||||
{
|
||||
return this->card == other.card && this->printing == other.printing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to safely get the card's name.
|
||||
* @return The name in the CardInfo, or an empty string if card is null
|
||||
*/
|
||||
QString ExactCard::getName() const
|
||||
{
|
||||
return card.isNull() ? "" : card->getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a view of the underlying cardInfoPtr.
|
||||
* @return A const reference to the CardInfo, or an empty CardInfo if card is null
|
||||
*/
|
||||
const CardInfo &ExactCard::getInfo() const
|
||||
{
|
||||
if (card.isNull()) {
|
||||
static CardInfoPtr emptyCard = CardInfo::newInstance("");
|
||||
return *emptyCard;
|
||||
}
|
||||
return *card;
|
||||
}
|
||||
|
||||
/**
|
||||
* The key used to identify this exact printing in the cache
|
||||
*/
|
||||
QString ExactCard::getPixmapCacheKey() const
|
||||
{
|
||||
QString uuid = printing.getUuid();
|
||||
QString suffix = uuid.isEmpty() ? "" : "_" + uuid;
|
||||
return QLatin1String("card_") + card->getName() + suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the card is null or empty.
|
||||
*/
|
||||
bool ExactCard::isEmpty() const
|
||||
{
|
||||
return card.isNull() || card->getName().isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if isEmpty() is false
|
||||
*/
|
||||
ExactCard::operator bool() const
|
||||
{
|
||||
return !isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the CardInfo to emit the pixmapUpdated signal
|
||||
*/
|
||||
void ExactCard::emitPixmapUpdated() const
|
||||
{
|
||||
emit card->pixmapUpdated(printing);
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
#ifndef EXACT_CARD_H
|
||||
#define EXACT_CARD_H
|
||||
|
||||
#include "../card_info.h"
|
||||
|
||||
/**
|
||||
* @class ExactCard
|
||||
* @ingroup CardPrintings
|
||||
*
|
||||
* @brief Represents a specific card instance, defined by its CardInfo
|
||||
* and a particular printing.
|
||||
*
|
||||
* An ExactCard identifies a card not only by its underlying CardInfoPtr
|
||||
* (which may be null), but also by its PrintingInfo, which specifies the
|
||||
* exact printing/variant. This allows distinguishing between different
|
||||
* printings of the same logical card (e.g., different sets, promos, foils).
|
||||
*/
|
||||
class ExactCard
|
||||
{
|
||||
CardInfoPtr card;
|
||||
PrintingInfo printing;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Constructs an empty ExactCard.
|
||||
*
|
||||
* The CardInfoPtr will be null, and PrintingInfo will be default-constructed.
|
||||
* An empty ExactCard represents "no card".
|
||||
*/
|
||||
ExactCard();
|
||||
|
||||
/**
|
||||
* @brief Constructs an ExactCard from a card and printing.
|
||||
*
|
||||
* @param _card The card info pointer. May be null.
|
||||
* @param _printing The printing details. Defaults to an empty PrintingInfo.
|
||||
*/
|
||||
explicit ExactCard(const CardInfoPtr &_card, const PrintingInfo &_printing = PrintingInfo());
|
||||
|
||||
/**
|
||||
* @brief Returns the underlying CardInfoPtr.
|
||||
*
|
||||
* May be null if the ExactCard is empty.
|
||||
*/
|
||||
[[nodiscard]] CardInfoPtr getCardPtr() const
|
||||
{
|
||||
return card;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns the printing information associated with this card.
|
||||
*
|
||||
* May be empty if no specific printing was assigned.
|
||||
*/
|
||||
[[nodiscard]] PrintingInfo getPrinting() const
|
||||
{
|
||||
return printing;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Compares both card pointer and printing for equality.
|
||||
*
|
||||
* Two ExactCard objects are equal only if both their CardInfoPtr and
|
||||
* PrintingInfo values are equal.
|
||||
*/
|
||||
bool operator==(const ExactCard &other) const;
|
||||
|
||||
/**
|
||||
* @brief Convenience helper to get the card's display name.
|
||||
*
|
||||
* @return The card's name, or an empty string if the CardInfoPtr is null.
|
||||
*/
|
||||
[[nodiscard]] QString getName() const;
|
||||
|
||||
/**
|
||||
* @brief Returns a reference to the underlying CardInfo object.
|
||||
*
|
||||
* If the CardInfoPtr is null, returns a reference to a static empty CardInfo
|
||||
* instance instead. This avoids null-dereferencing but means modifications
|
||||
* to the returned object do not affect the ExactCard.
|
||||
*
|
||||
* @return A const reference to the CardInfo object.
|
||||
*/
|
||||
[[nodiscard]] const CardInfo &getInfo() const;
|
||||
|
||||
/**
|
||||
* @brief Generates a stable cache key for pixmap caching.
|
||||
*
|
||||
* The key includes the card's name and (if present) the printing UUID,
|
||||
* allowing different printings of the same card to map to different cache entries.
|
||||
*/
|
||||
[[nodiscard]] QString getPixmapCacheKey() const;
|
||||
|
||||
/**
|
||||
* @brief Indicates whether this ExactCard represents no valid card.
|
||||
*
|
||||
* An ExactCard is considered empty if the CardInfoPtr is null or the
|
||||
* card's name is empty.
|
||||
*/
|
||||
[[nodiscard]] bool isEmpty() const;
|
||||
|
||||
/**
|
||||
* @brief Boolean conversion indicating whether the card is valid (non-empty).
|
||||
*
|
||||
* @return true if not empty, false otherwise.
|
||||
*/
|
||||
explicit operator bool() const;
|
||||
|
||||
/**
|
||||
* @brief Emits the pixmapUpdated signal on the underlying CardInfo.
|
||||
*
|
||||
* Assumes CardInfoPtr is non-null. If called on an empty ExactCard,
|
||||
* the behavior is undefined.
|
||||
*/
|
||||
void emitPixmapUpdated() const;
|
||||
};
|
||||
Q_DECLARE_METATYPE(ExactCard)
|
||||
|
||||
#endif // EXACT_CARD_H
|
||||
@@ -0,0 +1,20 @@
|
||||
#include "printing_info.h"
|
||||
|
||||
#include "../set/card_set.h"
|
||||
|
||||
PrintingInfo::PrintingInfo(const CardSetPtr &_set) : set(_set)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the uuid property of the printing, or an empty string if the property isn't present
|
||||
*/
|
||||
QString PrintingInfo::getUuid() const
|
||||
{
|
||||
return properties.value("uuid").toString();
|
||||
}
|
||||
|
||||
QString PrintingInfo::getFlavorName() const
|
||||
{
|
||||
return properties.value("flavorName").toString();
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
#ifndef COCKATRICE_PRINTING_INFO_H
|
||||
#define COCKATRICE_PRINTING_INFO_H
|
||||
|
||||
#include "../set/card_set.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QVariant>
|
||||
|
||||
class PrintingInfo;
|
||||
|
||||
using SetToPrintingsMap = QMap<QString, QList<PrintingInfo>>;
|
||||
|
||||
/**
|
||||
* @class PrintingInfo
|
||||
* @ingroup CardPrintings
|
||||
*
|
||||
* @brief Represents metadata for a specific variation of a card within a set.
|
||||
*
|
||||
* A card can have multiple variations across sets. PrintingInfo associates
|
||||
* a card with one such variation, and provides per-printing attributes
|
||||
* such as identifiers or additional properties.
|
||||
*
|
||||
* Equality is defined as both the set and the property values being equal.
|
||||
*/
|
||||
class PrintingInfo
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Constructs a PrintingInfo associated with a specific set.
|
||||
*
|
||||
* @param _set The set this printing belongs to (defaults to null).
|
||||
*/
|
||||
explicit PrintingInfo(const CardSetPtr &_set = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Destroys the PrintingInfo.
|
||||
*
|
||||
* Defaulted since no special cleanup is required.
|
||||
*/
|
||||
~PrintingInfo() = default;
|
||||
|
||||
/**
|
||||
* @brief Equality operator.
|
||||
*
|
||||
* Two PrintingInfo objects are equal if they refer to the same set
|
||||
* and contain the exact same property key/value pairs.
|
||||
*
|
||||
* @param other Another PrintingInfo to compare against.
|
||||
* @return True if both set and properties are equal, otherwise false.
|
||||
*/
|
||||
bool operator==(const PrintingInfo &other) const
|
||||
{
|
||||
return this->set == other.set && this->properties == other.properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief check if the info is empty, as if default constructed.
|
||||
*
|
||||
* @return True if both set and properties are empty, otherwise false.
|
||||
*/
|
||||
bool isEmpty() const
|
||||
{
|
||||
return set == nullptr && properties.isEmpty();
|
||||
}
|
||||
|
||||
private:
|
||||
CardSetPtr set; ///< The set this variation belongs to.
|
||||
QVariantHash properties; ///< Key-value store for variation-specific attributes.
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Returns the set this printing belongs to.
|
||||
*
|
||||
* @return Pointer to the associated CardSet.
|
||||
*/
|
||||
[[nodiscard]] CardSetPtr getSet() const
|
||||
{
|
||||
return set;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns the list of property names defined for this printing.
|
||||
*
|
||||
* @return List of keys stored in the properties map.
|
||||
*/
|
||||
[[nodiscard]] QStringList getProperties() const
|
||||
{
|
||||
return properties.keys();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Retrieves the value of a specific property.
|
||||
*
|
||||
* @param propertyName The key name of the property to query.
|
||||
* @return The property value as a string, or an empty string if not set.
|
||||
*/
|
||||
[[nodiscard]] QString getProperty(const QString &propertyName) const
|
||||
{
|
||||
return properties.value(propertyName).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sets or updates the value of a specific property.
|
||||
*
|
||||
* If the property already exists, its value is replaced.
|
||||
*
|
||||
* @param _name The name of the property.
|
||||
* @param _value The string value to assign.
|
||||
*/
|
||||
void setProperty(const QString &_name, const QString &_value)
|
||||
{
|
||||
properties.insert(_name, _value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns the providerID for this printing.
|
||||
*
|
||||
* @return A string representing the providerID.
|
||||
*/
|
||||
[[nodiscard]] QString getUuid() const;
|
||||
|
||||
/**
|
||||
* @brief Returns the flavorName for this printing.
|
||||
*
|
||||
* @return The flavorName, or empty if it isn't present.
|
||||
*/
|
||||
[[nodiscard]] QString getFlavorName() const;
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_PRINTING_INFO_H
|
||||
@@ -0,0 +1,16 @@
|
||||
#include "card_relation.h"
|
||||
|
||||
#include "card_relation_type.h"
|
||||
|
||||
CardRelation::CardRelation(const QString &_name,
|
||||
CardRelationType _attachType,
|
||||
bool _isCreateAllExclusion,
|
||||
bool _isVariableCount,
|
||||
int _defaultCount,
|
||||
bool _isPersistent,
|
||||
bool _isFaceDown)
|
||||
: name(_name), attachType(_attachType), isCreateAllExclusion(_isCreateAllExclusion),
|
||||
isVariableCount(_isVariableCount), defaultCount(_defaultCount), isPersistent(_isPersistent),
|
||||
isFaceDown(_isFaceDown)
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
#ifndef COCKATRICE_CARD_RELATION_H
|
||||
#define COCKATRICE_CARD_RELATION_H
|
||||
|
||||
#include "card_relation_type.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
/**
|
||||
* @class CardRelation
|
||||
* @ingroup Cards
|
||||
*
|
||||
* @brief Represents a relationship between two cards.
|
||||
*
|
||||
* CardRelation objects define directional relationships, such as:
|
||||
* - One card attaching to another.
|
||||
* - One card transforming into another.
|
||||
* - One card creating another instance.
|
||||
*
|
||||
* Relations may also define metadata such as whether multiple creations
|
||||
* are possible, whether the relation is persistent, and default counts.
|
||||
*/
|
||||
class CardRelation : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
QString name; ///< Name of the related card.
|
||||
CardRelationType attachType; ///< Type of attachment.
|
||||
bool isCreateAllExclusion; ///< True if this relation should exclude multiple creations in "create all" operations.
|
||||
bool isVariableCount; ///< True if the number of creations is variable.
|
||||
int defaultCount; ///< Default number of cards created or involved.
|
||||
bool isPersistent; ///< True if this relation persists (i.e. is not destroyed) on zone change.
|
||||
bool isFaceDown; ///< True if this relation creates the tokens facedown
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Constructs a CardRelation with optional parameters.
|
||||
*
|
||||
* @param _name Name of the related card.
|
||||
* @param _attachType Type of attachment.
|
||||
* @param _isCreateAllExclusion Whether this relation excludes mass creation.
|
||||
* @param _isVariableCount Whether the count is variable.
|
||||
* @param _defaultCount Default number for creations or transformations.
|
||||
* @param _isPersistent Whether the relation persists across zone changes.
|
||||
* @param _isFaceDown Whether the relation creates the token face down
|
||||
*/
|
||||
explicit CardRelation(const QString &_name = QString(),
|
||||
CardRelationType _attachType = CardRelationType::DoesNotAttach,
|
||||
bool _isCreateAllExclusion = false,
|
||||
bool _isVariableCount = false,
|
||||
int _defaultCount = 1,
|
||||
bool _isPersistent = false,
|
||||
bool _isFaceDown = false);
|
||||
|
||||
/**
|
||||
* @brief Returns the name of the related card.
|
||||
*
|
||||
* @return Name as QString reference.
|
||||
*/
|
||||
[[nodiscard]] inline const QString &getName() const
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns the type of attachment.
|
||||
*
|
||||
* @return Enum value representing the attachment type.
|
||||
*/
|
||||
[[nodiscard]] CardRelationType getAttachType() const
|
||||
{
|
||||
return attachType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns true if the card is attached to another.
|
||||
*
|
||||
* @return True if attached, false otherwise.
|
||||
*/
|
||||
[[nodiscard]] bool getDoesAttach() const
|
||||
{
|
||||
return attachType != CardRelationType::DoesNotAttach;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns true if this card transforms into another card.
|
||||
*
|
||||
* @return True if it transforms, false otherwise.
|
||||
*/
|
||||
[[nodiscard]] bool getDoesTransform() const
|
||||
{
|
||||
return attachType == CardRelationType::TransformInto;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns a string description of the attachment type.
|
||||
*
|
||||
* @return "attach" for AttachTo, "transform" for TransformInto, empty string otherwise.
|
||||
*/
|
||||
[[nodiscard]] QString getAttachTypeAsString() const
|
||||
{
|
||||
return cardAttachTypeToString(attachType);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Determines whether another instance can be created.
|
||||
*
|
||||
* @return True if creation is allowed, false if constrained by attachment.
|
||||
*/
|
||||
[[nodiscard]] bool getCanCreateAnother() const
|
||||
{
|
||||
return !getDoesAttach();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns whether this relation is excluded from "create all" operations.
|
||||
*
|
||||
* @return True if excluded, false otherwise.
|
||||
*/
|
||||
[[nodiscard]] bool getIsCreateAllExclusion() const
|
||||
{
|
||||
return isCreateAllExclusion;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns whether the relation count is variable.
|
||||
*
|
||||
* @return True if variable, false otherwise.
|
||||
*/
|
||||
[[nodiscard]] bool getIsVariable() const
|
||||
{
|
||||
return isVariableCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns the default count of related cards.
|
||||
*
|
||||
* @return Integer representing default number.
|
||||
*/
|
||||
[[nodiscard]] int getDefaultCount() const
|
||||
{
|
||||
return defaultCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns whether the relation is persistent.
|
||||
*
|
||||
* Persistent relations are not destroyed on zone changes.
|
||||
*
|
||||
* @return True if persistent, false otherwise.
|
||||
*/
|
||||
[[nodiscard]] bool getIsPersistent() const
|
||||
{
|
||||
return isPersistent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns whether the relation creates the token facedown.
|
||||
*
|
||||
* @return True if facedown, false otherwise.
|
||||
*/
|
||||
[[nodiscard]] bool getIsFaceDown() const
|
||||
{
|
||||
return isFaceDown;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_CARD_RELATION_H
|
||||
@@ -0,0 +1,35 @@
|
||||
#ifndef COCKATRICE_CARD_RELATION_TYPE_H
|
||||
#define COCKATRICE_CARD_RELATION_TYPE_H
|
||||
|
||||
#include <QString>
|
||||
|
||||
/**
|
||||
* @enum CardRelationType
|
||||
* @ingroup Cards
|
||||
* @brief Types of attachments between cards.
|
||||
*
|
||||
* DoesNotAttach: No attachment is present.
|
||||
* AttachTo: This card attaches to another card.
|
||||
* TransformInto: This card transforms into another card.
|
||||
*/
|
||||
enum class CardRelationType
|
||||
{
|
||||
DoesNotAttach = 0,
|
||||
AttachTo = 1,
|
||||
TransformInto = 2,
|
||||
};
|
||||
|
||||
// Helper function to transform the enum values into human-readable strings
|
||||
inline QString cardAttachTypeToString(CardRelationType type)
|
||||
{
|
||||
switch (type) {
|
||||
case CardRelationType::AttachTo:
|
||||
return "attach";
|
||||
case CardRelationType::TransformInto:
|
||||
return "transform";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
#endif // COCKATRICE_CARD_RELATION_TYPE_H
|
||||
@@ -0,0 +1,66 @@
|
||||
#include "card_set.h"
|
||||
|
||||
#include <QSet>
|
||||
#include <utility>
|
||||
|
||||
const char *CardSet::TOKENS_SETNAME = "TK";
|
||||
|
||||
CardSet::CardSet(ICardSetPriorityController *_priorityController,
|
||||
const QString &_shortName,
|
||||
const QString &_longName,
|
||||
const QString &_setType,
|
||||
const QDate &_releaseDate,
|
||||
const CardSet::Priority _priority)
|
||||
: priorityController(std::move(_priorityController)), shortName(_shortName), longName(_longName),
|
||||
releaseDate(_releaseDate), setType(_setType), priority(_priority)
|
||||
{
|
||||
loadSetOptions();
|
||||
}
|
||||
|
||||
CardSetPtr CardSet::newInstance(ICardSetPriorityController *_priorityController,
|
||||
const QString &_shortName,
|
||||
const QString &_longName,
|
||||
const QString &_setType,
|
||||
const QDate &_releaseDate,
|
||||
const Priority _priority)
|
||||
{
|
||||
CardSetPtr ptr(new CardSet(_priorityController, _shortName, _longName, _setType, _releaseDate, _priority));
|
||||
// ptr->setSmartPointer(ptr);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
QString CardSet::getCorrectedShortName() const
|
||||
{
|
||||
// For Windows machines.
|
||||
QSet<QString> invalidFileNames;
|
||||
invalidFileNames << "CON" << "PRN" << "AUX" << "NUL" << "COM1" << "COM2" << "COM3" << "COM4" << "COM5" << "COM6"
|
||||
<< "COM7" << "COM8" << "COM9" << "LPT1" << "LPT2" << "LPT3" << "LPT4" << "LPT5" << "LPT6" << "LPT7"
|
||||
<< "LPT8" << "LPT9";
|
||||
|
||||
return invalidFileNames.contains(shortName) ? shortName + "_" : shortName;
|
||||
}
|
||||
|
||||
void CardSet::loadSetOptions()
|
||||
{
|
||||
sortKey = priorityController->getSortKey(shortName);
|
||||
enabled = priorityController->isEnabled(shortName);
|
||||
isknown = priorityController->isKnown(shortName);
|
||||
}
|
||||
|
||||
void CardSet::setSortKey(unsigned int _sortKey)
|
||||
{
|
||||
sortKey = _sortKey;
|
||||
priorityController->setSortKey(shortName, _sortKey);
|
||||
}
|
||||
|
||||
void CardSet::setEnabled(bool _enabled)
|
||||
{
|
||||
enabled = _enabled;
|
||||
priorityController->setEnabled(shortName, _enabled);
|
||||
}
|
||||
|
||||
void CardSet::setIsKnown(bool _isknown)
|
||||
{
|
||||
isknown = _isknown;
|
||||
priorityController->setIsKnown(shortName, _isknown);
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
#ifndef COCKATRICE_CARD_SET_H
|
||||
#define COCKATRICE_CARD_SET_H
|
||||
|
||||
#include <QDate>
|
||||
#include <QList>
|
||||
#include <QSharedPointer>
|
||||
#include <QString>
|
||||
#include <libcockatrice/interfaces/interface_card_set_priority_controller.h>
|
||||
|
||||
class CardInfo;
|
||||
using CardInfoPtr = QSharedPointer<CardInfo>;
|
||||
|
||||
class CardSet;
|
||||
using CardSetPtr = QSharedPointer<CardSet>;
|
||||
|
||||
/**
|
||||
* @class CardSet
|
||||
* @ingroup CardSets
|
||||
*
|
||||
* @brief A collection of cards grouped under a common identifier.
|
||||
*
|
||||
* A set serves both as metadata (identifier, title, category, release date, and priority)
|
||||
* and as a container of all cards that belong to it. Each set can be enabled/disabled
|
||||
* and marked as known/unknown depending on context.
|
||||
*
|
||||
* The class inherits from `QList<CardInfoPtr>`, so it can be iterated over directly
|
||||
* to access its contents.
|
||||
*
|
||||
* Typical usage:
|
||||
* - Query metadata such as identifier, category, or release date.
|
||||
* - Enable or disable sets according to user preference.
|
||||
* - Store and retrieve CardInfo objects associated with the set.
|
||||
*/
|
||||
class CardSet : public QList<CardInfoPtr>
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @enum Priority
|
||||
* @brief Defines relative ordering and importance of sets.
|
||||
*/
|
||||
enum Priority
|
||||
{
|
||||
PriorityFallback = 0, ///< Used when no other priority is defined.
|
||||
PriorityPrimary = 10, ///< Primary, canonical set.
|
||||
PrioritySecondary = 20, ///< Secondary but relevant.
|
||||
PriorityReprint = 30, ///< Duplicate or reprint category.
|
||||
PriorityOther = 40, ///< Miscellaneous grouping.
|
||||
PriorityLowest = 100, ///< Lowest sorting priority.
|
||||
};
|
||||
|
||||
static const char *TOKENS_SETNAME; ///< Reserved identifier for token-like sets.
|
||||
|
||||
private:
|
||||
ICardSetPriorityController *priorityController; ///< Interface to the card set priority controller.
|
||||
QString shortName; ///< Short identifier for the set.
|
||||
QString longName; ///< Full name for the set.
|
||||
unsigned int sortKey; ///< Custom numeric sort key.
|
||||
QDate releaseDate; ///< Release date, may be empty if unknown.
|
||||
QString setType; ///< Type/category label for the set.
|
||||
Priority priority; ///< Priority level for sorting and relevance.
|
||||
bool enabled; ///< Whether the set is active/enabled.
|
||||
bool isknown; ///< Whether the set is considered known.
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Constructs a CardSet.
|
||||
*
|
||||
* @param priorityController Interface to a card set priority controller.
|
||||
* @param _shortName Identifier string.
|
||||
* @param _longName Full descriptive name.
|
||||
* @param _setType Type/category string.
|
||||
* @param _releaseDate Release date (optional).
|
||||
* @param _priority Sorting/priority level.
|
||||
*/
|
||||
explicit CardSet(ICardSetPriorityController *priorityController,
|
||||
const QString &_shortName = QString(),
|
||||
const QString &_longName = QString(),
|
||||
const QString &_setType = QString(),
|
||||
const QDate &_releaseDate = QDate(),
|
||||
const Priority _priority = PriorityFallback);
|
||||
|
||||
/**
|
||||
* @brief Creates and returns a new shared CardSet instance.
|
||||
*
|
||||
* @param priorityController Interface to a card set priority controller.
|
||||
* @param _shortName Identifier string.
|
||||
* @param _longName Full descriptive name.
|
||||
* @param _setType Type/category string.
|
||||
* @param _releaseDate Release date (optional).
|
||||
* @param _priority Sorting/priority level.
|
||||
* @return A shared pointer to the new CardSet.
|
||||
*/
|
||||
static CardSetPtr newInstance(ICardSetPriorityController *priorityController,
|
||||
const QString &_shortName = QString(),
|
||||
const QString &_longName = QString(),
|
||||
const QString &_setType = QString(),
|
||||
const QDate &_releaseDate = QDate(),
|
||||
const Priority _priority = PriorityFallback);
|
||||
|
||||
/**
|
||||
* @brief Returns a safe, sanitized version of the short name.
|
||||
*
|
||||
* Intended for file paths or identifiers where only certain
|
||||
* characters are allowed.
|
||||
*
|
||||
* @return Sanitized short name.
|
||||
*/
|
||||
[[nodiscard]] QString getCorrectedShortName() const;
|
||||
|
||||
/** @return Short identifier of the set. */
|
||||
[[nodiscard]] QString getShortName() const
|
||||
{
|
||||
return shortName;
|
||||
}
|
||||
|
||||
/** @return Descriptive name of the set. */
|
||||
[[nodiscard]] QString getLongName() const
|
||||
{
|
||||
return longName;
|
||||
}
|
||||
|
||||
/** @return Type/category string of the set. */
|
||||
[[nodiscard]] QString getSetType() const
|
||||
{
|
||||
return setType;
|
||||
}
|
||||
|
||||
/** @return Release date of the set. */
|
||||
[[nodiscard]] QDate getReleaseDate() const
|
||||
{
|
||||
return releaseDate;
|
||||
}
|
||||
|
||||
/** @return Priority level of the set. */
|
||||
[[nodiscard]] Priority getPriority() const
|
||||
{
|
||||
return priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sets the full name of the set.
|
||||
* @param _longName New full name.
|
||||
*/
|
||||
void setLongName(const QString &_longName)
|
||||
{
|
||||
longName = _longName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sets the category/type of the set.
|
||||
* @param _setType New category string.
|
||||
*/
|
||||
void setSetType(const QString &_setType)
|
||||
{
|
||||
setType = _setType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Sets the release date of the set.
|
||||
* @param _releaseDate New release date.
|
||||
*/
|
||||
void setReleaseDate(const QDate &_releaseDate)
|
||||
{
|
||||
releaseDate = _releaseDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Updates the priority of the set.
|
||||
* @param _priority New priority value.
|
||||
*/
|
||||
|
||||
void setPriority(const Priority _priority)
|
||||
{
|
||||
priority = _priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Loads state values (enabled, known, sort key) from configuration.
|
||||
*
|
||||
* Reads external configuration and applies it to this set.
|
||||
*/
|
||||
void loadSetOptions();
|
||||
|
||||
void setSortKeyInMemory(unsigned int _sortKey)
|
||||
{
|
||||
sortKey = _sortKey;
|
||||
}
|
||||
void setEnabledInMemory(bool _enabled)
|
||||
{
|
||||
enabled = _enabled;
|
||||
}
|
||||
|
||||
/** @return The sort key assigned to this set. */
|
||||
[[nodiscard]] int getSortKey() const
|
||||
{
|
||||
return sortKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Assigns a new sort key to this set.
|
||||
* @param _sortKey The numeric key to use for sorting.
|
||||
*/
|
||||
void setSortKey(unsigned int _sortKey);
|
||||
|
||||
/** @return True if the set is enabled. */
|
||||
[[nodiscard]] bool getEnabled() const
|
||||
{
|
||||
return enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Enables or disables the set.
|
||||
* @param _enabled True to enable, false to disable.
|
||||
*/
|
||||
void setEnabled(bool _enabled);
|
||||
|
||||
/** @return True if the set is considered known. */
|
||||
[[nodiscard]] bool getIsKnown() const
|
||||
{
|
||||
return isknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Marks the set as known or unknown.
|
||||
* @param _isknown True if known, false if unknown.
|
||||
*/
|
||||
void setIsKnown(bool _isknown);
|
||||
|
||||
/**
|
||||
* @brief Determines whether the set has incomplete metadata and should be ignored.
|
||||
*
|
||||
* @return True if the long name, type, and release date are all empty.
|
||||
*/
|
||||
[[nodiscard]] bool getIsKnownIgnored() const
|
||||
{
|
||||
return longName.length() + setType.length() + releaseDate.toString().length() == 0;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_CARD_SET_H
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* @file card_set_comparator.h
|
||||
* @ingroup CardSets
|
||||
*/
|
||||
//! \todo Document this file.
|
||||
|
||||
#ifndef SET_PRIORITY_COMPARATOR_H
|
||||
#define SET_PRIORITY_COMPARATOR_H
|
||||
|
||||
#include "../card_info.h"
|
||||
|
||||
class SetPriorityComparator
|
||||
{
|
||||
public:
|
||||
/*
|
||||
* Returns true if a has higher download priority than b
|
||||
* Enabled sets have priority over disabled sets
|
||||
* Both groups follow the user-defined order
|
||||
*/
|
||||
inline bool operator()(const CardSetPtr &a, const CardSetPtr &b) const
|
||||
{
|
||||
if (a->getEnabled()) {
|
||||
return !b->getEnabled() || a->getSortKey() < b->getSortKey();
|
||||
} else {
|
||||
return !b->getEnabled() && a->getSortKey() < b->getSortKey();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class SetReleaseDateComparator
|
||||
{
|
||||
public:
|
||||
/*
|
||||
* Returns true if a has higher download priority than b
|
||||
* Enabled sets have priority over disabled sets
|
||||
* Both groups follow the user-defined order
|
||||
*/
|
||||
inline bool operator()(const CardSetPtr &a, const CardSetPtr &b) const
|
||||
{
|
||||
if (a->getEnabled()) {
|
||||
return !b->getEnabled() || a->getReleaseDate() < b->getReleaseDate();
|
||||
} else {
|
||||
return !b->getEnabled() && a->getReleaseDate() < b->getReleaseDate();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class CardSetPriorityComparator
|
||||
{
|
||||
public:
|
||||
/*
|
||||
* Returns true if a has higher download priority than b
|
||||
* Enabled sets have priority over disabled sets
|
||||
* Both groups follow the user-defined order
|
||||
*/
|
||||
inline bool operator()(const PrintingInfo &a, const PrintingInfo &b) const
|
||||
{
|
||||
if (a.getSet()->getEnabled()) {
|
||||
return !b.getSet()->getEnabled() || a.getSet()->getSortKey() < b.getSet()->getSortKey();
|
||||
} else {
|
||||
return !b.getSet()->getEnabled() && a.getSet()->getSortKey() < b.getSet()->getSortKey();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#endif // SET_PRIORITY_COMPARATOR_H
|
||||
@@ -0,0 +1,127 @@
|
||||
#include "card_set_list.h"
|
||||
|
||||
class CardSetList::KeyCompareFunctor
|
||||
{
|
||||
public:
|
||||
inline bool operator()(const CardSetPtr &a, const CardSetPtr &b) const
|
||||
{
|
||||
if (a.isNull() || b.isNull()) {
|
||||
// qCWarning(CardInfoLog) << "SetList::KeyCompareFunctor a or b is null";
|
||||
return false;
|
||||
}
|
||||
|
||||
return a->getSortKey() < b->getSortKey();
|
||||
}
|
||||
};
|
||||
|
||||
void CardSetList::sortByKey()
|
||||
{
|
||||
std::sort(begin(), end(), KeyCompareFunctor());
|
||||
}
|
||||
|
||||
int CardSetList::getEnabledSetsNum()
|
||||
{
|
||||
int num = 0;
|
||||
for (int i = 0; i < size(); ++i) {
|
||||
CardSetPtr set = at(i);
|
||||
if (set && set->getEnabled()) {
|
||||
++num;
|
||||
}
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
int CardSetList::getUnknownSetsNum()
|
||||
{
|
||||
int num = 0;
|
||||
for (int i = 0; i < size(); ++i) {
|
||||
CardSetPtr set = at(i);
|
||||
if (set && !set->getIsKnown() && !set->getIsKnownIgnored()) {
|
||||
++num;
|
||||
}
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
QStringList CardSetList::getUnknownSetsNames()
|
||||
{
|
||||
QStringList sets = QStringList();
|
||||
for (int i = 0; i < size(); ++i) {
|
||||
CardSetPtr set = at(i);
|
||||
if (set && !set->getIsKnown() && !set->getIsKnownIgnored()) {
|
||||
sets << set->getShortName();
|
||||
}
|
||||
}
|
||||
return sets;
|
||||
}
|
||||
|
||||
void CardSetList::enableAllUnknown()
|
||||
{
|
||||
for (int i = 0; i < size(); ++i) {
|
||||
CardSetPtr set = at(i);
|
||||
if (set && !set->getIsKnown() && !set->getIsKnownIgnored()) {
|
||||
set->setIsKnown(true);
|
||||
set->setEnabled(true);
|
||||
} else if (set && set->getIsKnownIgnored() && !set->getEnabled()) {
|
||||
set->setEnabled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CardSetList::enableAll()
|
||||
{
|
||||
for (int i = 0; i < size(); ++i) {
|
||||
CardSetPtr set = at(i);
|
||||
|
||||
if (set == nullptr) {
|
||||
// qCWarning(CardInfoLog) << "enabledAll has null";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!set->getIsKnownIgnored()) {
|
||||
set->setIsKnown(true);
|
||||
}
|
||||
|
||||
set->setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
void CardSetList::markAllAsKnown()
|
||||
{
|
||||
for (int i = 0; i < size(); ++i) {
|
||||
CardSetPtr set = at(i);
|
||||
if (set && !set->getIsKnown() && !set->getIsKnownIgnored()) {
|
||||
set->setIsKnown(true);
|
||||
set->setEnabled(false);
|
||||
} else if (set && set->getIsKnownIgnored() && !set->getEnabled()) {
|
||||
set->setEnabled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CardSetList::guessSortKeys()
|
||||
{
|
||||
defaultSort();
|
||||
for (int i = 0; i < size(); ++i) {
|
||||
CardSetPtr set = at(i);
|
||||
if (set.isNull()) {
|
||||
// qCWarning(CardInfoLog) << "guessSortKeys set is null";
|
||||
continue;
|
||||
}
|
||||
set->setSortKey(i);
|
||||
}
|
||||
}
|
||||
|
||||
void CardSetList::defaultSort()
|
||||
{
|
||||
std::sort(begin(), end(), [](const CardSetPtr &a, const CardSetPtr &b) {
|
||||
// Sort by priority, then by release date, then by short name
|
||||
if (a->getPriority() != b->getPriority()) {
|
||||
return a->getPriority() < b->getPriority(); // lowest first
|
||||
} else if (a->getReleaseDate() != b->getReleaseDate()) {
|
||||
return a->getReleaseDate() > b->getReleaseDate(); // most recent first
|
||||
} else {
|
||||
return a->getShortName() < b->getShortName(); // alphabetically
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
#ifndef COCKATRICE_CARD_SET_LIST_H
|
||||
#define COCKATRICE_CARD_SET_LIST_H
|
||||
|
||||
#include "card_set.h"
|
||||
|
||||
#include <QList>
|
||||
|
||||
/**
|
||||
* @class CardSetList
|
||||
* @ingroup CardSets
|
||||
*
|
||||
* @brief A list-like container for CardSet objects with extended management methods.
|
||||
*
|
||||
* Extends `QList<CardSetPtr>` by adding convenience operations for sorting,
|
||||
* enabling/disabling sets, and tracking known/unknown status. Unlike a plain
|
||||
* list, this container provides domain-specific functionality for handling
|
||||
* groups of sets in bulk.
|
||||
*/
|
||||
class CardSetList : public QList<CardSetPtr>
|
||||
{
|
||||
private:
|
||||
/**
|
||||
* @class KeyCompareFunctor
|
||||
* @brief Internal comparison functor for sorting by sort key.
|
||||
*
|
||||
* Used internally by `sortByKey()` to order sets consistently
|
||||
* according to their assigned numeric sort keys.
|
||||
*/
|
||||
class KeyCompareFunctor;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Sorts the set list by each set’s assigned sort key.
|
||||
*
|
||||
* Uses KeyCompareFunctor internally. If two sets share the
|
||||
* same sort key, their relative order is unspecified.
|
||||
*/
|
||||
void sortByKey();
|
||||
|
||||
/**
|
||||
* @brief Reassigns sort keys based on the current order.
|
||||
*
|
||||
* Calls defaultSort() and then assigns sequential sort keys
|
||||
* to all sets according to their resulting positions, replacing
|
||||
* any existing sort keys to ensure consistent ordering.
|
||||
*/
|
||||
void guessSortKeys();
|
||||
|
||||
/**
|
||||
* @brief Enables all sets that are unknown or ignored.
|
||||
*
|
||||
* Sets that are not marked as known and not ignored are marked as known
|
||||
* and enabled. Ignored-known sets are also enabled, but remain ignored.
|
||||
*/
|
||||
void enableAllUnknown();
|
||||
|
||||
/**
|
||||
* @brief Enables all sets in the list.
|
||||
*
|
||||
* Equivalent to calling `setEnabled(true)` on each entry.
|
||||
*/
|
||||
void enableAll();
|
||||
|
||||
/**
|
||||
* @brief Marks all sets as known and adjusts their enabled state.
|
||||
*
|
||||
* Unknown, non-ignored sets become known and disabled.
|
||||
* Ignored-known sets are enabled if they were previously disabled.
|
||||
*/
|
||||
void markAllAsKnown();
|
||||
|
||||
/**
|
||||
* @brief Counts the number of sets that are currently enabled.
|
||||
*
|
||||
* @return Integer count of enabled sets.
|
||||
*/
|
||||
int getEnabledSetsNum();
|
||||
|
||||
/**
|
||||
* @brief Counts the number of sets that are currently unknown.
|
||||
*
|
||||
* @return Integer count of unknown sets.
|
||||
*/
|
||||
int getUnknownSetsNum();
|
||||
|
||||
/**
|
||||
* @brief Collects the short names of all sets marked as unknown.
|
||||
*
|
||||
* @return A list of unknown set names.
|
||||
*/
|
||||
QStringList getUnknownSetsNames();
|
||||
|
||||
/**
|
||||
* @brief Sorts the list by default rules.
|
||||
*
|
||||
* Orders sets first by priority (ascending), then by release date
|
||||
* (most recent first), and finally alphabetically by short name.
|
||||
*/
|
||||
void defaultSort();
|
||||
};
|
||||
|
||||
#endif // COCKATRICE_CARD_SET_LIST_H
|
||||
Reference in New Issue
Block a user