From ccdda39e78fc24d7cf5e2ea610f1faf611aa5197 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sat, 13 Dec 2025 15:17:55 +0100 Subject: [PATCH] Deck format legality checker (#6166) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Deck legality checker. Took 51 seconds Took 1 minute Took 1 minute Took 5 minutes Took 3 minutes * Adjust format parsing. Took 8 minutes Took 3 seconds * toString() the xmlName Took 4 minutes * more toStrings() Took 5 minutes * Comments Took 3 minutes * Layout Took 2 minutes * Layout part 2: Electric boogaloo Took 59 seconds * Update cockatrice/src/interface/widgets/visual_database_display/visual_database_display_format_legality_filter_widget.cpp Co-authored-by: RickyRister <42636155+RickyRister@users.noreply.github.com> * Move layout. Took 4 minutes Took 10 seconds * Emit deckModified Took 6 minutes * Fix qOverloads Took 4 minutes * Fix qOverloads Took 12 seconds * Consider text and name in a special way. Took 11 minutes * Adjust "Any number of" oracle text Took 5 minutes * Store allowedCounts by format Took 15 minutes Took 6 seconds * Only restrict vintage. Took 2 minutes * Adjust for DBConverter. Took 6 minutes --------- Co-authored-by: Lukas Brübach Co-authored-by: RickyRister <42636155+RickyRister@users.noreply.github.com> --- cockatrice/CMakeLists.txt | 1 + .../deck_editor_deck_dock_widget.cpp | 67 +++++- .../deck_editor_deck_dock_widget.h | 3 + .../deck_editor/deck_list_style_proxy.cpp | 3 +- .../tabs/api/archidekt/tab_archidekt.cpp | 4 +- ..._display_format_legality_filter_widget.cpp | 205 ++++++++++++++++++ ...se_display_format_legality_filter_widget.h | 43 ++++ ...tabase_display_main_type_filter_widget.cpp | 2 +- ...ual_database_display_set_filter_widget.cpp | 2 +- ...atabase_display_sub_type_filter_widget.cpp | 2 +- .../visual_database_display_widget.cpp | 2 + .../visual_database_display_widget.h | 2 + .../visual_deck_editor_sample_hand_widget.cpp | 4 +- ...ual_deck_storage_quick_settings_widget.cpp | 6 +- dbconverter/src/main.h | 72 +++++- 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 | 179 ++++++++++++++- .../card/database/parser/cockatrice_xml_4.h | 4 +- .../card/format/format_legality_rules.cpp | 53 +++++ .../card/format/format_legality_rules.h | 73 +++++++ .../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 | 105 ++++++++- .../models/deck_list/deck_list_model.h | 14 ++ oracle/src/oracleimporter.cpp | 72 +++++- oracle/src/oracleimporter.h | 1 + 37 files changed, 987 insertions(+), 35 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..268a13d5f 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; @@ -208,13 +214,16 @@ void DeckEditorDeckDockWidget::createDeckDock() upperLayout->addWidget(commentsLabel, 1, 0); upperLayout->addWidget(commentsEdit, 1, 1); - upperLayout->addWidget(bannerCardLabel, 2, 0); - upperLayout->addWidget(bannerCardComboBox, 2, 1); + upperLayout->addWidget(formatLabel, 2, 0); + upperLayout->addWidget(formatComboBox, 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,46 @@ 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 + formatComboBox->addItem("", ""); + for (auto it = allFormats.constBegin(); it != allFormats.constEnd(); ++it) { + QString displayText = QString("%1").arg(it.key()); + 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 + } + emit deckModified(); + }); } ExactCard DeckEditorDeckDockWidget::getCurrentCard() @@ -466,6 +515,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 +779,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/tabs/api/archidekt/tab_archidekt.cpp b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp index 1376efd47..e6615fa7b 100644 --- a/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp +++ b/cockatrice/src/interface/widgets/tabs/api/archidekt/tab_archidekt.cpp @@ -214,7 +214,7 @@ TabArchidekt::TabArchidekt(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor) minDeckSizeLogicCombo->addItems({"Exact", "≥", "≤"}); // Exact = unset, ≥ = GTE, ≤ = LTE minDeckSizeLogicCombo->setCurrentIndex(1); // default GTE - connect(minDeckSizeSpin, QOverload::of(&QSpinBox::valueChanged), this, &TabArchidekt::doSearch); + connect(minDeckSizeSpin, qOverload(&QSpinBox::valueChanged), this, &TabArchidekt::doSearch); connect(minDeckSizeLogicCombo, &QComboBox::currentTextChanged, this, &TabArchidekt::doSearch); // Page number @@ -224,7 +224,7 @@ TabArchidekt::TabArchidekt(TabSupervisor *_tabSupervisor) : Tab(_tabSupervisor) pageSpin->setRange(1, 9999); pageSpin->setValue(1); - connect(pageSpin, QOverload::of(&QSpinBox::valueChanged), this, &TabArchidekt::doSearch); + connect(pageSpin, qOverload(&QSpinBox::valueChanged), this, &TabArchidekt::doSearch); // Page display currentPageDisplay = new QWidget(container); 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..1f1b7b94c --- /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(&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_main_type_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_main_type_filter_widget.cpp index 32652c346..368ac8719 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_main_type_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_main_type_filter_widget.cpp @@ -33,7 +33,7 @@ VisualDatabaseDisplayMainTypeFilterWidget::VisualDatabaseDisplayMainTypeFilterWi spinBox->setMaximum(getMaxMainTypeCount()); // Set the max value dynamically spinBox->setValue(150); layout->addWidget(spinBox); - connect(spinBox, QOverload::of(&QSpinBox::valueChanged), this, + connect(spinBox, qOverload(&QSpinBox::valueChanged), this, &VisualDatabaseDisplayMainTypeFilterWidget::updateMainTypeButtonsVisibility); // Create the toggle button for Exact Match/Includes mode diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_set_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_set_filter_widget.cpp index 1ea6e8e67..dee89f06a 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_set_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_set_filter_widget.cpp @@ -27,7 +27,7 @@ VisualDatabaseDisplayRecentSetFilterSettingsWidget::VisualDatabaseDisplayRecentS filterToMostRecentSetsAmount->setMaximum(100); filterToMostRecentSetsAmount->setValue( SettingsCache::instance().getVisualDatabaseDisplayFilterToMostRecentSetsAmount()); - connect(filterToMostRecentSetsAmount, QOverload::of(&QSpinBox::valueChanged), &SettingsCache::instance(), + connect(filterToMostRecentSetsAmount, qOverload(&QSpinBox::valueChanged), &SettingsCache::instance(), &SettingsCache::setVisualDatabaseDisplayFilterToMostRecentSetsAmount); layout->addWidget(filterToMostRecentSetsCheckBox); diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_sub_type_filter_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_sub_type_filter_widget.cpp index 7a88b51cf..b34bb65d3 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_sub_type_filter_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_sub_type_filter_widget.cpp @@ -26,7 +26,7 @@ VisualDatabaseDisplaySubTypeFilterWidget::VisualDatabaseDisplaySubTypeFilterWidg spinBox->setMaximum(getMaxSubTypeCount()); spinBox->setValue(150); layout->addWidget(spinBox); - connect(spinBox, QOverload::of(&QSpinBox::valueChanged), this, + connect(spinBox, qOverload(&QSpinBox::valueChanged), this, &VisualDatabaseDisplaySubTypeFilterWidget::updateSubTypeButtonsVisibility); // Create search box 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/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp index 341b94a3b..778706f9a 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_sample_hand_widget.cpp @@ -24,9 +24,9 @@ VisualDeckEditorSampleHandWidget::VisualDeckEditorSampleHandWidget(QWidget *pare handSizeSpinBox = new QSpinBox(this); handSizeSpinBox->setValue(SettingsCache::instance().getVisualDeckEditorSampleHandSize()); handSizeSpinBox->setMinimum(1); - connect(handSizeSpinBox, QOverload::of(&QSpinBox::valueChanged), &SettingsCache::instance(), + connect(handSizeSpinBox, qOverload(&QSpinBox::valueChanged), &SettingsCache::instance(), &SettingsCache::setVisualDeckEditorSampleHandSize); - connect(handSizeSpinBox, QOverload::of(&QSpinBox::valueChanged), this, + connect(handSizeSpinBox, qOverload(&QSpinBox::valueChanged), this, &VisualDeckEditorSampleHandWidget::updateDisplay); resetAndHandSizeLayout->addWidget(handSizeSpinBox); diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp index c1e9bfa6a..a396b8cd3 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/visual_deck_storage_quick_settings_widget.cpp @@ -61,10 +61,10 @@ VisualDeckStorageQuickSettingsWidget::VisualDeckStorageQuickSettingsWidget(QWidg unusedColorIdentitiesOpacitySpinBox->setMaximum(100); unusedColorIdentitiesOpacitySpinBox->setValue( SettingsCache::instance().getVisualDeckStorageUnusedColorIdentitiesOpacity()); - connect(unusedColorIdentitiesOpacitySpinBox, QOverload::of(&QSpinBox::valueChanged), this, + connect(unusedColorIdentitiesOpacitySpinBox, qOverload(&QSpinBox::valueChanged), this, &VisualDeckStorageQuickSettingsWidget::unusedColorIdentitiesOpacityChanged); - connect(unusedColorIdentitiesOpacitySpinBox, QOverload::of(&QSpinBox::valueChanged), - &SettingsCache::instance(), &SettingsCache::setVisualDeckStorageUnusedColorIdentitiesOpacity); + connect(unusedColorIdentitiesOpacitySpinBox, qOverload(&QSpinBox::valueChanged), &SettingsCache::instance(), + &SettingsCache::setVisualDeckStorageUnusedColorIdentitiesOpacity); unusedColorIdentitiesOpacityLabel->setBuddy(unusedColorIdentitiesOpacitySpinBox); diff --git a/dbconverter/src/main.h b/dbconverter/src/main.h index 7a5809b1b..d6e734316 100644 --- a/dbconverter/src/main.h +++ b/dbconverter/src/main.h @@ -5,6 +5,10 @@ #include #include +static const QList kConstructedCounts = {{4, "legal"}, {0, "banned"}}; + +static const QList kSingletonCounts = {{1, "legal"}, {0, "banned"}}; + class CardDatabaseConverter : public CardDatabase { public: @@ -23,7 +27,73 @@ 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 = "A deck can have 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 minDeck = 60, int maxDeck = -1, int maxSideboardSize = 15, + const QList &allowedCounts = kConstructedCounts) -> FormatRulesPtr { + FormatRulesPtr f(new FormatRules); + f->formatName = name; + f->allowedCounts = allowedCounts; + 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("Standard"); + makeFormat("Modern"); + makeFormat("Legacy"); + makeFormat("Pioneer"); + makeFormat("Historic"); + makeFormat("Timeless"); + makeFormat("Future"); + makeFormat("OldSchool"); + makeFormat("Premodern"); + makeFormat("Pauper"); + makeFormat("Penny"); + + // ----------------- Singleton formats ----------------- + makeFormat("Commander", 100, 100, 15, kSingletonCounts); + makeFormat("Duel", 100, 100, 15, kSingletonCounts); + makeFormat("Brawl", 60, 60, 15, kSingletonCounts); + makeFormat("StandardBrawl", 60, 60, 15, kSingletonCounts); + makeFormat("Oathbreaker", 60, 60, 15, kSingletonCounts); + makeFormat("PauperCommander", 100, 100, 15, kSingletonCounts); + makeFormat("Predh", 100, 100, 15, kSingletonCounts); + + // ----------------- Restricted formats ----------------- + makeFormat("Vintage", 60, -1, 15, {{4, "legal"}, {1, "restricted"}, {0, "banned"}}); + + 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..0d46aad0f 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,116 @@ 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().toString() == "format") { + break; + } + + if (token != QXmlStreamReader::StartElement) { + continue; + } + + QString xmlName = xml.name().toString(); + + if (xmlName == "minDeckSize") { + rulesPtr->minDeckSize = xml.readElementText().toInt(); + } else if (xmlName == "maxDeckSize") { + QString text = xml.readElementText(); + rulesPtr->maxDeckSize = text.toInt(); + } else if (xmlName == "maxSideboardSize") { + rulesPtr->maxSideboardSize = xml.readElementText().toInt(); + } else if (xmlName == "allowedCounts") { + while (!xml.atEnd()) { + token = xml.readNext(); + + if (token == QXmlStreamReader::EndElement && xml.name().toString() == "allowedCounts") { + break; + } + + if (token == QXmlStreamReader::StartElement && xml.name().toString() == "count") { + + AllowedCount c; + + QString maxAttr = xml.attributes().value("max").toString(); + c.max = (maxAttr == "unlimited") ? -1 : maxAttr.toInt(); + + c.label = xml.readElementText().trimmed(); + + rulesPtr->allowedCounts.append(c); + } + } + } else if (xmlName == "exceptions") { + while (!xml.atEnd()) { + token = xml.readNext(); + + if (token == QXmlStreamReader::EndElement && xml.name().toString() == "exceptions") { + break; + } + + if (token == QXmlStreamReader::StartElement && xml.name().toString() == "exception") { + ExceptionRule ex; + + while (!xml.atEnd()) { + token = xml.readNext(); + + if (token == QXmlStreamReader::EndElement && xml.name().toString() == "exception") { + break; + } + + if (token == QXmlStreamReader::StartElement) { + QString ename = xml.name().toString(); + + if (ename == "maxCopies") { + QString text = xml.readElementText(); + ex.maxCopies = (text == "unlimited") ? -1 : text.toInt(); + } else if (ename == "cardCondition") { + CardCondition cond; + cond.field = xml.attributes().value("field").toString(); + cond.matchType = xml.attributes().value("match").toString(); + cond.value = xml.attributes().value("value").toString(); + ex.conditions.append(cond); + xml.skipCurrentElement(); + } else { + xml.skipCurrentElement(); + } + } + } + + rulesPtr->exceptions.append(ex); + } + } + } else { + xml.skipCurrentElement(); + } + } + + return rulesPtr; +} + +void CockatriceXml4Parser::loadFormats(QXmlStreamReader &xml) +{ + while (!xml.atEnd()) { + if (xml.readNext() == QXmlStreamReader::EndElement) { + break; + } + + if (xml.name().toString() == "format") { + auto rulesPtr = parseFormat(xml); + emit addFormat(rulesPtr); + } + } +} + void CockatriceXml4Parser::loadSetsFromXml(QXmlStreamReader &xml) { while (!xml.atEnd()) { @@ -273,6 +386,59 @@ void CockatriceXml4Parser::loadCardsFromXml(QXmlStreamReader &xml) } } +static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const QSharedPointer &rulesPtr) +{ + if (rulesPtr.isNull()) { + qCWarning(CockatriceXml4Log) << "&operator<< FormatRules is nullptr"; + return xml; + } + + const FormatRules &rules = *rulesPtr; + + xml.writeStartElement("format"); + if (!rules.formatName.isEmpty()) { + xml.writeAttribute("formatName", rules.formatName); + } + + xml.writeTextElement("minDeckSize", QString::number(rules.minDeckSize)); + xml.writeTextElement("maxDeckSize", rules.maxDeckSize >= 0 ? QString::number(rules.maxDeckSize) : "0"); + xml.writeTextElement("maxSideboardSize", QString::number(rules.maxSideboardSize)); + if (!rules.allowedCounts.isEmpty()) { + xml.writeStartElement("allowedCounts"); + + for (const AllowedCount &c : rules.allowedCounts) { + xml.writeStartElement("count"); + xml.writeAttribute("max", c.max == -1 ? "unlimited" : QString::number(c.max)); + xml.writeCharacters(c.label); + xml.writeEndElement(); // count + } + + xml.writeEndElement(); // allowedCounts + } + + if (!rules.exceptions.isEmpty()) { + xml.writeStartElement("exceptions"); + for (const ExceptionRule &ex : rules.exceptions) { + xml.writeStartElement("exception"); + xml.writeTextElement("maxCopies", ex.maxCopies == -1 ? "unlimited" : QString::number(ex.maxCopies)); + + for (const CardCondition &cond : ex.conditions) { + xml.writeStartElement("cardCondition"); + xml.writeAttribute("field", cond.field); + xml.writeAttribute("match", cond.matchType); + xml.writeAttribute("value", cond.value); + xml.writeEndElement(); // cardCondition + } + + xml.writeEndElement(); // exception + } + xml.writeEndElement(); // exceptions + } + + xml.writeEndElement(); // format + return xml; +} + static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardSetPtr &set) { if (set.isNull()) { @@ -399,7 +565,8 @@ static QXmlStreamWriter &operator<<(QXmlStreamWriter &xml, const CardInfoPtr &in return xml; } -bool CockatriceXml4Parser::saveToFile(SetNameMap _sets, +bool CockatriceXml4Parser::saveToFile(FormatRulesNameMap _formats, + SetNameMap _sets, CardNameMap cards, const QString &fileName, const QString &sourceUrl, @@ -426,6 +593,14 @@ bool CockatriceXml4Parser::saveToFile(SetNameMap _sets, xml.writeTextElement("sourceVersion", sourceVersion); xml.writeEndElement(); + if (_formats.count() > 0) { + xml.writeStartElement("formats"); + for (FormatRulesPtr format : _formats) { + xml << format; + } + xml.writeEndElement(); + } + if (_sets.count() > 0) { xml.writeStartElement("sets"); for (CardSetPtr set : _sets) { 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..a6b3bb038 --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/format/format_legality_rules.cpp @@ -0,0 +1,53 @@ +#include "format_legality_rules.h" + +#include + +bool cardMatchesCondition(const CardInfo &card, const CardCondition &cond) +{ + CardMatchType type = matchTypeFromString(cond.matchType); + QString fieldValue; + if (cond.field == "name") { + fieldValue = card.getName(); + } else if (cond.field == "text") { + fieldValue = card.getText(); + } else { + fieldValue = card.getProperty(cond.field); + } + + switch (type) { + case CardMatchType::Equals: + return fieldValue == cond.value; + case CardMatchType::NotEquals: + return fieldValue != cond.value; + case CardMatchType::Contains: + return fieldValue.contains(cond.value, Qt::CaseInsensitive); + case CardMatchType::NotContains: + return !fieldValue.contains(cond.value, Qt::CaseInsensitive); + case CardMatchType::Regex: { + QRegularExpression re(cond.value, QRegularExpression::CaseInsensitiveOption); + return re.match(fieldValue).hasMatch(); + } + default: + return false; + } +} + +bool exceptionAppliesToCard(const CardInfo &card, const ExceptionRule &rule) +{ + for (const CardCondition &cond : rule.conditions) { + if (!cardMatchesCondition(card, cond)) { + return false; // all conditions must match + } + } + return true; +} + +bool cardHasAnyException(const CardInfo &card, const FormatRules &format) +{ + for (const ExceptionRule &rule : format.exceptions) { + if (exceptionAppliesToCard(card, rule)) { + return true; + } + } + return false; +} \ 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..16f2359ab --- /dev/null +++ b/libcockatrice_card/libcockatrice/card/format/format_legality_rules.h @@ -0,0 +1,73 @@ +#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 AllowedCount +{ + int max = 0; // 4, 1, 0, or -1 for unlimited + QString label; // "legal", "restricted", "banned" +}; + +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; + + QList allowedCounts; + + 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..05ee480a5 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,90 @@ 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; +} + +int maxAllowedForLegality(const FormatRules &format, const QString &legality) +{ + for (const AllowedCount &c : format.allowedCounts) { + if (c.label == legality) { + return c.max; + } + } + return -1; // unknown legality → treat as illegal +} + + +bool DeckListModel::isCardQuantityLegalForCurrentFormat(const CardInfoPtr cardInfo, int quantity) +{ + auto formatRules = CardDatabaseManager::query()->getFormat(deckList->getGameFormat()); + + if (!formatRules) { + return true; + } + + // Exceptions always win + if (cardHasAnyException(*cardInfo, *formatRules)) { + return true; + } + + const QString legalityProp = "format-" + deckList->getGameFormat(); + if (!cardInfo->getProperties().contains(legalityProp)) { + return false; + } + + const QString legality = cardInfo->getProperty(legalityProp); + + int maxAllowed = maxAllowedForLegality(*formatRules, legality); + + if (maxAllowed == -1) { + return false; + } + + if (maxAllowed < 0) { // unlimited + return true; + } + + return quantity <= maxAllowed; +} + +void DeckListModel::refreshCardFormatLegalities() +{ + 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..813df49ae 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -13,6 +13,10 @@ #include #include +static const QList kConstructedCounts = {{4, "legal"}, {0, "banned"}}; + +static const QList kSingletonCounts = {{1, "legal"}, {0, "banned"}}; + SplitCardPart::SplitCardPart(const QString &_name, const QString &_text, const QVariantHash &_properties, @@ -463,6 +467,71 @@ 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 = "A deck can have 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 minDeck = 60, int maxDeck = -1, int maxSideboardSize = 15, + const QList &allowedCounts = kConstructedCounts) -> FormatRulesPtr { + FormatRulesPtr f(new FormatRules); + f->formatName = name; + f->allowedCounts = allowedCounts; + 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("Standard"); + makeFormat("Modern"); + makeFormat("Legacy"); + makeFormat("Pioneer"); + makeFormat("Historic"); + makeFormat("Timeless"); + makeFormat("Future"); + makeFormat("OldSchool"); + makeFormat("Premodern"); + makeFormat("Pauper"); + makeFormat("Penny"); + + // ----------------- Singleton formats ----------------- + makeFormat("Commander", 100, 100, 15, kSingletonCounts); + makeFormat("Duel", 100, 100, 15, kSingletonCounts); + makeFormat("Brawl", 60, 60, 15, kSingletonCounts); + makeFormat("StandardBrawl", 60, 60, 15, kSingletonCounts); + makeFormat("Oathbreaker", 60, 60, 15, kSingletonCounts); + makeFormat("PauperCommander", 100, 100, 15, kSingletonCounts); + makeFormat("Predh", 100, 100, 15, kSingletonCounts); + + // ----------------- Restricted formats ----------------- + makeFormat("Vintage", 60, -1, 15, {{4, "legal"}, {1, "restricted"}, {0, "banned"}}); + + return defaultFormatRulesNameMap; +} + int OracleImporter::startImport() { static ICardSetPriorityController *noOpController = new NoopCardSetPriorityController(); @@ -497,7 +566,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;