Merge pull request #8 from Continuum-Universes-Studio/feature/continuumatrice-product-migration

Feature/continuumatrice product migration
This commit is contained in:
Christian Daniel Jensen 2026-11-03 18:45:57 -07:00 • committed by GitHub
commit 533d2f04c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
150 changed files with 5268 additions and 2563 deletions

View file

@ -13,6 +13,7 @@
#include "libcockatrice/protocol/pb/event_server_shutdown.pb.h"
#include <QChar>
#include <QCoreApplication>
#include <QDateTime>
#include <QDebug>
#include <QLineEdit>
@ -217,7 +218,7 @@ void ConnectionController::onServerShutdownEvent(const Event_ServerShutdown &eve
"games will be lost.\nReason for shutdown: %1",
"", event.minutes())
.arg(QString::fromStdString(event.reason())));
serverShutdownMessageBox.setIconPixmap(themePixmap(QStringLiteral("cockatrice")).scaled(64, 64));
serverShutdownMessageBox.setIconPixmap(themePixmap(QStringLiteral("continuumatrice")).scaled(64, 64));
serverShutdownMessageBox.setText(tr("Scheduled server shutdown"));
serverShutdownMessageBox.setWindowModality(Qt::ApplicationModal);
serverShutdownMessageBox.setVisible(true);
@ -333,7 +334,7 @@ void ConnectionController::onLoginError(int r,
case Response::RespContextError: {
QMessageBox::critical(dialogParent, tr("Error"),
tr("An internal error has occurred, please close and reopen Cockatrice before "
tr("An internal error has occurred, please close and reopen Continuumatrice before "
"trying again.\nIf the error persists, ensure you are running the latest "
"version of the software and if needed contact the software developers."));
break;
@ -489,14 +490,14 @@ void ConnectionController::onProtocolVersionMismatch(int localVersion, int remot
{
if (localVersion > remoteVersion) {
QMessageBox::critical(dialogParent, tr("Error"),
tr("You are trying to connect to an obsolete server. Please downgrade your Cockatrice "
tr("You are trying to connect to an obsolete server. Please downgrade your Continuumatrice "
"version or connect to a suitable server.\n"
"Local version is %1, remote version is %2.")
.arg(localVersion)
.arg(remoteVersion));
} else {
QMessageBox::critical(dialogParent, tr("Error"),
tr("Your Cockatrice client is obsolete. Please update your Cockatrice version.\n"
tr("Your Continuumatrice client is obsolete. Please update your Continuumatrice version.\n"
"Local version is %1, remote version is %2.")
.arg(localVersion)
.arg(remoteVersion));
@ -631,7 +632,7 @@ void ConnectionController::onPromptForgotPasswordChallenge()
void ConnectionController::updateWindowTitle()
{
const QString appName = QStringLiteral("Cockatrice");
const QString appName = QCoreApplication::applicationName();
QString title;
switch (remoteClient->getStatus()) {
@ -653,7 +654,7 @@ void ConnectionController::updateWindowTitle()
break;
}
case StatusLoggedIn: {
title = remoteClient->getUserName() + "@" + remoteClient->peerName();
title = appName + " - " + remoteClient->getUserName() + "@" + remoteClient->peerName();
break;
}
case StatusRequestingForgotPassword:

View file

@ -30,13 +30,13 @@
#include <sys/types.h>
#endif
#define STABLERELEASE_URL "https://api.github.com/repos/Cockatrice/Cockatrice/releases/latest"
#define STABLEMANUALDOWNLOAD_URL "https://github.com/Cockatrice/Cockatrice/releases/latest"
#define STABLETAG_URL "https://api.github.com/repos/Cockatrice/Cockatrice/git/refs/tags/"
#define STABLERELEASE_URL "https://api.github.com/repos/Continuum-Universes-Studio/Continuumatrice/releases/latest"
#define STABLEMANUALDOWNLOAD_URL "https://github.com/Continuum-Universes-Studio/Continuumatrice/releases/latest"
#define STABLETAG_URL "https://api.github.com/repos/Continuum-Universes-Studio/Continuumatrice/git/refs/tags/"
#define BETARELEASE_URL "https://api.github.com/repos/Cockatrice/Cockatrice/releases"
#define BETAMANUALDOWNLOAD_URL "https://github.com/Cockatrice/Cockatrice/releases/"
#define BETARELEASE_CHANGESURL "https://github.com/Cockatrice/Cockatrice/compare/%1...%2"
#define BETARELEASE_URL "https://api.github.com/repos/Continuum-Universes-Studio/Continuumatrice/releases"
#define BETAMANUALDOWNLOAD_URL "https://github.com/Continuum-Universes-Studio/Continuumatrice/releases/"
#define BETARELEASE_CHANGESURL "https://github.com/Continuum-Universes-Studio/Continuumatrice/compare/%1...%2"
#define GIT_SHORT_HASH_LEN 7

View file

@ -256,6 +256,17 @@ void SettingsCache::loadPaths()
computePath("customSets", dataPath + "/customsets/");
computeFilePath("cardDatabase", dataPath + "/cards.xml");
// On a new Continuumatrice profile create an empty, valid XML v4 database.
// Never download MTG data or copy the user's Cockatrice card database.
const QString initialCardDb = dataPath + "/cards.xml";
if (!QFile::exists(initialCardDb)) {
if (!QFile::copy(QStringLiteral(":/resources/continuumatrice-empty-carddatabase.xml"), initialCardDb)) {
qCWarning(SettingsCacheLog) << "Could not create Continuumatrice starter database:" << initialCardDb;
} else {
QFile::setPermissions(initialCardDb, QFile::ReadOwner | QFile::WriteOwner);
}
}
computeFilePath("tokenDatabase", dataPath + "/tokens.xml");
computeFilePath("spoilerDatabase", dataPath + "/spoiler.xml");
}

View file

@ -118,7 +118,7 @@ QStringMap &SoundEngine::getAvailableThemes()
#elif defined(Q_OS_WIN)
"/sounds"
#else // linux
"/../share/cockatrice/sounds"
"/../share/continuumatrice/sounds"
#endif
);

View file

@ -0,0 +1,186 @@
#include "essence_pool_item.h"
#include "../../interface/pixel_map_generator.h"
#include <QFont>
#include <QGraphicsSceneHoverEvent>
#include <QGraphicsSceneMouseEvent>
#include <QInputDialog>
#include <QPainter>
#include <QPen>
#include <QRectF>
#include <QSize>
#include <QWidget>
#include <libcockatrice/game/board/counter_state.h>
#include <libcockatrice/game/player/player_actions.h>
#include <libcockatrice/game/player/player_info.h>
#include <libcockatrice/game/player/player_logic.h>
#include <libcockatrice/utility/essence_counter.h>
#include <limits>
EssencePoolItem::EssencePoolItem(PlayerLogic *_player, QGraphicsItem *parent)
: QGraphicsObject(parent), player(_player)
{
setAcceptedMouseButtons(Qt::LeftButton | Qt::RightButton);
setAcceptHoverEvents(true);
setZValue(5);
connect(player, &PlayerLogic::counterAdded, this, &EssencePoolItem::attachCounter);
connect(player, &PlayerLogic::counterRemoved, this, &EssencePoolItem::detachCounter);
// Reconnect and late view construction must paint the full snapshot,
// even when the counterAdded events happened before this item existed.
for (CounterState *state : player->getCounters()) {
attachCounter(state);
}
setToolTip(tr("Essence pool — left click +1, right click -1, Shift+left to set"));
}
QRectF EssencePoolItem::boundingRect() const
{
return QRectF(0, 0, Width, Height);
}
void EssencePoolItem::attachCounter(CounterState *state)
{
if (!state) {
return;
}
const QChar code = EssenceCounter::codeFromName(state->getName());
if (code.isNull()) {
return;
}
counters.insert(code, state);
connect(state, &CounterState::valueChanged, this, [this](int, int, bool) { update(); });
update();
}
void EssencePoolItem::detachCounter(int id)
{
for (auto it = counters.begin(); it != counters.end();) {
if (it.value() && it.value()->getId() == id) {
it = counters.erase(it);
} else {
++it;
}
}
update();
}
CounterState *EssencePoolItem::stateFor(QChar code) const
{
return counters.value(code).data();
}
QChar EssencePoolItem::codeAt(const QPointF &position) const
{
const qreal x = position.x() - Padding;
const qreal y = position.y() - HeaderHeight;
if (x < 0 || y < 0 || x >= Columns * CellWidth || y >= 4 * CellHeight) {
return QChar();
}
const int col = static_cast<int>(x) / CellWidth;
const int row = static_cast<int>(y) / CellHeight;
const int index = row * Columns + col;
const QString &codes = EssenceCounter::symbols();
return index >= 0 && index < codes.size() ? codes.at(index) : QChar();
}
void EssencePoolItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *)
{
painter->save();
painter->setRenderHint(QPainter::Antialiasing, true);
painter->setPen(QPen(QColor(118, 125, 158), 1));
painter->setBrush(QColor(16, 19, 31, 232));
painter->drawRoundedRect(boundingRect().adjusted(0.5, 0.5, -0.5, -0.5), 7, 7);
QFont titleFont = painter->font();
titleFont.setBold(true);
titleFont.setPixelSize(15);
painter->setFont(titleFont);
painter->setPen(QColor(236, 238, 248));
painter->drawText(QRectF(8, 3, Width - 16, 24), Qt::AlignCenter, tr("ESSENCE POOL"));
QFont countFont = painter->font();
countFont.setBold(true);
countFont.setPixelSize(14);
painter->setFont(countFont);
const QString &codes = EssenceCounter::symbols();
for (int i = 0; i < codes.size(); ++i) {
const QChar code = codes.at(i);
const int col = i % Columns;
const int row = i / Columns;
const qreal x = Padding + col * CellWidth;
const qreal y = HeaderHeight + row * CellHeight;
const QRectF tile(x, y, CellWidth - 3, CellHeight - 3);
painter->setPen(QPen(QColor(85, 91, 119), 1));
painter->setBrush(QColor(43, 47, 67, 228));
painter->drawRoundedRect(tile, 5, 5);
const QPixmap icon = ManaSymbolPixmapGenerator::generatePixmap(QString(code), QSize(32, 32));
if (!icon.isNull()) {
painter->drawPixmap(QPointF(x + 9, y + 2), icon);
} else {
painter->setPen(Qt::white);
painter->drawText(QRectF(x, y + 2, CellWidth - 3, 30), Qt::AlignCenter, QString(code));
}
CounterState *state = stateFor(code);
const int count = state ? state->getValue() : 0;
painter->setPen(count > 0 ? QColor(255, 244, 191) : QColor(178, 182, 200));
painter->drawText(QRectF(x, y + 33, CellWidth - 3, 18),
Qt::AlignCenter, QString::number(count));
}
painter->restore();
}
void EssencePoolItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
const QChar code = codeAt(event->pos());
CounterState *state = stateFor(code);
if (!state || !player->getPlayerInfo()->getLocalOrJudge()) {
event->ignore();
return;
}
PlayerActions *actions = player->getPlayerActions();
if (event->button() == Qt::LeftButton && (event->modifiers() & Qt::ShiftModifier)) {
const QString label = code == QChar('N') ? tr("Colorless (N)") :
code == QChar('J') ? tr("Jasper (J)") : tr("Essence %1").arg(code);
bool accepted = false;
QWidget *parent = event->widget() ? event->widget()->window() : nullptr;
const int value = QInputDialog::getInt(parent, tr("Set Essence"),
tr("Available %1:").arg(label),
state->getValue(), 0, std::numeric_limits<int>::max(), 1, &accepted);
if (accepted) {
actions->actSetCounter(state->getId(), value);
}
} else if (event->button() == Qt::LeftButton) {
actions->actIncrementCounter(state->getId(), 1);
} else if (event->button() == Qt::RightButton) {
if (state->getValue() > 0) {
actions->actIncrementCounter(state->getId(), -1);
}
} else {
event->ignore();
return;
}
event->accept();
}
void EssencePoolItem::hoverMoveEvent(QGraphicsSceneHoverEvent *event)
{
const QChar code = codeAt(event->pos());
CounterState *state = stateFor(code);
if (!state) {
setToolTip(tr("Essence pool — all types start at zero"));
return;
}
const QString name = code == QChar('N') ? tr("Colorless Essence") :
code == QChar('J') ? tr("Jasper Essence") : tr("Essence %1").arg(code);
setToolTip(tr("%1: %2 available\nLeft +1 | Right -1 | Shift+Left: Set")
.arg(name).arg(state->getValue()));
}

View file

@ -0,0 +1,57 @@
#ifndef CONTINUUMATRICE_ESSENCE_POOL_ITEM_H
#define CONTINUUMATRICE_ESSENCE_POOL_ITEM_H
#include <QGraphicsObject>
#include <QHash>
#include <QPointer>
class CounterState;
class PlayerLogic;
class QGraphicsSceneMouseEvent;
class QGraphicsSceneHoverEvent;
class QPainter;
class QStyleOptionGraphicsItem;
class QWidget;
/**
* An interactive, sixteen-type Essence resource pool attached to a player
* battlefield. All values are read from the existing server-owned CounterState
* objects; this graphics item does NOT own mutable match resource balances.
*
* Left click +1, right click -1, Shift+left to set an exact nonnegative amount.
* Only the owning player or a game judge can modify the pool.
*/
class EssencePoolItem final : public QGraphicsObject
{
Q_OBJECT
public:
static constexpr int Width = 224;
static constexpr int Height = 258;
explicit EssencePoolItem(PlayerLogic *player, QGraphicsItem *parent = nullptr);
QRectF boundingRect() const override;
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
protected:
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
void hoverMoveEvent(QGraphicsSceneHoverEvent *event) override;
private:
static constexpr int Columns = 4;
static constexpr int Padding = 8;
static constexpr int CellWidth = 52;
static constexpr int CellHeight = 54;
static constexpr int HeaderHeight = 29;
[[nodiscard]] QChar codeAt(const QPointF &position) const;
[[nodiscard]] CounterState *stateFor(QChar code) const;
void attachCounter(CounterState *state);
void detachCounter(int id);
PlayerLogic *player;
QHash<QChar, QPointer<CounterState>> counters;
};
#endif // CONTINUUMATRICE_ESSENCE_POOL_ITEM_H

View file

@ -20,6 +20,7 @@
#include "libcockatrice/utility/card_ref.h"
#include "menu/player_menu.h"
#include "player_area.h"
#include "essence_pool_item.h"
#include "player_dialogs.h"
#include "player_target.h"
@ -40,6 +41,7 @@
#include <libcockatrice/game/player/player_actions.h>
#include <libcockatrice/game/player/player_info.h>
#include <libcockatrice/game/player/player_logic.h>
#include <libcockatrice/utility/essence_counter.h>
#include <libcockatrice/game/zones/card_zone_logic.h>
#include <libcockatrice/game/zones/hand_zone_logic.h>
#include <libcockatrice/game/zones/pile_zone_logic.h>
@ -51,7 +53,10 @@
namespace
{
constexpr qreal EXTRA_PILE_COLUMN_WIDTH = CardDimensions::HEIGHT_F + 24;
// Reserve a third, compact column for the synchronized 4x4 Essence pool.
// This keeps the 16 resource balances visible without obscuring the table.
constexpr qreal EXTRA_PILE_COLUMN_WIDTH =
CardDimensions::HEIGHT_F + 24 + EssencePoolItem::Width + 16;
} // namespace
PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player)
@ -103,6 +108,13 @@ PlayerGraphicsItem::PlayerGraphicsItem(PlayerLogic *_player) : player(_player)
initializeZones();
// Board-owned renderer only. All Essence balances live in player runtime
// CounterState and are sent/validated by the server.
essencePool = new EssencePoolItem(player, this);
const qreal essenceLeft = CardDimensions::HEIGHT_F + counterAreaWidth + 15 + 8 +
CardDimensions::HEIGHT_F + 14;
essencePool->setPos(essenceLeft, 14);
connect(player, &PlayerLogic::addViewCustomZoneActionToCustomZoneMenu, this,
&PlayerGraphicsItem::onCustomZoneAdded);
@ -310,6 +322,11 @@ void PlayerGraphicsItem::setMirrored(bool _mirrored)
void PlayerGraphicsItem::onCounterAdded(CounterState *state)
{
// The Essence pool owns its own compact 4x4 display. Never add sixteen
// full-size generic counters down the narrow life-counter column.
if (EssenceCounter::isEssenceCounter(state->getName())) {
return;
}
AbstractCounter *widget;
if (state->getName() == "life") {
widget = playerTarget->addCounter(state);

View file

@ -31,6 +31,7 @@ class TableZone;
class AbstractCounter;
class CardZone;
class CounterState;
class EssencePoolItem;
class PlayerArea;
class PlayerLogic;
struct CardRef;
@ -160,6 +161,7 @@ private:
PlayerDialogs *playerDialogs;
PlayerArea *playerArea;
PlayerTarget *playerTarget;
EssencePoolItem *essencePool = nullptr;
QMap<int, AbstractCounter *> counterWidgets;
QMap<QString, CardZone *> zoneGraphicsItems;
PileZone *deckZoneGraphicsItem;

View file

@ -251,9 +251,11 @@ void TableZone::paintBattlefieldRows(QPainter *painter)
*/
void TableZone::reloadBattlefieldAppearance()
{
frontZoneColor = QColor(142, 52, 61, 128);
supportZoneColor = QColor(40, 105, 129, 128);
commandZoneColor = QColor(94, 66, 140, 128);
// Canonical default battlefield palette. Theme battlefield.ini can override
// these with #RRGGBB (opaque) or #AARRGGBB (alpha) values.
frontZoneColor = QColor(QStringLiteral("#dc143c"));
supportZoneColor = QColor(QStringLiteral("#c3b091"));
commandZoneColor = QColor(QStringLiteral("#0a0e3f"));
const QString themePath = themeManager->getCurrentThemePath();
if (!themePath.isEmpty()) {

View file

@ -25,14 +25,14 @@ InstallerCloseServer::InstallerCloseServer(QObject *parent) : QObject(parent)
QString InstallerCloseServer::socketNameForUser(qint64 processId, const QString &userName)
{
return QStringLiteral("CockatriceInstallerClose-%1-%2").arg(processId).arg(userName);
return QStringLiteral("ContinuumatriceInstallerClose-%1-%2").arg(processId).arg(userName);
}
QString InstallerCloseServer::socketName(qint64 processId)
{
// The same scoping helper the single-instance socket uses, so the probe only has to
// reproduce one rule (USER, then USERNAME, then the home directory name).
return scopedLocalServerName(QStringLiteral("CockatriceInstallerClose-%1").arg(processId));
return scopedLocalServerName(QStringLiteral("ContinuumatriceInstallerClose-%1").arg(processId));
}
bool InstallerCloseServer::listen()

View file

@ -0,0 +1,55 @@
#ifndef CONTINUUMATRICE_ESSENCE_CHART_COLORS_H
#define CONTINUUMATRICE_ESSENCE_CHART_COLORS_H
#include <QColor>
#include <QList>
#include <QMap>
#include <QPair>
#include <QSet>
#include <QString>
#include <libcockatrice/card/essence_types.h>
// CHART-ONLY presentation palette, NOT canon Essence colors or affiliations.
// Each type must remain distinguishable without inventing lore. N is colorless.
namespace EssenceCharts
{
inline QColor colorFor(const QString &code)
{
if (code == QStringLiteral("N")) {
return QColor(145, 149, 156);
}
const int index = Essence::symbols().indexOf(code);
if (index < 0) {
// These charts are also reused for card types/categories. Retain
// distinct readable visualization colors without assigning Essence lore.
const QColor recognizedColor(code);
if (recognizedColor.isValid()) {
return recognizedColor;
}
return QColor::fromHsv(static_cast<int>(qHash(code) % 360u), 110, 195);
}
return QColor::fromHsv((index * 137) % 360, 125, 210);
}
inline QList<QPair<QString, int>> ordered(const QMap<QString, int> &values)
{
QList<QPair<QString, int>> result;
QSet<QString> used;
for (const QChar code : Essence::symbols()) {
const QString key(code);
auto it = values.constFind(key);
if (it != values.cend()) {
result.append({it.key(), it.value()});
used.insert(key);
}
}
for (auto it = values.cbegin(); it != values.cend(); ++it) {
if (!used.contains(it.key())) {
result.append({it.key(), it.value()});
}
}
return result;
}
} // namespace EssenceCharts
#endif // CONTINUUMATRICE_ESSENCE_CHART_COLORS_H

View file

@ -44,7 +44,7 @@ void IntentUrlParser::handle(const QString &urlStr)
{
QUrl url(urlStr);
if (url.scheme() != "cockatrice") {
if (url.scheme() != "continuumatrice") {
return;
}

View file

@ -36,7 +36,7 @@ struct PendingIntentChain
};
/**
* @brief Parses cockatrice:// links and runs them as serialized intent chains.
* @brief Parses continuumatrice:// links and runs them as serialized intent chains.
*
* Links are parsed by action (joingame/opendeck) and translated into an intent
* chain. Chains are queued and run one at a time: a document can hand multiple

View file

@ -488,7 +488,10 @@ const QPixmap &ManaSymbolPixmapGenerator::masterIcon(const QString &symbol)
return it.value();
}
QImageReader reader("theme:icons/mana/" + symbol);
// Essence uses its own artwork namespace, including codes shared with MTG.
// Theme overrides are supported through icons/essence/<symbol>.svg/png.
const QString asset = themeManager->assetPath(QStringLiteral("icons/essence/") + symbol);
QImageReader reader(QStringLiteral("theme:") + asset);
QSize sourceSize = reader.size();
if (!sourceSize.isEmpty()) {
sourceSize.scale(QSize(MASTER_ICON_SIZE, MASTER_ICON_SIZE), Qt::KeepAspectRatio);

View file

@ -294,7 +294,7 @@ QString ThemeManager::assetPath(QStringView prefix) const
// on Windows where UAC VirtualStore can make a system dir appear writable).
bool ThemeManager::isDirReallyWritable(const QString &dirPath)
{
const QString probe = QDir(dirPath).absoluteFilePath(".cockatrice_write_test");
const QString probe = QDir(dirPath).absoluteFilePath(".continuumatrice_write_test");
QFile f(probe);
if (!f.open(QIODevice::WriteOnly)) {
return false;
@ -326,7 +326,7 @@ static QString systemThemesBasePath()
#elif defined(Q_OS_WIN)
base += "/themes";
#else // linux
base += "/../share/cockatrice/themes";
base += "/../share/continuumatrice/themes";
#endif
return base;
}

View file

@ -2,6 +2,7 @@
#include "../../../../client/settings/cache_settings.h"
#include "libcockatrice/card/card_info.h"
#include <libcockatrice/card/essence_types.h>
#include "mana_symbol_widget.h"
#include <QChar>
@ -41,9 +42,9 @@ ColorIdentityWidget::ColorIdentityWidget(QWidget *parent, const CardInfoPtr &car
void ColorIdentityWidget::populateManaSymbolWidgets()
{
// Define the full WUBRG set (White, Blue, Black, Red, Green)
QString fullColorIdentity = "WUBRG";
QStringList symbols = parseColorIdentity(colorIdentity); // Parse mana cost string
// Continuum Universes supports fourteen named Essence codes plus N (colorless).
const QString &allEssences = Essence::symbols();
QStringList symbols = parseColorIdentity(colorIdentity);
// clear old layout
QtUtils::clearLayoutRec(layout);
@ -55,7 +56,8 @@ void ColorIdentityWidget::populateManaSymbolWidgets()
// populate mana symbols
if (SettingsCache::instance().visualDeckStorage().getVisualDeckStorageDrawUnusedColorIdentities()) {
for (const QString symbol : fullColorIdentity) {
for (const QChar code : allEssences) {
const QString symbol(code);
auto *manaSymbol = new ManaSymbolWidget(this, symbol, symbols.contains(symbol));
layout->addWidget(manaSymbol);
}
@ -120,28 +122,11 @@ void ColorIdentityWidget::resizeEvent(QResizeEvent *event)
}
}
QStringList ColorIdentityWidget::parseColorIdentity(const QString &manaString)
QStringList ColorIdentityWidget::parseColorIdentity(const QString &essenceIdentity)
{
QStringList symbols;
// Handle split costs (e.g., "3U // 4UU")
QStringList splitCosts = manaString.split(" // ");
for (const QString &part : splitCosts) {
QRegularExpression regex(R"(\{([^}]+)\}|(\d+)|([WUBRGCSPX]))");
QRegularExpressionMatchIterator matches = regex.globalMatch(part);
while (matches.hasNext()) {
QRegularExpressionMatch match = matches.next();
if (match.captured(1).isEmpty()) { // If no `{}` group was captured, check other groups
if (!match.captured(2).isEmpty()) {
symbols.append(match.captured(2)); // Number match
} else {
symbols.append(match.captured(3)); // Single mana letter match
}
} else {
symbols.append(match.captured(1)); // `{}` enclosed match
}
}
for (const QChar code : Essence::normalizeIdentity(essenceIdentity)) {
symbols.append(QString(code));
}
return symbols;
}

View file

@ -1,6 +1,7 @@
#include "deck_color_identity.h"
#include "libcockatrice/card/card_info.h"
#include <libcockatrice/card/essence_types.h>
#include <QChar>
#include <QList>
@ -17,7 +18,7 @@ QString getDeckColorIdentity(const DeckList &deck, const CardDatabaseQuerier *db
return {};
}
QSet<QChar> colorSet; // A set to collect unique color symbols (e.g., W, U, B, R, G)
QSet<QChar> colorSet; // Unique Essence symbols (including N for colorless).
for (const QString &cardName : cardList) {
CardInfoPtr currentCard = db->getCardInfo(cardName);
@ -29,10 +30,9 @@ QString getDeckColorIdentity(const DeckList &deck, const CardDatabaseQuerier *db
}
}
// Ensure the color identity is in WUBRG order
// Canonical Essence order is shared by the database and all widgets.
QString colorIdentity;
const QString wubrgOrder = "WUBRG";
for (const QChar &color : wubrgOrder) {
for (const QChar &color : Essence::symbols()) {
if (colorSet.contains(color)) {
colorIdentity.append(color);
}

View file

@ -7,8 +7,9 @@ class CardDatabaseQuerier;
class DeckList;
/**
* @brief Computes the color identity of a deck (e.g. "WUBRG") from the color
* symbols of all cards in the main deck and sideboard, ordered WUBRG.
* @brief Computes the Essence identity of a deck from the defined Essence
* symbols (M,S,O,G,E,A,Q,C,J,B,R,L,V,T,P,N) of cards in main/sideboard.
* N denotes colorless.
*
* Shared as a free function so the deck storage previews and the deck share
* dialog compute identities identically.

View file

@ -0,0 +1,157 @@
#include "essence_filter_picker.h"
#include "../../../pixel_map_generator.h"
#include <QFontMetrics>
#include <QGridLayout>
#include <QIcon>
#include <QLabel>
#include <QMenu>
#include <QPushButton>
#include <QSignalBlocker>
#include <QSize>
#include <QSizePolicy>
#include <QStringList>
#include <QVBoxLayout>
#include <QWidget>
#include <QWidgetAction>
#include <libcockatrice/card/essence_types.h>
EssenceFilterPicker::EssenceFilterPicker(QWidget *parent) : QToolButton(parent)
{
setPopupMode(QToolButton::InstantPopup);
setToolButtonStyle(Qt::ToolButtonTextOnly); // Keep the summary visible even without a button icon.
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
// Force a stable toolbar footprint independent of popup and font sizing.
// Modes and all 16 Essence icons are contained inside the popup.
setFixedWidth(qMax(156, fontMetrics().horizontalAdvance(tr("Essence: Any")) + 42));
popupMenu = new QMenu(this);
auto *panel = new QWidget(popupMenu);
panelLayout = new QVBoxLayout(panel);
panelLayout->setContentsMargins(10, 10, 10, 10);
panelLayout->setSpacing(7);
auto *heading = new QLabel(tr("Filter by Essence"), panel);
panelLayout->addWidget(heading);
auto *grid = new QGridLayout();
grid->setSpacing(5);
int index = 0;
for (const QChar essence : Essence::symbols()) {
const QString symbol(essence);
auto *button = new QToolButton(panel);
button->setCheckable(true);
button->setAutoRaise(false);
button->setFixedSize(42, 42);
button->setIcon(QIcon(ManaSymbolPixmapGenerator::generatePixmap(symbol, QSize(32, 32))));
button->setIconSize(QSize(30, 30));
if (essence == QChar('N')) {
button->setToolTip(tr("N — Colorless Essence"));
button->setAccessibleName(tr("Colorless Essence N"));
} else if (essence == QChar('J')) {
button->setToolTip(tr("J — Jasper Essence"));
button->setAccessibleName(tr("Jasper Essence J"));
} else {
button->setToolTip(tr("Essence %1").arg(symbol));
button->setAccessibleName(tr("Essence %1").arg(symbol));
}
button->setProperty("essenceCode", symbol);
connect(button, &QToolButton::toggled, this, [this, essence](bool checked) {
updateSummary();
emit essenceToggled(essence, checked);
});
// Sixteen Essences form a compact 4x4 grid.
grid->addWidget(button, index / 4, index % 4);
buttons.insert(essence, button);
++index;
}
panelLayout->addLayout(grid);
auto *clearButton = new QPushButton(tr("Clear Essence selection"), panel);
connect(clearButton, &QPushButton::clicked, this, [this]() {
// Clear as ONE model operation, so Exact Match and Include/Exclude
// don't retain stale "not this Essence" predicates.
for (const QChar essence : Essence::symbols()) {
if (auto *button = buttons.value(essence); button && button->isChecked()) {
const QSignalBlocker blocker(button);
button->setChecked(false);
}
}
updateSummary();
emit selectionCleared();
});
panelLayout->addWidget(clearButton);
auto *panelAction = new QWidgetAction(popupMenu);
panelAction->setDefaultWidget(panel);
popupMenu->addAction(panelAction);
setMenu(popupMenu);
retranslateUi();
}
void EssenceFilterPicker::addPopupControl(QWidget *control)
{
if (!control || !panelLayout) {
return;
}
// Insert above the Clear button, keeping the entire configuration in one
// popup rather than competing for horizontal space in the main toolbar.
panelLayout->insertWidget(panelLayout->count() - 1, control);
}
QSet<QChar> EssenceFilterPicker::activeEssences() const
{
QSet<QChar> active;
for (auto it = buttons.cbegin(); it != buttons.cend(); ++it) {
if (it.value()->isChecked()) {
active.insert(it.key());
}
}
return active;
}
void EssenceFilterPicker::setEssenceActive(QChar essence, bool active)
{
auto *button = buttons.value(essence);
if (!button || button->isChecked() == active) {
return;
}
// Restoring external filter state must not rewrite the filter model.
const QSignalBlocker blocker(button);
button->setChecked(active);
updateSummary();
}
void EssenceFilterPicker::retranslateUi()
{
updateSummary();
}
void EssenceFilterPicker::updateSummary()
{
QStringList selected;
const QSet<QChar> active = activeEssences();
for (const QChar essence : Essence::symbols()) {
if (active.contains(essence)) {
selected.append(QString(essence));
}
}
if (selected.isEmpty()) {
setText(tr("Essence: Any"));
setToolTip(tr("Filter by Essence (none selected)"));
} else if (selected.size() == 1) {
setText(tr("Essence: %1").arg(selected.first()));
setToolTip(tr("Selected Essence: %1").arg(selected.first()));
} else {
// Keep the toolbar label short; the tooltip lists the exact types.
setText(tr("Essence (%1)").arg(selected.size()));
setToolTip(tr("Selected Essence: %1").arg(selected.join(QStringLiteral(", "))));
}
}

View file

@ -0,0 +1,47 @@
#ifndef CONTINUUMATRICE_ESSENCE_FILTER_PICKER_H
#define CONTINUUMATRICE_ESSENCE_FILTER_PICKER_H
#include <QChar>
#include <QHash>
#include <QSet>
#include <QToolButton>
class QMenu;
class QVBoxLayout;
class QWidget;
/**
* Compact, reusable multi-select Essence picker for the deck and card database
* toolbars. The menu contains all fifteen Essence symbols in a 5x3 icon grid.
* Clicking a symbol does not close the popup; programmatic synchronization
* does not emit a user-selection signal.
*/
class EssenceFilterPicker final : public QToolButton
{
Q_OBJECT
public:
explicit EssenceFilterPicker(QWidget *parent = nullptr);
[[nodiscard]] QSet<QChar> activeEssences() const;
void setEssenceActive(QChar essence, bool active);
// Put the host's filtering mode selector inside this popup too:
// neither toolbar should require a separate cramped control.
void addPopupControl(QWidget *control);
void retranslateUi();
signals:
void essenceToggled(QChar essence, bool active);
// A clear request removes ALL Essence filters, including exclusions,
// rather than creating fifteen negative filters in Exact/Exclude mode.
void selectionCleared();
private:
void updateSummary();
QHash<QChar, QToolButton *> buttons;
QMenu *popupMenu = nullptr;
QVBoxLayout *panelLayout = nullptr;
};
#endif // CONTINUUMATRICE_ESSENCE_FILTER_PICKER_H

View file

@ -1,6 +1,7 @@
#include "mana_cost_widget.h"
#include "libcockatrice/card/card_info.h"
#include <libcockatrice/card/essence_types.h>
#include "mana_symbol_widget.h"
#include <QHBoxLayout>
@ -51,28 +52,7 @@ void ManaCostWidget::resizeEvent(QResizeEvent *event)
}
}
QStringList ManaCostWidget::parseManaCost(const QString &cmc)
QStringList ManaCostWidget::parseManaCost(const QString &essenceCost)
{
QStringList symbols;
// Handle split costs (e.g., "3U // 4UU")
QStringList splitCosts = cmc.split(" // ");
for (const QString &part : splitCosts) {
QRegularExpression regex(R"(\{([^}]+)\}|(\d+)|([WUBRGCSPX]))");
QRegularExpressionMatchIterator matches = regex.globalMatch(part);
while (matches.hasNext()) {
QRegularExpressionMatch match = matches.next();
if (match.captured(1).isEmpty()) { // If no `{}` group was captured, check other groups
if (!match.captured(2).isEmpty()) {
symbols.append(match.captured(2)); // Number match
} else {
symbols.append(match.captured(3)); // Single mana letter match
}
} else {
symbols.append(match.captured(1)); // `{}` enclosed match
}
}
}
return symbols;
return Essence::parseCost(essenceCost);
}

View file

@ -57,14 +57,14 @@ ManaBaseConfigDialog::ManaBaseConfigDialog(DeckListStatisticsAnalyzer *analyzer,
void ManaBaseConfigDialog::retranslateUi()
{
setWindowTitle(tr("Mana Base Configuration"));
setWindowTitle(tr("Essence Production Configuration"));
displayTypeLabel->setText(tr("Display type:"));
displayType->clear();
displayType->addItems({tr("pie"), tr("bar"), tr("combinedBar")});
filterLabel->setText(tr("Filter Colors (optional):"));
filterLabel->setText(tr("Filter Essence types (optional):"));
buttons->button(QDialogButtonBox::Ok)->setText(tr("OK"));
buttons->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));

View file

@ -1,5 +1,6 @@
#include "mana_base_widget.h"
#include "../../../../essence_chart_colors.h"
#include "../../../general/display/charts/bars/bar_widget.h"
#include "../../../general/display/charts/bars/color_bar.h"
#include "../../../general/display/charts/pies/color_pie.h"
@ -30,7 +31,7 @@ namespace
{
AnalyticsPanelWidgetRegistrar registerManaBase{
"manaBase", ManaBaseWidget::tr("Mana Base"),
"manaBase", ManaBaseWidget::tr("Essence Production"),
[](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaBaseWidget(parent, analyzer); }};
} // anonymous namespace
@ -83,16 +84,12 @@ void ManaBaseWidget::updateDisplay()
// Choose display mode
if (config.displayType == "bar") {
const QList<QPair<QString, int>> sortedColors = GameSpecificColors::MTG::sortManaMapWUBRGCFirst(mapSorted);
static const QHash<QString, QColor> colorMap = {
{"W", QColor(248, 231, 185)}, {"U", QColor(14, 104, 171)}, {"B", QColor(21, 11, 0)},
{"R", QColor(211, 32, 42)}, {"G", QColor(0, 115, 62)}, {"C", QColor(150, 150, 150)},
};
const QList<QPair<QString, int>> sortedColors = EssenceCharts::ordered(mapSorted);
for (const auto &[color, count] : sortedColors) {
QString label = QString("%1 %2 (%3)").arg(color).arg(count).arg(cardCount.value(color));
BarWidget *bar = new BarWidget(label, count, highest, colorMap.value(color, Qt::gray), this);
BarWidget *bar = new BarWidget(label, count, highest, EssenceCharts::colorFor(color), this);
barLayout->addWidget(bar);
}

View file

@ -1,7 +1,7 @@
#include "mana_curve_category_widget.h"
#include "../../../general/display/charts/bars/bar_chart_widget.h"
#include "libcockatrice/utility/color.h"
#include "../../../../essence_chart_colors.h"
#include "libcockatrice/utility/qt_utils.h"
#include "mana_curve_config.h"
@ -110,7 +110,7 @@ void ManaCurveCategoryWidget::updateDisplay(int minCmc,
QVector<BarSegment> segments;
if (val > 0) {
segments.push_back({cat, val, cards, GameSpecificColors::MTG::colorHelper(cat)});
segments.push_back({cat, val, cards, EssenceCharts::colorFor(cat)});
}
catBars.push_back({QString::number(cmc), segments});

View file

@ -64,7 +64,7 @@ void ManaCurveConfigDialog::retranslateUi()
{
labelGroupBy->setText(tr("Group By:"));
groupBy->setItemText(0, tr("type"));
groupBy->setItemText(1, tr("color"));
groupBy->setItemText(1, tr("Essence"));
groupBy->setItemText(2, tr("subtype"));
groupBy->setItemText(3, tr("power"));
groupBy->setItemText(4, tr("toughness"));

View file

@ -1,7 +1,7 @@
#include "mana_curve_total_widget.h"
#include "../../../general/display/charts/bars/bar_chart_widget.h"
#include "libcockatrice/utility/color.h"
#include "../../../../essence_chart_colors.h"
#include "mana_curve_config.h"
#include <QHBoxLayout>
@ -71,7 +71,7 @@ void ManaCurveTotalWidget::updateDisplay(const QString &categoryName,
cards = catIt->value(cmc);
}
segments.push_back({category, value, cards, GameSpecificColors::MTG::colorHelper(category)});
segments.push_back({category, value, cards, EssenceCharts::colorFor(category)});
}
}

View file

@ -23,7 +23,7 @@ namespace
{
AnalyticsPanelWidgetRegistrar registerManaCurve{
"manaCurve", ManaCurveWidget::tr("Mana Curve"),
"manaCurve", ManaCurveWidget::tr("Essence Curve"),
[](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaCurveWidget(parent, analyzer); }};
} // anonymous namespace

View file

@ -63,7 +63,7 @@ void ManaDevotionConfigDialog::retranslateUi()
displayType->setItemText(1, tr("bar"));
displayType->setItemText(2, tr("combinedBar"));
labelFilters->setText(tr("Filter Colors (optional):"));
labelFilters->setText(tr("Filter Essence types (optional):"));
}
void ManaDevotionConfigDialog::accept()

View file

@ -1,5 +1,6 @@
#include "mana_devotion_widget.h"
#include "../../../../essence_chart_colors.h"
#include "../../../general/display/charts/bars/bar_widget.h"
#include "../../../general/display/charts/bars/color_bar.h"
#include "../../../general/display/charts/pies/color_pie.h"
@ -29,7 +30,7 @@ namespace
{
AnalyticsPanelWidgetRegistrar registerManaDevotion{
"manaDevotion", ManaDevotionWidget::tr("Mana Devotion"),
"manaDevotion", ManaDevotionWidget::tr("Essence Cost Symbols"),
[](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaDevotionWidget(parent, analyzer); }};
} // anonymous namespace
@ -82,18 +83,13 @@ void ManaDevotionWidget::updateDisplay()
mapSorted.insert(QString(it.key()), it.value());
}
// Color map
QHash<QChar, QColor> colors = {{'W', QColor(248, 231, 185)}, {'U', QColor(14, 104, 171)},
{'B', QColor(21, 11, 0)}, {'R', QColor(211, 32, 42)},
{'G', QColor(0, 115, 62)}, {'C', QColor(150, 150, 150)}};
// Choose display mode
if (config.displayType == "bar") {
// One BarWidget per devotion color
for (auto c : devoMap.keys()) {
QString label = QString("%1 %2 (%3)").arg(c).arg(devoMap[c]).arg(cardCount.value(QString(c)));
BarWidget *bar = new BarWidget(label, devoMap[c], highest, colors.value(c, Qt::gray), this);
BarWidget *bar = new BarWidget(label, devoMap[c], highest, EssenceCharts::colorFor(QString(c)), this);
barLayout->addWidget(bar);
}

View file

@ -10,11 +10,18 @@
#include <QListWidgetItem>
#include <QString>
#include <QStringList>
#include <libcockatrice/card/essence_types.h>
#include <QVBoxLayout>
class QWidget;
static const QStringList kColors = {"W", "U", "B", "R", "G", "C"};
static const QStringList kEssences = [] {
QStringList codes;
for (const QChar symbol : Essence::symbols()) {
codes.append(QString(symbol));
}
return codes;
}();
ManaDistributionConfigDialog::ManaDistributionConfigDialog(DeckListStatisticsAnalyzer *analyzer, QWidget *parent)
: QDialog(parent), analyzer(analyzer)
@ -33,7 +40,7 @@ ManaDistributionConfigDialog::ManaDistributionConfigDialog(DeckListStatisticsAna
filterList = new QListWidget(this);
filterList->setSelectionMode(QAbstractItemView::MultiSelection);
filterList->addItems(kColors); // dynamic/fixed, no translation needed
filterList->addItems(kEssences); // dynamic/fixed, no translation needed
lay->addWidget(filterList);
showColorRows = new QCheckBox(this);
@ -56,9 +63,9 @@ void ManaDistributionConfigDialog::retranslateUi()
displayType->setItemText(0, tr("pie"));
displayType->setItemText(1, tr("bar"));
labelFilters->setText(tr("Colors:"));
labelFilters->setText(tr("Essence types:"));
showColorRows->setText(tr("Show per-color rows"));
showColorRows->setText(tr("Show per-Essence rows"));
// QDialogButtonBox buttons are automatically translated
}

View file

@ -48,5 +48,5 @@ void ManaDistributionSingleDisplayWidget::setDevotion(int pips, int cards, int p
void ManaDistributionSingleDisplayWidget::setProduction(int pips, int cards, int percent)
{
productionBar->setValue(percent);
productionLabel->setText(QString(tr("%1 mana (%2 cards)")).arg(pips).arg(cards));
productionLabel->setText(QString(tr("%1 Essence (%2 cards)")).arg(pips).arg(cards));
}

View file

@ -14,18 +14,25 @@
#include <QList>
#include <QSharedPointer>
#include <QStringList>
#include <libcockatrice/card/essence_types.h>
#include <QVBoxLayout>
#include <QWidget>
namespace
{
AnalyticsPanelWidgetRegistrar registerManaDistribution{
"manaProdDevotion", ManaDistributionWidget::tr("Mana Production + Devotion"),
"manaProdDevotion", ManaDistributionWidget::tr("Essence Production + Costs"),
[](QWidget *parent, DeckListStatisticsAnalyzer *analyzer) { return new ManaDistributionWidget(parent, analyzer); }};
} // anonymous namespace
static const QStringList kColors = {"W", "U", "B", "R", "G", "C"};
static const QStringList kEssences = [] {
QStringList codes;
for (const QChar symbol : Essence::symbols()) {
codes.append(QString(symbol));
}
return codes;
}();
ManaDistributionWidget::ManaDistributionWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer)
: AbstractAnalyticsPanelWidget(parent, analyzer)
@ -49,7 +56,7 @@ ManaDistributionWidget::ManaDistributionWidget(QWidget *parent, DeckListStatisti
row = new QHBoxLayout();
containerLayout->addLayout(row);
for (const QString &c : kColors) {
for (const QString &c : kEssences) {
auto *w = new ManaDistributionSingleDisplayWidget(c, this);
row->addWidget(w);
rows[c] = w;
@ -65,7 +72,7 @@ void ManaDistributionWidget::updateDisplay()
const auto &prodPips = analyzer->getProductionPipCount();
const auto &prodCards = analyzer->getProductionCardCount();
QStringList filtered = config.filters.isEmpty() ? kColors : config.filters;
QStringList filtered = config.filters.isEmpty() ? kEssences : config.filters;
QMap<QString, int> devMap, prodMap;
for (const QString &c : filtered) {
@ -89,7 +96,7 @@ void ManaDistributionWidget::updateDisplay()
productionBarTop->setColors(prodMap);
}
for (const QString &c : kColors) {
for (const QString &c : kEssences) {
auto *w = rows.value(c);
if (!w) {
@ -126,7 +133,7 @@ void ManaDistributionWidget::updateDisplay()
QDialog *ManaDistributionWidget::createConfigDialog(QWidget *parent)
{
auto *dlg = new ManaDistributionConfigDialog(analyzer, parent);
dlg->setWindowTitle(tr("Mana Distribution Settings"));
dlg->setWindowTitle(tr("Essence Distribution Settings"));
dlg->setFromConfig(config);
connect(dlg, &QDialog::accepted, [this, dlg]() {

View file

@ -2,6 +2,7 @@
#include "deck_list_model.h"
#include "libcockatrice/card/card_info.h"
#include <libcockatrice/card/essence_types.h>
#include "libcockatrice/card/database/card_database_querier.h"
#include "libcockatrice/deck_list/tree/deck_list_card_node.h"
#include "libcockatrice/deck_list/tree/inner_deck_list_node.h"
@ -127,68 +128,39 @@ void DeckListStatisticsAnalyzer::analyze()
QHash<QString, int> DeckListStatisticsAnalyzer::determineManaProduction(const QString &rulesText)
{
QHash<QString, int> manaCounts = {{"W", 0}, {"U", 0}, {"B", 0}, {"R", 0}, {"G", 0}, {"C", 0}};
QString text = rulesText.toLower(); // Normalize case for matching
// Quick keyword-based checks for any color and colorless mana
if (text.contains("{t}: add one mana of any color") || text.contains("add one mana of any color")) {
for (const auto &color : {QStringLiteral("W"), QStringLiteral("U"), QStringLiteral("B"), QStringLiteral("R"),
QStringLiteral("G")}) {
manaCounts[color]++;
}
}
if (text.contains("{t}: add {c}") || text.contains("add one colorless mana")) {
manaCounts["C"]++;
QHash<QString, int> result;
for (const QChar code : Essence::symbols()) {
result.insert(QString(code), 0);
}
// Optimized regex for specific mana symbols
static const QRegularExpression specificColorRegex(R"(\{T\}:\s*Add\s*\{([WUBRG])\})");
QRegularExpressionMatch match = specificColorRegex.match(rulesText);
if (match.hasMatch()) {
manaCounts[match.captured(1)]++;
}
// Explicit Essence grants only. Inferring additional resources from MTG
// tap/mana rules would invent Continuum Universes gameplay semantics.
// E.g. "Add {M}" / "add {N}" both work.
static const QRegularExpression grantedEssence(
QStringLiteral(R"(\badd\s*\{([MSOGEAQCJBRLVTPN])\})"),
QRegularExpression::CaseInsensitiveOption);
return manaCounts;
auto matches = grantedEssence.globalMatch(rulesText);
while (matches.hasNext()) {
const QString code = matches.next().captured(1).toUpper();
result[code]++;
}
return result;
}
std::unordered_map<char, int> DeckListStatisticsAnalyzer::countManaSymbols(const QString &manaString)
std::unordered_map<char, int> DeckListStatisticsAnalyzer::countManaSymbols(const QString &essenceCost)
{
std::unordered_map<char, int> manaCounts = {{'W', 0}, {'U', 0}, {'B', 0}, {'R', 0}, {'G', 0}};
int len = manaString.length();
for (int i = 0; i < len; ++i) {
if (manaString[i] == '{') {
++i; // Move past '{'
if (i < len && manaCounts.find(manaString[i].toLatin1()) != manaCounts.end()) {
char mana1 = manaString[i].toLatin1();
++i; // Move to next character
if (i < len && manaString[i] == '/') {
++i; // Move past '/'
if (i < len && manaCounts.find(manaString[i].toLatin1()) != manaCounts.end()) {
char mana2 = manaString[i].toLatin1();
manaCounts[mana1]++;
manaCounts[mana2]++;
} else {
// Handle cases like "{W/}" where second part is invalid
manaCounts[mana1]++;
}
} else {
manaCounts[mana1]++;
}
}
// Ensure we always skip to the closing '}'
while (i < len && manaString[i] != '}') {
++i;
}
}
// Check if the character is a standalone mana symbol (not inside {})
else if (manaCounts.find(manaString[i].toLatin1()) != manaCounts.end()) {
manaCounts[manaString[i].toLatin1()]++;
}
std::unordered_map<char, int> result;
for (const QChar code : Essence::symbols()) {
result.emplace(code.toLatin1(), 0);
}
return manaCounts;
for (const QString &token : Essence::parseCost(essenceCost)) {
if (token.size() == 1 && Essence::isSymbol(token.at(0))) {
result[token.at(0).toLatin1()]++;
}
}
return result;
}
// Hypergeometric probability: P(X=k)

View file

@ -152,7 +152,7 @@ private:
std::unordered_map<int, int> manaCurveMap;
std::unordered_map<char, int> manaDevotionMap;
QHash<QString, int> devotionPipCount; // W/U/B/R/G total symbols
QHash<QString, int> devotionPipCount; // Counts for the fifteen Continuum Universes Essence codes
QHash<QString, int> devotionCardCount; // how many cards provide devotion
QHash<QString, int> productionPipCount; // mana produced by cards

View file

@ -36,9 +36,9 @@
// Executable that, when it sits next to the downloaded update installer, is installed instead of
// it. A packager shipping a custom build - or someone testing one - can drop the file there and
// have Cockatrice run it rather than the official installer, without the release channel having to
// have Continuumatrice run it rather than the official installer, without the release channel having to
// host an installer for that build. It is a drop-in and is run with the same arguments.
static const QString UPDATE_INSTALLER_OVERRIDE = "Cockatrice-Update-Override.exe";
static const QString UPDATE_INSTALLER_OVERRIDE = "Continuumatrice-Update-Override.exe";
DlgUpdate::DlgUpdate(QWidget *parent) : QDialog(parent)
{
@ -87,7 +87,7 @@ DlgUpdate::DlgUpdate(QWidget *parent) : QDialog(parent)
if (!QSslSocket::supportsSsl()) {
enableUpdateButton(false);
QMessageBox::critical(this, tr("Error"),
tr("Cockatrice was not built with SSL support, therefore you cannot download updates "
tr("Continuumatrice was not built with SSL support, therefore you cannot download updates "
"automatically! \nPlease visit the download page to update manually."));
}
@ -161,7 +161,7 @@ void DlgUpdate::finishedUpdateCheck(bool needToUpdate, bool isCompatible, Releas
// downloader themselves if there's a compatible build
QMessageBox::information(
window(), tr("No Update Available"),
tr("Cockatrice is up to date!") + "<br><br>" +
tr("Continuumatrice is up to date!") + "<br><br>" +
tr("You are already running the latest version available in the chosen release channel.") + "<br>" +
"<b>" + tr("Current version") + QString(":</b> %1<br>").arg(VERSION_STRING) + "<b>" +
tr("Selected release channel") +
@ -171,7 +171,7 @@ void DlgUpdate::finishedUpdateCheck(bool needToUpdate, bool isCompatible, Releas
publishDate = release->getPublishDate().toString(QLocale().dateFormat(QLocale::LongFormat));
if (isCompatible) {
const QString message = tr("A new version of Cockatrice is available!") + "<br><br>" + "<b>" +
const QString message = tr("A new version of Continuumatrice is available!") + "<br><br>" + "<b>" +
tr("New version") + QString(":</b> %1<br>").arg(release->getName()) + "<b>" +
tr("Released") +
QString(":</b> %1 (<a href=\"%2\">").arg(publishDate, release->getDescriptionUrl()) +
@ -185,7 +185,7 @@ void DlgUpdate::finishedUpdateCheck(bool needToUpdate, bool isCompatible, Releas
} else {
QMessageBox::information(
window(), tr("Update Available"),
tr("A new version of Cockatrice is available!") + "<br><br>" + "<b>" + tr("New version") +
tr("A new version of Continuumatrice is available!") + "<br><br>" + "<b>" + tr("New version") +
QString(":</b> %1<br>").arg(release->getName()) + "<b>" + tr("Released") +
QString(":</b> %1 (<a href=\"%2\">").arg(publishDate, release->getDescriptionUrl()) + tr("Changelog") +
"</a>)<br><br>" +
@ -235,7 +235,7 @@ void DlgUpdate::warnInstallerIsWaiting()
// would therefore swallow the installer's one and only request for the rest of its wait.
auto *notification = new QMessageBox(QMessageBox::Warning, tr("Update"),
tr("The update installer is already running and waits about a "
"minute for Cockatrice to close. Cockatrice is still busy, so "
"minute for Continuumatrice to close. Continuumatrice is still busy, so "
"save your work and close it before then. Otherwise the "
"installer gives up and the update is cancelled."),
QMessageBox::Ok, parentWidget());
@ -293,9 +293,9 @@ void DlgUpdate::downloadSuccessful(const QUrl &filepath)
process.setArguments({"/R", QString("/D=%1").arg(appDir)});
#endif
// Try to open the installer. If it opens, quit Cockatrice
// Try to open the installer. If it opens, quit Continuumatrice
if (process.startDetached()) {
qCInfo(DlgUpdateLog) << "Opened update installer successfully - closing Cockatrice";
qCInfo(DlgUpdateLog) << "Opened update installer successfully - closing Continuumatrice";
// Close the main window synchronously so file locks are released before the NSIS installer
// (already launched) starts replacing files. This also flushes settings and shuts down the
// tabs, but only when the close is actually accepted: MainWindow may veto it for a running
@ -324,8 +324,8 @@ void DlgUpdate::downloadSuccessful(const QUrl &filepath)
} else {
setLabel(tr("Error"));
QMessageBox::critical(this, tr("Update Error"),
tr("Cockatrice is unable to open the installer.") + "<br><br>" +
tr("Try to update manually by closing Cockatrice and running the installer.") +
tr("Continuumatrice is unable to open the installer.") + "<br><br>" +
tr("Try to update manually by closing Continuumatrice and running the installer.") +
"<br>" + tr("Download location") + QString(": %1").arg(filepath.toLocalFile()));
}
}

View file

@ -1,6 +1,6 @@
#include "color_bar.h"
#include "libcockatrice/utility/color.h"
#include "../../../../../essence_chart_colors.h"
#include <QBrush>
#include <QLinearGradient>
@ -19,14 +19,14 @@
#include <utility>
ColorBar::ColorBar(const QMap<QString, int> &_colors, QWidget *parent)
: QWidget(parent), colors(GameSpecificColors::MTG::sortManaMapWUBRGCFirst(_colors))
: QWidget(parent), colors(EssenceCharts::ordered(_colors))
{
setMouseTracking(true);
}
void ColorBar::setColors(const QMap<QString, int> &_colors)
{
colors = GameSpecificColors::MTG::sortManaMapWUBRGCFirst(_colors);
colors = EssenceCharts::ordered(_colors);
update();
}
@ -150,19 +150,5 @@ QString ColorBar::tooltipForPosition(int x) const
QColor ColorBar::colorFromName(const QString &name) const
{
static QMap<QString, QColor> map = {
{"R", QColor(220, 30, 30)}, {"G", QColor(40, 170, 40)}, {"U", QColor(40, 90, 200)},
{"W", QColor(235, 235, 230)}, {"B", QColor(30, 30, 30)},
};
if (map.contains(name)) {
return map[name];
}
QColor c(name);
if (!c.isValid()) {
c = Qt::gray;
}
return c;
}
return EssenceCharts::colorFor(name);
}

View file

@ -1,6 +1,6 @@
#include "color_pie.h"
#include "libcockatrice/utility/color.h"
#include "../../../../../essence_chart_colors.h"
#include <QBrush>
#include <QFontMetrics>
@ -20,14 +20,14 @@
#include <utility>
ColorPie::ColorPie(const QMap<QString, int> &_colors, QWidget *parent)
: QWidget(parent), colors(GameSpecificColors::MTG::sortManaMapWUBRGCFirst(_colors))
: QWidget(parent), colors(EssenceCharts::ordered(_colors))
{
setMouseTracking(true);
}
void ColorPie::setColors(const QMap<QString, int> &_colors)
{
colors = GameSpecificColors::MTG::sortManaMapWUBRGCFirst(_colors);
colors = EssenceCharts::ordered(_colors);
update();
}
@ -184,19 +184,5 @@ QString ColorPie::tooltipForPoint(const QPoint &pt) const
QColor ColorPie::colorFromName(const QString &name) const
{
static QMap<QString, QColor> map = {
{"R", QColor(220, 30, 30)}, {"G", QColor(40, 170, 40)}, {"U", QColor(40, 90, 200)},
{"W", QColor(235, 235, 230)}, {"B", QColor(30, 30, 30)},
};
if (map.contains(name)) {
return map[name];
}
QColor c(name);
if (!c.isValid()) {
c = Qt::gray;
}
return c;
}
return EssenceCharts::colorFor(name);
}

View file

@ -213,7 +213,7 @@ void FirstRunWizard::changeEvent(QEvent *event)
void FirstRunWizard::retranslateUi()
{
setWindowTitle(tr("Welcome to Cockatrice"));
setWindowTitle(tr("Welcome to Continuumatrice"));
backButton->setText(tr("Back"));
skipButton->setText(tr("Skip"));
for (FirstRunWizardPage *page : std::as_const(pages)) {

View file

@ -1511,7 +1511,7 @@ void TabSupervisor::processNotifyUserEvent(const Event_NotifyUser &event)
QMessageBox::information(
this, tr("Unknown Event"),
tr("The server has sent you a message that your client does not understand.\nThis message might mean "
"there is a new version of Cockatrice available or this server is running a custom or pre-release "
"there is a new version of Continuumatrice available or this server is running a custom or pre-release "
"version.\n\nTo update your client, go to Help -> Check for Updates."));
break;
case Event_NotifyUser::IDLEWARNING:

View file

@ -1,48 +1,44 @@
#include "visual_database_display_color_filter_widget.h"
#include "../cards/additional_info/mana_symbol_widget.h"
#include "../cards/additional_info/essence_filter_picker.h"
#include "filter_tree_model.h"
#include "libcockatrice/filters/filter_card.h"
#include <QAbstractItemModel>
#include <QComboBox>
#include <QDebug>
#include <QHBoxLayout>
#include <QPersistentModelIndex>
#include <QSharedPointer>
#include <QSizePolicy>
#include <QString>
#include <QVariant>
#include <QtGlobal>
#include <libcockatrice/card/essence_types.h>
#include <libcockatrice/filters/filter_tree.h>
/**
* This widget provides a graphical control element for the CardFilter::Attr::AttrColor filters applied to the filter
* model.
* @param parent The Qt Widget that this widget will parent to
* @param _filterModel The filter model that this widget will manipulate
*/
VisualDatabaseDisplayColorFilterWidget::VisualDatabaseDisplayColorFilterWidget(QWidget *parent,
FilterTreeModel *_filterModel)
: QWidget(parent), filterModel(_filterModel), layout(new QHBoxLayout(this))
{
setLayout(layout);
layout->setSpacing(5);
layout->setSpacing(3);
layout->setContentsMargins(0, 0, 0, 0);
QString fullColorIdentity = "WUBRG";
for (const QChar &color : fullColorIdentity) {
auto *manaSymbol = new ManaSymbolWidget(this, color, false, true);
manaSymbol->setFixedWidth(25);
// Share the same compact Essence selection UI as Visual Deck Storage.
// A 15-icon horizontal row cannot fit the database filter toolbar.
essencePicker = new EssenceFilterPicker(this);
layout->addWidget(essencePicker);
// FlowWidget measures THIS parent widget, not its nested button. Give it
// an explicit size hint that cannot clip the picker.
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
setMinimumWidth(essencePicker->width());
layout->addWidget(manaSymbol);
connect(essencePicker, &EssenceFilterPicker::essenceToggled, this,
&VisualDatabaseDisplayColorFilterWidget::handleColorToggled);
// Connect the color toggled signal
connect(manaSymbol, &ManaSymbolWidget::colorToggled, this,
&VisualDatabaseDisplayColorFilterWidget::handleColorToggled);
}
connect(essencePicker, &EssenceFilterPicker::selectionCleared, this, [this]() {
filterModel->clearFiltersOfType(CardFilter::Attr::AttrColor);
});
modeComboBox = new QComboBox(this);
layout->addWidget(modeComboBox);
essencePicker->addPopupControl(modeComboBox);
connect(modeComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
&VisualDatabaseDisplayColorFilterWidget::updateFilterMode);
@ -50,108 +46,83 @@ VisualDatabaseDisplayColorFilterWidget::VisualDatabaseDisplayColorFilterWidget(Q
connect(filterModel, &FilterTreeModel::layoutChanged, this,
[this]() { syncDebouncer.call([this] { syncWithFilterModel(); }); });
// Call retranslateUi to set the initial text
retranslateUi();
syncWithFilterModel();
}
void VisualDatabaseDisplayColorFilterWidget::retranslateUi()
{
essencePicker->retranslateUi();
modeComboBox->blockSignals(true);
modeComboBox->clear();
modeComboBox->addItem(tr("Exact match"), QVariant::fromValue(FilterMode::ExactMatch));
modeComboBox->addItem(tr("Includes"), QVariant::fromValue(FilterMode::Includes));
modeComboBox->addItem(tr("Include / Exclude"), QVariant::fromValue(FilterMode::IncludeExclude));
modeComboBox->setToolTip(tr("How selected and unselected Essence types are combined in the filter"));
modeComboBox->setToolTip(tr("How selected and unselected colors are combined in the filter"));
// Restore current mode
const int index = modeComboBox->findData(QVariant::fromValue(currentMode));
if (index >= 0) {
modeComboBox->setCurrentIndex(index);
}
modeComboBox->blockSignals(false);
}
void VisualDatabaseDisplayColorFilterWidget::handleColorToggled(QChar color, bool active)
void VisualDatabaseDisplayColorFilterWidget::handleColorToggled(QChar essence, bool active)
{
if (active) {
addFilter(color);
addFilter(essence);
} else {
removeFilter(color);
removeFilter(essence);
}
}
void VisualDatabaseDisplayColorFilterWidget::addFilter(QChar color)
void VisualDatabaseDisplayColorFilterWidget::addFilter(QChar essence)
{
QString colorString = color;
QString typeStr;
// Remove previous filters
QList<const CardFilter *> allColorFilters = filterModel->getFiltersOfType(CardFilter::Attr::AttrColor);
QList<const CardFilter *> matchingFilters;
for (const CardFilter *filter : allColorFilters) {
if (filter->term() == color) {
matchingFilters.append(filter);
QString essenceString(essence);
const QList<const CardFilter *> filters = filterModel->getFiltersOfType(CardFilter::Attr::AttrColor);
for (const CardFilter *filter : filters) {
if (filter->term() == essenceString) {
filterModel->removeFilter(filter);
}
}
for (const CardFilter *filter : matchingFilters) {
filterModel->removeFilter(filter);
}
// Add actual filter
switch (currentMode) {
case FilterMode::ExactMatch:
filterModel->addFilter(new CardFilter(colorString, CardFilter::Type::TypeAnd, CardFilter::Attr::AttrColor));
filterModel->addFilter(
new CardFilter(essenceString, CardFilter::Type::TypeAnd, CardFilter::Attr::AttrColor));
break;
case FilterMode::Includes:
filterModel->addFilter(new CardFilter(colorString, CardFilter::Type::TypeOr, CardFilter::Attr::AttrColor));
break;
case FilterMode::IncludeExclude:
filterModel->addFilter(new CardFilter(colorString, CardFilter::Type::TypeOr, CardFilter::Attr::AttrColor));
filterModel->addFilter(
new CardFilter(essenceString, CardFilter::Type::TypeOr, CardFilter::Attr::AttrColor));
break;
}
}
void VisualDatabaseDisplayColorFilterWidget::removeFilter(QChar color)
void VisualDatabaseDisplayColorFilterWidget::removeFilter(QChar essence)
{
QString colorString = color;
// Remove inclusion filters
QList<const CardFilter *> allColorFilters = filterModel->getFiltersOfType(CardFilter::Attr::AttrColor);
QList<const CardFilter *> matchingFilters;
for (const CardFilter *filter : allColorFilters) {
if (filter->term() == color) {
matchingFilters.append(filter);
QString essenceString(essence);
const QList<const CardFilter *> filters = filterModel->getFiltersOfType(CardFilter::Attr::AttrColor);
for (const CardFilter *filter : filters) {
if (filter->term() == essenceString) {
filterModel->removeFilter(filter);
}
}
for (const CardFilter *filter : matchingFilters) {
filterModel->removeFilter(filter);
// The empty selection means "Any Essence", not "exclude everything".
if (essencePicker->activeEssences().isEmpty()) {
filterModel->clearFiltersOfType(CardFilter::Attr::AttrColor);
return;
}
// Add exclusion filters if the mode demands it
switch (currentMode) {
case FilterMode::ExactMatch:
filterModel->addFilter(
new CardFilter(colorString, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColor));
break;
case FilterMode::IncludeExclude:
filterModel->addFilter(
new CardFilter(colorString, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColor));
new CardFilter(essenceString, CardFilter::Type::TypeAndNot, CardFilter::Attr::AttrColor));
break;
case FilterMode::Includes:
// No exclusion in Includes mode
break;
}
}
@ -170,10 +141,12 @@ void VisualDatabaseDisplayColorFilterWidget::updateFilterMode()
filterModel->clearFiltersOfType(CardFilter::Attr::AttrColor);
const QList<ManaSymbolWidget *> manaSymbolWidgets = findChildren<ManaSymbolWidget *>();
for (ManaSymbolWidget *manaSymbolWidget : manaSymbolWidgets) {
handleColorToggled(manaSymbolWidget->getSymbolChar(), manaSymbolWidget->isColorActive());
const QSet<QChar> selected = essencePicker->activeEssences();
// No selections should never generate 15 mutually excluding filters.
if (!selected.isEmpty()) {
for (const QChar essence : Essence::symbols()) {
handleColorToggled(essence, selected.contains(essence));
}
}
filterModel->blockSignals(false);
@ -184,79 +157,44 @@ void VisualDatabaseDisplayColorFilterWidget::updateFilterMode()
emit filterModeChanged(currentMode);
}
void VisualDatabaseDisplayColorFilterWidget::setManaSymbolActive(QChar color, bool active)
void VisualDatabaseDisplayColorFilterWidget::setEssenceActive(QChar essence, bool active)
{
QList<ManaSymbolWidget *> manaSymbolWidgets = findChildren<ManaSymbolWidget *>();
for (ManaSymbolWidget *manaSymbolWidget : manaSymbolWidgets) {
if (manaSymbolWidget->getSymbolChar() == color) {
manaSymbolWidget->setColorActive(active);
}
}
essencePicker->setEssenceActive(essence, active);
}
QList<QChar> VisualDatabaseDisplayColorFilterWidget::getActiveColors()
{
QList<QChar> activeColors;
QList<ManaSymbolWidget *> manaSymbolWidgets = findChildren<ManaSymbolWidget *>();
for (ManaSymbolWidget *manaSymbolWidget : manaSymbolWidgets) {
if (manaSymbolWidget->isColorActive()) {
activeColors.append(manaSymbolWidget->getSymbolChar());
QList<QChar> result;
const QSet<QChar> selected = essencePicker->activeEssences();
for (const QChar essence : Essence::symbols()) {
if (selected.contains(essence)) {
result.append(essence);
}
}
return activeColors;
return result;
}
void VisualDatabaseDisplayColorFilterWidget::syncWithFilterModel()
{
QList<const CardFilter *> allColorFilters = filterModel->getFiltersOfType(CardFilter::Attr::AttrColor);
const QList<const CardFilter *> filters = filterModel->getFiltersOfType(CardFilter::Attr::AttrColor);
QList<ManaSymbolWidget *> manaSymbolWidgets = findChildren<ManaSymbolWidget *>();
for (ManaSymbolWidget *manaSymbolWidget : manaSymbolWidgets) {
bool found = false;
for (const CardFilter *filter : allColorFilters) {
if (manaSymbolWidget->getSymbolChar() == filter->term()) {
switch (currentMode) {
case FilterMode::ExactMatch:
switch (filter->type()) {
case CardFilter::Type::TypeAnd:
setManaSymbolActive(filter->term().at(0), true);
break;
default:
setManaSymbolActive(filter->term().at(0), false);
break;
}
break;
case FilterMode::Includes:
switch (filter->type()) {
case CardFilter::Type::TypeOr:
setManaSymbolActive(filter->term().at(0), true);
break;
default:
setManaSymbolActive(filter->term().at(0), false);
break;
}
break;
case FilterMode::IncludeExclude:
switch (filter->type()) {
case CardFilter::Type::TypeOr:
setManaSymbolActive(filter->term().at(0), true);
break;
default:
setManaSymbolActive(filter->term().at(0), false);
break;
}
break;
}
found = true;
for (const QChar essence : Essence::symbols()) {
bool selected = false;
for (const CardFilter *filter : filters) {
if (filter->term() != QString(essence)) {
continue;
}
if (currentMode == FilterMode::ExactMatch) {
selected = filter->type() == CardFilter::Type::TypeAnd;
} else {
selected = filter->type() == CardFilter::Type::TypeOr;
}
if (selected) {
break;
}
}
if (!found) {
setManaSymbolActive(manaSymbolWidget->getSymbolChar(), false);
}
// Signals are blocked inside the picker; loading saved filters must
// not mutate the FilterTreeModel again.
setEssenceActive(essence, selected);
}
}

View file

@ -18,6 +18,7 @@
class FilterTreeModel;
class QComboBox;
class QHBoxLayout;
class EssenceFilterPicker;
class VisualDatabaseDisplayColorFilterCircleWidget : public QWidget
{
@ -63,7 +64,7 @@ private slots:
void addFilter(QChar color);
void removeFilter(QChar color);
void updateFilterMode();
void setManaSymbolActive(QChar color, bool active);
void setEssenceActive(QChar essence, bool active);
QList<QChar> getActiveColors();
void syncWithFilterModel();
@ -71,6 +72,7 @@ private:
FilterTreeModel *filterModel;
QHBoxLayout *layout;
QComboBox *modeComboBox;
EssenceFilterPicker *essencePicker = nullptr;
FilterMode currentMode = FilterMode::Includes; // Default mode
QtUtils::Debouncer syncDebouncer{100};
};

View file

@ -1,12 +1,12 @@
#include "deck_preview_color_identity_filter_widget.h"
#include "../../cards/additional_info/mana_symbol_widget.h"
#include "../../cards/additional_info/essence_filter_picker.h"
#include "../visual_deck_storage_sort_filter_proxy_model.h"
#include <QHBoxLayout>
#include <QPushButton>
#include <QSizePolicy>
#include <QSet>
#include <QSharedPointer>
#include <QString>
DeckPreviewColorIdentityFilterWidget::DeckPreviewColorIdentityFilterWidget(QWidget *parent)
@ -16,34 +16,30 @@ DeckPreviewColorIdentityFilterWidget::DeckPreviewColorIdentityFilterWidget(QWidg
layout->setSpacing(5);
layout->setContentsMargins(0, 0, 0, 0);
QString fullColorIdentity = "WUBRG";
for (const QChar &color : fullColorIdentity) {
auto *manaSymbol = new ManaSymbolWidget(this, color, false, true);
manaSymbol->setFixedWidth(25);
// Fifteen Essence codes don't fit as inline buttons beside sorting and
// search. The popup keeps all codes selectable without expanding the row.
essencePicker = new EssenceFilterPicker(this);
layout->addWidget(essencePicker);
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
setMinimumWidth(essencePicker->width());
layout->addWidget(manaSymbol);
// Initialize the activeColors map
activeColors[color] = false;
// Connect the color toggled signal
connect(manaSymbol, &ManaSymbolWidget::colorToggled, this,
&DeckPreviewColorIdentityFilterWidget::handleColorToggled);
}
connect(essencePicker, &EssenceFilterPicker::essenceToggled, this,
[this](QChar /*essence*/, bool /*active*/) { emit activeColorsChanged(); });
connect(essencePicker, &EssenceFilterPicker::selectionCleared, this,
[this]() { emit activeColorsChanged(); });
toggleButton = new QPushButton(this);
layout->addWidget(toggleButton);
// Connect the button's clicked signal
essencePicker->addPopupControl(toggleButton);
connect(toggleButton, &QPushButton::clicked, this, &DeckPreviewColorIdentityFilterWidget::updateFilterMode);
// Call retranslateUi to set the initial text
retranslateUi();
}
void DeckPreviewColorIdentityFilterWidget::retranslateUi()
{
// Set the toggle button text based on the current mode
essencePicker->retranslateUi();
// Preserve existing Exact / Includes / Excludes filter semantics.
switch (filterMode) {
case VisualDeckStorageSortFilterProxyModel::FilterMode::ExactMatch:
toggleButton->setText(tr("Mode: Exact Match"));
@ -55,32 +51,21 @@ void DeckPreviewColorIdentityFilterWidget::retranslateUi()
toggleButton->setText(tr("Mode: Excludes"));
break;
}
toggleButton->setToolTip(tr("Color identity filter mode (AND/OR/NOT conjunctions of filters)"));
toggleButton->setToolTip(tr("Essence filter mode (exact match, includes, or excludes)"));
}
/**
* @brief The colors that are currently toggled on.
*/
QSet<QChar> DeckPreviewColorIdentityFilterWidget::getActiveColors() const
{
QSet<QChar> activeColorSet;
for (auto it = activeColors.constBegin(); it != activeColors.constEnd(); ++it) {
if (it.value()) {
activeColorSet.insert(it.key());
}
}
return activeColorSet;
return essencePicker->activeEssences();
}
void DeckPreviewColorIdentityFilterWidget::handleColorToggled(QChar color, bool active)
void DeckPreviewColorIdentityFilterWidget::handleColorToggled(QChar /*essence*/, bool /*active*/)
{
activeColors[color] = active;
emit activeColorsChanged();
}
void DeckPreviewColorIdentityFilterWidget::updateFilterMode()
{
// Cycle through the modes
switch (filterMode) {
case VisualDeckStorageSortFilterProxyModel::FilterMode::ExactMatch:
filterMode = VisualDeckStorageSortFilterProxyModel::FilterMode::Includes;
@ -93,6 +78,6 @@ void DeckPreviewColorIdentityFilterWidget::updateFilterMode()
break;
}
retranslateUi(); // Update the button text
retranslateUi();
emit filterModeChanged(filterMode);
}

View file

@ -9,13 +9,13 @@
#include "../visual_deck_storage_sort_filter_proxy_model.h"
#include <QChar>
#include <QMap>
#include <QSet>
#include <QWidget>
#include <qtmetamacros.h>
class QHBoxLayout;
class QPushButton;
class EssenceFilterPicker;
class DeckPreviewColorIdentityFilterWidget : public QWidget
{
@ -57,7 +57,7 @@ private slots:
private:
QHBoxLayout *layout;
QPushButton *toggleButton;
QMap<QChar, bool> activeColors;
EssenceFilterPicker *essencePicker = nullptr;
VisualDeckStorageSortFilterProxyModel::FilterMode filterMode = VisualDeckStorageSortFilterProxyModel::Includes;
};

View file

@ -120,17 +120,17 @@
class LocalServerInterface;
#define GITHUB_PAGES_URL "https://cockatrice.github.io"
#define GITHUB_PAGES_URL "https://github.com/Continuum-Universes-Studio/Continuumatrice"
#define GITHUB_CONTRIBUTORS_URL "https://github.com/Cockatrice/Cockatrice/graphs/contributors"
#define GITHUB_CONTRIBUTE_URL "https://github.com/Cockatrice/Cockatrice#"
#define GITHUB_TRANSIFEX_TRANSLATORS_URL "https://github.com/Cockatrice/Cockatrice/wiki/Translator-Hall-of-Fame"
#define GITHUB_TRANSLATOR_FAQ_URL "https://github.com/Cockatrice/Cockatrice/wiki/Translation-FAQ"
#define GITHUB_ISSUES_URL "https://github.com/Cockatrice/Cockatrice/issues"
#define GITHUB_ISSUES_URL "https://github.com/Continuum-Universes-Studio/Continuumatrice/issues"
#define GITHUB_TROUBLESHOOTING_URL "https://github.com/Cockatrice/Cockatrice/wiki/Troubleshooting"
#define GITHUB_FAQ_URL "https://github.com/Cockatrice/Cockatrice/wiki/Frequently-Asked-Questions"
const QString MainWindow::appName = "Cockatrice";
const QStringList MainWindow::fileNameFilters = QStringList() << QObject::tr("Cockatrice card database (*.xml)")
const QString MainWindow::appName = "Continuumatrice";
const QStringList MainWindow::fileNameFilters = QStringList() << QObject::tr("Continuumatrice card database (*.xml)")
<< QObject::tr("All files (*.*)");
inline Q_LOGGING_CATEGORY(MainWindowLog, "main_window");
@ -231,7 +231,7 @@ void MainWindow::actWatchReplay()
{
QFileDialog dlg(this, tr("Load replay"));
dlg.setDirectory(SettingsCache::instance().paths().getReplaysPath());
dlg.setNameFilters(QStringList() << QObject::tr("Cockatrice replays (*.cor)"));
dlg.setNameFilters(QStringList() << QObject::tr("Continuumatrice replays (*.cor)"));
if (!dlg.exec()) {
return;
}
@ -292,27 +292,24 @@ void MainWindow::actExit()
void MainWindow::actAbout()
{
QMessageBox mb(
QMessageBox::NoIcon, tr("About Cockatrice"),
QString("<font size=\"8\"><b>Cockatrice</b></font> (" + QString::fromStdString(BUILD_ARCHITECTURE) + ")<br>" +
tr("Version") + QString(" %1").arg(VERSION_STRING) + "<br><br><b><a href='" + GITHUB_PAGES_URL + "'>" +
tr("Cockatrice Webpage") + "</a></b><br>" + "<br><b>" + tr("Project Manager:") +
"</b><br>Zach Halpern<br><br>" + "<b>" + tr("Past Project Managers:") +
"</b><br>Gavin Bisesi<br>Max-Wilhelm Bruker<br>Marcus Schütz<br><br>" + "<b>" + tr("Developers:") +
"</b><br>" + "<a href='" + GITHUB_CONTRIBUTORS_URL + "'>" + tr("Our Developers") + "</a><br>" +
"<a href='" + GITHUB_CONTRIBUTE_URL + "'>" + tr("Help Develop!") + "</a><br><br>" + "<b>" +
tr("Translators:") + "</b><br>" + "<a href='" + GITHUB_TRANSIFEX_TRANSLATORS_URL + "'>" +
tr("Our Translators") + "</a><br>" + "<a href='" + GITHUB_TRANSLATOR_FAQ_URL + "'>" +
tr("Help Translate!") + "</a><br><br>" + "<b>" + tr("Support:") + "</b><br>" + "<a href='" +
GITHUB_ISSUES_URL + "'>" + tr("Report an Issue") + "</a><br>" + "<a href='" +
GITHUB_TROUBLESHOOTING_URL + "'>" + tr("Troubleshooting") + "</a><br>" + "<a href='" + GITHUB_FAQ_URL +
"'>" + tr("F.A.Q.") + "</a><br>"),
QMessageBox::NoIcon, tr("About Continuumatrice"),
QStringLiteral("<font size=\"8\"><b>Continuumatrice</b></font><br>") +
tr("Continuum Universes Studio") + QStringLiteral(" — ") + tr("Version") +
QStringLiteral(" ") + QString(VERSION_STRING) +
QStringLiteral("<br><br>") +
tr("Continuumatrice is a standalone Continuum Universes card-game client.") +
QStringLiteral("<br><a href='https://github.com/Continuum-Universes-Studio/Continuumatrice'>") +
tr("Source code and issues") + QStringLiteral("</a><br><br>") +
tr("Based on the open-source Cockatrice project. The original developers and contributors "
"retain credit for their work, and the applicable open-source license remains in force.") +
QStringLiteral("<br><a href='https://github.com/Cockatrice/Cockatrice'>") +
tr("Upstream Cockatrice project") + QStringLiteral("</a>"),
QMessageBox::Ok, this);
mb.setIconPixmap(
themePixmap(QStringLiteral("cockatrice")).scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation));
themePixmap(QStringLiteral("continuumatrice")).scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation));
mb.setTextInteractionFlags(Qt::TextBrowserInteraction);
mb.exec();
}
void MainWindow::actTips()
{
if (tip != nullptr) {
@ -369,7 +366,7 @@ void MainWindow::retranslateUi()
#if defined(__APPLE__) /* For OSX */
cockatriceMenu->setTitle(tr("A&ctions"));
#else
cockatriceMenu->setTitle(tr("&Cockatrice"));
cockatriceMenu->setTitle(tr("&Continuumatrice"));
#endif
dbMenu->setTitle(tr("C&ard Database"));
@ -383,11 +380,13 @@ void MainWindow::retranslateUi()
tabsMenu->setTitle(tr("Tabs"));
helpMenu->setTitle(tr("&Help"));
aAbout->setText(tr("&About Cockatrice"));
aAbout->setText(tr("&About Continuumatrice"));
aTips->setText(tr("&Tip of the Day"));
aUpdate->setText(tr("Check for Client Updates"));
aCheckCardUpdates->setText(tr("Check for Card Updates..."));
aCheckCardUpdatesBackground->setText(tr("Check for Card Updates (Automatic)"));
aCheckCardUpdates->setText(tr("Legacy MTG Oracle importer (disabled)"));
aCheckCardUpdates->setEnabled(false);
aCheckCardUpdatesBackground->setText(tr("Automatic MTG card updates (disabled)"));
aCheckCardUpdatesBackground->setEnabled(false);
aStatusBar->setText(tr("Show Status Bar"));
aViewLog->setText(tr("View &Debug Log"));
aOpenSettingsFolder->setText(tr("Open Settings Folder"));
@ -651,20 +650,23 @@ void MainWindow::startupConfigCheck()
startLocalGame(options);
}
if (SettingsCache::instance().updates().getCheckUpdatesOnStartup()) {
actCheckClientUpdates();
}
actCheckCommanderBracketDefinitionUpdates();
// The fork has independent release channels. Until a Continuumatrice
// release is published, do not auto-check or run upstream Cockatrice updates.
SettingsCache::instance().updates().setCheckUpdatesOnStartup(false);
// No MTG Commander-bracket network requests in Continuumatrice.
if (isCleanInstall) {
// no config found, 99% new clean install
qCInfo(WindowMainStartupVersionLog)
<< "Startup: old client version empty, assuming first start after clean install";
SettingsCache::instance().downloads().resetToDefaultURLs(); // populate the download urls
SettingsCache::instance().downloads().resetToDefaultURLs(); // Empty in Continuumatrice: no MTG providers
SettingsCache::instance().network().setClientVersion(VERSION_STRING);
actCheckServerUpdates();
runFirstRunWizard(true);
// No upstream Cockatrice first-run wizard, Magic importer, or default
// server discovery. Cards are loaded from this application's own XML.
QMessageBox::information(this, tr("Welcome to Continuumatrice"),
tr("Continuumatrice uses its own card database. "
"Add your card XML files using Card Database > Add custom sets/cards "
"or place them in the Continuumatrice customsets directory."));
if (QString(VERSION_STRING).contains("custom", Qt::CaseInsensitive)) {
SettingsCache::instance().updates().setCheckUpdatesOnStartup(false);
@ -677,7 +679,7 @@ void MainWindow::startupConfigCheck()
<< "Startup: old client version" << SettingsCache::instance().network().getClientVersion()
<< "differs, assuming first start after update";
if (SettingsCache::instance().updates().getNotifyAboutNewVersion()) {
alertForcedOracleRun(VERSION_STRING, true);
// Do not automatically launch the upstream Magic-card Oracle importer.
}
qCInfo(WindowMainStartupShortcutsLog) << "Migrating shortcuts after update detected.";
@ -704,35 +706,8 @@ void MainWindow::startupConfigCheck()
// previous config from this version found
qCInfo(WindowMainStartupVersionLog) << "Startup: found config with current version";
if (SettingsCache::instance().updates().getCardUpdateCheckRequired()) {
if (SettingsCache::instance().updates().getStartupCardUpdateCheckPromptForUpdate()) {
auto startupCardCheckDialog = new DlgStartupCardCheck(this);
// No automatic upstream card database downloads in Continuumatrice.
if (startupCardCheckDialog->exec() == QDialog::Accepted) {
switch (startupCardCheckDialog->group->checkedId()) {
case 0: // foreground
actCheckCardUpdates();
break;
case 1: // background
actCheckCardUpdatesBackground();
break;
case 2: // background + always
SettingsCache::instance().updates().setStartupCardUpdateCheckPromptForUpdate(false);
SettingsCache::instance().updates().setStartupCardUpdateCheckAlwaysUpdate(true);
actCheckCardUpdatesBackground();
break;
case 3: // don't prompt again + don't run
SettingsCache::instance().updates().setStartupCardUpdateCheckPromptForUpdate(false);
SettingsCache::instance().updates().setStartupCardUpdateCheckAlwaysUpdate(false);
break;
default:
break;
}
}
} else if (SettingsCache::instance().updates().getStartupCardUpdateCheckAlwaysUpdate()) {
actCheckCardUpdatesBackground();
}
}
// Run the tips dialog only on subsequent startups.
// On the first run after an install/update the startup is already crowded enough
@ -860,12 +835,12 @@ void MainWindow::alertForcedOracleRun(const QString &version, bool isUpdate)
{
if (isUpdate) {
QMessageBox::information(this, tr("New Version"),
tr("Congratulations on updating to Cockatrice %1!\n"
tr("Congratulations on updating to Continuumatrice %1!\n"
"Oracle will now launch to update your card database.")
.arg(version));
} else {
QMessageBox::information(this, tr("Cockatrice installed"),
tr("Congratulations on installing Cockatrice %1!\n"
QMessageBox::information(this, tr("Continuumatrice installed"),
tr("Congratulations on installing Continuumatrice %1!\n"
"Oracle will now launch to install the initial card database.")
.arg(version));
}
@ -1110,7 +1085,7 @@ void MainWindow::cardDatabaseLoadingFailed()
QMessageBox msgBox;
msgBox.setWindowTitle(tr("Card database"));
msgBox.setIcon(QMessageBox::Question);
msgBox.setText(tr("Cockatrice is unable to load the card database.\n"
msgBox.setText(tr("Continuumatrice is unable to load the card database.\n"
"Do you want to update your card database now?\n"
"If unsure or first time user, choose \"Yes\""));
@ -1193,7 +1168,7 @@ void MainWindow::cardDatabaseAllNewSetsEnabled()
}
QMessageBox::information(
this, tr("Welcome"),
tr("Hi! It seems like you're running this version of Cockatrice for the first time.\nAll the sets in the "
tr("This is the first time you've loaded this card database in Continuumatrice.\nAll the sets in the "
"card "
"database have been enabled.\nRead more about changing the set order or disabling specific sets and "
"consequent effects in the \"Manage Sets\" dialog."));
@ -1207,16 +1182,28 @@ void MainWindow::cardDatabaseAllNewSetsEnabled()
/* CARD UPDATER */
void MainWindow::actCheckCardUpdates()
{
createCardUpdateProcess();
QMessageBox::information(this, tr("Continuumatrice card data"),
tr("The upstream Oracle importer downloads Magic card data and is disabled "
"in Continuumatrice. Import Continuum Universes XML cards using "
"Card Database > Add custom sets/cards instead."));
}
void MainWindow::actCheckCardUpdatesBackground()
{
createCardUpdateProcess(true);
// Never run the upstream Oracle MTG importer unattended.
qCInfo(CardDatabaseUpdateLog) << "The legacy Magic Oracle importer is disabled in Continuumatrice";
}
void MainWindow::createCardUpdateProcess(bool background)
{
Q_UNUSED(background);
// Direct callers must also be unable to launch the upstream MTG importer.
QMessageBox::information(this, tr("Importer unavailable"),
tr("Continuumatrice does not run the upstream Magic Oracle importer. "
"Import Continuum Universes XML sets directly."));
return;
// Legacy importer implementation retained for upstream reference only.
if (cardUpdateProcess) {
qCDebug(CardDatabaseUpdateLog) << "A card database update is already running; asking it to come to the front";
if (requestCardUpdateRaise()) {
@ -1309,7 +1296,7 @@ void MainWindow::createCardUpdateProcess(bool background)
statusBar()->showMessage(tr("Card database update running."));
if (trayIcon && SettingsCache::instance().updates().getCardDatabaseUpdateTrayNotification()) {
trayIcon->showMessage(tr("Card database update started"),
tr("Cockatrice is updating the card database in the background."));
tr("Continuumatrice is updating the card database in the background."));
}
}
}
@ -1451,7 +1438,7 @@ void MainWindow::cardUpdateFinished(int exitCode, QProcess::ExitStatus exitStatu
bool MainWindow::requestCardUpdateRaise()
{
const QString serverName = scopedLocalServerName(QStringLiteral("CockatriceOracleRaise"));
const QString serverName = scopedLocalServerName(QStringLiteral("ContinuumatriceOracleRaise"));
QLocalSocket socket;
socket.connectToServer(serverName);
if (!socket.waitForConnected(250)) {
@ -1469,7 +1456,7 @@ void MainWindow::announceCardUpdateRunning()
statusBar()->showMessage(tr("Card database update running."));
} else if (trayIcon && SettingsCache::instance().updates().getCardDatabaseUpdateTrayNotification()) {
trayIcon->showMessage(tr("Card database update running"),
tr("Cockatrice is updating the card database in the background."));
tr("Continuumatrice is updating the card database in the background."));
}
}
@ -1485,9 +1472,8 @@ void MainWindow::notifyCardUpdateFinishedFromTray(bool success)
void MainWindow::actCheckServerUpdates()
{
auto hps = new HandlePublicServers(this);
hps->downloadPublicServers();
connect(hps, &HandlePublicServers::sigPublicServersDownloadedSuccessfully, [=]() { hps->deleteLater(); });
// Upstream Cockatrice public servers are not Continuumatrice game servers.
qCInfo(MainWindowLog) << "Upstream public server discovery disabled in Continuumatrice.";
}
void MainWindow::actCheckClientUpdates()
@ -1507,12 +1493,8 @@ 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();
// Commander brackets are a Magic: The Gathering system, not Continuum Universes.
qCInfo(MainWindowLog) << "Upstream Commander bracket updates disabled in Continuumatrice.";
}
void MainWindow::refreshShortcuts()

View file

@ -224,11 +224,11 @@ public:
explicit MainWindow(QWidget *parent = nullptr);
void setConnectTo(QString url)
{
connectTo = QUrl(QString("cockatrice://%1").arg(url));
connectTo = QUrl(QString("continuumatrice://%1").arg(url));
}
// When set, the window's own startup connection (--connect or auto-connect
// on first activation) is skipped. Used for activation launches: the intent
// chain triggered by a cockatrice:// URL owns the connection, and letting
// chain triggered by a continuumatrice:// URL owns the connection, and letting
// auto-connect race against it caused two connectToServer calls to tear
// each other down. onUrlChainFinished() clears this and retries the startup
// connection when the link's chain ended without connecting.

View file

@ -69,6 +69,7 @@
#include <libcockatrice/card/database/card_database_manager.h>
#include <libcockatrice/rng/rng_sfmt.h>
#include <libcockatrice/settings/card_database_settings.h>
#include <libcockatrice/settings/download_settings.h>
#include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/settings/network_settings.h>
#include <libcockatrice/settings/personal_settings.h>
@ -84,7 +85,7 @@ SoundEngine *soundEngine;
QSystemTrayIcon *trayIcon;
ThemeManager *themeManager;
const QString translationPrefix = "cockatrice";
const QString translationPrefix = "cockatrice"; // Keep inherited translation catalogue until retranslated.
QString translationPath;
static void CockatriceLogger(QtMsgType type, const QMessageLogContext &ctx, const QString &message)
@ -137,9 +138,9 @@ QString const generateClientID()
static QString redactActivationUrl(const QString &url)
{
// Activation URLs carry secrets in their query string (e.g. the deck share
// token); log only the scheme and the action (cockatrice://opendeck), never
// token); log only the scheme and the action (continuumatrice://opendeck), never
// the parameters.
if (!url.startsWith(QStringLiteral("cockatrice://"))) {
if (!url.startsWith(QStringLiteral("continuumatrice://"))) {
return url;
}
const QUrl parsed(url);
@ -219,7 +220,7 @@ int main(int argc, char *argv[])
QApplication app(argc, argv);
#ifdef Q_OS_MAC
UrlSchemeEventFilter cockatriceFilter(QStringList{QStringLiteral("cockatrice")});
UrlSchemeEventFilter cockatriceFilter(QStringList{QStringLiteral("continuumatrice")});
QStringList pendingMacUrls;
@ -257,7 +258,7 @@ int main(int argc, char *argv[])
// Declared before crash-reporter init: the startup attach below needs the log tail while the
// file still holds the crashed session's lines.
const QString persistentLogPath =
SettingsCache::instance().getSettingsPath() + QStringLiteral("logs/cockatrice.log");
SettingsCache::instance().getSettingsPath() + QStringLiteral("logs/continuumatrice.log");
Logger::getInstance().setPersistentLogPath(persistentLogPath);
// Crash reporting: init the SDK early so a crash anywhere below is captured into the
@ -333,7 +334,7 @@ int main(int argc, char *argv[])
#elif defined(Q_OS_WIN)
translationPath = qApp->applicationDirPath() + "/translations";
#else // linux
translationPath = qApp->applicationDirPath() + "/../share/cockatrice/translations";
translationPath = qApp->applicationDirPath() + "/../share/continuumatrice/translations";
#endif
// Command-line parser
@ -416,6 +417,24 @@ int main(int argc, char *argv[])
CardDatabaseManager::setCardDatabasePathProvider(&SettingsCache::instance());
CardDatabaseManager::setCardSetPriorityController(&SettingsCache::instance().cardDatabase());
// Existing early fork profiles may still contain the upstream Scryfall /
// Gatherer defaults saved by older builds. Migrate only known MTG provider
// URLs, leaving user-authored Continuum Universes endpoints untouched.
auto &downloadSettings = SettingsCache::instance().downloads();
const QStringList savedImageUrls = downloadSettings.getAllURLs();
QStringList compatibleImageUrls;
for (const QString &url : savedImageUrls) {
if (!url.contains(QStringLiteral("scryfall.com"), Qt::CaseInsensitive) &&
!url.contains(QStringLiteral("scryfall.io"), Qt::CaseInsensitive) &&
!url.contains(QStringLiteral("gatherer.wizards.com"), Qt::CaseInsensitive)) {
compatibleImageUrls.append(url);
}
}
if (compatibleImageUrls != savedImageUrls) {
downloadSettings.setDownloadUrls(compatibleImageUrls);
qCInfo(MainLog) << "Removed inherited MTG image sources from Continuumatrice profile.";
}
qCInfo(MainLog) << "Starting main program";
// Front-load the card database before constructing the main window. The
@ -448,7 +467,7 @@ int main(int argc, char *argv[])
// auto-connect must not race against it (two connectToServer calls tear
// each other down via doDisconnectFromServer).
bool hasUrlActivation = std::any_of(startupFiles.begin(), startupFiles.end(), [](const QString &file) {
return file.startsWith(QStringLiteral("cockatrice://"));
return file.startsWith(QStringLiteral("continuumatrice://"));
});
#ifdef Q_OS_MAC
// On macOS the launch can arrive through the URL scheme instead of as a
@ -456,12 +475,12 @@ int main(int argc, char *argv[])
// window would auto-connect into the link's own connection attempt.
hasUrlActivation = hasUrlActivation ||
std::any_of(pendingMacUrls.cbegin(), pendingMacUrls.cend(),
[](const QString &url) { return url.startsWith(QStringLiteral("cockatrice://")); });
[](const QString &url) { return url.startsWith(QStringLiteral("continuumatrice://")); });
#endif
ui.setSkipStartupAutoConnect(hasUrlActivation);
auto handleActivation = [&ui](const QString &file) {
if (file.startsWith("cockatrice://")) {
if (file.startsWith("continuumatrice://")) {
qCInfo(MainLog) << "Handling URL activation:" << redactActivationUrl(file);
// Route through the window's persistent url parser: it serializes
// link chains so activations handed over while another chain is
@ -487,12 +506,12 @@ int main(int argc, char *argv[])
}
qCInfo(MainLog) << "MainWindow constructor finished";
ui.setWindowIcon(themePixmap(QStringLiteral("cockatrice")));
ui.setWindowIcon(themePixmap(QStringLiteral("continuumatrice")));
// Base name of the desktop file installed next to the binary, without the ".desktop"
// extension. Wayland reads the window icon from it, and xdg-desktop-portal looks up
// org.freedesktop.host.portal.Registry by this app ID, so it has to match the file name
// exactly, down to the case.
QGuiApplication::setDesktopFileName("cockatrice");
QGuiApplication::setDesktopFileName("continuumatrice");
SettingsCache::instance().network().setClientID(generateClientID());