This commit is contained in:
BruebachL 2026-06-20 22:56:24 -07:00 committed by GitHub
commit ba9623aa66
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 1664 additions and 7 deletions

View file

@ -298,6 +298,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_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_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/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/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_average_deck_api_response.cpp
src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_deck_api_response.cpp src/interface/widgets/tabs/api/edhrec/api_response/average_deck/edhrec_deck_api_response.cpp
@ -343,6 +350,9 @@ set(cockatrice_SOURCES
src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp src/interface/widgets/tabs/visual_deck_storage/tab_deck_storage_visual.cpp
src/interface/key_signals.cpp src/interface/key_signals.cpp
src/interface/logger.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_definitions.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.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_bracket_navigation_widget.h
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp

View file

@ -2,6 +2,8 @@
#include "../../interface/card_picture_loader/card_picture_loader_cache_method.h" #include "../../interface/card_picture_loader/card_picture_loader_cache_method.h"
#include "../../interface/card_picture_loader/card_picture_loader_local_schemes.h" #include "../../interface/card_picture_loader/card_picture_loader_local_schemes.h"
#include "../../interface/widgets/dialogs/dlg_settings.h"
#include "../../interface/widgets/settings_page/user_interface_settings_page.h"
#include "../network/update/client/release_channel.h" #include "../network/update/client/release_channel.h"
#include "card_counter_settings.h" #include "card_counter_settings.h"
#include "version_string.h" #include "version_string.h"
@ -191,6 +193,7 @@ SettingsCache::SettingsCache()
shortcutsSettings = new ShortcutsSettings(settingsPath, this); shortcutsSettings = new ShortcutsSettings(settingsPath, this);
cardDatabaseSettings = new CardDatabaseSettings(settingsPath, this); cardDatabaseSettings = new CardDatabaseSettings(settingsPath, this);
serversSettings = new ServersSettings(settingsPath, this); serversSettings = new ServersSettings(settingsPath, this);
commanderBracketSettings = new CommanderBracketSettings(settingsPath, this);
messageSettings = new MessageSettings(settingsPath, this); messageSettings = new MessageSettings(settingsPath, this);
gameFiltersSettings = new GameFiltersSettings(settingsPath, this); gameFiltersSettings = new GameFiltersSettings(settingsPath, this);
layoutsSettings = new LayoutsSettings(settingsPath, this); layoutsSettings = new LayoutsSettings(settingsPath, this);
@ -329,6 +332,22 @@ SettingsCache::SettingsCache()
deckEditorBannerCardComboBoxVisible = deckEditorBannerCardComboBoxVisible =
settings->value("interface/deckeditorbannercardcomboboxvisible", true).toBool(); settings->value("interface/deckeditorbannercardcomboboxvisible", true).toBool();
deckEditorTagsWidgetVisible = settings->value("interface/deckeditortagswidgetvisible", true).toBool(); deckEditorTagsWidgetVisible = settings->value("interface/deckeditortagswidgetvisible", true).toBool();
deckEditorCommanderSpellbookIntegrationEnabled =
settings
->value("interface/deck_editor/commander_spellbook_integration/enabled",
deckEditorCommanderSpellbookIntegrationEnabledIndexUnprompted)
.toInt();
deckEditorCommanderSpellbookIntegrationUseOfficialBracketNames =
settings->value("interface/deck_editor/commander_spellbook_integration/use_official_bracket_names", false)
.toBool();
auto definitions = commanderBracketSettings->loadDefinitions();
if (definitions.isEmpty()) {
definitions = CommanderBracketSettings::defaultDefinitions();
}
reloadBracketDefinitions(definitions);
visualDeckStorageCardSize = settings->value("interface/visualdeckstoragecardsize", 100).toInt(); visualDeckStorageCardSize = settings->value("interface/visualdeckstoragecardsize", 100).toInt();
visualDeckStorageSortingOrder = settings->value("interface/visualdeckstoragesortingorder", 0).toInt(); visualDeckStorageSortingOrder = settings->value("interface/visualdeckstoragesortingorder", 0).toInt();
visualDeckStorageShowFolders = settings->value("interface/visualdeckstorageshowfolders", true).toBool(); visualDeckStorageShowFolders = settings->value("interface/visualdeckstorageshowfolders", true).toBool();
@ -427,6 +446,22 @@ SettingsCache::SettingsCache()
clientVersion = settings->value("personal/clientversion", CLIENT_INFO_NOT_SET).toString(); clientVersion = settings->value("personal/clientversion", CLIENT_INFO_NOT_SET).toString();
} }
void SettingsCache::reloadBracketDefinitions(const QVariantList &definitions)
{
bracketDefinitions.clear();
for (const auto &entry : definitions) {
const auto map = entry.toMap();
CommanderBracketDefinition def;
def.tag = map.value("tag").toString();
def.officialName = map.value("officialName").toString();
def.displayName = map.value("displayName").toString();
def.explanation = map.value("explanation").toString();
if (!def.tag.isEmpty()) {
bracketDefinitions.addDefinition(def);
}
}
}
void SettingsCache::setUseTearOffMenus(bool _useTearOffMenus) void SettingsCache::setUseTearOffMenus(bool _useTearOffMenus)
{ {
useTearOffMenus = _useTearOffMenus; useTearOffMenus = _useTearOffMenus;
@ -834,6 +869,26 @@ void SettingsCache::setDeckEditorTagsWidgetVisible(QT_STATE_CHANGED_T _deckEdito
emit deckEditorTagsWidgetVisibleChanged(deckEditorTagsWidgetVisible); emit deckEditorTagsWidgetVisibleChanged(deckEditorTagsWidgetVisible);
} }
void SettingsCache::setDeckEditorCommanderSpellbookIntegrationEnabled(
int _deckEditorCommanderSpellbookIntegrationEnabled)
{
deckEditorCommanderSpellbookIntegrationEnabled = _deckEditorCommanderSpellbookIntegrationEnabled;
settings->setValue("interface/deck_editor/commander_spellbook_integration/enabled",
deckEditorCommanderSpellbookIntegrationEnabled);
emit deckEditorCommanderSpellbookIntegrationEnabledChanged(deckEditorCommanderSpellbookIntegrationEnabled);
}
void SettingsCache::setDeckEditorCommanderSpellbookIntegrationUseOfficialBracketNames(
bool _deckEditorCommanderSpellbookIntegrationUseOfficialBracketNames)
{
deckEditorCommanderSpellbookIntegrationUseOfficialBracketNames =
_deckEditorCommanderSpellbookIntegrationUseOfficialBracketNames;
settings->setValue("interface/deck_editor/commander_spellbook_integration/use_official_bracket_names",
deckEditorCommanderSpellbookIntegrationUseOfficialBracketNames);
emit deckEditorCommanderSpellbookIntegrationUseOfficialBracketNamesChanged(
deckEditorCommanderSpellbookIntegrationUseOfficialBracketNames);
}
void SettingsCache::setVisualDeckStorageSortingOrder(int _visualDeckStorageSortingOrder) void SettingsCache::setVisualDeckStorageSortingOrder(int _visualDeckStorageSortingOrder)
{ {
visualDeckStorageSortingOrder = _visualDeckStorageSortingOrder; visualDeckStorageSortingOrder = _visualDeckStorageSortingOrder;

View file

@ -9,6 +9,7 @@
#include "../../interface/card_picture_loader/card_picture_loader_cache_method.h" #include "../../interface/card_picture_loader/card_picture_loader_cache_method.h"
#include "../../interface/card_picture_loader/card_picture_loader_local_schemes.h" #include "../../interface/card_picture_loader/card_picture_loader_local_schemes.h"
#include "../../interface/widgets/tabs/api/commander_spellbook/commander_bracket_definitions.h"
#include "shortcuts_settings.h" #include "shortcuts_settings.h"
#include <QDate> #include <QDate>
@ -19,6 +20,7 @@
#include <libcockatrice/interfaces/interface_network_settings_provider.h> #include <libcockatrice/interfaces/interface_network_settings_provider.h>
#include <libcockatrice/settings/card_database_settings.h> #include <libcockatrice/settings/card_database_settings.h>
#include <libcockatrice/settings/card_override_settings.h> #include <libcockatrice/settings/card_override_settings.h>
#include <libcockatrice/settings/commander_bracket_settings.h>
#include <libcockatrice/settings/debug_settings.h> #include <libcockatrice/settings/debug_settings.h>
#include <libcockatrice/settings/download_settings.h> #include <libcockatrice/settings/download_settings.h>
#include <libcockatrice/settings/game_filters_settings.h> #include <libcockatrice/settings/game_filters_settings.h>
@ -158,6 +160,8 @@ signals:
void printingSelectorNavigationButtonsVisibleChanged(); void printingSelectorNavigationButtonsVisibleChanged();
void deckEditorBannerCardComboBoxVisibleChanged(bool _visible); void deckEditorBannerCardComboBoxVisibleChanged(bool _visible);
void deckEditorTagsWidgetVisibleChanged(bool _visible); void deckEditorTagsWidgetVisibleChanged(bool _visible);
void deckEditorCommanderSpellbookIntegrationEnabledChanged(int _enabled);
void deckEditorCommanderSpellbookIntegrationUseOfficialBracketNamesChanged(bool _useOfficialBracketNames);
void visualDeckStorageShowTagFilterChanged(bool _visible); void visualDeckStorageShowTagFilterChanged(bool _visible);
void visualDeckStorageDefaultTagsListChanged(); void visualDeckStorageDefaultTagsListChanged();
void visualDeckStorageShowColorIdentityChanged(bool _visible); void visualDeckStorageShowColorIdentityChanged(bool _visible);
@ -202,6 +206,7 @@ private:
ShortcutsSettings *shortcutsSettings; ShortcutsSettings *shortcutsSettings;
CardDatabaseSettings *cardDatabaseSettings; CardDatabaseSettings *cardDatabaseSettings;
ServersSettings *serversSettings; ServersSettings *serversSettings;
CommanderBracketSettings *commanderBracketSettings;
MessageSettings *messageSettings; MessageSettings *messageSettings;
GameFiltersSettings *gameFiltersSettings; GameFiltersSettings *gameFiltersSettings;
LayoutsSettings *layoutsSettings; LayoutsSettings *layoutsSettings;
@ -211,6 +216,8 @@ private:
DebugSettings *debugSettings; DebugSettings *debugSettings;
CardCounterSettings *cardCounterSettings; CardCounterSettings *cardCounterSettings;
CommanderBracketDefinitions bracketDefinitions;
QString lang; QString lang;
QString deckPath, filtersPath, replaysPath, picsPath, redirectCachePath, customPicsPath, cardDatabasePath, QString deckPath, filtersPath, replaysPath, picsPath, redirectCachePath, customPicsPath, cardDatabasePath,
customCardDatabasePath, themesPath, spoilerDatabasePath, tokenDatabasePath, themeName, homeTabBackgroundSource; customCardDatabasePath, themesPath, spoilerDatabasePath, tokenDatabasePath, themeName, homeTabBackgroundSource;
@ -253,6 +260,8 @@ private:
bool printingSelectorNavigationButtonsVisible; bool printingSelectorNavigationButtonsVisible;
bool deckEditorBannerCardComboBoxVisible; bool deckEditorBannerCardComboBoxVisible;
bool deckEditorTagsWidgetVisible; bool deckEditorTagsWidgetVisible;
int deckEditorCommanderSpellbookIntegrationEnabled;
bool deckEditorCommanderSpellbookIntegrationUseOfficialBracketNames;
int visualDeckStorageSortingOrder; int visualDeckStorageSortingOrder;
bool visualDeckStorageShowFolders; bool visualDeckStorageShowFolders;
bool visualDeckStorageShowColorIdentity; bool visualDeckStorageShowColorIdentity;
@ -734,6 +743,14 @@ public:
{ {
return openDeckInNewTab; return openDeckInNewTab;
} }
[[nodiscard]] int getDeckEditorCommanderSpellbookIntegrationEnabled() const
{
return deckEditorCommanderSpellbookIntegrationEnabled;
}
[[nodiscard]] bool getDeckEditorCommanderSpellbookIntegrationUseOfficialBracketNames() const
{
return deckEditorCommanderSpellbookIntegrationUseOfficialBracketNames;
}
[[nodiscard]] int getRewindBufferingMs() const [[nodiscard]] int getRewindBufferingMs() const
{ {
return rewindBufferingMs; return rewindBufferingMs;
@ -986,6 +1003,15 @@ public:
{ {
return *serversSettings; return *serversSettings;
} }
[[nodiscard]] CommanderBracketSettings &commanderBrackets() const
{
return *commanderBracketSettings;
}
void reloadBracketDefinitions(const QVariantList &definitions);
CommanderBracketDefinitions &commanderBracketDefs()
{
return bracketDefinitions;
}
[[nodiscard]] MessageSettings &messages() const [[nodiscard]] MessageSettings &messages() const
{ {
return *messageSettings; return *messageSettings;
@ -1082,6 +1108,9 @@ public slots:
void setPrintingSelectorNavigationButtonsVisible(QT_STATE_CHANGED_T _navigationButtonsVisible); void setPrintingSelectorNavigationButtonsVisible(QT_STATE_CHANGED_T _navigationButtonsVisible);
void setDeckEditorBannerCardComboBoxVisible(QT_STATE_CHANGED_T _deckEditorBannerCardComboBoxVisible); void setDeckEditorBannerCardComboBoxVisible(QT_STATE_CHANGED_T _deckEditorBannerCardComboBoxVisible);
void setDeckEditorTagsWidgetVisible(QT_STATE_CHANGED_T _deckEditorTagsWidgetVisible); void setDeckEditorTagsWidgetVisible(QT_STATE_CHANGED_T _deckEditorTagsWidgetVisible);
void setDeckEditorCommanderSpellbookIntegrationEnabled(int _deckEditorCommanderSpellbookIntegrationEnabled);
void setDeckEditorCommanderSpellbookIntegrationUseOfficialBracketNames(
bool _deckEditorCommanderSpellbookIntegrationUseOfficialBracketNames);
void setVisualDeckStorageSortingOrder(int _visualDeckStorageSortingOrder); void setVisualDeckStorageSortingOrder(int _visualDeckStorageSortingOrder);
void setVisualDeckStorageShowFolders(QT_STATE_CHANGED_T value); void setVisualDeckStorageShowFolders(QT_STATE_CHANGED_T value);
void setVisualDeckStorageShowTagFilter(QT_STATE_CHANGED_T _showTags); void setVisualDeckStorageShowTagFilter(QT_STATE_CHANGED_T _showTags);

View file

@ -1,13 +1,20 @@
#include "deck_editor_deck_dock_widget.h" #include "deck_editor_deck_dock_widget.h"
#include "../../../client/settings/cache_settings.h" #include "../../../client/settings/cache_settings.h"
#include "../settings_page/user_interface_settings_page.h"
#include "../tabs/api/commander_spellbook/commander_bracket_service.h"
#include "../tabs/api/commander_spellbook/commander_spellbook_api_accessor.h"
#include "../tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.h"
#include "deck_list_style_proxy.h" #include "deck_list_style_proxy.h"
#include "deck_state_manager.h" #include "deck_state_manager.h"
#include <QComboBox> #include <QComboBox>
#include <QDialogButtonBox>
#include <QDockWidget> #include <QDockWidget>
#include <QFormLayout>
#include <QHeaderView> #include <QHeaderView>
#include <QLabel> #include <QLabel>
#include <QMessageBox>
#include <QSplitter> #include <QSplitter>
#include <QTextEdit> #include <QTextEdit>
#include <libcockatrice/card/database/card_database_manager.h> #include <libcockatrice/card/database/card_database_manager.h>
@ -131,6 +138,40 @@ void DeckEditorDeckDockWidget::createDeckDock()
formatComboBox->addItem(tr("Loading Database...")); formatComboBox->addItem(tr("Loading Database..."));
formatComboBox->setEnabled(false); // Disable until loaded formatComboBox->setEnabled(false); // Disable until loaded
// --- Commander bracket row (hidden, unless format is 'commander') ---
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, &DeckEditorDeckDockWidget::requestBracketEstimate);
if (SettingsCache::instance().getDeckEditorCommanderSpellbookIntegrationEnabled() !=
deckEditorCommanderSpellbookIntegrationEnabledIndexUnprompted) {
connect(&SettingsCache::instance(), &SettingsCache::deckEditorCommanderSpellbookIntegrationEnabledChanged, this,
&DeckEditorDeckDockWidget::maybeAutoEstimateBracket);
connect(&SettingsCache::instance(),
&SettingsCache::deckEditorCommanderSpellbookIntegrationUseOfficialBracketNamesChanged, this,
&DeckEditorDeckDockWidget::maybeAutoEstimateBracket);
}
bracketLabel->setVisible(false);
bracketValueLabel->setVisible(false);
bracketInfoButton->setVisible(false);
bracketRefreshButton->setVisible(false);
connect(&CommanderBracketService::instance(), &CommanderBracketService::estimateFinished, this,
&DeckEditorDeckDockWidget::onEstimateBracketFinished);
commentsLabel = new QLabel(); commentsLabel = new QLabel();
commentsLabel->setObjectName("commentsLabel"); commentsLabel->setObjectName("commentsLabel");
commentsEdit = new QTextEdit; commentsEdit = new QTextEdit;
@ -216,13 +257,23 @@ void DeckEditorDeckDockWidget::createDeckDock()
upperLayout->addWidget(formatLabel, 2, 0); upperLayout->addWidget(formatLabel, 2, 0);
upperLayout->addWidget(formatComboBox, 2, 1); upperLayout->addWidget(formatComboBox, 2, 1);
upperLayout->addWidget(bannerCardLabel, 3, 0); upperLayout->addWidget(bracketLabel, 3, 0);
upperLayout->addWidget(bannerCardComboBox, 3, 1);
upperLayout->addWidget(deckTagsDisplayWidget, 4, 1); auto *bracketRow = new QHBoxLayout;
bracketRow->addWidget(bracketValueLabel);
bracketRow->addWidget(bracketInfoButton);
bracketRow->addWidget(bracketRefreshButton);
bracketRow->addStretch();
upperLayout->addWidget(activeGroupCriteriaLabel, 5, 0); upperLayout->addLayout(bracketRow, 3, 1);
upperLayout->addWidget(activeGroupCriteriaComboBox, 5, 1);
upperLayout->addWidget(bannerCardLabel, 4, 0);
upperLayout->addWidget(bannerCardComboBox, 4, 1);
upperLayout->addWidget(deckTagsDisplayWidget, 5, 1);
upperLayout->addWidget(activeGroupCriteriaLabel, 6, 0);
upperLayout->addWidget(activeGroupCriteriaComboBox, 6, 1);
hashLabel1 = new QLabel(); hashLabel1 = new QLabel();
hashLabel1->setObjectName("hashLabel1"); hashLabel1->setObjectName("hashLabel1");
@ -280,6 +331,147 @@ void DeckEditorDeckDockWidget::createDeckDock()
} }
} }
bool DeckEditorDeckDockWidget::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().getDeckEditorCommanderSpellbookIntegrationUseOfficialBracketNames() ? 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().setDeckEditorCommanderSpellbookIntegrationUseOfficialBracketNames(useOfficial);
}
connect(&SettingsCache::instance(), &SettingsCache::deckEditorCommanderSpellbookIntegrationEnabledChanged, this,
&DeckEditorDeckDockWidget::maybeAutoEstimateBracket);
connect(&SettingsCache::instance(),
&SettingsCache::deckEditorCommanderSpellbookIntegrationUseOfficialBracketNamesChanged, this,
&DeckEditorDeckDockWidget::maybeAutoEstimateBracket);
// Persist integration mode
if (clickedButton == disableBtn) {
SettingsCache::instance().setDeckEditorCommanderSpellbookIntegrationEnabled(
deckEditorCommanderSpellbookIntegrationEnabledIndexDisabled);
return false;
}
if (clickedButton == enableBtn) {
SettingsCache::instance().setDeckEditorCommanderSpellbookIntegrationEnabled(
deckEditorCommanderSpellbookIntegrationEnabledIndexEnabled);
return true;
}
if (clickedButton == automaticBtn) {
SettingsCache::instance().setDeckEditorCommanderSpellbookIntegrationEnabled(
deckEditorCommanderSpellbookIntegrationEnabledIndexAutomatic);
return true;
}
return false;
}
void DeckEditorDeckDockWidget::updateBracketVisibility(bool visible)
{
bracketLabel->setVisible(visible);
bracketValueLabel->setVisible(visible);
bracketInfoButton->setVisible(visible);
bracketRefreshButton->setVisible(visible);
}
void DeckEditorDeckDockWidget::requestBracketEstimate()
{
bracketRefreshButton->setEnabled(false);
bracketInfoButton->setEnabled(false);
bracketValueLabel->setText(tr("Calculating…"));
requestId = CommanderBracketService::instance().estimateBracket(deckStateManager->getDeckList(), this);
}
void DeckEditorDeckDockWidget::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().getDeckEditorCommanderSpellbookIntegrationUseOfficialBracketNames()
? result.officialName
: result.displayName);
bracketRefreshButton->setEnabled(true);
// Build tooltip
QString tooltip;
for (const auto &section : lastBracketExplanation.sections) {
tooltip += "<b>" + section.title + "</b><br>";
for (const auto &line : section.bulletPoints) {
tooltip += "" + line + "<br>";
}
tooltip += "<br>";
}
bracketInfoButton->setToolTip(tooltip);
bracketInfoButton->setEnabled(!tooltip.isEmpty());
}
void DeckEditorDeckDockWidget::initializeFormats() void DeckEditorDeckDockWidget::initializeFormats()
{ {
QStringList allFormats = CardDatabaseManager::query()->getAllFormatsWithCount().keys(); QStringList allFormats = CardDatabaseManager::query()->getAllFormatsWithCount().keys();
@ -300,15 +492,70 @@ void DeckEditorDeckDockWidget::initializeFormats()
// Ensure no selection is visible initially // Ensure no selection is visible initially
formatComboBox->setCurrentIndex(-1); formatComboBox->setCurrentIndex(-1);
} }
connect(formatComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index) { connect(formatComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index) {
QString formatKey;
if (index >= 0) { if (index >= 0) {
QString formatKey = formatComboBox->itemData(index).toString(); formatKey = formatComboBox->itemData(index).toString();
deckStateManager->setFormat(formatKey); deckStateManager->setFormat(formatKey);
} else { } else {
deckStateManager->setFormat(""); // clear format if deselected deckStateManager->setFormat(""); // clear format if deselected
} }
const bool isCommander = (formatKey.compare("commander", Qt::CaseInsensitive) == 0);
const bool commanderSpellbookIntegrationEnabled =
SettingsCache::instance().getDeckEditorCommanderSpellbookIntegrationEnabled() !=
deckEditorCommanderSpellbookIntegrationEnabledIndexDisabled;
const bool bracketVisible = isCommander && commanderSpellbookIntegrationEnabled;
updateBracketVisibility(bracketVisible);
if (!isCommander) {
bracketValueLabel->setText("-");
bracketInfoButton->setToolTip({});
bracketInfoButton->setEnabled(false);
bracketRefreshButton->setEnabled(false);
} else {
bracketRefreshButton->setEnabled(true);
maybeAutoEstimateBracket();
}
}); });
maybeAutoEstimateBracket();
}
void DeckEditorDeckDockWidget::maybeAutoEstimateBracket()
{
const QString formatKey = deckStateManager->getDeckList().getGameFormat();
const bool isCommander = (formatKey.compare("commander", Qt::CaseInsensitive) == 0);
int mode = SettingsCache::instance().getDeckEditorCommanderSpellbookIntegrationEnabled();
if (!isCommander || mode == deckEditorCommanderSpellbookIntegrationEnabledIndexDisabled) {
updateBracketVisibility(false);
return;
}
if (mode == deckEditorCommanderSpellbookIntegrationEnabledIndexUnprompted) {
if (!promptCommanderSpellbookIntegration()) {
updateBracketVisibility(false);
return;
}
}
updateBracketVisibility(true);
mode = SettingsCache::instance().getDeckEditorCommanderSpellbookIntegrationEnabled();
if (mode != deckEditorCommanderSpellbookIntegrationEnabledIndexAutomatic) {
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, &DeckEditorDeckDockWidget::requestBracketEstimate);
} }
ExactCard DeckEditorDeckDockWidget::getCurrentCard() ExactCard DeckEditorDeckDockWidget::getCurrentCard()
@ -743,6 +990,8 @@ void DeckEditorDeckDockWidget::retranslateUi()
commentsLabel->setText(tr("&Comments:")); commentsLabel->setText(tr("&Comments:"));
activeGroupCriteriaLabel->setText(tr("Group by:")); activeGroupCriteriaLabel->setText(tr("Group by:"));
formatLabel->setText(tr("Format:")); formatLabel->setText(tr("Format:"));
bracketInfoButton->setToolTip(tr("Why this bracket?"));
bracketRefreshButton->setToolTip(tr("Recalculate bracket"));
hashLabel1->setText(tr("Hash:")); hashLabel1->setText(tr("Hash:"));

View file

@ -10,6 +10,7 @@
#include "../../../interface/widgets/tabs/abstract_tab_deck_editor.h" #include "../../../interface/widgets/tabs/abstract_tab_deck_editor.h"
#include "../../key_signals.h" #include "../../key_signals.h"
#include "../tabs/api/commander_spellbook/commander_spellbook_bracket_explainer.h"
#include "../utility/custom_line_edit.h" #include "../utility/custom_line_edit.h"
#include "../visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.h" #include "../visual_deck_storage/deck_preview/deck_preview_deck_tags_display_widget.h"
#include "deck_list_history_manager_widget.h" #include "deck_list_history_manager_widget.h"
@ -21,6 +22,7 @@
#include <QTreeView> #include <QTreeView>
#include <libcockatrice/card/card_info.h> #include <libcockatrice/card/card_info.h>
struct CommanderBracketEstimate;
class DeckListModel; class DeckListModel;
class AbstractTabDeckEditor; class AbstractTabDeckEditor;
class DeckEditorDeckDockWidget : public QDockWidget class DeckEditorDeckDockWidget : public QDockWidget
@ -33,6 +35,9 @@ public:
QTreeView *deckView; QTreeView *deckView;
QComboBox *bannerCardComboBox; QComboBox *bannerCardComboBox;
void createDeckDock(); void createDeckDock();
bool promptCommanderSpellbookIntegration();
void updateBracketVisibility(bool visible);
void requestBracketEstimate();
ExactCard getCurrentCard(); ExactCard getCurrentCard();
void retranslateUi(); void retranslateUi();
@ -59,6 +64,8 @@ public slots:
void actSwapSelection(); void actSwapSelection();
void actRemoveCard(); void actRemoveCard();
void initializeFormats(); void initializeFormats();
void maybeAutoEstimateBracket();
void onEstimateBracketFinished(quint64 id, QObject *requester, const CommanderBracketEstimate &result);
signals: signals:
void selectedCardChanged(const ExactCard &card); void selectedCardChanged(const ExactCard &card);
@ -89,6 +96,15 @@ private:
QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard; QAction *aRemoveCard, *aIncrement, *aDecrement, *aSwapCard;
QLabel *bracketLabel;
QLabel *bracketValueLabel;
QToolButton *bracketInfoButton;
QToolButton *bracketRefreshButton;
BracketExplanation lastBracketExplanation;
quint64 requestId;
DeckListModel *getModel() const; DeckListModel *getModel() const;
[[nodiscard]] QModelIndexList getSelectedCardNodeSourceIndices() const; [[nodiscard]] QModelIndexList getSelectedCardNodeSourceIndices() const;
void offsetCountAtIndex(const QModelIndex &idx, bool isIncrement); void offsetCountAtIndex(const QModelIndex &idx, bool isIncrement);

View file

@ -149,6 +149,56 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
connect(&defaultDeckEditorTypeSelector, QOverload<int>::of(&QComboBox::currentIndexChanged), connect(&defaultDeckEditorTypeSelector, QOverload<int>::of(&QComboBox::currentIndexChanged),
&SettingsCache::instance(), &SettingsCache::setDefaultDeckEditorType); &SettingsCache::instance(), &SettingsCache::setDefaultDeckEditorType);
deckEditorCommanderSpellBookIntegrationUseOfficialBracketNamesExplainer.setText("?");
deckEditorCommanderSpellBookIntegrationUseOfficialBracketNamesExplainer.setAutoRaise(true);
deckEditorCommanderSpellBookIntegrationUseOfficialBracketNamesExplainer.setEnabled(false);
// Add items with userData = internal enum
deckEditorCommanderSpellbookIntegrationEnabledSelector.addItem(
tr("Disabled"), deckEditorCommanderSpellbookIntegrationEnabledIndexDisabled);
deckEditorCommanderSpellbookIntegrationEnabledSelector.addItem(
tr("Enabled"), deckEditorCommanderSpellbookIntegrationEnabledIndexEnabled);
deckEditorCommanderSpellbookIntegrationEnabledSelector.addItem(
tr("Automatic"), deckEditorCommanderSpellbookIntegrationEnabledIndexAutomatic);
int storedMode = SettingsCache::instance().getDeckEditorCommanderSpellbookIntegrationEnabled();
for (int i = 0; i < deckEditorCommanderSpellbookIntegrationEnabledSelector.count(); ++i) {
if (deckEditorCommanderSpellbookIntegrationEnabledSelector.itemData(i).toInt() == storedMode) {
deckEditorCommanderSpellbookIntegrationEnabledSelector.setCurrentIndex(i);
break;
}
}
connect(&deckEditorCommanderSpellbookIntegrationEnabledSelector,
QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index) {
int mode = deckEditorCommanderSpellbookIntegrationEnabledSelector.itemData(index).toInt();
SettingsCache::instance().setDeckEditorCommanderSpellbookIntegrationEnabled(mode);
updateCommanderSpellbookUiState();
});
deckEditorCommanderSpellbookIntegrationBracketNamingSelector.addItem(
tr("CommanderSpellbook bracket names")); // index 0 = false
deckEditorCommanderSpellbookIntegrationBracketNamingSelector.addItem(
tr("Official Commander bracket names (approximate)")); // index 1 = true
deckEditorCommanderSpellbookIntegrationBracketNamingSelector.setCurrentIndex(
SettingsCache::instance().getDeckEditorCommanderSpellbookIntegrationUseOfficialBracketNames() ? 1 : 0);
connect(&deckEditorCommanderSpellbookIntegrationBracketNamingSelector,
QOverload<int>::of(&QComboBox::currentIndexChanged), &SettingsCache::instance(), [](int index) {
SettingsCache::instance().setDeckEditorCommanderSpellbookIntegrationUseOfficialBracketNames(index == 1);
});
updateCommanderSpellbookUiState();
auto *labelLayout = new QHBoxLayout;
labelLayout->setContentsMargins(0, 0, 0, 0);
labelLayout->addWidget(&deckEditorCommanderSpellbookIntegrationUseOfficialBracketNamesLabel);
labelLayout->addWidget(&deckEditorCommanderSpellBookIntegrationUseOfficialBracketNamesExplainer);
auto *labelWidget = new QWidget;
labelWidget->setLayout(labelLayout);
auto *deckEditorGrid = new QGridLayout; auto *deckEditorGrid = new QGridLayout;
deckEditorGrid->addWidget(&openDeckInNewTabCheckBox, 0, 0); deckEditorGrid->addWidget(&openDeckInNewTabCheckBox, 0, 0);
deckEditorGrid->addWidget(&visualDeckStorageInGameCheckBox, 1, 0); deckEditorGrid->addWidget(&visualDeckStorageInGameCheckBox, 1, 0);
@ -157,6 +207,10 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
deckEditorGrid->addWidget(&visualDeckStoragePromptForConversionSelector, 3, 1); deckEditorGrid->addWidget(&visualDeckStoragePromptForConversionSelector, 3, 1);
deckEditorGrid->addWidget(&defaultDeckEditorTypeLabel, 4, 0); deckEditorGrid->addWidget(&defaultDeckEditorTypeLabel, 4, 0);
deckEditorGrid->addWidget(&defaultDeckEditorTypeSelector, 4, 1); deckEditorGrid->addWidget(&defaultDeckEditorTypeSelector, 4, 1);
deckEditorGrid->addWidget(&deckEditorCommanderSpellbookIntegrationEnabledLabel, 5, 0);
deckEditorGrid->addWidget(&deckEditorCommanderSpellbookIntegrationEnabledSelector, 5, 1);
deckEditorGrid->addWidget(labelWidget, 6, 0);
deckEditorGrid->addWidget(&deckEditorCommanderSpellbookIntegrationBracketNamingSelector);
deckEditorGroupBox = new QGroupBox; deckEditorGroupBox = new QGroupBox;
deckEditorGroupBox->setLayout(deckEditorGrid); deckEditorGroupBox->setLayout(deckEditorGrid);
@ -199,6 +253,27 @@ void UserInterfaceSettingsPage::setNotificationEnabled(QT_STATE_CHANGED_T i)
} }
} }
void UserInterfaceSettingsPage::updateCommanderSpellbookUiState()
{
const int mode = SettingsCache::instance().getDeckEditorCommanderSpellbookIntegrationEnabled();
const bool enabled = mode != deckEditorCommanderSpellbookIntegrationEnabledIndexDisabled &&
mode != deckEditorCommanderSpellbookIntegrationEnabledIndexUnprompted;
deckEditorCommanderSpellbookIntegrationBracketNamingSelector.setEnabled(enabled);
deckEditorCommanderSpellBookIntegrationUseOfficialBracketNamesExplainer.setEnabled(enabled);
deckEditorCommanderSpellbookIntegrationUseOfficialBracketNamesLabel.setVisible(enabled);
deckEditorCommanderSpellBookIntegrationUseOfficialBracketNamesExplainer.setVisible(enabled);
deckEditorCommanderSpellbookIntegrationBracketNamingSelector.setVisible(enabled);
if (enabled) {
// Sync selector with the current stored bool
const bool useOfficial =
SettingsCache::instance().getDeckEditorCommanderSpellbookIntegrationUseOfficialBracketNames();
deckEditorCommanderSpellbookIntegrationBracketNamingSelector.setCurrentIndex(useOfficial ? 1 : 0);
}
}
void UserInterfaceSettingsPage::retranslateUi() void UserInterfaceSettingsPage::retranslateUi()
{ {
generalGroupBox->setTitle(tr("General interface settings")); generalGroupBox->setTitle(tr("General interface settings"));
@ -236,6 +311,23 @@ void UserInterfaceSettingsPage::retranslateUi()
defaultDeckEditorTypeLabel.setText(tr("Default deck editor type")); defaultDeckEditorTypeLabel.setText(tr("Default deck editor type"));
defaultDeckEditorTypeSelector.setItemText(TabSupervisor::ClassicDeckEditor, tr("Classic Deck Editor")); defaultDeckEditorTypeSelector.setItemText(TabSupervisor::ClassicDeckEditor, tr("Classic Deck Editor"));
defaultDeckEditorTypeSelector.setItemText(TabSupervisor::VisualDeckEditor, tr("Visual Deck Editor")); defaultDeckEditorTypeSelector.setItemText(TabSupervisor::VisualDeckEditor, tr("Visual Deck Editor"));
deckEditorCommanderSpellbookIntegrationEnabledLabel.setText(
tr("CommanderSpellbook integration to estimate commander bracket"));
deckEditorCommanderSpellbookIntegrationEnabledSelector.setItemText(
deckEditorCommanderSpellbookIntegrationEnabledIndexDisabled, tr("Disabled"));
deckEditorCommanderSpellbookIntegrationEnabledSelector.setItemText(
deckEditorCommanderSpellbookIntegrationEnabledIndexEnabled, tr("Enabled"));
deckEditorCommanderSpellbookIntegrationEnabledSelector.setItemText(
deckEditorCommanderSpellbookIntegrationEnabledIndexAutomatic, tr("Automatic"));
deckEditorCommanderSpellbookIntegrationUseOfficialBracketNamesLabel.setText(tr("Bracket naming"));
deckEditorCommanderSpellbookIntegrationBracketNamingSelector.setItemText(
0, CommanderBracketNames::CommanderSpellbookBracketNames);
deckEditorCommanderSpellbookIntegrationBracketNamingSelector.setItemText(
1, CommanderBracketNames::OfficialCommanderBracketNames);
deckEditorCommanderSpellBookIntegrationUseOfficialBracketNamesExplainer.setToolTip(
CommanderBracketNames::Explainer);
replayGroupBox->setTitle(tr("Replay settings")); replayGroupBox->setTitle(tr("Replay settings"));
rewindBufferingMsLabel.setText(tr("Buffer time for backwards skip via shortcut:")); rewindBufferingMsLabel.setText(tr("Buffer time for backwards skip via shortcut:"));
rewindBufferingMsBox.setSuffix(" ms"); rewindBufferingMsBox.setSuffix(" ms");

View file

@ -8,13 +8,38 @@
#include <QGroupBox> #include <QGroupBox>
#include <QLabel> #include <QLabel>
#include <QSpinBox> #include <QSpinBox>
#include <QToolButton>
#include <libcockatrice/utility/macros.h> #include <libcockatrice/utility/macros.h>
enum deckEditorCommanderSpellbookIntegrationEnabledIndex
{
deckEditorCommanderSpellbookIntegrationEnabledIndexDisabled,
deckEditorCommanderSpellbookIntegrationEnabledIndexEnabled,
deckEditorCommanderSpellbookIntegrationEnabledIndexAutomatic,
deckEditorCommanderSpellbookIntegrationEnabledIndexUnprompted,
};
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
class UserInterfaceSettingsPage : public AbstractSettingsPage class UserInterfaceSettingsPage : public AbstractSettingsPage
{ {
Q_OBJECT Q_OBJECT
private slots: private slots:
void setNotificationEnabled(QT_STATE_CHANGED_T); void setNotificationEnabled(QT_STATE_CHANGED_T);
void updateCommanderSpellbookUiState();
private: private:
QCheckBox notificationsEnabledCheckBox; QCheckBox notificationsEnabledCheckBox;
@ -39,6 +64,11 @@ private:
QCheckBox visualDeckStorageSelectionAnimationCheckBox; QCheckBox visualDeckStorageSelectionAnimationCheckBox;
QLabel defaultDeckEditorTypeLabel; QLabel defaultDeckEditorTypeLabel;
QComboBox defaultDeckEditorTypeSelector; QComboBox defaultDeckEditorTypeSelector;
QLabel deckEditorCommanderSpellbookIntegrationEnabledLabel;
QComboBox deckEditorCommanderSpellbookIntegrationEnabledSelector;
QLabel deckEditorCommanderSpellbookIntegrationUseOfficialBracketNamesLabel;
QToolButton deckEditorCommanderSpellBookIntegrationUseOfficialBracketNamesExplainer;
QComboBox deckEditorCommanderSpellbookIntegrationBracketNamingSelector;
QLabel rewindBufferingMsLabel; QLabel rewindBufferingMsLabel;
QSpinBox rewindBufferingMsBox; QSpinBox rewindBufferingMsBox;
QGroupBox *generalGroupBox; QGroupBox *generalGroupBox;

View file

@ -0,0 +1,21 @@
#include "card_in_deck_request.h"
void CardInDeckRequest::fromJson(const QJsonObject &json)
{
card = json.value("card").toString();
quantity = json.value("quantity").toInt();
}
QJsonObject CardInDeckRequest::toJson() const
{
QJsonObject json;
json.insert("card", card);
json.insert("quantity", quantity);
return json;
}
void CardInDeckRequest::debugPrint() const
{
qDebug() << "Card:" << card;
qDebug() << "Quantity:" << quantity;
}

View file

@ -0,0 +1,23 @@
#ifndef COCKATRICE_CARD_IN_DECK_REQUEST_H
#define COCKATRICE_CARD_IN_DECK_REQUEST_H
#include <QJsonObject>
class CardInDeckRequest
{
public:
// Constructor
CardInDeckRequest() = default;
// Parse deck-related data from JSON
void fromJson(const QJsonObject &json);
QJsonObject toJson() const;
// Debug method for logging
void debugPrint() const;
private:
QString card;
int quantity;
};
#endif // COCKATRICE_CARD_IN_DECK_REQUEST_H

View file

@ -0,0 +1,22 @@
#include "commander_spellbook_card_result.h"
void CommanderSpellbookCardResult::fromJson(const QJsonObject &json)
{
id = json.value("id").toString();
name = json.value("name").toString();
oracleId = json.value("oracleId").toString();
spoiler = json.value("spoiler").toBool();
typeLine = json.value("typeLine").toString();
imageUriFrontPng = json.value("imageUriFrontPng").toString();
imageUriFrontLarge = json.value("imageUriFrontLarge").toString();
imageUriFrontNormal = json.value("imageUriFrontNormal").toString();
imageUriFrontSmall = json.value("imageUriFrontSmall").toString();
imageUriFrontArtCrop = json.value("imageUriFrontArtCrop").toString();
imageUriBackPng = json.value("imageUriBackPng").toString();
imageUriBackLarge = json.value("imageUriBackLarge").toString();
imageUriBackNormal = json.value("imageUriBackNormal").toString();
imageUriBackSmall = json.value("imageUriBackSmall").toString();
imageUriBackArtCrop = json.value("imageUriBackArtCrop").toString();
}

View file

@ -0,0 +1,30 @@
#ifndef COCKATRICE_COMMANDER_SPELLBOOK_CARD_RESULT_H
#define COCKATRICE_COMMANDER_SPELLBOOK_CARD_RESULT_H
#include <QJsonObject>
#include <QString>
class CommanderSpellbookCardResult
{
public:
void 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

View file

@ -0,0 +1,118 @@
#include "commander_spellbook_deck_request.h"
#include <QDebug>
#include <QJsonArray>
#include <libcockatrice/deck_list/deck_list.h>
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
void CommanderSpellbookDeckRequest::fromJson(const QJsonObject &json)
{
mainDeck.clear();
commanderDeck.clear();
// Main deck
const QJsonArray mainArray = json.value("main").toArray();
for (const QJsonValue &value : mainArray) {
if (!value.isObject()) {
continue;
}
CardInDeckRequest card;
card.fromJson(value.toObject());
mainDeck.append(card);
// Max size allowed by commanderspellbook
if (mainDeck.size() >= 600) {
break;
}
}
// Commanders
const QJsonArray commanderArray = json.value("commanders").toArray();
for (const QJsonValue &value : commanderArray) {
if (!value.isObject()) {
continue;
}
CardInDeckRequest card;
card.fromJson(value.toObject());
commanderDeck.append(card);
// Max size allowed by commanderspellbook
if (commanderDeck.size() >= 12) {
break;
}
}
}
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;
}
void CommanderSpellbookDeckRequest::fromDeckList(const DeckList &deck)
{
mainDeck.clear();
commanderDeck.clear();
// --- Mainboard ---
const auto mainCards = deck.getCardNodes({DECK_ZONE_MAIN});
for (const DecklistCardNode *node : mainCards) {
if (!node) {
continue;
}
CardInDeckRequest req;
QJsonObject json;
json.insert("card", node->getName());
json.insert("quantity", node->getNumber());
req.fromJson(json);
mainDeck.append(req);
// Max size allowed by commanderspellbook
if (mainDeck.size() >= 600) {
break;
}
}
// --- Commander (bannerCard) ---
const auto &metadata = deck.getMetadata();
if (!metadata.bannerCard.name.isEmpty()) {
CardInDeckRequest commander;
QJsonObject json;
json.insert("card", metadata.bannerCard.name);
json.insert("quantity", 1);
commander.fromJson(json);
commanderDeck.append(commander);
}
}
void CommanderSpellbookDeckRequest::debugPrint() const
{
qDebug() << "Main deck:";
for (const CardInDeckRequest &card : mainDeck) {
card.debugPrint();
}
qDebug() << "Commanders:";
for (const CardInDeckRequest &card : commanderDeck) {
card.debugPrint();
}
}

View file

@ -0,0 +1,34 @@
#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 <QJsonObject>
#include <QVector>
class CommanderSpellbookDeckRequest
{
public:
CommanderSpellbookDeckRequest() = default;
void fromJson(const QJsonObject &json);
QJsonObject toJson() const;
void fromDeckList(const DeckList &deck);
void debugPrint() const;
const QVector<CardInDeckRequest> &main() const
{
return mainDeck;
}
const QVector<CardInDeckRequest> &commanders() const
{
return commanderDeck;
}
private:
QVector<CardInDeckRequest> mainDeck; // maxItems: 600
QVector<CardInDeckRequest> commanderDeck; // maxItems: 12
};
#endif // COCKATRICE_COMMANDER_SPELLBOOK_DECK_REQUEST_H

View file

@ -0,0 +1,107 @@
#include "commander_spellbook_estimate_bracket_result.h"
void EstimateBracketResult::fromJson(const QJsonObject &json)
{
bracketTag = json.value("bracketTag").toString();
gameChangerCards.clear();
massLandDenialCards.clear();
extraTurnCards.clear();
massLandDenialTemplates.clear();
extraTurnTemplates.clear();
massLandDenialCombos.clear();
extraTurnCombos.clear();
lockCombos.clear();
skipTurnsCombos.clear();
definitelyTwoCardCombos.clear();
arguablyTwoCardCombos.clear();
//
// Cards
//
for (const auto &value : json.value("cards").toArray()) {
if (!value.isObject()) {
continue;
}
const QJsonObject obj = value.toObject();
CommanderSpellbookCardResult card;
card.fromJson(obj.value("card").toObject());
if (obj.value("gameChanger").toBool()) {
gameChangerCards.append(card);
}
if (obj.value("massLandDenial").toBool()) {
massLandDenialCards.append(card);
}
if (obj.value("extraTurn").toBool()) {
extraTurnCards.append(card);
}
}
//
// Templates
//
for (const auto &value : json.value("templates").toArray()) {
if (!value.isObject()) {
continue;
}
const QJsonObject obj = value.toObject();
CommanderSpellbookVariantResult variant;
variant.fromJson(obj);
if (obj.value("massLandDenial").toBool()) {
massLandDenialTemplates.append(variant);
}
if (obj.value("extraTurn").toBool()) {
extraTurnTemplates.append(variant);
}
}
//
// Combos
//
for (const auto &value : json.value("combos").toArray()) {
if (!value.isObject()) {
continue;
}
const QJsonObject obj = value.toObject();
CommanderSpellbookVariantResult combo;
combo.fromJson(obj);
if (obj.value("massLandDenial").toBool()) {
massLandDenialCombos.append(combo);
}
if (obj.value("extraTurn").toBool()) {
extraTurnCombos.append(combo);
}
if (obj.value("lock").toBool()) {
lockCombos.append(combo);
}
if (obj.value("skipTurns").toBool()) {
skipTurnsCombos.append(combo);
}
if (obj.value("definitelyTwoCard").toBool()) {
definitelyTwoCardCombos.append(combo);
}
if (obj.value("arguablyTwoCard").toBool()) {
arguablyTwoCardCombos.append(combo);
}
}
}

View file

@ -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 <QVector>
class EstimateBracketResult
{
public:
void fromJson(const QJsonObject &json);
QString bracketTag;
QVector<CommanderSpellbookCardResult> gameChangerCards;
QVector<CommanderSpellbookCardResult> massLandDenialCards;
QVector<CommanderSpellbookCardResult> extraTurnCards;
QVector<CommanderSpellbookVariantResult> massLandDenialTemplates;
QVector<CommanderSpellbookVariantResult> extraTurnTemplates;
QVector<CommanderSpellbookVariantResult> massLandDenialCombos;
QVector<CommanderSpellbookVariantResult> extraTurnCombos;
QVector<CommanderSpellbookVariantResult> lockCombos;
QVector<CommanderSpellbookVariantResult> skipTurnsCombos;
QVector<CommanderSpellbookVariantResult> definitelyTwoCardCombos;
QVector<CommanderSpellbookVariantResult> arguablyTwoCardCombos;
};
#endif

View file

@ -0,0 +1,31 @@
#include "commander_spellbook_variant_result.h"
void CommanderSpellbookVariantResult::fromJson(const QJsonObject &json)
{
id = json.value("id").toString();
status = json.value("status").toString();
uses = json.value("uses").toArray();
cardRequires = json.value("requires").toArray();
produces = json.value("produces").toArray();
of = json.value("of").toArray();
includes = json.value("includes").toArray();
manaNeeded = json.value("manaNeeded").toArray();
manaValueNeeded = json.value("manaValueNeeded").toArray();
easyPrerequisites = json.value("easyPrerequisites").toArray();
notablePrerequisites = json.value("notablePrerequisites").toArray();
description = json.value("description").toString();
notes = json.value("notes").toString();
popularity = json.value("popularity").toDouble();
spoiler = json.value("spoiler").toBool();
bracketTag = json.value("bracketTag").toString();
legalities = json.value("legalities").toObject();
prices = json.value("prices").toObject();
variantCount = json.value("variantCount").toInt();
}

View file

@ -0,0 +1,40 @@
#ifndef COCKATRICE_COMMANDER_SPELLBOOK_VARIANT_RESULT_H
#define COCKATRICE_COMMANDER_SPELLBOOK_VARIANT_RESULT_H
#include <QJsonArray>
#include <QJsonObject>
class CommanderSpellbookVariantResult
{
public:
void 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

View file

@ -0,0 +1,49 @@
#include "commander_bracket_definitions.h"
void CommanderBracketDefinitions::clear()
{
definitions.clear();
}
void CommanderBracketDefinitions::addDefinition(const CommanderBracketDefinition &definition)
{
definitions.insert(definition.tag, definition);
}
QString CommanderBracketDefinitions::officialName(const QString &tag) const
{
auto it = definitions.find(tag);
if (it == definitions.end()) {
return tag;
}
return it->officialName;
}
QString CommanderBracketDefinitions::displayName(const QString &tag) const
{
auto it = definitions.find(tag);
if (it == definitions.end()) {
return tag;
}
return it->displayName;
}
QString CommanderBracketDefinitions::explanation(const QString &tag) const
{
auto it = definitions.find(tag);
if (it == definitions.end()) {
return {};
}
return it->explanation;
}
bool CommanderBracketDefinitions::contains(const QString &tag) const
{
return definitions.contains(tag);
}

View file

@ -0,0 +1,34 @@
#ifndef COCKATRICE_COMMANDER_BRACKET_DEFINITIONS_H
#define COCKATRICE_COMMANDER_BRACKET_DEFINITIONS_H
#include <QHash>
#include <QString>
struct CommanderBracketDefinition
{
QString tag;
QString officialName;
QString displayName;
QString explanation;
};
class CommanderBracketDefinitions
{
public:
void clear();
void addDefinition(const CommanderBracketDefinition &definition);
QString officialName(const QString &tag) const;
QString displayName(const QString &tag) const;
QString explanation(const QString &tag) const;
bool contains(const QString &tag) const;
private:
QHash<QString, CommanderBracketDefinition> definitions;
};
#endif // COCKATRICE_COMMANDER_BRACKET_DEFINITIONS_H

View file

@ -0,0 +1,51 @@
#include "commander_bracket_service.h"
#include "../../../../../client/settings/cache_settings.h"
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 &definitions = SettingsCache::instance().commanderBracketDefs();
estimate.officialName = definitions.officialName(result.bracketTag);
estimate.displayName = definitions.displayName(result.bracketTag);
estimate.explanation = definitions.explanation(result.bracketTag);
emit estimateFinished(id, requester, estimate);
}
void CommanderBracketService::onEstimateBracketError(CommanderSpellbookApiAccessor::RequestId id,
QObject *requester,
const QString &error)
{
emit estimateError(id, requester, error);
}

View file

@ -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 <QObject>
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

View file

@ -0,0 +1,77 @@
#include "commander_spellbook_api_accessor.h"
#include "api_response/commander_spellbook_deck_request.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkRequest>
#include <QUrl>
#include <version_string.h>
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;
deckRequest.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<QObject *>();
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;
result.fromJson(doc.object());
emit estimateBracketFinished(id, requester, result);
}

View file

@ -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 <QNetworkAccessManager>
#include <QNetworkReply>
#include <QObject>
#include <libcockatrice/deck_list/deck_list.h>
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

View file

@ -0,0 +1,120 @@
#include "commander_spellbook_bracket_explainer.h"
static QString cardList(const QVector<CommanderSpellbookCardResult> &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 QVector<CommanderSpellbookVariantResult> &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;
}

View file

@ -0,0 +1,28 @@
#ifndef COCKATRICE_COMMANDER_SPELLBOOK_BRACKET_EXPLAINER_H
#define COCKATRICE_COMMANDER_SPELLBOOK_BRACKET_EXPLAINER_H
#include "api_response/commander_spellbook_estimate_bracket_result.h"
struct BracketExplanationSection
{
QString title;
QStringList bulletPoints;
};
struct BracketExplanation
{
QString bracket;
QList<BracketExplanationSection> sections;
bool isEmpty() const
{
return sections.isEmpty();
}
};
class BracketExplainer
{
public:
static BracketExplanation explain(const EstimateBracketResult &result);
};
#endif // COCKATRICE_COMMANDER_SPELLBOOK_BRACKET_EXPLAINER_H

View file

@ -0,0 +1,58 @@
#include "handle_commander_brackets.h"
#include "../../../../../client/settings/cache_settings.h"
#include "commander_bracket_definitions.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QUrl>
#define COMMANDER_BRACKET_JSON "https://cockatrice.github.io/commander-brackets.json"
HandleCommanderBrackets::HandleCommanderBrackets(QObject *parent)
: QObject(parent), nam(new QNetworkAccessManager(this)), reply(nullptr)
{
}
void HandleCommanderBrackets::downloadBracketDefinitions()
{
reply = nam->get(QNetworkRequest(QUrl(COMMANDER_BRACKET_JSON)));
connect(reply, &QNetworkReply::finished, this, &HandleCommanderBrackets::actFinishParsingDownloadedData);
}
void HandleCommanderBrackets::actFinishParsingDownloadedData()
{
reply = qobject_cast<QNetworkReply *>(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().reloadBracketDefinitions(bracketList);
}

View file

@ -0,0 +1,31 @@
#ifndef COCKATRICE_HANDLE_COMMANDER_BRACKETS_H
#define COCKATRICE_HANDLE_COMMANDER_BRACKETS_H
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QObject>
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

View file

@ -37,6 +37,7 @@
#include "version_string.h" #include "version_string.h"
#include "widgets/dialogs/dlg_connect.h" #include "widgets/dialogs/dlg_connect.h"
#include "widgets/server/handle_public_servers.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 "widgets/utility/get_text_with_max.h"
#include <QAction> #include <QAction>
@ -640,6 +641,7 @@ void MainWindow::alertForcedOracleRun(const QString &version, bool isUpdate)
actCheckCardUpdates(); actCheckCardUpdates();
actCheckServerUpdates(); actCheckServerUpdates();
actCheckCommanderBracketDefinitionUpdates();
} }
MainWindow::~MainWindow() MainWindow::~MainWindow()
@ -984,6 +986,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() void MainWindow::refreshShortcuts()
{ {
ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts(); ShortcutsSettings &shortcuts = SettingsCache::instance().shortcuts();

View file

@ -92,6 +92,7 @@ private slots:
void cardDatabaseAllNewSetsEnabled(); void cardDatabaseAllNewSetsEnabled();
void checkClientUpdatesFinished(bool needToUpdate, bool isCompatible, Release *release); void checkClientUpdatesFinished(bool needToUpdate, bool isCompatible, Release *release);
void actCheckCommanderBracketDefinitionUpdates();
void actOpenCustomFolder(); void actOpenCustomFolder();
void actOpenCustomsetsFolder(); void actOpenCustomsetsFolder();

View file

@ -5,6 +5,7 @@ set(CMAKE_AUTORCC ON)
set(HEADERS set(HEADERS
libcockatrice/settings/card_database_settings.h libcockatrice/settings/card_database_settings.h
libcockatrice/settings/card_override_settings.h libcockatrice/settings/card_override_settings.h
libcockatrice/settings/commander_bracket_settings.h
libcockatrice/settings/debug_settings.h libcockatrice/settings/debug_settings.h
libcockatrice/settings/download_settings.h libcockatrice/settings/download_settings.h
libcockatrice/settings/game_filters_settings.h libcockatrice/settings/game_filters_settings.h
@ -26,6 +27,7 @@ add_library(
${MOC_SOURCES} ${MOC_SOURCES}
libcockatrice/settings/card_database_settings.cpp libcockatrice/settings/card_database_settings.cpp
libcockatrice/settings/card_override_settings.cpp libcockatrice/settings/card_override_settings.cpp
libcockatrice/settings/commander_bracket_settings.cpp
libcockatrice/settings/debug_settings.cpp libcockatrice/settings/debug_settings.cpp
libcockatrice/settings/download_settings.cpp libcockatrice/settings/download_settings.cpp
libcockatrice/settings/game_filters_settings.cpp libcockatrice/settings/game_filters_settings.cpp

View file

@ -0,0 +1,137 @@
#include "commander_bracket_settings.h"
#include <QSettings>
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", "PA"},
{"officialName", "[1] Exhibition"},
{"displayName", "Precon Appropriate"},
{"explanation", "Lightly upgraded preconstructed decks or very casual builds."}},
QVariantMap{{"tag", "C"},
{"officialName", "[1] Casual"},
{"displayName", "Casual"},
{"explanation", "Relaxed decks with no strict optimization goals."}},
QVariantMap{{"tag", "U"},
{"officialName", "Unknown"},
{"displayName", "Unknown"},
{"explanation", "Unclassified or missing bracket definition."}},
};
}
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;
}

View file

@ -0,0 +1,35 @@
#ifndef COMMANDER_BRACKET_SETTINGS_H
#define COMMANDER_BRACKET_SETTINGS_H
#include "settings_manager.h"
#include <QObject>
#include <QVariantList>
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 setSchemaVersion(int version);
int getSchemaVersion() const;
private:
explicit CommanderBracketSettings(const QString &settingPath, QObject *parent = nullptr);
CommanderBracketSettings(const CommanderBracketSettings &) = delete;
CommanderBracketSettings &operator=(const CommanderBracketSettings &) = delete;
};
#endif // COMMANDER_BRACKET_SETTINGS_H

View file

@ -32,6 +32,7 @@ set(oracle_SOURCES
../cockatrice/src/interface/theme_manager.cpp ../cockatrice/src/interface/theme_manager.cpp
../cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp ../cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp
../cockatrice/src/interface/widgets/quick_settings/settings_popup_widget.cpp ../cockatrice/src/interface/widgets/quick_settings/settings_popup_widget.cpp
../cockatrice/src/interface/widgets/tabs/api/commander_spellbook/commander_bracket_definitions.cpp
${VERSION_STRING_CPP} ${VERSION_STRING_CPP}
) )