Deck format legality checker (#6166)

* Deck legality checker.

Took 51 seconds

Took 1 minute

Took 1 minute

Took 5 minutes

Took 3 minutes

* Adjust format parsing.

Took 8 minutes


Took 3 seconds

* toString() the xmlName

Took 4 minutes

* more toStrings()

Took 5 minutes

* Comments

Took 3 minutes

* Layout

Took 2 minutes

* Layout part 2: Electric boogaloo

Took 59 seconds

* Update cockatrice/src/interface/widgets/visual_database_display/visual_database_display_format_legality_filter_widget.cpp

Co-authored-by: RickyRister <42636155+RickyRister@users.noreply.github.com>

* Move layout.

Took 4 minutes


Took 10 seconds

* Emit deckModified

Took 6 minutes

* Fix qOverloads

Took 4 minutes

* Fix qOverloads

Took 12 seconds

* Consider text and name in a special way.

Took 11 minutes

* Adjust "Any number of" oracle text

Took 5 minutes

* Store allowedCounts by format

Took 15 minutes

Took 6 seconds

* Only restrict vintage.

Took 2 minutes

* Adjust for DBConverter.

Took 6 minutes

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Co-authored-by: RickyRister <42636155+RickyRister@users.noreply.github.com>
This commit is contained in:
BruebachL 2025-12-13 15:17:55 +01:00 committed by GitHub
parent 2e2682aad4
commit ccdda39e78
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 987 additions and 35 deletions

View file

@ -41,6 +41,8 @@ add_library(
libcockatrice/card/relation/card_relation.cpp
libcockatrice/card/set/card_set.cpp
libcockatrice/card/set/card_set_list.cpp
libcockatrice/card/format/format_legality_rules.cpp
libcockatrice/card/format/format_legality_rules.h
)
target_include_directories(

View file

@ -1,6 +1,7 @@
#ifndef CARD_INFO_H
#define CARD_INFO_H
#include "format/format_legality_rules.h"
#include "printing/printing_info.h"
#include <QDate>
@ -22,10 +23,12 @@ 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)

View file

@ -199,3 +199,8 @@ void CardDatabase::notifyEnabledSetsChanged()
// inform the carddatabasemodels that they need to re-check their list of cards
emit cardDatabaseEnabledSetsChanged();
}
void CardDatabase::addFormat(FormatRulesPtr format)
{
formats.insert(format->formatName.toLower(), format);
}

View file

@ -42,6 +42,8 @@ protected:
/// Sets indexed by short name
SetNameMap sets;
FormatRulesNameMap formats;
/// Loader responsible for file discovery and parsing
CardDatabaseLoader *loader;
@ -141,6 +143,8 @@ public slots:
*/
void addSet(CardSetPtr set);
void addFormat(FormatRulesPtr format);
/** @brief Loads card databases from configured paths. */
void loadCardDatabases();

View file

@ -23,6 +23,7 @@ CardDatabaseLoader::CardDatabaseLoader(QObject *parent,
// 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
@ -149,6 +150,6 @@ bool CardDatabaseLoader::saveCustomTokensToFile()
}
}
availableParsers.first()->saveToFile(tmpSets, tmpCards, fileName);
availableParsers.first()->saveToFile(FormatRulesNameMap(), tmpSets, tmpCards, fileName);
return true;
}

View file

@ -341,4 +341,27 @@ QMap<QString, int> CardDatabaseQuerier::getAllSubCardTypesWithCount() const
}
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;
}

View file

@ -214,6 +214,8 @@ public:
* @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.

View file

@ -38,6 +38,7 @@ public:
/**
* @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.
@ -45,7 +46,8 @@ public:
* @param sourceVersion Optional version string of the source.
* @return true if save succeeded.
*/
virtual bool saveToFile(SetNameMap sets,
virtual bool saveToFile(FormatRulesNameMap _formats,
SetNameMap sets,
CardNameMap cards,
const QString &fileName,
const QString &sourceUrl = "unknown",
@ -79,6 +81,8 @@ signals:
/** Emitted when a set is loaded from the database. */
void addSet(CardSetPtr set);
void addFormat(FormatRulesPtr format);
};
Q_DECLARE_INTERFACE(ICardDatabaseParser, "ICardDatabaseParser")

View file

@ -438,12 +438,15 @@ static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfoPtr &in
return xml;
}
bool CockatriceXml3Parser::saveToFile(SetNameMap _sets,
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;

View file

@ -46,7 +46,8 @@ public:
/**
* @brief Save sets and cards back to an XML3 file.
*/
bool saveToFile(SetNameMap _sets,
bool saveToFile(FormatRulesNameMap _formats,
SetNameMap _sets,
CardNameMap cards,
const QString &fileName,
const QString &sourceUrl = "unknown",

View file

@ -6,6 +6,7 @@
#include <QDebug>
#include <QFile>
#include <QXmlStreamReader>
#include <libcockatrice/card/format/format_legality_rules.h>
#include <version_string.h>
#define COCKATRICE_XML4_TAGNAME "cockatrice_carddatabase"
@ -60,7 +61,9 @@ void CockatriceXml4Parser::parseFile(QIODevice &device)
}
auto xmlName = xml.name().toString();
if (xmlName == "sets") {
if (xmlName == "formats") {
loadFormats(xml);
} else if (xmlName == "sets") {
loadSetsFromXml(xml);
} else if (xmlName == "cards") {
loadCardsFromXml(xml);
@ -78,6 +81,116 @@ void CockatriceXml4Parser::parseFile(QIODevice &device)
}
}
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()) {
@ -273,6 +386,59 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml)
}
}
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()) {
@ -399,7 +565,8 @@ static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfoPtr &in
return xml;
}
bool CockatriceXml4Parser::saveToFile(SetNameMap _sets,
bool CockatriceXml4Parser::saveToFile(FormatRulesNameMap _formats,
SetNameMap _sets,
CardNameMap cards,
const QString &fileName,
const QString &sourceUrl,
@ -426,6 +593,14 @@ bool CockatriceXml4Parser::saveToFile(SetNameMap _sets,
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) {

View file

@ -49,7 +49,8 @@ public:
/**
* @brief Save sets and cards back to an XML4 file.
*/
bool saveToFile(SetNameMap _sets,
bool saveToFile(FormatRulesNameMap _formats,
SetNameMap _sets,
CardNameMap cards,
const QString &fileName,
const QString &sourceUrl = "unknown",
@ -72,6 +73,7 @@ private:
*/
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.

View file

@ -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;
}

View file

@ -0,0 +1,73 @@
#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