Honor finite deck copy limits across printings and deck zones [skip ci]

This commit is contained in:
Bryce! 2026-11-01 17:37:05 -05:00
parent e08402ac94
commit 142341bc3c
9 changed files with 438 additions and 48 deletions

View file

@ -11,6 +11,7 @@
#include "libcockatrice/deck_list/tree/abstract_deck_list_node.h"
#include "libcockatrice/deck_list/tree/deck_list_card_node.h"
#include <QHash>
#include <QModelIndexList>
#include <QSet>
#include <QVariant>
@ -320,7 +321,7 @@ void DeckListModel::emitBackgroundUpdates(const QModelIndex &parent)
QModelIndex topLeft = index(0, 0, parent);
QModelIndex bottomRight = index(rows - 1, columnCount() - 1, parent);
emit dataChanged(topLeft, bottomRight, {Qt::BackgroundRole});
emit dataChanged(topLeft, bottomRight, {Qt::BackgroundRole, DeckRoles::IsLegalRole});
for (int r = 0; r < rows; ++r) {
QModelIndex child = index(r, 0, parent);
@ -348,7 +349,6 @@ bool DeckListModel::setData(const QModelIndex &index, const QVariant &value, con
switch (index.column()) {
case DeckListModelColumns::CARD_AMOUNT:
node->setNumber(value.toInt());
refreshCardFormatLegalities();
break;
case DeckListModelColumns::CARD_NAME:
node->setName(value.toString());
@ -366,6 +366,7 @@ bool DeckListModel::setData(const QModelIndex &index, const QVariant &value, con
return false;
}
refreshCardFormatLegalities();
emitRecursiveUpdates(index);
deckList->refreshDeckHash();
emit deckHashChanged();
@ -408,6 +409,7 @@ bool DeckListModel::removeRows(int row, int count, const QModelIndex &parent)
emitRecursiveUpdates(parent);
}
refreshCardFormatLegalities();
deckList->refreshDeckHash();
emit deckHashChanged();
@ -809,7 +811,6 @@ void DeckListModel::setActiveFormat(const QString &_format)
{
deckList->setGameFormat(_format);
refreshCardFormatLegalities();
emitBackgroundUpdates(QModelIndex()); // start from root
}
void DeckListModel::cleanList()
@ -885,17 +886,17 @@ QStringList DeckListModel::getCustomZoneNames(const QString &boardZoneName) cons
return zoneNames;
}
static int maxAllowedForLegality(const FormatRules &format, const QString &legality)
static std::optional<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
return std::nullopt; // Unknown legality is distinct from an unlimited count.
}
static bool isCardQuantityLegalForFormat(const QString &format, const CardInfo &cardInfo, int quantity)
static bool isCardQuantityLegalForFormat(const QString &format, const CardInfo &cardInfo, qint64 quantity)
{
if (format.isEmpty()) {
return true;
@ -908,9 +909,9 @@ static bool isCardQuantityLegalForFormat(const QString &format, const CardInfo &
return cardInfo.isLegalInFormat(format);
}
// Exceptions always win
if (cardHasAnyException(cardInfo, *formatRules)) {
return true;
// Explicit format exceptions retain precedence, including finite limits.
if (const auto limit = cardCopyLimitException(cardInfo, *formatRules)) {
return *limit == -1 || quantity <= *limit;
}
// check legality prop
@ -919,42 +920,37 @@ static bool isCardQuantityLegalForFormat(const QString &format, const CardInfo &
return false;
}
int maxAllowed = maxAllowedForLegality(*formatRules, legality);
if (maxAllowed == -1) {
return false;
}
if (maxAllowed < 0) { // unlimited
return true;
}
return quantity <= maxAllowed;
}
static bool isCardNodeLegalForFormat(const QString &format, const InnerDecklistNode *zone, const DecklistCardNode *card)
{
// Don't check legality for tokens
if (zone->getName() == DECK_ZONE_TOKENS) {
return true;
}
// unknown cards are not legal
ExactCard exactCard = CardDatabaseManager::query()->getCard(card->toCardRef());
if (!exactCard) {
return false;
}
// actual check
return isCardQuantityLegalForFormat(format, exactCard.getInfo(), card->getNumber());
const auto maxAllowed = maxAllowedForLegality(*formatRules, legality);
return maxAllowed && (*maxAllowed == -1 || (*maxAllowed >= 0 && quantity <= *maxAllowed));
}
void DeckListModel::refreshCardFormatLegalities()
{
QString format = deckList->getGameFormat();
deckList->forEachCard([&format](const InnerDecklistNode *zone, DecklistCardNode *card) {
bool legal = isCardNodeLegalForFormat(format, zone, card);
const QString format = deckList->getGameFormat();
QHash<QString, qint64> quantities;
QHash<const DecklistCardNode *, CardInfoPtr> cardInfos;
deckList->forEachCard([&](const InnerDecklistNode *zone, DecklistCardNode *card) {
if (zone->getName() == DECK_ZONE_TOKENS) {
return;
}
const ExactCard exactCard = CardDatabaseManager::query()->getCard(card->toCardRef());
if (exactCard) {
const auto info = exactCard.getCardPtr();
cardInfos.insert(card, info);
// The maybeboard is not part of the submitted deck. Count the other
// non-token zones together, including printings and nested custom zones.
if (zone->getName() != DECK_ZONE_MAYBEBOARD) {
quantities[info->getName()] += std::max(0, card->getNumber());
}
}
});
deckList->forEachCard([&](const InnerDecklistNode *zone, DecklistCardNode *card) {
const auto info = cardInfos.value(card);
const qint64 quantity =
info && zone->getName() != DECK_ZONE_MAYBEBOARD ? quantities.value(info->getName()) : card->getNumber();
const bool legal =
zone->getName() == DECK_ZONE_TOKENS || (info && isCardQuantityLegalForFormat(format, *info, quantity));
card->setFormatLegality(legal);
});
emitBackgroundUpdates(QModelIndex());
}