Deck legality checker.

Took 51 seconds

Took 1 minute

Took 1 minute

Took 5 minutes

Took 3 minutes
This commit is contained in:
Lukas Brübach 2025-09-10 09:40:28 +02:00
parent 2e2682aad4
commit 3f06c848e1
31 changed files with 921 additions and 27 deletions

View file

@ -202,6 +202,7 @@ set(cockatrice_SOURCES
src/interface/widgets/utility/sequence_edit.cpp src/interface/widgets/utility/sequence_edit.cpp
src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp src/interface/widgets/visual_database_display/visual_database_display_color_filter_widget.cpp
src/interface/widgets/visual_database_display/visual_database_display_filter_save_load_widget.cpp src/interface/widgets/visual_database_display/visual_database_display_filter_save_load_widget.cpp
src/interface/widgets/visual_database_display/visual_database_display_format_legality_filter_widget.cpp
src/interface/widgets/visual_database_display/visual_database_display_main_type_filter_widget.cpp src/interface/widgets/visual_database_display/visual_database_display_main_type_filter_widget.cpp
src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.cpp src/interface/widgets/visual_database_display/visual_database_display_name_filter_widget.cpp
src/interface/widgets/visual_database_display/visual_database_display_set_filter_widget.cpp src/interface/widgets/visual_database_display/visual_database_display_set_filter_widget.cpp

View file

@ -124,6 +124,12 @@ void DeckEditorDeckDockWidget::createDeckDock()
quickSettingsWidget->addSettingsWidget(showBannerCardCheckBox); quickSettingsWidget->addSettingsWidget(showBannerCardCheckBox);
quickSettingsWidget->addSettingsWidget(showTagsWidgetCheckBox); quickSettingsWidget->addSettingsWidget(showTagsWidgetCheckBox);
formatLabel = new QLabel(this);
formatComboBox = new QComboBox(this);
formatComboBox->addItem(tr("Loading Database..."));
formatComboBox->setEnabled(false); // Disable until loaded
commentsLabel = new QLabel(); commentsLabel = new QLabel();
commentsLabel->setObjectName("commentsLabel"); commentsLabel->setObjectName("commentsLabel");
commentsEdit = new QTextEdit; commentsEdit = new QTextEdit;
@ -205,16 +211,19 @@ void DeckEditorDeckDockWidget::createDeckDock()
upperLayout->addWidget(nameEdit, 0, 1); upperLayout->addWidget(nameEdit, 0, 1);
upperLayout->addWidget(quickSettingsWidget, 0, 2); upperLayout->addWidget(quickSettingsWidget, 0, 2);
upperLayout->addWidget(commentsLabel, 1, 0); upperLayout->addWidget(formatLabel, 1, 0);
upperLayout->addWidget(commentsEdit, 1, 1); upperLayout->addWidget(formatComboBox, 1, 1);
upperLayout->addWidget(bannerCardLabel, 2, 0); upperLayout->addWidget(commentsLabel, 2, 0);
upperLayout->addWidget(bannerCardComboBox, 2, 1); upperLayout->addWidget(commentsEdit, 2, 1);
upperLayout->addWidget(deckTagsDisplayWidget, 3, 1); upperLayout->addWidget(bannerCardLabel, 3, 0);
upperLayout->addWidget(bannerCardComboBox, 3, 1);
upperLayout->addWidget(activeGroupCriteriaLabel, 4, 0); upperLayout->addWidget(deckTagsDisplayWidget, 4, 1);
upperLayout->addWidget(activeGroupCriteriaComboBox, 4, 1);
upperLayout->addWidget(activeGroupCriteriaLabel, 5, 0);
upperLayout->addWidget(activeGroupCriteriaComboBox, 5, 1);
hashLabel1 = new QLabel(); hashLabel1 = new QLabel();
hashLabel1->setObjectName("hashLabel1"); hashLabel1->setObjectName("hashLabel1");
@ -263,6 +272,44 @@ void DeckEditorDeckDockWidget::createDeckDock()
refreshShortcuts(); refreshShortcuts();
retranslateUi(); retranslateUi();
connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this,
&DeckEditorDeckDockWidget::initializeFormats);
if (CardDatabaseManager::getInstance()->getLoadStatus() == LoadStatus::Ok) {
initializeFormats();
}
}
void DeckEditorDeckDockWidget::initializeFormats()
{
QMap<QString, int> allFormats = CardDatabaseManager::query()->getAllFormatsWithCount();
formatComboBox->clear(); // Remove "Loading Database..."
formatComboBox->setEnabled(true);
// Populate with formats
for (auto it = allFormats.constBegin(); it != allFormats.constEnd(); ++it) {
QString displayText = QString("%1 (%2)").arg(it.key()).arg(it.value());
formatComboBox->addItem(displayText, it.key()); // store the raw key in itemData
}
if (!deckModel->getDeckList()->getGameFormat().isEmpty()) {
deckModel->setActiveFormat(deckModel->getDeckList()->getGameFormat());
formatComboBox->setCurrentIndex(formatComboBox->findData(deckModel->getDeckList()->getGameFormat()));
} else {
// Ensure no selection is visible initially
formatComboBox->setCurrentIndex(-1);
}
connect(formatComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index) {
if (index >= 0) {
QString formatKey = formatComboBox->itemData(index).toString();
deckModel->setActiveFormat(formatKey);
} else {
deckModel->setActiveFormat(QString()); // clear format if deselected
}
});
} }
ExactCard DeckEditorDeckDockWidget::getCurrentCard() ExactCard DeckEditorDeckDockWidget::getCurrentCard()
@ -466,6 +513,12 @@ void DeckEditorDeckDockWidget::syncDisplayWidgetsToModel()
void DeckEditorDeckDockWidget::sortDeckModelToDeckView() void DeckEditorDeckDockWidget::sortDeckModelToDeckView()
{ {
deckModel->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder()); deckModel->sort(deckView->header()->sortIndicatorSection(), deckView->header()->sortIndicatorOrder());
deckModel->setActiveFormat(deckModel->getDeckList()->getGameFormat());
formatComboBox->setCurrentIndex(formatComboBox->findData(deckModel->getDeckList()->getGameFormat()));
deckView->expandAll();
deckView->expandAll();
emit deckChanged();
} }
DeckLoader *DeckEditorDeckDockWidget::getDeckLoader() DeckLoader *DeckEditorDeckDockWidget::getDeckLoader()
@ -724,6 +777,8 @@ void DeckEditorDeckDockWidget::retranslateUi()
showTagsWidgetCheckBox->setText(tr("Show tags selection menu")); showTagsWidgetCheckBox->setText(tr("Show tags selection menu"));
commentsLabel->setText(tr("&Comments:")); commentsLabel->setText(tr("&Comments:"));
activeGroupCriteriaLabel->setText(tr("Group by:")); activeGroupCriteriaLabel->setText(tr("Group by:"));
formatLabel->setText(tr("Format:"));
hashLabel1->setText(tr("Hash:")); hashLabel1->setText(tr("Hash:"));
aIncrement->setText(tr("&Increment number")); aIncrement->setText(tr("&Increment number"));

View file

@ -69,6 +69,7 @@ public slots:
void actSwapCard(); void actSwapCard();
void actRemoveCard(); void actRemoveCard();
void offsetCountAtIndex(const QModelIndex &idx, int offset); void offsetCountAtIndex(const QModelIndex &idx, int offset);
void initializeFormats();
void expandAll(); void expandAll();
signals: signals:
@ -100,6 +101,8 @@ private:
LineEditUnfocusable *hashLabel; LineEditUnfocusable *hashLabel;
QLabel *activeGroupCriteriaLabel; QLabel *activeGroupCriteriaLabel;
QComboBox *activeGroupCriteriaComboBox; QComboBox *activeGroupCriteriaComboBox;
QLabel *formatLabel;
QComboBox *formatComboBox;
QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard; QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard;

View file

@ -23,8 +23,7 @@ QVariant DeckListStyleProxy::data(const QModelIndex &index, int role) const
if (role == Qt::BackgroundRole) { if (role == Qt::BackgroundRole) {
if (isCard) { if (isCard) {
const bool legal = const bool legal = QIdentityProxyModel::data(index, DeckRoles::IsLegalRole).toBool();
true; // TODO: Not implemented yet. QIdentityProxyModel::data(index, DeckRoles::IsLegalRole).toBool();
int base = 255 - (index.row() % 2) * 30; int base = 255 - (index.row() % 2) * 30;
return legal ? QBrush(QColor(base, base, base)) : QBrush(QColor(255, base / 3, base / 3)); return legal ? QBrush(QColor(base, base, base)) : QBrush(QColor(255, base / 3, base / 3));
} else { } else {

View file

@ -0,0 +1,205 @@
#include "visual_database_display_format_legality_filter_widget.h"
#include "../../../filters/filter_tree_model.h"
#include <QPushButton>
#include <QSpinBox>
#include <QTimer>
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/filters/filter_tree.h>
VisualDatabaseDisplayFormatLegalityFilterWidget::VisualDatabaseDisplayFormatLegalityFilterWidget(
QWidget *parent,
FilterTreeModel *_filterModel)
: QWidget(parent), filterModel(_filterModel)
{
allFormatsWithCount = CardDatabaseManager::query()->getAllFormatsWithCount();
setMaximumHeight(75);
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum);
layout = new QHBoxLayout(this);
setLayout(layout);
layout->setContentsMargins(0, 1, 0, 1);
layout->setSpacing(1);
layout->setAlignment(Qt::AlignTop);
flowWidget = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAsNeeded);
layout->addWidget(flowWidget);
// Create the spinbox
spinBox = new QSpinBox(this);
spinBox->setMinimum(1);
spinBox->setMaximum(getMaxMainTypeCount()); // Set the max value dynamically
spinBox->setValue(150);
layout->addWidget(spinBox);
connect(spinBox, QOverload<int>::of(&QSpinBox::valueChanged), this,
&VisualDatabaseDisplayFormatLegalityFilterWidget::updateFormatButtonsVisibility);
// Create the toggle button for Exact Match/Includes mode
toggleButton = new QPushButton(this);
toggleButton->setCheckable(true);
layout->addWidget(toggleButton);
connect(toggleButton, &QPushButton::toggled, this,
&VisualDatabaseDisplayFormatLegalityFilterWidget::updateFilterMode);
connect(filterModel, &FilterTreeModel::layoutChanged, this, [this]() {
QTimer::singleShot(100, this, &VisualDatabaseDisplayFormatLegalityFilterWidget::syncWithFilterModel);
});
createFormatButtons(); // Populate buttons initially
updateFilterMode(false); // Initialize toggle button text
retranslateUi();
}
void VisualDatabaseDisplayFormatLegalityFilterWidget::retranslateUi()
{
spinBox->setToolTip(tr("Do not display formats with less than this amount of cards in the database"));
toggleButton->setToolTip(tr("Filter mode (AND/OR/NOT conjunctions of filters)"));
}
void VisualDatabaseDisplayFormatLegalityFilterWidget::createFormatButtons()
{
// Iterate through main types and create buttons
for (auto it = allFormatsWithCount.begin(); it != allFormatsWithCount.end(); ++it) {
auto *button = new QPushButton(it.key(), flowWidget);
button->setCheckable(true);
button->setStyleSheet("QPushButton { background-color: lightgray; border: 1px solid gray; padding: 5px; }"
"QPushButton:checked { background-color: green; color: white; }");
flowWidget->addWidget(button);
formatButtons[it.key()] = button;
// Connect toggle signal
connect(button, &QPushButton::toggled, this,
[this, mainType = it.key()](bool checked) { handleFormatToggled(mainType, checked); });
}
updateFormatButtonsVisibility(); // Ensure visibility is updated initially
}
void VisualDatabaseDisplayFormatLegalityFilterWidget::updateFormatButtonsVisibility()
{
int threshold = spinBox->value(); // Get the current spinbox value
// Iterate through buttons and hide/disable those below the threshold
for (auto it = formatButtons.begin(); it != formatButtons.end(); ++it) {
bool visible = allFormatsWithCount[it.key()] >= threshold;
it.value()->setVisible(visible);
it.value()->setEnabled(visible);
}
}
int VisualDatabaseDisplayFormatLegalityFilterWidget::getMaxMainTypeCount() const
{
int maxCount = 1;
for (auto it = allFormatsWithCount.begin(); it != allFormatsWithCount.end(); ++it) {
maxCount = qMax(maxCount, it.value());
}
return maxCount;
}
void VisualDatabaseDisplayFormatLegalityFilterWidget::handleFormatToggled(const QString &format, bool active)
{
activeFormats[format] = active;
if (formatButtons.contains(format)) {
formatButtons[format]->setChecked(active);
}
updateFormatFilter();
}
void VisualDatabaseDisplayFormatLegalityFilterWidget::updateFormatFilter()
{
// Clear existing filters related to main type
filterModel->blockSignals(true);
filterModel->filterTree()->blockSignals(true);
filterModel->clearFiltersOfType(CardFilter::Attr::AttrFormat);
if (exactMatchMode) {
// Exact Match: Only selected main types are allowed
QSet<QString> selectedTypes;
for (const auto &type : activeFormats.keys()) {
if (activeFormats[type]) {
selectedTypes.insert(type);
}
}
if (!selectedTypes.isEmpty()) {
// Require all selected types (TypeAnd)
for (const auto &type : selectedTypes) {
QString typeString = type;
filterModel->addFilter(
new CardFilter(typeString, CardFilter::Type::TypeAnd, CardFilter::Attr::AttrFormat));
}
// Exclude any other types (TypeAndNot)
for (const auto &type : formatButtons.keys()) {
if (!selectedTypes.contains(type)) {
QString typeString = type;
filterModel->addFilter(
new CardFilter(typeString, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrFormat));
}
}
}
} else {
// Default Includes Mode (TypeOr) - match any selected main types
for (const auto &type : activeFormats.keys()) {
if (activeFormats[type]) {
QString typeString = type;
filterModel->addFilter(
new CardFilter(typeString, CardFilter::Type::TypeAnd, CardFilter::Attr::AttrFormat));
}
}
}
filterModel->blockSignals(false);
filterModel->filterTree()->blockSignals(false);
emit filterModel->filterTree()->changed();
emit filterModel->layoutChanged();
}
void VisualDatabaseDisplayFormatLegalityFilterWidget::updateFilterMode(bool checked)
{
exactMatchMode = checked;
toggleButton->setText(exactMatchMode ? tr("Mode: Exact Match") : tr("Mode: Includes"));
updateFormatFilter();
}
void VisualDatabaseDisplayFormatLegalityFilterWidget::syncWithFilterModel()
{
// Temporarily block signals for each button to prevent toggling while updating button states
for (auto it = formatButtons.begin(); it != formatButtons.end(); ++it) {
it.value()->blockSignals(true);
}
// Uncheck all buttons
for (auto it = formatButtons.begin(); it != formatButtons.end(); ++it) {
it.value()->setChecked(false);
}
// Get active filters for main types
QSet<QString> activeTypes;
for (const auto &filter : filterModel->getFiltersOfType(CardFilter::AttrFormat)) {
if (filter->type() == CardFilter::Type::TypeAnd) {
activeTypes.insert(filter->term());
}
}
// Check the buttons for active types
for (const auto &type : activeTypes) {
activeFormats[type] = true;
if (formatButtons.contains(type)) {
formatButtons[type]->setChecked(true);
}
}
// Re-enable signal emissions for each button
for (auto it = formatButtons.begin(); it != formatButtons.end(); ++it) {
it.value()->blockSignals(false);
}
// Update the visibility of buttons
updateFormatButtonsVisibility();
}

View file

@ -0,0 +1,43 @@
#ifndef COCKATRICE_VISUAL_DATABASE_DISPLAY_FORMAT_LEGALITY_FILTER_WIDGET_H
#define COCKATRICE_VISUAL_DATABASE_DISPLAY_FORMAT_LEGALITY_FILTER_WIDGET_H
#include "../../../filters/filter_tree_model.h"
#include "../general/layout_containers/flow_widget.h"
#include <QMap>
#include <QPushButton>
#include <QSpinBox>
#include <QToolButton>
#include <QVBoxLayout>
#include <QWidget>
class VisualDatabaseDisplayFormatLegalityFilterWidget : public QWidget
{
Q_OBJECT
public:
explicit VisualDatabaseDisplayFormatLegalityFilterWidget(QWidget *parent, FilterTreeModel *filterModel);
void retranslateUi();
void createFormatButtons();
void updateFormatButtonsVisibility();
int getMaxMainTypeCount() const;
void handleFormatToggled(const QString &format, bool active);
void updateFormatFilter();
void updateFilterMode(bool checked);
void syncWithFilterModel();
private:
FilterTreeModel *filterModel;
QMap<QString, int> allFormatsWithCount;
QSpinBox *spinBox;
QHBoxLayout *layout;
FlowWidget *flowWidget;
QPushButton *toggleButton; // Mode switch button
QMap<QString, bool> activeFormats; // Track active filters
QMap<QString, QPushButton *> formatButtons; // Store toggle buttons
bool exactMatchMode = false; // Toggle between "Exact Match" and "Includes"
};
#endif // COCKATRICE_VISUAL_DATABASE_DISPLAY_FORMAT_LEGALITY_FILTER_WIDGET_H

View file

@ -206,6 +206,7 @@ void VisualDatabaseDisplayWidget::initialize()
saveLoadWidget = new VisualDatabaseDisplayFilterSaveLoadWidget(this, filterModel); saveLoadWidget = new VisualDatabaseDisplayFilterSaveLoadWidget(this, filterModel);
nameFilterWidget = new VisualDatabaseDisplayNameFilterWidget(this, deckEditor, filterModel); nameFilterWidget = new VisualDatabaseDisplayNameFilterWidget(this, deckEditor, filterModel);
mainTypeFilterWidget = new VisualDatabaseDisplayMainTypeFilterWidget(this, filterModel); mainTypeFilterWidget = new VisualDatabaseDisplayMainTypeFilterWidget(this, filterModel);
formatLegalityWidget = new VisualDatabaseDisplayFormatLegalityFilterWidget(this, filterModel);
subTypeFilterWidget = new VisualDatabaseDisplaySubTypeFilterWidget(this, filterModel); subTypeFilterWidget = new VisualDatabaseDisplaySubTypeFilterWidget(this, filterModel);
setFilterWidget = new VisualDatabaseDisplaySetFilterWidget(this, filterModel); setFilterWidget = new VisualDatabaseDisplaySetFilterWidget(this, filterModel);
@ -223,6 +224,7 @@ void VisualDatabaseDisplayWidget::initialize()
filterContainerLayout->addWidget(quickFilterSubTypeWidget); filterContainerLayout->addWidget(quickFilterSubTypeWidget);
filterContainerLayout->addWidget(quickFilterSetWidget); filterContainerLayout->addWidget(quickFilterSetWidget);
filterContainerLayout->addWidget(mainTypeFilterWidget); filterContainerLayout->addWidget(mainTypeFilterWidget);
filterContainerLayout->addWidget(formatLegalityWidget);
searchLayout->addWidget(colorFilterWidget); searchLayout->addWidget(colorFilterWidget);
searchLayout->addWidget(clearFilterWidget); searchLayout->addWidget(clearFilterWidget);

View file

@ -17,6 +17,7 @@
#include "../utility/custom_line_edit.h" #include "../utility/custom_line_edit.h"
#include "visual_database_display_color_filter_widget.h" #include "visual_database_display_color_filter_widget.h"
#include "visual_database_display_filter_save_load_widget.h" #include "visual_database_display_filter_save_load_widget.h"
#include "visual_database_display_format_legality_filter_widget.h"
#include "visual_database_display_main_type_filter_widget.h" #include "visual_database_display_main_type_filter_widget.h"
#include "visual_database_display_name_filter_widget.h" #include "visual_database_display_name_filter_widget.h"
#include "visual_database_display_set_filter_widget.h" #include "visual_database_display_set_filter_widget.h"
@ -91,6 +92,7 @@ private:
SettingsButtonWidget *quickFilterNameWidget; SettingsButtonWidget *quickFilterNameWidget;
VisualDatabaseDisplayNameFilterWidget *nameFilterWidget; VisualDatabaseDisplayNameFilterWidget *nameFilterWidget;
VisualDatabaseDisplayMainTypeFilterWidget *mainTypeFilterWidget; VisualDatabaseDisplayMainTypeFilterWidget *mainTypeFilterWidget;
VisualDatabaseDisplayFormatLegalityFilterWidget *formatLegalityWidget;
SettingsButtonWidget *quickFilterSubTypeWidget; SettingsButtonWidget *quickFilterSubTypeWidget;
VisualDatabaseDisplaySubTypeFilterWidget *subTypeFilterWidget; VisualDatabaseDisplaySubTypeFilterWidget *subTypeFilterWidget;
SettingsButtonWidget *quickFilterSetWidget; SettingsButtonWidget *quickFilterSetWidget;

View file

@ -23,7 +23,71 @@ public:
bool saveCardDatabase(const QString &fileName) bool saveCardDatabase(const QString &fileName)
{ {
CockatriceXml4Parser parser(new NoopCardPreferenceProvider()); CockatriceXml4Parser parser(new NoopCardPreferenceProvider());
return parser.saveToFile(sets, cards, fileName);
return parser.saveToFile(createDefaultMagicFormats(), sets, cards, fileName);
}
FormatRulesNameMap createDefaultMagicFormats()
{
// Predefined common exceptions
CardCondition superTypeIsBasic;
superTypeIsBasic.field = "type";
superTypeIsBasic.matchType = "contains";
superTypeIsBasic.value = "Basic Land";
ExceptionRule basicLands;
basicLands.conditions.append(superTypeIsBasic);
CardCondition anyNumberAllowed;
anyNumberAllowed.field = "text";
anyNumberAllowed.matchType = "contains";
anyNumberAllowed.value = "may contain any number of";
ExceptionRule mayContainAnyNumber;
mayContainAnyNumber.conditions.append(anyNumberAllowed);
// Map to store default rules
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 {
FormatRulesPtr f(new FormatRules);
f->formatName = name;
f->maxCopies = maxCopies;
f->minDeckSize = minDeck;
f->maxDeckSize = maxDeck;
f->maxSideboardSize = maxSideboardSize;
f->exceptions.append(basicLands);
f->exceptions.append(mayContainAnyNumber);
defaultFormatRulesNameMap.insert(name.toLower(), f);
return f;
};
// ----------------- 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("Vintage");
return defaultFormatRulesNameMap;
} }
}; };

View file

@ -41,6 +41,8 @@ add_library(
libcockatrice/card/relation/card_relation.cpp libcockatrice/card/relation/card_relation.cpp
libcockatrice/card/set/card_set.cpp libcockatrice/card/set/card_set.cpp
libcockatrice/card/set/card_set_list.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( target_include_directories(

View file

@ -1,6 +1,7 @@
#ifndef CARD_INFO_H #ifndef CARD_INFO_H
#define CARD_INFO_H #define CARD_INFO_H
#include "format/format_legality_rules.h"
#include "printing/printing_info.h" #include "printing/printing_info.h"
#include <QDate> #include <QDate>
@ -22,10 +23,12 @@ class ICardDatabaseParser;
typedef QSharedPointer<CardInfo> CardInfoPtr; typedef QSharedPointer<CardInfo> CardInfoPtr;
typedef QSharedPointer<CardSet> CardSetPtr; typedef QSharedPointer<CardSet> CardSetPtr;
typedef QSharedPointer<FormatRules> FormatRulesPtr;
typedef QMap<QString, QList<PrintingInfo>> SetToPrintingsMap; typedef QMap<QString, QList<PrintingInfo>> SetToPrintingsMap;
typedef QHash<QString, CardInfoPtr> CardNameMap; typedef QHash<QString, CardInfoPtr> CardNameMap;
typedef QHash<QString, CardSetPtr> SetNameMap; typedef QHash<QString, CardSetPtr> SetNameMap;
typedef QHash<QString, FormatRulesPtr> FormatRulesNameMap;
Q_DECLARE_METATYPE(CardInfoPtr) 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 // inform the carddatabasemodels that they need to re-check their list of cards
emit cardDatabaseEnabledSetsChanged(); 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 /// Sets indexed by short name
SetNameMap sets; SetNameMap sets;
FormatRulesNameMap formats;
/// Loader responsible for file discovery and parsing /// Loader responsible for file discovery and parsing
CardDatabaseLoader *loader; CardDatabaseLoader *loader;
@ -141,6 +143,8 @@ public slots:
*/ */
void addSet(CardSetPtr set); void addSet(CardSetPtr set);
void addFormat(FormatRulesPtr format);
/** @brief Loads card databases from configured paths. */ /** @brief Loads card databases from configured paths. */
void loadCardDatabases(); void loadCardDatabases();

View file

@ -23,6 +23,7 @@ CardDatabaseLoader::CardDatabaseLoader(QObject *parent,
// connect parser outputs to the database adders // connect parser outputs to the database adders
connect(p, &ICardDatabaseParser::addCard, database, &CardDatabase::addCard, Qt::DirectConnection); connect(p, &ICardDatabaseParser::addCard, database, &CardDatabase::addCard, Qt::DirectConnection);
connect(p, &ICardDatabaseParser::addSet, database, &CardDatabase::addSet, 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 // 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; return true;
} }

View file

@ -342,3 +342,26 @@ QMap<QString, int> CardDatabaseQuerier::getAllSubCardTypesWithCount() const
return typeCounts; 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. * @return Map of subtype string to count.
*/ */
[[nodiscard]] QMap<QString, int> getAllSubCardTypesWithCount() const; [[nodiscard]] QMap<QString, int> getAllSubCardTypesWithCount() const;
FormatRulesPtr getFormat(const QString &formatName) const;
QMap<QString, int> getAllFormatsWithCount() const;
private: private:
const CardDatabase *db; //!< Card database used for all lookups. 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. * @brief Saves card and set data to a file.
* @param _formats
* @param sets Map of sets to save. * @param sets Map of sets to save.
* @param cards Map of cards to save. * @param cards Map of cards to save.
* @param fileName Target file path. * @param fileName Target file path.
@ -45,7 +46,8 @@ public:
* @param sourceVersion Optional version string of the source. * @param sourceVersion Optional version string of the source.
* @return true if save succeeded. * @return true if save succeeded.
*/ */
virtual bool saveToFile(SetNameMap sets, virtual bool saveToFile(FormatRulesNameMap _formats,
SetNameMap sets,
CardNameMap cards, CardNameMap cards,
const QString &fileName, const QString &fileName,
const QString &sourceUrl = "unknown", const QString &sourceUrl = "unknown",
@ -79,6 +81,8 @@ signals:
/** Emitted when a set is loaded from the database. */ /** Emitted when a set is loaded from the database. */
void addSet(CardSetPtr set); void addSet(CardSetPtr set);
void addFormat(FormatRulesPtr format);
}; };
Q_DECLARE_INTERFACE(ICardDatabaseParser, "ICardDatabaseParser") Q_DECLARE_INTERFACE(ICardDatabaseParser, "ICardDatabaseParser")

View file

@ -438,12 +438,15 @@ static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfoPtr &in
return xml; return xml;
} }
bool CockatriceXml3Parser::saveToFile(SetNameMap _sets, bool CockatriceXml3Parser::saveToFile(FormatRulesNameMap _formats,
SetNameMap _sets,
CardNameMap cards, CardNameMap cards,
const QString &fileName, const QString &fileName,
const QString &sourceUrl, const QString &sourceUrl,
const QString &sourceVersion) const QString &sourceVersion)
{ {
Q_UNUSED(_formats);
QFile file(fileName); QFile file(fileName);
if (!file.open(QIODevice::WriteOnly)) { if (!file.open(QIODevice::WriteOnly)) {
return false; return false;

View file

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

View file

@ -6,6 +6,7 @@
#include <QDebug> #include <QDebug>
#include <QFile> #include <QFile>
#include <QXmlStreamReader> #include <QXmlStreamReader>
#include <libcockatrice/card/format/format_legality_rules.h>
#include <version_string.h> #include <version_string.h>
#define COCKATRICE_XML4_TAGNAME "cockatrice_carddatabase" #define COCKATRICE_XML4_TAGNAME "cockatrice_carddatabase"
@ -60,7 +61,9 @@ void CockatriceXml4Parser::parseFile(QIODevice &device)
} }
auto xmlName = xml.name().toString(); auto xmlName = xml.name().toString();
if (xmlName == "sets") { if (xmlName == "formats") {
loadFormats(xml);
} else if (xmlName == "sets") {
loadSetsFromXml(xml); loadSetsFromXml(xml);
} else if (xmlName == "cards") { } else if (xmlName == "cards") {
loadCardsFromXml(xml); loadCardsFromXml(xml);
@ -78,6 +81,110 @@ 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() == "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 == "maxCopies") {
rulesPtr->maxCopies = xml.readElementText().toInt();
} else if (xmlName == "maxRestrictedCopies") {
rulesPtr->maxRestrictedCopies = xml.readElementText().toInt();
} else if (xmlName == "exceptions") {
while (!xml.atEnd()) {
token = xml.readNext();
if (token == QXmlStreamReader::EndElement && xml.name() == "exceptions") {
break;
}
if (token == QXmlStreamReader::StartElement && xml.name() == "exception") {
ExceptionRule ex;
while (!xml.atEnd()) {
token = xml.readNext();
if (token == QXmlStreamReader::EndElement && xml.name() == "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()) {
auto token = xml.readNext();
// Stop when we hit </formats>
if (token == QXmlStreamReader::EndElement && xml.name() == "formats") {
break;
}
// Only handle start elements
if (token != QXmlStreamReader::StartElement)
continue;
if (xml.name() == "format") {
auto rulesPtr = parseFormat(xml);
emit addFormat(rulesPtr);
} else {
qCInfo(CockatriceXml4Log) << "Unknown format child" << xml.name() << ", skipping";
xml.skipCurrentElement();
}
}
}
void CockatriceXml4Parser::loadSetsFromXml(QXmlStreamReader &xml) void CockatriceXml4Parser::loadSetsFromXml(QXmlStreamReader &xml)
{ {
while (!xml.atEnd()) { while (!xml.atEnd()) {
@ -273,6 +380,49 @@ 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));
xml.writeTextElement("maxCopies", QString::number(rules.maxCopies));
xml.writeTextElement("maxRestrictedCopies", QString::number(rules.maxRestrictedCopies));
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) static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardSetPtr &set)
{ {
if (set.isNull()) { if (set.isNull()) {
@ -399,7 +549,8 @@ static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfoPtr &in
return xml; return xml;
} }
bool CockatriceXml4Parser::saveToFile(SetNameMap _sets, bool CockatriceXml4Parser::saveToFile(FormatRulesNameMap _formats,
SetNameMap _sets,
CardNameMap cards, CardNameMap cards,
const QString &fileName, const QString &fileName,
const QString &sourceUrl, const QString &sourceUrl,
@ -426,6 +577,14 @@ bool CockatriceXml4Parser::saveToFile(SetNameMap _sets,
xml.writeTextElement("sourceVersion", sourceVersion); xml.writeTextElement("sourceVersion", sourceVersion);
xml.writeEndElement(); xml.writeEndElement();
if (_formats.count() > 0) {
xml.writeStartElement("formats");
for (FormatRulesPtr format : _formats) {
xml << format;
}
xml.writeEndElement();
}
if (_sets.count() > 0) { if (_sets.count() > 0) {
xml.writeStartElement("sets"); xml.writeStartElement("sets");
for (CardSetPtr set : _sets) { for (CardSetPtr set : _sets) {

View file

@ -49,7 +49,8 @@ public:
/** /**
* @brief Save sets and cards back to an XML4 file. * @brief Save sets and cards back to an XML4 file.
*/ */
bool saveToFile(SetNameMap _sets, bool saveToFile(FormatRulesNameMap _formats,
SetNameMap _sets,
CardNameMap cards, CardNameMap cards,
const QString &fileName, const QString &fileName,
const QString &sourceUrl = "unknown", const QString &sourceUrl = "unknown",
@ -72,6 +73,7 @@ private:
*/ */
void loadCardsFromXml(QXmlStreamReader &xml); void loadCardsFromXml(QXmlStreamReader &xml);
void loadFormats(QXmlStreamReader &xml);
/** /**
* @brief Load all <set> elements from the XML stream. * @brief Load all <set> elements from the XML stream.
* @param xml The open QXmlStreamReader positioned at the <sets> element. * @param xml The open QXmlStreamReader positioned at the <sets> element.

View file

@ -0,0 +1,46 @@
#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 = 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,68 @@
#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 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;
int maxCopies = 4; // default constructed rule
int maxRestrictedCopies = 1; // Used for "restricted" legality tags
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

View file

@ -136,6 +136,8 @@ bool DeckList::readElement(QXmlStreamReader *xml)
metadata.lastLoadedTimestamp = xml->readElementText(); metadata.lastLoadedTimestamp = xml->readElementText();
} else if (childName == "deckname") { } else if (childName == "deckname") {
metadata.name = xml->readElementText(); metadata.name = xml->readElementText();
} else if (childName == "format") {
metadata.gameFormat = xml->readElementText();
} else if (childName == "comments") { } else if (childName == "comments") {
metadata.comments = xml->readElementText(); metadata.comments = xml->readElementText();
} else if (childName == "bannerCard") { } else if (childName == "bannerCard") {
@ -170,6 +172,7 @@ void writeMetadata(QXmlStreamWriter *xml, const DeckList::Metadata &metadata)
{ {
xml->writeTextElement("lastLoadedTimestamp", metadata.lastLoadedTimestamp); xml->writeTextElement("lastLoadedTimestamp", metadata.lastLoadedTimestamp);
xml->writeTextElement("deckname", metadata.name); xml->writeTextElement("deckname", metadata.name);
xml->writeTextElement("format", metadata.gameFormat);
xml->writeStartElement("bannerCard"); xml->writeStartElement("bannerCard");
xml->writeAttribute("providerId", metadata.bannerCard.providerId); xml->writeAttribute("providerId", metadata.bannerCard.providerId);
xml->writeCharacters(metadata.bannerCard.name); xml->writeCharacters(metadata.bannerCard.name);
@ -594,15 +597,16 @@ DecklistCardNode *DeckList::addCard(const QString &cardName,
const int position, const int position,
const QString &cardSetName, const QString &cardSetName,
const QString &cardSetCollectorNumber, const QString &cardSetCollectorNumber,
const QString &cardProviderId) const QString &cardProviderId,
const bool formatLegal)
{ {
auto *zoneNode = dynamic_cast<InnerDecklistNode *>(root->findChild(zoneName)); auto *zoneNode = dynamic_cast<InnerDecklistNode *>(root->findChild(zoneName));
if (zoneNode == nullptr) { if (zoneNode == nullptr) {
zoneNode = new InnerDecklistNode(zoneName, root); zoneNode = new InnerDecklistNode(zoneName, root);
} }
auto *node = auto *node = new DecklistCardNode(cardName, 1, zoneNode, position, cardSetName, cardSetCollectorNumber,
new DecklistCardNode(cardName, 1, zoneNode, position, cardSetName, cardSetCollectorNumber, cardProviderId); cardProviderId, formatLegal);
refreshDeckHash(); refreshDeckHash();
return node; return node;

View file

@ -126,6 +126,7 @@ public:
{ {
QString name; ///< User-defined deck name. QString name; ///< User-defined deck name.
QString comments; ///< Free-form comments or notes. QString comments; ///< Free-form comments or notes.
QString gameFormat; ///< The name of the game format this deck contains legal cards for
CardRef bannerCard; ///< Optional representative card for the deck. CardRef bannerCard; ///< Optional representative card for the deck.
QStringList tags; ///< User-defined tags for deck classification. QStringList tags; ///< User-defined tags for deck classification.
QString lastLoadedTimestamp; ///< Timestamp string of last load. QString lastLoadedTimestamp; ///< Timestamp string of last load.
@ -181,6 +182,10 @@ public:
{ {
metadata.lastLoadedTimestamp = _lastLoadedTimestamp; metadata.lastLoadedTimestamp = _lastLoadedTimestamp;
} }
void setGameFormat(const QString &_gameFormat = QString())
{
metadata.gameFormat = _gameFormat;
}
///@} ///@}
/// @brief Construct an empty deck. /// @brief Construct an empty deck.
@ -219,6 +224,10 @@ public:
{ {
return metadata.lastLoadedTimestamp; return metadata.lastLoadedTimestamp;
} }
QString getGameFormat() const
{
return metadata.gameFormat;
}
///@} ///@}
bool isBlankDeck() const bool isBlankDeck() const
@ -277,7 +286,8 @@ public:
int position, int position,
const QString &cardSetName = QString(), const QString &cardSetName = QString(),
const QString &cardSetCollectorNumber = QString(), const QString &cardSetCollectorNumber = QString(),
const QString &cardProviderId = QString()); const QString &cardProviderId = QString(),
const bool formatLegal = true);
bool deleteNode(AbstractDecklistNode *node, InnerDecklistNode *rootNode = nullptr); bool deleteNode(AbstractDecklistNode *node, InnerDecklistNode *rootNode = nullptr);
///@} ///@}

View file

@ -88,6 +88,12 @@ public:
/// @param _cardSetNumber Set the collector number. /// @param _cardSetNumber Set the collector number.
virtual void setCardCollectorNumber(const QString &_cardSetNumber) = 0; virtual void setCardCollectorNumber(const QString &_cardSetNumber) = 0;
/// @return The format legality of the card
virtual bool getFormatLegality() const = 0;
/// @param _formatLegal If the card is considered legal
virtual void setFormatLegality(const bool _formatLegal) = 0;
/** /**
* @brief Get the height of this node in the tree. * @brief Get the height of this node in the tree.
* *

View file

@ -51,6 +51,7 @@ class DecklistCardNode : public AbstractDecklistCardNode
QString cardSetShortName; ///< Short set code (e.g., "NEO"). QString cardSetShortName; ///< Short set code (e.g., "NEO").
QString cardSetNumber; ///< Collector number within the set. QString cardSetNumber; ///< Collector number within the set.
QString cardProviderId; ///< External provider identifier (e.g., UUID). QString cardProviderId; ///< External provider identifier (e.g., UUID).
bool formatLegal; ///< Format legality
public: public:
/** /**
@ -63,6 +64,7 @@ public:
* @param _cardSetShortName Short set code (e.g., "NEO"). * @param _cardSetShortName Short set code (e.g., "NEO").
* @param _cardSetNumber Collector number within the set. * @param _cardSetNumber Collector number within the set.
* @param _cardProviderId External provider ID (e.g., UUID). * @param _cardProviderId External provider ID (e.g., UUID).
* @param _formatLegality If the card is legal in the format
* *
* On construction, if a parent is provided, this node is inserted into * On construction, if a parent is provided, this node is inserted into
* the parents children list automatically. * the parents children list automatically.
@ -73,10 +75,11 @@ public:
int position = -1, int position = -1,
QString _cardSetShortName = QString(), QString _cardSetShortName = QString(),
QString _cardSetNumber = QString(), QString _cardSetNumber = QString(),
QString _cardProviderId = QString()) QString _cardProviderId = QString(),
bool _formatLegality = true)
: AbstractDecklistCardNode(_parent, position), name(std::move(_name)), number(_number), : AbstractDecklistCardNode(_parent, position), name(std::move(_name)), number(_number),
cardSetShortName(std::move(_cardSetShortName)), cardSetNumber(std::move(_cardSetNumber)), cardSetShortName(std::move(_cardSetShortName)), cardSetNumber(std::move(_cardSetNumber)),
cardProviderId(std::move(_cardProviderId)) cardProviderId(std::move(_cardProviderId)), formatLegal(_formatLegality)
{ {
} }
@ -150,6 +153,18 @@ public:
cardSetNumber = _cardSetNumber; cardSetNumber = _cardSetNumber;
} }
/// @return The format legality of the card
[[nodiscard]] bool getFormatLegality() const override
{
return formatLegal;
}
/// @param _formatLegal If the card is considered legal
void setFormatLegality(const bool _formatLegal) override
{
formatLegal = _formatLegal;
}
/// @return Always false; card nodes are not deck headers. /// @return Always false; card nodes are not deck headers.
[[nodiscard]] bool isDeckHeader() const override [[nodiscard]] bool isDeckHeader() const override
{ {

View file

@ -168,6 +168,10 @@ QVariant DeckListModel::data(const QModelIndex &index, int role) const
return card->depth(); return card->depth();
} }
case DeckRoles::IsLegalRole: {
return card->getFormatLegality();
}
default: { default: {
return {}; return {};
} }
@ -268,6 +272,7 @@ bool DeckListModel::setData(const QModelIndex &index, const QVariant &value, con
switch (index.column()) { switch (index.column()) {
case DeckListModelColumns::CARD_AMOUNT: case DeckListModelColumns::CARD_AMOUNT:
node->setNumber(value.toInt()); node->setNumber(value.toInt());
refreshCardFormatLegalities();
break; break;
case DeckListModelColumns::CARD_NAME: case DeckListModelColumns::CARD_NAME:
node->setName(value.toString()); node->setName(value.toString());
@ -414,8 +419,9 @@ QModelIndex DeckListModel::addCard(const ExactCard &card, const QString &zoneNam
// Determine the correct index // Determine the correct index
int insertRow = findSortedInsertRow(groupNode, cardInfo); int insertRow = findSortedInsertRow(groupNode, cardInfo);
auto *decklistCard = deckList->addCard(cardInfo->getName(), zoneName, insertRow, cardSetName, auto *decklistCard =
printingInfo.getProperty("num"), printingInfo.getProperty("uuid")); deckList->addCard(cardInfo->getName(), zoneName, insertRow, cardSetName, printingInfo.getProperty("num"),
printingInfo.getProperty("uuid"), isCardLegalForCurrentFormat(cardInfo));
beginInsertRows(parentIndex, insertRow, insertRow); beginInsertRows(parentIndex, insertRow, insertRow);
cardNode = new DecklistModelCardNode(decklistCard, groupNode, insertRow); cardNode = new DecklistModelCardNode(decklistCard, groupNode, insertRow);
@ -532,6 +538,13 @@ void DeckListModel::setActiveGroupCriteria(DeckListModelGroupCriteria::Type newC
rebuildTree(); rebuildTree();
} }
void DeckListModel::setActiveFormat(const QString &_format)
{
deckList->setGameFormat(_format);
refreshCardFormatLegalities();
emitBackgroundUpdates(QModelIndex()); // start from root
}
void DeckListModel::cleanList() void DeckListModel::cleanList()
{ {
setDeckList(new DeckList); setDeckList(new DeckList);
@ -596,3 +609,73 @@ QList<QString> DeckListModel::getZones() const
return zones; return zones;
} }
bool DeckListModel::isCardLegalForCurrentFormat(const CardInfoPtr cardInfo)
{
if (!deckList->getGameFormat().isEmpty()) {
if (cardInfo->getProperties().contains("format-" + deckList->getGameFormat())) {
QString formatLegality = cardInfo->getProperty("format-" + deckList->getGameFormat());
return formatLegality == "legal" || formatLegality == "restricted";
}
return false;
}
return true;
}
bool DeckListModel::isCardQuantityLegalForCurrentFormat(const CardInfoPtr cardInfo, int quantity)
{
auto formatRules = CardDatabaseManager::query()->getFormat(deckList->getGameFormat());
if (!formatRules) {
return true;
}
if (cardHasAnyException(*cardInfo.data(), *formatRules.data())) {
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;
}
}
}
return false;
}
void DeckListModel::refreshCardFormatLegalities()
{
InnerDecklistNode *listRoot = deckList->getRoot();
for (int i = 0; i < listRoot->size(); i++) {
auto *currentZone = static_cast<InnerDecklistNode *>(listRoot->at(i));
for (int j = 0; j < currentZone->size(); j++) {
auto *currentCard = static_cast<DecklistCardNode *>(currentZone->at(j));
// TODO: better sanity checking
if (currentCard == nullptr) {
continue;
}
ExactCard exactCard = CardDatabaseManager::query()->getCard(currentCard->toCardRef());
if (!exactCard) {
continue;
}
bool legal = isCardLegalForCurrentFormat(exactCard.getCardPtr());
if (legal) {
legal = isCardQuantityLegalForCurrentFormat(exactCard.getCardPtr(), currentCard->getNumber());
}
currentCard->setFormatLegality(legal);
}
}
}

View file

@ -164,6 +164,14 @@ public:
{ {
dataNode->setCardCollectorNumber(_cardSetNumber); dataNode->setCardCollectorNumber(_cardSetNumber);
} }
bool getFormatLegality() const override
{
return dataNode->getFormatLegality();
}
void setFormatLegality(const bool _formatLegal) override
{
dataNode->setFormatLegality(_formatLegal);
}
/** /**
* @brief Returns the underlying data node. * @brief Returns the underlying data node.
@ -209,6 +217,9 @@ public slots:
*/ */
void rebuildTree(); void rebuildTree();
public slots:
void setActiveFormat(const QString &_format);
signals: signals:
/** /**
* @brief Emitted whenever the deck hash changes due to modifications in the model. * @brief Emitted whenever the deck hash changes due to modifications in the model.
@ -301,6 +312,9 @@ public:
[[nodiscard]] QList<ExactCard> getCards() const; [[nodiscard]] QList<ExactCard> getCards() const;
[[nodiscard]] QList<ExactCard> getCardsForZone(const QString &zoneName) const; [[nodiscard]] QList<ExactCard> getCardsForZone(const QString &zoneName) const;
[[nodiscard]] QList<QString> getZones() const; [[nodiscard]] QList<QString> getZones() const;
bool isCardLegalForCurrentFormat(CardInfoPtr cardInfo);
bool isCardQuantityLegalForCurrentFormat(CardInfoPtr cardInfo, int quantity);
void refreshCardFormatLegalities();
/** /**
* @brief Sets the criteria used to group cards in the model. * @brief Sets the criteria used to group cards in the model.

View file

@ -463,6 +463,69 @@ int OracleImporter::importCardsFromSet(const CardSetPtr &currentSet, const QList
return numCards; return numCards;
} }
FormatRulesNameMap OracleImporter::createDefaultMagicFormats()
{
// Predefined common exceptions
CardCondition superTypeIsBasic;
superTypeIsBasic.field = "type";
superTypeIsBasic.matchType = "contains";
superTypeIsBasic.value = "Basic Land";
ExceptionRule basicLands;
basicLands.conditions.append(superTypeIsBasic);
CardCondition anyNumberAllowed;
anyNumberAllowed.field = "text";
anyNumberAllowed.matchType = "contains";
anyNumberAllowed.value = "may contain any number of";
ExceptionRule mayContainAnyNumber;
mayContainAnyNumber.conditions.append(anyNumberAllowed);
// Map to store default rules
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 {
FormatRulesPtr f(new FormatRules);
f->formatName = name;
f->maxCopies = maxCopies;
f->minDeckSize = minDeck;
f->maxDeckSize = maxDeck;
f->maxSideboardSize = maxSideboardSize;
f->exceptions.append(basicLands);
f->exceptions.append(mayContainAnyNumber);
defaultFormatRulesNameMap.insert(name.toLower(), f);
return f;
};
// ----------------- 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("Vintage");
return defaultFormatRulesNameMap;
}
int OracleImporter::startImport() int OracleImporter::startImport()
{ {
static ICardSetPriorityController *noOpController = new NoopCardSetPriorityController(); static ICardSetPriorityController *noOpController = new NoopCardSetPriorityController();
@ -497,7 +560,8 @@ int OracleImporter::startImport()
bool OracleImporter::saveToFile(const QString &fileName, const QString &sourceUrl, const QString &sourceVersion) bool OracleImporter::saveToFile(const QString &fileName, const QString &sourceUrl, const QString &sourceVersion)
{ {
CockatriceXml4Parser parser(new NoopCardPreferenceProvider()); CockatriceXml4Parser parser(new NoopCardPreferenceProvider());
return parser.saveToFile(sets, cards, fileName, sourceUrl, sourceVersion);
return parser.saveToFile(createDefaultMagicFormats(), sets, cards, fileName, sourceUrl, sourceVersion);
} }
void OracleImporter::clear() void OracleImporter::clear()

View file

@ -154,6 +154,7 @@ public:
int startImport(); int startImport();
bool saveToFile(const QString &fileName, const QString &sourceUrl, const QString &sourceVersion); bool saveToFile(const QString &fileName, const QString &sourceUrl, const QString &sourceVersion);
int importCardsFromSet(const CardSetPtr &currentSet, const QList<QVariant> &cardsList); int importCardsFromSet(const CardSetPtr &currentSet, const QList<QVariant> &cardsList);
FormatRulesNameMap createDefaultMagicFormats();
const CardNameMap &getCardList() const const CardNameMap &getCardList() const
{ {
return cards; return cards;