diff --git a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp index f530186d8..0d46aad0f 100644 --- a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp @@ -109,10 +109,26 @@ static QSharedPointer parseFormat(QXmlStreamReader &xml) rulesPtr->maxDeckSize = text.toInt(); } else if (xmlName == "maxSideboardSize") { rulesPtr->maxSideboardSize = xml.readElementText().toInt(); - } else if (xmlName == "maxCopies") { - rulesPtr->maxCopies = xml.readElementText().toInt(); - } else if (xmlName == "maxRestrictedCopies") { - rulesPtr->maxRestrictedCopies = 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(); @@ -387,8 +403,18 @@ static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const QSharedPointer< 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)); - xml.writeTextElement("maxCopies", QString::number(rules.maxCopies)); - xml.writeTextElement("maxRestrictedCopies", QString::number(rules.maxRestrictedCopies)); + 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"); diff --git a/libcockatrice_card/libcockatrice/card/format/format_legality_rules.h b/libcockatrice_card/libcockatrice/card/format/format_legality_rules.h index 018d3f01b..16f2359ab 100644 --- a/libcockatrice_card/libcockatrice/card/format/format_legality_rules.h +++ b/libcockatrice_card/libcockatrice/card/format/format_legality_rules.h @@ -15,6 +15,12 @@ struct CardCondition 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 conditions; // All must match @@ -28,8 +34,7 @@ struct FormatRules int maxDeckSize = -1; // -1 = unlimited int maxSideboardSize = 15; - int maxCopies = 4; // default constructed rule - int maxRestrictedCopies = 1; // Used for "restricted" legality tags + QList allowedCounts; QList exceptions; // Cards allowed to break maxCopies }; diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp index a066d7497..05ee480a5 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp @@ -622,6 +622,17 @@ bool DeckListModel::isCardLegalForCurrentFormat(const CardInfoPtr cardInfo) return true; } +int maxAllowedForLegality(const FormatRules &format, const QString &legality) +{ + for (const AllowedCount &c : format.allowedCounts) { + if (c.label == legality) { + return c.max; + } + } + return -1; // unknown legality → treat as illegal +} + + bool DeckListModel::isCardQuantityLegalForCurrentFormat(const CardInfoPtr cardInfo, int quantity) { auto formatRules = CardDatabaseManager::query()->getFormat(deckList->getGameFormat()); @@ -630,24 +641,29 @@ bool DeckListModel::isCardQuantityLegalForCurrentFormat(const CardInfoPtr cardIn return true; } - if (cardHasAnyException(*cardInfo.data(), *formatRules.data())) { + // Exceptions always win + if (cardHasAnyException(*cardInfo, *formatRules)) { return true; } - if (cardInfo->getProperties().contains("format-" + deckList->getGameFormat())) { - QString formatLegality = cardInfo->getProperty("format-" + deckList->getGameFormat()); - if (formatLegality == "legal") { - if (quantity <= formatRules->maxCopies) { - return true; - } - } else if (formatLegality == "restricted") { - if (quantity <= formatRules->maxRestrictedCopies) { - return true; - } - } + const QString legalityProp = "format-" + deckList->getGameFormat(); + if (!cardInfo->getProperties().contains(legalityProp)) { + return false; } - return false; + const QString legality = cardInfo->getProperty(legalityProp); + + int maxAllowed = maxAllowedForLegality(*formatRules, legality); + + if (maxAllowed == -1) { + return false; + } + + if (maxAllowed < 0) { // unlimited + return true; + } + + return quantity <= maxAllowed; } void DeckListModel::refreshCardFormatLegalities() diff --git a/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index 6f5bdf142..c320ec983 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -13,6 +13,10 @@ #include #include +static const QList kConstructedCounts = {{4, "legal"}, {1, "restricted"}, {0, "banned"}}; + +static const QList kSingletonCounts = {{1, "legal"}, {1, "restricted"}, {0, "banned"}}; + SplitCardPart::SplitCardPart(const QString &_name, const QString &_text, const QVariantHash &_properties, @@ -486,11 +490,11 @@ FormatRulesNameMap OracleImporter::createDefaultMagicFormats() FormatRulesNameMap defaultFormatRulesNameMap; // ----------------- Helper lambda to create format ----------------- - auto makeFormat = [&](const QString &name, int maxCopies = 4, int minDeck = 60, int maxDeck = -1, - int maxSideboardSize = 15) -> FormatRulesPtr { + auto makeFormat = [&](const QString &name, int minDeck = 60, int maxDeck = -1, int maxSideboardSize = 15, + const QList &allowedCounts = kConstructedCounts) -> FormatRulesPtr { FormatRulesPtr f(new FormatRules); f->formatName = name; - f->maxCopies = maxCopies; + f->allowedCounts = allowedCounts; f->minDeckSize = minDeck; f->maxDeckSize = maxDeck; f->maxSideboardSize = maxSideboardSize; @@ -501,27 +505,27 @@ FormatRulesNameMap OracleImporter::createDefaultMagicFormats() }; // ----------------- Standard formats ----------------- - makeFormat("Alchemy", 4, 60); - makeFormat("Brawl", 1, 60, 60); - makeFormat("Commander", 1, 100, 100); - makeFormat("Duel", 1, 100, 100); - makeFormat("Future"); - makeFormat("Gladiator", 4, 60, 60, 0); - makeFormat("Historic"); - makeFormat("Legacy"); - makeFormat("Modern"); - makeFormat("Oathbreaker", 1, 60, 60); - makeFormat("OldSchool"); - makeFormat("Pauper"); - makeFormat("PauperCommander", 1, 100, 100); - makeFormat("Penny"); - makeFormat("Pioneer"); - makeFormat("Predh", 1, 100, 100); - makeFormat("Premodern"); makeFormat("Standard"); - makeFormat("StandardBrawl", 1, 60, 60); - makeFormat("Timeless"); + makeFormat("Modern"); + makeFormat("Legacy"); makeFormat("Vintage"); + makeFormat("Pioneer"); + makeFormat("Historic"); + makeFormat("Timeless"); + makeFormat("Future"); + makeFormat("OldSchool"); + makeFormat("Premodern"); + makeFormat("Pauper"); + makeFormat("Penny"); + + // ----------------- Singleton formats ----------------- + makeFormat("Commander", 100, 100, 15, kSingletonCounts); + makeFormat("Duel", 100, 100, 15, kSingletonCounts); + makeFormat("Brawl", 60, 60, 15, kSingletonCounts); + makeFormat("StandardBrawl", 60, 60, 15, kSingletonCounts); + makeFormat("Oathbreaker", 60, 60, 15, kSingletonCounts); + makeFormat("PauperCommander", 100, 100, 15, kSingletonCounts); + makeFormat("Predh", 100, 100, 15, kSingletonCounts); return defaultFormatRulesNameMap; }