From 3f06c848e1bb0627de2f42e1bcdd75fdd7162c83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= Date: Wed, 10 Sep 2025 09:40:28 +0200 Subject: [PATCH] Deck legality checker. Took 51 seconds Took 1 minute Took 1 minute Took 5 minutes Took 3 minutes --- cockatrice/CMakeLists.txt | 1 + .../deck_editor_deck_dock_widget.cpp | 69 +++++- .../deck_editor_deck_dock_widget.h | 3 + .../deck_editor/deck_list_style_proxy.cpp | 3 +- ..._display_format_legality_filter_widget.cpp | 205 ++++++++++++++++++ ...se_display_format_legality_filter_widget.h | 43 ++++ .../visual_database_display_widget.cpp | 2 + .../visual_database_display_widget.h | 2 + dbconverter/src/main.h | 66 +++++- libcockatrice_card/CMakeLists.txt | 2 + .../libcockatrice/card/card_info.h | 3 + .../card/database/card_database.cpp | 5 + .../card/database/card_database.h | 4 + .../card/database/card_database_loader.cpp | 3 +- .../card/database/card_database_querier.cpp | 23 ++ .../card/database/card_database_querier.h | 2 + .../database/parser/card_database_parser.h | 6 +- .../card/database/parser/cockatrice_xml_3.cpp | 5 +- .../card/database/parser/cockatrice_xml_3.h | 3 +- .../card/database/parser/cockatrice_xml_4.cpp | 163 +++++++++++++- .../card/database/parser/cockatrice_xml_4.h | 4 +- .../card/format/format_legality_rules.cpp | 46 ++++ .../card/format/format_legality_rules.h | 68 ++++++ .../libcockatrice/deck_list/deck_list.cpp | 10 +- .../libcockatrice/deck_list/deck_list.h | 12 +- .../tree/abstract_deck_list_card_node.h | 6 + .../deck_list/tree/deck_list_card_node.h | 19 +- .../models/deck_list/deck_list_model.cpp | 89 +++++++- .../models/deck_list/deck_list_model.h | 14 ++ oracle/src/oracleimporter.cpp | 66 +++++- oracle/src/oracleimporter.h | 1 + 31 files changed, 921 insertions(+), 27 deletions(-) create mode 100644 cockatrice/src/interface/widgets/visual_database_display/visual_database_display_format_legality_filter_widget.cpp create mode 100644 cockatrice/src/interface/widgets/visual_database_display/visual_database_display_format_legality_filter_widget.h create mode 100644 libcockatrice_card/libcockatrice/card/format/format_legality_rules.cpp create mode 100644 libcockatrice_card/libcockatrice/card/format/format_legality_rules.h diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index dec1b3d62..c9fa80e6b 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -202,6 +202,7 @@ set(cockatrice_SOURCES 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_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_name_filter_widget.cpp src/interface/widgets/visual_database_display/visual_database_display_set_filter_widget.cpp diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp index 08ebf23ba..bbc1f5417 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.cpp @@ -124,6 +124,12 @@ void DeckEditorDeckDockWidget::createDeckDock() quickSettingsWidget->addSettingsWidget(showBannerCardCheckBox); 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->setObjectName("commentsLabel"); commentsEdit = new QTextEdit; @@ -205,16 +211,19 @@ void DeckEditorDeckDockWidget::createDeckDock() upperLayout->addWidget(nameEdit, 0, 1); upperLayout->addWidget(quickSettingsWidget, 0, 2); - upperLayout->addWidget(commentsLabel, 1, 0); - upperLayout->addWidget(commentsEdit, 1, 1); + upperLayout->addWidget(formatLabel, 1, 0); + upperLayout->addWidget(formatComboBox, 1, 1); - upperLayout->addWidget(bannerCardLabel, 2, 0); - upperLayout->addWidget(bannerCardComboBox, 2, 1); + upperLayout->addWidget(commentsLabel, 2, 0); + 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(activeGroupCriteriaComboBox, 4, 1); + upperLayout->addWidget(deckTagsDisplayWidget, 4, 1); + + upperLayout->addWidget(activeGroupCriteriaLabel, 5, 0); + upperLayout->addWidget(activeGroupCriteriaComboBox, 5, 1); hashLabel1 = new QLabel(); hashLabel1->setObjectName("hashLabel1"); @@ -263,6 +272,44 @@ void DeckEditorDeckDockWidget::createDeckDock() refreshShortcuts(); retranslateUi(); + + connect(CardDatabaseManager::getInstance(), &CardDatabase::cardDatabaseLoadingFinished, this, + &DeckEditorDeckDockWidget::initializeFormats); + + if (CardDatabaseManager::getInstance()->getLoadStatus() == LoadStatus::Ok) { + initializeFormats(); + } +} + +void DeckEditorDeckDockWidget::initializeFormats() +{ + QMap 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::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() @@ -466,6 +513,12 @@ void DeckEditorDeckDockWidget::syncDisplayWidgetsToModel() void DeckEditorDeckDockWidget::sortDeckModelToDeckView() { 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() @@ -724,6 +777,8 @@ void DeckEditorDeckDockWidget::retranslateUi() showTagsWidgetCheckBox->setText(tr("Show tags selection menu")); commentsLabel->setText(tr("&Comments:")); activeGroupCriteriaLabel->setText(tr("Group by:")); + formatLabel->setText(tr("Format:")); + hashLabel1->setText(tr("Hash:")); aIncrement->setText(tr("&Increment number")); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h index d1f1da300..08aa38e5e 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_deck_dock_widget.h @@ -69,6 +69,7 @@ public slots: void actSwapCard(); void actRemoveCard(); void offsetCountAtIndex(const QModelIndex &idx, int offset); + void initializeFormats(); void expandAll(); signals: @@ -100,6 +101,8 @@ private: LineEditUnfocusable *hashLabel; QLabel *activeGroupCriteriaLabel; QComboBox *activeGroupCriteriaComboBox; + QLabel *formatLabel; + QComboBox *formatComboBox; QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard; diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_list_style_proxy.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_list_style_proxy.cpp index 9c2265dfd..14c5faae7 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_list_style_proxy.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_list_style_proxy.cpp @@ -23,8 +23,7 @@ QVariant DeckListStyleProxy::data(const QModelIndex &index, int role) const if (role == Qt::BackgroundRole) { if (isCard) { - const bool legal = - true; // TODO: Not implemented yet. QIdentityProxyModel::data(index, DeckRoles::IsLegalRole).toBool(); + const bool legal = QIdentityProxyModel::data(index, DeckRoles::IsLegalRole).toBool(); int base = 255 - (index.row() % 2) * 30; return legal ? QBrush(QColor(base, base, base)) : QBrush(QColor(255, base / 3, base / 3)); } else { diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_format_legality_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_format_legality_filter_widget.cpp new file mode 100644 index 000000000..dccf57802 --- /dev/null +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_format_legality_filter_widget.cpp @@ -0,0 +1,205 @@ +#include "visual_database_display_format_legality_filter_widget.h" + +#include "../../../filters/filter_tree_model.h" + +#include +#include +#include +#include +#include + +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::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 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 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(); +} diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_format_legality_filter_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_format_legality_filter_widget.h new file mode 100644 index 000000000..a2a00b740 --- /dev/null +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_format_legality_filter_widget.h @@ -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 +#include +#include +#include +#include +#include + +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 allFormatsWithCount; + QSpinBox *spinBox; + QHBoxLayout *layout; + FlowWidget *flowWidget; + QPushButton *toggleButton; // Mode switch button + + QMap activeFormats; // Track active filters + QMap formatButtons; // Store toggle buttons + + bool exactMatchMode = false; // Toggle between "Exact Match" and "Includes" +}; + +#endif // COCKATRICE_VISUAL_DATABASE_DISPLAY_FORMAT_LEGALITY_FILTER_WIDGET_H diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp index cc0fc8033..c5eaa171a 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp @@ -206,6 +206,7 @@ void VisualDatabaseDisplayWidget::initialize() saveLoadWidget = new VisualDatabaseDisplayFilterSaveLoadWidget(this, filterModel); nameFilterWidget = new VisualDatabaseDisplayNameFilterWidget(this, deckEditor, filterModel); mainTypeFilterWidget = new VisualDatabaseDisplayMainTypeFilterWidget(this, filterModel); + formatLegalityWidget = new VisualDatabaseDisplayFormatLegalityFilterWidget(this, filterModel); subTypeFilterWidget = new VisualDatabaseDisplaySubTypeFilterWidget(this, filterModel); setFilterWidget = new VisualDatabaseDisplaySetFilterWidget(this, filterModel); @@ -223,6 +224,7 @@ void VisualDatabaseDisplayWidget::initialize() filterContainerLayout->addWidget(quickFilterSubTypeWidget); filterContainerLayout->addWidget(quickFilterSetWidget); filterContainerLayout->addWidget(mainTypeFilterWidget); + filterContainerLayout->addWidget(formatLegalityWidget); searchLayout->addWidget(colorFilterWidget); searchLayout->addWidget(clearFilterWidget); diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h index 5cab2ca9b..24990f8e5 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h @@ -17,6 +17,7 @@ #include "../utility/custom_line_edit.h" #include "visual_database_display_color_filter_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_name_filter_widget.h" #include "visual_database_display_set_filter_widget.h" @@ -91,6 +92,7 @@ private: SettingsButtonWidget *quickFilterNameWidget; VisualDatabaseDisplayNameFilterWidget *nameFilterWidget; VisualDatabaseDisplayMainTypeFilterWidget *mainTypeFilterWidget; + VisualDatabaseDisplayFormatLegalityFilterWidget *formatLegalityWidget; SettingsButtonWidget *quickFilterSubTypeWidget; VisualDatabaseDisplaySubTypeFilterWidget *subTypeFilterWidget; SettingsButtonWidget *quickFilterSetWidget; diff --git a/dbconverter/src/main.h b/dbconverter/src/main.h index 7a5809b1b..4476161b5 100644 --- a/dbconverter/src/main.h +++ b/dbconverter/src/main.h @@ -23,7 +23,71 @@ public: bool saveCardDatabase(const QString &fileName) { 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; } }; diff --git a/libcockatrice_card/CMakeLists.txt b/libcockatrice_card/CMakeLists.txt index 857376163..f516cde00 100644 --- a/libcockatrice_card/CMakeLists.txt +++ b/libcockatrice_card/CMakeLists.txt @@ -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( diff --git a/libcockatrice_card/libcockatrice/card/card_info.h b/libcockatrice_card/libcockatrice/card/card_info.h index 7bd51356c..00e8fec37 100644 --- a/libcockatrice_card/libcockatrice/card/card_info.h +++ b/libcockatrice_card/libcockatrice/card/card_info.h @@ -1,6 +1,7 @@ #ifndef CARD_INFO_H #define CARD_INFO_H +#include "format/format_legality_rules.h" #include "printing/printing_info.h" #include @@ -22,10 +23,12 @@ class ICardDatabaseParser; typedef QSharedPointer CardInfoPtr; typedef QSharedPointer CardSetPtr; +typedef QSharedPointer FormatRulesPtr; typedef QMap> SetToPrintingsMap; typedef QHash CardNameMap; typedef QHash SetNameMap; +typedef QHash FormatRulesNameMap; Q_DECLARE_METATYPE(CardInfoPtr) diff --git a/libcockatrice_card/libcockatrice/card/database/card_database.cpp b/libcockatrice_card/libcockatrice/card/database/card_database.cpp index 2e977901f..de84ad814 100644 --- a/libcockatrice_card/libcockatrice/card/database/card_database.cpp +++ b/libcockatrice_card/libcockatrice/card/database/card_database.cpp @@ -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); +} \ No newline at end of file diff --git a/libcockatrice_card/libcockatrice/card/database/card_database.h b/libcockatrice_card/libcockatrice/card/database/card_database.h index 6028cce86..7f8fc39db 100644 --- a/libcockatrice_card/libcockatrice/card/database/card_database.h +++ b/libcockatrice_card/libcockatrice/card/database/card_database.h @@ -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(); diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_loader.cpp b/libcockatrice_card/libcockatrice/card/database/card_database_loader.cpp index f56f6a0b5..91bb4c741 100644 --- a/libcockatrice_card/libcockatrice/card/database/card_database_loader.cpp +++ b/libcockatrice_card/libcockatrice/card/database/card_database_loader.cpp @@ -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; } diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp b/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp index 65999e590..b2a675b99 100644 --- a/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp +++ b/libcockatrice_card/libcockatrice/card/database/card_database_querier.cpp @@ -341,4 +341,27 @@ QMap CardDatabaseQuerier::getAllSubCardTypesWithCount() const } return typeCounts; +} + +FormatRulesPtr CardDatabaseQuerier::getFormat(const QString &formatName) const +{ + return db->formats.value(formatName.toLower()); +} + +QMap CardDatabaseQuerier::getAllFormatsWithCount() const +{ + QMap 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; } \ No newline at end of file diff --git a/libcockatrice_card/libcockatrice/card/database/card_database_querier.h b/libcockatrice_card/libcockatrice/card/database/card_database_querier.h index d9a980f06..ff8d7958b 100644 --- a/libcockatrice_card/libcockatrice/card/database/card_database_querier.h +++ b/libcockatrice_card/libcockatrice/card/database/card_database_querier.h @@ -214,6 +214,8 @@ public: * @return Map of subtype string to count. */ [[nodiscard]] QMap getAllSubCardTypesWithCount() const; + FormatRulesPtr getFormat(const QString &formatName) const; + QMap getAllFormatsWithCount() const; private: const CardDatabase *db; //!< Card database used for all lookups. diff --git a/libcockatrice_card/libcockatrice/card/database/parser/card_database_parser.h b/libcockatrice_card/libcockatrice/card/database/parser/card_database_parser.h index 35012dbc5..93d46a3cc 100644 --- a/libcockatrice_card/libcockatrice/card/database/parser/card_database_parser.h +++ b/libcockatrice_card/libcockatrice/card/database/parser/card_database_parser.h @@ -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") diff --git a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp index 35e2f3d83..9df396c74 100644 --- a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.cpp @@ -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; diff --git a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.h b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.h index a727561ea..c3a261739 100644 --- a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.h +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_3.h @@ -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", 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 ab4c8002d..1b6fd2500 100644 --- a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #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,110 @@ void CockatriceXml4Parser::parseFile(QIODevice &device) } } +static QSharedPointer 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 + 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) { while (!xml.atEnd()) { @@ -273,6 +380,49 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml) } } +static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const QSharedPointer &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) { if (set.isNull()) { @@ -399,7 +549,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 +577,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) { diff --git a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.h b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.h index b83f6929e..5b47e9090 100644 --- a/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.h +++ b/libcockatrice_card/libcockatrice/card/database/parser/cockatrice_xml_4.h @@ -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 elements from the XML stream. * @param xml The open QXmlStreamReader positioned at the element. diff --git a/libcockatrice_card/libcockatrice/card/format/format_legality_rules.cpp b/libcockatrice_card/libcockatrice/card/format/format_legality_rules.cpp new file mode 100644 index 000000000..27e2aa17b --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/format/format_legality_rules.cpp @@ -0,0 +1,46 @@ +#include "format_legality_rules.h" + +#include + +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; +} \ No newline at end of file diff --git a/libcockatrice_card/libcockatrice/card/format/format_legality_rules.h b/libcockatrice_card/libcockatrice/card/format/format_legality_rules.h new file mode 100644 index 000000000..018d3f01b --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/format/format_legality_rules.h @@ -0,0 +1,68 @@ +#ifndef COCKATRICE_FORMAT_LEGALITY_RULES_H +#define COCKATRICE_FORMAT_LEGALITY_RULES_H + +#include +#include +#include + +class CardInfo; +using CardInfoPtr = QSharedPointer; + +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 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 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 diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp index 333be017b..09326e619 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.cpp @@ -136,6 +136,8 @@ bool DeckList::readElement(QXmlStreamReader *xml) metadata.lastLoadedTimestamp = xml->readElementText(); } else if (childName == "deckname") { metadata.name = xml->readElementText(); + } else if (childName == "format") { + metadata.gameFormat = xml->readElementText(); } else if (childName == "comments") { metadata.comments = xml->readElementText(); } else if (childName == "bannerCard") { @@ -170,6 +172,7 @@ void writeMetadata(QXmlStreamWriter *xml, const DeckList::Metadata &metadata) { xml->writeTextElement("lastLoadedTimestamp", metadata.lastLoadedTimestamp); xml->writeTextElement("deckname", metadata.name); + xml->writeTextElement("format", metadata.gameFormat); xml->writeStartElement("bannerCard"); xml->writeAttribute("providerId", metadata.bannerCard.providerId); xml->writeCharacters(metadata.bannerCard.name); @@ -594,15 +597,16 @@ DecklistCardNode *DeckList::addCard(const QString &cardName, const int position, const QString &cardSetName, const QString &cardSetCollectorNumber, - const QString &cardProviderId) + const QString &cardProviderId, + const bool formatLegal) { auto *zoneNode = dynamic_cast(root->findChild(zoneName)); if (zoneNode == nullptr) { zoneNode = new InnerDecklistNode(zoneName, root); } - auto *node = - new DecklistCardNode(cardName, 1, zoneNode, position, cardSetName, cardSetCollectorNumber, cardProviderId); + auto *node = new DecklistCardNode(cardName, 1, zoneNode, position, cardSetName, cardSetCollectorNumber, + cardProviderId, formatLegal); refreshDeckHash(); return node; diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h index 6f90b6b14..e4206ebc6 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h +++ b/libcockatrice_deck_list/libcockatrice/deck_list/deck_list.h @@ -126,6 +126,7 @@ public: { QString name; ///< User-defined deck name. 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. QStringList tags; ///< User-defined tags for deck classification. QString lastLoadedTimestamp; ///< Timestamp string of last load. @@ -181,6 +182,10 @@ public: { metadata.lastLoadedTimestamp = _lastLoadedTimestamp; } + void setGameFormat(const QString &_gameFormat = QString()) + { + metadata.gameFormat = _gameFormat; + } ///@} /// @brief Construct an empty deck. @@ -219,6 +224,10 @@ public: { return metadata.lastLoadedTimestamp; } + QString getGameFormat() const + { + return metadata.gameFormat; + } ///@} bool isBlankDeck() const @@ -277,7 +286,8 @@ public: int position, const QString &cardSetName = QString(), const QString &cardSetCollectorNumber = QString(), - const QString &cardProviderId = QString()); + const QString &cardProviderId = QString(), + const bool formatLegal = true); bool deleteNode(AbstractDecklistNode *node, InnerDecklistNode *rootNode = nullptr); ///@} diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.h b/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.h index 1d201a4c1..88d8b0930 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.h +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/abstract_deck_list_card_node.h @@ -88,6 +88,12 @@ public: /// @param _cardSetNumber Set the collector number. 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. * diff --git a/libcockatrice_deck_list/libcockatrice/deck_list/tree/deck_list_card_node.h b/libcockatrice_deck_list/libcockatrice/deck_list/tree/deck_list_card_node.h index c2fe038e6..b3d42b89a 100644 --- a/libcockatrice_deck_list/libcockatrice/deck_list/tree/deck_list_card_node.h +++ b/libcockatrice_deck_list/libcockatrice/deck_list/tree/deck_list_card_node.h @@ -51,6 +51,7 @@ class DecklistCardNode : public AbstractDecklistCardNode QString cardSetShortName; ///< Short set code (e.g., "NEO"). QString cardSetNumber; ///< Collector number within the set. QString cardProviderId; ///< External provider identifier (e.g., UUID). + bool formatLegal; ///< Format legality public: /** @@ -63,6 +64,7 @@ public: * @param _cardSetShortName Short set code (e.g., "NEO"). * @param _cardSetNumber Collector number within the set. * @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 * the parent’s children list automatically. @@ -73,10 +75,11 @@ public: int position = -1, QString _cardSetShortName = QString(), QString _cardSetNumber = QString(), - QString _cardProviderId = QString()) + QString _cardProviderId = QString(), + bool _formatLegality = true) : AbstractDecklistCardNode(_parent, position), name(std::move(_name)), number(_number), 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; } + /// @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. [[nodiscard]] bool isDeckHeader() const override { 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 2856198b8..a066d7497 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.cpp @@ -168,6 +168,10 @@ QVariant DeckListModel::data(const QModelIndex &index, int role) const return card->depth(); } + case DeckRoles::IsLegalRole: { + return card->getFormatLegality(); + } + default: { return {}; } @@ -268,6 +272,7 @@ 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()); @@ -414,8 +419,9 @@ QModelIndex DeckListModel::addCard(const ExactCard &card, const QString &zoneNam // Determine the correct index int insertRow = findSortedInsertRow(groupNode, cardInfo); - auto *decklistCard = deckList->addCard(cardInfo->getName(), zoneName, insertRow, cardSetName, - printingInfo.getProperty("num"), printingInfo.getProperty("uuid")); + auto *decklistCard = + deckList->addCard(cardInfo->getName(), zoneName, insertRow, cardSetName, printingInfo.getProperty("num"), + printingInfo.getProperty("uuid"), isCardLegalForCurrentFormat(cardInfo)); beginInsertRows(parentIndex, insertRow, insertRow); cardNode = new DecklistModelCardNode(decklistCard, groupNode, insertRow); @@ -532,6 +538,13 @@ void DeckListModel::setActiveGroupCriteria(DeckListModelGroupCriteria::Type newC rebuildTree(); } +void DeckListModel::setActiveFormat(const QString &_format) +{ + deckList->setGameFormat(_format); + refreshCardFormatLegalities(); + emitBackgroundUpdates(QModelIndex()); // start from root +} + void DeckListModel::cleanList() { setDeckList(new DeckList); @@ -595,4 +608,74 @@ QList DeckListModel::getZones() const [](auto zoneNode) { return zoneNode->getName(); }); return zones; -} \ No newline at end of file +} + +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(listRoot->at(i)); + for (int j = 0; j < currentZone->size(); j++) { + auto *currentCard = static_cast(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); + } + } +} diff --git a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h index 0b47faed4..c03497bd5 100644 --- a/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h +++ b/libcockatrice_models/libcockatrice/models/deck_list/deck_list_model.h @@ -164,6 +164,14 @@ public: { 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. @@ -209,6 +217,9 @@ public slots: */ void rebuildTree(); +public slots: + void setActiveFormat(const QString &_format); + signals: /** * @brief Emitted whenever the deck hash changes due to modifications in the model. @@ -301,6 +312,9 @@ public: [[nodiscard]] QList getCards() const; [[nodiscard]] QList getCardsForZone(const QString &zoneName) const; [[nodiscard]] QList 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. diff --git a/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index f316e439d..a5c6649b0 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -463,6 +463,69 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QList 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() { static ICardSetPriorityController *noOpController = new NoopCardSetPriorityController(); @@ -497,7 +560,8 @@ int OracleImporter::startImport() bool OracleImporter::saveToFile(const QString &fileName, const QString &sourceUrl, const QString &sourceVersion) { CockatriceXml4Parser parser(new NoopCardPreferenceProvider()); - return parser.saveToFile(sets, cards, fileName, sourceUrl, sourceVersion); + + return parser.saveToFile(createDefaultMagicFormats(), sets, cards, fileName, sourceUrl, sourceVersion); } void OracleImporter::clear() diff --git a/oracle/src/oracleimporter.h b/oracle/src/oracleimporter.h index 3ec6da6e1..e83958d73 100644 --- a/oracle/src/oracleimporter.h +++ b/oracle/src/oracleimporter.h @@ -154,6 +154,7 @@ public: int startImport(); bool saveToFile(const QString &fileName, const QString &sourceUrl, const QString &sourceVersion); int importCardsFromSet(const CardSetPtr ¤tSet, const QList &cardsList); + FormatRulesNameMap createDefaultMagicFormats(); const CardNameMap &getCardList() const { return cards;