Don't hardcode brackets, fetch from cocaktrice.github.io, store as settings.

Took 3 minutes

Took 38 seconds

Took 7 minutes
This commit is contained in:
Lukas Brübach 2026-06-16 00:22:53 +02:00
parent f9f5489871
commit d2a63ca758
24 changed files with 664 additions and 206 deletions

View file

@ -350,6 +350,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_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.h
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp

View file

@ -193,6 +193,7 @@ SettingsCache::SettingsCache()
shortcutsSettings = new ShortcutsSettings(settingsPath, this);
cardDatabaseSettings = new CardDatabaseSettings(settingsPath, this);
serversSettings = new ServersSettings(settingsPath, this);
commanderBracketSettings = new CommanderBracketSettings(settingsPath, this);
messageSettings = new MessageSettings(settingsPath, this);
gameFiltersSettings = new GameFiltersSettings(settingsPath, this);
layoutsSettings = new LayoutsSettings(settingsPath, this);
@ -330,6 +331,7 @@ SettingsCache::SettingsCache()
deckEditorBannerCardComboBoxVisible =
settings->value("interface/deckeditorbannercardcomboboxvisible", true).toBool();
deckEditorTagsWidgetVisible = settings->value("interface/deckeditortagswidgetvisible", true).toBool();
deckEditorCommanderSpellbookIntegrationEnabled =
settings
->value("interface/deck_editor/commander_spellbook_integration/enabled",
@ -338,6 +340,13 @@ SettingsCache::SettingsCache()
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();
visualDeckStorageSortingOrder = settings->value("interface/visualdeckstoragesortingorder", 0).toInt();
visualDeckStorageShowFolders = settings->value("interface/visualdeckstorageshowfolders", true).toBool();
@ -436,6 +445,22 @@ SettingsCache::SettingsCache()
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)
{
useTearOffMenus = _useTearOffMenus;

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_local_schemes.h"
#include "../../interface/widgets/tabs/api/commander_spellbook/commander_bracket_definitions.h"
#include "shortcuts_settings.h"
#include <QDate>
@ -19,6 +20,7 @@
#include <libcockatrice/interfaces/interface_network_settings_provider.h>
#include <libcockatrice/settings/card_database_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/download_settings.h>
#include <libcockatrice/settings/game_filters_settings.h>
@ -203,6 +205,7 @@ private:
ShortcutsSettings *shortcutsSettings;
CardDatabaseSettings *cardDatabaseSettings;
ServersSettings *serversSettings;
CommanderBracketSettings *commanderBracketSettings;
MessageSettings *messageSettings;
GameFiltersSettings *gameFiltersSettings;
LayoutsSettings *layoutsSettings;
@ -212,6 +215,8 @@ private:
DebugSettings *debugSettings;
CardCounterSettings *cardCounterSettings;
CommanderBracketDefinitions bracketDefinitions;
QString lang;
QString deckPath, filtersPath, replaysPath, picsPath, redirectCachePath, customPicsPath, cardDatabasePath,
customCardDatabasePath, themesPath, spoilerDatabasePath, tokenDatabasePath, themeName, homeTabBackgroundSource;
@ -991,6 +996,15 @@ public:
{
return *serversSettings;
}
[[nodiscard]] CommanderBracketSettings &commanderBrackets() const
{
return *commanderBracketSettings;
}
void reloadBracketDefinitions(const QVariantList &definitions);
CommanderBracketDefinitions &commanderBracketDefs()
{
return bracketDefinitions;
}
[[nodiscard]] MessageSettings &messages() const
{
return *messageSettings;

View file

@ -2,6 +2,7 @@
#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"
@ -168,6 +169,9 @@ void DeckEditorDeckDockWidget::createDeckDock()
bracketInfoButton->setVisible(false);
bracketRefreshButton->setVisible(false);
connect(&CommanderBracketService::instance(), &CommanderBracketService::estimateFinished, this,
&DeckEditorDeckDockWidget::onEstimateBracketFinished);
commentsLabel = new QLabel();
commentsLabel->setObjectName("commentsLabel");
commentsEdit = new QTextEdit;
@ -433,29 +437,25 @@ void DeckEditorDeckDockWidget::requestBracketEstimate()
bracketInfoButton->setEnabled(false);
bracketValueLabel->setText(tr("Calculating…"));
requestId = CommanderSpellbookApiAccessor::instance().estimateBracket(deckStateManager->getDeckList(), this);
connect(&CommanderSpellbookApiAccessor::instance(), &CommanderSpellbookApiAccessor::estimateBracketFinished, this,
&DeckEditorDeckDockWidget::onEstimateBracketFinished);
requestId = CommanderBracketService::instance().estimateBracket(deckStateManager->getDeckList(), this);
}
void DeckEditorDeckDockWidget::onEstimateBracketFinished(CommanderSpellbookApiAccessor::RequestId id,
void DeckEditorDeckDockWidget::onEstimateBracketFinished(quint64 id,
QObject *requester,
const EstimateBracketResult &result)
const CommanderBracketEstimate &result)
{
if (requester != this || static_cast<int>(id) != requestId) {
if (requester != this || id != requestId) {
return;
}
BracketExplainer explainer;
lastBracketExplanation = explainer.explain(result);
lastBracketExplanation = explainer.explain(result.rawResult);
// Display bracket
if (SettingsCache::instance().getDeckEditorCommanderSpellbookIntegrationUseOfficialBracketNames()) {
bracketValueLabel->setText(CommanderSpellbookBracketTag::bracketTagToOfficialString(result.bracketTag));
} else {
bracketValueLabel->setText(CommanderSpellbookBracketTag::bracketTagToString(result.bracketTag));
}
bracketValueLabel->setText(
SettingsCache::instance().getDeckEditorCommanderSpellbookIntegrationUseOfficialBracketNames()
? result.officialName
: result.displayName);
bracketRefreshButton->setEnabled(true);
// Build tooltip

View file

@ -22,7 +22,7 @@
#include <QTreeView>
#include <libcockatrice/card/card_info.h>
class EstimateBracketResult;
struct CommanderBracketEstimate;
class DeckListModel;
class AbstractTabDeckEditor;
class DeckEditorDeckDockWidget : public QDockWidget
@ -65,7 +65,7 @@ public slots:
void actRemoveCard();
void initializeFormats();
void maybeAutoEstimateBracket();
void onEstimateBracketFinished(quint64 id, QObject *requester, const EstimateBracketResult &result);
void onEstimateBracketFinished(quint64 id, QObject *requester, const CommanderBracketEstimate &result);
signals:
void selectedCardChanged(const ExactCard &card);

View file

@ -1,85 +0,0 @@
#ifndef COCKATRICE_COMMANDER_SPELLBOOK_BRACKET_TAG_H
#define COCKATRICE_COMMANDER_SPELLBOOK_BRACKET_TAG_H
#include <QString>
namespace CommanderSpellbookBracketTag
{
enum class BracketTag
{
Ruthless,
Spicy,
Powerful,
Oddball,
PreconAppropriate,
Casual,
Unknown
};
inline static BracketTag bracketTagFromString(const QString &s)
{
if (s == "R") {
return BracketTag::Ruthless;
}
if (s == "S") {
return BracketTag::Spicy;
}
if (s == "P") {
return BracketTag::Powerful;
}
if (s == "O") {
return BracketTag::Oddball;
}
if (s == "PA") {
return BracketTag::PreconAppropriate;
}
if (s == "C") {
return BracketTag::Casual;
}
return BracketTag::Unknown;
}
inline static QString bracketTagToString(BracketTag tag)
{
switch (tag) {
case BracketTag::Ruthless:
return "Ruthless";
case BracketTag::Spicy:
return "Spicy";
case BracketTag::Powerful:
return "Powerful";
case BracketTag::Oddball:
return "Oddball";
case BracketTag::PreconAppropriate:
return "Precon Appropriate";
case BracketTag::Casual:
return "Casual";
case BracketTag::Unknown:
return "Unknown";
}
return {};
}
inline static QString bracketTagToOfficialString(BracketTag tag)
{
switch (tag) {
case BracketTag::Ruthless:
return "[5] cEDH";
case BracketTag::Spicy:
return "[4] Optimized";
case BracketTag::Powerful:
return "[3] Upgraded";
case BracketTag::Oddball:
return "[2] Core";
case BracketTag::PreconAppropriate:
return "[1] Exhibition";
case BracketTag::Casual:
return "[1] Casual";
case BracketTag::Unknown:
return "Unknown";
}
return {};
}
} // namespace CommanderSpellbookBracketTag
#endif // COCKATRICE_COMMANDER_SPELLBOOK_BRACKET_TAG_H

View file

@ -1,49 +1,107 @@
#include "commander_spellbook_estimate_bracket_result.h"
static void parseCards(const QJsonObject &json, const QString &key, QVector<CommanderSpellbookCardResult> &out)
{
out.clear();
for (const auto &v : json.value(key).toArray()) {
if (!v.isObject()) {
continue;
}
CommanderSpellbookCardResult c;
c.fromJson(v.toObject());
out.append(c);
}
}
static void parseVariants(const QJsonObject &json, const QString &key, QVector<CommanderSpellbookVariantResult> &out)
{
out.clear();
for (const auto &v : json.value(key).toArray()) {
if (!v.isObject()) {
continue;
}
CommanderSpellbookVariantResult vr;
vr.fromJson(v.toObject());
out.append(vr);
}
}
void EstimateBracketResult::fromJson(const QJsonObject &json)
{
bracketTag = CommanderSpellbookBracketTag::bracketTagFromString(json.value("bracketTag").toString());
bracketTag = json.value("bracketTag").toString();
parseCards(json, "gameChangerCards", gameChangerCards);
parseCards(json, "massLandDenialCards", massLandDenialCards);
parseCards(json, "extraTurnCards", extraTurnCards);
parseCards(json, "tutorCards", tutorCards);
gameChangerCards.clear();
massLandDenialCards.clear();
extraTurnCards.clear();
parseVariants(json, "massLandDenialTemplates", massLandDenialTemplates);
parseVariants(json, "massLandDenialCombos", massLandDenialCombos);
parseVariants(json, "extraTurnTemplates", extraTurnTemplates);
parseVariants(json, "extraTurnsCombos", extraTurnsCombos);
parseVariants(json, "tutorTemplates", tutorTemplates);
parseVariants(json, "lockCombos", lockCombos);
parseVariants(json, "skipTurnsCombos", skipTurnsCombos);
parseVariants(json, "definitelyEarlyGameTwoCardCombos", definitelyEarlyGameTwoCardCombos);
parseVariants(json, "arguablyEarlyGameTwoCardCombos", arguablyEarlyGameTwoCardCombos);
parseVariants(json, "definitelyLateGameTwoCardCombos", definitelyLateGameTwoCardCombos);
parseVariants(json, "borderlineLateGameTwoCardCombos", borderlineLateGameTwoCardCombos);
}
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

@ -1,5 +1,6 @@
#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"
@ -10,24 +11,22 @@ class EstimateBracketResult
public:
void fromJson(const QJsonObject &json);
CommanderSpellbookBracketTag::BracketTag bracketTag = CommanderSpellbookBracketTag::BracketTag::Unknown;
QString bracketTag;
QVector<CommanderSpellbookCardResult> gameChangerCards;
QVector<CommanderSpellbookCardResult> massLandDenialCards;
QVector<CommanderSpellbookCardResult> extraTurnCards;
QVector<CommanderSpellbookCardResult> tutorCards;
QVector<CommanderSpellbookVariantResult> massLandDenialTemplates;
QVector<CommanderSpellbookVariantResult> massLandDenialCombos;
QVector<CommanderSpellbookVariantResult> extraTurnTemplates;
QVector<CommanderSpellbookVariantResult> extraTurnsCombos;
QVector<CommanderSpellbookVariantResult> tutorTemplates;
QVector<CommanderSpellbookVariantResult> massLandDenialCombos;
QVector<CommanderSpellbookVariantResult> extraTurnCombos;
QVector<CommanderSpellbookVariantResult> lockCombos;
QVector<CommanderSpellbookVariantResult> skipTurnsCombos;
QVector<CommanderSpellbookVariantResult> definitelyEarlyGameTwoCardCombos;
QVector<CommanderSpellbookVariantResult> arguablyEarlyGameTwoCardCombos;
QVector<CommanderSpellbookVariantResult> definitelyLateGameTwoCardCombos;
QVector<CommanderSpellbookVariantResult> borderlineLateGameTwoCardCombos;
QVector<CommanderSpellbookVariantResult> definitelyTwoCardCombos;
QVector<CommanderSpellbookVariantResult> arguablyTwoCardCombos;
};
#endif // COCKATRICE_COMMANDER_SPELLBOOK_ESTIMATE_BRACKET_RESULT_H
#endif

View file

@ -22,7 +22,7 @@ void CommanderSpellbookVariantResult::fromJson(const QJsonObject &json)
popularity = json.value("popularity").toDouble();
spoiler = json.value("spoiler").toBool();
bracketTag = CommanderSpellbookBracketTag::bracketTagFromString(json.value("bracketTag").toString());
bracketTag = json.value("bracketTag").toString();
legalities = json.value("legalities").toObject();
prices = json.value("prices").toObject();

View file

@ -1,6 +1,5 @@
#ifndef COCKATRICE_COMMANDER_SPELLBOOK_VARIANT_RESULT_H
#define COCKATRICE_COMMANDER_SPELLBOOK_VARIANT_RESULT_H
#include "commander_spellbook_bracket_tag.h"
#include <QJsonArray>
#include <QJsonObject>
@ -30,7 +29,7 @@ public:
double popularity = 0.0;
bool spoiler = false;
CommanderSpellbookBracketTag::BracketTag bracketTag = CommanderSpellbookBracketTag::BracketTag::Unknown;
QString bracketTag;
QJsonObject legalities;
QJsonObject prices;

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

@ -24,47 +24,56 @@ BracketExplanation BracketExplainer::explain(const EstimateBracketResult &r)
BracketExplanation out;
out.bracket = r.bracketTag;
// --- Game changers ---
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;
}
// --- Tutors ---
if (!r.tutorCards.isEmpty()) {
if (!r.extraTurnCards.isEmpty() || !r.extraTurnTemplates.isEmpty() || !r.extraTurnCombos.isEmpty()) {
BracketExplanationSection s;
s.title = "Tutors";
s.bulletPoints << QString("The deck runs %1 tutor cards, including %2.")
.arg(r.tutorCards.size())
.arg(cardList(r.tutorCards));
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;
}
// --- Extra turns ---
if (!r.extraTurnCards.isEmpty()) {
BracketExplanationSection s;
s.title = "Extra turn effects";
s.bulletPoints << QString("Extra turn spells were detected (%1), such as %2.")
.arg(r.extraTurnCards.size())
.arg(cardList(r.extraTurnCards));
out.sections << s;
}
if (!r.massLandDenialCards.isEmpty() || !r.massLandDenialTemplates.isEmpty() || !r.massLandDenialCombos.isEmpty()) {
// --- Mass land denial ---
if (!r.massLandDenialCards.isEmpty() || !r.massLandDenialCombos.isEmpty()) {
BracketExplanationSection s;
s.title = "Mass land denial";
if (!r.massLandDenialCards.isEmpty()) {
s.bulletPoints << QString("The deck includes %1 mass land denial cards (%2).")
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));
@ -73,10 +82,10 @@ BracketExplanation BracketExplainer::explain(const EstimateBracketResult &r)
out.sections << s;
}
// --- Lock / skip turns ---
if (!r.lockCombos.isEmpty() || !r.skipTurnsCombos.isEmpty()) {
BracketExplanationSection s;
s.title = "Lock or skip-turn combos";
s.title = "Lock pieces";
if (!r.lockCombos.isEmpty()) {
s.bulletPoints << QString("%1 lock combo variants were detected.").arg(comboCount(r.lockCombos));
@ -89,39 +98,19 @@ BracketExplanation BracketExplainer::explain(const EstimateBracketResult &r)
out.sections << s;
}
// --- Early-game combos ---
if (!r.definitelyEarlyGameTwoCardCombos.isEmpty() || !r.arguablyEarlyGameTwoCardCombos.isEmpty()) {
if (!r.definitelyTwoCardCombos.isEmpty() || !r.arguablyTwoCardCombos.isEmpty()) {
BracketExplanationSection s;
s.title = "Early-game two-card combos";
s.title = "Two-card combos";
if (!r.definitelyEarlyGameTwoCardCombos.isEmpty()) {
s.bulletPoints << QString("%1 definitely early-game two-card combos were found.")
.arg(comboCount(r.definitelyEarlyGameTwoCardCombos));
if (!r.definitelyTwoCardCombos.isEmpty()) {
s.bulletPoints << QString("%1 definite two-card combo variants were identified.")
.arg(comboCount(r.definitelyTwoCardCombos));
}
if (!r.arguablyEarlyGameTwoCardCombos.isEmpty()) {
s.bulletPoints << QString("%1 arguably early-game two-card combos were found.")
.arg(comboCount(r.arguablyEarlyGameTwoCardCombos));
}
out.sections << s;
}
// --- Late-game combos ---
if (!r.definitelyLateGameTwoCardCombos.isEmpty() || !r.borderlineLateGameTwoCardCombos.isEmpty()) {
BracketExplanationSection s;
s.title = "Late-game two-card combos";
if (!r.definitelyLateGameTwoCardCombos.isEmpty()) {
s.bulletPoints << QString("%1 definitely late-game two-card combos were found.")
.arg(comboCount(r.definitelyLateGameTwoCardCombos));
}
if (!r.borderlineLateGameTwoCardCombos.isEmpty()) {
s.bulletPoints << QString("%1 borderline late-game two-card combos were found.")
.arg(comboCount(r.borderlineLateGameTwoCardCombos));
if (!r.arguablyTwoCardCombos.isEmpty()) {
s.bulletPoints << QString("%1 arguable two-card combo variants were identified.")
.arg(comboCount(r.arguablyTwoCardCombos));
}
out.sections << s;

View file

@ -10,7 +10,7 @@ struct BracketExplanationSection
struct BracketExplanation
{
CommanderSpellbookBracketTag::BracketTag bracket;
QString bracket;
QList<BracketExplanationSection> sections;
bool isEmpty() const

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 "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>
@ -640,6 +641,7 @@ void MainWindow::alertForcedOracleRun(const QString &version, bool isUpdate)
actCheckCardUpdates();
actCheckServerUpdates();
actCheckCommanderBracketDefinitionUpdates();
}
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()
{
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

@ -5,6 +5,7 @@ set(CMAKE_AUTORCC ON)
set(HEADERS
libcockatrice/settings/card_database_settings.h
libcockatrice/settings/card_override_settings.h
libcockatrice/settings/commander_bracket_settings.h
libcockatrice/settings/debug_settings.h
libcockatrice/settings/download_settings.h
libcockatrice/settings/game_filters_settings.h
@ -26,6 +27,7 @@ add_library(
${MOC_SOURCES}
libcockatrice/settings/card_database_settings.cpp
libcockatrice/settings/card_override_settings.cpp
libcockatrice/settings/commander_bracket_settings.cpp
libcockatrice/settings/debug_settings.cpp
libcockatrice/settings/download_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/widgets/quick_settings/settings_button_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}
)