[DeckEditor] Use CommanderSpellbook.com to estimate bracket if format is 'commander' (#6415)

* [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 <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL
2026-08-09 00:46:17 +02:00
committed by GitHub
parent e9eab328a4
commit 827e44a4d3
35 changed files with 1642 additions and 8 deletions

View File

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

View File

@@ -17,6 +17,7 @@
#include <libcockatrice/settings/card_override_settings.h>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/settings/chat_settings.h>
#include <libcockatrice/settings/commander_bracket_settings.h>
#include <libcockatrice/settings/debug_settings.h>
#include <libcockatrice/settings/deck_editor_settings.h>
#include <libcockatrice/settings/download_settings.h>
@@ -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();
}

View File

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

View File

@@ -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<int>::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"));
}
}

View File

@@ -21,6 +21,7 @@
#include <QTreeView>
#include <libcockatrice/card/card_info.h>
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);

View File

@@ -57,6 +57,14 @@ public:
*/
const DeckList &getDeckList() const;
/**
* @brief Gets the underlying DeckList.
*/
QSharedPointer<DeckList> getDeckListShared() const
{
return deckList;
}
/**
* @brief Creates a LoadedDeck containing the contents of the current deck and the current LoadInfo.
*

View File

@@ -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 <QGridLayout>
#include <libcockatrice/settings/cards_display_settings.h>
@@ -161,6 +162,57 @@ UserInterfaceSettingsPage::UserInterfaceSettingsPage()
connect(&defaultDeckEditorTypeSelector, QOverload<int>::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<int>::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<int>::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");

View File

@@ -8,6 +8,8 @@
#include <QGroupBox>
#include <QLabel>
#include <QSpinBox>
#include <QToolButton>
#include <libcockatrice/settings/cards_display_settings.h>
#include <libcockatrice/utility/macros.h>
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;

View File

@@ -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;
}

View File

@@ -0,0 +1,14 @@
#ifndef COCKATRICE_CARD_IN_DECK_REQUEST_H
#define COCKATRICE_CARD_IN_DECK_REQUEST_H
#include <QJsonObject>
struct CardInDeckRequest
{
static CardInDeckRequest fromJson(const QJsonObject &json);
QJsonObject toJson() const;
QString card;
int quantity;
};
#endif // COCKATRICE_CARD_IN_DECK_REQUEST_H

View File

@@ -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;
}

View File

@@ -0,0 +1,29 @@
#ifndef COCKATRICE_COMMANDER_SPELLBOOK_CARD_RESULT_H
#define COCKATRICE_COMMANDER_SPELLBOOK_CARD_RESULT_H
#include <QJsonObject>
#include <QString>
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

View File

@@ -0,0 +1,95 @@
#include "commander_spellbook_deck_request.h"
#include <QJsonArray>
#include <libcockatrice/deck_list/tree/deck_list_card_node.h>
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;
}

View File

@@ -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 <QJsonObject>
#include <QList>
struct CommanderSpellbookDeckRequest
{
static CommanderSpellbookDeckRequest fromJson(const QJsonObject &json);
static CommanderSpellbookDeckRequest fromDeckList(const DeckList &deck);
QJsonObject toJson() const;
QList<CardInDeckRequest> mainDeck; // maxItems: 600
QList<CardInDeckRequest> commanderDeck; // maxItems: 12
};
#endif // COCKATRICE_COMMANDER_SPELLBOOK_DECK_REQUEST_H

View File

@@ -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;
}

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 <QJsonObject>
#include <QList>
struct EstimateBracketResult
{
static EstimateBracketResult fromJson(const QJsonObject &json);
QString bracketTag;
QList<CommanderSpellbookCardResult> gameChangerCards;
QList<CommanderSpellbookCardResult> massLandDenialCards;
QList<CommanderSpellbookCardResult> extraTurnCards;
QList<CommanderSpellbookVariantResult> massLandDenialTemplates;
QList<CommanderSpellbookVariantResult> extraTurnTemplates;
QList<CommanderSpellbookVariantResult> massLandDenialCombos;
QList<CommanderSpellbookVariantResult> extraTurnCombos;
QList<CommanderSpellbookVariantResult> lockCombos;
QList<CommanderSpellbookVariantResult> skipTurnsCombos;
QList<CommanderSpellbookVariantResult> definitelyTwoCardCombos;
QList<CommanderSpellbookVariantResult> arguablyTwoCardCombos;
};
#endif // COCKATRICE_COMMANDER_SPELLBOOK_ESTIMATE_BRACKET_RESULT_H

View File

@@ -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;
}

View File

@@ -0,0 +1,39 @@
#ifndef COCKATRICE_COMMANDER_SPELLBOOK_VARIANT_RESULT_H
#define COCKATRICE_COMMANDER_SPELLBOOK_VARIANT_RESULT_H
#include <QJsonArray>
#include <QJsonObject>
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

View File

@@ -0,0 +1,53 @@
#include "commander_bracket_service.h"
#include "../../../../../client/settings/cache_settings.h"
#include <libcockatrice/settings/commander_bracket_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 &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);
}

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,269 @@
#include "commander_bracket_widget.h"
#include "../../../../../client/settings/cache_settings.h"
#include "commander_bracket_service.h"
#include <QComboBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QPushButton>
#include <QTimer>
#include <QToolButton>
#include <QVBoxLayout>
#include <libcockatrice/settings/deck_editor_settings.h>
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<DeckList> &_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 &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 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"));
}

View File

@@ -0,0 +1,46 @@
#ifndef COCKATRICE_COMMANDER_BRACKET_WIDGET_H
#define COCKATRICE_COMMANDER_BRACKET_WIDGET_H
#include "commander_spellbook_bracket_explainer.h"
#include <QSharedPointer>
#include <QWidget>
#include <libcockatrice/deck_list/deck_list.h>
class QLabel;
class QToolButton;
struct CommanderBracketEstimate;
class CommanderBracketWidget : public QWidget
{
Q_OBJECT
public:
explicit CommanderBracketWidget(QWidget *parent = nullptr);
void setDeck(const QSharedPointer<DeckList> &_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<DeckList> deck;
bool prompting = false;
quint64 requestId = 0;
QLabel *bracketLabel;
QLabel *bracketValueLabel;
QToolButton *bracketInfoButton;
QToolButton *bracketRefreshButton;
BracketExplanation lastBracketExplanation;
};
#endif // COCKATRICE_COMMANDER_BRACKET_WIDGET_H

View File

@@ -0,0 +1,75 @@
#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 = 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<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 = EstimateBracketResult::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 QList<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 QList<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,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<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,62 @@
#include "handle_commander_brackets.h"
#include "../../../../../client/settings/cache_settings.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QUrl>
#include <libcockatrice/settings/commander_bracket_settings.h>
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<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().commanderBrackets().reloadDefinitions(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

@@ -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 <QAction>
@@ -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();

View File

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

View File

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

View File

@@ -0,0 +1,194 @@
#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", "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);
}

View File

@@ -0,0 +1,56 @@
#ifndef COMMANDER_BRACKET_SETTINGS_H
#define COMMANDER_BRACKET_SETTINGS_H
#include "settings_manager.h"
#include <QHash>
#include <QObject>
#include <QString>
#include <QVariantList>
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<QString, CommanderBracketDefinition> definitions;
};
#endif // COMMANDER_BRACKET_SETTINGS_H

View File

@@ -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);
}

View File

@@ -5,6 +5,14 @@
#include <libcockatrice/interfaces/interface_deck_editor_settings_provider.h>
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);