From 827e44a4d32096daeeaae1c7ac69df10d720a0f7 Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Sun, 9 Aug 2026 00:46:17 +0200 Subject: [PATCH] [DeckEditor] Use CommanderSpellbook.com to estimate bracket if format is 'commander' (#6415) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [DeckEditor] Use CommanderSpellbook.com to estimate bracket if format is 'commander' Took 2 minutes Took 16 minutes * Convert json data holder to structs, rename variables, extract widget - Extract bracket estimation UI from DeckEditorDeckDockWidget into a new CommanderBracketWidget - Move CommanderSpellbook integration settings from CardsDisplaySettings to DeckEditorSettings (matching the settings refactor on master) - Rename CommanderSpellbook integration variables to drop the redundant 'DeckEditor' prefix Took 4 minutes # Commit time for manual adjustment: # Took 6 minutes # Commit time for manual adjustment: # Took 8 minutes --------- Co-authored-by: Lukas Brübach --- cockatrice/CMakeLists.txt | 10 + .../src/client/settings/cache_settings.cpp | 8 + .../src/client/settings/cache_settings.h | 6 + .../deck_editor_deck_dock_widget.cpp | 29 +- .../deck_editor_deck_dock_widget.h | 3 + .../widgets/deck_editor/deck_state_manager.h | 8 + .../user_interface_settings_page.cpp | 93 ++++++ .../user_interface_settings_page.h | 8 + .../api_response/card_in_deck_request.cpp | 17 ++ .../api_response/card_in_deck_request.h | 14 + .../commander_spellbook_card_result.cpp | 26 ++ .../commander_spellbook_card_result.h | 29 ++ .../commander_spellbook_deck_request.cpp | 95 +++++++ .../commander_spellbook_deck_request.h | 19 ++ ...nder_spellbook_estimate_bracket_result.cpp | 93 ++++++ ...mander_spellbook_estimate_bracket_result.h | 32 +++ .../commander_spellbook_variant_result.cpp | 35 +++ .../commander_spellbook_variant_result.h | 39 +++ .../commander_bracket_service.cpp | 53 ++++ .../commander_bracket_service.h | 45 +++ .../commander_bracket_widget.cpp | 269 ++++++++++++++++++ .../commander_bracket_widget.h | 46 +++ .../commander_spellbook_api_accessor.cpp | 75 +++++ .../commander_spellbook_api_accessor.h | 37 +++ .../commander_spellbook_bracket_explainer.cpp | 120 ++++++++ .../commander_spellbook_bracket_explainer.h | 43 +++ .../handle_commander_brackets.cpp | 62 ++++ .../handle_commander_brackets.h | 31 ++ cockatrice/src/interface/window_main.cpp | 14 + cockatrice/src/interface/window_main.h | 1 + libcockatrice_settings/CMakeLists.txt | 2 + .../settings/commander_bracket_settings.cpp | 194 +++++++++++++ .../settings/commander_bracket_settings.h | 56 ++++ .../settings/deck_editor_settings.cpp | 24 ++ .../settings/deck_editor_settings.h | 14 + 35 files changed, 1642 insertions(+), 8 deletions(-) create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.h create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.h create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.h create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.h create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.h create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.h create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.h create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.h create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.h create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp create mode 100644 cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.h create mode 100644 libcockatrice_settings/libcockatrice/settings/commander_bracket_settings.cpp create mode 100644 libcockatrice_settings/libcockatrice/settings/commander_bracket_settings.h diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index af24dfc26..ed8e49f2d 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -318,6 +318,13 @@ set(cockatrice_SOURCES src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_entry_display_widget.cpp src/interface/widgets/tabs/api/archidekt/display/archidekt_api_response_deck_listings_display_widget.cpp src/interface/widgets/tabs/api/archidekt/display/archidekt_deck_preview_image_display_widget.cpp + src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.cpp + src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.cpp + src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.cpp + src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.cpp + src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.cpp + src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.cpp + src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.cpp src/interface/widgets/tabs/api/edhrec/api_response/archidekt_links/edhrec_api_response_archidekt_links.cpp src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_average_deck_api_response.cpp src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_deck_api_response.cpp @@ -364,6 +371,9 @@ set(cockatrice_SOURCES src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp src/interface/key_signals.cpp src/interface/logger.cpp + src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.cpp + src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp + src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.cpp src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.h src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp diff --git a/cockatrice/src/client/settings/cache_settings.cpp b/cockatrice/src/client/settings/cache_settings.cpp index 1052629a5..eaa5d96ac 100644 --- a/cockatrice/src/client/settings/cache_settings.cpp +++ b/cockatrice/src/client/settings/cache_settings.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -143,6 +144,7 @@ SettingsCache::SettingsCache() visualDeckStorageSettings = new VisualDeckStorageSettings(settingsPath, this); appearanceSettings = new AppearanceSettings(settingsPath, this); networkSettings = new NetworkSettings(settingsPath, this); + commanderBracketSettings = new CommanderBracketSettings(settingsPath, this); // Forward ICardDatabasePathProvider signal from PathsSettings connect(pathsSettings, &PathsSettings::cardDatabasePathChanged, this, @@ -155,6 +157,12 @@ SettingsCache::SettingsCache() themeName = appearanceSettings->getThemeName(); + auto definitions = commanderBracketSettings->loadDefinitions(); + if (definitions.isEmpty()) { + definitions = CommanderBracketSettings::defaultDefinitions(); + } + commanderBracketSettings->reloadDefinitions(definitions); + loadPaths(); } diff --git a/cockatrice/src/client/settings/cache_settings.h b/cockatrice/src/client/settings/cache_settings.h index f2886d167..23cdb4dbf 100644 --- a/cockatrice/src/client/settings/cache_settings.h +++ b/cockatrice/src/client/settings/cache_settings.h @@ -28,6 +28,7 @@ class CardDatabaseSettings; class CardOverrideSettings; class CardsDisplaySettings; class ChatSettings; +class CommanderBracketSettings; class DebugSettings; class DeckEditorSettings; class DownloadSettings; @@ -83,6 +84,7 @@ private: VisualDeckStorageSettings *visualDeckStorageSettings; AppearanceSettings *appearanceSettings; NetworkSettings *networkSettings; + CommanderBracketSettings *commanderBracketSettings; QString themeName; @@ -150,6 +152,10 @@ public: [[nodiscard]] VisualDeckStorageSettings &visualDeckStorage() const; [[nodiscard]] AppearanceSettings &appearance() const; [[nodiscard]] NetworkSettings &network() const; + [[nodiscard]] CommanderBracketSettings &commanderBrackets() const + { + return *commanderBracketSettings; + } [[nodiscard]] bool getIsPortableBuild() const { 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 e33c09426..fc53b296f 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 @@ -2,6 +2,8 @@ #include "../../../client/settings/cache_settings.h" #include "../../../client/settings/shortcuts_settings.h" +#include "../settings_page/user_interface_settings_page.h" +#include "../tabs/api/commander_spellbook/commander_bracket_widget.h" #include "deck_list_style_proxy.h" #include "deck_state_manager.h" @@ -134,6 +136,8 @@ void DeckEditorDeckDockWidget::createDeckDock() formatComboBox->addItem(tr("Loading Database...")); formatComboBox->setEnabled(false); // Disable until loaded + commanderBracketWidget = new CommanderBracketWidget(this); + commentsLabel = new QLabel(); commentsLabel->setObjectName("commentsLabel"); commentsEdit = new QTextEdit; @@ -219,13 +223,15 @@ void DeckEditorDeckDockWidget::createDeckDock() upperLayout->addWidget(formatLabel, 2, 0); upperLayout->addWidget(formatComboBox, 2, 1); - upperLayout->addWidget(bannerCardLabel, 3, 0); - upperLayout->addWidget(bannerCardComboBox, 3, 1); + upperLayout->addWidget(commanderBracketWidget, 3, 0, 1, 2); - upperLayout->addWidget(deckTagsDisplayWidget, 4, 1); + upperLayout->addWidget(bannerCardLabel, 4, 0); + upperLayout->addWidget(bannerCardComboBox, 4, 1); - upperLayout->addWidget(activeGroupCriteriaLabel, 5, 0); - upperLayout->addWidget(activeGroupCriteriaComboBox, 5, 1); + upperLayout->addWidget(deckTagsDisplayWidget, 5, 1); + + upperLayout->addWidget(activeGroupCriteriaLabel, 6, 0); + upperLayout->addWidget(activeGroupCriteriaComboBox, 6, 1); hashLabel1 = new QLabel(); hashLabel1->setObjectName("hashLabel1"); @@ -303,15 +309,19 @@ void DeckEditorDeckDockWidget::initializeFormats() // Ensure no selection is visible initially formatComboBox->setCurrentIndex(-1); } - connect(formatComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, [this](int index) { + QString formatKey; if (index >= 0) { - QString formatKey = formatComboBox->itemData(index).toString(); + formatKey = formatComboBox->itemData(index).toString(); deckStateManager->setFormat(formatKey); } else { deckStateManager->setFormat(""); // clear format if deselected } + + commanderBracketWidget->setDeck(deckStateManager->getDeckListShared()); }); + + commanderBracketWidget->setDeck(deckStateManager->getDeckListShared()); } ExactCard DeckEditorDeckDockWidget::getCurrentCard() @@ -493,6 +503,8 @@ void DeckEditorDeckDockWidget::syncDisplayWidgetsToModel() formatComboBox->setCurrentIndex(formatComboBox->findData(deckStateManager->getMetadata().gameFormat)); formatComboBox->blockSignals(false); + commanderBracketWidget->setDeck(deckStateManager->getDeckListShared()); + deckTagsDisplayWidget->blockSignals(true); deckTagsDisplayWidget->setTags(deckStateManager->getMetadata().tags); deckTagsDisplayWidget->blockSignals(false); @@ -746,6 +758,7 @@ void DeckEditorDeckDockWidget::retranslateUi() commentsLabel->setText(tr("&Comments:")); activeGroupCriteriaLabel->setText(tr("Group by:")); formatLabel->setText(tr("Format:")); + commanderBracketWidget->retranslateUi(); hashLabel1->setText(tr("Hash:")); @@ -753,4 +766,4 @@ void DeckEditorDeckDockWidget::retranslateUi() aDecrement->setText(tr("&Decrement number")); aRemoveCard->setText(tr("&Remove row")); aSwapCard->setText(tr("Swap card to/from sideboard")); -} \ No newline at end of file +} 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 8dddf5882..540199f0d 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 @@ -21,6 +21,7 @@ #include #include +class CommanderBracketWidget; class DeckListModel; class AbstractTabDeckEditor; class DeckEditorDeckDockWidget : public QDockWidget @@ -89,6 +90,8 @@ private: QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard; + CommanderBracketWidget *commanderBracketWidget; + DeckListModel *getModel() const; [[nodiscard]] QModelIndexList getSelectedCardNodeSourceIndices() const; void offsetCountAtIndex(const QModelIndex &idx, bool isIncrement); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h index 10312d0a0..6fce6be57 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_state_manager.h @@ -57,6 +57,14 @@ public: */ const DeckList &getDeckList() const; + /** + * @brief Gets the underlying DeckList. + */ + QSharedPointer getDeckListShared() const + { + return deckList; + } + /** * @brief Creates a LoadedDeck containing the contents of the current deck and the current LoadInfo. * diff --git a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp index cfd855d33..634df0b15 100644 --- a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp +++ b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.cpp @@ -2,6 +2,7 @@ #include "../../../client/settings/cache_settings.h" #include "../interface/widgets/tabs/tab_supervisor.h" +#include "../tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.h" #include #include @@ -161,6 +162,57 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() connect(&defaultDeckEditorTypeSelector, QOverload::of(&QComboBox::currentIndexChanged), &SettingsCache::instance().deckEditor(), &DeckEditorSettings::setDefaultDeckEditorType); + commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setText("?"); + commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setAutoRaise(true); + commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setEnabled(false); + + // Add items with userData = internal enum + commanderSpellbookIntegrationEnabledSelector.addItem(tr("Disabled"), + commanderSpellbookIntegrationEnabledIndexDisabled); + commanderSpellbookIntegrationEnabledSelector.addItem(tr("Enabled"), + commanderSpellbookIntegrationEnabledIndexEnabled); + commanderSpellbookIntegrationEnabledSelector.addItem(tr("Automatic"), + commanderSpellbookIntegrationEnabledIndexAutomatic); + + int storedMode = SettingsCache::instance().deckEditor().getCommanderSpellbookIntegrationEnabled(); + for (int i = 0; i < commanderSpellbookIntegrationEnabledSelector.count(); ++i) { + if (commanderSpellbookIntegrationEnabledSelector.itemData(i).toInt() == storedMode) { + commanderSpellbookIntegrationEnabledSelector.setCurrentIndex(i); + break; + } + } + + connect(&commanderSpellbookIntegrationEnabledSelector, QOverload::of(&QComboBox::currentIndexChanged), this, + [this](int index) { + int mode = commanderSpellbookIntegrationEnabledSelector.itemData(index).toInt(); + SettingsCache::instance().deckEditor().setCommanderSpellbookIntegrationEnabled(mode); + updateCommanderSpellbookUiState(); + }); + + commanderSpellbookIntegrationBracketNamingSelector.addItem( + tr("CommanderSpellbook bracket names")); // index 0 = false + commanderSpellbookIntegrationBracketNamingSelector.addItem( + tr("Official Commander bracket names (approximate)")); // index 1 = true + + commanderSpellbookIntegrationBracketNamingSelector.setCurrentIndex( + SettingsCache::instance().deckEditor().getCommanderSpellbookIntegrationUseOfficialBracketNames() ? 1 : 0); + + connect(&commanderSpellbookIntegrationBracketNamingSelector, QOverload::of(&QComboBox::currentIndexChanged), + &SettingsCache::instance(), [](int index) { + SettingsCache::instance().deckEditor().setCommanderSpellbookIntegrationUseOfficialBracketNames(index == + 1); + }); + + updateCommanderSpellbookUiState(); + + auto *labelLayout = new QHBoxLayout; + labelLayout->setContentsMargins(0, 0, 0, 0); + labelLayout->addWidget(&commanderSpellbookIntegrationUseOfficialBracketNamesLabel); + labelLayout->addWidget(&commanderSpellbookIntegrationUseOfficialBracketNamesExplainer); + + auto *labelWidget = new QWidget; + labelWidget->setLayout(labelLayout); + auto *deckEditorGrid = new QGridLayout; deckEditorGrid->addWidget(&openDeckInNewTabCheckBox, 0, 0); deckEditorGrid->addWidget(&visualDeckStorageInGameCheckBox, 1, 0); @@ -169,6 +221,10 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage() deckEditorGrid->addWidget(&visualDeckStoragePromptForConversionSelector, 3, 1); deckEditorGrid->addWidget(&defaultDeckEditorTypeLabel, 4, 0); deckEditorGrid->addWidget(&defaultDeckEditorTypeSelector, 4, 1); + deckEditorGrid->addWidget(&commanderSpellbookIntegrationEnabledLabel, 5, 0); + deckEditorGrid->addWidget(&commanderSpellbookIntegrationEnabledSelector, 5, 1); + deckEditorGrid->addWidget(labelWidget, 6, 0); + deckEditorGrid->addWidget(&commanderSpellbookIntegrationBracketNamingSelector, 6, 1); deckEditorGroupBox = new QGroupBox; deckEditorGroupBox->setLayout(deckEditorGrid); @@ -212,6 +268,27 @@ void UserInterfaceSettingsPage::setNotificationEnabled(QT_STATE_CHANGED_T i) } } +void UserInterfaceSettingsPage::updateCommanderSpellbookUiState() +{ + const int mode = SettingsCache::instance().deckEditor().getCommanderSpellbookIntegrationEnabled(); + + const bool enabled = mode != commanderSpellbookIntegrationEnabledIndexDisabled && + mode != commanderSpellbookIntegrationEnabledIndexUnprompted; + + commanderSpellbookIntegrationBracketNamingSelector.setEnabled(enabled); + commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setEnabled(enabled); + commanderSpellbookIntegrationUseOfficialBracketNamesLabel.setVisible(enabled); + commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setVisible(enabled); + commanderSpellbookIntegrationBracketNamingSelector.setVisible(enabled); + + if (enabled) { + // Sync selector with the current stored bool + const bool useOfficial = + SettingsCache::instance().deckEditor().getCommanderSpellbookIntegrationUseOfficialBracketNames(); + commanderSpellbookIntegrationBracketNamingSelector.setCurrentIndex(useOfficial ? 1 : 0); + } +} + void UserInterfaceSettingsPage::retranslateUi() { generalGroupBox->setTitle(tr("General interface settings")); @@ -249,6 +326,22 @@ void UserInterfaceSettingsPage::retranslateUi() defaultDeckEditorTypeLabel.setText(tr("Default deck editor type")); defaultDeckEditorTypeSelector.setItemText(TabSupervisor::ClassicDeckEditor, tr("Classic Deck Editor")); defaultDeckEditorTypeSelector.setItemText(TabSupervisor::VisualDeckEditor, tr("Visual Deck Editor")); + + commanderSpellbookIntegrationEnabledLabel.setText( + tr("CommanderSpellbook integration to estimate commander bracket")); + commanderSpellbookIntegrationEnabledSelector.setItemText(commanderSpellbookIntegrationEnabledIndexDisabled, + tr("Disabled")); + commanderSpellbookIntegrationEnabledSelector.setItemText(commanderSpellbookIntegrationEnabledIndexEnabled, + tr("Enabled")); + commanderSpellbookIntegrationEnabledSelector.setItemText(commanderSpellbookIntegrationEnabledIndexAutomatic, + tr("Automatic")); + commanderSpellbookIntegrationUseOfficialBracketNamesLabel.setText(tr("Bracket naming")); + commanderSpellbookIntegrationBracketNamingSelector.setItemText( + 0, CommanderBracketNames::CommanderSpellbookBracketNames); + commanderSpellbookIntegrationBracketNamingSelector.setItemText( + 1, CommanderBracketNames::OfficialCommanderBracketNames); + + commanderSpellbookIntegrationUseOfficialBracketNamesExplainer.setToolTip(CommanderBracketNames::Explainer); replayGroupBox->setTitle(tr("Replay settings")); rewindBufferingMsLabel.setText(tr("Buffer time for backwards skip via shortcut:")); rewindBufferingMsBox.setSuffix(" ms"); diff --git a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h index e10ed2a06..9e6fada69 100644 --- a/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h +++ b/cockatrice/src/interface/widgets/settings_page/user_interface_settings_page.h @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include class UserInterfaceSettingsPage : public AbstractSettingsPage @@ -15,6 +17,7 @@ class UserInterfaceSettingsPage : public AbstractSettingsPage Q_OBJECT private slots: void setNotificationEnabled(QT_STATE_CHANGED_T); + void updateCommanderSpellbookUiState(); private: QCheckBox notificationsEnabledCheckBox; @@ -39,6 +42,11 @@ private: QCheckBox visualDeckStorageSelectionAnimationCheckBox; QLabel defaultDeckEditorTypeLabel; QComboBox defaultDeckEditorTypeSelector; + QLabel commanderSpellbookIntegrationEnabledLabel; + QComboBox commanderSpellbookIntegrationEnabledSelector; + QLabel commanderSpellbookIntegrationUseOfficialBracketNamesLabel; + QToolButton commanderSpellbookIntegrationUseOfficialBracketNamesExplainer; + QComboBox commanderSpellbookIntegrationBracketNamingSelector; QLabel rewindBufferingMsLabel; QSpinBox rewindBufferingMsBox; QGroupBox *generalGroupBox; diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.cpp new file mode 100644 index 000000000..3c95a4553 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.cpp @@ -0,0 +1,17 @@ +#include "card_in_deck_request.h" + +CardInDeckRequest CardInDeckRequest::fromJson(const QJsonObject &json) +{ + CardInDeckRequest request; + request.card = json.value("card").toString(); + request.quantity = json.value("quantity").toInt(); + return request; +} + +QJsonObject CardInDeckRequest::toJson() const +{ + QJsonObject json; + json.insert("card", card); + json.insert("quantity", quantity); + return json; +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.h new file mode 100644 index 000000000..b7eb96211 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/card_in_deck_request.h @@ -0,0 +1,14 @@ +#ifndef COCKATRICE_CARD_IN_DECK_REQUEST_H +#define COCKATRICE_CARD_IN_DECK_REQUEST_H +#include + +struct CardInDeckRequest +{ + static CardInDeckRequest fromJson(const QJsonObject &json); + QJsonObject toJson() const; + + QString card; + int quantity; +}; + +#endif // COCKATRICE_CARD_IN_DECK_REQUEST_H diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.cpp new file mode 100644 index 000000000..14558d234 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.cpp @@ -0,0 +1,26 @@ +#include "commander_spellbook_card_result.h" + +CommanderSpellbookCardResult CommanderSpellbookCardResult::fromJson(const QJsonObject &json) +{ + CommanderSpellbookCardResult result; + + result.id = json.value("id").toString(); + result.name = json.value("name").toString(); + result.oracleId = json.value("oracleId").toString(); + result.spoiler = json.value("spoiler").toBool(); + result.typeLine = json.value("typeLine").toString(); + + result.imageUriFrontPng = json.value("imageUriFrontPng").toString(); + result.imageUriFrontLarge = json.value("imageUriFrontLarge").toString(); + result.imageUriFrontNormal = json.value("imageUriFrontNormal").toString(); + result.imageUriFrontSmall = json.value("imageUriFrontSmall").toString(); + result.imageUriFrontArtCrop = json.value("imageUriFrontArtCrop").toString(); + + result.imageUriBackPng = json.value("imageUriBackPng").toString(); + result.imageUriBackLarge = json.value("imageUriBackLarge").toString(); + result.imageUriBackNormal = json.value("imageUriBackNormal").toString(); + result.imageUriBackSmall = json.value("imageUriBackSmall").toString(); + result.imageUriBackArtCrop = json.value("imageUriBackArtCrop").toString(); + + return result; +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.h new file mode 100644 index 000000000..eaa474097 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_card_result.h @@ -0,0 +1,29 @@ +#ifndef COCKATRICE_COMMANDER_SPELLBOOK_CARD_RESULT_H +#define COCKATRICE_COMMANDER_SPELLBOOK_CARD_RESULT_H +#include +#include + +struct CommanderSpellbookCardResult +{ + static CommanderSpellbookCardResult fromJson(const QJsonObject &json); + + QString id; + QString name; + QString oracleId; + bool spoiler = false; + QString typeLine; + + QString imageUriFrontPng; + QString imageUriFrontLarge; + QString imageUriFrontNormal; + QString imageUriFrontSmall; + QString imageUriFrontArtCrop; + + QString imageUriBackPng; + QString imageUriBackLarge; + QString imageUriBackNormal; + QString imageUriBackSmall; + QString imageUriBackArtCrop; +}; + +#endif // COCKATRICE_COMMANDER_SPELLBOOK_CARD_RESULT_H diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.cpp new file mode 100644 index 000000000..166469353 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.cpp @@ -0,0 +1,95 @@ +#include "commander_spellbook_deck_request.h" + +#include +#include + +CommanderSpellbookDeckRequest CommanderSpellbookDeckRequest::fromJson(const QJsonObject &json) +{ + CommanderSpellbookDeckRequest request; + + // Main deck + const QJsonArray mainArray = json.value("main").toArray(); + for (const QJsonValue &value : mainArray) { + if (!value.isObject()) { + continue; + } + + request.mainDeck.append(CardInDeckRequest::fromJson(value.toObject())); + + // Max size allowed by commanderspellbook + if (request.mainDeck.size() >= 600) { + break; + } + } + + // Commanders + const QJsonArray commanderArray = json.value("commanders").toArray(); + for (const QJsonValue &value : commanderArray) { + if (!value.isObject()) { + continue; + } + + request.commanderDeck.append(CardInDeckRequest::fromJson(value.toObject())); + + // Max size allowed by commanderspellbook + if (request.commanderDeck.size() >= 12) { + break; + } + } + + return request; +} + +QJsonObject CommanderSpellbookDeckRequest::toJson() const +{ + QJsonObject json; + + QJsonArray mainArray; + for (const CardInDeckRequest &card : mainDeck) { + mainArray.append(card.toJson()); + } + + QJsonArray commanderArray; + for (const CardInDeckRequest &card : commanderDeck) { + commanderArray.append(card.toJson()); + } + + json.insert("main", mainArray); + json.insert("commanders", commanderArray); + + return json; +} + +CommanderSpellbookDeckRequest CommanderSpellbookDeckRequest::fromDeckList(const DeckList &deck) +{ + CommanderSpellbookDeckRequest request; + + // --- Mainboard --- + const auto mainCards = deck.getCardNodes({DECK_ZONE_MAIN}); + for (const DecklistCardNode *node : mainCards) { + if (!node) { + continue; + } + + QJsonObject json; + json.insert("card", node->getName()); + json.insert("quantity", node->getNumber()); + request.mainDeck.append(CardInDeckRequest::fromJson(json)); + + // Max size allowed by commanderspellbook + if (request.mainDeck.size() >= 600) { + break; + } + } + + // --- Commander (bannerCard) --- + const auto &metadata = deck.getMetadata(); + if (!metadata.bannerCard.name.isEmpty()) { + QJsonObject json; + json.insert("card", metadata.bannerCard.name); + json.insert("quantity", 1); + request.commanderDeck.append(CardInDeckRequest::fromJson(json)); + } + + return request; +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.h new file mode 100644 index 000000000..46d8e053b --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_deck_request.h @@ -0,0 +1,19 @@ +#ifndef COCKATRICE_COMMANDER_SPELLBOOK_DECK_REQUEST_H +#define COCKATRICE_COMMANDER_SPELLBOOK_DECK_REQUEST_H +#include "card_in_deck_request.h" +#include "libcockatrice/deck_list/deck_list.h" + +#include +#include + +struct CommanderSpellbookDeckRequest +{ + static CommanderSpellbookDeckRequest fromJson(const QJsonObject &json); + static CommanderSpellbookDeckRequest fromDeckList(const DeckList &deck); + QJsonObject toJson() const; + + QList mainDeck; // maxItems: 600 + QList commanderDeck; // maxItems: 12 +}; + +#endif // COCKATRICE_COMMANDER_SPELLBOOK_DECK_REQUEST_H diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.cpp new file mode 100644 index 000000000..c3640ce79 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.cpp @@ -0,0 +1,93 @@ +#include "commander_spellbook_estimate_bracket_result.h" + +EstimateBracketResult EstimateBracketResult::fromJson(const QJsonObject &json) +{ + EstimateBracketResult result; + + result.bracketTag = json.value("bracketTag").toString(); + + // + // Cards + // + for (const auto &value : json.value("cards").toArray()) { + if (!value.isObject()) { + continue; + } + + const QJsonObject obj = value.toObject(); + + CommanderSpellbookCardResult card = CommanderSpellbookCardResult::fromJson(obj.value("card").toObject()); + + if (obj.value("gameChanger").toBool()) { + result.gameChangerCards.append(card); + } + + if (obj.value("massLandDenial").toBool()) { + result.massLandDenialCards.append(card); + } + + if (obj.value("extraTurn").toBool()) { + result.extraTurnCards.append(card); + } + } + + // + // Templates + // + for (const auto &value : json.value("templates").toArray()) { + if (!value.isObject()) { + continue; + } + + const QJsonObject obj = value.toObject(); + + CommanderSpellbookVariantResult variant = CommanderSpellbookVariantResult::fromJson(obj); + + if (obj.value("massLandDenial").toBool()) { + result.massLandDenialTemplates.append(variant); + } + + if (obj.value("extraTurn").toBool()) { + result.extraTurnTemplates.append(variant); + } + } + + // + // Combos + // + for (const auto &value : json.value("combos").toArray()) { + if (!value.isObject()) { + continue; + } + + const QJsonObject obj = value.toObject(); + + CommanderSpellbookVariantResult combo = CommanderSpellbookVariantResult::fromJson(obj); + + if (obj.value("massLandDenial").toBool()) { + result.massLandDenialCombos.append(combo); + } + + if (obj.value("extraTurn").toBool()) { + result.extraTurnCombos.append(combo); + } + + if (obj.value("lock").toBool()) { + result.lockCombos.append(combo); + } + + if (obj.value("skipTurns").toBool()) { + result.skipTurnsCombos.append(combo); + } + + if (obj.value("definitelyTwoCard").toBool()) { + result.definitelyTwoCardCombos.append(combo); + } + + if (obj.value("arguablyTwoCard").toBool()) { + result.arguablyTwoCardCombos.append(combo); + } + } + + return result; +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.h new file mode 100644 index 000000000..bb589a901 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_estimate_bracket_result.h @@ -0,0 +1,32 @@ +#ifndef COCKATRICE_COMMANDER_SPELLBOOK_ESTIMATE_BRACKET_RESULT_H +#define COCKATRICE_COMMANDER_SPELLBOOK_ESTIMATE_BRACKET_RESULT_H + +#include "commander_spellbook_card_result.h" +#include "commander_spellbook_variant_result.h" + +#include +#include + +struct EstimateBracketResult +{ + static EstimateBracketResult fromJson(const QJsonObject &json); + + QString bracketTag; + + QList gameChangerCards; + QList massLandDenialCards; + QList extraTurnCards; + + QList massLandDenialTemplates; + QList extraTurnTemplates; + + QList massLandDenialCombos; + QList extraTurnCombos; + QList lockCombos; + QList skipTurnsCombos; + + QList definitelyTwoCardCombos; + QList arguablyTwoCardCombos; +}; + +#endif // COCKATRICE_COMMANDER_SPELLBOOK_ESTIMATE_BRACKET_RESULT_H diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.cpp new file mode 100644 index 000000000..bbc23e15e --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.cpp @@ -0,0 +1,35 @@ +#include "commander_spellbook_variant_result.h" + +CommanderSpellbookVariantResult CommanderSpellbookVariantResult::fromJson(const QJsonObject &json) +{ + CommanderSpellbookVariantResult result; + + result.id = json.value("id").toString(); + result.status = json.value("status").toString(); + + result.uses = json.value("uses").toArray(); + result.cardRequires = json.value("requires").toArray(); + result.produces = json.value("produces").toArray(); + result.of = json.value("of").toArray(); + result.includes = json.value("includes").toArray(); + + result.manaNeeded = json.value("manaNeeded").toArray(); + result.manaValueNeeded = json.value("manaValueNeeded").toArray(); + + result.easyPrerequisites = json.value("easyPrerequisites").toArray(); + result.notablePrerequisites = json.value("notablePrerequisites").toArray(); + + result.description = json.value("description").toString(); + result.notes = json.value("notes").toString(); + result.popularity = json.value("popularity").toDouble(); + + result.spoiler = json.value("spoiler").toBool(); + result.bracketTag = json.value("bracketTag").toString(); + + result.legalities = json.value("legalities").toObject(); + result.prices = json.value("prices").toObject(); + + result.variantCount = json.value("variantCount").toInt(); + + return result; +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.h new file mode 100644 index 000000000..d908f1299 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/api_response/commander_spellbook_variant_result.h @@ -0,0 +1,39 @@ +#ifndef COCKATRICE_COMMANDER_SPELLBOOK_VARIANT_RESULT_H +#define COCKATRICE_COMMANDER_SPELLBOOK_VARIANT_RESULT_H + +#include +#include + +struct CommanderSpellbookVariantResult +{ + static CommanderSpellbookVariantResult fromJson(const QJsonObject &json); + + QString id; + QString status; + + QJsonArray uses; + QJsonArray cardRequires; + QJsonArray produces; + QJsonArray of; + QJsonArray includes; + + QJsonArray manaNeeded; + QJsonArray manaValueNeeded; + + QJsonArray easyPrerequisites; + QJsonArray notablePrerequisites; + + QString description; + QString notes; + double popularity = 0.0; + + bool spoiler = false; + QString bracketTag; + + QJsonObject legalities; + QJsonObject prices; + + int variantCount = 0; +}; + +#endif // COCKATRICE_COMMANDER_SPELLBOOK_VARIANT_RESULT_H diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.cpp new file mode 100644 index 000000000..b85129a3f --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.cpp @@ -0,0 +1,53 @@ +#include "commander_bracket_service.h" + +#include "../../../../../client/settings/cache_settings.h" + +#include + +CommanderBracketService &CommanderBracketService::instance() +{ + static CommanderBracketService service; + return service; +} + +CommanderBracketService::CommanderBracketService(QObject *parent) : QObject(parent) +{ + connect(&CommanderSpellbookApiAccessor::instance(), &CommanderSpellbookApiAccessor::estimateBracketFinished, this, + &CommanderBracketService::onEstimateBracketFinished); + + connect(&CommanderSpellbookApiAccessor::instance(), &CommanderSpellbookApiAccessor::estimateBracketError, this, + &CommanderBracketService::onEstimateBracketError); +} + +quint64 CommanderBracketService::estimateBracket(const DeckList &deck, QObject *requester) +{ + return CommanderSpellbookApiAccessor::instance().estimateBracket(deck, requester); +} + +void CommanderBracketService::onEstimateBracketFinished(CommanderSpellbookApiAccessor::RequestId id, + QObject *requester, + const EstimateBracketResult &result) +{ + CommanderBracketEstimate estimate; + + estimate.bracketTag = result.bracketTag; + + estimate.rawResult = result; + + auto &brackets = SettingsCache::instance().commanderBrackets(); + + estimate.officialName = brackets.officialName(result.bracketTag); + + estimate.displayName = brackets.displayName(result.bracketTag); + + estimate.explanation = brackets.explanation(result.bracketTag); + + emit estimateFinished(id, requester, estimate); +} + +void CommanderBracketService::onEstimateBracketError(CommanderSpellbookApiAccessor::RequestId id, + QObject *requester, + const QString &error) +{ + emit estimateError(id, requester, error); +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.h new file mode 100644 index 000000000..e0c2a9d55 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_service.h @@ -0,0 +1,45 @@ +#ifndef COCKATRICE_COMMANDER_BRACKET_SERVICE_H +#define COCKATRICE_COMMANDER_BRACKET_SERVICE_H + +#include "commander_spellbook_api_accessor.h" +#include "libcockatrice/deck_list/deck_list.h" + +#include + +struct CommanderBracketEstimate +{ + QString bracketTag; + + QString officialName; + QString displayName; + QString explanation; + + EstimateBracketResult rawResult; +}; + +class CommanderBracketService : public QObject +{ + Q_OBJECT + +public: + static CommanderBracketService &instance(); + + quint64 estimateBracket(const DeckList &deck, QObject *requester); + +signals: + void estimateFinished(quint64 requestId, QObject *requester, const CommanderBracketEstimate &estimate); + + void estimateError(quint64 requestId, QObject *requester, const QString &error); + +private slots: + void onEstimateBracketFinished(CommanderSpellbookApiAccessor::RequestId id, + QObject *requester, + const EstimateBracketResult &result); + + void onEstimateBracketError(CommanderSpellbookApiAccessor::RequestId id, QObject *requester, const QString &error); + +private: + explicit CommanderBracketService(QObject *parent = nullptr); +}; + +#endif diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp new file mode 100644 index 000000000..4f50e38a6 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.cpp @@ -0,0 +1,269 @@ +#include "commander_bracket_widget.h" + +#include "../../../../../client/settings/cache_settings.h" +#include "commander_bracket_service.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +CommanderBracketWidget::CommanderBracketWidget(QWidget *parent) : QWidget(parent) +{ + bracketLabel = new QLabel(tr("Bracket:"), this); + + bracketValueLabel = new QLabel(this); + bracketValueLabel->setText("-"); + bracketValueLabel->setObjectName("bracketValueLabel"); + + bracketInfoButton = new QToolButton(this); + bracketInfoButton->setText("?"); + bracketInfoButton->setAutoRaise(true); + bracketInfoButton->setEnabled(false); + + bracketRefreshButton = new QToolButton(this); + bracketRefreshButton->setIcon(QPixmap("theme:icons/reload")); + bracketRefreshButton->setAutoRaise(true); + + connect(bracketRefreshButton, &QToolButton::clicked, this, &CommanderBracketWidget::requestBracketEstimate); + + auto *layout = new QGridLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->addWidget(bracketLabel, 0, 0); + + auto *bracketRow = new QHBoxLayout; + bracketRow->addWidget(bracketValueLabel); + bracketRow->addWidget(bracketInfoButton); + bracketRow->addWidget(bracketRefreshButton); + bracketRow->addStretch(); + + layout->addLayout(bracketRow, 0, 1); + + connect(&CommanderBracketService::instance(), &CommanderBracketService::estimateFinished, this, + &CommanderBracketWidget::onEstimateBracketFinished); + connect(&CommanderBracketService::instance(), &CommanderBracketService::estimateError, this, + &CommanderBracketWidget::onEstimateBracketError); + + connect(&SettingsCache::instance().deckEditor(), &DeckEditorSettings::commanderSpellbookIntegrationEnabledChanged, + this, &CommanderBracketWidget::maybeAutoEstimateBracket); + connect(&SettingsCache::instance().deckEditor(), + &DeckEditorSettings::commanderSpellbookIntegrationUseOfficialBracketNamesChanged, this, + &CommanderBracketWidget::maybeAutoEstimateBracket); + + setVisible(false); +} + +void CommanderBracketWidget::setDeck(const QSharedPointer &_deck) +{ + deck = _deck; + requestId = 0; // invalidate any in-flight estimate for the previous deck + + // Reset the displayed bracket + bracketValueLabel->setText("-"); + bracketInfoButton->setToolTip({}); + bracketInfoButton->setEnabled(false); + bracketRefreshButton->setEnabled(true); + + maybeAutoEstimateBracket(); +} + +bool CommanderBracketWidget::promptCommanderSpellbookIntegration() +{ + QDialog dialog(this); + dialog.setWindowTitle(tr("CommanderSpellbook integration")); + + auto *mainLayout = new QVBoxLayout(&dialog); + + // Main text + auto *label = new QLabel(tr("CommanderSpellbook can analyze your deck and estimate its Commander bracket.\n\n" + "This sends your deck list to an external service.\n\n" + "CommanderSpellbook uses its own bracket naming system based on their own algorithm. " + "These names can be mapped to the official Commander brackets, but the mapping " + "is only an approximation.")); + label->setWordWrap(true); + mainLayout->addWidget(label); + + // Naming selector + auto *formLayout = new QFormLayout; + auto *namingCombo = new QComboBox(&dialog); + namingCombo->addItem(tr("CommanderSpellbook bracket names")); + namingCombo->addItem(tr("Official Commander bracket names (approximate)")); + namingCombo->setCurrentIndex( + SettingsCache::instance().deckEditor().getCommanderSpellbookIntegrationUseOfficialBracketNames() ? 1 : 0); + + // Create label + explainer button + auto *labelWidget = new QWidget(&dialog); + auto *labelLayout = new QHBoxLayout(labelWidget); + labelLayout->setContentsMargins(0, 0, 0, 0); + + auto *namingLabel = new QLabel(tr("Bracket naming:"), labelWidget); + auto *explainerButton = new QToolButton(labelWidget); + explainerButton->setText("?"); + explainerButton->setAutoRaise(true); + explainerButton->setEnabled(false); + explainerButton->setToolTip(CommanderBracketNames::Explainer); + + labelLayout->addWidget(namingLabel); + labelLayout->addWidget(explainerButton); + labelLayout->addStretch(); // push the button next to label, combo stays aligned + + // Add row with the custom label widget + formLayout->addRow(labelWidget, namingCombo); + mainLayout->addLayout(formLayout); + + // Buttons + auto *buttonBox = new QDialogButtonBox(&dialog); + auto *enableBtn = buttonBox->addButton(tr("Enable"), QDialogButtonBox::AcceptRole); + auto *automaticBtn = buttonBox->addButton(tr("Automatic"), QDialogButtonBox::ApplyRole); + auto *disableBtn = buttonBox->addButton(tr("Disable"), QDialogButtonBox::RejectRole); + mainLayout->addWidget(buttonBox); + + // Track which button was clicked + QAbstractButton *clickedButton = nullptr; + QObject::connect(buttonBox, &QDialogButtonBox::clicked, &dialog, [&](QAbstractButton *btn) { + clickedButton = btn; + dialog.accept(); + }); + + dialog.exec(); + + // Persist naming choice (if not disabled) + if (clickedButton != disableBtn) { + bool useOfficial = namingCombo->currentIndex() == 1; + SettingsCache::instance().deckEditor().setCommanderSpellbookIntegrationUseOfficialBracketNames(useOfficial); + } + + // Persist integration mode + if (clickedButton == disableBtn) { + SettingsCache::instance().deckEditor().setCommanderSpellbookIntegrationEnabled( + commanderSpellbookIntegrationEnabledIndexDisabled); + return false; + } + if (clickedButton == enableBtn) { + SettingsCache::instance().deckEditor().setCommanderSpellbookIntegrationEnabled( + commanderSpellbookIntegrationEnabledIndexEnabled); + return true; + } + if (clickedButton == automaticBtn) { + SettingsCache::instance().deckEditor().setCommanderSpellbookIntegrationEnabled( + commanderSpellbookIntegrationEnabledIndexAutomatic); + return true; + } + + return false; +} + +void CommanderBracketWidget::updateBracketVisibility(bool visible) +{ + setVisible(visible); +} + +void CommanderBracketWidget::requestBracketEstimate() +{ + bracketRefreshButton->setEnabled(false); + bracketInfoButton->setEnabled(false); + bracketValueLabel->setText(tr("Calculating…")); + + requestId = CommanderBracketService::instance().estimateBracket(*deck, this); +} + +void CommanderBracketWidget::onEstimateBracketFinished(quint64 id, + QObject *requester, + const CommanderBracketEstimate &result) +{ + if (requester != this || id != requestId) { + return; + } + + BracketExplainer explainer; + lastBracketExplanation = explainer.explain(result.rawResult); + + // Display bracket + bracketValueLabel->setText( + SettingsCache::instance().deckEditor().getCommanderSpellbookIntegrationUseOfficialBracketNames() + ? result.officialName + : result.displayName); + bracketRefreshButton->setEnabled(true); + + // Build tooltip + QString tooltip; + for (const auto §ion : lastBracketExplanation.sections) { + tooltip += "" + section.title + "
"; + for (const auto &line : section.bulletPoints) { + tooltip += "• " + line + "
"; + } + tooltip += "
"; + } + + bracketInfoButton->setToolTip(tooltip); + bracketInfoButton->setEnabled(!tooltip.isEmpty()); +} + +void CommanderBracketWidget::onEstimateBracketError(quint64 id, QObject *requester, const QString & /*error*/) +{ + if (requester != this || id != requestId) { + return; + } + + bracketValueLabel->setText("-"); + bracketRefreshButton->setEnabled(true); + bracketInfoButton->setToolTip({}); + bracketInfoButton->setEnabled(false); +} + +void CommanderBracketWidget::maybeAutoEstimateBracket() +{ + const QString formatKey = deck->getGameFormat(); + + const bool isCommander = (formatKey.compare("commander", Qt::CaseInsensitive) == 0); + + int mode = SettingsCache::instance().deckEditor().getCommanderSpellbookIntegrationEnabled(); + + if (!isCommander || mode == commanderSpellbookIntegrationEnabledIndexDisabled) { + updateBracketVisibility(false); + return; + } + + if (mode == commanderSpellbookIntegrationEnabledIndexUnprompted) { + if (prompting) { + return; + } + prompting = true; + const bool accepted = promptCommanderSpellbookIntegration(); + prompting = false; + if (!accepted) { + updateBracketVisibility(false); + return; + } + } + + updateBracketVisibility(true); + + mode = SettingsCache::instance().deckEditor().getCommanderSpellbookIntegrationEnabled(); + if (mode != commanderSpellbookIntegrationEnabledIndexAutomatic) { + return; + } + + // Avoid firing if we already have a result or a request in flight + if (!bracketRefreshButton->isEnabled()) { + return; + } + + // Defer to avoid races during init / model rebuild + QTimer::singleShot(0, this, &CommanderBracketWidget::requestBracketEstimate); +} + +void CommanderBracketWidget::retranslateUi() +{ + bracketLabel->setText(tr("Bracket:")); + bracketInfoButton->setToolTip(tr("Why this bracket?")); + bracketRefreshButton->setToolTip(tr("Recalculate bracket")); +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.h new file mode 100644 index 000000000..a45610fcf --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_widget.h @@ -0,0 +1,46 @@ +#ifndef COCKATRICE_COMMANDER_BRACKET_WIDGET_H +#define COCKATRICE_COMMANDER_BRACKET_WIDGET_H + +#include "commander_spellbook_bracket_explainer.h" + +#include +#include +#include + +class QLabel; +class QToolButton; +struct CommanderBracketEstimate; + +class CommanderBracketWidget : public QWidget +{ + Q_OBJECT + +public: + explicit CommanderBracketWidget(QWidget *parent = nullptr); + + void setDeck(const QSharedPointer &_deck); + void retranslateUi(); + +private slots: + void requestBracketEstimate(); + void onEstimateBracketFinished(quint64 id, QObject *requester, const CommanderBracketEstimate &result); + void onEstimateBracketError(quint64 id, QObject *requester, const QString &error); + void maybeAutoEstimateBracket(); + +private: + bool promptCommanderSpellbookIntegration(); + void updateBracketVisibility(bool visible); + + QSharedPointer deck; + bool prompting = false; + quint64 requestId = 0; + + QLabel *bracketLabel; + QLabel *bracketValueLabel; + QToolButton *bracketInfoButton; + QToolButton *bracketRefreshButton; + + BracketExplanation lastBracketExplanation; +}; + +#endif // COCKATRICE_COMMANDER_BRACKET_WIDGET_H diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.cpp new file mode 100644 index 000000000..590692d98 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.cpp @@ -0,0 +1,75 @@ +#include "commander_spellbook_api_accessor.h" + +#include "api_response/commander_spellbook_deck_request.h" + +#include +#include +#include +#include +#include + +static const QUrl ESTIMATE_BRACKET_URL(QStringLiteral("https://backend.commanderspellbook.com/estimate-bracket")); + +CommanderSpellbookApiAccessor &CommanderSpellbookApiAccessor::instance() +{ + static CommanderSpellbookApiAccessor instance; + return instance; +} + +CommanderSpellbookApiAccessor::CommanderSpellbookApiAccessor(QObject *parent) : QObject(parent) +{ +} + +CommanderSpellbookApiAccessor::RequestId CommanderSpellbookApiAccessor::estimateBracket(const DeckList &deck, + QObject *requester) +{ + CommanderSpellbookDeckRequest deckRequest = CommanderSpellbookDeckRequest::fromDeckList(deck); + + QJsonDocument doc(deckRequest.toJson()); + QByteArray body = doc.toJson(QJsonDocument::Compact); + + QNetworkRequest req(ESTIMATE_BRACKET_URL); + req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + req.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING)); + + QNetworkReply *reply = network.post(req, body); + + const RequestId id = nextRequestId++; + + reply->setProperty("requestId", QVariant::fromValue(id)); + reply->setProperty("requester", QVariant::fromValue(requester)); + + connect(reply, &QNetworkReply::finished, this, [this, reply]() { onEstimateReplyFinished(reply); }); + + return id; +} + +void CommanderSpellbookApiAccessor::onEstimateReplyFinished(QNetworkReply *reply) +{ + reply->deleteLater(); + + const RequestId id = reply->property("requestId").toULongLong(); + QObject *requester = reply->property("requester").value(); + + if (!requester) { + // Requester died — silently drop + return; + } + + if (reply->error() != QNetworkReply::NoError) { + emit estimateBracketError(id, requester, reply->errorString()); + return; + } + + QJsonParseError err; + QJsonDocument doc = QJsonDocument::fromJson(reply->readAll(), &err); + + if (err.error != QJsonParseError::NoError || !doc.isObject()) { + emit estimateBracketError(id, requester, QStringLiteral("Invalid JSON response")); + return; + } + + EstimateBracketResult result = EstimateBracketResult::fromJson(doc.object()); + + emit estimateBracketFinished(id, requester, result); +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.h new file mode 100644 index 000000000..682a48524 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_api_accessor.h @@ -0,0 +1,37 @@ +#ifndef COCKATRICE_COMMANDER_SPELLBOOK_API_ACCESSOR_H +#define COCKATRICE_COMMANDER_SPELLBOOK_API_ACCESSOR_H + +#include "api_response/commander_spellbook_estimate_bracket_result.h" + +#include +#include +#include +#include + +class CommanderSpellbookApiAccessor final : public QObject +{ + Q_OBJECT + +public: + static CommanderSpellbookApiAccessor &instance(); + + using RequestId = quint64; + + RequestId estimateBracket(const DeckList &deck, QObject *requester); + +signals: + void estimateBracketFinished(RequestId id, QObject *requester, const EstimateBracketResult &result); + + void estimateBracketError(RequestId id, QObject *requester, const QString &errorMessage); + +private: + explicit CommanderSpellbookApiAccessor(QObject *parent = nullptr); + Q_DISABLE_COPY_MOVE(CommanderSpellbookApiAccessor) + + void onEstimateReplyFinished(QNetworkReply *reply); + + QNetworkAccessManager network; + RequestId nextRequestId = 1; +}; + +#endif // COCKATRICE_COMMANDER_SPELLBOOK_API_ACCESSOR_H diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.cpp new file mode 100644 index 000000000..fc308ed60 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.cpp @@ -0,0 +1,120 @@ +#include "commander_spellbook_bracket_explainer.h" + +static QString cardList(const QList &cards, int max = 5) +{ + QStringList names; + for (int i = 0; i < cards.size() && i < max; ++i) { + names << cards[i].name; + } + + if (cards.size() > max) { + names << QString("and %1 more").arg(cards.size() - max); + } + + return names.join(", "); +} + +static QString comboCount(const QList &variants) +{ + return QString::number(variants.size()); +} + +BracketExplanation BracketExplainer::explain(const EstimateBracketResult &r) +{ + BracketExplanation out; + out.bracket = r.bracketTag; + + if (!r.gameChangerCards.isEmpty()) { + BracketExplanationSection s; + s.title = "Game-changing cards"; + + s.bulletPoints << QString("Your deck contains %1 game-changing cards, such as %2.") + .arg(r.gameChangerCards.size()) + .arg(cardList(r.gameChangerCards)); + + out.sections << s; + } + + if (!r.extraTurnCards.isEmpty() || !r.extraTurnTemplates.isEmpty() || !r.extraTurnCombos.isEmpty()) { + + BracketExplanationSection s; + s.title = "Extra turns"; + + if (!r.extraTurnCards.isEmpty()) { + s.bulletPoints << QString("The deck contains %1 extra-turn cards (%2).") + .arg(r.extraTurnCards.size()) + .arg(cardList(r.extraTurnCards)); + } + + if (!r.extraTurnTemplates.isEmpty()) { + s.bulletPoints << QString("%1 extra-turn templates were identified.").arg(comboCount(r.extraTurnTemplates)); + } + + if (!r.extraTurnCombos.isEmpty()) { + s.bulletPoints + << QString("%1 extra-turn combo variants were identified.").arg(comboCount(r.extraTurnCombos)); + } + + out.sections << s; + } + + if (!r.massLandDenialCards.isEmpty() || !r.massLandDenialTemplates.isEmpty() || !r.massLandDenialCombos.isEmpty()) { + + BracketExplanationSection s; + s.title = "Mass land denial"; + + if (!r.massLandDenialCards.isEmpty()) { + s.bulletPoints << QString("The deck contains %1 mass land denial cards (%2).") + .arg(r.massLandDenialCards.size()) + .arg(cardList(r.massLandDenialCards)); + } + + if (!r.massLandDenialTemplates.isEmpty()) { + s.bulletPoints + << QString("%1 mass land denial templates were identified.").arg(comboCount(r.massLandDenialTemplates)); + } + + if (!r.massLandDenialCombos.isEmpty()) { + s.bulletPoints << QString("%1 mass land denial combo variants were identified.") + .arg(comboCount(r.massLandDenialCombos)); + } + + out.sections << s; + } + + if (!r.lockCombos.isEmpty() || !r.skipTurnsCombos.isEmpty()) { + + BracketExplanationSection s; + s.title = "Lock pieces"; + + if (!r.lockCombos.isEmpty()) { + s.bulletPoints << QString("%1 lock combo variants were detected.").arg(comboCount(r.lockCombos)); + } + + if (!r.skipTurnsCombos.isEmpty()) { + s.bulletPoints << QString("%1 skip-turn combo variants were detected.").arg(comboCount(r.skipTurnsCombos)); + } + + out.sections << s; + } + + if (!r.definitelyTwoCardCombos.isEmpty() || !r.arguablyTwoCardCombos.isEmpty()) { + + BracketExplanationSection s; + s.title = "Two-card combos"; + + if (!r.definitelyTwoCardCombos.isEmpty()) { + s.bulletPoints << QString("%1 definite two-card combo variants were identified.") + .arg(comboCount(r.definitelyTwoCardCombos)); + } + + if (!r.arguablyTwoCardCombos.isEmpty()) { + s.bulletPoints << QString("%1 arguable two-card combo variants were identified.") + .arg(comboCount(r.arguablyTwoCardCombos)); + } + + out.sections << s; + } + + return out; +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.h new file mode 100644 index 000000000..9ed499742 --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.h @@ -0,0 +1,43 @@ +#ifndef COCKATRICE_COMMANDER_SPELLBOOK_BRACKET_EXPLAINER_H +#define COCKATRICE_COMMANDER_SPELLBOOK_BRACKET_EXPLAINER_H +#include "api_response/commander_spellbook_estimate_bracket_result.h" + +namespace CommanderBracketNames +{ +inline const char *CommanderSpellbookBracketNames = QT_TR_NOOP("CommanderSpellbook"); +inline const char *OfficialCommanderBracketNames = QT_TR_NOOP("Official (approximate)"); +inline const char *Explainer = QT_TR_NOOP( + "The bracket system combines both objective data, as well as subjective play experience to estimate a " + "bracket for a deck.\nCommanderSpellbook's estimation is algorithmical, which means that it can only operate " + "on the objective data, not the subjective intent. \nThey have chosen to represent this by defining their " + "own bracket system which matches their algorithm.\n" + "This custom bracket system maps loosely to the standard system. \nYou may choose to use these mapped " + "standardized names if these are more familiar to you, however, you should keep in mind that these are just " + "rough estimations.\n\nAlways consider the subjective factors of the bracket system when determing a deck's " + "final bracket!"); +} // namespace CommanderBracketNames + +struct BracketExplanationSection +{ + QString title; + QStringList bulletPoints; +}; + +struct BracketExplanation +{ + QString bracket; + QList sections; + + bool isEmpty() const + { + return sections.isEmpty(); + } +}; + +class BracketExplainer +{ +public: + static BracketExplanation explain(const EstimateBracketResult &result); +}; + +#endif // COCKATRICE_COMMANDER_SPELLBOOK_BRACKET_EXPLAINER_H diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp new file mode 100644 index 000000000..90b490efe --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.cpp @@ -0,0 +1,62 @@ +#include "handle_commander_brackets.h" + +#include "../../../../../client/settings/cache_settings.h" + +#include +#include +#include +#include + +static const QUrl COMMANDER_BRACKET_JSON_URL(QStringLiteral("https://cockatrice.github.io/commander-brackets.json")); + +HandleCommanderBrackets::HandleCommanderBrackets(QObject *parent) + : QObject(parent), nam(new QNetworkAccessManager(this)), reply(nullptr) +{ +} + +void HandleCommanderBrackets::downloadBracketDefinitions() +{ + if (reply) { + return; + } + + reply = nam->get(QNetworkRequest(COMMANDER_BRACKET_JSON_URL)); + + connect(reply, &QNetworkReply::finished, this, &HandleCommanderBrackets::actFinishParsingDownloadedData); +} + +void HandleCommanderBrackets::actFinishParsingDownloadedData() +{ + reply = qobject_cast(sender()); + + if (reply->error() != QNetworkReply::NoError) { + emit sigBracketDefinitionsDownloadFailed(reply->error()); + + reply->deleteLater(); + return; + } + + QJsonParseError parseError; + + auto document = QJsonDocument::fromJson(reply->readAll(), &parseError); + + if (parseError.error != QJsonParseError::NoError) { + emit sigBracketDefinitionsDownloadFailed(QNetworkReply::UnknownContentError); + + reply->deleteLater(); + return; + } + + updateBracketDefinitions(document.toVariant().toMap()); + + emit sigBracketDefinitionsDownloaded(); + + reply->deleteLater(); +} + +void HandleCommanderBrackets::updateBracketDefinitions(const QVariantMap &jsonMap) +{ + const auto bracketList = jsonMap.value("brackets").toList(); + SettingsCache::instance().commanderBrackets().saveDefinitions(bracketList); + SettingsCache::instance().commanderBrackets().reloadDefinitions(bracketList); +} diff --git a/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.h b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.h new file mode 100644 index 000000000..b8348b13c --- /dev/null +++ b/cockatrice/src/interface/widgets/tabs/api/commander_spellbook/handle_commander_brackets.h @@ -0,0 +1,31 @@ +#ifndef COCKATRICE_HANDLE_COMMANDER_BRACKETS_H +#define COCKATRICE_HANDLE_COMMANDER_BRACKETS_H + +#include +#include +#include + +class HandleCommanderBrackets : public QObject +{ + Q_OBJECT + +public: + explicit HandleCommanderBrackets(QObject *parent = nullptr); + + void downloadBracketDefinitions(); + +signals: + void sigBracketDefinitionsDownloaded(); + void sigBracketDefinitionsDownloadFailed(QNetworkReply::NetworkError error); + +private slots: + void actFinishParsingDownloadedData(); + +private: + void updateBracketDefinitions(const QVariantMap &jsonMap); + + QNetworkAccessManager *nam; + QNetworkReply *reply; +}; + +#endif // COCKATRICE_HANDLE_COMMANDER_BRACKETS_H diff --git a/cockatrice/src/interface/window_main.cpp b/cockatrice/src/interface/window_main.cpp index e21737d67..44e188760 100644 --- a/cockatrice/src/interface/window_main.cpp +++ b/cockatrice/src/interface/window_main.cpp @@ -38,6 +38,7 @@ #include "version_string.h" #include "widgets/dialogs/dlg_connect.h" #include "widgets/server/handle_public_servers.h" +#include "widgets/tabs/api/commander_spellbook/handle_commander_brackets.h" #include "widgets/utility/get_text_with_max.h" #include @@ -559,6 +560,8 @@ void MainWindow::startupConfigCheck() actCheckClientUpdates(); } + actCheckCommanderBracketDefinitionUpdates(); + if (SettingsCache::instance().network().getClientVersion() == CLIENT_INFO_NOT_SET) { // no config found, 99% new clean install qCInfo(WindowMainStartupVersionLog) @@ -661,6 +664,7 @@ void MainWindow::alertForcedOracleRun(const QString &version, bool isUpdate) actCheckCardUpdates(); actCheckServerUpdates(); + actCheckCommanderBracketDefinitionUpdates(); } MainWindow::~MainWindow() @@ -1005,6 +1009,16 @@ void MainWindow::checkClientUpdatesFinished(bool needToUpdate, bool /* isCompati } } +void MainWindow::actCheckCommanderBracketDefinitionUpdates() +{ + auto *handler = new HandleCommanderBrackets(this); + + connect(handler, &HandleCommanderBrackets::sigBracketDefinitionsDownloaded, this, + []() { qDebug() << "Bracket definitions loaded"; }); + + handler->downloadBracketDefinitions(); +} + void MainWindow::refreshShortcuts() { ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts(); diff --git a/cockatrice/src/interface/window_main.h b/cockatrice/src/interface/window_main.h index 610f11965..fa6c79915 100644 --- a/cockatrice/src/interface/window_main.h +++ b/cockatrice/src/interface/window_main.h @@ -92,6 +92,7 @@ private slots: void cardDatabaseAllNewSetsEnabled(); void checkClientUpdatesFinished(bool needToUpdate, bool isCompatible, Release *release); + void actCheckCommanderBracketDefinitionUpdates(); void actOpenCustomFolder(); void actOpenCustomsetsFolder(); diff --git a/libcockatrice_settings/CMakeLists.txt b/libcockatrice_settings/CMakeLists.txt index 9e2654a9a..80a465b6d 100644 --- a/libcockatrice_settings/CMakeLists.txt +++ b/libcockatrice_settings/CMakeLists.txt @@ -9,6 +9,7 @@ set(HEADERS libcockatrice/settings/card_override_settings.h libcockatrice/settings/cards_display_settings.h libcockatrice/settings/chat_settings.h + libcockatrice/settings/commander_bracket_settings.h libcockatrice/settings/debug_settings.h libcockatrice/settings/deck_editor_settings.h libcockatrice/settings/download_settings.h @@ -39,6 +40,7 @@ add_library( libcockatrice/settings/cache_storage_settings.cpp libcockatrice/settings/card_database_settings.cpp libcockatrice/settings/card_override_settings.cpp + libcockatrice/settings/commander_bracket_settings.cpp libcockatrice/settings/cards_display_settings.cpp libcockatrice/settings/chat_settings.cpp libcockatrice/settings/debug_settings.cpp diff --git a/libcockatrice_settings/libcockatrice/settings/commander_bracket_settings.cpp b/libcockatrice_settings/libcockatrice/settings/commander_bracket_settings.cpp new file mode 100644 index 000000000..0c5f5521e --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/commander_bracket_settings.cpp @@ -0,0 +1,194 @@ +#include "commander_bracket_settings.h" + +#include + +QVariantList CommanderBracketSettings::defaultDefinitions() +{ + return { + QVariantMap{{"tag", "R"}, + {"officialName", "[5] cEDH"}, + {"displayName", "Ruthless"}, + {"explanation", + "Top-tier competitive decks with maximum optimization, fast combos, and minimal variance."}}, + QVariantMap{{"tag", "S"}, + {"officialName", "[4] Optimized"}, + {"displayName", "Spicy"}, + {"explanation", "Highly tuned decks with strong synergy and occasional combo finishes."}}, + QVariantMap{{"tag", "P"}, + {"officialName", "[3] Upgraded"}, + {"displayName", "Powerful"}, + {"explanation", "Focused decks with clear win conditions and solid consistency."}}, + QVariantMap{{"tag", "O"}, + {"officialName", "[2] Core"}, + {"displayName", "Oddball"}, + {"explanation", "Unconventional or thematic decks with some structure but non-standard choices."}}, + QVariantMap{{"tag", "C"}, + {"officialName", "[2] Core"}, + {"displayName", "Core"}, + {"explanation", "Preconstructed or precon-level decks with straightforward strategies."}}, + QVariantMap{{"tag", "E"}, + {"officialName", "[1] Exhibition"}, + {"displayName", "Exhibition"}, + {"explanation", "Ultra-casual, theme-focused decks with minimal optimization."}}, + QVariantMap{{"tag", "B"}, + {"officialName", "Banned"}, + {"displayName", "Banned"}, + {"explanation", "The deck contains one or more cards banned in Commander."}}, + }; +} + +CommanderBracketSettings::CommanderBracketSettings(const QString &settingPath, QObject *parent) + : SettingsManager(settingPath + "commander_brackets.ini", "commander_brackets", QString(), parent) +{ +} + +void CommanderBracketSettings::setSchemaVersion(int version) +{ + setValue(version, "schemaVersion"); +} +int CommanderBracketSettings::getSchemaVersion() const +{ + QVariant value = getValue("schemaVersion"); + return value.isValid() ? value.toInt() : 0; +} + +void CommanderBracketSettings::clearDefinitions() +{ + auto settings = getSettings(); + + settings.beginGroup("commander_brackets"); + settings.remove(""); + settings.endGroup(); + + settings.sync(); +} + +void CommanderBracketSettings::saveDefinitions(const QVariantList &definitions) +{ + auto settings = getSettings(); + + settings.beginGroup("commander_brackets"); + + settings.remove(""); + + settings.setValue("schemaVersion", CurrentSchemaVersion); + + for (const auto &entry : definitions) { + QVariantMap map = entry.toMap(); + + QString tag = map.value("tag").toString(); + + if (tag.isEmpty()) { + continue; + } + + settings.beginGroup(tag); + + settings.setValue("officialName", map.value("officialName")); + + settings.setValue("displayName", map.value("displayName")); + + settings.setValue("explanation", map.value("explanation")); + + settings.endGroup(); + } + + settings.endGroup(); + + settings.sync(); +} + +QVariantList CommanderBracketSettings::loadDefinitions() const +{ + QVariantList result; + + auto settings = getSettings(); + + settings.beginGroup("commander_brackets"); + + int version = settings.value("schemaVersion", 0).toInt(); + + if (version != CurrentSchemaVersion) { + settings.endGroup(); + return result; + } + + QStringList groups = settings.childGroups(); + + for (const QString &tag : groups) { + settings.beginGroup(tag); + + QVariantMap map; + + map["tag"] = tag; + map["officialName"] = settings.value("officialName"); + map["displayName"] = settings.value("displayName"); + map["explanation"] = settings.value("explanation"); + + result.append(map); + + settings.endGroup(); + } + + settings.endGroup(); + + return result; +} + +void CommanderBracketSettings::reloadDefinitions(const QVariantList &definitionsList) +{ + definitions.clear(); + + for (const auto &entry : definitionsList) { + const auto map = entry.toMap(); + + CommanderBracketDefinition definition; + + definition.tag = map.value("tag").toString(); + definition.officialName = map.value("officialName").toString(); + definition.displayName = map.value("displayName").toString(); + definition.explanation = map.value("explanation").toString(); + + if (!definition.tag.isEmpty()) { + definitions.insert(definition.tag, definition); + } + } +} + +QString CommanderBracketSettings::officialName(const QString &tag) const +{ + auto it = definitions.find(tag); + + if (it == definitions.end()) { + return tag; + } + + return it->officialName; +} + +QString CommanderBracketSettings::displayName(const QString &tag) const +{ + auto it = definitions.find(tag); + + if (it == definitions.end()) { + return tag; + } + + return it->displayName; +} + +QString CommanderBracketSettings::explanation(const QString &tag) const +{ + auto it = definitions.find(tag); + + if (it == definitions.end()) { + return {}; + } + + return it->explanation; +} + +bool CommanderBracketSettings::contains(const QString &tag) const +{ + return definitions.contains(tag); +} diff --git a/libcockatrice_settings/libcockatrice/settings/commander_bracket_settings.h b/libcockatrice_settings/libcockatrice/settings/commander_bracket_settings.h new file mode 100644 index 000000000..5a29f5dc7 --- /dev/null +++ b/libcockatrice_settings/libcockatrice/settings/commander_bracket_settings.h @@ -0,0 +1,56 @@ +#ifndef COMMANDER_BRACKET_SETTINGS_H +#define COMMANDER_BRACKET_SETTINGS_H + +#include "settings_manager.h" + +#include +#include +#include +#include + +struct CommanderBracketDefinition +{ + QString tag; + + QString officialName; + QString displayName; + + QString explanation; +}; + +class CommanderBracketSettings : public SettingsManager +{ + Q_OBJECT + friend class SettingsCache; + +public: + static constexpr int CurrentSchemaVersion = 1; + + static QVariantList defaultDefinitions(); + + void clearDefinitions(); + + void saveDefinitions(const QVariantList &definitions); + + QVariantList loadDefinitions() const; + + void reloadDefinitions(const QVariantList &definitions); + + QString officialName(const QString &tag) const; + QString displayName(const QString &tag) const; + QString explanation(const QString &tag) const; + bool contains(const QString &tag) const; + + void setSchemaVersion(int version); + int getSchemaVersion() const; + +private: + explicit CommanderBracketSettings(const QString &settingPath, QObject *parent = nullptr); + + CommanderBracketSettings(const CommanderBracketSettings &) = delete; + CommanderBracketSettings &operator=(const CommanderBracketSettings &) = delete; + + QHash definitions; +}; + +#endif // COMMANDER_BRACKET_SETTINGS_H diff --git a/libcockatrice_settings/libcockatrice/settings/deck_editor_settings.cpp b/libcockatrice_settings/libcockatrice/settings/deck_editor_settings.cpp index d6b9b389b..65296a450 100644 --- a/libcockatrice_settings/libcockatrice/settings/deck_editor_settings.cpp +++ b/libcockatrice_settings/libcockatrice/settings/deck_editor_settings.cpp @@ -46,3 +46,27 @@ void DeckEditorSettings::setDefaultDeckEditorType(int _defaultDeckEditorType) { setValue(_defaultDeckEditorType, "defaultDeckEditorType"); } + +int DeckEditorSettings::getCommanderSpellbookIntegrationEnabled() const +{ + return getValue("commanderspellbookintegrationenabled", QString(), QString(), + commanderSpellbookIntegrationEnabledIndexUnprompted) + .toInt(); +} + +bool DeckEditorSettings::getCommanderSpellbookIntegrationUseOfficialBracketNames() const +{ + return getValue("commanderspellbookintegrationuseofficialbracketnames", QString(), QString(), false).toBool(); +} + +void DeckEditorSettings::setCommanderSpellbookIntegrationEnabled(int _commanderSpellbookIntegrationEnabled) +{ + setValue(_commanderSpellbookIntegrationEnabled, "commanderspellbookintegrationenabled"); + emit commanderSpellbookIntegrationEnabledChanged(_commanderSpellbookIntegrationEnabled); +} + +void DeckEditorSettings::setCommanderSpellbookIntegrationUseOfficialBracketNames(bool _useOfficialBracketNames) +{ + setValue(_useOfficialBracketNames, "commanderspellbookintegrationuseofficialbracketnames"); + emit commanderSpellbookIntegrationUseOfficialBracketNamesChanged(_useOfficialBracketNames); +} diff --git a/libcockatrice_settings/libcockatrice/settings/deck_editor_settings.h b/libcockatrice_settings/libcockatrice/settings/deck_editor_settings.h index 5929cf968..70f91be9b 100644 --- a/libcockatrice_settings/libcockatrice/settings/deck_editor_settings.h +++ b/libcockatrice_settings/libcockatrice/settings/deck_editor_settings.h @@ -5,6 +5,14 @@ #include +enum commanderSpellbookIntegrationEnabledIndex +{ + commanderSpellbookIntegrationEnabledIndexDisabled, + commanderSpellbookIntegrationEnabledIndexEnabled, + commanderSpellbookIntegrationEnabledIndexAutomatic, + commanderSpellbookIntegrationEnabledIndexUnprompted, +}; + class DeckEditorSettings : public SettingsManager, public IDeckEditorSettingsProvider { Q_OBJECT @@ -15,15 +23,21 @@ public: [[nodiscard]] bool getBannerCardComboBoxVisible() const override; [[nodiscard]] bool getTagsWidgetVisible() const override; [[nodiscard]] int getDefaultDeckEditorType() const override; + [[nodiscard]] int getCommanderSpellbookIntegrationEnabled() const; + [[nodiscard]] bool getCommanderSpellbookIntegrationUseOfficialBracketNames() const; void setOpenDeckInNewTab(bool _openDeckInNewTab); void setBannerCardComboBoxVisible(bool _bannerCardComboBoxVisible); void setTagsWidgetVisible(bool _tagsWidgetVisible); void setDefaultDeckEditorType(int _defaultDeckEditorType); + void setCommanderSpellbookIntegrationEnabled(int _commanderSpellbookIntegrationEnabled); + void setCommanderSpellbookIntegrationUseOfficialBracketNames(bool _useOfficialBracketNames); signals: void bannerCardComboBoxVisibleChanged(bool visible); void tagsWidgetVisibleChanged(bool visible); + void commanderSpellbookIntegrationEnabledChanged(int enabled); + void commanderSpellbookIntegrationUseOfficialBracketNamesChanged(bool useOfficialBracketNames); public: explicit DeckEditorSettings(const QString &settingPath, QObject *parent = nullptr);